精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

时间:2022-03-24 08:52:20

HACK #4 如何使用Git

本节介绍Git的使用方法。
Git是Linux内核等众多OSS(Open Source Software,开源软件)开发中所使用的SCM(Source Code Management,源码管理)系统。在2005年以前,在Linux内核开发中一直使用一个叫做BitKeeper的SCM。但是由于后来BitKeeper的许可证被更改,可能会对开发造成障碍,因此Linux不得不改用新的SCM进行开发。在这种情况下,Linux内核的创始人Linus Torvalds就开发了Git,将Linus树的仓库转移到了Git中。直到2011年的今天,Linus树仍然使用 Git进行管理,其他大部分的开发树使用的也是Git。目前Git由维护人员滨野纯(Junio C Hamano)等人持续进行开发。
Git的设计使其能够支持Linux或者OSS的开发模式。Git具有这些特征:分布式仓库;与互联网具有亲和性;版本更新记录管理不以单个文件为对象,而是将整个源码树作为一个对象;处理速度快等。
Git具有非常多的功能,如果一一进行说明的话能写成一本书。这里主要针对第一次使用Git的读者,通过使用指南的形式介绍日常使用较多的基本功能,同时对相关基本概念进行解说。
笔者使用的是1.7.1版本的Git。
分布式仓库型SCM
仓库是指保存SCM中源代码等信息及历史记录的原始数据的地方。CVS是以往使用较多的SCM,而在CVS中一直是将源代码从仓库签出(checkout)到本地工作区,进行修改后将代码提交到仓库中。像这样,仓库和工作区明确分开,多个开发者针对单一仓库进行提交的SCM称为“单一式仓库型”。
而Git采用的是与之相反的“分布式仓库型”结构。在Git中,工作区本身就是仓库。也就是说,开发者拥有各自的仓库,它们之间不存在结构层面的上下关系,所有仓库都是并行存在的。在Linux内核中一般认为linus树的仓库是“*”仓库,其实这只是大家一致认可的叫法,从Git的结构上看,完全没有任何设置是以linus树作为*的。
如上所述,Git直到完结时都是将本地磁盘的工作区作为仓库的。要能够熟练使用Git,首先必须掌握如何在本地仓库进行操作。下面将首先讲述在本地仓库进行操作的流程,然后介绍与其他仓库进行协作的方法。
在本地仓库进行操作
创建新的仓库
使用Git管理源代码的第一步是创建新的仓库。创建仓库需要创建普通的目录,并将该目录作为Git的仓库使用。操作过程如下。

$ mkdir 杙 ~/hello
$ cd ~/hello
$ git init

这时,~/hello就可以作为Git的仓库使用了。下面就使用这个仓库来了解一下Git的基本功能。
小贴士:Git的命令以git 的形式启动。每条命令都有man page(帮助页面),当想要阅读帮助页面时,请用连字符将man git-和子命令连接起来。
例如,当想要阅读init子命令的帮助页面时,应写为:

$ man git-init

如果写成:

$ man git init

显示出来的就是git(1)和init(8)的页面。
Git设置
在进行实际的文件操作前,首先要进行最低限度的必要设置。笔者一般进行的最低设置是提交者(committer)的姓名与邮件地址。在仓库目录下可以执行下列操作来进行这些设置。在这里设置的是笔者的信息。

$ git config --add user.email"m_ikeda@hogeraccho.com"
$ git config --add user.name"Munehiro \"Muuhh \" Ikeda"

把这个设置写入仓库目录的.git/config文件中。执行上面的git config命令后,这个文件中就应当写入了下列信息。
[user]

email = m_ikeda@hogeraccho.com
name = Munehiro \"Muuhh\" Ikeda

除此以外,还可以进行很多种设置,这里就先点到为止。
小贴士:写入.git/config的设置项目仅能适用于该仓库。
如果为git config指定--global选项,还可以参照要在用户已启动的所有仓库上共同使用的设置。与之对应的设置文件为~/.gitconfig。
当想要参照或添加整个系统的共同设置时,可以使用--system选项。与之对应的设置文件是/etc/gitconfig。
将文件添加到仓库中
现在,尝试创建文件并将其添加到Git的仓库中。首先,创建一个hello.c文件,其内容如下。

/* hello.c */
#include <stdio.h>

int main(void)
{
printf("Herro world!!\n");
return 0;
}

