Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
The git history command (lalitm.com)
426 points by turbocon 23 hours ago | hide | past | favorite | 300 comments
 help



I was uncomfortable with git until I read (the first 3 chapters of) the pro git book ( free here : https://git-scm.com/book/en/v2 ). It provides a great mental model of how git works under the hood. The UI of git - for better or worse - directly reflects its internals. And when I understood them, everything clicked into place.

> The UI of git - for better or worse - directly reflects its internals.

I disagree. For example, we have, from “git checkout —help”:

   git checkout has two main modes:

   1. Switch branches, with git checkout <branch>

   2. Restore a different version of a file
The first variant never loses changes you made that aren’t in git yet, the second is explicitly designed to revert them. How does that directly reflects git internals?

(And of course, there are many inconsistencies in the CLI that do not have to do with git internals that, even if you know what you want to do internally and which command to use, make figuring out the CLI incantation to do it harder than needed. That’s a different subject, but still contributes to “git is difficult to use”)


Switching branches involves: updating the field marking "current branch", and then updating the working copy to reflect the objects referenced. So for all trees/files, recursively restore wc to match the referenced blobs.

Restoring a different version of a specific file/tree, involves recursively restoring wc of those files/trees to match the referenced blobs.

So both are actually primarily the same task. Knowing a little about internals is exactly how it is easy to understand how the same code (and hence the verb "checkout") came to be used for both tasks.

The reason it's still not great UI is that switching branch and restoring file arise in very different circumstances, and I want different guarantees and warnings and confirmations -- so using the same command still only makes sense when thinking very mechanistically.


you can use git switch to change branches

I know, I use switch and restore now (for the most part). I'm explaining how knowing the internals does help understand the 2 use-cases being originally implemented via 1 command, i.e., checkout. The post I was responding to said the checkout case ran counter to the idea that knowing the internals helps use the commands, when it's actually a pretty standard case where it does help.

I think they tried to fix this by adding new separate commands, right? git restore?

Yes, `git switch` and `git restore` are both marked stable now and have relatively clean UX. It can be worth reskilling out of `git checkout` habits and into these two separate commands.

Git is not nearly as confusing as people make it out to be. They just never take the few hours it takes to understand it. Which is a sad state of affairs for such an essential tool in the belt of any software engineer.

    git commit -am “Changes”
just does not cut it, if you call yourself a professional.

Obligatory git joke.

Git gets easier once you understand branches are homeomorphic endofunctors mapping submanifolds of a Hilbert space

https://softwareengineering.stackexchange.com/questions/2564...


My observation has been that those who have never learned Git properly -- disregarding for a moment the issue with what "properly" means here -- just don't think they need to, sticking to very simple workflows that produce linear graphs featuring squash-rebased work throughout. Because they don't know Git's fundamental model (including what you'd think was the obligatory piece of information that commits are essentially immutable), they don't venture farther than their confidence suggests, keeping it within the circle so to speak.

The tragedy, if you ask me at least, is rather that they then start imposing the "keep it simple" culture onto everyone else, which frankly turns Git into what SVN and CVS were accomplishing before it, with all the drawbacks of those. This keeps 90% of the team satisfied 90% of the time because there's nothing wrong with strictly linear graphs and the pro's that are asked to produce these, can always squash-merge their intricate local development history before they push to Github (which some Git users also think is part of Git proper).

Something about using Git in the aforementioned way just bugs me strongly. I admit it's a me problem, very likely, but lately I've also had to admit that it's not just that either.

For instance, if I need to fix a bug I use `git blame` to find the commit where I have learned the bug originated. I do a `git checkout -b ... <commit>` to start working on a fix, creating a branch starting at the problematic commit. Already that is head and shoulders above what many people I work with do or know _what_ does (or why would one want to do that).

I do it because it creates a _trail_, certainly more so than just committing on top of `master`, even with a good commit message.

But my practice doesn't produce linear graphs, even as pushed to Github (which we have to use, for better and for worse) and shared with everyone. I then get people coming to me and complaining they can't merge and it becomes obvious they don't understand Git sufficiently to do anything else but `git reset --hard` or worse, `rm -rf * && git pull` and hope for the best.

Not sure where I was going with this, something about why use Git when it's the new SVN with everything SVN had and none of what SVN didn't have...


Ok but the final outcome, most of the time, is a linear graph unless you're maintaining extremely long-lived branches for some reason.

Setting practices so that linear graph will be easy to understand in history is still a good idea, no? I'm not religious about squash-merge on master but when I look at master's history, I'd expect each individual commit to have a good message and be releasable.


My philosophy is that if you could've written your patch on current master, you should rebase it, but if there's any divergence that requires actual merging, then you cannot discard the divergence and merge from history because that will mess up history. You can only discard that if you do an actual rebase onto master, fixing up bugs and ensuring each commit that you are pushing compiles and runs by itself - a "fix conflicts with master" commit at the very end isn't allowable.

This is because of bisect. Bisecting broken commits doesn't work. Every commit should build.

In either case you should still rebase -i your branch as necessary to make it look clean when standing on its own.


A linear graph doesn't do any developer in the team except Github (which isn't a developer in this context) any good because they have to pull down the graph at some point and when they have to fix something, although they will be able to track the blame to some commit in the middle of the graph, when they fix it and merge it it has to be squashed again (by hard or soft enforcement), which like I said negates much of the point with Git -- there's no usable history left.

Point being that branches aren't something to avoid, implying that complicated graphs are not to be avoided either, but embraced and knowing how Git works it's not a problem at all (unless your "bush" is accidentally complex in a bad way) -- except that if noone bothers to learn Git, it _becomes_ a problem, solved at the cost of the value that Git provides -- branching (not just ephemeral branching for your local convenience). Exacerbated by Github since everyone pulls from there -- meaning that production-grade code is not a true graph but a linked list, on average.


> the value that Git provides -- branching (not just ephemeral branching for your local convenience).

I think this is the disconnect. I could see big libraries maintaining branches for each major release and I understand why linux does it. But if I'm maintaining FooService at work then I actually do just want a linear graph of all the commits that have been in production, in commit order.


If you want that, mandate developers use prefixes like `!fixup` and do `git rebase` _on your end_ -- why force upon everyone your ideas especially if it also squashes all development history? This also removes development friction because people can work with Git on their end the way they like without regard to some convention that can be avoided -- save for the "!fixup" thing which can be considered useful metadata.

I want development history to be squashed. I want to have a nice clean `git log` that I can look back thru and not see a mess of 'WIP' and 'fix' and other noise.

Unless you're saving every keystroke from your editor you're already squashing history, we're just arguing about degrees.


I do enforce rebased, releasable merges to master on my end, I'm not campaigning to remove branching from git for everyone.

Just saying, in the common case, the simple approach actually is pretty good.


can you elaborate what concrete value you've managed to extract from doing things like this? my gut reaction is that any benefit would be overshadowed by multiplying effort for other team members to catch up on their work, and if it makes it harder to work when even one person does it I can't imagine it scaling on a full team (or worse, something like a monorepo)

> creating a branch starting at the problematic commit

Code always lives in a context, and when you're fixing a problem that exists in top-of-main the context of the fix needs to be top-of-main, not some commit way back in history. Also if you do it that way, you'd better hope that no later commit also touches that same line.

> I then get people coming to me and complaining they can't merge

A team needs to agree on git practice in advance, and not have one wildcard go off and do their own thing.


> Git is not nearly as confusing as people make it out to be.

The people are confused, it's a fact. So it is confusing. Stop gaslighting people.


There’s this important word in the sentence you seem to be ignoring: “as”

GP isn’t saying “git isn’t confusing”, they’re saying “git isn’t as confusing as people make it out to be”

The point being, people exaggerate git’s confusingness when discussing it. There’s an element of irrationality here that’s worth bringing up, especially when it comes to comparing it to competing software. Once it has a reputation for being confusing, you’re more likely to be scared of certain workflows, you avoid them, and the problem compounds. Similarly if a VCS has a reputation for being “simple”, people might gravitate to it, learn it first, then parrot conclusions that git is too complicated because they never really gave git a chance in the first place (often they do take the time to read the other VCS’s documentation, and if they would have done the same for git they would have had no problems.)

It doesn’t help that there are so many bad tutorials out there that simply give you what commands to run, and tell you dumb stuff like “just make another clone if you get stuck”, and that makes the problem so much worse, and isn’t git’s fault.

Git is a bit confusing sometimes, but it’s mostly for reasons like “the reset and checkout commands do too much”, and “some low level commands are easy to confuse with high level commands” not because it’s a fundamentally confusing tool.


> Similarly if a VCS has a reputation for being “simple”, people might gravitate to it, learn it first, then parrot conclusions that git is too complicated because they never really gave git a chance in the first place (often they do take the time to read the other VCS’s documentation, and if they would have done the same for git they would have had no problems.)

I don't think I need to reply to a comment that illustrate the main point of this sub thread.


You just did though…

Maybe try and explain in more than one-sentence quips why you think git deserves its reputation? Otherwise you’re just coming off as a bit of a troll. Have a nice day.


Disclaimer: I love git, it is by far the best version control system I've used in nearly 25+ years.

But, his point is objective, so he doesn't need to provide subjective proof to you.

Do lots of people find git confusing? Yes. It is a very common view.

Therefore, based only on observed evidence, we can pretty confidently say that git is confusing. Is it as confusing as something else, or as confusing as the reputation? We can't tell, because those are subjective opinions.

The statement "it isn't as confusing as people think" is pretty meaningless here, because you can only judge from your own frame of reference. If it wasn't as confusing as people think, why do they think it is confusing? Are they all stupid?


Interesting. "A is X" is different than "I experience X from A". The former employs an objectivist lens while the latter a subjectivist one. I have had the pleasure of witnessing debates about whether a room really, truly is cold or not, which all resulted from a simple "it is cold in here" comment. In my experience modern English speakers tend to bias toward the objectivist register while Japan tends the other way. One of the joys in my life is how absorbing languages and cultures elucidates previously-unperceived distinctions. This discussion makes me want to try picking up Ithkuil again.

/r


But not as many people are as confused as is sometimes made out. And those who are confused are not as confused as people make out¹. Implying that a great many people are very confused because some are is far more gaslighty than suggesting it isn't that hard in the majority of circumstances, IMO.

--------

