/*
 * Copyright (c) 1992, Brian Berliner and Jeff Polk
 * Copyright (c) 1989-1992, Brian Berliner
 * 
 * You may distribute under the terms of the GNU General Public License as
 * specified in the README file that comes with the CVS 1.4 kit.
 * 
 * Difference
 * 
 * Run diff against versions in the repository.  Options that are specified are
 * passed on directly to "rcsdiff".
 * 
 * Without any file arguments, runs diff against all the currently modified
 * files.
 */

#include "cvs.h"

enum diff_file
{
    DIFF_ERROR,
    DIFF_ADDED,
    DIFF_REMOVED,
    DIFF_DIFFERENT,
    DIFF_SAME
};

static Dtype diff_dirproc PROTO ((void *callerdat, char *dir,
				  char *pos_repos, char *update_dir,
				  List *entries));
static int diff_filesdoneproc PROTO ((void *callerdat, int err,
				      char *repos, char *update_dir,
				      List *entries));
static int diff_dirleaveproc PROTO ((void *callerdat, char *dir,
				     int err, char *update_dir,
				     List *entries));
static enum diff_file diff_file_nodiff PROTO ((struct file_info *finfo,
					       Vers_TS *vers,
					       enum diff_file));
static int diff_fileproc PROTO ((void *callerdat, struct file_info *finfo));
static void diff_mark_errors PROTO((int err));

static char *diff_rev1, *diff_rev2;
static char *diff_date1, *diff_date2;
static char *use_rev1, *use_rev2;

/* Revision of the user file, if it is unchanged from something in the
   repository and we want to use that fact.  */
static char *user_file_rev;

static char *options;
static char *opts;
static size_t opts_allocated = 1;
static int diff_errors;
static int empty_files = 0;

/* FIXME: should be documenting all the options here.  They don't
   perfectly match rcsdiff options (for example, we always support
   --ifdef and --context, but rcsdiff only does if diff does).  */
static const char *const diff_usage[] =
{
    "Usage: %s %s [-lNR] [rcsdiff-options]\n",
    "    [[-r rev1 | -D date1] [-r rev2 | -D date2]] [files...] \n",
    "\t-l\tLocal directory only, not recursive\n",
    "\t-R\tProcess directories recursively.\n",
    "\t-D d1\tDiff revision for date against working file.\n",
    "\t-D d2\tDiff rev1/date1 against date2.\n",
    "\t-N\tinclude diffs for added and removed files.\n",
    "\t-r rev1\tDiff revision for rev1 against working file.\n",
    "\t-r rev2\tDiff rev1/date1 against rev2.\n",
    "\t--ifdef=arg\tOutput diffs in ifdef format.\n",
    "(consult the documentation for your diff program for rcsdiff-options.\n",
    "The most popular is -c for context diffs but there are many more).\n",
    "(Specify the --help global option for a list of other help options)\n",
    NULL
};

/* I copied this array directly out of diff.c in diffutils 2.7, after
   removing the following entries, none of which seem relevant to use
   with CVS:
     --help
     --version
     --recursive
     --unidirectional-new-file
     --starting-file
     --exclude
     --exclude-from
     --sdiff-merge-assist

   I changed the options which take optional arguments (--context and
   --unified) to return a number rather than a letter, so that the
   optional argument could be handled more easily.  I changed the
   --paginate and --brief options to return a number, since -l and -q
   mean something else to cvs diff.

   The numbers 129- that appear in the fourth element of some entries
   tell the big switch in `diff' how to process those options. -- Ian

   The following options, which diff lists as "An alias, no longer
   recommended" have been removed: --file-label --entire-new-file
   --ascii --print.  */

