Skip to content

Commit 33a34ad

Browse files
committed
Mount a worktree's common folder for Git operations
1 parent b4651df commit 33a34ad

3 files changed

Lines changed: 475 additions & 5 deletions

File tree

src/spec-node/singleContainer.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export async function openDockerfileDevContainer(params: DockerResolverParameter
5151
// collapsedFeaturesConfig = async () => res.collapsedFeaturesConfig;
5252

5353
try {
54-
await spawnDevContainer(params, config, mergedConfig, updatedImageName, idLabels, workspaceConfig.workspaceMount, res.imageDetails, containerUser, res.labels || {});
54+
await spawnDevContainer(params, config, mergedConfig, updatedImageName, idLabels, workspaceConfig.workspaceMount, workspaceConfig.additionalMountString, res.imageDetails, containerUser, res.labels || {});
5555
} finally {
5656
// In 'finally' because 'docker run' can fail after creating the container.
5757
// Trying to get it here, so we can offer 'Rebuild Container' as an action later.
@@ -348,7 +348,7 @@ export async function extraRunArgs(common: ResolverParameters, params: DockerRes
348348
return extraArguments;
349349
}
350350

351-
export async function spawnDevContainer(params: DockerResolverParameters, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig, mergedConfig: MergedDevContainerConfig, imageName: string, labels: string[], workspaceMount: string | undefined, imageDetails: () => Promise<ImageDetails>, containerUser: string | undefined, extraLabels: Record<string, string>) {
351+
export async function spawnDevContainer(params: DockerResolverParameters, config: DevContainerFromDockerfileConfig | DevContainerFromImageConfig, mergedConfig: MergedDevContainerConfig, imageName: string, labels: string[], workspaceMount: string | undefined, additionalMountString: string | undefined, imageDetails: () => Promise<ImageDetails>, containerUser: string | undefined, extraLabels: Record<string, string>) {
352352
const { common } = params;
353353
common.progress(ResolverProgress.StartingContainer);
354354

@@ -357,6 +357,7 @@ export async function spawnDevContainer(params: DockerResolverParameters, config
357357
const exposed = (<string[]>[]).concat(...exposedPorts.map(port => ['-p', typeof port === 'number' ? `127.0.0.1:${port}:${port}` : port]));
358358

359359
const cwdMount = workspaceMount ? ['--mount', workspaceMount] : [];
360+
const additionalMount = additionalMountString ? ['--mount', additionalMountString] : [];
360361

361362
const envObj = mergedConfig.containerEnv || {};
362363
const containerEnv = Object.keys(envObj)
@@ -409,6 +410,7 @@ while sleep 1 & wait $!; do :; done`, '-']; // `wait $!` allows for the `trap` t
409410
'-a', 'STDERR',
410411
...exposed,
411412
...cwdMount,
413+
...additionalMount,
412414
...featureMounts,
413415
...getLabels(labels),
414416
...containerEnv,

src/spec-node/utils.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,24 +347,58 @@ export async function getHostMountFolder(cliHost: CLIHost, folderPath: string, m
347347
export interface WorkspaceConfiguration {
348348
workspaceMount: string | undefined;
349349
workspaceFolder: string | undefined;
350+
additionalMountString: string | undefined;
350351
}
351352

352353
export async function getWorkspaceConfiguration(cliHost: CLIHost, workspace: Workspace | undefined, config: DevContainerConfig, mountWorkspaceGitRoot: boolean, output: Log, consistency?: BindMountConsistency): Promise<WorkspaceConfiguration> {
353354
if ('dockerComposeFile' in config) {
354355
return {
355356
workspaceFolder: getRemoteWorkspaceFolder(config),
356357
workspaceMount: undefined,
358+
additionalMountString: undefined,
357359
};
358360
}
359361
let { workspaceFolder, workspaceMount } = config;
362+
let additionalMountString: string | undefined;
360363
if (workspace && (!workspaceFolder || !('workspaceMount' in config))) {
361364
const hostMountFolder = await getHostMountFolder(cliHost, workspace.rootFolderPath, mountWorkspaceGitRoot, output);
365+
366+
// Check if .git is a file (worktree) with a relative gitdir path
367+
let containerMountFolder = path.posix.join('/workspaces', cliHost.path.basename(hostMountFolder));
368+
if (mountWorkspaceGitRoot) {
369+
const dotGitPath = cliHost.path.join(hostMountFolder, '.git');
370+
if (await cliHost.isFile(dotGitPath)) {
371+
const dotGitContent = (await cliHost.readFile(dotGitPath)).toString();
372+
const match = /^gitdir:\s*(.+)$/m.exec(dotGitContent);
373+
if (match) {
374+
const gitdir = match[1];
375+
// Only handle if gitdir is a relative path
376+
if (!cliHost.path.isAbsolute(gitdir)) {
377+
// gitdir points to .git/worktrees/<name>/, common dir is .git/ (two levels up)
378+
const gitCommonDir = cliHost.path.resolve(hostMountFolder, gitdir, '..', '..');
379+
// Collect path segments from hostMountFolder up to the parent of gitCommonDir
380+
const segments: string[] = [];
381+
for (let current = hostMountFolder; !gitCommonDir.startsWith(current + cliHost.path.sep) && current !== cliHost.path.dirname(current); current = cliHost.path.dirname(current)) {
382+
segments.unshift(cliHost.path.basename(current));
383+
}
384+
containerMountFolder = path.posix.join('/workspaces', ...segments);
385+
// Calculate where the common dir should be mounted in the container
386+
const containerGitdir = cliHost.platform === 'win32' ? gitdir.replace(/\\/g, '/') : gitdir;
387+
const containerGitCommonDir = path.posix.resolve(containerMountFolder, containerGitdir, '..', '..');
388+
const cons = cliHost.platform !== 'linux' ? `,consistency=${consistency || 'consistent'}` : '';
389+
const srcQuote = gitCommonDir.indexOf(',') !== -1 ? '"' : '';
390+
const tgtQuote = containerGitCommonDir.indexOf(',') !== -1 ? '"' : '';
391+
additionalMountString = `type=bind,${srcQuote}source=${gitCommonDir}${srcQuote},${tgtQuote}target=${containerGitCommonDir}${tgtQuote}${cons}`;
392+
}
393+
}
394+
}
395+
}
396+
362397
if (!workspaceFolder) {
363-
const rel = cliHost.path.relative(cliHost.path.dirname(hostMountFolder), workspace.rootFolderPath);
364-
workspaceFolder = `/workspaces/${cliHost.platform === 'win32' ? rel.replace(/\\/g, '/') : rel}`;
398+
const rel = cliHost.path.relative(hostMountFolder, workspace.rootFolderPath);
399+
workspaceFolder = path.posix.join(containerMountFolder, cliHost.platform === 'win32' ? rel.replace(/\\/g, '/') : rel);
365400
}
366401
if (!('workspaceMount' in config)) {
367-
const containerMountFolder = `/workspaces/${cliHost.path.basename(hostMountFolder)}`;
368402
const cons = cliHost.platform !== 'linux' ? `,consistency=${consistency || 'consistent'}` : ''; // Podman does not tolerate consistency=
369403
const srcQuote = hostMountFolder.indexOf(',') !== -1 ? '"' : '';
370404
const tgtQuote = containerMountFolder.indexOf(',') !== -1 ? '"' : '';
@@ -374,6 +408,7 @@ export async function getWorkspaceConfiguration(cliHost: CLIHost, workspace: Wor
374408
return {
375409
workspaceFolder,
376410
workspaceMount,
411+
additionalMountString,
377412
};
378413
}
379414

0 commit comments

Comments
 (0)