Browse Source

Adds full commit searching to Graph (wip)

main
Eric Amodio 2 years ago
parent
commit
97ea2fdc27
11 changed files with 426 additions and 91 deletions
  1. +4
    -1
      src/env/node/git/git.ts
  2. +162
    -70
      src/env/node/git/localGitProvider.ts
  3. +6
    -1
      src/git/gitProvider.ts
  4. +11
    -1
      src/git/gitProviderService.ts
  5. +12
    -2
      src/git/models/repository.ts
  6. +14
    -0
      src/git/search.ts
  7. +99
    -1
      src/plus/github/githubGitProvider.ts
  8. +50
    -12
      src/plus/webviews/graph/graphWebview.ts
  9. +15
    -0
      src/plus/webviews/graph/protocol.ts
  10. +20
    -1
      src/webviews/apps/plus/graph/GraphWrapper.tsx
  11. +33
    -2
      src/webviews/apps/plus/graph/graph.tsx

+ 4
- 1
src/env/node/git/git.ts View File

@ -867,10 +867,13 @@ export class Git {
params.push(options?.ref); params.push(options?.ref);
} }
if (!params.includes('--')) {
params.push('--');
}
return this.git<string>( return this.git<string>(
{ cwd: repoPath, configs: options?.configs ?? gitLogDefaultConfigs, stdin: options?.stdin }, { cwd: repoPath, configs: options?.configs ?? gitLogDefaultConfigs, stdin: options?.stdin },
...params, ...params,
'--',
); );
} }

+ 162
- 70
src/env/node/git/localGitProvider.ts View File

