Merge branch 'js/range-diff'
authorJunio C Hamano <gitster@pobox.com>
Mon, 20 Aug 2018 18:33:53 +0000 (11:33 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 20 Aug 2018 18:33:53 +0000 (11:33 -0700)
"git tbdiff" that lets us compare individual patches in two
iterations of a topic has been rewritten and made into a built-in
command.

* js/range-diff: (21 commits)
range-diff: use dim/bold cues to improve dual color mode
range-diff: make --dual-color the default mode
range-diff: left-pad patch numbers
completion: support `git range-diff`
range-diff: populate the man page
range-diff --dual-color: skip white-space warnings
range-diff: offer to dual-color the diffs
diff: add an internal option to dual-color diffs of diffs
color: add the meta color GIT_COLOR_REVERSE
range-diff: use color for the commit pairs
range-diff: add tests
range-diff: do not show "function names" in hunk headers
range-diff: adjust the output of the commit pairs
range-diff: suppress the diff headers
range-diff: indent the diffs just like tbdiff
range-diff: right-trim commit messages
range-diff: also show the diff between patches
range-diff: improve the order of the shown commits
range-diff: first rudimentary implementation
Introduce `range-diff` to compare iterations of a topic branch
...

19 files changed:
.gitignore
Documentation/config.txt
Documentation/git-range-diff.txt [new file with mode: 0644]
Makefile
builtin.h
builtin/range-diff.c [new file with mode: 0644]
color.h
command-list.txt
contrib/completion/git-completion.bash
diff.c
diff.h
git.c
linear-assignment.c [new file with mode: 0644]
linear-assignment.h [new file with mode: 0644]
range-diff.c [new file with mode: 0644]
range-diff.h [new file with mode: 0644]
t/.gitattributes
t/t3206-range-diff.sh [new file with mode: 0755]
t/t3206/history.export [new file with mode: 0644]
index 3524803da5d23fe2fcde45cc31c746d4e73a7795..ffceea7d59fd21d5c5deed2c8d14f507bdbf1666 100644 (file)
 /git-pull
 /git-push
 /git-quiltimport
+/git-range-diff
 /git-read-tree
 /git-rebase
 /git-rebase--am
index 95ad715a440998fc9ed9e21895dd8a8127b089fb..1c78df7b90468db379cbca8790f3cac9a6aef6b3 100644 (file)
@@ -1225,8 +1225,10 @@ color.diff.<slot>::
        (highlighting whitespace errors), `oldMoved` (deleted lines),
        `newMoved` (added lines), `oldMovedDimmed`, `oldMovedAlternative`,
        `oldMovedAlternativeDimmed`, `newMovedDimmed`, `newMovedAlternative`
-       and `newMovedAlternativeDimmed` (See the '<mode>'
-       setting of '--color-moved' in linkgit:git-diff[1] for details).
+       `newMovedAlternativeDimmed` (See the '<mode>'
+       setting of '--color-moved' in linkgit:git-diff[1] for details),
+       `contextDimmed`, `oldDimmed`, `newDimmed`, `contextBold`,
+       `oldBold`, and `newBold` (see linkgit:git-range-diff[1] for details).
 
 color.decorate.<slot>::
        Use customized color for 'git log --decorate' output.  `<slot>` is one
diff --git a/Documentation/git-range-diff.txt b/Documentation/git-range-diff.txt
new file mode 100644 (file)
index 0000000..f693930
--- /dev/null
@@ -0,0 +1,252 @@
+git-range-diff(1)
+=================
+
+NAME
+----
+git-range-diff - Compare two commit ranges (e.g. two versions of a branch)
+
+SYNOPSIS
+--------
+[verse]
+'git range-diff' [--color=[<when>]] [--no-color] [<diff-options>]
+       [--no-dual-color] [--creation-factor=<factor>]
+       ( <range1> <range2> | <rev1>...<rev2> | <base> <rev1> <rev2> )
+
+DESCRIPTION
+-----------
+
+This command shows the differences between two versions of a patch
+series, or more generally, two commit ranges (ignoring merge commits).
+
+To that end, it first finds pairs of commits from both commit ranges
+that correspond with each other. Two commits are said to correspond when
+the diff between their patches (i.e. the author information, the commit
+message and the commit diff) is reasonably small compared to the
+patches' size. See ``Algorithm`` below for details.
+
+Finally, the list of matching commits is shown in the order of the
+second commit range, with unmatched commits being inserted just after
+all of their ancestors have been shown.
+
+
+OPTIONS
+-------
+--no-dual-color::
+       When the commit diffs differ, `git range-diff` recreates the
+       original diffs' coloring, and adds outer -/+ diff markers with
+       the *background* being red/green to make it easier to see e.g.
+       when there was a change in what exact lines were added.
++
+Additionally, the commit diff lines that are only present in the first commit
+range are shown "dimmed" (this can be overridden using the `color.diff.<slot>`
+config setting where `<slot>` is one of `contextDimmed`, `oldDimmed` and
+`newDimmed`), and the commit diff lines that are only present in the second
+commit range are shown in bold (which can be overridden using the config
+settings `color.diff.<slot>` with `<slot>` being one of `contextBold`,
+`oldBold` or `newBold`).
++
+This is known to `range-diff` as "dual coloring". Use `--no-dual-color`
+to revert to color all lines according to the outer diff markers
+(and completely ignore the inner diff when it comes to color).
+
+--creation-factor=<percent>::
+       Set the creation/deletion cost fudge factor to `<percent>`.
+       Defaults to 60. Try a larger value if `git range-diff` erroneously
+       considers a large change a total rewrite (deletion of one commit
+       and addition of another), and a smaller one in the reverse case.
+       See the ``Algorithm`` section below for an explanation why this is
+       needed.
+
+<range1> <range2>::
+       Compare the commits specified by the two ranges, where
+       `<range1>` is considered an older version of `<range2>`.
+
+<rev1>...<rev2>::
+       Equivalent to passing `<rev2>..<rev1>` and `<rev1>..<rev2>`.
+
+<base> <rev1> <rev2>::
+       Equivalent to passing `<base>..<rev1>` and `<base>..<rev2>`.
+       Note that `<base>` does not need to be the exact branch point
+       of the branches. Example: after rebasing a branch `my-topic`,
+       `git range-diff my-topic@{u} my-topic@{1} my-topic` would
+       show the differences introduced by the rebase.
+
+`git range-diff` also accepts the regular diff options (see
+linkgit:git-diff[1]), most notably the `--color=[<when>]` and
+`--no-color` options. These options are used when generating the "diff
+between patches", i.e. to compare the author, commit message and diff of
+corresponding old/new commits. There is currently no means to tweak the
+diff options passed to `git log` when generating those patches.
+
+
+CONFIGURATION
+-------------
+This command uses the `diff.color.*` and `pager.range-diff` settings
+(the latter is on by default).
+See linkgit:git-config[1].
+
+
+EXAMPLES
+--------
+
+When a rebase required merge conflicts to be resolved, compare the changes
+introduced by the rebase directly afterwards using:
+
+------------
+$ git range-diff @{u} @{1} @
+------------
+
+
+A typical output of `git range-diff` would look like this:
+
+------------
+-:  ------- > 1:  0ddba11 Prepare for the inevitable!
+1:  c0debee = 2:  cab005e Add a helpful message at the start
+2:  f00dbal ! 3:  decafe1 Describe a bug
+    @@ -1,3 +1,3 @@
+     Author: A U Thor <author@example.com>
+
+    -TODO: Describe a bug
+    +Describe a bug
+    @@ -324,5 +324,6
+      This is expected.
+
+    -+What is unexpected is that it will also crash.
+    ++Unexpectedly, it also crashes. This is a bug, and the jury is
+    ++still out there how to fix it best. See ticket #314 for details.
+
+      Contact
+3:  bedead < -:  ------- TO-UNDO
+------------
+
+In this example, there are 3 old and 3 new commits, where the developer
+removed the 3rd, added a new one before the first two, and modified the
+commit message of the 2nd commit as well its diff.
+
+When the output goes to a terminal, it is color-coded by default, just
+like regular `git diff`'s output. In addition, the first line (adding a
+commit) is green, the last line (deleting a commit) is red, the second
+line (with a perfect match) is yellow like the commit header of `git
+show`'s output, and the third line colors the old commit red, the new
+one green and the rest like `git show`'s commit header.
+
+A naive color-coded diff of diffs is actually a bit hard to read,
+though, as it colors the entire lines red or green. The line that added
+"What is unexpected" in the old commit, for example, is completely red,
+even if the intent of the old commit was to add something.
+
+To help with that, `range` uses the `--dual-color` mode by default. In
+this mode, the diff of diffs will retain the original diff colors, and
+prefix the lines with -/+ markers that have their *background* red or
+green, to make it more obvious that they describe how the diff itself
+changed.
+
+
+Algorithm
+---------
+
+The general idea is this: we generate a cost matrix between the commits
+in both commit ranges, then solve the least-cost assignment.
+
+The cost matrix is populated thusly: for each pair of commits, both
+diffs are generated and the "diff of diffs" is generated, with 3 context
+lines, then the number of lines in that diff is used as cost.
+
+To avoid false positives (e.g. when a patch has been removed, and an
+unrelated patch has been added between two iterations of the same patch
+series), the cost matrix is extended to allow for that, by adding
+fixed-cost entries for wholesale deletes/adds.
+
+Example: Let commits `1--2` be the first iteration of a patch series and
+`A--C` the second iteration. Let's assume that `A` is a cherry-pick of
+`2,` and `C` is a cherry-pick of `1` but with a small modification (say,
+a fixed typo). Visualize the commits as a bipartite graph:
+
+------------
+    1            A
+
+    2            B
+
+                C
+------------
+
+We are looking for a "best" explanation of the new series in terms of
+the old one. We can represent an "explanation" as an edge in the graph:
+
+
+------------
+    1            A
+              /
+    2 --------'  B
+
+                C
+------------
+
+This explanation comes for "free" because there was no change. Similarly
+`C` could be explained using `1`, but that comes at some cost c>0
+because of the modification:
+
+------------
+    1 ----.      A
+         |    /
+    2 ----+---'  B
+         |
+         `----- C
+         c>0
+------------
+
+In mathematical terms, what we are looking for is some sort of a minimum
+cost bipartite matching; `1` is matched to `C` at some cost, etc. The
+underlying graph is in fact a complete bipartite graph; the cost we
+associate with every edge is the size of the diff between the two
+commits' patches. To explain also new commits, we introduce dummy nodes
+on both sides:
+
+------------
+    1 ----.      A
+         |    /
+    2 ----+---'  B
+         |
+    o     `----- C
+         c>0
+    o            o
+
+    o            o
+------------
+
+The cost of an edge `o--C` is the size of `C`'s diff, modified by a
+fudge factor that should be smaller than 100%. The cost of an edge
+`o--o` is free. The fudge factor is necessary because even if `1` and
+`C` have nothing in common, they may still share a few empty lines and
+such, possibly making the assignment `1--C`, `o--o` slightly cheaper
+than `1--o`, `o--C` even if `1` and `C` have nothing in common. With the
+fudge factor we require a much larger common part to consider patches as
+corresponding.
+
+The overall time needed to compute this algorithm is the time needed to
+compute n+m commit diffs and then n*m diffs of patches, plus the time
+needed to compute the least-cost assigment between n and m diffs. Git
+uses an implementation of the Jonker-Volgenant algorithm to solve the
+assignment problem, which has cubic runtime complexity. The matching
+found in this case will look like this:
+
+------------
+    1 ----.      A
+         |    /
+    2 ----+---'  B
+       .--+-----'
+    o -'  `----- C
+         c>0
+    o ---------- o
+
+    o ---------- o
+------------
+
+
+SEE ALSO
+--------
+linkgit:git-log[1]
+
+GIT
+---
+Part of the linkgit:git[1] suite
index 73aa943e287fd095f1acd6cbcc48a3303f4e4444..d03df31c2a61b29caa60928eb3b3f131562152d3 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -876,6 +876,7 @@ LIB_OBJS += gpg-interface.o
 LIB_OBJS += graph.o
 LIB_OBJS += grep.o
 LIB_OBJS += hashmap.o
+LIB_OBJS += linear-assignment.o
 LIB_OBJS += help.o
 LIB_OBJS += hex.o
 LIB_OBJS += ident.o
@@ -931,6 +932,7 @@ LIB_OBJS += progress.o
 LIB_OBJS += prompt.o
 LIB_OBJS += protocol.o
 LIB_OBJS += quote.o
+LIB_OBJS += range-diff.o
 LIB_OBJS += reachable.o
 LIB_OBJS += read-cache.o
 LIB_OBJS += reflog-walk.o
@@ -1069,6 +1071,7 @@ BUILTIN_OBJS += builtin/prune-packed.o
 BUILTIN_OBJS += builtin/prune.o
 BUILTIN_OBJS += builtin/pull.o
 BUILTIN_OBJS += builtin/push.o
+BUILTIN_OBJS += builtin/range-diff.o
 BUILTIN_OBJS += builtin/read-tree.o
 BUILTIN_OBJS += builtin/rebase--helper.o
 BUILTIN_OBJS += builtin/receive-pack.o
index 0362f1ce25fb64631e28f00eafa46f35bc94c7eb..99206df4bd43fc0c4ff8db538912dec1c600c397 100644 (file)
--- a/builtin.h
+++ b/builtin.h
@@ -201,6 +201,7 @@ extern int cmd_prune(int argc, const char **argv, const char *prefix);
 extern int cmd_prune_packed(int argc, const char **argv, const char *prefix);
 extern int cmd_pull(int argc, const char **argv, const char *prefix);
 extern int cmd_push(int argc, const char **argv, const char *prefix);
+extern int cmd_range_diff(int argc, const char **argv, const char *prefix);
 extern int cmd_read_tree(int argc, const char **argv, const char *prefix);
 extern int cmd_rebase__helper(int argc, const char **argv, const char *prefix);
 extern int cmd_receive_pack(int argc, const char **argv, const char *prefix);
diff --git a/builtin/range-diff.c b/builtin/range-diff.c
new file mode 100644 (file)
index 0000000..f52d45d
--- /dev/null
@@ -0,0 +1,116 @@
+#include "cache.h"
+#include "builtin.h"
+#include "parse-options.h"
+#include "range-diff.h"
+#include "config.h"
+
+static const char * const builtin_range_diff_usage[] = {
+N_("git range-diff [<options>] <old-base>..<old-tip> <new-base>..<new-tip>"),
+N_("git range-diff [<options>] <old-tip>...<new-tip>"),
+N_("git range-diff [<options>] <base> <old-tip> <new-tip>"),
+NULL
+};
+
+static struct strbuf *output_prefix_cb(struct diff_options *opt, void *data)
+{
+       return data;
+}
+
+int cmd_range_diff(int argc, const char **argv, const char *prefix)
+{
+       int creation_factor = 60;
+       struct diff_options diffopt = { NULL };
+       int simple_color = -1;
+       struct option options[] = {
+               OPT_INTEGER(0, "creation-factor", &creation_factor,
+                           N_("Percentage by which creation is weighted")),
+               OPT_BOOL(0, "no-dual-color", &simple_color,
+                           N_("color both diff and diff-between-diffs")),
+               OPT_END()
+       };
+       int i, j, res = 0;
+       struct strbuf four_spaces = STRBUF_INIT;
+       struct strbuf range1 = STRBUF_INIT, range2 = STRBUF_INIT;
+
+       git_config(git_diff_ui_config, NULL);
+
+       diff_setup(&diffopt);
+       diffopt.output_format = DIFF_FORMAT_PATCH;
+       diffopt.flags.suppress_diff_headers = 1;
+       diffopt.output_prefix = output_prefix_cb;
+       strbuf_addstr(&four_spaces, "    ");
+       diffopt.output_prefix_data = &four_spaces;
+
+       argc = parse_options(argc, argv, NULL, options,
+                            builtin_range_diff_usage, PARSE_OPT_KEEP_UNKNOWN |
+                            PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0);
+
+       for (i = j = 1; i < argc && strcmp("--", argv[i]); ) {
+               int c = diff_opt_parse(&diffopt, argv + i, argc - i, prefix);
+
+               if (!c)
+                       argv[j++] = argv[i++];
+               else
+                       i += c;
+       }
+       while (i < argc)
+               argv[j++] = argv[i++];
+       argc = j;
+       diff_setup_done(&diffopt);
+
+       /* Make sure that there are no unparsed options */
+       argc = parse_options(argc, argv, NULL,
+                            options + ARRAY_SIZE(options) - 1, /* OPT_END */
+                            builtin_range_diff_usage, 0);
+
+       if (simple_color < 1) {
+               if (!simple_color)
+                       /* force color when --dual-color was used */
+                       diffopt.use_color = 1;
+               diffopt.flags.dual_color_diffed_diffs = 1;
+       }
+
+       if (argc == 2) {
+               if (!strstr(argv[0], ".."))
+                       die(_("no .. in range: '%s'"), argv[0]);
+               strbuf_addstr(&range1, argv[0]);
+
+               if (!strstr(argv[1], ".."))
+                       die(_("no .. in range: '%s'"), argv[1]);
+               strbuf_addstr(&range2, argv[1]);
+       } else if (argc == 3) {
+               strbuf_addf(&range1, "%s..%s", argv[0], argv[1]);
+               strbuf_addf(&range2, "%s..%s", argv[0], argv[2]);
+       } else if (argc == 1) {
+               const char *b = strstr(argv[0], "..."), *a = argv[0];
+               int a_len;
+
+               if (!b) {
+                       error(_("single arg format must be symmetric range"));
+                       usage_with_options(builtin_range_diff_usage, options);
+               }
+
+               a_len = (int)(b - a);
+               if (!a_len) {
+                       a = "HEAD";
+                       a_len = strlen(a);
+               }
+               b += 3;
+               if (!*b)
+                       b = "HEAD";
+               strbuf_addf(&range1, "%s..%.*s", b, a_len, a);
+               strbuf_addf(&range2, "%.*s..%s", a_len, a, b);
+       } else {
+               error(_("need two commit ranges"));
+               usage_with_options(builtin_range_diff_usage, options);
+       }
+
+       res = show_range_diff(range1.buf, range2.buf, creation_factor,
+                             &diffopt);
+
+       strbuf_release(&range1);
+       strbuf_release(&range2);
+       strbuf_release(&four_spaces);
+
+       return res;
+}
diff --git a/color.h b/color.h
index 5b744e1bc68617d196bdd864042e738cb4d75ebe..98894d6a17563d7005a2ba3a1fb6070c06cbcefc 100644 (file)
--- a/color.h
+++ b/color.h
@@ -36,6 +36,12 @@ struct strbuf;
 #define GIT_COLOR_BOLD_BLUE    "\033[1;34m"
 #define GIT_COLOR_BOLD_MAGENTA "\033[1;35m"
 #define GIT_COLOR_BOLD_CYAN    "\033[1;36m"
