Git 核心原理与命令实战:三区模型、回退与冲突处理
Git 核心原理与命令实战:三区模型、回退与冲突处理
我最初对 Git 的态度和很多人一样:会 add、会 commit、会 push,就觉得够用了。直到有次改错了仓库里的配置文件需要回退,我在 reset 和 revert 之间犹豫了半天,最后选了 reset 强推上去,顺手把同事当天的一条提交也干掉了。那次之后我把 Git 的底层模型认真补了一遍,就是这篇的内容。
一、Git 解决的是什么问题
Git 是分布式版本控制系统。它记录文件内容的变化,可以回退到任意历史版本、对比差异、多人协作不互相覆盖,并且追踪每次修改的作者和时间。
它和 SVN 这类集中式的核心区别在”副本”上:
| 对比项 | 集中式(SVN) | 分布式(Git) |
|---|---|---|
| 版本库位置 | 只在中央服务器 | 每个开发者都有完整副本 |
| 离线操作 | 只能查看,不能提交 | 可以提交、建分支、看历史 |
| 单点故障 | 服务器挂了全员停摆 | 任何一份克隆都能恢复 |
| 分支成本 | 昂贵,相当于复制目录 | 极低,本质只是加一个指针 |
| 速度 | 依赖网络 | 大部分操作在本地完成 |
工作里有个直观的体现:SVN 时代回滚要连服务器、要权限;Git 时代我在本地就能把整个历史翻个底朝天,出问题不慌。Git 存的是完整快照,不是差异——文件没变,就只保留一个指向前一版本的链接,所以它才敢”每个提交都是全量状态”。
二、三区模型:所有命令都能对上位置
学 Git 最大的分水岭是搞懂三个区。搞懂了,命令就不用背,靠推理就能想出来:
1 | 工作区(正在编辑的文件) |
- 工作区:我正在编辑的目录;
- 暂存区:
git add之后、还没提交的内容,相当于”这一版要提交哪些文件”的清单; - 本地仓库:
git commit之后进入.git的完整历史。
文件对应四种状态:Untracked(未跟踪)→ git add → Staged(已暂存)→ git commit → Committed(未修改)→ 再编辑 → Modified(已修改)。
git status -s 的输出符号值得记一下,排障时全靠它:
| 标志 | 含义 |
|---|---|
?? |
未跟踪的新文件 |
A |
已暂存的新增文件 |
M |
左列已暂存修改,右列未暂存修改 |
D |
删除 |
UU |
合并冲突,未解决 |
三、日常操作链路
1 | # 初始化与克隆 |
提交信息这件事,团队里最好一开始就定规矩:type(scope): 描述,type 用 feat / fix / docs / style / refactor / test / chore / perf / ci / revert。运维改动的仓库我一般直接用 OPS: 前缀,比如 OPS: 调整 nginx upstream 配置——以后用 git log --grep 筛运维改动时非常省事。
四、配置与 SSH 密钥
配置分三层,优先级是 本地 > 全局 > 系统:
1 | git config --global user.name "Your Name" |
SSH 密钥现在推荐 ed25519,比 RSA 短、更安全:
1 | ssh-keygen -t ed25519 -C "you@example.com" |
两个高频报错的排查方向,记下来能省不少时间:
Permission denied (publickey):先ssh -T测密钥,多半是公钥没配或换了机器;LF will be replaced by CRLF:检查core.autocrlf,Windows 和 Linux 混着提交时最容易出。
五、历史、差异与回退
1 | # 查看历史 |
这里有一条生产铁律,也是我那次翻车换来的:已经推送到远程的提交,回滚一律用 revert,不用 reset。reset 会重写历史,别人仓库里对不上号,麻烦比原来的问题还大。reset 只用在本地、没推过的提交上。
另外一个值得练的习惯:回退前先 git log --oneline -5 确认自己在哪个位置,回退目标是什么。误操作大多不是命令不会敲,而是根本没看清当前位置。
六、分支管理与工作流
分支的本质是”指向某个提交的可变指针”,创建分支只是新建一个指针,所以非常轻量。
1 | git branch # 本地分支 |
分支命名我见过两种典型风格,推荐带上用途和编号,看着就知道这条分支在干嘛:
1 | feature/JIRA-123-user-login # 新功能 |
团队工作流常见三种,选型看发布节奏:
| 策略 | 分支模型 | 适用场景 | 复杂度 |
|---|---|---|---|
| Git Flow | main + develop + feature + release + hotfix | 有明确发布周期的项目 | 高 |
| GitHub Flow | main + feature,合并即部署 | 持续部署的 Web 项目 | 低 |
| Trunk-Based | 所有人直接提交 main,短命分支 | 高频发布、CI/CD 成熟的团队 | 中 |
我经手的两个项目就是两个极端:老项目用 Git Flow,发布节奏一月一次;新项目 GitHub Flow,随时合并随时上线。选错的代价很直接——小团队硬上 Git Flow,光流程成本就够喝一壶。
七、合并与冲突处理
合并有三种模式,日常最需要记住的是 --no-ff:
1 | git merge feature/login # 默认 fast-forward,指针直接前移 |
fast-forward 的缺点是分支信息没了,以后看历史不知道这段代码从哪条分支来;--no-ff 会生成一条合并提交,历史里能看出”这里合并过一个功能分支”。团队协作场景我一般要求开 --no-ff。
冲突的产生条件很明确:两个分支改了同一文件的同一处,Git 无法自动裁决。冲突文件里的标记长这样:
1 | <<<<<<< HEAD |
解决流程六步:合并出现 CONFLICT → git status 找到冲突文件(UU 标记)→ 编辑文件、删掉标记保留正确内容 → git add 标记已解决 → git commit 完成合并 → 后悔就 git merge --abort。
1 | # 列出冲突文件 |
冲突不止 merge 一种场景,这是我踩过才整理全的:
| 场景 | 触发命令 | 解决方式 |
|---|---|---|
| merge 冲突 | git merge feature | 改文件 → git add → git commit |
| rebase 冲突 | git rebase main | 改文件 → git add → git rebase –continue |
| cherry-pick 冲突 | git cherry-pick xxx | 改文件 → git add → git cherry-pick –continue |
| pull 冲突 | git pull | 改文件 → git add → git commit,或先 stash 再 pull |
| stash pop 冲突 | git stash pop | 改文件 → git add → git stash drop(记录不会自动删) |
| 二进制文件冲突 | 两分支都改了 png/jar | 无法合并内容,只能选一侧或重新生成 |
二进制冲突没法”改文件”,只能二选一:
1 | git checkout --ours app.jar # 保留当前分支版本 |
解决冲突后有个动作绝不能省:本地编译 / 测试跑一遍再提交。冲突标记删干净只是格式上对了,逻辑可能已经被改得前后矛盾——我见过一次合并后登录模块直接失效,就是两边各保留了一半的校验逻辑。
八、远程仓库与几个救场命令
1 | git remote -v # 看远程地址 |
几个高频救场命令:
1 | # rebase:把当前分支的提交"移植"到新基点上,历史线性但会重写 SHA |
两条经验规则:永远不要 rebase 已推送到远程的公共分支(历史重写会让所有人对不上);永远不要对公共分支用 --force,真要强推就用 --force-with-lease,它会在远程有新提交时拒绝覆盖。发布时打 tag 配合平台的 Release 功能,回滚目标一目了然。
九、图形化工具要不要用
命令行是基本功,但图形化工具在两类场景下确实好用:看分支拓扑、给不熟命令行的同事演示。SourceTree 这类工具内置了图形化历史视图和冲突对比界面,排查”谁在什么时候改了什么”比 git log --graph 直观。注意 Windows 上装完默认用 PuTTY 的 SSH,如果之前用 Git Bash 生成过密钥,要在设置里把 SSH 客户端切成 OpenSSH,否则会报 host key 相关的认证失败。
不过服务器上操作永远以命令行为主,GUI 只是本机辅助。
小结
回退类操作的选择可以浓缩成一句话:工作区改错了用 checkout --,暂存区放错了用 reset HEAD,提交错了但没推用 reset,推出去的用 revert。把这句话记住,再配合”动手前先 git status 和 git log 看一眼”,Git 的事故率会下降一大截。


