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