Subject: [RFC PATCH] Teach checkout the -n|--dry-run option

From: Jens Lehmann <[email-masked]>
Date: Sat, 04 Sep 2010 22:19:50 +0200

This option can be used to determine if checking out a branch or commit
would touch files which are modified in the work tree. It doesn't change
the work tree contents or the index, so it can only tell if the checkout
would succeed using trivial merges.

It can neither be used when checking out only certain paths nor together
with the '-m' option.

Signed-off-by: Jens Lehmann <[email-masked]>
---

I need this new option for the recursive submodule checkout. Denying a
checkout inside a submodule just because it has modifications is a too
limiting condition (and it is way too expensive to check the whole tree,
just looking at those paths going to be changed by the checkout should
be much faster, especially for large submodules).

IMO the same behavior that applies for a checkout in the superproject
should apply for the checkout inside the submodule: no local changes
may be overwritten by the checkout (and the HEAD must match the commit
recorded in the superproject, but that is handled elsewhere).

To be able to test that, I added the -n|--dry-run option to checkout.
I think I found all relevant places, but a few more eyeballs are highly
appreciated as I am not very familiar with this part of the code.

A thing I'm not so sure about is the output of show_local_changes(),
with -n it doesn't show the changes as they would have been *after* the
- not executed - checkout but *without* having done it.
Is that a problem here?

 Documentation/git-checkout.txt |   13 +++++++++++--
 builtin/checkout.c             |   20 +++++++++++++++-----
 t/t2018-checkout-branch.sh     |   23 +++++++++++++++++++++++
 3 files changed, 49 insertions(+), 7 deletions(-)

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index f88e997..32c5c77 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -8,8 +8,8 @@ git-checkout - Checkout a branch or paths to the working tree
 SYNOPSIS
 --------
 [verse]
-'git checkout' [-q] [-f] [-m] [<branch>]
-'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
+'git checkout' [-q] [-f] [-m|-n] [<branch>]
+'git checkout' [-q] [-f] [-m|-n] [[-b|-B|--orphan] <new_branch>] [<start_point>]
 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...
 'git checkout' --patch [<tree-ish>] [--] [<paths>...]

@@ -77,6 +77,13 @@ OPTIONS
 When checking out paths from the index, do not fail upon unmerged
 entries; instead, unmerged entries are ignored.

+-n::
+--dry-run::
+	Don't really checkout the file(s), just check if it would succeed
+	using only trivial merges. Using this option will not touch the work
+	tree or the index. The command will fail when the checkout would change
+	locally modified files. This option can not be used together with `-m`.
+
 --ours::
 --theirs::
 	When checking out paths from the index, check out stage #2
@@ -158,6 +165,8 @@ should result in deletion of the path).
 +
 When checking out paths from the index, this option lets you recreate
 the conflicted merge in the specified paths.
++
+This option does not work together with '-n'.

 --conflict=<style>::
 	The same as --merge option above, but changes the way the