@ -106,7 +106,7 @@ import type { RemoteProvider } from '../../../git/remotes/remoteProvider';
import type { RemoteProviders } from '../../../git/remotes/remoteProviders'; import type { RemoteProviders } from '../../../git/remotes/remoteProviders';
import { getRemoteProviderMatcher, loadRemoteProviders } from '../../../git/remotes/remoteProviders'; import { getRemoteProviderMatcher, loadRemoteProviders } from '../../../git/remotes/remoteProviders';
import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider'; import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider';
import type { SearchPattern } from '../../../git/search';
import type { GitSearch, SearchPattern } from '../../../git/search';
import { parseSearchOperations } from '../../../git/search'; import { parseSearchOperations } from '../../../git/search';
import { Logger } from '../../../logger'; import { Logger } from '../../../logger';
import type { LogScope } from '../../../logger'; import type { LogScope } from '../../../logger';
@ -2638,90 +2638,107 @@ export class LocalGitProvider implements GitProvider, Disposable {
} }
@log() @log()
async getLogForSearch(
async searchForCommitsSimple(
repoPath: string, repoPath: string,
search: SearchPattern, search: SearchPattern,
options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' | null; skip?: number },
): Promise<GitLog | undefined> {
options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' },
): Promise<GitSearch> {
search = { matchAll: false, matchCase: false, matchRegex: true, ...search }; search = { matchAll: false, matchCase: false, matchRegex: true, ...search };
try { try {
const { args: searchArgs, files, commits } = this.getArgsFromSearchPattern(search);
if (commits?.length) {
return {
repoPath: repoPath,
pattern: search,
results: commits,
};
}
const refParser = getGraphRefParser();
const limit = options?.limit ?? configuration.get('advanced.maxSearchItems') ?? 0; const limit = options?.limit ?? configuration.get('advanced.maxSearchItems') ?? 0;
const similarityThreshold = configuration.get('advanced.similarityThreshold'); const similarityThreshold = configuration.get('advanced.similarityThreshold');
const operations = parseSearchOperations(search.pattern);
const searchArgs = new Set<string>();
const files: string[] = [];
let useShow = false;
let op;
let values = operations.get('commit:');
if (values != null) {
useShow = true;
searchArgs.add('-m');
searchArgs.add(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`);
for (const value of values) {
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.matchRegex && !search.matchCase) {
searchArgs.add('--regexp-ignore-case');
}
for ([op, values] of operations.entries()) {
switch (op) {
case 'message:':
searchArgs.add('-m');
if (search.matchAll) {
searchArgs.add('--all-match');
}
for (const value of values) {
searchArgs.add(
`--grep=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`,
);
}
const args = [
...refParser.arguments,
`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`,
'--use-mailmap',
];
if (limit) {
args.push(`-n${limit + 1}`);
}
if (options?.ordering) {
args.push(`--${options.ordering}-order`);
}
break;
async function searchForCommitsCore(
this: LocalGitProvider,
limit: number,
cursor?: { sha: string; skip: number },
): Promise<GitSearch> {
const data = await this.git.log2(
repoPath,
undefined,
...args,
...(cursor?.skip ? [`--skip=${cursor.skip}`] : []),
...searchArgs,
'--',
...files,
);
const results = [...refParser.parse(data)];
case 'author:':
searchArgs.add('-m');
for (const value of values) {
searchArgs.add(
`--author=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`,
);
}
const last = results[results.length - 1];
cursor =
last != null
? {
sha: last,
skip: results.length,
}
: undefined;
break;
return {
repoPath: repoPath,
pattern: search,
results: results,
paging:
limit !== 0 && results.length > limit
? {
limit: limit,
startingCursor: cursor?.sha,
more: true,
}
: undefined,
more: async (limit: number): Promise<GitSearch | undefined> =>
searchForCommitsCore.call(this, limit, cursor),
};
}
case 'change:':
for (const value of values) {
searchArgs.add(
search.matchRegex
? `-G${value.replace(doubleQuoteRegex, '')}`
: `-S${value.replace(doubleQuoteRegex, '')}`,
);
}
return searchForCommitsCore.call(this, limit);
} catch (ex) {
// TODO@eamodio handle error reporting -- just invalid patterns? or more detailed?
return {
repoPath: repoPath,
pattern: search,
results: [],
};
}
}
break;
@log()
async getLogForSearch(
repoPath: string,
search: SearchPattern,
options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' | null; skip?: number },
): Promise<GitLog | undefined> {
search = { matchAll: false, matchCase: false, matchRegex: true, ...search };
case 'file:':
for (const value of values) {
files.push(value.replace(doubleQuoteRegex, ''));
}
try {
const limit = options?.limit ?? configuration.get('advanced.maxSearchItems') ?? 0;
const similarityThreshold = configuration.get('advanced.similarityThreshold');
break;
}
}
}
const { args, files, commits } = this.getArgsFromSearchPattern(search);
const args = [...searchArgs.values(), '--'];
args.push(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, '--');
if (files.length !== 0) { if (files.length !== 0) {
args.push(...files); args.push(...files);
} }
@ -2730,7 +2747,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
ordering: configuration.get('advanced.commitOrdering'), ordering: configuration.get('advanced.commitOrdering'),
...options, ...options,
limit: limit, limit: limit,
useShow: useShow,
useShow: Boolean(commits?.length),
}); });
const log = GitLogParser.parse( const log = GitLogParser.parse(
this.container, this.container,
@ -2759,6 +2776,81 @@ export class LocalGitProvider implements GitProvider, Disposable {
} }
} }
private getArgsFromSearchPattern(search: SearchPattern): {
args: string[];
files: string[];
commits?: string[] | undefined;
} {
const operations = parseSearchOperations(search.pattern);
const searchArgs = new Set<string>();
const files: string[] = [];
let commits;
let op;
let values = operations.get('commit:');
if (values != null) {
// searchArgs.add('-m');
for (const value of values) {
searchArgs.add(value.replace(doubleQuoteRegex, ''));
}
commits = [...searchArgs.values()];
} else {
searchArgs.add('--all');
searchArgs.add('--full-history');
searchArgs.add(search.matchRegex ? '--extended-regexp' : '--fixed-strings');
if (search.matchRegex && !search.matchCase) {
searchArgs.add('--regexp-ignore-case');
}
for ([op, values] of operations.entries()) {
switch (op) {
case 'message:':
searchArgs.add('-m');
if (search.matchAll) {
searchArgs.add('--all-match');
}
for (const value of values) {
searchArgs.add(`--grep=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`);
}
break;
case 'author:':
searchArgs.add('-m');
for (const value of values) {
searchArgs.add(
`--author=${value.replace(doubleQuoteRegex, search.matchRegex ? '\\b' : '')}`,
);
}
break;
case 'change:':
for (const value of values) {
searchArgs.add(
search.matchRegex
? `-G${value.replace(doubleQuoteRegex, '')}`
: `-S${value.replace(doubleQuoteRegex, '')}`,
);
}
break;
case 'file:':
for (const value of values) {
files.push(value.replace(doubleQuoteRegex, ''));
}
break;
}
}
}
return { args: [...searchArgs.values()], files: files, commits: commits };
}
private getLogForSearchMoreFn( private getLogForSearchMoreFn(
log: GitLog, log: GitLog,
search: SearchPattern, search: SearchPattern,

+ 6
- 1
src/git/gitProvider.ts View File

@ -26,7 +26,7 @@ import type { GitWorktree } from './models/worktree';
import type { RemoteProvider } from './remotes/remoteProvider'; import type { RemoteProvider } from './remotes/remoteProvider';
import type { RemoteProviders } from './remotes/remoteProviders'; import type { RemoteProviders } from './remotes/remoteProviders';
import type { RichRemoteProvider } from './remotes/richRemoteProvider'; import type { RichRemoteProvider } from './remotes/richRemoteProvider';
import type { SearchPattern } from './search';
import type { GitSearch, SearchPattern } from './search';
export const enum GitProviderId { export const enum GitProviderId {
Git = 'git', Git = 'git',
@ -296,6 +296,11 @@ export interface GitProvider extends Disposable {
since?: string | undefined; since?: string | undefined;
}, },
): Promise<Set<string> | undefined>; ): Promise<Set<string> | undefined>;
searchForCommitsSimple(
repoPath: string | Uri,
search: SearchPattern,
options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' },
): Promise<GitSearch>;
getLogForSearch( getLogForSearch(
repoPath: string, repoPath: string,
search: SearchPattern, search: SearchPattern,

+ 11
- 1
src/git/gitProviderService.ts View File

@ -69,7 +69,7 @@ import type { GitWorktree } from './models/worktree';
import { RichRemoteProviders } from './remotes/remoteProviderConnections'; import { RichRemoteProviders } from './remotes/remoteProviderConnections';
import type { RemoteProviders } from './remotes/remoteProviders'; import type { RemoteProviders } from './remotes/remoteProviders';
import type { RichRemoteProvider } from './remotes/richRemoteProvider'; import type { RichRemoteProvider } from './remotes/richRemoteProvider';
import type { SearchPattern } from './search';
import type { GitSearch, SearchPattern } from './search';
const maxDefaultBranchWeight = 100; const maxDefaultBranchWeight = 100;
const weightedDefaultBranches = new Map<string, number>([ const weightedDefaultBranches = new Map<string, number>([
@ -1465,6 +1465,16 @@ export class GitProviderService implements Disposable {
} }
@log() @log()
searchForCommitsSimple(
repoPath: string | Uri,
search: SearchPattern,
options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' },
): Promise<GitSearch> {
const { provider, path } = this.getProvider(repoPath);
return provider.searchForCommitsSimple(path, search, options);
}
@log()
async getLogForSearch( async getLogForSearch(
repoPath: string | Uri, repoPath: string | Uri,
search: SearchPattern, search: SearchPattern,

+ 12
- 2
src/git/models/repository.ts View File

@ -23,7 +23,7 @@ import type { GitProviderDescriptor } from '../gitProvider';
import { loadRemoteProviders } from '../remotes/remoteProviders'; import { loadRemoteProviders } from '../remotes/remoteProviders';
import type { RemoteProviders } from '../remotes/remoteProviders'; import type { RemoteProviders } from '../remotes/remoteProviders';
import type { RichRemoteProvider } from '../remotes/richRemoteProvider'; import type { RichRemoteProvider } from '../remotes/richRemoteProvider';
import type { SearchPattern } from '../search';
import type { GitSearch, SearchPattern } from '../search';
import type { BranchSortOptions, GitBranch } from './branch'; import type { BranchSortOptions, GitBranch } from './branch';
import { getBranchNameWithoutRemote, getRemoteNameFromBranchName } from './branch'; import { getBranchNameWithoutRemote, getRemoteNameFromBranchName } from './branch';
import type { GitCommit } from './commit'; import type { GitCommit } from './commit';
@ -852,10 +852,20 @@ export class Repository implements Disposable {
this.runTerminalCommand('revert', ...args); this.runTerminalCommand('revert', ...args);
} }
searchForCommits(search: SearchPattern, options?: { limit?: number; skip?: number }): Promise<GitLog | undefined> {
searchForCommits(
search: SearchPattern,
options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo'; skip?: number },
): Promise<GitLog | undefined> {
return this.container.git.getLogForSearch(this.path, search, options); return this.container.git.getLogForSearch(this.path, search, options);
} }
searchForCommitsSimple(
search: SearchPattern,
options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' },
): Promise<GitSearch> {
return this.container.git.searchForCommitsSimple(this.path, search, options);
}
async setRemoteAsDefault(remote: GitRemote, value: boolean = true) { async setRemoteAsDefault(remote: GitRemote, value: boolean = true) {
await this.container.storage.storeWorkspace('remote:default', value ? remote.id : undefined); await this.container.storage.storeWorkspace('remote:default', value ? remote.id : undefined);

+ 14
- 0
src/git/search.ts View File

@ -35,6 +35,20 @@ export interface SearchPattern {
matchRegex?: boolean; matchRegex?: boolean;
} }
export interface GitSearch {
repoPath: string;
pattern: SearchPattern;
results: string[];
readonly paging?: {
readonly limit: number | undefined;
readonly startingCursor: string | undefined;
readonly more: boolean;
};
more?(limit: number): Promise<GitSearch | undefined>;
}
export function getKeyForSearchPattern(search: SearchPattern) { export function getKeyForSearchPattern(search: SearchPattern) {
return `${search.pattern}|${search.matchAll ? 'A' : ''}${search.matchCase ? 'C' : ''}${ return `${search.pattern}|${search.matchAll ? 'A' : ''}${search.matchCase ? 'C' : ''}${
search.matchRegex ? 'R' : '' search.matchRegex ? 'R' : ''

+ 99
- 1
src/plus/github/githubGitProvider.ts View File

@ -72,7 +72,7 @@ import type { RemoteProvider } from '../../git/remotes/remoteProvider';
import type { RemoteProviders } from '../../git/remotes/remoteProviders'; import type { RemoteProviders } from '../../git/remotes/remoteProviders';
import { getRemoteProviderMatcher, loadRemoteProviders } from '../../git/remotes/remoteProviders'; import { getRemoteProviderMatcher, loadRemoteProviders } from '../../git/remotes/remoteProviders';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider'; import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
import type { SearchPattern } from '../../git/search';
import type { GitSearch, SearchPattern } from '../../git/search';
import { parseSearchOperations } from '../../git/search'; import { parseSearchOperations } from '../../git/search';
import type { LogScope } from '../../logger'; import type { LogScope } from '../../logger';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
@ -1581,6 +1581,104 @@ export class GitHubGitProvider implements GitProvider, Disposable {
} }
@log() @log()
async searchForCommitsSimple(
repoPath: string,
search: SearchPattern,
_options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' },
): Promise<GitSearch> {
search = { matchAll: false, matchCase: false, matchRegex: true, ...search };
return {
repoPath: repoPath,
pattern: search,
results: [],
};
// try {
// const { args: searchArgs, files, commits } = this.getArgsFromSearchPattern(search);
// if (commits?.length) {
// return {
// repoPath: repoPath,
// pattern: search,
// results: commits,
// };
// }
// const refParser = getGraphRefParser();
// const limit = options?.limit ?? configuration.get('advanced.maxSearchItems') ?? 0;
// const similarityThreshold = configuration.get('advanced.similarityThreshold');
// const args = [
// 'log',
// ...refParser.arguments,
// `-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`,
// '--use-mailmap',
// ];
// if (limit) {
// args.push(`-n${limit + 1}`);
// }
// if (options?.ordering) {
// args.push(`--${options.ordering}-order`);
// }
// searchArgs.push(`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`, '--');
// if (files.length !== 0) {
// searchArgs.push(...files);
// }
// async function searchForCommitsCore(
// this: LocalGitProvider,
// limit: number,
// cursor?: { sha: string; skip: number },
// ): Promise<GitSearch> {
// const data = await this.git.log2(
// repoPath,
// undefined,
// ...args,
// ...(cursor?.skip ? [`--skip=${cursor.skip}`] : []),
// ...searchArgs,
// '--',
// ...files,
// );
// const results = [...refParser.parse(data)];
// const last = results[results.length - 1];
// cursor =
// last != null
// ? {
// sha: last,
// skip: results.length,
// }
// : undefined;
// return {
// repoPath: repoPath,
// pattern: search,
// results: results,
// paging:
// limit !== 0 && results.length > limit
// ? {
// limit: limit,
// startingCursor: cursor?.sha,
// more: true,
// }
// : undefined,
// more: async (limit: number): Promise<GitSearch | undefined> =>
// searchForCommitsCore.call(this, limit, cursor),
// };
// }
// return searchForCommitsCore.call(this, limit);
// } catch (ex) {
// // TODO@eamodio handle error reporting -- just invalid patterns? or more detailed?
// return {
// repoPath: repoPath,
// pattern: search,
// results: [],
// };
// }
}
@log()
async getLogForSearch( async getLogForSearch(
repoPath: string, repoPath: string,
search: SearchPattern, search: SearchPattern,

+ 50
- 12
src/plus/webviews/graph/graphWebview.ts View File

@ -14,6 +14,7 @@ import { GitGraphRowType } from '../../../git/models/graph';
import type { GitGraph } from '../../../git/models/graph'; import type { GitGraph } from '../../../git/models/graph';
import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository'; import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository';
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository'; import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository';
import type { SearchPattern } from '../../../git/search';
import { registerCommand } from '../../../system/command'; import { registerCommand } from '../../../system/command';
import { gate } from '../../../system/decorators/gate'; import { gate } from '../../../system/decorators/gate';
import { debug } from '../../../system/decorators/log'; import { debug } from '../../../system/decorators/log';
@ -35,9 +36,11 @@ import {
DidChangeNotificationType, DidChangeNotificationType,
DidChangeSelectionNotificationType, DidChangeSelectionNotificationType,
DidChangeSubscriptionNotificationType, DidChangeSubscriptionNotificationType,
DidSearchCommitsNotificationType,
DismissBannerCommandType, DismissBannerCommandType,
GetMissingAvatarsCommandType, GetMissingAvatarsCommandType,
GetMoreCommitsCommandType, GetMoreCommitsCommandType,
SearchCommitsCommandType,
UpdateColumnCommandType, UpdateColumnCommandType,
UpdateSelectedRepositoryCommandType, UpdateSelectedRepositoryCommandType,
UpdateSelectionCommandType, UpdateSelectionCommandType,
@ -194,7 +197,10 @@ export class GraphWebview extends WebviewBase {
onIpc(GetMissingAvatarsCommandType, e, params => this.onGetMissingAvatars(params.emails)); onIpc(GetMissingAvatarsCommandType, e, params => this.onGetMissingAvatars(params.emails));
break; break;
case GetMoreCommitsCommandType.method: case GetMoreCommitsCommandType.method:
onIpc(GetMoreCommitsCommandType, e, params => this.onGetMoreCommits(params.sha));
onIpc(GetMoreCommitsCommandType, e, params => this.onGetMoreCommits(params.sha, e.id));
break;
case SearchCommitsCommandType.method:
onIpc(SearchCommitsCommandType, e, params => this.onSearchCommits(params.search, e.id));
break; break;
case UpdateColumnCommandType.method: case UpdateColumnCommandType.method:
onIpc(UpdateColumnCommandType, e, params => this.onColumnUpdated(params.name, params.config)); onIpc(UpdateColumnCommandType, e, params => this.onColumnUpdated(params.name, params.config));
@ -363,7 +369,7 @@ export class GraphWebview extends WebviewBase {
} }
@gate() @gate()
private async onGetMoreCommits(sha?: string) {
private async onGetMoreCommits(sha?: string, completionId?: string) {
if (this._graph?.more == null || this._repository?.etag !== this._etagRepository) { if (this._graph?.more == null || this._repository?.etag !== this._etagRepository) {
this.updateState(true); this.updateState(true);
@ -378,7 +384,35 @@ export class GraphWebview extends WebviewBase {
debugger; debugger;
} }
void this.notifyDidChangeCommits();
void this.notifyDidChangeCommits(completionId);
}
@gate()
private async onSearchCommits(searchPattern: SearchPattern, completionId?: string) {
// if (this._repository?.etag !== this._etagRepository) {
// this.updateState(true);
// return;
// }
if (this._repository == null) return;
const search = await this._repository.searchForCommitsSimple(searchPattern, {
limit: 100,
ordering: configuration.get('graph.commitOrdering'),
});
void this.notify(
DidSearchCommitsNotificationType,
{
ids: search.results,
paging: {
startingCursor: search.paging?.startingCursor,
more: search.paging?.more ?? false,
},
},
completionId,
);
} }
private onRepositorySelectionChanged(path: string) { private onRepositorySelectionChanged(path: string) {
@ -498,20 +532,24 @@ export class GraphWebview extends WebviewBase {
} }
@debug() @debug()
private async notifyDidChangeCommits() {
private async notifyDidChangeCommits(completionId?: string) {
let success = false; let success = false;
if (this.isReady && this.visible) { if (this.isReady && this.visible) {
const data = this._graph!; const data = this._graph!;
success = await this.notify(DidChangeCommitsNotificationType, {
rows: data.rows,
avatars: Object.fromEntries(data.avatars),
selectedRows: this._selectedRows,
paging: {
startingCursor: data.paging?.startingCursor,
more: data.paging?.more ?? false,
success = await this.notify(
DidChangeCommitsNotificationType,
{
rows: data.rows,
avatars: Object.fromEntries(data.avatars),
selectedRows: this._selectedRows,
paging: {
startingCursor: data.paging?.startingCursor,
more: data.paging?.more ?? false,
},
}, },
});
completionId,
);
} }
this._pendingNotifyCommits = !success; this._pendingNotifyCommits = !success;

+ 15
- 0
src/plus/webviews/graph/protocol.ts View File

@ -2,6 +2,7 @@ import type { GraphRow, Remote } from '@gitkraken/gitkraken-components';
import type { DateStyle, GraphColumnConfig } from '../../../config'; import type { DateStyle, GraphColumnConfig } from '../../../config';
import type { RepositoryVisibility } from '../../../git/gitProvider'; import type { RepositoryVisibility } from '../../../git/gitProvider';
import type { GitGraphRowType } from '../../../git/models/graph'; import type { GitGraphRowType } from '../../../git/models/graph';
import type { SearchPattern } from '../../../git/search';
import type { Subscription } from '../../../subscription'; import type { Subscription } from '../../../subscription';
import type { DateTimeFormat } from '../../../system/date'; import type { DateTimeFormat } from '../../../system/date';
import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol'; import { IpcCommandType, IpcNotificationType } from '../../../webviews/protocol';
@ -24,6 +25,7 @@ export interface State {
// Props below are computed in the webview (not passed) // Props below are computed in the webview (not passed)
mixedColumnColors?: Record<string, string>; mixedColumnColors?: Record<string, string>;
searchResults?: DidSearchCommitsParams;
} }
export interface GraphPaging { export interface GraphPaging {
@ -87,6 +89,11 @@ export interface GetMoreCommitsParams {
} }
export const GetMoreCommitsCommandType = new IpcCommandType<GetMoreCommitsParams>('graph/getMoreCommits'); export const GetMoreCommitsCommandType = new IpcCommandType<GetMoreCommitsParams>('graph/getMoreCommits');
export interface SearchCommitsParams {
search: SearchPattern;
}
export const SearchCommitsCommandType = new IpcCommandType<SearchCommitsParams>('graph/searchCommits');
export interface UpdateColumnParams { export interface UpdateColumnParams {
name: string; name: string;
config: GraphColumnConfig; config: GraphColumnConfig;
@ -149,3 +156,11 @@ export interface DidChangeSelectionParams {
export const DidChangeSelectionNotificationType = new IpcNotificationType<DidChangeSelectionParams>( export const DidChangeSelectionNotificationType = new IpcNotificationType<DidChangeSelectionParams>(
'graph/selection/didChange', 'graph/selection/didChange',
); );
export interface DidSearchCommitsParams {
ids: string[];
paging?: GraphPaging;
}
export const DidSearchCommitsNotificationType = new IpcNotificationType<DidSearchCommitsParams>(
'graph/commits/didSearch',
);

+ 20
- 1
src/webviews/apps/plus/graph/GraphWrapper.tsx View File

@ -14,6 +14,7 @@ import { DateStyle } from '../../../../config';
import type { GraphColumnConfig } from '../../../../config'; import type { GraphColumnConfig } from '../../../../config';
import { RepositoryVisibility } from '../../../../git/gitProvider'; import { RepositoryVisibility } from '../../../../git/gitProvider';
import type { GitGraphRowType } from '../../../../git/models/graph'; import type { GitGraphRowType } from '../../../../git/models/graph';
import type { SearchPattern } from '../../../../git/search';
import type { import type {
DismissBannerParams, DismissBannerParams,
GraphComponentConfig, GraphComponentConfig,
@ -34,7 +35,8 @@ export interface GraphWrapperProps extends State {
onSelectRepository?: (repository: GraphRepository) => void; onSelectRepository?: (repository: GraphRepository) => void;
onColumnChange?: (name: string, settings: GraphColumnConfig) => void; onColumnChange?: (name: string, settings: GraphColumnConfig) => void;
onMissingAvatars?: (emails: { [email: string]: string }) => void; onMissingAvatars?: (emails: { [email: string]: string }) => void;
onMoreCommits?: () => void;
onMoreCommits?: (id?: string) => void;
onSearchCommits?: (search: SearchPattern) => void; //Promise<DidSearchCommitsParams>;
onDismissBanner?: (key: DismissBannerParams['key']) => void; onDismissBanner?: (key: DismissBannerParams['key']) => void;
onSelectionChange?: (selection: { id: string; type: GitGraphRowType }[]) => void; onSelectionChange?: (selection: { id: string; type: GitGraphRowType }[]) => void;
} }
@ -89,6 +91,16 @@ const getGraphDateFormatter = (config?: GraphComponentConfig): OnFormatCommitDat
return (commitDateTime: number) => formatCommitDateTime(commitDateTime, config?.dateStyle, config?.dateFormat); return (commitDateTime: number) => formatCommitDateTime(commitDateTime, config?.dateStyle, config?.dateFormat);
}; };
const getSearchHighlights = (searchResults: State['searchResults']): { [id: string]: boolean } | undefined => {
if (!searchResults?.ids?.length) return undefined;
const highlights: { [id: string]: boolean } = {};
for (const sha of searchResults.ids) {
highlights[sha] = true;
}
return highlights;
};
type DebouncableFn = (...args: any) => void; type DebouncableFn = (...args: any) => void;
type DebouncedFn = (...args: any) => void; type DebouncedFn = (...args: any) => void;
const debounceFrame = (func: DebouncableFn): DebouncedFn => { const debounceFrame = (func: DebouncableFn): DebouncedFn => {
@ -188,10 +200,12 @@ export function GraphWrapper({
onColumnChange, onColumnChange,
onMissingAvatars, onMissingAvatars,
onMoreCommits, onMoreCommits,
onSearchCommits,
onSelectionChange, onSelectionChange,
nonce, nonce,
mixedColumnColors, mixedColumnColors,
previewBanner = true, previewBanner = true,
searchResults: searchResults2,
trialBanner = true, trialBanner = true,
onDismissBanner, onDismissBanner,
}: GraphWrapperProps) { }: GraphWrapperProps) {
@ -228,11 +242,13 @@ export function GraphWrapper({
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [searchResults, setSearchResults] = useState<GraphRow[]>([]); const [searchResults, setSearchResults] = useState<GraphRow[]>([]);
const [searchResultKey, setSearchResultKey] = useState<string | undefined>(undefined); const [searchResultKey, setSearchResultKey] = useState<string | undefined>(undefined);
const [searchHighlights, setSearchHighlights] = useState(getSearchHighlights(searchResults2));
useEffect(() => { useEffect(() => {
if (searchValue === '' || searchValue.length < 3 || graphRows.length < 1) { if (searchValue === '' || searchValue.length < 3 || graphRows.length < 1) {
setSearchResults([]); setSearchResults([]);
setSearchResultKey(undefined); setSearchResultKey(undefined);
setSearchHighlights(undefined);
return; return;
} }
@ -276,6 +292,7 @@ export function GraphWrapper({
const currentValue = e.currentTarget.value; const currentValue = e.currentTarget.value;
setSearchValue(currentValue); setSearchValue(currentValue);
onSearchCommits?.({ pattern: currentValue });
}; };
useLayoutEffect(() => { useLayoutEffect(() => {
@ -310,6 +327,7 @@ export function GraphWrapper({
setSubscriptionSnapshot(state.subscription); setSubscriptionSnapshot(state.subscription);
setIsPrivateRepo(state.selectedRepositoryVisibility === RepositoryVisibility.Private); setIsPrivateRepo(state.selectedRepositoryVisibility === RepositoryVisibility.Private);
setIsLoading(state.loading); setIsLoading(state.loading);
setSearchHighlights(getSearchHighlights(state.searchResults));
} }
useEffect(() => subscriber?.(transformData), []); useEffect(() => subscriber?.(transformData), []);
@ -576,6 +594,7 @@ export function GraphWrapper({
graphRows={graphRows} graphRows={graphRows}
hasMoreCommits={pagingState?.more} hasMoreCommits={pagingState?.more}
height={mainHeight} height={mainHeight}
highlightedShas={searchHighlights}
// highlightRowssOnRefHover={graphConfig?.highlightRowsOnRefHover} // highlightRowssOnRefHover={graphConfig?.highlightRowsOnRefHover}
isLoadingRows={isLoading} isLoadingRows={isLoading}
isSelectedBySha={graphSelectedRows} isSelectedBySha={graphSelectedRows}

+ 33
- 2
src/webviews/apps/plus/graph/graph.tsx View File

@ -4,6 +4,7 @@ import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom'; import { render, unmountComponentAtNode } from 'react-dom';
import type { GraphColumnConfig } from '../../../../config'; import type { GraphColumnConfig } from '../../../../config';
import type { GitGraphRowType } from '../../../../git/models/graph'; import type { GitGraphRowType } from '../../../../git/models/graph';
import type { SearchPattern } from '../../../../git/search';
import type { import type {
DismissBannerParams, DismissBannerParams,
GraphRepository, GraphRepository,
@ -17,9 +18,11 @@ import {
DidChangeNotificationType, DidChangeNotificationType,
DidChangeSelectionNotificationType, DidChangeSelectionNotificationType,
DidChangeSubscriptionNotificationType, DidChangeSubscriptionNotificationType,
DidSearchCommitsNotificationType,
DismissBannerCommandType, DismissBannerCommandType,
GetMissingAvatarsCommandType, GetMissingAvatarsCommandType,
GetMoreCommitsCommandType, GetMoreCommitsCommandType,
SearchCommitsCommandType,
UpdateColumnCommandType, UpdateColumnCommandType,
UpdateSelectedRepositoryCommandType as UpdateRepositorySelectionCommandType, UpdateSelectedRepositoryCommandType as UpdateRepositorySelectionCommandType,
UpdateSelectionCommandType, UpdateSelectionCommandType,
@ -72,6 +75,7 @@ export class GraphApp extends App {
)} )}
onMissingAvatars={(...params) => this.onGetMissingAvatars(...params)} onMissingAvatars={(...params) => this.onGetMissingAvatars(...params)}
onMoreCommits={(...params) => this.onGetMoreCommits(...params)} onMoreCommits={(...params) => this.onGetMoreCommits(...params)}
onSearchCommits={(...params) => this.onSearchCommits(...params)}
onSelectionChange={debounce( onSelectionChange={debounce(
(selection: { id: string; type: GitGraphRowType }[]) => this.onSelectionChanged(selection), (selection: { id: string; type: GitGraphRowType }[]) => this.onSelectionChanged(selection),
250, 250,
@ -183,6 +187,13 @@ export class GraphApp extends App {
}); });
break; break;
case DidSearchCommitsNotificationType.method:
onIpc(DidSearchCommitsNotificationType, msg, params => {
this.setState({ ...this.state, searchResults: params });
this.refresh(this.state);
});
break;
case DidChangeSelectionNotificationType.method: case DidChangeSelectionNotificationType.method:
onIpc(DidChangeSelectionNotificationType, msg, params => { onIpc(DidChangeSelectionNotificationType, msg, params => {
this.setState({ ...this.state, selectedRows: params.selection }); this.setState({ ...this.state, selectedRows: params.selection });
@ -274,8 +285,28 @@ export class GraphApp extends App {
this.sendCommand(GetMissingAvatarsCommandType, { emails: emails }); this.sendCommand(GetMissingAvatarsCommandType, { emails: emails });
} }
private onGetMoreCommits(sha?: string) {
this.sendCommand(GetMoreCommitsCommandType, { sha: sha });
private onGetMoreCommits(sha?: string, wait?: boolean) {
if (wait) {
return this.sendCommandWithCompletion(
GetMoreCommitsCommandType,
{ sha: sha },
DidChangeCommitsNotificationType,
);
}
return this.sendCommand(GetMoreCommitsCommandType, { sha: sha });
}
private onSearchCommits(search: SearchPattern, wait?: boolean) {
if (wait) {
return this.sendCommandWithCompletion(
SearchCommitsCommandType,
{ search: search },
DidSearchCommitsNotificationType,
);
}
return this.sendCommand(SearchCommitsCommandType, { search: search });
} }
private onSelectionChanged(selection: { id: string; type: GitGraphRowType }[]) { private onSelectionChanged(selection: { id: string; type: GitGraphRowType }[]) {

Loading…
Cancel
Save