DevToolBox免费
博客

文本 Diff 在线检查器指南:算法、git diff 与最佳实践

14 分钟阅读作者 DevToolBox

TL;DR

文本 diff 工具可以精确找出两个版本的文件或字符串之间的差异。本指南涵盖三种主要 diff 算法(Myers、Patience、Histogram)、如何阅读 git diff 输出、终端工具如 diffcolordiffdelta、JavaScript(jsdiff)和 Python(difflib)中的编程化 diff、语义 diff 与行级 diff 的区别、三方合并与冲突解决、在 CI/CD 中使用 diff 进行回归检测,以及保持 diff 可读性的最佳实践。

核心要点

  • Myers 算法(Git 使用的默认算法)在 O(n*d) 时间内找到最短编辑脚本,适用于大多数 diff 场景。
  • Patience diff 和 Histogram diff 通过在唯一行上对齐来生成更易读的输出,减少噪声 hunk。
  • 统一 diff 格式(以 +- 为前缀的行和 @@ hunk 头)是 Git、补丁文件和代码审查工具的标准格式。
  • 终端查看器如 deltacolordiff 可以显著提高命令行 diff 输出的可读性。
  • 三方合并将两个分支与公共祖先进行比较;理解其机制对于正确解决 Git 冲突至关重要。
  • 将基于 diff 的检查集成到 CI/CD 流水线中,可以自动捕获对 API 契约、配置文件和快照的意外更改。

使用我们的免费文本 Diff 检查工具即时比较两段文本。

什么是 Diff 以及它为何重要

diff("difference"的缩写)是比较两段文本或文件并识别它们之间变化的输出。每一行被分类为添加、删除或未更改。这个概念起源于 1974 年 Unix 的 diff 命令,现在它支撑着现代软件工程中每个版本控制系统、代码审查平台和部署流水线。

Diff 之所以重要,是因为它回答了协作开发中最基本的问题:"什么发生了变化?" 在代码审查中,diff 是审查者检查的产物。在调试中,工作提交和故障提交之间的 diff 可以隔离根本原因。在部署中,当前版本和上一版本之间的 diff 定义了变更的影响范围。

除了代码,diff 还用于在更改前后比较配置文件、跟踪法律文件和合同中的编辑、检测关键系统文件的未授权修改,以及验证数据迁移是否正确转换了记录。任何涉及版本的工作流都能从 diff 中受益。

使用我们的免费文本 Diff 检查工具即时比较两段文本。

Diff 算法:Myers、Patience 和 Histogram

并非所有 diff 的计算方式都相同。算法的选择既影响计算速度,也影响结果输出的可读性。以下是你应该了解的三种最重要的算法:

Myers Diff 算法

Myers 算法由 Eugene Myers 于 1986 年发表,是 Git 的默认算法。它将 diff 问题建模为在编辑图中寻找最短路径,其中水平边表示删除,垂直边表示插入,对角线边表示匹配。算法运行时间为 O(n*d),其中 n 是输入大小,d 是差异数量。由于实际 diff 的更改通常远少于总行数,因此实际运行非常快。

Myers diff 找到最短编辑脚本(SES),即产生将文本 A 转换为文本 B 所需的最少添加和删除操作。这在数学上是最优的,但有时对人类来说难以阅读,特别是当代码被移动或重构而不是简单地就地编辑时。

Patience Diff 算法

Patience diff(由 Bram Cohen 发明)采用不同的方法。它首先识别在两个文件中只出现一次的行——这些"唯一公共行"充当可靠的锚点。然后使用这些锚点的最长递增子序列(LIS)建立对齐,并在锚点之间的间隙中使用标准 LCS 算法递归处理。

结果是输出倾向于在函数签名、类声明和其他结构标记上对齐,而不是在空白行或右花括号等偶然匹配上对齐。你可以在 Git 中通过 git diff --diff-algorithm=patience 或在 .gitconfig 中设置 diff.algorithm = patience 来启用。

Histogram Diff 算法

Histogram diff 是 Patience diff 的优化版本,为 JGit 项目开发。它构建行出现频率的直方图来识别低频行作为锚点,然后应用与 Patience diff 相同的递归策略。它通常产生与 Patience diff 相似质量的结果,但在某些输入上更快。

