Commit 950a7091 by Edward Thomson

Introduce git_rebase_next

`git_rebase_next` will apply the next patch (or cherry-pick)
operation, leaving the results checked out in the index / working
directory so that consumers can resolve any conflicts, as appropriate.
parent 4fe84d62
......@@ -69,6 +69,19 @@ GIT_EXTERN(int) git_rebase(
const git_rebase_options *opts);
/**
* Applies the next patch, updating the index and working directory with the
* changes. If there are conflicts, you will need to address those before
* committing the changes.
*
* @param repo The repository with a rebase in progress
* @param checkout_opts Options to specify how the patch should be checked out
* @return Zero on success; -1 on failure.
*/
GIT_EXTERN(int) git_rebase_next(
git_repository *repo,
git_checkout_options *checkout_opts);
/**
* Aborts a rebase that is currently in progress, resetting the repository
* and working directory to their state before rebase began.
*
......
......@@ -32,6 +32,7 @@
#define MSGNUM_FILE "msgnum"
#define END_FILE "end"
#define CMT_FILE_FMT "cmt.%d"
#define CURRENT_FILE "current"
#define ORIG_DETACHED_HEAD "detached HEAD"
......@@ -42,8 +43,14 @@ typedef enum {
GIT_REBASE_TYPE_NONE = 0,
GIT_REBASE_TYPE_APPLY = 1,
GIT_REBASE_TYPE_MERGE = 2,
GIT_REBASE_TYPE_INTERACTIVE = 3,
} git_rebase_type_t;
struct git_rebase_state_merge {
int32_t msgnum;
int32_t end;
};
typedef struct {
git_rebase_type_t type;
char *state_path;
......@@ -52,6 +59,10 @@ typedef struct {
char *orig_head_name;
git_oid orig_head_id;
union {
struct git_rebase_state_merge merge;
};
} git_rebase_state;
#define GIT_REBASE_STATE_INIT {0}
......@@ -92,6 +103,50 @@ done:
return 0;
}
static int rebase_state_merge(git_rebase_state *state, git_repository *repo)
{
git_buf path = GIT_BUF_INIT, msgnum = GIT_BUF_INIT, end = GIT_BUF_INIT;
int state_path_len, error;
GIT_UNUSED(repo);
if ((error = git_buf_puts(&path, state->state_path)) < 0)
goto done;
state_path_len = git_buf_len(&path);
if ((error = git_buf_joinpath(&path, path.ptr, MSGNUM_FILE)) < 0)
goto done;
if (git_path_isfile(path.ptr)) {
if ((error = git_futils_readbuffer(&msgnum, path.ptr)) < 0)
goto done;
git_buf_rtrim(&msgnum);
if ((error = git__strtol32(&state->merge.msgnum, msgnum.ptr, NULL, 10)) < 0)
goto done;
}
git_buf_truncate(&path, state_path_len);
if ((error = git_buf_joinpath(&path, path.ptr, END_FILE)) < 0 ||
(error = git_futils_readbuffer(&end, path.ptr)) < 0)
goto done;
git_buf_rtrim(&end);
if ((error = git__strtol32(&state->merge.end, end.ptr, NULL, 10)) < 0)
goto done;
done:
git_buf_free(&path);
git_buf_free(&msgnum);
git_buf_free(&end);
return error;
}
static int rebase_state(git_rebase_state *state, git_repository *repo)
{
git_buf path = GIT_BUF_INIT, orig_head_name = GIT_BUF_INIT,
......@@ -146,6 +201,22 @@ static int rebase_state(git_rebase_state *state, git_repository *repo)
if (!state->head_detached)
state->orig_head_name = git_buf_detach(&orig_head_name);
switch (state->type) {
case GIT_REBASE_TYPE_INTERACTIVE:
giterr_set(GITERR_REBASE, "Interactive rebase is not supported");
error = -1;
break;
case GIT_REBASE_TYPE_MERGE:
error = rebase_state_merge(state, repo);
break;
case GIT_REBASE_TYPE_APPLY:
giterr_set(GITERR_REBASE, "Patch application rebase is not supported");
error = -1;
break;
default:
abort();
}
done:
git_buf_free(&path);
git_buf_free(&orig_head_name);
......@@ -421,6 +492,91 @@ done:
return error;
}
static int rebase_next_merge(
git_repository *repo,
git_rebase_state *state,
git_checkout_options *checkout_opts)
{
git_buf path = GIT_BUF_INIT, current = GIT_BUF_INIT;
git_oid current_id;
git_commit *current_commit = NULL, *parent_commit = NULL;
git_tree *current_tree = NULL, *head_tree = NULL, *parent_tree = NULL;
git_index *index = NULL;
unsigned int parent_count;
int error;
if (state->merge.msgnum == state->merge.end)
return GIT_ITEROVER;
state->merge.msgnum++;
if ((error = git_buf_joinpath(&path, state->state_path, "cmt.")) < 0 ||
(error = git_buf_printf(&path, "%d", state->merge.msgnum)) < 0 ||
(error = git_futils_readbuffer(&current, path.ptr)) < 0)
goto done;
git_buf_rtrim(&current);
if ((error = git_oid_fromstr(&current_id, current.ptr)) < 0 ||
(error = git_commit_lookup(&current_commit, repo, &current_id)) < 0 ||
(error = git_commit_tree(&current_tree, current_commit)) < 0 ||
(error = git_repository_head_tree(&head_tree, repo)) < 0)
goto done;
if ((parent_count = git_commit_parentcount(current_commit)) > 1) {
giterr_set(GITERR_REBASE, "Cannot rebase a merge commit");
error = -1;
goto done;
} else if (parent_count) {
if ((error = git_commit_parent(&parent_commit, current_commit, 0)) < 0 ||
(error = git_commit_tree(&parent_tree, parent_commit)) < 0)
goto done;
}
if ((error = rebase_setupfile(repo, MSGNUM_FILE, "%d\n", state->merge.msgnum)) < 0 ||
(error = rebase_setupfile(repo, CURRENT_FILE, "%s\n", current.ptr)) < 0)
goto done;
if ((error = git_merge_trees(&index, repo, parent_tree, head_tree, current_tree, NULL)) < 0 ||
(error = git_merge__check_result(repo, index)) < 0 ||
(error = git_checkout_index(repo, index, checkout_opts)) < 0)
goto done;
done:
git_index_free(index);
git_tree_free(current_tree);
git_tree_free(head_tree);
git_tree_free(parent_tree);
git_commit_free(current_commit);
git_commit_free(parent_commit);
git_buf_free(&path);
git_buf_free(&current);
return error;
}
int git_rebase_next(git_repository *repo, git_checkout_options *opts)
{
git_rebase_state state = GIT_REBASE_STATE_INIT;
int error;
assert(repo);
if ((error = rebase_state(&state, repo)) < 0)
return -1;
switch (state.type) {
case GIT_REBASE_TYPE_MERGE:
error = rebase_next_merge(repo, &state, opts);
break;
default:
abort();
}
rebase_state_free(&state);
return error;
}
int git_rebase_abort(git_repository *repo, const git_signature *signature)
{
git_rebase_state state = GIT_REBASE_STATE_INIT;
......
#include "clar_libgit2.h"
#include "git2/rebase.h"
#include "posix.h"
#include <fcntl.h>
static git_repository *repo;
static git_signature *signature;
// Fixture setup and teardown
void test_rebase_merge__initialize(void)
{
repo = cl_git_sandbox_init("rebase");
}
void test_rebase_merge__cleanup(void)
{
cl_git_sandbox_cleanup();
}
void test_rebase_merge__next(void)
{
git_reference *branch_ref, *upstream_ref;
git_merge_head *branch_head, *upstream_head;
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
git_status_list *status_list;
const git_status_entry *status_entry;
git_oid file1_id;
checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
cl_git_pass(git_reference_lookup(&branch_ref, repo, "refs/heads/beef"));
cl_git_pass(git_reference_lookup(&upstream_ref, repo, "refs/heads/master"));
cl_git_pass(git_merge_head_from_ref(&branch_head, repo, branch_ref));
cl_git_pass(git_merge_head_from_ref(&upstream_head, repo, upstream_ref));
cl_git_pass(git_rebase(repo, branch_head, upstream_head, NULL, signature, NULL));
cl_git_pass(git_rebase_next(repo, &checkout_opts));
cl_assert_equal_file("da9c51a23d02d931a486f45ad18cda05cf5d2b94\n", 41, "rebase/.git/rebase-merge/current");
cl_assert_equal_file("1\n", 2, "rebase/.git/rebase-merge/msgnum");
cl_git_pass(git_status_list_new(&status_list, repo, NULL));
cl_assert_equal_i(1, git_status_list_entrycount(status_list));
cl_assert(status_entry = git_status_byindex(status_list, 0));
cl_assert_equal_s("beef.txt", status_entry->head_to_index->new_file.path);
git_oid_fromstr(&file1_id, "8d95ea62e621f1d38d230d9e7d206e41096d76af");
cl_assert_equal_oid(&file1_id, &status_entry->head_to_index->new_file.id);
git_status_list_free(status_list);
git_merge_head_free(branch_head);
git_merge_head_free(upstream_head);
git_reference_free(branch_ref);
git_reference_free(upstream_ref);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment