@@ -343,7 +343,15 @@ func computeWorkspaceConfig(workspace *Workspace, config *DevContainer, mountWor
343343 containerMountFolder := "/workspaces/" + filepath .Base (sourceFolder )
344344 var additionalMounts []string
345345 if mountWorkspaceGitRoot && mountGitWorktreeCommonDir && ! config .IsComposeConfig () {
346- if remapped , commonDirMount , ok := gitWorktreeCommonDirMount (sourceFolder , consistency ); ok {
346+ // A custom workspaceMount defines where the worktree is actually mounted in
347+ // the container; resolve the common dir relative to that (substituted)
348+ // target so the relative gitdir still resolves. Otherwise fall back to the
349+ // computed container mount folder (#1261).
350+ customTarget := ""
351+ if config .WorkspaceMount != "" {
352+ customTarget = substituteHostString (workspace , mountTarget (config .WorkspaceMount ))
353+ }
354+ if remapped , commonDirMount , ok := gitWorktreeCommonDirMount (sourceFolder , customTarget , consistency ); ok {
347355 containerMountFolder = remapped
348356 additionalMounts = append (additionalMounts , commonDirMount )
349357 }
@@ -402,7 +410,7 @@ func bindMount(source, target, consistency string) string {
402410// the shared common dir (the main repo's `.git`) into the container. ok is false
403411// for a normal clone (`.git` is a directory), an absolute gitdir, or a missing
404412// gitlink — all cases where no extra mount is needed.
405- func gitWorktreeCommonDirMount (hostMountFolder , consistency string ) (containerMountFolder , additionalMount string , ok bool ) {
413+ func gitWorktreeCommonDirMount (hostMountFolder , customContainerTarget , consistency string ) (containerMountFolder , additionalMount string , ok bool ) {
406414 info , err := os .Stat (filepath .Join (hostMountFolder , ".git" ))
407415 if err != nil || ! info .Mode ().IsRegular () {
408416 return "" , "" , false
@@ -429,11 +437,68 @@ func gitWorktreeCommonDirMount(hostMountFolder, consistency string) (containerMo
429437 }
430438 containerMountFolder = path .Join (append ([]string {"/workspaces" }, segments ... )... )
431439
432- // The common dir lands at the same relative offset inside the container.
433- containerGitCommonDir := path .Clean (path .Join (containerMountFolder , filepath .ToSlash (gitdir ), ".." , ".." ))
440+ // The common dir lands at the same relative offset from wherever the worktree
441+ // is mounted: a custom workspaceMount target when the config sets one, else the
442+ // computed container mount folder.
443+ worktreeContainerFolder := containerMountFolder
444+ if customContainerTarget != "" {
445+ worktreeContainerFolder = customContainerTarget
446+ }
447+ containerGitCommonDir := path .Clean (path .Join (worktreeContainerFolder , filepath .ToSlash (gitdir ), ".." , ".." ))
434448 return containerMountFolder , bindMount (gitCommonDir , containerGitCommonDir , consistency ), true
435449}
436450
451+ // mountTarget extracts the target= value from a `type=bind,...` mount spec,
452+ // tolerating a quoted source/target that itself contains commas.
453+ func mountTarget (spec string ) string {
454+ for _ , f := range splitMountFields (spec ) {
455+ if t , ok := strings .CutPrefix (f , "target=" ); ok {
456+ return t
457+ }
458+ }
459+ return ""
460+ }
461+
462+ // splitMountFields splits a docker mount spec on commas that are not inside
463+ // double quotes, stripping the quotes.
464+ func splitMountFields (spec string ) []string {
465+ var fields []string
466+ var b strings.Builder
467+ inQuote := false
468+ for _ , r := range spec {
469+ switch {
470+ case r == '"' :
471+ inQuote = ! inQuote
472+ case r == ',' && ! inQuote :
473+ fields = append (fields , b .String ())
474+ b .Reset ()
475+ default :
476+ b .WriteRune (r )
477+ }
478+ }
479+ return append (fields , b .String ())
480+ }
481+
482+ // substituteHostString applies host-phase variable substitution to s using a
483+ // minimal host context (platform, local workspace folder, env). Used for the
484+ // custom workspaceMount target, which is resolved before the main substitution
485+ // pass. Returns s unchanged on error or when it has no ${...} tags.
486+ func substituteHostString (workspace * Workspace , s string ) string {
487+ if ! strings .Contains (s , "${" ) {
488+ return s
489+ }
490+ ctx := HostSubContext {
491+ Platform : currentPlatform (),
492+ LocalWorkspaceFolder : strings .TrimRight (workspace .RootFolderPath , "/\\ " ),
493+ Env : envFromOS (),
494+ }
495+ out , err := NewVariableResolver ().resolveString (SubstitutionContext {HostSubContext : ctx }, PhaseHost , s )
496+ if err != nil {
497+ return s
498+ }
499+ return out
500+ }
501+
437502// parseGitdir extracts the target of a `gitdir: <path>` line from a `.git`
438503// gitlink file. No regexp, matching the rest of the package.
439504func parseGitdir (content string ) (string , bool ) {
0 commit comments