在 Git 中通过 git diff --diff-algorithm=histogram 启用。许多发现默认 Myers 输出在重度重构文件上令人困惑的开发者会切换到 Patience 或 Histogram 作为默认算法。

算法对比

AlgorithmTime ComplexityReadabilityBest For
MyersO(n*d)Good (minimal edits)General use, speed-critical
PatienceO(n*d) + LISExcellent (structural alignment)Code review, refactored files
HistogramO(n*d) + histogramExcellentLarge files, balanced speed/readability

git diff 详解:统一格式与并排视图

git diff 是开发者最频繁使用的文件版本比较工具。理解其输出格式是一项核心开发者技能。

统一 Diff 格式

默认情况下,git diff统一 diff 格式输出。以下是各部分的含义:

  • 文件头--- a/src/app.js 标识原始文件,+++ b/src/app.js 标识修改后的文件。a/b/ 前缀是虚拟路径标记。
  • Hunk 头:如 @@ -15,7 +15,9 @@ function processData() 表示 hunk 从原始文件第 15 行开始(显示 7 行),修改版本第 15 行开始(显示 9 行)。第二个 @@ 后的文本是 Git 添加的最近函数或作用域名称。
  • 更改行:以 - 开头的行被删除,以 + 开头的行被添加,无前缀的行是未更改的上下文行(默认 3 行,可用 -U<n> 调整)。
--- a/src/utils.js
+++ b/src/utils.js
@@ -10,7 +10,9 @@ function processData(input) {
   const cleaned = input.trim();
-  const result = transform(cleaned);
-  return result;
+  const validated = validate(cleaned);
+  const result = transform(validated);
+  return { data: result, status: 'ok' };
 }

 // Context lines above and below the change

常用 git diff 命令

# Compare working directory vs staging area (unstaged changes)
git diff

# Compare staging area vs last commit (staged changes)
git diff --staged

# Compare working directory vs HEAD (all uncommitted changes)
git diff HEAD

# Compare two branches
git diff main..feature-branch

# Compare two specific commits
git diff abc1234 def5678

# Diff a specific file only
git diff -- src/config.ts

# Word-level diff (useful for prose/docs)
git diff --word-diff

# Color-coded word-level diff (no markers)
git diff --color-words

# Show only filenames that changed
git diff --name-only HEAD~5

# Show filenames with status (A/M/D)
git diff --name-status main..feature

# Statistics: insertions/deletions per file
git diff --stat

# Choose diff algorithm
git diff --diff-algorithm=patience
git diff --diff-algorithm=histogram

# Ignore whitespace changes
git diff -w

# Show diff with more context (default is 3 lines)
git diff -U10

# Generate a patch file
git diff > changes.patch

# Apply a patch
git apply changes.patch

并排视图

Git 原生不生成并排输出,但有多种工具提供此视图。delta 分页器(下文讨论)可在终端中渲染带语法高亮的并排 diff。GitHub 和 GitLab 在其 PR 界面中呈现并排 diff。你也可以使用 GNU coreutils 的 diff -y 获得基本的两列布局。

终端中的文件比较:diff、colordiff、delta

虽然 git diff 覆盖了版本控制文件,但你经常需要比较任意文件。以下是必备的终端工具:

经典 diff 命令

在每个类 Unix 系统上都可用,diff 是原始的文件比较工具。关键标志包括 -u(统一输出)、-r(递归目录比较)、-q(仅报告文件是否不同)、-w(忽略空白)和 -y(并排输出)。

# Unified diff (most common)
diff -u original.txt modified.txt

# Side-by-side comparison
diff -y --width=120 file1.txt file2.txt

# Recursive directory comparison
diff -r dir1/ dir2/

# Report only which files differ (no details)
diff -rq dir1/ dir2/

# Ignore all whitespace
diff -u -w file1.txt file2.txt

# Ignore blank lines
diff -u -B file1.txt file2.txt

# Brief: only report if files differ (exit code 0 or 1)
diff -q file1.txt file2.txt

colordiff:带颜色的 diff

colordiff 包装器为 diff 的输出添加 ANSI 颜色代码。通过包管理器安装(apt install colordiffbrew install colordiff),作为直接替代品使用:colordiff -u file1.txt file2.txt

