Skip to content

Commit 1577ec3

Browse files
committed
Don't allow forceable resetting of git-worktrees
1 parent 97ede05 commit 1577ec3

File tree

5 files changed

+143
-64
lines changed

5 files changed

+143
-64
lines changed

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@
169169
{
170170
"label": "Agents: Spin up",
171171
"type": "shell",
172-
"command": "& { ./scripts/spin-up-agents.ps1 -RepoRoot \"${workspaceFolder}\" -Count ${input:agentCount} -ForceCleanup}",
172+
"command": "& { ./scripts/spin-up-agents.ps1 -RepoRoot \"${workspaceFolder}\" -Count ${input:agentCount}}",
173173
"detail": "Creates (or reuses) agent worktrees, containers, and VS Code windows.",
174174
"options": {
175175
"shell": {

scripts/git-utilities.ps1

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,3 @@ function Test-GitBranchExists {
172172
return ($output.Count -gt 0)
173173
}
174174

175-
function Reset-GitBranchToRef {
176-
[CmdletBinding()]
177-
param(
178-
[Parameter(Mandatory=$true)][string]$Branch,
179-
[Parameter(Mandatory=$true)][string]$Ref
180-
)
181-
182-
# Ensures the agent branch is a mirror of the desired base ref.
183-
Invoke-GitSafe @('branch','-f',$Branch,$Ref) -Quiet
184-
}

scripts/multi-agent-containers-workflow.md

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,16 @@ The script will:
6262
- `-OpenVSCode` - launches a VS Code window per agent with `FW_AGENT_CONTAINER` set
6363

6464
The script will:
65-
- Create worktrees at `...\worktrees\agent-1..N` with branches `agents/agent-1..N`
65+
- Create NEW worktrees at `...\worktrees\agent-1..N` with branches `agents/agent-1..N` from BaseRef
66+
- **SKIP existing worktrees** (preserves your work - never resets or modifies existing worktrees)
6667
- Build or reuse the `fw-build:ltsc2022` Windows image
67-
- Start containers `fw-agent-1..N` with per-agent NuGet caches
68-
- Generate `.vscode/tasks.json` wired to the matching container
68+
- Start containers `fw-agent-1..N` with per-agent NuGet caches (skipped for existing worktrees)
69+
- Generate `.vscode/tasks.json` wired to the matching container (only for new worktrees)
6970
- Generate `.vscode/settings.json` with unique colors to keep agent windows visually distinct
7071
- Optionally open each worktree in a new VS Code window
7172

73+
**IMPORTANT**: This script **NEVER modifies existing worktrees** to prevent data loss. If you want to reset a worktree to a different branch, use `tear-down-agents.ps1 -RemoveWorktrees` first, then re-run spin-up.
74+
7275
3. Work per agent
7376

7477
- Open the worktree folder in VS Code (one window per agent)
@@ -84,6 +87,16 @@ All registry and COM operations occur inside the container, isolated from your h
8487
- `gh pr create -H agents/agent-1 -B release/9.3 -t "..." -b "..."`
8588
- Avoid running Git inside the container for that worktree to prevent file locking.
8689

90+
**Branch reuse behavior**: If `agents/agent-N` branches already exist, spin-up will:
91+
- Create worktrees attached to the existing branches at their current commits
92+
- **NOT** reset branches to BaseRef automatically
93+
- Let you manually sync branches if needed:
94+
```powershell
95+
cd worktrees\agent-1
96+
git fetch origin
97+
git merge origin/release/9.3 # or git reset --hard origin/release/9.3
98+
```
99+
87100
## Performance and limits
88101

89102
- Avoid building all 5 agents simultaneously on a 32 GB machine; stagger builds or reduce msbuild parallelism
@@ -105,6 +118,26 @@ Remove containers, worktrees, agent branches, and per-agent NuGet caches:
105118
-RemoveWorktrees
106119
```
107120

121+
**IMPORTANT**: Tear-down will **ERROR and refuse to remove worktrees** that have uncommitted changes to prevent data loss. You'll see:
122+
```
123+
Worktree agent-1 has uncommitted changes at: C:\...\worktrees\agent-1
124+
125+
To protect your work, tear-down will NOT remove this worktree.
126+
```
127+
128+
To proceed, commit or stash your changes first:
129+
```powershell
130+
cd worktrees\agent-1
131+
git add .
132+
git commit -m "Work in progress"
133+
# Then re-run tear-down
134+
```
135+
136+
Only use `-ForceRemoveDirty` if you're **certain** you want to discard uncommitted work:
137+
```powershell
138+
.\scripts\tear-down-agents.ps1 -RepoRoot "C:\dev\FieldWorks" -RemoveWorktrees -ForceRemoveDirty
139+
```
140+
108141
You can pass `-WorktreesRoot` if you placed worktrees outside the default path; otherwise the script respects `FW_WORKTREES_ROOT` when present.
109142

110143
## Notes
@@ -152,11 +185,25 @@ docker logs fw-agent-1 # Check container logs
152185
```
153186

154187
### Worktree already exists
155-
The script safely reuses existing worktrees and containers. To start fresh:
188+
The script **preserves existing worktrees** and will skip them to prevent data loss. You'll see:
189+
```
190+
Worktree already exists: C:\...\worktrees\agent-1 (skipping - will not modify existing worktree)
191+
Branch: agents/agent-1 (current state preserved)
192+
```
193+
194+
To reset a worktree to a different branch or base ref:
156195
```powershell
196+
# Option 1: Remove all worktrees and recreate fresh
157197
.\scripts\tear-down-agents.ps1 -RepoRoot "C:\dev\FieldWorks" -Count 3 -RemoveWorktrees
198+
199+
# Option 2: Manually delete specific worktree, then re-run spin-up
200+
Remove-Item -Recurse -Force "C:\...\worktrees\agent-1"
201+
git worktree prune
202+
.\scripts\spin-up-agents.ps1 -RepoRoot "C:\dev\FieldWorks" -Count 3
158203
```
159204

205+
**Never** try to force-reset worktrees - this was removed to prevent accidental data loss.
206+
160207
### Build fails inside container
161208
Verify the solution path is correct and accessible:
162209
```powershell

scripts/spin-up-agents.ps1

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,24 @@ Prereqs:
66
- One primary clone on the host (e.g., C:\dev\FieldWorks)
77
- PowerShell 5+ (Windows)
88
9+
IMPORTANT: This script NEVER modifies existing worktrees to prevent data loss.
10+
- Existing worktrees are preserved as-is (skipped)
11+
- New worktrees are created from the specified BaseRef
12+
- If you want to reset a worktree, manually delete it first or use tear-down-agents.ps1
13+
914
Typical use:
1015
$env:FW_WORKTREES_ROOT = "C:\dev\FieldWorks\worktrees"
1116
1217
# Create agents based on current branch (default FieldWorks.proj build):
1318
.\scripts\spin-up-agents.ps1 -RepoRoot "C:\dev\FieldWorks" -Count 3
1419
15-
# Or specify a different base branch:
20+
# Or specify a different base branch (only affects NEW worktrees):
1621
.\scripts\spin-up-agents.ps1 -RepoRoot "C:\dev\FieldWorks" -Count 3 -BaseRef origin/release/9.3
1722
1823
# To prevent VS Code from opening automatically:
1924
.\scripts\spin-up-agents.ps1 -RepoRoot "C:\dev\FieldWorks" -Count 3 -SkipOpenVSCode
2025
21-
# To forcibly clean orphaned worktree directories without prompting:
22-
.\scripts\spin-up-agents.ps1 -RepoRoot "C:\dev\FieldWorks" -Count 3 -ForceCleanup
26+
# NOTE: -ForceCleanup parameter removed - use tear-down-agents.ps1 instead
2327
#>
2428

2529
[CmdletBinding()]
@@ -34,7 +38,6 @@ param(
3438
[switch]$SkipVsCodeSetup,
3539
[switch]$ForceVsCodeSetup,
3640
[switch]$SkipOpenVSCode,
37-
[switch]$ForceCleanup,
3841
[string]$ContainerMemory = "4g"
3942
)
4043

@@ -232,22 +235,8 @@ function Ensure-Image {
232235
}
233236
}
234237

235-
function Reset-AgentWorktree {
236-
param(
237-
[Parameter(Mandatory=$true)][string]$WorktreePath,
238-
[Parameter(Mandatory=$true)][string]$ResetRef
239-
)
240-
241-
if (-not (Test-Path -LiteralPath $WorktreePath)) { return }
242-
243-
Push-Location $WorktreePath
244-
try {
245-
Invoke-GitSafe @('reset','--hard',$ResetRef) -Quiet
246-
Invoke-GitSafe @('clean','-xfd') -Quiet
247-
} finally {
248-
Pop-Location
249-
}
250-
}
238+
# Reset-AgentWorktree function removed - we never modify existing worktrees
239+
# Existing worktrees are left untouched to prevent data loss
251240

252241
function Clear-AgentDirectory {
253242
param(
@@ -298,30 +287,39 @@ function Ensure-Worktree {
298287
}
299288

300289
if ($isRegistered -and $dirExists -and -not $isEmpty) {
301-
# Worktree is registered and has content - reset to requested base
302-
Write-Host "Worktree exists and is registered: $target"
303-
Reset-AgentWorktree -WorktreePath $target -ResetRef $BaseRef
290+
# Worktree exists with content - NEVER modify it to prevent data loss
291+
Write-Host "Worktree already exists: $target (skipping - will not modify existing worktree)"
292+
Write-Host " Branch: $branch (current state preserved)"
293+
# Skip to end - use existing worktree as-is
294+
$resolvedTarget = (Resolve-Path -LiteralPath $target).Path
295+
return @{ Branch = $branch; Path = $resolvedTarget; Skipped = $true }
304296
} elseif ($isRegistered -and (-not $dirExists -or $isEmpty)) {
305297
# Worktree is registered but directory is missing or empty - repair it
306298
Write-Host "Repairing worktree $target (directory missing or empty)..."
307299
Remove-GitWorktreePath -WorktreePath $thisWorktree.RawPath
308300
Prune-GitWorktreesNow
309301
$isRegistered = $false
310302
$branchWorktree = $null
311-
} elseif (-not $isRegistered -and $dirExists) {
312-
if (-not $ForceCleanup) {
313-
throw "Directory $target exists but is not a registered worktree. Close any VS Code windows, run tear-down with -RemoveWorktrees, or rerun spin-up with -ForceCleanup to reset it."
314-
}
315-
316-
Write-Host "Directory $target exists but is not registered; reinitializing worktree in place (contents will be reset)."
317-
try {
318-
Detach-GitWorktreeMetadata -WorktreePath $target | Out-Null
319-
} catch {
320-
$err = $_
321-
Write-Warning "Failed to remove stale git metadata from ${target}: $err"
322-
}
323-
324-
Clear-AgentDirectory -AgentPath $target
303+
} elseif (-not $isRegistered -and $dirExists -and -not $isEmpty) {
304+
# Directory exists with content but not a worktree - ERROR out
305+
throw @"
306+
Directory $target exists but is not a registered git worktree.
307+
308+
This could indicate:
309+
1. Leftover files from a previous worktree that wasn't cleaned up properly
310+
2. Manual files placed in the worktrees directory
311+
3. A corrupted worktree registration
312+
313+
To fix this:
314+
- OPTION 1 (preserve content): Move the directory elsewhere, then rerun this script
315+
- OPTION 2 (discard content): Delete the directory manually, then rerun this script
316+
- OPTION 3 (force cleanup): Run: .\scripts\tear-down-agents.ps1 -RemoveWorktrees
317+
318+
NEVER use -ForceCleanup as it will destroy your work without warning.
319+
"@
320+
} elseif (-not $isRegistered -and $dirExists -and $isEmpty) {
321+
# Directory exists but empty and not registered - safe to recreate
322+
Write-Host "Directory $target exists but is empty; will create worktree here."
325323
}
326324

327325
# Create worktree if needed
@@ -336,9 +334,8 @@ function Ensure-Worktree {
336334
$addArgs += $target
337335

338336
if (Test-GitBranchExists -Branch $branch) {
339-
Write-Host "Resetting $branch to $BaseRef before reuse..."
340-
Reset-GitBranchToRef -Branch $branch -Ref $BaseRef
341-
Write-Host "Creating worktree $target with existing branch $branch..."
337+
Write-Host "Creating worktree $target with existing branch $branch (preserving current branch state)..."
338+
Write-Host " Note: Branch will remain at its current commit; use 'git merge' or 'git reset' in the worktree if you want to sync with $BaseRef"
342339
$addArgs += $branch
343340
} else {
344341
Write-Host "Creating worktree $target with new branch $branch from $BaseRef..."
@@ -353,10 +350,7 @@ function Ensure-Worktree {
353350
}
354351
Ensure-RelativeGitDir -WorktreePath $target -RepoRoot $RepoRoot -WorktreeName "agent-$Index"
355352
$resolvedTarget = (Resolve-Path -LiteralPath $target).Path
356-
357-
if (-not $isRegistered) {
358-
Reset-AgentWorktree -WorktreePath $resolvedTarget -ResetRef $BaseRef
359-
}
353+
# Never reset worktrees - new worktrees are created at correct ref, existing ones are preserved
360354
} finally {
361355
Pop-Location
362356
}
@@ -595,6 +589,36 @@ $repoVsCodeSettings = Get-RepoVsCodeSettings -RepoRoot $RepoRoot
595589
$agents = @()
596590
for ($i=1; $i -le $Count; $i++) {
597591
$wt = Ensure-Worktree -Index $i
592+
593+
if ($wt.Skipped) {
594+
Write-Host "Agent-$i - Using existing worktree (no changes made)"
595+
596+
# Still open VS Code for existing worktrees if requested
597+
if (-not $SkipOpenVSCode) {
598+
$workspaceTarget = Join-Path $wt.Path "agent-$i.code-workspace"
599+
if (-not (Test-Path -LiteralPath $workspaceTarget)) {
600+
$workspaceTarget = $wt.Path
601+
}
602+
603+
if (Test-VSCodeWorkspaceOpen -WorkspacePath $workspaceTarget) {
604+
Write-Host "VS Code already open for agent-$i; skipping new window launch."
605+
} else {
606+
Write-Host "Opening VS Code for existing worktree agent-$i..."
607+
Open-AgentVsCodeWindow -Index $i -AgentPath $wt.Path -ContainerName "fw-agent-$i"
608+
}
609+
}
610+
611+
$agents += [pscustomobject]@{
612+
Index = $i
613+
Worktree = $wt.Path
614+
Branch = $wt.Branch
615+
Container = "(existing)"
616+
Theme = "(preserved)"
617+
Status = "Skipped - existing worktree preserved"
618+
}
619+
continue
620+
}
621+
598622
$ct = Ensure-Container -Index $i -AgentPath $wt.Path -RepoRoot $RepoRoot -WorktreesRoot $WorktreesRoot
599623
Write-AgentConfig -AgentPath $wt.Path -SolutionRelPath $SolutionRelPath -Container $ct -RepoRoot $RepoRoot
600624
$colors = Get-AgentColors -Index $i
@@ -617,6 +641,7 @@ for ($i=1; $i -le $Count; $i++) {
617641
Branch = $wt.Branch
618642
Container = $ct.Name
619643
Theme = $colors.Name
644+
Status = "Ready"
620645
}
621646
}
622647

scripts/tear-down-agents.ps1

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
<#
22
Stop and remove fw-agent-* containers and optionally clean up agents/* caches.
3-
Worktrees themselves are preserved for reuse (we still warn about uncommitted changes
4-
unless -ForceRemoveDirty is specified).
3+
Worktrees themselves are preserved for reuse.
4+
5+
SAFETY: This script will ERROR and refuse to remove worktrees that have uncommitted
6+
changes, preventing accidental data loss. Use -ForceRemoveDirty only if you're certain
7+
you want to discard uncommitted work.
58
69
# Examples
710
# .\scripts\tear-down-agents.ps1 -RepoRoot "C:\dev\FieldWorks"
811
# (Stops all fw-agent-* containers but leaves worktrees/branches.)
912
#
1013
# .\scripts\tear-down-agents.ps1 -RepoRoot "C:\dev\FieldWorks" -RemoveWorktrees -RemoveNuGetCaches
1114
# (Also removes agents/agent-* worktrees, git branches, and per-agent NuGet caches.)
15+
# (Will ERROR if any worktree has uncommitted changes.)
1216
#
1317
# .\scripts\tear-down-agents.ps1 -RepoRoot "C:\dev\FieldWorks" -RemoveWorktrees -ForceRemoveDirty
14-
# (Removes worktrees without prompting even if uncommitted changes exist.)
18+
# (⚠️ DANGEROUS: Removes worktrees even with uncommitted changes - DATA LOSS!)
1519
#>
1620

1721
[CmdletBinding()]
@@ -191,9 +195,22 @@ $removeCaches = $RemoveWorktrees -or $RemoveNuGetCaches
191195
if (Test-Path -LiteralPath $wtPath) {
192196
$hasChanges = Test-WorktreeHasUncommittedChanges -WorktreePath $wtPath
193197
if ($hasChanges -and -not $ForceRemoveDirty) {
194-
Write-Warning "Worktree agent-$i has uncommitted changes; content will remain but tracking will be detached (use -ForceRemoveDirty to skip this warning)."
198+
throw @"
199+
Worktree agent-$i has uncommitted changes at: $wtPath
200+
201+
To protect your work, tear-down will NOT remove this worktree.
202+
203+
Options:
204+
1. Commit or stash your changes in the worktree, then re-run tear-down
205+
2. Push your changes to a remote branch for safekeeping
206+
3. Use -ForceRemoveDirty to override (WARNING: This will DELETE uncommitted work)
207+
208+
To check what's uncommitted:
209+
cd '$wtPath'
210+
git status
211+
"@
195212
}
196-
Write-Host "Detaching worktree agent-$i while leaving $wtPath on disk."
213+
Write-Host "Detaching worktree agent-$i (no uncommitted changes detected)."
197214
} else {
198215
Write-Host "Worktree agent-$i not found on disk; only detaching branch metadata."
199216
}

0 commit comments

Comments
 (0)