a4e9d71edb
In a path-limited bisection, when the $bad commit is not changing the limited path, and the number of suspects is 1, the code miscounted and returned $bad from find_bisection(), which is not marked with TREECHANGE. This is of course filtered by the output routine, resulting in an empty output, in turn causing git-bisect driver to say "$bad was both good and bad". Illustration. Suppose you have these four commits, and only C changes path P. You know D is bad and A is good. A---B---C*--D git-bisect driver runs this to find a bisection point: $ git rev-list --bisect A..D -- P which calls find_bisection() with B, C and D. The set of commits that is given to this function is the same set of commits as rev-list without --bisect option and pathspec returns. Among them, only C is marked with TREECHANGE. Let's call the set of commits given to find_bisection() that are marked with TREECHANGE (or all of them if no path limiter is in effect) "the bisect set". In the above example, the size of the bisect set is 1 (contains only "C"). For each commit in its input, find_bisection() computes the number of commits it can reach in the bisect set. For a commit in the bisect set, this number includes itself, so the number is 1 or more. This number is called "depth", and computed by count_distance() function. When you have a bisect set of N commits, and a commit has depth D, how good is your bisection if you returned that commit? How good this bisection is can be measured by how many commits are effectively tested "together" by testing one commit. Currently you have (N-1) untested commits (the tip of the bisect set, although it is included in the bisect set, is already known to be bad). If the commit with depth D turns out to be bad, then your next bisect set will have D commits and you will have (D-1) untested commits left, which means you tested (N-1)-(D-1) = (N-D) commits with this bisection. If it turns out to be good, then your next bisect set will have (N-D) commits, and you will have (N-D-1) untested commits left, which means you tested (N-1)-(N-D-1) = D commits with this bisection. Therefore, the goodness of this bisection is is min(N-D, D), and find_bisection() function tries to find a commit that maximizes this, by initializing "closest" variable to 0 and whenever a commit with the goodness that is larger than the current "closest" is found, that commit and its goodness are remembered by updating "closest" variable. The "the commit with the best goodness so far" is kept in "best" variable, and is initialized to a commit that happens to be at the beginning of the list of commits given to this function (which may or may not be in the bisect set when path-limit is in use). However, when N is 1, then the sole tree-changing commit has depth of 1, and min(N-D, D) evaluates to 0. This is not larger than the initial value of "closest", and the "so far the best one" commit is never replaced in the loop. When path-limit is not in use, this is not a problem, as any commit in the input set is tree-changing. But when path-limit is in use, and when the starting "bad" commit does not change the specified path, it is not correct to return it. Signed-off-by: Junio C Hamano <junkio@cox.net>
295 lines
6.4 KiB
C
295 lines
6.4 KiB
C
#include "cache.h"
|
|
#include "refs.h"
|
|
#include "tag.h"
|
|
#include "commit.h"
|
|
#include "tree.h"
|
|
#include "blob.h"
|
|
#include "tree-walk.h"
|
|
#include "diff.h"
|
|
#include "revision.h"
|
|
#include "list-objects.h"
|
|
#include "builtin.h"
|
|
|
|
/* bits #0-15 in revision.h */
|
|
|
|
#define COUNTED (1u<<16)
|
|
|
|
static const char rev_list_usage[] =
|
|
"git-rev-list [OPTION] <commit-id>... [ -- paths... ]\n"
|
|
" limiting output:\n"
|
|
" --max-count=nr\n"
|
|
" --max-age=epoch\n"
|
|
" --min-age=epoch\n"
|
|
" --sparse\n"
|
|
" --no-merges\n"
|
|
" --remove-empty\n"
|
|
" --all\n"
|
|
" --stdin\n"
|
|
" ordering output:\n"
|
|
" --topo-order\n"
|
|
" --date-order\n"
|
|
" formatting output:\n"
|
|
" --parents\n"
|
|
" --objects | --objects-edge\n"
|
|
" --unpacked\n"
|
|
" --header | --pretty\n"
|
|
" --abbrev=nr | --no-abbrev\n"
|
|
" --abbrev-commit\n"
|
|
" special purpose:\n"
|
|
" --bisect"
|
|
;
|
|
|
|
static struct rev_info revs;
|
|
|
|
static int bisect_list;
|
|
static int show_timestamp;
|
|
static int hdr_termination;
|
|
static const char *header_prefix;
|
|
|
|
static void show_commit(struct commit *commit)
|
|
{
|
|
if (show_timestamp)
|
|
printf("%lu ", commit->date);
|
|
if (header_prefix)
|
|
fputs(header_prefix, stdout);
|
|
if (commit->object.flags & BOUNDARY)
|
|
putchar('-');
|
|
else if (revs.left_right) {
|
|
if (commit->object.flags & SYMMETRIC_LEFT)
|
|
putchar('<');
|
|
else
|
|
putchar('>');
|
|
}
|
|
if (revs.abbrev_commit && revs.abbrev)
|
|
fputs(find_unique_abbrev(commit->object.sha1, revs.abbrev),
|
|
stdout);
|
|
else
|
|
fputs(sha1_to_hex(commit->object.sha1), stdout);
|
|
if (revs.parents) {
|
|
struct commit_list *parents = commit->parents;
|
|
while (parents) {
|
|
struct object *o = &(parents->item->object);
|
|
parents = parents->next;
|
|
if (o->flags & TMP_MARK)
|
|
continue;
|
|
printf(" %s", sha1_to_hex(o->sha1));
|
|
o->flags |= TMP_MARK;
|
|
}
|
|
/* TMP_MARK is a general purpose flag that can
|
|
* be used locally, but the user should clean
|
|
* things up after it is done with them.
|
|
*/
|
|
for (parents = commit->parents;
|
|
parents;
|
|
parents = parents->next)
|
|
parents->item->object.flags &= ~TMP_MARK;
|
|
}
|
|
if (revs.commit_format == CMIT_FMT_ONELINE)
|
|
putchar(' ');
|
|
else
|
|
putchar('\n');
|
|
|
|
if (revs.verbose_header) {
|
|
static char pretty_header[16384];
|
|
pretty_print_commit(revs.commit_format, commit, ~0,
|
|
pretty_header, sizeof(pretty_header),
|
|
revs.abbrev, NULL, NULL, revs.relative_date);
|
|
printf("%s%c", pretty_header, hdr_termination);
|
|
}
|
|
fflush(stdout);
|
|
if (commit->parents) {
|
|
free_commit_list(commit->parents);
|
|
commit->parents = NULL;
|
|
}
|
|
free(commit->buffer);
|
|
commit->buffer = NULL;
|
|
}
|
|
|
|
static void show_object(struct object_array_entry *p)
|
|
{
|
|
/* An object with name "foo\n0000000..." can be used to
|
|
* confuse downstream git-pack-objects very badly.
|
|
*/
|
|
const char *ep = strchr(p->name, '\n');
|
|
if (ep) {
|
|
printf("%s %.*s\n", sha1_to_hex(p->item->sha1),
|
|
(int) (ep - p->name),
|
|
p->name);
|
|
}
|
|
else
|
|
printf("%s %s\n", sha1_to_hex(p->item->sha1), p->name);
|
|
}
|
|
|
|
static void show_edge(struct commit *commit)
|
|
{
|
|
printf("-%s\n", sha1_to_hex(commit->object.sha1));
|
|
}
|
|
|
|
/*
|
|
* This is a truly stupid algorithm, but it's only
|
|
* used for bisection, and we just don't care enough.
|
|
*
|
|
* We care just barely enough to avoid recursing for
|
|
* non-merge entries.
|
|
*/
|
|
static int count_distance(struct commit_list *entry)
|
|
{
|
|
int nr = 0;
|
|
|
|
while (entry) {
|
|
struct commit *commit = entry->item;
|
|
struct commit_list *p;
|
|
|
|
if (commit->object.flags & (UNINTERESTING | COUNTED))
|
|
break;
|
|
if (!revs.prune_fn || (commit->object.flags & TREECHANGE))
|
|
nr++;
|
|
commit->object.flags |= COUNTED;
|
|
p = commit->parents;
|
|
entry = p;
|
|
if (p) {
|
|
p = p->next;
|
|
while (p) {
|
|
nr += count_distance(p);
|
|
p = p->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nr;
|
|
}
|
|
|
|
static void clear_distance(struct commit_list *list)
|
|
{
|
|
while (list) {
|
|
struct commit *commit = list->item;
|
|
commit->object.flags &= ~COUNTED;
|
|
list = list->next;
|
|
}
|
|
}
|
|
|
|
static struct commit_list *find_bisection(struct commit_list *list)
|
|
{
|
|
int nr, closest;
|
|
struct commit_list *p, *best;
|
|
|
|
nr = 0;
|
|
p = list;
|
|
while (p) {
|
|
if (!revs.prune_fn || (p->item->object.flags & TREECHANGE))
|
|
nr++;
|
|
p = p->next;
|
|
}
|
|
closest = -1;
|
|
best = list;
|
|
|
|
for (p = list; p; p = p->next) {
|
|
int distance;
|
|
|
|
if (revs.prune_fn && !(p->item->object.flags & TREECHANGE))
|
|
continue;
|
|
|
|
distance = count_distance(p);
|
|
clear_distance(list);
|
|
if (nr - distance < distance)
|
|
distance = nr - distance;
|
|
if (distance > closest) {
|
|
best = p;
|
|
closest = distance;
|
|
}
|
|
}
|
|
if (best)
|
|
best->next = NULL;
|
|
return best;
|
|
}
|
|
|
|
static void read_revisions_from_stdin(struct rev_info *revs)
|
|
{
|
|
char line[1000];
|
|
|
|
while (fgets(line, sizeof(line), stdin) != NULL) {
|
|
int len = strlen(line);
|
|
if (line[len - 1] == '\n')
|
|
line[--len] = 0;
|
|
if (!len)
|
|
break;
|
|
if (line[0] == '-')
|
|
die("options not supported in --stdin mode");
|
|
if (handle_revision_arg(line, revs, 0, 1))
|
|
die("bad revision '%s'", line);
|
|
}
|
|
}
|
|
|
|
int cmd_rev_list(int argc, const char **argv, const char *prefix)
|
|
{
|
|
struct commit_list *list;
|
|
int i;
|
|
int read_from_stdin = 0;
|
|
|
|
git_config(git_default_config);
|
|
init_revisions(&revs, prefix);
|
|
revs.abbrev = 0;
|
|
revs.commit_format = CMIT_FMT_UNSPECIFIED;
|
|
argc = setup_revisions(argc, argv, &revs, NULL);
|
|
|
|
for (i = 1 ; i < argc; i++) {
|
|
const char *arg = argv[i];
|
|
|
|
if (!strcmp(arg, "--header")) {
|
|
revs.verbose_header = 1;
|
|
continue;
|
|
}
|
|
if (!strcmp(arg, "--timestamp")) {
|
|
show_timestamp = 1;
|
|
continue;
|
|
}
|
|
if (!strcmp(arg, "--bisect")) {
|
|
bisect_list = 1;
|
|
continue;
|
|
}
|
|
if (!strcmp(arg, "--stdin")) {
|
|
if (read_from_stdin++)
|
|
die("--stdin given twice?");
|
|
read_revisions_from_stdin(&revs);
|
|
continue;
|
|
}
|
|
usage(rev_list_usage);
|
|
|
|
}
|
|
if (revs.commit_format != CMIT_FMT_UNSPECIFIED) {
|
|
/* The command line has a --pretty */
|
|
hdr_termination = '\n';
|
|
if (revs.commit_format == CMIT_FMT_ONELINE)
|
|
header_prefix = "";
|
|
else
|
|
header_prefix = "commit ";
|
|
}
|
|
else if (revs.verbose_header)
|
|
/* Only --header was specified */
|
|
revs.commit_format = CMIT_FMT_RAW;
|
|
|
|
list = revs.commits;
|
|
|
|
if ((!list &&
|
|
(!(revs.tag_objects||revs.tree_objects||revs.blob_objects) &&
|
|
!revs.pending.nr)) ||
|
|
revs.diff)
|
|
usage(rev_list_usage);
|
|
|
|
save_commit_buffer = revs.verbose_header || revs.grep_filter;
|
|
track_object_refs = 0;
|
|
if (bisect_list)
|
|
revs.limited = 1;
|
|
|
|
prepare_revision_walk(&revs);
|
|
if (revs.tree_objects)
|
|
mark_edges_uninteresting(revs.commits, &revs, show_edge);
|
|
|
|
if (bisect_list)
|
|
revs.commits = find_bisection(revs.commits);
|
|
|
|
traverse_commit_list(&revs, show_commit, show_object);
|
|
|
|
return 0;
|
|
}
|