Skip to content

Commit 9c6aeb4

Browse files
noerwrogerluo410
andauthored
Link to previous blames in file blame page (#16259)
Adds a link to each blame hunk, to view the blame of an earlier version of the file, similar to GitHub. Also refactors the blame render from fmtstring based to template based. * Fix blame bottom line and add blame prior button * Jump to previous parent commit from the commit. * Fix previous commit link * Fix previous blame link * Fix the given file not exist in the previous commit. * Fix blameRow struct not export * fix theming issues, rename template var * remove unused LastCommit fetch * fix location of blame-hunk divider * rewrite previous commit checks * remove duplicate commit lookup its already resolved and stored in ctx.Repo.Commit! * split out blamePart processing into function Co-authored-by: rogerluo410 <[email protected]>
1 parent 59c5855 commit 9c6aeb4

File tree

6 files changed

+166
-101
lines changed

6 files changed

+166
-101
lines changed

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@ delete_preexisting_label = Delete
809809
delete_preexisting = Delete pre-existing files
810810
delete_preexisting_content = Delete files in %s
811811
delete_preexisting_success = Deleted unadopted files in %s
812+
blame_prior = View blame prior to this change
812813

813814
transfer.accept = Accept Transfer
814815
transfer.accept_desc = Transfer to "%s"

routers/web/repo/blame.go

Lines changed: 96 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package repo
66

77
import (
8-
"bytes"
98
"container/list"
109
"fmt"
1110
"html"
@@ -18,7 +17,6 @@ import (
1817
"code.gitea.io/gitea/modules/context"
1918
"code.gitea.io/gitea/modules/git"
2019
"code.gitea.io/gitea/modules/highlight"
21-
"code.gitea.io/gitea/modules/log"
2220
"code.gitea.io/gitea/modules/templates"
2321
"code.gitea.io/gitea/modules/timeutil"
2422
)
@@ -27,6 +25,20 @@ const (
2725
tplBlame base.TplName = "repo/home"
2826
)
2927

28+
type blameRow struct {
29+
RowNumber int
30+
Avatar gotemplate.HTML
31+
RepoLink string
32+
PartSha string
33+
PreviousSha string
34+
PreviousShaURL string
35+
IsFirstCommit bool
36+
CommitURL string
37+
CommitMessage string
38+
CommitSince gotemplate.HTML
39+
Code gotemplate.HTML
40+
}
41+
3042
// RefBlame render blame page
3143
func RefBlame(ctx *context.Context) {
3244
fileName := ctx.Repo.TreePath
@@ -39,19 +51,6 @@ func RefBlame(ctx *context.Context) {
3951
repoName := ctx.Repo.Repository.Name
4052
commitID := ctx.Repo.CommitID
4153

42-
commit, err := ctx.Repo.GitRepo.GetCommit(commitID)
43-
if err != nil {
44-
if git.IsErrNotExist(err) {
45-
ctx.NotFound("Repo.GitRepo.GetCommit", err)
46-
} else {
47-
ctx.ServerError("Repo.GitRepo.GetCommit", err)
48-
}
49-
return
50-
}
51-
if len(commitID) != 40 {
52-
commitID = commit.ID.String()
53-
}
54-
5554
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
5655
treeLink := branchLink
5756
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
@@ -74,25 +73,6 @@ func RefBlame(ctx *context.Context) {
7473
}
7574
}
7675

77-
// Show latest commit info of repository in table header,
78-
// or of directory if not in root directory.
79-
latestCommit := ctx.Repo.Commit
80-
if len(ctx.Repo.TreePath) > 0 {
81-
latestCommit, err = ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
82-
if err != nil {
83-
ctx.ServerError("GetCommitByPath", err)
84-
return
85-
}
86-
}
87-
ctx.Data["LatestCommit"] = latestCommit
88-
ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit)
89-
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
90-
91-
statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), models.ListOptions{})
92-
if err != nil {
93-
log.Error("GetLatestCommitStatus: %v", err)
94-
}
95-
9676
// Get current entry user currently looking at.
9777
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
9878
if err != nil {
@@ -102,9 +82,6 @@ func RefBlame(ctx *context.Context) {
10282

10383
blob := entry.Blob()
10484

105-
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
106-
ctx.Data["LatestCommitStatuses"] = statuses
107-
10885
ctx.Data["Paths"] = paths
10986
ctx.Data["TreeLink"] = treeLink
11087
ctx.Data["TreeNames"] = treeNames
@@ -145,70 +122,112 @@ func RefBlame(ctx *context.Context) {
145122
blameParts = append(blameParts, *blamePart)
146123
}
147124

125+
// Get Topics of this repo
126+
renderRepoTopics(ctx)
127+
if ctx.Written() {
128+
return
129+
}
130+
131+
commitNames, previousCommits := processBlameParts(ctx, blameParts)
132+
if ctx.Written() {
133+
return
134+
}
135+
136+
renderBlame(ctx, blameParts, commitNames, previousCommits)
137+
138+
ctx.HTML(http.StatusOK, tplBlame)
139+
}
140+
141+
func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[string]models.UserCommit, map[string]string) {
142+
// store commit data by SHA to look up avatar info etc
148143
commitNames := make(map[string]models.UserCommit)
144+
// previousCommits contains links from SHA to parent SHA,
145+
// if parent also contains the current TreePath.
146+
previousCommits := make(map[string]string)
147+
// and as blameParts can reference the same commits multiple
148+
// times, we cache the lookup work locally
149149
commits := list.New()
150+
commitCache := map[string]*git.Commit{}
151+
commitCache[ctx.Repo.Commit.ID.String()] = ctx.Repo.Commit
150152

151153
for _, part := range blameParts {
152154
sha := part.Sha
153155
if _, ok := commitNames[sha]; ok {
154156
continue
155157
}
156158

157-
commit, err := ctx.Repo.GitRepo.GetCommit(sha)
158-
if err != nil {
159-
if git.IsErrNotExist(err) {
160-
ctx.NotFound("Repo.GitRepo.GetCommit", err)
161-
} else {
162-
ctx.ServerError("Repo.GitRepo.GetCommit", err)
159+
// find the blamePart commit, to look up parent & email address for avatars
160+
commit, ok := commitCache[sha]
161+
var err error
162+
if !ok {
163+
commit, err = ctx.Repo.GitRepo.GetCommit(sha)
164+
if err != nil {
165+
if git.IsErrNotExist(err) {
166+
ctx.NotFound("Repo.GitRepo.GetCommit", err)
167+
} else {
168+
ctx.ServerError("Repo.GitRepo.GetCommit", err)
169+
}
170+
return nil, nil
171+
}
172+
commitCache[sha] = commit
173+
}
174+
175+
// find parent commit
176+
if commit.ParentCount() > 0 {
177+
psha := commit.Parents[0]
178+
previousCommit, ok := commitCache[psha.String()]
179+
if !ok {
180+
previousCommit, _ = commit.Parent(0)
181+
if previousCommit != nil {
182+
commitCache[psha.String()] = previousCommit
183+
}
184+
}
185+
// only store parent commit ONCE, if it has the file
186+
if previousCommit != nil {
187+
if haz1, _ := previousCommit.HasFile(ctx.Repo.TreePath); haz1 {
188+
previousCommits[commit.ID.String()] = previousCommit.ID.String()
189+
}
163190
}
164-
return
165191
}
166192

167193
commits.PushBack(commit)
168194

169195
commitNames[commit.ID.String()] = models.UserCommit{}
170196
}
171197

198+
// populate commit email addresses to later look up avatars.
172199
commits = models.ValidateCommitsWithEmails(commits)
173-
174200
for e := commits.Front(); e != nil; e = e.Next() {
175201
c := e.Value.(models.UserCommit)
176-
177202
commitNames[c.ID.String()] = c
178203
}
179204

180-
// Get Topics of this repo
181-
renderRepoTopics(ctx)
182-
if ctx.Written() {
183-
return
184-
}
185-
186-
renderBlame(ctx, blameParts, commitNames)
187-
188-
ctx.HTML(http.StatusOK, tplBlame)
205+
return commitNames, previousCommits
189206
}
190207

191-
func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]models.UserCommit) {
208+
func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]models.UserCommit, previousCommits map[string]string) {
192209
repoLink := ctx.Repo.RepoLink
193210

194211
var lines = make([]string, 0)
195-
196-
var commitInfo bytes.Buffer
197-
var lineNumbers bytes.Buffer
198-
var codeLines bytes.Buffer
212+
rows := make([]*blameRow, 0)
199213

200214
var i = 0
201-
for pi, part := range blameParts {
215+
var commitCnt = 0
216+
for _, part := range blameParts {
202217
for index, line := range part.Lines {
203218
i++
204219
lines = append(lines, line)
205220

206-
var attr = ""
207-
if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
208-
attr = " bottom-line"
221+
br := &blameRow{
222+
RowNumber: i,
209223
}
224+
210225
commit := commitNames[part.Sha]
226+
previousSha := previousCommits[part.Sha]
211227
if index == 0 {
228+
// Count commit number
229+
commitCnt++
230+
212231
// User avatar image
213232
commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Data["Lang"].(string))
214233

@@ -219,33 +238,27 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
219238
avatar = string(templates.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "mr-3"))
220239
}
221240

222-
commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s"><div class="blame-data"><div class="blame-avatar">%s</div><div class="blame-message"><a href="%s/commit/%s" title="%[5]s">%[5]s</a></div><div class="blame-time">%s</div></div></div>`, attr, avatar, repoLink, part.Sha, html.EscapeString(commit.CommitMessage), commitSince))
223-
} else {
224-
commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s">&#8203;</div>`, attr))
225-
}
226-
227-
//Line number
228-
if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
229-
lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d" class="bottom-line"></span>`, i, i))
230-
} else {
231-
lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d"></span>`, i, i))
241+
br.Avatar = gotemplate.HTML(avatar)
242+
br.RepoLink = repoLink
243+
br.PartSha = part.Sha
244+
br.PreviousSha = previousSha
245+
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, previousSha, ctx.Repo.TreePath)
246+
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, part.Sha)
247+
br.CommitMessage = html.EscapeString(commit.CommitMessage)
248+
br.CommitSince = commitSince
232249
}
233250

