Goit

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

AuthorJakob Wakeling <[email protected]>
Date2023-12-23 04:05:01
Commitb4b291ece361bb22761f24eac29936f15f83a2c5
Parentdb3692db87cf30f7d5914d7d7b94c5ef8778510f

Paginate repository log page

Diffstat

M res/repo/log.html | 15 +++++++++++++++
M res/style.css | 2 ++
M src/main.go | 10 ++++++++++
M src/repo/commit.go | 4 ++--
M src/repo/edit.go | 4 ++--
M src/repo/file.go | 4 ++--
M src/repo/log.go | 105 ++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
M src/repo/refs.go | 4 ++--
M src/repo/repo.go | 49 ++++++++-----------------------------------------
M src/repo/tree.go | 4 ++--
M src/util/util.go | 18 ++++++++++++++++--

11 files changed, 122 insertions, 97 deletions

diff --git a/res/repo/log.html b/res/repo/log.html
index 539beeb..24ea16c 100644
--- a/res/repo/log.html
+++ b/res/repo/log.html
@@ -31,5 +31,20 @@
 				{{end}}
 			</tbody>
 		</table>
+		<footer>
+			{{if gt .PrevOffset 0}}
+				<a href="/{{$.Name}}/log?o={{.PrevOffset}}">[prev]</a>
+			{{else if eq .PrevOffset 0}}
+				<a href="/{{$.Name}}/log">[prev]</a>
+			{{else}}
+				<span>[prev]</span>
+			{{end}}
+			<span>{{.Page}}</span>
+			{{if .NextOffset}}
+				<a href="/{{$.Name}}/log?o={{.NextOffset}}">[next]</a>
+			{{else}}
+				<span>[next]</span>
+			{{end}}
+		</footer>
 	</main>
 </body>
diff --git a/res/style.css b/res/style.css
index d84a048..ddf0589 100644
--- a/res/style.css
+++ b/res/style.css
@@ -6,6 +6,8 @@ a:hover { text-decoration: underline; }
 h1, h2 { font-size: 1em; margin: 0; }
 hr { border: 0; height: 1rem; margin: 0; }
 
+footer { padding: 0.4rem 0.4rem 1rem; }
+
 table td { padding: 0 0.4rem; }
 table td:empty::after { content: "\00a0"; }
 table td pre { margin: 0; }
