I never thought about it and instantly wanted to reply “wait why can’t you do that‽” but now that I thought about it, what would you want the history to look like in that case?
A slightly weird rebase?
A single commit which seemingly copy pasted the entire other branch with no relation to it left behind?
I don’t work with merges, so maybe I’m way off base, but I thought they meant, they’re working on another branch or fork, then merging the base branch into theirs every so often to get the newest changes, and then that creates multiple merge commits, which they can’t squash at the end…?
I’m not sure, about that last part, but the rest, I’ve definitely seen with contributors that didn’t know to work with rebases (and unfortunately we’re on GitHub, which only half-assedly supports working with rebases by default).
Sometimes you really don’t want to look over the commit history of your colleagues. As long as it’s a small feature, a single commit is a pretty good option.
It’s fine if the changes belong in a single commit. Otherwise, an interactive rebase to craft a clean, quality history before merging is much, much better.
Yeah, I’m with you. I mean, git isn’t magic. You “can” squash anything, including a merge commit, by just being at the end result, running git reset <commit you want to be squashed offof> and then running a manual git add and commit there. That’s basically all a squash is.
But what you’ll be left with us a single commit that contains all of the code from the branch you’re squashing and also all the code pulled in from every branch you merged, all written as though it all came from this one commit. And maybe that’s what you want? But it feels like also maybe it’s not?
I’m not a fan of changing history in general. Rebase can also he dangerous.
I think ultimately it’s a matter of scale. Sometimes it can be useful to look into the details of the development of a single feature, but in a large project, that rarely happens. I’m not a fan of squashing, but for large projects, it helps to keep your history manageable.
Rebasing is not dangerous. You can always go back if something is not to your liking.
You don’t rebase shared history, you use rebases to craft a clean, quality commit history for your own branches before merging. If everyone does this, then squashing is unnecessary, because garbage commits don’t exist. It is the far superior way of doing things if you actually care about having good commits.
Keeping a quality history rather than squashing also makes many other git tools much better, such as git blame, git revert, git bisect, and so on.
That doesn’t make sense. There’s a world between “garbage commit” and “fancy new feature” and most of it is irrelevant to anything.
I don’t want git bisect to make me check if “run clang-format” broke anything. I don’t want to revert a feature but leave in unit tests that will fail (or worse, the opposite). I don’t care when git blame tells me “rename X to Y”, I want to see the context that motivated this change.
Squashed commits are atomic, built and tested. Anything in between is whatever reviewers let slip in. It’s easier to check a MR description is well written than 5 commit messages (that might get rebased without you noticing)
Squashed commits are not atomic, unless the MR is so tiny that it logically fits into one commit. This is often not the case, though. It is frequently the case that the overall task requires modifying multiple different systems, which should themselves be their own commits, with tests for changes added to the same commit that makes the change.
A well-crafted MR should tell a story in its commits, with changes proceeding logically from one another.
It seems to me what you are really arguing against is poorly crafted history, which I fully agree is something to be stamped out.
To address the specific commands you mentioned:
That’s… really besides the point of git bisect. It’s binary search to find bad commits. If the selected midway point happens to be a formatting commit (which I’d argue should really be handled by pre-commit hooks anyway) and that commit is broken, it just means that the search proceeds to the midway point between that commit and the known good commit.
You can revert a range of commits, but the point of crafting a quality history is not doing things like having separate commits for tests. Tests belong with the code that it’s testing.
First off, any reasonable usage of git will have ticket/issue numbers in the commits that you can use to look up the full task information. But also, git blame won’t show the rename commit if it’s a separate commit from any changes (which it absolutely should be, due to how git handles renames). And finally, if you care about getting more detail about why the code is the way it is, git blame isn’t even the right tool for the job anyway, git log -L is.
You’re right that there is a risk, that rebasing introduces compile errors or even subtle breakages. The thing is, version control works best, if you keep the number of different versions to a minimum. That means merging back as soon as possible. And rebases simultaneously help with that, but also definitely work best when doing that.
There may be reasons why you cannot merge back quickly, typically organizational reasons why your devs can’t establish close-knit communication to avoid conflicts that way, or just not enough automation in testing. In that case, merges may be the right choice.
But I will always encourage folks to merge back as soon as possible, and if you can bring down the lifetime of feature branches (or ideally eliminate them entirely), then rebases are unlikely to introduces unintended changes and speed you up quite a bit.
Rebasing is dangerous if you rebase shared history. If you rebase a local branch, you have to be aware of how much of that local branch you may already have shared.
On top of that, if you’ve got a lot of commits you’re rebasing in a merge conflict that can become extremely repetitive.
So ideally, you only rebase single commits that you haven’t pushed yet. As long as you do that: always pull main and rebase on top of that before you push single commits, rebasing is fine. But the more you deviate from that, the riskier it becomes.
You don’t share feature branches. So you always know precisely what is shared history: the commit you branched from.
The workflow is branch from shared history, rebase your branch as many times as necessary during development to craft a quality history, then merge back.
I rebase dozens of times a day and have never had a single issue with it.
If you’re bothered by repetitive merge conflicts (which, in my experience, are quite rare if you’re doing things correctly), that’s what git rerere is for.
Rebasing is for crafting a quality history of your own commits (or getting your branch up to date with the trunk). Merging is for integrating your commits with the shared history.
I never thought about it and instantly wanted to reply “wait why can’t you do that‽” but now that I thought about it, what would you want the history to look like in that case? A slightly weird rebase? A single commit which seemingly copy pasted the entire other branch with no relation to it left behind?
I think it doesn’t really make sense. Because you can’t “squash” one commit. squash is taking multiple commits and making them one.
When you do a “squash merge” you are really saying “squash all the commits that are on this branch and not the target” then merge.
So you can’t “squash a merge commit” you need at least one additional commit to squash in.
I don’t work with merges, so maybe I’m way off base, but I thought they meant, they’re working on another branch or fork, then merging the base branch into theirs every so often to get the newest changes, and then that creates multiple merge commits, which they can’t squash at the end…?
I’m not sure, about that last part, but the rest, I’ve definitely seen with contributors that didn’t know to work with rebases (and unfortunately we’re on GitHub, which only half-assedly supports working with rebases by default).
Sometimes you really don’t want to look over the commit history of your colleagues. As long as it’s a small feature, a single commit is a pretty good option.
Rather than:
It’s fine if the changes belong in a single commit. Otherwise, an interactive rebase to craft a clean, quality history before merging is much, much better.
That’s basically my commit history for every repo where I need the pipeline to run to see if everything works.
When I do that I always have a Dev branch that I use as the production branch to run the actual calculations.
When I get something working I merge it off, clean up the history a little bit, rebase main onto it and then rebase de onto main.
Yeah, I’m with you. I mean, git isn’t magic. You “can” squash anything, including a merge commit, by just being at the end result, running
git reset <commit you want to be squashed off of>and then running a manual git add and commit there. That’s basically all a squash is.But what you’ll be left with us a single commit that contains all of the code from the branch you’re squashing and also all the code pulled in from every branch you merged, all written as though it all came from this one commit. And maybe that’s what you want? But it feels like also maybe it’s not?
I’m not a fan of changing history in general. Rebase can also he dangerous.
I think ultimately it’s a matter of scale. Sometimes it can be useful to look into the details of the development of a single feature, but in a large project, that rarely happens. I’m not a fan of squashing, but for large projects, it helps to keep your history manageable.
Rebasing is not dangerous. You can always go back if something is not to your liking.
You don’t rebase shared history, you use rebases to craft a clean, quality commit history for your own branches before merging. If everyone does this, then squashing is unnecessary, because garbage commits don’t exist. It is the far superior way of doing things if you actually care about having good commits.
Keeping a quality history rather than squashing also makes many other git tools much better, such as
git blame,git revert,git bisect, and so on.That doesn’t make sense. There’s a world between “garbage commit” and “fancy new feature” and most of it is irrelevant to anything.
I don’t want git bisect to make me check if “run clang-format” broke anything. I don’t want to revert a feature but leave in unit tests that will fail (or worse, the opposite). I don’t care when git blame tells me “rename X to Y”, I want to see the context that motivated this change.
Squashed commits are atomic, built and tested. Anything in between is whatever reviewers let slip in. It’s easier to check a MR description is well written than 5 commit messages (that might get rebased without you noticing)
Squashed commits are not atomic, unless the MR is so tiny that it logically fits into one commit. This is often not the case, though. It is frequently the case that the overall task requires modifying multiple different systems, which should themselves be their own commits, with tests for changes added to the same commit that makes the change.
A well-crafted MR should tell a story in its commits, with changes proceeding logically from one another.
It seems to me what you are really arguing against is poorly crafted history, which I fully agree is something to be stamped out.
To address the specific commands you mentioned:
git bisect. It’s binary search to find bad commits. If the selected midway point happens to be a formatting commit (which I’d argue should really be handled by pre-commit hooks anyway) and that commit is broken, it just means that the search proceeds to the midway point between that commit and the known good commit.git blamewon’t show the rename commit if it’s a separate commit from any changes (which it absolutely should be, due to how git handles renames). And finally, if you care about getting more detail about why the code is the way it is,git blameisn’t even the right tool for the job anyway,git log -Lis.You’re right that there is a risk, that rebasing introduces compile errors or even subtle breakages. The thing is, version control works best, if you keep the number of different versions to a minimum. That means merging back as soon as possible. And rebases simultaneously help with that, but also definitely work best when doing that.
There may be reasons why you cannot merge back quickly, typically organizational reasons why your devs can’t establish close-knit communication to avoid conflicts that way, or just not enough automation in testing. In that case, merges may be the right choice.
But I will always encourage folks to merge back as soon as possible, and if you can bring down the lifetime of feature branches (or ideally eliminate them entirely), then rebases are unlikely to introduces unintended changes and speed you up quite a bit.
Rebasing is dangerous if you rebase shared history. If you rebase a local branch, you have to be aware of how much of that local branch you may already have shared.
On top of that, if you’ve got a lot of commits you’re rebasing in a merge conflict that can become extremely repetitive.
So ideally, you only rebase single commits that you haven’t pushed yet. As long as you do that: always pull main and rebase on top of that before you push single commits, rebasing is fine. But the more you deviate from that, the riskier it becomes.
You don’t share feature branches. So you always know precisely what is shared history: the commit you branched from.
The workflow is branch from shared history, rebase your branch as many times as necessary during development to craft a quality history, then merge back.
I rebase dozens of times a day and have never had a single issue with it.
If you’re bothered by repetitive merge conflicts (which, in my experience, are quite rare if you’re doing things correctly), that’s what git rerere is for.
Rebasing is for crafting a quality history of your own commits (or getting your branch up to date with the trunk). Merging is for integrating your commits with the shared history.
I often end up squashing all my changes into a single commit, rebase it, and then reset HEAD^ to rewrite some commit history.
Brute force, but better than resolving 10 conflicts in the same file over and over