forked from NYANDEV/forgejo
- Add a indication to the file history if the file has been renamed, this indication contains a link to browse the history of the file further. - Added unit testing. - Added integration testing. - Resolves https://codeberg.org/forgejo/forgejo/issues/1279 (cherry picked from commit 72c297521b1830360aab4b50e37efcc7e67e0d5d) (cherry picked from commit 283f9648947f8dd2f315ecca19566ccca2b49c18) Conflicts: options/locale/locale_en-US.ini https://codeberg.org/forgejo/forgejo/pulls/1550 (cherry picked from commit 7c30af7fdee08efd02041c01abca47394a69bb8b) (cherry picked from commit f3be6eb269526a9f4ea7861189f07977f2d4a32f) (cherry picked from commit 78e1755b94c18c043e0c8f8c2849803cc8069feb) (cherry picked from commit 9f30b92009e8911c99412944bcd7cff55a7b98dc) (cherry picked from commit bb694684a4045150924c15aa5647e8e112321f02) (cherry picked from commit 721f0ccf3ea7196dbb877a6c159d23d05c37978b)
435 lines
12 KiB
Go
435 lines
12 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"
|
|
"net/http"
|
|
"strings"
|
|
|
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
|
"code.gitea.io/gitea/models/db"
|
|
git_model "code.gitea.io/gitea/models/git"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/base"
|
|
"code.gitea.io/gitea/modules/charset"
|
|
"code.gitea.io/gitea/modules/context"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/gitgraph"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/services/gitdiff"
|
|
git_service "code.gitea.io/gitea/services/repository"
|
|
)
|
|
|
|
const (
|
|
tplCommits base.TplName = "repo/commits"
|
|
tplGraph base.TplName = "repo/graph"
|
|
tplGraphDiv base.TplName = "repo/graph/div"
|
|
tplCommitPage base.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":
|
|
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("Commit not found", nil)
|
|
return
|
|
}
|
|
ctx.Data["PageIsViewCode"] = true
|
|
|
|
commitsCount, err := ctx.Repo.GetCommitsCount()
|
|
if err != nil {
|
|
ctx.ServerError("GetCommitsCount", err)
|
|
return
|
|
}
|
|
|
|
page := ctx.FormInt("page")
|
|
if page <= 1 {
|
|
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"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
|
|
|
|
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
|
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
|
ctx.Data["CommitCount"] = commitsCount
|
|
|
|
pager := context.NewPagination(int(commitsCount), pageSize, page, 5)
|
|
pager.SetDefaultParams(ctx)
|
|
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")
|
|
|
|
commitsCount, err := ctx.Repo.GetCommitsCount()
|
|
if err != nil {
|
|
ctx.ServerError("GetCommitsCount", err)
|
|
return
|
|
}
|
|
|
|
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{}
|
|
branches = []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
|
|
|
|
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
|
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
|
ctx.Data["CommitCount"] = commitsCount
|
|
|
|
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
|
|
paginator.AddParam(ctx, "mode", "Mode")
|
|
paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
|
|
for _, branch := range branches {
|
|
paginator.AddParamString("branch", branch)
|
|
}
|
|
for _, file := range files {
|
|
paginator.AddParamString("file", file)
|
|
}
|
|
ctx.Data["Page"] = paginator
|
|
if ctx.FormBool("div-only") {
|
|
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.BranchNameSubURL())
|
|
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"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
|
|
|
|
ctx.Data["Keyword"] = query
|
|
if all {
|
|
ctx.Data["All"] = "checked"
|
|
}
|
|
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
|
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
|
ctx.HTML(http.StatusOK, tplCommits)
|
|
}
|
|
|
|
// FileHistory show a file's reversions
|
|
func FileHistory(ctx *context.Context) {
|
|
ctx.Data["IsRepoToolbarCommits"] = true
|
|
|
|
fileName := ctx.Repo.TreePath
|
|
if len(fileName) == 0 {
|
|
Commits(ctx)
|
|
return
|
|
}
|
|
|
|
commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefName, fileName)
|
|
if err != nil {
|
|
ctx.ServerError("FileCommitsCount", err)
|
|
return
|
|
} else if commitsCount == 0 {
|
|
ctx.NotFound("FileCommitsCount", nil)
|
|
return
|
|
}
|
|
|
|
page := ctx.FormInt("page")
|
|
if page <= 1 {
|
|
page = 1
|
|
}
|
|
|
|
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
|
|
git.CommitsByFileAndRangeOptions{
|
|
Revision: ctx.Repo.RefName,
|
|
File: fileName,
|
|
Page: page,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("CommitsByFileAndRange", err)
|
|
return
|
|
}
|
|
oldestCommit := commits[len(commits)-1]
|
|
|
|
renamedFiles, err := git.GetCommitFileRenames(ctx, ctx.Repo.GitRepo.Path, oldestCommit.ID.String())
|
|
if err != nil {
|
|
ctx.ServerError("GetCommitFileRenames", err)
|
|
return
|
|
}
|
|
|
|
for _, renames := range renamedFiles {
|
|
if renames[1] == fileName {
|
|
ctx.Data["OldFilename"] = renames[0]
|
|
ctx.Data["OldFilenameHistory"] = fmt.Sprintf("%s/commits/commit/%s/%s", ctx.Repo.RepoLink, oldestCommit.ID.String(), renames[0])
|
|
break
|
|
}
|
|
}
|
|
|
|
ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
|
|
|
|
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
|
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
|
ctx.Data["FileName"] = fileName
|
|
ctx.Data["CommitCount"] = commitsCount
|
|
|
|
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
|
|
pager.SetDefaultParams(ctx)
|
|
ctx.Data["Page"] = pager
|
|
|
|
ctx.HTML(http.StatusOK, tplCommits)
|
|
}
|
|
|
|
func LoadBranchesAndTags(ctx *context.Context) {
|
|
response, err := git_service.LoadBranchesAndTags(ctx, ctx.Repo, ctx.Params("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.Params("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.Params(":sha")
|
|
var (
|
|
gitRepo *git.Repository
|
|
err error
|
|
)
|
|
|
|
if ctx.Data["PageIsWiki"] != nil {
|
|
gitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
|
|
if err != nil {
|
|
ctx.ServerError("Repo.GitRepo.GetCommit", err)
|
|
return
|
|
}
|
|
defer gitRepo.Close()
|
|
} else {
|
|
gitRepo = ctx.Repo.GitRepo
|
|
}
|
|
|
|
commit, err := 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) != git.SHAFullLength {
|
|
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.GetDiff(gitRepo, &gitdiff.DiffOptions{
|
|
AfterCommitID: commitID,
|
|
SkipTo: ctx.FormString("skip-to"),
|
|
MaxLines: maxLines,
|
|
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
|
|
MaxFiles: maxFiles,
|
|
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
|
|
}, files...)
|
|
if err != nil {
|
|
ctx.NotFound("GetDiff", err)
|
|
return
|
|
}
|
|
|
|
parents := make([]string, commit.ParentCount())
|
|
for i := 0; i < commit.ParentCount(); i++ {
|
|
sha, err := commit.ParentID(i)
|
|
if err != nil {
|
|
ctx.NotFound("repo.Diff", err)
|
|
return
|
|
}
|
|
parents[i] = sha.String()
|
|
}
|
|
|
|
ctx.Data["CommitID"] = commitID
|
|
ctx.Data["AfterCommitID"] = commitID
|
|
ctx.Data["Username"] = userName
|
|
ctx.Data["Reponame"] = repoName
|
|
|
|
var parentCommit *git.Commit
|
|
if commit.ParentCount() > 0 {
|
|
parentCommit, err = gitRepo.GetCommit(parents[0])
|
|
if err != nil {
|
|
ctx.NotFound("GetParentCommit", err)
|
|
return
|
|
}
|
|
}
|
|
setCompareContext(ctx, parentCommit, commit, userName, repoName)
|
|
ctx.Data["Title"] = commit.Summary() + " ยท " + base.ShortSha(commitID)
|
|
ctx.Data["Commit"] = commit
|
|
ctx.Data["Diff"] = diff
|
|
|
|
statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptions{ListAll: true})
|
|
if err != nil {
|
|
log.Error("GetLatestCommitStatus: %v", err)
|
|
}
|
|
|
|
ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses)
|
|
ctx.Data["CommitStatuses"] = statuses
|
|
|
|
verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
|
|
ctx.Data["Verification"] = verification
|
|
ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, commit)
|
|
ctx.Data["Parents"] = parents
|
|
ctx.Data["DiffNotAvailable"] = diff.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["Note"] = string(charset.ToUTF8WithFallback(note.Message))
|
|
ctx.Data["NoteCommit"] = note.Commit
|
|
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
|
|
}
|
|
|
|
ctx.Data["BranchName"], err = commit.GetBranchName()
|
|
if err != nil {
|
|
ctx.ServerError("commit.GetBranchName", err)
|
|
return
|
|
}
|
|
|
|
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 := git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
|
|
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.Params(":sha"),
|
|
git.RawDiffType(ctx.Params(":ext")),
|
|
ctx.Resp,
|
|
); err != nil {
|
|
if git.IsErrNotExist(err) {
|
|
ctx.NotFound("GetRawDiff",
|
|
errors.New("commit "+ctx.Params(":sha")+" does not exist."))
|
|
return
|
|
}
|
|
ctx.ServerError("GetRawDiff", err)
|
|
return
|
|
}
|
|
}
|