前言

在软件开发的世界里,“我的代码昨天还能跑”是最令人崩溃的瞬间之一。你改了某个函数,加了新功能,然后一切崩溃了——而你甚至不记得改了什么。这就是版本控制系统存在的意义,而 Git,就是这个领域的王者。

本文将从零开始,系统地介绍 Git 的核心概念和实用技巧,帮你从”只会 git clone”成长为能在团队中自如协作的 Git 用户。

一、什么是版本控制

想象你在写一篇论文:

  • 论文.doc论文_修改版.doc论文_最终版.doc论文_最终最终版.doc论文_死也不改了.doc

这就是最原始的手动版本控制。问题很明显:混乱、无法追溯、难以协作。

版本控制系统(VCS) 会记录文件在时间维度上的每一次变化。你可以:

  • 随时回退到任意历史版本
  • 查看每次修改的具体内容和修改者
  • 在不影响主线的情况下并行开发新功能
  • 将多个人的修改智能合并

VCS 经历了三代演进:

  1. 本地版本控制:只在本地维护一个简单的数据库来记录差异
  2. 集中式版本控制(如 SVN):所有版本数据存在一个中央服务器上,每个人从服务器检出一个快照
  3. 分布式版本控制(Git):每个人的本地仓库都是完整的镜像,包含全部历史记录

Git 诞生于 2005 年,由 Linus Torvalds(Linux 内核的创造者)开发。当时 Linux 内核社区使用的商业 VCS 工具 BitKeeper 收回了免费使用权,Linus 一怒之下花了不到两周写出了 Git 的第一个版本。

二、Git 的核心理念:快照,而非差异

理解这一点至关重要——大多数 VCS 以”文件变更列表”的方式存储信息,即记录每个文件在不同版本间的差异(delta-based):

1
版本1 → [修改A] → 版本2 → [修改B] → 版本3 → [修改C] → 版本4

Git 将数据视为小型文件系统的一组快照。每次你提交(commit)时,Git 会对所有文件拍一张”照片”,存储这个时刻所有文件的完整状态。如果文件没有变化,Git 不会重复存储,而是保存一个指向上一个相同文件的链接(指针)。

