今天我们来系统地讲一遍 Git 和 GitHub。如果你平时会用 AI 写代码的话,那这期视频你可要好好听听,因为 Git 和 GitHub 可以说是写代码必不可少的两个东西。以前是这样,在 AI 时代同样如此。Git 是一个软件,它是用来记录代码版本的。比如说你的项目做到一个阶段了,你就可以用 Git 把当时的代码保存下来,这样以后要是改坏了,你随时都能够退回到之前存过的那个版本。

而 GitHub 是一个网站,你可以用它来备份代码版本。也就是说你可以把代码和那些版本记录在 GitHub 那边存一份,这样的话就算哪天电脑坏了,这些东西也不会丢。当然 GitHub 还能做很多事情,比如说是多人一起去做开发,或者是把自己项目的源码公开出来分享给别人。总而言之 Git 和 GitHub 都是非常实用的工具。

这期视频我就带你把它们从头捋一遍,整个视频一共是分成了四个部分:包括 Git 的安装、存档和回滚、分支与合并,还有 GitHub 的相关操作。从这密密麻麻的时间戳里面你可以看出来,这期视频的内容非常全面,基本上 Git 和 GitHub 的核心功能都覆盖到了。可以说如果你想学 Git 和 GitHub 的话,那看这一期视频基本上就够用了。

另外这期视频里用到的代码我也都放到了我的 GitHub 上面,有需要的可以自行获取。好,话不多说,我们马上开始。如果你是 Windows 用户,那么你可以打开这个网站,然后点击这里面的 Install for Windows,再点击 Click here to download。然后它就会开始下载了,我们稍等一会儿。

下载完毕之后打开它。安装 Git 的方法跟安装普通软件差不多,我们就一路点击 Next 就可以了。然后我们关掉这个 View Release Notes 的选项,这个选项只是会给我们显示一下发布记录,对我们来说没有什么太大价值。最后我们点击完成,安装就到此结束了。

然后我们来点击开始菜单输入 Git Bash 打开它,再输入 git --version 这个命令。如果你像我一样出现的是这样的结果,那就说明 Git 已经安装成功了。如果你是 macOS 用户,那么安装 Git 通常来说比较简单,我们可以先打开终端。

然后输入 git --version。如果你像我这样这里正常显示出了 Git 的版本号,那恭喜你,你的电脑里面已经安装好 Git 了。Git 是一个非常常用的工具,很多软件都会依赖它。你可能在之前安装别的软件的时候顺带着就把它安装上去了。

不过还有另外一种情况,当我们输入完 git --version 然后回车之后,我们看到的可能是类似这样的一串回答。你的回答可能跟我这里并不是完全一样,不过大致意思应该差不多,就是说得安装 Xcode 命令行开发者工具之后才能够使用 Git。与此同时 Dock 栏里面会多出一个图标,就是这个图标。

点击它会出现一个对话框,这个对话框提示我们需要安装命令行开发者工具之后才能够使用 Git。命令行开发者工具就是苹果搞的一整套开发工具,里面除了 Git 之外还包含编译器、调试工具等很多开发时会用到的东西。大部分都是开发软件必备的,早晚要装,我建议你这里就按照提示把这一整套全家桶安装上去就行。

让我们来点击安装、同意协议,然后呢它就会开始安装了,稍微等一下。好 Git 安装好了,我们关掉这个对话框然后再执行 git --version 这个命令,此时就可以看到对应的 Git 版本号了。到这里 Git 就算是安装完成了。

安装好了 Git 之后我们就可以开始使用它了,这里我已经提前准备了一个简单的待办项目叫做马克待办。我们点进去看一下这个文件夹里面现在有两个文件:一个叫做 index.html,另外一个叫做 README.md。其中 index.html 里面放的是这个待办软件的页面代码。

把它拖进 VS Code 简单浏览一下就会发现,这个文件的代码量并不大只有差不多 100 多行的样子。我们再双击它打开看看效果,可以看到这就是一个非常简单的待办页面。另外一个文件是 README.md 这个文件一般是用来写项目说明的,比如说这个项目是用来做什么的、怎么使用等等。

我们也打开来看一下可以看到文件内容非常少最后这里的后续计划还写着具体内容待补充,看起来这个文件还没有写完。这个就是 README.md 的情况了。那我们刚才看了 index.html 和 README.md 这两个文件,它们的扩展名字虽然不一样但本质上都是非常普通的文本文件。

我们随时都可以用文本编辑器来打开并且修改它们,比如说我比较喜欢用的就是 VS Code。不过现在有一个问题啊:这个项目还没有被 Git 管理,这就意味着如果我们接下来把代码改坏了我们是很难回退到之前的正常状态的。因为我们根本就没有给正常的状态存档,所以下面我们要做的第一件事就是让 Git 开始管理这个项目。

我们先来到终端然后执行这个命令:cd 的意思是进入到某个文件夹,后面的这一串指的就是桌面上的马克待办文件夹了。所以这行命令就是用来进入到马克待办文件夹的,其实 cd 不是 Git 的命令但它的使用频率非常高建议大家还是记住。

进入到项目目录之后我们就可以执行我们的第一个 Git 命令了,我们的第一个 Git 命令是 git init。git init 的作用就是把当前的这个目录初始化成一个 Git 仓库,你可以理解为在执行了这个命令之后当前的这个项目就具备了用 Git 进行版本管理的能力了。

后面我们每完成一个功能就可以把它保存成一个对应的版本,这样即使改坏了我们也可以随时回滚到之前能够正常工作的状态。那它到底是怎么做到的呢?我们来看看它输出的这一行,它的意思就是说 Git 在当前目录下面创建了一个叫做 .git 的文件夹。

并且在这个文件夹里面初始化了一个空的 Git 仓库,所以你看 git init 本质上就是在当前的文件夹里面创建了一个叫做 .git 的文件夹。以后所有跟 Git 相关的东西都会存在这个 .git 文件夹里面,比如说是我们后面要保存的版本就会放在这里。

我们其实还可以去访达里面看一下这个 .git 文件夹,这个文件夹默认是隐藏的不会显示出来。所以我先按一下快捷键把隐藏文件显示出来,你看它就在这里。我们甚至还可以点进去看看里面有什么东西。

可以看出虽然这个 Git 仓库是刚刚创建好的但里面已经有了不少文件了,所以说 Git 仓库是空的意思是里面还没有保存任何版本这并不代表 .git 文件夹也是空的。Git 在初始化的时候就已经在里面准备好一些文件了,至于这些文件都是干什么的我们不必关心只需要知道怎么使用 Git 就好了。

看完了之后我们返回到项目目录再按一次快捷键把隐藏文件关掉,平时我们用不着看这些隐藏文件开着反而让文件这边显得有点乱所以我们还是让它们保持隐藏就好。好现在 Git 仓库已经初始化完成了,我们再来看看它目前的状态下面我们来执行第二个 Git 命令:git status。

git status 是一个非常常用的命令,它的作用就是查看当前 Git 仓库的状态。可以看出它的返回结果还是有点长的而且里面还有一些我们不明白的词,比如说是 branch、commit 什么的。没事这些我们暂时都不用管我们就重点研究这一部分就行。

这一块是在告诉我们目前目录里面有一些 untracked files,翻译成中文就是未跟踪的文件。具体是哪些文件呢?你看它已经列好了就是我们的 README.md 和 index.html。所以这其实就在说 Git 发现了我们这两个文件但是还没有开始跟踪这两个文件的内容,更没有把这两个文件的内容存成对应的版本。

当然这一部分还有一行告诉我们用 git add 命令就可以干啥干啥的,我们先不用管。好从这里的回复就可以看出 Git 发现了我们的文件但是到目前什么事也没有干,下面我就打算把 index.html 的内容保存成 Git 的一个版本。

这样就算是后面改坏了也能够回到之前那个能够正常运行的状态,至于那个 README.md 文件它还没有写完只是一个半成品所以我们先不存它等写完了之后再单独存。那具体应该怎么做呢?其实这个过程还稍微有点绕,为了防止后面把大家绕晕我先来画一张图给大家提前讲一下保存版本的过程中会发生什么。

把里面涉及到的原理讲清楚然后我们再回到终端实际操作一遍,这样你会更清楚一些。首先我们一起来看一下这个文件夹里面到底有什么:一共有三样东西分别是 index.html、README.md 还有刚才使用 git init 命令生成的 .git 文件夹。

这三样东西我们大体可以把它们分成两类:第一类是我们自己的项目文件也就是 index.html 和 README.md。Git 给这一部分起了个名字叫做工作区,你可以理解为这部分所包含的文件与我们的工作最为相关所以就叫做工作区。

第二类是 .git 文件夹,前面说过它是用 Git 生成的跟 Git 相关的数据都存在这里面。它里面的内容非常多不过我们不需要一个个去了解,我们只需要关注其中的两个部分就行了:一个是叫做暂存区另外一个是叫做提交历史。

所以可以看出这里面其实一共是有三个区域:分别是工作区、暂存区和提交历史。下面我就用一个具体的例子给大家说明一下这三个区域是怎么配合的:我们之前不是说想要把 index.html 当前的内容保存成一个版本吗?这个过程其实一共分为两步。

第一步我们需要使用 git add 命令把 index.html 的内容复制一份放到暂存区里面。至于 README.md 它还没有写完所以我们这次就不打算保存它,因此它也就不会出现在暂存区里面。第二步我们需要使用 git commit 命令把暂存区里面的所有内容打个包生成一个版本然后放到提交历史里面。

这里要多说一句刚才我一直在说版本但是在 Git 里面大家一般不这么叫而是直接叫做 commit。所以我们刚才做的就是提交了一个 commit,以后我再说 commit 的话你就直接把它理解为提交历史里面的一个版本就可以了。commit 翻译过来就是提交,所以提交历史也常被叫做 commit 历史这两个说法其实本质上就是一回事。

好这样我们就完成一次存档,以后即使我们把 index.html 改坏了甚至不小心删掉了也能够从这个已经保存好的 commit 里面把当时的内容恢复出来让马克待办正常运行。不过说到这里可能有些小伙伴会产生一个疑问:为什么要先把 index.html 的内容复制到暂存区里面呢?为什么不能直接把当前的 index.html 存成一个版本呢?

暂存区到底是有什么用?简单来说暂存区存在的意义就在于它可以让我们一点一点挑出下一次要提交的内容。我们刚才的那个例子比较简单只是提交了一个 index.html 文件所以暂存区的意义不是特别的明显。我们可以设想下假设我们后面代码做了一次重构,重构之后多出了很多的文件已有的 index.html 也做了一些修改。

此时我们就可以把想要提交成 commit 的文件慢慢挑出来,比如我们可以先挑 index.html再挑对应的 JS 和 CSS 文件。对你可能留意到了我们一次添加了两个文件,没错 git add 也支持一次添加多个文件的。最后全部挑好了之后我们确认一下暂存区的内容就是下一次要提交的东西,这个时候再使用 git commit 命令把暂存区里面的内容统一提交成一个 commit。

你看有了暂存区我们就可以慢慢挑选等所有的内容都准备好了再统一提交,好相信你对整个流程已经比较了解了其实就是两步:git add 和 git commit。我们现在回到终端实际操作一遍,首先我们用 git add 命令把 index.html 加到暂存区。

执行成功我们看一下当前 Git 的状态,这个时候可以看到 Git 的状态已经发生了变化了跟之前一样我们就挑重点来看就行。首先是这一块区域这里的 Changes to be committed 意思就是准备提交成一个 commit 的部分,换句话说这里所标示出的文件已经进入到暂存区了。

毕竟只有暂存区的内容才能被提交为 commit,这部分标示出的是哪个文件呢?显然就是 index.html 了。所以这就说明我们刚才的 git add 命令已经成功地把 index.html 添加到暂存区里面了,这个文件就等着被提交为 commit 了。

而你看一下下面的 README.md 还在 Untracked files 里面,说明它没有被 Git 跟踪。这些都符合我们的预期好 git add 执行完了下一步是什么呢?对就是 git commit,不过别着急在做 git commit 之前我们还需要做一件准备工作那就是设置一下自己的名字和邮箱。

为什么要这么做呢?那是因为 Git 每提交一次 commit 都会记录一下对应提交人的名字和邮箱,所以我们要提前把这两个信息告诉它。设置的方法也很简单首先我们要执行这一行命令:这行命令的功能就是告诉 Git 我的名字是 Mark。

然后我们再执行这一行命令:这行命令的功能就是把我的邮箱告诉 Git,这样就可以了。这两行命令只需要设置一次以后在这台电脑上无论是继续操作这个项目还是新建其他的项目都不用重新设置。当然要注意的是你可不要原样照抄啊,相信你的名字不是 Mark 吧你得把这里面的名字和邮箱换成你的才可以。

另外需要提醒你的是这里其实并不是在登录什么账号就是告诉 Git 你的名字和邮箱而已。Git 到时候会记录到 commit 里面所以你找个合适的名字和邮箱填上去就行,它不会做什么校验。好准备工作做完了现在可以执行 git commit 命令了具体的命令是这样子的:

你可能会问 git commit 后面加的这一串都是什么东西啊?别急我来给大家解释一下。这里的 -m 就是 message 的意思也就是给这一次的提交附上一段说明,因为一个项目可能会有几十个甚至上百个 commit。

所以我们最好描述一下每个 commit 到底做了什么,这样以后再回头查看历史的时候就可以很快知道每一个 commit 的修改内容。我这里写的消息是添加待办页面,这里所说的待办页面其实就是我们提交上去的 index.html 文件。好相信你已经彻底搞懂了 git commit 这个命令了我们回车执行它提交成功。

现在我们已经创建好一个 commit 了,我们可以执行一下 git log 这个命令确认一下。git log 就是用来查看 commit 历史的,到目前为止我们只提交了一次 commit所以这里只会看到一条记录。我们仔细研究一下这条记录看看它上面有什么东西:首先这里有一长串的字母和数字,这个就是当前这个 commit 的编号了。

每一个 commit 都有一个自己的编号而且不会跟别的 commit 重复,这有点像身份证号。然后是提交人的信息这里显示的就是我们之前设置好的名字和邮箱。这里是提交时间,再往下就是这次提交的说明也就是我们之前写在 git commit -m 后面的那句话。

看完了之后我们按 q 就可以退出这个界面了,然后我们再执行一下 git status 查看一下当前仓库的状态。这个时候会发现 index.html 已经不在改动列表里面了,而 README.md 依然还在 Untracked files 下面。这里不是说 index.html 文件不见了而是它现在的内容与刚才提交的 commit 一致没有什么新的内容需要提示所以就不需要在这里显示出来了。

至于 README.md 我们现在还没有

对它执行 git add 命令,所以 Git 仍然会把它显示出来,提醒我们它还没有被跟踪。好,在这里我们就完成了一次提交。简单来说,就是先用 git add 命令把工作区里面准备保存的内容记录到暂存区,然后再用 git commit 命令根据暂存区的内容在提交历史里面创建一个 commit。下面我们再用剩下的这个 README.md 文件走一遍,加深我们对这个流程的记忆。之前我们说过 README.md 还没有写完,所以我们就把它先写完吧。我们来打开这个文件,然后把后续计划下面的具体内容待补充换成接下来打算做的这两个功能。好改完了保存,下面我们就要把这个文件的内容提交成一个 commit。

我们先来到终端然后执行 git add 命令把它加入到暂存区里面,接着我们再执行 git status 命令看看当前的状态。可以看到 README.md 已经不会出现在 Untracked files 下面了,而是放在了这个 Changes to be committed 的下面。这就说明了它已经进入到暂存区现在处于准备提交的状态。换句话说,如果我们下一步执行 git commit 那么这次提交里面就会包含 README.md 当前的这些内容了。那我们就提交一下试试,看起来已经提交成功了。好这个时候 Git 就会创建一个新的 commit,这个新的 commit 里面就会包含我们的 README.md 文件。

这样我们的 README.md 就完整地走过了从工作区到暂存区再到提交历史的整个过程了。到现在为止我们应该就有两个 commit 了,为了确认一下我们可以再执行 git log 看看提交记录。可以看到现在一共是有两条提交记录,上面那一条是我们最新提交的对应 README.md 文件,下面的那一条是我们一开始提交的对应的就是 index.html 文件。看完了 git log 的结果之后呢我们就按 q 退出这个界面,然后呢我来执行一下 clear 命令把这个屏幕清个屏。

这样的话后续的演示会更清楚一些,当然 clear 这个命令跟 Git 没什么关系执行这个命令纯属个人喜好了你不知道也没关系不影响。好现在我们有两个 commit 了,以后如果继续修改项目每完成一个阶段性的工作我们都可以重复刚才的这套 git add 和 git commit 的流程把每个阶段性的成果都记录到提交历史里面。这样的话 Git 就会一步一步地把整个项目的历史保存下来,后面无论是回溯还是回滚都有据可查。你再也不怕把项目改坏了。

好到目前为止我们已经提交了两个 commit 了,我们先来看个动画一起复习一下到目前为止到底是发生了什么。首先我们之前说过整个项目目录一共是分成了三个部分,分别是工作区、暂存区和提交历史。其中工作区里面有两个文件,分别是 index.html 和 README.md,而暂存区和提交历史则放在 .git 文件夹的内部。我们通过 git add 命令可以把工作区的改动放到暂存区里面,通过 git commit 命令可以把暂存区的改动放到提交历史里。

这些我们之前都讲过大家应该记得很清楚吧,好下面我们就用动画来模拟一下我们刚才的 Git 操作。首先我们执行 git add 命令把 index.html 加入到了暂存区,然后再执行 git commit 命令把 index.html 的内容提交成了一个 commit。之后我们又对 README.md 文件执行了 git add 命令把它也加入到了暂存区,然后再执行 git commit 命令把暂存区的内容提交成了第二个 commit。这个就是我们之前所做的事情了,现在问题来了如果后面我们发现代码改着改着出问题了想回滚到之前的某个 commit 我们应该怎么做呢?

这个问题的答案取决于这份修改现在到底是走到了哪一步。我举个具体的例子,假如说我们后面又改了一下 index.html 文件把页面改成了深色主题。这个改动一共是有三种可能性:第一种可能性是改动只在工作区,也就是说我们改完了文件之后还没有做任何与 Git 相关的操作;第二种可能性是改动可能已经进入到暂存区了,具体来说就是改完了这个文件之后我们还执行一下 git add 命令把它放到了暂存区里;第三种可能性是改动已经提交为 commit 了,在这种情况下我们不仅把改动放到了暂存区还通过 git commit 命令提交了一个新的 commit。

这是三种不同的情况他们的回滚方法也各不一样,我们先看第一种情况也就是改动还只在工作区的情况。首先我们先改一下 index.html 的代码把它改成深色主题,现在一般都是 AI 写代码了不过为了演示更清楚一些我就手动改下好了请大家稍等。改好了我再复制一下整个文件的代码后面演示其他回滚场景的时候可能会用到,好那下面我们用浏览器看看修改效果怎么样。这个就是深色主题的效果了可能效果比较糟糕啊不必在意毕竟我只改了五行改成这样已经很不容易了。

通过刚才的演示大家可以看到我改完代码之后还没有对 Git 做任何操作,也就是说对应的修改还只在工作区里面。我们可以用之前学到的知识验证下首先执行 git status 命令可以看到 git status 列出了 index.html 前面标着 modified,说明这个文件已经被修改过了。上面的这一句 Changes not staged for commit 是什么意思呢关键是在于 stage 的这个词,在 Git 里面把改动放进暂存区的这个操作就叫做 stage。也就是我们执行 git add 所做的那件事情,已经放进暂存区里面的改动叫做 staged。

还没有放进去的就叫做 unstaged 或者说是 not staged,所以这句话的意思就是这些改动还没有被放进暂存区因此还没有准备好被提交一个 commit。所以 git status 就是在告诉它检测到 index.html 被修改了只不过这些修改内容还只在工作区里面并没有进入到暂存区更没有进入到什么提交历史里。不过 git status 只能够告诉我们哪个文件被修改看不到具体的修改内容,如果你想看具体修改内容的话可以去执行 git diff 命令。

从这里我们可以看出 index.html 的这两行被删掉了你看前面有个减号,然后在对应的地方增加了两行因为前面有一个加号。加号就代表增加下面也是一样这一行被删掉了这一行增加了,这一行被删掉了这里又增加了一行。只不过这里只展示一部分如果你想看更多的话可以按 j 往下一行移动,按 k 往上一行移动。如果感觉 j 和 k 移动太慢的话还可以按 f 往下移动一整页,按 b 往上移动一整页。当然我这里没有改多少所以体现不出这种翻页的效果不过我相信大家能理解,如果你看完了可以按 q 退出这个界面。

好假设现在我们觉得这个深色背景非常糟糕想要回滚具体该怎么做呢?其实刚才的 git status 命令已经给我们答案了,我们可以再看一下这两行括号的部分。第一行是在提示我们使用 git add 命令可以把这些改动添加到暂存区,这个其实并不是我们想要的不过没关系。第二行就跟我们的需求息息相关了我们来仔细研究一下这行,这里面的 discard changes 就是撤销改动的意思 working directory 就是工作区的意思。所以这句话翻译成中文就是用 git restore 可以撤销工作区的改动,你看这不就是我们想要的吗我们来试试。

好应该是回滚成功了我们看一下 git status 的输出来验证一下效果,这里 git status 一共是输出了两行我们只关注这一行就行。有些小伙伴可能看不大懂这一行在说什么,其中最大的卡点可能是这个 working tree working tree 什么。其实 working tree 就是工作区的意思,你可能会感觉奇怪之前不是刚说工作区是 working directory 吗怎么现在又变成 working tree 了呢。其实工作区在英文里面有两种说法有时候我们管它是叫 working tree,有时候我们是叫它 working directory。他们都是工作区的意思你不必过于纠结知道就好。

好既然知道了 working tree 是什么意思这句话就很好理解了,他就是在说没有什么需要提交的内容工作区很干净。干净的意思就是工作区没有任何改动,那说明回滚应该是生效了。我们看一下页面验证一下效果没问题确实是回到原来的浅色主题了,回滚已经成功了。这里顺便提一下我们刚才是回滚了一个文件也就是 index.html 文件,假如说你同时修改了好几个文件你想一起回滚的话该用什么命令呢?

很简单我们只需要给出每一个要回滚的文件就行,比如说如果我们想要同时回滚 index.html 和 README.md 的改动我们就可以这么写对直接在后面追加所有的文件就可以了。那如果说我们希望把当前目录的文件全部都回滚回去而当前目录改动的文件又特别多有几十个那该怎么办呢?我们应该不至于要把这几十个文件名都追加进去吧那就太麻烦了,当然是不用的。在这种情况下我们只需要把所有的文件名都换成 . 就可以了就像是这样。

. 就是当前目录的意思,所以这行命令就是在告诉 Git 我希望把当前目录的所有改动都回滚回去。一般情况下这个命令会用的更多一些毕竟现在都是 AI 写代码了一次改个几十个文件都是很正常的事情。当我们想要回滚的时候我们就得把这几十个文件一起回滚回去,这个时候用的就是这个命令了。之前我们处理的是改动还在工作区并没有进入到暂存区的情况,现在问题来了如果我们已经使用了 git add 命令把改动放到了暂存区里面怎么办呢?

这个自然也是有办法的,我们可以先把这个改动从暂存区里面去掉然后再把这个改动从工作区里面去掉。让我来给大家演示一下该怎么做,首先我们先修改一下 index.html 的内容把它改回深色背景然后我们看一下页面没问题确实是深色背景了。然后我们来到终端执行一下 git status 看一下当前的情况,没问题 Git 检测到 index.html 已经被修改了。然后这里面写的也是我们之前所看到的 Changes not staged for commit,意思也就是说当前的改动在工作区但是还没有放到暂存区里面。

下面我们就执行 git add 命令把这个文件放入到暂存区里面,然后我们再执行一下 git status 命令看一下当前的情况。好现在这里面仍然显示 index.html 被修改过了但是前面的提示发生变化了,之前我们看的是叫什么 Changes not staged for commit。但是现在它变成了 Changes to be committed,Changes to be committed 这句话其实我们之前见过这就是说 Git 检测到暂存区里面有一些改动。这些改动可以被提交成一个 commit 了,具体是哪些改动呢当然就是我们刚刚改过的那个 index.html。