# Install
brew install colordiff    # macOS
apt install colordiff     # Ubuntu/Debian

# Use as drop-in replacement
colordiff -u original.txt modified.txt

# Pipe standard diff through colordiff
diff -u file1.txt file2.txt | colordiff

delta:现代 Diff 查看器

delta 分页器将 diff 输出转换为具有语法高亮、行号、并排模式和导航的丰富格式显示。通过在 .gitconfig 中添加几行即可集成为 Git 分页器。配置后,每个 git diffgit log -pgit show 命令都会自动通过 delta 渲染。

# Install
brew install git-delta    # macOS
apt install delta         # Ubuntu 22.04+

# Configure as Git pager in ~/.gitconfig
[core]
    pager = delta

[interactive]
    diffFilter = delta --color-only

[delta]
    navigate = true
    side-by-side = true
    line-numbers = true
    syntax-theme = Dracula

# Now every git diff, git log -p, git show
# automatically renders through delta

JavaScript 中的 Diff:jsdiff 库

diff npm 包(通常称为 jsdiff)是在 JavaScript 和 TypeScript 中计算文本差异的标准库。它为许多基于 Web 的 diff 查看器提供支持,并用于测试框架的快照比较。

核心 Diff 函数

jsdiff 提供多种粒度级别:Diff.diffChars() 逐字符比较,Diff.diffWords() 按单词边界分割,Diff.diffLines() 逐行比较,Diff.diffSentences() 按句子操作。每个函数返回一个包含 valueaddedremoved 属性的变更对象数组。

import * as Diff from 'diff';

const oldText = `function greet(name) {
  console.log("Hello, " + name);
  return true;
}`;

const newText = `function greet(name, greeting = "Hello") {
  console.log(greeting + ", " + name + "!");
  return { success: true };
}`;

// Line-by-line diff
const changes = Diff.diffLines(oldText, newText);
changes.forEach(part => {
  const prefix = part.added ? '+' : part.removed ? '-' : ' ';
  part.value.split('\n').filter(Boolean).forEach(line => {
    console.log(prefix + ' ' + line);
  });
});

// Character-level diff
const charChanges = Diff.diffChars('hello world', 'hello there');
// Returns: [{value:'hello '}, {removed:true,value:'world'}, {added:true,value:'there'}]

// Word-level diff
const wordChanges = Diff.diffWords(
  'The quick brown fox',
  'The slow brown fox'
);
// Identifies 'quick' -> 'slow' as the only change

创建和应用补丁

Diff.createPatch() 生成统一 diff 字符串,可保存为 .patch 文件。Diff.applyPatch() 接受原始文本和补丁字符串并产生修改后的文本。这对于实现撤销/重做功能或通过网络高效传输更改非常有用。

import * as Diff from 'diff';

// Create a unified patch
const patch = Diff.createPatch(
  'greeting.js',   // filename
  oldText,          // old content
  newText,          // new content
  'v1',             // old header
  'v2'              // new header
);
console.log(patch);
// Outputs standard unified diff format

// Apply the patch to get the new text
const result = Diff.applyPatch(oldText, patch);
console.log(result === newText); // true

// Structured patch (returns parsed hunk objects)
const structured = Diff.structuredPatch(
  'old.js', 'new.js', oldText, newText, '', ''
);
console.log(structured.hunks);
// [{oldStart:1, oldLines:4, newStart:1, newLines:4, lines:[...]}]

使用我们的免费文本 Diff 检查工具即时比较两段文本。

Python 中的 Diff:difflib 模块

Python 的标准库中自带 difflib,从统一 diff 到 HTML 可视化比较报告,一应俱全。无需安装额外包。

生成统一 Diff

difflib.unified_diff() 接受两个字符串序列(通常是 splitlines() 的行),返回统一格式的 diff 行迭代器。传入 fromfiletofile 作为文件头,lineterm="" 避免打印时出现双换行。

import difflib

old = """def greet(name):
    print(f"Hello, {name}")
    return True""".splitlines()

new = """def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")
    return {"success": True}""".splitlines()

# Generate unified diff
diff = difflib.unified_diff(
    old, new,
    fromfile='greet_v1.py',
    tofile='greet_v2.py',
    lineterm=''
)
print('\n'.join(diff))

