摘要

Git 是一种分布式版本的版本控制系统,2019 年记录的太过肤浅,2022 年打算重新整理一下。

正文

2019 年整理的 Git使用技巧 有点粗糙,那时候也不懂得找官方文档,其实最好的学习方式,还是直接跟着官方学,这是最好的。

那时候,我自以为我已经把 Git 玩花了,其实不然,Git 使用门槛低,但上限却是出奇的高,由此打算重新整理一份实用的,当然了,也还是偏向基础的。

我这次是有备而来,我准备了官方文档、官方电子版的书、京东购买的书。并且汲取了其中 60% 对我有用的价值吧。

一、基础概念与概况

1.1 Git 与 SVN 的区别

  • SVN:集中式版本控制系统,所有历史记录都存储在中央服务器,提交和更新都依赖中央服务器。

  • Git:分布式版本控制系统,每个开发者的工作目录都可以是一个完整本地仓库,可以独立离线工作。支持同步到远程仓库。

1.2 三大区域

Git 仓库下,分为工作区域(Working Directory)、暂存区域(Staging Area)、存储库(Repository)。

add 是由工作区域提交到暂存区域。

commit 是由暂存区域提交到存储库。

1.3 分支的本质

分支只是指向某个 commit 的地址,就跟 Java 对象引用地址一样,我把对象清空,只是清空了指向,并没有将开辟的空间回收(回收那就是 GC 的事了)。

所以,分支(包括后面的标签),都是随意删除的,我甚至删一个 aaa 分支,再建一个 aaa 分支,都互不影响。

演示一下删除分支,我从 master 检出一个分支,并 commit 后,如图。

此时如果我把分支 cat 干掉,就变成了如下图。

由此也可知,分支是可以恢复的,只要你记住了 commit 地址即可,因为 commit 存在啊。

1.4 标签与分支的区别

标签与分支作用一样,只是起到指向的作用。标签与分支被删除都不会影响到实际的数据。

两者的作用是,分支会随着 commit 移动,但标签不会。

分支 = 会移动的标签,留下来的是标签,跟着走的是分支。

二、常用命令

2.1 仓库基础操作

2.1.1 初始化与文件管控

初始化仓库

sh
1
2
3
git init
# 查看文件状态
git status

把文件交给 Git 暂存区

sh
1
2
3
4
5
6
7
8
9
# 添加单个文件
git add test.txt
# 添加多个文件
git add test1.txt test2.txt
# 通过后缀添加
git add *.txt
# 添加所有
git add -A
git add *

如果我后悔提交到暂存区,可以通过如下命令撤回。

git restore --staged *

将暂存区内容,提交到存储库。

sh
1
git commit -m "提交描述"

如果觉得,先 git add -A,再 git commit,太麻烦。

可以 git commit -a -m "提交描述" 一步到位。

但这个 -a,只对于已跟踪的文件有效,新增的是无效的。

查看差异

sh
1
2
3
4
5
6
7
8
# 工作区 vs 暂存区
git diff
# 暂存区 vs 最新 commit
git diff --cached
# 工作区 vs 最新 commit
git diff HEAD
# 指定文件的差异
git diff -- test.txt

2.1.2 查看日志

通过如下命令,可以查看具体的 commit。

sh
1
2
3
4
5
6
7
git log
# 查看某个分支日志
git log 分支名称
# 将上面的内容简化成一行输出
git log --oneline
# 读取第一条
git log --oneline --max-count=1

2.1.3 修补最新 commit

如果 commit 之后,发现漏掉了某个文件,或者需要重新修改提交。

可以将修改暂存,然后执行如下命令,更新最新提交。

sh
1
2
3
git commit --amend
# 添加注释
git commit --amend -m "描述"

当你在修补最后的提交时,与其说是修复旧提交,倒不如说是完全用一个新的提交替换旧的提交,理解这一点非常重要。从效果上来说,就像是旧有的提交从未存在过一样,它并不会出现在仓库的历史中。

修补提交最明显的价值是可以稍微改进你最后的提交,而不会让"啊,忘了添加一个文件"或者"小修补,修正笔误"这种提交信息弄乱你的仓库历史。

如果是远程仓库,本地 amend 之后,强制推送更新即可。搜索 --force 后面有记录

2.1.4 暂存区贮藏(stash)

如果我已经修改了代码,但是此时又未完成,不能提交,而此时比如我要切换分支。

可以使用 stash,将修改 add 到暂存区,然后 stash 贮藏起来。

一定要存储到暂存区,否则 stash 也不会存储。

sh
1
2
3
4
5
6
# 超简洁版
git stash
# 简洁版
git stash push [暂存的某个文件]
# 描述版
git stash push -m "描述" [暂存的某个文件]

查看贮藏列表

sh
1
git stash list

查看修改内容

sh
1
git stash show

恢复并删除贮藏

sh
1
2
3
4
# 取出最新贮藏的到工作区域,理解成栈的后入先出
git stash pop
# 取出指定 stashId 标识内容到工作区域
git stash pop <stashId>

恢复并保留贮藏

sh
1
2
3
4
# 取出最新贮藏的到工作区域,理解成栈的后入先出
git stash apply
# 取出指定 stashId 标识内容到工作区域
git stash apply <stashId>

删除贮藏

sh
1
2
git stash drop
git stash drop <stashId>

清空贮藏

sh
1
git stash clear

2.2 远程仓库

以下示例是使用 http,ssh 个人习惯上感觉过于麻烦。

2.2.1 连接远程仓库

查看远程仓库连接信息

sh
1
git remote -v

与远程仓库建立远程连接

sh
1
git remote add origin http://git.meethigher.top/wanxiang/test.git

git remote 主要进行与远端 Git 有关的操作。

origin 指的是远端 Git 的一个别名,而且默认就是 origin。

add、remove 可以删除本地连接的远端 Git。

从远程仓库获取更新

sh
1
2
3
4
# 获取所有远程分支的更新,不合并
git fetch
# 获取指定远程仓库的更新
git fetch origin

fetch 与 pull 的区别:fetch 只下载远程更新到本地,不自动合并;pull = fetch + merge。 推荐先 fetch 查看差异,确认无误后再手动 merge,更安全。

2.2.2 push / pull

从远程分支拉取

sh
1
2
3
git pull origin <远程分支>:<本地分支>
# 如果分支名称相同,可以直接
git pull origin <分支>

将本地分支推送到远程分支

sh
1
2
3
git push origin <本地分支>:<远程分支>
# 如果分支名称相同,可以直接
git push origin <分支>

像使用 GitHub 时,会推荐 git push -u origin master。

-u 就是 upstream,此处可以理解成自动跟踪 master 分支。

也就是说,如果下次,我还是想推送到 master 分支,只需要执行 git push 即可。

强制推送

sh
1
2
3
4
# 强制推送,会覆盖远程分支上的所有历史
git push --force origin <分支>
# 安全的强制推送,仅当远程分支没有被他人更新时才会覆盖
git push --force-with-lease origin <分支>

--force 会无条件覆盖远程分支,如果他人已经推送了新提交,这些提交会丢失。 --force-with-lease 会先检查远程分支是否与本地预期一致,只有在没有他人推送的情况下才会覆盖,更安全。 推荐使用 --force-with-lease 代替 --force

2.3 分支与标签

2.3.1 分支:新增 / 重命名 / 删除 / 切换 / 跟踪

查看分支列表

sh
1
2
3
4
# 查看本地分支列表
git branch
# 查询本地和远端所有分支列表
git branch -a

在当前分支内容上,新开辟一个分支

sh
1
git branch <分支名称>

切换分支

sh
1
2
3
4
5
6
7
# 切换到一个已有分支
git checkout <分支名>
# 基于当前分支,复制一个新的名为 dev 的分支,并切换过去
# 等价于 git branch dev + git checkout dev
git checkout -b dev
# 创建一个从 0 开始的分支,适用于拉取远程分支时使用
git checkout --orphan dev

重命名分支

sh
1
2
# 现分支名可以省略,默认使用当前分支
git branch -m <现分支名> <新分支名>

删除分支

sh
1
2
3
4
5
6
7
8
# 删除本地分支
git branch -d <分支名>
# 删除本地分支,强制删除
git branch -D <分支名>
# 删除远程分支
git push origin --delete <分支名称>
# 删除远程分支简化版,可以理解成,推送空内容区更新分支,也就是将分支删除
git push origin :<分支名称>

因为对于 Git 来说,分支本身也是指向的 commit,所以 checkout 也可以直接指向 commit。

标签同理,标签也是指向 commit,实际切换的还是 commit。

sh
1
git checkout <commitId>

将修改后的某个文件,回滚到指定的分支或者 commitId。

sh
1
2
# 相对路径可以使用 git status 查看
git checkout [分支|commitId] -- 文件绝对或者相对路径

跟踪远端分支

sh
1
2
3
4
5
6
7
8
# 当前分支跟踪远程分支。本地分支省略,那么就是默认当前分支
git branch --set-upstream-to=origin/<远程分支> <本地分支>
# 当前分支取消远程跟踪
git branch --unset-upstream
# push 时通过 -u 自动建立跟踪关系
git push -u origin <分支>
# 查看所有分支与远程分支的跟踪关系
git branch -vv

一开始我跟踪的是 origin/h2-diy-api 分支,所以会提示比远程分支多修改了内容,尽快提交。

我重新跟踪正确分支,问题解决。在 Git 中,跟踪(track)是指建立一个分支与另一个分支之间的关联关系,使得它们可以相互追踪对方的提交记录。跟踪的作用是方便在推送和拉取代码时进行操作,并提供了一种简洁和直观的方式来管理分支之间的关系。

2.3.2 分支:合并(merge)

比如我在 master 分支,此时我要将 dev 的分支合并过来。

sh
1
2
3
4
# 合并后自动commit
git merge dev
# 合并后并保留merge commit。如果没有 --no-ff,快速合并就只是移动指针
git merge dev --no-ff

