Skip to content

Commit 552f0d1

Browse files
committed
Refactors merge/rebase status
- Unifies into paused operation status - Adds cherry-pick and revert statuses - Improves performance of detecting a paused operation Adds abort & continue actions (wip)
1 parent 4411b7f commit 552f0d1

38 files changed

+964
-587
lines changed

src/constants.views.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,10 @@ export type TreeViewNodeTypes =
126126
| 'grouping'
127127
| 'launchpad'
128128
| 'launchpad-item'
129-
| 'merge-status'
130129
| 'message'
131130
| 'pager'
131+
| 'paused-operation-status'
132132
| 'pullrequest'
133-
| 'rebase-status'
134133
| 'reflog'
135134
| 'reflog-record'
136135
| 'remote'

src/env/node/git/git.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
7575
export const GitErrors = {
7676
badRevision: /bad revision '(.*?)'/i,
7777
cantLockRef: /cannot lock ref|unable to update local ref/i,
78-
changesWouldBeOverwritten: /Your local changes to the following files would be overwritten/i,
78+
changesWouldBeOverwritten:
79+
/Your local changes to the following files would be overwritten|Your local changes would be overwritten/i,
7980
commitChangesFirst: /Please, commit your changes before you can/i,
8081
conflict: /^CONFLICT \([^)]+\): \b/m,
8182
failedToDeleteDirectoryNotEmpty: /failed to delete '(.*?)': Directory not empty/i,
@@ -93,18 +94,21 @@ export const GitErrors = {
9394
alreadyCheckedOut: /already checked out/i,
9495
mainWorkingTree: /is a main working tree/i,
9596
noUpstream: /^fatal: The current branch .* has no upstream branch/i,
97+
noPausedOperation:
98+
/no merge (?:in progress|to abort)|no cherry-pick(?: or revert)? in progress|no rebase in progress/i,
9699
permissionDenied: /Permission.*denied/i,
97100
pushRejected: /^error: failed to push some refs to\b/m,
98101
rebaseMultipleBranches: /cannot rebase onto multiple branches/i,
99102
remoteAhead: /rejected because the remote contains work/i,
100103
remoteConnection: /Could not read from remote repository/i,
101104
tagConflict: /! \[rejected\].*\(would clobber existing tag\)/m,
102-
unmergedFiles: /is not possible because you have unmerged files/i,
105+
unmergedFiles: /is not possible because you have unmerged files|You have unmerged files/i,
103106
unstagedChanges: /You have unstaged changes/i,
104107
tagAlreadyExists: /tag .* already exists/i,
105108
tagNotFound: /tag .* not found/i,
106109
invalidTagName: /invalid tag name/i,
107110
remoteRejected: /rejected because the remote contains work/i,
111+
unresolvedConflicts: /You must edit all merge conflicts|Resolve all conflicts/i,
108112
};
109113