static struct option const longopts[] =
{
    {"ignore-blank-lines", 0, 0, 'B'},
    {"context", 2, 0, 143},
    {"ifdef", 1, 0, 147},
    {"show-function-line", 1, 0, 'F'},
    {"speed-large-files", 0, 0, 'H'},
    {"ignore-matching-lines", 1, 0, 'I'},
    {"label", 1, 0, 'L'},
    {"new-file", 0, 0, 'N'},
    {"initial-tab", 0, 0, 'T'},
    {"width", 1, 0, 'W'},
    {"text", 0, 0, 'a'},
    {"ignore-space-change", 0, 0, 'b'},
    {"minimal", 0, 0, 'd'},
    {"ed", 0, 0, 'e'},
    {"forward-ed", 0, 0, 'f'},
    {"ignore-case", 0, 0, 'i'},
    {"paginate", 0, 0, 144},
    {"rcs", 0, 0, 'n'},
    {"show-c-function", 0, 0, 'p'},

    /* This is a potentially very useful option, except the output is so
       silly.  It would be much better for it to look like "cvs rdiff -s"
       which displays all the same info, minus quite a few lines of
       extraneous garbage.  */
    {"brief", 0, 0, 145},

    {"report-identical-files", 0, 0, 's'},
    {"expand-tabs", 0, 0, 't'},
    {"ignore-all-space", 0, 0, 'w'},
    {"side-by-side", 0, 0, 'y'},
    {"unified", 2, 0, 146},
    {"left-column", 0, 0, 129},
    {"suppress-common-lines", 0, 0, 130},
    {"old-line-format", 1, 0, 132},
    {"new-line-format", 1, 0, 133},
    {"unchanged-line-format", 1, 0, 134},
    {"line-format", 1, 0, 135},
    {"old-group-format", 1, 0, 136},
    {"new-group-format", 1, 0, 137},
    {"unchanged-group-format", 1, 0, 138},
    {"changed-group-format", 1, 0, 139},
    {"horizon-lines", 1, 0, 140},
    {"binary", 0, 0, 142},
    {0, 0, 0, 0}
};

static void strcat_and_allocate PROTO ((char **, size_t *, const char *));

/* *STR is a pointer to a malloc'd string.  *LENP is its allocated
   length.  Add SRC to the end of it, reallocating if necessary.  */
static void
strcat_and_allocate (str, lenp, src)
    char **str;
    size_t *lenp;
    const char *src;
{
    size_t new_size;

    new_size = strlen (*str) + strlen (src) + 1;
    if (*str == NULL || new_size >= *lenp)
    {
	while (new_size >= *lenp)
	    *lenp *= 2;
	*str = xrealloc (*str, *lenp);
    }
    strcat (*str, src);
}