diff --git a/src/main.go b/src/main.go
index 7087994..1933a86 100644
--- a/src/main.go
+++ b/src/main.go
@@ -86,6 +86,16 @@ func main() {
 	h.Use(middleware.RedirectSlashes)
 	h.Use(logHttp)
 
+	h.Use(func(h http.Handler) http.Handler {
+		return http.TimeoutHandler(h, 90*time.Second,
+			`<!DOCTYPE html><head lang="en-GB">
+		<meta charset="UTF-8"><title>503 Service Unavailable</title>
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<link rel="stylesheet" type="text/css" href="/static/style.css">
+		<link rel="icon" type="image/png" href="/static/favicon.png">
+		</head><body><b>503 Service Unavailable</b></body>`)
+	})
+
 	protect = csrf.Protect(
 		[]byte(goit.Conf.CsrfSecret), csrf.FieldName("csrf.Token"), csrf.CookieName("csrf"),
 		csrf.Secure(util.If(goit.Conf.UsesHttps, true, false)),
diff --git a/src/repo/commit.go b/src/repo/commit.go
index d4f5986..4c31712 100644
--- a/src/repo/commit.go
+++ b/src/repo/commit.go
@@ -75,10 +75,10 @@ func HandleCommit(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	} else {
-		if readme, _ := findReadme(gr, ref); readme != "" {
+		if readme, _ := findPattern(gr, ref, readmePattern); readme != "" {
 			data.Readme = filepath.Join("/", repo.Name, "file", readme)
 		}
-		if licence, _ := findLicence(gr, ref); licence != "" {
+		if licence, _ := findPattern(gr, ref, licencePattern); licence != "" {
 			data.Licence = filepath.Join("/", repo.Name, "file", licence)
 		}
 	}
diff --git a/src/repo/edit.go b/src/repo/edit.go
index 3a6f34d..e007c9d 100644
--- a/src/repo/edit.go
+++ b/src/repo/edit.go
@@ -105,10 +105,10 @@ func HandleEdit(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if ref != nil {
-		if readme, _ := findReadme(gr, ref); readme != "" {
+		if readme, _ := findPattern(gr, ref, readmePattern); readme != "" {
 			data.Readme = filepath.Join("/", repo.Name, "file", readme)
 		}
-		if licence, _ := findLicence(gr, ref); licence != "" {
+		if licence, _ := findPattern(gr, ref, licencePattern); licence != "" {
 			data.Licence = filepath.Join("/", repo.Name, "file", licence)
 		}
 	}
diff --git a/src/repo/file.go b/src/repo/file.go
index 38b0a78..0ec7a1b 100644
--- a/src/repo/file.go
+++ b/src/repo/file.go
@@ -72,10 +72,10 @@ func HandleFile(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if readme, _ := findReadme(gr, ref); readme != "" {
+	if readme, _ := findPattern(gr, ref, readmePattern); readme != "" {
 		data.Readme = path.Join("/", repo.Name, "file", readme)
 	}
-	if licence, _ := findLicence(gr, ref); licence != "" {
+	if licence, _ := findPattern(gr, ref, licencePattern); licence != "" {
 		data.Licence = path.Join("/", repo.Name, "file", licence)
 	}
 
diff --git a/src/repo/log.go b/src/repo/log.go
index 720b6f4..f36200e 100644
--- a/src/repo/log.go
+++ b/src/repo/log.go
@@ -6,9 +6,11 @@ package repo
 import (
 	"errors"
 	"fmt"
+	"io"
 	"log"
 	"net/http"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"time"
 
@@ -17,9 +19,10 @@ import (
 	"github.com/go-chi/chi/v5"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing"
-	"github.com/go-git/go-git/v5/plumbing/object"
 )
 
+const PAGE = 100
+
 func HandleLog(w http.ResponseWriter, r *http.Request) {
 	auth, user, err := goit.Auth(w, r, true)
 	if err != nil {
@@ -39,15 +42,15 @@ func HandleLog(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// var offset uint64 = 0
-	// if o := r.URL.Query().Get("o"); o != "" {
-	// 	if i, err := strconv.ParseUint(o, 10, 64); err != nil {
-	// 		goit.HttpError(w, http.StatusBadRequest)
-	// 		return
-	// 	} else {
-	// 		offset = i
-	// 	}
-	// }
+	offset := int64(0)
+	if o := r.URL.Query().Get("o"); o != "" {
+		if i, err := strconv.ParseInt(o, 10, 64); err != nil {
+			goit.HttpError(w, http.StatusBadRequest)
+			return
+		} else {
+			offset = i
+		}
+	}
 
 	type row struct{ Hash, Date, Message, Author, Files, Additions, Deletions string }
 	data := struct {
@@ -55,11 +58,15 @@ func HandleLog(w http.ResponseWriter, r *http.Request) {
 		Readme, Licence               string
 		Commits                       []row
 		Editable, IsMirror            bool
+		Page, PrevOffset, NextOffset  int64
 	}{
 		Title: repo.Name + " - Log", Name: repo.Name, Description: repo.Description,
-		Url:      util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
-		Editable: (auth && repo.OwnerId == user.Id),
-		IsMirror: repo.IsMirror,
+		Url:        util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
+		Editable:   (auth && repo.OwnerId == user.Id),
+		IsMirror:   repo.IsMirror,
+		Page:       offset/PAGE + 1,
+		PrevOffset: util.Max(offset-PAGE, -1),
+		NextOffset: offset + PAGE,
 	}
 
 	gr, err := git.PlainOpen(goit.RepoPath(repo.Name, true))
@@ -78,53 +85,63 @@ func HandleLog(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if readme, _ := findReadme(gr, ref); readme != "" {
+	if readme, _ := findPattern(gr, ref, readmePattern); readme != "" {
 		data.Readme = filepath.Join("/", repo.Name, "file", readme)
 	}
-	if licence, _ := findLicence(gr, ref); licence != "" {
+	if licence, _ := findPattern(gr, ref, licencePattern); licence != "" {
 		data.Licence = filepath.Join("/", repo.Name, "file", licence)
 	}
 
-	if iter, err := gr.Log(&git.LogOptions{From: ref.Hash()}); err != nil {
+	if iter, err := gr.Log(&git.LogOptions{
+		From: ref.Hash(), Order: git.LogOrderCommitterTime, PathFilter: func(s string) bool {
+			return tpath == "" || s == tpath || strings.HasPrefix(s, tpath+"/")
+		},
+	}); err != nil {
 		log.Println("[/repo/log]", err.Error())
 		goit.HttpError(w, http.StatusInternalServerError)
 		return
-	} else if err := iter.ForEach(func(c *object.Commit) error {
-		var files, additions, deletions int
-
-		if stats, err := goit.DiffStats(c); err != nil {
-			log.Println("[/repo/log]", err.Error())
-		} else if tpath != "" {
-			for _, s := range stats {
-				if s.Name == tpath || strings.HasPrefix(s.Name, tpath+"/") {
-					files += 1
+	} else {
+		for i := int64(0); i < offset; i += 1 {
+			if _, err := iter.Next(); err != nil && !errors.Is(err, io.EOF) {
+				log.Println("[/repo/log]", err.Error())
+				goit.HttpError(w, http.StatusInternalServerError)
+				return
+			}
+		}
+
+		for i := 0; i < PAGE; i += 1 {
+			c, err := iter.Next()
+			if errors.Is(err, io.EOF) {
+				data.NextOffset = 0
+				break
+			} else if err != nil {
+				log.Println("[/repo/log]", err.Error())
+				goit.HttpError(w, http.StatusInternalServerError)
+				return
+			}
+
+			var files, additions, deletions int
+
+			if stats, err := goit.DiffStats(c); err != nil {
+				log.Println("[/repo/log]", err.Error())
+			} else {
+				files = len(stats)
+				for _, s := range stats {
 					additions += s.Addition
 					deletions += s.Deletion
 				}
 			}
 
-			if files == 0 {
-				return nil
-			}
-		} else {
-			files = len(stats)
-			for _, s := range stats {
-				additions += s.Addition
-				deletions += s.Deletion
-			}
+			data.Commits = append(data.Commits, row{
+				Hash: c.Hash.String(), Date: c.Author.When.UTC().Format(time.DateTime),
+				Message: strings.SplitN(c.Message, "\n", 2)[0], Author: c.Author.Name, Files: fmt.Sprint(files),
+				Additions: "+" + fmt.Sprint(additions), Deletions: "-" + fmt.Sprint(deletions),
+			})
 		}
 
-		data.Commits = append(data.Commits, row{
-			Hash: c.Hash.String(), Date: c.Author.When.UTC().Format(time.DateTime),
-			Message: strings.SplitN(c.Message, "\n", 2)[0], Author: c.Author.Name, Files: fmt.Sprint(files),
-			Additions: "+" + fmt.Sprint(additions), Deletions: "-" + fmt.Sprint(deletions),
-		})
-
-		return nil
-	}); err != nil {
-		log.Println("[/repo/log]", err.Error())
-		goit.HttpError(w, http.StatusInternalServerError)
-		return
+		if _, err := iter.Next(); errors.Is(err, io.EOF) {
+			data.NextOffset = 0
+		}
 	}
 
 execute:
diff --git a/src/repo/refs.go b/src/repo/refs.go
index 2674034..af7b88e 100644
--- a/src/repo/refs.go
+++ b/src/repo/refs.go
@@ -62,10 +62,10 @@ func HandleRefs(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	} else {
-		if readme, _ := findReadme(gr, ref); readme != "" {
+		if readme, _ := findPattern(gr, ref, readmePattern); readme != "" {
 			data.Readme = filepath.Join("/", repo.Name, "file", readme)
 		}
-		if licence, _ := findLicence(gr, ref); licence != "" {
+		if licence, _ := findPattern(gr, ref, licencePattern); licence != "" {
 			data.Licence = filepath.Join("/", repo.Name, "file", licence)
 		}
 	}
diff --git a/src/repo/repo.go b/src/repo/repo.go
index 6cc40a9..aa799a7 100644
--- a/src/repo/repo.go
+++ b/src/repo/repo.go
@@ -8,8 +8,6 @@ import (
 
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing"
-	"github.com/go-git/go-git/v5/plumbing/object"
-	"github.com/go-git/go-git/v5/plumbing/storer"
 )
 
 type HeaderFields struct {
@@ -20,54 +18,23 @@ type HeaderFields struct {
 var readmePattern = regexp.MustCompile(`(?i)^readme(?:\.?(?:md|txt))?$`)
 var licencePattern = regexp.MustCompile(`(?i)^licence(?:\.?(?:md|txt))?$`)
 
-func findReadme(gr *git.Repository, ref *plumbing.Reference) (string, error) {
-	commit, err := gr.CommitObject(ref.Hash())
+/* Find a file that matches a regular expression in the root level of a reference. */
+func findPattern(gr *git.Repository, ref *plumbing.Reference, re *regexp.Regexp) (string, error) {
+	c, err := gr.CommitObject(ref.Hash())
 	if err != nil {
 		return "", err
 	}
 
-	iter, err := commit.Files()
+	t, err := c.Tree()
 	if err != nil {
 		return "", err
 	}
 
-	var filename string
-	if err := iter.ForEach(func(f *object.File) error {
-		if readmePattern.MatchString(f.Name) {
-			filename = f.Name
-			return storer.ErrStop
+	for _, e := range t.Entries {
+		if re.MatchString(e.Name) {
+			return e.Name, nil
 		}
-
-		return nil
-	}); err != nil {
-		return "", err
-	}
-
-	return filename, nil
-}
-
-func findLicence(gr *git.Repository, ref *plumbing.Reference) (string, error) {
-	commit, err := gr.CommitObject(ref.Hash())
-	if err != nil {
-		return "", err
-	}
-
-	iter, err := commit.Files()
-	if err != nil {
-		return "", err
-	}
-
-	var filename string
-	if err := iter.ForEach(func(f *object.File) error {
-		if licencePattern.MatchString(f.Name) {
-			filename = f.Name
-			return storer.ErrStop
-		}
-
-		return nil
-	}); err != nil {
-		return "", err
 	}
 
-	return filename, nil
+	return "", nil
 }
diff --git a/src/repo/tree.go b/src/repo/tree.go
index 26455e3..598e3bd 100644
--- a/src/repo/tree.go
+++ b/src/repo/tree.go
@@ -83,10 +83,10 @@ func HandleTree(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 	} else {
-		if readme, _ := findReadme(gr, ref); readme != "" {
+		if readme, _ := findPattern(gr, ref, readmePattern); readme != "" {
 			data.Readme = path.Join("/", repo.Name, "file", readme)
 		}
-		if licence, _ := findLicence(gr, ref); licence != "" {
+		if licence, _ := findPattern(gr, ref, licencePattern); licence != "" {
 			data.Licence = path.Join("/", repo.Name, "file", licence)
 		}
 
diff --git a/src/util/util.go b/src/util/util.go
index c4866a2..4447a8d 100644
--- a/src/util/util.go
+++ b/src/util/util.go
@@ -4,6 +4,7 @@
 package util
 
 import (
+	"cmp"
 	"errors"
 	"io/fs"
 	"net/http"
@@ -17,9 +18,22 @@ const ModeNotRegular = os.ModeSymlink | os.ModeDevice | os.ModeNamedPipe | os.Mo
 func If[T any](cond bool, a, b T) T {
 	if cond {
 		return a
-	} else {
-		return b
 	}
+	return b
+}
+
+func Min[T cmp.Ordered](a, b T) T {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+func Max[T cmp.Ordered](a, b T) T {
+	if a > b {
+		return a
+	}
+	return b
 }
 
 /* Return the named cookie or nil if not found or invalid. */