参考
Git是什么?
Git是快照,而不是差异
Git 与任何其他 VCS(包括 Subversion 和朋友)之间的主要区别在于 Git 考虑其数据的方式。从概念上讲,大多数其他系统将信息存储为基于文件的更改列表。这些其他系统(CVS、Subversion、Perforce、Bazaar 等)将它们存储的信息视为一组文件以及随时间对每个文件所做的更改(这通常被描述为基于增量的版本控制)。
图 4. 将数据存储为对每个文件的基本版本的更改
Git 不会以这种方式考虑或存储其数据。相反,Git 认为其数据更像是微型文件系统的一系列快照。使用 Git,每次提交或保存项目状态时,Git 基本上都会拍下当时所有文件的样子,并存储对该快照的引用。为了提高效率,如果文件没有改变,Git 不会再次存储文件,只是一个指向它已经存储的前一个相同文件的链接。Git 认为其数据更像是一连串快照。
图 5. 将数据存储为项目随时间的快照
这是 Git 与几乎所有其他 VCS 之间的重要区别。它使 Git 重新考虑了大多数其他系统从上一代复制而来的版本控制的几乎每个方面。这使得 Git 更像是一个迷你文件系统,在它之上构建了一些非常强大的工具,而不仅仅是一个 VCS。当我们在Git Branching 中介绍 Git 分支时,我们将探索通过以这种方式考虑数据而获得的一些好处。
几乎所有操作都是本地的
Git 中的大多数操作只需要本地文件和资源来操作——通常不需要来自网络上另一台计算机的信息。
当您在断网,断vpn情况下,完全可以正常工作。
实践中遇到的问题
1. 使用git stash来暂存文件
命令 | 意义 |
---|---|
git stash save "save message" | 执行存储时,添加备注,方便查找,只有git stash 也要可以的,但查找时不方便识别。 |
git stash list | 查看stash了哪些存储 |
git stash show | 显示做了哪些改动,默认show第一个存储,如果要显示其他存贮,后面加stash@{$num},比如第二个 git stash show stash@{1} |
git stash show -p | 显示第一个存储的改动,如果想显示其他存存储,命令:git stash show stash@{$num} -p ,比如第二个:git stash show stash@{1} -p |
git stash apply | 应用某个存储,但不会把存储从存储列表中删除,默认使用第一个存储,即stash@{0},如果要使用其他个,git stash apply stash@{$num} , 比如第二个:git stash apply stash@{1} |
git stash pop | 命令恢复之前缓存的工作目录,将缓存堆栈中的对应stash删除,并将对应修改应用到当前的工作目录下,默认为第一个stash,即stash@{0},如果要应用并删除其他stash,命令:git stash pop stash@{$num} ,比如应用并删除第二个:git stash pop stash@{1} |
git stash drop stash@{$num} | 丢弃stash@{$num}存储,从列表中删除这个存储 |
git stash clear | 删除所有缓存的stash |
2. 撤销操作
1. 取消已修改但并没有git add + git commit 的文件
git status告诉你如何做到这一点。在最后一个示例输出中,未暂存区域如下所示:
git checkout -- <file> ...
丢弃工作区的改动。2. 已经git add + git commit 的文件
使用git revert <commit-id>
回滚某次提交。
- git revert 撤销某次操作,此操作不会修改原本的提交记录,而是会新增一条提交记录来抵消某次操作。
- git revert <commit-id> 针对普通commit
- git revert <commit-id> -m 针对merge的commit
e.g.
我们想要将红框框起来的这一个commit撤回。
执行:git revert 3422d7f119cb48ec4d212154e71001ebe79b8336
操作后,我们再打开日志查看,如下图所示,可以看到是新增了一条commit记录,之前的commit记录并没有消失,此时也达到了代码回退的效果。
- git revert 也可以回滚多次的提交。
- git revert <commit-id1> <commit-id2>... 全部会撤回
e.g.
- git reset
对于撤回命令,还可以使用git reset
命令。那么二者有什么区别呢? - git revert会新建一条 commit 信息,来撤回之前的修改。
- git reset会直接将提交记录退回到指定的 commit 上。
e.g.
执行git reset
之前的提交记录
执行之后:
直接将代码退回到了这个提交记录之时,之后的所有记录都没有啦。
对于个人的 feature 分支而言,可以使用git reset来回退历史记录,之后使用git push --force进行推送到远程,但是如果是在多人协作的集成分支上,不推荐直接使用git reset命令,而是使用更加安全的git revert命令进行撤回提交。这样,提交的历史记录不会被抹去,可以安全的进行撤回。
3. git rebase 让你的提交记录更加清晰可读
git rebase 的使用
rebase 翻译为变基,他的作用和 merge 很相似,用于把一个分支的修改合并到当前分支上。
如下图所示,下图介绍了经过 rebase 前后提交历史的变化情况。
e.g.
假设我们现在有2条分支,一个为 master ,一个为 feature/1,他们都基于初始的一个提交add readme
进行检出分支,之后,master分支增加了3.js
,和4.js
的文件,分别进行了2次提交,feature/1也增加了1.js
和2.js
的文件,分别对应以下2条提交记录。
此时,对应分支的提交记录如下。
master 分支如下图:
feature/1分支如下图
结合起来看是这样的
此时,切换到 feature/1 分支下,执行 git rebase master ,成功之后,通过 log 查看记录。
如下图所示:可以看到先是逐个应用了 mater 分支的更改,然后以 master 分支最后的提交作为基点,再逐个应用 feature/1的每个更改。
所以,我们的提交记录就会非常清晰,没有分叉,上面演示的是比较顺利的情况,但是大部分情况下,rebase 的过程中会产生冲突的,此时,就需要手动解决冲突,然后使用git add 、git rebase --continue的方式来处理冲突,完成 rebase,如果不想要某次 rebase 的结果,那么需要使用 git rebase --skip来跳过这次 rebase。
git merge 和 git rebase 的区别
不同于 git rebase的是,git merge 在不是 fast-forward(快速合并)的情况下,会产生一条额外的合并记录,类似Merge branch 'xxx' into 'xxx'的一条提交信息。
另外,在解决冲突的时候,用 merge 只需要解决一次冲突即可,简单粗暴,而用 rebase 的时候 ,需要一次又一次的解决冲突。
4. git cherry-pick 获取指定的commit
git cherry-pick可以理解为”挑拣”提交,和 merge 合并一个分支的所有提交不同的是,它会获取某一个分支的单笔提交,并作为一个新的提交引入到你当前分支上。
语法:git cherry-pick [commit-id]
当前form1分支上的提交记录:
5. git log 查看提交历史
按时间倒序列出在该存储库中进行的提交 。
选项 | 描述 |
---|---|
-p |
显示每次提交引入的补丁。 |
--stat |
显示每次提交中修改的文件的统计信息。 |
--shortstat |
仅显示来自 --stat 命令的更改/插入/删除行。 |
--name-only |
显示提交信息后修改的文件列表。 |
--name-status |
显示受添加/修改/删除信息影响的文件列表。 |
--abbrev-commit |
仅显示 SHA-1 校验和的前几个字符,而不是全部 40 个字符。 |
--relative-date |
以相对格式(例如,“2 周前”)而不是使用完整日期格式显示日期。 |
--graph |
在日志输出旁边显示分支和合并历史的 ASCII 图形。 |
--pretty |
以替代格式显示提交。选项值包括 oneline、short、full、fuller 和 format(您可以在其中指定自己的格式)。 |
--oneline |
--pretty=oneline --abbrev-commit 一起使用的简写。 |
--pretty
此选项将日志输出更改为默认格式以外的格式。参数有:oneline、short、full、fuller显示在大致相同的格式,但分别与更少或更多的信息。
-
git log --pretty=oneline
-
git log --pretty=short
-
git log --pretty=full
-
git log --pretty=fuller
基础
1. git add
添加文件到暂存区
# 添加某个文件到暂存区,后面可以跟多个文件,以空格区分
git add xxx
# 添加当前更改的所有文件到暂存区。
git add .
2. git commit
# 提交暂存的更改,会新开编辑器进行编辑
git commit
# 提交暂存的更改,并记录下备注
git commit -m "you message"
# 等同于 git add . && git commit -m
git commit -am
# 对最近一次的提交的信息进行修改,此操作会修改commit的hash值
git commit --amend
3. git pull
# 从远程仓库拉取代码并合并到本地,可简写为 git pull 等同于 git fetch && git merge
git pull <远程主机名> <远程分支名>:<本地分支名>
# 使用rebase的模式进行合并
git pull --rebase <远程主机名> <远程分支名>:<本地分支名>
4. git fetch
与 git pull 不同的是git fetch操作仅仅只会拉取远程的更改,不会自动进行merge操作。对你当前的代码没有影响。
# 获取远程仓库特定分支的更新
git fetch <远程主机名> <分支名>
# 获取远程仓库所有分支的更新
git fetch --all
5. git branch
# 新建本地分支,但不切换
git branch <branch-name>
# 查看本地分支
git branch
# 查看远程分支
git branch -r
# 查看本地和远程分支
git branch -a
# 删除本地分支
git branch -D <branch-nane>
# 重新命名分支
git branch -m <old-branch-name> <new-branch-name>
工作中使用 git 解决问题的场景
3. git rebase 让你的提交记录更加清晰可读
第二种合并分支的方法是 git rebase,它的作用和merge很相似。
Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。
咱们还是实际操作一下吧……
maste分支在后,想要将其移动到最前端的节点:
git checkout master
git rebase [branch-name|hash值]
高级
在你项目的提交树上前后移动的几种方法
1. HEAD
我们首先看一下 “HEAD”。 HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。
分离的HEAD
2. 相对引用(^
)
通过指定提交记录哈希值的方式在 Git 中移动不太方便。在实际应用时,并没有像本程序中这么漂亮的可视化提交树供你参考,所以你就不得不用 git log 来查查看提交记录的哈希值。
并且哈希值在真实的 Git 世界中也会更长(译者注:基于 SHA-1,共 40 位)。例如前一关的介绍中的提交记录的哈希值可能是 fed2da64c0efc5293610bdd892f82a58e8cbc5d8。舌头都快打结了吧...
比较令人欣慰的是,Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。因此我可以仅输入fed2 而不是上面的一长串字符。
正如我前面所说,通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用。这个就很厉害了!
使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix 分支或 HEAD)开始计算。
相对引用非常给力,这里我介绍两个简单的用法:
使用 ^
向上移动 1 个提交记录
使用~<num>
向上移动多个提交记录,如 ~3
3. 相对引用(~
)
如果你想在提交树中向上移动很多步的话,敲那么多 ^ 貌似也挺烦人的,Git 当然也考虑到了这一点,于是又引入了操作符 ~。
该操作符后面可以跟一个数字(可选,不跟数字时与 ^ 相同,向上移动一次),指定向上移动多少次。
3. 强制修改分支位置
git branch -f master HEAD~3
4. 撤销变更
在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
主要有两种方法用来撤销变更 —— 一是 git reset,还有就是 git revert。接下来咱们逐个进行讲解。
1. git reset
git reset
通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
2. revert
虽然在你的本地分支中使用 git reset 很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!
为了撤销更改并分享给别人,我们需要使用 git revert。
!!!移动提交记录
到现在我们已经学习了 Git 的基础知识 —— 提交、分支以及在提交树上移动。 这些概念涵盖了 Git 90% 的功能,同样也足够满足开发者的日常需求。
然而, 剩余的 10% 在处理复杂的工作流时(或者当你陷入困惑时)可能就显示尤为重要了。接下来要讨论的这个话题是“整理提交记录” —— **开发人员有时会说“我想要把这个提交放到这里, 那个提交放到刚才那个提交的后面” **, 而接下来就讲的就是它的实现方式,非常清晰、灵活,还很生动。
看起来挺复杂, 其实是个很简单的概念。
1. Git Cherry-pick
本系列的第一个命令是 git cherry-pick, 命令形式为:
git cherry-pick <提交号>...
如果你想将一些提交复制到当前所在的位置(HEAD
)下面的话, cherry-pick
是最直接的方式了。我个人非常喜欢cherry-pick
,因为它特别简单。
要在心里牢记 cherry-pick 可以将提交树上任何地方的提交记录取过来追加到 HEAD 上(只要不是 HEAD 上游的提交就没问题)。
2. 交互式的 rebase
当你你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了
咱们具体来看一下……
交互式 rebase 指的是使用带参数 --interactive 的 rebase 命令, 简写为 -I
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
在实际使用时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim —— 中打开一个文件。 考虑到课程的初衷,我弄了一个对话框来模拟这些操作。
当 rebase UI界面打开时, 你能做3件事:
调整提交记录的顺序(通过鼠标拖放来完成)
删除你不想要的提交(通过切换 pick 的状态来完成,关闭就意味着你不想要这个提交记录)
合并提交。 遗憾的是由于某种逻辑的原因,我们的课程不支持此功能,因此我不会详细介绍这个操作。简而言之,它允许你把多个提交记录合并成一个。
接下来咱们看个实例:
- 调整提交记录
有两个提交:
git rebase -i HEAD~2
修改框中的提交顺序:
修改后的顺序为:
?
- 删除不想要的提交记录
- 合并提交记录
杂项
1. 只提取一个提交记录
本地栈式提交
来看一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。
这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!
最后就差把 bugFix 分支里的工作合并回 master 分支了。你可以选择通过 fast-forward 快速合并到 master 分支上,但这样的话 master 分支就会包含我这些调试语句了。你肯定不想这样,应该还有更好的方式……
实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。跟之前我们在“整理提交记录”中学到的一样,我们可以使用
git rebase -I
git cherry-pick
来达到目的。
高级话题
1. git tag
相信通过前面课程的学习你已经发现了:分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
你可能会问了:有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
当然有了!Git 的 tag 就是干这个用的啊,它们可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
咱们来看看标签到底是什么样。
2. 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 提交记录上有某个标签时,则只输出标签名称。
3. 选择父提交记录
操作符 ^ 与 ~ 符一样,后面也可以跟一个数字。
但是该操作符后面的数字与 ~ 后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个父提交。还记得前面提到过的一个合并提交有两个父提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。
Git 默认选择合并提交的“第一个”父提交,在操作符 ^ 后跟一个数字可以改变这一默认行为。
废话不多说,举个例子。
远程
1. pull & push git 远程仓库
1. git clone
从技术上来讲,git clone 命令在真实的环境下的作用是在本地创建一个远程仓库的拷贝(比如从 github.com)。
2. 远程分支
3. git fetch
4. git pull
5.模拟团队合作
6. git push
前面已经介绍过 git pull 就是 fetch 和 merge 的简写,类似的 git pull --rebase 就是 fetch 和 rebase 的简写!
2. 关于 origin 和它的周边 —— Git 远程仓库高级操作
3.Git 别名
如果您部分输入,Git 不会自动推断您的命令。如果您不想键入每个 Git 命令的完整文本,您可以使用git config. 以下是您可能想要设置的几个示例:
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status
$ git config --global alias.last 'log -1 HEAD'
删除设置的别名
$ git config --global --unset alias.logone
删除所有的别名
$ git config --global --remove-section alias