mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-15 20:40:52 +09:00
27e33b7ba1
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
481 lines
14 KiB
Go
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
|
|
}
|