mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 04:54:25 -07:00
feat(worktree): add Git worktree support for isolated parallel sessions (#22973)
This commit is contained in:
@@ -226,6 +226,51 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('parseArguments', () => {
|
||||
describe('worktree', () => {
|
||||
it('should parse --worktree flag when provided with a name', async () => {
|
||||
process.argv = ['node', 'script.js', '--worktree', 'my-feature'];
|
||||
const settings = createTestMergedSettings();
|
||||
settings.experimental.worktrees = true;
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.worktree).toBe('my-feature');
|
||||
});
|
||||
|
||||
it('should generate a random name when --worktree is provided without a name', async () => {
|
||||
process.argv = ['node', 'script.js', '--worktree'];
|
||||
const settings = createTestMergedSettings();
|
||||
settings.experimental.worktrees = true;
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.worktree).toBeDefined();
|
||||
expect(argv.worktree).not.toBe('');
|
||||
expect(typeof argv.worktree).toBe('string');
|
||||
});
|
||||
|
||||
it('should throw an error when --worktree is used but experimental.worktrees is not enabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--worktree', 'feature'];
|
||||
const settings = createTestMergedSettings();
|
||||
settings.experimental.worktrees = false;
|
||||
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
const mockConsoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments(settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'The --worktree flag is only available when experimental.worktrees is enabled in your settings.',
|
||||
),
|
||||
);
|
||||
|
||||
mockExit.mockRestore();
|
||||
mockConsoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description: 'long flags',
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import yargs from 'yargs/yargs';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import process from 'node:process';
|
||||
import * as path from 'node:path';
|
||||
import { execa } from 'execa';
|
||||
import { mcpCommand } from '../commands/mcp.js';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import { skillsCommand } from '../commands/skills.js';
|
||||
@@ -38,6 +39,9 @@ import {
|
||||
applyAdminAllowlist,
|
||||
applyRequiredServers,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
getProjectRootForWorktree,
|
||||
isGeminiWorktree,
|
||||
type WorktreeSettings,
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
@@ -48,6 +52,8 @@ import {
|
||||
type MergedSettings,
|
||||
saveModelChange,
|
||||
loadSettings,
|
||||
isWorktreeEnabled,
|
||||
type LoadedSettings,
|
||||
} from './settings.js';
|
||||
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
@@ -74,6 +80,7 @@ export interface CliArgs {
|
||||
debug: boolean | undefined;
|
||||
prompt: string | undefined;
|
||||
promptInteractive: string | undefined;
|
||||
worktree?: string;
|
||||
|
||||
yolo: boolean | undefined;
|
||||
approvalMode: string | undefined;
|
||||
@@ -115,6 +122,36 @@ const coerceCommaSeparated = (values: string[]): string[] => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pre-parses the command line arguments to find the worktree flag.
|
||||
* Used for early setup before full argument parsing with settings.
|
||||
*/
|
||||
export function getWorktreeArg(argv: string[]): string | undefined {
|
||||
const result = yargs(hideBin(argv))
|
||||
.help(false)
|
||||
.version(false)
|
||||
.option('worktree', { alias: 'w', type: 'string' })
|
||||
.strict(false)
|
||||
.exitProcess(false)
|
||||
.parseSync();
|
||||
|
||||
if (result.worktree === undefined) return undefined;
|
||||
return typeof result.worktree === 'string' ? result.worktree.trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a worktree is requested via CLI and enabled in settings.
|
||||
* Returns the requested name (can be empty string for auto-generated) or undefined.
|
||||
*/
|
||||
export function getRequestedWorktreeName(
|
||||
settings: LoadedSettings,
|
||||
): string | undefined {
|
||||
if (!isWorktreeEnabled(settings)) {
|
||||
return undefined;
|
||||
}
|
||||
return getWorktreeArg(process.argv);
|
||||
}
|
||||
|
||||
export async function parseArguments(
|
||||
settings: MergedSettings,
|
||||
): Promise<CliArgs> {
|
||||
@@ -158,6 +195,20 @@ export async function parseArguments(
|
||||
description:
|
||||
'Execute the provided prompt and continue in interactive mode',
|
||||
})
|
||||
.option('worktree', {
|
||||
alias: 'w',
|
||||
type: 'string',
|
||||
skipValidation: true,
|
||||
description:
|
||||
'Start Gemini in a new git worktree. If no name is provided, one is generated automatically.',
|
||||
coerce: (value: unknown): string => {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||
if (trimmed === '') {
|
||||
return Math.random().toString(36).substring(2, 10);
|
||||
}
|
||||
return trimmed;
|
||||
},
|
||||
})
|
||||
.option('sandbox', {
|
||||
alias: 's',
|
||||
type: 'boolean',
|
||||
@@ -335,6 +386,9 @@ export async function parseArguments(
|
||||
) {
|
||||
return `Invalid values:\n Argument: output-format, Given: "${argv['outputFormat']}", Choices: "text", "json", "stream-json"`;
|
||||
}
|
||||
if (argv['worktree'] && !settings.experimental?.worktrees) {
|
||||
return 'The --worktree flag is only available when experimental.worktrees is enabled in your settings.';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -420,6 +474,7 @@ export interface LoadCliConfigOptions {
|
||||
projectHooks?: { [K in HookEventName]?: HookDefinition[] } & {
|
||||
disabled?: string[];
|
||||
};
|
||||
worktreeSettings?: WorktreeSettings;
|
||||
}
|
||||
|
||||
export async function loadCliConfig(
|
||||
@@ -431,6 +486,9 @@ export async function loadCliConfig(
|
||||
const { cwd = process.cwd(), projectHooks } = options;
|
||||
const debugMode = isDebugMode(argv);
|
||||
|
||||
const worktreeSettings =
|
||||
options.worktreeSettings ?? (await resolveWorktreeSettings(cwd));
|
||||
|
||||
if (argv.sandbox) {
|
||||
process.env['GEMINI_SANDBOX'] = 'true';
|
||||
}
|
||||
@@ -802,6 +860,7 @@ export async function loadCliConfig(
|
||||
importFormat: settings.context?.importFormat,
|
||||
debugMode,
|
||||
question,
|
||||
worktreeSettings,
|
||||
|
||||
coreTools: settings.tools?.core || undefined,
|
||||
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
|
||||
@@ -943,3 +1002,48 @@ function mergeExcludeTools(
|
||||
]);
|
||||
return Array.from(allExcludeTools);
|
||||
}
|
||||
|
||||
async function resolveWorktreeSettings(
|
||||
cwd: string,
|
||||
): Promise<WorktreeSettings | undefined> {
|
||||
let worktreePath: string | undefined;
|
||||
try {
|
||||
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], {
|
||||
cwd,
|
||||
});
|
||||
const toplevel = stdout.trim();
|
||||
const projectRoot = await getProjectRootForWorktree(toplevel);
|
||||
|
||||
if (isGeminiWorktree(toplevel, projectRoot)) {
|
||||
worktreePath = toplevel;
|
||||
}
|
||||
} catch (_e) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!worktreePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let worktreeBaseSha: string | undefined;
|
||||
try {
|
||||
const { stdout } = await execa('git', ['rev-parse', 'HEAD'], {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
worktreeBaseSha = stdout.trim();
|
||||
} catch (e: unknown) {
|
||||
debugLogger.debug(
|
||||
`Failed to resolve worktree base SHA at ${worktreePath}: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!worktreeBaseSha) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: path.basename(worktreePath),
|
||||
path: worktreePath,
|
||||
baseSha: worktreeBaseSha,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -632,6 +632,10 @@ export function resetSettingsCacheForTesting() {
|
||||
settingsCache.clear();
|
||||
}
|
||||
|
||||
export function isWorktreeEnabled(settings: LoadedSettings): boolean {
|
||||
return settings.merged.experimental.worktrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads settings from user and workspace directories.
|
||||
* Project settings override user settings.
|
||||
|
||||
@@ -1906,6 +1906,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable local and remote subagents.',
|
||||
showInDialog: false,
|
||||
},
|
||||
worktrees: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Git Worktrees',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable automated Git worktree management for parallel work.',
|
||||
showInDialog: true,
|
||||
},
|
||||
extensionManagement: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Management',
|
||||
|
||||
@@ -199,6 +199,8 @@ vi.mock('./config/config.js', () => ({
|
||||
networkAccess: false,
|
||||
}),
|
||||
isDebugMode: vi.fn(() => false),
|
||||
getRequestedWorktreeName: vi.fn(() => undefined),
|
||||
getWorktreeArg: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock('read-package-up', () => ({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
WarningPriority,
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
type WorktreeInfo,
|
||||
type OutputPayload,
|
||||
type ConsoleLogPayload,
|
||||
type UserFeedbackPayload,
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
registerTelemetryConfig,
|
||||
setupSignalHandlers,
|
||||
} from './utils/cleanup.js';
|
||||
import { setupWorktree } from './utils/worktreeSetup.js';
|
||||
import {
|
||||
cleanupToolOutputFiles,
|
||||
cleanupExpiredSessions,
|
||||
@@ -210,6 +212,13 @@ export async function main() {
|
||||
const settings = loadSettings();
|
||||
loadSettingsHandle?.end();
|
||||
|
||||
// If a worktree is requested and enabled, set it up early.
|
||||
const requestedWorktree = cliConfig.getRequestedWorktreeName(settings);
|
||||
let worktreeInfo: WorktreeInfo | undefined;
|
||||
if (requestedWorktree !== undefined) {
|
||||
worktreeInfo = await setupWorktree(requestedWorktree || undefined);
|
||||
}
|
||||
|
||||
// Report settings errors once during startup
|
||||
settings.errors.forEach((error) => {
|
||||
coreEvents.emitFeedback('warning', error.message);
|
||||
@@ -426,6 +435,7 @@ export async function main() {
|
||||
const loadConfigHandle = startupProfiler.start('load_cli_config');
|
||||
const config = await loadCliConfig(settings.merged, sessionId, argv, {
|
||||
projectHooks: settings.workspace.settings.hooks,
|
||||
worktreeSettings: worktreeInfo,
|
||||
});
|
||||
loadConfigHandle?.end();
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ vi.mock('./config/config.js', () => ({
|
||||
} as unknown as Config),
|
||||
parseArguments: vi.fn().mockResolvedValue({}),
|
||||
isDebugMode: vi.fn(() => false),
|
||||
getRequestedWorktreeName: vi.fn(() => undefined),
|
||||
getWorktreeArg: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock('read-package-up', () => ({
|
||||
|
||||
@@ -8,10 +8,12 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { type SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import {
|
||||
ToolCallDecision,
|
||||
getShellConfiguration,
|
||||
type WorktreeSettings,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
@@ -24,19 +26,30 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof SessionContext>();
|
||||
const actual =
|
||||
await importOriginal<typeof import('../contexts/SessionContext.js')>();
|
||||
return {
|
||||
...actual,
|
||||
useSessionStats: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../contexts/ConfigContext.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../contexts/ConfigContext.js')>();
|
||||
return {
|
||||
...actual,
|
||||
useConfig: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const getShellConfigurationMock = vi.mocked(getShellConfiguration);
|
||||
const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||
|
||||
const renderWithMockedStats = async (
|
||||
metrics: SessionMetrics,
|
||||
sessionId = 'test-session',
|
||||
worktreeSettings?: WorktreeSettings,
|
||||
) => {
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
@@ -49,7 +62,11 @@ const renderWithMockedStats = async (
|
||||
|
||||
getPromptCount: () => 5,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
} as unknown as ReturnType<typeof SessionContext.useSessionStats>);
|
||||
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
getWorktreeSettings: () => worktreeSettings,
|
||||
} as never);
|
||||
|
||||
const result = await renderWithProviders(
|
||||
<SessionSummaryDisplay duration="1h 23m 45s" />,
|
||||
@@ -188,4 +205,30 @@ describe('<SessionSummaryDisplay />', () => {
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Worktree status', () => {
|
||||
it('renders worktree instructions when worktreeSettings are present', async () => {
|
||||
const worktreeSettings: WorktreeSettings = {
|
||||
name: 'foo-bar',
|
||||
path: '/path/to/foo-bar',
|
||||
baseSha: 'base-sha',
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = await renderWithMockedStats(
|
||||
emptyMetrics,
|
||||
'test-session',
|
||||
worktreeSettings,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('To resume work in this worktree:');
|
||||
expect(output).toContain(
|
||||
'cd /path/to/foo-bar && gemini --resume test-session',
|
||||
);
|
||||
expect(output).toContain(
|
||||
'To remove manually: git worktree remove /path/to/foo-bar',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type React from 'react';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { escapeShellArg, getShellConfiguration } from '@google/gemini-cli-core';
|
||||
|
||||
interface SessionSummaryDisplayProps {
|
||||
@@ -17,8 +18,19 @@ export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
|
||||
duration,
|
||||
}) => {
|
||||
const { stats } = useSessionStats();
|
||||
const config = useConfig();
|
||||
const { shell } = getShellConfiguration();
|
||||
const footer = `To resume this session: gemini --resume ${escapeShellArg(stats.sessionId, shell)}`;
|
||||
|
||||
const worktreeSettings = config.getWorktreeSettings();
|
||||
|
||||
const escapedSessionId = escapeShellArg(stats.sessionId, shell);
|
||||
let footer = `To resume this session: gemini --resume ${escapedSessionId}`;
|
||||
|
||||
if (worktreeSettings) {
|
||||
footer =
|
||||
`To resume work in this worktree: cd ${escapeShellArg(worktreeSettings.path, shell)} && gemini --resume ${escapedSessionId}\n` +
|
||||
`To remove manually: git worktree remove ${escapeShellArg(worktreeSettings.path, shell)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<StatsDisplay
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { setupWorktree } from './worktreeSetup.js';
|
||||
import * as coreFunctions from '@google/gemini-cli-core';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
getProjectRootForWorktree: vi.fn(),
|
||||
createWorktreeService: vi.fn(),
|
||||
debugLogger: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
writeToStdout: vi.fn(),
|
||||
writeToStderr: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('setupWorktree', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
const originalCwd = process.cwd;
|
||||
|
||||
const mockService = {
|
||||
setup: vi.fn(),
|
||||
maybeCleanup: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
process.env = { ...originalEnv };
|
||||
|
||||
// Mock process.cwd and process.chdir
|
||||
let currentPath = '/mock/project';
|
||||
process.cwd = vi.fn().mockImplementation(() => currentPath);
|
||||
process.chdir = vi.fn().mockImplementation((newPath) => {
|
||||
currentPath = newPath;
|
||||
});
|
||||
|
||||
// Mock successful execution of core utilities
|
||||
vi.mocked(coreFunctions.getProjectRootForWorktree).mockResolvedValue(
|
||||
'/mock/project',
|
||||
);
|
||||
vi.mocked(coreFunctions.createWorktreeService).mockResolvedValue(
|
||||
mockService as never,
|
||||
);
|
||||
mockService.setup.mockResolvedValue({
|
||||
name: 'my-feature',
|
||||
path: '/mock/project/.gemini/worktrees/my-feature',
|
||||
baseSha: 'base-sha',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
process.cwd = originalCwd;
|
||||
delete (process as { chdir?: typeof process.chdir }).chdir;
|
||||
});
|
||||
|
||||
it('should create and switch to a new worktree', async () => {
|
||||
await setupWorktree('my-feature');
|
||||
|
||||
expect(coreFunctions.getProjectRootForWorktree).toHaveBeenCalledWith(
|
||||
'/mock/project',
|
||||
);
|
||||
expect(coreFunctions.createWorktreeService).toHaveBeenCalledWith(
|
||||
'/mock/project',
|
||||
);
|
||||
expect(mockService.setup).toHaveBeenCalledWith('my-feature');
|
||||
expect(process.chdir).toHaveBeenCalledWith(
|
||||
'/mock/project/.gemini/worktrees/my-feature',
|
||||
);
|
||||
expect(process.env['GEMINI_CLI_WORKTREE_HANDLED']).toBe('1');
|
||||
});
|
||||
|
||||
it('should generate a name if worktreeName is undefined', async () => {
|
||||
mockService.setup.mockResolvedValue({
|
||||
name: 'generated-name',
|
||||
path: '/mock/project/.gemini/worktrees/generated-name',
|
||||
baseSha: 'base-sha',
|
||||
});
|
||||
|
||||
await setupWorktree(undefined);
|
||||
|
||||
expect(mockService.setup).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('should skip worktree creation if GEMINI_CLI_WORKTREE_HANDLED is set', async () => {
|
||||
process.env['GEMINI_CLI_WORKTREE_HANDLED'] = '1';
|
||||
|
||||
await setupWorktree('my-feature');
|
||||
|
||||
expect(coreFunctions.createWorktreeService).not.toHaveBeenCalled();
|
||||
expect(process.chdir).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors gracefully and exit', async () => {
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('PROCESS_EXIT');
|
||||
});
|
||||
|
||||
mockService.setup.mockRejectedValue(new Error('Git failure'));
|
||||
|
||||
await expect(setupWorktree('my-feature')).rejects.toThrow('PROCESS_EXIT');
|
||||
|
||||
expect(coreFunctions.writeToStderr).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Failed to create or switch to worktree: Git failure',
|
||||
),
|
||||
);
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
|
||||
mockExit.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
getProjectRootForWorktree,
|
||||
createWorktreeService,
|
||||
writeToStderr,
|
||||
type WorktreeInfo,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
/**
|
||||
* Sets up a git worktree for parallel sessions.
|
||||
*
|
||||
* This function uses a guard (GEMINI_CLI_WORKTREE_HANDLED) to ensure that
|
||||
* when the CLI relaunches itself (e.g. for memory allocation), it doesn't
|
||||
* attempt to create a nested worktree.
|
||||
*/
|
||||
export async function setupWorktree(
|
||||
worktreeName: string | undefined,
|
||||
): Promise<WorktreeInfo | undefined> {
|
||||
if (process.env['GEMINI_CLI_WORKTREE_HANDLED'] === '1') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const projectRoot = await getProjectRootForWorktree(process.cwd());
|
||||
const service = await createWorktreeService(projectRoot);
|
||||
|
||||
const worktreeInfo = await service.setup(worktreeName || undefined);
|
||||
|
||||
process.chdir(worktreeInfo.path);
|
||||
process.env['GEMINI_CLI_WORKTREE_HANDLED'] = '1';
|
||||
|
||||
return worktreeInfo;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
writeToStderr(`Failed to create or switch to worktree: ${errorMessage}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user