这个输出结果其实是符合我们的预期的,毕竟我们刚才做的就是把 index.html 的改动放到暂存区里面。现在我们要回滚了该怎么回滚呢?其实 Git 已经告诉我们答案了,你看这一部分这里提到了一个词 unstage。还记得这个词是什么意思吗?stage 就是把改动提交到暂存区的意思这个我们之前提过,unstage 就是要把改动从暂存区里面去掉。所以呢这句话的意思就是运行 git restore --staged 的命令就可以把这部分改动从暂存区里面去掉。那我们来试一下,--staged 的后面本来是要追加具体的文件名的比如说是 index.html。

不过这里面我们来试试之前所说的另外一种用法,我们用 . 就代表当前的目录所以这个命令就是在说从暂存区里面去掉当前目录下所有文件的改动。我们当前目录里只有 index.html 被改动了所以在当前的场景下面这行命令就跟执行 git restore --staged index.html 是一样的效果。好敲好了命令之后我们回车执行看起来已经是运行成功了,我们再执行一下 git status 命令看看当前的情况。现在 Git 的状态变了变成了 Changes not staged for commit,这句话我们之前见过意思就是以下改动还没有进入到暂存区因此无法提交一个 commit。

这里说的以下改动当然就是指我们所改的 index.html 文件了,这说明什么呢?当然说明我们刚才的命令成功了。我们很顺利的把 index.html 的改动从暂存区里面撤了下来,现在只有工作区里面有这部分改动了。大家这里要注意一下这部分改动其实还在只不过只在工作区里没有在暂存区里了,换句话说我们刚才所做的操作只是修改了 Git 的数据 index.html 的文件内容可是一点也没有改。它现在还是深色主题不信我们就看一下页面对吧,还是深色主题 index.html 的改动还在的只不过在 Git 那边的记录不太一样了而已。

下面我们要做的就是把这个改动从工作区里面去掉,这个相信大家都已经会了吧我们之前刚教过如果你忘了的话看里面的提示估计也能够想起来。下面就让我们把这些改动从工作区里面也去掉,同样我们这次不指定具体的文件了就直接写 . 就好了。

. 就代表当前目录意思就是把当前目录里面所有的改动从工作区里面去掉,我们当前的目录里面只有 index.html 被改了所以这行命令跟 git restore index.html 并没有什么太大区别。好执行成功了,我们执行一下 git status 命令看一下当前 Git 的状态。这行就代表 Git 没有检测到任何的改动无论是暂存区还是工作区都没有改动,这就说明我们刚才的回滚已经成功了。我们来到浏览器这边看一下效果没问题已经恢复到原来的浅色主题了,回滚成功了。

现在让我们回顾一下当改动已经被 git add 放到暂存区里面的时候我们需要依次执行以下两个命令来回滚。其中第一个命令用于把改动从暂存区里面去掉这样的话这个改动就只存在于工作区里面了,第二个命令用于把改动从工作区里面去掉这样这个改动就彻底消失了。下面我们来看第三种情况,在这种情况下我们不仅使用 git add 命令把这个改动放到了暂存区里面甚至还执行了 git commit 命令把这个改动提交为了一个 commit。

这个时候我们该怎么回滚呢?一个非常直接的想法就是把那个 commit 给去掉,然后确保暂存区和工作区里面的改动也去掉。这三个区域里面都没有相应的改动这样就能够达到回滚的目的了,我们来试试。我们先来布置一下环境啊,我们来把这个深色主题的改动提交一个 commit 这样后面才好演示回滚。先来改一下 index.html 的代码把它改成深色背景保存之后看一下页面确认一下确实是深色主题,没问题确实是深色的。

然后我们执行 git add 命令把这部分改动放到暂存区里面,然后我们再执行 git commit 命令把它提交为一个 commit。最后我们再来看一下当前的 git log 看起来没问题,我们的 commit 已经提交好了。下面我们来看一下怎么把刚提交好的 commit 删掉这个其实很简单,我们只需要执行一行命令就可以了。大家看好了我给大家打出来,这个命令看起来有点花哨里面有 HEAD hard 还有波浪号还有一个 1。这些都是什么玩意儿?

好既然你发问了我们就把这个命令给彻底搞清楚,搞清楚了之后我们再去执行它。首先你需要了解的是 HEAD 代表的就是提交历史里面最新的 commit,让我们打开 git log 看一下你就明白了。我们先把当前的命令删掉等会再执行我们先执行一下 git log 看一下,我们现在有三个 commit。这是我们提交的第一个 commit,这是我们提交的第二个 commit,这个是我们提交的第三个 commit。我们之前说过 HEAD 指向的就是最新的 commit,最新的 commit 是哪一个呢?没错就是第三个 commit。

其实你看 Git 已经给我们标识出来了,看到里面的 HEAD 了吗?这就是说 HEAD 对应的就是这个 commit。那 HEAD 之前的这些 commit 有名字吗?有,比如说第二个 commit 就是 HEAD~1,第一个 commit 就是 HEAD~2。以此类推,后面的数字可以一直加上去,当然我们也没有新的 commit 就没法加了。另外你要注意一点,这个 HEAD 所指向的 commit 并不是固定的,它是会一直移动的。假设我们后面又提交了一个新的 commit,比如说是这样,那这个 HEAD 就要从原来的那个 commit 挪到最新的 commit 上面了。HEAD~1 和 HEAD~2 也要依次移动,同时我们还会有一个 HEAD~3。好,相信你对 HEAD 已经非常清楚了。

回头再来看一下我们当初的命令 reset(就是重置的意思),--hard 我们先忽略,后面再讲。然后 HEAD~1 代表就是 HEAD 之前的提交,也就是倒数第二个提交。所以这个命令就是在说:我希望重置下 HEAD 的位置,让它指向 HEAD~1 那个 commit。我们拿当前的 Git commit 的历史模拟一下:在我们执行这个命令之后,HEAD 就会移动到 HEAD~1 的那个位置。既然 HEAD 的位置变了,HEAD~1 的位置也要沿着 HEAD 往下移。现在 HEAD~1 占据了原来 HEAD~2 的位置,所以 HEAD~2 就会被挤出去了。最后整体就会变成这个样子。

好,现在 HEAD 指向了“添加项目说明”这个 commit,“原来的使用深色主题”就没人管了。没人管那就不要了,所以最后我们 git log 里面只会留下一开始的两条 commit,“那个使用深色主题的 commit”就相当于是被删掉了。那 --hard 又是什么意思呢?我们前面讲过 Git 里面有三个区域:工作区、暂存区和提交历史。--hard 的意思就是三个区域一起回滚,全部恢复成 HEAD~1 的那个 commit 的样子。也就是说深色主题的改动不光从提交历史里面消失了,暂存区和工作区里面也不会留下除了 --hard 之外的一些其他的选项这个我们后面再讲,你不要着急。

好 git reset HEAD~1 的命令我们已经清楚了,下面我们来执行它看看效果是不是跟我们预期的一样。好看起来执行成功了 Git 在告诉我们 HEAD 现在指向“添加项目说明”的这个 commit 了。这应该就是说之前最新的 commit 已经被删掉了,这样我们就达到了回滚的目的了。我们来验证一下:先看 git log,没错原来有三个 commit 现在只剩两个了我们新提交的那个深色主题的 commit 消失了这个符合我们的预期。

然后我们再来看一下 git status 的返回 nothing to commit,这句话说明暂存区是空的 working tree clean,这说明工作区里面也没有改动。最后我们来看一下页面,页面也回到浅色系了回滚彻底完成虽然我们讲了这么长时间但其实也就是。一行命令就搞定了,不过最后要提醒一句 --hard 干得很彻底它会把工作区、暂存区和提交历史里面的改动全部都给清除掉。正因为很彻底,所以我们用的时候最好还是要小心一点。

如果你的工作区里面还有别的没有提交的改动执行 --hard 之后他们也会一起消失所以执行之前最好是先用 git status 看一眼确保没问题之后再执行。然后我们再说一下 git reset,其实有三种模式。我们之前所讲的那个 --hard 只是其中的一种这三种模式的区别是在于回滚时所影响的区域不一样。让我们来看一下我们之前说过 Git 里面有三个区域,分别是工作区、暂存区和提交历史。

我们之前所讲的 --hard 是三个区域的改动都会回滚除了 --hard 之外还有两个选项其中一个是叫做 --mixed。这个选项是会将改动留在工作区里面只把暂存区和提交历史里面的改动给回滚掉这个也是 git reset 的默认模式。也就是说如果你不写任何选项只用 git reset HEAD~1 的话那么它本质上用的就是 --mixed 的模式。还有另外一种模式是叫做 --soft这种模式会把工作区和暂存区的改动留下来只回滚提交历史。

好可以看到这三种模式的回滚区域都是有一些区别的 --hard 的回滚非常彻底也非常的方便那 mixed 和 soft 这两种模式是什么时候会用到呢?我给大家举个例子:比如说你 commit 完了之后发现里面有一些小问题想改一改再重新提交的话,这个时候你其实就可以用它们了。因为使用这两种模式的话 commit 被撤掉了但是相关的改动还在,你接着改就行,改完了之后再提交。如果说你就是不想要这个改动了想要它彻底消失的话就直接用 --hard 就好了。

好前面我们讲了怎么通过删除 commit 来达到回滚的目的,其实这并不是唯一的回滚方式。还有另外一种回滚方式是:通过增加一个回滚的 commit 来达到回滚的效果。我来给大家演示一下,首先这个就是我们回滚前的样子:工作区、暂存区和提交历史里面都有我们的改动。然后我们要做的事情不是像前面所说的那样把这个 commit 删掉,而是让 Git 自己把工作区的代码给改回来改成原来的浅色主题。

这个就像是我们文本编辑器里面按了几次撤销键一样,把所有的改动都撤回到原来的样子。比如说如果前面是增加了一行代码那么现在就删掉一行;如果之前是把 ABC 改成 XYZ,现在就把 XYZ 给改回 ABC。这主打的就是一个反着来然后我们把这个改动通过 git add 命令保存到暂存区,再通过 git commit 命令提交一个新的 commit。这样就达到了回滚的目的了这样回滚的好处在于我们提交历史里面仍然保留了原来深色主题的 commit。

如果你哪天又后悔了想用回深色主题还能够从历史里面把它找回来当然你可能会说这个回滚方式看起来好麻烦要先改工作区再改暂存区最后再提交一个 commit。这不就是我自己手动回滚吗?这还跟 Git 有什么关系其实你想多了,Git 早就给我们准备好了一个命令运行一遍这些改动就通通完成了非常的方便。下面我就来给大家演示一下我们先来把环境搭建好首先我们要把 HTML 改成深色主题。

好保存一下我们再来看一下页面没问题页面已经是深色背景了然后我们回到终端这里再执行命令提交这个深色主题的改动看起来已经提交成功了我们来运行一下 git log 命令检查一下可以看到这个就是我们刚才创建好的这个 commit 了那说明我们的命令都已经执行成功了我们按 q 退出这个界面。下面我们就来回滚这个 commit把页面变回浅色主题具体该怎么做呢?很简单直接执行这个命令就行。

前面我们说过了 HEAD 指向的是最新的一个 commit,所以 git revert HEAD 的意思就是说创建一个新的 commit。它对应的改动内容和之前最新的 commit 正好相反,也就是说我们新创建的这个 commit 可以用来抵消原来的那个最新的 commit从而达到回滚的效果。我们来试一下回车之后命令并没有直接结束而是进入了这样的一个界面这个界面一共分为两部分内容上面这段内容是我们将要创建的回滚 commit 的说明。

也就是我们之前放在 git commit -m 后面的那一部分你没看错这个说明不止一行commit的信息确实是可以有多行的只不过我们之前创建的 commit 信息都比较简单只有一行我们来仔细读一读这几行在说什么吧第一行是在说这个 commit 是用来回滚使用深色主题那个 commit 的。这里面的 Revert 就是回滚的意思跟我们 Git 命令里面用到的 revert 是一个词下面的这一行则是把回滚的 commit ID 给写了出来。

