程式扎記: [Git Pro] Ch3 : Git Branching - Part1

標籤

2012年4月28日 星期六

[Git Pro] Ch3 : Git Branching - Part1

Preface : 
Nearly every VCS has some form of branching support. Branching means you diverge from the main line of development and continue to do work without messing with that main line. In many VCS tools, this is a somewhat expensive process, often requiring you to create a new copy of your source code directory, which can take a long time for large projects. 

Some people refer to the branching model in Git as its "killer feature," and it certainly sets Git apart in the VCS community. Why is it so special? The way Git branches is incredibly lightweight, making branching operations nearly instantaneous and switching back and forth between branches generally just as fast. Unlike many other VCSs, Git encourages a workflow that branches and merges often, even multiple times in a day. Understanding and mastering this feature gives you a powerful and unique tool and can literally change the way that you develop. 

What a Branch Is : 
To really understand the way Git does branching, you need to take a step back and examine how Git stores its data. As you may remember from Chapter 1, Git doesn’t store data as a series of changesets or deltas, but instead as a series of snapshots. 

When you commit in Git, Git stores a commit object that contains a pointer to the snapshot of the content you staged, the author and message metadata, and zero or more pointers to the commit or commits that were the direct parents of this commit: zero parents for the first commit, one parent for a normal commit, and multiple parents for a commit that results from a merge of two or more branches

To visualize this, let’s assume that you have a directory containing three files, and you stage them all and commit. Staging the files checksums each one (the SHA-1 hash I mentioned in Chapter 1), stores that version of the file in the Git repository (Git refers to them as blobs), and adds that checksum to the staging area : 
~/GitCh3$ git add README test.rb LICENSE2
~/GitCh3$ git commit -m 'Initial commit of my project'
[master (root-commit) 18c4ba8] Initial commit of my project
3 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 LICENSE2
create mode 100644 README
create mode 100644 test.rb

When you create the commit by running git commit, Git checksums each subdirectory (in this case, just the root project directory) and stores those tree objects in the Git repository. Git then creates a commit object that has the metadata and a pointer to the root project tree so it can re-create that snapshot when needed. 

Your Git repository now contains five objects: one blob for the contents of each of your three files, one tree that lists the contents of the directory and specifies which file names are stored as which blobs, and one commit with the pointer to that root tree and all the commit metadata. Conceptually, the data in your Git repository looks something like Figure 3-1. 
 

If you make some changes and commit again, the next commit stores a pointer to the commit that came immediately before it. After two more commits, your history might look something like Figure 3-2 : 
 

A branch in Git is simply a lightweight movable pointer to one of these commits. The default branch name in Git is master. As you initially make commits, you’re given amaster branch that points to the last commit you made. Every time you commit, it moves forward automatically. 

What happens if you create a new branch? Well, doing so creates a new pointer for you to move around. Let’s say you create a new branch called testing. You do this with the git branch testing command. This creates a new pointer at the same commit you’re currently on (see Figure 3-4) : 
 

How does Git know what branch you’re currently on? It keeps a special pointer called HEAD. Note that this is a lot different than the concept of 'HEAD' in other VCSs you may be used to, such as Subversion or CVS. In Git, this is a pointer to the local branch you’re currently on. In this case, you’re still on master. The git branch command only created a new branch—it didn’t switch to that branch (see Figure 3-5) : 
 

To switch to an existing branch, you run the git checkout command. Let’s switch to the new testing branch : 
~/GitCh3$ git checkout testing
Switched to branch 'testing'

This moves HEAD to point to the testing branch (see Figure 3-6) : 
 

What is the significance of that? Well, do another commit : 
~/GitCh3$ echo "Test2" >> test.rb
~/GitCh3$ git commit -a -m 'made a change'
[testing 7d184a1] made a change
1 files changed, 1 insertions(+), 0 deletions(-)

Figure 3-7 illustrates the result : 
 

This is interesting, because now your testing branch has moved forward, but your master branch still points to the commit you were on when you ran git checkout to switch branches. Let’s switch back to the master branch : 
~/GitCh3$ git checkout master
Switched to branch 'master'

Figure 3-8 shows the result : 
 

That command did two things. It moved the HEAD pointer back to point to the master branch, and it reverted the files in your working directory back to the snapshot that master points to. This also means the changes you make from this point forward will diverge from an older version of the project. It essentially rewinds the work you’ve done in your testing branch temporarily so you can go in a different direction. 

Let’s make a few changes and commit again : 
~/GitCh3$ echo "This change for master" >> test.rb
~/GitCh3$ git commit -a -m 'made other changes'
[master 2c98a24] made other changes
1 files changed, 1 insertions(+), 0 deletions(-)

Now your project history has diverged (see Figure 3-9). You created and switched to a branch, did some work on it, and then switched back to your main branch and did other work. Both of those changes are isolated in separate branches: you can switch back and forth between the branches and merge them together when you’re ready. And you did all that with simple branch and checkout commands. 
 

Because a branch in Git is in actuality a simple file that contains the 40-character SHA-1 checksum of the commit it points to, branches are cheap to create and destroy. Creating a new branch is as quick and simple as writing 41 bytes to a file (40 characters and a newline). 

This is in sharp contrast to the way most VCS tools branch, which involves copying all of the project’s files into a second directory. This can take several seconds or even minutes, depending on the size of the project, whereas in Git the process is always instantaneous. Also, because you’re recording the parents when you commit, finding a proper merge base for merging is automatically done for you and is generally very easy. These features help encourage developers to create and use branches often. 

Basic Branching and Merging : 
Let’s go through a simple example of branching and merging with a workflow that you might use in the real world. You’ll follow these steps : 
1. Do work on a web site.
2. Create a branch for a new story you’re working on.
3. Do some work in that branch.

At this stage, you’ll receive a call that another issue is critical and you need a hotfix. You’ll do the following : 
1. Revert back to your production branch.
2. Create a branch to add the hotfix.
3. After it’s tested, merge the hotfix branch, and push to production.
4. Switch back to your original story, and continue working.

- Basic Branching 
First, let’s say you’re working on your project and have a couple of commits already (see Figure 3-10). 
 

You’ve decided that you’re going to work on issue #53 in whatever issue-tracking system your company uses. To be clear, Git isn’t tied into any particular issue-tracking system; but because #53 is a focused topic that you want to work on, you’ll create a new branch in which to work. To create a branch and switch to it at the same time, you can run the git check command with the -b switch : 
~/GitCh3$ git checkout -b iss53
Switched to a new branch 'iss53'

This is shorthand for : 
$ git branch iss53
$ git checkout iss53

Figure 3-11 illustrates the result. 
 

You work on your web site and do some commits. Doing so moves the iss53 branch forward, because you have it checked out (that is, your HEAD is pointing to it; see Figure 3-12) : 
~/GitCh3$ vi index.html
~/GitCh3$ echo "Fix bug for iss53" >> README
~/GitCh3$ git commit -a -m 'added a new page:index.html [issue 53]'
[iss53 6daf899] added a new page:index.html [issue 53]
1 files changed, 1 insertions(+), 0 deletions(-)

 

Now you get the call that there is an issue with the web site, and you need to fix it immediately. With Git, you don’t have to deploy your fix along with the is533 changes you’ve made, and you don’t have to put a lot of effort into reverting those changes before you can work on applying your fix to what is in production. All you have to do is switch back to your master branch. 

However, before you do that, note that if your working directory or staging area has uncommitted changes that conflict with the branch you’re checking out, Git won’t let you switch branches. It’s best to have a clean working state when you switch branches. There are ways to get around this (namely, stashing and commit amending) that I’ll cover later. For now, you’ve committed all your changes, so you can switch back to your master branch : 
~/GitCh3$ git checkout master
Switched to branch 'master'

At this point, your project working directory is exactly the way it was before you started working on issue #53, and you can concentrate on your hotfix. This is an important point to remember: Git resets your working directory to look like the snapshot of the commit that the branch you check out points to. It adds, removes, and modifies files automatically to make sure your working copy is what the branch looked like on your last commit to it. 

Next, you have a hotfix to make. You create a hotfix branch on which to work until it’s completed (see Figure 3-13) : 
~/GitCh3$ git checkout -b hotfix
Switched to branch 'hotfix'
~/GitCh3$ vi index.html
~/GitCh3$ git commit -a -m 'fixed the broken email address'
~/GitCh3$ git status
On branch hotfix
nothing to commit (working directory clean)

 

Run your tests, make sure the hotfix is what you want, and merge it back into your master branch to deploy to production. You do this with the git merge command : 
~/GitCh3$ git checkout master
Switched to branch 'master'
~/GitCh3$ git merge hotfix
Updating 860b3ac..326f664
Fast-forward
index.html | 7 +++++++
1 files changed, 7 insertions(+), 0 deletions(-)
create mode 100644 index.html

You’ll notice the phrase Fast forward in that merge. Because the commit pointed to by the branch you merged in was directly upstream of the commit you’re on, Git moves the pointer forward. To phrase that another way, when you try to merge one commit with a commit that can be reached by following the first commit’s history, Git simplifies things by moving the pointer forward because there is no divergent work to merge together—this is called a fast forward

Your change is now in the snapshot of the commit pointed to by the master branch, and you can deploy your change (see Figure 3-14). 
 

