Git 版本控制
写代码最痛的瞬间是什么?“昨天还能跑,今天怎么不行了?“——你忘了改了什么。或者更糟——多人协作时,A 改了 UserService.java,B 也改了 UserService.java,互相覆盖一团糟。版本控制系统(Version Control System, VCS) 就是为解决这个问题而生——它记录每次修改,让你随时回退、并行开发、合并代码。
Git 是 Linus Torvalds(Linux 之父)2005 年开发的分布式 VCS,现在是事实标准。这一章我们看 Git 怎么用、怎么协作。
一、为什么需要 Git
设想一个不用版本控制的团队:
- 备份靠复制 ——
UserService.java.bak、UserService.java.bak2、UserService_final.java、UserService_真的final.java……文件爆炸。 - 合并靠人工 —— A 把文件发给 B,B 手动 diff 合并,改到吐血。
- 回退靠记忆 —— “上周三我改了哪行?“——没人记得。
- 协作靠喊 —— “你别动这个文件,我在改!“——一不喊就冲突。
Git 解决这些问题:
- 完整历史 —— 每次提交(commit)都是个快照,能回到任意历史版本。
- 分支并行 —— 开新分支不影响主分支,多人各自开发。
- 自动合并 —— 同文件不同位置改动自动合并,冲突才需人工。
- 远程协作 —— push/pull 同步到远程仓库,团队共享。
二、Git 基础
2.1 三个区域与三种状态
Git 有三个核心区域:
- 工作区(Working Directory) —— 你看到的目录文件。
- 暂存区(Staging Area / Index) —— 准备提交的快照。
- 版本库(Repository) —— 提交历史。
对应三种状态:
工作区 --git add--> 暂存区 --git commit--> 版本库
(modified) (staged) (committed)
2.2 基本工作流
# 1. 初始化仓库
git init # 在当前目录创建 .git/
git clone https://github.com/user/repo.git # 克隆远程仓库
# 2. 配置 (一次性)
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
# 3. 日常工作流
git status # 查看状态
git add Main.java # 加入暂存区
git add . # 加入所有改动
git commit -m "修复登录bug" # 提交到版本库
git log --oneline -5 # 查看最近 5 次提交
# 4. 同步远程
git push origin main # 推送到远程 main 分支
git pull origin main # 拉取远程更新
git fetch # 拉取但不合并
提交信息(commit message)的好习惯:
- 用祈使句:
修复登录bug,不是修复了登录bug。 - 第一行简短(<50 字),空一行再写详细说明。
- 描述”为什么”而不是”做了什么”——diff 已经说明做了什么。
- 用前缀分类:
feat:、fix:、docs:、refactor:、test:。
fix: 修复用户登录时密码为空导致的 NPE
UserService.verify 在 password 为 null 时直接调用 equals,
导致 NPE. 提前判空返回 false.
2.3 .gitignore
有些文件不该进版本库——编译输出、IDE 配置、密钥。.gitignore 文件指定忽略规则:
# 编译输出
target/
build/
*.class
# IDE
.idea/
*.iml
.vscode/
.settings/
# 系统
.DS_Store
Thumbs.db
# 日志
*.log
logs/
# 敏感 (千万别提交!)
.env
application-local.yml
*.pem
*.key
# 依赖
node_modules/
github.com/github/gitignore 有各种语言的现成模板。密钥类文件千万别提交——一旦推到 GitHub 公共仓库,爬虫秒抓,必须立即吊销。
三、分支管理
分支是 Git 的灵魂——它让”并行开发”成为可能。每个分支是个独立的时间线,互不干扰。
3.1 基本分支操作
git branch # 查看本地分支
git branch -a # 查看所有分支 (含远程)
git branch feature-login # 创建分支
git checkout feature-login # 切换分支
git checkout -b feature-login # 创建并切换 (常用)
git switch feature-login # 切换分支 (新命令, 比 checkout 语义清晰)
git switch -c feature-login # 创建并切换
git branch -d feature-login # 删除分支 (已合并才能删)
git branch -D feature-login # 强制删除
git push origin --delete feature-login # 删除远程分支
3.2 分支策略
常见分支模型:
main/master—— 生产分支,永远可部署。develop—— 开发主线,集成各 feature 分支。feature/*—— 新功能分支,从 develop 拉出,完成后合并回。hotfix/*—— 紧急修复,从 main 拉出,合并回 main 和 develop。release/*—— 发布准备。
GitHub Flow(简化版):
main是生产分支。- 任何改动从
main拉新分支(feature/xxx)。 - 完成后发 PR,Code Review 通过后合并回
main。 - 没有
develop/release,足够小团队用。
3.3 合并 vs Rebase
把分支合并回主干有两种方式:
Merge——保留分支历史,创建合并提交:
git checkout main
git merge feature-login
# 生成 merge commit, 历史是分叉+合并的形状
* merge commit (main)
|\
| * feature commit 3 (feature-login)
| * feature commit 2
| * feature commit 1
|/
* previous main commit
Rebase——把分支提交”挪”到主干顶部,历史是直线的:
git checkout feature-login
git rebase main # 把 feature 的提交"重放"在 main 顶部
git checkout main
git merge feature-login # fast-forward, 直线历史
* feature commit 3 (main)
* feature commit 2
* feature commit 1
* previous main commit
| 方式 | 历史 | 优点 | 缺点 |
|---|---|---|---|
| Merge | 分叉+合并 | 保留完整上下文 | 历史杂乱 |
| Rebase | 直线 | 历史清晰 | 改写了提交哈希 |
经验:
- 团队主分支用 merge(保留上下文)。
- 个人 feature 分支更新主干时用 rebase(避免无谓的 merge commit)。
- 永远不要 rebase 已推到公共分支的提交——会改写历史,别人 pull 时冲突爆炸。
3.4 解决冲突
当两个分支改了同一行代码,合并时冲突:
git merge feature-login
# Auto-merging UserService.java
# CONFLICT (content): Merge conflict in UserService.java
打开文件,会看到冲突标记:
public void login(String name) {
<<<<<<< HEAD
System.out.println("main 分支版本");
=======
System.out.println("feature 分支版本");
>>>>>>> feature-login
}
手动选择保留哪个(或合并两者),删掉冲突标记,再 git add + git commit。
git mergetool # 用图形工具解决冲突
git add UserService.java
git commit # 完成合并
git merge --abort # 放弃合并, 回到合并前状态
减少冲突的策略——频繁同步 + 小步提交 + 短生命周期分支。一个分支活两周,冲突必然大;活两天,冲突就小。
四、远程协作
4.1 远程仓库
git remote -v # 查看远程仓库
git remote add origin https://github.com/user/repo.git # 添加远程
git remote remove origin # 移除
git remote set-url origin URL # 修改 URL
git push origin main # 推送
git push -u origin feature # 推送并设置上游 (后续 git push 不用指定)
git pull # 拉取并合并 (等价 fetch + merge)
git fetch # 拉取但不合并, 看 git log origin/main 看远程状态
4.2 SSH vs HTTPS
# HTTPS: 每次要输密码 (用 token 代替密码, GitHub 2021 起强制)
git clone https://github.com/user/repo.git
# SSH: 配好 key 后免密, 推荐
git clone [email protected]:user/repo.git
配 SSH key:
ssh-keygen -t ed25519 -C "[email protected]"
# 一路回车, 生成 ~/.ssh/id_ed25519 (私钥) 和 .pub (公钥)
# 把 .pub 内容粘到 GitHub -> Settings -> SSH and GPG keys
4.3 Pull Request (PR) 工作流
GitHub/Gitee 的核心协作机制——PR(Pull Request):
- Fork 主仓库到自己的账号(如果是开源贡献)。
- Clone 自己的 fork 到本地。
- 新建分支
feature/xxx,开发。 - Push 到自己的 fork。
- 在 GitHub 上发 Pull Request 到主仓库。
- 维护者 Code Review,提意见,你修改。
- 通过后 Merge 到主分支。
Code Review 是 PR 的核心——同事看你的代码,提建议、找 bug、统一风格。这是团队代码质量的关键保障。
# 同步上游 (主仓库) 的更新到自己的 fork
git remote add upstream https://github.com/original/repo.git
git fetch upstream
git checkout main
git merge upstream/main
git push origin main
五、实战:常用命令速查
由于 Git 是命令行工具,无法在 Piston 运行,下面用 Java 模拟一个简易提交历史——展示 commit/branch/merge 的概念。
观察重点:
main和feature/login是两条独立时间线——切分支不影响对方历史。merge创建合并提交——它有两个父提交,是两条分支的交点。log倒序遍历——从最新提交往回追父提交。- 冲突不在模拟里——真实 Git 同行改动才冲突,要手动解决。
六、撤销操作
撤销是 Git 最常用的”后悔药”:
# 撤销工作区改动 (未 add)
git restore Main.java # 新命令
git checkout -- Main.java # 老命令
# 撤销暂存 (已 add, 未 commit)
git restore --staged Main.java
git reset HEAD Main.java
# 撤销最近一次提交 (保留改动)
git reset --soft HEAD~1 # 改动留在暂存区
git reset HEAD~1 # 改动留在工作区 (默认 --mixed)
git reset --hard HEAD~1 # 完全丢弃改动 (危险!)
# 用新提交撤销旧提交 (不改历史, 安全)
git revert HEAD # 创建一个"反向"提交
# 暂存当前改动 (不提交, 留着以后用)
git stash # 保存当前工作区
git stash list # 查看
git stash pop # 取回最近一个
reset vs revert:
reset改写历史——把 HEAD 移到指定提交。只用于本地未推送的提交。revert创建新提交——历史里多一个”反向”提交,原提交还在。用于已推送的公共提交。
铁律:已推送到公共分支的提交,永远用 revert 不用 reset——改写历史会让其他人 pull 时爆炸。
七、高级技巧
7.1 cherry-pick
只挑某个分支的某个提交过来:
git cherry-pick abc1234 # 把 abc1234 这个提交应用到当前分支
场景:hotfix 分支修了个 bug,要同步到 develop,但不想合并整个分支——只 cherry-pick 那个修复提交。
7.2 stash
临时切换任务,当前改动没提交又不想丢:
git stash # 保存当前改动到栈
git checkout other-branch
# 干别的活...
git checkout original-branch
git stash pop # 取回
7.3 tag
打标签标记发布版本:
git tag v1.0.0 # 轻量标签
git tag -a v1.0.0 -m "正式版" # 附注标签 (推荐)
git push origin v1.0.0 # 推送标签
git tag # 查看标签
7.4 reflog
Git 的”后悔药之后悔药”——记录所有 HEAD 移动,连 reset 删掉的提交都能找回:
git reflog # 查看 HEAD 历史
git reset --hard HEAD@{5} # 回到 5 步之前的状态
误删分支、reset 错了都靠它救命。
八、本章小结
| 概念 | 核心要点 |
|---|---|
| 三个区域 | 工作区/暂存区/版本库 |
| 基本流 | add → commit → push |
| 分支 | 独立时间线,并行开发 |
| merge | 保留分支历史,创建 merge commit |
| rebase | 直线历史,不改公共提交 |
| 冲突 | 同行改动需手动解决 |
.gitignore | 排除文件不入库 |
| PR | Pull Request 协作工作流 |
reset | 改写历史,仅本地用 |
revert | 反向提交,公共分支用 |
记忆口诀:
- 三区流转——工作区 add 暂存区 commit 版本库 push 远程。
- 分支即时间线——切分支互不干扰。
- 公共分支用 merge——保留历史。
- 公共提交别 rebase——会改写哈希。
- 撤销本地用 reset,公共用 revert。
- 密钥进 .gitignore——千万别提交。
结语:从单机到协作
这一章我们看了 Git——从单机版本控制到远程协作,从分支并行到 PR Review。Git 是程序员必备工具,越早熟练越受益。下一章我们看另一个工程化利器——测试,用 JUnit + Mockito 让代码质量可量化、可回归。