还记得每个 commit 上面都有一个 ID 吗?我们之前用 git log 的时候看过这个 ID 就是 commit 的唯一标识所以它这里把要回滚的 commit ID 给写了出来这样就非常精确地说明了当前的 commit 到底是回滚了其他的哪个 commit。然后下面的这一部分只是注释而已你看它们都是以井号开头所以以井号开头的行都算是注释最终都会被忽略掉对结果没有任何影响你不必理会。

Git 之所以弹出这个界面就是为了方便我们去调整 commit 信息的如果你想修改这个 commit 信息的话那就改上面的这一部分就行。不过这整个界面是用 Vim 来承载的Vim 的按键跟我们普通的文本编辑器都不太一样新手进来可能会有点懵改都不知道怎么改我建议你进来之后呢就直接保存退出就行这样的话 Git 就会使用默认的那几行 commit 信息。

这其实没有什么太大影响我甚至觉得这就是最优解因为这几行 commit 信息准确地标识了当前 commit 的功能。那我们要退出了该怎么退出呢?按 :wq 再回车你看我们就退出来了看起来一切正常现在回滚 commit 应该是创建好了我们先来看一下页面没问题确实是浅色背景了然后我们再执行一下 git log 看看发生了什么。

你看这里面多了一个 commit这个 commit 与前面的 commit 的改动就是完全相反了如果深色主题的 commit 增加了一行那么最新的 commit 就会把那行给删掉;如果深色主题的 commit 改了几个字符,那么最新的 commit 就会把这几个字符给改回来。说白了就是你改了什么它就会再照样改回来这不就达到了回滚的效果了吗?而且这种回滚不会把原来的 commit 从历史里面删掉只是会新增一个新的 commit 把它给抵消掉所以整个过程依然是完整保留的以后我们想回头看当时改了些什么也能够查得到。

好回滚这一块我们就讲完了下面再来看一个问题:放在项目文件夹里面的文件是不是都需要被 Git 记录呢?其实并不一定的有些文件我们想留着但并不打算提交到这个项目的提交历史里面。比如说假设我学习的时候顺手在项目里面创建了一个文件叫做 notes.txt 记录自己的学习进度这个文件其实跟项目本身的功能并没有什么直接关联所以我希望 Git 能够忽略它。那能够做到吗?当然是可以的下面我们就来模拟一下这个过程首先我们先把 notes.txt 这个文件给创建出来。

这里我用 Vim 来创建你也可以用自己喜欢的文本编辑器比如说是 VS Code 什么的然后我们往里面写上两行写好了之后我们保存退出这样 notes.txt 就创建好了。我们到访达里面看一眼没问题文件夹里面确实是多了一个 notes.txt 文件我们回到终端执行一下 git status可以看到这份笔记出现在了未跟踪文件的列表里。

当然只要不对它执行 git add 命令它就不会被提交但每次查看状态的时候它都会出现在这里这有点烦人啊有没有办法告诉 Git 以后不需要再提示这个文件呢?毕竟这是我的个人学习笔记跟项目也没有什么直接关联我并不打算把它提交到项目里面那怎么才能够让 Git 忽略这个文件呢?其实很简单我们可以直接在项目里面新建一个文件名字就叫做 .gitignore。

然后把不想让 Git 跟踪的文件名写进去 gitignore 直译过来就是 Git 忽略写在里面的文件都会被 Git 忽略掉。建好了 .gitignore 文件之后我们再执行一下 git status 命令看一看可以看到 notes.txt 已经不在列表里面了现在还没有被跟踪的文件变成了 .gitignore。这个是我们刚才新建的文件它放在没有被跟踪的文件列表里面也算正常毕竟我们确实没有对它进行任何 Git 相关的操作。

既然 notes.txt 不在未跟踪的列表里面这就说明我们刚才的操作成功了 Git 确实忽略了我们的 notes.txt 文件。注意啊 notes.txt 这个文件本身并没有被删除我们可以再回到访达里面看一眼你看 notes.txt 这个文件还好好的我们还是可以打开它继续往里面写内容只不过 Git 不会再把它列到结果里面了。至于刚才新建的 .gitignore 文件因为它是隐藏的所以访达里面默认是看不到的但实际上它就存在于这个文件夹里面。

好可以看出来我们的 notes.txt 已经被 Git 忽略了但是它这个文件还在,这个完美符合我们的预期。我们刚才是往 .gitignore 里面添加了一个文件也就是那个 notes.txt 文件但实际上我们其实可以往 .gitignore 里面添加很多的文件。凡是添加到 .gitignore 里面的文件 Git 都会忽略掉比如说我们可以往 .gitignore 里面新增一个文件这个文件名就叫做 .DS_Store。

这是 Mac 上的一个隐藏文件你在访达里面调整一下文件夹的显示形式比如说是换个视图挪挪图标之类的 macOS 就会悄悄地生成这个文件我们现在这个文件夹里面还没有它不过我估计迟早会冒出来它跟代码没有什么关系所以我们提前把它给忽略掉。好现在我们保存退出 .gitignore 的内容我们就编辑到这里不过大家注意刚才的 git status 命令 .gitignore 是会出现在 git status 的输出里面的。

这个其实也正常它确实是我们新建的一个新的文件这个文件要不要提交呢?答案是要因为它是项目的一部分提交了之后这些规则就跟着项目一起保存下来了以后换一台电脑接着做这个项目的时候规则也在不用重新再写一遍。下面我们就把它给提交上去同样是两步第一步是运行 git add 第二步是运行 git commit执行好了之后我们再看一下 Git 当前的状态好可以看出这个文件已经提交成功了 .gitignore 我们就讲到这里了。

好前面我们讲了存档与回滚相关的内容下面我们来看看分支与合并首先来看一下分支其实 git log 的结果里面就有分支这个东西只不过当时我没有提你可能也就没有那么留意现在我们可以再运行一下 git log 仔细看一看大家看一下最新的这个 commit 旁边这里的括号除了上一部分我们讲过的 HEAD 之外还有一个东西叫做 master。

其实就是一个分支的名字有些人显示的可能不是 master 而是 main这个不用纠结它们都是分支的名字而已那分支到底是个什么东西呢?我们先回忆一下之前提交的这些 commit 最开始的时候我们提交了“添加待办页面”这个 commit Git 在这个 commit 上面贴了一个标签名字就叫做 master。后面我们又添加了项目说明新的 commit 就接到后面 master 这个标签也跟着挪到了新的 commit 上面。

再后来我们改成了深色主题然后又把它回滚了最后还加了一个 .gitignore 每提交一次 master 就往前挪一步所以它始终贴在最新的 commit 上面这个就是分支。它本质上就是一个贴在 commit 上面的标签我们从 master 出发往回找就能够找到这一串 commit这些就是 master 分支的提交历史了也就是 git log 所显示的内容。

我们之前所有的操作一直都是在 master 上进行的每次修改代码执行 git add、git commit 命令都是在这个 master 分支上进行的。那现在假设出现了一个新的需求比如我们要给马克待办增加一个新的功能支持用户给每一条待办设置截止时间。这个功能看起来好像不是特别大的样子但要真做起来其实可能也得拆成好几个 commit比如说我们可能要先把截止时间相关的界面给做出来

然后再把背后的保存逻辑给写好,最后还要判断待办有没有过期,并且给已经过期的待办加上提醒。如果按照这个思路来做的话,这个需求就得用三个 commit 才能完成了。现在试想一下,如果我们就像现在演示的这样,直接在 master 分支上面提交这些 commit,会发生什么呢?当我们提交完第一个处理前端界面的 commit 之后,与截止时间相关的保存逻辑还没有实现。这个时候 master 分支上的代码就处于一个半可用的状态,如果用户只用别的功能还好,但是一旦用户点了与截止时间相关的按钮,那产品就有可能报错。毕竟这个功能还没有完全实现嘛,这并不是我们想要的。

因为 master 或者是 main 分支在 Git 里面是一个非常特殊的分支,通常情况下它们都会被当成整个项目的主分支。也就是说,大家一般会希望这个分支上面的代码是相对稳定可以正常使用的,而不是长期放着一堆只做了一半还不能够正常工作功能。所以,对于这种需要分成好几个阶段才能够完成的新功能,如果我们直接在 master 上面一路提交,那么中间的那些还没有完成的 commit 也会进入到主分支去。这样一来 master 就有可能在一段时间里面处于不稳定甚至部分功能不可用的状态,这显然不是一个很理想的情况。

更常见的做法是,我们先基于当前稳定的 master 分支创建出一个新的分支,专门用来开发截止时间这个功能。接下来,与这个功能相关的几个 commit 都先提交到这个新的分支上,这样在我们开发新功能的整个过程中 master 分支都可以继续保持在原来稳定可用的状态。而这几个新的 commit,只有从新分支这个标签往回找才能够找到,master 那边是看不到的。等到这个新功能全部开发完成,我们也测试通过了之后,再把这个新分支合回 master。

这个合并过程本质上也就是把 master 标签往前挪到了跟新分支同样的那个位置而已。好,大概了解了分支是什么之后,下面我们就来实际创建一个分支看看。创建分支的命令非常简单,就是 git branch 后面再跟上你想要的分支名。这里我给分支起的名字叫做 feature/due-date,feature 是功能的意思,due date 翻译成中文就是截止时间。所以这个分支名合起来就代表这个分支用于添加截止时间这个功能。

你可能会好奇,中间的这个斜杠是怎么回事?其实吧,它没有什么特殊的含义,分支名里面是可以带斜杠的。很多团队喜欢用 feature/开头来表示这是一个新开发的功能,这样看上去就比较清楚一些。你完全可以把 feature/这部分给去掉,分支名就叫做 due-date 这样也是可以的,甚至说你把分支名叫做 abc 都可以。Git 并没有对分支名做非常严格的限制,这个命令执行之后终端不会返回任何内容,这个是正常的。

我们怎么确认分支到底有没有创建出来呢?我们可以直接执行 git branch 后面什么也不加。后面不加分支名的时候,git branch 的作用就是把当前仓库里面的所有分支都给列出来。可以看到现在我们有两个分支了,一个是原来就有的 master 分支,另外一个就是我们刚刚创建好的这个分支。这里还有个细节大家注意,master 前面有一个星号,这个星号表示的就是我们当前所在的分支。

也就是说,虽然新分支已经创建好了,但是我们现在还在 master 上面呢,这一点很重要大家记住了。创建分支和切换到另外一个分支是两件事情,git branch 只负责把分支给创建出来并不会把我们给带过去。那怎么切换到那个新创建好的分支里面呢?我们可以用 git switch 命令,git switch 就是切换的意思。这里 Git 告诉我们已经切换到 feature/due-date 这个分支上了,我们再执行一次 git branch 验证一下。

没问题,星号已经跑到 feature/due-date 前面了,说明我们现在确实是处于新分支上。不过实际开发的时候我们一般不会这么一步一步地来,因为创建完一条分支之后基本上就是要马上切过去开始干活的。所以 Git 给我们准备了一个更方便的写法,就是 git switch -c feature/due-date。这里面的 -c 就是 create 的缩写,所以这一行命令做的事情就是先创建出来这条分支然后立刻切过去。

相当于是把前面的那两条命令合成了一条,以后我们就直接用这个写法了。不过这条命令我这里就不执行了,因为 feature/due-date 这个分支我们刚才已经创建好了,再创建的话就是重复了。另外还有一点要提一下,网上还有一些 Git 教程可能会让你写另外一个命令就是这个 git checkout -b feature/due-date。这个 git checkout -b 跟我们刚才的 git switch -c 效果是完全一样的,都是创建并且切换分支。

checkout 是更老的写法,它能做的事情非常多除了切换分支之外还能干别的对新手来说反而会容易混淆。所以后来 Git 才把切换分支这件事情单独拆了出来做成了 git switch。我的想法是你只要知道 git checkout -b 是在干什么就可以了,我们自己用的话就用 git switch -c。毕竟这个就是 Git 后面拆出来更清楚的一种写法,好现在我们已经处在 feature/due-date 这个新分支上面了。

这里我要提醒大家一件事情,很多人第一次建完分支之后会以为项目会发生什么变化其实并不会。比如我们可以来到访达里面看一下项目目录,你看文件还是那么几个文件内容其实也一模一样。为什么呢?这是因为我们这个新分支是从 master 的最新 commit 上面岔出来的,此时此刻这两条分支指向的其实是同一个 commit。代码自然也就完全一样了用图来表示的话现在的情况就是这个样子的真正的区别要等到我们在新分支上提交了新的 commit 之后才能够显现出来。

下面我们就来提交几个 commit 试试看,好我们现在开始在新分支上面开发截止时间这个功能。这个功能我打算分成三个 commit 来完成:第一个添加截止时间界面,第二个添加截止时间保存逻辑,第三个添加过期提醒。这里面对应的代码不是我们这个教程的重点我就不一行一行让大家写了大家稍等我们快进一下。好三个 commit 都提交完了,我来给大家在页面上演示一下对应的功能好让大家有个直观的感觉。

首先可以看到页面上有一个设置截止时间的地方,在我们添加待办的时候就可以给这个待办设置一个对应的截止时间。点击添加之后这个截止时间便会显示在待办的右边,不仅如此我们还有一个过期提醒的功能。比如说在添加待办的时候如果设置的截止时间早于当前时间那么在点击添加之后这个截止时间便会以红色的方式显示而且旁边还标着已过期。

这就是我刚才所做的三个 commit 对应的功能了,下面我们来看一下 Git 的提交历史。大家可以看到这里我给 git log 增加了一个 --oneline 的选项,它的作用是让每个 commit 只占一行只会显示 commit 编号的前几位和提交说明作者和时间就不显示了。毕竟我们现在的 commit 有点多用这个选项看起来会更清楚一些,我们回车来看看效果。

这个就是 --oneline 的效果了可以看到每个 commit 确实只占了一行非常简洁。下面我们就来看看这个提交历史研究一下刚才发生了什么首先可以看出 feature/due-date 指向的是增加过期提醒这个 commit,而 master 分支指向的则是添加 .gitignore 这个 commit。所以 feature/due-date 比 master 多出了三个 commit,也就是我们刚刚提交的那三个。换句话说这三个 commit 只属于 feature/due-date在 master 上面是看不到的。

不信的话我们可以切回 master 验证下,我们来执行一下 git log。你看 master 上面还是原来的那 5 个 commit我们刚才提交的那 3 个 commit 在这里是看不到的。而且不仅 commit 看不到对应的页面里面也不会有截止时间相关的功能,不信我们打开页面看看你看日期选择框不见了页面又回到了原来的那个样子。这说明我们刚才在 feature/due-date 上面所做的所有改动完全没有影响到 master,这个就是我们在新分支上开发的好处。

在新功能还在开发的时候 master 分支始终是不受影响的,好现在我们截止时间这个功能已经开发完了也验证通过了确认没有问题了之后是时候把它们合并回 master 了。合并用的命令是 git merge,不过在执行之前有一件事情一定要搞清楚:合并是把别的分支合并到你当前所在的分支上。我们现在想把 feature/due-date 合并到 master 上面,所以我们必须要先来到 master 上然后再去合并 feature/due-date。

我们刚才已经切回到 master 了,不过为了保险起见还是先确认一下没问题星号在 master 前面那就表明我们是在 master 分支上面了。下面我们就来执行合并,合并成功了,我们来看一下页面确认一下效果没问题截止时间的功能已经出现在 master 上面了。然后我们再来看一看提交历史,我们刚才在新分支上面提交的那三个 commit 现在就全部跑到 master 的历史里面了。

而且你注意看最上面的这一条,master 和 feature/due-date 现在都标在了同一个 commit 上面。这说明这两条分支现在指向的是同一个地方代码完全一样,好功能已经合并回 master 了那这条 feature/due-date 分支还留着干什么呢?确实没什么用了,我们现在可以把它给删掉。这里面的 -d 就是 delete 的意思执行完了之后我们再看一眼好可以看到分支已经删掉了现在就只剩下 master 这么一条分支了。

到目前为止我们的合并都非常的顺利,一条命令下去就搞定了。但是在实际的开发过程中事情往往没有这么美好下面我们就来看一个会出问题的场景也就是大家经常听到的冲突。我们先来把场景搭建起来,现在我们手上有两个新功能要做:一个是排序也就是点一下之后能够把待办按照截止时间排好;另外一个是全部完成这个就是点一下能够把所有的待办一次性标记为完成。

按照之前刚立的规矩一个功能一个分支那我们就从当前的这个 master 上面拉出两个分支出来。我们先来创建负责排序功能的分支,这个功能我想分三个 commit 来做套路跟前面截止时间的功能是一样的。我们先添加排序按钮然后再实现按截止时间排序最后再支持反向排序,实际开发里面你如果想用一个 commit 一口气做完的话当然也是可以的我这里拆开是为了让后面的讲解看得更清楚一些大家稍等啊我们快进一下。

好排序这个功能就算是做完了,我们来页面这边看看效果。我们默认的这两条待办都没有截止时间为了看出排序的效果我们先随手增加三条带截止时间的待然后日期故意打乱。可以看出 a 和 c 这两个待办已经过期了所以显示成了红色的已过期这个就是我们上一个功能所做的。我们来点一下排序可以看出待办按照截止时间从近到远排好了,再来点一下排序它的这个截止时间就会反过来。

由此可以看出排序这个按钮运转得非常完美至于没有填截止时间的这两条呢它们无论是正着排还是倒着排都会被放到最后这个也是没有问题的。排序这个功能就算是做完了,下面我们来做全部完成的功能注意啊这个全部完成的新功能也是基于 master 分支来做的所以我们要先切回 master 然后再从那边创建新分支。全部完成这个功能我也打算分成三个 commit 来做所以呢这一块我也打算快进请大家稍等一下。

三个 commit 都做完了我们来页面这边看看效果,先随手加三条待办然后我们来点全部完成。可以看出所有待办一下子都勾上了这说明我们做的这个全部完成的功能运转得非常完美,好那前面我们做了两个功能一个是按照截止时间排序另外一个是全部完成。这两个功能各自占了一个分支所以到目前为止我们的这个目录里面应该是会有三个分支我们来确认一下可以看到这三个分支都在。

下面我们要做的就是把这两条分支上面的东西依次合并回 master,老规矩合并到 master 那我们就要先切回到 master 分支上然后我们执行合并的命令。好排序功能我们顺利地合进来了,下面我们再合并全部完成的功能出问题了这次的输出跟刚才可以说是完全不一样了。你看中间的那行大写的 CONFLICT 特别的显眼最后一行还写着什么 Automatic merge failed也就是自动合并失败了。这个就是我们常说的冲突这个场景了,为什么会冲突呢?我们直接来看代码就明白了。

先看 master 分支这块代码master 里面的代码当然不止这些我们只看跟冲突有关的这一块。你看这两行一行写着添加一行写着排序它们对应的就是页面上的那两个按钮。写着添加的这一行是 master 本来就有的,写着排序的这一行是刚才合并排序那条分支的时候加入进来的。再看我们正在合并的这条分支同样只看这一小块它是在做排序之前就从 master 岔出去的所以这里根本就没有写着排序那一行。

它干的事情是在添加这一行的下面又插了另外一行,这一行表示的就是全部完成那个按钮。看出了吧两条分支都想在添加的这一行下面插一行,插的内容还不一样 Git 到这就懵了。它面前是同一个位置两份不同的内容一份是说这边要放排序一份是说这边要放全部完成。那到底该放哪一个呢还是说两个都放?两个都放的话谁在前谁在后呢 Git 完全不知道。

既然 Git 不知道我们就得手动处理一下冲突了,那具体该怎么去解决这个冲突呢?老规矩我们先来运行一下 git status 看看当前是一个什么情况。这里面出现了一个我们之前没有见过的区域叫做 Unmerged paths也就是没有合并完成的文件,下面列出的是 index.html 前面还标了 both modified。意思就是这个文件两边都改过而且你看这一部分 Git 非常贴心地在括号里面告诉我们下一步该怎么办那就是 fix conflicts and run git commit。

翻译成中文就是先把冲突给解决掉然后再执行 git commit,你看下面那行还告诉我们如果不想合了还能够执行 git merge --abort 来放弃这次合并。不过我们现在还不想放弃我们这里当然是想要把冲突给解决掉,那具体该怎么做呢?其实冲突就写在 index.html 文件里面我们打开它修改下文件内容对应的冲突也就解决掉了。这里我用 Vim 来打开它你用别的文本编辑器也行比如说是 VS Code 之类的这个就是 Git 留给我们的冲突现场了。

你看这个文件里面多出了三行非常奇怪的符号这些都是 Git 写进去的它们用于标识出现冲突的区域。这三行是把它们所包含的代码分成了上下两个部分让我们一个部分一个部分地看首先看上面这一部分。这一块开头的这一行写的是 HEAD 代表的就是当前分支里面最新的 commit,我们现在是在 master 分支上面刚把排序相关的 commit 给合并下来所以最新的就是处理排序逻辑的。

因此你在这里面看到了排序这两个字是非常正常的事情,这行代码其实就是增加了一个叫做排序的按钮。然后我们再看下面这一截这里面写的是 feature/complete-all 也就是我们要合并进来的那条分支。这个分支处理的是全部完成的逻辑所以这行代码其实就是加上了一个叫做全部完成按钮。总结一下上面是我们 master 分支代码下面是我们要合并进来的分支代码它们俩一个要加排序按钮一个要加全部完成按钮。

按钮的名称不一样冲突也就出现了,现在 Git 完全不知道该怎么办好了那就把两边的说法都原封不动地摆在我们面前然后问我们这儿到底应该怎么办。好看清楚了下面我们就来解决它,解决的办法其实也是非常的简单直接把那几行标记的符号删除然后把这块内容改成我们最终想要的那个样子就可以了。我们是两个按钮都想要那就全部都留下

只把三行标记删掉就行。我们先删 HEAD 这行,然后再删掉中间这行,最后再把 feature/complete-all 这行也给删掉。那全部改好之后,我们保存退出。这样的话冲突就处理完了吗?还没有,我们还得把这次合并给提交一下。其实刚才 Git 已经告诉过我们了,我们回头看一下前面的 git status 的输出里面有这么一段话。这句话就是在说要先解决冲突,然后再执行 git commit 命令。

解决冲突这个事情我们刚才已经在 Vim 编辑器里面做掉了,但是 git commit 这个操作我还没有做。不过在执行 git commit 之前,我们应该还要再执行一下 git add 命令把这个改动加入到暂存区里面。所以我们后面要做的,就是这两件事情:git add 和 git commit。

好,提交成功!我们再执行一下 git status 命令确认一下。好,现在只剩下一句 "nothing to commit, working tree clean",这也就是说没有等着我们需要提交的东西,工作区也是干净的。我们可以再到页面上来看看验证下是不是真的合并好了。可以看到排序和全部完成按钮都出现在了页面上,这说明我们刚才的合并确实是成功了。

刚才我们连做了两次合并。第一次合并只执行了一条 git merge 就合并完成了;第二次合并,我们执行完 git merge 之后还得再执行一次 git commit 才算是完成。为什么第二次要多这么一步呢?这两次合并到底有什么区别?下面我们用动画来看一下。

一开始的时候我们是在 master 分支上开发的,一个 commit 接着一个 commit 地往后提交。后来我们要做排序功能,我们就基于 master 创建了一个排序分支在上面提交了三个 commit;再后来我们基于 master 又创建了一条全部完成的分支,在上面也提交了三个 commit。

先看第一次合并:你看,排序分支就是从 master 现在指的这个 commit 接着往后写的。master 有的东西它全都有,只不过是后面多了三个 commit 而已。那事情就简单多了:master 直接往前挪到排序分支这里就行。这种合并方式叫做 fast forward,翻译成中文就是快进。你看这个词是不是很形象?master 直接快进到排序分支的位置,整个合并就结束了。

