diff --git a/CHANGELOG.md b/CHANGELOG.md index 73639cf1a84c7..ffdb34906877b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Fixed +- Fixes creating a branch from a remote ref incorrectly setting upstream tracking when the new branch name differs from the remote branch name — e.g. creating `feature/foo` from `origin/main` no longer makes it track `origin/main`; affects _Create & Switch to Branch_, _Switch to... → Create & Switch to New Local Branch_, and _Create Branch in New Worktree_ ([#5360](https://github.com/gitkraken/vscode-gitlens/issues/5360)) - Fixes _Keep Staged_ not keeping staged changes when stashing selected files — choosing _Keep Staged_ while stashing specific tracked files no longer drops the `--keep-index` flag, so staged changes are correctly kept intact ([#5281](https://github.com/gitkraken/vscode-gitlens/issues/5281)) - Fixes pushing a branch that needs a force-push (e.g. after an amend or rebase) silently reporting success without updating the remote — a non-fast-forward (_tip of your current branch is behind_) rejection is now surfaced as an error instead of being swallowed as non-fatal ([#5364](https://github.com/gitkraken/vscode-gitlens/issues/5364)) - Fixes _Fetch_, _Pull_, _Switch_, _Reset_, and _Restore_ operations silently reporting success when the underlying Git command failed with a message Git treats as a warning (e.g. an unreachable remote, or an invalid ref/revision) — these failures are now surfaced as errors instead of being swallowed as non-fatal diff --git a/packages/git-cli/src/providers/__tests__/operations.test.ts b/packages/git-cli/src/providers/__tests__/operations.test.ts index 8b6053f9a3fba..0376462cfd82a 100644 --- a/packages/git-cli/src/providers/__tests__/operations.test.ts +++ b/packages/git-cli/src/providers/__tests__/operations.test.ts @@ -144,6 +144,33 @@ suite('OperationsGitSubProvider Test Suite', () => { ); }); + test('checkout with createBranch passes -b and ref', async () => { + await operations.checkout(repoPath, 'origin/main', { createBranch: 'feature/foo' }); + + const call = (gitStub.run as sinon.SinonStub).getCalls().find(c => c.args.includes('checkout')); + assert.ok(call, 'expected git.run to be invoked with checkout'); + const gitArgs = call.args.filter((a): a is string => typeof a === 'string'); + assert.deepStrictEqual(gitArgs, ['checkout', '-b', 'feature/foo', 'origin/main', '--']); + }); + + test('checkout with createBranch and noTracking passes --no-track', async () => { + await operations.checkout(repoPath, 'origin/main', { createBranch: 'feature/foo', noTracking: true }); + + const call = (gitStub.run as sinon.SinonStub).getCalls().find(c => c.args.includes('checkout')); + assert.ok(call, 'expected git.run to be invoked with checkout'); + const gitArgs = call.args.filter((a): a is string => typeof a === 'string'); + assert.deepStrictEqual(gitArgs, ['checkout', '-b', 'feature/foo', '--no-track', 'origin/main', '--']); + }); + + test('checkout with createBranch and noTracking=false omits --no-track', async () => { + await operations.checkout(repoPath, 'origin/main', { createBranch: 'feature/foo', noTracking: false }); + + const call = (gitStub.run as sinon.SinonStub).getCalls().find(c => c.args.includes('checkout')); + assert.ok(call, 'expected git.run to be invoked with checkout'); + const gitArgs = call.args.filter((a): a is string => typeof a === 'string'); + assert.deepStrictEqual(gitArgs, ['checkout', '-b', 'feature/foo', 'origin/main', '--']); + }); + test('checkout surfaces an invalid-ref failure as CheckoutError', async () => { // `unknownRevision` is a GitWarning, so without `errors: 'throw'` the default handler swallows // it and the checkout resolves as if it succeeded. diff --git a/packages/git-cli/src/providers/operations.ts b/packages/git-cli/src/providers/operations.ts index 26dbc1586f243..9b7fd601f34b4 100644 --- a/packages/git-cli/src/providers/operations.ts +++ b/packages/git-cli/src/providers/operations.ts @@ -52,14 +52,18 @@ export class OperationsGitSubProvider implements GitOperationsSubProvider { async checkout( repoPath: string, ref: string, - options?: { createBranch?: string }, + options?: { createBranch?: string; noTracking?: boolean }, runOptions?: GitOperationRunOptions, ): Promise { const scope = getScopedLogger(); const params = ['checkout']; if (options?.createBranch) { - params.push('-b', options.createBranch, ref, '--'); + params.push('-b', options.createBranch); + if (options.noTracking) { + params.push('--no-track'); + } + params.push(ref, '--'); } else { params.push(ref, '--'); } diff --git a/packages/git-cli/src/providers/worktrees.ts b/packages/git-cli/src/providers/worktrees.ts index 3c52e4d6b4996..6e1ef51142050 100644 --- a/packages/git-cli/src/providers/worktrees.ts +++ b/packages/git-cli/src/providers/worktrees.ts @@ -29,7 +29,13 @@ export class WorktreesGitSubProvider implements GitWorktreesSubProvider { async createWorktree( repoPath: string, path: string, - options?: { commitish?: string; createBranch?: string; detach?: boolean; force?: boolean }, + options?: { + commitish?: string; + createBranch?: string; + detach?: boolean; + force?: boolean; + noTracking?: boolean; + }, ): Promise { const scope = getScopedLogger(); @@ -40,6 +46,9 @@ export class WorktreesGitSubProvider implements GitWorktreesSubProvider { if (options?.createBranch) { args.push('-b', options.createBranch); } + if (options?.createBranch && options.noTracking) { + args.push('--no-track'); + } if (options?.detach) { args.push('--detach'); } @@ -76,7 +85,13 @@ export class WorktreesGitSubProvider implements GitWorktreesSubProvider { async createWorktreeWithResult( repoPath: string, path: string, - options?: { commitish?: string; createBranch?: string; detach?: boolean; force?: boolean }, + options?: { + commitish?: string; + createBranch?: string; + detach?: boolean; + force?: boolean; + noTracking?: boolean; + }, ): Promise { await this.createWorktree(repoPath, path, options); const normalized = normalizePath(path); diff --git a/packages/git/src/providers/operations.ts b/packages/git/src/providers/operations.ts index a091076477587..150680750911e 100644 --- a/packages/git/src/providers/operations.ts +++ b/packages/git/src/providers/operations.ts @@ -28,7 +28,7 @@ export interface GitOperationsSubProvider { checkout( repoPath: string, ref: string, - options?: { createBranch?: string | undefined }, + options?: { createBranch?: string | undefined; noTracking?: boolean }, runOptions?: GitOperationRunOptions, ): Promise; cherryPick( diff --git a/packages/git/src/providers/worktrees.ts b/packages/git/src/providers/worktrees.ts index ba556342991cb..ff71feb995632 100644 --- a/packages/git/src/providers/worktrees.ts +++ b/packages/git/src/providers/worktrees.ts @@ -5,12 +5,24 @@ export interface GitWorktreesSubProvider { createWorktree( repoPath: string, path: string, - options?: { commitish?: string; createBranch?: string; detach?: boolean; force?: boolean }, + options?: { + commitish?: string; + createBranch?: string; + detach?: boolean; + force?: boolean; + noTracking?: boolean; + }, ): Promise; createWorktreeWithResult( repoPath: string, path: string, - options?: { commitish?: string; createBranch?: string; detach?: boolean; force?: boolean }, + options?: { + commitish?: string; + createBranch?: string; + detach?: boolean; + force?: boolean; + noTracking?: boolean; + }, ): Promise; getWorktree( repoPath: string, diff --git a/src/commands/git/branch/create.ts b/src/commands/git/branch/create.ts index f2a62feeb00f3..7e962b23a446e 100644 --- a/src/commands/git/branch/create.ts +++ b/src/commands/git/branch/create.ts @@ -263,7 +263,10 @@ export class BranchCreateGitCommand extends QuickCommand { steps.markStepsComplete(); if (state.flags.includes('--switch')) { - await state.repo.git.switch(state.reference.ref, { createBranch: state.name }); + await state.repo.git.switch(state.reference.ref, { + createBranch: state.name, + ...(isRemoteBranch && state.name !== remoteBranchName ? { noTracking: true } : undefined), + }); } else { try { await state.repo.git.branches.createBranch?.( diff --git a/src/commands/git/switch.ts b/src/commands/git/switch.ts index 158bb8db9e055..c77c539d20332 100644 --- a/src/commands/git/switch.ts +++ b/src/commands/git/switch.ts @@ -92,6 +92,9 @@ export class SwitchGitCommand extends QuickCommand { } private async execute(state: StepState>) { + const isRemoteBranch = isBranchReference(state.reference) && state.reference.remote; + const remoteBranchName = isRemoteBranch ? getReferenceNameWithoutRemote(state.reference) : undefined; + await window.withProgress( { location: ProgressLocation.Notification, @@ -104,7 +107,13 @@ export class SwitchGitCommand extends QuickCommand { () => Promise.all( state.repos.map(r => - r.git.switch(state.reference.ref, { createBranch: state.createBranch, progress: false }), + r.git.switch(state.reference.ref, { + createBranch: state.createBranch, + ...(isRemoteBranch && state.createBranch !== remoteBranchName + ? { noTracking: true } + : undefined), + progress: false, + }), ), ), ); diff --git a/src/commands/git/worktree/create.ts b/src/commands/git/worktree/create.ts index c0ca932944857..e712fb07218ef 100644 --- a/src/commands/git/worktree/create.ts +++ b/src/commands/git/worktree/create.ts @@ -218,6 +218,7 @@ export class WorktreeCreateGitCommand extends QuickCommand { : undefined; const isRemoteBranch = isBranchReference(state.reference) && state.reference?.remote; + const remoteBranchName = isRemoteBranch ? getReferenceNameWithoutRemote(state.reference) : undefined; if ( (isRemoteBranch || isRevisionReference(state.reference) || state.worktree != null) && !state.flags.includes('-b') @@ -358,6 +359,9 @@ export class WorktreeCreateGitCommand extends QuickCommand { createBranch: state.flags.includes('-b') ? state.createBranch : undefined, detach: state.flags.includes('--detach'), force: state.flags.includes('--force'), + ...(isRemoteBranch && state.createBranch !== remoteBranchName + ? { noTracking: true } + : undefined), }); state.result?.fulfill(worktree); diff --git a/src/git/gitRepositoryService.ts b/src/git/gitRepositoryService.ts index 69c93d155965e..20e665026d898 100644 --- a/src/git/gitRepositoryService.ts +++ b/src/git/gitRepositoryService.ts @@ -513,7 +513,10 @@ export class GitRepositoryService { @gate() @debug() - async switch(ref: string, options?: { createBranch?: string | undefined; progress?: boolean }): Promise { + async switch( + ref: string, + options?: { createBranch?: string | undefined; noTracking?: boolean; progress?: boolean }, + ): Promise { const { progress, ...opts } = { progress: true, ...options }; if (!progress) return this.switchCore(ref, opts); @@ -528,7 +531,7 @@ export class GitRepositoryService { ); } - private async switchCore(ref: string, options?: { createBranch?: string }) { + private async switchCore(ref: string, options?: { createBranch?: string; noTracking?: boolean }) { try { await this.ops?.checkout(ref, options); } catch (ex) {