gitfoot
A local git activity tracker. Run it, see whether you showed up.
Commit count is not productivity
It isn't, and gitfoot doesn't claim it is. A day of hard thinking can produce one commit. A day of nothing can produce twelve. Anyone who has shipped software knows this.
What a commit history does record is attendance. Days you opened the editor, days you didn't, and how long the current run is. That is the whole claim. gitfoot is a mirror, not a score. There is no leaderboard, no ranking, no badge, and nothing to optimise against.
If you want it to say you were productive, you will have to lie to it.
What it looks like
Today 4 commits · 3 repos · +177 −108
Streak 3 days · longest 8
This week 12 commits · 3 of 7 days · +786 −262
api 3 · notes 3 · webapp 3
Sep Oct Nov Dec Jan Feb Mar Apr May Jun Jul Aug
Mon ▒··▓·▒█▓▓·▒▓·▒██▒█▓▓█▒░█···▒░·░▓▓·███▒▓▒░·▒█·▒▒▓█▒·▓
▓▓▒░▓·░▒▓▓▓··▒▒▒░░·▒▒▒▓▒▒█·▓░·░▒▓█·██▓·░▓▓░▒▓░·░·▓▒░
Wed ▓▒··░▓·█·▓▒░·██▒·▓░▒·▓▒▒▒░░░░·▒···█▓█·▓░░·░░▒▒·█░·▓▒
█▓▓·▓·▒·▓░█▓░▓▒·▒░▒▒·▒░▒·▒░▓·▒░▓█▒▒▒░·█▒···▒▒··▓▓▒░
Fri ▓▒▒█·▓·▓▒░··░·░▒▒··▒▒··▓·░░░░█▓▒·▓▓▓░▒█░▒░·▒·░·▓▓▒▓
·····░··░▒░·░░···░····▒·░··········▒░···▒·░···░░·░·
··░░···░········▒·░▒░·····▒··▓··········░····░·░···
Less ·░▒▓█ More 874 commits · 6 repos · last 52 weeks
Colour carries the same information as the shading, so it survives NO_COLOR,
a light background, and a pipe. The bottom two rows are the weekend.
Install
uvx gitfoot # run it without installing
pipx install gitfoot # keep it on PATH, isolated
pip install gitfoot
Python 3.11+ and git. Zero runtime dependencies: no click, no rich, no requests, nothing to resolve and nothing to break on upgrade. The install is one pure-Python package and the standard library.
Use
gitfoot init # pick the directories to scan, confirm which commits are yours
gitfoot sync # read new commits
gitfoot # the dashboard
init runs a full-history sync when it finishes, so the first dashboard has
everything in it. After that, sync is the one you repeat.
init offers the usual project directories (~/dev, ~/code, ~/src,
~/projects, ~/work, ~/repos, ~/git, ~/Documents/GitHub) and asks
before scanning any of them. It never picks on its own. Add -y to skip the
prompt, --no-sync to write config without reading anything yet, and
--root DIR / --email ADDR (both repeatable) to skip the interactive part
entirely.
sync is incremental: each repository is read from where it left off, with a
week of overlap to absorb rebases and clock skew. Re-running is always safe,
because commits are keyed by SHA. To widen the range:
gitfoot sync --days 90 # re-read the last 90 days
gitfoot sync --since 2024-01-01 # re-read from a date
gitfoot sync --all # re-read complete history
gitfoot sync -q # only report errors, for cron and shell hooks
The dashboard takes --weeks N for the size of the activity calendar. The
default is 52, a year, which fits in an 80-column terminal:
gitfoot --weeks 13
Other commands:
gitfoot repos # tracked repositories and scan roots
gitfoot repos --add ~/clients # add a scan root
gitfoot repos --remove ~/old # stop scanning a root, and forget its history
gitfoot config # current settings and where they live
gitfoot config --path # just the config file path
gitfoot config --add-email me@work.example
gitfoot doctor # git, config, identity, database, history
Global flags, accepted before or after the subcommand: --json, --no-color,
--db PATH, --version.
Privacy
There is no network code in this repository. No telemetry, no update check, no account, no API key. The only imports are stdlib, and the check takes ten seconds:
grep -rnE '^\s*(import|from) ' gitfoot/ | grep -E 'urllib|http|socket|ssl|requests'
That comes back empty. Verify it rather than take my word. The unanchored
pattern matters: an import hidden inside a function would slip past a ^import
grep, which is exactly the sort of check that looks like proof and isn't.
All the state is two files, both under standard XDG paths:
| What | Where |
|---|---|
| database | $XDG_DATA_HOME/gitfoot/gitfoot.db, default ~/.local/share/gitfoot/gitfoot.db |
| config | $XDG_CONFIG_HOME/gitfoot/config.toml, default ~/.config/gitfoot/config.toml |
GITFOOT_HOME=/path relocates both, which is how you keep the whole thing
inside a dotfiles repo or throw it away after trying it. --db PATH points a
single invocation at a different database. Deleting the two files above is a
complete uninstall of your data.
gitfoot reads commit SHAs, dates, author emails and diff line counts. It does not read commit messages, file names, file contents, or branch names.
How it decides what's yours
Only commits whose author email is in your configured list.
Most people have committed under more than one address over the years: a work
one, a personal one, a GitHub noreply. So gitfoot init counts the author
emails across the repositories it found, shows them with commit counts, marks
the ones git already knows are yours, and asks about the rest:
Scanned 2 repos
Counted as you
you@example.com
Also found
1 colleague@work.example 5 commits
2 you@work.example 3 commits
3 root@localhost 1 commit
also count as you? [numbers, or Enter for none] 2
Offered, never assumed. That distinction is the whole point: 0.x harvested every author from the last 100 commits of each repository and treated them all as you, so on a shared repository your colleagues' work quietly became yours.
Non-interactive when you need it: --email ADDR is repeatable, and -y takes
the git-configured identities without asking.
The list grows later with:
gitfoot config --add-email me@work.example
gitfoot sync --all
An empty email list matches nothing rather than everything. Two more rules worth knowing up front:
- Merge commits are excluded. They record an integration, not a day's work.
- All refs are scanned, not just the checked-out branch, so work sitting on a feature branch still counts. Duplicates collapse on SHA.
Repositories are identified by normalised remote URL, so two clones of the same project on one machine count as one repository, not two.
JSON
Every command takes --json and writes it to stdout. The two exceptions are
the flags that already do one thing: config --path prints a path, and
repos --add / --remove print a confirmation line.
gitfoot --json | jq '.streak.current'
gitfoot --json | jq -r '.days | to_entries[] | select(.value > 0) | "\(.key)\t\(.value)"'
gitfoot sync --json | jq '.commits_new'
gitfoot repos --json | jq -r '.repositories[] | "\(.commits)\t\(.name)"' | sort -rn | head
gitfoot doctor --json | jq -e 'all(.checks[]; .ok)'
Useful in a shell prompt:
printf 'streak: %s\n' "$(gitfoot --json | jq -r '.streak.current')"
Progress output goes to stderr, so pipes stay clean without -q.
Limitations
Honest list, not a roadmap.
- Squashed and rebased history reads as one day. If your team squashes pull requests, the merge date wins and the days you actually worked vanish. Nothing local can recover them.
- Co-authored commits count once, for the author.
Co-authored-bytrailers are not parsed, so pairing shows up only in the driver's history. - Rewritten history leaves orphans. Commits recorded before a rebase keep
their rows, because gitfoot never deletes.
--allre-reads but does not prune. - Discovery stops at the first
.gitit finds. Submodules and vendored checkouts inside a project are not counted separately. Nested independent repositories are invisible. - Discovery is a list, not a heuristic. Scan depth (6 levels below a root)
and the skipped directory names are both in
config.toml, and a repository outside those rules is simply not seen.gitfoot reposshows what was found, which is the fastest way to notice something missing. - Author dates, in whatever timezone the machine had at commit time. Travel
or a badly set clock will put a commit on the wrong day, and
git commit --dateis trusted as given. - Empty and unreachable repositories are skipped quietly; git failures are reported per repository at the end of a sync instead of aborting it.
Development
pip install -e '.[dev]'
pytest
The modules are small and single-purpose: gitscan (find repos, read commits),
store (SQLite), stats (pure date arithmetic), render and term (output),
cli (argparse). Patches that keep the dependency list empty are welcome;
docs/architecture.md explains why the data model looks
the way it does before you change it.
More answers, including the awkward ones, in docs/faq.md.
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gitfoot-1.0.0.tar.gz.
File metadata
- Download URL: gitfoot-1.0.0.tar.gz
- Upload date:
- Size: 67.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a30f6325010a609fc1dc2e4a7e16bc6ae2dd855d6921ed0279aec46f816ccef
|
|
| MD5 |
cc225ce4274ff93066cd240b4bc068d6
|
|
| BLAKE2b-256 |
62e2082cb5bd70ef265541336d1768f772dca452dc47c826a91df3d94411a255
|
File details
Details for the file gitfoot-1.0.0-py3-none-any.whl.
File metadata
- Download URL: gitfoot-1.0.0-py3-none-any.whl
- Upload date:
- Size: 43.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8325c7b724c44e49225776566940530ede7c7fcf3ed1b8c01944e559d7422ba5
|
|
| MD5 |
d725240cef9bcb6d2c8b17d6dc2509bd
|
|
| BLAKE2b-256 |
b8f95485aee736cc6323114d58937f7eaf5ebd4bcfe85c5de1068f905f264f08
|