花园
9479 字
47 分钟
Linus(林纳斯-本纳第克特-托瓦兹)的GIT学习笔记

GIT简介与诞生#

伟大的GIT是Linus杰出的作品。他能做出Linux已经很牛了,还研发了GIT,真是太太太太太太太牛了!!!!
2005年,Linus基于Linux内核开发而建立了一套分布式版本控制系统(DVCS),也就是GIT。他通过记录文件状态的快照,来实现版本控制。这和SVN有本质的差异。SVN采用的方案是集中式(CVCS),交由中心服务器进行存储新文件,开发者只能获得最新的文件,本地不保存完整的提交历史。而GIT克隆出来的都是完整的仓库副本,有着全部的提交历史,这样任何一个开发者的电脑都可以直接复刻出完整的项目文件。

通过本地建立git仓库,然后进行提交,分支,历史查看。无需任何的网络。所有的对象都通过SHA-1进行哈希校验,这使得数据更新历史很好的保存了下来。后续还可以push到服务器。

所谓SHA-1哈希校对,他本质是一种哈希算法,会计算出160位的二进制摘要,以40个16进制字符呈现(160bit位——>20byte字节——>40个16进制字符),通过字符的比对来防止开发文件的篡改,伪造等。确保我们的文件完整和真实。

GIT之后,就诞生了我们熟知的Github和国产的Gitee,这也是大家约定俗成的大型开源代码仓库。可以拉取等来交换代码,协作开发。由此可见git的影响力。

GIT大体框架#

git的大体框架可以分为:

工作区 ——> 暂存区 ——> 仓库

其通常有三种状态:

已修改:文件有新的改动,但还没有提交git暂存
已暂存:将文件的改动提交到暂存区,还没有正式commit提交
已提交:完整的git提交,安全的保存在本地git仓库

在GIT中,所谓工作区,就是我们实际编辑文件的地方。我们用vim,nano,VS code编辑的文件所有的改动都会被记录,GIT会记录到这些地方被更改。我们可以将其提交到暂存储区,其相当于一个虚拟清单,可以理解为可编辑文件的一个提交预览。当我们最终确认提交的时候,提交后,就会形成已经提交的提交区。

为什么git要有暂存区?他相当于超市购物的购物车,用于暂存我们对文件的改动,最后结账(提交)的时候,可以择需提交。

GIT通常由四种对象构成,其全部通过哈希进行引用:

  • Blob : 文件内容
  • Tree : 目录结构(对了,大家可以下一个tree(Bash的一个工具)很好用,可以按树形反馈当前目录结构)
  • Commit : 提交信息(通常指向 tree + 父 commit)
  • Tag: 指向特定的 commit 的引用

GIT的每一个对象都有SHA-1哈希值进行唯一标识,对象之间通过哈希进行引用。
所谓Blob,其实指的是文件内容,通常不包含文件的名字,权限,目录结构那些。
所谓Tree,指的是目录结构,他就是记录文件的名字,权限,同时对应上文件的blob或者子tree的哈希
对于commit,他指的是一次完整的提交记录,通常包含tree(指向父目录的tree对象),Parent(父提交的哈希值),committer(真实的提交者+时间戳),message(提交的信息)
对于Tag,其本质是一种标签。指向的是特定的commit的命名引用,通常他用于标记版本发布。
注意区分:commit记录了一次代码改动,并进行快照,记录中包含的有父提交。Tag对象是哈希引用commit的,给快照打上版本标记,就是和某一个commit绑定住

GIT分支#

所谓git的分支,就是指向commit的可移动的指针。创建一个分支=创建一个41字节的文件。通常我们有一个主分支main,随着提交,我们可能需要对某些文件进行更改测试,而我们又不想让这个改动测试影响到我们的主代码。所以我们可以在需要的commit节点上建立一个分支,然后在分支上面改。这样我们的主文件还是完整保留了。

main(主分支): A —> B —> C —> D
|
feature(假设我们在C创建了分支):E —> F 在C建立分支后,这条线上可以放心测试,然后迭代出E,F。不影响C,D主线文件完整性

GIT基础命令和实操#

NOTE

所有的注释都在图片下方

GIT下载,配置,初始化,暂存,提交,查询#

图片1

首先需要在Ubuntu下载GIT。GIT我们直接用Bash命令去下就行:

Terminal window
#bash:
sudo apt update && sudo apt install git -y #安装GIT
git --version #查看git是否安装成功

因为Ubuntu软件源服务器在国外,可能下载速度会很慢。所以我们也可以从国内的镜像网站下载:
首先需要临时新建下载源的配置单:位于:/tmp/tsinghua.list

IMPORTANT

大家应该注意到了,这个配置单的位置是/tmp下。这个是Linux设计时设计的临时文件区,其通常不写入硬盘,只在内存当中。当我们关机后,会随着内存的清除而清除数据。所以是临时清单

deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-security main restricted universe multiverse
#这三行代码就是配置清单,其格式是:deb <仓库地址> <版本代号> <软件分类>
第一行代码的jammy指定了ubuntu22.04的清华源主仓库
第二行代码的jammy-updates 指定了ubuntu22.04的清华源更新仓库
第三行代码的jammy-security指定了ubuntu22.04的清华源安全更新仓库
NOTE

注意的是:上述配置清单中,jammy是Ubuntu22.04版本的代号。所以这个命令只有这个版本的Ubuntu可以用。
如果我们的版本是:Ubuntu24.04,需要换成他的代号:noble
如果我们的版本是:Ubuntu20.04,需要换成他的代号:focal

之后可以在bash输入下载指令,系统就会自动根据我们配置的文件从清华源下载:

Terminal window
#bash:
sudo apt update \ #按照清单更新软件源
-o Dir::Etc::sourcelist=/tmp/tsinghua.list \
-o Dir::Etc::sourceparts=/dev/null
sudo apt install git -y \ #按照清单下载git
-o Dir::Etc::sourcelist=/tmp/tsinghua.list \
-o Dir::Etc::sourceparts=/dev/null

有了git后,我们在建立git仓库前要先设置好用户信息:

Terminal window
#bash:
git config --global user.name "这里是我们起的名字"
git config --global user.email "邮箱@example.com"
git config --list #设置好名字和邮箱后,我们来查看我们的设置是否成功

图片1

设置完成后,进入项目文件夹,然后就可以给项目文件夹创建git仓库了:

Terminal window
#bash:
git init #初始化git仓库

初始化git仓库,本质上会生成.git的隐藏文件夹,里面就是我们的git仓库。他通常包含:

文件夹内容
objects/这里存放的是GIT的四大对象和所有文件的快照
refs/里面存放的是分支,标签指针
HEAD记录当前所在的分支
index这是我们提交的暂存区
config存放我们之前配置的名字,邮箱。

注意一点是git init后并没有main分支,此时有HEAD,是一个悬空指针。只有提交commit后才会出现main分支

Terminal window
#bash:
git status #查看当前的状态,红色:未跟踪,也就是新出现的文件。 或者:未暂存,也就是改了但是还没有提交到暂存区 绿色:代表已经提交到暂存区,待提交
git add . #加入暂存区

图片1

Terminal window
#bash:
git commit -m "提交信息" #提交到仓库
git log #查看提交历史
git diff #查看改动的文件改动了什么
git log --oneline #查看简洁历史,只返回短的SHA和提交的版本标题
git log --oneline --graph #查看简洁历史的同时显示分支合并图
git log --oneline --all #显示所有分支的历史
git log --oneline --decorate #显示简洁历史的同时显示分支指向
git reset --hard HEAD~1 #返回上一个版本(这个命令后面Reflog里面会讲述)

GIT分支#

Terminal window
#bash:
git checkout -b dev #或者
git switch -c dev
#创建新的分支并切换到新分支

图片1

Terminal window
#bash:
git branch #查看所有分支的情况
git checkout main #或者
git switch main
#切换回main分支

git merge dev的合并原理#

Terminal window
#bash:
git merge dev #合并dev分支到main
git branch -d dev #删除dev分支

git merge dev在合并分支的时候会遇到很多的现实情况,针对不同的情况有不同的合并策略:

1.快进合并#

当我们的commit记录呈现这样时候:

A ——> B ——> 没有新的提交 #(main)
|
D ——> E #(分支)

合并之后,由于main没有新的提交,所以直接快进到了D——>E的分支上,连成了一条完整记录线:

A ——> B
|
D ——> E

2.三路合并#

当我们的commit记录呈现这样时候:

A ——> B ——> C ——> D #main有新的提交
|
E ——> F #dev分支也有新的提交

GIT会找到main分支上最新的提交也就是D,以及dev分支的最新提交F。汇总D&F的改动,形成新的提交G,示例如下:

A ——> B ——> C ——> D ————> G
| |
E ——> F ——————————

3.三路合并时发生冲突#

而此时,如果D & F 同时对某个文件进行改动的化,程序也不知道G该听哪个分支的改动,所以就会报错(merge conflict):
所以此时,我们需要看冲突的地方进行手动纠正:

<<<<<<< HEAD
这里会出现main中冲突的内容
=======
这里会出现dev中冲突的内容
>>>>>>> dev

解决冲突后提交:(记得删掉冲突文件中的<<<<<等)

Terminal window
#bash:
git add 文件
git commit -m 文件
#或者:
git merge --continue

此时G便创立了,成功合并了分支。

===========================================================================================================================

NOTE

截至目前的笔记,已经可以基础的操作git本地仓库了。后面的笔记是我学习的进阶的git玩法,也同样分享给大家:

============================================================================================================================

GIT进阶学习之Rebase#

交互式 Rebase(他可以用于交互式整理提交历史)#

我们通过命令启动:

Terminal window
#bash:
git rebase -i HEAD~5 #整理最近5次提交

此时就会出现交互式页面。就是我们常用的编辑器。里面可供我们选择:

#下面初始示例中:xxxxxx:是每个commit的简短的SHA,$$$$$$是对应的提交信息

#vim:
pick xxxxxx $$$$$$ #会显示最近五次的信息,预填的是pick:保留提交
pick xxxxxx $$$$$$
pick xxxxxx $$$$$$
pick xxxxxx $$$$$$
pick xxxxxx $$$$$$
#对于具体的每个commit的操作,我们只需要按需改变前面的命令就行:下面提供一些常用的操作:
pick xxxxxx $$$$$$ #保留提交
reword xxxxxx $$$$$$ #修改提交信息
squash xxxxxx $$$$$$ #合并到前一个提交
fixup xxxxxx $$$$$$ #合并但丢弃提交信息
drop xxxxxx $$$$$$ #删除提交
edit xxxxxx $$$$$$ #暂停修改

完成后保存退出,如果命令中有reword就会继续弹出新的编辑器,需要写具体的提交信息。squash也会弹出。而对于edit,则会暂停,等待我们对提交内容的修改。edit的使用方法在下一条利用Rebase修改历史提交讲述。fixup的使用方法在自动合并 fixup 提交(这个针对于GIT 2.34及以上版本) 讲述。

所有编辑器保存退出后,Rebase会自动执行操作。

关于Rebase的原理,他本质上讲是git为了方便我们整理各个提交历史,而推出的自动化方案。他在.git/rebase-merge/下生成文件git-rebase-todo,也就是我们进入Rebase的编辑文件,当我们编辑结束保存后。git会读取修改后的git-rebase-todo并按照里面的命令执行提交历史的管理。

git有一个core.editor,他是git的默认文本编辑器配置。我们通常可以设置我们用git默认打开的编辑器:

Terminal window
#bash:
git config --global core.editor vim #默认使用VIM编辑器
git config --global core.editor nano #默认使用nano编辑器
git config --global core.editor "code --wait" #默认使用VS Code编辑器,注意这个wait的目的是防止code打开返回结果。导致git错误执行。因为code这种图形化编辑器打开的时候就默认返回值。所以加上wait后git会知道只有关闭编辑器才算结束编辑。
git config --global core.editor "subl -n -w" #默认使用Sublime Text

git会利用core.editor的配置,打开/创建的git-rebase-todo。后续完成编辑后,读取,执行。

利用Rebase修改历史提交#

假设我们要修改3个提交前的某个文件

Terminal window
#bash:
git rebase -i HEAD~3 #先进入rebase
#然后对需要更改的提交前面改成edit,他会暂停,此时我们再修改文件。修改之后执行bash:
git add .
git commit --amend #这个提交会弹编辑器让我们写提交信息。如果不想编辑提交信息,我们可以:
git commit --amend --no-edit #这个和上一个命令2选1,取决于想不想更新提交信息
git rebase --continue #这一步执行后,rebase会结束暂停状态,所以必须有

自动合并 fixup 提交#

Terminal window
#bash:
git config --global rebase.autosquash true
git commit --fixup <commit-hash>
git rebase -i --autosquash HEAD~5

这三条命令是rebase中fixup核心的指令。他用于合并并丢弃提交信息。

当我们输入:git config --global rebase.autosquash true 后,本质上讲是设置了一个全局配置。在以后执行 git rebase -i 时,自动启用autosquash。(autosquash是git的一个自动整理功能,当我们运行git rebase -i时,git会自动扫描提交信息,这时以fixup!或squash!开头的提交,会自动移动到对应目标提交的后面,并把动作改成fixup或squash。这样我们就不需要手动在rebase的todo清单里拖来拖去了:传统不使用autosquash的话,我们需要把pick改成fixup,然后挪动到对应的位置,再在后面加上fixup!相当的麻烦。)

当我们输入:git commit --fixup <commit-hash>(这个 <commit-hash> 就是类似于a1b2c3d的简短的SHA)后,git会创建一个新的fixup提交,专门用来“修复”某个历史提交。
也就是说git会生成一个提交信息: fixup! 对应的提交信息

当我们输入:git rebase -i --autosquash HEAD~5 / git rebase -i HEAD~5(用这个后面的命令的前提是走了第一步,默认配置autosquash) 后,git会自动识别提交信息里以fixup!或者squash!开头的提交,并把其放到目标提交的后面,并自动把动作改成了fixup/squash。

可能有一些绕,所以我用一个实例来演示一下:

假设我们现在有三个提交记录:

  1. a1b2c3d 修复登录页面的样式
  2. e4f5g6h 添加用户注册
  3. i7j8k9l 优化数据库查询

我们在记录:a1b2c3d 修复登录页面的样式上进行了更改。因为我们发现漏掉了一个CSS类名。我们修改完成文件后:

Terminal window
#bash:
git commit --fixup a1b2c3d

此时git会帮我们创建一个fixup!提交,变成:

  1. a1b2c3d 修复登录页面的样式
  2. e4f5g6h 添加用户注册
  3. i7j8k9l 优化数据库查询
  4. m1n2o3p fixup! 修复登录页面的样式

这个时候我们用Rebase:

Terminal window
#bash:
git rebase -i --autosquash HEAD~4

因为有autosquash自动化,git会自动识别到fixup!,并挪动到合适的位置,最终把我们的git-rebase-todo变成:(如果没有autosquash,那么我们就需要手动把pick m1n2o3p fixup! 修复登录页面的样式 改成 fixup m1n2o3p fixup! 修复登录页面的样式并移动到第二行)

#Rebase(本质是txt):
pick a1b2c3d 修复登录页面的样式
fixup m1n2o3p fixup! 修复登录页面的样式
pick e4f5g6h 添加用户注册
pick i7j8k9l 优化数据库查询

这个时候,我们保存退出。git就会把m1n2o3p合并进a1b2c3d,形成最终的提交记录:

  1. b3n4h5j 修复登录页面的样式 (会是一个新的SHA)
  2. e4f5g6h 添加用户注册
  3. i7j8k9l 优化数据库查询

至此完成对此次更改的提交,并合并到前一个相同提交里面(沿用之前的提交信息。也就是我们说的丢弃提交信息)

另外,如果我们rebase 错了,我们可以通过:

Terminal window
#bash:
git rebase --abort

来回到rebase之前的状态

GIT进阶学习之Reflog#

Reflog基础使用#

Reflog其本质上相当于Git的一个小日志。通过记录我们的每次的分支切换,reset,rebase等等。如果我们改错了,可以利用他改回原来的版本。他记录的核心是HEAD和分支指针的移动。也就是我们一系列的:commit,checkout,reset,rebase,merge,cherry-pick操作。

我们可以用:

Terminal window
#bash:
git reflog #来查看所有SHA哈希引用的变更记录,也就是那些分支切换,reset,rebase等的记录
git reset --hard <commit-hash>(就是SHA,后续我就不备注这个了) #来恢复被reset的提交

具体来讲,当我们输入:git reset --hard <commit-hash> 后,git会把当前分支指针移到指定的commit,再把暂存区也重置到那个commit,同时把工作目录的文件也强制改成那个commit的样子。就相当于完全的回退!

精确撤销#

IMPORTANT

<file>是一个占位符,表示要操作的文件的具体路径。以下的命令在写的时候,要把<file>换成文件路径

