diff --git a/modules/git/commit.go b/modules/git/commit.go index 1c1648eb8b85f..0046200448f9d 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -162,17 +162,25 @@ func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, file // CommitsCountOptions the options when counting commits type CommitsCountOptions struct { - RepoPath string - Not string - Revision []string - RelPath []string - Since string - Until string + RepoPath string + Not string + Revision []string + RelPath []string + Since string + Until string + FollowRename bool } // CommitsCount returns number of total commits of until given revision. func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) { - cmd := NewCommand("rev-list", "--count") + var cmd *Command + followRename := len(opts.RelPath) > 0 && opts.FollowRename + + if followRename { + cmd = NewCommand("--no-pager", "log", "--pretty=format:%H") + } else { + cmd = NewCommand("rev-list", "--count") + } cmd.AddDynamicArguments(opts.Revision...) @@ -181,6 +189,9 @@ func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) } if len(opts.RelPath) > 0 { + if opts.FollowRename { + cmd.AddOptionValues("--follow") + } cmd.AddDashesAndList(opts.RelPath...) } @@ -188,7 +199,9 @@ func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) if err != nil { return 0, err } - + if followRename { + return int64(len(strings.Split(stdout, "\n"))), nil + } return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) } diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 4066a1ca7ba1f..dd4987aa97e3e 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -205,22 +205,29 @@ func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bo } // FileCommitsCount return the number of files at a revision -func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) { +func (repo *Repository) FileCommitsCount(revision, file string, followRename ...bool) (int64, error) { + _followRename := false + if len(followRename) > 0 { + _followRename = followRename[0] + } + return CommitsCount(repo.Ctx, CommitsCountOptions{ - RepoPath: repo.Path, - Revision: []string{revision}, - RelPath: []string{file}, + RepoPath: repo.Path, + Revision: []string{revision}, + RelPath: []string{file}, + FollowRename: _followRename, }) } type CommitsByFileAndRangeOptions struct { - Revision string - File string - Not string - Page int - Since string - Until string + Revision string + File string + Not string + Page int + Since string + Until string + FollowRename bool } // CommitsByFileAndRange return the commits according revision file and the page @@ -232,9 +239,18 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) }() go func() { stderr := strings.Builder{} - gitCmd := NewCommand("rev-list"). - AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). + var gitCmd *Command + + if !opts.FollowRename { + gitCmd = NewCommand("rev-list") + } else { + gitCmd = NewCommand("--no-pager", "log"). + AddOptionFormat("--pretty=format:%%H"). + AddOptionFormat("--follow") + } + gitCmd.AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize) + gitCmd.AddDynamicArguments(opts.Revision) if opts.Not != "" { @@ -253,7 +269,8 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) Stdout: stdoutWriter, Stderr: &stderr, }) - if err != nil { + + if err != nil && !(opts.FollowRename && err == io.ErrUnexpectedEOF) { _ = stdoutWriter.CloseWithError(ConcatenateError(err, (&stderr).String())) } else { _ = stdoutWriter.Close() @@ -270,7 +287,7 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) shaline := make([]byte, length+1) for { n, err := io.ReadFull(stdoutReader, shaline) - if err != nil || n < length { + if (err != nil && !(opts.FollowRename && err == io.ErrUnexpectedEOF)) || n < length { if err == io.EOF { err = nil } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 78ffe1c61846f..7b250ff2bef4b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1408,6 +1408,7 @@ editor.fork_branch_exists = Branch "%s" already exists in your fork, please choo commits.desc = Browse source code change history. commits.commits = Commits +commits.history_follow_rename = Include renames commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories. commits.nothing_to_compare = These branches are equal. commits.search.tooltip = You can prefix keywords with "author:", "committer:", "after:", or "before:", e.g. "revert author:Alice before:2019-01-13". diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index b3af138461c01..5a57d64496215 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -213,12 +213,14 @@ func SearchCommits(ctx *context.Context) { // FileHistory show a file's reversions func FileHistory(ctx *context.Context) { + followRename := strings.Contains(ctx.Req.RequestURI, "history_follow_rename=true") + if ctx.Repo.TreePath == "" { Commits(ctx) return } - commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath) + commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath, followRename) if err != nil { ctx.ServerError("FileCommitsCount", err) return @@ -231,9 +233,10 @@ func FileHistory(ctx *context.Context) { commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName - File: ctx.Repo.TreePath, - Page: page, + 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) diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index a0c5eacdd4b00..e6be1d40c03d1 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -8,6 +8,10 @@ {{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}} {{end}} +
+ + +
{{if .IsDiffCompare}}
{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}} diff --git a/web_src/js/features/repo-commit.ts b/web_src/js/features/repo-commit.ts index 98ec2328ec5f4..81ba3f5784cb4 100644 --- a/web_src/js/features/repo-commit.ts +++ b/web_src/js/features/repo-commit.ts @@ -24,3 +24,20 @@ export function initCommitStatuses() { }); }); } + +export function initCommitFileHistoryFollowRename() { + const checkbox : HTMLInputElement | null = document.querySelector('input[name=history-enable-follow-renames]'); + + if (!checkbox) { + return; + } + const url = new URL(window.location.toString()); + checkbox.checked = url.searchParams.has('history_follow_rename', 'true'); + + checkbox.addEventListener('change', () => { + const url = new URL(window.location); + + url.searchParams.set('history_follow_rename', `${checkbox.checked}`); + window.location.replace(url); + }); +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 7e84773bc18fa..603d2718fdc0f 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -22,7 +22,7 @@ import {initMarkupContent} from './markup/content.ts'; import {initPdfViewer} from './render/pdf.ts'; import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; -import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; +import {initRepoEllipsisButton, initCommitStatuses, initCommitFileHistoryFollowRename} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; import {initAdminCommon} from './features/admin/common.ts'; import {initRepoCodeView} from './features/repo-code.ts'; @@ -151,6 +151,7 @@ onDomReady(() => { initRepoRecentCommits, initCommitStatuses, + initCommitFileHistoryFollowRename, initCaptcha, initUserCheckAppUrl,