☰
Git基本操作入门:区域模型、分支合并与远程协作避坑指南
2026/10/26 19:48:16 网站建设 项目流程

版本控制这四个字,第一次听到的时候我以为是给文档改个名字加个日期。直到某天改代码改出了三个"最终版"、两个"最终版_bak",然后把好用的那版手滑覆盖掉,才明白"git基本操作"这件事不是选修课,而是必须尽早补上的基本功。这篇内容面向的是完全没接触过 git 的小白,也适合用过几次但只会add、commit、push三件套、一出问题就慌的人。我会把 git 的区域模型、仓库的构建方式、分支与合并的逻辑、远程协作的动作,以及新手最容易掉进去的坑,全部拆开讲一遍,每个命令都配上前因后果,看完能直接在自己机器上跑通一整套流程。不铺垫概念堆砌,直接上讲台。

1. 先把模型想清楚:Git到底在管什么东西

1.1 从"文件改名"这件小事说起

很多人第一次接触版本控制,脑子里想的是"备份"。于是做法就是复制文件夹:项目、项目_改、项目_改2、项目_最终。这套办法能撑一阵子,撑不住的原因有三个:第一,你不知道哪两个文件夹之间到底改了什么,只能靠肉眼对比;第二,两个人同时改同一个项目,靠复制粘贴合并,几乎必然丢代码;第三,出了问题想回退到"上周三那个能跑的版本",你只能翻文件创建时间,还不一定准。

版本控制解决的就是这三件事:记录每一次变更的差异、支持多人并行修改并安全合并、可以精确回到历史任意一个时间点。git 是这类工具里目前用得最广的一个,它的特点是分布式——每个人本地都有一份完整的提交历史,不依赖某台中心机器才能干活。

这里要建立一个关键认知:git 存的不是"文件的快照列表",而是每一次提交的差异与一份指向内容的对象链。你可以把它想成一本账本,每一笔都记着"这次动了哪些行、加了几行、删了几行",而不是每次都把整本书复印一遍。这个设计带来的直接好处是仓库体积小、历史追溯快,但代价是它的内部模型比"复制文件夹"抽象得多,所以新手必须先理解区域划分,否则会觉得命令是玄学。

1.2 四个区域与三种状态

git 的所有命令,本质上都是在四个区域之间搬运内容。把这张图刻在脑子里,八成的困惑会自动消失。

区域通俗理解对应命令
工作区(Working Directory)你在编辑器里能直接看到、改到的那些文件编辑、新建、删除文件
暂存区(Staging Area / Index)一个"待提交清单",你挑好这次要提交哪些改动git add
本地仓库(Local Repository)已经盖章存档的历史,每次提交是一条记录git commit
远程仓库(Remote)团队共享的那份,用于交换提交git push/git fetch

对应的文件状态也有三种:已修改(modified)表示工作区改了但没进暂存区;已暂存(staged)表示改动已放进清单,等着被提交;已提交(committed)表示已经落到本地仓库的历史里。新手最常见的误解是把add当成"保存",把commit当成"上传"。实际不是:add只是挑货上架,commit才是真正入库,push才是把库里的东西寄给远方的同事。

提示:暂存区这个中间层看起来多余,实则是 git 最实用的设计之一。它让你能把一堆混杂的改动拆成几个语义清晰的提交,而不是一次糊上去。

1.3 分布式与本地全量历史的意义

"分布式"这三个字经常被念但很少被真正理解。它的意思是:你clone一个仓库下来,拿到的不只是最新的文件,而是全部历史。所以断网状态下你照样能提交、能切分支、能看三个月前谁改了哪一行。远程仓库的角色从"唯一的真相来源"变成了"大家交换提交的中转站"。

这个特性带来两个很实际的好处。第一,容错高:中心服务临时不可用,团队工作不会停摆,恢复后补一次推送即可。第二,历史检索快:查某行代码的来历,不用发请求,本地直接算。但它也带来一个副作用——历史是公开的、会跟随仓库走的。你在提交信息里写了不该写的内容,或者把配置里的口令提交上去了,删掉文件再提交一次也删不掉历史里的那条记录。这一点后面第 5 章会专门讲怎么处理。

