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