Terminal window
#bash:
git checkout <commit-hash> -- <file> #用指定commit的版本去覆盖掉<file>,这个命令不会影响其他文件,同时会自动将该文件加入到暂存区
git revert <commit-hash> #他会生成一个新的提交来撤销之前指定提交的修改。这样既保留了撤销,又保留了之前的指定提交记录。特别适合于已经push服务器仓库的提交
git restore --staged <file> #在保留工作区修改的前提下,取消这个文件进入暂存区(也就是取消 git add)
git restore <file> #撤销工作区的修改,文件变成暂存区里面的版本

找回丢失的提交#

首先需要了解一个概念:悬空对象:

所谓悬空对象,就是指虽然还在GIT仓库里面,但是没有任何SHA哈希引用他(这个准确来讲是没有任何分支,标签,reflog指向它)。

当我们用:

Terminal window
#bash:
git reset --hard <commit-hash> #后,被我们回退掉的提交链条上的commit就只剩下reflog指向了,如果再清除,就变成了悬空对象
git rebase #或
git commit --amend #后,这两个命令都会生成commit,旧的commit如果没有指向的,就会悬空
git branch -D dev #删除分支后,如果说dev分支上的提交没有被合并到其他的分支,那么这个commit就会悬空

我们可以利用悬空对象这个机制来实现找回丢失的提交:

Terminal window
#bash:
git reflog #可以先看变更记录
git fsck --lost-found #查看悬空对象(dangling commits),并移动文件(我放后面解释这个移动)
git fsck --dangling #列出悬空对象
git show <dangling-hash> #查看悬空对象的具体的内容。 <dangling-hash>表示的是对象的SHA。
git prune #删除悬空对象
git gc #运行垃圾回收,清理悬空对象、压缩仓库等

当我们输入: git fsck --lost-found 后,他除了会查看悬空对象,还会将悬空对象复制到.git/lost-found/目录下。

  • 在.git/lost-found/commit/ 里放悬空的commit
  • 在.git/lost-found/other/ 里放悬空的blob,tree等

GIT进阶学习之高级工具下的分支操作#

用Cherry-pick——来挑选提交#

cherry-pick的作用是将其他的分支的某个或者某群commit应用到当前分支,并生成新的commit。就是复制别的分支的改动,整合到该分支下面。可以一次引用一个commit,也可以一次多个commit,以下是命令:<hash>还是表示SHA,和以前一样。

Terminal window
#bash:
git cherry-pick <commit-hash> #他会把hash指定的commit的改动应用到当前的分支,生成一个新的commit。
git cherry-pick <hash1> <hash2> <hash3> #应用的是我们用hash指定的多个commit(从左到右依次)
git cherry-pick <start-hash>..<end-hash> #这个应用的是从第一个hash,按照从左到右的顺序,依次到最后的hash所指向的commit。这里需要注意:左边不包含,右边包含(左开右闭)。但是,如果我们在起始的hash:<start-hash>面加上: ^ 那么就会变成左闭右闭!!
git cherry-pick --no-commit 或者 -n #应用改动但不自动提交,这样我们可以再修改。如果是多文件,还是同一个commit(合并改动)
git cherry-pick --edit 或者 -e #这会让我们可以在提交前编辑提交信息
git cherry-pick --continue #用于解决冲突后继续
git cherry-pick --abort #放弃本次cherry-pick
git cherry-pick --skip #跳过本次提交

大家应该注意到了,不论我们是用Rebase的fixup进行合并,还是用cherry-pick进行挑选合并,都会出现冲突的情况:就是因为两个分支同时更改了相同的文件,而文件的同一位置的内容又不一样,所以要人工修正成相同的版本,再继续。当然也可以放弃本次cherry-pick或者fixup

快进合并,三路合并,变基;merge,rebase,cherry-pick怎么选择?#

上面讲述了cherry-pick的分支整合。还有利用Rebase的fixup进行交互式整合历史和处理分支。但是在整合的时候,其实是可以分成三种情况的:

  • 三路合并:当两个分支都有各自的新提交,git没法直接“快进”时,就会走三路合并。
  • 变基:Rebase 不是简单的“合并”,而是把当前分支的提交摘下来,重新接到另一个分支的最新commit后面。这会使得结果历史会变成一条直线。
  • 快进合并:当main分支没有新的提交时候,git会把dev分支接续到main分支上,就叫快进合并。
    在这篇文章的:
    GIT基础命令和实操
    |________git merge dev的合并原理

    有简单的介绍分支合并的基础原理。这里将进行更细致的研究。

下面具体看下:

快进合并:#

我引用之前篇幅的图:

合并前:

A ——> B ——> 没有新的提交 #(main)
|
D ——> E #(分支)

合并后:

A ——> B
|
D ——> E #(main/dev)

这个路径是基于 bash: git merge dev
这个命令合并之后,我们可以发现main直接接续上了dev,没有任何之前有分支的记录。那么我们能不能保留分支的记录呢??
有办法:
就是加上 --no-ff

Terminal window
#bash:
git merge --no-ff dev

这样新的结构就会变成:

A ——> B ——> F #main
| |
| |
D ——> E #dev

就可以把记录保留下来了,本质是强制生成merge commit,保留dev分支曾经存在过的痕迹。

三路合并:#

我还是引用之前篇幅的图:

合并之前:

A ——> B ——> C ——> D #main有新的提交
|
E ——> F #dev分支也有新的提交

合并之后:

A ——> B ——> C ——> D ————> G #main
| |
E ——> F —————————— #dev

变基:#

变基就是摘下E——>F,把他接到D后面,形成一条直线。我用图来表示:

变基之前:

A ——> B ——> C ——> D #main有新的提交
|
E ——> F #dev分支也有新的提交

变基之后:

A ——> B ——> C ——> D #main
|
E ——> F #dev.注意这里和之前的快进合并不一样,变基还是保留了main & dev,但是快进合并只会形成一条线,另一条分支消失了。

我们可以通过如下命令操作:

Terminal window
#bash:
git rebase main #把当前分支(dev)变基到main上
git rebase -i main #这个是交互式变基,可以整理后进行提交。就是我们之前介绍Rebase时候的交互式编辑器
git rebase --continue #冲突解决后继续
git rebase --abort #放弃变基

合并工具怎么选择?#

面对这么这么这么这么多的合并工具,我们该怎么选择呢??

我们先来对比一下各个工具:

对比项目MergeRebasecherry-pick
操作对象一整个分支当前分支的历史单个/多个的commit
历史的结构保留分叉,可能有merge commit变成直线在当前分支末尾追加一个新的commit,历史仍然是线性的
是否生成新commit会生成merge commit当前分支提交会生成新commit是
原commit SHA不变变原提交不变;但是当前分支上生成的新commit的SHA不同
是否改写历史否是否
是否适合已push的分支适合不适合适合
冲突后继续的方法git commit 或 git merge --continuegit rebase --continuegit add . + git cherry-pick --continue

我们按使用场景区分:

场景适用方式
作为功能的分支想要合入主分支,且分支历史已经push到服务器,需要多人协作等情境下git merge 或 git merge --no-ff
还没push到服务器,想整理成干净的直线历史git rebase main
想修改历史提交,合并小提交,改提交信息git rebase -i
在快进合并的时候想保留分支存在的记录git merge --no-ff
把某个提交从别的分支拿过来git cherry-pick

孤儿分支#

孤儿分支??这是什么呢? 其实我们之前的分支都是基于main分支,然后在某一个commit节点分出去的分支。而孤儿分支,就是完全不走main分支主线。凭空产生的分支。对应到现实的例子就是:比方我们用kivy+buildozer+python开发一款apk,然后主线代码之外,在我们buildozer构建之后,会产生构建产物/bin/apk。因为构建产物和我们开发的代码无关,是我们迭代和发布的产品。我们为了不让他干预主代码,就需要先建立一个孤儿分支。让这个孤儿和main分支并行存放在同一个.git仓库。

我们可以通过代码来实现:

Terminal window
#bash:
git checkout --orphan apk-project #用 `checkout`创建一个孤儿分支,名字叫:apk-project。 `--orphan`用于强调创建的是一个没有历史,没有父提交的全新分支。
#如果git版本在2.23及以上,也可以:
git switch --orphan apk-project #效果一样

此时的仓库提交结构就会是:

main分支: A ——> B ——> C
apk-project分支: (存在,但是尚没有commit)
IMPORTANT

虽然这一步会创建一个空的apk-project分支,但是我们的工作区还保留着main分支的文件。直接提交会把main的文件交上去,所以我们需要清理下:

Terminal window
#bash:
git rm -rf . #这会将当前目录下的所有文件从暂存区和工作区都删掉

