arche / internal/cli/cmd_resolve.go

commit 154431fd
  1package cli
  2
  3import (
  4	"fmt"
  5	"os"
  6	"path/filepath"
  7	"strings"
  8	"time"
  9
 10	"arche/internal/object"
 11	"arche/internal/repo"
 12	"arche/internal/store"
 13	"arche/internal/wc"
 14
 15	"github.com/spf13/cobra"
 16)
 17
 18var resolveCmd = &cobra.Command{
 19	Use:   "resolve <path>",
 20	Short: "Mark a conflict at <path> as resolved",
 21	Long: `After editing a conflicted file to a satisfactory state, run this command
 22to replace the conflict object in the working-copy draft with the current
 23file contents and mark the path as resolved.`,
 24	Args: cobra.ExactArgs(1),
 25	RunE: func(cmd *cobra.Command, args []string) error {
 26		relPath := filepath.ToSlash(args[0])
 27		r := openRepo()
 28		defer r.Close()
 29
 30		head, headID, err := r.HeadCommit()
 31		if err != nil {
 32			return err
 33		}
 34
 35		absPath := filepath.Join(r.Root, relPath)
 36		data, err := os.ReadFile(absPath)
 37		if err != nil {
 38			return fmt.Errorf("read %s: %w", relPath, err)
 39		}
 40
 41		blob := &object.Blob{Content: data}
 42
 43		tx, err := r.Store.Begin()
 44		if err != nil {
 45			return err
 46		}
 47
 48		blobID, err := repo.WriteBlobTx(r.Store, tx, blob)
 49		if err != nil {
 50			r.Store.Rollback(tx)
 51			return err
 52		}
 53
 54		newTreeID, err := replaceEntryInTree(r, tx, head.TreeID, relPath, blobID, object.ModeFile)
 55		if err != nil {
 56			r.Store.Rollback(tx)
 57			return err
 58		}
 59
 60		now := time.Now()
 61		sig := object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now}
 62		newCommit := &object.Commit{
 63			TreeID:    newTreeID,
 64			Parents:   head.Parents,
 65			ChangeID:  head.ChangeID,
 66			Author:    sig,
 67			Committer: sig,
 68			Message:   head.Message,
 69			Phase:     head.Phase,
 70		}
 71		newCommitID, err := repo.WriteCommitTx(r.Store, tx, newCommit)
 72		if err != nil {
 73			r.Store.Rollback(tx)
 74			return err
 75		}
 76		if err := r.Store.SetChangeCommit(tx, head.ChangeID, newCommitID); err != nil {
 77			r.Store.Rollback(tx)
 78			return err
 79		}
 80
 81		if err := r.Store.ClearConflict(tx, relPath); err != nil {
 82			r.Store.Rollback(tx)
 83			return err
 84		}
 85		before := fmt.Sprintf("%x", headID[:6])
 86		op := store.Operation{
 87			Kind: "resolve", Timestamp: now.Unix(), Before: before,
 88			After: fmt.Sprintf("%x", newCommitID[:6]), Metadata: "resolved " + relPath,
 89		}
 90		if _, err := r.Store.InsertOperation(tx, op); err != nil {
 91			r.Store.Rollback(tx)
 92			return err
 93		}
 94		if err := r.Store.Commit(tx); err != nil {
 95			return err
 96		}
 97
 98		w := wc.New(r)
 99		if err := w.Materialize(newTreeID, object.FormatChangeID(head.ChangeID)); err != nil {
100			return err
101		}
102		fmt.Printf("Resolved: %s\n", relPath)
103		return nil
104	},
105}
106
107func replaceEntryInTree(r *repo.Repo, tx *store.Tx, treeID [32]byte, path string, blobID [32]byte, m object.EntryMode) ([32]byte, error) {
108	parts := strings.SplitN(path, "/", 2)
109
110	tree, err := r.ReadTree(treeID)
111	if err != nil {
112		return object.ZeroID, err
113	}
114
115	newEntries := make([]object.TreeEntry, 0, len(tree.Entries))
116	found := false
117	for _, e := range tree.Entries {
118		if e.Name == parts[0] {
119			found = true
120			if len(parts) == 1 {
121				newEntries = append(newEntries, object.TreeEntry{Mode: m, Name: parts[0], ObjectID: blobID})
122			} else {
123				subID, err := replaceEntryInTree(r, tx, e.ObjectID, parts[1], blobID, m)
124				if err != nil {
125					return object.ZeroID, err
126				}
127				newEntries = append(newEntries, object.TreeEntry{Mode: object.ModeDir, Name: parts[0], ObjectID: subID})
128			}
129		} else {
130			newEntries = append(newEntries, e)
131		}
132	}
133	if !found {
134		return object.ZeroID, fmt.Errorf("path %q not found in tree", path)
135	}
136	return repo.WriteTreeTx(r.Store, tx, &object.Tree{Entries: newEntries})
137}