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