Git 三区域模型:把 add、commit、reset、merge 一次讲透

引子

改了两小时代码,页面样式全乱了,想退回上一个能跑的版本,却发现根本没有可退的地方------这种体验大概每个人都经历过。

Git 解决的就是这件事。它命令不多,难点也不在命令本身,而在于要先把模型建起来 。这篇笔记按我的实际使用顺序整理:安装、存档、回滚、分支合并、远端备份。

一个前提:Git ≠ GitHub

这一点必须放在最前面说,因为太多人在这里绕不出来。

Git 是本地软件 ,管的是你电脑上这个项目文件夹的版本记录。GitHub 是远端网站 ,管的是把代码连同版本记录备份到云端,顺带支持多人协作和开源分享。

一个本地、一个远端。搞清楚这个分工,后面很多困惑会自动消失。

三个区域:理解 Git 的唯一钥匙

一个被 Git 管理起来的项目,可以拆成三块:

  1. 工作区 (working directory / working tree):你能看到、正在编辑的那些文件;
    2.** 暂存区**(staging area / index):准备提交的改动先放这儿;
  2. 提交历史 :已经存档的一个个 commit。
    工作区是你能直接看见的;暂存区和提交历史都在隐藏的 .git 文件夹里,平时不用管,知道它们在里面就行。
    git init 做的事情,就是在当前目录创建一个 .git 文件夹。它的输出会明确告诉你这一点,而这个文件夹里其实已经包含了不少 Git 初始化时准备好的文件------所以"仓库是空的"只是指还没有任何 commit。
    一次存档固定两步:
    git add <文件> git commit -m "<说明>"
    把改动放进暂存区的动作,英文叫 stage ;放进去了是 staged ,还没放是 unstaged 。这也是 git status 输出里那些词的含义。
    为什么要多一个暂存区? 因为可以一点点挑。提交单个文件时它的价值不明显,但当一次重构产生了几十个文件的改动,你就可以分批次把想提交的挑出来、确认后再统一提交,而不是被迫把手上所有东西一次性塞进去。现在大量代码由 AI 生成,一次改动几十个文件太常见了,git add . 直接全加也是常规操作。

回滚:先看改动停在哪一步

情况一:改动只在工作区。

git status 会显示 Changes not staged for commit。撤销:

git restore .

想看具体改了什么,git diff 会逐行对比:- 是删除的行,+ 是新增的行。j/k 上下移动,f/b 翻页,q 退出。

情况二:改动已经进了暂存区。

状态变成 Changes to be committed。分两步撤:

git restore --staged . git restore .

第一步只把改动从暂存区拿回来,文件内容不变;第二步才真正撤销改动。

情况三:改动已经变成 commit。

git reset --hard HEAD~1

HEAD 是最新 commit 的指针,每提交一次就往前挪一格;HEAD~1 就是它前面那一个。--hard 表示三个区域一起回到那个位置------工作区、暂存区、提交历史全部对齐到旧 commit 的状态,最新那个 commit 相当于被删掉了。

这个命令很彻底,工作区里其他没提交的改动也会消失 ,执行前一定要先 git status 确认。

reset 有三种模式,区别只在于影响哪些区域:

| 模式 | 提交历史 | 暂存区 | 工作区 |

| ---- | ---- | ---- |

| --soft | 回滚 | 保留 |保留 |

| --mixed(默认) | 回滚 | 回滚 | 保留 |

| --hard | 回滚 | 回滚 | 回滚 |

不写选项就是 --mixed。想改完重新提交,用 --soft 或 --mixed(commit 撤了但改动还在);想彻底不要,用 --hard。

另一种做法是 git revert HEAD------不删历史,而是新增一个内容相反的 commit 去抵消它。加过的行删掉、改过的字符改回来。好处是原 commit 还在,哪天想恢复那个功能还能找回来。命令会弹 vim,直接 :wq 退出即可。

分支:一个贴在 commit 上的标签

git log 里 HEAD 旁边那个 master(或 main)就是分支名。分支本质上是贴在 commit 上的标签 ,每提交一次就往前挪一步,从标签往回追溯就得到一串 commit,也就是这个分支的历史。

master/main 是特殊分支,一般被视为项目主分支,大家期望它随时可用。而一个功能常常要拆成好几个 commit 才能完成,如果直接在 master 上提交,中间那些半成品 commit 会一起进主分支------用户点了还没实现的按钮就可能报错。

所以做法是从稳定的 master 拉一条新分支,功能相关的提交都落在新分支上:

git switch -c feature/due-date

顺带一提:新分支从 master 最新 commit 岔出时,两边指向同一个 commit,文件内容完全一致------第一次建分支的人常以为项目发生了变化,其实没有。区别要等新分支产生新 commit 才显现。

另外,git branch 只创建分支、不切换分支,这是两件事。老教程的 git checkout -b 与 git switch -c 效果相同,但 checkout 职责太杂、容易混,现在更推荐 switch -c。

合并与冲突

`git switch master

git merge feature/due-date`

合并有两种情形:

- 快进(fast-forward): 新分支就是接着 master 当前 commit 往后写的,master 直接前移即可,不产生新 commit;

- 非快进: master 之后也往前走了,直接前移会丢掉别人的提交,Git 就新建一个 commit 把两边合起来。没有冲突时它自动提交;有冲突就停下来等你。

冲突 发生在两个分支改了同一处位置、且改法不同的时候。Git 不知道留哪个、也不知道谁前谁后,只好把两边方案都摆出来问你。

冲突文件里会出现三行标记,把内容分成上下两块:上面是当前分支(HEAD)的,下面是被合并分支的。解决办法就是删掉三行标记,把内容改成最终想要的样子------两个都要就都留下。

改完还要提交这一步:

`git add .

git commit -m "合并并解决冲突"`

git status 会给出提示:先解决冲突、再 git commit;不想合了还能 git merge --abort。

Rebase:让冲突早一点出现

功能做了两周、几十个 commit,一直等到最后才合并,冲突可能一大堆,甚至发现自己的实现跟主分支根本不兼容,得推倒重写。

所以更稳妥的做法是:开发过程中定期把分支挪到 master 最新位置后面。这个动作叫 rebase(re + base,重新换地基)。

git rebase master

它不是把整条分支一次性搬过去 ,而是把分支上的每个 commit 逐个在新地基上重做一遍。中途冲突解决后:

`git add .

git rebase --continue`

注意 rebase 场景下不是 git commit。完成后显示 Successfully rebased,剩下的 commit 自动继续(没冲突的悄悄就完成了)。之后再合回 master,通常就是一次快进,一行搞定。

好处很实在:没做完的代码一直留在自己分支,不污染主分支;master 的新代码随时能拿到;冲突当场解决;万一实现方向不兼容,也能提前发现。

Stash 与 Worktree

Stash :改到一半要临时切分支处理别的事,Git 会拦你------Your local changes would be overwritten。因为切分支要改写工作区文件,你未提交的改动会被覆盖,它索性停下来让你自己决定。

Git 给了两条路:提交成 commit(但那是半成品,不理想)或者 stash。

`git stash # 把工作区和暂存区未提交的改动整体收进 stash

git stash pop # 取回工作区`

stash 是 .git 里一块专门存放临时改动的区域。收起后文件回到上次提交的样子,工作区干净,切分支就顺畅了。输出里的 index 指暂存区,WIP 是 Work In Progress(进行中的未提交改动)。

stash 适合"临时打断、很快回来"。如果要同时开发多个分支 ,频繁 stash / pop 就很折腾。

Worktree 解决的是另一个层面的问题:同一时刻一个项目目录只能放一条分支的代码。那就再开一个目录:

`git worktree add ../项目统计 -b feature/statistics master

git worktree list

git worktree remove ../项目统计`

核心一句话:每个 worktree 有独立的工作区和暂存区,但所有 worktree 共用同一份提交历史 。 在哪个目录提交,另一个目录立刻能看到。这也是它跟"复制一份文件夹"的本质区别------复制出来的是两个互相独立的仓库。

要删掉 worktree 时别直接拖进废纸篓,Git 那边还留着记录,需要用命令一起清理。

远端备份:GitHub

SSH Key :GitHub 已不支持账号密码推送。SSH Key 是一对文件:私钥留本地、绝不外传,公钥交给 GitHub。推送时电脑用私钥生成签名,GitHub 用公钥验证签名是否出自配套的私钥------就像你签的字别人模仿不了一样。

`ssh-keygen -t rsa -C "你的邮箱" # 两次回车用默认值

cat ~/.ssh/id_rsa.pub # 复制公钥

ssh -T git@github.com # 测试(首次输入 yes)`

然后在 GitHub 的 Settings → SSH and GPG keys 里添加公钥即可。

推送已有仓库 :在 GitHub 新建一个空仓库(名字不支持中文,中文写在 Description;Public/Private 按需选;三个初始化选项都别勾)。按页面提示:

`git remote add origin git@github.com:用户名/仓库名.git

git push -u origin master`

origin 是远端地址的习惯性别名;-u 绑定本地与远端分支,之后 git push 即可。推送上去的是整个 Git 仓库的历史 ,GitHub 页面上能看到完整的 commit 记录。

日常流程就四步 :改代码 → git add → git commit → git push。

验证备份是否可靠 :把本地文件夹删掉,然后

git clone git@github.com:用户名/仓库名.git 项目名

所有提交记录和文件都回来了。

一点体会

命令是表,模型是里。

真正让 Git 从"背命令"变成"能理解"的,是三个区域这套框架。每次操作前问自己一句:我这次动的是工作区、暂存区还是提交历史?答案清楚了,命令自然就出来了。

相关推荐
高频因子挖掘机39 分钟前
行情监控脚本重启后,怎样快速恢复而不重复拉取数据?
后端·github·api
水饺编程1 小时前
第1章:开发环境搭建,在 Windows 中安装 Git
linux·c语言·汇编·git·ubuntu
idanzk1 小时前
Git 推送 GitHub 报 SSL_READ /src refspec main 不匹配 完整踩坑记录
git·github·ssl
粥里有勺糖1 小时前
视野修炼-技术周刊第135期 | 鼠标跟随宠物
前端·github·aigc
粥里有勺糖1 小时前
拿 Codex 协助清理磁盘,Nice!
前端·后端·github
miofly3 小时前
GitHub 日榜趋势速报 | 2026-10-05
开源·github
m4Rk_3 小时前
【论文阅读】Agent 记忆机制(90):HyperMem——用超图建模长期记忆中的高阶关联
论文阅读·人工智能·学习·开源·github
星恒随风3 小时前
Linux开发工具详解(二):Git版本控制、GitHub协作与GDB调试实战
linux·笔记·git·学习·github
cuijiecheng20184 小时前
Windows 下使用 GitHub Private Repository 管理私人 Visual Studio/C++ 项目
windows·github·visual studio