diff mbox series

[v2,2/3] bisect: introduce first-parent flag

Message ID 20200730002735.87655-3-alipman88@gmail.com
State New, archived
Headers show
Series Introduce --first-parent flag for git bisect | expand

Commit Message

Aaron Lipman July 30, 2020, 12:27 a.m. UTC
Upon seeing a merge commit when bisecting, this option may be used to
follow only the first parent.

In detecting regressions introduced through the merging of a branch, the
merge commit will be identified as introduction of the bug and its
ancestors will be ignored.

This option is particularly useful in avoiding false positives when a
merged branch contained broken or non-buildable commits, but the merge
itself was OK.

Signed-off-by: Aaron Lipman <alipman88@gmail.com>
---
 Documentation/git-bisect.txt | 13 ++++++++++++-
 bisect.c                     | 10 +++++++++-
 bisect.h                     |  2 ++
 builtin/bisect--helper.c     | 16 ++++++++++++----
 t/t6030-bisect-porcelain.sh  | 17 +++++++++++++++++
 5 files changed, 52 insertions(+), 6 deletions(-)

Comments

Junio C Hamano July 30, 2020, 4:17 a.m. UTC | #1
Aaron Lipman <alipman88@gmail.com> writes:

> diff --git a/bisect.c b/bisect.c
> index a11fdb1473..b495a19192 100644
> --- a/bisect.c
> +++ b/bisect.c
> @@ -460,6 +460,7 @@ static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN")
>  static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START")
>  static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG")
>  static GIT_PATH_FUNC(git_path_bisect_terms, "BISECT_TERMS")
> +static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT")
>  static GIT_PATH_FUNC(git_path_head_name, "head-name")

This is not a new issue, but duplication of the above and the same
set of PATH_FUNC in builtin/bisect-helper.c looks ugly.  We may want
to do something about it after this topic is done.

> @@ -981,6 +982,12 @@ void read_bisect_terms(const char **read_bad, const char **read_good)
>  	fclose(fp);
>  }
>  
> +int read_first_parent_option(void)

"static int", no?  I do not think we need to be able to call this
from anywhere outside this file.

> +{
> +	const char *filename = git_path_bisect_first_parent();
> +	return !access(filename, F_OK);
> +}
> +
>  /*
>   * We use the convention that return BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND (-10) means
>   * the bisection process finished successfully.
> @@ -997,7 +1004,7 @@ enum bisect_error bisect_next_all(struct repository *r, const char *prefix, int
>  	enum bisect_error res = BISECT_OK;
>  	struct object_id *bisect_rev;
>  	char *steps_msg;
> -	int first_parent_only = 0; /* TODO: pass --first-parent flag from git bisect start */
> +	int first_parent_only = read_first_parent_option();
>  
>  	read_bisect_terms(&term_bad, &term_good);
>  	if (read_bisect_refs())
> @@ -1141,6 +1148,7 @@ int bisect_clean_state(void)
>  	unlink_or_warn(git_path_bisect_names());
>  	unlink_or_warn(git_path_bisect_run());
>  	unlink_or_warn(git_path_bisect_terms());
> +	unlink_or_warn(git_path_bisect_first_parent());
>  	/* Cleanup head-name if it got left by an old version of git-bisect */
>  	unlink_or_warn(git_path_head_name());
>  	/*
> diff --git a/bisect.h b/bisect.h
> index a63af0505f..8ee80f5b48 100644
> --- a/bisect.h
> +++ b/bisect.h
> @@ -66,6 +66,8 @@ int estimate_bisect_steps(int all);
>  
>  void read_bisect_terms(const char **bad, const char **good);
>  
> +int read_first_parent_option(void);
> +

So this won't be needed, right?

> diff --git a/builtin/bisect--helper.c b/builtin/bisect--helper.c
> index ec4996282e..1236f5df1d 100644
> --- a/builtin/bisect--helper.c
> +++ b/builtin/bisect--helper.c
> @@ -422,7 +423,7 @@ static int bisect_append_log_quoted(const char **argv)
>  }
>  
>  static int bisect_start(struct bisect_terms *terms, int no_checkout,
> -			const char **argv, int argc)
> +			int first_parent_only, const char **argv, int argc)

Why do we need to pass this new parameter from cmd_bisect__helper(),
when we are going to parse it out of argv/argc outselves anyway?

I suspect that you would ask the same question to whoever added
no_checkout to this thing, and I wouldn't be surprised if we end up
removing both of these parameters (and parsing of the options inside
cmd_bisect__helper()) after thinking about them, but anyway, let's
read on.

> +# We want to automatically find the merge that
> +# introduced "line" into hello.

'introduced'?  

> +test_expect_success \
> +    '"git bisect run --first-parent" simple case' \

Let's not revert back to ancient line-folding style.  The next test
you can see in the postimage, i.e.

	test_expect_success 'title in single quote' '
		the first command &&
		the second command &&
		...
		the last command
	'

is what we want to mimic.

> +    'echo "#"\!"/bin/sh" > test_script.sh &&
> +     echo "grep line hello > /dev/null" >> test_script.sh &&
> +     echo "test \$? -ne 0" >> test_script.sh &&
> +     chmod +x test_script.sh &&

So, we want to say "bad" if $? is 0, i.e. the file 'hello' has a
string "line" in it and "good" if $? is not 0, i.e. the file 'hello'
does not have such a line?

A few things:

 - Use "write_script" to abstract away platform-specific details
   such as which $SHELL_PATH to use on "#!" line, and "chmod +x".

 - There is no SP between a redirection operator and its target
   file.

i.e. Ssomething like this instead of the above four lines:

	write_script test_script.sh <<-\EOF
	! grep line hello >/dev/null
	EOF

> +     git bisect start --first-parent &&
> +     test_path_is_file ".git/BISECT_FIRST_PARENT" &&
> +     git bisect good $HASH4 &&
> +     git bisect bad $B_HASH &&
> +     git bisect run ./test_script.sh > my_bisect_log.txt &&

Style: no SP between '>' and its target file.

> +     grep "$B_HASH is the first bad commit" my_bisect_log.txt &&
> +     git bisect reset &&
> +     test_path_is_missing ".git/BISECT_FIRST_PARENT"'

The final blame must lie on a commit on the first-parent chain,
which this test tries to ensure, but I wonder if it is also required
that all commits offered to be tested by "git bisect" are on the
first-parent chain, and if so, shouldn't we be make sure each and
every time "test_script" is run, the commit that is at HEAD is on
the first parent chain between the initial good..bad range?

>  test_expect_success 'good merge bases when good and bad are siblings' '
>  	git bisect start "$B_HASH" "$A_HASH" > my_bisect_log.txt &&
>  	test_i18ngrep "merge base must be tested" my_bisect_log.txt &&

Other than that, overall this seems to be going in the right
direction.

Thanks.
diff mbox series

Patch

diff --git a/Documentation/git-bisect.txt b/Documentation/git-bisect.txt
index 7586c5a843..0e993e4587 100644
--- a/Documentation/git-bisect.txt
+++ b/Documentation/git-bisect.txt
@@ -17,7 +17,7 @@  The command takes various subcommands, and different options depending
 on the subcommand:
 
  git bisect start [--term-{old,good}=<term> --term-{new,bad}=<term>]
-		  [--no-checkout] [<bad> [<good>...]] [--] [<paths>...]
+		  [--no-checkout] [--first-parent] [<bad> [<good>...]] [--] [<paths>...]
  git bisect (bad|new|<term-new>) [<rev>]
  git bisect (good|old|<term-old>) [<rev>...]
  git bisect terms [--term-good | --term-bad]
@@ -365,6 +365,17 @@  does not require a checked out tree.
 +
 If the repository is bare, `--no-checkout` is assumed.
 
+--first-parent::
++
+Follow only the first parent commit upon seeing a merge commit.
++
+In detecting regressions introduced through the merging of a branch, the merge
+commit will be identified as introduction of the bug and its ancestors will be
+ignored.
++
+This option is particularly useful in avoiding false positives when a merged
+branch contained broken or non-buildable commits, but the merge itself was OK.
+
 EXAMPLES
 --------
 
diff --git a/bisect.c b/bisect.c
index a11fdb1473..b495a19192 100644
--- a/bisect.c
+++ b/bisect.c
@@ -460,6 +460,7 @@  static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN")
 static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START")
 static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG")
 static GIT_PATH_FUNC(git_path_bisect_terms, "BISECT_TERMS")
+static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT")
 static GIT_PATH_FUNC(git_path_head_name, "head-name")
 
 static void read_bisect_paths(struct argv_array *array)
@@ -981,6 +982,12 @@  void read_bisect_terms(const char **read_bad, const char **read_good)
 	fclose(fp);
 }
 
+int read_first_parent_option(void)
+{
+	const char *filename = git_path_bisect_first_parent();
+	return !access(filename, F_OK);
+}
+
 /*
  * We use the convention that return BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND (-10) means
  * the bisection process finished successfully.
@@ -997,7 +1004,7 @@  enum bisect_error bisect_next_all(struct repository *r, const char *prefix, int
 	enum bisect_error res = BISECT_OK;
 	struct object_id *bisect_rev;
 	char *steps_msg;
-	int first_parent_only = 0; /* TODO: pass --first-parent flag from git bisect start */
+	int first_parent_only = read_first_parent_option();
 
 	read_bisect_terms(&term_bad, &term_good);
 	if (read_bisect_refs())
