libgit2 - 樱桃挑选多个提交

libgit2 - cherry pick multiple commits

我正在寻找一种方法来挑选两个或多个提交。

我的目标是能够挑选多个提交,以允许用户在提交之前查看这些更改,而不是要求用户在每次挑选后都提交。

我在下面添加了一个代码片段,它将接受存储库路径,然后是两次提交并尝试连续挑选它们。但是我不确定我需要设置哪些选项以允许挑选两个提交。

第一个 cherry pick 工作正常,但第二个失败了 1 个未提交的更改将被合并覆盖

我曾尝试使用选项 GIT_CHECKOUT_ALLOW_CONFLICTS 但没有成功。需要哪些选项来允许挑选多个提交?

#include <stdio.h>
#include "git2.h"

#define onError(error, errorMsg)\
if (error){\
    const git_error* lg2err = giterr_last();\
    if (lg2err){\
        printf("%s %s\n", errorMsg, lg2err->message);\
        return 1;\
    }\
}

int main(int argc, char* argv[])
{

    if(argc != 4) { printf("Provide repo commit1 commit2\n"); return 1;}

    printf("Repository: %s\n Commit1: %s\n Commit2: %s\n", argv[1], argv[2], argv[3]);

    int error;
    git_libgit2_init();
    git_repository * repo;
    git_oid cid1, cid2;
    git_commit *c1 =NULL;
    git_commit *c2 =NULL;

    error = git_repository_open(&repo, argv[1]);
    onError(error,"Repo open failed: ");

    git_cherrypick_options cherry_opts = GIT_CHERRYPICK_OPTIONS_INIT;

    git_oid_fromstr(&cid1, argv[2]);
    git_oid_fromstr(&cid2, argv[3]);
    error = git_commit_lookup(&c1, repo, &cid1);
    onError(error,"commit lookup failed: ");
    error = git_commit_lookup(&c2, repo, &cid2);
    onError(error,"commit2 lookup failed: ");

    error = git_cherrypick(repo, c1, &cherry_opts);
    onError(error,"cherry1 failed: ");
    error = git_cherrypick(repo, c2, &cherry_opts);
    onError(error,"cherry2 failed: ");

    return 0;
}

正在发生的事情是 libgit2 拒绝覆盖磁盘上已修改的文件,但其内容实际上并没有被 git 存储在任何地方。此文件是 "precious",git 和 libgit2 将竭尽全力避免覆盖它。

没有办法克服这个问题,因为 cherry-picking 不会根据您的工作目录内容应用提交中的差异。它将提交 中的差异应用到 HEAD。也就是说,您唯一的选择是忽略此 cherry-pick 中的更改或覆盖先前 cherry-pick 引入的更改。

举个具体的例子:

假设您在提交 1 时有一些文件:

one
two
three
four
five

并且您有一些基于 1(我们称之为 2)的提交,将文件更改为:

one
2
three
four
five

并且您在不同的分支中还有另一个提交。它也是基于 1(我们称之为 2')。它将文件更改为:

one
two
three
4
five

如果您正在进行提交 1 并在没有提交的情况下选择了 2 和 2',会发生什么情况?从逻辑上讲,您可能希望它进行合并!但它不会。

如果您在提交 1 上,并且在 libgit2(或命令行上的 git cherry-pick --no-commit)中 git_cherrypick 提交 2 是第一次提交,它将读取HEAD 中的文件,并应用提交 2 的更改。这是一个简单的示例,因此内容实际上与提交 2 的内容匹配。该文件将放在磁盘上。

现在,如果你什么都不做——你不提交这个——那么你仍然在提交 1。如果你再次执行 git_cherrypick(这次是提交 2'),那么 lib git2 将从 HEAD 中读取文件并应用提交 2' 的更改。同样,在这个简单的示例中,将 2' 中的更改应用到 1 中的文件会得到提交 2' 中文件的内容。

因为它不会做的是从工作目录中读取文件。

所以现在当它尝试将这些结果写入工作目录时,出现了检出冲突。因为磁盘上文件的内容与 HEAD 中的文件值或我们要检出的文件的值不匹配。所以你被屏蔽了。

您可能想要做的是在这个阶段创建一个提交。我知道你说过你不想避免 "requiring users to commit after each cherry pick"。但是在 libgit2 中创建一个提交对象是有区别的,它是轻量级的并且可以很容易地被丢弃(最终将被垃圾收集)和做 运行 git commit 的道德等价物更新分支指针。

如果您只是创建一个提交并将其写入对象数据库 - 无需切换到它或将其签出 - 那么您可以在工作的其他步骤中重用该数据,而无需让用户看起来已经完成一个承诺。它完全在内存中(还有一点在对象数据库中)而没有访问工作目录。

我鼓励你做的是挑选你想要的每个提交到一个索引中,它在内存中工作并且不接触磁盘。当您对结果感到满意时,您可以创建一个提交对象。您需要使用 git_cherrypick_commit API 而不是 git_cherrypick 来生成索引,然后将其转换为 .例如:

git_reference *head;
git_signature *signature;
git_commit *base1, *base2, *result1;
git_index *idx1, *idx2;
git_oid tree1;

/* Look up the HEAD reference */
git_repository_head(&head, repo);
git_reference_peel((git_object **)&base1, head, GIT_OBJ_COMMIT);

/* Pick the first cherry, getting back an index */
git_cherrypick_commit(&idx1, repo, c1, base1, 0, &cherry_opts);

/* Write that index into a tree */
git_index_write_tree(&tree_id1, idx1);

/* And create a commit object for that tree */
git_signature_now(&signature, "My Cherry-Picking System", "foo@example.com"); 
git_commit_create_from_ids(&result_id1,
    repo,
    NULL, /* don't update a reference */
    signature,
    signature,
    NULL,
    "Transient commit that will be GC'd eventually.",
    &tree_id1,
    1,
    &cid1);
git_commit_lookup(&result1, repo, &result_id1);

/* Now, you can pick the _second_ cherry with the commit you just created as a base... */
git_cherrypick_commit(&idx2, repo, c1, result1, 0, &cherry_opts);

最终你会得到你的终端提交并且你可以检查它 - 我的意思是在 libgit2 git_checkout 中检查的概念,它只是把那些内容放在你的工作目录。不过,不要更新任何分支指针。这将给出结果,其中文件仅在工作目录(和索引)中被修改但用户没有提交任何内容,他们的 HEAD 没有移动。

git_checkout_tree(repo, final_result_commit, NULL);

(你可以传递一个git_commit *git_checkout_tree。它知道该做什么。)

我可以给你一个git_cherrypick_treeAPI,让你容易很多。这将使您省去创建不需要的提交的中间人。但我认为没有人愿意这样做。 (对不起!)

我认为没有人愿意这样做的原因是因为您所描述的更准确地称为 rebase。 Rebase 是一组有序的补丁应用程序或精选步骤。 (Interactive rebase 有点复杂,所以我们暂时忽略它。)

libgit2 有一个 git_rebase 机制,可以完全在内存中工作,为您节省一些将索引转换为树和将提交写入磁盘所涉及的簿记工作。它可以被调用以完全在内存中工作(参见 rebase_commit_inmemory),这可能对您有所帮助。

无论哪种情况,最终结果基本相同,即在用户不知情的情况下将一系列提交写入对象数据库,并在最后更新他们的工作目录以匹配。