上一篇已经介绍了 Git 最基础的本地版本管理流程:
修改代码 ↓ git status ↓ git diff ↓ 编译 / 测试 ↓ git add ↓ git commit对于只有一个开发方向的小项目来说,这套流程已经够用了。
但实际开发很快就会遇到新的问题。
例如现在有一个已经可以正常运行的程序:
project接下来想尝试加入一个新功能。
最直接的做法当然是继续修改当前代码。
但新功能可能需要改很多文件,而且暂时还不知道能不能成功。
如果直接在当前代码上修改:
稳定版本 ↓ 修改 ↓ 继续修改 ↓ 功能还没做完 ↓ 程序暂时跑不起来这时候如果突然发现:
原来的稳定版本还需要继续使用怎么办?
当然可以依靠 commit 回退。
但 Git 还有一种更适合这种场景的机制:
branch也就是:
分支分支可以让我们从当前版本“分出去”一条新的开发路线,在不影响稳定代码的情况下开发新功能。
例如:
feature / main ──────────●之后可以变成:
main │ ├── 稳定版本继续保留 │ └── feature │ ├── 修改代码 ├── 测试新方案 └── 完成新功能如果新功能开发成功,就把它合并回 main。
如果失败,也可以直接删除这个分支。
这篇文章就从零开始介绍 Git 中最常用的分支操作:
branch switch merge不涉及 GitHub 和远程仓库,仍然只讨论本地 Git。
1. 为什么需要分支?
先看一个实际场景。
假设有一个 Linux C 项目:
input_project/ ├── main.c ├── input.c ├── input.h └── Makefile当前程序已经可以正常读取输入设备。
版本历史:
82a7f61 Add input device support 4c91e20 Fix read error handling 16af883 Initial project现在准备加入:
select()来实现多路输入监听。
这次修改可能涉及:
main.c input.c input.h而且开发过程中程序可能暂时无法编译。
如果直接在当前代码上修改:
main │ ├── 稳定版本 │ ├── 修改 main.c │ ├── 修改 input.c │ └── 功能开发中……此时 main 分支就不再是稳定状态。
更合理的方式是:
main │ ●────────────── 稳定版本 \ \ select-dev │ ├── 修改代码 ├── 编译 ├── 调试 └── 完成功能新功能全部在:
select-dev分支中完成。
main 不受影响。
等 select 功能确认没有问题以后:
select-dev │ └──────────→ main再合并回去。
这就是分支最核心的用途。
2. branch 到底是什么?
很多初学者第一次接触 Git 分支时,会把它想得比较复杂。
实际上可以先把 branch 理解成:
指向某个 commit 的一条开发路线。
假设现在有三个 commit:
A ↓ B ↓ Cmain 当前指向:
A → B → C ↑ main严格来说应该是:
A → B → C ↑ main这里的:
main本质上就是一个指向 commit C 的名字。
如果创建一个新的分支:
dev那么刚创建的时候:
A → B → C ↑ main devmain 和 dev 都指向同一个 commit。
此时两个分支中的代码完全一样。
接下来切换到 dev,并创建新的 commit:
A → B → C ↑ main \ D ↑ dev这时候两个分支就真正分开了。
main 仍然停留在:
Cdev 已经继续向前开发到:
D这就是所谓的:
分支开发3. 查看当前分支
先进入一个已经初始化过 Git 的项目。
例如:
cd input_project查看当前分支:
git branch可能看到:
* main前面的:
*表示当前所在分支。
也就是说:
* main表示:
当前正在 main 分支上工作。
也可以执行:
git status通常会看到:
On branch main所以查看当前分支,最常用的两个命令就是:
git branch和:
git status4. 创建一个新分支
假设准备开发 select 功能。
可以创建一个分支:
git branch select-dev再查看:
git branch可能看到:
* main select-dev这里要注意:
git branch select-dev只是:
创建了 select-dev 分支。
并没有切换过去。
当前仍然是:
* main也就是说代码还是处于 main 分支。
5. git switch:切换分支
切换到新分支:
git switch select-dev可能会看到:
Switched to branch 'select-dev'再次查看:
git branch现在变成:
main * select-dev说明当前已经进入:
select-dev以后产生的新 commit 都会记录在这个分支上。
6. 创建并切换分支
刚才用了两步:
git branch select-dev git switch select-dev实际开发中可以直接写成:
git switch -c select-dev这里:
-c可以理解为:
create也就是:
创建分支并立即切换过去。
因此以后最常用的方式通常是:
git switch -c 分支名例如:
git switch -c feature/select查看:
git branch可以看到:
main * feature/select7. 为什么很多分支叫 feature/xxx?
分支名字并没有强制格式。
完全可以写:
test或者:
dev但实际项目中一般希望名字能表达用途。
例如:
feature/select表示:
开发 select 功能还可以看到:
feature/v4l2-capture feature/rga-resize feature/video-encode如果是修复 Bug:
fix/read-error fix/memory-leak如果是实验:
experiment/new-filter experiment/three-frame这样看到分支名字就知道这个分支是干什么的。
例如:
main ├── feature/v4l2-capture ├── feature/rga-resize ├── fix/read-error └── experiment/new-filter比:
main ├── test1 ├── test2 ├── aaa └── new明显更容易维护。
8. 在新分支中修改代码
假设当前:
git branch得到:
main * feature/select现在修改:
main.c原来:
#include <stdio.h> int main(void) { printf("Input project\n"); return 0; }修改为:
#include <stdio.h> int main(void) { printf("Input project with select\n"); return 0; }查看:
git status会看到:
modified: main.c查看具体修改:
git diff确认没有问题以后:
git add main.c再检查:
git diff --staged最后提交:
git commit -m "Add select based input monitoring"此时分支历史大概变成:
D ↑ feature/select / A → B → C ↑ mainmain 还是:
Cfeature/select 已经到了:
D9. 切回 main 会发生什么?
现在执行:
git switch mainGit 会把工作区切换回 main 对应的状态。
也就是说:
feature/select中的修改不会出现在 main 中。
例如在 feature/select 里面:
printf("Input project with select\n");切回:
git switch main可能又看到原来的:
printf("Input project\n");这并不是代码丢失了。
而是因为两个分支代表两个不同版本。
可以理解为:
main │ └── 原来的稳定代码 feature/select │ └── 加入 select 的新代码你切换到哪个分支,工作区就显示哪个分支对应的代码。
这也是分支非常有用的地方。
10. 切换分支前为什么最好保持工作区干净?
假设当前正在:
feature/select里面修改代码,但还没有 commit。
执行:
git status看到:
Changes not staged for commit: modified: main.c这时候最好不要急着切换分支。
因为当前还有:
未提交修改Git 有时候允许带着这些修改切换分支。
但如果两个分支对同一个文件存在不同修改,也可能直接拒绝切换。
因此对于刚开始学习 Git 的时候,推荐形成一个简单习惯:
切换分支之前先执行 git status。
比较理想的状态是:
nothing to commit, working tree clean也就是:
working tree clean再执行:
git switch main会更加清楚,也不容易把不同功能的修改混在一起。
11. 查看所有分支
执行:
git branch例如:
* main feature/select feature/epoll说明当前仓库一共有三个本地分支:
main feature/select feature/epoll其中:
main前面有:
*所以当前在 main。
12. merge:把分支合并回来
现在假设:
feature/select已经开发完成,并且:
编译通过 测试通过 功能正常接下来希望把这个功能加入 main。
第一步:
git switch main确认:
git branch看到:
* main feature/select然后执行:
git merge feature/select意思就是:
把 feature/select 中的修改合并到当前分支 main。
注意这个方向。
我们现在是在:
main执行:
git merge feature/select所以相当于:
feature/select ↓ main而不是 main 合并到 feature/select。
13. 一个完整的分支流程
把前面的操作连起来就是:
首先确认当前状态:
git status创建功能分支:
git switch -c feature/select然后开发功能。
开发完成以后:
git status git diff编译:
make测试没有问题以后:
git add . git diff --staged git commit -m "Add select based input monitoring"此时功能已经保存在:
feature/select里面。
然后切回:
git switch main合并:
git merge feature/select最后查看:
git log --oneline整个过程可以画成:
main │ ● │ ├───────────────────────┐ │ │ │ feature/select │ │ │ 修改 │ ↓ │ 测试 │ ↓ │ commit │ │ │◀──────────────────────┘ │ merge │ ↓ main14. 什么是 Fast-forward?
第一次执行 merge 时,很可能看到类似:
Fast-forward例如:
Updating 82a7f61..9c31e42 Fast-forward main.c | 10 ++++++++-- 1 file changedFast-forward 中文通常叫:
快进合并假设最开始:
A → B → C ↑ main ↑ feature/select创建 feature/select 以后,main 一直没有产生新的 commit。
只有 feature/select 向前开发:
A → B → C → D ↑ ↑ main feature/select这时候 main 想合并 feature/select。
实际上 Git 不需要创建复杂的合并结构。
直接把 main 从:
C移动到:
D即可:
A → B → C → D ↑ main ↑ feature/select这就是:
Fast-forward可以简单理解为:
main 没有产生其他修改,所以直接向前移动即可。
对于刚开始学习 Git 来说,知道这个概念即可,不需要专门处理。
15. 合并完成以后删除分支
feature/select 已经开发完成,而且已经合并回 main。
这个分支通常就可以删除。
首先确保当前已经不在 feature/select:
git switch main然后:
git branch -d feature/select其中:
-d表示删除分支。
再查看:
git branch只剩:
* main这里需要注意:
删除分支不等于删除已经合并的代码。
因为 feature/select 中的 commit 已经进入了 main。
例如合并以后:
A → B → C → D ↑ mainfeature/select 只是一个指向 D 的分支名字。
删除:
feature/select以后:
A → B → C → D ↑ maincommit D 仍然存在。
代码当然也仍然存在。
16. 为什么有时候 git branch -d 删除失败?
假设创建:
git switch -c experiment/test然后在里面产生一个 commit。
但还没有合并回 main。
切换:
git switch main然后执行:
git branch -d experiment/testGit 可能拒绝删除。
原因是:
这个分支还有没有合并的 commit。
Git 是在保护你,避免直接丢失这些开发结果。
如果非常确定这个实验分支不要了,可以强制删除:
git branch -D experiment/test注意这里是大写:
-D它表示强制删除。
因此:
git branch -d 分支名比较安全。
而:
git branch -D 分支名要更加谨慎。
刚开始学习时,优先使用:
git branch -d即可。
17. 一个完整的 C 项目实战
下面真正走一次完整流程。
假设现在有:
input_demo/ ├── main.c └── Makefilemain.c:
#include <stdio.h> int main(void) { printf("Linux input demo\n"); return 0; }Makefile:
CC = gcc CFLAGS = -Wall -Wextra TARGET = input_demo all: $(CC) $(CFLAGS) main.c -o $(TARGET) clean: rm -f $(TARGET)首先初始化:
git init -b main加入文件:
git add .检查:
git diff --staged提交:
git commit -m "Initial input demo"此时:
git log --oneline可能看到:
16af883 Initial input demo18. 创建功能分支
准备增加 select 说明。
创建:
git switch -c feature/select查看:
git branch输出:
main * feature/select修改 main.c:
#include <stdio.h> #include <sys/select.h> int main(void) { printf("Linux input demo with select\n"); return 0; }查看:
git status再看修改:
git diff编译:
make如果看到:
gcc -Wall -Wextra main.c -o input_demo并且没有明显警告或错误,再运行:
./input_demo输出:
Linux input demo with select确认正常。
提交:
git add main.c git diff --staged git commit -m "Add select support preparation"查看:
git log --oneline可能得到:
a2137c4 Add select support preparation 16af883 Initial input demo19. 回到 main 看一下
现在:
git switch main查看:
git log --oneline可能只有:
16af883 Initial input demo因为:
a2137c4目前属于:
feature/select再查看 main.c。
会发现还是:
#include <stdio.h> int main(void) { printf("Linux input demo\n"); return 0; }说明 feature/select 中的开发完全没有影响 main。
这就是使用分支开发最直接的意义。
20. 合并功能分支
确认 feature/select 没有问题以后,在 main 中:
git merge feature/select可能看到:
Updating 16af883..a2137c4 Fast-forward再看:
git log --oneline变成:
a2137c4 Add select support preparation 16af883 Initial input demomain.c 也已经变成:
#include <stdio.h> #include <sys/select.h> int main(void) { printf("Linux input demo with select\n"); return 0; }说明:
feature/select中的功能已经进入 main。
最后删除:
git branch -d feature/select完成。
21. 如果两个分支同时开发会怎样?
分支真正有价值的场景并不是只有一条支线。
例如:
main ├── feature/select └── feature/epoll可以从稳定版本分别创建两个分支。
例如:
git switch main git switch -c feature/select开发 select。
完成以后切回:
git switch main还可以创建:
git switch -c feature/epoll专门开发 epoll。
于是两个方案互不影响。
概念上:
feature/select / A → B → C \ feature/epoll这在实验性开发中非常有用。
例如图像算法项目可能同时测试:
main ├── experiment/filter-a ├── experiment/filter-b └── experiment/filter-cA 方案不影响 B。
B 方案不影响 C。
最后哪个方案效果好,再决定哪个合并到 main。
22. 分支特别适合实验代码
例如一个深度学习项目已经有稳定模型:
main准备测试新的网络结构。
完全没有必要直接修改 main。
可以:
git switch -c experiment/new-attention测试注意力模块。
另外再:
git switch main git switch -c experiment/new-loss测试新的 loss。
结构就是:
main ├── experiment/new-attention └── experiment/new-loss即使:
new-attention最后效果不好,也不会影响:
main直接删除实验分支即可。
这也是 Git 在算法开发中非常实用的地方。
23. 分支也适合功能开发
嵌入式项目中同样如此。
例如一个 IPC 项目:
mini_ipc/ ├── src/ ├── include/ ├── CMakeLists.txt └── README.mdmain 保存当前稳定版本。
不同功能可以使用:
main ├── feature/v4l2-capture ├── feature/rga-resize ├── feature/h264-encode └── feature/rtsp-stream开发 V4L2:
git switch -c feature/v4l2-capture开发完成并测试通过以后:
git switch main git merge feature/v4l2-capture然后继续开发 RGA:
git switch -c feature/rga-resize这样项目历史会非常清楚。
24. 一个功能应该一个分支吗?
对于个人项目,不需要把规则搞得特别复杂。
可以先形成一个简单原则:
一个相对独立的功能,可以开一个分支。
例如:
加入摄像头采集可以:
feature/v4l2-capture加入编码:
feature/video-encode修复内存泄漏:
fix/memory-leak尝试新算法:
experiment/new-denoise但是如果只是:
README 改一个错别字或者:
修改一个很小的注释对于个人小项目,没有必要强制每一次都建立新分支。
分支是帮助管理开发的工具,不是为了增加开发流程。
25. main 分支最好保持什么状态?
个人项目中可以建立一个非常简单的习惯:
main = 相对稳定例如:
main │ ├── 可以正常编译 ├── 主要功能可以运行 └── 不放大量做到一半的代码新功能:
feature/xxxBug 修复:
fix/xxx实验:
experiment/xxx完成以后:
测试 ↓ commit ↓ merge main这样 main 通常保持在一个比较稳定的状态。
26. 常见错误:创建分支以后忘记切换
执行:
git branch feature/select然后直接开始修改代码。
但:
git branch显示:
* main feature/select这意味着:
虽然创建了 feature/select,但实际仍然在 main 上修改。
所以创建功能分支时更推荐:
git switch -c feature/select这样创建和切换一次完成。
并且修改代码以前先:
git status确认:
On branch feature/select27. 常见错误:merge 方向弄反
假设目标是:
把 feature/select 合并到 main。
正确做法:
git switch main git merge feature/select可以记成:
先站到“最终想保留代码”的分支,再 merge 另一个分支。
也就是:
我要把 B 合并到 A那么:
git switch A git merge B例如:
feature/select ↓ main就是:
git switch main git merge feature/select28. 常见错误:功能没测试就直接 merge
例如在:
feature/select修改完成以后,立即:
git switch main git merge feature/select不太推荐。
更合理的是先在 feature/select 中完成:
git status ↓ git diff ↓ 编译 ↓ 测试 ↓ git add ↓ git diff --staged ↓ git commit确认功能没有问题以后,再:
git switch main git merge feature/select这样 main 才更容易保持稳定。
29. 常见错误:工作区很乱的时候频繁切换分支
例如:
git status看到:
modified: main.c modified: input.c modified: Makefile而且这些修改都还没有整理。
这时候又:
git switch feature/xxx很容易把自己搞乱。
推荐习惯:
切换分支之前:
git status最好看到:
nothing to commit, working tree clean如果当前工作已经形成一个完整修改,就先 commit。
如果修改还没有完成,又确实必须切换分支,后面还可以学习:
git stash不过 stash 暂时不是这一篇的重点。
30. 常见错误:什么都建一个分支
分支虽然很好用,但也没有必要变成:
feature/change-one-line feature/change-comment feature/test-print feature/change-variable-name对于个人项目来说,这样反而增加管理负担。
比较适合开分支的是:
一个新功能 一个 Bug 修复 一个实验方案 一个较大的重构原则仍然是:
为开发服务,而不是为了使用 Git 而使用 Git。
31. git branch 常用命令整理
查看本地分支:
git branch创建分支:
git branch feature/select删除已经合并的分支:
git branch -d feature/select强制删除:
git branch -D feature/select查看当前分支也可以使用:
git status32. git switch 常用命令整理
切换分支:
git switch main创建并切换:
git switch -c feature/select例如:
git switch -c fix/read-error再切回 main:
git switch main33. git merge 常用方式
假设:
feature/select需要进入:
main执行:
git switch main git merge feature/select然后如果已经不需要这个分支:
git branch -d feature/select可以把它记成:
创建分支 ↓ 开发 ↓ 测试 ↓ commit ↓ 切回 main ↓ merge ↓ 删除功能分支34. 一套推荐的日常分支工作流
假设准备开发:
V4L2 capture先看当前状态:
git status确保:
working tree clean创建分支:
git switch -c feature/v4l2-capture开始开发。
开发完成以后:
git status git diff编译:
make或者:
cmake --build build测试正常以后:
git add . git diff --staged git commit -m "Add V4L2 video capture"再次确认:
git status然后:
git switch main合并:
git merge feature/v4l2-capture最后:
git branch -d feature/v4l2-capture这就是一个非常典型的功能开发流程。
35. 配合上一篇,Git 工作流已经完整很多了
上一篇只使用:
main工作流是:
修改 ↓ status ↓ diff ↓ 测试 ↓ add ↓ diff --staged ↓ commit学习分支以后,可以扩展成:
main ↓ 创建 feature 分支 ↓ 开发 ↓ status ↓ diff ↓ 测试 ↓ add ↓ diff --staged ↓ commit ↓ 切回 main ↓ merge ↓ 删除 feature 分支这已经比较接近真实项目中的 Git 使用方式。
36. 现在最需要掌握哪些命令?
这一篇真正需要记住的命令其实只有几个。
查看分支:
git branch创建并切换:
git switch -c feature/xxx切换:
git switch main合并:
git merge feature/xxx删除:
git branch -d feature/xxx加上上一篇的:
git status git diff git add . git diff --staged git commit git log --oneline已经可以处理很多个人项目。
37. 一个需要提前知道的问题:合并冲突
目前文章中的例子都比较简单。
例如:
main 没有修改 feature/select 有修改这时候 merge 通常非常顺利。
但实际开发中可能出现:
main修改了:
main.c与此同时:
feature/select也修改了 main.c 的同一部分。
例如 main 改成:
printf("Main version\n");而 feature/select 改成:
printf("Select version\n");Git 就可能不知道:
到底应该保留哪一个?
这时候会出现:
merge conflict也就是:
合并冲突Git 不会擅自替你决定,而是要求开发者处理冲突。
这一部分内容比较重要,也值得单独写一篇详细介绍。
所以这一篇先掌握:
branch switch merge下一步再解决:
merge conflict会更加清楚。
38. Git 分支可以怎么理解?
如果只记一个核心概念,可以这样理解:
main是主开发路线。
feature/xxx是一条临时分出去的开发路线。
例如:
feature/select / A ───── B ───── C \ feature/epoll不同方案分别开发。
某个方案确认成功以后:
feature/select │ ↓ main合并进去。
失败的实验:
experiment/test则可以不影响 main,直接删除。
因此分支真正解决的问题是:
同一个项目可以同时维护不同的开发方向,而不会互相干扰。
39. 推荐形成的使用习惯
开始开发一个较完整的新功能之前:
git status确认当前项目干净。
然后:
git switch -c feature/功能名在新分支中开发。
开发过程中正常使用:
git status git diff功能完成:
编译 测试没有问题以后:
git add . git diff --staged git commit -m "描述本次功能"再:
git switch main git merge feature/功能名最后:
git branch -d feature/功能名整个流程可以浓缩成:
main ↓ switch -c ↓ feature ↓ 开发 ↓ 测试 ↓ commit ↓ switch main ↓ merge ↓ main40. 后续还需要学习什么?
到现在为止,我们已经完成了 Git 本地使用中两个非常重要的部分。
第一部分:
工作区 ↓ 暂存区 ↓ 本地仓库掌握:
status diff add commit log restore第二部分:
分支开发掌握:
branch switch merge接下来就可以继续学习:
Git 本地基础 ↓ branch / merge ↓ GitHub ↓ remote ↓ push ↓ clone ↓ pull ↓ 多人协作 ↓ 冲突处理其中下一步最自然的是:
把本地 Git 仓库上传到 GitHub。
到时候就会接触:
git remote git push git clone git pull也会真正理解:
本地仓库 ↕ 远程仓库之间是什么关系。
总结
Git 分支的核心并不复杂。
首先有一个稳定的:
main需要开发新功能时:
git switch -c feature/xxx于是:
main \ feature/xxx在 feature 中完成:
修改 ↓ 测试 ↓ commit功能确认正常后:
git switch main git merge feature/xxx最后删除已经完成使命的分支:
git branch -d feature/xxx所以这一篇最需要记住的工作流就是:
git status ↓ git switch -c feature/xxx ↓ 开发 / 修改代码 ↓ git status ↓ git diff ↓ 编译 / 测试 ↓ git add . ↓ git diff --staged ↓ git commit ↓ git switch main ↓ git merge feature/xxx ↓ git branch -d feature/xxx如果上一篇解决的是:
如何保存代码的不同版本?
那么这一篇解决的就是:
如何同时维护不同的开发路线?
掌握分支以后,Git 就不再只是一个简单的“代码存档工具”,而开始真正成为项目开发中的版本管理工具。
下一篇继续学习 GitHub,把目前只存在本地电脑中的 Git 仓库真正放到远程服务器上。