From 1dcfdf3faf867846be6f9033e745a98aa67e9ab4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:49:42 +0100 Subject: [PATCH 1/2] Git - use random name for copilot worktree branch names --- extensions/git/src/commands.ts | 45 +----------------------- extensions/git/src/repository.ts | 60 +++++++++++++++++++++++++++++++- extensions/git/src/util.ts | 6 ++-- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 1fc850565de8ed..f7f59619817444 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -7,7 +7,6 @@ import * as os from 'os'; import * as path from 'path'; import { Command, commands, Disposable, MessageOptions, Position, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages, SourceControlArtifact, ProgressLocation } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; -import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import type { CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref } from './api/git'; import { ForcePushMode, GitErrorCodes, RefType, Status } from './api/git.constants'; import { Git, GitError, Repository as GitRepository, Stash, Worktree } from './git'; @@ -2943,48 +2942,6 @@ export class CommandCenter { await this._branch(repository, undefined, true); } - private async generateRandomBranchName(repository: Repository, separator: string): Promise { - const config = workspace.getConfiguration('git'); - const branchRandomNameDictionary = config.get('branchRandomName.dictionary')!; - - const dictionaries: string[][] = []; - for (const dictionary of branchRandomNameDictionary) { - if (dictionary.toLowerCase() === 'adjectives') { - dictionaries.push(adjectives); - } - if (dictionary.toLowerCase() === 'animals') { - dictionaries.push(animals); - } - if (dictionary.toLowerCase() === 'colors') { - dictionaries.push(colors); - } - if (dictionary.toLowerCase() === 'numbers') { - dictionaries.push(NumberDictionary.generate({ length: 3 })); - } - } - - if (dictionaries.length === 0) { - return ''; - } - - // 5 attempts to generate a random branch name - for (let index = 0; index < 5; index++) { - const randomName = uniqueNamesGenerator({ - dictionaries, - length: dictionaries.length, - separator - }); - - // Check for local ref conflict - const refs = await repository.getRefs({ pattern: `refs/heads/${randomName}` }); - if (refs.length === 0) { - return randomName; - } - } - - return ''; - } - private async promptForBranchName(repository: Repository, defaultName?: string, initialValue?: string): Promise { const config = workspace.getConfiguration('git'); const branchPrefix = config.get('branchPrefix')!; @@ -2998,7 +2955,7 @@ export class CommandCenter { } const getBranchName = async (): Promise => { - const branchName = branchRandomNameEnabled ? await this.generateRandomBranchName(repository, branchWhitespaceChar) : ''; + const branchName = await repository.generateRandomBranchName() ?? ''; return `${branchPrefix}${branchName}`; }; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 6810f3cca42231..bfd1cacc431581 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import TelemetryReporter from '@vscode/extension-telemetry'; +import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; import * as path from 'path'; @@ -24,7 +25,7 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; -import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isCopilotWorktree, isDescendant, isLinuxSnap, isRemote, isWindows, Limiter, onceEvent, pathEquals, relativePath } from './util'; +import { anyEvent, combinedDisposable, CopilotWorktreeBranchPrefix, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isCopilotWorktree, isDescendant, isLinuxSnap, isRemote, isWindows, Limiter, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; import { GitArtifactProvider } from './artifactProvider'; @@ -1861,6 +1862,14 @@ export class Repository implements Disposable { let worktreeName: string | undefined; let { path: worktreePath, commitish, branch } = options || {}; + // Use random branch name for copilot worktree branches + if (branch && branch.indexOf(CopilotWorktreeBranchPrefix) !== -1) { + const randomBranchName = await this.generateRandomBranchName(); + if (randomBranchName) { + branch = `${branch.substring(0, branch.indexOf(CopilotWorktreeBranchPrefix) + CopilotWorktreeBranchPrefix.length)}${randomBranchName}`; + } + } + // Create worktree path based on the branch name if (worktreePath === undefined && branch !== undefined) { worktreeName = branch.startsWith(branchPrefix) @@ -3294,6 +3303,55 @@ export class Repository implements Disposable { return this.unpublishedCommits; } + async generateRandomBranchName(): Promise { + const config = workspace.getConfiguration('git', Uri.file(this.root)); + const branchRandomNameEnabled = config.get('branchRandomName.enable', false); + + if (!branchRandomNameEnabled) { + return undefined; + } + + const branchWhitespaceChar = config.get('branchWhitespaceChar', '-'); + const branchRandomNameDictionary = config.get('branchRandomName.dictionary', ['adjectives', 'animals']); + + const dictionaries: string[][] = []; + for (const dictionary of branchRandomNameDictionary) { + if (dictionary.toLowerCase() === 'adjectives') { + dictionaries.push(adjectives); + } + if (dictionary.toLowerCase() === 'animals') { + dictionaries.push(animals); + } + if (dictionary.toLowerCase() === 'colors') { + dictionaries.push(colors); + } + if (dictionary.toLowerCase() === 'numbers') { + dictionaries.push(NumberDictionary.generate({ length: 3 })); + } + } + + if (dictionaries.length === 0) { + return undefined; + } + + // 5 attempts to generate a random branch name + for (let index = 0; index < 5; index++) { + const randomName = uniqueNamesGenerator({ + dictionaries, + length: dictionaries.length, + separator: branchWhitespaceChar + }); + + // Check for local ref conflict + const refs = await this.getRefs({ pattern: `refs/heads/${randomName}` }); + if (refs.length === 0) { + return randomName; + } + } + + return undefined; + } + dispose(): void { this.disposables = dispose(this.disposables); } diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index c6ec6ece45c693..cbf1b56e34e511 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -867,10 +867,12 @@ export function getStashDescription(stash: Stash): string | undefined { return descriptionSegments.join(' \u2022 '); } +export const CopilotWorktreeBranchPrefix = 'copilot-worktree-'; + export function isCopilotWorktree(path: string): boolean { const lastSepIndex = path.lastIndexOf(sep); return lastSepIndex !== -1 - ? path.substring(lastSepIndex + 1).startsWith('copilot-worktree-') - : path.startsWith('copilot-worktree-'); + ? path.substring(lastSepIndex + 1).startsWith(CopilotWorktreeBranchPrefix) + : path.startsWith(CopilotWorktreeBranchPrefix); } From c7a3d91433b37fc634c1f29da8f997c721d99030 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:05:02 +0100 Subject: [PATCH 2/2] Expose random name generation through git extension API --- extensions/git/src/api/api1.ts | 4 ++++ extensions/git/src/api/git.d.ts | 2 ++ extensions/git/src/commands.ts | 3 +-- extensions/git/src/repository.ts | 15 ++++----------- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index abe5c331074229..e5820c0ded74af 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -347,6 +347,10 @@ export class ApiRepository implements Repository { migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise { return this.#repository.migrateChanges(sourceRepositoryPath, options); } + + generateRandomBranchName(): Promise { + return this.#repository.generateRandomBranchName(); + } } export class ApiGit implements Git { diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 287dd4399bf2cc..122134c2c8b57b 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -325,6 +325,8 @@ export interface Repository { deleteWorktree(path: string, options?: { force?: boolean }): Promise; migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise; + + generateRandomBranchName(): Promise; } export interface RemoteSource { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index f7f59619817444..15f962b430703a 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2955,8 +2955,7 @@ export class CommandCenter { } const getBranchName = async (): Promise => { - const branchName = await repository.generateRandomBranchName() ?? ''; - return `${branchPrefix}${branchName}`; + return await repository.generateRandomBranchName() ?? branchPrefix; }; const getValueSelection = (value: string): [number, number] | undefined => { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index bfd1cacc431581..b79bb3bc4aabf6 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -25,7 +25,7 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; -import { anyEvent, combinedDisposable, CopilotWorktreeBranchPrefix, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isCopilotWorktree, isDescendant, isLinuxSnap, isRemote, isWindows, Limiter, onceEvent, pathEquals, relativePath } from './util'; +import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isCopilotWorktree, isDescendant, isLinuxSnap, isRemote, isWindows, Limiter, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; import { GitArtifactProvider } from './artifactProvider'; @@ -1862,14 +1862,6 @@ export class Repository implements Disposable { let worktreeName: string | undefined; let { path: worktreePath, commitish, branch } = options || {}; - // Use random branch name for copilot worktree branches - if (branch && branch.indexOf(CopilotWorktreeBranchPrefix) !== -1) { - const randomBranchName = await this.generateRandomBranchName(); - if (randomBranchName) { - branch = `${branch.substring(0, branch.indexOf(CopilotWorktreeBranchPrefix) + CopilotWorktreeBranchPrefix.length)}${randomBranchName}`; - } - } - // Create worktree path based on the branch name if (worktreePath === undefined && branch !== undefined) { worktreeName = branch.startsWith(branchPrefix) @@ -3311,6 +3303,7 @@ export class Repository implements Disposable { return undefined; } + const branchPrefix = config.get('branchPrefix', ''); const branchWhitespaceChar = config.get('branchWhitespaceChar', '-'); const branchRandomNameDictionary = config.get('branchRandomName.dictionary', ['adjectives', 'animals']); @@ -3343,9 +3336,9 @@ export class Repository implements Disposable { }); // Check for local ref conflict - const refs = await this.getRefs({ pattern: `refs/heads/${randomName}` }); + const refs = await this.getRefs({ pattern: `refs/heads/${branchPrefix}${randomName}` }); if (refs.length === 0) { - return randomName; + return `${branchPrefix}${randomName}`; } }