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.mdecho + > — 写入内容
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 readmeHEAD -> 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 会:
让 HEAD 指向新分支
把工作区的文件更新为该分支最新 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 教程强烈建议你拥抱 switch 和 restore。
git switch — 切换分支(新命令)
git switch 是 Git 2.23 引入的新命令,用来替代 git checkout 的分支切换功能。
为什么?因为 git checkout 职责太多(切换分支、恢复文件、创建分支...),容易混淆。
用法:
git switch main— 切换到 main 分支git switch -c feature— 创建并切换到 feature 分支
和 git checkout 的对应关系:
git checkout dev→git switch devgit checkout -b dev→git 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.gitorigin 是远程仓库的默认名字,你也可以自己取名字。查看已配置的远程仓库:
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.gitSSH:
git@github.com:user/repo.git(需要配置 SSH Key)
试试克隆一个示例仓库:
git fetch — 获取远程更新
使用 fetch 前,需要先配置远程仓库地址:
git remote add origin git@github.com:user/repo.git
git fetch originfetch 从远程下载新的 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 mainorigin 是远程仓库名,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— 查看所有 stashgit 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 log 和 git 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 合并
选择适合你团队的工作流,不要为了流程而流程。
评论