processor/history_languages.go GO 368 lines View on github.com → Search inside
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}

Code quality findings 12

Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(&sb, format, "Language", "Trend", "Code", "Share", "Change")
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(&sb, format, langCol, spark, codeStr, shareStr, changeStr)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_, _ = fmt.Fprintf(&sb, "# buckets: %d\n", o.bucket.N)
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_ = w.Write([]string{
Blank identifier discarding results; verify intentional ignoring of return values
warning correctness blank-identifier-discard
_ = w.Write([]string{
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
o.events = append(o.events, languagesTimelineEvent{
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for i, d := range series {
Unstructured output; use a structured logging library (e.g., slog, zap, zerolog, logrus)
info correctness fmt-println
fmt.Println("results written to " + FileOutput)
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for i, v := range traj {
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for i, code := range r.Trajectory {
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for i, code := range r.Trajectory {
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
jl.Series = append(jl.Series, languagesTimelineJSONBucket{

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.