Git doesn't have a CTRL Z equivalent -- it does have a souped up set of commands that give you finer grained control of how you want to backtrack.
It's certainly understandable if distinguishing these commands is just out of
reach or requires a few Google/Stackoverflow searches each time you reach for
any of one of these commands. (I've yet to even mentioned git reset
's
--soft
, --mixed
, and --hard
options!) And, to be honest, it's probably not
a huge deal 99% of the time. However, I would wager that knowing these tools
could make you more efficient as you take on more of a given codebase at a time,
not to mention you will actually, on occasion need these.
Let's try to set the record straight so you are more confident in selecting the right tool for the job.
Reverting is probably the most straightforward action of the bunch. It also is the safest to use when working on a published branch since reverting does not rewrite history. A revert is simply a commit that is the inverse of a previous commit -- there is no operation done at the file-level.
For example, let's say you pushed something to production, your reviewer didn't
catch the bug, and you hit that satisfying green "Squash and Merge" button. To
simply undo all of your work so master
is in a state that resembles the one
before you landed your bug, you would:
git switch master # Checkout master
git pull origin master # Update local master branch
git switch -c quickfix-my_bug # Start a new branch for your revert
git revert --no-commit [bug_commit_SHA] # Try to revert your changes in one commit
git commit -am "Whoops" # Commit your reversion
git push -u # Push your reversion
gh pr create \ # Open PR
--title "The bug is fixed" \
--body "Everything works again"
The above also works for a range of commits:
git revert master~3..master # Remember, `master` is just a pointer
Let's practice with our kimono
👘 repo. We're going to pretend that you are
the offender who recently merged a bug into production. Now it's up to you to
fix it. Further complicating matters is the fact that you don't know what the
problem is exactly (though you do know the PR at fault). To buy yourself some
time and fix the blunder you've introduced, a git revert
could be very useful
to you!
- Figure out which commit introduced the bug
- Pull from
master
and create a new branch -
git revert
your offending commit - Confirm that you undid what you expected to undo (i.e., no more; no less!)
- Push, PR, and breathe a sigh of relief
In August 2019, the makers and maintainers of git decided to split up the
checkout
command into two, experimental commands:
switch
restore
Historically, checkout
was a very confusing command because it could do so
much: switch your HEAD to a different branch or commit, pull older versions of
files into your working tree, and move changes between the index and working
directory (from the first lesson, Git: The Basics).
You can still use checkout
to perform any of the functions switch
or
restore
serve, but it might help to play with the latter two to get a better
feel for what you're actually doing.
switch
...switches branches by popping off the HEAD reference and sticking it
onto the commit or branch we specify. In doing so, git will attempt to do a
"trivial merge" with any work you're carrying in the index and the working tree
into the "new" frame of orientation.
Since branches are just pointers and pointers are just references (to commits),
you can also use switch
to jump back and forth through your git history. With
the -c
flag, git switch -c
is the equivalent of git checkout -b
for
creating branches (as exemplified in the code snippet in the previous section).
Not much else to it; see the docs for more info.
The remaining workload on checkout
falls to restore
, which as the better
naming might lead one to believe, "[restores] specified paths in the working
tree with some contents from a restore source." Well, what's a restore source?
That's for up to decide! It's this ability, to quickly change iterations of
files based on 'saved' versions by commit-ish objects, that makes restore
very
powerful.
Let's practice using --source
to restore a collection (or a single) file from
a different point in our project's history.
- Pick a file to work on that has a long-standing history in your project. Make a handful of changes.
- Decide that you're not happy with some of the changes.
- Use a
git restore
with the appropriate flags to add patches from a past version of the file - Commit what you care about and discard the rest of your changes
Note: If a path is tracked but does not exist in the restore source, it will be
removed to match the source. The command can also be used to restore the
content in the index with --staged
, or restore both the working tree and the
index with --staged --worktree
. By default, if --staged
is given, the contents
are restored from HEAD, otherwise from the index.
reset
is often considered a potentially "dangerous" or "destructive" command,
but knowing how the internals work (even trivially) should absolve you of that
fear. Introducing: the reflog.
git reflog
, in my mind, is sort of like HEAD's diary, chronicling where it
was, what it did, and when it did these things.
Another confusion that often arises is distinguishing reset
from the
checkout
-family of commands, especially when in the middle of your workflow.
The good news is that if you have a decently firm grasp of the three "rails" of
git (see Lesson 1), you should be able to understand the important distinctions
between these "undo"-type commands. The bad news is I don't have a novel, better
way to draw these distinctions aside from linking you to the docs:
Note: You can effectively consider switch
to be the equivalent of checkout
in this context.
Reading this doc and working a few examples of your own will make this come to life and illustrate what's going on. That being said, here's a handful of key points to keep in mind:
git reset
will [generally] move the branch pointer and HEAD, whereasgit checkout
will move HEAD but leave the branch pointer where it is.- The degrees of
reset
determine where (or on which "rail") the states of files and folders being reversed fall. - A heuristic I like to keep in mind is that "Reset isn't commit-safe but is
file-safe; Checkout isn't file-safe, but is commit-safe."
- When operating on a path,
git reset [commit] path
will change the index to the old version (specific by the[commit]
), but the changes to the file will still be available in the working directory. - When
git checkout [commit] -- path
is run, the working directory and index are updated, so you risk losing changes. Git will warn you if the trivial merge it tries to perform during acheckout
orrestore
from another commit or branch will result in potential loss of work.
- When operating on a path,
- Using these commands with the
-p
or--patch
option is incredibly useful for doing more precise, error-mitigating surgery to your files.
While I generally eschew cheatsheets, this is a useful summary of the usage of these two oft conflated commands: