arche / internal/cli/cmd_fold.go

commit 154431fd
  1package cli
  2
  3import (
  4	"fmt"
  5	"time"
  6
  7	"arche/internal/object"
  8	"arche/internal/repo"
  9	"arche/internal/store"
 10	"arche/internal/wc"
 11
 12	"github.com/spf13/cobra"
 13)
 14
 15var foldCmd = &cobra.Command{
 16	Use:   "fold <commit>",
 17	Short: "Fold (squash) a commit into its parent",
 18	Long: `Merge <commit>'s changes into its parent, combining them into a single commit.
 19The folded commit and its original are both marked obsolete.
 20HEAD is updated to the new combined commit.`,
 21	Args: cobra.ExactArgs(1),
 22	RunE: func(cmd *cobra.Command, args []string) error {
 23		r := openRepo()
 24		defer r.Close()
 25
 26		targetID, err := resolveRef(r, args[0])
 27		if err != nil {
 28			return err
 29		}
 30		target, err := r.ReadCommit(targetID)
 31		if err != nil {
 32			return err
 33		}
 34		if len(target.Parents) == 0 {
 35			return fmt.Errorf("cannot fold root commit (no parent)")
 36		}
 37		parentID := target.Parents[0]
 38		parent, err := r.ReadCommit(parentID)
 39		if err != nil {
 40			return err
 41		}
 42
 43		if !foldForceRewrite {
 44			for _, c := range []*object.Commit{target, parent} {
 45				if c.Phase == object.PhasePublic {
 46					return fmt.Errorf("commit ch:%s is public; use --force-rewrite to rewrite history", c.ChangeID)
 47				}
 48			}
 49		}
 50
 51		before, _ := r.CaptureRefState()
 52		now := time.Now()
 53		sig := object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now}
 54
 55		msg := parent.Message + "\n\n" + target.Message
 56
 57		tx, err := r.Store.Begin()
 58		if err != nil {
 59			return err
 60		}
 61
 62		newCID, err := r.Store.AllocChangeID(tx)
 63		if err != nil {
 64			r.Store.Rollback(tx)
 65			return err
 66		}
 67
 68		combined := &object.Commit{
 69			TreeID:    target.TreeID,
 70			Parents:   parent.Parents,
 71			ChangeID:  newCID,
 72			Author:    parent.Author,
 73			Committer: sig,
 74			Message:   msg,
 75			Phase:     parent.Phase,
 76		}
 77		newID, err := repo.WriteCommitTx(r.Store, tx, combined)
 78		if err != nil {
 79			r.Store.Rollback(tx)
 80			return err
 81		}
 82		if err := r.Store.SetChangeCommit(tx, newCID, newID); err != nil {
 83			r.Store.Rollback(tx)
 84			return err
 85		}
 86
 87		for _, oldID := range [][32]byte{parentID, targetID} {
 88			obs := &object.ObsoleteMarker{Predecessor: oldID, Successors: [][32]byte{newID}, Reason: "fold"}
 89			if _, err := repo.WriteObsoleteTx(r.Store, tx, obs); err != nil {
 90				r.Store.Rollback(tx)
 91				return err
 92			}
 93		}
 94
 95		after := fmt.Sprintf(`{"head":%q,"tip":%q}`, object.FormatChangeID(newCID), fmt.Sprintf("%x", newID))
 96		op := store.Operation{
 97			Kind: "fold", Timestamp: now.Unix(), Before: before, After: after,
 98			Metadata: "folded ch:" + target.ChangeID + " into parent",
 99		}
100		if _, err := r.Store.InsertOperation(tx, op); err != nil {
101			r.Store.Rollback(tx)
102			return err
103		}
104		if err := r.Store.Commit(tx); err != nil {
105			return err
106		}
107
108		_, headID, err := r.HeadCommit()
109		if err == nil && headID == targetID {
110			w := wc.New(r)
111			if err := w.Materialize(combined.TreeID, object.FormatChangeID(newCID)); err != nil {
112				return err
113			}
114			if err := r.WriteHead(object.FormatChangeID(newCID)); err != nil {
115				return err
116			}
117		}
118
119		fmt.Printf("Folded ch:%s into ch:%s → ch:%s\n", target.ChangeID, parent.ChangeID, newCID)
120		return nil
121	},
122}
123
124var foldForceRewrite bool
125
126func init() {
127	foldCmd.Flags().BoolVar(&foldForceRewrite, "force-rewrite", false, "allow rewriting public commits")
128}