libgit2使用教程(四) git status

status 这个操作是干啥的就不细解释了,这个不知道的话是不太适合看这套教程的,先去补补 git 相关的知识。这集的代码写起来还比较简单,在开动之前先复习一下基础概念。
HEAD、index、workdir:
HEAD 是当前所在的分支的最后一次 commit ,重点是已经 commit 了的状态。
index 是 git_index_add 之后的状态。但是要注意一下,这指的并不一定是使用命令行的 git add 命令完成。仅仅是加到 index 中就算了,并不需要完成 git_index_write 把内存中的状态写入磁盘。
workdir 还没加到 index 之前的都算在 workdir 的状态。

libgit 实现 status 大体上有两个方案,一个是简单一些,可以得到的信息也相对比较少的;另一个可以获得更详细的信息。我们从简单的开始

1. 简单方案

通过 callback 获取状态信息,
path 文件的相对路径
status_flags 是状态的枚举值
payload 是一个透传参数,从 git_status_foreach 第三个参数原样传入

关于状态的枚举值,这个枚举的声明是这样的:

IGNORED 是被忽略的文件,表示这个文件满足 .gitignore 中的规则。
CONFLICTED 是合并之后有冲突的文件。
GIT_STATUS_INDEX 开头的是 index 相对于 HEAD 的状态。
GIT_STATUS_WT 开头的是 workdir 相对于 index 的状态。

2. 详细方案

1) 初始化选项
2) 获取 git_status_list
3) 遍历 list 取出 git_status_entry

git_status_entry 除了有文件状态的标示,还带有 diff 的详细信息,而且严格区分了 HEAD 相对 index ,和 index 相对 workdir。
关于 diff 有很多要详细解释的东西,就放到下回了,我得花点时间读注释去了……

libcurl 撸记

废话不讲,直奔主题

1. 基本流程
首先在使用这个库的任何操作前,最先进行的一个操作就是全局初始化。当然,相对的,在结束后也需要全局的销毁。这两个函数在开始的时候最好就直接先拍上,肯定不会浪费的。

然而,这似乎并不是必须的,从 libcurl 提供的 examples 来看,并不是所有情况都有用这两个函数。具体情况就不太深究了,写上也没什么损失。根据官方资料,每个程序(application)都要执行一次。

2. 同步请求
同步请求就是使用 easy 系列函数,基本的操作流程就是:
初始化 → 设置属性 → 执行 → 获得返回数据 → 销毁
这里每一步都是阻塞式的,perform 这步实际发出 http 请求,收到回复前都会阻塞住。

3. 异步请求

异步请求就要复杂了,需要同时使用 easy 系列函数和 multi 系列函数。
multi 负责管理所有的添加进来的 easy 指针,然而它主要做的工作是异步的把所有加进来的 easy 都 perform 掉。最终取数据的时候,它会返回 easy 的指针,所以取数据还是要使用同步方式的操作。并且,需要自己手动调用 select 来执行异步操作的。
最终可以实现所有操作不会把线程阻塞住。
因为异步,所以流程并不固定,大概可以这样描述:
固定流程:初始化 → 循环[ 开始请求 → 取 fd_set → 执行 select → 取消息 ] → 销毁
添加请求:生成一个 easy 请求句柄 → 将指针添加给 multi
获取返回:通过 multi 取出的消息拿到 easy 指针 → 通过这个 easy 指针读取返回数据
这样生成请求和读取返回信息可以是完全异步完成的。

4. 返回数据接收
通过 curl_easy_getinfo 只能获取到返回的 response code 不能获得返回的数据。如果需要通过 http 请求获得数据返回或者下载文件的话,需要在 CURL 指针的设置中,挂一个接收数据的函数。

这个函数的声明是这样的:

buffer 数据指针
size 指针单个偏移的大小
nitems 指针能偏移多少个
outstream 由用户传入的指针 CURLOPT_WRITEDATA
返回值:本次接收的数据大小

然后我们在这个函数中来处理返回来的数据。
这里需要注意的是这个函数不一定是一次性返回全部的数据,所以要妥善处理才行,直到 curl_easy_perform 返回或者 curl_multi_info_read 拿到这个句柄才能认为返回数据已经写入完毕,否则接收到的数据都可能是不完整的。
下面是我下载部分的处理:

不过如果不需要特殊处理,可以不需要 CURLOPT_WRITEFUNCTION 传入一个函数,只需要 CURLOPT_WRITEDATA 传入一个 FILE* ,下载的数据会自动写入文件。(至少我使用的版本是这样的)

5. POST 和 GET
libcurl 默认的模式是 GET ,传参数只需要写在 url 后边就可以了,只要不做特殊的设置,就是以 GET 形式向服务器发请求。
那么如果我们要发 POST 呢?
首先,我们要显示的设置我们这个请求是一个 POST 请求:

POST 的参数不是接在 url 后边的,而是要单独传递:

这样设置好,perform 后就向服务器发送 POST 请求了。

6. 分块下载
使用 http 协议下载文件,数据是绝对顺序传输的,如果客户端或者服务器端在传输过程中发生了一些问题,就会导致 TCP 层重试,导致下载效率降低。所以如果只是直接单纯的顺序传输下载,效果是比较差的。因此同时开多个连接,同时下载文件的不同部分,会对网络的利用率更高一些。
下载文件的一个块,在 http1.1 中引入了 Range 参数。这个参数就指定了这个连接负责下载文件的哪个部分。

写文件的部分只需要每个连接各自拿一个文件句柄 seek 到起始位置就可以了。

然而如何让所有连接同时工作起来呢?
有两个选择:1) 每个连接各自使用 curl_easy_perform 发起请求;2) 加入到 multi 中异步请求。
1) 因为是会阻塞,所以单线程是无法完成一起工作的,所以如果选择这个种简单的方式,就需要手动开多个线程来完成。这里需要注意的是每个线程使用自己的 CURL* 基本上也不会出什么乱子。
2) 这种方式会比较省心一些,不用处理复杂的线程同步问题。只是 multi 异步架构起来会稍微复杂。
我个人倾向于选择方案2,因为没有太大必要折腾多线程来处理下载的问题。线程开的再多也并不会使网线带宽变大,所以也没必要花心思处理多线程数据同步问题。
方案2,只要处理好最大连接数,以及下载结束后的收尾,在性能和稳定性方面还是更靠谱一些的。

7. 断点续传
要实现断点续传首先我们应该确定断点,必须知道下载的文件断在了什么地方才能知道从哪里开始继续下载。
这里可以分两种情况来考虑
1) 不分块顺序下载,那么文件已经下载好的部分就是断点。那么下一次只需要取出已下载文件的大小,从这里开始继续下就可以了。这可以不必使用 Range 参数,可以使用 RESUME_FROM 设置起始点。不过对于大文件,不适用分块的话效率会比较低。
2) 分块下载,对于分块下载到同一个文件的情况,读取文件已完成部分大小的方式就显然不可行了。这里就需要借助另外一个文件来记录文件每块的下载进度了。在写被下载文件的同时再写一个记录进度的 log 文件,如果正常完成了下载,就可以将 log 文件删除了,如果没有完成,下一次可以先读取 log 文件,通过 log 文件中的记录继续剩余分块的下载。
那么再写一个 log 文件是不是会有性能问题呢?答案是肯定会多消耗一些磁盘的写带宽,但是对于下载这件事的影响有多大呢?理论上不会有什么影响,毕竟磁盘的写入带宽要远远大于网络传输带宽,所以对实际效果的影响应该是不大的。
据我观察迅雷等下载软件也是会给每个下载的文件多写一个 log 文件的。当然我并不确定它是不是用这个 log 文件作为断点记录。

写在最后
这篇贴出来的代码都是小片段,因为自己撸出来的代码确实挺丑,就不拿出来丢人了。贴出来除了多占篇幅,也起不到太大作用。
我封装了一个 http client 放到了 github 有需要的同学自取。实现了 http post 、http get 以及分块下载和断点续传。
点击前往:ftxHttpClient

libgit2使用教程(三) git commit

首先去看 API 中关于 commit 的部分都有哪个函数比较像,但是意外的发现了 API 中并没有官方 101-samples 中的 git_commit_create 函数,搞得我好意外。不过据我所知,实现 commit 操作肯定是要使用这个 git_commit_create 函数了。那咱们先去头文件看看它需要什么参数:

·id 返回一个 OID 的指针,这个 OID 表示的就是新创建的这个 commit
·repo 仓库指针
·update_ref 需要提交的目标引用,使用当前分支的引用就用 “HEAD” 就可以了
·author 作者 git_signature 指针。git_signature 记录作者的名字、email、和一个时间
·committer 提交人也是 git_signature 指针,跟上边可以是同一个
·message_encoding 这次提交说明信息的字符集
·message 这次提交的完整说明
·tree 我们需要传入一个 git_tree 实例的指针,这个 tree 对象必须属于这个仓库。这个 ·tree 将是我们下边要解决的第一个问题
·parent_count 这个 commit 的父级的数量,如果是 merge 后的 commit 会有两个甚至以上,咱们暂时先不考虑分支的事情,所以只玩儿1个的
·parents 是一个 git_commit 指针的数组,表示上一级的提交,如果是第一次 commit 数组应该是空的,同时 parent_count 应该是0。如果不是,咱们就得先把爸爸找出来,这将是下边要解决的第二个问题

1. tree:
这个 tree 实际上就是把 index 的 entry 写到一个 tree 中,最终把这个 tree 提交。从 API 文档可以找到 git_index_write_tree 这个函数。它的作用就是写一个 tree,所以就可以写出下面的代码:

2. parent:
因为我们还先不考虑 merge 的情况,所以父级 commit 只有一个,这个父级实际上就是 head 引用,所以我们第一步先拿出 head。

这里我们要考虑一个情况:在一个全新的仓库的情况下,是没有之前的 commit 的,因此,取出来的 ref_head 会是空的。所以要做一个判断:

这样我们就拿到了我们需要的 tree 和 parent commit ,下面就是签名,然后提交了:

这样 commit 操作就完成了。

示例代码是 sample3
同样放在 github 的 libgit2_samples 中

libgit2使用教程(特别篇)几个基本概念说明

本来打算这篇写 commit 但是感觉涉及的一些概念不细说一下可能不太好,因此花了一下午时间整理了这些基础概念。对这些概念理解之后会比较有利于理解 git 仓库的结构,对于理解 git 系统的工作原理也会有很大的帮助。所以,这篇没有任何功能实现,纯描述概念,可能有些无聊,对于对下面这些完全没有概念同学,还得代码敲起来,自己动手探索才能理解到其中的精髓。

  • blob
  • oid
  • tree
  • commit
  • reference
  • branch
  • index

1. blob:

blob 对象直接包含 git_object ,但是 git_object 这个概念是封装起来的,我们一般情况下是接触不到的。它将直接对应存放在仓库中的数据文件,对于 blob 我们就把它直接理解成我们文件夹中的文件就可以了。它是整个 git 仓库管理的基础单位,作为实际文件的代表,可以说 git 的版本管理就是花式玩儿 blob 。

2. oid:
指的是 git_object 的 id。每个独立的 git_object 都有一个 id,id 相等则可以判定是同一个对象。
它存储的是一个 SHA-1 值,20个字节大小,每个字节存放一个16进制数。如果转成字符串,则是一个40个字符长的字符串,两个字符表示一个16进制数。
相互转换的函数:
git_oid_fromstr 把 SHA-1 转成 oid
git_oid_tostr 把 oid 转成 SHA-1

3. tree 和 tree entry:
tree 就如它字面上的意思,是一个树形数据结构,tree entry 就是这个树的节点。

可以用上边的方法简单遍历 tree 的一层,这些 entry 可能是没有子节点的“文件”,也有可能是还有子节点的“文件夹”,也就是说,tree entry 还可以作为 tree 持有自己的 tree entry。

通过 git_filemode_t 我们可以看出,entry 就可以表示一个 blob。

这里有一个要注意的地方,这个枚举的值是以8进制表示的,直接以10进制数方式打印,跟这个字面值是不一样的。

4. commit:
commit 是版本的基本单位,版本库的记录都是以 commit 为基础的,谁提交的、什么时候提交的、提交的说明信息、提交时的 tree 的状态都是由 commit 管理的。它还要知道它的父级 commit 是哪个或者哪几个,最终 commit 们会组成一个有向图。
我们要找某一个版本,就是去找这个版本的 commit ,有了这个 commit 我们就可以通过它调出那个版本当时的 tree,然后由 tree 管理的所有文件就都可以找到了。可以说 commit 是 git 版本管理的核心,其他所有的概念都是围绕着 commit 展开的。

5. reference:
这个引用怎么用语言来描述我一直很纠结。reference 引用的是一条 commit 链——更准确的说不应该是链,因为 commit 网上追溯有可能有多个父 commit 的情况。它实际上是一个倒过来的树,reference 是它的根节点,commit 的 parent 就是子节点。这是为了方便向上追溯版本。
实际上每多一个分支,就相当于多了一个 reference ,reference 就包含着这个分支的最新的一个 commit。
git 系统也预置了几个重要的 reference 名称,用于方便索引。其中最重要的就是 HEAD 了,在工程的 .git 文件夹就可以看到这个文件,打开看看,它只有一个指向一个 refs 文件夹的路径,实际上它指向的就是当前所在的分支。所以,通过 HEAD 就可以非常方便的找到当前的 reference。

6. branch:
在上边 reference 的部分我多次提到了“分支”这个词,然而了解一些 git 命令行工具的同学肯定知道,git 工具分支的命令是 branch ,而且日常沟通也都是以 branch 的概念来代表分支的。
这没有错,git 概念中的分支就是 branch ,同时 libgit2 这个库中也有 branch 这个概念,它所指的实际上就是 reference 。就是给 reference 起了个名字,这个名字是方便记忆的,而不是 reference 用的一个路径。

从这个函数声明就可以看出,通过一个分支名返回是一个 reference ,同时,在函数实现的内部,也是通过 git_reference_lookup 实现的。因此可以看出,branch 实际上就是一个有名字的 reference 。

如果此时 HEAD 就是 master 分支的话,roid 和 boid 的值是相等的。

7. index 和 index entry:
index 索引的是当前工作区中未提交的内容。完整的 commit 操作就是将 index 中的内容写到一个 tree ,然后用这个新的 tree 创建一个新的 commit ,然后更新 reference 。而在这之前的 add 操作,就是将改动更新到当前 index。
index entry 就像 tree entry 也是作为文件的代表,而不一样的地方就是,单独的文件夹不再是 tree 了。可以通过它的 mode 属性,看出每一个文件是一个 blob ,submodule 是另外一个 blob。
实际上在没有改动的情况下 index entry 和 tree entry 是相同的 blob 。只有有新的 add 才会使同一个文件对应的 blob 不同。

初撸WordPress小记

撸这个 WordPress 站有一段时间了,也解决了一些比较蛋疼的问题,大晚上的一股鸡血上脑,就撸了一发小文,记录一下这段时间解决的几个比较有用的问题。

1. 中文标签和分类的问题
偶然发现直接点击中文标签或分类链接,wordpress 是根本找不到相关内容的。相当于访问 http://ftxtool.org/index.php/category/技术随笔/ 的时候服务器给我返回的是 404 ,然而,使用 http://ftxtool.org?category=技术随笔 传参数的方式却可以正常访问到,估计是 url 地址重定向时候的问题。
所以,网上能找到一类解决方案就是修改 rewrite.php 的代码。然而,就像某个复制粘贴的博主说的那样,根本没有找到别人说的那块代码在哪。应该是经过很多版本的更新,那部分代码早就不一样了,然而这种复制粘贴的精神依然屹立,永不过时。
另外一个解决方案,是修改 class-wp.php ,把 $_SERVER[‘PATH_INFO’] 和 $_SERVER[‘REQUEST_URI’] 转码,把 GBK 转成 UTF-8 。然而,经过测试好像并没有什么卵用。
方案三,修改别名。把别名改成一小串英文,亲测有效。具体操作方法就是:
仪表盘→分类目录/标签→选一个具体的分类或标签名字,点下边的快速编辑→别名(随便写个英文的名字),然后更新就好了。
方案四,插件。方案三虽然可以解决问题,但是每个标签都要手动改一遍还是有些蛋疼的。找来找去找到个插件,用起来还不错。插件名叫:IIS Chinese Tag Permalink 直接去安装插件里搜索这个名字就能找到。目前用起来还没啥问题,应该算比较靠谱的一个方案。然而还是有个别标签一直 404 ,只能通过改别名的办法解决,不知道有没有更好一些的插件可以解决这个问题。

2. sitemap
赵家百度的 sitemap 插件简直渣到爆,跟 google 的 XML-Sitemap 插件简直没法比。从设置页面就已经被秒成渣渣了。最蛋疼的是生成 sitemap 文件,我去点了按钮也没反应,一直是:没有生成 sitemap 文件。麻痹的没有任何提示了,只好去代码里下 log。
原来生成文件是在 wordpress 的跟目录,代码注释里竟然叫我把目录整个权限设成777。简直日了狗,你家运维都是这么干活的么?
然后,还是手动在根目录创建了个 sitemap 文件,然后加个写权限,就可以用了。
然而,还是 google 家的 sitemap 好使。自己自动生成好之后还能自动通知 google 和 bing 。

3. 友情链接
新版本添加友情链接还是蛮蛋疼的,网上能搜到的基本上都是老版本的方案,或者 link manager 插件,但是插件并不好使。真心想不明白,这帮人复制来复制去也没有经过验证,然后还被搜索引擎排的那么靠前。
新版本添加链接其实很简单,只要在侧边栏加上一个自定义菜单就可以了:
外观→自定义→小工具→侧边栏
点击【添加小工具】选择【自定义菜单】
然后选上自己添加好链接的菜单就可以了。

然而怎么添加一个菜单呢?
外观→菜单
点击【创建新菜单】
然后 填一个名字
然后 在【自定义链接】中添加
能看懂提示文字基本上不会错。

注意:这个菜单不要勾选 主菜单 。

libgit2使用教程(二) git add

主要内容:
1. git add <path>
2. git add .

示例代码:sample2

一、 open
上一篇初始化了一个 git 仓库,接下来开始使用我们本地的 git 仓库。几乎所有的操作都需要使用一个 git_repository 指针,所以第一步,我们先初始化这个指针。
如果是新创建一个仓库的话,使用上一篇的 git_repository_init 就可以了。如果是一个已有的仓库呢?
就用 git_repository_open 这个函数。这个函数很简单,两个参数,参数一是返回 git_repository 指针,参数二是仓库地址。

二、 add
add 就开始有些复杂了,我们要分情景讨论这个事。
1. add 指定文件
通过官方API我们找到了一个函数 git_index_add_bypath ,主要意思就是:通过指定 path 添加一个文件到 index [附一]
只有两个参数,参数一是 git_index 指针,参数二是指定文件的路径。
所以,我们要先拿到 git_index 的指针:

拿到了 index 指针,就可以 add 了:

然后运行一下,没有报错,然后到我们的仓库调用 git status 看一下。

On branch masterInitial commit

Untracked files:
(use “git add …” to include in what will be committed)

file

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

file 并没有被 add 进来,是什么原因呢?

其实,git_index_add_bypath 操作并没有失败,只不过它是对内存的操作,在程序结束的时候,并没有把内存中的 index 写到磁盘,如果要把 index 写到磁盘,需要调用一个函数:

好了,目前完整的 add 的代码就是:

2. add 全部
使用函数 git_index_add_all 可以把没有添加到 index 的改动全部添加到 index 中。写一个简单的测试代码:

有时候对于参数不知道到底要传什么,就可以先传个 null 或者 0,运行看程序给你报什么错,再根据具体的错误找解决方案。不过显然,这段代码的运行结果是符合我们的预期的,我们对这个仓库中所有文件的修改都被 add 到 index 中了。说明参数二 paths 传空,就相当于命令:
git add .
那么,后边两个参数的作用是什么的?
倒数第二个参数是一个函数指针 git_index_matched_path_cb ,而最后一个参数则是传递给这个回调函数的参数。
所以我们给 index 添加改动的时候,是可以通过这个回调函数获得一些信息的:

这个回调的参数一是有改动的文件路径,参数二是 git_index_add_all 的第二个参数中和这个文件路径匹配的 pathspec ,第三个参数是前边提到过的自定义参数。
最重要的是它的返回值:
0:正常添加
正数:跳过添加这个文件的改动
负数:直接报错返回,这个时候 git_index_add_all 将直接返回,返回值就是我们这个回调函数的返回值。

所以这个回调函数可以让我们根据自己的情况对批量添加做一个过滤。
然后,翻回头再说一下 git_index_add_all 的第二个参数[附二]。他是一个路径规则,不满足这个规则的文件将直接被跳过。也不会触发上边那个回调函数。满足规则的文件和对应的那一项规则,将成为回调函数的参数一和参数二。
所以调用的代码是这个样子的:

 

附一、解释一下 index
index 可以理解为一个存储区,存放被 add 进来的“改动”,add 的文件的改动被添加到 index ,commit 操作把这个 index 添加到本地仓库,可以通过 git rm —cached 从 index 中删除指定“文件”。只要不 commit 我们玩儿的就一直是同一个 index 。

附二、git_strarray
git_strarray 是一个结构体:

strings 是一个字符串数组,count 是这个数组有多少个元素。
这个结构后边会经常用到,与多个路径相关的参数和返回值,都会使用这个结构。