153 comments

[ 3.2 ms ] story [ 124 ms ] thread
These are some helpful heuristics, thanks.

This list is also one of many arguments for maintaining good Git discipline.

Jujutsu equivalents, if anyone is curious:

What Changes the Most

    jj log --no-graph -r 'ancestors(trunk()) & committer_date(after:"1 year ago")' \
      -T 'self.diff().files().map(|f| f.path() ++ "\n").join("")' \
      | sort | uniq -c | sort -nr | head -20
Who Built This

    jj log --no-graph -r 'ancestors(trunk()) & ~merges()' \
      -T 'self.author().name() ++ "\n"' \
      | sort | uniq -c | sort -nr
Where Do Bugs Cluster

    jj log --no-graph -r 'ancestors(trunk()) & description(regex:"(?i)fix|bug|broken")' \
      -T 'self.diff().files().map(|f| f.path() ++ "\n").join("")' \
      | sort | uniq -c | sort -nr | head -20
Is This Project Accelerating or Dying

    jj log --no-graph -r 'ancestors(trunk())' \
      -T 'self.committer().timestamp().format("%Y-%m") ++ "\n"' \
      | sort | uniq -c
How Often Is the Team Firefighting

    jj log --no-graph \
      -r 'ancestors(trunk()) & committer_date(after:"1 year ago") & description(regex:"(?i)revert|hotfix|emergency|rollback")'
Much more verbose, closer to programming than shell scripting. But less flags to remember.
a project isn’t dying because of no commits. Rather it’s stable

I often feel I need to setup bots to make superfluous commits just to make it look like my useful and stable repos are “active”

One example (not mine) a a qr-code generator library. Hasn’t been updated in 10 years. It’s perfect as is. It just provides the size and the bits. You convert those bits to any representation you want. It has no need to be updated

I don't want to program git, I want to get stuff done so I would reject using that tool and do what the article author did running tried and true pipeable Linux/UNIX commands. It's also the same reason why I dislike Gradle and use Maven, I don't want to program my build I want to define and run my build.
Saw all the replies crying over how verbose these are, clicked through to TFA expecting to see simpler commands. Nope, they're basically the same thing, just slightly shorter. I would never memorize either the jj or git versions if I planned to use them regularly; I'd make aliases.
crying over? Let's not be confrontational. I could say you're "crying" with this comment, but is that a good thing to say?
Cool! Thanks :) Jujutsu is such a nice tool. I don't understand why everything has to be so divisive nowadays. Enjoy the tools you like.
> The 20 most-changed files in the last year. The file at the top is almost always the one people warn me about. “Oh yeah, that file. Everyone’s afraid to touch it.”

The most changed file is the one people are afraid of touching?

Plotting Churn against Complexity is far more useful than merely churn.

It shows places that are problematic much better. High churn, low complexity: fine. Its recognized and optimizef that this is worked on a lot (e.g. some mapping file, a dsl, business rules etc). Low churn high complexity: fine too. Its a mess, but no-one has to be there. But both? Thats probably where most bugs originate, where PRs block, where test coverage is poor and where everyone knows time is needed to refactor.

In fact, quite often I found that a teams' call "to rewrite the app from scratch" was really about those few high-churn-high-complexity modules, files or classes.

Complexity is a deep topic, but even simple checks like how nested smt is, or how many statements can do.

pom.xml and package.json came up on couple of separate projects I ran the commands on. Which makes sense because the versions get bumped rather frequently. I guess context matters, as usual.
Maybe it's a start to find conflict-prone regions ?

otherwise you're right, it could be a long linear list of appends where people are happy to contribute.

That's the one that always broke something down the line leading to a stream of reverts and retries?
> If the team squashes every PR into a single commit, this output reflects who merged, not who wrote.

Squash-merge workflows are stupid (you lose information without gaining anything in return as it was easily filterable at retrieval anyway) and only useful as a workaround for people not knowing how to use git, but git stores the author and committer names separately, so it doesn't matter who merged, but rather whether the squashed patchset consisted of commits with multiple authors (and even then you could store it with Co-authored-by trailers, but that's harder to use in such oneliners).

Calling squash stupid sounds like a case of Dunning-Kruger.

If you've worked on a large team without squashing and without increasing frustration I'd be greatly interested to hear about it.

> The 20 most-changed files in the last year. The file at the top is almost always the one people warn me about.

What a weird check and assumption.

I mean, surely most of the "20 most-changed files" will be README and docs, plus language-specific lock-files etc. ?

So if you're not accounting for those in your git/jj syntax you're going to end up with an awful lot of false-positive noise.

Some nice ideas but the regexes should include word boundaries. For example:

