1. 为什么“git 基本操作”值得单独拎出来讲刚入行那会儿,我对 Git 的态度就是“能用就行”——clone、pull、push三板斧走天下,遇到冲突就慌,遇到回退就懵,实在不行就把整个项目删了重新拉一遍。后来参与协作的项目多了,代码提交历史越来越乱,才意识到 Git 这东西,基础不牢,后面全是坑。你可能也经历过:改了半天代码,一个git reset --hard下去,全没了;或者合并分支时冲突满屏,不知道该保留谁的代码;又或者提交了一堆没用的文件,仓库越来越大,想清理却无从下手。这篇内容就是写给那些“会用但不够熟”的人。我会把 Git 日常操作里最核心、最高频的部分拆开揉碎,从工作区、暂存区、本地仓库、远程仓库这四个区域的关系讲起,把add、commit、push、pull、branch、merge、rebase、reset、revert这些命令背后的逻辑说清楚。不是手册式的罗列参数,而是告诉你每个操作在什么场景下用、为什么这么设计、踩过哪些坑、怎么排查问题。适合刚接触版本控制的新手,也适合用了几年但一直“知其然不知其所以然”的开发者。我自己的经验是,Git 真正难的不是命令本身,而是对“状态流转”的理解。你把四个区域的关系搞明白了,大部分问题都能自己推理出解法。下面就从整体设计思路开始,一步步拆。2. Git 核心设计思路与四区模型拆解2.1 四个区域到底是怎么回事Git 的所有操作,本质上都是在四个区域之间搬运代码。这四个区域分别是:工作区(Working Directory)、暂存区(Staging Area / Index)、本地仓库(Local Repository)、远程仓库(Remote Repository)。工作区就是你肉眼能看到的项目文件夹,你在这里增删改文件。暂存区是一个中间层,你执行git add之后,改动会进入暂存区,相当于“购物车”——你把想提交的东西先放进去,确认无误再结账。本地仓库是你执行git commit之后代码真正落盘的地方,每次提交都会生成一个唯一的 commit hash,形成一条时间线。远程仓库则是团队共享的代码托管服务,git push把本地提交推上去,git pull把别人的提交拉下来。为什么要有暂存区这个“多此一举”的设计?我一开始也觉得麻烦,直接提交不就行了。后来发现,暂存区的价值在于精确控制提交粒度。比如你同时改了三个文件,其中两个是修复 bug,一个是调试用的临时输出,你可以只add那两个 bug 修复文件,提交一条干净的记录,临时输出留在工作区不提交。这样提交历史清晰,回滚也方便。没有暂存区,你就只能全提交或者全不提交,粒度控制不了。用生活类比:工作区是你的书桌,暂存区是你准备寄出的快递箱,本地仓库是你家的储物柜,远程仓库是公司的公共仓库。你从书桌上挑东西放进快递箱(add),封箱寄到储物柜(commit),再从储物柜搬到公共仓库(push)。反过来,从公共仓库取货到储物柜(fetch),再拆箱放到书桌(checkout/merge)。2.2 为什么 Git 用快照而不是差异很多人以为 Git 存的是每次改动的差异(diff),其实不是。Git 每次 commit 存的是整个项目在该时刻的完整快照。如果你改了一个文件,Git 会存这个文件的新版本;没改的文件,Git 不会重复存储,而是用一个指针指向之前已经存过的相同内容。这个设计叫“内容寻址存储”,每个文件内容通过 SHA-1 哈希值索引,相同内容只存一份。这样做的好处是切换分支极快。因为分支本质上只是一个指向某个 commit 的指针,切换分支就是移动 HEAD 指针,不需要像传统版本控制那样逐个文件对比差异再应用。你在分支之间来回跳,几乎是瞬间完成。这也是为什么 Git 鼓励频繁开分支——成本太低了。理解这一点,你就能明白为什么git checkout切换分支时,工作区文件会“瞬间”变成目标分支的内容。不是它逐个文件去改,而是直接根据目标 commit 的快照重建工作区。当然,如果有未提交的改动,Git 会尝试保留,保留不了就会报错阻止切换,防止你丢代码。2.3 HEAD 指针与分支的本质HEAD 是 Git 里最核心的概念之一。它就是一个指针,指向你当前所在的 commit。通常情况下,HEAD 指向某个分支名,分支名再指向具体的 commit。比如你在 main 分支上,HEAD 指向 main,main 指向最新的 commit。当你执行git checkout切换到另一个分支时,HEAD 就指向那个分支。当你执行git commit时,Git 会创建一个新 commit,然后把当前分支指针移动到新 commit,HEAD 跟着分支走。这就是为什么提交后你的分支“前进”了一步。如果 HEAD 直接指向某个 commit 而不是分支,这叫“分离头指针状态”(detached HEAD)。这种状态下提交的代码不属于任何分支,切换走后很容易丢失。新手经常在 checkout 某个历史 commit 后不小心提交,然后切回分支发现提交不见了,就是这个原因。解决办法是记住那个 commit hash,或者当时就新建一个分支。我踩过的坑:有一次排查问题,checkout 到一个旧 commit 看代码,顺手改了几行提交了,然后切回 main 分支继续干活。过了两天想起来那个改动,已经找不到 commit hash 了,只能重写。所以现在我要在历史版本上改东西,一定先git checkout -b fix-xxx建个分支再操作。3. 日常高频操作逐个拆解与实操要点3.1 从 clone 到第一次 push 的完整链路拿到一个新项目,第一步是git clone <仓库地址>。这个命令做了几件事:把远程仓库的所有 commit 下载到本地,创建本地仓库,自动把远程仓库命名为origin,检出默认分支(通常是 main 或 master)到工作区。clone 之后,你通常会配置一下用户信息,因为 commit 需要记录作者。全局配置用git config --global user.name "你的名字"和git config --global user.email "你的邮箱"。如果某个项目想用不同的身份,去掉--global在项目目录里单独配。然后就是改代码、提交、推送的标准流程:git status # 查看当前改动状态 git add <文件> # 把改动加入暂存区 git add . # 或者把所有改动加入暂存区 git commit -m "提交说明" # 提交到本地仓库 git push origin main # 推送到远程 main 分支这里有个细节:git add .会把当前目录下所有改动都加进去,包括新建的文件。但如果有不想提交的文件,比如编译产物、本地配置,应该提前写好.gitignore。.gitignore的规则是从上到下匹配,后面的规则可以覆盖前面的。常见写法:# 忽略所有 .log 文件 *.log # 但不忽略 important.log !important.log # 忽略 build 目录 build/ # 忽略所有 .tmp 文件,包括子目录 **/*.tmp注意:.gitignore只对未跟踪的文件生效。如果某个文件已经被提交过,后来才加进.gitignore,Git 仍然会继续跟踪它。需要先git rm --cached <文件>把它从跟踪列表移除,再提交一次。3.2 查看状态与差异的正确姿势git status是最常用的命令,但很多人只看个大概。其实它输出里区分了“已暂存的改动”和“未暂存的改动”,还有“未跟踪的文件”。养成提交前先git status看一眼的习惯,能避免很多误提交。git diff看的是工作区和暂存区的差异,也就是你改了但还没add的内容。git diff --staged(或--cached)看的是暂存区和最后一次 commit 的差异,也就是你add了但还没commit的内容。git diff HEAD则是工作区加暂存区和最后一次 commit 的总差异。我常用的组合是:改完代码先git diff确认改了什么,然后git add,再git diff --staged确认即将提交的内容,最后git commit。多这一步确认,能避免把调试代码、临时注释提交上去。还有一个实用命令git log --oneline --graph --all,用图形化方式展示所有分支的提交历史,一眼看清分支合并关系。--oneline让每条记录只显示一行,--graph画 ASCII 图形,--all显示所有分支。排查“这个改动是哪个分支合进来的”这类问题时特别好用。3.3 分支操作:创建、切换、合并、删除分支是 Git 的杀手锏。创建分支git branch <名字>,切换分支git checkout <名字>,创建并切换git checkout -b <名字>。新版 Git 推荐用git switch代替 checkout 做分支切换,语义更清晰:git switch <名字>、git switch -c <名字>。合并分支有两种方式:git merge和git rebase。merge 会创建一个新的合并 commit,保留完整的分支历史;rebase 会把当前分支的提交“搬”到目标分支最新 commit 之后,形成一条直线历史。什么时候用哪个?我的经验是:公共分支用 merge,个人分支用 rebase。因为 rebase 会改写 commit hash,如果别人已经基于你的分支干活了,你 rebase 后他们的历史就对不上了。个人分支还没推送到远程,或者只有自己在用,rebase 能让历史更干净。merge 冲突的解决流程:执行git merge <分支>后如果有冲突,Git 会在冲突文件里用<<<<<<<、=======、>>>>>>>标记冲突区域。你手动编辑文件,保留想要的内容,删掉标记符号,然后git add冲突文件,最后git commit完成合并。如果想放弃合并,git merge --abort回到合并前状态。删除分支:git branch -d <名字>删除已合并的分支,git branch -D <名字>强制删除未合并的分支。远程分支删除用git push origin --delete <名字>。提示:删除分支前先确认它的提交已经合并到其他分支,否则那些 commit 会变成“悬空对象”,虽然短期内还能通过 reflog 找回,但时间久了会被 Git 的垃圾回收清理掉。3.4 回退与撤销:reset、revert、checkout 的区别这是最容易搞混的一组命令。我按场景来说:场景一:改错了工作区文件,还没 add。用git checkout -- <文件>或git restore <文件>丢弃工作区改动,恢复到暂存区或最后一次 commit 的状态。这个操作不可逆,改之前想清楚。场景二:add 错了,想从暂存区拿出来。用git reset HEAD <文件>或git restore --staged <文件>,把文件从暂存区移回工作区,改动还在。场景三:commit 错了,想撤销这次提交但保留改动。用git reset --soft HEAD~1,撤销 commit,改动回到暂存区。如果想连暂存区也撤销,用git reset --mixed HEAD~1(默认)。如果想连工作区改动一起丢弃,用git reset --hard HEAD~1,这个最危险,改动全没。场景四:已经 push 到远程的提交要撤销。不能用 reset,因为会改写历史,别人拉取时会出问题。应该用git revert <commit>,它创建一个新的 commit 来抵消目标 commit 的改动,历史是向前的,安全。命令影响范围是否改写历史适用场景git reset --soft仅移动 HEAD是本地撤销 commit,保留改动git reset --mixedHEAD + 暂存区是本地撤销 commit 和 addgit reset --hardHEAD + 暂存区 + 工作区是彻底丢弃本地改动git revert新增反向 commit否撤销已推送的提交git checkout --工作区否丢弃未 add 的改动我个人的原则是:只要提交推送过,一律用 revert;只有本地未推送的提交,才考虑 reset。这样最安全,不会给协作者添麻烦。4. 协作场景下的实战流程与冲突处理4.1 多人协作的标准工作流团队协作最怕的就是各写各的,最后合并时冲突一大堆。我们团队现在用的流程是:主分支(main)保持稳定,每个人从 main 拉一个新分支开发,开发完推送到远程,发起合并请求,代码审查通过后合并回 main。具体操作:git checkout main git pull origin main # 确保本地 main 最新 git checkout -b feature/xxx # 创建功能分支 # ... 开发 ... git add . git commit -m "feat: 完成 xxx 功能" git push origin feature/xxx # 推送到远程同名分支然后在代码托管平台上发起合并请求。审查期间如果 main 有更新,需要把 main 的最新改动同步到功能分支:git checkout main git pull origin main git checkout feature/xxx git rebase main # 或 git merge main # 解决冲突后 git push origin feature/xxx --force-with-lease这里用--force-with-lease而不是--force,是因为前者会在推送前检查远程分支有没有别人的新提交,如果有就拒绝推送,防止覆盖别人的工作。--force则是无脑覆盖,危险。4.2 冲突产生的根源与解决思路冲突的本质是:两个分支修改了同一个文件的同一区域,Git 无法自动判断该保留哪个。常见于多人改同一个函数、同一个配置文件、同一段样式。解决冲突的核心思路是:理解双方的改动意图,手动合并成最终版本。不要无脑选“ours”或“theirs”,那样很可能丢掉别人的功能。冲突文件长这样:<<<<<<< HEAD const API_URL = 'https://api.example.com/v1'; ======= const API_URL = 'https://api.example.com/v2'; >>>>>>> feature/new-api<<<<<<< HEAD到=======之间是当前分支的内容,=======到>>>>>>>之间是合入分支的内容。你需要决定最终保留什么,然后删掉所有标记符号。如果两边都需要,就整合成一行;如果只保留一边,就删掉另一边。解决完所有冲突文件后,git add标记为已解决,然后git commit完成合并。如果是 rebase 过程中冲突,解决后执行git rebase --continue;想放弃就git rebase --abort。注意:冲突解决后一定要跑一遍测试,确认合并结果没有破坏功能。我遇到过冲突解决时手滑删了一行关键代码,本地没测就提交,结果合并后线上出问题。后来养成习惯,冲突解决后至少跑一遍单元测试和本地启动。4.3 拉取代码时 pull 和 fetch 的选择git pull等于git fetch加git merge。它先从远程拉取最新 commit 到本地远程跟踪分支,然后合并到当前分支。git fetch只做前半步,拉取后不合并,你可以先看看远程改了什么,再决定怎么合并。我现在的习惯是:先 fetch,再根据情况 merge 或 rebase。因为直接 pull 如果本地有未提交改动,可能会触发自动合并失败,或者产生意料之外的 merge commit。先 fetch 能看到远程分支和本地的差异,心里有数再操作。git fetch origin git log HEAD..origin/main --oneline # 看远程比本地多了哪些提交 git merge origin/main # 确认后再合并如果本地有未提交改动,pull 之前可以先git stash暂存起来,拉取后再git stash pop恢复。stash 适合临时切换分支或拉取代码时保存工作现场。5. 常见问题排查与避坑经验实录5.1 提交历史乱了怎么收拾提交历史乱通常表现为:commit message 写错了、提交了不该提交的文件、多个小提交想合并成一个、提交顺序不对想调整。改最近一次 commit message:git commit --amend -m "新说明"。这个命令会创建一个新 commit 替换掉原来的,如果已经推送过,需要 force push,所以只建议在未推送时用。合并多个提交:用git rebase -i HEAD~n,n 是最近 n 个提交。交互界面里把想合并的提交前面的pick改成squash或s,保存后会让你编辑合并后的 commit message。这个操作会改写历史,同样只建议在未推送的个人分支上用。从历史中删除某个文件:如果文件是最近一次提交加的,git rm --cached <文件>然后git commit --amend。如果是更早的提交,需要用git filter-branch或git filter-repo工具,操作复杂且危险,建议提前备份。我踩过的坑:有一次把包含本地数据库密码的配置文件提交了,发现时已经推送。当时手忙脚乱用 reset 回退再强推,结果把同事的提交也覆盖了。正确做法应该是:立即修改密码,然后用 revert 撤销那次提交,再清理历史。密码泄露的第一优先级是改密码,不是改历史,因为历史可能已经被别人拉走了。5.2 误操作后的急救指南Git 大部分误操作都能救回来,关键是别慌,别乱敲命令。误删分支:git reflog查看操作记录,找到删除前的 commit hash,git checkout -b <分支名> <hash>恢复。reset --hard 丢了改动:同样用git reflog找到 reset 之前的 commit hash,git reset --hard <hash>回去。reflog 默认保留 90 天的记录,时间够用。commit 后想撤销:参考第 3.4 节的 reset 和 revert 区别。push 错了想撤回:如果远程分支只有你在用,可以git reset --hard <目标commit>后git push --force-with-lease。如果有别人在用,用 revert 创建反向提交。git reflog是 Git 的“后悔药”,它记录了 HEAD 和分支指针的所有移动。我建议每个开发者都记住这个命令,关键时刻能救命。5.3 常见问题速查表问题现象可能原因解决方法push 被拒绝,提示 non-fast-forward远程有本地没有的提交先git pull --rebase再 push切换分支时提示有未提交改动工作区或暂存区有改动git stash暂存,或先 commit合并时冲突文件太多分支分叉太久定期从 main 同步到功能分支commit 后发现漏了文件提交不完整git add漏的文件后git commit --amend克隆仓库特别慢仓库历史太大用--depth 1浅克隆,只拉最新一次提交不小心提交了大文件仓库体积暴涨用 filter-repo 清理,或重建仓库中文文件名显示乱码编码配置问题git config --global core.quotepath false换行符警告Windows 和 Unix 换行符差异git config --global core.autocrlf input提示:浅克隆(--depth 1)适合只需要最新代码、不关心历史的场景,比如 CI 构建。但浅克隆的仓库不能直接 push,需要先git fetch --unshallow补全历史。5.4 我总结的几条实操心得第一,提交前必看 status 和 diff。这个习惯帮我避免了无数次误提交。多花十秒钟确认,省得后面花十分钟回退。第二,commit message 写清楚“为什么”而不是“做了什么”。代码本身能看出做了什么,但看不出为什么这么做。比如“修复登录 bug”不如“修复并发登录时 token 覆盖导致登出”,后者对排查问题的人更有价值。第三,功能分支及时同步主分支。不要等开发完了才合并,那时候冲突可能已经堆积如山。每天开工前从 main rebase 一次,冲突小且容易解决。第四,危险操作前先建备份分支。比如要 rebase 或 filter-branch 之前,git branch backup-xxx留个后路。出问题了直接切回备份分支,比重头再来强。第五,团队统一工作流。merge 还是 rebase、分支怎么命名、commit message 什么格式,这些最好团队内达成一致。工具层面可以用 commitlint、husky 做提交前检查,减少人为失误。Git 这东西,命令就那么多,但组合起来能应对的场景千变万化。我的建议是先把四区模型和 HEAD 指针理解透,然后遇到问题多查git help <命令>和官方文档,少复制粘贴来路不明的命令。每次操作前想清楚“这个命令会移动哪个指针、影响哪个区域”,大部分坑都能提前避开。
1. 为什么“git 基本操作”值得单独拎出来讲刚入行那会儿,我对 Git 的态度就是“能用就行”——clone、pull、push三板斧走天下,遇到冲突就慌,遇到回退就懵,实在不行就把整个项目删了重新拉一遍。后来参与协作的项目多了,代码提交历史越来越乱,才意识到 Git 这东西,基础不牢,后面全是坑。你可能也经历过:改了半天代码,一个git reset --hard下去,全没了;或者合并分支时冲突满屏,不知道该保留谁的代码;又或者提交了一堆没用的文件,仓库越来越大,想清理却无从下手。这篇内容就是写给那些“会用但不够熟”的人。我会把 Git 日常操作里最核心、最高频的部分拆开揉碎,从工作区、暂存区、本地仓库、远程仓库这四个区域的关系讲起,把add、commit、push、pull、branch、merge、rebase、reset、revert这些命令背后的逻辑说清楚。不是手册式的罗列参数,而是告诉你每个操作在什么场景下用、为什么这么设计、踩过哪些坑、怎么排查问题。适合刚接触版本控制的新手,也适合用了几年但一直“知其然不知其所以然”的开发者。我自己的经验是,Git 真正难的不是命令本身,而是对“状态流转”的理解。你把四个区域的关系搞明白了,大部分问题都能自己推理出解法。下面就从整体设计思路开始,一步步拆。2. Git 核心设计思路与四区模型拆解2.1 四个区域到底是怎么回事Git 的所有操作,本质上都是在四个区域之间搬运代码。这四个区域分别是:工作区(Working Directory)、暂存区(Staging Area / Index)、本地仓库(Local Repository)、远程仓库(Remote Repository)。工作区就是你肉眼能看到的项目文件夹,你在这里增删改文件。暂存区是一个中间层,你执行git add之后,改动会进入暂存区,相当于“购物车”——你把想提交的东西先放进去,确认无误再结账。本地仓库是你执行git commit之后代码真正落盘的地方,每次提交都会生成一个唯一的 commit hash,形成一条时间线。远程仓库则是团队共享的代码托管服务,git push把本地提交推上去,git pull把别人的提交拉下来。为什么要有暂存区这个“多此一举”的设计?我一开始也觉得麻烦,直接提交不就行了。后来发现,暂存区的价值在于精确控制提交粒度。比如你同时改了三个文件,其中两个是修复 bug,一个是调试用的临时输出,你可以只add那两个 bug 修复文件,提交一条干净的记录,临时输出留在工作区不提交。这样提交历史清晰,回滚也方便。没有暂存区,你就只能全提交或者全不提交,粒度控制不了。用生活类比:工作区是你的书桌,暂存区是你准备寄出的快递箱,本地仓库是你家的储物柜,远程仓库是公司的公共仓库。你从书桌上挑东西放进快递箱(add),封箱寄到储物柜(commit),再从储物柜搬到公共仓库(push)。反过来,从公共仓库取货到储物柜(fetch),再拆箱放到书桌(checkout/merge)。2.2 为什么 Git 用快照而不是差异很多人以为 Git 存的是每次改动的差异(diff),其实不是。Git 每次 commit 存的是整个项目在该时刻的完整快照。如果你改了一个文件,Git 会存这个文件的新版本;没改的文件,Git 不会重复存储,而是用一个指针指向之前已经存过的相同内容。这个设计叫“内容寻址存储”,每个文件内容通过 SHA-1 哈希值索引,相同内容只存一份。这样做的好处是切换分支极快。因为分支本质上只是一个指向某个 commit 的指针,切换分支就是移动 HEAD 指针,不需要像传统版本控制那样逐个文件对比差异再应用。你在分支之间来回跳,几乎是瞬间完成。这也是为什么 Git 鼓励频繁开分支——成本太低了。理解这一点,你就能明白为什么git checkout切换分支时,工作区文件会“瞬间”变成目标分支的内容。不是它逐个文件去改,而是直接根据目标 commit 的快照重建工作区。当然,如果有未提交的改动,Git 会尝试保留,保留不了就会报错阻止切换,防止你丢代码。2.3 HEAD 指针与分支的本质HEAD 是 Git 里最核心的概念之一。它就是一个指针,指向你当前所在的 commit。通常情况下,HEAD 指向某个分支名,分支名再指向具体的 commit。比如你在 main 分支上,HEAD 指向 main,main 指向最新的 commit。当你执行git checkout切换到另一个分支时,HEAD 就指向那个分支。当你执行git commit时,Git 会创建一个新 commit,然后把当前分支指针移动到新 commit,HEAD 跟着分支走。这就是为什么提交后你的分支“前进”了一步。如果 HEAD 直接指向某个 commit 而不是分支,这叫“分离头指针状态”(detached HEAD)。这种状态下提交的代码不属于任何分支,切换走后很容易丢失。新手经常在 checkout 某个历史 commit 后不小心提交,然后切回分支发现提交不见了,就是这个原因。解决办法是记住那个 commit hash,或者当时就新建一个分支。我踩过的坑:有一次排查问题,checkout 到一个旧 commit 看代码,顺手改了几行提交了,然后切回 main 分支继续干活。过了两天想起来那个改动,已经找不到 commit hash 了,只能重写。所以现在我要在历史版本上改东西,一定先git checkout -b fix-xxx建个分支再操作。3. 日常高频操作逐个拆解与实操要点3.1 从 clone 到第一次 push 的完整链路拿到一个新项目,第一步是git clone <仓库地址>。这个命令做了几件事:把远程仓库的所有 commit 下载到本地,创建本地仓库,自动把远程仓库命名为origin,检出默认分支(通常是 main 或 master)到工作区。clone 之后,你通常会配置一下用户信息,因为 commit 需要记录作者。全局配置用git config --global user.name "你的名字"和git config --global user.email "你的邮箱"。如果某个项目想用不同的身份,去掉--global在项目目录里单独配。然后就是改代码、提交、推送的标准流程:git status # 查看当前改动状态 git add <文件> # 把改动加入暂存区 git add . # 或者把所有改动加入暂存区 git commit -m "提交说明" # 提交到本地仓库 git push origin main # 推送到远程 main 分支这里有个细节:git add .会把当前目录下所有改动都加进去,包括新建的文件。但如果有不想提交的文件,比如编译产物、本地配置,应该提前写好.gitignore。.gitignore的规则是从上到下匹配,后面的规则可以覆盖前面的。常见写法:# 忽略所有 .log 文件 *.log # 但不忽略 important.log !important.log # 忽略 build 目录 build/ # 忽略所有 .tmp 文件,包括子目录 **/*.tmp注意:.gitignore只对未跟踪的文件生效。如果某个文件已经被提交过,后来才加进.gitignore,Git 仍然会继续跟踪它。需要先git rm --cached <文件>把它从跟踪列表移除,再提交一次。3.2 查看状态与差异的正确姿势git status是最常用的命令,但很多人只看个大概。其实它输出里区分了“已暂存的改动”和“未暂存的改动”,还有“未跟踪的文件”。养成提交前先git status看一眼的习惯,能避免很多误提交。git diff看的是工作区和暂存区的差异,也就是你改了但还没add的内容。git diff --staged(或--cached)看的是暂存区和最后一次 commit 的差异,也就是你add了但还没commit的内容。git diff HEAD则是工作区加暂存区和最后一次 commit 的总差异。我常用的组合是:改完代码先git diff确认改了什么,然后git add,再git diff --staged确认即将提交的内容,最后git commit。多这一步确认,能避免把调试代码、临时注释提交上去。还有一个实用命令git log --oneline --graph --all,用图形化方式展示所有分支的提交历史,一眼看清分支合并关系。--oneline让每条记录只显示一行,--graph画 ASCII 图形,--all显示所有分支。排查“这个改动是哪个分支合进来的”这类问题时特别好用。3.3 分支操作:创建、切换、合并、删除分支是 Git 的杀手锏。创建分支git branch <名字>,切换分支git checkout <名字>,创建并切换git checkout -b <名字>。新版 Git 推荐用git switch代替 checkout 做分支切换,语义更清晰:git switch <名字>、git switch -c <名字>。合并分支有两种方式:git merge和git rebase。merge 会创建一个新的合并 commit,保留完整的分支历史;rebase 会把当前分支的提交“搬”到目标分支最新 commit 之后,形成一条直线历史。什么时候用哪个?我的经验是:公共分支用 merge,个人分支用 rebase。因为 rebase 会改写 commit hash,如果别人已经基于你的分支干活了,你 rebase 后他们的历史就对不上了。个人分支还没推送到远程,或者只有自己在用,rebase 能让历史更干净。merge 冲突的解决流程:执行git merge <分支>后如果有冲突,Git 会在冲突文件里用<<<<<<<、=======、>>>>>>>标记冲突区域。你手动编辑文件,保留想要的内容,删掉标记符号,然后git add冲突文件,最后git commit完成合并。如果想放弃合并,git merge --abort回到合并前状态。删除分支:git branch -d <名字>删除已合并的分支,git branch -D <名字>强制删除未合并的分支。远程分支删除用git push origin --delete <名字>。提示:删除分支前先确认它的提交已经合并到其他分支,否则那些 commit 会变成“悬空对象”,虽然短期内还能通过 reflog 找回,但时间久了会被 Git 的垃圾回收清理掉。3.4 回退与撤销:reset、revert、checkout 的区别这是最容易搞混的一组命令。我按场景来说:场景一:改错了工作区文件,还没 add。用git checkout -- <文件>或git restore <文件>丢弃工作区改动,恢复到暂存区或最后一次 commit 的状态。这个操作不可逆,改之前想清楚。场景二:add 错了,想从暂存区拿出来。用git reset HEAD <文件>或git restore --staged <文件>,把文件从暂存区移回工作区,改动还在。场景三:commit 错了,想撤销这次提交但保留改动。用git reset --soft HEAD~1,撤销 commit,改动回到暂存区。如果想连暂存区也撤销,用git reset --mixed HEAD~1(默认)。如果想连工作区改动一起丢弃,用git reset --hard HEAD~1,这个最危险,改动全没。场景四:已经 push 到远程的提交要撤销。不能用 reset,因为会改写历史,别人拉取时会出问题。应该用git revert <commit>,它创建一个新的 commit 来抵消目标 commit 的改动,历史是向前的,安全。命令影响范围是否改写历史适用场景git reset --soft仅移动 HEAD是本地撤销 commit,保留改动git reset --mixedHEAD + 暂存区是本地撤销 commit 和 addgit reset --hardHEAD + 暂存区 + 工作区是彻底丢弃本地改动git revert新增反向 commit否撤销已推送的提交git checkout --工作区否丢弃未 add 的改动我个人的原则是:只要提交推送过,一律用 revert;只有本地未推送的提交,才考虑 reset。这样最安全,不会给协作者添麻烦。4. 协作场景下的实战流程与冲突处理4.1 多人协作的标准工作流团队协作最怕的就是各写各的,最后合并时冲突一大堆。我们团队现在用的流程是:主分支(main)保持稳定,每个人从 main 拉一个新分支开发,开发完推送到远程,发起合并请求,代码审查通过后合并回 main。具体操作:git checkout main git pull origin main # 确保本地 main 最新 git checkout -b feature/xxx # 创建功能分支 # ... 开发 ... git add . git commit -m "feat: 完成 xxx 功能" git push origin feature/xxx # 推送到远程同名分支然后在代码托管平台上发起合并请求。审查期间如果 main 有更新,需要把 main 的最新改动同步到功能分支:git checkout main git pull origin main git checkout feature/xxx git rebase main # 或 git merge main # 解决冲突后 git push origin feature/xxx --force-with-lease这里用--force-with-lease而不是--force,是因为前者会在推送前检查远程分支有没有别人的新提交,如果有就拒绝推送,防止覆盖别人的工作。--force则是无脑覆盖,危险。4.2 冲突产生的根源与解决思路冲突的本质是:两个分支修改了同一个文件的同一区域,Git 无法自动判断该保留哪个。常见于多人改同一个函数、同一个配置文件、同一段样式。解决冲突的核心思路是:理解双方的改动意图,手动合并成最终版本。不要无脑选“ours”或“theirs”,那样很可能丢掉别人的功能。冲突文件长这样:<<<<<<< HEAD const API_URL = 'https://api.example.com/v1'; ======= const API_URL = 'https://api.example.com/v2'; >>>>>>> feature/new-api<<<<<<< HEAD到=======之间是当前分支的内容,=======到>>>>>>>之间是合入分支的内容。你需要决定最终保留什么,然后删掉所有标记符号。如果两边都需要,就整合成一行;如果只保留一边,就删掉另一边。解决完所有冲突文件后,git add标记为已解决,然后git commit完成合并。如果是 rebase 过程中冲突,解决后执行git rebase --continue;想放弃就git rebase --abort。注意:冲突解决后一定要跑一遍测试,确认合并结果没有破坏功能。我遇到过冲突解决时手滑删了一行关键代码,本地没测就提交,结果合并后线上出问题。后来养成习惯,冲突解决后至少跑一遍单元测试和本地启动。4.3 拉取代码时 pull 和 fetch 的选择git pull等于git fetch加git merge。它先从远程拉取最新 commit 到本地远程跟踪分支,然后合并到当前分支。git fetch只做前半步,拉取后不合并,你可以先看看远程改了什么,再决定怎么合并。我现在的习惯是:先 fetch,再根据情况 merge 或 rebase。因为直接 pull 如果本地有未提交改动,可能会触发自动合并失败,或者产生意料之外的 merge commit。先 fetch 能看到远程分支和本地的差异,心里有数再操作。git fetch origin git log HEAD..origin/main --oneline # 看远程比本地多了哪些提交 git merge origin/main # 确认后再合并如果本地有未提交改动,pull 之前可以先git stash暂存起来,拉取后再git stash pop恢复。stash 适合临时切换分支或拉取代码时保存工作现场。5. 常见问题排查与避坑经验实录5.1 提交历史乱了怎么收拾提交历史乱通常表现为:commit message 写错了、提交了不该提交的文件、多个小提交想合并成一个、提交顺序不对想调整。改最近一次 commit message:git commit --amend -m "新说明"。这个命令会创建一个新 commit 替换掉原来的,如果已经推送过,需要 force push,所以只建议在未推送时用。合并多个提交:用git rebase -i HEAD~n,n 是最近 n 个提交。交互界面里把想合并的提交前面的pick改成squash或s,保存后会让你编辑合并后的 commit message。这个操作会改写历史,同样只建议在未推送的个人分支上用。从历史中删除某个文件:如果文件是最近一次提交加的,git rm --cached <文件>然后git commit --amend。如果是更早的提交,需要用git filter-branch或git filter-repo工具,操作复杂且危险,建议提前备份。我踩过的坑:有一次把包含本地数据库密码的配置文件提交了,发现时已经推送。当时手忙脚乱用 reset 回退再强推,结果把同事的提交也覆盖了。正确做法应该是:立即修改密码,然后用 revert 撤销那次提交,再清理历史。密码泄露的第一优先级是改密码,不是改历史,因为历史可能已经被别人拉走了。5.2 误操作后的急救指南Git 大部分误操作都能救回来,关键是别慌,别乱敲命令。误删分支:git reflog查看操作记录,找到删除前的 commit hash,git checkout -b <分支名> <hash>恢复。reset --hard 丢了改动:同样用git reflog找到 reset 之前的 commit hash,git reset --hard <hash>回去。reflog 默认保留 90 天的记录,时间够用。commit 后想撤销:参考第 3.4 节的 reset 和 revert 区别。push 错了想撤回:如果远程分支只有你在用,可以git reset --hard <目标commit>后git push --force-with-lease。如果有别人在用,用 revert 创建反向提交。git reflog是 Git 的“后悔药”,它记录了 HEAD 和分支指针的所有移动。我建议每个开发者都记住这个命令,关键时刻能救命。5.3 常见问题速查表问题现象可能原因解决方法push 被拒绝,提示 non-fast-forward远程有本地没有的提交先git pull --rebase再 push切换分支时提示有未提交改动工作区或暂存区有改动git stash暂存,或先 commit合并时冲突文件太多分支分叉太久定期从 main 同步到功能分支commit 后发现漏了文件提交不完整git add漏的文件后git commit --amend克隆仓库特别慢仓库历史太大用--depth 1浅克隆,只拉最新一次提交不小心提交了大文件仓库体积暴涨用 filter-repo 清理,或重建仓库中文文件名显示乱码编码配置问题git config --global core.quotepath false换行符警告Windows 和 Unix 换行符差异git config --global core.autocrlf input提示:浅克隆(--depth 1)适合只需要最新代码、不关心历史的场景,比如 CI 构建。但浅克隆的仓库不能直接 push,需要先git fetch --unshallow补全历史。5.4 我总结的几条实操心得第一,提交前必看 status 和 diff。这个习惯帮我避免了无数次误提交。多花十秒钟确认,省得后面花十分钟回退。第二,commit message 写清楚“为什么”而不是“做了什么”。代码本身能看出做了什么,但看不出为什么这么做。比如“修复登录 bug”不如“修复并发登录时 token 覆盖导致登出”,后者对排查问题的人更有价值。第三,功能分支及时同步主分支。不要等开发完了才合并,那时候冲突可能已经堆积如山。每天开工前从 main rebase 一次,冲突小且容易解决。第四,危险操作前先建备份分支。比如要 rebase 或 filter-branch 之前,git branch backup-xxx留个后路。出问题了直接切回备份分支,比重头再来强。第五,团队统一工作流。merge 还是 rebase、分支怎么命名、commit message 什么格式,这些最好团队内达成一致。工具层面可以用 commitlint、husky 做提交前检查,减少人为失误。Git 这东西,命令就那么多,但组合起来能应对的场景千变万化。我的建议是先把四区模型和 HEAD 指针理解透,然后遇到问题多查git help <命令>和官方文档,少复制粘贴来路不明的命令。每次操作前想清楚“这个命令会移动哪个指针、影响哪个区域”,大部分坑都能提前避开。