另外一个容易忽略的点:git 对"文件内容"的识别是按行来的。所以它对纯文本(代码、Markdown、配置)支持得极好,能精确到行级别的差异;但对二进制文件(图片、压缩包、设计稿)只能判断"变了/没变",不能告诉你变了哪儿。这也解释了为什么团队里通常约定:不要把大体积二进制文件塞进代码仓库。

2. 环境配置与第一次提交:把地基打正

2.1 安装完成后必须做的三项配置

装 git 本身没什么技术含量,各系统的包管理器或官方安装包都行。安装完打开终端敲git --version,能打印出版本号就说明 PATH 没问题。真正影响后续体验的是配置,尤其是下面三项,很多人跳过了,结果第一次提交就卡住。

# 1. 身份标识,每次提交都会写进记录里,不能空 git config --global user.name "你的名字" git config --global user.email "you@example.com" # 2. 默认分支名,新版本建议统一成 main git config --global init.defaultBranch main # 3. 换行符处理,跨系统协作的关键,后面 5.4 节详细说 git config --global core.autocrlf input

第一项是强制项,没有名字和邮箱,commit会直接报错。第二项是习惯项,统一成main能避免"为什么我的默认分支叫 master"这种不必要的困惑。第三项是跨平台项,Windows 和类 Unix 系统对行尾的表示不一样,不处理的话,同一次提交在别人机器上会显示"整个文件都改了",非常闹心。

配置完用git config --global --list复查一遍。这里有个细节:--global写的是当前用户级别的配置,存在用户主目录下的.gitconfig里;如果团队有特殊要求(比如公司内网的提交邮箱),可以在具体仓库里用不带--global的命令覆盖,优先级是"仓库级 > 用户级 > 系统级"。我一般建议新手全部用--global,先把全局环境跑顺,遇到需要单独处理的项目再覆盖。

注意:邮箱尽量填一个能识别的,后续排查问题、追溯责任人全靠它。如果用别人的邮箱或者随手填一个错的,将来团队里做代码审查会很尴尬。

2.2 git init 之后目录里多了什么

有了配置,就可以建仓库了。进到项目目录,敲git init,输出一行Initialized empty Git repository in ...。这时候目录里会多出一个隐藏的.git文件夹,里面装着对象库、引用、配置、钩子脚本等等。这个目录就是仓库本体,删掉它,目录就退化成一个普通文件夹,历史全没。

mkdir demo-project && cd demo-project git init ls -a # 能看到 .git git status # 查看当前状态,这是你以后敲得最频繁的命令

很多教程讲到这里会直接跳到add,但我想强调一个习惯:每次动手前先git status。它告诉你三件事——当前在哪个分支、有哪些改动进了暂存区、有哪些改了但没进。这个命令零风险、零副作用,可以无限次敲。我见过太多事故是因为作者不知道自己改了什么文件就一把add .上去。

如果是接手一个已有项目,用的不是init而是clone:

git clone https://git.example.com/team/demo-project.git

clone会自动帮你做三件事:建目录、拉全量历史、把远程地址记成origin并设置好跟踪关系。所以后面直接git pull就行,不用再手动remote add。

2.3 从工作区到仓库:add 与 commit 的节奏

真正开始干活,流程是这样:改文件 → 看状态 → 挑改动进暂存区 → 提交。举个具体的例子,假设你修了一个分页相关的 bug,同时顺手调了日志输出格式。这两件事逻辑上不相关,最理想的做法是拆成两个提交。

# 先看看动了什么 git status git diff # 看未暂存的具体差异 git diff --stat # 只看改了哪些文件、增删多少行 # 只把分页修复相关文件挑进暂存区 git add src/pagination.py git commit -m "fix: 修正页码越界导致返回空列表的问题" # 再把日志格式的改动单独提交 git add src/logger.py git commit -m "style: 统一日志输出格式,补上时间戳"

