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下载,配置,初始化,暂存,提交,查询

首先需要在Ubuntu下载GIT。GIT我们直接用Bash命令去下就行:
#bash:sudo apt update && sudo apt install git -y #安装GITgit --version #查看git是否安装成功因为Ubuntu软件源服务器在国外,可能下载速度会很慢。所以我们也可以从国内的镜像网站下载:
首先需要临时新建下载源的配置单:位于:/tmp/tsinghua.list
IMPORTANT大家应该注意到了,这个配置单的位置是/tmp下。这个是Linux设计时设计的临时文件区,其通常不写入硬盘,只在内存当中。当我们关机后,会随着内存的清除而清除数据。所以是临时清单
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiversedeb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiversedeb 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输入下载指令,系统就会自动根据我们配置的文件从清华源下载:
#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仓库前要先设置好用户信息:
#bash:git config --global user.name "这里是我们起的名字"git config --global user.email "邮箱@example.com"git config --list #设置好名字和邮箱后,我们来查看我们的设置是否成功
设置完成后,进入项目文件夹,然后就可以给项目文件夹创建git仓库了:
#bash:git init #初始化git仓库初始化git仓库,本质上会生成.git的隐藏文件夹,里面就是我们的git仓库。他通常包含:
| 文件夹 | 内容 |
|---|---|
| objects/ | 这里存放的是GIT的四大对象和所有文件的快照 |
| refs/ | 里面存放的是分支,标签指针 |
| HEAD | 记录当前所在的分支 |
| index | 这是我们提交的暂存区 |
| config | 存放我们之前配置的名字,邮箱。 |
注意一点是git init后并没有main分支,此时有HEAD,是一个悬空指针。只有提交commit后才会出现main分支
#bash:git status #查看当前的状态,红色:未跟踪,也就是新出现的文件。 或者:未暂存,也就是改了但是还没有提交到暂存区 绿色:代表已经提交到暂存区,待提交git add . #加入暂存区
#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分支
#bash:git checkout -b dev #或者git switch -c dev#创建新的分支并切换到新分支
#bash:git branch #查看所有分支的情况git checkout main #或者git switch main#切换回main分支git merge dev的合并原理
#bash:git merge dev #合并dev分支到maingit branch -d dev #删除dev分支git merge dev在合并分支的时候会遇到很多的现实情况,针对不同的情况有不同的合并策略:
1.快进合并
当我们的commit记录呈现这样时候:
A ——> B ——> 没有新的提交 #(main) | D ——> E #(分支)合并之后,由于main没有新的提交,所以直接快进到了D——>E的分支上,连成了一条完整记录线:
A ——> B | D ——> E2.三路合并
当我们的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解决冲突后提交:(记得删掉冲突文件中的<<<<<等)
#bash:git add 文件git commit -m 文件#或者:git merge --continue此时G便创立了,成功合并了分支。
===========================================================================================================================
NOTE截至目前的笔记,已经可以基础的操作git本地仓库了。后面的笔记是我学习的进阶的git玩法,也同样分享给大家:
============================================================================================================================
GIT进阶学习之Rebase
交互式 Rebase(他可以用于交互式整理提交历史)
我们通过命令启动:
#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默认打开的编辑器:
#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 Textgit会利用core.editor的配置,打开/创建的git-rebase-todo。后续完成编辑后,读取,执行。
利用Rebase修改历史提交
假设我们要修改3个提交前的某个文件
#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 提交
#bash:git config --global rebase.autosquash truegit 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。
可能有一些绕,所以我用一个实例来演示一下:
假设我们现在有三个提交记录:
- a1b2c3d 修复登录页面的样式
- e4f5g6h 添加用户注册
- i7j8k9l 优化数据库查询
我们在记录:a1b2c3d 修复登录页面的样式上进行了更改。因为我们发现漏掉了一个CSS类名。我们修改完成文件后:
#bash:git commit --fixup a1b2c3d此时git会帮我们创建一个fixup!提交,变成:
- a1b2c3d 修复登录页面的样式
- e4f5g6h 添加用户注册
- i7j8k9l 优化数据库查询
- m1n2o3p fixup! 修复登录页面的样式
这个时候我们用Rebase:
#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,形成最终的提交记录:
- b3n4h5j 修复登录页面的样式 (会是一个新的SHA)
- e4f5g6h 添加用户注册
- i7j8k9l 优化数据库查询
至此完成对此次更改的提交,并合并到前一个相同提交里面(沿用之前的提交信息。也就是我们说的丢弃提交信息)
另外,如果我们rebase 错了,我们可以通过:
#bash:git rebase --abort来回到rebase之前的状态
GIT进阶学习之Reflog
Reflog基础使用
Reflog其本质上相当于Git的一个小日志。通过记录我们的每次的分支切换,reset,rebase等等。如果我们改错了,可以利用他改回原来的版本。他记录的核心是HEAD和分支指针的移动。也就是我们一系列的:commit,checkout,reset,rebase,merge,cherry-pick操作。
我们可以用:
#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>换成文件路径
#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指向它)。
当我们用:
#bash:git reset --hard <commit-hash> #后,被我们回退掉的提交链条上的commit就只剩下reflog指向了,如果再清除,就变成了悬空对象git rebase #或git commit --amend #后,这两个命令都会生成commit,旧的commit如果没有指向的,就会悬空git branch -D dev #删除分支后,如果说dev分支上的提交没有被合并到其他的分支,那么这个commit就会悬空我们可以利用悬空对象这个机制来实现找回丢失的提交:
#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,和以前一样。
#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-pickgit 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
#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,但是快进合并只会形成一条线,另一条分支消失了。我们可以通过如下命令操作:
#bash:git rebase main #把当前分支(dev)变基到main上git rebase -i main #这个是交互式变基,可以整理后进行提交。就是我们之前介绍Rebase时候的交互式编辑器git rebase --continue #冲突解决后继续git rebase --abort #放弃变基合并工具怎么选择?
面对这么这么这么这么多的合并工具,我们该怎么选择呢??
我们先来对比一下各个工具:
| 对比项目 | Merge | Rebase | cherry-pick |
|---|---|---|---|
| 操作对象 | 一整个分支 | 当前分支的历史 | 单个/多个的commit |
| 历史的结构 | 保留分叉,可能有merge commit | 变成直线 | 在当前分支末尾追加一个新的commit,历史仍然是线性的 |
| 是否生成新commit | 会生成merge commit | 当前分支提交会生成新commit | 是 |
| 原commit SHA | 不变 | 变 | 原提交不变;但是当前分支上生成的新commit的SHA不同 |
| 是否改写历史 | 否 | 是 | 否 |
| 是否适合已push的分支 | 适合 | 不适合 | 适合 |
| 冲突后继续的方法 | git commit 或 git merge --continue | git rebase --continue | git 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仓库。
我们可以通过代码来实现:
#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的文件交上去,所以我们需要清理下:
#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红色的清掉) - 需要的时候将存的取出来,恢复工作区,暂存区的状态
我们可以输入:
#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忽略的文件除此之外,还有些功能:
#bashgit stash show #查看具体更改了哪些文件git stash show -p #用于查看具体 diff(改动)的内容git stash show stash@{1} -p #看指定的第二条stash的内容如果我们后续需要继续写git add . 放到暂存区)
#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的暂存办法:
#bash:git add -p project.py这个时候可以用hunk(hunk表示一小块的改动)。此时git会把他把这两处的改动分成两个hunk,因为他们中间的代码是没有被改动的。此时,git会问我们让我们一个一个改动的选择:
#bashStage 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里面输入,需要先进入项目目录)
#bash:git submodule add https://github.com/example/great-p.git themes/great-p #这里会根据这个url来建立一个子模块,指向目录:themes/great-pgit status #查看子模块状态git add .gitmodules themes/great-p #第一步的命令,git创建了 .gitmodules,还有themes/great-p,这里就是把这个改动加入暂存区git commit -m "添加great-p子模块" #commit带有Submodule的项目的完整远程克隆——>本地
现在假设,我们做完了apk-progect这个项目,push到了服务器。然后我们更换了一台电脑,现在需要在新的电脑上从服务器下载这个项目。
我们克隆这个项目到这台新的电脑上:
#bash:git clone https://github.com/apkapk/apk-project.git #我们从github上克隆我们之前提交的项目这种克隆方式克隆的结果是,有themes/great-p目录,但是ls后会发现是空的。因为此时父仓库只存储了一个指针去指向great-p,所以我们还需要拉取代码块:
#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
#bash:git pull origin main #这个命令可以在子模块内部拉取最新的代码git add themes/great-pgit commit -m "更新great-p子模块到最新版" #提交还有一种方法,不需要进入子模块,直接在父模块的目录执行:
#bash:git submodule update --remote themes/great-p #--remote表示去子模块的远程拉取最新代码,而不是只更新到父仓库记录的hashgit add themes/great-pgit commit -m "更新great-p子模块到最新版" #提交当我们更新时候,并没有输入远程的地址,那么git怎么知道去哪里拉取呢??其实他的工作流很有意思:
.gitmodules #git会先在父仓库里面读取这个,从而找到子模块的位置 | |.git/config #git读取父仓库里面的本地配置,确认已经激活了子模块 | |themes/great-p/.git/config #git读取子模块仓库的配置,知道了远程拉取的urlNOTE上面的流程针对的是直接从父仓库更新,如果我们cd进了子模块,采用了第一种方法,那么git就会直接读取子模块自己的config配置
(未完待续,感谢观看!!!)
陕公网安备61020002000144号