arche / internal/cli/cmd_explain.go

commit 154431fd
  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}