2837 linhas
90 KiB

import type { ColorTheme, ConfigurationChangeEvent, Uri, ViewColumn } from 'vscode';
import { CancellationTokenSource, Disposable, env, window } from 'vscode';
import type { CreatePullRequestActionContext } from '../../../api/gitlens';
import { getAvatarUri } from '../../../avatars';
import type {
CopyDeepLinkCommandArgs,
CopyMessageToClipboardCommandArgs,
CopyShaToClipboardCommandArgs,
OpenOnRemoteCommandArgs,
OpenPullRequestOnRemoteCommandArgs,
ShowCommitsInViewCommandArgs,
} from '../../../commands';
import { parseCommandContext } from '../../../commands/base';
import type { Config, GraphMinimapMarkersAdditionalTypes, GraphScrollMarkersAdditionalTypes } from '../../../config';
import type { StoredGraphFilters, StoredGraphIncludeOnlyRef, StoredGraphRefType } from '../../../constants';
import { Commands, GlyphChars } from '../../../constants';
import type { Container } from '../../../container';
import type { CommitSelectedEvent } from '../../../eventBus';
import { PlusFeatures } from '../../../features';
import * as BranchActions from '../../../git/actions/branch';
import {
openAllChanges,
openAllChangesWithWorking,
openFiles,
openFilesAtRevision,
showGraphDetailsView,
} from '../../../git/actions/commit';
import * as ContributorActions from '../../../git/actions/contributor';
import * as RepoActions from '../../../git/actions/repository';
import * as StashActions from '../../../git/actions/stash';
import * as TagActions from '../../../git/actions/tag';
import * as WorktreeActions from '../../../git/actions/worktree';
import { GitSearchError } from '../../../git/errors';
import { getBranchId, getBranchNameWithoutRemote, getRemoteNameFromBranchName } from '../../../git/models/branch';
import type { GitCommit } from '../../../git/models/commit';
import { uncommitted } from '../../../git/models/constants';
import { GitContributor } from '../../../git/models/contributor';
import type { GitGraph } from '../../../git/models/graph';
import { GitGraphRowType } from '../../../git/models/graph';
import type {
GitBranchReference,
GitReference,
GitRevisionReference,
GitStashReference,
GitTagReference,
} from '../../../git/models/reference';
import {
createReference,
getReferenceFromBranch,
getReferenceLabel,
isGitReference,
isSha,
shortenRevision,
} from '../../../git/models/reference';
import { getRemoteIconUri } from '../../../git/models/remote';
import { RemoteResourceType } from '../../../git/models/remoteResource';
import type { RepositoryChangeEvent, RepositoryFileSystemChangeEvent } from '../../../git/models/repository';
import {
isRepository,
Repository,
RepositoryChange,
RepositoryChangeComparisonMode,
} from '../../../git/models/repository';
import type { GitSearch } from '../../../git/search';
import { getSearchQueryComparisonKey } from '../../../git/search';
import { showRepositoryPicker } from '../../../quickpicks/repositoryPicker';
import {
executeActionCommand,
executeCommand,
executeCoreCommand,
executeCoreGitCommand,
registerCommand,
} from '../../../system/command';
import { configuration } from '../../../system/configuration';
import { getContext, onDidChangeContext } from '../../../system/context';
import { gate } from '../../../system/decorators/gate';
import { debug } from '../../../system/decorators/log';
import type { Deferrable } from '../../../system/function';
import { debounce, disposableInterval } from '../../../system/function';
import { find, last, map } from '../../../system/iterable';
import { updateRecordValue } from '../../../system/object';
import { getSettledValue } from '../../../system/promise';
import { isDarkTheme, isLightTheme } from '../../../system/utils';
import { isWebviewItemContext, isWebviewItemGroupContext, serializeWebviewItemContext } from '../../../system/webview';
import { RepositoryFolderNode } from '../../../views/nodes/viewNode';
import type { IpcMessage, IpcNotificationType } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController';
import { isSerializedState } from '../../../webviews/webviewsController';
import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService';
import type {
BranchState,
DimMergeCommitsParams,
DoubleClickedParams,
EnsureRowParams,
GetMissingAvatarsParams,
GetMissingRefsMetadataParams,
GetMoreRowsParams,
GraphBranchContextValue,
GraphColumnConfig,
GraphColumnName,
GraphColumnsConfig,
GraphColumnsSettings,
GraphCommitContextValue,
GraphComponentConfig,
GraphContributorContextValue,
GraphExcludedRef,
GraphExcludeRefs,
GraphExcludeTypes,
GraphHostingServiceType,
GraphIncludeOnlyRef,
GraphItemContext,
GraphItemGroupContext,
GraphItemRefContext,
GraphItemRefGroupContext,
GraphItemTypedContext,
GraphItemTypedContextValue,
GraphMinimapMarkerTypes,
GraphMissingRefsMetadataType,
GraphPullRequestContextValue,
GraphPullRequestMetadata,
GraphRefMetadata,
GraphRefMetadataType,
GraphRepository,
GraphScrollMarkerTypes,
GraphSelectedRows,
GraphStashContextValue,
GraphTagContextValue,
GraphUpstreamMetadata,
GraphUpstreamStatusContextValue,
GraphWorkingTreeStats,
SearchOpenInViewParams,
SearchParams,
State,
UpdateColumnsParams,
UpdateExcludeTypeParams,
UpdateGraphConfigurationParams,
UpdateRefsVisibilityParams,
UpdateSelectionParams,
} from './protocol';
import {
ChooseRepositoryCommandType,
DidChangeAvatarsNotificationType,
DidChangeColumnsNotificationType,
DidChangeFocusNotificationType,
DidChangeGraphConfigurationNotificationType,
DidChangeNotificationType,
DidChangeRefsMetadataNotificationType,
DidChangeRefsVisibilityNotificationType,
DidChangeRowsNotificationType,
DidChangeRowsStatsNotificationType,
DidChangeScrollMarkersNotificationType,
DidChangeSelectionNotificationType,
DidChangeSubscriptionNotificationType,
DidChangeWindowFocusNotificationType,
DidChangeWorkingTreeNotificationType,
DidEnsureRowNotificationType,
DidFetchNotificationType,
DidSearchNotificationType,
DimMergeCommitsCommandType,
DoubleClickedCommandType,
EnsureRowCommandType,
GetMissingAvatarsCommandType,
GetMissingRefsMetadataCommandType,
GetMoreRowsCommandType,
GraphRefMetadataTypes,
SearchCommandType,
SearchOpenInViewCommandType,
supportedRefMetadataTypes,
UpdateColumnsCommandType,
UpdateExcludeTypeCommandType,
UpdateGraphConfigurationCommandType,
UpdateIncludeOnlyRefsCommandType,
UpdateRefsVisibilityCommandType,
UpdateSelectionCommandType,
} from './protocol';
const defaultGraphColumnsSettings: GraphColumnsSettings = {
ref: { width: 130, isHidden: false, order: 0 },
graph: { width: 150, mode: undefined, isHidden: false, order: 1 },
message: { width: 300, isHidden: false, order: 2 },
author: { width: 130, isHidden: false, order: 3 },
changes: { width: 200, isHidden: false, order: 4 },
datetime: { width: 130, isHidden: false, order: 5 },
sha: { width: 130, isHidden: false, order: 6 },
};
const compactGraphColumnsSettings: GraphColumnsSettings = {
ref: { width: 32, isHidden: false },
graph: { width: 150, mode: 'compact', isHidden: false },
author: { width: 32, isHidden: false, order: 2 },
message: { width: 500, isHidden: false, order: 3 },
changes: { width: 200, isHidden: false, order: 4 },
datetime: { width: 130, isHidden: true, order: 5 },
sha: { width: 130, isHidden: false, order: 6 },
};
export class GraphWebviewProvider implements WebviewProvider<State> {
private _repository?: Repository;
private get repository(): Repository | undefined {
return this._repository;
}
private set repository(value: Repository | undefined) {
if (this._repository === value) {
this.ensureRepositorySubscriptions();
return;
}
this._repository = value;
this.resetRepositoryState();
this.ensureRepositorySubscriptions(true);
if (this.host.ready) {
this.updateState();
}
}
private _selection: readonly GitRevisionReference[] | undefined;
private get activeSelection(): GitRevisionReference | undefined {
return this._selection?.[0];
}
private readonly _disposable: Disposable;
private _etagSubscription?: number;
private _etagRepository?: number;
private _firstSelection = true;
private _graph?: GitGraph;
private readonly _ipcNotificationMap = new Map<IpcNotificationType<any>, () => Promise<boolean>>([
[DidChangeColumnsNotificationType, this.notifyDidChangeColumns],
[DidChangeGraphConfigurationNotificationType, this.notifyDidChangeConfiguration],
[DidChangeNotificationType, this.notifyDidChangeState],
[DidChangeRefsVisibilityNotificationType, this.notifyDidChangeRefsVisibility],
[DidChangeScrollMarkersNotificationType, this.notifyDidChangeScrollMarkers],
[DidChangeSelectionNotificationType, this.notifyDidChangeSelection],
[DidChangeSubscriptionNotificationType, this.notifyDidChangeSubscription],
[DidChangeWorkingTreeNotificationType, this.notifyDidChangeWorkingTree],
[DidChangeWindowFocusNotificationType, this.notifyDidChangeWindowFocus],
[DidFetchNotificationType, this.notifyDidFetch],
]);
private _refsMetadata: Map<string, GraphRefMetadata | null> | null | undefined;
private _search: GitSearch | undefined;
private _searchCancellation: CancellationTokenSource | undefined;
private _selectedId?: string;
private _selectedRows: GraphSelectedRows | undefined;
private _showDetailsView: Config['graph']['showDetailsView'];
private _theme: ColorTheme | undefined;
private _repositoryEventsDisposable: Disposable | undefined;
private _lastFetchedDisposable: Disposable | undefined;
private isWindowFocused: boolean = true;
constructor(
private readonly container: Container,
private readonly host: WebviewController<State>,
) {
this._showDetailsView = configuration.get('graph.showDetailsView');
this._theme = window.activeColorTheme;
this.ensureRepositorySubscriptions();
if (this.host.isView()) {
this.host.description = '✨';
}
this._disposable = Disposable.from(
configuration.onDidChange(this.onConfigurationChanged, this),
this.container.subscription.onDidChange(this.onSubscriptionChanged, this),
this.container.git.onDidChangeRepositories(() => void this.host.refresh(true)),
window.onDidChangeActiveColorTheme(this.onThemeChanged, this),
{
dispose: () => {
if (this._repositoryEventsDisposable == null) return;
this._repositoryEventsDisposable.dispose();
this._repositoryEventsDisposable = undefined;
},
},
);
}
dispose() {
this._disposable.dispose();
}
async onShowing(
loading: boolean,
_options: { column?: ViewColumn; preserveFocus?: boolean },
...args: [Repository, { ref: GitReference }, { state: Partial<State> }] | unknown[]
): Promise<boolean> {
this._firstSelection = true;
const [arg] = args;
if (isRepository(arg)) {
this.repository = arg;
} else if (hasGitReference(arg)) {
this.repository = this.container.git.getRepository(arg.ref.repoPath);
let id = arg.ref.ref;
if (!isSha(id)) {
id = await this.container.git.resolveReference(arg.ref.repoPath, id, undefined, {
force: true,
});
}
this.setSelectedRows(id);
if (this._graph != null) {
if (this._graph?.ids.has(id)) {
void this.notifyDidChangeSelection();
return true;
}
void this.onGetMoreRows({ id: id }, true);
}
} else {
if (isSerializedState<State>(arg) && arg.state.selectedRepository != null) {
this.repository = this.container.git.getRepository(arg.state.selectedRepository);
}
if (this.repository == null && this.container.git.repositoryCount > 1) {
const [contexts] = parseCommandContext(Commands.ShowGraph, undefined, ...args);
const context = Array.isArray(contexts) ? contexts[0] : contexts;
if (context.type === 'scm' && context.scm.rootUri != null) {
this.repository = this.container.git.getRepository(context.scm.rootUri);
} else if (context.type === 'viewItem' && context.node instanceof RepositoryFolderNode) {
this.repository = context.node.repo;
}
if (this.repository != null && !loading && this.host.ready) {
this.updateState();
}
}
}
return true;
}
onRefresh(force?: boolean) {
if (force) {
this.resetRepositoryState();
}
}
includeBootstrap(): Promise<State> {
return this.getState(true);
}
registerCommands(): Disposable[] {
return [
registerCommand(Commands.RefreshGraph, () => this.host.refresh(true)),
registerCommand('gitlens.graph.push', this.push, this),
registerCommand('gitlens.graph.pull', this.pull, this),
registerCommand('gitlens.graph.fetch', this.fetch, this),
registerCommand('gitlens.graph.publishBranch', this.publishBranch, this),
registerCommand('gitlens.graph.switchToAnotherBranch', this.switchToAnother, this),
registerCommand('gitlens.graph.createBranch', this.createBranch, this),
registerCommand('gitlens.graph.deleteBranch', this.deleteBranch, this),
registerCommand('gitlens.graph.copyRemoteBranchUrl', item => this.openBranchOnRemote(item, true), this),
registerCommand('gitlens.graph.openBranchOnRemote', this.openBranchOnRemote, this),
registerCommand('gitlens.graph.mergeBranchInto', this.mergeBranchInto, this),
registerCommand('gitlens.graph.rebaseOntoBranch', this.rebase, this),
registerCommand('gitlens.graph.rebaseOntoUpstream', this.rebaseToRemote, this),
registerCommand('gitlens.graph.renameBranch', this.renameBranch, this),
registerCommand('gitlens.graph.switchToBranch', this.switchTo, this),
registerCommand('gitlens.graph.hideLocalBranch', this.hideRef, this),
registerCommand('gitlens.graph.hideRemoteBranch', this.hideRef, this),
registerCommand('gitlens.graph.hideRemote', item => this.hideRef(item, { remote: true }), this),
registerCommand('gitlens.graph.hideRefGroup', item => this.hideRef(item, { group: true }), this),
registerCommand('gitlens.graph.hideTag', this.hideRef, this),
registerCommand('gitlens.graph.cherryPick', this.cherryPick, this),
registerCommand('gitlens.graph.copyRemoteCommitUrl', item => this.openCommitOnRemote(item, true), this),
registerCommand('gitlens.graph.showInDetailsView', this.openInDetailsView, this),
registerCommand('gitlens.graph.openCommitOnRemote', this.openCommitOnRemote, this),
registerCommand('gitlens.graph.openSCM', this.openSCM, this),
registerCommand('gitlens.graph.rebaseOntoCommit', this.rebase, this),
registerCommand('gitlens.graph.resetCommit', this.resetCommit, this),
registerCommand('gitlens.graph.resetToCommit', this.resetToCommit, this),
registerCommand('gitlens.graph.revert', this.revertCommit, this),
registerCommand('gitlens.graph.switchToCommit', this.switchTo, this),
registerCommand('gitlens.graph.undoCommit', this.undoCommit, this),
registerCommand('gitlens.graph.saveStash', this.saveStash, this),
registerCommand('gitlens.graph.applyStash', this.applyStash, this),
registerCommand('gitlens.graph.stash.delete', this.deleteStash, this),
registerCommand('gitlens.graph.stash.rename', this.renameStash, this),
registerCommand('gitlens.graph.createTag', this.createTag, this),
registerCommand('gitlens.graph.deleteTag', this.deleteTag, this),
registerCommand('gitlens.graph.switchToTag', this.switchTo, this),
registerCommand('gitlens.graph.createWorktree', this.createWorktree, this),
registerCommand('gitlens.graph.createPullRequest', this.createPullRequest, this),
registerCommand('gitlens.graph.openPullRequestOnRemote', this.openPullRequestOnRemote, this),
registerCommand('gitlens.graph.compareWithUpstream', this.compareWithUpstream, this),
registerCommand('gitlens.graph.compareWithHead', this.compareHeadWith, this),
registerCommand('gitlens.graph.compareWithWorking', this.compareWorkingWith, this),
registerCommand('gitlens.graph.compareAncestryWithWorking', this.compareAncestryWithWorking, this),
registerCommand('gitlens.graph.copy', this.copy, this),
registerCommand('gitlens.graph.copyMessage', this.copyMessage, this),
registerCommand('gitlens.graph.copySha', this.copySha, this),
registerCommand('gitlens.graph.addAuthor', this.addAuthor, this),
registerCommand('gitlens.graph.columnAuthorOn', () => this.toggleColumn('author', true)),
registerCommand('gitlens.graph.columnAuthorOff', () => this.toggleColumn('author', false)),
registerCommand('gitlens.graph.columnDateTimeOn', () => this.toggleColumn('datetime', true)),
registerCommand('gitlens.graph.columnDateTimeOff', () => this.toggleColumn('datetime', false)),
registerCommand('gitlens.graph.columnShaOn', () => this.toggleColumn('sha', true)),
registerCommand('gitlens.graph.columnShaOff', () => this.toggleColumn('sha', false)),
registerCommand('gitlens.graph.columnChangesOn', () => this.toggleColumn('changes', true)),
registerCommand('gitlens.graph.columnChangesOff', () => this.toggleColumn('changes', false)),
registerCommand('gitlens.graph.columnGraphOn', () => this.toggleColumn('graph', true)),
registerCommand('gitlens.graph.columnGraphOff', () => this.toggleColumn('graph', false)),
registerCommand('gitlens.graph.columnMessageOn', () => this.toggleColumn('message', true)),
registerCommand('gitlens.graph.columnMessageOff', () => this.toggleColumn('message', false)),
registerCommand('gitlens.graph.columnRefOn', () => this.toggleColumn('ref', true)),
registerCommand('gitlens.graph.columnRefOff', () => this.toggleColumn('ref', false)),
registerCommand('gitlens.graph.columnGraphCompact', () => this.setColumnMode('graph', 'compact')),
registerCommand('gitlens.graph.columnGraphDefault', () => this.setColumnMode('graph', undefined)),
registerCommand('gitlens.graph.scrollMarkerLocalBranchOn', () =>
this.toggleScrollMarker('localBranches', true),
),
registerCommand('gitlens.graph.scrollMarkerLocalBranchOff', () =>
this.toggleScrollMarker('localBranches', false),
),
registerCommand('gitlens.graph.scrollMarkerRemoteBranchOn', () =>
this.toggleScrollMarker('remoteBranches', true),
),
registerCommand('gitlens.graph.scrollMarkerRemoteBranchOff', () =>
this.toggleScrollMarker('remoteBranches', false),
),
registerCommand('gitlens.graph.scrollMarkerStashOn', () => this.toggleScrollMarker('stashes', true)),
registerCommand('gitlens.graph.scrollMarkerStashOff', () => this.toggleScrollMarker('stashes', false)),
registerCommand('gitlens.graph.scrollMarkerTagOn', () => this.toggleScrollMarker('tags', true)),
registerCommand('gitlens.graph.scrollMarkerTagOff', () => this.toggleScrollMarker('tags', false)),
registerCommand('gitlens.graph.copyDeepLinkToBranch', this.copyDeepLinkToBranch, this),
registerCommand('gitlens.graph.copyDeepLinkToCommit', this.copyDeepLinkToCommit, this),
registerCommand('gitlens.graph.copyDeepLinkToRepo', this.copyDeepLinkToRepo, this),
registerCommand('gitlens.graph.copyDeepLinkToTag', this.copyDeepLinkToTag, this),
registerCommand('gitlens.graph.openChangedFiles', this.openFiles, this),
registerCommand('gitlens.graph.openChangedFileDiffs', this.openAllChanges, this),
registerCommand('gitlens.graph.openChangedFileDiffsWithWorking', this.openAllChangesWithWorking, this),
registerCommand('gitlens.graph.openChangedFileRevisions', this.openRevisions, this),
registerCommand(
'gitlens.graph.resetColumnsDefault',
() => this.updateColumns(defaultGraphColumnsSettings),
this,
),
registerCommand(
'gitlens.graph.resetColumnsCompact',
() => this.updateColumns(compactGraphColumnsSettings),
this,
),
];
}
onWindowFocusChanged(focused: boolean): void {
this.isWindowFocused = focused;
void this.notifyDidChangeWindowFocus();
}
onFocusChanged(focused: boolean): void {
void this.notifyDidChangeFocus(focused);
if (!focused || this.activeSelection == null || !this.container.commitDetailsView.visible) {
this._showActiveSelectionDetailsDebounced?.cancel();
return;
}
this.showActiveSelectionDetails();
}
onVisibilityChanged(visible: boolean): void {
if (!visible) {
this._showActiveSelectionDetailsDebounced?.cancel();
}
if (
visible &&
((this.repository != null && this.repository.etag !== this._etagRepository) ||
this.container.subscription.etag !== this._etagSubscription)
) {
this.updateState(true);
return;
}
if (visible) {
if (this.host.ready) {
this.host.sendPendingIpcNotifications();
}
const { activeSelection } = this;
if (activeSelection == null) return;
this.showActiveSelectionDetails();
}
}
onMessageReceived(e: IpcMessage) {
switch (e.method) {
case ChooseRepositoryCommandType.method:
onIpc(ChooseRepositoryCommandType, e, () => this.onChooseRepository());
break;
case DimMergeCommitsCommandType.method:
onIpc(DimMergeCommitsCommandType, e, params => this.dimMergeCommits(params));
break;
case DoubleClickedCommandType.method:
onIpc(DoubleClickedCommandType, e, params => this.onDoubleClick(params));
break;
case EnsureRowCommandType.method:
onIpc(EnsureRowCommandType, e, params => this.onEnsureRow(params, e.completionId));
break;
case GetMissingAvatarsCommandType.method:
onIpc(GetMissingAvatarsCommandType, e, params => this.onGetMissingAvatars(params));
break;
case GetMissingRefsMetadataCommandType.method:
onIpc(GetMissingRefsMetadataCommandType, e, params => this.onGetMissingRefMetadata(params));
break;
case GetMoreRowsCommandType.method:
onIpc(GetMoreRowsCommandType, e, params => this.onGetMoreRows(params));
break;
case SearchCommandType.method:
onIpc(SearchCommandType, e, params => this.onSearch(params, e.completionId));
break;
case SearchOpenInViewCommandType.method:
onIpc(SearchOpenInViewCommandType, e, params => this.onSearchOpenInView(params));
break;
case UpdateColumnsCommandType.method:
onIpc(UpdateColumnsCommandType, e, params => this.onColumnsChanged(params));
break;
case UpdateGraphConfigurationCommandType.method:
onIpc(UpdateGraphConfigurationCommandType, e, params => this.updateGraphConfig(params));
break;
case UpdateRefsVisibilityCommandType.method:
onIpc(UpdateRefsVisibilityCommandType, e, params => this.onRefsVisibilityChanged(params));
break;
case UpdateSelectionCommandType.method:
onIpc(UpdateSelectionCommandType, e, this.onSelectionChanged.bind(this));
break;
case UpdateExcludeTypeCommandType.method:
onIpc(UpdateExcludeTypeCommandType, e, params => this.updateExcludedType(this._graph, params));
break;
case UpdateIncludeOnlyRefsCommandType.method:
onIpc(UpdateIncludeOnlyRefsCommandType, e, params =>
this.updateIncludeOnlyRefs(this._graph, params.refs),
);
break;
}
}
updateGraphConfig(params: UpdateGraphConfigurationParams) {
const config = this.getComponentConfig();
let key: keyof UpdateGraphConfigurationParams['changes'];
for (key in params.changes) {
if (config[key] !== params.changes[key]) {
switch (key) {
case 'minimap':
void configuration.updateEffective('graph.minimap.enabled', params.changes[key]);
break;
case 'minimapDataType':
void configuration.updateEffective('graph.minimap.dataType', params.changes[key]);
break;
case 'minimapMarkerTypes': {
const additionalTypes: GraphMinimapMarkersAdditionalTypes[] = [];
const markers = params.changes[key] ?? [];
for (const marker of markers) {
switch (marker) {
case 'localBranches':
case 'remoteBranches':
case 'stashes':
case 'tags':
additionalTypes.push(marker);
break;
}
}
void configuration.updateEffective('graph.minimap.additionalTypes', additionalTypes);
break;
}
default:
// TODO:@eamodio add more config options as needed
debugger;
break;
}
}
}
}
private _showActiveSelectionDetailsDebounced:
| Deferrable<GraphWebviewProvider['showActiveSelectionDetails']>
| undefined = undefined;
private showActiveSelectionDetails() {
if (this._showActiveSelectionDetailsDebounced == null) {
this._showActiveSelectionDetailsDebounced = debounce(this.showActiveSelectionDetailsCore.bind(this), 250);
}
this._showActiveSelectionDetailsDebounced();
}
private showActiveSelectionDetailsCore() {
const { activeSelection } = this;
if (activeSelection == null) return;
this.container.events.fire(
'commit:selected',
{
commit: activeSelection,
interaction: 'passive',
preserveFocus: true,
preserveVisibility: this._showDetailsView === false,
},
{
source: this.host.id,
},
);
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.changed(e, 'graph.showDetailsView')) {
this._showDetailsView = configuration.get('graph.showDetailsView');
}
if (configuration.changed(e, 'graph.commitOrdering')) {
this.updateState();
return;
}
if (
configuration.changed(e, 'defaultDateFormat') ||
configuration.changed(e, 'defaultDateStyle') ||
configuration.changed(e, 'advanced.abbreviatedShaLength') ||
configuration.changed(e, 'graph.avatars') ||
configuration.changed(e, 'graph.dateFormat') ||
configuration.changed(e, 'graph.dateStyle') ||
configuration.changed(e, 'graph.dimMergeCommits') ||
configuration.changed(e, 'graph.highlightRowsOnRefHover') ||
configuration.changed(e, 'graph.scrollRowPadding') ||
configuration.changed(e, 'graph.scrollMarkers.enabled') ||
configuration.changed(e, 'graph.scrollMarkers.additionalTypes') ||
configuration.changed(e, 'graph.showGhostRefsOnRowHover') ||
configuration.changed(e, 'graph.pullRequests.enabled') ||
configuration.changed(e, 'graph.showRemoteNames') ||
configuration.changed(e, 'graph.showUpstreamStatus') ||
configuration.changed(e, 'graph.minimap.enabled') ||
configuration.changed(e, 'graph.minimap.dataType') ||
configuration.changed(e, 'graph.minimap.additionalTypes')
) {
void this.notifyDidChangeConfiguration();
if (
(configuration.changed(e, 'graph.minimap.enabled') ||
configuration.changed(e, 'graph.minimap.dataType')) &&
configuration.get('graph.minimap.enabled') &&
configuration.get('graph.minimap.dataType') === 'lines' &&
!this._graph?.includes?.stats
) {
this.updateState();
}
}
}
@debug<GraphWebviewProvider['onRepositoryChanged']>({ args: { 0: e => e.toString() } })
private onRepositoryChanged(e: RepositoryChangeEvent) {
if (
!e.changed(
RepositoryChange.Config,
RepositoryChange.Head,
RepositoryChange.Heads,
// RepositoryChange.Index,
RepositoryChange.Remotes,
// RepositoryChange.RemoteProviders,
RepositoryChange.Stash,
RepositoryChange.Status,
RepositoryChange.Tags,
RepositoryChange.Unknown,
RepositoryChangeComparisonMode.Any,
)
) {
this._etagRepository = e.repository.etag;
return;
}
if (e.changed(RepositoryChange.Head, RepositoryChangeComparisonMode.Any)) {
this.setSelectedRows(undefined);
}
// Unless we don't know what changed, update the state immediately
this.updateState(!e.changed(RepositoryChange.Unknown, RepositoryChangeComparisonMode.Exclusive));
}
@debug({ args: false })
private onRepositoryFileSystemChanged(e: RepositoryFileSystemChangeEvent) {
if (e.repository?.path !== this.repository?.path) return;
void this.notifyDidChangeWorkingTree();
}
@debug({ args: false })
private onSubscriptionChanged(e: SubscriptionChangeEvent) {
if (e.etag === this._etagSubscription) return;
this._etagSubscription = e.etag;
void this.notifyDidChangeSubscription();
}
private onThemeChanged(theme: ColorTheme) {
if (this._theme != null) {
if (
(isDarkTheme(theme) && isDarkTheme(this._theme)) ||
(isLightTheme(theme) && isLightTheme(this._theme))
) {
return;
}
}
this._theme = theme;
this.updateState();
}
private dimMergeCommits(e: DimMergeCommitsParams) {
void configuration.updateEffective('graph.dimMergeCommits', e.dim);
}
private onColumnsChanged(e: UpdateColumnsParams) {
this.updateColumns(e.config);
}
private onRefsVisibilityChanged(e: UpdateRefsVisibilityParams) {
this.updateExcludedRefs(this._graph, e.refs, e.visible);
}
private onDoubleClick(e: DoubleClickedParams) {
if (e.type === 'ref' && e.ref.context) {
let item = this.getGraphItemContext(e.ref.context);
if (isGraphItemRefContext(item)) {
if (e.metadata != null) {
item = this.getGraphItemContext(e.metadata.data.context);
if (e.metadata.type === 'upstream' && isGraphItemTypedContext(item, 'upstreamStatus')) {
const { ahead, behind, ref } = item.webviewItemValue;
if (behind > 0) {
return void RepoActions.pull(ref.repoPath, ref);
}
if (ahead > 0) {
return void RepoActions.push(ref.repoPath, false, ref);
}
} else if (e.metadata.type === 'pullRequest' && isGraphItemTypedContext(item, 'pullrequest')) {
return void this.openPullRequestOnRemote(item);
}
return;
}
const { ref } = item.webviewItemValue;
if (e.ref.refType === 'head' && e.ref.isCurrentHead) {
return RepoActions.switchTo(ref.repoPath);
}
// Override the default confirmation if the setting is unset
return RepoActions.switchTo(
ref.repoPath,
ref,
configuration.isUnset('gitCommands.skipConfirmations') ? true : undefined,
);
}
} else if (e.type === 'row' && e.row) {
const commit = this.getRevisionReference(this.repository?.path, e.row.id, e.row.type);
if (commit != null) {
this.container.events.fire(
'commit:selected',
{
commit: commit,
interaction: 'active',
preserveFocus: e.preserveFocus,
preserveVisibility: false,
},
{
source: this.host.id,
},
);
const details =
configuration.get('graph.layout') === 'panel'
? this.container.graphDetailsView
: this.container.commitDetailsView;
if (!details.ready) {
void details.show({ preserveFocus: e.preserveFocus }, {
commit: commit,
interaction: 'active',
preserveVisibility: false,
} satisfies CommitSelectedEvent['data']);
}
}
}
return Promise.resolve();
}
@debug()
private async onEnsureRow(e: EnsureRowParams, completionId?: string) {
if (this._graph == null) return;
const ensureId = this._graph.remappedIds?.get(e.id) ?? e.id;
let id: string | undefined;
let remapped: string | undefined;
if (this._graph.ids.has(ensureId)) {
id = e.id;
remapped = e.id !== ensureId ? ensureId : undefined;
} else {
await this.updateGraphWithMoreRows(this._graph, ensureId, this._search);
void this.notifyDidChangeRows();
if (this._graph.ids.has(ensureId)) {
id = e.id;
remapped = e.id !== ensureId ? ensureId : undefined;
}
}
void this.host.notify(DidEnsureRowNotificationType, { id: id, remapped: remapped }, completionId);
}
private async onGetMissingAvatars(e: GetMissingAvatarsParams) {
if (this._graph == null) return;
const repoPath = this._graph.repoPath;
async function getAvatar(this: GraphWebviewProvider, email: string, id: string) {
const uri = await getAvatarUri(email, { ref: id, repoPath: repoPath });
this._graph!.avatars.set(email, uri.toString(true));
}
const promises: Promise<void>[] = [];
for (const [email, id] of Object.entries(e.emails)) {
if (this._graph.avatars.has(email)) continue;
promises.push(getAvatar.call(this, email, id));
}
if (promises.length) {
await Promise.allSettled(promises);
this.updateAvatars();
}
}
private async onGetMissingRefMetadata(e: GetMissingRefsMetadataParams) {
if (this._graph == null || this._refsMetadata === null || !getContext('gitlens:hasConnectedRemotes')) return;
const repoPath = this._graph.repoPath;
async function getRefMetadata(
this: GraphWebviewProvider,
id: string,
missingTypes: GraphMissingRefsMetadataType[],
) {
if (this._refsMetadata == null) {
this._refsMetadata = new Map();
}
const branch = (await this.container.git.getBranches(repoPath, { filter: b => b.id === id }))?.values?.[0];
const metadata = { ...this._refsMetadata.get(id) };
if (branch == null) {
for (const type of missingTypes) {
(metadata as any)[type] = null;
this._refsMetadata.set(id, metadata);
}
return;
}
for (const type of missingTypes) {
if (!supportedRefMetadataTypes.includes(type)) {
(metadata as any)[type] = null;
this._refsMetadata.set(id, metadata);
continue;
}
if (type === GraphRefMetadataTypes.PullRequest) {
const pr = await branch?.getAssociatedPullRequest();
if (pr == null) {
if (metadata.pullRequest === undefined || metadata.pullRequest?.length === 0) {
metadata.pullRequest = null;
}
this._refsMetadata.set(id, metadata);
continue;
}
const prMetadata: GraphPullRequestMetadata = {
// TODO@eamodio: This is iffy, but works right now since `github` and `gitlab` are the only values possible currently
hostingServiceType: pr.provider.id as GraphHostingServiceType,
id: Number.parseInt(pr.id) || 0,
title: pr.title,
author: pr.author.name,
date: (pr.mergedDate ?? pr.closedDate ?? pr.date)?.getTime(),
state: pr.state,
url: pr.url,
context: serializeWebviewItemContext<GraphItemContext>({
webviewItem: 'gitlens:pullrequest',
webviewItemValue: {
type: 'pullrequest',
id: pr.id,
url: pr.url,
},
}),
};
metadata.pullRequest = [prMetadata];
this._refsMetadata.set(id, metadata);
continue;
}
if (type === GraphRefMetadataTypes.Upstream) {
const upstream = branch?.upstream;
if (upstream == null || upstream == undefined || upstream.missing) {
metadata.upstream = null;
this._refsMetadata.set(id, metadata);
continue;
}
const upstreamMetadata: GraphUpstreamMetadata = {
name: getBranchNameWithoutRemote(upstream.name),
owner: getRemoteNameFromBranchName(upstream.name),
ahead: branch.state.ahead,
behind: branch.state.behind,
context: serializeWebviewItemContext<GraphItemContext>({
webviewItem: 'gitlens:upstreamStatus',
webviewItemValue: {
type: 'upstreamStatus',
ref: getReferenceFromBranch(branch),
ahead: branch.state.ahead,
behind: branch.state.behind,
},
}),
};
metadata.upstream = upstreamMetadata;
this._refsMetadata.set(id, metadata);
}
}
}
const promises: Promise<void>[] = [];
for (const id of Object.keys(e.metadata)) {
promises.push(getRefMetadata.call(this, id, e.metadata[id]));
}
if (promises.length) {
await Promise.allSettled(promises);
}
this.updateRefsMetadata();
}
@gate()
@debug()
private async onGetMoreRows(e: GetMoreRowsParams, sendSelectedRows: boolean = false) {
if (this._graph?.paging == null) return;
if (this._graph?.more == null || this.repository?.etag !== this._etagRepository) {
this.updateState(true);
return;
}
await this.updateGraphWithMoreRows(this._graph, e.id, this._search);
void this.notifyDidChangeRows(sendSelectedRows);
}
@debug()
private async onSearch(e: SearchParams, completionId?: string) {
if (e.search == null) {
this.resetSearchState();
// This shouldn't happen, but just in case
if (completionId != null) {
debugger;
}
return;
}
let search: GitSearch | undefined = this._search;
if (e.more && search?.more != null && search.comparisonKey === getSearchQueryComparisonKey(e.search)) {
search = await search.more(e.limit ?? configuration.get('graph.searchItemLimit') ?? 100);
if (search != null) {
this._search = search;
void (await this.ensureSearchStartsInRange(this._graph!, search));
void this.host.notify(
DidSearchNotificationType,
{
results:
search.results.size > 0
? {
ids: Object.fromEntries(
map(search.results, ([k, v]) => [this._graph?.remappedIds?.get(k) ?? k, v]),
),
count: search.results.size,
paging: { hasMore: search.paging?.hasMore ?? false },
}
: undefined,
},
completionId,
);
}
return;
}
if (search == null || search.comparisonKey !== getSearchQueryComparisonKey(e.search)) {
if (this.repository == null) return;
if (this.repository.etag !== this._etagRepository) {
this.updateState(true);
}
if (this._searchCancellation != null) {
this._searchCancellation.cancel();
this._searchCancellation.dispose();
}
const cancellation = new CancellationTokenSource();
this._searchCancellation = cancellation;
try {
search = await this.repository.searchCommits(e.search, {
limit: configuration.get('graph.searchItemLimit') ?? 100,
ordering: configuration.get('graph.commitOrdering'),
cancellation: cancellation.token,
});
} catch (ex) {
this._search = undefined;
void this.host.notify(
DidSearchNotificationType,
{
results: {
error: ex instanceof GitSearchError ? 'Invalid search pattern' : 'Unexpected error',
},
},
completionId,
);
return;
}
if (cancellation.token.isCancellationRequested) {
if (completionId != null) {
void this.host.notify(DidSearchNotificationType, { results: undefined }, completionId);
}
return;
}
this._search = search;
} else {
search = this._search!;
}
const firstResult = await this.ensureSearchStartsInRange(this._graph!, search);
let sendSelectedRows = false;
if (firstResult != null) {
sendSelectedRows = true;
this.setSelectedRows(firstResult);
}
void this.host.notify(
DidSearchNotificationType,
{
results:
search.results.size === 0
? { count: 0 }
: {
ids: Object.fromEntries(
map(search.results, ([k, v]) => [this._graph?.remappedIds?.get(k) ?? k, v]),
),
count: search.results.size,
paging: { hasMore: search.paging?.hasMore ?? false },
},
selectedRows: sendSelectedRows ? this._selectedRows : undefined,
},
completionId,
);
}
private onSearchOpenInView(e: SearchOpenInViewParams) {
if (this.repository == null) return;
void this.container.searchAndCompareView.search(this.repository.path, e.search, {
label: { label: `for ${e.search.query}` },
reveal: {
select: true,
focus: false,
expand: true,
},
});
}
private async onChooseRepository() {
// Ensure that the current repository is always last
const repositories = this.container.git.openRepositories.sort(
(a, b) =>
(a === this.repository ? 1 : -1) - (b === this.repository ? 1 : -1) ||
(a.starred ? -1 : 1) - (b.starred ? -1 : 1) ||
a.index - b.index,
);
const pick = await showRepositoryPicker(
`Switch Repository ${GlyphChars.Dot} ${this.repository?.name}`,
'Choose a repository to switch to',
repositories,
);
if (pick == null) return;
this.repository = pick.item;
}
private _fireSelectionChangedDebounced: Deferrable<GraphWebviewProvider['fireSelectionChanged']> | undefined =
undefined;
private onSelectionChanged(e: UpdateSelectionParams) {
const item = e.selection[0];
this.setSelectedRows(item?.id);
if (this._fireSelectionChangedDebounced == null) {
this._fireSelectionChangedDebounced = debounce(this.fireSelectionChanged.bind(this), 250);
}
this._fireSelectionChangedDebounced(item?.id, item?.type);
}
private fireSelectionChanged(id: string | undefined, type: GitGraphRowType | undefined) {
if (this.repository == null) return;
const commit = this.getRevisionReference(this.repository.path, id, type);
const commits = commit != null ? [commit] : undefined;
this._selection = commits;
if (commits == null) return;
this.container.events.fire(
'commit:selected',
{
commit: commits[0],
interaction: 'passive',
preserveFocus: true,
preserveVisibility: this._firstSelection
? this._showDetailsView === false
: this._showDetailsView !== 'selection',
},
{
source: this.host.id,
},
);
this._firstSelection = false;
}
private _notifyDidChangeStateDebounced: Deferrable<GraphWebviewProvider['notifyDidChangeState']> | undefined =
undefined;
private getRevisionReference(
repoPath: string | undefined,
id: string | undefined,
type: GitGraphRowType | undefined,
): GitStashReference | GitRevisionReference | undefined {
if (repoPath == null || id == null) return undefined;
switch (type) {
case GitGraphRowType.Stash:
return createReference(id, repoPath, {
refType: 'stash',
name: id,
number: undefined,
});
case GitGraphRowType.Working:
return createReference(uncommitted, repoPath, { refType: 'revision' });
default:
return createReference(id, repoPath, { refType: 'revision' });
}
}
@debug()
private updateState(immediate: boolean = false) {
this.host.clearPendingIpcNotifications();
if (immediate) {
void this.notifyDidChangeState();
return;
}
if (this._notifyDidChangeStateDebounced == null) {
this._notifyDidChangeStateDebounced = debounce(this.notifyDidChangeState.bind(this), 250);
}
void this._notifyDidChangeStateDebounced();
}
@debug()
private async notifyDidChangeFocus(focused: boolean): Promise<boolean> {
if (!this.host.ready || !this.host.visible) return false;
return this.host.notify(DidChangeFocusNotificationType, {
focused: focused,
});
}
@debug()
private async notifyDidChangeWindowFocus(): Promise<boolean> {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidChangeWindowFocusNotificationType, this._ipcNotificationMap, this);
return false;
}
return this.host.notify(DidChangeWindowFocusNotificationType, {
focused: this.isWindowFocused,
});
}
private _notifyDidChangeAvatarsDebounced: Deferrable<GraphWebviewProvider['notifyDidChangeAvatars']> | undefined =
undefined;
@debug()
private updateAvatars(immediate: boolean = false) {
if (immediate) {
void this.notifyDidChangeAvatars();
return;
}
if (this._notifyDidChangeAvatarsDebounced == null) {
this._notifyDidChangeAvatarsDebounced = debounce(this.notifyDidChangeAvatars.bind(this), 100);
}
void this._notifyDidChangeAvatarsDebounced();
}
@debug()
private async notifyDidChangeAvatars() {
if (this._graph == null) return;
const data = this._graph;
return this.host.notify(DidChangeAvatarsNotificationType, {
avatars: Object.fromEntries(data.avatars),
});
}
private _notifyDidChangeRefsMetadataDebounced:
| Deferrable<GraphWebviewProvider['notifyDidChangeRefsMetadata']>
| undefined = undefined;
@debug()
private updateRefsMetadata(immediate: boolean = false) {
if (immediate) {
void this.notifyDidChangeRefsMetadata();
return;
}
if (this._notifyDidChangeRefsMetadataDebounced == null) {
this._notifyDidChangeRefsMetadataDebounced = debounce(this.notifyDidChangeRefsMetadata.bind(this), 100);
}
void this._notifyDidChangeRefsMetadataDebounced();
}
@debug()
private async notifyDidChangeRefsMetadata() {
return this.host.notify(DidChangeRefsMetadataNotificationType, {
metadata: this._refsMetadata != null ? Object.fromEntries(this._refsMetadata) : this._refsMetadata,
});
}
@debug()
private async notifyDidChangeColumns() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidChangeColumnsNotificationType, this._ipcNotificationMap, this);
return false;
}
const columns = this.getColumns();
const columnSettings = this.getColumnSettings(columns);
return this.host.notify(DidChangeColumnsNotificationType, {
columns: columnSettings,
context: this.getColumnHeaderContext(columnSettings),
settingsContext: this.getGraphSettingsIconContext(columnSettings),
});
}
@debug()
private async notifyDidChangeScrollMarkers() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidChangeScrollMarkersNotificationType, this._ipcNotificationMap, this);
return false;
}
const columns = this.getColumns();
const columnSettings = this.getColumnSettings(columns);
return this.host.notify(DidChangeScrollMarkersNotificationType, {
context: this.getGraphSettingsIconContext(columnSettings),
});
}
@debug()
private async notifyDidChangeRefsVisibility() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(
DidChangeRefsVisibilityNotificationType,
this._ipcNotificationMap,
this,
);
return false;
}
return this.host.notify(DidChangeRefsVisibilityNotificationType, {
excludeRefs: this.getExcludedRefs(this._graph),
excludeTypes: this.getExcludedTypes(this._graph),
includeOnlyRefs: this.getIncludeOnlyRefs(this._graph),
});
}
@debug()
private async notifyDidChangeConfiguration() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(
DidChangeGraphConfigurationNotificationType,
this._ipcNotificationMap,
this,
);
return false;
}
return this.host.notify(DidChangeGraphConfigurationNotificationType, {
config: this.getComponentConfig(),
});
}
@debug()
private async notifyDidFetch() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidFetchNotificationType, this._ipcNotificationMap, this);
return false;
}
const lastFetched = await this.repository!.getLastFetched();
return this.host.notify(DidFetchNotificationType, {
lastFetched: new Date(lastFetched),
});
}
@debug()
private async notifyDidChangeRows(sendSelectedRows: boolean = false, completionId?: string) {
if (this._graph == null) return;
const graph = this._graph;
return this.host.notify(
DidChangeRowsNotificationType,
{
rows: graph.rows,
avatars: Object.fromEntries(graph.avatars),
downstreams: Object.fromEntries(graph.downstreams),
refsMetadata: this._refsMetadata != null ? Object.fromEntries(this._refsMetadata) : this._refsMetadata,
rowsStats: graph.rowsStats?.size ? Object.fromEntries(graph.rowsStats) : undefined,
rowsStatsLoading:
graph.rowsStatsDeferred?.isLoaded != null ? !graph.rowsStatsDeferred.isLoaded() : false,
selectedRows: sendSelectedRows ? this._selectedRows : undefined,
paging: {
startingCursor: graph.paging?.startingCursor,
hasMore: graph.paging?.hasMore ?? false,
},
},
completionId,
);
}
@debug({ args: false })
private async notifyDidChangeRowsStats(graph: GitGraph) {
if (graph.rowsStats == null) return;
return this.host.notify(DidChangeRowsStatsNotificationType, {
rowsStats: Object.fromEntries(graph.rowsStats),
rowsStatsLoading: graph.rowsStatsDeferred?.isLoaded != null ? !graph.rowsStatsDeferred.isLoaded() : false,
});
}
@debug()
private async notifyDidChangeWorkingTree() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidChangeWorkingTreeNotificationType, this._ipcNotificationMap, this);
return false;
}
return this.host.notify(DidChangeWorkingTreeNotificationType, {
stats: (await this.getWorkingTreeStats()) ?? { added: 0, deleted: 0, modified: 0 },
});
}
@debug()
private async notifyDidChangeSelection() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidChangeSelectionNotificationType, this._ipcNotificationMap, this);
return false;
}
return this.host.notify(DidChangeSelectionNotificationType, {
selection: this._selectedRows ?? {},
});
}
@debug()
private async notifyDidChangeSubscription() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidChangeSubscriptionNotificationType, this._ipcNotificationMap, this);
return false;
}
const [access] = await this.getGraphAccess();
return this.host.notify(DidChangeSubscriptionNotificationType, {
subscription: access.subscription.current,
allowed: access.allowed !== false,
});
}
@debug()
private async notifyDidChangeState() {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidChangeNotificationType, this._ipcNotificationMap, this);
return false;
}
return this.host.notify(DidChangeNotificationType, { state: await this.getState() });
}
private ensureRepositorySubscriptions(force?: boolean) {
void this.ensureLastFetchedSubscription(force);
if (!force && this._repositoryEventsDisposable != null) return;
if (this._repositoryEventsDisposable != null) {
this._repositoryEventsDisposable.dispose();
this._repositoryEventsDisposable = undefined;
}
const repo = this.repository;
if (repo == null) return;
this._repositoryEventsDisposable = Disposable.from(
repo.onDidChange(this.onRepositoryChanged, this),
repo.startWatchingFileSystem(),
repo.onDidChangeFileSystem(this.onRepositoryFileSystemChanged, this),
onDidChangeContext(key => {
if (key !== 'gitlens:hasConnectedRemotes') return;
this.resetRefsMetadata();
this.updateRefsMetadata();
}),
);
}
private async ensureLastFetchedSubscription(force?: boolean) {
if (!force && this._lastFetchedDisposable != null) return;
if (this._lastFetchedDisposable != null) {
this._lastFetchedDisposable.dispose();
this._lastFetchedDisposable = undefined;
}
const repo = this.repository;
if (repo == null) return;
const lastFetched = (await repo.getLastFetched()) ?? 0;
let interval = Repository.getLastFetchedUpdateInterval(lastFetched);
if (lastFetched !== 0 && interval > 0) {
this._lastFetchedDisposable = disposableInterval(() => {
// Check if the interval should change, and if so, reset it
const checkInterval = Repository.getLastFetchedUpdateInterval(lastFetched);
if (interval !== Repository.getLastFetchedUpdateInterval(lastFetched)) {
interval = checkInterval;
}
void this.notifyDidFetch();
}, interval);
}
}
private async ensureSearchStartsInRange(graph: GitGraph, search: GitSearch) {
if (search.results.size === 0) return undefined;
let firstResult: string | undefined;
for (const id of search.results.keys()) {
const remapped = graph.remappedIds?.get(id) ?? id;
if (graph.ids.has(remapped)) return remapped;
firstResult = remapped;
break;
}
if (firstResult == null) return undefined;
await this.updateGraphWithMoreRows(graph, firstResult);
void this.notifyDidChangeRows();
return graph.ids.has(firstResult) ? firstResult : undefined;
}
private getColumns(): Record<GraphColumnName, GraphColumnConfig> | undefined {
return this.container.storage.getWorkspace('graph:columns');
}
private getExcludedTypes(graph: GitGraph | undefined): GraphExcludeTypes | undefined {
if (graph == null) return undefined;
return this.getFiltersByRepo(graph)?.excludeTypes;
}
private getExcludedRefs(graph: GitGraph | undefined): Record<string, GraphExcludedRef> | undefined {
if (graph == null) return undefined;
let filtersByRepo: Record<string, StoredGraphFilters> | undefined;
const storedHiddenRefs = this.container.storage.getWorkspace('graph:hiddenRefs');
if (storedHiddenRefs != null && Object.keys(storedHiddenRefs).length !== 0) {
// Migrate hidden refs to exclude refs
filtersByRepo = this.container.storage.getWorkspace('graph:filtersByRepo') ?? {};
for (const id in storedHiddenRefs) {
const repoPath = getRepoPathFromBranchOrTagId(id);
filtersByRepo[repoPath] = filtersByRepo[repoPath] ?? {};
filtersByRepo[repoPath].excludeRefs = updateRecordValue(
filtersByRepo[repoPath].excludeRefs,
id,
storedHiddenRefs[id],
);
}
void this.container.storage.storeWorkspace('graph:filtersByRepo', filtersByRepo);
void this.container.storage.deleteWorkspace('graph:hiddenRefs');
}
const storedExcludeRefs = (filtersByRepo?.[graph.repoPath] ?? this.getFiltersByRepo(graph))?.excludeRefs;
if (storedExcludeRefs == null || Object.keys(storedExcludeRefs).length === 0) return undefined;
const useAvatars = configuration.get('graph.avatars', undefined, true);
const excludeRefs: GraphExcludeRefs = {};
const asWebviewUri = (uri: Uri) => this.host.asWebviewUri(uri);
for (const id in storedExcludeRefs) {
const ref: GraphExcludedRef = { ...storedExcludeRefs[id] };
if (ref.type === 'remote' && ref.owner) {
const remote = graph.remotes.get(ref.owner);
if (remote != null) {
ref.avatarUrl = (
(useAvatars ? remote.provider?.avatarUri : undefined) ??
getRemoteIconUri(this.container, remote, asWebviewUri)
)?.toString(true);
}
}
excludeRefs[id] = ref;
}
// For v13, we return directly the hidden refs without validating them
// This validation has too much performance impact. So we decided to comment those lines
// for v13 and have it as tech debt to solve after we launch.
// See: https://github.com/gitkraken/vscode-gitlens/pull/2211#discussion_r990117432
// if (this.repository == null) {
// this.repository = this.container.git.getBestRepositoryOrFirst();
// if (this.repository == null) return undefined;
// }
// const [hiddenBranches, hiddenTags] = await Promise.all([
// this.repository.getBranches({
// filter: b => !b.current && excludeRefs[b.id] != undefined,
// }),
// this.repository.getTags({
// filter: t => excludeRefs[t.id] != undefined,
// }),
// ]);
// const filteredHiddenRefsById: GraphHiddenRefs = {};
// for (const hiddenBranch of hiddenBranches.values) {
// filteredHiddenRefsById[hiddenBranch.id] = excludeRefs[hiddenBranch.id];
// }
// for (const hiddenTag of hiddenTags.values) {
// filteredHiddenRefsById[hiddenTag.id] = excludeRefs[hiddenTag.id];
// }
// return filteredHiddenRefsById;
return excludeRefs;
}
private getIncludeOnlyRefs(graph: GitGraph | undefined): Record<string, GraphIncludeOnlyRef> | undefined {
if (graph == null) return undefined;
const storedFilters = this.getFiltersByRepo(graph);
const storedIncludeOnlyRefs = storedFilters?.includeOnlyRefs;
if (storedIncludeOnlyRefs == null || Object.keys(storedIncludeOnlyRefs).length === 0) return undefined;
const includeOnlyRefs: Record<string, StoredGraphIncludeOnlyRef> = {};
for (const [key, value] of Object.entries(storedIncludeOnlyRefs)) {
let branch;
if (value.id === 'HEAD') {
branch = find(graph.branches.values(), b => b.current);
if (branch == null) continue;
includeOnlyRefs[branch.id] = { ...value, id: branch.id, name: branch.name };
} else {
includeOnlyRefs[key] = value;
}
// Add the upstream branches for any local branches if there are any
if (value.type === 'head') {
branch = branch ?? graph.branches.get(value.name);
if (branch?.upstream != null && !branch.upstream.missing) {
const id = getBranchId(graph.repoPath, true, branch.upstream.name);
includeOnlyRefs[id] = {
id: id,
type: 'remote',
name: getBranchNameWithoutRemote(branch.upstream.name),
owner: getRemoteNameFromBranchName(branch.upstream.name),
};
}
}
}
return includeOnlyRefs;
}
private getFiltersByRepo(graph: GitGraph | undefined): StoredGraphFilters | undefined {
if (graph == null) return undefined;
const filters = this.container.storage.getWorkspace('graph:filtersByRepo');
return filters?.[graph.repoPath];
}
private getColumnSettings(columns: Record<GraphColumnName, GraphColumnConfig> | undefined): GraphColumnsSettings {
const columnsSettings: GraphColumnsSettings = {
...defaultGraphColumnsSettings,
};
if (columns != null) {
for (const [column, columnCfg] of Object.entries(columns) as [GraphColumnName, GraphColumnConfig][]) {
columnsSettings[column] = {
...defaultGraphColumnsSettings[column],
...columnCfg,
};
}
}
return columnsSettings;
}
private getColumnHeaderContext(columnSettings: GraphColumnsSettings): string {
return serializeWebviewItemContext<GraphItemContext>({
webviewItem: 'gitlens:graph:columns',
webviewItemValue: this.getColumnContextItems(columnSettings).join(','),
});
}
private getGraphSettingsIconContext(columnsSettings?: GraphColumnsSettings): string {
return serializeWebviewItemContext<GraphItemContext>({
webviewItem: 'gitlens:graph:settings',
webviewItemValue: this.getSettingsIconContextItems(columnsSettings).join(','),
});
}
private getColumnContextItems(columnSettings: GraphColumnsSettings): string[] {
const contextItems: string[] = [];
// Old column settings that didn't get cleaned up can mess with calculation of only visible column.
// All currently used ones are listed here.
const validColumns = ['author', 'changes', 'datetime', 'graph', 'message', 'ref', 'sha'];
let visibleColumns = 0;
for (const [name, settings] of Object.entries(columnSettings)) {
if (!validColumns.includes(name)) continue;
if (!settings.isHidden) {
visibleColumns++;
}
contextItems.push(
`column:${name}:${settings.isHidden ? 'hidden' : 'visible'}${settings.mode ? `+${settings.mode}` : ''}`,
);
}
if (visibleColumns > 1) {
contextItems.push('columns:canHide');
}
return contextItems;
}
private getSettingsIconContextItems(columnSettings?: GraphColumnsSettings): string[] {
const contextItems: string[] = columnSettings != null ? this.getColumnContextItems(columnSettings) : [];
if (configuration.get('graph.scrollMarkers.enabled')) {
const configurableScrollMarkerTypes: GraphScrollMarkersAdditionalTypes[] = [
'localBranches',
'remoteBranches',
'stashes',
'tags',
];
const enabledScrollMarkerTypes = configuration.get('graph.scrollMarkers.additionalTypes');
for (const type of configurableScrollMarkerTypes) {
contextItems.push(
`scrollMarker:${type}:${enabledScrollMarkerTypes.includes(type) ? 'enabled' : 'disabled'}`,
);
}
}
return contextItems;
}
private getComponentConfig(): GraphComponentConfig {
const config: GraphComponentConfig = {
avatars: configuration.get('graph.avatars'),
dateFormat:
configuration.get('graph.dateFormat') ?? configuration.get('defaultDateFormat') ?? 'short+short',
dateStyle: configuration.get('graph.dateStyle') ?? configuration.get('defaultDateStyle'),
enabledRefMetadataTypes: this.getEnabledRefMetadataTypes(),
dimMergeCommits: configuration.get('graph.dimMergeCommits'),
enableMultiSelection: false,
highlightRowsOnRefHover: configuration.get('graph.highlightRowsOnRefHover'),
minimap: configuration.get('graph.minimap.enabled'),
minimapDataType: configuration.get('graph.minimap.dataType'),
minimapMarkerTypes: this.getMinimapMarkerTypes(),
scrollRowPadding: configuration.get('graph.scrollRowPadding'),
scrollMarkerTypes: this.getScrollMarkerTypes(),
showGhostRefsOnRowHover: configuration.get('graph.showGhostRefsOnRowHover'),
showRemoteNamesOnRefs: configuration.get('graph.showRemoteNames'),
idLength: configuration.get('advanced.abbreviatedShaLength'),
};
return config;
}
private getScrollMarkerTypes(): GraphScrollMarkerTypes[] {
if (!configuration.get('graph.scrollMarkers.enabled')) return [];
const markers: GraphScrollMarkerTypes[] = [
'selection',
'highlights',
'head',
'upstream',
...configuration.get('graph.scrollMarkers.additionalTypes'),
];
return markers;
}
private getMinimapMarkerTypes(): GraphMinimapMarkerTypes[] {
if (!configuration.get('graph.minimap.enabled')) return [];
const markers: GraphMinimapMarkerTypes[] = [
'selection',
'highlights',
'head',
'upstream',
...configuration.get('graph.minimap.additionalTypes'),
];
return markers;
}
private getEnabledRefMetadataTypes(): GraphRefMetadataType[] {
const types: GraphRefMetadataType[] = [];
if (configuration.get('graph.pullRequests.enabled')) {
types.push(GraphRefMetadataTypes.PullRequest as GraphRefMetadataType);
}
if (configuration.get('graph.showUpstreamStatus')) {
types.push(GraphRefMetadataTypes.Upstream as GraphRefMetadataType);
}
return types;
}
private async getGraphAccess() {
let access = await this.container.git.access(PlusFeatures.Graph, this.repository?.path);
this._etagSubscription = this.container.subscription.etag;
// If we don't have access, but the preview trial hasn't been started, auto-start it
if (access.allowed === false && access.subscription.current.previewTrial == null) {
await this.container.subscription.startPreviewTrial();
access = await this.container.git.access(PlusFeatures.Graph, this.repository?.path);
}
let visibility = access?.visibility;
if (visibility == null && this.repository != null) {
visibility = await this.container.git.visibility(this.repository?.path);
}
return [access, visibility] as const;
}
private getGraphItemContext(context: unknown): unknown | undefined {
const item = typeof context === 'string' ? JSON.parse(context) : context;
// Add the `webview` prop to the context if its missing (e.g. when this context doesn't come through via the context menus)
if (item != null && !('webview' in item)) {
item.webview = this.host.id;
}
return item;
}
private async getWorkingTreeStats(): Promise<GraphWorkingTreeStats | undefined> {
if (this.repository == null || this.container.git.repositoryCount === 0) return undefined;
const status = await this.container.git.getStatusForRepo(this.repository.path);
const workingTreeStatus = status?.getDiffStatus();
return {
added: workingTreeStatus?.added ?? 0,
deleted: workingTreeStatus?.deleted ?? 0,
modified: workingTreeStatus?.changed ?? 0,
context: serializeWebviewItemContext<GraphItemContext>({
webviewItem: 'gitlens:wip',
webviewItemValue: {
type: 'commit',
ref: this.getRevisionReference(this.repository.path, uncommitted, GitGraphRowType.Working)!,
},
}),
};
}
private async getState(deferRows?: boolean): Promise<State> {
if (this.container.git.repositoryCount === 0) {
return { timestamp: Date.now(), allowed: true, repositories: [] };
}
if (this.repository == null) {
this.repository = this.container.git.getBestRepositoryOrFirst();
if (this.repository == null) {
return { timestamp: Date.now(), allowed: true, repositories: [] };
}
}
this._etagRepository = this.repository?.etag;
this.host.title = `${this.host.originalTitle}: ${this.repository.formattedName}`;
const { defaultItemLimit } = configuration.get('graph');
// If we have a set of data refresh to the same set
const limit = Math.max(defaultItemLimit, this._graph?.ids.size ?? defaultItemLimit);
const ref = this._selectedId == null || this._selectedId === uncommitted ? 'HEAD' : this._selectedId;
const columns = this.getColumns();
const columnSettings = this.getColumnSettings(columns);
const dataPromise = this.container.git.getCommitsForGraph(
this.repository.path,
uri => this.host.asWebviewUri(uri),
{
include: {
stats:
(configuration.get('graph.minimap.enabled') &&
configuration.get('graph.minimap.dataType') === 'lines') ||
!columnSettings.changes.isHidden,
},
limit: limit,
ref: ref,
},
);
// Check for access and working tree stats
const promises = Promise.allSettled([
this.getGraphAccess(),
this.getWorkingTreeStats(),
this.repository.getBranch(),
this.repository.getLastFetched(),
]);
let data;
if (deferRows) {
queueMicrotask(async () => {
const data = await dataPromise;
this.setGraph(data);
this.setSelectedRows(data.id);
void this.notifyDidChangeRefsVisibility();
void this.notifyDidChangeRows(true);
});
} else {
data = await dataPromise;
this.setGraph(data);
this.setSelectedRows(data.id);
}
const [accessResult, workingStatsResult, branchResult, lastFetchedResult] = await promises;
const [access, visibility] = getSettledValue(accessResult) ?? [];
let branchState: BranchState | undefined;
const branch = getSettledValue(branchResult);
if (branch != null) {
branchState = { ...branch.state };
if (branch.upstream != null) {
branchState.upstream = branch.upstream.name;
const remote = await branch.getRemote();
if (remote?.provider != null) {
branchState.provider = {
name: remote.provider.name,
icon: remote.provider.icon === 'remote' ? 'cloud' : remote.provider.icon,
url: remote.provider.url({ type: RemoteResourceType.Repo }),
};
}
}
}
return {
timestamp: Date.now(),
windowFocused: this.isWindowFocused,
repositories: formatRepositories(this.container.git.openRepositories),
selectedRepository: this.repository.path,
selectedRepositoryVisibility: visibility,
branchName: branch?.name,
branchState: branchState,
lastFetched: new Date(getSettledValue(lastFetchedResult)!),
selectedRows: this._selectedRows,
subscription: access?.subscription.current,
allowed: (access?.allowed ?? false) !== false,
avatars: data != null ? Object.fromEntries(data.avatars) : undefined,
refsMetadata: this.resetRefsMetadata() === null ? null : {},
loading: deferRows,
rowsStatsLoading: data?.rowsStatsDeferred?.isLoaded != null ? !data.rowsStatsDeferred.isLoaded() : false,
rows: data?.rows,
downstreams: data != null ? Object.fromEntries(data.downstreams) : undefined,
paging:
data != null
? {
startingCursor: data.paging?.startingCursor,
hasMore: data.paging?.hasMore ?? false,
}
: undefined,
columns: columnSettings,
config: this.getComponentConfig(),
context: {
header: this.getColumnHeaderContext(columnSettings),
settings: this.getGraphSettingsIconContext(columnSettings),
},
excludeRefs: data != null ? this.getExcludedRefs(data) ?? {} : {},
excludeTypes: this.getExcludedTypes(data) ?? {},
includeOnlyRefs: data != null ? this.getIncludeOnlyRefs(data) ?? {} : {},
nonce: this.host.cspNonce,
workingTreeStats: getSettledValue(workingStatsResult) ?? { added: 0, deleted: 0, modified: 0 },
};
}
private updateColumns(columnsCfg: GraphColumnsConfig) {
let columns = this.container.storage.getWorkspace('graph:columns');
for (const [key, value] of Object.entries(columnsCfg)) {
columns = updateRecordValue(columns, key, value);
}
void this.container.storage.storeWorkspace('graph:columns', columns);
void this.notifyDidChangeColumns();
}
private updateExcludedRefs(graph: GitGraph | undefined, refs: GraphExcludedRef[], visible: boolean) {
if (refs == null || refs.length === 0) return;
let storedExcludeRefs: StoredGraphFilters['excludeRefs'] = this.getFiltersByRepo(graph)?.excludeRefs ?? {};
for (const ref of refs) {
storedExcludeRefs = updateRecordValue(
storedExcludeRefs,
ref.id,
visible
? undefined
: { id: ref.id, type: ref.type as StoredGraphRefType, name: ref.name, owner: ref.owner },
);
}
void this.updateFiltersByRepo(graph, { excludeRefs: storedExcludeRefs });
void this.notifyDidChangeRefsVisibility();
}
private updateFiltersByRepo(graph: GitGraph | undefined, updates: Partial<StoredGraphFilters>) {
if (graph == null) throw new Error('Cannot save repository filters since Graph is undefined');
const filtersByRepo = this.container.storage.getWorkspace('graph:filtersByRepo');
return this.container.storage.storeWorkspace(
'graph:filtersByRepo',
updateRecordValue(filtersByRepo, graph.repoPath, { ...filtersByRepo?.[graph.repoPath], ...updates }),
);
}
private updateIncludeOnlyRefs(graph: GitGraph | undefined, refs: GraphIncludeOnlyRef[] | undefined) {
let storedIncludeOnlyRefs: StoredGraphFilters['includeOnlyRefs'];
if (refs == null || refs.length === 0) {
if (this.getFiltersByRepo(graph)?.includeOnlyRefs == null) return;
storedIncludeOnlyRefs = undefined;
} else {
storedIncludeOnlyRefs = {};
for (const ref of refs) {
storedIncludeOnlyRefs[ref.id] = {
id: ref.id,
type: ref.type as StoredGraphRefType,
name: ref.name,
owner: ref.owner,
};
}
}
void this.updateFiltersByRepo(graph, { includeOnlyRefs: storedIncludeOnlyRefs });
void this.notifyDidChangeRefsVisibility();
}
private updateExcludedType(graph: GitGraph | undefined, { key, value }: UpdateExcludeTypeParams) {
let excludeTypes = this.getFiltersByRepo(graph)?.excludeTypes;
if ((excludeTypes == null || Object.keys(excludeTypes).length === 0) && value === false) {
return;
}
excludeTypes = updateRecordValue(excludeTypes, key, value);
void this.updateFiltersByRepo(graph, { excludeTypes: excludeTypes });
void this.notifyDidChangeRefsVisibility();
}
private resetRefsMetadata(): null | undefined {
this._refsMetadata = getContext('gitlens:hasConnectedRemotes') ? undefined : null;
return this._refsMetadata;
}
private resetRepositoryState() {
this.setGraph(undefined);
this.setSelectedRows(undefined);
}
private resetSearchState() {
this._search = undefined;
this._searchCancellation?.dispose();
this._searchCancellation = undefined;
}
private setSelectedRows(id: string | undefined) {
if (this._selectedId === id) return;
this._selectedId = id;
if (id === uncommitted) {
id = GitGraphRowType.Working;
}
this._selectedRows = id != null ? { [id]: true } : undefined;
}
private setGraph(graph: GitGraph | undefined) {
this._graph = graph;
if (graph == null) {
this.resetRefsMetadata();
this.resetSearchState();
} else {
void graph.rowsStatsDeferred?.promise.then(() => void this.notifyDidChangeRowsStats(graph));
}
}
private async updateGraphWithMoreRows(graph: GitGraph, id: string | undefined, search?: GitSearch) {
const { defaultItemLimit, pageItemLimit } = configuration.get('graph');
const updatedGraph = await graph.more?.(pageItemLimit ?? defaultItemLimit, id);
if (updatedGraph != null) {
this.setGraph(updatedGraph);
if (!search?.paging?.hasMore) return;
const lastId = last(search.results)?.[0];
if (lastId == null) return;
const remapped = updatedGraph.remappedIds?.get(lastId) ?? lastId;
if (updatedGraph.ids.has(remapped)) {
queueMicrotask(() => void this.onSearch({ search: search.query, more: true }));
}
} else {
debugger;
}
}
@debug()
private fetch(item?: GraphItemContext) {
const ref = item != null ? this.getGraphItemRef(item, 'branch') : undefined;
void RepoActions.fetch(this.repository, ref);
}
@debug()
private pull(item?: GraphItemContext) {
const ref = item != null ? this.getGraphItemRef(item, 'branch') : undefined;
void RepoActions.pull(this.repository, ref);
}
@debug()
private push(item?: GraphItemContext) {
const ref = item != null ? this.getGraphItemRef(item) : undefined;
void RepoActions.push(this.repository, undefined, ref);
}
@debug()
private createBranch(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
return BranchActions.create(ref.repoPath, ref);
}
@debug()
private deleteBranch(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
return BranchActions.remove(ref.repoPath, ref);
}
return Promise.resolve();
}
@debug()
private mergeBranchInto(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
return RepoActions.merge(ref.repoPath, ref);
}
return Promise.resolve();
}
@debug()
private openBranchOnRemote(item?: GraphItemContext, clipboard?: boolean) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
return executeCommand<OpenOnRemoteCommandArgs>(Commands.OpenOnRemote, {
repoPath: ref.repoPath,
resource: {
type: RemoteResourceType.Branch,
branch: ref.name,
},
remote: ref.upstream?.name,
clipboard: clipboard,
});
}
return Promise.resolve();
}
@debug()
private publishBranch(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
return RepoActions.push(ref.repoPath, undefined, ref);
}
return Promise.resolve();
}
@debug()
private rebase(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
return RepoActions.rebase(ref.repoPath, ref);
}
@debug()
private rebaseToRemote(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
if (ref.upstream != null) {
return RepoActions.rebase(
ref.repoPath,
createReference(ref.upstream.name, ref.repoPath, {
refType: 'branch',
name: ref.upstream.name,
remote: true,
}),
);
}
}
return Promise.resolve();
}
@debug()
private renameBranch(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
return BranchActions.rename(ref.repoPath, ref);
}
return Promise.resolve();
}
@debug()
private cherryPick(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'revision');
if (ref == null) return Promise.resolve();
return RepoActions.cherryPick(ref.repoPath, ref);
}
@debug()
private async copy(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref != null) {
await env.clipboard.writeText(
ref.refType === 'revision' && ref.message ? `${ref.name}: ${ref.message}` : ref.name,
);
} else if (isGraphItemTypedContext(item, 'contributor')) {
const { name, email } = item.webviewItemValue;
await env.clipboard.writeText(`${name}${email ? ` <${email}>` : ''}`);
} else if (isGraphItemTypedContext(item, 'pullrequest')) {
const { url } = item.webviewItemValue;
await env.clipboard.writeText(url);
}
return Promise.resolve();
}
@debug()
private copyMessage(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
return executeCommand<CopyMessageToClipboardCommandArgs>(Commands.CopyMessageToClipboard, {
repoPath: ref.repoPath,
sha: ref.ref,
message: 'message' in ref ? ref.message : undefined,
});
}
@debug()
private async copySha(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
let sha = ref.ref;
if (!isSha(sha)) {
sha = await this.container.git.resolveReference(ref.repoPath, sha, undefined, { force: true });
}
return executeCommand<CopyShaToClipboardCommandArgs>(Commands.CopyShaToClipboard, {
sha: sha,
});
}
@debug()
private openInDetailsView(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'revision');
if (ref == null) return Promise.resolve();
if (this.host.isView()) {
return void showGraphDetailsView(ref, { preserveFocus: true, preserveVisibility: false });
}
return executeCommand<ShowCommitsInViewCommandArgs>(Commands.ShowInDetailsView, {
repoPath: ref.repoPath,
refs: [ref.ref],
});
}
@debug()
private openSCM(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'revision');
if (ref == null) return Promise.resolve();
return executeCoreCommand('workbench.view.scm');
}
@debug()
private openCommitOnRemote(item?: GraphItemContext, clipboard?: boolean) {
const ref = this.getGraphItemRef(item, 'revision');
if (ref == null) return Promise.resolve();
return executeCommand<OpenOnRemoteCommandArgs>(Commands.OpenOnRemote, {
repoPath: ref.repoPath,
resource: {
type: RemoteResourceType.Commit,
sha: ref.ref,
},
clipboard: clipboard,
});
}
@debug()
private copyDeepLinkToBranch(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
return executeCommand<CopyDeepLinkCommandArgs>(Commands.CopyDeepLinkToBranch, { refOrRepoPath: ref });
}
return Promise.resolve();
}
@debug()
private copyDeepLinkToCommit(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'revision');
if (ref == null) return Promise.resolve();
return executeCommand<CopyDeepLinkCommandArgs>(Commands.CopyDeepLinkToCommit, { refOrRepoPath: ref });
}
@debug()
private copyDeepLinkToRepo(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
if (!ref.remote) return Promise.resolve();
return executeCommand<CopyDeepLinkCommandArgs>(Commands.CopyDeepLinkToRepo, {
refOrRepoPath: ref.repoPath,
remote: getRemoteNameFromBranchName(ref.name),
});
}
return Promise.resolve();
}
@debug()
private copyDeepLinkToTag(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'tag')) {
const { ref } = item.webviewItemValue;
return executeCommand<CopyDeepLinkCommandArgs>(Commands.CopyDeepLinkToTag, { refOrRepoPath: ref });
}
return Promise.resolve();
}
@debug()
private resetCommit(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'revision');
if (ref == null) return Promise.resolve();
return RepoActions.reset(
ref.repoPath,
createReference(`${ref.ref}^`, ref.repoPath, {
refType: 'revision',
name: `${ref.name}^`,
message: ref.message,
}),
);
}
@debug()
private resetToCommit(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'revision');
if (ref == null) return Promise.resolve();
return RepoActions.reset(ref.repoPath, ref);
}
@debug()
private revertCommit(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'revision');
if (ref == null) return Promise.resolve();
return RepoActions.revert(ref.repoPath, ref);
}
@debug()
private switchTo(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
return RepoActions.switchTo(ref.repoPath, ref);
}
@debug()
private hideRef(item?: GraphItemContext, options?: { group?: boolean; remote?: boolean }) {
let refs;
if (options?.group && isGraphItemRefGroupContext(item)) {
({ refs } = item.webviewItemGroupValue);
} else if (!options?.group && isGraphItemRefContext(item)) {
const { ref } = item.webviewItemValue;
if (ref.id != null) {
refs = [ref];
}
}
if (refs != null) {
this.updateExcludedRefs(
this._graph,
refs.map(r => {
const remoteBranch = r.refType === 'branch' && r.remote;
return {
id: r.id!,
name: remoteBranch ? (options?.remote ? '*' : getBranchNameWithoutRemote(r.name)) : r.name,
owner: remoteBranch ? getRemoteNameFromBranchName(r.name) : undefined,
type: r.refType === 'branch' ? (r.remote ? 'remote' : 'head') : 'tag',
};
}),
false,
);
}
return Promise.resolve();
}
@debug()
private switchToAnother(item?: GraphItemContext | unknown) {
const ref = this.getGraphItemRef(item);
if (ref == null) return RepoActions.switchTo(this.repository?.path);
return RepoActions.switchTo(ref.repoPath);
}
@debug()
private async undoCommit(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
const repo = await this.container.git.getOrOpenScmRepository(ref.repoPath);
const commit = await repo?.getCommit('HEAD');
if (commit?.hash !== ref.ref) {
void window.showWarningMessage(
`Commit ${getReferenceLabel(ref, {
capitalize: true,
icon: false,
})} cannot be undone, because it is no longer the most recent commit.`,
);
return;
}
return void executeCoreGitCommand('git.undoCommit', ref.repoPath);
}
@debug()
private saveStash(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
return StashActions.push(ref.repoPath);
}
@debug()
private applyStash(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'stash');
if (ref == null) return Promise.resolve();
return StashActions.apply(ref.repoPath, ref);
}
@debug()
private deleteStash(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'stash');
if (ref == null) return Promise.resolve();
return StashActions.drop(ref.repoPath, ref);
}
@debug()
private renameStash(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item, 'stash');
if (ref == null) return Promise.resolve();
return StashActions.rename(ref.repoPath, ref);
}
@debug()
private async createTag(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
return TagActions.create(ref.repoPath, ref);
}
@debug()
private deleteTag(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'tag')) {
const { ref } = item.webviewItemValue;
return TagActions.remove(ref.repoPath, ref);
}
return Promise.resolve();
}
@debug()
private async createWorktree(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
return WorktreeActions.create(ref.repoPath, undefined, ref);
}
@debug()
private async createPullRequest(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
const repo = this.container.git.getRepository(ref.repoPath);
const branch = await repo?.getBranch(ref.name);
const remote = await branch?.getRemote();
return executeActionCommand<CreatePullRequestActionContext>('createPullRequest', {
repoPath: ref.repoPath,
remote:
remote != null
? {
name: remote.name,
provider:
remote.provider != null
? {
id: remote.provider.id,
name: remote.provider.name,
domain: remote.provider.domain,
}
: undefined,
url: remote.url,
}
: undefined,
branch: {
name: ref.name,
upstream: ref.upstream?.name,
isRemote: ref.remote,
},
});
}
return Promise.resolve();
}
@debug()
private openPullRequestOnRemote(item?: GraphItemContext, clipboard?: boolean) {
if (
isGraphItemContext(item) &&
typeof item.webviewItemValue === 'object' &&
item.webviewItemValue.type === 'pullrequest'
) {
const { url } = item.webviewItemValue;
return executeCommand<OpenPullRequestOnRemoteCommandArgs>(Commands.OpenPullRequestOnRemote, {
pr: { url: url },
clipboard: clipboard,
});
}
return Promise.resolve();
}
@debug()
private async compareAncestryWithWorking(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
const branch = await this.container.git.getBranch(ref.repoPath);
if (branch == null) return undefined;
const commonAncestor = await this.container.git.getMergeBase(ref.repoPath, branch.ref, ref.ref);
if (commonAncestor == null) return undefined;
return this.container.searchAndCompareView.compare(
ref.repoPath,
{
ref: commonAncestor,
label: `ancestry with ${ref.ref} (${shortenRevision(commonAncestor)})`,
},
'',
);
}
@debug()
private compareHeadWith(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
return this.container.searchAndCompareView.compare(ref.repoPath, 'HEAD', ref.ref);
}
@debug()
private compareWithUpstream(item?: GraphItemContext) {
if (isGraphItemRefContext(item, 'branch')) {
const { ref } = item.webviewItemValue;
if (ref.upstream != null) {
return this.container.searchAndCompareView.compare(ref.repoPath, ref.ref, ref.upstream.name);
}
}
return Promise.resolve();
}
@debug()
private compareWorkingWith(item?: GraphItemContext) {
const ref = this.getGraphItemRef(item);
if (ref == null) return Promise.resolve();
return this.container.searchAndCompareView.compare(ref.repoPath, '', ref.ref);
}
@debug()
private async openFiles(item?: GraphItemContext) {
const commit = await this.getCommitFromGraphItemRef(item);
if (commit == null) return;
return openFiles(commit);
}
@debug()
private async openAllChanges(item?: GraphItemContext) {
const commit = await this.getCommitFromGraphItemRef(item);
if (commit == null) return;
return openAllChanges(commit);
}
@debug()
private async openAllChangesWithWorking(item?: GraphItemContext) {
const commit = await this.getCommitFromGraphItemRef(item);
if (commit == null) return;
return openAllChangesWithWorking(commit);
}
@debug()
private async openRevisions(item?: GraphItemContext) {
const commit = await this.getCommitFromGraphItemRef(item);
if (commit == null) return;
return openFilesAtRevision(commit);
}
@debug()
private addAuthor(item?: GraphItemContext) {
if (isGraphItemTypedContext(item, 'contributor')) {
const { repoPath, name, email, current } = item.webviewItemValue;
return ContributorActions.addAuthors(
repoPath,
new GitContributor(repoPath, name, email, 0, undefined, current),
);
}
return Promise.resolve();
}
@debug()
private async toggleColumn(name: GraphColumnName, visible: boolean) {
let columns = this.container.storage.getWorkspace('graph:columns');
let column = columns?.[name];
if (column != null) {
column.isHidden = !visible;
} else {
column = { isHidden: !visible };
}
columns = updateRecordValue(columns, name, column);
await this.container.storage.storeWorkspace('graph:columns', columns);
void this.notifyDidChangeColumns();
if (name === 'changes' && !column.isHidden && !this._graph?.includes?.stats) {
this.updateState();
}
}
@debug()
private async toggleScrollMarker(type: GraphScrollMarkersAdditionalTypes, enabled: boolean) {
let scrollMarkers = configuration.get('graph.scrollMarkers.additionalTypes');
let updated = false;
if (enabled && !scrollMarkers.includes(type)) {
scrollMarkers = scrollMarkers.concat(type);
updated = true;
} else if (!enabled && scrollMarkers.includes(type)) {
scrollMarkers = scrollMarkers.filter(marker => marker !== type);
updated = true;
}
if (updated) {
await configuration.updateEffective('graph.scrollMarkers.additionalTypes', scrollMarkers);
void this.notifyDidChangeScrollMarkers();
}
}
@debug()
private async setColumnMode(name: GraphColumnName, mode?: string) {
let columns = this.container.storage.getWorkspace('graph:columns');
let column = columns?.[name];
if (column != null) {
column.mode = mode;
} else {
column = { mode: mode };
}
columns = updateRecordValue(columns, name, column);
await this.container.storage.storeWorkspace('graph:columns', columns);
void this.notifyDidChangeColumns();
}
private getCommitFromGraphItemRef(item?: GraphItemContext): Promise<GitCommit | undefined> {
let ref: GitRevisionReference | GitStashReference | undefined = this.getGraphItemRef(item, 'revision');
if (ref != null) return this.container.git.getCommit(ref.repoPath, ref.ref);
ref = this.getGraphItemRef(item, 'stash');
if (ref != null) return this.container.git.getCommit(ref.repoPath, ref.ref);
return Promise.resolve(undefined);
}
private getGraphItemRef(item?: GraphItemContext | unknown | undefined): GitReference | undefined;
private getGraphItemRef(
item: GraphItemContext | unknown | undefined,
refType: 'branch',
): GitBranchReference | undefined;
private getGraphItemRef(
item: GraphItemContext | unknown | undefined,
refType: 'revision',
): GitRevisionReference | undefined;
private getGraphItemRef(
item: GraphItemContext | unknown | undefined,
refType: 'stash',
): GitStashReference | undefined;
private getGraphItemRef(item: GraphItemContext | unknown | undefined, refType: 'tag'): GitTagReference | undefined;
private getGraphItemRef(
item?: GraphItemContext | unknown,
refType?: 'branch' | 'revision' | 'stash' | 'tag',
): GitReference | undefined {
if (item == null) {
const ref = this.activeSelection;
return ref != null && (refType == null || refType === ref.refType) ? ref : undefined;
}
switch (refType) {
case 'branch':
return isGraphItemRefContext(item, 'branch') || isGraphItemTypedContext(item, 'upstreamStatus')
? item.webviewItemValue.ref
: undefined;
case 'revision':
return isGraphItemRefContext(item, 'revision') ? item.webviewItemValue.ref : undefined;
case 'stash':
return isGraphItemRefContext(item, 'stash') ? item.webviewItemValue.ref : undefined;
case 'tag':
return isGraphItemRefContext(item, 'tag') ? item.webviewItemValue.ref : undefined;
default:
return isGraphItemRefContext(item) ? item.webviewItemValue.ref : undefined;
}
}
}
function formatRepositories(repositories: Repository[]): GraphRepository[] {
if (repositories.length === 0) return [];
return repositories.map(r => ({
formattedName: r.formattedName,
id: r.id,
name: r.name,
path: r.path,
isVirtual: r.provider.virtual,
}));
}
function isGraphItemContext(item: unknown): item is GraphItemContext {
if (item == null) return false;
return isWebviewItemContext(item) && (item.webview === 'gitlens.graph' || item.webview === 'gitlens.views.graph');
}
function isGraphItemGroupContext(item: unknown): item is GraphItemGroupContext {
if (item == null) return false;
return (
isWebviewItemGroupContext(item) && (item.webview === 'gitlens.graph' || item.webview === 'gitlens.views.graph')
);
}
function isGraphItemTypedContext(
item: unknown,
type: 'contributor',
): item is GraphItemTypedContext<GraphContributorContextValue>;
function isGraphItemTypedContext(
item: unknown,
type: 'pullrequest',
): item is GraphItemTypedContext<GraphPullRequestContextValue>;
function isGraphItemTypedContext(
item: unknown,
type: 'upstreamStatus',
): item is GraphItemTypedContext<GraphUpstreamStatusContextValue>;
function isGraphItemTypedContext(
item: unknown,
type: GraphItemTypedContextValue['type'],
): item is GraphItemTypedContext {
if (item == null) return false;
return isGraphItemContext(item) && typeof item.webviewItemValue === 'object' && item.webviewItemValue.type === type;
}
function isGraphItemRefGroupContext(item: unknown): item is GraphItemRefGroupContext {
if (item == null) return false;
return (
isGraphItemGroupContext(item) &&
typeof item.webviewItemGroupValue === 'object' &&
item.webviewItemGroupValue.type === 'refGroup'
);
}
function isGraphItemRefContext(item: unknown): item is GraphItemRefContext;
function isGraphItemRefContext(item: unknown, refType: 'branch'): item is GraphItemRefContext<GraphBranchContextValue>;
function isGraphItemRefContext(
item: unknown,
refType: 'revision',
): item is GraphItemRefContext<GraphCommitContextValue>;
function isGraphItemRefContext(item: unknown, refType: 'stash'): item is GraphItemRefContext<GraphStashContextValue>;
function isGraphItemRefContext(item: unknown, refType: 'tag'): item is GraphItemRefContext<GraphTagContextValue>;
function isGraphItemRefContext(item: unknown, refType?: GitReference['refType']): item is GraphItemRefContext {
if (item == null) return false;
return (
isGraphItemContext(item) &&
typeof item.webviewItemValue === 'object' &&
'ref' in item.webviewItemValue &&
(refType == null || item.webviewItemValue.ref.refType === refType)
);
}
function getRepoPathFromBranchOrTagId(id: string): string {
return id.split('|', 1)[0];
}
export function hasGitReference(o: unknown): o is { ref: GitReference } {
if (o == null || typeof o !== 'object') return false;
if (!('ref' in o)) return false;
return isGitReference(o.ref);
}