mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-29 13:03:42 +01:00
Git - Handle tag conflict during pull operation (#167278)
Handle tag conflict during pull operation
This commit is contained in:
3
extensions/git/src/api/git.d.ts
vendored
3
extensions/git/src/api/git.d.ts
vendored
@@ -351,5 +351,6 @@ export const enum GitErrorCodes {
|
||||
NoPathFound = 'NoPathFound',
|
||||
UnknownPath = 'UnknownPath',
|
||||
EmptyCommitMessage = 'EmptyCommitMessage',
|
||||
BranchFastForwardRejected = 'BranchFastForwardRejected'
|
||||
BranchFastForwardRejected = 'BranchFastForwardRejected',
|
||||
TagConflict = 'TagConflict'
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user