Skip to content

Commit 6546e74

Browse files
committed
Closes #3531 improves cherry-pick
1 parent 9a13fab commit 6546e74

File tree

15 files changed

+314
-150
lines changed

15 files changed

+314
-150
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1515

1616
- Improves editor revision navigation ([#4200](https://github.com/gitkraken/vscode-gitlens/issues/4200))
1717
- Improves AI-related error messages ([#4227](https://github.com/gitkraken/vscode-gitlens/issues/4227))
18+
- Changes cherry-pick command no longer use/open a terminal ([#3531](https://github.com/gitkraken/vscode-gitlens/issues/3531))
1819

1920
### Fixed
2021

src/commands/git/cherry-pick.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1+
import { window } from 'vscode';
12
import type { Container } from '../../container';
3+
import { skipPausedOperation } from '../../git/actions/pausedOperation';
4+
import { CherryPickError, CherryPickErrorReason } from '../../git/errors';
25
import type { GitBranch } from '../../git/models/branch';
36
import type { GitLog } from '../../git/models/log';
7+
import type { GitPausedOperationStatus } from '../../git/models/pausedOperationStatus';
48
import type { GitReference } from '../../git/models/reference';
59
import type { Repository } from '../../git/models/repository';
610
import { getReferenceLabel, isRevisionReference } from '../../git/utils/reference.utils';
711
import { createRevisionRange } from '../../git/utils/revision.utils';
12+
import { showGenericErrorMessage } from '../../messages';
813
import type { FlagsQuickPickItem } from '../../quickpicks/items/flags';
914
import { createFlagsQuickPickItem } from '../../quickpicks/items/flags';
15+
import { executeCommand } from '../../system/-webview/command';
16+
import { Logger } from '../../system/logger';
1017
import type { ViewsWithRepositoryFolders } from '../../views/viewBase';
1118
import type {
1219
PartialStepState,
@@ -81,8 +88,50 @@ export class CherryPickGitCommand extends QuickCommand<State> {
8188
return false;
8289
}
8390

84-
private execute(state: CherryPickStepState<State<GitReference[]>>) {
85-
state.repo.cherryPick(...state.flags, ...state.references.map(c => c.ref).reverse());
91+
private async execute(state: CherryPickStepState<State<GitReference[]>>) {
92+
try {
93+
await state.repo.git.commits().cherryPick?.(
94+
state.references.map(c => c.ref),
95+
{
96+
edit: state.flags.includes('--edit'),
97+
noCommit: state.flags.includes('--no-commit'),
98+
},
99+
);
100+
} catch (ex) {
101+
Logger.error(ex, this.title);
102+
if (ex instanceof CherryPickError && ex.reason === CherryPickErrorReason.EmptyCommit) {
103+
let pausedOperation: GitPausedOperationStatus | undefined;
104+
try {
105+
pausedOperation = await state.repo.git.status().getPausedOperationStatus?.();
106+
pausedOperation ??= await state.repo
107+
.waitForRepoChange(500)
108+
.then(() => state.repo.git.status().getPausedOperationStatus?.());
109+
} catch {}
110+
111+
const pausedAt = pausedOperation
112+
? getReferenceLabel(pausedOperation?.incoming, { icon: false, label: true, quoted: true })
113+
: undefined;
114+
115+
const skip = { title: 'Skip' };
116+
const cancel = { title: 'Cancel', isCloseAffordance: true };
117+
const result = await window.showInformationMessage(
118+
`The cherry-pick operation cannot be completed because ${
119+
pausedAt ?? 'it'
120+
} resulted in an empty commit.\n\nDo you want to skip ${pausedAt ?? 'this commit'}?`,
121+
{ modal: true },
122+
skip,
123+
cancel,
124+
);
125+
if (result === skip) {
126+
return void skipPausedOperation(state.repo);
127+
}
128+
129+
void executeCommand('gitlens.showCommitsView');
130+
return;
131+
}
132+
133+
void showGenericErrorMessage(ex.message);
134+
}
86135
}
87136

88137
override isFuzzyMatch(name: string): boolean {
@@ -143,7 +192,7 @@ export class CherryPickGitCommand extends QuickCommand<State> {
143192
label: false,
144193
})}`;
145194

146-
if (state.counter < 2 || state.references == null || state.references.length === 0) {
195+
if (state.counter < 2 || !state.references?.length) {
147196
const result: StepResult<GitReference> = yield* pickBranchOrTagStep(
148197
state as CherryPickStepState,
149198
context,
@@ -225,7 +274,7 @@ export class CherryPickGitCommand extends QuickCommand<State> {
225274
}
226275

227276
endSteps(state);
228-
this.execute(state as CherryPickStepState<State<GitReference[]>>);
277+
void this.execute(state as CherryPickStepState<State<GitReference[]>>);
229278
}
230279

231280
return state.counter < 0 ? StepResultBreak : undefined;

src/env/node/git/git.ts

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import { GitErrorHandling } from '../../../git/commandOptions';
1414
import {
1515
BlameIgnoreRevsFileBadRevisionError,
1616
BlameIgnoreRevsFileError,
17-
CherryPickError,
18-
CherryPickErrorReason,
1917
FetchError,
2018
FetchErrorReason,
2119
PullError,
@@ -83,6 +81,7 @@ export const GitErrors = {
8381
commitChangesFirst: /Please, commit your changes before you can/i,
8482
conflict: /^CONFLICT \([^)]+\): \b/m,
8583
detachedHead: /You are in 'detached HEAD' state/i,
84+
emptyPreviousCherryPick: /The previous cherry-pick is now empty/i,
8685
entryNotUpToDate: /error:\s*Entry ['"].+['"] not uptodate\. Cannot merge\./i,
8786
failedToDeleteDirectoryNotEmpty: /failed to delete '(.*?)': Directory not empty/i,
8887
invalidLineCount: /file .+? has only \d+ lines/i,
@@ -568,35 +567,6 @@ export class Git {
568567
return this.exec({ cwd: repoPath }, ...params);
569568
}
570569

571-
async cherrypick(
572-
repoPath: string,
573-
sha: string,
574-
options: { noCommit?: boolean; errors?: GitErrorHandling } = {},
575-
): Promise<void> {
576-
const params = ['cherry-pick'];
577-
if (options?.noCommit) {
578-
params.push('-n');
579-
}
580-
params.push(sha);
581-
582-
try {
583-
await this.exec({ cwd: repoPath, errors: options?.errors }, ...params);
584-
} catch (ex) {
585-
const msg: string = ex?.toString() ?? '';
586-
let reason: CherryPickErrorReason = CherryPickErrorReason.Other;
587-
if (
588-
GitErrors.changesWouldBeOverwritten.test(msg) ||
589-
GitErrors.changesWouldBeOverwritten.test(ex.stderr ?? '')
590-
) {
591-
reason = CherryPickErrorReason.AbortedWouldOverwrite;
592-
} else if (GitErrors.conflict.test(msg) || GitErrors.conflict.test(ex.stdout ?? '')) {
593-
reason = CherryPickErrorReason.Conflicts;
594-
}
595-
596-
throw new CherryPickError(reason, ex, sha);
597-
}
598-
}
599-
600570
// TODO: Expand to include options and other params
601571
async clone(url: string, parentPath: string): Promise<string | undefined> {
602572
let count = 0;

src/env/node/git/sub-providers/commits.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { SearchQuery } from '../../../../constants.search';
44
import type { Container } from '../../../../container';
55
import type { GitCache } from '../../../../git/cache';
66
import { GitErrorHandling } from '../../../../git/commandOptions';
7+
import { CherryPickError, CherryPickErrorReason } from '../../../../git/errors';
78
import type { GitCommitsSubProvider, LeftRightCommitCountResult } from '../../../../git/gitProvider';
89
import { GitUri } from '../../../../git/gitUri';
910
import type { GitBlame } from '../../../../git/models/blame';
@@ -21,6 +22,7 @@ import { parseGitDiffNameStatusFiles } from '../../../../git/parsers/diffParser'
2122
import {
2223
createLogParserSingle,
2324
createLogParserWithFilesAndStats,
25+
getShaAndDatesLogParser,
2426
LogType,
2527
parseGitLog,
2628
parseGitLogAllFormat,
@@ -41,7 +43,7 @@ import { isFolderGlob } from '../../../../system/path';
4143
import type { CachedLog, TrackedGitDocument } from '../../../../trackers/trackedDocument';
4244
import { GitDocumentState } from '../../../../trackers/trackedDocument';
4345
import type { Git } from '../git';
44-
import { gitLogDefaultConfigs, gitLogDefaultConfigsWithFiles } from '../git';
46+
import { GitErrors, gitLogDefaultConfigs, gitLogDefaultConfigsWithFiles } from '../git';
4547
import type { LocalGitProvider } from '../localGitProvider';
4648

4749
const emptyPromise: Promise<GitBlame | ParsedGitDiffHunks | GitLog | undefined> = Promise.resolve(undefined);
@@ -59,6 +61,61 @@ export class CommitsGitSubProvider implements GitCommitsSubProvider {
5961
return configuration.get('advanced.caching.enabled');
6062
}
6163

64+
@log()
65+
async cherryPick(
66+
repoPath: string,
67+
revs: string[],
68+
options?: { edit?: boolean; noCommit?: boolean },
69+
): Promise<void> {
70+
const args = ['cherry-pick'];
71+
if (options?.edit) {
72+
args.push('-e');
73+
}
74+
if (options?.noCommit) {
75+
args.push('-n');
76+
}
77+
78+
if (revs.length > 1) {
79+
const parser = getShaAndDatesLogParser();
80+
// Ensure the revs are in reverse committer date order
81+
const data = await this.git.exec(
82+
{ cwd: repoPath, stdin: join(revs, '\n') },
83+
'log',
84+
'--no-walk',
85+
'--stdin',
86+
...parser.arguments,
87+
'--',
88+
);
89+
const commits = [...parser.parse(data)].sort(
90+
(c1, c2) => Number(c1.committerDate) - Number(c2.committerDate),
91+
);
92+
revs = commits.map(c => c.sha);
93+
}
94+
95+
args.push(...revs);
96+
97+
try {
98+
await this.git.exec({ cwd: repoPath, errors: GitErrorHandling.Throw }, ...args);
99+
} catch (ex) {
100+
const msg: string = ex?.toString() ?? '';
101+
102+
let reason: CherryPickErrorReason = CherryPickErrorReason.Other;
103+
if (
104+
GitErrors.changesWouldBeOverwritten.test(msg) ||
105+
GitErrors.changesWouldBeOverwritten.test(ex.stderr ?? '')
106+
) {
107+
reason = CherryPickErrorReason.AbortedWouldOverwrite;
108+
} else if (GitErrors.conflict.test(msg) || GitErrors.conflict.test(ex.stdout ?? '')) {
109+
reason = CherryPickErrorReason.Conflicts;
110+
} else if (GitErrors.emptyPreviousCherryPick.test(msg)) {
111+
reason = CherryPickErrorReason.EmptyCommit;
112+
}
113+
114+
debugger;
115+
throw new CherryPickError(reason, ex, revs);
116+
}
117+
}
118+
62119
@log()
63120
async getCommit(repoPath: string, rev: string): Promise<GitCommit | undefined> {
64121
if (isUncommitted(rev, true)) {

src/env/node/git/sub-providers/graph.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import type { GitWorktree } from '../../../../git/models/worktree';
2525
import {
2626
getGraphParser,
2727
getGraphStatsParser,
28-
getRefAndDateParser,
29-
getRefParser,
28+
getShaAndDatesLogParser,
29+
getShaLogParser,
3030
} from '../../../../git/parsers/logParser';
3131
import type { GitGraphSearch, GitGraphSearchResultData, GitGraphSearchResults } from '../../../../git/search';
3232
import { getGitArgsFromSearchQuery, getSearchQueryComparisonKey } from '../../../../git/search';
@@ -86,12 +86,12 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
8686
const deferStats = options?.include?.stats; // && defaultLimit > 1000;
8787

8888
const parser = getGraphParser(options?.include?.stats && !deferStats);
89-
const refParser = getRefParser();
89+
const shaParser = getShaLogParser();
9090
const statsParser = getGraphStatsParser();
9191

92-
const [refResult, stashResult, branchesResult, remotesResult, currentUserResult, worktreesResult] =
92+
const [shaResult, stashResult, branchesResult, remotesResult, currentUserResult, worktreesResult] =
9393
await Promise.allSettled([
94-
this.git.log(repoPath, undefined, undefined, ...refParser.arguments, '-n1', rev ?? 'HEAD'),
94+
this.git.log(repoPath, undefined, undefined, ...shaParser.arguments, '-n1', rev ?? 'HEAD'),
9595
this.provider.stash?.getStash(repoPath),
9696
this.provider.branches.getBranches(repoPath),
9797
this.provider.remotes.getRemotes(repoPath),
@@ -121,7 +121,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
121121

122122
const remotes = getSettledValue(remotesResult);
123123
const remoteMap = remotes != null ? new Map(remotes.map(r => [r.name, r])) : new Map<string, GitRemote>();
124-
const selectSha = first(refParser.parse(getSettledValue(refResult) ?? ''));
124+
const selectSha = first(shaParser.parse(getSettledValue(shaResult) ?? ''));
125125

126126
const downstreamMap = new Map<string, string[]>();
127127

@@ -660,7 +660,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
660660

661661
const comparisonKey = getSearchQueryComparisonKey(search);
662662
try {
663-
const refAndDateParser = getRefAndDateParser();
663+
const parser = getShaAndDatesLogParser();
664664

665665
const currentUser = search.query.includes('@me')
666666
? await this.provider.config.getCurrentUser(repoPath)
@@ -672,15 +672,15 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
672672
{ cwd: repoPath, cancellation: options?.cancellation, configs: gitLogDefaultConfigs },
673673
'show',
674674
'-s',
675-
...refAndDateParser.arguments,
675+
...parser.arguments,
676676
...shas.values(),
677677
...searchArgs,
678678
'--',
679679
);
680680

681681
let i = 0;
682682
const results: GitGraphSearchResults = new Map<string, GitGraphSearchResultData>(
683-
map(refAndDateParser.parse(data), c => [
683+
map(parser.parse(data), c => [
684684
c.sha,
685685
{
686686
i: i++,
@@ -720,7 +720,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
720720
}
721721

722722
const args = [
723-
...refAndDateParser.arguments,
723+
...parser.arguments,
724724
`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`,
725725
'--use-mailmap',
726726
];
@@ -770,7 +770,7 @@ export class GraphGitSubProvider implements GitGraphSubProvider {
770770

771771
let count = total;
772772

773-
for (const r of refAndDateParser.parse(data)) {
773+
for (const r of parser.parse(data)) {
774774
if (includeOnlyStashes && !stashes?.has(r.sha)) continue;
775775

776776
if (results.has(r.sha)) {

src/env/node/git/sub-providers/patch.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { window } from 'vscode';
22
import type { Container } from '../../../../container';
33
import { CancellationError } from '../../../../errors';
4-
import { GitErrorHandling } from '../../../../git/commandOptions';
54
import {
65
ApplyPatchCommitError,
76
ApplyPatchCommitErrorReason,
@@ -130,7 +129,7 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
130129

131130
// Apply the patch using a cherry pick without committing
132131
try {
133-
await this.git.cherrypick(targetPath, rev, { noCommit: true, errors: GitErrorHandling.Throw });
132+
await this.provider.commits.cherryPick(targetPath, [rev], { noCommit: true });
134133
} catch (ex) {
135134
Logger.error(ex, scope);
136135
if (ex instanceof CherryPickError) {

0 commit comments

Comments
 (0)