1.Git 是什么

Git 是一个分布式版本控制系统。简单说,它帮你记录文件的每一次修改,让你可以随时回退到任意历史版本。

核心思想:快照,不是差异。

很多版本控制系统(如 SVN)存储的是「每次改了什么」(差异补丁)。Git 不同——它每次保存的是整个项目的快照(snapshot),像是给所有文件拍了一张照片。

但这不意味着 Git 每次都复制一份完整文件。Git 用了一个巧妙的设计:

  • 没有变化的文件,Git 只存储一个指向上次快照的引用

  • 只有变化的文件,Git 才存储新的内容

所以 Git 的存储效率很高,同时又能瞬间切换到任意版本。

Git 的设计哲学:一切都可追溯,一切皆可回退。

Git 对象模型:blob、tree、commit

Git 的底层是三种对象,存在 .git/objects/ 目录下:

blob(文件内容)

存储文件的具体内容。一个 blob 就是一份文件数据,不包含文件名。

tree(目录结构)

存储目录的信息:哪些文件名对应哪些 blob,哪些子目录对应哪些 tree。

一个 tree 就是一张「文件名 → blob」的映射表。

commit(提交)

存储一次提交的全部信息:

  • 指向一个 tree(这次提交的项目快照)

  • 指向父 commit(上一次提交)

  • 作者、时间、提交信息

commit -> tree -> blob
  |
parent commit -> tree -> blob

每个对象用 SHA-1 哈希值作为唯一标识(如 a1b2c3d)。这就是为什么 Git 能保证数据完整性——内容变了,哈希就变了。

Git 的四个区域

这是理解 Git 工作流的关键。看看上方的四个区域:

1. Working Directory(工作区)

你实际编辑文件的地方。你用编辑器改代码,改的就是工作区的文件。

2. Staging Area / Index(暂存区)

一个「准备区」。你选择哪些修改要放进下一次提交。git add 就是把文件放进暂存区。

3. Local Repository(本地仓库)

提交历史存在这里。git commit 就是把暂存区的内容打包成一个快照,存进本地仓库。

4. Remote Repository(远程仓库)

服务器上的仓库(如 GitHub)。git push 把本地提交推到远程,git pull 把远程更新拉到本地。

工作区 --git add--> 暂存区 --git commit--> 本地仓库 --git push--> 远程仓库
                       ^                                              |
                       └──────────────── git pull ─────────────────────┘

暂存区的存在意义:让你可以精确控制每次提交包含哪些修改。不用每次都把所有改动一起提交。

2.初始化与配置

git init — 初始化仓库

git init 在当前目录创建一个 .git 隐藏文件夹,这个文件夹就是 Git 仓库的全部。

.git 目录包含:

  • objects/ — 所有 blob、tree、commit 对象

  • refs/ — 分支和标签的指针

  • HEAD — 指向当前分支

  • index — 暂存区

  • config — 仓库级配置

运行 git init 后,你的目录就变成了一个 Git 仓库。之后所有的版本控制操作都在这个目录下进行。

试试看:在终端输入 git init,观察上方「工作区」的变化。

git config — 配置用户信息

Git 需要知道「你是谁」才能记录提交的作者信息。

设置用户名和邮箱:

git config user.name "你的名字"
git config user.email "your@email.com"

查看当前配置:

git config user.name
git config user.email

这三个配置级别:

  • --local(默认)— 只对当前仓库生效,存在 .git/config

  • --global — 对当前用户的所有仓库生效,存在 ~/.gitconfig

  • --system — 对所有用户生效

git config --global user.name "你的名字"
git config --global user.email "your@email.com"

建议:用 --global 设置你的名字和邮箱,这样每个新仓库都会自动使用。

创建文件

在使用 Git 之前,我们先创建一些文件。以下是常用的辅助命令:

你也可以在工作区右键鼠标点击,选择「创建文件」或者右键已存在的文件「删除」。

双击文件也可以编辑,可通过红绿颜色区分已暂存和未暂存的修改。

touch — 创建空文件

