xref: /dragonfly/contrib/cvs-1.12/src/diff.c (revision 1847e88f)
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     optind = 0;
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     else if (vers->vn_user == NULL)
503     {
504 	/* The file does not exist in the working directory.  */
505 	if ((diff_rev1 || diff_date1)
506 	    && vers->srcfile != NULL)
507 	{
508 	    /* The file does exist in the repository.  */
509 	    if (empty_files)
510 		empty_file = DIFF_REMOVED;
511 	    else
512 	    {
513 		int exists;
514 
515 		exists = 0;
516 		/* special handling for TAG_HEAD */
517 		if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
518 		{
519 		    char *head =
520 			(vers->vn_rcs == NULL
521 			 ? NULL
522 			 : RCS_branch_head (vers->srcfile, vers->vn_rcs));
523 		    exists = head != NULL && !RCS_isdead (vers->srcfile, head);
524 		    if (head != NULL)
525 			free (head);
526 		}
527 		else
528 		{
529 		    Vers_TS *xvers;
530 
531 		    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1,
532 					1, 0);
533 		    exists = xvers->vn_rcs && !RCS_isdead (xvers->srcfile,
534 		                                           xvers->vn_rcs);
535 		    freevers_ts (&xvers);
536 		}
537 		if (exists)
538 		    error (0, 0,
539 			   "%s no longer exists, no comparison available",
540 			   finfo->fullname);
541 		goto out;
542 	    }
543 	}
544 	else
545 	{
546 	    error (0, 0, "I know nothing about %s", finfo->fullname);
547 	    goto out;
548 	}
549     }
550     else if (vers->vn_user[0] == '0' && vers->vn_user[1] == '\0')
551     {
552 	/* The file was added locally.  */
553 	int exists = 0;
554 
555 	if (vers->srcfile != NULL)
556 	{
557 	    /* The file does exist in the repository.  */
558 
559 	    if (diff_rev1 || diff_date1)
560 	    {
561 		/* special handling for TAG_HEAD */
562 		if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
563 		{
564 		    char *head =
565 			(vers->vn_rcs == NULL
566 			 ? NULL
567 			 : RCS_branch_head (vers->srcfile, vers->vn_rcs));
568 		    exists = head && !RCS_isdead (vers->srcfile, head);
569 		    if (head != NULL)
570 			free (head);
571 		}
572 		else
573 		{
574 		    Vers_TS *xvers;
575 
576 		    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1,
577 					1, 0);
578 		    exists = xvers->vn_rcs
579 		             && !RCS_isdead (xvers->srcfile, xvers->vn_rcs);
580 		    freevers_ts (&xvers);
581 		}
582 	    }
583 	    else
584 	    {
585 		/* The file was added locally, but an RCS archive exists.  Our
586 		 * base revision must be dead.
587 		 */
588 		/* No need to set, exists = 0, here.  That's the default.  */
589 	    }
590 	}
591 	if (!exists)
592 	{
593 	    /* If we got here, then either the RCS archive does not exist or
594 	     * the relevant revision is dead.
595 	     */
596 	    if (empty_files)
597 		empty_file = DIFF_ADDED;
598 	    else
599 	    {
600 		error (0, 0, "%s is a new entry, no comparison available",
601 		       finfo->fullname);
602 		goto out;
603 	    }
604 	}
605     }
606     else if (vers->vn_user[0] == '-')
607     {
608 	if (empty_files)
609 	    empty_file = DIFF_REMOVED;
610 	else
611 	{
612 	    error (0, 0, "%s was removed, no comparison available",
613 		   finfo->fullname);
614 	    goto out;
615 	}
616     }
617     else
618     {
619 	if (!vers->vn_rcs && !vers->srcfile)
620 	{
621 	    error (0, 0, "cannot find revision control file for %s",
622 		   finfo->fullname);
623 	    goto out;
624 	}
625 	else
626 	{
627 	    if (vers->ts_user == NULL)
628 	    {
629 		error (0, 0, "cannot find %s", finfo->fullname);
630 		goto out;
631 	    }
632 	    else if (!strcmp (vers->ts_user, vers->ts_rcs))
633 	    {
634 		/* The user file matches some revision in the repository
635 		   Diff against the repository (for remote CVS, we might not
636 		   have a copy of the user file around).  */
637 		user_file_rev = vers->vn_user;
638 	    }
639 	}
640     }
641 
642     empty_file = diff_file_nodiff (finfo, vers, empty_file, &rev1_cache);
643     if (empty_file == DIFF_SAME)
644     {
645 	/* In the server case, would be nice to send a "Checked-in"
646 	   response, so that the client can rewrite its timestamp.
647 	   server_checked_in by itself isn't the right thing (it
648 	   needs a server_register), but I'm not sure what is.
649 	   It isn't clear to me how "cvs status" handles this (that
650 	   is, for a client which sends Modified not Is-modified to
651 	   "cvs status"), but it does.  */
652 	err = 0;
653 	goto out;
654     }
655     else if (empty_file == DIFF_ERROR)
656 	goto out;
657 
658     /* Output an "Index:" line for patch to use */
659     cvs_output ("Index: ", 0);
660     cvs_output (finfo->fullname, 0);
661     cvs_output ("\n", 1);
662 
663     tocvsPath = wrap_tocvs_process_file (finfo->file);
664     if (tocvsPath)
665     {
666 	/* Backup the current version of the file to CVS/,,filename */
667 	fname = Xasprintf (fname, "%s/%s%s", CVSADM, CVSPREFIX, finfo->file);
668 	if (unlink_file_dir (fname) < 0)
669 	    if (!existence_error (errno))
670 		error (1, errno, "cannot remove %s", fname);
671 	rename_file (finfo->file, fname);
672 	/* Copy the wrapped file to the current directory then go to work */
673 	copy_file (tocvsPath, finfo->file);
674     }
675 
676     /* Set up file labels appropriate for compatibility with the Larry Wall
677      * implementation of patch if the user didn't specify.  This is irrelevant
678      * according to the POSIX.2 specification.
679      */
680     label1 = NULL;
681     label2 = NULL;
682     /* The user cannot set the rev2 label without first setting the rev1
683      * label.
684      */
685     if (!have_rev2_label)
686     {
687 	if (empty_file == DIFF_REMOVED)
688 	    label2 = make_file_label (DEVNULL, NULL, NULL);
689 	else
690 	    label2 = make_file_label (finfo->fullname, use_rev2,
691 	                              vers->srcfile);
692 	if (!have_rev1_label)
693 	{
694 	    if (empty_file == DIFF_ADDED)
695 		label1 = make_file_label (DEVNULL, NULL, NULL);
696 	    else
697 		label1 = make_file_label (finfo->fullname, use_rev1,
698 		                          vers->srcfile);
699 	}
700     }
701 
702     if (empty_file == DIFF_ADDED || empty_file == DIFF_REMOVED)
703     {
704 	/* This is fullname, not file, possibly despite the POSIX.2
705 	 * specification, because that's the way all the Larry Wall
706 	 * implementations of patch (are there other implementations?) want
707 	 * things and the POSIX.2 spec appears to leave room for this.
708 	 */
709 	cvs_output ("\
710 ===================================================================\n\
711 RCS file: ", 0);
712 	cvs_output (finfo->fullname, 0);
713 	cvs_output ("\n", 1);
714 
715 	cvs_output ("diff -N ", 0);
716 	cvs_output (finfo->fullname, 0);
717 	cvs_output ("\n", 1);
718 
719 	if (empty_file == DIFF_ADDED)
720 	{
721 	    if (use_rev2 == NULL)
722                 status = diff_exec (DEVNULL, finfo->file, label1, label2,
723 				    diff_argc, diff_argv, RUN_TTY);
724 	    else
725 	    {
726 		int retcode;
727 
728 		tmp = cvs_temp_name ();
729 		retcode = RCS_checkout (vers->srcfile, NULL, use_rev2, NULL,
730 					*options ? options : vers->options,
731 					tmp, NULL, NULL);
732 		if (retcode != 0)
733 		    goto out;
734 
735 		status = diff_exec (DEVNULL, tmp, label1, label2,
736 				    diff_argc, diff_argv, RUN_TTY);
737 	    }
738 	}
739 	else
740 	{
741 	    int retcode;
742 
743 	    tmp = cvs_temp_name ();
744 	    retcode = RCS_checkout (vers->srcfile, NULL, use_rev1, NULL,
745 				    *options ? options : vers->options,
746 				    tmp, NULL, NULL);
747 	    if (retcode != 0)
748 		goto out;
749 
750 	    status = diff_exec (tmp, DEVNULL, label1, label2,
751 				diff_argc, diff_argv, RUN_TTY);
752 	}
753     }
754     else
755     {
756 	status = RCS_exec_rcsdiff (vers->srcfile, diff_argc, diff_argv,
757                                    *options ? options : vers->options,
758                                    use_rev1, rev1_cache, use_rev2,
759                                    label1, label2, finfo->file);
760 
761     }
762 
763     if (label1) free (label1);
764     if (label2) free (label2);
765 
766     switch (status)
767     {
768 	case -1:			/* fork failed */
769 	    error (1, errno, "fork failed while diffing %s",
770 		   vers->srcfile->path);
771 	case 0:				/* everything ok */
772 	    err = 0;
773 	    break;
774 	default:			/* other error */
775 	    err = status;
776 	    break;
777     }
778 
779 out:
780     if( tocvsPath != NULL )
781     {
782 	if (unlink_file_dir (finfo->file) < 0)
783 	    if (! existence_error (errno))
784 		error (1, errno, "cannot remove %s", finfo->file);
785 
786 	rename_file (fname, finfo->file);
787 	if (unlink_file (tocvsPath) < 0)
788 	    error (1, errno, "cannot remove %s", tocvsPath);
789 	free (fname);
790     }
791 
792     /* Call CVS_UNLINK() rather than unlink_file() below to avoid the check
793      * for noexec.
794      */
795     if (tmp != NULL)
796     {
797 	if (CVS_UNLINK (tmp) < 0)
798 	    error (0, errno, "cannot remove %s", tmp);
799 	free (tmp);
800     }
801     if (rev1_cache != NULL)
802     {
803 	if (CVS_UNLINK (rev1_cache) < 0)
804 	    error (0, errno, "cannot remove %s", rev1_cache);
805 	free (rev1_cache);
806     }
807 
808     freevers_ts (&vers);
809     diff_mark_errors (err);
810     return err;
811 }
812 
813 
814 
815 /*
816  * Remember the exit status for each file.
817  */
818 static void
819 diff_mark_errors (int err)
820 {
821     if (err > diff_errors)
822 	diff_errors = err;
823 }
824 
825 
826 
827 /*
828  * Print a warm fuzzy message when we enter a dir
829  *
830  * Don't try to diff directories that don't exist! -- DW
831  */
832 /* ARGSUSED */
833 static Dtype
834 diff_dirproc (void *callerdat, const char *dir, const char *pos_repos,
835               const char *update_dir, List *entries)
836 {
837     /* XXX - check for dirs we don't want to process??? */
838 
839     /* YES ... for instance dirs that don't exist!!! -- DW */
840     if (!isdir (dir))
841 	return R_SKIP_ALL;
842 
843     if (!quiet)
844 	error (0, 0, "Diffing %s", update_dir);
845     return R_PROCESS;
846 }
847 
848 
849 
850 /*
851  * Concoct the proper exit status - done with files
852  */
853 /* ARGSUSED */
854 static int
855 diff_filesdoneproc (void *callerdat, int err, const char *repos,
856                     const char *update_dir, List *entries)
857 {
858     return diff_errors;
859 }
860 
861 
862 
863 /*
864  * Concoct the proper exit status - leaving directories
865  */
866 /* ARGSUSED */
867 static int
868 diff_dirleaveproc (void *callerdat, const char *dir, int err,
869                    const char *update_dir, List *entries)
870 {
871     return diff_errors;
872 }
873 
874 
875 
876 /*
877  * verify that a file is different
878  *
879  * INPUTS
880  *   finfo
881  *   vers
882  *   empty_file
883  *
884  * OUTPUTS
885  *   rev1_cache		Cache the contents of rev1 if we look it up.
886  */
887 static enum diff_file
888 diff_file_nodiff (struct file_info *finfo, Vers_TS *vers,
889                   enum diff_file empty_file, char **rev1_cache)
890 {
891     Vers_TS *xvers;
892     int retcode;
893 
894     TRACE (TRACE_FUNCTION, "diff_file_nodiff (%s, %d)",
895            finfo->fullname ? finfo->fullname : "(null)", empty_file);
896 
897     /* free up any old use_rev* variables and reset 'em */
898     if (use_rev1)
899 	free (use_rev1);
900     if (use_rev2)
901 	free (use_rev2);
902     use_rev1 = use_rev2 = NULL;
903 
904     if (diff_rev1 || diff_date1)
905     {
906 	/* special handling for TAG_HEAD */
907 	if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
908 	{
909 	    if (vers->vn_rcs != NULL && vers->srcfile != NULL)
910 		use_rev1 = RCS_branch_head (vers->srcfile, vers->vn_rcs);
911 	}
912 	else
913 	{
914 	    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1, 1, 0);
915 	    if (xvers->vn_rcs != NULL)
916 		use_rev1 = xstrdup (xvers->vn_rcs);
917 	    freevers_ts (&xvers);
918 	}
919     }
920     if (diff_rev2 || diff_date2)
921     {
922 	/* special handling for TAG_HEAD */
923 	if (diff_rev2 && strcmp (diff_rev2, TAG_HEAD) == 0)
924 	{
925 	    if (vers->vn_rcs && vers->srcfile)
926 		use_rev2 = RCS_branch_head (vers->srcfile, vers->vn_rcs);
927 	}
928 	else
929 	{
930 	    xvers = Version_TS (finfo, NULL, diff_rev2, diff_date2, 1, 0);
931 	    if (xvers->vn_rcs != NULL)
932 		use_rev2 = xstrdup (xvers->vn_rcs);
933 	    freevers_ts (&xvers);
934 	}
935 
936 	if (use_rev1 == NULL || RCS_isdead (vers->srcfile, use_rev1))
937 	{
938 	    /* The first revision does not exist.  If EMPTY_FILES is
939                true, treat this as an added file.  Otherwise, warn
940                about the missing tag.  */
941 	    if (use_rev2 == NULL || RCS_isdead (vers->srcfile, use_rev2))
942 		/* At least in the case where DIFF_REV1 and DIFF_REV2
943 		 * are both numeric (and non-existant (NULL), as opposed to
944 		 * dead?), we should be returning some kind of error (see
945 		 * basicb-8a0 in testsuite).  The symbolic case may be more
946 		 * complicated.
947 		 */
948 		return DIFF_SAME;
949 	    if (empty_files)
950 		return DIFF_ADDED;
951 	    if (use_rev1 != NULL)
952 	    {
953 		if (diff_rev1)
954 		{
955 		    error (0, 0,
956 		       "Tag %s refers to a dead (removed) revision in file `%s'.",
957 		       diff_rev1, finfo->fullname);
958 		}
959 		else
960 		{
961 		    error (0, 0,
962 		       "Date %s refers to a dead (removed) revision in file `%s'.",
963 		       diff_date1, finfo->fullname);
964 		}
965 		error (0, 0,
966 		       "No comparison available.  Pass `-N' to `%s diff'?",
967 		       program_name);
968 	    }
969 	    else if (diff_rev1)
970 		error (0, 0, "tag %s is not in file %s", diff_rev1,
971 		       finfo->fullname);
972 	    else
973 		error (0, 0, "no revision for date %s in file %s",
974 		       diff_date1, finfo->fullname);
975 	    return DIFF_ERROR;
976 	}
977 
978 	assert( use_rev1 != NULL );
979 	if( use_rev2 == NULL || RCS_isdead( vers->srcfile, use_rev2 ) )
980 	{
981 	    /* The second revision does not exist.  If EMPTY_FILES is
982                true, treat this as a removed file.  Otherwise warn
983                about the missing tag.  */
984 	    if (empty_files)
985 		return DIFF_REMOVED;
986 	    if( use_rev2 != NULL )
987 	    {
988 		if (diff_rev2)
989 		{
990 		    error( 0, 0,
991 		       "Tag %s refers to a dead (removed) revision in file `%s'.",
992 		       diff_rev2, finfo->fullname );
993 		}
994 		else
995 		{
996 		    error( 0, 0,
997 		       "Date %s refers to a dead (removed) revision in file `%s'.",
998 		       diff_date2, finfo->fullname );
999 		}
1000 		error( 0, 0,
1001 		       "No comparison available.  Pass `-N' to `%s diff'?",
1002 		       program_name );
1003 	    }
1004 	    else if (diff_rev2)
1005 		error (0, 0, "tag %s is not in file %s", diff_rev2,
1006 		       finfo->fullname);
1007 	    else
1008 		error (0, 0, "no revision for date %s in file %s",
1009 		       diff_date2, finfo->fullname);
1010 	    return DIFF_ERROR;
1011 	}
1012 	/* Now, see if we really need to do the diff.  We can't assume that the
1013 	 * files are different when the revs are.
1014 	 */
1015 	assert( use_rev2 != NULL );
1016 	if( strcmp (use_rev1, use_rev2) == 0 )
1017 	    return DIFF_SAME;
1018 	/* else fall through and do the diff */
1019     }
1020 
1021     /* If we had a r1/d1 & r2/d2, then at this point we must have a C3P0...
1022      * err...  ok, then both rev1 & rev2 must have resolved to an existing,
1023      * live version due to if statement we just closed.
1024      */
1025     assert (!(diff_rev2 || diff_date2) || (use_rev1 && use_rev2));
1026 
1027     if ((diff_rev1 || diff_date1) &&
1028 	(use_rev1 == NULL || RCS_isdead (vers->srcfile, use_rev1)))
1029     {
1030 	/* The first revision does not exist, and no second revision
1031            was given.  */
1032 	if (empty_files)
1033 	{
1034 	    if (empty_file == DIFF_REMOVED)
1035 		return DIFF_SAME;
1036 	    if( user_file_rev && use_rev2 == NULL )
1037 		use_rev2 = xstrdup( user_file_rev );
1038 	    return DIFF_ADDED;
1039 	}
1040 	if( use_rev1 != NULL )
1041 	{
1042 	    if (diff_rev1)
1043 	    {
1044 		error( 0, 0,
1045 		   "Tag %s refers to a dead (removed) revision in file `%s'.",
1046 		   diff_rev1, finfo->fullname );
1047 	    }
1048 	    else
1049 	    {
1050 		error( 0, 0,
1051 		   "Date %s refers to a dead (removed) revision in file `%s'.",
1052 		   diff_date1, finfo->fullname );
1053 	    }
1054 	    error( 0, 0,
1055 		   "No comparison available.  Pass `-N' to `%s diff'?",
1056 		   program_name );
1057 	}
1058 	else if ( diff_rev1 )
1059 	    error( 0, 0, "tag %s is not in file %s", diff_rev1,
1060 		   finfo->fullname );
1061 	else
1062 	    error( 0, 0, "no revision for date %s in file %s",
1063 		   diff_date1, finfo->fullname );
1064 	return DIFF_ERROR;
1065     }
1066 
1067     assert( !diff_rev1 || use_rev1 );
1068 
1069     if (user_file_rev)
1070     {
1071         /* drop user_file_rev into first unused use_rev */
1072         if (!use_rev1)
1073 	    use_rev1 = xstrdup (user_file_rev);
1074 	else if (!use_rev2)
1075 	    use_rev2 = xstrdup (user_file_rev);
1076 	/* and if not, it wasn't needed anyhow */
1077 	user_file_rev = NULL;
1078     }
1079 
1080     /* Now, see if we really need to do the diff.  We can't assume that the
1081      * files are different when the revs are.
1082      */
1083     if( use_rev1 && use_rev2)
1084     {
1085 	if (strcmp (use_rev1, use_rev2) == 0)
1086 	    return DIFF_SAME;
1087 	/* Fall through and do the diff. */
1088     }
1089     /* Don't want to do the timestamp check with both use_rev1 & use_rev2 set.
1090      * The timestamp check is just for the default case of diffing the
1091      * workspace file against its base revision.
1092      */
1093     else if( use_rev1 == NULL
1094              || ( vers->vn_user != NULL
1095                   && strcmp( use_rev1, vers->vn_user ) == 0 ) )
1096     {
1097 	if (empty_file == DIFF_DIFFERENT
1098 	    && vers->ts_user != NULL
1099 	    && strcmp (vers->ts_rcs, vers->ts_user) == 0
1100 	    && (!(*options) || strcmp (options, vers->options) == 0))
1101 	{
1102 	    return DIFF_SAME;
1103 	}
1104 	if (use_rev1 == NULL
1105 	    && (vers->vn_user[0] != '0' || vers->vn_user[1] != '\0'))
1106 	{
1107 	    if (vers->vn_user[0] == '-')
1108 		use_rev1 = xstrdup (vers->vn_user + 1);
1109 	    else
1110 		use_rev1 = xstrdup (vers->vn_user);
1111 	}
1112     }
1113 
1114     /* If we already know that the file is being added or removed,
1115        then we don't want to do an actual file comparison here.  */
1116     if (empty_file != DIFF_DIFFERENT)
1117 	return empty_file;
1118 
1119     /*
1120      * Run a quick cmp to see if we should bother with a full diff.
1121      */
1122 
1123     retcode = RCS_cmp_file( vers->srcfile, use_rev1, rev1_cache,
1124                             use_rev2, *options ? options : vers->options,
1125 			    finfo->file );
1126 
1127     return retcode == 0 ? DIFF_SAME : DIFF_DIFFERENT;
1128 }
1129