$ 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.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).
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...
`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'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 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.
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.
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.
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.
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.
BTW I have made a notion doc on git commands: https://rushilpatel.notion.site/git
It sounds like there was probably an equivalent using an existing command but this is easier for me to understand. Thanks for sharing!
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?
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.
git commit -am “Changes”
just does not cut it, if you call yourself a professional.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?
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!
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”)
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.
You can't get simpler than that.
Exactly. 50% of the times I type git rebase it is followed by --abort.
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.
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
Hell yes we are! Just because you aren't going to doesn't mean nobody else is going to.
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 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
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.
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'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.
Git gets easier once you understand branches are homeomorphic endofunctors mapping submanifolds of a Hilbert space
https://softwareengineering.stackexchange.com/questions/2564...
If what you said was true, https://ohshitgit.com/ wouldn't exist.
> It's not 1:1 at all.
I don't see any mention of "1:1" in the parent.
The people are confused, it's a fact. So it is confusing. Stop gaslighting people.
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...
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.
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.
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.
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.
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.
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
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 appreciate that these commands are just utilities / conveniences on top of a stable base though.
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.(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)
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.
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.
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.
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.
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.
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 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
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.
--------
[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
I also often wish to edit commits or resolve rebase conflicts or whatever by editing the patch rather than the files.
`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.
(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).
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.
> The UI of git - for better or worse - directly reflects its internals.
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".
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.
Maybe?
/r
Git sucks on so many points that there ain't be enough walls in the world to write our lamentations about it. But it's damn fast. If I have a doubt, I just create a backup branch before messing with the work at stake, and the bottleneck will be the time I take to type the command.
And I don't know if SVN had some advanced mode to do any branch in local, but in my frightening memories full of scarces, one had to synchronize everything to the central repo to do anything.
The split one would be handy though.
> 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.
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".
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.
jj, an alternative to git, gets discussed a lot these days (1, 2, 3, 4) and is often pitched as a solution. While I’m very sold on the problems jj is trying to solve, the way it solves them hasn’t quite hit home with me. 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 where git history comes in. It’s an experimental command that arrived across two releases, 2.54 (April, reword and split subcommands) and 2.55 (June, fixup subcommand). It got a flurry of attention on each release day, and then, as far as I can tell, not much community discussion since. Which is a shame, because IMO it already delivers several of the benefits people tout for jj without needing to switch your whole workflow. And the cool thing is that it’s part of the core git distribution, so you can try it without installing anything.
There are three subcommands: fixup, reword and split.
git history fixup fixes an old commit that has something wrong in it, then autorebases all your branches to match.
You stage the fix as usual with git add, then run git history fixup <commit> to fold those staged changes into the target commit. It’s like a git commit --fixup plus an autosquash rebase but with the extra magic that it also updates any other branch which contained that commit.
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). On the other hand it does not work in the presence of merge commits which, for some usages of git, is going to be a dealbreaker.
Here’s how it works in practice:
Before, with a fix staged for B:
After git history fixup B:
B* is B with the fix folded in. Rewriting a commit gives it a new hash, so C and D are automatically re-created on top as C* and D*, and the feat-1 and feat-2 branch tips move with them.
The most important property, common to all three commands, is that it’s atomic: it never leaves your tree in a half-broken state. It manages this by refusing any operation that could produce a conflict.
To be clear, this is strictly less powerful than jj. jj treats conflicts as first class so it can carry a conflicted state through a rebase and let you sort it out later. git history doesn’t do this yet but the docs leave the door open:
“This limitation is by design as history rewrites are not intended to be stateful operations. The limitation can be lifted once (if) Git learns about first-class conflicts.”
So basically, this limitation may change in the future; excited to see if it does!
git history reword updates the commit message on an old commit and automatically rebases everything on top. This is very useful for going back and fixing commit messages when the design shifts as you iterate.
git history reword <commit> opens your editor with that commit’s existing message. You edit it, save, and the rest of the stack is rebuilt on top with the branches following along. It’s exactly like fixup but for commit messages instead of the tree contents.
Because it only changes a message, reword (like split later) never touches your index or working tree at all; it works purely on the commit graph. So both let you rewrite a commit on a branch you don’t have checked out without disturbing whatever you’re in the middle of.
Before:
After git history reword B:
Only B’s message changes, but that still gives it a new hash, so C is rebuilt on top as C* and feat-1 follows along.
git history split takes one commit and splits it into two, interactively picking what you care about from each. It’s the equivalent of git add -p, but without needing gymnastics with git rebase. I’ve found this to be the most specialized of the three, but invaluable when I need it.
Specifically, git history split <commit> drops you into a hunk-by-hunk prompt over that commit’s diff. The hunks you keep make up the first commit, the rest fall into the second.
Before, with B bundling two unrelated changes:
After git history split B:
B becomes B1 and B2, and C is rebuilt on top of the pair as C*.
Judging by how many people are using jj, I do think there’s still some key mental shift which I’m not yet making. And to be clear, git history doesn’t close the full gap: jj still gives you an operation log with easy undo, models your working copy as a commit, and can carry conflicts through a rebase, none of which this is trying to do.
But for now, git history is a big step forward in adopting many of the pieces that attract people to jj, and it’s already in the tool I use every day. And the way the documentation is written makes me hopeful that more improvements will be coming in upcoming releases!
`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).
great way to encourage people to rebase then!
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.
Edit: thanks guys, that's very useful knowledge! Could have saved me many times in the past
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.
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.
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.
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.
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.
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-BI 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
aThen you are doing it wrong. Sadly this is very common.
> commits around it to figure out why that change was even made.
A commit should be such that the message can articulate the why.
Sorry, couldn't resist. The xzibit was too strong in your comment.
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.
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.
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.
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.
man git-bisectI'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.
(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)
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.
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.
Can't we say that the low level level commands reflect its internal? ...
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.
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.
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.
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.
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.Add --oneline to get the compact version from the other reply.
-<number>, -n <number>, --max-count=<number>
Limit the output to <number> commits.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> Just squash everything before merging and call it a day.
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
First, I agree commits shouldn't store broken snapshots -- if that's what you mean -- code that doesn't compile or outright is _known_ (as opposed to omissions that weren't caught otherwise and made it into the commit) to be broken, not run, produce runtime/type-checking errors, miss dependencies/assets etc. Not everyone would agree -- in our shop people routinely commit "stuff" for reasons they don't disclose that also remain unexplained to me (not having any explanations my way), and then more stuff on top of that. At some point they tag one of these commits as "v2.0.0" triggering automation that releases a package, often broken. Then they scramble for "v2.0.1" etc. I find all of it tedious and unnecessary -- my best practices would rather be to run automation that does checking and sufficient filtering for "bullshit code" as early as possible -- at the "pre-commit" stage, so only whatever passes through that ends up being committed in the first place. If it yet runs into regressions or other problems in production, I fix (re-triggering all the checking again) and release a strictly patch version -- unless fixing was impossible without breaking compatibility in which case it may be a minor or even a major version bump. I do tag commits, too, obviously. But in principle every commit is "production", the difference is not every commit makes up a _release_ (implying to the general public / intended audience).
Regarding rebasing -- that depends on what you mean exactly -- there's different ways to rebase -- you can squash, auto-squash and/or apply fix-ups, or you can just transplant stuff -- when you say "you should rebase" and "patch on current master", what do you mean? Github-driven projects often don't permit pushing `master` without a PR, so a rebase upfront would have been necessary if the author for some reason is not on a branch [other than `master`].
"Actual merging" -- do you mean merge commits as opposed to fast-forward merging which doesn't produce an additional commit? If the latter, what does "merge from history" mean, again? You just need to produce a working snapshot, expressing it with a commit with N parents, there's no more to it. It's fixing of the merge conflict that requires you to understand the graph, the resulting commit is just the period at the end of a long sentence, so to speak.
Anyway, I really missed the point you were trying to express with your first paragraph, I am sorry. Do elaborate, please.
Bisecting also _benefits_ from non-linear "actual development" graphs -- apart from commits that don't feature broken code committed haphazardly for "reasons" (some people use Git as backup, that I know for a fact) -- since they don't hide the work (one of the reasons I prefer raw graphs over squash-merged "polished" stuff).
One thing I should mention in any case is that I do find slicing work into atomic commits often very tedious and counter to what Git is supposed to sell (distributed friction-free concurrent versioning). Because my brain doesn't always work in terms of concepts aligned with Git's -- I routinely may start with one feature fix, on some branch, then discover N related issues, fix those because they're in front of my face then and there, end up with an unreadable diff I can't cleanly add in chunks, etc. None of that is facilitated by Git in any more capacity than just being a scalpel. I don't always want to cut down a tree with a scalpel, obviously.
Pijul, an alternative VCS, uses this nice patch theory guaranteeing you can "apply" commits in any order, compositing your changes essentially -- a feature is just some baseline that is added N commits (in any order, like the mathematical `+` operator). That way you don't have to rebase anything because parenthood doesn't break merging -- if you fix a bug with a patch in Pijul, you can store it once and count on it being applicable to any snapshot made later, even if the result isn't visible -- unlike with Git where rebasing some old commit will abort and ask you to fix a conflict because Git cannot continue.
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.
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.
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.
Just like "never type a profane word into Vim" is ostensibly a statement about how to use Vim, but of no technical value related to Vim use.
It is nothing similar to advice such as using a screwdriver to drive screws and a hammer for nails.
Some people regard the git repository as the object they are working on and want to create the perfect history in it, yet also to ship regularly. That requires shipped history be revised.
It's entirely possible to work productively with an upstream that revises history, producing non-fast-forward changes.
There are things you have to do and know that are not required in FF-only workflows, but nothing too terrible.
"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.
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.
See my reply here https://news.ycombinator.com/item?id=48903456
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.
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.
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 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.
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.
Classic strawman.
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.
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.
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.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.
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.
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.
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.
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.
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.
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.
Just saying, in the common case, the simple approach actually is pretty good.
Unless you're saving every keystroke from your editor you're already squashing history, we're just arguing about degrees.
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?
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.
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.
I am convinced that very few know about git bisect, much less use it regularly.
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'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."
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.
Yes, unfortunately not all companies have a high hiring bar. Some
`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.
This could probably be helpful https://mtlynch.io/code-review-love/
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.
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.
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.
> 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.
Certainly a way to do things. Not the most useful or productive… but it's a way for sure.
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.)
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.
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.
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.
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 :)
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.
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.
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.
> 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.
"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.