Chapter 4. Undoing: Fixing Your Mistakes

image

We all make mistakes, right? Humans have been making mistakes since time immemorial, and for a long time, making mistakes was pretty expensive (with punch cards and typewriters, we had to redo the whole thing). The reason was simple—we didn’t have a version control system. But guess what, now we do! Git gives you ample opportunity to undo your mistakes, easily and painlessly. Whether you accidentally added a file to the index, made a typo in a commit message, or made a badly formed commit, Git gives you plenty of levers to pull and buttons to push so that no one will ever know about that little, ahem, “slip-up.”

After this chapter, if you trip up, it won’t matter what kind of mistake you’ve made, you’ll know exactly what to do. So let’s go make some mistakes—and learn how to fix ‘em.

Planning an engagement party

Love is in the air, and we’ve got some news to share with you. Gitanjali and Aref are newly engaged! They want to throw an engagement party with their closest friends, and to make sure they get it right, they’ve decided to hire Trinity, an event planner.

Trinity and her partner Armstrong are true professionals—and huge proponents of Git. All of their ideas for invitation cards, guests, and gift lists are always tucked away in a Git repository that they create specifically for that client. This way, they can always use Git as their second (third, in this case) brain. It’s especially helpful because their clients are known to change their minds a lot.

Trinity had a great conversation with Gitanjali and Aref. She initialized a new Git repository almost as soon as she put the phone down: she wanted to capture her notes about their guest list and gift registry ideas right away. She created two files, guest-list.md and gift-registry.md, and committed them on the master branch.

Her mind was racing with ideas for their invitation card, so she created a file called invitation-card.md and jotted down some ideas, along with a tentative date for the festivities. She committed that as well. This is what her commit history looks like:

image

As you can see, Trinity’s repository contains one branch, master, and the two commits she has made so far. You can find Trinity’s repository, named gitanjali-aref, in the chapter04 folder of the source code for this book.

This engagement party is off to a great start! Now Trinity and Armstrong need to brainstorm some party themes.

An error in judgement

Trinity has just realized something: she made a scheduling error! Gitanjali and Aref suggested July 3 for the engagement party date. There were many things to discuss, so Trinity simply made the change to the invitation-card.md file, and they moved on to other topics.

But July 4 is the U.S’s Independence Day and a bank holiday, full of traffic and picnics. Oops! Trinity called the couple and brought this to their attention. They agreed it probably wasn’t the best weekend for their celebration, so they decided to keep the date they’d originally agreed on. Except they couldn’t remember what the original date was!

Fortunately, Trinity had not committed her changes yet. She used the git diff command to compare the changes in her working directory with the state of the index in the gitanjali-aref repository (which, as we know, contains a copy of the file from her first commit). This is the output she saw when she ran git diff:

Note

If you are wondering how Trinity would have fixed this if she had already committed her changes, worry not! We will see how to fix commits as well in this chapter.

image

Trinity knows what she’s seeing here: git diff, by default, compares the working directory with the index. Here is the state of the invitation-card.md file in the three regions of Git:

image

So how does Trinity recover from this?

Cubicle conversation

Armstrong: Good thing we use Git for all of our ideas. It’s so easy to see what changes we’ve made. Do you want me to just use the output of the git diff command and copy-paste all the changes back to where they were?

Trinity: You could do that, and in this case, it’s only two changes, so it’s certainly possible. But here’s something even better: we can ask Git to undo our change for us.

image

Armstrong: Really? How?

Trinity: Git is our memory store. We already committed invitation-card.md. This means there is a copy of this file in the index and in the object database. We can ask Git to replace the copy in the working directory with the one in the index.

Armstrong: OK, I get that. But how do I get the copy from the index into the working directory?

Trinity: The answer lies in a command called git restore. Here, take a look at the output of git status and see what it’s telling you:

image

Armstrong: Ah! I see. Git is telling us we can supply the file path to the git restore command, and that will discard any changes in the working directory.

