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}