"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
Re: got reintegrate
To:
gameoftrees@openbsd.org
Date:
Tue, 15 Oct 2019 10:19:06 +0200

Download raw body.

Thread
On Tue, Oct 15, 2019 at 10:14:33AM +0200, Stefan Sperling wrote:
> On Sat, Oct 12, 2019 at 05:02:01PM +0200, Stefan Sperling wrote:
> > This diff adds a 'reintegrate' command which performs steps 2 and 3.
> > It must be run in a work tree and will update files after running a few
> > safety checks. The command name is inspired by Subversion which has
> > an "svn merge --reintegrate" command.
> 
> New diff, still using the same command name.
> 
> But I realized the path-prefix restriction was pointless because file
> changes brought in by 'reintegrate' are guaranteed to be free of merge
> conflicts.

I forgot to remove a now unused error code in the previous diff.

diff d136cfcb987bd2fd865f8711449dc47b7f63455f /home/stsp/src/got
blob - c34b1bcb69fd3e41f6c82be87c06d624f89bf175
file + got/got.1
--- got/got.1
+++ got/got.1
@@ -1042,6 +1042,57 @@ If this option is used, no other command-line argument
 .It Cm he
 Short alias for
 .Cm histedit .
+.It Cm reintegrate Ar branch
+Reintegrate the specified
+.Ar branch
+into the work tree's current branch.
+Files in the work tree are updated to match the contents on the reintegrated
+.Ar branch ,
+and the reference of the work tree's branch is changed to point at the
+head commit of the reintegrated
+.Ar branch .
+.Pp
+Both branches can be considered equivalent after reintegration since they
+will be pointing at the same commit.
+Both branches remain available for future work, if desired.
+In case the reintegrated
+.Ar branch
+is no longer needed it may be deleted with
+.Cm got branch -d .
+.Pp
+Show the status of each affected file, using the following status codes:
+.Bl -column YXZ description
+.It U Ta file was updated
+.It D Ta file was deleted
+.It A Ta new file was added
+.It \(a~ Ta versioned file is obstructed by a non-regular file
+.It ! Ta a missing versioned file was restored
+.El
+.Pp
+.Cm got reintegrate
+will refuse to run if certain preconditions are not met.
+Most importantly, the
+.Ar branch
+must have been rebased onto the work tree's current branch with
+.Cm got rebase
+before it can be reintegrated.
+If the work tree contains multiple base commits it must first be updated
+to a single base commit with
+.Cm got update .
+If changes have been staged with
+.Cm got stage ,
+these changes must first be committed with
+.Cm got commit
+or unstaged with
+.Cm got unstage .
+If the work tree contains local changes, these changes must first be
+committed with
+.Cm got commit
+or reverted with
+.Cm got revert .
+.It Cm ri
+Short alias for
+.Cm reintegrate .
 .It Cm stage Oo Fl l Oc Oo Fl p Oc Oo Fl F Ar response-script Oc Op Ar path ...
 Stage local changes for inclusion in the next commit.
 If no
blob - 206285202f7c507b7a0760eda2c197cf1053c16e
file + got/got.c
--- got/got.c
+++ got/got.c
@@ -97,6 +97,7 @@ __dead static void	usage_cherrypick(void);
 __dead static void	usage_backout(void);
 __dead static void	usage_rebase(void);
 __dead static void	usage_histedit(void);
+__dead static void	usage_reintegrate(void);
 __dead static void	usage_stage(void);
 __dead static void	usage_unstage(void);
 __dead static void	usage_cat(void);
@@ -121,6 +122,7 @@ static const struct got_error*		cmd_cherrypick(int, ch
 static const struct got_error*		cmd_backout(int, char *[]);
 static const struct got_error*		cmd_rebase(int, char *[]);
 static const struct got_error*		cmd_histedit(int, char *[]);
+static const struct got_error*		cmd_reintegrate(int, char *[]);
 static const struct got_error*		cmd_stage(int, char *[]);
 static const struct got_error*		cmd_unstage(int, char *[]);
 static const struct got_error*		cmd_cat(int, char *[]);
@@ -146,6 +148,7 @@ static struct got_cmd got_commands[] = {
 	{ "backout",	cmd_backout,	usage_backout,	"bo" },
 	{ "rebase",	cmd_rebase,	usage_rebase,	"rb" },
 	{ "histedit",	cmd_histedit,	usage_histedit,	"he" },
+	{ "reintegrate",cmd_reintegrate,usage_reintegrate,"ri" },
 	{ "stage",	cmd_stage,	usage_stage,	"sg" },
 	{ "unstage",	cmd_unstage,	usage_unstage,	"ug" },
 	{ "cat",	cmd_cat,	usage_cat,	"" },
@@ -6454,6 +6457,139 @@ done:
 		got_worktree_close(worktree);
 	if (repo)
 		got_repo_close(repo);
+	return error;
+}
+
+__dead static void
+usage_reintegrate(void)
+{
+	fprintf(stderr, "usage: %s reintegrate branch\n", getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+cmd_reintegrate(int argc, char *argv[])
+{
+	const struct got_error *error = NULL;
+	struct got_repository *repo = NULL;
+	struct got_worktree *worktree = NULL;
+	char *cwd = NULL, *refname = NULL, *base_refname = NULL;
+	const char *branch_arg = NULL;
+	struct got_reference *branch_ref = NULL, *base_branch_ref = NULL;
+	struct got_fileindex *fileindex = NULL;
+	struct got_object_id *commit_id = NULL, *base_commit_id = NULL;
+	int ch, did_something = 0;
+
+	while ((ch = getopt(argc, argv, "")) != -1) {
+		switch (ch) {
+		default:
+			usage_reintegrate();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 1)
+		usage_reintegrate();
+	branch_arg = argv[0];
+
+	if (pledge("stdio rpath wpath cpath fattr flock proc exec sendfd "
+	    "unveil", NULL) == -1)
+		err(1, "pledge");
+
+	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 = check_rebase_or_histedit_in_progress(worktree);
+	if (error)
+		goto done;
+
+	error = got_repo_open(&repo, got_worktree_get_repo_path(worktree),
+	    NULL);
+	if (error != NULL)
+		goto done;
+
+	error = apply_unveil(got_repo_get_path(repo), 0,
+	    got_worktree_get_root_path(worktree));
+	if (error)
+		goto done;
+
+	if (asprintf(&refname, "refs/heads/%s", branch_arg) == -1) {
+		error = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	error = got_worktree_reintegrate_prepare(&fileindex, &branch_ref,
+	    &base_branch_ref, worktree, refname, repo);
+	if (error)
+		goto done;
+
+	refname = strdup(got_ref_get_name(branch_ref));
+	if (refname == NULL) {
+		error = got_error_from_errno("strdup");
+		got_worktree_reintegrate_abort(worktree, fileindex, repo,
+		    branch_ref, base_branch_ref);
+		goto done;
+	}
+	base_refname = strdup(got_ref_get_name(base_branch_ref));
+	if (base_refname == NULL) {
+		error = got_error_from_errno("strdup");
+		got_worktree_reintegrate_abort(worktree, fileindex, repo,
+		    branch_ref, base_branch_ref);
+		goto done;
+	}
+
+	error = got_ref_resolve(&commit_id, repo, branch_ref);
+	if (error)
+		goto done;
+
+	error = got_ref_resolve(&base_commit_id, repo, base_branch_ref);
+	if (error)
+		goto done;
+
+	if (got_object_id_cmp(commit_id, base_commit_id) == 0) {
+		error = got_error_msg(GOT_ERR_SAME_BRANCH,
+		    "specified branch has already been reintegrated");
+		got_worktree_reintegrate_abort(worktree, fileindex, repo,
+		    branch_ref, base_branch_ref);
+		goto done;
+	}
+
+	error = check_linear_ancestry(commit_id, base_commit_id, repo);
+	if (error) {
+		if (error->code == GOT_ERR_ANCESTRY)
+			error = got_error(GOT_ERR_REBASE_REQUIRED);
+		got_worktree_reintegrate_abort(worktree, fileindex, repo,
+		    branch_ref, base_branch_ref);
+		goto done;
+	}
+
+	error = got_worktree_reintegrate_continue(worktree, fileindex, repo,
+	    branch_ref, base_branch_ref, update_progress, &did_something,
+	    check_cancelled, NULL);
+	if (error)
+		goto done;
+
+	printf("Reintegrated %s into %s\n", refname, base_refname);
+done:
+	if (repo)
+		got_repo_close(repo);
+	if (worktree)
+		got_worktree_close(worktree);
+	free(cwd);
+	free(base_commit_id);
+	free(commit_id);
+	free(refname);
+	free(base_refname);
 	return error;
 }
 
blob - f8e9822b47d297dd837ffbfe9f018be2143c3b44
file + include/got_error.h
--- include/got_error.h
+++ include/got_error.h
@@ -124,6 +124,7 @@
 #define GOT_ERR_COMMIT_NO_EMAIL	108
 #define GOT_ERR_TAG_EXISTS	109
 #define GOT_ERR_GIT_REPO_FORMAT	110
+#define GOT_ERR_REBASE_REQUIRED	111
 
 static const struct got_error {
 	int code;
@@ -254,6 +255,7 @@ static const struct got_error {
 	    "with Git" },
 	{ GOT_ERR_TAG_EXISTS,"specified tag already exists" },
 	{ GOT_ERR_GIT_REPO_FORMAT,"unknown git repository format version" },
+	{ GOT_ERR_REBASE_REQUIRED,"specified branch must be rebased first" },
 };
 
 /*
blob - 096c4e4ceac795c28e6da4fba2b9be6fd783015b
file + include/got_worktree.h
--- include/got_worktree.h
+++ include/got_worktree.h
@@ -382,6 +382,29 @@ const struct got_error *got_worktree_histedit_abort(st
 const struct got_error *got_worktree_get_histedit_script_path(char **,
     struct got_worktree *);
 
+/*
+ * Prepare a work tree for a reintegrate operation.
+ * Return pointers to a fileindex and locked references which must be
+ * passed back to other reintegrate-related functions.
+ */
+const struct got_error *
+got_worktree_reintegrate_prepare(struct got_fileindex **,
+   struct got_reference **, struct got_reference **,
+    struct got_worktree *, const char *, struct got_repository *);
+
+/*
+ * Carry out a prepared reintegrate operation.
+ * Report affected files via the specified progress callback.
+ */
+const struct got_error *got_worktree_reintegrate_continue(
+    struct got_worktree *, struct got_fileindex *, struct got_repository *,
+    struct got_reference *, struct got_reference *,
+    got_worktree_checkout_cb, void *, got_cancel_cb, void *);
+
+/* Abort a prepared reintegrate operation. */
+const struct got_error *got_worktree_reintegrate_abort(struct got_worktree *,
+    struct got_fileindex *, struct got_repository *,
+    struct got_reference *, struct got_reference *);
 
 /*
  * Stage the specified paths for commit.
blob - d7dc1224936d7d6588be61fac9a25737e3bc8965
file + lib/worktree.c
--- lib/worktree.c
+++ lib/worktree.c
@@ -5613,6 +5613,143 @@ done:
 	return err;
 }
 
+const struct got_error *
+got_worktree_reintegrate_prepare(struct got_fileindex **fileindex,
+    struct got_reference **branch_ref, struct got_reference **base_branch_ref,
+    struct got_worktree *worktree, const char *refname,
+    struct got_repository *repo)
+{
+	const struct got_error *err = NULL;
+	char *fileindex_path = NULL;
+	struct check_rebase_ok_arg ok_arg;
+
+	*fileindex = NULL;
+	*branch_ref = NULL;
+	*base_branch_ref = NULL;
+
+	err = lock_worktree(worktree, LOCK_EX);
+	if (err)
+		return err;
+
+	if (strcmp(refname, got_worktree_get_head_ref_name(worktree)) == 0) {
+		err = got_error_msg(GOT_ERR_SAME_BRANCH,
+		    "cannot reintegrate a branch into itself; "
+		    "update -b or different branch name required");
+		goto done;
+	}
+
+	err = open_fileindex(fileindex, &fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	/* Preconditions are the same as for rebase. */
+	ok_arg.worktree = worktree;
+	ok_arg.repo = repo;
+	err = got_fileindex_for_each_entry_safe(*fileindex, check_rebase_ok,
+	    &ok_arg);
+	if (err)
+		goto done;
+
+	err = got_ref_open(branch_ref, repo, refname, 1);
+	if (err)
+		goto done;
+
+	err = got_ref_open(base_branch_ref, repo,
+	    got_worktree_get_head_ref_name(worktree), 1);
+done:
+	if (err) {
+		if (*branch_ref) {
+			got_ref_close(*branch_ref);
+			*branch_ref = NULL;
+		}
+		if (*base_branch_ref) {
+			got_ref_close(*base_branch_ref);
+			*base_branch_ref = NULL;
+		}
+		if (*fileindex) {
+			got_fileindex_free(*fileindex);
+			*fileindex = NULL;
+		}
+		lock_worktree(worktree, LOCK_SH);
+	}
+	return err;
+}
+
+const struct got_error *
+got_worktree_reintegrate_continue(struct got_worktree *worktree,
+    struct got_fileindex *fileindex, struct got_repository *repo,
+    struct got_reference *branch_ref, struct got_reference *base_branch_ref,
+    got_worktree_checkout_cb progress_cb, void *progress_arg,
+    got_cancel_cb cancel_cb, void *cancel_arg)
+{
+	const struct got_error *err = NULL, *sync_err, *unlockerr;
+	char *fileindex_path = NULL;
+	struct got_object_id *tree_id = NULL, *commit_id = NULL;
+
+	err = get_fileindex_path(&fileindex_path, worktree);
+	if (err)
+		goto done;
+
+	err = got_ref_resolve(&commit_id, repo, branch_ref);
+	if (err)
+		goto done;
+
+	err = got_object_id_by_path(&tree_id, repo, commit_id,
+	    worktree->path_prefix);
+	if (err)
+		goto done;
+
+	err = got_worktree_set_base_commit_id(worktree, repo, commit_id);
+	if (err)
+		goto done;
+
+	err = checkout_files(worktree, fileindex, "", tree_id, NULL, repo,
+	    progress_cb, progress_arg, cancel_cb, cancel_arg);
+	if (err)
+		goto sync;
+
+	err = got_ref_change_ref(base_branch_ref, commit_id);
+	if (err)
+		goto sync;
+
+	err = got_ref_write(base_branch_ref, repo);
+sync:
+	sync_err = sync_fileindex(fileindex, fileindex_path);
+	if (sync_err && err == NULL)
+		err = sync_err;
+
+done:
+	unlockerr = got_ref_unlock(branch_ref);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	got_ref_close(branch_ref);
+
+	unlockerr = got_ref_unlock(base_branch_ref);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	got_ref_close(base_branch_ref);
+
+	got_fileindex_free(fileindex);
+	free(fileindex_path);
+	free(tree_id);
+
+	unlockerr = lock_worktree(worktree, LOCK_SH);
+	if (unlockerr && err == NULL)
+		err = unlockerr;
+	return err;
+}
+
+const struct got_error *
+got_worktree_reintegrate_abort(struct got_worktree *worktree,
+    struct got_fileindex *fileindex, struct got_repository *repo,
+    struct got_reference *branch_ref, struct got_reference *base_branch_ref)
+{
+	got_ref_close(branch_ref);
+	got_ref_close(base_branch_ref);
+	got_fileindex_free(fileindex);
+	return lock_worktree(worktree, LOCK_SH);
+}
+
 struct check_stage_ok_arg {
 	struct got_object_id *head_commit_id;
 	struct got_worktree *worktree;
blob - 535e0b8450f2e076c694aaae464ec319797b783f
file + regress/cmdline/Makefile
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
@@ -1,6 +1,6 @@
 REGRESS_TARGETS=checkout update status log add rm diff blame branch tag \
-	ref commit revert cherrypick backout rebase import histedit stage \
-	unstage cat
+	ref commit revert cherrypick backout rebase import histedit \
+	reintegrate stage unstage cat
 NOOBJ=Yes
 
 checkout:
@@ -56,6 +56,9 @@ import:
 
 histedit:
 	./histedit.sh
+
+reintegrate:
+	./reintegrate.sh
 
 stage:
 	./stage.sh
blob - /dev/null
file + regress/cmdline/reintegrate.sh
--- regress/cmdline/reintegrate.sh
+++ regress/cmdline/reintegrate.sh
@@ -0,0 +1,297 @@
+#!/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_reintegrate_basic {
+	local testroot=`test_init reintegrate_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 > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got rebase failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && git checkout -q newbranch)
+	local new_commit1=`git_show_parent_commit $testroot/repo`
+	local new_commit2=`git_show_head $testroot/repo`
+
+	(cd $testroot/wt && got update -b master > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got update failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got reintegrate newbranch > $testroot/stdout)
+
+	echo "U  alpha" > $testroot/stdout.expected
+	echo "D  beta" >> $testroot/stdout.expected
+	echo "A  epsilon/new" >> $testroot/stdout.expected
+	echo "U  gamma/delta" >> $testroot/stdout.expected
+	echo "Reintegrated refs/heads/newbranch into refs/heads/master" \
+		>> $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 (master, newbranch)" \
+		> $testroot/stdout.expected
+	echo "commit $new_commit1" >> $testroot/stdout.expected
+	echo "commit $master_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"
+}
+
+function test_reintegrate_requires_rebase_first {
+	local testroot=`test_init reintegrate_requires_rebase_first`
+	local init_commit=`git_show_head $testroot/repo`
+
+	(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`
+
+	(cd $testroot/repo && git checkout -q newbranch)
+	local new_commit1=`git_show_parent_commit $testroot/repo`
+	local new_commit2=`git_show_head $testroot/repo`
+
+	got checkout -b master $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got reintegrate newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got reintegrate succeeded unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	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 "got: specified branch must be rebased first" \
+		> $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
+
+	(cd $testroot/repo && got log -c master | \
+		grep ^commit > $testroot/stdout)
+	echo "commit $master_commit (master)" > $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
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && got log -c newbranch | \
+		grep ^commit > $testroot/stdout)
+	echo "commit $new_commit2 (newbranch)" \
+		> $testroot/stdout.expected
+	echo "commit $new_commit1" >> $testroot/stdout.expected
+	echo "commit $init_commit" >> $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	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_reintegrate_path_prefix {
+	local testroot=`test_init reintegrate_path_prefix`
+
+	(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 > /dev/null)
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got rebase failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/repo && git checkout -q newbranch)
+	local new_commit1=`git_show_parent_commit $testroot/repo`
+	local new_commit2=`git_show_head $testroot/repo`
+
+	rm -r $testroot/wt
+	got checkout -b master -p epsilon $testroot/repo $testroot/wt \
+		> /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		echo "got checkout failed unexpectedly"
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd $testroot/wt && got reintegrate newbranch > $testroot/stdout)
+
+	echo "A  new" > $testroot/stdout.expected
+	echo "Reintegrated refs/heads/newbranch into refs/heads/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"
+}
+
+
+run_test test_reintegrate_basic
+run_test test_reintegrate_requires_rebase_first
+run_test test_reintegrate_path_prefix