Files
gitea/routers/web/repo/commit.go
bircni 27e33b7ba1 fix: incorrect co-author detection on commit page (#38386)
The commit page built its "co-authored by" list from
`AllParticipantIdentities()[1:]`, which only drops the author and leaves
the committer in the list even though the committer is already shown
separately as "committed by". This caused the committer to be wrongly
rendered as a co-author (issue #38384): a commit authored by
`silverwind`, committed by `bircni`, with a `Co-authored-by: silverwind`
trailer displayed "co-authored by bircni" instead of the actual trailer
identity. This adds a `Commit.CoAuthorIdentities()` method that excludes
both the author and the committer, uses it on the commit page, and
covers the fix with a regression test.

Closes #38384
2026-07-09 22:31:14 +02:00

481 lines
14 KiB
Go

// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"fmt"
"html/template"
"net/http"
"strings"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
"gitea.dev/models/gituser"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/renderhelper"
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
"gitea.dev/modules/charset"
"gitea.dev/modules/fileicon"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
"gitea.dev/modules/markup"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
git_service "gitea.dev/services/git"
"gitea.dev/services/gitdiff"
repo_service "gitea.dev/services/repository"
"gitea.dev/services/repository/gitgraph"
)
const (
tplCommits templates.TplName = "repo/commits"
tplGraph templates.TplName = "repo/graph"
tplGraphDiv templates.TplName = "repo/graph/div"
tplCommitPage templates.TplName = "repo/commit_page"
)
// RefCommits render commits page
func RefCommits(ctx *context.Context) {
switch {
case len(ctx.Repo.TreePath) == 0:
Commits(ctx)
case ctx.Repo.TreePath == "search": // FIXME: legacy dirty design, it conflicts with the FileHistory
SearchCommits(ctx)
default:
FileHistory(ctx)
}
}
// Commits render branch's commits
func Commits(ctx *context.Context) {
ctx.Data["PageIsCommits"] = true
if ctx.Repo.Commit == nil {
ctx.NotFound(nil)
return
}
ctx.Data["PageIsViewCode"] = true
commitsCount := ctx.Repo.CommitsCount
page := max(ctx.FormInt("page"), 1)
pageSize := ctx.FormInt("limit")
if pageSize <= 0 {
pageSize = setting.Git.CommitsRangeSize
}
// Both `git log branchName` and `git log commitId` work.
commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "", "", "")
if err != nil {
ctx.ServerError("CommitsByRange", err)
return
}
ctx.Data["Commits"], err = processGitCommits(ctx, commits)
if err != nil {
ctx.ServerError("processGitCommits", err)
return
}
commitIDs := make([]string, 0, len(commits))
for _, c := range commits {
commitIDs = append(commitIDs, c.ID.String())
}
commitsTagsMap, err := repo_model.FindTagsByCommitIDs(ctx, ctx.Repo.Repository.ID, commitIDs...)
if err != nil {
log.Error("FindTagsByCommitIDs: %v", err)
ctx.Flash.Error(ctx.Tr("internal_error_skipped", "FindTagsByCommitIDs"))
} else {
ctx.Data["CommitsTagsMap"] = commitsTagsMap
}
ctx.Data["CommitCount"] = commitsCount
pager := context.NewPagination(commitsCount, pageSize, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplCommits)
}
// Graph render commit graph - show commits from all branches.
func Graph(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.commit_graph")
ctx.Data["PageIsCommits"] = true
ctx.Data["PageIsViewCode"] = true
mode := strings.ToLower(ctx.FormTrim("mode"))
if mode != "monochrome" {
mode = "color"
}
ctx.Data["Mode"] = mode
hidePRRefs := ctx.FormBool("hide-pr-refs")
ctx.Data["HidePRRefs"] = hidePRRefs
branches := ctx.FormStrings("branch")
realBranches := make([]string, len(branches))
copy(realBranches, branches)
for i, branch := range realBranches {
if strings.HasPrefix(branch, "--") {
realBranches[i] = git.BranchPrefix + branch
}
}
ctx.Data["SelectedBranches"] = realBranches
files := ctx.FormStrings("file")
graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files)
if err != nil {
log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err)
realBranches = []string{}
graphCommitsCount, err = ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files)
if err != nil {
ctx.ServerError("GetCommitGraphsCount", err)
return
}
}
page := ctx.FormInt("page")
graph, err := gitgraph.GetCommitGraph(ctx.Repo.GitRepo, page, 0, hidePRRefs, realBranches, files)
if err != nil {
ctx.ServerError("GetCommitGraph", err)
return
}
if err := graph.LoadAndProcessCommits(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo); err != nil {
ctx.ServerError("LoadAndProcessCommits", err)
return
}
ctx.Data["Graph"] = graph
gitRefs, err := ctx.Repo.GitRepo.GetRefs()
if err != nil {
ctx.ServerError("GitRepo.GetRefs", err)
return
}
ctx.Data["AllRefs"] = gitRefs
divOnly := ctx.FormBool("div-only")
queryParams := ctx.Req.URL.Query()
queryParams.Del("div-only")
paginator := context.NewPagination(graphCommitsCount, setting.UI.GraphMaxCommitNum, page, 5)
paginator.AddParamFromQuery(queryParams)
ctx.Data["Page"] = paginator
if divOnly {
ctx.HTML(http.StatusOK, tplGraphDiv)
return
}
ctx.HTML(http.StatusOK, tplGraph)
}
// SearchCommits render commits filtered by keyword
func SearchCommits(ctx *context.Context) {
ctx.Data["PageIsCommits"] = true
ctx.Data["PageIsViewCode"] = true
query := ctx.FormTrim("q")
if len(query) == 0 {
ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.RefTypeNameSubURL())
return
}
all := ctx.FormBool("all")
opts := git.NewSearchCommitsOptions(query, all)
commits, err := ctx.Repo.Commit.SearchCommits(opts)
if err != nil {
ctx.ServerError("SearchCommits", err)
return
}
ctx.Data["CommitCount"] = len(commits)
ctx.Data["Commits"], err = processGitCommits(ctx, commits)
if err != nil {
ctx.ServerError("processGitCommits", err)
return
}
ctx.Data["Keyword"] = query
if all {
ctx.Data["All"] = true
}
ctx.HTML(http.StatusOK, tplCommits)
}
// FileHistory show a file's reversions
func FileHistory(ctx *context.Context) {
if ctx.Repo.TreePath == "" {
Commits(ctx)
return
}
followRename := ctx.FormBool("follow-rename")
ctx.Data["ShowFollowRename"] = true
ctx.Data["FollowRenameChecked"] = followRename
page := max(ctx.FormInt("page"), 1)
commits, hasMore, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName
File: ctx.Repo.TreePath,
Page: page,
FollowRename: followRename,
})
if err != nil {
ctx.ServerError("CommitsByFileAndRange", err)
return
}
var commitsCount int64
if followRename {
// there is no quick method to know the total count when "follow rename"
commitsCount = -1
} else {
commitsCount, err = gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("FileCommitsCount", err)
return
}
}
if len(commits) == 0 {
ctx.NotFound(nil)
return
}
ctx.Data["FileTreePath"] = ctx.Repo.TreePath
ctx.Data["CommitCount"] = commitsCount
ctx.Data["Commits"], err = processGitCommits(ctx, commits)
if err != nil {
ctx.ServerError("processGitCommits", err)
return
}
pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5)
if commitsCount == -1 {
pager.WithUnlimitedPaging(len(commits), hasMore)
}
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplCommits)
}
func LoadBranchesAndTags(ctx *context.Context) {
response, err := repo_service.LoadBranchesAndTags(ctx, ctx.Repo, ctx.PathParam("sha"))
if err == nil {
ctx.JSON(http.StatusOK, response)
return
}
ctx.NotFoundOrServerError(fmt.Sprintf("could not load branches and tags the commit %s belongs to", ctx.PathParam("sha")), git.IsErrNotExist, err)
}
// Diff show different from current commit to previous commit
func Diff(ctx *context.Context) {
ctx.Data["PageIsDiff"] = true
userName := ctx.Repo.Owner.Name
repoName := ctx.Repo.Repository.Name
commitID := ctx.PathParam("sha")
diffBlobExcerptData := &gitdiff.DiffBlobExcerptData{
BaseLink: ctx.Repo.RepoLink + "/blob_excerpt",
DiffStyle: GetDiffViewStyle(ctx),
AfterCommitID: commitID,
}
gitRepo := ctx.Repo.GitRepo
var gitRepoStore gitrepo.Repository = ctx.Repo.Repository
if ctx.Data["PageIsWiki"] != nil {
var err error
gitRepoStore = ctx.Repo.Repository.WikiStorageRepo()
gitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, gitRepoStore)
if err != nil {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
return
}
diffBlobExcerptData.BaseLink = ctx.Repo.RepoLink + "/wiki/blob_excerpt"
}
commit, err := gitRepo.GetCommit(commitID)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(err)
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
}
return
}
if len(commitID) != commit.ID.Type().FullLength() {
commitID = commit.ID.String()
}
fileOnly := ctx.FormBool("file-only")
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
files := ctx.FormStrings("files")
if fileOnly && (len(files) == 2 || len(files) == 1) {
maxLines, maxFiles = -1, -1
}
diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, &gitdiff.DiffOptions{
AfterCommitID: commitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: maxFiles,
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(GetWhitespaceBehavior(ctx)),
}, files...)
if err != nil {
ctx.NotFound(err)
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, gitRepoStore, gitRepo, "", commitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
return
}
ctx.Data["DiffShortStat"] = diffShortStat
parents := make([]string, commit.ParentCount())
for i := 0; i < commit.ParentCount(); i++ {
sha, err := commit.ParentID(i)
if err != nil {
ctx.NotFound(err)
return
}
parents[i] = sha.String()
}
ctx.Data["CommitID"] = commitID
ctx.Data["AfterCommitID"] = commitID
var parentCommit *git.Commit
var parentCommitID string
if commit.ParentCount() > 0 {
parentCommit, err = gitRepo.GetCommit(parents[0])
if err != nil {
ctx.NotFound(err)
return
}
parentCommitID = parentCommit.ID.String()
}
setCompareContext(ctx, parentCommit, commit, userName, repoName)
ctx.Data["Title"] = commit.MessageTitle() + " · " + base.ShortSha(commitID)
ctx.Data["Commit"] = commit
ctx.Data["Diff"] = diff
ctx.Data["DiffBlobExcerptData"] = diffBlobExcerptData
if !fileOnly {
diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, parentCommitID, commitID)
if err != nil {
ctx.ServerError("GetDiffTree", err)
return
}
renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
}
statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
if !ctx.Repo.Permission.CanRead(unit_model.TypeActions) {
git_model.CommitStatusesHideActionsURL(ctx, statuses)
}
ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses)
ctx.Data["CommitStatuses"] = statuses
verification := asymkey_service.ParseCommitWithSignature(ctx, commit)
ctx.Data["Verification"] = verification
ctx.Data["Author"] = user_model.GetUserByGitAuthor(ctx, commit)
ctx.Data["CommitOtherParticipants"] = gituser.BuildAvatarStackData(ctx, commit.CoAuthorIdentities(), nil).Participants
ctx.Data["Parents"] = parents
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
}, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}
note := &git.Note{}
err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note)
if err == nil {
ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = user_model.GetUserByGitAuthor(ctx, note.Commit)
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefSubURL: "commit/" + util.PathEscapeSegments(commitID)})
htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
ctx.Data["NoteRendered"] = markup.PostProcessCommitMessage(rctx, htmlMessage)
} else if !git.IsErrNotExist(err) {
log.Error("GetNote: %v", err)
}
pr, _ := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, commitID)
if pr != nil {
ctx.Data["MergedPRIssueNumber"] = pr.Index
}
ctx.HTML(http.StatusOK, tplCommitPage)
}
// RawDiff dumps diff results of repository in given commit ID to io.Writer
func RawDiff(ctx *context.Context) {
var gitRepo *git.Repository
if ctx.Data["PageIsWiki"] != nil {
wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
ctx.ServerError("OpenRepository", err)
return
}
defer wikiRepo.Close()
gitRepo = wikiRepo
} else {
gitRepo = ctx.Repo.GitRepo
if gitRepo == nil {
ctx.ServerError("GitRepo not open", fmt.Errorf("no open git repo for '%s'", ctx.Repo.Repository.FullName()))
return
}
}
if err := git.GetRawDiff(
gitRepo,
ctx.PathParam("sha"),
git.RawDiffType(ctx.PathParam("ext")),
ctx.Resp,
); err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
return
}
ctx.ServerError("GetRawDiff", err)
return
}
}
func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_model.SignCommitWithStatuses, error) {
commits, err := git_service.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository, ctx.Repo.RefFullName)
if err != nil {
return nil, err
}
if !ctx.Repo.Permission.CanRead(unit_model.TypeActions) {
for _, commit := range commits {
if commit.Status == nil {
continue
}
commit.Status.HideActionsURL(ctx)
git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
}
}
return commits, nil
}