234251
if i != len(lines)-1 {
235252
line += "\n"
236253
}
237254
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
238255
line = highlight.Code(fileName, line)
239-
line = `<code class="code-inner">` + line + `</code>`
240-
if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
241-
codeLines.WriteString(fmt.Sprintf(`<li class="L%d bottom-line" rel="L%d">%s</li>`, i, i, line))
242-
} else {
243-
codeLines.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, i, i, line))
244-
}
256+
257+
br.Code = gotemplate.HTML(line)
258+
rows = append(rows, br)
245259
}
246260
}
247261

248-
ctx.Data["BlameContent"] = gotemplate.HTML(codeLines.String())
249-
ctx.Data["BlameCommitInfo"] = gotemplate.HTML(commitInfo.String())
250-
ctx.Data["BlameLineNums"] = gotemplate.HTML(lineNumbers.String())
262+
ctx.Data["BlameRows"] = rows
263+
ctx.Data["CommitCnt"] = commitCnt
251264
}

templates/repo/blame.tmpl

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,40 @@
2323
<div class="file-view code-view">
2424
<table>
2525
<tbody>
26-
<tr>
27-
<td class="lines-commit">{{.BlameCommitInfo}}</td>
28-
<td class="lines-num">{{.BlameLineNums}}</td>
29-
<td class="lines-code"><code class="chroma"><ol class="linenums">{{.BlameContent}}</ol></code></td>
30-
</tr>
26+
{{range $row := .BlameRows}}
27+
<tr class="{{if and (gt $.CommitCnt 1) ($row.CommitMessage)}}top-line-blame{{end}}">
28+
<td class="lines-commit">
29+
<div class="blame-info">
30+
<div class="blame-data">
31+
<div class="blame-avatar">
32+
{{$row.Avatar}}
33+
</div>
34+
<div class="blame-message">
35+
<a href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}">
36+
{{$row.CommitMessage}}
37+
</a>
38+
</div>
39+
<div class="blame-time">
40+
{{$row.CommitSince}}
41+
</div>
42+
</div>
43+
</div>
44+
</td>
45+
<td class="lines-blame-btn">
46+
{{if $row.PreviousSha}}
47+
<a href="{{$row.PreviousShaURL}}" class="poping up" data-content='{{$.i18n.Tr "repo.blame_prior"}}' data-variation="tiny inverted">
48+
{{svg "octicon-versions"}}
49+
</a>
50+
{{end}}
51+
</td>
52+
<td class="lines-num">
53+
<span id="L{{$row.RowNumber}}" data-line-number="{{$row.RowNumber}}"></span>
54+
</td>
55+
<td rel="L{{$row.RowNumber}}" rel="L{{$row.RowNumber}}" class="lines-code blame-code chroma">
56+
<code class="code-inner pl-3">{{$row.Code}}</code>
57+
</td>
58+
</tr>
59+
{{end}}
3160
</tbody>
3261
</table>
3362
</div>

web_src/js/index.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2283,36 +2283,50 @@ function initCodeView() {
22832283
const $select = $(this);
22842284
let $list;
22852285
if ($('div.blame').length) {
2286-
$list = $('.code-view td.lines-code li');
2286+
$list = $('.code-view td.lines-code.blame-code');
22872287
} else {
22882288
$list = $('.code-view td.lines-code');
22892289
}
22902290
selectRange($list, $list.filter(`[rel=${$select.attr('id')}]`), (e.shiftKey ? $list.filter('.active').eq(0) : null));
22912291
deSelect();
2292-
showLineButton();
2292+
2293+
// show code view menu marker (don't show in blame page)
2294+
if ($('div.blame').length === 0) {
2295+
showLineButton();
2296+
}
22932297
});
22942298

22952299
$(window).on('hashchange', () => {
22962300
let m = window.location.hash.match(/^#(L\d+)-(L\d+)$/);
22972301
let $list;
22982302
if ($('div.blame').length) {
2299-
$list = $('.code-view td.lines-code li');
2303+
$list = $('.code-view td.lines-code.blame-code');
23002304
} else {
23012305
$list = $('.code-view td.lines-code');
23022306
}
23032307
let $first;
23042308
if (m) {
23052309
$first = $list.filter(`[rel=${m[1]}]`);
23062310
selectRange($list, $first, $list.filter(`[rel=${m[2]}]`));
2307-
showLineButton();
2311+
2312+
// show code view menu marker (don't show in blame page)
2313+
if ($('div.blame').length === 0) {
2314+
showLineButton();
2315+
}
2316+
23082317
$('html, body').scrollTop($first.offset().top - 200);
23092318
return;
23102319
}
23112320
m = window.location.hash.match(/^#(L|n)(\d+)$/);
23122321
if (m) {
23132322
$first = $list.filter(`[rel=L${m[2]}]`);
23142323
selectRange($list, $first);
2315-
showLineButton();
2324+
2325+
// show code view menu marker (don't show in blame page)
2326+
if ($('div.blame').length === 0) {
2327+
showLineButton();
2328+
}
2329+
23162330
$('html, body').scrollTop($first.offset().top - 200);
23172331
}
23182332
}).trigger('hashchange');
@@ -2911,7 +2925,6 @@ function selectRange($list, $select, $from) {
29112925
} else {
29122926
$issue.attr('href', `${$issue.attr('href')}%23L${a}-L${b}`);
29132927
}
2914-
29152928
return;
29162929
}
29172930
}

0 commit comments

Comments
 (0)