第 28 章 Git 版本控制系统
目录
版本控制之道
几乎所有项目[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/
将文件提交到 git索引:
git add file1 file2 file3 ……
更方便的作法是将当前文件夹中的所有文件全部加入到索引中
git add .
- 可以在
.gitignore
文件中设置排除的文件(通常把临时文件排除)
注意:git 只负责管理被索引的文件
此时,文件还没有被提交到版本库。向版本库提交第一个版本:
git commit
git commit -m "备注"
版本库状态
使用 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 = [email protected]
也使用全局用户信息,在~/.gitconfig
中写入上述内容,或者使用命令:
git config --global user.name "kardinal"
git config --global user.email [email protected]
语法高亮
在~/.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 <[email protected]>
Date: Wed Nov 5 14:08:50 2008 +0800
创建 git版本库
现在对test
文件作一些修改:
增加一行内容
git diff查看自上次提交以来发生什么改动:
diff --git a/test b/test
index e69de29..bae0882 100644
--- a/test
+++ b/test
@@ -0,0 +1 @@
+增加一行内容
接下来,把这次的更新作为新的版本提交
git add test
git commit -m "增加了一行内容"
将本次更新提交到索引(生成快照)。此时使用git diff查看改动纪录,看不到任何内容;但是仍可以使用git diff --cached查看缓存的改动纪录
提示:
git add
提交改动到索引,但并不提交到版本库。如果不想频繁的提交新版本,可以使用该命令提交改动到索引,比较和上一次提交的变化。只要不使用git commit
提交,版本库中不会有新的版本
使用git log查看版本库纪录
commit 13aa16309db3693ea8a6b93b8a818e731194824c
Author: kardinal <[email protected]>
Date: Wed Nov 5 14:28:04 2008 +0800
增加了一行内容
commit d63e709f565dcd60ab749f0eca27a947b02b8c26
Author: kardinal <[email protected]>
Date: Wed Nov 5 14:08:50 2008 +0800
创建git版本库
如果想查看每个版本的改动纪录,使用git log -p
commit 13aa16309db3693ea8a6b93b8a818e731194824c
Author: kardinal <[email protected]>
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 <[email protected]>
Date: Wed Nov 5 14:08:50 2008 +0800
创建git版本库
diff --git a/test b/test
new file mode 100644
index 0000000..e69de29
每次使用git add和git 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
时间机器
在test
文件中随意改动,然后提交
git commit -a -m "意外改动"
git log
,增加了一条纪录:
commit d9b03125921d20482937f43ea0bdbfbfb7fe1745
Author: kardinal <[email protected]>
Date: Wed Nov 5 15:18:49 2008 +0800
意外改动
使用git reset命令回溯到历史版本:
git reset HEAD^
git log
git diff
HEAD
表示当前版本,HEAD^
表示前一个版本,HEAD^^
表示前两个版本,HEAD~4
表示前四个版本;也可以使用“版本标签”或“版本ID”来指定版本(只要前几位就可以了)
git reset --soft回溯到已提交到索引但未提交到版本库的状态
git commit -a -m "意外改动"
git reset --soft HEAD^
git log
git diff
git diff --cached
改动已被提交到索引,但是未提交到版本库,所以缓存的改动纪录还可以查看
注意:
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
--hard选项存在一定风险,因为很多情况下,你不能确定内容算不算“意外改动”。这时,可以新建一个分支,在这个分支中进行回溯,处理完成后合并两个分支,参见“分支管理”一节
分支管理
创建分支
git branch命令查看分支:
git branch
* master
新建分支:
$ git branch slave
$ git checkout slave
M slave
Switched to branch "slave"
$ git branch
master
* slave
使用如下命令删除分支:(先不要删除,后面会用到)
git branch -D 分支名称
合并分支
使用git merge合并分支:
编辑 test
git commit -a -m "slave分支"
git checkout master
git diff master slave
git merge 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
修改这部分内容,保留正确的,然后提交
提示:冲突不只在合并分支时产生。无论何种冲突,处理的方法是一样的
合并后可以删除该分支:
git brancd -d slave
通过文件协作
git 可以通过补丁文件进行协作(使用 email 传送补丁文件)
首先通过 git clone 创建一个镜像版本库,使用 git branch -a
命令查看所有分支
$ git clone http://linuxtoy.org/path [local]
$ cd [local]
$ git branch -a
* master
origin/HEAD
origin/master
其中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
通过网络协作
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 [email protected]:~/sanbox master[:newest]
版本名称(可选。使用版本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 = ".";
修改项目描述,编辑项目根目录下的 .git/description
文件
这样就建立了一个 gitweb 站点,通过以下地址访问:
http://linuxtoy.org/sandbox/gitweb.cgi
如果想通过 http 协议使用,例如:
git clone http://linuxtoy.org/sandbox/.git
则需要在项目根目录下执行 git update-server-info