目录
1 问题描述
如果多人同时开发同一个分支,使用哪种方案:
- git pull = git fetch + git merge
- git pull –rebase = git fetch + git rebase
2 结论
建议多人协同 直接merge,解决冲突简单,减低协同成本,尤其经常性合并master到最新分支。 多人协同开发同一个分支,使用rebase的话,协作成本高:
涉及到合并master的 效率低,协同难 解决冲突,可能需要多次,繁琐。rebase价值?
- 线性化:公司很多业务都是主干使用merge方式,主干都是非线性的,所以分支用rebase保证线性 也没啥意义
- 规整commit:如果本地分支缩减commit,这个到是有价值。
关键结论:团队必须统一选择一种策略。
团队需求
|
推荐策略
|
配置命令
|
---|---|---|
保持线性历史,追求简洁
|
统一用 git pull –rebase
|
git config –global pull.rebase true # 所有git pull默认用–rebase
或针对当前仓库配置:
git config pull.rebase true
|
简化冲突解决,接受合并提交
|
统一用 git pull (merge)
|
统一全团队使用
git pull (禁用rebase),接受合并提交的缺点:git config –global pull.rebase false # 显式关闭rebase
|
3 多人 开发场景
-
git pull --rebase
的本质是:先将本地未推送的提交「暂时移除」,拉取远程最新代码后,再将本地提交「重新应用」到最新远程分支上。若有人已将master
合并到fa
,其他开发者拉取时会遇到两种情况:-
无冲突:若本地修改与
master
合并的代码无重叠,Git 会自动按顺序应用本地提交,无需手动干预; -
有冲突:若本地修改与
master
合并的代码冲突,Git 会暂停变基(rebase),要求开发者手动解决冲突,解决后需使用git rebase --continue
继续,直至所有本地提交都应用完毕。
-
-
与普通合并的区别:
-
普通合并(
git pull
)会生成一个「合并提交」,将本地与远程分支历史合并,冲突仅需解决一次; -
变基(
git pull --rebase
)会将本地提交「重新播放」在远程分支上,每个本地提交都可能触发冲突(若该提交修改的代码与master
合并的代码有重叠)。因此,若本地有多个提交,可能需要多次解决冲突。
-
3.1 多个人开发 feature(基于master拉的分支)。如果有人将最新master 合并到 feature,对其他人有什么影响呢
Merge master 做了什么?执行 git checkout feature 后,再执行 git merge main呢
假设初始提交历史如下:
1 2 3 |
A -- B -- C [main] \ D -- E [feature] |
执行
git checkout feature
后,再执行 git merge main
,Git 会:-
找到共同祖先:确定 feature 和 main 的共同祖先(这里是 B)。
-
准备合并:将 main 分支(C)和当前分支(feature 的 E)的修改与共同祖先(B)进行对比。
-
创建合并提交:如果没有冲突,Git 会创建一个新的提交(M),该提交有两个父节点(E 和 C)。
最终结果:
1 2 3 |
A -- B -- C [main] \ \ D -- E -- M [feature] |
团队协作同步成本更高,需严格遵循流程
协作的开发开发的成本太高。
-
信息同步要求: 若有人计划将
master
合并到fa
,必须提前通知团队,确保其他开发者暂停提交并同步远程分支。否则,若其他人正在基于旧的fa
分支开发并执行变基,可能导致与master
合并的代码产生复杂冲突。 -
推荐协作流程:
-
执行合并前:
-
合并者通知团队暂停提交;
-
所有人执行
git pull --rebase
同步最新fa
(此时可能需解决少量冲突); -
合并者将
master
合并到fa
并推送。
-
-
合并后:
-
其他开发者执行
git pull --rebase
,Git 会自动将本地提交「移到」master
合并提交之后,可能需解决冲突。
-
-
3.2 多个人开发 feature(基于master拉的分支)。如果有人将feature分支 rebase 到最新的master,对齐人有什么影响呢
当多人基于 master 分支拉取 feature 分支进行协作开发时,如果其中一个人将 feature 分支 rebase 到最新的 master,会对其他开发者产生以下影响:
Rebase master做了什么?执行 git checkout feature 后,再执行 git rebase main
当你执行
git rebase main
时,Git 会将 feature 分支的提交 “复制” 到 main 分支的最新提交之后,形成线性的提交历史。这个过程中,Git 实际上是丢弃了 feature 原有的提交记录,并在 main 的基础上重新应用这些提交的修改,生成新的提交对象。假设初始提交历史如下:
1 2 3 |
A -- B -- C [main] \ D -- E [feature] |
执行
git checkout feature
后,再执行 git rebase main
,Git 会:-
找到共同祖先:确定 feature 和 main 的共同祖先(这里是 B)。
-
提取提交补丁:将 feature 分支上的提交(D、E)生成补丁文件。
-
切换到 main:临时切换到 main 分支的最新提交(C)。
-
应用补丁:依次将 D、E 的补丁应用到 main 上,生成新的提交 D’ 和 E’。
-
更新分支指针:将 feature 分支指针指向新的提交链。
最终结果:
1 2 3 |
A -- B -- C [main] \ D' -- E' [feature] |
核心问题:提交历史被重写
Rebase 会丢弃原有提交并生成新的提交对象,即使修改内容相同,提交 ID 也会改变。这导致:
-
rebase 者的本地历史与远程仓库一致。
-
其他开发者的本地历史与远程仓库出现分叉,因为他们的提交 ID 仍然指向旧的提交。
具体影响场景
假设初始提交历史如下:
1 2 3 |
A -- B -- C [master] \ D -- E [feature(远程)] |
开发者 Alice 和 Bob 各自拉取了 feature 分支进行开发:
1 2 3 4 5 |
# Alice本地 A -- B -- C -- D -- E -- F [feature] # Bob本地 A -- B -- C -- D -- E -- G [feature] |
Alice 将 rebase 后的分支推送到远程:
1 |
git push -f origin feature # 强制推送,覆盖远程分支 |
此时,Alice 执行了
git rebase master
,将 feature 分支的提交重新应用到 master 最新提交 C 之后:
1 2 3 4 |
# Alice本地(rebase后) A -- B -- C [master] \ D' -- E' -- F' [feature] |
Alice 将 rebase 后的分支推送到远程:
1 |
git push -f origin feature # 强制推送,覆盖远程分支 |
远程仓库变为:
1 2 3 |
A -- B -- C [master] \ D' -- E' -- F' [origin/feature] |
Bob 的困境
当 Bob 尝试推送自己的提交 G 时:
1 |
git push origin feature # 失败!远程分支历史已被Alice修改 |
Bob 需要先拉取远程更新:
1 |
git pull origin feature |
Git 会发现本地提交 E 与远程提交 E’ 的 ID 不同,从而提示非快进(non-fast-forward)冲突,并生成类似以下的提交历史:
1 2 3 4 5 |
A -- B -- C [master] \ D' -- E' -- F' [origin/feature] / G [feature(本地)] |
Bob 如何解决冲突?
Bob 有两种选择,但都会导致混乱:
方案 1:强制推送(不推荐)
1 |
git push -f origin feature # 覆盖Alice的提交,导致她的修改丢失 |
这会引发 “提交战争”,双方不断强制覆盖对方的提交。
方案 2:合并或 rebase(仍有问题)
Bob 可以尝试合并远程分支:
1 |
git merge origin feature # 创建合并提交,但会保留重复的修改(D/E与D'/E') |
或再次 rebase:
1 |
git rebase origin feature # 丢弃本地E,重新应用G,但可能引发复杂冲突 |
无论哪种方式,都会导致提交历史变得混乱,难以维护。
更严重的问题:重复冲突
如果 Bob 和 Alice 继续在 feature 分支上开发,每次同步都会遇到重复冲突,因为 Git 无法识别 D/E 与 D’/E’ 是相同的修改。
如何避免这种情况?
-
禁止对共享分支 rebase:团队约定只对未推送的本地提交使用 rebase。
-
使用 merge 代替 rebase:多人协作时,优先使用
git merge
整合代码,保留完整历史。 -
保护远程分支:通过 Git 服务器配置(如 GitHub 的分支保护规则)禁止直接推送 feature 分支,所有修改必须通过 PR 合并。
-
定期同步 master:在开发早期频繁合并 master,减少冲突范围。
总结
对已共享的 feature 分支执行 rebase 会导致:
-
提交历史被重写,与其他开发者的本地历史分叉。
-
强制推送覆盖远程分支,可能丢失他人修改。
-
后续协作中频繁出现冲突,难以解决。
最佳实践:在多人协作的 feature 分支上,永远不要执行 rebase,改用git merge
保持历史一致性。
3.3 混用的问题:有的时候使用git pull,有的时候使用git pull –rebase
在同一个分支上混合使用
git pull
(默认合并方式)和 git pull --rebase
会导致严重问题,主要体现为历史记录混乱和冲突解决冗余。以下是具体分析和解决方案:历史记录污染
-
git pull
=git fetch
+git merge
会产生多余的合并提交(如Merge branch 'aa' into aa
),污染分支历史。 -
git pull --rebase
=git fetch
+git rebase
保持线性历史,无合并提交。 -
混合结果:历史记录中交替出现线性提交和合并提交,可读性极差:
1 2 3 4 5 6 7 8 |
- * Merge branch 'aa' (用户A使用git pull) |\ | * 提交3 (远程提交) * | 提交2 (用户A的本地提交) |/ * 提交1 * 提交4 (用户B使用git pull --rebase) |
混合问题举例
1 2 3 4 5 6 7 8 |
hint: You have divergent branches and need to specify how to reconcile them. hint: You can do so by running one of the following commands sometime before hint: your next pull: hint: hint: git config pull.rebase false # merge hint: git config pull.rebase true # rebase hint: git config pull.ff only # fast-forward only hint: |
你的本地分支和远程分支出现了分叉(divergent branches),通常是由于团队成员混合使用了
git pull
(合并方式)和 git pull --rebase
(变基方式)导致的-
分叉分支:你的本地提交和远程提交不在同一条历史线上
1 2 |
本地提交:A -> B 远程提交:A -> C |
解决方案
1 2 3 4 5 6 7 8 9 |
# 1. 获取远程最新代码但不合并git fetch origin # 2. 将本地提交变基到远程分支顶部git rebase origin/aa (1和2整合成 git pull --rebase) # 3. 解决可能出现的冲突(按提示操作) git add <冲突文件> git rebase --continue# 4. 推送到远程git push origin aa |
4 Git rebase 介绍
两大作用:
- 保证一个分支线性。Git rebase 实际上是丢弃了 feature 原有的提交记录,并在 main 的基础上重新应用这些提交的修改,生成新的提交对象,所以整个分支就是一条线,没有旁路分支,旁路都被删除了。rebase模式下,都是把旁路的comit丢弃,然后在分支最新header上回溯生成新的commit .
- 聚合本地分支的多个commit为一个
开发流程
1 2 3 4 5 6 7 8 |
# 开发前:确保基于最新远程代码 git fetch origin # 开发后提交前:变基整合远程更新 git pull --rebase # 推送代码 git push origin aa |
3解决冲突
1 2 3 4 5 6 7 |
# 1. git pull --rebase) # 2. 解决可能出现的冲突(按提示操作) git add <冲突文件> git rebase --continue# 4. 推送到远程git push origin aa |
3 Git介绍
Git 是一个分布式版本控制系统,核心原理围绕文件版本管理和分布式协作展开,以下是其核心原理的关键内容:
-
核心概念:快照而非差异
-
Git 不记录文件的 “变化差异”,而是保存文件在特定时刻的完整快照。
-
当文件未修改时,Git 不会重复存储,而是通过指针指向之前的快照,节省空间。
-
三个核心区域
-
工作区(Working Directory):本地可见的文件目录,正在编辑的文件存于此。
-
暂存区(Staging Area):临时存放待提交的文件快照,可理解为 “提交前的缓冲区”,通过
git add
命令将工作区文件添加到暂存区。 -
本地仓库(Local Repository):存储所有提交的快照和版本历史,通过
git commit
命令将暂存区内容提交到本地仓库。
-
分布式架构
-
每个用户的本地仓库都是完整的,包含全部历史记录,无需依赖中央服务器即可独立工作。
-
中央仓库(如 GitHub)主要用于同步协作,通过
git push
(本地推送到远程)和git pull
(远程拉取到本地)实现多人代码同步。
-
版本标识:哈希值(SHA-1)
-
所有快照、提交记录等都通过哈希值唯一标识(如
a1b2c3...
),确保数据完整性(任何修改都会导致哈希值变化)。
-
分支管理
-
分支本质是指向某个提交快照的可移动指针,默认主分支为
main
或master
。 -
新建分支(
git branch <name>
)会创建一个新指针,切换分支(git checkout
或git switch
)则移动当前指针指向目标分支。 -
合并分支(
git merge
)可将不同分支的修改整合,解决冲突后形成新的提交。
Git commit对象解析
在 Git 中,
commit
对象是版本控制系统的核心组件之一,它记录了代码库在某个时间点的状态。理解 commit
对象的结构和原理,有助于掌握 Git 的底层机制。-
Commit 对象的核心作用
-
保存代码快照:每个
commit
代表项目在某个时刻的完整状态(通过树对象 Tree 引用所有文件)。 -
记录历史关系:通过
parent
指针连接到父提交,形成提交链(即版本历史)。 -
元数据存储:包含作者、提交者、时间戳、提交信息等元数据。
-
Commit 对象的组成结构
每个
commit
对象由以下部分构成:(1)元数据(Metadata)
-
作者(Author):编写代码的人(格式:
姓名 <邮箱> 时间戳
)。 -
提交者(Committer):执行
git commit
的人(通常与作者相同,但若代码被他人提交,两者会不同)。 -
提交时间(Timestamp):记录提交的时间点。
-
提交信息(Message):描述本次提交的目的(如
"Fix bug #123"
)。
(2)内容引用
-
树对象(Tree):指向当前提交的文件目录结构(即项目快照)。
-
父提交(Parent):指向该提交的上一个提交(第一次提交无父节点,合并提交可能有多个父节点)。
(3)哈希值(SHA-1)
-
每个
commit
对象通过内容计算生成唯一的 40 位哈希值(如a1b2c3d4...
),用于标识和验证提交。
-
Commit 对象的底层存储
Git 使用 对象存储(Object Store) 来保存
commit
对象,这些对象位于仓库的 .git/objects
目录中。每个 commit
对象实际上是一个经过压缩的文件,包含上述所有信息。例如,一个简化的
commit
对象内容可能如下:
1 2 3 4 5 6 |
tree 56789... # 指向当前提交的树对象 parent 12345... # 指向父提交(第一次提交无此行) author John Doe <john@example.com> 1630412345 +0800 committer John Doe <john@example.com> 1630412345 +0800 Fix typo in README.md |
-
Commit 对象与其他 Git 对象的关系
Git 中有四种核心对象:
-
Blob:存储文件内容。
-
Tree:表示目录结构,包含 Blob 和子 Tree。
-
Commit:指向一个 Tree,并记录元数据。
-
Tag:指向特定 Commit 的引用(如版本标签)。
它们的关系如下:
1 2 |
Commit → Tree → Blob(s) ↘ Tree → Blob(s) |
-
每个
commit
指向一个根 Tree 对象。 -
Tree 对象递归引用子目录和文件(Blob)。
-
Blob 对象存储实际的文件内容。
-
提交链与分支
-
提交链:通过
parent
指针,多个commit
对象形成一条线性历史。 -
分支:本质是指向某个
commit
的指针(如main
分支指向最新提交)。 -
HEAD:指向当前活动分支的指针。
例如,一个简单的提交历史可能是:
1 2 3 |
A ← B ← C (main) ↑ HEAD |
-
如何查看 Commit 对象详情?
-
查看提交信息:
git show <commit-hash>
-
查看提交树结构:
git ls-tree <commit-hash>
-
查看原始对象内容:
git cat-file -p <commit-hash>
总结
Commit 对象是 Git 的核心数据结构,它通过引用树对象(Tree)保存文件快照,通过父指针(Parent)连接历史,形成完整的版本控制系统。理解这一结构有助于深入掌握 Git 的工作原理,尤其是在处理复杂的历史操作(如变基、合并)时。
Git 分叉指什么
在 Git 中,分叉(Diverged) 指的是本地分支与远程分支的提交历史出现分歧的状态。当两个分支基于同一提交点分别发展出不同的提交时,就会形成 “分叉”。
分叉的本质
Git 的分支是指向提交对象的指针。当多人协作时,如果:
-
用户 A 在本地分支
feature
上提交了新内容,并推送到远程。 -
用户 B 也在本地
feature
分支开发,但未同步远程最新提交,直接在本地提交了新内容。
此时,本地和远程的
feature
分支会各自发展出不同的提交历史,形成分叉。典型场景:多人协作同一分支
-
初始状态:本地和远程
feature
分支都指向提交C3
。
1 |
master: C1 → C2 → C3 (HEAD, origin/feature, feature) |
-
用户 A 在本地提交
C4
并推送到远程:1master: C1 → C2 → C3 → C4 (origin/feature)
-
用户 B 在未拉取远程更新的情况下,本地提交
C5
:
1 |
master: C1 → C2 → C3 → C5 (HEAD, feature) |
-
分叉形成:
-
远程分支
origin/feature
:C1 → C2 → C3 → C4
-
本地分支
feature
:C1 → C2 → C3 → C5
-
此时,Git 会提示 “本地分支与远程分支分叉”(Your branch and
origin/feature
have diverged)。5、分叉的影响
-
(1)无法直接推送:若用户 B 尝试
git push
,Git 会拒绝,因为提交历史与远程不一致。(2)需合并或变基:解决分叉需通过以下方式同步历史:
-
合并(Merge):使用
git pull
将远程分支合并到本地,生成一个新的 “合并提交”(如C6
)。
1 |
master: C1 → C2 → C3 → C4 → C5 → C6 (HEAD, feature) |
-
-
变基(Rebase):使用
git pull --rebase
将本地提交 “移动” 到远程分支的最新提交之后,保持线性历史。
-
避免分叉的最佳实践
-
频繁拉取更新:每次开发前先
git pull --rebase
,保持本地与远程同步。 -
分支隔离:避免多人直接在同一分支开发,使用独立分支(如
feature/userB
)开发后再合并。 -
理解工作流:根据团队协作模式选择合适的工作流(如 Git Flow、GitHub Flow)。
总结
分叉是多人协作中常见的情况,本质是提交历史的分歧。通过合理的拉取、合并或变基操作,可以解决分叉并保持代码历史的一致性。 -