[1] many (and I'd include myself here) are fine using the basics, perhaps with some less as-simple-as-possible workflows, but might need to scan the docs if they hit a conflict merging two branches that have developed independently for a fair few commits and have overlaps


If you ignore gravitation, a sphere earth can be very confusing.

If your system's public API requires me to understand all the internals to use it, then it's indeed confusing.

The common stance is that people try to use said API without reading the docs. Like the first paragraphs of the git-rebase description

  Transplant a series of commits onto a different starting point. You can also use git rebase to reorder or combine commits: see INTERACTIVE MODE below for how to do that.
And then later

  Rebasing interactively means that you have a chance to edit the commits which are rebased. You can reorder the commits, and you can remove them (weeding out bad or otherwise unwanted patches).
Those are obvious IF you know what a commit is and how they relate to each other (a DAG). But most people don’t. Instead, their only knowledge is what they’ve seen the operation do (tutorial or youtube video).

Git itself has a glossary[0] of the terms used in the docs.

[0] https://git-scm.com/docs/gitglossary/2.54.0


plenty of models do fine with assuming a flat earth. git is an incredibly competent swiss army knife for building VCS but I'm not convinced it's the best we can ever do (nor we have ever done) for the simple linear history we expect from your usual corpo crud.

>The people are confused, it's a fact. So it is confusing. Stop gaslighting people.

I don't think this sort of aggression is warranted, as the fact that I find something confusing doesn't directly imply an intrinsic feature of the object that I'm confused with. One can reach that conclusion by observing that, not everyone who learns about the object finds it equally confusing.

All that to say: "The people are confused" doesn't mean that "All people are confused".


There is no aggression.

If your product has a reputation to confuse users compared to other products, your product confuses users.

Let's not start to bend reality just to back up some narrative because after reading a N hours documentation everything is OK.


> Let's not start to bend reality just to back up some narrative because after reading a N hours documentation everything is OK.

If you expect everything to be as easy as pie without needing to make a little effort to read a bit of documentation, then perhaps the problem is not the product but the fact that you are in the wrong industry, or the wrong role within that industry, for you. Have you considered a management track?

Git is intended to make certain patterns possible, and practical. “Easy for people won't read some documentation” was never one of its design goals.


You conveniently cherry picked the least relevant part of my message.

No one said everything should be easy as pie.

There are other software similar to git that have not such reputation. Version control is not new.


> There are other software similar to git that have not such reputation. Version control is not new.

Yes, there are alternatives that aren't used by anyone except dedicated fans. If they had the wide user base that git enjoyed you'd hear just as many complaints about them.


Because people actually use Git instead of whatever you mean.

This is irrelevant to the topic.

I didn't ask why git was widely used. I didn't say git was useless.

Again, there were version controls before git came out.


I think the point was that a lot of people are confused by git because a lot of people use it. Meaning that the other software where people don’t complain would get the same amount of complaints if it had as many users as git has.

> If you expect everything to be as easy as pie without needing to make a little effort to read a bit of documentation

Classic strawman.


> If your product has a reputation to confuse users compared to other products, your product confuses users.

Is your contention that all reputations are deserved, by definition? That’s certainly an interesting take. It leaves zero room for the concept of an “unfair reputation” for example. Maybe you should rethink your argument here.


do you think everyone who claims to be confused by git is making it up? or do you just think you're better than them?

I've elaborated on why I think git's reputation is undeserved in sibling comments. kreco's contention is that "it has a reputation of being confusing, therefore it is confusing", which I think is bollocks. They're throwing accusations of "gaslighting" around.

It's valid to think "yes it has a reputation for being confusing, but much of it is undeserved". It's not black-and-white.


> "yes it has a reputation for being confusing, but much of it is undeserved"

This is not what is written here:

> Git is not nearly as confusing as people make it out to be. They just never take the few hours it takes to understand it. Which is a sad state of affairs for such an essential tool in the belt of any software engineer.

> just does not cut it, if you call yourself a professional.


you seem very emotionally attached to what is, at the end of the day, just one of many version control systems.

You are moving the goalposts.

I could show you thousands of message backing up the confusion.

But you can keep nitpick irrelevant part of messages and pretend you are very smart.


You've made one and only one point in this thread so far: I'm addressing only that point. You've provided no other elaborations. You're saying "it has a reputation for being confusing, therefore it is confusing." That's a bullshit statement, IMO.

If you made any other elaborations on why you disagree with the post you were arguing against, we'd have something else to discuss. But right now you're just posting shallow dismissals with fallacious arguments. You can do better.


> You've made one and only one point in this thread so far: I'm addressing only that point. You've provided no other elaborations. You're saying "it has a reputation for being confusing, therefore it is confusing." That's a bullshit statement, IMO.

I'll repeat myself, I can show you thousands of messages point out how git is confusing. I can show you plenty of message saying "I used X it was fine, but I used git and it's cumbersome", please show me the same amount of messages telling "I used git it was fine, but I used XXX and it's cumbersome."


Git has many orders of magnitude more users than other VCS's. The assumption should be that you'd find many orders of magnitude more people finding it confusing than other VCS's. Even if the UX was more or less the same.

I'm also not claiming git isn't confusing (I've explained that it is, in another post), I'm claiming its reputation is undeserved, in that it's not as confusing as people make it out to be. You seem to reject the notion that such a statement is even valid, claiming that reputations are always deserved. I find that to be bullshit.


> claiming that reputations are always deserved. I find that to be bullshit.

You are creating you own frustration. If you read the thread again I never said this. You just got obsessed with the word "reputation" and never let go.

People are not hallucinating being confused by git. That's my original message. You can be at peace now.


Same here. The git commands were confusing to me until I actually saw a Linus video on youtube explaining how the git data structures worked under the hood. It was dead simple, and one could easily mentally map the tools' functions onto how they operated on the data structures.

Once I understood that, as you said, everything clicked into place.

Somehow the "higher level abstractions" that git tries to do makes the things ever so more confusing.


No it doesn't. Half the git commands are low level, half are high level. It's not 1:1 at all.

So when someone tells you "understanding X helped me a lot", your answer is "no, it did not"?

> It's not 1:1 at all.

I don't see any mention of "1:1" in the parent.


They're replying to

> The UI of git - for better or worse - directly reflects its internals.


> Half the git commands are low level, half are high level.

Can't we say that the low level level commands reflect its internal? ...


of course we can, that's not what the original comment said though

> The UI of git directly reflects its internals

Maybe?


Mercurial had by far the best UX, never had to read anything to learn it. I just pretend that I'm using mercurial when using git and nuke local repo if I mess up. Been doing it for my whole career.

I love darcs! Somehow its interface just vibes with me. It is interactive by default, and the commands like “record”, “rollback”, etc, are somehow intuitive. Darcs also tried to put more semantic content in the patches, like file operations or search replace parches.

It is always satisfying to rename something, then merge in some work where someone used the old variable name in the meanwhile, and the merge would go through automatically applying the rename!


I've often found that many problems when working in teams can be traced back to `rebase`. I understand the benefits, but if you're already working on a team where history is not used in any serious way, then you cut out a lot of foot-guns by only using `merge`.

> if you're already working on a team where history is not used in any serious way

Then you are doing it wrong. Sadly this is very common.


Except the "merge mess" footgun.

I got into training people in Git (I now do it for O'Reilly) because I worked with 12 teams that merged _everything_ into sub-branches. The result was that the `git log --oneline` command resulted in screensful of just pipes.

My initial training goal in training was getting my students to understand a rebase to avoid that mess.


I loved Mercurial, but once I learned jj, I never looked back.

> scary rebase -i commands that can leave your tree in a half-broken state if you so much as sneeze

`git rebase --abort` exists. One can also set a tag or something before doing the rebase, do whatever, then `git reset --hard $set_tag` to go back. Nothing to be scared of. Not like the prior state is lost.


I have so many branches named `temp` or `before-rebase` for exactly that reason; I'm using them effectively as tags, but branches can be moved around with less ceremony than tags (since tags are designed to be for things like v1.2.3, placed once and then almost never moved again), so I usually just do `git branch before-rebase/some-feature` before running a big `rebase -i`.

I've almost never needed to run `get reset before-rebase`. But I have often done `git log -p before-rebase` and compared that to the post-rebase state of the branch, to ensure that the merge-conflict resolution(s) that came up during the rebase haven't accidentally introduced an unintended change.


Diffing diffs is so valuable for that reason, seriously underrated. Once I even found a use case for diffing diff diffs.

I also often wish to edit commits or resolve rebase conflicts or whatever by editing the patch rather than the files.


Hey yo,I heard you like diffs, so I put some diffs in your diff so you can diff your diff while you diff!

Sorry, couldn't resist. The xzibit was too strong in your comment.


> but branches can be moved around with less ceremony than tags

`git tag -f` to move a tag.

Personally, I just do `git show` when I'm feeling cautious, but I can generally just scroll up to find the last `git commit` I did with the hash in the output. `git reflog` should also have record of it, so everything else is kind of extra.


Good point, that's no more ceremony than moving a branch. I guess I've just gotten "branches are movable tags" so deep into my hindbrain that I absorbed "tags are hard to move". But that's not actually true, and for what I'm doing (save this point in history for a while) a tag makes slightly more sense, semantically, than a branch.

After a rebase, mybranch@{1} refers to the previous location of mybranch, so you don't need to manually track these before-rebase branches etc.

(In practice I find this syntax super annoying and usually end up typing `git reflog mybranch` and then copy-pasting the commit hash from the output).


git tags are more lightweight than they seem. It's why the UX defaults to not sharing tags, with the idea that you may need/want local-only tags. They also share the same "namespace" that you could have tags like `myname/some-personal-tag`. (I've used `project-name/v1.2.3` style tags in monorepos rather than solitary versioning the entire repo.)

`git tag -d` deletes tags and `git tag -f` moves them (forces them to change).

For the most part whether you prefer a branch or a tag for temporary marks is an aesthetic choice, though the twist is maybe using annotated tags. The annotated tag allows you to leave a commit message for yourself why you made the temporary tag in a way that you can review later. Just like optionally adding a stash message (which also reflects why stashes work under the hood more like tags than like branches).


This is the exact same thing I do, though with the prefix "pre-rebase/"

You don't even need to set a safety tag like that anywhere in git, the reflog does that for you automatically.

Do you use git reflog?

One can also use that, or just `git log -n1` and taking note of the commit hash. So many options.

Fun fact: you don't even need the `-n`. `git log -1` does the same thing, for any number.

    -<number>, -n <number>, --max-count=<number>
        Limit the output to <number> commits.

For scripting: git rev-parse HEAD

I do when I forget to run the git branch comment before. But I find it still hard to understand exactly which commit to reset to in the reflog.

The branch reflog (git reflog <branchname> or git log -g <branchname>) is a lot easier to follow in the case of rebase than the HEAD reflog (which gets an entry for each commit that gets checked out / applied).

> `git rebase --abort` exists

Exactly. 50% of the times I type git rebase it is followed by --abort.


No need for that even. Just git reflog and rewind back to any spot your want!

jj undo

You can't get simpler than that.


I don’t consider myself a coder or programmer, but learning git was like an organizational superpower for my particular brain wiring. I use it for websites, design projects, electronics engineering, music composition, personal knowledge bases, remote administration scripts, config management, snippets, so many applications and so many features for one system. It’s not always perfect, but I tell everyone I work with they should learn it.

I also took a leap in organization. But I don't think you're responding to git in particular, just a source control system.

git (and other version control systems) take what you made, organize and preserve it, and you can get any number of copies at a moment's notice.

It is good for people to be able to do work and not worry about it.

I remember reading David Allen's book Getting Things Done years ago. It takes effort to read through his writing, but he had good ideas.

One thing he mentioned is that you need a 'trusted system' where you can collect and organize your thoughts. It lets you offload what you're worrying about until the time you need to do something with it.

I actually use omnifocus to do this for my tasks/projects/lists, but for what I actually do on a computer, I trust git to organize and preserve the software (and some data) I have written.


How do you use it for music composition?

I use it for lilypond for notation. I always considered it superior to Finale and Sibelius anyway. Not sure how it stacks up against the more modern commercial notation apps. But I love having a real trackable version history of my pieces.

What's also cool is the more advanced llms know lilypond and music theory too, so they can do things like... I don't know, check for counterpoint errors. I've used it with limited success to expand my jazz lead sheets into two-hand piano arrangements just for practice exercises.


I've considered using Lilypond, but I really like the autoplay feature in Musescore, as well as the lack of delay between placing notes and seeing the result.

there are plenty of text-based digital music notation systems. Then, you commit periodically as your composition evolves, and you can have all the history/fork/undo whatever that you might want.

Also, files don't have to be text-based at all to version them with Git. When you're versioning things for your own sake, there's often not much difference between keeping text and binary files there, as it's not like you'll have to resolve an unexpected conflict.

I find it really useful to be able to diff two commits, which if you're using a binary format is only possible if you have a meaningful-diff tool for that format. Some formats have one already created, more formats don't. (Though these days, an LLM might be able to produce something suitable).

I've been theoretically a big fan of these commands; I use the `jj` equivalents all the time. The roadblock I've run into is that I as far as I can tell (from the man pages and the git source code) there's no way to get `git history` commands to sign commits they modify:

    $ git log --oneline --show-signature  # look ma, I signed my commits!
    3a1dd8f gpg: Signature made Mon 13 Jul 2026 10:45:50 PM CDT
    gpg:                using RSA key FBF32CDBCC134B44FD29B66FA851D929D52FB93F
    gpg:                issuer "chandler@chandlerswift.com"
    gpg: Good signature from "Chandler Swift <chandler@chandlerswift.com>" [ultimate]
    Second commit
    03c3f6e gpg: Signature made Mon 13 Jul 2026 10:45:16 PM CDT
    gpg:                using RSA key FBF32CDBCC134B44FD29B66FA851D929D52FB93F
    gpg:                issuer "chandler@chandlerswift.com"
    gpg: Good signature from "Chandler Swift <chandler@chandlerswift.com>" [ultimate]
    Initial commit
    $ git history reword HEAD~
    $ git log --oneline --show-signature  # oops! where'd they go?
    5662b2c (HEAD -> main) Second commit
    6bf6830 Initial commit amended
This has pushed me back to the time-honored `git rebase -i` since I do want to keep my commits signed.

Woah! That feels like a huge oversight and like it bypasses standard commit logic. The man pages do say `git history` is experimental. Sounds worth a bug report.

I've been doing regular significant re-writes to my local Git commit history with relative ease for over a decade even without this new Git command.

I'm convinced that a big part of people often finding Git and rebase difficult is a user interface issue, having to know all the correct command line options and having to make your own mental model of what's happening and what you want to do, all makes it difficult to get a firm grasp on things.

I can't comment on what equivalent alternatives (especially on macOS/Linux as I'm a Windows user), but in terms of a UI for Git which exposes enormous power in an easy-to-use way I've found TortoiseGit to be spectacular and I especially like its rebase dialog:

https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm...

Which you kick off from a context menu option from the commit line on the commit log you want to rebase onto with a simply named "Rebase <current reference> onto this" option:

https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht...

It's super easy to re-order, split, combine, edit commits from here and it's obvious that will happen. If for whatever reason you decide you want to back out midway, you just abort and you're back to where you were.

I will concede there are aspects of a TortoiseGit I find a bit annoying, particularly that I feel I have to click around too much, instead of a shell extension I would prefer an app you launch with tabs or something, but I think this is a fundamental design principle of TortoiseGit, which followed the same principle as TortoiseSVN.

There is Git Extensions for Windows which seems nice at a glance, I tried it briefly, but I think I didn't stick with it either due to being unable to find an option I regularly used or I was just so used to TortoiseGit already.


As a former Windows user now on Linux, one of the things I miss is the single unified shell, where every installed program could install new right-click options on files and folders, overlay icons (like TortoiseGit's VC status icons), special objects that aren't files or folders (like My Computer), and so on. I understand this peaked in XP and from Vista onwards Microsoft started changing the shell in less-extendible says.

(but I'm still really confused why VC++6 had some menu option that would create a folder-like shell object and open it in a file-open dialog instead of just using a listview like everyone else. It was some kind of "add component to project" wizard, and I'd always try to find that fake folder in My Computer while the dialog was open)


One interesting new twist in the Windows 11 removes extension points/makes extension points harder to use is first class built-in git version control integration: https://learn.microsoft.com/en-us/windows/advanced-settings/...

It is not as convenient and it is not yet as capable as things like Tortoise have been in the past, but it is still an interesting thing to see paths explored in Windows like that, even if somewhat hidden in "Advanced Settings".


Same, but at the same time the existing workflow is a bit more clumsy / these commands are a bit more ergonomic.

Fixup was already possible through `git commit --fixup`, and before that through `git rebase -i`; reword also through `git rebase -i`. I suppose the developers got enough feedback that editing a file to decide what to do with each commit was a bit frustrating for some users.

It's a higher level command on existing functionality I suppose.


In terms of UI for Git, I've never seen anything getting remotely close to Magit. It's both in-depth and a simplified stateful interface.

Here's what I mean by "stateful": one of the huge problems with command-line tools is that their output becomes mostly inaccessible upon subsequent invocations. Suppose you ran git-log, and now you want to apply the knowledge gained from reading its output to your next action (say, you want to cherry-pick a commit). But you are lucky if the hash of the commit in question is still somewhere in the terminal. Otherwise... you aren't writing that from memory...

Stateful UI to command-line tools allows multiple interactions in parallel and preservation of information obtained in previous interactions.

Git needs a lot of state. You need to remember facts s.a. what branch you are on, what branch did you switch from, what files have been modified, what commits haven't been pushed etc. The requirement to remember all of this is overwhelming and makes the effective use of the tool difficult. This is where various tools come to help (eg. Shell plug-ins that provide auto-completion or modify the prompt to incorporate some of that state info).

I believe that people complaining about difficulties of using Git are often handicapped in their use of surrounding tools that would otherwise mitigate the problems above: they don't know how to add Shell plug-ins that display the current branch, they don't know how to have multiple tabs/panes open in a single terminal session, how to copy text between those tabs/panes, they don't know how to invoke / search auto-complete options.

Genuinely, if you don't have tools to help you use Git, something that people who are used to navigating the world of terminal UI, using Git becomes very unpleasant.


In short, newer versions of git implemented three really frequent use cases of `git rebase --interactive` as separate lower-friction commands. Apparently they only work when there are no conflicts.

Wonder if the history command is all that useful.

I prefer the interactive rebase and use it frequently.

Would much rather “visually” move commits around than accidentally aim “git history” at an orphaned commit hash no longer in my local branch.


The implication seems to be that `git history` is the higher level, safer `git rebase` for common scenarios. If you are already comfortable with `git rebase` you might just keep using it and never need `git history`. If you are learning git as a junior developer (or teaching/mentoring a junior developer on it) `git history` becomes a good, safe stepping stone for a handful of common tasks.

I've been in workplaces where I wished I could disable `git rebase` for all junior developers because they kept learning its footguns faster than its capabilities. A stepping stone seems like a really good idea to me.


> `git history fixup` fixes an old commit that has something wrong in it, then autorebases all your branches to match.

Simplifying `git rebase -i` is a great idea, but unfortunately, that does not match my workflow.

I always keep an history of my branches. For example, when I develop a feature, the first version of the branch is `myfeature.1`, then `myfeature.2`, and so on. This is useful because I can retrieve an old version when something behaves differently in a newer one. (And no, `git reflog` will not help)

This has saved me multiple times. For example, I can determine that a problem was introduced between `myfeature.58` and `myfeature.59`. If the "feature" contains 15 commits, I do:

    git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59
This lets me see the changes between the 2 versions, commit by commit (even if they don't have the same base).

I don't want all the branches containing a commit to be rebased automatically (I already try using tags for old versions instead, but this does not quite fit).


I don't get all the effort people spend in perfectly curating git history. No one is ever going back and reading individual commits. Just squash everything before merging and call it a day.

> No one is ever going back and reading individual commits.

I violently disagree with this.

At a minimum, when I review PRs I look at the commit history to understand what's up. If the path that was taken to commit this is full of "oops" and "fix" messages, it's an immediate reject for me. The commits tell the story and it's a kindness to your human reviewers to not make them work harder to understand the point you're trying to get across.


> If the path that was taken to commit this is full of "oops" and "fix" messages

great way to encourage people to rebase then!


What's up with the fix commits? Maybe I misunderstood you, but there ain't nothing wrong in fixing stuff you offer in your PR. And there can also be multiple commits even before the PR while you're developing your PR.

> What's up with the fix commits?

They shouldn't show up in the commit history. In a PR, you merge them in the commit that they actually fix. Otherwise when you use git blame to get the context of why a line of code was changed, all you see is a useless "fixup" message that is worse than having nothing.

Anyone can do better than a fixup commit. And doing metter means merging them into the actual commits that are fixed.


Hence GP's advice,

> Just squash everything before merging and call it a day.


For a change small enough to fit in one commit, that works. For a larger merge, you might still want multiple commits merged together. For example "make the foobar extensible" and "extend the foobar to add the baz" are really two separate changes that may be merged as part of a single PR to add the baz.

“make the foobar extensible” has no value without also “adding baz”. If I need to remove baz, I can’t just revert “adding baz”. The extensible foobar needs to go as well because it’s a unused abstraction.

OP's advice is bad and lazy. You want to have a coherent commit history with isolated changes.

See my reply here https://news.ycombinator.com/item?id=48903456


> Otherwise when you use git blame to get the context of why a line of code was changed, all you see is a useless "fixup" message

Isn't this solved if you squash the commits when merging the PR? I personally don't care that much about the commits inside a PR, the are just temporary because when a PR is merged they are squashed and you only get one commit for the whole feature on the main branches


I once root-caused a revenue-losing bug that had the entire rest of my team stumped with ten minutes of git-bisect.

It only worked that well because the team had all internalized my advice to write small, coherent commits, so the bisect landed on a ten-line change.

I strongly dislike the recent trend towards squashing every branch into a single monster commit.


Most people who squash things have never used git bisect, cannot solve a merge conflict and when there is one will just delete the directory and clone everything again. I've worked with such people. They can go on like this for an entire lifetime.

I used git bisect once in 10 years, and it was when I learned about its existence.

I am convinced that very few know about git bisect, much less use it regularly.


> I am convinced that very few know about git bisect, much less use it regularly.

Yes, unfortunately not all companies have a high hiring bar. Some


For me (I know you used most, not all). A PR is an atomic thing. Either one bug or one feature. Commits inside it are mostly time snapshots, and fixing formatting and linting errors. If I where to properly present the PR, it will also have been a single commit.

A PR is an atomic thing, but at a higher level. It's an integration point. Merge commits are a great representation of an integration point. `git log --first-parent` gives you an integration log. Without `--first-parent` you have a "conversation log" with details about how PRs were formed.

`git bisect --first-parent` lets you start with your integration points (your PR merge commits), and because presumably you have Continuous Integration and make sure integrations points build and test successfully that should be a rather quick discovery run, and then when you discover the PR that introduced the issue, you have an opportunity to drill down into the even smaller specific change inside that PR that introduced the issue.

Merge commits are a navigation tool and an integration log. It seems useful to me to prefer them over rebases and squashes.


> you have an opportunity to drill down into the even smaller specific change inside that PR that introduced the issue.

But what would be the point?

Let’s say you found an issue in the first commit of the PR (assuming it’s curated and every commit can compile). But the PR is atomic, and later commits rely on the assumption made in the first one. You would need to replay the later changes as well to figure out the impact.

Squashing PR means you consider changes at an holistic level regardless of the workflow that created them. If a PR fails with a regression test, the whole thing is suspect, and I don’t really care when in the workflow it was introduced.

If I have a PR titled “Add support for flac files” that introduced a regression, I don’t really want to know if the bug is on the commit “extract sampling information” or the commit “support flac tags”, because what got released was the PR, not individual commits. Just like no one care if a typo was introduced in draft 5 or draft 8. The only things that matters is when it got published.

For me PR are releasable patches. Individual commits in them are the engineer’s workbench. Whether you want to curate the latter is up to you, as long as the PR is atomic.


The point is your ability to find needles in a haystack increases. A PR often is bigger than a 10-line change, but is often made up of smaller 10-line changes.

The ability to drill down with a second `git bisect` run (now with a known base and end commit, and even the ability to again use `--first-parent` to ignore merges inside the PR commit range) into the original contents of the PR is the ability to automate finding your needle in a 10-line change with its own git commit message and its context in the original conversation flow in the original PR.

That's a powerful ability.

Sure, you can probably comb the complete 100 or 1000 or 10,000 line PR to find the exact lines that caused that regression, you've narrowed down already to one useful haystack, but it's nice to have an optional second layer to break your haystacks down further sometimes.

(Especially if it turns out to be a regression from a merge commit inside that PR. Accidental bad merges happen all the time. Spotting them is hard sometimes. Spotting them after a rebase/squash happened is sometimes impossible because there's no unique record of the conflict resolutions unlike with a merge commit.)


> Sure, you can probably comb the complete 100 or 1000 or 10,000 line PR to find the exact lines that caused that regression, you've narrowed down already to one useful haystack

But the PR is one single atomic changes. even if it 100 or 1000 lines. This very measure makes it easy to review because there’s only one assumption change PR A (the good one) and PR B (the bad one and and also the current one).

Don’t forget that the codebase will also have several modules. With just one single patch, I can see which modules are affected and then reason where the bug may be. Using merge may not have helped as the 10 line changes in an individual commit may have been because that’s where I integrated stuff that was unused in the previous commits before the merge. That’s why an holistic view matters.

> Spotting them after a rebase/squash happened is sometimes impossible because there's no unique record of the conflict resolutions unlike with a merge commit

That’s something I never needed because the only thing that matters is codebase at state A, and codebase at state B, and the diff between those two states. Ideally, a single reason for the transition between the two.


I've had to deep regression analysis and archeology to make sure that regressions aren't recurring or stop recurring, because regressions can recur. An engineer that maybe read the wrong advice and got too eager in using a rerere cache only for it to cache bad regressions. Another engineer that missed a memo somewhere and regularly mismerges a feature thinking that work in progress is actually legacy code. A third engineer that accidentally committed temporary code used for testing that was more obvious looking at the exact commit where it was made than the PR it was made in.

There are so many such scenarios where more information is better. If you've got a good PR tool it might save caches of those branches pre-squash some amount of time and you can do some of that sort of archeology in your PR tool, but even GitHub will sometimes garbage collect PR commits from deleted branches eventually.

The git DAG being a two-dimensional data structure is a useful tool. I find that I want to preserve as much information as possible, including using `git merge --no-ff` in additional scenarios that many use `git rebase` for because I don't know when I will need that (integration or testing or process change) information, but if I find that I need that information it is good to have it.

It's related to the same reason we don't throw out commit messages on ancient commits. In my experience, no matter how outdated that information gets, you are going to find surprising reasons to need it. Source control isn't just about recent history, even if that is most of your day-to-day needs. Sometimes you do need to revisit the past and you don't always know exactly what you will need from that past until you do need that information search.


> I've had to deep regression analysis and archeology to make sure that regressions aren't recurring or stop recurring, because regressions can recur. An engineer that […] looking at the exact commit where it was made than the PR it was made in.

That mostly a staple of the merge workflows where people are crisscrossing merges all over the place. At the end you have those horrendous diffs.

A rebase (and squash) only considers the tip of the main branch (which is a working state) and add changes that bring it to the next working state. You’re always aware of the latest working model of the code because that’s the starting point of your work (not something from $days ago). There’s no bad merges in the history of the PR branch.


I've seen some really bad trainwreck merges in rebase heavy workflows, too. (Unwinding them is awful.) Rebases create just as many merge conflicts as merge commits do [0], but rebases don't save the evidence for them. Just because the evidence was lost of them doesn't mean the merge conflict markers were never there.

Any time you integrate two branches, no matter how long running or short running, you have possible merge conflicts. Like I said, I prefer keeping that integration log as a tangible source control artifact. I understand how many people don't care for it. But don't mistake it for solely an aesthetic choice. Merge conflicts are a necessary part of source control and sweeping them under the rug is one way with dealing with them, but in my opinion not exactly the healthiest way.

[0] ETA: Merge conflicts are not just a technical issue, but a communications and coordination issue. Software development is a social activity and as long as it is a social activity it creates merge conflicts.


I do agree that merge conflicts are a signal of a deeper collaboration issue.

> Merge conflicts are a necessary part of source control and sweeping them under the rug is one way with dealing with them, but in my opinion not exactly the healthiest way.

I don’t agree that retaining them is necessary. Merge workflows encourage long running branches. Sometimes divergence in understanding does not create conflicts and that’s how regression happens.

With most rebase workflow, the commit list is often kept short (which is why squashing them is often correct). Most of mine have been below five. Such patch is easy to review and reason according to the latest knowledge of the code. Also easier to cherrypick and apply to an old version of the code.


Use merge commits does not mean using "merge workflows with long running branches".

"Merge early and often" is just as useful of a concept as "rebase often". The workflow is often exactly the same. The only real difference is extra commits to mark the merge points, and the extra commits are mostly just a UI issue when code reviewing.

A good PR UI (and GitHub isn't always, but it tries) doesn't show commits brought in from the base branch. (I think GitHub would have a simpler thing if it defaulted to a simpler `--first-parent` approach (rather than trying to math from the base branch) with an option to drill down, but I'm not a a GitHub UX designer.

Don't confuse the workflow with the DAG shape, that is more aesthetic than not.


Good for you that we don't work together, because for sure I'd reject all of your pull requests until you learn :D

This could probably be helpful https://mtlynch.io/code-review-love/


The only point not addressed by reviewing changes at a PR level instead of a commit level is

  7. Break up large changelists
First a PR shouldn’t introduce scope screep, where you are actually introducing more than one change. And second.

  Instead of changing everything at once, can you change the dependencies first and add the new feature in a subsequent changelist? Can you keep the codebase in a sane state if you add half of the feature now and the other half in the next changelist?
When the only reason to change a dependency is for a new feature, you keep everything together. That way, we can revert a feature at once without needing to hunt down related commits. I abhor unused code in the main branch.

And I say that if you can’t review a PR as a single patch, there’s bigger problem. As a reviewer, the only thing that matters is the change and its purpose, not a particular workflow/ritual.


Great! Now your bisect won't tell you if the issue is caused by the dependency or the use of the dependency and you will have to do more manual investigation!

Certainly a way to do things. Not the most useful or productive… but it's a way for sure.


> if you squash the commits when merging the PR?

You tidy up and rebase before making a PR. Anything else is really disrespectful of your reviewer's time. That is also how all the larger open source projects operate.

> you only get one commit for the whole feature

If you are doing one logical commit per PR, you are doing way too many PRs.

Alternatively you don't have a working review process.


Yes, often you have separate logical changes that should still be reviewed together.

Not really, because that one commit represent a logical changes to the codebase. It’s either in or not. Splitting it would be only cosmetic. That’s what a good PR in my opinion.

Presenting a series of patches is good in an email format because when I’m adding them, I can evaluate each and decide whether I want it or not. But GitHub (and forges that copies it) is lacking in that regards without me taking over the branch.

So the word is to make the PR the unit of changes, and only review the whole diff, not the individual commit.


That's one approach, sure, probably works best if your units of work (e.g. everything inside of a PR) are small and atomic though.

Other use cases exist where each individual commit adds value / changes something important / is atomic. Which one is best depends on the use case.

What should definitely be avoided (or, what should not end up in main) is "work log" commits. Many people use git commit like a save / checkpoint operation, that's the kind of thing nobody needs to read. That's the "fix" commits.

Succinct guideline:

Good commits: "When applied, this commit will <commit message>"

Bad commits: "I did <commit message>"

Then whether it's one commit or the result of a squash merge it doesn't really matter much anymore.


> Isn't this solved if you squash the commits when merging the PR?

In theory, yes. Squashing is an extreme approach to merging fixup commits.

It also throws the baby out with the bathwater by removing individual commits that explain and clarify how and why some changes were introduced as part or a PR.

If your PRs are tiny and don't introduce major changes then squashing is ok. Instead, you should do the right thing and curate the set of commits featuring in your PR.


It depends on what you think "the right thing is".

Our right thing sounds different to your right thing. Our right thing is PRs less than 500~ lines, and a single logical change only if the overall goal is complex.

For example, in your "right thing" it sounds like you'll have a refactor commit somewhere in the chain of commits in your PR, that might introduce 2000 lines of change, and other logically coupled changes in the same PR, all resulting in a large PR.

We prefer smaller, complete, mergable PRs. And therefore we normally only ever start with a single commit in the PR because the dev squashes everything before raising.

I don't know which way is better, but I do know that when I come across large PRs, I zone out and review quality drops. In fact, I just don't approve them.


Making small atomic commits as you go in the age of AI tends not to go great because it forces too much human in the loop in a lot of cases, and the percentage of AI code rework is significantly higher than manual code, so the history tends to be harder to keep clean.

It's ironically easier to create a messy agent work branch then have the agent cherry pick independent PRs from it into atomic commits post-work.


> Making small atomic commits as you go in the age of AI tends not to go great because it forces too much human in the loop in a lot of cases (...)

You seem confused. In the age of AI your ai code assistants already do task-specific commits. In fact, you can easily create a skill to have ai do that for you. It can even use git history split.

You see where this is going?


Mine aren’t full of “oops” and “fix” messages, because I squashed them.

So you're "spending effort in perfectly curating git history" and agreeing with your parent comment.

Just squash everything before merging and call it a day

That is also a line from top comment. Everyone read „perfectly curating git history” and went rage commenting instead of reading and understanding what OP wrote.


Nope. Perfectly curating history is indeed the opposite of squashing—you squash because you couldn't be bothered to curate your commits. Squashing is a workaround not an alternative solution.

You just contradicted your previous comment that was pointing out that fcraaldo is „spending effort in perfectly curating history” … or your comment was a joke with no indication it is a joke.

I interpreted "I squashed them" as "I used git rebase -i to squash the oopses and fixes". If that's not what the user does, and rather squashes the PRs, then indeed I would be disagreeing with him.

Yep, the curated history is the main branch, which the PR targets. The commit log in the Pr reflects the workflow of the author, which I have no interest in. As the reviewer, I’m only interested in the content (the description and the composite diff of the whole PR). I don’t review commit by commit.

But parent wrote:

Just squash everything before merging and call it a day.

He didn’t write „leave a mess”. So it feels you wrote knee jerk comment or just writing whatever you wanted to write disregarding whatever was written.


Depends what the git history is supposed to show. Personally, I prefer people to leave their mistakes and reversions - though I'd require more description messages than "oops" or "fix", something that explained why it was being reverted or swapped out would be the minimum.

Sometimes you try things one way and they don't work out, so you go in a different direction. Capturing why this happened and when can go a long way towards explaining downstream decisions that might seem confusing to someone with a fresh perspective.


One part of me wishes for multiple levels of logical commits.

When using GH we essentially have one level. The PR is the like a roll-up commit and then we have the component commits it consists of.

It would be nice to be able to say this commit consists of N component commits. Then users can expand or collapse the commits depending on what level of detail they want.

So user A who likes to keep a record of how they actually went through the process with all the warts can have those "messy" commits as component. And user B who likes to see a coherent story told by the commits without unnecessary steps can look at the higher level commit.

But also, git is complicated enough so maybe not.


This happens when people insist on the (rare) always-merge policy for PRs. You end up with a shorter chain of merge commits (one per PR) directly chained to each other on one side, and their other sides have several real commits between each merge. It's not the easiest structure to work with on the command line but it's clear in any visualiser.

The why for a commit belongs in the message or code comments not in the history.

yea I look at commits several times a week at least, especially when commits are tied to a ticketing system/project it helps a lot going back months later on a large codebase going “how/why did this change happen”

I do tend to squash or make my entire change in one commit though so maybe I misunderstood your comment. If I have a fix commit often I’ll just tag a separate PR/ticket to keep the change history/change control clean


Parent doesn't have experience of working with codebases with slightly high than average code quality requirements.

In products s.a. storage, avionics, medical appliances etc. it's very typical to have a requirement for each commit to compile and to apply tests retroactively. I.e. once a test is added against an existing feature, it is run against every commit since the feature creation (this is also why git-bisect exists).

However, it's true that a lot of companies would probably do better with just rsync instead of Git. Their Git history is in such a bad state that it's basically useless. It just doesn't make sense to use such a complicated tool as Git to deal with the average workflow.


I think that the path that was taken should include mistakes. It's natural that code at some point would contain bugs and mistakes. If anything, your approach would encourage to squash those commits into one just to make it being review-worthy, but that misses the point then.

It shouldn't be that path that was taken but the path that will be taken when the PR is merged, split up into as many self-contained steps as possible to ease review now as well as triage if problems are found later.

You sound pleasant to work with. I bet your coworkers route around you when they can, and when they can't they cherry pick from their working branch to deliver monolithic commits while rolling their eyes.

No, he sounds like a professional with standards.

A professional with standards who wasn't also unpleasant would put the time in to review the content of the commits with a request to clean up the history. Someone who looks at the history, thinks to themselves "not how I like it" and just auto-rejects the entire PR without any further thought is just a bad coworker.

No the bad co worker is the one who is trying to offload his job on the reviewer.

A programmer's job is to deliver business value to their employer. If you're slowing down PR turnaround by mindlessly auto-rejecting on stuff that the suits don't care about, you better have a rock solid case for why that is going to deliver business value down the line, otherwise you're actively sabotaging your employer to bikeshed your personal preferences, which is the hallmark of a bad employee.

Rejecting an obviously bad PR after scanning the code quickly is one thing, burning business cycles on PR turnaround/latency to bikeshed bookkeeping without spending any time on the actual value producing portion of the PR is just bad. At the minimum you wasted an opportunity to give feedback on the proposed solution, thus probably necessitating another round of reviews, with the associated org latency.


You are assuming that the only thing the business in question cares about is moving fast without any consideration for long-term health. I'd consider that a bad place to work at.

In most business settings, the ticket is the unit of value to the business. If a ticket is to big, the best way is to split it into several ticket. Then you create a PR for each. There can be some automation that update the status of the ticket alongside the PR. Splitting a PR futher into commits doesn’t make any sense, because the whole business operates with tickets.

When I do it, it’s for my convenience. I expect the reviewer to review the diff at the PR level, not at the commit level. And when it’s approved, I’ll squash and merge, because only the whole PR matters.

When the commit is the unit of work (email workflow) I curate locally.


Do you really care if someone forgot to format before committing? They can always squash and push locally if they need to.

I really really really do not want the autoformatter stuff happening in the same commit as where the real thing happens. I don't care to review if the autoformatter is working properly.

> Do you really care if someone forgot to format before committing?

Not OP but yes I definitely do. If you expect others to spend time reviewing your code, you are obligated to start off by reviewing it yourself. Posting a mess helps no one and makes code harder to audit.


NGL AI usage is driven by friction in presentation and communication over petty details.

Oh that is such a bad heuristic ! The commits and history of how a PR was put together is no indicator of the quality of the PR or the thought process that led to it. Thats the equivalent of rejecting a (handwritten) essay for having too many corrections. ridiculous.

The code is all that should matter. Maybe comments for being nice to others and my future self. thats it.


> The commits and history of how a PR was put together is no indicator of the quality of the PR or the thought process that led to it.

It is, because it means the person posting the PR didn't even bothered to review the changes they are forcing others to review.

Just clean after yourself before asking others to read your stuff.


> No one is ever going back and reading individual commits

I do, regularly! In a repository where care has been taken, it can be super valuable when tracking down a bug or regression, and understanding the intent of the author


The debate between these who don't squash and these who do is the debate between these who use the history for bugfixing and those who don't. And I think not using it is throwing away a super valuable tool that can reduce the fix ETA by an order of magnitude.

> No one is ever going back and reading individual commits.

Straight from the git-log, maybe not, but sometimes you see code that makes you wonder how it came to be and it can help a lot to see it in context of the commit that introduced it. That'd be less helpful if that commit were some huge thing making lots of different changes at once.


Seeing the individual change in the context of the larger feature is actually more helpful. Otherwise you find a tiny commit that changes A to B and then have to chase down 13 other commits around it to figure out why that change was even made.

Yeah, there's a balance. I try to make my commits so they make sense on their own.

> commits around it to figure out why that change was even made.

A commit should be such that the message can articulate the why.


Having a well curated one-logical-change-per-commit history is incredibly valuable when bisecting a regression.

Didn't you mean "dissecting"?

Edit: thanks guys, that's very useful knowledge! Could have saved me many times in the past


Nope, `bisect`: https://git-scm.com/docs/git-bisect

Figure out a command to test it, a known-good sha and a known-bad sha, and it will binary search its way through the history to find the commit that introduced the failure.


Check out "man git-bisect".

  man git-bisect

How do I know you don’t work in a big team from this?

First thing I do every time I come back from vacation is to read through our git history to see what has happened.

It is also very useful when CI breaks. In fact I probably use some form of git history reading every day at work.

Also if your code base is so tiny and the features you work on are so small that you can just squash everything then maybe that is fine for you. I definitely love being able to actually see what is going on and selectively cherry picking or reverting commits


I do multiple times a week, with repos that have barely been touched in a decade and all the original devs are gone. Squashing would make figuring out why something is the way it is a lot more painful, so I'm glad these repos are svn where squashing wasn't an option. Several times I've discovered bugs that were introduced in linting commits that would have been squashed, so the fix ends up trivial since the intention is already there in the previous commit.

I am always aghast at how proudly people pronounce that they don’t know how to use one of the most central and essential tools in the belt of a software engineer.

Maintaining history is well worth it, and allows you to use tools like

    git rebase
    git revert
    git bisect
    git blame
    # …
It also makes reviewing and understand your code much easier.

git bisect, connected to a script that outputs good/bad, is one of computing's crowning achievements, imo.

Having maintained internal forks, and having needed to pull in and rebase upstream changes I do read a lot of commits. Projects that take care with their commits are much easier to work with.

This is also true when creating with security patches and other things that need to be backported to multiple releases.


One use-case for curating commits other than git history is carefully structuring code reviews to be easy to review. Eg "commit 1 just rearranges existing code, no business logic changes"..."change 2 modifies business logic, but in one localized place as the refactoring has already been done"..."change 3 only modifies comments"

yea i agree, the people who defend perfect git history are most likely not enforcing pull/merge requests. why would I care to look at git history when a pull/merge request is way more informative? they then back it up by saying how rebase is so much better than merge, but yet when you git blame a line of code that was merged and not rebased it's going to indicate the pull/merge request which is going to link back to a requirement. sorry for the harsh comments from these people about you, because imo perfected git history is actually the lazy approach instead of proper change/requirement management

Of course we do and it helps tremendously when the graph is reasonable.

Sorry, no. Just because you've never done it doesn't mean that no one ever will.

Beyond the archival benefits, I've found plenty of bugs by going back through my "wip" commits and creating a sane history from them.


Squashing everything has a significant downside compared to functional atomic commits: git-bisect gets WAY less useful.

Disagree. You need small, git bisectable commits. Use stgit to do it and it's nearly effortless.

I have a more nuanced view of this; in libraries and software projects that span years like Linux, curl, etc, this happens all the time. Bugs get fixed and they can point to the exact commit where it was first introduced, and that commit contains all information necessary to figure out why it was introduced in the first time.

But that's projects like Linux, which is a whole different use case than for example what I do for a living, front-end applications that have at best a lifespan of 10 years and whose individual features / screens / components have less than that, and it's much more rare that an older commit is still relevant today.

(that said, looking history up has made me dislike squash commits. I get it from a pragmatic point of view but it's not ideal)


I’m with you, but it’s obviously a sensitive topic for a lot of folks, based on the replies. Some people’s brains are just wired differently, I guess. I rarely look at history, other than git blame. For me, history does not need to be granular at all. The replies make it sound like curating git history is the job, rather than shipping solutions to customers’ problems. I guess, if it helps you ship faster / better, more power to you.

That is like saying once the program is done, throw away the source.

Lemme guess all your for commit say "wip"


It's really not. Source code, even with no history, can be modified, adapted, ported, fixed, and improved. Having history is nice, but it's way lower marginal utility.

One good reason is to keep your tests separate from the fixes that make your tests pass. That way you can check your test fails before the next commit makes it pass, eliminating the risk of a false negative (test passes that would have anyway).

That's one approach I suppose, it leaves evidence of "I wrote a test first" and such.

However, it also feels more like a "work log" than a "commit log"; I like to make my commits atomic (test + code at the same time), so that in theory, each individual commit will pass CI. If you separate them, you can't just revert one commit, you need multiple.


That sounds like it would break bisect

You could reorder so that the test comes in a commit after the fix rather than before if you want to be able to use plain git bisect, so that no commits are failing. The important thing is that you can use git (i.e. revert in this case) to edit the history to remove the fix and verify the test fails.

You can develop with failing tests and reorder them after the code change before publishing your branch.

Of course, but this thread was in the context of someone "carefully curating" their git history

Fair. I think what I'd say is that we don't have to use plain git bisect -- it would be quick to make a bisect script that doesn't land on the failing-test commits. Especially seeing as most teams squash before merging, we should have the freedom to create failing-test commits.

Right, maybe a better way to get what they're going for would be

  git checkout @~ ^tests
where ^tests is zsh's way of saying * except for tests. So you get the tests/ of the current commit and the code of the prior.

> That sounds like it would break bisect

Nonsense. First off, you can pick the starting commit, and nothing forces you to pick the test one. Second, bisect is designed to tracks changes from good state to bad state based on your personal criteria of what good and bad is. This means that you are free to put up tests that make sense to you (i.e., all tests except the one that was added as a red test) and even not run a test at all.


And you can also use bisect by first figuring out the broken commit some other way and then marking everything as good if its before that one or bad if its after - but that's not a very productive use of bisect and if its the only available one I think its fair to call it broken.

> And you can also use bisect by first figuring out the broken commit some other way (...)

Bisect exists to be a tool you can use to fix the problems you are facing.

It you are faced by problems you create for yourself and you are unwilling to work around the problems you are creating then there is no tool on earth that can help you.


Suppose we have a failing test. For many commits, gp carefully separated their change that would break our test, from the change that would fix the test. At some point, someone forgot to run the tests, and introduced a commit that broke the test without adding a second commit to fix it. Now it's treated as a "pre-existing failure," and many more commits are added on top.

How would you identify the commit that broke the test?


> How would you identify the commit that broke the test?

You're asking how to use bisect.

You start with a range of commits you picked. All that bisect does is help you search for a commit within that range that introduces a regression. The responsibility to specify which commits you cover is yours, not the tool.


And then bisect fails because it finds one of the many commits that broke the test before the actual regression

I could count the number of times I've written a detailed summary in the body of a commit message, and had it read by my colleague reviewing who then didn't need to leave a comment on the PR asking why I did _x_ on zero hands.

> No one is ever going back and reading individual commits.

Hell yes we are! Just because you aren't going to doesn't mean nobody else is going to.


> No one is ever going back and reading individual commits.

Your assumption doesn't match the real world practices I've experienced for years across multiple jobs. Even at the PR stage a clean commit history is of critical importance. Nowadays, with ai coding assistants assuming a central role in developing software, commit history is even used as input with context signal, allowing for flows such as "evaluate the changes in commit X and Y and apply the same pattern to project Z".

Just because you don't use a tool properly that doesn't mean everyone around you makes the same mistake.


I almost never went back to read the history, but now I often have Claude go through the history when I wonder how we got to a certain point. It can point me to the relevant issues as well. Squashing is fine, up to a point.

Might as well go back to the tar.gz days, eh?

Spoken like a guy who really really doesn't understand how git works and has never bothered to learn!

I do not agree at all. When you have multiple repos accross different services, commits are the best way to follow up with differents changes. We adopted conventional commits guidelines a few months ago and everyone is happy. Even ClaudeCode is able to keep up and auto fix stuffs with the proper commit messages. The changelog is dynamically updated. Everything so smooth when those commits messages are perfectly synchronized.

IME, the only real pain point with JJ is Git integration. I love it, it's great in almost everything it does. Formalizing all the DSLs and using them consistently throughout the config and CLI is exactly how filesets, revsets, and templating should be implemented. The focus on exposing concepts as interfaces (without "directly reflecting internals"[1] in the UI) is only as good as the concepts themselves, and in JJ's case, they are powerful. The problem is that they are too flexible, so Git interop relies heavily on convention and discipline... which is fatal. The fact that (almost?) all states of JJ are representable in Git makes it easy to mess up your remote, and then you need a very solid understanding of Git to fix it.

If you can give `jj` a try on a greenfield, single-developer project, you probably should. It feels exactly like Git felt compared to Subversion (when you focus on JJ's strengths and use it in favorable conditions - it is young, after all). Get acquainted with `jj` on its terms, and if things click for you, you'll be a bit better prepared for the bumpy road that is Git interop later on.

[1] https://news.ycombinator.com/item?id=48903054


In princple JJ has non-Git backends, but the only backend they state as being production ready is the Git backend. JJ without the Git interop is more a concept than a reality right now.

I love JJ, and the mental model of source control it presents, and I will continue to use it. However, still needing to "drop down" to Git every now and again to get something done makes it feel very much like a convenient wrapper on top of Git rather than a new SCM.

If Git adds commands to support the JJ workflow, it would be hard to justify having JJ installed any more.


The backend itself is fine, it's just trying to use JJ as a Git frontend for projects developed on Github (among 20 other devs using git, where each of them uses git through different interface and/or in different style) that's currently not ideal :(

> However, still needing to "drop down" to Git every now and again to get something done makes it feel very much like a convenient wrapper on top of Git rather than a new SCM.

Yeah, but I think that's fine. Git is a solid foundation; it's not wrong to try to build on it initially. It gets you a usable implementation faster. It is a pain point, though.

> If Git adds commands to support the JJ workflow, it would be hard to justify having JJ installed any more.

Is that a danger? I don't see Git as a fast-moving target. And among all the "convenient wrappers on top of Git", JJ seems to have the design least tied to Git, so I think it has the highest chance of breaking out of the Git dependency in the future.


> That last part goes further than git rebase --update-refs, which only moves refs sitting inside the range you’re actively rebasing. git history instead finds and rewrites every local branch descended from the commit (while also having an option to limit it to only the current branch).

I'm reading that to mean that when I use `git rebase --update-refs` in this situation, where I've currently checked out `D` and update `B` to `B'`:

  A ──► B ──► C ──► D  
        │              
        └───► E        
I'll end up with this state, where `E` remains untouched?

  A ──► B' ─► C' ─► D'  
  │                   
  └───► B ──► E        
(EDIT: Originally I had `E` point to `B'`, which doesn't make sense)

If I use `git history fixup`, it would also update `E` and end up with this?

  A ──► B' ─► C' ─► D'  
        │              
        └───► E'        
If that's the case, is there a way to get `git rebase` to have the same behavior? I've got decades of `git rebase` burned into my fingers at this point.

> If that's the case, is there a way to get `git rebase` to have the same behavior? I've got decades of `git rebase` burned into my fingers at this point.

One of the GitHub blog posts of git changelog highlights implied that this new power of `git history` was certainly under discussion for a possible feature addition to `git rebase` but it is partly still a discussion because it is a higher-level function and if the goal is to keep `git history` the "high level" and `git rebase` the "low level" then such features probably make sense to remain only in `git history` for now.

It also sounds like there are camps thinking `git history` is only the learning path to make a better `git rebase` UX and eventually the commands will reconverge somehow.

Both sides of that conversation sound worth exploring to me, and I'm curious to hear how that conversation continues.


> I'll end up with this state, where `E` remains untouched?

Can't, because a commit's hash takes into account the parent hashes.

Haven't used --update-refs, but reading it, it should result in your third graph. So,

> is there a way to get `git rebase` to have the same behavior?

is already the case.


> Haven't used --update-refs, but reading it, it should result in your third graph.

I don't think it does. I tried locally:

  mkdir test; cd test; git init
  touch a; git add a; git commit -m 'a'
  touch b; git add b; git commit -m 'b'
  touch c; git add c; git commit -m 'c'
  git checkout -b branched-feature HEAD~
  touch d; git add d; git commit -m 'd'
  git checkout main
  echo 'change' > b; git add b; git commit -m 'fix b'
  git rebase -i --root --update-refs
And ended up with this graph (`git log --graph --all`)

  * commit (HEAD -> main)
  |
  |     c
  |
  * commit 
  |
  |     b
  |
  | * commit (branched-feature)
  | |
  | |     d
  | |
  | * commit
  |/  
  |       b
  |
  * commit 
  
        a
Replacing the `git commit -m 'fix b'; git rebase -i --root --update-refs` with `git history fixup HEAD~` produces what I'd like:

  * commit (branched-feature)
  |
  |     d
  |
  | * commit (HEAD -> main)
  |/
  |       c
  |
  * commit
  |
  |     b
  |
  * commit 
  
        a

Huh.. That's a shame :(. Maybe what it refers to is if you had a branch on B rather than D, it might update that.

EDIT: Yeah, this seems to be it. `git branch b` on b, then `git rebase -i --update-ref @~3` from main caused branch ref `b` to move from d86229e to 02fcaf7:

  * 1e354fb (HEAD -> main) fix b
  * 40e6f70 c
  * 02fcaf7 (b) b
  | * f4188e0 (branched-feature) d
  | * d86229e b
  |/  
  * 5fe78fa a

> And ended up with this graph (`git log --graph --all`)

Add --oneline to get the compact version from the other reply.


It won't result in the third graph on its own as E isn't reachable from D, but it could if you first merged D and E together and then used --update-refs with --rebase-merges. You could then just discard the merged branch and only take care of D' and E' on their own (and since you don't care about the merged branch, you don't have to care about resolving conflicts while preparing it either).

Unless E remained untouched because it was not rewritten, and ended up staying parented on B instead of getting reparented onto B'.

Which is usually not what you want; most of the time you want E', which is E reparented onto B'. But sometimes you want E to remain untouched and stay parented on the original B. Depends on the situation.


Exactly, but the second graph where untouched E is reparented to B' is not something that git would allow.

Ah, good catch. That's more of a graphic issue and not what I was trying to express. Updated the OP.


I'm not sure that answers my question. That shows a linear set of branches (my-feature-v3 depends on my-feature-v2 depends on my-feature-v1 depends on main). I'm asking about the case where two or more branches fork from a common ancestor and you want to fix the common ancestor.

off-topic, but that is a very clear readable comment you left with those line-drawing characters.

You can thank ASCIIFlow for that ;)

https://asciiflow.com/


`git history split` is really going to help me help juniors break up their large PRs into smaller more concise changes. If only it had the option to split an entire branch in two easily.

Maybe the issue is they think of a PR as an expensive thing. Would be best if they could just do the small thing and make a PR of that from the get-go. If they want to base future changes on the ones they just did, they can just create a new feature branch from right there, and just not create the PR of the second feature until the first is merged, or create it and add a note to the reviewer that it includes the changes of the other PR so they should review that one first. Should they want to add changes to the first feature, they just need to checkout that branch, do the changes, and merge to the second feature branch so they're available there.

It's expensive when you factor in each PR having to wait for someone to come and review it - it's easier to get your work done in larger PRs when that's the case, although it's a perverse incentive

Even with low review turnaround, having each PR get to a defined "goal" makes it clear if the early commits that prepare the co-debase for the goal are going in the right direction. If they were submitted as individual PRs the reviewer can only check that they don't break things but has to trust the submitter that the design is right for what will follow.

easy solution is to design ahead of PR time

I mean, that's a positive feedback loop. It takes longer to review, because they're expecting them to be big. Big PRs/MRs are left there until there's a big enough free time block to tackle and discuss. If the PRs are small, obvious things, they can be merged in minutes.

(Depending on what the PR's about and how the team organizes. Ideally, there's consensus on the goals or who's responsible for what. Reviews can be limited to finding bugs and e.g. organizational problems)


Small stacked PRs are a NIGHTMARE unless your org is consistently turning around PRs in low single digit hours, and team members are working on decoupled code so you don't have any stack weaving. You end up in a situation where the engineering director and some tech leads really like tools like Graphite, while the entire team working under them mutters irately under their breath daily.

What does it mean to split a branch in two?

Probably to take a series of commits and decide "this one goes on branch A, this one on branch B", e.g. if you intermingled fixing bug A and B in the same branch, you could more easily go through and assign each commit to a new branch.

The existing workflow for that would be (there are several possible workflows, but this is what I would do):

  git checkout intermingled-branch
  git branch bugfix-A
  git branch bugfix-B
  git checkout bugfix-A
  git rebase -i
  # Edit the file, keep commits that fix bug A, drop commits that fix bug B
  git push origin bugfix-A:bugfix-A
  git checkout bugfix-B
  git rebase -i
  # Edit the file, keep commits that fix bug B, drop commits that fix bug A
  git push origin bugfix-B:bugfix-B

Fun fact, you can also automate more of that existing workflow with the rebase interactive TODO list. Roughly something like:

    pick $ACOMMIT1
    edit $ABMIXEDCOMMIT # split out A parts
    pick $ACOMMIT2
    exec make test # test everything compiles
    update-ref bugfix/A
    exec git switch --detach main # new branch point
    pick $BCOMMIT1
    edit $ABMIXEDCOMMIT # split out B parts
    pick $BCOMMIT2
    exec make test
    update-ref bugfix/B
    exec git merge bugfix/A
    pick $FOLLOWUPCOMMIT
    exec make test
    update-ref intermingled-branch # or bugfix/C
It's been a while since I've attempted a rebase TODO list that wild, so I may have forgotten something, but rebase interactive is pretty wild what it will let you try to automate.

So git cherry-pick?

> Every 3 months, for the last 1.5 years, I try it out for a few days, really trying to make it part of my workflow but eventually I give up and go back to git.1

That's not really trying, let's assume you need 1 week to change ingrained workflows, so you just waste time every 3 months stopping right before the threshold. Better try once every year, but for a few*4 days...


> scary rebase -i commands that can leave your tree in a half-broken state if you so much as sneeze

Complete bullshit. Might as well say "scary screwdriver that can turn up in your recturm if you so much as sneeze". Sure, I believe that might have happened to you (heard stories from friends that work in the emergency room), but I run rebase -i 10 times during the time I work on each branch (literally: I don't even care to write "proper" commit messages while writing the code, will reorganize and clean them up later anyway), and it never happened to me.


> commit messages while writing the code, will reorganize and clean them up later anyway

That's just too primitive of a workflow, so repeating it will not run into the risk. You need to add an actual conflict in some feature branch and get stuck being forced to resolve it to compete the rebase to appreciate the op warning


I like to be like an accountant. No editing history. Create a new "journal entry" (i.e. commit) to fix.

Commit graph is just a data structure. Sometimes it represents a "history", sometimes other things.

Personally, I like it when project's repository represents the history of the project rather than the history of random things developers do on their machines, but you do you.


I am thinking of remote. Edits before pushing OK with me (they ate equivalent of recloning and redoing anyway)

Remotes aren't equal either. Sometimes the remote is my other machine, sometimes it's a fork on a forge used for producing CI artifacts.

It's a good rule of thumb to consider shared branches to be append-only, but not every remote branch is "shared" and, as with any proper rule of thumb, you can always find exceptions.


Ok I amend to what I think I really mean: don't edit history for anything deployed to prod (or released to public or customers) already.

There is history that has already happened (e.g. what has been merged to master) and history that will happen (e.g. unmerged pull requests).

You probably shouldn’t be committing things that are broken…

You should be committing things that are broken all the time. Git works great as a persistent navigatible undo buffer, and you should commit every time you're in a state that you might want to return to.

That breaks git bisect.

Squash all your temporary commits before you push.

This is likely saying you shouldn't save a word document that contains spelling mistakes.

With tools like GitHub Actions and some added constraints, it's not always possible. You literally need a commit to trigger the CI workflow and it starts to trash your branch. Besides, aren't we all familiar with git commit -m "typo"?

> With tools like GitHub Actions and some added constraints, it's not always possible. You literally need a commit to trigger the CI workflow and it starts to trash your branch

I'm with you on your overall point, but as a side note: this state of affairs is so tragic. It's been normalized for there to be test suites that nobody can run except the CI runner, and the only real way to run all the tests is to have GitHub do it. It's pure lock-in, and it's very sad that everything's moved this way.

CI actions should be a simple file of "action: command" lines, with at most a separate file that describes when given actions should run. All actions should be runnable locally just as easily as they are in CI. Or if the actions are so complicated they need a complex-ish environment, that environment should be the kind of thing you could run locally too with your hypervisor of choice, without having to think about it. But every VCS host that has an actions/pipelines product is financially incentivized to encourage you not to set things up this way, to preserve their lock-in.


Commit yes you absolutely can. Merge to trunk? Probably not, depends on your strategy.

> scary rebase -i commands that can leave your tree in a half-broken state if you so much as sneeze

I'm glad to hear it, I thought that was just me. It gets especially hairy when moving commits around...

Plus I have 3-way diffs enabled and I usually get confused by which section is which at least once a day.

Also: does anyone know if `magit` has history support?


Not as such, but out of the box you get reword and fixup as first-class rebase macros. I use them all the time.

If you wanted to split a commit you'd still have to use a magit-assisted rebase.

So if magit added explicit 'history' support I just about wouldn't notice, it would mostly be under the hood.


100% agree, the fixup macro is amazing. It even fails gracefully when unable to commit for whatever reason.

The split one would be handy though.


Cool, didn't know about `history fixup`! I only ever did the manual dance with `rebase -i` and `rebase` for all other branches. That and `history split` will sure come handy!

What happens with a conflicting fix though? Do I still need to resolve all conflicts on the base branch and `reset --hard` all other branches?


git reflog is probably the Git command that has saved me the most stress. At some point you stop worrying about experimenting because you realize it's surprisingly hard to lose work permanently. Git is muuch more forgiving than it first appears.

Granted, I have a perspective of a game dev, but this kind of repo defiling just gives me the willies. IMO instead of finding a common ancestor and altering it, just make the desired change upstream and merge it to where it’s needed. If you can’t do that then you have already made a deal with the devil and might reconsider your approach. KISS

I like `git history reword`, would've found that useful in the past (e.g. fixing a bad typo in an older commit).

It sounds like there was probably an equivalent using an existing command but this is easier for me to understand. Thanks for sharing!


TIL, thx :-)

somewhat related Q:

how do you give two files the same ancestor? so git log will for each show the history to the beginnings of the originally unsplit file? useful for splitting large files.


That's not possible, because files don't have ancestors in git. Each commit's tree is tracked independently and copy/move is just something that's detected via heuristics.

But those heuristics could be better than they are now so that a git log <file> will show the shared history for both copies.

the "reword" option is same as git rebase -i HEAD~n, and then instead of pick replace it with reword to change the commit messsage.

BTW I have made a notion doc on git commands: https://rushilpatel.notion.site/git


These graphics are great. Makes for a skimmable high signal article, nice work!

is there a way to use it with lazygit? unfortunately I'm addicted atp, and "autorebase branches" is exactly what I was missing the entire time

lazygit can already reword

The thing is, when you are rewriting history to change an old commit, you have to solve the little problem that git commit messages refer to hashes such as:

   This regression was caused in <hash> on December 19, 2017.
The fixup and rewrite of history has to ferret out these has references and fix them to the new hash of the same commit.

I have scripted this before!


By the way, the proper job of doing this recognizes abbreviated hashes also and fixes them up to equally abbreviated hashes.

Don't change old commits. Change commits you haven't pushed yet.

That's not for you to dictate to someone about some project you have no idea about.

Git is not a religion or cult.

Whether you're rewriting published or unpublished commits, this particular problem is the same. If the commit series contains self-references---later commits in the series have commit messages which refer to the hashes of earlier commits---those references make no sense at all when the series is pushed upstream; they will refer to garbage objects that exist in your repo only, and which will eventually disappear for you too.


If you think me telling you how to use a product is religious or cult indoctrination, I dont really see how you are getting anything done while dodging the high priests of "man" and "info"

> Working with lots of changes in parallel on git can be painful. You end up juggling branches and commits, and running scary rebase -i commands that can leave your tree in a half-broken state if you so much as sneeze.

I mean, just use stgit[1] to maintain a stack of patches instead of parallel branches, it's so much better. You can easily wrangle thousands of patches (aka commits) with stgit.

A lot of people, on first encountering stgit, read a bit about it, think "why do I need this? I can do the same thing with just plain ol' git.". Yeah, you can. But not at scale. Not practically. Not with thousands of commits. And it makes the case when you just have a few tens of commits absolutely trivial. Try it for awhile and you'll find it indispensible.

[1] https://stacked-git.github.io/


Huh, TIL about `git commit —fixup`.

Yeah I learned that one a few months ago, before that I'd use interactive rebase and shuffle the commits around manually in the editor that opens.

I appreciate that these commands are just utilities / conveniences on top of a stable base though.


All I want to do is reword and correct commit messages / improve them without rebasing everything. When working with teams this is a sure fire way to create absolute chaos.

> Working with lots of changes in parallel on git can be painful. You end up juggling branches and commits, and running scary rebase -i commands that can leave your tree in a half-broken state if you so much as sneeze.

I think that only happens when you work on code as text files (i.e character streams) instead of code (i.e structured content with meaning). Like you have commit A and commit B that is in conflict, you should be glad because that's a rough signal that the intent of A and B differs. Your goal should be to think about how to compose A and B so that both intent survives (unless one supersedes the other). Which means you should be at least familiar with A and B.

The issue I found with people that fears conflict is that they often don't understand either A or B (or both). So they are a bad candidate to actually do the operation. It's not a matter of git's cli interface, it's a matter of codebase comprehension and how well you're familiar with the changes in question.


Git is the one treating code like a text file instead of code.

I pull out difftastic when it’s all too hard for this. Diffs based off tree sitter

Don't forget mergiraf, for tree-sitter-based conflict resolution:

https://mergiraf.org/


I absolutely love the images on the landing page. "Inappropriate use of blame, push and pull to resolve a conflict" is pure genius. Even the name had a lot of thought put into it (didn't know about "Giraffe language" before). Beautiful! :)

The documentation is truly a thing of beauty.

As someone who learned a lot from Non-Violent Communication, discovering mergiraf went something like

  > ...oh, cool - I've wanted this for years, great that someone buckled down and wrote it.
  >
  > Why 'mergiraf,' though? 
  >
  > [Sees NVC link on the site] 
  >
  > ...OH. Conflict resolution through understanding. That's absolutely brilliant, at so many levels.

Git store whole files. The diff and the merge algorithm works by line by default, but that's because line is a rough unit of code (statement, expression, and definition happens mostly within one line).

I can fully understand a conflict, know it's coming, and fear it anyway because I'll have to deal with Git's behavior to fix it.

Such as?

When you merge a commit in that changed a file that has been already changed since the common ancestor, Git runs a tool of your choice on this file. If the tool fails, it marks the file as needing a merge and doesn't let you commit it until you unmark it to confirm that you have merged it manually. In case of octopus merges, it will just abort early. That's basically its whole behavior when it comes to conflicts.


Such as moving down a fixup during interactive rebase when it's going to conflict with parents, or I've added more commits mid-rebase, or the rebase started in a tool and now I need to think about the tool's command and understand how many times the rebase point is view is backwards to interpret which is "ours" and which is "theirs" and whether it's the tool, my editor, or my own experience I should ignore because it's going to mislead me.

None of that word salad should matter, but it does. Git will ruin everything with glee and there is always an excuse for why that's fine and it's the user's fault.


There's nothing "backwards" in "rebase point of view". It's just automated cherry-picking - it's like applying patches, it's as forwards as it gets. People who think it's "backwards" usually just have holes in their mental model of the repository. And yes, I find tools doing what you're asking them to do to be rather fine. In fact, Git makes it easy to notice when you're adding commits mid-rebase, which is something I often do intentionally, as it tells you what the HEAD is (and even shows a detailed state of the in-progress rebase) while authoring the commit message.

I understand how rebase works and use it many times a day. Thanks for trying to explain but I won't respond further.

These aren't troubles of someone who understands. If anyone else reading this has these kinds of troubles too - don't worry, understanding abstractions takes time and effort, you'll get there eventually (not if you'll keep blaming others though).

Let me eat the crow of getting drawn back in and cap this conversation with a summary. I think this is important because criticizing Git's UX is always like this.

OP: No one should be worried about using Git to do a thing.

bulatb: I'm worried it will be unpleasant.

seba_dos1: Here is what will happen when you do the thing.

bulatb: I know, but doing it will be unpleasant.

seba_dos1: You must not understand what's going to happen.

bulatb: I do. The process is unpleasant.

seba_dos1: You must not understand what's going to happen.


Had you actually read what I wrote, you would rather summarize it as "Git is showing exactly what's going to happen when you attempt to do it, all you have to do to make is pleasant is to not ignore it". Reading things requires unpleasant effort though, I get it.

I'm sorry (like actually sorry) if I'm getting into counter-condescension here, but, like...

> all you have to do to make is pleasant

This is an assumption about what I find unpleasant and why. I take it you think "reading information" and "understanding instructions" are some of those things I don't like. Your conclusion that I must not understand is based on that assumption.

The assumption is wrong.

If you can grant me that, my problem looks different. If you can't or won't, my summary was right.


If the assumption was wrong, then your first comment was pretty much useless, and the second one didn't help either as it just describes doing things carelessly and without proper understanding. So if that reading of it is wrong, then it was just complaining for complaining's sake and not about Git at all, but rather about what you happen to find scary for some undisclosed reason?

People do have genuine troubles with these behaviors when they don't grasp what's going on well enough. These troubles go away pretty much entirely once they do. I think explaining them is the only thing one could write in such thread that will be useful to anyone, as the Git's UX already does make a pretty good effort to help the user orient themselves in these cases and has been visibly improving in this regard over time.


My point to the OP is the same thing I'm saying to you. "I understand the thing and think it's fine" does not imply that nobody who understands the thing can think it isn't fine, or anyone who doesn't like the thing just doesn't understand it.

It's not some deep or interesting idea, but it needs to be repeated any time "git gud" is used as an excuse for Git's UX, which is always.


So you do just want to complain and simply choose to ignore what I mention about how Git UX makes sure the user has everything they need to avoid these pitfalls. OK, I'm afraid this is just wasting everyone's time. I'm not excusing anything, to the contrary, I'm showing a concrete example of a good UX and point out that there's been more and more stuff like this showing up in Git in the recent years, as it wasn't always like this. But I get it - complaining about Git is basically free karma, so why not just show up and state "I don't like it" or "it ruins everything with glee"?...

> but it needs to be repeated any time "git gud" is used as an excuse for Git's UX, which is always.

It's uncanny how a completely different community can have discussions this similar. I wonder if somebody wrote an article like "Git is Dark Souls of VCS, here's 10 reasons why". It could become a hit in both communities (poor potential author, would he even live through that?)

EDIT: I'm doomed. The moment I saw ChatGPT generate this: https://klibert.pl/statics/jolly_collaboration.png ("using GUI frontends is to Git veterans like using summons for Souls veterans") I knew I must write that post; the commonalities are just too funny to pass on :)


> which is "ours" and which is "theirs"

"ours" is always HEAD, usually meaning the state of the working_tree, "theirs" is always the commit that is going to change the working_tree.

When merging, you are taking change from another branch (theirs) to create a new commit on the current branch (ours) that ties the two together. When rebasing interactively, you switch to the new base (ours) and replay the changes of the branch (theirs) according to the edit file.

Etymology matters. The conceptual model of git is simple, but people only focus on the operations. That's like trying to learn algorithm and data structures and focusing on the words "insert", "remove", "find", without trying to learn "list", "stack", "tree",... first.

Instead learn about Git's glossary [0], then how the operations use and modify those concepts.

[0] https://git-scm.com/docs/gitglossary


Tldr; use jj



Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: