Skip to content

Commit d0062ea

Browse files
julianknutsenclaude
andcommitted
Add PR mode with branch-based mutations and wl review command
Phase 1: Branch infrastructure — adds Mode field to federation config, mutationContext helper for branch checkout/return/push, branch helpers in dolt.go, and wl config get/set command. All 8 mutation commands now branch in PR mode and no-op in wild-west mode. Phase 2: wl review command — lists wl/* branches, shows diffs between main and a branch with --stat, --json, --md output formats. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 00aac36 commit d0062ea

20 files changed

Lines changed: 1006 additions & 27 deletions

cmd/wl/branch_helpers.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package main
2+
3+
import (
4+
"io"
5+
6+
"github.com/steveyegge/wasteland/internal/commons"
7+
"github.com/steveyegge/wasteland/internal/federation"
8+
)
9+
10+
// mutationContext wraps branch checkout/return/push logic so all mutation
11+
// commands don't duplicate it. In wild-west mode it's a no-op passthrough;
12+
// in PR mode it checks out a per-item branch and returns to main afterward.
13+
type mutationContext struct {
14+
cfg *federation.Config
15+
wantedID string
16+
branch string // computed branch name, empty in wild-west mode
17+
noPush bool
18+
stdout io.Writer
19+
}
20+
21+
// newMutationContext creates a mutation context for the given config and wanted ID.
22+
func newMutationContext(cfg *federation.Config, wantedID string, noPush bool, stdout io.Writer) *mutationContext {
23+
mc := &mutationContext{
24+
cfg: cfg,
25+
wantedID: wantedID,
26+
noPush: noPush,
27+
stdout: stdout,
28+
}
29+
if cfg.ResolveMode() == federation.ModePR {
30+
mc.branch = commons.BranchName(cfg.RigHandle, wantedID)
31+
}
32+
return mc
33+
}
34+
35+
// BranchName returns the branch name, or "" in wild-west mode.
36+
func (m *mutationContext) BranchName() string {
37+
return m.branch
38+
}
39+
40+
// Setup prepares the branch context. In PR mode it checks out the item branch.
41+
// The returned cleanup function must be deferred to return to main.
42+
func (m *mutationContext) Setup() (cleanup func(), err error) {
43+
noop := func() {}
44+
if m.branch == "" {
45+
return noop, nil
46+
}
47+
if err := commons.CheckoutBranch(m.cfg.LocalDir, m.branch); err != nil {
48+
return noop, err
49+
}
50+
return func() {
51+
_ = commons.CheckoutMain(m.cfg.LocalDir)
52+
}, nil
53+
}
54+
55+
// Push pushes changes to the appropriate remote(s).
56+
// In wild-west mode: PushWithSync (upstream + origin).
57+
// In PR mode: PushBranch (origin only).
58+
func (m *mutationContext) Push() {
59+
if m.noPush {
60+
return
61+
}
62+
if m.branch != "" {
63+
_ = commons.PushBranch(m.cfg.LocalDir, m.branch, m.stdout)
64+
} else {
65+
_ = commons.PushWithSync(m.cfg.LocalDir, m.stdout)
66+
}
67+
}

cmd/wl/branch_helpers_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/steveyegge/wasteland/internal/federation"
8+
)
9+
10+
func TestMutationContext_WildWest(t *testing.T) {
11+
cfg := &federation.Config{
12+
Upstream: "org/db",
13+
LocalDir: "/tmp/fake",
14+
RigHandle: "test-rig",
15+
Mode: "", // defaults to wild-west
16+
}
17+
18+
mc := newMutationContext(cfg, "w-abc123", true, &bytes.Buffer{})
19+
20+
if mc.BranchName() != "" {
21+
t.Errorf("BranchName() = %q, want empty in wild-west mode", mc.BranchName())
22+
}
23+
24+
cleanup, err := mc.Setup()
25+
if err != nil {
26+
t.Fatalf("Setup() error = %v", err)
27+
}
28+
// cleanup should be a no-op in wild-west mode.
29+
cleanup()
30+
}
31+
32+
func TestMutationContext_WildWestExplicit(t *testing.T) {
33+
cfg := &federation.Config{
34+
Upstream: "org/db",
35+
LocalDir: "/tmp/fake",
36+
RigHandle: "test-rig",
37+
Mode: federation.ModeWildWest,
38+
}
39+
40+
mc := newMutationContext(cfg, "w-abc123", true, &bytes.Buffer{})
41+
42+
if mc.BranchName() != "" {
43+
t.Errorf("BranchName() = %q, want empty in wild-west mode", mc.BranchName())
44+
}
45+
}
46+
47+
func TestMutationContext_PRMode_BranchName(t *testing.T) {
48+
cfg := &federation.Config{
49+
Upstream: "org/db",
50+
LocalDir: "/tmp/fake",
51+
RigHandle: "test-rig",
52+
Mode: federation.ModePR,
53+
}
54+
55+
mc := newMutationContext(cfg, "w-abc123", true, &bytes.Buffer{})
56+
57+
want := "wl/test-rig/w-abc123"
58+
if mc.BranchName() != want {
59+
t.Errorf("BranchName() = %q, want %q", mc.BranchName(), want)
60+
}
61+
}

cmd/wl/cmd_accept.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ func runAccept(cmd *cobra.Command, stdout, _ io.Writer, wantedID string, quality
7979
}
8080
rigHandle := wlCfg.RigHandle
8181

82+
mc := newMutationContext(wlCfg, wantedID, noPush, stdout)
83+
cleanup, err := mc.Setup()
84+
if err != nil {
85+
return err
86+
}
87+
defer cleanup()
88+
8289
store := commons.NewWLCommons(wlCfg.LocalDir)
8390

8491
stamp, err := acceptCompletion(store, wantedID, rigHandle, quality, reliability, severity, skillTags, message)
@@ -97,11 +104,12 @@ func runAccept(cmd *cobra.Command, stdout, _ io.Writer, wantedID string, quality
97104
fmt.Fprintf(stdout, " Message: %s\n", stamp.Message)
98105
}
99106
fmt.Fprintf(stdout, " Status: completed\n")
100-
101-
if !noPush {
102-
_ = commons.PushWithSync(wlCfg.LocalDir, stdout)
107+
if mc.BranchName() != "" {
108+
fmt.Fprintf(stdout, " Branch: %s\n", mc.BranchName())
103109
}
104110

111+
mc.Push()
112+
105113
return nil
106114
}
107115

cmd/wl/cmd_claim.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ func runClaim(cmd *cobra.Command, stdout, _ io.Writer, wantedID string, noPush b
4444
}
4545
rigHandle := wlCfg.RigHandle
4646

47+
mc := newMutationContext(wlCfg, wantedID, noPush, stdout)
48+
cleanup, err := mc.Setup()
49+
if err != nil {
50+
return err
51+
}
52+
defer cleanup()
53+
4754
store := commons.NewWLCommons(wlCfg.LocalDir)
4855
item, err := claimWanted(store, wantedID, rigHandle)
4956
if err != nil {
@@ -53,11 +60,12 @@ func runClaim(cmd *cobra.Command, stdout, _ io.Writer, wantedID string, noPush b
5360
fmt.Fprintf(stdout, "%s Claimed %s\n", style.Bold.Render("✓"), wantedID)
5461
fmt.Fprintf(stdout, " Claimed by: %s\n", rigHandle)
5562
fmt.Fprintf(stdout, " Title: %s\n", item.Title)
56-
57-
if !noPush {
58-
_ = commons.PushWithSync(wlCfg.LocalDir, stdout)
63+
if mc.BranchName() != "" {
64+
fmt.Fprintf(stdout, " Branch: %s\n", mc.BranchName())
5965
}
6066

67+
mc.Push()
68+
6169
return nil
6270
}
6371

cmd/wl/cmd_config.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/steveyegge/wasteland/internal/federation"
9+
)
10+
11+
func newConfigCmd(stdout, stderr io.Writer) *cobra.Command {
12+
cmd := &cobra.Command{
13+
Use: "config",
14+
Short: "Get or set wasteland configuration",
15+
Long: `View or modify wasteland configuration settings.
16+
17+
Use 'wl config get <key>' to read a setting.
18+
Use 'wl config set <key> <value>' to change a setting.
19+
20+
Supported keys:
21+
mode Workflow mode: wild-west (default) or pr`,
22+
Args: cobra.NoArgs,
23+
RunE: func(cmd *cobra.Command, _ []string) error {
24+
return cmd.Help()
25+
},
26+
}
27+
28+
cmd.AddCommand(
29+
newConfigGetCmd(stdout, stderr),
30+
newConfigSetCmd(stdout, stderr),
31+
)
32+
33+
return cmd
34+
}
35+
36+
func newConfigGetCmd(stdout, stderr io.Writer) *cobra.Command {
37+
return &cobra.Command{
38+
Use: "get <key>",
39+
Short: "Get a configuration value",
40+
Args: cobra.ExactArgs(1),
41+
RunE: func(cmd *cobra.Command, args []string) error {
42+
return runConfigGet(cmd, stdout, stderr, args[0])
43+
},
44+
}
45+
}
46+
47+
func newConfigSetCmd(stdout, stderr io.Writer) *cobra.Command {
48+
return &cobra.Command{
49+
Use: "set <key> <value>",
50+
Short: "Set a configuration value",
51+
Args: cobra.ExactArgs(2),
52+
RunE: func(cmd *cobra.Command, args []string) error {
53+
return runConfigSet(cmd, stdout, stderr, args[0], args[1])
54+
},
55+
}
56+
}
57+
58+
// validConfigKeys lists the keys that can be read/written via wl config.
59+
var validConfigKeys = map[string]bool{
60+
"mode": true,
61+
}
62+
63+
func runConfigGet(cmd *cobra.Command, stdout, _ io.Writer, key string) error {
64+
if !validConfigKeys[key] {
65+
return fmt.Errorf("unknown config key %q (supported: mode)", key)
66+
}
67+
68+
cfg, err := resolveWasteland(cmd)
69+
if err != nil {
70+
return fmt.Errorf("loading wasteland config: %w", err)
71+
}
72+
73+
if key == "mode" {
74+
fmt.Fprintln(stdout, cfg.ResolveMode())
75+
}
76+
return nil
77+
}
78+
79+
func runConfigSet(cmd *cobra.Command, stdout, _ io.Writer, key, value string) error {
80+
if !validConfigKeys[key] {
81+
return fmt.Errorf("unknown config key %q (supported: mode)", key)
82+
}
83+
84+
if key == "mode" {
85+
if err := validateMode(value); err != nil {
86+
return err
87+
}
88+
}
89+
90+
explicit, _ := cmd.Flags().GetString("wasteland")
91+
store := federation.NewConfigStore()
92+
cfg, err := federation.ResolveConfig(store, explicit)
93+
if err != nil {
94+
return fmt.Errorf("loading wasteland config: %w", err)
95+
}
96+
97+
if key == "mode" {
98+
cfg.Mode = value
99+
}
100+
101+
if err := store.Save(cfg); err != nil {
102+
return fmt.Errorf("saving wasteland config: %w", err)
103+
}
104+
105+
fmt.Fprintf(stdout, "%s = %s\n", key, value)
106+
return nil
107+
}
108+
109+
func validateMode(value string) error {
110+
switch value {
111+
case federation.ModeWildWest, federation.ModePR:
112+
return nil
113+
default:
114+
return fmt.Errorf("invalid mode %q: must be %q or %q", value, federation.ModeWildWest, federation.ModePR)
115+
}
116+
}

cmd/wl/cmd_config_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/steveyegge/wasteland/internal/federation"
7+
)
8+
9+
func TestValidateMode_Valid(t *testing.T) {
10+
for _, mode := range []string{federation.ModeWildWest, federation.ModePR} {
11+
if err := validateMode(mode); err != nil {
12+
t.Errorf("validateMode(%q) = %v, want nil", mode, err)
13+
}
14+
}
15+
}
16+
17+
func TestValidateMode_Invalid(t *testing.T) {
18+
for _, mode := range []string{"", "chaos", "merge", "WILD-WEST"} {
19+
if err := validateMode(mode); err == nil {
20+
t.Errorf("validateMode(%q) = nil, want error", mode)
21+
}
22+
}
23+
}
24+
25+
func TestValidConfigKeys(t *testing.T) {
26+
if !validConfigKeys["mode"] {
27+
t.Error("expected 'mode' to be a valid config key")
28+
}
29+
if validConfigKeys["nonexistent"] {
30+
t.Error("'nonexistent' should not be a valid config key")
31+
}
32+
}

cmd/wl/cmd_delete.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ func runDelete(cmd *cobra.Command, stdout, _ io.Writer, wantedID string, noPush
4545
return fmt.Errorf("loading wasteland config: %w", err)
4646
}
4747

48+
mc := newMutationContext(wlCfg, wantedID, noPush, stdout)
49+
cleanup, err := mc.Setup()
50+
if err != nil {
51+
return err
52+
}
53+
defer cleanup()
54+
4855
store := commons.NewWLCommons(wlCfg.LocalDir)
4956

5057
if err := deleteWanted(store, wantedID); err != nil {
@@ -53,11 +60,12 @@ func runDelete(cmd *cobra.Command, stdout, _ io.Writer, wantedID string, noPush
5360

5461
fmt.Fprintf(stdout, "%s Withdrawn %s\n", style.Bold.Render("✓"), wantedID)
5562
fmt.Fprintf(stdout, " Status: withdrawn\n")
56-
57-
if !noPush {
58-
_ = commons.PushWithSync(wlCfg.LocalDir, stdout)
63+
if mc.BranchName() != "" {
64+
fmt.Fprintf(stdout, " Branch: %s\n", mc.BranchName())
5965
}
6066

67+
mc.Push()
68+
6169
return nil
6270
}
6371

cmd/wl/cmd_done.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ func runDone(cmd *cobra.Command, stdout, _ io.Writer, wantedID, evidence string,
5555
}
5656
rigHandle := wlCfg.RigHandle
5757

58+
mc := newMutationContext(wlCfg, wantedID, noPush, stdout)
59+
cleanup, err := mc.Setup()
60+
if err != nil {
61+
return err
62+
}
63+
defer cleanup()
64+
5865
store := commons.NewWLCommons(wlCfg.LocalDir)
5966
completionID := commons.GeneratePrefixedID("c", wantedID, rigHandle)
6067

@@ -67,11 +74,12 @@ func runDone(cmd *cobra.Command, stdout, _ io.Writer, wantedID, evidence string,
6774
fmt.Fprintf(stdout, " Completed by: %s\n", rigHandle)
6875
fmt.Fprintf(stdout, " Evidence: %s\n", evidence)
6976
fmt.Fprintf(stdout, " Status: in_review\n")
70-
71-
if !noPush {
72-
_ = commons.PushWithSync(wlCfg.LocalDir, stdout)
77+
if mc.BranchName() != "" {
78+
fmt.Fprintf(stdout, " Branch: %s\n", mc.BranchName())
7379
}
7480

81+
mc.Push()
82+
7583
return nil
7684
}
7785

0 commit comments

Comments
 (0)