Devil May Code...

Vergil's Blog

来源:Is memcached a dinosaur in comparison to Redis?(其他人的回答同样值得一看)


这两年Redis火得可以,Redis也常常被当作Memcached的挑战者被提到桌面上来。关于Redis与Memcached的比较更是比比皆是。然而,Redis真的在功能、性能以及内存使用效率上都超越了Memcached吗?

下面内容来自Redis作者在stackoverflow上的一个回答,对应的问题是《Is memcached a dinosaur in comparison to Redis?》(相比Redis,Memcached真的过时了吗?)

  • You should not care too much about performances. Redis is faster per core with small values, but memcached is able to use multiple cores with a single executable and TCP port without help from the client. Also memcached is faster with big values in the order of 100k. Redis recently improved a lot about big values (unstable branch) but still memcached is faster in this use case. The point here is: nor one or the other will likely going to be your bottleneck for the query-per-second they can deliver.

  • 没有必要过多的关心性能,因为二者的性能都已经足够高了。由于Redis只使用单核,而Memcached可以使用多核,所以在比较上,平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。说了这么多,结论是,无论你使用哪一个,每秒处理请求的次数都不会成为瓶颈。(比如瓶颈可能会在网卡)

  • You should care about memory usage. For simple key-value pairs memcached is more memory efficient. If you use Redis hashes, Redis is more memory efficient. Depends on the use case.

  • 如果要说内存使用效率,使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。当然,这和你的应用场景和数据特性有关。

  • You should care about persistence and replication, two features only available in Redis. Even if your goal is to build a cache it helps that after an upgrade or a reboot your data are still there.

  • 如果你对数据持久化和数据同步有所要求,那么推荐你选择Redis,因为这两个特性Memcached都不具备。即使你只是希望在升级或者重启系统后缓存数据不会丢失,选择Redis也是明智的。

  • You should care about the kind of operations you need. In Redis there are a lot of complex operations, even just considering the caching use case, you often can do a lot more in a single operation, without requiring data to be processed client side (a lot of I/O is sometimes needed). This operations are often as fast as plain GET and SET. So if you don’t need just GEt/SET but more complex things Redis can help a lot (think at timeline caching).

  • 当然,最后还得说到你的具体应用需求。Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果你需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。


我想大家在写CSS的时候应该都对清除浮动的用法深有体会,今天我们就还讨论下clearfix的进化史吧。

clearfix清除浮动

首先在很多很多年以前我们常用的清除浮动是这样的。

.clear{clear:both;line-height:0;}

现在可能还可以在很多老的站点上可以看到这样的代码,相当暴力有效的解决浮动的问题。但是这个用法有一个致命伤,就是每次清除浮动的时候都需要增加一个空标签来使用。

这种做法如果在页面复杂的布局要经常清楚浮动的时候就会产生很多的空标签,增加了页面无用标签,不利于页面优化。但是我发现大型网站中居然还在使用这种清楚浮动的方法。有兴趣的同学可以上他们首页搜索一下他们的.blank0这个样式名称。

因此有很多大神就研究出了 clearfix 清除浮动的方法,直接解决了上面的缺陷,不需要增加空标签,直接在有浮动的外层加上这个样式就可以了,这也是我们今天要讨论的clearfix进化史。

起源

.clearfix:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: " ";
    clear: both;
    height: 0;
}
.clearfix { display: inline-table; }

* html .clearfix{height: 1%;}//Hides from IE-mac
.clearfix {display: block;}//End hide from IE-mac

解释一下以上的代码:

  • 对大多数符合标准的浏览器应用第一个声明块,目的是创建一个隐形的内容为空的块来为目标元素清除浮动。

  • 第二条为clearfix应用 inline-table 显示属性,仅仅针对IE/Mac。利用 * 对 IE/Mac 隐藏一些规则:

  • height:1% 用来触发 IE6 下的haslayout。

  • 重新对 IE/Mac 外的IE应用 block 显示属性。

  • 最后一行用于结束针对 IE/Mac 的hack。(是不是觉得很坑爹,Mac下还有IE)

起源代码可能也是很早期的时候了,再往后Mac下的IE5也发展到IE6了,各种浏览器开始向W3C这条标准慢慢靠齐了。所以就有了下面这个写法出现了。

.clearfix:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: " ";
    clear: both;
    height: 0;
}

* html .clearfix { zoom: 1; } /* IE6 */
*:first-child+html .clearfix { zoom: 1; } /* IE7 */

IE6 和 IE7 都不支持 :after 这个伪类,因此需要后面两条来触发IE6/7的haslayout,以清除浮动。幸运的是IE8支持 :after 伪类。因此只需要针对IE6/7的hack了。

在一个有float 属性元素的外层增加一个拥有clearfix属性的div包裹,可以保证外部div的height,即清除"浮动元素脱离了文档流,包围图片和文本的 div不占据空间"的问题。

Jeff Starr 在这里针对IE6/7用了两条语句来触发haslayout。我在想作者为什么不直接用 * 来直接对 IE6/7 同时应用 zoom:1 或者直接就写成:

.clearfix:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: " ";
    clear: both;
    height: 0;
}
.clearfix{*zoom:1;}

但是对于很多同学这种优化程度代码还是不够给力,clearfix 发展到现在的两个终极版。

重构clearfix浮动

构成Block Formatting Context的方法有下面几种:

  • float的值不为none。

  • overflow的值不为visible。

  • display的值为table-cell, table-caption, inline-block中的任何一个。

  • position的值不为relative和static。

很明显,float和position不合适我们的需求。那只能从overflow或者display中选取一个。

因为是应用了.clearfix和.menu的菜单极有可能是多级的,所以overflow: hiddenoverflow: auto也不满足需求
(会把下拉的菜单隐藏掉或者出滚动条),那么只能从display下手。

我们可以将.clearfix的display值设为table-cell, table-caption, inline-block中的任何一个

但是display: inline-block会产生多余空白,所以也排除掉。

剩下的只有table-cell, table-caption,为了保证兼容可以用display: table来使.clearfix形成一个Block Formatting Context
因为display: table会产生一些匿名盒子,这些匿名盒子的其中一个(display值为table-cell)会形成Block Formatting Context。

这样我们新的.clearfix就会闭合内部元素的浮动。

后面又有人对此进行了改良:

终极版一:

.clearfix:after {
    content:"\200B";
    display:block;
    height:0;
    clear:both;
}
.clearfix {*zoom:1;}/*IE/7/6*/

解释下:content:"\200B";这个参数,Unicode字符里有一个“零宽度空格”,即 U+200B,代替原来的“.”,可以缩减代码量。而且不再使用visibility:hidden

终极版二:

.clearfix:before,.clearfix:after{
    content:"";
    display:table;
}
.clearfix:after{clear:both;}
.clearfix{
    *zoom:1;/*IE/7/6*/
}

这两个终极版代码都很简洁,终极版一和二都可以使用,以上代码都经过测试,大家赶紧用一下吧,如果你还停留在clearfix的老代码的时候就赶紧更新一下代码吧。


从远程仓库获取

上一篇中我们把在GitHub上新建的仓库设置成了远程仓库,并向这个仓库push了feature-D分支。现在,所有能够访问这个远程仓库的人都可以获取feature-D分支并加以修改。

我们从实际开发者角度出发,在另一个目录下新建一个本地仓库,学习从远程仓库获取的相关操作。这就相当于刚刚执行过push操作的的目标仓库又有了另一名开发者来共同开发。

git clone —— 获取远程仓库

首先要切换到其他目录下,将GitHub上的仓库clone到本地。注意不要与之前的仓库在同一目录下。

➜  ~  cd Github 
➜  Github  git clone https://github.com/VergilLai/git-test.git
Cloning into 'git-test'...
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 20 (delta 3), reused 20 (delta 3), pack-reused 0
Unpacking objects: 100% (20/20), done.
Checking connectivity... done.
➜  Github  cd git-test 
➜  git-test git:(master) ls
README.md

执行git clone命令后会默认处于master分支下,同时系统会自动将origin设置成该仓库的标识符。也就是说,当前本地仓库的master分支雨GitHub端远程仓库(origin)的master分支在内容上时完全相同的。

➜  git-test git:(master) git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/feature-D
  remotes/origin/master

git branch -a命令查看当前分支的相关信息。添加-a参数可以同时显示本地仓库和远程仓库的分支信息。

结果中显示了remotes/origin/feature-D,证明远程仓库中已经有了feature-D分支。

获取远程的feature-D分支

将feature-D分支获取至本地仓库。

➜  git-test git:(master) git checkout -b feature-D origin/feature-D 
Branch feature-D set up to track remote branch feature-D from origin.
Switched to a new branch 'feature-D'

-b参数的后面是本地仓库中新建分支的名称。为了便于理解,仍将其命名为feature-D,让它与远程仓库的对应分支保持通明。新建分支名称后面时获取来源的分支名称。例子中指定了origin/feature-D,就时说以名为orgin的仓库(这里指GitHub仓库)的feature-D分支为来源,在本地仓库中创建feature-D分支。

向本地的feature-D分支提交更改

现在假设是另一名开发者,要做一个新的提交。在README.md中添加一行文字,查看更改。

➜  git-test git:(feature-D) echo "feature-D" >> README.md 
➜  git-test git:(feature-D) ✗ git diff

diff --git a/README.md b/README.md
index 66308ad..d1ff7a6 100644
--- a/README.md
+++ b/README.md
@@ -3,3 +3,4 @@ feature-A
 fix-B
 feature-C
+feature-D

➜  git-test git:(feature-D) ✗ git commit -am "Add feature-D"
[feature-D 3605345] Add feature-D
 1 file changed, 1 insertion(+)

推送feature-D分支

现在来推送feature-D分支。

➜  git-test git:(feature-D) git push
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 284 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/VergilLai/git-test.git
   98c18e6..3605345  feature-D -> feature-D

从远程仓库获取feature-D分支,在本地仓库中提交更改,再将feature-D分支推送回远程仓库,通过这一系列操作,就可以与其他开发者相互合作,共同培育feature-D分支,实现某些功能。

git pull —— 获取最新的远程仓库分支

现在我们放下刚刚操作的目录,回到原先的那个目录下。这边的本地仓库中只创建feature-D分支,并没有在feature-D分支中进行任何提交。然后远程仓库的feature-D分支中已经有了刚刚推送的提交。这时就可以使用git pull命令,将本地的feature-D分支更新到最新状态。

➜  git-test git:(master) git pull origin feature-D 
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/VergilLai/git-test
 * branch            feature-D  -> FETCH_HEAD
   98c18e6..3605345  feature-D  -> origin/feature-D
Updating 98c18e6..3605345
Fast-forward
 README.md | 1 +
 1 file changed, 1 insertion(+)
➜  git-test git:(master) cat README.md 
# Git基本操作
feature-A
fix-B
feature-C
feature-D

GitHub端远程仓库中的feature-D分支是最新状态,所以本地中的feature-D就会得到更新。今后只需要像平常一样在本地进行提交再push给远程仓库,就可以与其他开发同时在一个分支中进行作业,不断给feature-D增加新功能。

如果两人同时修改了同意部分源代码,push时就很容易发生冲突。所以多名开发者在同一个分支中进行作业时,为了减少冲突的情况发生免疫频繁地进行pushpull操作。


推送至远程仓库

Git时分散型的版本管理系统,但之前的文件所述,都是针对单一本地仓库的操作。下面,将开始接触远在网络另一头的远程仓库。远程仓库顾名思义,是与本地仓库相对独立的另一个仓库。我们现在GitHub上创建一个仓库,并将其设置为本地仓库的远程仓库。

为防止与其他仓库混淆,仓库名尽量与本地仓库保持一致(即git-test)。创建时不要勾选Initialize this repository with a README选项。因为一旦勾选该选项,GitHub一侧的仓库就会自动生成README.md文件,从创建之初便与本地仓库失去整合性。虽然到时也可以强制覆盖,但为防止这一情况发生还时建议不要勾选改选项,直接点击Create repository创建仓库。

git remote add —— 添加远程仓库

在GitHub上创建的仓库路径为git@github.com:用户名/仓库名.git或者https://github.com/用户名/仓库名.git。现在用git remote add命令将它设置成本地仓库的远程仓库。

➜  git-test git:(master) git remote add origin git@github.com:VergilLai/git-test.git

按照上述格式执行git remote add命令后,Git会自动将git@github.com:VergilLai/git-test.git远程仓库的名称设置为origin(标识符)。

git push —— 推送至远程仓库

推送至master分支

如果想将当前分支下本地仓库中的内容推送给远程仓库,需要用git push命令。现在假定在master分支下操作。

➜  git-test git:(master) git push -u origin master
Counting objects: 20, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (20/20), 1.53 KiB | 0 bytes/s, done.
Total 20 (delta 3), reused 0 (delta 0)
To https://github.com/VergilLai/git-test.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

像这样执行git push命令,当前分支的内容就会被推送给远程仓库origin的master分支。-u参数可以在推送的同时,将origin仓库的master分支设置为本地仓库当前分支的upstream(上游)。添加了这个参数,将来运行git pull命令从远程仓库获取内容时,本地仓库的这个分支就可以直接从origin的master分支获取内容,省去了另外添加参数的麻烦。

执行该操作后,当前本地仓库master分支的内容就会被推送到GitHub远程仓库中。在GitHub上也可以确认远程master分支的内容和本地master相同。

github

推送至master以外的分支

除了master分支之外,远程仓库也可以创建其他分支。举个例子,在本地仓库创建feature-D分支,并将它以同名的形式push至远程仓库。

➜  git-test git:(master) git checkout -b feature-D
Switched to a new branch 'feature-D'
➜  git-test git:(feature-D) git push -u origin feature-D
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/VergilLai/git-test.git
 * [new branch]      feature-D -> feature-D
Branch feature-D set up to track remote branch feature-D from origin.

现在,在远程仓库的GitHub页面就可以查看到feature-D分支了。

github


更改提交的操作

git reset —— 回溯历史版本

通过前面学习的操作,我们已经学会如何在实现功能后进行提交,累积提交日志作为历史记录,借此不断培育一款软件。

Git的另一特征便是可以灵活操作历史版本。借助分散仓库的优势,可以在不影响其他仓库的前提下对历史版本进行操作。

在这里,为了让各位熟悉对历史版本的操作,我们先回溯历史版本,创建一个名为fix-B的特性分支

回溯历史,创建fix-B分支

回溯到创建feature-A分支前

让我们先回溯到feature-A分支创建之前,创建一个名为fix-B的特性分支。

要让仓库的HEAD、暂存区、当前工作树回溯到指定状态,需要用到git reset --hard。只要提供目标时间点的哈希值,就可以完全恢复至该时间点的状态。

哈希值在每个环境中各不相同,可以使用git log查看。

➜  git-test git:(master) git log

commit c00815d1247d0ef656b3ea31c8cd2079e04bf52b
Merge: 7c1e8df ad5a4ba
Author: Vergil <vergil@vip.163.com>
Date:   Mon Sep 21 00:21:55 2015 +0800

    Merge branch 'feature-A'

commit ad5a4baec2ac4125c489b2461cb203ec14cd2658
Author: Vergil <vergil@vip.163.com>
Date:   Sun Sep 20 23:48:30 2015 +0800

    Add feature-A

commit 7c1e8dfd917605d9e6d7451f77a2e02bb7a2966b
Author: Vergil <vergil@vip.163.com>
Date:   Sun Sep 20 21:20:27 2015 +0800

    Add index

commit 69ca7dddd02bca848adccd5d00a62903081de979
Author: Vergil <vergil@vip.163.com>
Date:   Sun Sep 20 20:34:43 2015 +0800

    First commit

执行以下命令:

➜  git-test git:(master) git reset --hard 7c1e8dfd917605d9e6d7451f77a2e02bb7a2966b
HEAD is now at 7c1e8df Add index

现在已经成功回溯到特定分支(feature-A)创建之前的状态。由于所有文件都回溯到了指定哈希值对应的时间点上。README.md文件的内容也恢复到了当时的状态。

创建fix-B分支

现在来创建特性分支(fix-B)。

➜  git-test git:(master) git checkout -b fix-B
Switched to a new branch 'fix-B'

作为这个主题的作业内容,我们在README.md文件中添加一行文字:

➜  git-test git:(fix-B) echo "fix-B" >> README.md 

然后直接提交README.md文件。

➜  git-test git:(fix-B) ✗ git add README.md 
➜  git-test git:(fix-B) ✗ git commit -m "Fix B"
[fix-B 47aa230] Fix B
 1 file changed, 1 insertion(+)

现在的状态如图所示:

当前fix-B的状态

当前fix-B的状态

接下来的目标是下图所示的状态,即主干分支合并feature-A分支的修改后,又合并了fix-B的修改

fix-B分支的下一步目标

fix-B分支的下一步目标

推送至feature-A分支合并后的状态

首先恢复到feature-A分支合并后的状态。不妨称这一操作为“推进历史”。

git log命令只能查看以当前状态为终点的历史日志。所以这里要使用git reflog命令,查看当前仓库的操作日志。在日志中找出回溯历史之前的哈希值,通过 git reset --hard命令恢复到回溯历史前的状态。

首先执行git reflog命令,查看当前仓库执行过的操作日志。

➜  git-test git:(fix-B)  git reflog

47aa230 HEAD@{0}: commit: Fix B
7c1e8df HEAD@{1}: checkout: moving from master to fix-B
7c1e8df HEAD@{2}: reset: moving to 7c1e8dfd917605d9e6d7451f77a2e02bb7a2966b
c00815d HEAD@{3}: reset: moving to c00815d1247d0ef656b3ea31c8cd2079e04bf52b
7c1e8df HEAD@{4}: reset: moving to 7c1e8dfd917605d9e6d7451f77a2e02bb7a2966b
c00815d HEAD@{5}: merge feature-A: Merge made by the 'recursive' strategy.
7c1e8df HEAD@{6}: checkout: moving from feature-A to master
ad5a4ba HEAD@{7}: checkout: moving from master to feature-A
7c1e8df HEAD@{8}: checkout: moving from feature-A to master
ad5a4ba HEAD@{9}: commit: Add feature-A
7c1e8df HEAD@{10}: checkout: moving from feature-B to feature-A
7c1e8df HEAD@{11}: checkout: moving from feature-A to feature-B
7c1e8df HEAD@{12}: checkout: moving from master to feature-A
7c1e8df HEAD@{13}: commit: Add index
69ca7dd HEAD@{14}: commit (initial): First commit

在日志仲,可以看到commit、checkout、reset、merge等Git命令的执行记录。只要不进行Git的GC(Garbage Collection,垃圾回收),就可以通过日志随意调取近期的历史状态,就像给时间机器指定一个时间点,在过去未来中自由穿梭一般。即便开发者错误指定了Git操作,基本也都可以利用git reflog命令恢复到最原先的状态(如果人生可以像Git一样操作那该多好-_#)。

c00815d HEAD@{5}: merge feature-A: Merge made by the 'recursive' strategy.这一行表示feature-A特性分支合并后的状态,对应哈希值为c00815d(哈希值只输入4位以上就可以执行)。将HEAD、暂存区、工作树恢复到这个时间点的状态。

➜  git-test git:(fix-B) git checkout master 
Switched to branch 'master'
➜  git-test git:(master) git reset --hard c00815d
HEAD is now at c00815d Merge branch 'feature-A'

之前我们使用git reset --hard命令回溯了历史,这里又再次通过它恢复到了回溯前的历史状态。当前的状态如下图所示:

恢复历史后的状态

解决冲突

现在只要合并fix-B分支,就可以得到我们想要的状态。现在来进行合并操作。

➜  git-test git:(master) git merge --no-ff fix-B 
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

这时,系统告诉我们README.md文件发生了冲突(Conflict)。系统在合并README.md文件时,feature-A分支更改的部分与本次想要合并的fix-B分支更改的部分发生了冲突。

不解决冲突就无法完成合并,所以要打开README.md文件,解决这个冲突。

查看冲突部分并将其解决

用编辑器打开README.md文件,就会发现其内容变成了下面这个样子。

# Git基本操作
<<<<<<< HEAD
feature-A
=======
fix-B
>>>>>>> fix-B

=======以上的部分是当前HEAD的内容,以下的部分是要合并的fix-B分支中的内容。我们在编辑器中将起改成想要的样子。

# Git基本操作
feature-A
fix-B

如上所示,本次修正让feature-A与fix-B的内容并存于文件之中。但是在实际的软件开发中,往往需要删除其中之一,所以在处理冲突时,务必要仔细分析冲突部分的内容后再行修改。

提交解决后的结果

冲突解决后,执行git add命令和git commit命令。

➜  git-test git:(master) ✗ git add README.md 
➜  git-test git:(master) ✗ git commit -m "Fix conflict"
[master 0d9652a] Fix conflict

由于本次更改解决了冲突,所以提交信息记录为“Fix conflict”

git commit --amend —— 修改提交信息

要修改上一条提交信息,可以使用git commit --amend命令。

我们将上一条提交信息记录为“Fix conflict”,但它实际是fix-B分支的合并,解决合并时发生的冲突只是过程之一,这样标记实在不妥。于是,我们要修改这条提交信息。

➜  git-test git:(master) git commit --amend

执行上面的命令后,编辑器就会启动

Fix conflict

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Mon Sep 21 02:08:05 2015 +0800
#
# On branch master
# Changes to be committed:
#       modified:   README.md
#

编辑器中显示的内容如上所示,其中包含之前的提交信息。将提交信息的部分修改为Merge branch 'fix-B',然后保存文件,关闭编辑器。

[master 957d25d] Merge branch 'fix-B'

随后会显示上面这条结果。现在执行git log --graph命令,可以看到提交日志中的响应内容也已经被修改。

*   commit 957d25dd33395a5cec6a8a2d9c95c1e0256894b4
|\  Merge: c00815d 47aa230
| | Author: Vergil <vergil@vip.163.com>
| | Date:   Mon Sep 21 02:08:05 2015 +0800
| | 
| |     Merge branch 'fix-B'
| |   
| * commit 47aa2309167faed0c7a7da6b7c8bc162adafb4b6
| | Author: Vergil <vergil@vip.163.com>
| | Date:   Mon Sep 21 01:08:51 2015 +0800
| | 
| |     Fix B
| |     
* |   commit c00815d1247d0ef656b3ea31c8cd2079e04bf52b
|\ \  Merge: 7c1e8df ad5a4ba
| |/  Author: Vergil <vergil@vip.163.com>
|/|   Date:   Mon Sep 21 00:21:55 2015 +0800
| |   
| |       Merge branch 'feature-A'
| |   
| * commit ad5a4baec2ac4125c489b2461cb203ec14cd2658
|/  Author: Vergil <vergil@vip.163.com>
|   Date:   Sun Sep 20 23:48:30 2015 +0800
|   
|       Add feature-A
|  
* commit 7c1e8dfd917605d9e6d7451f77a2e02bb7a2966b
| Author: Vergil <vergil@vip.163.com>
| Date:   Sun Sep 20 21:20:27 2015 +0800
| 
|     Add index
|  
* commit 69ca7dddd02bca848adccd5d00a62903081de979
  Author: Vergil <vergil@vip.163.com>
  Date:   Sun Sep 20 20:34:43 2015 +0800
  
      First commit

git rebase -i —— 压缩历史

在合并特性分支之前,如果发现已提交的内容中有些许拼写错误等,不妨提交一个修改,然后将这个修改包含到前一个提交之中,压缩成一个历史记录。这是个经常会用到的技巧,我们来实际操作体会一下。

创建feature-C分支

首先,新建一个fearure-C特性分支。

➜  git-test git:(master) git checkout -b feature-C
Switched to a new branch 'feature-C'

作为fearure-C的功能实现,在README.md文件中添加一行文字,并且故意留下拼写错误,以便之后修正。

➜  git-test git:(feature-C) echo "faeture-C" >> README.md 

提交这部分内容。这个小小的变更就没必要先执行git add命令再执行git commit命令了,直接使用git commit -am来一次完成这两部操作。

➜  git-test git:(feature-C) ✗ git commit -am "Add feature-C"
[feature-C 3b3482c] Add feature-C
 1 file changed, 1 insertion(+)

修正拼写错误

看在来修正刚才预留的拼写错误。修正后的差别如下所示:

➜  git-test git:(feature-C) ✗ git diff

diff --git a/README.md b/README.md
index 9d181c5..847fb0f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
 # Git基本操作
 feature-A
 fix-B
-faeture-C
+feature-C

然后进行提交。

➜  git-test git:(feature-C) ✗ git commit -am "Fix typo"     
[feature-C e1eb7f3] Fix typo
 1 file changed, 1 insertion(+)

错字漏字等失误称为typo,所以我们将提交信息记为“Fix typo”。

实际上,我们不希望在历史记录上看到这类提交,因为健全的历史记录不需要它们。如果能在最初提交之前就发现并修正这些错误,也就不会出现这类提交了。

更改历史

因此,我们来更改历史。将“Fix repo”修正的内容与之前一次的提交合并,在历史记录中合并为一次完美的提交。为此,要用到git rebase命令。

➜  git-test git:(feature-C) git rebase -i HEAD~2

pick 3b3482c Add feature-C
pick e1eb7f3 Fix typo

# Rebase 957d25d..e1eb7f3 onto 957d25d (       2 TODO item(s))
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

我们将e1eb7f3的Fix typo的历史记录压缩到3b3482c的Add feature-C里。按以下所示,将e1eb7f3左侧的pick部分删除,改写为fixup

pick 3b3482c Add feature-C
fixup e1eb7f3 Fix typo

保存编辑器里的内容,关闭编辑器。

➜  git-test git:(feature-C) git rebase -i HEAD~2
[detached HEAD 26c1654] Add feature-C
 Date: Mon Sep 21 02:28:08 2015 +0800
 1 file changed, 2 insertions(+)
Successfully rebased and updated refs/heads/feature-C.

系统显示rebase成功。也就是以下面两个提交作为对象,将“Fix typo”的内容合并到了上一个提交“Add feature-C”中,改写成一个新的提交。

  • 3b3482c Add feature-C
  • e1eb7f3 Fix typo

现在再查看提交日志时发现Add feature-C的哈希值已经不是3b3482c,这证明提交已经被更改。

➜  git-test git:(feature-C) git log --graph 

* commit 26c16540cf42661c84da8e46b8cd8377783b5e5d
| Author: Vergil <vergil@vip.163.com>
| Date:   Mon Sep 21 02:28:08 2015 +0800
| 
|     Add feature-C
|    
*   commit 957d25dd33395a5cec6a8a2d9c95c1e0256894b4
|\  Merge: c00815d 47aa230
| | Author: Vergil <vergil@vip.163.com>
| | Date:   Mon Sep 21 02:08:05 2015 +0800
| | 
| |     Merge branch 'fix-B'
| |   
| * commit 47aa2309167faed0c7a7da6b7c8bc162adafb4b6
| | Author: Vergil <vergil@vip.163.com>
| | Date:   Mon Sep 21 01:08:51 2015 +0800
| | 
| |     Fix B
| |     
* |   commit c00815d1247d0ef656b3ea31c8cd2079e04bf52b
|\ \  Merge: 7c1e8df ad5a4ba
| |/  Author: Vergil <vergil@vip.163.com>
|/|   Date:   Mon Sep 21 00:21:55 2015 +0800
| |   
| |       Merge branch 'feature-A'
| |   
| * commit ad5a4baec2ac4125c489b2461cb203ec14cd2658
|/  Author: Vergil <vergil@vip.163.com>
|   Date:   Sun Sep 20 23:48:30 2015 +0800
|   
|       Add feature-A
|  
* commit 7c1e8dfd917605d9e6d7451f77a2e02bb7a2966b
| Author: Vergil <vergil@vip.163.com>
| Date:   Sun Sep 20 21:20:27 2015 +0800
| 
|     Add index
|  
* commit 69ca7dddd02bca848adccd5d00a62903081de979
  Author: Vergil <vergil@vip.163.com>
  Date:   Sun Sep 20 20:34:43 2015 +0800
  
      First commit

合并至master分支

feature-C分支的使命告一段落,将它与master合并。

➜  git-test git:(feature-C) git checkout master 
Switched to branch 'master'
➜  git-test git:(master) git merge --no-ff feature-C
Merge made by the 'recursive' strategy.
 README.md | 2 ++
 1 file changed, 2 insertions(+)

master分支整合了feature-C分支。


分支的操作

在进行多个并行作业时,我们会用到分支。在这类并行开发的过程中,往往同时存在多个最新代码状态。如图所示,从master分支创建feature-A分支和fix-B分支后,每个分支中都拥有自己的最新代码。

master是Git默认创建的分支,因此基本上所有开发都是以这个分支为中心进行的。

从master分支创建feature-A和fix-B分支

从master分支创建feature-A和fix-B分支

不同分支中,可以同时进行完全不同的作业。等该分支的作业完成之后在于master合并。比如feature-A分支作业结束后与master合并,如图所示:

feature-A分支作业结束后的状态

feature-A分支作业结束后的状态

通过灵活运用分支,可以让多人同时高效地进行并行开发。

git branch —— 显示分支一览表

git branch命令可以将分支名列表显示,同时可以确认当前所在分支。

➜  git-test git:(master) git branch 
* master

可以看到master分支左侧标有“*”(星号),表示这是当前所在的分支。也就是说,我们正在master分支下进行开发。结果中没有显示其他分支名,表示本地仓库中只存在master一个分支。

git checkout -b —— 创建、切换分支

如果想以当前的master分支为基础创建新的分支,需要用到git checkout -b命令

切换到feature-A分支并进行提交

执行下面的命令,创建名为feature-A的分支:

➜  git-test git:(master) git checkout -b feature-A
Switched to a new branch 'feature-A'

实际上,连续执行下面两条命令也能收到同样的效果:

➜  git-test git:(feature-A) git branch feature-A
➜  git-test git:(feature-A) git checkout feature-A
Switched to branch 'feature-A'

创建feature-A分支,并将当前分支切换为feature-A分支。这时再来查看分支列表,会显示处于feature-A分支下

➜  git-test git:(feature-A) git branch            
* feature-A
  master

feature-A分支左侧标有“*”,表示当前分支为feature-A。在这个状态下像正常开发那样修改代码、执行git add命令并进行提交的话,代码就会提交至feature-A分支。像这样不断对一个分支(例如feature-A)进行提交的操作,我们称为“培育分支”。

下面来实际操作一下,在README.md文件中添加一行:

➜  git-test git:(feature-A) echo "feature-A" >> README.md

这里我添加了feature-A这样一行字母,然后进行提交。

➜  git-test git:(feature-A) ✗ git add README.md 
➜  git-test git:(feature-A) ✗ git commit -m "Add feature-A"
[feature-A ad5a4ba] Add feature-A
 1 file changed, 1 insertion(+)

于是,这一行就添加了feature-A分支中了。

切换到master分支

现在再来看一下master分支有没有受到影响。首先切换至master分支。

➜  git-test git:(feature-A) git checkout master 
Switched to branch 'master'

然后查看README.md文件,会发现README.md文件仍然保持原先的状态,并没有被添加文字。

➜  git-test git:(master) cat README.md 
# Git基本操作

feature-A分支的更改不会影响到master分支,这正是在开发中创建分支的优点。只要创建多个分支,就可以在不互相影响的情况下同时进行多个功能的开发。

切换回上一个分支

现在,我们再切换回feature-A分支

➜  git-test git:(master) git checkout -
Switched to branch 'feature-A'

像上面这样用“-”(连字符)代替分支名,就可以切换至上一个分支。当然,将“-”替换成“feature-A”同样可以切换到feature-A分支。

特性分支

Git和Subversion(SVN)等集中型版本管理系统不同,创建分支时不需要连接中央仓库,所以能够相对轻松地创建分支。因此,当今大部分工作流程中都用到了特性(Topic)分支。

特性分支顾名思义,是集中实现单一特性(主题),除此之外不进行任何作业的分支。在日常开发中,往往会创建数个特性分支,同时在此之外再保留一个随时可以发布软件的稳定分支。稳定分支的角色通常由master分支担当。

之前我们创建了feature-A分支,这一分支主要实现feature-A,除此实现之外不进行任何作业。即便在开发过程中发现了BUG,也需要再创建新的分支,在新分支中进行修正。

基于特定主题的作业在特性分支中进行,主题完成后再与master分支合并。只要保持这样一个开发流程,就能保证master分支可以随时供人查看。这样一来,其他开发者也可以放心大胆地从master分支创建新的特性分支。

主干分支

主干分支时刚才的特性分支的原点,同时也是合并的终点。通常人们会用master分支作为主干分支。主干分支中并没有开发到一半的代码,可以随时供他人查看。

有时需要让这个主干分支总是配置在正式环境中,有时又需要用标签Tag等创建版本信息,同时管理多个版本发布。拥有多个版本发布时,主干分支也有多个。

git merge —— 合并分支

接下来,我们假设feature-A已经实现完毕,想要将它合并到主干分支master中。首先切换到master分支。

➜  git-test git:(feature-A) git checkout master
Switched to branch 'master'

然后合并feature-A分支。为了在历史记录中却名记录下本次分支合并,我们需要创建合并提交。因此,在合并时加上--no-ff参数

➜  git-test git:(master) git merge --no-ff feature-A

随后编辑器会启动,用于录入合并提交的信息。

Merge branch 'feature-A'

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

默认信息中已包含了是从feature-A分支合并过来的相关内容,所以可以不做任何更改。将编辑器中显示的内容保存,关闭编辑器,然后就会看到下面的结果。

Merge made by the 'recursive' strategy.
 README.md | 1 +
 1 file changed, 1 insertion(+)

这样一来,feature-A分支的内容就合并到了master分支中了。

git log --graph —— 以图表形式查看分支

git log --graph命令进行查看的话,能清楚地看到特性分支(feature-A)提交的内容已被合并。除此之外,特性分支的创建以及合并也都清楚明了。

➜  git-test git:(master) git log --graph 

*   commit c00815d1247d0ef656b3ea31c8cd2079e04bf52b
|\  Merge: 7c1e8df ad5a4ba
| | Author: Vergil <vergil@vip.163.com>
| | Date:   Mon Sep 21 00:21:55 2015 +0800
| | 
| |     Merge branch 'feature-A'
| |   
| * commit ad5a4baec2ac4125c489b2461cb203ec14cd2658
|/  Author: Vergil <vergil@vip.163.com>
|   Date:   Sun Sep 20 23:48:30 2015 +0800
|   
|       Add feature-A
|  
* commit 7c1e8dfd917605d9e6d7451f77a2e02bb7a2966b
| Author: Vergil <vergil@vip.163.com>
| Date:   Sun Sep 20 21:20:27 2015 +0800
| 
|     Add index
|  
* commit 69ca7dddd02bca848adccd5d00a62903081de979
  Author: Vergil <vergil@vip.163.com>
  Date:   Sun Sep 20 20:34:43 2015 +0800
  
      First commit

基本操作

git init —— 初始化仓库

要使用Git进行版本管理,必须先初始化仓库。使用git init命令进行初始化。

建立一个目录并初始化仓库:(以下示例shell环境使用oh-my-zsh

➜  ~  mkdir git-test
➜  ~  cd git-test 
➜  git-test  git init
Initialized empty Git repository in /Users/vergil/git-test/.git/
➜  git-test git:(master) ls -al
total 0
drwxr-xr-x   3 vergil  staff   102  9 20 19:08 .
drwxr-xr-x+ 59 vergil  staff  2006  9 20 19:08 ..
drwxr-xr-x  10 vergil  staff   340  9 20 19:08 .git

如果初始化成功,执行了git init命令的目录下就会生成.git目录。这个.git目录里存储着管理当前目录内容所需的仓库数据。
在Git中,我们讲这个目录的内容称为“附属于该仓库的工作树”。文件的编辑等操作在工作树中进行,然后记录到仓库中,以此管理文件的历史快照。如果想将文件文件恢复到原先的状态,可以从仓库中调取之前的快照,在工作树中打开。开发者可以通过这种方式获取以往的文件。

git status —— 查看仓库状态

git status命令用于显示Git仓库的状态。这是一个十分常用的命令。
工作树和仓库在被操作的过程中,状态会不断发生变化。在Git操作过程中时常用git status命令查看当前状态。下面,让我们来实际查看一下当前状态:

➜  git-test git:(master) git status
On branch master

Initial commit

nothing to commit (create/copy files and use "git add" to track)

结果显示了当前正处于master分支下。接着还显示了没有可提交的内容。所谓提交(Commit),是指“记录工作树中所有文件的当前状态”。
尚没有可提交的内容,就是说当前我们建立的这个仓库中还没有记录任何文件的状态。这里,建立一个README.md文件作为管理对象,为第一次提交做前期准备。

➜  git-test git:(master) touch README.md
➜  git-test git:(master) ✗ git status
On branch master

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    README.md

nothing added to commit but untracked files present (use "git add" to track)

可以看到在Untracked files中显示了README.md文件。类似地,只要对Git的工作树或仓库进行操作,git status命令的显示结果就会发生变化。

git add —— 向缓存区中添加文件

如果只是用Git仓库的工作树创建了文件,那么该文件并不会被记录入Git仓库的版本管理对象中。因此,用git status命令查看README.md文件时,它会显示在Untracked files里。

要想让文件成功Git仓库的管理对象,就需要用git add命令将其加入暂存区(Stage或者Index)中。暂存区是提交之前一个临时区域。

➜  git-test git:(master) ✗ git add README.md 
➜  git-test git:(master) ✗ git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   README.md

将README.md文件加入暂存区后,git status命令的显示结果发生了变化。可以看到,README.md文件显示在Changes to be committed中了。

git commit —— 保存仓库的历史记录

git commit命令可以将当前暂存区中的文件实际保存到仓库的历史记录中。通过这些记录,我们就可以在工作树中复原文件。

记述一行提交信息

来实际运行一下git commit命令:

➜  git-test git:(master) ✗ git commit -m "First commit"
[master (root-commit) 69ca7dd] First commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md

-m参数后的“First commit”称为提交信息,是对这个提交的概述。

记述详细提交信息

刚才只是简洁地记述了一行提交信息,如果想要记述得更加详细,请不加-m,直接执行git commit命令。执行后编辑器就会启动,并显示如下结果:

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
#       new file:   README.md
#

在编辑器中记述提交信息的格式如下:

  • 第一行:用一行文字简述提交的内容
  • 第二行:空行
  • 第三行以后:记述更改的原因和详细内容

只要按照上面的格式输入,今后便可以通过确认日志的命令货工具看到这些记录。

在以#(井号)标为注释的Changes to be committed(要提交的更改)栏中,可以查看本次提交中包含的文件。将提交信息按格式记述完毕后,保存并关闭编辑器,以#标为注释的行不必删除。随后,刚才记述的提交信息就会被提交。

中止提交

如果在编辑器启动后想中止提交,将提交信息留空并直接关闭编辑器,随后提交就会被中止。

查看提交后的状态

执行完git commit命令后再来看当前状态:

➜  git-test git:(master) git status
On branch master
nothing to commit, working directory clean

git log —— 查看提交日志

git log命令可以查看以往仓库中提交的日志。包括可以查看什么人在什么时候进行了提交或合并,以及操作前后有怎样的差别。

先来看看刚才的git commit命令是否被记录了。

➜  git-test git:(master) git log

commit 69ca7dddd02bca848adccd5d00a62903081de979
Author: Vergil <vergil@vip.163.com>
Date:   Sun Sep 20 20:34:43 2015 +0800

First commit

如上所示,显示了刚刚的提交操作。commit栏旁边显示的“69ca7dddd...”是指向这个提交的哈希值。Git的其他命令中,在指向提交时会用到这个哈希值,

Author栏中显示的是给Git设置的用户命和邮箱地址。Date栏中显示提交执行的日期和时间。再往下就是该提交的提交概述。

只显示提交信息的第一行

如果指向让程序显示第一行简述信息,可以在git log命令后加上--pretty=short。这样一来开发人员就能够轻松地把握多个提交。

➜  git-test git:(master) git log --pertty=short

只显示指定目录、文件的日志

只要在git log命令后面加上目录(文件)命,便会只显示该目录下的日志。如果加的是文件名,就会只显示与该文件相关的日志。

➜  git-test git:(master) git log README.md

显示文件的改动

如果想要查看提交所带来的改动,可以加上-p参数,文件的前后差别就会显示在提交信息之后。

➜  git-test git:(master) git log -p

commit 69ca7dddd02bca848adccd5d00a62903081de979
Author: Vergil <vergil@vip.163.com>
Date:   Sun Sep 20 20:34:43 2015 +0800

    First commit

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29

比如,执行下面的命令,就可以只看README.md文件的提交日志以及提交前后的区别。

➜  git-test git:(master) git log -p README.md 

如上所述,git log命令可以利用多种参数帮助开发者把握以往提交的内容。不必勉强自己一次记下全部参数,每当有想查看的日志就积极去查,慢慢就能得心应手了。

git diff —— 查看更改前后的差别

git diff命令可以查看工作树、暂存区、最新提交之间的差别。

单从字面上可能很难理解,大家不妨亲手试一试。

在刚刚提交的README.md中写点东西:

➜  git-test git:(master) echo "# Git基本操作" >> README.md 

这里用Markdown语法谢了一行题目。

查看工作树和暂存区的差别

执行git diff命令,查看当前工作树与暂存区的差别。

➜  git-test git:(master) ✗ git diff


diff --git a/README.md b/README.md
index e69de29..3d71f6a 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1 @@
+# Git基本操作

由于还没用git add命令向暂存区添加任何东西,所以程序只会显示工作树与最新提交状态之间的差别。

这里解析一下显示的内容。“+”号标出的是添加的行,被删除的行则用“-”号标出。可以看到,这次只添加了一行。

git add命令将README.md文件加入暂存区。

➜  git-test git:(master) ✗ git add README.md 

查看工作树和最新提交的差别

如果现在执行git diff命令,由于工作树和暂存区的状态并无差别,结果什么都不会显示。

要查看与最新提交的差别,请执行git diff HEAD

➜  git-test git:(master) ✗ git diff HEAD 

diff --git a/README.md b/README.md
index e69de29..3d71f6a 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1 @@
+# Git基本操作

不妨养成这样一个好习惯:在执行git commit命令之前先执行git diff HEAD命令,查看本次提交与上次提交有什么差别,等确认完毕再进行提交。这里的HEAD是指向当前分支中最新一次提交的指针。

由于刚刚确认过两个提交之间的差别,所以直接运行git commit命令。

➜  git-test git:(master) ✗ git commit -m "Add index"
[master 7c1e8df] Add index
 1 file changed, 1 insertion(+)

保险起见,查看以下提交日志,确认提交是否成功。

➜  git-test git:(master) git log


commit 7c1e8dfd917605d9e6d7451f77a2e02bb7a2966b
Author: Vergil <vergil@vip.163.com>
Date:   Sun Sep 20 21:20:27 2015 +0800

    Add index

commit 69ca7dddd02bca848adccd5d00a62903081de979
Author: Vergil <vergil@vip.163.com>
Date:   Sun Sep 20 20:34:43 2015 +0800

    First commit

成功查到了第二个提交。


今天在使用phpmailer smtp发送QQ邮箱的邮件时遇到些问题,在此记录一下:

QQ的发送邮件服务器:smtp.qq.com,使用SSL,端口号465或587
而且需要使用独立密码
代码如下:

$mailer = PHPMailer();
$mailer->IsSMTP();
$mailer->SMTPAuth = true;
$mailer->SMTPSecure = 'ssl';
$mailer->Password = 'QQ邮箱独立密码';
//...其他配置省略

Powered by Typecho.