GIT团队合作探讨之一-保持工作同步的概念和实践

感谢英文原文作者,这是我看到的关于git协同工作写的最清晰简洁的文章了:

https://www.atlassian.com/git/tutorials/syncing/git-push  

SVN使用一个单一的*库,作为所有开发人员的通信中枢,而合作是通过在开发人员的拷贝和*库之间传递变更集来实现的。这和GIT的合作模型大不相同。GIT下面,每一位开发人员都有一个repo的完整拷贝,本地工作完成后,开发人员就有了自己的本地历史和分支结构。开发人员通常需要分享一些列commits(而不是单一一个变更集)。GIT不像SVN那样从本地copy向*库递交单个变更集,GIT实际上是让你在不同的库之间分享分支。

      下面的米in高龄允许你管理和其他库之间的connections,通过pushing branches到其他的repo来发布Local history,而通过pulling branches来获取其他人员的贡献到local repo.

git remote

git remote 命令允许你create,view,delete和其他库的connection. Remote connection更像一个bookmark书签,而不是一个直接链接到其他的库中。这些remote connection并不会提供对其他库的实时访问机制,他们仅仅作为一种能够用以引用其他repo的并不友好的url而方便使用的名字而已。

例如,下图中显示了从你的repo分别指向“*库”和其他开发人员库。我们并不用使用全名称的URL来引用其他库,你只需要传入origin和john短connection名称给其他的git命令即可。

GIT团队合作探讨之一-保持工作同步的概念和实践

用法:

git remote
git remote -v     //列出所有指向其他repo的connections详细信息
git remote add <name> <url> //创建一个指向一个remote repo的新的connection.在上述命令执行后,你就可以通过使用<name>来作为引用<url>的便利手段了,比如:
git remote rm <name> //通过这个命令来删除和remote repo的一个<name>连接
git remote rename <old-name> <new-name> //重命名从old到new

探讨:

Git被设计成给每一个开发人员一个完整独立的开发环境。这意味着信息并不会自动地在不同repo之间流动。相反,开发人员需要手工地pull upstream commits到本地repo,或者手动地push local commits到*repo。git remote命令实际上仅仅是座位一种方便传递repo url到这些"sharing"命令的一种手段。

origin remote

当你clone一个repo时(git clone命令),git将自动创建一个被命名为origin的remote connection,这个origin connection将自动指向被clone的库。这对开发人员创建*库的本地copy并且实现本地开发非常有用,因为通过origin,开发人员可以非常方便地pull upstream或者publish local commits.这种行为也是为什么大多数git-based 项目就称呼他们的*库为origin的原因吧。

Repository URLs

Git支持非常多的方式来引用一个remote repo。两种最简单的方法是:HTTP或者SSH。HTTP是一种允许匿名访问的read-only的协议模式,例如:

http://host/path/to/repo.git

但是通常不允许push commit到一个HTTP地址。为了写操作,你需要使用SSH协议(加上一个帐号密码):

ssh://user@host/path/to/repo.git

你需要在host主机上有一个有效的SSH帐号。

例子:

除了origin,为了方便合作,通常你可能需要指向你的同事的repo的一个remote connection。

例如,如果你的同僚John,他可能维护了一个可以公开访问的repo:dev.example.com/john.git.你可以这样来增加一个remote connection:

git remote add john http://dev.example.com/john.git

 通过这种方式来访问个别开发人员的repo的模式使得git能够在“*库”之外来达到沟通合作。这种模式对于小团队大项目非常有用。也正因为这种模式使得在git中“*库”的概念越来越淡化,甚至可以没有central repo,因为每个人都可以称为*库(如果你愿意的话)

git fetch

git fetch命令从remote repo中import commits到本地repo.这个命令的结果是remote repo的commits被作为本地repo的一个remote branch的形式来保存在本地repo中。也就是说remote branch代表了我们的remote repo的真实内容。这种方式给你在真正集成远端变更之前来review确认他们的变更的一个机会

用法

git fetch <remote>  //从远端repo中fetch所有的分支。
git fetch <remote> <branch> //仅fetch远端repo的指定的branch分支

探讨:

Fetching是当你希望看到别人的工作成果时你需要做的。既然获取到的commits以一个remote branch来代表,也就意味着fetch方式对你本地开发工作没有任何影响。这种模式给你集成应用别人的改动之前来review提供了方便。这一点和svn update命令类似,它允许你看看*库有哪些变更,但是并不强迫你实际merge这些变更到你的repo中。

remote branches

remote branches和local branch是完全一样的,不同的是:remote branch代表着来自其他repo的commits(这里其他repo:有可能是其他人的,也可能是你自己的)。你也可以check out一个remote branch,但是这时你将会进入detached HEAD状态(就像checkout一个老的commit一样)。你可以把remote branch视为只读的branch. Remote branch会由一个remote connection名称来做前缀,这样你永远也不会将remote branch和local branch混淆起来。例如:

git branch -r
# origin/master           
# origin/develop
# origin/some-feature
//上面的命令就会列出remote名为origin的repo中存在着master,develop,some-feature分支

再次指出,你可以通过checkout那些remote branch并且通过git log命令来检查这些branch的变更。如果你批准了一个remote branch的变更,你可以通过git merge命令来merge这个分支的内容到你的本地分支上去。从这里看出,要获取别人的变更,需要两个过程:fetch和merge。但是git pull命令也提供了一种合二为一的方式。

下面给出一个实际的典型的场景:同步您的本地repo和远端repo的master分支:

$git fetch origin
a1e8fb5..45e66a4 master -> origin/master
a1e8fb5..9e8ab1c develop -> origin/develop
* [new branch] some-feature -> origin/some-feature
//上面的命令说明origin这个remote我们有master,develop,some-feature三个分支都已经拉回来了,分别放在origin/master。。。分支上。

在下面的图中,所有来自remote branch的commits都以方框表示。正如你看到的,git fetch给了你另一个repo整个分支结构的能力!

GIT团队合作探讨之一-保持工作同步的概念和实践

为了检查到底有了哪些改动在upstream master上,你可以通过给git log命令传入一个origin/master作为filter参数来实现:

git log --oneline master..origin/master

为了批准并且merge这些remote branch上的他人的变更(到你的local branch),你需要这么做:

git checkout master //切换到本地分支环境
git log origin/master //检查remote变更集
git merge origin/master //merge到本地

通过上面的命令完成后origin/master和master就同时指向了相同的commit,你就和upstream developments完全同步了。

git pull

在git-based协作工作流模型中,merge upstream变更到local repo是一个非常普通的任务,虽然通过git fetch+merge两步可以完成这个任务,但是git也提供了一种合二为一的命令git pull

git pull <remote> //获取remote的current branch并且立即merge到local分支,相当于以下命令:
git fetch <remote> & git merge origin/<current-branch>
git pull --rebase <remote>//和上面命令的区别是:使用git rebase而不是git merge来合入remote branch的变更

探讨:

你可以将git pull看作svn update相同功能的版本。它是一种便利地同步本地repo和upstream change的手段。下面几张图描绘了这个pull的过程。

GIT团队合作探讨之一-保持工作同步的概念和实践GIT团队合作探讨之一-保持工作同步的概念和实践GIT团队合作探讨之一-保持工作同步的概念和实践

你可能想当然地认为你的repo和remote是同步的,但是git fetch却发现了remote origin的master分支在你最后一次pull/fetch后已经向前走了几步:

pulling via rebase

上面命令例子中提到--rebase参数,这个参数可以通过阻止非必要的merge commit(这是默认的git pull merge模型中必然产生的无意义commit)产生,从而保证一个线性地历史记录。很多开发人员更喜欢rebase,而不是merge。这就像说这么句话:”我想把我的改动放在别人已经做过的工作之上“。

事实上,正是由于--rebase是一个如此common的工作流,以至于已经有了一个特定的配置选项:

git config --global branch.autosetuprebase always //执行这条命令后,所有的git pull命令将集成git rebase,而不是git merge!!!

