- 在本地仓库进行操作
- include<stdio.h>
- include<stdio.h>
- 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
- include<stdio.h>
- 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
- include<stdio.h>
- include<stdio.h>
在本地仓库进行操作
创建新的仓库
使用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所示的命令行参数来指定提交的散列值,限制输出范围。
散列值不需要完整输入,只需输入一定长度使其仅指示这次提交(但最少要输入4个字)。
最新提交可以用HEAD这一别名进行参照。另外还可以将从HEAD开始的相对位置指定为HEAD~2等。使用这一方法还可以进行如表1-12所示的指定。
在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
图 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)的最新提交。
图 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执行下列命令。
图 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的界面。
图 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完全是分布式仓库。