git commit -am “Changes”
just does not cut it, if you call yourself a professional.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.
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!
Then you are doing it wrong. Sadly this is very common.
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.
`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'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.
(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).
I also often wish to edit commits or resolve rebase conflicts or whatever by editing the patch rather than the files.
Sorry, couldn't resist. The xzibit was too strong in your comment.
`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.
Exactly. 50% of the times I type git rebase it is followed by --abort.
-<number>, -n <number>, --max-count=<number>
Limit the output to <number> commits.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.
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.
Simplifying `git rebase -i` is a great idea, but unfortunately, that does not match my workflow.
I always keep an history of my branches. For example, when I develop a feature, the first version of the branch is `myfeature.1`, then `myfeature.2`, and so on. This is useful because I can retrieve an old version when something behaves differently in a newer one. (And no, `git reflog` will not help)
This has saved me multiple times. For example, I can determine that a problem was introduced between `myfeature.58` and `myfeature.59`. If the "feature" contains 15 commits, I do:
git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59
This lets me see the changes between the 2 versions, commit by commit (even if they don't have the same base).I don't want all the branches containing a commit to be rebased automatically (I already try using tags for old versions instead, but this does not quite fit).
I'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.
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.
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.
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?
$ 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.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.
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.
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
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.
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.
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
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.
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.
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.
great way to encourage people to rebase then!
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.
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.
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
The code is all that should matter. Maybe comments for being nice to others and my future self. thats 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.
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 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.
> commits around it to figure out why that change was even made.
A commit should be such that the message can articulate the why.
Edit: thanks guys, that's very useful knowledge! Could have saved me many times in the past
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.
But that's projects like Linux, which is a whole different use case than for example what I do for a living, front-end applications that have at best a lifespan of 10 years and whose individual features / screens / components have less than that, and it's much more rare that an older commit is still relevant today.
(that said, looking history up has made me dislike squash commits. I get it from a pragmatic point of view but it's not ideal)
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
However, it also feels more like a "work log" than a "commit log"; I like to make my commits atomic (test + code at the same time), so that in theory, each individual commit will pass CI. If you separate them, you can't just revert one commit, you need multiple.
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.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.
This is also true when creating with security patches and other things that need to be backported to multiple releases.
Beyond the archival benefits, I've found plenty of bugs by going back through my "wip" commits and creating a sane history from them.
Lemme guess all your for commit say "wip"
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.
Hell yes we are! Just because you aren't going to doesn't mean nobody else is going to.
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.
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...
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'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.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.
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
aEDIT: 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 aAdd --oneline to get the compact version from the other reply.
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.
I appreciate that these commands are just utilities / conveniences on top of a stable base though.
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.
BTW I have made a notion doc on git commands: https://rushilpatel.notion.site/git
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.
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 sounds like there was probably an equivalent using an existing command but this is easier for me to understand. Thanks for sharing!
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.
That's just too primitive of a workflow, so repeating it will not run into the risk. You need to add an actual conflict in some feature branch and get stuck being forced to resolve it to compete the rebase to appreciate the op warning
I 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.
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 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.
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.
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.
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.
> 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.
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.
"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.