# Output:
# --- greet_v1.py
# +++ greet_v2.py
# @@ -1,3 +1,3 @@
# -def greet(name):
# -    print(f"Hello, {name}")
# -    return True
# +def greet(name, greeting="Hello"):
# +    print(f"{greeting}, {name}!")
# +    return {"success": True}

HTML 可视化 Diff

difflib.HtmlDiff() 生成包含并排表格的完整 HTML 页面,高亮显示差异。这对于在自动化测试或文档流水线中生成 diff 报告非常有价值。

import difflib

old = "Hello World\nFoo Bar\nBaz Qux".splitlines()
new = "Hello World\nFoo Baz\nBaz Qux\nNew Line".splitlines()

# Generate a complete HTML diff report
html_diff = difflib.HtmlDiff()
report = html_diff.make_file(old, new, 'Original', 'Modified')

with open('diff_report.html', 'w') as f:
    f.write(report)
# Opens in browser with side-by-side colored comparison

SequenceMatcher 计算相似度

difflib.SequenceMatcher 提供 ratio() 方法,返回 0 到 1 之间的浮点数表示两个序列的相似度。这对于模糊匹配、查重检测和在字符串列表中查找最接近的匹配非常有用。

from difflib import SequenceMatcher

a = "the quick brown fox"
b = "the slow brown fox jumps"

matcher = SequenceMatcher(None, a, b)
print(f"Similarity: {matcher.ratio():.2%}")  # ~72%

# Get matching blocks
for block in matcher.get_matching_blocks():
    print(f"a[{block.a}:{block.a+block.size}] == b[{block.b}:{block.b+block.size}]")

# Get opcodes (edit operations)
for op, i1, i2, j1, j2 in matcher.get_opcodes():
    print(f"{op:>8s} a[{i1}:{i2}] -> b[{j1}:{j2}]")
# Output: equal, replace, equal, insert

语义 Diff 与行级 Diff

传统 diff 工具在不理解内容的情况下对文本行进行操作。行级 diff 将每一行视为原子单元:如果一行中有一个字符改变,整行都被标记为已修改。这对大多数代码有效,但当发生格式更改(如重新缩进代码块或重排段落)时可能产生误导性输出。

语义 diff(也称为结构化 diff 或 AST diff)将内容解析为树结构(如代码的抽象语法树),然后比较树而非原始文本。这意味着它可以区分有意义的代码更改和纯格式更改,检测移动的函数或方法,并产生与语言逻辑结构对齐的 diff。

工具如 difftasticGumTree 和 VS Code 的 Semantic Diff 实现了这种方法。虽然比行级 diff 计算成本更高,但在理解意图比查看原始文本更改更重要的代码审查工作流中越来越受欢迎。

对于 Web 场景,Google 的 diff-match-patch 库提供字符级 diff 和语义清理启发式,即使没有完整的 AST 解析,也能将更改分组为人类可理解的单元。

三方合并与冲突解决

三方合并发生在 Git(或任何 VCS)需要合并从公共祖先分叉的两个分支的更改时。它不仅比较两个文件,还考虑三个版本:base(公共祖先)、ours(当前分支)和 theirs(传入分支)。

合并算法通过计算两个 diff 工作:base-to-ours 和 base-to-theirs。如果某个区域只在其中一个 diff 中更改,则自动接受该更改。如果两个 diff 以相同方式修改同一区域,更改也会被接受(收敛编辑)。冲突仅在两个 diff 以不同方式修改同一区域时发生。

当发生冲突时,Git 会在文件中写入冲突标记:

<<<<<<< HEAD (ours - current branch)
const timeout = 5000;
=======
const timeout = 10000;
>>>>>>> feature-branch (theirs - incoming branch)

# With diff3 conflict style (recommended):
# git config merge.conflictstyle diff3
<<<<<<< HEAD
const timeout = 5000;
||||||| merged common ancestor (base)
const timeout = 3000;
=======
const timeout = 10000;
>>>>>>> feature-branch

开发者必须手动选择保留哪个版本、合并它们或编写全新的代码。VS Code、IntelliJ 和 vimdiff 等合并工具以可视化方式呈现三方 diff,base 版本在中间,两个分支版本在两侧。