在Git中向仓库增加或修改文件的过程称为“提交”。提交分为两阶段进行,首先指定要提交的对象文件,然后进行实际的提交。

$ git add hello.c
$ git commit

执行git commit后,会启动一个用来输入提交信息的编辑器。由于这是第一次提交,因此在第一行中输入Initial commit,并保存文件。
小贴士:这里为git add明确指定了文件名hello.c,如果有多个文件,可以使用:

$ git add

将当前目录下的所有文件作为提交对象。
小贴士:在编辑提交信息时,如果没有保存文件就关闭了编辑器,就不会提交文件。
这样,hello.c就提交到了仓库中,以后就可以使用Git来管理和修改记录等。
修改并提交文件
仔细看一看刚才提交的文件,突然发现Hello居然写成了Herro了!下面就学习如何进行修改。
将hello.c的printf("Herro world!!n");行修改成printf("Hello world!!n");保存后提交。对文件进行修改时,也像新增加时一样需要对文件执行git add命令,将其作为提交对象。但是,当对多个文件进行修改时,一般会希望先把修改后的文件全部提交。在这种情况下,如果使用git commit的-a选项,就不需要执行git add命令。

$ git commit -a

这时会像新增加时一样启动编辑器,要求输入提交信息,在输入Correct misspelling后保存文件并关闭编辑器。
这样修改内容就提交到了仓库中。
小贴士:git commit -a原本是用来提交Git所管理的所有文件的。一次也没有执行git add命令的文件不会提交,例如,新创建的文件等。
确认工作区的状态
如果进行了很多修改,就需要确认已经提交工作区的仓库处于什么状态。我们试着稍微改变源代码来进行确认。
首先将hello.c的return 0;改为return 1;。然后创建新文件goodbye.c。

/* goodbye.c */
#include <stdio.h>

int main(void)
{
printf("Goodbye world!!\n");
return 0;
}

用来确认状态的第一个命令是git status,尝试执行以下命令。

$ git status
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: hello.c
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# goodbye.c
no changes added to commit (use "git add" and/or "git commit -a")

输出的内容显示hello.c的修改还没有提交,goodbye.c不在Git的管理范围内。
要确认哪个文件在Git的管理范围内,可以使用下列命令。

$ git ls-files
hello.c

如果想要看到修改hello.c的历史记录,可以执行下列命令。差别就会分段显示出来。

$ git diff
diff --git a/hello.c b/hello.c
index aa28db5..7ef0a54 100644
--- a/hello.c
+++ b/hello.c
@@ -4,6 +4,6 @@
int main(void)
{
printf("Hello world!!\n");
- return 0;
+ return 1;
}

显示出差别的只有Git管理范围内的文件。由于goodbye.c还不在Git的管理范围内,因此没有任何显示。
下面,对goodbye.c执行git add命令,确认前者是否在Git的管理范围内。

$ git add goodbye.c
$ git ls-files
goobye.c
hello.c

可以看到多出了goodbye.c。
这时可以再次使用git diff来显示差别。但奇怪的是,刚才明明对goodbye.c使用了git add命令,为什么没有显示呢?这时,再执行下列命令。

$ git add hello.c
$ git diff

而这次竟然什么也不显示了。这是怎么回事?
事实上,git diff显示的并不是最新提交与工作区之间的差别,而是“缓存区”(staging area,也称为分段存储区)与工作区之间的差别。缓存区是用来暂时存放下一次要提交到仓库的信息的区域。也就是说,git add的作用是将当前工作区的内容存放到缓存区。执行git commit后,最新提交和缓存区的内容是一致的。在工作区进行修改后再次执行git add,最新提交与缓存区的内容就变得不同,而缓存区与工作区的内容一致。在上例中,在执行git add hello.c后工作区与缓存区的内容是完全一致的。因此git diff就不会再显示任何内容。
当想看到的不是缓存区与当前工作区的差别,而是最新提交与当前工作区的差别时,可以为git diff指定表示最新提交的HEAD。

$ git diff HEAD
diff --git a/goodbye.c b/goodbye.c
new file mode 100644
index 0000000..13f79ea
--- /dev/null
+++ b/goodbye.c
@@ -0,0 +1,9 @@
+/* goodbye.c */
+#include <stdio.h>
+
+int main(void)
+{
+ printf("Goodbye world!!\n");
+ return 0;
+}
+
diff --git a/hello.c b/hello.c
index aa28db5..7ef0a54 100644
--- a/hello.c
+++ b/hello.c
@@ -4,6 +4,6 @@
int main(void)
{
printf("Hello world!!\n");
- return 0;
+ return 1;
}

