Skip to main content

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-by trailers 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. --all re-reads but does not prune.
  • Discovery stops at the first .git it 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 repos shows 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 --date is 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

gitfoot-1.1.0.tar.gz (76.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gitfoot-1.1.0-py3-none-any.whl (47.8 kB view details)

Uploaded Python 3

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

Hashes for gitfoot-1.1.0.tar.gz
Algorithm Hash digest
SHA256 b14c9b4854bd25d36a1fedcd19d1505df1a4e90ca63c77eb7f6ff4007106322c
MD5 1fda8596c560e90a1a69bb0f5c50b3ad
BLAKE2b-256 07e997c4e7fcaebb35806e346f1a83eeb17ac39eebf6c06d8244876ce4f07142

See more details on using hashes here.

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

Hashes for gitfoot-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b2aec7f3860e8df888e813fe932ac525a464309041efaee53ee52c261a0f9c29
MD5 f6932f6851dd23e05bc5d676465cec53
BLAKE2b-256 b2720ff6d85d9f5b125d3a0bfea0e3347b566311ccb983b546f0d039e34fa886

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page