diff --git a/README.md b/README.md index 992f952..725020f 100644 --- a/README.md +++ b/README.md @@ -1005,6 +1005,7 @@ See also [View Settings](#view-settings- 'Jump to the View settings') | `gitlens.advanced.blame.delayAfterEdit` | Specifies the time (in milliseconds) to wait before re-blaming an unsaved document after an edit. Use 0 to specify an infinite wait | | `gitlens.advanced.blame.sizeThresholdAfterEdit` | Specifies the maximum document size (in lines) allowed to be re-blamed after an edit while still unsaved. Use 0 to specify no maximum | | `gitlens.advanced.caching.enabled` | Specifies whether git output will be cached — changing the default is not recommended | +| `gitlens.advanced.commitOrdering` | Specifies the order by which commits will be shown. If unspecified, commits will be shown in reverse chronological order

`date` - shows commits in reverse chronological order of the commit timestamp
`author-date` - shows commits in reverse chronological order of the author timestamp
`topo` - shows commits in reverse chronological order of the commit timestamp, but avoids intermixing multiple lines of history | | `gitlens.advanced.externalDiffTool` | Specifies an optional external diff tool to use when comparing files. Must be a configured [Git difftool](https://git-scm.com/docs/git-config#Documentation/git-config.txt-difftool). | | `gitlens.advanced.externalDirectoryDiffTool` | Specifies an optional external diff tool to use when comparing directories. Must be a configured [Git difftool](https://git-scm.com/docs/git-config#Documentation/git-config.txt-difftool). | | `gitlens.advanced.fileHistoryFollowsRenames` | Specifies whether file histories will follow renames -- will affect how merge commits are shown in histories | diff --git a/package.json b/package.json index 250fc22..910b931 100644 --- a/package.json +++ b/package.json @@ -2520,6 +2520,27 @@ "markdownDescription": "Specifies whether git output will be cached — changing the default is not recommended", "scope": "window" }, + "gitlens.advanced.commitOrdering": { + "type": [ + "string", + "null" + ], + "default": null, + "enum": [ + null, + "date", + "author-date", + "topo" + ], + "enumDescriptions": [ + "Shows commits in reverse chronological order", + "Shows commits in reverse chronological order of the commit timestamp", + "Shows commits in reverse chronological order of the author timestamp", + "Shows commits in reverse chronological order of the commit timestamp, but avoids intermixing multiple lines of history" + ], + "markdownDescription": "Specifies the order by which commits will be shown. If unspecified, commits will be shown in reverse chronological order", + "scope": "window" + }, "gitlens.advanced.externalDiffTool": { "type": [ "string", diff --git a/src/config.ts b/src/config.ts index 9ed9183..18599ec 100644 --- a/src/config.ts +++ b/src/config.ts @@ -308,6 +308,7 @@ export interface AdvancedConfig { caching: { enabled: boolean; }; + commitOrdering: string | null; externalDiffTool: string | null; externalDirectoryDiffTool: string | null; fileHistoryFollowsRenames: boolean; diff --git a/src/git/git.ts b/src/git/git.ts index ab9c9ec..43e0b01 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -745,6 +745,7 @@ export namespace Git { format = 'default', limit, merges, + ordering, reverse, similarityThreshold, since, @@ -753,6 +754,7 @@ export namespace Git { format?: 'refs' | 'default'; limit?: number; merges?: boolean; + ordering?: string | null; reverse?: boolean; similarityThreshold?: number | null; since?: string; @@ -770,6 +772,10 @@ export namespace Git { params.push('--name-status'); } + if (ordering) { + params.push(`--${ordering}-order`); + } + if (limit && !reverse) { params.push(`-n${limit + 1}`); } @@ -812,6 +818,7 @@ export namespace Git { firstParent = false, format = 'default', limit, + ordering, renames = true, reverse = false, since, @@ -824,6 +831,7 @@ export namespace Git { firstParent?: boolean; format?: 'refs' | 'simple' | 'default'; limit?: number; + ordering?: string | null; renames?: boolean; reverse?: boolean; since?: string; @@ -839,6 +847,10 @@ export namespace Git { `--format=${format === 'default' ? GitLogParser.defaultFormat : GitLogParser.simpleFormat}`, ]; + if (ordering) { + params.push(`--${ordering}-order`); + } + if (limit && !reverse) { params.push(`-n${limit + 1}`); } @@ -903,7 +915,11 @@ export namespace Git { export async function log__file_recent( repoPath: string, fileName: string, - { ref, similarityThreshold }: { ref?: string; similarityThreshold?: number | null } = {}, + { + ordering, + ref, + similarityThreshold, + }: { ordering?: string | null; ref?: string; similarityThreshold?: number | null } = {}, ) { const params = [ 'log', @@ -912,6 +928,10 @@ export namespace Git { '--format=%H', ]; + if (ordering) { + params.push(`--${ordering}-order`); + } + if (ref) { params.push(ref); } @@ -925,8 +945,19 @@ export namespace Git { return data.length === 0 ? undefined : data.trim(); } - export async function log__find_object(repoPath: string, objectId: string, ref: string, file?: string) { + export async function log__find_object( + repoPath: string, + objectId: string, + ref: string, + ordering: string | null, + file?: string, + ) { const params = ['log', '-n1', '--no-renames', '--format=%H', `--find-object=${objectId}`, ref]; + + if (ordering) { + params.push(`--${ordering}-order`); + } + if (file) { params.push('--', file); } @@ -938,32 +969,47 @@ export namespace Git { return data.length === 0 ? undefined : data.trim(); } - export async function log__recent(repoPath: string) { + export async function log__recent(repoPath: string, ordering?: string | null) { + const params = ['log', '-n1', '--format=%H']; + + if (ordering) { + params.push(`--${ordering}-order`); + } + const data = await git( { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, - 'log', - '-n1', - '--format=%H', + ...params, '--', ); + return data.length === 0 ? undefined : data.trim(); } - export async function log__recent_committerdate(repoPath: string) { + export async function log__recent_committerdate(repoPath: string, ordering?: string | null) { + const params = ['log', '-n1', '--format=%ct']; + + if (ordering) { + params.push(`--${ordering}-order`); + } + const data = await git( { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, - 'log', - '-n1', - '--format=%ct', + ...params, '--', ); + return data.length === 0 ? undefined : data.trim(); } export function log__search( repoPath: string, search: string[] = emptyArray, - { limit, skip, useShow }: { limit?: number; skip?: number; useShow?: boolean } = {}, + { + limit, + ordering, + skip, + useShow, + }: { limit?: number; ordering?: string | null; skip?: number; useShow?: boolean } = {}, ) { const params = [ useShow ? 'show' : 'log', @@ -971,13 +1017,19 @@ export namespace Git { `--format=${GitLogParser.defaultFormat}`, '--use-mailmap', ]; + if (limit && !useShow) { params.push(`-n${limit + 1}`); } + if (skip && !useShow) { params.push(`--skip=${skip}`); } + if (ordering && !useShow) { + params.push(`--${ordering}-order`); + } + return git( { cwd: repoPath, configs: useShow ? undefined : ['-c', 'log.showSignature=false'] }, ...params, @@ -1038,18 +1090,32 @@ export namespace Git { export function reflog( repoPath: string, - { all, branch, limit, skip }: { all?: boolean; branch?: string; limit?: number; skip?: number } = {}, + { + all, + branch, + limit, + ordering, + skip, + }: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number } = {}, ): Promise { const params = ['log', '--walk-reflogs', `--format=${GitReflogParser.defaultFormat}`, '--date=iso8601']; + + if (ordering) { + params.push(`--${ordering}-order`); + } + if (all) { params.push('--all'); } + if (limit) { params.push(`-n${limit}`); } + if (skip) { params.push(`--skip=${skip}`); } + if (branch) { params.push(branch); } @@ -1122,6 +1188,7 @@ export namespace Git { export async function rev_parse__currentBranch( repoPath: string, + ordering: string | null, ): Promise<[string, string | undefined] | undefined> { try { const data = await git( @@ -1164,7 +1231,7 @@ export namespace Git { } if (GitWarnings.headNotABranch.test(msg)) { - const sha = await log__recent(repoPath); + const sha = await log__recent(repoPath, ordering); if (sha === undefined) return undefined; return [`(HEAD detached at ${GitRevision.shorten(sha)})`, sha]; diff --git a/src/git/gitService.ts b/src/git/gitService.ts index d5e94d4..1e62293 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -1215,14 +1215,14 @@ export class GitService implements Disposable { let [branch] = await this.getBranches(repoPath, { filter: b => b.current }); if (branch != null) return branch; - const data = await Git.rev_parse__currentBranch(repoPath); + const data = await Git.rev_parse__currentBranch(repoPath, Container.config.advanced.commitOrdering); if (data == null) return undefined; const [name, tracking] = data[0].split('\n'); if (GitBranch.isDetached(name)) { const [rebaseStatus, committerDate] = await Promise.all([ this.getRebaseStatus(repoPath), - Git.log__recent_committerdate(repoPath), + Git.log__recent_committerdate(repoPath, Container.config.advanced.commitOrdering), ]); branch = new GitBranch( @@ -1300,12 +1300,12 @@ export class GitService implements Disposable { if (data == null || data.length === 0) { let current; - const data = await Git.rev_parse__currentBranch(repoPath); + const data = await Git.rev_parse__currentBranch(repoPath, Container.config.advanced.commitOrdering); if (data != null) { const [name, tracking] = data[0].split('\n'); const [rebaseStatus, committerDate] = await Promise.all([ GitBranch.isDetached(name) ? this.getRebaseStatus(repoPath) : undefined, - Git.log__recent_committerdate(repoPath), + Git.log__recent_committerdate(repoPath, Container.config.advanced.commitOrdering), ]); current = new GitBranch( @@ -1498,6 +1498,7 @@ export class GitService implements Disposable { async getOldestUnpushedRefForFile(repoPath: string, fileName: string): Promise { const data = await Git.log__file(repoPath, fileName, '@{push}..', { format: 'refs', + ordering: Container.config.advanced.commitOrdering, renames: true, }); if (data == null || data.length === 0) return undefined; @@ -1876,6 +1877,7 @@ export class GitService implements Disposable { authors?: string[]; limit?: number; merges?: boolean; + ordering?: string | null; ref?: string; reverse?: boolean; since?: string; @@ -1888,6 +1890,7 @@ export class GitService implements Disposable { authors: options.authors, limit: limit, merges: options.merges == null ? true : options.merges, + ordering: options.ordering ?? Container.config.advanced.commitOrdering, reverse: options.reverse, similarityThreshold: Container.config.advanced.similarityThreshold, since: options.since, @@ -1928,6 +1931,7 @@ export class GitService implements Disposable { authors?: string[]; limit?: number; merges?: boolean; + ordering?: string | null; ref?: string; reverse?: boolean; since?: string; @@ -1944,6 +1948,7 @@ export class GitService implements Disposable { reverse: options.reverse, similarityThreshold: Container.config.advanced.similarityThreshold, since: options.since, + ordering: options.ordering ?? Container.config.advanced.commitOrdering, }); const commits = GitLogParser.parseRefsOnly(data); return new Set(commits); @@ -1954,7 +1959,14 @@ export class GitService implements Disposable { private getLogMoreFn( log: GitLog, - options: { authors?: string[]; limit?: number; merges?: boolean; ref?: string; reverse?: boolean }, + options: { + authors?: string[]; + limit?: number; + merges?: boolean; + ordering?: string | null; + ref?: string; + reverse?: boolean; + }, ): (limit: number | { until: string } | undefined) => Promise { return async (limit: number | { until: string } | undefined) => { const moreUntil = limit != null && typeof limit === 'object' ? limit.until : undefined; @@ -2021,7 +2033,7 @@ export class GitService implements Disposable { async getLogForSearch( repoPath: string, search: SearchPattern, - options: { limit?: number; skip?: number } = {}, + options: { limit?: number; ordering?: string | null; skip?: number } = {}, ): Promise { search = { matchAll: false, matchCase: false, matchRegex: true, ...search }; @@ -2098,7 +2110,12 @@ export class GitService implements Disposable { args.push(...files); } - const data = await Git.log__search(repoPath, args, { ...options, limit: limit, useShow: useShow }); + const data = await Git.log__search(repoPath, args, { + ordering: Container.config.advanced.commitOrdering, + ...options, + limit: limit, + useShow: useShow, + }); const log = GitLogParser.parse( data, GitCommitType.Log, @@ -2128,7 +2145,7 @@ export class GitService implements Disposable { private getLogForSearchMoreFn( log: GitLog, search: SearchPattern, - options: { limit?: number }, + options: { limit?: number; ordering?: string | null }, ): (limit: number | undefined) => Promise { return async (limit: number | undefined) => { limit = limit ?? Container.config.advanced.maxSearchItems ?? 0; @@ -2180,6 +2197,7 @@ export class GitService implements Disposable { options: { all?: boolean; limit?: number; + ordering?: string | null; range?: Range; ref?: string; renames?: boolean; @@ -2330,6 +2348,7 @@ export class GitService implements Disposable { }: { all?: boolean; limit?: number; + ordering?: string | null; range?: Range; ref?: string; renames?: boolean; @@ -2354,6 +2373,7 @@ export class GitService implements Disposable { } const data = await Git.log__file(root, file, ref, { + ordering: Container.config.advanced.commitOrdering, ...options, firstParent: options.renames, startLine: range == null ? undefined : range.start.line + 1, @@ -2404,7 +2424,15 @@ export class GitService implements Disposable { private getLogForFileMoreFn( log: GitLog, fileName: string, - options: { all?: boolean; limit?: number; range?: Range; ref?: string; renames?: boolean; reverse?: boolean }, + options: { + all?: boolean; + limit?: number; + ordering?: string | null; + range?: Range; + ref?: string; + renames?: boolean; + reverse?: boolean; + }, ): (limit: number | { until: string } | undefined) => Promise { return async (limit: number | { until: string } | undefined) => { const moreUntil = limit != null && typeof limit === 'object' ? limit.until : undefined; @@ -2669,10 +2697,11 @@ export class GitService implements Disposable { const fileName = GitUri.relativeTo(uri, repoPath); let data = await Git.log__file(repoPath, fileName, ref, { filters: filters, + format: 'simple', limit: skip + 1, - // startLine: editorLine != null ? editorLine + 1 : undefined, + ordering: Container.config.advanced.commitOrdering, reverse: true, - format: 'simple', + // startLine: editorLine != null ? editorLine + 1 : undefined, }); if (data == null || data.length === 0) return undefined; @@ -2681,9 +2710,10 @@ export class GitService implements Disposable { if (status === 'D') { data = await Git.log__file(repoPath, '.', nextRef, { filters: ['R', 'C'], + format: 'simple', limit: 1, + ordering: Container.config.advanced.commitOrdering, // startLine: editorLine != null ? editorLine + 1 : undefined - format: 'simple', }); if (data == null || data.length === 0) { return GitUri.fromFile(file ?? fileName, repoPath, nextRef); @@ -2918,9 +2948,10 @@ export class GitService implements Disposable { let data; try { data = await Git.log__file(repoPath, fileName, ref, { - limit: skip + 2, firstParent: firstParent, format: 'simple', + limit: skip + 2, + ordering: Container.config.advanced.commitOrdering, startLine: editorLine != null ? editorLine + 1 : undefined, }); } catch (ex) { @@ -2934,7 +2965,9 @@ export class GitService implements Disposable { } } - ref = await Git.log__file_recent(repoPath, fileName); + ref = await Git.log__file_recent(repoPath, fileName, { + ordering: Container.config.advanced.commitOrdering, + }); return GitUri.fromFile(fileName, repoPath, ref ?? GitRevision.deletedOrMissing); } @@ -3056,14 +3089,21 @@ export class GitService implements Disposable { @log() async getIncomingActivity( repoPath: string, - { limit, ...options }: { all?: boolean; branch?: string; limit?: number; skip?: number } = {}, + { + limit, + ...options + }: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number } = {}, ): Promise { const cc = Logger.getCorrelationContext(); limit = limit ?? Container.config.advanced.maxListItems ?? 0; try { // Pass a much larger limit to reflog, because we aggregate the data and we won't know how many lines we'll need - const data = await Git.reflog(repoPath, { ...options, limit: limit * 100 }); + const data = await Git.reflog(repoPath, { + ordering: Container.config.advanced.commitOrdering, + ...options, + limit: limit * 100, + }); if (data == null) return undefined; const reflog = GitReflogParser.parse(data, repoPath, reflogCommands, limit, limit * 100); @@ -3080,7 +3120,7 @@ export class GitService implements Disposable { private getReflogMoreFn( reflog: GitReflog, - options: { all?: boolean; branch?: string; limit?: number; skip?: number }, + options: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number }, ): (limit: number) => Promise { return async (limit: number | undefined) => { limit = limit ?? Container.config.advanced.maxSearchItems ?? 0; @@ -3721,6 +3761,7 @@ export class GitService implements Disposable { // TODO: Add caching // Get the most recent commit for this file name ref = await Git.log__file_recent(repoPath, fileName, { + ordering: Container.config.advanced.commitOrdering, similarityThreshold: Container.config.advanced.similarityThreshold, }); if (ref == null) return undefined; @@ -3728,8 +3769,9 @@ export class GitService implements Disposable { // Now check if that commit had any renames data = await Git.log__file(repoPath, '.', ref, { filters: ['R', 'C', 'D'], - limit: 1, format: 'simple', + limit: 1, + ordering: Container.config.advanced.commitOrdering, }); if (data == null || data.length === 0) break; @@ -4012,7 +4054,13 @@ export class GitService implements Disposable { const blob = await Git.rev_parse__verify(repoPath, ref, fileName); if (blob == null) return GitRevision.deletedOrMissing; - let promise: Promise = Git.log__find_object(repoPath, blob, ref, fileName); + let promise: Promise = Git.log__find_object( + repoPath, + blob, + ref, + Container.config.advanced.commitOrdering, + fileName, + ); if (options?.timeout != null) { promise = Promise.race([promise, Functions.wait(options.timeout)]); }