git 学习笔记

322 阅读15分钟

以下内容是对掘金小册《Git原理详解及实用指南》的学习笔记。

VCS (Version Control System)

一个代码版本控制系统需要拥有以下三个特征:

版本控制、主动提交、中央仓库

特征

版本控制

提供保存文件修改历史的功能。最简化的版本控制模型,是大多数主流文本编辑器都有的「撤销(Undo)」功能。

在写代码时,版本控制可以让开发人员方便地回溯历史版本,查看每一次的修改信息。

主动提交

如果代码的版本控制系统像编辑器那样,每次修改都是被动保存,那么会导致版本信息杂乱无章。所以代码的版本控制系统还要提供给开发人员主动提交改动的机制,也就是让开发人员来控制每次改动的存储颗粒度和改动信息。

中央仓库

当然写代码不仅仅是一个人写,代码的版本控制系统还要提供给开发人员合作的机制,那就是提供一个中央仓库作为代码的存储中心。所有合作的开发者将自己对代码的改动上传到该代码库并且从该代码库拉取最新的代码。

Centralized VCS 中央式版本控制系统

所有的历史改动信息都存储在中央仓库,本地并不存储历史改动信息,例如SVN。

工作模型

  1. 主工程师搭建了项目框架,并且将代码上传到中央仓库
  2. 开发人员将代码从中央仓库拉到本地
  3. 每个开发人员开发独立的功能,开发好之后将代码上传到中央仓库
  4. 每当有人上传了代码,其他的开发者就从中央仓库将最新的代码拉到本地。

DVCS(distributed VCS)  分布式版本控制系统

每一台下载了代码库的电脑都拥有历史改动信息

中央式 VCS 的中央仓库有两个主要功能:保存版本历史、同步团队代码。而在分布式VCS中,保存版本历史的工作转交到了每个团队成员的本地仓库中,中央仓库就只剩下了同步团队代码这一个主要任务。它的中央仓库依然也保存了历史版本,但这份历史版本更多的是作为团队间的同步中转站。

工作模型

其实和中央式版本控制的工作模型差不多。只不过在提交修改的时候,可以存储在本地,本地也有了回溯历史版本的功能,而不需要联网。简而言之,就是把代码的提交修改和上传分开了。

优点和缺点

优点
  1. 速度快,而且无需联网。本地存储了所有的历史改动信息,可以快速查看历史信息,并且无需联网就可以在本地提交修改(当然这个提交的修改是还没有上传到中央仓库的)。
  2. 可以将代码提交的颗粒度做得更细,方便review和回溯(这点其实需要多想一层,在中央式版本控制系统中,你当然也可以将每次提交做得很细,但是如果你提交的颗粒度很小,代码功能可能还未完全实现,这时,合作的开发者看到有新的更新,就将新代码拉到本地,可能导致运行出错或者合作者对代码产生疑问)。
缺点

这里需要先了解到,代码库不仅仅存储代码本省,还存储了所有的改动信息。

因为本地的代码库包含了所有的历史改动信息,并且因为每次提交的颗粒度比中央式版本控制系统小导致改动信息比中央式版本控制系统多的多,所以分布式版本控制系统代码库占用本地空间会比较大。

对于文本,vcs可以利用算法压缩每次改动信息和文本本身。所以纯文本的代码库不会很大。

但是许多存储游戏的代码库还是选择中央式版本控制系统,原因是游戏的代码库往往存有非常多多媒体文件,这会导致存储改动信息需要大量的空间。选择中央式版本控制系统,则会将所有的版本信息存储在中央仓库,本地只存储有代码和多媒体文件就可以了。

几个基本命令和概念

将代码从远端拉下来

$ git clone 地址

查看所有commit的基本信息

$ git log

查看当前工作目录状态

$ git status

将代码提交到staged area(暂存区域)

$ git add 文件名

所谓的 staging area,是 .git 目录下一个叫做 index 的文件(嗯,它的文件名并不叫 stage )。

提交修改

$ git commit

然后会进入一个用来填写提交信息(commit message)的界面。根据操作系统以及设置的不同,这个界面的编辑器可能是 nano 或者 vi。

