Browse Source

Refactors git cmds, commit/stash revealing, & more

Cleans up navigation key support for git commands
Changes search git command to show a quick pick menu of commit commands
Adds navigation key support to search git commands to reveal commits
Removes incorrect multiselect of repos in search git command
Adds navigation key support to stash git commands to reveal stashes
Changes stash list git command picker to stay in context
Changes Show In View to be Open in Search Commits View
Changes Show In View to be Open in File History View
Adds Reveal in Repositories View command in commit quick picks
Changes commit quick pick commands (ordering, descriptions, etc)
Adds progress notification to revealCommit & reveal parent walking
Changes findNode to use a loop & canTraverse callback & adds cancelation
Changes findCommit to search for commit in all branches
main
Eric Amodio 5 years ago
parent
commit
cdfcf5c76b
13 changed files with 477 additions and 322 deletions
  1. +71
    -45
      src/commands/git/search.ts
  2. +47
    -27
      src/commands/git/stash.ts
  3. +23
    -22
      src/commands/gitCommands.ts
  4. +4
    -1
      src/commands/quickCommand.ts
  5. +2
    -2
      src/commands/showQuickCommitDetails.ts
  6. +3
    -3
      src/commands/showQuickFileHistory.ts
  7. +7
    -0
      src/git/gitService.ts
  8. +3
    -3
      src/quickpicks/commitFileQuickPick.ts
  9. +56
    -67
      src/quickpicks/commitQuickPick.ts
  10. +49
    -56
      src/quickpicks/commonQuickPicks.ts
  11. +1
    -0
      src/views/nodes.ts
  12. +97
    -50
      src/views/repositoriesView.ts
  13. +114
    -46
      src/views/viewBase.ts

+ 71
- 45
src/commands/git/search.ts View File