之后确保我们在apk-project分支上,然后我们再 git add bin/*.apk git commit -m "添加apk构建产物"就可以把apk版本存进去了。

GIT进阶学习之暂存与现场管理#

STASH#

STASH可以理解为GIT的一个草稿箱,我们可以设想一个场景。我们和同伴公布共同开发这个apk,然后我们本地有仓库,服务器也存了仓库。我们和同伴通过服务器作为桥梁,共同pull和push整个项目。我们正在写project.py。突然,同伴说他刚刚push上去的代码好像有bug,想让咱pull下来看看。这个时候,由于project.py还是未完工的状态,我们又不想commit一个残缺的版本上去。这个时候怎么办?
诶!这个草稿箱就发挥出作用了。我们可以先把py交到Stash,然后后续继续写的时候取出来就行。

关于Stash,它其实只干三件事:

  • 将当前的工作区,暂存区打包
  • 将工作区恢复到干净的环境(就是把git status红色的清掉)
  • 需要的时候将存的取出来,恢复工作区,暂存区的状态

我们可以输入:

Terminal window
#bash:
git stash push -m "WIP: 登录功能" #表示打包。其实 git stash 就可以了,加上push -m是备注的意思,备注一下这次打包,方便后续查看和恢复
git stash list #查看存了哪些
git stash apply stash@{2} #取指定的第三条,(从0开始,数字越大越旧)
git stash pop #取最近的一条,相当于: stash@{0}
git stash branch <branch-name> stash@{0} #用指定的第一条(最新的一条)stash创建一个新分支,并在新分支上应用这条stash,其中<branch-name>指的是分支的名字,和前文的<file>一样都是占位符。不过这里代表的意思是分支的名字。前文的<file>表示的是文件路径。
git stash -u #-u是--include-untracked的简写,他可以在保存时把未跟踪的文件也一起打包进去。
git stash -a #-a则会同时保存未跟踪文件 + 被.gitignore忽略的文件

除此之外,还有些功能:

Terminal window
#bash
git stash show #查看具体更改了哪些文件
git stash show -p #用于查看具体 diff(改动)的内容
git stash show stash@{1} -p #看指定的第二条stash的内容

如果我们后续需要继续写.py但是pop出来后发现有冲突。这个时候git会保留住这一条stash,不会删掉。当我们解决完冲突后,可以自己删除掉:(记得删之前git add . 放到暂存区)

Terminal window
#bash:
git stash drop stash@{0}

需要注意的一点是:stash是本地草稿箱,所以我们无法把他给push到服务器的git仓库。

部分暂存hunk#

通常情况下,我们运行bash:git add project.py会把整个py暂存起来。但是我们的py里面比方只改动了两行代码:

#python:
class Project:
def __init__(self):
pass
def test1(self,name,id): #以前是def test2(self,name,id)
pass
def test2(self,work_days,day_pay): #以前是def test3(self,work_days,day_pay):
pass

所以我们可以在命令后面加上-p,示意git用hunk的暂存办法:

Terminal window
#bash:
git add -p project.py

这个时候可以用hunk(hunk表示一小块的改动)。此时git会把他把这两处的改动分成两个hunk,因为他们中间的代码是没有被改动的。此时,git会问我们让我们一个一个改动的选择:

Terminal window
#bash
Stage this hunk [y,n,q,a,d,s,e,?]? #git会输出类似的提问,我们按需选择y,n,q,a,d,s,e,?
按钮意思
y暂存这个hunk
n跳过这个hunk
q退出,停止这个及以后的hunk的提问,之前确认的hunk会按我们的选择提交
a暂存这个hunk以及后面所有同文件的hunk
d不要这个hunk以及后面所有同文件的hunk
s把这个hunk拆分的更小一些
e手动编辑这个hunk
?查看帮助

这样的好处是我们可以逐个筛选我们需要的hunk进行暂存,从而实现部分暂存的目的。

GIT进阶学习之子模块与子树#

当我们已经不满足于本仓库开发的时候,我们有拉取或者联合其他仓库的打算的时候。这个时候就需要构建子模块或者子树,来让两个仓库建立链接。

Submodule子模块#

认识Submodule#

假设一个情景:除了我们开发的apk项目之外,还有一个项目是apk软件的画质优化项目,这两个项目是单独维护的。而apk的项目需要依赖画质优化的项目。我们想让apk项目固定引用画质优化项目的某一个固定版本(commit)。这个时候我们就可以建立子模块:

我们假设我们的项目名字叫:apk-project, 该项目位于我们本地;画质优化的项目名字叫:great-p,整个画质优化的仓库位于远程地址:https://github.com/example/great-p.git

NOTE

这里的github网址,两个项目,都是是我根据假设起的,不是真实网址!

子模块(Submodule)的特征是:我们可以创建一个链接(通常是.gitmodules 文件以及链接的子目录的commit hash),这个链接会建立两个仓库的索引关系,但是并不会直接把画质优化项目归属为apk-project里面。他的本质是:虽然在themes/great-p克隆了完整的great-p项目,但是只构建归属关系。所以我们git add的时候并不会把themes/great-p的具体文件存到暂存区,而是只存一个指向这里的指针。子模块的目录本质上是另一个完整的仓库。

关于.gitmodules:他是git用来记录子模块信息的配置文件,其位于仓库根目录。当我们执行bash:git submodule add的时候会自动生成。方便git知道子模块在哪,父模块在哪。
他会记录以下信息:

[submodule "themes/great-p"]
path = themes/great-p #子模块放在我们项目里的具体目录
url = https://github.com/example/great-p.git #子模块的远程仓库地址
branch = main #表示跟踪的具体分支(可选)

使用Submodule本地建立#

使用Submodule:(在bash里面输入,需要先进入项目目录)

Terminal window
#bash:
git submodule add https://github.com/example/great-p.git themes/great-p #这里会根据这个url来建立一个子模块,指向目录:themes/great-p
git status #查看子模块状态
git add .gitmodules themes/great-p #第一步的命令,git创建了 .gitmodules,还有themes/great-p,这里就是把这个改动加入暂存区
git commit -m "添加great-p子模块" #commit

带有Submodule的项目的完整远程克隆——>本地#

现在假设,我们做完了apk-progect这个项目,push到了服务器。然后我们更换了一台电脑,现在需要在新的电脑上从服务器下载这个项目。

我们克隆这个项目到这台新的电脑上:

Terminal window
#bash:
git clone https://github.com/apkapk/apk-project.git #我们从github上克隆我们之前提交的项目

这种克隆方式克隆的结果是,有themes/great-p目录,但是ls后会发现是空的。因为此时父仓库只存储了一个指针去指向great-p,所以我们还需要拉取代码块:

Terminal window
#bash:
git submodule init #初始化子模块
git submodule update #更新子模块
#也可以用一行表示:
git submodule update --init
#如果子模块里面还嵌套了一个子模块,我们需要:
git submodule update --init --recursive #他会递归拉取子模块里嵌套的子模块
#我们整合成一个命令:
git clone --recurse-submodules https://github.com/apkapk/apk-project.git
#或者:
git clone --recursive https://github.com/apkapk/apk-project.git
#这样就可以在克隆的同时,保持递归嵌套克隆和拉取代码块

更新子模块#

如果说,开发great-p的同伴把github上面的提交更新了,我们的父仓库(apk-project)虽然有指针指向子模块,但是子模块在 themes/great-p存储的仍然是旧的代码块,所以这个时候需要更新子模块:

整个流程可以理解为:先去github拉取新的代码块,然后提交形成新的commit hash哈希。
我们先进入子模块的代码块目录:bash:cd themes/great-p

Terminal window
#bash:
git pull origin main #这个命令可以在子模块内部拉取最新的代码
git add themes/great-p
git commit -m "更新great-p子模块到最新版" #提交

还有一种方法,不需要进入子模块,直接在父模块的目录执行:

Terminal window
#bash:
git submodule update --remote themes/great-p #--remote表示去子模块的远程拉取最新代码,而不是只更新到父仓库记录的hash
git add themes/great-p
git commit -m "更新great-p子模块到最新版" #提交

当我们更新时候,并没有输入远程的地址,那么git怎么知道去哪里拉取呢??其实他的工作流很有意思:

.gitmodules #git会先在父仓库里面读取这个,从而找到子模块的位置
|
|
.git/config #git读取父仓库里面的本地配置,确认已经激活了子模块
|
|
themes/great-p/.git/config #git读取子模块仓库的配置,知道了远程拉取的url
NOTE

上面的流程针对的是直接从父仓库更新,如果我们cd进了子模块,采用了第一种方法,那么git就会直接读取子模块自己的config配置

(未完待续,感谢观看!!!)

Linus(林纳斯-本纳第克特-托瓦兹)的GIT学习笔记
https://zengyuchen.cn/posts/git/
作者
玖蝉鸣
发布于
2026-09-24
许可协议
CC BY-NC-SA 4.0