1package cli
2
3import (
4 "encoding/hex"
5 "fmt"
6 "sort"
7 "strings"
8 "time"
9
10 "arche/internal/diff"
11 "arche/internal/object"
12
13 "github.com/spf13/cobra"
14)
15
16var explainCmd = &cobra.Command{
17 Use: "explain <change-id>",
18 Short: "Show the full story of a change: diff, history, bookmarks, issues",
19 Long: `arche explain <change-id> shows everything Arche knows about a unit of work
20in a single command — the VCS equivalent of "show me all the context for this
21change." It gathers information from the object store, obsolescence markers,
22bookmark table, issue tracker, and operation log.
23
24Accepts change IDs (ch:xxxx), hash prefixes, bookmarks, and relative
25addresses (@ and @-N).`,
26 Args: cobra.ExactArgs(1),
27 RunE: func(cmd *cobra.Command, args []string) error {
28 r := openRepo()
29 defer r.Close()
30
31 commitID, err := resolveRef(r, args[0])
32 if err != nil {
33 return err
34 }
35
36 c, err := r.ReadCommit(commitID)
37 if err != nil {
38 return fmt.Errorf("read commit: %w", err)
39 }
40
41 phase, _ := r.Store.GetPhase(commitID)
42
43 fmt.Printf("Change: ch:%s\n", c.ChangeID)
44 fmt.Printf("Commit: %s\n", hex.EncodeToString(commitID[:]))
45 fmt.Printf("Author: %s <%s>\n", c.Author.Name, c.Author.Email)
46 fmt.Printf("Date: %s\n", c.Author.Timestamp.Format(time.RFC3339))
47 fmt.Printf("Phase: %s\n", phase)
48 fmt.Printf("Message: %s\n", c.Message)
49
50 bms, _ := r.Store.ListBookmarks()
51 var bmNames []string
52
53 for _, bm := range bms {
54 if bm.CommitID == commitID {
55 bmNames = append(bmNames, bm.Name)
56 }
57 }
58 if len(bmNames) > 0 {
59 sort.Strings(bmNames)
60 fmt.Printf("Bookmarks: %s\n", strings.Join(bmNames, ", "))
61 }
62
63 obsoleteIDs, _ := r.Store.ListObjectsByKind(string(object.KindObsolete))
64 type obsEntry struct {
65 pred [32]byte
66 succs [][32]byte
67 reason string
68 ts int64
69 }
70 var chain []obsEntry
71 for _, oid := range obsoleteIDs {
72 _, raw, err := r.Store.ReadObject(oid)
73 if err != nil {
74 continue
75 }
76 om, err := object.DecodeObsolete(raw)
77 if err != nil {
78 continue
79 }
80 involved := om.Predecessor == commitID
81 for _, s := range om.Successors {
82 if s == commitID {
83 involved = true
84 }
85 }
86 if involved {
87 chain = append(chain, obsEntry{
88 pred: om.Predecessor,
89 succs: om.Successors,
90 reason: om.Reason,
91 ts: om.Timestamp,
92 })
93 }
94 }
95
96 if len(chain) > 0 {
97 fmt.Println("\nObsolescence chain:")
98 for _, e := range chain {
99 ts := time.Unix(e.ts, 0).Format("2006-01-02 15:04:05")
100 predC, _ := r.ReadCommit(e.pred)
101 predMsg := ""
102 if predC != nil {
103 predMsg = " — " + bisectFirstLine(predC.Message)
104 }
105 fmt.Printf(" %s [%s]%s\n", object.Short(e.pred), ts, predMsg)
106 for _, s := range e.succs {
107 succC, _ := r.ReadCommit(s)
108 succMsg := ""
109 if succC != nil {
110 succMsg = " — " + bisectFirstLine(succC.Message)
111 }
112 reason := e.reason
113 if reason == "" {
114 reason = "rewrite"
115 }
116 fmt.Printf(" → %s (%s)%s\n", object.Short(s), reason, succMsg)
117 }
118 }
119 }
120
121 issueEventIDs, _ := r.Store.ListObjectsByKind(string(object.KindIssueEvent))
122 type issueRef struct {
123 issueID string
124 kind string
125 }
126 issueRefSet := make(map[string]string)
127 changeIDStr := "ch:" + c.ChangeID
128 commitHex := hex.EncodeToString(commitID[:])
129
130 for _, eid := range issueEventIDs {
131 _, raw, err := r.Store.ReadObject(eid)
132 if err != nil {
133 continue
134 }
135 ev, err := object.DecodeIssueEvent(raw)
136 if err != nil {
137 continue
138 }
139 payload := string(ev.Payload)
140 if strings.Contains(payload, changeIDStr) ||
141 strings.Contains(payload, commitHex[:12]) {
142 existing, ok := issueRefSet[ev.IssueID]
143 if !ok || existing != "ref" {
144 issueRefSet[ev.IssueID] = ev.Kind
145 }
146 }
147 }
148
149 if len(issueRefSet) > 0 {
150 fmt.Println("\nLinked issues:")
151 issueIDs := make([]string, 0, len(issueRefSet))
152 for id := range issueRefSet {
153 issueIDs = append(issueIDs, id)
154 }
155 sort.Strings(issueIDs)
156 for _, id := range issueIDs {
157 fmt.Printf(" #%s (via %s)\n", id, issueRefSet[id])
158 }
159 }
160
161 diffs, err := diff.CommitDiff(r, commitID)
162 if err == nil && len(diffs) > 0 {
163 fmt.Println("\nDiff:")
164 for _, d := range diffs {
165 lines := strings.Count(d.Patch, "\n")
166 added, removed := 0, 0
167 for _, line := range strings.Split(d.Patch, "\n") {
168 if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
169 added++
170 } else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
171 removed++
172 }
173 }
174 _ = lines
175 fmt.Printf(" %c %s (+%d -%d)\n", d.Status, d.Path, added, removed)
176 }
177 }
178
179 ops, _ := r.Store.ListOperations(200)
180 var relOps []string
181 for _, op := range ops {
182 if strings.Contains(op.Before, commitHex[:12]) ||
183 strings.Contains(op.After, commitHex[:12]) ||
184 strings.Contains(op.Metadata, c.ChangeID[:8]) {
185 ts := time.Unix(op.Timestamp, 0).Format("2006-01-02 15:04:05")
186 if op.Metadata != "" {
187 relOps = append(relOps, fmt.Sprintf(" #%-4d %-12s %s %s", op.Seq, op.Kind, ts, op.Metadata))
188 } else {
189 relOps = append(relOps, fmt.Sprintf(" #%-4d %-12s %s", op.Seq, op.Kind, ts))
190 }
191 }
192 }
193 if len(relOps) > 0 {
194 fmt.Println("\nOperation log:")
195 for _, l := range relOps {
196 fmt.Println(l)
197 }
198 }
199
200 return nil
201 },
202}