git log -i -E --grep="\b(fix|fixed|fixes|bug|broken)\b" --name-only --format='' | sort | uniq -c | sort -nr | head -20

I have a project with a large package named "debugger". The presence of "bug" within "debugger" causes the original command to go crazy.

This needs a small tweak to work on macOS, where git uses the POSIX version of grep (which doesn't support `\b`). You need to use the Perl Regexp option by switching -E with -P:

git log -i -P --grep="\b(fix|fixed|fixes|bug|broken)\b" --name-only --format='' | sort | uniq -c | gsort -nr | head -20

Similarly, we have a technical concept called "rollback" that is unrelated to a reverted commit.
Word boundaries are one way to address that, but they require you to list all the inflections (and you missed “fixing”). Another way is to say (?<!de)bug.
Great tips, added to notes.txt for future use ..

Another one I do, is:

    $alias gss='git for-each-ref --sort=-committerdate'

    $gss

    ce652ca83817e83f6041f7e5cd177f2d023a5489 commit refs/heads/project-feature-development
    ce652ca83817e83f6041f7e5cd177f2d023a5489 commit refs/remotes/origin/project-feature-development
    1ef272ea1d3552b59c3d22478afa9819d90dfb39 commit refs/remotes/origin/feature/feature-removal-from-good-state
    c30b4c67298a5fa944d0b387119c1e5ddaf551f1 commit refs/remotes/origin/feature/feature-removal
    eda340eb2c9e75eeb650b5a8850b1879b6b1f704 commit refs/remotes/origin/HEAD
    eda340eb2c9e75eeb650b5a8850b1879b6b1f704 commit refs/remotes/origin/main
    3f874b24fd49c1011e6866c8ec0f259991a24c94 commit refs/heads/project-bugfix-emergency
    ...

This way I can see right away which branches are 'ahead' of the pack, what 'the pack' looks like, and what is up and coming for future reference ... in fact I use the 'gss' alias to find out whats going on, regularly, i.e. "git fetch --all && gss" - doing this regularly, and even historically logging it to a file on login, helps see activity in the repo without too much digging. I just watch the hashes.
I have a summary alias that kind of does similar things

  # summary: print a helpful summary of some typical metrics
  summary = "!f() { \
    printf \"Summary of this branch...\n\"; \
    printf \"%s\n\" $(git rev-parse --abbrev-ref HEAD); \
    printf \"%s first commit timestamp\n\" $(git log --date-order --format=%cI | tail -1); \
    printf \"%s latest commit timestamp\n\" $(git log -1 --date-order --format=%cI); \
    printf \"%d commit count\n\" $(git rev-list --count HEAD); \
    printf \"%d date count\n\" $(git log --format=oneline --format=\"%ad\" --date=format:\"%Y-%m-%d\" | awk '{a[$0]=1}END{for(i in a){n++;} print n}'); \
    printf \"%d tag count\n\" $(git tag | wc -l); \
    printf \"%d author count\n\" $(git log --format=oneline --format=\"%aE\" | awk '{a[$0]=1}END{for(i in a){n++;} print n}'); \
    printf \"%d committer count\n\" $(git log --format=oneline --format=\"%cE\" | awk '{a[$0]=1}END{for(i in a){n++;} print n}'); \
    printf \"%d local branch count\n\" $(git branch | grep -v \" -> \" | wc -l); \
    printf \"%d remote branch count\n\" $(git branch -r | grep -v \" -> \" | wc -l); \
    printf \"\nSummary of this directory...\n\"; \
    printf \"%s\n\" $(pwd); \
    printf \"%d file count via git ls-files\n\" $(git ls-files | wc -l); \
    printf \"%d file count via find command\n\" $(find . | wc -l); \
    printf \"%d disk usage\n\" $(du -s | awk '{print $1}'); \
    printf \"\nMost-active authors, with commit count and %%...\n\"; git log-of-count-and-email | head -7; \
    printf \"\nMost-active dates, with commit count and %%...\n\"; git log-of-count-and-day | head -7; \
    printf \"\nMost-active files, with churn count\n\"; git churn | head -7; \
  }; f"
EDIT: props to https://github.com/GitAlias/gitalias
I was going to say, the OP's assertion that "they" are typing all these commands out by hand each time without an alias is just one of many tells that this post is AI slop. Nobody that proficient with shell commands should be typing any of those by hand more than once or twice without aliasing for reuse.
(comment deleted)
Rather than using an LLM to write fluffy paragraphs explaining what each command does and what it tells them, the author should have shown their output (truncated if necessary)
Just looking at how often a file changes without knowing how big the file is seems a bit silly. Surely it should be changes/line or something?
Trusting the messages to contain specific keywords seems optimistic. I don't think I used "emergency" or "hotfix" ever. "Revert" is some times automatically created by some tools (E.g. un-merging a PR).
We use branch names like `hotfix/some-hotfix-name`, don't think I've seen many commits mention hotfix directly.
(comment deleted)
> One caveat: squash-merge workflows compress authorship. If the team squashes every PR into a single commit, this output reflects who merged, not who wrote. Worth asking about the merge strategy before drawing conclusions.

In my experience, when the team doesn't squash, this will reflect the messiest members of the team.

The top committer on the repository I maintain has 8x more commits than the second one. They were fired before I joined and nobody even remembers what they did. Git itself says: not much, just changing the same few files over and over.

Of course if nobody is making a mess in their own commits, this is not an issue. But if they are, squash can be quite more truthful.

I was curious what information I could glean from these for some popular repos. Caveat: I'm primarily an low-level embedded developer so I don't interface with large open source projects at the source level very often (other than occasionally the linux kernel). I chose some projects at random that I use.

*Mainline linux*

Most changed files: pretty much what I expected for 1 and 2... the "cutting edge" of Linux development over other OSes -- bpf and containers. The bpf verifier and AMD GPU driver might get a boost in this list due to sheer LoCs in those files (26K and 14K respectively). An intel equivalent of amdgpu_dm is #21 in the list (drivers/gpu/drm/i915/display/intel_display.c) and nvidia is nowhere to be seen (presumably due to out-of-tree modules/blobs?).

    186 kernel/bpf/verifier.c
    174 fs/namespace.c
    162 drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
    161 kernel/sched/ext.c
    159 fs/f2fs/f2fs.h
Bus factor: obviously none. The top 4

    10399 Christoph Hellwig -> I only know his name because of drama last year regarding rust bindings to DMA subsystem
     8481 Mauro Carvalho Chehab -> I also know his name from the classic "Mauro, shut the fuck up!" Linus rant
     8413 Takashi Iwai -> Listed as maintainer for sound subsystem, I think he manages ALSA
     8072 Al Viro -> His name is all over bunch of filesystem code
Buggy files: Intel comes out on top of GPU drivers this time (twice). Along with KVM for x86(64), the main allocator, and BTRFS.

    1477 drivers/gpu/drm/i915/intel_display.c
    1406 MAINTAINERS
    1390 sound/pci/hda/patch_realtek.c
    1102 drivers/gpu/drm/i915/i915_drv.h
     943 arch/x86/kvm/x86.c
     928 mm/page_alloc.c
     871 drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
     862 drivers/gpu/drm/i915/i915_reg.h
     840 fs/btrfs/inode.c
*GCC*

Most changed files: IR autovectorization code, riscv heuristics tables, and C++ template handling (pt.c is "paramaterized types").

    152 gcc/tree-vect-stmts.cc
    145 gcc/config/riscv/riscv.cc
    131 gcc/tree-vect-loop.cc
    116 gcc/cp/pt.cc
Buggy files: DWARF debuginfo generation, x86 heuristics tables, RS6000(?!) heuristic tables. I had to look up RS6000, it's an IBM instruction set from the 90s lol. cp-tree.h is an interesting file, it seems be the main C(++) AST datastructures.

   1017 gcc/dwarf2out.c
    885 gcc/config/i386/i386.c
    796 gcc/cp/cp-tree.h
    740 gcc/config/rs6000/rs6000.c
    720 gcc/cp/pt.c
*xfwm4* Most changed files: the list is dominated by *.po localizations. I filtered these out. Even after this, I discovered there is very little active development in the last few years. If I extend to 4 years ago, I get: 1. src/client.c - Realizing this project is too "small" to glean much from this. client.c is just the core X client management code. Makes sense. 2. src/placement.c - Other core window management code.

This has not told me much other than where most of the functionality of this project lies.

Bus factor: Pretty huge. Not really an issue in this case due to lack of development I guess.

    3298  Olivier Fourdan
     530  Anonymous
     319  Xfce Bot
     121  Jasper Huijsmans

Files with bug commits: Very similar distribution to most changed files. Not enough datapoints in this one to draw any big conclusions.

I think these massive open projects (excl xfwm) are generally pretty consistent code quality across the heavily trodden areas because of the amount of manpower available to refactor the pain points. I've yet to see an example of...

This is better than the post itself. Showing real output from a real repo.
I love how the author thinks developers write commit messages.

All joking aside, it really is a chronic problem in the corporate world. Most codebases I encounter just have "changed stuff" or "hope this works now".

It's a small minority of developers (myself included) who consider the git commit log to be important enough to spend time writing something meaningful.

AI generated commit messages helps this a lot, if developers would actually use it (I hope they will).

It's because the vast majority of commit messages are never read by anyone, and there's other ways to fund out what happened in the handful of cases where you would need to.
Our small team has a lot of commit messages like this. For a while, we had a guy on the team who had come from a site that expected more. The pet peeve he brought along was that commit messages end with a period (my guess is that someone at their previous work place had reasoned that forcing periods encouraged developers to actually write meaningful sentences). When I look at that period of development, I see lots of messages like “stuff changed.” And “more stuff changed.” And then it goes back to just “stuff changed” around the time they moved on.
If developers don't write commit messages, that's a culture problem. At my company we demand that of each other.
> It's a small minority

Is it really a small minority? I have never worked on a project that didn't have commit messages that at least tried to be descriptive (sometimes people fail at it but its very different to an outright "changed stuff").

I don't remember any friend mentioning to me them encountering a work project where the messages were totally neglected either.

Commit messages are often squashed after merging a feature branch
Bad commit messages always fail PR review for me. It requires will and discipline, but it's not that hard.
tbh I'm not convinced that a git log history should be treated as a group journal because it's not.

relying on git commit messages assumes they're correct by convention since there is no technical constraint to enforce it. and it assumes no work in progress commits, sometimes it's just necessary to hit the save button real quick or move a workspace from one device to another.

my point is: git is a way of storing and loading files at its core.

Sometimes it could be just a ticket number/title
And in a squash and merge workflow, which are most teams I've been on the past 8 years, it really is the title of the pull request or merge request. That is what really matters.

And I really like that because it leaves room to let the developer do whatever kind of commit messages they want to that makes sense to them. Because nobody's really ever going to read those again after it squashed and merged.

I also like meaningful commit names. But am sometimes guilty of “hope this works now” commits, but they always follow a first fix that it turns out didn’t cut it.

I work on a lot of 2D system, and the only way to debug is often to plot 1000s of results and visually check it behaves as expected. Sometimes I will fix an issue, look at the results, and it seems resolved (was present is say 100 cases) only to realize that actually there are still 5 cases where it is still present. Sure I could amend the last commit, but I actually keep it as a trace of “careful this first version mostly did the job but actually not quite”

We have a hard division between "Core" repos (those which are deployed to production / customer sites) and everything else. The expectation in Core repos is that everything goes through a PR process where the pull request message is intended to explain the what and why of the change (perhaps with reference to a ticket, but with the key information restated in the PR), and goes through a review just like the code. Changes are then either squashed with that as the commit message or (if they're larger and benefit from a clear separation of commits), may be rebased with `git rebase -i` assuming the final PR body ends up in one of the commit messages.

Non-Core repos are absolute free-form anything goes. You can commit 8 times a day without a PR as long as you're not deploying to production. This is excellent for things like test harnesses, CI/CD nonsense, one-off experiments, demos, prototypes, etc.

My last Core commit had something like 20 to 1 ratio between lines of commit message to lines of code (small change touching something deep that required a lot of explanation). My last non-Core commit message was "hope this works" (it did not).

Many organizations squash their commit messages from PR, where most commits actually happen. Unless everyone is committing to trunk all the time, which almost never happens on a real job, I highly doubt the value of this.

Showing my Git ignorance here, of course - does "ancestors(trunk)" pull in all the commit messages?

> developers write commit messages

The people who don't write commit messages for us are the non developers.

Writing commit messages shouldn't take any time at all. If it does, then you probably have a range of other professional issues.

One big problem about commit messages is, that you write them from your perspective. If you write them while activly engaged with the topic and changes you are very heavily biased and might miss things which are obvious to you but not to future readers with less knowledge. I think abstracting your personal opinion is quite hard - I agree that LLMs are perfectly fitting for writing this since they just dont have a "personal opinion". Atleast I made the mistake in the past many times focusing on the things which were not obvious to me but then leaving out the non obvious things for others.
> Most codebases I encounter just have "changed stuff" or "hope this works now".

I have been told off several times at different jobs for writing commit messages that are "too big". Also for writing too much commentary in my code changes. Also also for complaining that other people aren't doing these things.

(Not that it stops me, mind, but it does make working relationships fractious.)

I do not approve PRs from junior devs until the commit message is useful.
Likely my own personal bias, but I have never once found git log/commit messages to be useful when debugging or understanding what happened. I'm sure that others do so I try to write useful commits (conventional commits syntax is helpful here) but I'd much rather spend my time and effort understanding the current state of the codebase instead of trying to diagnose how "fixed bug related to file naming" relates to a website going down.