惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
Google DeepMind News
Google DeepMind News
Attack and Defense Labs
Attack and Defense Labs
Webroot Blog
Webroot Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
TaoSecurity Blog
TaoSecurity Blog
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
News | PayPal Newsroom
S
Security Affairs
T
Tor Project blog
P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security @ Cisco Blogs
H
Heimdal Security Blog
Hacker News: Ask HN
Hacker News: Ask HN
Help Net Security
Help Net Security
U
Unit 42
云风的 BLOG
云风的 BLOG
The Hacker News
The Hacker News
Cisco Talos Blog
Cisco Talos Blog
量子位
F
Full Disclosure
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
有赞技术团队
有赞技术团队
T
Troy Hunt's Blog
P
Privacy & Cybersecurity Law Blog
Forbes - Security
Forbes - Security
人人都是产品经理
人人都是产品经理
L
Lohrmann on Cybersecurity
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Security Blog
Microsoft Security Blog
博客园 - Franky
腾讯CDC
AI
AI
Last Week in AI
Last Week in AI
Latest news
Latest news
Google Online Security Blog
Google Online Security Blog
N
Netflix TechBlog - Medium
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
IT之家
IT之家
Martin Fowler
Martin Fowler
Blog — PlanetScale
Blog — PlanetScale
V2EX - 技术
V2EX - 技术
酷 壳 – CoolShell
酷 壳 – CoolShell

博客园 - ddrsql

从“黑盒”到“全景”:在.NET中拥抱OpenTelemetry与SigNoz的可观测工具 关于git分支堆叠 记一次对老服务改造 国产麒麟(Kylin-Server-10)系统无外网环境安装docker vue3学习之axios、mockjs、nswag vue3学习之tabler组件Layout布局 vue3学习之BootstrapVueNext Framework升级到Core以及Dapper支持达梦数据库 Avalonia集成Prism与Abp Avalonia中使用EF增删改查DM数据库 Linux中使用原生Wpf之Avalonia WPF通过wine适配统信uos系统 单体仓库下通过helm优雅的将微服务部署到k8s DevOps 前端项目(angular、vue、react)打包静态资源生成一份Docker镜像支持部署不同环境 VS使用IIS调试(非附加进程).net core、.net framework程序 k8s集群通过nginx-ingress做tcp、udp 4层网络转发 DevOps .net core Jenkins持续集成Linux、Docker、K8S swaggerui集成oauth implicit redis cluster集群web管理工具 relumin
记录团队使用git合并代码丢失
ddrsql · 2025-08-17 · via 博客园 - ddrsql

记录团队使用git合并代码丢失

典型事故流程

  1. 分支合并
    在合并过程中,冲突处理不当、或遗漏逻辑,导致部分功能代码被覆盖或删除。
  2. 缺乏充分测试
    合并后未在测试环境跑全量回归(一般能做到全量回归比较少),直接进入发布流程。
  3. 上线后暴露问题
    用户反馈功能缺失、接口报错、数据异常等。

Git 合并时出现代码丢失,但团队却直接发布到了正式环境。这类事故在真实项目中并不少见,而且一旦上线,影响面可能非常大。

三路合并(Three Way Merge)

聊具体问题前先了解下Git 三路合并(Three Way Merge)是一种常见的合并策略,用于将两个分支的更改合并到一个共同的祖先分支上。
在 Git 中,每个提交都有一个唯一的哈希值,该哈希值可以用来确定提交之间的关系。在三路合并中,Git 使用三个提交来进行合并:

  1. Base Commit:这是两个分支的共同祖先提交,也被称为合并基础。它是两个分支的最早的共同提交。

  2. Source Commit:这是要合并的源分支的最新提交。

  3. Target Commit:这是要合并到的目标分支的最新提交。

image

  • 修改前的Base版本里的内容是:Print("bye")

  • Yours的版本里内容是:Print("hello")

  • Mine的版本里内容是:Print("bye")

说明Yours对这一行做了修改,而Mine对这行没有做修改,因此对YoursMine进行merge后的结果应该采用Yours的修改,于是就变成Print("hello")

模拟复现

先上一份完整分支图,在逐步拆解。
2025-08-17-19-51-51

1、Merge branch 'feature/user1-string'

user1完成feature时的代码,举例以输出 User1-StringBuilder-OK 为正常功能

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        var str = StringStringBuilder();
        str.Append("-OK");
        Console.WriteLine(str);
    }

    private static StringBuilder StringStringBuilder()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("User1");
        stringBuilder.Append("-");
        stringBuilder.Append("StringBuilder");
        return stringBuilder;
    }
}

user1完成feature时的功能正常输出是

Hello, World!
User1-StringBuilder-OK

2、Merge branch 'main' into feature/user2-json

先看合并后的结果 b80b369
2025-08-14-22-37-42
2025-08-14-22-33-08

问题就是出现在这一步,现在看很容易发现,因为分支图上看提交比较少。实际团队开发过程中多人协作分支图比示例复杂,有些feature合并到主干前为了同步最新代码会将主干合并到feature中在加上开发周期一长,就会变得非常交错、像“藤蔓”一样缠绕。
这种情况虽然技术上没问题,但从可读性和维护角度来看,确实会带来一些挑战,再从分支图上就不好直观看出来。
回溯使用 question/main 分支 merge into question/user2-json 时就能看见具体的冲突

