Git 从入门到实战:一篇搞定所有常用场景

不管你是刚入行的开发者,还是已经工作几年的老鸟,Git 都是绕不开的技能。本文从最基础的概念讲起,覆盖日常开发、团队协作、紧急修复、事故抢救等各种真实场景,力求让你读完就能上手,遇到问题能查能解决。


一、Git 是什么?为什么必须学?

1.1 一句话说清楚

Git 是一个分布式版本控制系统,它记录你代码的每一次变更,让你随时可以回到过去任何一个版本。

类比:如果写代码像写小说,Git 就是"无限撤销 + 分支剧情 + 多人协作"的神器。

1.2 集中式 vs 分布式

集中式(SVN):                              分布式(Git):
┌─────────┐                                ┌─────────┐
│  中央服务器 │◄──── 所有代码都在这            │  远程仓库  │
└────┬────┘                                └────┬────┘
     │                                          │
  ┌──┴──┐                                 ┌────┼────┐
  │开发者│ ← 必须联网才能干活              │开发者A│  │开发者B│  │开发者C│
  └─────┘                                 │(完整)│  │(完整)│  │(完整)│
                                          └─────┘  └─────┘  └─────┘
                                          每人都有完整的代码历史,离线也能工作!

1.3 安装与初次配置

# 安装(Windows 下载 Git-SCM,Mac 用 brew install git)

# 必做的全局配置(告诉 Git 你是谁)
git config --global user.name "你的名字"
git config --global user.email "你的邮箱@example.com"

# 推荐配置
git config --global init.defaultBranch main          # 默认分支名用 main
git config --global core.autocrlf input               # Windows 换行符自动转换
git config --global pull.rebase true                  # pull 默认用 rebase(保持历史整洁)

# 查看当前配置
git config --list

# 设置别名(效率神器)
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.lg "log --oneline --graph --all"

二、核心概念:四个区域和文件状态

2.1 Git 的四个区域

┌─────────────────────────────────────────────────────────────┐
│                    Git 四个区域全景图                          │
├───────────────┬─────────────────────────────────────────────┤
│     区域       │                  说明                        │
├───────────────┼─────────────────────────────────────────────┤
│ 工作区(Working)│ 你正在编辑的文件(磁盘上的实际文件)              │
│ 暂存区(Staging)│ git add 后的文件(即将提交的快照)               │
│ 本地仓库(Local)│ git commit 后的历史记录                       │
│ 远程仓库(Remote)│ GitHub/GitLab 等远程服务器上的仓库              │
└───────────────┴─────────────────────────────────────────────┘

你的代码在这四个区域之间流转:

  工作区 ──git add──► 暂存区 ──git commit──► 本地仓库 ──git push──► 远程仓库
    ▲                                                            │
    │◄──────────── git pull / git clone ◄────────────────────────┘

2.2 文件的四种状态

                    ┌──────────────┐
          ┌────────►│  Untracked   │(未跟踪:Git 还不知道这个文件的存在)
          │         └──────┬───────┘
          │                │ git add
          │                ▼
          │         ┌──────────────┐
  git rm  │         │   Staged     │(已暂存:准备放进下一次提交)
  --cached│         └──────┬───────┘
          │                │ git commit
          │                ▼
          │         ┌──────────────┐     修改文件      ┌──────────────┐
          └─────────│  Committed   │───────────────►│   Modified    │
                    │  (已提交)    │◄─── git add ───│   (已修改)    │
                    └──────────────┘                 └──────────────┘

2.3 最常用的状态查看命令

git status              # 查看当前状态(哪些文件被修改、暂存了)
git status -s           # 简短输出(?? 未跟踪, A 新增, M 修改, D 删除)
git diff                # 查看工作区和暂存区的差异
git diff --staged       # 查看暂存区和最近一次提交的差异
git diff HEAD           # 查看工作区和最近一次提交的差异
git log --oneline -10   # 查看最近10条提交记录(一行一条)
git log --oneline --graph --all  # 图形化查看所有分支历史

三、基础操作:从零开始管理一个项目

3.1 创建仓库

# 方式一:把已有项目纳入 Git 管理
cd my-project/
git init
git add .
git commit -m "Initial commit"

# 方式二:从远程克隆已有项目
git clone https://github.com/user/repo.git
# 或克隆指定分支
git clone -b develop https://github.com/user/repo.git
# 或浅克隆(只要最近的历史,大项目提速)
git clone --depth=1 https://github.com/user/repo.git

3.2 日常工作流

# 1. 编写/修改代码...
vim app.py

# 2. 查看改了什么
git status
git diff

# 3. 把修改加入暂存区
git add app.py              # 添加单个文件
git add src/                # 添加整个目录
git add .                   # 添加所有变更(慎用,建议先 git status 确认)

# 4. 提交到本地仓库
git commit -m "feat: 添加用户登录功能"

# 5. 推送到远程
git push origin main

3.3 .gitignore 文件(哪些文件不该提交)

# .gitignore 示例(放在仓库根目录)

# 编译产物
*.class
*.pyc
*.o
*.so
/build/
/dist/

# 依赖目录(不要提交第三方库!)
/node_modules/
/venv/
__pycache__/

# IDE 配置
.idea/
.vscode/
*.swp

# 系统文件
.DS_Store
Thumbs.db

# 环境变量和密钥(⚠️绝对不能提交!)
.env
*.key
*.pem
config/secrets.yml

# 日志
*.log
logs/

💡 血的教训:把 .env 或密钥文件提交到 Git 是最常见的安全事故之一。一旦推送到远程,即使后续删除,历史记录里仍然存在!一定要在 .gitignore 里排除。


四、Commit 提交规范

4.1 好的提交信息长什么样

# ✅ 好的提交(一眼就知道干了什么)
git commit -m "feat: 添加用户注册接口"
git commit -m "fix: 修复登录页面在 Safari 上的样式问题"
git commit -m "docs: 更新 API 文档中的错误码说明"

# ❌ 烂的提交(看不懂干了什么)
git commit -m "update"
git commit -m "fix bug"
git commit -m "111"
git commit -m "先提交一下回家再说"

4.2 Conventional Commits 规范

