diff --git a/src/commands/git/search.ts b/src/commands/git/search.ts index 8a5e30d..d8db71e 100644 --- a/src/commands/git/search.ts +++ b/src/commands/git/search.ts @@ -2,7 +2,7 @@ /* eslint-disable no-loop-func */ import { QuickInputButton } from 'vscode'; import { Container } from '../../container'; -import { GitLog, GitLogCommit, GitService, Repository } from '../../git/gitService'; +import { GitLog, GitLogCommit, GitService, Repository, searchOperators, SearchOperators } from '../../git/gitService'; import { GlyphChars } from '../../constants'; import { QuickCommandBase, StepAsyncGenerator, StepSelection, StepState } from '../quickCommand'; import { RepositoryQuickPickItem } from '../../quickpicks'; @@ -32,13 +32,18 @@ export interface SearchGitCommandArgs { prefillOnly?: boolean; } -const searchOperators = new Set(['', 'author:', 'change:', 'commit:', 'file:']); -const searchOperatorToTitleMap = new Map([ +const searchOperatorToTitleMap = new Map([ ['', 'Search by Message'], - ['author:', 'Search by Author or Committer'], - ['change:', 'Search by Changes'], + ['=:', 'Search by Message'], + ['message:', 'Search by Message'], + ['@:', 'Search by Author'], + ['author:', 'Search by Author'], + ['#:', 'Search by Commit ID'], ['commit:', 'Search by Commit ID'], - ['file:', 'Search by File'] + ['?:', 'Search by File'], + ['file:', 'Search by File'], + ['~:', 'Search by Changes'], + ['change:', 'Search by Changes'] ]); export class SearchGitCommand extends QuickCommandBase { @@ -132,30 +137,30 @@ export class SearchGitCommand extends QuickCommandBase { } if (state.search === undefined || state.counter < 2) { - const items: QuickPickItemOfT[] = [ + const items: QuickPickItemOfT[] = [ { label: searchOperatorToTitleMap.get('')!, - description: `pattern ${GlyphChars.Dash} use quotes to search for phrases`, - item: '' + description: `pattern or message: pattern or =: pattern ${GlyphChars.Dash} use quotes to search for phrases`, + item: 'message:' }, { label: searchOperatorToTitleMap.get('author:')!, - description: 'author: pattern', + description: 'author: pattern or @: pattern', item: 'author:' }, { label: searchOperatorToTitleMap.get('commit:')!, - description: 'commit: sha', + description: 'commit: sha or #: sha', item: 'commit:' }, { label: searchOperatorToTitleMap.get('file:')!, - description: 'file: glob', + description: 'file: glob or ?: glob', item: 'file:' }, { label: searchOperatorToTitleMap.get('change:')!, - description: 'change: pattern', + description: 'change: pattern or ~: pattern', item: 'change:' } ]; diff --git a/src/git/git.ts b/src/git/git.ts index dd44ac4..30b90b3 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -807,7 +807,12 @@ export class Git { search: string[] = emptyArray, { maxCount, useShow }: { maxCount?: number; useShow?: boolean } = {} ) { - const params = [useShow ? 'show' : 'log', '--name-status', `--format=${GitLogParser.defaultFormat}`]; + const params = [ + useShow ? 'show' : 'log', + '--name-status', + `--format=${GitLogParser.defaultFormat}`, + '--use-mailmap' + ]; if (maxCount && !useShow) { params.push(`-n${maxCount}`); } diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 69881cf..bc0cbac 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -84,15 +84,55 @@ const RepoSearchWarnings = { doesNotExist: /no such file or directory/i }; +const doubleQuoteRegex = /"/g; const userConfigRegex = /^user\.(name|email) (.*)$/gm; const mappedAuthorRegex = /(.+)\s<(.+)>/; -const searchMessageOperationRegex = /(?=(.*?)\s?(?:(?:author:|commit:|file:|change:)|$))/; -const searchMessageValuesRegex = /(?:^|\b)\s?(?:\s?"([^"]*)(?:"|$)|([^"\s]*))/g; -const searchOperationRegex = /((?:author|commit|file|change):)\s?(?=(.*?)\s?(?:(?:author:|commit:|file:|change:)|$))/g; +const searchMessageOperationRegex = /(?=(.*?)\s?(?:(?:=:|message:|@:|author:|#:|commit:|\?:|file:|~:|change:)|$))/; +const searchMessageValuesRegex = /(".+"|[^\b\s]+)/g; +const searchOperationRegex = /((?:=|message|@|author|#|commit|\?|file|~|change):)\s?(?=(.*?)\s?(?:(?:=:|message:|@:|author:|#:|commit:|\?:|file:|~:|change:)|$))/g; const emptyPromise: Promise = Promise.resolve(undefined); const reflogCommands = ['merge', 'pull']; +export type SearchOperators = + | '' + | '=:' + | 'message:' + | '@:' + | 'author:' + | '#:' + | 'commit:' + | '?:' + | 'file:' + | '~:' + | 'change:'; +export const searchOperators = new Set([ + '', + '=:', + 'message:', + '@:', + 'author:', + '#:', + 'commit:', + '?:', + 'file:', + '~:', + 'change:' +]); +const normalizeSearchOperatorsMap = new Map([ + ['', 'message:'], + ['=:', 'message:'], + ['message:', 'message:'], + ['@:', 'author:'], + ['author:', 'author:'], + ['#:', 'commit:'], + ['commit:', 'commit:'], + ['?:', 'file:'], + ['file:', 'file:'], + ['~:', 'change:'], + ['change:', 'change:'] +]); + export class GitService implements Disposable { private _onDidChangeRepositories = new EventEmitter(); get onDidChangeRepositories(): Event { @@ -1454,49 +1494,48 @@ export class GitService implements Disposable { searchArgs.add('-m'); searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`); for (const value of values) { - searchArgs.add(value); + searchArgs.add(value.replace(doubleQuoteRegex, '')); } } else { searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`); searchArgs.add('--all'); searchArgs.add('--full-history'); searchArgs.add(search.matchRegex ? '--extended-regexp' : '--fixed-strings'); - if (!search.matchCase) { + if (search.matchRegex && !search.matchCase) { searchArgs.add('--regexp-ignore-case'); } for ([op, values] of operations.entries()) { switch (op) { - case 'author:': + case 'message:': searchArgs.add('-m'); + if (search.matchAll) { + searchArgs.add('--all-match'); + } for (const value of values) { - searchArgs.add(`--author=${value}`); - searchArgs.add(`--committer=${value}`); + searchArgs.add(`--grep=${value.replace(doubleQuoteRegex, '\\b')}`); } break; - case 'change:': + case 'author:': + searchArgs.add('-m'); for (const value of values) { - searchArgs.add(`-G=${value}`); + searchArgs.add(`--author=${value.replace(doubleQuoteRegex, '\\b')}`); } break; - case 'file:': + case 'change:': for (const value of values) { - files.push(value); + searchArgs.add(`-G=${value}`); } break; - case '': - searchArgs.add('-m'); - if (search.matchAll) { - searchArgs.add('--all-match'); - } + case 'file:': for (const value of values) { - searchArgs.add(`--grep=${value}`); + files.push(value.replace(doubleQuoteRegex, '')); } break; @@ -2835,28 +2874,7 @@ export class GitService implements Disposable { let match = searchMessageOperationRegex.exec(search); if (match != null && match[1] !== '') { - const messageSearch = match[1]; - - let quoted; - let unquoted; - do { - match = searchMessageValuesRegex.exec(messageSearch); - if (match == null) break; - - [, quoted, unquoted] = match; - if (!quoted && !unquoted) { - searchMessageValuesRegex.lastIndex = 0; - break; - } - - let values = operations.get(''); - if (values === undefined) { - values = [quoted || unquoted]; - operations.set('', values); - } else { - values.push(quoted || unquoted); - } - } while (match != null); + this.parseSearchMessageOperations(match[1], operations); } do { @@ -2866,16 +2884,53 @@ export class GitService implements Disposable { [, op, value] = match; if (op !== undefined) { - let values = operations.get(op); - if (values === undefined) { - values = [value]; - operations.set(op, values); + op = normalizeSearchOperatorsMap.get(op as SearchOperators)!; + + if (op === 'message:') { + this.parseSearchMessageOperations(value, operations); } else { - values.push(value); + let values = operations.get(op); + if (values === undefined) { + values = [value]; + operations.set(op, values); + } else { + values.push(value); + } } } } while (match != null); return operations; } + + private static parseSearchMessageOperations(message: string, operations: Map) { + let values = operations.get('message:'); + + if (message === emptyStr) { + if (values === undefined) { + values = ['']; + operations.set('message:', values); + } else { + values.push(''); + } + + return; + } + + let match; + let value; + do { + match = searchMessageValuesRegex.exec(message); + if (match == null) break; + + [, value] = match; + + if (values === undefined) { + values = [value]; + operations.set('message:', values); + } else { + values.push(value); + } + } while (match != null); + } } diff --git a/src/views/nodes/searchNode.ts b/src/views/nodes/searchNode.ts index a5f8a01..a8b4c88 100644 --- a/src/views/nodes/searchNode.ts +++ b/src/views/nodes/searchNode.ts @@ -6,6 +6,7 @@ import { debug, gate, Iterables, log, Promises } from '../../system'; import { View } from '../viewBase'; import { CommandMessageNode, MessageNode } from './common'; import { ResourceType, unknownGitUri, ViewNode } from './viewNode'; +import { SearchOperators } from '../../git/gitService'; export class SearchNode extends ViewNode { private _children: (ViewNode | MessageNode)[] = []; @@ -21,9 +22,7 @@ export class SearchNode extends ViewNode { command: 'gitlens.showCommitSearch' }; - const getCommandArgs = ( - search: '' | 'author:' | 'change:' | 'commit:' | 'file:' - ): SearchCommitsCommandArgs => { + const getCommandArgs = (search: SearchOperators): SearchCommitsCommandArgs => { return { search: { pattern: search }, prefillOnly: true @@ -36,10 +35,10 @@ export class SearchNode extends ViewNode { this, { ...command, - arguments: [this, getCommandArgs('')] + arguments: [this, getCommandArgs('message:')] }, 'Search by Message', - `pattern ${GlyphChars.Dash} use quotes to search for phrases`, + `pattern or message: pattern or =: pattern ${GlyphChars.Dash} use quotes to search for phrases`, `Click to search for commits with matching messages ${GlyphChars.Dash} use quotes to search for phrases` ), new CommandMessageNode( @@ -49,9 +48,9 @@ export class SearchNode extends ViewNode { ...command, arguments: [this, getCommandArgs('author:')] }, - `${GlyphChars.Space.repeat(4)} or, Author or Committer`, - 'author: pattern', - 'Click to search for commits with matching authors or committers' + `${GlyphChars.Space.repeat(4)} or, Author`, + 'author: pattern or @: pattern', + 'Click to search for commits with matching authors' ), new CommandMessageNode( this.view, @@ -61,7 +60,7 @@ export class SearchNode extends ViewNode { arguments: [this, getCommandArgs('commit:')] }, `${GlyphChars.Space.repeat(4)} or, Commit ID`, - 'commit: sha', + 'commit: sha or #: sha', 'Click to search for commits with matching commit ids' ), new CommandMessageNode( @@ -72,7 +71,7 @@ export class SearchNode extends ViewNode { arguments: [this, getCommandArgs('file:')] }, `${GlyphChars.Space.repeat(4)} or, Files`, - 'file: glob', + 'file: glob or ?: glob', 'Click to search for commits with matching files' ), new CommandMessageNode( @@ -83,7 +82,7 @@ export class SearchNode extends ViewNode { arguments: [this, getCommandArgs('change:')] }, `${GlyphChars.Space.repeat(4)} or, Changes`, - 'change: pattern', + 'change: pattern or ~: pattern', 'Click to search for commits with matching changes' ) ];