@caddis/cli 0.4.27 → 0.4.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/skills/adopt-app/SKILL.md +7 -0
- package/bundles/antigravity-plugin/skills/adopt-app/references/standard.md +39 -6
- package/bundles/antigravity-plugin/skills/adopt-app/scripts/AdoptCommon.ps1 +59 -1
- package/bundles/antigravity-plugin/skills/adopt-app/scripts/Get-AppInventory.ps1 +39 -0
- package/bundles/antigravity-plugin/skills/adopt-app/scripts/Test-AppConformance.ps1 +16 -0
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/hld/references/gold-standard-hld.md +1 -1
- package/bundles/codex/.codex/skills/devops/adopt-app/SKILL.md +7 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/references/standard.md +39 -6
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/AdoptCommon.ps1 +59 -1
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Get-AppInventory.ps1 +39 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Test-AppConformance.ps1 +16 -0
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
|
@@ -74,6 +74,13 @@ facts - services, scheduled tasks, web sites - can only be read on the box itsel
|
|
|
74
74
|
flag yourself.** A credential that was ever committed must be ROTATED, not only deleted.
|
|
75
75
|
- **What runs it.** If nothing on the host references the folder and the app is live, it runs
|
|
76
76
|
somewhere you have not looked. Find out where before planning anything.
|
|
77
|
+
- **COMMITS NO PORT WOULD CARRY.** A worktree is an extra folder, not an extra repository, so
|
|
78
|
+
its branch travels with a clone like any other. A **detached HEAD** does not: a clone copies
|
|
79
|
+
`refs/heads/*` and `refs/tags/*`, and a detached HEAD is neither. Nothing points at those
|
|
80
|
+
commits, so git may also collect them. If this section appears, fix it before anything else -
|
|
81
|
+
the report prints the exact `git branch` command, it changes nothing else, and it is the
|
|
82
|
+
difference between porting the work and losing it. Stashes are the same problem: `refs/stash`
|
|
83
|
+
is not copied by a clone or a push.
|
|
77
84
|
|
|
78
85
|
Junctions and symlinks are reported and never followed. A junction inside a tree that git manages
|
|
79
86
|
is a data-loss hazard: git's recursive remove follows it and deletes the target's contents.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The adoption standard
|
|
2
2
|
|
|
3
|
-
`Test-AppConformance.ps1` (phase 3) measures an app against
|
|
3
|
+
`Test-AppConformance.ps1` (phase 3) measures an app against 21 items, in the order below - the
|
|
4
4
|
same order the script adds them in. Each one gets a verdict, never a guess:
|
|
5
5
|
|
|
6
6
|
| Verdict | Means |
|
|
@@ -247,7 +247,40 @@ run it - should not require finding and interrupting someone.
|
|
|
247
247
|
**Why it is on the list:** without it, every coding agent that touches the app re-derives its
|
|
248
248
|
non-obvious rules from scratch, and different agents derive them differently.
|
|
249
249
|
|
|
250
|
-
### 19. `
|
|
250
|
+
### 19. `work-is-in-a-ref` - Every commit is reachable from a branch or a tag
|
|
251
|
+
|
|
252
|
+
**Blocking.**
|
|
253
|
+
|
|
254
|
+
**Decided by:** `GAP` when a worktree is on a DETACHED HEAD that no branch and no tag contains.
|
|
255
|
+
`ASK` when there are no unreachable commits but there are stashes, or worktrees with
|
|
256
|
+
uncommitted files. `ASK` when git could not be run. `PASS` otherwise.
|
|
257
|
+
|
|
258
|
+
**Why it is on the list.** A port copies refs. A clone takes `refs/heads/*` and `refs/tags/*`,
|
|
259
|
+
so a worktree's BRANCH travels with it like any other - a worktree is an extra folder, not an
|
|
260
|
+
extra repository. A **detached HEAD is neither a branch nor a tag**, so its commits are not
|
|
261
|
+
copied, and because nothing points at them git is free to collect them. The work is not
|
|
262
|
+
protected by being on disk; it is protected by being in a ref.
|
|
263
|
+
|
|
264
|
+
Measured on a real app: two detached worktrees under a temporary path that the app's own
|
|
265
|
+
documents called a "recurring external cleanup hazard", with three recorded incidents. They
|
|
266
|
+
held **200 commits** of certified work. `branch --contains` returned 0 for both. Every other
|
|
267
|
+
check passed; a port would have carried 127 branches and left behind the only copy of the part
|
|
268
|
+
that mattered.
|
|
269
|
+
|
|
270
|
+
**The fix costs nothing** and the report prints it per worktree:
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
git -C <repo> branch rescue/<worktree-name> <sha>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
It creates a branch name and changes nothing else. After it, `branch --contains` returns 1 and
|
|
277
|
+
the commits port like anything else.
|
|
278
|
+
|
|
279
|
+
Stashes are the same class of problem for a smaller amount of work: `refs/stash` is not copied
|
|
280
|
+
by a clone or a push. Uncommitted files in a worktree are invisible to the main checkout's
|
|
281
|
+
`git status`, which is why the inventory reports each worktree's own count.
|
|
282
|
+
|
|
283
|
+
### 20. `no-junctions` - No junction or symlink inside the tree git manages
|
|
251
284
|
|
|
252
285
|
**Not blocking.**
|
|
253
286
|
|
|
@@ -261,7 +294,7 @@ Ordinary Windows delete operations (`Remove-Item -Recurse`, `cmd /c rmdir /s /q`
|
|
|
261
294
|
this trap easy to miss: a delete you tested yourself can look completely safe right up until git
|
|
262
295
|
does the deleting instead.
|
|
263
296
|
|
|
264
|
-
###
|
|
297
|
+
### 21. `profile` - The stack is declared, not assumed
|
|
265
298
|
|
|
266
299
|
**Blocking.**
|
|
267
300
|
|
|
@@ -308,9 +341,9 @@ what it really is.
|
|
|
308
341
|
| `1` | At least one blocking item is `GAP` and not waived. Checked first - if both a blocking `GAP` and a blocking unanswered `ASK` exist at once, exit code `1` wins. |
|
|
309
342
|
| `2` | No blocking `GAP` remains, but at least one blocking item is still an unanswered `ASK` and not waived. |
|
|
310
343
|
|
|
311
|
-
The
|
|
312
|
-
`remote-on-team-host`, `canonical-branch`, `source-only`, `no-secrets`, `
|
|
313
|
-
and `profile`. Which items block is decided in the script, by the `Blocking` argument to each
|
|
344
|
+
The nine blocking items, as the code actually enforces them, are: `git-repo`,
|
|
345
|
+
`remote-on-team-host`, `canonical-branch`, `source-only`, `no-secrets`, `work-is-in-a-ref`,
|
|
346
|
+
`run-declared`, `owner` and `profile`. Which items block is decided in the script, by the `Blocking` argument to each
|
|
314
347
|
item, and not by the project config. An earlier draft of the config carried a `blockingItems`
|
|
315
348
|
list that nothing read and that had drifted out of step with this set; it was removed from the
|
|
316
349
|
code and from `config.example.json` together, rather than documented as a thing that looks
|
|
@@ -196,7 +196,8 @@ $script:AdoptScanBlock = {
|
|
|
196
196
|
# a key that is missing because one git call did not run.
|
|
197
197
|
$g = @{ isRepo = $true; gitAvailable = $gitAvailable; gitError = $gitError; commits = $null; branch = $null
|
|
198
198
|
localBranches = 0; remoteBranches = 0; tags = 0; dirtyFiles = $null
|
|
199
|
-
remotes = @(); upstream = $null; unpushed = $null; lastCommit = $null
|
|
199
|
+
remotes = @(); upstream = $null; unpushed = $null; lastCommit = $null
|
|
200
|
+
worktrees = @(); unreachableCommits = @(); stashes = 0 }
|
|
200
201
|
$result.git = $g
|
|
201
202
|
if (-not $gitAvailable) {
|
|
202
203
|
$result.partial = $true
|
|
@@ -231,6 +232,63 @@ $script:AdoptScanBlock = {
|
|
|
231
232
|
if ($ahead) { $g.unpushed = [int]"$ahead" }
|
|
232
233
|
}
|
|
233
234
|
else { $g.upstream = $null; $g.unpushed = $null }
|
|
235
|
+
|
|
236
|
+
# ---- worktrees, and the commits a port would silently leave behind ----------
|
|
237
|
+
#
|
|
238
|
+
# A worktree is an extra FOLDER, not an extra repository: its branch lives in
|
|
239
|
+
# this same .git, so a mirror clone carries it. What a mirror does NOT carry is
|
|
240
|
+
# a DETACHED HEAD, because a mirror copies refs/heads/* and refs/tags/* and a
|
|
241
|
+
# detached HEAD is neither. Nothing points at those commits, so git is also free
|
|
242
|
+
# to collect them.
|
|
243
|
+
#
|
|
244
|
+
# This is not hypothetical. A real app had two detached worktrees on a temp path
|
|
245
|
+
# its own documents called a "recurring cleanup hazard", holding 200 commits of
|
|
246
|
+
# certified work. `branches containing it` was 0 for both. One `git worktree add`
|
|
247
|
+
# months earlier, and a port would have taken everything except the part that
|
|
248
|
+
# mattered.
|
|
249
|
+
$g.worktrees = @()
|
|
250
|
+
$g.unreachableCommits = @()
|
|
251
|
+
$wtOut = & $runGit @('worktree', 'list', '--porcelain')
|
|
252
|
+
if ($wtOut) {
|
|
253
|
+
$cur = $null
|
|
254
|
+
foreach ($line in @($wtOut)) {
|
|
255
|
+
$s = "$line"
|
|
256
|
+
if ($s -match '^worktree\s+(.+)$') {
|
|
257
|
+
if ($cur) { $g.worktrees += $cur }
|
|
258
|
+
$cur = @{ path = $Matches[1]; branch = $null; head = $null; detached = $false; dirtyFiles = $null; isMain = $false }
|
|
259
|
+
}
|
|
260
|
+
elseif ($s -match '^HEAD\s+([0-9a-f]{40})$' -and $cur) { $cur.head = $Matches[1] }
|
|
261
|
+
elseif ($s -match '^branch\s+refs/heads/(.+)$' -and $cur) { $cur.branch = $Matches[1] }
|
|
262
|
+
elseif ($s -eq 'detached' -and $cur) { $cur.detached = $true }
|
|
263
|
+
}
|
|
264
|
+
if ($cur) { $g.worktrees += $cur }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
foreach ($w in @($g.worktrees)) {
|
|
268
|
+
$w.isMain = ($w.path.TrimEnd('\', '/') -eq $root)
|
|
269
|
+
# Its OWN uncommitted files. A worktree has a separate working tree, so the
|
|
270
|
+
# main checkout's `git status` says nothing about it.
|
|
271
|
+
if (Test-Path -LiteralPath $w.path) {
|
|
272
|
+
$st = & git -c safe.directory=* -C $w.path status --porcelain 2>&1
|
|
273
|
+
if ($LASTEXITCODE -eq 0) { $w.dirtyFiles = @($st | Where-Object { "$_" -ne '' }).Count }
|
|
274
|
+
}
|
|
275
|
+
# The finding that matters: is this HEAD reachable from any branch or tag?
|
|
276
|
+
if ($w.detached -and $w.head) {
|
|
277
|
+
$containing = @(& git -c safe.directory=* -C $root branch --contains $w.head 2>&1 | Where-Object { "$_" -notmatch '^(error|fatal)' -and "$_" -ne '' })
|
|
278
|
+
$tagged = @(& git -c safe.directory=* -C $root tag --contains $w.head 2>&1 | Where-Object { "$_" -notmatch '^(error|fatal)' -and "$_" -ne '' })
|
|
279
|
+
if ($containing.Count -eq 0 -and $tagged.Count -eq 0) {
|
|
280
|
+
$ahead = & git -c safe.directory=* -C $root rev-list --count "$($w.head)" 2>&1
|
|
281
|
+
$g.unreachableCommits += @{
|
|
282
|
+
path = $w.path; head = $w.head
|
|
283
|
+
commits = $(if ($LASTEXITCODE -eq 0) { [int]"$ahead" } else { $null })
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# A stash is a ref, but `refs/stash` is not copied by a normal clone or push.
|
|
290
|
+
$stash = & $runGit @('stash', 'list')
|
|
291
|
+
$g.stashes = @($stash | Where-Object { "$_" -ne '' }).Count
|
|
234
292
|
}
|
|
235
293
|
|
|
236
294
|
# ---- which files git actually tracks ---------------------------------------------
|
|
@@ -166,6 +166,45 @@ elseif ($inventory.git.isRepo) {
|
|
|
166
166
|
}
|
|
167
167
|
else { Write-Host " repo : NO - there is no history to port; adoption starts with one import commit" -ForegroundColor Yellow }
|
|
168
168
|
|
|
169
|
+
if ($inventory.git.isRepo) {
|
|
170
|
+
$wts = @(Get-AdoptProp $inventory.git 'worktrees' @())
|
|
171
|
+
$stashes = [int](Get-AdoptProp $inventory.git 'stashes' 0)
|
|
172
|
+
if ($wts.Count -gt 1 -or $stashes -gt 0) {
|
|
173
|
+
Write-AdoptHeading 'Worktrees and stashes'
|
|
174
|
+
foreach ($w in $wts) {
|
|
175
|
+
$what = if ($w.branch) { "on $($w.branch)" } else { "DETACHED at $("$($w.head)".Substring(0,9))" }
|
|
176
|
+
$dirty = if ($null -eq $w.dirtyFiles) { 'unreadable' } else { "$($w.dirtyFiles) uncommitted" }
|
|
177
|
+
$tag = if ($w.isMain) { ' (the main checkout)' } else { '' }
|
|
178
|
+
Write-Host (" {0,-58} {1,-34} {2}{3}" -f $w.path, $what, $dirty, $tag)
|
|
179
|
+
}
|
|
180
|
+
if ($stashes -gt 0) {
|
|
181
|
+
Write-Host " $stashes stash entr(ies) - `refs/stash` is NOT copied by a clone or a push." -ForegroundColor Yellow
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# THE ONE THAT LOSES WORK. A mirror copies refs/heads/* and refs/tags/*; a detached
|
|
186
|
+
# HEAD is neither, so its commits are not carried - and with nothing pointing at them,
|
|
187
|
+
# git may collect them. Found on a real app: 200 commits of certified work on two
|
|
188
|
+
# detached worktrees, under a path its own docs called a recurring cleanup hazard.
|
|
189
|
+
$unreachable = @(Get-AdoptProp $inventory.git 'unreachableCommits' @())
|
|
190
|
+
if ($unreachable.Count -gt 0) {
|
|
191
|
+
Write-AdoptHeading 'COMMITS NO PORT WOULD CARRY'
|
|
192
|
+
Write-Host " These worktrees are on a DETACHED HEAD that no branch and no tag points at." -ForegroundColor Red
|
|
193
|
+
Write-Host " A clone copies branches and tags. These are neither, so they would be left" -ForegroundColor Red
|
|
194
|
+
Write-Host " behind - and git is free to delete them." -ForegroundColor Red
|
|
195
|
+
foreach ($u in $unreachable) {
|
|
196
|
+
$n = if ($u.commits) { "$($u.commits) commits" } else { 'unknown depth' }
|
|
197
|
+
Write-Host (" {0}`n HEAD {1} ({2})" -f $u.path, "$($u.head)".Substring(0,9), $n) -ForegroundColor Red
|
|
198
|
+
}
|
|
199
|
+
Write-Host ""
|
|
200
|
+
Write-Host " FIX IT BEFORE PORTING - one command each, and it changes nothing else:" -ForegroundColor Yellow
|
|
201
|
+
foreach ($u in $unreachable) {
|
|
202
|
+
$leaf = (Split-Path $u.path -Leaf) -replace '[^A-Za-z0-9._-]', '-'
|
|
203
|
+
Write-Host " git -C <repo> branch rescue/$leaf $("$($u.head)".Substring(0,9))" -ForegroundColor Yellow
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
169
208
|
# Neither committed nor ignored. This is the state that turns one careless `git add -A`
|
|
170
209
|
# into gigabytes of runtime data in the history for ever, and no filename pattern finds
|
|
171
210
|
# it - only git can tell an undeclared data tree from a source folder.
|
|
@@ -254,6 +254,22 @@ Add-Item 'agent-rules' 'A rules file the coding agents read' $false `
|
|
|
254
254
|
$(if ($hasAgentRules) { (@($inv.docs | Where-Object { $_ -match '(?i)^(agents|claude)\.md$' }) -join ', ') } else { 'none' }) `
|
|
255
255
|
'' 'Without it every agent re-derives the project rules, and each one derives them differently.'
|
|
256
256
|
|
|
257
|
+
# A port copies refs. Work that is in no ref is work a port silently drops.
|
|
258
|
+
$unreachable = @(Get-AdoptProp $inv.git 'unreachableCommits' @())
|
|
259
|
+
$stashCount = [int](Get-AdoptProp $inv.git 'stashes' 0)
|
|
260
|
+
$wtDirty = @(@(Get-AdoptProp $inv.git 'worktrees' @()) | Where-Object { $_.dirtyFiles -and $_.dirtyFiles -gt 0 })
|
|
261
|
+
Add-Item 'work-is-in-a-ref' 'Every commit is reachable from a branch or a tag' $true `
|
|
262
|
+
$(if (-not $gitUsable) { 'ASK' } elseif ($unreachable.Count -gt 0) { 'GAP' } elseif ($stashCount -gt 0 -or $wtDirty.Count -gt 0) { 'ASK' } else { 'PASS' }) `
|
|
263
|
+
$(if ($unreachable.Count -gt 0) {
|
|
264
|
+
"DETACHED and unreferenced: " + ((@($unreachable | ForEach-Object { "$($_.path) ($($_.commits) commits)" })) -join '; ')
|
|
265
|
+
}
|
|
266
|
+
elseif ($stashCount -gt 0 -or $wtDirty.Count -gt 0) {
|
|
267
|
+
"no unreachable commits, but $stashCount stash(es) and $($wtDirty.Count) worktree(s) with uncommitted files - none of that is in a ref either"
|
|
268
|
+
}
|
|
269
|
+
else { 'every worktree is on a branch; no stashes' }) `
|
|
270
|
+
'For each stash and each uncommitted file: does that work matter? Anything that does needs a branch before the port.' `
|
|
271
|
+
'A clone copies refs/heads/* and refs/tags/*. A detached HEAD is neither, so its commits are not carried - and nothing points at them, so git may collect them. `git branch <name> <sha>` costs nothing and fixes it.'
|
|
272
|
+
|
|
257
273
|
Add-Item 'no-junctions' 'No junction or symlink inside the tree git manages' $false `
|
|
258
274
|
$(if ($junctionsInTree.Count -eq 0) { 'PASS' } else { 'GAP' }) `
|
|
259
275
|
$(if ($junctionsInTree.Count -eq 0) { 'none outside dependency folders' } else { "$($junctionsInTree.Count): $((@($junctionsInTree | Select-Object -First 3 | ForEach-Object { $_.path })) -join ', ')" }) `
|
|
@@ -139,7 +139,7 @@ The sequence diagram below illustrates the batch execution sequence from cycle t
|
|
|
139
139
|
```mermaid
|
|
140
140
|
sequenceDiagram
|
|
141
141
|
autonumber
|
|
142
|
-
participant Sched as
|
|
142
|
+
participant Sched as Batch Scheduler (PROPOSED - not built)
|
|
143
143
|
participant Orch as Batch Orchestrator
|
|
144
144
|
participant Reader as Tagged-Record Ingestor
|
|
145
145
|
participant Reconcile as Financial Reconciliation Gate
|
|
@@ -74,6 +74,13 @@ facts - services, scheduled tasks, web sites - can only be read on the box itsel
|
|
|
74
74
|
flag yourself.** A credential that was ever committed must be ROTATED, not only deleted.
|
|
75
75
|
- **What runs it.** If nothing on the host references the folder and the app is live, it runs
|
|
76
76
|
somewhere you have not looked. Find out where before planning anything.
|
|
77
|
+
- **COMMITS NO PORT WOULD CARRY.** A worktree is an extra folder, not an extra repository, so
|
|
78
|
+
its branch travels with a clone like any other. A **detached HEAD** does not: a clone copies
|
|
79
|
+
`refs/heads/*` and `refs/tags/*`, and a detached HEAD is neither. Nothing points at those
|
|
80
|
+
commits, so git may also collect them. If this section appears, fix it before anything else -
|
|
81
|
+
the report prints the exact `git branch` command, it changes nothing else, and it is the
|
|
82
|
+
difference between porting the work and losing it. Stashes are the same problem: `refs/stash`
|
|
83
|
+
is not copied by a clone or a push.
|
|
77
84
|
|
|
78
85
|
Junctions and symlinks are reported and never followed. A junction inside a tree that git manages
|
|
79
86
|
is a data-loss hazard: git's recursive remove follows it and deletes the target's contents.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The adoption standard
|
|
2
2
|
|
|
3
|
-
`Test-AppConformance.ps1` (phase 3) measures an app against
|
|
3
|
+
`Test-AppConformance.ps1` (phase 3) measures an app against 21 items, in the order below - the
|
|
4
4
|
same order the script adds them in. Each one gets a verdict, never a guess:
|
|
5
5
|
|
|
6
6
|
| Verdict | Means |
|
|
@@ -247,7 +247,40 @@ run it - should not require finding and interrupting someone.
|
|
|
247
247
|
**Why it is on the list:** without it, every coding agent that touches the app re-derives its
|
|
248
248
|
non-obvious rules from scratch, and different agents derive them differently.
|
|
249
249
|
|
|
250
|
-
### 19. `
|
|
250
|
+
### 19. `work-is-in-a-ref` - Every commit is reachable from a branch or a tag
|
|
251
|
+
|
|
252
|
+
**Blocking.**
|
|
253
|
+
|
|
254
|
+
**Decided by:** `GAP` when a worktree is on a DETACHED HEAD that no branch and no tag contains.
|
|
255
|
+
`ASK` when there are no unreachable commits but there are stashes, or worktrees with
|
|
256
|
+
uncommitted files. `ASK` when git could not be run. `PASS` otherwise.
|
|
257
|
+
|
|
258
|
+
**Why it is on the list.** A port copies refs. A clone takes `refs/heads/*` and `refs/tags/*`,
|
|
259
|
+
so a worktree's BRANCH travels with it like any other - a worktree is an extra folder, not an
|
|
260
|
+
extra repository. A **detached HEAD is neither a branch nor a tag**, so its commits are not
|
|
261
|
+
copied, and because nothing points at them git is free to collect them. The work is not
|
|
262
|
+
protected by being on disk; it is protected by being in a ref.
|
|
263
|
+
|
|
264
|
+
Measured on a real app: two detached worktrees under a temporary path that the app's own
|
|
265
|
+
documents called a "recurring external cleanup hazard", with three recorded incidents. They
|
|
266
|
+
held **200 commits** of certified work. `branch --contains` returned 0 for both. Every other
|
|
267
|
+
check passed; a port would have carried 127 branches and left behind the only copy of the part
|
|
268
|
+
that mattered.
|
|
269
|
+
|
|
270
|
+
**The fix costs nothing** and the report prints it per worktree:
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
git -C <repo> branch rescue/<worktree-name> <sha>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
It creates a branch name and changes nothing else. After it, `branch --contains` returns 1 and
|
|
277
|
+
the commits port like anything else.
|
|
278
|
+
|
|
279
|
+
Stashes are the same class of problem for a smaller amount of work: `refs/stash` is not copied
|
|
280
|
+
by a clone or a push. Uncommitted files in a worktree are invisible to the main checkout's
|
|
281
|
+
`git status`, which is why the inventory reports each worktree's own count.
|
|
282
|
+
|
|
283
|
+
### 20. `no-junctions` - No junction or symlink inside the tree git manages
|
|
251
284
|
|
|
252
285
|
**Not blocking.**
|
|
253
286
|
|
|
@@ -261,7 +294,7 @@ Ordinary Windows delete operations (`Remove-Item -Recurse`, `cmd /c rmdir /s /q`
|
|
|
261
294
|
this trap easy to miss: a delete you tested yourself can look completely safe right up until git
|
|
262
295
|
does the deleting instead.
|
|
263
296
|
|
|
264
|
-
###
|
|
297
|
+
### 21. `profile` - The stack is declared, not assumed
|
|
265
298
|
|
|
266
299
|
**Blocking.**
|
|
267
300
|
|
|
@@ -308,9 +341,9 @@ what it really is.
|
|
|
308
341
|
| `1` | At least one blocking item is `GAP` and not waived. Checked first - if both a blocking `GAP` and a blocking unanswered `ASK` exist at once, exit code `1` wins. |
|
|
309
342
|
| `2` | No blocking `GAP` remains, but at least one blocking item is still an unanswered `ASK` and not waived. |
|
|
310
343
|
|
|
311
|
-
The
|
|
312
|
-
`remote-on-team-host`, `canonical-branch`, `source-only`, `no-secrets`, `
|
|
313
|
-
and `profile`. Which items block is decided in the script, by the `Blocking` argument to each
|
|
344
|
+
The nine blocking items, as the code actually enforces them, are: `git-repo`,
|
|
345
|
+
`remote-on-team-host`, `canonical-branch`, `source-only`, `no-secrets`, `work-is-in-a-ref`,
|
|
346
|
+
`run-declared`, `owner` and `profile`. Which items block is decided in the script, by the `Blocking` argument to each
|
|
314
347
|
item, and not by the project config. An earlier draft of the config carried a `blockingItems`
|
|
315
348
|
list that nothing read and that had drifted out of step with this set; it was removed from the
|
|
316
349
|
code and from `config.example.json` together, rather than documented as a thing that looks
|
|
@@ -196,7 +196,8 @@ $script:AdoptScanBlock = {
|
|
|
196
196
|
# a key that is missing because one git call did not run.
|
|
197
197
|
$g = @{ isRepo = $true; gitAvailable = $gitAvailable; gitError = $gitError; commits = $null; branch = $null
|
|
198
198
|
localBranches = 0; remoteBranches = 0; tags = 0; dirtyFiles = $null
|
|
199
|
-
remotes = @(); upstream = $null; unpushed = $null; lastCommit = $null
|
|
199
|
+
remotes = @(); upstream = $null; unpushed = $null; lastCommit = $null
|
|
200
|
+
worktrees = @(); unreachableCommits = @(); stashes = 0 }
|
|
200
201
|
$result.git = $g
|
|
201
202
|
if (-not $gitAvailable) {
|
|
202
203
|
$result.partial = $true
|
|
@@ -231,6 +232,63 @@ $script:AdoptScanBlock = {
|
|
|
231
232
|
if ($ahead) { $g.unpushed = [int]"$ahead" }
|
|
232
233
|
}
|
|
233
234
|
else { $g.upstream = $null; $g.unpushed = $null }
|
|
235
|
+
|
|
236
|
+
# ---- worktrees, and the commits a port would silently leave behind ----------
|
|
237
|
+
#
|
|
238
|
+
# A worktree is an extra FOLDER, not an extra repository: its branch lives in
|
|
239
|
+
# this same .git, so a mirror clone carries it. What a mirror does NOT carry is
|
|
240
|
+
# a DETACHED HEAD, because a mirror copies refs/heads/* and refs/tags/* and a
|
|
241
|
+
# detached HEAD is neither. Nothing points at those commits, so git is also free
|
|
242
|
+
# to collect them.
|
|
243
|
+
#
|
|
244
|
+
# This is not hypothetical. A real app had two detached worktrees on a temp path
|
|
245
|
+
# its own documents called a "recurring cleanup hazard", holding 200 commits of
|
|
246
|
+
# certified work. `branches containing it` was 0 for both. One `git worktree add`
|
|
247
|
+
# months earlier, and a port would have taken everything except the part that
|
|
248
|
+
# mattered.
|
|
249
|
+
$g.worktrees = @()
|
|
250
|
+
$g.unreachableCommits = @()
|
|
251
|
+
$wtOut = & $runGit @('worktree', 'list', '--porcelain')
|
|
252
|
+
if ($wtOut) {
|
|
253
|
+
$cur = $null
|
|
254
|
+
foreach ($line in @($wtOut)) {
|
|
255
|
+
$s = "$line"
|
|
256
|
+
if ($s -match '^worktree\s+(.+)$') {
|
|
257
|
+
if ($cur) { $g.worktrees += $cur }
|
|
258
|
+
$cur = @{ path = $Matches[1]; branch = $null; head = $null; detached = $false; dirtyFiles = $null; isMain = $false }
|
|
259
|
+
}
|
|
260
|
+
elseif ($s -match '^HEAD\s+([0-9a-f]{40})$' -and $cur) { $cur.head = $Matches[1] }
|
|
261
|
+
elseif ($s -match '^branch\s+refs/heads/(.+)$' -and $cur) { $cur.branch = $Matches[1] }
|
|
262
|
+
elseif ($s -eq 'detached' -and $cur) { $cur.detached = $true }
|
|
263
|
+
}
|
|
264
|
+
if ($cur) { $g.worktrees += $cur }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
foreach ($w in @($g.worktrees)) {
|
|
268
|
+
$w.isMain = ($w.path.TrimEnd('\', '/') -eq $root)
|
|
269
|
+
# Its OWN uncommitted files. A worktree has a separate working tree, so the
|
|
270
|
+
# main checkout's `git status` says nothing about it.
|
|
271
|
+
if (Test-Path -LiteralPath $w.path) {
|
|
272
|
+
$st = & git -c safe.directory=* -C $w.path status --porcelain 2>&1
|
|
273
|
+
if ($LASTEXITCODE -eq 0) { $w.dirtyFiles = @($st | Where-Object { "$_" -ne '' }).Count }
|
|
274
|
+
}
|
|
275
|
+
# The finding that matters: is this HEAD reachable from any branch or tag?
|
|
276
|
+
if ($w.detached -and $w.head) {
|
|
277
|
+
$containing = @(& git -c safe.directory=* -C $root branch --contains $w.head 2>&1 | Where-Object { "$_" -notmatch '^(error|fatal)' -and "$_" -ne '' })
|
|
278
|
+
$tagged = @(& git -c safe.directory=* -C $root tag --contains $w.head 2>&1 | Where-Object { "$_" -notmatch '^(error|fatal)' -and "$_" -ne '' })
|
|
279
|
+
if ($containing.Count -eq 0 -and $tagged.Count -eq 0) {
|
|
280
|
+
$ahead = & git -c safe.directory=* -C $root rev-list --count "$($w.head)" 2>&1
|
|
281
|
+
$g.unreachableCommits += @{
|
|
282
|
+
path = $w.path; head = $w.head
|
|
283
|
+
commits = $(if ($LASTEXITCODE -eq 0) { [int]"$ahead" } else { $null })
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# A stash is a ref, but `refs/stash` is not copied by a normal clone or push.
|
|
290
|
+
$stash = & $runGit @('stash', 'list')
|
|
291
|
+
$g.stashes = @($stash | Where-Object { "$_" -ne '' }).Count
|
|
234
292
|
}
|
|
235
293
|
|
|
236
294
|
# ---- which files git actually tracks ---------------------------------------------
|
|
@@ -166,6 +166,45 @@ elseif ($inventory.git.isRepo) {
|
|
|
166
166
|
}
|
|
167
167
|
else { Write-Host " repo : NO - there is no history to port; adoption starts with one import commit" -ForegroundColor Yellow }
|
|
168
168
|
|
|
169
|
+
if ($inventory.git.isRepo) {
|
|
170
|
+
$wts = @(Get-AdoptProp $inventory.git 'worktrees' @())
|
|
171
|
+
$stashes = [int](Get-AdoptProp $inventory.git 'stashes' 0)
|
|
172
|
+
if ($wts.Count -gt 1 -or $stashes -gt 0) {
|
|
173
|
+
Write-AdoptHeading 'Worktrees and stashes'
|
|
174
|
+
foreach ($w in $wts) {
|
|
175
|
+
$what = if ($w.branch) { "on $($w.branch)" } else { "DETACHED at $("$($w.head)".Substring(0,9))" }
|
|
176
|
+
$dirty = if ($null -eq $w.dirtyFiles) { 'unreadable' } else { "$($w.dirtyFiles) uncommitted" }
|
|
177
|
+
$tag = if ($w.isMain) { ' (the main checkout)' } else { '' }
|
|
178
|
+
Write-Host (" {0,-58} {1,-34} {2}{3}" -f $w.path, $what, $dirty, $tag)
|
|
179
|
+
}
|
|
180
|
+
if ($stashes -gt 0) {
|
|
181
|
+
Write-Host " $stashes stash entr(ies) - `refs/stash` is NOT copied by a clone or a push." -ForegroundColor Yellow
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# THE ONE THAT LOSES WORK. A mirror copies refs/heads/* and refs/tags/*; a detached
|
|
186
|
+
# HEAD is neither, so its commits are not carried - and with nothing pointing at them,
|
|
187
|
+
# git may collect them. Found on a real app: 200 commits of certified work on two
|
|
188
|
+
# detached worktrees, under a path its own docs called a recurring cleanup hazard.
|
|
189
|
+
$unreachable = @(Get-AdoptProp $inventory.git 'unreachableCommits' @())
|
|
190
|
+
if ($unreachable.Count -gt 0) {
|
|
191
|
+
Write-AdoptHeading 'COMMITS NO PORT WOULD CARRY'
|
|
192
|
+
Write-Host " These worktrees are on a DETACHED HEAD that no branch and no tag points at." -ForegroundColor Red
|
|
193
|
+
Write-Host " A clone copies branches and tags. These are neither, so they would be left" -ForegroundColor Red
|
|
194
|
+
Write-Host " behind - and git is free to delete them." -ForegroundColor Red
|
|
195
|
+
foreach ($u in $unreachable) {
|
|
196
|
+
$n = if ($u.commits) { "$($u.commits) commits" } else { 'unknown depth' }
|
|
197
|
+
Write-Host (" {0}`n HEAD {1} ({2})" -f $u.path, "$($u.head)".Substring(0,9), $n) -ForegroundColor Red
|
|
198
|
+
}
|
|
199
|
+
Write-Host ""
|
|
200
|
+
Write-Host " FIX IT BEFORE PORTING - one command each, and it changes nothing else:" -ForegroundColor Yellow
|
|
201
|
+
foreach ($u in $unreachable) {
|
|
202
|
+
$leaf = (Split-Path $u.path -Leaf) -replace '[^A-Za-z0-9._-]', '-'
|
|
203
|
+
Write-Host " git -C <repo> branch rescue/$leaf $("$($u.head)".Substring(0,9))" -ForegroundColor Yellow
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
169
208
|
# Neither committed nor ignored. This is the state that turns one careless `git add -A`
|
|
170
209
|
# into gigabytes of runtime data in the history for ever, and no filename pattern finds
|
|
171
210
|
# it - only git can tell an undeclared data tree from a source folder.
|
|
@@ -254,6 +254,22 @@ Add-Item 'agent-rules' 'A rules file the coding agents read' $false `
|
|
|
254
254
|
$(if ($hasAgentRules) { (@($inv.docs | Where-Object { $_ -match '(?i)^(agents|claude)\.md$' }) -join ', ') } else { 'none' }) `
|
|
255
255
|
'' 'Without it every agent re-derives the project rules, and each one derives them differently.'
|
|
256
256
|
|
|
257
|
+
# A port copies refs. Work that is in no ref is work a port silently drops.
|
|
258
|
+
$unreachable = @(Get-AdoptProp $inv.git 'unreachableCommits' @())
|
|
259
|
+
$stashCount = [int](Get-AdoptProp $inv.git 'stashes' 0)
|
|
260
|
+
$wtDirty = @(@(Get-AdoptProp $inv.git 'worktrees' @()) | Where-Object { $_.dirtyFiles -and $_.dirtyFiles -gt 0 })
|
|
261
|
+
Add-Item 'work-is-in-a-ref' 'Every commit is reachable from a branch or a tag' $true `
|
|
262
|
+
$(if (-not $gitUsable) { 'ASK' } elseif ($unreachable.Count -gt 0) { 'GAP' } elseif ($stashCount -gt 0 -or $wtDirty.Count -gt 0) { 'ASK' } else { 'PASS' }) `
|
|
263
|
+
$(if ($unreachable.Count -gt 0) {
|
|
264
|
+
"DETACHED and unreferenced: " + ((@($unreachable | ForEach-Object { "$($_.path) ($($_.commits) commits)" })) -join '; ')
|
|
265
|
+
}
|
|
266
|
+
elseif ($stashCount -gt 0 -or $wtDirty.Count -gt 0) {
|
|
267
|
+
"no unreachable commits, but $stashCount stash(es) and $($wtDirty.Count) worktree(s) with uncommitted files - none of that is in a ref either"
|
|
268
|
+
}
|
|
269
|
+
else { 'every worktree is on a branch; no stashes' }) `
|
|
270
|
+
'For each stash and each uncommitted file: does that work matter? Anything that does needs a branch before the port.' `
|
|
271
|
+
'A clone copies refs/heads/* and refs/tags/*. A detached HEAD is neither, so its commits are not carried - and nothing points at them, so git may collect them. `git branch <name> <sha>` costs nothing and fixes it.'
|
|
272
|
+
|
|
257
273
|
Add-Item 'no-junctions' 'No junction or symlink inside the tree git manages' $false `
|
|
258
274
|
$(if ($junctionsInTree.Count -eq 0) { 'PASS' } else { 'GAP' }) `
|
|
259
275
|
$(if ($junctionsInTree.Count -eq 0) { 'none outside dependency folders' } else { "$($junctionsInTree.Count): $((@($junctionsInTree | Select-Object -First 3 | ForEach-Object { $_.path })) -join ', ')" }) `
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED