分布式的工作流程
假设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),在 "rigin/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 pushgitcvs-migration.
公共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
如果是不同的用户:
$ git clone ssh://user@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, 命令一样giit-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
你可以点击这里: git config,查看remote..url, branch..remote, 和remote..push等选项的解释.
当推送代码失败时要怎么办
如果推送(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
Normally whenever a branch head in a public repository is modified, it is modified to point to a descendant of the commit that it pointed to before. By forcing a push in this situation, you break that convention.
Nevertheless, this is a common practice for people that need a simple way to publish a work-in-progress patch series, and it is an acceptable compromise as long as you warn other developers that this is how you intend to manage the branch.
It's also possible for a push to fail in this way when other people have the right to push to the same repository. In that case, the correct solution is to retry the push after first updating your work: either by a pull, or by a fetch followed by a rebase; see the next section and gitcvs-migration for more.
分支与合并@基础
一个Git仓库可以维护很多开发分支。现在我们来创建一个新的叫”experimental”的分支:
$ git branch experimental
如果你运行下面这条命令:
$ git branch
你会得到当前仓库中存在的所有分支列表:
experimental
* master
“experimental” 分支是你刚才创建的,“master”分支是Git系统默认创建的主分支。星号(“*”)标识了你当工作在哪个分支下,输入:
$ git checkout experimental
切换到”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 commit
file.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索引(index),同时亦提供了可视化索引的能力。只需简单键入'git add -i',即可使用此功能。Git会列出所有修改过的文件及它们的状态。
$>git add -i
staged unstaged path
1: unchanged +4/-0 assets/stylesheets/style.css
2: unchanged +23/-11 layout/book_index_template.html
3: unchanged +7/-7 layout/chapter_template.html
4: unchanged +3/-3 script/pdf.rb
5: unchanged +121/-0 text/14_Interactive_Rebasing/0_ Interactive_Rebasing.markdown
*** Commands ***
1: status 2: update 3: revert 4: add untracked
5: patch 6: diff 7: quit 8: help
What now>
在这个例子中,我们可以看到有5个修改过的文件还没有被加入到索引中(unstaged),甚至可以看到每个文件增加和减少的行数。紧接着是一个交互式的菜单,列出了我们可以在此模式中使用的命令。
如果我们想要暂存(stage)这些文件,我们可以键入'2'或者'u'进入更新(update)模式。然后我们可以通过键入文件的范围(本例中是1-4)来决定把哪些文件加入到索引之中。
What now> 2
staged unstaged path
1: unchanged +4/-0 assets/stylesheets/style.css
2: unchanged +23/-11 layout/book_index_template.html
3: unchanged +7/-7 layout/chapter_template.html
4: unchanged +3/-3 script/pdf.rb
5: unchanged +121/-0 text/14_Interactive_Rebasing/0_ Interactive_Rebasing.markdown
Update>> 1-4
staged unstaged path
* 1: unchanged +4/-0 assets/stylesheets/style.css
* 2: unchanged +23/-11 layout/book_index_template.html
* 3: unchanged +7/-7 layout/chapter_template.html
* 4: unchanged +3/-3 script/pdf.rb
5: unchanged +121/-0 text/14_Interactive_Rebasing/0_ Interactive_Rebasing.markdown
Update>>
如果键入回车,我会回到主菜单中,同时可以看到那些指定文件的状态已经发生了改变:
What now> status
staged unstaged path
1: +4/-0 nothing assets/stylesheets/style.css
2: +23/-11 nothing layout/book_index_template.html
3: +7/-7 nothing layout/chapter_template.html
4: +3/-3 nothing script/pdf.rb
5: unchanged +121/-0 text/14_Interactive_Rebasing/0_ Interactive_Rebasing.markdown
现在我们可以看到前4个文件已经被暂存,但是最后一个没有。
我们可以进行数个有用的操作,包括取消文件的暂存(3: revert),加入未跟踪的文件(4: add untracked)和查看差异(6: diff)。这些功能都很易懂。还有一个很“酷”的功能,就是暂存补丁(staging patches)(5: patch)。
如果你键入'5'或者'p',git会一个一个补丁(一块一块)地显示出差异,然后询问是否对每一块进行暂存操作。通过这个方法,你可以暂存文件修改中的一部分。如果你编辑了一个文件,只想提交其中一部分而不包括其他未完成编辑的部分,或者把文档、空白字符从大量的修改中分开提交,你可以使用'git add -i'去相对轻松地完成任务。
这里我暂存了book_index_template.html的部分修改,而不是全部修改:
staged unstaged path
1: +4/-0 nothing assets/stylesheets/style.css
2: +20/-7 +3/-4 layout/book_index_template.html
3: +7/-7 nothing layout/chapter_template.html
4: +3/-3 nothing script/pdf.rb
5: unchanged +121/-0 text/14_Interactive_Rebasing/0_ Interactive_Rebasing.markdown
6: unchanged +85/-0 text/15_Interactive_Adding/0_ Interactive_Adding.markdown
当你通过'git add -i'完成对索引的改动后,你只需要退出(7: quit),然后'git commit'去提交暂存的修改。切记不要运行'git commit -a',它会忽视你刚才辛辛苦苦做的修改而把所有东西都提交到仓库中去。