例子:

下面的例子演示如何和*库上的master branch来保持同步

git checkout master
git pull --rebase origin

上面的命令将你的local change直接放置于任何其他人的工作之上,从而你也就有了别人的贡献

git push

push是你如何将本地commits传到remote repo上去的方法,这和git fetch是相反的操作,然而fetch/pull是导入commits到local branch上去(也需要通过remote origin/branch做中转),push则是输出你的本地commits到remote repo的local branch(通过本地的remote orgin/branch做中转)中去。这有可能会覆盖变更,所以你需要小心使用,下面就谈谈这些潜在可能存在的问题:

用法:

git push <remote> <branch>

上述命令将本地的<branch> 上的所有commit和内部objects push到<remote>库中,并且这条命令的结果会在<remote> repo中创建一个local branch。为了阻止你可能覆盖remote repo中<branch>分支上的commits,GIT在得知push操作并不会在remote repo中产生一个fast-forward merge时,git会禁止你做push操作。也就是说,默认情况下,你的local branch相比remote origin的local branch(以origin/<branch>来代替)更新的情况下,才允许你push.(换句话说就是remote repo的branch没有做过变更!)

当然,git也为高级用户提供了--force命令来强制允许push操作:

git push <remote> --force//注意一定要小心使用,除非你知道你在干什么,否则别用
git push <remote> --all //push所有的本地branch到指定的remote上去
git push <remote> --tags//默认情况下tags并不会自动push上,使用该标志则push所有本地创建的tag

探讨:

git push最常见的使用场景是发布你的本地修改到*库中。在你积累了多个本地commit后,并且希望将这些变更分享给团队其他成员,你(optionally)可以通过rebase interactive  来将这些commit梳理一下,然后push到*库中。

GIT团队合作探讨之一-保持工作同步的概念和实践

上面这张图展示了当你local master相对于*库的master分支已经向前走了几步后,你通过执行git push origin master命令来发布你的本地commit到底发生了什么。注意一点:git push基本上就像是在remote repo中执行git merge localmaster命令一样,仔细体会一下

关于Force Pushing:

我们知道当git push会产生一个non-fast-forward merge时,git会拒绝执行这个git push命令,因为一旦执行这条命令,你就会覆盖*库的历史。

正因为此,如果remote history已经和你的local history分叉了(diverged),你需要首先git pull(git fetch/git merge)到你的本地,并且做好可能的冲突解决,随后再次push。这和SVN如何确保你通过在commit一个change set之前,必须做svn update来和*库保持同步是类似的概念。

--force标志将取消上面这种默认行为,产生的后果是:remote repo的branch将完全反映你的local history,其他自从你上次git pull操作后upstream分叉出来的所有变更历史都将会丢失。一个可能使用这个--force标志的场景是:当你发现你刚刚分享发布的commits是不对的,而你通过git commit --amend或者一个interactive rebase操作解决了这个问题从而再次发布。然而,你必须确认:没有团队成员pull过你在--force之前push的哪些commits.

Only push to Bare Repositories

而且,你应该只向那些通过--bare标志创建的repo去做push操作。既然push会将remote repo的local branch的结构弄的混乱(原因是push时会在remote repo中创建local branch),因此很重要的一点是:不要向另外一个开发人员的本地repo做push操作(当然也不是很绝对哦!!),由于bare库没有working directory,所以不可能打断任何人的开发工作。

例子:

下面的例子描述了将本地变更向*库发布的标准方法。首先,通过git fetch/git rebase操作来确保你的本地master分支是up-to-date的(已经包含了所有的remote变更,run local changes on top of them). interactive rebase在这时也是一个在分享发布你的变更前清理他们的很好的机会。然后,git push命令则将所有你的local master变更发布到central repo中去。

git checkout master
git fetch origin master
git rebase -i origin/master
#squash commits, fix up commit messages etc.
git push origin master

既然我们确保我们的local master 是uptodate的,这将会产生一个fast-forward merge,git push再也不会抱怨non-fast-forward问题了。