┌────────────────────────────────────────────────────┐
│          Commit 类型(type)速查表                    │
├──────────┬──────────────────────────────────────────┤
│   类型    │                说明                       │
├──────────┼──────────────────────────────────────────┤
│ feat     │ 新功能                                    │
│ fix      │ 修复 Bug                                  │
│ docs     │ 文档变更                                  │
│ style    │ 代码格式(不影响功能,如缩进、空格)           │
│ refactor │ 重构(不是新功能也不是修 Bug)               │
│ perf     │ 性能优化                                  │
│ test     │ 添加或修改测试                             │
│ chore    │ 构建过程或辅助工具的变动                     │
│ ci       │ CI/CD 配置变更                             │
│ revert   │ 回滚之前的提交                             │
└──────────┴──────────────────────────────────────────┘

# 完整格式
git commit -m "type(scope): 简短描述

可选的详细说明(正文)
可分多行写

Breaking Changes: 不兼容变更说明
Closes #123"

4.3 修改最后一次提交

# 发现提交信息写错了
git commit --amend -m "feat: 添加用户注册接口(正确的描述)"

# 发现漏了一个文件没提交
git add forgotten-file.py
git commit --amend --no-edit    # 追加到最后一次提交,不改提交信息

# ⚠️ 注意:amend 会修改 commit hash,已 push 的提交慎用!

五、分支管理:团队协作的核心

5.1 什么是分支?为什么用分支?

主线(main):代表生产环境的稳定代码
  │
  ├──► develop:日常开发主线
  │      │
  │      ├──► feature/login:开发登录功能
  │      ├──► feature/cart:开发购物车功能
  │      └──► feature/search:开发搜索功能
  │
  ├──► hotfix/fix-crash:紧急修复线上崩溃
  │
  └──► release/v2.0:准备发布 v2.0

类比:主线是正传,分支是平行宇宙。你在平行宇宙里折腾,不影响主线。
搞定后合并回来就行。

5.2 分支常用操作

# 查看分支
git branch                  # 查看本地分支
git branch -a               # 查看所有分支(包括远程)
git branch -v               # 查看分支及其最后一次提交

# 创建分支
git branch feature/login    # 创建新分支
git checkout -b feature/login   # 创建并切换到新分支(最常用)
git switch -c feature/login     # 同上(Git 2.23+ 新命令,推荐)

# 切换分支
git checkout main           # 切换到 main 分支
git switch main             # 同上(推荐)

# 删除分支
git branch -d feature/login     # 删除已合并的分支
git branch -D feature/login     # 强制删除(即使没合并)

# 重命名分支
git branch -m old-name new-name # 重命名当前分支
git branch -m feature/login feature/user-login

5.3 合并分支(merge vs rebase)

# ========= merge:保留完整历史 =========
git checkout main
git merge feature/login
# 会产生一个合并提交(merge commit),历史有分叉但完整

# ========= rebase:让历史变线性 =========
git checkout feature/login
git rebase main
# 把 feature 分支的提交"搬到" main 最新提交之后,历史是直线

# ⚠️ 黄金法则:不要对已经推送到远程的公共分支做 rebase!
# rebase 会改写历史,别人基于旧历史的工作会乱套
merge vs rebase 对比:

    merge 的历史:              rebase 的历史:
       │                           │
       │ C3 (main)                │ C3 (main)
       │                          │ C4' (feature)
      ╱│ C4 (feature)            │ C5' (feature)
     ╱ │ C5 (feature)            │ C6 (main)
    ╱  │                          │
   ╱   │ C6 (merge commit)       │ (线性,漂亮!)
       │
(保留分叉,真实但复杂)

5.4 解决合并冲突

# 合并时出现冲突:
# CONFLICT (content): Merge conflict in app.py
# Automatic merge failed; fix conflicts and then commit the result.

# 第一步:查看哪些文件冲突了
git status
# 会显示:both modified: app.py

# 第二步:打开冲突文件,手动编辑
# 冲突标记长这样:
# <<<<<<< HEAD
# 这是当前分支的代码(main)
# =======
# 这是要合并进来的代码(feature)
# >>>>>>> feature/login

# 你需要决定保留哪部分,删除冲突标记(<<< === >>>),保存

# 第三步:标记冲突已解决
git add app.py

# 第四步:完成合并
git commit -m "merge: 合并 feature/login 到 main"

# 💡 小技巧:如果想放弃合并,回到合并前的状态
git merge --abort

六、远程仓库操作

6.1 远程仓库管理

# 查看远程仓库
git remote -v
# origin  https://github.com/user/repo.git (fetch)
# origin  https://github.com/user/repo.git (push)

# 添加远程仓库
git remote add origin https://github.com/user/repo.git

# 修改远程仓库地址
git remote set-url origin https://github.com/user/new-repo.git

# 删除远程仓库
git remote remove origin

6.2 拉取和推送

# 拉取远程更新(两种方式)
git pull origin main              # pull = fetch + merge
git pull --rebase origin main     # pull = fetch + rebase(推荐,避免产生 merge commit)

# 推送到远程
git push origin main              # 推送当前分支到远程 main
git push -u origin feature/login  # -u 设置上游分支(首次推送新分支时用)
git push                          # 设置了上游后,直接 push 即可

# 查看远程分支和本地分支的追踪关系
git branch -vv

# 删除远程分支
git push origin --delete feature/login

6.3 fetch vs pull

┌──────────────────────────────────────────────────┐
│            fetch vs pull 的区别                    │
├──────────┬───────────────────────────────────────┤
│  git fetch │ 只下载远程更新,不修改工作区(安全)     │
│  git pull  │ 下载远程更新 + 自动合并(可能冲突)      │
└──────────┴───────────────────────────────────────┘

# 推荐工作流:先 fetch 看看有什么更新,再决定是否合并
git fetch origin         # 下载远程更新
git log HEAD..origin/main --oneline   # 看看远程有哪些新提交
git merge origin/main    # 确认没问题再合并

七、🎯 场景一:团队协作——Git Flow 工作流

这是企业中最常用的团队协作流程。

Git Flow 全景图:

main(生产)────●────────────●──────────●──────────
                │            │          │
release/v2.0────┤            │          │
                │            │          │
develop─────────●──●──●──●──●──────────●──────────
                   │     │
