From 37a9793bc583962b6de6343a331759161f28a6cb Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Tue, 13 Oct 2020 20:10:04 -0400 Subject: [PATCH] Adds ahead commit status to file/line history Adds ahead commit status for branches without an upstream --- package.json | 5 ++ src/git/git.ts | 18 +++++--- src/git/gitService.ts | 76 +++++++++++++++++++++++++++++++ src/git/parsers/logParser.ts | 18 ++++---- src/views/nodes/branchNode.ts | 11 +++-- src/views/nodes/commitFileNode.ts | 35 +++++++++++--- src/views/nodes/fileHistoryNode.ts | 20 ++++++-- src/views/nodes/fileHistoryTrackerNode.ts | 13 ++++-- src/views/nodes/lineHistoryNode.ts | 26 +++++++++-- src/views/nodes/lineHistoryTrackerNode.ts | 15 ++++-- src/views/viewCommands.ts | 4 +- 11 files changed, 197 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index c2d1730..02f2836 100644 --- a/package.json +++ b/package.json @@ -6754,6 +6754,11 @@ "group": "inline@1" }, { + "command": "gitlens.views.undoCommit", + "when": "!gitlens:readonly && viewItem =~ /gitlens:file\\b(?=.*?\\b\\+committed\\b)(?=.*?\\b\\+HEAD\\b)(?=.*?\\b\\+unpublished\\b)/", + "group": "inline@-1" + }, + { "command": "gitlens.views.openFile", "when": "viewItem =~ /gitlens:file\\b(?!.*?\\b\\+history\\b)/", "group": "inline@1", diff --git a/src/git/git.ts b/src/git/git.ts index 475d56d..68cecda 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -702,6 +702,7 @@ export namespace Git { ref: string | undefined, { authors, + format = 'default', limit, merges, reverse, @@ -709,6 +710,7 @@ export namespace Git { since, }: { authors?: string[]; + format?: 'refs' | 'default'; limit?: number; merges?: boolean; reverse?: boolean; @@ -718,12 +720,16 @@ export namespace Git { ) { const params = [ 'log', - '--name-status', - `--format=${GitLogParser.defaultFormat}`, + `--format=${format === 'refs' ? GitLogParser.simpleRefs : GitLogParser.defaultFormat}`, '--full-history', `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, '-m', ]; + + if (format !== 'refs') { + params.push('--name-status'); + } + if (limit && !reverse) { params.push(`-n${limit + 1}`); } @@ -763,25 +769,25 @@ export namespace Git { { all, filters, - limit, firstParent = false, + format = 'default', + limit, renames = true, reverse = false, since, skip, - format = 'default', startLine, endLine, }: { all?: boolean; filters?: GitDiffFilter[]; - limit?: number; firstParent?: boolean; + format?: 'refs' | 'simple' | 'default'; + limit?: number; renames?: boolean; reverse?: boolean; since?: string; skip?: number; - format?: 'refs' | 'simple' | 'default'; startLine?: number; endLine?: number; } = {}, diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 87fa547..a47a7a4 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -106,6 +106,15 @@ const mappedAuthorRegex = /(.+)\s<(.+)>/; const emptyPromise: Promise = Promise.resolve(undefined); const reflogCommands = ['merge', 'pull']; +const maxDefaultBranchWeight = 100; +const weightedDefaultBranches = new Map([ + ['master', maxDefaultBranchWeight], + ['main', 15], + ['default', 10], + ['develop', 5], + ['development', 1], +]); + export class GitService implements Disposable { private _onDidChangeRepositories = new EventEmitter(); get onDidChangeRepositories(): Event { @@ -1101,6 +1110,39 @@ export class GitService implements Disposable { return branch; } + @log({ + args: { + 0: b => b.name, + }, + }) + async getBranchAheadRange(branch: GitBranch) { + if (branch.state.ahead > 0) { + return GitRevision.createRange(branch.tracking, branch.ref); + } + + if (!branch.tracking) { + // If we have no tracking branch, try to find a best guess branch to use as the "base" + const branches = await this.getBranches(branch.repoPath, { + filter: b => weightedDefaultBranches.has(b.name), + }); + if (branches.length > 0) { + let weightedBranch: { weight: number; branch: GitBranch } | undefined; + for (const branch of branches) { + const weight = weightedDefaultBranches.get(branch.name)!; + if (weightedBranch == null || weightedBranch.weight < weight) { + weightedBranch = { weight: weight, branch: branch }; + } + + if (weightedBranch.weight === maxDefaultBranchWeight) break; + } + + return GitRevision.createRange(weightedBranch!.branch.ref, branch.ref); + } + } + + return undefined; + } + @log() async getBranches( repoPath: string | undefined, @@ -1680,6 +1722,40 @@ export class GitService implements Disposable { } } + @log() + async getLogRefsOnly( + repoPath: string, + { + ref, + ...options + }: { + authors?: string[]; + limit?: number; + merges?: boolean; + ref?: string; + reverse?: boolean; + since?: string; + } = {}, + ): Promise | undefined> { + const limit = options.limit ?? Container.config.advanced.maxListItems ?? 0; + + try { + const data = await Git.log(repoPath, ref, { + authors: options.authors, + format: 'refs', + limit: limit, + merges: options.merges == null ? true : options.merges, + reverse: options.reverse, + similarityThreshold: Container.config.advanced.similarityThreshold, + since: options.since, + }); + const commits = GitLogParser.parseRefsOnly(data); + return new Set(commits); + } catch (ex) { + return undefined; + } + } + private getLogMoreFn( log: GitLog, options: { authors?: string[]; limit?: number; merges?: boolean; ref?: string; reverse?: boolean }, diff --git a/src/git/parsers/logParser.ts b/src/git/parsers/logParser.ts index 9c45360..f4a4446 100644 --- a/src/git/parsers/logParser.ts +++ b/src/git/parsers/logParser.ts @@ -25,7 +25,7 @@ const fileStatusAndSummaryRegex = /^(\d+?|-)\s+?(\d+?|-)\s+?(.*)(?:\n\s(delete|r const fileStatusAndSummaryRenamedFileRegex = /(.+)\s=>\s(.+)/; const fileStatusAndSummaryRenamedFilePathRegex = /(.*?){(.+?)\s=>\s(.*?)}(.*)/; -const logFileRefsRegex = /^ (.*)/gm; +const logRefsRegex = /^ (.*)/gm; const logFileSimpleRegex = /^ (.*)\s*(?:(?:diff --git a\/(.*) b\/(.*))|(?:(\S)\S*\t([^\t\n]+)(?:\t(.+))?))/gm; const logFileSimpleRenamedRegex = /^ (\S+)\s*(.*)$/s; const logFileSimpleRenamedFilesRegex = /^(\S)\S*\t([^\t\n]+)(?:\t(.+)?)?$/gm; @@ -475,14 +475,14 @@ export class GitLogParser { let ref; let match; do { - match = logFileRefsRegex.exec(data); + match = logRefsRegex.exec(data); if (match == null) break; [, ref] = match; } while (true); // Ensure the regex state is reset - logFileRefsRegex.lastIndex = 0; + logRefsRegex.lastIndex = 0; return ref; } @@ -494,19 +494,19 @@ export class GitLogParser { let ref; let match; do { - match = logFileRefsRegex.exec(data); + match = logRefsRegex.exec(data); if (match == null) break; [, ref] = match; - if (ref == null || ref.length === 0) { - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - refs.push(` ${ref}`.substr(1)); - } + if (ref == null || ref.length === 0) continue; + + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + refs.push(` ${ref}`.substr(1)); } while (true); // Ensure the regex state is reset - logFileRefsRegex.lastIndex = 0; + logRefsRegex.lastIndex = 0; return refs; } diff --git a/src/views/nodes/branchNode.ts b/src/views/nodes/branchNode.ts index cb43004..49f1c5a 100644 --- a/src/views/nodes/branchNode.ts +++ b/src/views/nodes/branchNode.ts @@ -114,7 +114,8 @@ export class BranchNode if (this._children == null) { const children = []; - const [log, getBranchAndTagTips, pr, unpublished] = await Promise.all([ + const range = await Container.git.getBranchAheadRange(this.branch); + const [log, getBranchAndTagTips, pr, unpublishedCommits] = await Promise.all([ this.getLog(), Container.git.getBranchesAndTagsTipsFn(this.uri.repoPath, this.branch.name), this.view.config.pullRequests.enabled && @@ -122,10 +123,10 @@ export class BranchNode (this.branch.tracking || this.branch.remote) ? this.branch.getAssociatedPullRequest(this.root ? { include: [PullRequestState.Open] } : undefined) : undefined, - this.branch.state.ahead > 0 - ? Container.git.getLog(this.uri.repoPath!, { + range + ? Container.git.getLogRefsOnly(this.uri.repoPath!, { limit: 0, - ref: GitRevision.createRange(this.branch.tracking, this.branch.ref), + ref: range, }) : undefined, ]); @@ -188,7 +189,7 @@ export class BranchNode this.view, this, c, - unpublished?.commits.has(c.ref), + unpublishedCommits?.has(c.ref), this.branch, getBranchAndTagTips, ), diff --git a/src/views/nodes/commitFileNode.ts b/src/views/nodes/commitFileNode.ts index b3246a2..dbef702 100644 --- a/src/views/nodes/commitFileNode.ts +++ b/src/views/nodes/commitFileNode.ts @@ -1,10 +1,17 @@ 'use strict'; import * as paths from 'path'; -import { Command, Selection, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Command, Selection, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../../commands'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { CommitFormatter, GitFile, GitLogCommit, GitRevisionReference, StatusFileFormatter } from '../../git/git'; +import { + CommitFormatter, + GitBranch, + GitFile, + GitLogCommit, + GitRevisionReference, + StatusFileFormatter, +} from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { StashesView } from '../stashesView'; import { View } from '../viewBase'; @@ -16,7 +23,13 @@ export class CommitFileNode extends ViewRefFileNode { parent: ViewNode, public readonly file: GitFile, public commit: GitLogCommit, - private readonly _options: { displayAsCommit?: boolean; inFileHistory?: boolean; selection?: Selection } = {}, + private readonly _options: { + branch?: GitBranch; + displayAsCommit?: boolean; + inFileHistory?: boolean; + selection?: Selection; + unpublished?: boolean; + } = {}, ) { super(GitUri.fromFile(file, commit.repoPath, commit.sha), view, parent); } @@ -73,9 +86,15 @@ export class CommitFileNode extends ViewRefFileNode { item.description = this.description; item.tooltip = this.tooltip; - if (this._options.displayAsCommit && !(this.view instanceof StashesView) && this.view.config.avatars) { - item.iconPath = this.commit.getAvatarUri(Container.config.defaultGravatarsStyle); - } else { + if (this._options.displayAsCommit) { + if (!this.commit.isUncommitted && !(this.view instanceof StashesView) && this.view.config.avatars) { + item.iconPath = this._options.unpublished + ? new ThemeIcon('arrow-up') + : this.commit.getAvatarUri(Container.config.defaultGravatarsStyle); + } + } + + if (item.iconPath == null) { const icon = GitFile.getStatusIcon(this.file.status); item.iconPath = { dark: Container.context.asAbsolutePath(paths.join('images', 'dark', icon)), @@ -93,7 +112,9 @@ export class CommitFileNode extends ViewRefFileNode { protected get contextValue(): string { if (!this.commit.isUncommitted) { - return `${ContextValues.File}+committed${this._options.inFileHistory ? '+history' : ''}`; + return `${ContextValues.File}+committed${ + this._options.branch?.current && this._options.branch.sha === this.commit.ref ? '+HEAD' : '' + }${this._options.unpublished ? '+unpublished' : ''}${this._options.inFileHistory ? '+history' : ''}`; } return this.commit.isUncommittedStaged ? `${ContextValues.File}+staged` : `${ContextValues.File}+unstaged`; diff --git a/src/views/nodes/fileHistoryNode.ts b/src/views/nodes/fileHistoryNode.ts index 70ce900..b5bf2b2 100644 --- a/src/views/nodes/fileHistoryNode.ts +++ b/src/views/nodes/fileHistoryNode.ts @@ -5,6 +5,7 @@ import { LoadMoreNode, MessageNode } from './common'; import { Container } from '../../container'; import { FileHistoryTrackerNode } from './fileHistoryTrackerNode'; import { + GitBranch, GitLog, GitRevision, RepositoryChange, @@ -27,7 +28,7 @@ export class FileHistoryNode extends SubscribeableViewNode implements PageableVi protected splatted = true; - constructor(uri: GitUri, view: View, parent: ViewNode) { + constructor(uri: GitUri, view: View, parent: ViewNode, private readonly branch: GitBranch | undefined) { super(uri, view, parent); } @@ -46,9 +47,19 @@ export class FileHistoryNode extends SubscribeableViewNode implements PageableVi const children: ViewNode[] = []; - if (this.uri.sha == null) { - const status = await Container.git.getStatusForFile(this.uri.repoPath!, this.uri.fsPath); + const range = this.branch != null ? await Container.git.getBranchAheadRange(this.branch) : undefined; + const [log, status, unpublishedCommits] = await Promise.all([ + this.getLog(), + this.uri.sha == null ? Container.git.getStatusForFile(this.uri.repoPath!, this.uri.fsPath) : undefined, + range + ? Container.git.getLogRefsOnly(this.uri.repoPath!, { + limit: 0, + ref: range, + }) + : undefined, + ]); + if (this.uri.sha == null) { const commits = await status?.toPsuedoCommits(); if (commits?.length) { children.push( @@ -63,7 +74,6 @@ export class FileHistoryNode extends SubscribeableViewNode implements PageableVi } } - const log = await this.getLog(); if (log != null) { children.push( ...insertDateMarkers( @@ -71,8 +81,10 @@ export class FileHistoryNode extends SubscribeableViewNode implements PageableVi log.commits.values(), c => new CommitFileNode(this.view, this, c.files[0], c, { + branch: this.branch, displayAsCommit: true, inFileHistory: true, + unpublished: unpublishedCommits?.has(c.ref), }), ), this, diff --git a/src/views/nodes/fileHistoryTrackerNode.ts b/src/views/nodes/fileHistoryTrackerNode.ts index 7966535..56ba3d9 100644 --- a/src/views/nodes/fileHistoryTrackerNode.ts +++ b/src/views/nodes/fileHistoryTrackerNode.ts @@ -6,7 +6,7 @@ import { BranchSorting, TagSorting } from '../../configuration'; import { Container } from '../../container'; import { FileHistoryView } from '../fileHistoryView'; import { FileHistoryNode } from './fileHistoryNode'; -import { GitReference } from '../../git/git'; +import { GitReference, GitRevision } from '../../git/git'; import { GitCommitish, GitUri } from '../../git/gitUri'; import { Logger } from '../../logger'; import { ReferencePicker } from '../../quickpicks'; @@ -37,7 +37,7 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode { + async getChildren(): Promise { if (this._child == null) { if (this._fileUri == null && this.uri === unknownGitUri) { this.view.description = undefined; @@ -54,7 +54,14 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode b.name === commitish.sha }); + } + this._child = new FileHistoryNode(fileUri, this.view, this, branch); } return this._child.getChildren(); diff --git a/src/views/nodes/lineHistoryNode.ts b/src/views/nodes/lineHistoryNode.ts index 25c279b..afa154d 100644 --- a/src/views/nodes/lineHistoryNode.ts +++ b/src/views/nodes/lineHistoryNode.ts @@ -4,6 +4,7 @@ import { CommitFileNode } from './commitFileNode'; import { LoadMoreNode, MessageNode } from './common'; import { Container } from '../../container'; import { + GitBranch, GitCommitType, GitFile, GitLog, @@ -36,8 +37,9 @@ export class LineHistoryNode extends SubscribeableViewNode implements PageableVi uri: GitUri, view: View, parent: ViewNode, + private readonly branch: GitBranch | undefined, public readonly selection: Selection, - private readonly _editorContents: string | undefined, + private readonly editorContents: string | undefined, ) { super(uri, view, parent); } @@ -59,11 +61,24 @@ export class LineHistoryNode extends SubscribeableViewNode implements PageableVi let selection = this.selection; + const range = this.branch != null ? await Container.git.getBranchAheadRange(this.branch) : undefined; + const [log, blame, unpublishedCommits] = await Promise.all([ + this.getLog(selection), + this.uri.sha == null + ? this.editorContents + ? await Container.git.getBlameForRangeContents(this.uri, selection, this.editorContents) + : await Container.git.getBlameForRange(this.uri, selection) + : undefined, + range + ? Container.git.getLogRefsOnly(this.uri.repoPath!, { + limit: 0, + ref: range, + }) + : undefined, + ]); + if (this.uri.sha == null) { // Check for any uncommitted changes in the range - const blame = this._editorContents - ? await Container.git.getBlameForRangeContents(this.uri, selection, this._editorContents) - : await Container.git.getBlameForRange(this.uri, selection); if (blame != null) { for (const commit of blame.commits.values()) { if (!commit.isUncommitted) continue; @@ -183,7 +198,6 @@ export class LineHistoryNode extends SubscribeableViewNode implements PageableVi } } - const log = await this.getLog(selection); if (log != null) { children.push( ...insertDateMarkers( @@ -191,9 +205,11 @@ export class LineHistoryNode extends SubscribeableViewNode implements PageableVi log.commits.values(), c => new CommitFileNode(this.view, this, c.files[0], c, { + branch: this.branch, displayAsCommit: true, inFileHistory: true, selection: selection, + unpublished: unpublishedCommits?.has(c.ref), }), ), this, diff --git a/src/views/nodes/lineHistoryTrackerNode.ts b/src/views/nodes/lineHistoryTrackerNode.ts index f912564..5be8146 100644 --- a/src/views/nodes/lineHistoryTrackerNode.ts +++ b/src/views/nodes/lineHistoryTrackerNode.ts @@ -5,7 +5,7 @@ import { UriComparer } from '../../comparers'; import { BranchSorting, TagSorting } from '../../configuration'; import { Container } from '../../container'; import { FileHistoryView } from '../fileHistoryView'; -import { GitReference } from '../../git/git'; +import { GitReference, GitRevision } from '../../git/git'; import { GitCommitish, GitUri } from '../../git/gitUri'; import { LineHistoryView } from '../lineHistoryView'; import { LineHistoryNode } from './lineHistoryNode'; @@ -40,7 +40,7 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode { + async getChildren(): Promise { if (this._child == null) { if (this.uri === unknownGitUri) { this.view.description = undefined; @@ -60,7 +60,16 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode b.name === commitish.sha, + }); + } + this._child = new LineHistoryNode(fileUri, this.view, this, branch, this._selection!, this._editorContents); } return this._child.getChildren(); diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index 1b54c08..710ee4e 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -522,8 +522,8 @@ export class ViewCommands { } @debug() - private undoCommit(node: CommitNode) { - if (!(node instanceof CommitNode)) return Promise.resolve(); + private undoCommit(node: CommitNode | CommitFileNode) { + if (!(node instanceof CommitNode) && !(node instanceof CommitFileNode)) return Promise.resolve(); return GitActions.reset( node.repoPath,