Merge branch 'js/range-diff' into es/format-patch-rangediff

* 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
  ...
This commit is contained in:
Junio C Hamano 2018-08-14 14:21:46 -07:00
commit fe8f41fb2a
19 changed files with 1915 additions and 19 deletions

1
.gitignore vendored
View file

@ -113,6 +113,7 @@
/git-pull
/git-push
/git-quiltimport
/git-range-diff
/git-read-tree
/git-rebase
/git-rebase--am

View file

@ -1193,8 +1193,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

View file

@ -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

View file

@ -870,6 +870,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
@ -924,6 +925,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
@ -1062,6 +1064,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

View file

@ -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);

116
builtin/range-diff.c Normal file
View file

@ -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;
}

View file

@ -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"

View 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

View 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

105
diff.c
View file

@ -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)
@ -609,14 +621,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');
@ -634,8 +650,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);
@ -649,7 +667,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 {
@ -1168,7 +1186,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;
@ -1179,14 +1198,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);
}
@ -1196,7 +1221,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;
@ -1209,7 +1234,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:
@ -1236,7 +1261,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:
@ -1263,7 +1299,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;
@ -1291,7 +1343,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:
@ -1482,6 +1549,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;
@ -1502,6 +1570,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);
@ -3395,13 +3465,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;

10
diff.h
View file

@ -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) \

1
git.c
View file

@ -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 },

201
linear-assignment.c Normal file
View file

@ -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);
}

22
linear-assignment.h Normal file
View file

@ -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

435
range-diff.c Normal file
View file

@ -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;
}

9
range-diff.h Normal file
View file

@ -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

1
t/.gitattributes vendored
View 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

145
t/t3206-range-diff.sh Executable file
View file

@ -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

604
t/t3206/history.export Normal file
View file

@ -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