1package cli
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 "arche/internal/object"
9 "arche/internal/revset"
10
11 "github.com/spf13/cobra"
12)
13
14var (
15 logLimit int
16 logOps bool
17 logShowSecret bool
18 logWhere string
19 logGraph bool
20)
21
22type logCommit struct {
23 id [32]byte
24 commit *object.Commit
25 phase object.Phase
26 parents [][32]byte
27}
28
29var logCmd = &cobra.Command{
30 Use: "log",
31 Short: "Show the commit DAG",
32 Long: `Walk the commit graph backwards from HEAD (and all bookmarks) and display
33each commit in reverse chronological order.
34
35With --ops, show the operation log instead (equivalent to 'arche op log').`,
36 RunE: func(cmd *cobra.Command, args []string) error {
37 r := openRepo()
38 defer r.Close()
39
40 if logOps {
41 ops, err := r.Store.ListOperations(logLimit)
42 if err != nil {
43 return err
44 }
45 if len(ops) == 0 {
46 fmt.Println("No operations recorded.")
47 return nil
48 }
49 for _, op := range ops {
50 ts := time.Unix(op.Timestamp, 0).Format("2006-01-02 15:04:05")
51 if op.Metadata != "" {
52 fmt.Printf("#%-4d %-14s %s %s\n", op.Seq, op.Kind, ts, op.Metadata)
53 } else {
54 fmt.Printf("#%-4d %-14s %s\n", op.Seq, op.Kind, ts)
55 }
56 }
57 return nil
58 }
59
60 var whereFilter revset.Func
61 if logWhere != "" {
62 var err error
63 whereFilter, err = revset.Parse(logWhere)
64 if err != nil {
65 return err
66 }
67 }
68
69 bms, _ := r.Store.ListBookmarks()
70 bmIndex := make(map[[32]byte][]string)
71 for _, bm := range bms {
72 bmIndex[bm.CommitID] = append(bmIndex[bm.CommitID], bm.Name)
73 }
74 var curHeadID [32]byte
75 if _, id, err := r.HeadCommit(); err == nil {
76 curHeadID = id
77 }
78
79 tips := make(map[[32]byte]bool)
80 if curHeadID != ([32]byte{}) {
81 tips[curHeadID] = true
82 }
83 for _, bm := range bms {
84 tips[bm.CommitID] = true
85 }
86
87 type commitInfo = logCommit
88 allByID := map[[32]byte]*commitInfo{}
89 {
90 seen := map[[32]byte]bool{}
91 queue := make([][32]byte, 0, len(tips))
92 for id := range tips {
93 queue = append(queue, id)
94 }
95 for len(queue) > 0 {
96 id := queue[0]
97 queue = queue[1:]
98 if seen[id] {
99 continue
100 }
101 seen[id] = true
102 c, err := r.ReadCommit(id)
103 if err != nil {
104 continue
105 }
106 phase, _ := r.Store.GetPhase(id)
107 if !logShowSecret && phase == object.PhaseSecret {
108 for _, p := range c.Parents {
109 if !seen[p] {
110 queue = append(queue, p)
111 }
112 }
113 continue
114 }
115 if whereFilter != nil && !whereFilter(id, c, phase) {
116 for _, p := range c.Parents {
117 if !seen[p] {
118 queue = append(queue, p)
119 }
120 }
121 continue
122 }
123 allByID[id] = &commitInfo{id: id, commit: c, phase: phase, parents: c.Parents}
124 for _, p := range c.Parents {
125 if !seen[p] {
126 queue = append(queue, p)
127 }
128 }
129 }
130 }
131
132 childCount := map[[32]byte]int{}
133 for _, ci := range allByID {
134 for _, p := range ci.parents {
135 if _, ok := allByID[p]; ok {
136 childCount[p]++
137 }
138 }
139 }
140 topoQueue := make([][32]byte, 0, len(allByID))
141 for id := range allByID {
142 if childCount[id] == 0 {
143 topoQueue = append(topoQueue, id)
144 }
145 }
146 var ordered []commitInfo
147 for len(topoQueue) > 0 {
148 best := 0
149 for i := 1; i < len(topoQueue); i++ {
150 a := allByID[topoQueue[i]].commit.Author.Timestamp
151 b := allByID[topoQueue[best]].commit.Author.Timestamp
152 if a.After(b) {
153 best = i
154 }
155 }
156 id := topoQueue[best]
157 topoQueue[best] = topoQueue[len(topoQueue)-1]
158 topoQueue = topoQueue[:len(topoQueue)-1]
159
160 ci := allByID[id]
161 ordered = append(ordered, *ci)
162 for _, p := range ci.parents {
163 if _, ok := allByID[p]; !ok {
164 continue
165 }
166 childCount[p]--
167 if childCount[p] == 0 {
168 topoQueue = append(topoQueue, p)
169 }
170 }
171 }
172
173 if logLimit > 0 && len(ordered) > logLimit {
174 ordered = ordered[:logLimit]
175 }
176
177 if logGraph {
178 printGraphLog(ordered, bmIndex, curHeadID)
179 } else {
180 for _, ci := range ordered {
181 printCommit(ci.id, ci.commit, bmIndex[ci.id], ci.id == curHeadID)
182 }
183 }
184 return nil
185 },
186}
187
188func init() {
189 logCmd.Flags().IntVarP(&logLimit, "limit", "n", 0, "maximum number of commits to show (0 = all)")
190 logCmd.Flags().BoolVar(&logOps, "ops", false, "show the operation log instead of the commit graph")
191 logCmd.Flags().BoolVarP(&logShowSecret, "secret", "s", false, "include secret commits in output")
192 logCmd.Flags().StringVar(&logWhere, "where", "", `filter commits with a revset expression, e.g. --where 'author(alice) and not public()'`)
193 logCmd.Flags().BoolVar(&logGraph, "graph", false, "show ASCII DAG alongside the log")
194}
195
196func printCommit(id [32]byte, c *object.Commit, bookmarks []string, isHead bool) {
197 prefix := " "
198 if isHead {
199 prefix = "@ "
200 }
201 fmt.Printf("%scommit %x\n", prefix, id)
202 fmt.Printf(" change ch:%s\n", c.ChangeID)
203 if len(bookmarks) > 0 {
204 fmt.Printf(" marks %v\n", bookmarks)
205 }
206 fmt.Printf(" author %s <%s>\n", c.Author.Name, c.Author.Email)
207 fmt.Printf(" date %s\n", c.Author.Timestamp.Format(time.RFC1123))
208 fmt.Printf(" phase %s\n", c.Phase)
209 if c.Message != "" {
210 fmt.Printf("\n %s\n", c.Message)
211 }
212 fmt.Println()
213}
214
215func printGraphLog(ordered []logCommit, bmIndex map[[32]byte][]string, headID [32]byte) {
216 type entry struct {
217 id [32]byte
218 commit *object.Commit
219 parents [][32]byte
220 marks []string
221 isHead bool
222 }
223 entries := make([]entry, 0, len(ordered))
224 for _, ci := range ordered {
225 entries = append(entries, entry{
226 id: ci.id,
227 commit: ci.commit,
228 parents: ci.parents,
229 marks: bmIndex[ci.id],
230 isHead: ci.id == headID,
231 })
232 }
233
234 var zero [32]byte
235 lanes := [][32]byte{}
236
237 findLane := func(id [32]byte) int {
238 for i, l := range lanes {
239 if l == id {
240 return i
241 }
242 }
243 return -1
244 }
245 freeLane := func() int {
246 for i, l := range lanes {
247 if l == zero {
248 return i
249 }
250 }
251 lanes = append(lanes, zero)
252 return len(lanes) - 1
253 }
254
255 renderCols := func(node int, char string) string {
256 var b strings.Builder
257 for i, l := range lanes {
258 if i == node {
259 b.WriteString(char)
260 } else if l != zero {
261 b.WriteString("|")
262 } else {
263 b.WriteString(" ")
264 }
265 b.WriteString(" ")
266 }
267 return strings.TrimRight(b.String(), " ")
268 }
269
270 for _, e := range entries {
271 col := findLane(e.id)
272 if col == -1 {
273 col = freeLane()
274 }
275
276 lanes[col] = zero
277 nodeChar := "*"
278 if e.isHead {
279 nodeChar = "@"
280 }
281 prefix := renderCols(col, nodeChar)
282
283 msg := e.commit.Message
284 if i := strings.IndexByte(msg, '\n'); i >= 0 {
285 msg = msg[:i]
286 }
287 ts := e.commit.Author.Timestamp.Format("2006-01-02")
288 marks := ""
289 if len(e.marks) > 0 {
290 marks = " [" + strings.Join(e.marks, ", ") + "]"
291 }
292 fmt.Printf("%s ch:%s %s (%s)%s\n", prefix, e.commit.ChangeID, msg, ts, marks)
293
294 if len(e.parents) == 0 {
295 trailing := renderCols(-1, "|")
296 if strings.TrimSpace(trailing) != "" {
297 fmt.Println(trailing)
298 }
299 continue
300 }
301
302 parentCols := make([]int, 0, len(e.parents))
303 for i, p := range e.parents {
304 if i == 0 {
305 lanes[col] = p
306 parentCols = append(parentCols, col)
307 } else {
308 if pc := findLane(p); pc != -1 {
309 parentCols = append(parentCols, pc)
310 } else {
311 nc := freeLane()
312 lanes[nc] = p
313 parentCols = append(parentCols, nc)
314 }
315 }
316 }
317
318 if len(parentCols) > 1 {
319 var b strings.Builder
320 maxCol := 0
321 for _, pc := range parentCols {
322 if pc > maxCol {
323 maxCol = pc
324 }
325 }
326 pSet := map[int]bool{}
327 for _, pc := range parentCols {
328 pSet[pc] = true
329 }
330 for i := 0; i <= maxCol || i < len(lanes); i++ {
331 if i >= len(lanes) {
332 break
333 }
334 if i == col && pSet[i] {
335 b.WriteString("|")
336 } else if pSet[i] {
337 b.WriteString("\\")
338 } else if lanes[i] != zero {
339 b.WriteString("|")
340 } else {
341 b.WriteString(" ")
342 }
343 b.WriteString(" ")
344 }
345 fmt.Println(strings.TrimRight(b.String(), " "))
346 }
347
348 trailing := renderCols(-1, "|")
349 if strings.TrimSpace(trailing) != "" {
350 fmt.Println(trailing)
351 }
352
353 for len(lanes) > 0 && lanes[len(lanes)-1] == zero {
354 lanes = lanes[:len(lanes)-1]
355 }
356 }
357}