在mac上,按下任何键就会进入插入模式,然后输入信息,再按ESC,再按两次大写Z,就退出了编辑器,并且完成了commit操作。

origin/master

origin/master的中的 origin 是远端仓库的名称,在用 clone 指令初始化本地仓库时 Git 自动帮你起的默认名称;masterorigin 上的分支名称。

把本地提交发布(即上传到中央仓库)

$ git push

团队工作的基本工作模型

Git 的管理是目录级别,而不是设备级别的。

简单模型

同事 commit 代码到他的本地,并 push 到中央仓库, 你将最新代码 pull 到本地。然后你再 commit 代码到自己的本地,再push到中央仓库,同事再pull 你的代码到他的本地,这样来来回回。

这种合作有一个严重的问题:同一时间内,只能有一个人在工作(具体来说,只能有一个人在本地 commit)。另一个人必须等着他把工作做完,代码 push 到 中央仓库 以后,才能把 push 上去的代码 pull 到自己的本地。而如果同时做事,就会发生冲突:当一个人先于另一个人 push 代码(这种情况必然会发生),那么后 push 的这个人就会由于中央仓库上含有本地没有的commit而导致 push 失败。

为什么会失败?

因为 Git 的push 其实是用本地仓库的 commits 记录去覆盖远端仓库的 commits 记录(注:这是简化概念后的说法,push 的实质和这个说法略有不同),而如果在远端仓库含有本地没有的 commits 的时候,push (如果成功)将会导致远端的 commits 被擦掉。这种结果当然是不可行的,因此 Git 会在 push 的时候进行检查,如果出现这样的情况,push 就会失败。

这里必须要了解的是,每个commit都有自己的唯一码--它的 SHA-1 校验和。这个唯一码无论是在本地还是还是中央仓库都是一样的,通过这种方式来识别commit 的异同。

简单模型2.0

上面提到,第一种简单模型,同一时间内只能有一个人在本地commit,这显然是不合适的,也失去了 git 提交颗粒度小这个优势。所以我们来优化一下这个模型。

假设两个合作的人都在本地提交了commit, 这时第一个人将代码 push 到中央仓库。第二个人这时也要将代码 push 到中央仓库,就会报错。错误显示:

Updates were rejected because the remote contains work you don't have locally. This is usually casued by another reppository pushing to the same refs. You may want to first integrate the remote changes.

意思就是,中央仓库包含着本地没有的commit。所以第二个人需要先用 pull 将代码拉下来。

运行

$ git pull

但这次的pull 操作没有像上面的简单模型一样直接结束。因为git发现,不仅中央仓库含有本地没有的commit, 本地也含有中央仓库没有的commit。所以git就会把远端和本地的独有 commits 进行合并(merge),自动生成一个新的 commit。然后进入一个填写 commit 信息的页面

填写完成之后,就生成了一个新的 commit

这时再将本地代码 push 到中央仓库。

git pull 的操作其实是两个操作的合并,等于下面的操作

$ git fetch origin/master
$ git merge origin/master

HEAD、master 和 branch

这三个都是对于 具体的 commit引用

HEAD

指向当前 commit 的引用。所谓当前 commit这个概念很简单,它指的就是当前工作目录所对应的 commit。

每次当有新的 commit 的时候,工作目录自动与最新的 commit 对应;而与此同时,HEAD 也会转而指向最新的 commit。

master

master也是一个对于 具体 commit 的引用。

新创建的 repository(仓库)是没有任何 commit 的。但在它创建第一个 commit 时,会把 master 指向它,并把 HEAD 指向 master。

当有人使用 git clone 时,除了从远程仓库把 .git 这个仓库目录下载到工作目录中,还会 checkout (签出) master(checkout 的意思就是把某个 commit 作为当前 commit,把 HEAD 移动过去,并把工作目录的文件内容替换成这个 commit 所对应的内容)。

branch

HEAD 除了可以指向 master, 还可以指向其它的分支。master其实也是一个分支,跟其它分支相比没有什么特别之处。