touch readme.md

echo + > — 写入内容

echo "hello world" > readme.md

> 会覆盖文件原有内容。如果文件不存在,会自动创建。

echo + >> — 追加内容

echo "第二行" >> readme.md

>> 会在文件末尾追加,不影响已有内容。

cat — 查看文件内容

cat readme.md

把文件内容打印到终端。

这些不是 git 命令,但它们帮我们准备 git 要管理的文件。

试试看:先创建文件,再用 echo 写入内容

3.日常基础

git add — 暂存你的修改

git add 把文件从工作区移到暂存区。

暂存区(Index)是 Git 特有的概念。它是一个「预提交区」——你可以选择性地把某些修改放进去,而不是把所有改动一次性提交。

用法:

  • git add file.txt — 暂存单个文件

  • git add . — 暂存所有修改

  • git add *.js — 暂存所有 .js 文件

执行 git add 后,看看上方区域的变化——文件应该从「工作区」出现在「暂存区」了。

git add 不会创建提交,只是把文件放进「准备区」。你可以多次 add 后一次性 commit。

git status — 查看状态

git status 显示工作区和暂存区的当前状态。

它会告诉你:

  • 哪些文件已暂存(准备提交)

  • 哪些文件已修改但未暂存

  • 哪些文件未跟踪(新文件,Git 还不知道它的存在)

这是你最常用的命令之一。当你不确定当前状态时,先跑一下 git status

试试看:创建了文件之后,运行 git status 看看输出。

git diff — 查看差异

git diff 显示文件的具体修改内容,逐行比较两个版本,告诉你改了什么。

两种常用用法:

  • git diff --staged — 比较暂存区 vs 上一次 commit,看你 add 了什么

  • git diff — 比较工作区 vs 暂存区,看你改了但还没 add 的内容

输出格式:

- hello world     (旧行,被删除)
+ Hello Git       (新行,被添加)

刚才你已经 add 了 readme.md,现在用 git diff --staged 看看暂存区里有什么。

git commit — 提交更改

git commit -m "提交信息" 把暂存区的内容打包成一个 commit,存入本地仓库。

一次 commit 就是一份项目快照——记录了这一刻所有文件的状态。

提交信息应该简洁描述做了什么:

git commit -m "Add readme file"

提交后,看看上方「本地仓库」区域——你的第一个 commit 出现了!

每次 commit 都会记录:改了什么文件、改了什么内容、上一次 commit 是谁。这样就形成了一条完整的历史链。

git log — 查看提交历史

git log 显示从最新到最早的提交记录。真实输出格式如下:

commit a1b2c3d (HEAD -> main)
Author: Your Name <your@email.com>
Date:   Mon Jun 2 10:00:00 2025

    Add readme

HEAD -> main 表示 HEAD 当前指向 main 分支的最新 commit。HEAD 记录的就是「你当前在哪个 commit 上」——切换分支时,HEAD 会跟着移动。

常用选项:

  • git log -n 3 — 只看最近 3 条

  • git log --oneline — 简洁模式(一行一条)

试试看,你刚才的 commit 应该出现在历史里了。

git restore — 恢复文件

git restore 专门用于恢复文件内容。数据通常在三个地方流动:工作区、暂存区、提交历史(HEAD)。

场景 1:丢弃工作区的修改(最常用)

git restore readme.md

把工作区的文件恢复成暂存区里的样子。如果没暂存过,就恢复成最后一次提交的样子。

场景 2:取消暂存(撤销 git add)

git restore --staged readme.md

把文件从暂存区撤出来,但不会改变工作区你刚写的代码。

场景 3:同时丢弃暂存区和工作区的修改

git restore --staged --worktree readme.md

彻底放弃这个文件的所有修改,让它直接回到最后一次提交的状态。

场景 4:从历史提交中恢复文件

git restore --source=HEAD~1 readme.md

把上一次提交里的文件,直接拉到现在的工作区里。

注:HEAD 代表当前最新提交,HEAD~1 代表上一次提交,HEAD~2 是上两次,依此类推。

git checkout Hash

切换到指定的commit。

要注意使用该指令时处于分离HEAD状态,最好不要在此基础上修改文件。该指令常用于临时查看历史commit的内容

4.分支操作

git branch — 分支操作

分支是 Git 最强大的功能之一。

什么是分支?

分支就是一个指向某个 commit 的标记。创建分支只是加个标记,瞬间完成。

用法:

  • git branch — 列出所有分支

  • git branch dev — 创建名为 dev 的新分支

  • git branch -d dev — 删除分支

创建分支时,新分支指向当前 commit。两个分支共享之前的历史,之后各自独立发展。

main:    A -- B -- C
                    \
dev:                 D -- E

试试看:创建一个 dev 分支git checkout — 切换分支
git checkout 切换到指定分支。

切换分支时,Git 会:

  1. 让 HEAD 指向新分支

  2. 把工作区的文件更新为该分支最新 commit 的内容

创建并切换(一步到位):

git checkout -b dev — 创建 dev 分支并立即切换过去

切换后,你的工作区会变成 dev 分支的内容。两个分支可以各自独立开发,互不影响。

git checkout -b dev Hash — 从指定的commit处创建 dev 分支并立即切换过去

切换后,你的工作区会变成 dev 分支的内容。两个分支可以各自独立开发,互不影响,且是从指定的commit开始延申。

试试看:切换到 dev 分支

Git 史话:checkout、switch 与 restore

在早期,git checkout 承担了太多责任,核心逻辑是“把某个东西拿出来放到工作区”。这导致了极大的概念混乱:

1. 操作分支:git checkout dev(切换分支)

2. 操作文件:git checkout -- readme.md(丢弃修改,恢复文件)

分家(Git 2.23 引入):

为了让命令见名知意,Git 社区将 checkout 的功能一分为二,推出了两个专职的新命令:

  • git switch:专职负责切换分支。

  • git restore:专职负责恢复文件(还顺便接管了以前用 reset HEAD 来取消暂存的工作)。

虽然老将 checkout 依然可用,但现代 Git 教程强烈建议你拥抱 switchrestore

git switch — 切换分支(新命令)

git switch 是 Git 2.23 引入的新命令,用来替代 git checkout 的分支切换功能。

为什么?因为 git checkout 职责太多(切换分支、恢复文件、创建分支...),容易混淆。

用法:

  • git switch main — 切换到 main 分支

  • git switch -c feature — 创建并切换到 feature 分支

git checkout 的对应关系:

  • git checkout devgit switch dev

  • git checkout -b devgit switch -c dev

试试看:切回 main 分支

git merge — 合并分支

git merge
把指定分支的修改合并到当前分支。

用法:先切到目标分支(通常是 main),再 merge 其他分支:

git switch main
git merge dev

两种合并方式:

快进合并(Fast-forward)

main 没有新 commit,dev 在 main 前面。Git 只需把 main 移到 dev 的位置:

之前:  main ── A
               \
        dev      B ── C

之后:  main ── A ── B ── C

三方合并(3-way merge)

main 和 dev 都有新 commit。Git 找到分叉点,合并两边的修改,生成一个新的合并 commit:

main ── A ── B ── M
                      /
        dev     C ── D

M 是合并 commit,它有两个父 commit(B 和 D)。

先切到 dev,创建一个新文件并 commit,再切回 main 合并 dev。

合并冲突

当两个分支修改了同一个文件的同一位置,Git 无法自动合并,就会产生冲突。

冲突标记长这样:

<<<<<<< HEAD
当前分支的内容
=======
要合并的分支的内容
>>>>>>> dev

解决步骤:

1. 打开冲突文件,找到 <<<<<<< 标记

2. 决定保留哪部分(或合并两者)

3. 删除冲突标记

4. git add 标记为已解决

5. git commit 完成合并

冲突不可怕。它只是 Git 在说:「这两个修改我没法自动合并,你来决定。」

5.远程协作

SSH Key — 认证远程仓库

