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
uv tool install gitfoot # keep it on PATH, isolated
pipx install gitfoot # the same thing, if you use pipx
pip install gitfoot
To try it once without installing anything:
uvx gitfoot init
uvx runs from a throwaway environment and puts nothing on your PATH, so
every command stays uvx gitfoot ... until you install it properly. GitFoot
notices which of the two you are using and prints commands you can actually
run.
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 looks in the usual places (~/dev, ~/code, ~/src, ~/projects,
~/work, ~/repos, ~/git, ~/Documents/GitHub), counts the repositories
in each, and asks before scanning any of them. It never picks on its own:
Found these project directories:
1 ~/dev 10 repos
2 ~/work 3 repos
3 ~/Documents/GitHub 0 repos
scan these? [Enter for all, numbers, or paths] 1 2 ~/clients
Enter takes everything found, numbers take a subset, and anything that is not a number is read as a directory to add. Mix them freely. A path that does not exist is refused there and then rather than stored, because a typo would otherwise surface much later as a dashboard of zeros.
Add -y to accept the offered directories without asking, --no-sync to
write config without reading anything yet, and --root DIR / --email ADDR
(both repeatable) to skip the interactive part entirely.
Changing what you track
Run init again. Once you are set up it stops offering a fresh start and
shows what you have, so the one command anybody would try is the one that
works:
Scanning these:
1 ~/dev 10 repos ✓
2 ~/work 3 repos ✓
Also found:
3 ~/code 5 repos
change what? [numbers to toggle, or type to add, Enter to keep] 3 ~/clients
Numbers toggle: a ticked entry off, an unticked one on. Anything that is not a number is added. Enter changes nothing. Identities work the same way in the step that follows, listing who currently counts as you and who else appears in your history.
Turning something off forgets what it contributed. Dropping a directory drops its repositories and their commits; dropping an address drops the commits attributed to it. Otherwise history nobody claims would go on padding the streak, which is the sort of quiet wrongness this tool exists to avoid.
The flags still work if you prefer them, and are the only option in a script:
gitfoot repos --add ~/clients
gitfoot repos --remove ~/old
gitfoot config --add-email me@work.example
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.1.0.tar.gz.
File metadata
- Download URL: gitfoot-1.1.0.tar.gz
- Upload date:
- Size: 76.4 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 |
b14c9b4854bd25d36a1fedcd19d1505df1a4e90ca63c77eb7f6ff4007106322c
|
|
| MD5 |
1fda8596c560e90a1a69bb0f5c50b3ad
|
|
| BLAKE2b-256 |
07e997c4e7fcaebb35806e346f1a83eeb17ac39eebf6c06d8244876ce4f07142
|
File details
Details for the file gitfoot-1.1.0-py3-none-any.whl.
File metadata
- Download URL: gitfoot-1.1.0-py3-none-any.whl
- Upload date:
- Size: 47.8 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 |
b2aec7f3860e8df888e813fe932ac525a464309041efaee53ee52c261a0f9c29
|
|
| MD5 |
f6932f6851dd23e05bc5d676465cec53
|
|
| BLAKE2b-256 |
b2720ff6d85d9f5b125d3a0bfea0e3347b566311ccb983b546f0d039e34fa886
|