这里解释两个"为什么"。为什么要分开提交?因为提交是历史的最小检索单位。将来如果发现分页那个改动有问题想撤销,一个git revert就能干净地只回退那一笔,不影响日志格式的改动。如果两件事糊在一个提交里,就只能手工拆,非常麻烦。为什么用git add 文件名而不是git add .?后者会把所有改动一股脑塞进去,包括你调试时临时改的配置、随手删的注释、编辑器生成的临时文件。新手阶段建议老老实实按文件名挑,等对status输出了如指掌了再考虑用-A或.。

如果确实想让所有改动一起进去,也建议加个中间步骤:

git add -A git status # 确认清单里没有不该出现的东西 git commit -m "..."

gstatus这一步不能省。我自己的习惯是把status当成提交前的"安检门",只要清单里出现了意料之外的文件,一律先查清楚再提交,不抱侥幸。

2.4 用 git log 确认自己做了什么

提交完,用git log看历史。默认输出信息量偏大,新手容易看花眼,我推荐先记住两个精简用法:

git log --oneline # 每笔提交一行,含短哈希和提交信息 git log --oneline --graph # 加上分支合并的图形结构 git log --stat # 每笔提交附上改动文件清单和行数变化

一行输出大概是这个样子:a1b2c3d fix: 修正页码越界导致返回空列表的问题。前面那串十六进制就是提交哈希,是这笔提交的全局唯一编号,后续回退、对比、查看详情都靠它。你不用记全,取前 7 位通常就够用。

--stat那个参数值得单独说:它能让你快速判断某笔提交是不是"大改"。如果一笔标着"fix: 小修"的提交显示改了 800 行,多半是顺手格式化了整个文件,或者是把不该提交的东西提交了。这种情况下我通常会进去看一眼git show <哈希>,确认没有误伤。

还有个小技巧:git log -p -1可以直接看最近一笔提交的完整差异。排查"上一次提交到底改了什么"的时候,比翻编辑器历史快得多。

3. 分支与合并:Git最值钱的部分

3.1 分支不过是一个指针

很多人对分支有心理阴影,觉得是"高级功能"。实际上分支是 git 里最轻量的东西之一:它就是一个指向某笔提交的可移动指针。创建分支几乎不消耗时间也不消耗空间,本质上是在.git/refs/heads/下写一个四十字节左右的文件,里面存一个提交哈希。

理解这一点之后,很多行为就顺了。比如"切换到某个分支",本质是把 HEAD 这个"当前在哪"的指针指向那个分支,然后把工作区的文件同步成那个分支对应的内容。再比如"分支之间互不影响",是因为它们各自指向不同的提交,你在 A 分支上提交,只会移动 A 这个指针,B 纹丝不动。

git branch # 列出本地分支,星号表示当前所在 git branch -a # 连同远程分支一起列 git switch -c feature/paging # 新建并切换,等价于老写法 checkout -b git switch main # 切回主分支

这里提一下新旧命令的更替:早期都用git checkout,既能切分支又能恢复文件,一件事两个语义,容易误操作。后来 git 拆出了git switch(只管切分支)和git restore(只管恢复文件)。新手直接用新命令,checkout的坑能少踩一半。

分支命名建议带语义前缀,比如feature/、fix/、chore/,后面跟具体内容。别用test、dev1、zhangsan这种,隔两周自己都想不起来这是干什么的。

3.2 建分支、切分支、合并分支

一个完整的分支生命周期,通常是这样的:从主分支拉出新分支 → 在小分支上开发提交 → 合并回主分支 → 删掉小分支。

# 1. 确保主分支是最新的 git switch main git pull # 2. 拉出工作分支 git switch -c fix/paging-boundary # 3. 改代码,提交若干次 git add src/pagination.py git commit -m "fix: 页码超过总页数时返回最后一页" # 4. 切回主分支并合并 git switch main git merge fix/paging-boundary # 5. 清理 git branch -d fix/paging-boundary

git merge有两种结果,取决于历史形状。如果主分支在分叉后没有新提交,git 会做"快进合并"(fast-forward),直接把 main 指针往前挪,不产生新提交,历史是一条直线。如果主分支在分叉后也有了别人的提交,就会产生一个合并提交(merge commit),需要你写一条合并信息。后者虽然多了一个节点,但它保留了"这批改动是一个完整功能"的边界信息,团队协作里我更倾向于保留。

如果不想产生合并提交,还有一种方式是变基:

git switch fix/paging-boundary git rebase main git switch main git merge --ff-only fix/paging-boundary

变基会把你的提交"搬到"主分支最新提交之后,历史变成一条直线,看起来很漂亮。但它会重写提交哈希,如果这个分支已经推到远程了,别人拉下来就会冲突。所以有个通俗的约定:自己的私人分支可以变基,公共分支不要变基。新手阶段我建议先只用 merge,把这条路走熟了再考虑 rebase。

3.3 冲突产生的真实原因

冲突这件事被传得很吓人,其实原理很简单。假设 A 和 B 都从同一个提交出发,改同一个文件的同几行:A 把某行改成 X,B 把同一行改成 Y。合并的时候,git 会去比较两个分支相对共同祖先的差异,发现同一位置有两份不同的修改,它没法判断谁对谁错,于是停下来问你。

关键在于:git 只做机械比对,不做语义判断。所以两种修改落在不同行上不会有冲突,落在同一行上就有冲突——哪怕两边的意图其实是同一件事。

冲突发生时,终端会明确告诉你哪个文件冲突了,git status里会显示both modified。打开文件,会看到这样的标记:

<<<<<<< HEAD 当前分支上的内容(一般是 main) ======= 被合并分支上的内容(比如 fix/paging-boundary) >>>>>>> fix/paging-boundary

三个记号的含义要记牢:<<<<<<<到=======之间是当前分支的版本,=======到>>>>>>>之间是要合进来的版本。你要做的不是"选一个",而是把它改成最终想要的正确内容,然后把三行标记全部删掉。

3.4 一次冲突解决的完整实录

我按真实操作顺序走一遍。

git switch main git merge fix/paging-boundary # 输出:CONFLICT (content): Merge conflict in src/pagination.py # Automatic merge failed; fix conflicts and then commit the result.

第一步,先看全局:git status,确认冲突文件清单。如果冲突文件很多,可以用git diff --name-only --diff-filter=U单独把冲突文件列出来。

第二步,逐个打开文件处理。假设那个文件里我期望的最终结果是"页码超过总页数时返回最后一页,并且不超过最小值 1",那我应该写:

if page > total_pages: page = total_pages if page < 1: page = 1

而不是简单地把HEAD那半段留着、把另一半删掉。这一点非常重要:冲突解决是重新编写代码,不是做选择题。很多新手习惯性删掉一半保存,结果把别人的修复给弄没了。

第三步,验证。改完跑一遍测试或者至少启动一下服务,确认能跑。这一步经常被跳过,然后合并提交里带着一个语法错误进了主分支。

第四步,标记为已解决并完成合并:

git add src/pagination.py git status # 确认没有 remaining conflict git commit # 不加 -m 也行,会弹出默认的合并信息

如果中途发现搞砸了,想放弃这次合并回到干净状态,用git merge --abort。这个命令会尽量把工作区恢复成合并前的样子,前提是你还没提交合并结果。

提示:冲突文件里的<<<<<<<、=======、>>>>>>>这三类标记,提交前一定要全量搜索一遍。漏掉一处,代码里就会残留一行>>>>>>> fix/xxx,轻则报错重则逻辑错乱。用git diff --check可以自动检查有没有遗留的冲突标记。

4. 远程协作的四个基本动作

4.1 clone 与 remote:先把关系理清

本地仓库和远程仓库的关系是"通过一个别名绑定的"。clone会自动建好这个绑定,名字默认叫origin。如果你是在本地init的仓库,需要手动绑一次:

git remote add origin https://git.example.com/team/demo-project.git git remote -v # 查看已绑定的地址

一个本地仓库可以绑多个远程,常见的是origin加upstream(比如 fork 别人的项目时,origin 指向自己的,upstream 指向原始的)。这个机制的好处是你可以从不同来源拉取。

但要注意,remote add只是登记了地址,不会拉任何东西下来。绑完还得git fetch origin,把远程的提交取到本地,再决定怎么合并。这个顺序很多人搞混,以为加完 remote 就有了。

首次推送时因为本地分支和远程分支还没建立跟踪关系,需要显式指定:

git push -u origin main

-u是--set-upstream的简写,作用是把本地main和远程origin/main关联起来。关联过一次之后,以后在这个分支上直接git push、git pull就行,不用再带参数。这个细节能省掉大量重复输入,值得一开始就养成习惯。

4.2 fetch 和 pull 到底差在哪

这两个命令是最容易混淆的一对,我把它拆开说清楚。

git fetch做的事只有一件:把远程的新提交下载到本地的"远程跟踪分支"(比如origin/main),但不动你的工作区和当前分支。执行完之后,你的代码一行没变,只是本地的origin/main指针更新了。你可以用git log main..origin/main看看别人到底提交了什么,再决定要不要合。

git pull则是fetch加一次合并动作的合体。它先把远程内容拉下来,然后立刻对你的当前分支执行 merge(默认行为,也可以配置成 rebase)。也就是说,它会在你还没看清情况的时候就把代码改了。

命令是否改动工作区是否产生合并适用场景
git fetch否否想先看看别人改了什么再决定
git pull(默认 merge)是可能产生合并提交日常同步,历史允许分叉
git pull --rebase是不产生合并提交想让历史保持一条直线

我个人的习惯是:在可能有大改动的分支上先 fetch 再决定。比如休假回来、或者长期没同步的分支,直接 pull 很容易一上来就是一堆冲突,连发生了什么都不知道。先 fetch,用git log --oneline main..origin/main看一眼清单,心里有数了再合并,从容得多。

还有一点关于--rebase的建议:如果本地有尚未推送的提交,pull --rebase能把它们接到远程最新提交后面,历史干净。但如果本地提交已经推送出去了,再 rebase 会造成远程和本地历史不一致,别人拉取时痛苦。判断标准很简单:这些提交推没推过。没推过,随便整理;推过了,老老实实 merge。

4.3 push 被拒绝的三种常见情形

git push报错是新手最慌的时刻,其实绝大多数就是三种情况,对应三种解法。

第一种:远程有你本地没有的提交。报错关键字通常是rejected、non-fast-forward、fetch first。意思是远程往前走了,你直接推会覆盖别人的工作,git 拦住了。解法是先git pull(或者 fetch 后 merge),把远程提交合进来,再推。

第二种:本地分支没有跟踪远程分支。报错会提示no upstream branch。解法就是前面说的git push -u origin <分支名>。

第三种:权限或者地址问题。报错是Permission denied、Authentication failed、repository not found。这类跟代码无关,去确认账号权限、地址拼写、凭据是否过期。凭据缓存出问题时,清掉本地的凭据管理器记录重新登录通常就能解决。

最需要警惕的是第一种情况下的"暴力解法":git push -f。强制推送会用你的本地历史覆盖远程,别人的提交直接消失。除非这是你个人的分支、并且你清楚知道自己在做什么,否则永远不要用。我见过不止一次因为一个-f把同事半天的工作冲掉的事故。

注意:如果确实需要覆盖,有一种相对安全的写法是git push --force-with-lease,它会在远程被别人更新过的情况下拒绝执行,相当于给你加了一道保险。但新手阶段,最好的选择还是不用。

4.4 提交前的自检清单

协作场景下,提交质量直接决定别人排查问题的成本。我自己养成了一套固定的自检动作,敲三条命令,三十秒内完成。

git status # 有没有不该提交的文件混进来 git diff --cached # 暂存区里的具体改动长什么样 git log --oneline -3 # 最近三笔提交的信息是否清晰

