Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 23m ago 32290114
history blame raw 4.4 KiB
1package web
2
3// Commit pages: a commit's metadata above a tree of the files it touched, and
4// the diff of the one file that is open. A broad commit is reviewed file by
5// file, because rendering every patch at once turns one page load into every
6// blob the commit touched. This is read-only like the rest of the web — the
7// review itself is a git artifact, not a web session.
8
9import (
10 "net/http"
11
12 "rodent.software/vole/core/git"
13)
14
15// commitView is the commit page: the metadata, the changed-file tree, and the
16// pane the selected file's diff renders in.
17type commitView struct {
18 Page
19 Name string
20 Commit git.Commit
21 Body string
22 Parents []string
23 FileCount int
24 FilesTruncated bool
25 Tree []*treeNode
26 Main commitMainView
27}
28
29// commitMainView is the diff pane, both as the page's main column and as the
30// HTMX fragment a tree click swaps in.
31type commitMainView struct {
32 Path string
33 Files []diffFile
34 Added int
35 Removed int
36 // A commit page is read-only, so a diff line has nowhere to attach a review
37 // comment; the diff component reads this and draws no comment control.
38 LineCommentURL string
39}
40
41// CommitView renders one commit, opening the first file it touched. A path
42// deep-links one file, and tree clicks fetch only the diff pane over HTMX.
43func (w *Web) CommitView(rw http.ResponseWriter, r *http.Request) {
44 m, ok := w.open(rw, r)
45 if !ok {
46 return
47 }
48 g := m.Git()
49 sha, err := g.RevParse(r.PathValue("sha"))
50 if err != nil || sha == "" {
51 w.notFound(rw, r)
52 return
53 }
54 log, err := g.Log(sha, 1)
55 if err != nil || len(log) == 0 {
56 w.notFound(rw, r)
57 return
58 }
59 // The tree is built from the names alone: reading every file's hunks to find
60 // out which files the commit touched is what makes a commit page expensive.
61 changes, err := g.ShowNameStatus(sha)
62 if err != nil {
63 w.serverError(rw, r, "cannot read that commit: "+err.Error())
64 return
65 }
66 files := changedFiles(changes)
67 truncated := false
68 if len(files) > maxTreeFiles {
69 files = files[:maxTreeFiles]
70 truncated = true
71 }
72
73 selected := r.PathValue("path")
74 if selected == "" && len(files) > 0 {
75 selected = diffFilePath(files[0])
76 }
77 chosen := -1
78 for i := range files {
79 if diffFilePath(files[i]) == selected {
80 chosen = i
81 break
82 }
83 }
84 if selected != "" && chosen < 0 {
85 w.notFound(rw, r)
86 return
87 }
88 main, err := commitDiff(g, sha, selected, files, chosen)
89 if err != nil {
90 w.serverError(rw, r, "cannot read that commit: "+err.Error())
91 return
92 }
93 if htmxRequest(r) {
94 w.render(rw, "commit_main", main, 0)
95 return
96 }
97 body, _ := g.CommitBody(sha)
98 parents, _ := g.CommitParents(sha)
99 w.render(rw, "commit", commitView{
100 Page: w.page(r, m.Name+" · "+log[0].Subject),
101 Name: m.Name,
102 Commit: log[0],
103 Body: body,
104 Parents: parents,
105 FileCount: len(files),
106 FilesTruncated: truncated,
107 Tree: buildReviewTree(files, selected),
108 Main: main,
109 }, 0)
110}
111
112// commitDiff reads the patch of the one file being shown, and only that file's.
113func commitDiff(g *git.Repo, sha, selected string, files []diffFile, chosen int) (commitMainView, error) {
114 main := commitMainView{Path: selected}
115 if chosen < 0 {
116 return main, nil
117 }
118 f := files[chosen]
119 patch := ""
120 if len(f.pathspec()) > 0 {
121 var err error
122 if patch, err = g.ShowPatchPath(sha, f.pathspec()); err != nil {
123 return main, err
124 }
125 }
126 for _, p := range parseDiff(patch) {
127 if diffFilePath(p) == selected {
128 main.Files = []diffFile{p}
129 break
130 }
131 }
132 if main.Files == nil {
133 // A patch git printed nothing for (a mode change, for instance) still
134 // belongs in the pane: the tree lists it, so the pane can never pretend
135 // it is not there.
136 f.Language = prismLanguage(f.Label())
137 main.Files = []diffFile{f}
138 }
139 main.Added, main.Removed = diffSummary(main.Files)
140 return main, nil
141}
142
143// changedFiles turns a name-status list, or a change's inventory, into the rows a
144// changed-file tree and its pane need: the same shape a parsed diff produces, so
145// the status mark, the label and the line counts are derived the same way
146// whichever read built them.
147func changedFiles(changes []git.PathChange) []diffFile {
148 files := make([]diffFile, 0, len(changes))
149 for _, c := range changes {
150 f := diffFile{OldPath: c.Old, NewPath: c.New, Added: c.Added, Removed: c.Removed, Binary: c.Binary}
151 f.Language = prismLanguage(f.Label())
152 files = append(files, f)
153 }
154 return files
155}
156