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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
Re: got reintegrate
To:
Mark Jamsek <mark@jamsek.com>
Cc:
Evan Silberman <evan@jklol.net>, gameoftrees@openbsd.org
Date:
Tue, 15 Oct 2019 13:10:41 +0200

Download raw body.

Thread
On Tue, Oct 15, 2019 at 08:42:25PM +1100, Mark Jamsek wrote:
> Knowing what it does, ‘reintegrate’ makes sense perhaps
> even ‘integrate’ could work.

You and Evan both have made this suggestion, so this will likely
come up again. I am now convinced that we should drop the 're-'
prefix and call this command "got integrate". This is still close
enough to be recognizable to SVN users, and is shorter to type.

It occurs to me that in SVN a "reintegrate merge" was originally designed
as a one-time operation. A branch would be re-integrated into trunk and
then users were expected to delete this branch and never use it again!
The ability to "reintegrate" a branch multiple times was added later
(in SVN 1.8, I think), but the name stuck.
So perhaps that is why the developers who designed SVN's merge-tracking
called it that way. But I was not involved in this original design process,
which happened before my time with the SVN project.

Grammatical question: Would native English speakers use "integrate into"?
I was thinking perhaps "integrate with" was more correct, however when
phrased like that the implied hint at the direction in which changes flow
between branches is lost.

New diff:

diff d136cfcb987bd2fd865f8711449dc47b7f63455f /home/stsp/src/got
blob - c34b1bcb69fd3e41f6c82be87c06d624f89bf175
file + got/got.1
--- got/got.1
+++ got/got.1
@@ -1042,6 +1042,58 @@ If this option is used, no other command-line argument
 .It Cm he
 Short alias for
 .Cm histedit .
+.It Cm integrate Ar branch
+Integrate the specified
+.Ar branch
+into the work tree's current branch.
+Files in the work tree are updated to match the contents on the integrated
+.Ar branch ,
+and the reference of the work tree's branch is changed to point at the
+head commit of the integrated
+.Ar branch .
+.Pp
+Both branches can be considered equivalent after integration since they
+will be pointing at the same commit.
+Both branches remain available for future work, if desired.
+In case the integrated
+.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 integrate
+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 integrated, in order to linearize commit history and
+resolve merge conflicts.
+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 ig
+Short alias for
+.Cm integrate .
 .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_integrate(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_integrate(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" },
+	{ "integrate",  cmd_integrate,  usage_integrate,"ig" },
 	{ "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_integrate(void)
+{
+	fprintf(stderr, "usage: %s integrate branch\n", getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+cmd_integrate(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_integrate();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 1)
+		usage_integrate();
+	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_integrate_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_integrate_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_integrate_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 integrated");
+		got_worktree_integrate_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_integrate_abort(worktree, fileindex, repo,
+		    branch_ref, base_branch_ref);
+		goto done;
+	}
+
+	error = got_worktree_integrate_continue(worktree, fileindex, repo,
+	    branch_ref, base_branch_ref, update_progress, &did_something,
+	    check_cancelled, NULL);
+	if (error)
+		goto done;
+
+	printf("Integrated %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 integrating a branch.
+ * Return pointers to a fileindex and locked references which must be
+ * passed back to other integrate-related functions.
+ */
+const struct got_error *
+got_worktree_integrate_prepare(struct got_fileindex **,
+   struct got_reference **, struct got_reference **,
+    struct got_worktree *, const char *, struct got_repository *);
+
+/*
+ * Carry out a prepared branch integration operation.
+ * Report affected files via the specified progress callback.
+ */
+const struct got_error *got_worktree_integrate_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 branch integration operation. */
+const struct got_error *got_worktree_integrate_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_integrate_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 integrate 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_integrate_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_integrate_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 \
+	integrate stage unstage cat
 NOOBJ=Yes
 
 checkout:
@@ -56,6 +56,9 @@ import:
 
 histedit:
 	./histedit.sh
+
+integrate:
+	./integrate.sh
 
 stage:
 	./stage.sh
blob - /dev/null
file + regress/cmdline/integrate.sh
--- regress/cmdline/integrate.sh
+++ regress/cmdline/integrate.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_integrate_basic {
+	local testroot=`test_init integrate_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 integrate 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 "Integrated 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_integrate_requires_rebase_first {
+	local testroot=`test_init integrate_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 integrate newbranch \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret="$?"
+	if [ "$ret" == "0" ]; then
+		echo "got integrate 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_integrate_path_prefix {
+	local testroot=`test_init integrate_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 integrate newbranch > $testroot/stdout)
+
+	echo "A  new" > $testroot/stdout.expected
+	echo "Integrated 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_integrate_basic
+run_test test_integrate_requires_rebase_first
+run_test test_integrate_path_prefix