S00-00 General-Git
[TOC]
概述
什么是 Git
Git:
简单来说,Git 是一个“代码的时间机器”和“多人协作的神器”。
在技术层面上,Git 是目前世界上最先进的分布式版本控制系统(Distributed Version Control System),由 Linux 之父 Linus Torvalds 于 2005 年创建。
为了让你更直观地理解,我们可以从生活中的痛点和游戏两个角度来看:
为什么需要 Git
场景一:后悔药(版本回退)
你写论文或做设计时,是不是经常会有这种文件命名:
论文_最终版.doc论文_最终版_再改一下.doc论文_打死不改版.doc论文_真正的最终版.doc
Git 的作用: 它可以帮你记录文件的每一次改动(谁在什么时间改了哪一行)。你不需要存无数个备份文件,只需要一个文件。如果改错了,你可以随时“穿越”回昨天、上周甚至去年的任何一个状态。
场景二:游戏存档(Save Point)
玩 RPG 游戏打 Boss 之前,你通常会存一个档。如果打输了,就读取存档重来。
Git 的作用: 每当你完成一个功能,就提交(Commit) 一次,相当于存了一个档。如果后续代码写崩了,直接回档,安全无痛。
场景三:平行宇宙(分支管理)
想象你正在写一个软件的主线故事(正式版),突然想尝试一个疯狂的新功能(实验版)。
Git 的作用: 你可以开启一个“分支”(Branch),在这个平行宇宙里随意折腾,完全不会影响主线。如果实验成功,就把平行宇宙合并(Merge)回主线;如果失败,直接删掉这个宇宙,主线毫发无损。
核心工作流@
Git 的核心工作流:
Git 的操作主要在三个区域之间流转,理解了这个图就理解了 Git 的一半:
- 工作区 (Working Directory): 你电脑里能看到的文件夹,你平时就在这里修改代码。
- 暂存区 (Staging Area / Index): 一个中间区域。就像购物时的“购物车”,你把想提交的文件先放进来 (
git add),确认无误后再打包结算。 - 本地仓库 (Local Repository): 安全的数据库。当你执行“提交” (
git commit) 后,文件就被永久记录在这里了。
Git vs GitHub
Git vs GitHub:
很多初学者容易搞混这两个概念,它们的关系就像 MP3 和 Spotify,或者 视频文件 和 YouTube。
| 特性 | Git | GitHub (或 GitLab/Gitee) |
|---|---|---|
| 本质 | 一个工具软件 | 一个网站/云服务平台 |
| 安装位置 | 安装在你的电脑上 | 访问网页使用 |
| 主要功能 | 本地管理版本、记录历史 | 托管你的 Git 仓库、方便多人协作、代码备份 |
| 是否联网 | 不需要,断网也能用 | 需要联网 |
一句话总结: 你在本地用 Git 写代码、存版本,然后推送到 GitHub 上给别人看或备份。
优缺点
Git 是目前最主流的版本控制工具,占据了统治地位,但它并不完美。它的设计哲学非常极客(Geek),这也导致了它鲜明的优缺点。
以下是对 Git 优缺点的深度分析:
优点
Git 的核心优点:
1. 真正“分布式”带来的安全与便捷
- 每个人都拥有完整的历史记录: 当你从服务器下载(Clone)项目时,你不仅仅是下载了最新的文件,而是把整个项目的历史(每一次修改、每一个版本)都完整地复制到了你的电脑上。
- 安全: 即使服务器硬盘爆炸了,任何一个成员电脑里的仓库都可以直接还原出服务器的数据。
2. 极其强大的分支(Branch)管理
- 这是 Git 的“杀手锏”: 在 SVN 等老一代工具中,创建分支就像复制整个文件夹,速度慢且占空间。而在 Git 中,创建分支只是创建一个“指针”,瞬间完成。
- 鼓励实验: 因为开分支成本极低,Git 鼓励你为每一个小功能、每一个 Bug 修复都开一个新分支,互不干扰。
3. 速度极快
- 因为大部分操作都在本地磁盘进行,不需要频繁连接服务器,所以 Git 的提交、差异比较(Diff)等操作速度极快。
4. 暂存区(Staging Area)设计
- 这是 Git 独有的设计。它允许你把一部分修改先“放进购物车”(暂存),而不是必须一次性提交所有修改。这让你的提交记录(Commit Log)可以非常清晰、原子化。
5. 完整的数据保证
- Git 使用 SHA-1 哈希算法来标识每一次提交。这意味着如果不改变 ID,就不可能在不知情的情况下修改文件内容或历史记录。你的代码历史是防篡改的。
缺点
Git 的主要缺点:
1. 学习曲线陡峭(难上手)
- 概念抽象: 对初学者来说,理解“工作区、暂存区、本地库、远程库”的数据流向非常头大。
- 命令繁多且晦涩: Git 有上百个命令,而且参数复杂。出了错(比如
detached HEAD状态),报错信息往往很“技术化”,新手很难看懂怎么修。 - 容易“搞崩”心态: 很多新手都有过“我不小心合并错了,然后把代码搞丢了”的恐怖经历(其实很难真丢,但在新手看来就是丢了)。
2. 对大文件(二进制文件)支持不佳
- Git 擅长管理文本文件(代码)。
- 如果你把大量的图片、视频、编译后的
.exe或.dll文件塞进 Git,仓库体积会迅速膨胀。因为 Git 每次保存都会存一份副本,且这些二进制文件无法像文本那样进行“增量压缩”。 - 注:虽然有
Git LFS(Large File Storage)插件可以缓解,但原生体验依然不好。
3. 权限控制较弱
- Git 的设计理念是“全有或全无”。通常情况下,你要么能访问整个仓库,要么都不能访问。
- 很难像 SVN 那样精细地控制:让张三只能改 A 文件夹,李四只能改 B 文件夹。(这通常需要借助 GitHub/GitLab 等平台的额外功能来实现,而非 Git 本身的功能)。
4. 历史记录不可逆的“脏乱”
- 虽然这算是一个双刃剑,但 Git 允许你修改历史(Rebase)。如果不小心强制推送(Force Push)覆盖了别人的代码,可能会导致团队协作的灾难。
基本使用
常用命令
为了让你更清晰地掌握 Git,我将这些命令按照实际开发的工作流进行分类。我不只列出命令,还会重点解释那些高频使用的参数,帮你避坑。
为了辅助理解,这张图展示了文件在不同区域流转时对应的命令:
配置与初始化
配置与初始化:
在你开始写代码之前,必须先“报家门”。
git config:设置用户信息。git config --global user.name "你的名字"git config --global user.email "你的邮箱"--global:关键参数。表示这台电脑上所有的 Git 仓库都用这个配置。如果不加,就只对当前仓库有效。git config --list:查看当前所有的配置信息。
git init:把当前文件夹变成 Git 仓库。- 执行后会生成一个隐藏的
.git文件夹(千万别手动删它,否则历史记录全没)。
- 执行后会生成一个隐藏的
git clone [url]:从远程(如 GitHub)下载一个仓库。git clone -b [分支名] [url]:常用。直接克隆指定的某个分支,而不是默认的 master/main。
日常高频操作
日常高频操作:
这是你每天哪怕闭着眼都要敲几十遍的命令。
git status:查看当前仓库状态(有哪些改动?在哪个区?)。git status -s:常用。以精简模式显示(Short),看着不累。
git add:把文件从“工作区”放入“暂存区”。git add [文件名]:添加指定文件。git add .:最常用。添加当前目录下的所有变动(新建的、修改的),但不包括被删除的文件。git add -A:添加所有变动(包括删除的)。
git commit:把“暂存区”的内容提交到“本地仓库”(存档)。git commit -m "提交信息":必须带。-m后面写清楚你干了什么。git commit -a -m "信息":偷懒神器。跳过git add步骤,直接把所有已跟踪文件的修改提交(注意:对新建的未跟踪文件无效)。git commit --amend:后悔药。如果你刚才提交完发现漏了个文件,或者注释写错了,用这个命令可以修改“上一次提交”,而不会产生新的提交记录。
分支管理
分支管理:
Git 的灵魂。
git branch:查看或操作分支。git branch:列出本地所有分支,当前分支前会有*。git branch [新分支名]:创建一个新分支,但不切换过去。git branch -d [分支名]:删除已合并的分支。git branch -D [分支名]:强力删除。如果分支没合并过,用小写-d删不掉,必须用大写-D。
git switch/git checkout:切换分支。git switch [分支名]:推荐(新版本命令)。纯粹用于切换分支。git switch -c [新分支名]:常用。创建并立即切换到新分支(等同于git checkout -b)。git checkout [分支名]:老版本命令,功能太杂(既能切分支又能恢复文件),建议慢慢改用 switch。
git merge:合并分支。git merge [分支名]:把指定分支合并到当前分支。git merge --no-ff [分支名]:建议使用。关闭“快进模式”(No Fast Forward)。这样合并会在历史记录里生成一个新的节点,能清晰看出这里发生过一次合并。
远程同步
远程同步:
与同事协作、与 GitHub 服务器交互。
git pull:拉取远程代码并合并(= fetch + merge)。git pull origin main:拉取远程 main 分支并合并。git pull --rebase:高手习惯。拉取代码时,把你的本地提交“顶”到远程提交的最前面,保持提交历史是一条直线,没有分叉。
git push:把本地代码推送到远程。git push origin [分支名]:推送到指定分支。git push -u origin [分支名]:第一次推送时用。关联本地和远程分支,以后再推只需要敲git push即可。git push --force(或-f):危险! 强制推送。覆盖远程仓库的历史。除非你知道自己在干什么(比如只有你一个人在用这个分支),否则千万别在团队项目里用。
后悔与撤销
后悔与撤销:
这也是 Git 强大的地方,但比较容易晕,请仔细看。
git checkout -- [文件名]/git restore [文件名]:丢弃“工作区”的修改。- 场景:你改乱了一个文件,还没 add,想把它恢复成改动前的样子。
git reset:版本回退(时光穿梭)。git reset --soft HEAD^:软重置。撤销最近一次 commit,但保留代码在暂存区。- 场景:提交完觉得不满意,想撤回来重新调整一下再提交。
git reset --hard HEAD^:硬重置。撤销最近一次 commit,删除所有改动,代码彻底回到上个版本。- 场景:最近写的全是垃圾,我完全不要了。
注:
HEAD^表示上一个版本,HEAD~2表示往上两个版本。
git log:查看历史。git log --oneline:常用。一行显示一个提交,简洁。git log --graph:以图形化方式显示分支合并历史。
场景速查
| 场景 | 命令组合 |
|---|---|
| 我要把代码交上去 | git add . -> git commit -m "..." -> git push |
| 我切错分支了,想切回来 | git switch [目标分支] |
| 我看不到同事刚推的代码 | git pull |
| 我刚才的提交注释写错了 | git commit --amend (然后修改保存) |
| 这文件我改烂了,想还原 | git restore [文件名] |
| 想看所有人的提交轨迹 | git log --oneline --graph --all |
语法详解
分支合并
git merge
git merge 是 Git 中最核心的功能之一,它的作用是把两个分叉的“平行宇宙”(分支)合并成一个。
对于新手来说,Merge 最让人困惑的地方在于:有时候它默默就合并完了,有时候却会生成一个新节点,而有时候又会直接报错(冲突)。
我们要分三种情况来理解它。
核心原则:站在哪里合并谁?
这是一个“主语”和“宾语”的问题,千万别搞反。
原则: 你必须先切换到接收修改的那个分支(通常是主干),然后把别的分支拉过来。
# 场景:我想把 dev 分支的代码合并到 main 分支
git switch main # 1. 先站到 main 上 (接收方)
git merge dev # 2. 把 dev 拉过来 (贡献方)策略1:快进模式
策略一:快进模式 (Fast-Forward) —— "顺水推舟"
这是最简单的情况。
- 前提: 自从你创建
dev分支后,main分支没有任何新的提交,一直在原地等你。 - 动作: Git 只需要把
main的指针直接“滑”到dev的位置即可。 - 特点: 不会生成新的 Commit 节点,历史记录是一条直线。

