第 28 章 Git 版本控制系统

目录

版本控制之道

时间机器

分支控制

协同工作

冲突合并

常见版本控制系统

git 如何工作

补丁

git 对象

操作级别

初始化

创建版本库

版本库状态

配置

版本更新

版本标签

时间机器

分支管理

创建分支

合并分支

处理冲突

通过文件协作

通过网络协作

gitweb

版本控制之道

几乎所有项目[61],都要使用版本控制,它究竟有什么优势呢?

时间机器

假设你使用的编辑器,不支持删除,那你就得特别的谨小慎微,甚至是如履薄冰:因为你打错了字没法删除

放松下来,目前我所接触的所有编译器中,还没有变态到这种程度的。

如果编译器提供了删除功能,却没有 undo,那可能会更可怕:如果你不小心选中了全部文字,手一抖……因为不能 undo,你知道,如果此时不小心按下 delete,你就得从头来过……你会为可能产生的后果而发抖

然而,命运总在你不想被打扰的时候来敲你可爱的门,在你手抖的刹那,你真的鬼使神差的按了下那个可怕的键……你的选择只能是重新来过或者放弃……

你会比任何时候都希望时间倒流一秒钟

还好,几乎所有的编译器,都会帮你回到一秒种、一分钟、一小时……之前

假设你想回到一天之前?一觉醒来,你突然想起,有一部分内容其实应该保留下来……但是编辑器在重启之后,就不能够再帮你回到以前的任何时间……

这种情况下,版本控制才是你的救命稻草

分支控制

如果项目只能朝一个方向发展,你会时常在确定方向的问题上犹豫不决。而使用版本控制,创建一个分支各自发展,在适当的时候合并分支,是最好的解决办法

协同工作

很多项目需要协同工作,版本控制能够提供协同工作需要的环境,解决协同工作可能产生的问题

冲突合并

项目成员可能会对一处内容进行不同的修改,版本控制能够反馈这些冲突,以便解决它

常见版本控制系统

传统的版本控制系统,需要在服务器上搭建一个中央仓库,所有成员都是客户端,具有不同的权限。这种方式适应大教堂开发模式,但比较依赖网络,且不够灵活,使用比较广泛的有cvs、svn等

而分布式版本控制系统,则不需要中央仓库,所有的成员都可以完全掌控己方的版本控制系统,通过多种方式灵活的协作。这种方式适应集市开发模式,典型的代表是git


[61] 项目并不总是意味着开发一个大型软件;你帮领导打一份文件,其实也是项目

git 如何工作

尽管 Linus Torvalds 将 git 定位为:“傻瓜式的内容跟踪工具”,但它对不熟悉版本控制的朋友来说,还是过于复杂

所以我们需要先在概念上大概了解,git 是如何工作的

补丁

多数版本控制系统,使用补丁来纪录内容的改动。

当你修改了文件内容,版本控制系统会比较修改后的内容和原来的内容,并使用补丁纪录下来。 无论是查看版本之间的变化,或者需要回溯原来内容,都需要使用补丁中的内容

git 对象

工作树

git将工作目录称为:工作树

索引

工作树的快照,无论是添加、删除文件,或者对文件内容进行修改,都需要提交到索引。git只跟踪被索引的内容

将改动提交到索引,意味着建立一个快照

版本库

存储工作树的各种版本

工作树中只保存当前内容,各种版本通过补丁的形式存储在版本库中

版本名称

git可以使用“版本ID”和“版本标签”作为版本名称

版本ID自动生成,版本标签用git tag命令指定

操作级别

git可以在四种级别上实现版本控制:

改动纪录

改动了文件内容,提交到索引,但未提交到版本库

该级别的常见操作有:add diff

版本纪录

改动被提交到版本库后,就成为一个新的版本

该级别的常见操作有:commit log tag show reset

其中reset、以及分支操作,需要在commit之后,add之前,没有待提交改动纪录的情况下进行

分支

分支为该主线上的系列版本

版本库

协同工作时,需要合并项目成员的版本库

该级别常见的操作有:clone pull push

初始化

创建版本库

git 基于文件夹(工作树)进行版本控制,在一个文件夹中创建 git版本库:

$ cd project/  
$ git init   
Initialized empty Git repository in .git/

1 输出信息:在当前文件夹的 .git/ 目录下创建版本库

将文件提交到 git索引:

git add file1 file2 file3 ……

更方便的作法是将当前文件夹中的所有文件全部加入到索引中

