arche / internal/cli/cmd_merge.go

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