语法:
git merge dev
# (Git 默认会自动判断是否可以使用快进模式)策略2:普通合并 --no-ff
策略二:普通合并 (Non-Fast-Forward) —— "打个结"
这是最推荐的团队协作方式。
- 前提:
main分支在你开发期间也有了新的变动(分叉了);或者你强制 Git 必须留下合并记录。 - 动作: Git 会自动创建一个新的“合并提交”(Merge Commit),把两个分支的历史联结在一起。
- 特点: 历史记录会出现一个“圈”或“菱形”,能清晰地看出这里发生过一次合并。

语法:
git merge --no-ff dev--no-ff(No Fast Forward):关键参数! 即使可以快进,也强制生成一个新的 Merge 节点。- 为什么要这么做? 以后查历史时,你能清楚地看到:“哦,这一段代码是当时为了做‘用户登录’功能而合并进来的”,而不是混在主干里分不清。
策略3:压缩合并 --squash
策略三:压缩合并 (Squash)
如果你在 dev 分支上写了 50 个琐碎的提交(比如“修复错别字”、“又修复错别字”),你不想把这堆垃圾扔进 main 的历史里。
- 效果: 把
dev上的所有修改打包成一个新的变化,放在暂存区。 - 注意: 此时还没 Commit!你需要手动执行
git commit -m "完成用户登录功能"。 - 结局:
main分支上只会多出一个干干净净的提交,而不是 50 个。

