arche / internal/cli/cmd_stack.go

commit 154431fd
  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}