From cdfcf5c76bdbd305a01de493bc72f1fabee0d70f Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sun, 15 Sep 2019 23:05:08 -0400 Subject: [PATCH] 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 --- src/commands/git/search.ts | 116 ++++++++++++++---------- src/commands/git/stash.ts | 74 +++++++++------ src/commands/gitCommands.ts | 45 +++++----- src/commands/quickCommand.ts | 5 +- src/commands/showQuickCommitDetails.ts | 4 +- src/commands/showQuickFileHistory.ts | 6 +- src/git/gitService.ts | 7 ++ src/quickpicks/commitFileQuickPick.ts | 6 +- src/quickpicks/commitQuickPick.ts | 123 ++++++++++++------------- src/quickpicks/commonQuickPicks.ts | 105 ++++++++++------------ src/views/nodes.ts | 1 + src/views/repositoriesView.ts | 147 +++++++++++++++++++----------- src/views/viewBase.ts | 160 +++++++++++++++++++++++---------- 13 files changed, 477 insertions(+), 322 deletions(-) diff --git a/src/commands/git/search.ts b/src/commands/git/search.ts index cc548d0..a0a7a72 100644 --- a/src/commands/git/search.ts +++ b/src/commands/git/search.ts @@ -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 { 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({ - 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[] = [ { 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 = 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({ + 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 = 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); diff --git a/src/commands/git/stash.ts b/src/commands/git/stash.ts index 79a07d3..39521a9 100644 --- a/src/commands/git/stash.ts +++ b/src/commands/git/stash.ts @@ -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 = 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({ + 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 = 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; diff --git a/src/commands/gitCommands.ts b/src/commands/gitCommands.ts index fedf968..46b89a8 100644 --- a/src/commands/gitCommands.ts +++ b/src/commands/gitCommands.ts @@ -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; diff --git a/src/commands/quickCommand.ts b/src/commands/quickCommand.ts index 2a921c7..a8120c5 100644 --- a/src/commands/quickCommand.ts +++ b/src/commands/quickCommand.ts @@ -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 { 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; type StepItemType = T extends QuickPickStep ? U[] : T extends QuickInputStep ? string : never; +export type StepNavigationKeys = Exclude, 'alt+left'>, 'ctrl+left'>; export type StepSelection = T extends QuickPickStep ? U[] | Directive : T extends QuickInputStep diff --git a/src/commands/showQuickCommitDetails.ts b/src/commands/showQuickCommitDetails.ts index 410f638..ca5a4c1 100644 --- a/src/commands/showQuickCommitDetails.ts +++ b/src/commands/showQuickCommitDetails.ts @@ -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}` } } diff --git a/src/commands/showQuickFileHistory.ts b/src/commands/showQuickFileHistory.ts index 2f5e323..e5eabf2 100644 --- a/src/commands/showQuickFileHistory.ts +++ b/src/commands/showQuickFileHistory.ts @@ -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 ) diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 209d302..5e0f6c6 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -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' : '' diff --git a/src/quickpicks/commitFileQuickPick.ts b/src/quickpicks/commitFileQuickPick.ts index e4f2bea..8bba8bb 100644 --- a/src/quickpicks/commitFileQuickPick.ts +++ b/src/quickpicks/commitFileQuickPick.ts @@ -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] diff --git a/src/quickpicks/commitQuickPick.ts b/src/quickpicks/commitQuickPick.ts index 003f3a9..2dbd770 100644 --- a/src/quickpicks/commitQuickPick.ts +++ b/src/quickpicks/commitQuickPick.ts @@ -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 { + options = { showChanges: true, ...options }; + let previousCommand: (() => Promise) | undefined = undefined; let nextCommand: (() => Promise) | 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); diff --git a/src/quickpicks/commonQuickPicks.ts b/src/quickpicks/commonQuickPicks.ts index 8e803e1..0d184e0 100644 --- a/src/quickpicks/commonQuickPicks.ts +++ b/src/quickpicks/commonQuickPicks.ts @@ -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)); - } -} diff --git a/src/views/nodes.ts b/src/views/nodes.ts index b246bc4..0882bbc 100644 --- a/src/views/nodes.ts +++ b/src/views/nodes.ts @@ -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'; diff --git a/src/views/repositoriesView.ts b/src/views/repositoriesView.ts index ff19bec..d5df4c4 100644 --- a/src/views/repositoriesView.ts +++ b/src/views/repositoriesView.ts @@ -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 { 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; } }); diff --git a/src/views/viewBase.ts b/src/views/viewBase.ts index 7376a4a..85cfc82 100644 --- a/src/views/viewBase.ts +++ b/src/views/viewBase.ts @@ -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; + canTraverse?: (node: ViewNode) => boolean; maxDepth?: number; + token?: CancellationToken; } ): Promise; async findNode( predicate: (node: ViewNode) => boolean, options?: { allowPaging?: boolean; - getChildren?: (node: ViewNode) => ViewNode[] | Promise; + canTraverse?: (node: ViewNode) => boolean; maxDepth?: number; + token?: CancellationToken; } ): Promise; @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; + canTraverse?: (node: ViewNode) => boolean; maxDepth?: number; + token?: CancellationToken; } = {} ): Promise { - 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 { + // 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, - depth: number + canTraverse: ((node: ViewNode) => boolean) | undefined, + maxDepth: number, + token: CancellationToken | undefined ): Promise { - 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()