arche / internal/cli/cmd_stack_land.go

commit a22ffc45
  1package cli
  2
  3import (
  4	"fmt"
  5	"time"
  6
  7	"arche/internal/object"
  8	"arche/internal/store"
  9	"arche/internal/wc"
 10
 11	"github.com/spf13/cobra"
 12)
 13
 14var stackLandBookmark string
 15
 16var stackLandCmd = &cobra.Command{
 17	Use:   "land [change-id]",
 18	Short: "Promote a draft change to public (land it)",
 19	Long: `arche stack land promotes a draft change to public phase, marking it as
 20permanently landed. If no change-id is given, the oldest (bottom-most) draft
 21in the current stack is landed.
 22
 23The canonical commit for the change is set to public phase. Any draft commits
 24higher in the stack remain draft and are still parented to the now-public commit
 25— run 'arche stack rebase' afterwards if any re-chaining is needed.
 26
 27Use --bookmark to advance (or create) a named bookmark to the landed commit,
 28which makes it visible as a branch tip on the forge and to other clones.`,
 29	Args: cobra.MaximumNArgs(1),
 30	RunE: func(cmd *cobra.Command, args []string) error {
 31		r := openRepo()
 32		defer r.Close()
 33
 34		chain, err := collectDraftChain(r)
 35		if err != nil {
 36			return err
 37		}
 38		if len(chain) == 0 {
 39			return fmt.Errorf("no draft commits in stack — nothing to land")
 40		}
 41
 42		var targetChangeID string
 43		var targetCommitID [32]byte
 44
 45		if len(args) == 1 {
 46			resolvedID, rerr := resolveRef(r, args[0])
 47			if rerr != nil {
 48				return rerr
 49			}
 50			c, cerr := r.ReadCommit(resolvedID)
 51			if cerr != nil {
 52				return fmt.Errorf("read commit: %w", cerr)
 53			}
 54
 55			found := false
 56			for _, e := range chain {
 57				if e.commit.ChangeID == c.ChangeID {
 58					found = true
 59					break
 60				}
 61			}
 62			if !found {
 63				return fmt.Errorf("%s is not in the current draft stack", args[0])
 64			}
 65
 66			canonID, cerr2 := r.Store.GetChangeCommit(c.ChangeID)
 67			if cerr2 != nil || canonID == object.ZeroID {
 68				canonID = resolvedID
 69			}
 70			targetCommitID = canonID
 71			targetChangeID = c.ChangeID
 72		} else {
 73			e := chain[0]
 74			canonID, cerr := r.Store.GetChangeCommit(e.commit.ChangeID)
 75			if cerr != nil || canonID == object.ZeroID {
 76				canonID = e.commitID
 77			}
 78			targetCommitID = canonID
 79			targetChangeID = e.commit.ChangeID
 80		}
 81
 82		targetCommit, err := r.ReadCommit(targetCommitID)
 83		if err != nil {
 84			return fmt.Errorf("read target commit: %w", err)
 85		}
 86
 87		before, _ := r.CaptureRefState()
 88		now := time.Now()
 89
 90		tx, err := r.Store.Begin()
 91		if err != nil {
 92			return err
 93		}
 94
 95		if err := r.Store.SetPhase(tx, targetCommitID, object.PhasePublic); err != nil {
 96			r.Store.Rollback(tx)
 97			return fmt.Errorf("set phase: %w", err)
 98		}
 99
100		if stackLandBookmark != "" {
101			bm := store.Bookmark{Name: stackLandBookmark, CommitID: targetCommitID}
102			if err := r.Store.SetBookmark(tx, bm); err != nil {
103				r.Store.Rollback(tx)
104				return fmt.Errorf("set bookmark %q: %w", stackLandBookmark, err)
105			}
106		}
107
108		changeIDFmt := object.FormatChangeID(targetChangeID)
109		opAfter := buildMergeRefState(targetCommitID, changeIDFmt)
110		op := store.Operation{
111			Kind:      "stack-land",
112			Timestamp: now.Unix(),
113			Before:    before,
114			After:     opAfter,
115			Metadata:  fmt.Sprintf("landed %s", changeIDFmt),
116		}
117		if _, err := r.Store.InsertOperation(tx, op); err != nil {
118			r.Store.Rollback(tx)
119			return err
120		}
121
122		if err := r.Store.Commit(tx); err != nil {
123			return err
124		}
125
126		fmt.Printf("Landed %s — %s\n", changeIDFmt, bisectFirstLine(targetCommit.Message))
127		if stackLandBookmark != "" {
128			fmt.Printf("Bookmark %q → %x\n", stackLandBookmark, targetCommitID[:6])
129		}
130
131		_, headID, herr := r.HeadCommit()
132		if herr == nil && headID == targetCommitID {
133			w := wc.New(r)
134			_ = w.Materialize(targetCommit.TreeID, changeIDFmt)
135		}
136
137		remaining := 0
138		for _, e := range chain {
139			if e.commit.ChangeID != targetChangeID {
140				remaining++
141			}
142		}
143		if remaining > 0 {
144			fmt.Printf("%d draft commit(s) remain above the landed change.\n", remaining)
145			fmt.Println("Run 'arche stack rebase' if any re-chaining is needed.")
146		}
147
148		return nil
149	},
150}
151
152func init() {
153	stackLandCmd.Flags().StringVar(&stackLandBookmark, "bookmark", "", "create or advance this bookmark to the landed commit")
154}