devrites 5.2.4 → 5.4.0
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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/engine/internal/install/uninstall.go +3 -0
- package/engine/internal/lib/resolve.go +7 -0
- package/engine/internal/parallel/cli.go +62 -8
- package/engine/internal/parallel/git.go +46 -7
- package/engine/internal/parallel/lease.go +7 -0
- package/engine/internal/parallel/ops.go +256 -72
- package/engine/internal/parallel/ops_test.go +223 -6
- package/engine/internal/parallel/parallel_test.go +229 -6
- package/engine/internal/parallel/pathdisjoint.go +18 -3
- package/engine/internal/parallel/pathdisjoint_test.go +21 -0
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +2 -3
- package/pack/.claude/skills/rite-build/SKILL.md +9 -3
- package/pack/.claude/skills/rite-build/reference/checkpoint.md +13 -15
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +2 -2
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +87 -20
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +2 -3
- package/pack/generated/claude/skills/rite-build/SKILL.md +9 -3
- package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +13 -15
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +2 -2
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +87 -20
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +2 -3
- package/pack/generated/codex/skills/rite-build/SKILL.md +9 -3
- package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +13 -15
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +2 -2
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +87 -20
- package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +2 -3
- package/pack/generated/omp/skills/rite-build/SKILL.md +9 -3
- package/pack/generated/omp/skills/rite-build/reference/checkpoint.md +13 -15
- package/pack/generated/omp/skills/rite-build/reference/one-slice-cycle.md +2 -2
- package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +87 -20
- package/package.json +1 -1
|
@@ -7,6 +7,9 @@ import (
|
|
|
7
7
|
"path/filepath"
|
|
8
8
|
"sort"
|
|
9
9
|
"strings"
|
|
10
|
+
|
|
11
|
+
"github.com/devrites/devrites/internal/devritespaths"
|
|
12
|
+
"github.com/devrites/devrites/internal/state"
|
|
10
13
|
)
|
|
11
14
|
|
|
12
15
|
// Test seams: indirection over the real primitives so discriminating tests
|
|
@@ -14,6 +17,7 @@ import (
|
|
|
14
17
|
var (
|
|
15
18
|
writeLease = WriteLease
|
|
16
19
|
removeWorktree = worktreeRemove
|
|
20
|
+
salvage = salvageSlice
|
|
17
21
|
warnf = func(format string, args ...any) {
|
|
18
22
|
fmt.Fprintf(os.Stderr, "warning: parallel: "+format+"\n", args...)
|
|
19
23
|
}
|
|
@@ -29,11 +33,35 @@ type CreateOpts struct {
|
|
|
29
33
|
Slices []SlicePaths
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
// withLeaseLock serializes lease read-modify-write across engine processes:
|
|
37
|
+
// the host may issue record-green/integrate calls concurrently, and a
|
|
38
|
+
// lock-free pair of them would lose one update.
|
|
39
|
+
func withLeaseLock(repoRoot, slug string, fn func() error) error {
|
|
40
|
+
if err := validateSlug(slug); err != nil {
|
|
41
|
+
return err
|
|
42
|
+
}
|
|
43
|
+
return state.WithFeatureLock(
|
|
44
|
+
filepath.Join(repoRoot, devritespaths.DevritesRootName), slug, fn)
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
// Create path-disjoint-gates, creates 2-10 worktrees from base, and writes a running lease.
|
|
33
|
-
func Create(opts CreateOpts) (*Lease, error) {
|
|
48
|
+
func Create(opts CreateOpts) (lease *Lease, err error) {
|
|
49
|
+
err = withLeaseLock(opts.Root, opts.Slug, func() error {
|
|
50
|
+
lease, err = createLocked(opts)
|
|
51
|
+
return err
|
|
52
|
+
})
|
|
53
|
+
return lease, err
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func createLocked(opts CreateOpts) (*Lease, error) {
|
|
34
57
|
if err := validateSlug(opts.Slug); err != nil {
|
|
35
58
|
return nil, err
|
|
36
59
|
}
|
|
60
|
+
// Store absolute worktree paths in the lease so cleanup's destructive ops
|
|
61
|
+
// never depend on the caller's working directory.
|
|
62
|
+
if abs, err := filepath.Abs(opts.Root); err == nil {
|
|
63
|
+
opts.Root = abs
|
|
64
|
+
}
|
|
37
65
|
if err := validateBatchID(opts.BatchID); err != nil {
|
|
38
66
|
return nil, err
|
|
39
67
|
}
|
|
@@ -123,11 +151,20 @@ func Create(opts CreateOpts) (*Lease, error) {
|
|
|
123
151
|
cleanupPartial()
|
|
124
152
|
return nil, err
|
|
125
153
|
}
|
|
154
|
+
ensureScratchExcluded(opts.Root)
|
|
126
155
|
return lease, nil
|
|
127
156
|
}
|
|
128
157
|
|
|
129
158
|
// RecordGreen marks a slice green with its transfer commit.
|
|
130
|
-
func RecordGreen(repoRoot, slug, sliceID, commit string) (*Lease, error) {
|
|
159
|
+
func RecordGreen(repoRoot, slug, sliceID, commit string) (lease *Lease, err error) {
|
|
160
|
+
err = withLeaseLock(repoRoot, slug, func() error {
|
|
161
|
+
lease, err = recordGreenLocked(repoRoot, slug, sliceID, commit)
|
|
162
|
+
return err
|
|
163
|
+
})
|
|
164
|
+
return lease, err
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func recordGreenLocked(repoRoot, slug, sliceID, commit string) (*Lease, error) {
|
|
131
168
|
leasePath, err := LeasePath(repoRoot, slug)
|
|
132
169
|
if err != nil {
|
|
133
170
|
return nil, err
|
|
@@ -136,8 +173,8 @@ func RecordGreen(repoRoot, slug, sliceID, commit string) (*Lease, error) {
|
|
|
136
173
|
if err != nil {
|
|
137
174
|
return nil, err
|
|
138
175
|
}
|
|
139
|
-
if lease.Status != StatusRunning {
|
|
140
|
-
return nil, fmt.Errorf("record-green requires status=running (have %s)", lease.Status)
|
|
176
|
+
if lease.Status != StatusRunning && lease.Status != StatusIntegrateFailed {
|
|
177
|
+
return nil, fmt.Errorf("record-green requires status=running|integrate-failed (have %s)", lease.Status)
|
|
141
178
|
}
|
|
142
179
|
sha, err := revParse(repoRoot, commit)
|
|
143
180
|
if err != nil {
|
|
@@ -161,8 +198,17 @@ func RecordGreen(repoRoot, slug, sliceID, commit string) (*Lease, error) {
|
|
|
161
198
|
return lease, nil
|
|
162
199
|
}
|
|
163
200
|
|
|
164
|
-
// Abort marks the lease aborted
|
|
165
|
-
|
|
201
|
+
// Abort marks the lease aborted. Control is never rewound: a moved HEAD is
|
|
202
|
+
// external work, so divergence is reported, not reset.
|
|
203
|
+
func Abort(repoRoot, slug string) (lease *Lease, err error) {
|
|
204
|
+
err = withLeaseLock(repoRoot, slug, func() error {
|
|
205
|
+
lease, err = abortLocked(repoRoot, slug)
|
|
206
|
+
return err
|
|
207
|
+
})
|
|
208
|
+
return lease, err
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func abortLocked(repoRoot, slug string) (*Lease, error) {
|
|
166
212
|
leasePath, err := LeasePath(repoRoot, slug)
|
|
167
213
|
if err != nil {
|
|
168
214
|
return nil, err
|
|
@@ -171,12 +217,15 @@ func Abort(repoRoot, slug string, force bool) (*Lease, error) {
|
|
|
171
217
|
if err != nil {
|
|
172
218
|
return nil, err
|
|
173
219
|
}
|
|
220
|
+
if lease.Status == StatusComplete {
|
|
221
|
+
return nil, fmt.Errorf("abort refused: lease already complete — the batch landed in control; nothing to discard")
|
|
222
|
+
}
|
|
174
223
|
base, err := revParse(repoRoot, lease.BaseSHA)
|
|
175
224
|
if err != nil {
|
|
176
225
|
return nil, err
|
|
177
226
|
}
|
|
178
|
-
if err := ensureControlAtBase(repoRoot, base
|
|
179
|
-
|
|
227
|
+
if err := ensureControlAtBase(repoRoot, base); err != nil {
|
|
228
|
+
warnf("control not at base: %v", err)
|
|
180
229
|
}
|
|
181
230
|
lease.Status = StatusAborted
|
|
182
231
|
if err := WriteLease(leasePath, lease); err != nil {
|
|
@@ -185,7 +234,10 @@ func Abort(repoRoot, slug string, force bool) (*Lease, error) {
|
|
|
185
234
|
return lease, nil
|
|
186
235
|
}
|
|
187
236
|
|
|
188
|
-
|
|
237
|
+
// ensureControlAtBase verifies the control tip still equals the batch base.
|
|
238
|
+
// It never mutates: a moved or dirty control belongs to the user, so callers
|
|
239
|
+
// only learn about divergence — they must not repair it by rewinding.
|
|
240
|
+
func ensureControlAtBase(repoRoot, base string) error {
|
|
189
241
|
head, err := headSHA(repoRoot)
|
|
190
242
|
if err != nil {
|
|
191
243
|
return err
|
|
@@ -193,24 +245,7 @@ func ensureControlAtBase(repoRoot, base string, force bool) error {
|
|
|
193
245
|
if head == base {
|
|
194
246
|
return nil
|
|
195
247
|
}
|
|
196
|
-
|
|
197
|
-
if err != nil {
|
|
198
|
-
return err
|
|
199
|
-
}
|
|
200
|
-
if dirty && !force {
|
|
201
|
-
return fmt.Errorf("control tree dirty; refuse reset to base (pass --force to override)")
|
|
202
|
-
}
|
|
203
|
-
if err := resetHard(repoRoot, base); err != nil {
|
|
204
|
-
return err
|
|
205
|
-
}
|
|
206
|
-
head, err = headSHA(repoRoot)
|
|
207
|
-
if err != nil {
|
|
208
|
-
return err
|
|
209
|
-
}
|
|
210
|
-
if head != base {
|
|
211
|
-
return fmt.Errorf("failed to leave control at base %s", base)
|
|
212
|
-
}
|
|
213
|
-
return nil
|
|
248
|
+
return fmt.Errorf("control head %s moved past base %s; refusing to rewind external commits", head, base)
|
|
214
249
|
}
|
|
215
250
|
|
|
216
251
|
// IntegrateOpts configures staging integrate.
|
|
@@ -218,11 +253,18 @@ type IntegrateOpts struct {
|
|
|
218
253
|
Root string
|
|
219
254
|
Slug string
|
|
220
255
|
ApplyToControl bool
|
|
221
|
-
Force bool
|
|
222
256
|
}
|
|
223
257
|
|
|
224
258
|
// Integrate all-or-nothing applies sibling transfer commits onto a staging branch.
|
|
225
259
|
func Integrate(opts IntegrateOpts) (tip string, lease *Lease, err error) {
|
|
260
|
+
err = withLeaseLock(opts.Root, opts.Slug, func() error {
|
|
261
|
+
tip, lease, err = integrateLocked(opts)
|
|
262
|
+
return err
|
|
263
|
+
})
|
|
264
|
+
return tip, lease, err
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func integrateLocked(opts IntegrateOpts) (tip string, lease *Lease, err error) {
|
|
226
268
|
leasePath, err := LeasePath(opts.Root, opts.Slug)
|
|
227
269
|
if err != nil {
|
|
228
270
|
return "", nil, err
|
|
@@ -231,17 +273,36 @@ func Integrate(opts IntegrateOpts) (tip string, lease *Lease, err error) {
|
|
|
231
273
|
if err != nil {
|
|
232
274
|
return "", nil, err
|
|
233
275
|
}
|
|
234
|
-
if lease.Status != StatusRunning {
|
|
235
|
-
return "", nil, fmt.Errorf("integrate requires status=running (have %s)", lease.Status)
|
|
276
|
+
if lease.Status != StatusRunning && lease.Status != StatusIntegrateFailed {
|
|
277
|
+
return "", nil, fmt.Errorf("integrate requires status=running|integrate-failed (have %s)", lease.Status)
|
|
236
278
|
}
|
|
237
279
|
base, err := revParse(opts.Root, lease.BaseSHA)
|
|
238
280
|
if err != nil {
|
|
239
281
|
return "", nil, err
|
|
240
282
|
}
|
|
241
|
-
if err := ensureControlAtBase(opts.Root, base
|
|
283
|
+
if err := ensureControlAtBase(opts.Root, base); err != nil {
|
|
242
284
|
return "", nil, err
|
|
243
285
|
}
|
|
244
286
|
|
|
287
|
+
union := make([]string, 0, len(lease.Slices))
|
|
288
|
+
ids := make([]string, 0, len(lease.Slices))
|
|
289
|
+
for _, sl := range lease.Slices {
|
|
290
|
+
union = append(union, sl.Paths...)
|
|
291
|
+
ids = append(ids, sl.ID)
|
|
292
|
+
}
|
|
293
|
+
if opts.ApplyToControl {
|
|
294
|
+
// FF into control must not silently absorb or clobber the user's
|
|
295
|
+
// uncommitted work on slice paths. Non-overlapping dirty files are
|
|
296
|
+
// left alone — they are not part of the batch.
|
|
297
|
+
dirty, err := porcelainDirtyPaths(opts.Root, union)
|
|
298
|
+
if err != nil {
|
|
299
|
+
return "", nil, err
|
|
300
|
+
}
|
|
301
|
+
if dirty {
|
|
302
|
+
return "", nil, fmt.Errorf("control has uncommitted changes on slice paths; commit or stash them before integrate --apply-to-control")
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
245
306
|
for _, sl := range lease.Slices {
|
|
246
307
|
if sl.WrightStatus != WrightGreen {
|
|
247
308
|
return "", nil, fmt.Errorf("slice %s is not green", sl.ID)
|
|
@@ -284,8 +345,8 @@ func Integrate(opts IntegrateOpts) (tip string, lease *Lease, err error) {
|
|
|
284
345
|
leasePath, StatusRunning, retryErr)
|
|
285
346
|
}
|
|
286
347
|
}
|
|
287
|
-
if err := ensureControlAtBase(opts.Root, base
|
|
288
|
-
warnf("control
|
|
348
|
+
if err := ensureControlAtBase(opts.Root, base); err != nil {
|
|
349
|
+
warnf("control diverged from base during integrate: %v", err)
|
|
289
350
|
}
|
|
290
351
|
return "", lease, reason
|
|
291
352
|
}
|
|
@@ -318,29 +379,16 @@ func Integrate(opts IntegrateOpts) (tip string, lease *Lease, err error) {
|
|
|
318
379
|
}
|
|
319
380
|
}
|
|
320
381
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return fail(err)
|
|
324
|
-
}
|
|
325
|
-
canFF, err := isAncestor(opts.Root, stageHead, tc)
|
|
326
|
-
if err != nil {
|
|
327
|
-
return fail(err)
|
|
328
|
-
}
|
|
329
|
-
if canFF {
|
|
330
|
-
if err := mergeFFOnly(stageWT, tc); err != nil {
|
|
331
|
-
return fail(fmt.Errorf("ff-only integrate failed for slice %s: %w", sl.ID, err))
|
|
332
|
-
}
|
|
333
|
-
} else {
|
|
334
|
-
if err := cherryPickRange(stageWT, base, tc); err != nil {
|
|
335
|
-
cherryPickAbort(stageWT)
|
|
336
|
-
return fail(fmt.Errorf("cherry-pick replay failed for slice %s: %w", sl.ID, err))
|
|
337
|
-
}
|
|
382
|
+
if err := cherryPickNoCommit(stageWT, base, tc); err != nil {
|
|
383
|
+
cherryPickAbort(stageWT)
|
|
384
|
+
return fail(fmt.Errorf("squash apply failed for slice %s: %w", sl.ID, err))
|
|
338
385
|
}
|
|
339
386
|
}
|
|
340
387
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
388
|
+
msg := fmt.Sprintf("WIP(%s): parallel batch %s (%s)\n\n[devrites-context]\nslices: %s\nbase: %s",
|
|
389
|
+
opts.Slug, lease.BatchID, strings.Join(ids, ", "), strings.Join(ids, ", "), base)
|
|
390
|
+
if tip, err = stagePathsAndCommit(stageWT, msg, union); err != nil {
|
|
391
|
+
return fail(fmt.Errorf("squash commit: %w", err))
|
|
344
392
|
}
|
|
345
393
|
if err := removeWorktree(opts.Root, stageWT); err != nil {
|
|
346
394
|
if _, statErr := os.Stat(stageWT); statErr == nil {
|
|
@@ -359,14 +407,16 @@ func Integrate(opts IntegrateOpts) (tip string, lease *Lease, err error) {
|
|
|
359
407
|
if err := mergeFFOnly(opts.Root, tip); err != nil {
|
|
360
408
|
return fail(fmt.Errorf("control fast-forward to integrate tip failed: %w", err))
|
|
361
409
|
}
|
|
362
|
-
|
|
410
|
+
lease.Status = StatusComplete
|
|
411
|
+
if err := WriteLease(leasePath, lease); err != nil {
|
|
412
|
+
return "", nil, err
|
|
413
|
+
}
|
|
414
|
+
} else if err := ensureControlAtBase(opts.Root, base); err != nil {
|
|
363
415
|
return fail(err)
|
|
364
416
|
}
|
|
365
|
-
|
|
366
|
-
lease
|
|
367
|
-
|
|
368
|
-
return "", nil, err
|
|
369
|
-
}
|
|
417
|
+
// Without --apply-to-control the lease stays running: the squash commit
|
|
418
|
+
// lives only on the integrate branch, and a running lease keeps a
|
|
419
|
+
// non-forced cleanup from discarding the refs that hold it.
|
|
370
420
|
return tip, lease, nil
|
|
371
421
|
}
|
|
372
422
|
|
|
@@ -382,24 +432,105 @@ func pathListsEqual(a, b []string) bool {
|
|
|
382
432
|
return true
|
|
383
433
|
}
|
|
384
434
|
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
435
|
+
// Salvage records one worker branch kept by a non-complete cleanup: the
|
|
436
|
+
// rejected work stays reachable for mining instead of being discarded.
|
|
437
|
+
// Worktree is set when salvage itself failed and the worktree was left on
|
|
438
|
+
// disk as the only remaining copy.
|
|
439
|
+
type Salvage struct {
|
|
440
|
+
SliceID string `json:"slice_id"`
|
|
441
|
+
Branch string `json:"branch"`
|
|
442
|
+
Commit string `json:"commit"`
|
|
443
|
+
Worktree string `json:"worktree,omitempty"`
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ownedWorktreePath reports whether the lease's worktree path is exactly the
|
|
447
|
+
// deterministic <scratch>/<batch>/<slice> location Create wrote. Anything else
|
|
448
|
+
// is not ours: cleanup must neither salvage into it nor delete it.
|
|
449
|
+
func ownedWorktreePath(repoRoot, batchID string, sl LeaseSlice) bool {
|
|
450
|
+
got, err1 := filepath.Abs(sl.WorktreePath)
|
|
451
|
+
want, err2 := filepath.Abs(WorkerWorktreePath(repoRoot, batchID, sl.ID))
|
|
452
|
+
return err1 == nil && err2 == nil && got == want
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// salvageSlice commits any uncommitted allowlisted changes in a worker
|
|
456
|
+
// worktree onto its slice branch so forced cleanup loses no wright work.
|
|
457
|
+
func salvageSlice(sl LeaseSlice, batchID string) (string, error) {
|
|
458
|
+
if sl.WorktreePath == "" || len(sl.Paths) == 0 {
|
|
459
|
+
return "", nil
|
|
460
|
+
}
|
|
461
|
+
if _, err := os.Stat(sl.WorktreePath); err != nil {
|
|
462
|
+
// Worktree already gone; committed work stays reachable on the branch.
|
|
463
|
+
return "", nil
|
|
464
|
+
}
|
|
465
|
+
dirty, err := porcelainDirtyPaths(sl.WorktreePath, sl.Paths)
|
|
388
466
|
if err != nil {
|
|
467
|
+
return "", err
|
|
468
|
+
}
|
|
469
|
+
if !dirty {
|
|
470
|
+
return "", nil
|
|
471
|
+
}
|
|
472
|
+
return stagePathsAndCommit(sl.WorktreePath,
|
|
473
|
+
fmt.Sprintf("devrites: salvage %s WIP (%s)", sl.ID, batchID), sl.Paths)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Cleanup removes worker worktrees and clears the lease. A complete batch
|
|
477
|
+
// deletes the merged slice branches; a forced non-complete cleanup first
|
|
478
|
+
// salvages each sibling — uncommitted allowlisted changes become a commit on
|
|
479
|
+
// the slice branch — and keeps every branch whose tip moved past base as the
|
|
480
|
+
// durable rejected-work ref.
|
|
481
|
+
func Cleanup(repoRoot, slug string, force bool) (salvaged []Salvage, err error) {
|
|
482
|
+
err = withLeaseLock(repoRoot, slug, func() error {
|
|
483
|
+
salvaged, err = cleanupLocked(repoRoot, slug, force)
|
|
389
484
|
return err
|
|
485
|
+
})
|
|
486
|
+
return salvaged, err
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
func cleanupLocked(repoRoot, slug string, force bool) ([]Salvage, error) {
|
|
490
|
+
leasePath, err := LeasePath(repoRoot, slug)
|
|
491
|
+
if err != nil {
|
|
492
|
+
return nil, err
|
|
390
493
|
}
|
|
391
494
|
if _, err := os.Stat(leasePath); os.IsNotExist(err) {
|
|
392
|
-
return nil
|
|
495
|
+
return nil, nil
|
|
393
496
|
}
|
|
394
497
|
lease, err := ReadLease(leasePath)
|
|
395
498
|
if err != nil {
|
|
396
|
-
return err
|
|
499
|
+
return nil, err
|
|
397
500
|
}
|
|
398
501
|
if lease.Status != StatusComplete && !force {
|
|
399
|
-
return fmt.Errorf("cleanup refuses status=%s without --force (
|
|
502
|
+
return nil, fmt.Errorf("cleanup refuses status=%s without --force (non-complete cleanup salvages work onto slice branches)", lease.Status)
|
|
400
503
|
}
|
|
504
|
+
complete := lease.Status == StatusComplete
|
|
505
|
+
base := ""
|
|
506
|
+
if !complete {
|
|
507
|
+
if b, err := revParse(repoRoot, lease.BaseSHA); err != nil {
|
|
508
|
+
warnf("base sha %s: %v", lease.BaseSHA, err)
|
|
509
|
+
} else {
|
|
510
|
+
base = b
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
var salvaged []Salvage
|
|
514
|
+
keptWorktrees := map[string]bool{}
|
|
401
515
|
for _, sl := range lease.Slices {
|
|
402
|
-
|
|
516
|
+
salvageFailed := false
|
|
517
|
+
// Only the deterministic scratch layout is ours to touch. A forged or
|
|
518
|
+
// stale lease worktree_path is never salvaged into (git ops would run
|
|
519
|
+
// in an arbitrary repo) and never removed (RemoveAll would delete an
|
|
520
|
+
// arbitrary directory).
|
|
521
|
+
owned := sl.WorktreePath != "" && ownedWorktreePath(repoRoot, lease.BatchID, sl)
|
|
522
|
+
if sl.WorktreePath != "" && !owned {
|
|
523
|
+
warnf("slice %s worktree_path %q is outside %s; leaving it untouched",
|
|
524
|
+
sl.ID, sl.WorktreePath, ScratchRoot(repoRoot))
|
|
525
|
+
}
|
|
526
|
+
if !complete && owned {
|
|
527
|
+
if _, err := salvage(sl, lease.BatchID); err != nil {
|
|
528
|
+
warnf("salvage %s: %v", sl.ID, err)
|
|
529
|
+
salvageFailed = true
|
|
530
|
+
keptWorktrees[sl.ID] = true
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if owned && !salvageFailed {
|
|
403
534
|
if err := removeWorktree(repoRoot, sl.WorktreePath); err != nil {
|
|
404
535
|
warnf("worktree cleanup %s: %v", sl.WorktreePath, err)
|
|
405
536
|
}
|
|
@@ -407,23 +538,76 @@ func Cleanup(repoRoot, slug string, force bool) error {
|
|
|
407
538
|
warnf("worktree dir cleanup %s: %v", sl.WorktreePath, err)
|
|
408
539
|
}
|
|
409
540
|
}
|
|
541
|
+
if sl.Branch != "" && !ownedBranchName(slug, lease.BatchID, sl.Branch) {
|
|
542
|
+
warnf("slice %s branch %q is outside devrites/parallel/%s/%s/; leaving it untouched",
|
|
543
|
+
sl.ID, sl.Branch, slug, lease.BatchID)
|
|
544
|
+
if salvageFailed {
|
|
545
|
+
salvaged = append(salvaged, Salvage{SliceID: sl.ID, Worktree: sl.WorktreePath})
|
|
546
|
+
}
|
|
547
|
+
continue
|
|
548
|
+
}
|
|
410
549
|
if sl.Branch != "" {
|
|
411
|
-
|
|
550
|
+
keep := salvageFailed
|
|
551
|
+
tip := ""
|
|
552
|
+
if !complete {
|
|
553
|
+
if t, err := revParse(repoRoot, sl.Branch); err != nil {
|
|
554
|
+
warnf("branch tip %s: %v", sl.Branch, err)
|
|
555
|
+
keep = true // fail toward preservation
|
|
556
|
+
} else {
|
|
557
|
+
tip = t
|
|
558
|
+
keep = keep || base == "" || tip != base
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if keep {
|
|
562
|
+
s := Salvage{SliceID: sl.ID, Branch: sl.Branch, Commit: tip}
|
|
563
|
+
if salvageFailed {
|
|
564
|
+
s.Worktree = sl.WorktreePath
|
|
565
|
+
}
|
|
566
|
+
salvaged = append(salvaged, s)
|
|
567
|
+
} else if err := deleteBranch(repoRoot, sl.Branch); err != nil {
|
|
412
568
|
warnf("branch cleanup %s: %v", sl.Branch, err)
|
|
413
569
|
}
|
|
570
|
+
} else if salvageFailed {
|
|
571
|
+
salvaged = append(salvaged, Salvage{SliceID: sl.ID, Worktree: sl.WorktreePath})
|
|
414
572
|
}
|
|
415
573
|
}
|
|
416
574
|
ibranch := IntegrateBranchName(slug, lease.BatchID)
|
|
417
|
-
if err :=
|
|
418
|
-
|
|
575
|
+
if itip, err := revParse(repoRoot, ibranch); err == nil {
|
|
576
|
+
keep := true
|
|
577
|
+
if head, herr := headSHA(repoRoot); herr == nil {
|
|
578
|
+
if anc, aerr := isAncestor(repoRoot, itip, head); aerr == nil && anc {
|
|
579
|
+
keep = false
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if keep {
|
|
583
|
+
salvaged = append(salvaged, Salvage{SliceID: "integrate", Branch: ibranch, Commit: itip})
|
|
584
|
+
} else if err := deleteBranch(repoRoot, ibranch); err != nil {
|
|
585
|
+
warnf("integrate branch cleanup %s: %v", ibranch, err)
|
|
586
|
+
}
|
|
419
587
|
}
|
|
420
|
-
|
|
421
|
-
|
|
588
|
+
batchDir := filepath.Join(ScratchRoot(repoRoot), lease.BatchID)
|
|
589
|
+
if len(keptWorktrees) == 0 {
|
|
590
|
+
if err := os.RemoveAll(batchDir); err != nil {
|
|
591
|
+
warnf("scratch cleanup: %v", err)
|
|
592
|
+
}
|
|
593
|
+
} else if entries, err := os.ReadDir(batchDir); err == nil {
|
|
594
|
+
// A failed salvage keeps its worktree as the only copy — remove the
|
|
595
|
+
// batch dir's other entries but never a kept slice dir.
|
|
596
|
+
for _, e := range entries {
|
|
597
|
+
if keptWorktrees[e.Name()] {
|
|
598
|
+
continue
|
|
599
|
+
}
|
|
600
|
+
if err := os.RemoveAll(filepath.Join(batchDir, e.Name())); err != nil {
|
|
601
|
+
warnf("scratch entry cleanup %s: %v", e.Name(), err)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
warnf("scratch cleanup %s: %v", batchDir, err)
|
|
422
606
|
}
|
|
423
607
|
if _, err := git(repoRoot, "worktree", "prune"); err != nil {
|
|
424
608
|
warnf("worktree prune: %v", err)
|
|
425
609
|
}
|
|
426
|
-
return ClearLease(leasePath)
|
|
610
|
+
return salvaged, ClearLease(leasePath)
|
|
427
611
|
}
|
|
428
612
|
|
|
429
613
|
// StatusReport returns a human-readable lease status.
|