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