合并常见三种情况:

  1. Fast-forward:快进合并,自动处理
  2. Recursive strategy:递归合并,自动处理
  3. CONFLICT (content):冲突合并,手动处理

像 Fast-forward,如下图,此时 master 分支要合并 hotfix,是没有任何冲突的,master 只需要指向前方的地址值 C4 即可。这种合并叫做 Fast-forward。

像 recursive strategy,如下图,一般是因为两个分支有共同的祖先,但是各自分支上进行了更改,并没有发生冲突。

像 CONFLICT (content),如下图,一般是由于多人同时改了同一个文件,甚至同一行。git 并不会提交 commit,而是将冲突的内容保存在暂存区,等人工去纠正后再 commit。

人工处理冲突,可以通过三向合并工具。ide 基本都有内置该功能,比如 idea 的三向合并工具如图。在 cherry-pick、merge、stash 等发生冲突时,均会触发三向合并。

[Git - git-merge Documentation](https://git-scm.com/docs/git-merge#:~:text=With --no-commit perform,merges with --no-commit)

2.3.3 分支:复制 commit(cherry-pick)

cherry-pick,顾名思义,精心挑选。

与 merge 不同,它只合并指定的 commit。

sh
1
2
3
4
5
6
7
8
# 表示将 commit 为 111 和 222 的改动,合并到当前分支
git cherry-pick 111 222
# 表示将 commit 为 111 到 222 的所有改动,合并到当前分支
git cherry-pick 111...222
# 表示将 commit 为 111 和 222 的修改,放到当前暂存区
git cherry-pick 111 222 --no-commit
# 表示将 commit 为 111 到 222 的修改,放到当前暂存区
git cherry-pick 111...222 --no-commit

git cherry-pick 命令用法详解 - CSDN 博客

2.3.4 分支:回退(reset)

将当前分支的内容回退到指定的分支,指定分支之后的都被删除。

sh
1
2
3
4
# 硬删除,真实删除
git reset --hard commitid
# 软删除,虚假删除
git reset --soft commitid

2.3.5 分支:变基(rebase)

我的 Git 提交记录是 A -> B -> C -> D,我现在想要在 A 里面添加一个文件,并且不影响 git 记录。如下操作。

sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 1. 开启交互式变基
git rebase -i --root

# 2. 在编辑器中将要修改的 commit,由 pick 改为 edit

# 3. 添加文件,进行修补提交
git commit --amend -m "feat: test"

# 4. 提交变基
git rebase --continue

# 5. 让所有变基后的提交的 commitTime = authTime
git filter-branch -f --env-filter 'export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"'

2.3.6 标签:新增 / 删除 / 切换 / 推送

为 commit 为 51d54ff 的创建标签 ava。

sh
1
2
3
4
# 简洁版标签
git tag ava 51d54ff
# 描述版标签
git tag ava 51d54ff -a -m "描述"

切换标签

sh
1
git checkout ava

如果我想再切换到最新的 commit,只需要切换分支即可。

因为分支就是标识了最新的 commit。

创建了标签推送到远程分支

sh
1
git push origin --tags

删除本地标签 ava

sh
1
git tag -d ava

删除远程标签

sh
1
git push origin tag -d ava

2.4 历史重写工具(git-filter-repo)

git-filter-repo 是一个 Git 仓库历史重写工具,它可以对 Git 仓库的提交历史、文件、作者信息等做大规模修改。它是官方推荐替代 git filter-branch 的工具,比 filter-branch 快、稳定、易用,而且更安全。

安装了 python 环境后(推荐使用 pyenv),通过 pip 安装

sh
1
pip install git-filter-repo

2.4.1 替换 commit message

保持 git 元信息,替换 commit message。

sh
1
git filter-repo --replace-message a.txt --force

其中 a.txt 的内容结构如下:

sh
1
oldmsg==>newmsg

2.4.2 替换 commit 的某个文件

保持 git 元信息,直接替换文件内容。适合只改动过一次的文件。

sh
1
git filter-repo --replace-text a.txt --force

其中 a.txt 的内容结构如下:

sh
1
file.txt==>C:\path\to\repo\new_file.txt

2.4.3 重建历史并删除某个文件夹

保持历史结构,在所有提交中,删除某个文件夹的内容。

sh
1
git filter-repo --path unwanted-folder --invert-paths

三、提交规范与约定

3.1 Conventional Commits 规范

参考约定式提交

类型说明示例场景
feat新增功能feat: 添加用户注册模块
fix修复缺陷fix: 解决登录页面崩溃问题
docs文档变更docs: 更新 API 使用说明
style代码样式调整(不改变逻辑)style: 修正缩进、删除多余空行
refactor代码重构(既非修复也非新增功能)refactor: 优化数据验证逻辑结构
perf性能优化perf: 减少数据库查询次数
test测试相关变更test: 添加用户登录单元测试
chore构建/依赖维护chore: 更新 webpack 配置、升级 npm 包
ci持续集成配置变更ci: 调整 GitHub Actions 流程
build影响构建系统的变更build: 修改 Dockerfile 配置
revert回滚之前的提交revert: 撤销某次错误合并