git add .
  • 可以在 .gitignore 文件中设置排除的文件(通常把临时文件排除)

注意:git 只负责管理被索引的文件

此时,文件还没有被提交到版本库。向版本库提交第一个版本:

git commit
git commit -m "备注"

1 调用系统默认编辑器编辑备注内容

版本库状态

使用 git status 命令查看版本库状态。先创建一个演示版本库:

mkdir sandbox                 #新建一个文件夹
cd sandbox/                   #进入该文件夹
git init                      #初始化版本库
touch a b                     #新建 a b 两个文件
git add .                     #将这两个文件提交到索引
git commit -m "创建git版本库"  #将第一个版本提交到版本库

这时使用 git status 查看版本库状态:

# On branch master
nothing to commit (working directory clean)

对文件进行一些操作:

vi a      #编辑 a
rm b      #删除 b
touch c   #新建 c

再用 git status 查看:

# On branch master          #在 master 分支上
# Changes to be committed:  #已提交到索引,等待提交到版本库(其实本例中没有这一段)
#   (use "git reset HEAD <file>..." to unstage)
#
#       new file:  e
#       modified:  f
#
# Changed but not updated:  #改动未提交到索引
#   (use "git add/rm <file>..." to update what will be committed)
#
#       **modified:   a**
#       **deleted:    b**
#
# Untracked files:           #文件未提交到索引
#   (use "git add <file>..." to include in what will be committed)
#
#       **c**
no changes added to commit (use "git add" and/or "git commit -a")

注意:如果只是想删除该文件夹中的版本库,只要删除 .git/ 目录即可

rm -rf .git

配置

git 初始化后,会在.git/目录下创建一个版本库,其中.git/config为配置文件。

用户信息

为当前版本库添加用户信息[62]:

[user]
    name = kardinal
    email = 2999am@gmail.com

也使用全局用户信息,在~/.gitconfig中写入上述内容,或者使用命令:


git config --global user.name "kardinal"
git config --global user.email 2999am@gmail.com

语法高亮

~/.gitconfig文件中添加如下语句,使用容易阅读的彩色来输出信息:

[color]
    branch = auto
    diff = auto
    status = auto

或者自己定义:

branch.current          # color of the current branch
branch.local            # color of a local branch
branch.plain            # color of other branches
branch.remote           # color of a remote branch
diff                    # when to color diff output
diff.commit             # color of commit headers
diff.frag               # color of hunk headers
diff.meta               # color of metainformation
diff.new                # color of added lines
diff.old                # color of removed lines
diff.plain              # color of context text
diff.whitespace         # color of dubious whitespace
status                  # when to color output of git-status
status.added            # color of added, but not yet committed, files
status.changed          # color of changed, but not yet added in the index, files
status.header           # color of header text
status.untracked        # color of files not currently being tracked
status.updated          # color of updated, but not yet committed, files

[62] 这是必需的,请不要忽略

版本更新

现在创建一个 git版本库:(参见“初始化”一节)

mkdir sandbox
cd sandbox/
git init
touch test
git add .
git commit -m "创建git版本库"

git log查看版本纪录:

commit d63e709f565dcd60ab749f0eca27a947b02b8c26
Author: kardinal <2999am@gmail.com>
Date:   Wed Nov 5 14:08:50 2008 +0800

    创建 git版本库

1 版本ID(默认自动生成)

2 提交者

3 提交日期

4 备注

现在对test文件作一些修改:

增加一行内容

git diff查看自上次提交以来发生什么改动:

diff --git a/test b/test
index e69de29..bae0882 100644
--- a/test
+++ b/test
@@ -0,0 +1 @@
+增加一行内容

1 典型的diff输出,如果你设置了彩色输出,这些内容会非常直观的显示

接下来,把这次的更新作为新的版本提交

git add test
git commit -m "增加了一行内容"

1 将本次更新提交到索引(生成快照)。此时使用git diff查看改动纪录,看不到任何内容;但是仍可以使用git diff --cached查看缓存的改动纪录

2 提交为新版本后,便不能使用git diff查看改动纪录

提示:git add提交改动到索引,但并不提交到版本库。如果不想频繁的提交新版本,可以使用该命令提交改动到索引,比较和上一次提交的变化。只要不使用git commit提交,版本库中不会有新的版本

使用git log查看版本库纪录

commit 13aa16309db3693ea8a6b93b8a818e731194824c
Author: kardinal <2999am@gmail.com>
Date:   Wed Nov 5 14:28:04 2008 +0800

    增加了一行内容