Git 还支持 diff3 冲突样式(git config merge.conflictstyle diff3),在标记之间包含 base 版本,使得理解任一分支更改前的原始代码更加容易。

CI/CD 中的 Diff:自动化回归检测

Diff 不仅用于人工审查——它们在 CI/CD 流水线中也是强大的自动化质量门控工具。以下是常见模式:

  • 快照测试:Jest(JavaScript)和 pytest(Python)等框架序列化输出并与存储的快照进行比较。当 diff 非空时测试失败,迫使开发者修复回归或明确批准新快照。
  • API 契约检查openapi-diff 等工具比较当前分支和基准分支之间的 OpenAPI/Swagger 规范。破坏性更改(删除端点、更改类型)导致 CI 构建失败,防止意外的 API 破坏。
  • 配置漂移检测:Terraform 等基础设施即代码工具生成的"计划"本质上是期望状态与当前状态的 diff。CI 流水线可以阻止包含意外更改的应用。
  • 二进制制品 Diffdiffoscope 等工具可以比较编译的二进制文件、Docker 镜像和归档文件,检测构建输出中的意外更改。
  • 包大小监控:构建流水线可以 diff 提交之间的 JavaScript 包大小或 Docker 镜像大小,标记超过阈值的回归。
# CI/CD example: fail if API spec changed unexpectedly
# .github/workflows/api-check.yml
name: API Contract Check
on: [pull_request]
jobs:
  diff-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Check API spec for breaking changes
        run: |
          git diff origin/main -- openapi.yaml > api-diff.txt
          if [ -s api-diff.txt ]; then
            echo "API spec changed. Review required."
            cat api-diff.txt
            npx openapi-diff origin/main:openapi.yaml openapi.yaml
          fi

# Snapshot test in Jest
test('renders correctly', () => {
  const tree = renderer.create(<Component />).toJSON();
  expect(tree).toMatchSnapshot();
  // If output changes, test fails with a diff
});

# Bundle size check
CURRENT_SIZE=$(stat -f%z dist/bundle.js)
BASE_SIZE=$(git show origin/main:dist/bundle.js | wc -c)
DIFF=$((CURRENT_SIZE - BASE_SIZE))
if [ $DIFF -gt 10240 ]; then
  echo "Bundle size increased by $DIFF bytes (>10KB threshold)"
  exit 1
fi

保持 Diff 可读性的最佳实践

diff 的质量不仅取决于算法——它很大程度上受代码编写和提交方式的影响。以下是产生干净、易于审查的 diff 的实践:

  1. 原子化提交:每个提交应包含单一逻辑更改。将重构与功能添加混合会产生难以审查的嘈杂 diff。如果需要在添加功能前重构,请单独提交重构。
  2. 统一格式化:在项目中强制使用格式化工具(Prettier、Black、gofmt)。当每个人使用相同的格式化规则时,diff 永远不会包含纯格式噪声。将格式化工具作为 pre-commit hook 运行使其自动化。
  3. 避免不必要的空白更改:跨多个文件更改缩进样式或添加尾随换行会创建遮蔽真实更改的大量 diff。如果需要空白更改,在专用提交中进行。
  4. 优先追加而非重写:向列表、配置文件或数组添加条目时,在末尾添加而不是在中间插入。这可以最小化 diff 中更改的上下文,避免与修改同一列表的其他分支产生合并冲突。
  5. 在数组和对象中添加尾随逗号:在支持尾随逗号的语言中(JavaScript、TypeScript、Python、Rust),始终包含它们。这样添加新项目到列表只产生一行 diff,而不是修改前一个最后一行的两行 diff。
  6. 编写描述性提交消息:虽然不是 diff 本身的一部分,但好的提交消息帮助审查者在阅读 diff 之前理解其意图。这种上下文使审查过程更快更准确。
  7. 将大 PR 拆分为堆叠 diff:2000 行的 diff 令人望而却步。将大型更改分解为一系列较小的、顺序依赖的 PR。每个 PR 更容易理解和审查,整体更改引入 bug 的可能性更小。
# Example: trailing comma produces cleaner diffs

# WITHOUT trailing comma - 2-line diff to add "grape":
  const fruits = [
    "apple",
    "banana",
-   "cherry"
+   "cherry",
+   "grape"
  ];