+#define GIT_COLOR_FAINT_RED    "\033[2;31m"
+#define GIT_COLOR_FAINT_GREEN  "\033[2;32m"
+#define GIT_COLOR_FAINT_YELLOW "\033[2;33m"
+#define GIT_COLOR_FAINT_BLUE   "\033[2;34m"
+#define GIT_COLOR_FAINT_MAGENTA        "\033[2;35m"
+#define GIT_COLOR_FAINT_CYAN   "\033[2;36m"
 #define GIT_COLOR_BG_RED       "\033[41m"
 #define GIT_COLOR_BG_GREEN     "\033[42m"
 #define GIT_COLOR_BG_YELLOW    "\033[43m"
@@ -44,6 +50,7 @@ struct strbuf;
 #define GIT_COLOR_BG_CYAN      "\033[46m"
 #define GIT_COLOR_FAINT                "\033[2m"
 #define GIT_COLOR_FAINT_ITALIC "\033[2;3m"
+#define GIT_COLOR_REVERSE      "\033[7m"
 
 /* A special value meaning "no color selected" */
 #define GIT_COLOR_NIL "NIL"
index e1c26c1bb7e618f6f372d9a568e7cab75612d2db..a9dda3b8af6a754564f8f840f0ca63d93f6c88dc 100644 (file)
@@ -139,6 +139,7 @@ git-prune-packed                        plumbingmanipulators
 git-pull                                mainporcelain           remote
 git-push                                mainporcelain           remote
 git-quiltimport                         foreignscminterface
+git-range-diff                          mainporcelain
 git-read-tree                           plumbingmanipulators
 git-rebase                              mainporcelain           history
 git-receive-pack                        synchelpers
index 94c95516ebec7f32787cc5a2fb45cae83c7e28e3..d63d2dffd4e12186e734adb0cc2ae7c1db7a6374 100644 (file)
@@ -1976,6 +1976,20 @@ _git_push ()
        __git_complete_remote_or_refspec
 }
 
