Commit Diff


commit - 2b496619daecc1f25b1bc0c53e01685030dc2c74
commit + 818c750100809b9b2d2c638d39f1427a66929fce
blob - 892d74adc7e2c036bf693521b13f8399543ee673
blob + 0bcca9eaa8ffbc8bd3c160351ddde972552e16ff
--- got/got.1
+++ got/got.1
@@ -529,7 +529,73 @@ conflicts must be resolved first.
 .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
@@ -619,20 +685,9 @@ Rebase the
 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
@@ -25,6 +25,7 @@
 #include <err.h>
 #include <errno.h>
 #include <locale.h>
+#include <ctype.h>
 #include <signal.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -90,6 +91,7 @@ __dead static void	usage_revert(void);
 __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 *[]);
@@ -107,6 +109,7 @@ static const struct got_error*		cmd_revert(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,	"" },
@@ -125,6 +128,7 @@ static struct got_cmd got_commands[] = {
 	{ "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
@@ -3176,6 +3180,364 @@ done:
 	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
@@ -97,6 +97,10 @@
 #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;
@@ -189,6 +193,11 @@ static const struct got_error {
 	{ 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
@@ -16,6 +16,7 @@
 
 struct got_worktree;
 struct got_commitable;
+struct got_commit_object;
 
 /* status codes */
 #define GOT_STATUS_NO_CHANGE	' '
@@ -208,3 +209,66 @@ const char *got_commitable_get_path(struct got_commita
 
 /* 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
@@ -473,7 +473,8 @@ fetch_commits_from_open_branches(int *nfetched,
 	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)
@@ -517,7 +518,7 @@ fetch_commits_from_open_branches(int *nfetched,
 			    commit, repo);
 		if (err)
 			break;
-		if (changed && *changed_id == NULL)
+		if (changed && changed_id && *changed_id == NULL)
 			*changed_id = commit_id;
 	}
 done:
@@ -578,8 +579,16 @@ got_commit_graph_iter_start(struct got_commit_graph *g
 	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
@@ -65,3 +65,15 @@ struct got_commitable {
 
 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
@@ -725,6 +725,7 @@ static const struct got_error *
 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)
 {
@@ -734,7 +735,7 @@ merge_blob(int *local_changes_subsumed, struct got_wor
 	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;
 
@@ -789,16 +790,16 @@ merge_blob(int *local_changes_subsumed, struct got_wor
 		 */
 	}
 
-	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;
 
@@ -848,7 +849,7 @@ done:
 		free(blob_orig_path);
 	}
 	free(id_str);
-	free(label1);
+	free(label_deriv);
 	return err;
 }
 
@@ -1183,7 +1184,8 @@ update_blob(struct got_worktree *worktree,
 				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);
@@ -1348,8 +1350,8 @@ diff_new(void *arg, struct got_tree_entry *te, const c
 	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;
@@ -1361,7 +1363,7 @@ got_worktree_get_base_ref_name(char **refname, struct 
 	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;
@@ -1370,6 +1372,40 @@ got_worktree_get_base_ref_name(char **refname, struct 
 	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.
@@ -1660,6 +1696,7 @@ struct merge_file_cb_arg {
     void *progress_arg;
     got_worktree_cancel_cb cancel_cb;
     void *cancel_arg;
+    struct got_object_id *commit_id2;
 };
 
 static const struct got_error *
@@ -1706,7 +1743,7 @@ merge_file_cb(void *arg, struct got_blob_object *blob1
 		}
 
 		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);
@@ -1772,7 +1809,8 @@ merge_file_cb(void *arg, struct got_blob_object *blob1
 				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,
@@ -1839,35 +1877,18 @@ check_merge_ok(void *arg, struct got_fileindex_entry *
 	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);
@@ -1894,17 +1915,51 @@ got_worktree_merge_files(struct got_worktree *worktree
 	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;
@@ -3399,4 +3454,525 @@ unsigned int
 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
@@ -1,4 +1,5 @@
-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:
@@ -31,4 +32,7 @@ cherrypick:
 backout:
 	./backout.sh
 
+rebase:
+	./rebase.sh
+
 .include <bsd.regress.mk>
blob - 4135c2b665cb9eaecfe1d9ed4d76cd51551b0d01
blob + 05565fef26b7848ff5d3ce99c04bb7ddf94780e2
--- regress/cmdline/common.sh
+++ regress/cmdline/common.sh
@@ -44,12 +44,32 @@ function git_show_head
 	(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
@@ -0,0 +1,393 @@
+#!/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