Moving code around with branches
Several times in the earlier parts, you've seen mysterious references to
HEAD, the current commit. HEAD (almost always) is a pointer to a branch, and a branch is a pointer to a commit. The current branch is the one that HEAD points to. Normally, git updates HEAD as it goes along and it does the right thing. But sometimes, you need to take control and move HEAD yourself.
When you switch between branches, HEAD moves to the new branch. When you make a commit, the current branch moves to the new commit and takes HEAD with it.
git reset moves HEAD to another branch, making it the current commit. It also updates the index and working directory (normally).
Let's say you're in the position shown below. You've been working on
file.txt and made some changes to get to commit
2acad38. You've now done some more work on that file and realised that what you did in that commit was wrong, a blind alley. You could just create another commit based on the current one, but you'd prefer to keep the history a bit cleaner.
reset HEAD back to the previous commit,
3c67455 so that you can commit from there.
$ git reset HEAD~ --mixed
HEAD~ means "the parent of HEAD" and the
mixed mode is the default, so it's assumed if you leave it out.)
After giving that command, the situation looks like this:
HEAD has moved back to its parent. The current branch has also been moved back to the same commit. (Any other branches pointing at
2acad38 would remain there.) The Index has changed to mirror the contents of what is now the current commit. Files in the working directory have remained the same. With a mixed reset, you've not lost any work.
If you now make a new commit from there, you'll end up in this position.
HEAD and the current branch both point to the new commit, and the history of that commit points to
3c67455. From the point of view of the
main branch, commit
2acad38 never existed.
If there is another branch pointing to
2acad38, that commit will hang around. If not, git will eventually get around to removing it from the history, freeing up that bit of space needed to store the commit.
Other types of reset
That's the default, mixed commit. There are also soft and hard commits. All three types move HEAD; they differ on what they do with the Index and working directory. A soft reset does less than the normal one; a hard reset does more.
- Soft commit: preserves Index, preserves working directory
- Mixed commit: changes Index, preserves working directory
- Hard commit: changes Index, changes working directory
Soft resets: changing (private) history
Soft resets are useful when you want to collapse a sequence of commits into one. Let's say you've been working on some feature and have created a series of commits as you've been working. The new feature is complete, but the history of how you got there is messy.
If you then soft reset back to the
3c67455 commit, where the
main branch points, you get this situation, with
feature still being the current branch.
$ git reset --soft main
The soft reset means that the Index contains all the changes in the whole branch. If you create a new commit from here, you get all the changes for the new feature, without the complex history of the work-in-progress.
This isn't the only way of rewriting history: there's a whole process around changing and combining commits, mainly using interactive rebases and the
squash feature. But that's outside the scope of this article.
You can do something similar if you forget to use a branch when you started working on a new feature. Create the branch where you are,
switch to the main branch, then
reset --hard back a few commits. That will leave the feature branch at the tip of the developments, while resetting main back to where it should be. You'll need to do a hard reset so that the working directory is in the state it should be just after the last commit on main. You can then
switch back to the feature branch and continue from there.
Hard resets: erasing mistakes
Hard resets change the working directory, the files on disk. This can be very useful to undo major mistakes. If you're working on a feature and end up making a complete mess of everything, you'll want to reset the working directory to something sensible. You can use
git restore to restore individual files, but it may be easier to just thow away everything and start again.
If there is an earlier commit that you want to return to, you can use
git reset --hard <ID of commit> or
git reset --hard HEAD~ to reset to the previous commit. But often, you want to return to the state just after making the current commit. In that case,
git reset --hard HEAD
will restore the Index and working directory to the state you were in immediately after making the most recent commit.
It's something I've used many a time!
Checkout and detached head
The other command that moves HEAD is called
checkout. Before Git v2.23, the
switch commands didn't exist, and
checkout was used for both these purposes. You are likely to see many references to
checkout in git resources.
The introduction of
switch means that
checkout is no longer used for those functions. Instead,
checkout is reserved for the action of moving HEAD without also moving the current branch.
git checkout <someBranch> does the same thing as
git switch <someBranch>:
<someBranch> becomes the current branch and HEAD moves to point to it. But you can also checkout a specific commit, such as
git checkout 364ed5f. In that case, HEAD points directly at the commit, not at a branch. This is known as a detached head state.
You can work on this commit, make more commits based on it, and so on. But as soon as HEAD moves away from the commit, there will be no reference to it and the commits will be lost. This could be useful for experiments, but it's usually a mistake.
If you get into a detached HEAD situation, the best thing to do is to switch to an existing branch, or create a new branch that points to the current commit. You can do this with
git switch -c newBranch, the same as creating any other branch.
git checkout documentation has more information about the command. I don't want to go into too much detail about it here.
Thanks to Atilla Sztupak for comments on an earlier version of this article.
Cover photo by Scott Tobin