Commit Diff


commit - 03415a1a67d78f2decd6e46d82288a224bd4454d
commit + 5ef14e636366c988d1453d9fb6f4a2b9ff127141
blob - c120894f1b10c13a0d054b4f168f3adbb37416f9
blob + ccaccdbe1c46e978d4dd5c5c97878cd641408429
--- got/got.1
+++ got/got.1
@@ -412,7 +412,39 @@ to a single base commit with
 .Cm got update .
 If the work tree already contains files with merge conflicts, these
 conflicts must be resolved first.
+.It Cm backout Ar commit
+Reverse-merge changes from a single
+.Ar commit
+into the work tree.
+The specified
+.Ar commit
+must be on the same branch as the work tree's base commit.
+The expected argument is a reference or a SHA1 hash which corresponds to
+a commit object.
+.Pp
+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
+The reverse-merged changes will appear as local changes in the work tree,
+which may be viewed with
+.Cm got diff ,
+amended manually or with further
+.Cm got cherrypick
+comands,
+committed with
+.Cm got commit ,
+or discarded again with
+.Cm got revert .
+.Pp
+.El
 .Sh ENVIRONMENT
 .Bl -tag -width GOT_AUTHOR
 .It Ev GOT_AUTHOR
blob - 19de68852ed6874ce2254f35bf04bb2454d29ae6
blob + fd6712a8a82ee0d50d4a511136d1704b36783aa5
--- got/got.c
+++ got/got.c
@@ -87,6 +87,7 @@ __dead static void	usage_rm(void);
 __dead static void	usage_revert(void);
 __dead static void	usage_commit(void);
 __dead static void	usage_cherrypick(void);
+__dead static void	usage_backout(void);
 
 static const struct got_error*		cmd_checkout(int, char *[]);
 static const struct got_error*		cmd_update(int, char *[]);
@@ -101,6 +102,7 @@ static const struct got_error*		cmd_rm(int, char *[]);
 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 struct cmd got_commands[] = {
 	{ "checkout",	cmd_checkout,	usage_checkout,
@@ -129,6 +131,8 @@ static struct cmd got_commands[] = {
 	    "write changes from work tree to repository" },
 	{ "cherrypick",	cmd_cherrypick,	usage_cherrypick,
 	    "merge a single commit from another branch into a work tree" },
+	{ "backout",	cmd_backout,	usage_backout,
+	    "reverse-merge changes from a commit into a work tree" },
 };
 
 int
@@ -2719,3 +2723,110 @@ done:
 		got_repo_close(repo);
 	return error;
 }