+_git_range_diff ()
+{
+       case "$cur" in
+       --*)
+               __gitcomp "
+                       --creation-factor= --no-dual-color
+                       $__git_diff_common_options
+               "
+               return
+               ;;
+       esac
+       __git_complete_revlist
+}
+
 _git_rebase ()
 {
        __git_find_repo_path
diff --git a/diff.c b/diff.c
index 77e4684f36c9d092e1bde4900e293277096b99f7..145cfbae5929c69224f9f9e5bc473f2a221603de 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -70,6 +70,12 @@ static char diff_colors[][COLOR_MAXLEN] = {
        GIT_COLOR_BOLD_YELLOW,  /* NEW_MOVED ALTERNATIVE */
        GIT_COLOR_FAINT,        /* NEW_MOVED_DIM */
        GIT_COLOR_FAINT_ITALIC, /* NEW_MOVED_ALTERNATIVE_DIM */
+       GIT_COLOR_FAINT,        /* CONTEXT_DIM */
+       GIT_COLOR_FAINT_RED,    /* OLD_DIM */
+       GIT_COLOR_FAINT_GREEN,  /* NEW_DIM */
+       GIT_COLOR_BOLD,         /* CONTEXT_BOLD */
+       GIT_COLOR_BOLD_RED,     /* OLD_BOLD */
+       GIT_COLOR_BOLD_GREEN,   /* NEW_BOLD */
 };
 
 static const char *color_diff_slots[] = {
@@ -89,6 +95,12 @@ static const char *color_diff_slots[] = {
        [DIFF_FILE_NEW_MOVED_ALT]     = "newMovedAlternative",
        [DIFF_FILE_NEW_MOVED_DIM]     = "newMovedDimmed",
        [DIFF_FILE_NEW_MOVED_ALT_DIM] = "newMovedAlternativeDimmed",
+       [DIFF_CONTEXT_DIM]            = "contextDimmed",
+       [DIFF_FILE_OLD_DIM]           = "oldDimmed",
+       [DIFF_FILE_NEW_DIM]           = "newDimmed",
+       [DIFF_CONTEXT_BOLD]           = "contextBold",
+       [DIFF_FILE_OLD_BOLD]          = "oldBold",
+       [DIFF_FILE_NEW_BOLD]          = "newBold",
 };
 
 static NORETURN void die_want_option(const char *option_name)
@@ -611,14 +623,18 @@ static void check_blank_at_eof(mmfile_t *mf1, mmfile_t *mf2,
        ecbdata->blank_at_eof_in_postimage = (at - l2) + 1;
 }
 
-static void emit_line_0(struct diff_options *o, const char *set, const char *reset,
+static void emit_line_0(struct diff_options *o,
+                       const char *set, unsigned reverse, const char *reset,
                        int first, const char *line, int len)
 {
        int has_trailing_newline, has_trailing_carriage_return;
        int nofirst;
        FILE *file = o->file;
 
-       fputs(diff_line_prefix(o), file);
+       if (first)
+               fputs(diff_line_prefix(o), file);
+       else if (!len)
+               return;
 
        if (len == 0) {
                has_trailing_newline = (first == '\n');
@@ -636,8 +652,10 @@ static void emit_line_0(struct diff_options *o, const char *set, const char *res
        }
 
        if (len || !nofirst) {
+               if (reverse && want_color(o->use_color))
+                       fputs(GIT_COLOR_REVERSE, file);
                fputs(set, file);
-               if (!nofirst)
+               if (first && !nofirst)
                        fputc(first, file);
                fwrite(line, len, 1, file);
                fputs(reset, file);
@@ -651,7 +669,7 @@ static void emit_line_0(struct diff_options *o, const char *set, const char *res
 static void emit_line(struct diff_options *o, const char *set, const char *reset,
                      const char *line, int len)
 {
-       emit_line_0(o, set, reset, line[0], line+1, len-1);
+       emit_line_0(o, set, 0, reset, line[0], line+1, len-1);
 }
 
 enum diff_symbol {
@@ -1170,7 +1188,8 @@ static void dim_moved_lines(struct diff_options *o)
 
 static void emit_line_ws_markup(struct diff_options *o,
                                const char *set, const char *reset,
-                               const char *line, int len, char sign,
+                               const char *line, int len,
+                               const char *set_sign, char sign,
                                unsigned ws_rule, int blank_at_eof)
 {
        const char *ws = NULL;
@@ -1181,14 +1200,20 @@ static void emit_line_ws_markup(struct diff_options *o,
                        ws = NULL;
        }
 
-       if (!ws)
-               emit_line_0(o, set, reset, sign, line, len);
-       else if (blank_at_eof)
+       if (!ws && !set_sign)
+               emit_line_0(o, set, 0, reset, sign, line, len);
+       else if (!ws) {
+               /* Emit just the prefix, then the rest. */
+               emit_line_0(o, set_sign ? set_sign : set, !!set_sign, reset,
+                           sign, "", 0);
+               emit_line_0(o, set, 0, reset, 0, line, len);
+       } else if (blank_at_eof)
                /* Blank line at EOF - paint '+' as well */
-               emit_line_0(o, ws, reset, sign, line, len);
+               emit_line_0(o, ws, 0, reset, sign, line, len);
        else {
                /* Emit just the prefix, then the rest. */
-               emit_line_0(o, set, reset, sign, "", 0);
+               emit_line_0(o, set_sign ? set_sign : set, !!set_sign, reset,
+                           sign, "", 0);
                ws_check_emit(line, len, ws_rule,
                              o->file, set, reset, ws);
        }
@@ -1198,7 +1223,7 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
                                         struct emitted_diff_symbol *eds)
 {
        static const char *nneof = " No newline at end of file\n";
-       const char *context, *reset, *set, *meta, *fraginfo;
+       const char *context, *reset, *set, *set_sign, *meta, *fraginfo;
        struct strbuf sb = STRBUF_INIT;
 
        enum diff_symbol s = eds->s;
@@ -1211,7 +1236,7 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
                context = diff_get_color_opt(o, DIFF_CONTEXT);
                reset = diff_get_color_opt(o, DIFF_RESET);
                putc('\n', o->file);
-               emit_line_0(o, context, reset, '\\',
+               emit_line_0(o, context, 0, reset, '\\',
                            nneof, strlen(nneof));
                break;
        case DIFF_SYMBOL_SUBMODULE_HEADER:
@@ -1238,7 +1263,18 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
        case DIFF_SYMBOL_CONTEXT:
                set = diff_get_color_opt(o, DIFF_CONTEXT);
                reset = diff_get_color_opt(o, DIFF_RESET);
-               emit_line_ws_markup(o, set, reset, line, len, ' ',
+               set_sign = NULL;
+               if (o->flags.dual_color_diffed_diffs) {
+                       char c = !len ? 0 : line[0];
+
+                       if (c == '+')
+                               set = diff_get_color_opt(o, DIFF_FILE_NEW);
+                       else if (c == '@')
+                               set = diff_get_color_opt(o, DIFF_FRAGINFO);
+                       else if (c == '-')
+                               set = diff_get_color_opt(o, DIFF_FILE_OLD);
+               }
+               emit_line_ws_markup(o, set, reset, line, len, set_sign, ' ',
                                    flags & (DIFF_SYMBOL_CONTENT_WS_MASK), 0);
                break;
        case DIFF_SYMBOL_PLUS:
@@ -1265,7 +1301,23 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
                        set = diff_get_color_opt(o, DIFF_FILE_NEW);
                }
                reset = diff_get_color_opt(o, DIFF_RESET);
-               emit_line_ws_markup(o, set, reset, line, len, '+',
+               if (!o->flags.dual_color_diffed_diffs)
+                       set_sign = NULL;
+               else {
+                       char c = !len ? 0 : line[0];
+
+                       set_sign = set;
+                       if (c == '-')
+                               set = diff_get_color_opt(o, DIFF_FILE_OLD_BOLD);
+                       else if (c == '@')
+                               set = diff_get_color_opt(o, DIFF_FRAGINFO);
+                       else if (c == '+')
+                               set = diff_get_color_opt(o, DIFF_FILE_NEW_BOLD);
+                       else
+                               set = diff_get_color_opt(o, DIFF_CONTEXT_BOLD);
+                       flags &= ~DIFF_SYMBOL_CONTENT_WS_MASK;
+               }
+               emit_line_ws_markup(o, set, reset, line, len, set_sign, '+',
                                    flags & DIFF_SYMBOL_CONTENT_WS_MASK,
                                    flags & DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF);
                break;
@@ -1293,7 +1345,22 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
                        set = diff_get_color_opt(o, DIFF_FILE_OLD);
                }
                reset = diff_get_color_opt(o, DIFF_RESET);
-               emit_line_ws_markup(o, set, reset, line, len, '-',
+               if (!o->flags.dual_color_diffed_diffs)
+                       set_sign = NULL;
+               else {
+                       char c = !len ? 0 : line[0];
+
+                       set_sign = set;
+                       if (c == '+')
+                               set = diff_get_color_opt(o, DIFF_FILE_NEW_DIM);
+                       else if (c == '@')
+                               set = diff_get_color_opt(o, DIFF_FRAGINFO);
+                       else if (c == '-')
+                               set = diff_get_color_opt(o, DIFF_FILE_OLD_DIM);
+                       else
+                               set = diff_get_color_opt(o, DIFF_CONTEXT_DIM);
+               }
+               emit_line_ws_markup(o, set, reset, line, len, set_sign, '-',
                                    flags & DIFF_SYMBOL_CONTENT_WS_MASK, 0);
                break;
        case DIFF_SYMBOL_WORDS_PORCELAIN:
@@ -1484,6 +1551,7 @@ static void emit_hunk_header(struct emit_callback *ecbdata,
        const char *frag = diff_get_color(ecbdata->color_diff, DIFF_FRAGINFO);
        const char *func = diff_get_color(ecbdata->color_diff, DIFF_FUNCINFO);
        const char *reset = diff_get_color(ecbdata->color_diff, DIFF_RESET);
+       const char *reverse = ecbdata->color_diff ? GIT_COLOR_REVERSE : "";
        static const char atat[2] = { '@', '@' };
        const char *cp, *ep;
        struct strbuf msgbuf = STRBUF_INIT;
@@ -1504,6 +1572,8 @@ static void emit_hunk_header(struct emit_callback *ecbdata,
        ep += 2; /* skip over @@ */
 
        /* The hunk header in fraginfo color */
+       if (ecbdata->opt->flags.dual_color_diffed_diffs)
+               strbuf_addstr(&msgbuf, reverse);
        strbuf_addstr(&msgbuf, frag);
        strbuf_add(&msgbuf, line, ep - line);
        strbuf_addstr(&msgbuf, reset);
@@ -3397,13 +3467,16 @@ static void builtin_diff(const char *name_a,
                memset(&xpp, 0, sizeof(xpp));
                memset(&xecfg, 0, sizeof(xecfg));
                memset(&ecbdata, 0, sizeof(ecbdata));
+               if (o->flags.suppress_diff_headers)
+                       lbl[0] = NULL;
                ecbdata.label_path = lbl;
                ecbdata.color_diff = want_color(o->use_color);
                ecbdata.ws_rule = whitespace_rule(name_b);
                if (ecbdata.ws_rule & WS_BLANK_AT_EOF)
                        check_blank_at_eof(&mf1, &mf2, &ecbdata);
                ecbdata.opt = o;
-               ecbdata.header = header.len ? &header : NULL;
+               if (header.len && !o->flags.suppress_diff_headers)
+                       ecbdata.header = &header;
                xpp.flags = o->xdl_opts;
                xpp.anchors = o->anchors;
                xpp.anchors_nr = o->anchors_nr;
diff --git a/diff.h b/diff.h
index 3ada6ad33c0a03cccc5d0d65c848937945895cd6..89544e64bc5797fe40f069e84657d85589f0252a 100644 (file)
--- a/diff.h
+++ b/diff.h
@@ -94,6 +94,8 @@ struct diff_flags {
        unsigned funccontext:1;
        unsigned default_follow_renames:1;
        unsigned stat_with_summary:1;
+       unsigned suppress_diff_headers:1;
+       unsigned dual_color_diffed_diffs:1;
 };
 
 static inline void diff_flags_or(struct diff_flags *a,
@@ -246,7 +248,13 @@ enum color_diff {
        DIFF_FILE_NEW_MOVED = 13,
        DIFF_FILE_NEW_MOVED_ALT = 14,
        DIFF_FILE_NEW_MOVED_DIM = 15,
-       DIFF_FILE_NEW_MOVED_ALT_DIM = 16
+       DIFF_FILE_NEW_MOVED_ALT_DIM = 16,
+       DIFF_CONTEXT_DIM = 17,
+       DIFF_FILE_OLD_DIM = 18,
+       DIFF_FILE_NEW_DIM = 19,
+       DIFF_CONTEXT_BOLD = 20,
+       DIFF_FILE_OLD_BOLD = 21,
+       DIFF_FILE_NEW_BOLD = 22,
 };
 const char *diff_get_color(int diff_use_color, enum color_diff ix);
 #define diff_get_color_opt(o, ix) \
diff --git a/git.c b/git.c
index cc0fad93e0ac38459b981b0700260e5832e30cc9..c27c38738b2a9d9d61460b150d5ab4d36bb9cf5b 100644 (file)
--- a/git.c
+++ b/git.c
@@ -520,6 +520,7 @@ static struct cmd_struct commands[] = {
        { "prune-packed", cmd_prune_packed, RUN_SETUP },
        { "pull", cmd_pull, RUN_SETUP | NEED_WORK_TREE },
        { "push", cmd_push, RUN_SETUP },
+       { "range-diff", cmd_range_diff, RUN_SETUP | USE_PAGER },
        { "read-tree", cmd_read_tree, RUN_SETUP | SUPPORT_SUPER_PREFIX},
        { "rebase--helper", cmd_rebase__helper, RUN_SETUP | NEED_WORK_TREE },
        { "receive-pack", cmd_receive_pack },
diff --git a/linear-assignment.c b/linear-assignment.c
new file mode 100644 (file)
index 0000000..9b3e56e
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * Based on: Jonker, R., & Volgenant, A. (1987). <i>A shortest augmenting path
+ * algorithm for dense and sparse linear assignment problems</i>. Computing,
+ * 38(4), 325-340.
+ */
+#include "cache.h"
+#include "linear-assignment.h"
+
+#define COST(column, row) cost[(column) + column_count * (row)]
+
+/*
+ * The parameter `cost` is the cost matrix: the cost to assign column j to row
+ * i is `cost[j + column_count * i].
+ */
+void compute_assignment(int column_count, int row_count, int *cost,
+                       int *column2row, int *row2column)
+{
+       int *v, *d;
+       int *free_row, free_count = 0, saved_free_count, *pred, *col;
+       int i, j, phase;
+
+       memset(column2row, -1, sizeof(int) * column_count);
+       memset(row2column, -1, sizeof(int) * row_count);
+       ALLOC_ARRAY(v, column_count);
+
+       /* column reduction */
+       for (j = column_count - 1; j >= 0; j--) {
+               int i1 = 0;
+
+               for (i = 1; i < row_count; i++)
+                       if (COST(j, i1) > COST(j, i))
+                               i1 = i;
+               v[j] = COST(j, i1);
+               if (row2column[i1] == -1) {
+                       /* row i1 unassigned */
+                       row2column[i1] = j;
+                       column2row[j] = i1;
+               } else {
+                       if (row2column[i1] >= 0)
+                               row2column[i1] = -2 - row2column[i1];
+                       column2row[j] = -1;
+               }
+       }
+
+       /* reduction transfer */
+       ALLOC_ARRAY(free_row, row_count);
+       for (i = 0; i < row_count; i++) {
+               int j1 = row2column[i];
+               if (j1 == -1)
+                       free_row[free_count++] = i;
+               else if (j1 < -1)
+                       row2column[i] = -2 - j1;
+               else {
+                       int min = COST(!j1, i) - v[!j1];
+                       for (j = 1; j < column_count; j++)
+                               if (j != j1 && min > COST(j, i) - v[j])
+                                       min = COST(j, i) - v[j];
+                       v[j1] -= min;
+               }
+       }
+
+       if (free_count ==
+           (column_count < row_count ? row_count - column_count : 0)) {
+               free(v);
+               free(free_row);
+               return;
+       }
+
+       /* augmenting row reduction */
+       for (phase = 0; phase < 2; phase++) {
+               int k = 0;
+
+               saved_free_count = free_count;
+               free_count = 0;
+               while (k < saved_free_count) {
+                       int u1, u2;
+                       int j1 = 0, j2, i0;
+
+                       i = free_row[k++];
+                       u1 = COST(j1, i) - v[j1];
+                       j2 = -1;
+                       u2 = INT_MAX;
+                       for (j = 1; j < column_count; j++) {
+                               int c = COST(j, i) - v[j];
+                               if (u2 > c) {
+                                       if (u1 < c) {
+                                               u2 = c;
+                                               j2 = j;
+                                       } else {
+                                               u2 = u1;
+                                               u1 = c;
+                                               j2 = j1;
+                                               j1 = j;
+                                       }
+                               }
+                       }
+                       if (j2 < 0) {
+                               j2 = j1;
+                               u2 = u1;
+                       }
+
+                       i0 = column2row[j1];
+                       if (u1 < u2)
+                               v[j1] -= u2 - u1;
+                       else if (i0 >= 0) {
+                               j1 = j2;
+                               i0 = column2row[j1];
+                       }
+
+                       if (i0 >= 0) {
+                               if (u1 < u2)
+                                       free_row[--k] = i0;
+                               else
+                                       free_row[free_count++] = i0;
+                       }
+                       row2column[i] = j1;
+                       column2row[j1] = i;
+               }
+       }
+
+       /* augmentation */
+       saved_free_count = free_count;
+       ALLOC_ARRAY(d, column_count);
+       ALLOC_ARRAY(pred, column_count);
+       ALLOC_ARRAY(col, column_count);
+       for (free_count = 0; free_count < saved_free_count; free_count++) {
+               int i1 = free_row[free_count], low = 0, up = 0, last, k;
+               int min, c, u1;
+
+               for (j = 0; j < column_count; j++) {
+                       d[j] = COST(j, i1) - v[j];
+                       pred[j] = i1;
+                       col[j] = j;
+               }
+
+               j = -1;
+               do {
+                       last = low;
+                       min = d[col[up++]];
+                       for (k = up; k < column_count; k++) {
+                               j = col[k];
+                               c = d[j];
+                               if (c <= min) {
+                                       if (c < min) {
+                                               up = low;
+                                               min = c;
+                                       }
+                                       col[k] = col[up];
+                                       col[up++] = j;
+                               }
+                       }
+                       for (k = low; k < up; k++)
+                               if (column2row[col[k]] == -1)
+                                       goto update;
+
+                       /* scan a row */
+                       do {
+                               int j1 = col[low++];
+
+                               i = column2row[j1];
+                               u1 = COST(j1, i) - v[j1] - min;
+                               for (k = up; k < column_count; k++) {
+                                       j = col[k];
+                                       c = COST(j, i) - v[j] - u1;
+                                       if (c < d[j]) {
+                                               d[j] = c;
+                                               pred[j] = i;
+                                               if (c == min) {
+                                                       if (column2row[j] == -1)
+                                                               goto update;
+                                                       col[k] = col[up];
+                                                       col[up++] = j;
+                                               }
+                                       }
+                               }
+                       } while (low != up);
+               } while (low == up);
+
+update:
+               /* updating of the column pieces */
+               for (k = 0; k < last; k++) {
+                       int j1 = col[k];
+                       v[j1] += d[j1] - min;
+               }
+
+               /* augmentation */
+               do {
+                       if (j < 0)
+                               BUG("negative j: %d", j);
+                       i = pred[j];
+                       column2row[j] = i;
+                       SWAP(j, row2column[i]);
+               } while (i1 != i);
+       }
+
+       free(col);
+       free(pred);
+       free(d);
+       free(v);
+       free(free_row);
+}
diff --git a/linear-assignment.h b/linear-assignment.h
new file mode 100644 (file)
index 0000000..1dfea76
--- /dev/null
@@ -0,0 +1,22 @@
+#ifndef LINEAR_ASSIGNMENT_H
+#define LINEAR_ASSIGNMENT_H
+
+/*
+ * Compute an assignment of columns -> rows (and vice versa) such that every
+ * column is assigned to at most one row (and vice versa) minimizing the
+ * overall cost.
+ *
+ * The parameter `cost` is the cost matrix: the cost to assign column j to row
+ * i is `cost[j + column_count * i].
+ *
+ * The arrays column2row and row2column will be populated with the respective
+ * assignments (-1 for unassigned, which can happen only if column_count !=
+ * row_count).
+ */
+void compute_assignment(int column_count, int row_count, int *cost,
+                       int *column2row, int *row2column);
+
+/* The maximal cost in the cost matrix (to prevent integer overflows). */
+#define COST_MAX (1<<16)
+
+#endif
diff --git a/range-diff.c b/range-diff.c
new file mode 100644 (file)
index 0000000..b6b9aba
--- /dev/null
@@ -0,0 +1,435 @@
+#include "cache.h"
+#include "range-diff.h"
+#include "string-list.h"
+#include "run-command.h"
+#include "argv-array.h"
+#include "hashmap.h"
+#include "xdiff-interface.h"
+#include "linear-assignment.h"
+#include "diffcore.h"
+#include "commit.h"
+#include "pretty.h"
+#include "userdiff.h"
+
+struct patch_util {
+       /* For the search for an exact match */
+       struct hashmap_entry e;
+       const char *diff, *patch;
+
+       int i, shown;
+       int diffsize;
+       size_t diff_offset;
+       /* the index of the matching item in the other branch, or -1 */
+       int matching;
+       struct object_id oid;
+};
+
+/*
+ * Reads the patches into a string list, with the `util` field being populated
+ * as struct object_id (will need to be free()d).
+ */
+static int read_patches(const char *range, struct string_list *list)
+{
+       struct child_process cp = CHILD_PROCESS_INIT;
+       FILE *in;
+       struct strbuf buf = STRBUF_INIT, line = STRBUF_INIT;
+       struct patch_util *util = NULL;
+       int in_header = 1;
+
+       argv_array_pushl(&cp.args, "log", "--no-color", "-p", "--no-merges",
+                       "--reverse", "--date-order", "--decorate=no",
+                       "--no-abbrev-commit", range,
+                       NULL);
+       cp.out = -1;
+       cp.no_stdin = 1;
+       cp.git_cmd = 1;
+
+       if (start_command(&cp))
+               return error_errno(_("could not start `log`"));
+       in = fdopen(cp.out, "r");
+       if (!in) {
+               error_errno(_("could not read `log` output"));
+               finish_command(&cp);
+               return -1;
+       }
+
+       while (strbuf_getline(&line, in) != EOF) {
+               const char *p;
+
+               if (skip_prefix(line.buf, "commit ", &p)) {
+                       if (util) {
+                               string_list_append(list, buf.buf)->util = util;
+                               strbuf_reset(&buf);
+                       }
+                       util = xcalloc(sizeof(*util), 1);
+                       if (get_oid(p, &util->oid)) {
+                               error(_("could not parse commit '%s'"), p);
+                               free(util);
+                               string_list_clear(list, 1);
+                               strbuf_release(&buf);
+                               strbuf_release(&line);
+                               fclose(in);
+                               finish_command(&cp);
+                               return -1;
+                       }
+                       util->matching = -1;
+                       in_header = 1;
+                       continue;
+               }
+
+               if (starts_with(line.buf, "diff --git")) {
+                       in_header = 0;
+                       strbuf_addch(&buf, '\n');
+                       if (!util->diff_offset)
+                               util->diff_offset = buf.len;
+                       strbuf_addbuf(&buf, &line);
+               } else if (in_header) {
+                       if (starts_with(line.buf, "Author: ")) {
+                               strbuf_addbuf(&buf, &line);
+                               strbuf_addstr(&buf, "\n\n");
+                       } else if (starts_with(line.buf, "    ")) {
+                               strbuf_rtrim(&line);
+                               strbuf_addbuf(&buf, &line);
+                               strbuf_addch(&buf, '\n');
+                       }
+                       continue;
+               } else if (starts_with(line.buf, "@@ "))
+                       strbuf_addstr(&buf, "@@");
+               else if (!line.buf[0] || starts_with(line.buf, "index "))
+                       /*
+                        * A completely blank (not ' \n', which is context)
+                        * line is not valid in a diff.  We skip it
+                        * silently, because this neatly handles the blank
+                        * separator line between commits in git-log
+                        * output.
+                        *
+                        * We also want to ignore the diff's `index` lines
+                        * because they contain exact blob hashes in which
+                        * we are not interested.
+                        */
+                       continue;
+               else
+                       strbuf_addbuf(&buf, &line);
+
+               strbuf_addch(&buf, '\n');
+               util->diffsize++;
+       }
+       fclose(in);
+       strbuf_release(&line);
+
+       if (util)
+               string_list_append(list, buf.buf)->util = util;
+       strbuf_release(&buf);
+
+       if (finish_command(&cp))
+               return -1;
+
+       return 0;
+}
+
+static int patch_util_cmp(const void *dummy, const struct patch_util *a,
+                    const struct patch_util *b, const char *keydata)
+{
+       return strcmp(a->diff, keydata ? keydata : b->diff);
+}
+
+static void find_exact_matches(struct string_list *a, struct string_list *b)
+{
+       struct hashmap map;
+       int i;
+
+       hashmap_init(&map, (hashmap_cmp_fn)patch_util_cmp, NULL, 0);
+
+       /* First, add the patches of a to a hash map */
+       for (i = 0; i < a->nr; i++) {
+               struct patch_util *util = a->items[i].util;
+
+               util->i = i;
+               util->patch = a->items[i].string;
+               util->diff = util->patch + util->diff_offset;
+               hashmap_entry_init(util, strhash(util->diff));
+               hashmap_add(&map, util);
+       }
+
+       /* Now try to find exact matches in b */
+       for (i = 0; i < b->nr; i++) {
+               struct patch_util *util = b->items[i].util, *other;
+
+               util->i = i;
+               util->patch = b->items[i].string;
+               util->diff = util->patch + util->diff_offset;
+               hashmap_entry_init(util, strhash(util->diff));
+               other = hashmap_remove(&map, util, NULL);
+               if (other) {
+                       if (other->matching >= 0)
+                               BUG("already assigned!");
+
+                       other->matching = i;
+                       util->matching = other->i;
+               }
+       }
+
+       hashmap_free(&map, 0);
+}
+
+static void diffsize_consume(void *data, char *line, unsigned long len)
+{
+       (*(int *)data)++;
+}
+
+static int diffsize(const char *a, const char *b)
+{
+       xpparam_t pp = { 0 };
+       xdemitconf_t cfg = { 0 };
+       mmfile_t mf1, mf2;
+       int count = 0;
+
+       mf1.ptr = (char *)a;
+       mf1.size = strlen(a);
+       mf2.ptr = (char *)b;
+       mf2.size = strlen(b);
+
+       cfg.ctxlen = 3;
+       if (!xdi_diff_outf(&mf1, &mf2, diffsize_consume, &count, &pp, &cfg))
+               return count;
+
+       error(_("failed to generate diff"));
+       return COST_MAX;
+}
+
+static void get_correspondences(struct string_list *a, struct string_list *b,
+                               int creation_factor)
+{
+       int n = a->nr + b->nr;
+       int *cost, c, *a2b, *b2a;
+       int i, j;
+
+       ALLOC_ARRAY(cost, st_mult(n, n));
+       ALLOC_ARRAY(a2b, n);
+       ALLOC_ARRAY(b2a, n);
+
+       for (i = 0; i < a->nr; i++) {
+               struct patch_util *a_util = a->items[i].util;
+
+               for (j = 0; j < b->nr; j++) {
+                       struct patch_util *b_util = b->items[j].util;
+
+                       if (a_util->matching == j)
+                               c = 0;
+                       else if (a_util->matching < 0 && b_util->matching < 0)
+                               c = diffsize(a_util->diff, b_util->diff);
+                       else
+                               c = COST_MAX;
+                       cost[i + n * j] = c;
+               }
+
+               c = a_util->matching < 0 ?
+                       a_util->diffsize * creation_factor / 100 : COST_MAX;
+               for (j = b->nr; j < n; j++)
+                       cost[i + n * j] = c;
+       }
+
+       for (j = 0; j < b->nr; j++) {
+               struct patch_util *util = b->items[j].util;
+
+               c = util->matching < 0 ?
+                       util->diffsize * creation_factor / 100 : COST_MAX;
+               for (i = a->nr; i < n; i++)
+                       cost[i + n * j] = c;
+       }
+
+       for (i = a->nr; i < n; i++)
+               for (j = b->nr; j < n; j++)
+                       cost[i + n * j] = 0;
+
+       compute_assignment(n, n, cost, a2b, b2a);
+
+       for (i = 0; i < a->nr; i++)
+               if (a2b[i] >= 0 && a2b[i] < b->nr) {
+                       struct patch_util *a_util = a->items[i].util;
+                       struct patch_util *b_util = b->items[a2b[i]].util;
+
+                       a_util->matching = a2b[i];
+                       b_util->matching = i;
+               }
+
+       free(cost);
+       free(a2b);
+       free(b2a);
+}
+
+static void output_pair_header(struct diff_options *diffopt,
+                              int patch_no_width,
+                              struct strbuf *buf,
+                              struct strbuf *dashes,
+                              struct patch_util *a_util,
+                              struct patch_util *b_util)
+{
+       struct object_id *oid = a_util ? &a_util->oid : &b_util->oid;
+       struct commit *commit;
+       char status;
+       const char *color_reset = diff_get_color_opt(diffopt, DIFF_RESET);
+       const char *color_old = diff_get_color_opt(diffopt, DIFF_FILE_OLD);
+       const char *color_new = diff_get_color_opt(diffopt, DIFF_FILE_NEW);
+       const char *color_commit = diff_get_color_opt(diffopt, DIFF_COMMIT);
+       const char *color;
+
+       if (!dashes->len)
+               strbuf_addchars(dashes, '-',
+                               strlen(find_unique_abbrev(oid,
+                                                         DEFAULT_ABBREV)));
+
+       if (!b_util) {
+               color = color_old;
+               status = '<';
+       } else if (!a_util) {
+               color = color_new;
+               status = '>';
+       } else if (strcmp(a_util->patch, b_util->patch)) {
+               color = color_commit;
+               status = '!';
+       } else {
+               color = color_commit;
+               status = '=';
+       }
+
+       strbuf_reset(buf);
+       strbuf_addstr(buf, status == '!' ? color_old : color);
+       if (!a_util)
+               strbuf_addf(buf, "%*s:  %s ", patch_no_width, "-", dashes->buf);
+       else
+               strbuf_addf(buf, "%*d:  %s ", patch_no_width, a_util->i + 1,
+                           find_unique_abbrev(&a_util->oid, DEFAULT_ABBREV));
+
+       if (status == '!')
+               strbuf_addf(buf, "%s%s", color_reset, color);
+       strbuf_addch(buf, status);
+       if (status == '!')
+               strbuf_addf(buf, "%s%s", color_reset, color_new);
+
+       if (!b_util)
+               strbuf_addf(buf, " %*s:  %s", patch_no_width, "-", dashes->buf);
+       else
+               strbuf_addf(buf, " %*d:  %s", patch_no_width, b_util->i + 1,
+                           find_unique_abbrev(&b_util->oid, DEFAULT_ABBREV));
+
+       commit = lookup_commit_reference(the_repository, oid);
+       if (commit) {
+               if (status == '!')
+                       strbuf_addf(buf, "%s%s", color_reset, color);
+
+               strbuf_addch(buf, ' ');
+               pp_commit_easy(CMIT_FMT_ONELINE, commit, buf);
+       }
+       strbuf_addf(buf, "%s\n", color_reset);
+
+       fwrite(buf->buf, buf->len, 1, stdout);
+}
+
+static struct userdiff_driver no_func_name = {
+       .funcname = { "$^", 0 }
+};
+
+static struct diff_filespec *get_filespec(const char *name, const char *p)
+{
+       struct diff_filespec *spec = alloc_filespec(name);
+
+       fill_filespec(spec, &null_oid, 0, 0644);
+       spec->data = (char *)p;
+       spec->size = strlen(p);
+       spec->should_munmap = 0;
+       spec->is_stdin = 1;
+       spec->driver = &no_func_name;
+
+       return spec;
+}
+
+static void patch_diff(const char *a, const char *b,
+                             struct diff_options *diffopt)
+{
+       diff_queue(&diff_queued_diff,
+                  get_filespec("a", a), get_filespec("b", b));
+
+       diffcore_std(diffopt);
+       diff_flush(diffopt);
+}
+
+static void output(struct string_list *a, struct string_list *b,
+                  struct diff_options *diffopt)
+{
+       struct strbuf buf = STRBUF_INIT, dashes = STRBUF_INIT;
+       int patch_no_width = decimal_width(1 + (a->nr > b->nr ? a->nr : b->nr));
+       int i = 0, j = 0;
+
+       /*
+        * We assume the user is really more interested in the second argument
+        * ("newer" version). To that end, we print the output in the order of
+        * the RHS (the `b` parameter). To put the LHS (the `a` parameter)
+        * commits that are no longer in the RHS into a good place, we place
+        * them once we have shown all of their predecessors in the LHS.
+        */
+
+       while (i < a->nr || j < b->nr) {
+               struct patch_util *a_util, *b_util;
+               a_util = i < a->nr ? a->items[i].util : NULL;
+               b_util = j < b->nr ? b->items[j].util : NULL;
+
+               /* Skip all the already-shown commits from the LHS. */
+               while (i < a->nr && a_util->shown)
+                       a_util = ++i < a->nr ? a->items[i].util : NULL;
+
+               /* Show unmatched LHS commit whose predecessors were shown. */
+               if (i < a->nr && a_util->matching < 0) {
+                       output_pair_header(diffopt, patch_no_width,
+                                          &buf, &dashes, a_util, NULL);
+                       i++;
+                       continue;
+               }
+
+               /* Show unmatched RHS commits. */
+               while (j < b->nr && b_util->matching < 0) {
+                       output_pair_header(diffopt, patch_no_width,
+                                          &buf, &dashes, NULL, b_util);
+                       b_util = ++j < b->nr ? b->items[j].util : NULL;
+               }
+
+               /* Show matching LHS/RHS pair. */
+               if (j < b->nr) {
+                       a_util = a->items[b_util->matching].util;
+                       output_pair_header(diffopt, patch_no_width,
+                                          &buf, &dashes, a_util, b_util);
+                       if (!(diffopt->output_format & DIFF_FORMAT_NO_OUTPUT))
+                               patch_diff(a->items[b_util->matching].string,
+                                          b->items[j].string, diffopt);
+                       a_util->shown = 1;
+                       j++;
+               }
+       }
+       strbuf_release(&buf);
+       strbuf_release(&dashes);
+}
+
+int show_range_diff(const char *range1, const char *range2,
+                   int creation_factor, struct diff_options *diffopt)
+{
+       int res = 0;
+
+       struct string_list branch1 = STRING_LIST_INIT_DUP;
+       struct string_list branch2 = STRING_LIST_INIT_DUP;
+
+       if (read_patches(range1, &branch1))
+               res = error(_("could not parse log for '%s'"), range1);
+       if (!res && read_patches(range2, &branch2))
+               res = error(_("could not parse log for '%s'"), range2);
+
+       if (!res) {
+               find_exact_matches(&branch1, &branch2);
+               get_correspondences(&branch1, &branch2, creation_factor);
+               output(&branch1, &branch2, diffopt);
+       }
+
+       string_list_clear(&branch1, 1);
+       string_list_clear(&branch2, 1);
+
+       return res;
+}
diff --git a/range-diff.h b/range-diff.h
new file mode 100644 (file)
index 0000000..2407d46
--- /dev/null
@@ -0,0 +1,9 @@
+#ifndef RANGE_DIFF_H
+#define RANGE_DIFF_H
+
+#include "diff.h"
+
+int show_range_diff(const char *range1, const char *range2,
+                   int creation_factor, struct diff_options *diffopt);
+
+#endif
index 3bd959ae523cff7fe3a29f1abb92019b6d10515f..b17bf71b84e12cdecac72e996242313c5e37fbd7 100644 (file)
@@ -1,6 +1,7 @@
 t[0-9][0-9][0-9][0-9]/* -whitespace
 /diff-lib/* eol=lf
 /t0110/url-* binary
+/t3206/* eol=lf
 /t3900/*.txt eol=lf
 /t3901/*.txt eol=lf
 /t4034/*/* eol=lf
diff --git a/t/t3206-range-diff.sh b/t/t3206-range-diff.sh
new file mode 100755 (executable)
index 0000000..2237c7f
--- /dev/null
@@ -0,0 +1,145 @@
+#!/bin/sh
+
+test_description='range-diff tests'
+
+. ./test-lib.sh
+
+# Note that because of the range-diff's heuristics, test_commit does more
+# harm than good.  We need some real history.
+
+test_expect_success 'setup' '
+       git fast-import < "$TEST_DIRECTORY"/t3206/history.export
+'
+
+test_expect_success 'simple A..B A..C (unmodified)' '
+       git range-diff --no-color master..topic master..unmodified \
+               >actual &&
+       cat >expected <<-EOF &&
+       1:  4de457d = 1:  35b9b25 s/5/A/
+       2:  fccce22 = 2:  de345ab s/4/A/
+       3:  147e64e = 3:  9af6654 s/11/B/
+       4:  a63e992 = 4:  2901f77 s/12/B/
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'simple B...C (unmodified)' '
+       git range-diff --no-color topic...unmodified >actual &&
+       # same "expected" as above
+       test_cmp expected actual
+'
+
+test_expect_success 'simple A B C (unmodified)' '
+       git range-diff --no-color master topic unmodified >actual &&
+       # same "expected" as above
+       test_cmp expected actual
+'
+
+test_expect_success 'trivial reordering' '
+       git range-diff --no-color master topic reordered >actual &&
+       cat >expected <<-EOF &&
+       1:  4de457d = 1:  aca177a s/5/A/
+       3:  147e64e = 2:  14ad629 s/11/B/
+       4:  a63e992 = 3:  ee58208 s/12/B/
+       2:  fccce22 = 4:  307b27a s/4/A/
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'removed a commit' '
+       git range-diff --no-color master topic removed >actual &&
+       cat >expected <<-EOF &&
+       1:  4de457d = 1:  7657159 s/5/A/
+       2:  fccce22 < -:  ------- s/4/A/
+       3:  147e64e = 2:  43d84d3 s/11/B/
+       4:  a63e992 = 3:  a740396 s/12/B/
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'added a commit' '
+       git range-diff --no-color master topic added >actual &&
+       cat >expected <<-EOF &&
+       1:  4de457d = 1:  2716022 s/5/A/
+       2:  fccce22 = 2:  b62accd s/4/A/
+       -:  ------- > 3:  df46cfa s/6/A/
+       3:  147e64e = 4:  3e64548 s/11/B/
+       4:  a63e992 = 5:  12b4063 s/12/B/
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'new base, A B C' '
+       git range-diff --no-color master topic rebased >actual &&
+       cat >expected <<-EOF &&
+       1:  4de457d = 1:  cc9c443 s/5/A/
+       2:  fccce22 = 2:  c5d9641 s/4/A/
+       3:  147e64e = 3:  28cc2b6 s/11/B/
+       4:  a63e992 = 4:  5628ab7 s/12/B/
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'new base, B...C' '
+       # this syntax includes the commits from master!
+       git range-diff --no-color topic...rebased >actual &&
+       cat >expected <<-EOF &&
+       -:  ------- > 1:  a31b12e unrelated
+       1:  4de457d = 2:  cc9c443 s/5/A/
+       2:  fccce22 = 3:  c5d9641 s/4/A/
+       3:  147e64e = 4:  28cc2b6 s/11/B/
+       4:  a63e992 = 5:  5628ab7 s/12/B/
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'changed commit' '
+       git range-diff --no-color topic...changed >actual &&
+       cat >expected <<-EOF &&
+       1:  4de457d = 1:  a4b3333 s/5/A/
+       2:  fccce22 = 2:  f51d370 s/4/A/
+       3:  147e64e ! 3:  0559556 s/11/B/
+           @@ -10,7 +10,7 @@
+             9
+             10
+            -11
+           -+B
+           ++BB
+             12
+             13
+             14
+       4:  a63e992 ! 4:  d966c5c s/12/B/
+           @@ -8,7 +8,7 @@
+            @@
+             9
+             10
+           - B
+           + BB
+            -12
+            +B
+             13
+       EOF
+       test_cmp expected actual
+'
+
+test_expect_success 'changed message' '
+       git range-diff --no-color topic...changed-message >actual &&
+       sed s/Z/\ /g >expected <<-EOF &&
+       1:  4de457d = 1:  f686024 s/5/A/
+       2:  fccce22 ! 2:  4ab067d s/4/A/
+           @@ -2,6 +2,8 @@
+           Z
+           Z    s/4/A/
+           Z
+           +    Also a silly comment here!
+           +
+           Zdiff --git a/file b/file
+           Z--- a/file
+           Z+++ b/file
+       3:  147e64e = 3:  b9cb956 s/11/B/
+       4:  a63e992 = 4:  8add5f1 s/12/B/
+       EOF
+       test_cmp expected actual
+'
+
+test_done
diff --git a/t/t3206/history.export b/t/t3206/history.export
new file mode 100644 (file)
index 0000000..b8ffff0
--- /dev/null
@@ -0,0 +1,604 @@
+blob
+mark :1
+data 51
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+
+reset refs/heads/removed
+commit refs/heads/removed
+mark :2
+author Thomas Rast <trast@inf.ethz.ch> 1374424921 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374484724 +0200
+data 8
+initial
+M 100644 :1 file
+
+blob
+mark :3
+data 51
+1
+2
+3
+4
+A
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/topic
+mark :4
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+data 7
+s/5/A/
+from :2
+M 100644 :3 file
+
+blob
+mark :5
+data 51
+1
+2
+3
+A
+A
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/topic
+mark :6
+author Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+data 7
+s/4/A/
+from :4
+M 100644 :5 file
+
+blob
+mark :7
+data 50
+1
+2
+3
+A
+A
+6
+7
+8
+9
+10
+B
+12
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/topic
+mark :8
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+data 8
+s/11/B/
+from :6
+M 100644 :7 file
+
+blob
+mark :9
+data 49
+1
+2
+3
+A
+A
+6
+7
+8
+9
+10
+B
+B
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/topic
+mark :10
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+data 8
+s/12/B/
+from :8
+M 100644 :9 file
+
+blob
+mark :11
+data 10
+unrelated
+
+commit refs/heads/master
+mark :12
+author Thomas Rast <trast@inf.ethz.ch> 1374485127 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485127 +0200
+data 10
+unrelated
+from :2
+M 100644 :11 otherfile
+
+commit refs/heads/rebased
+mark :13
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485137 +0200
+data 7
+s/5/A/
+from :12
+M 100644 :3 file
+
+commit refs/heads/rebased
+mark :14
+author Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485138 +0200
+data 7
+s/4/A/
+from :13
+M 100644 :5 file
+
+commit refs/heads/rebased
+mark :15
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485138 +0200
+data 8
+s/11/B/
+from :14
+M 100644 :7 file
+
+commit refs/heads/rebased
+mark :16
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485138 +0200
+data 8
+s/12/B/
+from :15
+M 100644 :9 file
+
+commit refs/heads/added
+mark :17
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485341 +0200
+data 7
+s/5/A/
+from :2
+M 100644 :3 file
+
+commit refs/heads/added
+mark :18
+author Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485341 +0200
+data 7
+s/4/A/
+from :17
+M 100644 :5 file
+
+blob
+mark :19
+data 51
+1
+2
+3
+A
+A
+A
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/added
+mark :20
+author Thomas Rast <trast@inf.ethz.ch> 1374485186 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485341 +0200
+data 7
+s/6/A/
+from :18
+M 100644 :19 file
+
+blob
+mark :21
+data 50
+1
+2
+3
+A
+A
+A
+7
+8
+9
+10
+B
+12
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/added
+mark :22
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485341 +0200
+data 8
+s/11/B/
+from :20
+M 100644 :21 file
+
+blob
+mark :23
+data 49
+1
+2
+3
+A
+A
+A
+7
+8
+9
+10
+B
+B
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/added
+mark :24
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485341 +0200
+data 8
+s/12/B/
+from :22
+M 100644 :23 file
+
+commit refs/heads/reordered
+mark :25
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485350 +0200
+data 7
+s/5/A/
+from :2
+M 100644 :3 file
+
+blob
+mark :26
+data 50
+1
+2
+3
+4
+A
+6
+7
+8
+9
+10
+B
+12
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/reordered
+mark :27
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485350 +0200
+data 8
+s/11/B/
+from :25
+M 100644 :26 file
+
+blob
+mark :28
+data 49
+1
+2
+3
+4
+A
+6
+7
+8
+9
+10
+B
+B
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/reordered
+mark :29
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485350 +0200
+data 8
+s/12/B/
+from :27
+M 100644 :28 file
+
+commit refs/heads/reordered
+mark :30
+author Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485350 +0200
+data 7
+s/4/A/
+from :29
+M 100644 :9 file
+
+commit refs/heads/changed
+mark :31
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485507 +0200
+data 7
+s/5/A/
+from :2
+M 100644 :3 file
+
+commit refs/heads/changed
+mark :32
+author Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485507 +0200
+data 7
+s/4/A/
+from :31
+M 100644 :5 file
+
+blob
+mark :33
+data 51
+1
+2
+3
+A
+A
+6
+7
+8
+9
+10
+BB
+12
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/changed
+mark :34
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485507 +0200
+data 8
+s/11/B/
+from :32
+M 100644 :33 file
+
+blob
+mark :35
+data 50
+1
+2
+3
+A
+A
+6
+7
+8
+9
+10
+BB
+B
+13
+14
+15
+16
+17
+18
+19
+20
+
+commit refs/heads/changed
+mark :36
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485507 +0200
+data 8
+s/12/B/
+from :34
+M 100644 :35 file
+
+commit refs/heads/changed-message
+mark :37
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485530 +0200
+data 7
+s/5/A/
+from :2
+M 100644 :3 file
+
+commit refs/heads/changed-message
+mark :38
+author Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485530 +0200
+data 35
+s/4/A/
+
+Also a silly comment here!
+from :37
+M 100644 :5 file
+
+commit refs/heads/changed-message
+mark :39
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485536 +0200
+data 8
+s/11/B/
+from :38
+M 100644 :7 file
+
+commit refs/heads/changed-message
+mark :40
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485536 +0200
+data 8
+s/12/B/
+from :39
+M 100644 :9 file
+
+commit refs/heads/unmodified
+mark :41
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485631 +0200
+data 7
+s/5/A/
+from :2
+M 100644 :3 file
+
+commit refs/heads/unmodified
+mark :42
+author Thomas Rast <trast@inf.ethz.ch> 1374485024 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485631 +0200
+data 7
+s/4/A/
+from :41
+M 100644 :5 file
+
+commit refs/heads/unmodified
+mark :43
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485632 +0200
+data 8
+s/11/B/
+from :42
+M 100644 :7 file
+
+commit refs/heads/unmodified
+mark :44
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374485632 +0200
+data 8
+s/12/B/
+from :43
+M 100644 :9 file
+
+commit refs/heads/removed
+mark :45
+author Thomas Rast <trast@inf.ethz.ch> 1374485014 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374486061 +0200
+data 7
+s/5/A/
+from :2
+M 100644 :3 file
+
+commit refs/heads/removed
+mark :46
+author Thomas Rast <trast@inf.ethz.ch> 1374485036 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374486061 +0200
+data 8
+s/11/B/
+from :45
+M 100644 :26 file
+
+commit refs/heads/removed
+mark :47
+author Thomas Rast <trast@inf.ethz.ch> 1374485044 +0200
+committer Thomas Rast <trast@inf.ethz.ch> 1374486061 +0200
+data 8
+s/12/B/
+from :46
+M 100644 :28 file
+
+reset refs/heads/removed
+from :47
+