Trinity: Yep. git restore is the opposite of git add. It takes the copy of a file in the index and moves it back into the working directory.

Armstrong: Cool! Can we do that now?

Undoing changes to the working directory

Trinity has to fix the undo the changes she made to the working directory, by replacing her changes with the ones in the index. She can use the git restore command, supplying it the path to the file that is to be put back.

image

If all goes well, Git does not report anything. The only way to find out if all went well is to resort to our good friend, git status.

image

Git restores default behavior, as you see, is the exact opposite of the git add command. The add command takes the version of a file in the working directory, and makes a copy of it to the index, overwriting the previous version. The restore command, on the other hand, takes the version of the file stored in the index, and overwrites the version in the working directory.

image

Undoing changes to the index

Trinity had not added the invitation-card.md file to the index. But what if she had? How would she go about restoring her changes?

When a file is added to the index, Git makes a copy of the file in the working directory to the index. This is what the state of the working directory would look like for Trinity if she had added invitation-card.md to the index.

image

The answer lies in the output of git status:

image

Git tells us exactly what we need to do to fix this. We can use the same restore command, except this time we have to give it the --staged flag, followed by the file name, like so:

image

The git restore with the --staged flag is the command we can use to restore files in the index to their previous state. But what does this command actually do? We know git restore (without any flags) replaces the contents of the working directory with the contents held by the index.

When the git restore command is supplied with the --staged flag, Git takes the content of the file in the object database, specifically the contents as they were last recorded in a commit, and overwrites the contents of the file in the index with that content. This is what it looks like:

image

Earlier we discussed that git restore is the opposite of git add—the latter copies the contents of a file from the working directory into the index, the former copies from the index into the working directory. You can think of git restore with the --staged flag as the opposite of the git commit command. The git commit command, as you know, takes the contents of the index and stores them in the object database. The git restore command takes the previously committed contents of a file and overwrites the index with them.

image

Deleting files from Git repositories

“Huh. Well, that’s a first,” Trinity thought, as she read Gitanjali’s email informing her that the engaged couple have decided not to set up a gift registry for their engagement party. Instead, they want to set up a “home fund” that would allow their families and friends to contribute money directly towards their first home.

However, in previous discussions, Gitanjali and Aref did have some ideas for gifts, which Trinity listed in a file called gift-registry.md and committed on her master branch. Here is the list of files in the master branch:

image

Trinity would rather not have superfluous files in her repository, so she needs to delete the gift list. But how?

Git has a command for this—git rm. Just like restore, the git rm command takes the paths of one or more tracked files and removes them from the working directory and the index. To remove the gift-registry.md, Trinity should remove it from the registry:

image

Here is the state of Trinity’s repository after she runs this command:

image

Note that the object database is not affected. The question is—what is the status of the repository after we run the git rm command?

Committing to delete

What does the git rm command really do? Its role is to remove (“rm”) tracked files. After running git rm gift-registry.md, when Trinity lists the files in her working directory, this is what she sees:

image

As you can see, one effect of the git rm command is to delete the file from the working directory. It also removes the file from the index, as git status highlights:

image

The output of the git status command is something you haven’t seen before. It’s telling us that a file that a file was deleted, and that if we are indeed sure that this is what you want, we should commit these changes.

In other words, this commit will record the fact that a previously added and committed file is being deleted! That’s different from what we have done so far in this book, where we have always committed new or edited files.

At this point we can choose to make a commit with an appropriate message, or use our newly acquired restore command to undo the deletion.

There are two things to note here. First, you can only use the git rm command to delete tracked files. If you’ve only added a new file to the working directory, you can just delete it like you would any other file: by moving it to the Trash (Mac) or the Recycle Bin (Windows).

Note

Read that again! You can use the git restore command to get back a file that you just deleted, but haven’t committed yet. Super handy if you make a typo in the file name and accidentally remove the wrong file.

Second, the git rm command only deletes files from the working directory and the index. Versions of the file that were previously committed remain the same in the object database. This is because a commit represents the changes you made at the time of the commit. If a file existed at the time a commit was made, the commit will remember that for as long as the repository exists.

Editing commit messages

The engagement party planning was in full swing, with Gitanjali and Aref bouncing ideas off Trinity. One thing they feel all their friends will enjoy is spending time in nature, so on their last call they suggested a camping party. The plan is simple—everyone can bring a tent and contribute supplies like food and drinks, cooking supplies, and plasticware. They’ll make s’mores over the campfire and celebrate under the stars.

Trinity realized that this was just one of many ideas, so she created a branch in her repository called camping-trip. She started a new file called outdoor-supplies.md to create a checklist of guests and needed supplies. She added the file to the index, then committed it with the commit message “final outdoors plan”.

Trinity knew she had messed up as soon as she hit the Enter key. Gitanjali and Aref were still coming up with ideas and had yet to iron out all the details. They would probably ask for changes or even switch plans altogether, so the commit message “final outdoors plan” seemed a little premature.

Trinity is nothing if not a stickler for details. She is going to have to edit that commit message...

It’s a good thing Trinity caught the bad commit message as soon as she made it. Git allows for editing commit messages, using the git commit command, with a special flag, called --amend.

The first thing to check is that you are on the same branch as the commit you wish to edit. The next thing, and this is super important, is that you want to have a clean working directory. You can verify both of these with our good friend, the git status command.

Next, you can amend the last commit on the branch like so:

image

After this, Git will record a commit, replacing the one you had, except this time it will reflect the new commit message. This commit will have all the same changes as the original commit, including all the same metadata, which includes your name and email, including the same timestamp. In other words, the only thing that is different from the previous commit, and the amended commit is the commit message.

image

Aren’t you the observant one!

When you ask Git to amend a commit, Git pulls a fast one. It essentially looks at the commit you are appending and copies all the changes you made in that commit back into the index. It leaves the original commit as it is. It then runs git commit again, this time with the new commit message, which records the changes put in the index by the commit you are amending.

You see, Git commits are “immutable.” That is, once you create a commit, that version of the commit is preserved. Any edits to the commit (like amending it) will create a new commit that replaces the old commit in your history. Think of it as like writing in pen versus pencil: with a pen, you can cross out your mistakes, but you can’t erase them Immutable commits are one of Git’s biggest strengths, and you should always keep them in the back of your mind.

This is why you should always check the status of your repository before you amend a commit. If by chance you’ve added files to the index and you proceed to amend a commit, all of the files in the index will show up in the new commit. That is, the new commit will record more changes (both the files you had in the index and the files Git added from the amended commit).

As for the commit you amend? Git keeps it around for a while, but will eventually delete it from your repository.

Renaming branches

Trinity finds herself bemused: Gitanjali and Aref have just informed her that their outdoors engagement party isn’t just camping—it’s “glamping,” short for “glamorous camping.” “Glamping” still involves spending time with nature, but with all the comforts of home and then some: electricity, a roof over your head, and some fabulous furniture and decorations.

Trinity is always keen to learn new things, and she wants to get the details right. The branch name “camping-trip” seems incorrect now that she knows about glamping. She’s going to have to rename that branch.

image

There are plenty of reasons you might want to rename branches. Perhaps you don’t like the name “master” and you want to use “main” instead. Perhaps you made a typo in the name of your branch. No matter the reason, we aim to please.

Note

Yep, we hate tpyos too!

In Chapter 2, as you probably recall, you learned that a branch works like a Post-It note that records the name of the branch and the ID of the latest commit on that branch. Creating a branch is simply creating a new “Post-It.”

Renaming the branch is just as easy. Git simply grabs the “Post-It note” that represents the branch and overwrites the name!

