Git 基础操作
为什么要深入理解 Git 基础
Git 是现代前端工程师每天使用频率最高的工具之一。大多数开发者停留在 add → commit → push 的肌肉记忆层面,一旦遇到 detached HEAD、丢失 commit、或需要从 reflog 恢复数据时就束手无策。深入理解 Git 的内部机制,能将 Git 从"黑盒工具"转变为"精确可控的版本管理引擎"。
面试定位:Git 基础是工程素养的试金石。面试官通过"Git 的三个区域分别是什么?""commit 对象的结构?""HEAD 指向什么?"等问题,判断候选人对工具链的理解深度。
Git 的三层架构
Working Directory → Staging Area → Repository
Git 的核心工作流建立在三个区域的数据流转之上:
Working Directory → Staging Area (Index) → Repository (.git)
(工作区) git add (暂存区) git commit (本地仓库)
Working Directory(工作区):项目的实际文件系统。你编辑的所有文件都在这里。
Staging Area / Index(暂存区):一个二进制文件(.git/index),记录了下一次 commit 将要包含的文件快照。它是 Git 设计中最精妙的抽象——允许开发者精细控制每次 commit 的内容。
Repository(本地仓库):.git 目录,包含所有 commit 历史、分支指针、配置等。
为什么需要 Staging Area
很多初学者疑惑"为什么不能直接 commit?"。Staging Area 的价值在于:
- 原子性提交:修改了 5 个文件,但只有 3 个属于同一个逻辑变更,可以只 stage 这 3 个
- 部分暂存:
git add -p可以按 hunk(代码块)级别选择暂存内容 - 检查点:在 commit 前通过
git diff --staged审查即将提交的内容
# 查看工作区与暂存区的差异
git diff
# 查看暂存区与最近一次 commit 的差异
git diff --staged
# 按 hunk 交互式暂存
git add -p
Git 对象模型
四种核心对象
Git 本质上是一个 content-addressable 文件系统,所有数据存储为四种对象:
| 对象类型 | 作用 | 内容 |
|---|---|---|
| Blob | 存储文件内容 | 纯文件数据(不包含文件名) |
| Tree | 存储目录结构 | 指向 blob 和子 tree 的指针列表 |
| Commit | 存储提交信息 | 指向顶层 tree + parent commit + 作者 + 时间戳 + message |
| Tag | 存储标签信息 | 指向特定 commit 的有注释引用 |
SHA-1 哈希与内容寻址
每个 Git 对象通过 SHA-1 哈希标识(40 个十六进制字符)。哈希由对象类型 + 内容长度 + 内容计算得出:
# 查看 commit 对象的内容
git cat-file -p HEAD
# 输出示例:
# tree 4b825dc642cb6eb9a060e54bf899d8e9e1aded91
# parent 8a5cbc430f1a9c3d00b3425e3f6ff20015961541
# author Alice <[email protected]> 1700000000 +0800
# committer Alice <[email protected]> 1700000000 +0800
#
# feat: add user authentication module
# 查看 tree 对象
git cat-file -p HEAD^{tree}
# 查看对象类型
git cat-file -t <sha1>
内容相同 = 哈希相同
这个设计意味着:
- 两个相同内容的文件只存储一份 blob
- Git 天然具备去重能力
- 任何数据篡改都会改变哈希,保证了完整性
# 手动计算一个 blob 的哈希
echo -n "Hello" | git hash-object --stdin
# 输出: ce013625030ba8dba906f756967f9e9ca394464a
核心操作详解
git init — 初始化仓库
git init
# 创建 .git 目录,包含以下关键子目录:
# .git/objects/ — 对象数据库
# .git/refs/ — 分支和标签指针
# .git/HEAD — 指向当前分支的指针
# .git/config — 仓库级配置
# .git/index — 暂存区
git add — 暂存变更
git add 的本质是:将工作区文件的当前内容创建为 blob 对象,并更新 index 文件:
# 暂存单个文件
git add src/utils.ts
# 暂存所有已跟踪文件的修改(不包含新文件)
git add -u
# 暂存所有变更(包含新文件、修改、删除)
git add -A
# 按代码块暂存(最精细的控制)
git add -p
# 交互选项:
# y = 暂存此 hunk
# n = 跳过此 hunk
# s = 将 hunk 拆分为更小的块
# e = 手动编辑 hunk
git commit — 创建提交
git commit 的内部流程:
- 将暂存区的所有文件创建为 tree 对象
- 创建 commit 对象,包含 tree 引用、parent 引用、作者信息、提交信息
- 将当前分支指针前移到新 commit
# 基础提交
git commit -m "feat: add login component"
# 带详细描述的提交
git commit -m "feat: add login component" -m "Implements OAuth2 flow with Google and GitHub providers"
# 修改最近一次 commit(谨慎使用,会改变历史)
git commit --amend
# 跳过暂存直接提交已跟踪文件的修改
git commit -a -m "fix: resolve null pointer"
git log — 查看历史
# 精简单行显示
git log --oneline
# 图形化分支显示
git log --oneline --graph --all
# 查看特定文件的修改历史
git log --follow -- src/components/Button.tsx
# 查看某段时间的提交
git log --after="2024-01-01" --before="2024-06-30"
# 按作者过滤
git log --author="Alice"
# 搜索提交信息
git log --grep="fix"
# 查看每次提交的修改内容
git log -p
# 查看每次提交的文件变更统计
git log --stat
git diff — 比较差异
# 工作区 vs 暂存区
git diff
# 暂存区 vs 最近 commit
git diff --staged
# 两个 commit 之间的差异
git diff abc123..def456
# 两个分支之间的差异
git diff main..feature/login
# 只看文件名列表
git diff --name-only main..feature/login
# 查看特定文件的差异
git diff -- src/utils.ts
HEAD、分支与引用
HEAD 是什么
HEAD 是一个特殊指针,通常指向当前分支的引用(symbolic ref):
# 查看 HEAD 指向
cat .git/HEAD
# 输出: ref: refs/heads/main
# HEAD 指向具体 commit(detached HEAD 状态)
git checkout abc1234
cat .git/HEAD
# 输出: abc1234...(直接是 commit hash)
分支是什么
分支只是一个指向 commit 的可变指针。创建分支的成本几乎为零(只是创建一个包含 40 字符 SHA 的文件):
# 查看分支指向的 commit
cat .git/refs/heads/main
# 输出: 8a5cbc430f1a9c3d00b3425e3f6ff20015961541
# 创建分支
git branch feature/auth
# 等价于: 创建文件 .git/refs/heads/feature/auth,内容为当前 HEAD 的 SHA
相对引用
# HEAD 的父 commit
HEAD~1 或 HEAD^
# HEAD 的祖父 commit
HEAD~2
# merge commit 的第二个父 commit
HEAD^2
# 组合使用
HEAD~2^2
撤销操作全景
场景一:撤销工作区的修改(未 add)
# 撤销单个文件
git checkout -- src/utils.ts
# 或 Git 2.23+ 的新语法
git restore src/utils.ts
场景二:撤销暂存(已 add 但未 commit)
# 取消暂存但保留工作区修改
git reset HEAD src/utils.ts
# 或 Git 2.23+
git restore --staged src/utils.ts
场景三:撤销最近一次 commit(已 commit 但未 push)
# 软重置:撤销 commit,保留暂存区和工作区
git reset --soft HEAD~1
# 混合重置(默认):撤销 commit 和暂存,保留工作区
git reset HEAD~1
# 硬重置:撤销一切(危险操作)
git reset --hard HEAD~1
场景四:已 push 的 commit 需要撤销
# 创建一个反向 commit(安全的公共历史修改方式)
git revert HEAD
# 这会创建一个新 commit,内容是撤销 HEAD 的所有变更
场景五:找回"丢失"的 commit
# reflog 记录了 HEAD 的所有移动历史
git reflog
# 找到目标 commit 的 SHA,然后
git checkout <sha>
# 或创建分支指向它
git branch recovery <sha>
.gitignore 策略
基本语法
# 忽略所有 node_modules 目录
node_modules/
# 忽略所有 .env 文件(安全敏感)
.env
.env.local
.env.*.local
# 忽略构建产物
dist/
build/
.next/
out/
# 忽略操作系统文件
.DS_Store
Thumbs.db
# 忽略 IDE 配置(团队应统一决定是否忽略)
.vscode/
.idea/
# 但保留特定文件
!.vscode/settings.json
!.vscode/extensions.json
# 忽略日志
*.log
npm-debug.log*
全局 gitignore
# 设置全局忽略文件(针对个人环境)
git config --global core.excludesFile ~/.gitignore_global
已跟踪文件的忽略
# 如果文件已被跟踪,仅修改 .gitignore 不会生效
# 需要先从 index 中删除
git rm --cached .env
git commit -m "chore: remove .env from tracking"
Git 配置层级
Git 配置分三层,优先级从低到高:
| 层级 | 文件位置 | 作用范围 |
|---|---|---|
| System | /etc/gitconfig |
所有用户 |
| Global | ~/.gitconfig |
当前用户 |
| Local | .git/config |
当前仓库 |
# 常用配置
git config --global user.name "Alice"
git config --global user.email "[email protected]"
# 设置默认分支名
git config --global init.defaultBranch main
# 设置默认编辑器
git config --global core.editor "code --wait"
# 启用颜色输出
git config --global color.ui auto
# 设置命令别名
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.lg "log --oneline --graph --all"
# 查看所有配置及来源
git config --list --show-origin
实际工程场景
场景一:紧急修复线上 Bug
# 1. 保存当前工作进度
git stash push -m "WIP: feature/payment"
# 2. 切换到主分支创建修复分支
git checkout main
git pull origin main
git checkout -b hotfix/login-crash
# 3. 修复、提交、推送
git add src/auth/login.ts
git commit -m "fix: resolve null reference in login flow"
git push -u origin hotfix/login-crash
# 4. 修复完成后恢复之前的工作
git checkout feature/payment
git stash pop
场景二:误删了文件
# 从最近的 commit 恢复文件
git checkout HEAD -- src/deleted-file.ts
# 从特定 commit 恢复
git checkout abc1234 -- src/deleted-file.ts
场景三:查找引入 Bug 的 commit
# 使用 git bisect 二分查找
git bisect start
git bisect bad # 当前版本有 bug
git bisect good v1.2.0 # v1.2.0 版本正常
# Git 会自动 checkout 中间的 commit
# 测试后标记
git bisect good # 或 git bisect bad
# 重复直到找到首次引入 bug 的 commit
# 完成后退出
git bisect reset
Stash 的高级用法
# 保存包含未跟踪文件的 stash
git stash push -u -m "WIP: include new files"
# 查看 stash 列表
git stash list
# 查看 stash 内容
git stash show -p stash@{0}
# 应用 stash 但不删除
git stash apply stash@{0}
# 应用并删除
git stash pop
# 从 stash 创建分支(解决冲突时有用)
git stash branch new-branch stash@{0}
# 删除特定 stash
git stash drop stash@{1}
# 清空所有 stash
git stash clear
面试高频问题
Q: Git 的工作区、暂存区、仓库三者的关系?
回答要点:Working Directory 是实际文件系统,Staging Area(Index)是下一次 commit 的预览快照(存储在 .git/index),Repository 是完整的提交历史图谱(.git/objects)。git add 将工作区的内容快照到暂存区,git commit 将暂存区的快照固化为一个 commit 对象。这种三层设计的核心优势是允许开发者精细控制每次提交的粒度。
Q: git reset 的三种模式有什么区别?
回答要点:--soft 只移动 HEAD 指针,暂存区和工作区不变(适合重新组织 commit);--mixed(默认)移动 HEAD 并重置暂存区,工作区不变(适合重新选择要暂存的文件);--hard 三个区域全部重置(危险操作,不可逆,除非通过 reflog 恢复)。在已 push 的 commit 上应使用 git revert 而非 git reset。
Q: 什么是 detached HEAD?如何恢复?
回答要点:当 HEAD 直接指向一个 commit(而非分支引用)时,就处于 detached HEAD 状态。这发生在 git checkout <sha> 或 git checkout v1.0 时。在此状态下的新 commit 不属于任何分支,切换分支后可能"丢失"。恢复方法:git branch <new-branch> 为当前位置创建分支,或通过 git reflog 找到 commit SHA 后创建分支。
延展阅读
- Pro Git Book — Git Internals — 官方权威教材,深入讲解 Git 底层机制
- Git Reference Manual — 完整命令参考
- Learn Git Branching — 交互式可视化学习 Git 分支操作
- GitHub Git Cheat Sheet — 常用命令速查
- Atlassian Git Tutorials — 系统化 Git 教程
- Oh Shit, Git!?! — 常见 Git 错误的修复方法