https://rednafi.com/misc/on_rebasing/ [favicon]Redowan's Reflections * search * tags * archives * about Home >> Misc I kind of like rebasing June 18, 2024 Table of Contents * A few assumptions * Rebasing a feature branch onto the main branch * Rebasing interactively on the feature branch People tend to get pretty passionate about Git workflows on different online forums. Some like to rebase, while others prefer to keep the disorganized records. Some dislike the extra merge commit, while others love to preserve all the historical artifacts. There's merit to both sides of the discussion. That being said, I kind of like rebasing because I'm a messy committer who: * Usually doesn't care for keeping atomic commits^1. * Creates a lot of short commits with messages like "fix" or "wip". * Likes to clean up the untidy commits before sending the branch for peer review. * Prefers a linear history over a forked one so that git log --oneline --graph tells a nice story. Git rebase allows me to squash my disordered commits into a neat little one, which bundles all the changes with passing tests and documentation. Sure, a similar result can be emulated using git merge --squash feat_branch or GitHub's squash-merge feature, but to me, rebasing feels cleaner. Plus, over time, I've subconsciously picked up the tricks to work my way around rebase-related gotchas. Julia Evans explores the pros and cons of rebasing in detail here^2. Also, squashing commits is just one of the many things that you can do with the rebase command. Here, I just wanted to document my daily rebasing workflow where I mostly rename, squash, or fixup commits. A few assumptions# Broadly speaking, there are two common types of rebasing: rebasing a feature branch onto the main branch and interactive rebasing on the feature branch itself. The workflow assumes a usual web service development cadence where: * You'll be working on a feature branch that's forked off of a main branch. * The main branch is protected, and you can't directly push your changes to it. * Once you're done with your feature work, you'll need to create a pull request against the main branch. * After your PR is reviewed and merged onto the main branch, CI automatically deploys it to some staging environment. I'm aware this approach doesn't work for some niches in software development, but it's the one I'm most familiar with, so I'll go with it. Rebasing a feature branch onto the main branch# Let's say I want to start working on a new feature. Here's how I usually go about it: 1. Pull in the latest main with git pull. 2. Fork off a new branch via git switch -c feat_branch. 3. Do the work in feat_branch, and before sending the PR, do interactive rebasing if necessary, and then rebase the feat_branch onto the latest changes of main with: git pull --rebase origin main 4. Push the changes to the remote repository with git push origin HEAD and send a PR against main for review. Here, ...origin HEAD instructs git to push the current branch that HEAD is pointing to. The 3rd step is where I often do interactive rebasing before sending the PR to make my work presentable. The next section will explain that in detail. Occasionally, the 4th step doesn't go as expected, and merge conflicts occur when I run git rebase main from feat_branch. In those cases, I use my editor (VSCode) to fix the conflict, add the changes with git add ., and run git rebase --continue. This completes the rebase operation, and we're ready to push it to the remote. Rebasing interactively on the feature branch# This is an extension of the 3rd step of the previous section. Sometimes, while working on a feature, I quickly make many messy commits and push them to the remote branch. This happens quite frequently when I'm prototyping on a feature or updating something regarding GitHub Actions. In these cases, I tend to make quick changes, commit with a message like "fix" or "ci" and push to remote to see if the CI is passing. However, once I'm done, the commit log on that branch looks like this: git log main..@ --oneline --graph This command instructs git to show only the commits that exist on feat_branch but not on main. I learned recently that in git's context, @ indicates the current branch. Neat, this means I won't need to remember the branch name or do a git branch and then copy the name of the current branch. Running the command returns: * 148934c (HEAD -> feat_branch) ci * e0f6152 ci * 8f4dc4c ci * bf33bf7 ci * 2e3dce6 ci I'm not too proud of the state of this feat_branch and prefer to tidy things up before making a PR against main. One common thing I do is squash all these commits into one and then add a proper commit message. Interactive rebasing allows me to do that. Let's say you want to interactively rebase the 5 commits listed above and squash them. To do so, you can run the following command from the feat_branch: git rebase -i HEAD~5 This will open a file named git-rebase-todo in your default git editor (set via git config) that looks like this: pick 763e178 ci # empty pick 4b10faf ci # empty pick 7f7ce20 ci # empty pick 88fc529 ci # empty pick 8bc19b6 ci # empty # Rebase a2e45d3..8bc19b6 onto a2e45d3 (5 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup [-C | -c] = like "squash" but keep only the previous # commit's log message, unless -C is used, in which case # keep only this commit's message; -c is same as -C but # opens the editor # x, exec = run command (the rest of the line) using shell # b, break = stop here (continue rebase later with 'git rebase --continue') # d, drop = remove commit # l, label