To rename a branch, we use the git branch command—except this time, we supply the -m (or --move) flag.

Trinity wants to rename camping-trip to glamping-trip. There are two ways she can go about this.

image

The second option works regardless of what branch you are currently on—even if the branch you are attempting to rename is the current branch. This is why we always prefer using the second option.

Making alternative plans

When Trinity plans big events, she likes to have several ideas in her back pocket, just in case something falls through. Gitanjali and Aref are huge board-game fans with a massive collection of games. The second idea they’ve been throwing around is to celebrate their engagement doing what they love: strategizing, rolling the dice, and bonding with their friends over board games!

To capture this idea, Trinity created a new branch off master, which she called boardgame-night. She next created a file called indoor-party.md and took notes about which games to put on the menu, then committed it. The three of them also discussed potential venues to host the party, which Trinity put in a file called boardgame-venues.md. She also added a note about venue selection to indoor-party.md and made another commit.

Trinity feels good now. Gitanjali and Aref have two strong party ideas—one outdoors event, and one indoors.

image

Turns out there is! And its name is HEAD. You’ve seen HEAD before. In fact, in Chapter 2 we even gave you a rhyme to help you remember what it means. Here it is again:

If you’ve ever used a smartphone with a map application, then you already know what HEAD is. It’s the pin that shows you exactly where you are on the map.

Similarly, if you can visualize your commit history as a series of different timelines (branches), then HEAD marks your current location. Furthermore, HEAD knows about the “stops” you made along the way, also known as commits, so you can use HEAD to reference commits in relation to your current location and even to hop between commits.

The role of HEAD

Every time you switch branches, HEAD moves to reflect the new branch. Consider a hypothetical commit history. Let’s say you are on the master branch, so HEAD points to the latest commit on that branch. When you switch branches, HEAD moves to the new branch:

image

HEAD, like branches, is simply a reference. This difference is that a Git repository can have many branches, but there is only one HEAD. HEAD also serves as the launch point to decide how the commit history will change. In that, the commit that HEAD points to will be the parent of the next commit—it’s how Git knows where to add the new commit in the commit history.

Recall that every time you make a commit on a branch, Git rewrites the branch Post-It to point to the new commit on that branch. Well, there is one more thing that happens—Git moves HEAD to the new commit as well.

image

In Chapter 2, we spoke of merging branches. We referred to the branch you are on as the proposer, and the branch that is being merged in as the proposee. Once you merge the two branches, the proposing branch moves to reflect the merge—in the case of a fast-forward merge, the proposing branch moves to the latest commit on the proposee branch. In the case of a merge that creates a merge commit, again, the proposing branch moves to the to the merge commit that is created. In both cases, HEAD moves as well.

Referencing commits using HEAD

Given that HEAD points to the current commit, you can use HEAD to reference other commits based on where you are. For example, you can specify HEAD’s parent or grandparent commit. Git offers a special operator, the tilde (~), that allows you to do this. Consider this hypothetical commit history:

image

A number n following the tilde operator represents the nth generational ancestor. For example, HEAD~1 references the first parent of the commit you are on. HEAD~2 means the parent of the parent of the commit you are, and so on and so forth.

So how does this help you? Suppose you want to find the difference between the commit you are on and the previous commit, using the Git diff command. Instead of having to look up commit IDs, here is how you would go about doing it:

image

Traversing merge commits

Merge commits, as we discussed in Chapter 2, are special. They have more than one parent. So how do we go about navigating from HEAD to the first parent? Or the second parent? Recall that the first parent is the latest commit on the proposing branch, and the second commit is the latest commit from the proposee branch.

Git offers another operator that works with HEAD: the caret (^), which helps with navigating multiple parents. Take a look to see how that works for this hypothetical commit graph:

image

Like the tilde operator, the caret operator uses a number to figure out which parent of a merge commit you want to reference.

Finally, you can combine the ~ operator and the ^ operator. Here is how HEAD^1~2 would traverse the commit history:

