1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
// SPDX-License-Identifier: GPL-3.0-or-later
package main
import (
"errors"
"fmt"
"io"
"os"
"strings"
"krino/internal/engine"
"krino/internal/journal"
)
func init() { commands["log"] = cmdLog }
// pastTense renders one of the log's own action words (spec §9's wire
// format - journal.Run.Counts is keyed by these exact strings) as the past
// tense krino log displays. Those strings are never renamed to suit the
// display: krino undo parses them back out of the log, so this map is a
// display-only translation, not a second source of truth. Two families of
// action are deliberately absent, and so never appear in a run's counts
// line at all: run-start/run-end (bookkeeping, not something done to a
// file) and mkdir/undo-mkdir (a side effect of another step, not an
// outcome the user asked for).
var pastTense = map[string]string{
"copy": "copied",
"move": "moved",
"rename": "renamed",
"trash": "trashed",
"delete": "deleted",
"displace": "displaced",
"undo-copy": "undo-copied",
"undo-move": "undo-moved",
"undo-rename": "undo-renamed",
"undo-trash": "undo-trashed",
"undo-displace": "undo-displaced",
}
// countOrder is the fixed order krino log renders a run's counts in.
// journal.Run.Counts is a map, whose iteration order is random; a listing
// that reshuffled its own columns between two invocations of the same
// command would be unreadable.
var countOrder = []string{
"copy", "move", "rename", "trash", "delete", "displace",
"undo-copy", "undo-move", "undo-rename", "undo-trash", "undo-displace",
}
// cmdLog lists recent runs, newest first (spec §11: "krino log [-n N]").
func cmdLog(g *globals, args []string, stdout, stderr io.Writer) int {
fs := flagSet("log", g)
n := fs.Int("n", 10, "")
if code, ok := parse(fs, args, stdout, stderr); !ok {
return code
}
if fs.NArg() > 0 {
return usageError(stderr, "usage: krino log [-n N]")
}
e, errs := engine.Load(mainFile(g))
if len(errs) > 0 {
printDiags(stderr, errs)
return 2
}
runs, err := e.Runs(*n)
if err != nil {
// journal.Runs (via Engine.Runs) fails closed on a genuine read
// error - permissions, a corrupt file - and that must stay
// distinguishable from the ordinary "no log yet" case below rather
// than collapsing into the same message (dispatch notes).
if errors.Is(err, os.ErrNotExist) {
fmt.Fprintln(stdout, "nothing logged yet")
return 0
}
fmt.Fprintf(stderr, "krino: %v\n", err)
return 1
}
// A log file can exist and still hold no runs (a real run with nothing
// actionable still opens the journal - dispatch notes). That is just as
// ordinary as no log file at all: same message, same exit 0, and no
// table header is printed over zero rows.
if len(runs) == 0 {
fmt.Fprintln(stdout, "nothing logged yet")
return 0
}
for _, r := range runs {
fmt.Fprintln(stdout, formatRun(r))
}
return 0
}
// formatRun renders one journal.Run as krino log lists it: id, start time,
// the directories it touched, and its counts (see countsText), with
// "(undone)" appended when a later run has reversed it.
func formatRun(r journal.Run) string {
line := fmt.Sprintf("%s %s %s %s", r.ID, r.Start.Format("2006-01-02 15:04"), strings.Join(r.Dirs, ", "), countsText(r.Counts))
if r.Undone {
line += " (undone)"
}
return line
}
// countsText renders a run's counts in countOrder, past tense, skipping any
// action with no "ok" entries. "nothing applied" covers a run every one of
// whose files was declined or failed before anything ran - Counts only
// tallies "ok" entries, so such a run's map holds nothing this function
// would otherwise print.
func countsText(counts map[string]int) string {
var parts []string
for _, action := range countOrder {
if n := counts[action]; n > 0 {
parts = append(parts, fmt.Sprintf("%d %s", n, pastTense[action]))
}
}
if len(parts) == 0 {
return "nothing applied"
}
return strings.Join(parts, " · ")
}
|