arche / internal/cli/cmd_graft.go

commit a22ffc45
  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 graftCmd = &cobra.Command{
 17	Use:   "graft <id>",
 18	Short: "Copy a commit onto the current HEAD, preserving its change ID",
 19	Long: `arche graft copies the changes introduced by <id> onto the current HEAD.
 20The grafted commit carries the same change ID, author, and message as the
 21original. An ObsoleteMarker links the original commit to the grafted copy,
 22making the relationship visible in 'arche explain' and forge stack views.
 23
 24Unlike 'arche rebase', graft does not move the original commit — it copies it.
 25This makes graft suitable for backporting a fix to a release branch without
 26modifying the main-line history.`,
 27	Args: cobra.ExactArgs(1),
 28	RunE: func(cmd *cobra.Command, args []string) error {
 29		r := openRepo()
 30		defer r.Close()
 31
 32		srcID, err := resolveRef(r, args[0])
 33		if err != nil {
 34			return err
 35		}
 36
 37		src, err := r.ReadCommit(srcID)
 38		if err != nil {
 39			return fmt.Errorf("read source commit: %w", err)
 40		}
 41
 42		_, headID, err := r.HeadCommit()
 43		if err != nil {
 44			return err
 45		}
 46
 47		if srcID == headID {
 48			fmt.Println("Nothing to graft: source is already the current HEAD.")
 49			return nil
 50		}
 51
 52		headCommit, err := r.ReadCommit(headID)
 53		if err != nil {
 54			return err
 55		}
 56
 57		var baseTreeID [32]byte
 58		if len(src.Parents) > 0 {
 59			if pc, perr := r.ReadCommit(src.Parents[0]); perr == nil {
 60				baseTreeID = pc.TreeID
 61			}
 62		}
 63
 64		result, err := merge.Trees(r, baseTreeID, src.TreeID, headCommit.TreeID)
 65		if err != nil {
 66			return fmt.Errorf("graft merge: %w", err)
 67		}
 68
 69		before, _ := r.CaptureRefState()
 70		now := time.Now()
 71
 72		newCommit := &object.Commit{
 73			TreeID:    result.TreeID,
 74			Parents:   [][32]byte{headID},
 75			ChangeID:  src.ChangeID,
 76			Author:    src.Author,
 77			Committer: object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now},
 78			Message:   src.Message,
 79			Phase:     object.PhaseDraft,
 80		}
 81
 82		tx, err := r.Store.Begin()
 83		if err != nil {
 84			return err
 85		}
 86
 87		newCommitID, err := repo.WriteCommitTx(r.Store, tx, newCommit)
 88		if err != nil {
 89			r.Store.Rollback(tx)
 90			return err
 91		}
 92
 93		if err := r.Store.SetChangeCommit(tx, src.ChangeID, newCommitID); err != nil {
 94			r.Store.Rollback(tx)
 95			return err
 96		}
 97
 98		obs := &object.ObsoleteMarker{
 99			Predecessor: srcID,
100			Successors:  [][32]byte{newCommitID},
101			Reason:      "graft",
102			Timestamp:   now.Unix(),
103		}
104		if _, err := repo.WriteObsoleteTx(r.Store, tx, obs); err != nil {
105			r.Store.Rollback(tx)
106			return err
107		}
108
109		newChangeID := object.FormatChangeID(src.ChangeID)
110		opAfter := buildMergeRefState(newCommitID, newChangeID)
111		op := store.Operation{
112			Kind:      "graft",
113			Timestamp: now.Unix(),
114			Before:    before,
115			After:     opAfter,
116			Metadata:  fmt.Sprintf("grafted %s onto %x", newChangeID, headID[:6]),
117		}
118		if _, err := r.Store.InsertOperation(tx, op); err != nil {
119			r.Store.Rollback(tx)
120			return err
121		}
122
123		if err := r.Store.ClearAllConflicts(tx); err != nil {
124			r.Store.Rollback(tx)
125			return err
126		}
127		for _, cp := range result.Conflicts {
128			if err := r.Store.AddConflict(tx, cp); err != nil {
129				r.Store.Rollback(tx)
130				return err
131			}
132		}
133
134		if err := r.Store.Commit(tx); err != nil {
135			return err
136		}
137
138		w := wc.New(r)
139		if err := w.Materialize(result.TreeID, newChangeID); err != nil {
140			return err
141		}
142		if err := r.WriteHead(newChangeID); err != nil {
143			return err
144		}
145
146		if len(result.Conflicts) > 0 {
147			fmt.Printf("Graft paused at %s (%d conflict(s)):\n", newChangeID, len(result.Conflicts))
148			for _, p := range result.Conflicts {
149				fmt.Printf("  conflict: %s\n", p)
150			}
151			fmt.Println("Resolve conflicts, then run 'arche resolve <path>' and 'arche snap'.")
152			return nil
153		}
154
155		fmt.Printf("Grafted %s — %s\n", newChangeID, bisectFirstLine(src.Message))
156		return nil
157	},
158}