Blank identifier discarding results; verify intentional ignoring of return values
_, _ = fmt.Fprintf(&sb, format,
1// SPDX-License-Identifier: MIT23package processor45import (6 "encoding/csv"7 "fmt"8 "os"9 "slices"10 "strings"11 "time"1213 jsoniter "github.com/json-iterator/go"14 glanguage "golang.org/x/text/language"15 gmessage "golang.org/x/text/message"16)1718// authorTimelineSparkCells is the fixed width of the Activity sparkline cell19// in the 79-column tabular report. The fixed-resolution per-bucket series is20// downsampled to this many cells.21const authorTimelineSparkCells = 242223// authorTimelineTopN is the number of authors shown individually before the24// rest are folded into a single "others (N)" row whose sparkline sums their25// activity — mirroring the --by-author rollup rather than either hard-capping26// (hides the tail) or listing every author (a wall of sparklines). CSV/JSON27// stay uncapped.28const authorTimelineTopN = 152930// authorTimelineBucket is one bucket's worth of accumulator state for one31// author.32type authorTimelineBucket struct {33 Commits int34 CodeDelta int6435}3637// authorTimelineRow is the materialised per-author result.38type authorTimelineRow struct {39 Name string40 Email string41 TotalCommits int42 CodeDelta int6443 Series []authorTimelineBucket44}4546// authorTimelineEvent records one observed commit's contribution so the47// observer can bin it under the real window's Bucketing in Finalise (the48// engine doesn't expose the window until after the walk).49type authorTimelineEvent struct {50 Author authorID51 When time.Time52 CodeDelta int6453}5455// historyAuthorTimelineObserver collects per-commit events during the walk56// and bins them into per-(author, bucket) totals on Finalise. It implements57// MailmapObserver to pick up the mailmap without paying for the full58// start-tree classification — the report tracks deltas, not last-toucher59// attribution, so per-line baseline state is not needed.60type historyAuthorTimelineObserver struct {61 registry *authorRegistry62 events []authorTimelineEvent63 bucketCount int6465 bucket Bucketing66 window HistoryWindow67 rows []authorTimelineRow68}6970func newHistoryAuthorTimelineObserver(buckets int) *historyAuthorTimelineObserver {71 if buckets <= 0 {72 buckets = 6073 }74 return &historyAuthorTimelineObserver{75 registry: newAuthorRegistry(nil),76 bucketCount: buckets,77 }78}7980// SetMailmap satisfies MailmapObserver — rebuilds the registry with the81// repo's .mailmap so the timeline folds identities the same way the author82// rollup does.83func (o *historyAuthorTimelineObserver) SetMailmap(mm *mailmap) {84 o.registry = newAuthorRegistry(mm)85}8687func (o *historyAuthorTimelineObserver) Observe(c CommitInfo, changes []FileChange) {88 aid := o.registry.intern(c.Author, c.Email)8990 var delta int6491 for _, fc := range changes {92 added := splitAddedCodeLines(fc.AddedRanges, fc.LineTypes)93 removed := splitRemovedCodeLines(fc.RemovedRanges, fc.RemovedLineTypes)94 delta += int64(added) - int64(removed)95 }96 o.events = append(o.events, authorTimelineEvent{97 Author: aid,98 When: c.When,99 CodeDelta: delta,100 })101}102103func (o *historyAuthorTimelineObserver) Finalise(window HistoryWindow, head HeadSnapshot) {104 o.window = window105 o.bucket = NewBucketing(window.From, window.To, o.bucketCount)106107 series := map[authorID][]authorTimelineBucket{}108 for _, ev := range o.events {109 s := series[ev.Author]110 if s == nil {111 s = make([]authorTimelineBucket, o.bucket.N)112 series[ev.Author] = s113 }114 idx := o.bucket.Index(ev.When)115 s[idx].Commits++116 s[idx].CodeDelta += ev.CodeDelta117 }118119 rows := make([]authorTimelineRow, 0, len(series))120 for aid, s := range series {121 if aid == sentinelAuthorID {122 continue123 }124 rec := o.registry.record(aid)125 row := authorTimelineRow{126 Name: rec.Name,127 Email: rec.Email,128 Series: s,129 }130 for _, b := range s {131 row.TotalCommits += b.Commits132 row.CodeDelta += b.CodeDelta133 }134 rows = append(rows, row)135 }136137 slices.SortFunc(rows, func(a, b authorTimelineRow) int {138 if a.TotalCommits != b.TotalCommits {139 if a.TotalCommits < b.TotalCommits {140 return 1141 }142 return -1143 }144 return strings.Compare(a.Name, b.Name)145 })146 o.rows = rows147}148149// splitAddedCodeLines returns the count of added lines classified as code by150// the new blob's LineTypes vector. Mirrors splitChurnByType but only returns151// the code component.152func splitAddedCodeLines(added []LineRange, lineTypes []LineType) int {153 code := 0154 for _, r := range added {155 for i := 0; i < r.Count; i++ {156 ln := r.Start - 1 + i157 if ln < 0 || ln >= len(lineTypes) {158 continue159 }160 if lineTypes[ln] == LINE_CODE {161 code++162 }163 }164 }165 return code166}167168// splitRemovedCodeLines counts removed lines classified as code by the OLD169// blob's LineTypes vector. RemovedRanges are in old-blob (pre-diff) line170// coordinates, so they index removedLineTypes directly. Removed-side mirror171// of splitAddedCodeLines — together they yield a symmetric code-only delta.172// Returns 0 when the old blob could not be classified.173func splitRemovedCodeLines(removed []LineRange, removedLineTypes []LineType) int {174 code := 0175 for _, r := range removed {176 for i := 0; i < r.Count; i++ {177 ln := r.Start - 1 + i178 if ln < 0 || ln >= len(removedLineTypes) {179 continue180 }181 if removedLineTypes[ln] == LINE_CODE {182 code++183 }184 }185 }186 return code187}188189// runAuthorTimelineReport is the dispatch entry point called from Process()190// when --by-author --timeline is set. Opens the repo, walks the window with191// the configured bucket count, and writes the chosen format.192func runAuthorTimelineReport(repoPath string) error {193 observer := newHistoryAuthorTimelineObserver(HistoryBuckets)194 if _, err := runHistory(repoPath, observer); err != nil {195 return err196 }197 out, err := renderAuthorTimeline(observer)198 if err != nil {199 return err200 }201 if FileOutput == "" {202 fmt.Print(out)203 } else {204 if err := os.WriteFile(FileOutput, []byte(out), 0644); err != nil {205 return err206 }207 fmt.Println("results written to " + FileOutput)208 }209 return nil210}211212func renderAuthorTimeline(o *historyAuthorTimelineObserver) (string, error) {213 switch strings.ToLower(Format) {214 case "", "tabular", "wide":215 return renderAuthorTimelineTabular(o), nil216 case "csv":217 return renderAuthorTimelineCSV(o)218 case "json":219 return renderAuthorTimelineJSON(o)220 default:221 return "", fmt.Errorf("unsupported --format %q for --by-author --timeline (supported: tabular, csv, json)", Format)222 }223}224225// Tabular column format. 24+1+24+1+8+1+9+1+10 = 79.226var tabularShortAuthorTimelineFormatHead = "%-24s %-24s %8s %9s %-10s\n"227228// Wide tabular: same columns, Author widened to fill the 109-col rule so long229// names show in full. 54+1+24+1+8+1+9+1+10 = 109.230var tabularWideAuthorTimelineFormatHead = "%-54s %-24s %8s %9s %-10s\n"231232func renderAuthorTimelineTabular(o *historyAuthorTimelineObserver) string {233 wide := More || strings.EqualFold(Format, "wide")234 brk := tabularBreakFor(wide)235236 var sb strings.Builder237 sb.WriteString(historyHeader("Authors", o.window, wide))238239 p := gmessage.NewPrinter(glanguage.Make(os.Getenv("LANG")))240241 nameTrim, nameWidth, format := 23, 24, tabularShortAuthorTimelineFormatHead242 if wide {243 nameTrim, nameWidth, format = 53, 54, tabularWideAuthorTimelineFormatHead244 }245246 _, _ = fmt.Fprintf(&sb, format,247 "Author", "Activity", "Commits", "Code±", "")248 sb.WriteString(brk)249250 writeRow := func(r authorTimelineRow) {251 nameCol := unicodeAwareTrim(r.Name, nameTrim)252 nameCol = unicodeAwareRightPad(nameCol, nameWidth)253 spark := renderAuthorTimelineSparkline(r.Series, authorTimelineSparkCells)254 tag := authorTimelineTag(r.Series, o.bucket.Width)255 commitsStr := formatWithCommas(p, int64(r.TotalCommits))256 codeStr := formatCodeDelta(p, r.CodeDelta)257 _, _ = fmt.Fprintf(&sb, format,258 nameCol, spark, commitsStr, codeStr, tag)259 }260261 limit := min(len(o.rows), authorTimelineTopN)262 for _, r := range o.rows[:limit] {263 writeRow(r)264 }265 if len(o.rows) > limit {266 writeRow(aggregateOthersTimelineRow(o.rows[limit:]))267 }268269 sb.WriteString(brk)270 return sb.String()271}272273// aggregateOthersTimelineRow folds the tail authors into one synthetic row: the274// sparkline sums their per-bucket commit counts, so the combined activity trend275// of everyone below the cut is still visible rather than dropped.276func aggregateOthersTimelineRow(tail []authorTimelineRow) authorTimelineRow {277 others := authorTimelineRow{Name: fmt.Sprintf("others (%d)", len(tail))}278 if len(tail) == 0 {279 return others280 }281 n := len(tail[0].Series)282 others.Series = make([]authorTimelineBucket, n)283 for _, r := range tail {284 others.TotalCommits += r.TotalCommits285 others.CodeDelta += r.CodeDelta286 for i := 0; i < n && i < len(r.Series); i++ {287 others.Series[i].Commits += r.Series[i].Commits288 others.Series[i].CodeDelta += r.Series[i].CodeDelta289 }290 }291 return others292}293294// renderAuthorTimelineSparkline projects per-bucket commit counts to a295// sparkline using the shared helper from history_render.go.296func renderAuthorTimelineSparkline(series []authorTimelineBucket, cells int) string {297 if len(series) == 0 {298 if asciiOutput() {299 return strings.Repeat(".", cells)300 }301 return strings.Repeat("▁", cells)302 }303 values := make([]float64, len(series))304 for i, b := range series {305 values[i] = float64(b.Commits)306 }307 return renderSparkline(values, cells)308}309310// authorTimelineTag derives the trailing presentation tag. Returns:311// - "↑" — final bucket is >= 80% of the row's peak (still active).312// - "quiet Nmo" — trailing zero buckets cover >= 1 month of wall clock.313// - "" — no notable trend.314//315// Tags are tabular-only; CSV/JSON do not carry them.316func authorTimelineTag(series []authorTimelineBucket, width time.Duration) string {317 if len(series) == 0 {318 return ""319 }320 maxCommits := 0321 for _, b := range series {322 if b.Commits > maxCommits {323 maxCommits = b.Commits324 }325 }326 if maxCommits == 0 {327 return ""328 }329330 last := series[len(series)-1].Commits331 if last > 0 && float64(last) >= 0.8*float64(maxCommits) {332 return "↑"333 }334335 zeroTail := 0336 for i := len(series) - 1; i >= 0; i-- {337 if series[i].Commits != 0 {338 break339 }340 zeroTail++341 }342 if zeroTail == 0 || width <= 0 {343 return ""344 }345 totalQuiet := time.Duration(zeroTail) * width346 const month = 30 * 24 * time.Hour347 if totalQuiet < month {348 return ""349 }350 months := max(int((totalQuiet+month/2)/month), 1)351 return fmt.Sprintf("quiet %dmo", months)352}353354// formatCodeDelta renders a signed code delta with a leading sign and355// thousands separators, e.g. "+38,000" or "-21".356func formatCodeDelta(p *gmessage.Printer, delta int64) string {357 if delta >= 0 {358 return "+" + formatWithCommas(p, delta)359 }360 return "-" + formatWithCommas(p, -delta)361}362363func renderAuthorTimelineCSV(o *historyAuthorTimelineObserver) (string, error) {364 var sb strings.Builder365 sb.WriteString(formatWindowComment(o.window))366 sb.WriteByte('\n')367 _, _ = fmt.Fprintf(&sb, "# buckets: %d\n", o.bucket.N)368369 w := csv.NewWriter(&sb)370 _ = w.Write([]string{"Author", "Email", "BucketStart", "Commits", "CodeDelta"})371372 for _, r := range o.rows {373 for i, b := range r.Series {374 bucketStart := o.bucket.Start(i).UTC().Format(historyDateLayout)375 _ = w.Write([]string{376 r.Name,377 r.Email,378 bucketStart,379 fmt.Sprintf("%d", b.Commits),380 fmt.Sprintf("%d", b.CodeDelta),381 })382 }383 }384 w.Flush()385 if err := w.Error(); err != nil {386 return "", err387 }388 return sb.String(), nil389}390391type authorTimelineJSONBucket struct {392 BucketStart string `json:"bucketStart"`393 Commits int `json:"commits"`394 CodeDelta int64 `json:"codeDelta"`395}396397type authorTimelineJSONAuthor struct {398 Name string `json:"name"`399 Email string `json:"email"`400 TotalCommits int `json:"totalCommits"`401 CodeDelta int64 `json:"codeDelta"`402 Series []authorTimelineJSONBucket `json:"series"`403}404405type authorTimelineJSONWindow struct {406 Depth int `json:"depth"`407 Commits int `json:"commits"`408 From string `json:"from"`409 To string `json:"to"`410}411412type authorTimelineJSONDoc struct {413 Report string `json:"report"`414 Window authorTimelineJSONWindow `json:"window"`415 Buckets int `json:"buckets"`416 Authors []authorTimelineJSONAuthor `json:"authors"`417}418419func renderAuthorTimelineJSON(o *historyAuthorTimelineObserver) (string, error) {420 doc := authorTimelineJSONDoc{421 Report: "author-timeline",422 Window: authorTimelineJSONWindow{423 Depth: o.window.Depth,424 Commits: o.window.Commits,425 From: formatWindowDate(o.window.From),426 To: formatWindowDate(o.window.To),427 },428 Buckets: o.bucket.N,429 Authors: make([]authorTimelineJSONAuthor, 0, len(o.rows)),430 }431 for _, r := range o.rows {432 ja := authorTimelineJSONAuthor{433 Name: r.Name,434 Email: r.Email,435 TotalCommits: r.TotalCommits,436 CodeDelta: r.CodeDelta,437 Series: make([]authorTimelineJSONBucket, 0, len(r.Series)),438 }439 for i, b := range r.Series {440 ja.Series = append(ja.Series, authorTimelineJSONBucket{441 BucketStart: o.bucket.Start(i).UTC().Format(historyDateLayout),442 Commits: b.Commits,443 CodeDelta: b.CodeDelta,444 })445 }446 doc.Authors = append(doc.Authors, ja)447 }448 b, err := jsoniter.Marshal(doc)449 if err != nil {450 return "", err451 }452 return string(b), nil453}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.