@@ -1141,6 +1148,7 @@  int bisect_clean_state(void)
 	unlink_or_warn(git_path_bisect_names());
 	unlink_or_warn(git_path_bisect_run());
 	unlink_or_warn(git_path_bisect_terms());
+	unlink_or_warn(git_path_bisect_first_parent());
 	/* Cleanup head-name if it got left by an old version of git-bisect */
 	unlink_or_warn(git_path_head_name());
 	/*
diff --git a/bisect.h b/bisect.h
index a63af0505f..8ee80f5b48 100644
--- a/bisect.h
+++ b/bisect.h
@@ -66,6 +66,8 @@  int estimate_bisect_steps(int all);
 
 void read_bisect_terms(const char **bad, const char **good);
 
+int read_first_parent_option(void);
+
 int bisect_clean_state(void);
 
 #endif
diff --git a/builtin/bisect--helper.c b/builtin/bisect--helper.c
index ec4996282e..1236f5df1d 100644
--- a/builtin/bisect--helper.c
+++ b/builtin/bisect--helper.c
@@ -17,6 +17,7 @@  static GIT_PATH_FUNC(git_path_bisect_head, "BISECT_HEAD")
 static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG")
 static GIT_PATH_FUNC(git_path_head_name, "head-name")
 static GIT_PATH_FUNC(git_path_bisect_names, "BISECT_NAMES")
+static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT")
 
 static const char * const git_bisect_helper_usage[] = {
 	N_("git bisect--helper --next-all [--no-checkout]"),
@@ -28,7 +29,7 @@  static const char * const git_bisect_helper_usage[] = {
 	N_("git bisect--helper --bisect-next-check <good_term> <bad_term> [<term>]"),
 	N_("git bisect--helper --bisect-terms [--term-good | --term-old | --term-bad | --term-new]"),
 	N_("git bisect--helper --bisect-start [--term-{old,good}=<term> --term-{new,bad}=<term>]"
-					     "[--no-checkout] [<bad> [<good>...]] [--] [<paths>...]"),
+					    " [--no-checkout] [--first-parent] [<bad> [<good>...]] [--] [<paths>...]"),
 	NULL
 };
 
@@ -422,7 +423,7 @@  static int bisect_append_log_quoted(const char **argv)
 }
 
 static int bisect_start(struct bisect_terms *terms, int no_checkout,
-			const char **argv, int argc)
+			int first_parent_only, const char **argv, int argc)
 {
 	int i, has_double_dash = 0, must_write_terms = 0, bad_seen = 0;
 	int flags, pathspec_pos, res = 0;
@@ -453,6 +454,8 @@  static int bisect_start(struct bisect_terms *terms, int no_checkout,
 			break;
 		} else if (!strcmp(arg, "--no-checkout")) {
 			no_checkout = 1;
+		} else if (!strcmp(arg, "--first-parent")) {
+			first_parent_only = 1;
 		} else if (!strcmp(arg, "--term-good") ||
 			 !strcmp(arg, "--term-old")) {
 			i++;
@@ -577,6 +580,9 @@  static int bisect_start(struct bisect_terms *terms, int no_checkout,
 	 */
 	write_file(git_path_bisect_start(), "%s\n", start_head.buf);
 
