在本地仓库进行操作

创建新的仓库

使用Git管理源代码的第一步是创建新的仓库。创建仓库需要创建普通的目录,并将该目录作为Git的仓库使用。操作过程如下。


$mkdir-p~/hello

$cd~/hello

$git init


这时,~/hello就可以作为Git的仓库使用了。下面就使用这个仓库来了解一下Git的基本功能。

小贴士:Git的命令以git<command>的形式启动。每条命令都有man page(帮助页面),当想要阅读帮助页面时,请用连字符将man git-<command>和子命令连接起来。

例如,当想要阅读init子命令的帮助页面时,应写为:


$man git-init


如果写成:


$man git init


显示出来的就是git(1)和init(8)的页面。

Git设置

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


$git confg—add user.email"m_ikeda@hogeraccho.com"

$git confg—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-fles

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-fles

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

散列值不需要完整输入,只需输入一定长度使其仅指示这次提交(但最少要输入4个字)。

最新提交可以用HEAD这一别名进行参照。另外还可以将从HEAD开始的相对位置指定为HEAD~2等。使用这一方法还可以进行如表1-12所示的指定。

在本地仓库进行操作 - 图2

在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


在本地仓库进行操作 - 图3

图 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)的最新提交。

在本地仓库进行操作 - 图4

图 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执行下列命令。

在本地仓库进行操作 - 图5

图 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的界面。

在本地仓库进行操作 - 图6

图 1-8 gitk

提取补丁

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


$git format-patch ver1..ver2


当前目录下就会生成0001-Add-goodbye.c.patch等补丁文件。

在这里使用标签名称指定了补丁的起点和终点,除此以外,还可以指定提交的散列值与分支名。

提取源码树

执行下列命令,可以将版本2的源代码作为tar文件提取出来。


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


根目录下就会生成名为hello-v2.tar的tar文件。

小贴士:当各文件包含到tar文件中时,文件名前面会加上使用—prefix选项指定的文字。这只是单纯地添加了文字,因此,当指定tar文件内的根目录名时,要记得如上例中的“hello-v2/”这样在文字的最后加上“/”。

小贴士:.git目录不会包含到tar文件中,因此即使将这里生成的tar文件解压缩,也不能发挥Git仓库的功能。

相反的,如果将Git仓库的目录连同.git目录在内全部复制,就能够发挥与原来的仓库完全相同的功能。从这一点也可以看出Git完全是分布式仓库。