/*
 * Copyright (C) 1986-2005 The Free Software Foundation, Inc.
 *
 * Portions Copyright (C) 1998-2005 Derek Price, Ximbiot <http://ximbiot.com>,
 *                                  and others.
 *
 * Portions Copyright (C) 1992, Brian Berliner and Jeff Polk
 * Portions 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 source distribution.
 * 
 */

#include <sys/cdefs.h>
__FBSDID("$FreeBSD: head/contrib/cvs/src/classify.c 179826 2008-06-16 17:06:17Z obrien $");

#include "cvs.h"

static void sticky_ck PROTO ((struct file_info *finfo, int aflag,
			      Vers_TS * vers));



static inline int keywords_may_change PROTO ((int aflag, Vers_TS * vers));
static inline int
keywords_may_change (aflag, vers)
    int aflag;
    Vers_TS * vers;
{
    int retval;

    if (/* Options are different...  */
	strcmp (vers->entdata->options, vers->options)
	/* ...or...  */
	|| (/* ...clearing stickies...  */
	    aflag
	    /* ...and...  */
	    && (/* ...there used to be a tag which subs in Name keys...  */
		(vers->entdata->tag && !isdigit (vers->entdata->tag[0])
		    && vers->tag && !isdigit (vers->tag[0])
		    && strcmp (vers->entdata->tag, vers->tag))
		/* ...or there used to be a keyword mode which may be
		 * changed by -A...
		 */
		|| (strlen (vers->entdata->options)
		    && strcmp (vers->entdata->options, vers->options)
		    && strcmp (vers->entdata->options, "-kkv")
		    && strcmp (vers->entdata->options, "-kb"))))
	/* ...or...  */
	|| (/* ...this is not commit...  */
	    strcmp (cvs_cmd_name, "commit")
	    /* ...and...  */
	    && (/* ...the tag is changing in a way that affects Name keys...  */
		(vers->entdata->tag && vers->tag
		 && strcmp (vers->entdata->tag, vers->tag)
		 && !(isdigit (vers->entdata->tag[0])
		      && isdigit (vers->entdata->tag[0])))
		|| (!vers->entdata->tag && vers->tag
		    && !isdigit (vers->tag[0])))))
	retval = 1;
    else
	retval = 0;

    return retval;
}



/*
 * Classify the state of a file
 */
