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// languagesTimelineRow is the materialised per-language result.27type languagesTimelineRow struct {28 Language string29 StartingLines int6430 CodeNow int6431 Change int6432 SharePercent float6433 // Deltas is the per-bucket net code delta for this language.34 Deltas []int6435 // Trajectory is the absolute code count at the end of each bucket, i.e.36 // StartingLines + cumulative sum of Deltas. Used for the sparkline.37 Trajectory []int6438}3940// languagesTimelineEvent records one observed file change so the observer41// can bin it under the real window's Bucketing in Finalise (the engine42// doesn't expose the window until after the walk).43type languagesTimelineEvent struct {44 Language string45 When time.Time46 CodeDelta int6447}4849// historyLanguagesObserver collects per-commit per-language code deltas and50// the baseline snapshot, then materialises per-language trajectories in51// Finalise. Implements BaselineObserver so the engine seeds it with the52// pre-window tree before the walk.53type historyLanguagesObserver struct {54 starting map[string]int6455 events []languagesTimelineEvent56 bucketCount int5758 bucket Bucketing59 window HistoryWindow60 rows []languagesTimelineRow61}6263func newHistoryLanguagesObserver(buckets int) *historyLanguagesObserver {64 if buckets <= 0 {65 buckets = 6066 }67 return &historyLanguagesObserver{68 starting: map[string]int64{},69 bucketCount: buckets,70 }71}7273// Seed sums baseline code lines per language so each language gets an74// absolute starting line count.75func (o *historyLanguagesObserver) Seed(baseline BaselineSnapshot) {76 for _, bf := range baseline.Files {77 var code int6478 for _, lt := range bf.LineTypes {79 if lt == LINE_CODE {80 code++81 }82 }83 if code == 0 {84 continue85 }86 o.starting[bf.Language] += code87 }88}8990func (o *historyLanguagesObserver) Observe(c CommitInfo, changes []FileChange) {91 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 if delta == 0 {96 continue97 }98 o.events = append(o.events, languagesTimelineEvent{99 Language: fc.Language,100 When: c.When,101 CodeDelta: delta,102 })103 }104}105106func (o *historyLanguagesObserver) Finalise(window HistoryWindow, head HeadSnapshot) {107 o.window = window108 o.bucket = NewBucketing(window.From, window.To, o.bucketCount)109110 deltas := map[string][]int64{}111 for _, ev := range o.events {112 s := deltas[ev.Language]113 if s == nil {114 s = make([]int64, o.bucket.N)115 deltas[ev.Language] = s116 }117 idx := o.bucket.Index(ev.When)118 s[idx] += ev.CodeDelta119 }120121 // Union of languages — those with a starting count and those touched in122 // the window.123 langSet := map[string]struct{}{}124 for lang := range o.starting {125 langSet[lang] = struct{}{}126 }127 for lang := range deltas {128 langSet[lang] = struct{}{}129 }130131 rows := make([]languagesTimelineRow, 0, len(langSet))132 for lang := range langSet {133 start := o.starting[lang]134 series := deltas[lang]135 if series == nil {136 series = make([]int64, o.bucket.N)137 }138 traj := make([]int64, o.bucket.N)139 running := start140 for i, d := range series {141 running += d142 if running < 0 {143 running = 0144 }145 traj[i] = running146 }147 codeNow := start148 if len(traj) > 0 {149 codeNow = traj[len(traj)-1]150 }151 rows = append(rows, languagesTimelineRow{152 Language: lang,153 StartingLines: start,154 CodeNow: codeNow,155 Change: codeNow - start,156 Deltas: series,157 Trajectory: traj,158 })159 }160161 var grand int64162 for _, r := range rows {163 grand += r.CodeNow164 }165 if grand > 0 {166 for i := range rows {167 rows[i].SharePercent = float64(rows[i].CodeNow) / float64(grand) * 100.0168 }169 }170171 slices.SortFunc(rows, func(a, b languagesTimelineRow) int {172 if a.CodeNow != b.CodeNow {173 if a.CodeNow < b.CodeNow {174 return 1175 }176 return -1177 }178 return strings.Compare(a.Language, b.Language)179 })180 o.rows = rows181}182183// runLanguagesTimelineReport is the dispatch entry point called from184// Process() when --timeline is set without --by-author. Opens the repo,185// walks the window with the configured bucket count, and writes the chosen186// format.187func runLanguagesTimelineReport(repoPath string) error {188 observer := newHistoryLanguagesObserver(HistoryBuckets)189 if _, err := runHistory(repoPath, observer); err != nil {190 return err191 }192 out, err := renderLanguagesTimeline(observer)193 if err != nil {194 return err195 }196 if FileOutput == "" {197 fmt.Print(out)198 } else {199 if err := os.WriteFile(FileOutput, []byte(out), 0644); err != nil {200 return err201 }202 fmt.Println("results written to " + FileOutput)203 }204 return nil205}206207func renderLanguagesTimeline(o *historyLanguagesObserver) (string, error) {208 switch strings.ToLower(Format) {209 case "", "tabular", "wide":210 return renderLanguagesTimelineTabular(o), nil211 case "csv":212 return renderLanguagesTimelineCSV(o)213 case "json":214 return renderLanguagesTimelineJSON(o)215 default:216 return "", fmt.Errorf("unsupported --format %q for --timeline (supported: tabular, csv, json)", Format)217 }218}219220// Tabular column format. 20+1+26+1+11+1+8+1+10 = 79.221var tabularShortLanguagesTimelineFormatHead = "%-20s %-26s %11s %8s %10s\n"222223// Wide tabular: same columns, wider sparkline. 20+1+56+1+11+1+8+1+10 = 109.224var tabularWideLanguagesTimelineFormatHead = "%-20s %-56s %11s %8s %10s\n"225226func renderLanguagesTimelineTabular(o *historyLanguagesObserver) string {227 wide := More || strings.EqualFold(Format, "wide")228 brk := tabularBreakFor(wide)229230 var sb strings.Builder231 sb.WriteString(historyHeader("Languages", o.window, wide))232233 p := gmessage.NewPrinter(glanguage.Make(os.Getenv("LANG")))234235 format := tabularShortLanguagesTimelineFormatHead236 cells := languagesTimelineSparkCells237 if wide {238 format = tabularWideLanguagesTimelineFormatHead239 cells = languagesTimelineWideSparkCells240 }241242 _, _ = fmt.Fprintf(&sb, format, "Language", "Trend", "Code", "Share", "Change")243 sb.WriteString(brk)244245 for _, r := range o.rows {246 langCol := unicodeAwareTrim(r.Language, 19)247 langCol = unicodeAwareRightPad(langCol, 20)248 spark := renderLanguagesTrajectorySparkline(r.Trajectory, cells)249 codeStr := formatWithCommas(p, r.CodeNow)250 shareStr := fmt.Sprintf("%6.1f%%", r.SharePercent)251 changeStr := formatCodeDelta(p, r.Change)252 _, _ = fmt.Fprintf(&sb, format, langCol, spark, codeStr, shareStr, changeStr)253 }254255 sb.WriteString(brk)256 return sb.String()257}258259// renderLanguagesTrajectorySparkline downsamples the absolute trajectory to260// a sparkline. Each line is normalised to its own min/max for shape clarity261// (the Share column carries cross-language comparison).262func renderLanguagesTrajectorySparkline(traj []int64, cells int) string {263 if len(traj) == 0 {264 if asciiOutput() {265 return strings.Repeat(".", cells)266 }267 return strings.Repeat("▁", cells)268 }269 values := make([]float64, len(traj))270 for i, v := range traj {271 values[i] = float64(v)272 }273 return renderSparkline(values, cells)274}275276func renderLanguagesTimelineCSV(o *historyLanguagesObserver) (string, error) {277 var sb strings.Builder278 sb.WriteString(formatWindowComment(o.window))279 sb.WriteByte('\n')280 _, _ = fmt.Fprintf(&sb, "# buckets: %d\n", o.bucket.N)281282 w := csv.NewWriter(&sb)283 _ = w.Write([]string{284 "Language", "BucketStart", "Code", "CodeNow", "SharePercent", "Change",285 })286287 for _, r := range o.rows {288 for i, code := range r.Trajectory {289 bucketStart := o.bucket.Start(i).UTC().Format(historyDateLayout)290 _ = w.Write([]string{291 r.Language,292 bucketStart,293 fmt.Sprintf("%d", code),294 fmt.Sprintf("%d", r.CodeNow),295 fmt.Sprintf("%.1f", r.SharePercent),296 fmt.Sprintf("%d", r.Change),297 })298 }299 }300 w.Flush()301 if err := w.Error(); err != nil {302 return "", err303 }304 return sb.String(), nil305}306307type languagesTimelineJSONBucket struct {308 BucketStart string `json:"bucketStart"`309 Code int64 `json:"code"`310}311312type languagesTimelineJSONLang struct {313 Language string `json:"language"`314 CodeNow int64 `json:"codeNow"`315 SharePercent float64 `json:"sharePercent"`316 Change int64 `json:"change"`317 Series []languagesTimelineJSONBucket `json:"series"`318}319320type languagesTimelineJSONWindow struct {321 Depth int `json:"depth"`322 Commits int `json:"commits"`323 From string `json:"from"`324 To string `json:"to"`325}326327type languagesTimelineJSONDoc struct {328 Report string `json:"report"`329 Window languagesTimelineJSONWindow `json:"window"`330 Buckets int `json:"buckets"`331 Languages []languagesTimelineJSONLang `json:"languages"`332}333334func renderLanguagesTimelineJSON(o *historyLanguagesObserver) (string, error) {335 doc := languagesTimelineJSONDoc{336 Report: "languages-timeline",337 Window: languagesTimelineJSONWindow{338 Depth: o.window.Depth,339 Commits: o.window.Commits,340 From: formatWindowDate(o.window.From),341 To: formatWindowDate(o.window.To),342 },343 Buckets: o.bucket.N,344 Languages: make([]languagesTimelineJSONLang, 0, len(o.rows)),345 }346 for _, r := range o.rows {347 jl := languagesTimelineJSONLang{348 Language: r.Language,349 CodeNow: r.CodeNow,350 SharePercent: round1(r.SharePercent),351 Change: r.Change,352 Series: make([]languagesTimelineJSONBucket, 0, len(r.Trajectory)),353 }354 for i, code := range r.Trajectory {355 jl.Series = append(jl.Series, languagesTimelineJSONBucket{356 BucketStart: o.bucket.Start(i).UTC().Format(historyDateLayout),357 Code: code,358 })359 }360 doc.Languages = append(doc.Languages, jl)361 }362 b, err := jsoniter.Marshal(doc)363 if err != nil {364 return "", err365 }366 return string(b), nil367}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.