diff --git a/include/git2/rebase.h b/include/git2/rebase.h index 6eb279412..c760fbe74 100644 --- a/include/git2/rebase.h +++ b/include/git2/rebase.h @@ -68,6 +68,19 @@ GIT_EXTERN(int) git_rebase( const git_signature *signature, const git_rebase_options *opts); +/** + * Aborts a rebase that is currently in progress, resetting the repository + * and working directory to their state before rebase began. + * + * @param repo The repository with the in-progress rebase + * @param signature The identity that is aborting the rebase + * @return Zero on success; GIT_ENOTFOUND if a rebase is not in progress, + * -1 on other errors. + */ +GIT_EXTERN(int) git_rebase_abort( + git_repository *repo, + const git_signature *signature); + /** @} */ GIT_END_DECL #endif diff --git a/src/rebase.c b/src/rebase.c index 68d741d25..38ced24bb 100644 --- a/src/rebase.c +++ b/src/rebase.c @@ -44,6 +44,18 @@ typedef enum { GIT_REBASE_TYPE_MERGE = 2, } git_rebase_type_t; +typedef struct { + git_rebase_type_t type; + char *state_path; + + int head_detached : 1; + + char *orig_head_name; + git_oid orig_head_id; +} git_rebase_state; + +#define GIT_REBASE_STATE_INIT {0} + static int rebase_state_type( git_rebase_type_t *type_out, char **path_out, @@ -80,6 +92,84 @@ done: return 0; } +static int rebase_state(git_rebase_state *state, git_repository *repo) +{ + git_buf path = GIT_BUF_INIT, orig_head_name = GIT_BUF_INIT, + orig_head_id = GIT_BUF_INIT; + int state_path_len, error; + + memset(state, 0x0, sizeof(git_rebase_state)); + + if ((error = rebase_state_type(&state->type, &state->state_path, repo)) < 0) + goto done; + + if (state->type == GIT_REBASE_TYPE_NONE) { + giterr_set(GITERR_REBASE, "There is no rebase in progress"); + return GIT_ENOTFOUND; + } + + 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, HEAD_NAME_FILE)) < 0 || + (error = git_futils_readbuffer(&orig_head_name, path.ptr)) < 0) + goto done; + + git_buf_rtrim(&orig_head_name); + + if (strcmp(ORIG_DETACHED_HEAD, orig_head_name.ptr) == 0) + state->head_detached = 1; + + git_buf_truncate(&path, state_path_len); + + if ((error = git_buf_joinpath(&path, path.ptr, ORIG_HEAD_FILE)) < 0) + goto done; + + if (!git_path_isfile(path.ptr)) { + /* Previous versions of git.git used 'head' here; support that. */ + git_buf_truncate(&path, state_path_len); + + if ((error = git_buf_joinpath(&path, path.ptr, HEAD_FILE)) < 0) + goto done; + } + + if ((error = git_futils_readbuffer(&orig_head_id, path.ptr)) < 0) + goto done; + + git_buf_rtrim(&orig_head_id); + + if ((error = git_oid_fromstr(&state->orig_head_id, orig_head_id.ptr)) < 0) + goto done; + + if (!state->head_detached) + state->orig_head_name = git_buf_detach(&orig_head_name); + +done: + git_buf_free(&path); + git_buf_free(&orig_head_name); + git_buf_free(&orig_head_id); + git_buf_free(&onto_id); + return error; +} + +static void rebase_state_free(git_rebase_state *state) +{ + if (state == NULL) + return; + + git__free(state->orig_head_name); + git__free(state->state_path); +} + +static int rebase_finish(git_rebase_state *state) +{ + return git_path_isdir(state->state_path) ? + git_futils_rmdir_r(state->state_path, NULL, GIT_RMDIR_REMOVE_FILES) : + 0; +} + static int rebase_setupfile(git_repository *repo, const char *filename, const char *fmt, ...) { git_buf path = GIT_BUF_INIT, @@ -330,3 +420,41 @@ done: git_buf_free(&reflog); return error; } + +int git_rebase_abort(git_repository *repo, const git_signature *signature) +{ + git_rebase_state state = GIT_REBASE_STATE_INIT; + git_reference *orig_head_ref = NULL; + git_commit *orig_head_commit = NULL; + int error; + + assert(repo && signature); + + if ((error = rebase_state(&state, repo)) < 0) + goto done; + + error = state.head_detached ? + git_reference_create(&orig_head_ref, repo, GIT_HEAD_FILE, + &state.orig_head_id, 1, signature, "rebase: aborting") : + git_reference_symbolic_create( + &orig_head_ref, repo, GIT_HEAD_FILE, state.orig_head_name, 1, + signature, "rebase: aborting"); + + if (error < 0) + goto done; + + if ((error = git_commit_lookup( + &orig_head_commit, repo, &state.orig_head_id)) < 0 || + (error = git_reset(repo, (git_object *)orig_head_commit, + GIT_RESET_HARD, NULL, signature, NULL)) < 0) + goto done; + + error = rebase_finish(&state); + +done: + git_commit_free(orig_head_commit); + git_reference_free(orig_head_ref); + rebase_state_free(&state); + + return error; +} diff --git a/tests/rebase/abort.c b/tests/rebase/abort.c new file mode 100644 index 000000000..896eb4805 --- /dev/null +++ b/tests/rebase/abort.c @@ -0,0 +1,148 @@ +#include "clar_libgit2.h" +#include "git2/rebase.h" +#include "merge.h" +#include "posix.h" + +#include + +static git_repository *repo; + +// Fixture setup and teardown +void test_rebase_abort__initialize(void) +{ + repo = cl_git_sandbox_init("rebase"); +} + +void test_rebase_abort__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +static void test_abort(git_merge_head *branch, git_merge_head *onto) +{ + git_reference *head_ref, *branch_ref = NULL; + git_signature *signature; + git_status_list *statuslist; + git_reflog *reflog; + const git_reflog_entry *reflog_entry; + + cl_git_pass(git_signature_new(&signature, "Rebaser", "rebaser@example.com", 1404157834, -400)); + cl_git_pass(git_rebase_abort(repo, signature)); + + cl_assert_equal_i(GIT_REPOSITORY_STATE_NONE, git_repository_state(repo)); + + /* Make sure the refs are updated appropriately */ + cl_git_pass(git_reference_lookup(&head_ref, repo, "HEAD")); + + if (branch->ref_name == NULL) + cl_assert_equal_oid(git_merge_head_id(branch), git_reference_target(head_ref)); + else { + cl_assert_equal_s("refs/heads/beef", git_reference_symbolic_target(head_ref)); + cl_git_pass(git_reference_lookup(&branch_ref, repo, git_reference_symbolic_target(head_ref))); + cl_assert_equal_oid(git_merge_head_id(branch), git_reference_target(branch_ref)); + } + + git_status_list_new(&statuslist, repo, NULL); + cl_assert_equal_i(0, git_status_list_entrycount(statuslist)); + git_status_list_free(statuslist); + + /* Make sure the reflogs are updated appropriately */ + cl_git_pass(git_reflog_read(&reflog, repo, "HEAD")); + + cl_assert(reflog_entry = git_reflog_entry_byindex(reflog, 0)); + cl_assert_equal_oid(git_merge_head_id(onto), git_reflog_entry_id_old(reflog_entry)); + cl_assert_equal_oid(git_merge_head_id(branch), git_reflog_entry_id_new(reflog_entry)); + cl_assert_equal_s("rebase: aborting", git_reflog_entry_message(reflog_entry)); + + git_reflog_free(reflog); + git_reference_free(head_ref); + git_reference_free(branch_ref); + git_signature_free(signature); +} + +void test_rebase_abort__merge(void) +{ + git_reference *branch_ref, *onto_ref; + git_signature *signature; + git_merge_head *branch_head, *onto_head; + + cl_git_pass(git_reference_lookup(&branch_ref, repo, "refs/heads/beef")); + cl_git_pass(git_reference_lookup(&onto_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(&onto_head, repo, onto_ref)); + + cl_git_pass(git_signature_new(&signature, "Rebaser", "rebaser@example.com", 1404157834, -400)); + + cl_git_pass(git_rebase(repo, branch_head, NULL, onto_head, signature, NULL)); + cl_assert_equal_i(GIT_REPOSITORY_STATE_REBASE_MERGE, git_repository_state(repo)); + + test_abort(branch_head, onto_head); + + git_signature_free(signature); + + git_merge_head_free(branch_head); + git_merge_head_free(onto_head); + + git_reference_free(branch_ref); + git_reference_free(onto_ref); +} + +void test_rebase_abort__detached_head(void) +{ + git_oid branch_id; + git_reference *onto_ref; + git_signature *signature; + git_merge_head *branch_head, *onto_head; + + git_oid_fromstr(&branch_id, "b146bd7608eac53d9bf9e1a6963543588b555c64"); + cl_git_pass(git_reference_lookup(&onto_ref, repo, "refs/heads/master")); + + cl_git_pass(git_merge_head_from_id(&branch_head, repo, &branch_id)); + cl_git_pass(git_merge_head_from_ref(&onto_head, repo, onto_ref)); + + cl_git_pass(git_signature_new(&signature, "Rebaser", "rebaser@example.com", 1404157834, -400)); + + cl_git_pass(git_rebase(repo, branch_head, NULL, onto_head, signature, NULL)); + cl_assert_equal_i(GIT_REPOSITORY_STATE_REBASE_MERGE, git_repository_state(repo)); + + test_abort(branch_head, onto_head); + + git_signature_free(signature); + + git_merge_head_free(branch_head); + git_merge_head_free(onto_head); + + git_reference_free(onto_ref); +} + +void test_rebase_abort__old_style_head_file(void) +{ + git_reference *branch_ref, *onto_ref; + git_signature *signature; + git_merge_head *branch_head, *onto_head; + + cl_git_pass(git_reference_lookup(&branch_ref, repo, "refs/heads/beef")); + cl_git_pass(git_reference_lookup(&onto_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(&onto_head, repo, onto_ref)); + + cl_git_pass(git_signature_new(&signature, "Rebaser", "rebaser@example.com", 1404157834, -400)); + + cl_git_pass(git_rebase(repo, branch_head, NULL, onto_head, signature, NULL)); + cl_assert_equal_i(GIT_REPOSITORY_STATE_REBASE_MERGE, git_repository_state(repo)); + + p_rename("rebase-merge/.git/rebase-merge/orig-head", + "rebase-merge/.git/rebase-merge/head"); + + test_abort(branch_head, onto_head); + + git_signature_free(signature); + + git_merge_head_free(branch_head); + git_merge_head_free(onto_head); + + git_reference_free(branch_ref); + git_reference_free(onto_ref); +}