libgit2使用教程(三) git commit

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

/**
* Create new commit in the repository from a list of `git_object` pointers
*
* The message will **not** be cleaned up automatically. You can do that
* with the `git_message_prettify()` function.
*
* @param id Pointer in which to store the OID of the newly created commit
*
* @param repo Repository where to store the commit
*
* @param update_ref If not NULL, name of the reference that
* will be updated to point to this commit. If the reference
* is not direct, it will be resolved to a direct reference.
* Use "HEAD" to update the HEAD of the current branch and
* make it point to this commit. If the reference doesn't
* exist yet, it will be created. If it does exist, the first
* parent must be the tip of this branch.
*
* @param author Signature with author and author time of commit
*
* @param committer Signature with committer and * commit time of commit
*
* @param message_encoding The encoding for the message in the
* commit, represented with a standard encoding name.
* E.g. "UTF-8". If NULL, no encoding header is written and
* UTF-8 is assumed.
*
* @param message Full message for this commit
*
* @param tree An instance of a `git_tree` object that will
* be used as the tree for the commit. This tree object must
* also be owned by the given `repo`.
*
* @param parent_count Number of parents for this commit
*
* @param parents Array of `parent_count` pointers to `git_commit`
* objects that will be used as the parents for this commit. This
* array may be NULL if `parent_count` is 0 (root commit). All the
* given commits must be owned by the `repo`.
*
* @return 0 or an error code
* The created commit will be written to the Object Database and
* the given reference will be updated to point to it
*/
GIT_EXTERN(int) git_commit_create(
    git_oid *id,
    git_repository *repo,
    const char *update_ref,
    const git_signature *author,
    const git_signature *committer,
    const char *message_encoding,
    const char *message,
    const git_tree *tree,
    size_t parent_count,
    const git_commit *parents[]);

·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,所以就可以写出下面的代码:

git_oid new_tree_id;
git_tree *new_tree = nullptr;

// write index to tree
error = git_index_write_tree(&new_tree_id, index);
if (error < 0)
{
    const git_error *e = giterr_last();
    std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
    goto SHUTDOWN;
}
else
{
    git_index_free(index);
}
git_tree_lookup(&new_tree, rep, &new_tree_id);

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

git_reference* ref_head = nullptr; 
git_repository_head(&ref_head, rep);

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

size_t parent_count = 0;
git_commit *parent_commit = nullptr;
const git_commit *parents[] = {nullptr};

// has parent
if (error == GIT_OK)
{
    // get parent commit
    git_commit_lookup(&parent_commit, rep, git_reference_target(ref_head));
    parents[0] = parent_commit;
    parent_count = 1;
    git_reference_free(ref_head);
}
else if (error != GIT_EUNBORNBRANCH)
{
    const git_error *e = giterr_last();
    std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
    goto SHUTDOWN;
}

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

git_signature* me = nullptr; 
git_signature_now(&me, "XiaochenFTX", "xiaochenftx@gmail.com”);
git_oid new_commit;
error = git_commit_create(&new_commit,
                                    rep,
                                    "HEAD",
                                    me,
                                    me,
                                    "UTF-8",
                                    "commit message",
                                    new_tree,
                                    parent_count,
                                    parents);

这样 commit 操作就完成了。

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

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

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

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

1. blob:

struct git_blob {
    git_object object;
    git_odb_object *odb_object;
};

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 就是这个树的节点。

size_t tree_entry_count = git_tree_entrycount(tree);
std::cout << "tree entry count: " << tree_entry_count << std::endl;
for (size_t i = 0; i < tree_entry_count; ++i)
{
    const git_tree_entry *te = git_tree_entry_byindex(tree, i);
    const char *te_name = git_tree_entry_name(te);
    const git_oid *te_oid = git_tree_entry_id(te);
    const char *teid = git_oid_tostr_s(te_oid);
    git_otype otype = git_tree_entry_type(te);
    git_filemode_t filemode = git_tree_entry_filemode(te);
    std::cout << "tree entry file name: " << te_name << " \toid: " << teid << " \totype: " << otype <<
            " \tfilemode: " << filemode << std::endl;
}

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

if (otype == GIT_OBJ_TREE)
{
    git_tree *leaf_tree = nullptr;
    git_tree_lookup(&leaf_tree, rep, te_oid);
    size_t leaf_entry_count = git_tree_entrycount(leaf_tree);
    for (size_t j = 0; j < leaf_entry_count; ++j)
    {
        const git_tree_entry *leaf_te = git_tree_entry_byindex(tree, j);
        const char *leaf_te_name = git_tree_entry_name(leaf_te);
        const git_oid *leaf_te_oid = git_tree_entry_id(leaf_te);
        const char *leaf_teid = git_oid_tostr_s(leaf_te_oid);
        git_otype leaf_otype = git_tree_entry_type(leaf_te);
        git_filemode_t leaf_filemode = git_tree_entry_filemode(leaf_te);
        std::cout << "\tleaf tree entry file name: " << leaf_te_name << " \toid: " << leaf_teid << " \totype: " <<
                leaf_otype << " \tfilemode: " << leaf_filemode << std::endl;
    }
}

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

/** Valid modes for index and tree entries. */
typedef enum {
    GIT_FILEMODE_UNREADABLE = 0000000,
    GIT_FILEMODE_TREE = 0040000,
    GIT_FILEMODE_BLOB = 0100644,
    GIT_FILEMODE_BLOB_EXECUTABLE = 0100755,
    GIT_FILEMODE_LINK = 0120000,
    GIT_FILEMODE_COMMIT = 0160000,
} git_filemode_t;

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

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

// 通过 commit 获得 tree 
git_tree* tree = nullptr; 
git_commit_tree(&tree, commit);

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

git_reference *head_ref = nullptr;
// 当前 head 引用
git_repository_head(&head_ref, rep);

const git_oid *oid_ref = git_reference_target(head_ref);
const char *roid = git_oid_tostr_s(oid_ref);
    
git_commit *commit = nullptr;
// 这个分支中最新一个 commit
git_commit_lookup(&commit, rep, oid_ref);

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

GIT_EXTERN(int) git_branch_lookup( git_reference **out, git_repository *repo, const char *branch_name, git_branch_t branch_type);

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

git_reference *branch_ref = nullptr;
// 通过分支名取到 reference
git_branch_lookup(&branch_ref, rep, "master", GIT_BRANCH_LOCAL);
const git_oid *oid_branch = git_reference_target(branch_ref);
const char *boid = git_oid_tostr_s(oid_branch);

如果此时 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 不同。

git_index *index = nullptr;
git_repository_index(&index, rep);
const git_oid *indexoid = git_index_checksum(index);
const char *ioid = git_oid_tostr_s(indexoid);
std::cout << "index oid: " << ioid << std::endl;
size_t count = git_index_entrycount(index);
std::cout << "entry count: " << count << std::endl;
for (size_t i = 0; i < count; ++i)
{
    const git_index_entry *entry = git_index_get_byindex(index, i);
    const char *entrypath = entry->path;
    git_oid oid = entry->id;
    const char *id = git_oid_tostr_s(&oid);
    std::cout << "entry: " << entrypath << " \toid: " << id << " \tmode: " << entry->mode << std::endl;
}

libgit2使用教程(二) git add

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

示例代码:sample2

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

const char *path = "/Users/xiaochen/Documents/data";
git_repository *rep = nullptr;
int error = 0;
error = git_repository_open(&rep, path);

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

git_index* index = nullptr; 
git_repository_index(&index, rep);

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

git_index_add_bypath(index, "file");

然后运行一下,没有报错,然后到我们的仓库调用 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 写到磁盘,需要调用一个函数:

/* Write the in-memory index to disk */ 
git_index_write(index);

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

git_index* index = nullptr; 

// get index 
git_repository_index(&index, rep); 

// git add file 
error = git_index_add_bypath(index, "file"); 
if (error < 0) 
{ 
    const git_error *e = giterr_last(); 
    std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
} 
else 
{ 
    /* Write the in-memory index to disk */ 
    git_index_write(index); 
} 

// 
git_index_free(index);

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

git_index_add_all(index, nullptr, GIT_INDEX_ADD_DEFAULT, nullptr, nullptr); 
git_index_write(index);

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

int index_matched_path_cb(const char *path, const char *matched_pathspec, void *payload)
{
    std::cout << "path: " << path << "\n";
    std::cout << "matched_pathspec: " << matched_pathspec << std::endl;
    return 0;
}

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

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

char *strs[1];
git_strarray paths = {nullptr, 0};
    
strs[0] = "dir/*";
paths.strings = strs;
paths.count = 1;
    
error = git_index_add_all(index, &paths, GIT_INDEX_ADD_DEFAULT, index_matched_path_cb, nullptr);
if (error < 0)
{
    const git_error *e = giterr_last();
    std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
}
else
{
    /* Write the in-memory index to disk */
    git_index_write(index);
}

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

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

/** Array of strings */
typedef struct git_strarray {
    char **strings;
    size_t count;
} git_strarray;

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

libgit2使用教程(一)实现 git init

主要内容:
1. 使用 libgit2 的准备工作
2. 构建和运行
3. 初始化一个 git 仓库

示例代码:https://github.com/XiaochenFTX/libgit2_samples

进入正题

开始直接到 libgit2 的 readme 我们首先需要从那里获取一些有用的信息。
官网:https://libgit2.github.com/
API:http://libgit2.github.com/libgit2/
虽然官方文档写的挺水的,不过毕竟可以获得信息的途径就这么少,所以凑合着也还能用。

接下来提到了两个非常重要的函数, init 和 shutdown ,必须保证在任何操作之前调用初始化:

git_libgit2_init();

释放 init 申请的资源,使用:

git_libgit2_shutdown();

这样就可以开始第一个程序了

#include <git2.h> 

int main() 
{ 
    git_libgit2_init(); 
    
    git_libgit2_shutdown();

     return 0; 
}

ok 打完收工。

先把它跑一下看看,如果可以正常跑完,就说明所有的构建基本上都没啥问题了。

当然,这并没有什么卵用。接下来来搞个有意义的代码来结束这篇。
第一个例子就实现一下 git init

const char* path = "/Users/XiaochenFTX/Documents/data";
git_repository *rep = nullptr;
git_repository_init(&rep, path, 0);

打完收工。

当然,良好的编程习惯,在最后不要忘了释放资源。

git_repository_free(rep); 
git_libgit2_shutdown();

接口的详细说明可以去查官方的 API 我会在必要的地方对一些我觉得比较重要的东西进行说明。

git_repository_init 的第一个参数是将初始化好的 git_repository 指针返回,这个库的接口基本上都是这个风格的,以后就不再提这个事了。
这个函数会返回一个错误码,返回值为 0 表示执行成功,小于 0 表示有错误。所以可以通过判断返回值得方式来确定函数调用是否成功。
想知道具体报错信息,可以调用 giterr_last() 这个函数返回一个 git_error 结构体指针。

/**
* Structure to store extra details of the last error that occurred.
*
* This is kept on a per-thread basis if GIT_THREADS was defined when the
* library was build, otherwise one is kept globally for the library
*/
typedef struct {
    char *message;
    int klass;
} git_error;

klass 对应 git_error_t 的枚举值,用于说明是哪部分出的问题。message 是一个人类可以看懂的的说明信息。开发中遇到问题可以把这个信息打印出来,对找问题有很大帮助。

最终完成 sample1 的代码就是这个样子

#include <git2.h>
#include <iostream>

int main()
{
    git_libgit2_init();
    const char *path = "/Users/XiaochenFTX/Documents/data";
    git_repository *rep = nullptr;
    
    // git init
    int error = git_repository_init(&rep, path, 0);
    if (error < 0) {
        const git_error *e = giterr_last();
        std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
        
        goto SHUTDOWN;
    }
SHUTDOWN:
    git_repository_free(rep);
    git_libgit2_shutdown();
    
    return 0;
}

最后解释一下为什么要用 goto
很多同学在学习 C 语言的时候,如果遇到不负责任的老师,都会强调不要用 goto 这个东西,更有甚者干脆讲都不讲。而具体原因不外乎:会让代码逻辑混乱,可读性差,不好调试
当然,造成这些后果,都是在“如果用不好”的前提下的。然而,我认为这个语言特性真的不是不值一提的垃圾,把 goto 用好在一定程度上是可以让代码更美观、更简洁、更易读。
随便举个例子,比如跳出多层循环,难道还要引入一个外层变量再逐层判断吗?在这种情况下那种方式可读性更高?
还有就是在错误发生的情况下,直接跳到函数结尾进行清理。如果不使用 goto ,我见过几种奇葩方案,最突出的应该是用 do{}while(0); 吧,这样强行为了不用 goto 而产生的奇葩行为,我只能呵呵了。
所以,我推荐在必要的时候使用 goto 来使代码更清晰、简洁。当然,也不是随便瞎用,任何工具都有其最佳适用范围,不能矫枉过正。

附一、代码中引用 libgit2:
在根目录的 CMakeLists.txt 中把相关工程文件夹都用 add_subdirectory 加进来,并且指定 libgit2 的 include 为引用目录

include_directories(extras/libgit2/include)
add_subdirectory(extras/libgit2)
add_subdirectory(sample1)

在需要链接这个库的工程中使用 target_link_libraries 链接上 libgit2
就像我们的 sample1 中这样

target_link_libraries(sample1 git2)

有些环境下会报找不到 -lssh2 的错误:
ld: library not found for -lssh2
只要在根目录的 CMakeLists.txt 中加上一句:

LINK_DIRECTORIES(${LIBSSH2_LIBRARY_DIRS})

这样简单的构建系统就搭建好了,可以开始写代码了
使用其他 IDE 的话,直接用 cmake 导出对应的工程,在自己创建的工程中引入就可以了。
不太推荐直接编译好库放到工程里使用,我自己试了之后发现它用到的几个第三方库还需要自己手动构建再引用。

附二、示例代码的使用:

1. 从 github 上拉代码

git clone git@github.com:XiaochenFTX/libgit2_samples.git
cd libgit2_samples
git submodule update –init –recursive

2. 使用 cmake 导出熟悉的 IDE 工程,或者直接构建运行

mkdir build 
cd build/

Xcode:

cmake .. -G “Xcode”

Visual Studio:

cmake .. -G "Visual Studio"

直接构建:

cmake .. 
make

我使用的 IDE 是 Clion ,使用 Clion 导入工程也可以直接使用

RSS
Follow by Email
YouTube
YouTube
Pinterest
fb-share-icon
LinkedIn
Share
VK
Weibo
WeChat
WhatsApp
Reddit
FbMessenger
Copy link
URL has been copied successfully!