/*
 * filterdiff - extract (or exclude) a diff from a diff file
 * lsdiff - show which files are modified by a patch
 * grepdiff - show files modified by a patch containing a regexp
 * Copyright (C) 2001, 2002, 2003, 2004, 2008, 2009, 2011, 2013, 2014 Tim Waugh <twaugh@redhat.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <errno.h>
#ifdef HAVE_ERROR_H
# include <error.h>
#endif /* HAVE_ERROR_H */
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h> // for ssize_t
#endif /* HAVE_SYS_TYPES_H */
#include <fnmatch.h>
#include <getopt.h>
#include <locale.h>
#ifdef HAVE_PCRE2POSIX_H
# include <pcre2posix.h>
#else
# include <regex.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>

#include "util.h"
#include "diff.h"

struct range {
	struct range *next;
	unsigned long start;
	unsigned long end;
};

enum line_numbering {
	None = 0,
	Before,
	After,
	OriginalBefore,
	OriginalAfter
};

enum {
	output_none = 0,
	output_hunk,
	output_file
} output_matching = output_none;

enum {
	only_match_all = 0,
	only_match_rem,
	only_match_add,
	only_match_mod  /* bitwise rem | add */
} only_matching = only_match_all;

static struct patlist *pat_include = NULL;
static struct patlist *pat_exclude = NULL;
static struct range *hunks = NULL;
static struct range *lines = NULL;
static struct range *files = NULL;
static enum line_numbering number_lines = None;
static int number_files = 0;
static int files_exclude = 0;
static int lines_exclude = 0;
static int hunks_exclude = 0;

static int unzip = 0;
static enum {
	mode_filter,
	mode_list,
	mode_grep,
} mode;
static regex_t *regex = NULL;
static size_t num_regex = 0;
static int clean_comments = 0;
static int numbering = 0;
static int annotating = 0;
static int ignore_components = 0;
static int strip_components = 0;
static const char *prefix_to_add = NULL;
static const char *old_prefix_to_add = NULL;
static const char *new_prefix_to_add = NULL;
static int show_status = 0;
static int verbose = 0;
static int removing_timestamp = 0;
static int egrepping = 0;
static int print_patchnames = -1;
static int empty_files_as_absent = 0;
static unsigned long filecount=0;

static enum git_prefix_mode git_prefix_mode = GIT_PREFIX_KEEP;

enum git_extended_diffs_mode {
	GIT_EXTENDED_DIFFS_EXCLUDE = 0,  /* Skip extended diffs (0.4.3 behavior) */
	GIT_EXTENDED_DIFFS_INCLUDE = 1   /* Process extended diffs (Git workflow) */
};

static enum git_extended_diffs_mode git_extended_diffs_mode = GIT_EXTENDED_DIFFS_EXCLUDE;

/* Helper function to check if a git diff type should be excluded based on mode */
static int
should_skip_git_extended_diff (enum git_diff_type git_type)
{
	if (git_extended_diffs_mode == GIT_EXTENDED_DIFFS_INCLUDE)
		return 0;  /* Don't skip anything if include mode */

	/* In exclude mode, skip all diffs without content hunks.
	 * This includes renames, copies, mode-only changes, binary files,
	 * and new/deleted files without content (e.g., binary files).
	 * This restores 0.4.3 behavior where only files with actual
	 * patch hunks were shown. */
	switch (git_type) {
	case GIT_DIFF_NORMAL:
		return 0;  /* Don't skip - has hunks */
	default:
		return 1;  /* Skip all extended/special types */
	}
}

/* Helper function to check if current patch is a Git patch */
static int
is_git_patch (char **headers, unsigned int num_headers)
{
	unsigned int i;
	for (i = 0; i < num_headers; i++) {
		if (!strncmp (headers[i], "diff --git ", 11)) {
			return 1;
		}
	}
	return 0;
}

static int
regexecs (regex_t *regex, size_t num_regex, const char *string,
	  size_t nmatch, regmatch_t pmatch[], int eflags)
{
	size_t i;
	int ret = REG_NOMATCH;
	for (i = 0; i < num_regex; i++)
		if (!(ret = regexec (&regex[i], string, nmatch,
				     pmatch, eflags)))
			break;
	return ret;
}

static int file_exists (const char *name, const char *timestamp)
{
	struct tm t;
	long zone = -1;

	if (!strcmp (name, "/dev/null"))
		return 0;

	if (read_timestamp (timestamp, &t, &zone))
		return 1;

	/* If the time is less that fifteen hours either side of the
	 * start of 1970, and it's an exact multiple of 15 minutes, it's
	 * very likely to be the result of ctime(&zero). */
	if (t.tm_sec == 0 &&
	    ((t.tm_year == 69 && t.tm_mon == 11 && t.tm_mday == 31 &&
	      t.tm_hour >= 9) ||
	     (t.tm_year == 70 && t.tm_mon == 0 && t.tm_mday == 1 &&
	      t.tm_hour <= 15)) &&
	    (t.tm_min % 15) == 0) {
		if (zone != -1) {
			/* Extra checking, since we know the timezone. */
			long offset = 0;
			if (t.tm_year == 69) {
				offset = 100 * (t.tm_hour - 24);
				if (t.tm_min)
					offset += 100 + t.tm_min - 60;
			} else {
				offset = 100 * t.tm_hour;
				offset += t.tm_min;
			}

			if (offset != zone)
				return 1;
		}

		return 0;
	}

	/* Otherwise, it's a real file timestamp. */
	return 1;
}

static int output_header_line (const char *line)
{
	char *fn;

	if (strncmp (line, "diff", 4) == 0 && isspace (line[4])) {
		size_t          args = 0;
		const char      *end = line + 5, *begin = end, *ws = end;
		printf ("%.5s", line);
		while (*end != 0) {
			if (isspace (*begin))
				begin = end;
			if (isspace (*end)) {
				if (*begin == '-') {
					if (isspace (begin[1]))
						++args;
					printf ("%.*s", (int)(end - ws), ws);
				} else {
					printf ("%.*s", (int)(begin - ws), ws);
					if (args == 0 && old_prefix_to_add)
						fputs (old_prefix_to_add,
						       stdout);
					if (args == 1 && new_prefix_to_add)
						fputs (new_prefix_to_add,
						       stdout);
					++args;
					fn = xstrndup (begin, end - begin);
					fputs (stripped (fn,
							 strip_components),
					       stdout);
					free (fn);
				}
				ws = begin = end;
			}
			++end;
		}
		printf ("%.*s", (int)(end - ws), ws);
	} else if (strncmp (line, "---", 3) == 0 ||
		   strncmp (line, "+++", 3) == 0) {

		int h = strcspn (line + 4, "\t\n");
		fwrite (line, 1, 4, stdout);

		if (prefix_to_add)
			fputs (prefix_to_add, stdout);
		else {
			if (old_prefix_to_add && strncmp (line, "---", 3) == 0)
				fputs (old_prefix_to_add, stdout);
			if (new_prefix_to_add && strncmp (line, "+++", 3) == 0)
				fputs (new_prefix_to_add, stdout);
		}

		fn = xstrndup (line + 4, h);
		fputs (stripped (fn, strip_components), stdout);
		if (removing_timestamp)
			putchar ('\n');
		else
			fputs (line + 4 + h, stdout);

		free (fn);
	} else
		fputs (line, stdout);
	return 0;
}

static int
file_matches (void)
{
	int f = 0;
	struct range *r;

	// See if the file range list includes this file.  -1UL is a
	// wildcard.
	for (r = files; r; r = r->next)
		if ((r->start == -1UL ||
		     r->start <= filecount) &&
		    (r->end == -1UL ||
		     filecount <= r->end)) {
			f = 1;
			break;
		}

	if (files && !f && !files_exclude)
		return 0;
	if (files && f && files_exclude)
		return 0;

	return 1;
}