如果想要知道最新提交与缓存区的差别,可以使用git diff --cached。由于当前缓存区与工作区是完全一致的,因此输入的内容与上述内容相同。
然后,使用git commit -a提交所作的修改,提交信息为Add goodbye.c。
参照提交记录
当想要参照提交记录时,可以使用git log命令。如果在当前仓库执行这条命令,就会显示关于之前进行的3次提交的下列相关信息(日期等信息因环境不同而各异)。

$ git log
commit 9b670c34bd7bd772648a99738017802f2b24f859
Author: Munehiro "Muuhh" Ikeda <m_ikeda@hogeraccho.com>
Date: Sat Apr 2 14:09:39 2011 -0700

Add goodbye.c

commit c47feeef44a652bda15dbb580d48213dc1294664
Author: Munehiro "Muuhh" Ikeda <m_ikeda@hogeraccho.com>
Date: Sat Apr 2 13:20:10 2011 -0700

Correct misspelling

commit 83d9b3f95cdb43e76953c77f03d2700e978dde8d
Author: Munehiro "Muuhh" Ikeda <m_ikeda@hogeraccho.com>
Date: Sat Apr 2 13:18:07 2011 -0700
Initial commit

紧接着commit后面显示的,是仅指示该提交的散列(hash)值。Author描述的是提交者的信息。事先使用git config设置提交者的信息,就是为了将这些作为提交信息记录下来。可以发现,每次提交的最后一行显示的都是提交时输入的提交信息。
git log默认输出所有的提交记录,但也可以用如表1-11所示的命令行参数来指定提交的散列值,限制输出范围。
表1-11 指定git log的提交范围(绝对散列值)
精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

散列值不需要完整输入,只需输入一定长度使其仅指示这次提交(但最少要输入4个字)。
最新提交可以用HEAD这一别名进行参照。另外还可以将从HEAD开始的相对位置指定为HEAD~2等。使用这一方法还可以进行如表1-12所示的指定。
表1-12 指定git log的提交范围(相对于HEAD)
精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

在Git的其他子命令下指定提交范围的方法也是相同的。使用git diff等也可以输出指定范围的差别。
如果为git log指定文件,则仅输出与该文件有关的提交。还可以使用-p选项将提交后的变更内容以段落形式显示出来。当前仓库显示的内容如下。

$ git log -p goodbye.c
commit 9b670c34bd7bd772648a99738017802f2b24f859
Author: Munehiro "Muuhh" Ikeda <m_ikeda@hogeraccho.com>
Date: Sat Apr 2 14:09:39 2011 -0700

Add goodbye.c

diff --git a/goodbye.c b/goodbye.c
new file mode 100644
index 0000000..13f79ea
--- /dev/null
+++ b/goodbye.c
@@ -0,0 +1,9 @@
+/* goodbye.c */
+#include <stdio.h>
+
+int main(void)
+{
+ printf("Goodbye world!!\n");
+ return 0;
+}
+

修改提交
在进行提交后,有时也会想要对已经提交的内容进行修改。修改方法大致可以分为两种。
第一种方法是进行新的提交来取消某个提交。在这种情况下,原先的提交和后来为了取消它而进行的提交都会保留记录。要取消当前仓库的最新提交时,进行如下操作。

$ git revert HEAD

提交信息可以根据个人喜好进行修改,这里就不作修改,直接以默认内容保存,并关闭编辑器。goodbye.c从工作区被删除,hello.c的内容也回到前一次提交的状态(返回值为0)。使用git log -p来确认提交的内容,可以发现使用git revert进行的最新提交(提交信息Revert "Add goodbye.c")与前一次提交(提交信息Add goodbye.c)的变更是完全相反的。
第二种方法是直接对提交进行修改。直接修改提交也有三种作法,得出的结果在细节上有一些不同。
直接修改提交的第一种作法,适用于对最新提交进行较小修改的情况。假设在刚才的git revert中,想保留hello.c的返回值1,不作修改,并在提交信息中记录下来。在这种情况下需要进行如下操作。

$ vi hello.c

(将return 0;重新修改为return 1;)

$ git add hello.c
$ git commit - -amend

将提交信息修改为Revert "Add goodbye.c" except for return value,保存并关闭编辑器。
再用git log -p来确认提交信息与变更内容,可以发现返回值的更改从最新提交中被删除,提交信息也发生了改变。
直接修改提交的第二种作法,就是取消提交。假设想要取消最新提交,也就是将记录恢复到最新提交前一次的提交(HEAD~1),但是希望工作区的源代码维持原状。这时应执行下列命令。

$ git reset - -soft HEAD~1

然后用git log查看记录,可以发现Revert...的提交已经消失了。但是由于并没有对工作区作出修改,因此原本通过当前最新提交的Add goodbye.c应当添加的文件goodbye.c不存在。
最后一种作法,是在恢复记录的同时,将工作区也恢复到相应的状态。可以执行下列命令:

$ git reset - -hard HEAD

由于工作区也已经回到了当前的HEAD(即Add goodbye.c),因此goodbye.c恢复。目前记录、工作区都已经完全恢复到提交Add goodbye.c后的状态。
小贴士:对提交记录进行修改时请慎重。特别需要注意的是,这个方法如果用在被其他仓库参照的仓库中,会出现相互之间记录不兼容的问题,因此不能在此情况下使用。
为提交加标签
可以为每次提交加上“标签”。为发布版本等关键的提交加上标签,以后就可以使用标签名称来参照本次提交,十分方便。
假设将HEAD~1的提交Correct misspelling发布为版本1,然后尝试为ver1加上标签。

$ git tag ver1 HEAD~1

可以使用下列命令来显示仓库内的标签列表。

$ git tag -1

创建分支
想要在保留当前开发系统的同时进行其他系统的开发,就需要创建“分支”。下面以刚才加了标签ver1的提交为起点,创建名为ver1x的分支。ver1x是针对下一版本的开发分支。

$ git branch ver1x ver1

仓库的分支列表可以用下列命令来显示。

$ git branch
* master
ver1x

如图1-5所示,从输出的内容可以看到,新的分支ver1x已经创建。前面有*的是当前工作区需要的分支(当前分支)。然后,将当前分支更改为ver1x。

$ git checkout ver1x
Switched to branch 'ver1x'
$ git branch
master
* ver1x

精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git
图1-5 分支的创建
在ver1x分支中创建新的文件thanks.c,并提交。

/* thanks.c */
#include <stdio.h>

int main(void)
{
printf("Thank you guys!!\n");
return 0;
}

$ git add thanks.c
$ git commit

将ver1x:Add thanks.c作为提交信息。
使用git log查看记录,可以发现,master分支里的Add goodbye.c在分支ver1x内是无效的,提交Add thanks.c的祖先是分支ver1x的起点,即提交Correct misspelling。像这样创建分支,就可以在同一仓库内独立地进行其他系统的开发。
rebase命令
开发版必须一直在最新发行版的基础上进行开发。例如,当发行版安装新功能时,必须将其也安装到开发版中。在当前的仓库中,goodbye.c就相当于新安装的功能。这时就需要将开发分支ver1x的起点移动到发行版的最新提交中。这种分支起点的移动称为复位基底(rebase),如图1-6所示。想要将当前分支复位基底到分支master的最新提交,需要执行下列命令。由于现在的当前分支为ver1x,因此这条命令复位基底的是ver1x。
$ git rebase master
小贴士:上例中是rebase到master的最新提交,但可以使用--onto选项来指定要rebase到的任意提交。默认为所指定分支(上例中为master)的最新提交。

精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

图1-6 分支的rebase
合并分支
为了合并发行版master分支与开发版分支ver1x中各自进行修改与开发的情况,就需要对文件进行修改。
首先在目前需要的ver1x分支中进行修改。通过下列命令来修改goodbye.c的注释。

$ git branch
master
* ver1x
$ vi goodbye.c
(将/* goodbye.c */修改为/* goodbye.c : needed? */)
$ git commit -a

将提交信息写为ver1x: Modify comment in goodbye.c。然后移动到master分支,在这边也对goodbye.c进行修改。

$ git checkout master
$ vi goodbye.c
(将/* goodbye.c */修改为/* goodbye.c : yes, needed! */
return 0; 修改为return 1;)
$ git commit -a

将提交信息写为Modify comment and return value of goodbye.c。
到此为止,ver1x分支下的开发就基本完成了,假设即将将其作为版本2进行发布。在这种情况下,需要将分支ver1x合并到分支master中,将ver1x下的开发成果整合到发行版中(见图1-7)。将当前分支作为master执行下列命令。
精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

图1-7 分支的合并

$ git merge ver1x
Auto-merging goodbye.c
CONFLICT (content): Merge conflict in goodbye.c
Automatic merge failed; fix conflicts and then commit the result.

这时提示由于goodbye.c中发生了冲突而无法合并。这是由于在两个分支下都对goodbye.c进行了修改。发生冲突的文件可以使用git status命令,显示为unmerged。

$ git status
goodbye.c: needs merge
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: thanks.c
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# unmerged: goodbye.c
#

再查看一下goodbye.c的内容。

<<<<<<< HEAD:goodbye.c
/* goodbye.c : yes, needed! */
=======
/* goodbye.c : needed? */
>>>>>>> ver1x:goodbye.c
#include <stdio.h>

int main(void)
{
printf("Goodbye world!!\n");
return 1;
}

发生冲突的部分是用冲突标记<<<<<<<和>>>>>>>显示的。必须人工决定选择其中的哪一个。这里选择采用master分支下的修改,即yes,needed!。
将goodbye.c修改为下列内容。

/* goodbye.c : yes, needed! */
#include <stdio.h>

int main(void)
{
printf("Goodbye world!!\n");
return 1;
}

使用git add通知Git修改已结束,并进行提交。

$ git add goodbye.c
$ git commit

到这一步,两个分支的合并就结束了。使用git log确认记录,可以发现进行合并后,在ver1x分支中进行的ver1x: Add thanks.c等修改在master分支中也体现出来。冲突的解决也作为一个提交记录下来。
然后加上标签,以便将这个状态作为版本2进行参照。

$ git tag ver2

小贴士:即使在两个分支下对相同文件进行了修改,如果是针对不同行进行的修改,Git也会自动将这些修改合并。上例就在master分支下修改了goodbye.c的返回值,这个部分也由Git自动进行了合并。
参照图形记录
对分支进行合并后,提交之间的从属关系变得复杂,比较难把握。这时可以使用git log --graph命令,在文字界面上将从属关系以图形显示出来。
除此以外,也可以使用gitk数据包里所含的基于图形界面的工具gitk。图1-8所示为gitk的界面。
精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

图1-8 gitk
提取补丁
想要根据从版本1到版本2的各次提交的差别提取补丁文件,可以执行下列命令。

$ git format-patch ver1..ver2

当前目录下就会生成0001-Add-goodbye.c.patch等补丁文件。
在这里使用标签名称指定了补丁的起点和终点,除此以外,还可以指定提交的散列值与分支名。
提取源码树
执行下列命令,可以将版本2的源代码作为tar文件提取出来。

$ git archive -- format=tar - -prefix="hello-v2/" ver2 > ../hello-v2.tar

根目录下就会生成名为hello-v2.tar的tar文件。
小贴士:当各文件包含到tar文件中时,文件名前面会加上使用--prefix选项指定的文字。这只是单纯地添加了文字,因此,当指定tar文件内的根目录名时,要记得如上例中的“hello-v2/”这样在文字的最后加上“/”。
小贴士:.git目录不会包含到tar文件中,因此即使将这里生成的tar文件解压缩,也不能发挥Git仓库的功能。
相反的,如果将Git仓库的目录连同.git目录在内全部复制,就能够发挥与原来的仓库完全相同的功能。从这一点也可以看出Git完全是分布式仓库。
与远程仓库进行共同作业
本地仓库的操作已经基本掌握,下面就将介绍与远程仓库进行共同作业的方法。
这里将按照一般开发者进行Linux内核上游开发时的流程来说明。大致流程如下。
将上游的仓库复制到本地。
不断追踪上游仓库的最新状态,同时在本地仓库进行开发。
以补丁的形式将开发成果提交维护人员及开发邮件列表。
复制仓库
当进行上游开发时,首先要复制各维护人员所管理的开发仓库(远程仓库),建立本地仓库。在Git中将这一步称为“复制”。 复制是通过git clone命令来进行的。例如,复制Linus树的仓库的命令为:

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6

复制Linus树时要下载1GB以上的数据,因此需要花费很长时间,仅在必要时再进行这个操作。这里将之前生成的hello仓库当做远程仓库,将其复制到其他位置。

$ cd
$ git clone hello local

生成local目录后,里面包含的文件就与hello目录完全相同。进入local目录,使用git log确认记录,就可以发现至今为止的记录已完全复制过来。
建立本地分支
请在本地仓库执行git branch命令。在这一阶段只有master分支。这个master分支是在远程仓库(即hello仓库)的master分支关联的基础上,在本地仓库生成的分支。关联,就是指此后与远程仓库进行同步时,hello仓库在master分支下所作的改动会合并到这个分支。因此,如果在master分支中进行开发,进行同步时两个仓库所作的更改就有可能发生冲突。为了避免发生这种情况,就要事先从master分支中分出一个用于在本地进行开发的分支work。

$ git checkout -b work

git checkout -b命令将在创建分支的同时进行检查(针对当前分支的最新修改)。
追踪分支
为了便于说明,上文的描述比较简单,可能会让人认为本地仓库的master分支是hello仓库master分支的副本,而其实并不是这样。hello仓库的master分支,在本地仓库是以origin/master的标题出现的。这个分支才完全是hello仓库master分支的副本,这种分支称为“追踪分支”。在使用git pull对仓库进行同步时,首先同步的就是这个追踪分支。然后,把追踪分支的提交合并到追踪分支所关联的本地分支中。
通过执行git branch -r命令,可以显示追踪分支的列表。下方显示的就是在本地仓库中执行这一命令的输出结果(见图1-9)。

$ git branch -r
origin/HEAD -> origin/master
origin/master
origin/ver1x

远程仓库的相关信息可以使用git remote show命令来确认。虽然可以设置多个远程仓库,但仅设置一个时其默认名称为origin,因此执行该命令时可以指定origin。
精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

图1-9 本地分支的建立

$ git remote show origin
* remote origin
URL: /home/munehiro/hello
Remote branch merged with 'git pull' while on branch master
master
Tracked remote branches
master
ver1x

这里的输出具有下列含义。
远程仓库origin的URL:/home/munehiro/hello
在本地分支master上执行git pull时合并的远程分支:master
追踪分支:master,ver1x
这些信息是通过.git/config文件设置的。相关各部分的内容(section)如下所示。[remote "origin"]部分规定了远程仓库的URL、远程仓库上的分支、追踪分支之间的关系。[branch "master"]部分规定了合并到本地分支master的远程分支为origin仓库(即hello仓库)的master分支。

$ cat .git/config
...
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*
url = /home/munehiro/hello
[branch "master"]
remote = origin
merge = refs/heads/master
...

追踪分支是为了追踪远程仓库而存在的,因此不能在这个分支上进行本地修改(从技术上是可以的,但并不推荐)。
以追踪分支为起点建立本地分支后,本地分支就被追踪分支关联。例如,可以通过下列命令,建立与master分支的追踪分支相关联的本地分支master2。

$ git branch master2 origin/master

本地分支master及master2虽然被关联,但二者完全是本地分支。因此也可以直接在上面进行本地开发。但是,由于需要通过git pull进行合并,因此如果发生了冲突,就必须在这时候解决。
与远程仓库同步
想要看到在远程仓库上不断进行的开发,可以在hello仓库的master分支下对thanks.c进行如下修改并提交(将负(–)的行改为正(+)的行)。

-    return 0;
+ return 2;

$ git commit -a

将Modify return value of thanks.c into 2作为提交信息。
使用git pull命令可以让本地仓库与远程仓库的最新状态保持同步。在本地仓库执行下列命令后,在hello仓库的master分支下进行的修改就会全部整合到本地仓库的master分支。

$ git checkout master
$ git pull

这时使用git log查看记录,可以发现hello仓库的提交Modify return value of thanks.c into 2已经整合并完成同步。
将开发分支rebase到最新状态
在本地仓库的work分支下不断进行本地开发。将当前分支设置为work后,对thanks.c进行如下的修改并提交。

$ git checkout work

- printf("Thank you guys!!\n");
- return 0;
+ printf("Thank you so much guys!!\n");
+ return 1;

$ git commit -a

在此以前,提交信息都只有1行,而这次需要输入多行,如下所示。第2行只需另起一空行。

Modify message thanks.c

I really appreciate your efforts.

另外,本地开发成果必须基于最新版的远程仓库(上游仓库)。当前的work分支是以版本2为起点的,而这已经不是最新版。处于最新状态的是刚才进行了同步的本地仓库的master分支。因此,如图1-10所示,只需要将work分支复位基底到master分支的HEAD。
thanks.c内发生了冲突,可以按照与上文所述“合并分支”同样的方法来消除冲突。为了保留上游的修改,并加入自己的开发成果,需要对thanks.c进行如下修改。

$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Modify thanks.c
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging thanks.c
CONFLICT (content): Merge conflict in thanks.c
Failed to merge in the changes.
Patch failed at 0001 Modify thanks.c

When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To restore the original branch and stop rebasing run "git rebase --abort".

精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

图1-10 本地开发分支的复位基底
例1-1 thanks.c的冲突标记

<<<<<<< HEAD
printf("Thank you guys!!\n");
return 2;
=======
printf("Thank you so much guys!!\n");
return 1;
>>>>>>> Modify thanks.c

例1-2 thanks.c的修改结果

printf("Thank you so much guys!!\n");
return 2;

因为发生冲突而中断的复位基底,在消除冲突并对文件执行git add命令后,再执行git rebase--continue就会继续。

$ git add thanks.c
$ git rebase --continue

这样就成功地复位基底到最新版了。这时也可以使用git log确认记录。
用邮件将补丁发送给维护人员
现在,本地仓库的开发终于完成了。Linux内核开发的流程是先以补丁的形式将开发成果发送到邮件列表,经过评估与讨论后再整合到上游的仓库内。可以使用git format-patch将补丁输出为文件,粘贴到平时使用的电子邮件的正文并发送,而Git也备有直接发送邮件的功能—git send-email命令。这里使用这条命令来发送邮件。
另外,在笔者所使用的环境(Ubuntu 10.04、Fedora14)中,git send-mail命令原来是放在与Git不同的git-email数据包里的,因此需要事先下载。
小贴士:通过Git直接发送邮件,就可以避免电子邮件软件出现的换行符等问题。
要将本地仓库的开发成果,即以master分支为起点的work分支的各次提交(目前只有一个),作为补丁以邮件发送,可以执行下列命令。另外,如果事先在配置文件中设置各种选项,就不需要每次都在命令行进行输入。表1-13所示为与选项对应的配置文件的段落名。

$ git send-email --to=hello_maintainer@hogeraccho.com --cc=hello-ml@hogeraccho.com --smtp-server=smtp.googlemail.com --smtp-encryption=ssl --smtp-server-port=465 --smtp-user=youruser@gmail.com --smtp-pass=yourpw master..work

表1-13 git send-email的选项
精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

将--to、--cc等改成自己的地址,并尝试发送邮件,应当会收到如下列内容的邮件。

From: Munehiro Muuhh Ikeda <m_ikeda@hogeraccho.com>
To: you@your.domain.co.jp
Cc: Munehiro Muuhh Ikeda <m_ikeda@hogeraccho.com>
Subject: [PATCH] Modify message thanks.c

I really appreciate your efforts.
---
thanks.c | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/thanks.c b/thanks.c
index c28de46..806371d 100644
--- a/thanks.c
+++ b/thanks.c
@@ -3,7 +3,7 @@

int main(void)
{
- printf("Thank you guys!!\n");
+ printf("Thank you so much guys!!\n");
return 2;
}

--

1.7.1
提交信息的第1行是邮件标题,从空行之后(即第3行开始)是邮件的正文。
git send-email有非常多的选项,可以进行各种设置。建议浏览帮助页面(man page),进行各种尝试,最后生成最佳的配置文件。
其他有用的命令
除上面介绍的以外,还有很多其他有用的命令。表1-14简单整理了其中的一部分,详细内容请参考git的帮助页面。
表1-14 其他命令
精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #4 如何使用Git

小结
当进行Linux内核的上游开发时,Git可以说是必需的工具。Git具有优秀的功能与速度,在多个开发人员参与的项目中可以成为非常有效的工具,并且不仅限于Linux内核的开发。一直以来使用单一仓库型SCM的人可以体验分布式仓库型SCM的高度可扩展性。当然,对于初次使用SCM的人来说也一定会是很好用的工具。