致敬传奇工具王——git
在写这篇blog的时候参考了该篇文章关于Git这一篇就够了 (opens new window) 从大三开始就一直想认真地系统学习一下git相关的操作,但是一直没有时间系统地学习,去了华子实习之后也因为没有很好的进行版本控制所以出了不少岔子(还好没把主分支搞坏了orz)这个暑假必须要认真学习一下了!
发展过程(摘抄自csdn):这段觉得蛮有意思所以就摘录下来放在这里
Git最初是由Linux开发者Linus用了仅仅两周时间纯C语言编写而成,在编写完成之后就立马上手接管Linux源代码,不过在此之前Linux是由BitMover公司开发的BitKeeper分布式版本控制系统所管理源代码,它是商业收费的分布式版本控制器,但BitMover公司看中Linux开源精神,免费授权给Linux社区使用,在2002年时,Linux开始使用BitKeeper分布式版本控制系统管理源代码,但好景不长,有一天Linux社区成员Andrew(samba(局域网共享文件c/s程序)的作者)试图破解BitKeeper共享给所有人使用,被BitMover公司发现并收回了免费使用的版权,随后Linus就用了两周时间开发出了git(两周时间包括测试),也就是目前为止最好用的分布式版本控制系统。
大名鼎鼎的github用的就是git系统来管理它们的网站,这里需要区分一下,github和git是两个东西,github是一个社区,git是一个服务系统,github只支持git分布式系统,所以故名成为github。
# 正式介绍
# 一,配置git
第一步,我们需要下载git 对于windows,我们一般直接在官方网站上下载git for windows即可 对于macbook,用homebrew一键下载配置也就可以了,执行命令:
brew install git
在linux上安装的话就分不同的系统了
#在Debuain/Ubuntu上使用:
sudo apt update
sudo apt install -y git
#在Fedora上使用:
sudo dnf install -y git
#在Arch上使用:
sudo pacman -S git
2
3
4
5
6
7
8
9
第二步,我们需要配置git 具体命令使用
git config --global
config:参数是用来配置git环境的 --global:长命令表示配置整个git环境
一般需要配置的有两个东西,一个是用户名配置,一个是邮箱配置
git config --global user.name "你的用户名"
git config --global user.email "你的邮箱"
2
这将会在你的git真正需要与远程仓库交互的时候,使得使用你配置的邮箱和用户名进行登入
# 二,初始化仓库
这里仅仅涉及到一些简单的指令 以mkbk文件夹为例
mkdir mkbk #在当前目录创建mkbk文件夹,如果非linux用户无法使用的话可以手动创建
cd mkbk #进入该目录
git init
2
3
使用git init之后,git会自动做这些事情
- 在当前目录场景一个隐藏的目录: ./.git/,这个目录一般在文件管理器是不显示的,但是可以用
ls -a显示出来;这个文件夹里保存了当前这个仓库的所有核心信息,包括- 对象数据库(你的提交、树、文件内容的快照)
- 引用 refs(分支、标签指向哪个提交)
- HEAD(当前检出的分支指针)
- 配置文件 .git/config(该仓库级别配置:远程地址、分支跟踪等)
- 生成初始分支指针,一般是main或者master;(一般会在命令行那里看到一个用圆括号括起来的名字,就是分支名字
# 三,关联仓库
假设已经完成了git init操作
此时如果想要关联到某个新仓库(远程仓库最好先别添加 README/.gitignore/license,保持空更省事)
第一步,需要添加文件并且提交一次
cd mkbk #进入仓库文件夹
touch tmp.txt #随便创建一个文件
git add . # .表示对当前目录下的所有文件都执行git add 操作
# 如果希望对单个文件操作则使用 git add tmp.txt
git commit -m "Initial commit" # 提价文件
2
3
4
5
第二步,添加远程仓库地址,这里有两种方式
- https:
git remote add origin https://github.com/<user>/<repo>.git - ssh方式(免重复输入密码):
git remote add origin git@github.com:<user>/<repo>.git
下面给出两种示例
git remote add origin https://github.com/mkbk-with-circle/my_blog.git # 不要访问!因为你也访问不了🤓
git remote add origin git@github.com:mkbk-with-circle/my_blog.git
2
第三步,送建立上游跟踪
git push -u origin main/master #具体使用什么分支名看本地是什么
上述的 -u 等价于 --set-upstream,是为了让本地分支 main 跟踪远程分支 origin/main;建立跟踪关系后,本地 main 的“上游”就是 origin/main; 至此,以后你在 main 分支上:
- git push = 默认推到它的上游(通常就是 origin/main)
- git pull = 默认从上游拉取并合并/变基(取决于你的 pull 配置)
- git status 会显示类似
查看跟踪关系可以使用
git branch -vv
如果你不知道你的本地分支名字,可以使用命令:
git branch --show-current
至此,你的已经正式完成了你的仓库的初始化,下面就可以开始自由的编辑你的仓库内容! 但是我们编辑完之后就需要同步到仓库了对吧,这个时候该怎么做呢?
# 四,提交更改
依然假设在mkbk文件夹下进行了更改,然后需要提交更改
cd mkbk
touch tmp.txt
echo "oiiaioiiiaio" > tmp.txt #写入"oiiaioiiiaio"到tmp.txt中
git add tmp.txt
git commit -m "修改tmp.txt"
2
3
4
5
这个过程中,涉及到git add 和 git commit两个操作
- git add是用来将文件添加到本地缓存区
- git commit 则是提交到本地仓库,注意使用git commit 要加上
-m,只需要简单描述一下我们做了什么,不要像写注释那样写一大堆;如果需要换行的话可以多次使用-m,git会帮你自动实现换行
wait!那如果我们git commit的时候写错了怎么办?! 当然是魔高一尺道高一丈!
git commit --amend -m "正确的提交说明" # 只改最近一次的提交信息
如果说有新的文件也想一并提交,那么可以用--amend
git add <漏掉的文件>
git commit --amend
2
如果想要查看历史提交日志,那么可以用
git log
进行查看; 这边展示一下样例:
commit 760a7251133e5d0b67ec2c96a1b7286a4c7d4a6a (HEAD -> master, origin/master, origin/HEAD)
Author: 玛卡巴卡 <2200013153@stu.pku.edu.cn>
Date: Sat Jan 31 15:28:50 2026 +0800
修改标题
2
3
4
5
这里面会显示所有提交的版本号即commit后面的数字(一个哈希算法算出的id值),以及提交的Author用户名和邮箱,时间,还有git commit时用的注释
# 删除文件
我们不可避免的会需要删除文件并且提交这个删除的更改,这个时候如果直接rm <文件> 会有点难搞,所以我们直接使用:
git rm tmp.txt
git commit -m "删除tmp.txt" #即可
2
# commit之后的同步到远程仓库
我们在commit了之后,代码的更改只是提交到本地的仓库,但是还没有同步到远程的仓库,我们需要进行最后一步来同步到远程的github仓库了,也就是:
git push
但是git push其实是多人协作里面最容易产生冲突的地方,因为远程仓库的版本往往比你本地的版本更“新”,Git 为了防止你覆盖别人的代码,会阻止你的推送;
所以我们要养先pull在push的习惯
即先执行git pull的指令,将远程代码库的最新版本拉到本地与本地仓库进行合并
git pull --rebase
执行这条指令可以把你本地的改动“接”在远程最新改动的后面,保持提交历史是一条清爽的直线,而不是充满交错的合并线
--rebase,表示把当前的提交接在最新提交的后面--no-rebase,显式告诉 Git 使用 Merge 而不是 Rebase,会产生一个合并提交--ff-only,如果远程代码和你本地代码产生了分叉(即你们两个人都改了代码),这个命令会直接报错并拒绝合并-p或者--prune,在拉取代码的同时,顺便把本地那些“远程已经删除了的分支”的记录也删掉--autostash,一般配合--rebase使用,它会自动把你当前的改动暂存起来(stash),执行完 pull 之后,再自动帮你把改动恢复回来(pop)
在git push之前,最后也再确认几个事情
- 尽可能地跑一些测试脚本,保证自己的修改不会让别人的功能崩溃(最后针对某个功能写好一个自动化测试脚本,别人可以直接跑这个脚本来测试你的功能是否正常)
- 是否push到正确的分支上,一般来说master是可以直接给别人使用的分支,而dev或者其他分支则是自己开发的分支,必须分清楚我们要推送的分支是哪个;
# 五,代码回滚
知道了历史版本的id后,我们可以进行一个回滚代码仓库的操作,主要用来重置当前的提交,完成该操作的命令行是git reset,有三种常用的模式
--soft:只回退版本库--mixed:(默认是该选项),回退版本库和暂存区--hard:全盘回退
我们需要先理解git管理代码的三个区域
- 版本库 (HEAD):已经提交的历史记录
- 暂存区 (Index/Stage):你准备提交的内容(执行 git add 后的地方)。
- 工作目录 (Working Directory):你正在电脑上改动的实际文件
git reset的不同参数,本质上就是在决定:我们要把“回退”这个动作做到哪一层
--soft:仅仅把 HEAD 指针指向过去的某个 commit- 结果就是,代码改动都还在并且也已经放进了暂存区,只需要再进行新的commit即可(相当于仅仅撤回了一次git commit)
--mixed:把 HEAD 指针指向过去,并重置暂存区- 结果就是,代码改动还在,但变成了未暂存状态(红色状态),需要重新进行git add和git commit(相当于把commit和add都撤回了)
--hard(慎用):把 HEAD、暂存区、工作目录全部重置到指定的 commit- 结果就是,所有的未提交改动都会消失,文件内容会变得和那个历史版本一模一样(会丢失当前所有为提交的更改,并且无法找回)
命令示例:
git reset --soft HEAD~1 # HEAD~1 表示当前位置的上一个版本。
git reset --soft a1b2c3d # 也可以直接指定id,只需要前7位左右即可(保证前缀匹配唯一)
2
# 关于HEAD的进阶用法1
~ (波浪号) 与 ^ (脱字号) 的区别
- HEAD~n (纵向回溯):表示沿着当前分支向上找第 n 个祖先。HEAD~2 就是爷爷辈。
- HEAD^n (横向选择):主要用于合并节点。一个合并提交有两个“父亲”,HEAD^1 是合并时的当前分支,HEAD^2 是被合并过来的那个分支。
下面给出一个示例:假设我们在 main 分支上,合并了一个名为 feature 的分支。
A --- B --- M (HEAD, 这是一个合并提交)
\ /
X --- Y
2
3
- A, B 是
main分支上的原始提交。 - X, Y 是
feature分支上的改动。 - M 是合并(Merge)后产生的提交。它有两个“父亲”:B(主线)和 Y(支线)。
1:纵向回溯 ~ (Tilde) ~ 总是沿着**第一个父亲(主线)**一路向上爬。它不看旁边的支线。
HEAD~1:指向 B(M 的第一个父亲)。HEAD~2:指向 A(B 的父亲,也就是 M 的爷爷)。- 用法直觉:你想回到“很久以前”的某个节点,不管中间开了多少分支,只管沿着主干往回数。
2:横向选择 ^ (Caret) ^ 专门用于在合并节点的多个父亲之间做选择。
HEAD^1:指向 B(M 的第一个父亲,通常是你执行 merge 时所在的那个分支)。HEAD^2:指向 Y(M 的第二个父亲,即被合并进来的那个分支)。HEAD^3:报错(除非这是一个三方合并提交,即一次性合并了三个分支)。- 用法直觉:你刚做完合并,突然发现“被合并进来的那个分支”代码有问题,你想跳到支线的末尾(Y)去看看。
# 关于HEAD的进阶用法2
HEAD@{n},即使你执行了 git reset --hard 导致某些 commit 在 git log 里找不到了,它们依然记录在 reflog 中例如:git reset --hard HEAD@{5} # 回到 HEAD 在 5 次移动前所指向的位置
在实际操作中,可以根据情况灵活选择:
- 想回退一两步:用 HEAD~1 或 HEAD~2,简单快捷。
- 想跳到很久以前:先用 git log 查到 ID,直接复制 哈希值。
- 刚才操作失误了:立刻输入 git reflog 找到 HEAD@{n} 救命
# 六,分支操作
# 创建/切换分支
这里介绍一个强大的git命令:git checkout
- 最基础的操作就是从某个分支跳到另外一个分支
git checkout <分支名>
git checkout -b <分支名> # 如果是先跳转到一个全新分支(即先创建该分支然后跳转),加上-b
2
现在一般直接使用git witch来切换分支
git switch <branch>
git switch -c <branch>
2
或者使用git branch来创建一个分支但是不自动跳转
#等价于
git checkout dev
2
3
- 撤回到某文件最近一次的修改状态,让它变回上次提交时的样子(注意会丢失当前为保存的代码)
git checkout -- <文件名>
git checkout -- index.html
2
现在一般用git restore来恢复文件
git restore <file>
# 七,协作操作
我们在进行团队开发的时候,大家会进行分布式编写代码,可能会各自完成特定的功能,在功能本身解耦的情况下,大家可能会各自有一个自己的分支,那么最后我们会需要把大家的分支合并到一起去,或者还有其他的复杂操作,这一章专门来讲一下这个事情
# 建立连接
在团队协作开始前,我们必须先搞清楚这个协作代码库在哪里
git remote -v:查看远程仓库详情。它会告诉你本地代码关联的是哪个线上地址git fetch:它只下载远程仓库的新代码,但不合并到你的本地分支。它让你在不破坏现状的前提下,先看看队友们都干了啥。git remote prune origin:清理那些在远程已经被删除、但还残留在你本地列表里的分支。
# 分支策略(隔离与保护)
团队协作最忌讳所有人都在 main 分支上乱涂乱画,所以大家需要各开一个新的分支然后自己捣鼓捣鼓
git checkout -b feature/xxx:为每个新功能开辟独立分支。这是协作的基石,确保你的实验不会搞砸别人的生产环境。git merge --no-ff:在合并分支时强制生成一个合并提交; 这样在查看历史记录时,你能清晰地看到“这一堆提交是属于某个功能的”;而不是莫名其妙多了一大堆的功能git push -u origin <branch>:将你的新分支推送到服务器,让队友也能看到并拉取你的进度。
# 跨分支“搬运”与暂存
协作中经常会出现“我想要他的那个功能,但我不要他的全部代码“的情况,这个时候就需要跨分支的定点搬运
git cherry-pick <commit-id>:精准采摘。 如果队友在另一个分支修好了一个紧急 Bug,你不需要合并他的整个分支,只需要把那个修复 Bug 的提交“摘”到你的分支上。- 举个例子:你在 dev 分支上写了 10 个提交,其中第 5 个提交修复了一个紧急 Bug。现在 main 分支急需这个修复,但你又不想把 dev 分支那 9 个没写完的功能也合到 main 里;那么这个时候进行两个操作,首先切换到 main 分支,然后执行git cherry-pick <那个Bug修复的ID>,
- Git 会把那个提交的代码改动“复刻”一份,在 main 分支上生成一个新的提交
- 举个例子:你在 dev 分支上写了 10 个提交,其中第 5 个提交修复了一个紧急 Bug。现在 main 分支急需这个修复,但你又不想把 dev 分支那 9 个没写完的功能也合到 main 里;那么这个时候进行两个操作,首先切换到 main 分支,然后执行git cherry-pick <那个Bug修复的ID>,
git stash:临时储物柜。 当你正在开发功能,突然接到任务要切换分支去修 Bug,用它把当前的改动藏起来,等修完 Bug 再用 git stash pop 恢复
# 责任追踪与评审(代码审计)
协作不仅是写代码,更是对代码负责
git blame <file>:它可以逐行显示一个文件的内容,并在每一行前面标注:最后一次修改这行的人是谁、提交 ID 是什么、修改时间是什么- 如果只想看某几行的变化:
git blame -L 10,20 <文件名>
- 如果只想看某几行的变化:
git diff:在推送前快速预览你改动了哪些文件,以及改动的行数- git diff:对比 工作区 vs 暂存区(即:你改了但还没 add 的内容)。
- git diff --cached:对比 暂存区 vs 最后一次提交(即:你 add 了但还没 commit 的内容)。
- git diff HEAD:对比 工作区 vs 最后一次提交(看你自上次提交以来所有的改动)。
- git diff branch1 branch2:对比两个分支之间的差异。
# 八,如何使用gitignore
.gitignore 文件就像是 Git 仓库的“过滤器”或“保镖”。它的存在是为了告诉Git虽然这些文件存在于我的仓库,但是永远不要把它们提交到版本库中。”
# 创建与生效
.gitignore 是一个纯文本文件,通常存放在项目的根目录下。
- 创建文件:在根目录新建一个名为
.gitignore的文件(注意前面有个点)。 - 写入规则:每一行代表一个忽略规则。
- 保存并提交:你需要将
.gitignore文件本身提交到 Git 中,这样团队里的其他人也能共用这套过滤规则。
# 语法规则速查
你可以使用简单的通配符来描述要忽略的文件:
#:注释,Git 会忽略这一行。node_modules/:忽略整个文件夹(末尾加/效果更好)。*.log:忽略所有以.log结尾的文件(通配符*)。debug.log:只忽略根目录下名为debug.log的文件。**/temp/:忽略任何目录下的temp文件夹。!important.log:取反符号。如果之前设置了忽略所有.log,这一行表示“除了important.log以外”。
# 常见的忽略对象
不同类型的项目通常有固定的“垃圾文件”。下面是一个典型的配置参考:
| 类别 | 常见忽略项 | 原因 |
|---|---|---|
| 系统文件 | .DS_Store, Thumbs.db | 操作系统自动生成的缓存,对代码无意义。 |
| 依赖包 | node_modules/, venv/, target/ | 体积巨大,可以通过包管理器重新生成。 |
| 敏感信息 | .env, *.pem, config/secrets.yml | 包含 API 密钥、数据库密码,推送到远程极度危险。 |
| 编辑器配置 | .vscode/, .idea/, *.swp | 每个人的编辑器偏好不同,不应强加给队友。 |
| 构建产物 | dist/, build/, *.exe | 编译后的文件,应由 CI/CD 或本地构建生成。 |
# 最常见的问题:为什么设置了没生效?
这是新手最常遇到的坑:如果你在创建 .gitignore 之前,就已经把某个文件 commit 过了,那么即使你后来把它写进 .gitignore,Git 依然会继续跟踪它的变化。
解决方案(清除缓存) 你需要手动把这些文件从 Git 的“跟踪名单”里踢出去(但保留在硬盘上):
# 1. 停止跟踪单个文件
git rm --cached <文件名>
# 2. 如果想批量处理,先清空所有缓存(注意最后有个点)
git rm -r --cached .
# 3. 重新添加所有文件
# 此时 Git 会重新读取 .gitignore 并忽略掉那些该忽略的
git add .
# 4. 提交改动
git commit -m "Fix: 重新应用 .gitignore 规则"
2
3
4
5
6
7
8
9
10
11
12
核心建议:在项目开始的第一天,甚至在写第一行代码前,就先配好 .gitignore。这能帮你省去后续清理敏感信息和臃肿历史的大量麻烦。
# 九,子模块管理
我们的项目往往不是白手起家而是在别人的基础上进行开发,因此会在我们的项目里git clone别人的模块,作为我们项目的子模块(Submodule),它是一个“仓库中的仓库”,它允许你将另一个项目作为子目录保留在主项目中,同时保持两个项目的提交历史完全独立。
# 初始化与添加
当你第一次想引入一个子模块,或者克隆一个包含子模块的项目时,需要用到这些命令。
添加子模块 将远程仓库添加为当前项目的子目录:
git submodule add <远程仓库URL> <本地路径>
# 例子:把一个工具库加到 lib/utils 目录下
git submodule add https://github.com/user/utils.git lib/utils
2
3
执行后,Git 会生成一个 .gitmodules 文件,记录子模块的路径和 URL。
克隆带子模块的项目
如果你直接 git clone 一个含有子模块的项目,子模块目录默认是空的。你需要:
# 方法 A:一步到位(推荐)
git clone --recursive <项目URL>
# 方法 B:手动初始化(如果已经克隆了主项目)
git submodule init # 注册子模块信息
git submodule update # 抓取并检出子模块对应的提交
2
3
4
5
6
# 日常更新与维护
子模块最特殊的地方在于:主项目记录的是子模块的某一个特定 Commit ID,而不是分支名。
更新子模块到远程最新版:如果子模块的作者更新了代码,你想同步:
# 进入子模块目录手动拉取
cd lib/utils
git pull origin main
cd ../..
git add lib/utils # 必须在主项目提交这个“指针”的变化
# 或者在主项目目录一键更新
git submodule update --remote
2
3
4
5
6
7
8
批量操作:如果你的项目有很多子模块,想一次性在所有子模块里执行命令:
# 在所有子模块中切换到 main 分支
git submodule foreach 'git checkout main'
2
# 推送与协作
在团队协作中,可能出现:你更新了子模块,推了主项目,但忘了推子模块本身的改动。这会导致队友拉取时报错。
比较安全的做法:在推送主项目时,检查子模块是否已推送:
git push --recurse-submodules=check
# 如果子模块没推,Git 会直接报错拒绝推送主项目
2
自动推送
git push --recurse-submodules=on-demand
# Git 会先帮你把所有改动的子模块推送到远程,然后再推主项目
2
# 彻底删除子模块
这是 Git 中最繁琐的操作之一,因为信息分布在多个地方。现代 Git (2.7+) 已经简化了流程:
- 删除记录并移除文件:
git rm <子模块路径>1 - 清理残留(可选):
虽然
git rm删除了文件和.gitmodules记录,但.git/config中可能还有残留。rm -rf .git/modules/<子模块名>1 - 提交改动:
git commit -m "Remove submodule"1