110114
const GitWarnings = {
@@ -1543,7 +1547,7 @@ export class Git {
15431547
target,
15441548
);
15451549
} catch (ex) {
1546-
const msg = ex?.toString() ?? '';
1550+
const msg: string = ex?.toString() ?? '';
15471551

15481552
if (GitErrors.notAValidObjectName.test(msg)) {
15491553
throw new Error(

src/env/node/git/localGitProvider.ts

Lines changed: 40 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,10 @@ import type {
9797
GitGraphRowTag,
9898
} from '../../../git/models/graph';
9999
import type { GitLog } from '../../../git/models/log';
100-
import type { GitMergeStatus } from '../../../git/models/merge';
101100
import type { MergeConflict } from '../../../git/models/mergeConflict';
102-
import type { GitRebaseStatus } from '../../../git/models/rebase';
103-
import type { GitBranchReference, GitReference, GitTagReference } from '../../../git/models/reference';
104-
import { createReference, getReferenceFromBranch, isBranchReference } from '../../../git/models/reference.utils';
101+
import type { GitPausedOperationStatus } from '../../../git/models/pausedOperationStatus';
102+
import type { GitBranchReference, GitReference } from '../../../git/models/reference';
103+
import { createReference, isBranchReference } from '../../../git/models/reference.utils';
105104
import type { GitReflog } from '../../../git/models/reflog';
106105
import type { GitRemote } from '../../../git/models/remote';
107106
import { getVisibilityCacheKey, sortRemotes } from '../../../git/models/remote';
@@ -236,6 +235,7 @@ import {
236235
} from './git';
237236
import type { GitLocation } from './locator';
238237
import { findGitPath, InvalidGitConfigError, UnableToFindGitError } from './locator';
238+
import { abortPausedOperation, continuePausedOperation, getPausedOperationStatus } from './operations/pausedOperations';
239239
import { CancelledRunError, fsExists, RunError } from './shell';
240240

241241
const emptyArray = Object.freeze([]) as unknown as any[];
@@ -299,8 +299,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
299299
private readonly _branchCache = new Map<string, Promise<GitBranch | undefined>>();
300300
private readonly _branchesCache = new Map<string, Promise<PagedResult<GitBranch>>>();
301301
private readonly _contributorsCache = new Map<string, Map<string, Promise<GitContributor[]>>>();
302-
private readonly _mergeStatusCache = new Map<string, Promise<GitMergeStatus | undefined>>();
303-
private readonly _rebaseStatusCache = new Map<string, Promise<GitRebaseStatus | undefined>>();
302+
private readonly _pausedOperationStatusCache = new Map<string, Promise<GitPausedOperationStatus | undefined>>();
304303
private readonly _remotesCache = new Map<string, Promise<GitRemote[]>>();
305304
private readonly _repoInfoCache = new Map<string, RepositoryInfo>();
306305
private readonly _stashesCache = new Map<string, GitStash | null>();
@@ -356,14 +355,17 @@ export class LocalGitProvider implements GitProvider, Disposable {
356355
this._trackedPaths.clear();
357356
}
358357

359-
if (e.changed(RepositoryChange.Merge, RepositoryChangeComparisonMode.Any)) {
360-
this._branchCache.delete(repo.path);
361-
this._mergeStatusCache.delete(repo.path);
362-
}
363-
364-
if (e.changed(RepositoryChange.Rebase, RepositoryChangeComparisonMode.Any)) {
358+
if (
359+
e.changed(
360+
RepositoryChange.CherryPick,
361+
RepositoryChange.Merge,
362+
RepositoryChange.Rebase,
363+
RepositoryChange.Revert,
364+
RepositoryChangeComparisonMode.Any,
365+
)
366+
) {
365367
this._branchCache.delete(repo.path);
366-
this._rebaseStatusCache.delete(repo.path);
368+
this._pausedOperationStatusCache.delete(repo.path);
367369
}
368370

369371
if (e.changed(RepositoryChange.Stash, RepositoryChangeComparisonMode.Any)) {
@@ -1479,7 +1481,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
14791481
}
14801482

14811483
if (!caches.length || caches.includes('status')) {
1482-
cachesToClear.push(this._mergeStatusCache, this._rebaseStatusCache);
1484+
cachesToClear.push(this._pausedOperationStatusCache);
14831485
}
14841486

14851487
if (!caches.length || caches.includes('tags')) {
@@ -1872,7 +1874,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
18721874

18731875
// Trap and cache expected blame errors
18741876
if (document.state != null) {
1875-
const msg = ex?.toString() ?? '';
1877+
const msg: string = ex?.toString() ?? '';
18761878
Logger.debug(scope, `Cache replace (with empty promise): '${key}'; reason=${msg}`);
18771879

18781880
const value: CachedBlame = {
@@ -1968,7 +1970,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
19681970

19691971
// Trap and cache expected blame errors
19701972
if (document.state != null) {
1971-
const msg = ex?.toString() ?? '';
1973+
const msg: string = ex?.toString() ?? '';
19721974
Logger.debug(scope, `Cache replace (with empty promise): '${key}'; reason=${msg}`);
19731975

19741976
const value: CachedBlame = {
@@ -2220,13 +2222,14 @@ export class LocalGitProvider implements GitProvider, Disposable {
22202222

22212223
const [name, upstream] = data[0].split('\n');
22222224

2223-
const [rebaseStatusResult, committerDateResult] = await Promise.allSettled([
2224-
isDetachedHead(name) ? this.getRebaseStatus(repoPath) : undefined,
2225+
const [pausedOpStatusResult, committerDateResult] = await Promise.allSettled([
2226+
isDetachedHead(name) ? this.getPausedOperationStatus(repoPath) : undefined,
22252227
this.git.log__recent_committerdate(repoPath, commitOrdering),
22262228
]);
22272229

22282230
const committerDate = getSettledValue(committerDateResult);
2229-
const rebaseStatus = getSettledValue(rebaseStatusResult);
2231+
const pausedOpStatus = getSettledValue(pausedOpStatusResult);
2232+
const rebaseStatus = pausedOpStatus?.type === 'rebase' ? pausedOpStatus : undefined;
22302233

22312234
return new GitBranch(
22322235
this.container,
@@ -3539,7 +3542,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
35393542
} catch (ex) {
35403543
// Trap and cache expected diff errors
35413544
if (document.state != null) {
3542-
const msg = ex?.toString() ?? '';
3545+
const msg: string = ex?.toString() ?? '';
35433546
Logger.debug(scope, `Cache replace (with empty promise): '${key}'`);
35443547

35453548
const value: CachedDiff = {
@@ -3624,7 +3627,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
36243627
} catch (ex) {
36253628
// Trap and cache expected diff errors
36263629
if (document.state != null) {
3627-
const msg = ex?.toString() ?? '';
3630+
const msg: string = ex?.toString() ?? '';
36283631
Logger.debug(scope, `Cache replace (with empty promise): '${key}'`);
36293632

36303633
const value: CachedDiff = {
@@ -4417,170 +4420,24 @@ export class LocalGitProvider implements GitProvider, Disposable {
44174420
}
44184421
}
44194422

4423+
@gate()
44204424
@log()
4421-
async getMergeStatus(repoPath: string): Promise<GitMergeStatus | undefined> {
4422-
let status = this.useCaching ? this._mergeStatusCache.get(repoPath) : undefined;
4423-
if (status == null) {
4424-
async function getCore(this: LocalGitProvider): Promise<GitMergeStatus | undefined> {
4425-
const merge = await this.git.rev_parse__verify(repoPath, 'MERGE_HEAD');
4426-
if (merge == null) return undefined;
4427-
4428-
const [branchResult, mergeBaseResult, possibleSourceBranchesResult] = await Promise.allSettled([
4429-
this.getBranch(repoPath),
4430-
this.getMergeBase(repoPath, 'MERGE_HEAD', 'HEAD'),
4431-
this.getCommitBranches(repoPath, ['MERGE_HEAD'], undefined, { all: true, mode: 'pointsAt' }),
4432-
]);
4433-
4434-
const branch = getSettledValue(branchResult);
4435-
const mergeBase = getSettledValue(mergeBaseResult);
4436-
const possibleSourceBranches = getSettledValue(possibleSourceBranchesResult);
4437-
4438-
return {
4439-
type: 'merge',
4440-
repoPath: repoPath,
4441-
mergeBase: mergeBase,
4442-
HEAD: createReference(merge, repoPath, { refType: 'revision' }),
4443-
current: getReferenceFromBranch(branch!),
4444-
incoming:
4445-
possibleSourceBranches?.length === 1
4446-
? createReference(possibleSourceBranches[0], repoPath, {
4447-
refType: 'branch',
4448-
name: possibleSourceBranches[0],
4449-
remote: false,
4450-
})
4451-
: undefined,
4452-
} satisfies GitMergeStatus;
4453-
}
4454-
4455-
status = getCore.call(this);
4456-
if (this.useCaching) {
4457-
this._mergeStatusCache.set(repoPath, status);
4458-
}
4459-
}
4460-
4461-
return status;
4425+
getPausedOperationStatus(repoPath: string): Promise<GitPausedOperationStatus | undefined> {
4426+
return getPausedOperationStatus.call(
4427+
this,
4428+
repoPath,
4429+
this.useCaching ? this._pausedOperationStatusCache : undefined,
4430+
);
44624431
}
44634432

44644433
@log()
4465-
async getRebaseStatus(repoPath: string): Promise<GitRebaseStatus | undefined> {
4466-
let status = this.useCaching ? this._rebaseStatusCache.get(repoPath) : undefined;
4467-
if (status == null) {
4468-
async function getCore(this: LocalGitProvider): Promise<GitRebaseStatus | undefined> {
4469-
const gitDir = await this.getGitDir(repoPath);
4470-
const [rebaseMergeHeadResult, rebaseApplyHeadResult] = await Promise.allSettled([
4471-
this.git.readDotGitFile(gitDir, ['rebase-merge', 'head-name']),
4472-
this.git.readDotGitFile(gitDir, ['rebase-apply', 'head-name']),
4473-
]);
4474-
const rebaseMergeHead = getSettledValue(rebaseMergeHeadResult);
4475-
const rebaseApplyHead = getSettledValue(rebaseApplyHeadResult);
4476-
4477-
let branch = rebaseApplyHead ?? rebaseMergeHead;
4478-
if (branch == null) return undefined;
4479-
4480-
const path = rebaseApplyHead != null ? 'rebase-apply' : 'rebase-merge';
4481-
4482-
const [
4483-
rebaseHeadResult,
4484-
origHeadResult,
4485-
ontoResult,
4486-
stepsNumberResult,
4487-
stepsTotalResult,
4488-
stepsMessageResult,
4489-
] = await Promise.allSettled([
4490-
this.git.rev_parse__verify(repoPath, 'REBASE_HEAD'),
4491-
this.git.readDotGitFile(gitDir, [path, 'orig-head']),
4492-
this.git.readDotGitFile(gitDir, [path, 'onto']),
4493-
this.git.readDotGitFile(gitDir, [path, 'msgnum'], { numeric: true }),
4494-
this.git.readDotGitFile(gitDir, [path, 'end'], { numeric: true }),
4495-
this.git
4496-
.readDotGitFile(gitDir, [path, 'message'], { throw: true })
4497-
.catch(() => this.git.readDotGitFile(gitDir, [path, 'message-squashed'])),
4498-
]);
4499-
4500-
const origHead = getSettledValue(origHeadResult);
4501-
const onto = getSettledValue(ontoResult);
4502-
if (origHead == null || onto == null) return undefined;
4503-
4504-
let mergeBase;
4505-
const rebaseHead = getSettledValue(rebaseHeadResult);
4506-
if (rebaseHead != null) {
4507-
mergeBase = await this.getMergeBase(repoPath, rebaseHead, 'HEAD');
4508-
} else {
4509-
mergeBase = await this.getMergeBase(repoPath, onto, origHead);
4510-
}
4511-
4512-
if (branch.startsWith('refs/heads/')) {
4513-
branch = branch.substring(11).trim();
4514-
}
4515-
4516-
const [branchTipsResult, tagTipsResult] = await Promise.allSettled([
4517-
this.getCommitBranches(repoPath, [onto], undefined, { all: true, mode: 'pointsAt' }),
4518-
this.getCommitTags(repoPath, onto, { mode: 'pointsAt' }),
4519-
]);
4520-
4521-
const branchTips = getSettledValue(branchTipsResult);
4522-
const tagTips = getSettledValue(tagTipsResult);
4523-
4524-
let ontoRef: GitBranchReference | GitTagReference | undefined;
4525-
if (branchTips != null) {
4526-
for (const ref of branchTips) {
4527-
if (ref.startsWith('(no branch, rebasing')) continue;
4528-
4529-
ontoRef = createReference(ref, repoPath, {
4530-
refType: 'branch',
4531-
name: ref,
4532-
remote: false,
4533-
});
4534-
break;
4535-
}
4536-
}
4537-
if (ontoRef == null && tagTips != null) {
4538-
for (const ref of tagTips) {
4539-
if (ref.startsWith('(no branch, rebasing')) continue;
4540-
4541-
ontoRef = createReference(ref, repoPath, {
4542-
refType: 'tag',
4543-
name: ref,
4544-
});
4545-
break;
4546-
}
4547-
}
4548-
4549-
return {
4550-
type: 'rebase',
4551-
repoPath: repoPath,
4552-
mergeBase: mergeBase,
4553-
HEAD: createReference(rebaseHead ?? origHead, repoPath, { refType: 'revision' }),
4554-
onto: createReference(onto, repoPath, { refType: 'revision' }),
4555-
current: ontoRef,
4556-
incoming: createReference(branch, repoPath, {
4557-
refType: 'branch',
4558-
name: branch,
4559-
remote: false,
4560-
}),
4561-
steps: {
4562-
current: {
4563-
number: getSettledValue(stepsNumberResult) ?? 0,
4564-
commit:
4565-
rebaseHead != null
4566-
? createReference(rebaseHead, repoPath, {
4567-
refType: 'revision',
4568-
message: getSettledValue(stepsMessageResult),
4569-
})
4570-
: undefined,
4571-
},
4572-
total: getSettledValue(stepsTotalResult) ?? 0,
4573-
},
4574-
} satisfies GitRebaseStatus;
4575-
}
4576-
4577-
status = getCore.call(this);
4578-
if (this.useCaching) {
4579-
this._rebaseStatusCache.set(repoPath, status);
4580-
}
4581-
}
4434+
abortPausedOperation(repoPath: string, options?: { quit?: boolean }): Promise<void> {
4435+
return abortPausedOperation.call(this, repoPath, options);
4436+
}
45824437

4583-
return status;
4438+
@log()
4439+
continuePausedOperation(repoPath: string, options?: { skip?: boolean }): Promise<void> {
4440+
return continuePausedOperation.call(this, repoPath, options);
45844441
}
45854442

45864443
@log()
@@ -5335,11 +5192,11 @@ export class LocalGitProvider implements GitProvider, Disposable {
53355192
const status = parseGitStatus(data, repoPath, porcelainVersion);
53365193

53375194
if (status?.detached) {
5338-
const rebaseStatus = await this.getRebaseStatus(repoPath);
5339-
if (rebaseStatus != null) {
5195+
const pausedOpStatus = await this.getPausedOperationStatus(repoPath);
5196+
if (pausedOpStatus?.type === 'rebase') {
53405197
return new GitStatus(
53415198
repoPath,
5342-
rebaseStatus.incoming.name,
5199+
pausedOpStatus.incoming.name,
53435200
status.sha,
53445201
status.files,
53455202
status.state,

0 commit comments

Comments
 (0)