关于Git,你真的学会了吗?

“锋哥,Git有什么可说的,不就是git add添加,git commit提交嘛” 据说我要写一篇Git教程,小明不屑一顾地说。 “..."。java

小明是个人一个学生。目前,是一名Android开发工程师。git

过了几天,我又再次见到了小明。程序员

“锋哥,今天,我在Github新建了一个版本库,本地提交后推送远程的时候,却被拒绝了,是怎么回事?”github

如下是小明的操做记录:数据库

git init
git add .
git commit -m "Init commit"
git remote add origin git@github.com:xiaoming/xxx.git
git pull origin master
复制代码

以上操做触发了下面的错误:编程

From git@github.com:xiaoming/xxx.git
 * branch            master     -> FETCH_HEAD
 * [new branch]      master     -> origin/master
fatal: refusing to merge unrelated histories
复制代码

“小明,注意看最后一句提示。翻译成中文的意思是 ‘拒绝合并不相关的历史’,这个问题有两个方案能够处理。"安全

  • git pull命令实际上是触发了拉取git fetch和合并git merge两个操做。而本地的版本库和远程版本库在第一次拉取或推送完成以前是绝不相关的,Git为了不没必要要的合并,默认不容许进行这样的操做。但你能够手动添加--allow-unrelated-histories强制进行合并,这是方案一。
git pull origin master --allow-unrelated-histories
复制代码
  • 再来看方案二,从你上面的操做来看,你只是在本地初始化了一个版本库,并完成了基础的提交。接下来,你但愿和远程版本库创建关联,将提交推送到远程。这种状况下,其实你可能并不须要远程的默认数据(一般是一个空的README文件)。因此,你能够添加-f参数,将提交强制提交并覆盖远程版本库。
git push -f origin master
复制代码

小明如有所思地点点头,这是小明第一次遇到Git问题。我想,接下来他应该会比较顺利了。bash

没想到,过了几天,我又收到了小明的消息。这一次,他发来的是对Git的抱怨。服务器

“锋哥,Git好讨厌,提交日志出现了错误,也不能修改。你知道搜狗输入法有时候不够智能,输入太快不当心就输错了...😓”app

“🙂,你这孩子,别轻易下结论哈。其实,Git是容许修改提交记录的。使用Git最舒服的一点就是:Git永远都会给你反悔的机会。这一点,其它的版本控制工具是作不到的!”

“哦,原来是这样啊!那快说说看,要怎么作?” 小明已经一副火烧眉毛的表情了。

git commit命令中有一个参数叫--amend就是为解决这个问题而生的。所以,若是是最近的提交,你只须要按照下面的命令操做便可。”

git commit --amend -m "这是新的提交日志"
复制代码

看完个人消息,小明给我发来一个微笑的表情。小明的抱怨让我想起一句好气又可笑的农村俗语 “屙屎不出怪茅坑”,哈哈。

本觉得一切能够风平浪静了。没想到,过了一个月左右,忽然接到了小明的紧急电话。电话那头,小明彷佛心情很急躁。

“锋哥,我不当心进行了还原操做,我写的代码全丢了。几千行的代码啊,明天晚上就要发版本了,有办法找回来吗?”

听到这个消息,我内心盘算,大约有50%的几率应该是找不回来了。这孩子比较粗心,可能根本就没提交到版本库。但若是他正好提交到了版本库,兴许还有救。所以,我安慰他说 “小明,别急!你打开TeamViewer,我远程帮你看看”

连上机器后,我使用history命令看到小明在提交以后使用了git reset --hard xxx命令进行重置。--hardgit reset命令中惟一一个不安全的操做,它会真正地销毁数据,以致于你在git log中彻底看不到操做日志。但是,Git真的很聪明,它还保存了另一份日志叫reflog,这个日志记录了你每次修改HEAD的操做。所以,你能够经过下面的命令对数据进行还原:

git reflog

// 使用这个命令,你看到的日志大概是这样
c8278f9 (HEAD -> master) HEAD@{0}: reset: moving to c8278f9914a91e3aca6ab0993b48073ba1e41b2b
3e59423 HEAD@{1}: commit: a
c8278f9 (HEAD -> master) HEAD@{2}: commit (amend): v2 update
2dc167b HEAD@{3}: commit: v2
2e342e9 HEAD@{4}: commit (initial): Init commit
复制代码