每次执行 git commit 的时候,HEAD会拉着当前指向的分支一起往前指向最新的 commit

你还可以把一个 branch 理解为从初始 commit 到 branch 所指向的 commit 之间的所有 commits 的一个「串」。branch 包含了从初始 commit 到它的所有路径,而不是一条路径。这些路径之间也是彼此平等的。

上图中 master 合并了 branch之后,就包含了两条路径,1、2、3、4、7和1、2、5、6、7,这两个路径是相等的,而且都是可以通过 master 访问到的。

创建 branch

$ git branch branch-name
$ git checkout branch-name

或者

$ git checkout -b branch-name

删除 branch

$ git branch -d feature1

2点注意事项:

  1. HEAD 指向的 branch 不能删除。如果要删除 HEAD 指向的 branch,需要先用 checkout 把 HEAD 指向其他地方。
  2. 由于 branch 只是一个引用,所以删除 branch 的操作也只会删掉这个引用,并不会删除任何的 commit。(不过如果一个 commit 不在任何一个 branch 的「路径」上,或者换句话说,如果没有任何一个 branch 可以回溯到这条 commit(也许可以称为野生 commit?),那么在一定时间后,它会被 Git 的回收机制删除掉。)

出于安全考虑,没有被合并到 master 过的 branch 在删除时会失败(因为怕你误删掉「未完成」的 branch 啊):

这种情况如果你确认是要删除这个 branch (例如某个未完成的功能被团队确认永久毙掉了,不再做了),可以把 -d 改成 -D,小写换成大写,就能删除了。

引用的本质

所谓「引用」(reference),其实就是一个个的字符串。这个字符串可以是一个 commit 的 SHA-1 码(例:c08de9a4d8771144cd23986f9f76c4ed729e69b0),也可以是一个 branch(例:ref: refs/heads/feature3)。

Git 中的 HEAD 和每一个 branch 以及其他的引用,都是以文本文件的形式存储在本地仓库 .git 目录中,而 Git 在工作的时候,就是通过这些文本文件的内容来判断这些所谓的「引用」是指向谁的

push

push 做的事是将当前 branch 路径上的commit 包括它自身指向的commit 上传到远端仓库。

push master

直接在 master 分支下运行 git push 就可以了

push 其它的分支

$ git checkout feature1
$ git push origin feature1

在 Git 中(2.0 及它之后的版本),默认情况下,你用不加参数的 git push 只能上传那些之前从远端 clone 下来或者 pull 下来的分支。

如果需要 push 你本地的自己创建的分支,则需要手动指定目标仓库和目标分支(并且目标分支的名称必须和本地分支完全相同)

你可以通过 git config 指令来设置 push.default 的值来改变 push 的行为逻辑,例如可以设置为「所有分支都可以用 git push 来直接 push,目标自动指向 origin 仓库的同名分支」(对应的 push.default 值:current),或者别的什么行为逻辑,你甚至可以设置为每次执行 git push 时就自动把所有本地分支全部同步到远程仓库(虽然这可能有点耗时和危险)。

在 feature1 被 push 时,远程仓库的 HEAD 并没有和本地仓库的 HEAD 一样指向 feature1。这是因为,push 的时候只会上传当前的 branch 的指向,并不会把本地的 HEAD 的指向也一起上传到远程仓库。事实上,远程仓库的 HEAD 是永远指向它的默认分支(即 master,如果不修改它的名称的话),并会随着master分支的移动而移动的。

merge

指定一个 commit 合并到当前 commit(HEAD) 中来。

具体的是:从目标 commit 和 当前 commit 分叉的位置起,把目标 commit 路径上的 commit 的所有内容合并到当前commit,并生成一个新的commit。

使用场景

  1. 合并分支
  2. pull 的内部操作

情况一:合并冲突

以下两种情况 合并分支会自动完成修改:

  1. 如果一个分支改了 A 文件,另一个分支改了 B 文件。
  2. 如果两个分支都改了同一个文件,但一个改的是第 1 行,另一个改的是第 2 行,那么合并后就是第 1 行和第 2 行都改,也是自动完成。

如果两个分支修改了同一部分内容,merge 的自动算法就搞不定了。这种情况 Git 称之为:冲突

解决冲突

  1. 解决掉冲突
  2. 手动 commit 一下

merge 时如果产生冲突,git会标记产生冲突的地方,git会将两个分支冲突的内容放在一起,并且标记出边界与分支名称。

在冲突部分,将决定保留的保留下来,删掉决定删掉的地方,保存,然后再 执行 add 和 commit,这时冲突就被解决了,并且创建了一个新的 commit,将原来的两个分支合并在了一起。

被冲突中断的 merge,在手动 commit 的时候依然会自动填写提交信息。这是因为在发生冲突后,Git 仓库处于一个「merge 冲突待解决」的中间状态,在这种状态下 commit,Git 就会自动地帮你添加「这是一个 merge commit」的提交信息。

想要放弃 merge?

$ git merge -abort

输入这行代码,Git 仓库就会回到 merge 前的状态。

情况二:HEAD 领先于 目标 commit

当 HEAD 和目标 commit 不存在分叉,并且领先于 目标 commit,那么执行 merge 其实就是空操作。

情况三:HEAD 落后于 目标 commit (fast-forward)

将 HEAD 连带着 branch 移动到 目标 commit

这种操作有一个专有称谓,叫做 "fast-forward"(快速前移)。

fast-forward的用处

本地的 master 没有新提交,而远端仓库的 master 有新提交,这时在本地执行 git pull 就会执行 fast-forward的操作。

origin/master 和 origin/HEAD 是对远端仓库的 master 和 HEAD 的本地镜像, 在执行 git fetch 的时候,本地的镜像会得到相应的更新,也就是origin/master 和 origin/HEAD 移动到了最新的 commit

git pull 的第二步也就是执行 fast-forward 操作

$ git merge origin/HEAD

Feature Branching

前面提到的 简单模式2.0, 所有人都在 master 上开发。在这种模式下,虽然大家可以并行开发了,但是因为所有人的代码直接push 到 master,所以每个人的代码在正式启用前没法别别人查看(当然也是可以的,那就是让别人 git clone 你本地的代码,但是这种方式超级不方便),这会导致 开发人员没法在 代码正式 启用前 进行讨论或者 code review。

所以需要一种新的 开发模式 Feature Branching ,这是目前最流行(不论是中国还是世界)的团队开发的工作流。

简而言之就是这么两步:

  1. 任何新的功能(feature)和 bug 都新建一个branch来开发
  2. 写完 branch后,将branch 合并到 master,然后删除branch

流程

假如你要开发一个新功能 叫 feature1

那么你先创建 一个叫 feature1 的分支

$ git checkout -b feature1

在许多个 commit s之后,你就将代码上传到 云端仓库。并且通知同事可以review 代码了。

$ git push origin feature1

同事就会从云端仓库 拉下 feature1 分支的代码,然后进行review

$ git pull
$ git checkout feature1

同事review完代码,觉得没问题,就会通知你,这时你就可以将feature1 分支合并到master。

$ git checkout master
$ git pull # merge 之前pull一下,让master 和云端仓库同步
$ git merge feature1

然后将 master push 到云端代码库,再删掉 feature1 分支

$ git push
$ git branch -d feature1
$ git push origin -d feature1

如果代码 review 发现有问题,那么就在本地修改,再push,直到通过review,最后再执行上面的步骤。

Pull Request

Pull Request 不是 git 自带的功能,是github等平台提出来,帮助简化上面的过程的。

过程如下:

  1. 将 feature1 push 到云端仓库
  2. 在云端仓库建立一个 pull request, base是master,compare是 feature1
  3. 你的同事就可以在github看到你的 Pull Request,他们可以在 GitHub 的这个页面查看你的 commits,也可以给你评论表示赞同或提意见,你接下来也可以根据他们的意见把新的 commits push 到云端的 feature1 上来,这个页面会随着你新的 push 而展示出最新的 commits。
  4. 在讨论结束之后,你们一致认为这个 branch 可以合并了,只需点一下绿色的 merge pull request, github 就会自动在云端仓库将 feature1合并到 master。另外,GitHub 还设计了一个贴心的 "Delete branch" 按钮,方便你在合并之后一键删除云端的 feature1分支。
  5. 最后你在本地的 master pull 一下,拉下最新的master 的commit,然后再删掉本地的 feature1 分支就可以了。

如何解决pull request 冲突?

一人多任务

当你正在 feature1 分支开发,如果这时有新的 feature2 功能开发急需要你。那么你可以先 提交一个 commit,message就叫 'TODO'

$ git add .
$ git commmit -m 'TODO'

然后再切回 master,重新创建一个 feature2 分支

$ git checkout master
$ git checkout -b feature2

当feature2 开发完,就可以再切回 feature1 工作了

add

add 命令将 文件的改动 存进 暂存区(staging area)

add . 将所有改动存进暂存区

$ git add .

将所有改动存进 暂存区,非常方便

add 添加的是文件改动,而不是具体的文件

查看 commit 的信息

# 查看所有 commit的命令
$ git log
$ git log -p # -p 是 --patch 的缩写,这个命令可以看到每个 commit 的具体信息
$ git log --stat # 查看每个 commit 的简要统计信息


# 查看单个 commit 的命令

$ git show # 查看当前 commit
$ git show commit-id # 查看某个 commit
$ git show commit-id file-name # 查看某个commit 中某个文件的 改动

查看未提交的改动

$ git diff # 查看当前目录的改动和 暂存区的差别,也就是查看当前目录 哪些内容没有存储到暂存区。
$ git diff --staged # 查看 暂存区和当前commit(HEAD)的差别, 跟 --cached 完全等价
$ git diff head # 上面两个命令之和,查看当前目录和 当前 head 的差别
$ git diff commit-id # 查看当前目录 和某个 commit 的差别

rebase

rebase 的意思是,给你的 commit 序列重新设置基础点(也就是父 commit)。展开来说就是,把你指定的 commit 以及它所在的 commit 串,以指定的目标 commit 为基础,依次重新提交一次。

在使用 merge 的时候,会使得历史分叉又汇合,如果不想让历史出现分叉的情况,而是一条直线,那么可以使用rebase命令。

比如想要让feature1 并到 master 上来

那么可以这样操作:

$ git checkout feature1
$ git rebase master

rebase 命令会把 feature1 基础点(commit 2 ) 后面的所有commit (5和6) 复制一份 到master 对应的 commit(4) 后面。 要注意,5,6和7,8的内容虽然相同,但是却是不同的commit。 执行完这个操作之后,master 会落后于 HEAD,那么这时需要切回 master然后执行 fast-forward 操作。

$ git checkout master
$ git merge feature1

为什么不直接从 master rebase feature1?

上面提到,rebase 的时候,会把当前分支从基础点(分歧点)开始的commit都复制一份迁移到以目标 commit 为父节点的位置,如果从master rebase feature1, 那么会导致,本地master分支和云端的master 分支中间差了几个节点,并且master原本从分歧点开始后面的 commit虽然内容一样,但是已经不是同样的commit了。 这会导致,本地的master无法push 到云端仓库。

为了防止 本地master 和云端仓库的 master 产生冲突,一般不要 将本地 master rebase 到其它分支。

修改当前 commit

当你提交了commit,发现某些部分改错了,这时候,你有两种做法来修正修改。

第一种方式,将错误的修改改正,然后再提交一次 commit。这种方式不优雅,增加了多余的commit。

第二种方式使用 commit --amend

commit --amend 执行的操作是将 staging area 里面的内容和当前的commit的内容结合起来创建一个新的commit, 并用这个新的 commit 覆盖掉当前的commit。

要非常注意的是, commit --amend 操作 是用新的commit 覆盖掉当前commit。所以如果操作的是master分支,并且云端仓库已经有了当前 commit,那么这种做法就不合适,否则会因为本地master缺少云端的commit而产生冲突,那么就需要先执行 git pull,将本地最新的commit和云端的master merge之后形成一个最新的commit,然后再push。这么做多此一举,还不如直接生成一个新的commit然后再push。

修改倒数第n个 commit

先来熟悉两个概念

偏移符号 ^ 和 ~n

两个偏移符号都是往回偏移 ^的数量代表往回偏移几位,例如 HEAD^^代表 往回偏移两个的commit ~数字,代表往前偏移数字个的commit, 例如 HEAD~5代表 往回偏移5个的commit

rebase -i

-i 是 --interactive 的简写,rebase -i 意思就是 交互式rebase

假设 要rebase的commit是 A, 目标 commit 为 B 如果不是 交互式 rebase的话,就只是将 A和B 分歧点开始到 A 拷贝到 B后面,但如果 B后面本身就有了这些commit(也就是B是这些commit的父节点,实际上就是 A和 B在一条路径上),那就不做任何操作。

把分支 rebase 到该分支路径上的 commit下,也叫做 原地rebase

例子: 假设 有这么一个 commit 窜 1 - 2 - 3 - 4 - HEAD 执行

$ git rebase HEAD^^ # HEAD^^代表的是 commit 3

这就会是空操作。

交互式rebase 则可以让你在rebase操作完成之前,对A和B分歧点开始 到 A的 所有commit进行操作。

$ git rebase -i HEAD^^ # HEAD^^代表的是 commit 3

master 和 3 的分歧点 其实就是 3 本身,所以我们接下来可以操作 4 和 HEAD

这时控制台会打印出

pick 4 提交信息
pcik HEAD 提交信息  # 排列顺序是 从上往下 依次是 从旧到新 的
...

pick 是对 每个commit的默认操作,意思为简单 选取 这个commit而不做任何操作。所以如果你这时如果退出编辑器,那么也是空操作。

假设我们要修改 commit 4, 那么我们 可以将 控制台信息改成如下

edit 4 提交信息
pcik HEAD 提交信息  # 排列顺序是 从上往下 依次是 从旧到新 的
...

然后退出

这时控制台会打出:

Stopped at 4 提交信息
You can amend the commit with

git commit --amend

Once you are satisfied with your changes, run

git rebase --continue

也就是我们现在停留在了 commit 4,那么我们改正了一些修改之后,就执行如下的操作:

$ git add .
$ git commit --amend
$ git rebase --continue

这样我们就修改了 commit 4。 这里commit --amend 跟上面提到的操作一样,也就是将commit 4修改,并且用新的commit(假设为5)去覆盖。那么新的commit链就变成了如下: 1 - 2 - 3 - 5 - HEAD

同样的,与修改当前commit的操作一样,如果在master 分支下,最好要被修改的commit还没被上传到云端仓库。

丢弃最新的commit

如果想要丢弃最新的commit,执行:

$ git reset --hard HEAD^

其实这条操作执行了:将HEAD 连着当前分支移动到 HEAD前一个分支。其实当前的分支不是不见了,而是无法被检索了。但是如果你记下了它的 SHA-1码,那么在 git 还有删除它之前,你还能通过SHA-1码检索到。

丢弃 倒数第 n个 commit

上面讲到, reset --hard 是将 HEAD 连着当前分支移动到 目标 commit,如果你要丢弃的是倒数第2个commit,直接使用 git reset --hard HEAD^^  就会将倒数第2个和当前的commit都抛弃。

所以如果我们要抛弃倒数第2个commit的话,我们可以用上文 提到的 交互式rebase

跟上文的交互式rebase操作一样,当终端打印出信息时,我们直接将需要删掉的那条commit 删掉删掉就可以了

~~pick 4 提交信息 ~~ pcik HEAD 提交信息  # 排列顺序是 从上往下 依次是 从旧到新 的 ...

退出编辑器,这时commit 4 就被删掉了

用 rebase --onto 撤销提交

$ git rebase --onto 目标commit 起点commit 终点commit

如果 rebase 没有 --onto 选项的话,要rebase到目标 commit的commit链的起点跟终点都是自动确定的。 起点:当前 分支 跟目标 commit的历史分叉点 终点: 也就是当前分支

加上了 --onto选项,那么你就可以指定要rebase到目标commit的起点和终点。

你就可以利用这个规则删除倒数第 2 条 commit,只需要运行:

$ git rebase ^^HEAD ^HEAD HEAD

需要注意的是,rebase的时候,会把起点排除在外。

代码已经 push 上去了才发现写错

独立开发分支写错

可以用 commit --amend 或者 rebase -i 和 rebase --onto 等修改完 commit后,直接运行一下命令

$ git push origin feature1 -f # -f 是 --force 的缩写,意为强制

这样,在本地修改了错误的 commits,然后强制 push 上去,问题就解决了。

出错的内容已经合并到master

如果使用强制 push,你会冲掉同事的 commit,所以一般不在 master 上强制 push。

我们可以选择新提交一个 commit 的方式。

如果你打算完全撤销某个 commit 的修改,可以使用一个简单的 命令

$ git revert HEAD^

这个命令会生成一个 跟 HEAD^ 的操作完全相反的 commit,从而和 HEAD^ 完全抵消。 生成这个新的commit之后,再 push 上去就好了。

reset

有个概念要先了解一下,git 的历史只能往回看,而不能往前看。也就是 git log 打印的时候,只会打印出当前commit 往回的所有commit的信息。

reset 的本质是 移动 HEAD 以及它连带的分支(如果有的话)到目标 commit。

在执行 reset 操作的同时,可以指定对当前目录的操作。

reset --hard

当前目录中的修改会被完全抛弃,目录会变成跟目标 commit完全一样。

reset --soft

保留工作目录和暂存区内容,并且把重置 HEAD 带来的差异放进 暂存区内。

reset --mixed

如果不带参数执行 reset, 默认就是执行 reset --mixed 这个操作相当于执行了 reset --soft 之后再清空暂存区的内容

checkout

本质上是 签出到指定的 commit。 以下操作都是可以的:

$ git checkout HEAD^
$ git checkout HEAD~5
$ git checkout commit-id

checkout 还可以拿来撤销工作目录的文件修改

$ git checkout --filename

checkout 和 reset 的区别

两者都会移动 HEAD,最大区别是 reset 会连带着HEAD指向的分支一起移动到目标 commit,而使用checkout的时候,会导致 HEAD 和当前分支脱离了。

还有一个专门用来让 HEAD和当前分支分离的操作指令: checkout --depatch

这个指令会让 HEAD 直接指向 当前分支对应的 commit, 而不是当前分支

stash

$ git stash

这个命令会将工作目录的改动和暂存区的所有内容都保存进本地的一个独立的地方,并且清空工作目录和暂存区的内容清空。

然后可以切换到其它分支上去做点事情,再切回来的时候,想要恢复原来的内容,只需要运行:

$ git stash pop

现在所有原来工作区和暂存区的内容就又回来了。值得注意的是,原来暂存区的内容也会被恢复到工作目录,而不是暂存区,所以现在暂存区为空。

另外,没有被 track 的文件不会被 stash 命令保存起来,git 会忽略它们。如果要连这些文件一起保存,则运行如下的命令

$ git stash -u # -u 是 --include--untracked 的缩写

reflog

reflog 是 reference log 的简称,用它可以查看 git 仓库中的引用移动记录。

所以你可以通过这条命令来找回被误删除的分支

$ git reflog # 不带参数,就是查看 HEAD的移动记录

注意:不再被引用直接或间接指向的 commits 会在一定时间后被 Git 回收,所以使用 reflog 来找回删除的 branch 的操作一定要及时,不然有可能会由于 commit 被回收而再也找不回来。

tag

对当前commit创建标签

$ git tag v1.0.0

对特定commit创建标签

$ git tag v1.0.0 commit-id

查看标签

$ git tag

push标签

$ git push origin v1.0.0

一次性push 本地未推送的标签

$ git push origin --tags

删除tag

$ git tag -d v1.0.0 # 删除本地
$ git push origin :refs/tags/v1.0.0 #删除云端