1/*
2 * Copyright (C) 1986-2005 The Free Software Foundation, Inc.
3 *
4 * Portions Copyright (C) 1998-2005 Derek Price, Ximbiot <http://ximbiot.com>,
5 *                                  and others.
6 *
7 * Portions Copyright (C) 1992, Brian Berliner and Jeff Polk
8 * Portions Copyright (C) 1989-1992, Brian Berliner
9 *
10 * You may distribute under the terms of the GNU General Public License as
11 * specified in the README file that comes with the CVS source distribution.
12 *
13 * Difference
14 *
15 * Run diff against versions in the repository.  Options that are specified are
16 * passed on directly to "rcsdiff".
17 *
18 * Without any file arguments, runs diff against all the currently modified
19 * files.
20 *
21 * $FreeBSD$
22 */
23
24#include <assert.h>
25#include "cvs.h"
26
27enum diff_file
28{
29    DIFF_ERROR,
30    DIFF_ADDED,
31    DIFF_REMOVED,
32    DIFF_DIFFERENT,
33    DIFF_SAME
34};
35
36static Dtype diff_dirproc PROTO ((void *callerdat, const char *dir,
37                                  const char *pos_repos,
38                                  const char *update_dir,
39                                  List *entries));
40static int diff_filesdoneproc PROTO ((void *callerdat, int err,
41                                      const char *repos,
42                                      const char *update_dir,
43                                      List *entries));
44static int diff_dirleaveproc PROTO ((void *callerdat, const char *dir,
45                                     int err, const char *update_dir,
46                                     List *entries));
47static enum diff_file diff_file_nodiff PROTO(( struct file_info *finfo,
48					       Vers_TS *vers,
49					       enum diff_file,
50					       char **rev1_cache ));
51static int diff_fileproc PROTO ((void *callerdat, struct file_info *finfo));
52static void diff_mark_errors PROTO((int err));
53
54
55/* Global variables.  Would be cleaner if we just put this stuff in a
56   struct like log.c does.  */
57
58/* Command line tags, from -r option.  Points into argv.  */
59static char *diff_rev1, *diff_rev2;
60/* Command line dates, from -D option.  Malloc'd.  */
61static char *diff_date1, *diff_date2;
62static char *diff_join1, *diff_join2;
63static char *use_rev1, *use_rev2;
64static int have_rev1_label, have_rev2_label;
65
66/* Revision of the user file, if it is unchanged from something in the
67   repository and we want to use that fact.  */
68static char *user_file_rev;
69
70static char *options;
71static char **diff_argv;
72static int diff_argc;
73static size_t diff_arg_allocated;
74static int diff_errors;
75static int empty_files = 0;
76
77static const char *const diff_usage[] =
78{
79    "Usage: %s %s [-lR] [-k kopt] [format_options]\n",
80    "    [[-r rev1 | -D date1] [-r rev2 | -D date2]] [files...] \n",
81    "\t-l\tLocal directory only, not recursive\n",
82    "\t-R\tProcess directories recursively.\n",
83    "\t-k kopt\tSpecify keyword expansion mode.\n",
84    "\t-D d1\tDiff revision for date against working file.\n",
85    "\t-D d2\tDiff rev1/date1 against date2.\n",
86    "\t-r rev1\tDiff revision for rev1 against working file.\n",
87    "\t-r rev2\tDiff rev1/date1 against rev2.\n",
88    "\nformat_options:\n",
89    "  -i  --ignore-case  Consider upper- and lower-case to be the same.\n",
90    "  -w  --ignore-all-space  Ignore all white space.\n",
91    "  -b  --ignore-space-change  Ignore changes in the amount of white space.\n",
92    "  -B  --ignore-blank-lines  Ignore changes whose lines are all blank.\n",
93    "  -I RE  --ignore-matching-lines=RE  Ignore changes whose lines all match RE.\n",
94    "  --binary  Read and write data in binary mode.\n",
95    "  -a  --text  Treat all files as text.\n\n",
96    "  -c  -C NUM  --context[=NUM]  Output NUM (default 2) lines of copied context.\n",
97    "  -u  -U NUM  --unified[=NUM]  Output NUM (default 2) lines of unified context.\n",
98    "    -NUM  Use NUM context lines.\n",
99    "    -L LABEL  --label LABEL  Use LABEL instead of file name.\n",
100    "    -p  --show-c-function  Show which C function each change is in.\n",
101    "    -F RE  --show-function-line=RE  Show the most recent line matching RE.\n",
102    "  --brief  Output only whether files differ.\n",
103    "  -e  --ed  Output an ed script.\n",
104    "  -f  --forward-ed  Output something like an ed script in forward order.\n",
105    "  -n  --rcs  Output an RCS format diff.\n",
106    "  -y  --side-by-side  Output in two columns.\n",
107    "    -W NUM  --width=NUM  Output at most NUM (default 130) characters per line.\n",
108    "    --left-column  Output only the left column of common lines.\n",
109    "    --suppress-common-lines  Do not output common lines.\n",
110    "  --ifdef=NAME  Output merged file to show `#ifdef NAME' diffs.\n",
111    "  --GTYPE-group-format=GFMT  Similar, but format GTYPE input groups with GFMT.\n",
112    "  --line-format=LFMT  Similar, but format all input lines with LFMT.\n",
113    "  --LTYPE-line-format=LFMT  Similar, but format LTYPE input lines with LFMT.\n",
114    "    LTYPE is `old', `new', or `unchanged'.  GTYPE is LTYPE or `changed'.\n",
115    "    GFMT may contain:\n",
116    "      %%<  lines from FILE1\n",
117    "      %%>  lines from FILE2\n",
118    "      %%=  lines common to FILE1 and FILE2\n",
119    "      %%[-][WIDTH][.[PREC]]{doxX}LETTER  printf-style spec for LETTER\n",
120    "        LETTERs are as follows for new group, lower case for old group:\n",
121    "          F  first line number\n",
122    "          L  last line number\n",
123    "          N  number of lines = L-F+1\n",
124    "          E  F-1\n",
125    "          M  L+1\n",
126    "    LFMT may contain:\n",
127    "      %%L  contents of line\n",
128    "      %%l  contents of line, excluding any trailing newline\n",
129    "      %%[-][WIDTH][.[PREC]]{doxX}n  printf-style spec for input line number\n",
130    "    Either GFMT or LFMT may contain:\n",
131    "      %%%%  %%\n",
132    "      %%c'C'  the single character C\n",
133    "      %%c'\\OOO'  the character with octal code OOO\n\n",
134    "  -t  --expand-tabs  Expand tabs to spaces in output.\n",
135    "  -T  --initial-tab  Make tabs line up by prepending a tab.\n\n",
136    "  -N  --new-file  Treat absent files as empty.\n",
137    "  -s  --report-identical-files  Report when two files are the same.\n",
138    "  --horizon-lines=NUM  Keep NUM lines of the common prefix and suffix.\n",
139    "  -d  --minimal  Try hard to find a smaller set of changes.\n",
140    "  -H  --speed-large-files  Assume large files and many scattered small changes.\n",
141    "\n(Specify the --help global option for a list of other help options)\n",
142    NULL
143};
144
145/* I copied this array directly out of diff.c in diffutils 2.7, after
146   removing the following entries, none of which seem relevant to use
147   with CVS:
148     --help
149     --version (-v)
150     --recursive (-r)
151     --unidirectional-new-file (-P)
152     --starting-file (-S)
153     --exclude (-x)
154     --exclude-from (-X)
155     --sdiff-merge-assist
156     --paginate (-l)  (doesn't work with library callbacks)
157
158   I changed the options which take optional arguments (--context and
159   --unified) to return a number rather than a letter, so that the
160   optional argument could be handled more easily.  I changed the
161   --brief and --ifdef options to return numbers, since -q  and -D mean
162   something else to cvs diff.
163
164   The numbers 129- that appear in the fourth element of some entries
165   tell the big switch in `diff' how to process those options. -- Ian
166
167   The following options, which diff lists as "An alias, no longer
168   recommended" have been removed: --file-label --entire-new-file
169   --ascii --print.  */
170
171static struct option const longopts[] =
172{
173    {"ignore-blank-lines", 0, 0, 'B'},
174    {"context", 2, 0, 143},
175    {"ifdef", 1, 0, 131},
176    {"show-function-line", 1, 0, 'F'},
177    {"speed-large-files", 0, 0, 'H'},
178    {"ignore-matching-lines", 1, 0, 'I'},
179    {"label", 1, 0, 'L'},
180    {"new-file", 0, 0, 'N'},
181    {"initial-tab", 0, 0, 'T'},
182    {"width", 1, 0, 'W'},
183    {"text", 0, 0, 'a'},
184    {"ignore-space-change", 0, 0, 'b'},
185    {"minimal", 0, 0, 'd'},
186    {"ed", 0, 0, 'e'},
187    {"forward-ed", 0, 0, 'f'},
188    {"ignore-case", 0, 0, 'i'},
189    {"rcs", 0, 0, 'n'},
190    {"show-c-function", 0, 0, 'p'},
191
192    /* This is a potentially very useful option, except the output is so
193       silly.  It would be much better for it to look like "cvs rdiff -s"
194       which displays all the same info, minus quite a few lines of
195       extraneous garbage.  */
196    {"brief", 0, 0, 145},
197
198    {"report-identical-files", 0, 0, 's'},
199    {"expand-tabs", 0, 0, 't'},
200    {"ignore-all-space", 0, 0, 'w'},
201    {"side-by-side", 0, 0, 'y'},
202    {"unified", 2, 0, 146},
203    {"left-column", 0, 0, 129},
204    {"suppress-common-lines", 0, 0, 130},
205    {"old-line-format", 1, 0, 132},
206    {"new-line-format", 1, 0, 133},
207    {"unchanged-line-format", 1, 0, 134},
208    {"line-format", 1, 0, 135},
209    {"old-group-format", 1, 0, 136},
210    {"new-group-format", 1, 0, 137},
211    {"unchanged-group-format", 1, 0, 138},
212    {"changed-group-format", 1, 0, 139},
213    {"horizon-lines", 1, 0, 140},
214    {"binary", 0, 0, 142},
215    {0, 0, 0, 0}
216};
217
218
219
220/* Add one of OPT or LONGOPT, and ARGUMENT, when present, to global DIFF_ARGV.
221 *
222 * INPUTS
223 *   opt		A character option representation.
224 *   longopt		A long option name.
225 *   argument		Optional option argument.
226 *
227 * GLOBALS
228 *   diff_argc		The number of arguments in DIFF_ARGV.
229 *   diff_argv		Array of argument strings.
230 *   diff_arg_allocated	Allocated length of DIFF_ARGV.
231 *
232 * NOTES
233 *   Behavior when both OPT & LONGOPT are provided is undefined.
234 *
235 * RETURNS
236 *   Nothing.
237 */
238static void
239add_diff_args (char opt, const char *longopt, const char *argument)
240{
241    char *tmp;
242
243    /* Add opt or longopt to diff_arv.  */
244    assert (opt || (longopt && *longopt));
245    assert (!(opt && (longopt && *longopt)));
246    if (opt)
247    {
248	tmp = xmalloc (3);
249	sprintf (tmp, "-%c", opt);
250    }
251    else
252    {
253	tmp = xmalloc (3 + strlen (longopt));
254	sprintf (tmp, "--%s", longopt);
255    }
256    run_add_arg_p (&diff_argc, &diff_arg_allocated, &diff_argv, tmp);
257    free (tmp);
258
259    /* When present, add ARGUMENT to DIFF_ARGV.  */
260    if (argument)
261	run_add_arg_p (&diff_argc, &diff_arg_allocated, &diff_argv, argument);
262}
263
264
265
266/* CVS 1.9 and similar versions seemed to have pretty weird handling
267   of -y and -T.  In the cases where it called rcsdiff,
268   they would have the meanings mentioned below.  In the cases where it
269   called diff, they would have the meanings mentioned in "longopts".
270   Noone seems to have missed them, so I think the right thing to do is
271   just to remove the options altogether (which I have done).
272
273   In the case of -z and -q, "cvs diff" did not accept them even back
274   when we called rcsdiff (at least, it hasn't accepted them
275   recently).
276
277   In comparing rcsdiff to the new CVS implementation, I noticed that
278   the following rcsdiff flags are not handled by CVS diff:
279
280	   -y: perform diff even when the requested revisions are the
281		   same revision number
282	   -q: run quietly
283	   -T: preserve modification time on the RCS file
284	   -z: specify timezone for use in file labels
285
286   I think these are not really relevant.  -y is undocumented even in
287   RCS 5.7, and seems like a minor change at best.  According to RCS
288   documentation, -T only applies when a RCS file has been modified
289   because of lock changes; doesn't CVS sidestep RCS's entire lock
290   structure?  -z seems to be unsupported by CVS diff, and has a
291   different meaning as a global option anyway.  (Adding it could be
292   a feature, but if it is left out for now, it should not break
293   anything.)  For the purposes of producing output, CVS diff appears
294   mostly to ignore -q.  Maybe this should be fixed, but I think it's
295   a larger issue than the changes included here.  */
296
297int
298diff (argc, argv)
299    int argc;
300    char **argv;
301{
302    int c, err = 0;
303    int local = 0;
304    int which;
305    int option_index;
306
307    if (argc == -1)
308	usage (diff_usage);
309
310    have_rev1_label = have_rev2_label = 0;
311
312    /*
313     * Note that we catch all the valid arguments here, so that we can
314     * intercept the -r arguments for doing revision diffs; and -l/-R for a
315     * non-recursive/recursive diff.
316     */
317
318    /* Clean out our global variables (multiroot can call us multiple
319       times and the server can too, if the client sends several
320       diff commands).  */
321    if (diff_argc)
322    {
323	run_arg_free_p (diff_argc, diff_argv);
324	diff_argc = 0;
325    }
326    diff_rev1 = NULL;
327    diff_rev2 = NULL;
328    diff_date1 = NULL;
329    diff_date2 = NULL;
330    diff_join1 = NULL;
331    diff_join2 = NULL;
332
333    optind = 0;
334    /* FIXME: This should really be allocating an argv to be passed to diff
335     * later rather than strcatting onto the opts variable.  We have some
336     * handling routines that can already handle most of the argc/argv
337     * maintenance for us and currently, if anyone were to attempt to pass a
338     * quoted string in here, it would be split on spaces and tabs on its way
339     * to diff.
340     */
341    while ((c = getopt_long (argc, argv,
342	       "+abcdefhilnpstuwy0123456789BHNRTC:D:F:I:L:U:W:k:r:j:",
343			     longopts, &option_index)) != -1)
344    {
345	switch (c)
346	{
347	    case 'y':
348		add_diff_args (0, "side-by-side", NULL);
349		break;
350	    case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
351	    case 'h': case 'i': case 'n': case 'p': case 's': case 't':
352	    case 'u': case 'w':
353            case '0': case '1': case '2': case '3': case '4': case '5':
354            case '6': case '7': case '8': case '9':
355	    case 'B': case 'H': case 'T':
356		add_diff_args (c, NULL, NULL);
357		break;
358	    case 'L':
359		if (have_rev1_label++)
360		    if (have_rev2_label++)
361		    {
362			error (0, 0, "extra -L arguments ignored");
363			break;
364		    }
365		/* Fall through.  */
366	    case 'C': case 'F': case 'I': case 'U': case 'W':
367		add_diff_args (c, NULL, optarg);
368		break;
369	    case 129: case 130: case 131: case 132: case 133: case 134:
370	    case 135: case 136: case 137: case 138: case 139: case 140:
371	    case 141: case 142: case 143: case 145: case 146:
372		add_diff_args (0, longopts[option_index].name,
373			      longopts[option_index].has_arg ? optarg : NULL);
374		break;
375	    case 'R':
376		local = 0;
377		break;
378	    case 'l':
379		local = 1;
380		break;
381	    case 'k':
382		if (options)
383		    free (options);
384		options = RCS_check_kflag (optarg);
385		break;
386	    case 'j':
387		{
388		    char *ptr;
389		    char *cpy = strdup(optarg);
390
391		    if ((ptr = strchr(optarg, ':')) != NULL)
392			*ptr++ = 0;
393		    if (diff_rev2 != NULL || diff_date2 != NULL)
394			error (1, 0,
395			   "no more than two revisions/dates can be specified");
396		    if (diff_rev1 != NULL || diff_date1 != NULL) {
397			diff_join2 = cpy;
398			diff_rev2 = optarg;
399			diff_date2 = ptr ? Make_Date(ptr) : NULL;
400		    } else {
401			diff_join1 = cpy;
402			diff_rev1 = optarg;
403			diff_date1 = ptr ? Make_Date(ptr) : NULL;
404		    }
405		}
406		break;
407	    case 'r':
408		if (diff_rev2 != NULL || diff_date2 != NULL)
409		    error (1, 0,
410		       "no more than two revisions/dates can be specified");
411		if (diff_rev1 != NULL || diff_date1 != NULL)
412		    diff_rev2 = optarg;
413		else
414		    diff_rev1 = optarg;
415		break;
416	    case 'D':
417		if (diff_rev2 != NULL || diff_date2 != NULL)
418		    error (1, 0,
419		       "no more than two revisions/dates can be specified");
420		if (diff_rev1 != NULL || diff_date1 != NULL)
421		    diff_date2 = Make_Date (optarg);
422		else
423		    diff_date1 = Make_Date (optarg);
424		break;
425	    case 'N':
426		empty_files = 1;
427		break;
428	    case '?':
429	    default:
430		usage (diff_usage);
431		break;
432	}
433    }
434    argc -= optind;
435    argv += optind;
436
437    /* make sure options is non-null */
438    if (!options)
439	options = xstrdup ("");
440
441#ifdef CLIENT_SUPPORT
442    if (current_parsed_root->isremote) {
443	/* We're the client side.  Fire up the remote server.  */
444	start_server ();
445
446	ign_setup ();
447
448	if (local)
449	    send_arg("-l");
450	if (empty_files)
451	    send_arg("-N");
452	send_options (diff_argc, diff_argv);
453	if (options[0] != '\0')
454	    send_arg (options);
455	if (diff_join1)
456	    option_with_arg ("-j", diff_join1);
457	else if (diff_rev1)
458	    option_with_arg ("-r", diff_rev1);
459	else if (diff_date1)
460	    client_senddate (diff_date1);
461
462	if (diff_join2)
463	    option_with_arg ("-j", diff_join2);
464	else if (diff_rev2)
465	    option_with_arg ("-r", diff_rev2);
466	else if (diff_date2)
467	    client_senddate (diff_date2);
468	send_arg ("--");
469
470	/* Send the current files unless diffing two revs from the archive */
471	if (diff_rev2 == NULL && diff_date2 == NULL)
472	    send_files (argc, argv, local, 0, 0);
473	else
474	    send_files (argc, argv, local, 0, SEND_NO_CONTENTS);
475
476	send_file_names (argc, argv, SEND_EXPAND_WILD);
477
478	send_to_server ("diff\012", 0);
479        err = get_responses_and_close ();
480    } else
481#endif
482    { /* FreeBSD addition - warning idention not changed til matching-} */
483    if (diff_rev1 != NULL)
484	tag_check_valid (diff_rev1, argc, argv, local, 0, "");
485    if (diff_rev2 != NULL)
486	tag_check_valid (diff_rev2, argc, argv, local, 0, "");
487
488    which = W_LOCAL;
489    if (diff_rev1 != NULL || diff_date1 != NULL)
490	which |= W_REPOS | W_ATTIC;
491
492    wrap_setup ();
493
494    /* start the recursion processor */
495    err = start_recursion (diff_fileproc, diff_filesdoneproc, diff_dirproc,
496			   diff_dirleaveproc, NULL, argc, argv, local,
497			   which, 0, CVS_LOCK_READ, (char *) NULL, 1,
498			   (char *) NULL);
499    } /* FreeBSD addition */
500
501    /* clean up */
502    free (options);
503    options = NULL;
504
505    if (diff_date1 != NULL)
506	free (diff_date1);
507    if (diff_date2 != NULL)
508	free (diff_date2);
509    if (diff_join1 != NULL)
510	free (diff_join1);
511    if (diff_join2 != NULL)
512	free (diff_join2);
513
514    return (err);
515}
516
517/*
518 * Do a file diff
519 */
520/* ARGSUSED */
521static int
522diff_fileproc (callerdat, finfo)
523    void *callerdat;
524    struct file_info *finfo;
525{
526    int status, err = 2;		/* 2 == trouble, like rcsdiff */
527    Vers_TS *vers;
528    enum diff_file empty_file = DIFF_DIFFERENT;
529    char *tmp = NULL;
530    char *tocvsPath = NULL;
531    char *fname = NULL;
532    char *label1;
533    char *label2;
534    char *rev1_cache = NULL;
535
536    user_file_rev = 0;
537    vers = Version_TS (finfo, NULL, NULL, NULL, 1, 0);
538
539    if (diff_rev2 != NULL || diff_date2 != NULL)
540    {
541	/* Skip all the following checks regarding the user file; we're
542	   not using it.  */
543    }
544    else if (vers->vn_user == NULL)
545    {
546	/* The file does not exist in the working directory.  */
547	if ((diff_rev1 != NULL || diff_date1 != NULL)
548	    && vers->srcfile != NULL)
549	{
550	    /* The file does exist in the repository.  */
551	    if (empty_files)
552		empty_file = DIFF_REMOVED;
553	    else
554	    {
555		int exists;
556
557		exists = 0;
558		/* special handling for TAG_HEAD */
559		if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
560		{
561		    char *head =
562			(vers->vn_rcs == NULL
563			 ? NULL
564			 : RCS_branch_head (vers->srcfile, vers->vn_rcs));
565		    exists = head != NULL && !RCS_isdead(vers->srcfile, head);
566		    if (head != NULL)
567			free (head);
568		}
569		else
570		{
571		    Vers_TS *xvers;
572
573		    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1,
574					1, 0);
575		    exists = xvers->vn_rcs != NULL && !RCS_isdead(xvers->srcfile, xvers->vn_rcs);
576		    freevers_ts (&xvers);
577		}
578		if (exists)
579		    error (0, 0,
580			   "%s no longer exists, no comparison available",
581			   finfo->fullname);
582		goto out;
583	    }
584	}
585	else
586	{
587	    error (0, 0, "I know nothing about %s", finfo->fullname);
588	    goto out;
589	}
590    }
591    else if (vers->vn_user[0] == '0' && vers->vn_user[1] == '\0')
592    {
593	/* The file was added locally.  */
594	int exists = 0;
595
596	if (vers->srcfile != NULL)
597	{
598	    /* The file does exist in the repository.  */
599
600	    if ((diff_rev1 != NULL || diff_date1 != NULL))
601	    {
602		/* special handling for TAG_HEAD */
603		if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
604		{
605		    char *head =
606			(vers->vn_rcs == NULL
607			 ? NULL
608			 : RCS_branch_head (vers->srcfile, vers->vn_rcs));
609		    exists = head != NULL && !RCS_isdead(vers->srcfile, head);
610		    if (head != NULL)
611			free (head);
612		}
613		else
614		{
615		    Vers_TS *xvers;
616
617		    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1,
618					1, 0);
619		    exists = xvers->vn_rcs != NULL
620		             && !RCS_isdead (xvers->srcfile, xvers->vn_rcs);
621		    freevers_ts (&xvers);
622		}
623	    }
624	    else
625	    {
626		/* The file was added locally, but an RCS archive exists.  Our
627		 * base revision must be dead.
628		 */
629		/* No need to set, exists = 0, here.  That's the default.  */
630	    }
631	}
632	if (!exists)
633	{
634	    /* If we got here, then either the RCS archive does not exist or
635	     * the relevant revision is dead.
636	     */
637	    if (empty_files)
638		empty_file = DIFF_ADDED;
639	    else
640	    {
641		error (0, 0, "%s is a new entry, no comparison available",
642		       finfo->fullname);
643		goto out;
644	    }
645	}
646    }
647    else if (vers->vn_user[0] == '-')
648    {
649	if (empty_files)
650	    empty_file = DIFF_REMOVED;
651	else
652	{
653	    error (0, 0, "%s was removed, no comparison available",
654		   finfo->fullname);
655	    goto out;
656	}
657    }
658    else
659    {
660	if (vers->vn_rcs == NULL && vers->srcfile == NULL)
661	{
662	    error (0, 0, "cannot find revision control file for %s",
663		   finfo->fullname);
664	    goto out;
665	}
666	else
667	{
668	    if (vers->ts_user == NULL)
669	    {
670		error (0, 0, "cannot find %s", finfo->fullname);
671		goto out;
672	    }
673	    else if (!strcmp (vers->ts_user, vers->ts_rcs))
674	    {
675		/* The user file matches some revision in the repository
676		   Diff against the repository (for remote CVS, we might not
677		   have a copy of the user file around).  */
678		user_file_rev = vers->vn_user;
679	    }
680	}
681    }
682
683    empty_file = diff_file_nodiff( finfo, vers, empty_file, &rev1_cache );
684    if( empty_file == DIFF_SAME )
685    {
686	/* In the server case, would be nice to send a "Checked-in"
687	   response, so that the client can rewrite its timestamp.
688	   server_checked_in by itself isn't the right thing (it
689	   needs a server_register), but I'm not sure what is.
690	   It isn't clear to me how "cvs status" handles this (that
691	   is, for a client which sends Modified not Is-modified to
692	   "cvs status"), but it does.  */
693	err = 0;
694	goto out;
695    }
696    else if( empty_file == DIFF_ERROR )
697	goto out;
698
699    /* Output an "Index:" line for patch to use */
700    cvs_output ("Index: ", 0);
701    cvs_output (finfo->fullname, 0);
702    cvs_output ("\n", 1);
703
704    tocvsPath = wrap_tocvs_process_file(finfo->file);
705    if( tocvsPath != NULL )
706    {
707	/* Backup the current version of the file to CVS/,,filename */
708	fname = xmalloc (strlen (finfo->file)
709			 + sizeof CVSADM
710			 + sizeof CVSPREFIX
711			 + 10);
712	sprintf(fname,"%s/%s%s",CVSADM, CVSPREFIX, finfo->file);
713	if (unlink_file_dir (fname) < 0)
714	    if (! existence_error (errno))
715		error (1, errno, "cannot remove %s", fname);
716	rename_file (finfo->file, fname);
717	/* Copy the wrapped file to the current directory then go to work */
718	copy_file (tocvsPath, finfo->file);
719    }
720
721    /* Set up file labels appropriate for compatibility with the Larry Wall
722     * implementation of patch if the user didn't specify.  This is irrelevant
723     * according to the POSIX.2 specification.
724     */
725    label1 = NULL;
726    label2 = NULL;
727    if (!have_rev1_label)
728    {
729	if (empty_file == DIFF_ADDED)
730	    label1 =
731		make_file_label (DEVNULL, NULL, NULL);
732	else
733	    label1 =
734                make_file_label (finfo->fullname, use_rev1,
735                                 vers ? vers->srcfile : NULL);
736    }
737
738    if (!have_rev2_label)
739    {
740	if (empty_file == DIFF_REMOVED)
741	    label2 =
742		make_file_label (DEVNULL, NULL, NULL);
743	else
744	    label2 =
745                make_file_label (finfo->fullname, use_rev2,
746                                 vers ? vers->srcfile : NULL);
747    }
748
749    if (empty_file == DIFF_ADDED || empty_file == DIFF_REMOVED)
750    {
751	/* This is fullname, not file, possibly despite the POSIX.2
752	 * specification, because that's the way all the Larry Wall
753	 * implementations of patch (are there other implementations?) want
754	 * things and the POSIX.2 spec appears to leave room for this.
755	 */
756	cvs_output ("\
757===================================================================\n\
758RCS file: ", 0);
759	cvs_output (finfo->fullname, 0);
760	cvs_output ("\n", 1);
761
762	cvs_output ("diff -N ", 0);
763	cvs_output (finfo->fullname, 0);
764	cvs_output ("\n", 1);
765
766	if (empty_file == DIFF_ADDED)
767	{
768	    if (use_rev2 == NULL)
769                status = diff_exec (DEVNULL, finfo->file, label1, label2,
770				    diff_argc, diff_argv, RUN_TTY);
771	    else
772	    {
773		int retcode;
774
775		tmp = cvs_temp_name ();
776		retcode = RCS_checkout (vers->srcfile, (char *) NULL,
777					use_rev2, (char *) NULL,
778					(*options
779					 ? options
780					 : vers->options),
781					tmp, (RCSCHECKOUTPROC) NULL,
782					(void *) NULL);
783		if( retcode != 0 )
784		    goto out;
785
786		status = diff_exec (DEVNULL, tmp, label1, label2,
787				    diff_argc, diff_argv, RUN_TTY);
788	    }
789	}
790	else
791	{
792	    int retcode;
793
794	    tmp = cvs_temp_name ();
795	    retcode = RCS_checkout (vers->srcfile, (char *) NULL,
796				    use_rev1, (char *) NULL,
797				    *options ? options : vers->options,
798				    tmp, (RCSCHECKOUTPROC) NULL,
799				    (void *) NULL);
800	    if (retcode != 0)
801		goto out;
802
803	    status = diff_exec (tmp, DEVNULL, label1, label2,
804				diff_argc, diff_argv, RUN_TTY);
805	}
806    }
807    else
808    {
809	status = RCS_exec_rcsdiff (vers->srcfile, diff_argc, diff_argv,
810                                   *options ? options : vers->options,
811                                   use_rev1, rev1_cache, use_rev2,
812                                   label1, label2, finfo->file);
813
814    }
815
816    if (label1) free (label1);
817    if (label2) free (label2);
818
819    switch (status)
820    {
821	case -1:			/* fork failed */
822	    error (1, errno, "fork failed while diffing %s",
823		   vers->srcfile->path);
824	case 0:				/* everything ok */
825	    err = 0;
826	    break;
827	default:			/* other error */
828	    err = status;
829	    break;
830    }
831
832out:
833    if( tocvsPath != NULL )
834    {
835	if (unlink_file_dir (finfo->file) < 0)
836	    if (! existence_error (errno))
837		error (1, errno, "cannot remove %s", finfo->file);
838
839	rename_file (fname, finfo->file);
840	if (unlink_file (tocvsPath) < 0)
841	    error (1, errno, "cannot remove %s", tocvsPath);
842	free (fname);
843    }
844
845    /* Call CVS_UNLINK() rather than unlink_file() below to avoid the check
846     * for noexec.
847     */
848    if( tmp != NULL )
849    {
850	if (CVS_UNLINK(tmp) < 0)
851	    error (0, errno, "cannot remove %s", tmp);
852	free (tmp);
853    }
854    if( rev1_cache != NULL )
855    {
856	if( CVS_UNLINK( rev1_cache ) < 0 )
857	    error( 0, errno, "cannot remove %s", rev1_cache );
858	free( rev1_cache );
859    }
860
861    freevers_ts (&vers);
862    diff_mark_errors (err);
863    return err;
864}
865
866/*
867 * Remember the exit status for each file.
868 */
869static void
870diff_mark_errors (err)
871    int err;
872{
873    if (err > diff_errors)
874	diff_errors = err;
875}
876
877/*
878 * Print a warm fuzzy message when we enter a dir
879 *
880 * Don't try to diff directories that don't exist! -- DW
881 */
882/* ARGSUSED */
883static Dtype
884diff_dirproc (callerdat, dir, pos_repos, update_dir, entries)
885    void *callerdat;
886    const char *dir;
887    const char *pos_repos;
888    const char *update_dir;
889    List *entries;
890{
891    /* XXX - check for dirs we don't want to process??? */
892
893    /* YES ... for instance dirs that don't exist!!! -- DW */
894    if (!isdir (dir))
895	return (R_SKIP_ALL);
896
897    if (!quiet)
898	error (0, 0, "Diffing %s", update_dir);
899    return (R_PROCESS);
900}
901
902/*
903 * Concoct the proper exit status - done with files
904 */
905/* ARGSUSED */
906static int
907diff_filesdoneproc (callerdat, err, repos, update_dir, entries)
908    void *callerdat;
909    int err;
910    const char *repos;
911    const char *update_dir;
912    List *entries;
913{
914    return (diff_errors);
915}
916
917/*
918 * Concoct the proper exit status - leaving directories
919 */
920/* ARGSUSED */
921static int
922diff_dirleaveproc (callerdat, dir, err, update_dir, entries)
923    void *callerdat;
924    const char *dir;
925    int err;
926    const char *update_dir;
927    List *entries;
928{
929    return (diff_errors);
930}
931
932/*
933 * verify that a file is different
934 */
935static enum diff_file
936diff_file_nodiff( finfo, vers, empty_file, rev1_cache )
937    struct file_info *finfo;
938    Vers_TS *vers;
939    enum diff_file empty_file;
940    char **rev1_cache;		/* Cache the content of rev1 if we have to look
941				 * it up.
942				 */
943{
944    Vers_TS *xvers;
945    int retcode;
946
947    /* free up any old use_rev* variables and reset 'em */
948    if (use_rev1)
949	free (use_rev1);
950    if (use_rev2)
951	free (use_rev2);
952    use_rev1 = use_rev2 = (char *) NULL;
953
954    if (diff_rev1 || diff_date1)
955    {
956	/* special handling for TAG_HEAD */
957	if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
958	{
959	    if (vers->vn_rcs != NULL && vers->srcfile != NULL)
960		use_rev1 = RCS_branch_head (vers->srcfile, vers->vn_rcs);
961	}
962	else
963	{
964	    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1, 1, 0);
965	    if (xvers->vn_rcs != NULL)
966		use_rev1 = xstrdup (xvers->vn_rcs);
967	    freevers_ts (&xvers);
968	}
969    }
970    if (diff_rev2 || diff_date2)
971    {
972	/* special handling for TAG_HEAD */
973	if (diff_rev2 && strcmp (diff_rev2, TAG_HEAD) == 0)
974	{
975	    if (vers->vn_rcs != NULL && vers->srcfile != NULL)
976		use_rev2 = RCS_branch_head (vers->srcfile, vers->vn_rcs);
977	}
978	else
979	{
980	    xvers = Version_TS (finfo, NULL, diff_rev2, diff_date2, 1, 0);
981	    if (xvers->vn_rcs != NULL)
982		use_rev2 = xstrdup (xvers->vn_rcs);
983	    freevers_ts (&xvers);
984	}
985
986	if( use_rev1 == NULL || RCS_isdead( vers->srcfile, use_rev1 ) )
987	{
988	    /* The first revision does not exist.  If EMPTY_FILES is
989               true, treat this as an added file.  Otherwise, warn
990               about the missing tag.  */
991	    if( use_rev2 == NULL || RCS_isdead( vers->srcfile, use_rev2 ) )
992		/* At least in the case where DIFF_REV1 and DIFF_REV2
993		 * are both numeric (and non-existant (NULL), as opposed to
994		 * dead?), we should be returning some kind of error (see
995		 * basicb-8a0 in testsuite).  The symbolic case may be more
996		 * complicated.
997		 */
998		return DIFF_SAME;
999	    if( empty_files )
1000		return DIFF_ADDED;
1001	    if( use_rev1 != NULL )
1002	    {
1003		if (diff_rev1)
1004		{
1005		    error( 0, 0,
1006		       "Tag %s refers to a dead (removed) revision in file `%s'.",
1007		       diff_rev1, finfo->fullname );
1008		}
1009		else
1010		{
1011		    error( 0, 0,
1012		       "Date %s refers to a dead (removed) revision in file `%s'.",
1013		       diff_date1, finfo->fullname );
1014		}
1015		error( 0, 0,
1016		       "No comparison available.  Pass `-N' to `%s diff'?",
1017		       program_name );
1018	    }
1019	    else if (diff_rev1)
1020		error (0, 0, "tag %s is not in file %s", diff_rev1,
1021		       finfo->fullname);
1022	    else
1023		error (0, 0, "no revision for date %s in file %s",
1024		       diff_date1, finfo->fullname);
1025	    return DIFF_ERROR;
1026	}
1027
1028	assert( use_rev1 != NULL );
1029	if( use_rev2 == NULL || RCS_isdead( vers->srcfile, use_rev2 ) )
1030	{
1031	    /* The second revision does not exist.  If EMPTY_FILES is
1032               true, treat this as a removed file.  Otherwise warn
1033               about the missing tag.  */
1034	    if (empty_files)
1035		return DIFF_REMOVED;
1036	    if( use_rev2 != NULL )
1037	    {
1038		if (diff_rev2)
1039		{
1040		    error( 0, 0,
1041		       "Tag %s refers to a dead (removed) revision in file `%s'.",
1042		       diff_rev2, finfo->fullname );
1043		}
1044		else
1045		{
1046		    error( 0, 0,
1047		       "Date %s refers to a dead (removed) revision in file `%s'.",
1048		       diff_date2, finfo->fullname );
1049		}
1050		error( 0, 0,
1051		       "No comparison available.  Pass `-N' to `%s diff'?",
1052		       program_name );
1053	    }
1054	    else if (diff_rev2)
1055		error (0, 0, "tag %s is not in file %s", diff_rev2,
1056		       finfo->fullname);
1057	    else
1058		error (0, 0, "no revision for date %s in file %s",
1059		       diff_date2, finfo->fullname);
1060	    return DIFF_ERROR;
1061	}
1062	/* Now, see if we really need to do the diff.  We can't assume that the
1063	 * files are different when the revs are.
1064	 */
1065	assert( use_rev2 != NULL );
1066	if( strcmp (use_rev1, use_rev2) == 0 )
1067	    return DIFF_SAME;
1068	/* else fall through and do the diff */
1069    }
1070
1071    /* If we had a r1/d1 & r2/d2, then at this point we must have a C3P0...
1072     * err...  ok, then both rev1 & rev2 must have resolved to an existing,
1073     * live version due to if statement we just closed.
1074     */
1075    assert (!(diff_rev2 || diff_date2) || (use_rev1 && use_rev2));
1076
1077    if ((diff_rev1 || diff_date1) &&
1078	(use_rev1 == NULL || RCS_isdead (vers->srcfile, use_rev1)))
1079    {
1080	/* The first revision does not exist, and no second revision
1081           was given.  */
1082	if (empty_files)
1083	{
1084	    if (empty_file == DIFF_REMOVED)
1085		return DIFF_SAME;
1086	    if( user_file_rev && use_rev2 == NULL )
1087		use_rev2 = xstrdup( user_file_rev );
1088	    return DIFF_ADDED;
1089	}
1090	if( use_rev1 != NULL )
1091	{
1092	    if (diff_rev1)
1093	    {
1094		error( 0, 0,
1095		   "Tag %s refers to a dead (removed) revision in file `%s'.",
1096		   diff_rev1, finfo->fullname );
1097	    }
1098	    else
1099	    {
1100		error( 0, 0,
1101		   "Date %s refers to a dead (removed) revision in file `%s'.",
1102		   diff_date1, finfo->fullname );
1103	    }
1104	    error( 0, 0,
1105		   "No comparison available.  Pass `-N' to `%s diff'?",
1106		   program_name );
1107	}
1108	else if ( diff_rev1 )
1109	    error( 0, 0, "tag %s is not in file %s", diff_rev1,
1110		   finfo->fullname );
1111	else
1112	    error( 0, 0, "no revision for date %s in file %s",
1113		   diff_date1, finfo->fullname );
1114	return DIFF_ERROR;
1115    }
1116
1117    assert( !diff_rev1 || use_rev1 );
1118
1119    if (user_file_rev)
1120    {
1121        /* drop user_file_rev into first unused use_rev */
1122        if (!use_rev1)
1123	    use_rev1 = xstrdup (user_file_rev);
1124	else if (!use_rev2)
1125	    use_rev2 = xstrdup (user_file_rev);
1126	/* and if not, it wasn't needed anyhow */
1127	user_file_rev = NULL;
1128    }
1129
1130    /* Now, see if we really need to do the diff.  We can't assume that the
1131     * files are different when the revs are.
1132     */
1133    if( use_rev1 && use_rev2)
1134    {
1135	if (strcmp (use_rev1, use_rev2) == 0)
1136	    return DIFF_SAME;
1137	/* Fall through and do the diff. */
1138    }
1139    /* Don't want to do the timestamp check with both use_rev1 & use_rev2 set.
1140     * The timestamp check is just for the default case of diffing the
1141     * workspace file against its base revision.
1142     */
1143    else if( use_rev1 == NULL
1144             || ( vers->vn_user != NULL
1145                  && strcmp( use_rev1, vers->vn_user ) == 0 ) )
1146    {
1147	if (empty_file == DIFF_DIFFERENT
1148	    && vers->ts_user != NULL
1149	    && strcmp (vers->ts_rcs, vers->ts_user) == 0
1150	    && (!(*options) || strcmp (options, vers->options) == 0))
1151	{
1152	    return DIFF_SAME;
1153	}
1154	if (use_rev1 == NULL
1155	    && (vers->vn_user[0] != '0' || vers->vn_user[1] != '\0'))
1156	{
1157	    if (vers->vn_user[0] == '-')
1158		use_rev1 = xstrdup (vers->vn_user + 1);
1159	    else
1160		use_rev1 = xstrdup (vers->vn_user);
1161	}
1162    }
1163
1164    /* If we already know that the file is being added or removed,
1165       then we don't want to do an actual file comparison here.  */
1166    if (empty_file != DIFF_DIFFERENT)
1167	return empty_file;
1168
1169    /*
1170     * Run a quick cmp to see if we should bother with a full diff.
1171     */
1172
1173    retcode = RCS_cmp_file( vers->srcfile, use_rev1, rev1_cache,
1174                            use_rev2, *options ? options : vers->options,
1175			    finfo->file );
1176
1177    return retcode == 0 ? DIFF_SAME : DIFF_DIFFERENT;
1178}
1179