新同事第一天入职,组长让我抽个空把开发环境给他过一遍。我原本的计划特别朴素:花二十分钟把 Git 的日常链路演示一遍,能 clone、add、commit、push、pull,先跑起来别出岔子就行。结果这位新同学第一句“Git 和 SVN 到底差在哪”就把我拽进了深水区,二十分钟硬是讲成了一个半小时,最后还顺手给组里沉淀了一份内部速查手册。
后来我琢磨了一下,这还真不是他基础差。绝大多数新人接触 Git 都会被同一个东西卡脖子:一上来就背命令,却不知道每一条命令背后到底发生了什么。命令背熟了,遇到合并冲突、误操作、分支错乱照样两眼一抹黑。所以这篇东西我不打算写成命令大全,而是把这次“即兴培训”里最值钱的几块沉淀下来——先建正确的 Git 心智模型,再讲日常命令怎么落手,然后拆分支策略、手把手解一次冲突,最后给出一整套事故现场的恢复方案。不管是带新人的老员工,还是刚入职正被 Git 反复折腾的同学,应该都能从里面找到可以直接拿去用的东西。
1. 为什么我给新人讲 Git,先花半小时讲这三个关键概念
我坚持认为,新手学 Git 的第一个障碍不是命令,而是脑子里的模型不对。模型对了,命令看一眼就能记住;模型不对,背一百条命令也是散装知识,错一次就崩盘。
1.1 先把“网盘思维”纠正成“快照思维”
很多新人会把 Git 理解成“带历史版本的网盘”:反正能传、能存、能拉回来。这个说法不能算全错,但它会埋不少暗坑。你要是真把它当网盘用,第一次遇到冲突或者提交被覆盖的时候,根本不知道去哪找线索。
我当时是这么给我那位新同事解释的:网盘同步的是“文件的最新状态”,而 Git 记录的是“项目在每个提交时刻的完整快照”。你可以把每次 commit 想象成给整个项目拍一张照片,照片里不止有文件内容,还有拍摄人、拍摄时间和拍摄理由。想回到任何时候,就拿出来对应那张照片看一眼,项目源代码分毫不差地恢复成当时的样子。
这个认知是所有操作的根。理解了“快照”,你才会明白为什么 Git 几乎不会丢数据——每次提交都被完整保存在本地仓库里,只要你没有主动清理,历史上做过的所有动作都还在,误删了分支都有机会捞回来。我后边将讲的 reflog 恢复,就是建立在这个基础上的。
1.2 工作区、暂存区、本地仓库、远程仓库到底各管什么
我给新人画过一张特别土的图,但效果好得出奇。把项目历程想象成一条流水线:
- 工作区:就是你的草稿本,所有正在改的代码躺在这里。
- 暂存区:一个检视台。草稿本上有十处改动,你挑出要定稿的三处,先放进检视台。
- 本地仓库:归档柜。放进来的提交就是一张张已经装订好的档案,随时可取。
- 远程仓库:团队公用的档案室。别人可以从这里借阅档案,你也可以把本地归档推上去共享。
对应到 Git 的状态,一个文件会经历四种身份:未跟踪、已修改、已暂存、已提交。新手最容易不耐烦的就是“我都改完了,为什么不能直接保存,还要先 add 再 commit?”答案就是暂存区给了你一个“选择性提交”的能力。很多时候你在一个文件里同时改了正经功能和调试日志,没有暂存区的话,这两类修改就只能捆在一起提交,将来想单独回溯其中一部分就特别痛苦。能力越大责任越大,这一步真不能省。
提示:让新人每天开工第一件事就是敲一遍
git status,看看当前在哪个分支、工作区有没有遗留的修改。这个习惯能避免大半“我明明没动这个文件,它怎么变了”的灵异事件。
1.3 分支是便宜的平行世界,不是危险的玩具
快照模型理解之后,分支就顺理成章了。每一个分支本质上是一条独立的快照时间线,切换分支就是让工作区恢复到另一条时间线上的某一个状态。
我给新同事打了个比方:主分支是团队的“正史”,大家约定俗成的稳定版本都记录在这条线上。功能分支就是平行宇宙——你在里面怎么折腾都不影响正史,折腾成功了再合并回来,折腾失败了直接丢弃,一切当没发生过。
新人的观念误区是觉得“分支很重、很危险”。实际上 Git 的分支就是一个指向某次提交的指针,创建分支只是多写了一个引用,几乎不占任何成本。多开分支完全不心疼,真正需要警惕的只有一件事:不要把别人正在用的共享分支乱改历史。这个原则后边讲 rebase 的时候会再详细说。
2. 新人第二天就能上手的 8 条日常命令
概念讲完,就该动手了。我的原则是“少而精”:先让他把一条主链路跑顺,再逐步扩展。链路是:克隆 → 查看状态 → 暂存 → 提交 → 推送 → 拉取 → 看历史。
2.1 克隆项目:用 git clone 而不是下载 ZIP
新同事来了,第一件事永远是拿代码。很多人习惯去网页上点“下载 ZIP”,我明确反对。ZIP 下载下来的只是一个目录快照,没有历史记录,没有分支信息,它和真正的“把这个仓库拉到本地”是两回事。
正确的是用 clone,它会把你需要的完整历史都搬到本地:
git clone git@example.com:team/project.git cd project克隆完成后可以看看本地生成了哪些分支:
git branch -a如果仓库特别大,我只想快速看代码的时候会用--depth 1做浅克隆,只拉最新一版,省时间也省磁盘。但浅克隆对日常开发不友好,没有历史就没法追溯问题,所以我只把这个当“体验模式”推荐,正经干活还是老老实实全量克隆。
2.2 提交三连:add、commit、push 的正确用法
这是全组使用频率最高的一串命令,值得讲细一点。先看最基础的三步:
git add . # 把改动加入暂存区 git commit -m "feat: 添加支付回调处理" # 提交 git push # 推送到远程对新人我要强调两点。第一,git add不建议无脑git add .,除非你非常确定当前改动都是干净的。我习惯是先用git status和git diff看清楚改了啥,再决定怎么加。需要一批一批提交的时候,git add -p可以进入交互模式,逐个 hunk 决定要不要暂存,配合暂存区的存在意义,非常好用。
第二,提交信息是写给未来的自己和其他同事看的。我要求组里的信息格式是“类型: 一句话描述”,比如fix: 修复登录态过期问题、refactor: 抽取通用分页组件。这样看历史的时候,扫一眼就明白每个提交干了什么。每次提交只干一件事,尽量做小步提交,这是所有 Git 老手都会强调的习惯。小提交让回滚更精准,也让冲突定位更容易。
说到git push,首次推送到一个新分支时,我让新人加-u参数:
git push -u origin feature/payment-u全称是--set-upstream,它的作用是让本地分支和远程分支建立跟踪关系。以后在这个分支上直接敲git push、git pull就够了,不用每次带一大串参数。
2.3 pull 的两种姿势,以及我为什么推荐 --rebase
git pull其实是两步动作的合成:先git fetch把远程的最新提交拉下来,再做一次合并把远程提交接入当前分支。问题就出在这个“合并”上:默认策略会生成一个 merge commit,时间长了你可能在本地分支上看到一堆“Merge remote-tracking branch”之类的噪音节点。
我给新同事推荐的姿势是:在自己私有分支上拉取时加--rebase。
git pull --rebaserebase 的意思是:把你本地还没推送的提交先摘下来,等远程的新提交回来后,再一个一个接上去。效果就是历史看起来像一条直线,不会被莫名多出来的合并节点打乱。注意,这样做的前提是你的工作区干净,有未提交的修改会导致 rebase 无法进行,需要先提交或暂存。
那git pull的默认合并在什么时候用?在共享分支上,比如main分支,我更倾向于让它生成 merge commit,因为它忠实记录了“我和别人曾经并行修改过”这个事实。总结成一句:
| 场景 | 推荐姿势 | 理由 |
|---|---|---|
| 私有功能分支 | git pull --rebase | 历史干净、线性,方便阅读 |
| 共享主分支 | git pull(默认合并) | 保留并行开发痕迹,少动别人历史 |
2.4 两条“看东西”的命令:status 和 log
很多新人不知道 Git 自带一套挺友好的信息查询系统。git status告诉你“现在处于什么状态”,git log告诉你“历史发生了什么”。日常就够用的两条:
git status -sb git log --oneline --graph --decorate -10-sb是精简模式:-s输出紧凑的改动列表,-b显示当前分支以及和远程的领先/落后关系。一眼就能看出本地是不是有没推送的提交。git log加--graph会用字符画出分支合并路线,配合--oneline每条提交只占一行,非常直观。我给新人建议直接 alias 成短命令:
git config --global alias.st "status -sb" git config --global alias.lg "log --oneline --graph --decorate --all"以后敲git st、git lg就能更快地进入状态。这俩 alias 几乎是我安利过最多人的两条。
3. 分支策略这样讲,新同事才敢放心开分支
3.1 为什么主分支不是用来直接写代码的
我带过的团队里,凡是允许直接在main上写代码的,早晚都会闹一次“线上炸了但没人知道是哪次提交搞的”事故。主分支承担的是团队交付物的稳定基线,它是所有人的共同底座,在上面直接提交相当于把入口敞开,任何人的半成品都会瞬间暴露给全组。
所以我在组里立了一条最简单但也最有效的规定:main永远保持可发布状态,任何开发都从main拉出新分支,绝不在主分支上直接写业务。新同事一开始觉得麻烦,但经历过一次“一个坏提交让全组都拉不下来代码”的现场后,就都服气了。
3.2 一套可以照抄的功能分支操作流
我推荐给新人的是一套朴素的分支命名和流转流程,没有复杂的工作流概念,小团队和个人项目都够用。分支命名按用途区分:
| 分支前缀 | 用途 | 示例 |
|---|---|---|
feature/ | 新功能 | feature/payment |
bugfix/ | 缺陷修复 | bugfix/login-expire |
chore/ | 杂务、重构、依赖升级 | chore/upgrade-utils |
整套操作流程如下:
# 1. 先把主分支更新到最新 git switch main git pull # 2. 从最新 main 切出新分支 git switch -c feature/payment # 3. 开发过程中小步提交 git add -p git commit -m "feat: 接入支付回调" # 4. 推送到远程并建立跟踪 git push -u origin feature/payment # 5. 功能完成后合并回 main git switch main git pull git merge --no-ff feature/payment这里面的--no-ff值得单独解释。即使功能分支上只有一个提交,--no-ff也会强制生成一个合并节点,把这个功能“装入”主分支的动作单独记录一笔。这么做的好处是将来一眼能看到“这个功能是哪个合并点进主线”,回滚也方便——直接 revert 那个合并节点就行。相比之下,快进合并虽然在历史上没有岔路,但功能的边界会模糊。
合并完成后,顺手删除已经没用的分支:
git branch -d feature/payment git push origin --delete feature/payment本地删除用-d,如果分支没被合并会提示并阻止删除,这是 Git 在保护你。远程的分支删掉是避免分支越积越多,看着整洁,也不容易混淆。
3.3 让新同事亲眼看到分支的全家福
讲分支只靠抽象概念是记不牢的,我习惯让新人跑一遍git lg看看真实的提交图。类似这样的输出:
* 9d8f2a1 (HEAD -> feature/payment) Merge main into feature/payment |\ | * c4e9b03 (main) fix: 调整首页样式 * | 72fa01c feat: 接入支付渠道回调 * | 5b2e09d feat: 新增支付订单表 |/ * a1d3f77 feat: 初始化支付模块看到这张图之后,新人对“分支是时间线的分叉与汇合”这个感知会立刻清晰起来。分叉的部分是并行开发,汇合的点就是一次合并。我一直觉得,读提交图的能力是 Git 学习者从入门到进阶的一道分水岭。
4. 合并冲突手把手拆解:一次演示胜过十遍口述
4.1 冲突不是事故,而是 Git 把决定权交还给你
新人第一次看到“CONFLICT”这个单词时,手心都会冒汗。我得先给他们定心丸:冲突不是坏事,恰恰说明 Git 发现了它无法独自裁决的地方。它再聪明也是个程序,当两个分支在同一行代码上有不同修改,它不知道该听谁的,只能把你请出来做决定。
一个特别贴切的类比:你让两个同事分别在同一页 PPT 上修改,改完合到一起,编辑不可能同时保留两个版本,他必须找你确认到底以谁为准。Git 的冲突提示,就是那个编辑在旁边举着牌子等你拍板。
4.2 手工解决一次冲突的完整流程
我给新人演示冲突解决时,习惯造一个很小的场景:两个分支都改了同一个文件,然后合并。冲突发生后,先看git status,Git 会标出哪些文件处于未合并状态:
git merge feature/payment # Auto-merging payment.go # CONFLICT (content): Merge conflict in payment.go打开那个文件,会看到冲突标记:
<<<<<<< HEAD currentVersion := "v1" ======= currentVersion := "v2" >>>>>>> feature/payment这个标记的意思是:<<<<<<< HEAD到=======之间是当前分支的版本,=======到>>>>>>> feature/payment之间是合并进来那个分支的版本。你需要做的是选择保留哪个、删掉哪个,或者两个都要再组合一下,然后把三组标记行全部删掉。
解决完毕之后,把文件标记为已解决并提交:
git add payment.go git commit这里的git commit不需要带-m,因为 Git 会帮你生成一条合并提交信息,直接确认即可。
注意:如果解决到一半发现自己处理不了,或者改乱了,随时可以用
git merge --abort回到合并之前的状态。这不是认怂,是止损。
另外有一种冲突是文本合并解决不了的——二进制文件冲突,比如图片、打包产物。Git 没法帮你逐行合并,你只能二选一。这时可以用:
git checkout --ours path/to/file # 保留当前分支版本 git checkout --theirs path/to/file # 保留合并进来的版本但要提醒一句,在合并过程中“ours”指当前所在分支,“theirs”指被合并进来的分支,方向和平时直觉可能正好相反。用之前先想清楚自己在哪。
4.3 merge 和 rebase 怎么选:一张表说清楚
如果说矛盾的命令给新手恐惧,那 merge 和 rebase 的选择给新手的多半是困惑。我在组里要求新人记住下面这张表:
| 场景 | 用什么 | 为什么 |
|---|---|---|
| 把功能分支合并回主分支 | merge | 保留一次合并节点,记录功能进入主线的边界 |
| 同步主分支新代码到自己的功能分支 | rebase | 让功能分支历史线性,减少难懂的合并节点 |
| 已经推送且可能被其他人使用的分支 | 一律 merge | rebase 会重写历史,让别人的本地记录失配 |
最后一条是铁律:绝不 rebase 一个已经推送到远程、并且可能被别人拉取过的共享分支。rebase 会改写提交的哈希,你本地重写一次,队友那边原有的引用就对不上了,再次拉取时 Git 会误以为出现了分叉,然后制造出一堆重复提交或者让队友需要手工处理,场面很难看。私有分支反正只有你自己在用,随便 rebase,追求历史干净没问题。
5. Git 事故现场:新人必会的五种恢复操作
新人出事故不可怕,可怕的是不知道有多少种后悔药。我特意给新同事准备了一场“模拟事故演练”,在一个练习仓库里故意制造各种错误,然后一个个救回来。这里挑五个最典型的分享给大家。
5.1 提交信息写错了、文件漏传了:用 commit --amend 补救
刚提交完发现信息拼错了,或者忘了把一个文件加进来,这种小事故几乎每个人都遇过。如果提交还没推送出去,处理很简单:
git add forgot-file.go git commit --amend--amend的含义是“用一个新的提交替换当前最后一个提交”,它会把暂存区里还没提交的改动一并合进去,同时给你机会修改提交信息。注意,amend 本质上也是重写历史,所以它同样只适用于尚未推送的提交。如果已经推送了,需要非常谨慎,不建议新人碰。
5.2 提交完了想反悔:reset 三兄弟怎么选
git reset会根据参数的不同,把当前分支的 HEAD 挪到指定提交,并决定要不要同步重置暂存区和工作区:
| 参数 | 影响范围 | 使用场景 |
|---|---|---|
--soft | 只移动 HEAD,暂存区和工作区都保留 | 提交信息想重写、提交粒度想重分 |
--mixed(默认) | 移动 HEAD 并清空暂存区,工作区改动保留 | 想取消暂存但不丢代码 |
--hard | HEAD、暂存区、工作区全部重置 | 本地改乱且确定不想要了 |
举例,你发现刚才提交里混进了一段调试代码,想重新组织提交:
git reset --soft HEAD~1HEAD 回到上一个提交,本地代码还在,暂存区也还保留,你可以调整后再提交。而--hard是最危险的,它会连工作区的未提交修改一起删掉。用完git reset --hard发现代码没了,唯一救星就是后面要说的 reflog。
还需要强调:已经推送出去的提交,千万不能用 reset 然后强推的方式抹除,尤其不在共享分支上这么干。这是团队协作的大忌,宁可新增一个提交去把错误翻篇。
5.3 已经推送到远程的坏提交:用 revert 翻篇
遇到已经推送的坏提交,正确姿势是git revert。它是“反着应用一次修改”,生成一个新的提交,把目标提交造成的影响抵消掉,而不去改动历史:
git revert 8f3a2e1这种方式的优点是对共享分支非常安全,因为它不重写任何已有提交,其他同事拉取时只需要正常合并。坏提交本身留在历史上,想看还能看到,但它造成的效果已经被新提交抵消。这是处理远程坏提交的首选,没有之一。
5.4 分支被删、reset 过头:reflog 就是后悔药
分支删了、reset 错了,只要提交对象还在,就没到绝境。Git 有一个隐藏的日志叫 reflog,它记录了 HEAD 指针每一次移动的历史。哪怕是“删掉一个分支”这种操作,分支指向的提交对象也会在 reflog 里留痕:
git reflog # 8f3a2e1 HEAD@{0}: reset: moving to HEAD~2 # c4e9b03 HEAD@{1}: commit: feat: 新增支付模块 # a1d3f77 HEAD@{2}: branch: Created from main从输出里找到你想要回到的提交哈希,然后基于它新建一条分支,一切就能救回来:
git switch -c recover-branch c4e9b03我给新同事的叮嘱是:reflog 是比备份更可靠的存在,只要不跑git gc主动清理,默认情况下它会保留一段时间。所以遇到误操作,第一反应不是慌张,而是git reflog。
5.5 改到一半要切分支:stash 临时藏起现场
最常发生的事故不是提交错了,而是“我改到一半,突然要去修个紧急 bug”。不能带着未提交的修改直接切分支,因为 Git 会提示工作区不干净。这时候用 stash 把现场打包存起来:
git stash git switch main # 紧急修复完成后再回来 git switch feature/payment git stash popgit stash把当前未提交的修改保存到一个暂存栈里,工作区恢复干净。git stash pop会把栈顶的修改重新应用回工作区。如果同时存了多个现场,可以用git stash list查看,用git stash apply stash@{1}指定应用某一个。这个命令不复杂,但关键时刻能省下很多“完了我改了半天的代码要被覆盖了”的绝望。
6. 让新人从“抄命令”进化到“独立排障”
6.1 第一课:学会读懂 Git 给你的提示
我发现很多新人遇到问题第一反应是关掉终端,切换软件重来。我不让他们这么做。Git 最友好的一面就是它非常话痨,几乎每个错误提示都在告诉你下一步该怎么走。
举个例子,新手第一天就会遇到 push 被拒:
To github.com:example/project.git ! [rejected] main -> main (fetch first) error: failed to push some refs hint: Updates were rejected because the remote contains work hint: you do not have locally.翻译成人话就是:远程仓库有了你没有的提交,你的推送会被覆盖别人的内容,所以被拒绝了。解决办法其实就是提示里隐含的——先把远程的拉下来合并,再推送。我让新人养成的习惯是:报错之后先读三遍屏幕上的英文提示,你至少能获取 80% 的解决线索。
6.2 建一个随便折腾的本地练习场
我强烈建议新人在自己的电脑上建一个专门拿来“搞破坏”的练习仓库:
mkdir git-playground && cd git-playground git init echo "hello" > test.txt git add . && git commit -m "first"在这个目录里,随便 rebase、reset、删分支、制造冲突,什么姿势都可以试。反正是一堆废代码,没有任何心理负担。我那位新同事就是在这样的沙盒里反复折腾了两天,把冲突标记、reflog、stash 都亲手玩了一遍,之后就再也不怕真实的项目操作了。这个方法比任何教程都管用,因为肌肉记忆只能靠动手建立。
6.3 我的最后一条建议:维护一份“事故台账”
我带过不少新人,发现一个很有意思的规律:同一个错误,几乎每个人都会踩一遍。所以我在组里给新人的最后一个任务是建一个文档,名字就叫“我的 Git 事故台账”。格式很简单,三列:现象、原因、解决办法。
比如记一条:“push 被拒 → 远程有新提交我没拉 → 先 git pull --rebase 再 push”。这招看起来特别土,但效果奇好。新人遇到新问题,先查台账,查不到再问人,解决后补一笔。三个月下来,那份文档就是他自己最珍贵的一手经验集,比任何网上教程都贴合他的实际工作场景。
我自己的 Git 水平也远说不上顶尖,但这么多年下来最大的体会是:只要模型正确、恢复手段熟练,Git 就是那个“怎么折腾都摔不死”的工具。新同事从“怕 Git”到“把 Git 当朋友”,往往也就是一周的事。关键是别让他一开始就淹没在命令的海洋里,先把主干打通,剩下的交给时间去积累。