Pulpcode

捕获,搅碎,拼接,吞咽

0%

性价比学习git

其实我一直想写一篇博客总结git的使用。但是却又没想好怎么写,即使我知道这篇博客的题目应该叫《性价比学习git》,我也没有想好如何性价比的学习git,我不可能把《pro git》上大段的git底层知识贴过来我用自己的话再描述一遍,也不能给你讲一些你这辈子都不会用到的git命令。直到最近我才有了灵感,所以在这篇博客里,我将试图先假设一个场景,并描述清楚你所遇到的问题,然后再告诉你要用git如何解决,最后我才可能会提到这背后的原理是啥,即-“为什么git可以这么玩?”,所以我所指的性价比学习git是说如何通过真实的使用场景学习git。不过我举得例子绝对都是自己开发真正遇到过的问题,同时也有一点点难度,而不是诸如“我想开发一个新功能”这样的简单例子。所以这篇博客也假设你已经学会了git的基本操作,而不会教你一些基础简单的命令,比如什么是分支?如何创建一个分支?BTW,这种写作方式,像极了《xxx cookbook系列丛书》

开发前忘记checkout一个分支

你打算为你的项目开发一个新功能,原本你应该在你的master分支上checkout出来一个feature-xxx-xxx,然后在测试完成后合并到你的master分支上。然而当你开发到一半的时候,发现你根本没有checkout一个分支,你的开发都是在master上完成的,这个时候你要怎么办?

实际上,如果你只是忘记从master分支checkout出来一个新分支feature-xxx-xxx这么简单的话,那么你只需要现在checkout就行了,你会发现你现在已经有了feature-xxx-xxx,并且你所修改的文件都被标记为:“M(odify)”,那现在你就当自己就是在新分支上做的开发就行了。不过还有一些棘手的时候,就是你本来想在feature-1上做开发,但是却错误的在feature-2上做了开发。现在你要如何让这些修改放到feature-1上?其实问题的本质还是feature-2的这些修改直接平移到feature-1会不会产生冲突,如果你修改的某个文件,两个分支已经提交的部分就不同,那你根本不能checkout成功:

Your local changes to the following files would be overwritten by checkout

这个时候只能你手工的去贴这些修改,然后去修复它们。因为程序并不能智能的知道,你到底想要啥。

开发途中要你修改一个线上问题

当你遇到线上问题需要紧急修复的时候,往往你会从master分支上checkout出来一个 hotfix-xxx-xxx,然后测试无误后合并到master上上线,但是你此时已经在某个feature分支上做了一部分开发,而且算不上一个功能还是什么的,这时你怎么切到master?难道你要在你的分支上看到大量的commit,它们的内容都是“提交一次,防止丢失”么?

你可以先用git stash,将这部分修改储藏起来,然后切到master上去做hot-fix,之后在回到此分支来做git stash pop。看到pop你就明白这是一个栈,但其实也不算,因为你可以指定将某个stash 应用到此分支,pop充其量就表示栈的最上面。

git stash pop 到了错误的分支

你在我之前的描述中学会了git stash 和git stash pop这两个例子,你本来要切回到你的feature-1上git stash pop的,结果没想到,你不小心切到了feature-2,并且你git stash pop了,这下你要怎么办?

首先你直接使用git stash pop并不是一个好的习惯,正确的方式因该是先git stash apply,然后在git stash drop。不过事已至此,你先要把当前目录那些不该有的stash都剔除掉(checkout 掉它们就好了),然后你仔细看git的提示信息,在你git stash pop或者git stash drop的时候,
会有一个hash值。信息类似于:

1
Dropped refs/stash@{0} (xxxxxx)

git的原理中会告诉你,git本质上就是一个k,v数据库,任何东西都可以表示成一个hash值,一个comit,一个branch,一个文件快照,甚至是一次stash。所以虽然你在git stash list上已经看不到那个git stash了,但你依旧可以用hash值引用这次stash,所以只要切回到原来的那个分支,然后git stash apply hash值。

顺便说一句,只有你成功的git stash pop才会删除掉原来的那个stash,如果你在stash pop的时候产生了冲突,或者因为有未提交文件导致的stash pop失败,都不会在stash栈上去删除你的stash的。这种失败可能是因为你在错误的分支上进行了stash pop,也可能是你已经在上次stash的基础上又开发了一些新功能,从而导致了冲突。反正git本身并不在乎你是否在一个分支A上pop了一个分支B的stash,不产生冲突就算成功。

如何让远程分支也回到过去

你开发了一部分代码,并合并到本地的master,然后推送到了服务器,接着你不想要这部分功能了,所以你在本地的master reset了这部分代码,但是你并不能提交它们(软件定律之一:只做加法,不做减法),你要怎么让远程分支上的代码也回到过去?

git push -f ,虽然你可以这么做,但你的同事会杀了你的。其实不要这部分功能最好的方式是git revert,但是git revert只是针对某次提交做一个反向操作,所以不要以为你revert到某个版本就行了,实际上你应该把每个提交的commit都revert才可以。不过有些直男会选择自己手工改回去。

这里多提一句,revert并不是回到某个版本,而是将某个版本的代码进行撤销。

你想reset,但是你还想利用这些修改

你知道git reset可以让代码回归到某个版本,所以你经常使用git reset —hard来放弃这些修改,但是有一天,你想回到某个版本的状态,同时在这些修改的部分上做一些处理,再次提交,而不是直接扔掉它们你要怎么做?
你可以选择-mixed 来让这部分修改都放到stage,或者用-soft来让这部分修改都放到工作目录中。实际上我之前写过一篇博客介绍: git reset

不过顺便说一句,git reset的用法并不是回到过去这么简单,比如git reset --mixed,看上去是把变化放到暂存区,但其实算是一个把多次commit合并为一次的操作。

但是因为这篇博客讨论的是性价比,所以你并不应该过于详细的了解git reset的底层原理,或者是死记硬背他们,只要记用法和规则就行了,比如:

git reset --hard:完全不要这部分修改。
git reset --mixed:把多次commit 合并为一次的操作。
git reset --soft:在提交前想在做点修改。

如何让某几个文件回归到历史版本?

你并不想要整个commit都回退,你只想让某个文件回退到某个版本。

git log能看修改记录,git log 后面如果跟上 分支名,就能看到某个分支的修改记录,同理,如果你在git log后面跟上 文件名,是可以看到某个文件的修改记录的,然后你只要git reset hash值 文件名就可以了。文件的提交历史和版本的提交历史,看上去是同构的。

如何找回已经被你reset的分支?

你不小心一个 git reset —hard了一些commit,你要如何找回这些commit?

先用git reflog查看操作记录,然后找到那个hash值,

1
git reset --hard xxx

这么说来git reset并不是说做了一个删除的动作,它就是在让当前分支的指针(HEAD)指向某次提交(前提是年代不要太过久远,这些节点还没有被垃圾回收)。所以你在恢复之后,会看到这样的日志:

HEAD is now at xxxx。

这也就是为什么回退和恢复都可以用git reset来操作。

什么时候使用git rebase?

如果一个人对git的某些原理本身并不是很熟悉,他八成推荐你不要用git rebase,就用git merge。但实际上为啥不能用,他也说不出个所以然。现在你就想知道我什么时候应该用git rebase?

git rebase和merge的区别主要在,merge是一个标准的分支合并,让某个分支流入你的分支, 所以你会看到你的两个分支在共同的父节点开叉,又在你合并的节点聚合。但实际上,开发新功能用分支是好的,但是如果你想让一个主分支看上去很干净,就像你的新功能是在你主分支上开发的,
而不是从某一个分支流出再流入,那么rebase是一个不错的选择。在下面这个例子中,C3以rebase的方式变成了C3’。

git-rebase

一般在介绍rebase的时候,都会提到在共同的父节点上,开始选择重放一边,这样C3’看上去就是在C4上开发的,而非C2。还要注意的是C3’不是C3,你可以理解为你的C3不存在了。

那么为什么很多人不建议你用git rebase?因为GIT rebase在重放的时候,解决冲突要一个版本一个版本的重放,比如你已经开发了C8,C9,C10, 那么衍合的时候会产生三个提交:C8’,C9’,C10’,看上去很麻烦。(实际上冲突就是冲突,都是要解决的,你merge也要解决这些,解决不了大不了reset)

不过更重要的原因是,” 一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行衍合操作。“为什么?因为衍合就是变相的在做git reset,那些被衍合的分支,已经算被抛弃了(这也就是为什么你对本地的远程分支衍合需要 push -f),那么如果有人在你被衍和的分支上进行开发,或者merge了你被衍合的代码,那他不得不在把那些被抛弃的分支在引用回来,然后你们的代码commit会变成一层套一层。因为要有现在有两个头需要合并。

那你再仔细想想,为啥rebase要产生三个提交?因为说了rebase是为了让自己的commit是”好像“在目标分支上开发的一样,所以原来有多少次commit现在就有多少次commit。

再说一点有趣的,比如下面这个rebase,会重放C8,C9而非C3,C8,C9。

git-rebase

如何查看某个文件的修改记录?

你们线上出现了bug,原因就是因为某一行愚蠢的代码,你和你的同事们都在为此甩锅,这个时候你想知道到底这行愚蠢的代码是谁写的?

实际上大多数用IntelliJIDEA的同学,都知道在编辑框的左侧右击选择Annotate是可以看到此文件的修改提交记录的,但这说白了还是包装了git操作。那么用命令行怎么玩呢?

1
git blame file_name

还可以指定行数:

1
git blame -L m,n file_name

比较文件的差异。

你平时使用git diff hash1 hash2 来比较两个commit的不同,但是你这时就像看下两次提交中,某个文件有啥不同,

git diff hash1 hash2 – 文件名

但你可能会用diff比较其它一些场景,比如git diff默认是比较工作区和暂存区的差别。
再比如git diff —cached比较暂存区和HEAD的差别。你还可以用git diff HEAD来比较工作区和HEAD的差别。

举这个例子是为了说明,git的很多命令都可以在同一级对象上等价使用,比如commit对commit, branch对branch,文件对文件。因为git本质上就是一个kv数据库。

查看提交记录

相比于git log,我更常用git log --pretty=oneline,因为看起来更方便,直接排列下来有的时候会找错对应的commithash值。