git 大概是程序员电脑里被吐槽最多、却又最离不开的工具。平时不觉得它多重要,等 merge 冲突铺满整屏,或者提交记录乱成一锅粥的时候,才想起当初应该把基本指令用明白。写这篇东西的初衷很简单:我在好几个项目里见过太多人拿着一套固定指令凑合着用,遇到分支、回滚、远程同步这些场景就开始发怵,最后要么翻一堆零散文章,要么用笨办法折腾半天。这次我把日常开发里真正高频的 git 使用指令从头到尾梳理了一遍,原理和踩坑记录一起讲,适合刚接触 git 的入门者,也适合那些已经写了一阵子代码、但还没系统整理过 git 知识的同学。
1. Git 日常使用的核心心智模型
1.1 三个工作区域,别再把 git 当成黑盒
很多教程喜欢直接从命令行讲起,导致新手总在背指令,却不知道每条指令到底动了什么。我自己的经验是,先记住 git 管理代码的三个区域,再去记指令会轻松得多。
- 工作区(working directory):你电脑里能直接看到的文件夹,你在这里写代码、改文件。
- 暂存区(staging area / index):可以理解成一个"打包盒",你把想要提交的文件先扔进这个盒子里,git 会记录文件名和内容快照。
- 本地仓库(local repository):暂存区里的东西被正式归档,生成一条提交记录,形成一个版本节点。
还有一个远程仓库(remote),说白了就是放在服务器上的一份仓库副本,本地仓库通过 push 和 pull 和它同步。
如果你打开一个项目目录,看到git status显示一堆"Changes not staged for commit",就说明工作区有改动还没进暂存区;显示"Changes to be committed",说明已经在打包盒里了,但没有生成提交记录。脑子里有了这个区域模型,绝大多数 git 状态提示都能秒懂。
1.2 为什么非要设计一个暂存区
我刚开始用 git 的时候很困惑:既然工作区改了文件,为什么不能直接提交?非要 add 一下再 commit 一下,多此一举吗?后来在一个写代码经常"改一个功能顺便动了另一个功能"的场景里,我才彻底理解暂存区的价值。
暂存区给了你一个"挑选内容"的机会。你改了 A 模块和 B 模块的代码,但想把它们做成两次逻辑独立的提交,就可以git add只选择 A 模块的文件,先提交一次,再把 B 模块的文件加进去提交第二次。如果没有中间这个打包盒,每次提交就只能把当前所有改动全部打包,提交历史会变得非常粗糙。
用生活化的方式理解:你在厨房做了三道菜,不能因为都做好了就一起倒进一个碗里端上桌。你需要先把第一道菜装进盘子端上去,再回来装第二道菜。暂存区就是那个盘子,add是装盘,commit是端上桌。
2. 入门必用的基础指令,把地基打牢
2.1 初始化项目:init 与 clone 的正确打开方式
创建一个新项目,最常用的是git init。它会在当前目录里生成一个.git文件夹,这个文件夹里保存了仓库的全部元数据。注意:.git文件夹不要手动翻着改,里面全是内部结构,改坏了仓库基本就废了。
cd my-project git init还有一个高频场景是从远程仓库拉取项目。git clone会把远程仓库完整复制到本地,同时自动配置好一个名为origin的远程仓库地址,还会为本地分支建立跟踪关系。
git clone git@github.com:某团队/my-project.git这里有个细节:如果你只需要看代码并不打算改,可以加上--depth 1做浅克隆,只拉最新一次提交,速度会快很多,适合大仓库。
git clone --depth 1 git@github.com:某团队/my-project.git我在实际项目里经常遇到有人纠结"该用 init 还是 clone"。判断标准很简单:本地目录本来就是空的新项目,用 init;要接手别人的已有仓库,用 clone。还有一点,某公司在用自己的代码托管平台时,建议先在平台上建空仓库,再在本地 init 后手动关联远程地址,这样分支保护和权限配置都在平台上控制得更顺手。
2.2 看清当下状态:status 和 diff
如果只允许我教别人两个 git 指令,我会先教git status和git diff。因为绝大多数"我好像把仓库搞乱了"的瞬间,你需要的不是猛操作,而是先搞清楚当前到底处于什么状态。
git statusstatus会告诉你三件事:哪个分支、相比远程领先或落后几个提交、工作区里有哪些改动。它不会告诉你改了什么内容,只想看具体改动时,用git diff。
git diff默认情况下,git diff显示的是工作区相对暂存区的差异,也就是"你后来又改了哪些还没 add 的东西"。想看已经暂存但尚未提交的内容,用git diff --staged(有些版本记成git diff --cached,两者等价)。
git diff --staged我自己的习惯是每准备提交一次改动前,先跑一遍git diff --staged,确认准备打包的内容正是自己脑子里想的那批。很多粗心提交都是因为少看这一步,把调试日志、临时文件甚至敏感信息一起提交上去了。
2.3 把改动提交干净:add 与 commit 的细节
git add是把文件从工作区放进暂存区的动作。最简单粗暴的是全加:
git add .我更推荐按文件加,或者用交互式分段加。按文件加能避免把无关改动卷进来:
git add src/utils/format.js tests/format.test.js交互式分段暂时不用展开,记住有个git add -p的指令就可以了,它会逐个 hunk 问你"要不要把这个片段加进去",适合一个文件里既有功能改动又有格式化改动的情况。这个功能我几乎每天用,属于极易上手的实用技巧。
git commit是真正生成提交记录的指令。务必记住写清楚提交信息,不要偷懒只写git commit -m "update"。好的提交信息应该让别人(以及三个月后的你)一眼看懂这次提交的意图。
git commit -m "refactor: 抽取日期格式化逻辑到独立工具函数"如果提交后发现漏了一个文件,或者提交信息写错了,可以用--amend修正上一次提交,而不用生成一条新的提交记录:
git add src/utils/format.js git commit --amend -m "refactor: 抽取日期格式化逻辑到独立工具函数并补充单元测试"需要特别提醒:--amend会改写提交记录,如果这条提交已经 push 到远程并且其他同事基于它做了工作,尽量不要用 amend,避免给别人带来一堆同步麻烦。
3. 分支与合并,把开发路径理顺
3.1 新建分支与切换分支
分支是 git 最被称赞的设计,也是很多团队协作跑起来的基础。理解分支时,可以把它想象成两个互不干扰的平行世界:你在 main 分支上维护稳定版本,在 feature 分支上开发新功能,两边互不打扰。
查看本地分支:
git branch新建并切换分支:
git switch -c feature/login很多旧教程让你用git checkout -b feature/login,功能一样。不过我现在更推荐git switch,因为checkout承担的职责太杂,容易把概念搞混。switch只负责切换分支,语义更干净。
分支命名也有讲究。我见过有人在分支上写"test"、"bugfix1"、"asdf",过两周根本不知道这个分支对应什么问题。建议用一套容易理解的分词方式,比如feature/用户登录、fix/修复支付超时、chore/升级依赖版本。分隔符用斜杠还可以在平台里自动归组,查看分支列表时非常清晰。
3.2 合并分支前的自我检查
功能开发完,要把 feature 分支合回 main,常用的是git merge。
git switch main git pull origin main git merge feature/login这里有一个非常容易被忽略的脏坑:合并前一定要先同步远程的最新 main 分支。如果你在本地合并前 main 已经落后了,合并出来的结果很可能带着冲突,甚至把别人已经删掉的代码又加回来。所以我的标准流程是:先切到 main,pull 一下,确认本地 main 和远程一致,再执行 merge。
合并完成后,feature 分支通常就被删除:
git branch -d feature/login如果 feature 分支上的某些提交还没合并就删除,会提示你删不掉,这是 git 在保护你。想强行删除可以用-D,但千万别养成就-D的习惯,误删分支的事我见过太多次。
3.3 rebase 的正确使用姿势
git rebase是另一个整理提交历史的工具。它和 merge 的结果很像,但思路完全不同。
merge 是"把两条路径汇合到一点",因此会产生一个额外的合并提交节点;rebase 是把当前分支上的提交"摘下来",重新排列到目标分支的最新提交后面,像是把笔记本从一叠乱纸中抽出来重新整理,最后呈现出一条直线的历史记录。
git switch feature/login git rebase main用 rebase 的好处是提交历史非常干净,每条提交从早到晚排成一条直线,做 code review 时很舒服。但代价是它改写了提交记录的时间线逻辑。这里有一个铁律:绝不 rebase 一个已经推送到远程、并且别人也基于它开发过的分支。rebase 后的提交哈希全都变了,别人再 pull 的时候会遇到一堆莫名其妙的冲突,甚至可能导致好几份错乱副本。
我刚学 rebase 时也犯过这个错,把两个同事的工作搅成一团乱麻,最后被迫一个个 reflog 手工找提交。那一次之后我给自己立了个规矩:只对本地的、还没推送的分支做 rebase,已经推上去的提交用 merge 处理。
4. 远程协作与发布
4.1 远程仓库的基本管理
本地仓库和远程仓库之间的联系,通过一个叫"remote"的配置来管理。查看当前关联了哪些远程仓库:
git remote -v新增远程仓库:
git remote add origin git@github.com:某团队/my-project.git如果项目换了一个托管平台,或者原地址失效了,可以修改关联地址:
git remote set-url origin git@github.com:某团队/my-project.git还有一种常见情况:本地初始化项目后,想把项目推到平台,需要先关联远程再 push。很多新人一上来就 push,结果报"no remote configured"错误。完整的初始提交流程大致是:
git remote add origin 仓库地址 git branch -M main git push -u origin maingit branch -M main是把当前分支重命名为 main,-u是建立上游跟踪关系,这样之后直接输入git push就能推送到对应的远程分支,不需要每次带上远程分支名。
4.2 push、pull、fetch 三兄弟别搞混
远程同步相关的三个指令,很容易混。我做了个表格帮助记忆:
| 指令 | 动作 | 是否合并到本地 | 典型场景 |
|---|---|---|---|
git push | 本地提交推送到远程 | 否 | 把功能推上远程分支 |
git fetch | 从远程拉取最新提交信息 | 否 | 先看一眼远程都有什么新提交,暂时不动本地代码 |
git pull | 从远程拉取并合并 | 是 | 同步远程最新代码到本地分支 |
很多人只知道git pull,却忽略了fetch。其实fetch非常有用:你先拉取远程状态,然后用git diff origin/main看看远程改了哪些内容,确认没问题再决定merge还是rebase。我一般会在大规模远程变动的场景下用 fetch + 手动合并,避免 pull 自动合并带来意外冲突。
git pull默认行为是 fetch + merge,会产生一个合并提交。如果想保持历史线更干净,可以用git pull --rebase,本质上是先把本地未推送的提交暂存起来,拉取远程新提交,再把你自己的提交重放到最新位置。注意:这条指令同样遵守"不要对已推送分支随意使用"的原则,对个人功能分支倒是很合适。
4.3 给版本打标签
当一个版本准备发布时,我习惯用 tag 标记一个里程碑提交。tag 相当于给提交打一个可读的名字,例如 v1.0.0、v1.2.3。
轻量标签直接创建:
git tag v1.0.0附注标签会包含打标签人、时间、注释等元信息,更适合发布版本:
git tag -a v1.0.0 -m "正式发布 1.0.0 版本"查看所有标签:
git tag -l注意:tag 不会通过 push 自动推到远程,需要显式推送:
git push origin v1.0.0或者一次性推送所有标签:
git push --tags我在参与某个前端项目时,项目组就吃过没打 tag 的亏:发布后线上出问题,翻代码库找不到"当时上线的是哪个提交",只能靠时间推断,费了很大劲。从那以后我每次发布必然打带注释的 tag,方便后续回滚定位。
5. 撤销与回滚,把后悔药吃明白
5.1 还没提交时:工作区改动怎么还原
工作中最典型的场景是:改了一堆代码,突然发现自己走错了方向,想把文件恢复到上一次提交的状态。这时用:
git checkout -- src/utils/format.js这条指令的含义是"用暂存区或上一次提交的内容,覆盖工作区文件"。
如果改动的文件已经git add进了暂存区,想撤销暂存状态,但保留工作区改动:
git restore --staged src/utils/format.js新版 git 更推荐用git restore系列指令,语义更清晰。checkout身兼职责太多,容易让人困惑:它既能切分支又能还原文件,我建议普通文件还原用restore,分支切换用switch。
5.2 已经提交但没推送:reset 的三种模式
一旦你生成了一条提交记录,要撤销或重写这条记录,就用git reset。它有三种模式,也是很多新人最容易搞混的地方。
git reset --soft HEAD~1--soft只是把 HEAD 指针回移到上一个提交,工作区和暂存区都不动。相当于"把提交拿下来,但你自己改的内容全部还保留着,而且已经 add 好了"。适合提交信息写错了,想重新提交的场景。
git reset --mixed HEAD~1--mixed是默认模式,它会撤销提交,同时把暂存区清空,但工作区改动保留。适合提交后发现想拆成多个提交的场景。
git reset --hard HEAD~1--hard是最危险的一个,它不仅移动 HEAD,还重置暂存区和工作区。执行完之后,工作区里所有改动直接消失。
我在带新人时反复强调:如果没搞懂三个模式的区别,宁可先用git log查清楚再动,也不要拿--hard乱试。--hard一旦执行,很多改动就再也找不回来了,除非借助 reflog 恢复。
5.3 已经推送:revert 才是正道
如果你已经执行过 push,别人也可能基于这份代码做了工作,这时候再用 reset 就会造成远程和本地历史不一致,非常难处理。正确的做法是git revert。
git revert 8f3a30arevert不是抹掉历史,而是生成一条新的提交,把之前的改动反向执行一遍。这样旧提交依然保留在历史里,但它的效果被新提交抵销了,远程协作不会出现历史重写问题。
这个方法也适用于"已经发布的版本出了问题,我想快速回滚到上一版"的场景。处理方式是在 main 分支上直接 revert 掉那个有问题的提交,然后 push,线上代码就恢复正常了。历史记录里保留了一次"撤销了某某提交"的记录,对后续排查很有帮助。
5.4 stash 临时保存手头工作
还有一种常见情况:你在 feature 分支改了代码,但还没改完,突然要切到另一个分支去修一个紧急 bug。如果直接切换,工作区改动会被带回分支,或者被阻塞在切换操作中。这时用git stash把未提交的改动存起来,把工作区恢复干净,切分支改 bug,然后再回来取出改动。
git stash git switch main git switch feature/login git stash popgit stash相当于"把所有未提交的改动放进一个临时小仓库"。git stash list可以查看有哪些暂存记录。如果要应用某一条而不是最新的一条:
git stash apply stash@{1}stash是我实际使用频率非常高的指令,尤其适合那种"代码写到一半,老板突然让你去查 bug"的场景。需要注意,stash不会保存未跟踪的新文件,除非显式加-u:
git stash -u6. 常见问题与排查技巧实录
6.1 detached HEAD 状态是怎么回事
git status里出现"detached HEAD"时,很多新人有点慌:这感觉像是"头掉了"。
其实它表示 HEAD 没有指向任何一个分支,而是直接指向某个具体的提交。通常是因为你做过类似git checkout <commit>的操作。在这个状态下,你提交的代码会临时处在一个悬空的提交上,没有任何分支引用它。如果此时再切回分支,这些提交可能被 git 当作不可达提交清理掉。
解决办法很简单:如果只是想看看旧代码,看完切换回分支即可。如果发现旧提交上有一段值得保留的工作,立刻创建一个新分支,让分支指向当前悬空前后的位置:
git switch -c temp-branch我见过的典型误操作是:想查看某次历史版本,用了git checkout 9fce2a1,新同学改了半天发现保存不了,最后被强制切走导致工作丢失。遇到这种情况,先冷静,用 reflog 还能找回来一些,详情见 6.3。
6.2 冲突解决的完整流程
冲突是 git 使用中最绕不过去的一道坎。它本质上是两个分支修改了同一处内容,git 不知道应该保留哪一份。一般在 merge 或 rebase 过程中出现。
冲突发生后,被标记为冲突的文件里会出现类似这样的内容:
<<<<<<< HEAD 你当前分支的代码 ======= 另一个分支的代码 >>>>>>> feature/login处理流程并不复杂:
- 直接打开冲突文件,把
<<<<<<<、=======、>>>>>>>这些标记删掉。 - 仔细阅读两边代码,决定保留哪一份,或者手动把两边逻辑融合起来。
- 保存文件后,执行
git add把解决后的文件加入暂存区。 - 继续执行 merge 的收尾动作,比如
git merge --continue或git commit。
我在实际项目中处理冲突的经验是:不要试图"快刀斩乱麻",一定要理解两边代码的意图。有一次同事为了快点解决冲突,直接选了某一侧的版本,结果把对方刚封装的接口调用直接删掉了,合上去后编译失败,大家一起排查了很久。真正高效的冲突解决,是花时间看上下文、跑测试,确认结果是对的再提交。
6.3 误删分支、误改文件后的恢复思路
误操作后最有效的工具是git reflog。它记录了 HEAD 曾经指向过的每一个位置,包括被 reset、checkout、merge 等操作改写之前的状态。
git reflog输出形如:
8f3a30a HEAD@{0}: reset: moving to HEAD~1 5c4e2d1 HEAD@{1}: commit: fix: 修复登录超时问题 1b9d0cf HEAD@{2}: checkout: moving from main to feature/login如果你误删了一个分支,通过 reflog 找到分支最后一次指向的提交哈希,然后重新创建分支:
git branch feature/login 5c4e2d1如果你误执行了git reset --hard,reflog 里还能看到被 reset 前的提交,用同样方式把 HEAD 指回去,就能找回丢失的工作。这个方法我至少救过三四个人的代码,知道的人不是很多。
需要注意:reflog 是本地仓库的日志,不能通过克隆获取。而且它包含的信息也会随着仓库的 gc 清理而消失。所以,出现误操作后越早恢复成功率越高,不要等几天再来处理。
6.4 几条最实用的排查命令
除了上面讲过的git status、git diff、git reflog,我再分享几条调试和排查时特别有用的指令。
查看提交历史,用git log --oneline --graph --all。--oneline折叠成简洁单行,--graph画出分支走向,--all把本地和远程所有分支都展示出来。这个组合是我平时最常见的视图:
git log --oneline --graph --all只看某个人在某段时间提交了哪些内容:
git log --author="某开发者" --since="2024-01-01" --until="2024-06-30" --oneline查看某次提交具体改了哪些文件:
git show 8f3a30a --stat还有一个很浅但很多人没注意的:git status会直接告诉你下一步该执行什么指令。比如提示 "use git restore to discard changes in working directory",如果你实在不知道接下来该干嘛,就跟着 git 的提示走,基本不会出错。
我在团队里常说:git 排查指令不求多,但求用熟。上面这些足够覆盖日常工作中九成以上的问题。遇到没见过的报错,先看提示信息,再思考自己刚才到底对哪个区域做了操作,比顺手乱敲几条指令高效得多。
我个人在实际操作中的体会是:git 指令记住多少不重要,真正重要的是每次敲指令前,脑子里要清楚这条指令作用在哪个区域、会改变什么。理解了工作区、暂存区、本地仓库、远程仓库这四层结构,你就不会再怕"git 又搞出什么奇怪状态"。最后再分享一个很小但很受益的习惯:每天结束工作前,跑一遍git status加git log --oneline -5,花不到一分钟,却能让你对项目变化保持清晰的掌控感。这套方法我在多个项目里用下来,几乎没再为版本管理的事焦虑过。