1
2
3
版本1: [完整文件A] [完整文件B] [完整文件C]
版本2: [完整文件A'] [→链接到版本1的B] [完整文件C']
版本3: [→链接到版本2的A'] [完整文件B'] [→链接到版本2的C']

这种设计带来了几个巨大优势: - 切换分支几乎瞬间完成 - 查看任意历史版本无需联网 - 本地操作极快

三、安装与初始配置

安装

Windows:去 git-scm.com 下载安装包,一路默认即可。安装完成后右键菜单会出现”Git Bash Here”,这是一个类 Unix 终端,推荐使用。

macOS:终端执行 git --version,如果没有会自动弹出安装引导;或使用 brew install git

Linuxsudo apt install git (Debian/Ubuntu) 或 sudo yum install git (CentOS)。

最小化配置

安装后第一件事——告诉 Git 你是谁:

1
2
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"

这两个信息会嵌入到你的每一次提交中。--global 表示这是全局配置,对所有仓库生效。去掉这个参数则只对当前仓库生效。

几个其他常用配置:

1
2
3
4
5
6
7
8
9
# 设置默认分支名为 main(GitHub 自 2020 年起默认使用 main 替代 master)
git config --global init.defaultBranch main

# 设置默认编辑器(用于编写提交信息)
git config --global core.editor "code --wait" # VS Code
git config --global core.editor vim # Vim

# 查看所有配置
git config --list

配置文件的存储位置: - 全局:~/.gitconfig~/.config/git/config - 项目级:项目目录/.git/config

四、Git 的三种状态与三个区域

这是 Git 最基本也是最重要的概念。一个文件在 Git 中可能处于三种状态之一:

  1. 已修改(modified):文件已更改,但尚未记录到 Git 中
  2. 已暂存(staged):已标记修改,等待下次提交时纳入版本记录
  3. 已提交(committed):数据已安全存入本地仓库

对应地,Git 项目有三个区域:

1
2
3
4
5
6
7
工作目录 (Working Directory)
│ git add

暂存区 (Staging Area / Index)
│ git commit

本地仓库 (Local Repository / .git directory)
  • 工作目录:你看到的项目文件夹,实际编辑文件的地方
  • 暂存区:一个中间层,存放”下次提交应该包含哪些文件”的信息。这让你可以精细控制每次提交的内容
  • .git 目录:Git 仓库的核心,存储元数据和对象数据库

这就是 Git 工作流的核心循环:

1
修改文件 → git add(加入暂存区)→ git commit(提交到仓库)

一个值得思考的设计:为何需要暂存区?

如果没有暂存区,每次提交都会包含工作目录的所有变更。暂存区让你可以把一个大改动拆成多个有意义的提交。比如你同时修改了三个 bug,可以分别 git add 再分别 git commit,而不是混在一个”修复了一些bug”的提交里。

五、基础操作:从创建到提交

创建仓库

有两种方式开始使用 Git:

1
2
3
4
5
6
# 方式一:在现有项目中初始化
cd my-project
git init

# 方式二:克隆远程仓库
git clone https://github.com/user/repo.git

git init 会在当前目录创建 .git 子目录,包含了仓库的所有骨架文件。此时仓库还是空的,你需要显式地添加文件。

查看状态

这是你使用最频繁的命令:

1
git status

它会告诉你: - 哪些文件被修改了但还没暂存(红色显示) - 哪些文件已经在暂存区准备提交(绿色显示) - 当前在哪个分支

一个简略输出版:git status -sgit status --short

添加与提交

1
2
3
4
5
6
7
8
9
10
11
# 将指定文件加入暂存区
git add file.txt

# 将当前目录下所有变更加入暂存区
git add .

# 提交暂存区内容到仓库
git commit -m "描述你的修改"

# 跳过暂存区,直接提交所有已跟踪文件的修改(注意:新文件不会包含在内)
git commit -a -m "跳过暂存直接提交"

关于提交信息(Commit Message)

提交信息是给你未来的自己(和同事)看的。一个约定俗成的格式:

1
2
3
<type>: <简短描述>

<详细说明(可选)>

常见的 type: - feat:新功能 - fix:Bug 修复 - docs:文档修改 - refactor:重构(不改变功能) - style:代码格式(不影响逻辑) - test:测试相关 - chore:构建或辅助工具的变动

示例:

1
2
3
4
fix: 修复登录页面的密码验证不生效

之前当密码包含特殊字符时,正则表达式匹配失败。
改为直接进行字符串比较,并在服务端做二次校验。

查看历史

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看提交历史
git log

# 简洁的单行输出
git log --oneline

# 带分支图的单行输出
git log --oneline --graph --all

# 查看某个文件的历史
git log -- file.txt

# 查看每次提交的具体改动
git log -p

# 按作者、日期等筛选
git log --author="John" --since="2025-01-01"

六、撤销操作

修改最近一次提交

1
2
3
4
# 遗漏了文件或者想改提交信息
git commit -m "初始提交"
git add 遗漏的文件
git commit --amend

--amend 会用新的提交替代上一个。注意:如果已经推送到远程,不要对已推送的提交使用 --amend,这会搞乱别人的历史。

取消暂存与撤销修改

1
2
3
4
5
6
7
8
# 将文件从暂存区移除(保留工作目录的修改)
git restore --staged file.txt

# 丢弃工作目录的修改(恢复到上次提交的状态)
git restore file.txt

# 恢复到某个历史版本的状态
git restore --source=HEAD~2 file.txt

版本回退

1
2
3
4
5
6
7
8
# 撤销最近一个提交,但保留修改在工作目录
git reset --soft HEAD~1

# 撤销最近一个提交,保留修改但不暂存
git reset --mixed HEAD~1

# 完全撤销最近一个提交(修改也会丢失!)
git reset --hard HEAD~1

三个参数的区别: - --soft:只移动 HEAD 指针,暂存区和工作目录都不变 - --mixed(默认):移动 HEAD,重置暂存区,工作目录不变 - --hard:移动 HEAD,重置暂存区,工作目录也会被重置——危险操作

当你彻底搞砸了,还可以用 git reflog 查看所有 HEAD 的移动记录,找回”丢失”的提交:

1
2
3
git reflog
# 找到你想恢复的提交的 hash,比如 abc1234
git reset --hard abc1234

七、远程仓库

管理远程地址

1
2
3
4
5
6
7
8
9
10
11
# 查看远程仓库
git remote -v

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

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

# 删除远程仓库
git remote remove origin

“origin” 只是约定俗成的名称,你可以起任何名字。

推送与拉取

1
2
3
4
5
6
7
8
9
10
11
# 推送到远程(-u 设置上游分支,之后可以只用 git push)
git push -u origin main

# 普通推送
git push

# 拉取远程更新并合并
git pull

# 仅拉取不合并
git fetch

fetch vs pull

git pull = git fetch + git merge。它直接从远程拉取并合并到当前分支。

git fetch 只拉取远程数据,不影响你的工作目录。你可以先看看远程有什么变化,再决定是否合并:

1
2
3
git fetch origin
git log origin/main --oneline # 看看远程多了哪些提交
git merge origin/main # 确认无误后合并

推荐在不确定远程变更内容时用 fetch 代替 pull。

HTTP vs SSH

远程地址有两种协议:

  • HTTPShttps://github.com/user/repo.git——方便,但每次推送需要输密码(可使用 credential helper 缓存)
  • SSHgit@github.com:user/repo.git——需要配置 SSH Key,但配置好后无需再输密码

配置 SSH Key:

1
2
3
4
# 生成密钥
ssh-keygen -t ed25519 -C "your.email@example.com"

# 将公钥内容(~/.ssh/id_ed25519.pub)添加到 GitHub 的 Settings → SSH and GPG keys

八、分支

什么是分支

分支是 Git 的灵魂。本质上,分支只是一个指向某个提交对象的可移动指针

1
2
3
4
5
                    main

A ─── B ─── C ─── D

feature

创建一个新分支只需要写 41 个字节(一个 SHA-1 哈希值)。这就是 Git 分支如此轻量的原因。

分支操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 查看所有分支
git branch # 本地
git branch -r # 远程
git branch -a # 全部

# 创建分支
git branch feature-login

# 切换分支
git switch feature-login
# 或旧式语法
git checkout feature-login

# 创建并切换到新分支
git switch -c feature-payment
# 旧式语法
git checkout -b feature-payment

# 删除分支
git branch -d feature-login # 安全删除(已合并才允许)
git branch -D feature-login # 强制删除

# 重命名分支
git branch -m old-name new-name

合并分支

1
2
3
4
5
# 将 feature 分支合并到当前分支
git merge feature

# 合并时禁止快进(fast-forward),始终生成合并提交
git merge --no-ff feature

合并分两种方式:

  1. 快进合并(Fast-forward):当被合并分支直接位于当前分支的下游时,Git 只是把指针往前移,不会创建新的合并提交。
1
2
3
4
5
6
7
8
9
10
11
12
13
合并前:
main

A ─── B

feature

合并后(fast-forward):
main

A ─── B ─── C ─── D

feature
  1. 三方合并(Three-way merge):当两个分支各自有提交时,Git 会找出它们的共同祖先,进行三方合并。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
合并前:
main

A ─── B ─── C ─── D

└── E ─── F

feature

合并后(merge commit):
main

A ─── B ─── C ─── D ─── M(合并提交)
│ │
└── E ─── F ─────┘

feature

变基(Rebase)

1
2
3
4
5
# 将当前分支变基到 main 上
git rebase main

# 交互式变基(强大且实用)
git rebase -i HEAD~3 # 对最近3个提交进行操作

变基会把你当前分支的所有提交”重新播放”到目标分支的顶端,结果是线性的提交历史。

1
2
3
4
5
6
7
变基前:
A ─── B ─── C (main)

└── D ─── E (feature)

变基后:
A ─── B ─── C (main) ─── D' ─── E' (feature)

merge vs rebase 的选择: - merge 保留完整历史,适合公共分支 - rebase 产生干净线性历史,适合私有分支整理 - 黄金法则:不要对已经推送到公共仓库的提交执行 rebase

交互式变基极其强大,你可以:

1
2
3
4
5
git rebase -i HEAD~3
# 编辑器中会显示:
# pick abc1234 功能A
# pick def5678 功能B
# pick ghi9012 修复了一个小bug

你可以执行以下操作: - pick:保留该提交 - reword:修改提交信息 - squash:将该提交合并到前一个提交 - fixup:同 squash,但丢弃提交信息 - drop:删除该提交 - edit:暂停变基让你修改提交内容

解决合并冲突

当两个分支修改了同一个文件的同一部分时,Git 无法自动合并,就需要手动解决冲突。冲突文件会显示类似这样的标记:

1
2
3
4
5
<<<<<<< HEAD
当前分支的版本
=======
被合并分支的版本
>>>>>>> feature-branch

解决步骤:

1
2
3
4
5
# 1. 编辑冲突文件,手动选择保留的内容,删除标记符号
# 2. 标记为已解决
git add 冲突文件
# 3. 继续合并/变基
git merge --continue # 或 git rebase --continue

如果搞不定想放弃:

1
2
git merge --abort       # 放弃合并
git rebase --abort # 放弃变基

九、常见分支策略

Git Flow

经典的分支模型,适合有明确发布周期的项目:

1
2
3
4
5
6
7
8
9
main ─────●──────────────●────────●
│ │ │
develop ──┼──●──●──●───●──●──●───┼
│ │ │ │
feature ──┘ │ │ │
│ │ │
release ─────┘ ─────────┘ │

hotfix ──────────────────────────┘
  • main:生产就绪代码,只接受来自 release 和 hotfix 的合并
  • develop:开发主线,feature 分支合并到这里
  • feature/xxx:从 develop 分出,开发完后合并回 develop
  • release/x.x:从 develop 分出,准备发布,只做 bug 修复
  • hotfix/xxx:从 main 分出,紧急修复生产环境问题,合并回 main 和 develop

GitHub Flow

GitHub 使用的简化模型,适合持续部署:

  • main 分支始终可部署
  • 从 main 创建描述性分支(如 fix-login-validation
  • 提交到该分支,并定期推送到远程同名分支
  • 需要反馈或准备合并时,发起 Pull Request
  • 审查通过后合并到 main,并立即部署

这个模型的核心:任何进入 main 的代码都经过了 Pull Request

Trunk-Based Development

一个极端的简化策略:所有人直接在 main(或 trunk)上提交,分支存活时间极短(不超过一天)。依赖优秀的测试覆盖和特性开关(feature flag)来保证稳定性。Google 和 Facebook 等大厂使用此模式。

十、实用进阶技巧

储藏(Stash)

当你工作到一半需要切换分支,但又不想提交时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 储藏所有修改
git stash

# 包含未跟踪的文件
git stash -u

# 查看储藏列表
git stash list

# 恢复最近一次储藏
git stash pop # 恢复并从列表中删除
git stash apply # 恢复但保留在列表中

# 恢复指定储藏
git stash apply stash@{2}

# 删除指定储藏
git stash drop stash@{0}

挑选(Cherry-pick)

只想要另一个分支上的某一个提交,而不是合并整个分支:

1
2
3
4
git cherry-pick abc1234

# 挑选多个提交
git cherry-pick abc1234 def5678

标签(Tag)

标记重要节点(如发布版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建轻量标签
git tag v1.0.0

# 创建附注标签(推荐,包含作者、日期、信息)
git tag -a v1.0.0 -m "第一次正式发布"

# 查看标签
git tag

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

# 在特定提交上打标签
git tag -a v1.0.0 abc1234 -m "v1.0.0"

二分查找(Bisect)

你发现了一个 bug,但不确定是哪次提交引入的。Git 可以用二分法帮你定位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 启动二分查找
git bisect start

# 标记当前版本是坏的
git bisect bad

# 标记一个确认好的版本
git bisect good abc1234

# Git 会自动切换到一个中间版本
# 你测试后告诉 Git 结果:
git bisect good # 这个版本是好的
# 或
git bisect bad # 这个版本是坏的

# 重复测试几次,Git 会定位到引入 bug 的提交

# 结束后恢复正常状态
git bisect reset

忽略文件

创建 .gitignore 文件来告诉 Git 忽略特定的文件或模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 编译产物
*.o
*.out
*.exe
build/
dist/

# 依赖
node_modules/

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

# 系统文件
.DS_Store
Thumbs.db

# 环境变量(包含密码等敏感信息)
.env
.env.local

# 日志
*.log

别名

一些能大幅提升效率的别名:

1
2
3
4
5
6
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.st status
git config --global alias.lg "log --oneline --graph --all"
git config --global alias.unstage "restore --staged"
git config --global alias.last "log -1 HEAD"

之后 git st 就等同于 git statusgit lg 就会显示漂亮的提交历史图。

十一、协作工作流

Pull Request 流程

  1. Fork 目标仓库(如果是别人的项目)
  2. Clone 到本地
  3. 创建功能分支:git switch -c my-feature
  4. 开发、提交、推送
  5. 在 GitHub/GitLab 上发起 Pull Request
  6. 代码审查(Code Review)
  7. 讨论修改,可能需要推送额外提交
  8. 合并

保持分支同步

当你开发功能时,main 分支可能已经前进:

1
2
3
4
5
6
7
8
9
10
# 先拉取最新的远程信息
git fetch origin

# 方式一:合并 main 到功能分支
git switch feature-branch
git merge origin/main

# 方式二:变基到最新的 main(产生线性历史)
git switch feature-branch
git rebase origin/main

Squash 合并

很多团队在合并 PR 时使用 squash merge,将一个分支上的多个提交压成一个:

1
2
3
4
5
6
7
PR 分支:
abc1234 实现登录
def5678 修复类型错误
ghi9012 添加输入验证

Squash 合并后 main 上只有一个提交:
jkl0123 feat: 实现用户登录功能

这保持了 main 分支历史整洁,但代价是丢失了功能分支上的提交粒度。

十二、与 GitHub 的协同

GitHub 不只是代码托管

  • Issues:任务跟踪、bug 报告、功能请求
  • Pull Requests:代码审查和合并请求
  • Actions:CI/CD 自动化工作流
  • Projects:看板式项目管理
  • Wiki:项目文档
  • Releases:基于 Git Tag 的版本发布

一个示例:修复 bug 的完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 1. 创建 issue(在 GitHub 网页上)

# 2. 本地创建修复分支
git switch -c fix-login-error

# 3. 修复 bug 并提交
git add src/login.js
git commit -m "fix: 修复密码验证在特殊字符时失败的问题"

# 4. 推送分支
git push -u origin fix-login-error

# 5. 在 GitHub 上创建 PR,描述中写 "Closes #42"(关联 issue)

# 6. 同事审查代码,提出修改意见

# 7. 根据意见修改并追加提交
git add src/login.js
git commit -m "fixup: 处理 code review 意见"
git push

# 8. 审查通过,合并到 main(Squash merge)

# 9. 删除远程分支(GitHub 会在合并后提示一键删除)
git push origin --delete fix-login-error

# 10. 本地切回 main 并更新
git switch main
git pull

# 11. 清理本地分支
git branch -d fix-login-error

十三、常见问题与排查

提交到了错误的分支

1
2
3
4
5
6
7
# 撤销最近 3 个提交但保留修改
git reset --soft HEAD~3
# 暂存到正确分支
git stash
git switch correct-branch
git stash pop
git commit

不小心提交了敏感信息

如果还没推送:

1
2
3
4
git reset --soft HEAD~1
# 修改文件,排除敏感信息
git add .
git commit -c ORIG_HEAD # 复用之前的提交信息

如果已经推送:

  1. 立即修改密码/密钥(已经暴露了!)
  2. 使用 git filter-branchBFG Repo-Cleaner 从历史中清除
  3. 强制推送(警告:会搞乱协作者的仓库)

合并冲突时的心理建设

合并冲突不是 Git 的 bug,而是 Git 发现两个修改逻辑互斥时请求你的决策。解决冲突时:

  1. 理解双方修改的意图
  2. 和原作者沟通(如果可能)
  3. 保留你想要的部分,或写一个新的合并方案
  4. 删除冲突标记(<<<<<<<=======>>>>>>>
  5. 测试确认

detached HEAD 状态

当你 checkout 到某个具体提交而不是分支时,会进入 detached HEAD 状态:

1
2
git checkout abc1234
# "You are in 'detached HEAD' state..."

此时你的修改不会属于任何分支。解决方案:

1
2
3
4
5
# 如果要保留此次的修改
git switch -c new-branch-name

# 如果只是看看,看完切回去就好
git switch main

十四、工具推荐

  • VS Code:内置 Git 集成极其优秀,可视化 diff 和冲突解决
  • GitHub Desktop:适合不习惯命令行的用户
  • Sourcetree:功能强大的免费 Git GUI
  • Lazygit:终端里的 TUI Git 客户端,效率极高
  • tig:终端里的 Git 浏览器,查看历史非常方便
  • git-extras:一组额外的 Git 命令(如 git ignoregit changeloggit undo

十五、学习建议

不要死记命令

Git 命令非常多(150+),但日常使用的也就 10-15 个。更重要的是理解 Git 的数据模型——一旦理解了”快照+指针”的核心理念,大部分命令都能推导出来。

推荐的练习方式

  1. git init 创建一个测试仓库,随意练习
  2. 使用 git log --oneline --graph --all 频繁查看状态
  3. 安装 lazygit,它的交互式界面能帮你直观感受分支变化
  4. 阅读 Pro Git(免费在线,有中文版)
  5. 在真实的团队项目中实践——这是最有效的学习方式

心态

  • 不要害怕”搞坏”仓库——Git 几乎所有操作都是可撤销的(git reflog 是救命稻草)
  • 频繁提交,小而精的提交比大坨的提交好得多
  • 提交信息写清楚,三个月后的你会感谢现在的你
  • push 前多看一眼 git statusgit diff

结语

Git 是一种思维方式。它教会你用版本化的视角看待代码的演进,用分支来管理并行和不确定性。刚开始可能会觉得繁琐,但当你真正遇到”代码回不去了”或”多人协作时冲突乱成一团”的情况时,你会感谢曾经花时间学习 Git 的自己。

记住,任何一个 Git 高手都是从 git init 开始的。