找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2275

积分

0

好友

304

主题
发表于 2 小时前 | 查看: 1| 回复: 0

写在前面

这是一篇 Git 经验分享帖。起因是刚结束的一段项目经历里,涉及多人协作,我终于有机会把 Git 放进真实开发流程里完整跑一遍。

本科期间我自学过一些 Git 入门,自己的博客管理也一直基于 Git。但那种单人使用更像“小打小闹”:很多多人协作才会遇到的分支推进、冲突处理、远程同步等问题,根本碰不到。直到这次进入实际工况,才算把这套工具真正摸熟。

网上关于 Git 的内容不少,但不少文章停留在命令罗列,脱离开发现场;“会敲命令”和“能把协作流程跑顺”之间差得很远。本文更专注于开发实践:讲清楚一些操作背后的本质与适用场景,适合已经具备一定 Git 基础的读者。


Git:概念理解

理解 commit

Git 提交(commit,后文用 commit 替代“提交”)的本质是“修改”,而不是“快照”。

快照更像一张照片:记录某一时刻工作区的全量状态;而 commit 更像“补丁”:记录当前 commit 相对上一个 commit(父节点)的更改内容。

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 1

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 2

这种“记录修改”的方式有什么实际价值?

举个很常见的协作场景:当前我有 A、B 两个分支并行开发两个不同功能。我在 A 分支新写了一个函数 f,刚好能被 B 分支复用。这时不需要把 A 分支整套代码搬过去,只要把“添加函数 f”这一份修改在 A 上提交,然后把该提交挪到(cherry-pick)B 分支,就能完成迁移。

从 B 分支的视角,它拿到的不是 A 的全部内容,而是:“我也需要在这里添加函数 f”。于是同样的功能可以在不同基底的代码上被正确添加。

因此,理解 commit 记录的是“修改”而不是“快照”,是熟练使用 Git 的前提。


理解 branch

“多开分支,早开分支”是很多 Git 教程都会提到的技巧。但更重要的是:你为什么要开分支?

更合理的逻辑是:当我要处理一个新功能 / 修复 bug 时,我担心新改动会破坏主分支上已有的稳定功能,于是新开一个分支管理改动。这样新功能都在分支上迭代;等改完并验证通过,再由主分支把这个分支的功能“拿走”(merge),也就是合并。

分支命名也值得规范化。我现在更习惯用“文件夹”式命名:

  • 功能开发:以 feat/ 开头,后接具体功能名
  • bug 修复:以 fix/ 开头,后接具体 bug 命名

例如:feat/dyn 表示动力学功能开发,fix/fig 表示画图功能的 bug 修复。

需要注意:Git 的引用存储机制是“松散引用”,因此不支持 featfeat/xxx 同时存在,会出现“文件-目录同名冲突”。


和 branch 紧密相关的还有 HEAD 指针。需要明确两点:

  • branch 的本质也是一个指针:指向该分支的最新提交
  • HEAD 指针指明“你当前所处的位置”

当我们处于某个分支上时,HEAD 的指向是 branch 指针(如果不太直观,配合图示理解更快)。

下图展示的是:当前处于 main 分支,同时存在一个 feat/A 的功能分支。

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 3


一个容易让人困惑的问题是:如果 HEAD 不再指向 branch 指针,而是直接指向某个具体 commit,会发生什么?

这就是 detached HEAD mode。在这种状态下:

  • 你创建的提交无法被 branch 指针跟进,容易被 Git 的机制“抛弃”
  • 当前无法把分支 push 到远程仓库
    (除非你在当前 commit 上新建分支,把它接住)

图示如下:

下图展示的是:detached HEAD mode,此时 HEAD 指针没有指向分支,而是指向一个具体提交。

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 4

除了上述影响外,detached HEAD mode 并没有别的“额外惩罚”。实际中它常用于追溯历史提交、查看历史状态;或者你也可以停在某个历史提交上,再从这里创建一个新分支继续试验。


理解 merge

很多人对 merge 的直觉是“把两个分支合在一起”。但有一个很容易被忽略的关键点:merge 是有方向的

假设有两个分支:用于新功能开发的 A 分支,以及 main 主分支。A 分支已经开发完毕,有

$$n_A$$
个提交;与此同时 main 在这段时间也合入了其他功能,有了新增提交。

执行 merge 后,Git 提交树会创建一个新的 commit:它以两个分支的最新提交作为父节点,并同样记录“修改”信息。

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 5

关键点就在这里:merge 的方向决定了新 commit 记录的修改内容是什么

  • 如果在 main 分支上执行 git merge A
    含义是把 A 的修改拿到 main
    新 commit 会存储“A 比 main 多了哪些修改”;merge 后 HEAD 停留在 main,新 commit 也属于 main

  • 如果在 A 分支上执行 git merge main
    含义是把 main 的修改拿到 A 上
    新 commit 会存储“main 比 A 多了哪些修改”;merge 后 HEAD 停留在 A,新 commit 属于 A。

那在 Git 提交树上怎么区分 merge 的方向?看两点就够了:

  1. 新提交最终落在哪个分支上
  2. 或者查看该提交的注释信息

merge 具体怎么“合”?本质是把所有修改做加法。因为 Git 的提交记录的是修改,所以围绕 Git 的操作,其实都是在对“修改”做处理:计算两边修改之和,然后按 merge 方向把修改应用到目标分支上;若出现冲突,再交给人手动处理。


理解冲突

VSCode 提供了比较顺手的冲突处理体验:在左侧版本管理里,带感叹号的文件通常表示存在冲突;点进去后会给出多种处理选项(保留当前、更改来源、同时保留并手工编辑等)。