int
diff (argc, argv)
    int argc;
    char **argv;
{
    char tmp[50];
    int c, err = 0;
    int local = 0;
    int which;
    int option_index;

    if (argc == -1)
	usage (diff_usage);

    /*
     * Note that we catch all the valid arguments here, so that we can
     * intercept the -r arguments for doing revision diffs; and -l/-R for a
     * non-recursive/recursive diff.
     */

    /* For server, need to be able to do this command more than once
       (according to the protocol spec, even if the current client
       doesn't use it).  */
    if (opts == NULL)
    {
	opts_allocated = 1;
	opts = xmalloc (opts_allocated);
    }
    opts[0] = '\0';

    optind = 0;
    while ((c = getopt_long (argc, argv,
	       "+abcdefhilnpstuwy0123456789BHNRTC:D:F:I:L:U:V:W:k:r:",
			     longopts, &option_index)) != -1)
    {
	switch (c)
	{
	    case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
	    case 'h': case 'i': case 'n': case 'p': case 's': case 't':
	    case 'u': case 'w': case 'y': case '0': case '1': case '2':
	    case '3': case '4': case '5': case '6': case '7': case '8':
	    case '9': case 'B': case 'H': case 'T':
		(void) sprintf (tmp, " -%c", (char) c);
		strcat_and_allocate (&opts, &opts_allocated, tmp);
		break;
	    case 'C': case 'F': case 'I': case 'L': case 'U': case 'V':
	    case 'W':
		(void) sprintf (tmp, " -%c", (char) c);
		strcat_and_allocate (&opts, &opts_allocated, tmp);
		strcat_and_allocate (&opts, &opts_allocated, optarg);
		break;
	    case 147:
		/* --ifdef.  */
		strcat_and_allocate (&opts, &opts_allocated, " -D");
		strcat_and_allocate (&opts, &opts_allocated, optarg);
		break;
	    case 129: case 130: case 131: case 132: case 133: case 134:
	    case 135: case 136: case 137: case 138: case 139: case 140:
	    case 141: case 142: case 143: case 144: case 145: case 146:
		strcat_and_allocate (&opts, &opts_allocated, " --");
		strcat_and_allocate (&opts, &opts_allocated,
				     longopts[option_index].name);
		if (longopts[option_index].has_arg == 1
		    || (longopts[option_index].has_arg == 2
			&& optarg != NULL))
		{
		    strcat_and_allocate (&opts, &opts_allocated, "=");
		    strcat_and_allocate (&opts, &opts_allocated, optarg);
		}
		break;
	    case 'R':
		local = 0;
		break;
	    case 'l':
		local = 1;
		break;
	    case 'k':
		if (options)
		    free (options);
		options = RCS_check_kflag (optarg);
		break;
	    case 'r':
		if (diff_rev2 != NULL || diff_date2 != NULL)
		    error (1, 0,
		       "no more than two revisions/dates can be specified");
		if (diff_rev1 != NULL || diff_date1 != NULL)
		    diff_rev2 = optarg;
		else
		    diff_rev1 = optarg;
		break;
	    case 'D':
		if (diff_rev2 != NULL || diff_date2 != NULL)
		    error (1, 0,
		       "no more than two revisions/dates can be specified");
		if (diff_rev1 != NULL || diff_date1 != NULL)
		    diff_date2 = Make_Date (optarg);
		else
		    diff_date1 = Make_Date (optarg);
		break;
	    case 'N':
		empty_files = 1;
		break;
	    case '?':
	    default:
		usage (diff_usage);
		break;
	}
    }
    argc -= optind;
    argv += optind;

    /* make sure options is non-null */
    if (!options)
	options = xstrdup ("");

#ifdef CLIENT_SUPPORT
    if (client_active) {
	/* We're the client side.  Fire up the remote server.  */
	start_server ();
	
	ign_setup ();

	if (local)
	    send_arg("-l");
	if (empty_files)
	    send_arg("-N");
	send_option_string (opts);
	if (options[0] != '\0')
	    send_arg (options);
	if (diff_rev1)
	    option_with_arg ("-r", diff_rev1);
	if (diff_date1)
	    client_senddate (diff_date1);
	if (diff_rev2)
	    option_with_arg ("-r", diff_rev2);
	if (diff_date2)
	    client_senddate (diff_date2);

	send_file_names (argc, argv, SEND_EXPAND_WILD);

	/* Send the current files unless diffing two revs from the archive */
	if (diff_rev2 == NULL && diff_date2 == NULL)
	    send_files (argc, argv, local, 0, 0);
	else
	    send_files (argc, argv, local, 0, SEND_NO_CONTENTS);

	send_to_server ("diff\012", 0);
        err = get_responses_and_close ();
	free (options);
	return (err);
    }
#endif

    if (diff_rev1 != NULL)
	tag_check_valid (diff_rev1, argc, argv, local, 0, "");
    if (diff_rev2 != NULL)
	tag_check_valid (diff_rev2, argc, argv, local, 0, "");

    which = W_LOCAL;
    if (diff_rev1 != NULL || diff_date1 != NULL)
	which |= W_REPOS | W_ATTIC;

    wrap_setup ();

    /* start the recursion processor */
    err = start_recursion (diff_fileproc, diff_filesdoneproc, diff_dirproc,
			   diff_dirleaveproc, NULL, argc, argv, local,
			   which, 0, 1, (char *) NULL, 1);

    /* clean up */
    free (options);
    return (err);
}

/*
 * Do a file diff
 */
/* ARGSUSED */
static int
diff_fileproc (callerdat, finfo)
    void *callerdat;
    struct file_info *finfo;
{
    int status, err = 2;		/* 2 == trouble, like rcsdiff */
    Vers_TS *vers;
    enum diff_file empty_file = DIFF_DIFFERENT;
    char *tmp;
    char *tocvsPath;
    char *fname;

    user_file_rev = 0;
    vers = Version_TS (finfo, NULL, NULL, NULL, 1, 0);

    if (diff_rev2 != NULL || diff_date2 != NULL)
    {
	/* Skip all the following checks regarding the user file; we're
	   not using it.  */
    }
    else if (vers->vn_user == NULL)
    {
	/* The file does not exist in the working directory.  */
	if ((diff_rev1 != NULL || diff_date1 != NULL)
	    && vers->srcfile != NULL)
	{
	    /* The file does exist in the repository.  */
	    if (empty_files)
		empty_file = DIFF_REMOVED;
	    else
	    {
		int exists;

		exists = 0;
		/* special handling for TAG_HEAD */
		if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
		    exists = vers->vn_rcs != NULL;
		else
		{
		    Vers_TS *xvers;

		    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1,
					1, 0);
		    exists = xvers->vn_rcs != NULL;
		    freevers_ts (&xvers);
		}
		if (exists)
		    error (0, 0,
			   "%s no longer exists, no comparison available",
			   finfo->fullname);
		freevers_ts (&vers);
		diff_mark_errors (err);
		return (err);
	    }
	}
	else
	{
	    error (0, 0, "I know nothing about %s", finfo->fullname);
	    freevers_ts (&vers);
	    diff_mark_errors (err);
	    return (err);
	}
    }
    else if (vers->vn_user[0] == '0' && vers->vn_user[1] == '\0')
    {
	if (empty_files)
	    empty_file = DIFF_ADDED;
	else
	{
	    error (0, 0, "%s is a new entry, no comparison available",
		   finfo->fullname);
	    freevers_ts (&vers);
	    diff_mark_errors (err);
	    return (err);
	}
    }
    else if (vers->vn_user[0] == '-')
    {
	if (empty_files)
	    empty_file = DIFF_REMOVED;
	else
	{
	    error (0, 0, "%s was removed, no comparison available",
		   finfo->fullname);
	    freevers_ts (&vers);
	    diff_mark_errors (err);
	    return (err);
	}
    }
    else
    {
	if (vers->vn_rcs == NULL && vers->srcfile == NULL)
	{
	    error (0, 0, "cannot find revision control file for %s",
		   finfo->fullname);
	    freevers_ts (&vers);
	    diff_mark_errors (err);
	    return (err);
	}
	else
	{
	    if (vers->ts_user == NULL)
	    {
		error (0, 0, "cannot find %s", finfo->fullname);
		freevers_ts (&vers);
		diff_mark_errors (err);
		return (err);
	    }
	    else if (!strcmp (vers->ts_user, vers->ts_rcs)) 
	    {
		/* The user file matches some revision in the repository
		   Diff against the repository (for remote CVS, we might not
		   have a copy of the user file around).  */
		user_file_rev = vers->vn_user;
	    }
	}
    }

    empty_file = diff_file_nodiff (finfo, vers, empty_file);
    if (empty_file == DIFF_SAME || empty_file == DIFF_ERROR)
    {
	freevers_ts (&vers);
	if (empty_file == DIFF_SAME)
	{
	    /* In the server case, would be nice to send a "Checked-in"
	       response, so that the client can rewrite its timestamp.
	       server_checked_in by itself isn't the right thing (it
	       needs a server_register), but I'm not sure what is.
	       It isn't clear to me how "cvs status" handles this (that
	       is, for a client which sends Modified not Is-modified to
	       "cvs status"), but it does.  */
	    return (0);
	}
	else
	{
	    diff_mark_errors (err);
	    return (err);
	}
    }

    if (empty_file == DIFF_DIFFERENT)
    {
	int dead1, dead2;

	if (use_rev1 == NULL)
	    dead1 = 0;
	else
	    dead1 = RCS_isdead (vers->srcfile, use_rev1);
	if (use_rev2 == NULL)
	    dead2 = 0;
	else
	    dead2 = RCS_isdead (vers->srcfile, use_rev2);

	if (dead1 && dead2)
	{
	    freevers_ts (&vers);
	    return (0);
	}
	else if (dead1)
	{
	    if (empty_files)
	        empty_file = DIFF_ADDED;
	    else
	    {
		error (0, 0, "%s is a new entry, no comparison available",
		       finfo->fullname);
		freevers_ts (&vers);
		diff_mark_errors (err);
		return (err);
	    }
	}
	else if (dead2)
	{
	    if (empty_files)
		empty_file = DIFF_REMOVED;
	    else
	    {
		error (0, 0, "%s was removed, no comparison available",
		       finfo->fullname);
		freevers_ts (&vers);
		diff_mark_errors (err);
		return (err);
	    }
	}
    }

    /* Output an "Index:" line for patch to use */
    cvs_output ("Index: ", 0);
    cvs_output (finfo->fullname, 0);
    cvs_output ("\n", 1);

    tocvsPath = wrap_tocvs_process_file(finfo->file);
    if (tocvsPath)
    {
	/* Backup the current version of the file to CVS/,,filename */
	fname = xmalloc (strlen (finfo->file)
			 + sizeof CVSADM
			 + sizeof CVSPREFIX
			 + 10);
	sprintf(fname,"%s/%s%s",CVSADM, CVSPREFIX, finfo->file);
	if (unlink_file_dir (fname) < 0)
	    if (! existence_error (errno))
		error (1, errno, "cannot remove %s", fname);
	rename_file (finfo->file, fname);
	/* Copy the wrapped file to the current directory then go to work */
	copy_file (tocvsPath, finfo->file);
    }

    if (empty_file == DIFF_ADDED || empty_file == DIFF_REMOVED)
    {
	/* This is file, not fullname, because it is the "Index:" line which
	   is supposed to contain the directory.  */
	cvs_output ("\
===================================================================\n\
RCS file: ", 0);
	cvs_output (finfo->file, 0);
	cvs_output ("\n", 1);

	cvs_output ("diff -N ", 0);
	cvs_output (finfo->file, 0);
	cvs_output ("\n", 1);

	if (empty_file == DIFF_ADDED)
	{
	    if (use_rev2 == NULL)
		status = diff_exec (DEVNULL, finfo->file, opts, RUN_TTY);
	    else
	    {
		int retcode;

		tmp = cvs_temp_name ();
		retcode = RCS_checkout (vers->srcfile, (char *) NULL,
					use_rev2, (char *) NULL,
					(*options
					 ? options
					 : vers->options),
					tmp, (RCSCHECKOUTPROC) NULL,
					(void *) NULL);
		if (retcode == -1)
		{
		    (void) CVS_UNLINK (tmp);
		    error (1, errno, "fork failed during checkout of %s",
			   vers->srcfile->path);
		}
		/* FIXME: what if retcode > 0?  */

		status = diff_exec (DEVNULL, tmp, opts, RUN_TTY);
	    }
	}
	else
	{
	    int retcode;

	    tmp = cvs_temp_name ();
	    retcode = RCS_checkout (vers->srcfile, (char *) NULL,
				    use_rev1, (char *) NULL,
				    *options ? options : vers->options,
				    tmp, (RCSCHECKOUTPROC) NULL,
				    (void *) NULL);
	    if (retcode == -1)
	    {
		(void) CVS_UNLINK (tmp);
		error (1, errno, "fork failed during checkout of %s",
		       vers->srcfile->path);
	    }
	    /* FIXME: what if retcode > 0?  */

	    status = diff_exec (tmp, DEVNULL, opts, RUN_TTY);
	}
    }
    else
    {
	status = RCS_exec_rcsdiff (vers->srcfile->path,
				   opts,
				   *options ? options : vers->options,
				   use_rev1,
				   use_rev2);
    }

    switch (status)
    {
	case -1:			/* fork failed */
	    error (1, errno, "fork failed while diffing %s",
		   vers->srcfile->path);
	case 0:				/* everything ok */
	    err = 0;
	    break;
	default:			/* other error */
	    err = status;
	    break;
    }

    if (tocvsPath)
    {
	if (unlink_file_dir (finfo->file) < 0)
	    if (! existence_error (errno))
		error (1, errno, "cannot remove %s", finfo->file);

	rename_file (fname, finfo->file);
	if (unlink_file (tocvsPath) < 0)
	    error (1, errno, "cannot remove %s", tocvsPath);
	free (fname);
    }

    if (empty_file == DIFF_REMOVED
	|| (empty_file == DIFF_ADDED && use_rev2 != NULL))
    {
	(void) CVS_UNLINK (tmp);
	free (tmp);
    }

    freevers_ts (&vers);
    diff_mark_errors (err);
    return (err);
}

/*
 * Remember the exit status for each file.
 */
static void
diff_mark_errors (err)
    int err;
{
    if (err > diff_errors)
	diff_errors = err;
}

/*
 * Print a warm fuzzy message when we enter a dir
 *
 * Don't try to diff directories that don't exist! -- DW
 */
/* ARGSUSED */
static Dtype
diff_dirproc (callerdat, dir, pos_repos, update_dir, entries)
    void *callerdat;
    char *dir;
    char *pos_repos;
    char *update_dir;
    List *entries;
{
    /* XXX - check for dirs we don't want to process??? */

    /* YES ... for instance dirs that don't exist!!! -- DW */
    if (!isdir (dir) )
      return (R_SKIP_ALL);
  
    if (!quiet)
	error (0, 0, "Diffing %s", update_dir);
    return (R_PROCESS);
}

/*
 * Concoct the proper exit status - done with files
 */
/* ARGSUSED */
static int
diff_filesdoneproc (callerdat, err, repos, update_dir, entries)
    void *callerdat;
    int err;
    char *repos;
    char *update_dir;
    List *entries;
{
    return (diff_errors);
}

/*
 * Concoct the proper exit status - leaving directories
 */
/* ARGSUSED */
static int
diff_dirleaveproc (callerdat, dir, err, update_dir, entries)
    void *callerdat;
    char *dir;
    int err;
    char *update_dir;
    List *entries;
{
    return (diff_errors);
}

