58 comments

[ 2.8 ms ] story [ 108 ms ] thread
> Create a backup branch and push it up to GitHub/Gitlab/etc. This backup branch will have all your work nice and safe in case you need to start over.

The way I teach programmers to do this is to make use of cheap branches and to instead work on a temporary branch, and eventually use ‘git reset --hard branchName’ to turn your original branch into a mirror of the temporary one when ready.

This is a helpful mindset to get into; you can just throw it away in a worst case.

For rebasing specifically I often don’t make a temporary branch. I’ll use the reflog and reset to the commit hash if something gets messed up.

Those “backup” branches really aren’t that for me. They’re just named commits which makes it easier to figure out where I was. But they are sometimes necessary since the reflog isn’t always nice and easy to read.

And pushing that branch to a remote? That’s unnecessary unless you want to delete the whole repo and clone it again in case you “mess it up”. Well, I guess that could be useful in a tutorial.

Bear in mind we’re teaching folks how to rebase - reflog can be an intimidating dive for a first blush discussion
`git reset --hard ORIG_HEAD` is often enough to "undo" a rebase that went haywire.

`git rebase --abort` can be helpful too, if you run into some unexpected merge conflicts along the way.

Is this the same as `merge —squash`?

I’m pretty sure it just creates a single commit on the target branch rather than a merge commit.

Yeah, this is easier ... and broken down ...

Work on a temporary branch, get stuff all working there, keep merging changes elsewhere into the temporary branch to stay up to date with your PR target branch.

Push the temporary branch early and often and don't worry too much about its commit messages (will never be part of official history) except as they pertain to your understanding of the evolution.

Then create a new branch (for PR) off the one you'll eventually PR to, and `merge --squash` your temporary branch into the branch for PR with an awesome commit message. Submit the PR.

Kill your temporary branch when no longer useful.

Modify the strategy a bit if you want to break up the PR into multiple coarse commits ... 100 temporary-branch commits, 3 new-material commits in for-PR branch, and 4 back-merges of PR target branch to for-PR branch into temporary branch. This keeps a full context of your changes vs upstream changes.

I recently started using `git commit --fixup` which I really like. It lets you take the current changes and apply them to a commit which is not the most recent (in a fairly roundabout way). It's good for when I have a stack of diffs and want to make changes to multiple commits at once. (Note this is a pretty advanced technique, if you're not super comfortable with `git rebase -i` I wouldn't bother even trying to understand `--fixup`)

https://fle.github.io/git-tip-keep-your-branch-clean-with-fi...

if you do this often and want some automation, `git absorb` [1] may be worth a look. it will try to find lines that can be unambiguously attributed to a diff based on when the line and its surrounding lines are last modified, and then generate fix-up commits or amend existing ones.

[1]: https://github.com/tummychow/git-absorb

Fixing up merge conflicts multiple times when rebasing is indeed a huge pain (has anyone actually used git rerere? If I ever find myself in this situation I always just squash first instead.)

