Merge branch 'es/blame-L-twice'
authorJunio C Hamano <gitster@pobox.com>
Mon, 9 Sep 2013 21:35:11 +0000 (14:35 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 9 Sep 2013 21:35:11 +0000 (14:35 -0700)
Teaches "git blame" to take more than one -L ranges.

* es/blame-L-twice:
line-range: reject -L line numbers less than 1
t8001/t8002: blame: add tests of -L line numbers less than 1
line-range: teach -L^:RE to search from start of file
line-range: teach -L:RE to search from end of previous -L range
line-range: teach -L^/RE/ to search from start of file
line-range-format.txt: document -L/RE/ relative search
log: teach -L/RE/ to search from end of previous -L range
blame: teach -L/RE/ to search from end of previous -L range
line-range: teach -L/RE/ to search relative to anchor point
blame: document multiple -L support
t8001/t8002: blame: add tests of multiple -L options
blame: accept multiple -L ranges
blame: inline one-line function into its lone caller
range-set: publish API for re-use by git-blame -L
line-range-format.txt: clarify -L:regex usage form
git-log.txt: place each -L option variation on its own line

Documentation/blame-options.txt
Documentation/git-blame.txt
Documentation/git-log.txt
Documentation/line-range-format.txt
builtin/blame.c
line-log.c
line-log.h
line-range.c
line-range.h
t/annotate-tests.sh
t/t4211-line-log.sh
index 4e55b1564e8ad6526c53f797141cfccd83648fc7..0cebc4f6927211ffbc013de9368f03f480dba65d 100644 (file)
 
 -L <start>,<end>::
 -L :<regex>::
-       Annotate only the given line range.  <start> and <end> are optional.
-       ``-L <start>'' or ``-L <start>,'' spans from <start> to end of file.
-       ``-L ,<end>'' spans from start of file to <end>.
+       Annotate only the given line range. May be specified multiple times.
+       Overlapping ranges are allowed.
++
+<start> and <end> are optional. ``-L <start>'' or ``-L <start>,'' spans from
+<start> to end of file. ``-L ,<end>'' spans from start of file to <end>.
 +
-<start> and <end> can take one of these forms:
-
 include::line-range-format.txt[]
 
 -l::
index 6cea7f1ce1ba3a917ddd0cc9a12195d6f768bf76..f2c85cc6334e50f716c23be6cac4e502fe4d228f 100644 (file)
@@ -9,7 +9,7 @@ SYNOPSIS
 --------
 [verse]
 'git blame' [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-e] [-p] [-w] [--incremental]
-           [-L n,m | -L :fn] [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>]
+           [-L <range>] [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>]
            [--abbrev=<n>] [<rev> | --contents <file> | --reverse <rev>] [--] <file>
 
 DESCRIPTION
@@ -18,7 +18,8 @@ DESCRIPTION
 Annotates each line in the given file with information from the revision which
 last modified the line. Optionally, start annotating from the given revision.
 
-The command can also limit the range of lines annotated.
+When specified one or more times, `-L` restricts annotation to the requested
+lines.
 
 The origin of lines is automatically followed across whole-file
 renames (currently there is no option to turn the rename-following
@@ -130,7 +131,10 @@ SPECIFYING RANGES
 
 Unlike 'git blame' and 'git annotate' in older versions of git, the extent
 of the annotation can be limited to both line ranges and revision
-ranges.  When you are interested in finding the origin for
+ranges. The `-L` option, which limits annotation to a range of lines, may be
+specified multiple times.
+
+When you are interested in finding the origin for
 lines 40-60 for file `foo`, you can use the `-L` option like so
 (they mean the same thing -- both ask for 21 lines starting at
 line 40):
index ac2694d04cfc73c9b5ae6e6982d2f7f290ed67fe..34097efea707aa59b6a5703826445f5c3fccabcf 100644 (file)
@@ -62,7 +62,8 @@ produced by --stat etc.
        Note that only message is considered, if also a diff is shown
        its size is not included.
 
--L <start>,<end>:<file>, -L :<regex>:<file>::
+-L <start>,<end>:<file>::
+-L :<regex>:<file>::
 
        Trace the evolution of the line range given by "<start>,<end>"
        (or the funcname regex <regex>) within the <file>.  You may
@@ -71,8 +72,6 @@ produced by --stat etc.
        give zero or one positive revision arguments.
        You can specify this option more than once.
 +
-<start> and <end> can take one of these forms:
-
 include::line-range-format.txt[]
 
 <revision range>::
index 3e7ce72daab23d6dfcf12d1e0dd1dd6be705e56d..d7f26039cac500ff922dd3fb9470912b03718335 100644 (file)
@@ -1,3 +1,5 @@
+<start> and <end> can take one of these forms:
+
 - number
 +
 If <start> or <end> is a number, it specifies an
@@ -7,7 +9,10 @@ absolute line number (lines count from 1).
 - /regex/
 +
 This form will use the first line matching the given
-POSIX regex.  If <end> is a regex, it will search
+POSIX regex. If <start> is a regex, it will search from the end of
+the previous `-L` range, if any, otherwise from the start of file.
+If <start> is ``^/regex/'', it will search from the start of file.
+If <end> is a regex, it will search
 starting at the line given by <start>.
 +
 
@@ -15,11 +20,10 @@ starting at the line given by <start>.
 +
 This is only valid for <end> and will specify a number
 of lines before or after the line given by <start>.
-+
 
-- :regex
 +
-If the option's argument is of the form :regex, it denotes the range
+If ``:<regex>'' is given in place of <start> and <end>, it denotes the range
 from the first funcname line that matches <regex>, up to the next
-funcname line.
-+
+funcname line. ``:<regex>'' searches from the end of the previous `-L` range,
+if any, otherwise from the start of file.
+``^:<regex>'' searches from the start of file.
index aa1abb6d5e19ed60197a477a8b4bafa6fbd20c76..00927e0347ce884392bd51f8bc5d886248e3d6e4 100644 (file)
@@ -22,6 +22,7 @@
 #include "utf8.h"
 #include "userdiff.h"
 #include "line-range.h"
+#include "line-log.h"
 
 static char blame_usage[] = N_("git blame [options] [rev-opts] [rev] [--] file");
 
@@ -1937,18 +1938,6 @@ static const char *add_prefix(const char *prefix, const char *path)
        return prefix_path(prefix, prefix ? strlen(prefix) : 0, path);
 }
 
-/*
- * Parsing of -L option
- */
-static void prepare_blame_range(struct scoreboard *sb,
-                               const char *bottomtop,
-                               long lno,
-                               long *bottom, long *top)
-{
-       if (parse_range_arg(bottomtop, nth_line_cb, sb, lno, bottom, top, sb->path))
-               usage(blame_usage);
-}
-
 static int git_blame_config(const char *var, const char *value, void *cb)
 {
        if (!strcmp(var, "blame.showroot")) {
@@ -2245,29 +2234,18 @@ static int blame_move_callback(const struct option *option, const char *arg, int
        return 0;
 }
 
-static int blame_bottomtop_callback(const struct option *option, const char *arg, int unset)
-{
-       const char **bottomtop = option->value;
-       if (!arg)
-               return -1;
-       if (*bottomtop)
-               die("More than one '-L n,m' option given");
-       *bottomtop = arg;
-       return 0;
-}
-
 int cmd_blame(int argc, const char **argv, const char *prefix)
 {
        struct rev_info revs;
        const char *path;
        struct scoreboard sb;
        struct origin *o;
-       struct blame_entry *ent;
-       long dashdash_pos, bottom, top, lno;
+       struct blame_entry *ent = NULL;
+       long dashdash_pos, lno;
        const char *final_commit_name = NULL;
        enum object_type type;
 
-       static const char *bottomtop = NULL;
+       static struct string_list range_list;
        static int output_option = 0, opt = 0;
        static int show_stats = 0;
        static const char *revs_file = NULL;
@@ -2293,13 +2271,16 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                OPT_STRING(0, "contents", &contents_from, N_("file"), N_("Use <file>'s contents as the final image")),
                { OPTION_CALLBACK, 'C', NULL, &opt, N_("score"), N_("Find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback },
                { OPTION_CALLBACK, 'M', NULL, &opt, N_("score"), N_("Find line movements within and across files"), PARSE_OPT_OPTARG, blame_move_callback },
-               OPT_CALLBACK('L', NULL, &bottomtop, N_("n,m"), N_("Process only line range n,m, counting from 1"), blame_bottomtop_callback),
+               OPT_STRING_LIST('L', NULL, &range_list, N_("n,m"), N_("Process only line range n,m, counting from 1")),
                OPT__ABBREV(&abbrev),
                OPT_END()
        };
 
        struct parse_opt_ctx_t ctx;
        int cmd_is_annotate = !strcmp(argv[0], "annotate");
+       struct range_set ranges;
+       unsigned int range_i;
+       long anchor;
 
        git_config(git_blame_config, NULL);
        init_revisions(&revs, NULL);
@@ -2492,22 +2473,48 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        num_read_blob++;
        lno = prepare_lines(&sb);
 
-       bottom = top = 0;
-       if (bottomtop)
-               prepare_blame_range(&sb, bottomtop, lno, &bottom, &top);
-       if (lno < top || ((lno || bottom) && lno < bottom))
-               die("file %s has only %lu lines", path, lno);
-       if (bottom < 1)
-               bottom = 1;
-       if (top < 1)
-               top = lno;
-       bottom--;
-
-       ent = xcalloc(1, sizeof(*ent));
-       ent->lno = bottom;
-       ent->num_lines = top - bottom;
-       ent->suspect = o;
-       ent->s_lno = bottom;
+       if (lno && !range_list.nr)
+               string_list_append(&range_list, xstrdup("1"));
+
+       anchor = 1;
+       range_set_init(&ranges, range_list.nr);
+       for (range_i = 0; range_i < range_list.nr; ++range_i) {
+               long bottom, top;
+               if (parse_range_arg(range_list.items[range_i].string,
+                                   nth_line_cb, &sb, lno, anchor,
+                                   &bottom, &top, sb.path))
+                       usage(blame_usage);
+               if (lno < top || ((lno || bottom) && lno < bottom))
+                       die("file %s has only %lu lines", path, lno);
+               if (bottom < 1)
+                       bottom = 1;
+               if (top < 1)
+                       top = lno;
+               bottom--;
+               range_set_append_unsafe(&ranges, bottom, top);
+               anchor = top + 1;
+       }
+       sort_and_merge_range_set(&ranges);
+
+       for (range_i = ranges.nr; range_i > 0; --range_i) {
+               const struct range *r = &ranges.ranges[range_i - 1];
+               long bottom = r->start;
+               long top = r->end;
+               struct blame_entry *next = ent;
+               ent = xcalloc(1, sizeof(*ent));
+               ent->lno = bottom;
+               ent->num_lines = top - bottom;
+               ent->suspect = o;
+               ent->s_lno = bottom;
+               ent->next = next;
+               if (next)
+                       next->prev = ent;
+               origin_incref(o);
+       }
+       origin_decref(o);
+
+       range_set_release(&ranges);
+       string_list_clear(&range_list, 0);
 
        sb.ent = ent;
        sb.path = path;
index 1c3ac8dccd1dd7c1c15aaa82e80d6dbdf129bb56..d40c79dc2be2cc2a6f405bd9c399a929d1324dcd 100644 (file)
@@ -23,7 +23,7 @@ static void range_set_grow(struct range_set *rs, size_t extra)
 /* Either initialization would be fine */
 #define RANGE_SET_INIT {0}
 
-static void range_set_init(struct range_set *rs, size_t prealloc)
+void range_set_init(struct range_set *rs, size_t prealloc)
 {
        rs->alloc = rs->nr = 0;
        rs->ranges = NULL;
@@ -31,7 +31,7 @@ static void range_set_init(struct range_set *rs, size_t prealloc)
                range_set_grow(rs, prealloc);
 }
 
-static void range_set_release(struct range_set *rs)
+void range_set_release(struct range_set *rs)
 {
        free(rs->ranges);
        rs->alloc = rs->nr = 0;
@@ -56,7 +56,7 @@ static void range_set_move(struct range_set *dst, struct range_set *src)
 }
 
 /* tack on a _new_ range _at the end_ */
-static void range_set_append_unsafe(struct range_set *rs, long a, long b)
+void range_set_append_unsafe(struct range_set *rs, long a, long b)
 {
        assert(a <= b);
        range_set_grow(rs, 1);
@@ -65,7 +65,7 @@ static void range_set_append_unsafe(struct range_set *rs, long a, long b)
        rs->nr++;
 }
 
-static void range_set_append(struct range_set *rs, long a, long b)
+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);
@@ -107,7 +107,7 @@ static void range_set_check_invariants(struct range_set *rs)
  * 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
  */
-static void sort_and_merge_range_set(struct range_set *rs)
+void sort_and_merge_range_set(struct range_set *rs)
 {
        int i;
        int o = 0; /* output cursor */
@@ -291,7 +291,6 @@ static void line_log_data_insert(struct line_log_data **list,
 
        if (p) {
                range_set_append_unsafe(&p->ranges, begin, end);
-               sort_and_merge_range_set(&p->ranges);
                free(path);
                return;
        }
@@ -299,7 +298,6 @@ static void line_log_data_insert(struct line_log_data **list,
        p = xcalloc(1, sizeof(struct line_log_data));
        p->path = path;
        range_set_append(&p->ranges, begin, end);
-       sort_and_merge_range_set(&p->ranges);
        if (ip) {
                p->next = ip->next;
                ip->next = p;
@@ -566,12 +564,14 @@ parse_lines(struct commit *commit, const char *prefix, struct string_list *args)
        struct nth_line_cb cb_data;
        struct string_list_item *item;
        struct line_log_data *ranges = NULL;
+       struct line_log_data *p;
 
        for_each_string_list_item(item, args) {
                const char *name_part, *range_part;
                char *full_name;
                struct diff_filespec *spec;
                long begin = 0, end = 0;
+               long anchor;
 
                name_part = skip_range_arg(item->string);
                if (!name_part || *name_part != ':' || !name_part[1])
@@ -590,8 +590,14 @@ parse_lines(struct commit *commit, const char *prefix, struct string_list *args)
                cb_data.lines = lines;
                cb_data.line_ends = ends;
 
+               p = search_line_log_data(ranges, full_name, NULL);
+               if (p && p->ranges.nr)
+                       anchor = p->ranges.ranges[p->ranges.nr - 1].end + 1;
+               else
+                       anchor = 1;
+
                if (parse_range_arg(range_part, nth_line, &cb_data,
-                                   lines, &begin, &end,
+                                   lines, anchor, &begin, &end,
                                    full_name))
                        die("malformed -L argument '%s'", range_part);
                if (lines < end || ((lines || begin) && lines < begin))
@@ -608,6 +614,9 @@ parse_lines(struct commit *commit, const char *prefix, struct string_list *args)
                ends = NULL;
        }
 
+       for (p = ranges; p; p = p->next)
+               sort_and_merge_range_set(&p->ranges);
+
        return ranges;
 }
 
index 8bea45fd78f0368fef373be080ea5fa77d428c7f..a9212d84e492304b4e57da1e0327189b3b6434ed 100644 (file)
@@ -25,6 +25,18 @@ struct diff_ranges {
        struct range_set target;
 };
 
+extern void range_set_init(struct range_set *, size_t prealloc);
+extern void range_set_release(struct range_set *);
+/* Range includes start; excludes end */
+extern void range_set_append_unsafe(struct range_set *, long start, long end);
+/* New range must begin at or after end of last added range */
+extern 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.
+ */
+extern 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.
  *
index 69e8d6b6c02101b4243e1850aa52c377ccaa0a2b..de4e32f9424fbc0770756c3f61751fc69bfeb17e 100644 (file)
@@ -6,6 +6,18 @@
 
 /*
  * Parse one item in the -L option
+ *
+ * 'begin' is applicable only to relative range anchors. Absolute anchors
+ * ignore this value.
+ *
+ * When parsing "-L A,B", parse_loc() is called once for A and once for B.
+ *
+ * When parsing A, 'begin' must be a negative number, the absolute value of
+ * which is the line at which relative start-of-range anchors should be
+ * based. Beginning of file is represented by -1.
+ *
+ * When parsing B, 'begin' must be the positive line number immediately
+ * following the line computed for 'A'.
  */
 static const char *parse_loc(const char *spec, nth_line_fn_t nth_line,
                             void *data, long lines, long begin, long *ret)
@@ -42,10 +54,23 @@ static const char *parse_loc(const char *spec, nth_line_fn_t nth_line,
        }
        num = strtol(spec, &term, 10);
        if (term != spec) {
-               if (ret)
+               if (ret) {
+                       if (num <= 0)
+                               die("-L invalid line number: %ld", num);
                        *ret = num;
+               }
                return term;
        }
+
+       if (begin < 0) {
+               if (spec[0] != '^')
+                       begin = -begin;
+               else {
+                       begin = 1;
+                       spec++;
+               }
+       }
+
        if (spec[0] != '/')
                return spec;
 
@@ -85,7 +110,8 @@ static const char *parse_loc(const char *spec, nth_line_fn_t nth_line,
        else {
                char errbuf[1024];
                regerror(reg_error, &regexp, errbuf, 1024);
-               die("-L parameter '%s': %s", spec + 1, errbuf);
+               die("-L parameter '%s' starting at line %ld: %s",
+                   spec + 1, begin + 1, errbuf);
        }
 }
 
@@ -138,7 +164,7 @@ static const char *find_funcname_matching_regexp(xdemitconf_t *xecfg, const char
 }
 
 static const char *parse_range_funcname(const char *arg, nth_line_fn_t nth_line_cb,
-                                       void *cb_data, long lines, long *begin, long *end,
+                                       void *cb_data, long lines, long anchor, long *begin, long *end,
                                        const char *path)
 {
        char *pattern;
@@ -150,6 +176,11 @@ static const char *parse_range_funcname(const char *arg, nth_line_fn_t nth_line_
        int reg_error;
        regex_t regexp;
 
+       if (*arg == '^') {
+               anchor = 1;
+               arg++;
+       }
+
        assert(*arg == ':');
        term = arg+1;
        while (*term && *term != ':') {
@@ -164,7 +195,8 @@ static const char *parse_range_funcname(const char *arg, nth_line_fn_t nth_line_
 
        pattern = xstrndup(arg+1, term-(arg+1));
 
-       start = nth_line_cb(cb_data, 0);
+       anchor--; /* input is in human terms */
+       start = nth_line_cb(cb_data, anchor);
 
        drv = userdiff_find_by_path(path);
        if (drv && drv->funcname.pattern) {
@@ -182,7 +214,8 @@ static const char *parse_range_funcname(const char *arg, nth_line_fn_t nth_line_
 
        p = find_funcname_matching_regexp(xecfg, (char*) start, &regexp);
        if (!p)
-               die("-L parameter '%s': no match", pattern);
+               die("-L parameter '%s' starting at line %ld: no match",
+                   pattern, anchor + 1);
        *begin = 0;
        while (p > nth_line_cb(cb_data, *begin))
                (*begin)++;
@@ -210,19 +243,24 @@ static const char *parse_range_funcname(const char *arg, nth_line_fn_t nth_line_
 }
 
 int parse_range_arg(const char *arg, nth_line_fn_t nth_line_cb,
-                   void *cb_data, long lines, long *begin, long *end,
-                   const char *path)
+                   void *cb_data, long lines, long anchor,
+                   long *begin, long *end, const char *path)
 {
        *begin = *end = 0;
 
-       if (*arg == ':') {
-               arg = parse_range_funcname(arg, nth_line_cb, cb_data, lines, begin, end, path);
+       if (anchor < 1)
+               anchor = 1;
+       if (anchor > lines)
+               anchor = lines + 1;
+
+       if (*arg == ':' || (*arg == '^' && *(arg + 1) == ':')) {
+               arg = parse_range_funcname(arg, nth_line_cb, cb_data, lines, anchor, begin, end, path);
                if (!arg || *arg)
                        return -1;
                return 0;
        }
 
-       arg = parse_loc(arg, nth_line_cb, cb_data, lines, 1, begin);
+       arg = parse_loc(arg, nth_line_cb, cb_data, lines, -anchor, begin);
 
        if (*arg == ',')
                arg = parse_loc(arg + 1, nth_line_cb, cb_data, lines, *begin + 1, end);
@@ -240,8 +278,8 @@ int parse_range_arg(const char *arg, nth_line_fn_t nth_line_cb,
 
 const char *skip_range_arg(const char *arg)
 {
-       if (*arg == ':')
-               return parse_range_funcname(arg, NULL, NULL, 0, NULL, NULL, NULL);
+       if (*arg == ':' || (*arg == '^' && *(arg + 1) == ':'))
+               return parse_range_funcname(arg, NULL, NULL, 0, 0, NULL, NULL, NULL);
 
        arg = parse_loc(arg, NULL, NULL, 0, -1, NULL);
 
index ae3d0123b447c1c619695c2d0aaf619d1daaa991..83ba3c25e88a4601dc17ce01dc57dfbc33c05b2e 100644 (file)
@@ -9,6 +9,9 @@
  * line 'lno' inside the 'cb_data'.  The caller is expected to already
  * have a suitable map at hand to make this a constant-time lookup.
  *
+ * 'anchor' is the 1-based line at which relative range specifications
+ * should be anchored. Absolute ranges are unaffected by this value.
+ *
  * Returns 0 in case of success and -1 if there was an error.  The
  * actual range is stored in *begin and *end.  The counting starts
  * at 1!  In case of error, the caller should show usage message.
@@ -18,7 +21,7 @@ typedef const char *(*nth_line_fn_t)(void *data, long lno);
 
 extern int parse_range_arg(const char *arg,
                           nth_line_fn_t nth_line_cb,
-                          void *cb_data, long lines,
+                          void *cb_data, long lines, long anchor,
                           long *begin, long *end,
                           const char *path);
 
index ce5b8ed304d1c1374589fc96152eb4f0244e65b9..99caa42f5cdc199504d1c5fad442d8f8036e376e 100644 (file)
@@ -185,6 +185,18 @@ test_expect_success 'blame -L Y,X (undocumented)' '
        check_count -L6,3 B 1 B1 1 B2 1 D 1
 '
 
+test_expect_success 'blame -L -X' '
+       test_must_fail $PROG -L-1 file
+'
+
+test_expect_success 'blame -L 0' '
+       test_must_fail $PROG -L0 file
+'
+
+test_expect_success 'blame -L ,0' '
+       test_must_fail $PROG -L,0 file
+'
+
 test_expect_success 'blame -L ,+0' '
        test_must_fail $PROG -L,+0 file
 '
@@ -271,6 +283,75 @@ test_expect_success 'blame -L ,Y (Y > nlines)' '
        test_must_fail $PROG -L,12345 file
 '
 
+test_expect_success 'blame -L multiple (disjoint)' '
+       check_count -L2,3 -L6,7 A 1 B1 1 B2 1 "A U Thor" 1
+'
+
+test_expect_success 'blame -L multiple (disjoint: unordered)' '
+       check_count -L6,7 -L2,3 A 1 B1 1 B2 1 "A U Thor" 1
+'
+
+test_expect_success 'blame -L multiple (adjacent)' '
+       check_count -L2,3 -L4,5 A 1 B 1 B2 1 D 1
+'
+
+test_expect_success 'blame -L multiple (adjacent: unordered)' '
+       check_count -L4,5 -L2,3 A 1 B 1 B2 1 D 1
+'
+
+test_expect_success 'blame -L multiple (overlapping)' '
+       check_count -L2,4 -L3,5 A 1 B 1 B2 1 D 1
+'
+
+test_expect_success 'blame -L multiple (overlapping: unordered)' '
+       check_count -L3,5 -L2,4 A 1 B 1 B2 1 D 1
+'
+
+test_expect_success 'blame -L multiple (superset/subset)' '
+       check_count -L2,8 -L3,5 A 1 B 1 B1 1 B2 1 C 1 D 1 "A U Thor" 1
+'
+
+test_expect_success 'blame -L multiple (superset/subset: unordered)' '
+       check_count -L3,5 -L2,8 A 1 B 1 B1 1 B2 1 C 1 D 1 "A U Thor" 1
+'
+
+test_expect_success 'blame -L /RE/ (relative)' '
+       check_count -L3,3 -L/fox/ B1 1 B2 1 C 1 D 1 "A U Thor" 1
+'
+
+test_expect_success 'blame -L /RE/ (relative: no preceding range)' '
+       check_count -L/dog/ A 1 B 1 B1 1 B2 1 C 1 D 1 "A U Thor" 1
+'
+
+test_expect_success 'blame -L /RE/ (relative: adjacent)' '
+       check_count -L1,1 -L/dog/,+1 A 1 E 1
+'
+
+test_expect_success 'blame -L /RE/ (relative: not found)' '
+       test_must_fail $PROG -L4,4 -L/dog/ file
+'
+
+test_expect_success 'blame -L /RE/ (relative: end-of-file)' '
+       test_must_fail $PROG -L, -L/$/ file
+'
+
+test_expect_success 'blame -L ^/RE/ (absolute)' '
+       check_count -L3,3 -L^/dog/,+2 A 1 B2 1
+'
+
+test_expect_success 'blame -L ^/RE/ (absolute: no preceding range)' '
+       check_count -L^/dog/,+2 A 1 B2 1
+'
+
+test_expect_success 'blame -L ^/RE/ (absolute: not found)' '
+       test_must_fail $PROG -L4,4 -L^/tambourine/ file
+'
+
+test_expect_success 'blame -L ^/RE/ (absolute: end-of-file)' '
+       n=$(expr $(wc -l <file) + 1) &&
+       check_count -L$n -L^/$/,+2 A 1 C 1 E 1
+'
+
 test_expect_success 'setup -L :regex' '
        tr Q "\\t" >hello.c <<-\EOF &&
        int main(int argc, const char *argv[])
@@ -313,6 +394,39 @@ test_expect_success 'blame -L :nomatch' '
        test_must_fail $PROG -L:nomatch hello.c
 '
 
+test_expect_success 'blame -L :RE (relative)' '
+       check_count -f hello.c -L3,3 -L:ma.. F 1 H 4
+'
+
+test_expect_success 'blame -L :RE (relative: no preceding range)' '
+       check_count -f hello.c -L:ma.. F 4 G 1
+'
+
+test_expect_success 'blame -L :RE (relative: not found)' '
+       test_must_fail $PROG -L3,3 -L:tambourine hello.c
+'
+
+test_expect_success 'blame -L :RE (relative: end-of-file)' '
+       test_must_fail $PROG -L, -L:main hello.c
+'
+
+test_expect_success 'blame -L ^:RE (absolute)' '
+       check_count -f hello.c -L3,3 -L^:ma.. F 4 G 1
+'
+
+test_expect_success 'blame -L ^:RE (absolute: no preceding range)' '
+       check_count -f hello.c -L^:ma.. F 4 G 1
+'
+
+test_expect_success 'blame -L ^:RE (absolute: not found)' '
+       test_must_fail $PROG -L4,4 -L^:tambourine hello.c
+'
+
+test_expect_success 'blame -L ^:RE (absolute: end-of-file)' '
+       n=$(printf "%d" $(wc -l <hello.c)) &&
+       check_count -f hello.c -L$n -L^:ma.. F 4 G 1 H 1
+'
+
 test_expect_success 'setup incremental' '
        (
        GIT_AUTHOR_NAME=I &&
@@ -333,8 +447,8 @@ test_expect_success 'blame empty' '
        check_count -h HEAD^^ -f incremental
 '
 
-test_expect_success 'blame -L 0 empty (undocumented)' '
-       check_count -h HEAD^^ -f incremental -L0
+test_expect_success 'blame -L 0 empty' '
+       test_must_fail $PROG -L0 incremental HEAD^^
 '
 
 test_expect_success 'blame -L 1 empty' '
@@ -349,8 +463,8 @@ test_expect_success 'blame half' '
        check_count -h HEAD^ -f incremental I 1
 '
 
-test_expect_success 'blame -L 0 half (undocumented)' '
-       check_count -h HEAD^ -f incremental -L0 I 1
+test_expect_success 'blame -L 0 half' '
+       test_must_fail $PROG -L0 incremental HEAD^
 '
 
 test_expect_success 'blame -L 1 half' '
@@ -369,8 +483,8 @@ test_expect_success 'blame full' '
        check_count -f incremental I 1
 '
 
-test_expect_success 'blame -L 0 full (undocumented)' '
-       check_count -f incremental -L0 I 1
+test_expect_success 'blame -L 0 full' '
+       test_must_fail $PROG -L0 incremental
 '
 
 test_expect_success 'blame -L 1 full' '
@@ -412,3 +526,7 @@ test_expect_success 'blame -L X,+N (non-numeric N)' '
 test_expect_success 'blame -L X,-N (non-numeric N)' '
        test_must_fail $PROG -L1,-N file
 '
+
+test_expect_success 'blame -L ,^/RE/' '
+       test_must_fail $PROG -L1,^/99/ file
+'
index b01b3ddebb568e0fe74843d29933b5f0cda1a7d3..7369d3c517294feda894eba85f7b02ac9a57ccae 100755 (executable)
@@ -48,7 +48,7 @@ canned_test "-M -L '/long f/,/^}/:b.c' move-support" move-support-f
 canned_test "-M -L ':f:b.c' parallel-change" parallel-change-f-to-main
 
 canned_test "-L 4,12:a.c -L :main:a.c simple" multiple
-canned_test "-L 4,18:a.c -L :main:a.c simple" multiple-overlapping
+canned_test "-L 4,18:a.c -L ^:main:a.c simple" multiple-overlapping
 canned_test "-L :main:a.c -L 4,18:a.c simple" multiple-overlapping
 canned_test "-L 4:a.c -L 8,12:a.c simple" multiple-superset
 canned_test "-L 8,12:a.c -L 4:a.c simple" multiple-superset