commit d63e709f565dcd60ab749f0eca27a947b02b8c26
Author: kardinal <2999am@gmail.com>
Date:   Wed Nov 5 14:08:50 2008 +0800

    创建git版本库

如果想查看每个版本的改动纪录,使用git log -p

commit 13aa16309db3693ea8a6b93b8a818e731194824c
Author: kardinal <2999am@gmail.com>
Date:   Wed Nov 5 14:28:04 2008 +0800

    增加了一行内容

diff --git a/test b/test
index e69de29..bae0882 100644
--- a/test
+++ b/test
@@ -0,0 +1 @@
+增加一行内容

commit d63e709f565dcd60ab749f0eca27a947b02b8c26
Author: kardinal <2999am@gmail.com>
Date:   Wed Nov 5 14:08:50 2008 +0800

    创建git版本库

diff --git a/test b/test
new file mode 100644
index 0000000..e69de29

每次使用git addgit commit两个命令提交版本更新很繁琐,可以使用git commit -a提交(已索引文件的改动)

git commit -a -m "一次新的提交"

版本标签

使用git tag为某一版本创建版本标签:

git tag 1.0 d63e70
git tag 1.1 13aa16
git tag newest HEAD
  • 版本标签存储在.git/refs/tags/目录

使用容易记忆的版本标签进行操作:

git diff 1.0 1.1
git diff 1.0 13aa16
git log 1.0

1 查看1.0和1.1之间的变化

2 查看1.0纪录

时间机器

test文件中随意改动,然后提交

git commit -a -m "意外改动"

git log,增加了一条纪录:

commit d9b03125921d20482937f43ea0bdbfbfb7fe1745
Author: kardinal <2999am@gmail.com>
Date:   Wed Nov 5 15:18:49 2008 +0800

    意外改动

使用git reset命令回溯到历史版本:

git reset HEAD^  
git log
git diff

1 git reset默认使用--mixed选项

2 HEAD表示当前版本,HEAD^表示前一个版本,HEAD^^表示前两个版本,HEAD~4表示前四个版本;也可以使用“版本标签”或“版本ID”来指定版本(只要前几位就可以了)

3 可以看到版本纪录中最后一次提交已经取消

4 可以看到,--mixed选项回溯到提交到索引之前的状态

git reset --soft回溯到已提交到索引但未提交到版本库的状态

git commit -a -m "意外改动"
git reset --soft HEAD^  
git log
git diff
git diff --cached

1 再一次将这些改变提交

2 使用--soft选项回溯到上一版本

3 版本纪录中已取消该版本

4 改动纪录中没有任何内容

5 改动已被提交到索引,但是未提交到版本库,所以缓存的改动纪录还可以查看

注意:git reset 回溯到git add之前的状态;git reset --soft回溯到git add之后的状态

以上方法回溯到历史版本,只是回溯版本库和索引的纪录,而文件的内容并不会回溯到之前的状态,使用git reset --hard命令,将文件内容也一同回溯

git commit -a -m "意外改动"
git reset --hard HEAD^  
git log
git diff --cached  
cat test

1 ……还得提交一次,谁让它是“意外改动”

2 使用--hard选项回溯到上一版本

3 版本纪录中已取消该版本

4 没有任何改动纪录待提交

5 文件内容回溯到上一版本的状态

--hard选项存在一定风险,因为很多情况下,你不能确定内容算不算“意外改动”。这时,可以新建一个分支,在这个分支中进行回溯,处理完成后合并两个分支,参见“分支管理”一节

分支管理

创建分支

git branch命令查看分支:

git branch
* master

1 不带选项,默认为查看分支

2 *表示当前分支

3 master为默认分支

新建分支:

$ git branch slave
$ git checkout slave
M       slave
Switched to branch "slave"
$ git branch  
  master
* slave

1 git branch使用分支名称作参数,新建分支

2 git checkout,切换到指定分支

3 查看分支

4 当前分支已变为slave

使用如下命令删除分支:(先不要删除,后面会用到)

git branch -D 分支名称

合并分支

使用git merge合并分支:

编辑 test
git commit -a -m "slave分支"
git checkout master
git diff master slave      
git merge slave

1 增加一点内容

2 在当前分支提交此版本

3 切换到 master分支

4 比较两个分支

5 合并 slave分支 的内容

处理冲突

如果没有冲突的内容,git 会自动处理合并。如果产生冲突(同一行的内容不一致),git 会输出如下信息:

