diff --git a/package.json b/package.json index 859ea51..ff8b17c 100644 --- a/package.json +++ b/package.json @@ -418,6 +418,11 @@ "title": "Open Blame History Explorer", "category": "GitLens" }, + { + "command": "gitlens.showCommitSearch", + "title": "Search Commits", + "category": "GitLens" + }, { "command": "gitlens.showFileHistory", "title": "Open File History Explorer", @@ -797,6 +802,12 @@ "mac": "alt+-", "when": "gitlens:enabled" }, + { + "command": "gitlens.showCommitSearch", + "key": "alt+f", + "mac": "alt+f", + "when": "gitlens:enabled" + }, { "command": "gitlens.showQuickFileHistory", "key": "alt+h", diff --git a/src/commands.ts b/src/commands.ts index 8536d91..03a6e74 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -23,6 +23,7 @@ export * from './commands/showFileHistory'; export * from './commands/showLastQuickPick'; export * from './commands/showQuickCommitDetails'; export * from './commands/showQuickCommitFileDetails'; +export * from './commands/showCommitSearch'; export * from './commands/showQuickFileHistory'; export * from './commands/showQuickBranchHistory'; export * from './commands/showQuickCurrentBranchHistory'; diff --git a/src/commands/common.ts b/src/commands/common.ts index 52a7cee..fe3e5d9 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -6,7 +6,7 @@ import { Telemetry } from '../telemetry'; export type Commands = 'gitlens.closeUnchangedFiles' | 'gitlens.copyMessageToClipboard' | 'gitlens.copyShaToClipboard' | 'gitlens.diffDirectory' | 'gitlens.diffWithBranch' | 'gitlens.diffWithNext' | 'gitlens.diffWithPrevious' | 'gitlens.diffLineWithPrevious' | 'gitlens.diffWithWorking' | 'gitlens.diffLineWithWorking' | 'gitlens.openChangedFiles' | 'gitlens.openCommitInRemote' | 'gitlens.openFileInRemote' | 'gitlens.openInRemote' | - 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showFileHistory' | + 'gitlens.showBlame' | 'gitlens.showBlameHistory' | 'gitlens.showCommitSearch' | 'gitlens.showFileHistory' | 'gitlens.showLastQuickPick' | 'gitlens.showQuickBranchHistory' | 'gitlens.showQuickCommitDetails' | 'gitlens.showQuickCommitFileDetails' | 'gitlens.showQuickFileHistory' | 'gitlens.showQuickRepoHistory' | @@ -30,6 +30,7 @@ export const Commands = { OpenInRemote: 'gitlens.openInRemote' as Commands, ShowBlame: 'gitlens.showBlame' as Commands, ShowBlameHistory: 'gitlens.showBlameHistory' as Commands, + ShowCommitSearch: 'gitlens.showCommitSearch' as Commands, ShowFileHistory: 'gitlens.showFileHistory' as Commands, ShowLastQuickPick: 'gitlens.showLastQuickPick' as Commands, ShowQuickCommitDetails: 'gitlens.showQuickCommitDetails' as Commands, diff --git a/src/commands/showCommitSearch.ts b/src/commands/showCommitSearch.ts new file mode 100644 index 0000000..3645dac --- /dev/null +++ b/src/commands/showCommitSearch.ts @@ -0,0 +1,100 @@ +'use strict'; +import { commands, InputBoxOptions, TextEditor, Uri, window } from 'vscode'; +import { ActiveEditorCachedCommand, Commands } from './common'; +import { Git, GitService, GitUri } from '../gitService'; +import { Logger } from '../logger'; +import { CommandQuickPickItem, CommitsQuickPick } from '../quickPicks'; + +export class ShowCommitSearchCommand extends ActiveEditorCachedCommand { + + constructor(private git: GitService) { + super(Commands.ShowCommitSearch); + } + + async execute(editor: TextEditor, uri?: Uri, search?: string, searchBy?: undefined | 'author' | 'files' | 'message' | 'sha', goBackCommand?: CommandQuickPickItem) { + if (!(uri instanceof Uri)) { + if (!editor || !editor.document) return undefined; + uri = editor.document.uri; + } + + const gitUri = await GitUri.fromUri(uri, this.git); + + if (!search || searchBy == null) { + search = await window.showInputBox({ + value: search, + prompt: `Please enter a search string`, + placeHolder: `search by message, author (use a:), files (use f:), or sha (use s:)` + } as InputBoxOptions); + if (!search) return undefined; + + if (Git.isSha(search)) { + searchBy = 'sha'; + } + else if (search.startsWith('a:')) { + searchBy = 'author'; + search = search.substring((search[2] === ' ') ? 3 : 2); + } + else if (search.startsWith('f:')) { + searchBy = 'files'; + search = search.substring((search[2] === ' ') ? 3 : 2); + } + else if (search.startsWith('s:')) { + searchBy = 'sha'; + search = search.substring((search[2] === ' ') ? 3 : 2); + } + else { + searchBy = 'message'; + } + } + + try { + const log = await this.git.getLogForRepoSearch(gitUri.repoPath, search, searchBy); + + let originalSearch: string; + let placeHolder: string; + switch (searchBy) { + case 'author': + originalSearch = `a:${search}`; + placeHolder = `commits with author matching '${search}'`; + break; + case 'files': + originalSearch = `f:${search}`; + placeHolder = `commits with files matching '${search}'`; + break; + case 'message': + originalSearch = search; + placeHolder = `commits with message matching '${search}'`; + break; + case 'sha': + originalSearch = `s:${search}`; + placeHolder = `commits with sha matching '${search}'`; + break; + } + + if (!goBackCommand) { + // Create a command to get back to the branch history + goBackCommand = new CommandQuickPickItem({ + label: `go back \u21A9`, + description: `\u00a0 \u2014 \u00a0\u00a0 to commit search` + }, Commands.ShowCommitSearch, [gitUri, originalSearch]); + } + + const pick = await CommitsQuickPick.show(this.git, log, placeHolder, goBackCommand); + if (!pick) return undefined; + + if (pick instanceof CommandQuickPickItem) { + return pick.execute(); + } + + return commands.executeCommand(Commands.ShowQuickCommitDetails, new GitUri(pick.commit.uri, pick.commit), pick.commit.sha, pick.commit, + new CommandQuickPickItem({ + label: `go back \u21A9`, + description: `\u00a0 \u2014 \u00a0\u00a0 to search for ${placeHolder}` + }, Commands.ShowCommitSearch, [gitUri, search, searchBy, goBackCommand])); + } + catch (ex) { + Logger.error(ex, 'ShowCommitSearchCommand'); + return window.showErrorMessage(`Unable to find commits. See output channel for more details`); + } + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 6f749ec..bfe595d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,7 +11,9 @@ import { CopyMessageToClipboardCommand, CopyShaToClipboardCommand } from './comm import { DiffDirectoryCommand, DiffLineWithPreviousCommand, DiffLineWithWorkingCommand, DiffWithBranchCommand, DiffWithNextCommand, DiffWithPreviousCommand, DiffWithWorkingCommand} from './commands'; import { ShowBlameCommand, ToggleBlameCommand } from './commands'; import { ShowBlameHistoryCommand, ShowFileHistoryCommand } from './commands'; -import { ShowLastQuickPickCommand, ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand, ShowQuickFileHistoryCommand } from './commands'; +import { ShowLastQuickPickCommand } from './commands'; +import { ShowQuickBranchHistoryCommand, ShowQuickCurrentBranchHistoryCommand, ShowQuickFileHistoryCommand } from './commands'; +import { ShowQuickCommitDetailsCommand, ShowQuickCommitFileDetailsCommand, ShowCommitSearchCommand } from './commands'; import { ShowQuickRepoStatusCommand, ShowQuickStashListCommand } from './commands'; import { StashApplyCommand, StashDeleteCommand, StashSaveCommand } from './commands'; import { ToggleCodeLensCommand } from './commands'; @@ -106,6 +108,7 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(new ShowQuickCurrentBranchHistoryCommand(git)); context.subscriptions.push(new ShowQuickCommitDetailsCommand(git)); context.subscriptions.push(new ShowQuickCommitFileDetailsCommand(git)); + context.subscriptions.push(new ShowCommitSearchCommand(git)); context.subscriptions.push(new ShowQuickFileHistoryCommand(git)); context.subscriptions.push(new ShowQuickRepoStatusCommand(git)); context.subscriptions.push(new ShowQuickStashListCommand(git)); diff --git a/src/git/git.ts b/src/git/git.ts index 90af54c..1bafd06 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -235,6 +235,15 @@ export class Git { return gitCommand(root, ...params); } + static log_search(repoPath: string, search?: string[], maxCount?: number) { + const params = [...defaultLogParams, `-m`, `-i`]; + if (maxCount) { + params.push(`-n${maxCount}`); + } + + return gitCommand(repoPath, ...params, ...search); + } + static async ls_files(repoPath: string, fileName: string): Promise { try { return await gitCommand(repoPath, 'ls-files', fileName); diff --git a/src/gitService.ts b/src/gitService.ts index 1b81046..9ed80d6 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -562,6 +562,39 @@ export class GitService extends Disposable { } } + async getLogForRepoSearch(repoPath: string, search: string, searchBy: 'author' | 'files' | 'message' | 'sha', maxCount?: number): Promise { + Logger.log(`getLogForRepoSearch('${repoPath}', ${search}, ${searchBy}, ${maxCount})`); + + if (maxCount == null) { + maxCount = this.config.advanced.maxQuickHistory || 0; + } + + let searchArgs: string[]; + switch (searchBy) { + case 'author': + searchArgs = [`'--author='${search}`]; + break; + case 'files': + searchArgs = [`--`, `${search}`]; + break; + case 'message': + searchArgs = [`--grep=${search}`]; + break; + case 'sha': + searchArgs = [search]; + maxCount = 1; + break; + } + + try { + const data = await Git.log_search(repoPath, searchArgs, maxCount); + return GitLogParser.parse(data, 'branch', repoPath, undefined, undefined, maxCount, false, undefined); + } + catch (ex) { + return undefined; + } + } + getLogForFile(repoPath: string, fileName: string, sha?: string, maxCount?: number, range?: Range, reverse: boolean = false): Promise { Logger.log(`getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, ${range && `[${range.start.line}, ${range.end.line}]`}, ${reverse})`); diff --git a/src/quickPicks.ts b/src/quickPicks.ts index 255356f..33b0ae5 100644 --- a/src/quickPicks.ts +++ b/src/quickPicks.ts @@ -4,6 +4,7 @@ export * from './quickPicks/common'; export * from './quickPicks/branches'; export * from './quickPicks/commitDetails'; export * from './quickPicks/commitFileDetails'; +export * from './quickPicks/commits'; export * from './quickPicks/branchHistory'; export * from './quickPicks/fileHistory'; export * from './quickPicks/remotes'; diff --git a/src/quickPicks/commits.ts b/src/quickPicks/commits.ts new file mode 100644 index 0000000..02a280b --- /dev/null +++ b/src/quickPicks/commits.ts @@ -0,0 +1,32 @@ +'use strict'; +import { Iterables } from '../system'; +import { QuickPickOptions, window } from 'vscode'; +import { Keyboard } from '../commands'; +import { GitService, IGitLog } from '../gitService'; +import { CommandQuickPickItem, CommitQuickPickItem, getQuickPickIgnoreFocusOut } from '../quickPicks'; + +export class CommitsQuickPick { + + static async show(git: GitService, log: IGitLog, placeHolder: string, goBackCommand?: CommandQuickPickItem): Promise { + const items = ((log && Array.from(Iterables.map(log.commits.values(), c => new CommitQuickPickItem(c)))) || []) as (CommitQuickPickItem | CommandQuickPickItem)[]; + + if (goBackCommand) { + items.splice(0, 0, goBackCommand); + } + + const scope = await Keyboard.instance.beginScope({ left: goBackCommand }); + + const pick = await window.showQuickPick(items, { + matchOnDescription: true, + placeHolder: placeHolder, + ignoreFocusOut: getQuickPickIgnoreFocusOut() + // onDidSelectItem: (item: QuickPickItem) => { + // scope.setKeyCommand('right', item); + // } + } as QuickPickOptions); + + await scope.dispose(); + + return pick; + } +} \ No newline at end of file