@ -13,7 +13,7 @@ import {
} from '../../git/gitService';
import { GlyphChars } from '../../constants';
import { QuickCommandBase, StepAsyncGenerator, StepSelection, StepState } from '../quickCommand';
import { RepositoryQuickPickItem } from '../../quickpicks';
import { CommandQuickPickItem, CommitQuickPick, RepositoryQuickPickItem } from '../../quickpicks';
import { Iterables, Mutable, Strings } from '../../system';
import { Logger } from '../../logger';
import {
@ -23,12 +23,8 @@ import {
QuickPickItemOfT
} from '../../quickpicks/gitQuickPicks';
interface State {
interface State extends Required<SearchPattern> {
repo: Repository;
search: string;
matchAll: boolean;
matchCase: boolean;
matchRegex: boolean;
showInView: boolean;
}
@ -141,7 +137,7 @@ export class SearchGitCommand extends QuickCommandBase {
counter++;
}
if (args.state.search !== undefined && !args.prefillOnly) {
if (args.state.pattern !== undefined && !args.prefillOnly) {
counter++;
}
@ -195,7 +191,6 @@ export class SearchGitCommand extends QuickCommandBase {
const active = state.repo ? state.repo : await Container.git.getActiveRepository();
const step = this.createPickStep<RepositoryQuickPickItem>({
multiselect: true,
title: this.title,
placeholder: 'Choose repositories',
items: await Promise.all(
@ -218,7 +213,7 @@ export class SearchGitCommand extends QuickCommandBase {
}
}
if (state.search === undefined || state.counter < 2) {
if (state.pattern === undefined || state.counter < 2) {
const items: QuickPickItemOfT<SearchOperators>[] = [
{
label: searchOperatorToTitleMap.get('')!,
@ -268,7 +263,7 @@ export class SearchGitCommand extends QuickCommandBase {
matchOnDetail: true,
additionalButtons: [matchCaseButton, matchAllButton, matchRegexButton, showInViewButton],
items: items,
value: state.search,
value: state.pattern,
onDidAccept: (quickpick): boolean => {
const pick = quickpick.selectedItems[0];
if (!searchOperators.has(pick.item)) return true;
@ -352,11 +347,11 @@ export class SearchGitCommand extends QuickCommandBase {
continue;
}
state.search = selection[0].item.trim();
state.pattern = selection[0].item.trim();
}
const search: SearchPattern = {
pattern: state.search,
pattern: state.pattern,
matchAll: state.matchAll,
matchCase: state.matchCase,
matchRegex: state.matchRegex
@ -373,7 +368,7 @@ export class SearchGitCommand extends QuickCommandBase {
state.repo.path,
search,
{
label: { label: `for ${state.search}` }
label: { label: `for ${state.pattern}` }
},
resultsPromise
);
@ -387,10 +382,10 @@ export class SearchGitCommand extends QuickCommandBase {
title: `${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.formattedName}`,
placeholder:
results === undefined
? `No results for ${state.search}`
? `No results for ${state.pattern}`
: `${Strings.pluralize('result', results.count, {
number: results.truncated ? `${results.count}+` : undefined
})} for ${state.search}`,
})} for ${state.pattern}`,
matchOnDescription: true,
matchOnDetail: true,
items:
@ -416,10 +411,31 @@ export class SearchGitCommand extends QuickCommandBase {
state.repo!.path,
search,
{
label: { label: `for ${state.search}` }
label: { label: `for ${state.pattern}` }
},
results
);
},
keys: ['right', 'alt+right', 'ctrl+right'],
onDidPressKey: async (quickpick, key) => {
if (quickpick.activeItems.length === 0) return;
const commit = quickpick.activeItems[0].item;
if (key === 'ctrl+right') {
await Container.repositoriesView.revealCommit(commit, {
select: true,
focus: false,
expand: true
});
} else {
await Container.searchView.search(
commit.repoPath,
{ pattern: SearchPattern.fromCommit(commit) },
{
label: { label: `for commit id ${commit.shortSha}` }
}
);
}
}
});
const selection: StepSelection<typeof step> = yield step;
@ -428,38 +444,48 @@ export class SearchGitCommand extends QuickCommandBase {
continue;
}
state.counter--;
pickedCommit = selection[0].item;
void Container.searchView.search(
pickedCommit.repoPath,
{ pattern: `commit:${pickedCommit.sha}` },
{
label: { label: `for commit id ${pickedCommit.shortSha}` }
if (pickedCommit !== undefined) {
const step = this.createPickStep<CommandQuickPickItem>({
title: `${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repo.formattedName
}${Strings.pad(GlyphChars.Dot, 2, 2)}${pickedCommit.shortSha}`,
placeholder: `${pickedCommit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${
pickedCommit.author ? `${pickedCommit.author}, ` : ''
}${pickedCommit.formattedDate} ${Strings.pad(
GlyphChars.Dot,
1,
1
)} ${pickedCommit.getShortMessage()}`,
items: await CommitQuickPick.getItems(pickedCommit, pickedCommit.toGitUri(), {
showChanges: false
}),
additionalButtons: [this.Buttons.OpenInView],
onDidClickButton: (quickpick, button) => {
if (button !== this.Buttons.OpenInView) return;
void Container.searchView.search(
pickedCommit!.repoPath,
{ pattern: SearchPattern.fromCommit(pickedCommit!) },
{
label: { label: `for commit id ${pickedCommit!.shortSha}` }
}
);
}
});
const selection: StepSelection<typeof step> = yield step;
if (!this.canPickStepMoveNext(step, state, selection)) {
continue;
}
);
// const gitCommandArgs: GitCommandsCommandArgs = {
// command: 'search',
// state: { ...state }
// };
// const commandArgs: ShowQuickCommitDetailsCommandArgs = {
// sha: commit.sha,
// commit: commit,
// goBackCommand: new CommandQuickPickItem(
// {
// label: 'Back',
// description: ''
// },
// Commands.GitCommands,
// [gitCommandArgs]
// )
// };
// void commands.executeCommand(Commands.ShowQuickCommitDetails, commit.toGitUri(), commandArgs);
// break;
const command = selection[0];
if (command instanceof CommandQuickPickItem) {
command.execute();
break;
}
}
} catch (ex) {
Logger.error(ex, this.title);

+ 47
- 27
src/commands/git/stash.ts View File

@ -1,11 +1,12 @@
'use strict';
/* eslint-disable no-loop-func */
import { commands, QuickInputButton, QuickInputButtons, QuickPickItem, Uri, window } from 'vscode';
import { QuickInputButton, QuickInputButtons, QuickPickItem, Uri, window } from 'vscode';
import { Container } from '../../container';
import { GitStashCommit, GitUri, Repository } from '../../git/gitService';
import { GitStashCommit, GitUri, Repository, SearchPattern } from '../../git/gitService';
import { BreakQuickCommand, QuickCommandBase, StepAsyncGenerator, StepSelection, StepState } from '../quickCommand';
import {
CommandQuickPickItem,
CommitQuickPick,
CommitQuickPickItem,
Directive,
DirectiveQuickPickItem,
@ -17,8 +18,6 @@ import { Iterables, Strings } from '../../system';
import { GlyphChars } from '../../constants';
import { Logger } from '../../logger';
import { Messages } from '../../messages';
import { GitCommandsCommandArgs, ShowQuickCommitDetailsCommandArgs } from '../../commands';
import { Commands } from '../common';
interface ApplyState {
subcommand: 'apply';
@ -586,6 +585,17 @@ export class StashGitCommand extends QuickCommandBase {
expand: true
});
}
},
keys: ['right', 'alt+right', 'ctrl+right'],
onDidPressKey: async (quickpick, key) => {
if (quickpick.activeItems.length === 0) return;
const stash = quickpick.activeItems[0].item;
await Container.repositoriesView.revealStash(stash, {
select: true,
focus: false,
expand: true
});
}
});
const selection: StepSelection<typeof step> = yield step;
@ -594,33 +604,43 @@ export class StashGitCommand extends QuickCommandBase {
break;
}
state.counter--;
pickedStash = selection[0].item;
// const node = await Container.repositoriesView.findStashNode(pickedStash);
// if (node !== undefined) {
// Container.repositoriesView.reveal(node, { select: true, expand: true });
// }
if (pickedStash !== undefined) {
const step = this.createPickStep<CommandQuickPickItem>({
title: `${this.title} ${getSubtitle(state.subcommand)}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repo.formattedName
}${Strings.pad(GlyphChars.Dot, 2, 2)}${pickedStash.shortSha}`,
placeholder: `${
pickedStash.number === undefined ? '' : `${pickedStash.number}: `
}${pickedStash.getShortMessage()}`,
items: await CommitQuickPick.getItems(pickedStash, pickedStash.toGitUri(), { showChanges: false }),
additionalButtons: [this.Buttons.OpenInView],
onDidClickButton: (quickpick, button) => {
if (button !== this.Buttons.OpenInView) return;
void Container.searchView.search(
pickedStash!.repoPath,
{ pattern: SearchPattern.fromCommit(pickedStash!) },
{
label: { label: `for commit id ${pickedStash!.shortSha}` }
}
);
}
});
const selection: StepSelection<typeof step> = yield step;
if (!this.canPickStepMoveNext(step, state, selection)) {
continue;
}
const gitCommandArgs: GitCommandsCommandArgs = {
command: 'stash',
state: { ...state }
};
const command = selection[0];
if (command instanceof CommandQuickPickItem) {
command.execute();
const commandArgs: ShowQuickCommitDetailsCommandArgs = {
sha: pickedStash.sha,
commit: pickedStash,
goBackCommand: new CommandQuickPickItem(
{
label: 'Back',
description: ''
},
Commands.GitCommands,
[gitCommandArgs]
)
};
void commands.executeCommand(Commands.ShowQuickCommitDetails, pickedStash.toGitUri(), commandArgs);
throw new BreakQuickCommand();
}
}
}
return undefined;

+ 23
- 22
src/commands/gitCommands.ts View File

@ -24,6 +24,7 @@ import { StashGitCommand, StashGitCommandArgs } from './git/stash';
import { SwitchGitCommand, SwitchGitCommandArgs } from './git/switch';
import { Container } from '../container';
import { configuration } from '../configuration';
import { KeyMapping } from '../keyboard';
const sanitizeLabel = /\$\(.+?\)|\s/g;
@ -140,18 +141,18 @@ export class GitCommandsCommand extends Command {
}
};
const scope = Container.keyboard.createScope({
left: { onDidPressKey: goBack },
right: {
onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(input, key)
},
'ctrl+right': {
onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(input, key)
},
'alt+right': {
onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(input, key)
const mapping: KeyMapping = {
left: { onDidPressKey: goBack }
};
if (step.onDidPressKey !== undefined && step.keys !== undefined && step.keys.length !== 0) {
for (const key of step.keys) {
mapping[key] = {
onDidPressKey: key => step.onDidPressKey!(input, key)
};
}
});
}
const scope = Container.keyboard.createScope(mapping);
scope.start();
disposables.push(
@ -234,18 +235,18 @@ export class GitCommandsCommand extends Command {
}
};
const scope = Container.keyboard.createScope({
left: { onDidPressKey: goBack },
right: {
onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(quickpick, key)
},
'ctrl+right': {
onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(quickpick, key)
},
'alt+right': {
onDidPressKey: key => step.onDidPressKey && step.onDidPressKey(quickpick, key)
const mapping: KeyMapping = {
left: { onDidPressKey: goBack }
};
if (step.onDidPressKey !== undefined && step.keys !== undefined && step.keys.length !== 0) {
for (const key of step.keys) {
mapping[key] = {
onDidPressKey: key => step.onDidPressKey!(quickpick, key)
};
}
});
}
const scope = Container.keyboard.createScope(mapping);
scope.start();
let overrideItems = false;

+ 4
- 1
src/commands/quickCommand.ts View File

@ -15,6 +15,7 @@ export class BreakQuickCommand extends Error {
export interface QuickInputStep {
additionalButtons?: QuickInputButton[];
buttons?: QuickInputButton[];
keys?: StepNavigationKeys[];
placeholder?: string;
title?: string;
value?: string;
@ -31,12 +32,13 @@ export function isQuickInputStep(item: QuickPickStep | QuickInputStep): item is
export interface QuickPickStep<T extends QuickPickItem = any> {
additionalButtons?: QuickInputButton[];
buttons?: QuickInputButton[];
selectedItems?: QuickPickItem[];
items: (DirectiveQuickPickItem | T)[] | DirectiveQuickPickItem[];
keys?: StepNavigationKeys[];
matchOnDescription?: boolean;
matchOnDetail?: boolean;
multiselect?: boolean;
placeholder?: string;
selectedItems?: QuickPickItem[];
title?: string;
value?: string;
@ -54,6 +56,7 @@ export function isQuickPickStep(item: QuickPickStep | QuickInputStep): item is Q
export type StepAsyncGenerator = AsyncGenerator<QuickPickStep | QuickInputStep, undefined, any | undefined>;
type StepItemType<T> = T extends QuickPickStep<infer U> ? U[] : T extends QuickInputStep ? string : never;
export type StepNavigationKeys = Exclude<Exclude<Exclude<Keys, 'left'>, 'alt+left'>, 'ctrl+left'>;
export type StepSelection<T> = T extends QuickPickStep<infer U>
? U[] | Directive
: T extends QuickInputStep

+ 2
- 2
src/commands/showQuickCommitDetails.ts View File

@ -2,7 +2,7 @@
import { commands, TextEditor, Uri } from 'vscode';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { GitCommit, GitLog, GitLogCommit, GitUri } from '../git/gitService';
import { GitCommit, GitLog, GitLogCommit, GitUri, SearchPattern } from '../git/gitService';
import { Logger } from '../logger';
import { Messages } from '../messages';
import { CommandQuickPickItem, CommitQuickPick, CommitWithFileStatusQuickPickItem } from '../quickpicks';
@ -122,7 +122,7 @@ export class ShowQuickCommitDetailsCommand extends ActiveEditorCachedCommand {
if (args.showInView) {
void (await Container.searchView.search(
repoPath!,
{ pattern: `commit:${args.commit.sha}` },
{ pattern: SearchPattern.fromCommit(args.commit) },
{
label: { label: `for commit id ${args.commit.shortSha}` }
}

+ 3
- 3
src/commands/showQuickFileHistory.ts View File

@ -8,8 +8,8 @@ import { Messages } from '../messages';
import {
CommandQuickPickItem,
FileHistoryQuickPick,
ShowFileHistoryFromQuickPickItem,
ShowFileHistoryInViewQuickPickItem
OpenFileHistoryInViewQuickPickItem,
ShowFileHistoryFromQuickPickItem
} from '../quickpicks';
import { Iterables, Strings } from '../system';
import { ActiveEditorCachedCommand, command, CommandContext, Commands, getCommandUri } from './common';
@ -148,7 +148,7 @@ export class ShowQuickFileHistoryCommand extends ActiveEditorCachedCommand {
: undefined,
showInViewCommand:
args.log !== undefined
? new ShowFileHistoryInViewQuickPickItem(
? new OpenFileHistoryInViewQuickPickItem(
gitUri,
(args.reference && args.reference.ref) || gitUri.sha
)

+ 7
- 0
src/git/gitService.ts View File

@ -40,6 +40,7 @@ import {
GitBlameParser,
GitBranch,
GitBranchParser,
GitCommit,
GitCommitType,
GitContributor,
GitDiff,
@ -129,6 +130,12 @@ export interface SearchPattern {
}
export namespace SearchPattern {
export function fromCommit(ref: string): string;
export function fromCommit(commit: GitCommit): string;
export function fromCommit(refOrCommit: string | GitCommit) {
return `commit:${GitService.shortenSha(typeof refOrCommit === 'string' ? refOrCommit : refOrCommit.sha)}`;
}
export function toKey(search: SearchPattern) {
return `${search.pattern}|${search.matchAll ? 'A' : ''}${search.matchCase ? 'C' : ''}${
search.matchRegex ? 'R' : ''

+ 3
- 3
src/quickpicks/commitFileQuickPick.ts View File

@ -229,7 +229,7 @@ export class CommitFileQuickPick {
new CommandQuickPickItem(
{
label: '$(clippy) Copy Commit ID to Clipboard',
description: `${commit.shortSha}`
description: ''
},
Commands.CopyShaToClipboard,
[uri, copyShaCommandArgs]
@ -243,8 +243,8 @@ export class CommitFileQuickPick {
items.push(
new CommandQuickPickItem(
{
label: '$(clippy) Copy Commit Message to Clipboard',
description: `${commit.getShortMessage()}`
label: `$(clippy) Copy ${commit.isStash ? 'Stash' : 'Commit'} Message to Clipboard`,
description: ''
},
Commands.CopyMessageToClipboard,
[uri, copyMessageCommandArgs]

+ 56
- 67
src/quickpicks/commitQuickPick.ts View File

@ -31,8 +31,9 @@ import {
CommandQuickPickItem,
getQuickPickIgnoreFocusOut,
KeyCommandQuickPickItem,
OpenCommitInViewQuickPickItem,
QuickPickItem,
ShowCommitInViewQuickPickItem
RevealCommitInViewQuickPickItem
} from './commonQuickPicks';
import { OpenRemotesCommandQuickPickItem } from './remotesQuickPick';
@ -146,6 +147,7 @@ export interface CommitQuickPickOptions {
currentCommand?: CommandQuickPickItem;
goBackCommand?: CommandQuickPickItem;
repoLog?: GitLog;
showChanges?: boolean;
}
export class CommitQuickPick {
@ -156,6 +158,8 @@ export class CommitQuickPick {
uri: Uri,
options: CommitQuickPickOptions = {}
): Promise<CommitWithFileStatusQuickPickItem | CommandQuickPickItem | undefined> {
options = { showChanges: true, ...options };
let previousCommand: (() => Promise<KeyCommandQuickPickItem | typeof KeyNoopCommand>) | undefined = undefined;
let nextCommand: (() => Promise<KeyCommandQuickPickItem | typeof KeyNoopCommand>) | undefined = undefined;
if (!commit.isStash) {
@ -220,7 +224,7 @@ export class CommitQuickPick {
'alt+.': nextCommand
});
const pick = await window.showQuickPick(this.getItems(commit, uri, options), {
const pick = await window.showQuickPick(CommitQuickPick.getItems(commit, uri, options), {
matchOnDescription: true,
matchOnDetail: true,
placeHolder: `${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${
@ -240,29 +244,23 @@ export class CommitQuickPick {
return pick;
}
private async getItems(commit: GitLogCommit, uri: Uri, options: CommitQuickPickOptions = {}) {
const items: (CommitWithFileStatusQuickPickItem | CommandQuickPickItem)[] = commit.files.map(
fs => new CommitWithFileStatusQuickPickItem(commit, fs)
);
const stash = commit.isStash;
let index = 0;
static async getItems(commit: GitLogCommit, uri: Uri, options: CommitQuickPickOptions = {}) {
const items: CommandQuickPickItem[] = [];
let remotes;
if (stash) {
if (GitStashCommit.is(commit)) {
const stashApplyCommmandArgs: StashApplyCommandArgs = {
deleteAfter: false,
stashItem: commit as GitStashCommit,
stashItem: commit,
goBackCommand: options.currentCommand
};
items.splice(
index++,
0,
items.push(
new CommandQuickPickItem(
{
label: '$(git-pull-request) Apply Stash',
description: `${commit.getShortMessage()}`
description: `${
commit.number === undefined ? '' : `${commit.number}: `
}${commit.getShortMessage()}`
},
Commands.StashApply,
[stashApplyCommmandArgs]
@ -270,31 +268,33 @@ export class CommitQuickPick {
);
const stashDeleteCommmandArgs: StashDeleteCommandArgs = {
stashItem: commit as GitStashCommit,
stashItem: commit,
goBackCommand: options.currentCommand
};
items.splice(
index++,
0,
items.push(
new CommandQuickPickItem(
{
label: '$(x) Delete Stash',
description: `${commit.getShortMessage()}`
description: `${
commit.number === undefined ? '' : `${commit.number}: `
}${commit.getShortMessage()}`
},
Commands.StashDelete,
[stashDeleteCommmandArgs]
)
);
items.splice(index++, 0, new ShowCommitInViewQuickPickItem(commit));
items.push(new OpenCommitInViewQuickPickItem(commit));
items.push(new RevealCommitInViewQuickPickItem(commit));
} else {
items.splice(index++, 0, new ShowCommitInViewQuickPickItem(commit));
items.push(new OpenCommitInViewQuickPickItem(commit));
items.push(new RevealCommitInViewQuickPickItem(commit));
items.push(new OpenCommitFilesCommandQuickPickItem(commit));
items.push(new OpenCommitFileRevisionsCommandQuickPickItem(commit));
remotes = await Container.git.getRemotes(commit.repoPath, { sort: true });
if (remotes.length) {
items.splice(
index++,
0,
items.push(
new OpenRemotesCommandQuickPickItem(
remotes,
{
@ -307,18 +307,13 @@ export class CommitQuickPick {
}
}
items.splice(index++, 0, new OpenCommitFilesCommandQuickPickItem(commit));
items.splice(index++, 0, new OpenCommitFileRevisionsCommandQuickPickItem(commit));
const previousSha = await Container.git.resolveReference(commit.repoPath, commit.previousFileSha);
let diffDirectoryCommmandArgs: DiffDirectoryCommandArgs = {
ref1: previousSha,
ref2: commit.sha
};
items.splice(
index++,
0,
items.push(
new CommandQuickPickItem(
{
label: '$(git-compare) Open Directory Compare with Previous Revision',
@ -334,9 +329,7 @@ export class CommitQuickPick {
diffDirectoryCommmandArgs = {
ref1: commit.sha
};
items.splice(
index++,
0,
items.push(
new CommandQuickPickItem(
{
label: '$(git-compare) Open Directory Compare with Working Tree',
@ -347,17 +340,15 @@ export class CommitQuickPick {
)
);
if (!stash) {
if (!GitStashCommit.is(commit)) {
const copyShaCommandArgs: CopyShaToClipboardCommandArgs = {
sha: commit.sha
};
items.splice(
index++,
0,
items.push(
new CommandQuickPickItem(
{
label: '$(clippy) Copy Commit ID to Clipboard',
description: `${commit.shortSha}`
description: ''
},
Commands.CopyShaToClipboard,
[uri, copyShaCommandArgs]
@ -369,27 +360,23 @@ export class CommitQuickPick {
message: commit.message,
sha: commit.sha
};
items.splice(
index++,
0,
items.push(
new CommandQuickPickItem(
{
label: '$(clippy) Copy Commit Message to Clipboard',
description: `${commit.getShortMessage()}`
label: `$(clippy) Copy ${commit.isStash ? 'Stash' : 'Commit'} Message to Clipboard`,
description: ''
},
Commands.CopyMessageToClipboard,
[uri, copyMessageCommandArgs]
)
);
if (!stash) {
if (!GitStashCommit.is(commit)) {
if (remotes !== undefined && remotes.length) {
const copyRemoteUrlCommandArgs: CopyRemoteFileUrlToClipboardCommandArgs = {
sha: commit.sha
};
items.splice(
index++,
0,
items.push(
new CommandQuickPickItem(
{
label: '$(clippy) Copy Remote Url to Clipboard'
@ -401,24 +388,26 @@ export class CommitQuickPick {
}
}
const commitDetailsCommandArgs: ShowQuickCommitDetailsCommandArgs = {
commit: commit,
repoLog: options.repoLog,
sha: commit.sha,
goBackCommand: options.goBackCommand
};
items.splice(
index++,
0,
new CommandQuickPickItem(
{
label: 'Changed Files',
description: commit.getFormattedDiffStatus()
},
Commands.ShowQuickCommitDetails,
[uri, commitDetailsCommandArgs]
)
);
if (options.showChanges) {
const commitDetailsCommandArgs: ShowQuickCommitDetailsCommandArgs = {
commit: commit,
repoLog: options.repoLog,
sha: commit.sha,
goBackCommand: options.goBackCommand
};
items.push(
new CommandQuickPickItem(
{
label: 'Changed Files',
description: commit.getFormattedDiffStatus()
},
Commands.ShowQuickCommitDetails,
[uri, commitDetailsCommandArgs]
)
);
items.push(...commit.files.map(fs => new CommitWithFileStatusQuickPickItem(commit, fs)));
}
if (options.goBackCommand) {
items.splice(0, 0, options.goBackCommand);

+ 49
- 56
src/quickpicks/commonQuickPicks.ts View File

@ -3,9 +3,10 @@ import { CancellationTokenSource, commands, QuickPickItem, window } from 'vscode
import { Commands } from '../commands';
import { configuration } from '../configuration';
import { Container } from '../container';
import { GitLog, GitLogCommit, GitStashCommit, GitUri, SearchPattern } from '../git/gitService';
import { GitLogCommit, GitStashCommit, GitUri, SearchPattern } from '../git/gitService';
import { KeyMapping, Keys } from '../keyboard';
import { ReferencesQuickPick, ReferencesQuickPickItem } from './referencesQuickPick';
import { GlyphChars } from '../constants';
export function getQuickPickIgnoreFocusOut() {
return !configuration.get('advanced', 'quickPick', 'closeOnFocusOut');
@ -97,67 +98,76 @@ export class MessageQuickPickItem extends CommandQuickPickItem {
}
}
export class ShowCommitInViewQuickPickItem extends CommandQuickPickItem {
export class OpenCommitInViewQuickPickItem extends CommandQuickPickItem {
constructor(
public readonly commit: GitLogCommit,
item: QuickPickItem = {
label: '$(eye) Show in View',
description: `shows the ${commit.isStash ? 'stash' : 'commit'} in the Repositories view`
label: '$(eye) Open in Search Commits View',
description: ''
}
) {
super(item, undefined, undefined);
}
async execute(): Promise<{} | undefined> {
if (GitStashCommit.is(this.commit)) {
void (await Container.repositoriesView.revealStash(this.commit, {
select: true,
focus: true,
expand: true
}));
return undefined;
}
void (await Container.searchView.search(
this.commit.repoPath,
{
pattern: SearchPattern.fromCommit(this.commit)
},
{
label: { label: `for ${this.commit.isStash ? 'stash' : 'commit'} id ${this.commit.shortSha}` }
}
));
const node = await Container.repositoriesView.revealCommit(this.commit, {
select: true,
focus: true,
expand: true
});
return undefined;
}
}
if (node === undefined) {
// Fallback to commit search, if we can't find it
void (await Container.searchView.search(
this.commit.repoPath,
{ pattern: `commit:${this.commit.sha}` },
{
label: { label: `for commit id ${this.commit.shortSha}` }
}
));
export class OpenFileHistoryInViewQuickPickItem extends CommandQuickPickItem {
constructor(
public readonly uri: GitUri,
public readonly baseRef: string | undefined,
item: QuickPickItem = {
label: '$(eye) Open in File History View',
description: 'shows the file history in the File History view'
}
) {
super(item, undefined, undefined);
}
return undefined;
async execute(): Promise<{} | undefined> {
return void (await Container.fileHistoryView.showHistoryForUri(this.uri, this.baseRef));
}
}
export class ShowCommitSearchResultsInViewQuickPickItem extends CommandQuickPickItem {
export class RevealCommitInViewQuickPickItem extends CommandQuickPickItem {
constructor(
public readonly search: SearchPattern,
public readonly results: GitLog,
public readonly resultsLabel: string | { label: string; resultsType?: { singular: string; plural: string } },
public readonly commit: GitLogCommit | GitStashCommit,
item: QuickPickItem = {
label: '$(eye) Show in View',
description: 'shows the search results in the Search Commits view'
label: '$(eye) Reveal in Repositories View',
description: `${commit.isStash ? '' : `${GlyphChars.Dash} this can take a while`}`
}
) {
super(item, undefined, undefined);
}
execute(): Promise<{} | undefined> {
Container.searchView.showSearchResults(this.results.repoPath, this.search, this.results, {
label: this.resultsLabel
});
return Promise.resolve(undefined);
async execute(): Promise<{} | undefined> {
if (GitStashCommit.is(this.commit)) {
void (await Container.repositoriesView.revealStash(this.commit, {
select: true,
focus: true,
expand: true
}));
} else {
void (await Container.repositoriesView.revealCommit(this.commit, {
select: true,
focus: true,
expand: true
}));
}
return undefined;
}
}
@ -182,20 +192,3 @@ export class ShowFileHistoryFromQuickPickItem extends CommandQuickPickItem {
});
}
}
export class ShowFileHistoryInViewQuickPickItem extends CommandQuickPickItem {
constructor(
public readonly uri: GitUri,
public readonly baseRef: string | undefined,
item: QuickPickItem = {
label: '$(eye) Show in View',
description: 'shows the file history in the File History view'
}
) {
super(item, undefined, undefined);
}
async execute(): Promise<{} | undefined> {
return void (await Container.fileHistoryView.showHistoryForUri(this.uri, this.baseRef));
}
}

+ 1
- 0
src/views/nodes.ts View File

@ -5,6 +5,7 @@ export * from './nodes/common';
export * from './nodes/viewNode';
export * from './nodes/branchesNode';
export * from './nodes/branchNode';
export * from './nodes/branchOrTagFolderNode';
export * from './nodes/branchTrackingStatusNode';
export * from './nodes/commitFileNode';
export * from './nodes/commitNode';

+ 97
- 50
src/views/repositoriesView.ts View File

@ -1,15 +1,31 @@
'use strict';
import { commands, ConfigurationChangeEvent, Event, EventEmitter } from 'vscode';
import {
CancellationToken,
commands,
ConfigurationChangeEvent,
Event,
EventEmitter,
ProgressLocation,
window
} from 'vscode';
import { configuration, RepositoriesViewConfig, ViewFilesLayout, ViewsConfig } from '../configuration';
import { CommandContext, setCommandContext, WorkspaceState } from '../constants';
import { Container } from '../container';
import { BranchesNode, BranchNode, RepositoriesNode, RepositoryNode, StashesNode, StashNode, ViewNode } from './nodes';
import {
BranchesNode,
BranchNode,
BranchOrTagFolderNode,
CompareBranchNode,
RepositoriesNode,
RepositoryNode,
StashesNode,
StashNode,
ViewNode
} from './nodes';
import { ViewBase } from './viewBase';
import { ViewShowBranchComparison } from '../config';
import { CompareBranchNode } from './nodes/compareBranchNode';
import { GitLogCommit, GitStashCommit } from '../git/git';
const emptyArray = (Object.freeze([]) as any) as any[];
import { GitService } from '../git/gitService';
export class RepositoriesView extends ViewBase<RepositoriesNode> {
constructor() {
@ -115,50 +131,51 @@ export class RepositoriesView extends ViewBase {
return { ...Container.config.views, ...Container.config.views.repositories };
}
findCommitNode(commit: GitLogCommit | { repoPath: string; ref: string }) {
findCommitNode(commit: GitLogCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(commit.repoPath);
return this.findNode((n: any) => n.commit !== undefined && n.commit.ref === commit.ref, {
allowPaging: true,
maxDepth: 2,
getChildren: async n => {
// Only search for commit nodes in the same repo within the root BranchNode
if (n.id != null && n.id.startsWith(`gitlens${RepositoryNode.key}`)) {
if (!n.id.startsWith(repoNodeId)) return emptyArray;
if (n instanceof BranchNode) {
if (!n.root) return emptyArray;
} else if (!(n instanceof RepositoryNode) && !(n instanceof BranchesNode)) {
return emptyArray;
}
maxDepth: 6,
canTraverse: n => {
// Only search for commit nodes in the same repo within BranchNodes
if (n instanceof RepositoriesNode) return true;
if (
n instanceof RepositoryNode ||
n instanceof BranchesNode ||
n instanceof BranchOrTagFolderNode ||
n instanceof BranchNode
) {
return n.id.startsWith(repoNodeId);
}
return n.getChildren();
}
return false;
},
token: token
});
}
findStashNode(stash: GitStashCommit | { repoPath: string; ref: string }) {
findStashNode(stash: GitStashCommit | { repoPath: string; ref: string }, token?: CancellationToken) {
const repoNodeId = RepositoryNode.getId(stash.repoPath);
return this.findNode(StashNode.getId(stash.repoPath, stash.ref), {
maxDepth: 2,
getChildren: async n => {
maxDepth: 3,
canTraverse: n => {
// Only search for stash nodes in the same repo within a StashesNode
if (n.id != null && n.id.startsWith(`gitlens${RepositoryNode.key}`)) {
if (!n.id.startsWith(repoNodeId)) return emptyArray;
if (n instanceof RepositoriesNode) return true;
if (!(n instanceof RepositoryNode) && !(n instanceof StashesNode)) {
return emptyArray;
}
if (n instanceof RepositoryNode || n instanceof StashesNode) {
return n.id.startsWith(repoNodeId);
}
return n.getChildren();
}
return false;
},
token: token
});
}
async revealCommit(
revealCommit(
commit: GitLogCommit | { repoPath: string; ref: string },
options?: {
select?: boolean;
@ -166,28 +183,60 @@ export class RepositoriesView extends ViewBase {
expand?: boolean | number;
}
) {
const node = await this.findCommitNode(commit);
if (node !== undefined) {
await this.reveal(node, options);
}
return window.withProgress(
{
location: ProgressLocation.Notification,
title: `Revealing commit '${GitService.shortenSha(commit.ref)}' in the Repositories view...`,
cancellable: true
},
async (progress, token) => {
const node = await this.findCommitNode(commit, token);
if (node === undefined) return node;
// Not sure why I need to reveal each parent, but without it the node won't be revealed
const nodes: ViewNode[] = [];
let parent: ViewNode | undefined = node;
while (parent !== undefined) {
nodes.push(parent);
parent = parent.getParent();
}
nodes.pop();
return node;
for (const n of nodes.reverse()) {
try {
await this.reveal(n, options);
} catch {}
}
return node;
}
);
}
async revealStash(
stash: GitStashCommit | { repoPath: string; ref: string },
stash: GitStashCommit | { repoPath: string; ref: string; stashName: string },
options?: {
select?: boolean;
focus?: boolean;
expand?: boolean | number;
}
) {
const node = await this.findStashNode(stash);
if (node !== undefined) {
await this.reveal(node, options);
}
return window.withProgress(
{
location: ProgressLocation.Notification,
title: `Revealing stash '${stash.stashName}' in the Repositories view...`,
cancellable: true
},
async (progress, token) => {
const node = await this.findStashNode(stash, token);
if (node !== undefined) {
await this.reveal(node, options);
}
return node;
return node;
}
);
}
async revealStashes(
@ -202,17 +251,15 @@ export class RepositoriesView extends ViewBase {
const node = await this.findNode(StashesNode.getId(repoPath), {
maxDepth: 2,
getChildren: async n => {
// Only search for nodes in the same repo
if (n.id != null && n.id.startsWith(`gitlens${RepositoryNode.key}`)) {
if (!n.id.startsWith(repoNodeId)) return emptyArray;
if (!(n instanceof RepositoryNode)) {
return emptyArray;
}
canTraverse: n => {
// Only search for stashes nodes in the same repo
if (n instanceof RepositoriesNode) return true;
if (n instanceof RepositoryNode) {
return n.id.startsWith(repoNodeId);
}
return n.getChildren();
return false;
}
});

+ 114
- 46
src/views/viewBase.ts View File

@ -1,5 +1,6 @@
'use strict';
import {
CancellationToken,
commands,
ConfigurationChangeEvent,
ConfigurationTarget,
@ -157,16 +158,18 @@ export abstract class ViewBase> implements TreeData
id: string,
options?: {
allowPaging?: boolean;
getChildren?: (node: ViewNode) => ViewNode[] | Promise<ViewNode[]>;
canTraverse?: (node: ViewNode) => boolean;
maxDepth?: number;
token?: CancellationToken;
}
): Promise<ViewNode | undefined>;
async findNode(
predicate: (node: ViewNode) => boolean,
options?: {
allowPaging?: boolean;
getChildren?: (node: ViewNode) => ViewNode[] | Promise<ViewNode[]>;
canTraverse?: (node: ViewNode) => boolean;
maxDepth?: number;
token?: CancellationToken;
}
): Promise<ViewNode | undefined>;
@log({
@ -179,85 +182,150 @@ export abstract class ViewBase> implements TreeData
predicate: string | ((node: ViewNode) => boolean),
{
allowPaging = false,
getChildren,
maxDepth = 2
canTraverse,
maxDepth = 2,
token
}: {
allowPaging?: boolean;
getChildren?: (node: ViewNode) => ViewNode[] | Promise<ViewNode[]>;
canTraverse?: (node: ViewNode) => boolean;
maxDepth?: number;
token?: CancellationToken;
} = {}
): Promise<ViewNode | undefined> {
if (getChildren === undefined) {
getChildren = n => n.getChildren();
}
// If we have no root (e.g. never been initialized) force it so the tree will load properly
if (this._root === undefined) {
await this.show();
}
return this.findNodeCore(
// const node = await this.findNodeCoreDFS(
// typeof predicate === 'string' ? n => n.id === predicate : predicate,
// await this.ensureRoot().getChildren(),
// allowPaging,
// canTraverse,
// maxDepth
// );
const node = await this.findNodeCoreBFS(
typeof predicate === 'string' ? n => n.id === predicate : predicate,
await getChildren(this.ensureRoot()),
this.ensureRoot(),
allowPaging,
getChildren,
maxDepth
canTraverse,
maxDepth,
token
);
return node;
}
private async findNodeCore(
// private async findNodeCoreDFS(
// predicate: (node: ViewNode) => boolean,
// nodes: ViewNode[],
// allowPaging: boolean,
// canTraverse: ((node: ViewNode) => boolean) | undefined,
// depth: number
// ): Promise<ViewNode | undefined> {
// if (depth === 0) return undefined;
// const defaultPageSize = Container.config.advanced.maxListItems;
// let child;
// let children;
// for (const node of nodes) {
// if (canTraverse !== undefined && !canTraverse(node)) continue;
// children = await node.getChildren();
// if (children.length === 0) continue;
// child = children.find(predicate);
// if (child !== undefined) return child;
// if (PageableViewNode.is(node)) {
// if (node.maxCount !== 0 && allowPaging) {
// let pageSize = defaultPageSize === 0 ? 0 : (node.maxCount || 0) + defaultPageSize;
// while (true) {
// await this.showMoreNodeChildren(node, pageSize);
// child = (await node.getChildren()).find(predicate);
// if (child !== undefined) return child;
// if (pageSize === 0) break;
// pageSize = 0;
// }
// }
// // Don't traverse into paged children
// continue;
// }
// return this.findNodeCoreDFS(predicate, children, allowPaging, canTraverse, depth - 1);
// }
// return undefined;
// }
private async findNodeCoreBFS(
predicate: (node: ViewNode) => boolean,
nodes: ViewNode[],
root: ViewNode,
allowPaging: boolean,
getChildren: (node: ViewNode) => ViewNode[] | Promise<ViewNode[]>,
depth: number
canTraverse: ((node: ViewNode) => boolean) | undefined,
maxDepth: number,
token: CancellationToken | undefined
): Promise<ViewNode | undefined> {
const decendents: ViewNode[] = [];
const queue: (ViewNode | undefined)[] = [root, undefined];
const defaultPageSize = Container.config.advanced.maxListItems;
let child;
let children;
for (const node of nodes) {
children = await getChildren(node);
if (children.length === 0) continue;
let depth = 0;
let node: ViewNode | undefined;
let children: ViewNode[];
while (queue.length > 1) {
if (token !== undefined && token.isCancellationRequested) return undefined;
child = children.find(predicate);
if (child !== undefined) return child;
node = queue.shift();
if (node === undefined) {
depth++;
if (PageableViewNode.is(node)) {
// Don't descend into paged children
if (!allowPaging || node.maxCount === 0) continue;
queue.push(undefined);
if (depth > maxDepth) break;
if (defaultPageSize !== 0 && (node.maxCount === undefined || node.maxCount < defaultPageSize)) {
await this.showMoreNodeChildren(node, defaultPageSize);
continue;
}
children = await getChildren(node);
if (children.length === 0) continue;
if (predicate(node)) return node;
if (canTraverse !== undefined && !canTraverse(node)) continue;
child = children.find(predicate);
if (child !== undefined) return child;
}
children = await node.getChildren();
if (children.length === 0) continue;
await this.showMoreNodeChildren(node, 0);
if (PageableViewNode.is(node)) {
let child = children.find(predicate);
if (child !== undefined) return child;
children = await getChildren(node);
if (children.length === 0) continue;
if (node.maxCount !== 0 && allowPaging) {
let pageSize = defaultPageSize === 0 ? 0 : (node.maxCount || 0) + defaultPageSize;
while (true) {
if (token !== undefined && token.isCancellationRequested) return undefined;
child = children.find(predicate);
if (child !== undefined) return child;
await this.showMoreNodeChildren(node, pageSize);
child = (await node.getChildren()).find(predicate);
if (child !== undefined) return child;
if (pageSize === 0) break;
// Don't descend into paged children
pageSize = 0;
}
}
// Don't traverse into paged children
continue;
}
decendents.push(...children);
queue.push(...children);
}
depth--;
if (depth === 0) return undefined;
return this.findNodeCore(predicate, decendents, allowPaging, getChildren, depth);
return undefined;
}
@debug()

Loading…
Cancel
Save