From cdc22412794bf3a45b05c5b97330ec6bd0f98cbc Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Mon, 10 Dec 2018 22:46:58 -0500 Subject: [PATCH] Adds detailed tracking status to all branches in the repos view --- CHANGELOG.md | 5 ++ README.md | 4 ++ src/git/models/branch.ts | 10 +-- src/git/models/status.ts | 9 +-- src/views/nodes.ts | 2 +- src/views/nodes/branchNode.ts | 33 +++++++-- src/views/nodes/branchTrackingStatusNode.ts | 108 ++++++++++++++++++++++++++++ src/views/nodes/repositoryNode.ts | 8 +-- src/views/nodes/statusUpstreamNode.ts | 91 ----------------------- src/views/nodes/viewNode.ts | 4 +- src/views/viewCommands.ts | 14 ++-- 11 files changed, 167 insertions(+), 121 deletions(-) create mode 100644 src/views/nodes/branchTrackingStatusNode.ts delete mode 100644 src/views/nodes/statusUpstreamNode.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4159aef..fa45e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added +- Adds more detailed branch tracking status (if available) to the **Branches** list in the _Repositories_ view + - **\* Commits Behind** — quickly see and explore the specific commits behind the upstream (i.e. commits that haven't been pulled) + - Only provided if the current branch is tracking a remote branch and is behind it + - **\* Commits Ahead** — quickly see and explore the specific commits ahead of the upstream (i.e. commits that haven't been pushed) + - Only provided if the current branch is tracking a remote branch and is ahead of it - Adds control over the contributed menu commands to the Source Control side bar to the GitLens interactive settings editor (via the `gitlens.menus` setting) - Adds Git extended regex support to commit searches diff --git a/README.md b/README.md index 4e9d5a9..397a025 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,10 @@ The repositories view provides the following features, - An inline toolbar provides quick access to the _Checkout_, _Compare with Remote_ (if available), _Compare with HEAD_ (`alt-click` for _Compare with Working Tree_), and _Open Branch on Remote_ (if available) commands - A context menu provides access to more common branch commands - Each branch expands to list its revision (commit) history + - **\* Commits Behind** — quickly see and explore the specific commits behind the upstream (i.e. commits that haven't been pulled) + - Only provided if the current branch is tracking a remote branch and is behind it + - **\* Commits Ahead** — quickly see and explore the specific commits ahead of the upstream (i.e. commits that haven't been pushed) + - Only provided if the current branch is tracking a remote branch and is ahead of it - An inline toolbar provides quick access to the _Compare with HEAD_ (`alt-click` for _Compare with Working Tree_), _Copy Commit ID to Clipboard_ (`alt-click` for _Copy Commit Message to Clipboard_), and _Open Commit on Remote_ (if available) commands - A context menu provides access to more common revision (commit) commands - Each revision (commit) expands to list its set of changed files, complete with status indicators for adds, changes, renames, and deletes diff --git a/src/git/models/branch.ts b/src/git/models/branch.ts index 3320d1c..2be1953 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -2,15 +2,17 @@ import { Git } from '../git'; import { GitStatus } from './status'; +export interface GitTrackingState { + ahead: number; + behind: number; +} + export class GitBranch { readonly detached: boolean; readonly name: string; readonly remote: boolean; readonly tracking?: string; - readonly state: { - ahead: number; - behind: number; - }; + readonly state: GitTrackingState; constructor( public readonly repoPath: string, diff --git a/src/git/models/status.ts b/src/git/models/status.ts index bc2d679..c5cc690 100644 --- a/src/git/models/status.ts +++ b/src/git/models/status.ts @@ -3,14 +3,9 @@ import { Uri } from 'vscode'; import { GlyphChars } from '../../constants'; import { Strings } from '../../system'; import { GitUri } from '../gitUri'; -import { GitBranch } from './branch'; +import { GitBranch, GitTrackingState } from './branch'; import { GitFile, GitFileStatus } from './file'; -export interface GitStatusUpstreamState { - ahead: number; - behind: number; -} - export class GitStatus { readonly detached: boolean; @@ -19,7 +14,7 @@ export class GitStatus { public readonly branch: string, public readonly sha: string, public readonly files: GitStatusFile[], - public readonly state: GitStatusUpstreamState, + public readonly state: GitTrackingState, public readonly upstream?: string ) { this.detached = GitBranch.isDetached(branch); diff --git a/src/views/nodes.ts b/src/views/nodes.ts index 8446867..e3742cd 100644 --- a/src/views/nodes.ts +++ b/src/views/nodes.ts @@ -3,6 +3,7 @@ export * from './nodes/viewNode'; export * from './nodes/branchesNode'; export * from './nodes/branchNode'; +export * from './nodes/branchTrackingStatusNode'; export * from './nodes/commitFileNode'; export * from './nodes/commitNode'; export * from './nodes/fileHistoryNode'; @@ -24,6 +25,5 @@ export * from './nodes/stashFileNode'; export * from './nodes/stashNode'; export * from './nodes/statusFileNode'; export * from './nodes/statusFilesNode'; -export * from './nodes/statusUpstreamNode'; export * from './nodes/tagsNode'; export * from './nodes/tagNode'; diff --git a/src/views/nodes/branchNode.ts b/src/views/nodes/branchNode.ts index 681994f..8496c10 100644 --- a/src/views/nodes/branchNode.ts +++ b/src/views/nodes/branchNode.ts @@ -6,6 +6,7 @@ import { Container } from '../../container'; import { GitBranch, GitUri } from '../../git/gitService'; import { Iterables } from '../../system'; import { RepositoriesView } from '../repositoriesView'; +import { BranchTrackingStatusNode } from './branchTrackingStatusNode'; import { CommitNode } from './commitNode'; import { MessageNode, ShowMoreNode } from './common'; import { getBranchesAndTagTipsFn, insertDateMarkers } from './helpers'; @@ -22,15 +23,16 @@ export class BranchNode extends ViewRefNode implements Pageabl view: RepositoriesView, parent: ViewNode, public readonly branch: GitBranch, - private readonly _markCurrent: boolean = true + // Specifies that the node is shown as a root under the repository node + private readonly _root: boolean = false ) { super(uri, view, parent); } get id(): string { - return `gitlens:repository(${this.branch.repoPath}):branch(${this.branch.name})${ + return `gitlens:repository(${this.branch.repoPath}):${this._root ? 'root:' : ''}branch(${this.branch.name})${ this.branch.remote ? ':remote' : '' - }${this._markCurrent ? ':current' : ''}`; + }`; } get current(): boolean { @@ -50,6 +52,24 @@ export class BranchNode extends ViewRefNode implements Pageabl async getChildren(): Promise { if (this._children === undefined) { + const children = []; + if (!this._root && this.branch.tracking) { + const status = { + ref: this.branch.ref, + repoPath: this.branch.repoPath, + state: this.branch.state, + upstream: this.branch.tracking + }; + + if (this.branch.state.behind) { + children.push(new BranchTrackingStatusNode(this.view, this, status, 'behind')); + } + + if (this.branch.state.ahead) { + children.push(new BranchTrackingStatusNode(this.view, this, status, 'ahead')); + } + } + const log = await Container.git.getLog(this.uri.repoPath!, { maxCount: this.maxCount || this.view.config.defaultItemLimit, ref: this.ref @@ -57,7 +77,7 @@ export class BranchNode extends ViewRefNode implements Pageabl if (log === undefined) return [new MessageNode(this.view, this, 'No commits could be found.')]; const getBranchAndTagTips = await getBranchesAndTagTipsFn(this.uri.repoPath, this.branch.name); - const children = [ + children.push( ...insertDateMarkers( Iterables.map( log.commits.values(), @@ -65,7 +85,7 @@ export class BranchNode extends ViewRefNode implements Pageabl ), this ) - ]; + ); if (log.truncated) { children.push(new ShowMoreNode(this.view, this, 'Commits')); @@ -104,7 +124,8 @@ export class BranchNode extends ViewRefNode implements Pageabl } const item = new TreeItem( - `${this._markCurrent && this.current ? `${GlyphChars.Check} ${GlyphChars.Space}` : ''}${name}`, + // Hide the current branch checkmark when the node is displayed as a root under the repository node + `${!this._root && this.current ? `${GlyphChars.Check} ${GlyphChars.Space}` : ''}${name}`, TreeItemCollapsibleState.Collapsed ); item.id = this.id; diff --git a/src/views/nodes/branchTrackingStatusNode.ts b/src/views/nodes/branchTrackingStatusNode.ts new file mode 100644 index 0000000..68e0751 --- /dev/null +++ b/src/views/nodes/branchTrackingStatusNode.ts @@ -0,0 +1,108 @@ +'use strict'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Container } from '../../container'; +import { GitTrackingState, GitUri } from '../../git/gitService'; +import { Iterables, Strings } from '../../system'; +import { View } from '../viewBase'; +import { CommitNode } from './commitNode'; +import { ShowMoreNode } from './common'; +import { insertDateMarkers } from './helpers'; +import { PageableViewNode, ResourceType, ViewNode } from './viewNode'; + +export interface BranchTrackingStatus { + ref: string; + repoPath: string; + state: GitTrackingState; + upstream?: string; +} + +export class BranchTrackingStatusNode extends ViewNode implements PageableViewNode { + readonly supportsPaging: boolean = true; + maxCount: number | undefined; + + constructor( + view: View, + parent: ViewNode, + public readonly status: BranchTrackingStatus, + public readonly direction: 'ahead' | 'behind', + // Specifies that the node is shown as a root under the repository node + private readonly _root: boolean = false + ) { + super(GitUri.fromRepoPath(status.repoPath), view, parent); + } + + get id(): string { + return `gitlens:repository(${this.status.repoPath}):${this._root ? 'root:' : ''}branch(${ + this.status.ref + }):status:upstream:(${this.status.upstream}):${this.direction}`; + } + + async getChildren(): Promise { + const ahead = this.direction === 'ahead'; + const range = ahead + ? `${this.status.upstream}..${this.status.ref}` + : `${this.status.ref}..${this.status.upstream}`; + + const log = await Container.git.getLog(this.uri.repoPath!, { + maxCount: this.maxCount || this.view.config.defaultItemLimit, + ref: range + }); + if (log === undefined) return []; + + let children; + if (ahead) { + // Since the last commit when we are looking 'ahead' can have no previous (because of the range given) -- look it up + const commits = [...log.commits.values()]; + const commit = commits[commits.length - 1]; + if (commit.previousSha === undefined) { + const previousLog = await Container.git.getLog(this.uri.repoPath!, { maxCount: 2, ref: commit.sha }); + if (previousLog !== undefined) { + commits[commits.length - 1] = Iterables.first(previousLog.commits.values()); + } + } + + children = [...insertDateMarkers(Iterables.map(commits, c => new CommitNode(this.view, this, c)), this, 1)]; + } + else { + children = [ + ...insertDateMarkers( + Iterables.map(log.commits.values(), c => new CommitNode(this.view, this, c)), + this, + 1 + ) + ]; + } + + if (log.truncated) { + children.push(new ShowMoreNode(this.view, this, 'Commits')); + } + return children; + } + + async getTreeItem(): Promise { + const ahead = this.direction === 'ahead'; + const label = ahead + ? `${Strings.pluralize('commit', this.status.state.ahead)} ahead` + : `${Strings.pluralize('commit', this.status.state.behind)} behind`; + + const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); + item.id = this.id; + if (this._root) { + item.contextValue = ahead ? ResourceType.StatusAheadOfUpstream : ResourceType.StatusBehindUpstream; + } + else { + item.contextValue = ahead + ? ResourceType.BranchStatusAheadOfUpstream + : ResourceType.BranchStatusBehindUpstream; + } + item.tooltip = `${label}${ahead ? ' of ' : ''}${this.status.upstream}`; + + const iconSuffix = ahead ? 'upload' : 'download'; + item.iconPath = { + dark: Container.context.asAbsolutePath(`images/dark/icon-${iconSuffix}.svg`), + light: Container.context.asAbsolutePath(`images/light/icon-${iconSuffix}.svg`) + }; + + return item; + } +} diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts index 11ef7b1..47ea585 100644 --- a/src/views/nodes/repositoryNode.ts +++ b/src/views/nodes/repositoryNode.ts @@ -15,11 +15,11 @@ import { Dates, debug, Functions, gate, log, Strings } from '../../system'; import { RepositoriesView } from '../repositoriesView'; import { BranchesNode } from './branchesNode'; import { BranchNode } from './branchNode'; +import { BranchTrackingStatusNode } from './branchTrackingStatusNode'; import { MessageNode } from './common'; import { RemotesNode } from './remotesNode'; import { StashesNode } from './stashesNode'; import { StatusFilesNode } from './statusFilesNode'; -import { StatusUpstreamNode } from './statusUpstreamNode'; import { TagsNode } from './tagsNode'; import { ResourceType, SubscribeableViewNode, ViewNode } from './viewNode'; @@ -59,14 +59,14 @@ export class RepositoryNode extends SubscribeableViewNode { status.state.behind, status.detached ); - children.push(new BranchNode(this.uri, this.view, this, branch, false)); + children.push(new BranchNode(this.uri, this.view, this, branch, true)); if (status.state.behind) { - children.push(new StatusUpstreamNode(this.view, this, status, 'behind')); + children.push(new BranchTrackingStatusNode(this.view, this, status, 'behind', true)); } if (status.state.ahead) { - children.push(new StatusUpstreamNode(this.view, this, status, 'ahead')); + children.push(new BranchTrackingStatusNode(this.view, this, status, 'ahead', true)); } if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) { diff --git a/src/views/nodes/statusUpstreamNode.ts b/src/views/nodes/statusUpstreamNode.ts deleted file mode 100644 index 1f78f2b..0000000 --- a/src/views/nodes/statusUpstreamNode.ts +++ /dev/null @@ -1,91 +0,0 @@ -'use strict'; -import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { Container } from '../../container'; -import { GitStatus, GitUri } from '../../git/gitService'; -import { Iterables, Strings } from '../../system'; -import { View } from '../viewBase'; -import { CommitNode } from './commitNode'; -import { ShowMoreNode } from './common'; -import { insertDateMarkers } from './helpers'; -import { RepositoryNode } from './repositoryNode'; -import { PageableViewNode, ResourceType, ViewNode } from './viewNode'; - -export class StatusUpstreamNode extends ViewNode implements PageableViewNode { - readonly supportsPaging: boolean = true; - maxCount: number | undefined; - - constructor( - view: View, - parent: RepositoryNode, - public readonly status: GitStatus, - public readonly direction: 'ahead' | 'behind' - ) { - super(GitUri.fromRepoPath(status.repoPath), view, parent); - } - - get id(): string { - return `gitlens:repository(${this.status.repoPath}):status:upstream:${this.direction}`; - } - - async getChildren(): Promise { - const ahead = this.direction === 'ahead'; - const range = ahead - ? `${this.status.upstream}..${this.status.ref}` - : `${this.status.ref}..${this.status.upstream}`; - - const log = await Container.git.getLog(this.uri.repoPath!, { - maxCount: this.maxCount || this.view.config.defaultItemLimit, - ref: range - }); - if (log === undefined) return []; - - let children; - if (ahead) { - // Since the last commit when we are looking 'ahead' can have no previous (because of the range given) -- look it up - const commits = [...log.commits.values()]; - const commit = commits[commits.length - 1]; - if (commit.previousSha === undefined) { - const previousLog = await Container.git.getLog(this.uri.repoPath!, { maxCount: 2, ref: commit.sha }); - if (previousLog !== undefined) { - commits[commits.length - 1] = Iterables.first(previousLog.commits.values()); - } - } - - children = [...insertDateMarkers(Iterables.map(commits, c => new CommitNode(this.view, this, c)), this, 1)]; - } - else { - children = [ - ...insertDateMarkers( - Iterables.map(log.commits.values(), c => new CommitNode(this.view, this, c)), - this, - 1 - ) - ]; - } - - if (log.truncated) { - children.push(new ShowMoreNode(this.view, this, 'Commits')); - } - return children; - } - - async getTreeItem(): Promise { - const ahead = this.direction === 'ahead'; - const label = ahead - ? `${Strings.pluralize('commit', this.status.state.ahead)} ahead` - : `${Strings.pluralize('commit', this.status.state.behind)} behind`; - - const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); - item.id = this.id; - item.contextValue = ahead ? ResourceType.StatusAheadOfUpstream : ResourceType.StatusBehindUpstream; - item.tooltip = `${label}${ahead ? ' of ' : ''}${this.status.upstream}`; - - const iconSuffix = ahead ? 'upload' : 'download'; - item.iconPath = { - dark: Container.context.asAbsolutePath(`images/dark/icon-${iconSuffix}.svg`), - light: Container.context.asAbsolutePath(`images/light/icon-${iconSuffix}.svg`) - }; - - return item; - } -} diff --git a/src/views/nodes/viewNode.ts b/src/views/nodes/viewNode.ts index 5ddbcbf..dad8ff8 100644 --- a/src/views/nodes/viewNode.ts +++ b/src/views/nodes/viewNode.ts @@ -9,9 +9,11 @@ export enum ResourceType { ActiveFileHistory = 'gitlens:history:active:file', ActiveLineHistory = 'gitlens:history:active:line', Branch = 'gitlens:branch', - BranchWithTracking = 'gitlens:branch:tracking', Branches = 'gitlens:branches', BranchesWithRemotes = 'gitlens:branches:remotes', + BranchStatusAheadOfUpstream = 'gitlens:branch-status:upstream:ahead', + BranchStatusBehindUpstream = 'gitlens:branch-status:upstream:behind', + BranchWithTracking = 'gitlens:branch:tracking', CurrentBranch = 'gitlens:branch:current', CurrentBranchWithTracking = 'gitlens:branch:current:tracking', RemoteBranch = 'gitlens:branch:remote', diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index 34242db..0b1764e 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -19,6 +19,7 @@ import { GitService, GitUri } from '../git/gitService'; import { Arrays } from '../system'; import { BranchNode, + BranchTrackingStatusNode, canDismissNode, CommitFileNode, CommitNode, @@ -28,7 +29,6 @@ import { StashFileNode, StashNode, StatusFileNode, - StatusUpstreamNode, TagNode, ViewNode, ViewRefNode @@ -134,8 +134,8 @@ export class ViewCommands implements Disposable { return; } - private pull(node: RepositoryNode | StatusUpstreamNode) { - if (node instanceof StatusUpstreamNode) { + private pull(node: RepositoryNode | BranchTrackingStatusNode) { + if (node instanceof BranchTrackingStatusNode) { node = node.getParent() as RepositoryNode; } if (!(node instanceof RepositoryNode)) return; @@ -143,8 +143,8 @@ export class ViewCommands implements Disposable { return node.pull(); } - private push(node: RepositoryNode | StatusUpstreamNode, force?: boolean) { - if (node instanceof StatusUpstreamNode) { + private push(node: RepositoryNode | BranchTrackingStatusNode, force?: boolean) { + if (node instanceof BranchTrackingStatusNode) { node = node.getParent() as RepositoryNode; } if (!(node instanceof RepositoryNode)) return; @@ -502,13 +502,13 @@ export class ViewCommands implements Disposable { this.sendTerminalCommand('rebase', `-i ${node.ref}`, node.repoPath); } - terminalRebaseBranchToRemote(node: BranchNode | StatusUpstreamNode) { + terminalRebaseBranchToRemote(node: BranchNode | BranchTrackingStatusNode) { if (node instanceof BranchNode) { if (!node.branch.current || !node.branch.tracking) return; this.sendTerminalCommand('rebase', `-i ${node.branch.tracking}`, node.repoPath); } - else if (node instanceof StatusUpstreamNode) { + else if (node instanceof BranchTrackingStatusNode) { this.sendTerminalCommand('rebase', `-i ${node.status.upstream}`, node.status.repoPath); } }