/*
 * verify that a file is different
 */
static enum diff_file
diff_file_nodiff (finfo, vers, empty_file)
    struct file_info *finfo;
    Vers_TS *vers;
    enum diff_file empty_file;
{
    Vers_TS *xvers;
    int retcode;

    /* free up any old use_rev* variables and reset 'em */
    if (use_rev1)
	free (use_rev1);
    if (use_rev2)
	free (use_rev2);
    use_rev1 = use_rev2 = (char *) NULL;

    if (diff_rev1 || diff_date1)
    {
	/* special handling for TAG_HEAD */
	if (diff_rev1 && strcmp (diff_rev1, TAG_HEAD) == 0)
	    use_rev1 = xstrdup (vers->vn_rcs);
	else
	{
	    xvers = Version_TS (finfo, NULL, diff_rev1, diff_date1, 1, 0);
	    if (xvers->vn_rcs != NULL)
		use_rev1 = xstrdup (xvers->vn_rcs);
	    freevers_ts (&xvers);
	}
    }
    if (diff_rev2 || diff_date2)
    {
	/* special handling for TAG_HEAD */
	if (diff_rev2 && strcmp (diff_rev2, TAG_HEAD) == 0)
	    use_rev2 = xstrdup (vers->vn_rcs);
	else
	{
	    xvers = Version_TS (finfo, NULL, diff_rev2, diff_date2, 1, 0);
	    if (xvers->vn_rcs != NULL)
		use_rev2 = xstrdup (xvers->vn_rcs);
	    freevers_ts (&xvers);
	}

	if (use_rev1 == NULL)
	{
	    /* The first revision does not exist.  If EMPTY_FILES is
               true, treat this as an added file.  Otherwise, warn
               about the missing tag.  */
	    if (use_rev2 == NULL)
		/* At least in the case where DIFF_REV1 and DIFF_REV2
		   are both numeric, we should be returning some kind
		   of error (see basicb-8a0 in testsuite).  The symbolic
		   case may be more complicated.  */
		return DIFF_SAME;
	    else if (empty_files)
		return DIFF_ADDED;
	    else if (diff_rev1)
		error (0, 0, "tag %s is not in file %s", diff_rev1,
		       finfo->fullname);
	    else
		error (0, 0, "no revision for date %s in file %s",
		       diff_date1, finfo->fullname);
	    return DIFF_ERROR;
	}

	if (use_rev2 == NULL)
	{
	    /* The second revision does not exist.  If EMPTY_FILES is
               true, treat this as a removed file.  Otherwise warn
               about the missing tag.  */
	    if (empty_files)
		return DIFF_REMOVED;
	    else if (diff_rev2)
		error (0, 0, "tag %s is not in file %s", diff_rev2,
		       finfo->fullname);
	    else
		error (0, 0, "no revision for date %s in file %s",
		       diff_date2, finfo->fullname);
	    return DIFF_ERROR;
	}

	/* now, see if we really need to do the diff */
	if (strcmp (use_rev1, use_rev2) == 0)
	    return DIFF_SAME;
	else
	    return DIFF_DIFFERENT;
    }

    if ((diff_rev1 || diff_date1) && use_rev1 == NULL)
    {
	/* The first revision does not exist, and no second revision
           was given.  */
	if (empty_files)
	{
	    if (empty_file == DIFF_REMOVED)
		return DIFF_SAME;
	    else
	    {
		if (user_file_rev && use_rev2 == NULL)
		    use_rev2 = xstrdup (user_file_rev);
		return DIFF_ADDED;
	    }
	}
	else
	{
	    if (diff_rev1)
		error (0, 0, "tag %s is not in file %s", diff_rev1,
		       finfo->fullname);
	    else
		error (0, 0, "no revision for date %s in file %s",
		       diff_date1, finfo->fullname);
	    return DIFF_ERROR;
	}
    }

    if (user_file_rev)
    {
        /* drop user_file_rev into first unused use_rev */
        if (!use_rev1) 
	    use_rev1 = xstrdup (user_file_rev);
	else if (!use_rev2)
	    use_rev2 = xstrdup (user_file_rev);
	/* and if not, it wasn't needed anyhow */
	user_file_rev = 0;
    }

    /* now, see if we really need to do the diff */
    if (use_rev1 && use_rev2) 
    {
	if (strcmp (use_rev1, use_rev2) == 0)
	    return DIFF_SAME;
	else
	    return DIFF_DIFFERENT;
    }

    if (use_rev1 == NULL
	|| (vers->vn_user != NULL && strcmp (use_rev1, vers->vn_user) == 0))
    {
	if (empty_file == DIFF_DIFFERENT
	    && vers->ts_user != NULL
	    && strcmp (vers->ts_rcs, vers->ts_user) == 0
	    && (!(*options) || strcmp (options, vers->options) == 0))
	{
	    return DIFF_SAME;
	}
	if (use_rev1 == NULL
	    && (vers->vn_user[0] != '0' || vers->vn_user[1] != '\0'))
	{
	    if (vers->vn_user[0] == '-')
		use_rev1 = xstrdup (vers->vn_user + 1);
	    else
		use_rev1 = xstrdup (vers->vn_user);
	}
    }

    /* If we already know that the file is being added or removed,
       then we don't want to do an actual file comparison here.  */
    if (empty_file != DIFF_DIFFERENT)
	return empty_file;

    /*
     * with 0 or 1 -r option specified, run a quick diff to see if we
     * should bother with it at all.
     */

    retcode = RCS_cmp_file (vers->srcfile, use_rev1,
			    *options ? options : vers->options,
			    finfo->file);

    return retcode == 0 ? DIFF_SAME : DIFF_DIFFERENT;
}
