Skip to content

Instantly share code, notes, and snippets.

@Per48edjes
Last active February 27, 2023 19:40
Show Gist options
  • Save Per48edjes/0294722f5f9c13891b505e7ccfe85c26 to your computer and use it in GitHub Desktop.
Save Per48edjes/0294722f5f9c13891b505e7ccfe85c26 to your computer and use it in GitHub Desktop.
[Git Gym] Lesson 2: The 'Undo'

Lesson 2: The 'Undo'

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.

Git: reset vs. checkout vs. restore vs. switch vs. revert

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.

Revert (git revert), the public 'jk'

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

Demo: I Broke Production 😫

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

Checkout (checkout, switch, restore), the most overworked command

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).

Checkout General Behavior

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.

Switching with switch

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.

Restoring with restore

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.

Demo: Surgery on a File

  • 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 (reset --soft, reset --mixed, reset --hard), fear me not

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, whereas git 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 a checkout or restore from another commit or branch will result in potential loss of work.
  • 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:

Reset vs. Checkout Cheatsheet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment