The major downside of rebase is that even if you don't squash/skip it changes the hash of every commit. This is very problematic when others have ever checked out your branch locally, or made commits that haven't been pushed. It takes greater communication between the team in my experience.
You can also, as a substitute for greater communication, establish a protocol around branch naming and follow it.
What works for us is using the word "release" and "wip" in our branch names. If a branch is a "release" branch, then it is safe to base other work on it. If a branch is a "wip" branch, then it is not safe for merging upstream.
These two are not exclusive. For example, you might have a branch "dev" and a branch "release", and maybe another branch "release-dev-wip" – these all have different properties. "release-dev-wip" is a short-lived merge target for features that might not be completed yet. It is not safe to merge this upstream, unless you've checked with all of your colleagues who merged feature branches to it, and they all certified that it is no longer "Work In Progress."
The key I think to make this protocol work is to distinguish between "feature branches" and "environment branches" – dev is an environment branch, and a permanent one, so it should not be rebased. Feature branches merge back to environment branches, and environment branches are deployable.
You can break this rule, say if someone hotfixes master, which is upstream from dev... but it should probably be an exception to do this, and not a regular occurrence, as many people may have already based their work on "dev" and they will all need to rebase on the new (rebased) dev, in order to get a clean merge later. This is where the communication is not always optional. It might be a better choice, if the hotfix is unavoidable, but the project is large and this type of communication is logistically impossible, to merge in reverse (checkout dev, then merge master – back to dev). It might be ugly, but it's considerate. Either way, there should be a clear protocol and no ambiguity on the matter of whether you have a feature branch or an environment branch when you hold it in your hand. Feature branches represent work, environment branches probably ought to just combine the work, and maybe keep a record of how it was deployed.
The branch "dev-wip" is a temporary environment branch – it might be deployed to a dev environment for example, but you should not expect it to remain permanently in the git history. At some point, perhaps it will be renamed to describe the features it contains, and then rebased and merged back to dev. If you merged your feature to it, you might expect that you will need to keep the feature branch around, so you can rebase it on "dev" or "master" later, and finally merge it back.
The whole branch might not get merged upstream at once. (You can also call it "release-dev-wip" and then, the person who looks at it will know that it may contain some completed features that for some reason were not ready to merge upstream, but perhaps should not be discarded entirely. I personally like to rebase wip branches on their upstream before discarding them, just to be sure I'm not throwing away someone's work that they may have thought they merged.)
Protocol is just a different form of communication that is done up-front. If you decide on a protocol and forget to explain it to your team before you implement it, you will obviously not have solved any problems. It's also important to be clear and confirm understanding, so that you can be sure nobody is imputing meanings that you didn't intend. Some teams might choose to only do prod deploys from the "release" branch, and that anything in the "master" branch must be safe to merge to release and send off to production. You could easily get yourself into trouble if you didn't understand when your team expects to work this way. Some teams might prefer to organize their releases on a "release" branch, and then use Continuous Delivery to trigger prod deploys when the release is merged to master. Other teams might prefer to use a tag for that.
Mostly I think we can all agree that you should not rewrite a commit once it has been tagged, but again, this is not something that is strongly enforced by git, so it may vary from team to team. If a release that was tagged broke prod, it might actually make sense to wipe that tag from history and reroute the master branch around it. I've never seen that, but I think you're right, the most important thing is to communicate with your team so there is no ambiguity around these kinds of expectations.
Being able to completely rewrite a feature branch at will before merging it (or rebasing or cherry picking or whatever) to master lets you go full on XCKD[1] with your commit messages but still compress them down to 1-2 logical "add change 1" "fix bug 2" commits at the end.
Thinking about commit messages mid-flow isn't worth it ;)
Most people do that though because they don't want potentially unbuildable commits on master. And the value of keeping around WIP commits from a feature branch is questionable.
There are three ways to rebase that you should learn, and I only have 5 minutes, but I'll try to sum them up quickly:
rebase -i: This takes a single upstream (parent) commit as an argument and assumes you want to change something, or everything after that. That was the most important part to understand... rebase assumes you have some stable base, and that is the commit that you want to keep. (You can still squash other commits into it, or rewrite the message, but if you want to remove it altogether, you should pick the commit before it as the stable base.)
Say you have a commit where you tried something, and then three commits that follow which shored that up into a working something. You may want to use rebase -i and squash those small commits, to make one good commit. (You might also use "fixup" to erase those commit messages about fixing something, because this time you're just going to deliver one commit that works. As a reviewer, I often tell my coworkers that while history is nice, I don't need to see every little thing you tried if it turned out that simply didn't work. If you want me to review your commit history, you should review it too, and feel free to clean it up first before sending it over to me!)
git rebase [original-base] --onto [new-base]
This is like cherry-pick for branches. This is where `--preserve-merges` really shines. Say you have a branch that you merged another branch into, and you merged that branch into your wip-release branch, and it still isn't finished, so you want to preserve all of that history when you rebase it ahead of master, where someone else has already merged their changes in before you. The tool for this is rebase --onto. Imagine you pruned the tree, leaving the node where your work started "original-base" intact, and grafted the branch containing your work that you trimmed onto "new-base". This almost has the argument semantics of a tool like "mv" -- mv [from-file] [to-file].
Then last but not least, git pull --rebase
Someone has merged into your upstream (say, master) ahead of you, and you don't have any interesting merge history that you want to preserve. You just want to pretend you started your work after they finished theirs, and any empty merge commits you might have accumulated since then can be discarded, because it's really just one feature branch you're working on. (When you get good at working this way, you might actually find that you merge more often simply because it's so easy to discard the merges when they are no longer needed.)
You may want to do this sooner than later, because you suspect their will be a merge conflict with their work, and you want to resolve it now, while their work and their merge is fresh in their head... so you can ask them what they were thinking, and this merge won't be looming over you for the duration of your feature branch. Depending on how you work, feature branches or wip-release branches could live for a duration of several weeks or longer. Pull rebase works like `git pull` in how it resolves merges, except it takes your commits and applies them one at a time, rewriting each commit to resolve the merge. This strategy won't generate any new merge commits.
OK, one more! This one isn't a rebase, but you'll probably want it if you are using all of the above... "merge --no-ff" – once you are an expert at rebasing, you are probably going to miss those clean merge commits, because by default all of your merges are going to be fast-forwards, and there's actually some merit to the idea of having a merge commit that shows "here, a branch was ended." Basically every Github or Bitbucket will do this for you when you merge a Pull Request without choosing the "squash" option. It's handy to have in your command-line toolbox as well. Your co-workers will know that they can base their work off of yours and they can trust that you're probably not going to squash your commits behind that point again, but you can get all of that without the overhead of opening a pull request, and putting it through a formal review.
This does not have the ugly semantics of a regular merge with two parents (that is the merge that everyone complains about, which you maybe cannot readily express as one "diff" against a single parent commit). As long as you use it together with rebase you can have a clear merge history while always maintaining the capability of tracing each commit's lineage to one parent with a single, clear diff. You just get a nice empty merge commit to show that "a merge happened here."
There are many ways to use rebase, and not all of them remove history. Hope this helps!
I did not know about rebase-merges!
> The --rebase-merges mode is similar in spirit to --preserve-merges, but in contrast to that option works well in interactive rebases: commits can be reordered, inserted and dropped at will.
Yeah, I hate rebase, but the git boys (a very clever bunch) seem to like it.
Much less this handwavy commit exposed to uninterested parties.
Although you still fix the conflicts each time, it means you don't have many merge commits as you go along
It's always possible to make smaller commits, and it's always possible to cherry pick onto master regularly. If you're not doing so, you're being lazy.
This style has more benefits than easier merges. Smaller commits are easier to test and less prone to bugs. Also less likely to be affected by context switching.