可能大家都听过Git和GitHub,关于他们也有很多的描述,要说清楚他们之间的关系,故事还得从那一年说起。
时间 | 描述 |
1970-80年初 | 最初美国贝尔实验室的Ken Thompson,以BCPL语言为基础,设计出很简单接近硬件的B语言,并用B语言写了第一个UNIX操作系统;早期程序员可以用手工的方式进行备份,并以注释或者新建文本文件来记录变动,如使用cp 命令备份,使用tar命令将一些文件归为一个.tar文件。 后来Walter F. Tichy使用C开发了RCS (Revision Control System)用于版本控制,RCS允许多个用户同时读取文件,但只允许一个用户锁定(locking)并写入文件 (类似于多线程的mutex)。RCS的互斥写入机制避免了多人同时修改同一个文件的可能,但代价是程序员长时间的等待,给团队合作带来不便。(RCS本地版本控制系统) |
1986 | 直到1986年,Dick Grune写了一系列的shell脚本用于版本管理,并最终以这些脚本为基础,构成了CVS (Concurrent Versions System)版本控制系统,CVS后来用C语言重写,CVS是开源软件,CVS被包含在GNU的软件包中,并因此得到广泛的推广,CVS继承了RCS的集中管理的理念,CVS引进了分支(branch)的概念,分支是主干文件在本地复制的副本,用户对本地副本进行修改,且可以在分支提交(commit)多次修改。用户在分支的工作结束之后,需要将分支合并到主干中,以便让其他人看到自己的改动。CVS也有许多被人诟病的地方,如两个用户同时合并,那么合并结果将是某种错乱的混合体。 后来Karl Fogel和Jim Blandy(是长期的CVS用户)开发了Subversion,依赖类似于硬连接(hard link)的方式来提高效率,避免过多的复制文件本身。 |
1991-2001 | Linux 之父 Linus Tovalds 在 1991 年创建开源的 Linux 操作系统之后,起初参与Linux开源项目的代码是由Linus Torvalds本人通过“diff”和“patch”命令来手动为别人整合代码的。Linux开源的内核项目管理起来一直很麻烦,因为项目实在是太大了,开发者花费过多的时间在版本管理上,社区的弟兄们也对这种方式表达了强烈不满。 |
2002 | 到了2002年,一个叫Tim Kemp 的人发现 Subversion 是一个非常好的版本管理系统,但是缺乏一个好的图形界面客户端程序。做一个与 Windows 外壳整合的 Subversion 客户端程序的想法是受一个叫 TortoiseCVS 的 CVS 客户端程序所启发的。Tim 研究了 TortoiseCVS 的源码并以此为 TortoiseSVN 的基础,这也就是我们现在使用的SVN(是一种集中式的版本控制系统)。 Linus Torvald本人相当厌恶CVS以及Subversion,于是Linus选择了一个商业的版本控制系统BitKeeper(分布式VCS)BitKeeper的东家BitMover公司出于人道主义精神,授权Linux社区免费使用这个版本控制系统,但前提是Linux社区的用户不去破解BitKeeper。 |
2005 | 一晃眼,到了2005年,Linux社区牛人聚集,由于社区开发Samba的Andrew好奇地破解了BitKeeper公司的分布式VCS产品然后被对方发现,导致Linux项目被BitMover公司回收了免费使用权。Linus向BitMover公司道个歉,于是Linus最终决定写一款开源的分布式VCS软件,花了两周时间自己用C写了一个分布式版本控制系统,这就是Git!一个月之内,Linux系统的源码已经由Git管理了!(git分布式版本控制系统)(分布式,当我们连接共享版本库时,可以先将服务器上的项目,克隆到本地,相当于每一台电脑上都有整个项目的文件备份,在没有网时也可以开发,完成开发后,可以先提交到本地仓库,当有网的时候,再提交到共享版本库,这样一来,如果我们的服务器或者我们自己的电脑出故障,我们也没有任何担心的) |
2008 | GitHub(Git中心枢纽),于2007年10月1日开始开发,由GitHub公司,的开发者Chris Wanstrath、PJ Hyett和Tom Preston-Werner使用Ruby on Rails编写而成,他的UI设计确实有点糟糕。网站于2008年2月以beta版本开始上线,4月份正式上线。2008年7月,发布了Gists功能,用于托管代码片段。2008年12月,发布了GitHub Pages功能,这样大家就可以基于这个的repo,创建网站了。 |
2011 | 到了2011年,GitHub公司启动GitHub Enterprise项目,探索盈利模式。也是在11月,Github拥有了100万用户。 |
2014 | 2014年5月,Atom编辑器免费开源。现在大家常用的VSCode就是基于Atom。 |
2018 | 2018年6月,微软宣布收购GitHub,耗资75亿美元。GitHub上已经有了3000万开发者。 |
2019 | 1月份,GitHub宣布私有仓库全部免费,无限创建,但是最多只能有三个合作者。因为GitHub上性别严重失衡,男性群体高达95%以上,所以GitHub经常被大家戏称为GayHub,也是全球最大同性交友网站。 |
总结:Git 是由 Linux 之父 Linus Tovalds 为了更好地管理linux内核开发而创立的分布式版本控制系统。GitHub 的核心是一个名为 Git 的开源版本控制系统,Git 负责在您的计算机上本地发生的所有与 GitHub 相关的事情,GitHub可以托管各种git库,并提供一个web界面。GitHub的独特卖点在于从另外一个项目进行分支的简易性。为一个项目贡献代码非常简单,首先点击项目站点的“fork”按钮,然后将代码检出并将修改加入到刚才分出的代码库中,最后通过内建的“pull request”机制向项目负责人申请代码合并。
不要觉得这个有多难,理清思路,一步一步来,所有问题都可以迎刃而解。
在了解git的使用是,我们需要先了解一下几个相关的素语,就能更好的理解git了。git的大致流程如下图所示:
简单的说,工作区就是一个目录,一个项目存放的地方(本地导出或新建项目的地方)。
暂存区(stage),数据(快照)暂时存放的地方,git add就是将工作区的修改缓存在暂存区。git commit 时就是一次性把暂存区所有修改提交到仓库(分支)。
我们可以把暂存区的内容提交到我们的(本地)仓库,也叫版本库(Respository)该目录下的所有文件都会被 git 管理起来,每个文件的修改、删除、git 都能跟踪,以便随时追踪历史,和还原。
.git
隐藏目录就是 git 的版本库,里面存了很多东西,最重要的就是 stage(index) 暂存区,还有第一个分支 master,以及指向 master 的 HEAD 指针。
注:HEAD 是一个指针,总是指向当前分支。
远程仓库其实就是指托管在因特网或其他网络中(一台服务器)的你的项目的版本库(当然也可以有好几个远程仓库)。每个人都可以从这个远程仓库克隆一份到自己的电脑上,并且各自把各自的内容提交并推送到远程仓库里,也可以从远程仓库中拉取其他人提交的文件到本地。如:GitHub、Gitlab 等都属于远程仓库。
就好像我们购物一样,我们看到好几个商品,觉得有点意向,然后把他们添加(git add)到购物车(暂存区)里,在没提交订单前,购物车里的东西我们增增减减,最后我们终于选好了我们的商品,点击提交(commit)订单,我们提交了订单,但是还没支付,也就没有推送给商家(远程仓库)。看到花了不少钱,我们得确定下订单,于是我们找到未支付的订单列表(本地仓库),当然也能看到我们以前的一些订单,确认没问题了,我们准备支付推送(Push)给商家(远程仓库),商家就会收到这个订单。其实如果你用过SVN,对checkout和commit并不会陌生。
在了解GitHub前,我们先来了解一下git的基本使用。
我们去官网下载一个版本控制软件,地址https://git-scm.com/downloads。
后续步骤一直往下点就行,安装完成。后续步骤均在windows中完成。
创建一个空的 Git 存储库或重新初始化一个现有的存储库。我们均可以使用git init --help 命令来查看帮助文档。
# 语法:
git init [-q | --quiet] [--bare] [--template=]
[--separate-git-dir ] [--object-format=]
[-b | --initial-branch=]
[--shared[=]] [directory]
#-------------------常用命令--------------------
# 在当前目录新建一个 Git 仓库
$ git init
# 新建一个目录,并将其初始化为 Git 仓库
$ git init [project-name]
# 为新创建的存储库中的初始分支使用指定的名称
$ git -b [branch-name]
将存储库克隆到新目录中
语法:
git clone [--template=]
[-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]
[-o ] [-b ] [-u ] [--reference ]
[--dissociate] [--separate-git-dir ]
[--depth ] [--[no-]single-branch] [--no-tags]
[--recurse-submodules[=]] [--[no-]shallow-submodules]
[--[no-]remote-submodules] [--jobs ] [--sparse] [--[no-]reject-shallow]
[--filter=<过滤器>] [--] <存储库>
[<目录>]
# 从远程下载一个仓库
$ git clone [url]
使用git init,在当前目录新建一个仓库,当前目录下多了一个.git
的目录,这个目录是Git来跟踪管理版本库的。
执行 git init会在当前目录生成.git,其中就包含如下这些文件/文件夹,为了更好的了解git,我们需要了解一下这些目录的作用。
Git 默认会在这个目录中放置一些示例脚本。 这些脚本除了本身可以被调用外,可以设置特定的git命令后触发相应的脚本。
钩子种类 | 描述 |
提交工作流钩子 |
|
电子邮件工作流钩子 | git am命令会调用: applypatch-msg、pre-applypatch、 post-applypatch这些钩子。 |
其它钩子 | pre-rebase;可以禁止对已经推送的提交;
post-checkout:
... |
存储库的其他信息将记录在此目录,我们可以看到目录下有个info/exclude文件,用来 忽略指定模式的文件,和 .gitignore 类似,但是 .gitignore 是针对每个目录的。
保存所有更新的引用记录。其目录结构如下:
├── HEAD
└── refs
├── heads
│ ├── master
...
HEAD 记录所有更改记录,包括切换分支,logs/refs 下存储本地更改记录,如果有远程,也会记录远程remotes更改记录。
目录存放了各个分支(包括各个远端和本地的HEAD)所指向的commit对象的指针(引用),也就是对应的sha-1值。
如:
$ cat .git/HEAD
ref: refs/heads/master
Git对象是一个内容可寻址的文件系统,Git核心是一个简单的键值数据存储。可以将任何类型的内容插入到 Git 数据库中,Git 将为您返回一个唯一的键值,通过该键值可以在任意时刻再次检索该内容。
注:这里的key 即为 SHA1计算后的值。objects目录下存储有三种对象:数据对象(blob object)、树对象(tree object)、提交对象(commit object)。
进入.git\objects目录,我们会发现有很多2个字符的目录、info 和 pack目录。
1)info 和 pack 目录
当存储的文件很大时,git 会进行压缩,会存储到 info 和 pack 下;
2)2个字符的目录
这两个字母是计算的SHA1值(总共40个字符)的前两个字符,剩余的38个是该目录下的文件名。
他们是怎么存储的呢?我们需要了解一下git存储的原理,以便后面更好的理解并使用git的高级命令。
git使用了blob对象去存储了文件的内容。使用hash-object 命令将对象内容写入数据库,以此生成一个blob object。
# 创建一个新的数据对象并将它手动存入你的新 Git 数据库中
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
# 这些都是底层命令 实际上我们后面会用一个高级命令
# 命令参数选项
-w: 设置该参数指示hash-object 命令将对象内容写入数据库,若不指定此选项该命令仅返回对应的键值
–stdin: 表示从标准输入读取对象内容,若不指定此选项,则必须在尾部命令给出存入数据库的路径
–: 标记后续参数类型,即 – 后面的参数会被解析为file
# 如果你对linux命令属性 | 管道的应用就不会陌生,当然我们也可以这样做
#例如:
# 在工作区中新建一个文件
echo "test content" > testdc.txt
# 将文件中的内容,写入数据库(存文件)
git hash-object -w 文件路径
# 如果不加参数,不会写入数据库(返回对应文件的键值)
git hash-obejct 文件路径
上述命令的输出是一个 40 个字符的校验和哈希。这是 SHA-1 散列,改变内容则哈希值发送变化(和md5算法道理类似)。使用git命令查看数据库:
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ find .git/objects/ -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
在.git/objects目录下,我们可以看到多了一个文件,/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4,也就是说,Git存储内容的方式是使用SHA-1 散列的前2位做子目录,后38位作为存储内容的文件名。 我们使用cat-file 命令输出内容和对象类型:
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
$ git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4
blob
#参数
-p 输出对象内容
-t 输出对象类型
有没有发现,当我们使用hash-object命令去git数据库中存储文件时,我们在仓库的目录(d/gitRepository)中找不到对应的文件。因为我们没给它一个文件名。只是将文件内容存储在.git/objects目录下。
为了解决存储文件名的问题,于是有了树对象,允许我们将多个文件组织到一起。Git 以类似于 UNIX 文件系统的方式存储内容,所有内容都存储为树和 blob 对象,树对应于 UNIX 目录,而数据对象(blob)对应inode 或文件内容。单个树对象包含一个或多个条目,每个条目都是 blob 或子树的 SHA-1 哈希值及其关联的模式、类型和文件名。一个树对象也可以包含另外一个树对象。
官方的介绍都是比较精炼的,大致意思可以这么说,在实际项目过程中,我们每次提交都是一个多文件的提交。很少的时候是单文件的,那此时Git就不是单单存储一个 blob对象了,而是 tree对象。tree对象,见名知意,就是一个树对象,类似于操作系统目录,tree的分支,可能还是tree,也可能是blob。
2.1、构建树对象
要构建树对象,并塞入到暂存区,需要使用update-index、write-tree、read-tree等命令。
1)先创建一个test.txt的数据对象(blob 对象)
# 清空之前的版本库,初始化之后,我们在工作目录中新建一个文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ echo "test version1" > test.txt
# 使用hash-object 命令将对象内容写入数据库
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git hash-object -w ./test.txt
warning: LF will be replaced by CRLF in ./test.txt.
The file will have its original line endings in your working directory
fbb2ff04e2cae17293a83c5e83af505f62cd13d1
# 目前工作目录中只有一个文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ ls -l
total 1
-rw-r--r-- 1 Administrator 197121 14 Jul 21 21:57 test.txt
# 查看暂存区中的文件,现在是没有的
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git ls-files -s
2)使用update-index来创建test.txt文件的首个版本,并加入到暂存区,通过write-tree生成树对象,产生我们管理的项目的第一个版本。
# 将test.txt加入到暂存区中
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git update-index --add --cacheinfo 100164 fbb2ff04e2cae17293a83c5e83af505f62cd13d1 test.txt
--add 因为此文件并不在暂存区,首次需要-add
--cacheinfo 会从已存在的数据库(Object)中取得对应的内容并添加到索引中。
--文件模式:
--100644 表明这是一个普通文件
--100755 表示一个可执行文件
--120000 表示一个符号链接
# git ls-files -s查看暂存区中的文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git ls-files -s
100755 fbb2ff04e2cae17293a83c5e83af505f62cd13d1 0 test.txt
# find .git/objects/ -type f 查看数据库中的对象
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ find .git/objects/ -type f
.git/objects/fb/b2ff04e2cae17293a83c5e83af505f62cd13d1
# git write-tree生成树对象(给暂存区拍一张快照)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git write-tree
59e4512814d504004c6a86b2f0792adf8b004a1f
# 再次查看数据库中的对象,发现多了一个(树对象)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ find .git/objects/ -type f
.git/objects/59/e4512814d504004c6a86b2f0792adf8b004a1f
.git/objects/fb/b2ff04e2cae17293a83c5e83af505f62cd13d1
# 查看对象的类型
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git cat-file -t 59e4512814d504004c6a86b2f0792adf8b004a1f
tree
详细解析:当我们把文件添加到暂存区后,然后使用write-tree生成树对象(给暂存区拍一张快照),并存储到数据库中,这时数据库中的这个树对象,其实就是我们管理的项目的第一个版本了。
快照和我们平时说的备份,有什么区别呢?备份:从一个地方复制到另一个地方,两个地方的数据都是完整独立的,而快照:更像文件系统的一种存储标记,比如,我们有一个文件a(1,2,3)--将其内容修改a(1,4,3),并生成一个快照A,快照A的存储仅仅是修改过程中数据的变化部分,即记录(数据位置2-变成了-4)而不是像备份复制那样某个路径下完整的数据。
3)新增new.txt 将new.txt和test.txt的第二个版本加入到暂存区,并生成树对象。
# 工作目录中新建一个new.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ echo "new v1" > new.txt
# 将new.txt加入到数据库中(生成blob 对象)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git hash-object -w new.txt
warning: LF will be replaced by CRLF in new.txt.
The file will have its original line endings in your working directory
eae614245cc5faa121ed130b4eba7f9afbcc7cd9
# 查看此时数据库中的对象
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ find .git/objects/ -type f
.git/objects/59/e4512814d504004c6a86b2f0792adf8b004a1f
.git/objects/ea/e614245cc5faa121ed130b4eba7f9afbcc7cd9
.git/objects/fb/b2ff04e2cae17293a83c5e83af505f62cd13d1
# 查看修改前的test.txt文件内容
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ cat test.txt
test version1
# 修改test.txt的文件内容
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ vim test.txt
# 将修改的test.txt文件加入到数据库中(生成blob 对象)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git hash-object -w test.txt
warning: LF will be replaced by CRLF in test.txt.
The file will have its original line endings in your working directory
3a31d973aae0a644183e2e72395e683973f4acc1
# 查看数据库中的对象
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ find .git/objects/ -type f
.git/objects/3a/31d973aae0a644183e2e72395e683973f4acc1
.git/objects/59/e4512814d504004c6a86b2f0792adf8b004a1f
.git/objects/ea/e614245cc5faa121ed130b4eba7f9afbcc7cd9
.git/objects/fb/b2ff04e2cae17293a83c5e83af505f62cd13d1
# 将test.txt加入的暂存区,此时暂存区中的test.txt已经更新了(加入到暂存区一定要指定文件,加入文件名写错了,也会覆盖)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git update-index --cacheinfo 100644 3a31d973aae0a644183e2e72395e683973f4acc1 test.txt
# 将new.txt加入到暂存区
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git update-index --add --cacheinfo 100644 eae614245cc5faa121ed130b4eba7f9afbcc7cd9 new.txt
# 查看暂存区,当前暂存区和工作目录中的内容一致
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git ls-files -s
100644 eae614245cc5faa121ed130b4eba7f9afbcc7cd9 0 new.txt
100644 3a31d973aae0a644183e2e72395e683973f4acc1 0 test.txt
# 生成树对象(拍第二个快照,也就是我们项目的第二个版本)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git write-tree
434388dae0ea7dd251c040ff1735d5a59454133c
# 查看数据库中的对象
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ find .git/objects/ -type f
.git/objects/3a/31d973aae0a644183e2e72395e683973f4acc1
.git/objects/43/4388dae0ea7dd251c040ff1735d5a59454133c
.git/objects/59/e4512814d504004c6a86b2f0792adf8b004a1f
.git/objects/ea/e614245cc5faa121ed130b4eba7f9afbcc7cd9
.git/objects/fb/b2ff04e2cae17293a83c5e83af505f62cd13d1
详细解析:我们往数据库中新增了一个数据对象,并修改了test.txt,也将其加入到数据库中,次数数据库中的对象有4个,分别是:
.git/objects/3a/31d973aae0a644183e2e72395e683973f4acc1 test.txt的第二版本
.git/objects/59/e4512814d504004c6a86b2f0792adf8b004a1f 树对象,即项目的第一个版本
.git/objects/ea/e614245cc5faa121ed130b4eba7f9afbcc7cd9 new.txt的第一个版本
.git/objects/fb/b2ff04e2cae17293a83c5e83af505f62cd13d1 test.txt的第一个版本
当我们把修改后的test.txt和new.txt加入到暂存区后,此时工作目录和暂存区中的内容是一致的,使用git write-tree命令生成树对象,我们的数据库中就多了一个树对象
.git/objects/43/4388dae0ea7dd251c040ff1735d5a59454133c 树对象,即项目的第二个版本
4)read-tree命令的使用
若需要对某个存在三级文件夹的二级文件夹进行write-tree操作, 在把三级文件夹下的所有修改文件生成blob后,进行整体tree对象化,之后再与二级文件夹同级的文件夹和文件进行相同操作。此时就需要用到: read-tree 命令。但是实际操作中我们并不会这么去手动操作,将一棵树接到另一颗树上。
# 该操作会把tree对象59e4512814d504004c6a86b2f0792adf8b004a1f 加入暂存区中,并取名bak(实际Git会把此prefix默认为文件夹的名字)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git read-tree --prefix=bak 59e4512814d504004c6a86b2f0792adf8b004a1f
# 查看暂存区中的内容
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git ls-files -s
100755 fbb2ff04e2cae17293a83c5e83af505f62cd13d1 0 bak/test.txt
100644 eae614245cc5faa121ed130b4eba7f9afbcc7cd9 0 new.txt
100644 3a31d973aae0a644183e2e72395e683973f4acc1 0 test.txt
# 生成新的树对象
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git write-tree
9de7f27122d384812680907c0052ab66454c3fb4
第三棵树:9de7f27122d384812680907c0052ab66454c3fb4(项目的第三个版本)
最终就形成了如图所示效果:
项目的第三个版本并没有修改文件,只是将第一棵树加入到暂存区(索引),并生成了一颗新的树。现在呢,我们已经有了三个树对象,分别代表了我们要跟踪的项目的不同快照,如果想重用这些快照,就必须记住这三个哈希值,并且我们也不知道是谁保存了这些快照,什么时候保存的,以及为什么保存这些快照,于是就有了提交对象(commit object)。
提交对象能为你保存基本信息,我们可以通过调用commit-tree命令创建额提交对象,为此需要指定一个树对象的哈希值,以及该提交对象的父提交对象(第一次提交,则没有父对象)。
1)为第一棵树创建一个提交对象
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository/.git (GIT_DIR!)
$ echo 'first commit' | git commit-tree 59e4512814d504004c6a86b2f0792adf8b004a1f
384c65d38b4b57e074fd5bb8e1602516a2ac6b62
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository/.git (GIT_DIR!)
$ git cat-file -t 384c65d38b4b57e074fd5bb8e1602516a2ac6b62
commit
# 查看提交对象的信息,包含了第一棵树及作者、提交者(config中指定的user.name、user.email信息)及提交说明,提交对象是对树对象的一次封装
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository/.git (GIT_DIR!)
$ git cat-file -p 384c65d38b4b57e074fd5bb8e1602516a2ac6b62
tree 59e4512814d504004c6a86b2f0792adf8b004a1f
author mjx <[email protected]> 1626965281 +0800
committer mjx <[email protected]> 1626965281 +0800
first commit
2)为第二棵树创建一个提交对象
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository/.git (GIT_DIR!)
$ echo 'second commit' | git commit-tree 434388 -p 384c65
a78a51fff0535891210554d1bdcb8f0f3962fc11
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository/.git (GIT_DIR!)
$ git cat-file -p a78a51fff0535891210554d1bdcb8f0f3962fc11
tree 434388dae0ea7dd251c040ff1735d5a59454133c
parent 384c65d38b4b57e074fd5bb8e1602516a2ac6b62
author mjx <[email protected]> 1626965733 +0800
committer mjx <[email protected]> 1626965733 +0800
second commit
于是就能形成如官网所说的这种效果:
总结:项目的一个版本是一个提交对象,本质上项目的一个快照是一个树对象。
1)生成blob对象:git hash-object -w 文件路径
2)加入暂存区(更新索引):
git update-index --add 文件路径
git update-index --add --cacheinfo mode sha-1 文件名
git read-tree --prefix=test sha-1(某个tree的sha-1) 把某个tree读入索引中
3)创建树对象: git write-tree
4)创建commit对象: git commit-tree sha-1 -m "提交信息"
echo "提交信息" | git commit-tree sha-1 -p 父级sha-1
5)其他底层命令:
对象的内容查询:git cat-file -p sha-1
对象的类型查询:git cat-file -t sha-1
查看暂存区:git ls-files -s
以上就是git的底层原理,一般我们使用高级命令。
引用官方一张文件状态的生命周期图:
此文件工作区中, 但并没有加入到git库, 不参与版本控制,通过git add
状态变为Staged。
工作目录下的文件要么是未跟踪,要么是已跟踪(已跟踪有三种状态:未修改、已修改、已暂存)
版本库中的文件快照内容与工作区中完全一致,这种类型的文件可以是已经提交到版本库的文件或从远程仓库克隆到本地库而检出到工作区中的文件。如果它被修改, 而变为Modified,
如果使用git rm
移出版本库, 则成为Untracked
文件。
对工作区中的文件而言,文件已经修改了,但并没有进行其他操作。如果通过git add
可进入暂存staged
状态, 使用git checkout
则丢弃修改过, 返回到unmodified
状态, 这个git checkout
即从库中取出文件, 覆盖当前修改。
在工作区中的文件被添加到暂存区,在工作区中新增/修改的文件使用git add
可进入暂存staged
状态,执行git commit
则将修改同步到本地库中, 这时库中的文件和工作区的文件又变为一致, 文件为Unmodified
状态,执行git reset HEAD filename
取消暂存, 文件状态为Untracked/Modified.
前面我们已经使用到了git init初始化了一个仓库,初始化后,在当前目录下会出现一个名为.git的目录,所有Git需要的数据和资源都存放在这个目录。
Git 的配置文件是 .gitconfig
,可以放在用户的主目录(全局配置)下或项目目录下(项目配置)。
# 显示当前的 Git 配置
$ git config --list
# 编辑 Git 配置
$ git config -e [--global]
# 设置用来提交代码的用户信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]"
git add的作用是将文件/修改内容添加到暂存区。新增操作我们一般的执行步骤是:1)先执行git status 查看工作目录的文件状态;2)执行git add 添加文件;3)执行git commit -m 提交暂存区当前内容到仓库。
# 将指定文件添加到暂存区中
$ git add [file1] [file2] ...
# 将指定目录添加到暂存区中,包括子目录
$ git add [dir]
# 将当前目录中的所有文件添加到暂存区中
$ git add .
# 在添加每个更改之前都进行确认
# 对于同一个文件的多个更改,建议分开提交
$ git add -p
使用案例:
1)首先在git管理的目录下,新建两个文件(test.txt);
2)然后使用git add test.txt 将文件添加到暂存区(Stage),此时的文件状态为已暂存(staged);
3)使用git Gui工具可以看到,暂存区已经有一个文件了。(当然,我们也可以使用命令查看仓库的状态git status )
详细解析:
我们使用git add test.txt 文件相当于执行了:
git hash-object -w ./test.txt 生成了blob对象,并保存了相应的sha-1值
git update-index --cacheinfo sha-1 命令把 test.txt 放入了暂存区,并添加到了索引文件(index)中
注:git add 是先把内容放到了数据库中,在从数据库中取文件将其放入暂存区,并不是直接从工作目录到暂存区。
git status查看工作目录中文件的状态(已跟踪(已提交、已暂存、已修改)、未跟踪)
# 以长格式输出输出,这是默认设置(可以不写后面的参数)。
$ git status --long
# 以短格式输出输出。
$ git status -s
使用案例:查看我们添加的文件test.txt
# 查看工作目录中文件的状态
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached ..." to unstage)
new file: test.txt
详细解析:
使用 git status 后,Git会对所有文件进行sha-1值计算,若计算到与.git/index索引中得对应文件的sha-1值不同了,则代表有所改动,则标记为 已修改(Modify),若发现索引中不存在对应文件的sha-1值, 则标记为未跟踪(Untracked )。
上图告诉我们一个新文件,test.txt还没被提交,同时也告诉我们可以使用it rm --cached
git commit提交暂存区当前内容并添加描述信息(对仓库的更改)
# 将暂存区中的文件提交到代码仓库
$ git commit -m "commit message"
# 将指定的文件从暂存区中提交到仓库
$ git commit [file1] [file2] ... -m "commit message"
# 将工作区的更改直接提交到仓库
$ git commit -a
# 提交前展示所有的变动
$ git commit -v
# 使用新提交代替上次提交
# 如果代码没有任何变动,将会用于重写上次提交的提交信息
$ git commit --amend -m "commit message"
# 重做上次的提交,并将指定的文件包含其中
$ git commit --amend [file1] [file2] ...
使用案例:
# 提交暂存区的文件到仓库,如果注释比较少可以使用-m参数添加,如果注释较多,则不加参数会进入文件编辑模式
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git commit -m "第一次提交test.txt"
[master (root-commit) 2ca6bc4] 第一次提交test.txt
1 file changed, 1 insertion(+)
create mode 100644 test.txt
# 查看提交信息
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log
commit 2ca6bc4bea79b316fed32ceb1a9bbbe8e0ec6efb (HEAD -> master)
Author: mjx <[email protected]>
Date: Sat Jul 24 11:00:56 2021 +0800
第一次提交test.txt
# 此时工作目录和仓库是一致的
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git status
On branch master
nothing to commit, working tree clean
详细解析:
我们使用git commit -m "第一次提交test.txt" 文件相当于执行了:
git write-tree 生成了树(tree)对象,并产生了相应的树对象的sha-1值(保存项目的快照)
git commit-tree sha-1 -m 同时执行了git commit-tree对树对象的封装,包含提交注释、提交者的信息、时间,这是项目一个版本。
查看未暂存/未提交的修改。
# 显示暂存区和工作区的文件差别
$ git diff
# 显示暂存区和上一次提交的差别
$ git diff --cached [file]
# 显示工作区和当前分支的最近一次提交的差别
$ git diff HEAD
# 显示指定两次提交的差别
$ git diff [first-branch]...[second-branch]
# 显示今天提交了多少代码
$ git diff --shortstat "@{0 day ago}"
使用案例一:显示暂存区和工作区的文件差别
# 查看暂存区文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git ls-files -s
100644 915c628f360b2d8c3edbe1ac65cf575b69029b61 0 test.txt
# 编辑test.txt文件,新增一行内容test v2
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ vim test.txt
# 使用git diff 命令查看暂存区和工作目录的区别,
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git diff
warning: LF will be replaced by CRLF in test.txt.
The file will have its original line endings in your working directory
diff --git a/test.txt b/test.txt
index 915c628..95b7ed0 100644
--- a/test.txt
+++ b/test.txt
@@ -1 +1,2 @@
test v1
+test v2
详细解析:我们提交文件到仓库并不会清空暂存区的,所以当我们修改工作目录中的文件后,使用git diff 目录就能看到工作目录和暂存区中的区别。
使用案例一:显示暂存区和上一次提交的差别
# 将文件加入到暂存区
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git add test.txt
warning: LF will be replaced by CRLF in test.txt.
The file will have its original line endings in your working directory
# 查看暂存区和上一次提交的差别
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git diff --cached
diff --git a/test.txt b/test.txt
index 915c628..95b7ed0 100644
--- a/test.txt
+++ b/test.txt
@@ -1 +1,2 @@
test v1
+test v2
# 将指定文件从工作区删除,并将本次删除添加到暂存区
$ git rm [file1] [file2] ...
# 停止追踪指定的文件,不会删除文件
$ git rm --cached [file]
删除文件使用案例:
1)使用git rm test.txt 删除文件(工作区中的文件被删除),在暂存区有待提交的删除文件;
2)使用git commit删除(版本库)文件;
3)而使用 git rm --cached test_rmcached.txt 停止跟踪文件,在暂存区的文件被删除了,本地文件恢复Untracked状态。
# 删除文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git rm test.txt
rm 'test.txt'
# 工作目录被删除的文件待提交
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git status
On branch master
Changes to be committed:
(use "git restore --staged ..." to unstage)
deleted: test.txt
# 删除文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git commit -m "删除test.txt"
[master 74338f9] 删除test.txt
1 file changed, 1 deletion(-)
delete mode 100644 test.txt
# 新建一个文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ vim test_rmcached.txt
# 将文件加入都暂存区
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git add test_rmcached.txt
# 查看工作目录文件的状态
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git status
On branch master
Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: test_rmcached.txt
# 停止跟踪文件,但不删除文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git rm --cached test_rmcached.txt
rm 'test_rmcached.txt'
# 文件为未跟踪状态
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git status
On branch master
Untracked files:
(use "git add ..." to include in what will be committed)
test_rmcached.txt
详细解析:git rm 除了删除本地文件外也会将删除动作加入到暂存区;
# 移动/重命名文件
$ git mv
# 强制移动/重命名文件
$ git mv -f
使用案例:使用mv 命令将readme.txt 重命名为test.txt
# 新建readme.txt文件后提交到仓库,然后查看暂存区中的文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git ls-files -s
100644 504a7438c95afbd7f5280d756fb405bd85fbf19e 0 readme.txt
# 移动/重命名文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ mv readme.txt test.txt
# 查看工作目录中文件的状态
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm ..." to update what will be committed)
(use "git restore ..." to discard changes in working directory)
deleted: readme.txt
Untracked files:
(use "git add ..." to include in what will be committed)
test.txt
no changes added to commit (use "git add" and/or "git commit -a")
使用该命令后,去 .git/logs 下寻找当前分支对应的文件名,文件中的内容即为每一次提交的信息。
# 显示提交历史(它会列出所有历史记录,显示提交对象的哈希值,作者、提交日期、和提交说明)
$ git log
# 显示前n条提交历史
$ git log -n
# 只显示提交的 SHA1 值和提交信息
$ git log --oneline
# 显示两天前的提交历史
$ git log --since=2.days
# 指定作者
$ git log --author=mjx
# 指定关键字为“test”的所有提交
$ git log --grep=test
# 格式化输出模板
$ git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative
# 以列表方式查看指定文件的提交历史
$ git blame
使用案例:
# 只显示提交的 SHA1 值和提交信息
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --oneline
c9b83dc (HEAD -> master) 移动删除文件
158f2d9 tjsc
f4b81ca tj readme
2ca6bc4 第一次提交test.txt
# 显示提交注释中包含tj的历史
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --grep="tj" --oneline
2261741 (HEAD -> master) tj
158f2d9 tjsc
f4b81ca tj readme
# 格式输出提交历史
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative
* 2261741 - (HEAD -> master) tj (8 minutes ago)
* 4196d41 - 提交修改 (9 minutes ago)
* cc5b6e6 - 提交修改后的文件 (11 minutes ago)
* c9b83dc - 移动删除文件 (8 hours ago)
* 158f2d9 - tjsc (8 hours ago)
* f4b81ca - tj readme (8 hours ago)
* 2ca6bc4 - 第一次提交test.txt (11 hours ago)
# 以列表的形式显示提交记录
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git blame test.txt
cc5b6e6a (mjx 2021-07-24 21:45:52 +0800 1) readme v1
4196d419 (mjx 2021-07-24 21:47:38 +0800 2) test v2
22617412 (mjx 2021-07-24 21:49:28 +0800 3) blame 1
22617412 (mjx 2021-07-24 21:49:28 +0800 4) blame 1-1
几乎所有的版本控制系统都以某种形式支持分支,使用分支意味着你可以把你的工作从开发主线上分离出来,以免影响开发主线,在很多的版本控制系统中需要创建一个源代码的副本。对于大项目来说,这样的过程耗费很多时间。而Git的分支模型及其的高效轻量。
在git上创建一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
# 查看分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git branch
* master
详细解析:
每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支,使用git branch,输出的是master,它是一个主分支(默认)。当我们在提交的时候,指针也会随着提交往前走。
前面我们在介绍目录的时候,有一个叫HEAD的文件和一个refs的目录。
HEAD:存放的是一个具体的路径,也就是refs文件夹下的某个具体分支,意义:指向当前的工作分支。项目中的HEAD 是指向当前 commit 的引用,它具有唯一性,每个仓库中只有一个 HEAD。在每次提交时它都会自动向前移动到最新的commit;
refs的目录:目录存放了各个分支,所指向的commit对象的指针(引用),也就是对应的sha-1值。
当我们打开HEAD文件,里面的内容是ref: refs/heads/master,说明当前的分支是master主分支。
我们在进入refs/heads目录下,看到有个文件master,打开它,里面的有个哈希值(8707bd8b38058abbe339f03be91f4f0804a61369),查看后它是一个提交对象,也是最后一次的提交对象。
由此看来:分支就是一条提交对象串成的时间线,并且有一个活动的指针,指向当前最新的提交对象。
使用案例:git branch demo 命令创建一个demo分支
# 使用命令查看提交历史
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --oneline
8707bd8 (HEAD -> master) 第四次提交 mv readme文件
68c6120 第三次提交 readme.txt
ea4add4 修改test内容提交
3a003f5 第一次提交test
# 创建一个demo分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git branch demo
# 再次查看提交历史,我们发现(HEAD -> master, demo)里多了一个demo
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --oneline
8707bd8 (HEAD -> master, demo) 第四次提交 mv readme文件
68c6120 第三次提交 readme.txt
ea4add4 修改test内容提交
3a003f5 第一次提交test
创建好了一个demo分支,我们看到refs目录下也多出了一个文件,同时它里面也存在着和master同样的哈希值(8707bd8b38058abbe339f03be91f4f0804a61369),但是我们的HEAD文件中的内容依然是:ref: refs/heads/master,说明当前指针还在master主分支上。所以实际上分支,也是一个 commit 对象的引用。只是在GIT中专门有文件记录了分支名和指向。
当然还有git checkout -b /git switch -c 可以创建分支,这个更常用,我们甚至可以通过创建文件的方式,直接创建branch。
cd .git/refs/head/
echo '最新提交对象的哈希值' > 分支名
# 创建并切换到新分支
git switch -c dev
# 切换分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git checkout demo
Switched to branch 'demo'
D readme.txt
# (demo)切换成功
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (demo)
$
# 在demo分支上新建文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (demo)
$ echo "demo v1" > demo.txt
# 将demo.txt加入暂存区
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (demo)
$ git add ./
warning: LF will be replaced by CRLF in demo.txt.
The file will have its original line endings in your working directory
# 将demo.txt提交的仓库
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (demo)
$ git commit -m "我是demo分支的 demo.txt"
[demo 4acce70] 我是demo分支的 demo.txt
2 files changed, 1 insertion(+), 1 deletion(-)
create mode 100644 demo.txt
delete mode 100644 readme.txt
# 查看我的提交记录
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (demo)
$ git log --oneline
4acce70 (HEAD -> demo) 我是demo分支的 demo.txt
8707bd8 (master) 第四次提交 mv readme文件
68c6120 第三次提交 readme.txt
ea4add4 修改test内容提交
3a003f5 第一次提交test
详细解析:
当使用 git checkout 的时候, Git内部实际上就是把当前的HEAD指针给指向了另一个分支,而实际上也就是把 .git/HEAD 文件内容修改为切换的分支,而 .git/HEAD 内容指向的就是 .git/refs/heads中的分支,此文件内容又是一个 commit 对象的 sha-1值,所以也就间接指向了某个具体的commit对象了, 从这个commit对象可得到它的父级对象,依次类推,即可得到完整的代码。
当前分支示意图:
当我们的测试代码使用完成后,已经完成了他的使命,我们不需要它了。需要删除分支。可以使用:
git branch -D
git branch -d
# 切回主分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (demo)
$ git checkout master
Switched to branch 'master'
# 查看当前分支的提交历史,这里没有demo分支的提交记录,因为还没合并,不会影响主分支,但是demo.txt在仓库中还是存在的
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --oneline
8707bd8 (HEAD -> master) 第四次提交 mv readme文件
68c6120 第三次提交 readme.txt
ea4add4 修改test内容提交
3a003f5 第一次提交test
# 删除分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git branch -D demo
Deleted branch demo (was 4acce70).
# 查看分支列表,只剩主分支了
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git branch
* master
前面我们是默认在当前最新的提交对象上创建新的分支,如果我想要在更早的版本上创建分支呢?我们可以使用:git branch name commithash,在指定的提交对象上新建分支。
使用案例:
# 查看提交的历史记录
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --oneline --decorate --graph --all
* 8707bd8 (HEAD -> master) 第四次提交 mv readme文件
* 68c6120 第三次提交 readme.txt
* ea4add4 修改test内容提交
* 3a003f5 第一次提交test
# 指定提交对象创建分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git branch firstTime 3a003f5
# 切换到新的分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git checkout firstTime
Switched to branch 'firstTime'
# 查看当前分支下的内容
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (firstTime)
$ ls -l
total 1
-rw-r--r-- 1 Administrator 197121 9 Jul 25 00:15 test.txt
# 查看项目的分支历史
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (firstTime)
$ git log --oneline --decorate --graph --all
* 8707bd8 (master) 第四次提交 mv readme文件
* 68c6120 第三次提交 readme.txt
* ea4add4 修改test内容提交
* 3a003f5 (HEAD -> firstTime) 第一次提交test
切换分支时,会更改:HEAD、暂存区和工作目录。应尽量保持当前分支的工作目录是干净的。(没有未跟踪的文件,所有的文件应处于已提交状态)
# 初始化一个仓库
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (test)
$ git init
Initialized empty Git repository in D:/gitRepository/.git/
# 新建a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ echo "a" > a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ echo "b" > b.txt
# 将文件添加至暂存区
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git add ./
warning: LF will be replaced by CRLF in a.txt.
The file will have its original line endings in your working directory
warning: LF will be replaced by CRLF in b.txt.
The file will have its original line endings in your working directory
# 提交文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git commit -m "提交a b"
[master (root-commit) 3d641b3] 提交a b
2 files changed, 2 insertions(+)
create mode 100644 a.txt
create mode 100644 b.txt
# 创建分支(这个命令更常用,创建成功后并且切换到分支上)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git checkout -b test
Switched to a new branch 'test'
# 创建c.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (test)
$ echo "c" > c.txt
# 工作区中存在未跟踪的文件,直接切换到主分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (test)
$ git checkout master
Switched to branch 'master'
# 在主分支上还保存着test分支未跟踪的文件,这对git来说是很好的功能,保证用户的文件不应切换而丢失,但是这样可能会影响到主分支,所以建议切换分支时保存当前分支没有未跟踪的文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git status
On branch master
Untracked files:
(use "git add ..." to include in what will be committed)
c.txt
nothing added to commit but untracked files present (use "git add" to track)
详细解析:当我们在test分支上新增了一个文件,但是没有被git管理起来,就切换到主分支。在主分支上还保存着test分支未跟踪的文件,这对git来说是很好的功能,保证用户的文件不应切换而丢失,但是这样可能会影响到主分支,所以建议切换分支时保存当前分支没有未跟踪的文件。
因为git checkout 命令职责较多、不够明确,git 2.23 版本新增了switch命令则专门用来切换分支、创建并切换分支。
# 创建一个新的分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository ((3d641b3...))
$ git switch -c swtest
Switched to a new branch 'swtest'
# 查看提交历史,当前分支在swtest分支上
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (swtest)
$ git log --oneline
3d641b3 (HEAD -> swtest, test, master) 提交a b
在git中合并分支有三种方法,分别是merge,rebase,cherry-pick。
git-merge命令是用于将两个或两个以上的开发历史合并在一起的操作。可以使用git merge -h 命令查看相关参数。语法:
语法:git merge [
] [ ...] 选项:
--stat 会在合并结果的末端显示文件差异的状态。文件差异的状态也可以在git配置文件中的merge.stat配置
--log[=] 在合并提交时,除了含有分支名以外,还将含有最多n个被合并commit节点的日志信息。
--squash 压缩合并。将待合并的分支的内容压缩成一个新的提交合并进来。
--commit 参数使得合并后产生一个合并结果的commit节点。该参数可以覆盖--no-commit
--no-commit 参数使得合并后,为了防止合并失败并不自动提交,能够给使用者一个机会在提交前审视和修改合并结果。
-e, --edit 用于在成功合并、提交前调用编辑器来进一步编辑自动生成的合并信息。
--ff 是默认参数(指fast-forward命令),快速合并。如果合并过程中出现冲突,git会显示出冲突。如:git merge --ff branchName
--ff-only 只有能快速合并的情况下才合并。如果合并过程中出现冲突,git会自动about此次merge
--no-ff 不使用快速合并,会生成一次新的提交记录,这个记录只是标识这里进行了一次merge操作。
-s, --strategy用于指定合并的策略。默认情况如果没有指定该参数,git将按照下列情况采用默认的合并策略,合并节点只含有单个父节点时(如采用fast-forward模式时),采用recursive策略。合并节点含有多个父节点时(如采用no-fast-forward模式时),采用octopus策略 语法:git merge --abort
放弃本次merge操作。该命令仅仅在合并后导致冲突时才使用。
git merge --abort
将会抛弃合并过程并且尝试重建合并前的状态。但是,当合并开始时如果存在未commit的文件,git merge --abort
在某些情况下将无法重现合并前的状态。(建议使用git-stash
命令将这些未commit文件暂存起来)语法:git merge --continue 继续本次merge操作
在了解git-merge命令前,我们先了解一下Git 合并的策略,其中常见的是 Fast-forward、Recursive 、Ours、Theirs、Octopus。 Git 默认会帮你自动挑选合适的合并策略,如果你需要强制指定,使用git merge -s <策略名字>。
下面的策略作为了解,这里只简单介绍一下。
策略一:Fast-forward
Fast-forward是最简单的一种合并策略,适用于只有一个分叉时,采用这种方式。
如:(上图)合并 bugfix分支到master分支时,如果master分支的状态没有被更改过,这种合并不会产生冲突,只需要将master的指针往前移(如下图效果),这样的合并被称为fast-forward合并,即快进合并。
策略二:Recursive
Recursive 是 Git 分支合并策略中最重要也是最常用的策略,是 Git 在合并两个有分叉的分支时的默认行为。
其算法可以简单描述为:递归寻找路径最短的唯一共同祖先节点,然后以其为 base 节点进行递归三向合并,有冲突需要手动解决,然后生成新的提交并合并。
策略三:Ours
该参数将强迫冲突发生时,自动使用当前分支的版本。这种合并方式不会产生任何困扰情况,甚至git都不会去检查其他分支版本所包含的冲突内容这种方式会抛弃对方分支任何冲突内容。如:git merge -s ours bugfix,则会放弃bugfix(X、Y)的内容,然后产生一个新提交并合并,此时的E和D的内容是一致的。
策略四:theirs
和ours相反。完全抛弃掉当前分支的文件内容,直接采用对方分支的文件内容。git merge -s theirs bugfix,则会放弃C、D的提交内容,使用bugfix的内容,则E节点的内容与bugfix的内容完全一致。
策略五:Octopus
一般来说我们的合并节点都只有两个 parent,而这种合并策略可以做两个以上分支的合并,这也是 git merge 两个以上分支时的默认行为。比如在 dev1 分支上执行git merge dev2 dev3。
Fast-Forward Merge是git的各种合并策略中最容易理解、最简单的一种情况。
语法:git merge branchName
案例描述:当我们在做项目时,生产上有个紧急的bug需要修复。在master分支上新建两个文件a.txt,b.txt,然后创建bugfix分支,新建x.txt,y.txt,然后在将代码提交。
使用案例:
# 省略创建a.txt b.txt的过程,查看当前提交历史
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --oneline
f24ebdb (HEAD -> master) 第二次提交 b.txt
7b5880d 第一次提交 a.txt
# 创建并切换到bugfix分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git switch -c bugfix
Switched to a new branch 'bugfix'
# 省略创建x.txt y.txt的过程,查看当前提交历史
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (bugfix)
$ git log --oneline
ac4baf2 (HEAD -> bugfix) 第四次提交 y.txt
5b217e7 第三次提交 x.txt
f24ebdb (master) 第二次提交 b.txt
7b5880d 第一次提交 a.txt
# 切换到主分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (bugfix)
$ git switch master
Switched to branch 'master'
# 使用 git merge bugfix将bugfix合并到主分支上
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git merge bugfix
Updating f24ebdb..ac4baf2
Fast-forward
x.txt | 1 +
y.txt | 1 +
2 files changed, 2 insertions(+)
create mode 100644 x.txt
create mode 100644 y.txt
# 查看提交历史记录(依然是四个,如果想要生成新的提交节点,可以使用--no-ff选项)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --oneline
ac4baf2 (HEAD -> master, bugfix) 第四次提交 y.txt
5b217e7 第三次提交 x.txt
f24ebdb 第二次提交 b.txt
7b5880d 第一次提交 a.txt
# 删除bugfix分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git branch -d bugfix
Deleted branch bugfix (was ac4baf2).
# 查看提交历史
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --oneline
ac4baf2 (HEAD -> master) 第四次提交 y.txt
5b217e7 第三次提交 x.txt
f24ebdb 第二次提交 b.txt
7b5880d 第一次提交 a.txt
详细解析:
执行完git merge bugfix命令后,我们发现,在.git目录下多了一个文件,ORIG_HEAD文件,我们看下这个文件的内容:
这里正是记录的master分支合并前的最后一次的提交对象,记录这个是为了做回滚操作。我们从合并后的提示信息中也能看出:Updating f24ebdb..ac4baf2,master分支的指针从f24ebdb提交对象更新成ac4baf2,Git只是简单地把master的head指针向前移动,合并就完成了,这就是所谓的Fast-Forward Merge。因为不涉及内容变更的比较,所以这种合并方式效率很高。不过它有个缺点,作为被合并的bugfix分支,它的提交历史会和master分支重合。
如果我们想在合并后保留来自被合并分支的提交历史,并显式标注出合并发生的位置,那就需要在执行合并时加上参数--no-ff
。当然,这样也表示我们在合并时将不使用Fast-Forward Merge策略。
首先我们执行:git reset --hard f24ebdb 回退到合并前的状态;
再执行:git merge --no-ff -m "第五次提交 合并" bugfix
两个分支的合并,git采用recursive(递归) 策略合并,也叫three-Way Merge(三方合并)。
什么是三方合并?git为什么采用三方合并?
假定,我们在master分支上,有个A提交节点,我们在A的基础上创建了一个dev开发分支,并且产生了一个B提交节点,但是,这个时候其他人往master主分支上又提交了一次,产生了一个A1提交节点。
Two-Way Merge:做法是用diff工具生成A1与A的差异,然后在B上面应用这个patch,期望得到B1,用数学的公式表示就是:B1= B + (A1 - A) ,虽然这种做法也可以,工具在帮我们做合并时,只知道两个文件在同一行上有差异,却没办法知道在合并后的版本里,到底该保留谁的版本,所以只能交给用户自己手工来决定。
Recusive Three-Way Merge:使用三方合并的方式,Git最大的不同在于它记录了文件的提交历史,因此可以向前回溯文件修改前的“原件”。用数学的公式表示就是:B'1= merge(A, A1, B)。它在合并时不仅会看两人各自的文件内容,还会看之前的原件。通过和原来版本的对比,就可以清楚地知道应该保留谁。
例如:A中(a.txt)文件的第一行内容:(my name is a)
A1中(a.txt)文件的第一行内容:(my name is a)
B中(a.txt)文件的第一行内容:(my name is b)
使用Two-Way Merge方式的合并,工具就需要我们自己确定保留谁。使用Recusive Three-Way Merge,会找到A1和B的最近的根节点A,因为A和A1中并没有修改第一行的内容,但是B修改了第一行的内容,这时工具就能确定我们需要保留B中的第一行,合并后的节点第一行的内容就是:(my name is b),对于这种情况,就无须我们手工确认。
前面我们的合并,都是没有冲突的合并,在项目中,不同的需求可能改到同一个文件,甚至是同一个文件的同一行代码,这时合并时就会产生冲突。
案例描述:新建一个a.txt原始版本提交,修改后a.txt作为主分支的A版本提交,然后创建B分支,再对a.txt文件进行修改提交,然后切回主分支,在主分支中也对a.txt进行修改提交,最后再合并版本。
使用案例:
# 初始化仓库
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git init
Initialized empty Git repository in D:/gitRepository/.git/
# 新建a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ vim a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ cat a.txt
1111
2222
3333
4444
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git add ./
warning: LF will be replaced by CRLF in a.txt.
The file will have its original line endings in your working directory
# 提交到数据仓库
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git commit a.txt -m "第一次提交原始版本文件"
warning: LF will be replaced by CRLF in a.txt.
The file will have its original line endings in your working directory
[master (root-commit) b4219d7] 第一次提交原始版本文件
1 file changed, 4 insertions(+)
create mode 100644 a.txt
# 修改a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ vim a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ cat a.txt
1111
2222
3333
4444
5555
# 提交A的第一个版本(公共父节点)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git commit -a -m "提交A的第一个版本1111-5555"
warning: LF will be replaced by CRLF in a.txt.
The file will have its original line endings in your working directory
[master 2292ec6] 提交A的第一个版本1111-5555
1 file changed, 1 insertion(+)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git switch -c b
Switched to a new branch 'b'
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b)
$ vim a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b)
$ git commit -a -m "提交分支b 对a.txt的修改"
warning: LF will be replaced by CRLF in a.txt.
The file will have its original line endings in your working directory
[b a3fa244] 提交分支b 对a.txt的修改
1 file changed, 2 insertions(+), 1 deletion(-)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b)
$ cat a.txt
1111
22222222
3333
4444
5555
bbbb
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b)
$ git switch master
Switched to branch 'master'
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ vim a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ cat a.txt
aaaa
1111
2222aaaa
3333
aaaa
4444
5555
# 提交对a.txt的修改
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git commit -a -m "提交主分支对a.txt的修改"
[master fdffea4] 提交主分支对a.txt的修改
1 file changed, 3 insertions(+), 1 deletion(-)
# 合并时出现冲突,自动合并失败
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git merge -m "合并分支" b
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
Automatic merge failed; fix conflicts and then commit the result.
# 此时暂存区有三个版本的文件(三方合并)
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master|MERGING)
$ git ls-files -s
100644 acaaa8e6154f09812fb3d5d7b0281a9bdc6f5be4 1 a.txt
100644 d205cd2a67851342ee7fb5e88c385f81f1d8218f 2 a.txt
100644 a407972f81076265f6ec65f3fa90e5c851be8564 3 a.txt
上述过程已经到合并阶段,但是合并出现冲突。需要我们手动编辑文件解决冲突。
# 此时a.txt的文件因为产生冲突,出现如下样子
aaaa
1111
<<<<<<< HEAD
2222aaaa
=======
22222222
>>>>>>> b
3333
aaaa
4444
5555
bbbb
<<<<<<<
表示当前分支冲突开头
>>>>>>>
表示被合并的分支冲突结尾
=======
分隔冲突的不同
我们修改冲突文件,保留我们需要的:
# 编辑冲突文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master|MERGING)
$ vim a.txt
# 最后保留的
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master|MERGING)
$ cat a.txt
aaaa
1111
22222222
3333
aaaa
4444
5555
bbbb
# 将修改加到暂存区
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master|MERGING)
$ git add a.txt
# 此时暂存区就一个最终版的文件了
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master|MERGING)
$ git ls-files -s
100644 2348831345a1810c9ce26bdd9996e6b9d6997a72 0 a.txt
# 将合并的文件提交,生成新的节点
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master|MERGING)
$ git commit -m "提交解决冲突的版本"
[master b8ab617] 提交解决冲突的版本
最终效果:
这就是三方合并。合并后的对象b8ab617对应着两个父提交对象:2292ec6和a3fa244。
--squash参数的使用案例:
--squash参数会把被合并分支上的所有变更“压缩(squash)”成一个提交,追加到当前分支的后面,作为“合并提交”(merge commit)。从参与合并的文件变更上来说,Squash Merge和普通Merge唯一的区别就是:对于普通的Merge而言,在当前分支上的合并提交通常会有两个parent;而Squash Merge却只有一个。
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git reset --hard fdffea4
HEAD is now at fdffea4 提交主分支对a.txt的修改
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git merge --squash b
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
Squash commit -- not updating HEAD
Automatic merge failed; fix conflicts and then commit the result.
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ vim a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git add a.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git commit -m "修改冲突后提交"
[master 53bebc3] 修改冲突后提交
1 file changed, 2 insertions(+), 1 deletion(-)
最终效果:
git rebase(变基),就是改变一次提交记录的base。可以用来合并分支,也可以用来合并多次提交。rebase 实际上就是取出一系列的提交记录,复制它们,在另外一个地方逐个的接进去。rebase的优势就是可以创造更线性的提交历史。
git rebase和git merge的区别:
git merge 保留真实的用户提交记录,且在merge时会生成一个新的提交。(它是一种非破坏性的操作,现有分支不会以任何方式被更改,但是会产生新的节点)
而git rebase 会改写历史提交记录(提交对象都会发生变化),但是他也可以合并一些提交节点。
注:不要对任何已经提交到公共/远程仓库中的commit进行(git rebase)。
案例描述:
我们在b分支上快乐的开发着,结果项目经理说已经更新了主分支,并且在我们之后提交了两个已经上线的版本了,这意味着我们的代码a.txt已经不是最新的啦。
1)如果前面合并了的话,可以使用git reset --hard fdffea4 重置我们的分支,到如下状态:
2)在主分支上再新增一个文件c.txt(cccc三行)
3)再切换到b分支,新建一个b.txt(bbb三行)
4)最终形成如下效果(master分支:a.txt、c.txt;b分支:a.txt和b.txt)
项目经理说,有两个紧急需求已经上线了,叫改到a/c文件的同事需要注意一下。于是我们要合并一下代码再测试一下,已经上线的功能加进来是否功能依然正常。于是我们的基版代码则最好是以(8d20dde)新的基点,操作如下:
# 当前b分支 只有a.txt b.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b)
$ ls
a.txt b.txt
# 在b分支上执行git rebase master合并(调基,把master当前基点之后的内容全补丁进来),但是发生冲突
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b)
$ git rebase master
error: could not apply a3fa244... 提交分支b 对a.txt的修改
Resolve all conflicts manually, mark them as resolved with
"git add/rm ", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply a3fa244... 提交分支b 对a.txt的修改
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
# 解决一下冲突
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b|REBASE 1/2)
$ vim a.txt
# 再将合并好的没有冲突的代码加入暂存区
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b|REBASE 1/2)
$ git add a.txt
# git rebase --continue git会继续应用余下的补丁,并且完成了
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b|REBASE 1/2)
$ git rebase --continue
[detached HEAD b01c4ff] 提交分支b 对a.txt的修改
1 file changed, 2 insertions(+), 1 deletion(-)
Successfully rebased and updated refs/heads/b.
详细解析: git rebase master,会将会将本地当前分支里的每个提交(commit)取消掉,然后把将本地当前分支更新为master最新的提交(8d20dde)作为基点,然后逐个的复制b分支中的节点到8d20dde(相当于为各个原提交做了个副本,它们拥有相同的修改集、同一个作者、日期以及注释信息),但是b分支中的原提交对象都发生了变化(如:原b.txt的提交对象16a888b,变基后A920a12)。变基后的形式就会到了,最初的git merge-Fast-forward的形式。于是:
# 切换到master分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (b)
$ git switch master
Switched to branch 'master'
# git merge合并分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git merge b
Updating 8d20dde..a920a12
Fast-forward
a.txt | 3 ++-
b.txt | 3 +++
2 files changed, 5 insertions(+), 1 deletion(-)
create mode 100644 b.txt
另外有几个参数可以了解一下:
git rebase --abort :取消当前变基操作
git rebase --onto :可以将一个分支移植到另外一个分支;
git rebase --interactive (缩写-i):变基操作会进入交互状态
例如:
我对第一个提交使用reword,当我执行变基时,弹出交互式,让我修改提交信息。
git rebase 另一个作用就是合并提交,常常一个功能的开发,修修补补 commit 了 n 多次,如果像我这样的强迫症,干不掉它,看着也会很烦。这里的合并,其实可以把它理解成是 "重新设置基线" ,为你的当前分支重新设置开始点。
git rebase -i HEAD~n,git rebase –i ,这里的 "-i" 是指交互模式,HEAD~2当前分支最近n次提交。就是说你可以干预rebase这个事务的过程。
# 删除之前合并的b分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git branch -d b
Deleted branch b (was 134bda7).
# 查看当前的提交历史
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git log --oneline --graph --all
* 134bda7 (HEAD -> master) 提交b.txt文件
* 1c03b47 提交分支b 对a.txt的修改
* 8d20dde 提交c.txt文件
* fdffea4 提交主分支对a.txt的修改
* 2292ec6 提交A的第一个版本1111-5555
* b4219d7 第一次提交原始版本文件
使用案例:准备好了上述操作之后,我们以fdffea4提交对象为基点,合并掉8d20dde、134bda7和1c03b47提交对象。
执行命令:git rebase -i HEAD~3
弹窗:我们不能修改最近的一次pick,因为我们要合并成一个提交
保存后,我们把我们最终合并的第一条注释修改掉,因为在下文中我们也可以看到此次的操作就是,新增b.txt和c.txt,并且修改a.txt,保存后,即执行成功。
查看合并后的情况:
使用 cherry-pick,可以从其他分支复制指定的提交,然后导入到现在的分支。
案例描述:在如下情况下,新建一个分支dev,然后新建d.txt文件,提交的数据仓库。发现我们忘记切换分支了,为此我们需要把在dev分支上新建的内容,复制到master分支上,这时我们可以使用git cherry-pick命令。
使用案例:
# 新建分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git switch -c dev
Switched to a new branch 'dev'
# 新建文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (dev)
$ echo "b.txt v1" > d.txt
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (dev)
$ git add d.txt
warning: LF will be replaced by CRLF in d.txt.
The file will have its original line endings in your working directory
# 提交到仓库
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (dev)
$ git commit -m "提交d.txt"
[dev 53e5c0f] 提交d.txt
1 file changed, 1 insertion(+)
create mode 100644 d.txt
# 此时git仓库的提交历史
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (dev)
$ git log --oneline --graph --all
* 53e5c0f (HEAD -> dev) 提交d.txt
* 1aef9c6 (master) 提交b.txt c.txt文件 并且修改a.txt
* fdffea4 提交主分支对a.txt的修改
* 2292ec6 提交A的第一个版本1111-5555
* b4219d7 第一次提交原始版本文件
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (dev)
$ git switch master
Switched to branch 'master'
# 在master分支上,将制定的提交对象复制到当前分支
Administrator@EA424S6AH1D5MQG MINGW64 /d/gitRepository (master)
$ git cherry-pick 53e5c0f
[master c18cba9] 提交d.txt
Date: Fri Jul 30 22:41:47 2021 +0800
1 file changed, 1 insertion(+)
create mode 100644 d.txt
前面我们了解到在git本地仓库中有三大区:工作区、暂存区、本地仓库。当我们有操作失误时,想要撤销相应区域的操作,我们可以这么做呢?
使用案例:我们创建revert.txt(revert v1)版本进行第一次提交,修改revert.txt,新增一行revert v2在提交形成如下效果后,再还原到第一个版本。
git reset用于重置我们的提交,安装不同的命令选项,会对工作区、暂存区有不同的影响。git revert还原版本,会在当前版本上重新生成一个提交节点,而git reset会丢弃之前的提交历史,重置我们的HEAD。
使用案例:接着上面的案例,我们在开发中,结果不小心提交了一个测试文件a.txt,我们只使用,并不希望将它提交到仓库,使用重置命令,将主节点重置到之前的节点。
案例分析:提交a.txt 后,我们将其还原,并保留暂存区和工作区中的a.txt使用--soft 选项。如果不想保留的话暂存区的话:默认会是--mixed,如果都不想保留的话,则使用--hard 选项。
使用案例:1)将之前的案例还原一下,修改a.txt,再还原工作区的修改
2)新建一个b.txt,使用 git restore --staged,将未在仓库,但是在暂存区的文件撤出暂存区
1)git rm --cached file的使用
2)git checkout --
将文件从暂存区(有的话,没有的话找版本库)中检出。案例还原工作区revert.txt。
3)git commit --amend 替换上一次提交(一般用于修改提交备注)
使用案例:我们提交b.txt删除a.txt,但是发现提交备注少写了,使用git commit --amend覆盖最近一次的提交。
在Git 中,标签与分支类似,都是指向某一个commit 提交的引用或指针。不同的是,分支可以移动,而标签不能移动。git有commit 为什么还要引入tag标签?
其实这跟我们记百度网址一样,我们记住的是百度的域名(www.baidu.com),而不是他的IP,DNS会帮我们转换成对应的IP地址。同样commit对象是一串哈希值,太长杂也不好记,于是tag就来了。如果你希望记住某个版本的话,你可以给这个版本取个tag名(你可以认为是commit的别名)。
1)git tag tagName -m "message" 或者 git tag -a tagName -m "message"
详细解析:-a 参数的意思是强制需要加注释附注信息,所以其实git tag tagName -m和git tag -a tagName -m是等效的。
2)git tag tagName
为当前提交对象创建标签,可以不写附注信息
1)git tag 默认按字母排序显示标签列表
2)git tag -l 模糊匹配查找标签
1)git tag -d tagName
在git中还有一些细节,我们需要了解一下。
验证数据库中对象的连通性和有效性。
案例描述:1)初始化仓库;2)新建一个test.txt(内容:test v1),并且add到暂存区;2)修改test.txt,并且add到暂存区;3)再次修改test.txt,并ad到暂存区,并且查看暂存区的test.txt的blob对象为:4827c00c91ead263ef2b7cab9d5c6405828e7444
4)将test.txt提交到仓库;
5)查看仓库中的文件,一次提交有一个blob对象、一个tree对象和一个commit对象;剩下两个就是我们前两次add多出来的垃圾对象。
6)使用git fsck 查看
从对象数据库中删除所有不可达的对象。
git prune 有两个选项,-n (不要删除任何东西; 只是报告它会删除什么);-v(报告所有移除的对象)
在仓库中打包/解包的对象。
使用案例:
1)先添加一些内容,并把他们提交到仓库。并添加一个标签。
2)执行 git repack -d -l
将所有未被包含在一个pack的松散对象连结成一个pack,如果有某些现有包冗余,则移除冗余包。
3)我们发现打包之后,.git/objects目录下的文件只剩下info目录和pack目录了,那些blob对象、tree对象和commit对象都没有了。
4)使用git verify-pack -v命令查看压缩后的文件内容,发现之前的之前的一些对象都保存在这个文件里面了。并且大小从之前的12M变成了7.7M
5)解压缩 git unpack-objects < ***.pack
使用一个标准输入去解压一个压缩文件,第一次为什么没有成功呢?因为pack文件存储的压缩对象本身就存在objects目录下,为此要解压一个文件,我们需要把他移出当前目录,在进行解压。
管理reflog信息。当想恢复到之前的新版本,有没有设置标签,也不记得之前的commit对象怎么办?那么你可以使用git reflog,它记录了你的每一次命令。
注:git log可以查看提交历史
git reflog查看命令历史
清理不必要的文件并优化本地存储库。
git的底层并没有采用如CVS、SVN 底层所采用的那套增量式文件系统,当文件发生变化时,提交上去的是不是差异信息,而是文件的快照,即整个文件的内容,并保存指向快照的索引。这种做法,提高 Git 分支的使用效率;但也容易导致仓库中内容重复程度过高,从而仓库体积过大。遇到这种情况时,就需要Git中的gc(garbage collect)功能。
Git会收集所有松散对象并将它们存入 packfile,合并这些 packfile 进一个大的 packfile,然后将不被任何 commit 引用并且已存在一段时间 (数月) 的对象删除。 此外,Git还会将所有引用 (references) 并入一个单独文件。
其指向过程如下:
使用案例:查看.git/objects/目录下的文件,在使用git gc 命令,它不仅会帮我们把objets命令下文件打包,refs目录下的文件也会一起打包。
前面我们学习的都是在本地操作一个git的版本库。接下来我们了解一下远程仓库GitHub的使用。
前面我们也介绍过了Git和GitHub的历史渊源,Git 是一款免费、开源的分布式版本控制系统,而 GitHub主要提供基于 git 的版本托管服务。如今,GitHub已经成为了全球最大的开源社区了,很多全球的顶级科技公司纷纷加入 GitHub,许多顶级开源项目都优先选择在 GitHub 上开源,如:Linux、Nodejs、Ruby等,Vue也曾在GitHub上被大家发现。在GitHub上不仅学习优秀的开源项目,也可以多人协作开发项目,搭建博客和个人网站等。
但当我想学习如何使用GitHub时,发现很多的文章,居然无从下手。使用远程仓库的作用:备份和实现代码共享集中化管理。
首先去GitHub官网注册一个属于你的账号,这里就不详细的描述注册的过程了,准备一个可以使用的邮箱账号就行。
注册好账号后:我们的Github主页就是这个样子的。下面会对主页一些内容进行简单的介绍。
1)相关概念:
拉取请求(Pull Request):给项目提出请求(包括自己的提交情况,或者别人提交给你的请求)。例如你发现一个项目很有意思,于是你把项目fork过来,然后你发现项目中有些bug,你修改了之后请求去合并你的修改,就是你给别人提交你的请求,如果被审核通过并正式合并,这样你就为该项目做贡献了。
问题(Issue):评论/话题,对项目提出各种讨论,可以增加开发者和使用者的交流。(你给别人提的问题或者别人给你提的问题,比如 Bug、Build、help、wanted等,有各种问题都可以提)。比如,你开源了一个仓库,别人看见了,或者你的团队朋友看见了,发现一个bug!就点击Issues,新建一个Issues,告诉你问题,然后设置一个标签,标明是bug还是warning还是文档写错啦。
市场(Marketplace):里面有一些工具,比如持续集成,代码检视等,商用的话大部分需要花钱。
探索(Explore):Explore下面有很多东西。Explore下面的explore一栏里面github会给我推荐一些项目,topics是比较热门的项目,github也给我们进行了归类,treading中是一些热度上升较快的项目,collections对项目进行了归类,类似topics,但是不热门的项目你在这边也可以找到,events是一些GitHub的新闻,GitHub Sponsors是一些赞助商和代码爱好者介绍。
2)更多功能(加号里的内容):
新建仓库(New repository):一个github账号可以创建多个仓库(repository),可以保存多个代码工程和项目的代码,资源,文本、图片......等。(官方定义:仓库就像项目的文件夹。 项目的仓库包含项目的所有文件,并存储每个文件的修订记录。 您也可以在仓库中讨论并管理项目的工作。您可以个人拥有仓库,也可以与组织中的其他人共享仓库的所有权。)
导入仓库(Import repository):导入一个已经存在的仓库。
新建要点(New gist):用于共享代码段,注释,任务列表等(单个文件)。
新建组织(New organization):一个github账号可以创建多个组织(organization),一个组织可以创建多个仓库(repository),一个仓库(repository)也可以对应多个组织(organization);组织(organization)还可以创建团队(Team)创建团队的当前账号,在Team内的role是Maintainer,还可以邀请别的人进入这个team,默认将Role设置为Member。
新建项目板(New project):Github Projects 就是一个个看板,可以把 Issues 加入看板中,方便管理它们的进程。项目板也可用于创建满足您需求的自定义工作流程,例如对特定功能工作、全面路线图甚至发布检查清单进行跟踪和排列优先级。
3) 个人设置
注:这里的个人资料,也就是我们的个人主页
我们可以在GitHub 仓库中存储各种项目,下面我们就来介绍一下如何在GitHub中创建一个仓库。
首先我们点击页面的右上角更多功能(+下拉菜单中)选择New repository(新建仓库)。就会进入到如下界面:
第一步:右上角,使用 下拉菜单选择 New repository(新建仓库)
第二步:为仓库取个简短的名字。"hello-world"
第三步:(可选)添加仓库的说明。
第四步:选择仓库可见性。
第五步:选择初始化仓库需要一并创建的内容(使用自述文件初始化此仓库)。此时会出现:This will set main as the default branch. Change the default name in your settings.意思是默认的主分支叫main,当然我们可以重设主分支名字。
第六步:点击创建(Create repository)。
创建仓库后,会进入到我们的仓库主页,界面如下:
在仓库主页,我们看到很多的内容,下面就对其中一些内容做简单的介绍:
1)Watch、Star、Fork
关注(Watch):代表有多少人关注了该项目,当项目更新时,关注的人会收到邮件通知。
收藏(Star):代表有多少人收藏了该项目,方便他们参考代码。
分叉/克隆(Fork):Fork字面意思“叉”,就是把整个项目源代码,复制到自己的代码仓中。复分叉的越多,也说明代码的价值被认可度越高。
注:关注数、收藏数、分叉数,代表了大家对该项目的热度和项目认可度。
2)仓库文件列表(Code)
main、branch、tags:项目的分支和标签信息
go to file:跳转到文件搜索页面
add File:添加或上传文件
code:下载或克隆项目
about:可以修改项目描述信息
Releases:可以创建一个发行版来打包软件,以及发行说明和二进制文件链接,供其他人使用。类似于一些项目提供多个版本下载及长期更新升级服务。
Packages:GitHub Packages 是一种包托管服务,与 GitHub 完全集成。 GitHub Packages 将您的源代码和软件包组合在一起,以提供集成的权限管理和计费,使您能够在 GitHub 上专注于软件开发。(例如 npm、RubyGems、Apache Maven、Gradle、Docker 和 Nuget)
3)持续集成服务(Actions)
GItHub 推出的一个 CI\CD 服务,即持续集成服务,有 GItHub Action 以后,能做的事情就更多,比如在 master 分支上提交了一段代码, GitHub Action 可以自动部署到指定的服务器上去,还可以把代码做成镜像,将镜像自动提交到镜像仓库里。
4)维基(Wiki)
可以将仓库文档托管在 wiki 中,以便其他人使用和参与您的项目。一般为:
5)安全(Security)
GitHub 允许客户在 Advanced Security 许可下使用额外的安全功能。
6)洞察统计(Insights)
包括项目的总体浏览相关统计信息,例如:看用户在相应日期发送提交、添加代码、删除代码的大致数量,服务端的代码仓库克隆情况等信息。
7)设置(Settings)
对仓库进行任何设置。如:
我们使用网页版,去操作Github的仓库,添加文件有两种方式,一是:Create new File,二是:Upload files。
添加文件方式一:Create new File-新建文件
添加文件方式二:Upload files-上传文件
当然我们也可以添加目录及文件:
在code文件列表中点击文件名,即可进入文件详情页面,修改文件内容。
查看提交信息:
点击Go to file,即可进入搜索页面
然后可以在Your stars查看你的所有收藏:
使用案例:我们用另外一个账号,创建了一个hello-world-hls仓库,信息如下:
第一步:搜索hello-world-hls这个仓库,并Fork这个仓库,当前仓库Fork了hello-world-hls仓库后会自己生成一个一模一样的仓库,然后我们对项目中的文件进行修改,并提交。
提交后:
第二步:我修改hello-world-hls这个仓库的文件,希望能更新到这个仓库,这时候就需要使用Pull requests了。
第三步:我们在hlsol/hello-world-hls仓库中看到:
第四步:查看修改并接收合并
第五步:查看项目,已经和mjxol/hello-world-hls项目同步了
即使我们没有注册github账号,我们也可以下载源码,然后查看源码。点击Download ZIP。
GitHub为我们提供了一个管理远程仓库的应用程序,可以在https://desktop.github.com/下载。下载完成后登录:
登录之后,主页如下:
第一步:点击Create a New Repository,创建一个本地仓库。
第二步:在本地仓库中新增两个文件abc.txt和gitDemo.txt
第三步:将新增的两个文件提交到本地仓库。
第四步:修改abc.txt,并提交到本地仓库。
第五步:取消默认勾选的私有选项,点击Publish repository上传到远程仓库。(等待发布成功,过程可能会有点慢)
第一步:本地新增一个文件def.txt,并提交到本地仓库。现在显示本地没有改变,但是上面push origin显示了1,代表的是我们与远程的github不同步,本地有一个更新。
第二步:将更新推送到远程仓库push origin。
第一步:我在github网页上直接添加一个other.txt并提交,这时我们的本地仓库和远程仓库就不同步了。
第二步:我们点击Fetch origin,出现Pull origin有个下载1,说明我们和远程仓库不同步了。点击Pull origin,就可以把远程的difficult更新到本地了。
假如我不小心把本地的def.txt文件误删了,并且还同步到远程仓库了。我们可以右击提交记录选择Revert到提交前的版本。
然后再点击Push origin将被误删的def.txt再次提交到远程仓库
第一步:创建一个dev分支,并添加cc.txt文件
切换到主分支(main),记住永远都是把其他分支merge到当前。
这时主分支上就多了一个commit,就是我们从dev分支合并过来的
第一步:在github远程仓库中,点击Open with Github Desktop
第二步:在Github Desktop上显示如下,我们直接点击Clone
最后我们可以看到克隆成功:
1)使用https方式下载
在非登陆的情况下我们同样也可以使用命令下载源码到本地。
第一步:复制链接https://github.com/vuejs/vue.git(源码网址后多了个.git)
第二步:在git命令行工具中输入: git clone https://github.com/vuejs/vue.git
第四步:如果出现这种情况,别慌,一般是这是因为服务器的SSL证书没有经过第三方机构的签署,所以才报错。
第五步:输入: git config --global http.sslVerify "false",即可。
1)使用SSH方式下载
SSH方式下载传输效率更高。不过这种方式下载需要注册。使用SSH的原理就是密钥登陆(也就是所谓的免密登陆)
如果希望了解SSH密钥登陆,可以看我的这篇文章: https://blog.csdn.net/xiaoxianer321/article/details/109625658
第一步:首先我们在客户端,也就是git命令行工具上,执行如下命令,会生成一个私钥和一个公钥:
ssh-keygen -t rsa -C "github email"
第二步:复制公钥,点击Add SSH Key,填写密码验证。
第三步:验证是否配置成功 ssh -T -v [email protected],已经认证成功。
第四步:复制链接[email protected]:vuejs/vue.git 第五步:克隆仓库 git clone [email protected]:vuejs/vue.git
使用命令:git remote add
注:shortname 为url的别名,如使用:origin,添加新的可以使用其他名字
使用命令:git remote -v 查看远程仓库信息(显示的是需要读写远程仓库使用的 Git 保存的简写与其对应的 URL)
git remote show
详细解析:
命令展示的内容:远程仓库
Fetch 对应的URL:[email protected]:mjxol/hello-world.git
Push 对应的URL:[email protected]:mjxol/hello-world.git
HEAD 指向分支:main
远程分支:main 分支追踪状态
为“git pull”配置的本地分支:main与远程main合并
为“git推送”配置的本地引用:本地main推送远程main(最新)
使用命令:git remote remove
git remote rename
前面呢我们已经会使用git命令去克隆/关联一个远程仓库。接下来我们将会接触到几个新的命令,git fetch 从远程仓库抓取、git push 推送至远程仓库、git pull 拉取(自动抓取后合并该远程分支到当前分支),可以说:git pull = git fetch + git merge FETCH_HEAD(可能会产生冲突,需要手动解决) ,用一张图来表示就是:
git fetch
git fetch
抓取远程仓库中的内容到本地仓库。会抓取克隆(或上一次抓取)后新推送的所有工作。
注:git fetch
命令只会将数据下载到你的本地仓库——它并不会自动合并或修改你当前的工作。如果需要指定分支,使用:git fetch
那他是怎么工作的呢?删除之前的仓库,我们重新开始操作一个案例:
第一步:克隆仓库到本地,此时本地和远程仓库是一致的。
前面我们也讲过,克隆项目其实下载下来的是一个压缩包。 查看本地仓库main主分支的最新对象(cat .git/refs/heads/main),并查看下载下来的项目的最新的提交对象(cat .git/packed-refs)
第二步:在远程仓库上新增一个文件,并且远程仓库最新的提交对象已经是9b76bef了
第三步:使用git fetch命令会抓取全部更新到本地仓库(全部更新:本地克隆(或上一次抓取)后新推送的所有工作)
我们可以看到:最后一行显示,最新的对象已从 4f208f4到9b76bef了。
第四步:使用git log -p FETCH_HEAD命令查看刚取回的更新信息
同样我们可以在本地仓库的文件中找到依据:
详细解析:
本地仓库中的main分支最新的commit对象还是没有变,而本地仓库中记录的远程仓库中的commit对象已经更新。即origin/main分支中的内容已经更新,我们需要把远程下载下来的代码合并到本地仓库。
第五步:合并远程下载到本地仓库,git merge origin [branchName]命令,本地工作目录
案例:我们在下面这样一个远程仓库中,包含一个dev分支、一个main分支。
且当前工作目录与远程仓库一致:
第一步:在远程仓库dev,分支中新增一个文件。
第二步: 在main分支上执行git pull
详细解析:在main分支上执行git pull,查看日志,虽然发现dev分支的第二次提交已经更新到本次数据库了。却没有帮我们合并,不是说git pull=git fetch + git merge?,这是什么情况?
第三步:我们查看本地仓库的.git/FETCH_HEAD文件
详细解析:
FETCH_HEAD指的是: 某个branch在服务器上的最新状态。每一个
执行过fetch操作的项目都会存在一个FETCH_HEAD列表,其中每一行对应于远程服务器的一个分支。
当前分支指向的FETCH_HEAD, 就是这个文件第一行对应的那个分支。
其状态,若都是 not-for-merge
则不会有接下来的merge动作,这也是为什么没有在main分支下,执行git pull下来的dev分支不会被自动合并。在当前分支执行git fetch之后,FETCH_HEAD则会更新对应分支的状态。
第四步:切换到dev分支,再执行一次git pull -v (-v参数是打印出详细信息)
我们看到执行了一次merge(Fast-Forward)
第一步:新增一个文件,并提交到本地仓库
第二步:执行git push
这样,就推送到了我们的远程仓库。
最后附上一张总结的思维导图:
希望在后续学习中对Git及GitHub有更深一步的了解。下一篇文章,将会继续讲解如何在GitHub上搭建一个个人博客。