Git 基础操作

理解 Git 三层架构与对象模型,掌握日常操作背后的机制,建立版本控制的工程直觉。

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 的价值在于:

  1. 原子性提交:修改了 5 个文件,但只有 3 个属于同一个逻辑变更,可以只 stage 这 3 个
  2. 部分暂存git add -p 可以按 hunk(代码块)级别选择暂存内容
  3. 检查点:在 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 的内部流程:

  1. 将暂存区的所有文件创建为 tree 对象
  2. 创建 commit 对象,包含 tree 引用、parent 引用、作者信息、提交信息
  3. 将当前分支指针前移到新 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 后创建分支。


延展阅读