Languages

One Language Per Change

Every scored change has one primary language: whichever language made up the biggest part of the diff. Both language features run off that single label, the filter on the dashboard and activity pages and the Score by Language chart.

A PR that touches mostly Ruby with a bit of YAML counts as Ruby. It does not count partly as each. So every change is counted once wherever you look at it, and no language total adds up to more work than your team actually did.

How the Primary Language Is Chosen

Four rules decide the label:

  1. Only files that counted toward the score get a vote. Anything your exclusion filters removed contributed nothing to the score, so it has no say in what the score is about.
  2. Generated manifests never decide the language. package-lock.json, yarn.lock, Cargo.lock, go.sum, Gemfile.lock and their equivalents are always skipped when we pick the label. This is separate from your exclusion patterns. If your organization scores lockfile diffs, those lines still count toward the score. They just never label it, because a dependency bump is not "a JSON change."
  3. The language with the most effective lines wins. File count does not matter. What matters is effective lines, the same measure behind the Effort Scale Factor. One large Rust file plus four one-line YAML edits is Rust.
  4. Ties break alphabetically. An exact 50/50 split resolves the same way every time, so re-scoring a PR can never quietly move it from one language to another.

One caveat on older work. Where a change was classified after the fact and its per-file line counts were gone, the label falls back to a file-count majority, and every file weighs the same. That flips the example above: one large Rust file and four small YAML files becomes YAML, because YAML has more files. This affects historical labels only, and only on mixed changes where the biggest language is not also the most-edited one.

GitVelocity recognizes languages by file path. Mainstream application languages are covered, along with config, markup, and infrastructure formats. Related extensions are grouped where they represent one body of work: .ts and .tsx are both TypeScript, and .js, .jsx, .mjs and .cjs are all JavaScript. Single-file-component formats keep their own identity. .vue is Vue and .svelte is Svelte, because those files are not TypeScript or JavaScript.

Filtering by Language

The language picker appears in two places. On the dashboard it sits beside the Team Performance chart and narrows contributor trends to one language, so you can ask how your Go work has trended this quarter instead of how everything has. On Activity it joins the date, repository, score and author filters above the pull request list.

Language combines with every other filter, so you can look at one contributor's Python work in one repository above a score threshold. The selection lives in the page URL, so a filtered view is a link you can paste to a colleague or bookmark, and it survives a refresh. CSV exports respect it too. An export taken from a filtered view has exactly the rows you were looking at.

The picker also shows you what you are about to get. Pick a language and it tells you how many changes match, and it warns you when there are too few to compare reliably. A two-PR sample should not look like a trend.

Score by Language

At the bottom of the dashboard, the Score by Language chart compares your five busiest languages. It opens on Total: the summed score per month, stacked so the bands show what each language contributed. Switch to Per change for the average score behind one change in that language.

The two views cover different windows, on purpose. Total shows the last eleven complete months and leaves out the month you are in, because three days of work summed and plotted beside eleven whole months reads as your output falling off a cliff. Per change keeps the current month, since an average over a partial month is still a real average. So if you are looking for this month and cannot find it, switch to Per change.

The line under the chart title tells you which reading you are on and names what that view leaves out. It has room for one caveat, so when your organization writes more than five languages it spends it on that, and the unclassified remainder goes unmentioned. Both hold either way: neither view covers languages outside the top five, and neither covers changes we could not put a language to.

Bigger changes score higher by design

A score is the raw complexity rating multiplied by the Effort Scale Factor, and the ESF is a size multiplier. So a language's average score mostly tracks how big its changes tend to be. It is not a measure of how good the work is. Config languages usually sit low on this chart because config diffs are small, and small changes score low on purpose.

"Our YAML averages 7" says something about diff size. It says nothing about the people writing YAML.

So the Per change view shows every point's size too. Hover one and you get the number of changes behind it and the average effective lines each one carried. A low average over small diffs is a config language behaving exactly as expected. A low average over large diffs is worth a look. The Total view leaves the size out on purpose: an average size explains a mean, but a sum has already multiplied size and volume together.

Work With No Language

Some changes have no primary language at all, and we treat that as unknown. A change is unclassified when every file it touched was excluded from scoring, was a generated manifest, or had no recognizable file type.

Unclassified changes are hidden when you apply a language filter. They do not get swept into an "Other" bucket, because "Other" would imply we know what they are. When the unclassified share gets big enough to distort what you are seeing, the picker says so and gives you the count.

Two consequences worth knowing:

  • Language totals can be smaller than your overall totals. The gap is the unclassified remainder. We show it instead of hiding it.
  • Unscored pull requests have no language. They have no score, so there is nothing to classify. A language filter combined with the unscored view returns nothing.

Frequently Asked Questions

Does the language filter change any scores?

No. Language is a label on work that was already scored. Filtering changes which rows you see and nothing else. Turning the filter on, off, or to a different language never alters a score.

Why is one of my languages missing from the picker?

The picker lists languages your organization has actually scored work in. A language shows up once at least one merged change has it as its primary language. If you only use a language for small supporting edits, it may never be the biggest part of a diff, so it may never appear.

Why did a mixed pull request get filed under one language?

By design. The label is whichever language made up the biggest part of the diff, so every change is counted once across all language views. If the label looks wrong on an older change, see the caveat on retroactive classification above. Historical work can be labelled by file count instead of by size.

My highest-scoring language is not the one my team considers hardest. Why?

Usually because that language's changes are bigger, not harder. Score scales with size through the Effort Scale Factor, so the language your team makes big structural changes in will outrank the language they make careful, surgical changes in. Hover the points and compare average effective lines. That usually explains it.