From 14d45f402a057422adccfdd47a6606255bd8658a Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 2 Jan 2021 04:18:30 -0500 Subject: [PATCH] Adds rebase status (wip) --- src/git/formatters/commitFormatter.ts | 9 +- src/git/gitService.ts | 149 +++++++++-- src/git/models/branch.ts | 3 +- src/git/models/merge.ts | 9 +- src/git/models/models.ts | 1 + src/git/models/rebase.ts | 15 ++ src/git/models/status.ts | 1 + src/views/commitsView.ts | 4 +- src/views/nodes.ts | 4 + src/views/nodes/branchNode.ts | 137 ++++++---- src/views/nodes/fileRevisionAsCommitNode.ts | 14 +- src/views/nodes/mergeConflictCurrentChangesNode.ts | 100 ++++++++ src/views/nodes/mergeConflictFileNode.ts | 126 ++++++++++ .../nodes/mergeConflictIncomingChangesNode.ts | 113 +++++++++ src/views/nodes/mergeStatusNode.ts | 279 +++------------------ src/views/nodes/rebaseStatusNode.ts | 210 ++++++++++++++++ src/views/nodes/repositoryNode.ts | 21 +- src/views/nodes/viewNode.ts | 5 +- src/views/viewCommands.ts | 2 +- 19 files changed, 850 insertions(+), 352 deletions(-) create mode 100644 src/git/models/rebase.ts create mode 100644 src/views/nodes/mergeConflictCurrentChangesNode.ts create mode 100644 src/views/nodes/mergeConflictFileNode.ts create mode 100644 src/views/nodes/mergeConflictIncomingChangesNode.ts create mode 100644 src/views/nodes/rebaseStatusNode.ts diff --git a/src/git/formatters/commitFormatter.ts b/src/git/formatters/commitFormatter.ts index ec9abb7..4ff9d63 100644 --- a/src/git/formatters/commitFormatter.ts +++ b/src/git/formatters/commitFormatter.ts @@ -33,6 +33,7 @@ const emptyStr = ''; export interface CommitFormatOptions extends FormatOptions { autolinkedIssuesOrPullRequests?: Map; + avatarSize?: number; dateStyle?: DateStyle; footnotes?: Map; getBranchAndTagTips?: (sha: string) => string | undefined; @@ -210,7 +211,7 @@ export class CommitFormatter extends Formatter { presence.status === 'dnd' ? 'in ' : emptyStr }${presence.statusText.toLocaleLowerCase()}`; - const avatarMarkdownPromise = this._getAvatarMarkdown(title); + const avatarMarkdownPromise = this._getAvatarMarkdown(title, this._options.avatarSize); return avatarMarkdownPromise.then(md => this._padOrTruncate( `${md}${this._getPresenceMarkdown(presence, title)}`, @@ -219,11 +220,11 @@ export class CommitFormatter extends Formatter { ); } - return this._getAvatarMarkdown(this._item.author); + return this._getAvatarMarkdown(this._item.author, this._options.avatarSize); } - private async _getAvatarMarkdown(title: string) { - const size = Container.config.hovers.avatarSize; + private async _getAvatarMarkdown(title: string, size?: number) { + size = size ?? Container.config.hovers.avatarSize; const avatarPromise = this._item.getAvatarUri({ defaultStyle: Container.config.defaultGravatarsStyle, size: size, diff --git a/src/git/gitService.ts b/src/git/gitService.ts index e26b0ae..16aa1e8 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -1,6 +1,7 @@ 'use strict'; import * as fs from 'fs'; import * as paths from 'path'; +import { TextDecoder } from 'util'; import { ConfigurationChangeEvent, Disposable, @@ -51,6 +52,7 @@ import { GitLogCommit, GitLogParser, GitMergeStatus, + GitRebaseStatus, GitReference, GitReflog, GitRemote, @@ -119,6 +121,8 @@ const weightedDefaultBranches = new Map([ ['development', 1], ]); +const textDecoder = new TextDecoder('utf8'); + export class GitService implements Disposable { private _onDidChangeRepositories = new EventEmitter(); get onDidChangeRepositories(): Event { @@ -1139,16 +1143,23 @@ export class GitService implements Disposable { const [name, tracking] = data[0].split('\n'); if (GitBranch.isDetached(name)) { - const committerDate = await Git.log__recent_committerdate(repoPath); + const [rebaseStatus, committerDate] = await Promise.all([ + this.getRebaseStatus(repoPath), + Git.log__recent_committerdate(repoPath), + ]); branch = new GitBranch( repoPath, - name, + rebaseStatus?.incoming.name ?? name, false, true, - committerDate == null ? undefined : new Date(Number(committerDate) * 1000), + committerDate != null ? new Date(Number(committerDate) * 1000) : undefined, data[1], tracking, + undefined, + undefined, + undefined, + rebaseStatus != null, ); } @@ -1214,17 +1225,24 @@ export class GitService implements Disposable { const data = await Git.rev_parse__currentBranch(repoPath); if (data != null) { - const committerDate = await Git.log__recent_committerdate(repoPath); - 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), + ]); + current = new GitBranch( repoPath, - name, + rebaseStatus?.incoming.name ?? name, false, true, - committerDate == null ? undefined : new Date(Number(committerDate) * 1000), + committerDate != null ? new Date(Number(committerDate) * 1000) : undefined, data[1], tracking, + undefined, + undefined, + undefined, + rebaseStatus != null, ); } @@ -1426,7 +1444,7 @@ export class GitService implements Disposable { const shortlog = GitShortLogParser.parse(data, repoPath); if (shortlog != null) { // Mark the current user - const currentUser = await Container.git.getCurrentUser(repoPath); + const currentUser = await this.getCurrentUser(repoPath); if (currentUser != null) { const index = shortlog.contributors.findIndex( c => currentUser.email === c.email && currentUser.name === c.name, @@ -2379,28 +2397,92 @@ export class GitService implements Disposable { } } - async getMergeStatus(repoPath: string): Promise; - async getMergeStatus(status: GitStatus): Promise; - @log({ args: false }) - async getMergeStatus(repoPathOrStatus: string | GitStatus): Promise { - let status; - if (typeof repoPathOrStatus === 'string') { - status = await this.getStatusForRepo(repoPathOrStatus); - } else { - status = repoPathOrStatus; - } - if (status?.hasConflicts !== true) return undefined; + @log() + async getMergeStatus(repoPath: string): Promise { + const merge = await Git.rev_parse__verify(repoPath, 'MERGE_HEAD'); + if (merge == null) return undefined; - const [mergeBase, possibleSourceBranches] = await Promise.all([ - Container.git.getMergeBase(status.repoPath, 'MERGE_HEAD', 'HEAD'), - Container.git.getCommitBranches(status.repoPath, 'MERGE_HEAD', { mode: 'pointsAt' }), + const [branch, mergeBase, possibleSourceBranches] = await Promise.all([ + this.getBranch(repoPath), + this.getMergeBase(repoPath, 'MERGE_HEAD', 'HEAD'), + this.getCommitBranches(repoPath, 'MERGE_HEAD', { mode: 'pointsAt' }), ]); + return { - repoPath: status.repoPath, + type: 'merge', + repoPath: repoPath, mergeBase: mergeBase, - conflicts: status.files.filter(f => f.conflicted), - into: status.branch, - incoming: possibleSourceBranches?.length === 1 ? possibleSourceBranches[0] : undefined, + HEAD: GitReference.create(merge, repoPath, { refType: 'revision' }), + current: GitReference.fromBranch(branch!), + incoming: + possibleSourceBranches?.length === 1 + ? GitReference.create(possibleSourceBranches[0], repoPath, { + refType: 'branch', + name: possibleSourceBranches[0], + remote: false, + }) + : undefined, + }; + } + + @log() + async getRebaseStatus(repoPath: string): Promise { + const rebase = await Git.rev_parse__verify(repoPath, 'REBASE_HEAD'); + if (rebase == null) return undefined; + + const [mergeBase, headNameBytes, ontoBytes, stepBytes, stepMessageBytes, stepsBytes] = await Promise.all([ + this.getMergeBase(repoPath, 'REBASE_HEAD', 'HEAD'), + workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'head-name'))), + workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'onto'))), + workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'msgnum'))), + workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'message'))), + workspace.fs.readFile(Uri.file(paths.join(repoPath, '.git', 'rebase-merge', 'end'))), + ]); + + let branch = textDecoder.decode(headNameBytes); + if (branch.startsWith('refs/heads/')) { + branch = branch.substr(11).trim(); + } + + const onto = textDecoder.decode(ontoBytes).trim(); + const step = Number.parseInt(textDecoder.decode(stepBytes).trim(), 10); + const steps = Number.parseInt(textDecoder.decode(stepsBytes).trim(), 10); + + const possibleSourceBranches = await this.getCommitBranches(repoPath, onto, { mode: 'pointsAt' }); + + let possibleSourceBranch: string | undefined; + for (const b of possibleSourceBranches) { + if (b.startsWith('(no branch, rebasing')) continue; + + possibleSourceBranch = b; + break; + } + + return { + type: 'rebase', + repoPath: repoPath, + mergeBase: mergeBase, + HEAD: GitReference.create(rebase, repoPath, { refType: 'revision' }), + current: + possibleSourceBranch != null + ? GitReference.create(possibleSourceBranch, repoPath, { + refType: 'branch', + name: possibleSourceBranch, + remote: false, + }) + : undefined, + + incoming: GitReference.create(branch, repoPath, { + refType: 'branch', + name: branch, + remote: false, + }), + step: step, + stepCurrent: GitReference.create(rebase, repoPath, { + refType: 'revision', + message: textDecoder.decode(stepMessageBytes).trim(), + }), + steps: steps, }; } @@ -3338,6 +3420,21 @@ export class GitService implements Disposable { similarityThreshold: Container.config.advanced.similarityThreshold, }); const status = GitStatusParser.parse(data, repoPath, porcelainVersion); + + if (status?.detached) { + const rebaseStatus = await this.getRebaseStatus(repoPath); + if (rebaseStatus != null) { + return new GitStatus( + repoPath, + rebaseStatus.incoming.name, + status.sha, + status.files, + status.state, + status.upstream, + true, + ); + } + } return status; } diff --git a/src/git/models/branch.ts b/src/git/models/branch.ts index 0d6a4c6..efffb6a 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -8,7 +8,7 @@ import { GitStatus } from './status'; import { Dates, debug, memoize } from '../../system'; const whitespaceRegex = /\s/; -const detachedHEADRegex = /^(?=.*\bHEAD\b)(?=.*\bdetached\b).*$/; +const detachedHEADRegex = /^(?=.*\bHEAD\b)?(?=.*\bdetached\b).*$/; export const BranchDateFormatting = { dateFormat: undefined! as string | null, @@ -96,6 +96,7 @@ export class GitBranch implements GitBranchReference { ahead: number = 0, behind: number = 0, detached: boolean = false, + public readonly rebasing: boolean = false, ) { this.id = `${repoPath}|${remote ? 'remotes/' : 'heads/'}${name}`; diff --git a/src/git/models/merge.ts b/src/git/models/merge.ts index feab95a..838e8ff 100644 --- a/src/git/models/merge.ts +++ b/src/git/models/merge.ts @@ -1,10 +1,11 @@ 'use strict'; -import { GitStatusFile } from './status'; +import { GitBranchReference, GitRevisionReference } from './models'; export interface GitMergeStatus { + type: 'merge'; repoPath: string; - into: string; + HEAD: GitRevisionReference; mergeBase: string | undefined; - incoming: string | undefined; - conflicts: GitStatusFile[]; + current: GitBranchReference; + incoming: GitBranchReference | undefined; } diff --git a/src/git/models/models.ts b/src/git/models/models.ts index d10f0e6..df33916 100644 --- a/src/git/models/models.ts +++ b/src/git/models/models.ts @@ -351,6 +351,7 @@ export * from './log'; export * from './logCommit'; export * from './merge'; export * from './pullRequest'; +export * from './rebase'; export * from './reflog'; export * from './remote'; export * from './repository'; diff --git a/src/git/models/rebase.ts b/src/git/models/rebase.ts new file mode 100644 index 0000000..426980e --- /dev/null +++ b/src/git/models/rebase.ts @@ -0,0 +1,15 @@ +'use strict'; +import { GitBranchReference, GitRevisionReference } from './models'; + +export interface GitRebaseStatus { + type: 'rebase'; + repoPath: string; + HEAD: GitRevisionReference; + mergeBase: string | undefined; + current: GitBranchReference | undefined; + incoming: GitBranchReference; + + step: number; + stepCurrent: GitRevisionReference; + steps: number; +} diff --git a/src/git/models/status.ts b/src/git/models/status.ts index 93c0705..630de92 100644 --- a/src/git/models/status.ts +++ b/src/git/models/status.ts @@ -32,6 +32,7 @@ export class GitStatus { public readonly files: GitStatusFile[], public readonly state: GitTrackingState, public readonly upstream?: string, + public readonly rebasing: boolean = false, ) { this.detached = GitBranch.isDetached(branch); if (this.detached) { diff --git a/src/views/commitsView.ts b/src/views/commitsView.ts index ddc3c68..b8a3ee8 100644 --- a/src/views/commitsView.ts +++ b/src/views/commitsView.ts @@ -217,8 +217,8 @@ export class CommitsViewNode extends ViewNode { const status = branch.getTrackingStatus(); this.view.description = `${status ? `${status} ${GlyphChars.Dot} ` : ''}${branch.name}${ - lastFetched ? ` ${GlyphChars.Dot} Last fetched ${Repository.formatLastFetched(lastFetched)}` : '' - }`; + branch.rebasing ? ' (Rebasing)' : '' + }${lastFetched ? ` ${GlyphChars.Dot} Last fetched ${Repository.formatLastFetched(lastFetched)}` : ''}`; } else { this.view.description = undefined; } diff --git a/src/views/nodes.ts b/src/views/nodes.ts index ac014f9..89ee4b3 100644 --- a/src/views/nodes.ts +++ b/src/views/nodes.ts @@ -21,8 +21,12 @@ export * from './nodes/fileRevisionAsCommitNode'; export * from './nodes/folderNode'; export * from './nodes/lineHistoryNode'; export * from './nodes/lineHistoryTrackerNode'; +export * from './nodes/mergeConflictFileNode'; +export * from './nodes/mergeConflictCurrentChangesNode'; +export * from './nodes/mergeConflictIncomingChangesNode'; export * from './nodes/mergeStatusNode'; export * from './nodes/pullRequestNode'; +export * from './nodes/rebaseStatusNode'; export * from './nodes/reflogNode'; export * from './nodes/reflogRecordNode'; export * from './nodes/remoteNode'; diff --git a/src/views/nodes/branchNode.ts b/src/views/nodes/branchNode.ts index 3c01533..8cde5bb 100644 --- a/src/views/nodes/branchNode.ts +++ b/src/views/nodes/branchNode.ts @@ -22,6 +22,7 @@ import { GitUri } from '../../git/gitUri'; import { insertDateMarkers } from './helpers'; import { MergeStatusNode } from './mergeStatusNode'; import { PullRequestNode } from './pullRequestNode'; +import { RebaseStatusNode } from './rebaseStatusNode'; import { RemotesView } from '../remotesView'; import { RepositoriesView } from '../repositoriesView'; import { RepositoryNode } from './repositoryNode'; @@ -42,6 +43,7 @@ export class BranchNode showAsCommits: boolean; showComparison: false | ViewShowBranchComparison; showCurrent: boolean; + showStatus: boolean; showTracking: boolean; authors?: string[]; }; @@ -60,6 +62,7 @@ export class BranchNode showAsCommits?: boolean; showComparison?: false | ViewShowBranchComparison; showCurrent?: boolean; + showStatus?: boolean; showTracking?: boolean; authors?: string[]; }, @@ -70,9 +73,11 @@ export class BranchNode expanded: false, showAsCommits: false, showComparison: false, - // Hide the current branch checkmark when the node is displayed as a root under the repository node + // Hide the current branch checkmark when the node is displayed as a root showCurrent: !this.root, - // Don't show tracking info the node is displayed as a root under the repository node + // Don't show merge/rebase status info the node is displayed as a root + showStatus: true, //!this.root, + // Don't show tracking info the node is displayed as a root showTracking: !this.root, ...options, }; @@ -96,14 +101,16 @@ export class BranchNode if (this.options.showAsCommits) return 'Commits'; const branchName = this.branch.getNameWithoutRemote(); - return this.view.config.branches?.layout !== ViewBranchesLayout.Tree || + return `${ + this.view.config.branches?.layout !== ViewBranchesLayout.Tree || this.compacted || this.root || this.current || this.branch.detached || this.branch.starred - ? branchName - : this.branch.getBasename(); + ? branchName + : this.branch.getBasename() + }${this.branch.rebasing ? ' (Rebasing)' : ''}`; } get ref(): GitBranchReference { @@ -121,12 +128,24 @@ export class BranchNode const children = []; const range = await Container.git.getBranchAheadRange(this.branch); - const [log, getBranchAndTagTips, mergeStatus, pr, unpublishedCommits] = await Promise.all([ + const [ + log, + getBranchAndTagTips, + status, + mergeStatus, + rebaseStatus, + pr, + unpublishedCommits, + ] = await Promise.all([ this.getLog(), Container.git.getBranchesAndTagsTipsFn(this.uri.repoPath, this.branch.name), - this.options.showTracking && this.branch.current + this.options.showStatus && this.branch.current + ? Container.git.getStatusForRepo(this.uri.repoPath) + : undefined, + this.options.showStatus && this.branch.current ? Container.git.getMergeStatus(this.uri.repoPath!) : undefined, + this.options.showStatus ? Container.git.getRebaseStatus(this.uri.repoPath!) : undefined, this.view.config.pullRequests.enabled && this.view.config.pullRequests.showForBranches && (this.branch.tracking || this.branch.remote) @@ -158,54 +177,62 @@ export class BranchNode children.push(new PullRequestNode(this.view, this, pr, this.branch)); } - if (this.options.showTracking) { - if (mergeStatus != null) { - children.push(new MergeStatusNode(this.view, this, this.branch, mergeStatus)); - } else { - const status = { - ref: this.branch.ref, - repoPath: this.branch.repoPath, - state: this.branch.state, - upstream: this.branch.tracking, - }; - - if (this.branch.tracking) { - if (this.root && !status.state.behind && !status.state.ahead) { + if (this.options.showStatus && mergeStatus != null) { + children.push( + new MergeStatusNode( + this.view, + this, + this.branch, + mergeStatus, + status ?? (await Container.git.getStatusForRepo(this.uri.repoPath)), + this.root, + ), + ); + } else if ( + this.options.showStatus && + rebaseStatus != null && + (this.branch.current || this.branch.name === rebaseStatus.incoming.name) + ) { + children.push( + new RebaseStatusNode( + this.view, + this, + this.branch, + rebaseStatus, + status ?? (await Container.git.getStatusForRepo(this.uri.repoPath)), + this.root, + ), + ); + } else if (this.options.showTracking) { + const status = { + ref: this.branch.ref, + repoPath: this.branch.repoPath, + state: this.branch.state, + upstream: this.branch.tracking, + }; + + if (this.branch.tracking) { + if (this.root && !status.state.behind && !status.state.ahead) { + children.push( + new BranchTrackingStatusNode(this.view, this, this.branch, status, 'same', this.root), + ); + } else { + if (status.state.behind) { children.push( - new BranchTrackingStatusNode(this.view, this, this.branch, status, 'same', this.root), + new BranchTrackingStatusNode(this.view, this, this.branch, status, 'behind', this.root), + ); + } + + if (status.state.ahead) { + children.push( + new BranchTrackingStatusNode(this.view, this, this.branch, status, 'ahead', this.root), ); - } else { - if (status.state.behind) { - children.push( - new BranchTrackingStatusNode( - this.view, - this, - this.branch, - status, - 'behind', - this.root, - ), - ); - } - - if (status.state.ahead) { - children.push( - new BranchTrackingStatusNode( - this.view, - this, - this.branch, - status, - 'ahead', - this.root, - ), - ); - } } - } else { - children.push( - new BranchTrackingStatusNode(this.view, this, this.branch, status, 'none', this.root), - ); } + } else { + children.push( + new BranchTrackingStatusNode(this.view, this, this.branch, status, 'none', this.root), + ); } } @@ -249,7 +276,7 @@ export class BranchNode let tooltip: string | MarkdownString = `${ this.current ? 'Current branch' : 'Branch' - } $(git-branch) ${this.branch.getNameWithoutRemote()}`; + } $(git-branch) ${this.branch.getNameWithoutRemote()}${this.branch.rebasing ? ' (Rebasing)' : ''}`; let contextValue: string = ContextValues.Branch; if (this.current) { @@ -303,7 +330,11 @@ export class BranchNode description = this.options.showAsCommits ? `${this.branch.getTrackingStatus({ suffix: Strings.pad(GlyphChars.Dot, 1, 1), - })}${this.branch.getNameWithoutRemote()}${Strings.pad(arrows, 2, 2)}${this.branch.tracking}` + })}${this.branch.getNameWithoutRemote()}${this.branch.rebasing ? ' (Rebasing)' : ''}${Strings.pad( + arrows, + 2, + 2, + )}${this.branch.tracking}` : `${this.branch.getTrackingStatus({ suffix: `${GlyphChars.Space} ` })}${arrows}${ GlyphChars.Space } ${this.branch.tracking}`; diff --git a/src/views/nodes/fileRevisionAsCommitNode.ts b/src/views/nodes/fileRevisionAsCommitNode.ts index 6a6b244..b0d4abc 100644 --- a/src/views/nodes/fileRevisionAsCommitNode.ts +++ b/src/views/nodes/fileRevisionAsCommitNode.ts @@ -15,7 +15,8 @@ import { } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { LineHistoryView } from '../lineHistoryView'; -import { MergeConflictCurrentChangesNode, MergeConflictIncomingChangesNode } from './mergeStatusNode'; +import { MergeConflictCurrentChangesNode } from './mergeConflictCurrentChangesNode'; +import { MergeConflictIncomingChangesNode } from './mergeConflictIncomingChangesNode'; import { ViewsWithCommits } from '../viewBase'; import { ContextValues, ViewNode, ViewRefFileNode } from './viewNode'; @@ -55,12 +56,15 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode { if (!this.commit.hasConflicts) return []; - const mergeStatus = await Container.git.getMergeStatus(this.commit.repoPath); - if (mergeStatus == null) return []; + const [mergeStatus, rebaseStatus] = await Promise.all([ + Container.git.getMergeStatus(this.commit.repoPath), + Container.git.getRebaseStatus(this.commit.repoPath), + ]); + if (mergeStatus == null && rebaseStatus == null) return []; return [ - new MergeConflictCurrentChangesNode(this.view, this, mergeStatus, this.file), - new MergeConflictIncomingChangesNode(this.view, this, mergeStatus, this.file), + new MergeConflictCurrentChangesNode(this.view, this, (mergeStatus ?? rebaseStatus)!, this.file), + new MergeConflictIncomingChangesNode(this.view, this, (mergeStatus ?? rebaseStatus)!, this.file), ]; } diff --git a/src/views/nodes/mergeConflictCurrentChangesNode.ts b/src/views/nodes/mergeConflictCurrentChangesNode.ts new file mode 100644 index 0000000..c09fc6a --- /dev/null +++ b/src/views/nodes/mergeConflictCurrentChangesNode.ts @@ -0,0 +1,100 @@ +'use strict'; +import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Commands, DiffWithCommandArgs } from '../../commands'; +import { BuiltInCommands, GlyphChars } from '../../constants'; +import { Container } from '../../container'; +import { FileHistoryView } from '../fileHistoryView'; +import { CommitFormatter, GitFile, GitMergeStatus, GitRebaseStatus, GitReference } from '../../git/git'; +import { GitUri } from '../../git/gitUri'; +import { LineHistoryView } from '../lineHistoryView'; +import { ViewsWithCommits } from '../viewBase'; +import { ContextValues, ViewNode } from './viewNode'; + +export class MergeConflictCurrentChangesNode extends ViewNode { + constructor( + view: ViewsWithCommits | FileHistoryView | LineHistoryView, + parent: ViewNode, + private readonly status: GitMergeStatus | GitRebaseStatus, + private readonly file: GitFile, + ) { + super(GitUri.fromFile(file, status.repoPath, 'HEAD'), view, parent); + } + + getChildren(): ViewNode[] { + return []; + } + + async getTreeItem(): Promise { + const commit = await Container.git.getCommit(this.status.repoPath, 'HEAD'); + + const item = new TreeItem('Current changes', TreeItemCollapsibleState.None); + item.contextValue = ContextValues.MergeConflictCurrentChanges; + item.description = `${GitReference.toString(this.status.current, { expand: false, icon: false })}${ + commit != null ? ` (${GitReference.toString(commit, { expand: false, icon: false })})` : ' (HEAD)' + }`; + item.iconPath = this.view.config.avatars + ? (await commit?.getAvatarUri({ defaultStyle: Container.config.defaultGravatarsStyle })) ?? + new ThemeIcon('diff') + : new ThemeIcon('diff'); + item.tooltip = new MarkdownString( + `Current changes to $(file)${GlyphChars.Space}${this.file.fileName} on ${GitReference.toString( + this.status.current, + )}${ + commit != null + ? `\n\n${await CommitFormatter.fromTemplateAsync( + `$(git-commit) \${id} ${GlyphChars.Dash} \${avatar} __\${author}__, \${ago}\${' via 'pullRequest}   _(\${date})_ \n\n\${message}`, + commit, + { + avatarSize: 16, + dateFormat: Container.config.defaultDateFormat, + markdown: true, + // messageAutolinks: true, + messageIndent: 4, + }, + )}` + : '' + }`, + true, + ); + item.command = this.getCommand(); + + return item; + } + + getCommand(): Command | undefined { + if (this.status.mergeBase == null) { + return { + title: 'Open Revision', + command: BuiltInCommands.Open, + arguments: [GitUri.toRevisionUri('HEAD', this.file.fileName, this.status.repoPath)], + }; + } + + const commandArgs: DiffWithCommandArgs = { + lhs: { + sha: this.status.mergeBase, + uri: GitUri.fromFile(this.file, this.status.repoPath, undefined, true), + title: `${this.file.fileName} (merge-base)`, + }, + rhs: { + sha: 'HEAD', + uri: GitUri.fromFile(this.file, this.status.repoPath), + title: `${this.file.fileName} (${GitReference.toString(this.status.current, { + expand: false, + icon: false, + })})`, + }, + repoPath: this.status.repoPath, + line: 0, + showOptions: { + preserveFocus: true, + preview: true, + }, + }; + return { + title: 'Open Changes', + command: Commands.DiffWith, + arguments: [commandArgs], + }; + } +} diff --git a/src/views/nodes/mergeConflictFileNode.ts b/src/views/nodes/mergeConflictFileNode.ts new file mode 100644 index 0000000..bdaf18c --- /dev/null +++ b/src/views/nodes/mergeConflictFileNode.ts @@ -0,0 +1,126 @@ +'use strict'; +import * as paths from 'path'; +import { Command, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { BuiltInCommands } from '../../constants'; +import { FileNode } from './folderNode'; +import { GitFile, GitMergeStatus, GitRebaseStatus, StatusFileFormatter } from '../../git/git'; +import { GitUri } from '../../git/gitUri'; +import { MergeConflictCurrentChangesNode } from './mergeConflictCurrentChangesNode'; +import { MergeConflictIncomingChangesNode } from './mergeConflictIncomingChangesNode'; +import { ViewsWithCommits } from '../viewBase'; +import { ContextValues, ViewNode } from './viewNode'; + +export class MergeConflictFileNode extends ViewNode implements FileNode { + constructor( + view: ViewsWithCommits, + parent: ViewNode, + public readonly status: GitMergeStatus | GitRebaseStatus, + public readonly file: GitFile, + ) { + super(GitUri.fromFile(file, status.repoPath, status.HEAD.ref), view, parent); + } + + toClipboard(): string { + return this.fileName; + } + + get baseUri(): Uri { + return GitUri.fromFile(this.file, this.status.repoPath, this.status.mergeBase ?? 'HEAD'); + } + + get fileName(): string { + return this.file.fileName; + } + + get repoPath(): string { + return this.status.repoPath; + } + + getChildren(): ViewNode[] { + return [ + new MergeConflictCurrentChangesNode(this.view, this, this.status, this.file), + new MergeConflictIncomingChangesNode(this.view, this, this.status, this.file), + ]; + } + + getTreeItem(): TreeItem { + const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed); + item.description = this.description; + item.contextValue = `${ContextValues.File}+conflicted`; + item.tooltip = StatusFileFormatter.fromTemplate( + // eslint-disable-next-line no-template-curly-in-string + '${file}\n${directory}/\n\n${status}${ (originalPath)} in Index (staged)', + this.file, + ); + // Use the file icon and decorations + item.resourceUri = GitUri.resolveToUri(this.file.fileName, this.repoPath); + item.iconPath = ThemeIcon.File; + item.command = this.getCommand(); + + // Only cache the label/description for a single refresh + this._label = undefined; + this._description = undefined; + + return item; + } + + private _description: string | undefined; + get description() { + if (this._description == null) { + this._description = StatusFileFormatter.fromTemplate( + this.view.config.formats.files.description, + this.file, + { + relativePath: this.relativePath, + }, + ); + } + return this._description; + } + + private _folderName: string | undefined; + get folderName() { + if (this._folderName == null) { + this._folderName = paths.dirname(this.uri.relativePath); + } + return this._folderName; + } + + private _label: string | undefined; + get label() { + if (this._label == null) { + this._label = StatusFileFormatter.fromTemplate(this.view.config.formats.files.label, this.file, { + relativePath: this.relativePath, + }); + } + return this._label; + } + + get priority(): number { + return 0; + } + + private _relativePath: string | undefined; + get relativePath(): string | undefined { + return this._relativePath; + } + set relativePath(value: string | undefined) { + this._relativePath = value; + this._label = undefined; + this._description = undefined; + } + + getCommand(): Command | undefined { + return { + title: 'Open File', + command: BuiltInCommands.Open, + arguments: [ + GitUri.resolveToUri(this.file.fileName, this.repoPath), + { + preserveFocus: true, + preview: true, + }, + ], + }; + } +} diff --git a/src/views/nodes/mergeConflictIncomingChangesNode.ts b/src/views/nodes/mergeConflictIncomingChangesNode.ts new file mode 100644 index 0000000..ab1ea65 --- /dev/null +++ b/src/views/nodes/mergeConflictIncomingChangesNode.ts @@ -0,0 +1,113 @@ +'use strict'; +import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Commands, DiffWithCommandArgs } from '../../commands'; +import { BuiltInCommands, GlyphChars } from '../../constants'; +import { Container } from '../../container'; +import { FileHistoryView } from '../fileHistoryView'; +import { CommitFormatter, GitFile, GitMergeStatus, GitRebaseStatus, GitReference } from '../../git/git'; +import { GitUri } from '../../git/gitUri'; +import { LineHistoryView } from '../lineHistoryView'; +import { ViewsWithCommits } from '../viewBase'; +import { ContextValues, ViewNode } from './viewNode'; + +export class MergeConflictIncomingChangesNode extends ViewNode { + constructor( + view: ViewsWithCommits | FileHistoryView | LineHistoryView, + parent: ViewNode, + private readonly status: GitMergeStatus | GitRebaseStatus, + private readonly file: GitFile, + ) { + super(GitUri.fromFile(file, status.repoPath, status.HEAD.ref), view, parent); + } + + getChildren(): ViewNode[] { + return []; + } + + async getTreeItem(): Promise { + const commit = await Container.git.getCommit( + this.status.repoPath, + this.status.type === 'rebase' ? this.status.stepCurrent.ref : this.status.HEAD.ref, + ); + + const item = new TreeItem('Incoming changes', TreeItemCollapsibleState.None); + item.contextValue = ContextValues.MergeConflictIncomingChanges; + item.description = `${GitReference.toString(this.status.incoming, { expand: false, icon: false })}${ + this.status.type === 'rebase' + ? ` (${GitReference.toString(this.status.stepCurrent, { expand: false, icon: false })})` + : ` (${GitReference.toString(this.status.HEAD, { expand: false, icon: false })})` + }`; + item.iconPath = this.view.config.avatars + ? (await commit?.getAvatarUri({ defaultStyle: Container.config.defaultGravatarsStyle })) ?? + new ThemeIcon('diff') + : new ThemeIcon('diff'); + item.tooltip = new MarkdownString( + `Incoming changes to $(file)${GlyphChars.Space}${this.file.fileName}${ + this.status.incoming != null + ? ` from ${GitReference.toString(this.status.incoming)}${ + commit != null + ? `\n\n${await CommitFormatter.fromTemplateAsync( + `$(git-commit) \${id} ${GlyphChars.Dash} \${avatar} __\${author}__, \${ago}\${' via 'pullRequest}   _(\${date})_ \n\n\${message}`, + commit, + { + avatarSize: 16, + dateFormat: Container.config.defaultDateFormat, + markdown: true, + // messageAutolinks: true, + messageIndent: 4, + }, + )}` + : this.status.type === 'rebase' + ? `\n\n${GitReference.toString(this.status.stepCurrent, { + capitalize: true, + label: false, + })}` + : `\n\n${GitReference.toString(this.status.HEAD, { capitalize: true, label: false })}` + }` + : '' + }`, + true, + ); + item.command = this.getCommand(); + + return item; + } + + getCommand(): Command | undefined { + if (this.status.mergeBase == null) { + return { + title: 'Open Revision', + command: BuiltInCommands.Open, + arguments: [GitUri.toRevisionUri(this.status.HEAD.ref, this.file.fileName, this.status.repoPath)], + }; + } + + const commandArgs: DiffWithCommandArgs = { + lhs: { + sha: this.status.mergeBase, + uri: GitUri.fromFile(this.file, this.status.repoPath, undefined, true), + title: `${this.file.fileName} (merge-base)`, + }, + rhs: { + sha: this.status.HEAD.ref, + uri: GitUri.fromFile(this.file, this.status.repoPath), + title: `${this.file.fileName} (${ + this.status.incoming != null + ? GitReference.toString(this.status.incoming, { expand: false, icon: false }) + : 'incoming' + })`, + }, + repoPath: this.status.repoPath, + line: 0, + showOptions: { + preserveFocus: true, + preview: true, + }, + }; + return { + title: 'Open Changes', + command: Commands.DiffWith, + arguments: [commandArgs], + }; + } +} diff --git a/src/views/nodes/mergeStatusNode.ts b/src/views/nodes/mergeStatusNode.ts index 3e80063..1e404cc 100644 --- a/src/views/nodes/mergeStatusNode.ts +++ b/src/views/nodes/mergeStatusNode.ts @@ -1,34 +1,36 @@ 'use strict'; import * as paths from 'path'; -import { Command, MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { BranchNode } from './branchNode'; -import { Commands, DiffWithCommandArgs } from '../../commands'; import { ViewFilesLayout } from '../../configuration'; -import { BuiltInCommands } from '../../constants'; import { FileNode, FolderNode } from './folderNode'; -import { GitBranch, GitFile, GitMergeStatus, StatusFileFormatter } from '../../git/git'; +import { GitBranch, GitMergeStatus, GitReference, GitStatus } from '../../git/git'; import { GitUri } from '../../git/gitUri'; +import { MergeConflictFileNode } from './mergeConflictFileNode'; import { Arrays, Strings } from '../../system'; -import { View, ViewsWithCommits } from '../viewBase'; +import { ViewsWithCommits } from '../viewBase'; import { ContextValues, ViewNode } from './viewNode'; export class MergeStatusNode extends ViewNode { static key = ':merge'; - static getId(repoPath: string, name: string): string { - return `${BranchNode.getId(repoPath, name, true)}${this.key}`; + static getId(repoPath: string, name: string, root: boolean): string { + return `${BranchNode.getId(repoPath, name, root)}${this.key}`; } constructor( view: ViewsWithCommits, parent: ViewNode, public readonly branch: GitBranch, - public readonly status: GitMergeStatus, + public readonly mergeStatus: GitMergeStatus, + public readonly status: GitStatus | undefined, + // Specifies that the node is shown as a root + public readonly root: boolean, ) { - super(GitUri.fromRepoPath(status.repoPath), view, parent); + super(GitUri.fromRepoPath(mergeStatus.repoPath), view, parent); } get id(): string { - return MergeStatusNode.getId(this.status.repoPath, this.status.into); + return MergeStatusNode.getId(this.mergeStatus.repoPath, this.mergeStatus.current.name, this.root); } get repoPath(): string { @@ -36,8 +38,10 @@ export class MergeStatusNode extends ViewNode { } getChildren(): ViewNode[] { + if (this.status?.hasConflicts !== true) return []; + let children: FileNode[] = this.status.conflicts.map( - f => new MergeConflictFileNode(this.view, this, this.status, f), + f => new MergeConflictFileNode(this.view, this, this.mergeStatus, f), ); if (this.view.config.files.layout !== ViewFilesLayout.List) { @@ -61,251 +65,32 @@ export class MergeStatusNode extends ViewNode { getTreeItem(): TreeItem { const item = new TreeItem( - `Resolve conflicts before merging ${this.status.incoming ? `${this.status.incoming} ` : ''}into ${ - this.status.into - }`, + `${this.status?.hasConflicts ? 'Resolve conflicts before merging' : 'Merging'} ${ + this.mergeStatus.incoming != null + ? `${GitReference.toString(this.mergeStatus.incoming, { expand: false, icon: false })} ` + : '' + }into ${GitReference.toString(this.mergeStatus.current, { expand: false, icon: false })}`, TreeItemCollapsibleState.Expanded, ); item.id = this.id; item.contextValue = ContextValues.Merge; - item.description = Strings.pluralize('conflict', this.status.conflicts.length); - item.iconPath = new ThemeIcon('warning', new ThemeColor('list.warningForeground')); + item.description = this.status?.hasConflicts + ? Strings.pluralize('conflict', this.status.conflicts.length) + : undefined; + item.iconPath = this.status?.hasConflicts + ? new ThemeIcon('warning', new ThemeColor('list.warningForeground')) + : new ThemeIcon('debug-pause', new ThemeColor('list.foreground')); item.tooltip = new MarkdownString( - `Merging ${this.status.incoming ? `$(git-branch) ${this.status.incoming} ` : ''} into $(git-branch) ${ - this.status.into - }\n\n${Strings.pluralize('conflicted file', this.status.conflicts.length)} - `, - true, - ); - - return item; - } -} - -export class MergeConflictFileNode extends ViewNode implements FileNode { - constructor(view: View, parent: ViewNode, private readonly status: GitMergeStatus, public readonly file: GitFile) { - super(GitUri.fromFile(file, status.repoPath, 'MERGE_HEAD'), view, parent); - } - - toClipboard(): string { - return this.fileName; - } - - get baseUri(): Uri { - return GitUri.fromFile(this.file, this.status.repoPath, this.status.mergeBase ?? 'HEAD'); - } - - get fileName(): string { - return this.file.fileName; - } - - get repoPath(): string { - return this.status.repoPath; - } - - getChildren(): ViewNode[] { - return [ - new MergeConflictCurrentChangesNode(this.view, this, this.status, this.file), - new MergeConflictIncomingChangesNode(this.view, this, this.status, this.file), - ]; - } - - getTreeItem(): TreeItem { - const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed); - item.description = this.description; - item.contextValue = `${ContextValues.File}+conflicted`; - item.tooltip = StatusFileFormatter.fromTemplate( - // eslint-disable-next-line no-template-curly-in-string - '${file}\n${directory}/\n\n${status}${ (originalPath)} in Index (staged)', - this.file, - ); - // Use the file icon and decorations - item.resourceUri = GitUri.resolveToUri(this.file.fileName, this.repoPath); - item.iconPath = ThemeIcon.File; - item.command = this.getCommand(); - - // Only cache the label/description for a single refresh - this._label = undefined; - this._description = undefined; - - return item; - } - - private _description: string | undefined; - get description() { - if (this._description == null) { - this._description = StatusFileFormatter.fromTemplate( - this.view.config.formats.files.description, - this.file, - { - relativePath: this.relativePath, - }, - ); - } - return this._description; - } - - private _folderName: string | undefined; - get folderName() { - if (this._folderName == null) { - this._folderName = paths.dirname(this.uri.relativePath); - } - return this._folderName; - } - - private _label: string | undefined; - get label() { - if (this._label == null) { - this._label = StatusFileFormatter.fromTemplate(this.view.config.formats.files.label, this.file, { - relativePath: this.relativePath, - }); - } - return this._label; - } - - get priority(): number { - return 0; - } - - private _relativePath: string | undefined; - get relativePath(): string | undefined { - return this._relativePath; - } - set relativePath(value: string | undefined) { - this._relativePath = value; - this._label = undefined; - this._description = undefined; - } - - getCommand(): Command | undefined { - return { - title: 'Open File', - command: BuiltInCommands.Open, - arguments: [ - GitUri.resolveToUri(this.file.fileName, this.repoPath), - { - preserveFocus: true, - preview: true, - }, - ], - }; - } -} - -export class MergeConflictCurrentChangesNode extends ViewNode { - constructor(view: View, parent: ViewNode, private readonly status: GitMergeStatus, private readonly file: GitFile) { - super(GitUri.fromFile(file, status.repoPath, 'HEAD'), view, parent); - } - - getChildren(): ViewNode[] { - return []; - } - - getTreeItem(): TreeItem { - const item = new TreeItem('Current changes', TreeItemCollapsibleState.None); - item.contextValue = ContextValues.MergeCurrentChanges; - item.description = this.status.into; - item.iconPath = new ThemeIcon('diff'); - item.tooltip = new MarkdownString( - `Current changes to $(file) ${this.file.fileName} on $(git-branch) ${this.status.into}`, - true, - ); - item.command = this.getCommand(); - - return item; - } - - getCommand(): Command | undefined { - if (this.status.mergeBase == null) { - return { - title: 'Open Revision', - command: BuiltInCommands.Open, - arguments: [GitUri.toRevisionUri('HEAD', this.file.fileName, this.status.repoPath)], - }; - } - - const commandArgs: DiffWithCommandArgs = { - lhs: { - sha: this.status.mergeBase, - uri: GitUri.fromFile(this.file, this.status.repoPath, undefined, true), - title: `${this.file.fileName} (merge-base)`, - }, - rhs: { - sha: 'HEAD', - uri: GitUri.fromFile(this.file, this.status.repoPath), - title: `${this.file.fileName} (${this.status.into})`, - }, - repoPath: this.status.repoPath, - line: 0, - showOptions: { - preserveFocus: true, - preview: true, - }, - }; - return { - title: 'Open Changes', - command: Commands.DiffWith, - arguments: [commandArgs], - }; - } -} - -export class MergeConflictIncomingChangesNode extends ViewNode { - constructor(view: View, parent: ViewNode, private readonly status: GitMergeStatus, private readonly file: GitFile) { - super(GitUri.fromFile(file, status.repoPath, 'MERGE_HEAD'), view, parent); - } - - getChildren(): ViewNode[] { - return []; - } - - getTreeItem(): TreeItem { - const item = new TreeItem('Incoming changes', TreeItemCollapsibleState.None); - item.contextValue = ContextValues.MergeIncomingChanges; - item.description = this.status.incoming; - item.iconPath = new ThemeIcon('diff'); - item.tooltip = new MarkdownString( - `Incoming changes to $(file) ${this.file.fileName}${ - this.status.incoming ? ` from $(git-branch) ${this.status.incoming}` : '' + `${`Merging ${ + this.mergeStatus.incoming != null ? GitReference.toString(this.mergeStatus.incoming) : '' + }into ${GitReference.toString(this.mergeStatus.current)}`}${ + this.status?.hasConflicts + ? `\n\n${Strings.pluralize('conflicted file', this.status.conflicts.length)}` + : '' }`, true, ); - item.command = this.getCommand(); return item; } - - getCommand(): Command | undefined { - if (this.status.mergeBase == null) { - return { - title: 'Open Revision', - command: BuiltInCommands.Open, - arguments: [GitUri.toRevisionUri('MERGE_HEAD', this.file.fileName, this.status.repoPath)], - }; - } - - const commandArgs: DiffWithCommandArgs = { - lhs: { - sha: this.status.mergeBase, - uri: GitUri.fromFile(this.file, this.status.repoPath, undefined, true), - title: `${this.file.fileName} (merge-base)`, - }, - rhs: { - sha: 'MERGE_HEAD', - uri: GitUri.fromFile(this.file, this.status.repoPath), - title: `${this.file.fileName} (${this.status.incoming ? this.status.incoming : 'incoming'})`, - }, - repoPath: this.status.repoPath, - line: 0, - showOptions: { - preserveFocus: true, - preview: true, - }, - }; - return { - title: 'Open Changes', - command: Commands.DiffWith, - arguments: [commandArgs], - }; - } } diff --git a/src/views/nodes/rebaseStatusNode.ts b/src/views/nodes/rebaseStatusNode.ts new file mode 100644 index 0000000..85466ba --- /dev/null +++ b/src/views/nodes/rebaseStatusNode.ts @@ -0,0 +1,210 @@ +'use strict'; +import * as paths from 'path'; +import { Command, MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { BranchNode } from './branchNode'; +import { ViewFilesLayout } from '../../configuration'; +import { FileNode, FolderNode } from './folderNode'; +import { + CommitFormatter, + GitBranch, + GitLogCommit, + GitRebaseStatus, + GitReference, + GitRevisionReference, + GitStatus, +} from '../../git/git'; +import { GitUri } from '../../git/gitUri'; +import { MergeConflictFileNode } from './mergeConflictFileNode'; +import { Arrays, Strings } from '../../system'; +import { ViewsWithCommits } from '../viewBase'; +import { ContextValues, ViewNode, ViewRefNode } from './viewNode'; +import { Container } from '../../container'; +import { GlyphChars } from '../../constants'; +import { CommitFileNode } from './commitFileNode'; +import { Commands, DiffWithPreviousCommandArgs } from '../../commands'; + +export class RebaseStatusNode extends ViewNode { + static key = ':rebase'; + static getId(repoPath: string, name: string, root: boolean): string { + return `${BranchNode.getId(repoPath, name, root)}${this.key}`; + } + + constructor( + view: ViewsWithCommits, + parent: ViewNode, + public readonly branch: GitBranch, + public readonly rebaseStatus: GitRebaseStatus, + public readonly status: GitStatus | undefined, + // Specifies that the node is shown as a root + public readonly root: boolean, + ) { + super(GitUri.fromRepoPath(rebaseStatus.repoPath), view, parent); + } + + get id(): string { + return RebaseStatusNode.getId(this.rebaseStatus.repoPath, this.rebaseStatus.incoming.name, this.root); + } + + get repoPath(): string { + return this.uri.repoPath!; + } + + async getChildren(): Promise { + if (this.status?.hasConflicts !== true) return []; + + let children: FileNode[] = this.status.conflicts.map( + f => new MergeConflictFileNode(this.view, this, this.rebaseStatus, f), + ); + + if (this.view.config.files.layout !== ViewFilesLayout.List) { + const hierarchy = Arrays.makeHierarchical( + children, + n => n.uri.relativePath.split('/'), + (...parts: string[]) => Strings.normalizePath(paths.join(...parts)), + this.view.config.files.compact, + ); + + const root = new FolderNode(this.view, this, this.repoPath, '', hierarchy); + children = root.getChildren() as FileNode[]; + } else { + children.sort((a, b) => + a.label!.localeCompare(b.label!, undefined, { numeric: true, sensitivity: 'base' }), + ); + } + + const commit = await Container.git.getCommit(this.rebaseStatus.repoPath, this.rebaseStatus.stepCurrent.ref); + if (commit != null) { + children.splice(0, 0, new RebaseCommitNode(this.view, this, commit) as any); + } + + return children; + } + + getTreeItem(): TreeItem { + const item = new TreeItem( + `${this.status?.hasConflicts ? 'Resolve conflicts to continue rebasing' : 'Rebasing'} ${ + this.rebaseStatus.incoming != null + ? `${GitReference.toString(this.rebaseStatus.incoming, { expand: false, icon: false })} ` + : '' + }(${this.rebaseStatus.step}/${this.rebaseStatus.steps})`, + TreeItemCollapsibleState.Expanded, + ); + item.id = this.id; + item.contextValue = ContextValues.Rebase; + item.description = this.status?.hasConflicts + ? Strings.pluralize('conflict', this.status.conflicts.length) + : undefined; + item.iconPath = this.status?.hasConflicts + ? new ThemeIcon('warning', new ThemeColor('list.warningForeground')) + : new ThemeIcon('debug-pause', new ThemeColor('list.foreground')); + item.tooltip = new MarkdownString( + `${`Rebasing ${ + this.rebaseStatus.incoming != null ? GitReference.toString(this.rebaseStatus.incoming) : '' + }onto ${GitReference.toString(this.rebaseStatus.current)}`}\n\nStep ${this.rebaseStatus.step} of ${ + this.rebaseStatus.steps + }\\\nStopped at ${GitReference.toString(this.rebaseStatus.stepCurrent, { icon: true })}${ + this.status?.hasConflicts + ? `\n\n${Strings.pluralize('conflicted file', this.status.conflicts.length)}` + : '' + }`, + true, + ); + + return item; + } +} + +export class RebaseCommitNode extends ViewRefNode { + constructor(view: ViewsWithCommits, parent: ViewNode, public readonly commit: GitLogCommit) { + super(commit.toGitUri(), view, parent); + } + + toClipboard(): string { + let message = this.commit.message; + const index = message.indexOf('\n'); + if (index !== -1) { + message = `${message.substring(0, index)}${GlyphChars.Space}${GlyphChars.Ellipsis}`; + } + + return `${this.commit.shortSha}: ${message}`; + } + + get ref(): GitRevisionReference { + return this.commit; + } + + private get tooltip() { + return CommitFormatter.fromTemplate( + `\${author}\${ (email)} ${ + GlyphChars.Dash + } \${id}\${ (tips)}\n\${ago} (\${date})\${\n\nmessage}${this.commit.getFormattedDiffStatus({ + expand: true, + prefix: '\n\n', + separator: '\n', + })}\${\n\n${GlyphChars.Dash.repeat(2)}\nfootnotes}`, + this.commit, + { + dateFormat: Container.config.defaultDateFormat, + messageIndent: 4, + }, + ); + } + + getChildren(): ViewNode[] { + const commit = this.commit; + + let children: FileNode[] = commit.files.map( + s => new CommitFileNode(this.view, this, s, commit.toFileCommit(s)!), + ); + + if (this.view.config.files.layout !== ViewFilesLayout.List) { + const hierarchy = Arrays.makeHierarchical( + children, + n => n.uri.relativePath.split('/'), + (...parts: string[]) => Strings.normalizePath(paths.join(...parts)), + this.view.config.files.compact, + ); + + const root = new FolderNode(this.view, this, this.repoPath, '', hierarchy); + children = root.getChildren() as FileNode[]; + } else { + children.sort((a, b) => + a.label!.localeCompare(b.label!, undefined, { numeric: true, sensitivity: 'base' }), + ); + } + + return children; + } + + getTreeItem(): TreeItem { + const item = new TreeItem(`Stopped at commit ${this.commit.shortSha}`, TreeItemCollapsibleState.Collapsed); + + // item.contextValue = ContextValues.RebaseCommit; + + // eslint-disable-next-line no-template-curly-in-string + item.description = CommitFormatter.fromTemplate('${message}', this.commit, { + messageTruncateAtNewLine: true, + }); + item.iconPath = new ThemeIcon('git-commit'); + item.tooltip = this.tooltip; + + return item; + } + + getCommand(): Command | undefined { + const commandArgs: DiffWithPreviousCommandArgs = { + commit: this.commit, + uri: this.uri, + line: 0, + showOptions: { + preserveFocus: true, + preview: true, + }, + }; + return { + title: 'Open Changes with Previous Revision', + command: Commands.DiffWithPrevious, + arguments: [undefined, commandArgs], + }; + } +} diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts index b22d787..79b2c71 100644 --- a/src/views/nodes/repositoryNode.ts +++ b/src/views/nodes/repositoryNode.ts @@ -21,6 +21,7 @@ import { BranchTrackingStatusNode } from './branchTrackingStatusNode'; import { MessageNode } from './common'; import { ContributorsNode } from './contributorsNode'; import { MergeStatusNode } from './mergeStatusNode'; +import { RebaseStatusNode } from './rebaseStatusNode'; import { ReflogNode } from './reflogNode'; import { RemotesNode } from './remotesNode'; import { StashesNode } from './stashesNode'; @@ -68,6 +69,7 @@ export class RepositoryNode extends SubscribeableViewNode { status.state.ahead, status.state.behind, status.detached, + status.rebasing, ); if (this.view.config.showBranchComparison !== false) { @@ -83,11 +85,15 @@ export class RepositoryNode extends SubscribeableViewNode { ); } - if (status.hasConflicts) { - const mergeStatus = await Container.git.getMergeStatus(status); - if (mergeStatus != null) { - children.push(new MergeStatusNode(this.view, this, branch, mergeStatus)); - } + const [mergeStatus, rebaseStatus] = await Promise.all([ + Container.git.getMergeStatus(status.repoPath), + Container.git.getRebaseStatus(status.repoPath), + ]); + + if (mergeStatus != null) { + children.push(new MergeStatusNode(this.view, this, branch, mergeStatus, status, true)); + } else if (rebaseStatus != null) { + children.push(new RebaseStatusNode(this.view, this, branch, rebaseStatus, status, true)); } else if (this.view.config.showUpstreamStatus) { if (status.upstream) { if (!status.state.behind && !status.state.ahead) { @@ -127,6 +133,7 @@ export class RepositoryNode extends SubscribeableViewNode { showAsCommits: true, showComparison: false, showCurrent: false, + showStatus: false, showTracking: false, }), ); @@ -186,7 +193,7 @@ export class RepositoryNode extends SubscribeableViewNode { const status = await this._status; if (status != null) { - tooltip += `\n\nCurrent branch $(git-branch) ${status.branch}`; + tooltip += `\n\nCurrent branch $(git-branch) ${status.branch}${status.rebasing ? ' (Rebasing)' : ''}`; if (this.view.config.includeWorkingTree && status.files.length !== 0) { workingStatus = status.getFormattedDiffStatus({ @@ -199,7 +206,7 @@ export class RepositoryNode extends SubscribeableViewNode { suffix: Strings.pad(GlyphChars.Dot, 1, 1), }); - description = `${upstreamStatus}${status.branch}${workingStatus}`; + description = `${upstreamStatus}${status.branch}${status.rebasing ? ' (Rebasing)' : ''}${workingStatus}`; let providerName; if (status.upstream != null) { diff --git a/src/views/nodes/viewNode.ts b/src/views/nodes/viewNode.ts index 8a16460..af2581e 100644 --- a/src/views/nodes/viewNode.ts +++ b/src/views/nodes/viewNode.ts @@ -40,11 +40,12 @@ export enum ContextValues { Folder = 'gitlens:folder', LineHistory = 'gitlens:history:line', Merge = 'gitlens:merge', - MergeCurrentChanges = 'gitlens:merge:current', - MergeIncomingChanges = 'gitlens:merge:incoming', + MergeConflictCurrentChanges = 'gitlens:merge-conflict:current', + MergeConflictIncomingChanges = 'gitlens:merge-conflict:incoming', Message = 'gitlens:message', Pager = 'gitlens:pager', PullRequest = 'gitlens:pullrequest', + Rebase = 'gitlens:rebase', Reflog = 'gitlens:reflog', ReflogRecord = 'gitlens:reflog-record', Remote = 'gitlens:remote', diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index 055cbe0..f937889 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -777,7 +777,7 @@ export class ViewCommands { if (node instanceof MergeConflictFileNode) { void executeCommand(Commands.DiffWith, { lhs: { - sha: 'MERGE_HEAD', + sha: node.status.HEAD.ref, uri: GitUri.fromFile(node.file, node.repoPath, undefined, true), }, rhs: {