gitbook 简介
- 欢迎来使用Git, 它是一个快速的分布式版本控制系统。
- 以介绍Git如何存储数据为始,了解它和其它版本控制系统有何异同
- 介绍一些Git的基本用法,将在90%的时间都在使用的命令。
- Git中级用法,这些用法也许会替换掉前面的基本用法。在了解前面的基本用法后, 这些看起来像魔术一样的命令,你可能会用起来很爽。
- Git的高级用法,在特定的环境会非常有用,获得全面的Git知识。
- 学习 Git 配合脚本、部署工具、编辑器和其它工具一起工作。将Git 集成进工作环境。
- Git hacker :low-level documentation,包括Git 的内核和协议如何运作等等。
GIT对象模型
- SHA: 区分不同对象名。
- 对象:每个对象(object) 包括三个部分:类型,大小和内容。
- 与SVN的区别
- Blob对象: 一个blob通常用来存储文件的内容.
$ git show 6ff87c4664
- Tree 对象: 一个tree对象有一串(bunch)指向blob对象或是其它tree对象的指针,它一般用来表示内容之间的目录层次关系。
$ git ls-tree fb3a8bdd0ce
- Commit对象: "commit对象"指向一个"tree对象", 并且带有相关的描述信息; 一个“commit”只指向一个"tree",它用来标记项目某一个特定时间点的状态。它包括一些关于时间点的元数据,如时间戳、最近一次提交的作者、指向上次提交(commits)的指针等等。
$ git show -s --pretty=raw 2be7fcb476
-
对象模型
commit到一个Git仓库中示意图
你可以看到: 每个目录都创建了 tree对象 (包括根目录), 每个文件都创建了一个对应的 blob对象 . 最后有一个 commit对象来指向根tree对象(root of trees), 这样我们就可以追踪项目每一项提交内容. - 标签对象: 一个标签对象包括一个对象名(即SHA1签名), 对象类型, 标签名, 标签创建人的名字("tagger"), 还有一条可能包含有签名(signature)的消息.
$ git cat-file tag v1.5.0
Git目录 与 工作目录
- Git目录: 是为你的项目存储所有历史和元信息的目录
$>tree -L 1
.
|-- HEAD # 这个git项目当前处在哪个分支里
|-- config # 项目的配置信息,git config命令会改动它
|-- description # 项目的描述信息
|-- hooks/ # 系统默认钩子脚本目录
|-- index # 索引文件
|-- logs/ # 各个refs的历史信息
|-- objects/ # Git本地仓库的所有对象 (commits, trees, blobs, tags)
`-- refs/ # 标识你项目里的每个分支指向了哪个提交(commit)。
- 工作目录:
- 'Git目录' 一般就是指项目根目录下的'.git'目录.
- Git的 '工作目录' 存储着你现在签出(checkout)来用来编辑的文件. 当你在项目的不同分支间切换时, 工作目录里的文件经常会被替换和删除. 所有历史信息都保存在 'Git目录'中 ; 工作目录只用来临时保存签出(checkout) 文件的地方, 你可以编辑工作目录的文件直到下次提交(commit)为止.
Git索引
Git索引是一个在你的工作目录和项目仓库间的暂存区(staging area). 有了它, 你可以把许多内容的修改一起提交(commit). 如果你创建了一个提交(commit), 那么提交的是当前索引(index)里的内容, 而不是工作目录中的内容.
- 查看索引
$>git status
# On branch master
# Your branch is behind 'origin/master' by 11 commits, and can be fast-forwarded.
#
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: daemon.c
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: grep.c
# modified: grep.h
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# blametree
# blametree-init
# git-gui/git-citool
使用 git status 命令是查看索引内容的最简单办法, 运行可看到:
- 哪些文件被暂存了(就是在你的Git索引中);
- 哪些文件被修改了但是没有暂存;
- 还有哪些文件没有被跟踪(untracked).
如果完全掌握了索引(index), 你就一般不会丢失任何信息, 只要你记得名字描述信息(name of the tree that it described)就能把它们找回来.
Git 安装与初始化
Git 配置
$ git config --global user.name "zhangjun"
$ git config --global user.email "zhangjun@zhihu.com"
基本用法
获得一个Git仓库
- Clone一个仓库
$ git clone https://git.coding.net/staufenzhang/xsbAppTest.git
-
初始化一个新的仓库
假设有一个”project.tar.gz”的压缩文件里包含了一些文件,执行命令让它置于Git的版本控制管理之下:
$ tar xzf project.tar.gz
$ cd project
$ git init
Git会输出:
Initialized empty Git repository in .git/
project目录下会有一个名叫".git" 的目录被创建,这意味着一个仓库被初始化了。
正常的工作流程
$ git add file1
$ git diff
$ git status
$ git commit "修改注释"
$ git push
git正常的提交流程.png
git正常的撤销流程.png
分支与合并@基础
分支与合并.png- 一个Git仓库可以维护很多开发分支。创建一个新的叫”experimental”的分支:
$ git branch experimental
$ git branch
> experimental
>* master
- “experimental” 分支是你刚才创建的,“master”分支是Git系统默认创建的主分支。星号(“”)*标识了你当工作在哪个分支下,输入:
$ git checkout experimental
- 切换到”experimental”分支,先编辑里面的一个文件,再提交(commit)改动,最后切换回 “master”分支。
切换到”experimental”分支,先编辑里面的一个文件,再提交(commit)改动,最后切换回 “master”分支。
(edit file)
$ git commit -a
$ git checkout master
- 你现在可以看一下你原来在“experimental”分支下所作的修改还在不在;因为你现在切换回了“master”分支,所以原来那些修改就不存在了。
你现在可以在“master”分支下再作一些不同的修改:
(edit file)
$ git commit -a
- 这时,两个分支就有了各自不同的修改(diverged);我们可以通过下面的命令来合并“experimental”和“master”两个分支:
$ git merge experimental
- 如果这个两个分支间的修改没有冲突(conflict), 那么合并就完成了。如有有冲突,输入下面的命令就可以查看当前有哪些文件产生了冲突:
$ git diff
- 当你编辑了有冲突的文件,解决了冲突后就可以提交了:
$ git commit -a
- 提交(commit)了合并的内容后就可查看一下:
$ gitk
执行了gitk后会有一个很漂亮的图形的显示项目的历史。
- 这时你就可以删除掉你的 “experimental” 分支了(如果愿意):
$ git branch -d experimental
git branch -d只能删除那些已经被当前分支的合并的分支. 如果你要强制删除某个分支的话就用git branch –D;下面假设你要强制删除一个叫”crazy-idea”的分支:
$ git branch -D crazy-idea
分支是很轻量级且容易的,这样就很容易来尝试它。
如何合并
- 你可以用下面的命令来合并两个分离的分支:
git merge
:
$ git merge branchname
- 这个命令把分支"branchname"合并到了当前分支里面。如有冲突(冲突--同一个文件在远程分支和本地分支里按不同的方式被修改了);那么命令的执行输出就像下面一样
$ git merge next
100% (4/4) done
Auto-merged file.txt
CONFLICT (content): Merge conflict in file.txt
Automatic merge failed; fix conflicts and then commit the result.
- 在有问题的文件上会有冲突标记,在你手动解决完冲突后就可以把此文件添 加到索引(index)中去,用git commit命令来提交,就像平时修改了一个文件 一样。
如果你用gitk ?
来查看commit的结果,你会看到它有两个父分支:一个指向当前 的分支,另外一个指向刚才合并进来的分支。
解决合并中的冲突
- 如果执行自动合并没有成功的话,git会在索引和工作树里设置一个特殊的状态, 提示你如何解决合并中出现的冲突。
有冲突(conflicts)的文件会保存在索引中,除非你解决了问题了并且更新了索引,否则执行git commit
都会失败:
$ git commitfile.txt: needs merge
- 如果执行
git status
会显示这些文件没有合并(unmerged),这些有冲突的文件里面会添加像下面的冲突标识符:
<<<<<<< HEAD:file.txt
Hello world
=======
Goodbye
>>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt
- 你所需要的做是就是编辑解决冲突,(接着把冲突标识符删掉),再执行下面的命令:
$ git add file.txt
$ git commit
注意:提交注释里已经有一些关于合并的信息了,通常是用这些默认信息,但是你可以添加一些你想要的注释。
上面这些就是你要做一个简单合并所要知道的,但是git提供更多的一些信息来帮助解决冲突。
撒销一个合并
- 如果你觉得你合并后的状态是一团乱麻,想把当前的修改都放弃,你可以用下面的命令回到合并之前的状态:
$ git reset --hard HEAD
或者你已经把合并后的代码提交,但还是想把它们撒销:
$ git reset --hard ORIG_HEAD
但是刚才这条命令在某些情况会很危险,如果你把一个已经被另一个分支合并的分支给删了,那么 以后在合并相关的分支时会出错。
快速向前合并
- 还有一种需要特殊对待的情况,在前面没有提到。通常,一个合并会产生一个合并提交(commit), 把两个父分支里的每一行内容都合并进来。
- 但是,如果当前的分支和另一个分支没有内容上的差异,就是说当前分支的每一个提交(commit)都已经存在另一个分支里了,git 就会执行一个“快速向前"(fast forward)操作;git 不创建任何新的提交(commit),只是将当前分支指向合并进来的分支。
查看历史 -Git日志
$ git log
-
日志统计
如果用--stat选项使用'git log',它会显示在每个提交(commit)中哪些文件被修改了, 这些文件分别添加或删除了多少行内容.
$ git log --stat
git log --stat.png
- 格式化日志
- 你可以按你的要求来格式化日志输出。‘--pretty'参数可以使用若干表现格式,如‘oneline':
$ git log --pretty=oneline
- 或者你也可以使用 'short' 格式:
$ git log --pretty=short
- 你也可用‘medium','full','fuller','email' 或‘raw'. 如果这些格式不完全符合你的相求, 你也可以用‘--pretty=format'参数(参见:git log)来创建你自己的"格式“.
$ git log --pretty=format:'%h was %an, %ar, message: %s'
- 另一个有趣的事是:你可以用'--graph'选项来可视化你的提交图(commit graph),就像下面这样:
$ git log --pretty=format:'%h : %s' --graph
它会用ASCII字符来画出一个很漂亮的提交历史(commit history)线。
- 日志排序
-
按默认情况,提交(commits)会按逆时间(reverse chronological)顺序显示。
-
但是你也可以指定‘--topo-order'参数,这就会让提交(commits)按拓朴顺序来显示(就是子提交在它们的父提交前显示). 如果你用git log命令按拓朴顺序来显示git仓库的提交日志,你会看到“开发线"(development lines)都会集合在一起.
$ git log --pretty=format:'%h : %s' --topo-order --graph
- 你也可以用'--date-order'参数,这样显示提交日志的顺序主要按提交日期来排序. 这个参数和'--topo-order'有一点像,没有父分支会在它们的子分支前显示,但是其它的东东还是按交时间来排序显示。你会看到"开发线"(development lines)没有集合一起,它们会像并行开发(parallel development)一样跳来跳去的:
$ git log --pretty=format:'%h : %s' --date-order --graph
- 最后,你也可以用 ‘--reverse'参数来逆向显示所有日志。
比较提交 - Git Diff
- 你可以用 git diff 来比较项目中任意两个版本的差异。
$ git diff master..test
上面这条命令只显示两个分支间的差异,如果你想找出‘master’,‘test’的共有 父分支和'test'分支之间的差异,你用3个‘.'来取代前面的两个'.' 。
-
哪些内容会被提交(commit)
如果你要看在下次提交时要提交的内容(staged,添加到索引中),你可以运行:
$ git diff --cached
上面的命令会显示你当前的索引和上次提交间的差异;这些内容在不带"-a"参数运行 "git commit"命令时就会被提交。
$ git diff HEAD
上面这条命令会显示你工作目录与上次提交时之间的所有差别,这条命令所显示的 内容都会在执行"git commit -a"命令时被提交。
- 更多的比较选项
- 如果你要查看当前的工作目录与另外一个分支的差别,你可以用下面的命令执行:
$ git diff test
- 这会显示你当前工作目录与另外一个叫'test'分支的差别。你也以加上路径限定符,来只 比较某一个文件或目录。
$ git diff HEAD -- ./lib
- 如果不是查看每个文件的详细差别,而是统计一下有哪些文件被改动,有多少行被改 动,就可以使用‘--stat' 参数。
$>git diff --stat
分布式的工作流程
- 假设Alice现在开始了一个新项目,在/home/alice/project建了一个新的git 仓库(repository);另一个Bob的工作目录也在同一台机器,他要提交代码。
Bob 执行了这样的命令:
$ git clone /home/alice/project myrepo
这就建了一个新的叫"myrepo"的目录,这个目录里包含了一份Alice的仓库的 克隆(clone). 这份克隆和原始的项目一模一样,并且拥有原始项目的历史记 录。
- Bob 做了一些修改并且提交(commit)它们:
(edit files)
$ git commit -a
(repeat as necessary)
- 当他准备好了,他告诉Alice从仓库/home/bob/myrepo中把他的修改给拉 (pull)下来。她执行了下面几条命令:
$ cd /home/alice/project
$ git pull /home/bob/myrepo master
这就把Bob的主(master)分支合并到了Alice的当前分支里了。
-
如果Alice在 Bob修改文件内容的 同时 也做了修改的话
,她可能需要手工去修复冲突**. (注意:"master"参数在上面的命令中并不一定是必须的,因为这是一个 默认参数)
- git pull命令执行两个操作: 它从远程分支(remote branch)抓取修改 的内容,然后把它合并进当前的分支。
如果你要经常操作远程分支(remote branch)
,你可以定义它们的缩写:
$ git remote add bob /home/bob/myrepo
- 这样,Alic可以用
"git fetch"" 来执行"git pull"前半部分的工作
, 但是这条命令并不会把抓下来的修改合并到当前分支里。
$ git fetch bob
- 我们用
git remote 命令建立了Bob的运程仓库的缩写
,用这个(缩写) 名字我从Bob那得到所有远程分支的历史记录。在这里远程分支的名字就叫bob/master
$ git log -p master..bob/master
上面的命令把Bob从Alice的主分支(master)中签出后所做的修改全部显示出来。
- 当检查完修改后,Alice就可以把修改合并到她的主分支中。
$ git merge bob/master
这种合并(merge)也可以用pull来完成,就像下面的命令一样:
$ git pull . remotes/bob/master
注意:git pull 会把远程分支合并进当前的分支里,而不管你在命令 行里指定什么。
- 其后,Bob可以更新它的本地仓库--把Alice做的修改拉过来(pull):
$ git pull
- 如果Bob从Alice的仓库克隆(clone),那么他就不需要指定Alice仓库的地 址;因为Git把Alice仓库的地址存储到Bob的仓库配库文件,这个地址就是 在git pull时使用:
$ git config --get remote.origin.url/home/alice/project
(如果要查看git clone创建的所有配置参数,可以使用"git config -l", git config 的帮助文件里解释了每个参数的含义.)
Git同时也保存了一份最初(pristine)的Alice主分支(master),在 "origin/master"下面.
$ git branch -r origin/master
- 如果Bob打算在另外一台主机上工作,他可以通过
ssh协议
来执行"clone" 和"pull"操作:
$ git clone alice.org:/home/alice/project myrepo
- git有他自带的协议(native protocol),还可以使用rsync或http; 你可以点 这里 git pull 看一看更詳細的用法。
- Git也可以像CVS一样来工作:有一个中心仓库,不同的用户向它推送(push) 自己所作的修改;
公共Git仓库
- 另外一个提交修改的办法,就是告诉项目的维护者(maintainer)用 git pull 命令从你的仓库里把修改拉下来。这和从主仓库"里更新代码类似,但是是从 另外一个方向来更新的。
- 如果你和维护者(maintainer)都在同一台机器上有帐号,那么你们可以互相从对 方的仓库目录里直接拉(pull)所作的修改;git命令里的仓库地址也可以是本地 的某个目录名:
$ git clone /path/to/repository$ git pull /path/to/other/repository
- 也可以是一个ssh地址:
$ git clone ssh://yourhost/~you/repository
- 如果你的项目只有很少几个开发者,或是只需要同步很少的几个私有仓库, 上面的方法也许够你用的。
-
然而,更通用的作法是维护几个不同的公开仓库(public repository).
这样可以把每个人的工作进度和公开仓库清楚的分开。
- 你还是每天在你的本地私人仓库里工作,但是会定期的把本地的修改推(push) 到你的公开仓库中;其它开发者就可以从这个公开仓库来拉(pull)最新的代码。 如果其它开发者也有他自己的公共仓库,那么他们之间的开发流程就如下图 所示:
you push
your personal repo ------------------> your public repo
^ |
| |
| you pull | they pull
| |
| |
| they push V
their public repo <------------------- their repo
将修改推到一个公共仓库
-
通过http或是git协议
,其它维护者可以抓取(fetch)你最近的修改,但是他们 没有写权限。这样,这需要将本地私有仓库的最近修改上传公共仓库中。
- 译者注: 通过http的WebDav协议是可以有写权限的,也有人配置了git over http.
- 最简单的办法就是用 git push命令 和ssh协议; 用你本地的"master" 分支去更新远程的"master"分支,执行下面的命令:
$ git push ssh://yourserver.com/~you/proj.git master:master
或是:
$ git push ssh://yourserver.com/~you/proj.git master
- 和git-fetch命令一样git-push如果命令的执行结果不是"快速向前"(fast forward) 就会报错; 下面的章节会讲如何处理这种情况.
- 推(push)命令的目地仓库一般是个裸仓库(bare respository). 你也可以推到一 个签出工作目录树(checked-out working tree)的仓库,但是工作目录中内 容不会被推命令所更新。如果你把自己的分支推到一个已签出的分支里,这 会导致不可预知的后果。
- 在用git-fetch命令时,你也可以修改配置参数,让你少打字:)。
下面这些是例子:
$ cat >>.git/config <<EOF
[remote "public-repo"]
url = ssh://yourserver.com/~you/proj.git
EOF
- 你可以用下面的命令来代替前面复杂的命令:
$ git push public-repo master
当推送代码失败时要怎么办
- 如果推送(push)结果不是"快速向前"(fast forward),那么它 可能会报像下面一样的错误:
error: remote 'refs/heads/master' is not an ancestor of
local 'refs/heads/master'.
Maybe you are not up-to-date and need to pull first?
error: failed to push to 'ssh://yourserver.com/~you/proj.git'
- 这种情况通常由以下的原因产生:
-
用
git-reset --hard
删除了一个已经发布了的一个提交,或是 -
用
git-commit --amend
去替换一个已经发布的提交,或是 -
用
git-rebase
去rebase一个已经发布的提交.
- 你可以强制git-push在上传修改时先更新,只要在分支名前面加一个加号。
$ git push ssh://yourserver.com/~you/proj.git +master
-
通常不论公共仓库的分支是否被修改,他都被修改为指向原来指向的提交(commit) 跟随的下一个提交(commit)。如果在这种情况下强制地推送,你就破坏了之前的约定。
-
尽管如此,这也是一种通常的用法来简单地发布一系列正在修正的补丁,并且只要你 通知了其他的开发者你打算怎样操作这个分支,这也是一种可以接受的折中办法。
-
一个推送(push)也可能因为其他人有向这个仓库(repository)推送的权利而失败。 在这种情况下,正确地解决办法是首先用"pull"命令或者"fetch"命令和"rebase" 命令更新你的代码,然后重新尝试推送(push);
Git标签
-
轻量级标签
我们可以用 git tag不带任何参数创建一个标签(tag)指定某个提交(commit):
$ git tag stable-1 1b2e1d63ff
- 这样,我们可以用
stable-1
作为提交(commit)"1b2e1d63ff"
的代称(refer)。 - 前面这样创建的是一个“轻量级标签",这种分支通常是从来不移动的。
-
如果你想为一个标签(tag)添加注释
,或是为它添加一个签名(sign it cryptographically), 那么我们就需要创建一个”标签对象"
.
- 标签对象
- 如果有 "-a", "-s" 或是 "-u " 中间的一个命令参数被指定,那么就会创建 一个标签对象,并且需要一个标签消息(tag message). 如果没有"-m " 或是 "-F " 这些参数,那么就会启动一个编辑器来让用户输入标签消息(tag message).
- 译者注:大家觉得这个标签消息是不是提交注释(commit comment)比较像。
- 当这样的一条命令执行后,一个新的对象被添加到Git对象库中,并且标签引用就指向了 一个标签对象,而不是指向一个提交(commit). 这样做的好处就是:你可以为一个标签 打处签名(sign), 方便你以后来查验这是不是一个正确的提交(commit).
下面是一个创建标签对象的例子:
$ git tag -a stable-1 1b2e1d63ff
- 标签对象可以指向任何对象,但是在通常情况下是一个提交(commit). (在Linux内核代 码中,第一个标签对象是指向一个树对象(tree),而不是指向一个提交(commit)).
- 签名的标签
- 如果你配有
GPG key
,那么你就很容易创建签名的标签.首先你要在你的*.git/config 或 *~.gitconfig里配好key
.
下面是示例:
[user]
signingkey = <gpg-key-id>
- 你也可以用命令行来配置:
$ git config (--global) user.signingkey <gpg-key-id>
- 现在你可以直接用"-s" 参数来创“签名的标签”。
$ git tag -s stable-1 1b2e1d63ff
- 如果没有在配置文件中配GPG key,你可以用"-u“ 参数直接指定。
$ git tag -u <gpg-key-id> stable-1 1b2e1d63ff
网友评论