用了这么多年 Git,我发现一个规律:真正影响日常开发效率的,不是那些花里胡哨的高级技巧,而是最常用那几十个命令的熟练程度。很多人卡住,不是不会用 Git,而是对几个核心命令的理解停留在“背下来”的层面,一遇到分支合并、撤销回滚就头皮发麻。这篇博文不打算给你堆一份命令大全,而是从实际场景出发,把 Git 日常使用中最绕不开的命令讲透,讲清楚每个命令背后的逻辑和适用场景。不管你是刚接触 Git 的新手,还是用了很久但总感觉差点意思的老手,这份总结应该都能帮你把脑子里那团浆糊理顺。
1. 从仓库初始化开始:两种姿势对应两种需求
1.1 git init 与 git clone,到底该怎么选
很多新人容易在这两个命令上犯迷糊。git init 是在本地把当前目录变成一个 Git 仓库,也就是说项目是从零开始的,还没有远程仓库这个概念。git clone 则是把远程仓库完整复制到本地,它包含所有历史提交、所有分支、所有标签,是一个完整的副本。这两个命令的使用场景差异非常大。
如果你的工作流程是“先在本地写代码,之后才决定托管到远程”,那 git init 就是起点。初始化完成之后,你需要手动添加远程地址,用 git remote add origin 这个命令把本地仓库和远程仓库关联起来。这里有个细节值得注意:如果你在初始化之后直接提交了一堆代码,再关联远程仓库,第一次 push 的时候大概率会遇到问题。远程仓库里如果已经有了文件(比如 README、LICENSE,或者别人已经提交过代码),你的本地历史和远程历史就是两条互不相干的线,直接 push 会被拒绝,让你先 pull。要处理这种情况,要么用 git pull --rebase origin main 把两边历史合并,要么在关联远程时确认远程仓库确实是空的。
git clone 就不存在这个问题。它把远程仓库的所有信息都拉下来,本地分支会自动跟踪对应的远程分支。这里有个实用的经验:克隆下来的仓库默认在 main 分支(或 master),但如果远程仓库不止一个分支,你在本地执行 git branch 可能只看得到当前分支,其他分支用 git branch -a 才能看到。需要切换到某个远程分支时,直接 git checkout 分支名,Git 会自动帮你创建本地分支并建立跟踪关系。这个机制对新人特别友好,不需要手动去建分支、设 upstream。
两种方式没有谁比谁更好,只看你的场景。独立开发一个全新项目,用 init;加入现有项目或者接手别人的代码,用 clone。动手之前先想清楚这一点,能避免后面很多不必要的麻烦。
1.2 三个区的关系搞不懂,后面全乱套
Git 的核心概念之一是三个区域:工作区、暂存区、版本库。工作区就是你本地能看到、能编辑的文件目录;暂存区用 git add 把文件放进去,可以理解为“准备提交的缓冲区”;版本库存放的是已提交的历史快照。日常操作无非就是在这三个区之间搬动文件。
我见过很多人搞不懂“为什么我 commit 了,但 git status 还是显示有改动”,其实是他 commit 之前忘了 git add。Git 的设计逻辑是你先决定哪些文件进入这次提交(add 到暂存区),再提交(commit)。这样做的意义在于,你可以把一次逻辑上完整的变更拆成多个提交,每个提交只做一件事,历史记录更清晰,出问题也好回溯。
三个区之间的移动方向,也对应着不同的命令:
- 工作区 -> 暂存区:git add 文件名,或者 git add . 添加所有改动
- 暂存区 -> 版本库:git commit -m "提交说明"
- 版本库 -> 工作区(或者其他分支):git checkout 或 git reset 等命令,这个后面细讲
有一个高频疑问是“我不小心把不该 add 的文件 add 了怎么办”,答案是 git reset HEAD 文件名,把文件从暂存区撤回到工作区,但文件内容不会变。如果已经 commit 了呢,那就涉及更复杂的回滚操作了,后面专门用一节来讲。总之,把三个区的关系搞清楚,很多命令不用背也能自己推出来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 日常提交链路:status、add、commit 里的门道
2.1 git status 的简洁输出和完整输出怎么配合
git status 几乎是用的最频繁的命令,它告诉你的就是“当前仓库的状态”。默认输出比较详细,会列出哪些文件有改动、哪些文件已经暂存、当前在哪个分支。我看很多老手喜欢用 git status -s,也就是短格式输出。每一行前面有两个字符位,第一列表示暂存区状态,第二列表示工作区状态。比如 M 表示已修改,A 表示新增,?? 表示未跟踪文件。
短格式输出非常高效,一眼扫过去就知道哪些文件做了什么操作。追求效率的话,可以在全局配置里设置一个别名,比如 git config --global alias.s "status -s",之后敲 git s 就能直接看简洁状态。长期使用下来,这个习惯能帮你省下不少时间。
但短格式也有局限,它不显示文件具体修改了哪些内容。如果你需要快速看一眼改动内容,可以配合 git diff 使用。这里有一个很常见的坑:git diff 默认只显示工作区和暂存区之间的差异,也就是“你改了但还没 add 的部分”。如果你已经 add 了,想看暂存区里的改动相对于上一次提交的差异,要用 git diff --cached(或者 --staged,两个是等价的)。很多人在这里搞混,执行 git diff 发现什么都没输出,以为代码没改,其实只是改动已经在暂存区里了。
2.2 git add 的几种形态,以及那些该被忽略的文件
git add 后面可以跟文件名、目录名、通配符或者点号。git add . 是最常用的,把当前目录下所有改动都加入暂存区。但有一点要注意,git add . 不会添加已经被 .gitignore 忽略的文件,也不会添加被明确标记为 skip-worktree 的文件。如果你删除了某个文件,git add . 也可以把这个删除操作记录到暂存区,所以它是“记录所有改动”,不只是新增。
还有一种不常用但非常实用的写法:git add -p。这个命令会逐块(hunk)显示工作区与暂存区的差异,然后让你选择要不要暂存这一块。适合什么场景呢?比如你改了同一个文件里的两个功能,但逻辑上想分成两次提交,用 git add -p 就能把第一个功能的改动暂存,提交,再把第二个功能的改动暂存,提交。这样提交历史就干净很多,以后回滚或者 bisect 查问题时也容易定位。我实际用这个命令挺多的,尤其是独立维护一个项目的时候,能把杂乱的改动梳理得清清楚楚。
.gitignore 是配合 git add 使用的关键文件。很多新人把这个文件写好之后发现不生效,原因是文件已经被跟踪(tracked),.gitignore 只对未跟踪的文件生效。如果文件已经进了版本库,你需要先把它们从 Git 的跟踪中移除:git rm --cached 文件名,这个命令只移除跟踪信息,不会删除本地文件。再提交一次,后续改动就会按 .gitignore 规则被忽略。这里有个额外的技巧:git rm --cached -r . 可以一次性清除所有文件的跟踪状态,配合改动后的 .gitignore 重新 add,能彻底重新梳理哪些文件进版本库、哪些不进。但操作之前务必确认本地文件都还在,别把辛苦写的代码误删了。
2.3 commit 提交规范和 amend 的正确打开方式
git commit -m "说明" 是最基础的提交方式。但说实话,“说明”写得好不好,直接影响你三个月后回顾项目时的体验。我一直提倡提交信息要“说明为什么,而不是说明做了什么”。比如你改了一个缓存逻辑,提交信息写 “优化缓存失效策略,解决并发时数据不一致问题”,就比 “update cache” 有用得多。单行提交信息适合小改动,如果改动复杂,可以用 -m 加标题、-m 加正文的方式,Git 会把两次 -m 的内容分开作为标题和正文。
这里必须提一个很多新人热衷但我不推荐频繁使用的命令:git commit --amend。它的作用是修改最近一次的提交,不生成新的提交记录,而是把当前暂存区的改动合并到上一次提交里,同时你还可以改提交信息。什么时候适合用?你上一次提交信息写错了、漏了一个小文件、或者刚提交就发现一个笔误,这些场景用 amend 很顺手。但一定要注意:如果这个提交已经推送到远程,并且可能被多人拉取过,amend 会把提交历史重写,导致别人的本地仓库和你远程仓库历史对不上,出现各种各样的冲突。原则很简单:还没 push 的提交,随便 amend;已经 push 的,老老实实新开一个提交。
2.4 git log 不只是看历史,关键是筛选
git log 默认输出从当前 HEAD 向过去追溯的所有提交记录,格式是完整的哈希值、作者、日期、提交信息。看多了你会发现信息有点冗余,可以用 git log --oneline,每条记录只显示短哈希和提交信息,一眼扫过去很清爽。
真正提升效率的是过滤参数的组合。git log --oneline --graph 可以显示分支拓扑图,清楚地看到各分支从哪个点分出、合并了哪些提交。git log --oneline --author="某开发者" 只看某个人的提交;git log --oneline --since="2 weeks ago" 只看最近两周的。还有一个我很常用的:git log -p,它显示每次提交的完整差异,适合深入审查某次提交到底改了哪些代码。git log 配合 -S 参数甚至可以搜索某个字符串是什么时候被加入或删除的,这在排查问题时经常派上用场,比如一条数据异常,你不知道是从哪个提交开始引入的,用 git log -S "某个关键变量名" 就能定位到那次提交。
3. 分支与合并:多人协作的基石
3.1 git branch 和 git checkout,搞清楚创建的到底是谁
git branch 分支名 是创建分支,git checkout 分支名 是切换分支,这两个命令在实践中的使用频率极高。但有一个细节很多人没意识到:你每切换一次分支,工作区的文件内容会跟着变化。Git 的分支本质是提交链上的指针,切换分支其实就是让工作区内容变成该指针指向的那个快照。正是这个机制,决定了 Git 分支切换是轻量级的,但也容易在切换时碰到冲突(比如工作区有未提交的改动,挡了切换的路)。
创建并切换分支,可以简写为 git checkout -b 新分支名。这个组合命令十次有九次是这个用法,因为创建完分支你多半马上要进去工作。还有一种写法是 git switch 新分支名,这是较新版本 Git 提供的更语义化的命令,git switch 只管切换,git switch -c 创建并切换,和 git checkout -b 对应。如果你用的 Git 版本比较新,建议优先用 switch 和 restore,命令职责更单一,心智负担轻不少。
分支命名也值得讲究。功能分支用 feature/xxx,修复分支用 fix/xxx,发布分支用 release/xxx,这种前缀约定在多个开发者协作时特别重要,否则分支一多,根本分不清哪个分支是干什么的。我见过把分支命名为 test1、test2 的,过了两周自己都忘了内容,还得一个一个 checkout 去看,非常浪费时间。
3.2 merge 和 rebase:两种合并到底差在哪
这是 Git 里最容易被误解的一对命令。git merge 合并分支,会把另一个分支的提交历史“缝合”到当前分支上,产生一个合并提交(merge commit),分支拓扑是网状交叉的。git rebase 则是把你当前分支的提交“剪切”下来,按顺序重新接到目标分支的最新提交之后,历史是线性的,没有合并提交。
拿实际场景来说:你从 main 拉了一个分支做功能开发,期间 main 上别人又提交了新内容。你想让自己的分支包含 main 的最新改动,把 main 合并进你的分支,这叫 git merge main;如果用 git rebase main,则是把你的提交重新放到 main 最新提交之后。
两者没有绝对的对错,只有适不适合。我的经验是:还没 push 到远程的分支,用 rebase 来整理历史,让提交线尽量线性,读起来像是在讲一个连贯的故事;已经 push 并且别人可能基于它继续开发的分支,不要 rebase,老老实实用 merge,否则你改写了公共历史,别人 pull 的时候就会出现一堆莫名其妙的分叉和冲突。有一个不算严格但相当实用的约定:共享分支用 merge,私有分支用 rebase。记住这条,大部分情况下不会出大问题。
merge 本身也有两种模式,默认是 --merge 会创建合并提交,git merge --ff-only 只在能快进(fast-forward)时合并,不能快进就报错。这个参数适合什么场景?你想确认“我的分支确实完全基于目标分支最新提交,合并不会产生意外历史”,用 --ff-only 是最安全的。如果只想把分支删除前的代码并入主开发线,又不想产生多余的合并提交,也可以用 git merge --squash,它会把分支上的所有提交压缩成一次改动,放到暂存区,由你自己执行一次 commit。这样历史会非常干净,但也意味着分支上的每一次小步提交记录都丢失了,适合功能完成后整体并入主线的场景。
4. 远程协作:fetch、pull、push、tag 的配合逻辑
4.1 fetch 和 pull,一字之差,逻辑差很多
很多人以为 pull 就是 fetch 加 merge 的意思,这个说法没错,但不够全面,而且正是这种简化理解让不少人踩了坑。git fetch 只会把远程仓库的分支、提交、标签等数据下载到本地,但它不会动你当前分支的任何东西。也就是说,fetch 之后你的工作区还是原样,远程的最新状态只是“躺”在本地的一个特殊分支上,比如 origin/main,你需要手动决定怎么处理它。
git pull 则是一个组合命令,它默认先 fetch,然后把远程分支合并到你当前分支。这里的问题在于:如果你当前分支还没 commit 的改动,pull 的时候 Git 有可能会拒绝合并,或者产生冲突。更常见的情况是,你以为 pull 之后代码会自动更新好,结果因为本地有冲突文件,pull 打出一大段冲突提示,直接把工作区搅乱。这时候你要是慌了手脚乱敲命令,很容易丢改动。
我个人的操作习惯是:先 git fetch,看一眼 git log --oneline HEAD..origin/main 了解远程新增了哪些提交,再决定是 git merge origin/main 还是 git rebase origin/main。这样每一步都知道自己在做什么。如果你更习惯直接 pull,建议至少设置一下 pull 的默认行为:git config --global pull.rebase true,这样 pull 默认用 rebase 而不是 merge,本地提交会被自动重新放置到远程提交之后,历史更线性,冲突处理也更直观。
pull 和 push 是一对,但 push 的细节也不少。git push 默认推当前分支到同名远程分支,如果远程没有同名分支,会新建一个。这里有个经常碰到的问题:某些公司的统一发布仓库不允许直接 push main,需要你从 main 拉分支开发,提交,再发起合并申请。还有一个值得注意的场景:你本地删除了某个分支,想同步删除远程的对应分支,要用 git push origin --delete 分支名。
4.2 分支跟踪信息 remote -v 和 branch -vv 该怎么看
远程仓库相关的基础命令是 git remote -v,它列出当前配置的所有远程地址。通常只有 origin 一个,但也要看情况,比如你有上游仓库时,可能有 origin 和 upstream 两个远程。git remote add 别名 URL 可以添加新的远程,git remote remove 别名 可以删除远程。
分支跟踪信息用 git branch -vv 查看,它会显示每个本地分支正在跟踪哪个远程分支,以及本地领先或落后多少个提交。这个命令相当实用,特别是本地分支一多的时候,你能一眼看出哪些分支没有上游分支、哪些分支有未推送的提交。“本地领先 3 个提交,落后 1 个提交”——这种信息能帮你判断自己分支的状态是否和远程同步。
新版本 Git 里,git push 默认行为在不同配置下不一样。老版本默认 push 到当前分支的同名远程分支,如果不存在就报错;新版本则默认 push 当前分支并建立跟踪关系。如果你想统一行为,建议 git config --global push.default simple,这是当前项目主流的默认设置,既安全又直观。push.default 还有 matching 等选项,但在大多数现代工作流中 simple 就够了,不需要折腾。
4.3 tag:发布节点的标记,和分支的定位完全不一样
git tag 做的事情是给某个提交打一个永久的标签,一般用在发布节点上,比如 v1.0.0、v1.2.1。它的特殊之处在于:标签不是会移动的指针,一旦打了就不该变,除非你显式删除或强制覆盖。这一点和分支有本质区别——分支指针始终指向最新提交,标签则固定指向某一次提交。
打标签的命令是 git tag 标签名,如果想给标签加上说明,用 git tag -a 标签名 -m "说明"。带 -a 的标注标签(annotated tag)会额外存储标签创建者、创建时间、说明信息,更正式一些;不带 -a 的轻量标签(lightweight tag)只是一个指向某次提交的引用,适合临时标记。发布项目建议用标注标签,方便同事读取发布信息。
标签默认不会自动推送到远程,需要显式执行 git push origin 标签名,或者一次性推送所有标签 git push origin --tags。删除标签分两步,本地 git tag -d 标签名,远程 git push origin :refs/tags/标签名。很多人只删了本地,忘删远程,结果旧标签一直在远程仓库里挂着,很容易误导其他人拉取到过时版本。
5. 撤销与回滚:这些命令平时不用,但必须懂原理
5.1 工作区、暂存区、仓库区的回滚,分别对应什么命令
撤销操作的难点在于:你脑子里得清楚“要把哪里的状态恢复到哪个时间点”。如果只想撤销工作区的修改,也就是把已跟踪文件还原到上次提交时的内容,用 git restore 文件名(新语法)或 git checkout -- 文件名(旧语法)。这个操作会直接丢弃工作区改动,且不可恢复,执行前务必确认那些改动你确实不想要了。
如果错误地把文件 add 进了暂存区,但工作区改动还想保留,用 git restore --staged 文件名,它只是把文件从暂存区移回工作区,文件内容完全不变。与之等价的旧命令是 git reset HEAD 文件名。注意区分:restore --staged 不管工作区的内容,它只负责调整暂存状态;restore 不带 --staged 则会同时把暂存区和工作区都恢复到某个版本。
如果已经 commit 了怎么办?这就涉及更“重”的操作了。想要完全回退到之前某次提交的状态,可以用 git reset。reset 有三件套:--soft(只移动 HEAD,暂存区保留)、--mixed(默认,移动 HEAD 并且重置暂存区,但不碰工作区)、--hard(移动 HEAD,重置暂存区,同时覆盖工作区文件)。一句话总结:--soft 最温柔,--hard 最暴力。git reset --hard HEAD~1 能让你丢掉最近一次提交连同它的所有改动,但请认识到,这个操作一旦执行,被丢弃的提交不会出现在 reflog 的正常路径里,恢复起来非常麻烦。
5.2 reset 和 revert:一个改写历史,一个追加新提交
reset 和 revert 都用来撤销提交,但哲学完全不同。git reset 是把 HEAD 指针往回移动,相当于“假装这些提交没发生过”,这本质上是改写历史。适合本地还没 push 的提交。git revert 则不同,它不是删除历史,而是创建一个反向提交,把之前某次提交的改动“反过来”再做一遍,历史是向前走的。revert 适合已经 push 到远程的提交,因为不改写历史,不会破坏其他人的本地状态。
举一个常见场景:你在 main 上合并了一个功能分支,发现引入了一个严重 bug,现在需要撤回这个合并。如果分支是私有分支且还没有人基于它开发,用 git reset --hard 回到合并前的位置没问题。但如果这个分支已经被其他人拉取过,或者已经有多人基于 main 继续提交,用 git revert -m 1 合并提交哈希 来生成一个反向提交,让 main 保持原有历史不断往前走。
还有一个相当好用但常被忽略的命令是 git cherry-pick,它能把任意分支上的某一个提交精确地复制到当前分支上。什么场景用?你在 main 上修了一个紧急 bug,提交之后发现 dev 分支也需要这个修复,但你不想把整条 main 分支合并进 dev。此时切到 dev 分支,git cherry-pick main分支上那个提交的哈希,就把这个修复“摘”过来,只带上这个提交的内容。cherry-pick 是跨分支精确传递提交的利器,比反复 merge 整条分支干净得多。
5.3 误删文件、误重设提交,怎么靠 reflog 救回来
reflog 是 Git 的一个安全网,它记录了你本地 HEAD 指针每一次移动的历史。即使你执行了 git reset --hard 回到过去,reflog 里依然保留着刚才那个“未来”提交的哈希。也就是说,只要这个仓库的 reflog 还没被清理,你就有机会找回几乎所有误操作丢掉的内容。
实操路径是这样的:误操作后先 git reflog,找到你想回去的那个提交哈希(reflog 里每条记录会显示操作类型,reset 前的位置就在那附近),然后 git reset --hard 那个哈希,回到误操作之前的状态。我处理过不少同事的误删现场,大部分都能靠 reflog 救回来,真正救不回来的情况基本都是 reflog 过期时间过了或者手动清理过日志。
reflog 也有保留时限,默认是 90 天,过期的记录会被 Git 的自动垃圾回收机制清掉。如果你有重要操作怕失效,可以提前用 git reflog expire --expire=never 来延长特定记录的生命周期。但日常开发中,只要你不是刻意清理,90 天足够覆盖绝大多数意外。
6. 常见问题与避坑实录
6.1 合并冲突,别慌,按这个流程处理
冲突是 Git 使用中最容易让人头皮发麻的场面,但本质上它只是告诉你“两处改动都动了同一块代码,我帮你标记出来,你自己决定留哪边”。冲突发生时,Git 会在相关文件里插入冲突标记,<<<<<<< HEAD 和 ======= 之间是当前分支的内容,======= 和 >>>>>>> 某个分支 之间是合并进来的那个分支的内容。
处理步骤我自己一直在用,可以说相当顺手。先执行 git status,确认哪些文件冲突了;然后打开这些文件,搜索冲突标记,逐个判断是保留当前分支、保留合并分支、还是手动改成两者结合;改完之后,对每个冲突文件执行 git add,表示“这个冲突我解决了”;最后 commit,生成合并提交。整个过程不要慌,不要在冲突状态下乱敲 reset、checkout 之类命令,否则很容易把还没解决完的冲突状态搞得更乱。
冲突处理有一个小技巧值得提:如果合并进来的分支改动比较复杂,直接看冲突标记里的代码片段不容易判断语义,就先用 git log 查看那个分支的提交历史,理解它这条线的改动意图,再决定怎么取舍。很多时候冲突不是真的“代码矛盾”,而是“两个人的理解不一样”,搞懂对方想干嘛,冲突自然就化解了。
6.2 .gitignore 不生效,和文件缓存有关系
前面提过 .gitignore 只对未跟踪文件生效,这是最常见的“不生效”原因。另一个常见原因是:你可能用 git add -f 强制添加过某些被忽略的文件,或者早先把文件加入跟踪后没有清理缓存。
处理办法是清理 Git 的索引缓存(index),然后重新提交。命令是 git rm -r --cached .,这会移出所有文件的跟踪状态,然后你重新 git add .,Git 会按最新的 .gitignore 规则重新决定哪些文件被跟踪。我再重复一次:git rm --cached 不会删除本地文件,它只移除 Git 索引里的记录。但如果你想删除的是整个目录,务必确认没有手滑加上了 --force 之类的参数导致物理删除。
还有一个冷门但真实存在的情况:.gitignore 的规则写错了。比如你写了一个 rules/*.log,但实际日志文件在 logs/ 目录下,那匹配不上。Git 的忽略规则支持通配符、目录名、取反(! 开头),最好先用 git check-ignore 文件名 验证某条规则是否命中,再决定改规则还是改文件路径。这个命令虽然小众,但排查“为什么没被忽略”时非常高效。
6.3 大小写重命名、权限变化等冷门问题,一个命令搞定
文件大小写问题在 Windows 和 macOS 上尤其常见。你在 Linux 上把文件名从 README.MD 改成了 readme.md,提交到远程没问题,但在 macOS 默认文件系统上,Git 可能认为文件没变化,因为大小写不敏感。处理办法是 git mv README.MD readme.md,先 rename,再提交;实在不行就用 git add -A 试试。如果文件已经在远程仓库里存了错误的大小写,改完之后记得让 team 成员重新 clone 或者显式执行 git reset 刷新索引。
另一个容易忽略的问题是文件权限变化。Git 默认不跟踪文件的执行权限变化,除非你在仓库里配置了 fileMode。如果你发现明明把某个文件改成可执行了,但 git status 没反应,检查一下 git config core.fileMode 是否被全局设为 false。在一些跨平台协作项目里,这个配置经常被踩到,解决方法也很直接:确认仓库要求的默认文件权限,然后统一执行 git update-index --chmod=+x 文件路径 来标记执行位。
Git 的命令体系很庞大,但支撑日常开发的核心链路其实并不复杂。把这条链路里的每个命令理解透、用熟练,比记住一百个冷门命令更实在。我自己从磕磕绊绊到现在熟练操作,最大的感触就是:Git 的难点永远不是命令语法,而是你脑海里对仓库状态有没有一个清晰的模型。只要三个区、分支指针、远程跟踪这些概念在脑子里立住了,大部分命令都可以顺理成章地推出来。
