diff --git a/extensions/git/package.json b/extensions/git/package.json index 7e6339d29dc..96ece71a59f 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1004,6 +1004,24 @@ "title": "%command.graphCompareRef%", "category": "Git", "enablement": "!operationInProgress" + }, + { + "command": "git.graph.openIncomingChanges", + "title": "%command.graphOpenIncomingChanges%", + "category": "Git", + "enablement": "!operationInProgress && git.currentHistoryItemIsBehind" + }, + { + "command": "git.graph.openOutgoingChanges", + "title": "%command.graphOpenOutgoingChanges%", + "category": "Git", + "enablement": "!operationInProgress && git.currentHistoryItemIsAhead" + }, + { + "command": "git.graph.compareWithMergeBase", + "title": "%command.graphCompareWithMergeBase%", + "category": "Git", + "enablement": "!operationInProgress && git.currentHistoryItemHasMergeBase" } ], "continueEditSession": [ @@ -1612,6 +1630,18 @@ "command": "git.graph.cherryPick", "when": "false" }, + { + "command": "git.graph.openIncomingChanges", + "when": "false" + }, + { + "command": "git.graph.openOutgoingChanges", + "when": "false" + }, + { + "command": "git.graph.compareWithMergeBase", + "when": "false" + }, { "command": "git.diff.stageHunk", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && diffEditorOriginalUri =~ /^git\\:.*%22ref%22%3A%22~%22%7D$/" @@ -2230,6 +2260,21 @@ "command": "git.publish", "when": "scmProvider == git && !scmCurrentHistoryItemRefHasRemote", "group": "navigation@903" + }, + { + "command": "git.graph.openIncomingChanges", + "when": "scmProvider == git", + "group": "1_changes@1" + }, + { + "command": "git.graph.openOutgoingChanges", + "when": "scmProvider == git", + "group": "1_changes@2" + }, + { + "command": "git.graph.compareWithMergeBase", + "when": "scmProvider == git", + "group": "2_compare@1" } ], "scm/historyItem/context": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 80ffec8c433..fe81fc4322c 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -138,6 +138,9 @@ "command.graphDeleteBranch": "Delete Branch", "command.graphDeleteTag": "Delete Tag", "command.graphCompareRef": "Compare With...", + "command.graphOpenIncomingChanges": "Open Incoming Changes", + "command.graphOpenOutgoingChanges": "Open Outgoing Changes", + "command.graphCompareWithMergeBase": "Compare With Merge Base", "command.blameToggleEditorDecoration": "Toggle Git Blame Editor Decoration", "command.blameToggleStatusBarItem": "Toggle Git Blame Status Bar Item", "command.api.getRepositories": "Get Repositories", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index b6ff0a6ff30..096a1b4b8f3 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,7 +5,7 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, MessageOptions, Position, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages } from 'vscode'; +import { Command, commands, Disposable, MessageOptions, Position, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages, SourceControlHistoryItemRef } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import { ForcePushMode, GitErrorCodes, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref } from './api/git'; @@ -3142,8 +3142,9 @@ export class CommandCenter { return; } + const sourceCommit = sourceRef.ref.commit; + try { - const sourceCommit = sourceRef.ref.commit; const changes = await repository.diffTrees(sourceCommit, historyItem.id); if (changes.length === 0) { @@ -3164,7 +3165,65 @@ export class CommandCenter { resources }); } catch (err) { - throw new Error(l10n.t('Failed to compare references: {0}', err.message ?? err)); + window.showErrorMessage(l10n.t('Failed to compare references "{0}" and "{1}": {2}', sourceCommit, historyItem.id, err.message)); + } + } + + @command('git.graph.openIncomingChanges', { repository: true }) + async openIncomingChanges(repository: Repository): Promise { + await this._openChangesBetweenRefs( + repository, + repository.historyProvider.currentHistoryItemRef, + repository.historyProvider.currentHistoryItemRemoteRef); + } + + @command('git.graph.openOutgoingChanges', { repository: true }) + async openOutgoingChanges(repository: Repository): Promise { + await this._openChangesBetweenRefs( + repository, + repository.historyProvider.currentHistoryItemRemoteRef, + repository.historyProvider.currentHistoryItemRef); + } + + @command('git.graph.compareWithMergeBase', { repository: true }) + async compareWithMergeBase(repository: Repository): Promise { + await this._openChangesBetweenRefs( + repository, + repository.historyProvider.currentHistoryItemBaseRef, + repository.historyProvider.currentHistoryItemRef); + } + + private async _openChangesBetweenRefs( + repository: Repository, + ref1: SourceControlHistoryItemRef | undefined, + ref2: SourceControlHistoryItemRef | undefined + ): Promise { + if (!repository || !ref1 || !ref2) { + return; + } + + try { + const changes = await repository.diffTrees(ref1.id, ref2.id); + + if (changes.length === 0) { + window.showInformationMessage(l10n.t('There are no changes between "{0}" and "{1}".', ref1.name, ref2.name)); + return; + } + + const resources = changes.map(change => toMultiFileDiffEditorUris(change, ref1.id, ref2.id)); + const title = `${ref1.name} ↔ ${ref2.name}`; + const multiDiffSourceUri = Uri.from({ + scheme: 'git-ref-compare', + path: `${repository.root}/${ref1.id}..${ref2.id}` + }); + + await commands.executeCommand('_workbench.openMultiDiffEditor', { + multiDiffSourceUri, + title, + resources + }); + } catch (err) { + window.showErrorMessage(l10n.t('Failed to open changes between "{0}" and "{1}": {2}', ref1.name, ref2.name, err.message)); } } diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 99f68fbf534..abd6dccdc23 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent, workspace, ConfigurationChangeEvent, MarkdownString, Command } from 'vscode'; +import { CancellationToken, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent, workspace, ConfigurationChangeEvent, MarkdownString, Command, commands } from 'vscode'; import { Repository, Resource } from './repository'; import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, fromNow, getCommitShortHash, subject, truncate } from './util'; import { toMultiFileDiffEditorUris } from './uri'; @@ -185,6 +185,15 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } } + // Update context keys for HEAD + if (this._HEAD?.ahead !== this.repository.HEAD?.ahead) { + commands.executeCommand('setContext', 'git.currentHistoryItemIsAhead', (this.repository.HEAD?.ahead ?? 0) > 0); + } + if (this._HEAD?.behind !== this.repository.HEAD?.behind) { + commands.executeCommand('setContext', 'git.currentHistoryItemIsBehind', (this.repository.HEAD?.behind ?? 0) > 0); + } + commands.executeCommand('setContext', 'git.currentHistoryItemHasMergeBase', this._currentHistoryItemBaseRef !== undefined); + this._HEAD = this.repository.HEAD; this._currentHistoryItemRef = {