要和 GitHub 等远程仓库通信,你需要证明「你是你」。SSH Key 是最常用的方式。

原理:

  • 你生成一对密钥:私钥(留在本地)和公钥(上传到 GitHub)

  • GitHub 用公钥验证你的身份,你用私钥签名

步骤 1:生成密钥

ssh-keygen -t rsa -C "your@email.com"

一路回车即可。默认生成在 ~/.ssh/id_rsa

步骤 2:复制公钥

cat ~/.ssh/id_rsa.pub

复制输出的整行内容。

步骤 3:添加到 GitHub

GitHub → Settings → SSH and GPG keys → New SSH key → 粘贴公钥

步骤 4:测试连接

ssh -T git@github.com

看到 Hi username! 就说明配置成功了。

配置好 SSH 后,clone/push/pull 都可以用 git@github.com:用户名/仓库.git 格式的 URL,无需每次输入密码。

远程仓库与 git push

远程仓库是托管在服务器上的 Git 仓库(如 GitHub)。本地仓库通过 push/pull 和远程仓库同步。

添加远程仓库:

git remote add origin git@github.com:user/repo.git

origin 是远程仓库的默认名字,你也可以自己取名字。查看已配置的远程仓库:

git remote -v

推送到远程:

git push origin main

把本地 main 分支的 commit 发送到远程。之后在 GitHub 上就能看到你的代码了。

试试把你之前的 commit 推送到远程。

重置教程

你已经学会了本地 Git 操作和远程推送。接下来我们换一个场景:从远程克隆一个项目。

为了模拟这个过程,请先重置教程状态:

reset-tutorial

这是一个辅助命令,会清除当前所有数据,回到初始状态。

重置后,我们将在下一张卡片中克隆一个示例仓库。

git clone — 克隆仓库

git clone 从远程仓库复制一份完整的项目到本地:

git clone git@github.com:user/repo.git

它会:

1. 下载远程仓库的全部内容

2. 创建本地仓库和工作区

3. 检出默认分支(通常是 main)

4. 自动设置远程地址为 origin

克隆完成后,你拥有完整的项目历史,和远程仓库一模一样。这就是「分布式」的含义——每个人的本地都是一个完整的仓库。

两种 URL 格式:

  • HTTPS:https://github.com/user/repo.git

  • SSH:git@github.com:user/repo.git(需要配置 SSH Key)

试试克隆一个示例仓库:

git fetch — 获取远程更新

使用 fetch 前,需要先配置远程仓库地址:

git remote add origin git@github.com:user/repo.git
git fetch origin

fetch 从远程下载新的 commit,但不会合并到你的工作区。它只更新本地的远程记录(如 origin/main),让你知道远程有什么新内容,但不改动你的文件。

fetch 前:  本地 main → C,  远程 main → E
fetch 后:  本地 main → C,  origin/main → E
            (你还是在 C,但知道远程有 E 了)

git pull = git fetch + git merge。想精确控制时,可以先 fetch 再决定是否 merge。

试试 fetch,然后用 git status 看看有什么变化。

git pull — 拉取并合并

使用 pull 前,需要先配置远程仓库地址(如果还没配置过):

git remote add origin git@github.com:user/repo.git
git pull origin main

origin 是远程仓库名,main 是分支名。默认情况下,git pull = git fetch + git merge

pull 先从远程下载 commit,然后在 local repo 里合并分支,最后更新工作区的文件。日常开发中,开始工作前先 git pull 是个好习惯。

本地:  A -- B -- C
远程:  A -- B -- D -- E

pull 后: A -- B -- C -- M (merge commit)
                  \      /
                   D -- E

扩展:GitHub Pull Request (PR) 的合并方式

当你把代码 push 到远程并提起 PR 后,在 GitHub 页面上通常有三种合并方式:

1. Create a merge commit:保留所有 commit 历史,并生成一个新的 Merge commit(类似上面的 M)。适合保留完整开发过程。

2. Squash and merge:把你的多个 commit 压缩成一个全新的 commit 加入主分支。主分支历史最干净,适合小功能。

3. Rebase and merge:把你的 commit 逐个复制并重新排列在主分支最前面,没有 merge commit,历史是一条直线。

到这里,你已经掌握了 Git 的核心工作流:

add → commit → push & pull

这四个命令覆盖了日常开发 90% 的场景。后面的章节是进阶内容,希望你接下来能在实践中练习

6.撤销与修正

git reset — 重置

git reset 通过移动 HEAD 指针来撤销 commit。它有三种模式,区别在于如何处理暂存区和工作区:

--soft(软重置)

仅仅把 HEAD 移回旧 commit。你撤销的那些提交内容,会全部被放在暂存区里。

适用场景:commit 拆分、合并,或者信息写错了想重新写。

--mixed(默认)

把 HEAD 移回旧 commit,并且把暂存区也重置为那个状态。但不触碰工作区。

适用场景:相当于撤销了 commit 并且撤销了 git add,文件还在,你需要重新整理修改。

--hard(硬重置)

移动 HEAD,并且把暂存区和工作区全部强行重置!

危险操作,未提交的修改会永久丢失!

适用场景:当前改动完全不要了,彻底回到干净的旧状态。

git reset --soft HEAD~1   # 仅撤销 commit
git reset --mixed HEAD~1  # 撤销 commit 和 add (默认)
git reset --hard HEAD~1   # 彻底回退,抛弃所有修改

git revert — 撤销提交

git revert 创建一个新的 commit,内容是「撤销指定 commit 的修改」。

reset 的区别:

  • reset 是删除历史(改写已有的 commit)

  • revert 是新增历史(创建一个反向 commit)

原来:       A -- B -- C
revert B:   A -- B -- C -- B'
                       (B' 把 B 的改动反向做了一遍)

黄金法则:commit 已经 push 到远程并被其他人使用了,用 revert,不用 reset。reset 改写历史会导致其他人的代码出问题。

7.进阶工具

git rebase — 变基

git rebase main 把当前分支的 commit 逐个「移植」到 main 的最新 commit 之后。

rebase vs merge:

假设你在 dev 分支上有两个 commit(D、E),main 上也有新 commit(B、C):

用 merge 合并:

main:  A ── B ── C ── M   (M 是合并 commit,有两个父 commit)
              \      /
dev:           D ── E

历史保留了分叉结构,但多了一个 M。

用 rebase 合并:

main:  A ── B ── C
                 \
dev:              D' ── E'   (D' 和 E' 是 D、E 的副本,哈希不同)

历史变成一条直线,没有合并 commit。

rebase 的本质:把你的 commit 摘下来,接到目标分支的最新位置,像「嫁接」一样。

黄金法则:不要 rebase 已经 push 到远程的 commit! rebase 会改写 commit 的哈希值,导致其他人的代码出问题。只在 push 之前 rebase 本地 commit。

git stash — 临时保存

git stash 把当前工作区和暂存区的修改「藏起来」,让工作区恢复干净。

场景:你在 feature 分支开发到一半,突然需要切到 main 修复 bug。但你的修改还没法 commit。这时候 stash 就派上用场了。

用法:

  • git stash — 保存当前修改

  • git stash list — 查看所有 stash

  • git stash pop — 恢复最近一次 stash 并删除记录

  • git stash apply — 恢复但保留记录

git stash          # 藏起来
git switch main    # 切到 main 修 bug
git switch feature # 切回来
git stash pop      # 恢复之前的工作

git worktree — 多开工作区

正常情况下,一个 Git 仓库只能同时检出一个分支的代码在工作区里。

git worktree 允许你为同一个本地仓库创建多个并行的工作目录。

痛点场景:

你在 feature 分支写了大量代码,本地环境正跑着。突然老板让你立刻切到 main 修紧急 Bug。

  • 直接切?有大量未提交修改,Git 会阻止。

  • 用 stash 藏起来切?切分支会导致项目依赖或编译产物被冲刷,切来切去很痛苦。

Worktree 的完美解法:

保持当前目录一动不动,直接在旁边开辟一个新目录修 Bug!它们底层共享同一个 Git 数据库。

# 在当前仓库外创建一个名为 hotfix 的新目录,并检出 main 分支
git worktree add ../hotfix main

# 修完 Bug 提交后,清理掉这个临时目录
git worktree remove ../hotfix

极速、省空间,而且彻底隔离了开发环境,保护了你的开发心流。

git cherry-pick — 拣选提交

git cherry-pick 把某个 commit 的改动内容(diff)提取出来,在当前分支重新应用一遍。

场景:你在 dev 分支修了一个 bug(commit D),但不想合并整个 dev,只想把这个修复拿到 main 上。

dev:   A ── B ── C ── D    (D 改了 login.py 的第 10 行)
main:  A ── B

git switch main
git cherry-pick D
main:  A ── B ── D'         (D' 也改了 login.py 的第 10 行)

