Skip to content

Commit

Permalink
Link to previous blames in file blame page (go-gitea#16259)
Browse files Browse the repository at this point in the history
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 <rogerluo410@gmail.com>
  • Loading branch information
2 people authored and AbdulrhmnGhanem committed Aug 10, 2021
1 parent 6c2eec8 commit 0fea95c
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 101 deletions.
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ delete_preexisting_label = Delete
delete_preexisting = Delete pre-existing files
delete_preexisting_content = Delete files in %s
delete_preexisting_success = Deleted unadopted files in %s
blame_prior = View blame prior to this change

transfer.accept = Accept Transfer
transfer.accept_desc = Transfer to "%s"
Expand Down
179 changes: 96 additions & 83 deletions routers/web/repo/blame.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package repo

import (
"bytes"
"container/list"
"fmt"
"html"
Expand All @@ -18,7 +17,6 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
)
Expand All @@ -27,6 +25,20 @@ const (
tplBlame base.TplName = "repo/home"
)

type blameRow struct {
RowNumber int
Avatar gotemplate.HTML
RepoLink string
PartSha string
PreviousSha string
PreviousShaURL string
IsFirstCommit bool
CommitURL string
CommitMessage string
CommitSince gotemplate.HTML
Code gotemplate.HTML
}

// RefBlame render blame page
func RefBlame(ctx *context.Context) {
fileName := ctx.Repo.TreePath
Expand All @@ -39,19 +51,6 @@ func RefBlame(ctx *context.Context) {
repoName := ctx.Repo.Repository.Name
commitID := ctx.Repo.CommitID

commit, err := ctx.Repo.GitRepo.GetCommit(commitID)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("Repo.GitRepo.GetCommit", err)
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
}
return
}
if len(commitID) != 40 {
commitID = commit.ID.String()
}

branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treeLink := branchLink
rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
Expand All @@ -74,25 +73,6 @@ func RefBlame(ctx *context.Context) {
}
}

// Show latest commit info of repository in table header,
// or of directory if not in root directory.
latestCommit := ctx.Repo.Commit
if len(ctx.Repo.TreePath) > 0 {
latestCommit, err = ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
}
}
ctx.Data["LatestCommit"] = latestCommit
ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit)
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)

statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), models.ListOptions{})
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}

// Get current entry user currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
Expand All @@ -102,9 +82,6 @@ func RefBlame(ctx *context.Context) {

blob := entry.Blob()

ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
ctx.Data["LatestCommitStatuses"] = statuses

ctx.Data["Paths"] = paths
ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames
Expand Down Expand Up @@ -145,70 +122,112 @@ func RefBlame(ctx *context.Context) {
blameParts = append(blameParts, *blamePart)
}

// Get Topics of this repo
renderRepoTopics(ctx)
if ctx.Written() {
return
}

commitNames, previousCommits := processBlameParts(ctx, blameParts)
if ctx.Written() {
return
}

renderBlame(ctx, blameParts, commitNames, previousCommits)

ctx.HTML(http.StatusOK, tplBlame)
}

func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[string]models.UserCommit, map[string]string) {
// store commit data by SHA to look up avatar info etc
commitNames := make(map[string]models.UserCommit)
// previousCommits contains links from SHA to parent SHA,
// if parent also contains the current TreePath.
previousCommits := make(map[string]string)
// and as blameParts can reference the same commits multiple
// times, we cache the lookup work locally
commits := list.New()
commitCache := map[string]*git.Commit{}
commitCache[ctx.Repo.Commit.ID.String()] = ctx.Repo.Commit

for _, part := range blameParts {
sha := part.Sha
if _, ok := commitNames[sha]; ok {
continue
}

commit, err := ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("Repo.GitRepo.GetCommit", err)
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
// find the blamePart commit, to look up parent & email address for avatars
commit, ok := commitCache[sha]
var err error
if !ok {
commit, err = ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("Repo.GitRepo.GetCommit", err)
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
}
return nil, nil
}
commitCache[sha] = commit
}

// find parent commit
if commit.ParentCount() > 0 {
psha := commit.Parents[0]
previousCommit, ok := commitCache[psha.String()]
if !ok {
previousCommit, _ = commit.Parent(0)
if previousCommit != nil {
commitCache[psha.String()] = previousCommit
}
}
// only store parent commit ONCE, if it has the file
if previousCommit != nil {
if haz1, _ := previousCommit.HasFile(ctx.Repo.TreePath); haz1 {
previousCommits[commit.ID.String()] = previousCommit.ID.String()
}
}
return
}

commits.PushBack(commit)

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

// populate commit email addresses to later look up avatars.
commits = models.ValidateCommitsWithEmails(commits)

for e := commits.Front(); e != nil; e = e.Next() {
c := e.Value.(models.UserCommit)

commitNames[c.ID.String()] = c
}

// Get Topics of this repo
renderRepoTopics(ctx)
if ctx.Written() {
return
}

renderBlame(ctx, blameParts, commitNames)

ctx.HTML(http.StatusOK, tplBlame)
return commitNames, previousCommits
}

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

var lines = make([]string, 0)

var commitInfo bytes.Buffer
var lineNumbers bytes.Buffer
var codeLines bytes.Buffer
rows := make([]*blameRow, 0)