冲突一般怎么来的?本质原因很简单:两个分支同时改了同一段代码,Git 无法自动判断“到底保留哪份修改”,于是把选择权交给你。

(VSCode 的 GUI 中会用感叹号标记冲突。)

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 6


Git:命令细节

畅游提交树

Git 的基础能力之一,是能在提交树上熟练移动。

一般来说,大多数“切换当前位置”的操作都可以通过 checkout 完成,比如:

git checkout c3efa5 # 切换到某历史提交
git checkout -b feat/A # 切换并新建 feat/A 分支
git checkout feat/B # 切换至分支 feat/B

上面这些命令在日常版本管理里出现频率非常高。


理解暂存区

创建一个提交一般是“两步走”:

git add -A # 添加所有文件至暂存区
git commit -m "comment" # 提交!

很多人第一次认真用暂存区时都会疑惑:暂存区到底有什么用?为什么不能直接 commit,非得 add 一步?

暂存区的价值在于:它允许你把“当前阶段已经确认的改动”先锁定下来,让 Git 开始跟踪;同时你还能继续在工作区做新的改动,并把两批改动拆开管理。

它适合什么场景?比如你完成了一小段功能开发,这些改动“基本靠谱”,但又不足以单独成一个提交;同时你担心后续继续写会把当前状态冲乱,导致不好回退。此时可以先 add 进暂存区,后续再继续写,改动就会自然分层。

见图:

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 7

如图所示,我在 index.md 里做了两次修改:一次在第 11 行,一次在第 13 行。我在添加第 11 行之后就把 index.md add 进暂存区,所以之后的第 13 行修改会留在暂存区外。这样你就能有选择地对两份修改分别做“回退”等操作。


理解远程仓库

GitHub 提供仓库托管能力,存储在 GitHub 上的那份代码,就是远程仓库的代码。GitHub 上的 repo 即远程仓库。

和远程仓库交互最核心的是两个动作:

  • push:把本地提交同步到远程仓库
  • pull:把远程提交拉取到本地

当然还有 fetch 等命令,但在我的日常里使用频率相对低一些。

需要留意的点主要有这些:

1)远程分支在本地通常显示为:远程仓库名/分支名
一般远程仓库名叫 origin,所以远程主分支常见为:origin/main

2)有几类情况处理起来会比较棘手。

情况 1:本地有改动,push 时发现远程已更新

如果本地已经做了更改,但 push 时发现远程出现新提交,Git 会阻止你 push,要求先 pull。但本地有未处理的改动又导致你无法直接 pull。此时 stash 就很关键。

相关指令如下:

git stash # 把当前更改均打包
git stash pop # 应用 stash@{0} 中的更改,并且丢弃 stash@{0}
git stash apply # 只应用,不丢弃
git stash pop stash@{*} # 选择处理哪个 stash 包,如最后一个参数不写,缺省为 stash@{0}
git stash apply stash@{*} # 说明同 pop

git stash 的意义是把当前更改打包,给 pull“让路”。面对该情况,可以这样做:

git stash # 把当前更改均打包
git pull origin master # pull origin 远程仓库的 master 分支
git stash pop # 把刚才打包的更改重新应用

需要注意:pop 之后可能会与远程改动产生冲突,此时需要手动解决,并提交一个 merge commit。

情况 2:本地已有多个提交,远程也被别人推进了多个提交

更麻烦的是:本地已经有几个提交,同时远程也被其他人推进了几个 commit。这时你再 pull,Git 也可能难以自动决定如何处理历史。

(原文此处配图与整理的方案如下。)

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 8


有关 rebase

在我的使用过程中,还没有遇到需要频繁使用 rebase 的场景。相比之下,我更常用的是 merge 和 cherry-pick,所以这里暂时不展开 rebase 的经验。


Git:工具使用(VSCode)

Git 的管理如果纯粹依靠命令行,体验会比较痛苦(有点像手动配 vim 编辑器……)。

VSCode 本身就带了不错的 Git 版本管理工具。

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 9

如前文所示,左侧第三行工具栏提供了更直观的 Git 管理能力。相较于纯命令行,它至少有这些优势:

  1. 直观看见有哪些更改
  2. 能有选择地把相应文件添加进暂存区
  3. 能有选择地回退部分文件更改
  4. 方便处理合并冲突
  5. (我个人最喜欢的)commit 注释区更适合写多行注释

此外,VSCode 还有 Git Graph 插件(插件市场安装即可),能更清楚地展示当前 Git 提交树与各分支的推进情况。

Git多人协作进阶:commit本质、merge方向与stash实战 - 图片 - 10

使用 Git Graph 时也有几点需要注意:

  1. Git Graph 加粗的分支即为当前 HEAD 指针所在位置
  2. Git Graph 并不会主动更新远程仓库的提交进度
  3. Git Graph 会尽可能把当前所在分支放在最左侧,所以你看到的视图可能会动态变化

如果后续有时间,我可能会补一篇更基础的 Git 内容。这里也放一个我之前整理的链接,感兴趣可以跳转: https://dream-oyh.pages.dev/code/git.html

想继续扩展 Git 与协作流程(如分支规范、提交规范、PR 流程等)的话,也可以在 云栈社区开源实战 板块里看看大家的实战讨论。




上一篇:pad.ws 开源:白板+云端IDE一体化自托管指南
下一篇:Go抽象设计指南:如何避免不恰当封装
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-18 21:32 , Processed in 0.226916 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表