bdec06fca065121838440a7e92546fc600627c85
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/git/repo.go b/git/repo.go
2index 107fc571aa8f595443b838883d236fb8a0d08d38..92b52238ade8d3e5499d5b2ae5eab8dff3dbbb70 100644
3--- a/git/repo.go
4+++ b/git/repo.go
5@@ -140,9 +140,17 @@ func (r *Repository) TreePath(ref *Reference, path string) (*Tree, error) {
6 return t.SubTree(path)
7 }
8
9-// Diff returns the diff for the given commit.
10-func (r *Repository) Diff(commit *Commit) (*Diff, error) {
11- diff, err := r.Repository.Diff(commit.ID.String(), DiffMaxFiles, DiffMaxFileLines, DiffMaxLineChars, git.DiffOptions{
12+// DiffLimits bounds the amount of diff data Git parses for one request.
13+type DiffLimits struct {
14+ MaxFiles int
15+ MaxFileLines int
16+ MaxLineChars int
17+}
18+
19+// DiffWithLimits returns the diff for commit using request-local limits.
20+// It does not modify the legacy package-level DiffMax* defaults.
21+func (r *Repository) DiffWithLimits(commit *Commit, limits DiffLimits) (*Diff, error) {
22+ diff, err := r.Repository.Diff(commit.ID.String(), limits.MaxFiles, limits.MaxFileLines, limits.MaxLineChars, git.DiffOptions{
23 CommandOptions: git.CommandOptions{
24 Envs: []string{"GIT_CONFIG_GLOBAL=/dev/null"},
25 },
26@@ -153,6 +161,15 @@ func (r *Repository) Diff(commit *Commit) (*Diff, error) {
27 return toDiff(diff), nil
28 }
29
30+// Diff returns the diff for the given commit using the legacy default limits.
31+func (r *Repository) Diff(commit *Commit) (*Diff, error) {
32+ return r.DiffWithLimits(commit, DiffLimits{
33+ MaxFiles: DiffMaxFiles,
34+ MaxFileLines: DiffMaxFileLines,
35+ MaxLineChars: DiffMaxLineChars,
36+ })
37+}
38+
39 // Patch returns the patch for the given reference.
40 func (r *Repository) Patch(commit *Commit) (string, error) {
41 diff, err := r.Diff(commit)
42diff --git a/pkg/web/pages/controller.go b/pkg/web/pages/controller.go
43index 8c85f1fad50cd91afe88cf0fcecf34150120fc46..332b8ef0f5f9dbf7295569aa3f59f8ced75f356c 100644
44--- a/pkg/web/pages/controller.go
45+++ b/pkg/web/pages/controller.go
46@@ -28,6 +28,9 @@ func Controller(_ context.Context, r *mux.Router) {
47 r.HandleFunc("/@/assets/site.css", stylesheet).Methods(http.MethodGet)
48 r.HandleFunc("/{repo:.*}/@/tree", treeBrowser).Methods(http.MethodGet)
49 r.HandleFunc("/{repo:.*}/@/raw", rawFile).Methods(http.MethodGet)
50+ r.HandleFunc("/{repo:.*}/@/commits", commits).Methods(http.MethodGet)
51+ r.HandleFunc("/{repo:.*}/@/commit", commit).Methods(http.MethodGet)
52+ r.HandleFunc("/{repo:.*}/@/refs", refs).Methods(http.MethodGet)
53 r.HandleFunc("/{repo:.*}", repositoryOverview).Methods(http.MethodGet)
54 }
55
56diff --git a/pkg/web/pages/history.go b/pkg/web/pages/history.go
57new file mode 100644
58index 0000000000000000000000000000000000000000..4979d459da77a78bade6ca479b47b080a88709ff
59--- /dev/null
60+++ b/pkg/web/pages/history.go
61@@ -0,0 +1,347 @@
62+package pages
63+
64+import (
65+ "errors"
66+ "html/template"
67+ "net/http"
68+ "net/url"
69+ "sort"
70+ "strconv"
71+ "strings"
72+ "unicode/utf8"
73+
74+ "github.com/charmbracelet/soft-serve/git"
75+ "github.com/charmbracelet/soft-serve/pkg/config"
76+)
77+
78+const (
79+ commitsPerPage = 30
80+ maxCommitPage = 1000
81+ maxDiffFiles = 50
82+ maxDiffFileLines = 100
83+ maxDiffLineChars = 250
84+ maxDiffBytes = 2 << 20
85+)
86+
87+var errInvalidPage = errors.New("invalid page")
88+
89+type commitsPage struct {
90+ ServerName string
91+ Repository string
92+ Ref string
93+ OverviewURL string
94+ Entries []commitListEntry
95+ PreviousURL string
96+ NextURL string
97+}
98+
99+type commitListEntry struct {
100+ Subject string
101+ ShortHash string
102+ Author string
103+ Date string
104+ URL string
105+}
106+
107+type commitPage struct {
108+ ServerName string
109+ Repository string
110+ OverviewURL string
111+ Hash string
112+ Author string
113+ Committer string
114+ Timestamp string
115+ Message string
116+ Diff template.HTML
117+ Truncated bool
118+}
119+
120+type refsPage struct {
121+ ServerName string
122+ Repository string
123+ OverviewURL string
124+ Branches []refPageEntry
125+ Tags []refPageEntry
126+}
127+
128+type refPageEntry struct {
129+ Name string
130+ ShortHash string
131+ Subject string
132+ Date string
133+ CommitURL string
134+ TreeURL string
135+}
136+
137+func commits(w http.ResponseWriter, r *http.Request) {
138+ repo, gitRepo, ok := openPublicRepository(w, r)
139+ if !ok {
140+ return
141+ }
142+ ref, err := ResolveRef(gitRepo, r.URL.Query().Get("ref"))
143+ if err != nil {
144+ http.Error(w, "not found", http.StatusNotFound)
145+ return
146+ }
147+ pageNumber, err := historyPageNumber(r.URL.Query().Get("page"))
148+ if err != nil {
149+ http.Error(w, "not found", http.StatusNotFound)
150+ return
151+ }
152+
153+ // Request one extra commit so pagination never needs a full-history count.
154+ commitList, err := gitRepo.CommitsByPage(ref, pageNumber, commitsPerPage+1)
155+ if err != nil {
156+ http.Error(w, "not found", http.StatusNotFound)
157+ return
158+ }
159+ hasNext := len(commitList) > commitsPerPage
160+ if hasNext {
161diff --git a/pkg/web/pages/history_test.go b/pkg/web/pages/history_test.go
162new file mode 100644
163index 0000000000000000000000000000000000000000..3515df64a0da69aae86bfa20fe268ac84d900f79
164--- /dev/null
165+++ b/pkg/web/pages/history_test.go
166@@ -0,0 +1,138 @@
167+package pages
168+
169+import (
170+ "bytes"
171+ "os"
172+ "os/exec"
173+ "path/filepath"
174+ "strings"
175+ "testing"
176+
177+ "github.com/charmbracelet/soft-serve/git"
178+)
179+
180+func TestHistoryPaginationUsesThirtyItemsAndBounds(t *testing.T) {
181+ repo := newTestRepository(t, map[string]string{"log.txt": "0\n"})
182+ for i := 1; i <= commitsPerPage; i++ {
183+ appendCommit(t, repo.Path, "log.txt", "next "+strings.Repeat("x", i))
184+ }
185+ ref, err := ResolveRef(repo, "")
186+ if err != nil {
187+ t.Fatal(err)
188+ }
189+ commits, err := repo.CommitsByPage(ref, 1, commitsPerPage+1)
190+ if err != nil {
191+ t.Fatal(err)
192+ }
193+ if len(commits) != commitsPerPage+1 {
194+ t.Fatalf("extra commit request returned %d commits, want %d", len(commits), commitsPerPage+1)
195+ }
196+ if got := len(commitEntries("project", commits[:commitsPerPage])); got != commitsPerPage {
197+ t.Fatalf("display entries = %d, want %d", got, commitsPerPage)
198+ }
199+ if _, err := historyPageNumber("0"); err == nil {
200+ t.Fatal("zero page was accepted")
201+ }
202+ if got, err := historyPageNumber("1001"); err != nil || got != maxCommitPage {
203+ t.Fatalf("out-of-range page = %d, %v; want %d, nil", got, err, maxCommitPage)
204+ }
205+}
206+
207+func TestRefsSupportSlashBranchesAndAnnotatedTags(t *testing.T) {
208+ repo := newTestRepository(t, map[string]string{"README.md": "hello"})
209+ runGit(t, repo.Path, "branch", "feature/with-slash")
210+ runGit(t, repo.Path, "tag", "-a", "release/v1", "-m", "release")
211+ references, err := repo.References()
212+ if err != nil {
213+ t.Fatal(err)
214+ }
215+ var branch, tag *git.Reference
216+ for _, ref := range references {
217+ switch ref.Name().String() {
218+ case "refs/heads/feature/with-slash":
219+ branch = ref
220+ case "refs/tags/release/v1":
221+ tag = ref
222+ }
223+ }
224+ if branch == nil || tag == nil {
225+ t.Fatalf("missing branch or tag in %#v", references)
226+ }
227+ branchEntry := refEntry(repo, "team/project", branch)
228+ tagEntry := refEntry(repo, "team/project", tag)
229+ for _, entry := range []refPageEntry{branchEntry, tagEntry} {
230+ if entry.CommitURL == "" || !strings.Contains(entry.CommitURL, "/@/commit?hash=") {
231+ t.Fatalf("commit link missing: %#v", entry)
232+ }
233+ }
234+ if !strings.Contains(branchEntry.TreeURL, "ref=refs%2Fheads%2Ffeature%2Fwith-slash") {
235+ t.Fatalf("branch tree link does not encode full ref: %q", branchEntry.TreeURL)
236+ }
237+ if !strings.Contains(tagEntry.TreeURL, "ref=refs%2Ftags%2Frelease%2Fv1") {
238+ t.Fatalf("tag tree link does not encode full ref: %q", tagEntry.TreeURL)
239+ }
240+}
241+
242+func TestCommitLookupValidationAndRepositoryBoundary(t *testing.T) {
243+ first := newTestRepository(t, map[string]string{"first.txt": "first"})
244+ second := newTestRepository(t, map[string]string{"second.txt": "second"})
245+ if _, err := LookupCommit(first, "not-a-hash"); err == nil {
246+ t.Fatal("invalid hash was accepted")
247+ }
248+ treeID := strings.TrimSpace(runGitOutput(t, first.Path, "rev-parse", "HEAD^{tree}"))
249+ if _, err := LookupCommit(first, treeID); err == nil {
250+ t.Fatal("non-commit object was accepted")
251+ }
252+ secondID := strings.TrimSpace(runGitOutput(t, second.Path, "rev-parse", "HEAD"))
253+ if _, err := LookupCommit(first, secondID); err == nil {
254+ t.Fatal("commit from another repository was disclosed")
255+ }
256+}
257+
258+func TestCommitMessageEscapingAndBoundedDiff(t *testing.T) {
259+ repo := newTestRepository(t, map[string]string{"changed.txt": "start\n"})
260+ appendCommit(t, repo.Path, "changed.txt", strings.Repeat("line\n", 120))
261+ hash := strings.TrimSpace(runGitOutput(t, repo.Path, "rev-parse", "HEAD"))
262+ commit, err := LookupCommit(repo, hash)
263+ if err != nil {
264+ t.Fatal(err)
265+ }
266diff --git a/pkg/web/pages/templates/history.gohtml b/pkg/web/pages/templates/history.gohtml
267new file mode 100644
268index 0000000000000000000000000000000000000000..df92a1705fcc67abc8f101f461cd1dcf60a386c0
269--- /dev/null
270+++ b/pkg/web/pages/templates/history.gohtml
271@@ -0,0 +1,61 @@
272+{{ define "commits" }}<!doctype html>
273+<html lang="en">
274+<head>
275+ <meta charset="utf-8">
276+ <meta name="viewport" content="width=device-width, initial-scale=1">
277+ <title>Commits · {{ .Repository }} · {{ .ServerName }}</title>
278+ <link rel="stylesheet" href="/@/assets/site.css">
279+</head>
280+<body>
281+ <header class="site-header"><nav aria-label="Primary"><a class="site-name" href="/">{{ .ServerName }}</a></nav></header>
282+ <main>
283+ <p><a href="{{ .OverviewURL }}">{{ .Repository }}</a> / <code>{{ .Ref }}</code></p>
284+ <h1>Commits</h1>
285+ <ol class="commit-list">{{ range .Entries }}
286+ <li><article><h2><a href="{{ .URL }}">{{ .Subject }}</a></h2><p><code>{{ .ShortHash }}</code> · {{ .Author }} · <time>{{ .Date }}</time></p></article></li>
287+ {{ else }}<li>No commits found.</li>{{ end }}</ol>
288+ {{ if or .PreviousURL .NextURL }}<nav aria-label="Commit history pages">{{ if .PreviousURL }}<a href="{{ .PreviousURL }}">Previous</a>{{ end }} {{ if .NextURL }}<a href="{{ .NextURL }}">Next</a>{{ end }}</nav>{{ end }}
289+ </main>
290+</body>
291+</html>{{ end }}
292+
293+{{ define "commit" }}<!doctype html>
294+<html lang="en">
295+<head>
296+ <meta charset="utf-8">
297+ <meta name="viewport" content="width=device-width, initial-scale=1">
298+ <title>Commit {{ .Hash }} · {{ .Repository }} · {{ .ServerName }}</title>
299+ <link rel="stylesheet" href="/@/assets/site.css">
300+</head>
301+<body>
302+ <header class="site-header"><nav aria-label="Primary"><a class="site-name" href="/">{{ .ServerName }}</a></nav></header>
303+ <main>
304+ <p><a href="{{ .OverviewURL }}">{{ .Repository }}</a></p>
305+ <article>
306+ <h1>Commit <code>{{ .Hash }}</code></h1>
307+ <dl><dt>Author</dt><dd>{{ .Author }}</dd><dt>Committer</dt><dd>{{ .Committer }}</dd><dt>Date</dt><dd><time>{{ .Timestamp }}</time></dd></dl>
308+ <h2>Message</h2><pre>{{ .Message }}</pre>
309+ <h2>Diff</h2>{{ if .Truncated }}<p>This diff is truncated to protect this page.</p>{{ end }}<pre class="source"><code>{{ .Diff }}</code></pre>
310+ </article>
311+ </main>
312+</body>
313+</html>{{ end }}
314+
315+{{ define "refs" }}<!doctype html>
316+<html lang="en">
317+<head>
318+ <meta charset="utf-8">
319+ <meta name="viewport" content="width=device-width, initial-scale=1">
320+ <title>Branches and tags · {{ .Repository }} · {{ .ServerName }}</title>
321+ <link rel="stylesheet" href="/@/assets/site.css">
322+</head>
323+<body>
324+ <header class="site-header"><nav aria-label="Primary"><a class="site-name" href="/">{{ .ServerName }}</a></nav></header>
325+ <main>
326+ <p><a href="{{ .OverviewURL }}">{{ .Repository }}</a></p>
327+ <h1>Branches and tags</h1>