forgejo/routers/web/repo/commit.go
Matthew Walowski f766b00293
Add ability to specify '--not' from GetAllCommits (#24409)
For my specific use case, I'd like to get all commits that are on one
branch but NOT on the other branch.

For instance, I'd like to get all the commits on `Branch1` that are not
also on `master` (I.e. all commits that were made after `Branch1` was
created).

This PR adds a `not` query param that gets passed down to the `git log`
command to allow the user to exclude items from `GetAllCommits`.

See [git
documentation](https://git-scm.com/docs/git-log#Documentation/git-log.txt---not)

---------

Co-authored-by: Giteabot <teabot@gitea.io>
2023-04-29 08:34:14 -04:00

413 lines
11 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"
)
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
ctx.Data["RefName"] = ctx.Repo.RefName
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
ctx.Data["RefName"] = ctx.Repo.RefName
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.Data["RefName"] = ctx.Repo.RefName
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(ctx.Repo.RefName, fileName, page)
if err != nil {
ctx.ServerError("CommitsByFileAndRange", 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["FileName"] = fileName
ctx.Data["CommitCount"] = commitsCount
ctx.Data["RefName"] = ctx.Repo.RefName
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplCommits)
}
// 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{})
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.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.Data["TagName"], err = commit.GetTagName()
if err != nil {
ctx.ServerError("commit.GetTagName", 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
}
}