← 编程返回顶部

Git:版本控制与团队协作

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 → 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

对比MergeRebase
原有历史保留重写被重放的提交
提交图可能产生分叉和 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
撤销共享分支上的 Commitgit 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