go.mod | 1 + go.sum | 2 ++ main.go | 156 +++++++++++++++++++++++++++++++++++++---------------- diff --git a/go.mod b/go.mod index 4da372e196dfe343a5b4166c927e369896318857fbc03e3d98b1e13ea1bbb708..36284d87244623883c67d80795d325f11c1788f0948971a9a5d9f8ea823774ea 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/go-git/go-git/v6 v6.0.0-alpha.1 + lukechampine.com/blake3 v1.4.1 ) require ( diff --git a/go.sum b/go.sum index 5de4f4253158e8bc5d607b8c27abc69ecc695bfda4082809746b5cfbcad727bc..a08b2de1cd5b5b19a535621cec1c4c8122b1e7177731188bc661a2a4d7dc1cdc 100644 --- a/go.sum +++ b/go.sum @@ -64,3 +64,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/main.go b/main.go index ec619cb111cfe70237de7f27e7f2ec2ea7da09b5c5361d5d6f12b9ec68418782..88ae862df9f329ef56d6a4acd08826e630d72f8d7b26a0dabccc1d45dcba71cc 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "context" + "encoding/base64" "fmt" "html" "io" @@ -18,21 +20,26 @@ "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/filemode" "github.com/go-git/go-git/v6/plumbing/object" + "lukechampine.com/blake3" ) const ( + Version = "sgit 0.2.0" LogMax = 5 TextPlain = "Content-Type: text/plain; charset=utf-8" ErrGeneral = "general error" + TimeFmt = time.DateTime + " Z07:00" ) var ( - BaseDir string - Dirs []string - CloneURLs []string - P, B, H, F string - Repo *git.Repository - C *object.Commit + BaseDir string + Dirs []string + CloneURLs []string + P, B, H, F string + Repo *git.Repository + C *object.Commit + IfNoneMatch = os.Getenv("HTTP_IF_NONE_MATCH") + ETag = blake3.New(32, nil) ) func makeErr(err string, status int) { @@ -41,19 +48,30 @@ fmt.Print(TextPlain + "\n\n") fmt.Println(err) } -func htmlStart(title string) { +func ETagGet() string { + return `"` + base64.RawURLEncoding.EncodeToString(ETag.Sum(nil)) + `"` +} + +func htmlStart(title string) bool { + etag := ETagGet() + if IfNoneMatch == etag { + fmt.Printf("Status: 304\nETag: %s\n\n", IfNoneMatch) + return false + } fmt.Printf(`Status: 200 +ETag: %s Content-Type: text/html; charset=utf-8 - + %s -`, title) +`, etag, Version, title) + return true } func htmlStop() { @@ -61,10 +79,10 @@ fmt.Print("\n") } func doList() { - htmlStart("Git repositories") - fmt.Print(` - -`) + ETag.Write([]byte("list")) + var names []string + var descrs []string + var whens []time.Time for _, name := range Dirs { descr, err := os.ReadFile(path.Join(BaseDir, name, "description")) if err != nil { @@ -84,31 +102,46 @@ if err != nil { r.Close() continue } - fmt.Printf(` + names = append(names, name) + descrs = append(descrs, string(descr)) + whens = append(whens, c.Committer.When) + ETag.Write([]byte(name)) + ETag.Write(descr) + ETag.Write([]byte(c.Committer.When.String())) + } + if htmlStart("Git repositories") { + fmt.Print(`
repodescriptionidle
+ +`) + for i, name := range names { + fmt.Printf(` -`, name, name, string(descr), humanize.Time(c.Committer.When)) +`, name, name, descrs[i], whens[i].Format(TimeFmt)) + } + fmt.Print("
repodescriptionlast
%s %s %s
") + htmlStop() } - fmt.Print("") - htmlStop() } func doSum() { + ETag.Write([]byte("sum")) descr, err := os.ReadFile(path.Join(BaseDir, P, "description")) if err != nil { return } - htmlStart(P + " summary") - fmt.Print(string(descr) + "
\n
\n")
+	var buf bytes.Buffer
+	w := io.MultiWriter(&buf, ETag)
+	fmt.Fprint(w, string(descr)+"
\n
\n")
 	for _, u := range CloneURLs {
-		fmt.Println(u + P)
+		fmt.Fprintln(w, u+P)
 	}
-	fmt.Print(`
+ fmt.Fprint(w, `

-`) +`) var c *object.Commit var name string { @@ -123,18 +156,18 @@ if err != nil { return nil } name = strings.TrimPrefix(ref.Name().String(), "refs/heads/") - fmt.Printf(` + fmt.Fprintf(w, ` -`, P, name, name, humanize.Time(c.Author.When), html.EscapeString(c.Author.Name)) +`, P, name, name, c.Author.When.Format(TimeFmt), html.EscapeString(c.Author.Name)) return nil }) } - fmt.Print(`
branchidleauthor
branchwhenauthor
%s %s %s
+ fmt.Fprint(w, ` -`) +`) { tags, err := Repo.Tags() if err != nil { @@ -146,22 +179,20 @@ tags.ForEach(func(ref *plumbing.Reference) error { t, err = Repo.TagObject(ref.Hash()) switch err { case nil: - fmt.Printf(` + fmt.Fprintf(w, ` - `, P, t.Name, t.Name, - t.Tagger.When.Format(time.DateTime+" Z07:00"), - humanize.Time(t.Tagger.When), + t.Tagger.When.Format(TimeFmt), html.EscapeString(t.Tagger.Name)) case plumbing.ErrObjectNotFound: c, err = Repo.CommitObject(ref.Hash()) if err == nil { name = strings.TrimPrefix(ref.Name().String(), "refs/tags/") - fmt.Printf(` + fmt.Fprintf(w, ` @@ -172,14 +203,19 @@ } return nil }) } - fmt.Print("
tagdateageauthor
tagwhenauthor
%s%s %s %s
%s (light) %s %s
\n") - htmlStop() + if htmlStart(P + " summary") { + io.Copy(os.Stdout, &buf) + fmt.Print("\n") + htmlStop() + } } func doLog() { + ETag.Write([]byte("log")) if B == "" { B = "HEAD" } + ETag.Write([]byte(B)) h, err := Repo.ResolveRevision(plumbing.Revision(B)) if err != nil { makeErr("bad b", http.StatusNotFound) @@ -190,33 +226,38 @@ if err != nil { makeErr(ErrGeneral, http.StatusInternalServerError) return } - htmlStart(P + " log") + var buf bytes.Buffer + w := io.MultiWriter(&buf, ETag) var c *object.Commit for range LogMax { c, err = ci.Next() if err != nil { break } - fmt.Printf(`
commit %s [browse]
+		fmt.Fprintf(w, `
commit %s [browse]
 Author: %s
 Date: %s
 
 %s

`, - P, B, h.String(), h.String(), - P, B, h.String(), + P, B, c.Hash.String(), c.Hash.String(), + P, B, c.Hash.String(), html.EscapeString(c.Author.Name), - c.Author.When.Format(time.DateTime+" Z07:00"), + c.Author.When.Format(TimeFmt), html.EscapeString(c.Message)) } if _, err = ci.Next(); err == nil { - fmt.Println("clone the repository to get more history") + fmt.Fprintln(w, "clone the repository to get more history") } - htmlStop() + if htmlStart(P + " log") { + io.Copy(os.Stdout, &buf) + htmlStop() + } } func doShow() { + ETag.Write([]byte("show")) fromTree, err := C.Tree() if err != nil { makeErr(ErrGeneral, http.StatusInternalServerError) @@ -241,13 +282,20 @@ if err != nil { makeErr(ErrGeneral, http.StatusInternalServerError) return } + etag := ETagGet() + if IfNoneMatch == etag { + fmt.Printf("Status: 304\nETag: %s\n\n", IfNoneMatch) + return + } fmt.Println("Status:", http.StatusOK) + fmt.Println("ETag:", etag) fmt.Print(TextPlain + "\n\n") fmt.Println(patch.Stats()) fmt.Print(patch.String()) } func doTree() { + ETag.Write([]byte("tree")) tree, err := C.Tree() if err != nil { makeErr(ErrGeneral, http.StatusInternalServerError) @@ -268,32 +316,42 @@ break } } } - htmlStart(P + html.EscapeString(F)) + var buf bytes.Buffer + w := io.MultiWriter(&buf, ETag) var isDir string for _, e := range tree.Entries { isDir = "" switch e.Mode { case filemode.Submodule: - fmt.Printf(`%s (submodule)
`, html.EscapeString(e.Name)) + fmt.Fprintf(w, `%s (submodule)
`, html.EscapeString(e.Name)) case filemode.Dir: isDir = "/" fallthrough case filemode.Regular, filemode.Deprecated, filemode.Executable, filemode.Symlink: - fmt.Printf(`%s%s`, + fmt.Fprintf(w, `%s%s`, P, B, H, F, e.Name, isDir, html.EscapeString(e.Name), isDir) if e.Mode == filemode.Symlink { fmt.Print(" (symlink)") } - fmt.Print("
\n") + fmt.Fprint(w, "
\n") default: - fmt.Printf(`%s (bad type)
`, html.EscapeString(e.Name)) + fmt.Fprintf(w, `%s (bad type)
`, html.EscapeString(e.Name)) } - } - htmlStop() + if htmlStart(P + html.EscapeString(F)) { + io.Copy(os.Stdout, &buf) + htmlStop() + } } func doBlob() { + ETag.Write([]byte("blob")) + ETag.Write([]byte(F)) + etag := ETagGet() + if IfNoneMatch == etag { + fmt.Printf("Status: 304\nETag: %s\n\n", IfNoneMatch) + return + } file, err := C.File(strings.TrimPrefix(F, "/")) if err != nil { makeErr("bad f", http.StatusNotFound) @@ -310,6 +368,7 @@ makeErr(ErrGeneral, http.StatusInternalServerError) return } fmt.Println("Status:", http.StatusOK) + fmt.Println("ETag:", etag) fmt.Println("Content-Length:", file.Blob.Size) if isBin { fmt.Print("Content-Type: application/octet-stream\n\n") @@ -327,6 +386,7 @@ if err != nil { makeErr(err.Error(), http.StatusBadRequest) return } + ETag.Write([]byte(Version)) { var d *os.File d, err = os.Open(BaseDir) @@ -347,6 +407,7 @@ if P == "" { doList() return } + ETag.Write([]byte(P)) for _, p := range Dirs { if P == p { goto RepoFound @@ -388,6 +449,7 @@ if !ok { makeErr("bad h", http.StatusBadRequest) return } + ETag.Write([]byte(H)) C, err = Repo.CommitObject(hsh) if err != nil { makeErr("bad commit", http.StatusNotFound)