Managing Git History

Skylar S
SkyTech
Published in
5 min readMar 25, 2019

If you’re comfortable committing, pushing, and pulling with Git, but you’re a little fuzzy on rebasing, merging, reverting, and resetting, this article is for you. I’m going to go over some git commands, with examples of situations where they would be useful.

Fast forward merges

Running git merge has different results depending on whether the branches you are merging have diverged. If they haven’t, Git will perform a fast-forward merge, which takes the commits from one branch, and simply adds them to the end of the other. This happens all the time: every time I push and there have been no changes to my remote branch, Git is performing a fast-forward merge.

However, say I was on the feature branch, and wanted to merge it to master, but there had been a commit to master since it branched. When I merge, it will add the merge commit to master, but my other commits will remain on the feature branch, and not in masters commit history.

We can see the difference using git log --graph

Here, the second commit was actually made on the feature branch. But because the branches hadn’t diverged, when I merged them, Git didn’t leave a message saying I merged them, but just added my feature commit right on the master branch. This is considered undesirable because it makes it hard to tell which commits originated on a feature branch.

If you want to make sure that your feature commits do not migrate to master, you can use the no fast-forward flag. This helps you log only major changes to the master branch, while smaller changes can be found in feature branches.

git checkout master
git merge feature --no-ff
// will create a merge commit even if fast-forward is possible

Conversely, there is a -ff-only flag that will prevent merge commits from occurring. This is useful if your workflow involves rebasing.

Rebasing and Squashing

On the other hand, there may be times when you don’t want to keep all commits in history, even on the feature branch. Say I was working on a small feature, and I made several commits. I did this to keep a log of the changes that I was making, and to have an up-to-date backup should I make a mistake in my code. However, everything went smoothly, and there’s no need to save so many commits. In that case, I can rebase and squash.

  • Rebasing will help you move commits onto a new branch.
  • Squashing combines commits into larger chunks, so that you don’t pollute the log with a lot of small changes. This allows you to contribute onto the branch of your choosing, as far as the log is concerned, without having to actually do your development on that branch.

Do not rebase on a shared branch. You should never delete commits that someone else may be using. Rebase on personal branches, unless you are planning on deleting the public branch after rebase.

Here’s how you would use rebasing to merge a feature branch back into master:

git checkout featuregit rebase -i master // choose which commits to keep, and which to squash (combine into the next commit) by marking them with pick or squash. ( there's also merge, reword, edit, fixup and  drop)// resolve any conflicts,git rebase -continue // complete the rebase.// at this point, your feature branch is ahead of master. All merge conflicts have been resolved.git checkout mastergit merge feature
// perform a fast-forward merge, transferring your latest commits onto master.

Another use for rebasing is when pulling changes down. If you pass a rebase flag to git pull, it will rebase your commits onto the remote branch. This will save you a merge commit.

git pull --rebase

Undoing Changes — Revert and Reset

But what happens if you need to revise some of your work? Say you accidentally added a bug, and you need to revert to and older version.

If you haven’t committed, and you want to clear your working directory of all changes, use git stash or git reset — hard. They will both clear your working directory, but with git stash, you’ll be able to re-apply the changes using git stash apply; git reset — hard will permanently delete them.

git reset --hard 
// go back in time to your last commit. Erases all changes
git stash
// copy changes into a "clipboard" before removing them. Use git stash apply to restore them.

If you have committed, but you haven’t shared your work with another person, you can use git reset HEAD~1 — hard to go back one commit and throw away your changes. HEAD~1 refers to the last commit on the branch you are on. HEAD~2 goes back two commits, and so on. You can also use commit hashes directly; just use git log to find the number, and type that number instead of HEAD.

git reset --hard HEAD~1 
//go back in time to the commit before your last commit. Erase all changes since then.

Instead of throwing out your buggy code, you may decide to move it to a separate branch. You can do this by performing a git reset (without the — hard flag!) to whatever the last clean commit was, and then branching. Without the hard flag, git reset will delete your new commits, but will keep your changes in your working directory. You can then branch, and re-commit your changes on the new branch. That way, you can work on the bug without breaking the functionality you already have.

git reset HEAD~1 
//Erase the last commit, but keep your files as they are currently.
git checkout -b feature
// move your current files to a separate branch
git add .
git commit -m 'feature with bug'
// commit your working directory to the new branch. //You have now essentially moved your last commit to this new branch. (and amended any uncommitted changes you may have had. To move just the commit, call git stash before resetting.)

If you’ve already shared your work, you should not use git reset to delete your commits, as people may be using them. Use git revert instead.

  • Git revert will make a new commit that does the opposite of whatever the last commit did. In other words, it will undo your changes, but leave the commits in history.

To go back one commit, use git revert HEAD.

To go back multiple commits, however, you’ll need to specify the series of reverts you want to make, using the -no-commit flag, or -n. Here’s how to do so:

git revert --no-commit oldest-commit..HEAD
/or
git revert -n oldest-commit..HEAD

where oldest commit can be something like HEAD~2, or a commit hash. git revert HEAD~2..HEAD, for example, undoes all changes in the last two commits (undoing the last two commits means that your files will be as they were three commits ago, to be clear).

git revert -n HEAD~2..HEAD
// undo the last two commits, but keep them in history

To sum things up, rebasing and resetting a branch will alter its past history, while reverting and merging will not.

Hopefully, you now have a richer understanding of how and when to modify commit history. It’s ultimately up to the team of developers as to what they want their commit history to look like, and what makes the most sense for their project.

Thanks for reading.

--

--