Merge branch 'tr/line-log'
authorJunio C Hamano <gitster@pobox.com>
Sun, 2 Jun 2013 23:00:44 +0000 (16:00 -0700)
committerJunio C Hamano <gitster@pobox.com>
Sun, 2 Jun 2013 23:00:44 +0000 (16:00 -0700)
* tr/line-log:
git-log(1): remove --full-line-diff description
line-log: fix documentation formatting
log -L: improve comments in process_all_files()
log -L: store the path instead of a diff_filespec
log -L: test merge of parallel modify/rename
t4211: pass -M to 'git log -M -L...' test
log -L: fix overlapping input ranges
log -L: check range set invariants when we look it up
Speed up log -L... -M
log -L: :pattern:file syntax to find by funcname
Implement line-history search (git log -L)
Export rewrite_parents() for 'log -L'
Refactor parse_loc

31 files changed:
Documentation/blame-options.txt
Documentation/git-blame.txt
Documentation/git-log.txt
Documentation/line-range-format.txt [new file with mode: 0644]
Makefile
builtin/blame.c
builtin/log.c
line-log.c [new file with mode: 0644]
line-log.h [new file with mode: 0644]
line-range.c [new file with mode: 0644]
line-range.h [new file with mode: 0644]
log-tree.c
revision.c
revision.h
t/perf/p4211-line-log.sh [new file with mode: 0755]
t/t4211-line-log.sh [new file with mode: 0755]
t/t4211/expect.beginning-of-file [new file with mode: 0644]
t/t4211/expect.end-of-file [new file with mode: 0644]
t/t4211/expect.move-support-f [new file with mode: 0644]
t/t4211/expect.multiple [new file with mode: 0644]
t/t4211/expect.multiple-overlapping [new file with mode: 0644]
t/t4211/expect.multiple-superset [new file with mode: 0644]
t/t4211/expect.parallel-change-f-to-main [new file with mode: 0644]
t/t4211/expect.simple-f [new file with mode: 0644]
t/t4211/expect.simple-f-to-main [new file with mode: 0644]
t/t4211/expect.simple-main [new file with mode: 0644]
t/t4211/expect.simple-main-to-end [new file with mode: 0644]
t/t4211/expect.two-ranges [new file with mode: 0644]
t/t4211/expect.vanishes-early [new file with mode: 0644]
t/t4211/history.export [new file with mode: 0644]
t/t8003-blame-corner-cases.sh
index b0d31df0e750141fb8adc7a487c81663246ddf04..e9f984ba01597234abee94fffd4f71beb2fbbc02 100644 (file)
@@ -9,28 +9,11 @@
 --show-stats::
        Include additional statistics at the end of blame output.
 
--L <start>,<end>::
+-L <start>,<end>, -L :<regex>::
        Annotate only the given line range.  <start> and <end> can take
        one of these forms:
 
-       - number
-+
-If <start> or <end> is a number, it specifies an
-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
-starting at the line given by <start>.
-+
-
-- +offset or -offset
-+
-This is only valid for <end> and will specify a number
-of lines before or after the line given by <start>.
-+
+include::line-range-format.txt[]
 
 -l::
        Show long rev (Default: off).
index 9a05c2b3d24ca63163d76476d710e17f31617ff4..6cea7f1ce1ba3a917ddd0cc9a12195d6f768bf76 100644 (file)
@@ -8,9 +8,9 @@ git-blame - Show what revision and author last modified each line of a file
 SYNOPSIS
 --------
 [verse]
-'git blame' [-c] [-b] [-l] [--root] [-t] [-f] [-n] [-s] [-e] [-p] [-w] [--incremental] [-L n,m]
-           [-S <revs-file>] [-M] [-C] [-C] [-C] [--since=<date>] [--abbrev=<n>]
-           [<rev> | --contents <file> | --reverse <rev>] [--] <file>
+'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>]
+           [--abbrev=<n>] [<rev> | --contents <file> | --reverse <rev>] [--] <file>
 
 DESCRIPTION
 -----------
index a976534ab870a89a236ec7190f985e20f771e7f7..4687fe8192594b358f7058dcdec985ab30c2a3fb 100644 (file)
@@ -62,6 +62,19 @@ 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>::
+
+       Trace the evolution of the line range given by "<start>,<end>"
+       (or the funcname regex <regex>) within the <file>.  You may
+       not give any pathspec limiters.  This is currently limited to
+       a walk starting from a single revision, i.e., you may only
+       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>::
        Show only commits in the specified revision range.  When no
        <revision range> is specified, it defaults to `HEAD` (i.e. the
@@ -140,6 +153,11 @@ Examples
        This makes sense only when following a strict policy of merging all
        topic branches when staying on a single integration branch.
 
+git log -L '/int main/',/^}/:main.c::
+
+       Shows how the function `main()` in the file 'main.c' evolved
+       over time.
+
 `git log -3`::
        Limits the number of commits to show to 3.
 
diff --git a/Documentation/line-range-format.txt b/Documentation/line-range-format.txt
new file mode 100644 (file)
index 0000000..3e7ce72
--- /dev/null
@@ -0,0 +1,25 @@
+- number
++
+If <start> or <end> is a number, it specifies an
+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
+starting at the line given by <start>.
++
+
+- +offset or -offset
++
+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
+from the first funcname line that matches <regex>, up to the next
+funcname line.
++
index 35e3f7dd8132834cecd94bd88d8dea71b53a3af6..985598b01471428b81525a92fa2cfbf55c147bf3 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -681,6 +681,8 @@ LIB_H += help.h
 LIB_H += http.h
 LIB_H += kwset.h
 LIB_H += levenshtein.h
+LIB_H += line-log.h
+LIB_H += line-range.h
 LIB_H += list-objects.h
 LIB_H += ll-merge.h
 LIB_H += log-tree.h
@@ -808,6 +810,8 @@ LIB_OBJS += hex.o
 LIB_OBJS += ident.o
 LIB_OBJS += kwset.o
 LIB_OBJS += levenshtein.o
+LIB_OBJS += line-log.o
+LIB_OBJS += line-range.o
 LIB_OBJS += list-objects.o
 LIB_OBJS += ll-merge.o
 LIB_OBJS += lockfile.o
index 57a487e052fccc9ad45fa90a68a74fd54a1cdf72..079dcd3407881ce0ea988c3f487faf8ca6d3e5d2 100644 (file)
@@ -21,6 +21,7 @@
 #include "parse-options.h"
 #include "utf8.h"
 #include "userdiff.h"
+#include "line-range.h"
 
 static char blame_usage[] = N_("git blame [options] [rev-opts] [rev] [--] file");
 
@@ -566,11 +567,16 @@ static void dup_entry(struct blame_entry *dst, struct blame_entry *src)
        dst->score = 0;
 }
 
-static const char *nth_line(struct scoreboard *sb, int lno)
+static const char *nth_line(struct scoreboard *sb, long lno)
 {
        return sb->final_buf + sb->lineno[lno];
 }
 
+static const char *nth_line_cb(void *data, long lno)
+{
+       return nth_line((struct scoreboard *)data, lno);
+}
+
 /*
  * It is known that lines between tlno to same came from parent, and e
  * has an overlap with that range.  it also is known that parent's
@@ -1931,83 +1937,6 @@ static const char *add_prefix(const char *prefix, const char *path)
        return prefix_path(prefix, prefix ? strlen(prefix) : 0, path);
 }
 
-/*
- * Parsing of (comma separated) one item in the -L option
- */
-static const char *parse_loc(const char *spec,
-                            struct scoreboard *sb, long lno,
-                            long begin, long *ret)
-{
-       char *term;
-       const char *line;
-       long num;
-       int reg_error;
-       regex_t regexp;
-       regmatch_t match[1];
-
-       /* Allow "-L <something>,+20" to mean starting at <something>
-        * for 20 lines, or "-L <something>,-5" for 5 lines ending at
-        * <something>.
-        */
-       if (1 < begin && (spec[0] == '+' || spec[0] == '-')) {
-               num = strtol(spec + 1, &term, 10);
-               if (term != spec + 1) {
-                       if (spec[0] == '-')
-                               num = 0 - num;
-                       if (0 < num)
-                               *ret = begin + num - 2;
-                       else if (!num)
-                               *ret = begin;
-                       else
-                               *ret = begin + num;
-                       return term;
-               }
-               return spec;
-       }
-       num = strtol(spec, &term, 10);
-       if (term != spec) {
-               *ret = num;
-               return term;
-       }
-       if (spec[0] != '/')
-               return spec;
-
-       /* it could be a regexp of form /.../ */
-       for (term = (char *) spec + 1; *term && *term != '/'; term++) {
-               if (*term == '\\')
-                       term++;
-       }
-       if (*term != '/')
-               return spec;
-
-       /* try [spec+1 .. term-1] as regexp */
-       *term = 0;
-       begin--; /* input is in human terms */
-       line = nth_line(sb, begin);
-
-       if (!(reg_error = regcomp(&regexp, spec + 1, REG_NEWLINE)) &&
-           !(reg_error = regexec(&regexp, line, 1, match, 0))) {
-               const char *cp = line + match[0].rm_so;
-               const char *nline;
-
-               while (begin++ < lno) {
-                       nline = nth_line(sb, begin);
-                       if (line <= cp && cp < nline)
-                               break;
-                       line = nline;
-               }
-               *ret = begin;
-               regfree(&regexp);
-               *term++ = '/';
-               return term;
-       }
-       else {
-               char errbuf[1024];
-               regerror(reg_error, &regexp, errbuf, 1024);
-               die("-L parameter '%s': %s", spec + 1, errbuf);
-       }
-}
-
 /*
  * Parsing of -L option
  */
@@ -2016,15 +1945,7 @@ static void prepare_blame_range(struct scoreboard *sb,
                                long lno,
                                long *bottom, long *top)
 {
-       const char *term;
-
-       term = parse_loc(bottomtop, sb, lno, 1, bottom);
-       if (*term == ',') {
-               term = parse_loc(term + 1, sb, lno, *bottom + 1, top);
-               if (*term)
-                       usage(blame_usage);
-       }
-       if (*term)
+       if (parse_range_arg(bottomtop, nth_line_cb, sb, lno, bottom, top, sb->path))
                usage(blame_usage);
 }
 