feature/login──────●     │
feature/cart───────────●──┘

hotfix/urgent──────────────────●───────┘(从 main 拉,修完回 main + develop)
# === 完整的 Git Flow 流程 ===

# 1. 克隆项目
git clone https://github.com/team/project.git
cd project

# 2. 创建 develop 分支(如果还没有)
git checkout -b develop main

# 3. 开始新功能开发
git checkout -b feature/user-profile develop

# 4. 日常开发(频繁提交)
# ... 编写代码 ...
git add .
git commit -m "feat: 添加用户头像上传功能"
# ... 继续编写 ...
git add .
git commit -m "feat: 添加个人简介编辑功能"

# 5. 功能开发完成,合并回 develop
git checkout develop
git pull origin develop              # 先拉取最新
git merge --no-ff feature/user-profile  # --no-ff 保留分支历史
git push origin develop

# 6. 准备发布
git checkout -b release/v2.0 develop
# ... 只做 Bug 修复和版本号更新 ...
git commit -m "chore: 更新版本号到 v2.0"

# 7. 发布:合并到 main 和 develop
git checkout main
git merge --no-ff release/v2.0
git tag -a v2.0 -m "Release v2.0"
git push origin main --tags

git checkout develop
git merge release/v2.0
git push origin develop

# 8. 清理分支
git branch -d feature/user-profile
git branch -d release/v2.0

八、🎯 场景二:紧急修复线上 Bug(Hotfix)

线上突然出 Bug 了!客户在催!怎么快速修复?

# 第一步:立即从 main 拉一个 hotfix 分支
git checkout main
git pull origin main
git checkout -b hotfix/fix-payment-error

# 第二步:快速修复
vim src/payment.py    # 修复代码
git add src/payment.py
git commit -m "fix: 修复支付金额计算错误"

# 第三步:测试通过后,合并回 main(发布)
git checkout main
git merge --no-ff hotfix/fix-payment-error
git tag -a v1.0.1 -m "Hotfix: 修复支付金额计算错误"
git push origin main --tags

# 第四步:同步修复到 develop(防止下次发布又出现这个 Bug)
git checkout develop
git merge hotfix/fix-payment-error
git push origin develop

# 第五步:清理
git branch -d hotfix/fix-payment-error

# 💡 整个过程:拉分支 → 修 Bug → 合并 → 发布,5 分钟搞定!

九、🎯 场景三:写了一半的代码,突然要切分支

你正在 feature 分支写新功能,写到一半,突然被告知线上有个紧急 Bug 要修。代码还没写完,不想提交,怎么办?

# 方案一:git stash(推荐,最常用)

# 1. 把当前工作暂存起来
git stash save "用户注册功能写了一半"
# 或者直接 git stash

# 2. 查看暂存列表
git stash list
# stash@{0}: On feature/register: 用户注册功能写了一半

# 3. 切去修 Bug
git checkout hotfix/fix-bug
# ... 修 Bug,提交,合并 ...

# 4. 回来继续开发
git checkout feature/register
git stash pop          # 恢复最近的暂存,并从列表删除
# 或
git stash apply stash@{0}  # 恢复指定暂存,不删除

# 方案二:直接提交(草稿提交)
git add .
git commit -m "WIP: 用户注册功能(未完成)"
# 切去修 Bug...
# 回来后撤销这个提交,代码还在
git reset HEAD~1        # 撤销最后一次提交,保留修改在工作区

十、🎯 场景四:撤回操作(各种"后悔药")

这是大家最常遇到的情况:提交错了、push 错了、想回退版本。

10.1 撤回场景速查表

┌──────────────────────────────────────────────────────────────────┐
│                    撤回操作速查表(按场景选命令)                    │
├───────────────────────────┬──────────────────────────────────────┤
│          场景              │              命令                     │
├───────────────────────────┼──────────────────────────────────────┤
│ git add 添加错了           │ git restore --staged 文件名           │
│                           │ 或 git reset HEAD 文件名              │
├───────────────────────────┼──────────────────────────────────────┤
│ 工作区改乱了,想放弃修改    │ git restore 文件名                    │
│                           │ 或 git checkout -- 文件名             │
├───────────────────────────┼──────────────────────────────────────┤
│ commit 提交错了(未 push) │ git reset HEAD~1      ← 保留修改     │
│                           │ git reset --hard HEAD~1 ← 丢弃修改   │
├───────────────────────────┼──────────────────────────────────────┤
│ commit 提交错了(已 push) │ git revert HEAD       ← 安全回退     │
│                           │ git push origin main                 │
├───────────────────────────┼──────────────────────────────────────┤
│ 想回到某个历史版本          │ git checkout 提交哈希                 │
│                           │ 或 git reset --hard 提交哈希          │
├───────────────────────────┼──────────────────────────────────────┤
│ 删错了分支                 │ git reflog → 找到哈希 → git checkout │
└───────────────────────────┴──────────────────────────────────────┘

10.2 reset vs revert:一分钟搞懂核心区别

这是 Git 面试必考题,也是实际开发中最容易搞混的命令。先用一个比喻帮你记住:

📝 比喻记忆法:

reset = 橡皮擦 🔴  →  直接擦掉历史,就像什么都没发生过
revert = 撤销条 🟢  →  在后面贴一张"声明",说"前面的操作作废"

举例:
提交历史:A → B → C(C 有 bug)

用 reset:A → B          ← C 直接消失了!历史被改写
用 revert:A → B → C → C' ← C 还在,但 C' 是一个"反向操作",效果等于删除了 C 的改动

核心区别一图看懂:

