summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@microsoft.com>2014-07-14 14:35:01 -0400
committerEdward Thomson <ethomson@microsoft.com>2014-10-26 22:59:08 -0400
commit867a36f3a67d0d1905572b84a3e44093fcac643b (patch)
treed56a5153b3d7aabad6706963a26492bfe2a814db /src
parent9e44289c8dfece4171c7f272389b14d040c72228 (diff)
downloadlibgit2-867a36f3a67d0d1905572b84a3e44093fcac643b.tar.gz
Introduce git_rebase to set up a rebase session
Introduce `git_rebase` to set up a rebase session that can then be continued. Immediately, only merge-type rebase is supported.
Diffstat (limited to 'src')
-rw-r--r--src/merge.c25
-rw-r--r--src/rebase.c332
-rw-r--r--src/repository.c22
-rw-r--r--src/repository.h2
4 files changed, 357 insertions, 24 deletions
diff --git a/src/merge.c b/src/merge.c
index 8252f6767..3cafc5d03 100644
--- a/src/merge.c
+++ b/src/merge.c
@@ -1835,29 +1835,6 @@ done:
/* Merge setup / cleanup */
-static int write_orig_head(
- git_repository *repo,
- const git_merge_head *our_head)
-{
- git_filebuf file = GIT_FILEBUF_INIT;
- git_buf file_path = GIT_BUF_INIT;
- int error = 0;
-
- assert(repo && our_head);
-
- if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_ORIG_HEAD_FILE)) == 0 &&
- (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_MERGE_FILE_MODE)) == 0 &&
- (error = git_filebuf_printf(&file, "%s\n", our_head->oid_str)) == 0)
- error = git_filebuf_commit(&file);
-
- if (error < 0)
- git_filebuf_cleanup(&file);
-
- git_buf_free(&file_path);
-
- return error;
-}
-
static int write_merge_head(
git_repository *repo,
const git_merge_head *heads[],
@@ -2229,7 +2206,7 @@ int git_merge__setup(
assert (repo && our_head && heads);
- if ((error = write_orig_head(repo, our_head)) == 0 &&
+ if ((error = git_repository__set_orig_head(repo, &our_head->oid)) == 0 &&
(error = write_merge_head(repo, heads, heads_len)) == 0 &&
(error = write_merge_mode(repo)) == 0) {
error = write_merge_msg(repo, heads, heads_len);
diff --git a/src/rebase.c b/src/rebase.c
new file mode 100644
index 000000000..68d741d25
--- /dev/null
+++ b/src/rebase.c
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include "common.h"
+#include "buffer.h"
+#include "repository.h"
+#include "posix.h"
+#include "filebuf.h"
+#include "merge.h"
+#include "array.h"
+
+#include <git2/types.h>
+#include <git2/rebase.h>
+#include <git2/commit.h>
+#include <git2/reset.h>
+#include <git2/revwalk.h>
+
+#define REBASE_APPLY_DIR "rebase-apply"
+#define REBASE_MERGE_DIR "rebase-merge"
+
+#define HEAD_NAME_FILE "head-name"
+#define ORIG_HEAD_FILE "orig-head"
+#define HEAD_FILE "head"
+#define ONTO_FILE "onto"
+#define ONTO_NAME_FILE "onto_name"
+#define QUIET_FILE "quiet"
+
+#define MSGNUM_FILE "msgnum"
+#define END_FILE "end"
+#define CMT_FILE_FMT "cmt.%d"
+
+#define ORIG_DETACHED_HEAD "detached HEAD"
+
+#define REBASE_DIR_MODE 0777
+#define REBASE_FILE_MODE 0666
+
+typedef enum {
+ GIT_REBASE_TYPE_NONE = 0,
+ GIT_REBASE_TYPE_APPLY = 1,
+ GIT_REBASE_TYPE_MERGE = 2,
+} git_rebase_type_t;
+
+static int rebase_state_type(
+ git_rebase_type_t *type_out,
+ char **path_out,
+ git_repository *repo)
+{
+ git_buf path = GIT_BUF_INIT;
+ git_rebase_type_t type = GIT_REBASE_TYPE_NONE;
+
+ if (git_buf_joinpath(&path, repo->path_repository, REBASE_APPLY_DIR) < 0)
+ return -1;
+
+ if (git_path_isdir(git_buf_cstr(&path))) {
+ type = GIT_REBASE_TYPE_APPLY;
+ goto done;
+ }
+
+ git_buf_clear(&path);
+ if (git_buf_joinpath(&path, repo->path_repository, REBASE_MERGE_DIR) < 0)
+ return -1;
+
+ if (git_path_isdir(git_buf_cstr(&path))) {
+ type = GIT_REBASE_TYPE_MERGE;
+ goto done;
+ }
+
+done:
+ *type_out = type;
+
+ if (type != GIT_REBASE_TYPE_NONE && path_out)
+ *path_out = git_buf_detach(&path);
+
+ git_buf_free(&path);
+
+ return 0;
+}
+
+static int rebase_setupfile(git_repository *repo, const char *filename, const char *fmt, ...)
+{
+ git_buf path = GIT_BUF_INIT,
+ contents = GIT_BUF_INIT;
+ va_list ap;
+ int error;
+
+ va_start(ap, fmt);
+ git_buf_vprintf(&contents, fmt, ap);
+ va_end(ap);
+
+ if ((error = git_buf_joinpath(&path, repo->path_repository, REBASE_MERGE_DIR)) == 0 &&
+ (error = git_buf_joinpath(&path, path.ptr, filename)) == 0)
+ error = git_futils_writebuffer(&contents, path.ptr, O_RDWR|O_CREAT, REBASE_FILE_MODE);
+
+ git_buf_free(&path);
+ git_buf_free(&contents);
+
+ return error;
+}
+
+/* TODO: git.git actually uses the literal argv here, this is an attempt
+ * to emulate that.
+ */
+static const char *rebase_onto_name(const git_merge_head *onto)
+{
+ if (onto->ref_name && git__strncmp(onto->ref_name, "refs/heads/", 11) == 0)
+ return onto->ref_name + 11;
+ else if (onto->ref_name)
+ return onto->ref_name;
+ else
+ return onto->oid_str;
+}
+
+static int rebase_setup_merge(
+ git_repository *repo,
+ const git_merge_head *branch,
+ const git_merge_head *upstream,
+ const git_merge_head *onto,
+ const git_rebase_options *opts)
+{
+ git_revwalk *revwalk = NULL;
+ git_commit *commit;
+ git_buf commit_filename = GIT_BUF_INIT;
+ git_oid id;
+ char id_str[GIT_OID_HEXSZ];
+ bool merge;
+ int commit_cnt = 0, error;
+
+ GIT_UNUSED(opts);
+
+ if (!upstream)
+ upstream = onto;
+
+ if ((error = git_revwalk_new(&revwalk, repo)) < 0 ||
+ (error = git_revwalk_push(revwalk, &branch->oid)) < 0 ||
+ (error = git_revwalk_hide(revwalk, &upstream->oid)) < 0)
+ goto done;
+
+ git_revwalk_sorting(revwalk, GIT_SORT_REVERSE | GIT_SORT_TIME);
+
+ while ((error = git_revwalk_next(&id, revwalk)) == 0) {
+ if ((error = git_commit_lookup(&commit, repo, &id)) < 0)
+ goto done;
+
+ merge = (git_commit_parentcount(commit) > 1);
+ git_commit_free(commit);
+
+ if (merge)
+ continue;
+
+ commit_cnt++;
+
+ git_buf_clear(&commit_filename);
+ git_buf_printf(&commit_filename, CMT_FILE_FMT, commit_cnt);
+
+ git_oid_fmt(id_str, &id);
+ if ((error = rebase_setupfile(repo, commit_filename.ptr,
+ "%.*s\n", GIT_OID_HEXSZ, id_str)) < 0)
+ goto done;
+ }
+
+ if (error != GIT_ITEROVER ||
+ (error = rebase_setupfile(repo, END_FILE, "%d\n", commit_cnt)) < 0)
+ goto done;
+
+ error = rebase_setupfile(repo, ONTO_NAME_FILE, "%s\n",
+ rebase_onto_name(onto));
+
+done:
+ git_revwalk_free(revwalk);
+ git_buf_free(&commit_filename);
+
+ return error;
+}
+
+static int rebase_setup(
+ git_repository *repo,
+ const git_merge_head *branch,
+ const git_merge_head *upstream,
+ const git_merge_head *onto,
+ const git_rebase_options *opts)
+{
+ git_buf state_path = GIT_BUF_INIT;
+ const char *orig_head_name;
+ int error;
+
+ if (git_buf_joinpath(&state_path, repo->path_repository, REBASE_MERGE_DIR) < 0)
+ return -1;
+
+ if ((error = p_mkdir(state_path.ptr, REBASE_DIR_MODE)) < 0) {
+ giterr_set(GITERR_OS, "Failed to create rebase directory '%s'",
+ state_path.ptr);
+ goto done;
+ }
+
+ if ((error = git_repository__set_orig_head(repo, &branch->oid)) < 0)
+ goto done;
+
+ orig_head_name = branch->ref_name ? branch->ref_name : ORIG_DETACHED_HEAD;
+
+ if ((error = rebase_setupfile(repo, HEAD_NAME_FILE, "%s\n", orig_head_name)) < 0 ||
+ (error = rebase_setupfile(repo, ONTO_FILE, "%s\n", onto->oid_str)) < 0 ||
+ (error = rebase_setupfile(repo, ORIG_HEAD_FILE, "%s\n", branch->oid_str)) < 0 ||
+ (error = rebase_setupfile(repo, QUIET_FILE, opts->quiet ? "t\n" : "\n")) < 0)
+ goto done;
+
+ error = rebase_setup_merge(repo, branch, upstream, onto, opts);
+
+done:
+ if (error < 0)
+ git_repository__cleanup_files(repo, (const char **)&state_path.ptr, 1);
+
+ git_buf_free(&state_path);
+
+ return error;
+}
+
+int git_rebase_init_options(git_rebase_options *opts, unsigned int version)
+{
+ GIT_INIT_STRUCTURE_FROM_TEMPLATE(
+ opts, version, git_rebase_options, GIT_REBASE_OPTIONS_INIT);
+ return 0;
+}
+
+static void rebase_normalize_options(
+ git_rebase_options *opts,
+ const git_rebase_options *given_opts)
+{
+ if (given_opts)
+ memcpy(&opts, given_opts, sizeof(git_rebase_options));
+}
+
+static int rebase_ensure_not_in_progress(git_repository *repo)
+{
+ int error;
+ git_rebase_type_t type;
+
+ if ((error = rebase_state_type(&type, NULL, repo)) < 0)
+ return error;
+
+ if (type != GIT_REBASE_TYPE_NONE) {
+ giterr_set(GITERR_REBASE, "There is an existing rebase in progress");
+ return -1;
+ }
+
+ return 0;
+}
+
+static int rebase_ensure_not_dirty(git_repository *repo)
+{
+ git_tree *head = NULL;
+ git_index *index = NULL;
+ git_diff *diff = NULL;
+ int error;
+
+ if ((error = git_repository_head_tree(&head, repo)) < 0 ||
+ (error = git_repository_index(&index, repo)) < 0 ||
+ (error = git_diff_tree_to_index(&diff, repo, head, index, NULL)) < 0)
+ goto done;
+
+ if (git_diff_num_deltas(diff) > 0) {
+ giterr_set(GITERR_REBASE, "Uncommitted changes exist in index");
+ error = -1;
+ goto done;
+ }
+
+ git_diff_free(diff);
+ diff = NULL;
+
+ if ((error = git_diff_index_to_workdir(&diff, repo, index, NULL)) < 0)
+ goto done;
+
+ if (git_diff_num_deltas(diff) > 0) {
+ giterr_set(GITERR_REBASE, "Unstaged changes exist in workdir");
+ error = -1;
+ }
+
+done:
+ git_diff_free(diff);
+ git_index_free(index);
+ git_tree_free(head);
+
+ return error;
+}
+
+int git_rebase(
+ git_repository *repo,
+ const git_merge_head *branch,
+ const git_merge_head *upstream,
+ const git_merge_head *onto,
+ const git_signature *signature,
+ const git_rebase_options *given_opts)
+{
+ git_rebase_options opts = GIT_REBASE_OPTIONS_INIT;
+ git_reference *head_ref = NULL;
+ git_buf reflog = GIT_BUF_INIT;
+ git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
+ int error;
+
+ assert(repo && branch && (upstream || onto));
+
+ GITERR_CHECK_VERSION(given_opts, GIT_MERGE_OPTIONS_VERSION, "git_merge_options");
+ rebase_normalize_options(&opts, given_opts);
+
+ if ((error = git_repository__ensure_not_bare(repo, "rebase")) < 0 ||
+ (error = rebase_ensure_not_in_progress(repo)) < 0 ||
+ (error = rebase_ensure_not_dirty(repo)) < 0)
+ goto done;
+
+ if (!onto)
+ onto = upstream;
+
+ if ((error = rebase_setup(repo, branch, upstream, onto, &opts)) < 0)
+ goto done;
+
+ if ((error = git_buf_printf(&reflog,
+ "rebase: checkout %s", rebase_onto_name(onto))) < 0 ||
+ (error = git_reference_create(&head_ref, repo, GIT_HEAD_FILE,
+ &onto->oid, 1, signature, reflog.ptr)) < 0)
+ goto done;
+
+ checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+ error = git_checkout_head(repo, &checkout_opts);
+
+done:
+ git_reference_free(head_ref);
+ git_buf_free(&reflog);
+ return error;
+}
diff --git a/src/repository.c b/src/repository.c
index f032c899d..2bab52919 100644
--- a/src/repository.c
+++ b/src/repository.c
@@ -1728,6 +1728,28 @@ cleanup:
return error;
}
+int git_repository__set_orig_head(git_repository *repo, const git_oid *orig_head)
+{
+ git_filebuf file = GIT_FILEBUF_INIT;
+ git_buf file_path = GIT_BUF_INIT;
+ char orig_head_str[GIT_OID_HEXSZ];
+ int error = 0;
+
+ git_oid_fmt(orig_head_str, orig_head);
+
+ if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_ORIG_HEAD_FILE)) == 0 &&
+ (error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_MERGE_FILE_MODE)) == 0 &&
+ (error = git_filebuf_printf(&file, "%.*s\n", GIT_OID_HEXSZ, orig_head_str)) == 0)
+ error = git_filebuf_commit(&file);
+
+ if (error < 0)
+ git_filebuf_cleanup(&file);
+
+ git_buf_free(&file_path);
+
+ return error;
+}
+
int git_repository_message(git_buf *out, git_repository *repo)
{
git_buf path = GIT_BUF_INIT;
diff --git a/src/repository.h b/src/repository.h
index aba16a016..45b95a0f0 100644
--- a/src/repository.h
+++ b/src/repository.h
@@ -170,6 +170,8 @@ GIT_INLINE(int) git_repository__ensure_not_bare(
return GIT_EBAREREPO;
}
+int git_repository__set_orig_head(git_repository *repo, const git_oid *orig_head);
+
int git_repository__cleanup_files(git_repository *repo, const char *files[], size_t files_len);
#endif