var i = 0
for pi, part := range blameParts {
var commitCnt = 0
for _, part := range blameParts {
for index, line := range part.Lines {
i++
lines = append(lines, line)

var attr = ""
if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
attr = " bottom-line"
br := &blameRow{
RowNumber: i,
}

commit := commitNames[part.Sha]
previousSha := previousCommits[part.Sha]
if index == 0 {
// Count commit number
commitCnt++

// User avatar image
commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Data["Lang"].(string))

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

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))
} else {
commitInfo.WriteString(fmt.Sprintf(`<div class="blame-info%s">&#8203;</div>`, attr))
}

//Line number
if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d" class="bottom-line"></span>`, i, i))
} else {
lineNumbers.WriteString(fmt.Sprintf(`<span id="L%d" data-line-number="%d"></span>`, i, i))
br.Avatar = gotemplate.HTML(avatar)
br.RepoLink = repoLink
br.PartSha = part.Sha
br.PreviousSha = previousSha
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, previousSha, ctx.Repo.TreePath)
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, part.Sha)
br.CommitMessage = html.EscapeString(commit.CommitMessage)
br.CommitSince = commitSince
}

if i != len(lines)-1 {
line += "\n"
}
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
line = highlight.Code(fileName, line)
line = `<code class="code-inner">` + line + `</code>`
if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
codeLines.WriteString(fmt.Sprintf(`<li class="L%d bottom-line" rel="L%d">%s</li>`, i, i, line))
} else {
codeLines.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, i, i, line))
}

br.Code = gotemplate.HTML(line)
rows = append(rows, br)
}
}

ctx.Data["BlameContent"] = gotemplate.HTML(codeLines.String())
ctx.Data["BlameCommitInfo"] = gotemplate.HTML(commitInfo.String())
ctx.Data["BlameLineNums"] = gotemplate.HTML(lineNumbers.String())
ctx.Data["BlameRows"] = rows
ctx.Data["CommitCnt"] = commitCnt
}
39 changes: 34 additions & 5 deletions templates/repo/blame.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,40 @@
<div class="file-view code-view">
<table>
<tbody>
<tr>
<td class="lines-commit">{{.BlameCommitInfo}}</td>
<td class="lines-num">{{.BlameLineNums}}</td>
<td class="lines-code"><code class="chroma"><ol class="linenums">{{.BlameContent}}</ol></code></td>
</tr>
{{range $row := .BlameRows}}
<tr class="{{if and (gt $.CommitCnt 1) ($row.CommitMessage)}}top-line-blame{{end}}">
<td class="lines-commit">
<div class="blame-info">
<div class="blame-data">
<div class="blame-avatar">
{{$row.Avatar}}
</div>
<div class="blame-message">
<a href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}">
{{$row.CommitMessage}}
</a>
</div>
<div class="blame-time">
{{$row.CommitSince}}
</div>
</div>
</div>
</td>
<td class="lines-blame-btn">
{{if $row.PreviousSha}}
<a href="{{$row.PreviousShaURL}}" class="poping up" data-content='{{$.i18n.Tr "repo.blame_prior"}}' data-variation="tiny inverted">
{{svg "octicon-versions"}}
</a>
{{end}}
</td>
<td class="lines-num">
<span id="L{{$row.RowNumber}}" data-line-number="{{$row.RowNumber}}"></span>
</td>
<td rel="L{{$row.RowNumber}}" rel="L{{$row.RowNumber}}" class="lines-code blame-code chroma">
<code class="code-inner pl-3">{{$row.Code}}</code>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
Expand Down
25 changes: 19 additions & 6 deletions web_src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2283,36 +2283,50 @@ function initCodeView() {
const $select = $(this);
let $list;
if ($('div.blame').length) {
$list = $('.code-view td.lines-code li');
$list = $('.code-view td.lines-code.blame-code');
} else {
$list = $('.code-view td.lines-code');
}
selectRange($list, $list.filter(`[rel=${$select.attr('id')}]`), (e.shiftKey ? $list.filter('.active').eq(0) : null));
deSelect();
showLineButton();

// show code view menu marker (don't show in blame page)
if ($('div.blame').length === 0) {
showLineButton();
}
});

$(window).on('hashchange', () => {
let m = window.location.hash.match(/^#(L\d+)-(L\d+)$/);
let $list;
if ($('div.blame').length) {
$list = $('.code-view td.lines-code li');
$list = $('.code-view td.lines-code.blame-code');
} else {
$list = $('.code-view td.lines-code');
}
let $first;
if (m) {
$first = $list.filter(`[rel=${m[1]}]`);
selectRange($list, $first, $list.filter(`[rel=${m[2]}]`));
showLineButton();

// show code view menu marker (don't show in blame page)
if ($('div.blame').length === 0) {
showLineButton();
}

$('html, body').scrollTop($first.offset().top - 200);
return;
}
m = window.location.hash.match(/^#(L|n)(\d+)$/);
if (m) {
$first = $list.filter(`[rel=L${m[2]}]`);
selectRange($list, $first);
showLineButton();

// show code view menu marker (don't show in blame page)
if ($('div.blame').length === 0) {
showLineButton();
}

$('html, body').scrollTop($first.offset().top - 200);
}
}).trigger('hashchange');
Expand Down Expand Up @@ -2911,7 +2925,6 @@ function selectRange($list, $select, $from) {
} else {
$issue.attr('href', `${$issue.attr('href')}%23L${a}-L${b}`);
}

return;
}
}
Expand Down
Loading

0 comments on commit 0fea95c

Please sign in to comment.