Subject: [PATCH 3/4] branch: add --dry-run option to branch

From: Glen Choo <[email-masked]>
Date: Mon, 22 Nov 2021 14:32:51 -0800

When running "git branch --recurse-submodules topic", it would be useful
to know whether or not 'topic' is a valid branch for all repositories.
Currently there is no way to test this without actually creating the
branch.

Add a --dry-run option to branch creation that can check whether or not
a branch name and start point would be valid for a repository without
creating a branch. Refactor cmd_branch() to make the chosen action more
obvious.

Incidentally, fix an incorrect usage string that combined the 'list'
usage of git branch (-l) with the 'create' usage; this string has been
incorrect since its inception, a8dfd5eac4 (Make builtin-branch.c use
parse_options., 2007-10-07).

Signed-off-by: Glen Choo <[email-masked]>
---
The --dry-run option is motivated mainly by --recurse-submodules. To my
knowledge, there isn't a strong existing demand, but this might be
mildly useful to some users.

 Documentation/git-branch.txt |  8 ++++++-
 branch.c                     |  6 ++---
 branch.h                     | 22 ++++++++++++++++++
 builtin/branch.c             | 44 ++++++++++++++++++++++++++----------
 t/t3200-branch.sh            | 13 +++++++++++
 5 files changed, 77 insertions(+), 16 deletions(-)

diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt
index 5449767121..8cdc33c097 100644
--- a/Documentation/git-branch.txt
+++ b/Documentation/git-branch.txt
@@ -16,7 +16,7 @@ SYNOPSIS
 	[--points-at <object>] [--format=<format>]
 	[(-r | --remotes) | (-a | --all)]
 	[--list] [<pattern>...]
-'git branch' [--track | --no-track] [-f] <branchname> [<start-point>]
+'git branch' [--track | --no-track] [-f] [--dry-run | -n] <branchname> [<start-point>]
 'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
 'git branch' --unset-upstream [<branchname>]
 'git branch' (-m | -M) [<oldbranch>] <newbranch>
@@ -205,6 +205,12 @@ This option is only applicable in non-verbose mode.
 --no-abbrev::
 	Display the full sha1s in the output listing rather than abbreviating them.
 
+-n::
+--dry-run::
+	Can only be used when creating a branch. If the branch creation
+	would fail, show the relevant error message. If the branch
+	creation would succeed, show nothing.
+
 -t::
 --track::
 	When creating a new branch, set up `branch.<name>.remote` and
diff --git a/branch.c b/branch.c
index f8b755513f..528cb2d639 100644
--- a/branch.c
+++ b/branch.c
@@ -206,9 +206,9 @@ N_("\n"
 "will track its remote counterpart, you may want to use\n"
 "\"git push -u\" to set the upstream config as you push.");
 
-static void validate_branch_start(struct repository *r, const char *start_name,
-				  enum branch_track track,
-				  struct object_id *oid, char **full_ref)
+void validate_branch_start(struct repository *r, const char *start_name,
+			   enum branch_track track, struct object_id *oid,
+			   char **full_ref)
 {
 	struct commit *commit;
 	int explicit_tracking = 0;
diff --git a/branch.h b/branch.h
index 75cefcdcbd..d8e5ff4e28 100644
--- a/branch.h
+++ b/branch.h
@@ -3,6 +3,7 @@
 
 struct repository;
 struct strbuf;
+struct object_id;
 
 enum branch_track {
 	BRANCH_TRACK_UNSPECIFIED = -1,
@@ -17,6 +18,27 @@ extern enum branch_track git_branch_track;
 
 /* Functions for acting on the information about branches. */
 
+/*
+ * Validates whether a ref is a valid starting point for a branch, where:
+ *
+ *   - r is the repository to validate the branch for
+ *
+ *   - start_name is the ref that we would like to test
+ *
+ *   - track is the tracking mode of the new branch. If tracking is
+ *     explicitly requested, start_name must be a branch (because
+ *     otherwise start_name cannot be tracked)
+ *
+ *   - oid is an out parameter containing the object_id of start_name
+ *
+ *   - full_ref is an out parameter containing the 'full' form of
+ *     start_name e.g. refs/heads/main instead of main
+ *
+ */
+void validate_branch_start(struct repository *r, const char *start_name,
+			   enum branch_track track, struct object_id *oid,
+			   char **full_ref);
+
 /*
  * This sets the branch.<new_ref>.{remote,merge} config settings so that
  * branch 'new_ref' tracks 'orig_ref'. This is called when branches are
diff --git a/builtin/branch.c b/builtin/branch.c
index eb5c117a6e..5d4b9c82b4 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -27,7 +27,8 @@
 
 static const char * const builtin_branch_usage[] = {
 	N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
-	N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"),
+	N_("git branch [<options>] [-l] [<pattern>...]"),
+	N_("git branch [<options>] [-f] [--dry-run | -n] <branch-name> [<start-point>]"),
 	N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
 	N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
 	N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
@@ -616,14 +617,14 @@ static int edit_branch_description(const char *branch_name)
 
 int cmd_branch(int argc, const char **argv, const char *prefix)
 {
-	int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
-	int show_current = 0;
-	int reflog = 0, edit_description = 0;
-	int quiet = 0, unset_upstream = 0;
+	/* possible actions */
+	int delete = 0, rename = 0, copy = 0, force = 0, list = 0, create = 0,
+	    unset_upstream = 0, show_current = 0, edit_description = 0;
+	/* possible options */
+	int reflog = 0, quiet = 0, dry_run = 0, icase = 0;
 	const char *new_upstream = NULL;
 	enum branch_track track;
 	struct ref_filter filter;
-	int icase = 0;
 	static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting;
 	struct ref_format format = REF_FORMAT_INIT;
 
@@ -670,6 +671,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 			N_("print only branches of the object"), parse_opt_object_name),
 		OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
 		OPT_STRING(  0 , "format", &format.format, N_("format"), N_("format to use for the output")),
+		OPT__DRY_RUN(&dry_run, N_("show whether the branch would be created")),
 		OPT_END(),
 	};
 