(question/main -> origin)
# git log -1 --show-signature
commit 74ba0a2188495b820d628a29fcfb2e68740f53e3 (HEAD -> question/main, origin/question/main)

(question/user2-json -> origin)
# git log -1 --show-signature
commit 49e0d1f3a07daec9b0c5fafe82696849713f4522 (HEAD -> question/user2-json, origin/question/user2-json)

# 最近共同祖先
# git merge-base question/user2-json question/main
555a7550a72ef3177a85d6a1fb1f8131bee4760c

如下图左侧对应 question/main 74ba0a2 右侧对应 question/user2-json 49e0d1f,底部对应 Merge branch 'feature/Program' 555a755 进行三方合并比较,由于10行内容左右两侧都修改过出现冲突提示。但是因为人为操作错误选择导致错误覆盖。
image

还有存在把自动生成的冲突文件清单信息修改成其它自定义信息。

自动生成
* Merge branch 'main' into feature/user2-json

# Conflicts:
#	ConsoleApp1/Program.cs

自定义
feat: 合并main最新代码

User2合并main后 test/user2-json 最新代码输出

Hello, World!
{"Name":"Test-User2-Json","Description":"User1-StringBuilder"}
User1-StringBuilder

正常功能的User1-StringBuilder-OK变成了User1-StringBuilder,当然实际功能不可是这样简单一眼就能看出来,实际测试过程中测试人员也只会关注feature/user2-json功能进行测试,且在没有全量功能回归测试覆盖时很容易把这样的错误忽略以至于应用到正式环境。

3、Merge branch 'feature/user2-json'

在没有把User2的代码合并到主干前,Merge branch 'feature/user3-cw'User3合并代码到主干时输出功能都是正常输出

User3-CW
Hello, World!
User1-StringBuilder-OK

再看合并后的结果 ef007f7
2025-08-14-23-22-10
image
这一步合并会主干就会使用到2中错误解决冲突选择悄无声息覆盖掉User1的提交

(solve/main -> origin)
# git log -1 --show-signature
commit cc46997a990e8c41e273c23ebc5135a0be9401a3 (HEAD -> solve/main, origin/solve/main)

(solve/user2-json -> origin)
# git log -1 --show-signature
commit 6f99550be227de3a5228fe42709e7d712c5915bb (HEAD -> solve/user2-json, origin/solve/user2-json, origin/feature/user2-json, feature/user2-json)

# 最近共同祖先
# git merge-base solve/user2-json solve/main
74ba0a2188495b820d628a29fcfb2e68740f53e3

再用三路合并对比,分别找到对应签名的文件模拟合并比较,是不会出现冲突。
74ba0a2是Base

using System;
using System.Text;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        var str = StringStringBuilder();
        str.Append("-OK");
        Console.WriteLine(str);
    }

    private static StringBuilder StringStringBuilder()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("User1");
        stringBuilder.Append("-");
        stringBuilder.Append("StringBuilder");
        return stringBuilder;
    }
}

cc46997是Mine

using System;
using System.Text;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("User3-CW");
        Console.WriteLine("Hello, World!");
        var str = StringStringBuilder();
        str.Append("-OK");
        Console.WriteLine(str);
    }

    private static StringBuilder StringStringBuilder()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("User1");
        stringBuilder.Append("-");
        stringBuilder.Append("StringBuilder");
        return stringBuilder;
    }
}

6f99550是Yours

using System;
using System.Text;
using System.Text.Json;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
        var str = StringStringBuilder();
        str.Append("User2-Json");
        Test(str.ToString());
        Console.WriteLine(str);
    }

    private static StringBuilder StringStringBuilder()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("User1");
        stringBuilder.Append("-");
        stringBuilder.Append("StringBuilder");
        return stringBuilder;
    }

    private static void Test(string description)
    {
        var str = JsonSerializer.Serialize(new
        {
            Name = "Test-User2-Json",
            Description = description
        });
        Console.WriteLine(str);
    }
}

2025-08-14-23-41-31
2025-08-14-23-41-53

使用rebase方式“重放”

使用 solve/user2-json 分支 rebase onto solve/main 会将目标分支的提交一个一个地“重放”在新的基础上。强制你在每个冲突点做出明确选择,避免“看起来没问题,实际上有 bug”。也能看见具体的冲突。
2025-08-17-19-30-31

事故原因剖析

1、冲突解决不严谨:合并时直接保留一方改动,另一方逻辑被丢弃。
2、逆向合并历史污染:曾经把主干合到分支时删除了代码,后续再合回主干时,这些删除被“合法化”

如何避免

1、合并存在冲突时与冲突当事人确认合并保留。
2、保持 feature 分支短小:尽量让每个 feature 分支的生命周期短一些,快速开发、快速合并。
3、使用 rebase 保持提交历史整洁、squash 合并(压缩提交)让分支图更清晰。
4、全量测试安全闸:“合并后 → 测试 → 灰度 → 全量”的安全闸。

结语

Git 本身是一个强大而灵活的工具,但它的效果最终取决于人怎么用。合并出问题,往往不是 Git 的锅,而是使用者在流程、沟通或理解上的疏漏。

测试示例 https://github.com/ddrsql/gitmerge