Git基础:commit、branch和merge
1. 概述
Git是几乎每个项目都会使用的版本管理工具,但很多初学者只记住了几个命令,却不理解其本质。本文介绍三个核心概念:commit(保存一个快照)、branch(独立的工作分支)和merge(合并分支)。读完后,你将能够自信地创建新分支、正确地commit、处理merge时的冲突,并知道何时应该用merge而不是rebase。
2. Commit——保存一个快照
2.1. Commit是什么
每个commit都是整个文件在某一时刻状态的“快照”,包含指向父commit的指针。Git不保存diff,而是保存快照——这使得分支和合并既快又便宜。
# 初始化repo并进行第一次commit
git init order-service
cd order-service
echo "# Order Service" > README.md
git add README.md
git commit -m "docs: 为Order Service添加README"
解释:
git init创建新repo,生成隐藏目录.git。git add README.md将文件放入staging area(等待commit的缓冲区)。git commit -m "..."创建快照,附加描述更改的message。
检查历史记录:
git log --oneline
a1b2c3d docs: 为Order Service添加README
2.2. 写好commit message
一个commit应该回答“这个改动做什么,为什么要做”。常见的约定(conventional commits):
feat: 添加创建订单的API
fix: 修复应用优惠券时计算总额错误的bug
refactor: 将OrderValidator从OrderService中提取出来
注意: 避免含糊的commit message,如
"update"或"fix bug"。几个月后再看git log,你根本不知道那个commit修复了什么。
2.3. 用git add -p分阶段添加文件
当一个文件有多个不相关的改动时,不要一次性commit所有内容。
git add -p src/main/java/com/nonladev/OrderService.java
Git会询问你想暂存哪些“hunk”(改动块),让你能拆分成多个小的、易于review且易于revert的commit。
3. Branch——独立地在多个分支上工作
3.1. 创建和切换分支
Branch只是一个轻量级的指针,指向某个特定的commit。创建新分支不会复制整个代码,只是添加另一个指针。
# 创建新分支并切换到它
git branch feature/add-payment
git checkout feature/add-payment
# 或者将上面两个命令合并为一个(Git ≥ 2.23)
git switch -c feature/add-payment
解释:
git branch <name>创建分支但仍保持在当前分支。git checkout <name>或git switch <name>将HEAD移动到该分支。git switch中的-c是创建并切换的简写。
3.2. 查看分支列表
git branch -v
* feature/add-payment d4e5f6a 添加PaymentService骨架
main a1b2c3d docs: 为Order Service添加README
* 指示当前分支(HEAD所在的位置)。
3.3. 遵循分支命名规范
清晰的分支名称帮助整个团队了解该分支的用途:
feature/add-payment # 添加新功能
fix/order-total-wrong # 修复bug
refactor/extract-validator
hotfix/prod-null-pointer
3.4. 完成后删除分支
# 删除已merge的本地分支
git branch -d feature/add-payment
# 删除未merge的分支(强制删除,小心丢失commit)
git branch -D feature/add-payment
-d(小写)更安全,因为如果分支未merge到任何地方,Git会发出警告。
4. Merge——合并分支
4.1. Fast-forward merge
如果从分割分支以来main没有新commit,Git只需将main指针移动到feature分支的最后一个commit——这叫做fast-forward。
git switch main
git merge feature/add-payment
Updating a1b2c3d..d4e5f6a
Fast-forward
PaymentService.java | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
没有创建新的merge commit,历史记录仍然是一条直线。
4.2. Merge commit(3-way merge)
如果两个分支都在分割点之后有自己的commit,Git会创建一个有两个父节点的merge commit,合并两个分支的内容。
git switch main
git commit -am "fix: 修复item数量验证" # main有新commit
git switch feature/add-payment
git switch main
git merge feature/add-payment
Merge made by the 'recursive' strategy.
PaymentService.java | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
Git自动创建merge commit,默认message为Merge branch 'feature/add-payment'。
4.3. 处理merge冲突
当两个分支修改了同一处代码时就会产生冲突。例如OrderService类:
// 在main中
public class OrderService {
public BigDecimal calculateTotal(Order order) {
return order.getSubtotal().multiply(BigDecimal.valueOf(1.1)); // 添加10%税
}
}
// 在feature/add-payment中
public class OrderService {
public BigDecimal calculateTotal(Order order) {
return order.getSubtotal().subtract(order.getDiscount()); // 减去折扣
}
}
Merge时,Git无法自己决定保留哪一行,文件会出现标记:
public class OrderService {
public BigDecimal calculateTotal(Order order) {
<<<<<<< HEAD
return order.getSubtotal().multiply(BigDecimal.valueOf(1.1));
=======
return order.getSubtotal().subtract(order.getDiscount());
>>>>>>> feature/add-payment
}
}
你需要手动修改以正确结合逻辑(例如:先减去折扣,再加上税),删除<<<<<<<、=======、>>>>>>>标记,然后:
git add OrderService.java
git commit
注意: resolve冲突后的commit不需要
-m参数,Git会打开编辑器显示默认message,说明这是merge commit——保持原有message即可,除非你要添加额外说明。
4.4. Merge vs Rebase
| 标准 | merge |
rebase |
|---|---|---|
| 历史记录 | 保持原样,包含merge commit | 重写历史,分支线性 |
| 安全性 | 不修改旧commit | 修改旧commit(改变hash) |
| 使用场景 | 公开共享的分支(main、develop) |
个人分支,在创建PR前想要整洁历史 |
# 将feature分支rebase到最新的main上
git switch feature/add-payment
git rebase main
注意: 不要对已push且他人正在从中pull的分支进行
rebase——你会搞乱他人的历史,导致他们重新pull时冲突连连。
5. 用JUnit测试merge场景
虽然Git不是Java代码,但你完全可以写测试来确认merge两个分支后业务逻辑没有破裂——这就是为什么CI总是在merge commit上运行测试。以第4.3节已resolve冲突的OrderService为例:
public class OrderService {
public BigDecimal calculateTotal(Order order) {
BigDecimal afterDiscount = order.getSubtotal().subtract(order.getDiscount());
return afterDiscount.multiply(BigDecimal.valueOf(1.1));
}
}
class OrderServiceTest {
private final OrderService orderService = new OrderService();
@Test
void shouldApplyDiscountThenTax() {
Order order = new Order(
BigDecimal.valueOf(100), // 小计
BigDecimal.valueOf(10) // 折扣
);
BigDecimal total = orderService.calculateTotal(order);
assertThat(total).isEqualByComparingTo(BigDecimal.valueOf(99.0));
}
}
解释:小计100减去折扣10剩90,再乘以1.1(10%税)得99。这个测试就是“安全网”,确保merge两个逻辑(税和折扣)时不会互相抵消。
✓ shouldApplyDiscountThenTax()
6. 小团队推荐的工作流程
- 每个task都从
main创建新分支:git switch -c feature/xxx。 - 小而频繁的commit,message清晰。
- 创建Pull Request前,更新分支到最新的
main:git switch feature/add-payment git fetch origin git merge origin/main - Resolve冲突(如有),在本地重新运行测试。
- Push并创建PR,让CI在merge commit上运行测试后再合并到
main。
注意: 多人团队中不要直接commit到
main——总是通过分支+PR,让CI检查和代码review先行。
7. 常见错误
- 忘记
git add就commit:Git说nothing to commit, working tree clean,虽然你已修改文件——因为文件还未暂存。 - Merge到错误的分支:merge前总要
git status和git branch检查你在哪里。 - Resolve冲突时留下标记:忘记删除
<<<<<<<会导致代码无法编译——resolve冲突后立即构建/测试。 - Force push到共享分支:在已与他人共享的分支上
git push --force会删除同事的历史。只用--force-with-lease,且仅限个人分支。
8. 总结
Commit、branch和merge是你每天与Git打交道的三个基本操作。掌握commit可以让代码历史清晰易读,掌握branch可以并行工作而不相互干扰,理解merge(包括如何resolve冲突)让你在整合多个人的代码时充满自信。记住:commit要小,分支要有规范命名,merge到main前总要运行测试。