image

Unneeded commits

Turns out, all the scouting Trinity did to find a place to host board-game night was in vain. Gitanjali and Aref have decided it will be much easier to host the party at their home. This way, if the party goes on late into the night, the guests won’t have to drive back home—they can just crash there for the night.

Unfortunately, Trinity has already committed the boardgame-venues.md file (with options for venues) in her repository. She also hinted at a possible venue selection coming soon in the indoor-party.md file she created for boardgame night. Here is Trinity’s commit history:

image

As you can see, Trinity has a commit on the boardgame-night branch that is no longer needed. And now she has to figure out how to get rid of it.

Removing commits

Turns out, Trinity has two options. The first option is to simply move the board-game branch back one commit. If we could do this, all our problems would be gone. Essentially, after moving the branch back, our commit graph would look like this:

image

In other words, we would like to move HEAD to HEAD~1. The command that allows us to do this is the git reset. Git reset can be supplied a reference to a commit, either a commit ID, or using one of the operators we spoke of, namely ~ or ^.

image

The Git reset command has two immediate effects—it moves the HEAD and the branch to the commit you specify. But every commit that you make in a repository records a set of changes—you might have added or removed files, or edited existing files, or both. So what happens to those changes?

Well, that’s the million-dollar question, isn’t it?

The three types of reset

Git has three distinct places where changes can live—the working directory, the index, and the object database. Therefore, the Git reset command offers three options to undo a commit, each option doing different things with regards to the changes that you are undoing, and how it affects each of the three areas of Git.

Bear in mind, that the one thing the Git reset command always does is to move the HEAD and the branch to the commit you specify. The only question we are aiming to answer is, what happens to the changes you had committed? Let’s say your repository had two commits—commit ID B and its parent, A.

image

git reset --soft

The Git reset command can be given the --soft option. This option basically takes the edits you committed and moves them back into the index, and then from the index it copies those changes into the working directory alone.

In other words, the edits you had committed are gone from the object database. It’s like you never made the commit to begin with! But because they are in the index, you’re just one git commit command away from committing them back.

image

git reset (or git reset --mixed)

The Git reset command’s default mode is --mixed, so you can invoke git reset or git reset --mixed with the same results. This is how you would use it:

git reset A OR git reset --mixed A

The --mixed mode does a bit more work than the --soft mode does, and it is made up of two steps.

image
  1. First, it moves the changes in commit B (the commit you are undoing) into the index, and then copies those changes from the index into the working directory, just like --soft mode does.

  2. It then copies the contents of commit A into the index. That is the index now looks exactly like the commit you just reset to.

    image
    image

Contrasting the two: --soft mode leaves both the index and the working directory changed. But --mixed mode only leaves the working directory changed.

git reset --hard

Finally, the third option that reset offers is --hard. Remember, the intent is to undo the changes in a commit. --soft mode moves the changes in the commit you are undoing and puts them in both the index and the working directory. --mixed mode on the other hand, affects the working directory, but the index and the object database look the same. Effectively, --mixed mode takes the changes you had in the commit that you just undid, and makes them appear in the working directory.

Finally, the --hard mode takes what the --mixed mode does to its logical end. In mixed mode, the second step copies the contents of the object database into the index, and stops there.

--hard mode does not. It then takes the contents of the index (which have the changes as they are in commit A), and overwrites the working directory. This means that the object database, index and the working directory all look the same. It’s as if commit B never happened!

image
image
image

Another way to undo commits

When we started talking about undoing commits, we mentioned that Trinity has two options. The first approach is using the git reset command.

However, Git offers us another approach, but before we get to that, let’s take a minute to talk about what a commit is. A commit records a set of changes—you might have edited a bunch of files, maybe added or deleted a few. You can see these changes if you were to use the git diff command comparing a commit with, say it’s parent, as a set of pluses (“+”) and minuses (“-”). This is referred to as the “delta”, or the variation between two commits.