Ctype
Classify_File (finfo, tag, date, options, force_tag_match, aflag, versp,
	       pipeout)
    struct file_info *finfo;
    char *tag;
    char *date;

    /* Keyword expansion options.  Can be either NULL or "" to
       indicate none are specified here.  */
    char *options;

    int force_tag_match;
    int aflag;
    Vers_TS **versp;
    int pipeout;
{
    Vers_TS *vers;
    Ctype ret;

    /* get all kinds of good data about the file */
    vers = Version_TS (finfo, options, tag, date,
		       force_tag_match, 0);

    if (vers->vn_user == NULL)
    {
	/* No entry available, ts_rcs is invalid */
	if (vers->vn_rcs == NULL)
	{
	    /* there is no RCS file either */
	    if (vers->ts_user == NULL)
	    {
		/* there is no user file */
		/* FIXME: Why do we skip this message if vers->tag or
		   vers->date is set?  It causes "cvs update -r tag98 foo"
		   to silently do nothing, which is seriously confusing
		   behavior.  "cvs update foo" gives this message, which
		   is what I would expect.  */
		if (!force_tag_match || !(vers->tag || vers->date))
		    if (!really_quiet)
			error (0, 0, "nothing known about %s", finfo->fullname);
		ret = T_UNKNOWN;
	    }
	    else
	    {
		/* there is a user file */
		/* FIXME: Why do we skip this message if vers->tag or
		   vers->date is set?  It causes "cvs update -r tag98 foo"
		   to silently do nothing, which is seriously confusing
		   behavior.  "cvs update foo" gives this message, which
		   is what I would expect.  */
		if (!force_tag_match || !(vers->tag || vers->date))
		    if (!really_quiet)
			error (0, 0, "use `%s add' to create an entry for %s",
			       program_name, finfo->fullname);
		ret = T_UNKNOWN;
	    }
	}
	else if (RCS_isdead (vers->srcfile, vers->vn_rcs))
	{
	    /* there is an RCS file, but it's dead */
	    if (vers->ts_user == NULL)
		ret = T_UPTODATE;
	    else
	    {
		error (0, 0, "use `%s add' to create an entry for %s",
		       program_name, finfo->fullname);
		ret = T_UNKNOWN;
	    }
	}
	else if (!pipeout && vers->ts_user && No_Difference (finfo, vers))
	{
	    /* the files were different so it is a conflict */
	    if (!really_quiet)
		error (0, 0, "move away %s; it is in the way",
		       finfo->fullname);
	    ret = T_CONFLICT;
	}
	else
	    /* no user file or no difference, just checkout */
	    ret = T_CHECKOUT;
    }
    else if (strcmp (vers->vn_user, "0") == 0)
    {
	/* An entry for a new-born file; ts_rcs is dummy */

	if (vers->ts_user == NULL)
	{
	    if (pipeout)
	    {
		ret = T_CHECKOUT;
	    }
	    else
	    {
		/*
		 * There is no user file, but there should be one; remove the
		 * entry
		 */
		if (!really_quiet)
		    error (0, 0, "warning: new-born %s has disappeared",
			   finfo->fullname);
		ret = T_REMOVE_ENTRY;
	    }
	}
	else if (vers->vn_rcs == NULL ||
		 RCS_isdead (vers->srcfile, vers->vn_rcs))
	    /* No RCS file or RCS file revision is dead  */
	    ret = T_ADDED;
	else
	{
	    if (pipeout)
	    {
		ret = T_CHECKOUT;
	    }
	    else
	    {
		if (vers->srcfile->flags & INATTIC
		    && vers->srcfile->flags & VALID)
		{
		    /* This file has been added on some branch other than
		       the one we are looking at.  In the branch we are
		       looking at, the file was already valid.  */
		    if (!really_quiet)
			error (0, 0,
			   "conflict: %s has been added, but already exists",
			       finfo->fullname);
		}
		else
		{
		    /*
		     * There is an RCS file, so someone else must have checked
		     * one in behind our back; conflict
		     */
		    if (!really_quiet)
			error (0, 0,
			   "conflict: %s created independently by second party",
			       finfo->fullname);
		}
		ret = T_CONFLICT;
	    }
	}
    }
    else if (vers->vn_user[0] == '-')
    {
	/* An entry for a removed file, ts_rcs is invalid */

	if (vers->ts_user == NULL)
	{
	    /* There is no user file (as it should be) */

	    if (vers->vn_rcs == NULL
		|| RCS_isdead (vers->srcfile, vers->vn_rcs))
	    {

		/*
		 * There is no RCS file; this is all-right, but it has been
		 * removed independently by a second party; remove the entry
		 */
		ret = T_REMOVE_ENTRY;
	    }
	    else if (strcmp (vers->vn_rcs, vers->vn_user + 1) == 0)
		/*
		 * The RCS file is the same version as the user file was, and
		 * that's OK; remove it
		 */
		ret = T_REMOVED;
	    else if (pipeout)
		/*
		 * The RCS file doesn't match the user's file, but it doesn't
		 * matter in this case
		 */
		ret = T_NEEDS_MERGE;
	    else
	    {

		/*
		 * The RCS file is a newer version than the removed user file
		 * and this is definitely not OK; make it a conflict.
		 */
		if (!really_quiet)
		    error (0, 0,
			   "conflict: removed %s was modified by second party",
			   finfo->fullname);
		ret = T_CONFLICT;
	    }
	}
	else
	{
	    /* The user file shouldn't be there */
	    if (!really_quiet)
		error (0, 0, "%s should be removed and is still there",
		       finfo->fullname);
	    ret = T_REMOVED;
	}
    }
    else
    {
	/* A normal entry, TS_Rcs is valid */
	if (vers->vn_rcs == NULL || RCS_isdead (vers->srcfile, vers->vn_rcs))
	{
	    /* There is no RCS file */

	    if (vers->ts_user == NULL)
	    {
		/* There is no user file, so just remove the entry */
		if (!really_quiet)
		    error (0, 0, "warning: %s is not (any longer) pertinent",
			   finfo->fullname);
		ret = T_REMOVE_ENTRY;
	    }
	    else if (strcmp (vers->ts_user, vers->ts_rcs) == 0)
	    {

		/*
		 * The user file is still unmodified, so just remove it from
		 * the entry list
		 */
		if (!really_quiet)
		    error (0, 0, "%s is no longer in the repository",
			   finfo->fullname);
		ret = T_REMOVE_ENTRY;
	    }
	    else if (No_Difference (finfo, vers))
	    {
		/* they are different -> conflict */
		if (!really_quiet)
		    error (0, 0,
	       "conflict: %s is modified but no longer in the repository",
			   finfo->fullname);
		ret = T_CONFLICT;
	    }
	    else
	    {
		/* they weren't really different */
		if (!really_quiet)
		    error (0, 0,
			   "warning: %s is not (any longer) pertinent",
			   finfo->fullname);
		ret = T_REMOVE_ENTRY;
	    }
	}
	else if (strcmp (vers->vn_rcs, vers->vn_user) == 0)
	{
	    /* The RCS file is the same version as the user file */

	    if (vers->ts_user == NULL)
	    {

		/*
		 * There is no user file, so note that it was lost and
		 * extract a new version
		 */
		/* Comparing the cvs_cmd_name against "update", in
		   addition to being an ugly way to operate, means
		   that this message does not get printed by the
		   server.  That might be considered just a straight
		   bug, although there is one subtlety: that case also
		   gets hit when a patch fails and the client fetches
		   a file.  I'm not sure there is currently any way
		   for the server to distinguish those two cases.  */
		if (strcmp (cvs_cmd_name, "update") == 0)
		    if (!really_quiet)
			error (0, 0, "warning: %s was lost", finfo->fullname);
		ret = T_CHECKOUT;
	    }
	    else if (!strcmp (vers->ts_user,
			      vers->ts_conflict
			      ? vers->ts_conflict : vers->ts_rcs))
	    {

		/*
		 * The user file is still unmodified, so nothing special at
		 * all to do -- no lists updated, unless the sticky -k option
		 * has changed.  If the sticky tag has changed, we just need
		 * to re-register the entry
		 */
		/* TODO: decide whether we need to check file permissions
		   for a mismatch, and return T_CONFLICT if so. */
		if (keywords_may_change (aflag, vers))
		    ret = T_PATCH;
		else if (vers->ts_conflict)
		    ret = T_CONFLICT;
		else
		{
		    ret = T_UPTODATE;
		    sticky_ck (finfo, aflag, vers);
		}
	    }
	    else if (No_Difference (finfo, vers))
	    {

		/*
		 * they really are different; modified if we aren't
		 * changing any sticky -k options, else needs merge
		 */
#ifdef XXX_FIXME_WHEN_RCSMERGE_IS_FIXED
		if (strcmp (vers->entdata->options ?
		       vers->entdata->options : "", vers->options) == 0)
		    ret = T_MODIFIED;
		else
		    ret = T_NEEDS_MERGE;
#else
		/* Files with conflict markers and new timestamps fall through
		 * here, but they need to.  T_CONFLICT is an error in
		 * commit_fileproc, whereas T_CONFLICT with conflict markers
		 * is caught but only warned about.  Similarly, update_fileproc
		 * currently reregisters a file that was conflicted but lost
		 * its markers.
		 */
		ret = T_MODIFIED;
		sticky_ck (finfo, aflag, vers);
#endif
	    }
	    else if (strcmp (vers->entdata->options ?
		       vers->entdata->options : "", vers->options) != 0)
	    {
		/* file has not changed; check out if -k changed */
		ret = T_CHECKOUT;
	    }
	    else
	    {

		/*
		 * else -> note that No_Difference will Register the
		 * file already for us, using the new tag/date. This
		 * is the desired behaviour
		 */
		ret = T_UPTODATE;
	    }
	}
	else
	{
	    /* The RCS file is a newer version than the user file */

	    if (vers->ts_user == NULL)
	    {
		/* There is no user file, so just get it */

		/* See comment at other "update" compare, for more
		   thoughts on this comparison.  */
		if (strcmp (cvs_cmd_name, "update") == 0)
		    if (!really_quiet)
			error (0, 0, "warning: %s was lost", finfo->fullname);
		ret = T_CHECKOUT;
	    }
	    else if (strcmp (vers->ts_user, vers->ts_rcs) == 0)

		/*
		 * The user file is still unmodified, so just get it as well
		 */
		ret = T_PATCH;
	    else if (No_Difference (finfo, vers))
		/* really modified, needs to merge */
		ret = T_NEEDS_MERGE;
	    else
		ret = T_PATCH;
	}
    }

    /* free up the vers struct, or just return it */
    if (versp != (Vers_TS **) NULL)
	*versp = vers;
    else
	freevers_ts (&vers);

    /* return the status of the file */
    return (ret);
}

static void
sticky_ck (finfo, aflag, vers)
    struct file_info *finfo;
    int aflag;
    Vers_TS *vers;
{
    if (aflag || vers->tag || vers->date)
    {
	char *enttag = vers->entdata->tag;
	char *entdate = vers->entdata->date;

	if ((enttag && vers->tag && strcmp (enttag, vers->tag)) ||
	    ((enttag && !vers->tag) || (!enttag && vers->tag)) ||
	    (entdate && vers->date && strcmp (entdate, vers->date)) ||
	    ((entdate && !vers->date) || (!entdate && vers->date)))
	{
	    Register (finfo->entries, finfo->file, vers->vn_user, vers->ts_rcs,
		      vers->options, vers->tag, vers->date, vers->ts_conflict);

#ifdef SERVER_SUPPORT
	    if (server_active)
	    {
		/* We need to update the entries line on the client side.
		   It is possible we will later update it again via
		   server_updated or some such, but that is OK.  */
		server_update_entries
		  (finfo->file, finfo->update_dir, finfo->repository,
		   strcmp (vers->ts_rcs, vers->ts_user) == 0 ?
		   SERVER_UPDATED : SERVER_MERGED);
	    }
#endif
	}
    }
}