@@ -2574,10 +2495,6 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        bottom = top = 0;
        if (bottomtop)
                prepare_blame_range(&sb, bottomtop, lno, &bottom, &top);
-       if (bottom && top && top < bottom) {
-               long tmp;
-               tmp = top; top = bottom; bottom = tmp;
-       }
        if (bottom < 1)
                bottom = 1;
        if (top < 1)
index 6e56a50002194e0a452266a580932b9e4e9647c9..9e2123295ffa80ecab83977099ed6a43fd4a93f9 100644 (file)
@@ -19,6 +19,7 @@
 #include "remote.h"
 #include "string-list.h"
 #include "parse-options.h"
+#include "line-log.h"
 #include "branch.h"
 #include "streaming.h"
 #include "version.h"
@@ -42,6 +43,12 @@ static const char * const builtin_log_usage[] = {
        NULL
 };
 
+struct line_opt_callback_data {
+       struct rev_info *rev;
+       const char *prefix;
+       struct string_list args;
+};
+
 static int parse_decoration_style(const char *var, const char *value)
 {
        switch (git_config_maybe_bool(var, value)) {
@@ -76,6 +83,19 @@ static int decorate_callback(const struct option *opt, const char *arg, int unse
        return 0;
 }
 
+static int log_line_range_callback(const struct option *option, const char *arg, int unset)
+{
+       struct line_opt_callback_data *data = option->value;
+
+       if (!arg)
+               return -1;
+
+       data->rev->line_level_traverse = 1;
+       string_list_append(&data->args, arg);
+
+       return 0;
+}
+
 static void cmd_log_init_defaults(struct rev_info *rev)
 {
        if (fmt_pretty)
@@ -98,6 +118,7 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
 {
        struct userformat_want w;
        int quiet = 0, source = 0, mailmap = 0;
+       static struct line_opt_callback_data line_cb = {NULL, NULL, STRING_LIST_INIT_DUP};
 
        const struct option builtin_log_options[] = {
                OPT_BOOL(0, "quiet", &quiet, N_("suppress diff output")),
@@ -105,9 +126,15 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
                OPT_BOOL(0, "use-mailmap", &mailmap, N_("Use mail map file")),
                { OPTION_CALLBACK, 0, "decorate", NULL, NULL, N_("decorate options"),
                  PARSE_OPT_OPTARG, decorate_callback},
+               OPT_CALLBACK('L', NULL, &line_cb, "n,m:file",
+                            "Process line range n,m in file, counting from 1",
+                            log_line_range_callback),
                OPT_END()
        };
 
+       line_cb.rev = rev;
+       line_cb.prefix = prefix;
+
        mailmap = use_mailmap_config;
        argc = parse_options(argc, argv, prefix,
                             builtin_log_options, builtin_log_usage,
@@ -161,6 +188,10 @@ static void cmd_log_init_finish(int argc, const char **argv, const char *prefix,
                rev->show_decorations = 1;
                load_ref_decorations(decoration_style);
        }
+
+       if (rev->line_level_traverse)
+               line_log_init(rev, line_cb.prefix, &line_cb.args);
+
        setup_pager();
 }
 
diff --git a/line-log.c b/line-log.c
new file mode 100644 (file)
index 0000000..4bbb09b
--- /dev/null
@@ -0,0 +1,1273 @@
+#include "git-compat-util.h"
+#include "line-range.h"
+#include "cache.h"
+#include "tag.h"
+#include "blob.h"
+#include "tree.h"
+#include "diff.h"
+#include "commit.h"
+#include "decorate.h"
+#include "revision.h"
+#include "xdiff-interface.h"
+#include "strbuf.h"
+#include "log-tree.h"
+#include "graph.h"
+#include "userdiff.h"
+#include "line-log.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}
+
+static 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);
+}
+
+static void range_set_release(struct range_set *rs)
+{
+       free(rs->ranges);
+       rs->alloc = rs->nr = 0;
+       rs->ranges = NULL;
+}
+
+/* dst must be uninitialized! */
+static void range_set_copy(struct range_set *dst, struct range_set *src)
+{
+       range_set_init(dst, src->nr);
+       memcpy(dst->ranges, src->ranges, src->nr*sizeof(struct range_set));
+       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_ */
+static 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++;
+}
+
+static 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)
+{
+       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
+ */
+static void sort_and_merge_range_set(struct range_set *rs)
+{
+       int i;
+       int o = 1; /* output cursor */
+
+       qsort(rs->ranges, rs->nr, sizeof(struct range), range_cmp);
+
+       for (i = 1; i < rs->nr; i++) {
+               if (rs->ranges[i].start <= rs->ranges[o-1].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.
+ *
+ * This is also where the ranges are consolidated into canonical form:
+ * overlapping and adjacent ranges are merged, and empty ranges are
+ * removed.
+ */
+static void range_set_union(struct range_set *out,
+                            struct range_set *a, struct range_set *b)
+{
+       int i = 0, j = 0, o = 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;
+               if (i < a->nr && j < b->nr) {
+                       if (ra[i].start < rb[j].start)
+                               new = &ra[i++];
+                       else if (ra[i].start > rb[j].start)
+                               new = &rb[j++];
+                       else if (ra[i].end < rb[j].end)
+                               new = &ra[i++];
+                       else
+                               new = &rb[j++];
+               } else if (i < a->nr)      /* b exhausted */
+                       new = &ra[i++];
+               else                       /* a exhausted */
+                       new = &rb[j++];
+               if (new->start == new->end)
+                       ; /* empty range */
+               else if (!o || out->ranges[o-1].end < new->start) {
+                       range_set_grow(out, 1);
+                       out->ranges[o].start = new->start;
+                       out->ranges[o].end = new->end;
+                       o++;
+               } else if (out->ranges[o-1].end < new->end) {
+                       out->ranges[o-1].end = new->end;
+               }
+       }
+       out->nr = o;
+}
+
+/*
+ * 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)
+{
+       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);
+}
+
+void line_log_data_init(struct line_log_data *r)
+{
+       memset(r, 0, sizeof(struct line_log_data));
+       range_set_init(&r->ranges, 0);
+}
+
+static void line_log_data_clear(struct line_log_data *r)
+{
+       range_set_release(&r->ranges);
+       if (r->pair)
+               diff_free_filepair(r->pair);
+}
+
+static void free_line_log_data(struct line_log_data *r)
+{
+       while (r) {
+               struct line_log_data *next = r->next;
+               line_log_data_clear(r);
+               free(r);
+               r = next;
+       }
+}
+
+static struct line_log_data *
+search_line_log_data(struct line_log_data *list, const char *path,
+                    struct line_log_data **insertion_point)
+{
+       struct line_log_data *p = list;
+       if (insertion_point)
+               *insertion_point = NULL;
+       while (p) {
+               int cmp = strcmp(p->path, path);
+               if (!cmp)
+                       return p;
+               if (insertion_point && cmp < 0)
+                       *insertion_point = p;
+               p = p->next;
+       }
+       return NULL;
+}
+
+/*
+ * Note: takes ownership of 'path', which happens to be what the only
+ * caller needs.
+ */
+static void line_log_data_insert(struct line_log_data **list,
+                                char *path,
+                                long begin, long end)
+{
+       struct line_log_data *ip;
+       struct line_log_data *p = search_line_log_data(*list, path, &ip);
+
+       if (p) {
+               range_set_append_unsafe(&p->ranges, begin, end);
+               sort_and_merge_range_set(&p->ranges);
+               free(path);
+               return;
+       }
+
+       p = xcalloc(1, sizeof(struct line_log_data));
+       p->path = path;
+       range_set_append(&p->ranges, begin, end);
+       if (ip) {
+               p->next = ip->next;
+               ip->next = p;
+       } else {
+               p->next = *list;
+               *list = p;
+       }
+}
+
+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 void 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;
+       xdi_diff(parent, target, &xpp, &xecfg, &ecb);
+}
+
+/*
+ * 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)
+{
+       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);
+}
+
+static void dump_line_log_data(struct line_log_data *r)
+{
+       char buf[4096];
+       while (r) {
+               snprintf(buf, 4096, "file %s\n", r->path);
+               dump_range_set(&r->ranges, buf);
+               r = r->next;
+       }
+}
+
+static void dump_diff_ranges(struct diff_ranges *diff, const char *desc)
+{
+       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
+
+
+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)
+{
+       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)
+{
+       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);
+       }
+}
+
+/*
+ * 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)
+{
+       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;
+}
+
+static struct commit *check_single_commit(struct rev_info *revs)
+{
+       struct object *commit = NULL;
+       int found = -1;
+       int i;
+
+       for (i = 0; i < revs->pending.nr; i++) {
+               struct object *obj = revs->pending.objects[i].item;
+               if (obj->flags & UNINTERESTING)
+                       continue;
+               while (obj->type == OBJ_TAG)
+                       obj = deref_tag(obj, NULL, 0);
+               if (obj->type != OBJ_COMMIT)
+                       die("Non commit %s?", revs->pending.objects[i].name);
+               if (commit)
+                       die("More than one commit to dig from: %s and %s?",
+                           revs->pending.objects[i].name,
+                           revs->pending.objects[found].name);
+               commit = obj;
+               found = i;
+       }
+
+       if (!commit)
+               die("No commit specified?");
+
+       return (struct commit *) commit;
+}
+
+static void fill_blob_sha1(struct commit *commit, struct diff_filespec *spec)
+{
+       unsigned mode;
+       unsigned char sha1[20];
+
+       if (get_tree_entry(commit->object.sha1, spec->path,
+                          sha1, &mode))
+               die("There is no path %s in the commit", spec->path);
+       fill_filespec(spec, sha1, 1, mode);
+
+       return;
+}
+
+static void fill_line_ends(struct diff_filespec *spec, long *lines,
+                          unsigned long **line_ends)
+{
+       int num = 0, size = 50;
+       long cur = 0;
+       unsigned long *ends = NULL;
+       char *data = NULL;
+
+       if (diff_populate_filespec(spec, 0))
+               die("Cannot read blob %s", sha1_to_hex(spec->sha1));
+
+       ends = xmalloc(size * sizeof(*ends));
+       ends[cur++] = 0;
+       data = spec->data;
+       while (num < spec->size) {
+               if (data[num] == '\n' || num == spec->size - 1) {
+                       ALLOC_GROW(ends, (cur + 1), size);
+                       ends[cur++] = num;
+               }
+               num++;
+       }
+
+       /* shrink the array to fit the elements */
+       ends = xrealloc(ends, cur * sizeof(*ends));
+       *lines = cur-1;
+       *line_ends = ends;
+}
+
+struct nth_line_cb {
+       struct diff_filespec *spec;
+       long lines;
+       unsigned long *line_ends;
+};
+
+static const char *nth_line(void *data, long line)
+{
+       struct nth_line_cb *d = data;
+       assert(d && line <= d->lines);
+       assert(d->spec && d->spec->data);
+
+       if (line == 0)
+               return (char *)d->spec->data;
+       else
+               return (char *)d->spec->data + d->line_ends[line] + 1;
+}
+
+static struct line_log_data *
+parse_lines(struct commit *commit, const char *prefix, struct string_list *args)
+{
+       long lines = 0;
+       unsigned long *ends = NULL;
+       struct nth_line_cb cb_data;
+       struct string_list_item *item;
+       struct line_log_data *ranges = NULL;
+
+       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;
+
+               name_part = skip_range_arg(item->string);
+               if (!name_part || *name_part != ':' || !name_part[1])
+                       die("-L argument '%s' not of the form start,end:file",
+                           item->string);
+               range_part = xstrndup(item->string, name_part - item->string);
+               name_part++;
+
+               full_name = prefix_path(prefix, prefix ? strlen(prefix) : 0,
+                                       name_part);
+
+               spec = alloc_filespec(full_name);
+               fill_blob_sha1(commit, spec);
+               fill_line_ends(spec, &lines, &ends);
+               cb_data.spec = spec;
+               cb_data.lines = lines;
+               cb_data.line_ends = ends;
+
+               if (parse_range_arg(range_part, nth_line, &cb_data,
+                                   lines, &begin, &end,
+                                   full_name))
+                       die("malformed -L argument '%s'", range_part);
+               if (begin < 1)
+                       begin = 1;
+               if (end < 1)
+                       end = lines;
+               begin--;
+               if (lines < end || lines < begin)
+                       die("file %s has only %ld lines", name_part, lines);
+               line_log_data_insert(&ranges, full_name, begin, end);
+
+               free_filespec(spec);
+               free(ends);
+               ends = NULL;
+       }
+
+       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 = NULL;
+       struct line_log_data *new = NULL;
+
+       old = lookup_decoration(&revs->line_log_data, &commit->object);
+       if (old && range) {
+               new = line_log_data_merge(old, range);
+               free_line_log_data(old);
+       } else if (range)
+               new = line_log_data_copy(range);
+
+       if (new)
+               add_decoration(&revs->line_log_data, &commit->object, new);
+}
+
+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;
+}
+
+void line_log_init(struct rev_info *rev, const char *prefix, struct string_list *args)
+{
+       struct commit *commit = NULL;
+       struct line_log_data *range;
+
+       commit = check_single_commit(rev);
+       range = parse_lines(commit, prefix, args);
+       add_line_range(rev, commit, range);
+
+       if (!rev->diffopt.detect_rename) {
+               int i, count = 0;
+               struct line_log_data *r = range;
+               const char **paths;
+               while (r) {
+                       count++;
+                       r = r->next;
+               }
+               paths = xmalloc((count+1)*sizeof(char *));
+               r = range;
+               for (i = 0; i < count; i++) {
+                       paths[i] = xstrdup(r->path);
+                       r = r->next;
+               }
+               paths[count] = NULL;
+               init_pathspec(&rev->diffopt.pathspec, paths);
+               free(paths);
+       }
+}
+
+static void load_tree_desc(struct tree_desc *desc, void **tree,
+                          const unsigned char *sha1)
+{
+       unsigned long size;
+       *tree = read_object_with_reference(sha1, tree_type, &size, NULL);
+       if (!*tree)
+               die("Unable to read tree (%s)", sha1_to_hex(sha1));
+       init_tree_desc(desc, *tree, size);
+}
+
+static int count_parents(struct commit *commit)
+{
+       struct commit_list *parents = commit->parents;
+       int count = 0;
+       while (parents) {
+               count++;
+               parents = parents->next;
+       }
+       return count;
+}
+
+static void move_diff_queue(struct diff_queue_struct *dst,
+                           struct diff_queue_struct *src)
+{
+       assert(src != dst);
+       memcpy(dst, src, sizeof(struct diff_queue_struct));
+       DIFF_QUEUE_CLEAR(src);
+}
+
+static void filter_diffs_for_paths(struct line_log_data *range, int keep_deletions)
+{
+       int i;
+       struct diff_queue_struct outq;
+       DIFF_QUEUE_CLEAR(&outq);
+
+       for (i = 0; i < diff_queued_diff.nr; i++) {
+               struct diff_filepair *p = diff_queued_diff.queue[i];
+               struct line_log_data *rg = NULL;
+
+               if (!DIFF_FILE_VALID(p->two)) {
+                       if (keep_deletions)
+                               diff_q(&outq, p);
+                       else
+                               diff_free_filepair(p);
+                       continue;
+               }
+               for (rg = range; rg; rg = rg->next) {
+                       if (!strcmp(rg->path, p->two->path))
+                               break;
+               }
+               if (rg)
+                       diff_q(&outq, p);
+               else
+                       diff_free_filepair(p);
+       }
+       free(diff_queued_diff.queue);
+       diff_queued_diff = outq;
+}
+
+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); */
+                       return 1;
+               }
+       return 0;
+}
+
+static void queue_diffs(struct line_log_data *range,
+                       struct diff_options *opt,
+                       struct diff_queue_struct *queue,
+                       struct commit *commit, struct commit *parent)
+{
+       void *tree1 = NULL, *tree2 = NULL;
+       struct tree_desc desc1, desc2;
+
+       assert(commit);
+       load_tree_desc(&desc2, &tree2, commit->tree->object.sha1);
+       if (parent)
+               load_tree_desc(&desc1, &tree1, parent->tree->object.sha1);
+       else
+               init_tree_desc(&desc1, "", 0);
+
+       DIFF_QUEUE_CLEAR(&diff_queued_diff);
+       diff_tree(&desc1, &desc2, "", opt);
+       if (opt->detect_rename) {
+               filter_diffs_for_paths(range, 1);
+               if (diff_might_be_rename())
+                       diffcore_std(opt);
+               filter_diffs_for_paths(range, 0);
+       }
+       move_diff_queue(queue, &diff_queued_diff);
+
+       if (tree1)
+               free(tree1);
+       if (tree2)
+               free(tree2);
+}
+
+static char *get_nth_line(long line, unsigned long *ends, void *data)
+{
+       if (line == 0)
+               return (char *)data;
+       else
+               return (char *)data + ends[line] + 1;
+}
+
+static void print_line(const char *prefix, char first,
+                      long line, unsigned long *ends, void *data,
+                      const char *color, const char *reset)
+{
+       char *begin = get_nth_line(line, ends, data);
+       char *end = get_nth_line(line+1, ends, data);
+       int had_nl = 0;
+
+       if (end > begin && end[-1] == '\n') {
+               end--;
+               had_nl = 1;
+       }
+
+       fputs(prefix, stdout);
+       fputs(color, stdout);
+       putchar(first);
+       fwrite(begin, 1, end-begin, stdout);
+       fputs(reset, stdout);
+       putchar('\n');
+       if (!had_nl)
+               fputs("\\ No newline at end of file\n", stdout);
+}
+
+static char *output_prefix(struct diff_options *opt)
+{
+       char *prefix = "";
+
+       if (opt->output_prefix) {
+               struct strbuf *sb = opt->output_prefix(opt, opt->output_prefix_data);
+               prefix = sb->buf;
+       }
+
+       return prefix;
+}
+
+static void dump_diff_hacky_one(struct rev_info *rev, struct line_log_data *range)
+{
+       int i, j = 0;
+       long p_lines, t_lines;
+       unsigned long *p_ends = NULL, *t_ends = NULL;
+       struct diff_filepair *pair = range->pair;
+       struct diff_ranges *diff = &range->diff;
+
+       struct diff_options *opt = &rev->diffopt;
+       char *prefix = output_prefix(opt);
+       const char *c_reset = diff_get_color(opt->use_color, DIFF_RESET);
+       const char *c_frag = diff_get_color(opt->use_color, DIFF_FRAGINFO);
+       const char *c_meta = diff_get_color(opt->use_color, DIFF_METAINFO);
+       const char *c_old = diff_get_color(opt->use_color, DIFF_FILE_OLD);
+       const char *c_new = diff_get_color(opt->use_color, DIFF_FILE_NEW);
+       const char *c_plain = diff_get_color(opt->use_color, DIFF_PLAIN);
+
+       if (!pair || !diff)
+               return;
+
+       if (pair->one->sha1_valid)
+               fill_line_ends(pair->one, &p_lines, &p_ends);
+       fill_line_ends(pair->two, &t_lines, &t_ends);
+
+       printf("%s%sdiff --git a/%s b/%s%s\n", prefix, c_meta, pair->one->path, pair->two->path, c_reset);
+       printf("%s%s--- %s%s%s\n", prefix, c_meta,
+              pair->one->sha1_valid ? "a/" : "",
+              pair->one->sha1_valid ? pair->one->path : "/dev/null",
+              c_reset);
+       printf("%s%s+++ b/%s%s\n", prefix, c_meta, pair->two->path, c_reset);
+       for (i = 0; i < range->ranges.nr; i++) {
+               long p_start, p_end;
+               long t_start = range->ranges.ranges[i].start;
+               long t_end = range->ranges.ranges[i].end;
+               long t_cur = t_start;
+               int j_last;
+
+               while (j < diff->target.nr && diff->target.ranges[j].end < t_start)
+                       j++;
+               if (j == diff->target.nr || diff->target.ranges[j].start > t_end)
+                       continue;
+
+               /* Scan ahead to determine the last diff that falls in this range */
+               j_last = j;
+               while (j_last < diff->target.nr && diff->target.ranges[j_last].start < t_end)
+                       j_last++;
+               if (j_last > j)
+                       j_last--;
+
+               /*
+                * Compute parent hunk headers: we know that the diff
+                * has the correct line numbers (but not all hunks).
+                * So it suffices to shift the start/end according to
+                * the line numbers of the first/last hunk(s) that
+                * fall in this range.
+                */
+               if (t_start < diff->target.ranges[j].start)
+                       p_start = diff->parent.ranges[j].start - (diff->target.ranges[j].start-t_start);
+               else
+                       p_start = diff->parent.ranges[j].start;
+               if (t_end > diff->target.ranges[j_last].end)
+                       p_end = diff->parent.ranges[j_last].end + (t_end-diff->target.ranges[j_last].end);
+               else
+                       p_end = diff->parent.ranges[j_last].end;
+
+               if (!p_start && !p_end) {
+                       p_start = -1;
+                       p_end = -1;
+               }
+
+               /* Now output a diff hunk for this range */
+               printf("%s%s@@ -%ld,%ld +%ld,%ld @@%s\n",
+                      prefix, c_frag,
+                      p_start+1, p_end-p_start, t_start+1, t_end-t_start,
+                      c_reset);
+               while (j < diff->target.nr && diff->target.ranges[j].start < t_end) {
+                       int k;
+                       for (; t_cur < diff->target.ranges[j].start; t_cur++)
+                               print_line(prefix, ' ', t_cur, t_ends, pair->two->data,
+                                          c_plain, c_reset);
+                       for (k = diff->parent.ranges[j].start; k < diff->parent.ranges[j].end; k++)
+                               print_line(prefix, '-', k, p_ends, pair->one->data,
+                                          c_old, c_reset);
+                       for (; t_cur < diff->target.ranges[j].end && t_cur < t_end; t_cur++)
+                               print_line(prefix, '+', t_cur, t_ends, pair->two->data,
+                                          c_new, c_reset);
+                       j++;
+               }
+               for (; t_cur < t_end; t_cur++)
+                       print_line(prefix, ' ', t_cur, t_ends, pair->two->data,
+                                  c_plain, c_reset);
+       }
+
+       free(p_ends);
+       free(t_ends);
+}
+
+/*
+ * NEEDSWORK: manually building a diff here is not the Right
+ * Thing(tm).  log -L should be built into the diff pipeline.
+ */
+static void dump_diff_hacky(struct rev_info *rev, struct line_log_data *range)
+{
+       puts(output_prefix(&rev->diffopt));
+       while (range) {
+               dump_diff_hacky_one(rev, range);
+               range = range->next;
+       }
+}
+
+/*
+ * Unlike most other functions, this destructively operates on
+ * 'range'.
+ */
+static int process_diff_filepair(struct rev_info *rev,
+                                struct diff_filepair *pair,
+                                struct line_log_data *range,
+                                struct diff_ranges **diff_out)
+{
+       struct line_log_data *rg = range;
+       struct range_set tmp;
+       struct diff_ranges diff;
+       mmfile_t file_parent, file_target;
+
+       assert(pair->two->path);
+       while (rg) {
+               assert(rg->path);
+               if (!strcmp(rg->path, pair->two->path))
+                       break;
+               rg = rg->next;
+       }
+
+       if (!rg)
+               return 0;
+       if (rg->ranges.nr == 0)
+               return 0;
+
+       assert(pair->two->sha1_valid);
+       diff_populate_filespec(pair->two, 0);
+       file_target.ptr = pair->two->data;
+       file_target.size = pair->two->size;
+
+       if (pair->one->sha1_valid) {
+               diff_populate_filespec(pair->one, 0);
+               file_parent.ptr = pair->one->data;
+               file_parent.size = pair->one->size;
+       } else {
+               file_parent.ptr = "";
+               file_parent.size = 0;
+       }
+
+       diff_ranges_init(&diff);
+       collect_diff(&file_parent, &file_target, &diff);
+
+       /* NEEDSWORK should apply some heuristics to prevent mismatches */
+       free(rg->path);
+       rg->path = xstrdup(pair->one->path);
+
+       range_set_init(&tmp, 0);
+       range_set_map_across_diff(&tmp, &rg->ranges, &diff, diff_out);
+       range_set_release(&rg->ranges);
+       range_set_move(&rg->ranges, &tmp);
+
+       diff_ranges_release(&diff);
+
+       return ((*diff_out)->parent.nr > 0);
+}
+
+static struct diff_filepair *diff_filepair_dup(struct diff_filepair *pair)
+{
+       struct diff_filepair *new = xmalloc(sizeof(struct diff_filepair));
+       new->one = pair->one;
+       new->two = pair->two;
+       new->one->count++;
+       new->two->count++;
+       return new;
+}
+
+static void free_diffqueues(int n, struct diff_queue_struct *dq)
+{
+       int i, j;
+       for (i = 0; i < n; i++)
+               for (j = 0; j < dq[i].nr; j++)
+                       diff_free_filepair(dq[i].queue[j]);
+       free(dq);
+}
+
+static int process_all_files(struct line_log_data **range_out,
+                            struct rev_info *rev,
+                            struct diff_queue_struct *queue,
+                            struct line_log_data *range)
+{
+       int i, changed = 0;
+
+       *range_out = line_log_data_copy(range);
+
+       for (i = 0; i < queue->nr; i++) {
+               struct diff_ranges *pairdiff = NULL;
+               struct diff_filepair *pair = queue->queue[i];
+               if (process_diff_filepair(rev, pair, *range_out, &pairdiff)) {
+                       /*
+                        * Store away the diff for later output.  We
+                        * tuck it in the ranges we got as _input_,
+                        * since that's the commit that caused the
+                        * diff.
+                        *
+                        * NEEDSWORK not enough when we get around to
+                        * doing something interesting with merges;
+                        * currently each invocation on a merge parent
+                        * trashes the previous one's diff.
+                        *
+                        * NEEDSWORK tramples over data structures not owned here
+                        */
+                       struct line_log_data *rg = range;
+                       changed++;
+                       while (rg && strcmp(rg->path, pair->two->path))
+                               rg = rg->next;
+                       assert(rg);
+                       rg->pair = diff_filepair_dup(queue->queue[i]);
+                       memcpy(&rg->diff, pairdiff, sizeof(struct diff_ranges));
+               }
+       }
+
+       return changed;
+}
+
+int line_log_print(struct rev_info *rev, struct commit *commit)
+{
+       struct line_log_data *range = lookup_line_range(rev, commit);
+
+       show_log(rev);
+       dump_diff_hacky(rev, range);
+       return 1;
+}
+
+static int process_ranges_ordinary_commit(struct rev_info *rev, struct commit *commit,
+                                         struct line_log_data *range)
+{
+       struct commit *parent = NULL;
+       struct diff_queue_struct queue;
+       struct line_log_data *parent_range;
+       int changed;
+
+       if (commit->parents)
+               parent = commit->parents->item;
+
+       queue_diffs(range, &rev->diffopt, &queue, commit, parent);
+       changed = process_all_files(&parent_range, rev, &queue, range);
+       if (parent)
+               add_line_range(rev, parent, parent_range);
+       return changed;
+}
+
+static int process_ranges_merge_commit(struct rev_info *rev, struct commit *commit,
+                                      struct line_log_data *range)
+{
+       struct diff_queue_struct *diffqueues;
+       struct line_log_data **cand;
+       struct commit **parents;
+       struct commit_list *p;
+       int i;
+       int nparents = count_parents(commit);
+
+       diffqueues = xmalloc(nparents * sizeof(*diffqueues));
+       cand = xmalloc(nparents * sizeof(*cand));
+       parents = xmalloc(nparents * sizeof(*parents));
+
+       p = commit->parents;
+       for (i = 0; i < nparents; i++) {
+               parents[i] = p->item;
+               p = p->next;
+               queue_diffs(range, &rev->diffopt, &diffqueues[i], commit, parents[i]);
+       }
+
+       for (i = 0; i < nparents; i++) {
+               int changed;
+               cand[i] = NULL;
+               changed = process_all_files(&cand[i], rev, &diffqueues[i], range);
+               if (!changed) {
+                       /*
+                        * This parent can take all the blame, so we
+                        * don't follow any other path in history
+                        */
+                       add_line_range(rev, parents[i], cand[i]);
+                       clear_commit_line_range(rev, commit);
+                       commit->parents = xmalloc(sizeof(struct commit_list));
+                       commit->parents->item = parents[i];
+                       commit->parents->next = NULL;
+                       free(parents);
+                       free(cand);
+                       free_diffqueues(nparents, diffqueues);
+                       /* NEEDSWORK leaking like a sieve */
+                       return 0;
+               }
+       }
+
+       /*
+        * No single parent took the blame.  We add the candidates
+        * from the above loop to the parents.
+        */
+       for (i = 0; i < nparents; i++) {
+               add_line_range(rev, parents[i], cand[i]);
+       }
+
+       clear_commit_line_range(rev, commit);
+       free(parents);
+       free(cand);
+       free_diffqueues(nparents, diffqueues);
+       return 1;
+
+       /* NEEDSWORK evil merge detection stuff */
+       /* NEEDSWORK leaking like a sieve */
+}
+
+static int process_ranges_arbitrary_commit(struct rev_info *rev, struct commit *commit)
+{
+       struct line_log_data *range = lookup_line_range(rev, commit);
+       int changed = 0;
+
+       if (range) {
+               if (!commit->parents || !commit->parents->next)
+                       changed = process_ranges_ordinary_commit(rev, commit, range);
+               else
+                       changed = process_ranges_merge_commit(rev, commit, range);
+       }
+
+       if (!changed)
+               commit->object.flags |= TREESAME;
+
+       return changed;
+}
+
+static enum rewrite_result line_log_rewrite_one(struct rev_info *rev, struct commit **pp)
+{
+       for (;;) {
+               struct commit *p = *pp;
+               if (p->parents && p->parents->next)
+                       return rewrite_one_ok;
+               if (p->object.flags & UNINTERESTING)
+                       return rewrite_one_ok;
+               if (!(p->object.flags & TREESAME))
+                       return rewrite_one_ok;
+               if (!p->parents)
+                       return rewrite_one_noparents;
+               *pp = p->parents->item;
+       }
+}
+
+int line_log_filter(struct rev_info *rev)
+{
+       struct commit *commit;
+       struct commit_list *list = rev->commits;
+       struct commit_list *out = NULL, **pp = &out;
+
+       while (list) {
+               struct commit_list *to_free = NULL;
+               commit = list->item;
+               if (process_ranges_arbitrary_commit(rev, commit)) {
+                       *pp = list;
+                       pp = &list->next;
+               } else
+                       to_free = list;
+               list = list->next;
+               free(to_free);
+       }
+       *pp = NULL;
+
+       for (list = out; list; list = list->next)
+               rewrite_parents(rev, list->item, line_log_rewrite_one);
+
+       rev->commits = out;
+
+       return 0;
+}
diff --git a/line-log.h b/line-log.h
new file mode 100644 (file)
index 0000000..8bea45f
--- /dev/null
@@ -0,0 +1,53 @@
+#ifndef LINE_LOG_H
+#define LINE_LOG_H
+
+#include "diffcore.h"
+
+struct rev_info;
+struct commit;
+
+/* A range [start,end].  Lines are numbered starting at 0, and the
+ * ranges include start but exclude end. */
+struct range {
+       long start, end;
+};
+
+/* A set of ranges.  The ranges must always be disjoint and sorted. */
+struct range_set {
+       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;
+};
+
+/* Linked list of interesting files and their associated ranges.  The
+ * list must be kept sorted by path.
+ *
+ * For simplicity, even though this is highly redundant, each
+ * line_log_data owns its 'path'.
+ */
+struct line_log_data {
+       struct line_log_data *next;
+       char *path;
+       char status;
+       struct range_set ranges;
+       int arg_alloc, arg_nr;
+       const char **args;
+       struct diff_filepair *pair;
+       struct diff_ranges diff;
+};
+
+extern void line_log_data_init(struct line_log_data *r);
+
+extern void line_log_init(struct rev_info *rev, const char *prefix, struct string_list *args);
+
+extern int line_log_filter(struct rev_info *rev);
+
+extern int line_log_print(struct rev_info *rev, struct commit *commit);
+
+#endif /* LINE_LOG_H */
diff --git a/line-range.c b/line-range.c
new file mode 100644 (file)
index 0000000..8faf943
--- /dev/null
@@ -0,0 +1,243 @@
+#include "git-compat-util.h"
+#include "line-range.h"
+#include "xdiff-interface.h"
+#include "strbuf.h"
+#include "userdiff.h"
+
+/*
+ * Parse one item in the -L option
+ */
+static const char *parse_loc(const char *spec, nth_line_fn_t nth_line,
+                            void *data, long lines, long begin, long *ret)
+{
+       char *term;
+       const char *line;
+       long num;
+       int reg_error;
+       regex_t regexp;
+       regmatch_t match[1];
+
+       /* Allow "-L <something>,+20" to mean starting at <something>
+        * for 20 lines, or "-L <something>,-5" for 5 lines ending at
+        * <something>.
+        */
+       if (1 < begin && (spec[0] == '+' || spec[0] == '-')) {
+               num = strtol(spec + 1, &term, 10);
+               if (term != spec + 1) {
+                       if (!ret)
+                               return term;
+                       if (spec[0] == '-')
+                               num = 0 - num;
+                       if (0 < num)
+                               *ret = begin + num - 2;
+                       else if (!num)
+                               *ret = begin;
+                       else
+                               *ret = begin + num;
+                       return term;
+               }
+               return spec;
+       }
+       num = strtol(spec, &term, 10);
+       if (term != spec) {
+               if (ret)
+                       *ret = num;
+               return term;
+       }
+       if (spec[0] != '/')
+               return spec;
+
+       /* it could be a regexp of form /.../ */
+       for (term = (char *) spec + 1; *term && *term != '/'; term++) {
+               if (*term == '\\')
+                       term++;
+       }
+       if (*term != '/')
+               return spec;
+
+       /* in the scan-only case we are not interested in the regex */
+       if (!ret)
+               return term+1;
+
+       /* try [spec+1 .. term-1] as regexp */
+       *term = 0;
+       begin--; /* input is in human terms */
+       line = nth_line(data, begin);
+
+       if (!(reg_error = regcomp(&regexp, spec + 1, REG_NEWLINE)) &&
+           !(reg_error = regexec(&regexp, line, 1, match, 0))) {
+               const char *cp = line + match[0].rm_so;
+               const char *nline;
+
+               while (begin++ < lines) {
+                       nline = nth_line(data, begin);
+                       if (line <= cp && cp < nline)
+                               break;
+                       line = nline;
+               }
+               *ret = begin;
+               regfree(&regexp);
+               *term++ = '/';
+               return term;
+       }
+       else {
+               char errbuf[1024];
+               regerror(reg_error, &regexp, errbuf, 1024);
+               die("-L parameter '%s': %s", spec + 1, errbuf);
+       }
+}
+
+static int match_funcname(xdemitconf_t *xecfg, const char *bol, const char *eol)
+{
+       if (xecfg) {
+               char buf[1];
+               return xecfg->find_func(bol, eol - bol, buf, 1,
+                                       xecfg->find_func_priv) >= 0;
+       }
+
+       if (bol == eol)
+               return 0;
+       if (isalpha(*bol) || *bol == '_' || *bol == '$')
+               return 1;
+       return 0;
+}
+
+static const char *find_funcname_matching_regexp(xdemitconf_t *xecfg, const char *start,
+                                                regex_t *regexp)
+{
+       int reg_error;
+       regmatch_t match[1];
+       while (1) {
+               const char *bol, *eol;
+               reg_error = regexec(regexp, start, 1, match, 0);
+               if (reg_error == REG_NOMATCH)
+                       return NULL;
+               else if (reg_error) {
+                       char errbuf[1024];
+                       regerror(reg_error, regexp, errbuf, 1024);
+                       die("-L parameter: regexec() failed: %s", errbuf);
+               }
+               /* determine extent of line matched */
+               bol = start+match[0].rm_so;
+               eol = start+match[0].rm_eo;
+               while (bol > start && *bol != '\n')
+                       bol--;
+               if (*bol == '\n')
+                       bol++;
+               while (*eol && *eol != '\n')
+                       eol++;
+               if (*eol == '\n')
+                       eol++;
+               /* is it a funcname line? */
+               if (match_funcname(xecfg, (char*) bol, (char*) eol))
+                       return bol;
+               start = eol;
+       }
+}
+
+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,
+                                       const char *path)
+{
+       char *pattern;
+       const char *term;
+       struct userdiff_driver *drv;
+       xdemitconf_t *xecfg = NULL;
+       const char *start;
+       const char *p;
+       int reg_error;
+       regex_t regexp;
+
+       assert(*arg == ':');
+       term = arg+1;
+       while (*term && *term != ':') {
+               if (*term == '\\' && *(term+1))
+                       term++;
+               term++;
+       }
+       if (term == arg+1)
+               return NULL;
+       if (!begin) /* skip_range_arg case */
+               return term;
+
+       pattern = xstrndup(arg+1, term-(arg+1));
+
+       start = nth_line_cb(cb_data, 0);
+
+       drv = userdiff_find_by_path(path);
+       if (drv && drv->funcname.pattern) {
+               const struct userdiff_funcname *pe = &drv->funcname;
+               xecfg = xcalloc(1, sizeof(*xecfg));
+               xdiff_set_find_func(xecfg, pe->pattern, pe->cflags);
+       }
+
+       reg_error = regcomp(&regexp, pattern, REG_NEWLINE);
+       if (reg_error) {
+               char errbuf[1024];
+               regerror(reg_error, &regexp, errbuf, 1024);
+               die("-L parameter '%s': %s", pattern, errbuf);
+       }
+
+       p = find_funcname_matching_regexp(xecfg, (char*) start, &regexp);
+       if (!p)
+               die("-L parameter '%s': no match", pattern);
+       *begin = 0;
+       while (p > nth_line_cb(cb_data, *begin))
+               (*begin)++;
+
+       if (*begin >= lines)
+               die("-L parameter '%s' matches at EOF", pattern);
+
+       *end = *begin+1;
+       while (*end < lines) {
+               const char *bol = nth_line_cb(cb_data, *end);
+               const char *eol = nth_line_cb(cb_data, *end+1);
+               if (match_funcname(xecfg, bol, eol))
+                       break;
+               (*end)++;
+       }
+
+       regfree(&regexp);
+       free(xecfg);
+       free(pattern);
+
+       /* compensate for 1-based numbering */
+       (*begin)++;
+
+       return term;
+}
+
+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)
+{
+       if (*arg == ':') {
+               arg = parse_range_funcname(arg, nth_line_cb, cb_data, lines, begin, end, path);
+               if (!arg || *arg)
+                       return -1;
+               return 0;
+       }
+
+       arg = parse_loc(arg, nth_line_cb, cb_data, lines, 1, begin);
+
+       if (*arg == ',')
+               arg = parse_loc(arg + 1, nth_line_cb, cb_data, lines, *begin + 1, end);
+
+       if (*arg)
+               return -1;
+
+       return 0;
+}
+
+const char *skip_range_arg(const char *arg)
+{
+       if (*arg == ':')
+               return parse_range_funcname(arg, NULL, NULL, 0, NULL, NULL, NULL);
+
+       arg = parse_loc(arg, NULL, NULL, 0, -1, NULL);
+
+       if (*arg == ',')
+               arg = parse_loc(arg+1, NULL, NULL, 0, 0, NULL);
+
+       return arg;
+}
diff --git a/line-range.h b/line-range.h
new file mode 100644 (file)
index 0000000..ae3d012
--- /dev/null
@@ -0,0 +1,36 @@
+#ifndef LINE_RANGE_H
+#define LINE_RANGE_H
+
+/*
+ * Parse one item in an -L begin,end option w.r.t. the notional file
+ * object 'cb_data' consisting of 'lines' lines.
+ *
+ * The 'nth_line_cb' callback is used to determine the start of the
+ * 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.
+ *
+ * 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.
+ */
+
+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,
+                          long *begin, long *end,
+                          const char *path);
+
+/*
+ * Scan past a range argument that could be parsed by
+ * 'parse_range_arg', to help the caller determine the start of the
+ * filename in '-L n,m:file' syntax.
+ *
+ * Returns a pointer to the first character after the 'n,m' part, or
+ * NULL in case the argument is obviously malformed.
+ */
+
+extern const char *skip_range_arg(const char *arg);
+
+#endif /* LINE_RANGE_H */
index 1946e9ce8d88b73f134490760f76e00b63f1be69..2eb69bcfed096d28bff736deca715458605d902a 100644 (file)
@@ -10,6 +10,7 @@
 #include "color.h"
 #include "gpg-interface.h"
 #include "sequencer.h"