static void display_filename (unsigned long linenum, char status,
			      const char *filename, const char *patchname)
{
	const char *processed_filename;
	char *git_stripped_filename = NULL;

	if (mode == mode_list && !file_matches ())
		/* This is lsdiff --files=... and this file is not to be
		 * listed. */
		return;

	if (print_patchnames)
		printf ("%s:", patchname);
	if (numbering)
		printf ("%lu\t", linenum);
	if (number_files)
		printf ("File #%-3lu\t", filecount);
	if (show_status)
		printf ("%c ", status);
	if (prefix_to_add)
		fputs (prefix_to_add, stdout);

	/* Handle git prefix stripping if needed */
	git_stripped_filename = strip_git_prefix_from_filename(filename, git_prefix_mode);
	processed_filename = stripped(git_stripped_filename, strip_components);

	puts (processed_filename);

	/* Clean up allocated memory */
	free (git_stripped_filename);
}

static int
hunk_matches (unsigned long orig_offset, unsigned long orig_count,
	      unsigned long hunknum)
{
	int h = 0, l = 0;
	struct range *r;

	/* The hunk can't match if the containing file doesn't. */
	if (!file_matches ())
		return 0;

	// For the purposes of matching, zero lines at offset n counts
	// as line n.
	if (!orig_count)
		orig_count = 1;

	// See if the hunk range list includes this hunk.  -1UL is a
	// wildcard.
	for (r = hunks; r; r = r->next)
		if ((r->start == -1UL ||
		     r->start <= hunknum) &&
		    (r->end == -1UL ||
		     hunknum <= r->end)) {
			h = 1;
			break;
		}

	// See if the lines range list includes this hunk.  -1UL is a
	// wildcard.
	for (r = lines; r; r = r->next)
		if ((r->start == -1UL ||
		     r->start < (orig_offset + orig_count)) &&
		    (r->end == -1UL ||
		     r->end >= orig_offset)) {
			l = 1;
			break;
		}

	if (hunks && !h && !hunks_exclude)
		return 0;

	if (hunks && h && hunks_exclude)
		return 0;

	if (lines && !l && !lines_exclude)
		return 0;

	if (lines && l && lines_exclude)
		return 0;

	return 1;
}

static int
do_git_diff_no_hunks (FILE *f, char **header, unsigned int num_headers,
		      int match, char **line, size_t *linelen,
		      unsigned long *linenum, unsigned long start_linenum,
		      char status, const char *bestname, const char *patchname,
		      int *orig_file_exists, int *new_file_exists,
		      enum git_diff_type git_type)
{
	unsigned int i;
	int displayed_filename = 0;

	/* Set file existence based on git diff type */
	switch (git_type) {
	case GIT_DIFF_NEW_FILE:
		*orig_file_exists = 0;
		*new_file_exists = 1;
		break;
	case GIT_DIFF_DELETED_FILE:
		*orig_file_exists = 1;
		*new_file_exists = 0;
		break;
	case GIT_DIFF_RENAME:
	case GIT_DIFF_COPY:
	case GIT_DIFF_BINARY:
	case GIT_DIFF_MODE_ONLY:
	default:
		*orig_file_exists = 1;
		*new_file_exists = 1;
		break;
	}

	/* If this diff matches the filter, display it */
	if (match) {
		if (mode == mode_filter) {
			/* Output all headers for this git diff */
			for (i = 0; i < num_headers; i++)
				fputs (header[i], stdout);
		} else if (mode == mode_list && !displayed_filename) {
			if (!show_status) {
				display_filename (start_linenum, status,
						  bestname, patchname);
			}
			displayed_filename = 1;
		}
	}

	/* Look for any additional content after headers (like binary patches)
	 * Note: we might already have a content line in *line if we broke out of header reading */
	int first_iteration = 1;
	while (first_iteration || getline (line, linelen, f) != -1) {
		if (!first_iteration)
			++*linenum;
		first_iteration = 0;

		/* Check if we've hit the next diff */
		if (!strncmp (*line, "diff ", 5))
			return 1; /* Found next diff, return to main loop */

		/* Check for binary patch content */
		if (!strncmp (*line, "GIT binary patch", 16) ||
		    !strncmp (*line, "literal ", 8) ||
		    !strncmp (*line, "delta ", 6)) {
			/* Output binary content if this diff matches */
			if (match && mode == mode_filter) {
				fputs (*line, stdout);
				/* Continue reading binary content until next diff or EOF */
				while (getline (line, linelen, f) != -1) {
					++*linenum;
					if (!strncmp (*line, "diff ", 5))
						return 1;
					fputs (*line, stdout);
					/* Binary patches end with empty line typically */
					if ((*line)[0] == '\n')
						break;
				}
			} else {
				/* Skip binary content if not matching */
				while (getline (line, linelen, f) != -1) {
					++*linenum;
					if (!strncmp (*line, "diff ", 5))
						return 1;
					if ((*line)[0] == '\n')
						break;
				}
			}
			break;
		}

		/* For other types of content, just output if matching */
		if (match && mode == mode_filter)
			fputs (*line, stdout);
	}

	return 0; /* EOF reached */
}

static int
do_unified (FILE *f, char **header, unsigned int num_headers,
	    int match, char **line,
	    size_t *linelen, unsigned long *linenum,
	    unsigned long start_linenum, char status,
	    const char *bestname, const char *patchname,
	    int *orig_file_exists, int *new_file_exists)
{
	/* Skip hunk. */
	unsigned long orig_count = 0, new_count = 0;
	unsigned long orig_offset, new_offset;
	unsigned long hunknum = 0;
	unsigned long track_linenum = 0;
	int header_displayed = 0;
	int hunk_match = match;
	long munge_offset = 0;
	int displayed_filename = 0;
	unsigned long last_hunkmatch = 0;
	unsigned long hunk_linenum = *linenum;
	FILE *match_tmpf = NULL;
	int grepmatch = 0;
	long delayed_munge = 0;
	int ret = 0;
	int orig_is_empty = 1, new_is_empty = 1; /* assume until otherwise */

	if (output_matching == output_file)
		match_tmpf = xtmpfile ();

	for (;;) {
		ssize_t got = getline (line, linelen, f);
		if (got == -1) {
			ret = EOF;
			goto out;
		}
		++*linenum;

		if (!orig_count && !new_count && **line != '\\') {
			char *trailing;

			if (strncmp (*line, "@@ ", 3)) {
				/* Check if this is the start of the next diff */
				if (!strncmp (*line, "diff ", 5) ||
				    !strncmp (*line, "--- ", 4) ||
				    !strncmp (*line, "*** ", 4)) {
					ret = 1; /* Found next diff */
				}
				break;
			}

			/* Next chunk. */
			hunknum++;
			hunk_linenum = *linenum;

			if (output_matching == output_hunk && !grepmatch)
				// We are missing this hunk out, but
				// we need to remember how many lines
				// it would have added or removed.
				munge_offset += delayed_munge;

			if (output_matching != output_file)
				grepmatch = 0;
			if (output_matching == output_hunk) {
				if (match_tmpf)
					fclose (match_tmpf);
				match_tmpf = xtmpfile ();
			}

			if (read_atatline (*line, &orig_offset, &orig_count,
					   &new_offset, &new_count))
				error (EXIT_FAILURE, 0,
				      "line not understood: %s", *line);

			if (orig_count)
				orig_is_empty = 0;
			if (new_count)
				new_is_empty = 0;

			// Decide if this hunk matches.
			if (match)
				hunk_match = hunk_matches (orig_offset,
							   orig_count,
							   hunknum);
			else hunk_match = 0;

			trailing = strchr (*line, '+');
			trailing += strcspn (trailing, " \n");
			if (*trailing == ' ')
				trailing++;
			trailing += strspn (trailing, "@");

			if (hunk_match && numbering && verbose &&
			    mode != mode_grep) {
				if (print_patchnames)
					printf ("%s-", patchname);
				printf ("\t%lu\tHunk #%lu",
					hunk_linenum, hunknum);
				if (verbose > 1) {
					char *p = trailing;
					if (*p != '\n')
						p++;
					printf ("\t%s", p);
				} else
					putchar ('\n');
			}

			if (hunk_match &&
			    (mode == mode_filter ||
			     output_matching != output_none)) {
				int first_hunk = !header_displayed;
				FILE *output_to = stdout;

				if (mode == mode_grep) {
					delayed_munge = orig_count - new_count;
					if (!grepmatch)
						output_to = match_tmpf;
				}

				if (!header_displayed &&
				    mode != mode_grep) {
					// Display the header.
					unsigned int i;
					for (i = 0; i < num_headers - 2; i++)
						output_header_line (header[i]);
					if (number_lines != After && number_lines != OriginalAfter)
						output_header_line (header[num_headers - 2]);
					if (number_lines != Before && number_lines != OriginalBefore)
						output_header_line (header[num_headers - 1]);
					header_displayed = 1;
				}
				switch (number_lines) {
				case None:
					// Display the offsets and
					// counts, adjusting for any
					// hunks we've previously
					// missed out.
					fprintf (output_to,
						 "@@ -%lu", orig_offset);
					if (orig_count != 1)
						fprintf (output_to,
							 ",%lu", orig_count);
					fprintf (output_to, " +%lu",
						 new_offset + munge_offset);
					if (new_count != 1)
						fprintf (output_to,
							 ",%lu", new_count);
					fprintf (output_to, " @@");

					if (annotating)
						fprintf (output_to,
							 " Hunk #%lu, %s",
							 hunknum, bestname);

					if (clean_comments)
						fputs ("\n", output_to);
					else {
						int idx = trailing - *line;
						fwrite (trailing, (size_t) got-idx,
							1, output_to);
					}
					break;
				case Before:
					// Note the initial line number
					track_linenum = orig_offset;
					if (!first_hunk ||
					    (output_matching == output_file &&
					     hunknum > 1))
						fputs ("...\n", output_to);
					break;
				case After:
					// Note the initial line number
					track_linenum = (new_offset +
							 munge_offset);
					if (!first_hunk ||
					    (output_matching == output_file &&
					     hunknum > 1))
						fputs ("...\n", output_to);
					break;
				case OriginalBefore:
					// Note the initial line number (original, no munge)
					track_linenum = orig_offset;
					if (!first_hunk ||
					    (output_matching == output_file &&
					     hunknum > 1))
						fputs ("...\n", output_to);
					break;
				case OriginalAfter:
					// Note the initial line number (original, no munge)
					track_linenum = new_offset;
					if (!first_hunk ||
					    (output_matching == output_file &&
					     hunknum > 1))
						fputs ("...\n", output_to);
					break;
				}
			} else if (mode == mode_filter)
				// We are missing this hunk out, but
				// we need to remember how many lines
				// it would have added or removed.
				munge_offset += orig_count - new_count;

			continue;
		}

		if (**line != '\\') {
			if (orig_count && **line != '+')
				orig_count--;
			if (new_count && **line != '-')
				new_count--;
		}

		if (hunk_match && mode == mode_grep &&
		    (only_matching == only_match_all
		     || (**line == '-' && only_matching & only_match_rem)
		    || (**line == '+' && only_matching & only_match_add)
		    ) &&
		    !regexecs (regex, num_regex, *line + 1, 0, NULL, 0)) {
			if (output_matching == output_none) {
				if (!displayed_filename) {
					char display_status = status;
					/* Update status based on file existence for grepdiff -s */
					if (show_status) {
						int orig_absent = orig_file_exists && !*orig_file_exists;
						int new_absent = new_file_exists && !*new_file_exists;

						/* Check for empty files if --empty-files-as-absent is set */
						if (empty_files_as_absent) {
							if (orig_file_exists && *orig_file_exists && orig_is_empty)
								orig_absent = 1;
							if (new_file_exists && *new_file_exists && new_is_empty)
								new_absent = 1;
						}

						if (orig_absent)
							display_status = '+';
						else if (new_absent)
							display_status = '-';
					}
					displayed_filename = 1;
					display_filename (start_linenum,
							  display_status, bestname,
							  patchname);
				}

				if (numbering && verbose &&
				    hunknum > last_hunkmatch) {
					last_hunkmatch = hunknum;
					if (print_patchnames)
						printf ("%s-", patchname);
					printf ("\t%lu\tHunk #%lu\n",
						hunk_linenum, hunknum);
				}
			} else {
				if (match_tmpf) {
					if (!header_displayed) {
						unsigned int i;
						for (i = 0; i < num_headers - 2; i++)
							output_header_line (header[i]);
						if (number_lines != After && number_lines != OriginalAfter)
							output_header_line (header[num_headers - 2]);
						if (number_lines != Before && number_lines != OriginalBefore)
							output_header_line (header[num_headers - 1]);
						header_displayed = 1;
					}

					rewind (match_tmpf);
					while (!feof (match_tmpf)) {
						int ch = fgetc (match_tmpf);
						if (ch == EOF)
							break;
						putchar (ch);
					}
					fclose (match_tmpf);
					match_tmpf = NULL;
				}
				grepmatch = 1;
			}
		}

		if (hunk_match &&
		    (mode == mode_filter ||
		     output_matching != output_none)) {
			FILE *output_to = stdout;
			if (mode == mode_grep && !grepmatch)
				output_to = match_tmpf;
			if (number_lines == None)
				// Just display each line.
				fwrite (*line, (size_t) got, 1, output_to);
			else if ((number_lines == Before && **line != '+') ||
				 (number_lines == After && **line != '-') ||
				 (number_lines == OriginalBefore && **line != '+') ||
				 (number_lines == OriginalAfter && **line != '-')) {
				// Numbered line.
				const char *rest = *line;
				if (rest[0] != '\n')
					// Handle whitespace damage
					rest++;

				fprintf (output_to, "%lu\t:%s",
					 track_linenum++, rest);
			}
		}
	}

 out:
	if (match_tmpf)
		fclose (match_tmpf);

	if (empty_files_as_absent) {
		if (orig_file_exists != NULL && orig_is_empty)
			*orig_file_exists = 0;
		if (new_file_exists != NULL && new_is_empty)
			*new_file_exists = 0;
	}

	return ret;
}

static int
do_context (FILE *f, char **header, unsigned int num_headers,
	    int match, char **line,
	    size_t *linelen, unsigned long *linenum,
	    unsigned long start_linenum, char status,
	    const char *bestname, const char *patchname,
	    int *orig_file_exists, int *new_file_exists)
{
	/* Skip before and after segments. */
	unsigned long line_start, line_end, line_count;
	unsigned long hunknum = 0;
	unsigned long track_linenum = 0;
	unsigned long changed[2];
	long munge_offset = 0;
	int header_displayed = 0;
	char *n, *end;
	int i;
	int hunk_match = 0;
	int displayed_filename = 0;
	unsigned long last_hunkmatch = 0;
	unsigned long hunk_linenum = *linenum;
	FILE *match_tmpf = NULL;
	int grepmatch = 0;
	int ret = 0;
	unsigned long unchanged;
	int first_hunk = 0;
	int orig_is_empty = 1, new_is_empty = 1; /* assume until otherwise */
	size_t got = 0;

	/* Context diff hunks are like this:
	 *
	 * *************** [GNU diff can put stuff here]
	 * *** start[,end] **** [we sometimes put stuff here]
	 *   from lines... (omitted if there are only insertions)
	 *[*************** [GNU diff can put stuff here]]
	 * --- start[,end] ----
	 *   to lines... (omitted if there are only deletions)
	 *[*** start[,end] ****
	 * ...]
	 *
	 * Both from and to lines may end with:
	 * \ No newline at end of file
	 */

	if (getline (line, linelen, f) == -1)
		return EOF;
	++*linenum;

	if (strncmp (*line, "***************", 15))
		return 1;

	if (getline (line, linelen, f) == -1)
		return EOF;
	++*linenum;

	if (output_matching == output_file)
		match_tmpf = xtmpfile ();

 next_hunk:
	unchanged = 0;
	changed[0] = changed[1] = 0; // for munge calculation

	for (i = 0; i < 2; i++) {
		int first = 1;

		if (i == 0)
			first_hunk = !header_displayed;

		if (!i && !strncmp (*line, "***************", 15)) {
			/* Some diffs seem to have this for every
			 * set of changes.  SuSV2 says not to,
			 * but the GNU diff info page disagrees. */
			i--;

			if (getline (line, linelen, f) == -1) {
			    ret = EOF;
			    goto out;
			}

			++*linenum;
			continue;
		}

		if (strncmp (*line, i ? "--- " : "*** ", 4)) {
			ret = 1;
			goto out;
		}

		if (!i) {
			hunknum++;
			hunk_linenum = *linenum;
			if (output_matching != output_file)
				grepmatch = 0;
			if (output_matching == output_hunk) {
				if (match_tmpf)
					fclose (match_tmpf);
				match_tmpf = xtmpfile ();
			}
		}

	do_line_counts:
		n = *line + 4;
		line_start = strtoul (n, &end, 10);
		if (n == end) {
			ret = 1;
			goto out;
		}

		if (*end == ',') {
			n = end + 1;
			line_end = strtoul (n, &end, 10);
			if (n == end) {
				ret = 1;
				goto out;
			}

			if (line_start > line_end) {
				ret = 1;
				goto out;
			}

			line_count = line_end - line_start + 1;
		} else {
			line_end = line_start;
			line_count = line_start ? 1 : 0;
		}

		n = strchr (n, '*');
		if (n)
			n += 4;

		if (!i) {
			if (match)
				hunk_match = hunk_matches (line_start,
							   line_count,
							   hunknum);
			else hunk_match = 0;

			if (hunk_match && numbering && verbose &&
			    mode != mode_grep) {
				if (print_patchnames)
					printf ("%s-", patchname);
				printf ("\t%lu\tHunk #%lu\n",
					hunk_linenum, hunknum);
			}
		}

		if (hunk_match &&
		    (mode == mode_filter || output_matching != output_none)) {
			FILE *output_to= stdout;

			if (mode == mode_grep && !grepmatch)
				output_to = match_tmpf;

			// Display the line counts.
			if (!header_displayed && mode == mode_filter) {
				unsigned int i;
				for (i = 0; i < num_headers - 2; i++)
					output_header_line (header[i]);
				if (number_lines != After && number_lines != OriginalAfter)
					output_header_line (header[num_headers - 2]);
				if (number_lines != Before && number_lines != OriginalBefore)
					output_header_line (header[num_headers - 1]);
				header_displayed = 1;
			}

			if (number_lines == None) switch (i) {
			case 0:
				fputs ("***************\n", output_to);
				fprintf (output_to, "*** %lu", line_start);
				if (line_end != line_start)
					fprintf (output_to, ",%lu", line_end);
				fputs (" ****", output_to);

				if (annotating)
					fprintf (output_to,
						 " Hunk #%lu, %s\n",
						 hunknum, bestname);
				else if (n)
					fputs (n, output_to);
				else
					fputc ('\n', output_to);

				break;
			case 1:
				fprintf (output_to, "--- %lu",
					 line_start + munge_offset);
				if (line_end != line_start)
					fprintf (output_to, ",%lu",
						 line_end + munge_offset);
				fputs (" ----\n", output_to);
				break;
			}

			switch (number_lines) {
			case None:
				break;

			case Before:
				if (i != 0)
					break;

				// Note the initial line number.
				track_linenum = line_start;
				if (!first_hunk ||
				    (output_matching == output_file &&
				     hunknum > 1))
					fputs ("...\n", output_to);
				break;

			case After:
				if (i != 1)
					break;

				track_linenum = line_start + munge_offset;
				if (!first_hunk ||
				    (output_matching == output_file &&
				     hunknum > 1))
					fputs ("...\n", output_to);
				break;

			case OriginalBefore:
				if (i != 0)
					break;

				// Note the initial line number (original, no munge).
				track_linenum = line_start;
				if (!first_hunk ||
				    (output_matching == output_file &&
				     hunknum > 1))
					fputs ("...\n", output_to);
				break;

			case OriginalAfter:
				if (i != 1)
					break;

				// Note the initial line number (original, no munge).
				track_linenum = line_start;
				if (!first_hunk ||
				    (output_matching == output_file &&
				     hunknum > 1))
					fputs ("...\n", output_to);
				break;
			}
		}

		got = getline (line, linelen, f);
		if (got == -1) {
			ret = EOF;
			goto out;
		}

		++*linenum;

		if (i && line_count == unchanged)
			break;

		while ((line_count == 0 && **line == '\\') ||
		       line_count--) {
			if (hunk_match && mode == mode_grep &&
			    (only_matching == only_match_all
			     || (**line != ' ' && i == 0 && only_matching & only_match_rem)
			     || (**line != ' ' && i == 1 && only_matching & only_match_add)
			    ) &&
			    !regexecs (regex, num_regex, *line + 2,
				       0, NULL, 0)) {
				if (output_matching == output_none) {
					if (!displayed_filename) {
						char display_status = status;
						/* Update status based on file existence for grepdiff -s */
						if (show_status) {
							int orig_absent = orig_file_exists && !*orig_file_exists;
							int new_absent = new_file_exists && !*new_file_exists;

							/* Check for empty files if --empty-files-as-absent is set */
							if (empty_files_as_absent) {
								if (orig_file_exists && *orig_file_exists && orig_is_empty)
									orig_absent = 1;
								if (new_file_exists && *new_file_exists && new_is_empty)
									new_absent = 1;
							}

							if (orig_absent)
								display_status = '+';
							else if (new_absent)
								display_status = '-';
						}
						displayed_filename = 1;
						display_filename(start_linenum,
								 display_status,
								 bestname,
								 patchname);
					}

					if (numbering && verbose &&
					    hunknum > last_hunkmatch) {
						last_hunkmatch = hunknum;
						if (print_patchnames)
							printf ("%s-",
								patchname);
						printf ("\t%lu\tHunk #%lu\n",
							hunk_linenum, hunknum);
					}
				} else {
					if (!header_displayed) {
						unsigned int i;
						for (i = 0; i < num_headers - 2; i++)
							output_header_line (header[i]);
						if (number_lines != After && number_lines != OriginalAfter)
							output_header_line (header[num_headers - 2]);
						if (number_lines != Before && number_lines != OriginalBefore)
							output_header_line (header[num_headers - 1]);
						header_displayed = 1;
					}

					if (match_tmpf) {
						rewind (match_tmpf);
						while (!feof (match_tmpf)) {
							int ch;
							ch = fgetc(match_tmpf);
							if (ch == EOF)
								break;
							putchar (ch);
						}
						fclose (match_tmpf);
						match_tmpf = NULL;
					}

					grepmatch = 1;
				}
			}

			if (!i && first) {
				first = 0;
				if (!strncmp (*line, "--- ", 4)) {
					/* From lines were
					 * omitted. */
					i++;
					goto do_line_counts;
				}
			}

			if (**line == ' ')
				unchanged++;

			if (empty_files_as_absent) switch (**line) {
			case ' ':
			case '!':
				new_is_empty = orig_is_empty = 0;
				break;
			case '+':
				new_is_empty = 0;
				break;
			case '-':
				orig_is_empty = 0;
				break;
			}

			if (hunk_match &&
			    (mode == mode_filter ||
			     output_matching != output_none)) {
				FILE *output_to = stdout;
				if (mode == mode_grep && !grepmatch)
					output_to = match_tmpf;

				if (number_lines == None)
					fwrite (*line, (size_t) got,
						1, output_to);
				else if ((number_lines == Before && !i) ||
					 (number_lines == After && i) ||
					 (number_lines == OriginalBefore && !i) ||
					 (number_lines == OriginalAfter && i)) {
					fprintf (output_to, "%lu\t:",
						 track_linenum++);
					fwrite (2 + *line, (size_t) got - 2,
						1, output_to);
				}
			}

			if ((mode == mode_filter && !hunk_match) ||
			    output_matching == output_hunk)
				switch (**line) {
				case '!':
				case '\\':
					changed[i]++;
					break;
				case '+':
					changed[1]++;
					break;
				case '-':
					changed[0]++;
					break;
				}

			got = getline (line, linelen, f);
			if (got == -1) {
				ret = EOF;
				goto out;
			}

			++*linenum;
		}
	}

	if (output_matching != output_hunk || !grepmatch)
		munge_offset += changed[0] - changed[1];
	goto next_hunk;

out:
	if (match_tmpf)
		fclose (match_tmpf);

	if (empty_files_as_absent) {
		if (orig_file_exists != NULL && orig_is_empty)
			*orig_file_exists = 0;
		if (new_file_exists != NULL && new_is_empty)
			*new_file_exists = 0;
	}

	return ret;
}

#define MAX_HEADERS 6
static int filterdiff (FILE *f, const char *patchname)
{
	static unsigned long linenum = 1;
	char *names[2];
	char *header[MAX_HEADERS + 2] = { NULL, NULL };
	unsigned int num_headers = 0;
	char *line = NULL;
	size_t linelen = 0;
	char *p;
	const char *p_stripped;
	int match;
	int i;

	if (getline (&line, &linelen, f) == -1)
		return 0;

	for (;;) {
		char status = '!';
		unsigned long start_linenum;
		int orig_file_exists, new_file_exists;
		int is_context = -1;
		int is_git_diff = 0;  /* Flag to track if we're processing a git diff */
		int result;
		int (*do_diff) (FILE *, char **, unsigned int,
				int, char **, size_t *,
				unsigned long *, unsigned long,
				char, const char *, const char *,
				int *, int *);

		orig_file_exists = 0; // shut gcc up

		// Search for start of patch ("diff ", or "--- " for
		// unified diff, "*** " for context).
		for (;;) {
			if (!strncmp (line, "diff ", 5))
					break;

			if (!strncmp (line, "--- ", 4)) {
				is_context = 0;
				break;
			}

			if (!strncmp (line, "*** ", 4)) {
				is_context = 1;
				break;
			}

			/* Show non-diff lines if excluding, or if
			 * in verbose mode, and if --clean isn't specified. */
			if (mode == mode_filter && (pat_exclude || verbose)
				&& !clean_comments)
				fputs (line, stdout);

			if (getline (&line, &linelen, f) == -1)
				goto eof;
			linenum++;
		}

		start_linenum = linenum;
		header[0] = xstrdup (line);
		num_headers = 1;

		if (is_context == -1) {
			int valid_extended = 1;
			int hit_eof = 0;
			for (;;) {
				if (getline (&line, &linelen, f) == -1) {
					hit_eof = 1;
					break;
				}
				linenum++;

				if (!strncmp (line, "diff ", 5)) {
					/* Found start of next diff - don't add to headers */
					break;
				}

				if (!strncmp (line, "--- ", 4))
					is_context = 0;
				else if (!strncmp (line, "*** ", 4))
					is_context = 1;
				else if (!strncmp (line, "Binary files ", 13)) {
					/* Binary files line - this is diff content, break to process */
					break;
				} else if (!strncmp (line, "GIT binary patch", 16)) {
					/* GIT binary patch line - this is diff content, break to process */
					break;
				} else if (strncmp (line, "old mode ", 9) &&
				    strncmp (line, "new mode ", 9) &&
				    strncmp (line, "deleted file mode ", 18) &&
				    strncmp (line, "new file mode ", 14) &&
				    strncmp (line, "copy from ", 10) &&
				    strncmp (line, "copy to ", 8) &&
				    strncmp (line, "rename from ", 12) &&
				    strncmp (line, "rename to ", 10) &&
				    strncmp (line, "similarity index ", 17) &&
				    strncmp (line, "dissimilarity index ", 20) &&
				    strncmp (line, "index ", 6))
					valid_extended = 0;

				if (!valid_extended)
					break;

				/* Drop excess header lines */
				if (num_headers > MAX_HEADERS )
					free (header[--num_headers]);

				header[num_headers++] = xstrdup (line);

				if (is_context != -1)
					break;
			}

			if (!valid_extended)
				goto flush_continue;

			/* If we hit EOF while reading extended headers, but have valid git headers,
			 * we should still process this as a git diff without hunks */
			if (hit_eof && valid_extended && is_context == -1) {
				/* Process as git diff without hunks and then exit */
				enum git_diff_type git_type = detect_git_diff_type (header, num_headers);


				/* Skip extended diffs if in exclude mode */
				if (should_skip_git_extended_diff (git_type))
					goto eof;
				if (git_type != GIT_DIFF_NORMAL) {
					char *git_old_name = NULL, *git_new_name = NULL;
					const char *p_stripped;
					int match;

					is_git_diff = 1;  /* Mark that we're processing a git diff */

					/* Extract filenames from git headers */
					if (extract_git_filenames (header, num_headers,
								 &git_old_name, &git_new_name, git_prefix_mode) == 0) {
						/* Increment file count for this git diff without hunks */
						filecount++;

						/* Use the best name for filtering */
						char *names[2] = { git_old_name, git_new_name };
						p = best_name (2, names);
						p_stripped = stripped (p, ignore_components);

						/* Apply include/exclude filters */
						match = !patlist_match(pat_exclude, p_stripped);
						if (match && pat_include != NULL)
							match = patlist_match(pat_include, p_stripped);

						/* Apply file number filter */
						if (match)
							match = file_matches();

						/* Print filename if in list mode and matches */
						if (match && !show_status && mode == mode_list)
							display_filename (start_linenum, status, p, patchname);

						/* Output headers if matching and in filter mode */
						if (match && mode == mode_filter) {
							unsigned int i;
							for (i = 0; i < num_headers; i++)
								fputs (header[i], stdout);
						}

						/* Set file existence based on git diff type */
						switch (git_type) {
						case GIT_DIFF_NEW_FILE:
							orig_file_exists = 0;
							new_file_exists = 1;
							break;
						case GIT_DIFF_DELETED_FILE:
							orig_file_exists = 1;
							new_file_exists = 0;
							break;
						default:
							orig_file_exists = 1;
							new_file_exists = 1;
							break;
						}

						/* Print filename with status if in list mode and matches */
						if (match && show_status && mode == mode_list) {
							if (!orig_file_exists)
								status = '+';
							else if (!new_file_exists)
								status = '-';

							display_filename (start_linenum, status, p, patchname);
						}

						/* Clean up */
						free (git_old_name);
						free (git_new_name);
					}
				}
				goto eof;
			}
		}

		if (is_context == -1) {
			/* Check if this is a git diff without hunks or with content like Binary files */
			enum git_diff_type git_type = detect_git_diff_type (header, num_headers);


			/* Skip extended diffs if in exclude mode */
			if (should_skip_git_extended_diff (git_type))
				goto flush_continue;
			if (git_type != GIT_DIFF_NORMAL) {
				/* This is a git diff without hunks - handle it */
				char *git_old_name = NULL, *git_new_name = NULL;
				const char *p_stripped;
				int match;
				int result;

				is_git_diff = 1;  /* Mark that we're processing a git diff */

				/* Extract filenames from git headers */
				if (extract_git_filenames (header, num_headers,
							 &git_old_name, &git_new_name, git_prefix_mode)) {
					/* Fallback to traditional method if git extraction fails */
					goto flush_continue;
				}

				/* Increment file count for this git diff without hunks */
				filecount++;

				/* Use the best name for filtering */
				char *names[2] = { git_old_name, git_new_name };
				p = best_name (2, names);
				p_stripped = stripped (p, ignore_components);

				/* Apply include/exclude filters */
				match = !patlist_match(pat_exclude, p_stripped);
				if (match && pat_include != NULL)
					match = patlist_match(pat_include, p_stripped);

				/* Apply file number filter */
				if (match)
					match = file_matches();

				/* Process the git diff (it will handle filename display) */
				result = do_git_diff_no_hunks (f, header, num_headers,
							     match, &line, &linelen, &linenum,
							     start_linenum, status, p, patchname,
							     &orig_file_exists, &new_file_exists,
							     git_type);

				/* Print filename with status if in list mode and matches */
				if (match && show_status && mode == mode_list) {
					if (!orig_file_exists)
						status = '+';
					else if (!new_file_exists)
						status = '-';

					display_filename (start_linenum, status, p, patchname);
				}

				/* Clean up */
				free (git_old_name);
				free (git_new_name);

				/* Handle result */
				if (result == EOF)
					goto eof;

				goto next_diff;
			} else {
				/* Not a git diff without hunks - use original logic */
				unsigned int i = 0;
			flush_continue:
				if (mode == mode_filter && (pat_exclude || verbose)
				    && !clean_comments) {
					for (i = 0; i < num_headers; i++)
						fputs (header[i], stdout);
				}
				for (i = 0; i < num_headers; i++) {
					free (header[i]);
					header[i] = NULL;
				}
				num_headers = 0;
				continue;
			}
		}

		if (is_git_patch (header, num_headers)) {
			names[0] = filename_from_header_with_git_prefix_mode (line + 4, git_prefix_mode);
		} else {
			names[0] = filename_from_header (line + 4);
		}
		if (mode != mode_filter && show_status)
			orig_file_exists = file_exists (names[0], line + 4 +
							strlen (names[0]));

		if (getline (&line, &linelen, f) == -1) {
			/* Show non-diff lines if excluding, or if
			 * in verbose mode, and if --clean isn't specified. */
			if (mode == mode_filter && (pat_exclude || verbose)
				&& !clean_comments)
				fputs (header[0], stdout);
			free (names[0]);
			goto eof;
		}
		linenum++;

		if (strncmp (line, is_context ? "--- " : "+++ ", 4)) {
			/* Show non-diff lines if excluding, or if
			 * in verbose mode, and if --clean isn't specified. */
			free (names[0]);
			goto flush_continue;
		}

		filecount++;
		header[num_headers++] = xstrdup (line);
		if (is_git_patch (header, num_headers)) {
			names[1] = filename_from_header_with_git_prefix_mode (line + 4, git_prefix_mode);
		} else {
			names[1] = filename_from_header (line + 4);
		}

		if (mode != mode_filter && show_status)
			new_file_exists = file_exists (names[1], line + 4 +
						       strlen (names[1]));

		// Decide whether this matches this pattern.
		p = best_name (2, names);
		p_stripped = stripped (p, ignore_components);

		match = !patlist_match(pat_exclude, p_stripped);
		if (match && pat_include != NULL)
			match = patlist_match(pat_include, p_stripped);

		// print if it matches.
		if (match && !show_status && mode == mode_list)
			display_filename (start_linenum, status,
					  p, patchname);

		if (is_context)
			do_diff = do_context;
		else
			do_diff = do_unified;

		result = do_diff (f, header, num_headers,
				  match, &line,
				  &linelen, &linenum,
				  start_linenum, status, p, patchname,
				  &orig_file_exists, &new_file_exists);

		// print if it matches.
		if (match && show_status && mode == mode_list) {
			if (!orig_file_exists)
				status = '+';
			else if (!new_file_exists)
				status = '-';

			display_filename (start_linenum, status,
					  p, patchname);
		}

		switch (result) {
		case EOF:
			free (names[0]);
			free (names[1]);
			goto eof;
		case 1:
			/* Found next diff - line variable contains the start of next diff */
			free (names[0]);
			free (names[1]);
			for (i = 0; i < num_headers; i++) {
				free (header[i]);
				header[i] = NULL;
			}
			num_headers = 0;
			/* Continue processing with the current line (don't call getline) */
			continue;
		}

	next_diff:
		/* Only free names if we're not processing a git diff (which handles its own cleanup) */
		if (!is_git_diff) {
			for (i = 0; i < 2; i++)
				free (names[i]);
		}
		for (i = 0; i < num_headers; i++) {
			free (header[i]);
			header[i] = NULL;
		}
		num_headers = 0;
	}

 eof:
	for (i = 0; i < num_headers; i++)
		if (header[i])
			free (header[i]);

	if (line)
		free (line);

	return 0;
}

const char * syntax_str =
"Options:\n"
"  -x PAT, --exclude=PAT\n"
"            exclude files matching PAT\n"
"  -X FILE, --exclude-from-file=FILE\n"
"            exclude files that match any pattern in FILE\n"
"  -i PAT, --include=PAT\n"
"            include only files matching PAT\n"
"  -I FILE, --include-from-file=FILE\n"
"            include only files that match any pattern in FILE\n"
"  -# H, --hunks=H\n"
"            include only hunks in range H, if range begins with x show all excluding range H\n"
"  --lines=L include only hunks with (original) lines in range L, if range begins with x show all excluding range L\n"
"  -F F, --files=F\n"
"            include only files in range F, if range begins with x show all excluding range F\n"
"  --annotate {filterdiff, patchview, grepdiff}\n"
"            annotate each hunk with the filename and hunk number {filterdiff, patchview, grepdiff}\n"
"  --as-numbered-lines=before|after|original-before|original-after {filterdiff, patchview, grepdiff}\n"
"            display lines as they would look before, or after, the patch is applied; {filterdiff, patchview, grepdiff}\n"
"            or with original line numbers from the diff (original-before or original-after) {filterdiff, patchview, grepdiff}\n"
"  --format=context|unified {filterdiff, patchview, grepdiff}\n"
"            set output format {filterdiff, patchview, grepdiff}\n"
"  --output-matching=hunk|file {grepdiff}\n"
"            show matching hunks or file-level diffs {grepdiff}\n"
"  --only-match=rem|removals|add|additions|mod|modifications|all {grepdiff}\n"
"            regex will only match removals, additions, modifications or {grepdiff}\n"
"            the whole hunk {grepdiff}\n"
"  --remove-timestamps {filterdiff, patchview, grepdiff}\n"
"            don't show timestamps from output {filterdiff, patchview, grepdiff}\n"
"  --clean {filterdiff}\n"
"            remove all comments (non-diff lines) from output {filterdiff}\n"
"  --in-place {filterdiff}\n"
"            write output to the original input files {filterdiff}\n"
"  -z, --decompress\n"
"            decompress .gz and .bz2 files\n"
"  -n, --line-number {lsdiff, grepdiff}\n"
"            show line numbers {lsdiff, grepdiff}\n"
"  -N, --number-files {lsdiff, grepdiff}\n"
"            show file numbers, for use with filterdiff's --files option {lsdiff, grepdiff}\n"
"  -H, --with-filename {lsdiff, grepdiff}\n"
"            show patch file names {lsdiff, grepdiff}\n"
"  -h, --no-filename {lsdiff, grepdiff}\n"
"            suppress patch file names {lsdiff, grepdiff}\n"
"  -p N, --strip-match=N\n"
"            initial pathname components to ignore\n"
"  --strip=N initial pathname components to strip\n"
"  --git-prefixes=strip|keep\n"
"            how to handle a/ and b/ prefixes in Git diffs for both filename\n"
"            matching (-i/-x) and output (default: keep)\n"
"  --git-extended-diffs=exclude|include\n"
"            process Git diffs without hunks: renames, copies, mode-only\n"
"            changes, binary files; default is exclude\n"
"  --addprefix=PREFIX\n"
"            prefix pathnames with PREFIX\n"
"  --addoldprefix=PREFIX\n"
"            prefix pathnames in old files with PREFIX\n"
"  --addnewprefix=PREFIX\n"
"            prefix pathnames in new files with PREFIX\n"
"  -s, --status {lsdiff, grepdiff}\n"
"            show file additions (+), removals (-), and modifications (!) {lsdiff, grepdiff}\n"
"  -v, --verbose\n"
"            verbose output -- use more than once for extra verbosity\n"
"  -E, --extended-regexp {grepdiff}\n"
#ifdef HAVE_PCRE2POSIX_H
"            this option has no effect as PCRE regexes are used by default {grepdiff}\n"
#else
"            use extended regexps, like egrep {grepdiff}\n"
#endif
"  -E, --empty-files-as-absent {lsdiff}\n"
"            treat empty files as absent {lsdiff}\n"
"  --empty-files-as-absent {grepdiff}\n"
"            treat empty files as absent {grepdiff}\n"
"  -f FILE, --file=FILE {grepdiff}\n"
"            read regular expressions from FILE {grepdiff}\n"
"  --filter  run as 'filterdiff' {grepdiff, patchview, lsdiff}\n"
"  --list    run as 'lsdiff' {filterdiff, patchview, grepdiff}\n"
"  --grep    run as 'grepdiff' {filterdiff, patchview, lsdiff}\n"
;

NORETURN
static void syntax (int err)
{
	char *s = xstrdup (syntax_str);
	const char *usage = "usage: %s [OPTION]... [files ...]\n";
	char *p, *next;
	if (mode == mode_grep)
		usage = "usage: %s [OPTION]... REGEX [files ...]\n";
	fprintf (err ? stderr : stdout, usage, progname);
	for (p = s; p && *p; p = next) {
		char *endp;
		next = strchr (p, '\n');
		if (!next)
			break;
		endp = next;
		*next++ = '\0';
		if (*--endp == '}') {
			char *begp = strrchr (p, '{');
			char *comma;
			if (!begp)
				break;
			*begp++ = '\0';
			*endp = '\0';
			do {
				comma = strchr (begp, ',');
				if (comma)
					*comma = '\0';
				if (!strcmp (progname, begp)) {
					puts (p);
					break;
				}
				if (comma)
					begp = (comma + 1 +
						strspn (comma + 1, " "));
			} while (comma);
		}
		else puts (p);
	}
	exit (err);
}

static void
parse_range (struct range **r, const char *rstr)
{
	unsigned long n;
	char *end;

	if (*rstr == '-')
		n = -1UL;
	else {
		n = strtoul (rstr, &end, 0);
		if (rstr == end) {
			if (*end)
				error (EXIT_FAILURE, 0,
				       "not understood: '%s'", end);
			else
				error (EXIT_FAILURE, 0,
				       "missing number in range list");

			*r = NULL;
			return;
		}

		rstr = end;
	}

	*r = xmalloc (sizeof **r);
	(*r)->start = (*r)->end = n;
	(*r)->next = NULL;
	if (*rstr == '-') {
		rstr++;
		n = strtoul (rstr, &end, 0);
		if (rstr == end)
			n = -1UL;

		(*r)->end = n;
		rstr = end;

		if ((*r)->start != -1UL && (*r)->start > (*r)->end)
			error (EXIT_FAILURE, 0, "invalid range: %lu-%lu",
			       (*r)->start, (*r)->end);
	}

	if (*rstr == ',')
		parse_range (&(*r)->next, rstr + 1);
	else if (*rstr != '\0')
		error (EXIT_FAILURE, 0, "not understood: '%s'", rstr);
}

static void set_list (void)
{
	/* This is lsdiff. */
	set_progname ("lsdiff");
	mode = mode_list;
}

static void set_filter (void)
{
	/* This is filterdiff. */
	set_progname ("filterdiff");
	mode = mode_filter;
}

static void set_view (void)
{
	/* This is patchview. */
	set_progname ("patchview");
	mode = mode_filter;
}

static void set_grep (void)
{
	/* This is grepdiff. */
	set_progname ("grepdiff");
	mode = mode_grep;
}

static void determine_mode_from_name (const char *argv0)
{
	/* This is filterdiff, unless it is named 'lsdiff', 'grepdiff' or 'patchview'. */
	const char *p = strrchr (argv0, '/');
	if (!p++)
		p = argv0;
	if (strstr (p, "lsdiff"))
		set_list ();
	else if (strstr (p, "grepdiff"))
		set_grep ();
	else if (strstr (p, "patchview")) {
		set_view ();
	}
	else
		set_filter ();
}

static FILE *convert_format (FILE *f, char format)
{
	switch (format) {
	default:
		break;
	case 'c':
		f = convert_to_context (f, "rb", 0);
		break;
	case 'u':
		f = convert_to_unified (f, "rb", 0);
		break;
	}

	return f;
}

static int
read_regex_file (const char *file)
{
	FILE *f = fopen (file, "r");
	char *line = NULL;
	size_t linelen = 0;
	ssize_t got;
	int err;

	if (!f)
		error (EXIT_FAILURE, errno, "cannot open %s", file);

	while ((got = getline (&line, &linelen, f)) > 0) {
		if (line[--got] == '\n')
			line[got] = '\0';

		regex = xrealloc (regex, ++num_regex * sizeof (regex[0]));
		err = regcomp (&regex[num_regex - 1], line,
			       REG_NOSUB | egrepping);
		if (err) {
			char errstr[300];
			regerror (err, &regex[num_regex - 1], errstr,
				  sizeof (errstr));
			error (EXIT_FAILURE, 0, "%s", errstr);
			exit (1);
		}
	}

	free (line);
	return fclose (f);
}

int main (int argc, char *argv[])
{
	int i;
	FILE *f = stdin;
	char format = '\0';
	int regex_file_specified = 0;
	int have_switches = 0;
	int inplace_mode = 0;

	setlocale (LC_TIME, "C");
	determine_mode_from_name (argv[0]);
	while (1) {
		static struct option long_options[] = {
			{"help", 0, 0, 1000 + 'H'},
			{"version", 0, 0, 1000 + 'V'},
			{"verbose", 0, 0, 'v'},
			{"list", 0, 0, 'l'},
			{"filter", 0, 0, 1000 + 'f'},
			{"grep", 0, 0, 'g'},
			{"strip", 1, 0, 1000 + 'S'},
			{"addprefix", 1, 0, 1000 + 'A'},
			{"addoldprefix", 1, 0, 1000 + 'O'},
			{"addnewprefix", 1, 0, 1000 + 'N'},
			{"hunks", 1, 0, '#'},
			{"lines", 1, 0, 1000 + ':'},
			{"files", 1, 0, 'F'},
			{"as-numbered-lines", 1, 0, 1000 + 'L'},
			{"annotate", 0, 0, 1000 + 'a'},
			{"format", 1, 0, 1000 + 'F'},
			{"output-matching", 1, 0, 1000 + 'o'},
			{"only-match", 1, 0, 1000 + 'm'},
			{"remove-timestamps", 0, 0, 1000 + 'r'},
			{"with-filename", 0, 0, 'H'},
			{"no-filename", 0, 0, 'h'},
			{"empty-files-as-absent", 0, 0, 1000 + 'E'},
			{"number-files", 0, 0, 'N'},
			{"clean", 0, 0, 1000 + 'c'},
			{"strip-match", 1, 0, 'p'},
			{"include", 1, 0, 'i'},
			{"exclude", 1, 0, 'x'},
			{"include-from-file", 1, 0, 'I'},
			{"exclude-from-file", 1, 0, 'X'},
			{"decompress", 0, 0, 'z'},
			{"line-number", 0, 0, 'n'},
			{"strip-match", 1, 0, 'p'},
			{"status", 0, 0, 's'},
			{"extended-regexp", 0, 0, 'E'},
			{"empty-files-as-removed", 0, 0, 1000 + 'E'},
			{"file", 1, 0, 'f'},
			{"in-place", 0, 0, 1000 + 'w'},
			{"git-prefixes", 1, 0, 1000 + 'G'},
			{"git-extended-diffs", 1, 0, 1000 + 'D'},
			{0, 0, 0, 0}
		};
		char *end;
		int c = getopt_long (argc, argv, "vp:i:I:x:X:zns#:F:Ef:HhN",
				     long_options, NULL);
		if (c == -1)
			break;

		have_switches = 1;
		switch (c) {
		case 'g':
			set_grep ();
			break;
		case 1000 + 'f':
			set_filter ();
			break;
		case 'l':
			set_list ();
			break;
		case 'E':
			if (mode == mode_grep)
				egrepping = REG_EXTENDED;
			else if (mode == mode_list)
				empty_files_as_absent = 1;
			else syntax (1);
			break;
		case 1000 + 'E':
			/* Long form --empty-files-as-absent or --empty-files-as-removed */
			if (mode == mode_grep || mode == mode_list)
				empty_files_as_absent = 1;
			else syntax (1);
			break;
		case 'f':
			if (mode == mode_grep) {
				regex_file_specified = 1;
				read_regex_file (optarg);
			} else syntax (1);
			break;
		case 1000 + 'V':
			printf("%s - patchutils version %s\n", progname,
			       VERSION);
			exit(0);
		case 1000 + 'H':
			syntax (0);
			break;
		case 1000 + 'S':
			strip_components = strtoul (optarg, &end, 0);
			if (optarg == end)
				syntax (1);
			break;
		case 1000 + 'A':
			prefix_to_add = optarg;
			break;
		case 1000 + 'O':
			old_prefix_to_add = optarg;
			break;
		case 1000 + 'N':
			new_prefix_to_add = optarg;
			break;
		case 'p':
			ignore_components = strtoul (optarg, &end, 0);
			if (optarg == end)
				syntax (1);
			break;
		case 'x':
			patlist_add (&pat_exclude, optarg);
			break;
		case 'X':
			patlist_add_file (&pat_exclude, optarg);
			break;
		case 'i':
			patlist_add (&pat_include, optarg);
			break;
		case 'I':
			patlist_add_file (&pat_include, optarg);
			break;
		case 'z':
			unzip = 1;
			break;
		case 'n':
			numbering = 1;
			break;
		case 'N':
			number_files = 1;
			break;
		case 's':
			show_status = 1;
			break;
		case 'v':
			verbose++;
			if (numbering && verbose > 1)
				number_files = 1;
			break;
		case '#':
			if (hunks)
				syntax (1);
			if (*optarg == 'x') {
				hunks_exclude = 1;
				optarg = optarg + 1;
			}
			parse_range (&hunks, optarg);
			break;
		case 'H':
			if (mode == mode_list || mode == mode_grep)
				print_patchnames = 1;
			else syntax (1);
			break;
		case 'h':
			if (mode == mode_list || mode == mode_grep)
				print_patchnames = 0;
			else syntax (1);
			break;
		case 1000 + ':':
			if (lines)
				syntax (1);
			if (*optarg == 'x') {
				lines_exclude = 1;
				optarg = optarg + 1;
			}
			parse_range (&lines, optarg);
			break;
		case 'F':
			if (files)
				syntax (1);
			if (*optarg == 'x') {
				files_exclude = 1;
				optarg = optarg + 1;
			}
			parse_range (&files, optarg);
			break;
		case 1000 + 'L':
			if (!strcmp (optarg, "before"))
				number_lines = Before;
			else if (!strcmp (optarg, "after"))
				number_lines = After;
			else if (!strcmp (optarg, "original-before"))
				number_lines = OriginalBefore;
			else if (!strcmp (optarg, "original-after"))
				number_lines = OriginalAfter;
			else syntax (1);
			break;
		case 1000 + 'a':
			if (mode == mode_list)
				syntax (1);
			annotating = 1;
			break;
		case 1000 + 'F':
			if (!strcmp (optarg, "context") && !format)
				format = 'c';
			else if (!strcmp (optarg, "unified") && !format)
				format = 'u';
			else syntax (1);
			break;
		case 1000 + 'o':
			if (!strncmp (optarg, "hunk", 4))
				output_matching = output_hunk;
			else if (!strncmp (optarg, "file", 4))
				output_matching = output_file;
			else syntax (1);
			break;
		case 1000 + 'm':
			if (!strncmp (optarg, "all", 3))
				only_matching = only_match_all;
			else if (!strncmp (optarg, "rem", 3) ||
				 !strncmp (optarg, "removals", 8))
				only_matching = only_match_rem;
			else if (!strncmp (optarg, "add", 3) ||
				 !strncmp (optarg, "additions", 9))
				only_matching = only_match_add;
			else if (!strncmp (optarg, "mod", 3) ||
				 !strncmp (optarg, "modifications", 13))
				only_matching = only_match_mod;
			else syntax (1);
			break;
		case 1000 + 'r':
			removing_timestamp = 1;
			break;
		case 1000 + 'c':
			clean_comments = 1;
			break;
		case 1000 + 'w':
			inplace_mode = 1;
			break;
		case 1000 + 'G':
			if (!strcmp(optarg, "strip")) {
				git_prefix_mode = GIT_PREFIX_STRIP;
			} else if (!strcmp(optarg, "keep")) {
				git_prefix_mode = GIT_PREFIX_KEEP;
			} else {
				error(EXIT_FAILURE, 0, "invalid argument to --git-prefixes: %s (expected 'strip' or 'keep')", optarg);
			}
			break;
		case 1000 + 'D':
			if (!strcmp(optarg, "exclude")) {
				git_extended_diffs_mode = GIT_EXTENDED_DIFFS_EXCLUDE;
			} else if (!strcmp(optarg, "include")) {
				git_extended_diffs_mode = GIT_EXTENDED_DIFFS_INCLUDE;
			} else {
				error(EXIT_FAILURE, 0, "invalid argument to --git-extended-diffs: %s (expected 'exclude' or 'include')", optarg);
			}
			break;
		default:
			syntax(1);
		}
	}
    if (have_switches == 0 && strcmp (progname, "patchview") == 0) {
	mode = mode_list;
	number_files = 1;
    }

	/* Preserve the old semantics of -p. */
	if (mode != mode_filter && ignore_components && !strip_components &&
	    !pat_include && !pat_exclude) {
		fprintf (stderr,
			 "-p given without -i or -x; guessing that you "
			 "meant --strip instead.\n");
		strip_components = ignore_components;
		ignore_components = 0;
	}

	if (mode != mode_grep && output_matching != output_none)
		error (EXIT_FAILURE, 0, "--output-matching only applies to "
		       "grep mode");

	if (mode != mode_grep && only_matching != only_match_all)
		error (EXIT_FAILURE, 0, "--only-match only applies to "
		       "grep mode");

	if (numbering &&
	    !(mode == mode_list ||
	      (mode == mode_grep && output_matching == output_none)))
		error (EXIT_FAILURE, 0, "-n only applies to list mode");

	if (mode != mode_filter &&
	    output_matching == output_none &&
	    number_lines != None)
		error (EXIT_FAILURE, 0, "--as-numbered-lines is "
		       "inappropriate in this context");

	if (mode == mode_filter &&
	    verbose && clean_comments)
		error (EXIT_FAILURE, 0, "can't use --verbose and "
		       "--clean options simultaneously");

	if (inplace_mode && unzip)
		error (EXIT_FAILURE, 0,
		       "--in-place and --decompress are mutually exclusive");

	if (inplace_mode && mode != mode_filter)
		error (EXIT_FAILURE, 0,
		       "--in-place only applies to filter mode");

	if (mode == mode_grep && !regex_file_specified) {
		int err;

		if (optind == argc)
			syntax (1);

		regex = xrealloc (regex, ++num_regex * sizeof (regex[0]));
		err = regcomp (&regex[num_regex - 1], argv[optind++],
			       REG_NOSUB | egrepping);
		if (err) {
			char errstr[300];
			regerror (err, &regex[num_regex - 1], errstr,
				  sizeof (errstr));
			error (EXIT_FAILURE, 0, "%s", errstr);
			exit (1);
		}
	}

	if (number_lines != None ||
	    output_matching != output_none) {
		if (print_patchnames == 1)
			error (EXIT_FAILURE, 0,
			       "-H is inappropriate in this context");
	} else if (print_patchnames == -1) {
		if ((mode == mode_list || mode == mode_grep) &&
		    optind + 1 < argc)
			print_patchnames = 1;
		else
			print_patchnames = 0;
	}

	if (inplace_mode && optind == argc)
		error (EXIT_FAILURE, 0,
		       "--in-place cannot be used with standard input");

	if (optind == argc) {
		f = convert_format (stdin, format);
		filterdiff (f, "(standard input)");
		fclose (f);
	} else {
		for (i = optind; i < argc; i++) {
			if (unzip) {
				f = xopen_unzip (argv[i], "rb");
			} else {
				f = xopen(argv[i], "rbm");
			}

			f = convert_format (f, format);

			if (inplace_mode) {
				/* Redirect stdout to temporary file for in-place processing */
				FILE *temp_output = redirectfd(stdout);

				filterdiff (f, argv[i]);

				/* Write temp file contents back to original file */
				if (write_file_inplace(argv[i], temp_output) != 0) {
					error (EXIT_FAILURE, errno, "failed to write %s", argv[i]);
				}

				fclose (temp_output);
			} else {
				filterdiff (f, argv[i]);
			}

			fclose (f);
		}
	}

	return 0;
}

