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(`
-| repo | description | idle |
-`)
+ 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(`
+| repo | description | last |
+`)
+ for i, name := range names {
+ fmt.Printf(`
| %s |
%s |
%s |
-`, name, name, string(descr), humanize.Time(c.Committer.When))
+`, name, name, descrs[i], whens[i].Format(TimeFmt))
+ }
+ fmt.Print("
")
+ 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, `
-| branch | idle | author |
`)
+| branch | when | author |
`)
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, `
| %s |
%s |
%s |
-`, 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(`
+ fmt.Fprint(w, `
-| tag | date | age | author |
`)
+| tag | when | author |
`)
{
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, `
| %s |
-%s |
%s |
%s |
`,
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, `
| %s (light) |
%s |
%s |
@@ -172,14 +203,19 @@ }
return nil
})
}
- fmt.Print("
\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)