┌─────────────────────────────────────────────────────────────────────┐
│                  reset vs revert 全面对比                            │
├──────────────────┬──────────────────────┬────────────────────────────┤
│      维度         │     git reset        │      git revert            │
├──────────────────┼──────────────────────┼────────────────────────────┤
│  本质            │ 改写历史(移动指针)    │ 新增一个"反向提交"           │
├──────────────────┼──────────────────────┼────────────────────────────┤
│  历史记录        │ 提交会消失 ⚠️         │ 提交保留,新增撤销提交 ✅     │
├──────────────────┼──────────────────────┼────────────────────────────┤
│  是否安全        │ 有丢代码风险          │ 安全,不会丢代码             │
├──────────────────┼──────────────────────┼────────────────────────────┤
│  对远程的影响    │ 会改写远程历史 💥      │ 不改写历史,正常push         │
├──────────────────┼──────────────────────┼────────────────────────────┤
│  团队协作        │ 危险!其他人会受影响   │ 安全!不影响其他人           │
├──────────────────┼──────────────────────┼────────────────────────────┤
│  适用场景        │ 本地未push的提交      │ 已push的提交                │
├──────────────────┼──────────────────────┼────────────────────────────┤
│  撤销方式        │ 把 HEAD 指针往回拨    │ 创建新提交来抵消旧提交       │
├──────────────────┼──────────────────────┼────────────────────────────┤
│  可逆性          │ 用 reflog 可恢复      │ 天然可逆(再 revert 回来)   │
├──────────────────┼──────────────────────┼────────────────────────────┤
│  冲突可能性      │ 无冲突(直接移动指针) │ 可能产生冲突(需手动解决)   │
└──────────────────┴──────────────────────┴────────────────────────────┘

如何选择?看这个决策树:

你的提交已经 push 了吗?
  │
  ├─ 没有 push(只在本地)
  │    │
  │    ├─ 还要保留代码修改?
  │    │    ├─ 要 → git reset --soft HEAD~1  (修改在暂存区)
  │    │    └─ 不要 → git reset --hard HEAD~1 (修改全丢)
  │    │
  │    └─ 想让修改回到工作区(未暂存)?
  │         └─ git reset HEAD~1 (等同于 --mixed)
  │
  └─ 已经 push 了
       │
       ├─ 是你一个人在用的分支?
       │    ├─ 是 → 可以用 git reset,然后 git push --force
       │    └─ 否(团队共用) → 必须用 git revert ⚠️
       │
       └─ revert 后 push 即可,不会影响其他人

10.3 reset 的三种模式:逐层深入理解

reset 有三种模式,区别在于"回退到哪一步"。理解的关键是知道 Git 的三个区域:

Git 的三个区域:

  工作区(Working Directory)   →  你能看到、能编辑的文件
  暂存区(Staging Area/Index)  →  git add 之后,准备提交的文件
  仓库区(Repository)          →  git commit 之后,正式的提交记录

完整流程:工作区 → git add → 暂存区 → git commit → 仓库区

reset 的三种模式,就是决定"回退几步":
# 假设当前状态:你做了第三次提交 C
# 提交历史:A → B → C(HEAD 指向 C)
# C 里面包含了对 file.py 的修改

# ═══════════════════════════════════════════
# 模式一:--soft(最温柔,代码全保留)
# ═══════════════════════════════════════════
git reset --soft HEAD~1

# 效果图解:
#   仓库区:  HEAD 指针从 C 退回到 B(C 提交消失)
#   暂存区:  C 的修改还在这里 ✅(已经 add 的状态)
#   工作区:  不受影响
#
# 相当于:撤销了 commit,但 add 还在
# 适用场景:
#   - commit 信息写错了,想重新写
#   - 想把几次提交合并成一次
#   - 修改还在,可以直接 git commit -m "新信息" 重新提交

# 实战例子:
git log --oneline
# abc1234 (HEAD) feat: 添加登录功能
# def5678 feat: 添加注册功能

git reset --soft HEAD~1
# HEAD 退回 def5678,但登录功能的代码还在暂存区

git status
# Changes to be committed:
#   modified:   src/login.py     ← 注意!还在暂存区,可以直接重新提交

git commit -m "feat: 添加登录功能(含表单验证)"  # 重新提交,这次信息写对了


# ═══════════════════════════════════════════
# 模式二:--mixed(默认模式,最常用)
# ═══════════════════════════════════════════
git reset HEAD~1
# 等同于 git reset --mixed HEAD~1

# 效果图解:
#   仓库区:  HEAD 指针从 C 退回到 B(C 提交消失)
#   暂存区:  C 的修改被移出(变回未暂存状态)
#   工作区:  C 的修改还在这里 ✅(文件内容不变)
#
# 相当于:撤销了 commit + add,但文件修改还在
# 适用场景:
#   - 提交错了,想把某些文件从暂存区拿出来
#   - 想重新选择要提交的文件

# 实战例子:
git log --oneline
# abc1234 (HEAD) feat: 添加登录功能和注册功能(一次提交了两个功能)

# 你发现应该分开提交
git reset HEAD~1
# 两个功能的修改都回到了工作区(未暂存)

git status
# Changes not staged for commit:
#   modified:   src/login.py
#   modified:   src/register.py

# 现在可以分开提交了
git add src/login.py
git commit -m "feat: 添加登录功能"
git add src/register.py
git commit -m "feat: 添加注册功能"


# ═══════════════════════════════════════════
# 模式三:--hard(最危险,代码全丢!)
# ═══════════════════════════════════════════
git reset --hard HEAD~1

# 效果图解:
#   仓库区:  HEAD 指针从 C 退回到 B(C 提交消失)
#   暂存区:  C 的修改被清空
#   工作区:  C 的修改被清空 ❌(文件也回到 B 的状态)
#
# 相当于:一切回到 B 提交时的样子,C 的修改彻底消失
# ⚠️ 警告:代码真的会丢!除非用 reflog 找回
# 适用场景:
#   - 确定不要这些修改了(比如试错后发现方向不对)
#   - 想完全回到之前的某个版本

# 实战例子:
git log --oneline
# abc1234 (HEAD) try: 尝试新方案(失败了)
# def5678 feat: 添加主功能

git reset --hard HEAD~1
# HEAD 回到 def5678,abc1234 的所有修改全部消失
# 文件恢复到 def5678 时的状态

git status
# nothing to commit, working tree clean  ← 干干净净,好像什么都没发生过

# 💡 如果后悔了怎么办?用 reflog 救命!
git reflog
# def5678 HEAD@{0}: reset: moving to HEAD~1
# abc1234 HEAD@{1}: commit: try: 尝试新方案
git reset --hard abc1234   # 还能找回来!