语法:
git merge --squash dev策略选择
| 你的需求 | 推荐命令 | 历史记录样子 |
|---|---|---|
| 个人开发,想快速同步 | git merge dev (默认 FF) | 一条直线,简洁 |
| 团队协作,需要保留功能开发记录 | git merge --no-ff dev | 有分叉和合并点 (像项链) |
| Feature 分支太乱,想净化主干 | git merge --squash dev | 主干只增加一个干净的点 |
解决冲突
解决冲突:这是新手的噩梦,但其实不可怕。
- 原因: 你在
dev分支改了第 10 行,同事在main分支也改了第 10 行。Git 懵了:“我不确定该听谁的,你自己决定吧。” - 现象: 执行 merge 后,Git 报
CONFLICT,并且自动停止。
解决步骤:
打开文件:你会看到代码里出现了一堆乱码符号(
<<<<<<<,=======,>>>>>>>)。<<<<<<< HEAD:当前分支(main)的代码。>>>>>>> feature:要把合并进来的(feature)代码。=======:分割线。

手动修改:删掉这些符号,保留你想要的代码(可以两个都留,也可以只留一个)。
提交:
bashgit add . # 告诉 Git 我修好了 git commit # 完成合并 (不需要写 -m,Git 会自动生成 "Merge branch 'dev'" 的信息)
git rebase
git rebase:变基,是 Git 中最优雅但也最“危险”的高级技巧。
如果说 git merge 是把两条河流汇聚成一个湖泊(保留了汇聚的过程),那么 git rebase 就是把你的支流直接“嫁接”到主河道的最前端,让历史看起来像是一条直线,从来没有分叉过。
用法1:更新代码 (保持直线)
用法1:更新代码 (保持直线)
这是最常用的场景:你想把主干 (main) 的最新代码同步到你的分支 (feature),但你不想产生那个丑陋的“Merge branch 'main' into feature”的提交记录。
语法:
# 1. 切回你的分支
git switch feature
# 2. 执行变基 (把 main 当作我的新地基)
git rebase main发生了什么:
- Git 会把你
feature分支上独有的提交(C、D)先“存”起来。 - 把
feature的指针指向main的最新提交(B)。 - 把存起来的 C 和 D,依次重新应用(Replay)在 B 后面。
- 结果: 历史记录变成直线:
A -> B -> C' -> D'。

