标签:
-- 故国神游,多情应笑我,早生华发。
Git是一个版本控制工具,代码管理工具,团队协作工具。它跟SVN等传统工具实现同样的目的;但从某种程度来说,它更快,更灵活。我想绝大多数读者都已经在接触这个工具了,并且用于日常的项目中去了。我的这篇文章,不是作为一个Git入门教程,也不是作为一本大块头的教科书。(说到教科书,我推荐下面的这本。这本书确实好,很全面。我的这篇文章,其实就是这本书的读书笔记而已。)
Pro Git -- http://git.oschina.net/progit/
接着说。我的这篇文章,主旨在于启发大家超越平时的使用局限,从另一种视角去看待Git和使用Git,让人有一种“啊哈,Git也能这么用“的感觉。这有点王婆卖瓜的味道了,这我必须承认。
很多项目组,对于刚入职的程序员,会像下面这样对他们普及Git的知识:
#使用git clone克隆项目的代码 git clone https://your-project-address #使用git add添加修改的文件 git add your-filename #或者使用git add --all添加所有修改的文件 git add --all #使用git commit提交代码到本地的代码库 git commit -m some-description-message #使用git push提交代码到远程的代码库 git push
#使用git pull拉下远程代码库中更新的代码
git pull
仅使用这五个命令,确实就足够进行团队协作了。这是使用Git的一种模式,而且无可厚非。不过,这样做有种仅仅把Git当成了纯粹的代码共享库的感觉,多多少少显得有点不大规范。其实,Git有更加丰富的能力,它能让你的项目开发有更加规范的流程,整个项目历史有更加清晰的追溯,分工协作上也更加井然有序。
Git的特色有哪些呢?
Git的最大特色可能就是作为一款分布式代码管理工具了。Git的绝大部分操作都是在本地进行的,这使得Git更快,而且更灵活。更灵活是因为在你的改动被提交到远程代码库之前,你可以在本地进行任意的操作。另一方面,这也意味着改动提交到远程被共享之后,就不方便再改动旧的提交历史了。所以,请慎重上传改动到远程。
代码的提交历史是一个项目开发历程的记录。这其中不仅仅是情怀的记录。更重要的是,提交历史给了我们项目回溯的能力,从而让我们能够找出问题代码的来源,并修正它。Git的提交历史很强大,我们可以去翻看历史,还能附加上丰富的筛选条件;甚至可以修改有问题的历史。为了更好地利用Git的历史功能,我们要时刻保持提交历史的细致与干净。这意味着,我们要恰到好处地提交我们的代码,以及给它一个恰到好处的描述。
Git的另一特色就是分支了。简单地说,分支就是不同的代码故事。它们相互区别又互有联系,渐行渐远又能在某一处汇合。使用分支,我们能够控制代码的不同走向,从而很好地安排我们的开发工作。灵活使用分支可以简化我们的很多工作。
说Git简单,听起来有点怪怪的。如果你要了解Git 内部原理,那确实够复杂的。不过,Git的特色在于,即使你不了解这些原理,你仍然可以灵活运用它。只用去学Git的一些基本的概念就足够了,例如仓库,暂存,提交,历史,分支等。说实话,我到现在是说不出这些概念的具体定义的,而且也只是浅要地了解了下这些概念的原理。但是,这并不影响我去使用它。Git的特色在于,学些基本的概念和原理;剩下的,去用就够了。这让我想起了很多的Unix工具,如Vim,LaTex等,以及Unix哲学。
所谓Git哲学,是指使用Git的思维方式。下面是我觉得的Git哲学中最要紧的几点:
如果说提交历史是一个版本控制工具最核心的模块,我觉得一点也不为过。代码的提交历史要涵盖到具体的每一次改动,并且要有清晰一致的描述。这要求我们:
每次的提交得是一个具体的整体。所谓具体的,是指你的提交不能是一次笼统的或概括的改动;所谓整体,是指你的提交是一个已经完成的改动,而不能是悬而未决的。其实,这两点可以从你的提交说明中检验出来:
其实做到这一点也很容易 --
尽可能得多提交,尽可能得早提交,当完成一个小的功能点时就提交。
描述应该简洁明了。简洁是指能够用一句话描述你做的事情,并且不要覆盖其他不必要的信息;明了是指绝对不能含糊,不仅你能看得懂,也要让局外人也能看得懂。我列了几个要点:
由于每个提交都是一个细致的改动单元,当项目进行到一定阶段,整个提交历史一定会显得很长。这时候你可能会觉得提交很多很杂,会淹没一些重要的版本发布提交,例如上线的版本v1.0,v1.1。我想这是不必要的担心,认真地对待提交历史,会让整个提交历史显得多而不杂。而且,完全可以为重要的提交打标签。请记住 git tag 命令,它可以为重要的提交打标签。
Git的提交历史是相当灵活的。你可以查看历史;更厉害的,你还可以去修改它。所以,你可以在任意时刻去修正你的历史。这里有个例外,就是你无法修改远程仓库的提交历史。一方面,有的远程代码仓库不支持提交历史的修改;另一方面,即使支持修改历史,这样的修改也会对其他开发者造成混乱。关于远程提交的准则是:
-- 一个分支就是一个故事,一个剧情。使用分支是为了不让第三者破坏完美的剧情。
要有意识地去使用多分支,而不是去忽略它。可以在下面的情况下考虑使用分支:
这两个使用分支的策略都是本地临时分支使用策略。无论是添加特性分支还是BUG修复分支,它们都有存在周期短的特点,并且都只是为解决某个特定问题而存在的。当这个问题被解决以后,它们就要被并入到主分支,这样就完成了自己的历史使命。使用分支的一种情况是:当你要同时进行多个任务的时候。
无论是主分支,还是上面的特性添加分支或者BUG修复分支,都是你自己一个人的任务。你要同时完成两个或者三个任务。这时候才是分支发挥它最大作用的时候。不要在一个分支里同时做这几件事情,这样会违反第一哲学。记住:
每次只做一个任务;如果不得已要临时切换到其他任务,请使用分支。
Git既存在本地分支也存在远程分支;擅用分支,实际指的是灵活运用本地分支,而不是滥用远程分支。你可以随意地创建本地分支,频繁地在多个分支的下来回工作。但不要把这一切牵扯进远程分支。一般来说,远程分支是长期分支,是作为项目不同的发展阶段或者发展路线而存在的。例如稳定版、开发版、激进版等。这一般是项目决策者操心的事情。
再啰嗦一句:如果分支不再需要了,就删除它。
私有化意味着封装。类似于面向对象上的封装,团队协作上也有封装的概念。你可以在本地灵活地使用Git的各种操作,而不会对他人造成任何影响。不过这种灵活性应局限于本地,而不要将冗余公开给远程的共享库。这方面有些准则,其中有些是对上面两个哲学的总结,如下:
体现Git哲学的最好方式就是实践它,用Git的方式去工作,学会使用Git思维。接下来我会涉及到具体的Git操作。
【这一部分纯当是复习。】
初始化一个新仓库:
git init
从现有仓库克隆:
git clone <your-project-address>
时刻检查目录状态:
git status
.gitignore 文件可以声明脱离版本控制的文件:
#haha
暂存修改:
git add <new-file-or-modified-file> git add --all
如果不小心 git add 了某个文件,使用 git rm 从暂存区移除(附带--cached参数移除跟踪但不删除文件,以便稍后在 .gitignore
文件中补上):
git rm --cached <your-added-file>
想知道代码做了哪些改动(会精确到行),使用:
git diff <some-file> git diff #这会显示所有文件的改动
上面的命令是显示工作目录中当前文件和暂存区域快照之间的差异;如果想知道暂存区域与最近一次提交之间的差异,附加 --cached 参数:
git diff --cached <some-file> git diff --cached
提交更新:
git commit #这会为你打开一个文本编辑器
git commit -m <your-commit-message>
为了更好地说明用法,我使用了JFinal项目的Git源码作为例子。
使用 git log 可以查看提交历史。你应该看到类似于下面的结果:
$ git log
commit 121e247032be9e4d6a3c7eb8035914f59857c43d Author: James <jfinal@126.com> Date: Sun Jul 26 13:40:44 2015 +0800 修改变量名,actoin 改为 action commit d330532f9db493034578d5dac1ece43c65136569 Author: James <jfinal@126.com> Date: Sun Jul 26 11:00:55 2015 +0800 变量名修改
可以看到SHA-1 校验和,作者的信息,时间以及提交说明。Git使用校验和来指代每一次提交,例如输出第一行的 121e247032be9e4d6a3c7eb8035914f59857c43d 。另外,如果不产生歧义的话,可以用校验和的前几个字符来指代提交,例如 121e247032be9e4d6a3c 和 121e247032 。
如果想看某一次具体的提交,指明它的校验和即可:
$ git log d330532f9 commit d330532f9db493034578d5dac1ece43c65136569 Author: James <jfinal@126.com> Date: Sun Jul 26 11:00:55 2015 +0800 变量名修改 commit cdec1c3ceaceecfa4297c72efd8d8095640757f8 Author: James <jfinal@126.com> Date: Thu Jul 16 10:10:49 2015 +0800 修改 Cache.expireAt(...) 方法上的注释
这会显示从指定提交开始往前回溯的提交历史。如果只是想显示指定的这一个提交,用 -1 参数( -2 则显示两次提交,依次类推):
$ git log d330532f9 -1 commit d330532f9db493034578d5dac1ece43c65136569 Author: James <jfinal@126.com> Date: Sun Jul 26 11:00:55 2015 +0800 变量名修改
使用 git log --pretty=oneline 可以显示更紧凑的提交信息,每次提交用一行来显示:
$ git log --pretty=oneline 121e247032be9e4d6a3c7eb8035914f59857c43d 修改变量名,actoin 改为 action d330532f9db493034578d5dac1ece43c65136569 变量名修改 cdec1c3ceaceecfa4297c72efd8d8095640757f8 修改 Cache.expireAt(...) 方法上的注释 749fbe0d8e9da0e33fe2b30085f7467e86881e17 JFinal 2.0 release ^_^ 52633ce2da704cf35a48aa9b8a1afe99d6c2ed1d JFinal 2.0 release ^_^ 1e00c07348bd7b1ed16bb5c224e0a0de67b9b13b JFinal 2.0 release ^_^ 0330d7ed5f3d742eaca201456927d9cecb40e215 JFinal 2.0 release ^_^ aa4a95af60a1dc12dfd649bd208de473dcfb369f jfinal 1.9 release ^_^ af6469eb6f49c23cba8215f2b0e9c8d51cd5f8c9 jfinal 1.9 release ^_^ 4ab71ce41cca8b7d16bef89655b51e6de6548d30 jfinal 1.9 release ^_^ 03a3c5a2bdb848ad2f9a30e29eba4f468176497f jfinal 1.9 release ^_^
使用 -p 选项展开每次提交所做的修改。例如我们使用下面的命令展开哈希前缀为 d330532f9 的那次提交的修改:
$ git log d330532f9 -p -1 commit d330532f9db493034578d5dac1ece43c65136569 Author: James <jfinal@126.com> Date: Sun Jul 26 11:00:55 2015 +0800 变量名修改 diff --git a/.gitignore b/.gitignore index 416db66..b1e58ba 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ integration-repo /build/ # IDEA metadata and output dirs +/.idea/^M *.iml *.ipr *.iws diff --git a/src/com/jfinal/config/Routes.java b/src/com/jfinal/config/Routes.ja index fab2604..8ad1b03 100644 --- a/src/com/jfinal/config/Routes.java +++ b/src/com/jfinal/config/Routes.java @@ -86,11 +86,11 @@ public abstract class Routes {
使用 --stat 选项仅显示代码修改的统计信息(仅显示简要的增删行数统计,特别适合作代码审查):
$ git log --stat commit 121e247032be9e4d6a3c7eb8035914f59857c43d Author: James <jfinal@126.com> Date: Sun Jul 26 13:40:44 2015 +0800 修改变量名,actoin 改为 action src/com/jfinal/core/ActionMapping.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) commit d330532f9db493034578d5dac1ece43c65136569 Author: James <jfinal@126.com> Date: Sun Jul 26 11:00:55 2015 +0800 变量名修改 .gitignore | 1 + src/com/jfinal/config/Routes.java | 6 +++--- src/com/jfinal/plugin/activerecord/Sqls.java | 4 ++-- src/com/jfinal/plugin/activerecord/tx/TxByMethods.java | 14 +++++++------- src/com/jfinal/plugin/redis/RedisInterceptor.java | 11 ++++++++++- 5 files changed, 23 insertions(+), 13 deletions(-)
我从《Pro Git》中摘出了一些选项及说明:
选项 说明 -p 按补丁格式显示每个更新之间的差异。 --stat 显示每次更新的文件修改统计信息。 --shortstat 只显示 --stat 中最后的行数修改添加移除统计。 --name-only 仅在提交信息后显示已修改的文件清单。 --name-status 显示新增、修改、删除的文件清单。 --abbrev-commit 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。 --relative-date 使用较短的相对时间显示(比如,“2 weeks ago”)。 --graph 显示 ASCII 图形表示的分支合并历史。 --pretty 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。
Git提交历史的一大特色在于可以按照条件筛选历史。例如筛选出最近两周到最近一周的提交历史:
$ git log --since=2.weeks --until=1.weeks commit cdec1c3ceaceecfa4297c72efd8d8095640757f8 Author: James <jfinal@126.com> Date: Thu Jul 16 10:10:49 2015 +0800 修改 Cache.expireAt(...) 方法上的注释
我也列出了一些能够设置筛选条件的一些选项,同样来源于《Pro Git》:
选项 说明 -(n) 仅显示最近的 n 条提交 --since, --after 仅显示指定时间之后的提交。 --until, --before 仅显示指定时间之前的提交。 --author 仅显示指定作者相关的提交。 --committer 仅显示指定提交者相关的提交。 -- 命令的最后一个选项,后跟路径名表示只关心某一路径下的提交历史
Git提交历史的另一大特色在于你可以修改它。不过格外注意的是,不要修改那些已经推送到远程版本库的提交历史。
首先介绍如何撤销。这多少与修改历史无关,因为此时的历史可能还未形成。
如果只是希望将某个文件的改动从暂存区拿出来,而保持文件原有内容不变,使用命令:
git reset HEAD <your-file>
其中HEAD指代最近一次提交(*)。你可以使用HEAD变量来避免使用哈希值。
下述命令会令整个项目的状态回退(保留修改):
git reset HEAD
如果想将文件恢复到最近一次提交的版本,使用命令 git checkout 。这会使得其从暂存区拿出来(如果已经存入暂存区),并且文件内容恢复到最近一次提交的状态:
git checkout <your-file>
如果想直接撤销所有改动(包括修改),使用命令:
git reset --hard HEAD #恢复所有改动到最近一次提交
git reset --hard HEAD~1 #恢复所有改动到最近一次提交的上一次提交
git reset --hard HEAD~2 #恢复所有改动到最近一次提交的前两次提交
git reset --hard d330532 #恢复到指定提交
带有 --hard 选项的版本回退是一个不可恢复的操作,请谨慎用之。
当我们完成提交后,发现漏掉了某些改动(例如忘记添加相应的API注释)。这时我们当然可以将改动作为一次新的提交提交一发。不过,还有其他的方案,即将漏掉的改动重新补回到对应的历史提交中,毕竟漏掉的改动确实是那次提交的一部分。Git给了我们修改提交的能力。
如果只是要修改最近的一次提交,使用 git add 命令暂存相应的改动,然后输入命令 git commit --amend 。这会把你代入修改提交说明的编辑器中。保存后退出,我们就完成了我们的提交修改。
如果只是想修改最近一次提交的提交说明,直接输入 git commit --amend ,然后进入编辑器修改提交说明并保存退出即可。
要修改历史中更早的提交,就要用到 git rebase -i 交互式的提交历史修改工具。我在命令行中输入:
git rebase -i HEAD~4
它会带我进入一个文本编辑器中(在我的系统里意外地是VIM)。关注前四行,会看到我们可以修改最近的四次提交历史( HEAD~4 指定)。其中提交由早到晚地排列,与 git log 显示顺序相反。
1 pick 749fbe0 JFinal 2.0 release ^_^ 2 pick cdec1c3 修改 Cache.expireAt(...) 方法上的注释 3 pick d330532 变量名修改 4 pick 121e247 修改变量名,actoin 改为 action
这时我们可以修改某次提交,例如修改第2行的提交,只需将 pick 换成 edit 即可。然后保存并退出编辑器。
pick 749fbe0 JFinal 2.0 release ^_^ edit cdec1c3 修改 Cache.expireAt(...) 方法上的注释 pick d330532 变量名修改 pick 121e247 修改变量名,actoin 改为 action
像之前一样,使用 git commit --amend 修改提交;最后使用 git rebase --continue 完成修改任务。
我们也可以删除某个提交,只需将相应的提交行删掉即可。例如删除第2行的提交:
pick 749fbe0 JFinal 2.0 release ^_^ pick d330532 变量名修改 pick 121e247 修改变量名,actoin 改为 action
重新排列提交也是OK的。只需要重新排列即可。例如下面的编辑把最近一次提交移到最前,最早的一次提交移到最近:
pick 121e247 修改变量名,actoin 改为 action pick cdec1c3 修改 Cache.expireAt(...) 方法上的注释 pick d330532 变量名修改 pick 749fbe0 JFinal 2.0 release ^_^
一个有用的功能是压制提交,即把多个提交合并成一次提交。例如下面的编辑将多个描述都为”JFinal 2.0 release ^_^“的提交合并为一个提交:
pick 0330d7e JFinal 2.0 release ^_^ squash 1e00c07 JFinal 2.0 release ^_^ squash 52633ce JFinal 2.0 release ^_^ squash 749fbe0 JFinal 2.0 release ^_^ pick cdec1c3 修改 Cache.expireAt(...) 方法上的注释
相应地,可以拆分一个提交。它的技巧在于 edit 某次提交,然后调用 git reset HEAD^ 回到父提交,然后再多次 git commit 即可。
git reset HEAD^ git add file1 git commit -m "add file1" git add file2 git commit -m "add file2" git rebase --continue
当准备进入交互式rebase工具时,如果当前的工作区存在修改而没有被提交,则会被禁止进入。此时我们可以先将修改提交;有时又不希望这么做,可能我们的代码改动还不能构成一个完整的提交。我们只希望先保存自己的工作,然后在需要的时候释放出来。这时就用到 git stash 命令了。
我们可以将改动保存在一个栈中,称之为储藏(Stashing):
git stash
然后查看栈状态:
$ git stash list stash@{0}: WIP on master: 121e247 修改变量名,actoin 改为 action
因为是栈,可以多次调用 git stash ,然后查看栈状态:
$ git stash list stash@{0}: WIP on master: 121e247 修改变量名,actoin 改为 action stash@{1}: WIP on master: 121e247 修改变量名,actoin 改为 action stash@{2}: WIP on master: 121e247 修改变量名,actoin 改为 action
然后在适当的时候,释放一个储藏。既可以释放指定的储藏,例如 stash@{0} 、 stash@{1} 、 stash@{2},也可以默认释放最近的储藏(stash@{0}):
git stash apply git stash apply stash@{2}
当储藏不再需要时,删除它:
git stash drop stash@{2}
或者立即释放最近的储藏并删除它:
git stash pop
储藏的典型应用场景是当工作区有未提交的修改时,要切回到旧提交去修改( git rebase -i )或者切换到其他分支( git checkout )。
Git的分支是从某个分支引出的一个分叉。在Git中创建分支时,一定是以某个分支为基准,这个分支就是你当前工作的分支。
要查看当前在哪个分支下工作,输入命令:
git branch
此时会列出所有本地分支,行首标有*号的是当前工作分支。
创建分支:
git branch <new-branch-name>
切换到新分支:
git checkout <new-branch-name>
同时创建及切换分支:
git checkout -b <new-branch-name>
合并分支:
git checkout master
git merge <new-branch-name>
这能保证将新分支的改动合并到主分支。
删除分支:
git branch -d <new-branch-name>
只有已被合并的分支才能顺利删除,否则会提示错误。
如果要强制删除分支,使用命令:
git branch -D <new-branch-name>
以上只是分支的基本操作命令。要想活用这些命令,就要知道分支的使用思维。分支的使用思维,我觉得就是一句话,重复之前的一句话:
当你要放下手中的任务,临时切换到其他任务时,使用分支。
我这里想举一个关于分支使用的简单的不能再简单的例子。
首先你在进行master上进行主线开发,实现功能点一。突然你临时接到任务,完成功能点二,并马上上线。此时你不得不放下手中的工作,投入到实现功能点二中去。
这时,你首先要做的是保存你正在进行的工作以便将来可以恢复。可以使用储藏 git stash 或者临时提交你的代码以在将来通过 git commit --amend 修改你的历史。这里假设你使用的是储藏。
然后新建分支并切换到新分支工作:
git checkout -b feature2
当你完成功能点二的开发时,提交你的代码:
git add --all git commit -m "finish feature 2"
之后切换到主分支,合并feature2分支:
git checkout master
git merge feature2
改动可以提交到远程上线:
git push
然后你可以使用 git stash pop 恢复你的工作。
队伍中的长期分支控制了不同的开发进度。一般来说,应该有一个稳定分支和开发分支:你可以将master作为稳定分支,并配有一个develop分支;或者反过来,将master作为开发分支,并配有一个stable分支。这里假设master是稳定分支,而develop是开发分支。develop分支一般比master要超前,并且当测试稳定后才会并入到master分支发布。所以一般的工作流程就是:
在develop分支开发,测试稳定后并入到master分支发布
另外,如果已发布的版本遭遇到一个紧急BUG亟待修复,这时你应该保存develop分支的工作,然后切换到master分支去修复。因为要紧急发布,你应当切换到稳定的master分支完成BUG修复,而不是基于不稳定develop分支。当修复完毕并发布后,再回到develop分支恢复工作。
利用分支,我们可以很好地分离了我们的不同工作,不让它们相互干扰,从而减少bug的来源;利用历史,我们可以追踪代码的变化,为我们找出问题代码提供了途径。其实Git也为我们提供了其他的好用的工具,利于我们调试。
git blame
:谁动了我的代码你是否会惊奇你的代码为什么突然变成这副模样?没关系,使用 git blame 命令可以显示你的代码是谁最后修改的。命令格式:
git blame -L 12,22 simple.rb
可以显示文件simple.rb的第12至22行这块代码最后是谁、什么时候修改并提交的。
还有一个有趣的命令是 git bisect 。它可以以二分查找的策略逐步逼近你在意的坏代码的来源。
首先输入 git bisect start 来启动二分查找。
输入 git bisect bad 来告知当前的提交已经是问题提交了。
然后输入 git bisect good <good-commit> 来告知你知道的最晚的正常提交。
接着就可以确定坏代码的来源在<good-commit>和当前提交之间,二分查找策略就可以开启了,这时Git监测处于中间的一个提交。假设从<good-commit>到当前提交一共有12个提交,记编号为0、1、…、11. 这时我们检出的提交应该是6.
你可以输入 git bisect good 来告知这个提交是正常的,这样它会舍弃编号为0、1、…、6的提交;也可以输入 git bisect bad 来告知问题依然存在,这样它会舍弃编号为7、8、…、11的提交。然后继续二分策略…一直到我们只剩下一个问题提交的时候。这个提交就是我们的问题提交的最初来源。
当你完成的时候,应该运行 git bisect reset 重新回到最初的地方。
Git是伟大的,但我们依然要超越Git;毕竟Git只是一个工具,而我们是使用工具的人。Git的功能是代码管理和版本控制,但Git的本质在于其重视团队,重视协作的开发精神。它重视协作,但又不束缚个人的创造。它使得每个人都可以自由地编码,同时又保持整个项目开发的协调一致。
标签:
原文地址:http://www.cnblogs.com/starstone/p/4687020.html