git版本管理工具复习
资料来源:Learn Git Branching
git init
我们初始化完成仓库之后就可以往对应的目录添加文件
假设现在我们编写一个了readme.txt文件,那么我们要使用git管理控制这个文件,还需要将这个文件添加到仓库,而不是仅仅将它放在仓库所在的文件夹
git add readme.txt
注意:git add不仅可以添加单个文件,还可以添加文件夹,这样文件夹中任意文件的变化都可以由git进行监控与管理
在初始化阶段我们创建了本地仓库,当文件发生改动,我们就可以将这个改动提交到本地仓库进行保存
实际上可以这么理解,git仓库保存的是你添加到仓库的所有文件的一个快照,每次进行提交git都会将新快照和旧快照进行对比,找出所有差异并保存为一个提交记录。
多次提交之后,git依然会为我们保存之前所有的提交记录
指令语句:
git commit -m
m:每次提交的额外信息,这个信息是可选的,但是强烈建议写上,因为这标识了每次提交的改动信息,为我们之后的版本管理提供了重要的依据
git commit
git的分支简单来说就是基于同一个提交历史进行不同的工作,当这些工作完成之后再将他们合并起来。这是团队合作开发的核心理念。
git的分支是非常轻量的,即使一个仓库建立了诸多分支,也不会对内存资源带来大的消耗。
git branch newImage
git commit
master分支前进,但newImage分支原地不动,因为我们当前实在master分支上进行工作(图中的星号)
当然我们也可以切换到另一个分支进行工作,使用checkout
git checkout newImage
git commit
当我们的开发者们在不同的分支上完成工作之后,就需要将这些工作进行整合了,这就是分支的合并
当我们使用git merge时会产生一个特殊的提交记录,这个提交记录有两个父节点
git merge bugFix
可以很清楚的看到,使用merge合并分支之后,master分支就拥有了c2上的所有改动。
而对于bugFix分支,同样也可以通过merge,获得c3上的所有改动
git checkout bugFix
git merge master
这个指令也是用来合并分支的,听上去和merge功能相同,但是它却和merge有着一定的区别。
之前的merge指令就像将两个分支的河流最终汇合成为一条河流,汇合之后,两条河流汇合之前的部分依然存在。
但是rebase更像是将其中一条河流的水全部取出,倒入另一条河流,之后直接将其填平。显然这样合并之后,被取水的那条河流不复存在。
git rebase master
可以看到bugFix上的修改记录c3被直接复制粘贴到了master分支顶端成为c3(副本),同时原先的修改记录(图中暗掉的c3)将不复存在,无法再访问到。
HEAD可以理解为一个指针,它默认总是指向当前分支上的最近一次提交记录,大多数修改提交树的命令都是从分离HEAD开始的
git commit
git checkout c2
我们之前的例子当中“c1”、“c2”等等这些,其实就是我们每次提交的唯一标识符。它在实际的git仓库中是一串哈希值,共40位。
显然我们之前用到哈希值的操作,需要我们每次都要去通过git log获取每一次提交的哈希值。
类似于这样的哈希值:fed2da64c0efc5293610bdd892f82a58e8cbc5d8
不过好消息是,我们不一定要输完整的哈希值,我们可以仅仅输入fed2,这样就可以了
然而即使是这样,如果每一次我们进行版本移动都要哈希值很明显是不方便的,于是就有了相对引用的方法
使用^
向上移动一个提交记录,使用~[num]
向上移动num个提交记录
git checkout master^
git checkout HEAD~4
我们可以使用branch -f强制让分支指向一个提交记录
git branch -f master HEAD~3
git reset 通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
git reset HEAD~1
虽然在你的本地分支中使用 git reset 很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的
为了撤销更改并分享给别人,我们需要使用 git revert
git revert HEAD
这是因为新提交记录 C2’ 引入了更改 —— 这些更改刚好是用来撤销 C2 这个提交的。也就是说 C2’ 的状态与 C1 是相同的。
revert 之后就可以把你的更改推送到远程仓库与别人分享了
如果你想将一些提交复制到当前所在的位置(HEAD)下面的话, Cherry-pick 是最直接的方式了,这个功能看起来和rebase很像,但是使用上是有区别的
git cherry-pick c2 c4
我们只需要提交记录 C2 和 C4,所以 Git 就将被它们抓过来放到当前分支下了。
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了
交互式 rebase 指的是使用带参数 --interactive 的 rebase 命令, 简写为 -i
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
在实际使用时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim —— 中打开一个文件
当 rebase UI界面打开时, 你能做3件事:
git rebase -i HEAD~4
相信通过前面课程的学习你已经发现了:分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
你可能会问了:有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
Git 的 tag 就是干这个用的啊,它们可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
git tag V1 c1
由于标签在代码库中起着“锚点”的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令
远程仓库并不复杂, 在如今的云计算盛行的世界很容易把远程仓库想象成一个富有魔力的东西, 但实际上它们只是你的仓库在另个一台计算机上的拷贝。你可以通过因特网与这台计算机通信 —— 也就是增加或是获取提交记录
话虽如此, 远程仓库却有一系列强大的特性
首先也是最重要的的点, 远程仓库是一个强大的备份。本地仓库也有恢复文件到指定版本的能力, 但所有的信息都是保存在本地的。有了远程仓库以后,即使丢失了本地所有数据, 你仍可以通过远程仓库拿回你丢失的数据。
还有就是, 远程让代码社交化了! 既然你的项目被托管到别的地方了, 你的朋友可以更容易地为你的项目做贡献(或者拉取最新的变更)
现在用网站来对远程仓库进行可视化操作变得越发流行了(像 GitHub 或 Phabricator), 但远程仓库永远是这些工具的顶梁柱, 因此理解其概念非常的重要!
从技术上来讲,git clone 命令在真实的环境下的作用是在本地创建一个远程仓库的拷贝(比如从 github.com)
git clone
git clone之后我们注意到的第一个事就是在我们的本地仓库多了一个名为 o/master 的分支, 这种类型的分支就叫远程分支。由于远程分支的特性导致其拥有一些特殊属性。
远程分支反映了远程仓库(在你上次和它通信时)的状态。这会有助于你理解本地的工作与公共工作的差别 —— 这是你与别人分享工作成果前至关重要的一步.
远程分支有一个特别的属性,在你检出时自动进入分离 HEAD 状态。Git 这么做是出于不能直接在这些分支上进行操作的原因, 你必须在别的地方完成你的工作, (更新了远程分支之后)再用远程分享你的工作成果。
因此,如果你看到一个名为 origin/master 的分支,那么这个分支就叫 master,远程仓库的名称就是 origin
git checkout o/master
git commit
正如你所见,Git 变成了分离 HEAD 状态,当添加新的提交时 o/master 也不会更新。这是因为 o/master 只有在远程仓库中相应的分支更新了以后才会更新。
Git 远程仓库相当的操作实际可以归纳为两点:向远程仓库传输数据以及从远程仓库获取数据。既然我们能与远程仓库同步,那么就可以分享任何能被 Git 管理的更新(因此可以分享代码、文件、想法、情书等等)。
本节课我们将学习如何从远程仓库获取数据 —— 命令如其名,它就是 git fetch。
你会看到当我们从远程仓库获取数据时, 远程分支也会更新以反映最新的远程仓库。
git fetch
就是这样了! C2,C3 被下载到了本地仓库,同时远程分支 o/master 也被更新,反映到了这一变化
git fetch 做了些什么:
git fetch 完成了仅有的但是很重要的两步:
git fetch 实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。
如果你还记得上一节课程中我们说过的,远程分支(origin/master)反映了远程仓库在你最后一次与它通信时的状态,git fetch 就是你与远程仓库通信的方式了!希望我说的够明白了,你已经了解 git fetch 与远程分支之间的关系了吧。
git fetch 通常通过互联网(使用 http:// 或 git:// 协议) 与远程仓库通信。
git fetch 不会做的事:
git fetch 并不会改变你本地仓库的状态。它不会更新你的 master 分支,也不会修改你磁盘上的文件。
理解这一点很重要,因为许多开发人员误以为执行了 git fetch 以后,他们本地仓库就与远程仓库同步了。它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件。我们在后面的课程中将会讲解能完成该操作的命令
所以, 你可以将 git fetch 的理解为单纯的下载操作。
既然我们已经知道了如何用 git fetch 获取远程的数据, 现在我们学习如何将这些变化更新到我们的工作当中。
其实有很多方法的 —— 当远程分支中有新的提交时,你可以像合并本地分支那样来合并远程分支。也就是说就是你可以执行以下命令:
git cherry-pick o/master
git rebase o/master
git merge o/master
等等
实际上,由于先抓取更新再合并到本地分支这个流程很常用,因此 Git 提供了一个专门的命令来完成这两个操作。它就是我们要讲的 git pull。
git fetch
git merge o/master
我们用 fetch 下载了 C3, 然后通过 git merge o/master 合并了这一提交记录。现在我们的 master 分支包含了远程仓库中的更新(在本例中远程仓库名为 origin)
git pull
同样的结果!这清楚地说明了 git pull 就是 git fetch 和 git merge 的缩写!
OK,我们已经学过了如何从远程仓库获取更新并合并到本地的分支当中。这非常棒……但是我如何与大家分享我的成果呢?
嗯,上传自己分享内容与下载他人的分享刚好相反,那与 git pull 相反的命令是什么呢?git push!
git push 负责将你的变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。一旦 git push 完成, 你的朋友们就可以从这个远程仓库下载你分享的成果了!
你可以将 git push 想象成发布你成果的命令。它有许多应用技巧,稍后我们会了解到,但是咱们还是先从基础的开始吧……
注意 —— git push 不带任何参数时的行为与 Git 的一个名为 push.default 的配置有关。在你的项目中进行推送之前,最好检查一下这个配置。
git push
现在我们已经知道了如何从其它地方 pull 提交记录,以及如何 push 我们自己的变更。看起来似乎没什么难度,但是为何还会让人们如此困惑呢?
困难来自于远程库提交历史的偏离。在讨论这个问题的细节前,我们先来看一个例子……
假设你周一克隆了一个仓库,然后开始研发某个新功能。到周五时,你新功能开发测试完毕,可以发布了。但是 ——天啊!你的同事这周写了一堆代码,还改了许多你的功能中使用的API,这些变动会导致你新开发的功能变得不可用。但是他们已经将那些提交推送到远程仓库了,因此你的工作就变成了基于项目旧版的代码,与远程仓库最新的代码不匹配了。
这种情况下, git push 就不知道该如何操作了。如果你执行 git push,Git 应该让远程仓库回到星期一那天的状态吗?还是直接在新代码的基础上添加你的代码,亦或由于你的提交已经过时而直接忽略你的提交?
因为这情况(历史偏离)有许多的不确定性,Git 是不会允许你 push 变更的。实际上它会强制你先合并远程最新的代码,然后才能分享你的工作。
git push
命令失败了!git push 失败是因为你最新提交的 C3 基于远程分支中的 C1。而远程仓库中该分支已经更新到 C2 了,所以 Git 拒绝了你的推送请求。
那该如何解决这个问题呢?很简单,你需要做的就是使你的工作基于最新的远程分支。
有许多方法做到这一点呢,不过最直接的方法就是通过 rebase 调整你的工作。咱们继续,看看怎么 rebase!
git fetch
git rebase o/master
git push
我们用 git fetch 更新了本地仓库中的远程分支,然后用 rebase 将我们的工作移动到最新的提交记录下,最后再用 git push 推送到远程仓库。
还有其它的方法可以在远程仓库变更了以后更新我的工作吗? 当然有,我们还可以使用 merge
尽管 git merge 不会移动你的工作(它会创建新的合并提交),但是它会告诉 Git 你已经合并了远程仓库的所有变更。这是因为远程分支现在是你本地分支的祖先,也就是说你的提交已经包含了远程分支的所有变化。
咱们们用 merge 替换 rebase 来试一下…
git fetch
git merge o/master
git push
很好!但是要敲那么多命令,有没有更简单一点的?
当然 —— 前面已经介绍过 git pull 就是 fetch 和 merge 的简写,类似的 git pull --rebase 就是 fetch 和 rebase 的简写!
git pull --rebase
git push
如果用常规的pull:
git pull
git push
git fetch
git rebase o/master side1
git rebase side1 side2
git rebase side2 side3
git rebase side3 master
git push
为了 push 新变更到远程仓库,你要做的就是包含远程仓库中最新变更。意思就是只要你的本地分支包含了远程分支(如 o/master)中的最新变更就可以了,至于具体是用 rebase 还是 merge,并没有限制。
在开发社区里,有许多关于 merge 与 rebase 的讨论。以下是关于 rebase 的优缺点:
优点:
Rebase 使你的提交树变得很干净, 所有的提交都在一条线上
缺点:
Rebase 修改了提交树的历史
比如, 提交 C1 可以被 rebase 到 C3 之后。这看起来 C1 中的工作是在 C3 之后进行的,但实际上是在 C3 之前。
一些开发人员喜欢保留提交历史,因此更偏爱 merge。而其他人可能更喜欢干净的提交树,于是偏爱 rebase
git checkout master
git pull
git merge side1
git merge side2
git merge side3
git push
在前几节课程中有件事儿挺神奇的,Git 好像知道 master 与 o/master 是相关的。当然这些分支的名字是相似的,可能会让你觉得是依此将远程分支 master 和本地的 master 分支进行了关联。这种关联在以下两种情况下可以清楚地得到展示:
直接了当地讲,master 和 o/master 的关联关系就是由分支的**“remote tracking”**属性决定的。master 被设定为跟踪 o/master —— 这意味着为 master 分支指定了推送的目的地以及拉取后合并的目标。
你可能想知道 master 分支上这个属性是怎么被设定的,你并没有用任何命令指定过这个属性呀!好吧, 当你克隆仓库的时候, Git 就自动帮你把这个属性设置好了。
当你克隆时, Git 会为远程仓库中的每个分支在本地仓库中创建一个远程分支(比如 o/master)。然后再创建一个跟踪远程仓库中活动分支的本地分支,默认情况下这个本地分支会被命名为 master。
克隆完成后,你会得到一个本地分支(如果没有这个本地分支的话,你的目录就是“空白”的),但是可以查看远程仓库中所有的分支(如果你好奇心很强的话)。这样做对于本地仓库和远程仓库来说,都是最佳选择。
这也解释了为什么会在克隆的时候会看到下面的输出:
local branch “master” set to track remote branch “o/master”
当然了,你也可以自定义这个属性。你可以让任意分支跟踪 o/master, 然后该分支会像 master 分支一样得到隐含的 push 目的地以及 merge 的目标。 这意味着你可以在分支 totallyNotMaster 上执行 git push,将工作推送到远程仓库的 master 分支上。
有两种方法设置这个属性:
第一种就是通过远程分支检出一个新的分支,执行:git checkout -b totallyNotMaster o/master
就可以创建一个名为 totallyNotMaster 的分支,它跟踪远程分支 o/master。
git checkout -b foo o/master
git pull
正如你所看到的, 我们使用了隐含的目标 o/master 来更新 foo 分支。需要注意的是 master 并未被更新!
同样的,push同样适用:
git checkout -b foo o/master
git commit
git push
我们将一个并不叫 master 的分支上的工作推送到了远程仓库中的 master 分支上
另一种设置远程追踪分支的方法就是使用:git branch -u 命令,执行:git branch -u o/master foo
这样 foo 就会跟踪 o/master 了。如果当前就在 foo 分支上, 还可以省略 foo:
git branch -u o/master
git branch -u o/master foo;
git commit;
git push
首先来看 git push。在远程跟踪课程中,你已经学到了 Git 是通过当前检出分支的属性来确定远程仓库以及要 push 的目的地的。这是未指定参数时的行为,我们可以为 push 指定参数,语法是:
git push
git push origin master
把这个命令翻译过来就是:
切到本地仓库中的“master”分支,获取所有的提交,再到远程仓库“origin”中找到“master”分支,将远程仓库中没有的提交记录都添加上去,搞定之后告诉我。
我们通过“place”参数来告诉 Git 提交记录来自于 master, 要推送到远程仓库中的 master。它实际就是要同步的两个仓库的位置。
需要注意的是,因为我们通过指定参数告诉了 Git 所有它需要的信息, 所以它就忽略了我们所检出的分支(HEAD)的属性!
git checkout c0
git push origin master
好了! 通过指定参数, 远程仓库中的 master 分支得到了更新。
如果不指定参数会发生什么呢?
git checkout c0
git push
命令失败了! 因为我们所检出的 HEAD 没有跟踪任何分支。
接下来你可能想问 —— 如果来源和去向分支的名称不同呢?比如你想把本地的 foo 分支推送到远程仓库中的 bar 分支。
要同时为源和目的地指定 的话,只需要用冒号 : 将二者连起来就可以了:
git push origin
这个参数实际的值是个 refspec,“refspec” 是一个自造的词,意思是 Git 能识别的位置(比如分支 foo 或者 HEAD~1)
一旦你指定了独立的来源和目的地,就可以组织出言简意赅的远程操作命令了
git push origin foo^:master
这是个另人困惑的命令,但是它确实是可以运行的 —— Git 将 foo^ 解析为一个位置,上传所有未被包含到远程仓库里 master 分支中的提交记录。
如果你要推送到的目的分支不存在会怎么样呢?没问题!Git 会在远程仓库中根据你提供的名称帮你创建这个分支!
git push origin master:newBranch
我们刚学习了 git push 的参数,很酷的
参数,还有用冒号分隔的 refspecs(
。 这些参数可以用于 git fetch 吗?
你猜中了!git fetch 的参数和 git push 极其相似。他们的概念是相同的,只是方向相反罢了(因为现在你是下载,而非上传)
如果你像如下命令这样为 git fetch 设置 的话:
git fetch origin foo
Git 会到远程仓库的 foo 分支上,然后获取所有本地不存在的提交,放到本地的 o/foo 上。
git fetch origin foo
我们只下载了远程仓库中 foo 分支中的最新提交记录,并更新了 o/foo
你可能会好奇 —— 为何 Git 会将新提交放到 o/foo 而不是放到我本地的 foo 分支呢?之前不是说这样的 参数就是同时应用于本地和远程的位置吗?
好吧, 本例中 Git 做了一些特殊处理,因为你可能在 foo 分支上的工作还未完成,你也不想弄乱它。还记得在 git fetch 里我们讲到的吗 —— 它不会更新你的本地的非远程分支, 只是下载提交记录(这样, 你就可以对远程分支进行检查或者合并了)。
“如果我们指定 会发生什么呢?”
如果你觉得直接更新本地分支很爽,那你就用冒号分隔的 refspec 吧。不过,你不能在当前检出的分支上干这个事,但是其它分支是可以的。
这里有一点是需要注意的 —— source 现在指的是远程仓库中的位置,而
才是要放置提交的本地仓库的位置。它与 git push 刚好相反,这是可以讲的通的,因为我们在往相反的方向传送数据。
理论上虽然行的通,但开发人员很少这么做。我在这里介绍它主要是为了从概念上说明 fetch 和 push 的相似性,只是方向相反罢了。
既然你已经掌握关于 git fetch 和 git push 参数的方方面面了,关于 git pull 几乎没有什么可以讲的了
因为 git pull 到头来就是 fetch 后跟 merge 的缩写。你可以理解为用同样的参数执行 git fetch,然后再 merge 你所抓取到的提交记录。
git pull origin master