Author | Jakob Wakeling <[email protected]> |
Date | 2023-08-06 11:29:48 |
Commit | 3e159d3dd66b838ce7d8089fe9b44c936df8f973 |
Parent | 1f376a942c45404d3fdf74160d09a12b61a23a79 |
Implement commit viewing
Diffstat
M | go.mod | | | 1 | + |
M | go.sum | | | 2 | ++ |
M | main.go | | | 2 | +- |
A | res/repo/commit.html | | | 28 | ++++++++++++++++++++++++++++ |
M | res/repo/log.html | | | 4 | ++-- |
M | res/res.go | | | 3 | +++ |
M | res/style.css | | | 32 | ++++++++++++++++++++++++++++++++ |
M | src/git.go | | | 30 | +++++++++++++++--------------- |
M | src/http.go | | | 1 | + |
A | src/repo/commit.go | | | 125 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
10 files changed, 210 insertions, 18 deletions
diff --git a/go.mod b/go.mod index cc85f1e..9c41378 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/Microsoft/go-winio v0.5.2 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/buildkite/terminal-to-html/v3 v3.9.1 github.com/cloudflare/circl v1.3.3 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect diff --git a/go.sum b/go.sum index faeb509..b26649a 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/buildkite/terminal-to-html/v3 v3.9.1 h1:8SOCKFK9ntpYvPE3yUAXHiZYdQI4xf9o9S3wOX7x12A= +github.com/buildkite/terminal-to-html/v3 v3.9.1/go.mod h1:Nsx19oOIo6MZM/cEPookXi/nrQQmnSJFLZL1KS05t+A= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= diff --git a/main.go b/main.go index 9aea17f..6767f94 100644 --- a/main.go +++ b/main.go @@ -40,7 +40,7 @@ func main() { h.Path("/{repo:.+(?:\\.git)$}").Methods("GET").HandlerFunc(redirectDotGit) h.Path("/{repo}").Methods("GET").HandlerFunc(repo.HandleLog) h.Path("/{repo}/log").Methods("GET").HandlerFunc(repo.HandleLog) - h.Path("/{repo}/log/{path:.*}").Methods("GET").HandlerFunc(repo.HandleLog) + h.Path("/{repo}/commit/{hash}").Methods("GET").HandlerFunc(repo.HandleCommit) h.Path("/{repo}/tree").Methods("GET").HandlerFunc(repo.HandleTree) h.Path("/{repo}/tree/{path:.*}").Methods("GET").HandlerFunc(repo.HandleTree) h.Path("/{repo}/file/{path:.*}").Methods("GET").HandlerFunc(repo.HandleFile) diff --git a/res/repo/commit.html b/res/repo/commit.html new file mode 100644 index 0000000..1805337 --- /dev/null +++ b/res/repo/commit.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<head lang="en-GB">{{template "base/head" .}}</head> +<body> + <header>{{template "repo/header" .}}</header><hr> + <main> + <table> + <tr><td>Author</td><td>{{.Author}}</td></tr> + <tr><td>Date</td><td>{{.Date}}</td></tr> + <tr><td>Commit</td><td><a href="/{{.Name}}/commit/{{.Commit}}">{{.Commit}}</a></td></tr> + {{range $i, $h := .Parents}} + <tr><td>Parent</td><td><a href="/{{$.Name}}/commit/{{$h}}">{{$h}}</a></td></tr> + {{end}} + </table> + <p>{{.MessageSubject}}</p> + <p>{{.MessageBody}}</p> + <h2>Diffstat</h2> + <table> + {{range .DiffStat}} + <tr> + <td><a href="/{{$.Name}}/file/{{.Name}}">{{.Name}}</a></td> + <td>|</td><td>{{.Num}}</td><td>{{.Diff}}</td> + </tr> + {{end}} + </table> + <p>{{.Summary}}</p> + <pre>{{.Diff}}</pre> + </main> +</body> diff --git a/res/repo/log.html b/res/repo/log.html index e814904..6f458fd 100644 --- a/res/repo/log.html +++ b/res/repo/log.html @@ -22,8 +22,8 @@ <td><a href="/{{$.Name}}/commit/{{.Hash}}">{{.Message}}</a></td> <td>{{.Author}}</td> <td style="text-align: right;">{{.Files}}</td> - <td style="text-align: right;" style="color: #008800">{{.Additions}}</td> - <td style="text-align: right;" style="color: #AA0000">{{.Deletions}}</td> + <td style="text-align: right; color: #008800;">{{.Additions}}</td> + <td style="text-align: right; color: #AA0000;">{{.Deletions}}</td> </tr> {{end}} {{else}} diff --git a/res/res.go b/res/res.go index 3e6405c..1e106ac 100644 --- a/res/res.go +++ b/res/res.go @@ -47,6 +47,9 @@ var RepoCreate string //go:embed repo/log.html var RepoLog string +//go:embed repo/commit.html +var RepoCommit string + //go:embed repo/tree.html var RepoTree string diff --git a/res/style.css b/res/style.css index 2b6c58a..a39e700 100644 --- a/res/style.css +++ b/res/style.css @@ -28,3 +28,35 @@ form table textarea { border: 2px solid #333333; border-radius: 3px; background-color: #111111; color: #888888; max-height: 18rem; min-height: 6rem; padding: 2px; resize: vertical; width: 24em; } + +.term-fg1 { font-weight: bold; } /* Bold */ +.term-fg2 { color: #888888; } /* Faint */ +.term-fg3 { font-style: italic; } /* Italic */ +.term-fg4 { text-decoration: underline; } /* Underline */ +.term-fg5 { animation: blink-animation 1s steps(3, start) infinite; } /* Blink */ +.term-fg9 { text-decoration: line-through; } /* Strikethrough */ + +.term-fg30 { color: #000000; } /* Black */ +.term-fg31 { color: #AA0000; } /* Red */ +.term-fg32 { color: #00AA00; } /* Green */ +.term-fg33 { color: #AA5500; } /* Yellow */ +.term-fg34 { color: #0000FF; } /* Blue */ +.term-fg35 { color: #AA00AA; } /* Magenta */ +.term-fg36 { color: #00AAAA; } /* Cyan */ + +.term-bg40 { background: #000000; } /* Black */ +.term-bg41 { background: #AA0000; } /* Red */ +.term-bg42 { background: #00AA00; } /* Green */ +.term-bg43 { background: #AA5500; } /* Yellow */ +.term-bg44 { background: #0000FF; } /* Blue */ +.term-bg45 { background: #AA00AA; } /* Magenta */ +.term-bg46 { background: #00AAAA; } /* Cyan */ + +.term-fgi90 { color: #555555; } /* Bright Black */ +.term-fgi91 { color: #FF5555; } /* Bright Red */ +.term-fgi92 { color: #55FF55; } /* Bright Green */ +.term-fgi93 { color: #FFFF55; } /* Bright Yellow */ +.term-fgi94 { color: #5555FF; } /* Bright Blue */ +.term-fgi95 { color: #FF55FF; } /* Bright Magenta */ +.term-fgi96 { color: #55FFFF; } /* Bright Cyan */ +.term-fgi97 { color: #FFFFFF; } /* Bright White */ diff --git a/src/git.go b/src/git.go index 5a2438f..0dbd706 100644 --- a/src/git.go +++ b/src/git.go @@ -21,7 +21,7 @@ import ( type gitCommand struct { prog string args []string - dir string + Dir string env []string } @@ -33,12 +33,12 @@ func HandleInfoRefs(w http.ResponseWriter, r *http.Request) { return } - c := newCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", "--advertise-refs", ".") - c.addEnv(os.Environ()...) - c.addEnv("GIT_PROTOCOL=version=2") - c.dir = RepoPath(repo.Name) + c := NewGitCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", "--advertise-refs", ".") + c.AddEnv(os.Environ()...) + c.AddEnv("GIT_PROTOCOL=version=2") + c.Dir = RepoPath(repo.Name) - refs, _, err := c.run(nil, nil) + refs, _, err := c.Run(nil, nil) if err != nil { log.Println("[Git HTTP]", err.Error()) HttpError(w, http.StatusInternalServerError) @@ -161,18 +161,18 @@ func gitHttpRpc(w http.ResponseWriter, r *http.Request, service string, repo *Re } } - c := newCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", ".") - c.addEnv(os.Environ()...) - c.dir = RepoPath(repo.Name) + c := NewGitCommand(strings.TrimPrefix(service, "git-"), "--stateless-rpc", ".") + c.AddEnv(os.Environ()...) + c.Dir = RepoPath(repo.Name) if p := r.Header.Get("Git-Protocol"); p == "version=2" { - c.addEnv("GIT_PROTOCOL=version=2") + c.AddEnv("GIT_PROTOCOL=version=2") } w.Header().Add("Content-Type", "application/x-"+service+"-result") w.WriteHeader(http.StatusOK) - if _, _, err := c.run(body, w); err != nil { + if _, _, err := c.Run(body, w); err != nil { log.Println("[Git RPC]", err.Error()) HttpError(w, http.StatusInternalServerError) return @@ -187,17 +187,17 @@ func pktLine(str string) []byte { func pktFlush() []byte { return []byte("0000") } -func newCommand(args ...string) *gitCommand { +func NewGitCommand(args ...string) *gitCommand { return &gitCommand{prog: "git", args: args} } -func (C *gitCommand) addEnv(env ...string) { +func (C *gitCommand) AddEnv(env ...string) { C.env = append(C.env, env...) } -func (C *gitCommand) run(in io.Reader, out io.Writer) ([]byte, []byte, error) { +func (C *gitCommand) Run(in io.Reader, out io.Writer) ([]byte, []byte, error) { c := exec.Command(C.prog, C.args...) - c.Dir = C.dir + c.Dir = C.Dir c.Env = C.env c.Stdin = in diff --git a/src/http.go b/src/http.go index 5846028..d4e1137 100644 --- a/src/http.go +++ b/src/http.go @@ -33,6 +33,7 @@ func init() { template.Must(Tmpl.New("repo/create").Parse(res.RepoCreate)) template.Must(Tmpl.New("repo/log").Parse(res.RepoLog)) + template.Must(Tmpl.New("repo/commit").Parse(res.RepoCommit)) template.Must(Tmpl.New("repo/tree").Parse(res.RepoTree)) template.Must(Tmpl.New("repo/file").Parse(res.RepoFile)) template.Must(Tmpl.New("repo/refs").Parse(res.RepoRefs)) diff --git a/src/repo/commit.go b/src/repo/commit.go new file mode 100644 index 0000000..d43ce07 --- /dev/null +++ b/src/repo/commit.go @@ -0,0 +1,125 @@ +package repo + +import ( + "errors" + "fmt" + "html/template" + "log" + "net/http" + "strconv" + "strings" + "time" + + goit "github.com/Jamozed/Goit/src" + "github.com/Jamozed/Goit/src/util" + "github.com/buildkite/terminal-to-html/v3" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/gorilla/mux" +) + +func HandleCommit(w http.ResponseWriter, r *http.Request) { + auth, uid := goit.AuthCookie(w, r, true) + + repo, err := goit.GetRepoByName(mux.Vars(r)["repo"]) + if err != nil { + goit.HttpError(w, http.StatusInternalServerError) + return + } else if repo == nil || (repo.IsPrivate && (!auth || repo.OwnerId != uid)) { + goit.HttpError(w, http.StatusNotFound) + return + } + + data := struct { + Title, Name, Description, Url string + Readme, Licence string + Author, Date, Commit string + Parents []string + MessageSubject, MessageBody string + DiffStat []struct{ Name, Num, Diff string } + Summary string + Diff template.HTML + }{ + Title: repo.Name + " - Log", Name: repo.Name, Description: repo.Description, + Url: util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name, + } + + gr, err := git.PlainOpen(goit.RepoPath(repo.Name)) + if err != nil { + log.Println("[/repo/commit]", err.Error()) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + commit, err := gr.CommitObject(plumbing.NewHash(mux.Vars(r)["hash"])) + if errors.Is(err, plumbing.ErrObjectNotFound) { + goit.HttpError(w, http.StatusNotFound) + return + } else if err != nil { + log.Println("[/repo/commit]", err.Error()) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + data.Author = commit.Author.String() + data.Date = commit.Author.When.UTC().Format(time.DateTime) + data.Commit = commit.Hash.String() + + for _, h := range commit.ParentHashes { + data.Parents = append(data.Parents, h.String()) + } + + message := strings.SplitN(commit.Message, "\n", 2) + data.MessageSubject = message[0] + data.MessageBody = message[1] + + st, err := commit.Stats() + if err != nil { + log.Println("[/repo/commit]", err.Error()) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + var files, additions, deletions int = len(st), 0, 0 + for _, s := range st { + /* TODO handle renames and colored plusses and minuses */ + f := struct{ Name, Num, Diff string }{Name: s.Name} + f.Num = strconv.FormatInt(int64(s.Addition+s.Deletion), 10) + + if s.Addition+s.Deletion > 80 { + f.Diff = strings.Repeat("+", (s.Addition*80)/(s.Addition+s.Deletion)) + f.Diff += strings.Repeat("-", (s.Deletion*80)/(s.Addition+s.Deletion)) + } else { + f.Diff = strings.Repeat("+", s.Addition) + strings.Repeat("-", s.Deletion) + } + + data.DiffStat = append(data.DiffStat, f) + + additions += s.Addition + deletions += s.Deletion + } + + data.Summary = fmt.Sprintf("%d files changed, %d insertions, %d deletions", files, additions, deletions) + + var phash string + if commit.NumParents() > 0 { + phash = commit.ParentHashes[0].String() + } else { + phash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" + } + + c := goit.NewGitCommand("diff", "--color=always", "-p", phash, commit.Hash.String()) + c.Dir = goit.RepoPath(repo.Name) + out, _, err := c.Run(nil, nil) + if err != nil { + log.Println("[/repo/commit]", err.Error()) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + data.Diff = template.HTML(terminal.Render(out)) + + if err := goit.Tmpl.ExecuteTemplate(w, "repo/commit", data); err != nil { + log.Println("[/repo/commit]", err.Error()) + } +}