cherry-pick 不是复制文件快照,而是提取「改了什么」,在当前位置重新改一遍。所以 D' 和 D 的哈希不同,但改动内容相同。

注意:这是「复制改动」不是「移动」。dev 上的 D 仍然存在。

git tag — 标签

标签用于标记发布版本(v1.0, v2.0, ...),方便以后快速找到这个版本。

标签和分支类似,都是指向 commit 的标记。区别是:

  • 分支会随着新 commit 往前移动

  • 标签永远固定在创建时的 commit 上

git tag v1.0              # 给当前 commit 打标签
git tag                   # 列出所有标签
git show v1.0             # 查看标签对应的 commit
git tag -d v1.0           # 删除标签

轻量标签(上面用的)只是一个标记。还有一种附注标签(git tag -a v1.0 -m "message"),包含作者信息和说明,适合正式发布。

git rm — 删除文件

git rm 从工作区和暂存区同时删除文件,并记录这次删除。之后 commit 即可。

git rm readme.md       # 删除文件 + 暂存删除操作
git commit -m "Remove readme"

和手动删除的区别:

  • 手动删除文件后,需要 git add 告诉 Git 你删了什么

  • git rm 一步到位:删文件 + 暂存删除操作

如果只想从 Git 跟踪中移除(保留本地文件):

git rm --cached <file>

适用场景:不小心把 .env 之类的文件 add 进去了,想从跟踪中移除。

git blame — 追溯修改

git blame 逐行显示文件的最后修改者和 commit。

输出格式:

a1b2c3d (Alice 2025-01-15) def login():
b2c3d4e (Bob   2025-02-01)     check_token()

每行前面显示:commit 哈希、作者、日期、内容。

用途:排查某行代码是谁改的、为什么改。配合 git loggit show 可以追溯完整上下文。

git blame login.py         # 查看整个文件
git blame -L 10,20 login.py  # 只看第 10-20 行

8.工作流总览

日常开发流程

一个典型的 Git 工作日:

# 1. 拉取最新代码
git pull origin main

# 2. 创建功能分支
git switch -c feature/login

# 3. 开发、提交
git add .
git commit -m "Add login form"

# 4. 推送到远程
git push origin feature/login

# 5. 创建 Pull Request(在 GitHub 上)

# 6. 代码审查后合并到 main

# 7. 切回 main,拉取最新
git switch main
git pull origin main

# 8. 删除功能分支
git branch -d feature/login

核心循环就是:修改 → add → commit → push → PR → merge

Git Flow 工作流

Git Flow 是一种流行的分支管理策略:

  • main — 永远是生产环境的代码

  • develop — 开发主线

  • feature/* — 功能分支,从 develop 分出

  • release/* — 发布准备分支

  • hotfix/* — 紧急修复分支

main:     A --------- M --------- R
           \         ^           ^
dev:        D -- F -- D -- F --- D
               ^    ^
feature:      f1   f2

小团队可能不需要这么复杂。简单的策略:

  • main 保持可部署

  • 每个功能一个分支

  • 通过 PR 合并

选择适合你团队的工作流,不要为了流程而流程。