Your GitHub language bar is lying (how to fix it)

tl;dr; GitHub’s language bar counts every byte in your repo, including generated docs and test suites. A few lines in .gitattributes tell it what to skip. For my project, that took it from “75% HTML” to roughly 62% Rust and 38% Python, which is what the project really is.


You know that colorful bar on every GitHub repo’s page, the one that says what the project is written in? I just learned you can tell it what to count. On one of my projects, it made a big difference.

Why does GitHub think my Rust project is HTML?

My project chameleon-rs is a Rust re-implementation of the Chameleon template engine, shipped as a Python package (I wrote about it here). Most of the engine is Rust, with a Python layer for the public API and framework integrations. Here’s what GitHub said it was:

  • HTML: 75.5%
  • Python: 12.4%
  • Rust: 9.1%
  • JavaScript: 2.8%

HTML? Three quarters of it? There’s barely any hand-written HTML in the whole repo. Yes, it’s a templating engine, but the templates live in the tests and docs.

The documentation site was to blame. I build the docs with Great Docs and commit the output to site/ so it can be served straight from the repo. That’s about 9 MB of generated HTML, JavaScript and CSS, and GitHub counted every byte of it as if I’d written it by hand.

How does GitHub decide what language a repo is?

The bar comes from Linguist, GitHub’s language detection library. Linguist goes over every tracked file, works out its language, and adds up the bytes. More bytes, bigger slice. It already skips some things on its own, like well-known vendor folders, minified files and common generated files. But it has no way to know that your site/ folder is build output.

You tell it with a .gitattributes file at the root of your repo. It’s the same file Git uses for line endings and diff settings, and Linguist reads a few attributes of its own from it:

  • linguist-generated marks build output. It’s left out of the stats, and GitHub collapses its diffs in pull requests too (nice).
  • linguist-vendored marks third-party code you’ve copied in. Also left out of the stats.
  • linguist-documentation marks docs, which are left out as well.
  • linguist-detectable=false just says “don’t count this toward the languages” and means nothing else.

Each line is a path pattern followed by attributes. Think of a .gitignore line with some settings tacked on the end.

Round one: the docs

Two lines took care of the worst of it:

site/** linguist-generated
tests/upstream/** linguist-vendored

The second line covers Chameleon’s own test suite, which I vendor unmodified to measure compatibility. It’s not my code, so it shouldn’t count either.

After pushing, the bar read 55.9% Python, 43.3% Rust. Much better, but still not right.

Round two: the tests

The engine is mostly Rust, so why was Python winning? I measured the bytes:

Where Size
Python package 420 KB
Python tests 685 KB
Rust, everything 950 KB

The Python tests were bigger than the Python package! It’s nice to have a thorough test suite. But it was making a Rust engine look like a Python project.

The tests are coming out too. To keep things fair, I excluded the Rust tests and benchmarks along with the Python tests, otherwise I’d just be skewing it the other way. Here’s the final file:

# GitHub's language statistics (Linguist) skip these: the Great Docs build output (`make docs`)
# is generated, and Chameleon's test suite is vendored unmodified.
site/** linguist-generated
tests/upstream/** linguist-vendored

# Tests and benchmarks aren't the product: the Python tests outweigh the Python package, so
# counting them makes a mostly-Rust engine read as mostly Python. The Rust ones are left out
# too, to measure both languages alike (inline `#[cfg(test)]` modules still count).
tests/** linguist-detectable=false
crates/*/tests/** linguist-detectable=false
crates/*/benches/** linguist-detectable=false
crates/chameleon-core/src/tests.rs linguist-detectable=false

You can check your patterns before pushing with git check-attr:

git check-attr linguist-generated linguist-detectable -- site/index.html tests/test_api.py

Where did it end up?

The bar should land around 62% Rust and 38% Python. That matches what chameleon-rs is: a Rust engine with a Python API on top. GitHub recalculates a few minutes after you push to the default branch.

That bar is one of the first things people see when they land on your project. If yours is off because of a docs build, vendored code, or a big test suite, it’s a few lines of .gitattributes to fix. I’m off to do the same for some of my other projects now.