前言
Git是目前世界上最先进的分布式版本控制系统。
工作原理 / 流程:
Workspace:工作区
Index / Stage:暂存区
Repository:仓库区(或本地仓库)
Remote:远程仓库
SVN与Git的最主要的区别?
SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而干活的时候,用的都是自己的电脑,所以首先要从中央服务器哪里得到最新的版本,然后干活,干完后,需要把自己做完的活推送到中央服务器。集中式版本控制系统是必须联网才能工作,如果在局域网还可以,带宽够大,速度够快,如果在互联网下,如果网速慢的话,就纳闷了。
Git是分布式版本控制系统,那么它就没有中央服务器的,每个人的电脑就是一个完整的版本库,这样,工作的时候就不需要联网了,因为版本都是在自己的电脑上。既然每个人的电脑都有一个完整的版本库,那多个人如何协作呢?比如说自己在电脑上改了文件A,其他人也在电脑上改了文件A,这时,你们两之间只需把各自的修改推送给对方,就可以互相看到对方的修改了
一,基础篇
1,提交代码
$ git add . //添加分支代码到暂存区域
$ git commit -m "fix :-----" //提交分支代码---填写提交信息
2,创建分支
$ git branch newImage // 创建新分支 newImage
$ git checkout newImage 或 git switch newImage //切换到本地newImage分支
$ git checkout -b newImage 创建新分支并切换到新分支
3,合并分支
$ git merge bugFix //合并bugFix 分支到当前分支
$ git rebase bugFix //合并bugFix 分支到当前分支
区别:
rebase 一个两个分支 就各位了一个分支,test合并前所有的 patch也就是commit消失了
merge 则还是两个分支,只不过在merge后这个点交汇
图解:
建议使用merge合并代码
二,高级篇
1,分离HEAD
HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。HEAD 通常情况下是指向分支名的。
将其总结为以下几种情况:
·如果临时想基于某个commit做变更,试试新方案是否可行,就可以采用分离头指针的方式。测试后发现新方案不成熟,直接reset回其他分支即可,省去了建、删分支的麻烦,git会忽略管理这个没有分支commit。
·进行到某个commit,试试新方案,为该commit建立新的branch,可以进行特性开发。
·分离头指针对应的commit,你如果认为有用需要保留,那就用新分支把它保留。
·如果当下无master分支,可以在分离头指针状态下,git checkout -b master HEAD
2,相对引用
通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用。使用相对引用的话,你就可以从一个易于记忆的地方(比如bugFix 分支或 HEAD)开始计算。
下面介绍两个简单的用法:
· 使用 ^ 向上移动 1 个提交记录
· 使用 ~<num> 向上移动多个提交记录,如~3
<1>.操作符 (^)
把这个符号加在引用名称的后面,表示让 Git 寻找指定提交记录的父提交。
所以 master^ 相当于“master 的父节点”。
master^^ 是 master 的第二个父节点
演示操作:切换到 master 的父节点
$ git checkout master^
也可以将 HEAD 作为相对引用的参照。下面咱们就用 HEAD 在提交树中向上移动几次。
我们可以一直使用 HEAD^ 向上移动。
$ git checkout C3
$ git checkout HEAD^
$ git checkout HEAD^
$ git checkout HEAD^
<2>.操作符(~)
如果想在提交树中向上移动很多步的话,敲那么多 ^ 貌似也挺烦人的,Git 当然也考虑到了这一点,于是又引入了操作符 ~。
该操作符后面可以跟一个数字(可选,不跟数字时与 ^ 相同,向上移动一次),指定向上移动多少次。下面实际操作
用 ~<num> 一次后退四步,这种方法比前面的简单了不少。
$ git checkout HEAD~4
3,撤销变更
主要有两种方法用来撤销变更 —— 一是 git reset,还有就是 git revert。
·git reset 通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
$ git reset HEAD~1
Git 把 master 分支移回到 C1;现在我们的本地代码库根本就不知道有C2 这个提交了。
(译者注:在reset后, C2 所做的变更还在,但是处于未加入暂存区状态。)
·虽然在你的本地分支中使用 git reset 很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!
为了撤销更改并分享给别人,我们需要使用 git revert。
$ git revert HEAD
在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录 C2' 引入了更改 —— 这些更改刚好是用来撤销 C2 这个提交的。也就是说 C2' 的状态与 C1 是相同的。
revert 之后就可以把你的更改推送到远程仓库与别人分享啦。
三,移动提交记录
1,git cherry-pick <提交号>...
如果你想将一些提交复制到当前所在的位置(HEAD)下面的话, Cherry-pick 是最直接的方式了。
$ git cherry-pick c2 c4
2,交互式的 rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了
交互式 rebase 指的是使用带参数 --interactive 的 rebase 命令, 简写为 -i
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
在实际使用时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim —— 中打开一个文件。
当 rebase UI界面打开时, 你能做3件事:
调整提交记录的顺序(通过鼠标拖放来完成)
删除你不想要的提交(通过切换 pick 的状态来完成,关闭就意味着你不想要这个提交记录)
合并提交。 遗憾的是由于某种逻辑的原因,我们的课程不支持此功能,因此我不会详细介绍这个操作。简而言之,它允许你把多个提交记录合并成一个。
实例操作:
要通过本关, 做一次交互式的 rebase,整理成目标窗口中的提交顺序。 记住,你随时都可以用 undo、reset 修正错误,这是不会记入步数的 😄
$ git rebase -i HEAD~4 //删除c2,调整顺序c3,c5,c4
四,杂项
1, 只取一个提交记录
$ git cherry-pick c2 // 只需要其中一个代码提交记录
2,Git tag
git tag可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能切换到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
$ git tag v1 c1
我们将这个标签命名为 v1,并且明确地让它指向提交记录 C1,如果你不指定提交记录,Git 会用 HEAD 所指向的位置。
3,Git Describe
由于标签在代码库中起着“锚点”的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
git describe 的语法是:
git describe <ref>
<ref> 可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会以你目前所检出的位置(HEAD)。
它输出的结果是这样的:
<tag>_<numCommits>_g<hash>
tag 表示的是离 ref 最近的标签, numCommits 是表示这个 ref 与 tag 相差有多少个提交记录, hash 表示的是你所给定的 ref 所表示的提交记录哈希值的前几位。
当 ref 提交记录上有某个标签时,则只输出标签名称
五,高级话题
1,多分支 rebase
咱们把这些分支 rebase 到 main 上,希望得到有序的提交历史,也就是我们最终的结果应该是 C6' 在 C7' 上面, C5' 在 C6' 上面,依此类推
2,两个父节点
3 ,纠缠不清的分支
现在我们的 main 分支是比 one、two 和 three 要多几个提交。出于某种原因,我们需要把 main 分支上最近的几次提交做不同的调整后,分别添加到各个的分支上。
one 需要重新排序并删除 C5,two 仅需要重排排序,而 three 只需要提交一次。
六,Push & Pull —— Git 远程仓库!
1,git init & git clone
Git 使用 git init 命令来初始化一个 Git 仓库,Git 的很多命令都需要在 Git 的仓库中运行,所以 git init 是使用 Git 的第一个命令。
在执行完成 git init 命令后,Git 仓库会生成一个 .git 目录,该目录包含了资源的所有元数据,其他的项目目录保持不变。
远程仓库并不复杂, 在如今的云计算盛行的世界很容易把远程仓库想象成一个富有魔力的东西, 但实际上它们只是你的仓库在另个一台计算机上的拷贝。你可以通过因特网与这台计算机通信 —— 也就是增加或是获取提交记录
话虽如此, 远程仓库却有一系列强大的特性
首先也是最重要的的点, 远程仓库是一个强大的备份。本地仓库也有恢复文件到指定版本的能力, 但所有的信息都是保存在本地的。有了远程仓库以后,即使丢失了本地所有数据, 你仍可以通过远程仓库拿回你丢失的数据。
还有就是, 远程让代码社交化了! 既然你的项目被托管到别的地方了, 你的朋友可以更容易地为你的项目做贡献(或者拉取最新的变更)
2,远程分支
3,Git Fetch
Git 远程仓库相当的操作实际可以归纳为两点:向远程仓库传输数据以及从远程仓库获取数据。既然我们能与远程仓库同步,那么就可以分享任何能被 Git 管理的更新(因此可以分享代码、文件、想法、情书等等)。
git fetch 完成了仅有的但是很重要的两步:
<1>从远程仓库下载本地仓库中缺失的提交记录
<2>更新远程分支指针(如 o/main)
git fetch 实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。
如果你还记得上一节课程中我们说过的,远程分支反映了远程仓库在你最后一次与它通信时的状态,git fetch 就是你与远程仓库通信的方式了!希望我说的够明白了,你已经了解 git fetch 与远程分支之间的关系了吧。
git fetch 通常通过互联网(使用 http:// 或 git:// 协议) 与远程仓库通信。
git fetch 并不会改变你本地仓库的状态。它不会更新你的 main 分支,也不会修改你磁盘上的文件。
4,Git Pull
既然我们已经知道了如何用 git fetch 获取远程的数据, 现在我们学习如何将这些变化更新到我们的工作当中。
其实有很多方法的 —— 当远程分支中有新的提交时,你可以像合并本地分支那样来合并远程分支。也就是说就是你可以执行以下命令:
git cherry-pick o/main
git rebase o/main
git merge o/main
等等
实际上,由于先抓取更新再合并到本地分支这个流程很常用,因此 Git 提供了一个专门的命令来完成这两个操作。它就是我们要讲的 git pull。
git pull 就是 git fetch 和 git merge 的缩写!
5,fakeTeamwork
6,Git Push
git push 负责将你的变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。
注意 —— git push 不带任何参数时的行为与 Git 的一个名为 push.default 的配置有关。它的默认值取决于你正使用的 Git 的版本,但是在教程中我们使用的是 upstream。 这没什么太大的影响,但是在你的项目中进行推送之前,最好检查一下这个配置。
7,偏离的工作
多个小伙伴同时开发,因代码提交顺序导致git push失败
还有其它的方法可以在远程仓库变更了以后更新我的工作吗? 当然有,我们还可以使用 merge
尽管 git merge 不会移动你的工作(它会创建新的合并提交),但是它会告诉 Git 你已经合并了远程仓库的所有变更。这是因为远程分支现在是你本地分支的祖先,也就是说你的提交已经包含了远程分支的所有变化。
很好!但是要敲那么多命令,有没有更简单一点的?
当然 —— 前面已经介绍过 git pull 就是 fetch 和 merge 的简写,类似的 git pull --rebase 就是 fetch 和 rebase 的简写!
8,远程服务器拒绝!(Remote Rejected)
如果你是在一个大的合作团队中工作, 很可能是main被锁定了, 需要一些Pull Request流程来合并修改。如果你直接提交(commit)到本地main, 然后试图推送(push)修改, 你将会收到这样类似的信息:
! [远程服务器拒绝] main -> main (TF402455: 不允许推送(push)这个分支; 你必须使用pull request来更新这个分支.)
为什么会被拒绝?
远程服务器拒绝直接推送(push)提交到main, 因为策略配置要求 pull requests 来提交更新.
你应该按照流程,新建一个分支, 推送(push)这个分支并申请pull request,但是你忘记并直接提交给了main.现在你卡住并且无法推送你的更新.
解决办法
新建一个分支feature, 推送到远程服务器. 然后reset你的main分支和远程服务器保持一致, 否则下次你pull并且他人的提交和你冲突的时候就会有问题.
七,关于 origin 和它的周边 —— Git 远程仓库高级操作
1,合并特性分支
实例:
这里共有三个特性分支 —— side1 side2 和 side3
我需要将这三分支按顺序推送到远程仓库
因为远程仓库已经被更新过了,所以我们还要把那些工作合并过来
图示:
2,合并远程仓库
rebase 的优缺点:
优点:Rebase 使你的提交树变得很干净, 所有的提交都在一条线上
缺点:Rebase 修改了提交树的历史
比如, 提交 C1 可以被 rebase 到 C3 之后。这看起来 C1 中的工作是在 C3 之后进行的,但实际上是在 C3 之前。
一些开发人员喜欢保留提交历史,因此更偏爱 merge。而其他人(比如我自己)可能更喜欢干净的提交树,于是偏爱 rebase。
3,远程跟踪分支
pull 操作时, 提交记录会被先下载到 o/main 上,之后再合并到本地的 main 分支。隐含的合并目标由这个关联确定的。
push 操作时, 我们把工作从 main 推到远程仓库中的 main 分支(同时会更新远程分支 o/main) 。这个推送的目的地也是由这种关联确定的!
你可以让任意分支跟踪 o/main, 然后该分支会像 main 分支一样得到隐含的 push 目的地以及 merge 的目标。 这意味着你可以在分支 totallyNotMain 上执行 git push,将工作推送到远程仓库的 main 分支上。
有两种方法设置这个属性:
第一种就是通过远程分支检出一个新的分支,执行:
git checkout -b totallyNotMain o/main
就可以创建一个名为 totallyNotMain 的分支,它跟踪远程分支 o/main
另一种设置远程追踪分支的方法就是使用:git branch -u 命令,执行:
git branch -u o/main foo
这样 foo 就会跟踪 o/main 了。如果当前就在 foo 分支上, 还可以省略 foo:
git branch -u o/main
4,Git Push 的参数
$ git push <remote> <place>
如果来源和去向分支的名称不同呢?比如你想把本地的 foo 分支推送到远程仓库中的 bar 分支
5,Git fetch 的参数
<place> 参数
如果你像如下命令这样为 git fetch 设置 的话:
$ git fetch origin foo
Git 会到远程仓库的 foo 分支上,然后获取所有本地不存在的提交,放到本地的 o/foo 上。
你可能会好奇 —— 为何 Git 会将新提交放到 o/foo 而不是放到我本地的 foo 分支呢?之前不是说这样的 参数就是同时应用于本地和远程的位置吗?
好吧, 本例中 Git 做了一些特殊处理,因为你可能在 foo 分支上的工作还未完成,你也不想弄乱它。还记得在 git fetch 课程里我们讲到的吗 —— 它不会更新你的本地的非远程分支, 只是下载提交记录(这样, 你就可以对远程分支进行检查或者合并了)。
“如果我们指定 <source>:<destination> 会发生什么呢?”
如果你觉得直接更新本地分支很爽,那你就用冒号分隔的 refspec 吧。不过,你不能在当前检出的分支上干这个事,但是其它分支是可以的。
这里有一点是需要注意的 —— source 现在指的是远程仓库中的位置,而 <destination> 才是要放置提交的本地仓库的位置。它与 git push 刚好相反,这是可以讲的通的,因为我们在往相反的方向传送数据。
6,没有source的source
Git 有两种关于 <source> 的用法是比较诡异的,即你可以在 git push 或 git fetch 时不指定任何 source,方法就是仅保留冒号和 destination 部分,source 部分留空。
$ git push origin :side
$ git fetch origin :bugFix
7,Git pull 参数
$ git pull origin foo // 相当于
$ git fetch origin foo;
$ git merge o/foo
$ git pull origin bar~1:bugFix // 相当于
$ git fetch origin bar~1:bugFix;
$ git merge bugFix
看到了? git pull 实际上就是 fetch + merge 的缩写, git pull 唯一关注的是提交最终合并到哪里(也就是为 git fetch 所提供的 destination 参数)