Auto-merged test
CONFLICT (content): Merge conflict in test
Automatic merge failed; fix conflicts and then commit the result.
  • test文件在合并时发生冲突,需要手动处理冲突,然后后再次提交

现在处理冲突,打开test文件,有如下内容:

 <<<<<<< HEAD:test
 这是master分支中的一行
 =======
 这是slave分支中的一行
 >>>>>>> slave:test

1 当前内容信息

2 当前内容

3 分隔线,分隔冲突的内容

4 slave分支内容

5 slave分支:test文件

修改这部分内容,保留正确的,然后提交

提示:冲突不只在合并分支时产生。无论何种冲突,处理的方法是一样的

合并后可以删除该分支:

git brancd -d slave

1 -D强行删除分支;-d只有分支内容被合并后才能删除

通过文件协作

git 可以通过补丁文件进行协作(使用 email 传送补丁文件)

首先通过 git clone 创建一个镜像版本库,使用 git branch -a命令查看所有分支

$ git clone http://linuxtoy.org/path  [local]
$ cd [local]
$ git branch -a
* master
  origin/HEAD
  origin/master

1 原始版本库路径

2 镜像版本库路径。它是可选的,如果没有指定,则使用和发起者同样的路径(文件夹名称)

其中origin 为原始版本库镜像,在 master 分支上的工作,要生成对于 origin 的补丁,origin 必须与原始版本库保持一致,不要试图修改它

git fetch origin   #更新 origin 分支。如果 origin 分支不是最新的原始版本库,会产生错误的补丁文件
git rebase  origin  #将工作迁移到最新原始版本库基础上
git format-patch origin #生成补丁文件
  • 使用 git rebase 后可能会产生冲突,手动处理

生成的补丁文件为 0001-[备注].patch,发起者得到补丁后,使用 git am 命令将这个补丁应用到版本库

git checkout -b patched
git am 0001-[备注].patch  
git checkout master
git diff master patched
git merge patched

1 为谨慎起见,创建一个名为 “patched” 的分支,切换到此分支

2 在 “patched” 分支中应用补丁

通过网络协作

git 提供相当灵活的协作方式,最常见的方式为:协作者获得原始版本库的镜像,并在上面工作;发起者从协作者那里获取更新

协作者通过git clone创建一个镜像版本库:

git clone user@url:~/path [local]

网络对于 git 来说是透明的,凡是可以访问的位置,如 http、ftp、ssh……,甚至本地路径,对于 git 来说没有什么区别。

通过以下命令,创建一个本机原始版本库sandbox的镜像project,是允许的:

git clone ~/sandbox project

对于没有指定协议的远程路径,git 默认使用 ssh

(ssh://)user`@`127.0.0.1`:`~/sandbox

使用git pull获取协作者版本库中的内容:

git pull user@127.0.0.1:~/sanbox master[:newest]

1 版本库

2 分支名称

3 版本名称(可选。使用版本ID、版本标签,请不要使用“HEAD”)

提示:git pull 基于“版本”操作,也就是说,只有提交后才可以进行;这个命令会比较两个版本的时间戳,只获取更新的版本

当发起者进行了更新后,协作者应从发起者那里获取最新的原始版本库,并将当前工作迁移到最新的原始版本库基础上

git fetch origin         #获取最新原始版本库
git rebase origin/master #将工作迁移到最新原始版本库

这时发起者再次使用 git pull 从协作者那里获取更新……

gitweb

首先配置 web 服务器,使其支持 cgi,参见“CGI”一节

将 git 工作树拷贝到 web 服务器目录下:

cp -r sandbox /home/lighttpd/html/

gitweb 通常随 git 一同安装,拷贝文件到 git 工作树

cp /usr/share/gitweb/* /home/lighttpd/html/sandbox

检查 /home/lighttpd/html/sandbox/gitweb.cgi 文件中的如下语句

our $GIT = "/usr/bin/git";
our $projectroot = ".";

1 git 执行文件位置

2 项目根目录,也可以使用绝对路径,如 /home/lighttpd/html/sandbox

修改项目描述,编辑项目根目录下的 .git/description 文件

这样就建立了一个 gitweb 站点,通过以下地址访问:

http://linuxtoy.org/sandbox/gitweb.cgi

如果想通过 http 协议使用,例如:

git clone http://linuxtoy.org/sandbox/.git

则需要在项目根目录下执行 git update-server-info