+#include "line-log.h"
 
 struct decoration name_decoration = { "object names" };
 
@@ -796,6 +797,9 @@ int log_tree_commit(struct rev_info *opt, struct commit *commit)
        log.parent = NULL;
        opt->loginfo = &log;
 
+       if (opt->line_level_traverse)
+               return line_log_print(opt, commit);
+
        shown = log_tree_diff(opt, commit, &log);
        if (!shown && opt->loginfo && opt->always_show_header) {
                log.parent = NULL;
index 7f7a8ab7cb38eaceeb177472385a4a5fcfabe91c..518cd086939f306f8fc6d1228bd6e61ea6d0bc22 100644 (file)
@@ -13,6 +13,7 @@
 #include "decorate.h"
 #include "log-tree.h"
 #include "string-list.h"
+#include "line-log.h"
 #include "mailmap.h"
 
 volatile show_early_output_fn_t show_early_output;
@@ -1914,6 +1915,12 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
        if (revs->combine_merges)
                revs->ignore_merges = 0;
        revs->diffopt.abbrev = revs->abbrev;
+
+       if (revs->line_level_traverse) {
+               revs->limited = 1;
+               revs->topo_order = 1;
+       }
+
        diff_setup_done(&revs->diffopt);
 
        grep_commit_pattern_type(GREP_PATTERN_TYPE_UNSPECIFIED,
@@ -2185,6 +2192,8 @@ int prepare_revision_walk(struct rev_info *revs)
                        return -1;
        if (revs->topo_order)
                sort_in_topological_order(&revs->commits, revs->lifo);
+       if (revs->line_level_traverse)
+               line_log_filter(revs);
        if (revs->simplify_merges)
                simplify_merges(revs);
        if (revs->children.name)
@@ -2192,12 +2201,6 @@ int prepare_revision_walk(struct rev_info *revs)
        return 0;
 }
 
-enum rewrite_result {
-       rewrite_one_ok,
-       rewrite_one_noparents,
-       rewrite_one_error
-};
-
 static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp)
 {
        struct commit_list *cache = NULL;
@@ -2219,12 +2222,13 @@ static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp
        }
 }
 
