diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e0ece8f9f..dcdfa611e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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" diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 1a3e1dcb9..4ade9e9a9 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -5,7 +5,6 @@ package repo import ( - "bytes" "container/list" "fmt" "html" @@ -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" ) @@ -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 @@ -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() @@ -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 { @@ -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 @@ -145,8 +122,33 @@ 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 @@ -154,14 +156,38 @@ func RefBlame(ctx *context.Context) { 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) @@ -169,46 +195,39 @@ func RefBlame(ctx *context.Context) { 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)) @@ -219,16 +238,14 @@ 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(`
` + line + `
`
- if len(part.Lines)-1 == index && len(blameParts)-1 != pi {
- codeLines.WriteString(fmt.Sprintf(`{{.BlameCommitInfo}} | -{{.BlameLineNums}} | -
|
- |
+
+
+
+
+
+ {{$row.Avatar}}
+
+
+
+ {{$row.CommitSince}}
+
+ |
+ + {{if $row.PreviousSha}} + + {{svg "octicon-versions"}} + + {{end}} + | ++ + | +
+ {{$row.Code}}
+ |
+