1package cli
2
3import (
4 "fmt"
5 "time"
6
7 "arche/internal/merge"
8 "arche/internal/object"
9 "arche/internal/repo"
10 "arche/internal/store"
11 "arche/internal/wc"
12
13 "github.com/spf13/cobra"
14)
15
16var mergeCmd = &cobra.Command{
17 Use: "merge <commit>",
18 Short: "Merge a commit into the working copy",
19 Long: `Perform a three-way merge of the given commit into the current working copy.
20Conflicts are stored as first-class objects and written to the working
21directory with standard conflict markers - no operation is blocked.`,
22 Args: cobra.ExactArgs(1),
23 RunE: func(cmd *cobra.Command, args []string) error {
24 r := openRepo()
25 defer r.Close()
26
27 theirsID, err := resolveRef(r, args[0])
28 if err != nil {
29 return err
30 }
31
32 theirs, err := r.ReadCommit(theirsID)
33 if err != nil {
34 return err
35 }
36
37 head, headID, err := r.HeadCommit()
38 if err != nil {
39 return err
40 }
41
42 baseID := findMergeBase(r, headID, theirsID)
43
44 before, _ := r.CaptureRefState()
45 now := time.Now()
46
47 result, err := merge.Trees(r, baseID, head.TreeID, theirs.TreeID)
48 if err != nil {
49 return fmt.Errorf("merge failed: %w", err)
50 }
51
52 sig := object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now}
53 mMsg := fmt.Sprintf("Merge ch:%s", theirs.ChangeID)
54 if len(result.Conflicts) > 0 {
55 mMsg += fmt.Sprintf(" (%d conflict(s))", len(result.Conflicts))
56 }
57
58 tx, err := r.Store.Begin()
59 if err != nil {
60 return err
61 }
62
63 newChangeID, err := r.Store.AllocChangeID(tx)
64 if err != nil {
65 r.Store.Rollback(tx)
66 return err
67 }
68
69 mc := &object.Commit{
70 TreeID: result.TreeID,
71 Parents: [][32]byte{headID, theirsID},
72 ChangeID: newChangeID,
73 Author: sig,
74 Committer: sig,
75 Message: mMsg,
76 Phase: object.PhaseDraft,
77 }
78 mcID, err := repo.WriteCommitTx(r.Store, tx, mc)
79 if err != nil {
80 r.Store.Rollback(tx)
81 return err
82 }
83 if err := r.Store.SetChangeCommit(tx, newChangeID, mcID); err != nil {
84 r.Store.Rollback(tx)
85 return err
86 }
87
88 if err := r.Store.ClearAllConflicts(tx); err != nil {
89 r.Store.Rollback(tx)
90 return err
91 }
92 for _, cp := range result.Conflicts {
93 if err := r.Store.AddConflict(tx, cp); err != nil {
94 r.Store.Rollback(tx)
95 return err
96 }
97 }
98 after := buildMergeRefState(mcID, object.FormatChangeID(newChangeID))
99 op := store.Operation{
100 Kind: "merge", Timestamp: now.Unix(), Before: before, After: after,
101 Metadata: "merged ch:" + theirs.ChangeID,
102 }
103 if _, err := r.Store.InsertOperation(tx, op); err != nil {
104 r.Store.Rollback(tx)
105 return err
106 }
107 if err := r.Store.Commit(tx); err != nil {
108 return err
109 }
110
111 w := wc.New(r)
112 newCID := object.FormatChangeID(newChangeID)
113 if err := w.Materialize(result.TreeID, newCID); err != nil {
114 return fmt.Errorf("materialize merge: %w", err)
115 }
116 if err := r.WriteHead(newCID); err != nil {
117 return err
118 }
119
120 if len(result.Conflicts) > 0 {
121 fmt.Printf("Merged ch:%s into %s with %d conflict(s):\n",
122 theirs.ChangeID, newCID, len(result.Conflicts))
123 for _, p := range result.Conflicts {
124 fmt.Printf(" conflict: %s\n", p)
125 }
126 fmt.Println("Resolve conflicts then run 'arche resolve <path>'.")
127 } else {
128 fmt.Printf("Merged ch:%s into %s (clean)\n", theirs.ChangeID, newCID)
129 }
130 return nil
131 },
132}
133
134func findMergeBase(r *repo.Repo, a, b [32]byte) [32]byte {
135 ancestorsA := make(map[[32]byte]bool)
136 queueA := [][32]byte{a}
137 for len(queueA) > 0 {
138 id := queueA[0]
139 queueA = queueA[1:]
140 if ancestorsA[id] {
141 continue
142 }
143 ancestorsA[id] = true
144 c, err := r.ReadCommit(id)
145 if err != nil {
146 break
147 }
148 queueA = append(queueA, c.Parents...)
149 }
150
151 queueB := [][32]byte{b}
152 seenB := make(map[[32]byte]bool)
153 for len(queueB) > 0 {
154 id := queueB[0]
155 queueB = queueB[1:]
156 if seenB[id] {
157 continue
158 }
159 seenB[id] = true
160 if ancestorsA[id] {
161 return id
162 }
163 c, err := r.ReadCommit(id)
164 if err != nil {
165 break
166 }
167 queueB = append(queueB, c.Parents...)
168 }
169 return object.ZeroID
170}
171
172func buildMergeRefState(commitID [32]byte, changeID string) string {
173 return fmt.Sprintf(`{"head":%q,"tip":%q}`, changeID, fmt.Sprintf("%x", commitID))
174}