第一条查的是"范围",重点看有没有编辑器临时文件、日志文件、本地配置文件混进清单。第二条查的是"内容",尤其是那些"顺手改的"小地方,比如调试用的print、注释掉的代码块、临时降低的日志级别。这些东西混进提交里,review 的时候非常扎眼。第三条查的是"表达",fix bug、update、修改这类提交信息,三个月后看等于没写。

一条合格的提交信息,我的标准是:能让人不看代码就知道这次动了什么、为什么动。格式上习惯用类型: 描述,类型包括feat(新功能)、fix(修缺陷)、refactor(重构)、docs(文档)、style(格式)、chore(杂项)。描述部分尽量写"结果"而不是"动作",比如"页码越界时返回最后一页"比"修改了判断条件"更有信息量。长度控制在首行 50 字以内,需要详细说明就在空一行后写正文。

5. 新手最容易踩的坑与排查表

5.1 add 错了文件怎么退

这是最高频的失误。刚敲完git add,git status一看,发现把config.local.ini也加进去了。这时候别慌,还没commit就有救。

# 把某个文件从暂存区撤出,保留工作区的修改 git restore --staged config.local.ini # 老写法,效果一样 git reset HEAD config.local.ini

关键理解:--staged只影响暂存区,工作区里的文件内容一个字节都不会变。很多新手以为这是"撤销修改",吓得不敢用,其实它只是把你从清单里拿下来。

如果已经commit了但还没push,处理方式有两种。第一种是"补一次提交":

git rm --cached config.local.ini # 从版本控制中移除,但保留本地文件 git commit -m "chore: 移除误提交的本地配置文件"

第二种是"直接改掉那笔提交":

git restore --staged config.local.ini git commit --amend --no-edit # 用当前暂存区内容替换上一笔提交

--amend很实用,但有个铁律:只对没推送过的提交用。因为它改的是提交本身,哈希会变,推过的提交再 amend,别人拉取时会看到两条平行的历史,非常麻烦。

至于已经推送上去的情况,正确做法不是抹历史,而是新增一笔提交把它删掉(用git rm --cached),然后立刻把那个文件加进.gitignore。历史里那一条改不了,但如果里面是敏感信息(口令、密钥),就得走"更换凭据"这条路——把泄露的口令作废重新生成,比纠结怎么删历史重要得多。

5.2 提交信息写错、漏提交文件

提交信息打错字,或者提交完发现漏了一个文件,用--amend一次解决:

git add forgotten-file.py git commit --amend -m "fix: 正确的提交信息"

这条命令会把当前暂存区的内容并入上一笔提交,同时替换提交信息。也就是说它能同时干两件事:补文件、改信息。同样,只对未推送的提交用。

如果漏掉的文件比较多,或者改动已经乱成一团,还有一种思路是软重置回上一个提交,重新组织:

git reset --soft HEAD~1

--soft的意思是"只把分支指针往回挪一格,暂存区和工作区都保持原样"。执行完你相当于回到了"改动都已暂存但还没提交"的状态,可以重新拆分、重新写信息。这个操作很安全,因为它不丢任何内容。

5.3 .gitignore 不生效的真实原因

.gitignore写了但没用,几乎只有一个原因:那个文件已经被 git 跟踪过了。忽略规则只对"未跟踪文件"生效,一个文件只要进过一次提交,它就在版本控制里了,忽略规则对它无效。

解法是两步,缺一不可:

git rm --cached build/output.log git commit -m "chore: 停止跟踪构建产物"

第一步把文件从版本控制中移除但保留在磁盘上,第二步提交这个"移除"动作。之后.gitignore里那行规则才会真正起作用。

.gitignore的写法也有几个小坑值得说清:

写法含义
*.log忽略所有层级的 .log 文件
/build/只忽略根目录下的 build 目录
build/忽略任意层级的 build 目录
!important.log例外,不忽略这个文件(要放在对应忽略规则之后)
**/temp匹配任意深度的 temp