However I typically squash by just doing a soft `git reset` to the merge-base (the last commit in common with my branch and the one I'm building my work from) and making a new commit. It avoids touching the working tree at all, which avoids breaking my build cache and confusing my IDE.

If I instead want to do a squash-rebase onto the _latest_ of the upstream branch, I do the same as the above, except I `git merge` first (to resolve any merge conflicts in one shot.) Then the merge-base is just the upstream head. This is way easier than doing `rebase -i` and squashing, because you don't have to fix the merge conflicts over and over again for every commit you're trying to squash... just fix the merge conflicts once during the merge, and the "reset and commit" trick is going to create one clean commit in the end anyway.

Man, this is way too complex for me.
The wording might seems complex (I still had to think about what is "upstream" and "merge-base" since I'm not using this terms very often but the main idea the author proposed is quite simple and brilliant if you know "how git works" and what "git reset" does. I don't do it but I like it a lot :) Probably many git users works like this because it all makes sense.

The commands will look like that if you want to just squash your changes: git reset --soft $(git merge-base HEAD origin/master) git commit -m 'squashed commit` and like this if you want to squash and rebase on "upstream" (most likely "master" on "origin" remote), andrewmackrodt already wrote it in another comment: git fetch git merge origin/main git reset --soft origin/main git commit -m 'HN-1337 Awesome new feature'

I like this approach (haven't tested it yet!) because as author wrote it doesn't unnecessary change much files in you current working directory, and IDEs sometimes get crazy when you do rebase.

Currently, I usually try to write my individual commits to be logical changes grouped together and if I see there are some WIP commits or commits that don't look like one logical change, then I just squash this commits (not whole branch). This helps a little in avoiding unnecessary merge conflicts in rebase, but it's not always perfect solution.

I still need to figure out what this "git rerere" magic does though...

If the interim commits are not interesting (which they often aren’t) then it is definitely a lot easier to wrangle a single commit than multiple. This is good for cherry picks as well. Much easier to pick a single commit than a bunch.

I also like the “reset and recommit” approach to squashing in place. It greatly benefits from being easier to explain to coworkers who are less experienced with Git. Reset and especially commit are common operations. Interactive rebase and the text editor that pops up and requires manual editing can be overwhelming for less experienced team members.

I've tried rerere in the past. I just never remember exactly how to enable/disable it as needed, so it seemed like it was either off when I need it or auto-fixing something when I don't want it to.
I use rerere on all git installations, it's a massive time saver. On the very rare occasion I do want to squash a single commit onto the branch HEAD, I use the same workflow as yourself. Using rebase for that workflow as described in the article isn't necessary IMO.

For anyone wondering what that command would look like:

git fetch

git merge origin/main

git reset --soft origin/main

git commit -m 'HN-1337 Awesome new feature'

git rerere is the bees knees. it's the sunshine of my life. it's the silving lining to the clouds of rebasing. it's ... well, it's just the absolute shiznit and if you use git, you should have it enabled always.

Last year, I finally merged an 18 month "long" dev branch that I had kept up to date with rebasing many times during work on that branch. Why 18 months? Some things just take a long time. git rerere made this easy and almost painless.

Semi-related: my favorite dev tool is diff2html

I aliased `diff` to open up my browser showing me the full diff of what I'm working on - super useful!

https://diff2html.xyz/

How does this differ from “git difftool” with something like Meld installed?
10+ years and I still don’t understand why developers and orgs think it’s necessary to squash and rebase.
For me it's a combination of commiting often and keeping the history readable. To achieve both I have to a) squash commits before merging otherwise I end up with dozens of intermediate commits and b) rebase, otherwise I end up with a history that looks like the Tokyo subway system.
Downside of squashing as opposed to making a merge commit (without fast forward) or merging by rebasing, however, is losing granularity. Later it'll be hard to roll back a small part of PR while keeping rest of the changes.

Also it'll be harder to go back and review changes when troubleshooting issues because formatting changes were squashed together with business logic changes. With merges at least there are still original commits available, and it'll be possible to either rollback everything or a single commit.

I could be wrong, I feel like OP is talking about the workflow where the entire branch is squashed before it is integrated into the main branch, not squashing intermediary commits into more logical ones (which is what I think you are talking about).
I've seen two main arguments to that workflow.

One argument, and which I hear often, is that they want to keep the git history clean.

The other argument is that keeping things rebased & squashed allows you to do reverts easily.

I personally don't mind the merge commits cluttering the history, and especially when working on a longer lived branch I will always prefer merging-in changes rather than constantly rebasing and fixing same conflicts ad nauseam.

If you use git-rerere you don't have to fix the same conflicts over and over again
You can do reverts just as easy with merge commits.

git revert -m 1 $commit

You can also visualize a clean history with merge commits.

git log —-first-parent

The only argument I’ve heard that holds weight is that GitHub’s UI squashes the merge commit history into a linear view.

AFAIK it's usually to keep the git history clean but doing it manually is so painful. In my company, we enabled GitHub's "Squash and Merge" option (1) so we have the best of both worlds: a clean history and an 1-click squash process.

(1) https://github.blog/2016-04-01-squash-your-commits/

With GitHub's squash + merge, clean history still depends on the person doing the merge, because when clicking the squash + merge button, GitHub places all commit messages in one large chunk of text, which devs are able to edit before confirming the merge, but it takes some discipline to do so.

In my experience, devs rarely pay attention to this, so commit messages end up as a big list of:

  * Ticket-1234 feature
  * fix
  * cleanups
  * fix
  * another fix
  * now the real fix
Which I wouldn't qualify as a contributor to a "clean" history. It'd be great if GitHub's UX around crafting the commit messages would be more considerate and foster more meaningful commit messages.

(edit: formatting)

There’s nothing particularily clean about a multi-week pull request with 50+ commits getting squashed with all the commit messages concatenated together as bullet points. I’ve tried to go back to commits/issues like that and it’s hard to understand what one section of the commit message corresponds to in the diff.

There’s no way to automate version control.

Is it that you truly do not understand? Or don't agree with common reasoning behind squash/rebase?
About half the time I get stuck during squashing and rebasing I end up trashing everything, re-cloning the repo and just doing one commit with all my changes from the other branch. Much easier than trying to figure out what series of errors or issues git is having.

Fundamentally, this is my fault - I don’t have a good mental model of git, and even when I do have a solid understanding of what I’m attempting to achieve the actual repos/situations I find myself in rarely line up with the examples to any useful degree.

'git reflog' is your friend as it keeps a history of local operations, then you can do a 'git reset --hard HEAD@{X}' where X is the point in your reflog before the rebase started.
While this is true, it's also more of a crutch. You should practice to predict, before your rebase, what conflicts your are gonna hit. And aborting a rebase is a significantly safer option. I would only recommend reflog when you rebased (on) the wrong commit and you want to undo a successful rebase.
Before I rebase, I always create a "backup" branch (`git branch backup`). Then if I mess up, I can always reset back to it (`git reset --hard backup`). Maybe better than trashing and re-cloning :-)
As long as your source and target branch are present on the remote (i.e. github/lab/whatever) you probably don't need to trash the entire repo. Just `git --rebase abort` and if you're halfway through a rebase just delete your local source branch and re-fetch it from the remote. That's my process anyway if I somehow lose track of what I'm doing. Or sometimes as a reference I just open the remote branch in github/lab/whatever directly so I have a little cheat sheet, since the "current/incoming" somehow always confuses me.
You could just preserve the history (= don't use rebase, just merge), it has benefits too in addition to making your immediate life easier and safer.
I do the same unfortunately. I have a cheat sheet text doc called Git Shit that I use to attempt to keep track of the myriad ways to do shit in git. What a lot of people don't understand is that one wrong move and you've LOST ALL YOUR WORK! Which I have done enough times that I take the safe way that you describe a lot of times.
For ardour, we almost never squash commits because we prefer our (always linear) git history to show the actually "story" of a branch's development.

There are two exceptions.

1. branch developer suffered from several "thinkos" along the way, the branch doesn't contain that many changes, and there's simply no benefit to seeing the contrast between the initial (mistaken) changes and the final result.

2. the branch commits create a situation where git bisect can't be properly used because intermediate commits will not compile. Squash is one option here, though it is often preferable to just re-branch and re-work commits to avoid both the bisect problem and the squash.

With a 21 year development history, we have found it invaluable to be able to trace the "thought processes" behind a series of commits, and squash would rob us of that.

I find that forcing developers to record and publish every little mistake they made along the way has bigger downsides than the upsides of being able to follow along with their thought processes as a branch or PR evolves.

Similarly, I don't really care how many times you merged main in the course of developing on a branch. Those commits are just noise to me.

As long as only a single developer is working on a branch at once, I'm very comfortable with them treating commits as a work in progress, then doing an interactive rebase before merging to main to clean things up.

Even in the case where multiple devs are working on a feature branch, as long as they're coordinating properly (and using safeguards such as `git push --force-with-lease`), I find more value in having tidy, organized commits at merge time than a full history of the development process.

You have no way of knowing if a developer squashed locally...
Correct. Rules for this stuff are pointless, but guidelines to generally follow are not.
Did you every see the commit history of someone trying to fix their Azure DevOps pipeline?
Amazing that we don't yet have a source control solution that's simpler than git. It should not be a barrier to entry for our profession.
mercurial is calling you.
If a person dislikes Git then the best they can do now is to use another VCS locally and only interface with Git remotely. Although they might be forced to use a lot of Git features if their boss wants people to actively rebase their own pull requests.
> Did you ever need to do git rebase --continue over and over again just to get your own branch up to date?

Not really. The secret is to rebase fairly often, at the very least daily (depending on the size of the organization you work in, this could be more often). I often see complaints like this from junior devs. They work in isolation on their branch for a week, and then they complain about how hard it is when they try to do a rebase on top of the 100+ commits that happened while they were out there doing their own thing.

Another common issue comes from not understanding how rebase works well enough. You are supposed to adjust just the changes of this commit, not take anything else into account. If you start making unrelated changes when rebasing, your commits will soon conflict with each other, which means you'll run into a lot of very confusing conflicts.

It's fine to squash before a rebase if you are not interested in the history of your branch. But if you are, it's just not an acceptable solution.

Thanks everyone for sharing your workflows! I’m totally linking this thread in the article.

Git history management can be tricky business, and most teams have their own norms, as this discussion shows. What matters most of all is that everyone on your team learns and feels supported with your chosen git processes, including junior devs, and that you have some consensus on it.

For anyone who is overwhelmed by the options and syntax, one of my friends swears by Fork, for those who prefer a GUI over doing git by hand. It has its own learning curve, but works better for some devs.

If I get a PR to review I want the set of commits to be clean and represent logical changes. No “oops” commits.

What’s more, I also want only commits that relate to the feature implemented on the branch. Preferably no merges from the parent branch.

To get zero oops commits requires squashing in the branch. Note that this is independent of whether the branch is merged or squashed when the PR is completed. Squashing 10 branch commits to 3 is the important step. If those 3 are squashed to a single on the target branch is less important and is a tradeoff.

As a developer I’ll not care to maintain build/testable code on feature branches, which is my main reason for usually squashing to the target branch. For larger changes I merge (after cleaning up and interactively rebasing), but git bisect will have to use —first-parent to work.

> If I get a PR to review I want the set of commits to be clean and represent logical changes. No “oops” commits.

+100. Though every time this comes up on HN you would be surprised by how much some people just vehemently disagree.

I will also add another requirement - all unit tests should pass at each commit in the PR. That way, you can use the rebase strategy to merge and you would still be able to bisect.

Passing unit tests on each commit is only a realistic goal if developers can run all tests locally in less than (say) an hour. If the test suite is ten hours and you have ten commits on a branch then it quickly becomes silly to bog down your build servers (or cloud bills) with building and testing the intermediate commits over 90h. Using your own machine to do it (blocking it from doing other work) is obviously not a good idea either.
> If the test suite is ten hours and you have ten commits on a branch then it quickly becomes silly to bog down your build servers

That's a fair point, but I would say that if you encourage your devs to make each commit logical, meaningful and self contained, this is worth the extra cost.

Also, you don't need to run ALL unit tests - just the ones affected by commit you are testing. This of course requires a build system with good dependency analysis.

Having logical commits are a nice-to-have IMO, but I wouldn't be so strict. An oops commit here or there doesn't necessarily throw off one's understanding of the history of what a particular group of commits is intended to do, and there isn't necessarily anything wrong with an oops commit; it's correcting an issue in a previous commit. Besides, it's more common to look at a PR as a whole rather than as individual commits. I know that sometimes you do, but at that point "oops" commits are rather trivial to look at.

What's more important is that the commit history of the main branch remain sane. That's why you might want to squash-merge, since it mostly solves the issue of having too many extraneously-named commits in a place where one indeed may want to examine individual commits.

Honestly, I couldn't give less of a crap what sort of commits someone has on their branch as long as it's not so many that I can't possibly review all of the code.

Don't get me wrong, because I think it's great if you want to maintain that sort of standard in your own work. I don't find it particularly reasonable to expect this of anyone else because it's more of an aesthetic choice than a practical one, not that either are exclusive of the other in this case.

> Preferably no merges from the parent branch.

Yes. Always rebase. Rebase frequently.

Oopses are so trivial to fix though, it’s literally a second’s work for the author to do and costs more seconds in penalty to the reviewer. So I think it’s just a reasonable etiquette to show that you value your colleagues’ time.
Yeah, that's totally fair, and I think I generally agree. Perhaps I just see it as less of a must. I interpreted your point of view as being relatively strict. If there was some sort there was some sort of standard for always squashing oops commits on a software team I was a part of, I'd probably have an issue with it.