Skip to content

Commit 0b78146

Browse files
joshsmithxrmclaude
andauthored
fix: devcontainer obj/ files owned by root break incremental builds (#534)
* fix: devcontainer obj/ files owned by root break incremental builds postCreateCommand runs dotnet restore as root in some devcontainer CLI versions, creating obj/ directories owned by root:root. MSBuild's incremental build then fails with MSB3374 because utimensat() requires file ownership to set timestamps — 777 permissions aren't sufficient. Fix: explicitly set remoteUser to vscode and add a chown safety net in setup.sh after dotnet restore. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add rebase command + simplify push with force-with-lease New `rebase` command rebases a feature branch onto origin/main: - Syncs origin refs first (reuses bundle mechanism) - Launches Claude Code for conflict resolution if rebase fails - Guards against running on main (use `sync` instead) Simplified `push` divergence handling: - Removed auto-rebase-onto-origin/branch + Claude logic from push - Replaced with force-with-lease when history diverged (e.g., after rebase) - Uses remoteSha from ls-remote as the expected lease value Also addresses review feedback: use vscode:vscode instead of 1000:1000 in setup.sh chown for robustness across image versions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: use positional args for bash -c in devcontainer exec Replace string interpolation of $workdir (and other variables) in bash -c commands with positional arguments via -- $arg. This prevents shell metacharacter injection from directory/branch names. Converts all 13 call sites to use 'cd "$1" && ...' -- $workdir pattern, verified working through the PowerShell → devcontainer exec → docker exec → bash argument chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: scope chown to root-owned files, consistency for git log context setup.sh: use -h (no symlink follow) and --from=root (only fix root-owned files from dotnet restore, skip vscode-owned files). devcontainer.ps1: run git log in workdir context for consistency with all other devcontainer exec calls in the rebase block. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3421a03 commit 0b78146

File tree

3 files changed

+82
-55
lines changed

3 files changed

+82
-55
lines changed

.devcontainer/devcontainer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"source=ppds-auth-cache,target=/home/vscode/.ppds,type=volume",
1717
"source=ppds-claude-sessions,target=/home/vscode/.claude/projects,type=volume"
1818
],
19+
"remoteUser": "vscode",
1920
"postCreateCommand": "bash .devcontainer/setup.sh",
2021
"customizations": {
2122
"vscode": {

.devcontainer/setup.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,13 @@ node -e "
100100
echo "=== Restoring .NET packages ==="
101101
dotnet restore PPDS.sln
102102

103+
# --- Step 5: Fix workspace ownership ---
104+
# postCreateCommand may run as root depending on devcontainer CLI version.
105+
# dotnet restore creates obj/ dirs — if root-owned, MSBuild can't set timestamps
106+
# (utimensat requires file ownership, not just write permission).
107+
if [ "$(id -u)" = "0" ]; then
108+
echo "=== Fixing workspace ownership (running as root) ==="
109+
chown -hR --from=root vscode:vscode ./
110+
fi
111+
103112
echo "=== Setup complete ==="

scripts/devcontainer.ps1

Lines changed: 72 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
.\scripts\devcontainer.ps1 down # stop container
1717
.\scripts\devcontainer.ps1 push # push container commits via host (prompts for worktree)
1818
.\scripts\devcontainer.ps1 push query-engine-v3 # push worktree branch via host
19+
.\scripts\devcontainer.ps1 rebase # rebase onto origin/main (prompts for worktree)
20+
.\scripts\devcontainer.ps1 rebase query-engine-v3 # rebase worktree onto origin/main
1921
.\scripts\devcontainer.ps1 send # send host files to container (prompts for worktree)
2022
.\scripts\devcontainer.ps1 send query-engine-v3 # send host worktree to container worktree
2123
.\scripts\devcontainer.ps1 sync # sync origin git state into container
@@ -24,7 +26,7 @@
2426

2527
param(
2628
[Parameter(Position = 0)]
27-
[ValidateSet('up', 'shell', 'claude', 'ppds', 'down', 'status', 'send', 'push', 'sync', 'reset', 'help')]
29+
[ValidateSet('up', 'shell', 'claude', 'ppds', 'down', 'status', 'send', 'push', 'rebase', 'sync', 'reset', 'help')]
2830
[string]$Command = 'help',
2931

3032
[Parameter(Position = 1)]
@@ -371,14 +373,14 @@ switch ($Command) {
371373
$subdir = Select-WorkingDirectory -Target $Target
372374
$workdir = if ($subdir) { $subdir } else { '.' }
373375
Write-Step 'Building PPDS CLI...'
374-
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && dotnet build src/PPDS.Cli -f net10.0"
376+
devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && dotnet build src/PPDS.Cli -f net10.0' -- $workdir
375377
if ($LASTEXITCODE -ne 0) {
376378
Write-Err 'Build failed.'
377379
exit 1
378380
}
379381

380382
Write-Step 'Launching TUI...'
381-
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && PPDS_FORCE_TUI=1 dotnet src/PPDS.Cli/bin/Debug/net10.0/ppds.dll"
383+
devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && PPDS_FORCE_TUI=1 dotnet src/PPDS.Cli/bin/Debug/net10.0/ppds.dll' -- $workdir
382384
}
383385

384386
'down' {
@@ -404,12 +406,12 @@ switch ($Command) {
404406
$workdir = if ($subdir) { $subdir } else { '.' }
405407

406408
# Get branch name and local HEAD SHA from container
407-
$branch = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git branch --show-current").Trim()
409+
$branch = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git branch --show-current' -- $workdir).Trim()
408410
if (-not $branch) {
409411
Write-Err "Could not determine branch (detached HEAD?)."
410412
exit 1
411413
}
412-
$localSha = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git rev-parse HEAD").Trim()
414+
$localSha = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git rev-parse HEAD' -- $workdir).Trim()
413415

414416
# Compare against actual remote state (not local tracking refs which may be stale)
415417
$remoteSha = (git -C $WorkspaceFolder ls-remote origin "refs/heads/${branch}" 2>$null)
@@ -421,73 +423,37 @@ switch ($Command) {
421423
}
422424

423425
if (-not $remoteSha) {
424-
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git rev-list --count HEAD").Trim()
426+
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git rev-list --count HEAD' -- $workdir).Trim()
425427
Write-Step "New branch '$branch' ($ahead commit(s))."
426428
}
427429
else {
428-
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git rev-list --count $remoteSha..HEAD 2>/dev/null").Trim()
430+
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git rev-list --count "$2..HEAD" 2>/dev/null' -- $workdir $remoteSha).Trim()
429431
if (-not $ahead -or $ahead -eq '0') {
430432
Write-Ok "Branch '$branch' is already up-to-date on origin."
431433
return
432434
}
433435
Write-Step "Branch '$branch' is $ahead commit(s) ahead of origin."
434436
}
435437

436-
# Detect history divergence (e.g., branch was rebased on origin)
438+
# Check if force push is needed (e.g., after rebasing onto main)
439+
$forceNeeded = $false
437440
if ($remoteSha) {
438-
$hasRemote = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git cat-file -t $remoteSha 2>/dev/null && echo yes || echo no").Trim()
441+
$hasRemote = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git cat-file -t "$2" 2>/dev/null && echo yes || echo no' -- $workdir $remoteSha).Trim()
439442
if ($hasRemote -eq 'yes') {
440-
$isFF = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git merge-base --is-ancestor $remoteSha HEAD 2>/dev/null && echo yes || echo no").Trim()
443+
$isFF = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git merge-base --is-ancestor "$2" HEAD 2>/dev/null && echo yes || echo no' -- $workdir $remoteSha).Trim()
441444
}
442445
else {
443446
$isFF = 'no'
444447
}
445-
446448
if ($isFF -eq 'no') {
447-
Write-Step "History diverged (origin was rebased) — syncing origin state to container..."
448-
449-
$containerId = (docker ps -q --filter "label=devcontainer.local_folder=$WorkspaceFolder").Trim()
450-
451-
# Fetch latest from origin on host, bundle it, and send to container
452-
git -C $WorkspaceFolder fetch origin $branch
453-
$originBundle = Join-Path $env:TEMP 'ppds-origin.bundle'
454-
git -C $WorkspaceFolder bundle create $originBundle "origin/$branch"
455-
docker cp $originBundle "${containerId}:/tmp/origin.bundle" | Out-Null
456-
Remove-Item $originBundle -ErrorAction SilentlyContinue
457-
458-
# Container updates its origin ref from the bundle
459-
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git fetch /tmp/origin.bundle 'refs/remotes/origin/${branch}:refs/remotes/origin/${branch}'"
460-
461-
# Container rebases new work on top of updated origin
462-
Write-Step "Rebasing new commits onto updated origin/$branch..."
463-
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git rebase origin/$branch"
464-
if ($LASTEXITCODE -ne 0) {
465-
devcontainer exec --workspace-folder $WorkspaceFolder rm -f /tmp/origin.bundle
466-
Write-Err "Rebase has conflicts in '$workdir'."
467-
Write-Step 'Launching Claude Code to help resolve conflicts...'
468-
$planInstruction = if ($NoPlanMode) {
469-
'Resolve all conflicts directly.'
470-
} else {
471-
'Start by using plan mode to analyze the conflicts and present a resolution strategy before making changes.'
472-
}
473-
$safeBranch = $branch -replace '[^a-zA-Z0-9_\-/.]', ''
474-
$conflictPrompt = "A git rebase of branch '${safeBranch}' onto origin/${safeBranch} has resulted in merge conflicts. Run git status to see conflicted files. Analyze each conflict, resolve them, git add the resolved files, and run git rebase --continue. If there are multiple conflicting commits, continue resolving until the rebase is complete. ${planInstruction}"
475-
$escapedPrompt = $conflictPrompt.Replace("'", "'\\''")
476-
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && claude --dangerously-skip-permissions -p '${escapedPrompt}'"
477-
Write-Step "Claude session ended. Re-run 'push' when conflicts are resolved."
478-
exit 1
479-
}
480-
devcontainer exec --workspace-folder $WorkspaceFolder rm -f /tmp/origin.bundle
481-
482-
# Update ahead count after rebase
483-
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git rev-list --count origin/${branch}..HEAD").Trim()
484-
Write-Ok "Rebased onto origin. $ahead new commit(s) to push."
449+
Write-Step "Branch has been rebased — will force push with lease."
450+
$forceNeeded = $true
485451
}
486452
}
487453

488454
# Create git bundle in container
489455
Write-Step 'Bundling commits...'
490-
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git bundle create /tmp/push.bundle $branch" | Out-Null
456+
devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git bundle create /tmp/push.bundle "$2"' -- $workdir $branch | Out-Null
491457
if ($LASTEXITCODE -ne 0) {
492458
Write-Err 'Failed to create git bundle.'
493459
exit 1
@@ -512,10 +478,16 @@ switch ($Command) {
512478
}
513479

514480
# Push from host using FETCH_HEAD (host has git credentials)
515-
Write-Step 'Pushing to origin...'
516-
git -C $WorkspaceFolder push origin "FETCH_HEAD:refs/heads/${branch}"
481+
if ($forceNeeded) {
482+
Write-Step 'Force pushing to origin (with lease)...'
483+
git -C $WorkspaceFolder push --force-with-lease="refs/heads/${branch}:${remoteSha}" origin "FETCH_HEAD:refs/heads/${branch}"
484+
}
485+
else {
486+
Write-Step 'Pushing to origin...'
487+
git -C $WorkspaceFolder push origin "FETCH_HEAD:refs/heads/${branch}"
488+
}
517489
if ($LASTEXITCODE -ne 0) {
518-
Write-Err 'Push failed. You may need to pull/rebase first.'
490+
Write-Err 'Push failed. Run rebase to sync with origin first.'
519491
Remove-Item $tempBundle -ErrorAction SilentlyContinue
520492
exit 1
521493
}
@@ -526,9 +498,52 @@ switch ($Command) {
526498

527499
# Update container's remote tracking ref so git status shows up-to-date
528500
Write-Step 'Updating container remote refs...'
529-
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git fetch origin $branch 2>/dev/null || git update-ref refs/remotes/origin/$branch HEAD"
501+
devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git fetch origin "$2" 2>/dev/null || git update-ref "refs/remotes/origin/$2" HEAD' -- $workdir $branch
502+
503+
$verb = if ($forceNeeded) { 'Force pushed' } else { 'Pushed' }
504+
Write-Ok "$verb '$branch' to origin ($ahead commit(s))."
505+
}
506+
507+
'rebase' {
508+
# Rebase feature branch onto origin/main (syncs origin refs first)
509+
Ensure-ContainerRunning
510+
$subdir = Select-WorkingDirectory -Target $Target
511+
$workdir = if ($subdir) { $subdir } else { '.' }
512+
513+
# Get current branch
514+
$branch = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git branch --show-current' -- $workdir).Trim()
515+
if (-not $branch) {
516+
Write-Err "Could not determine branch (detached HEAD?)."
517+
exit 1
518+
}
519+
if ($branch -eq 'main') {
520+
Write-Err "Already on main — use 'sync' to fast-forward main instead."
521+
exit 1
522+
}
523+
524+
# Sync origin refs so origin/main is current
525+
Sync-ContainerFromOrigin
526+
527+
# Rebase onto origin/main
528+
Write-Step "Rebasing '$branch' onto origin/main..."
529+
devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git rebase origin/main' -- $workdir
530+
if ($LASTEXITCODE -ne 0) {
531+
Write-Err "Rebase has conflicts."
532+
Write-Step 'Launching Claude Code to help resolve conflicts...'
533+
$planInstruction = if ($NoPlanMode) {
534+
'Resolve all conflicts directly.'
535+
} else {
536+
'Start by using plan mode to analyze the conflicts and present a resolution strategy before making changes.'
537+
}
538+
$safeBranch = $branch -replace '[^a-zA-Z0-9_\-/.]', ''
539+
$conflictPrompt = "A git rebase of branch '${safeBranch}' onto origin/main has resulted in merge conflicts. Run git status to see conflicted files. Analyze each conflict, resolve them, git add the resolved files, and run git rebase --continue. If there are multiple conflicting commits, continue resolving until the rebase is complete. ${planInstruction}"
540+
devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && claude --dangerously-skip-permissions -p "$2"' -- $workdir $conflictPrompt
541+
Write-Step "Claude session ended. Verify rebase is complete, then 'push' when ready."
542+
exit 1
543+
}
530544

531-
Write-Ok "Pushed '$branch' to origin ($ahead commit(s))."
545+
$newBase = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git log --oneline -1 origin/main' -- $workdir).Trim()
546+
Write-Ok "Rebased '$branch' onto origin/main ($newBase)."
532547
}
533548

534549
'send' {
@@ -617,6 +632,7 @@ switch ($Command) {
617632
Write-Host ' down Stop the container'
618633
Write-Host ' status Check if container is running'
619634
Write-Host ' push [worktree] Push container commits to origin via host credentials'
635+
Write-Host ' rebase [worktree] Rebase feature branch onto origin/main (syncs first)'
620636
Write-Host ' send [worktree] Send host files to container (preserves .git state)'
621637
Write-Host ' sync Sync origin git state into container (auto-runs on up)'
622638
Write-Host ' reset Nuke container + all volumes + rebuild from scratch'
@@ -631,6 +647,7 @@ switch ($Command) {
631647
Write-Host ' .\scripts\devcontainer.ps1 ppds query-engine-v3 # TUI from worktree'
632648
Write-Host ' .\scripts\devcontainer.ps1 shell main # shell at repo root'
633649
Write-Host ' .\scripts\devcontainer.ps1 push query-engine-v3 # push worktree branch via host'
650+
Write-Host ' .\scripts\devcontainer.ps1 rebase query-engine-v3 # rebase worktree onto origin/main'
634651
Write-Host ' .\scripts\devcontainer.ps1 send query-engine-v3 # send host worktree to container'
635652
Write-Host ' .\scripts\devcontainer.ps1 sync # sync origin refs into container'
636653
Write-Host ''

0 commit comments

Comments
 (0)