Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"source=ppds-auth-cache,target=/home/vscode/.ppds,type=volume",
"source=ppds-claude-sessions,target=/home/vscode/.claude/projects,type=volume"
],
"remoteUser": "vscode",
"postCreateCommand": "bash .devcontainer/setup.sh",
"customizations": {
"vscode": {
Expand Down
9 changes: 9 additions & 0 deletions .devcontainer/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,13 @@ node -e "
echo "=== Restoring .NET packages ==="
dotnet restore PPDS.sln

# --- Step 5: Fix workspace ownership ---
# postCreateCommand may run as root depending on devcontainer CLI version.
# dotnet restore creates obj/ dirs — if root-owned, MSBuild can't set timestamps
# (utimensat requires file ownership, not just write permission).
if [ "$(id -u)" = "0" ]; then
echo "=== Fixing workspace ownership (running as root) ==="
chown -R vscode:vscode "$(pwd)"
fi

echo "=== Setup complete ==="
127 changes: 72 additions & 55 deletions scripts/devcontainer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
.\scripts\devcontainer.ps1 down # stop container
.\scripts\devcontainer.ps1 push # push container commits via host (prompts for worktree)
.\scripts\devcontainer.ps1 push query-engine-v3 # push worktree branch via host
.\scripts\devcontainer.ps1 rebase # rebase onto origin/main (prompts for worktree)
.\scripts\devcontainer.ps1 rebase query-engine-v3 # rebase worktree onto origin/main
.\scripts\devcontainer.ps1 send # send host files to container (prompts for worktree)
.\scripts\devcontainer.ps1 send query-engine-v3 # send host worktree to container worktree
.\scripts\devcontainer.ps1 sync # sync origin git state into container
Expand All @@ -24,7 +26,7 @@

param(
[Parameter(Position = 0)]
[ValidateSet('up', 'shell', 'claude', 'ppds', 'down', 'status', 'send', 'push', 'sync', 'reset', 'help')]
[ValidateSet('up', 'shell', 'claude', 'ppds', 'down', 'status', 'send', 'push', 'rebase', 'sync', 'reset', 'help')]
[string]$Command = 'help',

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

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

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

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

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

if (-not $remoteSha) {
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git rev-list --count HEAD").Trim()
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git rev-list --count HEAD' -- $workdir).Trim()
Write-Step "New branch '$branch' ($ahead commit(s))."
}
else {
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git rev-list --count $remoteSha..HEAD 2>/dev/null").Trim()
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git rev-list --count "$2..HEAD" 2>/dev/null' -- $workdir $remoteSha).Trim()
if (-not $ahead -or $ahead -eq '0') {
Write-Ok "Branch '$branch' is already up-to-date on origin."
return
}
Write-Step "Branch '$branch' is $ahead commit(s) ahead of origin."
}

# Detect history divergence (e.g., branch was rebased on origin)
# Check if force push is needed (e.g., after rebasing onto main)
$forceNeeded = $false
if ($remoteSha) {
$hasRemote = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git cat-file -t $remoteSha 2>/dev/null && echo yes || echo no").Trim()
$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()
if ($hasRemote -eq 'yes') {
$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()
$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()
}
else {
$isFF = 'no'
}

if ($isFF -eq 'no') {
Write-Step "History diverged (origin was rebased) — syncing origin state to container..."

$containerId = (docker ps -q --filter "label=devcontainer.local_folder=$WorkspaceFolder").Trim()

# Fetch latest from origin on host, bundle it, and send to container
git -C $WorkspaceFolder fetch origin $branch
$originBundle = Join-Path $env:TEMP 'ppds-origin.bundle'
git -C $WorkspaceFolder bundle create $originBundle "origin/$branch"
docker cp $originBundle "${containerId}:/tmp/origin.bundle" | Out-Null
Remove-Item $originBundle -ErrorAction SilentlyContinue

# Container updates its origin ref from the bundle
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git fetch /tmp/origin.bundle 'refs/remotes/origin/${branch}:refs/remotes/origin/${branch}'"

# Container rebases new work on top of updated origin
Write-Step "Rebasing new commits onto updated origin/$branch..."
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git rebase origin/$branch"
if ($LASTEXITCODE -ne 0) {
devcontainer exec --workspace-folder $WorkspaceFolder rm -f /tmp/origin.bundle
Write-Err "Rebase has conflicts in '$workdir'."
Write-Step 'Launching Claude Code to help resolve conflicts...'
$planInstruction = if ($NoPlanMode) {
'Resolve all conflicts directly.'
} else {
'Start by using plan mode to analyze the conflicts and present a resolution strategy before making changes.'
}
$safeBranch = $branch -replace '[^a-zA-Z0-9_\-/.]', ''
$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}"
$escapedPrompt = $conflictPrompt.Replace("'", "'\\''")
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && claude --dangerously-skip-permissions -p '${escapedPrompt}'"
Write-Step "Claude session ended. Re-run 'push' when conflicts are resolved."
exit 1
}
devcontainer exec --workspace-folder $WorkspaceFolder rm -f /tmp/origin.bundle

# Update ahead count after rebase
$ahead = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git rev-list --count origin/${branch}..HEAD").Trim()
Write-Ok "Rebased onto origin. $ahead new commit(s) to push."
Write-Step "Branch has been rebased — will force push with lease."
$forceNeeded = $true
}
}

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

# Push from host using FETCH_HEAD (host has git credentials)
Write-Step 'Pushing to origin...'
git -C $WorkspaceFolder push origin "FETCH_HEAD:refs/heads/${branch}"
if ($forceNeeded) {
Write-Step 'Force pushing to origin (with lease)...'
git -C $WorkspaceFolder push --force-with-lease="refs/heads/${branch}:${remoteSha}" origin "FETCH_HEAD:refs/heads/${branch}"
}
else {
Write-Step 'Pushing to origin...'
git -C $WorkspaceFolder push origin "FETCH_HEAD:refs/heads/${branch}"
}
if ($LASTEXITCODE -ne 0) {
Write-Err 'Push failed. You may need to pull/rebase first.'
Write-Err 'Push failed. Run rebase to sync with origin first.'
Remove-Item $tempBundle -ErrorAction SilentlyContinue
exit 1
}
Expand All @@ -526,9 +498,52 @@ switch ($Command) {

# Update container's remote tracking ref so git status shows up-to-date
Write-Step 'Updating container remote refs...'
devcontainer exec --workspace-folder $WorkspaceFolder bash -c "cd $workdir && git fetch origin $branch 2>/dev/null || git update-ref refs/remotes/origin/$branch HEAD"
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

$verb = if ($forceNeeded) { 'Force pushed' } else { 'Pushed' }
Write-Ok "$verb '$branch' to origin ($ahead commit(s))."
}

'rebase' {
# Rebase feature branch onto origin/main (syncs origin refs first)
Ensure-ContainerRunning
$subdir = Select-WorkingDirectory -Target $Target
$workdir = if ($subdir) { $subdir } else { '.' }

# Get current branch
$branch = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git branch --show-current' -- $workdir).Trim()
if (-not $branch) {
Write-Err "Could not determine branch (detached HEAD?)."
exit 1
}
if ($branch -eq 'main') {
Write-Err "Already on main — use 'sync' to fast-forward main instead."
exit 1
}

# Sync origin refs so origin/main is current
Sync-ContainerFromOrigin

# Rebase onto origin/main
Write-Step "Rebasing '$branch' onto origin/main..."
devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && git rebase origin/main' -- $workdir
if ($LASTEXITCODE -ne 0) {
Write-Err "Rebase has conflicts."
Write-Step 'Launching Claude Code to help resolve conflicts...'
$planInstruction = if ($NoPlanMode) {
'Resolve all conflicts directly.'
} else {
'Start by using plan mode to analyze the conflicts and present a resolution strategy before making changes.'
}
$safeBranch = $branch -replace '[^a-zA-Z0-9_\-/.]', ''
$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}"
devcontainer exec --workspace-folder $WorkspaceFolder bash -c 'cd "$1" && claude --dangerously-skip-permissions -p "$2"' -- $workdir $conflictPrompt
Write-Step "Claude session ended. Verify rebase is complete, then 'push' when ready."
exit 1
}

Write-Ok "Pushed '$branch' to origin ($ahead commit(s))."
$newBase = (devcontainer exec --workspace-folder $WorkspaceFolder bash -c "git log --oneline -1 origin/main").Trim()
Write-Ok "Rebased '$branch' onto origin/main ($newBase)."
}

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