1package cli
2
3import (
4 "database/sql"
5 "encoding/hex"
6 "fmt"
7 "strings"
8
9 "arche/internal/object"
10 "arche/internal/repo"
11)
12
13func resolveRef(r *repo.Repo, ref string) ([32]byte, error) {
14 if ref == "@" || ref == "" {
15 _, id, err := r.HeadCommit()
16 return id, err
17 }
18 if strings.HasPrefix(ref, "@-") {
19 n := 1
20 suffix := ref[2:]
21 if suffix != "" {
22 if _, err := fmt.Sscanf(suffix, "%d", &n); err != nil {
23 return object.ZeroID, fmt.Errorf("invalid relative ref %q", ref)
24 }
25 }
26 _, commitID, err := r.HeadCommit()
27 if err != nil {
28 return object.ZeroID, err
29 }
30 return nthAncestor(r, commitID, n)
31 }
32
33 if strings.HasPrefix(ref, "ch:") {
34 bare := ref[3:]
35 id, err := r.Store.GetChangeCommit(bare)
36 if err != nil {
37 if err == sql.ErrNoRows {
38 return object.ZeroID, fmt.Errorf("change ID %q not found", ref)
39 }
40 return object.ZeroID, err
41 }
42 return id, nil
43 }
44
45 if isChangeIDChars(ref) {
46 id, err := r.Store.GetChangeCommit(ref)
47 if err == nil {
48 return id, nil
49 }
50 if err != sql.ErrNoRows {
51 return object.ZeroID, err
52 }
53 }
54
55 if isHexString(ref) && len(ref) >= 4 {
56 id, err := resolveHashPrefix(r, ref)
57 if err == nil {
58 return id, nil
59 }
60 }
61
62 bm, err := r.Store.GetBookmark(ref)
63 if err == nil && bm != nil {
64 return bm.CommitID, nil
65 }
66
67 return object.ZeroID, fmt.Errorf("unknown commit reference %q", ref)
68}
69
70func nthAncestor(r *repo.Repo, id [32]byte, n int) ([32]byte, error) {
71 cur := id
72 for i := 0; i < n; i++ {
73 c, err := r.ReadCommit(cur)
74 if err != nil {
75 return object.ZeroID, err
76 }
77 if len(c.Parents) == 0 {
78 return object.ZeroID, fmt.Errorf("commit %s has no parent (went %d/%d)", object.Short(cur), i, n)
79 }
80 cur = c.Parents[0]
81 }
82 return cur, nil
83}
84
85func resolveHashPrefix(r *repo.Repo, prefix string) ([32]byte, error) {
86 p := prefix
87 if len(p)%2 != 0 {
88 p = p + "0"
89 }
90 lower, err := hex.DecodeString(p)
91 if err != nil {
92 return object.ZeroID, err
93 }
94
95 var found [32]byte
96 var count int
97
98 allChanges, err := listAllCommitIDs(r)
99 if err != nil {
100 return object.ZeroID, err
101 }
102 for _, id := range allChanges {
103 h := fmt.Sprintf("%x", id)
104 if strings.HasPrefix(h, prefix) {
105 found = id
106 count++
107 }
108 }
109 _ = lower
110 if count == 0 {
111 return object.ZeroID, fmt.Errorf("hash prefix %q not found", prefix)
112 }
113 if count > 1 {
114 return object.ZeroID, fmt.Errorf("ambiguous hash prefix %q matches %d commits", prefix, count)
115 }
116 return found, nil
117}
118
119func listAllCommitIDs(r *repo.Repo) ([][32]byte, error) {
120 bms, err := r.Store.ListBookmarks()
121 if err != nil {
122 return nil, err
123 }
124
125 seen := make(map[[32]byte]bool)
126 var queue [][32]byte
127
128 _, headID, err := r.HeadCommit()
129 if err == nil {
130 queue = append(queue, headID)
131 }
132 for _, bm := range bms {
133 queue = append(queue, bm.CommitID)
134 }
135
136 var all [][32]byte
137 for len(queue) > 0 {
138 id := queue[0]
139 queue = queue[1:]
140 if seen[id] {
141 continue
142 }
143 seen[id] = true
144 all = append(all, id)
145 c, err := r.ReadCommit(id)
146 if err != nil {
147 continue
148 }
149 for _, p := range c.Parents {
150 if !seen[p] {
151 queue = append(queue, p)
152 }
153 }
154 }
155 return all, nil
156}
157
158func isHexString(s string) bool {
159 for _, c := range s {
160 if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
161 return false
162 }
163 }
164 return true
165}
166
167func isChangeIDChars(s string) bool {
168 const alpha = "abcdefghjkmnpqrstvwxyz"
169 for _, c := range s {
170 if !strings.ContainsRune(alpha, c) {
171 return false
172 }
173 }
174 return len(s) >= 4
175}