用法2:交互式变基 (整理历史)
用法2:交互式变基 (整理历史)(推荐)
这是 Rebase 的杀手级功能。 有些公司要求:“每个 Pull Request 只能有一个 Commit”。但你在开发时可能提交了 10 次(比如“保存”、“修个bug”、“又修个bug”)。 你可以用交互式 Rebase 把这 10 个垃圾提交压缩(Squash) 成一个完美的提交。
语法:
# 整理最近的 4 次提交
git rebase -i HEAD~4-i意思是 Interactive (交互式)。
操作界面:
执行命令后,Git 会弹出一个编辑器(如 Vim),显示类似下面的内容:
你需要修改每一行前面的指令(默认为 pick):
| 指令 | 缩写 | 含义 | 作用 |
|---|---|---|---|
| pick | p | 保留 | 原封不动保留这个提交。 |
| reword | r | 修改 | 保留代码,但允许你修改 Commit Message(提交注释)。 |
| squash | s | 挤压 | 核心功能。把这个提交合并到上一个提交里去。 |
| drop | d | 丢弃 | 彻底删掉这个提交(代码也会没)。 |
| fixup | f | 修正 | 类似 squash,但直接丢弃注释,只留代码。 |
例子: 如果你想把第 2/4 个提交合并到第 1/3 个里,就把后 2/4 行的 pick 改成 s (squash),保存退出即可。

