Git 从入门到理解原理:一篇写给自己的完整笔记
这篇分两部分:先讲原理(搞懂了原理,命令就不用背),再给命令手册(按场景查就行)。
最后有一张「常见报错对照表」,收录了我自己踩过的坑。
一、Git 到底在解决什么问题
没有版本控制的时候,我们是这样干活的:
论文.docx
论文-修改版.docx
论文-修改版2.docx
论文-最终版.docx
论文-最终版-真的最终.docx
问题很明显:不知道每一版改了什么、谁改的、为什么改;想回到上周的样子只能靠记忆;多人协作更是灾难。
Git 解决的就是这件事:它把"文件的每一次变化"变成一条条有记录、可回溯、可对比、可合并的历史。
关键认知:Git 不是"网盘",也不只是"把代码传到服务器"。它是一台本地的时间机器 —— 你电脑上就有一个完整的仓库,断网也能提交、看历史、切分支;服务器只是用来同步和备份的另一个副本。
二、核心原理(理解这 6 点就够了)
1. 三个区域:工作区 → 暂存区 → 本地仓库
工作区(你正在编辑的文件)
│ git add
▼
暂存区(staging area / index,准备提交的内容)
│ git commit
▼
本地仓库(.git 目录,永久保存的历史)
│ git push
▼
远程仓库(Gitea / GitHub)
为什么要多一个"暂存区"? 因为一次提交应该只包含一件完整的事。你改了 5 个文件,但其中 2 个属于"修 bug"、3 个属于"加功能",可以分两次提交 —— 暂存区就是让你挑着提交的地方。
2. 一次提交 = 一个快照,不是"差异"
很多人以为 Git 存的是"改了哪几行"(像 diff 那样)。不是。 Git 每次提交保存的是当时所有文件的完整快照(没变的文件只记录一个指针,不重复存储,所以很省空间)。
好处:切分支、回退版本极快,因为不需要"反向应用补丁"。
3. 提交连成一张图(DAG)
每个提交都记录着"我的父提交是谁":
A ← B ← C ← D (main)
↑
E ← F (feature)
这就形成了有向无环图(DAG)。分支合并,其实就是把两条线的历史接起来。
4. 分支只是一个"指针"(这是 Git 最妙的设计)
main 不是文件夹、不是副本,它只是一个写着某个提交 ID 的文件:
cat .git/refs/heads/main
# 输出:f2be902b1a... (40 个字符的 SHA-1)
所以创建分支是瞬间的(就是写一个 41 字节的文件);这也是为什么 Git 鼓励你"随便开分支"。
而 HEAD 是另一个指针,指向"我现在在哪个分支上"。
5. 一切都是按内容寻址的(哈希)
.git/objects 里存着三种对象:
| 对象 | 存什么 |
|---|---|
| blob | 一个文件的内容 |
| tree | 一个目录的结构(文件名 → blob/tree) |
| commit | 一次提交:指向一个 tree + 父提交 + 作者 + 说明 |
它们的名字都是内容的 SHA-1 哈希。这意味着:
- 内容一样 → 哈希一样 → 只存一份(省空间)
- 内容改一个字符 → 哈希完全不同 → 历史无法被悄悄篡改(改一个字节,后面所有提交的 ID 都会变)
可以自己验证:
git cat-file -t HEAD # 看 HEAD 是什么类型 → commit
git cat-file -p HEAD # 看提交内容(树、父提交、作者、说明)
git cat-file -p HEAD^{tree} # 看这个提交对应的目录
6. 分布式:每个人手里都是完整的仓库
克隆时,你拿到的是全部历史,不只是一个"当前版本"。所以:
- 断网也能提交、看 log、切分支
- 服务器挂了,任何一台克隆过的电脑都能重建整个仓库
push/pull只是"两个仓库之间同步提交"
三、动手:从零到第一次推送
0. 只做一次:配置身份和凭据
# 提交署名(会写进每一条提交记录)
git config --global user.name "你的名字"
git config --global user.email "你的邮箱"
# 让 Git 记住密码(Windows 默认就有,Mac/Linux 按需)
git config --global credential.helper store # 明文存,方便
技巧:
--global影响所有仓库。想给某个仓库单独设身份(比如 Gitea 用一套、GitHub 用另一套),去掉--global,在仓库目录里执行即可。
1. 两种开始方式
方式 A:服务器上先建仓库,然后克隆下来(推荐新手)
git clone http://你的服务器/zxq/my-repo.git
cd my-repo
方式 B:本地已有文件夹,绑定到远程仓库
cd 你的文件夹
git init # 让这个文件夹变成 Git 仓库(生成 .git 目录)
git add .
git commit -m "第一次提交"
git branch -M main # 把默认的 master 改名成 main
git remote add origin http://你的服务器/zxq/my-repo.git # 绑定远程仓库
git push -u origin main
-u的意思是"记住这次的上游分支",以后直接git push/git pull就行。
2. 日常三步曲(90% 的时间只用这三条)
git add . # ① 把改动放进暂存区
git commit -m "说明这次改了啥" # ② 提交到本地仓库
git push # ③ 同步到服务器
3. 写提交说明的规矩
好的说明长这样:修复登录页在 Safari 下错位的问题
差的说明:update、修改、111、asdf
为什么重要:三个月后你回头看历史,只有说明能救你。团队协作里,别人靠它判断你的改动要干什么。
四、命令手册(按场景查)
查看状态
git status # 现在有哪些改动、哪些已暂存
git log --oneline # 历史(一行一条)
git log --oneline --graph --all # 带分支图的完整历史
git diff # 工作区 vs 暂存区(还没 add 的改动)
git diff --staged # 暂存区 vs 上次提交(马上要提交的内容)
git show 提交ID # 看某次提交具体改了什么
分支
git branch # 列出本地分支(* 是当前分支)
git switch -c feature # 新建并切到 feature 分支
git switch main # 切回 main
git merge feature # 把 feature 合并进当前分支
git branch -d feature # 删除已合并的分支
git branch -M main # 给当前分支改名(master → main)
撤销与回退(新手最需要,也最容易搞混)
| 我想…… | 命令 | 说明 |
|---|---|---|
| 放弃某个文件的未提交改动 | git restore 文件名 |
恢复到上次提交的样子,改了的内容会丢 |
| 把文件从暂存区拿出来 | git restore --staged 文件名 |
改动还在,只是不准备提交了 |
| 修改最后一次提交的说明 | git commit --amend -m "新说明" |
只改说明/补文件,历史还是那条 |
| 撤销最后一次提交,改动保留 | git reset --soft HEAD~1 |
最安全,常用于"提交早了" |
| 撤销最后一次提交,改动也丢掉 | git reset --hard HEAD~1 |
⚠️ 危险,内容真没了 |
| 撤销一次已推送的提交 | git revert 提交ID |
生成一条"反向提交",历史可追溯(团队场景用这个) |
一句话记忆:restore 动文件,reset 动历史(本地),revert 动历史(安全、可推送)。
远程
git remote -v # 看当前绑定的远程地址
git remote set-url origin 新地址 # 换地址
git remote add origin 地址 # 绑定(首次)
git fetch # 只下载,不合并
git pull # 下载 + 合并(= fetch + merge)
git pull --rebase # 下载后把自己的提交"接"在最上面(历史更干净)
git push # 上传
git push -u origin main # 首次推送,并记住上游
忽略文件:.gitignore
在项目根目录建一个 .gitignore,一行写一个规则:
node_modules/
*.exe
*.o
.env
密码.txt
build/
被忽略的文件不会出现在 git status 里,也不会被提交。密码、密钥、编译产物都应该写进去。
临时保存:git stash
改到一半要切分支、又不想提交:
git stash # 把当前改动先收起来(工作区变干净)
git switch main # 去干别的事
git switch -c xxx
git stash pop # 把收起来的改动拿回来
五、冲突:为什么会有、怎么解
什么时候会冲突:你和别人(或者你另一台电脑)改了同一个文件的同一处,Git 没法替你决定保留哪个。
冲突长这样:
<<<<<<< HEAD
这行是你当前分支的内容
=======
这行是对方分支的内容
>>>>>>> feature
解决步骤:
- 打开文件,手工改成你想要的样子,把
<<<<<<<、=======、>>>>>>>这三行标记全部删掉 git add 这个文件git commit(或者git rebase --continue)- 想放弃这次合并:
git merge --abort
心态:冲突不是错误,是 Git 在提醒你"这里有两个人改动重叠了,需要人来判断"。每次冲突都是一次理解代码的机会。
六、常见报错对照表(踩过的坑)
| 报错 | 原因 | 解决 |
|---|---|---|
src refspec main does not match any |
本地分支叫 master,却推 main |
git branch -M main 再推 |
Please tell me who you are |
没配身份 | git config --global user.name/email |
Updates were rejected ... fetch first |
服务器上有你本地没有的提交 | git pull --rebase 后重新 push |
Permission denied (publickey) |
公钥没加到平台,或用的不是那把私钥 | ssh -T git@服务器 自测,重新添加公钥 |
REMOTE HOST IDENTIFICATION HAS CHANGED |
服务器重装过,主机密钥变了 | ssh-keygen -R 服务器IP,再连一次输 yes |
Connection timed out(SSH) |
端口被防火墙挡了,或 IP 被 fail2ban 封了 | 确认端口放行;让管理员解封 |
fatal: not a git repository |
当前目录不是仓库 | git init 或 cd 到正确目录 |
nothing to commit |
没有新改动(或没 add) |
先 git status 看看 |
| 推送要输密码 | 用了 HTTP 地址 | 用 SSH 地址,或把访问令牌写进 URL |
七、一张图记住全部
┌──────────────┐ git add ┌──────────┐ git commit ┌────────────┐
│ 工作区 │ ───────────► │ 暂存区 │ ───────────► │ 本地仓库 │
│ (你编辑文件) │ ◄─────────── │ (准备提交)│ ◄─────────── │ (.git) │
└──────────────┘ git restore └──────────┘ git reset └─────┬──────┘
│ git push
▼
┌────────────┐
│ 远程仓库 │
│ (Gitea等) │
└────────────┘
│ git pull
▼
(回到本地)
核心就一句话:add 挑内容 → commit 存档 → push 同步;出问题用 status 看现场、用 log 看历史。
八、练习建议
- 建一个仓库,把这篇笔记存进去,提交 3 次(每次改一点)
- 新建分支
test,改点东西提交,再合并回main - 故意制造一次冲突,把它解开
- 用
git reset --soft HEAD~1撤销一次提交,再重新提交 - 用
git cat-file -p HEAD看看提交对象长什么样 —— 你会对"原理"有实感
把这五步做完,Git 就算入门了。剩下的命令,用到再查就行。