diff --git a/Makefile b/Makefile index 0976a69b4ca424..f3f50b82d46097 100644 --- a/Makefile +++ b/Makefile @@ -1256,6 +1256,7 @@ LIB_OBJS += prune-packed.o LIB_OBJS += pseudo-merge.o LIB_OBJS += quote.o LIB_OBJS += range-diff.o +LIB_OBJS += range-set.o LIB_OBJS += reachable.o LIB_OBJS += read-cache.o LIB_OBJS += rebase-interactive.o @@ -1533,6 +1534,7 @@ CLAR_TEST_SUITES += u-oid-array CLAR_TEST_SUITES += u-oidmap CLAR_TEST_SUITES += u-oidtree CLAR_TEST_SUITES += u-prio-queue +CLAR_TEST_SUITES += u-range-set CLAR_TEST_SUITES += u-reftable-basics CLAR_TEST_SUITES += u-reftable-block CLAR_TEST_SUITES += u-reftable-merged diff --git a/diff.c b/diff.c index 5a584fa1d569e7..ac15341f25ff93 100644 --- a/diff.c +++ b/diff.c @@ -17,6 +17,7 @@ #include "quote.h" #include "diff.h" #include "diffcore.h" +#include "range-set.h" #include "delta.h" #include "hex.h" #include "xdiff-interface.h" diff --git a/diffcore.h b/diffcore.h index d75038d1b3a1b2..9053aa288f33c8 100644 --- a/diffcore.h +++ b/diffcore.h @@ -9,6 +9,7 @@ struct diff_options; struct mem_pool; struct oid_array; +struct range_set; struct repository; struct strintmap; struct strmap; @@ -19,17 +20,6 @@ struct userdiff_driver; * in anything else. */ -/* A range [start, end). Lines are numbered starting at 0. */ -struct range { - long start, end; -}; - -/* A set of ranges. The ranges must always be disjoint and sorted. */ -struct range_set { - unsigned int alloc, nr; - struct range *ranges; -}; - /* We internally use unsigned short as the score value, * and rely on an int capable to hold 32-bits. -B can take * -Bmerge_score/break_score format and the two scores are diff --git a/line-log.c b/line-log.c index 5fc75ae275e03a..03e5f2320ea7cb 100644 --- a/line-log.c +++ b/line-log.c @@ -2,6 +2,7 @@ #include "git-compat-util.h" #include "diffcore.h" +#include "range-set.h" #include "line-range.h" #include "hex.h" #include "tag.h" @@ -19,226 +20,30 @@ #include "bloom.h" #include "tree-walk.h" -static void range_set_grow(struct range_set *rs, size_t extra) -{ - ALLOC_GROW(rs->ranges, rs->nr + extra, rs->alloc); -} - -/* Either initialization would be fine */ -#define RANGE_SET_INIT {0} - -void range_set_init(struct range_set *rs, size_t prealloc) -{ - rs->alloc = rs->nr = 0; - rs->ranges = NULL; - if (prealloc) - range_set_grow(rs, prealloc); -} - -void range_set_release(struct range_set *rs) -{ - FREE_AND_NULL(rs->ranges); - rs->alloc = rs->nr = 0; -} - -/* dst must be uninitialized! */ -static void range_set_copy(struct range_set *dst, struct range_set *src) -{ - range_set_init(dst, src->nr); - COPY_ARRAY(dst->ranges, src->ranges, src->nr); - dst->nr = src->nr; -} - -static void range_set_move(struct range_set *dst, struct range_set *src) -{ - range_set_release(dst); - dst->ranges = src->ranges; - dst->nr = src->nr; - dst->alloc = src->alloc; - src->ranges = NULL; - src->alloc = src->nr = 0; -} - -/* tack on a _new_ range _at the end_ */ -void range_set_append_unsafe(struct range_set *rs, long a, long b) -{ - assert(a <= b); - range_set_grow(rs, 1); - rs->ranges[rs->nr].start = a; - rs->ranges[rs->nr].end = b; - rs->nr++; -} - -void range_set_append(struct range_set *rs, long a, long b) -{ - assert(rs->nr == 0 || rs->ranges[rs->nr-1].end <= a); - range_set_append_unsafe(rs, a, b); -} - -static int range_cmp(const void *_r, const void *_s) -{ - const struct range *r = _r; - const struct range *s = _s; - - /* this could be simply 'return r.start-s.start', but for the types */ - if (r->start == s->start) - return 0; - if (r->start < s->start) - return -1; - return 1; -} - -/* - * Check that the ranges are non-empty, sorted and non-overlapping - */ -static void range_set_check_invariants(struct range_set *rs) -{ - unsigned int i; - - if (!rs) - return; - - if (rs->nr) - assert(rs->ranges[0].start < rs->ranges[0].end); - - for (i = 1; i < rs->nr; i++) { - assert(rs->ranges[i-1].end < rs->ranges[i].start); - assert(rs->ranges[i].start < rs->ranges[i].end); - } -} - -/* - * In-place pass of sorting and merging the ranges in the range set, - * to establish the invariants when we get the ranges from the user - */ -void sort_and_merge_range_set(struct range_set *rs) -{ - unsigned int i; - unsigned int o = 0; /* output cursor */ - - QSORT(rs->ranges, rs->nr, range_cmp); - - for (i = 0; i < rs->nr; i++) { - if (rs->ranges[i].start == rs->ranges[i].end) - continue; - if (o > 0 && rs->ranges[i].start <= rs->ranges[o-1].end) { - if (rs->ranges[o-1].end < rs->ranges[i].end) - rs->ranges[o-1].end = rs->ranges[i].end; - } else { - rs->ranges[o].start = rs->ranges[i].start; - rs->ranges[o].end = rs->ranges[i].end; - o++; - } - } - assert(o <= rs->nr); - rs->nr = o; - - range_set_check_invariants(rs); -} - /* - * Union of range sets (i.e., sets of line numbers). Used to merge - * them when searches meet at a common ancestor. + * "git log -L": follow a set of line ranges through history. + * + * Each interesting commit carries a "struct line_log_data" (a per-path + * list of tracked "struct range_set" line ranges) as a decoration in + * rev->line_log_data. Walking from a commit to its parent maps those + * ranges back across the commit's diff (process_ranges_ordinary_commit() + * via range_set_map_across_diff()), so each parent inherits the ranges + * it is responsible for. * - * This is also where the ranges are consolidated into canonical form: - * overlapping and adjacent ranges are merged, and empty ranges are - * removed. + * The walk runs in one of two modes: + * + * - Eager, under parent rewriting (want_ancestry(), e.g. --graph): + * line_log_filter() processes every commit in prepare_revision_walk() + * and rewrites the parent links. + * + * - Lazy, for a plain "git log -L": each commit is processed on demand + * during the walk, from get_commit_action() in revision.c. + * + * Both share line_log_process_commit(), which processes + * one commit and reports whether it changed a tracked range. */ -static void range_set_union(struct range_set *out, - struct range_set *a, struct range_set *b) -{ - unsigned int i = 0, j = 0; - struct range *ra = a->ranges; - struct range *rb = b->ranges; - /* cannot make an alias of out->ranges: it may change during grow */ - - assert(out->nr == 0); - while (i < a->nr || j < b->nr) { - struct range *new_range; - if (i < a->nr && j < b->nr) { - if (ra[i].start < rb[j].start) - new_range = &ra[i++]; - else if (ra[i].start > rb[j].start) - new_range = &rb[j++]; - else if (ra[i].end < rb[j].end) - new_range = &ra[i++]; - else - new_range = &rb[j++]; - } else if (i < a->nr) /* b exhausted */ - new_range = &ra[i++]; - else /* a exhausted */ - new_range = &rb[j++]; - if (new_range->start == new_range->end) - ; /* empty range */ - else if (!out->nr || out->ranges[out->nr-1].end < new_range->start) { - range_set_grow(out, 1); - out->ranges[out->nr].start = new_range->start; - out->ranges[out->nr].end = new_range->end; - out->nr++; - } else if (out->ranges[out->nr-1].end < new_range->end) { - out->ranges[out->nr-1].end = new_range->end; - } - } -} -/* - * Difference of range sets (out = a \ b). Pass the "interesting" - * ranges as 'a' and the target side of the diff as 'b': it removes - * the ranges for which the commit is responsible. - */ -static void range_set_difference(struct range_set *out, - struct range_set *a, struct range_set *b) -{ - unsigned int i, j = 0; - for (i = 0; i < a->nr; i++) { - long start = a->ranges[i].start; - long end = a->ranges[i].end; - while (start < end) { - while (j < b->nr && start >= b->ranges[j].end) - /* - * a: |------- - * b: ------| - */ - j++; - if (j >= b->nr || end <= b->ranges[j].start) { - /* - * b exhausted, or - * a: ----| - * b: |---- - */ - range_set_append(out, start, end); - break; - } - if (start >= b->ranges[j].start) { - /* - * a: |--???? - * b: |------| - */ - start = b->ranges[j].end; - } else if (end > b->ranges[j].start) { - /* - * a: |-----| - * b: |--????? - */ - if (start < b->ranges[j].start) - range_set_append(out, start, b->ranges[j].start); - start = b->ranges[j].end; - } - } - } -} - -static void diff_ranges_init(struct diff_ranges *diff) -{ - range_set_init(&diff->parent, 0); - range_set_init(&diff->target, 0); -} - -static void diff_ranges_release(struct diff_ranges *diff) -{ - range_set_release(&diff->parent); - range_set_release(&diff->target); -} +/* line_log_data: the sorted per-path list of tracked line ranges. */ static void line_log_data_init(struct line_log_data *r) { @@ -312,168 +117,119 @@ static void line_log_data_insert(struct line_log_data **list, } } -struct collect_diff_cbdata { - struct diff_ranges *diff; -}; - -static int collect_diff_cb(long start_a, long count_a, - long start_b, long count_b, - void *data) +static struct line_log_data *line_log_data_copy_one(struct line_log_data *r) { - struct collect_diff_cbdata *d = data; - - if (count_a >= 0) - range_set_append(&d->diff->parent, start_a, start_a + count_a); - if (count_b >= 0) - range_set_append(&d->diff->target, start_b, start_b + count_b); - - return 0; -} + struct line_log_data *ret = xmalloc(sizeof(*ret)); -static int collect_diff(mmfile_t *parent, mmfile_t *target, struct diff_ranges *out) -{ - struct collect_diff_cbdata cbdata = {NULL}; - xpparam_t xpp; - xdemitconf_t xecfg; - xdemitcb_t ecb; + assert(r); + line_log_data_init(ret); + range_set_copy(&ret->ranges, &r->ranges); - memset(&xpp, 0, sizeof(xpp)); - memset(&xecfg, 0, sizeof(xecfg)); - xecfg.ctxlen = xecfg.interhunkctxlen = 0; + ret->path = xstrdup(r->path); - cbdata.diff = out; - xecfg.hunk_func = collect_diff_cb; - memset(&ecb, 0, sizeof(ecb)); - ecb.priv = &cbdata; - return xdi_diff(parent, target, &xpp, &xecfg, &ecb); + return ret; } -/* - * These are handy for debugging. Removing them with #if 0 silences - * the "unused function" warning. - */ -#if 0 -static void dump_range_set(struct range_set *rs, const char *desc) +static struct line_log_data * +line_log_data_copy(struct line_log_data *r) { - int i; - printf("range set %s (%d items):\n", desc, rs->nr); - for (i = 0; i < rs->nr; i++) - printf("\t[%ld,%ld]\n", rs->ranges[i].start, rs->ranges[i].end); -} + struct line_log_data *ret = NULL; + struct line_log_data *tmp = NULL, *prev = NULL; -static void dump_line_log_data(struct line_log_data *r) -{ - char buf[4096]; + assert(r); + ret = tmp = prev = line_log_data_copy_one(r); + r = r->next; while (r) { - snprintf(buf, 4096, "file %s\n", r->path); - dump_range_set(&r->ranges, buf); + tmp = line_log_data_copy_one(r); + prev->next = tmp; + prev = tmp; r = r->next; } + + return ret; } -static void dump_diff_ranges(struct diff_ranges *diff, const char *desc) +/* merge two range sets across files */ +static struct line_log_data *line_log_data_merge(struct line_log_data *a, + struct line_log_data *b) { - int i; - assert(diff->parent.nr == diff->target.nr); - printf("diff ranges %s (%d items):\n", desc, diff->parent.nr); - printf("\tparent\ttarget\n"); - for (i = 0; i < diff->parent.nr; i++) { - printf("\t[%ld,%ld]\t[%ld,%ld]\n", - diff->parent.ranges[i].start, - diff->parent.ranges[i].end, - diff->target.ranges[i].start, - diff->target.ranges[i].end); - } -} -#endif + struct line_log_data *head = NULL, **pp = &head; + while (a || b) { + struct line_log_data *src; + struct line_log_data *src2 = NULL; + struct line_log_data *d; + int cmp; + if (!a) + cmp = 1; + else if (!b) + cmp = -1; + else + cmp = strcmp(a->path, b->path); + if (cmp < 0) { + src = a; + a = a->next; + } else if (cmp == 0) { + src = a; + a = a->next; + src2 = b; + b = b->next; + } else { + src = b; + b = b->next; + } + d = xmalloc(sizeof(struct line_log_data)); + line_log_data_init(d); + d->path = xstrdup(src->path); + *pp = d; + pp = &d->next; + if (src2) + range_set_union(&d->ranges, &src->ranges, &src2->ranges); + else + range_set_copy(&d->ranges, &src->ranges); + } -static int ranges_overlap(struct range *a, struct range *b) -{ - return !(a->end <= b->start || b->end <= a->start); + return head; } -/* - * Given a diff and the set of interesting ranges, determine all hunks - * of the diff which touch (overlap) at least one of the interesting - * ranges in the target. - */ -static void diff_ranges_filter_touched(struct diff_ranges *out, - struct diff_ranges *diff, - struct range_set *rs) +static void add_line_range(struct rev_info *revs, struct commit *commit, + struct line_log_data *range) { - unsigned int i, j = 0; + struct line_log_data *old_line = NULL; + struct line_log_data *new_line = NULL; - assert(out->target.nr == 0); + old_line = lookup_decoration(&revs->line_log_data, &commit->object); + if (old_line && range) { + new_line = line_log_data_merge(old_line, range); + free_line_log_data(old_line); + } else if (range) + new_line = line_log_data_copy(range); - for (i = 0; i < diff->target.nr; i++) { - while (diff->target.ranges[i].start >= rs->ranges[j].end) { - j++; - if (j == rs->nr) - return; - } - if (ranges_overlap(&diff->target.ranges[i], &rs->ranges[j])) { - range_set_append(&out->parent, - diff->parent.ranges[i].start, - diff->parent.ranges[i].end); - range_set_append(&out->target, - diff->target.ranges[i].start, - diff->target.ranges[i].end); - } - } + if (new_line) + add_decoration(&revs->line_log_data, &commit->object, new_line); } -/* - * Adjust the line counts in 'rs' to account for the lines - * added/removed in the diff. - */ -static void range_set_shift_diff(struct range_set *out, - struct range_set *rs, - struct diff_ranges *diff) +static void clear_commit_line_range(struct rev_info *revs, struct commit *commit) { - unsigned int i, j = 0; - long offset = 0; - struct range *src = rs->ranges; - struct range *target = diff->target.ranges; - struct range *parent = diff->parent.ranges; - - for (i = 0; i < rs->nr; i++) { - while (j < diff->target.nr && src[i].start >= target[j].start) { - offset += (parent[j].end-parent[j].start) - - (target[j].end-target[j].start); - j++; - } - range_set_append(out, src[i].start+offset, src[i].end+offset); - } + struct line_log_data *r; + r = lookup_decoration(&revs->line_log_data, &commit->object); + if (!r) + return; + free_line_log_data(r); + add_decoration(&revs->line_log_data, &commit->object, NULL); } -/* - * Given a diff and the set of interesting ranges, map the ranges - * across the diff. That is: observe that the target commit takes - * blame for all the + (target-side) ranges. So for every pair of - * ranges in the diff that was touched, we remove the latter and add - * its parent side. - */ -static void range_set_map_across_diff(struct range_set *out, - struct range_set *rs, - struct diff_ranges *diff, - struct diff_ranges **touched_out) +static struct line_log_data *lookup_line_range(struct rev_info *revs, + struct commit *commit) { - struct diff_ranges *touched = xmalloc(sizeof(*touched)); - struct range_set tmp1 = RANGE_SET_INIT; - struct range_set tmp2 = RANGE_SET_INIT; - - diff_ranges_init(touched); - diff_ranges_filter_touched(touched, diff, rs); - range_set_difference(&tmp1, rs, &touched->target); - range_set_shift_diff(&tmp2, &tmp1, diff); - range_set_union(out, &tmp2, &touched->parent); - range_set_release(&tmp1); - range_set_release(&tmp2); - - *touched_out = touched; + return lookup_decoration(&revs->line_log_data, &commit->object); } +/* + * Parsing "-L" arguments into an initial set of tracked ranges and the + * pathspec that limits the diff. line_log_init() is the entry point. + */ + static struct commit *check_single_commit(struct rev_info *revs) { struct object *commit = NULL; @@ -510,8 +266,6 @@ static void fill_blob_sha1(struct repository *r, struct commit *commit, if (get_tree_entry(r, &commit->object.oid, spec->path, &oid, &mode)) die("There is no path %s in the commit", spec->path); fill_filespec(spec, &oid, 1, mode); - - return; } static void fill_line_ends(struct repository *r, @@ -628,122 +382,6 @@ parse_lines(struct repository *r, struct commit *commit, return ranges; } -static struct line_log_data *line_log_data_copy_one(struct line_log_data *r) -{ - struct line_log_data *ret = xmalloc(sizeof(*ret)); - - assert(r); - line_log_data_init(ret); - range_set_copy(&ret->ranges, &r->ranges); - - ret->path = xstrdup(r->path); - - return ret; -} - -static struct line_log_data * -line_log_data_copy(struct line_log_data *r) -{ - struct line_log_data *ret = NULL; - struct line_log_data *tmp = NULL, *prev = NULL; - - assert(r); - ret = tmp = prev = line_log_data_copy_one(r); - r = r->next; - while (r) { - tmp = line_log_data_copy_one(r); - prev->next = tmp; - prev = tmp; - r = r->next; - } - - return ret; -} - -/* merge two range sets across files */ -static struct line_log_data *line_log_data_merge(struct line_log_data *a, - struct line_log_data *b) -{ - struct line_log_data *head = NULL, **pp = &head; - - while (a || b) { - struct line_log_data *src; - struct line_log_data *src2 = NULL; - struct line_log_data *d; - int cmp; - if (!a) - cmp = 1; - else if (!b) - cmp = -1; - else - cmp = strcmp(a->path, b->path); - if (cmp < 0) { - src = a; - a = a->next; - } else if (cmp == 0) { - src = a; - a = a->next; - src2 = b; - b = b->next; - } else { - src = b; - b = b->next; - } - d = xmalloc(sizeof(struct line_log_data)); - line_log_data_init(d); - d->path = xstrdup(src->path); - *pp = d; - pp = &d->next; - if (src2) - range_set_union(&d->ranges, &src->ranges, &src2->ranges); - else - range_set_copy(&d->ranges, &src->ranges); - } - - return head; -} - -static void add_line_range(struct rev_info *revs, struct commit *commit, - struct line_log_data *range) -{ - struct line_log_data *old_line = NULL; - struct line_log_data *new_line = NULL; - - old_line = lookup_decoration(&revs->line_log_data, &commit->object); - if (old_line && range) { - new_line = line_log_data_merge(old_line, range); - free_line_log_data(old_line); - } else if (range) - new_line = line_log_data_copy(range); - - if (new_line) - add_decoration(&revs->line_log_data, &commit->object, new_line); -} - -static void clear_commit_line_range(struct rev_info *revs, struct commit *commit) -{ - struct line_log_data *r; - r = lookup_decoration(&revs->line_log_data, &commit->object); - if (!r) - return; - free_line_log_data(r); - add_decoration(&revs->line_log_data, &commit->object, NULL); -} - -static struct line_log_data *lookup_line_range(struct rev_info *revs, - struct commit *commit) -{ - struct line_log_data *ret = NULL; - struct line_log_data *d; - - ret = lookup_decoration(&revs->line_log_data, &commit->object); - - for (d = ret; d; d = d->next) - range_set_check_invariants(&d->ranges); - - return ret; -} - static int same_paths_in_pathspec_and_range(struct pathspec *pathspec, struct line_log_data *range) { @@ -788,6 +426,47 @@ void line_log_init(struct rev_info *rev, const char *prefix, struct string_list free_line_log_data(range); } +/* + * Compute one commit's diff against a parent as a set of changed line + * ranges (collect_diff), then walk the tracked ranges back across it. + */ + +struct collect_diff_cbdata { + struct diff_ranges *diff; +}; + +static int collect_diff_cb(long start_a, long count_a, + long start_b, long count_b, + void *data) +{ + struct collect_diff_cbdata *d = data; + + if (count_a >= 0) + range_set_append(&d->diff->parent, start_a, start_a + count_a); + if (count_b >= 0) + range_set_append(&d->diff->target, start_b, start_b + count_b); + + return 0; +} + +static int collect_diff(mmfile_t *parent, mmfile_t *target, struct diff_ranges *out) +{ + struct collect_diff_cbdata cbdata = {NULL}; + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + + memset(&xpp, 0, sizeof(xpp)); + memset(&xecfg, 0, sizeof(xecfg)); + xecfg.ctxlen = xecfg.interhunkctxlen = 0; + + cbdata.diff = out; + xecfg.hunk_func = collect_diff_cb; + memset(&ecb, 0, sizeof(ecb)); + ecb.priv = &cbdata; + return xdi_diff(parent, target, &xpp, &xecfg, &ecb); +} + static void move_diff_queue(struct diff_queue_struct *dst, struct diff_queue_struct *src) { @@ -829,11 +508,8 @@ static inline int diff_might_be_rename(void) { int i; for (i = 0; i < diff_queued_diff.nr; i++) - if (!DIFF_FILE_VALID(diff_queued_diff.queue[i]->one)) { - /* fprintf(stderr, "diff_might_be_rename found creation of: %s\n", */ - /* diff_queued_diff.queue[i]->two->path); */ + if (!DIFF_FILE_VALID(diff_queued_diff.queue[i]->one)) return 1; - } return 0; } @@ -1049,6 +725,8 @@ static int bloom_filter_check(struct rev_info *rev, return result; } +/* Walking history: map each commit's tracked ranges to its parent(s). */ + static int process_ranges_ordinary_commit(struct rev_info *rev, struct commit *commit, struct line_log_data *range) { @@ -1134,7 +812,15 @@ static int process_ranges_merge_commit(struct rev_info *rev, struct commit *comm /* NEEDSWORK evil merge detection stuff */ } -int line_log_process_ranges_arbitrary_commit(struct rev_info *rev, struct commit *commit) +/* + * Entry point for one commit (both traversal modes call this). Dispatch + * on the commit's shape: no tracked ranges (nothing to do); a Bloom + * filter proving the file untouched (carry ranges to the sole parent + * without diffing); a merge (map across each parent, splitting the + * ranges); or an ordinary commit. A commit that changed no tracked + * range is marked TREESAME so the caller can prune it. + */ +int line_log_process_commit(struct rev_info *rev, struct commit *commit) { struct line_log_data *range = lookup_line_range(rev, commit); int changed = 0; @@ -1182,7 +868,7 @@ int line_log_filter(struct rev_info *rev) while (list) { struct commit_list *to_free = NULL; commit = list->item; - if (line_log_process_ranges_arbitrary_commit(rev, commit)) { + if (line_log_process_commit(rev, commit)) { *pp = list; pp = &list->next; } else diff --git a/line-log.h b/line-log.h index 99e1755ce3d568..7e0eb0133664fa 100644 --- a/line-log.h +++ b/line-log.h @@ -1,31 +1,13 @@ #ifndef LINE_LOG_H #define LINE_LOG_H -#include "diffcore.h" /* struct range, struct range_set */ +#include "diffcore.h" /* struct diff_filepair */ +#include "range-set.h" struct rev_info; struct commit; struct string_list; -/* A diff, encoded as the set of pre- and post-image ranges where the - * files differ. A pair of ranges corresponds to a hunk. */ -struct diff_ranges { - struct range_set parent; - struct range_set target; -}; - -void range_set_init(struct range_set *, size_t prealloc); -void range_set_release(struct range_set *); -/* Range includes start; excludes end */ -void range_set_append_unsafe(struct range_set *, long start, long end); -/* New range must begin at or after end of last added range */ -void range_set_append(struct range_set *, long start, long end); -/* - * In-place pass of sorting and merging the ranges in the range set, - * to sort and make the ranges disjoint. - */ -void sort_and_merge_range_set(struct range_set *); - /* Linked list of interesting files and their associated ranges. The * list must be kept sorted by path. * @@ -43,8 +25,7 @@ struct line_log_data { void line_log_init(struct rev_info *rev, const char *prefix, struct string_list *args); int line_log_filter(struct rev_info *rev); -int line_log_process_ranges_arbitrary_commit(struct rev_info *rev, - struct commit *commit); +int line_log_process_commit(struct rev_info *rev, struct commit *commit); void line_log_queue_pairs(struct rev_info *rev, struct commit *commit); diff --git a/meson.build b/meson.build index 3247697f74aae1..25da0ae5ae567f 100644 --- a/meson.build +++ b/meson.build @@ -444,6 +444,7 @@ libgit_sources = [ 'pseudo-merge.c', 'quote.c', 'range-diff.c', + 'range-set.c', 'reachable.c', 'read-cache.c', 'rebase-interactive.c', diff --git a/range-set.c b/range-set.c new file mode 100644 index 00000000000000..ae9a96289c2029 --- /dev/null +++ b/range-set.c @@ -0,0 +1,307 @@ +#include "git-compat-util.h" +#include "range-set.h" + +static void range_set_grow(struct range_set *rs, size_t extra) +{ + ALLOC_GROW(rs->ranges, rs->nr + extra, rs->alloc); +} + +void range_set_init(struct range_set *rs, size_t prealloc) +{ + rs->alloc = rs->nr = 0; + rs->ranges = NULL; + if (prealloc) + range_set_grow(rs, prealloc); +} + +void range_set_release(struct range_set *rs) +{ + FREE_AND_NULL(rs->ranges); + rs->alloc = rs->nr = 0; +} + +/* dst must be uninitialized! */ +void range_set_copy(struct range_set *dst, struct range_set *src) +{ + range_set_init(dst, src->nr); + COPY_ARRAY(dst->ranges, src->ranges, src->nr); + dst->nr = src->nr; +} + +void range_set_move(struct range_set *dst, struct range_set *src) +{ + range_set_release(dst); + dst->ranges = src->ranges; + dst->nr = src->nr; + dst->alloc = src->alloc; + src->ranges = NULL; + src->alloc = src->nr = 0; +} + +/* tack on a _new_ range _at the end_ */ +void range_set_append_unsafe(struct range_set *rs, long a, long b) +{ + assert(a <= b); + range_set_grow(rs, 1); + rs->ranges[rs->nr].start = a; + rs->ranges[rs->nr].end = b; + rs->nr++; +} + +void range_set_append(struct range_set *rs, long a, long b) +{ + assert(rs->nr == 0 || rs->ranges[rs->nr-1].end <= a); + range_set_append_unsafe(rs, a, b); +} + +static int range_cmp(const void *_r, const void *_s) +{ + const struct range *r = _r; + const struct range *s = _s; + + /* this could be simply 'return r.start-s.start', but for the types */ + if (r->start == s->start) + return 0; + if (r->start < s->start) + return -1; + return 1; +} + +/* + * Check that the ranges are non-empty, sorted and non-overlapping + */ +static void range_set_check_invariants(struct range_set *rs) +{ + unsigned int i; + + if (!rs) + return; + + if (rs->nr) + assert(rs->ranges[0].start < rs->ranges[0].end); + + for (i = 1; i < rs->nr; i++) { + assert(rs->ranges[i-1].end < rs->ranges[i].start); + assert(rs->ranges[i].start < rs->ranges[i].end); + } +} + +/* + * In-place pass of sorting and merging the ranges in the range set, + * to establish the invariants when we get the ranges from the user + */ +void sort_and_merge_range_set(struct range_set *rs) +{ + unsigned int i; + unsigned int o = 0; /* output cursor */ + + QSORT(rs->ranges, rs->nr, range_cmp); + + for (i = 0; i < rs->nr; i++) { + if (rs->ranges[i].start == rs->ranges[i].end) + continue; + if (o > 0 && rs->ranges[i].start <= rs->ranges[o-1].end) { + if (rs->ranges[o-1].end < rs->ranges[i].end) + rs->ranges[o-1].end = rs->ranges[i].end; + } else { + rs->ranges[o].start = rs->ranges[i].start; + rs->ranges[o].end = rs->ranges[i].end; + o++; + } + } + assert(o <= rs->nr); + rs->nr = o; + + range_set_check_invariants(rs); +} + +/* + * Union of two range sets. The output is in canonical form: overlapping + * and adjacent ranges are merged, and empty ranges are removed. + */ +void range_set_union(struct range_set *out, + struct range_set *a, struct range_set *b) +{ + unsigned int i = 0, j = 0; + struct range *ra = a->ranges; + struct range *rb = b->ranges; + /* cannot make an alias of out->ranges: it may change during grow */ + + assert(out->nr == 0); + while (i < a->nr || j < b->nr) { + struct range *new_range; + if (i < a->nr && j < b->nr) { + if (ra[i].start < rb[j].start) + new_range = &ra[i++]; + else if (ra[i].start > rb[j].start) + new_range = &rb[j++]; + else if (ra[i].end < rb[j].end) + new_range = &ra[i++]; + else + new_range = &rb[j++]; + } else if (i < a->nr) /* b exhausted */ + new_range = &ra[i++]; + else /* a exhausted */ + new_range = &rb[j++]; + if (new_range->start == new_range->end) + ; /* empty range */ + else if (!out->nr || out->ranges[out->nr-1].end < new_range->start) { + range_set_grow(out, 1); + out->ranges[out->nr].start = new_range->start; + out->ranges[out->nr].end = new_range->end; + out->nr++; + } else if (out->ranges[out->nr-1].end < new_range->end) { + out->ranges[out->nr-1].end = new_range->end; + } + } +} + +/* + * Difference of two range sets: out = a \ b (the parts of a not covered + * by any range in b). + */ +static void range_set_difference(struct range_set *out, + struct range_set *a, struct range_set *b) +{ + unsigned int i, j = 0; + for (i = 0; i < a->nr; i++) { + long start = a->ranges[i].start; + long end = a->ranges[i].end; + while (start < end) { + while (j < b->nr && start >= b->ranges[j].end) + /* + * a: |------- + * b: ------| + */ + j++; + if (j >= b->nr || end <= b->ranges[j].start) { + /* + * b exhausted, or + * a: ----| + * b: |---- + */ + range_set_append(out, start, end); + break; + } + if (start >= b->ranges[j].start) { + /* + * a: |--???? + * b: |------| + */ + start = b->ranges[j].end; + } else if (end > b->ranges[j].start) { + /* + * a: |-----| + * b: |--????? + */ + if (start < b->ranges[j].start) + range_set_append(out, start, b->ranges[j].start); + start = b->ranges[j].end; + } + } + } +} + +/* diff_ranges: a diff expressed as paired parent/target range sets. */ + +void diff_ranges_init(struct diff_ranges *diff) +{ + range_set_init(&diff->parent, 0); + range_set_init(&diff->target, 0); +} + +void diff_ranges_release(struct diff_ranges *diff) +{ + range_set_release(&diff->parent); + range_set_release(&diff->target); +} + +/* + * Mapping a range set backward across a diff: the target side accounts + * for its "+" (target-side) ranges, so a touched range is replaced by + * its parent-side counterpart (range_set_map_across_diff). + */ + +static int ranges_overlap(struct range *a, struct range *b) +{ + return !(a->end <= b->start || b->end <= a->start); +} + +/* + * Given a diff and the set of interesting ranges, determine all hunks + * of the diff which touch (overlap) at least one of the interesting + * ranges in the target. + */ +static void diff_ranges_filter_touched(struct diff_ranges *out, + struct diff_ranges *diff, + struct range_set *rs) +{ + unsigned int i, j = 0; + + assert(out->target.nr == 0); + + for (i = 0; i < diff->target.nr; i++) { + while (diff->target.ranges[i].start >= rs->ranges[j].end) { + j++; + if (j == rs->nr) + return; + } + if (ranges_overlap(&diff->target.ranges[i], &rs->ranges[j])) { + range_set_append(&out->parent, + diff->parent.ranges[i].start, + diff->parent.ranges[i].end); + range_set_append(&out->target, + diff->target.ranges[i].start, + diff->target.ranges[i].end); + } + } +} + +/* + * Adjust the line counts in 'rs' to account for the lines + * added/removed in the diff. + */ +static void range_set_shift_diff(struct range_set *out, + struct range_set *rs, + struct diff_ranges *diff) +{ + unsigned int i, j = 0; + long offset = 0; + struct range *src = rs->ranges; + struct range *target = diff->target.ranges; + struct range *parent = diff->parent.ranges; + + for (i = 0; i < rs->nr; i++) { + while (j < diff->target.nr && src[i].start >= target[j].start) { + offset += (parent[j].end-parent[j].start) + - (target[j].end-target[j].start); + j++; + } + range_set_append(out, src[i].start+offset, src[i].end+offset); + } +} + +/* + * Map a range set across a diff (see range-set.h): each touched + * target-side range is replaced by its parent-side counterpart, and the + * untouched ranges are shifted by the diff's net line change. + */ +void range_set_map_across_diff(struct range_set *out, + struct range_set *rs, + struct diff_ranges *diff, + struct diff_ranges **touched_out) +{ + struct diff_ranges *touched = xmalloc(sizeof(*touched)); + struct range_set tmp1 = { 0 }; + struct range_set tmp2 = { 0 }; + + diff_ranges_init(touched); + diff_ranges_filter_touched(touched, diff, rs); + range_set_difference(&tmp1, rs, &touched->target); + range_set_shift_diff(&tmp2, &tmp1, diff); + range_set_union(out, &tmp2, &touched->parent); + range_set_release(&tmp1); + range_set_release(&tmp2); + + *touched_out = touched; +} diff --git a/range-set.h b/range-set.h new file mode 100644 index 00000000000000..eb6ba2c6fe9595 --- /dev/null +++ b/range-set.h @@ -0,0 +1,64 @@ +#ifndef RANGE_SET_H +#define RANGE_SET_H + +/* + * Sets of disjoint line ranges, and mapping them backward across a diff. + * Used to follow line ranges through history: "git log -L" (line-log.c) + * and "git blame -L". + */ + +/* A range [start, end). Lines are numbered starting at 0. */ +struct range { + long start, end; +}; + +/* A set of ranges. The ranges must always be disjoint and sorted. */ +struct range_set { + unsigned int alloc, nr; + struct range *ranges; +}; + +/* + * A diff, encoded as the set of pre- and post-image ranges where the + * files differ. A pair of ranges corresponds to a hunk. + */ +struct diff_ranges { + struct range_set parent; + struct range_set target; +}; + +void range_set_init(struct range_set *, size_t prealloc); +void range_set_release(struct range_set *); +/* dst must be uninitialized! */ +void range_set_copy(struct range_set *dst, struct range_set *src); +void range_set_move(struct range_set *dst, struct range_set *src); +/* Range includes start; excludes end */ +void range_set_append_unsafe(struct range_set *, long start, long end); +/* New range must begin at or after end of last added range */ +void range_set_append(struct range_set *, long start, long end); +/* + * In-place pass of sorting and merging the ranges in the range set, + * to sort and make the ranges disjoint. + */ +void sort_and_merge_range_set(struct range_set *); + +/* Union of two (sorted, disjoint) range sets. */ +void range_set_union(struct range_set *out, + struct range_set *a, struct range_set *b); + +void diff_ranges_init(struct diff_ranges *diff); +void diff_ranges_release(struct diff_ranges *diff); + +/* + * Map the interesting ranges 'rs' backward across 'diff': the target + * side takes blame for the ranges it touched, which are replaced by + * their parent-side counterparts, while untouched ranges are shifted by + * the lines the diff added or removed before them. The touched hunks + * are returned in *touched_out, which the caller releases with + * diff_ranges_release() and then frees. + */ +void range_set_map_across_diff(struct range_set *out, struct range_set *rs, + struct diff_ranges *diff, + struct diff_ranges **touched_out); + +#endif /* RANGE_SET_H */ diff --git a/revision.c b/revision.c index 6a8101e8b7ef5f..9f7ce86516a2d7 100644 --- a/revision.c +++ b/revision.c @@ -4159,37 +4159,40 @@ static timestamp_t comparison_date(const struct rev_info *revs, commit->date; } -enum commit_action get_commit_action(struct rev_info *revs, struct commit *commit) +/* + * Which commits are ignored regardless of content, and before any + * line-level log processing. Extracted so the line-log walk driver + * (simplify_commit) can apply the same gate before it calls + * line_log_process_commit(). + */ +static int commit_early_ignore(struct rev_info *revs, struct commit *commit) { if (commit->object.flags & SHOWN) - return commit_ignore; + return 1; if (revs->maximal_only && (commit->object.flags & CHILD_VISITED)) - return commit_ignore; + return 1; if (revs->unpacked && has_object_pack(revs->repo, &commit->object.oid)) - return commit_ignore; - if (revs->no_kept_objects) { - if (has_object_kept_pack(revs->repo, &commit->object.oid, - revs->keep_pack_cache_flags)) - return commit_ignore; - } + return 1; + if (revs->no_kept_objects && + has_object_kept_pack(revs->repo, &commit->object.oid, + revs->keep_pack_cache_flags)) + return 1; if (commit->object.flags & UNINTERESTING) + return 1; + return 0; +} + +/* + * A pure predicate: should this commit be shown, ignored, or does it + * error? Line-level log filtering for the no-parent-rewriting case + * mutates the tracked ranges, so it is done by simplify_commit() before + * it calls us; keeping this function side-effect free lets callers such + * as the commit graph use it as a plain query. + */ +enum commit_action get_commit_action(struct rev_info *revs, struct commit *commit) +{ + if (commit_early_ignore(revs, commit)) return commit_ignore; - if (revs->line_level_traverse && !want_ancestry(revs)) { - /* - * In case of line-level log with parent rewriting - * prepare_revision_walk() already took care of all line-level - * log filtering, and there is nothing left to do here. - * - * If parent rewriting was not requested, then this is the - * place to perform the line-level log filtering. Notably, - * this check, though expensive, must come before the other, - * cheaper filtering conditions, because the tracked line - * ranges must be adjusted even when the commit will end up - * being ignored based on other conditions. - */ - if (!line_log_process_ranges_arbitrary_commit(revs, commit)) - return commit_ignore; - } if (revs->min_age != -1 && comparison_date(revs, commit) > revs->min_age) return commit_ignore; @@ -4298,7 +4301,24 @@ struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit) { - enum commit_action action = get_commit_action(revs, commit); + enum commit_action action; + + /* + * Line-level log without parent rewriting is filtered here, as the + * walk reaches each commit; with parent rewriting, + * prepare_revision_walk() already did it via line_log_filter(). Run + * it before get_commit_action() (a pure predicate), under the same + * commit_early_ignore() gate: a SHOWN or UNINTERESTING commit is not + * processed, but the tracked ranges must still be carried across a + * commit that later, cheaper conditions will ignore, so this must + * come first. + */ + if (revs->line_level_traverse && !want_ancestry(revs) && + !commit_early_ignore(revs, commit) && + !line_log_process_commit(revs, commit)) + return commit_ignore; + + action = get_commit_action(revs, commit); if (action == commit_show && revs->prune && revs->dense && want_ancestry(revs)) { diff --git a/t/meson.build b/t/meson.build index c5832fee053561..e1e957ad99c5b0 100644 --- a/t/meson.build +++ b/t/meson.build @@ -11,6 +11,7 @@ clar_test_suites = [ 'unit-tests/u-oidmap.c', 'unit-tests/u-oidtree.c', 'unit-tests/u-prio-queue.c', + 'unit-tests/u-range-set.c', 'unit-tests/u-reftable-basics.c', 'unit-tests/u-reftable-block.c', 'unit-tests/u-reftable-merged.c', diff --git a/t/unit-tests/u-range-set.c b/t/unit-tests/u-range-set.c new file mode 100644 index 00000000000000..921a77311b1aad --- /dev/null +++ b/t/unit-tests/u-range-set.c @@ -0,0 +1,125 @@ +#include "unit-test.h" +#include "range-set.h" + +/* Assert that 'rs' holds exactly the n given [start, end) ranges. */ +static void assert_ranges(struct range_set *rs, const long *expect, size_t n) +{ + size_t i; + + cl_assert_equal_i((int)rs->nr, (int)n); + for (i = 0; i < n; i++) { + cl_assert_equal_i((int)rs->ranges[i].start, (int)expect[2 * i]); + cl_assert_equal_i((int)rs->ranges[i].end, (int)expect[2 * i + 1]); + } +} + +void test_range_set__sort_and_merge(void) +{ + struct range_set rs; + const long expect[] = { 1, 4, 5, 8 }; + + /* Out-of-order and overlapping input is canonicalized. */ + range_set_init(&rs, 0); + range_set_append_unsafe(&rs, 5, 8); + range_set_append_unsafe(&rs, 1, 3); + range_set_append_unsafe(&rs, 2, 4); /* overlaps [1,3) */ + sort_and_merge_range_set(&rs); + + assert_ranges(&rs, expect, 2); /* [1,4), [5,8) */ + range_set_release(&rs); +} + +void test_range_set__union(void) +{ + struct range_set a, b, out; + const long expect[] = { 1, 5, 7, 9 }; + + range_set_init(&a, 0); + range_set_init(&b, 0); + range_set_init(&out, 0); + range_set_append(&a, 1, 3); + range_set_append(&a, 7, 9); + range_set_append(&b, 3, 5); /* adjacent to [1,3), merges */ + range_set_union(&out, &a, &b); + + assert_ranges(&out, expect, 2); /* [1,5), [7,9) */ + range_set_release(&a); + range_set_release(&b); + range_set_release(&out); +} + +static void free_touched(struct diff_ranges *touched) +{ + range_set_release(&touched->parent); + range_set_release(&touched->target); + free(touched); +} + +void test_range_set__map_untouched_range_shifts(void) +{ + /* + * A range the commit did not touch is carried to the parent, shifted + * by the lines added or removed before it. Here the commit inserts + * two lines at the top (parent [1,1) -> target [1,3)), so a tracked + * target range [5,8) maps to parent [3,6), and nothing is blamed. + */ + struct range_set rs, out; + struct diff_ranges diff; + struct diff_ranges *touched = NULL; + const long expect[] = { 3, 6 }; + + range_set_init(&rs, 0); + range_set_append(&rs, 5, 8); + + range_set_init(&diff.parent, 0); + range_set_init(&diff.target, 0); + range_set_append(&diff.parent, 1, 1); /* 0 parent lines */ + range_set_append(&diff.target, 1, 3); /* 2 inserted lines */ + + range_set_init(&out, 0); + range_set_map_across_diff(&out, &rs, &diff, &touched); + + assert_ranges(&out, expect, 1); + cl_assert_equal_i((int)touched->target.nr, 0); + + range_set_release(&rs); + range_set_release(&out); + range_set_release(&diff.parent); + range_set_release(&diff.target); + free_touched(touched); +} + +void test_range_set__map_touched_range_blames_commit(void) +{ + /* + * A range the commit changed is blamed on it: the range is replaced + * by its parent-side counterpart. Here the commit rewrote target + * [2,4) (from parent [2,3)); tracking exactly [2,4) maps to the + * parent side [2,3), and [2,4) is recorded as touched. + */ + struct range_set rs, out; + struct diff_ranges diff; + struct diff_ranges *touched = NULL; + const long expect_out[] = { 2, 3 }; + const long expect_touched[] = { 2, 4 }; + + range_set_init(&rs, 0); + range_set_append(&rs, 2, 4); + + range_set_init(&diff.parent, 0); + range_set_init(&diff.target, 0); + range_set_append(&diff.parent, 2, 3); + range_set_append(&diff.target, 2, 4); + + range_set_init(&out, 0); + range_set_map_across_diff(&out, &rs, &diff, &touched); + + assert_ranges(&out, expect_out, 1); + assert_ranges(&touched->target, expect_touched, 1); + + range_set_release(&rs); + range_set_release(&out); + range_set_release(&diff.parent); + range_set_release(&diff.target); + free_touched(touched); +}