##热身 该Blog起源于最近和朋友讨论关于Git的一些特性, 在讨论中发现他对Git多人协作当中的模型并不是很熟悉.
如果你是Git的初学者, 建议先去Git-Learn-Branching 玩玩前2~3个Level, 了解下rebase, pull
如果该项目只有你一人开发, 那么git的pull是不会有问题的.
##背景介绍
当前, 我们有2位开发者: sunus, vivian 他们想进行pair programming一个项目. 并且该项目是由god发起的,已有2次commits. 他们会将对新的代码提交到dev分支上, 之后由god将新代码合并到稳定分支master
sunus@mbp~[/private/var/tmp/git-pull/awesome-project] (master ✔)
[22:36]:cat git.c
#include <stdio.h>
int main()
{
printf("Hello Git!");
return 0;
}
sunus@mbp~[/private/var/tmp/git-pull/awesome-project] (master ✔)
[22:36]:git log
commit 163a6d700226b780b7852a79fe1370a6d38c819a
Author: god <god@mbp>
Date: Mon Dec 9 22:13:15 2013 +0800
remove FILE
commit d22bf163d093afb494ad619d8964572e55c73167
Author: god <god@mbp>
Date: Mon Dec 9 22:11:40 2013 +0800
write first lines of codes
commit 45f2016a51ce7b8317e074a961647c091a50cd94
Author: sunus <god@mbp>
Date: Mon Dec 9 22:04:41 2013 +0800
add first file
sunus@mbp~[/private/var/tmp/git-pull/awesome-project] (master ✔)
[22:48]:git branch
dev
* master
PS, 在命令行的末端会显示我们当前所在的branch, 比如在这儿是master.
PSS, branch之后的符号是表示当前的branch是否有被修改但是还没commit的内容: ✔表示没有, ⚡表示有.
现在, sunus, vivian 他们分别将项目clone到他们的本地.
[22:52]:echo "I am sunus:)"
I am sunus:)
sunus@mbp~[/private/var/tmp/git-pull]
[22:52]:git clone awesome-project sunus
Cloning into 'sunus'...
done.
Checking connectivity... done
sunus@mbp~[/private/var/tmp/git-pull]
[22:52]:cd sunus
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ✔)
[22:52]:ls
git.c
sunus@mbp~[/private/var/tmp/git-pull]
[22:54]:echo "I am vivian ^^"
I am vivian ^^
sunus@mbp~[/private/var/tmp/git-pull]
[22:55]:git clone awesome-project vivian
Cloning into 'vivian'...
done.
Checking connectivity... done
sunus@mbp~[/private/var/tmp/git-pull]
[22:55]:cd vivian
sunus@mbp~[/private/var/tmp/git-pull/vivian] (master ✔)
[22:55]:ls
git.c
好了, 现在开始Pair Progamming:)
####sunus 写了一些代码, 并且在本地分支有2个commits.
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ⚡)
[23:07]:git diff
diff --git a/git.c b/git.c
index 7d26397..127e99a 100644
--- a/git.c
+++ b/git.c
@@ -2,6 +2,8 @@
int main()
{
+ void *p;
printf("Hello Git!");
+ printf("I am sunus and I am here with vivian");
return 0;
}
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ⚡)
[23:07]:git add git.c
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ⚡)
[23:09]:git commit -m 'I add a intro'
[master 1c0b75b] I add a intro
1 file changed, 1 insertion(+)
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ✔)
[23:09]:
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ⚡)
[23:13]:git diff
diff --git a/git.c b/git.c
index 668a8f3..50aae80 100644
--- a/git.c
+++ b/git.c
@@ -1,8 +1,16 @@
#include <stdio.h>
+void *magic()
+{
+ return (void *)magic;
+}
+
int main()
{
+ void *p;
printf("Hello Git!");
- printf("I am sunus and I am here with vivian");
+ printf("I am sunus and I am here with vivian\n");
+ p = magic();
+ printf("I will show you a magic: %p", p);
return 0;
}
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ⚡)
[23:13]:git add git.c
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ⚡)
[23:13]:git commit -m 'show you a magic'
[master 6df90b8] show you a magic
1 file changed, 9 insertions(+), 1 deletion(-)
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ✔)
以下是sunus的log
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ✔)
[23:19]:git log
commit 6df90b8dc07988fb9590100338af6897d119ca1b
Author: sunus <sunuslee@gmail.com>
Date: Mon Dec 9 23:14:07 2013 +0800
show you a magic
commit 1c0b75b60d68ccc58eca5519f7fd15912277be84
Author: sunus <sunuslee@gmail.com>
Date: Mon Dec 9 23:09:41 2013 +0800
I add a intro
commit 163a6d700226b780b7852a79fe1370a6d38c819a
Author: god <god@mbp>
Date: Mon Dec 9 22:13:15 2013 +0800
remove FILE
commit d22bf163d093afb494ad619d8964572e55c73167
Author: god <god@mbp>
Date: Mon Dec 9 22:11:40 2013 +0800
write first lines of codes
commit 45f2016a51ce7b8317e074a961647c091a50cd94
Author: god <god@mbp>
Date: Mon Dec 9 22:04:41 2013 +0800
add first file
####vivian 也写了一些代码, 并且在本地分支有1个commit
sunus@mbp~[/private/var/tmp/git-pull/vivian] (master ⚡)
[23:25]:git diff
diff --git a/git.c b/git.c
index 7d26397..003e1ee 100644
--- a/git.c
+++ b/git.c
@@ -3,5 +3,6 @@
int main()
{
printf("Hello Git!");
+ printf("I am vivian, I am new to Programming in C:<");
return 0;
}
sunus@mbp~[/private/var/tmp/git-pull/vivian] (master ⚡)
[23:25]:git add git.c
sunus@mbp~[/private/var/tmp/git-pull/vivian] (master ⚡)
[23:25]:git commit -m 'vivian committttt^^'
[master 1838ec2] vivian committttt^^
1 file changed, 1 insertion(+)
sunus@mbp~[/private/var/tmp/git-pull/vivian] (master ✔)
[23:25]:git log
commit 1838ec2b16be49b5aa084eb463e8d03e3b1f47de
Author: vivian <vivian@gmail.com>
Date: Mon Dec 9 23:25:34 2013 +0800
vivian committttt^^
commit 163a6d700226b780b7852a79fe1370a6d38c819a
Author: god <god@mbp>
Date: Mon Dec 9 22:13:15 2013 +0800
remove FILE
commit d22bf163d093afb494ad619d8964572e55c73167
Author: god <god@mbp>
Date: Mon Dec 9 22:11:40 2013 +0800
write first lines of codes
commit 45f2016a51ce7b8317e074a961647c091a50cd94
Author: god <god@mbp>
Date: Mon Dec 9 22:04:41 2013 +0800
add first file
sunus@mbp~[/private/var/tmp/git-pull/vivian] (master ✔)
[23:25]:
####现在是什么情况?
sunus, vivian都在本地基于origin上的远程分支编写了自己的代码. 但是他们不知道对方干了什么. 于是, 他们需要合并两人的修改, 并且将更新提交到远程dev分支上
vivian动作比较快, 什么也没想就push了.
sunus@mbp~[/private/var/tmp/git-pull/vivian] (master ✔)
[23:34]:git push -u origin master:dev
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 343 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To /private/var/tmp/git-pull/awesome-project
163a6d7..1838ec2 master -> dev
Branch master set up to track remote branch dev from origin.
这看起来是成功了, god也能够看到vivian的改动:)
sunus@mbp~[/private/var/tmp/git-pull/awesome-project] (dev ✔)
[23:35]:cat git.c
#include <stdio.h>
int main()
{
printf("Hello Git!");
printf("I am vivian, I am new to Programming in C:<");
return 0;
}
sunus@mbp~[/private/var/tmp/git-pull/awesome-project] (dev ✔)
[23:35]:git log
commit 1838ec2b16be49b5aa084eb463e8d03e3b1f47de
Author: vivian <vivian@gmail.com>
Date: Mon Dec 9 23:25:34 2013 +0800
vivian committttt^^
commit 163a6d700226b780b7852a79fe1370a6d38c819a
Author: god <god@mbp>
Date: Mon Dec 9 22:13:15 2013 +0800
remove FILE
接下来看以前的sunus会怎么做(他要倒霉了)
##PULL
该是git初学者们常用的一个操作, 他们一般认为该操作知识将本地版本库与远程的版本库同步更新.1
git pull
但是并不知道这背后实际发生了什么, 这也是为什么pull在大多数情况下,单个/少数开发者合作能够work, 但是在实际和多人协作中会造成问题的原因.
下面是简单的workflow:
首先, sunus并不知道origin是否有改动, 他也是直接push.
[23:42]:git push -u origin master:dev
To /private/var/tmp/git-pull/awesome-project
! [rejected] master -> dev (fetch first)
error: failed to push some refs to '/private/var/tmp/git-pull/awesome-project'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first merge the remote changes (e.g.,
hint: 'git pull') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
很明显, push不成功, 因为vivian抢先一步对远程版本库做了修改. 所以, sunus看到了要先做
的hint.1
git pull
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ✔)
[23:47]:git pull origin dev
From /private/var/tmp/git-pull/awesome-project
* branch dev -> FETCH_HEAD
Auto-merging git.c
CONFLICT (content): Merge conflict in git.c
Automatic merge failed; fix conflicts and then commit the result.
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ⚡)
[23:48]:cat git.c
#include <stdio.h>
void *magic()
{
return (void *)magic;
}
int main()
{
void *p;
printf("Hello Git!");
<<<<<<< HEAD
printf("I am sunus and I am here with vivian\n");
p = magic();
printf("I will show you a magic: %p", p);
=======
printf("I am vivian, I am new to Programming in C:<");
>>>>>>> 1838ec2b16be49b5aa084eb463e8d03e3b1f47de
return 0;
}
好了, 接下来还是蛮常见的事情, sunus, vivian都对相关的代码做了修改, 现在有冲突了, sunus需要手动解决.
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ✔)
[23:52]:cat git.c
#include <stdio.h>
void *magic()
{
return (void *)magic;
}
int main()
{
void *p;
printf("Hello Git!");
printf("I am sunus and I am here with vivian\n");
p = magic();
printf("I will show you a magic: %p", p);
printf("I am vivian, I am new to Programming in C:<");
return 0;
}
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ✔)
[23:52]:git log
commit 135990d6a92554009966c7b88133501adba767f2
Merge: 6df90b8 1838ec2
Author: sunus <sunuslee@gmail.com>
Date: Mon Dec 9 23:51:59 2013 +0800
pull and resolved a conflict
commit 1838ec2b16be49b5aa084eb463e8d03e3b1f47de
Author: vivian <vivian@gmail.com>
Date: Mon Dec 9 23:25:34 2013 +0800
vivian committttt^^
ok,在这儿, sunus看了把手上的工作也做完了, 可以把代码push交到远程origin了.(会发生什么事呢?)
我们先比较一下当前sunus, vivian两人在本地的git仓库情况:
vivivan
sunus
sunus开始push.
sunus@mbp~[/private/var/tmp/git-pull/sunus] (master ✔)
[0:12]:git push -u origin master:dev
Counting objects: 13, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (9/9), 903 bytes | 0 bytes/s, done.
Total 9 (delta 2), reused 0 (delta 0)
To /private/var/tmp/git-pull/awesome-project
1838ec2..135990d master -> dev
Branch master set up to track remote branch dev from origin.
push成功了, 接下来, 看看当前sunus, vivian, god 本地分支的情况:
sunus
vivian
god
看起来好似没有问题, 不就是有个环吗?
但是, 尝试下
会发现, 这儿根本没有 sunus push之后的详细日志, 不可思议吧?!1
git log -p
也就是说, 除了sunus, 别人并不知道sunus和vivian他们俩的代码, 最终是如何合并的.
除非对单个commit依次进行diff
##Fetch + Rebase
让我们再来看看另一种做法, 也是我比较推荐的. 使用fetch 然后再进行rebase.
fetch: 只把origin源改动下载到本地, 但是并不进行合并.
rebase: 把当前的branch放到另一个branch的顶端, 体现的形式是开发的过程是线性的, 而不是一个环(pull/merge)
我们回到刚才sunus的情形: vivian已经push了代码.
这次, sunus使用fetch
sunus@mbp~[/private/var/tmp/git-fetch-rebase/sunus] (master ⚡)
[11:15]:git fetch origin
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ../awesome-project
* [new branch] dev -> origin/dev
* [new branch] master -> origin/master
我们把新的改动下载后, 新的分支有:
- origin/dev 该分支有vivian的新改动
- origin/master 远程origin的master分支, 在这不需要理会.
接下来, 我们要做的事情是, 把我们的改动放在origin/dev分支的最顶部, 即紧接着vivian的改动. 这样看起来像一个人写的代码一样.
sunus@mbp~[/private/var/tmp/git-fetch-rebas/sunus] (master ⚡)
[11:16]:git rebase origin/dev
First, rewinding head to replay your work on top of it...
Applying: I add a intro
Using index info to reconstruct a base tree...
M git.c
Falling back to patching base and 3-way merge...
Auto-merging git.c
CONFLICT (content): Merge conflict in git.c
Failed to merge in the changes.
Patch failed at 0001 I add a intro
The copy of the patch that failed is found in:
/private/var/tmp/git-fetch-rebase/sunus/.git/rebase-apply/patch
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
sunus@mbp~[/private/var/tmp/git-fetch-rebase/sunus] ((no ⚡)
[11:16]:git mergetool
Merging:
git.c
Normal merge conflict for 'git.c':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (vimdiff):
4 files to edit
ok, 在这儿会遇到一次merge的冲突, 我们使用mergetool解决. 然后继续rebase
sunus@mbp~[/private/var/tmp/git-fetch-rebase/sunus] ((no ⚡)
[11:18]:git rebase --continue
Applying: I add a intro
Applying: show you a magic
Using index info to reconstruct a base tree...
M git.c
Falling back to patching base and 3-way merge...
Auto-merging git.c
CONFLICT (content): Merge conflict in git.c
Failed to merge in the changes.
Patch failed at 0002 show you a magic
The copy of the patch that failed is found in:
/private/var/tmp/git-fetch-rebase/sunus/.git/rebase-apply/patch
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
sunus@mbp~[/private/var/tmp/git-fetch-rebase/sunus] ((no ⚡)
[11:18]:git mergetool
Merging:
git.c
Normal merge conflict for 'git.c':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (vimdiff):
4 files to edit
sunus@mbp~[/private/var/tmp/git-fetch-rebase/sunus] ((no ⚡)
[11:19]:cat git.c
#include <stdio.h>
void *magic()
{
return (void *)magic;
}
int main()
{
void *p;
printf("Hello Git!");
printf("I am sunus and I am here with vivian\n");
p = magic();
printf("I will show you a magic: %p", p);
printf("I am vivian, I am new to Programming in C:<");
return 0;
}
sunus@mbp~[/private/var/tmp/git-fetch-rebase/sunus] ((no ⚡)
[11:19]:git rebase --continue
Applying: show you a magic
ok, rebase完成, 可以看到最后sunus的2个commit: 5580/8318 是在当前log的最顶端.
sunus@mbp~[/private/var/tmp/git-fetch-rebase/sunus] (master ✔)
[11:19]:git log
commit 5580978c60d157da68816644aba7afecd328a4be
Author: sunus <sunuslee@gmail.com>
Date: Mon Dec 9 23:14:07 2013 +0800
show you a magic
commit 8318c499e6d5c4d9fd9ba46c19994c326a6cb1c5
Author: sunus <sunuslee@gmail.com>
Date: Mon Dec 9 23:09:41 2013 +0800
I add a intro
commit 1838ec2b16be49b5aa084eb463e8d03e3b1f47de
Author: vivian <vivian@gmail.com>
Date: Mon Dec 9 23:25:34 2013 +0800
vivian committttt^^
commit 163a6d700226b780b7852a79fe1370a6d38c819a
Author: god <god@mbp>
Date: Mon Dec 9 22:13:15 2013 +0800
remove FILE
接下来, 把我们本地的变动提交到远程仓库.
sunus@mbp~[/private/var/tmp/git-fetch-rebase/sunus] (master ✔)
[11:32]:git push -u origin master:dev
Counting objects: 8, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 663 bytes | 0 bytes/s, done.
Total 6 (delta 1), reused 0 (delta 0)
To ../awesome-project
1838ec2..5580978 master -> dev
Branch master set up to track remote branch dev from origin.
我们看看sunus, god当前的历史情况.
sunus
god
ok, 看起来很不错!
然后看看vivian需要做什么获取最新的代码
sunus@mbp~[/private/var/tmp/git-fetch-rebase/vivian] (master ✔)
[11:56]:git fetch
remote: Counting objects: 8, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (6/6), done.
From ../awesome-project
* [new branch] dev -> origin/dev
* [new branch] master -> origin/master
sunus@mbp~[/private/var/tmp/git-fetch-rebase/vivian] (master ✔)
[11:57]:git diff master origin/dev
diff --git a/git.c b/git.c
index 003e1ee..0187070 100644
--- a/git.c
+++ b/git.c
@@ -1,8 +1,17 @@
#include <stdio.h>
+void *magic()
+{
+ return (void *)magic;
+}
+
int main()
{
+ void *p;
printf("Hello Git!");
+ printf("I am sunus and I am here with vivian\n");
+ p = magic();
+ printf("I will show you a magic: %p", p);
printf("I am vivian, I am new to Programming in C:<");
return 0;
}
sunus@mbp~[/private/var/tmp/git-fetch-rebase/vivian] (master ✔)
[11:57]:git log
commit 1838ec2b16be49b5aa084eb463e8d03e3b1f47de
Author: vivian <vivian@gmail.com>
Date: Mon Dec 9 23:25:34 2013 +0800
vivian committttt^^
commit 163a6d700226b780b7852a79fe1370a6d38c819a
Author: god <god@mbp>
Date: Mon Dec 9 22:13:15 2013 +0800
remove FILE
sunus@mbp~[/private/var/tmp/git-fetch-rebase/vivian] (master ✔)
[11:57]:git merge origin/dev
Updating 1838ec2..5580978
Fast-forward
git.c | 9 +++++++++
1 file changed, 9 insertions(+)
sunus@mbp~[/private/var/tmp/git-fetch-rebase-bak/vivian] (master ✔)
[11:58]:git log
commit 5580978c60d157da68816644aba7afecd328a4be
Author: sunus <sunuslee@gmail.com>
Date: Mon Dec 9 23:14:07 2013 +0800
show you a magic
commit 8318c499e6d5c4d9fd9ba46c19994c326a6cb1c5
Author: sunus <sunuslee@gmail.com>
Date: Mon Dec 9 23:09:41 2013 +0800
I add a intro
commit 1838ec2b16be49b5aa084eb463e8d03e3b1f47de
Author: vivian <vivian@gmail.com>
Date: Mon Dec 9 23:25:34 2013 +0800
vivian committttt^^
commit 163a6d700226b780b7852a79fe1370a6d38c819a
Author: god <god@mbp>
Date: Mon Dec 9 22:13:15 2013 +0800
remove FILE
嗯, vivian这边也没什么问题, 也同步了本地的版本库.
最后看看她本地的历史:
嗯, 看起来好极了~
##总结
- 如果你只是一个人在开发一个项目, 并且在第三方托管(比如github) 那么不管是使用pull还是/fetch rebase都不会有太大问题, 而且pull还是更方便
- github的pull request也是通过将他人的改动, 放到当前历史的最顶端来解决这个问题.
-
如果是多人合作的话, 大部分情况下pull是不会有问题的, 但是会照成合并之后的版本日志混乱, 开发的过程混乱.(因为版本的修改记录是一个环)
- 所以最好还是, 多fetch, 多rebase.这样, 版本的记录是能够保持线性的, 并且每次改动都能在日志里看得很明白.
##Update:
经好友@hunt提醒, 对本文提出了一点看法(关于merge与rebase):
内核在更顶端的地方的开发者/维护者使用的是 merge,比如 Linus 合并网络模块 maintainer David Miller 的 tree(Merge git://git.kernel.org/pub/scm/linux/kernel/git/davem/net), David Miller 合并 OpenvSwitch 维护者 Jesse Gross 的 tree(Merge branch ‘master’ of git://git.kernel.org/pub/scm/linux/kernel/git/jesse/openvswitch)。而在更为下游的地方,比如 OpenvSwitch 社区中,提交给内核模块的代码则是要求开发者使用 rebase 来形成一个线性的提交。这样子形成了一个 非常好的分工,Jesse Gross 负责 OpenvSwitch 的模块代码的维护,David Miller 则轻松地进行合并,并关注 net 模块核心的一些相关的改动,Linus 同样能轻松地合并 net 模块中的内容,只需要去关注主干树上对基础代码的改动。对 开发者来讲,也很容易能明白哪些代码应该提交到哪个列表中,并抄送改动涉及/波及到的相关列表。