ProGit 阅读笔记

Git 由来

版本控制

版本控制是一种记录若干文件内容变化,以便将来查阅特定版本修订情况的系统。

集中化的版本控制系统

如 CVS, Subversion 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,客户端通过客户端连接到服务器,取出最新文件或者提交更新。

1.jpg

分布式版本控制系统

如 Git, Bazaar 等,客户端并不只是提取最新版本的文件快照,而是把原始的代码仓库完整地镜像下来。这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的
本地仓库恢复。因为每一次的提取操作,实际上都是一次对代码仓库的完整备份)。

2.jpg

Git 相比于 svn 等的特点

  • 直接快照,而非比较差异,为提高性能。
3.jpg 4.jpg
  • 近乎所有操作都可本地执行。

    由于其分布式的特点,每个客户端都保存着仓库的备份,在 Git 中的绝大多数操作都只需要访问本地文件和资源,不用连网,如提交更新。

  • 时刻保持数据完整性。

    在保存到Git 之前,所有数据都要进行内容的校验和(checksum)计算,并将此结果作为数据的唯一标识和索引。如果文件在传输时变得不完整,或者磁盘损坏导致文件数据缺失,Git 都能立即察觉。Git 使用SHA-1 算法计算数据的校验和,通过对文件的内容或目录的结构计算出一个SHA-1 哈希值,作为指纹字符串。

三种状态

对于任何一个文件,在 Git 内都只有三种状态:已提交
(committed),已修改(modified)和已暂存(staged)。

Git 管理项目时,文件流转的三个工作区域:Git 的本地数据目录,工作目录以􀷽暂存区
域。

5.jpg

Git 基础

取得项目的 Git 仓库

从当前目录初始化:

1
$ git init

从现有仓库克隆:

1
$ git clone git://github.com/schacon/grit.git

克隆某个分支:

1
$ git clone -b breach-name git://github.com/schacon/grit.git

记录每次更新到仓库

工作目录下面的所有文件都不外乎这两种状态:已跟踪或未跟踪。

已跟踪的文件是指本来就被纳入版本控制管理的文件,在上次快照中有它们的记录,工作一段时间后,它们的状态可能是未更新,已修改或者已放入暂存区。而所有其他文件都属于未跟踪文件。它们既没有上次更新时的快照,也不在当前的暂存区域。

6.jpg

检查当前文件状态:

1
$ git status

跟踪新文件以及将跟踪后更新后暂存:

1
$ git add README

要暂存这次更新,需要运行git add 命令(这是个多功能命令,根据目标文件的状态不同,此命令的效果也不同:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时
把有冲突的文件标记为已解决状态等)

忽略某些文件,编辑 .gitignore 文件:

1
2
3
$ cat .gitignore
*.[oa]
*~

查看已暂存和未暂存的更新:

1
2
3
4
5
6
7
8
$ git diff
修改之后还没有暂存起来的变化内
$ git diff --cached
看已经暂存起来的文件和上次提交时的快照之间的差异

提交更新:

1
$ git commit

移除文件,即从已跟踪文件清除:

1
$ git rm grit.gemspec

如果删除之前修改过并且已经放到暂存区域的话,则必须
要用强制删除选项-f(译注:即force 的首字母),以防误删除文件后丢失修改的内容。

把文件从Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。用–cached 选项即可。

查看历史提交

回顾提交历史:

1
$ git log

-2 仅显示最近的两次更新:

1
$ git log -2

git log 还有其他一些选项,方便查看内容。

撤销操作

撤消刚才的提交操作,重新提交:

1
$ git commit --amend

取消已经暂存的文件,不小心用 git add * 全加到了暂存区域。该如何撤消暂存其中的一个文件呢?取消暂存benchmarks.rb 文件:

1
2
3
git add *
git reset HEAD benchmarks.rb

取消对文件的修改,回到之前的状态,与上面的相比,该命令取消了未暂存的文件修改:

1
$ git checkout -- benchmarks.rb

远程仓库的使用

查看当前的远程库:

1
$ git remote -v

添加远程仓库:

1
git remote add pb git://github.com/paulboone/ticgit.git

从远程仓库抓取数据:

1
$ git fetch [remote-name]

有一点很重要,
需要记住,fetch 命令只是将远端的数据拉到本地仓库,并不自动合并到当前工作分支,只有当你确实准备好
了,才能手工合并,git pull 相当于 git fetch 和 git merge。

