Blank identifier discarding results; verify intentional ignoring of return values
_, _ = fmt.Fprintf(&sb, format, "Language", "Trend", "Code", "Share", "Change")
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// languagesTimelineSparkCells is the fixed width of the Trend sparkline cell19// in the 79-column tabular report. The fixed-resolution per-bucket series is20// downsampled to this many cells.21const languagesTimelineSparkCells = 262223// languagesTimelineWideSparkCells is the sparkline width for --wide (109 cols).24const languagesTimelineWideSparkCells = 562526// languagesTimelineTopN caps tabular rows. CSV/JSON are uncapped.27const languagesTimelineTopN = 122829// languagesTimelineRow is the materialised per-language result.30type languagesTimelineRow struct {31 Language string32 StartingLines int6433 CodeNow int6434 Change int6435 SharePercent float6436 // Deltas is the per-bucket net code delta for this language.37 Deltas []int6438 // Trajectory is the absolute code count at the end of each bucket, i.e.39 // StartingLines + cumulative sum of Deltas. Used for the sparkline.40 Trajectory []int6441}4243// languagesTimelineEvent records one observed file change so the observer44// can bin it under the real window's Bucketing in Finalise (the engine45// doesn't expose the window until after the walk).46type languagesTimelineEvent struct {47 Language string48 When time.Time49 CodeDelta int6450}5152// historyLanguagesObserver collects per-commit per-language code deltas and53// the baseline snapshot, then materialises per-language trajectories in54// Finalise. Implements BaselineObserver so the engine seeds it with the55// pre-window tree before the walk.56type historyLanguagesObserver struct {57 starting map[string]int6458 events []languagesTimelineEvent59 bucketCount int6061 bucket Bucketing62 window HistoryWindow63 rows []languagesTimelineRow64}6566func newHistoryLanguagesObserver(buckets int) *historyLanguagesObserver {67 if buckets <= 0 {68 buckets = 6069 }70 return &historyLanguagesObserver{71 starting: map[string]int64{},72 bucketCount: buckets,73 }74}7576// Seed sums baseline code lines per language so each language gets an77// absolute starting line count.78func (o *historyLanguagesObserver) Seed(baseline BaselineSnapshot) {79 for _, bf := range baseline.Files {80 var code int6481 for _, lt := range bf.LineTypes {82 if lt == LINE_CODE {83 code++84 }85 }86 if code == 0 {87 continue88 }89 o.starting[bf.Language] += code90 }91}9293func (o *historyLanguagesObserver) Observe(c CommitInfo, changes []FileChange) {94 for _, fc := range changes {95 added := splitAddedCodeLines(fc.AddedRanges, fc.LineTypes)96 removed := splitRemovedCodeLines(fc.RemovedRanges, fc.RemovedLineTypes)97 delta := int64(added) - int64(removed)98 if delta == 0 {99 continue100 }101 o.events = append(o.events, languagesTimelineEvent{102 Language: fc.Language,103 When: c.When,104 CodeDelta: delta,105 })106 }107}108109func (o *historyLanguagesObserver) Finalise(window HistoryWindow, head HeadSnapshot) {110 o.window = window111 o.bucket = NewBucketing(window.From, window.To, o.bucketCount)112113 deltas := map[string][]int64{}114 for _, ev := range o.events {115 s := deltas[ev.Language]116 if s == nil {117 s = make([]int64, o.bucket.N)118 deltas[ev.Language] = s119 }120 idx := o.bucket.Index(ev.When)121 s[idx] += ev.CodeDelta122 }123124 // Union of languages — those with a starting count and those touched in125 // the window.126 langSet := map[string]struct{}{}127 for lang := range o.starting {128 langSet[lang] = struct{}{}129 }130 for lang := range deltas {131 langSet[lang] = struct{}{}132 }133134 rows := make([]languagesTimelineRow, 0, len(langSet))135 for lang := range langSet {136 start := o.starting[lang]137 series := deltas[lang]138 if series == nil {139 series = make([]int64, o.bucket.N)140 }141 traj := make([]int64, o.bucket.N)142 running := start143 for i, d := range series {144 running += d145 if running < 0 {146 running = 0147 }148 traj[i] = running149 }150 codeNow := start151 if len(traj) > 0 {152 codeNow = traj[len(traj)-1]153 }154 rows = append(rows, languagesTimelineRow{155 Language: lang,156 StartingLines: start,157 CodeNow: codeNow,158 Change: codeNow - start,159 Deltas: series,160 Trajectory: traj,161 })162 }163164 var grand int64165 for _, r := range rows {166 grand += r.CodeNow167 }168 if grand > 0 {169 for i := range rows {170 rows[i].SharePercent = float64(rows[i].CodeNow) / float64(grand) * 100.0171 }172 }173174 slices.SortFunc(rows, func(a, b languagesTimelineRow) int {175 if a.CodeNow != b.CodeNow {176 if a.CodeNow < b.CodeNow {177 return 1178 }179 return -1180 }181 return strings.Compare(a.Language, b.Language)182 })183 o.rows = rows184}185186// runLanguagesTimelineReport is the dispatch entry point called from187// Process() when --timeline is set without --by-author. Opens the repo,188// walks the window with the configured bucket count, and writes the chosen189// format.190func runLanguagesTimelineReport(repoPath string) error {191 observer := newHistoryLanguagesObserver(HistoryBuckets)192 if _, err := runHistory(repoPath, observer); err != nil {193 return err194 }195 out, err := renderLanguagesTimeline(observer)196 if err != nil {197 return err198 }199 if FileOutput == "" {200 fmt.Print(out)201 } else {202 if err := os.WriteFile(FileOutput, []byte(out), 0644); err != nil {203 return err204 }205 fmt.Println("results written to " + FileOutput)206 }207 return nil208}209210func renderLanguagesTimeline(o *historyLanguagesObserver) (string, error) {211 switch strings.ToLower(Format) {212 case "", "tabular", "wide":213 return renderLanguagesTimelineTabular(o), nil214 case "csv":215 return renderLanguagesTimelineCSV(o)216 case "json":217 return renderLanguagesTimelineJSON(o)218 default:219 return "", fmt.Errorf("unsupported --format %q for --timeline (supported: tabular, csv, json)", Format)220 }221}222223// Tabular column format. 20+1+26+1+11+1+8+1+10 = 79.224var tabularShortLanguagesTimelineFormatHead = "%-20s %-26s %11s %8s %10s\n"225226// Wide tabular: same columns, wider sparkline. 20+1+56+1+11+1+8+1+10 = 109.227var tabularWideLanguagesTimelineFormatHead = "%-20s %-56s %11s %8s %10s\n"228229func renderLanguagesTimelineTabular(o *historyLanguagesObserver) string {230 wide := More || strings.EqualFold(Format, "wide")231 brk := tabularBreakFor(wide)232233 var sb strings.Builder234 sb.WriteString(historyHeader("Languages", o.window, wide))235236 p := gmessage.NewPrinter(glanguage.Make(os.Getenv("LANG")))237238 format := tabularShortLanguagesTimelineFormatHead239 cells := languagesTimelineSparkCells240 if wide {241 format = tabularWideLanguagesTimelineFormatHead242 cells = languagesTimelineWideSparkCells243 }244245 _, _ = fmt.Fprintf(&sb, format, "Language", "Trend", "Code", "Share", "Change")246 sb.WriteString(brk)247248 limit := min(len(o.rows), languagesTimelineTopN)249250 for i := range limit {251 r := o.rows[i]252 langCol := unicodeAwareTrim(r.Language, 19)253 langCol = unicodeAwareRightPad(langCol, 20)254 spark := renderLanguagesTrajectorySparkline(r.Trajectory, cells)255 codeStr := formatWithCommas(p, r.CodeNow)256 shareStr := fmt.Sprintf("%6.1f%%", r.SharePercent)257 changeStr := formatCodeDelta(p, r.Change)258 _, _ = fmt.Fprintf(&sb, format, langCol, spark, codeStr, shareStr, changeStr)259 }260261 sb.WriteString(brk)262 return sb.String()263}264265// renderLanguagesTrajectorySparkline downsamples the absolute trajectory to266// a sparkline. Each line is normalised to its own min/max for shape clarity267// (the Share column carries cross-language comparison).268func renderLanguagesTrajectorySparkline(traj []int64, cells int) string {269 if len(traj) == 0 {270 if asciiOutput() {271 return strings.Repeat(".", cells)272 }273 return strings.Repeat("▁", cells)274 }275 values := make([]float64, len(traj))276 for i, v := range traj {277 values[i] = float64(v)278 }279 return renderSparkline(values, cells)280}281282func renderLanguagesTimelineCSV(o *historyLanguagesObserver) (string, error) {283 var sb strings.Builder284 sb.WriteString(formatWindowComment(o.window))285 sb.WriteByte('\n')286 _, _ = fmt.Fprintf(&sb, "# buckets: %d\n", o.bucket.N)287288 w := csv.NewWriter(&sb)289 _ = w.Write([]string{290 "Language", "BucketStart", "Code", "CodeNow", "SharePercent", "Change",291 })292293 for _, r := range o.rows {294 for i, code := range r.Trajectory {295 bucketStart := o.bucket.Start(i).UTC().Format(historyDateLayout)296 _ = w.Write([]string{297 r.Language,298 bucketStart,299 fmt.Sprintf("%d", code),300 fmt.Sprintf("%d", r.CodeNow),301 fmt.Sprintf("%.1f", r.SharePercent),302 fmt.Sprintf("%d", r.Change),303 })304 }305 }306 w.Flush()307 if err := w.Error(); err != nil {308 return "", err309 }310 return sb.String(), nil311}312313type languagesTimelineJSONBucket struct {314 BucketStart string `json:"bucketStart"`315 Code int64 `json:"code"`316}317318type languagesTimelineJSONLang struct {319 Language string `json:"language"`320 CodeNow int64 `json:"codeNow"`321 SharePercent float64 `json:"sharePercent"`322 Change int64 `json:"change"`323 Series []languagesTimelineJSONBucket `json:"series"`324}325326type languagesTimelineJSONWindow struct {327 Depth int `json:"depth"`328 Commits int `json:"commits"`329 From string `json:"from"`330 To string `json:"to"`331}332333type languagesTimelineJSONDoc struct {334 Report string `json:"report"`335 Window languagesTimelineJSONWindow `json:"window"`336 Buckets int `json:"buckets"`337 Languages []languagesTimelineJSONLang `json:"languages"`338}339340func renderLanguagesTimelineJSON(o *historyLanguagesObserver) (string, error) {341 doc := languagesTimelineJSONDoc{342 Report: "languages-timeline",343 Window: languagesTimelineJSONWindow{344 Depth: o.window.Depth,345 Commits: o.window.Commits,346 From: formatWindowDate(o.window.From),347 To: formatWindowDate(o.window.To),348 },349 Buckets: o.bucket.N,350 Languages: make([]languagesTimelineJSONLang, 0, len(o.rows)),351 }352 for _, r := range o.rows {353 jl := languagesTimelineJSONLang{354 Language: r.Language,355 CodeNow: r.CodeNow,356 SharePercent: round1(r.SharePercent),357 Change: r.Change,358 Series: make([]languagesTimelineJSONBucket, 0, len(r.Trajectory)),359 }360 for i, code := range r.Trajectory {361 jl.Series = append(jl.Series, languagesTimelineJSONBucket{362 BucketStart: o.bucket.Start(i).UTC().Format(historyDateLayout),363 Code: code,364 })365 }366 doc.Languages = append(doc.Languages, jl)367 }368 b, err := jsoniter.Marshal(doc)369 if err != nil {370 return "", err371 }372 return string(b), nil373}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.