之前介绍了如何用git构建项目库及其后续操作的问题,但主要还是个人的操作问题,不太涉及到项目协作方面的问题,所以来说下这块。传送门在这里(后面的可以不用看了)。
1.同步
首先就式同步问题:在项目协作的时候,本地开发了新功能以后就需要和中央库或者其他开发人员的库进行同步。主要进行的操作有:创建当前库与其他库的链接(git remote),把本地库变动推送到别的库(git push),查看整合其他开发者的的变动到本地库(git pull, git fetch)。下面就是这些命令的具体介绍。
git remote
这个命令用于创建,查看,删去与其他库的连接。创建的链接的名字可以代替原始的URL,从而让同步操作时不需要打入不那么好记的URL地址,非常方便。
$ git remote # 显示所有远程连接的库
$ git remote -v # 在前一个命令的基础上增加显示每个库的URL
$ git remote add <name> <url> # 创建url远程库的链接,之后可以用<name>来代表<url>
$ git remote rm <name> # 通过nam删除远程库的链接
$ git remote rename <old-name> <new-name> # 重命名某个远程链接的名字
当你在执行git clone把某个远程库复制到本地的时候会自动创建一个名为origin的链接代表那个远程库,之后就可以直接用git pull来进行把远程库的变动同步到本地库。在git项目中,一般中央库都的链接会叫做origin。
这里用到的url链接可以时ssh链接也可以是时http链接。http链接允许对库进行匿名但只读的访问,在这种状态下时不可能把commit推送到中央库的(比如http://host/path/to/repo.git,无用户名匿名访问)。而使用ssh链接的时候是可读写的,但是不允许匿名(比如ssh://user@host/path/to/repo.git)。
git fetch
这个命令是把远程库的commit输入到本地库,但存储的位置是远程分支而非当前工作的本地分支,所以则合并到本地分支之前是可以先查看确认下的。
$ git fetch <remote> # 抓取远程库的所有分支
$ git fetch origin
a1e8fb5..45e66a4 master -> origin/master a1e8fb5..9e8ab1c develop -> origin/develop * [new branch] some-feature -> origin/some-feature # 命令会显示哪些分支被下载到了本地
$ git fetch <remote> <branch> # 只抓取远程库中的某个分支
远程分支与本地分支除了是别人写的以外没啥区别(处于只读状态),所以可以用git checkout命令切换到某个远程分支,而此时的状态处于之前提到的checkout旧的commit的时候分离HEAD指针状态。查看这些远程分支可以用git branch -r来查看,而远程库的名字需要最为前缀以便与本地库的分支进行区分;当然也可以git checkout和git log命令来查看远程分支的commit历史。比如:
$ git branch -r # origin/master # origin/new-feature
$ git log --oneline master..origin/master # 查看上图中master到origin/master新加入的commit
$ git checkout master
$ git log origin/master
$ git merge origin/master # 确认完origin/master的commit状态后合并到master(当前工作)分支
git pull
这个命令就是git fetch和git merge合并在一起的快捷方式,即一键将远程库中的当前分支同步到本地。
git pull <remote>
# 等价于
$ git fetch <remote>
$ git merge origin/<current-branch>
# git pull --rebase <remote> # 用rebase而非merge进行合并(rebase之前有讲这里就不说了)
很多开发人员喜欢用rebase(把自己的变动放在其他人之后,保证本地commit历史的连续性),可以在将git rebase设置为git pull的默认合并方式。
$ git config --global branch.autosetuprebase always
git pull的工作流程示意图如下:
git push
这个命令与git fetch相反,是将本地分支的commit上传到远程库分支。
$ git push <remote> <branch> # 会在远程库创建一个本地分支。为了避免覆盖远程库中的一些commit,当push会造成非快进合并的时候会报错终止操作。
$ git push --force <remote> # 与上面的命令相比,及时遇到会产生非快进合并的状态也会继续合并(因此会影响其他开发者的commit,所以确定之前不要滥用这个命令)
$ git push --all <remote> # 把当前库的所有分支都上传到远程库
$ git push --tags <remote> # 把分支所带的tag也上传到远程分支
注:非快进合并(non-fast-forward merge)的意思就是说本地分支合并到远程分支的时候在分支起点和合并后的节点之间存在变动(这一般是远程分支合并了其他开发者提交的commit而你的本地库缺并未同步所造成的,这个时候就需要先git pull同步本地库之后再git push)。
此外,一般情况下不要push到别人的库里面,除非你很确定不会修改别人的工作。
综上,一般的同步操作如下所示进行:
$ git checkout master
$ git fetch origin master
$ git rebase -i origin/master
# Squash commits, fix up commit messages etc.
$ git push origin master
2.提出pull request(这个部分更多是用atlassian的bitbucket里面的pull request而非git本身的,不确定两者有多大区别,所以可以跳过这部分)
pull request机制是让开发者在需要添加新功能到中央库的时候通知其他项目协作者进行审查和讨论,而当在开发的时候遇到问题的时候也可以用pull request把问题提醒到其他开发人员一起想办法解决,充当项目论坛的功能(bitbucket提供了网页界面来进行讨论,而git则是通过邮件)。
pull request的工作机制
pull request相当对其他开发者发送请求把本地库分支上传到他的库里面。这个流程需要提供4个信息:源库,源分支,目标库,目标分支。pull request可以用于3种工作流程:功能分支工作流(feature branch workflow),git流工作流(gitflow workflow)和分叉工作流(forking workflow)(之后会具体介绍这3种工作流程的方式)。一般通用流程如下:
1.当前开发者在本地库开发了一个新功能
2.开发者把分支push到公共的库(bitbucket库)
3.开发者通过bitbucket发起pull request
4.其他开发人员review代码,讨论并提出修改意见
5.修改之后由项目管理员合并分支到公共库并关闭pull request
pull request用于功能分支工作流
这种工作流程通过一个共享库(bitbucket)来管理协作,开发者实在隔离的分支上进行功能开发的。在将功能合并到master分支之前必须用pull request发起新功能的评估讨论,之后才能合并。流程如下图所示:
由于在这种工作流中只有一个公共库,所以pull request的目标库和来源库都是同一个,而一般情况下新功能分支是组为源分支,master分支作为目标分支。当工作分支是完整成熟的时候,库管理员只需将分支合并,然后关闭pull request即可;而当工作分支不完善,此时一般是开发者遇到问题进行求助,就需要项目组进行讨论。
pull request用于git流工作流
这个工作流与之前的类似,只是在项目发布的模式建立了一个严格的分支模型。在gitflow工作流进行pull request可以很方便的进行关于分支发布和当前分支的维护工作方面的讨论。工作流程如下图所示:
pull request的机制与之前相同。gitflow工作流是将开发分支与发布分支隔离,新的功能是被整合到develop分支,而已发布的分支有错误进行了hotfix之后则把hotfix分支合并到develop和master分支。
pull request用于分叉工作流
在这种工作流程种,开发者把新功能push到自己的公共库而非项目管理员建立的共享库。之后发起pull request通知项目管理员进行审查。由于每个开发者都有自己的公共库,所以与上面两种情况不同个,pull request的来源库和目标库是不同的:源库就是公共库,源分支就是新功能分支;而如果是要合并到主库,那么目标库就是正式库,master分支就是目标分支。
由于由个人公共库,除了与正式项目组的其他开发人员协同开发以外,还可以和其他项目外的人进行协同。此时的流程如下图(目标库和目标分支由变动):
下面就用一个栗子来具体说下pull request在分叉工作流中的工作流程:
首先把正式库fork到自己的公共库中。其中,john是项目管理员,他的库作为正式库。
然后把自己库中的项目下载(clone)到本机:git clone https://user@bitbucket.org/user/repo.git
开发新功能
$ git checkout -b some-feature
# Edit some code
$ git commit -a -m "Add first draft of some feature"
把新功能上传到自己的公共库`git push origin some-branch`
发起pull request请求将新功能分支合并到正式库中
项目管理源审查分支,然后提出修改意见,开发人员修改以后进行接受pull request进行合并,然后关闭pull request。
3.使用分支--git branch/git checkout/git merge
这些命令将用于创建新分支,选择新分支进行新的功能开发,然后将分支合并从而保证项目在开发新功能的任何时候都是稳定可用的。
git branch
分支代表了项目的开发过程的一条独立路线,并不会影响项目已存的代码。分支是编辑,缓存和commit过程的抽象,因此可以认为是请求新的工作目录,缓存区和项目历时的方法。新的commit会记录在当前分支的历史上,所以分支就会造成项目开发历史的分叉。git branch命令只能创建,查看,重命名和删除分支,不能进行切换和合并操作,这两个需要git checkout和git merge两个命令来执行。(git的储存分支是保存了指向commit最前端的的指针而非所有commit,要获取其他commit则需要通过commit之间的关系来提取。)
$ git branch # 列出所有当前库的分支
$ git branch <branch> # 新建名为<branch>的分支,但并不切换过去
$ git branch -d <branch> # 删除指定分支,但如果有没有合并的变动则无法删除分支
$ git branch -D <branch> # 强行删除指定分支
$ git branch -m <branch> # 重命名分支
如上图所示,通过分支进行新功能开发是无法同时编辑两个分支,同时又能把master分支与开发中的问题代码分隔开。
git checkout
这个命令在之前也有提到过,如果是commit或file对象是查看历史commit的作用(只读状态),而这里的对象是分支,其功能用切换分支,这个时候就不是只读状态了。
$ git checkout <existing-branch> # 切换到已存的分支
$ git checkout -b <new-branch> # 创建并切换到新建分支
$ git checkout -b <new-branch> <existing-branch> # 从已存分支新建并切换分支
这里介绍下之前提到过HEAD指针分离状态(如下图所示)。HEAD指针是指向当前的快照,而当git checkout commit或者分支的时候会把HEAD指针指向指定的commit和branch,此时的状态就是HEAD指针分离状态。这种状态开发新功能的话,编辑的内容会以存入不存在的分支(因为没有新建),当切换到别的分支以后就没办法找到之前新开发的功能。所以在开发新功能的时候需要注意是否处于HEAD指针分离状态。
git merge
这个命令是把独立的开发路线合并成一条的命令。合并的时候变动的是你当前所在的分支,需要合并的目标分支并没有任何变化,所以一般合并后会用git branch -d函数删除过期的目标分支。
$ git merge <branch> # 合并指定分支到当前分支(自动选择合并方式)
$ git merge --no-ff <branch> # 用non-fast-forward的形式合并分支,会产生一个合并commit,在记录本地库的合并历史方面很有用。
git有两种合并方式:快进合并(fast-forward merge)和3路合并(3-way merge)。快进合并发生在当前分支的尖端和目标分支之间是线性关系,不存在分叉(可见下图)。这种合并操作只是将HEAD指针从当前分支的尖端移动到了目标分支的尖端。
如果当前分支与目标分支之间并不是线性关系而是存在分叉的时候,合并的方式只能用3路合并。合并产生的commit来自当前分支和目标分支的尖端以及两个分支最近的共同commit(这也是3路合并这个名称的由来)。如果合并发两个分支在同一个文件的同一部分都做了修改,git是无法判断采用哪种变动的,会停止合并让你手动解决冲突。所以当git merge报错的时候,可以用git status查看变动的文件找出产生冲突的来源。
一般情况下,快进合并用于小的功能更新或者bug修复,而3路合并用于长期大功能的合并(由于开发时间长,master分支很可能会有新的commit加入导致分叉)。
快进合并的栗子:
# Start a new feature
$ git checkout -b new-feature master
# Edit some files
$ git add <file>
$ git commit -m "Start a feature"
# Edit some files
$ git add <file>
$ git commit -m "Finish a feature"
# Merge in the new-feature branch
$ git checkout master
$ git merge new-feature
$ git branch -d new-feature
3路合并的栗子:
# Start a new feature
$ git checkout -b new-feature master
# Edit some files
$ git add <file>
$ git commit -m "Start a feature"
# Edit some files
$ git add <file>
$ git commit -m "Finish a feature"
# Develop the master branch
$ git checkout master
# Edit some files - 让master分支与目标分支有分叉
$ git add <file>
$ git commit -m "Make some super-stable changes to master"
# Merge in the new-feature branch
$ git merge new-feature
$ git branch -d new-feature
4.四种开发流程流程的比较--中央式工作流(centralized workflow),功能分支工作流(feature branch workflow),gitflow工作流,分叉工作流(forking workflow)
git项目建库的基本命令就是以上这些了,这部分主要讲几种项目协作模式,有兴趣的可以继续看下
中央式工作流
中央流的工作方式类似与SVN,但是用git有更多优点,包括每个人都有完整独立的库,方便安全灵活的分支和合并操作方式等。这种工作流的特点是用一个中央库作为所有项目改动的单一入口,因此这种方式也就不需要有多个分支(只需一个分支,默认为master)。所有项目开发者首先都需要clone这个中央库到本机,在开发新功能后开发者们把本机的master分支push到中央库。
由于中央库是作为标准的,所以如果某个开发者的本地库与中央库有偏离(如下),那么git是不允许把本地的变动push到中央库的,因为这会覆盖中央库的commit历史。这个时候就需要开发者先把远程库同步到本地,然后再push了。而如果本地库与中央库再同一文件的同一位置都有修改再rebase的时候会报错,这个时候就需要用git status来查看冲突的地方进行修正。
下面是中央式工作流的一个具体栗子:
1.首先需要建立一个中央库(一般是没有工作目录的裸库:git init --bare /path/to/repo.git
)
2.每个开发人员都clone中央库到本地(git clone ssh://user@host/path/to/repo.git
)
3.某个开发者进行新功能开发,然后编辑,缓存,commit
$ git status # View the state of the repo
$ git add <some-file> # Stage a file
$ git commit # Commit a file</some-file>
4.其他开发人员同上述步骤进行开发,由于每个人的库都是独立的,所以每个人开发不同的功能这时并不会对别人有影响。
5.某个开发者push commit到中央库(git push origin master
)
6.此时其他开发人员想要push commit到中央库就需要先把中央库同步到本地,因为此时中央库与其他人的本地库已经有偏差,直接git push会报错,需要先git pull进行同步(git pull --rebase origin master
:用rebase标签是把本地改动的commit移动到同步后的master分支的最前端)。一般情况下,两个开发者开发不同的功能再合并的时候不太会产生冲突报错,但如果合并分支的时候确实有报错,则需要用git status来查看引起冲突的文件。修改之后重新rebase,然后才可以用git push把本机的变动上传到中央库。
功能分支工作流
相比与中央流工作流,功能分支流更加方便协作并简化了开发者之间的交流过程。这种流程的核心思想是所有新功能开发都应该相应的特定分支上,而非都是再master分支上。因此在合并新功能分支以前master分支永远都是完整稳定的,开发过程不会对主代码库有任何影响。封装功能分支的同时用pull request进行协作,在合并分支之前发起pull request可以让其他开发人员在分支被整合到master分支以前可以审阅讨论,方便沟通和了解其他开发者的工作和对你的开发的影响。如果pull request被项目管理员接受,之后的操作就是跟中央流一样了:保证中央库与本地库同步之后,将功能分支合并到master分支然后push到中央库。
与前一种方式一样,功能分支工作流也需要一个中央库并且master分支作为正式项目的历史。但是在开发每一个新功能的时候,不是直接在master分支上进行,而是新建新建分支在新分支上进行开发工作(分支的名字一般要起的有意义,能表示新加的功能)。新的功能分支可以被push到中央库(不合并),让其他开发人员查看,也可以作为本机commit的备份。
下面是功能分支流的具体栗子:
1.新建并切换到功能分支(git checkout -b marys-feature master
),然后进行开发(git add & git commit)
2.将开发一般的分支push到中央库作为备份,也让其他开发人员可以查看
$ git push -u origin marys-feature # -u标签表示把marys-feature作为远程跟踪分支添加到中央库。
3.继续并完成开发后上传,确认中央库已经有最近的commit历史,然后发起pull request,通知项目组其他开发人员进行审查和讨论,如果没问题则要将功能分支合并到master分支中。
4.项目管理员收到pull request后与开发者进行反馈交流,开发者根据反馈进行修改,然后用同样的方式上传分支。于此同时其他开发人员也可以一直追踪这个新分支,并git
pull同步到本地按照自己的需求和理解进行修改,而这些修改也需要pull request通知其他开发人员。
5.合并功能分支到中央库master分支,这会产生一个合并commit。如果你想要线性的commit历史,那么可以用rebase来进行合并。
git checkout master
git pull # 切换到master分支并保证中央库已经同步到本地
git pull origin marys-feature # 与git merge marys-feature功能类似,只不过前者能保证pull的是最新的分支
git push
6.其他开发人员可以同时进行相同的操作。
Gitflow工作流
与功能分支流相比,gitflow工作流的分支结构更加复杂一些:除master分支以外还有一个develop分支,新的功能分支都建立在develop分支上,而master分支主要用于版本发布。
gitflow工作流还是有一个中央库,分支结构主要有3层:
a.历史分支-master分支和develop分支
master分支作为项目历史的正式记录分支(所以一般会用版本号来对master分支的commit进行打标),而develop分支则是整合功能分支的分支。
b.功能分支层
与之前一样,所有新功能都需要在特定的分支上进行开发,但是分支不是从master分支而是从develop分支叉出来的,之后合并的时候也是合并到develop分支,因此直接与功能分支进行交互的是develop分支而非master分支。
c.发布分支
当develop分支已经有足够的新功能用于发版,就会从develop分支上分出一个发布分支用于发版。创建这个分支以后就不允许添加新的功能了,只能进行bug修复,文档生成和其他一些发版的相关工作。准备好以后就把发版分支合并到master分支并用版本号进行打标,同时也需要合并会 develop分支,因此中间可能其他操作。这种做法的好处就是在进行发版工作的时候并不会影响正常新功能的开发工作。
d.维护分支
维护分支用于快速小版本的发布或紧急的bug修复,是直接从master分支分出,之后需要合并回master分支(标记上版本号)和develop分支。这样的好处就是bug的修复不需要等下次的版本发布。
下面是具体的gitlow工作流的栗子:
1.首先让一个开发者创建develop分支,可以在本地创建一个develop分支然后push到中央库,然后其他开发者可以clone中央库到本地并为develop分支创建追踪分支。
$ git branch develop
$ git push -u origin develop # develop分支就会有当前项目的全部历史,而master分支确是有删减的版本
$ git clone ssh://user@host/path/to/repo.git
$ git checkout -b develop origin/develop # 在本地创建并切到develop分支
2.从本地develop分支分出功能分支进行功能开发,然后进行功能开发。
$ git checkout -b some-feature develop
$ git status
$ git add <some-file>
$ git commit
3.完成功能开发合并到中央库develop分支(操作与功能分支工作流一致,也是通过pull request进行开发者之间的协作)
```shell
$ git pull origin develop # 保证中央库最新的develop分支已经同步到本地
$ git checkout develop
$ git merge some-feature
$ git push
$ git branch -d some-feature
```
4.准备发版,从develop分支分出release分支,然后进行版本发布所需的测试、文档更新等工作,push到中央库,然后pull request通知项目管理员和其他开发者审查,一致同意后合并到master和develop分支中并删除release分支。当你把分支合并到master分支的时候,需要对commit进行打标以便进行检索,也可以在库的master分支出现合并操作的时候自动生成公开发版,简化发版操作。
```shell
$ git checkout -b release-0.1 develop # 发版的版本号在这个时候建立
# edit/stage/commit
$ git checkout master
$ git merge release-0.1 # 合并到master分支
$ git push
$ git checkout develop
$ git merge release-0.1 # 合并到develop分支
$ git push
$ git branch -d release-0.1
$ git tag -a 0.1 -m "Initial public release" master
$ git push --tags # 标签不会被,除非加上--tags标签
```
5.当用户发现当前版本存在某个bug需要紧急修复时,从master分支分出hotfix分支,处理完以后合并回master和develop分支,然后删除hotfix分支。
```shell
$ git checkout -b issue-#001 master
# Fix the bug
$ git checkout master
$ git merge issue-#001
$ git push
$ git checkout develop
$ git merge issue-#001
$ git push
$ git branch -d issue-#001
```
分叉工作流
分支工作流的模式与之前的不太一样:每个开发人员除了本地库之外还有单独一个服务器端的库(2个库),而非之前的所有人公用一个公共库。除此以外,项目管理员还有一个单独的正式库。这样的好处就是开发者只能把新功能push到自己的库,而没有push到正式库的权限,只有项目管理者能从各开发者的服务器端库push commit到正式库,进一步保证了项目的安全性。
首先分支工作流需要有一个正式库,然后其他开发人员不是直接clone这个库到本机,而是fork这个库到服务器作为个人的服务器端公开库(这就是forking工作流名称的由来)。这个公开库只有自己能够push commit,其他人只能pull变动到本地。下一步就是把本人服务器的公共库clone到本机。当新功能开发完成的时候,开发者把新功能push到个人公开库,然后发起pull request通知项目管理员和其他开发人员功能开发完成并进行审查。项目开发者pull同步开发者的公开库到本机,确认无误后合并到本机的master分支,然后push到正式库。
要注意的是其实正式库和其他开发者的库没有本质区别,只是正式库是项目管理员的库,只用来发版。开发功能的分支做法与之前两种一样,唯一不同的是开发完成厚度分支分支工作流是push到本人的公开库,而前两种是push到正式库。
下面是具体分叉工作流的栗子:
1.项目管理员创建正式库
$ ssh user@host
$ git init --bare /path/to/repo.git # 公共库(包括正式库)一般都是空的。
2.开发者fork正式库到个人服务器
$ ssh developer@host
$ git clone https://user@git.com/user/repo.git # fork其实就是服务器端的clone
3.开发者将服务器公共库clone到本机。由于有 两个库,所以一般需要建立有两个远程链接:一个是个人公开库,一般用origin代表;另一个是正式库,一般用upstream代表。
$ git clone https://user@github.com/user/repo.git
$ git remote add upstream https://github.com/maintainer/repo
$ git remote add upstream https://user@github.com/maintainer/repo.git # 如果库需要认证项目组成员
4.开发新功能,这个与之前两种方式一样
$ git checkout -b some-feature
# Edit some code
$ git commit -a -m "Add first draft of some feature"
$ git pull upstream master # 如果正式库在你fork之后有变动,就需要先进行同步
5.完成开发的新功能分支上传到个人公开库,让其他开发人员可以pull进行同步。然后发起pull request通知项目管理需要合并的分支
$ git push origin feature-branch
6.项目管理员整合开发人员pull request的功能分支到正式库。首先需要查看新功能的变动,如果正式库的master分支和新的功能分支有分叉,就需要项目管理员将开发者的库先同步到本机来解决这些冲突,然后进行合并。合并完这些功能以后push到正式库。
$ git fetch https://github.com/user/repo feature-branch
# Inspect the changes
$ git checkout master
$ git merge FETCH_HEAD
$ git push origin master
7.开发人员同步正式库到本地并更新到个人服务器
$ git pull upstream master
$ git push origin master
总结:
这部分大致介绍了一下项目工作流程以及具体协作的一些命令,那么用git进行项目开发的基本操作就是这些了。想要更加完整的git命令参数,那就直接去看git的文档吧。