@@ -705,10 +707,15 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
 		list = 1;
 
-	if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +
-	    list + edit_description + unset_upstream > 1)
+	create = 1 - (!!delete + !!rename + !!copy + !!new_upstream +
+		      !!show_current + !!list + !!edit_description +
+		      !!unset_upstream);
+	if (create < 0)
 		usage_with_options(builtin_branch_usage, options);
 
+	if (dry_run && !create)
+		die(_("--dry-run can only be used when creating branches"));
+
 	if (filter.abbrev == -1)
 		filter.abbrev = DEFAULT_ABBREV;
 	filter.ignore_case = icase;
@@ -844,7 +851,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		strbuf_addf(&buf, "branch.%s.merge", branch->name);
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
-	} else if (argc > 0 && argc <= 2) {
+	} else if (create && argc > 0 && argc <= 2) {
+		const char *branch_name = argv[0];
+		const char *start_name = (argc == 2) ? argv[1] : head;
+
 		if (filter.kind != FILTER_REFS_BRANCHES)
 			die(_("The -a, and -r, options to 'git branch' do not take a branch name.\n"
 				  "Did you mean to use: -a|-r --list <pattern>?"));
@@ -852,10 +862,20 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		if (track == BRANCH_TRACK_OVERRIDE)
 			die(_("the '--set-upstream' option is no longer supported. Please use '--track' or '--set-upstream-to' instead."));
 
-		create_branch(the_repository,
-			      argv[0], (argc == 2) ? argv[1] : head,
-			      force, 0, reflog, quiet, track);
+		if (dry_run) {
+			struct strbuf buf = STRBUF_INIT;
+			char *unused_full_ref;
+			struct object_id unused_oid;
 
+			validate_new_branchname(branch_name, &buf, force);
+			validate_branch_start(the_repository, start_name, track,
+					      &unused_oid, &unused_full_ref);
+			strbuf_release(&buf);
+			FREE_AND_NULL(unused_full_ref);
+			return 0;
+		}
+		create_branch(the_repository, branch_name, start_name, force, 0,
+			      reflog, quiet, track);
 	} else
 		usage_with_options(builtin_branch_usage, options);
 
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 6bf95a1707..653891736a 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -59,6 +59,19 @@ test_expect_success 'git branch --force abc should succeed when abc exists' '
 	test_cmp expect actual
 '
 
+test_expect_success 'git branch --dry-run abc should fail when abc exists' '
+	test_must_fail git branch --dry-run abc
+'
+
+test_expect_success 'git branch --dry-run --force abc should succeed when abc exists' '
+	git branch --dry-run --force abc
+'
+
+test_expect_success 'git branch --dry-run def should not create a branch' '
+	git branch --dry-run def &&
+	test_must_fail git rev-parse def
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c
 '
-- 
2.33.GIT