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

记录团队使用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

posted @ 2025-08-17 20:29  ddrsql  阅读(63)  评论(0)    收藏  举报