作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。
目录一.版本控制系统VCS
1.为什么使用VCS
版本控制系统(Version Control System,VCS):
是一种软件,可以帮助软件团队的开发人员协同工作,并存档他们工作的完整历史记录。
在实际开发过程中,经常会有这种需求或问题:
- 代码可能被破坏,比如误删除等,希望还能找回;
- 代码出现严重的Bug,希望回滚至数周前的旧代码;
- 需要在已经发布的程序中添加新的功能,如果测试验证后没有问题,就会使用新的代码,而在测试验证期间,不能影响原来的代码;
- 同一个软件需要由多个版本并发开发,满足不同的应用需求;
- 实际项目开发基本都是多个人合作完成,在多个人写代码时,就牵扯到合并成一份的问题;
2.版本控制系统分类
2.1 本地版本控制系统
第一代版本控制系统被称为本地版本控制系统。通过加锁将并发执行转换顺序执行。一次只能有一个人处理文件。
具体流程如下:
- 首先,应该把文件放在一个服务器上,方便使用上传或下载文件;
- 其次,任何人相对文件修改时,需要现将这个文件加锁,通过checkout指令,使得其他人无法修改;
- 最后,当修改完成后,需要释放锁,通过checkin指令,形成一个新的版本,存放到服务器端;
第一代版本控制系统主要有RCS,SCCS(1972年发布)和DSEE(被认为是Atria ClearCase的前身)。目前,有些项目还在使用。
悲观锁和乐观锁:
- 悲观锁:
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。
由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。
- 乐观锁:
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。
如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则数据进行更新。
由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。乐观锁一般会使用版本号机制或CAS算法实现。
2.2 集中版本控制系统
用户想要完成任何的提交和回滚都依赖与链接集中的代码服务器才能实现,比如下班后回家,如果无法链接至代码服务器,将无法提交代码。
换句话说,集中版本控制系统不支持离线开发。
此外,集中版本控制系统还存在单点故障问题,需要运维人员给出高可用解决方案。
2.3 分布式版本控制系统
在每个用户都有一个完整的服务器,然后再部署一个中央服务器。
用户可以先将代码提交到本地,没有网络也可以先提交到本地,然后在有网络的时候再提交到重要服务器,这样就大大方便了开发者相比于集中式的版本控制系统,工作的时候需要先从中央服务器后去最新的代码,改完之后需要提交,如果是一个比较打的文件则需要足够快的网络才能快速提交完成,而使用分布式的版本控制系统,每个用户都是一个完整的版本库,即使没有中央服务器也可以提交代码或者回滚,最终再把改好的代码提交至中央服务器进行合并即可。
3.常见的版本控制系统
3.1 CVS(Concurrent Version System)集中式版本控制系统
并发版本系统(Concurrent Version System,简称CVS)是初代的第二个版本控制系统。大约十年间,它是最为流行的版本控制系统,直到2000年被Subversion所取代。
CVS最早由一位名叫Dick Grune的荷兰科学家于1986年6月23日公开发布了该代码。CVS最初仅仅是一个包装了RCS(修订控制系统(Revision Control System))的shell脚本集合。最终演变成当前版本的CVS的代码始于1989年4月Brian Berliner的贡献,后来由Jeff Polk和许多其他贡献者提供帮助。Brian Berliner撰写了一篇论文,介绍了他对CVS程序的改进-该程序描述了该工具是如何在Prysma内部扩展和实用的。1990年11月19日,CVS1.0版被提交自由软件基金会进行开发和分发。
CVS是一个C/S系统,是一个早期常用的代码版本控制软件。多个开发人员通过中心版本系统控制来记录文件版本,从而达到保证文件同步的目的。CVS版本控制系统是一种GNU软件包,主要用于在多人开发环境下的源码的维护。
由于CVS是集中式控制系统,所以它有客户端和服务端之区分。但是开始使用CVS的话,即使只在你本地机器上使用,也必须设置CVS服务器。
3.2 Subversion(SVN)集中式版本控制系统
SVN由CollabNet公司于2000年资助并发起开发,目的是创建一个更好的版本控制系统以取代CVS。
2000年2月,CollabNe联系了Open Source Development with CVS(Coriolis,1999)的作者Karl Fogel,问他是否愿意为这个新项目工作。这时Karl已经和他的偏移Jim Blandy讨论一个新的版本控制系统的设计。它不仅已经起好了名字"Subversion",而且有了Subversion资料库的基本设计。
经过14个月的编码,在2001你那8月31号,Subversion可以"自我寄生"了。就是说,SubVersion开发人员挺尸使用CVS管理Subversion的源代码,开始使用Subversion代替。
2009年11月,Subversion被Apache Incubator项目所接收。2010年1月,正式成为Apache软件基金会的一个顶级项目。
SVN依赖于网络,需要在各个开发主机上安装客户端软件,并且在一台服务器集中进行版本管理和存储,目前依然由部分公司在使用。
Subversion特征:
- 优点:
- 管理方便,逻辑明确,符合一般人思维习惯;
- 易于管理,集中式服务器更能保证安全性;
- 代码一致性非常高;
- 适合开发人数不多的项目开发;
- 缺点:
- 服务器压力太大,数据库容量暴增;
- 如果不能链接到服务器上,基本上不可以工作,如果服务器不能连接上,就不能提交,还原,对比等等;
- 不适合开源开发(开发人数非常非常多,但是Google app engine就是用svc的)。但是一般集中式管理的有非常明确的权限管理机制(例如分支访问权限),就可以实现分层管理,从而很好解决开发人数众多的问题;
SVN和CVS相同点:
每次提交的文件都单独保存,即按照文件的提交时间区分不同的版本,保存至不同的逻辑存储区域,后期恢复时候直接基于之前版本恢复。
将他们存储的信息看做是一组基于文件和每个文件随时间逐步积累的差异(它们通常称作基于差异(delta-based)的版本控制)。
3.3 Git
在Linux开源的初期,Linux开源项目的代码是linus本人通过Linux命令diff和patch两条命令手动完成。随着Linux代码越来越壮大,靠linus一个人来手动合并已经不现实。2002年,linus选择了一个商业版本控制BitKeeper作为Linux内核代码管理工具(BitKeeper的开发商BitMover授权Linux社区免费使用)。但是免费使用是有很多限制的,因此Linux社区的大佬开始破解BitKeeper。其中,Samba的作者andrew破解成功了。但是被BitMover公司发现,收回免费使用权。
迫不得已,linus选择了自己开发一个分布式版本控制工具以替代BitKeeper。Linus闭关一个月,写出来Git。在一个月后,Git成功接管了Linux社区的版本控制工作,并且开始开源,维基百科中,有如下历史记录:
- 2005年4月3日,开始开发git。
- 2005年4月6日,项目发布。
- 2005年4月7日,Git就可以作为自身的版本控制工具了。
- 2005年4月18日,发布第一个多分支合并。
- 2005年4月29日,git性能就已经达到了linus的预期。
- 2005年6月16日,Linux 2.6.12发布,那时git已经维护Linux核心的源代码了。
- 2005年7月26日,linus功成身退,将git的维护交给另外一个git的主要贡献者Junio C Hamano。
- 2016年5月,Bitkeeper宣布使用Apache 2.0许可证开源。
Git重要特性:
在本地就可以完成提交,因此不需要网络,提交完成后,可以有网络环境时,再同步远程仓库服务器。
- 优点:
- 适合分布式开发,强调个体;
- 公共服务器压力和数据量都不会太大;
- 速度快,灵活;
- 任意两个开发者之间可以很容易的解决冲突;
- 支持离线工作;
- 缺点:
- 不符合常规思维;
- 学习周期相对而言比较长;
- 代码保密性差,一旦开发者把整个库克隆下来就可以完全公开所有代码和版本信息;
在Git中,每当你提交更新或保存项目状态时,它基本上就会对当前的全部文件创建一个快照并保存这个快照的索引。
为了效率,如果文件没有修改,Git不在重新存储该文件,而是只保留以一个链接指向之前存储的文件。Git对待数据更像一个快照流。
官方中文文档:
https://git-scm.com/book/zh/v2
Git和SVN区别:
- git是分布式的,svc是集中式的;
- git是每个历史版本都存储完整的文件,便于恢复,svc是存储差异文件;
- git可离线完成大部分操作,svc则不能;
- git有着更优雅的分支和合并实现;
- git有着更强的撤销修改和修改历史版本的能力;
- git速度更快,效率更高;
3.4 GitHub网站
2008年1月,Wanstrath和Preston-Werner推出使用Ruby on Ralls编写而成的GitHub个人测试版。2月,他们又增加了 第三位联合创始人PJ Hyett,到2008年3月,GitHub的beat班已经拥有了2000名用户。GitHub与2008年4月推出公测版,然后逐渐在开发者社区中流行起来,到2009年7月,用户达到了10万。
由于GitHub在软件开发人员中很瘦欢迎,成立后的四年,GitHub通过向个人程序员和企业授权每月访问平台的费用,在没有外部资金的情况下得以生存下来。
GitHub网站为开源项目免费提供Git存储,无数开运啊项目开始迁移至GitHub。包括JQuery,PHP,Ruby等等。GitHub同时提供付费账户和免费账户。这两种账户都可以创建公开的代码仓库,但是只有付费账户可以创建私有的代码仓库。
2018年6月5日,微软花费75亿美元收购GitHub。
2019年1月7日,免费的GitHub用户现在可以获得不受限制的私人项目,最多可以有三个协同合作者。
2020年4月14日,GitHub宣布向所有用户和团队提供不受限制协作人数的私有仓库,同时,GitHub的核心功能对所有人免费开发。
官网:
https://www.github.com
3.5 Gitlab网站和软件
GitHub提供了公有云的软件仓库服务,但现实私有仓库早期是收费的,而Gitlab的出现解决了这一问题。Gitlab由乌克兰程序员DmitriyZaporozhets和ValerySizov开发,它使用Ruby编写而成。后来,一些部分用Go语言重写,是完全免费的开源软件,按照MIT许可证分发。
2013年7月,Gitlab产品被拆分为两个版本:GitLab CE(社区版)和GitLab EE(企业版)。
2014年2月,Gitlab宣布采用开放核心业务模式。
GitlabEE设置在专有许可证下,并且包含CE版本中不存在的功能,GitlabCE是使用MIT许可证的基于网络的git仓库管理工具,且具有wiki和issue跟踪功能。使用git作为代码管理工具,并且再此基础上搭建的web服务。
从安全方面来看,公司不希望员工获取到全部的代码,这个时候Gitlab是最佳的选择,但对于开源项目而言,GitHub依然是代码托管的首选平台。
gitlab官方地址:
https://about.gitlab.com/
gitlab的优势:
- 开源免费,搭建简单,维护成本较低,可适用于中小型公司内部项目使用;
- 权限管理功能强大灵活,能实现代码对部分人课件,确保项目的安全性;
- 支持离线提交,基于git实现,可以不在实时依赖网络环境中进行代码提交;
3.6 Gitee(码云)
Gitee(码云)是开源中国于2013年推出的基于Git的代码托管平台、企业级研发效能平台,提供中国本土化的代码托管服务。
截至2023年7月,Gitee 已经有 1200万名注册用户和2800万个代码仓库,是中国境内规模最大的代码托管平台。
同时,旗下企业级 DevOps 研发效能管理平台 Gitee 企业版已服务超过30万家企业。
Gitee简史:
2013年,Gitee 前身 Git@OSC 面世。
2016年,Gitee 推出企业版,打造一体化的云端协作开发平台。
2018年,Gitee 高校版上线。 [4]
2019年,专注于企业私有化部署的 Gitee 专业版上线。
2020年,针对超大规模企业私有化部署 Gitee 旗舰版上线。
2020年,承接工信部国家开源托管平台项目。
2021年,举办首届全球开源技术峰会 GOTC。
2022年,Gitee 用户总数突破 1200 万。
2023年,拥抱 AI 发展,Gitee 支持 AI 大模型托管。
2023年,开源中国/Gitee 获得7.75亿元B+轮战略融资,股份重组成为完全中立平台。
gitee官方地址:
https://gitee.com/
二.git概念和原理
1.Git的区域及文件的状态变化
如上图所示,Git的区域分位四类:
- 工作区(workspace):
clone的代码或者开发编写代码文件所在目录,通常是一个服务代码所在的目录名称,对应于"项目目录"。
- 暂存区(index):
用于存储在工作区中对代码进行修改后的文件所保存的地方,只有放入此区的文件才能被git进行管理。
使用"git add"添加对应为"项目目录"到".git/index"文件。
- 本地仓库(repo):
用于存储在工作区和暂存区中改过并提交的文件地方,使用"git commit"提交,对应于"/<项目目录>/.git"。
- 远程仓库(Remote warehouse):
多个开发人员共同协作提交代码的仓库,及私有gitlab服务器或公有云github,gitee网站等。
如上图所示,Git的区域文件的状态也并不相同,大概分位四类:
- Untracked状态:
当新建一个文件,还没有进行任何Git操作时,该文件处于未跟踪状态。
- Staged状态:
将Untracked状态的文件需要通过git add命令将文件推送到暂存区域,从而进入Staged状态。
- Modified状态:
对本地仓库中的文件进行编辑后,文件状态会变为已修改。
如果将已修改的文件通过git add命令推送到暂存区域,文件状态会从Modified变为Staged。
- Unmodified状态:
暂存区域中的Staged状态文件通过git commit命令提交到本地仓库后,状态变更为Unmodified。
2.Git分支和标签
在Git中,分支用于并行开发和维护不同版本的代码。而标签则是一种对特定提交的引用,通常用于标记发行版或重要的里程碑。
分支(branch)在实际中有什么用:
假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能测试等工作。
如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。现在有了分支,就不用担心了。
你创建了一个独立的分支,对其他人是透明的,没有影响,他们还继续在原来的分支上正常工作,而你在自己的分支上工作,想提交就提交,直到开发完毕后,在一次性合并到原来的分支上,这样既安全又不影响到别人工作。
标签(tag)在实际中有什么用:
给某个状态打个标签用于记录当前状态,相当于里程碑。tag是git版本库里的一个标记,指向某个commit的指针。
tag主要用于发布版本的管理,一个版本发布之后,可以为git打上v15.2.17,v18.2.3,...这样类似的标签。
tag跟branch有点相似,但是本质上和分工是不同的:
tag对应某次commit,是一个点,是不可移动的。
branch对应一系列commit,是很多点连成的一根线,有一个HEAD指针,是可用依靠HEAD指针移动的。
所以,两者的区别决定了使用方式,改动代码用branch,不改动只查看用tag。
tag和branch的相互配合使用,有时候起到非常方便的效果。
例如: 已经发布了v1.0,v2.0,v3.0三个版本,这个时候,我突然想不改现有代码的前提下,在v2.0的基础上加个新功能,作为v4.0发布。就可以checkout v2.0的代码作为一个branch,然后作为开发分支。
三.Git环境快速搭建
1.Git环境安装
1.1 Git下载站点
如上图所示,表示git版本的下载站点,根据各自的操作系统下载对应平台的软件即可。
推荐阅读:
https://git-scm.com/downloads
1.2 各个Linux平台包安装方式
如上图所示,代表是Linux安装的方式:
https://git-scm.com/downloads/linux
1.3 CentOS编译安装git
(1)源码下载
https://mirrors.edge.kernel.org/pub/software/scm/git/
https://github.com/git/git/tags
(2)CentOS编译安装git
yum -y install gcc make openssl-devel curl-devel expat-devel
(3)下载源码
wget https://github.com/git/git/archive/refs/tags/v2.39.1.tar.gz
wget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.39.1.tar.gz
(4)解压软件包
tar xf git-2.39.1.tar.gz -C /usr/local/src/
(5)查看安装文档
cat /usr/local/src/git-2.39.1/INSTALL
(6)安装git
mkdir -pv /yinzhengjie/{softwares,data,logs}
cd /usr/local/src/git-2.39.1/
make -j `grep -c processor /proc/cpuinfo` prefix=/yinzhengjie/softwares/git all
make prefix=/yinzhengjie/softwares/git install
(7)配置环境变量
echo 'PATH=$PATH:/yinzhengjie/softwares/git/bin' > /etc/profile.d/git.sh
. /etc/profile.d/git.sh
git version
1.4 Windows安装git
如上图所示,下载Windows软件包:
https://git-scm.com/download/win
1.5 mac安装git
如上图所示,下载mac软件包:
https://git-scm.com/download/mac
2.git常用方法
2.1 git常用的子命令
- git help <子命令>
不加<子命令>是查看git命令的帮助。
- git <子命令> --help
不加<子命令>是查看git命令的帮助
- git version
查看版本
- git init
本地仓库初始化
- git init --bare
创建无工作区的祼仓库,适用于充当远程仓库,一般对应的目录以.git为后缀
- git clone http://url
克隆url指定的项目的所有文件,并在本地创建此项目的git仓库
- git clone -b develop http://url
克隆url指定的项目中develop分支
- git clone -b tag http://url
克隆url指定的项目中tag
- git config --global user.name "yinzhengjie"
设置当前用户的git全局用户名,和下面两项都存放在~/.gitconfig文件中
- git config --global user.email "[email protected]"
设置全局邮箱
- git config --global color.ui true
让Git显示颜色,会让命令输出看起来更醒目
- git config --global core.editor vim
git默认的编辑器为nano,不常用,需要修改为vim
- git config --global --list|-l
列出用户全局设置,默认保存在~/.gitconfig文件中
- git config --global -e
交互编辑配置
- git add index.html / .
添加指定文件、目录或当前目录下所有数据到暂存区.
- git rm --cached file
只删除暂存区的文件,不删除工作区文件,相当于git add 反操作
- git rm file
从删除工作目录和暂存区删除文件
- git checkout file
从暂存区复制文件到工作目录
- git restore file
新版EXPERIMENTAL命令,可以恢复修改的工作区文件
- git ls-files
查看暂存区文件,选项-s显示mode bits, object name and stage number,-o显示untracked文件。
- git cat-file -p <blogid>
查看仓库对象的内容,仓库对象存放在.git/objects目录下
- git diff [file]
对比工作区和暂存区的区别
- git diff --cached file
对比暂存区和本地仓库的区别
- git diff <commit> -- <path>
对比工作区和指定提交的区别
- git diff <commit> <commit> -- <path>
提交和提交之间的区别
- git commit -m “comment“
提交文件到工作区
- git commit -am "comment"
添加所有修改(不包括新文件)到暂存区并提交,相当于git add + git commit。
- git commit --amend --no-edit
重新覆盖上次的提交
- git commit --amend -m "comment"
重新提交,覆盖上次的提交
- git status
查看工作区的状态
- git log -p
查看每次提交的变化
- git log --pretty=oneline
- git log --oneline -1
查看最近一条commit日志
- git log origin/main
查看远程仓库的日志
- git reflog
查看分支或其它引用在本地仓库的完整历史记录
- git reset --hard HEAD^^
git版本回滚, HEAD为当前版本,加一个^为上一个,^^为上上一个版本
- git reset --hard HEAD~n
回滚前n个版本
- git reset --hard 5ae4b06
回退到指定id的版本,使用 git reflog 获取每次提交的ID
- git reset --hard v1.0
回退到指定的版本。
- git branch
查看分支及当前所处的分支
- git branch -av
查看所有本地和远程分支
- git branch <分支名>
创建分支<分支名>
- git branch <分支名> <commit_id>
基于指定提交创建新分支
- git branch -d <分支名>
删除分支
- git branch -m dev develop
修改分支名dev为develop
- git branch -M main
修改当前分支名称为main
- git checkout <分支名>
切换到已有的分支
- git checkout v1.0
切换至指定标签v1.0
- git checkout -b <分支名>
创建并切换到一个新分支
- git checkout -b <分支名> origin/<分支名>
利用服务器远程仓库的分支,同步在本地创建分支
- git checkout [<commit>]-- <file> ...
找回在工作区删除的已存入暂存区或提交的文件
- git merge master -m "mastertodev"
将master分支合并至当前分支,无选项-m, 则为交互式
- git tag v1.0
当前状态创建标签
- git tag -a v1.0 b720aaf -m "v1.0"
将指定commit创建标签
- git tag -d v1.0
删除标签
- git reset --hard v1.0
回滚到指定标签
- git tag
查看标签
- git show v1.0
查看标签
- git remote -v
查看远程仓库
- git remote show origin
查看远程仓库详细信息
- git remote add origin [email protected]:testgroup/testproject.git
建立远程仓库和本地origin关联
- git remote rename origin old-origin
修改名称
- git remote remove origin
删除关联
- git push
将本地仓库提交代码到远程服务器
- git push origin master
将当前分支推送到远程的 master 分支
- git push origin dev
将当前分支推送到远程的dev分支,如果远程没有dev会自动创建
- git push -u origin master
将本地 master 分支推送到远程仓库 master,并且参数-u表示以后,即可直接用git push代替此命令。
- git push origin main:dev
将本地main分支推送至远程dev分支
- git push origin :dev
将空分支推送到远程dev,即删除远程dev分支
- git push origin v1.0
将本地仓库v1.0的tag推送到远程
- git push -u origin --all
推送所有分支
- git push origin --tags
将本地仓库所有的tag都推送到远程
- git push origin --tag v1.0
只推送指定的tag到远程
- git pull
从远程服务器获取代码到本地仓库,并合并到本地仓库
- git pull origin dev
从远程服务器分支dev拉取代码到本地仓库
- git fetch
从远程仓库获取最新版本,不合并至本地仓库
- vim .gitignore
定义忽略文件,即不放在仓库的文件
推荐阅读:
https://git-scm.com/book/zh/v2
2.2 git log显式日志格式
参数 | 说明 |
---|---|
%H | commit hash |
%h | commit short hash |
%T | tree hash |
%t | tree short hash |
%P | parent hash |
%p | parent short hash |
%an | 作者名称 |
%aN | ".mailmap"中对应的作者名称 |
%ae | 作者邮箱 |
%aE | ".mailmap"中对应的作者邮箱 |
%ad | --date=制定的日志格式 |
%aD | RFC2822日期格式 |
%ar | 日期格式,例如: 1 day ago |
%at | UNIX timestamp日期格式 |
%ai | ISO 8601日期格式 |
%cn | 提交者名称 |
%cN | .mailmap对应的提交的名字 |
%ce | 提交者邮箱 |
%cE | .mailmap对应的提交者的邮箱 |
%cd | --date=制定的提交日志格式 |
%cD | RFC2822提交日期的格式 |
%cr | 提交日期的格式,例如: 1 day ago |
%ct | UNIX timestamp提交日期的格式 |
%ci | ISO 8601提交日期的格式 |
%d | ref名称 |
%e | encoding |
%s | commit信息标题 |
%f | 过滤commit信息的标题使之可以作为文件名 |
%b | commit信息内容 |
%N | commit notes |
%gD | reflog selector, 例如: refs/stash@ |
%gd | shortened reflog selector,例如: stash@ |
%gs | reflog subject |
%Cred | 切换至红色 |
%Cgreen | 切换至绿色 |
%Creset | 重置颜色 |
%C(color) | 制定颜色,as described in color.branch.* config option |
%m | left right or boundary mark |
%n | 换行 |
%% a | raw % |
%x00 | print a byte from a hex code |
%w([[,[,]]]) | switch line wrapping, like the-w option of git-shorlog(1). |
git log显式日志格式语法如下:
git log --graph --pretty=format:'参数'
参数说明如上表所示。
2.3.git reset回滚版本
get reset 是对本地仓库的项目进行回滚操作的命令,它主要有如下三个参数:
--soft:
工作目录和暂存区都不会被改变,只是本地仓库中的文件回滚到当前的那个版本。
--mixed:
默认选项,回滚暂存区和本地仓库,但是工作目录不受影响。
--hard:
本地仓库,暂存区和工作目录都回滚到指定版本,该选项非常危险。
get reset官方说明:
https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified
3.Git使用快速入门指南
推荐阅读:
标签:git,提交,管理系统,--,代码,Git,概述,版本
From: https://www.cnblogs.com/yinzhengjie/p/18550940