Skip to content

Commit d4a2996

Browse files
authored
Git - Handle tag conflict during pull operation (microsoft#167278)
Handle tag conflict during pull operation
1 parent a05f15f commit d4a2996

File tree

3 files changed

+74
-4
lines changed

3 files changed

+74
-4
lines changed

extensions/git/src/api/git.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,5 +351,6 @@ export const enum GitErrorCodes {
351351
NoPathFound = 'NoPathFound',
352352
UnknownPath = 'UnknownPath',
353353
EmptyCommitMessage = 'EmptyCommitMessage',
354-
BranchFastForwardRejected = 'BranchFastForwardRejected'
354+
BranchFastForwardRejected = 'BranchFastForwardRejected',
355+
TagConflict = 'TagConflict'
355356
}

extensions/git/src/git.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,6 +1764,25 @@ export class Repository {
17641764
}
17651765
}
17661766

1767+
async fetchTags(options: { remote: string; tags: string[]; force?: boolean }): Promise<void> {
1768+
const args = ['fetch'];
1769+
const spawnOptions: SpawnOptions = {
1770+
env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent }
1771+
};
1772+
1773+
args.push(options.remote);
1774+
1775+
for (const tag of options.tags) {
1776+
args.push(`refs/tags/${tag}:refs/tags/${tag}`);
1777+
}
1778+
1779+
if (options.force) {
1780+
args.push('--force');
1781+
}
1782+
1783+
await this.exec(args, spawnOptions);
1784+
}
1785+
17671786
async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
17681787
const args = ['pull'];
17691788

@@ -1803,6 +1822,8 @@ export class Repository {
18031822
err.gitErrorCode = GitErrorCodes.CantLockRef;
18041823
} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
18051824
err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
1825+
} else if (/! \[rejected\].*\(would clobber existing tag\)/m.test(err.stderr || '')) {
1826+
err.gitErrorCode = GitErrorCodes.TagConflict;
18061827
}
18071828

18081829
throw err;

extensions/git/src/repository.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import TelemetryReporter from '@vscode/extension-telemetry';
1111
import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery, FetchOptions } from './api/git';
1212
import { AutoFetcher } from './autofetch';
1313
import { debounce, memoize, throttle } from './decorators';
14-
import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git';
14+
import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions } from './git';
1515
import { StatusBarCommands } from './statusbar';
1616
import { toGitUri } from './uri';
1717
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util';
@@ -1658,12 +1658,28 @@ export class Repository implements Disposable {
16581658
}
16591659

16601660
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
1661-
await this.repository.pull(rebase, remote, branch, { unshallow, tags });
1661+
this._pullAndHandleTagConflict(rebase, remote, branch, { unshallow, tags });
16621662
}
16631663
});
16641664
});
16651665
}
16661666

1667+
private async _pullAndHandleTagConflict(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
1668+
try {
1669+
await this.repository.pull(rebase, remote, branch, options);
1670+
}
1671+
catch (err) {
1672+
if (err.gitErrorCode !== GitErrorCodes.TagConflict) {
1673+
throw err;
1674+
}
1675+
1676+
// Handle tag(s) conflict
1677+
if (await this.handleTagConflict(remote, err.stderr)) {
1678+
await this.repository.pull(rebase, remote, branch, options);
1679+
}
1680+
}
1681+
}
1682+
16671683
@throttle
16681684
async push(head: Branch, forcePushMode?: ForcePushMode): Promise<void> {
16691685
let remote: string | undefined;
@@ -1724,7 +1740,7 @@ export class Repository implements Disposable {
17241740
}
17251741

17261742
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
1727-
await this.repository.pull(rebase, remoteName, pullBranch, { tags, cancellationToken });
1743+
this._pullAndHandleTagConflict(rebase, remoteName, pullBranch, { tags, cancellationToken });
17281744
}
17291745
};
17301746

@@ -2476,6 +2492,38 @@ export class Repository implements Disposable {
24762492
return config.get<boolean>('optimisticUpdate') === true;
24772493
}
24782494

2495+
private async handleTagConflict(remote: string | undefined, raw: string): Promise<boolean> {
2496+
// Ensure there is a remote
2497+
remote = remote ?? this.HEAD?.upstream?.remote;
2498+
if (!remote) {
2499+
throw new Error('Unable to resolve tag conflict due to missing remote.');
2500+
}
2501+
2502+
// Extract tag names from message
2503+
const tags: string[] = [];
2504+
for (const match of raw.matchAll(/^ ! \[rejected\]\s+([^\s]+)\s+->\s+([^\s]+)\s+\(would clobber existing tag\)$/gm)) {
2505+
if (match.length === 3) {
2506+
tags.push(match[1]);
2507+
}
2508+
}
2509+
if (tags.length === 0) {
2510+
throw new Error(`Unable to extract tag names from error message: ${raw}`);
2511+
}
2512+
2513+
// Notification
2514+
const replaceLocalTags = l10n.t('Replace Local Tag(s)');
2515+
const message = l10n.t('Unable to pull from remote repository due to conflicting tag(s): {0}. Would you like to resolve the conflict by replacing the local tag(s)?', tags.join(', '));
2516+
const choice = await window.showErrorMessage(message, { modal: true }, replaceLocalTags);
2517+
2518+
if (choice !== replaceLocalTags) {
2519+
return false;
2520+
}
2521+
2522+
// Force fetch tags
2523+
await this.repository.fetchTags({ remote, tags, force: true });
2524+
return true;
2525+
}
2526+
24792527
public isBranchProtected(name: string = this.HEAD?.name ?? ''): boolean {
24802528
return this.isBranchProtectedMatcher ? this.isBranchProtectedMatcher(name) : false;
24812529
}

0 commit comments

Comments
 (0)