xref: /openbsd-src/gnu/usr.bin/cvs/src/diff.c (revision 443998a44aec1b782e28ea78ab54ad82e5be4c96)
1 /*
2  * Copyright (c) 1992, Brian Berliner and Jeff Polk
3  * Copyright (c) 1989-1992, Brian Berliner
4  *
5  * You may distribute under the terms of the GNU General Public License as
6  * specified in the README file that comes with the CVS source distribution.
7  *
8  * Difference
9  *
10  * Run diff against versions in the repository.  Options that are specified are
11  * passed on directly to "rcsdiff".
12  *
13  * Without any file arguments, runs diff against all the currently modified
14  * files.
15  */
16 
17 #include "cvs.h"
18 
19 enum diff_file
20 {
21     DIFF_ERROR,
22     DIFF_ADDED,
23     DIFF_REMOVED,
24     DIFF_DIFFERENT,
25     DIFF_SAME
26 };
27 
28 static Dtype diff_dirproc PROTO ((void *callerdat, char *dir,
29 				  char *pos_repos, char *update_dir,
30 				  List *entries));
31 static int diff_filesdoneproc PROTO ((void *callerdat, int err,
32 				      char *repos, char *update_dir,
33 				      List *entries));
34 static int diff_dirleaveproc PROTO ((void *callerdat, char *dir,
35 				     int err, char *update_dir,
36 				     List *entries));
37 static enum diff_file diff_file_nodiff PROTO ((struct file_info *finfo,
38 					       Vers_TS *vers,
39 					       enum diff_file));
40 static int diff_fileproc PROTO ((void *callerdat, struct file_info *finfo));
41 static void diff_mark_errors PROTO((int err));
42 
43 static char *diff_rev1, *diff_rev2;
44 static char *diff_date1, *diff_date2;
45 static char *use_rev1, *use_rev2;
46 static int have_rev1_label, have_rev2_label;
47 
48 /* Revision of the user file, if it is unchanged from something in the
49    repository and we want to use that fact.  */
50 static char *user_file_rev;
51 
52 static char *options;
53 static char *opts;
54 static size_t opts_allocated = 1;
55 static int diff_errors;
56 static int empty_files = 0;
57 
58 /* FIXME: should be documenting all the options here.  They don't
59    perfectly match rcsdiff options (for example, we always support
60    --ifdef and --context, but rcsdiff only does if diff does).  */
61 static const char *const diff_usage[] =
62 {
63     "Usage: %s %s [-lNR] [rcsdiff-options]\n",
64     "    [[-r rev1 | -D date1] [-r rev2 | -D date2]] [files...] \n",
65     "\t-l\tLocal directory only, not recursive\n",
66     "\t-R\tProcess directories recursively.\n",
67     "\t-D d1\tDiff revision for date against working file.\n",
68     "\t-D d2\tDiff rev1/date1 against date2.\n",
69     "\t-N\tinclude diffs for added and removed files.\n",
70     "\t-r rev1\tDiff revision for rev1 against working file.\n",
71     "\t-r rev2\tDiff rev1/date1 against rev2.\n",
72     "\t--ifdef=arg\tOutput diffs in ifdef format.\n",
73     "(consult the documentation for your diff program for rcsdiff-options.\n",
74     "The most popular is -c for context diffs but there are many more).\n",
75     "(Specify the --help global option for a list of other help options)\n",
76     NULL
77 };
78 
79 /* I copied this array directly out of diff.c in diffutils 2.7, after
80    removing the following entries, none of which seem relevant to use
81    with CVS:
82      --help
83      --version
84      --recursive
85      --unidirectional-new-file
86      --starting-file
87      --exclude
88      --exclude-from
89      --sdiff-merge-assist
90 
91    I changed the options which take optional arguments (--context and
92    --unified) to return a number rather than a letter, so that the
93    optional argument could be handled more easily.  I changed the
94    --paginate and --brief options to return a number, since -l and -q
95    mean something else to cvs diff.
96 
97    The numbers 129- that appear in the fourth element of some entries
98    tell the big switch in `diff' how to process those options. -- Ian
99 
100    The following options, which diff lists as "An alias, no longer
101    recommended" have been removed: --file-label --entire-new-file
102    --ascii --print.  */
103 
104 static struct option const longopts[] =
105 {
106     {"ignore-blank-lines", 0, 0, 'B'},
107     {"context", 2, 0, 143},
108     {"ifdef", 1, 0, 131},
109     {"show-function-line", 1, 0, 'F'},
110     {"speed-large-files", 0, 0, 'H'},
111     {"ignore-matching-lines", 1, 0, 'I'},
112     {"label", 1, 0, 'L'},
113     {"new-file", 0, 0, 'N'},
114     {"initial-tab", 0, 0, 148},
115     {"width", 1, 0, 'W'},
116     {"text", 0, 0, 'a'},
117     {"ignore-space-change", 0, 0, 'b'},
118     {"minimal", 0, 0, 'd'},
119     {"ed", 0, 0, 'e'},
120     {"forward-ed", 0, 0, 'f'},
121     {"ignore-case", 0, 0, 'i'},
122     {"paginate", 0, 0, 144},
123     {"rcs", 0, 0, 'n'},
124     {"show-c-function", 0, 0, 'p'},
125 
126     /* This is a potentially very useful option, except the output is so
127        silly.  It would be much better for it to look like "cvs rdiff -s"
128        which displays all the same info, minus quite a few lines of
129        extraneous garbage.  */
130     {"brief", 0, 0, 145},
131 
132     {"report-identical-files", 0, 0, 's'},
133     {"expand-tabs", 0, 0, 't'},
134     {"ignore-all-space", 0, 0, 'w'},
135     {"side-by-side", 0, 0, 147},
136     {"unified", 2, 0, 146},
137     {"left-column", 0, 0, 129},
138     {"suppress-common-lines", 0, 0, 130},
139     {"old-line-format", 1, 0, 132},
140     {"new-line-format", 1, 0, 133},
141     {"unchanged-line-format", 1, 0, 134},
142     {"line-format", 1, 0, 135},
143     {"old-group-format", 1, 0, 136},
144     {"new-group-format", 1, 0, 137},
145     {"unchanged-group-format", 1, 0, 138},
146     {"changed-group-format", 1, 0, 139},
147     {"horizon-lines", 1, 0, 140},
148     {"binary", 0, 0, 142},
149     {0, 0, 0, 0}
150 };
151 
152 /* CVS 1.9 and similar versions seemed to have pretty weird handling
153    of -y and -T.  In the cases where it called rcsdiff,
154    they would have the meanings mentioned below.  In the cases where it
155    called diff, they would have the meanings mentioned in "longopts".
156    Noone seems to have missed them, so I think the right thing to do is
157    just to remove the options altogether (which I have done).
158 
159    In the case of -z and -q, "cvs diff" did not accept them even back
160    when we called rcsdiff (at least, it hasn't accepted them
161    recently).
162 
163    In comparing rcsdiff to the new CVS implementation, I noticed that
164    the following rcsdiff flags are not handled by CVS diff:
165 
166 	   -y: perform diff even when the requested revisions are the
167 		   same revision number
168 	   -q: run quietly
169 	   -T: preserve modification time on the RCS file
170 	   -z: specify timezone for use in file labels
171 
172    I think these are not really relevant.  -y is undocumented even in
173    RCS 5.7, and seems like a minor change at best.  According to RCS
174    documentation, -T only applies when a RCS file has been modified
175    because of lock changes; doesn't CVS sidestep RCS's entire lock
176    structure?  -z seems to be unsupported by CVS diff, and has a
177    different meaning as a global option anyway.  (Adding it could be
178    a feature, but if it is left out for now, it should not break
179    anything.)  For the purposes of producing output, CVS diff appears
180    mostly to ignore -q.  Maybe this should be fixed, but I think it's
181    a larger issue than the changes included here.  */
182 
183 static void strcat_and_allocate PROTO ((char **, size_t *, const char *));
184 
185 /* *STR is a pointer to a malloc'd string.  *LENP is its allocated
186    length.  Add SRC to the end of it, reallocating if necessary.  */
187 static void
188 strcat_and_allocate (str, lenp, src)
189     char **str;
190     size_t *lenp;
191     const char *src;
192 {
193     size_t new_size;
194 
195     new_size = strlen (*str) + strlen (src) + 1;
196     if (*str == NULL || new_size >= *lenp)
197     {
198 	while (new_size >= *lenp)
199 	    *lenp *= 2;
200 	*str = xrealloc (*str, *lenp);
201     }
202     strcat (*str, src);
203 }
204 
205 int
206 diff (argc, argv)
207     int argc;
208     char **argv;
209 {
210     char tmp[50];
211     int c, err = 0;
212     int local = 0;
213     int which;
214     int option_index;
215 
216     if (argc == -1)
217 	usage (diff_usage);
218 
219     have_rev1_label = have_rev2_label = 0;
220 
221     /*
222      * Note that we catch all the valid arguments here, so that we can
223      * intercept the -r arguments for doing revision diffs; and -l/-R for a
224      * non-recursive/recursive diff.
225      */
226 
227     /* For server, need to be able to do this command more than once
228        (according to the protocol spec, even if the current client
229        doesn't use it).  */
230     if (opts == NULL)
231     {
232 	opts_allocated = 1;
233 	opts = xmalloc (opts_allocated);
234     }
235     opts[0] = '\0';
236 
237     optind = 0;
238     while ((c = getopt_long (argc, argv,
239 	       "+abcdefhilnpstuw0123456789BHNRC:D:F:I:L:U:V:W:k:r:",
240 			     longopts, &option_index)) != -1)
241     {
242 	switch (c)
243 	{
244 	    case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
245 	    case 'h': case 'i': case 'n': case 'p': case 's': case 't':
246 	    case 'u': case 'w': case '0': case '1': case '2':
247 	    case '3': case '4': case '5': case '6': case '7': case '8':
248 	    case '9': case 'B': case 'H':
249 		(void) sprintf (tmp, " -%c", (char) c);
250 		strcat_and_allocate (&opts, &opts_allocated, tmp);
251 		break;
252 	    case 'L':
253 		if (have_rev1_label++)
254 		    if (have_rev2_label++)
255 		    {
256 			error (0, 0, "extra -L arguments ignored");
257 			break;
258 		    }
259 
260 	        strcat_and_allocate (&opts, &opts_allocated, " -L");
261 	        strcat_and_allocate (&opts, &opts_allocated, optarg);
262 		break;
263 	    case 'C': case 'F': case 'I': case 'U': case 'V': case 'W':
264 		(void) sprintf (tmp, " -%c", (char) c);
265 		strcat_and_allocate (&opts, &opts_allocated, tmp);
266 		strcat_and_allocate (&opts, &opts_allocated, optarg);
267 		break;
268 	    case 131:
269 		/* --ifdef.  */
270 		strcat_and_allocate (&opts, &opts_allocated, " -D");
271 		strcat_and_allocate (&opts, &opts_allocated, optarg);
272 		break;
273 	    case 129: case 130:           case 132: case 133: case 134:
274 	    case 135: case 136: case 137: case 138: case 139: case 140:
275 	    case 141: case 142: case 143: case 144: case 145: case 146:
276 	    case 147: case 148:
277 		strcat_and_allocate (&opts, &opts_allocated, " --");
278 		strcat_and_allocate (&opts, &opts_allocated,
279 				     longopts[option_index].name);
280 		if (longopts[option_index].has_arg == 1
281 		    || (longopts[option_index].has_arg == 2
282 			&& optarg != NULL))
283 		{
284 		    strcat_and_allocate (&opts, &opts_allocated, "=");
285 		    strcat_and_allocate (&opts, &opts_allocated, optarg);
286 		}
287 		break;
288 	    case 'R':
289 		local = 0;
290 		break;
291 	    case 'l':
292 		local = 1;
293 		break;
294 	    case 'k':
295 		if (options)
296 		    free (options);
297 		options = RCS_check_kflag (optarg);
298 		break;
299 	    case 'r':
300 		if (diff_rev2 != NULL || diff_date2 != NULL)
301 		    error (1, 0,
302 		       "no more than two revisions/dates can be specified");
303 		if (diff_rev1 != NULL || diff_date1 != NULL)
304 		    diff_rev2 = optarg;
305 		else
306 		    diff_rev1 = optarg;
307 		break;
308 	    case 'D':
309 		if (diff_rev2 != NULL || diff_date2 != NULL)
310 		    error (1, 0,
311 		       "no more than two revisions/dates can be specified");
312 		if (diff_rev1 != NULL || diff_date1 != NULL)
313 		    diff_date2 = Make_Date (optarg);
314 		else
315 		    diff_date1 = Make_Date (optarg);
316 		break;
317 	    case 'N':
318 		empty_files = 1;
319 		break;
320 	    case '?':
321 	    default:
322 		usage (diff_usage);
323 		break;
324 	}
325     }
326     argc -= optind;
327     argv += optind;
328 
329     /* make sure options is non-null */
330     if (!options)
331 	options = xstrdup ("");
332 
333 #ifdef CLIENT_SUPPORT
334     if (client_active) {
335 	/* We're the client side.  Fire up the remote server.  */
336 	start_server ();
337 
338 	ign_setup ();
339 
340 	if (local)
341 	    send_arg("-l");
342 	if (empty_files)
343 	    send_arg("-N");
344 	send_option_string (opts);
345 	if (options[0] != '\0')
346 	    send_arg (options);
347 	if (diff_rev1)
348 	    option_with_arg ("-r", diff_rev1);
349 	if (diff_date1)
350 	    client_senddate (diff_date1);
351 	if (diff_rev2)
352 	    option_with_arg ("-r", diff_rev2);
353 	if (diff_date2)
354 	    client_senddate (diff_date2);
355 
356 	send_file_names (argc, argv, SEND_EXPAND_WILD);
357 
358 	/* Send the current files unless diffing two revs from the archive */
359 	if (diff_rev2 == NULL && diff_date2 == NULL)
360 	    send_files (argc, argv, local, 0, 0);
361 	else
362 	    send_files (argc, argv, local, 0, SEND_NO_CONTENTS);
363 
364 	send_to_server ("diff\012", 0);
365         err = get_responses_and_close ();
366 	free (options);
367 	return (err);
368     }
369 #endif
370 
371     if (diff_rev1 != NULL)
372 	tag_check_valid (diff_rev1, argc, argv, local, 0, "");
373     if (diff_rev2 != NULL)
374 	tag_check_valid (diff_rev2, argc, argv, local, 0, "");
375 
376     which = W_LOCAL;
377     if (diff_rev1 != NULL || diff_date1 != NULL)
378 	which |= W_REPOS | W_ATTIC;
379 
380     wrap_setup ();
381 
382     /* start the recursion processor */
383     err = start_recursion (diff_fileproc, diff_filesdoneproc, diff_dirproc,
384 			   diff_dirleaveproc, NULL, argc, argv, local,
385 			   which, 0, 1, (char *) NULL, 1);
386 
387     /* clean up */
388     free (options);
389     return (err);
390 }
391 
392 /*
393  * Do a file diff
394  */
395 /* ARGSUSED */
396 static int
397 diff_fileproc (callerdat, finfo)
398     void *callerdat;
399     struct file_info *finfo;
400 {
401     int status, err = 2;		/* 2 == trouble, like rcsdiff */
402     Vers_TS *vers;
403     enum diff_file empty_file = DIFF_DIFFERENT;
404     char *tmp;
405     char *tocvsPath;
406     char *fname;
407 
408     /* Initialize these solely to avoid warnings from gcc -Wall about
409        variables that might be used uninitialized.  */
410     tmp = NULL;
411     fname = NULL;
412 
413     user_file_rev = 0;
414     vers = Version_TS (finfo, NULL, NULL, NULL, 1, 0);
415 
416     if (diff_rev2 != NULL || diff_date2 != NULL)
417     {
418 	/* Skip all the following checks regarding the user file; we're
419 	   not using it.  */
420     }
421     else if (vers->vn_user == NULL)
422     {
423 	/* The file does not exist in the working directory.  */
424 	if ((diff_rev1 != NULL || diff_date1 != NULL)
425 	    && vers->srcfile != NULL)
426 	{
427 	    /* The file does exist in the repository.  */
428 	    if (empty_files)
429 		empty_file = DIFF_REMOVED;
430 	    else
431 	    {
432 		int exists;
433 
434 		exists = 0;
435 		/* special handling for TAG_HEAD */
436 		if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
437 		{
438 		    char *head =
439 			(vers->vn_rcs == NULL
440 			 ? NULL
441 			 : RCS_branch_head (vers->srcfile, vers->vn_rcs));
442 		    exists = head != NULL;
443 		    if (head != NULL)
444 			free (head);
445 		}
446 		else
447 		{
448 		    Vers_TS *xvers;
449 
450 		    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1,
451 					1, 0);
452 		    exists = xvers->vn_rcs != NULL;
453 		    freevers_ts (&xvers);
454 		}
455 		if (exists)
456 		    error (0, 0,
457 			   "%s no longer exists, no comparison available",
458 			   finfo->fullname);
459 		freevers_ts (&vers);
460 		diff_mark_errors (err);
461 		return (err);
462 	    }
463 	}
464 	else
465 	{
466 	    error (0, 0, "I know nothing about %s", finfo->fullname);
467 	    freevers_ts (&vers);
468 	    diff_mark_errors (err);
469 	    return (err);
470 	}
471     }
472     else if (vers->vn_user[0] == '0' && vers->vn_user[1] == '\0')
473     {
474 	if (empty_files)
475 	    empty_file = DIFF_ADDED;
476 	else
477 	{
478 	    error (0, 0, "%s is a new entry, no comparison available",
479 		   finfo->fullname);
480 	    freevers_ts (&vers);
481 	    diff_mark_errors (err);
482 	    return (err);
483 	}
484     }
485     else if (vers->vn_user[0] == '-')
486     {
487 	if (empty_files)
488 	    empty_file = DIFF_REMOVED;
489 	else
490 	{
491 	    error (0, 0, "%s was removed, no comparison available",
492 		   finfo->fullname);
493 	    freevers_ts (&vers);
494 	    diff_mark_errors (err);
495 	    return (err);
496 	}
497     }
498     else
499     {
500 	if (vers->vn_rcs == NULL && vers->srcfile == NULL)
501 	{
502 	    error (0, 0, "cannot find revision control file for %s",
503 		   finfo->fullname);
504 	    freevers_ts (&vers);
505 	    diff_mark_errors (err);
506 	    return (err);
507 	}
508 	else
509 	{
510 	    if (vers->ts_user == NULL)
511 	    {
512 		error (0, 0, "cannot find %s", finfo->fullname);
513 		freevers_ts (&vers);
514 		diff_mark_errors (err);
515 		return (err);
516 	    }
517 	    else if (!strcmp (vers->ts_user, vers->ts_rcs))
518 	    {
519 		/* The user file matches some revision in the repository
520 		   Diff against the repository (for remote CVS, we might not
521 		   have a copy of the user file around).  */
522 		user_file_rev = vers->vn_user;
523 	    }
524 	}
525     }
526 
527     empty_file = diff_file_nodiff (finfo, vers, empty_file);
528     if (empty_file == DIFF_SAME || empty_file == DIFF_ERROR)
529     {
530 	freevers_ts (&vers);
531 	if (empty_file == DIFF_SAME)
532 	{
533 	    /* In the server case, would be nice to send a "Checked-in"
534 	       response, so that the client can rewrite its timestamp.
535 	       server_checked_in by itself isn't the right thing (it
536 	       needs a server_register), but I'm not sure what is.
537 	       It isn't clear to me how "cvs status" handles this (that
538 	       is, for a client which sends Modified not Is-modified to
539 	       "cvs status"), but it does.  */
540 	    return (0);
541 	}
542 	else
543 	{
544 	    diff_mark_errors (err);
545 	    return (err);
546 	}
547     }
548 
549     if (empty_file == DIFF_DIFFERENT)
550     {
551 	int dead1, dead2;
552 
553 	if (use_rev1 == NULL)
554 	    dead1 = 0;
555 	else
556 	    dead1 = RCS_isdead (vers->srcfile, use_rev1);
557 	if (use_rev2 == NULL)
558 	    dead2 = 0;
559 	else
560 	    dead2 = RCS_isdead (vers->srcfile, use_rev2);
561 
562 	if (dead1 && dead2)
563 	{
564 	    freevers_ts (&vers);
565 	    return (0);
566 	}
567 	else if (dead1)
568 	{
569 	    if (empty_files)
570 	        empty_file = DIFF_ADDED;
571 	    else
572 	    {
573 		error (0, 0, "%s is a new entry, no comparison available",
574 		       finfo->fullname);
575 		freevers_ts (&vers);
576 		diff_mark_errors (err);
577 		return (err);
578 	    }
579 	}
580 	else if (dead2)
581 	{
582 	    if (empty_files)
583 		empty_file = DIFF_REMOVED;
584 	    else
585 	    {
586 		error (0, 0, "%s was removed, no comparison available",
587 		       finfo->fullname);
588 		freevers_ts (&vers);
589 		diff_mark_errors (err);
590 		return (err);
591 	    }
592 	}
593     }
594 
595     /* Output an "Index:" line for patch to use */
596     cvs_output ("Index: ", 0);
597     cvs_output (finfo->fullname, 0);
598     cvs_output ("\n", 1);
599 
600     tocvsPath = wrap_tocvs_process_file(finfo->file);
601     if (tocvsPath)
602     {
603 	/* Backup the current version of the file to CVS/,,filename */
604 	fname = xmalloc (strlen (finfo->file)
605 			 + sizeof CVSADM
606 			 + sizeof CVSPREFIX
607 			 + 10);
608 	sprintf(fname,"%s/%s%s",CVSADM, CVSPREFIX, finfo->file);
609 	if (unlink_file_dir (fname) < 0)
610 	    if (! existence_error (errno))
611 		error (1, errno, "cannot remove %s", fname);
612 	rename_file (finfo->file, fname);
613 	/* Copy the wrapped file to the current directory then go to work */
614 	copy_file (tocvsPath, finfo->file);
615     }
616 
617     if (empty_file == DIFF_ADDED || empty_file == DIFF_REMOVED)
618     {
619 	/* This is file, not fullname, because it is the "Index:" line which
620 	   is supposed to contain the directory.  */
621 	cvs_output ("\
622 ===================================================================\n\
623 RCS file: ", 0);
624 	cvs_output (finfo->file, 0);
625 	cvs_output ("\n", 1);
626 
627 	cvs_output ("diff -N ", 0);
628 	cvs_output (finfo->file, 0);
629 	cvs_output ("\n", 1);
630 
631 	if (empty_file == DIFF_ADDED)
632 	{
633 	    if (use_rev2 == NULL)
634 		status = diff_exec (DEVNULL, finfo->file, opts, RUN_TTY);
635 	    else
636 	    {
637 		int retcode;
638 
639 		tmp = cvs_temp_name ();
640 		retcode = RCS_checkout (vers->srcfile, (char *) NULL,
641 					use_rev2, (char *) NULL,
642 					(*options
643 					 ? options
644 					 : vers->options),
645 					tmp, (RCSCHECKOUTPROC) NULL,
646 					(void *) NULL);
647 		if (retcode != 0)
648 		{
649 		    diff_mark_errors (err);
650 		    return err;
651 		}
652 
653 		status = diff_exec (DEVNULL, tmp, opts, RUN_TTY);
654 	    }
655 	}
656 	else
657 	{
658 	    int retcode;
659 
660 	    tmp = cvs_temp_name ();
661 	    retcode = RCS_checkout (vers->srcfile, (char *) NULL,
662 				    use_rev1, (char *) NULL,
663 				    *options ? options : vers->options,
664 				    tmp, (RCSCHECKOUTPROC) NULL,
665 				    (void *) NULL);
666 	    if (retcode != 0)
667 	    {
668 		diff_mark_errors (err);
669 		return err;
670 	    }
671 
672 	    status = diff_exec (tmp, DEVNULL, opts, RUN_TTY);
673 	}
674     }
675     else
676     {
677 	char *label1 = NULL;
678 	char *label2 = NULL;
679 
680 	if (!have_rev1_label)
681 	    label1 =
682 		make_file_label (finfo->fullname, use_rev1, vers->srcfile);
683 
684 	if (!have_rev2_label)
685 	    label2 =
686 		make_file_label (finfo->fullname, use_rev2, vers->srcfile);
687 
688 	status = RCS_exec_rcsdiff (vers->srcfile, opts,
689 				   *options ? options : vers->options,
690 				   use_rev1, use_rev2,
691 				   label1, label2,
692 				   finfo->file);
693 
694 	if (label1) free (label1);
695 	if (label2) free (label2);
696     }
697 
698     switch (status)
699     {
700 	case -1:			/* fork failed */
701 	    error (1, errno, "fork failed while diffing %s",
702 		   vers->srcfile->path);
703 	case 0:				/* everything ok */
704 	    err = 0;
705 	    break;
706 	default:			/* other error */
707 	    err = status;
708 	    break;
709     }
710 
711     if (tocvsPath)
712     {
713 	if (unlink_file_dir (finfo->file) < 0)
714 	    if (! existence_error (errno))
715 		error (1, errno, "cannot remove %s", finfo->file);
716 
717 	rename_file (fname, finfo->file);
718 	if (unlink_file (tocvsPath) < 0)
719 	    error (1, errno, "cannot remove %s", tocvsPath);
720 	free (fname);
721     }
722 
723     if (empty_file == DIFF_REMOVED
724 	|| (empty_file == DIFF_ADDED && use_rev2 != NULL))
725     {
726 	if (CVS_UNLINK (tmp) < 0)
727 	    error (0, errno, "cannot remove %s", tmp);
728 	free (tmp);
729     }
730 
731     freevers_ts (&vers);
732     diff_mark_errors (err);
733     return (err);
734 }
735 
736 /*
737  * Remember the exit status for each file.
738  */
739 static void
740 diff_mark_errors (err)
741     int err;
742 {
743     if (err > diff_errors)
744 	diff_errors = err;
745 }
746 
747 /*
748  * Print a warm fuzzy message when we enter a dir
749  *
750  * Don't try to diff directories that don't exist! -- DW
751  */
752 /* ARGSUSED */
753 static Dtype
754 diff_dirproc (callerdat, dir, pos_repos, update_dir, entries)
755     void *callerdat;
756     char *dir;
757     char *pos_repos;
758     char *update_dir;
759     List *entries;
760 {
761     /* XXX - check for dirs we don't want to process??? */
762 
763     /* YES ... for instance dirs that don't exist!!! -- DW */
764     if (!isdir (dir))
765 	return (R_SKIP_ALL);
766 
767     if (!quiet)
768 	error (0, 0, "Diffing %s", update_dir);
769     return (R_PROCESS);
770 }
771 
772 /*
773  * Concoct the proper exit status - done with files
774  */
775 /* ARGSUSED */
776 static int
777 diff_filesdoneproc (callerdat, err, repos, update_dir, entries)
778     void *callerdat;
779     int err;
780     char *repos;
781     char *update_dir;
782     List *entries;
783 {
784     return (diff_errors);
785 }
786 
787 /*
788  * Concoct the proper exit status - leaving directories
789  */
790 /* ARGSUSED */
791 static int
792 diff_dirleaveproc (callerdat, dir, err, update_dir, entries)
793     void *callerdat;
794     char *dir;
795     int err;
796     char *update_dir;
797     List *entries;
798 {
799     return (diff_errors);
800 }
801 
802 /*
803  * verify that a file is different
804  */
805 static enum diff_file
806 diff_file_nodiff (finfo, vers, empty_file)
807     struct file_info *finfo;
808     Vers_TS *vers;
809     enum diff_file empty_file;
810 {
811     Vers_TS *xvers;
812     int retcode;
813 
814     /* free up any old use_rev* variables and reset 'em */
815     if (use_rev1)
816 	free (use_rev1);
817     if (use_rev2)
818 	free (use_rev2);
819     use_rev1 = use_rev2 = (char *) NULL;
820 
821     if (diff_rev1 || diff_date1)
822     {
823 	/* special handling for TAG_HEAD */
824 	if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
825 	    use_rev1 = ((vers->vn_rcs == NULL || vers->srcfile == NULL)
826 			? NULL
827 			: RCS_branch_head (vers->srcfile, vers->vn_rcs));
828 	else
829 	{
830 	    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1, 1, 0);
831 	    if (xvers->vn_rcs != NULL)
832 		use_rev1 = xstrdup (xvers->vn_rcs);
833 	    freevers_ts (&xvers);
834 	}
835     }
836     if (diff_rev2 || diff_date2)
837     {
838 	/* special handling for TAG_HEAD */
839 	if (diff_rev2 && strcmp (diff_rev2, TAG_HEAD) == 0)
840 	    use_rev2 = ((vers->vn_rcs == NULL || vers->srcfile == NULL)
841 			? NULL
842 			: RCS_branch_head (vers->srcfile, vers->vn_rcs));
843 	else
844 	{
845 	    xvers = Version_TS (finfo, NULL, diff_rev2, diff_date2, 1, 0);
846 	    if (xvers->vn_rcs != NULL)
847 		use_rev2 = xstrdup (xvers->vn_rcs);
848 	    freevers_ts (&xvers);
849 	}
850 
851 	if (use_rev1 == NULL)
852 	{
853 	    /* The first revision does not exist.  If EMPTY_FILES is
854                true, treat this as an added file.  Otherwise, warn
855                about the missing tag.  */
856 	    if (use_rev2 == NULL)
857 		/* At least in the case where DIFF_REV1 and DIFF_REV2
858 		   are both numeric, we should be returning some kind
859 		   of error (see basicb-8a0 in testsuite).  The symbolic
860 		   case may be more complicated.  */
861 		return DIFF_SAME;
862 	    else if (empty_files)
863 		return DIFF_ADDED;
864 	    else if (diff_rev1)
865 		error (0, 0, "tag %s is not in file %s", diff_rev1,
866 		       finfo->fullname);
867 	    else
868 		error (0, 0, "no revision for date %s in file %s",
869 		       diff_date1, finfo->fullname);
870 	    return DIFF_ERROR;
871 	}
872 
873 	if (use_rev2 == NULL)
874 	{
875 	    /* The second revision does not exist.  If EMPTY_FILES is
876                true, treat this as a removed file.  Otherwise warn
877                about the missing tag.  */
878 	    if (empty_files)
879 		return DIFF_REMOVED;
880 	    else if (diff_rev2)
881 		error (0, 0, "tag %s is not in file %s", diff_rev2,
882 		       finfo->fullname);
883 	    else
884 		error (0, 0, "no revision for date %s in file %s",
885 		       diff_date2, finfo->fullname);
886 	    return DIFF_ERROR;
887 	}
888 
889 	/* now, see if we really need to do the diff */
890 	if (strcmp (use_rev1, use_rev2) == 0)
891 	    return DIFF_SAME;
892 	else
893 	    return DIFF_DIFFERENT;
894     }
895 
896     if ((diff_rev1 || diff_date1) && use_rev1 == NULL)
897     {
898 	/* The first revision does not exist, and no second revision
899            was given.  */
900 	if (empty_files)
901 	{
902 	    if (empty_file == DIFF_REMOVED)
903 		return DIFF_SAME;
904 	    else
905 	    {
906 		if (user_file_rev && use_rev2 == NULL)
907 		    use_rev2 = xstrdup (user_file_rev);
908 		return DIFF_ADDED;
909 	    }
910 	}
911 	else
912 	{
913 	    if (diff_rev1)
914 		error (0, 0, "tag %s is not in file %s", diff_rev1,
915 		       finfo->fullname);
916 	    else
917 		error (0, 0, "no revision for date %s in file %s",
918 		       diff_date1, finfo->fullname);
919 	    return DIFF_ERROR;
920 	}
921     }
922 
923     if (user_file_rev)
924     {
925         /* drop user_file_rev into first unused use_rev */
926         if (!use_rev1)
927 	    use_rev1 = xstrdup (user_file_rev);
928 	else if (!use_rev2)
929 	    use_rev2 = xstrdup (user_file_rev);
930 	/* and if not, it wasn't needed anyhow */
931 	user_file_rev = 0;
932     }
933 
934     /* now, see if we really need to do the diff */
935     if (use_rev1 && use_rev2)
936     {
937 	if (strcmp (use_rev1, use_rev2) == 0)
938 	    return DIFF_SAME;
939 	else
940 	    return DIFF_DIFFERENT;
941     }
942 
943     if (use_rev1 == NULL
944 	|| (vers->vn_user != NULL && strcmp (use_rev1, vers->vn_user) == 0))
945     {
946 	if (empty_file == DIFF_DIFFERENT
947 	    && vers->ts_user != NULL
948 	    && strcmp (vers->ts_rcs, vers->ts_user) == 0
949 	    && (!(*options) || strcmp (options, vers->options) == 0))
950 	{
951 	    return DIFF_SAME;
952 	}
953 	if (use_rev1 == NULL
954 	    && (vers->vn_user[0] != '0' || vers->vn_user[1] != '\0'))
955 	{
956 	    if (vers->vn_user[0] == '-')
957 		use_rev1 = xstrdup (vers->vn_user + 1);
958 	    else
959 		use_rev1 = xstrdup (vers->vn_user);
960 	}
961     }
962 
963     /* If we already know that the file is being added or removed,
964        then we don't want to do an actual file comparison here.  */
965     if (empty_file != DIFF_DIFFERENT)
966 	return empty_file;
967 
968     /*
969      * with 0 or 1 -r option specified, run a quick diff to see if we
970      * should bother with it at all.
971      */
972 
973     retcode = RCS_cmp_file (vers->srcfile, use_rev1,
974 			    *options ? options : vers->options,
975 			    finfo->file);
976 
977     return retcode == 0 ? DIFF_SAME : DIFF_DIFFERENT;
978 }
979