Skip to content

Handle plugin marketplace pull recovery#318270

Open
aaronpowell wants to merge 5 commits into
microsoft:mainfrom
aaronpowell:aaronpowell/plugin-update-failure
Open

Handle plugin marketplace pull recovery#318270
aaronpowell wants to merge 5 commits into
microsoft:mainfrom
aaronpowell:aaronpowell/plugin-update-failure

Conversation

@aaronpowell
Copy link
Copy Markdown
Contributor

Force-pushed marketplace branches can leave cached plugin repos in a diverged state, causing Chat: Update Plugins to fail repeatedly. This change makes plugin marketplace sync more resilient without relying on brittle git error-message matching.

What changed

  • Updated LocalGitService.pull to use state-based failover:
    • attempt git pull --ff-only
    • if it fails, git fetch --prune and retry git pull --ff-only
    • if retries still fail, only hard-reset when the repo is clean and truly diverged (both ahead and behind @{u})
  • Added a user-facing recovery action in plugin update failures:
    • Purge Marketplace Cache and Reclone
    • removes the cached marketplace repo, reclones it, and prompts retry
  • Added targeted unit tests for the new pull behavior:
    • ff-only success
    • fetch + retry success
    • divergence fallback to hard reset
    • dirty-tree guard (no hard reset)
    • non-divergent retry failure passthrough

Notes

Use state-based pull failover for plugin marketplace repos: retry ff-only pull after fetch, then hard-reset only for clean, truly diverged repos. Also add a user-facing purge-and-reclone recovery action when update still fails.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 26, 2026 01:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR improves resilience of plugin marketplace updates by adding recovery options in the UI and making git pulls more robust (retrying after fetch and optionally recovering from diverged histories).

Changes:

  • Add a notification action to purge the marketplace cache and reclone when plugin update fails.
  • Enhance LocalGitService.pull() with a fetch + retry strategy and optional hard-reset recovery when history is diverged and the working tree is clean.
  • Add targeted unit tests covering the new pull() recovery behaviors.
Show a summary per file
File Description
src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts Adds “Purge Marketplace Cache and Reclone” action and implements purge+reclone workflow with progress + notifications.
src/vs/platform/git/node/localGitService.ts Implements pull retry after fetch and (in specific cases) hard reset to upstream when repo is diverged.
src/vs/platform/git/test/node/localGitService.test.ts Adds test coverage for pull retry and diverged-history recovery scenarios.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 3

Comment thread src/vs/workbench/contrib/chat/browser/agentPluginRepositoryService.ts Outdated
Comment on lines +53 to +70
try {
await this._exec(operationId, ['pull', '--ff-only'], repoPath);
} catch (err) {
this._logService.warn(`[LocalGitService] Fast-forward pull failed for ${repoPath}. Retrying after fetch.`);
await this._exec(operationId, ['fetch', '--prune'], repoPath);

try {
await this._exec(operationId, ['pull', '--ff-only'], repoPath);
} catch (retryErr) {
const upstream = await this._getSafeHardResetTarget(operationId, repoPath);
if (!upstream) {
throw retryErr;
}

this._logService.warn(`[LocalGitService] Pull retries exhausted for ${repoPath}. Performing hard reset to ${upstream}.`);
await this._exec(operationId, ['reset', '--hard', upstream], repoPath);
}
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will be fine as this code path isn't intended for use by the user.


private async _revListCount(operationId: string, repoPath: string, fromRef: string, toRef: string): Promise<number> {
const result = await this._exec(operationId, ['rev-list', '--count', `${fromRef}..${toRef}`], repoPath);
return Number(result.trim()) || 0;
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@aaronpowell aaronpowell requested a review from Copilot May 26, 2026 01:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 5

Comment on lines +50 to +70
async pull(operationId: string, repoPath: string): Promise<boolean> {
const before = (await this._exec(operationId, ['rev-parse', 'HEAD'], repoPath)).trim();
await this._exec(operationId, ['pull', '--ff-only'], repoPath);

try {
await this._exec(operationId, ['pull', '--ff-only'], repoPath);
} catch (err) {
this._logService.warn(`[LocalGitService] Fast-forward pull failed for ${repoPath}. Retrying after fetch.`);
await this._exec(operationId, ['fetch', '--prune'], repoPath);

try {
await this._exec(operationId, ['pull', '--ff-only'], repoPath);
} catch (retryErr) {
const upstream = await this._getSafeHardResetTarget(operationId, repoPath);
if (!upstream) {
throw retryErr;
}

this._logService.warn(`[LocalGitService] Pull retries exhausted for ${repoPath}. Performing hard reset to ${upstream}.`);
await this._exec(operationId, ['reset', '--hard', upstream], repoPath);
}
}
Comment on lines +55 to +57
} catch (err) {
this._logService.warn(`[LocalGitService] Fast-forward pull failed for ${repoPath}. Retrying after fetch.`);
await this._exec(operationId, ['fetch', '--prune'], repoPath);
Comment on lines +76 to +87
private async _getSafeHardResetTarget(operationId: string, repoPath: string): Promise<string | undefined> {
const status = (await this._exec(operationId, ['status', '--porcelain'], repoPath)).trim();
if (status.length > 0) {
return undefined;
}

let upstream: string;
try {
upstream = (await this._exec(operationId, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], repoPath)).trim();
} catch {
return undefined;
}
Comment on lines +178 to +182
const primaryActions = [new Action('showGitOutput', localize('showGitOutput', "Show Git Output"), undefined, true, () => this._commandService.executeCommand('git.showOutput'))];

if (marketplace.kind !== MarketplaceReferenceKind.LocalFileUri) {
primaryActions.push(new Action('purgeAndRecloneMarketplace', localize('purgeAndRecloneMarketplace', "Purge Marketplace Cache and Reclone"), undefined, true, () => this._purgeAndRecloneMarketplace(marketplace, options?.marketplaceType, updateLabel)));
}
async () => {
const exists = await this._fileService.exists(repoDir);
if (exists) {
await this._fileService.del(repoDir, { recursive: true });
aaronpowell and others added 3 commits May 26, 2026 11:32
Make hard-reset recovery opt-in for LocalGitService.pull and enable it only for plugin marketplace sync. Also return promises from notification actions, align failure labels, avoid trash during cache purge, tighten rev-list parsing, and add coverage for no-upstream and opt-in behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Inject execFile into LocalGitService so unit tests can provide a fake implementation instead of stubbing child_process.execFile. This avoids the ESM stub failure in Linux Electron CI and keeps the recovery coverage intact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Defer the fake execFile callback with queueMicrotask so LocalGitService sees the child process registration before the callback fires. This matches the real async contract and avoids the Canceled path in Electron unit tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@connor4312
Copy link
Copy Markdown
Member

I would actually be okay if we just wanted to replace the git pull flow with git fetch + git checkout origin/<ref> entirely

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants