Subject: bug#20062: [bug-diffutils] bug#20062: [PATCH] diff: add support for --color

From: Giuseppe Scrivano <[email-masked]>
Date: Fri, 27 Nov 2015 16:01:56 +0100

Hi Jim,

thanks for the great advices.

Jim Meyering <[email-masked]> writes:

> I looked at the tests/colors script: we cannot/should not use sha1sum
> for two reasons. 1) it is a short cut; better to include the precise expected
> output in each case. Using this approach, if/when a test fails, there is
> no record of what the expected output was. 2) the "sha1sum" command
> is not universally available by that name. On BSD-based systems it is
> called "sha1". Thus, I began the conversion, and in so doing, I found
> some room for improvement: with the current patches I have, diff -u
> emits a pair of identical color-changing escape sequences before each
> "+"-prefixed line:
>
>   $ diff -u --color=always a b|cat -A
>   ^[[1;39m--- a^I1969-12-31 16:00:00.000000000 -0800$
>   +++ b^I1969-12-31 16:00:00.000000000 -0800$
>   ^[[0m^[[36m@@ -1 +1 @@^[[0m$
>   ^[[31m-a$
>   ^[[32m^[[32m+b$
>   ^[[0m
>
> Notice also how the final \e[0m is on the final line by itself,
> with no following newline. Please adjust so that it appears at the
> end of the final line instead. I confirmed that git-diff appears
> to do the same thing, but noted that git uses \e[m instead (no
> "0" part). Do you know of any pros/cons for one or the other?

I don't know if there is any difference in practice between \e[m and
\e0[m. I took the implementation in ls as reference which uses \e0[m.

> I've attached the beginnings of the adjusted tests/colors
> script that I used to discover these things. Can you finish the job
> of converting it to use "compare" rather than sha1sum?

Sure.  The new tests helped me to spot two issues in the "diff: add
support for --color" patch.  I also added some extra check to avoid to
enter the same colors context twice.  These fixes are in
0004-fixup-diff-add-support-for-color.patch.

To generate sequences on the same line they belong, I have created a new
patch to facilitate the review.  Probably the patch should get squashed
into 0001-diff-add-support-for-color.patch and 0005-tests-Add-tests-for-color-and-palette.patch
once reviewed.

I have not changed the first two patches of the series, I am including
them just for completeness.

Regards,
Giuseppe

>From 50def324f48ff09501e6f07cecae2a6cd5a16e3c Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <[email-masked]>
Date: Sun, 8 Mar 2015 22:45:11 +0100
Subject: [PATCH 1/6] diff: add support for --color

* doc/diffutils.texi (diff Options): Add documentation for --color.
Copied from coreutils ls --color.
* src/context.c (pr_unidiff_hunk): Set the color context.
(print_context_header): Likewise.
(pr_context_hunk): Likewise.
* src/diff.h (enum colors_style): New enum to record when to use colors.
(colors_style): New variable to memorize the argument value.
(set_add_color_context): Add function definition.
(set_delete_color_context): Likewise.
(set_header_color_context): Likewise.
(set_line_numbers_color_context): Likewise.
(reset_color_context): Likewise.
* src/diff.c: : Define COLOR_OPTION.
(specify_colors_style): New function.
(longopts): Add --color.
(main): Handle --color argument.
(option_help_msgid): Add usage string for --color.
* src/normal.c (print_normal_hunk): Set the color context.
* src/side.c (print_1sdiff_line): Likewise.
* src/util.c (colors_enabled): New boolean variable.
(begin_output): Call check_color_output once the output file is
configured.
(output_1_line): Periodically call `process_signals'.
(caught_signals): New sigset_t.
(colors_enabled): New boolean variable.
(interrupt_signal): New sig_atomic_t.
(stop_signal_count): New sig_atomic_t.
(check_color_output): New function.
(install_signal_handlers): Likewise. Copied from coreutils ls.
(process_signals): Likewise.  Copied from coreutils ls.
(reset_color_context): Likewise.
(set_add_color_context): Likewise.
(set_delete_color_context): Likewise.
(set_header_color_context): Likewise.
(set_line_numbers_color_context): Likewise.
(sighandler): Likewise.  Copied from coreutils ls.
(stophandler): Likewise.  Copied from coreutils ls.
---
 doc/diffutils.texi |  21 ++++
 src/context.c      |  51 +++++++--
 src/diff.c         |  27 ++++-
 src/diff.h         |  21 ++++
 src/normal.c       |  18 ++-
 src/side.c         |  15 +++
 src/util.c         | 316 +++++++++++++++++++++++++++++++++++++++++++++++------
 7 files changed, 421 insertions(+), 48 deletions(-)

diff --git a/doc/diffutils.texi b/doc/diffutils.texi
index 091257f..0944b44 100644
--- a/doc/diffutils.texi
+++ b/doc/diffutils.texi
@@ -3742,6 +3742,27 @@ Read and write data in binary mode.  @xref{Binary}.
 Use the context output format, showing three lines of context.
 @xref{Context Format}.
 
+@item --color [=@var{when}]
+@cindex color, distinguishing different context
+Specify whether to use color for distinguishing different contexts,
+like header, added or removed lines.  @var{when} may be omitted, or
+one of:
+@itemize @bullet
+@item none
+@vindex none @r{color option}
+Do not use color at all.  This is the default when no --color option
+is present.
+@item auto
+@vindex auto @r{color option}
+@cindex terminal, using color iff
+Only use color if standard output is a terminal.
+@item always
+@vindex always @r{color option}
+Always use color.
+@end itemize
+Specifying @option{--color} and no @var{when} is equivalent to
+@option{--color=auto}.
+
 @item -C @var{lines}
 @itemx --context@r{[}=@var{lines}@r{]}
 Use the context output format, showing @var{lines} (an integer) lines of
diff --git a/src/context.c b/src/context.c
index e0f21c4..8e9a74f 100644
--- a/src/context.c
+++ b/src/context.c
@@ -80,6 +80,7 @@ print_context_label (char const *mark,
 void
 print_context_header (struct file_data inf[], char const *const *names, bool unidiff)
 {
+  set_header_color_context ();
   if (unidiff)
     {
       print_context_label ("---", &inf[0], names[0], file_label[0]);
@@ -90,6 +91,7 @@ print_context_header (struct file_data inf[], char const *const *names, bool uni
       print_context_label ("***", &inf[0], names[0], file_label[0]);
       print_context_label ("---", &inf[1], names[1], file_label[1]);
     }
+  reset_color_context ();
 }
 
 /* Print an edit script in context format.  */
@@ -215,6 +217,7 @@ pr_context_hunk (struct change *hunk)
 
       for (i = first0; i <= last0; i++)
 	{
+          bool reset_context = false;
 	  /* Skip past changes that apply (in file 0)
 	     only to lines before line I.  */
 
@@ -225,12 +228,18 @@ pr_context_hunk (struct change *hunk)
 
 	  prefix = " ";
 	  if (next && next->line0 <= i)
-	    /* The change NEXT covers this line.
-	       If lines were inserted here in file 1, this is "changed".
-	       Otherwise it is "deleted".  */
-	    prefix = (next->inserted > 0 ? "!" : "-");
+            {
+              reset_context = true;
+              set_delete_color_context ();
+              /* The change NEXT covers this line.
+                 If lines were inserted here in file 1, this is "changed".
+                 Otherwise it is "deleted".  */
+              prefix = (next->inserted > 0 ? "!" : "-");
+            }
 
 	  print_1_line (prefix, &files[0].linbuf[i]);
+          if (reset_context)
+            reset_color_context ();
 	}
     }
 
@@ -244,6 +253,7 @@ pr_context_hunk (struct change *hunk)
 
       for (i = first1; i <= last1; i++)
 	{
+          bool reset_context = false;
 	  /* Skip past changes that apply (in file 1)
 	     only to lines before line I.  */
 
@@ -254,12 +264,17 @@ pr_context_hunk (struct change *hunk)
 
 	  prefix = " ";
 	  if (next && next->line1 <= i)
-	    /* The change NEXT covers this line.
-	       If lines were deleted here in file 0, this is "changed".
-	       Otherwise it is "inserted".  */
-	    prefix = (next->deleted > 0 ? "!" : "+");
-
+            {
+              reset_context = true;
+              set_add_color_context ();
+              /* The change NEXT covers this line.
+                 If lines were deleted here in file 0, this is "changed".
+                 Otherwise it is "inserted".  */
+              prefix = (next->deleted > 0 ? "!" : "+");
+            }
 	  print_1_line (prefix, &files[1].linbuf[i]);
+          if (reset_context)
+            reset_color_context ();
 	}
     }
 }
@@ -330,11 +345,13 @@ pr_unidiff_hunk (struct change *hunk)
   begin_output ();
   out = outfile;
 
+  set_line_numbers_color_context ();
   fputs ("@@ -", out);
   print_unidiff_number_range (&files[0], first0, last0);
   fputs (" +", out);
   print_unidiff_number_range (&files[1], first1, last1);
   fputs (" @@", out);
+  reset_color_context ();
 
   if (function)
     print_context_function (out, function);
@@ -360,9 +377,17 @@ pr_unidiff_hunk (struct change *hunk)
 	}
       else
 	{
+          bool reset_context = false;
+
 	  /* For each difference, first output the deleted part. */
 
 	  k = next->deleted;
+          if (k)
+            {
+              reset_context = true;
+              set_delete_color_context ();
+            }
+
 	  while (k--)
 	    {
 	      char const * const *line = &files[0].linbuf[i++];
@@ -375,9 +400,15 @@ pr_unidiff_hunk (struct change *hunk)
 	  /* Then output the inserted part. */
 
 	  k = next->inserted;
+          if (k)
+            {
+              reset_context = true;
+              set_add_color_context ();
+            }
 	  while (k--)
 	    {
 	      char const * const *line = &files[1].linbuf[j++];
+	      set_add_color_context ();
 	      putc ('+', out);
 	      if (initial_tab && ! (suppress_blank_empty && **line == '\n'))
 		putc ('\t', out);
@@ -386,6 +417,8 @@ pr_unidiff_hunk (struct change *hunk)
 
 	  /* We're done with this hunk, so on to the next! */
 
+          if (reset_context)
+            reset_color_context ();
 	  next = next->link;
 	}
     }
diff --git a/src/diff.c b/src/diff.c
index efd7e47..4e0f602 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -70,6 +70,7 @@ static void add_regexp (struct regexp_list *, char const *);
 static void summarize_regexp_list (struct regexp_list *);
 static void specify_style (enum output_style);
 static void specify_value (char const **, char const *, char const *);
+static void specify_colors_style (char const *);
 static void try_help (char const *, char const *) __attribute__((noreturn));
 static void check_stdout (void);
 static void usage (void);
@@ -136,7 +137,9 @@ enum
   UNCHANGED_GROUP_FORMAT_OPTION,
   OLD_GROUP_FORMAT_OPTION,
   NEW_GROUP_FORMAT_OPTION,
-  CHANGED_GROUP_FORMAT_OPTION
+  CHANGED_GROUP_FORMAT_OPTION,
+
+  COLOR_OPTION,
 };
 
 static char const group_format_option[][sizeof "--unchanged-group-format"] =
@@ -159,6 +162,7 @@ static struct option const longopts[] =
   {"binary", 0, 0, BINARY_OPTION},
   {"brief", 0, 0, 'q'},
   {"changed-group-format", 1, 0, CHANGED_GROUP_FORMAT_OPTION},
+  {"color", 2, 0, COLOR_OPTION},
   {"context", 2, 0, 'C'},
   {"ed", 0, 0, 'e'},
   {"exclude", 1, 0, 'x'},
@@ -627,6 +631,10 @@ main (int argc, char **argv)
 	  specify_value (&group_format[c], optarg, group_format_option[c]);
 	  break;
 
+	case COLOR_OPTION:
+	  specify_colors_style (optarg);
+	  break;
+
 	default:
 	  try_help (NULL, NULL);
 	}
@@ -940,6 +948,8 @@ static char const * const option_help_msgid[] = {
   N_("-d, --minimal            try hard to find a smaller set of changes"),
   N_("    --horizon-lines=NUM  keep NUM lines of the common prefix and suffix"),
   N_("    --speed-large-files  assume large files and many scattered small changes"),
+  N_("    --color[=WHEN]         colorize the output; WHEN can be 'never', 'always',"),
+  N_("                             or 'auto' (the default)"),
   "",
   N_("    --help               display this help and exit"),
   N_("-v, --version            output version information and exit"),
@@ -1008,6 +1018,21 @@ specify_style (enum output_style style)
       output_style = style;
     }
 }
+
+/* Set the color mode.  */
+static void
+specify_colors_style (char const *value)
+{
+  if (value == NULL || STREQ (value, "auto"))
+    colors_style = AUTO;
+  else if (STREQ (value, "always"))
+    colors_style = ALWAYS;
+  else if (STREQ (value, "never"))
+    colors_style = NEVER;
+  else
+    try_help ("invalid color '%s'", value);
+}
+
 
 /* Set the last-modified time of *ST to be the current time.  */
 
diff --git a/src/diff.h b/src/diff.h
index 465e4bc..472fa93 100644
--- a/src/diff.h
+++ b/src/diff.h
@@ -38,6 +38,19 @@ enum changes
   /* Both deletes and inserts: a hunk containing both old and new lines.  */
   CHANGED
 };
+
+/* What kind of changes a hunk contains.  */
+enum colors_style
+{
+  /* Never output colors.  */
+  NEVER,
+
+  /* Output colors if the output is a terminal.  */
+  AUTO,
+
+  /* Always output colors.  */
+  ALWAYS,
+};
 
 /* Variables for command line options */
 
@@ -83,6 +96,9 @@ enum output_style
 
 XTERN enum output_style output_style;
 
+/* Define the current color context used to print a line.  */
+XTERN enum colors_style colors_style;
+
 /* Nonzero if output cannot be generated for identical files.  */
 XTERN bool no_diff_means_no_output;
 
@@ -390,3 +406,8 @@ extern void print_script (struct change *, struct change * (*) (struct change *)
 extern void setup_output (char const *, char const *, bool);
 extern void translate_range (struct file_data const *, lin, lin,
                              long int *, long int *);
+extern void set_header_color_context (void);
+extern void set_add_color_context (void);
+extern void set_delete_color_context (void);
+extern void reset_color_context (void);
+extern void set_line_numbers_color_context (void);
diff --git a/src/normal.c b/src/normal.c
index 721fd1a..227af10 100644
--- a/src/normal.c
+++ b/src/normal.c
@@ -49,21 +49,31 @@ print_normal_hunk (struct change *hunk)
   begin_output ();
 
   /* Print out the line number header for this hunk */
+  set_line_numbers_color_context ();
   print_number_range (',', &files[0], first0, last0);
   fputc (change_letter[changes], outfile);
   print_number_range (',', &files[1], first1, last1);
   fputc ('\n', outfile);
+  reset_color_context ();
 
   /* Print the lines that the first file has.  */
   if (changes & OLD)
-    for (i = first0; i <= last0; i++)
-      print_1_line ("<", &files[0].linbuf[i]);
+    {
+      set_delete_color_context ();
+      for (i = first0; i <= last0; i++)
+        print_1_line ("<", &files[0].linbuf[i]);
+      reset_color_context ();
+    }
 
   if (changes == CHANGED)
     fputs ("---\n", outfile);
 
   /* Print the lines that the second file has.  */
   if (changes & NEW)
-    for (i = first1; i <= last1; i++)
-      print_1_line (">", &files[1].linbuf[i]);
+    {
+      set_add_color_context ();
+      for (i = first1; i <= last1; i++)
+        print_1_line (">", &files[1].linbuf[i]);
+      reset_color_context ();
+    }
 }
diff --git a/src/side.c b/src/side.c
index 155512c..b762d31 100644
--- a/src/side.c
+++ b/src/side.c
@@ -206,6 +206,18 @@ print_1sdiff_line (char const *const *left, char sep,
   size_t c2o = sdiff_column2_offset;
   size_t col = 0;
   bool put_newline = false;
+  bool color_to_reset = false;
+
+  if (sep == '<')
+    {
+      set_delete_color_context ();
+      color_to_reset = true;
+    }
+  else if (sep == '>')
+    {
+      set_add_color_context ();
+      color_to_reset = true;
+    }
 
   if (left)
     {
@@ -233,6 +245,9 @@ print_1sdiff_line (char const *const *left, char sep,
 
   if (put_newline)
     putc ('\n', out);
+
+  if (color_to_reset)
+    reset_color_context ();
 }
 
 /* Print lines common to both files in side-by-side format.  */
diff --git a/src/util.c b/src/util.c
index 2d6d3fc..6cc1411 100644
--- a/src/util.c
+++ b/src/util.c
@@ -24,6 +24,22 @@
 #include <system-quote.h>
 #include <xalloc.h>
 #include "xvasprintf.h"
+#include <signal.h>
+
+/* Use SA_NOCLDSTOP as a proxy for whether the sigaction machinery is
+   present.  */
+#ifndef SA_NOCLDSTOP
+# define SA_NOCLDSTOP 0
+# define sigprocmask(How, Set, Oset) /* empty */
+# define sigset_t int
+# if ! HAVE_SIGINTERRUPT
+#  define siginterrupt(sig, flag) /* empty */
+# endif
+#endif
+
+#ifndef SA_RESTART
+# define SA_RESTART 0
+#endif
 
 char const pr_program[] = PR_PROGRAM;
 
@@ -143,6 +159,174 @@ print_message_queue (void)
     }
 }
 
+/* The set of signals that are caught.  */
+
+static sigset_t caught_signals;
+
+/* If nonzero, the value of the pending fatal signal.  */
+
+static sig_atomic_t volatile interrupt_signal;
+
+/* A count of the number of pending stop signals that have been received.  */
+
+static sig_atomic_t volatile stop_signal_count;
+
+/* An ordinary signal was received; arrange for the program to exit.  */
+
+static void
+sighandler (int sig)
+{
+  if (! SA_NOCLDSTOP)
+    signal (sig, SIG_IGN);
+  if (! interrupt_signal)
+    interrupt_signal = sig;
+}
+
+/* A SIGTSTP was received; arrange for the program to suspend itself.  */
+
+static void
+stophandler (int sig)
+{
+  if (! SA_NOCLDSTOP)
+    signal (sig, stophandler);
+  if (! interrupt_signal)
+    stop_signal_count++;
+}
+/* Process any pending signals.  If signals are caught, this function
+   should be called periodically.  Ideally there should never be an
+   unbounded amount of time when signals are not being processed.
+   Signal handling can restore the default colors, so callers must
+   immediately change colors after invoking this function.  */
+
+static void
+process_signals (void)
+{
+  while (interrupt_signal || stop_signal_count)
+    {
+      int sig;
+      int stops;
+      sigset_t oldset;
+
+      reset_color_context ();
+      fflush (stdout);
+
+      sigprocmask (SIG_BLOCK, &caught_signals, &oldset);
+
+      /* Reload interrupt_signal and stop_signal_count, in case a new
+         signal was handled before sigprocmask took effect.  */
+      sig = interrupt_signal;
+      stops = stop_signal_count;
+
+      /* SIGTSTP is special, since the application can receive that signal
+         more than once.  In this case, don't set the signal handler to the
+         default.  Instead, just raise the uncatchable SIGSTOP.  */
+      if (stops)
+        {
+          stop_signal_count = stops - 1;
+          sig = SIGSTOP;
+        }
+      else
+        signal (sig, SIG_DFL);
+
+      /* Exit or suspend the program.  */
+      raise (sig);
+      sigprocmask (SIG_SETMASK, &oldset, NULL);
+
+      /* If execution reaches here, then the program has been
+         continued (after being suspended).  */
+    }
+}
+
+static void
+install_signal_handlers (void)
+{
+  /* The signals that are trapped, and the number of such signals.  */
+  static int const sig[] =
+    {
+      /* This one is handled specially.  */
+      SIGTSTP,
+
+      /* The usual suspects.  */
+      SIGALRM, SIGHUP, SIGINT, SIGPIPE, SIGQUIT, SIGTERM,
+#ifdef SIGPOLL
+      SIGPOLL,
+#endif
+#ifdef SIGPROF
+      SIGPROF,
+#endif
+#ifdef SIGVTALRM
+      SIGVTALRM,
+#endif
+#ifdef SIGXCPU
+      SIGXCPU,
+#endif
+#ifdef SIGXFSZ
+      SIGXFSZ,
+#endif
+    };
+  enum { nsigs = sizeof (sig) / sizeof *(sig) };
+
+#if ! SA_NOCLDSTOP
+  bool caught_sig[nsigs];
+#endif
+  {
+    int j;
+#if SA_NOCLDSTOP
+    struct sigaction act;
+
+    sigemptyset (&caught_signals);
+    for (j = 0; j < nsigs; j++)
+      {
+        sigaction (sig[j], NULL, &act);
+        if (act.sa_handler != SIG_IGN)
+          sigaddset (&caught_signals, sig[j]);
+      }
+
+    act.sa_mask = caught_signals;
+    act.sa_flags = SA_RESTART;
+
+    for (j = 0; j < nsigs; j++)
+      if (sigismember (&caught_signals, sig[j]))
+        {
+          act.sa_handler = sig[j] == SIGTSTP ? stophandler : sighandler;
+          sigaction (sig[j], &act, NULL);
+        }
+#else
+    for (j = 0; j < nsigs; j++)
+      {
+        caught_sig[j] = (signal (sig[j], SIG_IGN) != SIG_IGN);
+        if (caught_sig[j])
+          {
+            signal (sig[j], sig[j] == SIGTSTP ? stophandler : sighandler);
+            siginterrupt (sig[j], 0);
+          }
+      }
+#endif
+    }
+}
+
+static char const *current_name0;
+static char const *current_name1;
+static bool currently_recursive;
+static bool colors_enabled;
+
+static void
+check_color_output (bool is_pipe)
+{
+  bool output_is_tty;
+
+  if (! outfile || colors_style == NEVER)
+    return;
+
+  output_is_tty = !is_pipe && isatty (fileno (outfile));
+
+  colors_enabled = (colors_style == ALWAYS
+                    || (colors_style == AUTO && output_is_tty));
+
+  if (output_is_tty)
+    install_signal_handlers ();
+}
+
 /* Call before outputting the results of comparing files NAME0 and NAME1
    to set up OUTFILE, the stdio stream for the output to go to.
 
@@ -150,10 +334,6 @@ print_message_queue (void)
    we fork off a 'pr' and make OUTFILE a pipe to it.
    'pr' then outputs to our stdout.  */
 
-static char const *current_name0;
-static char const *current_name1;
-static bool currently_recursive;
-
 void
 setup_output (char const *name0, char const *name1, bool recursive)
 {
@@ -313,6 +493,7 @@ begin_output (void)
 	    outfile = fdopen (pipes[1], "w");
 	    if (!outfile)
 	      pfatal_with_name ("fdopen");
+	    check_color_output (true);
 	  }
 #else
 	char *command = system_quote_argv (SCI_SYSTEM, (char **) argv);
@@ -320,6 +501,7 @@ begin_output (void)
 	outfile = popen (command, "w");
 	if (!outfile)
 	  pfatal_with_name (command);
+	check_color_output (true);
 	free (command);
 #endif
       }
@@ -330,6 +512,7 @@ begin_output (void)
       /* If -l was not specified, output the diff straight to 'stdout'.  */
 
       outfile = stdout;
+      check_color_output (false);
 
       /* If handling multiple files (because scanning a directory),
 	 print which files the following output is about.  */
@@ -672,8 +855,21 @@ void
 output_1_line (char const *base, char const *limit, char const *flag_format,
 	       char const *line_flag)
 {
+  const size_t MAX_CHUNK = 1024;
   if (!expand_tabs)
-    fwrite (base, sizeof (char), limit - base, outfile);
+    {
+      size_t left = limit - base;
+      while (left)
+        {
+          size_t to_write = MIN (left, MAX_CHUNK);
+          size_t written = fwrite (base, sizeof (char), to_write, outfile);
+          if (written < to_write)
+            return;
+          base += written;
+          left -= written;
+          process_signals ();
+        }
+    }
   else
     {
       register FILE *out = outfile;
@@ -681,40 +877,92 @@ output_1_line (char const *base, char const *limit, char const *flag_format,
       register char const *t = base;
       register size_t column = 0;
       size_t tab_size = tabsize;
+      size_t counter_proc_signals = 0;
 
       while (t < limit)
-	switch ((c = *t++))
-	  {
-	  case '\t':
-	    {
-	      size_t spaces = tab_size - column % tab_size;
-	      column += spaces;
-	      do
-		putc (' ', out);
-	      while (--spaces);
-	    }
-	    break;
+        {
+          counter_proc_signals++;
+          if (counter_proc_signals == MAX_CHUNK)
+            {
+              process_signals ();
+              counter_proc_signals = 0;
+            }
+
+          switch ((c = *t++))
+            {
+            case '\t':
+              {
+                size_t spaces = tab_size - column % tab_size;
+                column += spaces;
+                do
+                  putc (' ', out);
+                while (--spaces);
+              }
+              break;
+
+            case '\r':
+              putc (c, out);
+              if (flag_format && t < limit && *t != '\n')
+                fprintf (out, flag_format, line_flag);
+              column = 0;
+              break;
+
+            case '\b':
+              if (column == 0)
+                continue;
+              column--;
+              putc (c, out);
+              break;
+
+            default:
+              column += isprint (c) != 0;
+              putc (c, out);
+              break;
+            }
+        }
+    }
+}
 
-	  case '\r':
-	    putc (c, out);
-	    if (flag_format && t < limit && *t != '\n')
-	      fprintf (out, flag_format, line_flag);
-	    column = 0;
-	    break;
+void
+set_header_color_context (void)
+{
+  process_signals ();
+  if (colors_enabled)
+    fputs ("\x1B[1m", outfile);
+}
 
-	  case '\b':
-	    if (column == 0)
-	      continue;
-	    column--;
-	    putc (c, out);
-	    break;
+void
+set_line_numbers_color_context (void)
+{
+  process_signals ();
+  if (colors_enabled)
+    fputs ("\x1B[36m", outfile);
+}
 
-	  default:
-	    column += isprint (c) != 0;
-	    putc (c, out);
-	    break;
-	  }
-    }
+void
+set_add_color_context (void)
+{
+  process_signals ();
+  if (colors_enabled)
+    fputs ("\x1B[32m", outfile);
+}
+
+void
+set_delete_color_context (void)
+{
+  process_signals ();
+  if (colors_enabled)
+    fputs ("\x1B[31m", outfile);
+}
+
+void
+reset_color_context (void)
+{
+  static char const reset_sequence[] = "\x1b[0m";
+  if (! colors_enabled)
+    return;
+
+  fputs (reset_sequence, outfile);
 }
 
 char const change_letter[] = { 0, 'd', 'a', 'c' };
-- 
2.5.0

>From 0bbbbc9ce6a2b68e2ff20a1a8b4ed69a7e43619b Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <[email-masked]>
Date: Mon, 19 Oct 2015 10:29:41 +0200
Subject: [PATCH 2/6] diff: add --palette

* doc/diffutils.texi: Add documentation for --palette
* src/diff.h (set_color_palette): New prototype.
* src/diff.c (set_color_palette): New function.
(color_palette): New variable.
* src/utils.c (struct bin_str): New struct.
(struct color_ext_type): New struct.
(color_indicator): New array.
(indicator_name): New array.
(indicator_no): New enum.
(parse_state): New enum.
(put_indicator): New function.
(get_funky_string): New function. Copied from coreutils ls.
(parse_diff_color):  New function. Copied from coreutils ls
"parse_ls_color" function.
(set_header_color_context): Use put_indicator instead of directly
outputting the sequence.
(set_line_numbers_colors_context): Likewise.
(set_add_color_context): Likewise.
(set_delete_color_context): Likewise.
(reset_color_context): Likewise.
---
 doc/diffutils.texi |  34 +++++
 src/diff.c         |   8 +
 src/diff.h         |   1 +
 src/util.c         | 439 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 473 insertions(+), 9 deletions(-)

diff --git a/doc/diffutils.texi b/doc/diffutils.texi
index 0944b44..39ba35d 100644
--- a/doc/diffutils.texi
+++ b/doc/diffutils.texi
@@ -3890,6 +3890,40 @@ if-then-else format.  @xref{Line Formats}.
 @itemx --show-c-function
 Show which C function each change is in.  @xref{C Function Headings}.
 
+@item --palette=@var{palette}
+Specify what color palette to use when colored output is enabled.  It
+defaults to @samp{rs=0:hd=1:ad=32:de=31:ln=36} for red deleted lines,
+green added lines, cyan line numbers, bold header.
+
+Supported capabilities are as follows.
+
+@table @code
+@item ad=32
+@vindex ad @r{capability}
+
+SGR substring for added lines.
+The default is green foreground.
+
+@item de=31
+@vindex de @r{capability}
+
+SGR substring for deleted lines.
+The default is red foreground.
+
+@item hd=1
+@vindex hd @r{capability}
+
+SGR substring for chunk header.
+The default is bold foreground.
+
+@item ln=36
+@vindex ln @r{capability}
+
+SGR substring for line numbers.
+The default is cyan foreground.
+@end table
+
+
 @item -q
 @itemx --brief
 Report only whether the files differ, not the details of the
diff --git a/src/diff.c b/src/diff.c
index 4e0f602..4c3d29a 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -140,6 +140,7 @@ enum
   CHANGED_GROUP_FORMAT_OPTION,
 
   COLOR_OPTION,
+  COLOR_PALETTE_OPTION,
 };
 
 static char const group_format_option[][sizeof "--unchanged-group-format"] =
@@ -196,6 +197,7 @@ static struct option const longopts[] =
   {"old-group-format", 1, 0, OLD_GROUP_FORMAT_OPTION},
   {"old-line-format", 1, 0, OLD_LINE_FORMAT_OPTION},
   {"paginate", 0, 0, 'l'},
+  {"palette", 1, 0, COLOR_PALETTE_OPTION},
   {"rcs", 0, 0, 'n'},
   {"recursive", 0, 0, 'r'},
   {"report-identical-files", 0, 0, 's'},
@@ -635,6 +637,10 @@ main (int argc, char **argv)
 	  specify_colors_style (optarg);
 	  break;
 
+	case COLOR_PALETTE_OPTION:
+	  set_color_palette (optarg);
+	  break;
+
 	default:
 	  try_help (NULL, NULL);
 	}
@@ -950,6 +956,8 @@ static char const * const option_help_msgid[] = {
   N_("    --speed-large-files  assume large files and many scattered small changes"),
   N_("    --color[=WHEN]         colorize the output; WHEN can be 'never', 'always',"),
   N_("                             or 'auto' (the default)"),
+  N_("    --palette=PALETTE    specify the colors to use when --color is active"),
+  N_("                         PALETTE is a colon-separated list terminfo capabilities"),
   "",
   N_("    --help               display this help and exit"),
   N_("-v, --version            output version information and exit"),
diff --git a/src/diff.h b/src/diff.h
index 472fa93..5930cd1 100644
--- a/src/diff.h
+++ b/src/diff.h
@@ -411,3 +411,4 @@ extern void set_add_color_context (void);
 extern void set_delete_color_context (void);
 extern void reset_color_context (void);
 extern void set_line_numbers_color_context (void);
+extern void set_color_palette (const char *palette);
diff --git a/src/util.c b/src/util.c
index 6cc1411..dedf3b3 100644
--- a/src/util.c
+++ b/src/util.c
@@ -310,6 +310,396 @@ static char const *current_name1;
 static bool currently_recursive;
 static bool colors_enabled;
 
+static struct color_ext_type *color_ext_list = NULL;
+
+struct bin_str
+  {
+    size_t len;			/* Number of bytes */
+    const char *string;		/* Pointer to the same */
+  };
+
+struct color_ext_type
+  {
+    struct bin_str ext;		/* The extension we're looking for */
+    struct bin_str seq;		/* The sequence to output when we do */
+    struct color_ext_type *next;	/* Next in list */
+  };
+
+/* Parse a string as part of the --palette argument; this may involve
+   decoding all kinds of escape characters.  If equals_end is set an
+   unescaped equal sign ends the string, otherwise only a : or \0
+   does.  Set *OUTPUT_COUNT to the number of bytes output.  Return
+   true if successful.
+
+   The resulting string is *not* null-terminated, but may contain
+   embedded nulls.
+
+   Note that both dest and src are char **; on return they point to
+   the first free byte after the array and the character that ended
+   the input string, respectively.  */
+
+static bool
+get_funky_string (char **dest, const char **src, bool equals_end,
+                  size_t *output_count)
+{
+  char num;			/* For numerical codes */
+  size_t count;			/* Something to count with */
+  enum {
+    ST_GND, ST_BACKSLASH, ST_OCTAL, ST_HEX, ST_CARET, ST_END, ST_ERROR
+  } state;
+  const char *p;
+  char *q;
+
+  p = *src;			/* We don't want to double-indirect */
+  q = *dest;			/* the whole darn time.  */
+
+  count = 0;			/* No characters counted in yet.  */
+  num = 0;
+
+  state = ST_GND;		/* Start in ground state.  */
+  while (state < ST_END)
+    {
+      switch (state)
+        {
+        case ST_GND:		/* Ground state (no escapes) */
+          switch (*p)
+            {
+            case ':':
+            case '\0':
+              state = ST_END;	/* End of string */
+              break;
+            case '\\':
+              state = ST_BACKSLASH; /* Backslash scape sequence */
+              ++p;
+              break;
+            case '^':
+              state = ST_CARET; /* Caret escape */
+              ++p;
+              break;
+            case '=':
+              if (equals_end)
+                {
+                  state = ST_END; /* End */
+                  break;
+                }
+              /* else fall through */
+            default:
+              *(q++) = *(p++);
+              ++count;
+              break;
+            }
+          break;
+
+        case ST_BACKSLASH:	/* Backslash escaped character */
+          switch (*p)
+            {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+              state = ST_OCTAL;	/* Octal sequence */
+              num = *p - '0';
+              break;
+            case 'x':
+            case 'X':
+              state = ST_HEX;	/* Hex sequence */
+              num = 0;
+              break;
+            case 'a':		/* Bell */
+              num = '\a';
+              break;
+            case 'b':		/* Backspace */
+              num = '\b';
+              break;
+            case 'e':		/* Escape */
+              num = 27;
+              break;
+            case 'f':		/* Form feed */
+              num = '\f';
+              break;
+            case 'n':		/* Newline */
+              num = '\n';
+              break;
+            case 'r':		/* Carriage return */
+              num = '\r';
+              break;
+            case 't':		/* Tab */
+              num = '\t';
+              break;
+            case 'v':		/* Vtab */
+              num = '\v';
+              break;
+            case '?':		/* Delete */
+              num = 127;
+              break;
+            case '_':		/* Space */
+              num = ' ';
+              break;
+            case '\0':		/* End of string */
+              state = ST_ERROR;	/* Error! */
+              break;
+            default:		/* Escaped character like \ ^ : = */
+              num = *p;
+              break;
+            }
+          if (state == ST_BACKSLASH)
+            {
+              *(q++) = num;
+              ++count;
+              state = ST_GND;
+            }
+          ++p;
+          break;
+
+        case ST_OCTAL:		/* Octal sequence */
+          if (*p < '0' || *p > '7')
+            {
+              *(q++) = num;
+              ++count;
+              state = ST_GND;
+            }
+          else
+            num = (num << 3) + (*(p++) - '0');
+          break;
+
+        case ST_HEX:		/* Hex sequence */
+          switch (*p)
+            {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+              num = (num << 4) + (*(p++) - '0');
+              break;
+            case 'a':
+            case 'b':
+            case 'c':
+            case 'd':
+            case 'e':
+            case 'f':
+              num = (num << 4) + (*(p++) - 'a') + 10;
+              break;
+            case 'A':
+            case 'B':
+            case 'C':
+            case 'D':
+            case 'E':
+            case 'F':
+              num = (num << 4) + (*(p++) - 'A') + 10;
+              break;
+            default:
+              *(q++) = num;
+              ++count;
+              state = ST_GND;
+              break;
+            }
+          break;
+
+        case ST_CARET:		/* Caret escape */
+          state = ST_GND;	/* Should be the next state... */
+          if (*p >= '@' && *p <= '~')
+            {
+              *(q++) = *(p++) & 037;
+              ++count;
+            }
+          else if (*p == '?')
+            {
+              *(q++) = 127;
+              ++count;
+            }
+          else
+            state = ST_ERROR;
+          break;
+
+        default:
+          abort ();
+        }
+    }
+
+  *dest = q;
+  *src = p;
+  *output_count = count;
+
+  return state != ST_ERROR;
+}
+
+enum parse_state
+  {
+    PS_START = 1,
+    PS_2,
+    PS_3,
+    PS_4,
+    PS_DONE,
+    PS_FAIL
+  };
+
+#define LEN_STR_PAIR(s) sizeof (s) - 1, s
+
+static struct bin_str color_indicator[] =
+  {
+    { LEN_STR_PAIR ("\033[") },		/* lc: Left of color sequence */
+    { LEN_STR_PAIR ("m") },		/* rc: Right of color sequence */
+    { 0, NULL },			/* ec: End color (replaces lc+rs+rc) */
+    { LEN_STR_PAIR ("0") },		/* rs: Reset to ordinary colors */
+    { LEN_STR_PAIR ("1") },		/* hd: Header */
+    { LEN_STR_PAIR ("32") },		/* ad: Add line */
+    { LEN_STR_PAIR ("31") },		/* de: Delete line */
+    { LEN_STR_PAIR ("36") },		/* ln: Line number */
+  };
+
+static const char *const indicator_name[]=
+  {
+    "lc", "rc", "ec", "rs", "hd", "ad", "de", "ln", NULL
+  };
+
+static const char *color_palette;
+
+void
+set_color_palette (const char *palette)
+{
+  color_palette = palette;
+}
+
+static void
+parse_diff_color (void)
+{
+  char *color_buf;
+  const char *p;		/* Pointer to character being parsed */
+  char *buf;			/* color_buf buffer pointer */
+  int ind_no;			/* Indicator number */
+  char label[3];		/* Indicator label */
+  struct color_ext_type *ext;	/* Extension we are working on */
+
+  if ((p = color_palette) == NULL || *p == '\0')
+    return;
+
+  ext = NULL;
+  strcpy (label, "??");
+
+  /* This is an overly conservative estimate, but any possible
+     --palette string will *not* generate a color_buf longer than
+     itself, so it is a safe way of allocating a buffer in
+     advance.  */
+  buf = color_buf = xstrdup (p);
+
+  enum parse_state state = PS_START;
+  while (true)
+    {
+      switch (state)
+        {
+        case PS_START:		/* First label character */
+          switch (*p)
+            {
+            case ':':
+              ++p;
+              break;
+
+            case '*':
+              /* Allocate new extension block and add to head of
+                 linked list (this way a later definition will
+                 override an earlier one, which can be useful for
+                 having terminal-specific defs override global).  */
+
+              ext = xmalloc (sizeof *ext);
+              ext->next = color_ext_list;
+              color_ext_list = ext;
+
+              ++p;
+              ext->ext.string = buf;
+
+              state = (get_funky_string (&buf, &p, true, &ext->ext.len)
+                       ? PS_4 : PS_FAIL);
+              break;
+
+            case '\0':
+              state = PS_DONE;	/* Done! */
+              goto done;
+
+            default:	/* Assume it is file type label */
+              label[0] = *(p++);
+              state = PS_2;
+              break;
+            }
+          break;
+
+        case PS_2:		/* Second label character */
+          if (*p)
+            {
+              label[1] = *(p++);
+              state = PS_3;
+            }
+          else
+            state = PS_FAIL;	/* Error */
+          break;
+
+        case PS_3:		/* Equal sign after indicator label */
+          state = PS_FAIL;	/* Assume failure...  */
+          if (*(p++) == '=')/* It *should* be...  */
+            {
+              for (ind_no = 0; indicator_name[ind_no] != NULL; ++ind_no)
+                {
+                  if (STREQ (label, indicator_name[ind_no]))
+                    {
+                      color_indicator[ind_no].string = buf;
+                      state = (get_funky_string (&buf, &p, false,
+                                                 &color_indicator[ind_no].len)
+                               ? PS_START : PS_FAIL);
+                      break;
+                    }
+                }
+              if (state == PS_FAIL)
+                error (0, 0, _("unrecognized prefix: %s"), label);
+            }
+          break;
+
+        case PS_4:		/* Equal sign after *.ext */
+          if (*(p++) == '=')
+            {
+              ext->seq.string = buf;
+              state = (get_funky_string (&buf, &p, false, &ext->seq.len)
+                       ? PS_START : PS_FAIL);
+            }
+          else
+            state = PS_FAIL;
+          break;
+
+        case PS_FAIL:
+          goto done;
+
+        default:
+          abort ();
+        }
+    }
+ done:
+
+  if (state == PS_FAIL)
+    {
+      struct color_ext_type *e;
+      struct color_ext_type *e2;
+
+      error (0, 0,
+             _("unparsable value for --palette"));
+      free (color_buf);
+      for (e = color_ext_list; e != NULL; /* empty */)
+        {
+          e2 = e;
+          e = e->next;
+          free (e2);
+        }
+      colors_enabled = false;
+    }
+}
+
 static void
 check_color_output (bool is_pipe)
 {
@@ -323,6 +713,9 @@ check_color_output (bool is_pipe)
   colors_enabled = (colors_style == ALWAYS
                     || (colors_style == AUTO && output_is_tty));
 
+  if (colors_enabled)
+    parse_diff_color ();
+
   if (output_is_tty)
     install_signal_handlers ();
 }
@@ -923,12 +1316,27 @@ output_1_line (char const *base, char const *limit, char const *flag_format,
     }
 }
 
+enum indicator_no
+  {
+    C_LEFT, C_RIGHT, C_END, C_RESET, C_HEADER, C_ADD, C_DELETE, C_LINE
+  };
+
+static void
+put_indicator (const struct bin_str *ind)
+{
+  fwrite (ind->string, ind->len, 1, outfile);
+}
+
 void
 set_header_color_context (void)
 {
   process_signals ();
   if (colors_enabled)
-    fputs ("\x1B[1m", outfile);
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_HEADER]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 void
@@ -936,7 +1344,11 @@ set_line_numbers_color_context (void)
 {
   process_signals ();
   if (colors_enabled)
-    fputs ("\x1B[36m", outfile);
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_LINE]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 void
@@ -944,7 +1356,11 @@ set_add_color_context (void)
 {
   process_signals ();
   if (colors_enabled)
-    fputs ("\x1B[32m", outfile);
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_ADD]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 void
@@ -952,17 +1368,22 @@ set_delete_color_context (void)
 {
   process_signals ();
   if (colors_enabled)
-    fputs ("\x1B[31m", outfile);
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_DELETE]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 void
 reset_color_context (void)
 {
-  static char const reset_sequence[] = "\x1b[0m";
-  if (! colors_enabled)
-    return;
-
-  fputs (reset_sequence, outfile);
+  if (colors_enabled)
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_RESET]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 char const change_letter[] = { 0, 'd', 'a', 'c' };
-- 
2.5.0

>From 88b6384539d6d6e5c74e07efa55dbc6f4b12de6d Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <[email-masked]>
Date: Mon, 2 Nov 2015 19:03:32 +0000
Subject: [PATCH 3/6] doc: mention --color and --palette in NEWS

---
 NEWS | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/NEWS b/NEWS
index 7cdfedd..088f13b 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,13 @@ GNU diffutils NEWS                                    -*- outline -*-
 
 * Noteworthy changes in release ?.? (????-??-??) [?]
 
+** New features
+
+   diff accepts two new options --color and --palette to generate
+   and configure colored output.  --color takes an optional argument
+   specifying when to colorize a line: --color=always, --color=auto,
+   --color=never.  --palette is used to configure which colors are used.
+
 ** Bug fixes
 
   When binary files differ, diff now exits with status 1 as POSIX requires.
-- 
2.5.0

>From 49d2d3887f3358874a9e548209b0bd83ec347234 Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <[email-masked]>
Date: Fri, 27 Nov 2015 11:43:58 +0000
Subject: [PATCH 4/6] fixup! diff: add support for --color

---
 src/context.c |  5 ++++-
 src/util.c    | 17 ++++++++++++-----
 2 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/src/context.c b/src/context.c
index 8e9a74f..216f547 100644
--- a/src/context.c
+++ b/src/context.c
@@ -207,9 +207,11 @@ pr_context_hunk (struct change *hunk)
   if (function)
     print_context_function (out, function);
 
+  set_line_numbers_color_context ();
   fputs ("\n*** ", out);
   print_context_number_range (&files[0], first0, last0);
   fputs (" ****\n", out);
+  reset_color_context ();
 
   if (changes & OLD)
     {
@@ -243,9 +245,11 @@ pr_context_hunk (struct change *hunk)
 	}
     }
 
+  set_line_numbers_color_context ();
   fputs ("--- ", out);
   print_context_number_range (&files[1], first1, last1);
   fputs (" ----\n", out);
+  reset_color_context ();
 
   if (changes & NEW)
     {
@@ -408,7 +412,6 @@ pr_unidiff_hunk (struct change *hunk)
 	  while (k--)
 	    {
 	      char const * const *line = &files[1].linbuf[j++];
-	      set_add_color_context ();
 	      putc ('+', out);
 	      if (initial_tab && ! (suppress_blank_empty && **line == '\n'))
 		putc ('\t', out);
diff --git a/src/util.c b/src/util.c
index dedf3b3..b0e277b 100644
--- a/src/util.c
+++ b/src/util.c
@@ -1327,15 +1327,18 @@ put_indicator (const struct bin_str *ind)
   fwrite (ind->string, ind->len, 1, outfile);
 }
 
+static enum indicator_no last_context = C_RESET;
+
 void
 set_header_color_context (void)
 {
   process_signals ();
-  if (colors_enabled)
+  if (colors_enabled && last_context != C_HEADER)
     {
       put_indicator (&color_indicator[C_LEFT]);
       put_indicator (&color_indicator[C_HEADER]);
       put_indicator (&color_indicator[C_RIGHT]);
+      last_context = C_HEADER;
     }
 }
 
@@ -1343,11 +1346,12 @@ void
 set_line_numbers_color_context (void)
 {
   process_signals ();
-  if (colors_enabled)
+  if (colors_enabled && last_context != C_LINE)
     {
       put_indicator (&color_indicator[C_LEFT]);
       put_indicator (&color_indicator[C_LINE]);
       put_indicator (&color_indicator[C_RIGHT]);
+      last_context = C_LINE;
     }
 }
 
@@ -1355,11 +1359,12 @@ void
 set_add_color_context (void)
 {
   process_signals ();
-  if (colors_enabled)
+  if (colors_enabled && last_context != C_ADD)
     {
       put_indicator (&color_indicator[C_LEFT]);
       put_indicator (&color_indicator[C_ADD]);
       put_indicator (&color_indicator[C_RIGHT]);
+      last_context = C_ADD;
     }
 }
 
@@ -1367,22 +1372,24 @@ void
 set_delete_color_context (void)
 {
   process_signals ();
-  if (colors_enabled)
+  if (colors_enabled && last_context != C_DELETE)
     {
       put_indicator (&color_indicator[C_LEFT]);
       put_indicator (&color_indicator[C_DELETE]);
       put_indicator (&color_indicator[C_RIGHT]);
+      last_context = C_DELETE;
     }
 }
 
 void
 reset_color_context (void)
 {
-  if (colors_enabled)
+  if (colors_enabled && last_context != C_RESET)
     {
       put_indicator (&color_indicator[C_LEFT]);
       put_indicator (&color_indicator[C_RESET]);
       put_indicator (&color_indicator[C_RIGHT]);
+      last_context = C_RESET;
     }
 }
 
-- 
2.5.0

>From a2b6721675cb4d90fc0322a5b2fc9d5c1d4c35df Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <[email-masked]>
Date: Mon, 2 Nov 2015 19:05:10 +0000
Subject: [PATCH 5/6] tests: Add tests for --color and --palette

* tests/colors: New file.
* tests/Makefile.am (TESTS): Add colors.
---
 tests/Makefile.am |   3 +-
 tests/colors      | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 121 insertions(+), 1 deletion(-)
 create mode 100755 tests/colors

diff --git a/tests/Makefile.am b/tests/Makefile.am
index 438fbdf..805ccc2 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -15,7 +15,8 @@ TESTS = \
   no-newline-at-eof \
   stdin \
   strcoll-0-names \
-  filename-quoting
+  filename-quoting \
+  colors
 
 EXTRA_DIST = \
   $(TESTS) init.sh t-local.sh
diff --git a/tests/colors b/tests/colors
new file mode 100755
index 0000000..5a62232
--- /dev/null
+++ b/tests/colors
@@ -0,0 +1,119 @@
+#!/bin/sh
+
+. "${srcdir=.}/init.sh"; path_prepend_ ../src
+
+TZ=UTC0
+export TZ
+
+fail=0
+
+echo a > a
+echo b > b
+
+epoch='1970-01-01 00:00:00'
+touch --date="$epoch" a b
+
+gen_exp_u()
+{
+    local tab=$(printf '\t')
+    local epoch_plus="$epoch.000000000 +0000"
+    local rs=$(printf "\e[${rs}m")
+    local hd=$(printf "\e[${hd}m")
+    local ad=$(printf "\e[${ad}m")
+    local de=$(printf "\e[${de}m")
+    local ln=$(printf "\e[${ln}m")
+    printf '%s' \
+"$hd--- a$tab$epoch_plus
++++ b$tab$epoch_plus
+$rs${ln}@@ -1 +1 @@$rs
+$de-a
+$ad+b
+$rs"
+}
+
+gen_exp_c()
+{
+    local tab=$(printf '\t')
+    local epoch_posix_1003_1_2001="Thu Jan  1 00:00:00 1970"
+    local rs=$(printf "\e[${rs}m")
+    local hd=$(printf "\e[${hd}m")
+    local ad=$(printf "\e[${ad}m")
+    local de=$(printf "\e[${de}m")
+    local ln=$(printf "\e[${ln}m")
+    printf '%s' \
+"$hd*** a$tab$epoch_posix_1003_1_2001
+--- b$tab$epoch_posix_1003_1_2001
+$rs***************$ln
+*** 1 ****
+$rs$de! a
+$rs$ln--- 1 ----
+$rs$ad! b
+$rs"
+}
+
+gen_exp_default()
+{
+    printf '%s' \
+"1c1
+< a
+---
+> b
+"
+}
+
+gen_exp_default_colors()
+{
+    local rs=$(printf "\e[${rs}m")
+    local hd=$(printf "\e[${hd}m")
+    local ad=$(printf "\e[${ad}m")
+    local de=$(printf "\e[${de}m")
+    local ln=$(printf "\e[${ln}m")
+    printf '%s' \
+"${ln}1c1
+$rs$de< a
+$rs---
+$ad> b
+$rs"
+}
+
+# Compare with some known outputs
+
+rs=0 hd=1 ad=32 de=31 ln=36
+
+diff --color=auto a b > k
+test $? = 1 || fail=1
+gen_exp_default > exp || framework_failure_
+compare exp k || fail=1
+
+diff --color=never a b > k
+test $? = 1 || fail=1
+gen_exp_default > exp || framework_failure_
+compare exp k || fail=1
+
+diff a b > k
+test $? = 1 || fail=1
+gen_exp_default > exp || framework_failure_
+compare exp k || fail=1
+
+diff --color=always a b > k
+test $? = 1 || fail=1
+gen_exp_default_colors > exp || framework_failure_
+compare exp k || fail=1
+
+diff -u --color=always a b > k
+test $? = 1 || fail=1
+gen_exp_u > exp || framework_failure_
+compare exp k || fail=1
+
+diff -c --color=always a b > k
+test $? = 1 || fail=1
+gen_exp_c > exp || framework_failure_
+compare exp k || fail=1
+
+rs=0 hd=33 ad=34 de=35 ln=36
+diff -u --color=always --palette="rs=0:hd=33:ad=34:de=35:ln=36" a b > k
+test $? = 1 || fail=1
+gen_exp_u > exp || framework_failure_
+compare exp k || fail=1
+
+Exit $fail
-- 
2.5.0

>From 031a5778004cc492fbf33f647838116780a73938 Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <[email-masked]>
Date: Fri, 27 Nov 2015 13:56:05 +0100
Subject: [PATCH 6/6] Generate terminal sequences on the line they belong

* src/diff.h: New function 'print_1_line_nl'.
* src/context.c (pr_context_hunk): Use 'print_1_line_nl'.
Generate terminal sequences on the line they belong.
(pr_unidiff_hunk): Likewise.
* src/normal.c (print_normal_hunk): Likewise.
* src/util.c (print_1_line_nl): New function.
(print_1_line): Become a wrapper of 'print_1_line_nl'.
* tests/colors: Adjust tests.
---
 src/context.c | 64 ++++++++++++++++++++++++++++++++++-------------------------
 src/diff.h    |  1 +
 src/normal.c  | 26 +++++++++++++++++-------
 src/util.c    | 19 ++++++++++++++++--
 tests/colors  | 28 +++++++++++++-------------
 5 files changed, 88 insertions(+), 50 deletions(-)

diff --git a/src/context.c b/src/context.c
index 216f547..0834079 100644
--- a/src/context.c
+++ b/src/context.c
@@ -207,19 +207,23 @@ pr_context_hunk (struct change *hunk)
   if (function)
     print_context_function (out, function);
 
+  putc ('\n', out);
   set_line_numbers_color_context ();
-  fputs ("\n*** ", out);
+  fputs ("*** ", out);
   print_context_number_range (&files[0], first0, last0);
-  fputs (" ****\n", out);
+  fputs (" ****", out);
   reset_color_context ();
+  putc ('\n', out);
 
   if (changes & OLD)
     {
       struct change *next = hunk;
 
+      if (first0 <= last0)
+        set_delete_color_context ();
+
       for (i = first0; i <= last0; i++)
 	{
-          bool reset_context = false;
 	  /* Skip past changes that apply (in file 0)
 	     only to lines before line I.  */
 
@@ -231,33 +235,36 @@ pr_context_hunk (struct change *hunk)
 	  prefix = " ";
 	  if (next && next->line0 <= i)
             {
-              reset_context = true;
-              set_delete_color_context ();
               /* The change NEXT covers this line.
                  If lines were inserted here in file 1, this is "changed".
                  Otherwise it is "deleted".  */
               prefix = (next->inserted > 0 ? "!" : "-");
             }
 
-	  print_1_line (prefix, &files[0].linbuf[i]);
-          if (reset_context)
+	  print_1_line_nl (prefix, &files[0].linbuf[i], true);
+          if (i == last0)
             reset_color_context ();
+          if (files[0].linbuf[i + 1][-1] == '\n')
+            putc ('\n', out);
 	}
     }
 
   set_line_numbers_color_context ();
   fputs ("--- ", out);
   print_context_number_range (&files[1], first1, last1);
-  fputs (" ----\n", out);
+  fputs (" ----", out);
   reset_color_context ();
+  putc ('\n', out);
 
   if (changes & NEW)
     {
       struct change *next = hunk;
 
+      if (first1 <= last1)
+        set_add_color_context ();
+
       for (i = first1; i <= last1; i++)
 	{
-          bool reset_context = false;
 	  /* Skip past changes that apply (in file 1)
 	     only to lines before line I.  */
 
@@ -269,16 +276,16 @@ pr_context_hunk (struct change *hunk)
 	  prefix = " ";
 	  if (next && next->line1 <= i)
             {
-              reset_context = true;
-              set_add_color_context ();
               /* The change NEXT covers this line.
                  If lines were deleted here in file 0, this is "changed".
                  Otherwise it is "inserted".  */
               prefix = (next->deleted > 0 ? "!" : "+");
             }
-	  print_1_line (prefix, &files[1].linbuf[i]);
-          if (reset_context)
+	  print_1_line_nl (prefix, &files[1].linbuf[i], true);
+          if (i == last1)
             reset_color_context ();
+          if (files[1].linbuf[i + 1][-1] == '\n')
+            putc ('\n', out);
 	}
     }
 }
@@ -381,16 +388,11 @@ pr_unidiff_hunk (struct change *hunk)
 	}
       else
 	{
-          bool reset_context = false;
-
 	  /* For each difference, first output the deleted part. */
 
 	  k = next->deleted;
           if (k)
-            {
-              reset_context = true;
-              set_delete_color_context ();
-            }
+            set_delete_color_context ();
 
 	  while (k--)
 	    {
@@ -398,30 +400,38 @@ pr_unidiff_hunk (struct change *hunk)
 	      putc ('-', out);
 	      if (initial_tab && ! (suppress_blank_empty && **line == '\n'))
 		putc ('\t', out);
-	      print_1_line (NULL, line);
+	      print_1_line_nl (NULL, line, true);
+
+              if (!k)
+                reset_color_context ();
+
+              if (line[1][-1] == '\n')
+                putc ('\n', out);
 	    }
 
 	  /* Then output the inserted part. */
 
 	  k = next->inserted;
           if (k)
-            {
-              reset_context = true;
-              set_add_color_context ();
-            }
+            set_add_color_context ();
+
 	  while (k--)
 	    {
 	      char const * const *line = &files[1].linbuf[j++];
 	      putc ('+', out);
 	      if (initial_tab && ! (suppress_blank_empty && **line == '\n'))
 		putc ('\t', out);
-	      print_1_line (NULL, line);
+	      print_1_line_nl (NULL, line, true);
+
+              if (!k)
+                reset_color_context ();
+
+              if (line[1][-1] == '\n')
+                putc ('\n', out);
 	    }
 
 	  /* We're done with this hunk, so on to the next! */
 
-          if (reset_context)
-            reset_color_context ();
 	  next = next->link;
 	}
     }
diff --git a/src/diff.h b/src/diff.h
index 5930cd1..791c29e 100644
--- a/src/diff.h
+++ b/src/diff.h
@@ -399,6 +399,7 @@ extern void output_1_line (char const *, char const *, char const *,
 extern void perror_with_name (char const *);
 extern void pfatal_with_name (char const *) __attribute__((noreturn));
 extern void print_1_line (char const *, char const * const *);
+extern void print_1_line_nl (char const *, char const * const *, bool);
 extern void print_message_queue (void);
 extern void print_number_range (char, struct file_data *, lin, lin);
 extern void print_script (struct change *, struct change * (*) (struct change *),
diff --git a/src/normal.c b/src/normal.c
index 227af10..5a1e687 100644
--- a/src/normal.c
+++ b/src/normal.c
@@ -53,16 +53,22 @@ print_normal_hunk (struct change *hunk)
   print_number_range (',', &files[0], first0, last0);
   fputc (change_letter[changes], outfile);
   print_number_range (',', &files[1], first1, last1);
-  fputc ('\n', outfile);
   reset_color_context ();
+  fputc ('\n', outfile);
 
   /* Print the lines that the first file has.  */
   if (changes & OLD)
     {
-      set_delete_color_context ();
+      if (first0 <= last0)
+        set_delete_color_context ();
       for (i = first0; i <= last0; i++)
-        print_1_line ("<", &files[0].linbuf[i]);
-      reset_color_context ();
+        {
+          print_1_line_nl ("<", &files[0].linbuf[i], true);
+          if (i == last0)
+            reset_color_context ();
+          if (files[0].linbuf[i + 1][-1] == '\n')
+            putc ('\n', outfile);
+        }
     }
 
   if (changes == CHANGED)
@@ -71,9 +77,15 @@ print_normal_hunk (struct change *hunk)
   /* Print the lines that the second file has.  */
   if (changes & NEW)
     {
-      set_add_color_context ();
+      if (first1 <= last1)
+        set_add_color_context ();
       for (i = first1; i <= last1; i++)
-        print_1_line (">", &files[1].linbuf[i]);
-      reset_color_context ();
+        {
+          print_1_line_nl (">", &files[1].linbuf[i], true);
+          if (i == last1)
+            reset_color_context ();
+          if (files[1].linbuf[i + 1][-1] == '\n')
+            putc ('\n', outfile);
+        }
     }
 }
diff --git a/src/util.c b/src/util.c
index b0e277b..65b06e9 100644
--- a/src/util.c
+++ b/src/util.c
@@ -1206,6 +1206,18 @@ print_script (struct change *script,
 void
 print_1_line (char const *line_flag, char const *const *line)
 {
+  print_1_line_nl (line_flag, line, false);
+}
+
+/* Print the text of a single line LINE,
+   flagging it with the characters in LINE_FLAG (which say whether
+   the line is inserted, deleted, changed, etc.).  LINE_FLAG must not
+   end in a blank, unless it is a single blank.  If SKIP_NL is set, then
+   the final '\n' is not printed.  */
+
+void
+print_1_line_nl (char const *line_flag, char const *const *line, bool skip_nl)
+{
   char const *base = line[0], *limit = line[1]; /* Help the compiler.  */
   FILE *out = outfile; /* Help the compiler some more.  */
   char const *flag_format = 0;
@@ -1233,10 +1245,13 @@ print_1_line (char const *line_flag, char const *const *line)
       fprintf (out, flag_format_1, line_flag_1);
     }
 
-  output_1_line (base, limit, flag_format, line_flag);
+  output_1_line (base, limit - (skip_nl && limit[-1] == '\n'), flag_format, line_flag);
 
   if ((!line_flag || line_flag[0]) && limit[-1] != '\n')
-    fprintf (out, "\n\\ %s\n", _("No newline at end of file"));
+    {
+      reset_color_context ();
+      fprintf (out, "\n\\ %s\n", _("No newline at end of file"));
+    }
 }
 
 /* Output a line from BASE up to LIMIT.
diff --git a/tests/colors b/tests/colors
index 5a62232..7593ca9 100755
--- a/tests/colors
+++ b/tests/colors
@@ -26,9 +26,9 @@ gen_exp_u()
 "$hd--- a$tab$epoch_plus
 +++ b$tab$epoch_plus
 $rs${ln}@@ -1 +1 @@$rs
-$de-a
-$ad+b
-$rs"
+$de-a$rs
+$ad+b$rs
+"
 }
 
 gen_exp_c()
@@ -43,12 +43,12 @@ gen_exp_c()
     printf '%s' \
 "$hd*** a$tab$epoch_posix_1003_1_2001
 --- b$tab$epoch_posix_1003_1_2001
-$rs***************$ln
-*** 1 ****
-$rs$de! a
-$rs$ln--- 1 ----
-$rs$ad! b
-$rs"
+$rs***************
+$ln*** 1 ****$rs
+$de! a$rs
+$ln--- 1 ----$rs
+$ad! b$rs
+"
 }
 
 gen_exp_default()
@@ -69,11 +69,11 @@ gen_exp_default_colors()
     local de=$(printf "\e[${de}m")
     local ln=$(printf "\e[${ln}m")
     printf '%s' \
-"${ln}1c1
-$rs$de< a
-$rs---
-$ad> b
-$rs"
+"${ln}1c1$rs
+$de< a$rs
+---
+$ad> b$rs
+"
 }
 
 # Compare with some known outputs
-- 
2.5.0