Browse Source

Adds context menus to the Graph (wip)

main
Eric Amodio 2 years ago
parent
commit
9bb1012c40
6 changed files with 411 additions and 18 deletions
  1. +133
    -0
      package.json
  2. +95
    -13
      src/env/node/git/localGitProvider.ts
  3. +3
    -1
      src/git/models/graph.ts
  4. +164
    -3
      src/plus/webviews/graph/graphWebview.ts
  5. +15
    -0
      src/system/webview.ts
  6. +1
    -1
      src/webviews/apps/plus/graph/graph.html

+ 133
- 0
package.json View File

@ -6249,6 +6249,56 @@
"command": "gitlens.disableDebugLogging",
"title": "Disable Debug Logging",
"category": "GitLens"
},
{
"command": "gitlens.graph.switchToAnotherBranch",
"title": "Switch to Another Branch...",
"category": "GitLens",
"icon": "$(gitlens-switch)"
},
{
"command": "gitlens.graph.switchToBranch",
"title": "Switch to Branch...",
"category": "GitLens",
"icon": "$(gitlens-switch)"
},
{
"command": "gitlens.graph.switchToCommit",
"title": "Switch to Commit...",
"category": "GitLens",
"icon": "$(gitlens-switch)"
},
{
"command": "gitlens.graph.switchToTag",
"title": "Switch to Tag...",
"category": "GitLens",
"icon": "$(gitlens-switch)"
},
{
"command": "gitlens.graph.rebaseOntoCommit",
"title": "Rebase Current Branch onto Commit...",
"category": "GitLens"
},
{
"command": "gitlens.graph.resetCommit",
"title": "Reset Current Branch to Previous Commit...",
"category": "GitLens"
},
{
"command": "gitlens.graph.resetToCommit",
"title": "Reset Current Branch to Commit...",
"category": "GitLens"
},
{
"command": "gitlens.graph.revert",
"title": "Revert Commit...",
"category": "GitLens"
},
{
"command": "gitlens.graph.undoCommit",
"title": "Undo Commit",
"category": "GitLens",
"icon": "$(discard)"
}
],
"icons": {
@ -8037,6 +8087,42 @@
"when": "false"
},
{
"command": "gitlens.graph.switchToAnotherBranch",
"when": "false"
},
{
"command": "gitlens.graph.switchToBranch",
"when": "false"
},
{
"command": "gitlens.graph.switchToCommit",
"when": "false"
},
{
"command": "gitlens.graph.switchToTag",
"when": "false"
},
{
"command": "gitlens.graph.rebaseOntoCommit",
"when": "false"
},
{
"command": "gitlens.graph.resetCommit",
"when": "false"
},
{
"command": "gitlens.graph.resetToCommit",
"when": "false"
},
{
"command": "gitlens.graph.revert",
"when": "false"
},
{
"command": "gitlens.graph.undoCommit",
"when": "false"
},
{
"command": "gitlens.enableDebugLogging",
"when": "config.gitlens.outputLevel != debug"
},
@ -10467,6 +10553,53 @@
"group": "1_gitlens@0"
}
],
"webview/context": [
{
"command": "gitlens.graph.switchToAnotherBranch",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem =~ /gitlens:branch\\b(?=.*?\\b\\+current\\b)/",
"group": "1_gitlens_actions@1"
},
{
"command": "gitlens.graph.switchToBranch",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem =~ /gitlens:branch\\b(?!.*?\\b\\+current\\b)/",
"group": "1_gitlens_actions@1"
},
{
"command": "gitlens.graph.undoCommit",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem =~ /gitlens:commit\\b(?=.*?\\b\\+HEAD\\b)/",
"group": "1_gitlens_actions@1"
},
{
"command": "gitlens.graph.revert",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem =~ /gitlens:commit\\b(?=.*?\\b\\+current\\b)/",
"group": "1_gitlens_actions@3"
},
{
"command": "gitlens.graph.resetToCommit",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem =~ /gitlens:commit\\b(?=.*?\\b\\+current\\b)/",
"group": "1_gitlens_actions@4"
},
{
"command": "gitlens.graph.resetCommit",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem =~ /gitlens:commit\\b(?=.*?\\b\\+current\\b)/",
"group": "1_gitlens_actions@5"
},
{
"command": "gitlens.graph.rebaseOntoCommit",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem =~ /gitlens:commit\\b/",
"group": "1_gitlens_actions@6"
},
{
"command": "gitlens.graph.switchToCommit",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem =~ /gitlens:commit\\b/",
"group": "1_gitlens_actions@7"
},
{
"command": "gitlens.graph.switchToTag",
"when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && webviewItem =~ /gitlens:tag\\b/",
"group": "1_gitlens_actions@1"
}
],
"gitlens/commit/browse": [
{
"command": "gitlens.views.browseRepoAtRevision",

+ 95
- 13
src/env/node/git/localGitProvider.ts View File

@ -60,6 +60,7 @@ import { GitFileChange } from '../../../git/models/file';
import type {
GitGraph,
GitGraphRow,
GitGraphRowContexts,
GitGraphRowHead,
GitGraphRowRemoteHead,
GitGraphRowTag,
@ -118,6 +119,7 @@ import {
showGitMissingErrorMessage,
showGitVersionUnsupportedErrorMessage,
} from '../../../messages';
import type { GraphItemContext, GraphItemRefContext } from '../../../plus/webviews/graph/graphWebview';
import { countStringLength, filterMap } from '../../../system/array';
import { TimedCancellationSource } from '../../../system/cancellation';
import { gate } from '../../../system/decorators/gate';
@ -140,6 +142,7 @@ import { any, fastestSettled, getSettledValue } from '../../../system/promise';
import { equalsIgnoreCase, getDurationMilliseconds, interpolate, md5, splitSingle } from '../../../system/string';
import { PathTrie } from '../../../system/trie';
import { compare, fromString } from '../../../system/version';
import { serializeWebviewItemContext } from '../../../system/webview';
import type { CachedBlame, CachedDiff, CachedLog, TrackedDocument } from '../../../trackers/gitDocumentTracker';
import { GitDocumentState } from '../../../trackers/gitDocumentTracker';
import type { Git } from './git';
@ -1731,12 +1734,15 @@ export class LocalGitProvider implements GitProvider, Disposable {
const rows: GitGraphRow[] = [];
let current = false;
let headCommit = false;
let refHeads: GitGraphRowHead[];
let refRemoteHeads: GitGraphRowRemoteHead[];
let refTags: GitGraphRowTag[];
let parents: string[];
let remoteName: string;
let isStashCommit: boolean;
let stashCommit: GitStashCommit | undefined;
let tag: GitGraphRowTag;
let contexts: GitGraphRowContexts | undefined;
let count = 0;
@ -1753,24 +1759,40 @@ export class LocalGitProvider implements GitProvider, Disposable {
refHeads = [];
refRemoteHeads = [];
refTags = [];
contexts = undefined;
headCommit = false;
if (commit.tips) {
for (let tip of commit.tips.split(', ')) {
if (tip === 'refs/stash') continue;
if (tip.startsWith('tag: ')) {
refTags.push({
tag = {
name: tip.substring(5),
// Not currently used, so don't bother looking it up
annotated: true,
};
tag.context = serializeWebviewItemContext<GraphItemRefContext>({
webviewItem: 'gitlens:tag',
webviewItemValue: {
type: 'tag',
ref: GitReference.create(tag.name, repoPath, {
refType: 'tag',
name: tag.name,
}),
},
});
refTags.push(tag);
continue;
}
current = tip.startsWith('HEAD');
if (current && tip !== 'HEAD') {
tip = tip.substring(8);
if (current) {
headCommit = true;
if (tip !== 'HEAD') {
tip = tip.substring(8);
}
}
remoteName = getRemoteNameFromBranchName(tip);
@ -1788,6 +1810,18 @@ export class LocalGitProvider implements GitProvider, Disposable {
remote.provider?.avatarUri ??
getRemoteIconUri(this.container, remote, asWebviewUri)
)?.toString(true),
context: serializeWebviewItemContext<GraphItemRefContext>({
webviewItem: 'gitlens:branch+remote',
webviewItemValue: {
type: 'branch',
ref: GitReference.create(branchName, repoPath, {
refType: 'branch',
name: branchName,
remote: true,
upstream: remote.name,
}),
},
}),
});
continue;
@ -1797,15 +1831,61 @@ export class LocalGitProvider implements GitProvider, Disposable {
refHeads.push({
name: tip,
isCurrentHead: current,
// TODO@eamodio Add +tracking
context: serializeWebviewItemContext<GraphItemRefContext>({
webviewItem: `gitlens:branch${current ? '+current' : ''}`,
webviewItemValue: {
type: 'branch',
ref: GitReference.create(tip, repoPath, {
refType: 'branch',
name: tip,
remote: false,
// upstream: undefined,
}),
},
}),
});
}
}
isStashCommit = stash?.commits.has(commit.sha) ?? false;
stashCommit = stash?.commits.get(commit.sha);
contexts = {};
if (stashCommit != null) {
contexts.row = serializeWebviewItemContext<GraphItemRefContext>({
webviewItem: 'gitlens:stash',
webviewItemValue: {
type: 'stash',
ref: GitReference.create(commit.sha, repoPath, {
refType: 'stash',
name: stashCommit.name,
number: stashCommit.number,
}),
},
});
} else {
contexts.row = serializeWebviewItemContext<GraphItemRefContext>({
webviewItem: `gitlens:commit${headCommit ? '+HEAD' : ''}${true ? '+current' : ''}`,
webviewItemValue: {
type: 'commit',
ref: GitReference.create(commit.sha, repoPath, {
refType: 'revision',
message: commit.message,
}),
},
});
contexts.avatar = serializeWebviewItemContext<GraphItemContext>({
webviewItem: 'gitlens:avatar',
webviewItemValue: {
type: 'avatar',
email: commit.authorEmail,
},
});
}
parents = commit.parents ? commit.parents.split(' ') : [];
// Remove the second & third parent, if exists, from each stash commit as it is a Git implementation for the index and untracked files
if (isStashCommit && parents.length > 1) {
if (stashCommit != null && parents.length > 1) {
// Skip the "index commit" (e.g. contains staged files) of the stash
skipStashParents.add(parents[1]);
// Skip the "untracked commit" (e.g. contains untracked files) of the stash
@ -1813,7 +1893,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
parents.splice(1, 2);
}
if (!isStashCommit && !avatars.has(commit.authorEmail)) {
if (stashCommit == null && !avatars.has(commit.authorEmail)) {
const uri = getCachedAvatarUri(commit.authorEmail);
if (uri != null) {
avatars.set(commit.authorEmail, uri.toString(true));
@ -1824,18 +1904,20 @@ export class LocalGitProvider implements GitProvider, Disposable {
sha: commit.sha,
parents: parents,
author: commit.author,
email: commit.authorEmail ?? '',
email: commit.authorEmail,
date: Number(ordering === 'author-date' ? commit.authorDate : commit.committerDate) * 1000,
message: emojify(commit.message.trim()),
// TODO: review logic for stash, wip, etc
type: isStashCommit
? GitGraphRowType.Stash
: parents.length > 1
? GitGraphRowType.MergeCommit
: GitGraphRowType.Commit,
type:
stashCommit != null
? GitGraphRowType.Stash
: parents.length > 1
? GitGraphRowType.MergeCommit
: GitGraphRowType.Commit,
heads: refHeads,
remotes: refRemoteHeads,
tags: refTags,
contexts: contexts,
});
}

+ 3
- 1
src/git/models/graph.ts View File

@ -1,8 +1,9 @@
import type { GraphRow, Head, Remote, Tag } from '@gitkraken/gitkraken-components';
import type { GraphRow, Head, Remote, RowContexts, Tag } from '@gitkraken/gitkraken-components';
export type GitGraphRowHead = Head;
export type GitGraphRowRemoteHead = Remote;
export type GitGraphRowTag = Tag;
export type GitGraphRowContexts = RowContexts;
export const enum GitGraphRowType {
Commit = 'commit-node',
MergeCommit = 'merge-node',
@ -17,6 +18,7 @@ export interface GitGraphRow extends GraphRow {
heads?: GitGraphRowHead[];
remotes?: GitGraphRowRemoteHead[];
tags?: GitGraphRowTag[];
contexts?: GitGraphRowContexts;
}
export interface GitGraph {

+ 164
- 3
src/plus/webviews/graph/graphWebview.ts View File

@ -4,18 +4,25 @@ import { getAvatarUri } from '../../../avatars';
import { parseCommandContext } from '../../../commands/base';
import { GitActions } from '../../../commands/gitCommands.actions';
import { configuration } from '../../../configuration';
import { Commands, ContextKeys } from '../../../constants';
import { Commands, ContextKeys, CoreGitCommands } from '../../../constants';
import type { Container } from '../../../container';
import { setContext } from '../../../context';
import { PlusFeatures } from '../../../features';
import type { GitCommit } from '../../../git/models/commit';
import { GitGraphRowType } from '../../../git/models/graph';
import type { GitGraph } from '../../../git/models/graph';
import type {
GitBranchReference,
GitRevisionReference,
GitStashReference,
GitTagReference,
} from '../../../git/models/reference';
import { GitReference } from '../../../git/models/reference';
import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository';
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository';
import type { GitSearch } from '../../../git/search';
import { getSearchQueryComparisonKey } from '../../../git/search';
import { registerCommand } from '../../../system/command';
import { executeCoreGitCommand, registerCommand } from '../../../system/command';
import { gate } from '../../../system/decorators/gate';
import { debug } from '../../../system/decorators/log';
import type { Deferrable } from '../../../system/function';
@ -23,6 +30,8 @@ import { debounce } from '../../../system/function';
import { first, last } from '../../../system/iterable';
import { updateRecordValue } from '../../../system/object';
import { isDarkTheme, isLightTheme } from '../../../system/utils';
import type { WebviewItemContext } from '../../../system/webview';
import { isWebviewItemContext } from '../../../system/webview';
import { RepositoryFolderNode } from '../../../views/nodes/viewNode';
import type { IpcMessage } from '../../../webviews/protocol';
import { onIpc } from '../../../webviews/protocol';
@ -191,7 +200,18 @@ export class GraphWebview extends WebviewBase {
}
protected override registerCommands(): Disposable[] {
return [registerCommand(Commands.RefreshGraphPage, () => this.refresh(true))];
return [
registerCommand(Commands.RefreshGraphPage, () => this.refresh(true)),
registerCommand('gitlens.graph.switchToAnotherBranch', this.switchToAnother, this),
registerCommand('gitlens.graph.switchToBranch', this.switchTo, this),
registerCommand('gitlens.graph.undoCommit', this.undoCommit, this),
registerCommand('gitlens.graph.revert', this.revertCommit, this),
registerCommand('gitlens.graph.resetToCommit', this.resetToCommit, this),
registerCommand('gitlens.graph.resetCommit', this.resetCommit, this),
registerCommand('gitlens.graph.rebaseOntoCommit', this.rebase, this),
registerCommand('gitlens.graph.switchToCommit', this.switchTo, this),
registerCommand('gitlens.graph.switchToTag', this.switchTo, this),
];
}
protected override onInitializing(): Disposable[] | undefined {
@ -794,6 +814,97 @@ export class GraphWebview extends WebviewBase {
this._selectedSha = sha;
this._selectedRows = sha != null ? { [sha]: true } : {};
}
@debug()
private rebase(item: GraphItemContext) {
if (isGraphItemRefContext(item)) {
const { ref } = item.webviewItemValue;
return GitActions.rebase(ref.repoPath, ref);
}
return Promise.resolve();
}
@debug()
private resetCommit(item: GraphItemContext) {
if (isGraphItemRefContext(item) && item.webviewItemValue.ref.refType === 'revision') {
const { ref } = item.webviewItemValue;
return GitActions.revert(
ref.repoPath,
GitReference.create(`${ref.ref}^`, ref.repoPath, {
refType: 'revision',
name: `${ref.name}^`,
message: ref.message,
}),
);
}
return Promise.resolve();
}
@debug()
private resetToCommit(item: GraphItemContext) {
if (isGraphItemRefContext(item) && item.webviewItemValue.ref.refType === 'revision') {
const { ref } = item.webviewItemValue;
return GitActions.reset(ref.repoPath, ref);
}
return Promise.resolve();
}
@debug()
private revertCommit(item: GraphItemContext) {
if (isGraphItemRefContext(item) && item.webviewItemValue.ref.refType === 'revision') {
const { ref } = item.webviewItemValue;
return GitActions.revert(ref.repoPath, ref);
}
return Promise.resolve();
}
@debug()
private switchTo(item: GraphItemContext) {
if (isGraphItemRefContext(item)) {
const { ref } = item.webviewItemValue;
return GitActions.switchTo(ref.repoPath, ref);
}
return Promise.resolve();
}
@debug()
private switchToAnother(item: GraphItemContext) {
if (isGraphItemRefContext(item)) {
const { ref } = item.webviewItemValue;
return GitActions.switchTo(ref.repoPath);
}
return Promise.resolve();
}
@debug()
private async undoCommit(item: GraphItemContext) {
if (isGraphItemRefContext(item) && item.webviewItemValue.ref.refType === 'revision') {
const ref = item.webviewItemValue.ref;
const repo = await this.container.git.getOrOpenScmRepository(ref.repoPath);
const commit = await repo?.getCommit('HEAD');
if (commit?.hash !== ref.ref) {
void window.showWarningMessage(
`Commit ${GitReference.toString(ref, {
capitalize: true,
icon: false,
})} cannot be undone, because it is no longer the most recent commit.`,
);
return;
}
return void executeCoreGitCommand(CoreGitCommands.UndoCommit, ref.repoPath);
}
return Promise.resolve();
}
}
function formatRepositories(repositories: Repository[]): GraphRepository[] {
@ -806,3 +917,53 @@ function formatRepositories(repositories: Repository[]): GraphRepository[] {
path: r.path,
}));
}
export type GraphItemContext = WebviewItemContext<GraphItemContextValue>;
export type GraphItemRefContext = WebviewItemContext<GraphItemRefContextValue>;
export type GraphItemRefContextValue =
| GraphBranchContextValue
| GraphCommitContextValue
| GraphStashContextValue
| GraphTagContextValue;
export type GraphItemContextValue = GraphAvatarContextValue | GraphColumnsContextValue | GraphItemRefContextValue;
export interface GraphAvatarContextValue {
type: 'avatar';
email: string;
}
export interface GraphColumnsContextValue {
type: 'columns';
}
export interface GraphBranchContextValue {
type: 'branch';
ref: GitBranchReference;
}
export interface GraphCommitContextValue {
type: 'commit';
ref: GitRevisionReference;
}
export interface GraphStashContextValue {
type: 'stash';
ref: GitStashReference;
}
export interface GraphTagContextValue {
type: 'tag';
ref: GitTagReference;
}
function isGraphItemContext(item: unknown): item is GraphItemContext {
if (item == null) return false;
return isWebviewItemContext(item) && item.webview === 'gitlens.graph';
}
function isGraphItemRefContext(item: unknown): item is GraphItemRefContext {
if (item == null) return false;
return isGraphItemContext(item) && 'ref' in item.webviewItemValue;
}

+ 15
- 0
src/system/webview.ts View File

@ -0,0 +1,15 @@
export interface WebviewItemContext<TValue = unknown> {
webview?: string;
webviewItem: string;
webviewItemValue: TValue;
}
export function isWebviewItemContext<TValue = unknown>(item: unknown): item is WebviewItemContext<TValue> {
if (item == null) return false;
return 'webview' in item && 'webviewItem' in item;
}
export function serializeWebviewItemContext<T = WebviewItemContext>(context: T): string {
return JSON.stringify(context);
}

+ 1
- 1
src/webviews/apps/plus/graph/graph.html View File

@ -4,7 +4,7 @@
<meta charset="utf-8" />
</head>
<body class="graph-app preload">
<body class="graph-app preload" data-vscode-context='{ "preventDefaultContextMenuItems": true }'>
<div id="root" class="graph-app__container">
<p>A repository must be selected.</p>
</div>

Loading…
Cancel
Save