1package cli
2
3import (
4 "fmt"
5 "os"
6 "strings"
7
8 "arche/internal/object"
9 "arche/internal/repo"
10 "arche/internal/store"
11 "arche/internal/syncpkg"
12
13 "github.com/spf13/cobra"
14)
15
16var stackCmd = &cobra.Command{
17 Use: "stack",
18 Short: "Manage stacked changes",
19}
20
21var stackPushCmd = &cobra.Command{
22 Use: "push [remote]",
23 Short: "Publish the draft stack as per-change bookmarks for review",
24 Long: `arche stack push publishes the chain of draft commits from the current HEAD
25back to the nearest public ancestor as individual bookmarks on the remote.
26
27Each draft change gets a bookmark named stack/<change-id-prefix>, grouped as a
28reviewable unit on the forge. When you amend a change and run stack push again
29all affected downstream bookmarks are updated in a single operation.
30
31Only draft commits are included. The first public (or secret) ancestor marks
32the base of the stack.`,
33 Args: cobra.MaximumNArgs(1),
34 RunE: func(cmd *cobra.Command, args []string) error {
35 r := openRepo()
36 defer r.Close()
37
38 remoteName := "origin"
39 if len(args) == 1 {
40 remoteName = args[0]
41 }
42
43 rc := findRemote(r.Cfg.Remotes, remoteName)
44 if rc == nil {
45 return fmt.Errorf("no remote named %q; add one to .arche/config.toml:\n\n [[remote]]\n name = %q\n url = \"http://host:8765\"\n token = \"secret\"", remoteName, remoteName)
46 }
47
48 chain, err := collectDraftChain(r)
49 if err != nil {
50 return err
51 }
52 if len(chain) == 0 {
53 return fmt.Errorf("no draft commits in stack — HEAD is already public")
54 }
55
56 tx, err := r.Store.Begin()
57 if err != nil {
58 return err
59 }
60
61 var bmNames []string
62 for _, e := range chain {
63 name := "stack/" + e.commit.ChangeID[:8]
64 bm := store.Bookmark{Name: name, CommitID: e.commitID}
65 if setErr := r.Store.SetBookmark(tx, bm); setErr != nil {
66 r.Store.Rollback(tx)
67 return fmt.Errorf("set bookmark %q: %w", name, setErr)
68 }
69 bmNames = append(bmNames, name)
70 }
71
72 if err := r.Store.Commit(tx); err != nil {
73 return err
74 }
75
76 fmt.Printf("Pushing stack of %d change(s) to %s (%s):\n\n", len(chain), remoteName, rc.URL)
77 for i, e := range chain {
78 name := bmNames[i]
79 fmt.Printf(" ch:%s → %s — %s\n", e.commit.ChangeID, name, bisectFirstLine(e.commit.Message))
80 }
81 fmt.Println()
82
83 client := syncpkg.NewClient(r, rc.URL, rc.Token)
84 pushOpts := syncpkg.PushOptions{}
85 if err := client.PushWith(pushOpts); err != nil {
86 fmt.Fprintf(os.Stderr, "arche stack push: push failed: %v\n", err)
87 return err
88 }
89
90 fmt.Printf("Stack pushed: %s\n", strings.Join(bmNames, ", "))
91 return nil
92 },
93}
94
95type draftEntry struct {
96 commitID [32]byte
97 commit *object.Commit
98}
99
100func collectDraftChain(r *repo.Repo) ([]draftEntry, error) {
101 _, headID, err := r.HeadCommit()
102 if err != nil {
103 return nil, err
104 }
105
106 var chain []draftEntry
107 cur := headID
108 for {
109 c, err := r.ReadCommit(cur)
110 if err != nil {
111 break
112 }
113 phase, _ := r.Store.GetPhase(cur)
114 if phase != object.PhaseDraft {
115 break
116 }
117 chain = append(chain, draftEntry{commitID: cur, commit: c})
118 if len(c.Parents) == 0 {
119 break
120 }
121 cur = c.Parents[0]
122 }
123
124 for i, j := 0, len(chain)-1; i < j; i, j = i+1, j-1 {
125 chain[i], chain[j] = chain[j], chain[i]
126 }
127 return chain, nil
128}
129
130func init() {
131 stackCmd.AddCommand(stackPushCmd)
132}