git使用技巧
转载自:http://172.17.144.8/iceway.zhang/shares/201604/201604_git_tips.md.html
我们在工作中几乎每天都会用到git,由于git的特性非常多,而且支持很多命令及参数,学习曲线比较陡峭。这里总结一些git的使用技巧,希望能通过学习本文内容掌握更多git的技巧以提高使用效率。
理解git原理
这里有一张git命令和对应的数据传输图,理解了这个图就基本理解了git的命令操作原理,后面学习git的命令时会事半功倍。
- stash:临时保存区,
git stash
命令的数据在这里保存。 - workspace:工作区,
git checkout
命令检出的内容。 - index:暂存区(也可以称为索引区),
git add
命令将改动保存到暂存区,之后的commit
会把暂存区的内容提交。 - local repository:本地仓库,
clone
及fetch
命令拉取的数据库内容,commit
后就会记录在本地仓库。 - remote repository:远程仓库,
push
命令将本地仓库的内容推送到远程仓库。
配置
使用ssh密钥访问ssh协议的仓库
我们的代码服务器是通过ssh协议访问的,需要和远程仓库交互时都要输入密码,使用ssh密钥认证的方式可以省去这一步骤。
- 生成ssh密钥,通过
ssh-keygen
命令生成一对密钥。
注意:生成RSA密钥对的过程中,一般为安全起见应该给RSA私钥文件加密,这样即使RSA私钥文件泄漏也可以多一层保护。但是给私钥创建了密码的话,以后每次使用的时候还得输入私钥文件的密码以读取私钥,我们的目的是不想每次git和远程仓库交互都要手动输入密码,而且都是在公司内网中使用,所以这里没有给RSA私钥加密。
- 将RSA公钥拷贝到远程服务器。
生成的RSA密钥对,私钥文件不能泄漏出去,而公钥文件可以任意分发。将公钥拷贝到远程服务器,下次使用ssh协议访问该服务器时会自动读取本地的私钥文件做认证。通过命令ssh-copy-id
可以将公钥拷贝到远程主机的合适位置下。
注意:这里运行ssh-copy-id命令,还要输入一次登录dnisever的密码,因为此时服务器上还不存在你的公钥文件。一旦该命令执行完成,以后通过ssh协议访问dniserver就不用再输入密码。
别名
有些命令有很长的参数,git可以像shell中的alias一样,自己定义一些命令(+参数)的别名。比如,git的配置文件中有以下别名定义:
以后,当执行git st
时,就等于执行了git status
,当执行git graph
时,就等于执行了git log --graph --pretty=format:'%C(yellow)%h %C(blue)%d %C(reset)%s %C(white)%an, %ar%C(reset)
。
添加改动
交互式添加改动块
场景:fix了某个bug,准备提交时发现改动了太多,想要把这些改动分成更小的补丁提交。
技巧:使用git add -p [file,...]
命令,会自动将所有改动分成较小的块依次显示,并让你确认是否要把这一块加入本次提交中。如下面的示例,git add -p
没有跟具体文件名,则会把当前工作区的所有改动自动分割成较小的块,且让用户确认是否要将当前的块加入到暂存区。这里给出的提示是Stage this hunk [y,n,q,a,d,/,e,?]?
,常用的可输入的字符含义如下:
- y:把该块加入暂存区,并继续进入下一块。
- n:不要把该块加入暂存区,跳过并进入下一块。
- q:直接退出,不要再查看后面的改动块。
- a:把该块和后续的所有改动块都加入暂存区并退出。
- d:不要把该块和后续所有在同一个文件中的改动块加入暂存区,然后继续。
- /:搜索一个符合给定正则表达式的改动块。
- e:手动编辑改动块(如果自动分割的最小改动块还是不够小,可以手动编辑以选择具体要把哪些改动加入暂存区)。
- s:如果当前自动分割的块不够小,键入s可以让git尝试将当前块分割成更小的块。
- ?: 输出所有可用命令的帮助信息。
交互式添加文件
场景:加入几个新文件、删除了某些文件、且修改了其中几个文件,同时还已经通过git add
把几个文件加入了暂存区。现在想要把已经加入暂存区的某个文件从暂存区移除(本次提交不准备包含该文件),另外把其他的一些文件加入暂存区。
技巧:使用git add -i
命令,会自动列出所有可能想提交到暂存区的文件,让用户选择哪些要加入、哪些不要;此外还可以选择把已经加入暂存区的某些文件移出暂存区。该命令执行后,提示菜单中可以选择的有:
- status:列出当前已追踪文件(也就是说在之前的历史中已经有的文件)的状态,默认会显示该命令的状态。
- update:选择要把哪些已经在追踪历史中但是还没加入暂存区的且有改动的文件加入暂存区。
- revert:从暂存区移出文件。
- add untracked:可以把未追踪的文件(也就是新创建的文件,之前历史中没有)加入暂存区。
- patch:类似update,但是选择文件后进入交互式添加改动块的模式,相当于选择某个文件后会自动执行
git add -p <file>
。 - diff:可以查看某个文件的具体改动差异。
- quit:直接退出交互式添加状态。
- help:查看帮助文件。
追加改动
场景:当前已经在本地提交的内容,发现还有一点点改动应该加入到上次提交中。
技巧:使用命令git commit --amend
实现。当你commit提交之后,如果还要再给上次提交加入内容,直接编辑文件加入所需改动,然后也是git add <file>
把你改动的文件加入到暂存区,接下来执行git commit --amend
,这个命令会把当前暂存区的内容追加到上次的提交中,而不是新建一次提交。
当然,你也可以不做任何改动,直接执行git commit --amend
,这可以让你能够修改上次的提交信息。
快速操作
临时保存未提交的内容
场景:正在这个分支上改动,而且所有的改动都还没有提交,突然有紧急事情要把当前分支reset到之前的某次提交上(比如要在之前的某次提交上编译)。
技巧:使用git stash
暂存当前未提交的改动。git stash常见用法如下:
-
git stash
:将当前所有未提交的改动保存到临时保存区,包括已经提交到暂存区的和没有提交到暂存区的。 -
git stash list
:列出当前临时保存区所有的保存内容。 -
git stash apply [<stash>]
:将对应的某次保存内容(如过没指定<stash>
,则使用最后一次保存)恢复到当前工作区。 -
git stash pop [<stash>]
:类似apply,但是在恢复之后把这次保存的内容从临时保存区删除。
不切换分支,获取某个文件在另外分支的内容
场景:在当前分支改动时突然想到某个文件可以直接使用其在另一个分支上的内容。
技巧:命令git checkout <branch> -- <file_path>
可以是实现。
比如当前在分支A上工作,改动过程中想起文件src/action.c
可以使用分支B中的同名文件,那么可以使用如下命令:
获取某个文件在某次提交时的内容
场景:正在debug一个问题,发现是某个脚本执行不正确导致,但是找不到具体的错误。可以确定的是以前的某个版本中该脚本没有问题,想看看之前这次版本中的内容。
技巧:命令git show <commit>:<file_path>
可以实现。
比如当前正在改动时,先看看Makefile这个文件在标签net-cgi-dni007
对应的内容,命令如下:
git show net-cgi-dni007:Makefile
查看某个文件在不同版本间的差异
场景:想知道文件src/action.c
在TAGA和TAGB之间的差异。
技巧:命令git diff <start_commit>..[<end_commit>] -- <file_path>
命令实现,如果end_commit
没有给出,默认是HEAD。
比如,下面的命令可以看到net-cgi-dni005
到net-cgi-dni007
之间对文件src/apply.c
的所有修改:
git diff net-cgi-dni005..net-cgi-dni007 -- src/apply.c
查看所有的历史提交,及其与标签和分支的对应关系
场景:想快速知道各个tag和分支都对应到哪些提交历史,不同tag之间都有哪些改动。
技巧:命令git log --pretty=oneline --graph --decorate=full
可以实现,如下:
要想查看git提交历史,标签及各分支对应关系,表现最好的还是tig工具。
分支操作
查看已合并或未合并的分支
场景:有多个分支,现在想知道那些分支还没有合并到主干分支,哪些分支已经合并到主干分支。
技巧:使用git branch --merged
或者git branch --no-merged
命令。--merged[=commit]
参数可以列出本地分支中已经合并进commmit的分支:也就是说从commit这里往上追踪历史,一定能到到这这些分支的最顶部的提交。--no-merged[=commit]
参数可以列出本地分支中还没有合并进commit的分支。如果没有给定commit参数,默认是HEAD。
根据commit找分支
场景:有很多个分支,现在只知道使用了其中某个commit id对应的代码,想找到包含这个commit id的分支,添加新的功能或者fix bug。
技巧:使用git branch -a --contains=[commit]
命令,commit可以是某个具体的commit id、或者tag。这个命令会列出所有本地分支和远程分支中已经包含了给定commit的分支。
查看所有分支和所有提交的对应关系
场景:有多个分支,现在想了解所有的提交中每个分支上都有哪些提交。
技巧:使用git show-branch
命令可以实现。最上面逐行缩进的部分列出了要显示的所有分支信息,最前面彩色的*
和!
表示下面显示这个分支对应的提交时使用的颜色,*
代表当前分支,!
代表其他分支。
下面就开始显示所有的commit提交(直到某一次commit是所有列出的分支都包含的为止),某个分支对应的这一竖列中,空格表示这行commit在这个分支中没有,*
和+
表示这行commit在这个分支中存在(*
表示当前分支)。
合并
场景:你是某个仓库的admin,今天收到两个人发过来补丁,而且这个两个人的补丁修改了相同的文件导致补丁有点冲突。也就是说先git am
了A的补丁,则git am
B的补丁会失败,反之先应用B的补丁,则A的补丁不能打上去。
技巧:使用git merge
命令合并,让git尝试自动修复冲突,如果不能自动修复,则git会把所有改动都加入到问家中,让你手动编辑文件中冲突的地方。
此时,可以先创建一个临时分支(假设名字为temp_br
),然后把A的补丁应用在当前分支上,而把B的补丁应用在临时分支上,然后在当前分支执行git merge temp_br
,git会自动把临时分支上最新改动合并到当前分支,如果有冲突不能自动解决,则手动打开冲突文件并编辑内容(冲突标记<<<<<<<
与=======
之间的内容是当前分支修改的,=======
与>>>>>>>
之间的内容是要合并的分支修改的)然后保存你想要保存的代码,然后再运行git add <file>; git commit
即可。
重演
场景:为了fix某个bug,添加了5次提交,然后才发现第2次提交的内容中有一点小问题需要修改。
技巧:使用git rebase -i <commit>
重演历史。比如git rebase -i HEAD~5
会弹出如下的交互界面,可以按照下面的命令提示修改没一行的前缀实现自定义编辑。对于这个场景,我们应该把第2行前面的“pick”改成”e”,然后退出。这样git会自动回退到第2次提交,然后停下来,这时你再改正代码并提交之后,然后键入git rebase --contine
,git会自动把后面剩下的3次提交按顺序应用。
对已经push到远程仓库的历史,不要使用rebase,因为rebase命令会破坏历史。
历史追踪
找回已删除的历史
场景:为解bug忙碌了半天,不小心把HEAD reset到之前的某次提交导致刚才所做的提交都丢失。
技巧:命令git reflog
可以找回丢失的提交。
假设刚才做了3次提交,但是不小心git reset --hard HEAD~10
,这样前面的7次提交可以从远程仓库pull下来,但是最后的3次提交还没有push到服务器,现在通过HEAD也找不到这三次提交,找回步骤如下:
-
git reflog
,显示内容类似下面的示例: 这里看到最上面一行是说HEAD往前移动了10次后,现在的HEAD指向
e511cd8
。第2行的意思是说在第1行的动作之前HEAD指向1500fa5
,所以要恢复之前的历史,只要把HEAD重新指向这个commit id就行。-
git reset --hard 1500fa5
,即可找回之前的历史。
git log -g
作用和git reflog
相同,只是这个命令以log形式显示HEAD指针的历史。
注意:之所以能找回删除历史,是因为所谓的“删除”只是移动了HEAD指针而导致之前的commit id不能通过一个好记的名字访问到,这些“删除”的历史提交还是在仓库的数据库中(
.git
目录)。如果删除掉.git
目录、或者执行了git的垃圾回收而导致这些“删除”的提交真的不存在,就没办法再找回来。
查看某行改动是从哪次提交引入的
场景:某个文件中有一行内容和需求有点冲突,想知道是在哪次commit中引入了这个改动,看看是不是当时提交的需求和现在的需求冲突。
技巧:使用git blame <file>
命令实现。该命令会打开给定file,并在每行内容前面显示这行内容是从哪次commit中由谁在什么时候引入的。
git blame命令有以下几个参数可以忽略一些干扰内容。
延伸阅读
最好的学习方式就是经常反复阅读经典书籍,而且要在实际工作中坚持使用学到的新内容。这里推荐两本学习git的经典书籍,有空可以多看看。
[1] Git Magic
[2] Pro Git