Finding Uncommitted Changes in Your Git Repos
Like most people, I’ve got a ton of Git repos sitting in a folder at
~/src. Turns out I have terrible habits and I tend to leave
a lot of changes uncommitted, just sitting in my local repo. I want
these repos to have remotes that are up to date, and that means I need
to commit the changes, but checking each one by hand is way too tedious.
I’d like an automation that tells me which repos have dirty working
copies.
Here’s my first pass at solving this problem1. It’s a short Python program that searches the working directory
recursively for Git repos, and when it finds them it checks if they have
modified files that haven’t been committed (using
git status -s). If they do, it prints the directory
location and what files are still ‘dirty’.
Here’s the source code in full, with notes below:
#!/usr/bin/env python3
import subprocess
from pathlib import Path
def git_dirs(dir: Path):
for p in sorted([p for p in dir.iterdir() if p.is_dir()]):
if (p / ".git").is_dir():
yield p
else:
yield from git_dirs(p)
def git_status(repo: Path) -> list[str]:
return (
subprocess.run(
["git", "status", "-s"],
cwd=repo,
capture_output=True,
)
.stdout.decode()
.split("\n")
)
def main():
num_dirty_repos = 0
for p in git_dirs(Path.cwd()):
status = git_status(p)
if not status or status == [""]:
continue
num_dirty_repos += 1
print(f"\033[1m{p.relative_to(Path.cwd())}\033[0m")
for line in status:
print(line)
print(f"{num_dirty_repos} repos have uncommitted changes")
if __name__ == "__main__":
main()
The git_dirs function finds Git repos in the current
directory, or its descendants, and yields them. Python’s
yield from makes this kind of recursive generator super
easy to write. Note that I used Path.iterdir() with manual
recursion instead of .glob() because this way I can skip
scanning inside Git repos, which saves a lot of time.
git_status is just a single expression that runs
git status -s (the -s stands for “short”) in
the provided directory and reports the results. I’m not doing any kind
of parallelism even though that might speed things up a lot — I’ll worry
about speed later, this is just a rough cut. I chose to return a list of
lines instead of one big string because I’m thinking about adding more
features to this like displaying the number of changed files in the
future, and that might come in handy.
With these tools, it’s pretty simple to put the final script together in
main. Iterate over the Git repos, skip the ones with
nothing reported from git_status, and print the results
with a nice bold header per repo.
I’ve symlinked the program to ~/bin/dirty so I can summon
it at any time by running dirty at the command line. Here’s
what it looks like when I run it in ~/src:
> dirty | head -n 25
archive/hackathon-2023
M .DS_Store
M backend/__pycache__/application.cpython-311.pyc
?? backend/users/sdflskdjfl.json
archive/rpi-controller
M server/main.py
M server/run-server-command.sh
?? package-lock.json
c-to-zig
M build.zig
D compute.c
A compute.zig
comp/3030
M a1/q1b.typ
M a4/q1.typ
M a4/q5.typ
M definitions.typ
?? a2/appeal.typ
?? a4/q6.typ
comp/3060
M unitD/D1/D1a.js
> dirty | wc -l
264
Oof. I should really get started cleaning this up.
After the first draft of this post, I added a simple counter so that I can print the number of dirty repos at the end (the code above is the new version with the counter). This counter serves the important function of making me feel bad, and since then I’ve reduced the number of dirty repos from 35 to 24. That’s progress!
In the future I’d like to add more summary statistics, like the number
of clean vs. dirty repos or how many changes are hanging around in each
repo, but this works pretty well for now. Rest assured that if I do add
features, I’ll write them up and post them here. I wonder if there’s a
better way to get
information out of Git than just calling subprocess.run…
1: Well, the true first pass was this:
for dir in (ls); echo $dir; pushd $dir; git status; popd; end