Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-08-06 11:29:48
Commit3e159d3dd66b838ce7d8089fe9b44c936df8f973
Parent1f376a942c45404d3fdf74160d09a12b61a23a79

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())
+	}
+}