devrites 4.2.0 → 4.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 +6 -0
- package/README.md +1 -1
- package/docs/cli.md +15 -0
- package/docs/engine/commands.md +20 -2
- package/docs/orchestration.md +6 -3
- package/engine/commands.go +1 -1
- package/engine/internal/lib/cli_observe.go +66 -0
- package/engine/internal/lib/observe_summary.go +79 -0
- package/engine/internal/lib/open_visual.go +254 -0
- package/engine/internal/lib/open_visual_test.go +280 -0
- package/engine/internal/lib/skilltrust.go +196 -0
- package/engine/internal/lib/taskgraph.go +162 -0
- package/engine/internal/lib/taskgraph_test.go +150 -0
- package/engine/internal/lib/visual_outline.go +163 -0
- package/engine/internal/lib/visual_outline_test.go +161 -0
- package/engine/internal/lib/workspace_read.go +29 -0
- package/engine/internal/parallel/cli.go +467 -0
- package/engine/internal/parallel/git.go +181 -0
- package/engine/internal/parallel/lease.go +244 -0
- package/engine/internal/parallel/ops.go +416 -0
- package/engine/internal/parallel/parallel_test.go +242 -0
- package/engine/internal/parallel/pathdisjoint.go +129 -0
- package/engine/internal/parallel/pathdisjoint_test.go +97 -0
- package/engine/internal/state/status.go +41 -16
- package/engine/main.go +76 -3
- package/engine/root_routing_test.go +50 -15
- package/engine/testdata/visual/open-visual-smoke.html +675 -0
- package/engine/testdata/visual/open-visual-smoke.outline.md +54 -0
- package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
- package/pack/.claude/skills/rite-build/SKILL.md +29 -36
- package/pack/.claude/skills/rite-build/reference/afk-discipline.md +19 -25
- package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
- package/pack/.claude/skills/rite-build/reference/output.md +2 -0
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +64 -0
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +6 -5
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -3
- package/pack/.claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
- package/pack/.claude/skills/rite-customize/SKILL.md +3 -2
- package/pack/.claude/skills/rite-define/SKILL.md +10 -1
- package/pack/.claude/skills/rite-doctor/SKILL.md +5 -1
- package/pack/.claude/skills/rite-explain/SKILL.md +23 -7
- package/pack/.claude/skills/rite-explain/reference/intake.md +11 -2
- package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -2
- package/pack/.claude/skills/rite-spec/reference/state-workspace.md +26 -3
- package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
- package/pack/generated/claude/skills/rite-build/SKILL.md +29 -36
- package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +19 -25
- package/pack/generated/claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
- package/pack/generated/claude/skills/rite-build/reference/output.md +2 -0
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +64 -0
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +6 -5
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -3
- package/pack/generated/claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
- package/pack/generated/claude/skills/rite-customize/SKILL.md +3 -2
- package/pack/generated/claude/skills/rite-define/SKILL.md +10 -1
- package/pack/generated/claude/skills/rite-doctor/SKILL.md +5 -1
- package/pack/generated/claude/skills/rite-explain/SKILL.md +23 -7
- package/pack/generated/claude/skills/rite-explain/reference/intake.md +11 -2
- package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -2
- package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +26 -3
- package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
- package/pack/generated/codex/skills/rite-build/SKILL.md +29 -36
- package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +19 -25
- package/pack/generated/codex/skills/rite-build/reference/checkpoint-protocol.md +29 -59
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +8 -3
- package/pack/generated/codex/skills/rite-build/reference/output.md +2 -0
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +64 -0
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +6 -5
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -3
- package/pack/generated/codex/skills/rite-clarify/reference/anti-patterns.md +24 -0
- package/pack/generated/codex/skills/rite-customize/SKILL.md +3 -2
- package/pack/generated/codex/skills/rite-define/SKILL.md +10 -1
- package/pack/generated/codex/skills/rite-doctor/SKILL.md +5 -1
- package/pack/generated/codex/skills/rite-explain/SKILL.md +23 -7
- package/pack/generated/codex/skills/rite-explain/reference/intake.md +11 -2
- package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -2
- package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +26 -3
- package/package.json +1 -1
- package/scripts/install-lib.sh +1 -1
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
package parallel
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"os"
|
|
8
|
+
"strings"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Exit codes match the engine CLI conventions.
|
|
12
|
+
const (
|
|
13
|
+
ExitOK = 0
|
|
14
|
+
ExitUsage = 2
|
|
15
|
+
ExitBlocked = 3
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// Run is the engine entrypoint for `parallel …` and `check path-disjoint`.
|
|
19
|
+
// command is either "parallel" or "path-disjoint".
|
|
20
|
+
func Run(command string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
21
|
+
switch command {
|
|
22
|
+
case "path-disjoint":
|
|
23
|
+
return runPathDisjoint(args, stdin, stdout, stderr)
|
|
24
|
+
case "parallel":
|
|
25
|
+
return runParallel(args, stdin, stdout, stderr)
|
|
26
|
+
default:
|
|
27
|
+
fmt.Fprintf(stderr, "parallel: unknown command %q\n", command)
|
|
28
|
+
return ExitUsage
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func runPathDisjoint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
33
|
+
root := ""
|
|
34
|
+
jsonPath := "-"
|
|
35
|
+
for i := 0; i < len(args); i++ {
|
|
36
|
+
switch args[i] {
|
|
37
|
+
case "--root":
|
|
38
|
+
if i+1 >= len(args) {
|
|
39
|
+
fmt.Fprintln(stderr, "usage: check path-disjoint [--root <dir>] [<json-file>|-]")
|
|
40
|
+
return ExitUsage
|
|
41
|
+
}
|
|
42
|
+
i++
|
|
43
|
+
root = args[i]
|
|
44
|
+
case "-h", "--help":
|
|
45
|
+
fmt.Fprintln(stdout, "usage: check path-disjoint [--root <dir>] [<json-file>|-]")
|
|
46
|
+
return ExitOK
|
|
47
|
+
default:
|
|
48
|
+
if args[i] != "-" && strings.HasPrefix(args[i], "-") {
|
|
49
|
+
fmt.Fprintf(stderr, "path-disjoint: unknown flag %s\n", args[i])
|
|
50
|
+
return ExitUsage
|
|
51
|
+
}
|
|
52
|
+
jsonPath = args[i]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
data, err := readJSONInput(jsonPath, stdin)
|
|
56
|
+
if err != nil {
|
|
57
|
+
fmt.Fprintf(stderr, "path-disjoint: %v\n", err)
|
|
58
|
+
return ExitBlocked
|
|
59
|
+
}
|
|
60
|
+
slices, err := ParseSlicesJSON(data)
|
|
61
|
+
if err != nil {
|
|
62
|
+
fmt.Fprintf(stderr, "path-disjoint: %v\n", err)
|
|
63
|
+
return ExitBlocked
|
|
64
|
+
}
|
|
65
|
+
ids, err := CheckPathDisjoint(slices, root)
|
|
66
|
+
if err != nil {
|
|
67
|
+
fmt.Fprintf(stderr, "path-disjoint: %v\n", err)
|
|
68
|
+
return ExitBlocked
|
|
69
|
+
}
|
|
70
|
+
fmt.Fprintf(stdout, "path-disjoint: ok (%d slices)\n", len(ids))
|
|
71
|
+
return ExitOK
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func runParallel(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
75
|
+
if len(args) == 0 {
|
|
76
|
+
fmt.Fprintln(stderr, parallelUsage())
|
|
77
|
+
return ExitUsage
|
|
78
|
+
}
|
|
79
|
+
sub, rest := args[0], args[1:]
|
|
80
|
+
switch sub {
|
|
81
|
+
case "create":
|
|
82
|
+
return cmdCreate(rest, stdin, stdout, stderr)
|
|
83
|
+
case "record-green":
|
|
84
|
+
return cmdRecordGreen(rest, stdout, stderr)
|
|
85
|
+
case "abort":
|
|
86
|
+
return cmdAbort(rest, stdout, stderr)
|
|
87
|
+
case "integrate":
|
|
88
|
+
return cmdIntegrate(rest, stdout, stderr)
|
|
89
|
+
case "cleanup":
|
|
90
|
+
return cmdCleanup(rest, stdout, stderr)
|
|
91
|
+
case "status":
|
|
92
|
+
return cmdStatus(rest, stdout, stderr)
|
|
93
|
+
case "lease-write", "write-lease":
|
|
94
|
+
return cmdLeaseWrite(rest, stdin, stdout, stderr)
|
|
95
|
+
case "lease-read", "read-lease":
|
|
96
|
+
return cmdLeaseRead(rest, stdout, stderr)
|
|
97
|
+
case "lease-clear", "clear-lease":
|
|
98
|
+
return cmdLeaseClear(rest, stdout, stderr)
|
|
99
|
+
case "check-disjoint", "path-disjoint":
|
|
100
|
+
return runPathDisjoint(rest, stdin, stdout, stderr)
|
|
101
|
+
case "-h", "--help", "help":
|
|
102
|
+
fmt.Fprintln(stdout, parallelUsage())
|
|
103
|
+
return ExitOK
|
|
104
|
+
default:
|
|
105
|
+
fmt.Fprintf(stderr, "parallel: unknown subcommand %q\n\n%s\n", sub, parallelUsage())
|
|
106
|
+
return ExitUsage
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func parallelUsage() string {
|
|
111
|
+
return strings.TrimSpace(`usage: parallel <subcommand> [options]
|
|
112
|
+
|
|
113
|
+
Subcommands:
|
|
114
|
+
create --root --slug --batch --base --json
|
|
115
|
+
record-green --root --slug --slice --commit
|
|
116
|
+
abort --root --slug [--force]
|
|
117
|
+
integrate --root --slug [--apply-to-control] [--force]
|
|
118
|
+
cleanup --root --slug [--force]
|
|
119
|
+
status --root --slug
|
|
120
|
+
lease-write --root --slug --json
|
|
121
|
+
lease-read --root --slug [--field name]
|
|
122
|
+
lease-clear --root --slug
|
|
123
|
+
check-disjoint [--root] [<json-file>|-]
|
|
124
|
+
|
|
125
|
+
Exit codes: 0 ok, 2 usage, 3 blocked`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type flagSet struct {
|
|
129
|
+
Root, Slug, Batch, Base, Slice, Commit, JSON, Field, Session string
|
|
130
|
+
ApplyToControl, Force bool
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func parseFlags(args []string, stderr io.Writer) (flagSet, []string, int) {
|
|
134
|
+
var f flagSet
|
|
135
|
+
i := 0
|
|
136
|
+
for i < len(args) {
|
|
137
|
+
a := args[i]
|
|
138
|
+
need := func(name string) (string, bool) {
|
|
139
|
+
if i+1 >= len(args) {
|
|
140
|
+
fmt.Fprintf(stderr, "parallel: %s requires a value\n", name)
|
|
141
|
+
return "", false
|
|
142
|
+
}
|
|
143
|
+
i++
|
|
144
|
+
return args[i], true
|
|
145
|
+
}
|
|
146
|
+
switch a {
|
|
147
|
+
case "--root":
|
|
148
|
+
v, ok := need(a)
|
|
149
|
+
if !ok {
|
|
150
|
+
return f, nil, ExitUsage
|
|
151
|
+
}
|
|
152
|
+
f.Root = v
|
|
153
|
+
case "--slug":
|
|
154
|
+
v, ok := need(a)
|
|
155
|
+
if !ok {
|
|
156
|
+
return f, nil, ExitUsage
|
|
157
|
+
}
|
|
158
|
+
f.Slug = v
|
|
159
|
+
case "--batch":
|
|
160
|
+
v, ok := need(a)
|
|
161
|
+
if !ok {
|
|
162
|
+
return f, nil, ExitUsage
|
|
163
|
+
}
|
|
164
|
+
f.Batch = v
|
|
165
|
+
case "--base":
|
|
166
|
+
v, ok := need(a)
|
|
167
|
+
if !ok {
|
|
168
|
+
return f, nil, ExitUsage
|
|
169
|
+
}
|
|
170
|
+
f.Base = v
|
|
171
|
+
case "--slice":
|
|
172
|
+
v, ok := need(a)
|
|
173
|
+
if !ok {
|
|
174
|
+
return f, nil, ExitUsage
|
|
175
|
+
}
|
|
176
|
+
f.Slice = v
|
|
177
|
+
case "--commit":
|
|
178
|
+
v, ok := need(a)
|
|
179
|
+
if !ok {
|
|
180
|
+
return f, nil, ExitUsage
|
|
181
|
+
}
|
|
182
|
+
f.Commit = v
|
|
183
|
+
case "--json":
|
|
184
|
+
v, ok := need(a)
|
|
185
|
+
if !ok {
|
|
186
|
+
return f, nil, ExitUsage
|
|
187
|
+
}
|
|
188
|
+
f.JSON = v
|
|
189
|
+
case "--field":
|
|
190
|
+
v, ok := need(a)
|
|
191
|
+
if !ok {
|
|
192
|
+
return f, nil, ExitUsage
|
|
193
|
+
}
|
|
194
|
+
f.Field = v
|
|
195
|
+
case "--session":
|
|
196
|
+
v, ok := need(a)
|
|
197
|
+
if !ok {
|
|
198
|
+
return f, nil, ExitUsage
|
|
199
|
+
}
|
|
200
|
+
f.Session = v
|
|
201
|
+
case "--apply-to-control":
|
|
202
|
+
f.ApplyToControl = true
|
|
203
|
+
case "--force":
|
|
204
|
+
f.Force = true
|
|
205
|
+
default:
|
|
206
|
+
return f, args[i:], ExitOK
|
|
207
|
+
}
|
|
208
|
+
i++
|
|
209
|
+
}
|
|
210
|
+
return f, nil, ExitOK
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func requireRootSlug(f flagSet, stderr io.Writer) int {
|
|
214
|
+
if f.Root == "" || f.Slug == "" {
|
|
215
|
+
fmt.Fprintln(stderr, "parallel: --root and --slug are required")
|
|
216
|
+
return ExitUsage
|
|
217
|
+
}
|
|
218
|
+
return ExitOK
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func readJSONInput(path string, stdin io.Reader) ([]byte, error) {
|
|
222
|
+
if path == "" || path == "-" {
|
|
223
|
+
return io.ReadAll(stdin)
|
|
224
|
+
}
|
|
225
|
+
if strings.Contains(path, "..") {
|
|
226
|
+
return nil, fmt.Errorf("json path must not contain '..'")
|
|
227
|
+
}
|
|
228
|
+
return os.ReadFile(path)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func cmdCreate(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
232
|
+
f, _, code := parseFlags(args, stderr)
|
|
233
|
+
if code != ExitOK {
|
|
234
|
+
return code
|
|
235
|
+
}
|
|
236
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
237
|
+
return code
|
|
238
|
+
}
|
|
239
|
+
if f.Batch == "" || f.Base == "" || f.JSON == "" {
|
|
240
|
+
fmt.Fprintln(stderr, "usage: parallel create --root --slug --batch --base --json")
|
|
241
|
+
return ExitUsage
|
|
242
|
+
}
|
|
243
|
+
data, err := readJSONInput(f.JSON, stdin)
|
|
244
|
+
if err != nil {
|
|
245
|
+
fmt.Fprintf(stderr, "parallel create: %v\n", err)
|
|
246
|
+
return ExitBlocked
|
|
247
|
+
}
|
|
248
|
+
slices, err := ParseSlicesJSON(data)
|
|
249
|
+
if err != nil {
|
|
250
|
+
fmt.Fprintf(stderr, "parallel create: %v\n", err)
|
|
251
|
+
return ExitBlocked
|
|
252
|
+
}
|
|
253
|
+
lease, err := Create(CreateOpts{
|
|
254
|
+
Root: f.Root,
|
|
255
|
+
Slug: f.Slug,
|
|
256
|
+
BatchID: f.Batch,
|
|
257
|
+
BaseSHA: f.Base,
|
|
258
|
+
Session: f.Session,
|
|
259
|
+
Slices: slices,
|
|
260
|
+
})
|
|
261
|
+
if err != nil {
|
|
262
|
+
fmt.Fprintf(stderr, "parallel create: %v\n", err)
|
|
263
|
+
return ExitBlocked
|
|
264
|
+
}
|
|
265
|
+
fmt.Fprintf(stdout, "created: %d worktrees; batch=%s status=%s\n", lease.N, lease.BatchID, lease.Status)
|
|
266
|
+
return ExitOK
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func cmdRecordGreen(args []string, stdout, stderr io.Writer) int {
|
|
270
|
+
f, _, code := parseFlags(args, stderr)
|
|
271
|
+
if code != ExitOK {
|
|
272
|
+
return code
|
|
273
|
+
}
|
|
274
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
275
|
+
return code
|
|
276
|
+
}
|
|
277
|
+
if f.Slice == "" || f.Commit == "" {
|
|
278
|
+
fmt.Fprintln(stderr, "usage: parallel record-green --root --slug --slice --commit")
|
|
279
|
+
return ExitUsage
|
|
280
|
+
}
|
|
281
|
+
lease, err := RecordGreen(f.Root, f.Slug, f.Slice, f.Commit)
|
|
282
|
+
if err != nil {
|
|
283
|
+
fmt.Fprintf(stderr, "parallel record-green: %v\n", err)
|
|
284
|
+
return ExitBlocked
|
|
285
|
+
}
|
|
286
|
+
fmt.Fprintf(stdout, "record-green: slice=%s status=%s\n", f.Slice, lease.Status)
|
|
287
|
+
return ExitOK
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
func cmdAbort(args []string, stdout, stderr io.Writer) int {
|
|
291
|
+
f, _, code := parseFlags(args, stderr)
|
|
292
|
+
if code != ExitOK {
|
|
293
|
+
return code
|
|
294
|
+
}
|
|
295
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
296
|
+
return code
|
|
297
|
+
}
|
|
298
|
+
lease, err := Abort(f.Root, f.Slug, f.Force)
|
|
299
|
+
if err != nil {
|
|
300
|
+
fmt.Fprintf(stderr, "parallel abort: %v\n", err)
|
|
301
|
+
return ExitBlocked
|
|
302
|
+
}
|
|
303
|
+
fmt.Fprintf(stdout, "aborted: status=%s base=%s\n", lease.Status, lease.BaseSHA)
|
|
304
|
+
return ExitOK
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
func cmdIntegrate(args []string, stdout, stderr io.Writer) int {
|
|
308
|
+
f, _, code := parseFlags(args, stderr)
|
|
309
|
+
if code != ExitOK {
|
|
310
|
+
return code
|
|
311
|
+
}
|
|
312
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
313
|
+
return code
|
|
314
|
+
}
|
|
315
|
+
tip, lease, err := Integrate(IntegrateOpts{
|
|
316
|
+
Root: f.Root,
|
|
317
|
+
Slug: f.Slug,
|
|
318
|
+
ApplyToControl: f.ApplyToControl,
|
|
319
|
+
Force: f.Force,
|
|
320
|
+
})
|
|
321
|
+
if err != nil {
|
|
322
|
+
fmt.Fprintf(stderr, "parallel integrate: %v\n", err)
|
|
323
|
+
return ExitBlocked
|
|
324
|
+
}
|
|
325
|
+
fmt.Fprintf(stdout, "integrate-ok: tip=%s status=%s apply_to_control=%v\n", tip, lease.Status, f.ApplyToControl)
|
|
326
|
+
return ExitOK
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
func cmdCleanup(args []string, stdout, stderr io.Writer) int {
|
|
330
|
+
f, _, code := parseFlags(args, stderr)
|
|
331
|
+
if code != ExitOK {
|
|
332
|
+
return code
|
|
333
|
+
}
|
|
334
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
335
|
+
return code
|
|
336
|
+
}
|
|
337
|
+
if err := Cleanup(f.Root, f.Slug, f.Force); err != nil {
|
|
338
|
+
fmt.Fprintf(stderr, "parallel cleanup: %v\n", err)
|
|
339
|
+
return ExitBlocked
|
|
340
|
+
}
|
|
341
|
+
fmt.Fprintln(stdout, "cleanup: done")
|
|
342
|
+
return ExitOK
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func cmdStatus(args []string, stdout, stderr io.Writer) int {
|
|
346
|
+
f, _, code := parseFlags(args, stderr)
|
|
347
|
+
if code != ExitOK {
|
|
348
|
+
return code
|
|
349
|
+
}
|
|
350
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
351
|
+
return code
|
|
352
|
+
}
|
|
353
|
+
text, err := StatusReport(f.Root, f.Slug)
|
|
354
|
+
if err != nil {
|
|
355
|
+
fmt.Fprintf(stderr, "parallel status: %v\n", err)
|
|
356
|
+
return ExitBlocked
|
|
357
|
+
}
|
|
358
|
+
fmt.Fprint(stdout, text)
|
|
359
|
+
return ExitOK
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func cmdLeaseWrite(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
363
|
+
f, _, code := parseFlags(args, stderr)
|
|
364
|
+
if code != ExitOK {
|
|
365
|
+
return code
|
|
366
|
+
}
|
|
367
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
368
|
+
return code
|
|
369
|
+
}
|
|
370
|
+
if f.JSON == "" {
|
|
371
|
+
fmt.Fprintln(stderr, "usage: parallel lease-write --root --slug --json")
|
|
372
|
+
return ExitUsage
|
|
373
|
+
}
|
|
374
|
+
data, err := readJSONInput(f.JSON, stdin)
|
|
375
|
+
if err != nil {
|
|
376
|
+
fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
|
|
377
|
+
return ExitBlocked
|
|
378
|
+
}
|
|
379
|
+
var lease Lease
|
|
380
|
+
if err := json.Unmarshal(data, &lease); err != nil {
|
|
381
|
+
fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
|
|
382
|
+
return ExitBlocked
|
|
383
|
+
}
|
|
384
|
+
if lease.CreatedAt == "" {
|
|
385
|
+
lease.CreatedAt = NowUTC()
|
|
386
|
+
}
|
|
387
|
+
path, err := LeasePath(f.Root, f.Slug)
|
|
388
|
+
if err != nil {
|
|
389
|
+
fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
|
|
390
|
+
return ExitUsage
|
|
391
|
+
}
|
|
392
|
+
if err := WriteLease(path, &lease); err != nil {
|
|
393
|
+
fmt.Fprintf(stderr, "parallel lease-write: %v\n", err)
|
|
394
|
+
return ExitBlocked
|
|
395
|
+
}
|
|
396
|
+
fmt.Fprintln(stdout, path)
|
|
397
|
+
return ExitOK
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
func cmdLeaseRead(args []string, stdout, stderr io.Writer) int {
|
|
401
|
+
f, _, code := parseFlags(args, stderr)
|
|
402
|
+
if code != ExitOK {
|
|
403
|
+
return code
|
|
404
|
+
}
|
|
405
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
406
|
+
return code
|
|
407
|
+
}
|
|
408
|
+
path, err := LeasePath(f.Root, f.Slug)
|
|
409
|
+
if err != nil {
|
|
410
|
+
fmt.Fprintf(stderr, "parallel lease-read: %v\n", err)
|
|
411
|
+
return ExitUsage
|
|
412
|
+
}
|
|
413
|
+
lease, err := ReadLease(path)
|
|
414
|
+
if err != nil {
|
|
415
|
+
fmt.Fprintf(stderr, "parallel lease-read: %v\n", err)
|
|
416
|
+
return ExitBlocked
|
|
417
|
+
}
|
|
418
|
+
switch f.Field {
|
|
419
|
+
case "", "yaml", "md":
|
|
420
|
+
text, err := EmitLeaseMarkdown(lease)
|
|
421
|
+
if err != nil {
|
|
422
|
+
fmt.Fprintf(stderr, "parallel lease-read: %v\n", err)
|
|
423
|
+
return ExitBlocked
|
|
424
|
+
}
|
|
425
|
+
fmt.Fprint(stdout, text)
|
|
426
|
+
case "json":
|
|
427
|
+
text, err := LeaseJSON(lease)
|
|
428
|
+
if err != nil {
|
|
429
|
+
fmt.Fprintf(stderr, "parallel lease-read: %v\n", err)
|
|
430
|
+
return ExitBlocked
|
|
431
|
+
}
|
|
432
|
+
fmt.Fprintln(stdout, text)
|
|
433
|
+
case "status":
|
|
434
|
+
fmt.Fprintln(stdout, lease.Status)
|
|
435
|
+
case "base_sha":
|
|
436
|
+
fmt.Fprintln(stdout, lease.BaseSHA)
|
|
437
|
+
case "batch_id":
|
|
438
|
+
fmt.Fprintln(stdout, lease.BatchID)
|
|
439
|
+
case "n":
|
|
440
|
+
fmt.Fprintln(stdout, lease.N)
|
|
441
|
+
default:
|
|
442
|
+
fmt.Fprintf(stderr, "parallel lease-read: unknown field %q\n", f.Field)
|
|
443
|
+
return ExitUsage
|
|
444
|
+
}
|
|
445
|
+
return ExitOK
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
func cmdLeaseClear(args []string, stdout, stderr io.Writer) int {
|
|
449
|
+
f, _, code := parseFlags(args, stderr)
|
|
450
|
+
if code != ExitOK {
|
|
451
|
+
return code
|
|
452
|
+
}
|
|
453
|
+
if code := requireRootSlug(f, stderr); code != ExitOK {
|
|
454
|
+
return code
|
|
455
|
+
}
|
|
456
|
+
path, err := LeasePath(f.Root, f.Slug)
|
|
457
|
+
if err != nil {
|
|
458
|
+
fmt.Fprintf(stderr, "parallel lease-clear: %v\n", err)
|
|
459
|
+
return ExitUsage
|
|
460
|
+
}
|
|
461
|
+
if err := ClearLease(path); err != nil {
|
|
462
|
+
fmt.Fprintf(stderr, "parallel lease-clear: %v\n", err)
|
|
463
|
+
return ExitBlocked
|
|
464
|
+
}
|
|
465
|
+
fmt.Fprintf(stdout, "cleared: %s\n", path)
|
|
466
|
+
return ExitOK
|
|
467
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
package parallel
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"errors"
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
9
|
+
"os/exec"
|
|
10
|
+
"strings"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"github.com/devrites/devrites/internal/gitenv"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const gitTimeout = 60 * time.Second
|
|
17
|
+
|
|
18
|
+
func git(repo string, args ...string) (string, error) {
|
|
19
|
+
ctx, cancel := context.WithTimeout(context.Background(), gitTimeout)
|
|
20
|
+
defer cancel()
|
|
21
|
+
cmd := exec.CommandContext(ctx, "git", append([]string{"-C", repo}, args...)...)
|
|
22
|
+
env := append(gitenv.Sanitize(os.Environ()),
|
|
23
|
+
"GIT_TERMINAL_PROMPT=0",
|
|
24
|
+
"GCM_INTERACTIVE=never",
|
|
25
|
+
"GIT_PAGER=cat",
|
|
26
|
+
"PAGER=cat",
|
|
27
|
+
"LC_ALL=C",
|
|
28
|
+
)
|
|
29
|
+
if gitOpNeedsIdentity(args) {
|
|
30
|
+
env = ensureGitIdentityEnv(env)
|
|
31
|
+
}
|
|
32
|
+
cmd.Env = env
|
|
33
|
+
var stdout, stderr bytes.Buffer
|
|
34
|
+
cmd.Stdout = &stdout
|
|
35
|
+
cmd.Stderr = &stderr
|
|
36
|
+
err := cmd.Run()
|
|
37
|
+
out := strings.TrimSpace(stdout.String())
|
|
38
|
+
if err != nil {
|
|
39
|
+
detail := strings.TrimSpace(stderr.String())
|
|
40
|
+
if detail == "" {
|
|
41
|
+
detail = out
|
|
42
|
+
}
|
|
43
|
+
if ctx.Err() != nil {
|
|
44
|
+
return out, fmt.Errorf("git %s: %w", strings.Join(args, " "), ctx.Err())
|
|
45
|
+
}
|
|
46
|
+
var exitErr *exec.ExitError
|
|
47
|
+
if errors.As(err, &exitErr) {
|
|
48
|
+
return out, fmt.Errorf("git %s: exit %d: %s", strings.Join(args, " "), exitErr.ExitCode(), detail)
|
|
49
|
+
}
|
|
50
|
+
return out, fmt.Errorf("git %s: %v: %s", strings.Join(args, " "), err, detail)
|
|
51
|
+
}
|
|
52
|
+
return out, nil
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func gitOpNeedsIdentity(args []string) bool {
|
|
56
|
+
for _, a := range args {
|
|
57
|
+
switch a {
|
|
58
|
+
case "cherry-pick", "commit", "merge", "rebase", "am":
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ensureGitIdentityEnv fills author/committer when the host has none (common on
|
|
66
|
+
// CI images). Prefer existing env identity when present.
|
|
67
|
+
func ensureGitIdentityEnv(env []string) []string {
|
|
68
|
+
keys := map[string]string{
|
|
69
|
+
"GIT_AUTHOR_NAME": "devrites-engine",
|
|
70
|
+
"GIT_AUTHOR_EMAIL": "engine@devrites.invalid",
|
|
71
|
+
"GIT_COMMITTER_NAME": "devrites-engine",
|
|
72
|
+
"GIT_COMMITTER_EMAIL": "engine@devrites.invalid",
|
|
73
|
+
}
|
|
74
|
+
have := make(map[string]bool, len(env))
|
|
75
|
+
for _, e := range env {
|
|
76
|
+
if i := strings.IndexByte(e, '='); i > 0 {
|
|
77
|
+
have[e[:i]] = true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for k, v := range keys {
|
|
81
|
+
if !have[k] {
|
|
82
|
+
env = append(env, k+"="+v)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return env
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func revParse(repo, rev string) (string, error) {
|
|
89
|
+
return git(repo, "rev-parse", "--verify", rev+"^{commit}")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func isAncestor(repo, ancestor, descendant string) (bool, error) {
|
|
93
|
+
_, err := git(repo, "merge-base", "--is-ancestor", ancestor, descendant)
|
|
94
|
+
if err == nil {
|
|
95
|
+
return true, nil
|
|
96
|
+
}
|
|
97
|
+
if strings.Contains(err.Error(), "exit 1") {
|
|
98
|
+
return false, nil
|
|
99
|
+
}
|
|
100
|
+
return false, err
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func diffNames(repo, a, b string) ([]string, error) {
|
|
104
|
+
out, err := git(repo, "diff", "--name-only", a, b)
|
|
105
|
+
if err != nil {
|
|
106
|
+
return nil, err
|
|
107
|
+
}
|
|
108
|
+
if out == "" {
|
|
109
|
+
return nil, nil
|
|
110
|
+
}
|
|
111
|
+
lines := strings.Split(out, "\n")
|
|
112
|
+
for i := range lines {
|
|
113
|
+
lines[i] = strings.TrimSpace(lines[i])
|
|
114
|
+
}
|
|
115
|
+
return lines, nil
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func headSHA(repo string) (string, error) {
|
|
119
|
+
return revParse(repo, "HEAD")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func resetHard(repo, sha string) error {
|
|
123
|
+
_, err := git(repo, "reset", "--hard", sha)
|
|
124
|
+
return err
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func porcelainDirty(repo string) (bool, error) {
|
|
128
|
+
out, err := git(repo, "status", "--porcelain", "--untracked-files=no")
|
|
129
|
+
if err != nil {
|
|
130
|
+
return false, err
|
|
131
|
+
}
|
|
132
|
+
return out != "", nil
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func branchExists(repo, branch string) (bool, error) {
|
|
136
|
+
_, err := git(repo, "show-ref", "--verify", "--quiet", "refs/heads/"+branch)
|
|
137
|
+
if err == nil {
|
|
138
|
+
return true, nil
|
|
139
|
+
}
|
|
140
|
+
if strings.Contains(err.Error(), "exit 1") {
|
|
141
|
+
return false, nil
|
|
142
|
+
}
|
|
143
|
+
return false, err
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func deleteBranch(repo, branch string) error {
|
|
147
|
+
exists, err := branchExists(repo, branch)
|
|
148
|
+
if err != nil || !exists {
|
|
149
|
+
return err
|
|
150
|
+
}
|
|
151
|
+
_, err = git(repo, "branch", "-D", branch)
|
|
152
|
+
return err
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func worktreeAdd(repo, path, branch, startPoint string) error {
|
|
156
|
+
_, err := git(repo, "worktree", "add", "-b", branch, path, startPoint)
|
|
157
|
+
return err
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func worktreeRemove(repo, path string) error {
|
|
161
|
+
_, err := git(repo, "worktree", "remove", "--force", path)
|
|
162
|
+
if err != nil {
|
|
163
|
+
_ = os.RemoveAll(path)
|
|
164
|
+
_, _ = git(repo, "worktree", "prune")
|
|
165
|
+
}
|
|
166
|
+
return err
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func mergeFFOnly(repo, commit string) error {
|
|
170
|
+
_, err := git(repo, "merge", "--ff-only", commit)
|
|
171
|
+
return err
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func cherryPickRange(repo, fromExclusive, toInclusive string) error {
|
|
175
|
+
_, err := git(repo, "cherry-pick", "-x", fromExclusive+".."+toInclusive)
|
|
176
|
+
return err
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func cherryPickAbort(repo string) {
|
|
180
|
+
_, _ = git(repo, "cherry-pick", "--abort")
|
|
181
|
+
}
|