Git - Handle tag conflict during pull operation (#167278)

Handle tag conflict during pull operation
This commit is contained in:
Ladislau Szomoru
2022-11-25 18:05:22 +01:00
committed by GitHub
parent a05f15f5c8
commit d4a299631a
3 changed files with 74 additions and 4 deletions

View File

@@ -351,5 +351,6 @@ export const enum GitErrorCodes {
NoPathFound = 'NoPathFound',
UnknownPath = 'UnknownPath',
EmptyCommitMessage = 'EmptyCommitMessage',
BranchFastForwardRejected = 'BranchFastForwardRejected'
BranchFastForwardRejected = 'BranchFastForwardRejected',
TagConflict = 'TagConflict'
}

View File

@@ -1764,6 +1764,25 @@ export class Repository {
}
}
async fetchTags(options: { remote: string; tags: string[]; force?: boolean }): Promise<void> {
const args = ['fetch'];
const spawnOptions: SpawnOptions = {
env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent }
};
args.push(options.remote);
for (const tag of options.tags) {
args.push(`refs/tags/${tag}:refs/tags/${tag}`);
}
if (options.force) {
args.push('--force');
}
await this.exec(args, spawnOptions);
}
async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
const args = ['pull'];
@@ -1803,6 +1822,8 @@ export class Repository {
err.gitErrorCode = GitErrorCodes.CantLockRef;
} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
} else if (/! \[rejected\].*\(would clobber existing tag\)/m.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.TagConflict;
}
throw err;

View File

@@ -11,7 +11,7 @@ import TelemetryReporter from '@vscode/extension-telemetry';
import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery, FetchOptions } from './api/git';
import { AutoFetcher } from './autofetch';
import { debounce, memoize, throttle } from './decorators';
import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git';
import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions } from './git';
import { StatusBarCommands } from './statusbar';
import { toGitUri } from './uri';
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 {
}
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
await this.repository.pull(rebase, remote, branch, { unshallow, tags });
this._pullAndHandleTagConflict(rebase, remote, branch, { unshallow, tags });
}
});
});
}
private async _pullAndHandleTagConflict(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
try {
await this.repository.pull(rebase, remote, branch, options);
}
catch (err) {
if (err.gitErrorCode !== GitErrorCodes.TagConflict) {
throw err;
}
// Handle tag(s) conflict
if (await this.handleTagConflict(remote, err.stderr)) {
await this.repository.pull(rebase, remote, branch, options);
}
}
}
@throttle
async push(head: Branch, forcePushMode?: ForcePushMode): Promise<void> {
let remote: string | undefined;
@@ -1724,7 +1740,7 @@ export class Repository implements Disposable {
}
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
await this.repository.pull(rebase, remoteName, pullBranch, { tags, cancellationToken });
this._pullAndHandleTagConflict(rebase, remoteName, pullBranch, { tags, cancellationToken });
}
};
@@ -2476,6 +2492,38 @@ export class Repository implements Disposable {
return config.get<boolean>('optimisticUpdate') === true;
}
private async handleTagConflict(remote: string | undefined, raw: string): Promise<boolean> {
// Ensure there is a remote
remote = remote ?? this.HEAD?.upstream?.remote;
if (!remote) {
throw new Error('Unable to resolve tag conflict due to missing remote.');
}
// Extract tag names from message
const tags: string[] = [];
for (const match of raw.matchAll(/^ ! \[rejected\]\s+([^\s]+)\s+->\s+([^\s]+)\s+\(would clobber existing tag\)$/gm)) {
if (match.length === 3) {
tags.push(match[1]);
}
}
if (tags.length === 0) {
throw new Error(`Unable to extract tag names from error message: ${raw}`);
}
// Notification
const replaceLocalTags = l10n.t('Replace Local Tag(s)');
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(', '));
const choice = await window.showErrorMessage(message, { modal: true }, replaceLocalTags);
if (choice !== replaceLocalTags) {
return false;
}
// Force fetch tags
await this.repository.fetchTags({ remote, tags, force: true });
return true;
}
public isBranchProtected(name: string = this.HEAD?.name ?? ''): boolean {
return this.isBranchProtectedMatcher ? this.isBranchProtectedMatcher(name) : false;
}