Git协作
冲突
在版本控制系统中,特别是在使用Git时,冲突(Conflict)指的是当多个团队成员对同一个文件的相同部分进行更改并尝试合并这些更改时发生的问题。冲突通常出现在合并分支、重基(rebase)操作或应用补丁时,因为这些操作涉及将一个分支的更改整合到另一个分支上。
冲突的发生情况
冲突发生的典型情况包括:
-
编辑冲突:
两个或更多开发者修改了同一个文件的同一部分。例如,如果开发者A删除了一个函数,而开发者B在同一函数中添加了一些新代码,当这两个分支合并时,Git无法自动决定哪个版本是正确的。 -
文件冲突:
一个开发者修改了一个文件,而另一个开发者删除了同一个文件,或两个开发者重命名/移动了同一个文件但目标不同。
解决冲突
当冲突发生时,Git会标记冲突的文件,并在文件内容中插入特定的标记,指示冲突的位置。这些标记分为以下几部分:
- 开始冲突标记
<<<<<<<
:此标记后跟的是当前分支的内容。 - 分隔符
=======
:分隔当前分支内容和其他分支的内容。 - 结束冲突标记
>>>>>>>
:此标记后跟的是合并进来的分支的内容。
例如,如果在两个分支上对同一文件的同一行进行了不同的更改,Git无法自动合并,文件将包含类似以下内容:
<<<<<<< HEAD
console.log("This is the version in the current branch.");
=======
console.log("This is the version in the branch being merged.");
>>>>>>> feature-branch
如何处理冲突
解决冲突通常涉及以下步骤:
-
识别冲突:
使用git status
查看哪些文件存在冲突。 -
手动编辑文件:
打开冲突文件,查看冲突标记,并决定保留哪个版本的更改,或者可能合并这两个版本的更改。 -
标记文件为已解决:
解决完冲突后,使用git add <file>
命令将文件标记为已解决。这告诉Git您已经手动解决了这些冲突。 -
完成合并:
一旦所有冲突都被解决并标记,完成合并过程,通常是通过git commit
命令。通常,Git会提供一个默认的合并提交消息,确认即可。
处理冲突需要细致的注意力,确保合并后的结果符合预期,同时保持代码的功能和完整性。在团队环境中,有效的沟通和代码审查可以大大减少冲突的发生。
1 分支
1.1 什么是Git分支
在处理不同任务的时候开不同的分支,把不同的任务区别开
多个不同的分支可以合并成一个分支,各个分支互不影响,除非拉取新的更改
为每个任务创建分支是个值得采用的做法,可以更好的追溯变更。
1.2 创建分支
创建新的分支不会更改存储库,只是指出了提交。如下图所示,使用 git branch 命令创建一个名为issue1
的分支,存储库将保持不变,只是为当前提交添加了一个新指针。
2 切换分支
git checkout 命令可更新工作树中的文件,以匹配存储在您希望切换到的分支中的版本。类似于切换目录或者工作区。
2.1 指向分支
-
HEAD 用于表示分支的当前快照。对于一个新的存储库,在默认情况下,Git 会将 HEAD 指向主分支。更改 HEAD 指向的位置将更新您的活动分支。
- (代字号) 和 ^ (插入符号) 指向相对于特定提交的位置。这些符号与提交引用一起使用,通常是 HEAD 或提交哈希(hash)。
- ~ 指的是祖先 (多少代取决于~后的数字)。
- HEAD~1 指的是提交的第一个父级。
- HEAD~2 指的是提交的第一个祖父级。
- ^ 指的是合并提交的父级。
- HEAD^1 指的是 HEAD 的第一个父级,其中 head 是合并提交。
- HEAD^2 指的是 HEAD 的第一个祖父级,其中 head 是合并提交。
合并提交中的提交可以有多个父项。
2.2 暂存分支
当您在使用Git进行版本控制时,可能会遇到需要在多个分支之间切换的情况,同时又希望保留未提交的更改。这时,Git提供了一些策略来处理这些未提交的更改,以便您可以无碍地在不同分支间进行工作。
切换分支与未提交更改的处理
-
基本行为:
- 当您在工作树中有未提交的更改(包括新文件、修改或删除的文件)时,这些更改可以被带到新分支上,前提是不会与新分支的文件产生冲突。
- 如果您提交了这些更改,它们将仅存在于您切换到的新分支上。
-
冲突防止切换:
- 如果存在冲突(即,当前分支的未提交更改与目标分支的文件内容不兼容),Git不会允许您直接切换分支。这是为了防止潜在的代码丢失。
使用 Stash 临时保存更改
为了解决在分支间切换时带来的问题,Git 提供了一个称为 stash
的功能,允许您临时存储未提交的更改,以便您可以清洁地切换到其他分支继续工作。
Stash 的工作原理:
-
存储更改:
使用git stash
或git stash push
命令将当前工作树中的更改保存起来,这样您的工作树就回到了干净的状态(如同刚刚克隆仓库后的状态)。git stash
-
列出存储的更改:
可以通过git stash list
查看所有存储的更改条目。git stash list
-
恢复更改:
当您需要恢复之前存储的更改时,可以使用git stash pop
(应用最近的更改并从stash列表中移除它)或git stash apply
(应用更改但保留在stash列表中)。git stash pop
-
让我们通过一个具体的例子来说明如何使用
git stash
来管理工作区中未提交的更改,使您能够在不同的分支之间灵活切换。场景设定
假设您正在开发一个新功能,在分支
feature-x
上工作。在您开发的过程中,突然接到通知需要立刻解决主分支main
上的一个紧急bug。此时,您的工作区有未提交的更改,这些更改还不够稳定,不能直接提交。同时,您需要切换到
main
分支来修复bug。使用 Git Stash
-
保存当前更改
在您的feature-x
分支上,使用以下命令将所有未提交的更改(包括暂存和未暂存的)存入stash:git stash push -m "WIP: Feature X adjustments"
这里的
-m
选项允许您为stash项添加一个描述性消息,便于以后识别和恢复。 -
查看Stash列表
输入以下命令查看当前保存的stash项:git stash list
这会显示所有stash项,包括您刚刚添加的。
-
切换到主分支
清理了工作区后,您现在可以安全地切换到main
分支:git checkout main
-
进行紧急修复
在main
分支上进行必要的更改并提交。这可能包括测试、修改和提交修复:git add . git commit -m "Fix critical bug in main"
-
切换回功能分支
修复完成后,切换回您的功能分支:git checkout feature-x
-
恢复之前的工作
使用以下命令将之前stashed的更改重新应用到工作区:git stash pop
这将应用最近的stash项并从stash列表中删除它。
-
3 远程分支
3.1 快进合并
在Git中,快进合并(fast-forward merge)是一种特殊类型的合并,它发生在当一条分支可以直接前进到另一条分支的末端时,无需进行任何实际的合并操作。这种情况通常发生在没有新的并行提交影响这两个分支的情况下,即当前分支的末端提交是要合并分支的基础上直接发展出来的提交。
快进合并的工作机制
-
简化的视图:
当一个分支的所有提交都在另一个分支的直接历史线上时,就可以进行快进合并。这意味着可以简单地将接收分支的指针(HEAD)前移到源分支的最新提交,而不需要创建一个新的合并提交。 -
没有分支点:
快进合并不会创建一个新的合并提交,因为它仅仅涉及指针的移动。这样做保持了项目历史的线性和简洁。
快进合并的场景
假设您有两个分支:main
和 feature
。feature
分支从main
分支分出来,main
分支在此期间没有新的提交,而feature
分支有若干提交。当你决定将feature
分支合并回main
分支时:
-
操作:
git checkout main git merge feature
-
结果:
如果main
分支在feature
分出后没有新的提交,Git会执行快进合并,直接将main
分支的HEAD指针移动到feature
分支的最新提交。这样,main
分支现在包含了所有feature
分支的更改。
非快进合并
相对于快进合并,非快进合并发生在当目标分支自分支点以来有新的提交时,即两个分支都有各自独立的提交。在这种情况下,Git无法简单地进行指针前移,因为这样会丢失目标分支上的更改。
-
操作:
如果你想确保即使可以进行快进合并也要创建一个新的合并提交,可以使用--no-ff
选项:git merge --no-ff feature
-
结果:
这将强制Git创建一个新的合并提交,即使是快进合并也不例外。这样做可以保留分支信息和合并历史。
快进合并的影响
快进合并的优点是保持了历史的清洁和线性,但在某些情况下,保留分支的合并历史(非快进合并)可能更有助于了解项目历史的结构和重要决策。选择使用哪种合并策略取决于团队的偏好和项目的特定需求。
3.2 拉取(git pull)
您可以使用 git pull 命令将远程存储库中的最新更改应用到本地存储库。
例如,假设远程分支位于本地分支的上游。远程分支将包含本地分支的所有更改,如下所示。
- 远程分支在本地分支的上游。
在这种情况下,如果我们要将远程分支 (origin/main) 的合并应用到我们的本地分支 (main),这将是一个快进合并。
但是如果本地 main 分支中的更改不存在于远程 origin/main 分支中,则拉取命令将执行合并,且将创建将这些更改绑定在一起的合并提交。
- 如果本地分支与远程分支不同,Git 必须在拉取之前合并和提交。
执行拉取时,会在本地存储库中自动创建合并提交。如果存在冲突,您将必须解决冲突并手动提交合并。
如果没有冲突,提交将自动合并。
3.3 获取(git fetch)
只要没有冲突,在执行拉取时,来自远程分支的更改会自动合并到您当前的本地分支。如果您想获取远程的修改但又不想将它们合并到您当前的本地分支中,您可以执行 git fetch 命令。
获取将从远程下载本地分支上尚不存在的更改。获取FETCH_HEAD ref
将跟踪从远程存储库中获取的更改。
当远程和本地分支都包含不同的后代时,修订历史记录将如下所示:
远程和本地分支具有不同 main 时的修订历史记录。
更改获取后,您可以通过合并获取_HEAD 或执行拉取将这些更改应用到本地存储库。
合并后,更改将应用于本地存储库。
一旦获取FETCH_HEAD
合并,修订历史记录将产生与git pull
操作相同的结果。拉取是同时执行获取和合并操作。
git fetch
vs. git pull
-
git fetch
- 用途:
git fetch
仅仅从远程仓库下载到本地仓库中尚不存在的更改,但不会自动合并或修改您的当前工作。 - 结果:执行
git fetch
后,您的本地仓库将包含远程仓库的最新更改,但这些更改不会影响您的任何工作分支,除非您显式合并它们。 - 修订历史:
FETCH_HEAD
是一个特殊的引用,它指向刚刚从远程仓库获取的最新提交。
- 用途:
-
git pull
- 用途:
git pull
是git fetch
和git merge
的组合。它不仅下载最新的远程更改,还会尝试将这些更改合并到当前分支中。 - 结果:如果合并成功(没有冲突),您的当前分支将自动更新以包括远程分支的更改。
- 修订历史:如果合并成功,您的本地分支修订历史将包括这些远程更改。
- 用途:
具体使用示例
(1)使用 git fetch
检查远程更改
假设您正在本地的 master
分支上工作,并想查看远程仓库(如 origin
)中的更新,但不立即合并这些更改:
git fetch origin
执行此命令后,您可以使用以下命令检查远程分支的状态而不影响您的本地分支:
git log --oneline master..origin/master
这会显示从远程 master
分支获取的提交,这些提交还没有被合并到您的本地 master
分支。
(2) 合并 FETCH_HEAD
如果您决定要将这些更改合并到您的本地分支,可以使用:
git merge FETCH_HEAD
这会将从 git fetch
获取的更改合并到您当前的分支中。
(3)使用 git pull
直接更新和合并
如果您确定要立即获取并合并远程分支的更改,可以直接使用:
git pull origin master
这条命令等同于先执行 git fetch origin master
然后执行 git merge origin/master
。
3.4 推送(git push)
在将本地分支推送到远程存储库之前,所有提交都可用。换句话说,您可以按照自己的节奏在本地分支工作,而不会影响其他团队成员。
当您将本地分支推送到远程时,Git 将快进合并到目标存储库。
但是,如果推送导致非快进合并,Git 将拒绝您的推送以防止您覆盖以前的提交。在这种情况下,您必须拉取最新的远程更改并再次推送。
4 分支的工作流程
在成功的Git分支模型中,提到的五种分支类型,每种都具有特定的作用和管理规则。这些分支类型协助团队在不同的开发阶段进行组织和管理,确保软件开发流程的清晰性和效率。以下是对这些分支的概述和整理:
1. 主分支(Main Branch)
- 别称:
master
或main
- 用途: 包含生产环境中的代码,始终保持稳定且可部署。
- 特点: 通常用于合并其他分支的成果,如新功能、修补或发布准备。不直接在此分支上进行日常工作。
2. 功能分支(Feature Branches)
- 别称: 主题分支
- 用途: 用于开发新功能或实验,每个分支通常源自主分支或开发分支,并最终合并回去。
- 特点: 提供了一个隔离的环境,允许开发者在不影响主分支稳定性的情况下工作,便于代码审查和管理。
3. 发布分支(Release Branches)
- 用途: 用于准备即将发布的版本。从开发分支(如
develop
)分出,进行最后的测试和微调。 - 特点: 主要处理bug修复、文档生成和其他发布任务。一旦版本准备就绪并确认可以发布,发布分支将合并到主分支和开发分支中,随后关闭。
4. 修补分支(Hotfix Branches)
- 用途: 快速修复生产环境中的紧急问题。这些分支从主分支分出,并在问题解决后立即合并回主分支和开发分支。
- 特点: 旨在迅速解决生产中的关键问题,确保对正在进行的开发活动的干扰最小。
5. 开发分支(Development Branch)
- 别称: 集成分支,通常命名为
develop
- 用途: 包含所有为下一版本准备好的功能,是功能分支合并的目标。
- 特点: 在发布周期的大部分时间内保持活跃,用于集成即将发布的功能。在发布前,通常会从中创建一个发布分支。
这些分支策略提供了清晰的结构框架,帮助团队高效管理复杂的开发流程,优化协作和交付速度。根据项目和组织的需求,团队可以对这些策略进行调整,以最适合自己的工作方式。
4.1 主分支
在存储库中进行第一次提交时,默认情况下 Git 会自动创建一个主分支。随后的提交将在主分支下进行,直到您决定创建并切换到另一个分支。
驻留在主分支的代码库是生产就绪的。当最新的提交准备好用于特定版本时,它将被赋予一个发布标签。
4.2 功能发布
当您开始处理新功能或 bug 修复时,您应该创建一个功能分支 (即主题分支)。功能分支通常是在开发分支之外创建的。在功能的整个开发生命周期中,该主题分支可以驻留在您的本地机器中。
每当您准备好将变更集与开发分支合并时,您将把这个分支推送到远程存储库。
4.3 发布分支
当您推出新版本时,您会创建一个发布分支。发布分支可帮助您确保新功能的正常运行。
按照惯例,在命名发布分支时以前缀release-
开头。
通常当它接近生产就绪时,您会在开发分支之外创建发布分支。
团队成员应仅解决此分支上的 bug 修复和与发布相关的问题。这允许其他团队成员继续将新功能推送到开发分支,而不会中断发布的工作流程。
准备发布时,将发布分支与主分支合并,并为新创建的合并提交标记发布编号。
您还应该将发布分支与开发分支合并,以便主分支和开发分支都可从发布分支接收最新的更改/bug 修复。
4.4 修补分支
当您需要快速向生产代码库添加关键修复时,您可以在主分支之外创建一个修补分支。
按照惯例,在命名修补分支时以前缀hotfix-
开头。
修补分支的优点是它允许您快速发布补丁,并将更改与主分支合并,而无需等待下一个版本。
修补分支也应该与开发分支合并。
4.5 开发分支
您的团队应该始终保持开发分支 (即集成分支) 的稳定。您的团队从这个分支创建新的分支,它可以在生产环境中运行。持续集成工具,例如 Jenkins 可以做到这一点。
当一些更改需要合并到开发分支时,创建一个功能/分支来处理通常是个好主意。
4.6 Git分支工作流程示例
在这个Git分支策略工作流程示例中,我们通过处理一个实际场景来阐明如何在开发新功能的同时快速响应并修复生产中的bug。这种分支策略有效地结合了开发/集成分支和功能/主题分支的使用,确保开发流程的连续性和灵活性。
分支策略工作流程步骤详解
1. 发现生产中的Bug
- 在开发新功能的同时,生产环境中出现了一个bug。为了不干扰当前的功能开发,需要创建一个专门的bug修复分支。
2. 创建Bug修复分支
- 从开发分支(通常是
develop
)分出一个新的分支来专门处理这个bug。这保证了bug修复工作与功能开发工作的隔离。
3. 修复Bug并合并
- 在这个新的bug修复分支上进行工作,一旦完成修复,就将这个分支合并回开发分支。这一步确保所有的bug修复都会反映到主要的开发线上。
4. 返回到功能分支
- 修复完bug后,切换回原先的功能分支继续新功能的开发。
5. 同步开发分支的更改
- 开发新功能时,您可能需要刚刚合并到开发分支上的修复。这需要您将开发分支的更改(包括bug修复)整合到您的功能分支上。
- 您可以通过以下两种方式来实现:
- 合并(Merge):将开发分支合并到您的功能分支,这将保留两个分支的历史。
- 变基(Rebase):将您的功能分支变基到最新的开发分支上。这使得历史更为线性,好像您是在最新的开发状态基础上开始添加新功能。
6. 使用变基进行同步
- 选择使用变基来更新您的功能分支,这样您的分支看起来就像是直接基于最新的开发分支开始的。这样可以使项目历史更清晰,也更易于理解。
7. 继续功能开发
- 完成变基后,您的功能分支现在包含了必要的bug修复。您可以继续开发新功能,确保所依赖的代码是最新且稳定的。
5 整合分支
- 合并方法:保留合并分支的所有更改和历史记录。多次合并后,修订历史记录可能会变得复杂。
- 变基方法:维护一个干净的修订历史记录,因为合并的提交会附加在目标分支的末尾。与合并的方法相比,冲突可能更频繁地发生。
为了使您的修订历史记录保持简洁,您可以在将功能分支合并到开发分支之前,将功能分支变基。这会导致快进合并,而不会创建额外的合并提交。
5.1 合并分支
您可以使用 git merge 指令来将多个分支集成。
考虑下面的情况。有两个分支:一个bugfix
分支,其中有一些来自main
分支的提交。
在这种情况下,将“bugfix“合并回“主要“分支并不是什么大问题。那是因为自从创建“bugfix”分支以来,“主要“分支没有改变。Git 将通过将“主要“分支位置移动到“bugfix“分支的最新位置来合并它。这种合并称为“快进“。
然而,在下面的示例中,自从bugfix
分支出来后,main
分支已经更新了几次。在这两个分支上执行合并时,必须组合来自bugfix
和main
分支的更改。
对于这种合并,创建一个“合并提交“并将“主要“位置更新为新创建的合并提交。
即使快进合并是可能的,您仍然可以明确地强制它在没有快进合并的情况下进行合并。
如上所示,非快进合并保留了bugfix
分支。这让您更清楚地了解功能分支bugfix
。您可以轻松找到功能分支的开始或结束位置,并跟踪对功能分支所做的更改。
5.2 将分支变基
要获得更清晰的修订历史记录,您可以使用 git rebase 命令来整合您的分支。
假设我们有两个具有非快进合并场景的分支。
变基将导致分支历史记录看起来类似于下面的示例。
当您将bugfix
分支变基到主分支时,来自bugfix
分支的提交将被重播并附加到主分支的末尾。结果是bugfix
分支历史记录中的单个简单提交串流。
如果在附加提交时发生冲突,Git 会要求您解决冲突,然后再继续对其他提交进行变基。
变基不会移动main
的位置。在任何情况下,您都可以在变基后进行快进或从bugfix
到main
的干净合并。
6 标签
Git 标签标记并标记历史记录中的特定提交。标签通常用于指示发布版本,发布名称 (即 v1.0) 是标签的名称。
Git 标签有两种类型:
- 轻量标签
- 附注标签
轻量标签类似于不会改变的分支。它只是直接指向历史记录中的特定提交。轻量标签主要在您的本地工作区中暂时使用。
附注标签是校验和的,通常在计划标记重要提交时使用。您可以添加消息、签名、日期以及标记者的姓名和电子邮件。
7 查看变更
(1)git diff
用途:git diff
命令用于显示工作目录中未暂存的文件修改和暂存的文件修改之间的差异,或比较两个提交之间的差异。
功能详解:
-
未暂存的变更:
git diff
默认显示工作目录中所有未暂存的变更。git diff
-
暂存的变更:使用
git diff --staged
或git diff --cached
查看已暂存的变更。git diff --staged
-
两个提交之间的差异:指定两个提交的哈希值来查看它们之间的差异。
git diff commit1 commit2
(2)git show
用途:git show
命令用于显示一个对象(通常是提交)的类型、大小、内容等信息。
功能详解:
-
查看特定提交:显示单个提交的详细信息,包括提交的差异和元数据。
git show [commit-hash]
(3)git log
用途:git log
命令用于查看提交历史记录,可以与不同的选项组合使用,以查看特定的历史变更。
功能详解:
-
查看特定文件的提交历史:显示指定文件的提交历史,包括相关的更改。
git log -p [filename]
-
图形化显示分支合并历史:使用图形选项查看更直观的分支历史。
git log --graph --oneline --all
(4)git blame
用途:git blame
命令用于显示每一行文件的最后修改者信息,非常有用于追踪特定行的变更历史。
功能详解:
-
查看文件修改者:逐行显示文件的修改记录,包括提交哈希和作者。
git blame [filename]
其他
git status
和 git log
是两个基本的 Git 命令,它们在日常 Git 使用中扮演着不同的角色。这两个命令提供的信息有着根本的区别,分别关注当前工作区的状态和项目的提交历史。
git status
用途:git status
命令用于显示 Git 工作目录和暂存区的状态。它是诊断和解决代码中状态相关问题的首选工具。
主要功能:
- 显示哪些文件处于修改状态(已修改但未暂存)。
- 显示哪些文件已暂存待提交(已修改并已暂存)。
- 显示当前工作目录与指定分支(通常是当前分支)的差异。
- 提示如何暂存或取消暂存更改,以及如何回滚对文件的修改。
- 提供当前分支和其上游分支的同步状态(前提是设置了上游分支)。
输出示例:
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: example.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: readme.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
sample.txt
git log
用途:git log
命令用于显示当前分支的提交历史。它可以帮助开发者回顾和审查项目的发展过程。
主要功能:
- 显示提交历史,包括提交的哈希值、作者、日期和提交消息。
- 可以通过各种选项来定制显示的日志,如日期范围、作者、文件更改历史等。
- 支持图形化显示历史,通过
--graph
选项展示分支合并历史。
输出示例:
commit fa3e98197e714dbf123681f6927a05538db6aa56 (HEAD -> main, origin/main, origin/HEAD)
Author: John Doe <[email protected]>
Date: Wed Sep 15 14:56:29 2021 +0200
Add new feature
commit aea57694cd1bfeda98234aeccc8ae202b58db2b4
Author: Jane Smith <[email protected]>
Date: Tue Sep 14 12:48:53 2021 +0200
Update README.md
总结
- git status 用于查看工作目录和暂存区的当前状态,是了解当前工作进度和状态的最直接的工具。
- git log 用于查看提交历史,帮助你了解项目的版本历史和过去的开发活动。
这两个命令在日常的 Git 使用中非常关键,为开发者提供了关于项目状态和历史的重要信息。