diff --git a/book/02-git-basics/1-git-basics.asc b/book/02-git-basics/1-git-basics.asc index 850e36562..555bf00e3 100644 --- a/book/02-git-basics/1-git-basics.asc +++ b/book/02-git-basics/1-git-basics.asc @@ -76,7 +76,7 @@ You need to make some changes and commit snapshots of those changes into your re Remember that each file in your working directory can be in one of two states: tracked or untracked. Tracked files are files that were in the last snapshot; they can be unmodified, modified, or staged. -Untracked files are everything else - any files in your working directory that were not in your last snapshot and are not in your staging area. +Untracked files are everything else – any files in your working directory that were not in your last snapshot and are not in your staging area. When you first clone a repository, all of your files will be tracked and unmodified because you just checked them out and haven’t edited anything. As you edit files, Git sees them as modified, because you’ve changed them since your last commit. @@ -926,7 +926,7 @@ $ git commit --amend ---- This command takes your staging area and uses it for the commit. -If you’ve made no changes since your last commit (for instance, you run this command immediately after your previous commit), then your snapshot will look exactly the same and all you’ll change is your commit message. +If you’ve made no changes since your last commit (for instance, you run this command immediately after your previous commit), then your snapshot will look exactly the same, and all you’ll change is your commit message. The same commit-message editor fires up, but it already contains the message of your previous commit. You can edit the message the same as always, but it overwrites your previous commit. @@ -942,6 +942,7 @@ $ git commit --amend You end up with a single commit – the second commit replaces the results of the first. +[[_unstaging]] ==== Unstaging a Staged File The next two sections demonstrate how to wrangle your staging area and working directory changes. diff --git a/book/03-git-branching/1-git-branching.asc b/book/03-git-branching/1-git-branching.asc index be9e66fef..cdde4d45c 100644 --- a/book/03-git-branching/1-git-branching.asc +++ b/book/03-git-branching/1-git-branching.asc @@ -804,6 +804,7 @@ To https://github.com/schacon/simplegit Basically all this does is remove the pointer from the server. The Git server will generally keep the data there for a while until a garbage collection runs, so if it was accidentally deleted, it's often easy to recover. +[[_rebasing]] === Rebasing In Git, there are two main ways to integrate changes from one branch into another: the `merge` and the `rebase`. diff --git a/book/07-git-tools/1-git-tools.asc b/book/07-git-tools/1-git-tools.asc index 6621988fd..8d859837c 100644 --- a/book/07-git-tools/1-git-tools.asc +++ b/book/07-git-tools/1-git-tools.asc @@ -903,6 +903,315 @@ Merge made by the 'recursive' strategy. === Searching +[[_reset]] +==== Reset Demystified + +Before moving on to more specialized tools, let's talk about `reset` and `checkout`. +These commands are two of the most confusing parts of Git when you first encounter them. +They do so many things, that it seems hopeless to actually understand them, and employ them properly. +For this, we recommend a simple metaphor. + +===== The Three Trees + +An easier way to think about `reset` and `checkout` is through the mental frame of Git being a content manager of three different trees. +By ``tree'' here we really mean ``collection of files'', not specifically the data structure. +(There are a few cases where the index doesn't exactly act like a tree, but for our purposes it is easier.) + +Git as a system manages and manipulates three trees in its normal operation: + +[cols="1,2",options="header"] +|================================ +| Tree | Role +| HEAD | Last commit snapshot, next parent +| Index | Proposed next commit snapshot +| Working Directory | Sandbox +|================================ + +====== The HEAD + +HEAD is the pointer to the current branch reference, which is in turn a pointer to the last commit made on that branch. +That means HEAD will be the parent of the next commit that is created. +It's generally simplest to think of HEAD as the snapshot of *your last commit*. + +In fact, it's pretty easy to see what that snapshot looks like. +Here is an example of getting the actual directory listing and SHA checksums for each file in the HEAD snapshot: + +[source,shell] +---- +$ cat .git/HEAD +ref: refs/heads/master + +$ cat .git/refs/heads/master +e9a570524b63d2a2b3a7c3325acf5b89bbeb131e + +$ git cat-file -p e9a570524b63d2a2b3a7c3325acf5b89bbeb131e +tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf +author Scott Chacon 1301511835 -0700 +committer Scott Chacon 1301511835 -0700 + +initial commit + +$ git ls-tree -r cfda3bf379e4f8dba8717dee55aab78aef7f4daf +100644 blob a906cb2a4a904a152... README +100644 blob 8f94139338f9404f2... Rakefile +040000 tree 99f1a6d12cb4b6f19... lib +---- + +====== The Index + +The Index is your *proposed next commit*. +Git populates it with a list of all the file contents that were last checked out into your working directory and what they looked like when they were originally checked out. +You then replace some of those files with new versions of them, and `git commit` converts that into the tree for a new commit. + +[source,shell] +---- +$ git ls-files -s +100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README +100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile +100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb +---- + +The index is not technically a tree structure – it's a flattened manifest – but for our purposes it's close enough. + +====== The Working Directory + +Finally, you have your working directory. +The other two trees store their content in an efficient but inconvenient manner, inside the `.git` folder. +The Working Directory unpacks them into actual files, which makes it much easier for you to edit them. +Think of the Working Directory as a *sandbox*, where you can try changes out before committing them to history. + +[source,shell] +---- +$ tree +. +├── README +├── Rakefile +└── lib + └── simplegit.rb + +1 directory, 3 files +---- + +===== The Workflow + +Git's main purpose is to record snapshots of your project in successively better states, by manipulating these three trees. + +image::images/reset-workflow.png[] + +Let's visualize this process: say you go into a new directory with a single file in it. +We'll call this *v1* of the file, and we'll indicate it in blue. +Now we run `git init`, which will create a Git repository with a HEAD reference which points to an unborn branch (`master` doesn't exist yet). + +image::images/reset-ex2.png[] + +At this point, only the Working Directory tree has any content. + +Now we want to commit this file, so we use `git add` to take content in the Working Directory and copy it to the Index. + +image::images/reset-ex3.png[] + +Then we run `git commit`, which takes the contents of the Index and saves it as a permanent snapshot, creates a commit object which points to that snapshot, and updates `master` to point to that commit. + +image::images/reset-ex4.png[] + +If we run `git status`, we'll see no changes, because all three trees are the same. + +Now we want to make a change to that file and commit it. +We'll go through the same process; first we change the file in our working directory. +Let's call this *v2* of the file, and indicate it in green. + +image::images/reset-ex5.png[] + +If we run `git status` right now, we'll see the file in red as ``Changes not staged for commit,'' because that entry differs between the Index and the Working Directory. +Next we run `git add` on it to stage it into our Index. + +image::images/reset-ex6.png[] + +At this point if we run `git status` we will see the file in green +under ``Changes to be committed'' because the Index and HEAD differ – that is, our proposed next commit is now different from our last commit. +Finally, we run `git commit` to finalize the commit. + +image::images/reset-ex7.png[] + +Now `git status` will give us no output, because all three trees are the same again. + +Switching branches or cloning goes through a similar process. +When you checkout a branch, it changes *HEAD* to point to the new branch ref, populates your *Index* with the snapshot of that commit, then copies the contents of the *Index* into your *Working Directory*. + +===== The Role of Reset + +The `reset` command makes more sense when viewed in this context. +It directly manipulates these three trees in a simple and predictable way. +It does up to three basic operations. + +====== Step 1: Move HEAD + +The first thing `reset` will do is move what HEAD points to. +This isn't the same as changing HEAD itself (which is what `checkout` does); `reset` moves the branch that HEAD is pointing to. +This means if HEAD is set to the `master` branch, running `git reset 9e5e64a` will start by making `master` point to `9e5e64a`. + +image::images/reset-soft.png[] + +No matter what form of `reset` with a commit you invoke, this is the first thing it will always try to do. +With `reset --soft`, it will simply stop there. + +Now take a second to look at that diagram and realize what happened: it essentially undid the last `git commit` command. +When you run `git commit`, Git creates a new commit and moves the branch that HEAD points to up to it. +When you `reset` back to `HEAD~` (the parent of HEAD), you are moving the branch back to where it was, without changing the Index or Working Directory. +You could now update the Index and run `git commit` again to accomplish what `git commit --amend` would have done (see <<_amend>>). + +====== Step 2: Updating the Index (`--mixed`) + +Note that if you run `git status` now you'll see in green the difference between the Index and what the new HEAD is. + +The next thing `reset` will do is to update the Index with the contents of whatever snapshot HEAD now points to. + +image::images/reset-mixed.png[] + +If you specify the `--mixed` option, `reset` will stop at this point. +This is also the default, so if you specify no option at all, this is where the command will stop. + +Now take another second to look at THAT diagram and realize what happened: it still undid your last `commit`, but also _unstaged_ everything. +You rolled back to before you ran all your `git add`s _AND_ `git commit`. + +====== Step 3: Updating the Working Directory (`--hard`) + +The third thing that `reset` will do is to make the Working Directory look like the Index. +If you use the `--hard` option, it will continue to this stage. + +image::images/reset-hard.png[] + +Finally, take yet another second to look at _THAT_ diagram and think about what happened. +You undid your last commit, all the `git add`s, _AND_ all the work you did in your working directory. + +It's important to note that this is the only way `reset` to make the `reset` command dangerous, and one of the very few cases where Git will actually destroy data. +Any other invocation of `reset` can be pretty easily undone, but the `--hard` option cannot, since it forcibly overwrites files in the Working Directory. +In this particular case, we still have the *v3* version of our file in a commit in our Git DB, and we could get it back by looking at our `reflog`, but if we had not committed it, Git still would have overwritten the file. + +====== Recap + +The `reset` command overwrites these three trees in a specific order, stopping when you tell it to: + +1. Move the branch HEAD points to _(stop here if `--soft`)_ +2. Make the Index look like HEAD _(stop here unless `--hard`)_ +3. Make the Working Directory look like the Index + +===== Reset With a Path + +That covers the behavior of `reset` in its basic form, but you can also provide it with a path to act upon. +If you specify a path, `reset` will skip step 1, and limit the remainder of its actions to a specific file or set of files. +This actually sort of makes sense – HEAD is just a pointer, and you can't point to part of one commit and part of another. +But the Index and Working directory _can_ be partially updated, so reset proceeds with steps 2 and 3. + +So, assume we run `git reset file.txt`. +This form (since you did not specify a commit SHA or branch, and you didn't specify `--soft` or `--hard`) is shorthand for `git reset --mixed HEAD file.txt`, which will: + +1. Move the branch HEAD points to _(skipped)_ +2. Make the Index look like HEAD _(stop here)_ + +So it essentially just copies `file.txt` from HEAD to the Index. + +image::images/reset-path1.png[] + +This has the practical effect of _unstaging_ the file. +If we look at the diagram for that command and think about what `git add` does, they are exact opposites. + +image::images/reset-path2.png[] + +This is why the output of the `git status` command suggests that you run this to unstage a file. +(See <<_unstaging>> for more on this.) + +We could just as easily not let Git assume we meant ``pull the data from HEAD'' by specifying a specific commit to pull that file version from. +We would just run something like `git reset eb43bf file.txt`. + +image::images/reset-path3.png[] + +This effectively does the same thing as if we had reverted the content of the file to *v1* in the Working Directory, ran `git add` on it, then reverted it back to *v3* again (without actually going through all those steps). +If we run `git commit` now, it will record a change that reverts that file back to *v1*, even though we never actually had it in our Working Directory again. + +It's also interesting to note that like `git add`, the `reset` command will accept a `--patch` option to unstage content on a hunk-by-hunk basis. +So you can selectively unstage or revert content. + +===== Squashing + +Let's look at how to do something interesting with this newfound power – squashing commits. + +Say you have a series of commits with messages like ``oops.'', ``WIP'' and ``forgot this file''. +You can use `reset` to quickly and easily squash them into a single commit that makes you look really smart. +(<<_rebasing>> shows another way to do this, but in this example it's simpler to use `reset`.) + +Let's say you have a project where the first commit has one file, the second commit added a new file and changed the first, and the third commit changed the first file again. +The second commit was a work in progress and you want to squash it down. + +image::images/reset-squash-r1.png[] + +You can run `git reset --soft HEAD~2` to move the HEAD branch back to an older commit (the first commit you want to keep): + +image::images/reset-squash-r2.png[] + +And then simply run `git commit` again: + +image::images/reset-squash-r3.png[] + +Now you can see that your reachable history, the history you would push, now looks like you had one commit with `file-a.txt` v1, then a second that both modified `file-a.txt` to v2 and added `file-b.txt`. + + +===== Check It Out + +Finally, you may wonder what the difference between `checkout` and `reset` is. +Like `reset`, `checkout` manipulates the three trees, and it is a bit different depending on whether you give the command a file path or not. + +====== Without Paths + +Running `git checkout [branch]` is pretty similar to running `git reset --hard [branch]` in that it updates all three trees for you to look like `[branch]`, but there are two important differences. + +First, unlike `reset --hard`, `checkout` is working-directory safe; it will check to make sure it's not blowing away files that have changes to them. +Actually, it's a bit smarter than that – it tries to do a trivial merge in the Working Directory, so all of the files you _haven't_ changed in will be updated. +`reset --hard`, on the other hand, will simply replace everything across the board without checking. + +The second important difference is how it updates HEAD. +Where `reset` will move the branch that HEAD points to, `checkout` will move HEAD itself to point to another branch. + +For instance, say we have `master` and `develop` branches which point at different commits, and we're currently on `develop` (so HEAD points to it). +If we run `git reset master`, `develop` itself will now point to the same commit that `master` does. +If we instead run `git checkout master`, `develop` does not move, HEAD itself does. +HEAD will now point to `master`. + +So, in both cases we're moving HEAD to point to commit A, but _how_ we do so is very different. +`reset` will move the branch HEAD points to, `checkout` moves HEAD itself. + +image::images/reset-checkout.png[] + +====== With Paths + +The other way to run `checkout` is with a file path, which, like `reset`, does not move HEAD. +It is just like `git reset [branch] file` in that it updates the index with that file at that commit, but it also overwrites the file in the working directory. +It would be exactly like `git reset --hard [branch] file` (if `reset` would let you run that) – it's not working-directory safe, and it does not move HEAD. + +Also, like `git reset` and `git add`, `checkout` will accept a `--patch` option to allow you to selectively revert file contents on a hunk-by-hunk basis. + +===== Summary + +Hopefully now you understand and feel more comfortable with the `reset` command, but are probably still a little confused about how exactly it differs from `checkout` and could not possibly remember all the rules of the different invocations. + +Here's a cheat-sheet for which commands affect which trees. +The ``HEAD'' column reads ``REF'' if that command moves the ref HEAD points to, and ``HEAD'' if it moves HEAD itself. +Pay especial attention to the 'WD Safe?' column – if it says *NO*, take a second to think before running that command. + +[options="header", cols="3,1,1,1,1"] +|================================ +| | HEAD | Index | Workdir | WD Safe? +| *Commit Level* | | | | +| `reset --soft [commit]` | REF | NO | NO | YES +| `reset [commit]` | REF | YES | NO | YES +| `reset --hard [commit]` | REF | YES | YES | *NO* +| `checkout [commit]` | HEAD | YES | YES | YES +| *File Level* | | | | +| `reset (commit) [file]` | NO | YES | NO | YES +| `checkout (commit) [file]` | NO | YES | YES | *NO* +|================================ + + === Rewriting History Many times, when working with Git, you may want to revise your commit history for some reason. diff --git a/book/07-git-tools/images/reset-checkout.png b/book/07-git-tools/images/reset-checkout.png new file mode 100644 index 000000000..4a633699d Binary files /dev/null and b/book/07-git-tools/images/reset-checkout.png differ diff --git a/book/07-git-tools/images/reset-ex2.png b/book/07-git-tools/images/reset-ex2.png new file mode 100644 index 000000000..4a042aff1 Binary files /dev/null and b/book/07-git-tools/images/reset-ex2.png differ diff --git a/book/07-git-tools/images/reset-ex3.png b/book/07-git-tools/images/reset-ex3.png new file mode 100644 index 000000000..c672d9521 Binary files /dev/null and b/book/07-git-tools/images/reset-ex3.png differ diff --git a/book/07-git-tools/images/reset-ex4.png b/book/07-git-tools/images/reset-ex4.png new file mode 100644 index 000000000..4cd33366d Binary files /dev/null and b/book/07-git-tools/images/reset-ex4.png differ diff --git a/book/07-git-tools/images/reset-ex5.png b/book/07-git-tools/images/reset-ex5.png new file mode 100644 index 000000000..e570e29a1 Binary files /dev/null and b/book/07-git-tools/images/reset-ex5.png differ diff --git a/book/07-git-tools/images/reset-ex6.png b/book/07-git-tools/images/reset-ex6.png new file mode 100644 index 000000000..77ef91d66 Binary files /dev/null and b/book/07-git-tools/images/reset-ex6.png differ diff --git a/book/07-git-tools/images/reset-ex7.png b/book/07-git-tools/images/reset-ex7.png new file mode 100644 index 000000000..c44014b67 Binary files /dev/null and b/book/07-git-tools/images/reset-ex7.png differ diff --git a/book/07-git-tools/images/reset-hard.png b/book/07-git-tools/images/reset-hard.png new file mode 100644 index 000000000..d10c623c3 Binary files /dev/null and b/book/07-git-tools/images/reset-hard.png differ diff --git a/book/07-git-tools/images/reset-mixed.png b/book/07-git-tools/images/reset-mixed.png new file mode 100644 index 000000000..b8da84510 Binary files /dev/null and b/book/07-git-tools/images/reset-mixed.png differ diff --git a/book/07-git-tools/images/reset-path1.png b/book/07-git-tools/images/reset-path1.png new file mode 100644 index 000000000..669ad3be2 Binary files /dev/null and b/book/07-git-tools/images/reset-path1.png differ diff --git a/book/07-git-tools/images/reset-path2.png b/book/07-git-tools/images/reset-path2.png new file mode 100644 index 000000000..7bd5ed573 Binary files /dev/null and b/book/07-git-tools/images/reset-path2.png differ diff --git a/book/07-git-tools/images/reset-path3.png b/book/07-git-tools/images/reset-path3.png new file mode 100644 index 000000000..a918e69d4 Binary files /dev/null and b/book/07-git-tools/images/reset-path3.png differ diff --git a/book/07-git-tools/images/reset-soft.png b/book/07-git-tools/images/reset-soft.png new file mode 100644 index 000000000..791a02a4f Binary files /dev/null and b/book/07-git-tools/images/reset-soft.png differ diff --git a/book/07-git-tools/images/reset-squash-r1.png b/book/07-git-tools/images/reset-squash-r1.png new file mode 100644 index 000000000..fefff3580 Binary files /dev/null and b/book/07-git-tools/images/reset-squash-r1.png differ diff --git a/book/07-git-tools/images/reset-squash-r2.png b/book/07-git-tools/images/reset-squash-r2.png new file mode 100644 index 000000000..9db8a7714 Binary files /dev/null and b/book/07-git-tools/images/reset-squash-r2.png differ diff --git a/book/07-git-tools/images/reset-squash-r3.png b/book/07-git-tools/images/reset-squash-r3.png new file mode 100644 index 000000000..77dc6fee1 Binary files /dev/null and b/book/07-git-tools/images/reset-squash-r3.png differ diff --git a/book/07-git-tools/images/reset-workflow.png b/book/07-git-tools/images/reset-workflow.png new file mode 100644 index 000000000..adc88c5ab Binary files /dev/null and b/book/07-git-tools/images/reset-workflow.png differ