Another approach to undoing a commit is as simple as negating a commit—for every file added, we could delete it, and vice-versa. For every line in every file that was added, we delete it, and for every line that was deleted, bring it back.

image
image

Given that Git can calculate the diff introduced by a commit, it can also calculate the reverse of the diff, or if you like, the “anti-commit”. And we can use this to do “undo” a commit.

Note

If it helps, think of matter and anti-matter coming in contact with each other. End result: complete annihilation!

Reverting commits

You can create “anti-commits” by using the git revert command. The revert command, like the reset command can be given commit ID or a reference to a commit. There is a big difference though—the git revert command is to be given the ID or reference of the commit you wish you undo. Consider our hypothetical repository again—we wish to undo commit B. This is how we would use the git revert command:

image
image

Git looks at the changes introduced in B, and calculates the anti-commit. This is an actual commit that Git will prepare. Now just like any other commit, this commit needs a commit message. So Git will use your preconfigured editor, and bring it up, prompting you to supply a message for the newly created commit:

Note

We have seen this before, in Chapter 2: when we merge two branches, that results in an merge commit. Recall that Git brings up your editor and prompts you for a commit message.

image

We usually prefer to keep the message as is. Once you close the editor, Git will confirm creating a new commit. So what is the effect of a revert? This is what your commit history will look like after a revert:

image

Like the git reset command, the git revert command moves the HEAD, except in this case, we are not erasing commits. Rather, we are adding new commits. However, both commands allow us to “undo” commits.

image

Tonight’s talk: The RESET and REVERT commands answer the question: “Who’s the better undoer?”

The RESET command: The REVERT command:
Look, I have incredible powers. I mean, come on: I have the ability to erase history! I am absolutely what everyone should be using to undo bad commits.

Yeah, but you’re so negative. Going back in time? Really? I am a glass-half-full kind of person—I let folks undo their mistakes by adding to their commit history. This keeps their commit history intact. Not to mention I’m a lot easier for people to wrap their heads around.

So two wrongs make a right, huh? No way! I allow for a clean commit history. If you’ve mistakenly committed some work, why would you want a constant reminder?

Sure. But you are more complicated to use. There’s the “soft” mode, and then there’s the “mixed” mode. Not to mention “hard” mode, which is destructive. People can lose their changes if they accidentally run you in hard mode!

It’s called flexibility! I give folks choices. What do you do? Create a commit that’s the exact opposite of the commit to be undone. Pfft! Our readers could do that manually. So what good are you?

Undoing commits by hand can involve hundreds of files or changes. It’s so tedious, not to mention error-prone. I automate that away. Isn’t that what computers are for?

Fine. Whatever.

I’m just going say one last thing, so listen up: once our readers learn how to use Git as a collaboration tool, they won’t need you. Maybe you should consider another line of work.

Ha! We’ll see about that.
Note

We will be revisiting this topic in future chapters. Stay tuned!

Annnnnd that’s a wrap!

Trinity is so excited for Gitanjali and Aref—all the effort that she put into planning their party paid off. Everyone had a wonderful time celebrating their engagement. She wishes them the very best in their life together.

Trinity’s repository looks clean. She has some cleanup to do, so she merges the boardgame-night branch into the master branch. She also deletes the unmerged glamping-trip branch—God that hurts! That’s a lot of work—but as long as her clients are happy, Trinity’s happy.

Note

We talked about cleaning up your branches in Chapter 2.

Not to mention, Gitanjali now wants Trinity to plan their wedding as well! She wants something really exotic—the South Pole has been mentioned once or twice—Trinity may have to talk her out of that one. Oh well. Time to create another repository ...

image

As for you, well done! It was quite a journey learning how to undo your work in Git. Just remember: Git, for the most part, is not destructive when you undo. In other words, you can undo an undo if you need to, so breathe easy.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.218.147