# ═══════════════════════════════════════════
# 三种模式对比总结
# ═══════════════════════════════════════════
#
#              仓库(commit)  暂存区(add)  工作区(文件)
# --soft       撤销 ✅       保留 ✅      保留 ✅
# --mixed      撤销 ✅       撤销 ✅      保留 ✅
# --hard       撤销 ✅       撤销 ✅      撤销 ✅
#
# 记忆口诀:
#   soft  = 只动 commit,最安全
#   mixed = 动 commit + add,默认值
#   hard  = 全都动,最危险

10.4 revert:安全的"反向操作"(已 push 的代码必须用这个)

revert 不是"删掉历史",而是"加一条新记录来抵消之前的操作"。就像会计做账:错了不能涂掉,要在下面写一笔冲正

# ═══════════════════════════════════════════
# 为什么已 push 的代码必须用 revert?
# ═══════════════════════════════════════════
#
# 假设提交历史:A → B → C(C 已 push)
# 你的同事张三已经 pull 了这份代码
#
# 如果你用 reset:
#   你的历史:A → B         (C 消失了)
#   张三的历史:A → B → C   (C 还在!)
#   → 你 push 的时候需要 --force
#   → 张三 pull 的时候会冲突、丢失代码 💥
#
# 如果你用 revert:
#   你的历史:A → B → C → C'(C' 抵消了 C)
#   张三 pull 后:A → B → C → C'(安全同步 ✅)


# ═══════════════════════════════════════════
# revert 基本用法
# ═══════════════════════════════════════════

# 撤销最后一次提交
git revert HEAD
# 会弹出编辑器让你写撤销原因,保存后会生成一个新提交

# 撤销指定提交
git revert abc123f
# 找到 abc123f 这个提交的改动,生成一个"反向改动"的新提交

# 撤销多个提交(连续的)
git revert HEAD~3..HEAD
# 撤销最近 3 个提交(注意:会按从新到旧的顺序逐个撤销)

# 撤销但不自动提交(先看看效果)
git revert --no-commit HEAD
# 撤销的改动放在暂存区,你可以检查后再手动 commit
git commit -m "revert: 撤销登录功能(有 bug)"


# ═══════════════════════════════════════════
# revert 遇到冲突怎么办?
# ═══════════════════════════════════════════
# 如果后来的提交修改了和被撤销提交相同的文件,revert 会产生冲突

git revert abc123f
# CONFLICT (content): Merge conflict in src/login.py
# error: could not revert abc123f

# 解决步骤:
# 1. 打开冲突文件,手动选择要保留的内容
# 2. 解决后:
git add src/login.py
git revert --continue

# 如果放弃撤销:
git revert --abort


# ═══════════════════════════════════════════
# push 失败了怎么办?——force push 详解
# ═══════════════════════════════════════════

# 🤔 为什么 push 会失败?
# ──────────────────────────────
# 当你用 reset 回退了本地历史后,本地和远程的提交历史就不一致了:
#
# 远程:A → B → C → D
# 本地:A → B          (你 reset 掉了 C 和 D)
#
# 此时 git push 会报错:
# ! [rejected]        main -> main (non-fast-forward)
# error: failed to push some refs to 'origin/main'
#
# Git 拒绝推送,因为它发现你本地"少"了提交,担心你误操作丢代码!


# 📋 三种解决方案对比
# ──────────────────────────────
# ┌──────────────────────────────────────────────────────┐
# │ 方案             │ 命令                    │ 危险等级 │
# ├──────────────────────────────────────────────────────┤
# │ ① revert + push  │ git revert → git push   │ 🟢 安全  │
# │ ② force push     │ git push --force        │ 🟡 危险  │
# │ ③ force push 安全版│ git push --force-with-lease │ 🟡 较安全│
# └──────────────────────────────────────────────────────┘


# 方案一:revert + push(推荐!最安全)
# ──────────────────────────────
# 不改写历史,用 revert 生成反向提交,然后正常 push
git revert C D          # 逐个撤销 C 和 D 的改动
git push origin main    # ✅ 正常 push 成功!
# 历史变成:A → B → C → D → C' → D'
# 代码效果回到了 B 的状态,但历史完整保留


# 方案二:force push(强制推送,⚠️ 慎用!)
# ──────────────────────────────
# 场景:你确定要覆盖远程历史,而且其他人没有基于 C/D 做新提交
git push --force origin main
# 或者简写:
git push -f origin main

# ⚠️ 危险在哪?
# 如果同事小明已经基于 D 拉取了代码并做了新提交 E:
#   小明:A → B → C → D → E
#   你 force push 后远程变成:A → B
#   小明下次 pull 时会冲突爆炸💥,甚至可能丢失 E!


# 方案三:force-with-lease(安全版的 force push 🌟推荐)
# ──────────────────────────────
# 比 --force 多了一层保护:会检查远程分支是否被别人更新过
git push --force-with-lease origin main

# 工作原理:
# 如果远程分支还是你上次看到的状态(没人动过)→ ✅ 推送成功
# 如果远程分支已经被别人更新了 → ❌ 推送失败,保护了别人的代码!

# 对比:
# git push --force           → 管他三七二十一,直接覆盖!
# git push --force-with-lease → 先确认没人动过,再覆盖(有保镖的 force push)


# ═══════════════════════════════════════════
# 实战场景 1:本地 reset 后需要推送
# ═══════════════════════════════════════════
# 你在本地 reset 了最近 2 个提交:
git reset --hard HEAD~2
# 本地:A → B(C 和 D 被删了)
# 远程:A → B → C → D

# 方案 A:用 revert(安全,推荐团队协作)
git revert HEAD@{1} HEAD@{2}   # 撤销 C 和 D
git push origin main            # 正常 push

# 方案 B:用 force-with-lease(个人分支,确认没别人用)
git push --force-with-lease origin main
# 远程变成:A → B ✅

# 方案 C:用 force(只有你一个人在用的分支)
git push --force origin main
# 远程变成:A → B ✅ 但没有保镖保护


# ═══════════════════════════════════════════
# 实战场景 2:rebase 后需要推送
# ═══════════════════════════════════════════
# rebase 也会改写历史,push 同样会失败
git rebase -i HEAD~3            # 整理了最近 3 个提交
git push origin main            # ❌ rejected!

# 解决:
git push --force-with-lease origin main   # ✅ 推送成功


# ═══════════════════════════════════════════
# 什么时候该用哪个?决策指南
# ═══════════════════════════════════════════
#
# 问自己两个问题:
#
# 1️⃣ 这个分支有没有其他人也在用?
#    ├── 有 → 用 revert + push(方案一)🟢
#    └── 没有(个人分支)→ 继续问 ↓
#
# 2️⃣ 你确定要覆盖远程历史吗?
#    ├── 确定 → 用 --force-with-lease(方案三)🟡
#    └── 不确定 → 用 revert + push(方案一)🟢
#
# 🔴 永远不要在 main/master 等公共分支上用 --force!
# 🔴 团队协作的分支,优先用 revert!
# 🟡 个人分支可以用 --force-with-lease
# 🟢 不确定时,revert + push 永远是最安全的选择


# ═══════════════════════════════════════════
# 实战场景:完整的 revert 流程
# ═══════════════════════════════════════════

# 场景:你昨天提交了一个支付功能,今天测试发现有严重 bug
# 提交记录:
# efg7890 (HEAD) docs: 更新 API 文档
# cde4567 feat: 添加用户管理
# abc1234 feat: 添加支付功能    ← 这个有 bug,要撤销!

# 第一步:撤销支付功能的提交
git revert abc1234
# Git 会生成一个新提交,内容和 abc1234 完全相反

# 第二步:查看历史
git log --oneline
# hij0123 (HEAD) Revert "feat: 添加支付功能"
# efg7890 docs: 更新 API 文档
# cde4567 feat: 添加用户管理
# abc1234 feat: 添加支付功能    ← 还在!但效果被上面的 Revert 抵消了

# 第三步:推送到远程
git push origin main
# 完美!同事们 pull 后,支付功能就被安全地移除了

# 等你修好 bug 后,可以重新提交,也可以 revert 那个 revert:
git revert hij0123   # 恢复支付功能!


# ═══════════════════════════════════════════
# 一句话总结
# ═══════════════════════════════════════════
#
# 🔴 reset = 时光倒流(改写历史)→ 只在本地用!
# 🟢 revert = 发布补丁(保留历史)→ 已推送的代码用这个!
#
# 记住:已经 push 的代码,永远用 revert!
#       没有 push 的代码,reset 更方便!

十一、🎯 场景五:代码写岔了,怎么搬移提交?

11.1 cherry-pick:把某个提交搬到另一个分支

# 场景:你在 develop 分支做了一个修复,但这个修复也需要同步到 main

git checkout main
git cherry-pick abc123f       # 把 abc123f 这个提交"复制"到当前分支

# cherry-pick 多个提交
git cherry-pick abc123f def456g

# cherry-pick 一个范围
git cherry-pick feature/login~3..feature/login

# 如果 cherry-pick 遇到冲突
# 解决冲突后:
git add .
git cherry-pick --continue

# 放弃 cherry-pick
git cherry-pick --abort

11.2 交互式 rebase:修改历史提交

# 场景:提交历史太乱,想整理一下(合并提交、修改提交信息、删除提交、调整顺序)

git rebase -i HEAD~5          # 整理最近 5 个提交

# 会打开编辑器,显示:
# pick abc1234 feat: 添加登录页面
# pick def5678 fix: 修复登录按钮样式
# pick ghi9012 feat: 添加注册页面
# pick jkl3456 feat: 添加忘记密码功能
# pick mno7890 docs: 更新 README

# 你可以修改每行开头的命令:
# pick   = 保留这个提交
# squash = 把这个提交合并到前一个提交
# reword = 保留提交,但修改提交信息
# edit   = 保留提交,但停下来让你修改
# drop   = 删除这个提交
# 把 def5678 那行改成 squash(合并到 abc1234),保存退出即可

# ⚠️ 同样的规则:不要 rebase 已经推送的公共分支!

十二、🎯 场景六:误操作的终极救星——reflog

如果你不小心 reset --hard 了、删了分支、rebase 搞乱了——别慌!reflog 记录了你的所有操作历史。

# 查看操作历史(本地所有 HEAD 的移动记录)
git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~1
# def5678 HEAD@{1}: commit: feat: 添加了很重要的功能
# ghi9012 HEAD@{2}: checkout: moving from main to feature/login
# ...

# 找到你想回到的那个点(比如 HEAD@{1})
git reset --hard def5678     # 回到那个状态

# 误删分支也能找回
git reflog                       # 找到分支最后一次提交的哈希
git checkout -b recovered-branch abc1234   # 用那个哈希重建分支

# 💡 reflog 默认保留 90 天,这期间几乎能恢复一切!

十三、🎯 场景七:大型项目中的高级技巧

13.1 子模块(Submodule)

# 场景:项目依赖另一个 Git 仓库(如公共组件库)

# 添加子模块
git submodule add https://github.com/team/shared-lib.git libs/shared-lib
git commit -m "chore: 添加 shared-lib 子模块"

# 克隆包含子模块的项目
git clone --recurse-submodules https://github.com/team/project.git

# 已克隆的项目拉取子模块
git submodule init
git submodule update

# 更新子模块到最新
git submodule update --remote

13.2 子树(Subtree)—— 比 Submodule 更简单的替代方案

# 添加子树
git subtree add --prefix=libs/shared-lib https://github.com/team/shared-lib.git main --squash

# 拉取子树更新
git subtree pull --prefix=libs/shared-lib https://github.com/team/shared-lib.git main --squash

13.3 大文件处理(Git LFS)

# Git 不擅长管理大文件(视频、模型、数据集等)
# Git LFS(Large File Storage)解决此问题

# 安装(一次性)
git lfs install

# 追踪大文件类型
git lfs track "*.psd"
git lfs track "*.mp4"
git lfs track "datasets/**"

# 正常 add/commit 即可,LFS 自动处理
git add .gitattributes
git add large-file.psd
git commit -m "chore: 配置 Git LFS 追踪大文件"

13.4 二分查找(bisect)—— 找出哪个提交引入了 Bug

# 场景:某个 Bug 不知道是哪次提交引入的

git bisect start
git bisect bad                  # 当前版本有 Bug
git bisect good v1.0.0          # v1.0.0 版本没 Bug

# Git 会自动二分查找,每次让你测试一个版本
# 测试后告诉 Git:
git bisect good                 # 这个版本没问题
git bisect bad                  # 这个版本有 Bug

