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}