推送数据到远程仓库:

1
$ git push [remote-name] [branch-name]

重命名远端仓库:

1
$ git remote rename [remote-name] [new-name]

删除远端仓库:

1
$ git remote rm [remote-name]

Git 分支

何谓分支

在Git 中提交时,会保存一个提交(commit)对象,它包含一个指向暂存内容快照的指针,作者和相关附属信息,以􀷽一定数量(也可能没有)指向该提交对象直接祖先的指针:第一次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。

7.jpg 8.jpg

Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。


创建 test 分支:

1
$ git branch testing

会在当前 commit 对象上新建一个分支指针。

9.jpg

切换到新建的 testing 分支:

1
$ git checkout testing

基本的分支与合并

在 master 上与 hotfix 分支合并:

1
2
$ git checkout master
$ git merge hotfix

12.jpg

上述方式的合并叫快进。

master 和 iss53 的合并叫合并提交:

1
2
$ git checkout master
$ git merge iss53

13.jpg

有时合并的时候可能会遇到冲突,任何包含未解决冲突的文件都会以未合并(unmerged)状态列出。

14.jpg

可以看到======= 隔开的上半部分,是 HEAD (即 master 分支,在运行 merge 命令时检出的分支)中的内容,下半部分是在 iss53 分支中的内容。解决冲突的办法无非是二者选其一或者由你亲自整合到一起。

解决了所有文件里的所有冲突后,运行 git add 将把它们标记为已解决(resolved)。因为一旦暂存,就表示冲突已经解决。

分支管理

给出当前所有分支的清单:

1
2
3
4
$ git branch
iss53
* master
testing

要从该清单中筛选出你已经(或尚未)与当前分支合并的分支,可以用–merge 和–no-merged 选项,比如git branch -merge 查看哪些分支已被并入当前分支:

1
2
3
$ git branch --merged
iss53
* master

远程分支

远程分支(remote branch)是对远程仓库状态的索引。它们是一些无法移动的本地分支;只有在进行 Git 的网络活动时才会更新。

(远程仓库名)/(分支名) 这样的形式表示远程分支。

15.jpg

可以运行 git fetch origin 来进行同步。该命令首先找到origin 是哪个服务器,从上面获取你尚未拥有的数据,更新你本地的数据库,然后把 origin/master 的指针移到它最新的位置。

16.jpg

如果你有个叫 serverfix 的分支需要和他人一起开发,可以运行git push (远程仓库名) (分支名):

1
$ git push origin serverfix:serferfix

它的意思是“提取我的 serverfix 并更新到远程仓库的serverfix”。通过此语法,你可以把本地分支推送到某个命名不同的远程分支:若想把远程分支叫作 awesomebranch,可以用git push origin serverfix:awesomebranch 来推送数据。

删除远程分支,git push [远程名] :[分支名]。

1
$ git push origin :serverfix

衍合

把一个分支整合到另一个分支的办法有两种:merge(合并) 和 rebase(衍合)。

衍合(rebase),有了rebase 命令,就可以把在一个分支里提交的改变在另一个分支里重放一遍。

它的原理是回到两个分支(你所在的分支和你想要衍合进去的分支)的共同祖先,提取你所在分支每次提交时产生的差异(diff),把这些差异分别保存到临时文件里,然后从当前分支转换到你需要衍合入的分支,依序施用每一个差异补丁文件。

1
2
3
4
$ git checkout experiment
$ git rebase master
$ git checkout master
$ git merge experiment
17.jpg

衍合的风险:永远不要衍合那些已经推送到公共仓库的更新。

服务器上的 Git

协议

Git 可以使用四种主要的协议来传输数据:本地传输,SSH 协议,Git 协议和HTTP 协议。

SSH 协议

SSH 是唯一一个同时便于读和写操作的网络协议。SSH 同
时也是一个验证授权的网络协议。

通过SSH 克隆一个Git 仓库:

1
$ git clone ssh://user@server:project.git

Git 默认使用 SSH。

1
$ git clone user@server:project.git

优点:

* 首先,如果你想拥有对网络仓库的写权限,基本上不可能不使用 SSH 。
* 再次,通过 SSH 进行访问是安全的——所有数据传输都是加密和授权的。

缺点:

* SSH 的限制在于你不能通过它实现仓库的匿名访问。即使仅为读取数据,人们也必须在能通过 SSH 访问主机的前提下才能访问仓库,这使得 SSH 不利于开源的项目。

Git 协议

这是一个包含在 Git 软件包中的特殊守护进程; 它会监听一个提供类似于 SSH 服务的特定端口(9418)

优点:

* Git 协议是现存最快的传输协议,因为省去了加密和授权的开销。

缺点:

* Git 协议消极的一面是缺少授权机制。用 Git 协议作为访问项目的唯一方法通常是不可取的。一般做法是,同时提供SSH 接口,让几个开发者拥有推送(写)权限,其他人通过git:// 拥有只读权限。

HTTP/S 协议

HTTP 或 HTTPS 协议的优美之处在于架设的简便性。基本上, 只需要把 Git 的纯仓库文件放在 HTTP 的文件根目录下,配置一个特定的 post-update 挂钩(hook),就搞定了。

克隆仓库:

1
$ git clone http://example.com/gitproject.git

优点:

* 使用 HTTP 协议的好处是易于架设。几条必要的命令就可以让全世界读取到仓库的内容。

缺点:

* 相对来说客户端效率更低。克隆或者下载仓库内容可能会花费更多时间,而且 HTTP 传输的体积和网络开销比其他任何一个协议都大。因为它没有按需供应的能力——传输过程中没有服务端的动态计算——因而 HTTP 协议经常会被称为傻瓜(dumb) 协议。

分布式 Git

储藏

经常有这样的事情发生,当你正在进行项目中某一部分的工作,里面的东西处于一个比较杂乱的状态,而你想转到其他分支上进行一些工作。问题是,你不想提交进行了一半的工作,否则以后你无法回到这个工作点。解决这个问题的办法就是git stash命令。

现在你想切换分支,但是你还不想提交你正在进行中的工作;所以你储藏这些变更。为了往堆栈推送一个新的储藏,只要运行git stash:

1
$ git stash

要查看现有的储藏,你可以使用git stash list:

1
2
3
4
$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051... Revert "added file_size"
stash@{2}: WIP on master: 21d80a5... added number to log

你可以重新应用你刚刚实施的储藏:

1
2
3
4
5
$ git stash apply
or
$ git stash apply stash@2

apply 选项只尝试应用储藏的工作——储藏的内容仍然在栈上。要移除它,你可以运行git stash drop:

1
$ git stash drop stash@{0}

你也可以运行git stash pop 来重新应用储藏,同时立刻将其从堆栈中移走。

重写历史

改变最近一次提交:

1
$ git commit --amend

修改多个提交说明:

如果你想修改或改变说明、增加文件、删除文件或任何其他事情。你可以通过给 git rebase 增加 -i 选项来以交互方式地运行 rebase。你必须通过告诉命令衍合到哪次提交,来指明你需要重写的提交的回溯深度。

修改最近三次提交,范围内的每一次提交都会被重写,无论你是否修改说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git rebase -i HEAD~3
pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick = use commit
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

可以使用交互式的衍合来彻底重排或删除提交。

重排提交:

1
2
3
pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

改为:

1
2
pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit

压制(Squashing)提交:

1
2
3
pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file

当你保存并退出编辑器,Git 会应用全部三次变更然后将你送回编辑器来归并三次提交说明。

拆分提交就是撤销一次提交,然后多次部分地暂存或提交直到结束。

将“updated README formatting and added blame”拆分成两次提交:第一次为“updated
README formatting”,第二次为“added blame”。你可以在rebase -i脚本中修改你想拆分的提交前的指令
为“edit”。

拆分提交:

1
2
3
pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

1
2
3
4
5
6
$ git reset HEAD^
$ git add README
$ git commit -m 'updated README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'added blame'
$ git rebase --continue

衍合与挑拣

另一个引入代码的方法是挑拣。挑拣类似于针对某次特定提交的衍合。它首先提取某次提交的补丁,然后试着应用在当前分支上。如果某个特性分支上有多个 commits,但你只想引入其中之一就可以使用这种方法。

1
2
3
4
$ git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf
Finished one cherry-pick.
[master]: created a0a41a9: "More friendly message when locking the index fails."
3 files changed, 17 insertions(+), 3 deletions(-)
18.jpg