'use strict'; import * as path from 'path'; import { commands, QuickPickOptions, TextDocumentShowOptions, Uri, window } from 'vscode'; import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffDirectoryCommandArgs, DiffWithPreviousCommandArgs, ShowQuickCommitDetailsCommandArgs, StashApplyCommandArgs, StashDeleteCommandArgs } from '../commands'; import { GlyphChars } from '../constants'; import { Container } from '../container'; import { GitFile, GitFileStatus, GitLog, GitLogCommit, GitStashCommit, GitUri, RemoteResource } from '../git/gitService'; import { KeyCommand, KeyNoopCommand, Keys } from '../keyboard'; import { Arrays, Iterables, Strings } from '../system'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem, OpenFilesCommandQuickPickItem, QuickPickItem, ShowCommitInResultsQuickPickItem } from './commonQuickPicks'; import { OpenRemotesCommandQuickPickItem } from './remotesQuickPick'; export class CommitWithFileStatusQuickPickItem extends OpenFileCommandQuickPickItem { readonly status: GitFileStatus; readonly commit: GitLogCommit; constructor(commit: GitLogCommit, file: GitFile) { const octicon = GitFile.getStatusOcticon(file.status); const description = GitFile.getFormattedDirectory(file, true); super(GitUri.toRevisionUri(commit.sha, file, commit.repoPath), { label: `${Strings.pad(octicon, 4, 2)} ${path.basename(file.fileName)}`, description: description }); this.commit = commit.toFileCommit(file); this.status = file.status; } get sha(): string { return this.commit.sha; } onDidPressKey(key: Keys): Promise<{} | undefined> { if (this.commit.previousSha === undefined) return super.onDidPressKey(key); return commands.executeCommand(Commands.DiffWithPrevious, this.commit.toGitUri(), { commit: this.commit, showOptions: { preserveFocus: true, preview: false } as TextDocumentShowOptions } as DiffWithPreviousCommandArgs) as Promise<{} | undefined>; } } export class OpenCommitFilesCommandQuickPickItem extends OpenFilesCommandQuickPickItem { constructor(commit: GitLogCommit, versioned: boolean = false, item?: QuickPickItem) { const repoPath = commit.repoPath; const uris = Arrays.filterMap(commit.files, f => GitUri.fromFile(f, repoPath)); super( uris, item || { label: `$(file-symlink-file) Open Files`, description: '' // detail: `Opens all of the changed file in the working tree` } ); } } export class OpenCommitFileRevisionsCommandQuickPickItem extends OpenFilesCommandQuickPickItem { constructor(commit: GitLogCommit, item?: QuickPickItem) { const uris = Arrays.filterMap(commit.files, f => GitUri.toRevisionUri(f.status === 'D' ? commit.previousFileSha : commit.sha, f, commit.repoPath) ); super( uris, item || { label: `$(file-symlink-file) Open Revisions`, description: `${Strings.pad(GlyphChars.Dash, 2, 3)} in ${GlyphChars.Space}$(git-commit) ${ commit.shortSha }` // detail: `Opens all of the changed files in $(git-commit) ${commit.shortSha}` } ); } } export class CommitQuickPick { static async show( commit: GitLogCommit, uri: Uri, goBackCommand?: CommandQuickPickItem, currentCommand?: CommandQuickPickItem, repoLog?: GitLog ): Promise { await commit.resolvePreviousFileSha(); const items: (CommitWithFileStatusQuickPickItem | CommandQuickPickItem)[] = commit.files.map( fs => new CommitWithFileStatusQuickPickItem(commit, fs) ); const stash = commit.isStash; let index = 0; if (stash) { items.splice( index++, 0, new CommandQuickPickItem( { label: `$(git-pull-request) Apply Stashed Changes`, description: `${Strings.pad(GlyphChars.Dash, 2, 3)} ${commit.getShortMessage()}` }, Commands.StashApply, [ { confirm: true, deleteAfter: false, stashItem: commit as GitStashCommit, goBackCommand: currentCommand } as StashApplyCommandArgs ] ) ); items.splice( index++, 0, new CommandQuickPickItem( { label: `$(x) Delete Stashed Changes`, description: `${Strings.pad(GlyphChars.Dash, 2, 3)} ${commit.getShortMessage()}` }, Commands.StashDelete, [ { confirm: true, stashItem: commit as GitStashCommit, goBackCommand: currentCommand } as StashDeleteCommandArgs ] ) ); items.splice(index++, 0, new ShowCommitInResultsQuickPickItem(commit)); } else { items.splice(index++, 0, new ShowCommitInResultsQuickPickItem(commit)); const remotes = await Container.git.getRemotes(commit.repoPath); if (remotes.length) { items.splice( index++, 0, new OpenRemotesCommandQuickPickItem( remotes, { type: 'commit', sha: commit.sha } as RemoteResource, currentCommand ) ); } } items.splice(index++, 0, new OpenCommitFilesCommandQuickPickItem(commit)); items.splice(index++, 0, new OpenCommitFileRevisionsCommandQuickPickItem(commit)); items.splice( index++, 0, new CommandQuickPickItem( { label: `$(git-compare) Open Directory Compare with Previous Revision`, description: `${Strings.pad(GlyphChars.Dash, 2, 3)} $(git-commit) ${commit.previousFileShortSha} ${ GlyphChars.Space } $(git-compare) ${GlyphChars.Space} $(git-commit) ${commit.shortSha}` }, Commands.DiffDirectory, [ commit.uri, { ref1: commit.previousFileSha, ref2: commit.sha } as DiffDirectoryCommandArgs ] ) ); items.splice( index++, 0, new CommandQuickPickItem( { label: `$(git-compare) Open Directory Compare with Working Tree`, description: `${Strings.pad(GlyphChars.Dash, 2, 3)} $(git-commit) ${commit.shortSha} ${ GlyphChars.Space } $(git-compare) ${GlyphChars.Space} $(file-directory) Working Tree` }, Commands.DiffDirectory, [ uri, { ref1: commit.sha } as DiffDirectoryCommandArgs ] ) ); if (!stash) { items.splice( index++, 0, new CommandQuickPickItem( { label: `$(clippy) Copy Commit ID to Clipboard`, description: `${Strings.pad(GlyphChars.Dash, 2, 3)} ${commit.shortSha}` }, Commands.CopyShaToClipboard, [ uri, { sha: commit.sha } as CopyShaToClipboardCommandArgs ] ) ); } items.splice( index++, 0, new CommandQuickPickItem( { label: `$(clippy) Copy Commit Message to Clipboard`, description: `${Strings.pad(GlyphChars.Dash, 2, 3)} ${commit.getShortMessage()}` }, Commands.CopyMessageToClipboard, [ uri, { message: commit.message, sha: commit.sha } as CopyMessageToClipboardCommandArgs ] ) ); items.splice( index++, 0, new CommandQuickPickItem( { label: `Changed Files`, description: commit.getFormattedDiffStatus() }, Commands.ShowQuickCommitDetails, [ uri, { commit, repoLog, sha: commit.sha, goBackCommand } as ShowQuickCommitDetailsCommandArgs ] ) ); if (goBackCommand) { items.splice(0, 0, goBackCommand); } let previousCommand: KeyCommand | (() => Promise) | undefined = undefined; let nextCommand: KeyCommand | (() => Promise) | undefined = undefined; if (!stash) { // If we have the full history, we are good if (repoLog !== undefined && !repoLog.truncated && repoLog.sha === undefined) { previousCommand = commit.previousSha === undefined ? undefined : new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [ commit.previousUri, { repoLog, sha: commit.previousSha, goBackCommand } as ShowQuickCommitDetailsCommandArgs ]); nextCommand = commit.nextSha === undefined ? undefined : new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [ commit.nextUri, { repoLog, sha: commit.nextSha, goBackCommand } as ShowQuickCommitDetailsCommandArgs ]); } else { previousCommand = async () => { let log = repoLog; let c = log && log.commits.get(commit.sha); // If we can't find the commit or the previous commit isn't available (since it isn't trustworthy) if (c === undefined || c.previousSha === undefined) { log = await Container.git.getLog(commit.repoPath, { maxCount: Container.config.advanced.maxListItems, ref: commit.sha }); c = log && log.commits.get(commit.sha); if (c) { // Copy over next info, since it is trustworthy at this point c.nextSha = commit.nextSha; } } if (c === undefined || c.previousSha === undefined) return KeyNoopCommand; return new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [ c.previousUri, { repoLog: log, sha: c.previousSha, goBackCommand } as ShowQuickCommitDetailsCommandArgs ]); }; nextCommand = async () => { let log = repoLog; let c = log && log.commits.get(commit.sha); // If we can't find the commit or the next commit isn't available (since it isn't trustworthy) if (c === undefined || c.nextSha === undefined) { log = undefined; c = undefined; // Try to find the next commit const nextLog = await Container.git.getLog(commit.repoPath, { maxCount: 1, reverse: true, ref: commit.sha }); const next = nextLog && Iterables.first(nextLog.commits.values()); if (next !== undefined && next.sha !== commit.sha) { c = commit; c.nextSha = next.sha; } } if (c === undefined || c.nextSha === undefined) return KeyNoopCommand; return new KeyCommandQuickPickItem(Commands.ShowQuickCommitDetails, [ c.nextUri, { repoLog: log, sha: c.nextSha, goBackCommand } as ShowQuickCommitDetailsCommandArgs ]); }; } } const scope = await Container.keyboard.beginScope({ left: goBackCommand, ',': previousCommand, '.': nextCommand }); const pick = await window.showQuickPick(items, { matchOnDescription: true, matchOnDetail: true, placeHolder: `${commit.shortSha} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${ commit.author ? `${commit.author}, ` : '' }${commit.formattedDate} ${Strings.pad(GlyphChars.Dot, 1, 1)} ${commit.getShortMessage()}`, ignoreFocusOut: getQuickPickIgnoreFocusOut(), onDidSelectItem: (item: QuickPickItem) => { void scope.setKeyCommand('right', item); if (typeof item.onDidSelect === 'function') { item.onDidSelect(); } } } as QuickPickOptions); await scope.dispose(); return pick; } }