+
+__dead static void
+usage_backout(void)
+{
+	fprintf(stderr, "usage: %s backout commit-id\n", getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+cmd_backout(int argc, char *argv[])
+{
+	const struct got_error *error = NULL;
+	struct got_worktree *worktree = NULL;
+	struct got_repository *repo = NULL;
+	char *cwd = NULL, *commit_id_str = NULL;
+	struct got_object_id *commit_id = NULL;
+	struct got_commit_object *commit = NULL;
+	struct got_object_qid *pid;
+	struct got_reference *head_ref = NULL;
+	int ch, did_something = 0;
+
+	while ((ch = getopt(argc, argv, "")) != -1) {
+		switch (ch) {
+		default:
+			usage_backout();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 1)
+		usage_backout();
+
+	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_object_resolve_id_str(&commit_id, repo, argv[0]);
+	if (error != NULL) {
+		struct got_reference *ref;
+		if (error->code != GOT_ERR_BAD_OBJ_ID_STR)
+			goto done;
+		error = got_ref_open(&ref, repo, argv[0], 0);
+		if (error != NULL)
+			goto done;
+		error = got_ref_resolve(&commit_id, repo, ref);
+		got_ref_close(ref);
+		if (error != NULL)
+			goto done;
+	}
+	error = got_object_id_str(&commit_id_str, commit_id);
+	if (error)
+		goto done;
+
+	error = got_ref_open(&head_ref, repo,
+	    got_worktree_get_head_ref_name(worktree), 0);
+	if (error != NULL)
+		goto done;
+
+	error = check_same_branch(commit_id, head_ref, repo);
+	if (error)
+		goto done;
+
+	error = got_object_open_as_commit(&commit, repo, commit_id);
+	if (error)
+		goto done;
+	pid = SIMPLEQ_FIRST(got_object_commit_get_parent_ids(commit));
+	if (pid == NULL) {
+		error = got_error(GOT_ERR_ROOT_COMMIT);
+		goto done;
+	}
+
+	error = got_worktree_merge_files(worktree, commit_id, pid->id, repo,
+	    update_progress, &did_something, check_cancelled, NULL);
+	if (error != NULL)
+		goto done;
+
+	if (did_something)
+		printf("backed out commit %s\n", commit_id_str);
+done:
+	if (commit)
+		got_object_commit_close(commit);
+	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;
+}
blob - e19e3956f955247d86d4c8d6910b38709f202a31
blob + 0186e720548b6da8fde30b6fd002a9076c3adc16
--- include/got_error.h
+++ include/got_error.h
@@ -93,7 +93,7 @@
 #define GOT_ERR_BRANCH_MOVED	77
 #define GOT_ERR_OBJ_TOO_LARGE	78
 #define GOT_ERR_SAME_BRANCH	79
-/* 80 is currently free for re-use */
+#define GOT_ERR_ROOT_COMMIT	80
 #define GOT_ERR_MIXED_COMMITS	81
 #define GOT_ERR_CONFLICTS	82
 
@@ -182,7 +182,7 @@ static const struct got_error {
 	    "different branch; new head reference and/or update -b required" },
 	{ GOT_ERR_OBJ_TOO_LARGE,	"object too large" },
 	{ GOT_ERR_SAME_BRANCH,	"commit is already contained in this branch" },
-	{ 80,	"unused error code" },
+	{ GOT_ERR_ROOT_COMMIT,	"specified commit has no parent commit" },
 	{ GOT_ERR_MIXED_COMMITS,"work tree contains files from multiple "
 	    "base commits; the entire work tree must be updated first" },
 	{ GOT_ERR_CONFLICTS,	"work tree contains conflicted files; these "
blob - 5776af5d999314a63da955a325813c1315eb8bfa
blob + a8340495b91e2f1614460a1b9c12ed746be6921f
--- regress/cmdline/Makefile
+++ regress/cmdline/Makefile
@@ -28,4 +28,7 @@ commit:
 cherrypick:
 	./cherrypick.sh
 
+backout:
+	./backout.sh
+
 .include <bsd.regress.mk>
blob - /dev/null
blob + d6140ca8eb2f4465f26784c06d824446c3fda162 (mode 755)
--- /dev/null
+++ regress/cmdline/backout.sh
@@ -0,0 +1,74 @@
+#!/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_backout_basic {
+	local testroot=`test_init backout_basic`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret="$?"
+	if [ "$ret" != "0" ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "modified alpha" > $testroot/wt/alpha
+	(cd $testroot/wt && got commit -m "changing alpha" > /dev/null)
+
+	local bad_commit=`git_show_head $testroot/repo`
+
+
+	(cd $testroot/wt && got update > /dev/null)
+
+	echo "modified beta" > $testroot/wt/beta
+	(cd $testroot/wt && got commit -m "changing beta" > /dev/null)
+
+	(cd $testroot/wt && got update > /dev/null)
+
+	(cd $testroot/wt && got backout $bad_commit > $testroot/stdout)
+
+	echo "G  alpha" > $testroot/stdout.expected
+	echo "backed out commit $bad_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
+
+	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
+
+	echo 'M  alpha' > $testroot/stdout.expected
+	(cd $testroot/wt && got status > $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"
+}
+
+run_test test_backout_basic