diff --git a/extensions/git/package.json b/extensions/git/package.json index 89287db3dec..3bc3a7b4af7 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2340,6 +2340,12 @@ "default": false, "description": "%config.rebaseWhenSync%" }, + "git.fetchBeforeCheckout": { + "type": "boolean", + "scope": "resource", + "default": false, + "description": "%config.fetchBeforeCheckout%" + }, "git.fetchOnPull": { "type": "boolean", "scope": "resource", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 9cb0eedbf1d..683e7142348 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -200,6 +200,7 @@ "config.rebaseWhenSync": "Force git to use rebase when running the sync command.", "config.confirmEmptyCommits": "Always confirm the creation of empty commits for the 'Git: Commit Empty' command.", "config.fetchOnPull": "When enabled, fetch all branches when pulling. Otherwise, fetch just the current one.", + "config.fetchBeforeCheckout": "Controls whether a branch that does not have outgoing commits is fast-forwarded before it is checked out.", "config.pullTags": "Fetch all tags when pulling.", "config.pruneOnFetch": "Prune when fetching.", "config.autoStash": "Stash any changes before pulling and restore them after successful pull.", diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 2e10affa154..ddfd457a088 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -350,5 +350,6 @@ export const enum GitErrorCodes { PatchDoesNotApply = 'PatchDoesNotApply', NoPathFound = 'NoPathFound', UnknownPath = 'UnknownPath', - EmptyCommitMessage = 'EmptyCommitMessage' + EmptyCommitMessage = 'EmptyCommitMessage', + BranchFastForwardRejected = 'BranchFastForwardRejected' } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 71628013c54..c52ec4bc929 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -33,13 +33,18 @@ class CheckoutItem implements QuickPickItem { constructor(protected repository: Repository, protected ref: Ref) { } async run(opts?: { detached?: boolean }): Promise { - const ref = this.ref.name; - - if (!ref) { + if (!this.ref.name) { return; } - await this.repository.checkout(ref, opts); + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const fetchBeforeCheckout = config.get('fetchBeforeCheckout', false) === true; + + if (fetchBeforeCheckout) { + await this.repository.fastForwardBranch(this.ref.name!); + } + + await this.repository.checkout(this.ref.name, opts); } } @@ -49,6 +54,14 @@ class CheckoutTagItem extends CheckoutItem { override get description(): string { return localize('tag at', "Tag at {0}", this.shortCommit); } + + override async run(opts?: { detached?: boolean }): Promise { + if (!this.ref.name) { + return; + } + + await this.repository.checkout(this.ref.name, opts); + } } class CheckoutRemoteHeadItem extends CheckoutItem { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 9e351566f60..205ba569cdd 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1708,6 +1708,9 @@ export class Repository { err.gitErrorCode = GitErrorCodes.NoRemoteRepositorySpecified; } else if (/Could not read from remote repository/.test(err.stderr || '')) { err.gitErrorCode = GitErrorCodes.RemoteConnectionError; + } else if (/! \[rejected\].*\(non-fast-forward\)/m.test(err.stderr || '')) { + // The local branch has outgoing changes and it cannot be fast-forwarded. + err.gitErrorCode = GitErrorCodes.BranchFastForwardRejected; } throw err; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 8f7dee12e54..d622fd23c87 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1363,6 +1363,27 @@ export class Repository implements Disposable { await this.run(Operation.RenameBranch, () => this.repository.renameBranch(name)); } + @throttle + async fastForwardBranch(name: string): Promise { + // Get branch details + const branch = await this.getBranch(name); + if (!branch.upstream?.remote || !branch.upstream?.name || !branch.name) { + return; + } + + try { + // Fast-forward the branch if possible + const options = { remote: branch.upstream.remote, ref: `${branch.upstream.name}:${branch.name}` }; + await this.run(Operation.Fetch, async () => this.repository.fetch(options)); + } catch (err) { + if (err.gitErrorCode === GitErrorCodes.BranchFastForwardRejected) { + return; + } + + throw err; + } + } + async cherryPick(commitHash: string): Promise { await this.run(Operation.CherryPick, () => this.repository.cherryPick(commitHash)); }