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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
got reintegrate
To:
gameoftrees@openbsd.org
Date:
Sat, 12 Oct 2019 17:02:01 +0200

Download raw body.

Thread
We don't yet have a command to fold a branch back into its parent branch.
Technically, the current command set is already able to achieve this,
but it's not straightforward and lacks safety checks.

With got 0.18, this would work as follows.

Given the following graph of commits:

       +-- E -- F -- G (mybranch)
      /
 A --+-- B -- C -- D (master)

Step 1:
Rebase mybranch with 'got rebase' to arrive at a linear history,
rewriting commits with IDs E, F, G with new IDs H, I, J:

 A -- B -- C -- D (master) -- H -- I -- J (mybranch)

Step 2:
Use 'got ref' to change the master reference to point at commit G:

 A -- B -- C -- D -- H -- I -- J (master, mybranch)

Step3:
Commit J now contains all changes from both branches, so we can update
or checkout a work tree from master and get the merged state.

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.


diff d136cfcb987bd2fd865f8711449dc47b7f63455f /home/stsp/src/got
blob - c34b1bcb69fd3e41f6c82be87c06d624f89bf175
file + got/got.1
--- got/got.1
+++ got/got.1
@@ -1042,6 +1042,61 @@ 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 .
+If the
+.Ar branch
+contains changes to files outside of the work tree's path prefix,
+the work tree cannot be used to reintegrate this branch.
+.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,153 @@ 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;
+	struct got_object_id_queue commits;
+	int ch, did_something = 0;
+
+	SIMPLEQ_INIT(&commits);
+
+	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;
+	}
+
+	/* Run path-prefix check on commits in the branch. */
+	error = collect_commits(&commits, commit_id, commit_id,
+	    base_commit_id, got_worktree_get_path_prefix(worktree),
+	    GOT_ERR_REINTEGRATE_PATH, repo);
+	if (error) {
+		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);
+	got_object_id_queue_free(&commits);
+	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,8 @@
 #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
+#define GOT_ERR_REINTEGRATE_PATH 112
 
 static const struct got_error {
 	int code;
@@ -254,6 +256,9 @@ 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" },
+	{ GOT_ERR_REINTEGRATE_PATH, "cannot reintegrate branch which contains "
+	    "changes outside of this work tree's path prefix" },
 };
 
 /*
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,308 @@
+#!/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 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: cannot reintegrate branch which contains " \
+		> $testroot/stderr.expected
+	echo "changes outside of this work tree's path prefix" \
+		>> $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"
+}
+
+
+run_test test_reintegrate_basic
+run_test test_reintegrate_requires_rebase_first
+run_test test_reintegrate_path_prefix