Git 是分布式版本控制系统,用于管理代码历史、分支、合并和团队协作。
1. 核心模型
Working Tree(工作区)
↓ git add
Index / Staging Area(暂存区)
↓ git commit
Local Repository(本地仓库)
↓ git push
Remote Repository(远程仓库)
三个关键区域:
| 区域 | 作用 |
|---|---|
| Working Tree | 当前正在编辑的文件 |
| Staging Area | 准备提交的文件内容 |
| Local Repository | 已保存的 Commit 历史 |
需要注意:git add 不只是开始跟踪文件,更重要的是把当前文件内容的快照放入暂存区。
因此,同一个文件可以同时存在已暂存和未暂存的修改。
2. 核心概念
Repository
Git 仓库包含提交对象、分支引用、历史记录等信息。
git init
git clone <url>
仓库内部通常包含 .git/ 目录。
Tracked / Untracked
- Tracked:Git 已经跟踪的文件。
- Untracked:Git 尚未跟踪的文件。
- Modified:文件内容发生变化。
- Staged:修改已经进入暂存区。
git status
.gitignore
用于忽略尚未跟踪的文件。
node_modules/
.env
dist/
__pycache__/
已经被跟踪的文件,即使加入 .gitignore,仍然会继续被跟踪。
停止跟踪但保留本地文件:
git rm --cached .env
如果敏感信息已经提交到 Git 历史,停止跟踪不能清除已有历史中的泄露内容,还需要撤销相应密钥并考虑清理历史。
Commit
Commit 可以理解为项目某一时刻的完整快照,并保存父 Commit、作者、时间和提交信息等元数据。
A ── B ── C ── D
↑
main
每个 Commit 有唯一的对象 ID。
HEAD
HEAD 通常是指向当前分支的符号引用。
HEAD → main → D
常见表达式:
| 表达式 | 含义 |
|---|---|
HEAD | 当前提交 |
HEAD^ | 第一父提交 |
HEAD~2 | 沿第一父提交向前两次 |
HEAD~3 | 沿第一父提交向前三次 |
HEAD^2 | 第二父提交,常见于 Merge Commit |
HEAD^3 不是前三个提交,而是第三父提交。
Detached HEAD
直接切换到某个 Commit:
git switch --detach abc123
此时 HEAD 直接指向 Commit,而不是某个分支。
适合检查历史代码。如果准备在这里继续开发,应创建新分支:
git switch -c fix/old-version
3. 本地分支与远程跟踪分支
Git 是分布式版本控制系统。
GitHub:
main → C
Local:
main → B
origin/main → C
main:本地分支。origin/main:本地保存的远程跟踪分支状态。origin:远程仓库名称,通常由 clone 自动创建。
如果远程产生新提交:
GitHub:
A ── B ── C ── D
Local:
main → B
origin/main → C
运行:
git fetch origin
更新为:
main → B
origin/main → D
但不会直接修改本地 main。
fetch 与 pull
git fetch origin
获取远程更新,不直接合并当前工作分支。
git pull
先 Fetch,再根据配置执行 Merge、Rebase 或仅允许 Fast-forward。
明确指定:
git pull --no-rebase
git pull --rebase
git pull --ff-only
对于希望避免意外合并提交的场景,可以使用 --ff-only。
4. 基础命令
初始化与配置
git init
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git clone <url>
查看状态
git status
git branch
git branch -a
git remote -v
暂存与提交
git add src/foo.ts
git add .
git commit -m "feat: add user profile"
git add . 将当前目录及子目录中符合条件的新增、修改和删除加入暂存区。
建议提交之前检查:
git diff --staged
查看差异
# 工作区与暂存区
git diff
# 暂存区与 HEAD
git diff --staged
# 两个 Commit
git diff A B
# 查看单次提交
git show <commit>
查看历史
git log
git log --oneline
git log --oneline --graph --decorate --all
git log -n 5
git log --author="Author Name"
其中:
git log --oneline --graph --decorate --all
适合查看分支和合并关系。
5. 分支管理
创建和切换
git branch feature/login
git switch feature/login
# 创建并切换
git switch -c feature/login
删除
# 安全删除
git branch -d feature/login
# 强制删除
git branch -D feature/login
# 删除远程分支
git push origin --delete feature/login
-d 会检查分支是否已被适当合并;-D 跳过这一安全检查。
重命名
git branch -m new-name
推送与 Upstream
git push -u origin feature/login
-u 建立本地分支与上游分支之间的跟踪关系。
之后通常可以:
git push
git pull
但实际 Push 目标还受到 Git 配置影响。
6. Merge:合并分支
假设:
C ── D feature
/
A ── B ── E main
在 main 上:
git switch main
git merge feature
结果:
C ── D ──┐
/ │
A ── B ── E ───── M
↑
main
Merge Commit M 有两个父提交。
Git 根据共同祖先和双方变化进行三方合并。
Fast-forward Merge
如果 main 没有产生新提交:
A ── B main
\
C ── D feature
执行:
git switch main
git merge feature
结果:
A ── B ── C ── D
↑
main, feature
此时 Git 只需要向前移动分支指针,不一定创建新的 Merge Commit。
7. Rebase:变基
假设:
A ── B ── C main
\
D ── E feature
在 feature 分支执行:
git switch feature
git rebase main
结果:
A ── B ── C main
\
D' ── E' feature
Rebase 将分支中的提交重新应用到新的基础提交之后。
由于父提交等信息发生变化,通常会产生新的 Commit ID。
Merge 与 Rebase
| 对比 | Merge | Rebase |
|---|---|---|
| 原有历史 | 保留 | 重写被重放的提交 |
| 提交图 | 可能产生分叉和 Merge Commit | 通常更线性 |
| 共享分支 | 相对安全 | 不应随意重写 |
| 常见用途 | 合并功能分支 | 更新个人开发分支 |
常见原则:
- 已经被团队共享的历史,优先避免 Rebase 改写。
- 自己独立维护的功能分支,可以使用 Rebase 整理历史。
- 具体以团队的 Git 工作流规范为准。
8. Merge / Rebase 冲突
冲突示例:
<<<<<<< HEAD
const timeout = 30;
=======
const timeout = 60;
>>>>>>> feature
需要手动决定最终内容,再删除冲突标记。
Merge 冲突
git status
# 修改冲突文件
git add config.ts
git merge --continue
也可以在合适情况下执行 git commit 完成合并。
放弃 Merge:
git merge --abort
Rebase 冲突
git status
# 修改冲突文件
git add config.ts
git rebase --continue
放弃 Rebase:
git rebase --abort
注意:Rebase 时 ours 和 theirs 的含义容易与普通 Merge 混淆,不能机械地将其理解为“我的代码”和“别人的代码”。
9. Restore、Reset、Revert
这三个命令都与撤销有关,但职责不同。
git restore
主要恢复工作区或暂存区的文件内容。
# 丢弃文件未暂存的修改,恢复到 Index
git restore src/foo.ts
# 取消暂存,工作区修改保留
git restore --staged src/foo.ts
# 暂存区和工作区都恢复到 HEAD
git restore --source=HEAD --staged --worktree src/foo.ts
注意:默认 git restore file 使用 Index 作为来源,不一定等于 HEAD。
git reset
可以移动当前分支的 Commit 指针,并根据模式决定是否修改暂存区和工作区。
| 模式 | 移动 HEAD | 重置 Index | 重置 Working Tree |
|---|---|---|---|
--soft | 是 | 否 | 否 |
--mixed(默认) | 是 | 是 | 否 |
--hard | 是 | 是 | 是 |
例如:
# 撤销最后一次 Commit,保留暂存
git reset --soft HEAD~1
# 撤销最后一次 Commit,保留工作区修改但取消暂存
git reset HEAD~1
# 回到指定提交,重置工作区和暂存区
git reset --hard abc123
--hard 具有破坏性,可能使未提交的工作丢失。对于已被其他人拉取的提交,通常不应随意 Reset 后强制推送。
指定文件时:
git reset HEAD -- src/foo.ts
只是将该文件的暂存内容恢复到 HEAD,不移动当前分支。
git revert
创建一个新的反向提交,不删除原历史。
A ── B ── C ── D
↓ revert C
A ── B ── C ── D ── E
git revert <commit>
适合已经 Push 到团队共享分支的提交。
如何选择?
| 需求 | 推荐 |
|---|---|
| 丢弃未暂存修改 | git restore |
| 取消暂存 | git restore --staged |
| 撤销未推送的 Commit,保留修改 | git reset --soft |
| 撤销共享分支上的 Commit | git revert |
| 完全丢弃本地修改并回退 | git reset --hard,谨慎 |
10. 修改最后一次 Commit
修改提交信息:
git commit --amend
忘记添加文件:
git add forgotten.py
git commit --amend --no-edit
--amend 实际上会创建一个新的 Commit 替换当前分支末端的旧 Commit。
如果旧提交已经 Push 到共享分支,应谨慎使用。
11. Stash:临时保存修改
git stash 用于临时保存尚未提交的工作。
git stash
默认保存已跟踪文件的已暂存和未暂存修改,但不包含普通 Untracked 文件。
需要包含新文件:
git stash push -u -m "WIP: profile form"
常用命令:
git stash list
# 恢复最近一次 Stash,并从列表删除
git stash pop
# 恢复但保留 Stash
git stash apply
# 删除指定 Stash
git stash drop stash@{0}
适合开发过程中临时切换任务。
注意:stash pop 可能产生冲突,恢复过程不一定成功。
12. Cherry-pick:选择指定提交
假设:
main: A ── B ── C
feature: A ── B ── D ── E
只想把 D 的修改应用到 main:
git switch main
git cherry-pick <D>
结果:
main: A ── B ── C ── D'
适用场景:
- 从其他分支挑选 Bug Fix。
- 将特定修复移植到发布分支。
- 不希望合并整个功能分支。
Cherry-pick 通常会创建新的 Commit。
13. Reflog:找回丢失的提交
git reflog 记录本地 HEAD 和引用的部分历史变动。
git reflog
例如执行了:
git reset --hard HEAD~2
发现回退错了,可以先通过:
git reflog
找到原来的 Commit ID。
然后创建恢复分支:
git branch recovery <commit-id>
或者确认安全后重新 Reset。
需要注意:
- Reflog 主要是本地记录。
- Reflog 条目会过期。
- 无法保证所有未提交的修改都能找回。
它尤其适合恢复意外 Reset、Rebase 后找不到的 Commit。
14. Tag:版本标签
Tag 通常用于标记发布版本。
轻量标签:
git tag v1.0.0
附注标签:
git tag -a v1.0.0 -m "Release v1.0.0"
推送:
git push origin v1.0.0
查看:
git tag
git show v1.0.0
Tag 与 Branch 不同:
- Branch 通常随新 Commit 移动。
- Tag 通常固定指向特定对象。
15. 强制推送
git push --force
可以强制覆盖远程分支历史,具有较高风险。
更推荐在确实需要改写自己分支历史时使用:
git push --force-with-lease
它会检查远程引用是否仍符合本地预期,降低覆盖他人新提交的风险。
但它不等于绝对安全,尤其在后台 Fetch 或复杂团队协作场景下仍需谨慎。
不要随意强制推送共享的 main 分支。
16. 日常开发工作流
第一步:创建功能分支
git switch main
git pull --ff-only
git switch -c feature/avatar-upload
第二步:开发和提交
git status
git diff
git add .
git diff --staged
git commit -m "feat: add avatar upload"
第三步:推送
git push -u origin feature/avatar-upload
在 GitHub 创建 Pull Request,等待 Review。
第四步:根据 Review 修改
git add .
git commit -m "fix: validate avatar file type"
git push
第五步:PR 合并后清理
git switch main
git pull --ff-only
git branch -d feature/avatar-upload
如果远程功能分支没有被平台自动删除,可以按需删除:
git push origin --delete feature/avatar-upload