能够看到,咱们在版本3e59423进行了git reset操做,最新版本是3e59423。所以,咱们能够再次经过git reset命令回到这个版本:

git reset --hard 3e59423
复制代码

以上操做完成后,你会惊喜地发现,丢失的数据竟然神奇般地回来了。

“谢谢锋哥!!!🌺 🌺 🌺”

“下次别这样操做了哈。另外,你怎么一次性丢失这么多代码。必定要记得勤提交。” 小明出现这样的问题,与平时的不规范操做也是分不开的。所以,最后我还不忘嘱咐了他一句。

“好的,我知道了。对了,我一个还有比较疑惑的问题。git checkoutgit reset到底有啥区别?我之前用SVN的时候git checkout是用来检出代码的,在Git中能够用它切换分支或者指定版本,但git reset一样能够作到。难道二者是彻底同样的吗?” 小明在QQ中给我发来了回复消息。

“这是一个比较有深度的问题,解释这个问题须要一点时间。接下来,你仔细听”

理解Git工做空间

理解这个问题以前,先来简单学习一些Git基础知识。Git有三种状态:

  • 已提交(commited):数据已彻底保存到本地数据库中
  • 已修改(modified):修改了文件,但尚未保存到数据库中
  • 已暂存(staged):对一个已修改的文件作了标记,将包含在下一次提交的版本快照中

这三种状态对应Git三个工做区域:Git版本库、暂存区和工做区

Git版本库是Git用来保存项目的元数据和对象数据库的地方,使用git clone命令时拷贝的就是这里的数据。

工做目录是对某个版本独立检出的内容,这些数据能够供你使用和修改。

暂存区在Git内部对应一个名为index的文件,它保存了下次将要提交的文件列表信息。所以,暂存区有时候也被叫做 “索引”。

一个基础的Git工做流程以下: 1)在工做区修改文件 2)使用git add将文件添加到暂存区,也就是记录到index文件中 3)使用git commit将暂存区中记录的文件列表,使用快照永久地保存到Git版本库中

理解HEAD

解释这个问题,你还须要简单理解HEAD是什么。简单来讲,HEAD是当前分支引用的指针,它永远指向该分支上最后一次提交。为了让你更容易理解HEAD,你能够将HEAD看做上一次提交数据的快照。

若是你感兴趣,你可使用一个底层命令来查看当前HEAD的快照信息:

git ls-tree -r HEAD

100644 blob aca4b576b7d4534266cb818ab1191d91887508b9	demo/src/main/java/com/youngfeng/snake/demo/Constant.java
100644 blob b8691ec87867b180e6ffc8dd5a7e85747698630d	demo/src/main/java/com/youngfeng/snake/demo/SnakeApplication.java
100644 blob 9a70557b761171ca196196a7c94a26ebbec89bb1	demo/src/main/java/com/youngfeng/snake/demo/activities/FirstActivity.java
100644 blob fab8d2f5cb65129df09185c5bd210d20484154ce	demo/src/main/java/com/youngfeng/snake/demo/activities/SecondActivity.java
100644 blob a7509233ecd8fe6c646f8585f756c74842ef0216	demo/src/main/java/com/youngfeng/snake/demo/activities/SplashActivity.java
复制代码

这里简单解释一下每一个字段的意思:100644表示文件模式,其对应一个普通文件。blob表示Git内部存储对象数据类型,另外还有一种数据类型tree,对应一个树对象,中间较长的字符串对应当前文件的SHA-1值,这部分不须要记住,简单了解便可。

因此,简单来讲,HEAD对应一个树形结构,存储了当前分支全部的Git对象快照:

咱们用一个表格简单来总结一下以上知识点:

HEAD Index(暂存区) 工做区
上一次提交的快照,下一次提交的父节点 预期的下一次提交快照 当前正在操做的沙盒目录

理解git resetgit checkout区别主要是理解Git内部是怎么操做以上三棵树的。

接下来,咱们用一个简单的例子来看一下使用git reset到底发生了什么。先建立一个Git版本库并触发三次提交:

git init repo
touch file.txt
git add file.txt
git commit -m "v1"

echo v2 > file.txt
git add file.txt
git commit -m "v2"

echo v3 > file.txt
git add file.txt
git commit -m "v3"
复制代码

以上操做完成后,版本库如今看起来是这样的:

接下来执行命令git reset 14ad152看看会发生什么。如下是命令执行完成后看到的结果:

git log --abbrev-commit --pretty=oneline
### This is output ###
14ad152 (HEAD -> master) v2
bcc49f4 v1

git status -s
### This is output ###
 M file.txt

cat file.txt
### This is output ###
v3
复制代码

能够看到版本库中文件版本回退到了V2,工做区文件内容同以前的版本V3一致;为了确认暂存区发生了什么变化,咱们再使用一个底层命令对比一下暂存区数据和版本库数据是否一致:

# 查看暂存区信息
git ls-files -s
### This is output ###
100644 8c1384d825dbbe41309b7dc18ee7991a9085c46e 0	file.txt

# 查看版本库快照信息
git ls-tree -r HEAD
### This is output ###
100644 blob 8c1384d825dbbe41309b7dc18ee7991a9085c46e	file.txt
复制代码

能够看到当前版本库和暂存区信息是彻底一致的,HEAD指向了v2提交,用一个图形来表示整个过程,应该是这样:

看一眼上图,理解一下刚刚发生的事情:首先,HEAD指针发生了移动,指向了V2,并撤销了上一次提交。目前,版本库和暂存区都保存的是第二次提交的记录,工做区却保存了最近一次修改。稍微联想一下,你就会发现,此次的git reset命令刚好是最近一次提交的逆向操做。让数据彻底回到了上一次提交前的状态。因此,若是你想撤销最近一次提交,能够这么作。

增长--soft参数测试

以上是咱们对git reset命令的第一次尝试,在下一轮尝试前,先执行git help reset看看reset命令的用法:

git reset [-q] [<tree-ish>] [--] <paths>...
git reset (--patch | -p) [<tree-ish>] [--] [<paths>...]
git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>]
复制代码

看最后一句发现,reset命令后面还能够接5个不一样的参数: --soft--mixed--hard--merge--keep。这里咱们主要关注前面三个,其中--mixed其实刚刚已经尝试过,它和不带参数的git reset命令是一样的效果。换而言之,--mixedgit reset命令的默认行为。接下来执行git reset --soft 14ad152看看会发生什么。命令执行完成后,按照惯例,咱们一样使用基础命令看看发生了什么变化:

git log --abbrev-commit --pretty=oneline
### This is output ###
14ad152 (HEAD -> master) v2
bcc49f4 v1

git status -s
### This is output ###
M  file.txt

cat file.txt
### This is output ###
v3
复制代码

奇怪了?为何会和上次不带任何参数的执行结果彻底一致?难道Git出现了设计错误。相信你看到结果必定会有这样的疑问,其实否则!由于,这里我用文本粘贴了输出结果,忽略了命令的字体颜色,其实这里第二条命令输出结果中的M颜色与上一次执行结果是不同的。为了让你看到不一样,看下面的截图:

这个颜色表示:file.txt文件已经被添加到了暂存区,使用 git commit命令就能够完成提交。为了严谨,咱们依然使用上面的底层命令看看版本库和暂存区信息是否一致。注意:这里的结果应该是不一致才对,由于版本库记录的文件版本是v2,而暂存区记录的文件版本实际上是v3。

git ls-tree -r HEAD
### This is output ###
100644 blob 8c1384d825dbbe41309b7dc18ee7991a9085c46e	file.txt

git ls-files -s
### This is output ###
100644 29ef827e8a45b1039d908884aae4490157bcb2b4 0	file.txt
复制代码

能够看到,两个命令执行输出的SHA-1并不一致,验证了咱们的猜测。

这里咱们能够得出一个结论:--soft和默认行为(--mixed)不同的地方是:--soft会将工做区的最新文件版本再作一步操做,添加到暂存区。使用这个命令能够用来合并提交。即:若是你在某一次提交中有未完成的工做,而你反悔了,你可使用这个命令撤销提交,等工做作完后继续一次性完成提交。

增长--hard参数测试

接下来咱们对最后一个参数进行测试,这也是小明在使用过程出现问题的一个参数。执行命令git reset --hard 14ad152,看看发生了什么:

git log --abbrev-commit --pretty=oneline
### This is output ###
14ad152 (HEAD -> master) v2
bcc49f4 v1

git status -s
### This is output ###
>>> No output <<<

cat file.txt
v2
复制代码

注意看,此次使用git status -s彻底看不到输出,这就证实:当前工做区,暂存区,版本库数据是彻底一致的。查看文件内容,发现文件回到了v2版本。一般状况下,若是你看到这种状况,必定会吓一跳,你最近一次提交的数据竟然彻底丢失了。的确,这是Git命令中少有的几个真正销毁数据的命令之一。除非你很是清楚地知道本身在作什么,不然,请尽可能不要使用这个命令!

咱们依然用一张图,完整地描述这个命令到底发什么了什么:

能够看到,相对于默认行为,--hard将工做区的数据也还原到了V2版本,以致于V3版本的提交已经彻底丢失。

git checkout

接下来看git checkout, 按照惯例,先执行git checkout 14ad152看看会发生什么:

git log --abbrev-commit --pretty=oneline
### This is output ###
14ad152 (HEAD -> master) v2
bcc49f4 v1

git status -s
### This is output ###
>>> No output <<<

cat file.txt
v2
复制代码

能够看到,又出现了神奇的一幕,这一次git checkout命令的执行结果的确和git reset --hard彻底一致。这是否意味着二者就没有任何区别了呢?固然也不是。严格来讲,二者有两个“本质”的区别:

  • 相对而言,git checkout对工做目录是安全的,它不会将工做区已经修改的文件还原,git reset则无论三七二十一一股脑所有还原。
  • 另一个比较重要的区别是,git checkout并不移动HEAD分支的指向,它是经过直接修改HEAD引用来完成指针的指向。

第二个不一样点相对比较难理解,咱们用一张图来更直观地展现两者的区别:

简单来讲,git reset会经过移动指针来完成HEAD的指向,而git checkout则经过直接修改HEAD自己来完成指向的移动。

命令做用于部分文件

git resetgit checkout还能够做用于一个文件,或者部分文件,即带文件路径执行。这种状况下,两个命令的表现不太同样。咱们来试试看,先执行git reset 14ad15 -- file.txt命令尝试将文件恢复到V2版本。命令执行完成,按照惯例用一些基础命令来看看发生了什么:

git log --abbrev-commit --pretty=oneline
### This is output ###
4521405 (HEAD -> master) v3
14ad152 v2
bcc49f4 v1

git status -v
### This is output ###
diff --git a/file.txt b/file.txt
index 29ef827..8c1384d 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1 @@
-v3
+v2

cat file.txt
v3
复制代码

能够看到,版本库和工做区的数据都没有发生变化。惟一发生变化的是暂存区,暂存区记录下一次提交的改动将致使数据从V3恢复到V2版本!

这里咱们能够这样理解:执行这条命令后,Git先将暂存区和工做区的文件版本恢复到V2,再将工做区的文件版本恢复到V3。与--hard不同的地方是:这个命令并不会覆盖工做区已经修改的文件,是安全操做。

执行带路径的git checkout命令和git reset命令有一些细微的差异,相对于git resetgit checkout带路径执行会覆盖工做区已经修改的内容,致使数据丢失,是一个非安全操做。

针对上面的全部实验,咱们用一个简单的表格来总结他们的区别,以及操做是否安全:

不带路径执行

命令行 HEAD 暂存区 工做区 目录安全
git reset [--mixed] YES YES NO YES
git reset --soft YES YES NO YES
git reset --hard YES YES YES NO
git checkout Modify YES YES YES

带路径执行

命令行 HEAD 暂存区 工做区 目录安全
git reset -- NO YES NO YES
git checkout NO YES YES NO

“小明,你明白了吗?” 消息发送过去以后,等了好久却一直没有响应。 “哎,这孩子!估计听睡着了... 😆”

自从此次问到Git的问题后,已经两年过去了,小明再没有问到关于Git的问题。而就在昨天,忽然又收到了小明的消息。

“锋哥,我如今已是Android Leader了。如今安卓团队一共6我的,咱们如今在作一个社交类应用,在Git管理方面我仍是发现了一些问题。其中一个问题就是,如今版本库有好多分支,其中开发主要在develop分支。主干分支是master主要用于版本发布。可还有一些分支却显得很是混乱,有什么办法改善这种状况吗?”

“关于Git的分支设计,目前有一个公认比较好的设计叫 Git Flow模型。关于Git Flow模型,你能够查看这篇文章 nvie.com/posts/a-suc… 了解一下"

"好的!还有一个困扰了我好久的问题是,你们的提交日志写的比较笼统。在查找问题的时候很是不便,并且大部分同窗一次性提交好多文件,致使解决问题的时候不能准肯定位到具体是哪一次提交致使的。我告诉你们,一次提交改动要尽量小。但当别人问到具体的提交规则的时候我又不知道从何提及..."

“这是一个很好的问题 。中国程序员广泛存在的一个问题是,巴不得把这辈子能提交的代码一次性搞定。甚至有人用屡次提交太麻烦的借口来搪塞问责人。简单来讲,能够用一句话归纳提交原则:一个idea,一次提交。另外,你说的没错,提交必须尽量小,注释必须尽量表述准确!”

给小明讲了这么多Git,我忍不住半开玩笑地问他,“小明,你如今还以为Git简单吗?”

小明发了一个无奈的表情!说道,“之前是我才疏学浅,略知皮毛,不知道Git原来还有这么多玩法,忍不住为Git的发明者点赞了。对了,锋哥,Git究竟是谁开发的?”

”关于Git的故事,互联网上其实已经烂大街了。我简单给你介绍一下吧!Git的诞生实际上是一个偶然,其初始使命是为Linux内核代码管理服务的。早年的时候Linux内核源码是用Bitkeeper版本控制工具管理的。但是,后来由于某些利益关系,Bitkeeper要求Linux社区付费使用。这一举动激怒了Linus,也就是Linux的创始人,他决定本身开发一个分布式版本控制系统。几周时间下来,Git的雏形就诞生了,而且开始在Linux社区中应用开来。虽然Linus是Git的创始人,但是背后的最大功臣倒是一个日本人 Junio C Hamano。Linus在Git开源版本库的提交只有258次,而Junio C Hamano却提交了4000屡次。也就是说,在Linus开发后不久项目的管理权就交给了这个日本人。关于 Junio C Hamano,你感兴趣的话能够Google了解一下。他如今在Google工做,如同Linus同样很是低调。“

“这个故事也告诉我:不要用技术去挑战一个程序员 @_@ ”

这个故事讲完,小明与Git的故事就已经告一段落了。其实,还有一些比较常见的问题,小明并无问到过。这里,我为你准备了一个附录,给你介绍一些经常使用的小命令帮你解决平常小问题。它颇有用,必定要拿笔记下来,或者收藏这篇文章备用。

常见问题

问题一:公司的Git服务器是搭建在一个内网服务器上面的,我想把代码同时提交到OsChina上面,以便在家拉取代码,远程办公,怎么办? Git自己是一个分布式的版本管理系统,实现这个需求很是简单,使用git remote add命令添加多个远程版本库关联便可。

git remote add company git@xxx
git remote add home git@xxx
复制代码

问题二:在拉取远程代码的时候,若是本地有代码尚未提交,Git就会提示先提交代码到版本库。可暂时我又不想提交,怎么办? 针对这个问题,Git提供了一个临时区域用于保存不想提交的记录,对应的命令是git stash。一般状况下,你能够这样操做:

# 将暂时还不想提交的数据保存到临时区域,保存成功后,工做区将和版本库彻底一致
git stash
# 还原stash数据到工做区
git stash apply
# 以上操做完成后,stash数据依然保存在临时区域中,为了删除这部分数据,使用以下命令便可。
git stash drop
# 若是你想在还原数据的同时从临时区域删除数据,能够这样操做:
git statsh pop
# 以上两个命令若是不接任何参数将删除掉全部的临时区域数据,若是你只想删除其中一条记录,指定对应索引数据便可。
git stash pop/drop stash@{index}
# 查看临时区域全部数据,使用以下命令:
git stash list
复制代码

问题三:做为项目负责人,我但愿迅速找出问题代码的“元凶”,有什么办法吗? 针对这个问题,最好的答案是git blame,使用这个命令并指定具体文件它将显示文件每一行代码的最近修改记录,你能够清晰地看待最近代码的修改人。

总结

Git是一个很是优秀的版本控制系统,我极力推荐你在平常开发中使用。这篇文章从小明的角度解释了几个常见问题的解决方案,毫无悬念地,你可能还会遇到其它的一些问题。遇到问题,你能够尝试Google搜索解决方案。也能够在文章下方给我留言,我很是乐意为你解答Git问题。


我是欧阳锋,我愿为你鞍前马后,助你平步青云。若是你喜欢个人文章,请在下方留下你爱的印记。若是你不喜欢个人文章,请先喜欢上个人文章,而后再留下爱的印记。

下次文章再见!拜拜!


学更多编程知识,扫描下方二维码关注欧阳锋工做室

欧阳锋工做室
相关文章
相关标签/搜索