顺序很重要:.gitignore是自上而下匹配的,后面的规则能覆盖前面的。所以"先忽略全部、再放行个别"的写法,例外那条一定要写在后面。

还有一点经验:项目一开始就建.gitignore,比事后补要省事得多。常见的忽略对象包括构建产物目录、依赖安装目录、日志、本地环境配置、编辑器临时文件。宁可多写几行,也别等到几百兆的依赖目录被提交上去再返工。

5.4 换行符、中文文件名与编码

换行符的问题是这样的:Windows 上文本文件的行尾是两个字符(CRLF),类 Unix 系统是一个(LF)。如果不做处理,一个 Windows 上提交的文件被 Linux 拉下来,git 会认为"每一行都改了",diff全是红色,代码审查根本没法看。

标准做法是在 Windows 上配core.autocrlf=true(提交时转 LF,检出时转 CRLF),在类 Unix 上配input(提交时转 LF,检出时不转)。团队统一配置后,仓库里存的永远是 LF,各系统自己适配。更严谨的项目会在根目录放一个.gitattributes文件,把规则固化在仓库里,避免每个人配置不一致:

* text=auto *.sh text eol=lf *.bat text eol=crlf

中文文件名和中文路径的问题,根源在core.quotepath这个配置。默认情况下 git 会把非 ASCII 字符转成八进制转义,git status里就显示成"\344\270\255\346\226\207.txt"这种。关掉就好:

git config --global core.quotepath false

至于提交信息里的中文,正常用没问题,前提是终端和编辑器的编码统一是 UTF-8。如果git log里中文显示成乱码,先检查终端编码,而不是急着改仓库配置。

5.5 reset / restore / revert 速查

最后整理一张表,把这几个最容易混的命令放在一起对比。它们的共同点是"回退",但作用的位置和影响完全不同。

命令移动分支指针影响暂存区影响工作区适合场景
git reset --soft HEAD~1是否否撤销提交但保留全部改动,重新组织
git reset --mixed HEAD~1是是(清空)否默认模式,改动退回工作区重新挑选
git reset --hard HEAD~1是是是(丢弃)彻底丢弃,危险,改动不可恢复
git restore <文件>否否是(丢弃)单个文件放弃修改
git restore --staged <文件>否是否把文件撤出暂存区
git revert <哈希>否(新增反向提交)是是撤销已推送的提交,历史保留

理解这张表的关键在于抓住"三个位置":分支指针、暂存区、工作区。--soft只动第一个,--mixed动前两个,--hard三个全动。--hard之所以危险,是因为工作区里的未提交改动会被直接抹掉,没有任何回收站。

git revert的逻辑跟它们完全不同:它不删除历史,而是新增一笔"反向操作"的提交,把这笔修改抵消掉。所以它是唯一适合处理已推送提交的方式——历史是只增不改的,别人才不会受影响。

提示:如果手滑执行了reset --hard且还没关终端,可以用git reflog找回来。reflog记录了 HEAD 的每一次移动,找到操作前的那条记录,git reset --hard <那条哈希>就能恢复。这是最后的救命稻草,但它只对"已提交过的内容"有效,从没提交过的工作区改动,谁也救不了。

我自己的做法是:给reset --hard加一层心理缓冲,敲之前先把git status和git diff各看一遍,确认没有未提交的改动;如果确实有,先git stash存一份再动手。stash这个命令新手可以早点学会,它像是一个临时抽屉,把当前改动先收起来,需要时再git stash pop拿出来,配合各种回退操作非常好用。

至于开头提到的那种"项目_改2_最终版"的目录,现在应该可以彻底告别了。真正让我觉得这套东西值得花时间学的,不是它有多少命令,而是它把"我改了什么、为什么改、什么时候改的"这件事变得可查、可回溯。踩过几次坑之后,我基本养成了三个肌肉记忆:动手前status、提交前diff、推送前log。这三条不需要记任何参数,但能挡掉新手阶段八成的事故。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询