After your super-important fix is deployed, you’re ready to switch back to the work you were doing before you were interrupted. However, first you’ll delete the hotfixbranch, because you no longer need it—the master branch points at the same place. You can delete it with the -d option to git branch : 
~/GitCh3$ git branch -d hotfix
Deleted branch hotfix (was 326f664).

Now you can switch back to your work-in-progress branch on issue #53 and continue working on it (see Figure 3-15) : 
~/GitCh3$ git checkout iss53
Switched to branch 'iss53'
~/GitCh3$ echo "Change of iss53" >> README
~/GitCh3$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ee9a64a] finished the new footer [issue 53]
1 files changed, 1 insertions(+), 0 deletions(-)

 
- Basic Merging 
Suppose you’ve decided that your issue #53 work is complete and ready to be merged into your master branch. In order to do that, you’ll merge in your iss53 branch, much like you merged in your hotfix branch earlier. All you have to do is check out the branch you wish to merge into and then run the git merge command : 
~/GitCh3$ git checkout master
Switched to branch 'master'
~/GitCh3$ git merge iss53
Merge made by recursive.
README | 2 ++
TEST | 1 +
2 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 TEST

This looks a bit different than the hotfix merge you did earlier. In this case, your development history has diverged from some older point. Because the commit on the branch you’re on isn’t a direct ancestor of the branch you’re merging in, Git has to do some work. In this case, Git does a simple three-way merge, using the two snapshots pointed to by the branch tips and the common ancestor of the two. Figure 3-16 highlights the three snapshots that Git uses to do its merge in this case : 
 
Figure 3-16. Git automatically identifies the best common ancestor merge base for branch merging. 

Instead of just moving the branch pointer forward, Git creates a new snapshot that results from this three-way merge and automatically creates a new commit that points to it (see Figure 3-17). This is referred to as a merge commit and is special in that it has more than one parent. 
 
Figure 3-17Git automatically creates a new commit object that contains the merged work 

If you use gitk to check, you will have below information :\\ 
 

Now that your work is merged in, you have no further need for the iss53 branch. You can delete it and then manually close the ticket in your ticket-tracking system : 
~/GitCh3$ git branch -d iss53
Deleted branch iss53 (was ee9a64a).

- Basic Merge Conflicts 
Occasionally, this process doesn’t go smoothly. If you changed the same part of the same file differently in the two branches you’re merging together, Git won’t be able to merge them cleanly. If your fix for issue #53 modified the same part of a file as the hotfix, you get a merge conflict that looks something like this : 
~/GitCh3$ git merge iss53
Auto-merging TEST
CONFLICT (content): Merge conflict in TEST
Automatic merge failed; fix conflicts and then commit the result.

Git hasn’t automatically created a new merge commit. It has paused the process while you resolve the conflict. If you want to see which files are unmerged at any point after a merge conflict, you can run git status : 
~/GitCh3$ git status
# On branch master
Unmerged paths:
# (use "git add/rm ..." as appropriate to mark resolution)
#
both modified: TEST
#
no changes added to commit (use "git add" and/or "git commit -a")

Anything that has merge conflicts and hasn’t been resolved is listed as unmerged. Git adds standard conflict-resolution markers to the files that have conflicts, so you can open them manually and resolve those conflicts. Your file contains a section that looks something like this : 
Test
Test for merge conflict
<<<<<<< HEAD
New for master branch
=======
New for is533
>>>>>>> iss53

This means the version in HEAD (your master branch, because that was what you had checked out when you ran your merge command) is the top part of that block (everything above the =======), whereas the version in your iss53 branch looks like everything in the bottom part. In order to resolve the conflict, you have to either choose one side or the other or merge the contents yourself. For instance, you might resolve this conflict by replacing the entire block with this : 
Test
Test for merge conflict

New for merge of master and is533

This resolution has a little of each section, and I’ve fully removed the <<<<<<<<, ========, and >>>>>>>> lines. After you’ve resolved each of these sections in each conflicted file, run git add on each file to mark it as resolved. Staging the file marks it as resolved in Git. If you want to use a graphical tool to resolve these issues, you can run git mergetool, which fires up an appropriate visual merge tool and walks you through the conflicts : 
 

If you want to use a merge tool other than the default, you can see all the supported tools listed at the top after "merge tool candidates". Type the name of the tool you’d rather use. (In Chapter 7 will discuss how you can change this default value for your environment.) After you exit the merge tool, Git asks you if the merge was successful. If you tell the script that it was, it stages the file to mark it as resolved for you. 

You can run git status again to verify that all conflicts have been resolved : 
~/GitCh3$ git status
# On branch master
Changes to be committed:
#
# modified: TEST
#

If you’re happy with that, and you verify that everything that had conflicts has been staged, you can type git commit to finalize the merge commit.

沒有留言:

張貼留言

網誌存檔