# 重复几次,Git 会定位到具体哪个提交引入了 Bug
# 找到后:
git bisect reset                # 结束二分查找

十四、🎯 场景八:处理敏感信息泄露

# ⚠️ 最可怕的场景:不小心把密码/API Key 推到了远程仓库!
# 光删文件没用,Git 历史里还有!

# 方案一:BFG Repo-Cleaner(推荐,简单快速)
# 安装 BFG(Java 工具)
# 替换敏感信息
bfg --replace-text passwords.txt my-repo/
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force

# 方案二:git filter-branch(内置,慢但通用)
git filter-branch --force --index-filter \\
  'git rm --cached --ignore-unmatch path/to/sensitive-file' \\
  --prune-empty --tag-name-filter cat -- --all

# 方案三:git filter-repo(推荐替代 filter-branch)
pip install git-filter-repo
git filter-repo --path path/to/sensitive-file --invert-paths

# 💡 最好的方案是预防:
# 1. .gitignore 里排除 .env 和密钥文件
# 2. 使用 pre-commit 钩子检测敏感信息
# 3. 使用 git-secrets 等工具

十五、🎯 场景九:多仓库/多账号配置

公司用 GitLab,个人用 GitHub,两套账号怎么管理?

# 在 ~/.ssh/ 下生成两套密钥
ssh-keygen -t ed25519 -C "work@company.com" -f ~/.ssh/id_ed25519_work
ssh-keygen -t ed25519 -C "personal@gmail.com" -f ~/.ssh/id_ed25519_personal

# 配置 SSH config(~/.ssh/config)
# 公司 GitLab
Host gitlab.company.com
    HostName gitlab.company.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work

# 个人 GitHub
Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_personal

# 不同仓库使用不同的 Git 配置
# 全局配置(默认用个人的)
git config --global user.name "个人名字"
git config --global user.email "personal@gmail.com"

# 在公司项目里覆盖
cd /path/to/work-project
git config user.name "工作名字"
git config user.email "work@company.com"

十六、🎯 场景十:代码审查(Code Review)

16.1 Pull Request / Merge Request 流程

PR/MR 工作流:

1. 开发者 Fork 或从 develop 拉分支
        │
2. 在自己的分支上开发,提交代码
        │
3. 推送到远程,创建 Pull Request
        │
4. 团队成员 Code Review
   ├── 提出修改意见 → 开发者修改 → 再提交
   ├── Approve(通过)
   └── Request Changes(要求修改)
        │
5. CI/CD 自动测试通过
        │
6. 合并到主分支
        │
7. 自动部署

16.2 PR 提交者的最佳实践

# 1. PR 粒度要小(一个 PR 做一件事)
#    ✅ 好的 PR:只添加用户登录功能
#    ❌ 烂的 PR:登录 + 注册 + 购物车 + 重构 + 改 UI

# 2. 提交前自检
git diff main...feature/login    # 看看和主分支的差异
git log main..feature/login --oneline  # 看看有哪些新提交

# 3. 如果提交太多太碎,交互式 rebase 合并一下
git rebase -i HEAD~5   # squash 合并碎片提交

# 4. 确保通过了所有测试
npm test    # 或 pytest、go test 等

# 5. 写好 PR 描述
# 标题:feat: 添加用户登录功能
# 描述:
# - 实现了邮箱/密码登录
# - 添加了 JWT Token 认证
# - 补充了单元测试
# - 截图/录屏演示

16.3 Code Review 者的最佳实践

# 快速审查命令
git fetch origin
git checkout feature/login
git log --oneline main..HEAD     # 看提交历史
git diff main...HEAD             # 看代码差异
git diff main...HEAD --stat      # 看改了哪些文件

# 用 GitLab/GitHub 的行内评论功能
# 关注点:
# 1. 逻辑是否正确
# 2. 是否有安全漏洞
# 3. 代码风格是否统一
# 4. 是否有更好的实现方式
# 5. 测试覆盖是否充分

十七、Git 钩子(Hooks)自动化

# Git 钩子放在 .git/hooks/ 目录下

# 常用钩子:
# pre-commit      → 提交前执行(代码检查、格式化)
# commit-msg      → 验证提交信息格式
# pre-push        → 推送前执行(运行测试)
# post-merge      → 合并后执行(自动安装依赖)

# ===== 示例:pre-commit 钩子(代码检查)=====
# .git/hooks/pre-commit(去掉 .sample 后缀即可生效)

# 检查是否有 console.log 残留
if git diff --cached --name-only | grep '\.js$' | xargs grep -l 'console.log'; then
    echo "发现 console.log,请移除后再提交!"
    exit 1
fi

# 检查是否有调试代码
if git diff --cached | grep -E 'debugger|breakpoint'; then
    echo "发现调试代码,请移除后再提交!"
    exit 1
fi

echo "代码检查通过"

17.1 用 Husky 管理钩子(推荐)

# 原生 Git Hooks 的问题:
# 1. .git/hooks 不会被 Git 跟踪,团队共享困难
# 2. 手动管理麻烦,容易忘

# Husky:让钩子配置写在 package.json 里,团队共享!
npm install husky --save-dev
npx husky init           # 初始化

# 添加 pre-commit 钩子
echo "npm run lint" > .husky/pre-commit

# 添加 commit-msg 钩子(配合 commitlint 使用)
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

17.2 实战:commitlint 规范提交信息

# 安装 commitlint
npm install @commitlint/cli @commitlint/config-conventional --save-dev

# 合法的提交信息示例:
# git commit -m "feat: 添加用户注册功能"
# git commit -m "fix: 修复登录页面空白问题"
# git commit -m "docs: 更新 API 文档"

# 非法的提交信息(会被拒绝):
# git commit -m "更新了东西"     → 缺少类型前缀
# git commit -m "abc: xxx"       → 不合法的类型

十八、Git 标签(Tag)管理

# Tag 就像书签,给某个提交打个标记,通常用于版本发布

# 创建轻量标签
git tag v1.0.0

# 创建附注标签(推荐,包含更多信息)
git tag -a v1.0.0 -m "正式版 1.0.0 发布"

# 给历史提交打标签
git tag -a v0.9.0 abc1234 -m "0.9.0 版本"

# 查看所有标签
git tag
git tag -l "v1.*"        # 按模式过滤

# 推送标签到远程
git push origin v1.0.0    # 推送单个标签
git push origin --tags     # 推送所有标签

# 删除标签
git tag -d v1.0.0                    # 删除本地标签
git push origin :refs/tags/v1.0.0   # 删除远程标签

18.1 语义化版本号(SemVer)

# 版本号格式:MAJOR.MINOR.PATCH
# 例如:2.1.3
#   2 → MAJOR(大版本,可能有不兼容的变更)
#   1 → MINOR(小版本,新增功能,向后兼容)
#   3 → PATCH(补丁,修复 bug,向后兼容)

# 什么时候改什么版本号?
# 修复了一个 bug              → PATCH+1(2.1.3 → 2.1.4)
# 新增了一个功能              → MINOR+1(2.1.3 → 2.2.0)
# 改了接口,旧代码不能用了     → MAJOR+1(2.1.3 → 3.0.0)

十九、常见问题与故障排查

19.1 提交错了怎么办?

# 场景1:刚 commit,发现少加了一个文件
git add forgotten_file.js
git commit --amend --no-edit    # 追加到上一次提交

# 场景2:刚 commit,发现提交信息写错了
git commit --amend -m "正确的提交信息"

# 场景3:刚 push 了,需要撤回(慎用!)
# 如果只有你一个人在用这个分支:
git reset --hard HEAD~1
git push --force

# 如果有其他人在用这个分支:用 revert 更安全
git revert <commit-hash>
git push

19.2 合并冲突怎么解决?

# 冲突长什么样:
# <<<<<<< HEAD
# 这是你的代码
# =======
# 这是别人的代码
# >>>>>>> feature-branch

# 解决步骤:
# 1. 打开冲突文件
# 2. 手动选择保留哪部分代码
# 3. 删除所有标记符号
# 4. 保存文件
git add .
git commit -m "解决合并冲突"

19.3 误删分支怎么恢复?

# 查看被删除分支的最后一次提交
git reflog

# 恢复分支
git checkout -b feature-recovered abc1234

# reflog 是 Git 的后悔药,记录了所有 HEAD 的移动
# 默认保留 90 天

19.4 大文件提交了怎么彻底删除?

# 场景:不小心把大文件提交了
# 即使删了再提交,Git 历史里还保留着

# 用 BFG Repo-Cleaner(推荐,快)
bfg --delete-files large-file.zip
git reflog expire --expire=now --all
git gc --prune=now --aggressive

# 用 git-filter-repo(推荐,现代化)
pip install git-filter-repo
git filter-repo --path large-file.zip --invert-paths

19.5 .gitignore 不生效怎么办?

# 原因:文件已经被 Git 跟踪了
# 解决:先取消跟踪,再添加到 .gitignore
git rm --cached path/to/file
git rm -r --cached node_modules/
echo "node_modules/" >> .gitignore
git add .gitignore
git commit -m "停止跟踪 node_modules"

19.6 push 被拒绝怎么办?

# 错误:! [rejected] main -> main (fetch first)
# 原因:远程有你本地没有的新提交

# 解决:先 pull 再 push
git pull --rebase origin main
git push origin main

# 或者用更安全的 force
git push --force-with-lease origin main

二十、Git 进阶技巧与可视化工具

20.1 实用技巧

# 暂存部分文件(交互式)
git add -p           # 逐块选择要暂存的代码

# 查看某个文件的修改历史
git log --follow -p path/to/file.js

# 查看某行代码是谁写的
git blame path/to/file.js
git blame -L 10,20 path/to/file.js   # 只看第10-20行

# 找到引入 bug 的提交
git bisect start
git bisect bad                    # 当前版本有 bug
git bisect good v1.0.0           # 这个版本没 bug
git bisect run npm test           # 自动运行测试来定位

# 清理本地已合并的分支
git branch --merged | grep -v "\*\|main\|develop" | xargs -n 1 git branch -d

# 垃圾回收
git gc --aggressive --prune=now

20.2 可视化工具推荐

工具              类型              特点
─────────────────────────────────────────────────
VS Code GitLens   VS Code 插件      最强 Git 可视化插件
SourceTree        独立 GUI          免费且功能全面
GitKraken         独立 GUI          跨平台,界面美观
GitHub Desktop    独立 GUI          简单易用,官方出品
lazygit           终端 UI           键盘操作,效率极高

推荐:
初学者 → GitHub Desktop 或 SourceTree
进阶    → VS Code + GitLens
高手    → lazygit

20.3 换行符问题(Windows/Mac/Linux)

# Windows 用 CRLF,Mac/Linux 用 LF
# Windows 用户
git config --global core.autocrlf true

# Mac/Linux 用户
git config --global core.autocrlf input

# 项目统一配置(.gitattributes 文件)
* text=auto
*.sh text eol=lf
*.bat text eol=crlf

总结:Git 学习路线图

入门阶段(1-2周)
├── git init / clone / add / commit / push / pull
├── 理解工作区 → 暂存区 → 仓库 的概念
├── 学会看 git status / git log
└── 练习:创建仓库,提交代码,推送到 GitHub

进阶阶段(2-4周)
├── 分支管理(branch / merge / rebase)
├── 解决合并冲突
├── .gitignore 配置
├── git stash 暂存
└── 练习:多人协作开发一个项目

高级阶段(1-2月)
├── 交互式 rebase(整理提交历史)
├── cherry-pick(精准摘取提交)
├── Git Hooks 自动化
├── bisect 定位 bug
├── 大文件处理 / 历史重写
└── 练习:参与开源项目贡献

专家阶段(持续)
├── 自定义 Git 工作流
├── Git 内部原理(对象模型、引用)
├── 性能优化(大仓库管理)
├── 编写自定义钩子 / CI/CD 集成
└── 指导团队 Git 最佳实践

Git 的学习不需要一次性掌握所有命令。从最基础的 add/commit/push 开始,在日常开发中逐步积累经验。遇到问题别慌,git reflog 是你的后悔药,git stash 是你的暂存箱。

记住一句话:"先学会走,再学会跑,最后再飞。"祝你在 Git 的世界里畅行无阻!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