commit - 2b496619daecc1f25b1bc0c53e01685030dc2c74
commit + 818c750100809b9b2d2c638d39f1427a66929fce
blob - 892d74adc7e2c036bf693521b13f8399543ee673
blob + 0bcca9eaa8ffbc8bd3c160351ddde972552e16ff
--- got/got.1
+++ got/got.1
.It Cm bo
Short alias for
.Cm backout .
+.It Cm rebase Ar branch
+Rebase commits on the specified
+.Ar branch
+onto the tip of the current branch of the work tree.
+The
+.Ar branch
+must share common ancestry with the work tree's current branch.
+Rebasing begins with the first descendent of the youngest common
+ancestor commit of
+.Ar branch
+and the work tree's current branch, and stops once the tip commit
+of
+.Ar branch
+has been reached.
+.Pp
+Rebased commits are accumulated on a temporary branch and represent
+the same changes and log messages as their counterparts on the original
+.Ar branch ,
+but with different commit IDs.
+Once rebasing has completed successfully, the temporary branch becomes
+the new version of
+.Ar branch
+and the work tree is automatically switched to it.
+.Pp
+While rebasing commits, show the status of each affected file,
+using the following status codes:
+.Bl -column YXZ description
+.It G Ta file was merged
+.It C Ta file was merged and conflicts occurred during merge
+.It ! Ta changes destined for a missing file were not merged
+.It D Ta file was deleted
+.It d Ta file's deletion was obstructed by local modifications
+.It A Ta new file was added
+.It ~ Ta changes destined for a non-regular file were not merged
.El
+.Pp
+If merge conflicts occur, the rebase operation will be interrupted and
+may be continued once conflicts have been resolved.
+Alternatively, the rebase operation may be aborted which will leave
+.Ar branch
+unmodified and the work tree switched back to its original branch.
+.Pp
+.Cm got rebase
+will refuse to run if certain preconditions are not met.
+If the work tree contains multiple base commits it must first be updated
+to a single base commit with
+.Cm got update .
+If the work tree contains local changes, these changes must be committed
+or reverted first.
+.Pp
+Some
+.Nm
+commands may refuse to run while a rebase operation is in progress.
+.Pp
+The options for
+.Cm got rebase
+are as follows:
+.Bl -tag -width Ds
+.It Fl a
+Abort an interrupted rebase operation.
+.It Fl c
+Continue an interrupted current rebase operation.
+.El
+.It Cm rb
+Short alias for
+.Cm rebase .
+.El
.Sh ENVIRONMENT
.Bl -tag -width GOT_AUTHOR
.It Ev GOT_AUTHOR
branch on top of the new head commit of the
.Dq master
branch.
-This step currently requires
-.Xr git 1 :
.Pp
-.Dl $ git clone /var/git/src.git ~/src-git-wt
-.Dl $ cd ~/src-git-wt
-.Dl $ git checkout unified-buffer-cache
-.Dl $ git rebase master
-.Dl $ git push -f
-.Pp
-Update the work tree to the newly rebased
-.Dq unified-buffer-cache
-branch:
-.Pp
-.Dl $ got update -b unified-buffer-cache
+.Dl $ got update -b master
+.Dl $ got rebase unified-buffer-cache
.Sh SEE ALSO
.Xr git-repository 5
.Xr got-worktree 5
blob - 9ab824b4938b0e7ee27eb4d390a76c74e7f61c9f
blob + e6bdb5cd9d11bf52149587239d83c4a3b11b0613
--- got/got.c
+++ got/got.c
#include <err.h>
#include <errno.h>
#include <locale.h>
+#include <ctype.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
__dead static void usage_commit(void);
__dead static void usage_cherrypick(void);
__dead static void usage_backout(void);
+__dead static void usage_rebase(void);
static const struct got_error* cmd_init(int, char *[]);
static const struct got_error* cmd_checkout(int, char *[]);
static const struct got_error* cmd_commit(int, char *[]);
static const struct got_error* cmd_cherrypick(int, char *[]);
static const struct got_error* cmd_backout(int, char *[]);
+static const struct got_error* cmd_rebase(int, char *[]);
static struct got_cmd got_commands[] = {
{ "init", cmd_init, usage_init, "" },
{ "commit", cmd_commit, usage_commit, "ci" },
{ "cherrypick", cmd_cherrypick, usage_cherrypick, "cy" },
{ "backout", cmd_backout, usage_backout, "bo" },
+ { "rebase", cmd_rebase, usage_rebase, "rb" },
};
static void
free(commit_id_str);
if (head_ref)
got_ref_close(head_ref);
+ if (worktree)
+ got_worktree_close(worktree);
+ if (repo)
+ got_repo_close(repo);
+ return error;
+}
+
+__dead static void
+usage_rebase(void)
+{
+ fprintf(stderr, "usage: %s rebase [-a] [-c] | branch\n", getprogname());
+ exit(1);
+}
+
+static const struct got_error *
+show_rebase_progress(struct got_commit_object *commit,
+ struct got_object_id *old_id, struct got_object_id *new_id)
+{
+ const struct got_error *err;
+ char *old_id_str = NULL, *new_id_str = NULL;
+ char *logmsg0 = NULL, *logmsg, *nl;
+ size_t len;
+
+ err = got_object_id_str(&old_id_str, old_id);
+ if (err)
+ goto done;
+
+ err = got_object_id_str(&new_id_str, new_id);
+ if (err)
+ goto done;
+
+ logmsg0 = strdup(got_object_commit_get_logmsg(commit));
+ if (logmsg0 == NULL) {
+ err = got_error_from_errno("strdup");
+ goto done;
+ }
+ logmsg = logmsg0;
+
+ while (isspace(logmsg[0]))
+ logmsg++;
+
+ old_id_str[12] = '\0';
+ new_id_str[12] = '\0';
+ len = strlen(logmsg);
+ if (len > 42)
+ len = 42;
+ logmsg[len] = '\0';
+ nl = strchr(logmsg, '\n');
+ if (nl)
+ *nl = '\0';
+ printf("%s -> %s: %s\n", old_id_str, new_id_str, logmsg);
+done:
+ free(old_id_str);
+ free(new_id_str);
+ free(logmsg0);
+ return err;
+}
+
+static void
+rebase_progress(void *arg, unsigned char status, const char *path)
+{
+ unsigned char *rebase_status = arg;
+
+ while (path[0] == '/')
+ path++;
+ printf("%c %s\n", status, path);
+
+ if (*rebase_status == GOT_STATUS_CONFLICT)
+ return;
+ if (status == GOT_STATUS_CONFLICT || status == GOT_STATUS_MERGE)
+ *rebase_status = status;
+}
+
+static const struct got_error *
+rebase_complete(struct got_worktree *worktree, struct got_reference *branch,
+ struct got_reference *new_base_branch, struct got_reference *tmp_branch,
+ struct got_repository *repo)
+{
+ printf("Switching work tree to %s\n", got_ref_get_name(branch));
+ return got_worktree_rebase_complete(worktree,
+ new_base_branch, tmp_branch, branch, repo);
+}
+
+static const struct got_error *
+cmd_rebase(int argc, char *argv[])
+{
+ const struct got_error *error = NULL;
+ struct got_worktree *worktree = NULL;
+ struct got_repository *repo = NULL;
+ char *cwd = NULL;
+ struct got_reference *branch = NULL;
+ struct got_reference *new_base_branch = NULL, *tmp_branch = NULL;
+ struct got_object_id *commit_id = NULL, *parent_id = NULL;
+ struct got_object_id *resume_commit_id = NULL;
+ struct got_object_id *branch_head_commit_id = NULL, *yca_id = NULL;
+ struct got_commit_graph *graph = NULL;
+ struct got_commit_object *commit = NULL;
+ int ch, rebase_in_progress = 0, abort_rebase = 0, continue_rebase = 0;
+ unsigned char rebase_status = GOT_STATUS_NO_CHANGE;
+ struct got_object_id_queue commits;
+ const struct got_object_id_queue *parent_ids;
+ struct got_object_qid *qid, *pid;
+
+ SIMPLEQ_INIT(&commits);
+
+ while ((ch = getopt(argc, argv, "ac")) != -1) {
+ switch (ch) {
+ case 'a':
+ abort_rebase = 1;
+ break;
+ case 'c':
+ continue_rebase = 1;
+ break;
+ default:
+ usage_rebase();
+ /* NOTREACHED */
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (abort_rebase && continue_rebase)
+ usage_rebase();
+ else if (abort_rebase || continue_rebase) {
+ if (argc != 0)
+ usage_rebase();
+ } else if (argc != 1)
+ usage_rebase();
+
+ cwd = getcwd(NULL, 0);
+ if (cwd == NULL) {
+ error = got_error_from_errno("getcwd");
+ goto done;
+ }
+ error = got_worktree_open(&worktree, cwd);
+ if (error)
+ goto done;
+
+
+ error = got_repo_open(&repo, got_worktree_get_repo_path(worktree));
+ if (error != NULL)
+ goto done;
+
+ error = apply_unveil(got_repo_get_path(repo), 0,
+ got_worktree_get_root_path(worktree), 0);
+ if (error)
+ goto done;
+
+ error = got_worktree_rebase_in_progress(&rebase_in_progress, worktree);
+ if (error)
+ goto done;
+
+ if (rebase_in_progress && abort_rebase) {
+ int did_something;
+ error = got_worktree_rebase_continue(&resume_commit_id,
+ &new_base_branch, &tmp_branch, &branch, worktree, repo);
+ if (error)
+ goto done;
+ printf("Switching work tree to %s\n",
+ got_ref_get_symref_target(new_base_branch));
+ error = got_worktree_rebase_abort(worktree, repo,
+ new_base_branch, update_progress, &did_something);
+ if (error)
+ goto done;
+ printf("Rebase of %s aborted\n", got_ref_get_name(branch));
+ goto done; /* nothing else to do */
+ } else if (abort_rebase) {
+ error = got_error(GOT_ERR_NOT_REBASING);
+ goto done;
+ }
+
+ if (continue_rebase) {
+ struct got_object_id *new_commit_id;
+
+ error = got_worktree_rebase_continue(&resume_commit_id,
+ &new_base_branch, &tmp_branch, &branch, worktree, repo);
+ if (error)
+ goto done;
+ error = got_commit_graph_find_youngest_common_ancestor(&yca_id,
+ got_worktree_get_base_commit_id(worktree),
+ resume_commit_id, repo);
+ if (error)
+ goto done;
+ if (yca_id == NULL) {
+ error = got_error_msg(GOT_ERR_ANCESTRY,
+ "cannot determine common ancestor commit for "
+ "continued rebase operation");
+ goto done;
+ }
+ error = got_object_open_as_commit(&commit, repo,
+ resume_commit_id);
+ if (error)
+ goto done;
+
+ error = got_worktree_rebase_commit(&new_commit_id, worktree,
+ tmp_branch, commit, resume_commit_id, repo);
+ if (error)
+ goto done;
+ error = show_rebase_progress(commit, resume_commit_id,
+ new_commit_id);
+ free(new_commit_id);
+ free(resume_commit_id);
+
+ resume_commit_id = got_object_id_dup(SIMPLEQ_FIRST(
+ got_object_commit_get_parent_ids(commit))->id);
+ if (resume_commit_id == NULL) {
+ error = got_error_from_errno("got_object_id_dup");
+ goto done;
+ }
+ got_object_commit_close(commit);
+ commit = NULL;
+ commit_id = resume_commit_id;
+ if (got_object_id_cmp(resume_commit_id, yca_id) == 0) {
+ error = rebase_complete(worktree, branch,
+ new_base_branch, tmp_branch, repo);
+ /* YCA has been reached; we are done. */
+ goto done;
+ }
+ } else {
+ error = got_ref_open(&branch, repo, argv[0], 0);
+ if (error != NULL)
+ goto done;
+
+ error = check_same_branch(
+ got_worktree_get_base_commit_id(worktree), branch, repo);
+ if (error) {
+ if (error->code != GOT_ERR_ANCESTRY)
+ goto done;
+ error = NULL;
+ } else {
+ error = got_error_msg(GOT_ERR_SAME_BRANCH,
+ "specified branch resolves to a commit which "
+ "is already contained in work tree's branch");
+ goto done;
+ }
+
+ error = got_ref_resolve(&branch_head_commit_id, repo, branch);
+ if (error)
+ goto done;
+ error = got_commit_graph_find_youngest_common_ancestor(&yca_id,
+ got_worktree_get_base_commit_id(worktree),
+ branch_head_commit_id, repo);
+ if (error)
+ goto done;
+ if (yca_id == NULL) {
+ error = got_error_msg(GOT_ERR_ANCESTRY,
+ "specified branch shares no common ancestry "
+ "with work tree's branch");
+ goto done;
+ }
+
+ error = got_worktree_rebase_prepare(&new_base_branch,
+ &tmp_branch, worktree, branch, repo);
+ if (error)
+ goto done;
+ commit_id = branch_head_commit_id;
+ }
+
+ error = got_object_open_as_commit(&commit, repo, commit_id);
+ if (error)
+ goto done;
+
+ error = got_commit_graph_open(&graph, commit_id, "/", 1, repo);
+ if (error)
+ goto done;
+ parent_ids = got_object_commit_get_parent_ids(commit);
+ pid = SIMPLEQ_FIRST(parent_ids);
+ error = got_commit_graph_iter_start(graph, pid->id, repo);
+ got_object_commit_close(commit);
+ commit = NULL;
+ if (error)
+ goto done;
+ while (got_object_id_cmp(commit_id, yca_id) != 0) {
+ error = got_commit_graph_iter_next(&parent_id, graph);
+ if (error) {
+ if (error->code == GOT_ERR_ITER_COMPLETED) {
+ error = got_error_msg(GOT_ERR_ANCESTRY,
+ "ran out of commits to rebase before "
+ "youngest common ancestor commit has "
+ "been reached?!?");
+ goto done;
+ } else if (error->code != GOT_ERR_ITER_NEED_MORE)
+ goto done;
+ error = got_commit_graph_fetch_commits(graph, 1, repo);
+ if (error)
+ goto done;
+ } else {
+ error = got_object_qid_alloc(&qid, commit_id);
+ if (error)
+ goto done;
+ SIMPLEQ_INSERT_HEAD(&commits, qid, entry);
+ commit_id = parent_id;
+ }
+ }
+
+ if (SIMPLEQ_EMPTY(&commits)) {
+ error = got_error(GOT_ERR_EMPTY_REBASE);
+ goto done;
+ }
+
+ pid = NULL;
+ SIMPLEQ_FOREACH(qid, &commits, entry) {
+ struct got_object_id *new_commit_id;
+
+ commit_id = qid->id;
+ parent_id = pid ? pid->id : yca_id;
+ pid = qid;
+
+ error = got_worktree_rebase_merge_files(worktree, parent_id,
+ commit_id, repo, rebase_progress, &rebase_status,
+ check_cancelled, NULL);
+ if (error)
+ goto done;
+
+ if (rebase_status == GOT_STATUS_CONFLICT)
+ break;
+
+ error = got_object_open_as_commit(&commit, repo, commit_id);
+ if (error)
+ goto done;
+ error = got_worktree_rebase_commit(&new_commit_id, worktree,
+ tmp_branch, commit, commit_id, repo);
+ if (error)
+ goto done;
+ error = show_rebase_progress(commit, commit_id, new_commit_id);
+ free(new_commit_id);
+ got_object_commit_close(commit);
+ commit = NULL;
+ if (error)
+ goto done;
+
+ }
+
+ if (rebase_status == GOT_STATUS_CONFLICT) {
+ error = got_worktree_rebase_postpone(worktree);
+ if (error)
+ goto done;
+ error = got_error_msg(GOT_ERR_CONFLICTS,
+ "conflicts must be resolved before rebase can be resumed");
+ } else
+ error = rebase_complete(worktree, branch, new_base_branch,
+ tmp_branch, repo);
+done:
+ got_object_id_queue_free(&commits);
+ free(branch_head_commit_id);
+ free(resume_commit_id);
+ free(yca_id);
+ if (graph)
+ got_commit_graph_close(graph);
+ if (commit)
+ got_object_commit_close(commit);
+ if (branch)
+ got_ref_close(branch);
+ if (new_base_branch)
+ got_ref_close(new_base_branch);
+ if (tmp_branch)
+ got_ref_close(tmp_branch);
if (worktree)
got_worktree_close(worktree);
if (repo)
blob - 83b7c99d663d7a35621aa740a8b3f4da13281612
blob + 441dea9a556adbeec96868da881ff0e40f1f605d
--- include/got_error.h
+++ include/got_error.h
#define GOT_ERR_MIXED_COMMITS 81
#define GOT_ERR_CONFLICTS 82
#define GOT_ERR_BRANCH_EXISTS 83
+#define GOT_ERR_MODIFIED 84
+#define GOT_ERR_NOT_REBASING 85
+#define GOT_ERR_EMPTY_REBASE 86
+#define GOT_ERR_REBASE_COMMITID 87
static const struct got_error {
int code;
{ GOT_ERR_CONFLICTS, "work tree contains conflicted files; these "
"conflicts must be resolved first" },
{ GOT_ERR_BRANCH_EXISTS,"specified branch already exists" },
+ { GOT_ERR_MODIFIED, "work tree contains local changes; these "
+ "changes must be committed or reverted first" },
+ { GOT_ERR_NOT_REBASING, "rebase operation not in progress" },
+ { GOT_ERR_EMPTY_REBASE, "no commits to rebase" },
+ { GOT_ERR_REBASE_COMMITID,"rebase commit ID mismatch" },
};
/*
blob - 2dd6e8cfae54c6bc3ebc6d27f919785066d57529
blob + c61b327efec88778ab8c25f0964c86989d6075bb
--- include/got_worktree.h
+++ include/got_worktree.h
struct got_worktree;
struct got_commitable;
+struct got_commit_object;
/* status codes */
#define GOT_STATUS_NO_CHANGE ' '
/* Get the status of a commitable worktree item. */
unsigned int got_commitable_get_status(struct got_commitable *);
+
+/*
+ * Prepare for rebasing a branch onto the work tree's current branch.
+ * This function creates references to a temporary branch, the branch
+ * being rebased, and the work tree's current branch, under the
+ * "got/worktree/rebase/" namespace. These references are used to
+ * keep track of rebase operation state and are used as input and/or
+ * output arguments with other rebase-related functions.
+ */
+const struct got_error *got_worktree_rebase_prepare(struct got_reference **,
+ struct got_reference **, struct got_worktree *, struct got_reference *,
+ struct got_repository *);
+
+/*
+ * Continue an interrupted rebase operation.
+ * This function returns existing references created when rebase was prepared,
+ * and the ID of the commit currently being rebased. This should be called
+ * before either resuming or aborting a rebase operation.
+ */
+const struct got_error *got_worktree_rebase_continue(struct got_object_id **,
+ struct got_reference **, struct got_reference **, struct got_reference **,
+ struct got_worktree *, struct got_repository *);
+
+/* Check whether a, potentially interrupted, rebase operation is in progress. */
+const struct got_error *got_worktree_rebase_in_progress(int *,
+ struct got_worktree *);
+
+/*
+ * Merge changes from the commit currently being rebased into the work tree.
+ * Report affected files, including merge conflicts, via the specified
+ * progress callback.
+ */
+const struct got_error *got_worktree_rebase_merge_files(
+ struct got_worktree *, struct got_object_id *, struct got_object_id *,
+ struct got_repository *, got_worktree_checkout_cb, void *,
+ got_worktree_cancel_cb, void *);
+
+/*
+ * Commit merged rebased changes to a temporary branch and return the
+ * ID of the newly created commit.
+ */
+const struct got_error *got_worktree_rebase_commit(struct got_object_id **,
+ struct got_worktree *, struct got_reference *, struct got_commit_object *,
+ struct got_object_id *, struct got_repository *);
+
+/* Postpone the rebase operation. Should be called after a merge conflict. */
+const struct got_error *got_worktree_rebase_postpone(struct got_worktree *);
+
+/*
+ * Complete the current rebase operation. This should be called once all
+ * commits have been rebased successfully.
+ */
+const struct got_error *got_worktree_rebase_complete(struct got_worktree *,
+ struct got_reference *, struct got_reference *, struct got_reference *,
+ struct got_repository *);
+
+/*
+ * Abort the current rebase operation.
+ * Report reverted files via the specified progress callback.
+ */
+const struct got_error *got_worktree_rebase_abort(struct got_worktree *,
+ struct got_repository *, struct got_reference *,
+ got_worktree_checkout_cb, void *);
blob - df3ae1a5f75b418b82f1861704326ed50c7b09b7
blob + 05aa9d3b001104a9b0383a1b83f76f77924f6916
--- lib/commit_graph.c
+++ lib/commit_graph.c
int i, ntips;
*nfetched = 0;
- *changed_id = NULL;
+ if (changed_id)
+ *changed_id = NULL;
ntips = got_object_idset_num_elements(graph->open_branches);
if (ntips == 0)
commit, repo);
if (err)
break;
- if (changed && *changed_id == NULL)
+ if (changed && changed_id && *changed_id == NULL)
*changed_id = commit_id;
}
done:
int changed;
start_node = got_object_idset_get(graph->node_ids, id);
- if (start_node == NULL)
- return got_error_no_obj(id);
+ while (start_node == NULL) {
+ int ncommits;
+ err = fetch_commits_from_open_branches(&ncommits, NULL, graph,
+ repo);
+ if (err)
+ return err;
+ if (ncommits == 0)
+ return got_error_no_obj(id);
+ start_node = got_object_idset_get(graph->node_ids, id);
+ }
err = got_object_open_as_commit(&commit, repo, &start_node->id);
if (err)
blob - 9809abfda0e41da7cba9bee90a6e329ed1c7f01f
blob + 317d0fdf6a3b2bf43047f3743caed1b085199df0
--- lib/got_lib_worktree.h
+++ lib/got_lib_worktree.h
const struct got_error *got_worktree_get_base_ref_name(char **,
struct got_worktree *worktree);
+
+/* Temporary branch which accumulates commits during a rebase operation. */
+#define GOT_WORKTREE_REBASE_TMP_REF_PREFIX "refs/got/worktree/rebase/tmp"
+
+/* Symbolic reference pointing at the name of the new base branch. */
+#define GOT_WORKTREE_NEWBASE_REF_PREFIX "refs/got/worktree/rebase/newbase"
+
+/* Symbolic reference pointing at the name of the branch being rebased. */
+#define GOT_WORKTREE_REBASE_BRANCH_REF_PREFIX "refs/got/worktree/rebase/branch"
+
+/* Reference pointing at the ID of the current commit being rebased. */
+#define GOT_WORKTREE_REBASE_COMMIT_REF_PREFIX "refs/got/worktree/rebase/commit"
blob - 6c06691116548c4c4bf246510416f6568ebfdb71
blob + 4d551f4bea130a721ce5deddee5d882ebf873ec1
--- lib/worktree.c
+++ lib/worktree.c
merge_blob(int *local_changes_subsumed, struct got_worktree *worktree,
struct got_blob_object *blob_orig, const char *ondisk_path,
const char *path, uint16_t st_mode, struct got_blob_object *blob_deriv,
+ struct got_object_id *deriv_base_commit_id,
struct got_repository *repo, got_worktree_checkout_cb progress_cb,
void *progress_arg)
{
char *blob_deriv_path = NULL, *blob_orig_path = NULL;
char *merged_path = NULL, *base_path = NULL;
char *id_str = NULL;
- char *label1 = NULL;
+ char *label_deriv = NULL;
int overlapcnt = 0;
char *parent;
*/
}
- err = got_object_id_str(&id_str, worktree->base_commit_id);
+ err = got_object_id_str(&id_str, deriv_base_commit_id);
if (err)
goto done;
- if (asprintf(&label1, "commit %s", id_str) == -1) {
+ if (asprintf(&label_deriv, "commit %s", id_str) == -1) {
err = got_error_from_errno("asprintf");
goto done;
}
err = got_merge_diff3(&overlapcnt, merged_fd, blob_deriv_path,
- blob_orig_path, ondisk_path, label1, path);
+ blob_orig_path, ondisk_path, label_deriv, path);
if (err)
goto done;
free(blob_orig_path);
}
free(id_str);
- free(label1);
+ free(label_deriv);
return err;
}
goto done;
}
err = merge_blob(&update_timestamps, worktree, blob2,
- ondisk_path, path, sb.st_mode, blob, repo,
+ ondisk_path, path, sb.st_mode, blob,
+ worktree->base_commit_id, repo,
progress_cb, progress_arg);
if (blob2)
got_object_blob_close(blob2);
return err;
}
-const struct got_error *
-got_worktree_get_base_ref_name(char **refname, struct got_worktree *worktree)
+static const struct got_error *
+get_ref_name(char **refname, struct got_worktree *worktree, const char *prefix)
{
const struct got_error *err = NULL;
char *uuidstr = NULL;
if (uuid_status != uuid_s_ok)
return got_error_uuid(uuid_status);
- if (asprintf(refname, "%s-%s", GOT_WORKTREE_BASE_REF_PREFIX, uuidstr)
+ if (asprintf(refname, "%s-%s", prefix, uuidstr)
== -1) {
err = got_error_from_errno("asprintf");
*refname = NULL;
return err;
}
+const struct got_error *
+got_worktree_get_base_ref_name(char **refname, struct got_worktree *worktree)
+{
+ return get_ref_name(refname, worktree, GOT_WORKTREE_BASE_REF_PREFIX);
+}
+
+static const struct got_error *
+get_rebase_tmp_ref_name(char **refname, struct got_worktree *worktree)
+{
+ return get_ref_name(refname, worktree,
+ GOT_WORKTREE_REBASE_TMP_REF_PREFIX);
+}
+
+static const struct got_error *
+get_newbase_symref_name(char **refname, struct got_worktree *worktree)
+{
+ return get_ref_name(refname, worktree, GOT_WORKTREE_NEWBASE_REF_PREFIX);
+}
+
+static const struct got_error *
+get_rebase_branch_symref_name(char **refname, struct got_worktree *worktree)
+{
+ return get_ref_name(refname, worktree,
+ GOT_WORKTREE_REBASE_BRANCH_REF_PREFIX);
+}
+
+static const struct got_error *
+get_rebase_commit_ref_name(char **refname, struct got_worktree *worktree)
+{
+ return get_ref_name(refname, worktree,
+ GOT_WORKTREE_REBASE_COMMIT_REF_PREFIX);
+}
+
+
/*
* Prevent Git's garbage collector from deleting our base commit by
* setting a reference to our base commit's ID.
void *progress_arg;
got_worktree_cancel_cb cancel_cb;
void *cancel_arg;
+ struct got_object_id *commit_id2;
};
static const struct got_error *
}
err = merge_blob(&local_changes_subsumed, a->worktree, blob1,
- ondisk_path, path2, sb.st_mode, blob2, repo,
+ ondisk_path, path2, sb.st_mode, blob2, a->commit_id2, repo,
a->progress_cb, a->progress_arg);
} else if (blob1) {
ie = got_fileindex_entry_get(a->fileindex, path1);
goto done;
}
err = merge_blob(&local_changes_subsumed, a->worktree,
- NULL, ondisk_path, path2, sb.st_mode, blob2, repo,
+ NULL, ondisk_path, path2, sb.st_mode, blob2,
+ a->commit_id2, repo,
a->progress_cb, a->progress_arg);
if (status == GOT_STATUS_DELETE) {
err = update_blob_fileindex_entry(a->worktree,
return NULL;
}
-const struct got_error *
-got_worktree_merge_files(struct got_worktree *worktree,
- struct got_object_id *commit_id1, struct got_object_id *commit_id2,
- struct got_repository *repo, got_worktree_checkout_cb progress_cb,
- void *progress_arg, got_worktree_cancel_cb cancel_cb, void *cancel_arg)
+static const struct got_error *
+merge_files(struct got_worktree *worktree, struct got_fileindex *fileindex,
+ const char *fileindex_path, struct got_object_id *commit_id1,
+ struct got_object_id *commit_id2, struct got_repository *repo,
+ got_worktree_checkout_cb progress_cb, void *progress_arg,
+ got_worktree_cancel_cb cancel_cb, void *cancel_arg)
{
- const struct got_error *err = NULL, *sync_err, *unlockerr;
+ const struct got_error *err = NULL, *sync_err;
struct got_object_id *tree_id1 = NULL, *tree_id2 = NULL;
struct got_tree_object *tree1 = NULL, *tree2 = NULL;
struct merge_file_cb_arg arg;
- char *fileindex_path = NULL;
- struct got_fileindex *fileindex = NULL;
- struct check_merge_ok_arg mok_arg;
-
- err = lock_worktree(worktree, LOCK_EX);
- if (err)
- return err;
-
- err = open_fileindex(&fileindex, &fileindex_path, worktree);
- if (err)
- goto done;
- mok_arg.worktree = worktree;
- mok_arg.repo = repo;
- err = got_fileindex_for_each_entry_safe(fileindex, check_merge_ok,
- &mok_arg);
- if (err)
- goto done;
-
if (commit_id1) {
err = got_object_id_by_path(&tree_id1, repo, commit_id1,
worktree->path_prefix);
arg.progress_arg = progress_arg;
arg.cancel_cb = cancel_cb;
arg.cancel_arg = cancel_arg;
+ arg.commit_id2 = commit_id2;
err = got_diff_tree(tree1, tree2, "", "", repo, merge_file_cb, &arg);
sync_err = sync_fileindex(fileindex, fileindex_path);
if (sync_err && err == NULL)
err = sync_err;
done:
- got_fileindex_free(fileindex);
if (tree1)
got_object_tree_close(tree1);
if (tree2)
got_object_tree_close(tree2);
+ return err;
+}
+const struct got_error *
+got_worktree_merge_files(struct got_worktree *worktree,
+ struct got_object_id *commit_id1, struct got_object_id *commit_id2,
+ struct got_repository *repo, got_worktree_checkout_cb progress_cb,
+ void *progress_arg, got_worktree_cancel_cb cancel_cb, void *cancel_arg)
+{
+ const struct got_error *err, *unlockerr;
+ char *fileindex_path = NULL;
+ struct got_fileindex *fileindex = NULL;
+ struct check_merge_ok_arg mok_arg;
+
+ err = lock_worktree(worktree, LOCK_EX);
+ if (err)
+ return err;
+
+ err = open_fileindex(&fileindex, &fileindex_path, worktree);
+ if (err)
+ goto done;
+
+ mok_arg.worktree = worktree;
+ mok_arg.repo = repo;
+ err = got_fileindex_for_each_entry_safe(fileindex, check_merge_ok,
+ &mok_arg);
+ if (err)
+ goto done;
+
+ err = merge_files(worktree, fileindex, fileindex_path, commit_id1,
+ commit_id2, repo, progress_cb, progress_arg, cancel_cb, cancel_arg);
+done:
+ if (fileindex)
+ got_fileindex_free(fileindex);
+ free(fileindex_path);
unlockerr = lock_worktree(worktree, LOCK_SH);
if (unlockerr && err == NULL)
err = unlockerr;
got_commitable_get_status(struct got_commitable *ct)
{
return ct->status;
+}
+
+struct check_rebase_ok_arg {
+ struct got_worktree *worktree;
+ struct got_repository *repo;
+ int rebase_in_progress;
+};
+
+static const struct got_error *
+check_rebase_ok(void *arg, struct got_fileindex_entry *ie)
+{
+ const struct got_error *err = NULL;
+ struct check_rebase_ok_arg *a = arg;
+ unsigned char status;
+ struct stat sb;
+ char *ondisk_path;
+
+ if (!a->rebase_in_progress) {
+ /* Reject rebase of a work tree with mixed base commits. */
+ if (memcmp(ie->commit_sha1, a->worktree->base_commit_id->sha1,
+ SHA1_DIGEST_LENGTH))
+ return got_error(GOT_ERR_MIXED_COMMITS);
+ }
+
+ if (asprintf(&ondisk_path, "%s/%s", a->worktree->root_path, ie->path)
+ == -1)
+ return got_error_from_errno("asprintf");
+
+ /* Reject rebase of a work tree with modified or conflicted files. */
+ err = get_file_status(&status, &sb, ie, ondisk_path, a->repo);
+ free(ondisk_path);
+ if (err)
+ return err;
+
+ if (a->rebase_in_progress) {
+ if (status == GOT_STATUS_CONFLICT)
+ return got_error(GOT_ERR_CONFLICTS);
+ } else if (status != GOT_STATUS_NO_CHANGE)
+ return got_error(GOT_ERR_MODIFIED);
+
+ return NULL;
+}
+
+const struct got_error *
+got_worktree_rebase_prepare(struct got_reference **new_base_branch_ref,
+ struct got_reference **tmp_branch, struct got_worktree *worktree,
+ struct got_reference *branch, struct got_repository *repo)
+{
+ const struct got_error *err = NULL;
+ char *tmp_branch_name = NULL, *new_base_branch_ref_name = NULL;
+ char *branch_ref_name = NULL;
+ struct got_fileindex *fileindex = NULL;
+ char *fileindex_path = NULL;
+ struct check_rebase_ok_arg ok_arg;
+ struct got_reference *wt_branch = NULL, *branch_ref = NULL;
+
+ *new_base_branch_ref = NULL;
+ *tmp_branch = NULL;
+
+ err = lock_worktree(worktree, LOCK_EX);
+ if (err)
+ return err;
+
+ err = open_fileindex(&fileindex, &fileindex_path, worktree);
+ if (err)
+ goto done;
+
+ ok_arg.worktree = worktree;
+ ok_arg.repo = repo;
+ ok_arg.rebase_in_progress = 0;
+ err = got_fileindex_for_each_entry_safe(fileindex, check_rebase_ok,
+ &ok_arg);
+ if (err)
+ goto done;
+
+ err = get_rebase_tmp_ref_name(&tmp_branch_name, worktree);
+ if (err)
+ goto done;
+
+ err = get_newbase_symref_name(&new_base_branch_ref_name, worktree);
+ if (err)
+ goto done;
+
+ err = get_rebase_branch_symref_name(&branch_ref_name, worktree);
+ if (err)
+ goto done;
+
+ err = got_ref_open(&wt_branch, repo, worktree->head_ref_name,
+ 0);
+ if (err)
+ goto done;
+
+ err = got_ref_alloc_symref(new_base_branch_ref,
+ new_base_branch_ref_name, wt_branch);
+ if (err)
+ goto done;
+ err = got_ref_write(*new_base_branch_ref, repo);
+ if (err)
+ goto done;
+
+ /* TODO Lock original branch's ref while rebasing? */
+
+ err = got_ref_alloc_symref(&branch_ref, branch_ref_name, branch);
+ if (err)
+ goto done;
+
+ err = got_ref_write(branch_ref, repo);
+ if (err)
+ goto done;
+
+ err = got_ref_alloc(tmp_branch, tmp_branch_name,
+ worktree->base_commit_id);
+ if (err)
+ goto done;
+ err = got_ref_write(*tmp_branch, repo);
+ if (err)
+ goto done;
+
+ err = got_worktree_set_head_ref(worktree, *tmp_branch);
+ if (err)
+ goto done;
+done:
+ free(fileindex_path);
+ if (fileindex)
+ got_fileindex_free(fileindex);
+ free(tmp_branch_name);
+ free(new_base_branch_ref_name);
+ free(branch_ref_name);
+ if (branch_ref)
+ got_ref_close(branch_ref);
+ if (wt_branch)
+ got_ref_close(wt_branch);
+ if (err) {
+ if (*new_base_branch_ref) {
+ got_ref_close(*new_base_branch_ref);
+ *new_base_branch_ref = NULL;
+ }
+ if (*tmp_branch) {
+ got_ref_close(*tmp_branch);
+ *tmp_branch = NULL;
+ }
+ lock_worktree(worktree, LOCK_SH);
+ }
+ return err;
+}
+
+const struct got_error *
+got_worktree_rebase_continue(struct got_object_id **commit_id,
+ struct got_reference **new_base_branch, struct got_reference **tmp_branch,
+ struct got_reference **branch, struct got_worktree *worktree,
+ struct got_repository *repo)
+{
+ const struct got_error *err;
+ char *commit_ref_name = NULL, *new_base_branch_ref_name = NULL;
+ char *tmp_branch_name = NULL, *branch_ref_name = NULL;
+ struct got_reference *commit_ref = NULL, *branch_ref = NULL;
+
+ *commit_id = NULL;
+
+ err = get_rebase_tmp_ref_name(&tmp_branch_name, worktree);
+ if (err)
+ return err;
+
+ err = get_rebase_branch_symref_name(&branch_ref_name, worktree);
+ if (err)
+ goto done;
+
+ err = get_rebase_commit_ref_name(&commit_ref_name, worktree);
+ if (err)
+ goto done;
+
+ err = get_newbase_symref_name(&new_base_branch_ref_name, worktree);
+ if (err)
+ goto done;
+
+ err = got_ref_open(&branch_ref, repo, branch_ref_name, 0);
+ if (err)
+ goto done;
+
+ err = got_ref_open(branch, repo,
+ got_ref_get_symref_target(branch_ref), 0);
+ if (err)
+ goto done;
+
+ err = got_ref_open(&commit_ref, repo, commit_ref_name, 0);
+ if (err)
+ goto done;
+
+ err = got_ref_resolve(commit_id, repo, commit_ref);
+ if (err)
+ goto done;
+
+ err = got_ref_open(new_base_branch, repo,
+ new_base_branch_ref_name, 0);
+ if (err)
+ goto done;
+
+ err = got_ref_open(tmp_branch, repo, tmp_branch_name, 0);
+ if (err)
+ goto done;
+done:
+ free(commit_ref_name);
+ free(branch_ref_name);
+ if (commit_ref)
+ got_ref_close(commit_ref);
+ if (branch_ref)
+ got_ref_close(branch_ref);
+ if (err) {
+ free(*commit_id);
+ *commit_id = NULL;
+ if (*tmp_branch) {
+ got_ref_close(*tmp_branch);
+ *tmp_branch = NULL;
+ }
+ if (*new_base_branch) {
+ got_ref_close(*new_base_branch);
+ *new_base_branch = NULL;
+ }
+ if (*branch) {
+ got_ref_close(*branch);
+ *branch = NULL;
+ }
+ }
+ return err;
}
+
+const struct got_error *
+got_worktree_rebase_in_progress(int *in_progress, struct got_worktree *worktree)
+{
+ const struct got_error *err;
+ char *tmp_branch_name = NULL;
+
+ err = get_rebase_tmp_ref_name(&tmp_branch_name, worktree);
+ if (err)
+ return err;
+
+ *in_progress = (strcmp(tmp_branch_name, worktree->head_ref_name) == 0);
+ free(tmp_branch_name);
+ return NULL;
+}
+
+static const struct got_error *
+collect_rebase_commit_msg(struct got_pathlist_head *commitable_paths,
+ char **logmsg, void *arg)
+{
+ struct got_commit_object *commit = arg;
+
+ *logmsg = strdup(got_object_commit_get_logmsg(commit));
+ if (*logmsg == NULL)
+ return got_error_from_errno("strdup");
+
+ return NULL;
+}
+
+static const struct got_error *
+rebase_status(void *arg, unsigned char status, const char *path,
+ struct got_object_id *blob_id, struct got_object_id *commit_id)
+{
+ return NULL;
+}
+
+const struct got_error *
+got_worktree_rebase_merge_files(struct got_worktree *worktree,
+ struct got_object_id *parent_commit_id, struct got_object_id *commit_id,
+ struct got_repository *repo, got_worktree_checkout_cb progress_cb,
+ void *progress_arg, got_worktree_cancel_cb cancel_cb, void *cancel_arg)
+{
+ const struct got_error *err;
+ struct got_fileindex *fileindex;
+ char *fileindex_path, *commit_ref_name = NULL;
+ struct got_reference *commit_ref = NULL;
+
+ /* Work tree is locked/unlocked during rebase prepartion/teardown. */
+
+ err = open_fileindex(&fileindex, &fileindex_path, worktree);
+ if (err)
+ return err;
+
+ err = get_rebase_commit_ref_name(&commit_ref_name, worktree);
+ if (err)
+ goto done;
+ err = got_ref_open(&commit_ref, repo, commit_ref_name, 0);
+ if (err) {
+ if (err->code != GOT_ERR_NOT_REF)
+ goto done;
+ err = got_ref_alloc(&commit_ref, commit_ref_name, commit_id);
+ if (err)
+ goto done;
+ err = got_ref_write(commit_ref, repo);
+ if (err)
+ goto done;
+ } else {
+ struct got_object_id *stored_id;
+ int cmp;
+
+ err = got_ref_resolve(&stored_id, repo, commit_ref);
+ if (err)
+ goto done;
+ cmp = got_object_id_cmp(commit_id, stored_id);
+ free(stored_id);
+ if (cmp != 0) {
+ err = got_error(GOT_ERR_REBASE_COMMITID);
+ goto done;
+ }
+ }
+
+ err = merge_files(worktree, fileindex, fileindex_path,
+ parent_commit_id, commit_id, repo, progress_cb, progress_arg,
+ cancel_cb, cancel_arg);
+done:
+ got_fileindex_free(fileindex);
+ free(fileindex_path);
+ if (commit_ref)
+ got_ref_close(commit_ref);
+ return err;
+}
+
+const struct got_error *
+got_worktree_rebase_commit(struct got_object_id **new_commit_id,
+ struct got_worktree *worktree, struct got_reference *tmp_branch,
+ struct got_commit_object *orig_commit,
+ struct got_object_id *orig_commit_id, struct got_repository *repo)
+{
+ const struct got_error *err;
+ char *commit_ref_name = NULL;
+ struct got_reference *commit_ref = NULL;
+ struct got_object_id *commit_id = NULL;
+
+ /* Work tree is locked/unlocked during rebase prepartion/teardown. */
+
+ err = get_rebase_commit_ref_name(&commit_ref_name, worktree);
+ if (err)
+ goto done;
+ err = got_ref_open(&commit_ref, repo, commit_ref_name, 0);
+ if (err)
+ goto done;
+ err = got_ref_resolve(&commit_id, repo, commit_ref);
+ if (err)
+ goto done;
+ if (got_object_id_cmp(commit_id, orig_commit_id) != 0) {
+ err = got_error(GOT_ERR_REBASE_COMMITID);
+ goto done;
+ }
+
+ err = got_worktree_commit(new_commit_id, worktree, NULL,
+ got_object_commit_get_author(orig_commit),
+ got_object_commit_get_committer(orig_commit),
+ collect_rebase_commit_msg, orig_commit,
+ rebase_status, NULL, repo);
+ if (err)
+ goto done;
+
+ err = got_ref_delete(commit_ref, repo);
+ if (err)
+ goto done;
+
+ err = got_ref_change_ref(tmp_branch, *new_commit_id);
+done:
+ free(commit_ref_name);
+ if (commit_ref)
+ got_ref_close(commit_ref);
+ if (err) {
+ free(*new_commit_id);
+ *new_commit_id = NULL;
+ }
+ return err;
+}
+
+const struct got_error *
+got_worktree_rebase_postpone(struct got_worktree *worktree)
+{
+ return lock_worktree(worktree, LOCK_SH);
+}
+
+const struct got_error *
+got_worktree_rebase_complete(struct got_worktree *worktree,
+ struct got_reference *new_base_branch, struct got_reference *tmp_branch,
+ struct got_reference *rebased_branch,
+ struct got_repository *repo)
+{
+ const struct got_error *err, *unlockerr;
+ struct got_object_id *new_head_commit_id = NULL;
+
+ err = got_ref_resolve(&new_head_commit_id, repo, tmp_branch);
+ if (err)
+ return err;
+
+ err = got_ref_change_ref(rebased_branch, new_head_commit_id);
+ if (err)
+ goto done;
+
+ err = got_ref_write(rebased_branch, repo);
+ if (err)
+ goto done;
+
+ err = got_worktree_set_head_ref(worktree, rebased_branch);
+ if (err)
+ goto done;
+
+ err = got_ref_delete(tmp_branch, repo);
+ if (err)
+ goto done;
+
+ err = got_ref_delete(new_base_branch, repo);
+ if (err)
+ goto done;
+done:
+ free(new_head_commit_id);
+ unlockerr = lock_worktree(worktree, LOCK_SH);
+ if (unlockerr && err == NULL)
+ err = unlockerr;
+ return err;
+}
+
+struct collect_revertible_paths_arg {
+ struct got_pathlist_head *revertible_paths;
+ struct got_worktree *worktree;
+};
+
+static const struct got_error *
+collect_revertible_paths(void *arg, unsigned char status, const char *relpath,
+ struct got_object_id *blob_id, struct got_object_id *commit_id)
+{
+ struct collect_revertible_paths_arg *a = arg;
+ const struct got_error *err = NULL;
+ struct got_pathlist_entry *new = NULL;
+ char *path = NULL;
+
+ if (status != GOT_STATUS_ADD &&
+ status != GOT_STATUS_DELETE &&
+ status != GOT_STATUS_MODIFY &&
+ status != GOT_STATUS_CONFLICT &&
+ status != GOT_STATUS_MISSING)
+ return NULL;
+
+ if (asprintf(&path, "%s/%s", a->worktree->root_path, relpath) == -1)
+ return got_error_from_errno("asprintf");
+
+ err = got_pathlist_insert(&new, a->revertible_paths, path, NULL);
+ if (err || new == NULL)
+ free(path);
+ return err;
+}
+
+const struct got_error *
+got_worktree_rebase_abort(struct got_worktree *worktree,
+ struct got_repository *repo, struct got_reference *new_base_branch,
+ got_worktree_checkout_cb progress_cb, void *progress_arg)
+{
+ const struct got_error *err, *unlockerr;
+ struct got_reference *resolved = NULL;
+ struct got_object_id *commit_id = NULL;
+ struct got_fileindex *fileindex = NULL;
+ char *fileindex_path = NULL;
+ struct got_pathlist_head revertible_paths;
+ struct got_pathlist_entry *pe;
+ struct collect_revertible_paths_arg crp_arg;
+
+ TAILQ_INIT(&revertible_paths);
+
+ err = lock_worktree(worktree, LOCK_EX);
+ if (err)
+ return err;
+
+ err = open_fileindex(&fileindex, &fileindex_path, worktree);
+ if (err)
+ goto done;
+
+ err = got_ref_open(&resolved, repo,
+ got_ref_get_symref_target(new_base_branch), 0);
+ if (err)
+ goto done;
+
+ err = got_worktree_set_head_ref(worktree, resolved);
+ if (err)
+ goto done;
+
+ /*
+ * XXX commits to the base branch could have happened while
+ * we were busy rebasing; should we store the original commit ID
+ * when rebase begins and read it back here?
+ */
+ err = got_ref_resolve(&commit_id, repo, resolved);
+ if (err)
+ goto done;
+
+ err = got_worktree_set_base_commit_id(worktree, repo, commit_id);
+ if (err)
+ goto done;
+
+ err = got_worktree_checkout_files(worktree, "", repo, progress_cb,
+ progress_arg, NULL, NULL);
+ if (err)
+ goto done;
+
+ crp_arg.revertible_paths = &revertible_paths;
+ crp_arg.worktree = worktree;
+ err = got_worktree_status(worktree, "", repo,
+ collect_revertible_paths, &crp_arg, NULL, NULL);
+ if (err)
+ goto done;
+
+ TAILQ_FOREACH(pe, &revertible_paths, entry) {
+ err = revert_file(worktree, fileindex, pe->path,
+ progress_cb, progress_arg, repo);
+ if (err)
+ break;
+ }
+done:
+ got_ref_close(resolved);
+ free(commit_id);
+ got_fileindex_free(fileindex);
+ free(fileindex_path);
+ TAILQ_FOREACH(pe, &revertible_paths, entry)
+ free((char *)pe->path);
+ got_pathlist_free(&revertible_paths);
+
+ unlockerr = lock_worktree(worktree, LOCK_SH);
+ if (unlockerr && err == NULL)
+ err = unlockerr;
+ return err;
+}
blob - 55653a3b4b9d82d357392bf627327bc0efc63549
blob + cc53c4052b7de555d29b4d516d502908cf15873f
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
-REGRESS_TARGETS=checkout update status log add rm diff commit cherrypick backout
+REGRESS_TARGETS=checkout update status log add rm diff commit \
+ cherrypick backout rebase
NOOBJ=Yes
checkout:
backout:
./backout.sh
+rebase:
+ ./rebase.sh
+
.include <bsd.regress.mk>
blob - 4135c2b665cb9eaecfe1d9ed4d76cd51551b0d01
blob + 05565fef26b7848ff5d3ce99c04bb7ddf94780e2
--- regress/cmdline/common.sh
+++ regress/cmdline/common.sh
(cd $repo && git show --no-patch --pretty='format:%H')
}
+function git_show_parent_commit
+{
+ local repo="$1"
+ (cd $repo && git show --no-patch --pretty='format:%P')
+}
+
function git_show_tree
{
local repo="$1"
(cd $repo && git show --no-patch --pretty='format:%T')
}
+function trim_obj_id
+{
+ let trimcount=$1
+ id=$2
+
+ pat=""
+ while [ trimcount -gt 0 ]; do
+ pat="[0-9a-f]$pat"
+ let trimcount--
+ done
+
+ echo ${id%$pat}
+}
+
function git_commit_tree
{
local repo="$1"
blob - /dev/null
blob + ca79164bd6cb0593ca6f129a8ab8026fb8532b83 (mode 755)
--- /dev/null
+++ regress/cmdline/rebase.sh
+#!/bin/sh
+#
+# Copyright (c) 2019 Stefan Sperling <stsp@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ./common.sh
+
+function test_rebase_basic {
+ local testroot=`test_init rebase_basic`
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified delta on branch" > $testroot/repo/gamma/delta
+ git_commit $testroot/repo -m "committing to delta on newbranch"
+
+ echo "modified alpha on branch" > $testroot/repo/alpha
+ (cd $testroot/repo && git rm -q beta)
+ echo "new file on branch" > $testroot/repo/epsilon/new
+ (cd $testroot/repo && git add epsilon/new)
+ git_commit $testroot/repo -m "committing more changes on newbranch"
+
+ local orig_commit1=`git_show_parent_commit $testroot/repo`
+ local orig_commit2=`git_show_head $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q master)
+ echo "modified zeta on master" > $testroot/repo/epsilon/zeta
+ git_commit $testroot/repo -m "committing to zeta on master"
+ local master_commit=`git_show_head $testroot/repo`
+
+ got checkout $testroot/repo $testroot/wt > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got rebase newbranch > $testroot/stdout)
+
+ (cd $testroot/repo && git checkout -q newbranch)
+ local new_commit1=`git_show_parent_commit $testroot/repo`
+ local new_commit2=`git_show_head $testroot/repo`
+
+ local short_orig_commit1=`trim_obj_id 28 $orig_commit1`
+ local short_orig_commit2=`trim_obj_id 28 $orig_commit2`
+ local short_new_commit1=`trim_obj_id 28 $new_commit1`
+ local short_new_commit2=`trim_obj_id 28 $new_commit2`
+
+ echo "G gamma/delta" >> $testroot/stdout.expected
+ echo -n "$short_orig_commit1 -> $short_new_commit1" \
+ >> $testroot/stdout.expected
+ echo ": committing to delta on newbranch" >> $testroot/stdout.expected
+ echo "G alpha" >> $testroot/stdout.expected
+ echo "D beta" >> $testroot/stdout.expected
+ echo "A epsilon/new" >> $testroot/stdout.expected
+ echo -n "$short_orig_commit2 -> $short_new_commit2" \
+ >> $testroot/stdout.expected
+ echo ": committing more changes on newbranch" \
+ >> $testroot/stdout.expected
+ echo "Switching work tree to refs/heads/newbranch" \
+ >> $testroot/stdout.expected
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "modified delta on branch" > $testroot/content.expected
+ cat $testroot/wt/gamma/delta > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "modified alpha on branch" > $testroot/content.expected
+ cat $testroot/wt/alpha > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ if [ -e $testroot/wt/beta ]; then
+ echo "removed file beta still exists on disk" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "new file on branch" > $testroot/content.expected
+ cat $testroot/wt/epsilon/new > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo -n > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got log -l3 | grep ^commit > $testroot/stdout)
+ echo "commit $new_commit2 (newbranch)" > $testroot/stdout.expected
+ echo "commit $new_commit1" >> $testroot/stdout.expected
+ echo "commit $master_commit (master)" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ fi
+ test_done "$testroot" "$ret"
+}
+
+function test_rebase_ancestry_check {
+ local testroot=`test_init rebase_ancestry_check`
+
+ got checkout $testroot/repo $testroot/wt > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified delta on branch" > $testroot/repo/gamma/delta
+ git_commit $testroot/repo -m "committing to delta on newbranch"
+
+ (cd $testroot/wt && got rebase newbranch > $testroot/stdout \
+ 2> $testroot/stderr)
+
+ echo -n > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo -n "got: specified branch resolves to a commit " \
+ > $testroot/stderr.expected
+ echo "which is already contained in work tree's branch" \
+ >> $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ fi
+ test_done "$testroot" "$ret"
+}
+
+function test_rebase_continue {
+ local testroot=`test_init rebase_continue`
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified alpha on branch" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on newbranch"
+ local orig_commit1=`git_show_head $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q master)
+ echo "modified alpha on master" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on master"
+ local master_commit=`git_show_head $testroot/repo`
+
+ got checkout $testroot/repo $testroot/wt > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got rebase newbranch > $testroot/stdout \
+ 2> $testroot/stderr)
+
+ echo "C alpha" > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "got: conflicts must be resolved before rebase can be resumed" \
+ > $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "<<<<<<< commit $orig_commit1" > $testroot/content.expected
+ echo "modified alpha on branch" >> $testroot/content.expected
+ echo "=======" >> $testroot/content.expected
+ echo "modified alpha on master" >> $testroot/content.expected
+ echo '>>>>>>> alpha' >> $testroot/content.expected
+ cat $testroot/wt/alpha > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo "C alpha" > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ # resolve the conflict
+ echo "modified alpha on branch and master" > $testroot/wt/alpha
+
+ (cd $testroot/wt && got rebase -c > $testroot/stdout)
+
+ (cd $testroot/repo && git checkout -q newbranch)
+ local new_commit1=`git_show_head $testroot/repo`
+
+ local short_orig_commit1=`trim_obj_id 28 $orig_commit1`
+ local short_new_commit1=`trim_obj_id 28 $new_commit1`
+
+ echo -n "$short_orig_commit1 -> $short_new_commit1" \
+ > $testroot/stdout.expected
+ echo ": committing to alpha on newbranch" >> $testroot/stdout.expected
+ echo "Switching work tree to refs/heads/newbranch" \
+ >> $testroot/stdout.expected
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+
+ (cd $testroot/wt && got log -l2 | grep ^commit > $testroot/stdout)
+ echo "commit $new_commit1 (newbranch)" > $testroot/stdout.expected
+ echo "commit $master_commit (master)" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ fi
+ test_done "$testroot" "$ret"
+}
+
+function test_rebase_abort {
+ local testroot=`test_init rebase_abort`
+
+ local init_commit=`git_show_head $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q -b newbranch)
+ echo "modified alpha on branch" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on newbranch"
+ local orig_commit1=`git_show_head $testroot/repo`
+
+ (cd $testroot/repo && git checkout -q master)
+ echo "modified alpha on master" > $testroot/repo/alpha
+ git_commit $testroot/repo -m "committing to alpha on master"
+ local master_commit=`git_show_head $testroot/repo`
+
+ got checkout $testroot/repo $testroot/wt > /dev/null
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got rebase newbranch > $testroot/stdout \
+ 2> $testroot/stderr)
+
+ echo "C alpha" > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "got: conflicts must be resolved before rebase can be resumed" \
+ > $testroot/stderr.expected
+ cmp -s $testroot/stderr.expected $testroot/stderr
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "<<<<<<< commit $orig_commit1" > $testroot/content.expected
+ echo "modified alpha on branch" >> $testroot/content.expected
+ echo "=======" >> $testroot/content.expected
+ echo "modified alpha on master" >> $testroot/content.expected
+ echo '>>>>>>> alpha' >> $testroot/content.expected
+ cat $testroot/wt/alpha > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got status > $testroot/stdout)
+
+ echo "C alpha" > $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got rebase -a > $testroot/stdout)
+
+ (cd $testroot/repo && git checkout -q newbranch)
+
+ echo "Switching work tree to refs/heads/master" \
+ > $testroot/stdout.expected
+ echo 'R alpha' >> $testroot/stdout.expected
+ echo "Rebase of refs/heads/newbranch aborted" \
+ >> $testroot/stdout.expected
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ echo "modified alpha on master" > $testroot/content.expected
+ cat $testroot/wt/alpha > $testroot/content
+ cmp -s $testroot/content.expected $testroot/content
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/content.expected $testroot/content
+ test_done "$testroot" "$ret"
+ return 1
+ fi
+
+ (cd $testroot/wt && got log -l3 -c newbranch \
+ | grep ^commit > $testroot/stdout)
+ echo "commit $orig_commit1 (newbranch)" > $testroot/stdout.expected
+ echo "commit $init_commit" >> $testroot/stdout.expected
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret="$?"
+ if [ "$ret" != "0" ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ fi
+ test_done "$testroot" "$ret"
+}
+
+run_test test_rebase_basic
+run_test test_rebase_ancestry_check
+run_test test_rebase_continue
+run_test test_rebase_abort