-static int rewrite_parents(struct rev_info *revs, struct commit *commit)
+int rewrite_parents(struct rev_info *revs, struct commit *commit,
+       rewrite_parent_fn_t rewrite_parent)
 {
        struct commit_list **pp = &commit->parents;
        while (*pp) {
                struct commit_list *parent = *pp;
-               switch (rewrite_one(revs, &parent->item)) {
+               switch (rewrite_parent(revs, &parent->item)) {
                case rewrite_one_ok:
                        break;
                case rewrite_one_noparents:
@@ -2390,7 +2394,7 @@ enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit)
        if (action == commit_show &&
            !revs->show_all &&
            revs->prune && revs->dense && want_ancestry(revs)) {
-               if (rewrite_parents(revs, commit) < 0)
+               if (rewrite_parents(revs, commit, rewrite_one) < 0)
                        return commit_error;
        }
        return action;
index 878a5556fd5e1c82f0fc35e1669e33623397ceeb..a313a1340536321080c44d502a2bb6bc105940d5 100644 (file)
@@ -97,7 +97,8 @@ struct rev_info {
                        cherry_mark:1,
                        bisect:1,
                        ancestry_path:1,
-                       first_parent_only:1;
+                       first_parent_only:1,
+                       line_level_traverse:1;
 
        /* Diff flags */
        unsigned int    diff:1,
@@ -176,6 +177,9 @@ struct rev_info {
        int count_left;
        int count_right;
        int count_same;
+
+       /* line level range that we are chasing */
+       struct decoration line_log_data;
 };
 
 #define REV_TREE_SAME          0
@@ -242,4 +246,14 @@ enum commit_action {
 extern enum commit_action get_commit_action(struct rev_info *revs, struct commit *commit);
 extern enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit);
 
+enum rewrite_result {
+       rewrite_one_ok,
+       rewrite_one_noparents,
+       rewrite_one_error
+};
+
+typedef enum rewrite_result (*rewrite_parent_fn_t)(struct rev_info *revs, struct commit **pp);
+
+extern int rewrite_parents(struct rev_info *revs, struct commit *commit,
+       rewrite_parent_fn_t rewrite_parent);
 #endif
diff --git a/t/perf/p4211-line-log.sh b/t/perf/p4211-line-log.sh
new file mode 100755 (executable)
index 0000000..3d074b0
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+test_description='Tests log -L performance'
+. ./perf-lib.sh
+
+test_perf_default_repo
+
+# Pick a file to log pseudo-randomly.  The sort key is the blob hash,
+# so it is stable.
+test_expect_success 'select a file' '
+       git ls-tree HEAD | grep ^100644 |
+       sort -k 3 | head -1 | cut -f 2 >filelist
+'
+
+file=$(cat filelist)
+export file
+
+test_perf 'git rev-list --topo-order (baseline)' '
+       git rev-list --topo-order HEAD >/dev/null
+'
+
+test_perf 'git log --follow (baseline for -M)' '
+       git log --oneline --follow -- "$file" >/dev/null
+'
+
+test_perf 'git log -L' '
+       git log -L 1:"$file" >/dev/null
+'
+
+test_perf 'git log -M -L' '
+       git log -M -L 1:"$file" >/dev/null
+'
+
+test_done
diff --git a/t/t4211-line-log.sh b/t/t4211-line-log.sh
new file mode 100755 (executable)
index 0000000..7776f93
--- /dev/null
@@ -0,0 +1,67 @@
+#!/bin/sh
+
+test_description='test log -L'
+. ./test-lib.sh
+
+test_expect_success 'setup (import history)' '
+       git fast-import < "$TEST_DIRECTORY"/t4211/history.export &&
+       git reset --hard
+'
+
+canned_test_1 () {
+       test_expect_$1 "$2" "
+               git log $2 >actual &&
+               test_cmp \"\$TEST_DIRECTORY\"/t4211/expect.$3 actual
+       "
+}
+
+canned_test () {
+       canned_test_1 success "$@"
+}
+canned_test_failure () {
+       canned_test_1 failure "$@"
+}
+
+test_bad_opts () {
+       test_expect_success "invalid args: $1" "
+               test_must_fail git log $1 2>errors &&
+               grep '$2' errors
+       "
+}
+
+canned_test "-L 4,12:a.c simple" simple-f
+canned_test "-L 4,+9:a.c simple" simple-f
+canned_test "-L '/long f/,/^}/:a.c' simple" simple-f
+canned_test "-L :f:a.c simple" simple-f-to-main
+
+canned_test "-L '/main/,/^}/:a.c' simple" simple-main
+canned_test "-L :main:a.c simple" simple-main-to-end
+
+canned_test "-L 1,+4:a.c simple" beginning-of-file
+
+canned_test "-L 20:a.c simple" end-of-file
+
+canned_test "-L '/long f/',/^}/:a.c -L /main/,/^}/:a.c simple" two-ranges
+canned_test "-L 24,+1:a.c simple" vanishes-early
+
+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 :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
+
+test_bad_opts "-L" "switch.*requires a value"
+test_bad_opts "-L b.c" "argument.*not of the form"
+test_bad_opts "-L 1:" "argument.*not of the form"
+test_bad_opts "-L 1:nonexistent" "There is no path"
+test_bad_opts "-L 1:simple" "There is no path"
+test_bad_opts "-L '/foo:b.c'" "argument.*not of the form"
+test_bad_opts "-L 1000:b.c" "has only.*lines"
+test_bad_opts "-L 1,1000:b.c" "has only.*lines"
+test_bad_opts "-L :b.c" "argument.*not of the form"
+test_bad_opts "-L :foo:b.c" "no match"
+
+test_done
diff --git a/t/t4211/expect.beginning-of-file b/t/t4211/expect.beginning-of-file
new file mode 100644 (file)
index 0000000..91b4054
--- /dev/null
@@ -0,0 +1,43 @@
+commit 4a23ae5c98d59a58c6da036156959f2dc9f472ad
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:47:40 2013 +0100
+
+    change at very beginning
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -1,3 +1,4 @@
++#include <unistd.h>
+ #include <stdio.h>
+ long f(long x)
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -1,3 +1,3 @@
+ #include <stdio.h>
+-int f(int x)
++long f(long x)
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +1,3 @@
++#include <stdio.h>
++
++int f(int x)
diff --git a/t/t4211/expect.end-of-file b/t/t4211/expect.end-of-file
new file mode 100644 (file)
index 0000000..bd25bb2
--- /dev/null
@@ -0,0 +1,62 @@
+commit 4659538844daa2849b1a9e7d6fadb96fcd26fc83
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:43 2013 +0100
+
+    change back to complete line
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -20,3 +20,5 @@
+       printf("%ld\n", f(15));
+       return 0;
+-}
+\ No newline at end of file
++}
++
++/* incomplete lines are bad! */
+
+commit 100b61a6f2f720f812620a9d10afb3a960ccb73c
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:10 2013 +0100
+
+    change to an incomplete line at end
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -20,3 +20,3 @@
+       printf("%ld\n", f(15));
+       return 0;
+-}
++}
+\ No newline at end of file
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -19,3 +19,3 @@
+-      printf("%d\n", f(15));
++      printf("%ld\n", f(15));
+       return 0;
+ }
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +18,3 @@
++      printf("%d\n", f(15));
++      return 0;
++}
diff --git a/t/t4211/expect.move-support-f b/t/t4211/expect.move-support-f
new file mode 100644 (file)
index 0000000..c905e01
--- /dev/null
@@ -0,0 +1,80 @@
+commit 6ce3c4ff690136099bb17e1a8766b75764726ea7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:49:50 2013 +0100
+
+    another simple change
+
+diff --git a/b.c b/b.c
+--- a/b.c
++++ b/b.c
+@@ -4,9 +4,9 @@
+ long f(long x)
+ {
+       int s = 0;
+       while (x) {
+-              x >>= 1;
++              x /= 2;
+               s++;
+       }
+       return s;
+ }
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,9 +3,9 @@
+-int f(int x)
++long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+
+commit f04fb20f2c77850996cba739709acc6faecc58f7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:55 2013 +0100
+
+    change f()
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,8 +3,9 @@
+ int f(int x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
++      return s;
+ }
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +3,8 @@
++int f(int x)
++{
++      int s = 0;
++      while (x) {
++              x >>= 1;
++              s++;
++      }
++}
diff --git a/t/t4211/expect.multiple b/t/t4211/expect.multiple
new file mode 100644 (file)
index 0000000..76ad5b5
--- /dev/null
@@ -0,0 +1,104 @@
+commit 4659538844daa2849b1a9e7d6fadb96fcd26fc83
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:43 2013 +0100
+
+    change back to complete line
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -18,5 +18,7 @@
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
+\ No newline at end of file
++}
++
++/* incomplete lines are bad! */
+
+commit 100b61a6f2f720f812620a9d10afb3a960ccb73c
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:10 2013 +0100
+
+    change to an incomplete line at end
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -18,5 +18,5 @@
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
++}
+\ No newline at end of file
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,9 +3,9 @@
+-int f(int x)
++long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+@@ -17,5 +17,5 @@
+ int main ()
+ {
+-      printf("%d\n", f(15));
++      printf("%ld\n", f(15));
+       return 0;
+ }
+
+commit f04fb20f2c77850996cba739709acc6faecc58f7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:55 2013 +0100
+
+    change f()
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,8 +3,9 @@
+ int f(int x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
++      return s;
+ }
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +3,8 @@
++int f(int x)
++{
++      int s = 0;
++      while (x) {
++              x >>= 1;
++              s++;
++      }
++}
diff --git a/t/t4211/expect.multiple-overlapping b/t/t4211/expect.multiple-overlapping
new file mode 100644 (file)
index 0000000..d930b6e
--- /dev/null
@@ -0,0 +1,187 @@
+commit 4659538844daa2849b1a9e7d6fadb96fcd26fc83
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:43 2013 +0100
+
+    change back to complete line
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -4,19 +4,21 @@
+ long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+ /*
+  * This is only an example!
+  */
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
+\ No newline at end of file
++}
++
++/* incomplete lines are bad! */
+
+commit 100b61a6f2f720f812620a9d10afb3a960ccb73c
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:10 2013 +0100
+
+    change to an incomplete line at end
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -4,19 +4,19 @@
+ long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+ /*
+  * This is only an example!
+  */
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
++}
+\ No newline at end of file
+
+commit 39b6eb2d5b706d3322184a169f666f25ed3fbd00
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:41 2013 +0100
+
+    touch comment
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,19 +3,19 @@
+ long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+ /*
+- * A comment.
++ * This is only an example!
+  */
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+ }
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,19 +3,19 @@
+-int f(int x)
++long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+ /*
+  * A comment.
+  */
+ int main ()
+ {
+-      printf("%d\n", f(15));
++      printf("%ld\n", f(15));
+       return 0;
+ }
+
+commit f04fb20f2c77850996cba739709acc6faecc58f7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:55 2013 +0100
+
+    change f()
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,18 +3,19 @@
+ int f(int x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
++      return s;
+ }
+ /*
+  * A comment.
+  */
+ int main ()
+ {
+       printf("%d\n", f(15));
+       return 0;
+ }
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +3,18 @@
++int f(int x)
++{
++      int s = 0;
++      while (x) {
++              x >>= 1;
++              s++;
++      }
++}
++
++/*
++ * A comment.
++ */
++
++int main ()
++{
++      printf("%d\n", f(15));
++      return 0;
++}
diff --git a/t/t4211/expect.multiple-superset b/t/t4211/expect.multiple-superset
new file mode 100644 (file)
index 0000000..a1f5bc4
--- /dev/null
@@ -0,0 +1,59 @@
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,9 +3,9 @@
+-int f(int x)
++long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+
+commit f04fb20f2c77850996cba739709acc6faecc58f7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:55 2013 +0100
+
+    change f()
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,8 +3,9 @@
+ int f(int x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
++      return s;
+ }
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +3,8 @@
++int f(int x)
++{
++      int s = 0;
++      while (x) {
++              x >>= 1;
++              s++;
++      }
++}
diff --git a/t/t4211/expect.parallel-change-f-to-main b/t/t4211/expect.parallel-change-f-to-main
new file mode 100644 (file)
index 0000000..052def8
--- /dev/null
@@ -0,0 +1,160 @@
+commit 0469c60bc4837d52d97b1f081dec5f98dea20fed
+Merge: ba227c6 6ce3c4f
+Author: Thomas Rast <trast@inf.ethz.ch>
+Date:   Fri Apr 12 16:16:24 2013 +0200
+
+    Merge across the rename
+
+
+commit 6ce3c4ff690136099bb17e1a8766b75764726ea7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:49:50 2013 +0100
+
+    another simple change
+
+diff --git a/b.c b/b.c
+--- a/b.c
++++ b/b.c
+@@ -4,14 +4,14 @@
+ long f(long x)
+ {
+       int s = 0;
+       while (x) {
+-              x >>= 1;
++              x /= 2;
+               s++;
+       }
+       return s;
+ }
+ /*
+  * This is only an example!
+  */
+
+commit ba227c6632349700fbb957dec2b50f5e2358be3f
+Author: Thomas Rast <trast@inf.ethz.ch>
+Date:   Fri Apr 12 16:15:57 2013 +0200
+
+    change on another line of history while rename happens
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -4,14 +4,14 @@
+ long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+ /*
+- * This is only an example!
++ * This is only a short example!
+  */
+
+commit 39b6eb2d5b706d3322184a169f666f25ed3fbd00
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:41 2013 +0100
+
+    touch comment
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,14 +3,14 @@
+ long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+ /*
+- * A comment.
++ * This is only an example!
+  */
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,14 +3,14 @@
+-int f(int x)
++long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+ /*
+  * A comment.
+  */
+
+commit f04fb20f2c77850996cba739709acc6faecc58f7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:55 2013 +0100
+
+    change f()
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,13 +3,14 @@
+ int f(int x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
++      return s;
+ }
+ /*
+  * A comment.
+  */
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +3,13 @@
++int f(int x)
++{
++      int s = 0;
++      while (x) {
++              x >>= 1;
++              s++;
++      }
++}
++
++/*
++ * A comment.
++ */
++
diff --git a/t/t4211/expect.simple-f b/t/t4211/expect.simple-f
new file mode 100644 (file)
index 0000000..a1f5bc4
--- /dev/null
@@ -0,0 +1,59 @@
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,9 +3,9 @@
+-int f(int x)
++long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+
+commit f04fb20f2c77850996cba739709acc6faecc58f7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:55 2013 +0100
+
+    change f()
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,8 +3,9 @@
+ int f(int x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
++      return s;
+ }
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +3,8 @@
++int f(int x)
++{
++      int s = 0;
++      while (x) {
++              x >>= 1;
++              s++;
++      }
++}
diff --git a/t/t4211/expect.simple-f-to-main b/t/t4211/expect.simple-f-to-main
new file mode 100644 (file)
index 0000000..a475768
--- /dev/null
@@ -0,0 +1,100 @@
+commit 39b6eb2d5b706d3322184a169f666f25ed3fbd00
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:41 2013 +0100
+
+    touch comment
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,14 +3,14 @@
+ long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+ /*
+- * A comment.
++ * This is only an example!
+  */
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,14 +3,14 @@
+-int f(int x)
++long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+ /*
+  * A comment.
+  */
+
+commit f04fb20f2c77850996cba739709acc6faecc58f7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:55 2013 +0100
+
+    change f()
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,13 +3,14 @@
+ int f(int x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
++      return s;
+ }
+ /*
+  * A comment.
+  */
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +3,13 @@
++int f(int x)
++{
++      int s = 0;
++      while (x) {
++              x >>= 1;
++              s++;
++      }
++}
++
++/*
++ * A comment.
++ */
++
diff --git a/t/t4211/expect.simple-main b/t/t4211/expect.simple-main
new file mode 100644 (file)
index 0000000..39ce39b
--- /dev/null
@@ -0,0 +1,68 @@
+commit 4659538844daa2849b1a9e7d6fadb96fcd26fc83
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:43 2013 +0100
+
+    change back to complete line
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -18,5 +18,5 @@
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
+\ No newline at end of file
++}
+
+commit 100b61a6f2f720f812620a9d10afb3a960ccb73c
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:10 2013 +0100
+
+    change to an incomplete line at end
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -18,5 +18,5 @@
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
++}
+\ No newline at end of file
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -17,5 +17,5 @@
+ int main ()
+ {
+-      printf("%d\n", f(15));
++      printf("%ld\n", f(15));
+       return 0;
+ }
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +16,5 @@
++int main ()
++{
++      printf("%d\n", f(15));
++      return 0;
++}
diff --git a/t/t4211/expect.simple-main-to-end b/t/t4211/expect.simple-main-to-end
new file mode 100644 (file)
index 0000000..8480bd9
--- /dev/null
@@ -0,0 +1,70 @@
+commit 4659538844daa2849b1a9e7d6fadb96fcd26fc83
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:43 2013 +0100
+
+    change back to complete line
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -18,5 +18,7 @@
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
+\ No newline at end of file
++}
++
++/* incomplete lines are bad! */
+
+commit 100b61a6f2f720f812620a9d10afb3a960ccb73c
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:10 2013 +0100
+
+    change to an incomplete line at end
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -18,5 +18,5 @@
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
++}
+\ No newline at end of file
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -17,5 +17,5 @@
+ int main ()
+ {
+-      printf("%d\n", f(15));
++      printf("%ld\n", f(15));
+       return 0;
+ }
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +16,5 @@
++int main ()
++{
++      printf("%d\n", f(15));
++      return 0;
++}
diff --git a/t/t4211/expect.two-ranges b/t/t4211/expect.two-ranges
new file mode 100644 (file)
index 0000000..6109aa0
--- /dev/null
@@ -0,0 +1,102 @@
+commit 4659538844daa2849b1a9e7d6fadb96fcd26fc83
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:43 2013 +0100
+
+    change back to complete line
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -18,5 +18,5 @@
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
+\ No newline at end of file
++}
+
+commit 100b61a6f2f720f812620a9d10afb3a960ccb73c
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:10 2013 +0100
+
+    change to an incomplete line at end
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -18,5 +18,5 @@
+ int main ()
+ {
+       printf("%ld\n", f(15));
+       return 0;
+-}
++}
+\ No newline at end of file
+
+commit a6eb82647d5d67f893da442f8f9375fd89a3b1e2
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:45:16 2013 +0100
+
+    touch both functions
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,9 +3,9 @@
+-int f(int x)
++long f(long x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+ }
+@@ -17,5 +17,5 @@
+ int main ()
+ {
+-      printf("%d\n", f(15));
++      printf("%ld\n", f(15));
+       return 0;
+ }
+
+commit f04fb20f2c77850996cba739709acc6faecc58f7
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:55 2013 +0100
+
+    change f()
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -3,8 +3,9 @@
+ int f(int x)
+ {
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
++      return s;
+ }
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +3,8 @@
++int f(int x)
++{
++      int s = 0;
++      while (x) {
++              x >>= 1;
++              s++;
++      }
++}
diff --git a/t/t4211/expect.vanishes-early b/t/t4211/expect.vanishes-early
new file mode 100644 (file)
index 0000000..1f7cd06
--- /dev/null
@@ -0,0 +1,39 @@
+commit 4659538844daa2849b1a9e7d6fadb96fcd26fc83
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:43 2013 +0100
+
+    change back to complete line
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -22,1 +24,1 @@
+-}
+\ No newline at end of file
++/* incomplete lines are bad! */
+
+commit 100b61a6f2f720f812620a9d10afb3a960ccb73c
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:48:10 2013 +0100
+
+    change to an incomplete line at end
+
+diff --git a/a.c b/a.c
+--- a/a.c
++++ b/a.c
+@@ -22,1 +22,1 @@
+-}
++}
+\ No newline at end of file
+
+commit de4c48ae814792c02a49c4c3c0c757ae69c55f6a
+Author: Thomas Rast <trast@student.ethz.ch>
+Date:   Thu Feb 28 10:44:48 2013 +0100
+
+    initial
+
+diff --git a/a.c b/a.c
+--- /dev/null
++++ b/a.c
+@@ -0,0 +20,1 @@
++}
diff --git a/t/t4211/history.export b/t/t4211/history.export
new file mode 100644 (file)
index 0000000..f9f41e2
--- /dev/null
@@ -0,0 +1,406 @@
+blob
+mark :1
+data 157
+#include <stdio.h>
+
+int f(int x)
+{
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+}
+
+/*
+ * A comment.
+ */
+
+int main ()
+{
+       printf("%d\n", f(15));
+       return 0;
+}
+
+reset refs/tags/simple
+commit refs/tags/simple
+mark :2
+author Thomas Rast <trast@student.ethz.ch> 1362044688 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362044688 +0100
+data 8
+initial
+M 100644 :1 a.c
+
+blob
+mark :3
+data 168
+#include <stdio.h>
+
+int f(int x)
+{
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+}
+
+/*
+ * A comment.
+ */
+
+int main ()
+{
+       printf("%d\n", f(15));
+       return 0;
+}
+
+commit refs/tags/simple
+mark :4
+author Thomas Rast <trast@student.ethz.ch> 1362044695 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362044695 +0100
+data 11
+change f()
+from :2
+M 100644 :3 a.c
+
+blob
+mark :5
+data 171
+#include <stdio.h>
+
+long f(long x)
+{
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+}
+
+/*
+ * A comment.
+ */
+
+int main ()
+{
+       printf("%ld\n", f(15));
+       return 0;
+}
+
+commit refs/tags/simple
+mark :6
+author Thomas Rast <trast@student.ethz.ch> 1362044716 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362044716 +0100
+data 21
+touch both functions
+from :4
+M 100644 :5 a.c
+
+blob
+mark :7
+data 185
+#include <stdio.h>
+
+long f(long x)
+{
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+}
+
+/*
+ * This is only an example!
+ */
+
+int main ()
+{
+       printf("%ld\n", f(15));
+       return 0;
+}
+
+commit refs/tags/simple
+mark :8
+author Thomas Rast <trast@student.ethz.ch> 1362044741 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362044741 +0100
+data 14
+touch comment
+from :6
+M 100644 :7 a.c
+
+blob
+mark :9
+data 205
+#include <unistd.h>
+#include <stdio.h>
+
+long f(long x)
+{
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+}
+
+/*
+ * This is only an example!
+ */
+
+int main ()
+{
+       printf("%ld\n", f(15));
+       return 0;
+}
+
+commit refs/tags/simple
+mark :10
+author Thomas Rast <trast@student.ethz.ch> 1362044860 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362044860 +0100
+data 25
+change at very beginning
+from :8
+M 100644 :9 a.c
+
+blob
+mark :11
+data 204
+#include <unistd.h>
+#include <stdio.h>
+
+long f(long x)
+{
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+}
+
+/*
+ * This is only an example!
+ */
+
+int main ()
+{
+       printf("%ld\n", f(15));
+       return 0;
+}
+commit refs/tags/simple
+mark :12
+author Thomas Rast <trast@student.ethz.ch> 1362044890 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362044890 +0100
+data 36
+change to an incomplete line at end
+from :10
+M 100644 :11 a.c
+
+blob
+mark :13
+data 238
+#include <unistd.h>
+#include <stdio.h>
+
+long f(long x)
+{
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+}
+
+/*
+ * This is only an example!
+ */
+
+int main ()
+{
+       printf("%ld\n", f(15));
+       return 0;
+}
+
+/* incomplete lines are bad! */
+
+commit refs/tags/simple
+mark :14
+author Thomas Rast <trast@student.ethz.ch> 1362044923 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362044923 +0100
+data 29
+change back to complete line
+from :12
+M 100644 :13 a.c
+
+commit refs/tags/move-support
+mark :15
+author Thomas Rast <trast@student.ethz.ch> 1362044968 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362044968 +0100
+data 10
+move file
+from :14
+D a.c
+M 100644 :13 b.c
+
+blob
+mark :16
+data 237
+#include <unistd.h>
+#include <stdio.h>
+
+long f(long x)
+{
+       int s = 0;
+       while (x) {
+               x /= 2;
+               s++;
+       }
+       return s;
+}
+
+/*
+ * This is only an example!
+ */
+
+int main ()
+{
+       printf("%ld\n", f(15));
+       return 0;
+}
+
+/* incomplete lines are bad! */
+
+commit refs/tags/move-support
+mark :17
+author Thomas Rast <trast@student.ethz.ch> 1362044990 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362044990 +0100
+data 22
+another simple change
+from :15
+M 100644 :16 b.c
+
+blob
+mark :18
+data 254
+#include <unistd.h>
+#include <stdio.h>
+
+long f(long x);
+
+/*
+ * This is only an example!
+ */
+
+int main ()
+{
+       printf("%ld\n", f(15));
+       return 0;
+}
+
+/* incomplete lines are bad! */
+
+long f(long x)
+{
+       int s = 0;
+       while (x) {
+               x /= 2;
+               s++;
+       }
+       return s;
+}
+
+commit refs/heads/master
+mark :19
+author Thomas Rast <trast@student.ethz.ch> 1362045024 +0100
+committer Thomas Rast <trast@student.ethz.ch> 1362045024 +0100
+data 21
+move within the file
+from :17
+M 100644 :18 b.c
+
+blob
+mark :20
+data 243
+#include <unistd.h>
+#include <stdio.h>
+
+long f(long x)
+{
+       int s = 0;
+       while (x) {
+               x >>= 1;
+               s++;
+       }
+       return s;
+}
+
+/*
+ * This is only a short example!
+ */
+
+int main ()
+{
+       printf("%ld\n", f(15));
+       return 0;
+}
+
+/* incomplete lines are bad! */
+
+commit refs/heads/parallel-change
+mark :21
+author Thomas Rast <trast@inf.ethz.ch> 1365776157 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1365776157 +0200
+data 55
+change on another line of history while rename happens
+from :14
+M 100644 :20 a.c
+
+blob
+mark :22
+data 242
+#include <unistd.h>
+#include <stdio.h>
+
+long f(long x)
+{
+       int s = 0;
+       while (x) {
+               x /= 2;
+               s++;
+       }
+       return s;
+}
+
+/*
+ * This is only a short example!
+ */
+
+int main ()
+{
+       printf("%ld\n", f(15));
+       return 0;
+}
+
+/* incomplete lines are bad! */
+
+commit refs/heads/parallel-change
+mark :23
+author Thomas Rast <trast@inf.ethz.ch> 1365776184 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1365776191 +0200
+data 24
+Merge across the rename
+from :21
+merge :17
+D a.c
+M 100644 :22 b.c
+
+reset refs/heads/parallel-change
+from :23
+
index 230143cf318705fb01e61f10072a096e86186934..e7cac1db55113188c7149686399292bded9c07dd 100755 (executable)
@@ -175,6 +175,12 @@ test_expect_success 'blame -L with invalid end' '
        grep "has only 2 lines" errors
 '
 
+test_expect_success 'blame parses <end> part of -L' '
+       git blame -L1,1 tres >out &&
+       cat out &&
+       test $(wc -l < out) -eq 1
+'
+
 test_expect_success 'indent of line numbers, nine lines' '
        git blame nine_lines >actual &&
        test $(grep -c "  " actual) = 0