+	if (first_parent_only)
+		write_file(git_path_bisect_first_parent(), "\n");
+
 	if (no_checkout) {
 		if (get_oid(start_head.buf, &oid) < 0) {
 			res = error(_("invalid ref: '%s'"), start_head.buf);
@@ -632,7 +638,7 @@  int cmd_bisect__helper(int argc, const char **argv, const char *prefix)
 		BISECT_TERMS,
 		BISECT_START
 	} cmdmode = 0;
-	int no_checkout = 0, res = 0, nolog = 0;
+	int no_checkout = 0, first_parent_only = 0, res = 0, nolog = 0;
 	struct option options[] = {
 		OPT_CMDMODE(0, "next-all", &cmdmode,
 			 N_("perform 'git bisect next'"), NEXT_ALL),
@@ -656,6 +662,8 @@  int cmd_bisect__helper(int argc, const char **argv, const char *prefix)
 			 N_("start the bisect session"), BISECT_START),
 		OPT_BOOL(0, "no-checkout", &no_checkout,
 			 N_("update BISECT_HEAD instead of checking out the current commit")),
+		OPT_BOOL(0, "first-parent", &first_parent_only,
+			 N_("only trace the first parent of merge commits")),
 		OPT_BOOL(0, "no-log", &nolog,
 			 N_("no log for BISECT_WRITE")),
 		OPT_END()
@@ -713,7 +721,7 @@  int cmd_bisect__helper(int argc, const char **argv, const char *prefix)
 		break;
 	case BISECT_START:
 		set_terms(&terms, "bad", "good");
-		res = bisect_start(&terms, no_checkout, argv, argc);
+		res = bisect_start(&terms, no_checkout, first_parent_only, argv, argc);
 		break;
 	default:
 		return error("BUG: unknown subcommand '%d'", cmdmode);
diff --git a/t/t6030-bisect-porcelain.sh b/t/t6030-bisect-porcelain.sh
index 36d9b2b2e4..7965698310 100755
--- a/t/t6030-bisect-porcelain.sh
+++ b/t/t6030-bisect-porcelain.sh
@@ -458,6 +458,23 @@  test_expect_success 'many merge bases creation' '
 	grep "$SIDE_HASH5" merge_bases.txt
 '
 
+# We want to automatically find the merge that
+# introduced "line" into hello.
+test_expect_success \
+    '"git bisect run --first-parent" simple case' \
+    'echo "#"\!"/bin/sh" > test_script.sh &&
+     echo "grep line hello > /dev/null" >> test_script.sh &&
+     echo "test \$? -ne 0" >> test_script.sh &&
+     chmod +x test_script.sh &&
+     git bisect start --first-parent &&
+     test_path_is_file ".git/BISECT_FIRST_PARENT" &&
+     git bisect good $HASH4 &&
+     git bisect bad $B_HASH &&
+     git bisect run ./test_script.sh > my_bisect_log.txt &&
+     grep "$B_HASH is the first bad commit" my_bisect_log.txt &&
+     git bisect reset &&
+     test_path_is_missing ".git/BISECT_FIRST_PARENT"'
+
 test_expect_success 'good merge bases when good and bad are siblings' '
 	git bisect start "$B_HASH" "$A_HASH" > my_bisect_log.txt &&
 	test_i18ngrep "merge base must be tested" my_bisect_log.txt &&