arche / internal/cli/cmd_log.go

commit a22ffc45
  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}