写在前面: 如果各位想要入门学习git,我更推荐Bilibili的这个视频: 【【GeekHour】一小时Git教程】 www.bilibili.com/video/BV1HM… 这个视频讲的非常好,动画也很生动,概念相对也比较清晰,适合入门。
什么是git?
在介绍什么是git之前,需要对版本控制系统有一个简单的了解。
想象一下,你在写一个项目。你写了一大堆代码,但是最后发现这一坨代码的方向似乎是有问题的。于是你把这一坨代码全部删除,重新写了另一坨代码,然后发现方向还不如最开始那一坨。
在没有版本控制系统的情况下,之前的代码需要重写,工作量很大。
于是,版本控制系统应运而生。顾名思义,它会记录目前仓库(关于仓库这个定义的解释之后再讲)所有文件的所有历史版本,这样你就可以很轻易地恢复到之前的某一个版本。
目前的版本控制系统分为两大种:以SVN为代表的集中式版本控制系统和以git为代表的分布式版本控制系统。
上面那个例子里面这两种情况看不出区别,因为集中和分布是针对于多人协作开发来讲的。
集中式版本控制系统里,代码仓库集中在某一台服务器中,所有人的文件修改都需要向这个服务器提交。
而分布式版本控制系统中,每个人的电脑上都有一份完整的代码仓库,每个人的文件修改也是向本地仓库提交,在所有工作完成之后再把所有人的仓库合并成大仓库。
git中的概念
首先,一个完整的git仓库有三个区:工作区、暂存区、仓库。
工作区就相当于是这个git仓库所在的文件目录。git会监视这个目录下的所有文件,监测所有文件的修改。在修改过一个文件之后,可以通过git add命令将它放入暂存区。
暂存区存储已经修改过,并且已经准备好放入仓库的文件。如果对于一个文件的修改已经完成,那么就把这个文件添加到暂存区,准备放入仓库。
而仓库本身会存放这个目录下每次提交的内容,相当于是工作区修改的一个历史记录。当暂存区的内容准备完成之后,通过git commit命令把暂存区的文件送入仓库,完成一次提交。
仓库可以是本地的一个仓库,也可以是互联网位置的一个现有仓库。比如Github就可以看作一个git仓库的托管网站。
git的配置
首先需要安装git。对于Linux或者FreeBSD这类的类Unix系统来讲,应该没有哪个的软件仓库里没有git了。
所以在你的Linux上执行:(以apt为例,其它的包管理器同理)
sudo apt update
sudo apt install git
对于Windows来讲,需要去git官网下载安装包。安装完成后,在开始菜单里会有以下程序入口:
其中Git Bash是一个类似于Linux环境的git管理器,而Git CMD是Windows终端环境的git管理器。不过我还是更建议把git添加到环境变量中,这样可以在任何位置访问到git。
在安装完成git之后,需要先进行第一次配置。
# 配置用户名
git config --global user.name "Y.Y. Daniel"
# 配置邮箱
git config --global user.email "626986815@qq.com"
这两行命令分别把用户名和邮箱设置成“Y.Y. Daniel”和“626986815@qq.com”。其中--global选项意图是让这台电脑对任何仓库做的提交都使用这个用户名和这个邮箱。
在设置完用户名和邮箱之后,git的使用前配置就完成了。
git的使用:提交
简单地说,从创建仓库到完成一次提交,经过三步走:
git init
git add .
git commit -m "第一次提交"
初始化仓库:git init
这个命令用来基于当前目录生成一个仓库。
检测当前目录是否为仓库的一个标志是查看是否有.git的隐藏文件夹。
在初始化完成仓库之后,才可以进行后续的add和commit的操作。
我们会注意到,在init完成之后,出现了[master]的标识,这个标识来自post-git扩展,如果该目录是一个git仓库,那么post-git会显示这个仓库目前的分支。分支会在后面讲。
添加暂存:git add
这个命令用来将已经修改过的工作区文件添加到暂存区。
add之后还需要一个参数,如果像上述那样使用add .,则表示将当前所有修改过的文件都添加到暂存区。
也可以在add之后添加某一个特定的文件,用来把这个文件单独添加到暂存区。
注意来自post-git扩展的显示。在新建一个文件之后,用红色显示了“+1 ~0 -0”的字样,表示1个文件有增加但是没有添加到暂存区。add之后,这行显示变绿,表示1个有增加的文件已经添加到了暂存区。
查看差异:git diff
这个命令用来详细查看某个文件或者整个仓库所修改的内容。
实际上diff可以比较的内容非常广泛,不是上面那一行文字可以总结的。从比较任意两区之间的差异,到比较两个分支之间的差异,到比较某个文件某两个版本之间的差异,都是diff的能力范围。
(在写这一条的时候,我已经完成了一次提交。所以post-git的显示状态是下面commit完成之后的状态。)
在两个被比较对象之间没有差异的时候,该命令没有任何输出。
这行命令之后不添加任何参数时会比较工作区和最新版本的仓库之间的差异。
添加--staged或者--cached参数可以比较暂存区和最新版本的仓库之间的差异。
在这个例子中,我先把上述的修改添加到暂存区。
而添加HEAD参数可以同时比较三区的修改。
在这个例子中,我会让三区的文件各不相同。
关于HEAD指针和HEAD指针的变式会在下面详细讲。
添加文件名作参数可以比较单一文件在工作区和最新仓库内的不同(比较暂存区加上--staged参数)。
添加提交ID
提交:git commit
这个命令用来将暂存区的文件存到仓库中。
添加-m选项之后,需要跟一个字符串,这个字符串会表示这一次提交的ID,这个ID会作为每一次历史提交的标题。
commit之后,post-git的文件修改提示消失,表示工作区和仓库的文件一致。
小技巧:同时添加暂存和提交
在commit中加入-a参数。就像这个样子:
git commit -a -m "test1"
在这之前做的修改不用手动add,这行命令可以同时完成add和commit的两个步骤。
(但是仅对文件修改生效,文件添加是无效的)
查看当前目录文件:git ls-files
git的使用:查看历史
git log
HEAD指针
永远指向最后一次提交的一个指针。后面的时候会提到,回退版本的时候可以直接根据提交的特征码(即log图里的黄色字符串),也可以根据HEAD指针进行偏移。
HEAD指针有如下的偏移规则,这些偏移规则也可以反过来应用在上面的diff命令中:
HEAD~X:从HEAD指针指向的commit版本开始数,往回数X个版本;
HEAD^^^(X个^):同上。
git的使用:撤销修改
通过git checkout命令可以撤销某个文件在工作区的修改。
例如,git checkout -- file.txt可以撤销file.txt这个文件在工作区的修改。
(checkout命令主要的用在分支控制上,之后会讲)
另外还有一个命令,git rm命令可以在工作区和暂存区中同时删除某一个文件。添加--cached选项可以让这个文件在工作区中保留,仅从暂存区中删除。
如果需要删除文件夹,像linux一样,使用-r参数使其递归删除。
git的使用:版本回退
reset的三种模式
分别是soft、hard和mixed。
下表中,⭕表示保存,×表示不保存。
| soft | hard | mixed | |
|---|---|---|---|
| 工作区 | ⭕ | × | ⭕ |
| 暂存区 | ⭕ | × | × |
模式可以用形如--mixed的跟随参数的形式指定。在不指定参数的情况下,使用mixed模式。
如图所示,在经过几次提交之后,我拥有了如下四次提交:
将其reset到dda535c那一次的状态,不添加任何参数(即mixed模式),可见扩展的显示回到了红色的工作区修改但未提交到暂存区的状态:
重新提交test4,添加soft参数将其恢复到上一次,可见目前状态是已经加入暂存区但是没有提交:
重新提交test4,添加hard参数,可见上一次提交之后的所有修改,从工作区到暂存区被全部抹除。
git的使用:文件过滤
在开发代码时,我们需要将所有的代码全部加入到仓库中。但是实际上,很多的项目开发,例如CMake项目开发时,会生成大量的临时文件。这些临时文件可以并且我们更希望由编译器结合CMakeLists.txt在编译时现场生成。所以在将代码加入仓库时,需要过滤掉这些临时文件。
在git中有一个独特的隐藏文件.gitignore,这个文件规定了哪些文件不会被加入到仓库中。
(简单的使用方法就是把文件罗列进gitignore里,但是我反复操作都没有在Windows上成功。这一个内容之后再补充。)
git的分支管理
分支也是git里相当相当非常非常重要的一个概念。
一个项目的开发流程一定会有一条大主线。围绕着这条大主线,其他的开发者会去开发其他的功能。这些功能如果被验证没有问题,那么该功能就可以合入大主线中。这些开发其他功能的流程就可以被叫做是主线的分支。分支之间独立存在互不干扰,但是可以相互合并。
如果你仔细看的话,上面的所有图中,扩展的显示内容里都有一个master。这个的意思是,上述所有的操作都是针对master分支操作的。
master(或者是main,现在实际上因为某些原因更多的地方会称其为main了)是一个仓库的主分区,是这个开发项目的主线。一个代码开发完成之后,从逻辑上讲就必须把代码合并到主分区内。
至于其他的分支,由开发者自行创建,但是从逻辑上讲,需要保证分支名称是有意义的。
新建和删除分支:git branch
这个命令是管理分支最主要的命令。
直接执行git branch,会列出仓库的所有分支,并且在当前分支上打星号。
如果后面带上了一个字符串参数,例如:
git branch dev
那么git会创建一个dev的分支。
dev这个名字可以任意修改,但是如我上方所说,请从逻辑上确保这个新分支的名字是有意义的。
默认情况下是新建一个分支,但是再加上-d的参数之后,就可以删除一个分支。
git branch -d dev
切换分支:git checkout (branchname)
这个命令用来切换分支。
上文讲过checkout的一个比较偏的用法,但是毕竟不是主流用法。checkout的主流用法是用来切换分支。
(或者也可以直接使用git switch命令)
合并分支:git merge
这个命令用来合并两个分支。
例如,我在上面的test1分支中新建一些内容:
在完成之后,第一步,切换到主支;第二步,将test1分支merge过来。
git checkout master
git merge test1
这里便提示test1_branch.txt文件(即上面在test1分支中创建的文件)被合入了。此时我们检测master中的文件,也能发现test1_branch.txt文件。
那么这一次的合并就完成了。
如果确认合并完成之后的分支已经没有用了,请删除分支。
git branch -d test1
合并分支:git rebase
这是另外一种合并分支的方法。
首先介绍一下rebase和merge的区别。
merge时发生的变化如图所示。
在merge时,两条分支本身互不影响,在合并者的head之上新建一个commit记录,两个分支相交在这个commit上,然后沿着合并者的分支继续下去。
即:在合并之前的提交记录,是哪个分支的仍然是哪个分支。
而rebase中文翻译叫“变基”,顾名思义,改变一个分支的基。
如图所示,先切换到dev分支,然后rebase到main分支之后,dev分支整个被直接接在了main分支的head后面。
如果是先切换到main分支,然后rebase到dev分支,那么main4和main5两个提交就会被变基到dev2之后。
但不管如何变基,两条分支最后会被合并成一条分支,并不像merge那样仍旧并行。
由于这里图中的“线”实际上是仓库的提交历史记录,根据这个线中的节点可以用来回溯提交,因此merge和rebase的优缺点也显而易见:
- merge的优点:不会改变原本的提交记录,方便回溯
- merge的缺点:合并的时候会新建一个合并的commit,因此会显得历史记录图更乱
- rebase的优点:让提交记录变得线性,使得整个开发过程更加直观简洁
- rebase的缺点:改变了提交记录
通常来讲,rebase不会用在多人协作开发里。如果是单人开发,并且希望历史提交记录更加简洁明了,可以使用rebase。
分支冲突
但不是每一次的合并都是这样子单纯的文件的加加减减。很可能两个分支会修改同一个文件的同一个位置。因此在合并的时候,git不知道应该使用哪一个文件,这被叫做合并冲突。
举个例子。
我新建了一个dev分支,对test3.txt文件进行了修改,然后完成了提交。
然后我回到主分支,进行了同样的操作。但是在两个分支内对test3.txt的文件修改内容并不相同。
随后,我尝试合并两个分支。
出现了错误提示:
warning: Cannot merge binary files: test3.txt (HEAD vs. dev)
Auto-merging test3.txt
CONFLICT (content): Merge conflict in test3.txt
Automatic merge failed; fix conflicts and then commit the result.
在这个错误提示中,给出了以下信息:
- HEAD指向的分支和dev分支有冲突
- 冲突文件在test3.txt
- 自动合并失败,需要手动解决冲突。
通过git status命令,可以查看有冲突的具体信息。
(另外,通过这个提示,我们知道我们也可以通过git merge --abort命令来中止这次合并。)
也可以通过给git diff命令查看这个文件的详细冲突情况。
(但不清楚是否为git版本原因还是文件本身原因,这个命令的执行效果并不好)
在这之后,就需要我们手动解决这个文件中冲突的部分。解决完成之后,就可以自动merge了。
远程仓库管理(以gitee为例)
之所以使用gitee而不是更加主流的GitHub为例,纯属是因为gitee是国内的,访问起来稳定(
但是没有关系,Gitee和Github同样都是使用git托管代码的网站,因此两者的操作都是大差不差的。
建立SSH连接
早期与Github这种网站通信的方式都是https,在访问的时候需要确认你的Github账户和密码。
关于SSH本身,我们暂时只需要知道它是一种利用非对称加密技术进行安全通信的方式。现在的Github已经全面使用SSH替代https。
在建立SSH连接之前,首先需要配置好SSH。对于Github和Gitee而言,它们需要的是公钥,而公钥需要在自己的电脑上生成。
通过这一行命令:
ssh-keygen -t rsa -b 4096
可以生成一组长度为4096的公钥和私钥。在交互的时候会向你询问密钥的存储地址和是否有密码保护,如何选择按照具体情况。
在结束之后,去到刚刚确认的密钥存储位置,至少可以发现两个文件。
其中,没有.pub扩展名的是公钥,公钥会在之后填写到Github或者Gitee中;私钥保管好,打死都不要给别人。
然后,打开Github或者Gitee,在设置里通常可以看到设置SSH密钥的选项。
把公钥粘贴到对应位置,给一个标题,确认之后公钥就算是添加完成了。
从代码托管网站向本地导入仓库
将一个本地仓库与远程仓库连接起来
Github和Gitee官方实际上也都给出了这种指导。
这里的核心步骤就是git remote add。
例如,我在本地又初始化了一个git仓库test2.
然后执行:
git remote add origin git@gitee.com:dyysb/git-learning.git
在远程仓库和本地仓库之间同步:push和pull
图上画出了本地仓库和远程仓库之间的关系。通过两个很形象的命令:
# 从本地仓库往云端仓库同步
git push
# 从云端仓库拉取最新的同步
git pull
可以实现两个存储库之间的同步。
通过git clone克隆下来的本地仓库会自动和云端仓库建立联系。如果是一个单纯的本地仓库,在上述的git remote add步骤之后,也会和远程仓库建立联系。
举个例子,我使用上述已经建立起联系的test2仓库,向里面commit一次。
然后执行git push命令。
需要注意的是,有可能会出现这个报错。
fatal: The current branch master has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin master
To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.
这是因为我们还没有设置上游分支。在git中有一个上游分支的概念,push就是向上游分支中推送,pull就是从上游分支中拉取。所以我们先按照引导设置好上游分支。
这样,我们就完成了一次push。
pull也是同理。例如,我们在远程仓库里创建一个readme。
然后执行一遍git pull,就可以把远程仓库内的新更改拉下来。