Merge branch 'cc/bisect' (early part)
* 'cc/bisect' (early part): bisect: make "git bisect" use new "--next-all" bisect-helper function bisect: add "check_good_are_ancestors_of_bad" function bisect: implement the "check_merge_bases" function bisect: automatically sort sha1_array if needed when looking it up bisect: make skipped array functions more generic bisect: remove too much function nesting bisect: use new "struct argv_array" to prepare argv for "setup_revisions" bisect: store good revisions in a "sha1_array" bisect: implement "rev_argv_push" to fill an argv with revs bisect: use "sha1_array" to store skipped revisions am: simplify "sq" function by using "git rev-parse --sq-quote" bisect: use "git rev-parse --sq-quote" instead of a custom "sq" function rev-parse: add --sq-quote to shell quote arguments rev-list: remove stringed output flag from "show_bisect_vars" bisect--helper: remove "--next-vars" option as it is now useless bisect: use "git bisect--helper --next-exit" in "git-bisect.sh" bisect--helper: add "--next-exit" to output bisect results bisect: move common bisect functionality to "bisect_common" rev-list: refactor printing bisect vars rev-list: make "estimate_bisect_steps" non static
This commit is contained in:
commit
d34f715853
@ -30,6 +30,11 @@ OPTIONS
|
||||
Only meaningful in `--parseopt` mode. Tells the option parser to echo
|
||||
out the first `--` met instead of skipping it.
|
||||
|
||||
--sq-quote::
|
||||
Use 'git-rev-parse' in shell quoting mode (see SQ-QUOTE
|
||||
section below). In contrast to the `--sq` option below, this
|
||||
mode does only quoting. Nothing else is done to command input.
|
||||
|
||||
--revs-only::
|
||||
Do not output flags and parameters not meant for
|
||||
'git-rev-list' command.
|
||||
@ -64,7 +69,8 @@ OPTIONS
|
||||
properly quoted for consumption by shell. Useful when
|
||||
you expect your parameter to contain whitespaces and
|
||||
newlines (e.g. when using pickaxe `-S` with
|
||||
'git-diff-\*').
|
||||
'git-diff-\*'). In contrast to the `--sq-quote` option,
|
||||
the command input is still interpreted as usual.
|
||||
|
||||
--not::
|
||||
When showing object names, prefix them with '{caret}' and
|
||||
@ -406,6 +412,33 @@ C? option C with an optional argument"
|
||||
eval `echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?`
|
||||
------------
|
||||
|
||||
SQ-QUOTE
|
||||
--------
|
||||
|
||||
In `--sq-quote` mode, 'git-rev-parse' echoes on the standard output a
|
||||
single line suitable for `sh(1)` `eval`. This line is made by
|
||||
normalizing the arguments following `--sq-quote`. Nothing other than
|
||||
quoting the arguments is done.
|
||||
|
||||
If you want command input to still be interpreted as usual by
|
||||
'git-rev-parse' before the output is shell quoted, see the `--sq`
|
||||
option.
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
------------
|
||||
$ cat >your-git-script.sh <<\EOF
|
||||
#!/bin/sh
|
||||
args=$(git rev-parse --sq-quote "$@") # quote user-supplied arguments
|
||||
command="git frotz -n24 $args" # and use it inside a handcrafted
|
||||
# command line
|
||||
eval "$command"
|
||||
EOF
|
||||
|
||||
$ sh your-git-script.sh "a b'c"
|
||||
------------
|
||||
|
||||
EXAMPLES
|
||||
--------
|
||||
|
||||
|
1
Makefile
1
Makefile
@ -1249,7 +1249,6 @@ $(patsubst %.sh,%,$(SCRIPT_SH)) : % : %.sh
|
||||
$(QUIET_GEN)$(RM) $@ $@+ && \
|
||||
sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \
|
||||
-e 's|@SHELL_PATH@|$(SHELL_PATH_SQ)|' \
|
||||
-e 's|@@PERL@@|$(PERL_PATH_SQ)|g' \
|
||||
-e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' \
|
||||
-e 's/@@NO_CURL@@/$(NO_CURL)/g' \
|
||||
$@.sh >$@+ && \
|
||||
|
456
bisect.c
456
bisect.c
@ -6,15 +6,30 @@
|
||||
#include "list-objects.h"
|
||||
#include "quote.h"
|
||||
#include "sha1-lookup.h"
|
||||
#include "run-command.h"
|
||||
#include "bisect.h"
|
||||
|
||||
static unsigned char (*skipped_sha1)[20];
|
||||
static int skipped_sha1_nr;
|
||||
static int skipped_sha1_alloc;
|
||||
struct sha1_array {
|
||||
unsigned char (*sha1)[20];
|
||||
int sha1_nr;
|
||||
int sha1_alloc;
|
||||
int sorted;
|
||||
};
|
||||
|
||||
static const char **rev_argv;
|
||||
static int rev_argv_nr;
|
||||
static int rev_argv_alloc;
|
||||
static struct sha1_array good_revs;
|
||||
static struct sha1_array skipped_revs;
|
||||
|
||||
static const unsigned char *current_bad_sha1;
|
||||
|
||||
struct argv_array {
|
||||
const char **argv;
|
||||
int argv_nr;
|
||||
int argv_alloc;
|
||||
};
|
||||
|
||||
static const char *argv_diff_tree[] = {"diff-tree", "--pretty", NULL, NULL};
|
||||
static const char *argv_checkout[] = {"checkout", "-q", NULL, "--", NULL};
|
||||
static const char *argv_show_branch[] = {"show-branch", NULL, NULL};
|
||||
|
||||
/* bits #0-15 in revision.h */
|
||||
|
||||
@ -398,23 +413,37 @@ struct commit_list *find_bisection(struct commit_list *list,
|
||||
return best;
|
||||
}
|
||||
|
||||
static void argv_array_push(struct argv_array *array, const char *string)
|
||||
{
|
||||
ALLOC_GROW(array->argv, array->argv_nr + 1, array->argv_alloc);
|
||||
array->argv[array->argv_nr++] = string;
|
||||
}
|
||||
|
||||
static void argv_array_push_sha1(struct argv_array *array,
|
||||
const unsigned char *sha1,
|
||||
const char *format)
|
||||
{
|
||||
struct strbuf buf = STRBUF_INIT;
|
||||
strbuf_addf(&buf, format, sha1_to_hex(sha1));
|
||||
argv_array_push(array, strbuf_detach(&buf, NULL));
|
||||
}
|
||||
|
||||
static void sha1_array_push(struct sha1_array *array,
|
||||
const unsigned char *sha1)
|
||||
{
|
||||
ALLOC_GROW(array->sha1, array->sha1_nr + 1, array->sha1_alloc);
|
||||
hashcpy(array->sha1[array->sha1_nr++], sha1);
|
||||
}
|
||||
|
||||
static int register_ref(const char *refname, const unsigned char *sha1,
|
||||
int flags, void *cb_data)
|
||||
{
|
||||
if (!strcmp(refname, "bad")) {
|
||||
ALLOC_GROW(rev_argv, rev_argv_nr + 1, rev_argv_alloc);
|
||||
rev_argv[rev_argv_nr++] = xstrdup(sha1_to_hex(sha1));
|
||||
current_bad_sha1 = sha1;
|
||||
} else if (!prefixcmp(refname, "good-")) {
|
||||
const char *hex = sha1_to_hex(sha1);
|
||||
char *good = xmalloc(strlen(hex) + 2);
|
||||
*good = '^';
|
||||
memcpy(good + 1, hex, strlen(hex) + 1);
|
||||
ALLOC_GROW(rev_argv, rev_argv_nr + 1, rev_argv_alloc);
|
||||
rev_argv[rev_argv_nr++] = good;
|
||||
sha1_array_push(&good_revs, sha1);
|
||||
} else if (!prefixcmp(refname, "skip-")) {
|
||||
ALLOC_GROW(skipped_sha1, skipped_sha1_nr + 1,
|
||||
skipped_sha1_alloc);
|
||||
hashcpy(skipped_sha1[skipped_sha1_nr++], sha1);
|
||||
sha1_array_push(&skipped_revs, sha1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
@ -425,7 +454,7 @@ static int read_bisect_refs(void)
|
||||
return for_each_ref_in("refs/bisect/", register_ref, NULL);
|
||||
}
|
||||
|
||||
void read_bisect_paths(void)
|
||||
void read_bisect_paths(struct argv_array *array)
|
||||
{
|
||||
struct strbuf str = STRBUF_INIT;
|
||||
const char *filename = git_path("BISECT_NAMES");
|
||||
@ -440,8 +469,8 @@ void read_bisect_paths(void)
|
||||
|
||||
strbuf_trim(&str);
|
||||
quoted = strbuf_detach(&str, NULL);
|
||||
res = sq_dequote_to_argv(quoted, &rev_argv,
|
||||
&rev_argv_nr, &rev_argv_alloc);
|
||||
res = sq_dequote_to_argv(quoted, &array->argv,
|
||||
&array->argv_nr, &array->argv_alloc);
|
||||
if (res)
|
||||
die("Badly quoted content in file '%s': %s",
|
||||
filename, quoted);
|
||||
@ -451,26 +480,45 @@ void read_bisect_paths(void)
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
static int skipcmp(const void *a, const void *b)
|
||||
static int array_cmp(const void *a, const void *b)
|
||||
{
|
||||
return hashcmp(a, b);
|
||||
}
|
||||
|
||||
static void prepare_skipped(void)
|
||||
static void sort_sha1_array(struct sha1_array *array)
|
||||
{
|
||||
qsort(skipped_sha1, skipped_sha1_nr, sizeof(*skipped_sha1), skipcmp);
|
||||
qsort(array->sha1, array->sha1_nr, sizeof(*array->sha1), array_cmp);
|
||||
|
||||
array->sorted = 1;
|
||||
}
|
||||
|
||||
static const unsigned char *skipped_sha1_access(size_t index, void *table)
|
||||
static const unsigned char *sha1_access(size_t index, void *table)
|
||||
{
|
||||
unsigned char (*skipped)[20] = table;
|
||||
return skipped[index];
|
||||
unsigned char (*array)[20] = table;
|
||||
return array[index];
|
||||
}
|
||||
|
||||
static int lookup_skipped(unsigned char *sha1)
|
||||
static int lookup_sha1_array(struct sha1_array *array,
|
||||
const unsigned char *sha1)
|
||||
{
|
||||
return sha1_pos(sha1, skipped_sha1, skipped_sha1_nr,
|
||||
skipped_sha1_access);
|
||||
if (!array->sorted)
|
||||
sort_sha1_array(array);
|
||||
|
||||
return sha1_pos(sha1, array->sha1, array->sha1_nr, sha1_access);
|
||||
}
|
||||
|
||||
static char *join_sha1_array_hex(struct sha1_array *array, char delim)
|
||||
{
|
||||
struct strbuf joined_hexs = STRBUF_INIT;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < array->sha1_nr; i++) {
|
||||
strbuf_addstr(&joined_hexs, sha1_to_hex(array->sha1[i]));
|
||||
if (i + 1 < array->sha1_nr)
|
||||
strbuf_addch(&joined_hexs, delim);
|
||||
}
|
||||
|
||||
return strbuf_detach(&joined_hexs, NULL);
|
||||
}
|
||||
|
||||
struct commit_list *filter_skipped(struct commit_list *list,
|
||||
@ -481,15 +529,14 @@ struct commit_list *filter_skipped(struct commit_list *list,
|
||||
|
||||
*tried = NULL;
|
||||
|
||||
if (!skipped_sha1_nr)
|
||||
if (!skipped_revs.sha1_nr)
|
||||
return list;
|
||||
|
||||
prepare_skipped();
|
||||
|
||||
while (list) {
|
||||
struct commit_list *next = list->next;
|
||||
list->next = NULL;
|
||||
if (0 <= lookup_skipped(list->item->object.sha1)) {
|
||||
if (0 <= lookup_sha1_array(&skipped_revs,
|
||||
list->item->object.sha1)) {
|
||||
/* Move current to tried list */
|
||||
*tried = list;
|
||||
tried = &list->next;
|
||||
@ -508,49 +555,328 @@ struct commit_list *filter_skipped(struct commit_list *list,
|
||||
|
||||
static void bisect_rev_setup(struct rev_info *revs, const char *prefix)
|
||||
{
|
||||
struct argv_array rev_argv = { NULL, 0, 0 };
|
||||
int i;
|
||||
|
||||
init_revisions(revs, prefix);
|
||||
revs->abbrev = 0;
|
||||
revs->commit_format = CMIT_FMT_UNSPECIFIED;
|
||||
|
||||
/* argv[0] will be ignored by setup_revisions */
|
||||
ALLOC_GROW(rev_argv, rev_argv_nr + 1, rev_argv_alloc);
|
||||
rev_argv[rev_argv_nr++] = xstrdup("bisect_rev_setup");
|
||||
/* rev_argv.argv[0] will be ignored by setup_revisions */
|
||||
argv_array_push(&rev_argv, xstrdup("bisect_rev_setup"));
|
||||
argv_array_push_sha1(&rev_argv, current_bad_sha1, "%s");
|
||||
for (i = 0; i < good_revs.sha1_nr; i++)
|
||||
argv_array_push_sha1(&rev_argv, good_revs.sha1[i], "^%s");
|
||||
argv_array_push(&rev_argv, xstrdup("--"));
|
||||
read_bisect_paths(&rev_argv);
|
||||
argv_array_push(&rev_argv, NULL);
|
||||
|
||||
setup_revisions(rev_argv.argv_nr, rev_argv.argv, revs, NULL);
|
||||
revs->limited = 1;
|
||||
}
|
||||
|
||||
static void bisect_common(struct rev_info *revs, int *reaches, int *all)
|
||||
{
|
||||
if (prepare_revision_walk(revs))
|
||||
die("revision walk setup failed");
|
||||
if (revs->tree_objects)
|
||||
mark_edges_uninteresting(revs->commits, revs, NULL);
|
||||
|
||||
revs->commits = find_bisection(revs->commits, reaches, all,
|
||||
!!skipped_revs.sha1_nr);
|
||||
}
|
||||
|
||||
static void exit_if_skipped_commits(struct commit_list *tried,
|
||||
const unsigned char *bad)
|
||||
{
|
||||
if (!tried)
|
||||
return;
|
||||
|
||||
printf("There are only 'skip'ped commits left to test.\n"
|
||||
"The first bad commit could be any of:\n");
|
||||
print_commit_list(tried, "%s\n", "%s\n");
|
||||
if (bad)
|
||||
printf("%s\n", sha1_to_hex(bad));
|
||||
printf("We cannot bisect more!\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
static int is_expected_rev(const unsigned char *sha1)
|
||||
{
|
||||
const char *filename = git_path("BISECT_EXPECTED_REV");
|
||||
struct stat st;
|
||||
struct strbuf str = STRBUF_INIT;
|
||||
FILE *fp;
|
||||
int res = 0;
|
||||
|
||||
if (stat(filename, &st) || !S_ISREG(st.st_mode))
|
||||
return 0;
|
||||
|
||||
fp = fopen(filename, "r");
|
||||
if (!fp)
|
||||
return 0;
|
||||
|
||||
if (strbuf_getline(&str, fp, '\n') != EOF)
|
||||
res = !strcmp(str.buf, sha1_to_hex(sha1));
|
||||
|
||||
strbuf_release(&str);
|
||||
fclose(fp);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static void mark_expected_rev(char *bisect_rev_hex)
|
||||
{
|
||||
int len = strlen(bisect_rev_hex);
|
||||
const char *filename = git_path("BISECT_EXPECTED_REV");
|
||||
int fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
|
||||
|
||||
if (fd < 0)
|
||||
die("could not create file '%s': %s",
|
||||
filename, strerror(errno));
|
||||
|
||||
bisect_rev_hex[len] = '\n';
|
||||
write_or_die(fd, bisect_rev_hex, len + 1);
|
||||
bisect_rev_hex[len] = '\0';
|
||||
|
||||
if (close(fd) < 0)
|
||||
die("closing file %s: %s", filename, strerror(errno));
|
||||
}
|
||||
|
||||
static int bisect_checkout(char *bisect_rev_hex)
|
||||
{
|
||||
int res;
|
||||
|
||||
mark_expected_rev(bisect_rev_hex);
|
||||
|
||||
argv_checkout[2] = bisect_rev_hex;
|
||||
res = run_command_v_opt(argv_checkout, RUN_GIT_CMD);
|
||||
if (res)
|
||||
exit(res);
|
||||
|
||||
argv_show_branch[1] = bisect_rev_hex;
|
||||
return run_command_v_opt(argv_show_branch, RUN_GIT_CMD);
|
||||
}
|
||||
|
||||
static struct commit *get_commit_reference(const unsigned char *sha1)
|
||||
{
|
||||
struct commit *r = lookup_commit_reference(sha1);
|
||||
if (!r)
|
||||
die("Not a valid commit name %s", sha1_to_hex(sha1));
|
||||
return r;
|
||||
}
|
||||
|
||||
static struct commit **get_bad_and_good_commits(int *rev_nr)
|
||||
{
|
||||
int len = 1 + good_revs.sha1_nr;
|
||||
struct commit **rev = xmalloc(len * sizeof(*rev));
|
||||
int i, n = 0;
|
||||
|
||||
rev[n++] = get_commit_reference(current_bad_sha1);
|
||||
for (i = 0; i < good_revs.sha1_nr; i++)
|
||||
rev[n++] = get_commit_reference(good_revs.sha1[i]);
|
||||
*rev_nr = n;
|
||||
|
||||
return rev;
|
||||
}
|
||||
|
||||
static void handle_bad_merge_base(void)
|
||||
{
|
||||
if (is_expected_rev(current_bad_sha1)) {
|
||||
char *bad_hex = sha1_to_hex(current_bad_sha1);
|
||||
char *good_hex = join_sha1_array_hex(&good_revs, ' ');
|
||||
|
||||
fprintf(stderr, "The merge base %s is bad.\n"
|
||||
"This means the bug has been fixed "
|
||||
"between %s and [%s].\n",
|
||||
bad_hex, bad_hex, good_hex);
|
||||
|
||||
exit(3);
|
||||
}
|
||||
|
||||
fprintf(stderr, "Some good revs are not ancestor of the bad rev.\n"
|
||||
"git bisect cannot work properly in this case.\n"
|
||||
"Maybe you mistake good and bad revs?\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
void handle_skipped_merge_base(const unsigned char *mb)
|
||||
{
|
||||
char *mb_hex = sha1_to_hex(mb);
|
||||
char *bad_hex = sha1_to_hex(current_bad_sha1);
|
||||
char *good_hex = join_sha1_array_hex(&good_revs, ' ');
|
||||
|
||||
fprintf(stderr, "Warning: the merge base between %s and [%s] "
|
||||
"must be skipped.\n"
|
||||
"So we cannot be sure the first bad commit is "
|
||||
"between %s and %s.\n"
|
||||
"We continue anyway.\n",
|
||||
bad_hex, good_hex, mb_hex, bad_hex);
|
||||
free(good_hex);
|
||||
}
|
||||
|
||||
/*
|
||||
* "check_merge_bases" checks that merge bases are not "bad".
|
||||
*
|
||||
* - If one is "bad", it means the user assumed something wrong
|
||||
* and we must exit with a non 0 error code.
|
||||
* - If one is "good", that's good, we have nothing to do.
|
||||
* - If one is "skipped", we can't know but we should warn.
|
||||
* - If we don't know, we should check it out and ask the user to test.
|
||||
*/
|
||||
static void check_merge_bases(void)
|
||||
{
|
||||
struct commit_list *result;
|
||||
int rev_nr;
|
||||
struct commit **rev = get_bad_and_good_commits(&rev_nr);
|
||||
|
||||
result = get_merge_bases_many(rev[0], rev_nr - 1, rev + 1, 0);
|
||||
|
||||
for (; result; result = result->next) {
|
||||
const unsigned char *mb = result->item->object.sha1;
|
||||
if (!hashcmp(mb, current_bad_sha1)) {
|
||||
handle_bad_merge_base();
|
||||
} else if (0 <= lookup_sha1_array(&good_revs, mb)) {
|
||||
continue;
|
||||
} else if (0 <= lookup_sha1_array(&skipped_revs, mb)) {
|
||||
handle_skipped_merge_base(mb);
|
||||
} else {
|
||||
printf("Bisecting: a merge base must be tested\n");
|
||||
exit(bisect_checkout(sha1_to_hex(mb)));
|
||||
}
|
||||
}
|
||||
|
||||
free(rev);
|
||||
free_commit_list(result);
|
||||
}
|
||||
|
||||
/*
|
||||
* This function runs the command "git rev-list $_good ^$_bad"
|
||||
* and returns 1 if it produces some output, 0 otherwise.
|
||||
*/
|
||||
static int check_ancestors(void)
|
||||
{
|
||||
struct argv_array rev_argv = { NULL, 0, 0 };
|
||||
struct strbuf str = STRBUF_INIT;
|
||||
int i, result = 0;
|
||||
struct child_process rls;
|
||||
FILE *rls_fout;
|
||||
|
||||
argv_array_push(&rev_argv, xstrdup("rev-list"));
|
||||
argv_array_push_sha1(&rev_argv, current_bad_sha1, "^%s");
|
||||
for (i = 0; i < good_revs.sha1_nr; i++)
|
||||
argv_array_push_sha1(&rev_argv, good_revs.sha1[i], "%s");
|
||||
argv_array_push(&rev_argv, NULL);
|
||||
|
||||
memset(&rls, 0, sizeof(rls));
|
||||
rls.argv = rev_argv.argv;
|
||||
rls.out = -1;
|
||||
rls.git_cmd = 1;
|
||||
if (start_command(&rls))
|
||||
die("Could not launch 'git rev-list' command.");
|
||||
rls_fout = fdopen(rls.out, "r");
|
||||
while (strbuf_getline(&str, rls_fout, '\n') != EOF) {
|
||||
strbuf_trim(&str);
|
||||
if (*str.buf) {
|
||||
result = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(rls_fout);
|
||||
finish_command(&rls);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* "check_good_are_ancestors_of_bad" checks that all "good" revs are
|
||||
* ancestor of the "bad" rev.
|
||||
*
|
||||
* If that's not the case, we need to check the merge bases.
|
||||
* If a merge base must be tested by the user, its source code will be
|
||||
* checked out to be tested by the user and we will exit.
|
||||
*/
|
||||
static void check_good_are_ancestors_of_bad(const char *prefix)
|
||||
{
|
||||
const char *filename = git_path("BISECT_ANCESTORS_OK");
|
||||
struct stat st;
|
||||
int fd;
|
||||
|
||||
if (!current_bad_sha1)
|
||||
die("a bad revision is needed");
|
||||
|
||||
/* Check if file BISECT_ANCESTORS_OK exists. */
|
||||
if (!stat(filename, &st) && S_ISREG(st.st_mode))
|
||||
return;
|
||||
|
||||
/* Bisecting with no good rev is ok. */
|
||||
if (good_revs.sha1_nr == 0)
|
||||
return;
|
||||
|
||||
if (check_ancestors())
|
||||
check_merge_bases();
|
||||
|
||||
/* Create file BISECT_ANCESTORS_OK. */
|
||||
fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
|
||||
if (fd < 0)
|
||||
warning("could not create file '%s': %s",
|
||||
filename, strerror(errno));
|
||||
else
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/*
|
||||
* We use the convention that exiting with an exit code 10 means that
|
||||
* the bisection process finished successfully.
|
||||
* In this case the calling shell script should exit 0.
|
||||
*/
|
||||
int bisect_next_all(const char *prefix)
|
||||
{
|
||||
struct rev_info revs;
|
||||
struct commit_list *tried;
|
||||
int reaches = 0, all = 0, nr;
|
||||
const unsigned char *bisect_rev;
|
||||
char bisect_rev_hex[41];
|
||||
|
||||
if (read_bisect_refs())
|
||||
die("reading bisect refs failed");
|
||||
|
||||
ALLOC_GROW(rev_argv, rev_argv_nr + 1, rev_argv_alloc);
|
||||
rev_argv[rev_argv_nr++] = xstrdup("--");
|
||||
|
||||
read_bisect_paths();
|
||||
|
||||
ALLOC_GROW(rev_argv, rev_argv_nr + 1, rev_argv_alloc);
|
||||
rev_argv[rev_argv_nr++] = NULL;
|
||||
|
||||
setup_revisions(rev_argv_nr, rev_argv, revs, NULL);
|
||||
|
||||
revs->limited = 1;
|
||||
}
|
||||
|
||||
int bisect_next_vars(const char *prefix)
|
||||
{
|
||||
struct rev_info revs;
|
||||
struct rev_list_info info;
|
||||
int reaches = 0, all = 0;
|
||||
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.revs = &revs;
|
||||
info.bisect_show_flags = BISECT_SHOW_TRIED | BISECT_SHOW_STRINGED;
|
||||
check_good_are_ancestors_of_bad(prefix);
|
||||
|
||||
bisect_rev_setup(&revs, prefix);
|
||||
|
||||
if (prepare_revision_walk(&revs))
|
||||
die("revision walk setup failed");
|
||||
if (revs.tree_objects)
|
||||
mark_edges_uninteresting(revs.commits, &revs, NULL);
|
||||
bisect_common(&revs, &reaches, &all);
|
||||
|
||||
revs.commits = find_bisection(revs.commits, &reaches, &all,
|
||||
!!skipped_sha1_nr);
|
||||
revs.commits = filter_skipped(revs.commits, &tried, 0);
|
||||
|
||||
return show_bisect_vars(&info, reaches, all);
|
||||
if (!revs.commits) {
|
||||
/*
|
||||
* We should exit here only if the "bad"
|
||||
* commit is also a "skip" commit.
|
||||
*/
|
||||
exit_if_skipped_commits(tried, NULL);
|
||||
|
||||
printf("%s was both good and bad\n",
|
||||
sha1_to_hex(current_bad_sha1));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
bisect_rev = revs.commits->item->object.sha1;
|
||||
memcpy(bisect_rev_hex, sha1_to_hex(bisect_rev), 41);
|
||||
|
||||
if (!hashcmp(bisect_rev, current_bad_sha1)) {
|
||||
exit_if_skipped_commits(tried, current_bad_sha1);
|
||||
printf("%s is first bad commit\n", bisect_rev_hex);
|
||||
argv_diff_tree[2] = bisect_rev_hex;
|
||||
run_command_v_opt(argv_diff_tree, RUN_GIT_CMD);
|
||||
/* This means the bisection process succeeded. */
|
||||
exit(10);
|
||||
}
|
||||
|
||||
nr = all - reaches - 1;
|
||||
printf("Bisecting: %d revisions left to test after this "
|
||||
"(roughly %d steps)\n", nr, estimate_bisect_steps(all));
|
||||
|
||||
return bisect_checkout(bisect_rev_hex);
|
||||
}
|
||||
|
||||
|
9
bisect.h
9
bisect.h
@ -9,10 +9,13 @@ extern struct commit_list *filter_skipped(struct commit_list *list,
|
||||
struct commit_list **tried,
|
||||
int show_all);
|
||||
|
||||
extern void print_commit_list(struct commit_list *list,
|
||||
const char *format_cur,
|
||||
const char *format_last);
|
||||
|
||||
/* bisect_show_flags flags in struct rev_list_info */
|
||||
#define BISECT_SHOW_ALL (1<<0)
|
||||
#define BISECT_SHOW_TRIED (1<<1)
|
||||
#define BISECT_SHOW_STRINGED (1<<2)
|
||||
|
||||
struct rev_list_info {
|
||||
struct rev_info *revs;
|
||||
@ -24,6 +27,8 @@ struct rev_list_info {
|
||||
|
||||
extern int show_bisect_vars(struct rev_list_info *info, int reaches, int all);
|
||||
|
||||
extern int bisect_next_vars(const char *prefix);
|
||||
extern int bisect_next_all(const char *prefix);
|
||||
|
||||
extern int estimate_bisect_steps(int all);
|
||||
|
||||
#endif
|
||||
|
@ -4,24 +4,24 @@
|
||||
#include "bisect.h"
|
||||
|
||||
static const char * const git_bisect_helper_usage[] = {
|
||||
"git bisect--helper --next-vars",
|
||||
"git bisect--helper --next-all",
|
||||
NULL
|
||||
};
|
||||
|
||||
int cmd_bisect__helper(int argc, const char **argv, const char *prefix)
|
||||
{
|
||||
int next_vars = 0;
|
||||
int next_all = 0;
|
||||
struct option options[] = {
|
||||
OPT_BOOLEAN(0, "next-vars", &next_vars,
|
||||
"output next bisect step variables"),
|
||||
OPT_BOOLEAN(0, "next-all", &next_all,
|
||||
"perform 'git bisect next'"),
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
argc = parse_options(argc, argv, options, git_bisect_helper_usage, 0);
|
||||
|
||||
if (!next_vars)
|
||||
if (!next_all)
|
||||
usage_with_options(git_bisect_helper_usage, options);
|
||||
|
||||
/* next-vars */
|
||||
return bisect_next_vars(prefix);
|
||||
/* next-all */
|
||||
return bisect_next_all(prefix);
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ static inline int exp2i(int n)
|
||||
*
|
||||
* and P(2^n + x) < 0.5 means 2^n < 3x
|
||||
*/
|
||||
static int estimate_bisect_steps(int all)
|
||||
int estimate_bisect_steps(int all)
|
||||
{
|
||||
int n, x, e;
|
||||
|
||||
@ -225,20 +225,37 @@ static int estimate_bisect_steps(int all)
|
||||
return (e < 3 * x) ? n : n - 1;
|
||||
}
|
||||
|
||||
static void show_tried_revs(struct commit_list *tried, int stringed)
|
||||
void print_commit_list(struct commit_list *list,
|
||||
const char *format_cur,
|
||||
const char *format_last)
|
||||
{
|
||||
for ( ; list; list = list->next) {
|
||||
const char *format = list->next ? format_cur : format_last;
|
||||
printf(format, sha1_to_hex(list->item->object.sha1));
|
||||
}
|
||||
}
|
||||
|
||||
static void show_tried_revs(struct commit_list *tried)
|
||||
{
|
||||
printf("bisect_tried='");
|
||||
for (;tried; tried = tried->next) {
|
||||
char *format = tried->next ? "%s|" : "%s";
|
||||
printf(format, sha1_to_hex(tried->item->object.sha1));
|
||||
}
|
||||
printf(stringed ? "' &&\n" : "'\n");
|
||||
print_commit_list(tried, "%s|", "%s");
|
||||
printf("'\n");
|
||||
}
|
||||
|
||||
static void print_var_str(const char *var, const char *val)
|
||||
{
|
||||
printf("%s='%s'\n", var, val);
|
||||
}
|
||||
|
||||
static void print_var_int(const char *var, int val)
|
||||
{
|
||||
printf("%s=%d\n", var, val);
|
||||
}
|
||||
|
||||
int show_bisect_vars(struct rev_list_info *info, int reaches, int all)
|
||||
{
|
||||
int cnt, flags = info->bisect_show_flags;
|
||||
char hex[41] = "", *format;
|
||||
char hex[41] = "";
|
||||
struct commit_list *tried;
|
||||
struct rev_info *revs = info->revs;
|
||||
|
||||
@ -269,28 +286,14 @@ int show_bisect_vars(struct rev_list_info *info, int reaches, int all)
|
||||
}
|
||||
|
||||
if (flags & BISECT_SHOW_TRIED)
|
||||
show_tried_revs(tried, flags & BISECT_SHOW_STRINGED);
|
||||
format = (flags & BISECT_SHOW_STRINGED) ?
|
||||
"bisect_rev=%s &&\n"
|
||||
"bisect_nr=%d &&\n"
|
||||
"bisect_good=%d &&\n"
|
||||
"bisect_bad=%d &&\n"
|
||||
"bisect_all=%d &&\n"
|
||||
"bisect_steps=%d\n"
|
||||
:
|
||||
"bisect_rev=%s\n"
|
||||
"bisect_nr=%d\n"
|
||||
"bisect_good=%d\n"
|
||||
"bisect_bad=%d\n"
|
||||
"bisect_all=%d\n"
|
||||
"bisect_steps=%d\n";
|
||||
printf(format,
|
||||
hex,
|
||||
cnt - 1,
|
||||
all - reaches - 1,
|
||||
reaches - 1,
|
||||
all,
|
||||
estimate_bisect_steps(all));
|
||||
show_tried_revs(tried);
|
||||
|
||||
print_var_str("bisect_rev", hex);
|
||||
print_var_int("bisect_nr", cnt - 1);
|
||||
print_var_int("bisect_good", all - reaches - 1);
|
||||
print_var_int("bisect_bad", reaches - 1);
|
||||
print_var_int("bisect_all", all);
|
||||
print_var_int("bisect_steps", estimate_bisect_steps(all));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -402,6 +402,18 @@ static int cmd_parseopt(int argc, const char **argv, const char *prefix)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_sq_quote(int argc, const char **argv)
|
||||
{
|
||||
struct strbuf buf = STRBUF_INIT;
|
||||
|
||||
if (argc)
|
||||
sq_quote_argv(&buf, argv, 0);
|
||||
printf("%s\n", buf.buf);
|
||||
strbuf_release(&buf);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void die_no_single_rev(int quiet)
|
||||
{
|
||||
if (quiet)
|
||||
@ -419,6 +431,9 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
|
||||
if (argc > 1 && !strcmp("--parseopt", argv[1]))
|
||||
return cmd_parseopt(argc - 1, argv + 1, prefix);
|
||||
|
||||
if (argc > 1 && !strcmp("--sq-quote", argv[1]))
|
||||
return cmd_sq_quote(argc - 2, argv + 2);
|
||||
|
||||
prefix = setup_git_directory();
|
||||
git_config(git_default_config, NULL);
|
||||
for (i = 1; i < argc; i++) {
|
||||
|
@ -44,11 +44,7 @@ else
|
||||
fi
|
||||
|
||||
sq () {
|
||||
for sqarg
|
||||
do
|
||||
printf "%s" "$sqarg" |
|
||||
sed -e 's/'\''/'\''\\'\'''\''/g' -e 's/.*/ '\''&'\''/'
|
||||
done
|
||||
git rev-parse --sq-quote "$@"
|
||||
}
|
||||
|
||||
stop_here () {
|
||||
|
176
git-bisect.sh
176
git-bisect.sh
@ -33,16 +33,6 @@ require_work_tree
|
||||
_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
|
||||
_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
|
||||
|
||||
sq() {
|
||||
@@PERL@@ -e '
|
||||
for (@ARGV) {
|
||||
s/'\''/'\'\\\\\'\''/g;
|
||||
print " '\''$_'\''";
|
||||
}
|
||||
print "\n";
|
||||
' "$@"
|
||||
}
|
||||
|
||||
bisect_autostart() {
|
||||
test -s "$GIT_DIR/BISECT_START" || {
|
||||
echo >&2 'You need to start by "git bisect start"'
|
||||
@ -107,7 +97,7 @@ bisect_start() {
|
||||
for arg; do
|
||||
case "$arg" in --) has_double_dash=1; break ;; esac
|
||||
done
|
||||
orig_args=$(sq "$@")
|
||||
orig_args=$(git rev-parse --sq-quote "$@")
|
||||
bad_seen=0
|
||||
eval=''
|
||||
while [ $# -gt 0 ]; do
|
||||
@ -147,7 +137,7 @@ bisect_start() {
|
||||
# Write new start state.
|
||||
#
|
||||
echo "$start_head" >"$GIT_DIR/BISECT_START" &&
|
||||
sq "$@" >"$GIT_DIR/BISECT_NAMES" &&
|
||||
git rev-parse --sq-quote "$@" >"$GIT_DIR/BISECT_NAMES" &&
|
||||
eval "$eval" &&
|
||||
echo "git bisect start$orig_args" >>"$GIT_DIR/BISECT_LOG" || exit
|
||||
#
|
||||
@ -177,10 +167,6 @@ is_expected_rev() {
|
||||
test "$1" = $(cat "$GIT_DIR/BISECT_EXPECTED_REV")
|
||||
}
|
||||
|
||||
mark_expected_rev() {
|
||||
echo "$1" > "$GIT_DIR/BISECT_EXPECTED_REV"
|
||||
}
|
||||
|
||||
check_expected_revs() {
|
||||
for _rev in "$@"; do
|
||||
if ! is_expected_rev "$_rev"; then
|
||||
@ -199,7 +185,7 @@ bisect_skip() {
|
||||
*..*)
|
||||
revs=$(git rev-list "$arg") || die "Bad rev input: $arg" ;;
|
||||
*)
|
||||
revs=$(sq "$arg") ;;
|
||||
revs=$(git rev-parse --sq-quote "$arg") ;;
|
||||
esac
|
||||
all="$all $revs"
|
||||
done
|
||||
@ -279,162 +265,22 @@ bisect_auto_next() {
|
||||
bisect_next_check && bisect_next || :
|
||||
}
|
||||
|
||||
exit_if_skipped_commits () {
|
||||
_tried=$1
|
||||
_bad=$2
|
||||
if test -n "$_tried" ; then
|
||||
echo "There are only 'skip'ped commit left to test."
|
||||
echo "The first bad commit could be any of:"
|
||||
echo "$_tried" | tr '[|]' '[\012]'
|
||||
test -n "$_bad" && echo "$_bad"
|
||||
echo "We cannot bisect more!"
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
bisect_checkout() {
|
||||
_rev="$1"
|
||||
_msg="$2"
|
||||
echo "Bisecting: $_msg"
|
||||
mark_expected_rev "$_rev"
|
||||
git checkout -q "$_rev" -- || exit
|
||||
git show-branch "$_rev"
|
||||
}
|
||||
|
||||
is_among() {
|
||||
_rev="$1"
|
||||
_list="$2"
|
||||
case "$_list" in *$_rev*) return 0 ;; esac
|
||||
return 1
|
||||
}
|
||||
|
||||
handle_bad_merge_base() {
|
||||
_badmb="$1"
|
||||
_good="$2"
|
||||
if is_expected_rev "$_badmb"; then
|
||||
cat >&2 <<EOF
|
||||
The merge base $_badmb is bad.
|
||||
This means the bug has been fixed between $_badmb and [$_good].
|
||||
EOF
|
||||
exit 3
|
||||
else
|
||||
cat >&2 <<EOF
|
||||
Some good revs are not ancestor of the bad rev.
|
||||
git bisect cannot work properly in this case.
|
||||
Maybe you mistake good and bad revs?
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
handle_skipped_merge_base() {
|
||||
_mb="$1"
|
||||
_bad="$2"
|
||||
_good="$3"
|
||||
cat >&2 <<EOF
|
||||
Warning: the merge base between $_bad and [$_good] must be skipped.
|
||||
So we cannot be sure the first bad commit is between $_mb and $_bad.
|
||||
We continue anyway.
|
||||
EOF
|
||||
}
|
||||
|
||||
#
|
||||
# "check_merge_bases" checks that merge bases are not "bad".
|
||||
#
|
||||
# - If one is "good", that's good, we have nothing to do.
|
||||
# - If one is "bad", it means the user assumed something wrong
|
||||
# and we must exit.
|
||||
# - If one is "skipped", we can't know but we should warn.
|
||||
# - If we don't know, we should check it out and ask the user to test.
|
||||
#
|
||||
# In the last case we will return 1, and otherwise 0.
|
||||
#
|
||||
check_merge_bases() {
|
||||
_bad="$1"
|
||||
_good="$2"
|
||||
_skip="$3"
|
||||
for _mb in $(git merge-base --all $_bad $_good)
|
||||
do
|
||||
if is_among "$_mb" "$_good"; then
|
||||
continue
|
||||
elif test "$_mb" = "$_bad"; then
|
||||
handle_bad_merge_base "$_bad" "$_good"
|
||||
elif is_among "$_mb" "$_skip"; then
|
||||
handle_skipped_merge_base "$_mb" "$_bad" "$_good"
|
||||
else
|
||||
bisect_checkout "$_mb" "a merge base must be tested"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# "check_good_are_ancestors_of_bad" checks that all "good" revs are
|
||||
# ancestor of the "bad" rev.
|
||||
#
|
||||
# If that's not the case, we need to check the merge bases.
|
||||
# If a merge base must be tested by the user we return 1 and
|
||||
# otherwise 0.
|
||||
#
|
||||
check_good_are_ancestors_of_bad() {
|
||||
test -f "$GIT_DIR/BISECT_ANCESTORS_OK" &&
|
||||
return
|
||||
|
||||
_bad="$1"
|
||||
_good=$(echo $2 | sed -e 's/\^//g')
|
||||
_skip="$3"
|
||||
|
||||
# Bisecting with no good rev is ok
|
||||
test -z "$_good" && return
|
||||
|
||||
_side=$(git rev-list $_good ^$_bad)
|
||||
if test -n "$_side"; then
|
||||
# Return if a checkout was done
|
||||
check_merge_bases "$_bad" "$_good" "$_skip" || return
|
||||
fi
|
||||
|
||||
: > "$GIT_DIR/BISECT_ANCESTORS_OK"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
bisect_next() {
|
||||
case "$#" in 0) ;; *) usage ;; esac
|
||||
bisect_autostart
|
||||
bisect_next_check good
|
||||
|
||||
# Get bad, good and skipped revs
|
||||
bad=$(git rev-parse --verify refs/bisect/bad) &&
|
||||
good=$(git for-each-ref --format='^%(objectname)' \
|
||||
"refs/bisect/good-*" | tr '\012' ' ') &&
|
||||
skip=$(git for-each-ref --format='%(objectname)' \
|
||||
"refs/bisect/skip-*" | tr '\012' ' ') || exit
|
||||
# Perform all bisection computation, display and checkout
|
||||
git bisect--helper --next-all
|
||||
res=$?
|
||||
|
||||
# Maybe some merge bases must be tested first
|
||||
check_good_are_ancestors_of_bad "$bad" "$good" "$skip"
|
||||
# Return now if a checkout has already been done
|
||||
test "$?" -eq "1" && return
|
||||
# Check if we should exit because bisection is finished
|
||||
test $res -eq 10 && exit 0
|
||||
|
||||
# Get bisection information
|
||||
eval=$(eval "git bisect--helper --next-vars") &&
|
||||
eval "$eval" || exit
|
||||
# Check for an error in the bisection process
|
||||
test $res -ne 0 && exit $res
|
||||
|
||||
if [ -z "$bisect_rev" ]; then
|
||||
# We should exit here only if the "bad"
|
||||
# commit is also a "skip" commit (see above).
|
||||
exit_if_skipped_commits "$bisect_tried"
|
||||
echo "$bad was both good and bad"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$bisect_rev" = "$bad" ]; then
|
||||
exit_if_skipped_commits "$bisect_tried" "$bad"
|
||||
echo "$bisect_rev is first bad commit"
|
||||
git diff-tree --pretty $bisect_rev
|
||||
exit 0
|
||||
fi
|
||||
|
||||
bisect_checkout "$bisect_rev" "$bisect_nr revisions left to test after this (roughly $bisect_steps steps)"
|
||||
return 0
|
||||
}
|
||||
|
||||
bisect_visualize() {
|
||||
|
Loading…
Reference in New Issue
Block a user