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.

Blame · server/web/commit.go

Line attribution on main · 155 line(s)

78912785
Keenan Nemetz
17d ago
1 package web
2
628d4523
Keenan Nemetz
11d ago
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.
78912785
Keenan Nemetz
17d ago
8
9 import (
10 "net/http"
11
d37d301f
Keenan Nemetz
18h ago
12 "rodent.software/vole/core/git"
78912785
Keenan Nemetz
17d ago
13 )
14
628d4523
Keenan Nemetz
11d ago
15 // commitView is the commit page: the metadata, the changed-file tree, and the
16 // pane the selected file's diff renders in.
17 type 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.
31 type commitMainView struct {
f64c77e4
Keenan Nemetz
9d ago
32 Path string
33 Files []diffFile
34 Added int
35 Removed int
628d4523
Keenan Nemetz
11d ago
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.
78912785
Keenan Nemetz
17d ago
43 func (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 == "" {
e84a66f5
Keenan Nemetz
15d ago
51 w.notFound(rw, r)
78912785
Keenan Nemetz
17d ago
52 return
53 }
54 log, err := g.Log(sha, 1)
55 if err != nil || len(log) == 0 {
e84a66f5
Keenan Nemetz
15d ago
56 w.notFound(rw, r)
78912785
Keenan Nemetz
17d ago
57 return
58 }
628d4523
Keenan Nemetz
11d ago
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)
78912785
Keenan Nemetz
17d ago
89 if err != nil {
e84a66f5
Keenan Nemetz
15d ago
90 w.serverError(rw, r, "cannot read that commit: "+err.Error())
78912785
Keenan Nemetz
17d ago
91 return
92 }
628d4523
Keenan Nemetz
11d ago
93 if htmxRequest(r) {
94 w.render(rw, "commit_main", main, 0)
95 return
96 }
78912785
Keenan Nemetz
17d ago
97 body, _ := g.CommitBody(sha)
f09ef39e
Keenan Nemetz
15d ago
98 parents, _ := g.CommitParents(sha)
628d4523
Keenan Nemetz
11d ago
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,
f09ef39e
Keenan Nemetz
15d ago
104 Parents: parents,
628d4523
Keenan Nemetz
11d ago
105 FileCount: len(files),
106 FilesTruncated: truncated,
107 Tree: buildReviewTree(files, selected),
108 Main: main,
78912785
Keenan Nemetz
17d ago
109 }, 0)
110 }
628d4523
Keenan Nemetz
11d ago
111
112 // commitDiff reads the patch of the one file being shown, and only that file's.
113 func 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]
bef0c093
Keenan Nemetz
9d ago
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 }
628d4523
Keenan Nemetz
11d ago
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 {
f64c77e4
Keenan Nemetz
9d ago
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.
628d4523
Keenan Nemetz
11d ago
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
f64c77e4
Keenan Nemetz
9d ago
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.
628d4523
Keenan Nemetz
11d ago
147 func changedFiles(changes []git.PathChange) []diffFile {
148 files := make([]diffFile, 0, len(changes))
149 for _, c := range changes {
f64c77e4
Keenan Nemetz
9d ago
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)
628d4523
Keenan Nemetz
11d ago
153 }
154 return files
155 }