目录
- 前言
- 模拟一:单一共享分支协作模式
- 1.1 环境准备与分支创建
- 1.2 开发者1的本地环境同步
- 1.3 开发者2的初始设置
- 1.4 开发者1的编码与推送
- 1.5 开发者2的操作与冲突解决
- 1.6 将功能分支合并到主干
- 1.7 清理工作
- 1.8 单一分支协作总结
- 模拟二:多分支(功能分支)协作模式
- 2.1 开发者1创建并推送功能分支
- 2.2 开发者2创建并推送功能分支
- 2.3 跨分支协作场景(工作交接)
- 2.4 通过 Pull Request 合并分支
- 解决git branch -a 打印已被删除的远程分支的方法
- 结论
前言
在现代软件开发中,版本控制系统(Version Control System, VCS)是团队协作的基石,而 Git 凭借其分布式特性、强大的分支管理能力和高效的性能,已成为事实上的行业标准。掌握 Git 不仅仅是知道add、commit、push、pull这几个基本命令,更关键的是理解其背后的协作模型和工作流程。本文将通过两个详尽的模拟场景,深入探讨 Git 在多人协作环境下的两种核心工作模式,并最终介绍如何进行仓库的日常维护,确保每一位开发者都能在一个清晰、高效的环境中工作。
模拟一:单一共享分支协作模式
这种模式通常应用于小型团队或快速迭代的简单项目中,团队成员在同一个功能分支上进行开发。虽然直接,但它也更容易引发冲突,对团队成员的沟通和操作时机有更高要求。
目标:在master分支下的fille.txt文件中,最终新增两行内容:“aaa” 和 “bbb”。
实现:由开发者1新增 “aaa”,开发者2新增 “bbb”。
条件:两位开发者在同一个名为dev的共享分支下完成各自的开发任务。
1.1 环境准备与分支创建
协作的第一步是建立一个共享的工作空间,也就是一个专门用于本次功能开发的分支。通常,这个分支会由项目负责人或其中一位开发者在远程仓库(如 Gitee、GitHub)上先行创建。
上图展示了在 Gitee 的网页界面上创建新分支的操作。从master分支创建了一个名为dev的新分支。这是一个非常普遍的起点,确保了我们的功能开发是基于一个稳定、干净的主线版本。
1.2 开发者1的本地环境同步
假设开发者1已经克隆了该项目。此时,其本地仓库并不知道远程仓库刚刚发生的变化(即新建了dev分支)。我们需要通过一系列命令来验证并同步这个状态。
首先,查看本地存在哪些分支。
gitbranch执行git branch命令后,输出结果只显示了master分支,并且星号*表明当前工作区正处于master分支。这证实了本地仓库对远程新建的dev分支一无所知。
接着,我们查看本地仓库所记录的远程分支信息。
gitbranch -r使用-r(–remotes)参数,我们可以看到本地缓存的远程分支列表。输出origin/master和origin/HEAD -> origin/master,同样没有dev分支的踪迹。这说明本地的远程分支信息也已经过时。
为了获取远程仓库的最新状态,需要执行git pull或git fetch。git pull是一个复合命令,相当于git fetch(从远程抓取最新信息)后跟git merge(将远程分支合并到当前本地分支)。在当前场景下,我们在master分支上,执行git pull会尝试抓取所有远程更新,并更新本地的master分支。
gitpull上图的输出信息非常关键。From gitee.com:caijiuuyk/mypicture表明了数据来源。* [new branch] dev -> origin/dev是最重要的信息,它明确指出 Git 发现了一个新的远程分支dev,并已在本地创建了一个对应的远程跟踪分支origin/dev。Already up to date.则表示本地的master分支与远程的master分支内容一致,无需合并。
现在,我们再次检查所有分支,以确认dev分支已被本地 Git 识别。
gitbranch -a使用-a(–all)参数,可以列出所有本地分支和远程跟踪分支。从图中可以看到,除了本地的master和远程的origin/master,现在列表中明确出现了红色的remotes/origin/dev。这标志着开发者1的本地环境已经成功与远程仓库同步,可以开始在dev分支上进行工作了。
1.3 开发者2的初始设置
为了模拟真实协作,我们为开发者2准备一个全新的工作环境,即重新克隆整个仓库。
当一个新的开发者通过git clone命令克隆一个仓库时,Git 会自动将远程仓库的所有分支信息下载到本地,并创建一个默认的master(或main)本地分支来跟踪origin/master。此时,开发者2的本地仓库同样拥有了origin/dev的信息。
1.4 开发者1的编码与推送
开发者1现在需要在dev分支上添加内容 “aaa”。
首先,需要创建一个本地的dev分支,并切换过去。同时,为了方便后续的push和pull操作,最好让这个本地分支直接与远程的origin/dev分支建立“跟踪”(tracking)关系。
gitcheckout -b dev origin/dev这个命令是一个非常高效的快捷方式,它完成了三件事:
git branch dev:创建一个名为dev的新本地分支。git checkout dev:切换到这个新创建的dev分支。- 设置本地
dev分支跟踪(track)远程的origin/dev分支。
我们可以通过git branch -vv命令来验证这种跟踪关系。
gitbranch -vv-vv参数提供了更详细的分支信息。输出中的[origin/dev]部分明确地告诉我们,本地的dev分支正在跟踪origin/dev。这意味着当在此分支上执行git push或git pull时,Git 会自动知道目标是origin/dev。
接下来,开发者1修改fille.txt文件,添加 “aaa”。
修改完成后,执行标准的提交流程:git add将文件更改添加到暂存区,git commit将暂存区的更改记录为一个新的提交。
本地提交完成后,开发者1需要将这些更改推送到远程共享的dev分支,以便开发者2可以看到。
由于之前已经建立了跟踪关系,所以可以直接使用git push。Git 成功地将本地dev分支的提交推送到了origin/dev。
在 Gitee 网页上刷新并切换到dev分支,可以看到fille.txt文件已经包含了 “aaa” 这行内容,证明开发者1的工作已成功同步到云端。
1.5 开发者2的操作与冲突解决
现在轮到开发者2了。开发者2的目标是在fille.txt中添加 “bbb”。
开发者2首先也需要一个本地的dev分支。这里演示一种与开发者1不同的创建方式。
gitcheckout -b devgitbranch -vvgit checkout -b dev仅仅是基于当前分支(这里是master)创建了一个新的本地分支dev并切换过去。从git branch -vv的输出中可以看到,这个新建的dev分支后面没有[origin/dev]标记,说明它没有与任何远程分支建立跟踪关系。
在这种状态下尝试git pull,希望获取开发者1的更新,会发生什么呢?
gitpullGit 提示了一个非常常见的错误信息:“There is no tracking information for the current branch.” (当前分支没有跟踪信息)。这是因为 Git 不知道本地的dev分支应该从远程的哪个分支拉取数据。Git 在提示中还给出了解决方案:执行git branch --set-upstream-to=origin/<branch> dev来建立连接。
我们按照提示操作,将本地dev分支与远程origin/dev分支关联起来。
gitbranch --set-upstream-to=origin/dev devgitbranch -vv命令执行成功,再次用git branch -vv检查,可以看到[origin/dev]已经出现,跟踪关系建立成功。
然而,开发者2此时忘记了执行git pull来同步开发者1的最新代码,直接在自己过时的版本上开始修改,向fille.txt添加了 “bbb”。
修改后,开发者2进行add和commit,然后尝试push。
推送被远程服务器拒绝(rejected)。这是多人协作中极其常见的场景。错误提示updates were rejected because the remote contains work that you do not have locally明确地指出了原因:远程dev分支包含了本地所没有的提交(即开发者1添加 “aaa” 的提交)。Git 为了防止意外覆盖他人的工作,强制要求开发者必须先将远程的变更合并到本地,解决所有差异后,才能推送。
遵从 Git 的指示,开发者2执行git pull。
gitpullgit pull尝试自动合并远程的origin/dev分支。但是,由于开发者1和开发者2都修改了fille.txt文件的同一区域,Git 无法自动决定最终应该保留哪个版本,于是报告了一个合并冲突(Merge conflict)。终端输出CONFLICT (content): Merge conflict in fille.txt和Automatic merge failed; fix conflicts and then commit the result.,清晰地指明了冲突文件和下一步操作。
此时,打开fille.txt文件,会看到 Git 插入的冲突标记。
冲突标记的含义如下:
<<<<<<< HEAD:这之下到=======之前的内容,是当前本地分支(HEAD)的修改,即开发者2添加的 “bbb”。=======:分割线,区分本地修改和远程修改。>>>>>>> ...:这之上到=======之后的内容,是从远程拉取下来的、导致冲突的修改,即开发者1添加的 “aaa”。
解决冲突需要开发者手动编辑该文件,移除冲突标记,并根据业务需求整合代码,形成最终的正确版本。在这个场景中,目标是同时保留 “aaa” 和 “bbb”。
修改文件并保存后,需要告知 Git 冲突已经解决。首先,查看当前仓库的状态。
gitstatusgit status的输出显示了Unmerged paths(未合并的路径),提示我们需要通过git add <file>...来标记冲突已解决。按照这个流程,我们将解决后的fille.txt添加到暂存区,然后创建一个新的“合并提交”(merge commit)。
在上图中,开发者2执行了git add和git commit(Git 在pull冲突后会自动生成一个默认的合并提交信息,可以直接使用),然后再次执行git push。这一次,由于本地dev分支的历史记录已经包含了远程dev分支的所有历史,并在此基础上新增了一个合并提交,所以推送成功了。
此时,远程的dev分支已经达到了我们的预期目标。
1.6 将功能分支合并到主干
dev分支的开发任务已经完成,下一步是将其成果合并回master主干分支。通常有两种方式:
发起 Pull Request (PR) 或 Merge Request (MR):这是团队协作中最推荐的方式。开发者在 Gitee/GitHub 等平台上创建一个 PR,请求将
dev分支合并到master。这会启动一个代码审查(Code Review)流程,团队其他成员可以检查代码、提出修改意见。只有在审查通过后,代码才会被合并。这确保了主干代码的质量。本地直接合并:如果项目流程允许,或者是由项目负责人操作,也可以在本地完成合并再推送到远程。
这里我们演示第二种方式:本地合并。一个健壮的合并流程如下:
最佳实践:在将功能分支(dev)合并到目标分支(master)之前,应先将最新的目标分支合并到功能分支,以在功能分支内部解决可能存在的冲突。这可以保持master分支的提交历史干净,避免在master上出现复杂的合并冲突。
我们回到开发者1的终端来完成这个操作。
首先,确保本地的master分支是最新版本。
上图显示开发者1切换到了master分支,并执行了git pull,确保与远程master同步。
然后,切换回dev分支,并将最新的master合并进来。
在这个简单的场景中,master分支自dev分支创建以来没有任何新的提交,所以git merge master的结果是Already up to date,没有产生任何变化。但在真实项目中,这一步非常重要,可能会需要解决冲突。
最后,切换到master分支,执行最终的合并。
执行git merge dev,Git 会将dev分支的所有提交应用到master分支上。这里使用了Fast-forward模式,因为master分支的指针可以直接向前移动到dev分支的最新提交位置。
合并完成后,本地master分支已经包含了所有更改,最后一步是将其推送到远程仓库。
1.7 清理工作
功能开发和合并都已完成,dev分支的历史使命也就结束了。为了保持仓库的整洁,应该删除这个不再需要的分支。可以在 Gitee 网页上删除远程分支。
同时,开发者也应该删除各自的本地dev分支,使用git branch -d dev。
最终,在master分支上查看fille.txt文件,可以看到 “aaa” 和 “bbb” 都已存在,任务圆满完成。
1.8 单一分支协作总结
这种工作模式的核心流程可以概括为:
- 尝试直接
git push推送本地修改。 - 如果推送失败,说明远程分支有更新,必须先执行
git pull。 git pull可能会导致合并冲突,此时需要手动解决冲突文件。- 解决冲突后,通过
git add和git commit完成合并提交。 - 再次
git push,此时应该会成功。 - 功能完成后,将该分支合并到
master,然后删除分支。
模拟二:多分支(功能分支)协作模式
这是目前业界最主流、最推荐的协作模式,也常被称为“Git Flow”或“GitHub Flow”的简化实践。每个开发者为自己负责的功能或修复的 Bug 单独创建一个分支,开发工作在各自的分支中隔离进行,完成后通过 Pull Request 合并回主干。
目标:在master分支下,新增function1.txt和function2.txt两个文件。
实现:由开发者1在feature-1分支上创建function1.txt,开发者2在feature-2分支上创建function2.txt。
条件:两位开发者在完全隔离的两个不同分支下协作。
2.1 开发者1创建并推送功能分支
开发者1的任务是创建function1.txt。他将为此创建一个名为feature-1的分支。
gitcheckout -b feature-1该命令从当前的master分支创建了feature-1并切换过去。
开发者1在feature-1分支上创建function1.txt文件,写入内容,然后进行add和commit。
当开发者1尝试git push时,会遇到一个问题。
Git 报错fatal: The current branch feature-1 has no upstream branch.。这是因为feature-1是一个纯本地分支,远程仓库并不知道它的存在,因此 Git 不知道该将它推送到哪里。
解决方案是明确告诉 Git 推送的目标,并建立跟踪关系。
gitpush origin feature-1这个命令的含义是:将本地的feature-1分支推送到名为origin的远程仓库,并在远程也创建一个名为feature-1的分支。输出中的[new branch]表明远程成功创建了新分支。
(注:更常用的命令是git push -u origin feature-1,-u或--set-upstream参数会在推送的同时自动设置好本地分支与远程分支的跟踪关系。)
我们可以通过git branch -a来确认远程跟踪分支已经建立。
remotes/origin/feature-1的出现证实了这一点。
在 Gitee 网页上也可以看到这个新分支以及function1.txt文件。
至此,开发者1的任务初步完成。
2.2 开发者2创建并推送功能分支
现在轮到开发者2,他的任务是创建function2.txt。
首先,开发者2需要确保自己的工作是基于最新的主干代码。这是一个至关重要的习惯。
开发者2先用git branch -a查看了所有分支,发现自己还在之前的dev分支上。
他切换回master分支。
gitcheckout master然后执行git pull,同步远程master分支可能存在的任何更新。
gitpull在最新的master基础上,开发者2创建自己的功能分支feature-2。
gitcheckout -b feature-2接着,开发者2创建function2.txt文件,添加内容,并进行add和commit。
与开发者1一样,他也需要将这个新的本地分支推送到远程。
gitpush origin feature-2推送成功后,远程仓库现在同时存在feature-1和feature-2两个功能分支,它们各自独立开发,互不影响。
2.3 跨分支协作场景(工作交接)
我们模拟一个真实场景:开发者2突然生病,需要开发者1接手feature-2分支继续开发。
开发者1的本地仓库目前并没有feature-2这个本地分支,只知道远程存在origin/feature-2。他需要先获取远程的最新信息。
gitpull开发者1当前在feature-1分支上,他执行git pull时,Git 提示feature-1没有设置上游分支(因为他之前创建时没有用-u参数)。尽管有这个提示,但git pull的fetch部分仍然成功执行了,它从远程抓取了所有分支的最新信息,包括feature-2的存在。
通过git branch -a可以看到,remotes/origin/feature-2已经出现在列表中。
现在,开发者1需要创建一个本地的feature-2分支来跟进这项工作。
gitcheckout -b feature-2 origin/feature-2这个命令我们之前见过,它会创建一个本地的feature-2分支,并自动设置为跟踪origin/feature-2。
开发者1接手后,对function2.txt进行了修改,并提交、推送。
几天后,开发者2康复归来,他需要继续feature-2的工作。他的本地feature-2分支还是他生病前的状态,已经落后于远程。他尝试git pull同步。
由于开发者2当初创建feature-2时也没有建立跟踪关系,所以他也收到了同样的“no upstream branch”错误。
他需要先建立跟踪关系。
gitbranch --set-upstream-to=origin/feature-2 feature-2建立关系后,再次git pull,成功将开发者1所做的修改同步到了本地。他在此基础上完成了最后的开发。
最后,开发者2将最终版本提交并推送到远程feature-2分支。
2.4 通过 Pull Request 合并分支
现在,feature-1和feature-2的开发工作都已完成,是时候将它们合并到master了。我们将使用 Pull Request 来完成这个过程。
首先,开发者2为feature-2分支发起一个 Pull Request,目标是合并到master。
项目负责人或其他团队成员会对这个 PR 进行代码审查。
审查通过后,点击“合并”按钮。
Gitee 会执行合并操作,并在master分支上创建一个合并提交。
此时,master分支已经包含了function2.txt。
接下来轮到合并feature-1。此时,master分支已经因为合并了feature-2而向前演进了。feature-1分支现在是基于一个“旧”的master创建的。直接合并feature-1可能会在master分支上产生复杂的合并历史,甚至冲突。
最佳实践:在为功能分支创建 PR 之前,应先将最新的master分支合并到该功能分支中,确保功能分支是基于最新的主干代码。
开发者1来执行这个操作。首先,同步并更新本地的master分支,以获取feature-2被合并后的最新状态。
gitcheckout mastergitpull然后,切换到feature-1分支,并将master合并进来。
gitcheckout feature-1gitmerge master由于feature-1和更新后的master(包含了function2.txt)修改的是不同的文件,所以没有冲突。Git 自动创建了一个合并提交。执行git merge后,通常会弹出一个文本编辑器,让用户编辑合并提交的信息,保存并退出即可。
现在,本地的feature-1分支既包含了它自己的功能(function1.txt),也包含了来自master的最新更新(function2.txt)。将这个更新后的feature-1推送到远程。
此时,远程的feature-1分支已经与master完全同步,并且包含了自身的开发成果。现在为它发起 PR,将会是一个非常干净、无冲突的合并。
合并这个 PR。
最终,master分支上成功地包含了function1.txt和function2.txt,并且整个协作过程清晰、隔离、可控。
解决git branch -a 打印已被删除的远程分支的方法
在功能分支被合并和删除后,我们的远程仓库已经很干净了。但在本地,情况可能并非如此。
执行git branch -a查看所有分支。
可以看到,即使远程的dev,feature-1,feature-2分支都已被删除,本地的remotes/origin/...列表中依然保留着它们的记录。这些被称为“过时”的远程跟踪分支。虽然它们不影响正常工作,但会造成列表冗余,影响可读性。
我们可以使用git remote show origin命令来查看origin这个远程仓库的详细信息,它会明确指出哪些分支已经过时。
gitremote show origin在输出中,Stale tracking branches部分明确列出了dev,feature-1,feature-2是过时的。
要清理这些过时的本地记录,可以使用git remote prune origin命令。
gitremote prune originprune的意思是“修剪”,这个命令会删除所有在本地存在、但在origin远程上已不存在的远程跟踪分支。
清理完成后,再次执行git branch -a。
gitbranch -a现在列表变得非常干净,只剩下仍然存在的master分支。这是一种良好的仓库维护习惯,能让协作者对项目的当前分支结构有清晰的认识。
(注:一个更便捷的方式是使用git fetch --prune或git fetch -p,它可以在每次从远程拉取信息时,自动清理掉过时的远程跟踪分支。)
结论
本文通过两个详尽的模拟实验,展示了 Git 的两种核心多人协作模式。
- 单一共享分支模式简单直接,适用于小型、快速的项目,但对开发者的操作同步性和沟通要求较高,冲突风险也更大。
- 多分支(功能分支)模式提供了极佳的隔离性,每个功能都在独立的环境中开发,通过 Pull Request 机制引入了代码审查环节,极大地保障了主干代码的质量和稳定性。它通过“先更新再合并”的原则,将冲突解决的责任下放到功能分支内部,保证了
master分支的整洁与线性。这是当今绝大多数团队推荐并采用的标准化工作流程。
无论采用哪种模式,清晰的沟通、统一的规范和对 Git 工作流的深刻理解都是高效协作的关键。掌握这些模式,并辅以如git remote prune这样的仓库维护技巧,将使团队的开发流程更加顺畅、代码库更加健康。