diff --git a/builtin/checkout.c b/builtin/checkout.c
index f365169..ba6ca24 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -32,6 +32,7 @@ struct checkout_opts {
 	int force;
 	int writeout_stage;
 	int writeout_error;
+	int dry_run;

 	/* not set by parse_options */
 	int branch_exists;
@@ -370,6 +371,8 @@ static int merge_working_tree(struct checkout_opts *opts,

 	resolve_undo_clear();
 	if (opts->force) {
+		if (opts->dry_run)
+			return 0;
 		ret = reset_tree(new->commit->tree, opts, 1);
 		if (ret)
 			return ret;
@@ -394,7 +397,7 @@ static int merge_working_tree(struct checkout_opts *opts,

 		/* 2-way merge to the new branch */
 		topts.initial_checkout = is_cache_unborn();
-		topts.update = 1;
+		topts.update = !opts->dry_run;
 		topts.merge = 1;
 		topts.gently = opts->merge && old->commit;
 		topts.verbose_update = !opts->quiet;
@@ -469,8 +472,8 @@ static int merge_working_tree(struct checkout_opts *opts,
 		}
 	}

-	if (write_cache(newfd, active_cache, active_nr) ||
-	    commit_locked_index(lock_file))
+	if (!opts->dry_run && (write_cache(newfd, active_cache, active_nr) ||
+			       commit_locked_index(lock_file)))
 		die("unable to write new index file");

 	if (!opts->force && !opts->quiet)
@@ -603,7 +606,7 @@ static int switch_branches(struct checkout_opts *opts, struct branch_info *new)
 	}

 	ret = merge_working_tree(opts, &old, new);
-	if (ret)
+	if (ret || opts->dry_run)
 		return ret;

 	/*
@@ -704,6 +707,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 		OPT_BOOLEAN('m', "merge", &opts.merge, "merge"),
 		OPT_STRING(0, "conflict", &conflict_style, "style",
 			   "conflict style (merge or diff3)"),
+		OPT_BOOLEAN('n', "dry-run", &opts.dry_run, "show if the checkout would fail because it touches locally modified files"),
 		OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"),
 		{ OPTION_BOOLEAN, 0, "guess", &dwim_new_local_branch, NULL,
 		  "second guess 'git checkout no-such-branch'",
@@ -731,7 +735,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	if (opts.new_branch_force)
 		opts.new_branch = opts.new_branch_force;

-	if (patch_mode && (opts.track > 0 || opts.new_branch
+	if (patch_mode && (opts.track > 0 || opts.new_branch || opts.dry_run
 			   || opts.new_branch_log || opts.merge || opts.force))
 		die ("--patch is incompatible with all other options");

@@ -766,6 +770,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	if (opts.force && opts.merge)
 		die("git checkout: -f and -m are incompatible");

+	if (opts.merge && opts.dry_run)
+		die("git checkout: -m and -n are incompatible");
+
 	/*
 	 * case 1: git checkout <ref> -- [<paths>]
 	 *
@@ -890,6 +897,9 @@ no_reference:
 		if (1 < !!opts.writeout_stage + !!opts.force + !!opts.merge)
 			die("git checkout: --ours/--theirs, --force and --merge are incompatible when\nchecking out of the index.");

+		if (opts.dry_run)
+			die("git checkout: updating paths is incompatible with -n.");
+
 		return checkout_paths(source_tree, pathspec, &opts);
 	}

diff --git a/t/t2018-checkout-branch.sh b/t/t2018-checkout-branch.sh
index fa69016..61c82ef 100755
--- a/t/t2018-checkout-branch.sh
+++ b/t/t2018-checkout-branch.sh
@@ -169,4 +169,27 @@ test_expect_success 'checkout -f -B to an existing branch with mergeable changes
 	test_must_fail test_dirty_mergeable
 '

+test_expect_success 'checkout -n/--dry-run does not change work tree or index' '
+	echo precious >expect &&
+	echo precious >file1 &&
+	test_must_fail git checkout -n branch1 &&
+	test_cmp expect file1 &&
+	git checkout --dry-run -f branch1 &&
+	test_cmp expect file1 &&
+	test_must_fail git checkout -b new_branch --dry-run branch1 &&
+	test_cmp expect file1 &&
+	git checkout -b new_branch -n -f branch1 &&
+	test_cmp expect file1 &&
+	git checkout -f branch1 &&
+	git status -s -uno > actual &&
+	! test -s actual &&
+	git checkout -n HEAD^ &&
+	git status -s -uno > actual &&
+	! test -s actual
+'
+
+test_expect_success 'checkout -n/--dry-run is not allowed when checking out only certain paths' '
+	test_must_fail git checkout -n branch1 file1 &&
+	test_must_fail git checkout -n HEAD file1
+'
 test_done
-- 
1.7.2.2.570.ga3f70