解决冲突
解决冲突:
在 Rebase 的过程中(Replay),每一层提交都可能冲突。
Git 暂停:报冲突,停下来等你修。
解决冲突:打开文件,手动修好。
添加到暂存区:
git add .继续变基(注意:这里不是 commit):
bash# 继续变基 git rebase --continue # 放弃这次变基,回到原点 git rebase --abort
禁止的操作
永远不要在“公共分支”上使用 Rebase:
- 公共分支:指推送到 GitHub 上、同事们都在用的分支(如
main,dev)。 - 私有分支:只有你自己用的分支。
为什么:
Rebase 会修改历史(Commit ID 会变)。
如果你把 main 分支 rebase 了,而你的同事是基于旧的 main 开发的。当你强推上去后,你同事的代码历史就彻底乱了,Git 会迫使他们手动合并大量冲突,你会被同事打死的。
一句话总结: git rebase 只能用于你自己的、还没推送到远程(或者只有你一个人在用)的分支。
Merge vs Rebase
Merge vs Rebase:
假设你正在 feature 分支开发,而 main 分支有了新的提交。
- Merge(合并):
创建一个新的合并节点(Merge Commit)。两条线最终汇合,历史变成了网状。 - Rebase(变基):
把你分支上的改动“暂时拿下来”,把地基换成main的最新位置,然后再把你刚才的改动“重新播放”一遍。
| 维度 | Merge | Rebase |
|---|---|---|
| 历史形态 | 真实、网状、有点乱 | 干净、直线、像故事书 |
| 安全性 | 极其安全,不会破坏历史 | 有风险,改写历史 |
| 操作难度 | 简单 (一键合并) | 中等 (可能需多次解冲突) |
| 适用场景 | 公共分支合并 (dev -> main) | 个人分支同步主干 (main -> my_feature) |
git cherry-pick
git cherry-pick 是 Git 里的精准手术刀。只把别的分支上的某一个(或几个)特定的提交,复制到当前分支来。
相比于 git merge(大卡车进货,把整个分支拖过来)和 git rebase(整条生产线搬迁),git cherry-pick 的操作更精细。
应用场景:
场景一:紧急修复 (Hotfix)
- 你在
dev分支上开发功能,修好了一个严重的 Bug(提交 ID:a1b2c3),但dev上还有很多没写完的烂尾代码,不能发布。 - 需求: 只要那个 Bug 修复,不要其他的垃圾。
- 操作: 切到
main,把a1b2c3挑过来。
场景二:提交错了分支
- 你在
main上写了个功能,突然发现:“哎呀,这应该写在feature分支上的!” - 操作: 切到
feature,把刚才那个提交 cherry-pick 过来,然后切回main把那个提交删掉。
基本语法
基本语法:
前提: 请先切换到接收代码的那个分支(目标分支)。
git cherry-pick [参数] [Commit ID]-e/--edit:在提交之前打开编辑器,允许你修改提交信息 (Commit Message)。默认情况下,cherry-pick 会直接使用原提交的信息。使用此选项可以补充说明,例如“Backport from feature branch”。
bash# 执行后,Git 会弹出编辑器让你重新写 Commit Message。 git cherry-pick -e f8c5b2a
-n/--no-commit:只将变更应用到工作区和暂存区,但不自动产生新的提交。- 场景:你想挑选多个提交的内容,然后手动合并成一个新的提交。
bash# 执行后,代码过来了,但没生成 Commit。你可以手动修改一下,再 git commit。 git cherry-pick -n f8c5b2a-x:在提交信息的末尾自动追加一行cherry picked from commit ...。- 非常有用!这能让你在未来追踪这个提交到底是从哪里来的,保持历史的可追溯性。
-m/--mainline:仅用于挑选合并提交 (Merge Commit)。指定哪一个父节点作为主线。- 因为合并提交有两个父节点,Git 不知道该相对于哪一边计算变更。通常
-m 1代表保留主分支的视角。
- 因为合并提交有两个父节点,Git 不知道该相对于哪一边计算变更。通常
示例: 假设 dev 分支有个提交 f8c5b2a 是修复登录 Bug 的。
git switch main # 1. 回到主干
git cherry-pick f8c5b2a # 2. 把那个提交“摘”过来结果: Git 会自动把那个提交里的修改内容拿过来,在
main分支上生成一个新的提交。注意: 虽然内容一样,但 Commit ID 是新的!(因为父节点变了)。
用法1:挑选多个非连续提交
用法1:挑选多个非连续提交
如果你想把 dev 上的 A、C、E 提交拿过来,跳过 B 和 D:
git cherry-pick [ID_A] [ID_C] [ID_E]
# 比如:git cherry-pick a1b2c3 d4e5f6 g7h8i9用法2:挑选连续区间
用法2:挑选连续区间
如果你想把从 A 到 Z 之间的所有提交都拿过来:
不包含 A (开区间):
(A, Z]bashgit cherry-pick A..Z包含 A (闭区间):
[A, Z]注:
A^表示 A 的前一个版本,所以范围就涵盖了 Abashgit cherry-pick A^..Z
解决冲突
解决冲突:
和 Merge/Rebase 一样,Cherry-pick 也会冲突(比如你要摘的那个文件,在当前分支已经被改得面目全非了)。
流程如下:
Git 暂停报错: 提示
conflict。手动修代码: 打开文件,解决冲突。
添加到暂存区:
git add .继续摘取:
bash# 注意:这里不是 commit git cherry-pick --continue如果要放弃:
git cherry-pick --abort(回到操作前的状态)。如果要跳过这个提交:
git cherry-pick --skip(这个提交太难修了,我不要了,继续下一个)。
注意事项
虽然 Cherry-pick 很爽,但尽量少用,或者说不要把它作为常规工作流。
原因:它是复制提交
如果你在 dev 做了一次 cherry-pick 到 main,以后你再把 dev merge 回 main 时,Git 会发现有两个“内容一样但 ID 不一样”的提交。 虽然 Git 通常足够聪明能忽略重复内容,但如果代码发生过微调,很容易产生不必要的冲突,导致历史记录混乱。
历史记录
git reset
git reset 是 Git 中最强大但也最容易让人晕头转向的命令之一。如果不小心用错参数,可能会导致代码丢失。
为了让你彻底理解,我们先建立一个核心概念:Git 的版本管理其实是三个区域的配合。git reset 的不同参数,决定了它回退时会影响到哪几个区域。
核心概念:三个区域
在执行回退之前,请脑补这三个区域:
- 历史库 (HEAD): 已经 commit 过的历史记录。
- 暂存区 (Index/Stage):
git add之后待提交的内容。 - 工作区 (Working Tree): 你正在写代码的文件(还没 add 的)。
基本语法
git reset [模式] [目标版本]- 目标版本:通常是
HEAD^(上一个版本)、HEAD~3(往上3个版本)或具体的 Commit ID(如a1b2c3d)。 - 模式:主要有三种
--soft、--mixed(默认)、--hard。
三种模式详解
我们将通过一个场景来解释:
场景: 你写了一堆代码,执行了
add,又执行了commit。现在你后悔了,想撤销这次commit。
--soft
模式一:--soft (软重置) —— 最温和
口语解释: “我只是撤销了‘提交’在这个动作,代码别动,暂存状态也别动。”
命令:
git reset --soft HEAD^影响:
- 历史库 (HEAD): 回退到上一个版本 ✅
- 暂存区 (Index): 保留(刚才提交的代码还在暂存区,处于
git add后的状态)。 - 工作区 (Work): 保留(代码文件内容完全不变)。
适用场景:
- 提交完发现注释写烂了,想重新写。
- 想把多次琐碎的 commit 合并成一个大的 commit(先 soft reset 回去,再重新 commit 一次)。
--mixed
模式二:--mixed (混合重置 / 默认模式) —— 中等
口语解释: “撤销‘提交’和‘放入暂存区’这两个动作,把代码退回到还没 add 的状态。”
命令:
git reset HEAD^(不加参数默认就是 mixed)影响:
- 历史库 (HEAD): 回退到上一个版本 ✅
- 暂存区 (Index): 清空(刚才提交的代码被“拉”了出来,变成了未 add 状态)。
- 工作区 (Work): 保留(代码文件内容完全不变)。
适用场景:
- 你 commit 了错误的文件,想撤销后重新挑选文件进行 add。
- 这是最常用的回退方式,因为代码都在,只是状态变了。
--hard
模式三:--hard (硬重置) —— 危险且强力
口语解释: “我要穿越时空,把一切都恢复成那个版本的样子。刚写的代码我全都不要了!”
命令:
git reset --hard HEAD^影响:
- 历史库 (HEAD): 回退到上一个版本 ✅
- 暂存区 (Index): 重置(回到上个版本的状态)。
- 工作区 (Work): 重置(你刚才写的所有新代码全部消失,文件内容变回上个版本)。
适用场景:
- 当前代码写得太烂了,完全无法挽回,想彻底放弃,重头再来。
- 警示: 使用此命令前,请确保你真的不需要现在的代码了,因为找回很难。
图解对比表
| 模式 | 历史库 (HEAD) | 暂存区 (Stage) | 工作区 (Work) | 你的代码去哪了? |
|---|---|---|---|---|
| --soft | ❌回退 | ✅保留 (已 add) | ✅保留 | 在暂存区,随时可以再次 commit |
| --mixed | ❌回退 | ❌回退 (未 add) | ✅保留 | 变成了红色的“修改未提交”状态 |
| --hard | ❌回退 | ❌回退 | ❌回退 | 彻底消失 (除非用 reflog 找回) |
应用:撤销暂存的文件
撤销暂存的文件:
git reset 不仅仅用于回退版本,常用于把文件从暂存区拿出来(Unstage)。
场景: 你不小心执行了 git add . 把所有文件都放进去了,但其中有一个 secret.txt 你不想提交。
- 命令:
git reset HEAD secret.txt注意:这里没有
--hard或--soft,且后面跟的是文件名效果:
secret.txt会从暂存区移出来。- 文件内容本身不会变,它只是变回了“未 add”的状态。
应用:误用--hard恢复
误用:--hard恢复
如果你手滑执行了 git reset --hard,发现重要的代码没了,别慌,还有一招终极解法:Git Reflog。
Git 会记录你的每一次 HEAD 移动(即使是被 reset 掉的)。
输入命令:
git reflog- 你会看到类似这样的列表:
e4b2c1d HEAD@{0}: reset: moving to HEAD^(这是你刚才闯的祸)f8a9b0c HEAD@{1}: commit: 完成了登录功能(这是你想要找回的那个提交)
找到救命 ID: 找到你 reset 之前的那个 Commit ID(比如上面的
f8a9b0c)。穿越回去:
bashgit reset --hard f8a9b0c
你的代码就复活了!
git revert
git revert 是处理公开代码错误的最佳工具。
如果在 git reset 那个章节,你的关键词是“时光倒流”(直接删掉过去),那么 git revert 的关键词就是“负负得正”。
它不会删掉任何历史记录,而是生成一个新的提交,这个新提交的内容正好与你想撤销的那个提交完全相反。
git revert 的必要性:
想象一下,你和同事正在一起盖一座高楼(一条时间线):
Reset (回退): 你发现第 50 层盖歪了,于是你强行把第 50 层炸掉。
- 后果: 你的同事如果已经在第 50 层上面盖了第 51 层,你这一炸,他的第 51 层就悬空了,大家的代码全乱套了。
Revert (反做): 你发现第 50 层盖歪了,你不拆掉它,而是在第 51 层盖了一个“反向修正层”来抵消第 50 层的错误。
- 后果: 楼层继续往上盖,历史记录完整,同事的代码也不会受影响。
基本语法
git revert [参数] [Commit ID]- Commit ID: 你想要撤销的那一次提交的哈希值(比如
a1b2c3d)。 - 参数:主要有两种参数
--no-edit和--no-commit - 执行后,Git 会自动弹出一个编辑器,让你写一个新的提交信息(默认是
Revert "原提交信息")。保存退出后,一个新的 Commit 就生成了。
示例:
假设当前历史是:
A -> B -> C (坏)你执行
git revert C(注意是C,表示要撤销C,不是B)。现在的历史变成了:
A -> B -> C -> C反 (C的相反操作)代码的状态实际上回到了 B,但历史记录里 C 依然存在。
常用参数
虽然基本命令够用,但以下参数能让你处理复杂情况更顺手:
--no-edit
--no-edit: 直接用默认的提交信息,别弹出编辑器让我确认了。
- 命令:
git revert [Commit ID] --no-edit - 场景: 你非常确定要撤销,懒得还要进 Vim/Nano 编辑器敲个回车。
--no-commit
--no-commit:缩写:-n,批量处理神器 ,把撤销的改动放到暂存区和工作区,但先不要自动生成新的 Commit。
命令:
git revert -n [Commit ID]场景:
你刚才连续提交了 3 个有 Bug 的版本(Commit A, B, C)。
如果你直接 revert 三次,历史记录会多出 3 条 revert 记录,太丑了。
你可以:
bashgit revert -n A git revert -n B git revert -n C git commit -m "一次性撤销 A, B, C 的所有错误"这样可以用一个新的提交,一次性抵消掉前面多个错误。
进阶:撤销合并节点
进阶:撤销合并节点(Revert Merge)
这是一个深坑,新手慎入。
如果你想撤销一次 Merge(比如把 feature 分支合到了 main,结果发现合错了),直接 git revert [Merge ID] 是会报错的。
因为 Merge 节点有两个“父亲”(Parent),Git 不知道你要回退到哪一边。
- 语法:
git revert -m [数字] [Merge ID] - 参数:
-m 1通常代表保留当前所在分支的内容(主干),撤销合并进来的那条分支。
解决冲突
解决冲突:
就像 merge 会冲突一样,revert 也可能冲突。比如你想撤销很久以前的一行代码,但这行代码后来又被别人改过了。
Git 会停下来让你手动修文件。流程如下:
执行 revert,Git 报错“Conflict”。
手动打开文件,解决冲突(保留你想要的)。
git add [文件名]注意: 这里不要用
git commit,而是用:bashgit revert --continue
Reset vs Revert
Reset vs Revert:
这是面试和实战中最关键的区别,请务必记牢:
| 维度 | Git Reset | Git Revert |
|---|---|---|
| 操作方向 | 向后退 (时光倒流) | 向前走 (亡羊补牢) |
| 历史记录 | 历史被删除/修改 (变干净) | 历史新增一条记录 (变长) |
| 安全性 | 危险 (可能丢代码) | 安全 (永远可以再 revert 回来) |
| 适用场景 | 本地 (还没 push 出去) 的私有代码 | 远程 (已经 push 出去) 的公共代码 |
一句话口诀: 只要代码已经推送到 GitHub 给了别人,永远只用 Revert,绝对别用 Reset!
git stash
git stash 是 Git 中最实用的“救火”指令。
Commit vs Stash:如果把 Git 仓库比作你的书桌:
commit:是把文件归档进抽屉(永久保存)。stash:是把桌面上乱七八糟的文件先扫进临时储物箱,让书桌瞬间变干净,方便你干别的事。等你忙完了,再把储物箱里的东西倒回桌面上继续做。
经典使用场景:
你正在 dev 分支狂写代码,写了一半,功能还没跑通(不能 commit)。突然老板冲过来说:“线上有个严重 Bug,马上切到 master 分支去修!”
这时候你面临两个问题:
- 代码没写完,不想提交脏代码。
- 如果不提交,Git 禁止你切换分支(因为会覆盖修改)。
git stash 就是为此而生的。
核心命令
存:git stash
作用: 把所有已跟踪(Tracked)文件的修改(包括暂存区和工作区)保存起来,并把工作区还原成上一次 Commit 的干净状态。
语法:
git stash- 执行完后,你的代码就“消失”了(其实在储物箱里),你现在可以自由切换分支了。
取:git stash pop/apply
等你修完 Bug 回来,想继续写刚才的代码:
方式 A(推荐):恢复并删除存档
bashgit stash pop作用: 把最近一次 stash 的内容应用回工作区,如果成功,就自动把这个 stash 删掉。
类比:剪贴板的“剪切粘贴”。
方式 B(保守):恢复但保留存档
bashgit stash apply作用: 把 stash 的内容应用回工作区,但不删除 stash 列表里的记录。
类比:剪贴板的“复制粘贴”。
常用参数
默认的 git stash 有两个大坑:
- 它不存新建的文件(Untracked files)。
- 它存的名字是自动生成的(比如
WIP on master...),存多了根本不知道哪个是哪个。
请务必掌握以下两个参数:
--include-untracked
--include-untracked:缩写:-u,存新文件。
git stash -u- 默认情况下,如果你新建了一个文件但还没
git add过,git stash会无视它。切分支时这个文件可能会产生冲突或丢失。加上-u,Git 才会把这些“新文件”也一起收进储物箱。
-m "说明信息"
-m "说明信息":给存档起名。
git stash -m "登录功能写了一半,暂停去修Bug"- 给你的 stash 加个注释。当你 stash 了好几次后,你会感谢这个习惯的。
管理多个 Stash
管理多个 Stash(堆栈管理):
Git 的 stash 是一个栈(Stack)结构,即使你 stash 了很多次,它们都在里面。
查看列表
bashgit stash list输出示例:
textstash@{0}: On master: 登录功能写了一半... stash@{1}: On dev: 昨天的临时修改...stash@{0}永远是最新存进去的那一个。
应用指定的 stash:如果你想恢复
stash@{1}而不是最新的stash@{0}:bashgit stash pop stash@{1}删除指定的 stash
bashgit stash drop stash@{1}清空所有 stash (慎用)
bashgit stash clear
工作流程
- 突发状况: 正在写代码 -> 需要切分支。
- 保存:
git stash -u -m "写了一半"-> 工作区变干净。 - 干活: 切分支 -> 修 Bug -> 提交。
- 回来: 切回原来的分支。
- 恢复:
git stash pop-> 继续刚才的工作。
进阶:从 Stash 创建分支
如果你 stash 之后很久才回来,发现原来的分支已经改得面目全非了,这时候直接 pop 可能会有大量冲突。
你可以用这个命令,直接把 stash 的内容变成一个新的分支:
git stash branch [新分支名] [stash]这会创建一个新分支,把你 stash 时的状态完整复原进去,非常安全。
git stash branch 工作原理:
.jpg)
常见实战
常见问题
TUN 模式下 git push 遭遇网络问题
git push 时报错:
报错:
sh> git push github main:main kex_exchange_identification: Connection closed by remote host Connection closed by 20.27.177.113 port 22 fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists.分析:
这个错误
kex_exchange_identification: Connection closed by remote host的核心意思是:SSH 握手阶段就被强行切断了。这几乎可以肯定是因为 GitHub 的默认 SSH 端口(22 端口)在你当前的网络环境下被防火墙干扰或阻断了。
解决方案:
- 方案一:关闭 TUN 模式
- 方案二:使用 ssh.github.com 替代 github.com
方案一:关闭 TUN 模式
关闭 TUN 模式:检查出梯子使用了 TUN 模式,关闭 TUN 模式后再次 push 就成功了。
方案二:使用 ssh.github.com 替代 github.com
以下是关于 “如何在开启 v2rayN TUN 模式下,稳定进行 Git SSH 推送” 的终极解决方案总结。
核心原理:
在 TUN 模式下,代理软件会接管网卡流量。此时,传统的“本地端口监听”(如 127.0.0.1:10808)可能会因为防回环机制或端口独占而被关闭或屏蔽,导致使用 ProxyCommand 强行指定端口时报错 errno=10061。
最佳策略是: 放弃指定本地代理端口,改用 ssh.github.com (443 端口)。这会让 SSH 流量伪装成 HTTPS 流量,由 TUN 网卡自然捕获并代理,既稳定又无需关心本地端口号。
操作步骤:
步骤1:修改 SSH 配置文件:
这是最关键的一步。
文件位置:
C:\Users\你的用户名\.ssh\config修改内容: 将你的 GitHub 配置修改为以下格式(尤其是
Hostname和Port,并注释掉ProxyCommand):text# === 专为 TUN 模式优化的配置(代理软件关闭时也可用) === Host github-proxy User git # 使用 ssh.github.com 替代 github.com Hostname ssh.github.com # 强制使用 443 端口 (防火墙友好,且利用 TUN 自动代理) Port 443 # 重点:务必注释掉 ProxyCommand! # ProxyCommand connect -H 127.0.0.1:10808 %h %p # === (可选) 如果你想保留原版直连配置 === Host github.com User git Hostname github.com
步骤2:配置 Git 仓库远程地址:
确保你的 Git 仓库使用的是你在 config 文件中定义的别名(这里是 github-proxy)。
在项目根目录下打开终端:
修改远程地址命令:
bashgit remote set-url origin git@github-proxy:用户名/仓库名.git(注意:将
用户名和仓库名替换为实际内容)或者也可以直接修改
.git/config配置文件(效果同上):text[core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true [remote "origin"] url = git@github.com:用户名/仓库名.git fetch = +refs/heads/*:refs/remotes/origin/* [remote "github-proxy"] url = git@github-proxy:用户名/仓库名.git fetch = +refs/heads/*:refs/remotes/origin/* [branch "main"] remote = github-proxy merge = refs/heads/main
步骤3:首次连接验证:
由于 ssh.github.com 的指纹与普通 github.com 不同,第一次连接时需要手动确认。
执行命令:
bashssh -T git@github-proxy看到提示:
Are you sure you want to continue connecting (yes/no/[fingerprint])?操作:
输入
yes并回车。成功标志:
看到
Hi <你的用户名>! You've successfully authenticated...。
方案优势:
无视端口变化: 不需要关心 v2rayN 的本地端口是 10808 还是 10809,也不怕端口被改。
兼容性强: 完美适配 TUN 模式,同时也兼容普通代理模式(只要代理软件处于运行状态,甚至代理软件关闭也可以使用)。
穿透性好: 使用 443 端口,在公司或学校等严苛网络环境下也能正常连接。
接下来,你可以愉快地使用 git push 了!