The Git history command deserves more attention(lalitm.com)
lalitm.com
The Git history command deserves more attention
https://lalitm.com/post/git-history/
150 comments
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.
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.
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.
> It's not 1:1 at all.
I don't see any mention of "1:1" in the parent.
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.
> 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.
`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.
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.
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.
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.
`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.
[deleted]
This is the exact same thing I do, though with the prefix "pre-rebase/"
Do you use git reflog?
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).
No need for that even. Just git reflog and rewind back to any spot your want!
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.
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.
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.
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 "[email protected]"
gpg: Good signature from "Chandler Swift <[email protected]>" [ultimate]
Second commit
03c3f6e gpg: Signature made Mon 13 Jul 2026 10:45:16 PM CDT
gpg: using RSA key FBF32CDBCC134B44FD29B66FA851D929D52FB93F
gpg: issuer "[email protected]"
gpg: Good signature from "Chandler Swift <[email protected]>" [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.
These graphics are great. Makes for a skimmable high signal article, nice work!
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.
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.
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.
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.
> 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
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.
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.
> 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
Yes, unfortunately not all companies have a high hiring bar. Some
> 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.
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.
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.
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.
> If the path that was taken to commit this is full of "oops" and "fix" messages
great way to encourage people to rebase then!
great way to encourage people to rebase then!
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.
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.
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
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
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.
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.
The why for a commit belongs in the message or code comments not in the history.
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 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.
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.
Do you really care if someone forgot to format before committing? They can always squash and push locally if they need to.
> 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.
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.
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.
NGL AI usage is driven by friction in presentation and communication over petty details.
> 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
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.
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.
> 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 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.
This is also true when creating with security patches and other things that need to be backported to multiple releases.
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
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.
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-bisectHow 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
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.
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 sounds like it would break bisect
You can develop with failing tests and reorder them after the code change before publishing your branch.
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.
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.
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"
Of course we do and it helps tremendously when the graph is reasonable.
Disagree. You need small, git bisectable commits. Use stgit to do it and it's nearly effortless.
Might as well go back to the tar.gz days, eh?
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.
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.
Beyond the archival benefits, I've found plenty of bugs by going back through my "wip" commits and creating a sane history from them.
That is like saying once the program is done, throw away the source.
Lemme guess all your for commit say "wip"
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.
> 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.
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.
> 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.
Hell yes we are! Just because you aren't going to doesn't mean nobody else is going to.
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.
> 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...
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...
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.
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.
`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
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):
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> 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'`:
If I use `git history fixup`, it would also update `E` and end up with this?
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.> 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.
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.
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).
> Haven't used --update-refs, but reading it, it should result in your third graph.
I don't think it does. I tried locally:
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
aHuh.. 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:
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.
Add --oneline to get the compact version from the other reply.
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.
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.
In that case,
> when I use `git rebase --update-refs` [...] I'll end up with this state, where `E` remains untouched?
would be backwards. The second graph would be what you'd get if you don't use --update-refs. The third graph would be what you'd get when you do use it. At least, according to my reading:
> --update-refs Automatically force-update any branches that point to commits that are being rebased. Any branches that are checked out in a worktree are not updated in this way.
EDIT: This is wrong. Actual behavior in https://news.ycombinator.com/item?id=48901607
> when I use `git rebase --update-refs` [...] I'll end up with this state, where `E` remains untouched?
would be backwards. The second graph would be what you'd get if you don't use --update-refs. The third graph would be what you'd get when you do use it. At least, according to my reading:
> --update-refs Automatically force-update any branches that point to commits that are being rebased. Any branches that are checked out in a worktree are not updated in this way.
EDIT: This is wrong. Actual behavior in https://news.ycombinator.com/item?id=48901607
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/
https://asciiflow.com/
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.
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
BTW I have made a notion doc on git commands: https://rushilpatel.notion.site/git
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.
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.
> 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.
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
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
Great write-up. `git log` really is underrated—learning the flags like --oneline, --graph, -p, and -S turns it into a powerful debugging and code-archaeology tool.
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 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.
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.
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.
That's why you always rebase instead of merge
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.
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"?
This is likely saying you shouldn't save a word document that contains spelling mistakes.
Commit yes you absolutely can. Merge to trunk? Probably not, depends on your strategy.
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!
It sounds like there was probably an equivalent using an existing command but this is easier for me to understand. Thanks for sharing!
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:
I have scripted this before!
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!
> 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/
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/
> 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.
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/
https://mergiraf.org/
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.
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.
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.
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.
> 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.
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.
Let's agree some people won't be reached.
> 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
"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
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.