diff.c: color moved lines differently

When a patch consists mostly of moving blocks of code around, it can be quite tedious to ensure that the blocks are moved verbatim, and not undesirably modified in the move. To that end, color blocks that are moved within the same patch differently. For example (OM, del, add, and NM are different colors): [OM] -void sensitive_stuff(void) [OM] -{ [OM] - if (!is_authorized_user()) [OM] - die("unauthorized"); [OM] - sensitive_stuff(spanning, [OM] - multiple, [OM] - lines); [OM] -} void another_function() { [del] - printf("foo"); [add] + printf("bar"); } [NM] +void sensitive_stuff(void) [NM] +{ [NM] + if (!is_authorized_user()) [NM] + die("unauthorized"); [NM] + sensitive_stuff(spanning, [NM] + multiple, [NM] + lines); [NM] +} However adjacent blocks may be problematic. For example, in this potentially malicious patch, the swapping of blocks can be spotted: [OM] -void sensitive_stuff(void) [OM] -{ [OMA] - if (!is_authorized_user()) [OMA] - die("unauthorized"); [OM] - sensitive_stuff(spanning, [OM] - multiple, [OM] - lines); [OMA] -} void another_function() { [del] - printf("foo"); [add] + printf("bar"); } [NM] +void sensitive_stuff(void) [NM] +{ [NMA] + sensitive_stuff(spanning, [NMA] + multiple, [NMA] + lines); [NM] + if (!is_authorized_user()) [NM] + die("unauthorized"); [NMA] +} If the moved code is larger, it is easier to hide some permutation in the code, which is why some alternative coloring is needed. This patch implements the first mode: * basic alternating 'Zebra' mode This conveys all information needed to the user. Defer customization to later patches. First I implemented an alternative design, which would try to fingerprint a line by its neighbors to detect if we are in a block or at the boundary. This idea iss error prone as it inspected each line and its neighboring lines to determine if the line was (a) moved and (b) if was deep inside a hunk by having matching neighboring lines. This is unreliable as the we can construct hunks which have equal neighbors that just exceed the number of lines inspected. (Think of 'AXYZBXYZCXYZD..' with each letter as a line, that is permutated to AXYZCXYZBXYZD..'). Instead this provides a dynamic programming greedy algorithm that finds the largest moved hunk and then has several modes on highlighting bounds. A note on the options '--submodule=diff' and '--color-words/--word-diff': In the conversion to use emit_line in the prior patches both submodules as well as word diff output carefully chose to call emit_line with sign=0. All output with sign=0 is ignored for move detection purposes in this patch, such that no weird looking output will be generated for these cases. This leads to another thought: We could pass on '--color-moved' to submodules such that they color up moved lines for themselves. If we'd do so only line moves within a repository boundary are marked up. It is useful to have moved lines colored, but there are annoying corner cases, such as a single line moved, that is very common. For example in a typical patch of C code, we have closing braces that end statement blocks or functions. While it is technically true that these lines are moved as they show up elsewhere, it is harmful for the review as the reviewers attention is drawn to such a minor side annoyance. For now let's have a simple solution of hardcoding the number of moved lines to be at least 3 before coloring them. Note, that the length is applied across all blocks to find the 'lonely' blocks that pollute new code, but do not interfere with a permutated block where each permutation has less lines than 3. Helped-by: Jonathan Tan <jonathantanmy@google.com> Signed-off-by: Stefan Beller <sbeller@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Stefan Beller committed Jun 30, 2017 at 13:53 UTC 2e2d5ac184de8facde4e14cec8b4e2a154480ed8
3 files changed +602 -15
diff.c
+330 -14
@@ -16,6 +16,7 @@
16 #include "userdiff.h"
17 #include "submodule-config.h"
18 #include "submodule.h"
19 +#include "hashmap.h"
20 #include "ll-merge.h"
21 #include "string-list.h"
22 #include "argv-array.h"
@@ -32,6 +33,7 @@ static int diff_indent_heuristic = 1;
33 static int diff_rename_limit_default = 400;
34 static int diff_suppress_blank_empty;
35 static int diff_use_color_default = -1;
36 +static int diff_color_moved_default;
37 static int diff_context_default = 3;
38 static int diff_interhunk_context_default;
39 static const char *diff_word_regex_cfg;
@@ -56,6 +58,10 @@ static char diff_colors[][COLOR_MAXLEN] = {
58 GIT_COLOR_YELLOW, /* COMMIT */
59 GIT_COLOR_BG_RED, /* WHITESPACE */
60 GIT_COLOR_NORMAL, /* FUNCINFO */
61 + GIT_COLOR_MAGENTA, /* OLD_MOVED */
62 + GIT_COLOR_BLUE, /* OLD_MOVED ALTERNATIVE */
63 + GIT_COLOR_CYAN, /* NEW_MOVED */
64 + GIT_COLOR_YELLOW, /* NEW_MOVED ALTERNATIVE */
65 };
66
67 static NORETURN void die_want_option(const char *option_name)
@@ -81,6 +87,14 @@ static int parse_diff_color_slot(const char *var)
87 return DIFF_WHITESPACE;
88 if (!strcasecmp(var, "func"))
89 return DIFF_FUNCINFO;
90 + if (!strcasecmp(var, "oldmoved"))
91 + return DIFF_FILE_OLD_MOVED;
92 + if (!strcasecmp(var, "oldmovedalternative"))
93 + return DIFF_FILE_OLD_MOVED_ALT;
94 + if (!strcasecmp(var, "newmoved"))
95 + return DIFF_FILE_NEW_MOVED;
96 + if (!strcasecmp(var, "newmovedalternative"))
97 + return DIFF_FILE_NEW_MOVED_ALT;
98 return -1;
99 }
100
@@ -229,12 +243,40 @@ int git_diff_heuristic_config(const char *var, const char *value, void *cb)
243 return 0;
244 }
245
246 +static int parse_color_moved(const char *arg)
247 +{
248 + switch (git_parse_maybe_bool(arg)) {
249 + case 0:
250 + return COLOR_MOVED_NO;
251 + case 1:
252 + return COLOR_MOVED_DEFAULT;
253 + default:
254 + break;
255 + }
256 +
257 + if (!strcmp(arg, "no"))
258 + return COLOR_MOVED_NO;
259 + else if (!strcmp(arg, "zebra"))
260 + return COLOR_MOVED_ZEBRA;
261 + else if (!strcmp(arg, "default"))
262 + return COLOR_MOVED_DEFAULT;
263 + else
264 + return error(_("color moved setting must be one of 'no', 'default', 'zebra'"));
265 +}
266 +
267 int git_diff_ui_config(const char *var, const char *value, void *cb)
268 {
269 if (!strcmp(var, "diff.color") || !strcmp(var, "color.diff")) {
270 diff_use_color_default = git_config_colorbool(var, value);
271 return 0;
272 }
273 + if (!strcmp(var, "diff.colormoved")) {
274 + int cm = parse_color_moved(value);
275 + if (cm < 0)
276 + return -1;
277 + diff_color_moved_default = cm;
278 + return 0;
279 + }
280 if (!strcmp(var, "diff.context")) {
281 diff_context_default = git_config_int(var, value);
282 if (diff_context_default < 0)
@@ -602,7 +644,9 @@ enum diff_symbol {
644 * 13-15 are WSEH_NEW | WSEH_OLD | WSEH_CONTEXT
645 * 16 is marking if the line is blank at EOF
646 */
605 -#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF (1<<16)
647 +#define DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF (1<<16)
648 +#define DIFF_SYMBOL_MOVED_LINE (1<<17)
649 +#define DIFF_SYMBOL_MOVED_LINE_ALT (1<<18)
650 #define DIFF_SYMBOL_CONTENT_WS_MASK (WSEH_NEW | WSEH_OLD | WSEH_CONTEXT | WS_RULE_MASK)
651
652 /*
@@ -645,6 +689,243 @@ static void append_emitted_diff_symbol(struct diff_options *o,
689 f->line = e->line ? xmemdupz(e->line, e->len) : NULL;
690 }
691
692 +struct moved_entry {
693 + struct hashmap_entry ent;
694 + const struct emitted_diff_symbol *es;
695 + struct moved_entry *next_line;
696 +};
697 +
698 +static int next_byte(const char **cp, const char **endp,
699 + const struct diff_options *diffopt)
700 +{
701 + int retval;
702 +
703 + if (*cp > *endp)
704 + return -1;
705 +
706 + if (DIFF_XDL_TST(diffopt, IGNORE_WHITESPACE_CHANGE)) {
707 + while (*cp < *endp && isspace(**cp))
708 + (*cp)++;
709 + /*
710 + * After skipping a couple of whitespaces, we still have to
711 + * account for one space.
712 + */
713 + return (int)' ';
714 + }
715 +
716 + if (DIFF_XDL_TST(diffopt, IGNORE_WHITESPACE)) {
717 + while (*cp < *endp && isspace(**cp))
718 + (*cp)++;
719 + /* return the first non-ws character via the usual below */
720 + }
721 +
722 + retval = (unsigned char)(**cp);
723 + (*cp)++;
724 + return retval;
725 +}
726 +
727 +static int moved_entry_cmp(const struct diff_options *diffopt,
728 + const struct moved_entry *a,
729 + const struct moved_entry *b,
730 + const void *keydata)
731 +{
732 + const char *ap = a->es->line, *ae = a->es->line + a->es->len;
733 + const char *bp = b->es->line, *be = b->es->line + b->es->len;
734 +
735 + if (!(diffopt->xdl_opts & XDF_WHITESPACE_FLAGS))
736 + return a->es->len != b->es->len || memcmp(ap, bp, a->es->len);
737 +
738 + if (DIFF_XDL_TST(diffopt, IGNORE_WHITESPACE_AT_EOL)) {
739 + while (ae > ap && isspace(*ae))
740 + ae--;
741 + while (be > bp && isspace(*be))
742 + be--;
743 + }
744 +
745 + while (1) {
746 + int ca, cb;
747 + ca = next_byte(&ap, &ae, diffopt);
748 + cb = next_byte(&bp, &be, diffopt);
749 + if (ca != cb)
750 + return 1;
751 + if (ca < 0)
752 + return 0;
753 + }
754 +}
755 +
756 +static unsigned get_string_hash(struct emitted_diff_symbol *es, struct diff_options *o)
757 +{
758 + if (o->xdl_opts & XDF_WHITESPACE_FLAGS) {
759 + static struct strbuf sb = STRBUF_INIT;
760 + const char *ap = es->line, *ae = es->line + es->len;
761 + int c;
762 +
763 + strbuf_reset(&sb);
764 + while (ae > ap && isspace(*ae))
765 + ae--;
766 + while ((c = next_byte(&ap, &ae, o)) > 0)
767 + strbuf_addch(&sb, c);
768 +
769 + return memhash(sb.buf, sb.len);
770 + } else {
771 + return memhash(es->line, es->len);
772 + }
773 +}
774 +
775 +static struct moved_entry *prepare_entry(struct diff_options *o,
776 + int line_no)
777 +{
778 + struct moved_entry *ret = xmalloc(sizeof(*ret));
779 + struct emitted_diff_symbol *l = &o->emitted_symbols->buf[line_no];
780 +
781 + ret->ent.hash = get_string_hash(l, o);
782 + ret->es = l;
783 + ret->next_line = NULL;
784 +
785 + return ret;
786 +}
787 +
788 +static void add_lines_to_move_detection(struct diff_options *o,
789 + struct hashmap *add_lines,
790 + struct hashmap *del_lines)
791 +{
792 + struct moved_entry *prev_line = NULL;
793 +
794 + int n;
795 + for (n = 0; n < o->emitted_symbols->nr; n++) {
796 + struct hashmap *hm;
797 + struct moved_entry *key;
798 +
799 + switch (o->emitted_symbols->buf[n].s) {
800 + case DIFF_SYMBOL_PLUS:
801 + hm = add_lines;
802 + break;
803 + case DIFF_SYMBOL_MINUS:
804 + hm = del_lines;
805 + break;
806 + default:
807 + prev_line = NULL;
808 + continue;
809 + }
810 +
811 + key = prepare_entry(o, n);
812 + if (prev_line && prev_line->es->s == o->emitted_symbols->buf[n].s)
813 + prev_line->next_line = key;
814 +
815 + hashmap_add(hm, key);
816 + prev_line = key;
817 + }
818 +}
819 +
820 +static int shrink_potential_moved_blocks(struct moved_entry **pmb,
821 + int pmb_nr)
822 +{
823 + int lp, rp;
824 +
825 + /* Shrink the set of potential block to the remaining running */
826 + for (lp = 0, rp = pmb_nr - 1; lp <= rp;) {
827 + while (lp < pmb_nr && pmb[lp])
828 + lp++;
829 + /* lp points at the first NULL now */
830 +
831 + while (rp > -1 && !pmb[rp])
832 + rp--;
833 + /* rp points at the last non-NULL */
834 +
835 + if (lp < pmb_nr && rp > -1 && lp < rp) {
836 + pmb[lp] = pmb[rp];
837 + pmb[rp] = NULL;
838 + rp--;
839 + lp++;
840 + }
841 + }
842 +
843 + /* Remember the number of running sets */
844 + return rp + 1;
845 +}
846 +
847 +/* Find blocks of moved code, delegate actual coloring decision to helper */
848 +static void mark_color_as_moved(struct diff_options *o,
849 + struct hashmap *add_lines,
850 + struct hashmap *del_lines)
851 +{
852 + struct moved_entry **pmb = NULL; /* potentially moved blocks */
853 + int pmb_nr = 0, pmb_alloc = 0;
854 + int n, flipped_block = 1, block_length = 0;
855 +
856 +
857 + for (n = 0; n < o->emitted_symbols->nr; n++) {
858 + struct hashmap *hm = NULL;
859 + struct moved_entry *key;
860 + struct moved_entry *match = NULL;
861 + struct emitted_diff_symbol *l = &o->emitted_symbols->buf[n];
862 + int i;
863 +
864 + switch (l->s) {
865 + case DIFF_SYMBOL_PLUS:
866 + hm = del_lines;
867 + key = prepare_entry(o, n);
868 + match = hashmap_get(hm, key, o);
869 + free(key);
870 + break;
871 + case DIFF_SYMBOL_MINUS:
872 + hm = add_lines;
873 + key = prepare_entry(o, n);
874 + match = hashmap_get(hm, key, o);
875 + free(key);
876 + break;
877 + default:
878 + flipped_block = 1;
879 + }
880 +
881 + if (!match) {
882 + if (block_length < COLOR_MOVED_MIN_BLOCK_LENGTH) {
883 + for (i = 0; i < block_length + 1; i++) {
884 + l = &o->emitted_symbols->buf[n - i];
885 + l->flags &= ~DIFF_SYMBOL_MOVED_LINE;
886 + }
887 + }
888 + pmb_nr = 0;
889 + block_length = 0;
890 + continue;
891 + }
892 +
893 + l->flags |= DIFF_SYMBOL_MOVED_LINE;
894 + block_length++;
895 +
896 + /* Check any potential block runs, advance each or nullify */
897 + for (i = 0; i < pmb_nr; i++) {
898 + struct moved_entry *p = pmb[i];
899 + struct moved_entry *pnext = (p && p->next_line) ?
900 + p->next_line : NULL;
901 + if (pnext && !hm->cmpfn(o, pnext, match, NULL)) {
902 + pmb[i] = p->next_line;
903 + } else {
904 + pmb[i] = NULL;
905 + }
906 + }
907 +
908 + pmb_nr = shrink_potential_moved_blocks(pmb, pmb_nr);
909 +
910 + if (pmb_nr == 0) {
911 + /*
912 + * The current line is the start of a new block.
913 + * Setup the set of potential blocks.
914 + */
915 + for (; match; match = hashmap_get_next(hm, match)) {
916 + ALLOC_GROW(pmb, pmb_nr + 1, pmb_alloc);
917 + pmb[pmb_nr++] = match;
918 + }
919 +
920 + flipped_block = (flipped_block + 1) % 2;
921 + }
922 +
923 + if (flipped_block)
924 + l->flags |= DIFF_SYMBOL_MOVED_LINE_ALT;
925 + }
926 +
927 + free(pmb);
928 +}
929
930 static void emit_line_ws_markup(struct diff_options *o,
931 const char *set, const char *reset,
@@ -720,14 +1001,24 @@ static void emit_diff_symbol_from_struct(struct diff_options *o,
1001 flags & (DIFF_SYMBOL_CONTENT_WS_MASK), 0);
1002 break;
1003 case DIFF_SYMBOL_PLUS:
723 - set = diff_get_color_opt(o, DIFF_FILE_NEW);
1004 + if (flags & DIFF_SYMBOL_MOVED_LINE_ALT)
1005 + set = diff_get_color_opt(o, DIFF_FILE_NEW_MOVED_ALT);
1006 + else if (flags & DIFF_SYMBOL_MOVED_LINE)
1007 + set = diff_get_color_opt(o, DIFF_FILE_NEW_MOVED);
1008 + else
1009 + set = diff_get_color_opt(o, DIFF_FILE_NEW);
1010 reset = diff_get_color_opt(o, DIFF_RESET);
1011 emit_line_ws_markup(o, set, reset, line, len, '+',
1012 flags & DIFF_SYMBOL_CONTENT_WS_MASK,
1013 flags & DIFF_SYMBOL_CONTENT_BLANK_LINE_EOF);
1014 break;
1015 case DIFF_SYMBOL_MINUS:
730 - set = diff_get_color_opt(o, DIFF_FILE_OLD);
1016 + if (flags & DIFF_SYMBOL_MOVED_LINE_ALT)
1017 + set = diff_get_color_opt(o, DIFF_FILE_OLD_MOVED_ALT);
1018 + else if (flags & DIFF_SYMBOL_MOVED_LINE)
1019 + set = diff_get_color_opt(o, DIFF_FILE_OLD_MOVED);
1020 + else
1021 + set = diff_get_color_opt(o, DIFF_FILE_OLD);
1022 reset = diff_get_color_opt(o, DIFF_RESET);
1023 emit_line_ws_markup(o, set, reset, line, len, '-',
1024 flags & DIFF_SYMBOL_CONTENT_WS_MASK, 0);
@@ -3740,6 +4031,8 @@ void diff_setup(struct diff_options *options)
4031 options->a_prefix = "a/";
4032 options->b_prefix = "b/";
4033 }
4034 +
4035 + options->color_moved = diff_color_moved_default;
4036 }
4037
4038 void diff_setup_done(struct diff_options *options)
@@ -3849,6 +4142,9 @@ void diff_setup_done(struct diff_options *options)
4142
4143 if (DIFF_OPT_TST(options, FOLLOW_RENAMES) && options->pathspec.nr != 1)
4144 die(_("--follow requires exactly one pathspec"));
4145 +
4146 + if (!options->use_color || external_diff())
4147 + options->color_moved = 0;
4148 }
4149
4150 static int opt_arg(const char *arg, int arg_short, const char *arg_long, int *val)
@@ -4273,7 +4569,19 @@ int diff_opt_parse(struct diff_options *options,
4569 }
4570 else if (!strcmp(arg, "--no-color"))
4571 options->use_color = 0;
4276 - else if (!strcmp(arg, "--color-words")) {
4572 + else if (!strcmp(arg, "--color-moved")) {
4573 + if (diff_color_moved_default)
4574 + options->color_moved = diff_color_moved_default;
4575 + if (options->color_moved == COLOR_MOVED_NO)
4576 + options->color_moved = COLOR_MOVED_DEFAULT;
4577 + } else if (!strcmp(arg, "--no-color-moved"))
4578 + options->color_moved = COLOR_MOVED_NO;
4579 + else if (skip_prefix(arg, "--color-moved=", &arg)) {
4580 + int cm = parse_color_moved(arg);
4581 + if (cm < 0)
4582 + die("bad --color-moved argument: %s", arg);
4583 + options->color_moved = cm;
4584 + } else if (!strcmp(arg, "--color-words")) {
4585 options->use_color = 1;
4586 options->word_diff = DIFF_WORDS_COLOR;
4587 }
@@ -5086,16 +5394,8 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)
5394 if (WSEH_NEW & WS_RULE_MASK)
5395 die("BUG: WS rules bit mask overlaps with diff symbol flags");
5396
5089 - /*
5090 - * For testing purposes we want to make sure the diff machinery
5091 - * works completely with the buffer. If there is anything emitted
5092 - * outside the emit_string, then the order is screwed
5093 - * up and the tests will fail.
5094 - *
5095 - * TODO (later in this series):
5096 - * We'll unset this pointer in a later patch.
5097 - */
5098 - o->emitted_symbols = &esm;
5397 + if (o->color_moved)
5398 + o->emitted_symbols = &esm;
5399
5400 for (i = 0; i < q->nr; i++) {
5401 struct diff_filepair *p = q->queue[i];
@@ -5104,6 +5404,21 @@ static void diff_flush_patch_all_file_pairs(struct diff_options *o)
5404 }
5405
5406 if (o->emitted_symbols) {
5407 + if (o->color_moved) {
5408 + struct hashmap add_lines, del_lines;
5409 +
5410 + hashmap_init(&del_lines,
5411 + (hashmap_cmp_fn)moved_entry_cmp, o, 0);
5412 + hashmap_init(&add_lines,
5413 + (hashmap_cmp_fn)moved_entry_cmp, o, 0);
5414 +
5415 + add_lines_to_move_detection(o, &add_lines, &del_lines);
5416 + mark_color_as_moved(o, &add_lines, &del_lines);
5417 +
5418 + hashmap_free(&add_lines, 0);
5419 + hashmap_free(&del_lines, 0);
5420 + }
5421 +
5422 for (i = 0; i < esm.nr; i++)
5423 emit_diff_symbol_from_struct(o, &esm.buf[i]);
5424
@@ -5185,6 +5500,7 @@ void diff_flush(struct diff_options *options)
5500 fclose(options->file);
5501 options->file = xfopen("/dev/null", "w");
5502 options->close_file = 1;
5503 + options->color_moved = 0;
5504 for (i = 0; i < q->nr; i++) {
5505 struct diff_filepair *p = q->queue[i];
5506 if (check_pair_status(p))
diff.h
+11 -1
@@ -188,6 +188,12 @@ struct diff_options {
188 int diff_path_counter;
189
190 struct emitted_diff_symbols *emitted_symbols;
191 + enum {
192 + COLOR_MOVED_NO = 0,
193 + COLOR_MOVED_ZEBRA = 2,
194 + } color_moved;
195 + #define COLOR_MOVED_DEFAULT COLOR_MOVED_ZEBRA
196 + #define COLOR_MOVED_MIN_BLOCK_LENGTH 3
197 };
198
199 void diff_emit_submodule_del(struct diff_options *o, const char *line);
@@ -208,7 +214,11 @@ enum color_diff {
214 DIFF_FILE_NEW = 5,
215 DIFF_COMMIT = 6,
216 DIFF_WHITESPACE = 7,
211 - DIFF_FUNCINFO = 8
217 + DIFF_FUNCINFO = 8,
218 + DIFF_FILE_OLD_MOVED = 9,
219 + DIFF_FILE_OLD_MOVED_ALT = 10,
220 + DIFF_FILE_NEW_MOVED = 11,
221 + DIFF_FILE_NEW_MOVED_ALT = 12
222 };
223 const char *diff_get_color(int diff_use_color, enum color_diff ix);
224 #define diff_get_color_opt(o, ix) \
t/t4015-diff-whitespace.sh
+261
@@ -972,4 +972,265 @@ test_expect_success 'option overrides diff.wsErrorHighlight' '
972
973 '
974
975 +test_expect_success 'detect moved code, complete file' '
976 + git reset --hard &&
977 + cat <<-\EOF >test.c &&
978 + #include<stdio.h>
979 + main()
980 + {
981 + printf("Hello World");
982 + }
983 + EOF
984 + git add test.c &&
985 + git commit -m "add main function" &&
986 + git mv test.c main.c &&
987 + test_config color.diff.oldMoved "normal red" &&
988 + test_config color.diff.newMoved "normal green" &&
989 + git diff HEAD --color-moved --no-renames | test_decode_color >actual &&
990 + cat >expected <<-\EOF &&
991 + <BOLD>diff --git a/main.c b/main.c<RESET>
992 + <BOLD>new file mode 100644<RESET>
993 + <BOLD>index 0000000..a986c57<RESET>
994 + <BOLD>--- /dev/null<RESET>
995 + <BOLD>+++ b/main.c<RESET>
996 + <CYAN>@@ -0,0 +1,5 @@<RESET>
997 + <BGREEN>+<RESET><BGREEN>#include<stdio.h><RESET>
998 + <BGREEN>+<RESET><BGREEN>main()<RESET>
999 + <BGREEN>+<RESET><BGREEN>{<RESET>
1000 + <BGREEN>+<RESET><BGREEN>printf("Hello World");<RESET>
1001 + <BGREEN>+<RESET><BGREEN>}<RESET>
1002 + <BOLD>diff --git a/test.c b/test.c<RESET>
1003 + <BOLD>deleted file mode 100644<RESET>
1004 + <BOLD>index a986c57..0000000<RESET>
1005 + <BOLD>--- a/test.c<RESET>
1006 + <BOLD>+++ /dev/null<RESET>
1007 + <CYAN>@@ -1,5 +0,0 @@<RESET>
1008 + <BRED>-#include<stdio.h><RESET>
1009 + <BRED>-main()<RESET>
1010 + <BRED>-{<RESET>
1011 + <BRED>-printf("Hello World");<RESET>
1012 + <BRED>-}<RESET>
1013 + EOF
1014 +
1015 + test_cmp expected actual
1016 +'
1017 +
1018 +test_expect_success 'detect malicious moved code, inside file' '
1019 + test_config color.diff.oldMoved "normal red" &&
1020 + test_config color.diff.newMoved "normal green" &&
1021 + test_config color.diff.oldMovedAlternative "blue" &&
1022 + test_config color.diff.newMovedAlternative "yellow" &&
1023 + git reset --hard &&
1024 + cat <<-\EOF >main.c &&
1025 + #include<stdio.h>
1026 + int stuff()
1027 + {
1028 + printf("Hello ");
1029 + printf("World\n");
1030 + }
1031 +
1032 + int secure_foo(struct user *u)
1033 + {
1034 + if (!u->is_allowed_foo)
1035 + return;
1036 + foo(u);
1037 + }
1038 +
1039 + int main()
1040 + {
1041 + foo();
1042 + }
1043 + EOF
1044 + cat <<-\EOF >test.c &&
1045 + #include<stdio.h>
1046 + int bar()
1047 + {
1048 + printf("Hello World, but different\n");
1049 + }
1050 +
1051 + int another_function()
1052 + {
1053 + bar();
1054 + }
1055 + EOF
1056 + git add main.c test.c &&
1057 + git commit -m "add main and test file" &&
1058 + cat <<-\EOF >main.c &&
1059 + #include<stdio.h>
1060 + int stuff()
1061 + {
1062 + printf("Hello ");
1063 + printf("World\n");
1064 + }
1065 +
1066 + int main()
1067 + {
1068 + foo();
1069 + }
1070 + EOF
1071 + cat <<-\EOF >test.c &&
1072 + #include<stdio.h>
1073 + int bar()
1074 + {
1075 + printf("Hello World, but different\n");
1076 + }
1077 +
1078 + int secure_foo(struct user *u)
1079 + {
1080 + foo(u);
1081 + if (!u->is_allowed_foo)
1082 + return;
1083 + }
1084 +
1085 + int another_function()
1086 + {
1087 + bar();
1088 + }
1089 + EOF
1090 + git diff HEAD --no-renames --color-moved=zebra| test_decode_color >actual &&
1091 + cat <<-\EOF >expected &&
1092 + <BOLD>diff --git a/main.c b/main.c<RESET>
1093 + <BOLD>index 27a619c..7cf9336 100644<RESET>
1094 + <BOLD>--- a/main.c<RESET>
1095 + <BOLD>+++ b/main.c<RESET>
1096 + <CYAN>@@ -5,13 +5,6 @@<RESET> <RESET>printf("Hello ");<RESET>
1097 + printf("World\n");<RESET>
1098 + }<RESET>
1099 + <RESET>
1100 + <BRED>-int secure_foo(struct user *u)<RESET>
1101 + <BRED>-{<RESET>
1102 + <BLUE>-if (!u->is_allowed_foo)<RESET>
1103 + <BLUE>-return;<RESET>
1104 + <BRED>-foo(u);<RESET>
1105 + <BLUE>-}<RESET>
1106 + <BLUE>-<RESET>
1107 + int main()<RESET>
1108 + {<RESET>
1109 + foo();<RESET>
1110 + <BOLD>diff --git a/test.c b/test.c<RESET>
1111 + <BOLD>index 1dc1d85..2bedec9 100644<RESET>
1112 + <BOLD>--- a/test.c<RESET>
1113 + <BOLD>+++ b/test.c<RESET>
1114 + <CYAN>@@ -4,6 +4,13 @@<RESET> <RESET>int bar()<RESET>
1115 + printf("Hello World, but different\n");<RESET>
1116 + }<RESET>
1117 + <RESET>
1118 + <BGREEN>+<RESET><BGREEN>int secure_foo(struct user *u)<RESET>
1119 + <BGREEN>+<RESET><BGREEN>{<RESET>
1120 + <YELLOW>+<RESET><YELLOW>foo(u);<RESET>
1121 + <BGREEN>+<RESET><BGREEN>if (!u->is_allowed_foo)<RESET>
1122 + <BGREEN>+<RESET><BGREEN>return;<RESET>
1123 + <YELLOW>+<RESET><YELLOW>}<RESET>
1124 + <YELLOW>+<RESET>
1125 + int another_function()<RESET>
1126 + {<RESET>
1127 + bar();<RESET>
1128 + EOF
1129 +
1130 + test_cmp expected actual
1131 +'
1132 +
1133 +test_expect_success 'no effect from --color-moved with --word-diff' '
1134 + cat <<-\EOF >text.txt &&
1135 + Lorem Ipsum is simply dummy text of the printing and typesetting industry.
1136 + EOF
1137 + git add text.txt &&
1138 + git commit -a -m "clean state" &&
1139 + cat <<-\EOF >text.txt &&
1140 + simply Lorem Ipsum dummy is text of the typesetting and printing industry.
1141 + EOF
1142 + git diff --color-moved --word-diff >actual &&
1143 + git diff --word-diff >expect &&
1144 + test_cmp expect actual
1145 +'
1146 +
1147 +test_expect_success 'move detection ignoring whitespace ' '
1148 + git reset --hard &&
1149 + cat <<\EOF >lines.txt &&
1150 +line 1
1151 +line 2
1152 +line 3
1153 +line 4
1154 +line 5
1155 +line 6
1156 +line 7
1157 +EOF
1158 + git add lines.txt &&
1159 + git commit -m "add poetry" &&
1160 + cat <<\EOF >lines.txt &&
1161 + line 5
1162 + line 6
1163 + line 7
1164 +line 1
1165 +line 2
1166 +line 3
1167 +line 4
1168 +EOF
1169 + test_config color.diff.oldMoved "magenta" &&
1170 + test_config color.diff.newMoved "cyan" &&
1171 + git diff HEAD --no-renames --color-moved| test_decode_color >actual &&
1172 + cat <<-\EOF >expected &&
1173 + <BOLD>diff --git a/lines.txt b/lines.txt<RESET>
1174 + <BOLD>index 734156d..eb89ead 100644<RESET>
1175 + <BOLD>--- a/lines.txt<RESET>
1176 + <BOLD>+++ b/lines.txt<RESET>
1177 + <CYAN>@@ -1,7 +1,7 @@<RESET>
1178 + <GREEN>+<RESET> <GREEN>line 5<RESET>
1179 + <GREEN>+<RESET> <GREEN>line 6<RESET>
1180 + <GREEN>+<RESET> <GREEN>line 7<RESET>
1181 + line 1<RESET>
1182 + line 2<RESET>
1183 + line 3<RESET>
1184 + line 4<RESET>
1185 + <RED>-line 5<RESET>
1186 + <RED>-line 6<RESET>
1187 + <RED>-line 7<RESET>
1188 + EOF
1189 + test_cmp expected actual &&
1190 +
1191 + git diff HEAD --no-renames -w --color-moved| test_decode_color >actual &&
1192 + cat <<-\EOF >expected &&
1193 + <BOLD>diff --git a/lines.txt b/lines.txt<RESET>
1194 + <BOLD>index 734156d..eb89ead 100644<RESET>
1195 + <BOLD>--- a/lines.txt<RESET>
1196 + <BOLD>+++ b/lines.txt<RESET>
1197 + <CYAN>@@ -1,7 +1,7 @@<RESET>
1198 + <CYAN>+<RESET> <CYAN>line 5<RESET>
1199 + <CYAN>+<RESET> <CYAN>line 6<RESET>
1200 + <CYAN>+<RESET> <CYAN>line 7<RESET>
1201 + line 1<RESET>
1202 + line 2<RESET>
1203 + line 3<RESET>
1204 + line 4<RESET>
1205 + <MAGENTA>-line 5<RESET>
1206 + <MAGENTA>-line 6<RESET>
1207 + <MAGENTA>-line 7<RESET>
1208 + EOF
1209 + test_cmp expected actual
1210 +'
1211 +
1212 +test_expect_success 'move detection with submodules' '
1213 + test_create_repo bananas &&
1214 + echo ripe >bananas/recipe &&
1215 + git -C bananas add recipe &&
1216 + test_commit fruit &&
1217 + test_commit -C bananas recipe &&
1218 + git submodule add ./bananas &&
1219 + git add bananas &&
1220 + git commit -a -m "bananas are like a heavy library?" &&
1221 + echo foul >bananas/recipe &&
1222 + echo ripe >fruit.t &&
1223 +
1224 + git diff --submodule=diff --color-moved >actual &&
1225 +
1226 + # no move detection as the moved line is across repository boundaries.
1227 + test_decode_color <actual >decoded_actual &&
1228 + ! grep BGREEN decoded_actual &&
1229 + ! grep BRED decoded_actual &&
1230 +
1231 + # nor did we mess with it another way
1232 + git diff --submodule=diff | test_decode_color >expect &&
1233 + test_cmp expect decoded_actual
1234 +'
1235 +
1236 test_done