再看第二次合并:这个时候 master 已经挪到了排序的最后一个 commit 上,而全部完成还是从 master 原来的位置岔出去的。排序的这三个 commit 它根本就没有!所以这一次就不能像刚才那样快进了。快进就是把 master 直接挪过去,可要是把 master 挪到全部完成分支这里,排序这三个 commit 就不在 master 上了,排序功能就算是丢了。

所以 Git 换了个办法:它在后面新造了一个 commit,让它同时接在两边最新的 commit 后面。这个 commit 就是专门用来把两个分支代码合起来的,然后 master 挪到这个新的 commit 上。这样第二次合并就完成了。

现在可以回答开头的那个问题了,也就是为什么第一次不需要执行 git commit 命令而第二次需要。答案就是:第一次合并是快进,master 挪过去就完了,根本就没有新的 commit,自然也没什么要提交的。

第二次合并的时候 Git 要新造一个 commit,这个 commit 就是专门用来把两边合并起来的。排序和全部完成的代码都在里面。要是两边没有冲突的话,Git 会自己提交这个 commit,我们什么都不用做;可在我们的这个场景下,两个分支改到了同一行,Git 不知道该留哪边的就只好停下来。等我们把冲突改好之后再自己手动执行 git commit 把它给提交上去,这个就是第二次多出来的那一步了。

好,到这里分支、合并、冲突就都讲完了。不过还有个地方值得留意:刚才那个冲突是在哪里爆出来的?就是在合并节点上,也就是功能全部做完最后要合并的那一刻。我们这个功能只有三个 commit,冲突也只有一处,解起来并不费劲。

但是在真实的开发过程中,一个功能往往要做一两周,总共可能要提交几十个 commit。在开发新功能的过程中原来的 master 也在不停地更新。如果等到最后一刻才合并的话,冲突可能会有很多处;更糟糕的是你可能会发现自己的实现方式跟 master 上面的代码根本就不兼容还得返工重写。

那能不能让冲突早一点冒出来呢?当然是可以的!办法就是在开发的过程中时不时地把自己的分支挪到 master 最新的位置后面。比如说我们假设一个功能要做一两个周,里面要提交很多的 commit。在一开始的时候我们从 master 上拉出一条 feature 分支在上面写代码,过了一阵……

master 上合进了别人的新代码。这个时候我们的功能才做了一半还不能合进 master,但是我们可以把 feature 分支挪到 master 最新的位置后面。这样别人的新代码我们就拿到了!

挪的时候要是别人的代码跟我们改了同一个地方的话那一样会有冲突。不过这个时候改动还少,当场就能解决!之后我们在 feature 分支上接着写代码,master 也在继续合进新的代码。又过了一阵我们再把 feature 分支挪到 master 最新的位置后面。

等功能全部做完最后要合并的时候,因为我们的分支一直跟在 master 的最新位置后面这次合并就是一次快进。两行直接并成一行,冲突也早在前面一次次解决掉了!这样一来没做完的代码一直留在自己的分支上不会合进 master 上的新代码,我们又随时都能拿到有冲突也能及时解决。

如果我们的代码跟 master 分支不兼容的话也可以提前感知、提前处理,不用等到最后一次合并的时候才发现。所以在开发的过程中时不时地把自己的分支挪到最新的 master 上去就是更为稳妥的做法!下面我就以我们的项目为例给大家演示一下怎么挪分支。

我们先在图上把时间倒回到合并之前,也就是那个还没有合并节点的时候。这个时候全部完成分支是从这个 commit 岔出去的,而 master 分支已经往前走到了这里。我们要做的就是把全部完成分支挪到 master 分支最新位置的后面,就像是这样!

这种挪动方式在 Git 里面其实有个专门的名字叫做 rebase。这个词拆开就是 re 加上 base:re 就是重新的意思,base 可以理解成地基。说得再简单一点,就是分支岔出去的那个 commit……全部完成分支原来的地基是在这里,现在我们要把它挪到这里。所以 rebase 就是给分支重新换一个地基!下面我们就来实际做一遍:给全部完成分支换个地基。

首先要明确是我们项目现在的状态是这个样子的,两条分支已经合并好了!要真的试一次 rebase 的话就得让项目回到合并之前的状态。方法也很简单:我们只需要把最后用于合并的 commit 删掉!

删掉了之后 master 就会从合并节点退回到这个节点上。你看,这不就是我们合并前的样子吗?在这之后呢我们再去处理 rebase 相关的逻辑。那下面我们就来到终端,先执行这个删除 commit 的操作!

删除的方法呢我们之前在回滚的章节里面讲过,其实就是这个命令。忘了的同学可以到前面看一下!删完了之后我们再回到全部完成分支上。下面我们就要把它的地基换到最新的 master 上了,我们执行的命令是这样子的!

这个返回结果的信息量好像有点大,我们慢慢看。先看 error 开头的这一行:这行就是说没有办法应用“把全部完成按钮加到界面上”这个 commit。注意啊,它说的是一个 commit 而不是整条分支!

刚才我们演示的时候是把整条分支一下子挪过去了,其实那是一个简化的说法。rebase 实际做的并不是把整条分支搬过去,而是把上面的 commit 一个一个地在新地基里面重新做一遍!

我们还是用动画来演示一下刚才 rebase 的过程中发生了什么。首先这是我们 rebase 之前的样子,执行了 git rebase master 命令之后 Git 便会把橙色分支里面的每一个 commit 挨个挪到 master 分支的后面。

比如先挪第一个,这个没问题很顺利地就挪过去了;然后再挪第二个,这个时候就出现冲突了!

冲突出现的原因就在于:这个 commit 会在添加按钮的后面插入一个叫做“全部完成”的按钮,但是之前蓝色的 commit 已经在添加按钮的后面插入了一个排序按钮了。一个要插入全部完成按钮,一个要插入排序按钮,Git 不知道怎么办所以就报冲突了!

这种冲突的解决方式跟之前一样:我们只需要打开文本编辑器告诉 Git 到底应该怎么处理就可以了。之后 Git 就可以完成这个 commit 的迁移,然后再做第三个!

挪完第三 commit 之后全部完成分支本身就会跟着挪过去。这样整个 rebase 操作就完成了!好,通过这个讲解我们可以得知下一步我们需要做的,就是解决这个冲突。

这里我们用 Vim 来打开这个文件,相信这个冲突的样子你已经非常熟悉了:上面是 master 分支的内容,下面是我们正在 rebase 的全部完成分支的内容。我们需要做的就是把 Git 加上去的这三行给去掉,这样就达到了同时保留两个按钮的目的了!

改完之后我们退出然后执行 git add 命令。git add 执行完后,然后我们要做什么呢?有些同学说:我们要做的是 git commit!其实不是啊。在 rebase 的这个场景下面执行了 git add 之后,你要做的事情不是 git commit 而是 git rebase --continue!这一点一定要留意。

好,回车执行!回车之后 Git 会弹出一个编辑器里面是这个 commit 原来的提交说明。我们不用改直接保存退出就行!你看最后一行 "successfully rebased",这说明 rebase 已经顺利完成了。

刚才发生了什么呢?第二个 commit 的冲突我们解决掉了之后 Git 就会自己接着往下走,把剩下的第三个 commit 也搬了过去。第三个 commit 没有什么冲突所以一声不吭地就结束了!最后呢 rebase 也就顺利地结束了。

好,现在我们这条分支已经是在最新的 master 上面了可以接着往下开发了!要是还有别的东西要做就直接在这里面提交 commit 就行。不过我们这个全部完成的功能正好也做完了那就直接地合回 master 吧!

首先我们需要把分支挪到 master 上面然后再去执行 git merge 命令。注意看这次合并用的是 fast forward,也就是快进!这种模式跟合并排序分支时所用的模式是一样的。

因为我们刚才已经给全部完成分支换好了地基,它现在就接在 master 分支最新的那个 commit 后面。所以在这种情况下我们只需要把 master 分支挪到全部完成分支的那个位置上,整个合并就结束了!这个就是 fast forward,根本不需要像之前那样添加一个新的 commit。

好,上一部分我们把分支、合并、冲突还有 rebase 都给讲完了!下面我们再来看另外一个非常常用的工具,就是 stash。老规矩我们先从一个问题开始:现在排序和全部完成这两个功能都已经合并到 master 上面了,我们接着要做下个功能那就是清空已完成。

具体来说我们要做一个按钮点一下就可以把所有已经完成的待办一次性全部删掉!按照前面立的规矩一个功能一个分支,所以我们先来创建一个分支以便处理这个清空已完成的功能。

好,分支创建成功!下面我们就来开发这个功能:我们还是分几个 commit 来完成这功能的啊。我们的第一个 commit 是要把按钮加到界面上!下面我来改一下代码,好代码改完了我们来提交上去用的就是那个经典的 git add 和 git commit 命令!

接着我们来写第二个 commit,也就是那个清空按钮背后所对应的逻辑。我们首先在这个地方加一个函数!好我现在代码写了一半,这个函数还没有写完……就假设在这个时候有个用户反馈了一个问题。

他说他勾选了一条带截止时间待办,然后把它标记成了已完成之后后面的那个日期也被划上了删除线了。但是按照我们的设定它其实不应该被划上!既然用户反馈了我们就去页面那边看一下:我们来加一条带截止时间待办然后再把它勾上,看一下效果!

你看这个待办的文字被删掉了这个是没问题的这本来就是我们想要的结果;但是后面那个日期也被划掉了这个就不符合我们的设计了!我们不希望日期被划掉,这个问题要赶紧修掉我们就切回到 master 分支上去修吧!

哎,切不过去?这是为什么?我们来看报错的第一句:这句话就是在说要是切过去的话你本地的改动就会被覆盖掉!哪个文件的改动会被覆盖掉呢?下面写着就是这个 index.html。记不记得我们刚才有段代码只写了一半还没有提交!

如果我们现在切换到 master 分支的话,我们写了一半的改动就会被 master 分支的代码覆盖掉。这样的话我们的这份没有提交的改动就没了呀!Git 不敢替我们做出这个决定所以它就干脆停下来让我们自己想办法。

那我们该怎么办呢?其实 Git 已经在报错信息里面给了我们两条路了,分别是 commit changes 或者是 stash them!我们来一个一个看啊:首先是第一个 "commit your changes"。

这个 commit changes 的意思就是提交成一个 commit!不过我们刚才那个函数其实才写了一半逻辑都还没写完,提交上去的话就是一个半成品这个就不太好了!还剩下第二条路就是 "stash them"。

这个 stash 就是我们这一节的主角,我们来研究一下!我们还是用前面的三个区域来看一下这个 stash 到底是个什么意思。现在是 index.html 改了一半,这份改动只在工作区暂存区和提交历史里面还没有!

刚才我们切不过去的原因其实就是因为它:因为在切分支的时候 Git 要改写工作区里面的文件这份改动就会被覆盖掉。能不能先把这些改动挪到别的地方放一放,等切完分支之后忙完别的再拿回来呢?当然是可以的!

stash 干的就是这个事情。stash 这个词的意思是藏起来或者说是存起来!其实 .git 里面除了暂存区和提交历史之外还有一块专门用来收东西的地方,它的名字就叫做 stash!之前它一直是空的所以我们就没提它。

在执行了 git stash 命令之后 Git 就会把工作区里面的这份改动整个地挪到 stash 里面!挪走之后 index.html 就回到了上一次提交时的样子,这个时候再切分支就不会覆盖掉任何东西了!

等我们后面忙完别的再把这份改动从 stash 里面取回工作区接着改就行了!我们现在就来试一下……看起来应该是成功了。不过这个返回里面有几个词我们可能得研究一下:首先是 index,它是什么意思呢?

其实它就是暂存区的意思!暂存区在英文里面有两种说法:staging area 或者是 index。之前我们做动画演示的时候只演示了工作区的情况,但注意哦 stash 也会处理暂存区的情况!

换句话说只要是还没有提交成一个 commit 的改动都会被 stash 收走。这里还有一个词啊叫做 WIP,是 work in progress 的缩写!意思就是正在做的活。在 Git 里面正在做的活就是指那些还没有提交的改动,也就是放在工作区或者是暂存区里面的改动!

了解了这两个词的含义之后这一整句话的含义就呼之欲出了:它的意思就是已经把工作区和暂存区里面还没有提交的改动保存起来了!至于后面的这些信息记录的就是它在哪个分支、哪个 commit 上面存的,方便我们以后认出是哪一份!

具体是什么意思呢,我们就不研究了,也不是特别重要。好,看起来 git stash 命令已经执行成功了,我们执行 git status 看一下当前的状态:“nothing to commit, working tree clean”。这个意思就是工作区和暂存区里面,不存在还没有提交的改动了。那就说明我们刚才的 git stash 命令就生效了。

我们再切回到编辑器里面看一眼,之前我们在这个地方有个写了一半的函数,现在这块代码也不见了,文件又回到了上一次提交的样子。这说明我们的那个 stash 命令生效了,所有还没有提交的改动都被收起来了。现在改动已经收干净了,我们再来切换一下分支试试,这次就顺利地切过去了,因为工作区是干净的,Git 改写文件也不会覆盖掉任何东西。

我们现在就着手去修复用户所说的这个问题吧。不过我们最好不要直接在 master 上面改,之前讲分支的时候说过,master 是主分支,大家都希望它随时都是能够正常使用的状态。如果我们在 master 分支上面直接去修复这个 bug 的话,很有可能会把它已有的功能也给修掉。

所以更规范的做法是,从 master 上面再拉一条分支出来,在那条分支上改,确认没有问题了再合回 master。现在我们就来基于 master 创建一个新的分支:git checkout -b hotfix/bug-fix-1。这里面的 hotfix 就是紧急修复的意思,它跟我们之前用的那个 feature 一样,都是一种命名习惯而已。hotfix 就是用来表示这条分支是用来修 bug 的。

下面我们就来修一下这个 bug,具体的修补过程我就不演示了,我们来快进一下。代码已经改完了,我们去页面里面看看:同样加一条带截止时间待办,然后再完成它。可以看出日期不再被划掉了,我们的改动生效了。我们再把它给提交上去,用的就是经典的 git add 和 git commit 命令。

提交完成之后,我们再把这部分改动合回 master:git checkout master && git merge hotfix/bug-fix-1。合并完成,我们再把这条用完的分支给删掉:git branch -d hotfix/bug-fix-1。好,现在这个 bug 算是彻底处理完成了。下面我们就回到自己原来的那条分支上面,接着写还没完成的那个功能:git checkout feature/todo-list。

好,我们已经切换到原来的分支了。那现在问题来了:我们之前的代码已经被 git stash 命令收起来了,该怎么把它弄回来呢?很简单,我们就执行这个命令就行:git stash pop。pop 这个词是弹出来的意思,git stash pop 就是把 stash 里面存着的那份改动取回工作区。对的就是动画里面的这个操作。

好,下面就让我们回车执行这个命令。这上面有一大段输出,其实就是在告诉我们 index.html 又变成了那种有改动但还没有暂存的状态了。也就是说,我们那个半成品已经回来了。我们用 git diff 命令来验证一下:git diff index.html。

没问题,你看这里面有一个叫做 clearCompleted 函数,这个就是我们之前写了一半的函数。当前这个状态就跟我们收起来之前是一模一样的。那接下来我们就可以继续写了,后面具体写代码的过程我就不演示了,同样我们快进一下。好,代码写完了,我们来页面上看一下效果:我们首先创建一个待办,然后把它标记为完成。

再点击一下我们新创建的这个清空已完成按钮:点击 clear completed 按钮。没问题,已经完成的待办都已经被删除掉了,说明我们这个代码已经生效了。下面我们就把这部分改动给提交上去:git add . && git commit -m "feat: implement clear completed function"。

提交完毕之后,我们再把这部分改动合回 master:git checkout master && git merge feature/todo-list。合并顺利完成,那 stash 我们就讲到这里了。好,stash 我们已经会用了,它可以把没有写完的代码先收起来,让工作区变干净。然后我们就可以去干别的了,干完了再把代码取回来。

所以 stash 很适合临时打断一下的情况:先去干点别的,然后很快就回来。但有些情况就不太适合了,比如说是同时开发多个分支。为什么呢?因为同时开发多个分支的话,就是这边要写一会儿,那边要写一会儿。如果要用 stash 的话,就得频繁的去敲 git stash 和 git stash pop 命令,在分支之间来来回回的折腾。这就很麻烦了。

这个问题的根源在于:我们只有一个项目目录,在同一时刻,它里面只能够放一条分支的代码。那我们换个思路啊:能不能再开一个目录呢?原来的目录里面,写到一半的代码原封不动的放着。我们到新开的目录里面去做别的事情,两个目录之间互不干扰。这个是可以的。

我们还是看之前的三个区域的图:现在只有一个目录,它就是我们的工作区,暂存区和提交历史都在 .git 里面。现在我们再开一个目录,就相当于多了一个工作区。不仅如此,这个工作区还有一份自己独有的暂存区。两个暂存区之间互不干扰。不过要注意提交历史只有一份,两边是共用的。

所以无论是在哪边提交 commit 都会记到一份历史里面。像这样:一个工作区加上自己的暂存区,就是一个 worktree 了。所以在我们这个图里面,有两个 worktree。你要记住啊:每一个 worktree 都有自己独立的工作区和暂存区,但是所有的 worktree 都共用同一份提交历史。这句话是这一节的核心。

好,概念说完了,我们直接上手试试。现在假设我们要做一个导出功能:也就是在页面上放一个导出按钮,点了之后就可以把当前的待办列表给导出来。按照前面的规矩:我们先从 master 拉一条新分支出来,然后接下来的活就在新分支上面干了。第一个 commit:先把按钮给加上去。

好,我们改完了到页面上看看:按钮出来了。不过现在点它还没有任何反应,因为导出的逻辑我们一行还没写。先把这个页面改动给提交上去:git add . && git commit -m "feat: add export button"。页面处理完了,我们再来接着写导出的逻辑:修改 index.html 添加导出功能代码。

现在我们写了一半,假设在这个时候工作上插进来了一件事情:产品那边想新增一个统计功能,用于在页面上显示一共有几项待办、还剩几项待办之类的一些信息。而且功能比较急,下周就要上线。但是吧我们的导出功能才写了一半,也不可能说扔就扔。

按照之前的方法:我们可以使用 stash 先把手上的这些没有提交的改动给存起来,然后再切到负责统计的分支上开发统计功能。不过这次我们就换一个做法:使用 worktree 来实现。我们运行一下这个命令来新建一个 worktree:git worktree add ../mark-todo-stats feature/stats。

这个命令看起来很长啊,不过它的目的很简单:其实就是要创建一个 worktree。这从 git worktree add 这三个单词也能看出来。只不过你想,要是想创建 worktree 的话至少有两个东西需要指定:第一个是这个 worktree 对应的分支是什么,第二个是这个 worktree 要放在哪一个目录里面。所以后面的这一大长串就是为了说明这两点,我们一个一个看。

首先我们来看第一个问题:这个 worktree 对应的分支是什么?答案有点散,是在这两个地方指定的。这其实就是在说:要基于 master 分支创建一个叫做 feature/stats 的分支,然后我们的 worktree 就负责存储该分支的代码。worktree 对应的分支明白了,那下一个问题:这个 worktree 要放在哪一个目录里?

答案就在这里:“../mark-todo-stats”。.. 代表父级目录,所以这个地方就在说:在父级目录里面创建一个叫做“马克待办(统计)”的文件夹,我们的 worktree 代码就放在这个目录里面。相信现在你已经彻底明白这行代码的意思了,下面我们就到访达里面看一眼确认一下目录的 worktree 是不是真的创建好了。

你看这里面,真的多出来了一个文件夹:就是我们刚才创建的那个文件夹。这说明我们的命令真的生效了。下面我们来到终端,执行一下这个命令:git worktree list 看看目前一共有多少个 worktree。诶路径怎么变成这样了?这其实不是报错,也不是终端坏了:是 Git 在打印路径的时候默认会把中文转义成这种八进制的编码。这个算是 Git 的历史遗留问题了,我们再来执行个命令不让它转义就可以了:export GIT_ASKPASS=echo。

执行完了我们再跑一下之前的 git worktree list 的命令:git worktree list --porcelain。好,这下正常了。可以看出目前有两个 worktree 对应的分支名也写在每行的最后了: /workpath/mark-todo .gitdir=/workpath/.git/worktrees/mark-todo refs/heads/master HEAD=... /workpath/mark-todo-stats .gitdir=/workpath/.git/worktrees/stats refs/heads/master HEAD=...

注意第一行,它就是我们原来的目录。所以你看我们原来的目录本身也是一个 worktree:其实从一开始我们就一直在用 worktree,只不过呢之前一直只有一个 worktree。现在是两个:一个负责导出,另外一个负责统计。好,worktree 基本上是搞懂了,现在我们需要干活了:我们到新目录里面把这个统计的功能给做出来。

我们来打开新目录里面的 index.html 改下代码:在 mark-todo-stats/index.html 中添加统计逻辑。大家稍等一下,我们在新目录里面提交:先 cd 到这个目录里面去(cd ../mark-todo-stats),然后执行 git add 和 git commit 命令把它提交上去:git add . && git commit -m "feat: implement stats feature"。

提交完了,顺便看一下现在哪些分支:一共是三条分支,前面的符号各不相同。星号我们熟就代表当前所在的分支,我们现在就在 feature/stats 这个统计分支上:git branch。feature/export 前面的不是空格是一个加号,加号就表示对应的分支正在另外一个 worktree 里面打开着:也就是我们原来的那个目录,写了一半的导出功能就放在那。

好统计做完了:而且这个是产品催着要的,下一周就要上线了。那就别等了现在就合回 master:git checkout ../mark-todo && git merge feature/stats --no-ff。worktree 里面新建的分支是一条普通的分支,所以合并的方法跟前面一模一样:我们先切到 master 分支上来然后再把它给合并进来。

合并完成,统计就这么交差了:既然已经合进 master feature/stats 这个分支也就没用了我们顺手来删掉它。不过终端现在还停在这个新创建的目录里,如果直接把它删掉终端所在的位置也就不存在了后面执行命令可能会出现一些问题。所以我们先用 cd 回到原来的马克待办目录,然后再删除它:cd ../mark-todo && git worktree remove ../mark-todo-stats。

那怎么删除呢?有些同学可能会想:不就是个目录吗,把它拖进废纸篓里面不就行了吗?其实这么做不太行啊:主要是因为 Git 那边也存储了这个文件夹的相关信息。如果直接把文件拖进废纸篓里面的话,只有文件被删除了 Git 那边的记录还在。这就会出现一些问题:比如后续无法正确识别 worktree,或者产生冲突。

所以我们需要把这个文件夹和对应的 Git 记录一起删除:说白了就是把整个 worktree 给删掉,对应的命令就是这个。执行成功之后没有任何输出,这个是正常的:我们来看一下现在还剩几个 worktree:git worktree list。可以看出只剩下原来那一个 worktree 了,访达里面的那个文件夹也一并消失了:ls ../mark-todo-stats。

那现在停一下,你可能会有点慌:你想我们新目录里面写着统计功能,不过现在那个新目录已经删了分支也删了。那它的代码是不是也就跟着没了呢?其实没有:我们来看一下 Git 的提交历史 git log --oneline。我们的统计功能就在这个地方没有丢:你看这个统计功能是在新目录里面提交的,在新目录里面合进 master 的。

然后现在新目录已经删掉了:但是我们在原来的目录里面仍然能够看到对应的 commit。这也印证了我们前面所说的那句话:两个目录用的是同一个 Git 的仓库,提交历史只有一份。这也是 worktree 跟直接复制一份文件夹最大的区别:新增的 worktree 跟原来的那个 worktree 共用同一个仓库。

无论是在哪边提交,另一边马上就能看到:就算是把新目录给删了它在里面提交代码在原来那个目录里依然能够找到。这跟单纯的复制粘贴文件夹是不一样的:如果你在访达里面把原来的项目目录复制一份的话得到的会是两个各自独立的仓库。你在一边提交了一些代码另一边是完全不知道的:它们没有共享 .git 目录,因此无法同步历史。

统计这个功能我们就写到这里了:现在可以不慌不忙的回来写原来的导出功能了。我们回到原目录继续开发 export 分支:git checkout feature/export && git add . && git commit -m "feat: implement export logic"。写完了先试试能不能用:在浏览器中测试导出功能,点击按钮下载文件。

能用:下载下来的文件里面就是我们的两条待办一行一条没问题我们提交 git add . && git commit -m "feat: complete export feature"。然后我们把导出功能合并到 master 分支上:git checkout master && git merge feature/export --no-ff。现在导出功能已经合并到 master 上面了:git log --oneline | head -n 5。

这样的话之前的导出分支也没什么用了我们来把它删掉:git branch -d feature/export。好删完了,这下导出功能算是彻底做完了。之前我们的代码都是用 Git 管理的:这样我们就可以清楚地了解项目的开发历史并能够随时回滚到之前的任意可用的状态。

不过这里面还有一个很大的问题:你看我们之前无论是代码还是 Git 相关的记录都是放在自己的电脑上的。万一哪天电脑坏了或者是项目文件夹被误删了那这些东西都没了:之前记录得再清楚也没用。所以我们需要在网上再存一份保存代码最常用的地方就是 GitHub:有些同学可能搞不清楚 Git 和 GitHub 是什么关系。

这里我简单说明一下:Git 是安装在你电脑上的一个软件,负责记录你对项目所做的每一次改动。而 GitHub 则是用来存储代码和 Git 记录的一个网站:有了 GitHub 之后我们就可以把项目的 Git 记录和对应的代码完整地传一份到 GitHub 上。这样即使本地的代码被删掉了我们也可以从 GitHub 上面把这些数据给拉回来:所以 Git 是一个本地软件,GitHub 是一个远程网站。一个本地一个远程这个就是他们的最大区别:接下来我们要做的就是把电脑上面的项目传一份到 GitHub 上面去。

既然要把代码传到 GitHub 上,首先你就需要一个 GitHub 账号:大家直接去 GitHub 官网注册就行了这里就不演示了。账号有了,下面我们就要做一些配置:以便让 GitHub 能够验证我们的身份。这样以后从这台电脑往 GitHub 上面推代码的时候GitHub 才能确认确实是你在操作而不是别人在冒充你。那怎么让 GitHub 确认我们的身份呢?

以前可以直接输 GitHub 的账号密码:这算是最直接的方式了。但是现在 GitHub 已经不支持了(针对命令行操作):目前比较常用的办法是 SSH key。你可以把 SSH key 理解成一种密码的替代品,只不过它不是你要记住的一串字符:而是两个文件。一个叫做私钥另外一个叫做公钥,整个身份验证的过程是这样子的。

你首先要在自己的电脑上面把这两个文件给生成出来:私钥留在本地绝对不能公开也不能够发给任何人。公钥则发给 GitHub 一份它是可以公开的:名字里面的“公”就是公开的意思。之后你每次往 GitHub 上面推代码的时候你的电脑都会用私钥生成一个特殊的标记跟着代码一块发过去。这个标记有个特点:只有你的私钥才能做得出来别人仿不了就像你在文件上面签的字一样。

所以这个标记一般就叫做签名:GitHub 收到之后就会拿你之前交给它的公钥来检查这个签名。公钥能认出这个签名到底是不是它配的那把私钥做出来的:只要能对得上就说明签名确实是来自你的私钥。而私钥只有你才有,所以 GitHub 就知道这个代码就是你推过来的:这就是整个过程了。要是这一段你没太听明白的话也没有关系不重要,你就记住一件事就行了:我们要生成两个文件分别是私钥和公钥。

私钥留在自己的电脑上面,公钥发给 GitHub:下面我们来到终端生成这两个文件。我们需要执行的命令是这个 ssh-keygen 就是生成 SSH key 的命令:ssh-keygen -t ed25519 -C "[email protected]"。这里面的 gen 就是 generate 的缩写:-t ed25519 是指定 SSH key 的类型。这个是 GitHub 推荐类型,你不用管什么意思照着写就行了:然后后面的 -C 是一段备注。

一般情况下写的就是自己的邮箱以方便后面认出来这个 key 到底是谁的:回车之后它会问我们几个问题。第一个问题是生成的文件放在哪,括号里面是默认的位置:也就是用户目录下面的 .ssh 文件夹里面的 id_ed25519。我们直接回车用默认的就行了:接着它问我们要不要给私钥设个密码?

设了更安全但是用起来会更麻烦一些:自己一个人用的电脑的话不设也没有关系。所以这个地方我们按回车直接跳过去就行了:这个密码还需要确认所以我们还需要再按一次回车。好生成好了,主要是要看前两行:这个呢是私钥(id_ed25519),然后这个呢是公钥:公钥里面的这个 .pub 就是 public 的意思。下面的那一串指纹和那个图案我们暂时就不用管了:好现在两个文件都生成完毕了。

下面我们就来看一下公钥的内容以便把它上传到 GitHub 上面去:cat ~/.ssh/id_ed25519.pub。复制输出内容,登录 GitHub 进入 Settings -> SSH and GPG keys -> New SSH key:粘贴公钥并保存。配置完成后,初始化本地 Git 仓库(如果尚未初始化),添加远程 origin:git remote add origin [email protected]:username/repository.git。最后推送代码到 GitHub:git push -u origin master。

这里面的 cat 的命令,就是用来把文件内容显示出来的。后面接的就是公钥的文件路径,所以输出结果就是公钥的内容了。我们把这个内容复制下来,然后再回到 GitHub 这边,点击右上角的头像,再点击 Settings。在左边找到 SSH and GPG keys,再点击 New SSH key。然后我们直接把公钥的内容粘贴在这个文本框里面,再点击 Add SSH key。现在公钥就算是发给 GitHub 了。

我们回到终端测试一下有没有生效,这个命令就是尝试去连接一下 GitHub。第一次连的时候可能会出现这样的一个提示,意思就是说这台电脑以前没有连过 github.com,没有办法确认它的身份,问你还要不要继续连。这个是正常的,我们输入 yes 回车,以后就不会再问了。

好出现这个提示就说明我们连接 GitHub 已经成功了。到这里这台电脑的 SSH key 就已经配置好了,以后我们推代码的时候,GitHub 就能够用你添加的公钥来验证身份。

那身份验证相关的工作做完了之后,下面我们就在 GitHub 上面创建一个仓库,后面我们就会把本地代码推送到这个仓库里面。那让我们回到 GitHub 页面这里,然后再点击我们的头像选择里面的 Repositories,再点击右上角的 New。这样我们就进入到了创建仓库的页面。

我们一起来看一下这里面的 Repository name 是仓库的名称,我就写 MarkTodo 就行了。仓库名不能用中文,所以没法直接叫马克待办。中文名可以写在下面的 Description 里面。再往下是 Choose visibility,默认是 Public,也就是公开仓库。

如果你不希望公开给别人看的话可以改成 Private,这样的话只有自己能看到。然后下面还有三个选项分别是 Add README、Add .gitignore 和 Add license,我们都不加。因为我们的目标就是把本地代码推送到 GitHub 上面,不需要 GitHub 额外给我们添加什么文件。

最后我们点击 Create repository,仓库就创建好了。目前这还是一个空的仓库,GitHub 在页面上面给了我们一段提示。我们看中间这一段 Push an existing repository from the command line,意思就是说如果你已经有了一个仓库了可以在命令行里面这样把它推上来。

下面就是具体推送的命令了,这种情况正好是我们所需要的那种。下面我就来给大家演示如何使用这三条命令把我们本地的项目代码推送到新创建的 GitHub 仓库中。

下面我们一条条看,我们首先复制第一条要执行的命令然后来到终端粘贴。这行命令就是让 Git 把我们刚才创建的 GitHub 仓库的地址记下来,并且给它个名称就叫做 origin。以后我们在这个项目里面说 origin Git 就知道指的就是这个 GitHub 仓库,不用每次都写那么一长串的地址。

你可能比较好奇为什么叫做 origin,这个名字有点奇怪其实吧这只是大家的习惯而已你取别的名字也是可以的。这个命令执行完后什么都不返回那应该是成功了。我们再回到页面上刚才我们执行完了第一个命令,下面我们要执行的是不是第二个命令呢?

其实吧第二条命令我们不需要执行。因为第二条命令的功能是把主分支名从 master 改成 main,我们就用 master 就行了不用改了。所以这一条跳过下面我们来复制第三行代码这行代码就是用来把本地的项目代码和 Git 记录推送到 GitHub 上面的。

我们复制一下然后来到终端粘贴不过这里有一点要注意这个命令里面写的是 main,不过我们的主分支名不是 main 而是 master所以我们需要改一下。然后我来给大家解释一下这行代码是什么意思 push 是推送的意思,所以这条命令就是说把本地的 master 分支推送到 origin 也就是 GitHub 上面的仓库。

这里 -u 的目的是把本地的 master 跟 GitHub 上面的 master 绑定起来绑定了之后以后在 master 上推代码的时候直接执行 git push 就行了。对以后推送命令就剩两个单词了就不用再写这么一大长串了好我们来执行。

推送完了我们回到 GitHub 刷新一下可以看到刚才的这些提示都不见了页面变成了我们项目的样子。中间是我们的三个项目文件下面还显示出了 README 的内容。GitHub 会自动把 README 展示在仓库首页上,所以大家一般都用这个文件来介绍自己的项目。

你看这里还写了 22 个 commits,我们点进去看一下可以看到我们本地的 Git 提交记录全都在这跟我们在 git log 里面看到的一模一样。所以 push 上去的不仅是现在的这几个文件而是整个 Git 仓库的历史。

第一次推送上去之后日常使用就简单了,本地该怎么开发还怎么改完之后照样 git add、git commit。只不过最后还要再加一步那就是执行 git push 把最新的 commit 推送到 GitHub 上面来。我们来试一下我们把这个流程过一遍。

我们目前的 README.md 里面的功能列表还是最早那几条,后来加的那些什么截止时间、排序这些功能都没有写进去。我们来把它给补上,补完了之后我们提交首先是运行 git add 和 git commit 命令。

好执行完了,我们用 git status 命令看一下当前的情况。你看这里面多了一句什么 Your branch is ahead of origin/master by 1 commit,意思就是本地的 master 比 GitHub 上的 master 多了一个 commit。也就是刚才的这个 commit 还在本地没有传到 GitHub,括号里面 Git 也提示了可以用 git push 把它给推上去。我们来执行一下。

看起来推送成功了,我们再看一眼 git status。注意这句话 Your branch is up to date with origin/master。origin/master 就是 GitHub 仓库上面的那个 master 分支,所以这句话就是说我们本地的 master 代码跟 GitHub 上面的 master 代码完全一样。

也就是说我们的推送成功了,所以我们日常的流程就是先改代码,改完代码之后我们执行三个命令分别是 git add、git commit 和 git push。这样不仅可以保证 Git 上面存有了我们完整的项目开发历史,还能够保证 GitHub 上面一直会有一个最新的备份即使丢了也没关系。

最后我们再来验证一下我们的 GitHub 上面的备份到底管不管用,假设有一天项目文件夹不小心被删了,我们能不能把这些代码给找回来?我们模拟一下这个情况首先把马克待办这个文件夹删掉。你看现在本地的代码和提交记录都没了,要找回来的话那我们就要从 GitHub 这边下手了。

首先我们来到 GitHub 页面然后我们需要点击这里面的 Code,我们确保这里面选的是 SSH。然后再点击复制按钮复制仓库的地址之后我们来到终端先进入到桌面,然后输入命令 git clone 后面加上我们在 GitHub 上面复制好的仓库地址。

然后再加上马克待办这几个字,这个命令就是用来把 GitHub 上面的仓库拉到我们本地上来的。并且拉完了之后我们希望对应的文件夹的名字叫做马克待办,而不是原来的那个 MarkTodo。

好看起来应该是执行完毕了我们进入到这个文件夹里面可以看到所有的提交记录都回来了。我们再打开 index.html 可以发现页面也跟之前一模一样。好 GitHub 就讲到这里了,到这里我们本期视频就彻底结束了我是马克,我们下期再见拜拜。