Skip to content

Commit a342dfc

Browse files
Merge pull request #5 from git-fire/feat/gmt-watch-rewrite-scenarios
git: add bounded rewrite scenario orchestration
2 parents 377c314 + 4e4e936 commit a342dfc

3 files changed

Lines changed: 620 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,36 @@ go vet ./...
2222
go test -race -count=1 ./...
2323
```
2424

25+
## Rewrite Orchestration (git-fcuk flow)
26+
27+
The `git` package now includes bounded multi-pass rewrite orchestration via
28+
`RunRewriteScenario`, designed for flows that must:
29+
30+
1. Detect whether intervention is needed.
31+
2. Intervene (run one rewrite pass).
32+
3. Verify the repo is clean.
33+
4. Rerun from detect until clean or attempts are exhausted.
34+
35+
Use it for safety-first rewrite loops where you need deterministic stop
36+
conditions and pass-by-pass telemetry (`RewriteScenarioResult.Passes`).
37+
38+
### Validate locally
39+
40+
```bash
41+
# Targeted rewrite orchestration tests
42+
go test -count=1 ./git -run RunRewriteScenario
43+
44+
# Full module sanity
45+
go test -count=1 ./...
46+
```
47+
48+
Expected outcome:
49+
50+
- Targeted run: `ok github.com/git-fire/git-harness/git ...`
51+
- Full run: all packages return `ok` with no failing tests
52+
- If a rewrite loop never reaches clean within `MaxAttempts`, callers receive
53+
`ErrRewriteAttemptsExceeded`
54+
2555
## Stability
2656

2757
`v0.x` releases may change APIs; pin a minor or patch version in consumers.

‎git/rewrite_scenario.go‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package git
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
)
8+
9+
// ErrRewriteAttemptsExceeded indicates a rewrite run remained dirty after all
10+
// configured attempts were used.
11+
var ErrRewriteAttemptsExceeded = errors.New("rewrite scenario exceeded maximum attempts")
12+
13+
// RewriteScenarioOptions configures detect -> intervene -> verify orchestration.
14+
//
15+
// The flow is:
16+
// 1. Detect whether rewrite intervention is required.
17+
// 2. Intervene when required.
18+
// 3. Verify whether the repo is now clean.
19+
// 4. Repeat until clean or attempts are exhausted.
20+
type RewriteScenarioOptions struct {
21+
// MaxAttempts bounds total detect passes. Must be >= 1.
22+
MaxAttempts int
23+
24+
// Detect reports whether intervention is needed for the attempt.
25+
// Return true when the repository still needs rewrite intervention.
26+
Detect func(ctx context.Context, attempt int) (needsIntervention bool, err error)
27+
28+
// Intervene performs one rewrite intervention pass for the attempt.
29+
Intervene func(ctx context.Context, attempt int) error
30+
31+
// Verify checks whether the repository is clean after intervention.
32+
// Return true when no further rewrite intervention is needed.
33+
Verify func(ctx context.Context, attempt int) (clean bool, err error)
34+
}
35+
36+
// RewriteAttempt captures one detect pass in the orchestration loop.
37+
type RewriteAttempt struct {
38+
Attempt int // 1-based pass number
39+
NeedsIntervention bool // Detect outcome
40+
Intervened bool // Whether Intervene ran
41+
CleanAfterVerify bool // Verify outcome when Intervene ran
42+
}
43+
44+
// RewriteScenarioResult captures pass-by-pass outcomes.
45+
type RewriteScenarioResult struct {
46+
Clean bool
47+
Attempts int
48+
Passes []RewriteAttempt
49+
}
50+
51+
// RunRewriteScenario executes bounded detect -> intervene -> verify passes until
52+
// the repository is clean or attempts are exhausted.
53+
func RunRewriteScenario(ctx context.Context, opts RewriteScenarioOptions) (RewriteScenarioResult, error) {
54+
result := RewriteScenarioResult{}
55+
56+
if ctx == nil {
57+
ctx = context.Background()
58+
}
59+
if opts.MaxAttempts < 1 {
60+
return result, fmt.Errorf("max attempts must be >= 1")
61+
}
62+
if opts.Detect == nil {
63+
return result, fmt.Errorf("detect callback is required")
64+
}
65+
if opts.Intervene == nil {
66+
return result, fmt.Errorf("intervene callback is required")
67+
}
68+
if opts.Verify == nil {
69+
return result, fmt.Errorf("verify callback is required")
70+
}
71+
72+
for attempt := 1; attempt <= opts.MaxAttempts; attempt++ {
73+
if err := ctx.Err(); err != nil {
74+
result.Attempts = attempt - 1
75+
return result, fmt.Errorf("rewrite scenario cancelled before attempt %d: %w", attempt, err)
76+
}
77+
78+
pass := RewriteAttempt{Attempt: attempt}
79+
80+
needsIntervention, err := opts.Detect(ctx, attempt)
81+
if err != nil {
82+
result.Attempts = attempt
83+
result.Passes = append(result.Passes, pass)
84+
return result, fmt.Errorf("detect failed on attempt %d: %w", attempt, err)
85+
}
86+
pass.NeedsIntervention = needsIntervention
87+
88+
if !needsIntervention {
89+
result.Attempts = attempt
90+
result.Clean = true
91+
result.Passes = append(result.Passes, pass)
92+
return result, nil
93+
}
94+
95+
if err := ctx.Err(); err != nil {
96+
result.Attempts = attempt
97+
result.Passes = append(result.Passes, pass)
98+
return result, fmt.Errorf("rewrite scenario cancelled before intervene on attempt %d: %w", attempt, err)
99+
}
100+
101+
if err := opts.Intervene(ctx, attempt); err != nil {
102+
result.Attempts = attempt
103+
result.Passes = append(result.Passes, pass)
104+
return result, fmt.Errorf("intervene failed on attempt %d: %w", attempt, err)
105+
}
106+
pass.Intervened = true
107+
108+
if err := ctx.Err(); err != nil {
109+
result.Attempts = attempt
110+
result.Passes = append(result.Passes, pass)
111+
return result, fmt.Errorf("rewrite scenario cancelled before verify on attempt %d: %w", attempt, err)
112+
}
113+
114+
clean, err := opts.Verify(ctx, attempt)
115+
if err != nil {
116+
result.Attempts = attempt
117+
result.Passes = append(result.Passes, pass)
118+
return result, fmt.Errorf("verify failed on attempt %d: %w", attempt, err)
119+
}
120+
pass.CleanAfterVerify = clean
121+
result.Passes = append(result.Passes, pass)
122+
123+
if clean {
124+
result.Attempts = attempt
125+
result.Clean = true
126+
return result, nil
127+
}
128+
129+
if err := ctx.Err(); err != nil {
130+
result.Attempts = attempt
131+
return result, fmt.Errorf("rewrite scenario cancelled after verify on attempt %d: %w", attempt, err)
132+
}
133+
}
134+
135+
result.Attempts = opts.MaxAttempts
136+
return result, fmt.Errorf("%w: attempts=%d", ErrRewriteAttemptsExceeded, opts.MaxAttempts)
137+
}

0 commit comments

Comments
 (0)