Labeling Things and Undoing Things
Overview
Teaching: 20 min
Exercises: 20 minQuestions
How do I label versions in git?
How do I examine different versions and change between them?
How do I roll back to a previous state I want and continue a different way?
Objectives
Understand how to name specific elements for easier access later.
Understand how to get back older versions of your work.
Two of the key elements of version control are being able to get back to a previous version when you want it, and being able to try out different options from whatever point you’d like to revisit.
GitHub makes it easy to look around your version history to see what the entire repository looks like at different points in time.
To use this lesson as an example, the original Software Carpentry edition of this class was designed to be 4 hours long and had 14 lessons in it: https://github.com/dlstrong/git-novice/tree/patch-1/_episodes
However, for the Spring 2018 IT Pro Forum edition, I needed to fit the class into 2 hours, so I simplified the setup based on the availability of the Siebel labs and cut 5 lessons out: https://github.com/dlstrong/git-novice/tree/it-pro-forum-2018/_episodes
This year we have up to 3 hours for the introduction, so I’m adding in new information that wasn’t included in any previous Software Carpentry edition of Git (this page).
In the future, I might want to add this page into a 4-hour edition of the class and take something else out. But the page you see at https://dlstrong.github.io/git-novice only displays the most recent thing in the “gh-pages” branch, meaning that if I want to show the 4-hour edition in the web page instead, I’d need to grab that version out of the repository history and copy it to the newest point in the gh-pages branch again.
At the time of this writing, my version of the lesson is “124 commits ahead and 765 commits behind” the Software Carpentry lesson, based on Steve Bond (biologyguy)’s particular fork rather than the main repository. That’s a lot of differences to go through and compare, if and when I’m trying to find this version later. So I use branching and tagging and releases to mark different editions that I can easily get hold of later.
Each of the versions could be developed independently of each other, or an update to one page could be shared across each of them. For a visual example:
(The original version is yellow; the cut-in-half version is pink. When you add a new blue lesson to the pink version you get the purple version. When you add that blue lesson to a trim of the yellow version, you get green. Gh-pages is the name of the branch that github.io watches for updates to what’s published on the live website, so other versions need to be put in other branches/tags.)
So in order to be able to manage your versions like this, you need to be able to do four things:
- Identify which parts of the history you want to reuse (ideally, they’ll be labeled)
- Accurately get hold of those parts and bring them to your working space
- Update your work
- Put it back in the repository (and ideally label that version as well)
There are several ways of labeling, depending on what you want to identify. Messages and tags stay with a particular commit; branch labels continue forward as progress on that branch proceeds.
So if I’ve just marked the 2018 version of gh-pages with tag ITPF-2018, then start adding new content to the gh-pages branch for this session, the gh-pages branch identity will still apply to the new content but the ITPF-2018 tag will not.
Understanding the label methods will help you navigate your history in order to find the spot in time you want to return to and select the content you want to access.
Activity: Navigate versions in GitHub
In https://github.com/dlstrong/git-novice/tree/gh-pages/_episodes, take a look at some of the different branches. Some of them will return 404 error messages.
- In some circumstances, switching to the 2015.08 branch returns an error message. In other circumstances it doesn’t.
- Can you figure out the pattern?
- Starting from https://github.com/dlstrong/git-novice/tree/gh-pages/_episodes again, under the Branch:gh-pages drop menu, go to the Tags section and pick it-pro-forum-2018.
- What else changed about your view when you chose that tag?
How would you examine the differences between my patch-1 and my gh-pages? (Hint: There’s a Compare link on the right, below the History button.)
How would you examine the differences between my it-pro-forum-2018 and biologyguy’s gh-pages?
- Between Software Carpentry’s instance, biologyguy’s instance, and my instance, how would you identify the difference between what’s called a fork, what’s called a branch, and what’s called a tag? (Bonus points for explaining when and why you would use each.)
Activity: Tags
Let’s create some tags now, then make more changes so that you can grab that label as one of the options for later rolling-back.
On the command line, you can apply tags at any stage of development and to the whole repository or to individual commits within a repository. On GitHub.com, you can only create tags when you create releases, but you can navigate with tags that were created by the command line and pushed to the remote repository.
Tagging your current state
What command would you use to tag things as they are right now?
(Name it something like v0.5.0. The table earlier in the lesson can help.)
Solution:
git tag -a v0.5.0 -m "I think we're halfway to a release"
Tagging a couple of past commits
Sometimes you don’t realize how significant something will be until later on. Let’s say we want to mark a version 0.2.0 as well. Use GitHub interface’s History button to identify two earlier commits. (For example, https://github.com/dlstrong/git-novice/commits/gh-pages/_episodes shows several commits on June 10, 2018 that could be selected. Your version history and hashes will be different, because hashes are calculated based on both the contents of the file and the metadata like the date, time, author, and commit message.)
- Use the clipboard button beside the hash code to copy it.
- On the command line, use
git tag
to apply the tag “v0.2.0” to a commit you selected. (The table above can be used for reference.)Solution:
git tag -a v0.2.0 -m "This may be useful later" <commit hash>
(Substitute the actual hashes, and don’t put angle brackets around them.)
Activity: Discuss GitHub’s preferred tag naming conventions
If you follow a tag naming convention like v1.0.0, GitHub can give you some automatic assistance with making connections between tags, pull requests, issues, and resolutions to issues. This is a good time to discuss those conventions with the professional developers among the teaching crew!
Grabbing a specific version from different levels of storage
Now that we have different ways of labeling things, we need to look at how to get hold of the piece you want in the way you want it. Remember that you have basically 4 different spaces in Git (before considering forks and branches as additional spaces):
Git commands can be used to reset each of these spaces with individual commits, individual tags, and/or individual branches. Here are some of the commands used to reset these spaces:
Command | Scope | Common use cases |
git checkout
|
Commit/branch/tag-level | Switch between branches or inspect old versions |
git checkout
|
File-level | Discard changes in the working directory |
(Advanced) git reset
|
Commit/branch/tag-level | Discard commits in a private branch or throw away uncommited changes - use with caution |
(Advanced) git reset
|
File-level | Unstage a file - use with caution |
git revert
|
Commit/branch/tag-level | Undo commits in a public branch (or undo something you might want to refer to later) |
git revert
|
File-level | (N/A) |
(Slightly modified from the source: https://www.atlassian.com/git/tutorials/resetting-checking-out-and-reverting )
Git checkout
Git checkout can be used on individual files, particular commits, or entire branches. In essence, it’s a “give me this other thing to look at or work with” command. It behaves slightly differently when you use it on an individual file or an entire commit.
To look at a particular version of a specific file, you include the file name (and/or its path as needed for disambiguation) in the command. This brings that version of the file into your working directory:
git checkout <description of which version> <filename>
If you don’t tell it which version of a file you want, it assumes you want to get the version in your staging area:
git checkout file.txt
This will bring the staged version of file.txt
back into your working directory.
(Where’s my HEAD?)
Keeping track of your HEAD is a good idea in several contexts, including both git and the French Revolution.
Most of the time, your git HEAD is going to point at the most recent thing you’ve been working with. Imagine it as the top item in a stack of things on a table, the thing that you’re currently working with. (In this case, we’ll change our mental model from folders to books, because people are used to checking books out from a library, and checkout is one of the commands that moves HEAD around.)
However, when you have a pile of commit-“books” to look through, sometimes you want to look further back in time, or to try out a new line of thought. You may want a way to look at what was in that thing you were working with a week ago without knocking over the rest of the book-pile on your digital desk.
Looking further back in the stack with checkout
If you tell checkout which version you want from your local repository, it will pull from the local repository rather than from the stage. To save commit lookup time, you can count how many versions back from your current HEAD pointer position you’d like to look – for example, HEAD~2 refers to two versions back from the latest commit in your currently active branch, or two books lower in your book-pile.
So if commit C3 is the latest, and commit A1 is two before that, then here’s how to get a copy of the A1 version of file
and put it in your working directory, but leave the rest of your work at its current state:
git checkout HEAD~2 file
Working with entire commits
If you decide that everything you’ve done since the last commit was a bad idea and you want to clear the decks and re-start from your last commit, you’d use this command to copy everything from commit C3 (the latest version and “HEAD” of the line) back into your working directory:
git checkout HEAD
In contrast, if you check out an entire commit that’s older than the latest, you’re probably going to want to name a branch while you do it to avoid developing in a detached HEAD state. (Just take my word for it right now. We’ll translate that into English later.)
If you don’t have a branch already made and you’d like to make a new branch, you use -b to name the branch during the checkout:
git checkout <some-commit> -b <branchname>
In this illustration, we’re getting version A1 by using HEAD~2 and naming the new branch BranchA during the checkout:
git checkout HEAD~2 -b BranchA
Note the red Nope symbol over version C3 in the working directory? Those files will get overwritten with a checkout.
If you want to save your changes temporarily – or if Git gives you a warning that you’ve got changes it doesn’t want to overwrite with a checkout – you can use git stash
to put working directory changes in temporary holding.
If you already have a branch, and you want to update your working directory to work on that branch, the command looks like this:
git checkout <branchname>
In the images below from Atlassian’s tutorial, we’re switching from the master branch to the hotfix branch by using git checkout hotfix
:
When you’re done working with the hotfix branch and have added and committed your change there, you can go back to the main branch by telling it that you want to check out “master” again:
git checkout master
Checking out tags and specific hashes
Remember the version 0.2.0 and 0.5.0 tags we made in the earlier activity? You can check out tagged versions by telling Git that the identifying word you use is a tag name rather than a branch name:
git checkout tags/v0.2.0
or
git checkout tags/itpf2019
You can get a list of tag names in the GitHub.com interface or with the git tag
command on the command line.
(If your tag names and branch names are entirely different, you could also do git checkout v0.2.0
without specifying tags/. However, if you don’t specify what type of label you mean, git checkout
’s first guess is that you mean a branch name.)
If the hash for a particular commit is more readily available than the tag name or how far back in a branch the particular version is, you can also check out via hash code:
git checkout hash/a1b2c3d
or
git checkout a1b2c3d
(Generally speaking, your tag and branch names shouldn’t be the same as a hash, so you probably won’t need to tell git which type of identifier you’re using here.)
Detached heads
In the picture above, you’ll notice that the word HEAD is pointing at the last bubble in each sequence. That means your HEAD is not detached. (This is usually a good idea both in git and in life.)
What that means in English is that git has a branch name associated with the place that you’re working and that branch name will continue to be associated with the most recent changes as you make more commits.
If you’re working under a detached HEAD that’s not pointing at the latest item in a named branch, git won’t know how to connect new commits to the rest of the tree. You can git checkout older versions without branch names (i.e. put yourself in a detached HEAD state) to look around the history, but you’re risking potential confusion if you make any changes from that point.
I’d recommend using GitHub’s history features to explore past states when you can, and checking out into named branches to avoid chaos when you need to make changes starting from past states.
Activity: Checking out an individual file
Let’s say you want to look at a past version of a particular file to see if that version works better for your needs.
- How would you identify which version of the file you want?
- How would you tell git to bring that version back?
Solutions:
On the command line, you can use
git log
andgit diff
to inspect commit history and messages. On GitHub.com, the History and Compare options can help you navigate past versions and compare them to each other.If the version you want is on the stage but not in a commit,
git checkout <filename>
will get the staged version for you. If it’s recent enough to count the commits back,git checkout HEAD~# <filename>
will select the version placed # of commits before the current one. If it’s further than you want to count, or if you have the commit hash ID or a tag handy,git checkout <tag or hash>/<identifier> <filename>
will get that version.
Activity: Check out a tag or a hash
Check out something you’ve tagged, and look around your working directory. Then check out something by using its hash code.
When you’re done, change back to the master branch with:
git checkout master
Solutions:
git checkout tags/<tagname>
and
git checkout hash/<hashcode>
will tell git what type of labeling item you’re looking for.
Activity: Working at two points in time
Let’s say you’re working on a bug fix that isn’t ready to go live yet, but someone needs a patch for something that is live right now.
- How would you save your current progress?
- How would you identify the current live state that you want to roll back to?
- How would you get that live state back for making the fix?
- How would you return to your current project?
Solutions:
(1.) You can use the regular add-commit sequence to add your work to your active branch and make a new branch identity with
git checkout -b branchname
, or usegit stash
to make a temporary copy. (There are more options as well, but these are the two we’ve mentioned so far.)(2.) This depends on how you’ve labeled your releases, whether your master/HEAD is always your current release or whether you use tags, other branches, or the GitHub.com release labeling system for identification. (Labeling your releases will make your life simpler than leaving notes to future-you in commit messages!)
(3.) After using
git status
to make sure you don’t have unsaved changes, you’ll want togit checkout
the version you identified in step 2.If you used GitHub.com’s Release system to make a release with a tag, you’ll want to get that tag identity copied to your local repository before you use it to check out and make a branch for it: \
git fetch --all --tags \ git checkout tags/<tag_name> -b <branch_name>
(4.) After making your changes and committing them to that branch, you can use checkout to change back to your previous branch as well.
If you were working in the master branch before, the command would be:
git checkout master
(If you weren’t using master, substitute the name of the branch you were working in.)
Revert and Reset
Checkout lets you grab other things to work on, and test out different ways of doing things from various points in time. However, it doesn’t try to change the past.
Revert and reset are two different ways of saying “Whoops, that should not have happened at all. Let’s erase that (with varying amounts of eraser marks left behind).”
In general:
-
Reset is complex and powerful; it will be covered in more detail in the Advanced section. Right now, we want to let you know it exists. Reset can apply to individual files or whole commits at different locations, possibly including staged spaces and repositories. \
-
Revert is usually a safer option, because it leaves more visibility into what was done. Revert is particularly useful when that thing needs to be undone in a shared repository (by making a new commit that specifically undoes what a previous commit did). This is important in shared spaces because someone else’s work may depend on a particular commit continuing to exist, even if it’s not the latest commit. (Sometimes the person who needs that past commit can be you!) There is no file-level revert; it only works with whole commits.
SECURITY NOTE: If the thing that needs to be undone involved putting a file containing a password into GitHub.com, the very first thing you should do is change that password on that system, before you do anything else. (There’s no way for ordinary mortals to beat the speed of the bots looking for passwords on GitHub.com.)
The checkout command grabs a copy from that earlier point in time, but git still remembers that the later commits exist. The reset command moves pointers so that git will forget those commits.
Revert
In contrast with reset, revert
leaves all the commits available to the history, but creates a new commit that specifically undoes everything in a named commit (and leaves the rest of the commits in place).
(This is important in a collaborative setting where other people may be relying on some of the commits that a reset
would remove from the timeline.)
Note that unlike reset, which undoes everything between the commits, revert only undoes the commits that you specify. So if you have commits A1, B2, and C3, the command git revert B2
will create a new commit D4 that includes A1 and C3.
git revert B2
or
git revert HEAD~1
If you want to revert everything since commit A1, you need to designate each commit to undo:
git revert B2 C3
or
git revert HEAD~1 HEAD~2
Activity: Fireside Chat with Ed
“Ask Ed Anything” about his experiences with when and how to use different methods of undoing things in real use cases!
Key Points
Tags are human-friendlier ways of identifying a specific commit than hexadecimal codes. A tag stays with its commit and identifies one point in time.
Branches are names that will move forward as you add more commits. Branches identify lines of thought or particular variations.
git checkout
brings specified versions into your working directory for investigation.
git checkout -b
creates a named branch during checkout so that you won’t develop new code from a detached HEAD.
git reset
removes commits from your history - use with caution!
git revert
makes a new commit that counteracts the commits you want to undo.Always write a log message when committing changes.