# WITH trailing comma - 1-line diff to add "grape":
  const fruits = [
    "apple",
    "banana",
    "cherry",
+   "grape",
  ];

常见问题

代码审查中最好的 diff 算法是什么?

对于大多数代码审查场景,Patience diff 算法生成最易读的输出,因为它在函数签名和类声明等唯一结构行上对齐。你可以通过 git config --global diff.algorithm patience 将其设为 Git 默认值。默认的 Myers 算法更快且产生数学上最小的 diff,但当代码被移动或重构时其输出可能令人困惑。Histogram diff 是一个好的折中方案。

如何在不安装任何东西的情况下在线比较两个文本文件?

将两个文件的内容粘贴到在线 diff 检查工具中,如我们的免费文本 Diff 检查器。工具会以绿色高亮添加的行,红色高亮删除的行。所有处理都在浏览器中进行,数据不会离开你的设备。对于大文件或频繁比较,可以使用 VS Code(内置 diff 编辑器)或命令行 diff 命令。

双向 diff 和三方合并有什么区别?

双向 diff 直接比较两个文件并显示它们之间的变化。三方合并将两个文件与其公共祖先(base 版本)进行比较。这种额外的上下文允许合并工具自动解决非重叠的更改。三方合并是 Git 合并分支时使用的方式。只有当两个分支以不同方式修改 base 的同一区域时才会发生冲突。

如何让 git diff 输出更易读?

安装 delta 分页器并在 .gitconfig 中将其设为 Git 分页器。Delta 为所有 Git diff 输出添加语法高亮、行号和并排模式。你也可以使用 git diff --word-diff 以单词粒度查看更改,这对文档更改特别有用。另一个选项是 git diff --color-words,它内联高亮更改的单词。

CI/CD 流水线中可以使用 diff 工具吗?

是的,diff 在 CI/CD 中广泛用于回归检测。常见模式包括快照测试(Jest、pytest)、使用 openapi-diff 进行 API 契约检查、使用 Terraform plan 进行配置漂移检测和包大小监控。diff 命令本身可以在 shell 脚本中使用其退出码:文件相同返回 0,不同返回 1,便于在意外更改时使流水线步骤失败。

什么是语义 diff,何时应该使用?

语义 diff(或结构化 diff)将代码解析为抽象语法树并比较树而非原始文本行。这意味着它可以区分真正的逻辑更改和格式更改,并可以检测在文件内移动的代码。difftastic 和 GumTree 等工具实现了语义 diff。当审查大型重构、团队格式不一致或需要理解更改意图时使用语义 diff。

Patience diff 算法与 Myers 有何不同?

Myers 算法通过贪心地探索编辑图来找到数学上最短的编辑脚本。它速度快(O(n*d) 时间)但当代码被重新排列时可能产生违反直觉的输出。Patience diff 首先识别在两个文件中都是唯一的行并将其用作稳定锚点,然后递归地填充间隙。这倾向于产生与代码逻辑结构对齐的 diff(例如将函数头与函数头匹配)而不是在空白行或花括号等偶然匹配上对齐。

Conclusion

理解 diff 工具和算法对于有效的代码审查、调试和部署工作流至关重要。从选择正确的算法(Myers 追求速度,Patience 追求可读性)到将自动化 diff 检查集成到 CI/CD,快速准确地识别文件版本之间变化的能力是一项核心工程技能。对于无需任何设置的快速比较,请使用我们的在线工具。对于深度集成到日常工作流,请将 delta 配置为 Git 分页器,并考虑切换到 Patience 或 Histogram diff 作为默认算法。

使用我们的免费在线 Diff 检查工具即时比较两段文本。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

±Text Diff Checker{ }JSON Formatter#Hash Generator

相关文章

Diff 检查器与文本比较完全指南:含代码示例

免费在线 Diff 检查器和文本比较工具。了解 diff 算法工作原理,比较两个文件,掌握 Git diff,含 JavaScript、Python、Bash 代码示例。

Git 命令速查表:开发者必备命令大全

完整的 Git 命令速查表:涵盖配置、分支、合并、变基、暂存和高级工作流程。

Git Rebase vs Merge:何时使用哪个(图解对比)

理解 git rebase 和 merge 的区别。学习何时使用哪个,避免常见陷阱,掌握 Git 工作流。