tag.c   [plain text]


/*
 * 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.
 *
 * Tag and Rtag
 *
 * Add or delete a symbolic name to an RCS file, or a collection of RCS files.
 * Tag uses the checked out revision in the current directory, rtag uses
 * the modules database, if necessary.
 */

#include "cvs.h"
#include "save-cwd.h"

static int rtag_proc (int argc, char **argv, char *xwhere,
		      char *mwhere, char *mfile, int shorten,
		      int local_specified, char *mname, char *msg);
static int check_fileproc (void *callerdat, struct file_info *finfo);
static int check_filesdoneproc (void *callerdat, int err,
				const char *repos, const char *update_dir,
				List *entries);
static int pretag_proc (const char *_repository, const char *_filter,
                        void *_closure);
static void masterlist_delproc (Node *_p);
static void tag_delproc (Node *_p);
static int pretag_list_to_args_proc (Node *_p, void *_closure);

static Dtype tag_dirproc (void *callerdat, const char *dir,
                          const char *repos, const char *update_dir,
                          List *entries);
static int rtag_fileproc (void *callerdat, struct file_info *finfo);
static int rtag_delete (RCSNode *rcsfile);
static int tag_fileproc (void *callerdat, struct file_info *finfo);

static char *numtag;			/* specific revision to tag */
static bool numtag_validated = false;
static char *date = NULL;
static char *symtag;			/* tag to add or delete */
static bool delete_flag;		/* adding a tag by default */
static bool branch_mode;		/* make an automagic "branch" tag */
static bool disturb_branch_tags = false;/* allow -F,-d to disturb branch tags */
static bool force_tag_match = true;	/* force tag to match by default */
static bool force_tag_move;		/* don't force tag to move by default */
static bool check_uptodate;		/* no uptodate-check by default */
static bool attic_too;			/* remove tag from Attic files */
static bool is_rtag;

struct tag_info
{
    Ctype status;
    char *oldrev;
    char *rev;
    char *tag;
    char *options;
};

struct master_lists
{
    List *tlist;
};

static List *mtlist;

static const char rtag_opts[] = "+aBbdFflnQqRr:D:";
static const char *const rtag_usage[] =
{
    "Usage: %s %s [-abdFflnR] [-r rev|-D date] tag modules...\n",
    "\t-a\tClear tag from removed files that would not otherwise be tagged.\n",
    "\t-b\tMake the tag a \"branch\" tag, allowing concurrent development.\n",
    "\t-B\tAllows -F and -d to disturb branch tags.  Use with extreme care.\n",
    "\t-d\tDelete the given tag.\n",
    "\t-F\tMove tag if it already exists.\n",
    "\t-f\tForce a head revision match if tag/date not found.\n",
    "\t-l\tLocal directory only, not recursive.\n",
    "\t-n\tNo execution of 'tag program'.\n",
    "\t-R\tProcess directories recursively.\n",
    "\t-r rev\tExisting revision/tag.\n",
    "\t-D\tExisting date.\n",
    "(Specify the --help global option for a list of other help options)\n",
    NULL
};

static const char tag_opts[] = "+BbcdFflQqRr:D:";
static const char *const tag_usage[] =
{
    "Usage: %s %s [-bcdFflR] [-r rev|-D date] tag [files...]\n",
    "\t-b\tMake the tag a \"branch\" tag, allowing concurrent development.\n",
    "\t-B\tAllows -F and -d to disturb branch tags.  Use with extreme care.\n",
    "\t-c\tCheck that working files are unmodified.\n",
    "\t-d\tDelete the given tag.\n",
    "\t-F\tMove tag if it already exists.\n",
    "\t-f\tForce a head revision match if tag/date not found.\n",
    "\t-l\tLocal directory only, not recursive.\n",
    "\t-R\tProcess directories recursively.\n",
    "\t-r rev\tExisting revision/tag.\n",
    "\t-D\tExisting date.\n",
    "(Specify the --help global option for a list of other help options)\n",
    NULL
};



int
cvstag (int argc, char **argv)
{
    bool local = false;			/* recursive by default */
    int c;
    int err = 0;
    bool run_module_prog = true;

    is_rtag = (strcmp (cvs_cmd_name, "rtag") == 0);

    if (argc == -1)
	usage (is_rtag ? rtag_usage : tag_usage);

    optind = 0;
    while ((c = getopt (argc, argv, is_rtag ? rtag_opts : tag_opts)) != -1)
    {
	switch (c)
	{
	    case 'a':
		attic_too = true;
		break;
	    case 'b':
		branch_mode = true;
		break;
	    case 'B':
		disturb_branch_tags = true;
		break;
	    case 'c':
		check_uptodate = true;
		break;
	    case 'd':
		delete_flag = true;
		break;
            case 'F':
		force_tag_move = true;
		break;
	    case 'f':
		force_tag_match = false;
		break;
	    case 'l':
		local = true;
		break;
	    case 'n':
		run_module_prog = false;
		break;
	    case 'Q':
	    case 'q':
		/* The CVS 1.5 client sends these options (in addition to
		   Global_option requests), so we must ignore them.  */
		if (!server_active)
		    error (1, 0,
			   "-q or -Q must be specified before \"%s\"",
			   cvs_cmd_name);
		break;
	    case 'R':
		local = false;
		break;
            case 'r':
		parse_tagdate (&numtag, &date, optarg);
                break;
            case 'D':
                if (date) free (date);
                date = Make_Date (optarg);
                break;
	    case '?':
	    default:
		usage (is_rtag ? rtag_usage : tag_usage);
		break;
	}
    }
    argc -= optind;
    argv += optind;

    if (argc < (is_rtag ? 2 : 1))
	usage (is_rtag ? rtag_usage : tag_usage);
    symtag = argv[0];
    argc--;
    argv++;

    if (date && delete_flag)
	error (1, 0, "-d makes no sense with a date specification.");
    if (delete_flag && branch_mode)
	error (0, 0, "warning: -b ignored with -d options");
    RCS_check_tag (symtag);

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

	if (attic_too)
	    send_arg ("-a");
	if (branch_mode)
	    send_arg ("-b");
	if (disturb_branch_tags)
	    send_arg ("-B");
	if (check_uptodate)
	    send_arg ("-c");
	if (delete_flag)
	    send_arg ("-d");
	if (force_tag_move)
	    send_arg ("-F");
	if (!force_tag_match)
	    send_arg ("-f");
	if (local)
	    send_arg ("-l");
	if (!run_module_prog)
	    send_arg ("-n");

	if (numtag)
	    option_with_arg ("-r", numtag);
	if (date)
	    client_senddate (date);

	send_arg ("--");

	send_arg (symtag);

	if (is_rtag)
	{
	    int i;
	    for (i = 0; i < argc; ++i)
		send_arg (argv[i]);
	    send_to_server ("rtag\012", 0);
	}
	else
	{
	    send_files (argc, argv, local, 0,

		    /* I think the -c case is like "cvs status", in
		       which we really better be correct rather than
		       being fast; it is just too confusing otherwise.  */
			check_uptodate ? 0 : SEND_NO_CONTENTS);
	    send_file_names (argc, argv, SEND_EXPAND_WILD);
	    send_to_server ("tag\012", 0);
	}

        return get_responses_and_close ();
    }
#endif

    if (is_rtag)
    {
	DBM *db;
	int i;
	db = open_module ();
	for (i = 0; i < argc; i++)
	{
	    /* XXX last arg should be repository, but doesn't make sense here */
	    history_write ('T', (delete_flag ? "D" : (numtag ? numtag :
			   (date ? date : "A"))), symtag, argv[i], "");
	    err += do_module (db, argv[i], TAG,
			      delete_flag ? "Untagging" : "Tagging",
			      rtag_proc, NULL, 0, local, run_module_prog,
			      0, symtag);
	}
	close_module (db);
    }
    else
    {
	err = rtag_proc (argc + 1, argv - 1, NULL, NULL, NULL, 0, local, NULL,
			 NULL);
    }

    return err;
}



struct pretag_proc_data {
     List *tlist;
     bool delete_flag;
     bool force_tag_move;
     char *symtag;
};

/*
 * called from Parse_Info, this routine processes a line that came out
 * of the posttag file and turns it into a command and executes it.
 *
 * RETURNS
 *    the absolute value of the return value of run_exec, which may or
 *    may not be the return value of the child process.  this is
 *    contrained to return positive values because Parse_Info is summing
 *    return values and testing for non-zeroness to signify one or more
 *    of its callbacks having returned an error.
 */
static int
posttag_proc (const char *repository, const char *filter, void *closure)
{
    char *cmdline;
    const char *srepos = Short_Repository (repository);
    struct pretag_proc_data *ppd = closure;

    /* %t = tag being added/moved/removed
     * %o = operation = "add" | "mov" | "del"
     * %b = branch mode = "?" (delete ops - unknown) | "T" (branch)
     *                    | "N" (not branch)
     * %c = cvs_cmd_name
     * %p = path from $CVSROOT
     * %r = path from root
     * %{sVv} = attribute list = file name, old version tag will be deleted
     *                           from, new version tag will be added to (or
     *                           deleted from until
     *                           SUPPORT_OLD_INFO_FMT_STRINGS is undefined).
     */
    /*
     * Cast any NULL arguments as appropriate pointers as this is an
     * stdarg function and we need to be certain the caller gets what
     * is expected.
     */
    cmdline = format_cmdline (
#ifdef SUPPORT_OLD_INFO_FMT_STRINGS
			      false, srepos,
#endif /* SUPPORT_OLD_INFO_FMT_STRINGS */
			      filter,
			      "t", "s", ppd->symtag,
			      "o", "s", ppd->delete_flag
			      ? "del" : ppd->force_tag_move ? "mov" : "add",
			      "b", "c", delete_flag
			      ? '?' : branch_mode ? 'T' : 'N',
			      "c", "s", cvs_cmd_name,
#ifdef SERVER_SUPPORT
			      "R", "s", referrer ? referrer->original : "NONE",
#endif /* SERVER_SUPPORT */
			      "p", "s", srepos,
			      "r", "s", current_parsed_root->directory,
			      "sVv", ",", ppd->tlist,
			      pretag_list_to_args_proc, (void *) NULL,
			      (char *) NULL);

    if (!cmdline || !strlen (cmdline))
    {
	if (cmdline) free (cmdline);
	error (0, 0, "pretag proc resolved to the empty string!");
	return 1;
    }

    run_setup (cmdline);

    free (cmdline);
    return abs (run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL));
}



/*
 * Call any postadmin procs.
 */
static int
tag_filesdoneproc (void *callerdat, int err, const char *repository,
                   const char *update_dir, List *entries)
{
    Node *p;
    List *mtlist, *tlist;
    struct pretag_proc_data ppd;

    TRACE (TRACE_FUNCTION, "tag_filesdoneproc (%d, %s, %s)", err, repository,
           update_dir);

    mtlist = callerdat;
    p = findnode (mtlist, update_dir);
    if (p != NULL)
        tlist = ((struct master_lists *) p->data)->tlist;
    else
        tlist = NULL;
    if (tlist == NULL || tlist->list->next == tlist->list)
        return err;

    ppd.tlist = tlist;
    ppd.delete_flag = delete_flag;
    ppd.force_tag_move = force_tag_move;
    ppd.symtag = symtag;
    Parse_Info (CVSROOTADM_POSTTAG, repository, posttag_proc,
                PIOPT_ALL, &ppd);

    return err;
}



/*
 * callback proc for doing the real work of tagging
 */
/* ARGSUSED */
static int
rtag_proc (int argc, char **argv, char *xwhere, char *mwhere, char *mfile,
           int shorten, int local_specified, char *mname, char *msg)
{
    /* Begin section which is identical to patch_proc--should this
       be abstracted out somehow?  */
    char *myargv[2];
    int err = 0;
    int which;
    char *repository;
    char *where;

#ifdef HAVE_PRINTF_PTR
    TRACE (TRACE_FUNCTION,
	   "rtag_proc (argc=%d, argv=%p, xwhere=%s,\n"
      "                mwhere=%s, mfile=%s, shorten=%d,\n"
      "                local_specified=%d, mname=%s, msg=%s)",
	    argc, (void *)argv, xwhere ? xwhere : "(null)",
	    mwhere ? mwhere : "(null)", mfile ? mfile : "(null)",
	    shorten, local_specified,
	    mname ? mname : "(null)", msg ? msg : "(null)" );
#else
    TRACE (TRACE_FUNCTION,
	   "rtag_proc (argc=%d, argv=%lx, xwhere=%s,\n"
      "                mwhere=%s, mfile=%s, shorten=%d,\n"
      "                local_specified=%d, mname=%s, msg=%s )",
	    argc, (unsigned long)argv, xwhere ? xwhere : "(null)",
	    mwhere ? mwhere : "(null)", mfile ? mfile : "(null)",
	    shorten, local_specified,
	    mname ? mname : "(null)", msg ? msg : "(null)" );
#endif

    if (is_rtag)
    {
	repository = xmalloc (strlen (current_parsed_root->directory)
                              + strlen (argv[0])
			      + (mfile == NULL ? 0 : strlen (mfile) + 1)
                              + 2);
	(void) sprintf (repository, "%s/%s", current_parsed_root->directory,
                        argv[0]);
	where = xmalloc (strlen (argv[0])
                         + (mfile == NULL ? 0 : strlen (mfile) + 1)
			 + 1);
	(void) strcpy (where, argv[0]);

	/* If MFILE isn't null, we need to set up to do only part of the
         * module.
         */
	if (mfile != NULL)
	{
	    char *cp;
	    char *path;

	    /* If the portion of the module is a path, put the dir part on
             * REPOS.
             */
	    if ((cp = strrchr (mfile, '/')) != NULL)
	    {
		*cp = '\0';
		(void) strcat (repository, "/");
		(void) strcat (repository, mfile);
		(void) strcat (where, "/");
		(void) strcat (where, mfile);
		mfile = cp + 1;
	    }

	    /* take care of the rest */
	    path = xmalloc (strlen (repository) + strlen (mfile) + 5);
	    (void) sprintf (path, "%s/%s", repository, mfile);
	    if (isdir (path))
	    {
		/* directory means repository gets the dir tacked on */
		(void) strcpy (repository, path);
		(void) strcat (where, "/");
		(void) strcat (where, mfile);
	    }
	    else
	    {
		myargv[0] = argv[0];
		myargv[1] = mfile;
		argc = 2;
		argv = myargv;
	    }
	    free (path);
	}

	/* cd to the starting repository */
	if (CVS_CHDIR (repository) < 0)
	{
	    error (0, errno, "cannot chdir to %s", repository);
	    free (repository);
	    free (where);
	    return 1;
	}
	/* End section which is identical to patch_proc.  */

	if (delete_flag || attic_too || (force_tag_match && numtag))
	    which = W_REPOS | W_ATTIC;
	else
	    which = W_REPOS;
    }
    else
    {
        where = NULL;
        which = W_LOCAL;
        repository = "";
    }

    if (numtag != NULL && !numtag_validated)
    {
	tag_check_valid (numtag, argc - 1, argv + 1, local_specified, 0,
			 repository, false);
	numtag_validated = true;
    }

    /* check to make sure they are authorized to tag all the
       specified files in the repository */

    mtlist = getlist ();
    err = start_recursion (check_fileproc, check_filesdoneproc,
                           NULL, NULL, NULL,
			   argc - 1, argv + 1, local_specified, which, 0,
			   CVS_LOCK_READ, where, 1, repository);

    if (err)
    {
       error (1, 0, "correct the above errors first!");
    }

    /* It would be nice to provide consistency with respect to
       commits; however CVS lacks the infrastructure to do that (see
       Concurrency in cvs.texinfo and comment in do_recursion).  */

    /* start the recursion processor */
    err = start_recursion
	(is_rtag ? rtag_fileproc : tag_fileproc,
	 tag_filesdoneproc, tag_dirproc, NULL, mtlist, argc - 1, argv + 1,
	 local_specified, which, 0, CVS_LOCK_WRITE, where, 1,
	 repository);
    dellist (&mtlist);
    if (which & W_REPOS) free (repository);
    if (where != NULL)
	free (where);
    return err;
}



/* check file that is to be tagged */
/* All we do here is add it to our list */
static int
check_fileproc (void *callerdat, struct file_info *finfo)
{
    const char *xdir;
    Node *p;
    Vers_TS *vers;
    List *tlist;
    struct tag_info *ti;
    int addit = 1;

    TRACE (TRACE_FUNCTION, "check_fileproc (%s, %s, %s)",
	   finfo->repository ? finfo->repository : "(null)",
	   finfo->fullname ? finfo->fullname : "(null)",
	   finfo->rcs ? (finfo->rcs->path ? finfo->rcs->path : "(null)")
	   : "NULL");

    if (check_uptodate)
    {
	switch (Classify_File (finfo, NULL, NULL, NULL, 1, 0, &vers, 0))
	{
	case T_UPTODATE:
	case T_CHECKOUT:
	case T_PATCH:
	case T_REMOVE_ENTRY:
	    break;
	case T_UNKNOWN:
	case T_CONFLICT:
	case T_NEEDS_MERGE:
	case T_MODIFIED:
	case T_ADDED:
	case T_REMOVED:
	default:
	    error (0, 0, "%s is locally modified", finfo->fullname);
	    freevers_ts (&vers);
	    return 1;
	}
    }
    else
	vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0);

    if (finfo->update_dir[0] == '\0')
	xdir = ".";
    else
	xdir = finfo->update_dir;
    if ((p = findnode (mtlist, xdir)) != NULL)
    {
	tlist = ((struct master_lists *) p->data)->tlist;
    }
    else
    {
	struct master_lists *ml;

	tlist = getlist ();
	p = getnode ();
	p->key = xstrdup (xdir);
	p->type = UPDATE;
	ml = xmalloc (sizeof (struct master_lists));
	ml->tlist = tlist;
	p->data = ml;
	p->delproc = masterlist_delproc;
	(void) addnode (mtlist, p);
    }
    /* do tlist */
    p = getnode ();
    p->key = xstrdup (finfo->file);
    p->type = UPDATE;
    p->delproc = tag_delproc;
    if (vers->srcfile == NULL)
    {
        if (!really_quiet)
	    error (0, 0, "nothing known about %s", finfo->file);
	freevers_ts (&vers);
	freenode (p);
	return 1;
    }

    /* Here we duplicate the calculation in tag_fileproc about which
       version we are going to tag.  There probably are some subtle races
       (e.g. numtag is "foo" which gets moved between here and
       tag_fileproc).  */
    p->data = ti = xmalloc (sizeof (struct tag_info));
    ti->tag = xstrdup (numtag ? numtag : vers->tag);
    if (!is_rtag && numtag == NULL && date == NULL)
	ti->rev = xstrdup (vers->vn_user);
    else
	ti->rev = RCS_getversion (vers->srcfile, numtag, date,
				  force_tag_match, NULL);

    if (ti->rev != NULL)
    {
        ti->oldrev = RCS_getversion (vers->srcfile, symtag, NULL, 1, NULL);

	if (ti->oldrev == NULL)
        {
            if (delete_flag)
            {
		/* Deleting a tag which did not exist is a noop and
		   should not be logged.  */
                addit = 0;
            }
        }
	else if (delete_flag)
	{
	    free (ti->rev);
#ifdef SUPPORT_OLD_INFO_FMT_STRINGS
	    /* a hack since %v used to mean old or new rev */
	    ti->rev = xstrdup (ti->oldrev);
#else /* SUPPORT_OLD_INFO_FMT_STRINGS */
	    ti->rev = NULL;
#endif /* SUPPORT_OLD_INFO_FMT_STRINGS */
	}
        else if (strcmp(ti->oldrev, p->data) == 0)
            addit = 0;
        else if (!force_tag_move)
            addit = 0;
    }
    else
	addit = 0;
    if (!addit)
    {
	free(p->data);
	p->data = NULL;
    }
    freevers_ts (&vers);
    (void)addnode (tlist, p);
    return 0;
}



static int
check_filesdoneproc (void *callerdat, int err, const char *repos,
                     const char *update_dir, List *entries)
{
    int n;
    Node *p;
    List *tlist;
    struct pretag_proc_data ppd;

    p = findnode (mtlist, update_dir);
    if (p != NULL)
        tlist = ((struct master_lists *) p->data)->tlist;
    else
        tlist = NULL;
    if (tlist == NULL || tlist->list->next == tlist->list)
        return err;

    ppd.tlist = tlist;
    ppd.delete_flag = delete_flag;
    ppd.force_tag_move = force_tag_move;
    ppd.symtag = symtag;
    if ((n = Parse_Info (CVSROOTADM_TAGINFO, repos, pretag_proc, PIOPT_ALL,
			 &ppd)) > 0)
    {
        error (0, 0, "Pre-tag check failed");
        err += n;
    }
    return err;
}



/*
 * called from Parse_Info, this routine processes a line that came out
 * of a taginfo file and turns it into a command and executes it.
 *
 * RETURNS
 *    the absolute value of the return value of run_exec, which may or
 *    may not be the return value of the child process.  this is
 *    contrained to return positive values because Parse_Info is adding up
 *    return values and testing for non-zeroness to signify one or more
 *    of its callbacks having returned an error.
 */
static int
pretag_proc (const char *repository, const char *filter, void *closure)
{
    char *newfilter = NULL;
    char *cmdline;
    const char *srepos = Short_Repository (repository);
    struct pretag_proc_data *ppd = closure;

#ifdef SUPPORT_OLD_INFO_FMT_STRINGS
    if (!strchr (filter, '%'))
    {
	error (0,0,
               "warning: taginfo line contains no format strings:\n"
               "    \"%s\"\n"
               "Filling in old defaults ('%%t %%o %%p %%{sv}'), but please be aware that this\n"
               "usage is deprecated.", filter);
	newfilter = xmalloc (strlen (filter) + 16);
	strcpy (newfilter, filter);
	strcat (newfilter, " %t %o %p %{sv}");
	filter = newfilter;
    }
#endif /* SUPPORT_OLD_INFO_FMT_STRINGS */

    /* %t = tag being added/moved/removed
     * %o = operation = "add" | "mov" | "del"
     * %b = branch mode = "?" (delete ops - unknown) | "T" (branch)
     *                    | "N" (not branch)
     * %c = cvs_cmd_name
     * %p = path from $CVSROOT
     * %r = path from root
     * %{sVv} = attribute list = file name, old version tag will be deleted
     *                           from, new version tag will be added to (or
     *                           deleted from until
     *                           SUPPORT_OLD_INFO_FMT_STRINGS is undefined)
     */
    /*
     * Cast any NULL arguments as appropriate pointers as this is an
     * stdarg function and we need to be certain the caller gets what
     * is expected.
     */
    cmdline = format_cmdline (
#ifdef SUPPORT_OLD_INFO_FMT_STRINGS
			      false, srepos,
#endif /* SUPPORT_OLD_INFO_FMT_STRINGS */
			      filter,
			      "t", "s", ppd->symtag,
			      "o", "s", ppd->delete_flag ? "del" :
			      ppd->force_tag_move ? "mov" : "add",
			      "b", "c", delete_flag
			      ? '?' : branch_mode ? 'T' : 'N',
			      "c", "s", cvs_cmd_name,
#ifdef SERVER_SUPPORT
			      "R", "s", referrer ? referrer->original : "NONE",
#endif /* SERVER_SUPPORT */
			      "p", "s", srepos,
			      "r", "s", current_parsed_root->directory,
			      "sVv", ",", ppd->tlist,
			      pretag_list_to_args_proc, (void *) NULL,
			      (char *) NULL);

    if (newfilter) free (newfilter);

    if (!cmdline || !strlen (cmdline))
    {
	if (cmdline) free (cmdline);
	error (0, 0, "pretag proc resolved to the empty string!");
	return 1;
    }

    run_setup (cmdline);

    /* FIXME - the old code used to run the following here:
     *
     * if (!isfile(s))
     * {
     *     error (0, errno, "cannot find pre-tag filter '%s'", s);
     *     free(s);
     *     return (1);
     * }
     *
     * not sure this is really necessary.  it might give a little finer grained
     * error than letting the execution attempt fail but i'm not sure.  in any
     * case it should be easy enough to add a function in run.c to test its
     * first arg for fileness & executability.
     */

    free (cmdline);
    return abs (run_exec (RUN_TTY, RUN_TTY, RUN_TTY, RUN_NORMAL));
}



static void
masterlist_delproc (Node *p)
{
    struct master_lists *ml = p->data;

    dellist (&ml->tlist);
    free (ml);
    return;
}



static void
tag_delproc (Node *p)
{
    struct tag_info *ti;
    if (p->data)
    {
	ti = (struct tag_info *) p->data;
	if (ti->oldrev) free (ti->oldrev);
	if (ti->rev) free (ti->rev);
	free (ti->tag);
        free (p->data);
        p->data = NULL;
    }
    return;
}



/* to be passed into walklist with a list of tags
 * p->key = tagname
 * p->data = struct tag_info *
 * p->data->oldrev = rev tag will be deleted from
 * p->data->rev = rev tag will be added to
 * p->data->tag = tag oldrev is attached to, if any
 *
 * closure will be a struct format_cmdline_walklist_closure
 * where closure is undefined
 */
static int
pretag_list_to_args_proc (Node *p, void *closure)
{
    struct tag_info *taginfo = (struct tag_info *)p->data;
    struct format_cmdline_walklist_closure *c =
            (struct format_cmdline_walklist_closure *)closure;
    char *arg = NULL;
    const char *f;
    char *d;
    size_t doff;

    if (!p->data) return 1;

    f = c->format;
    d = *c->d;
    /* foreach requested attribute */
    while (*f)
    {
   	switch (*f++)
	{
	    case 's':
		arg = p->key;
		break;
	    case 'T':
		arg = taginfo->tag ? taginfo->tag : "";
		break;
	    case 'v':
		arg = taginfo->rev ? taginfo->rev : "NONE";
		break;
	    case 'V':
		arg = taginfo->oldrev ? taginfo->oldrev : "NONE";
		break;
	    default:
		error(1,0,
                      "Unknown format character or not a list attribute: %c",
		      f[-1]);
		break;
	}
	/* copy the attribute into an argument */
	if (c->quotes)
	{
	    arg = cmdlineescape (c->quotes, arg);
	}
	else
	{
	    arg = cmdlinequote ('"', arg);
	}

	doff = d - *c->buf;
	expand_string (c->buf, c->length, doff + strlen (arg));
	d = *c->buf + doff;
	strncpy (d, arg, strlen (arg));
	d += strlen (arg);

	free (arg);

	/* and always put the extra space on.  we'll have to back up a char when we're
	 * done, but that seems most efficient
	 */
	doff = d - *c->buf;
	expand_string (c->buf, c->length, doff + 1);
	d = *c->buf + doff;
	*d++ = ' ';
    }
    /* correct our original pointer into the buff */
    *c->d = d;
    return 0;
}


/*
 * Called to rtag a particular file, as appropriate with the options that were
 * set above.
 */
/* ARGSUSED */
static int
rtag_fileproc (void *callerdat, struct file_info *finfo)
{
    RCSNode *rcsfile;
    char *version = NULL, *rev = NULL;
    int retcode = 0;
    int retval = 0;
    static bool valtagged = false;

    /* find the parsed RCS data */
    if ((rcsfile = finfo->rcs) == NULL)
    {
	retval = 1;
	goto free_vars_and_return;
    }

    /*
     * For tagging an RCS file which is a symbolic link, you'd best be
     * running with RCS 5.6, since it knows how to handle symbolic links
     * correctly without breaking your link!
     */

    if (delete_flag)
    {
	retval = rtag_delete (rcsfile);
	goto free_vars_and_return;
    }

    /*
     * If we get here, we are adding a tag.  But, if -a was specified, we
     * need to check to see if a -r or -D option was specified.  If neither
     * was specified and the file is in the Attic, remove the tag.
     */
    if (attic_too && (!numtag && !date))
    {
	if ((rcsfile->flags & VALID) && (rcsfile->flags & INATTIC))
	{
	    retval = rtag_delete (rcsfile);
	    goto free_vars_and_return;
	}
    }

    version = RCS_getversion (rcsfile, numtag, date, force_tag_match, NULL);
    if (version == NULL)
    {
	/* If -a specified, clean up any old tags */
	if (attic_too)
	    (void)rtag_delete (rcsfile);

	if (!quiet && !force_tag_match)
	{
	    error (0, 0, "cannot find tag `%s' in `%s'",
		   numtag ? numtag : "head", rcsfile->path);
	    retval = 1;
	}
	goto free_vars_and_return;
    }
    if (numtag
	&& isdigit ((unsigned char)*numtag)
	&& strcmp (numtag, version) != 0)
    {

	/*
	 * We didn't find a match for the numeric tag that was specified, but
	 * that's OK.  just pass the numeric tag on to rcs, to be tagged as
	 * specified.  Could get here if one tried to tag "1.1.1" and there
	 * was a 1.1.1 branch with some head revision.  In this case, we want
	 * the tag to reference "1.1.1" and not the revision at the head of
	 * the branch.  Use a symbolic tag for that.
	 */
	rev = branch_mode ? RCS_magicrev (rcsfile, version) : numtag;
	retcode = RCS_settag(rcsfile, symtag, numtag);
	if (retcode == 0)
	    RCS_rewrite (rcsfile, NULL, NULL);
    }
    else
    {
	char *oversion;

	/*
	 * As an enhancement for the case where a tag is being re-applied to
	 * a large body of a module, make one extra call to RCS_getversion to
	 * see if the tag is already set in the RCS file.  If so, check to
	 * see if it needs to be moved.  If not, do nothing.  This will
	 * likely save a lot of time when simply moving the tag to the
	 * "current" head revisions of a module -- which I have found to be a
	 * typical tagging operation.
	 */
	rev = branch_mode ? RCS_magicrev (rcsfile, version) : version;
	oversion = RCS_getversion (rcsfile, symtag, NULL, 1, NULL);
	if (oversion != NULL)
	{
	    int isbranch = RCS_nodeisbranch (finfo->rcs, symtag);

	    /*
	     * if versions the same and neither old or new are branches don't
	     * have to do anything
	     */
	    if (strcmp (version, oversion) == 0 && !branch_mode && !isbranch)
	    {
		free (oversion);
		goto free_vars_and_return;
	    }

	    if (!force_tag_move)
	    {
		/* we're NOT going to move the tag */
		(void)printf ("W %s", finfo->fullname);

		(void)printf (" : %s already exists on %s %s",
			      symtag, isbranch ? "branch" : "version",
			      oversion);
		(void)printf (" : NOT MOVING tag to %s %s\n",
			      branch_mode ? "branch" : "version", rev);
		free (oversion);
		goto free_vars_and_return;
	    }
	    else /* force_tag_move is set and... */
		if ((isbranch && !disturb_branch_tags) ||
		    (!isbranch && disturb_branch_tags))
	    {
	        error(0,0, "%s: Not moving %s tag `%s' from %s to %s%s.",
			finfo->fullname,
			isbranch ? "branch" : "non-branch",
			symtag, oversion, rev,
			isbranch ? "" : " due to `-B' option");
		free (oversion);
		goto free_vars_and_return;
	    }
	    free (oversion);
	}
	retcode = RCS_settag (rcsfile, symtag, rev);
	if (retcode == 0)
	    RCS_rewrite (rcsfile, NULL, NULL);
    }

    if (retcode != 0)
    {
	error (1, retcode == -1 ? errno : 0,
	       "failed to set tag `%s' to revision `%s' in `%s'",
	       symtag, rev, rcsfile->path);
        retval = 1;
	goto free_vars_and_return;
    }

free_vars_and_return:
    if (branch_mode && rev) free (rev);
    if (version) free (version);
    if (!delete_flag && !retval && !valtagged)
    {
	tag_check_valid (symtag, 0, NULL, 0, 0, NULL, true);
	valtagged = true;
    }
    return retval;
}



/*
 * If -d is specified, "force_tag_match" is set, so that this call to
 * RCS_getversion() will return a NULL version string if the symbolic
 * tag does not exist in the RCS file.
 *
 * If the -r flag was used, numtag is set, and we only delete the
 * symtag from files that have numtag.
 *
 * This is done here because it's MUCH faster than just blindly calling
 * "rcs" to remove the tag... trust me.
 */
static int
rtag_delete (RCSNode *rcsfile)
{
    char *version;
    int retcode, isbranch;

    if (numtag)
    {
	version = RCS_getversion (rcsfile, numtag, NULL, 1, NULL);
	if (version == NULL)
	    return (0);
	free (version);
    }

    version = RCS_getversion (rcsfile, symtag, NULL, 1, NULL);
    if (version == NULL)
	return 0;
    free (version);


    isbranch = RCS_nodeisbranch (rcsfile, symtag);
    if ((isbranch && !disturb_branch_tags) ||
	(!isbranch && disturb_branch_tags))
    {
	if (!quiet)
	    error (0, 0,
                   "Not removing %s tag `%s' from `%s'%s.",
                   isbranch ? "branch" : "non-branch",
                   symtag, rcsfile->path,
                   isbranch ? "" : " due to `-B' option");
	return 1;
    }

    if ((retcode = RCS_deltag(rcsfile, symtag)) != 0)
    {
	if (!quiet)
	    error (0, retcode == -1 ? errno : 0,
		   "failed to remove tag `%s' from `%s'", symtag,
		   rcsfile->path);
	return 1;
    }
    RCS_rewrite (rcsfile, NULL, NULL);
    return 0;
}



/*
 * Called to tag a particular file (the currently checked out version is
 * tagged with the specified tag - or the specified tag is deleted).
 */
/* ARGSUSED */
static int
tag_fileproc (void *callerdat, struct file_info *finfo)
{
    char *version, *oversion;
    char *nversion = NULL;
    char *rev;
    Vers_TS *vers;
    int retcode = 0;
    int retval = 0;
    static bool valtagged = false;

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

    if (numtag || date)
    {
        nversion = RCS_getversion (vers->srcfile, numtag, date,
                                   force_tag_match, NULL);
        if (!nversion)
	    goto free_vars_and_return;
    }
    if (delete_flag)
    {

	int isbranch;
	/*
	 * If -d is specified, "force_tag_match" is set, so that this call to
	 * RCS_getversion() will return a NULL version string if the symbolic
	 * tag does not exist in the RCS file.
	 *
	 * This is done here because it's MUCH faster than just blindly calling
	 * "rcs" to remove the tag... trust me.
	 */

	version = RCS_getversion (vers->srcfile, symtag, NULL, 1, NULL);
	if (version == NULL || vers->srcfile == NULL)
	    goto free_vars_and_return;

	free (version);

	isbranch = RCS_nodeisbranch (finfo->rcs, symtag);
	if ((isbranch && !disturb_branch_tags) ||
	    (!isbranch && disturb_branch_tags))
	{
	    if (!quiet)
		error(0, 0,
		       "Not removing %s tag `%s' from `%s'%s.",
			isbranch ? "branch" : "non-branch",
			symtag, vers->srcfile->path,
			isbranch ? "" : " due to `-B' option");
	    retval = 1;
	    goto free_vars_and_return;
	}

	if ((retcode = RCS_deltag (vers->srcfile, symtag)) != 0)
	{
	    if (!quiet)
		error (0, retcode == -1 ? errno : 0,
		       "failed to remove tag %s from %s", symtag,
		       vers->srcfile->path);
	    retval = 1;
	    goto free_vars_and_return;
	}
	RCS_rewrite (vers->srcfile, NULL, NULL);

	/* warm fuzzies */
	if (!really_quiet)
	{
	    cvs_output ("D ", 2);
	    cvs_output (finfo->fullname, 0);
	    cvs_output ("\n", 1);
	}

	goto free_vars_and_return;
    }

    /*
     * If we are adding a tag, we need to know which version we have checked
     * out and we'll tag that version.
     */
    if (!nversion)
        version = vers->vn_user;
    else
        version = nversion;
    if (!version)
	goto free_vars_and_return;
    else if (strcmp (version, "0") == 0)
    {
	if (!quiet)
	    error (0, 0, "couldn't tag added but un-commited file `%s'",
	           finfo->file);
	goto free_vars_and_return;
    }
    else if (version[0] == '-')
    {
	if (!quiet)
	    error (0, 0, "skipping removed but un-commited file `%s'",
		   finfo->file);
	goto free_vars_and_return;
    }
    else if (vers->srcfile == NULL)
    {
	if (!quiet)
	    error (0, 0, "cannot find revision control file for `%s'",
		   finfo->file);
	goto free_vars_and_return;
    }

    /*
     * As an enhancement for the case where a tag is being re-applied to a
     * large number of files, make one extra call to RCS_getversion to see
     * if the tag is already set in the RCS file.  If so, check to see if it
     * needs to be moved.  If not, do nothing.  This will likely save a lot of
     * time when simply moving the tag to the "current" head revisions of a
     * module -- which I have found to be a typical tagging operation.
     */
    rev = branch_mode ? RCS_magicrev (vers->srcfile, version) : version;
    oversion = RCS_getversion (vers->srcfile, symtag, NULL, 1, NULL);
    if (oversion != NULL)
    {
	int isbranch = RCS_nodeisbranch (finfo->rcs, symtag);

	/*
	 * if versions the same and neither old or new are branches don't have
	 * to do anything
	 */
	if (strcmp (version, oversion) == 0 && !branch_mode && !isbranch)
	{
	    free (oversion);
	    if (branch_mode)
		free (rev);
	    goto free_vars_and_return;
	}

	if (!force_tag_move)
	{
	    /* we're NOT going to move the tag */
	    cvs_output ("W ", 2);
	    cvs_output (finfo->fullname, 0);
	    cvs_output (" : ", 0);
	    cvs_output (symtag, 0);
	    cvs_output (" already exists on ", 0);
	    cvs_output (isbranch ? "branch" : "version", 0);
	    cvs_output (" ", 0);
	    cvs_output (oversion, 0);
	    cvs_output (" : NOT MOVING tag to ", 0);
	    cvs_output (branch_mode ? "branch" : "version", 0);
	    cvs_output (" ", 0);
	    cvs_output (rev, 0);
	    cvs_output ("\n", 1);
	    free (oversion);
	    if (branch_mode)
		free (rev);
	    goto free_vars_and_return;
	}
	else 	/* force_tag_move == 1 and... */
		if ((isbranch && !disturb_branch_tags) ||
		    (!isbranch && disturb_branch_tags))
	{
	    error (0,0, "%s: Not moving %s tag `%s' from %s to %s%s.",
		   finfo->fullname,
		   isbranch ? "branch" : "non-branch",
		   symtag, oversion, rev,
		   isbranch ? "" : " due to `-B' option");
	    free (oversion);
	    if (branch_mode)
		free (rev);
	    goto free_vars_and_return;
	}
	free (oversion);
    }

    if ((retcode = RCS_settag(vers->srcfile, symtag, rev)) != 0)
    {
	error (1, retcode == -1 ? errno : 0,
	       "failed to set tag %s to revision %s in %s",
	       symtag, rev, vers->srcfile->path);
	if (branch_mode)
	    free (rev);
	retval = 1;
	goto free_vars_and_return;
    }
    if (branch_mode)
	free (rev);
    RCS_rewrite (vers->srcfile, NULL, NULL);

    /* more warm fuzzies */
    if (!really_quiet)
    {
	cvs_output ("T ", 2);
	cvs_output (finfo->fullname, 0);
	cvs_output ("\n", 1);
    }

 free_vars_and_return:
    if (nversion != NULL)
        free (nversion);
    freevers_ts (&vers);
    if (!delete_flag && !retval && !valtagged)
    {
	tag_check_valid (symtag, 0, NULL, 0, 0, NULL, true);
	valtagged = true;
    }
    return retval;
}



/*
 * Print a warm fuzzy message
 */
/* ARGSUSED */
static Dtype
tag_dirproc (void *callerdat, const char *dir, const char *repos,
             const char *update_dir, List *entries)
{

    if (ignore_directory (update_dir))
    {
	/* print the warm fuzzy message */
	if (!quiet)
	  error (0, 0, "Ignoring %s", update_dir);
        return R_SKIP_ALL;
    }

    if (!quiet)
	error (0, 0, "%s %s", delete_flag ? "Untagging" : "Tagging",
               update_dir);
    return R_PROCESS;
}



/* Code relating to the val-tags file.  Note that this file has no way
   of knowing when a tag has been deleted.  The problem is that there
   is no way of knowing whether a tag still exists somewhere, when we
   delete it some places.  Using per-directory val-tags files (in
   CVSREP) might be better, but that might slow down the process of
   verifying that a tag is correct (maybe not, for the likely cases,
   if carefully done), and/or be harder to implement correctly.  */

struct val_args {
    const char *name;
    int found;
};

static int
val_fileproc (void *callerdat, struct file_info *finfo)
{
    RCSNode *rcsdata;
    struct val_args *args = callerdat;
    char *tag;

    if ((rcsdata = finfo->rcs) == NULL)
	/* Not sure this can happen, after all we passed only
	   W_REPOS | W_ATTIC.  */
	return 0;

    tag = RCS_gettag (rcsdata, args->name, 1, NULL);
    if (tag != NULL)
    {
	/* FIXME: should find out a way to stop the search at this point.  */
	args->found = 1;
	free (tag);
    }
    return 0;
}



/* This routine determines whether a tag appears in CVSROOT/val-tags.
 *
 * The val-tags file will be open read-only when IDB is NULL.  Since writes to
 * val-tags always append to it, the lack of locking is okay.  The worst case
 * race condition might misinterpret a partially written "foobar" matched, for
 * instance,  a request for "f", "foo", of "foob".  Such a mismatch would be
 * caught harmlessly later.
 *
 * Before CVS adds a tag to val-tags, it will lock val-tags for write and
 * verify that the tag is still not present to avoid adding it twice.
 *
 * NOTES
 *   This function expects its parent to handle any necessary locking of the
 *   val-tags file.
 *
 * INPUTS
 *   idb	When this value is NULL, the val-tags file is opened in
 *   		in read-only mode.  When present, the val-tags file is opened
 *   		in read-write mode and the DBM handle is stored in *IDB.
 *   name	The tag to search for.
 *
 * OUTPUTS
 *   *idb	The val-tags file opened for read/write, or NULL if it couldn't
 *   		be opened.
 *
 * ERRORS
 *   Exits with an error message if the val-tags file cannot be opened for
 *   read (failure to open val-tags read/write is harmless - see below).
 *
 * RETURNS
 *   true	1. If NAME exists in val-tags.
 *   		2. If IDB is non-NULL and val-tags cannot be opened for write.
 *   		   This allows callers to ignore the harmless inability to
 *   		   update the val-tags cache.
 *   false	If the file could be opened and the tag is not present.
 */
static int is_in_val_tags (DBM **idb, const char *name)
{
    DBM *db = NULL;
    char *valtags_filename;
    datum mytag;
    int status;

    /* Casting out const should be safe here - input datums are not
     * written to by the myndbm functions.
     */
    mytag.dptr = (char *)name;
    mytag.dsize = strlen (name);

    valtags_filename = Xasprintf ("%s/%s/%s", current_parsed_root->directory,
				  CVSROOTADM, CVSROOTADM_VALTAGS);

    if (idb)
    {
	mode_t omask;

	omask = umask (cvsumask);
	db = dbm_open (valtags_filename, O_RDWR | O_CREAT, 0666);
	umask (omask);

	if (!db)
	{

	    error (0, errno, "warning: cannot open `%s' read/write",
		   valtags_filename);
	    *idb = NULL;
	    return 1;
	}

	*idb = db;
    }
    else
    {
	db = dbm_open (valtags_filename, O_RDONLY, 0444);
	if (!db && !existence_error (errno))
	    error (1, errno, "cannot read %s", valtags_filename);
    }

    /* If the file merely fails to exist, we just keep going and create
       it later if need be.  */

    status = 0;
    if (db)
    {
	datum val;

	val = dbm_fetch (db, mytag);
	if (val.dptr != NULL)
	    /* Found.  The tag is valid.  */
	    status = 1;

	/* FIXME: should check errors somehow (add dbm_error to myndbm.c?).  */

	if (!idb) dbm_close (db);
    }

    free (valtags_filename);
    return status;
}



/* Add a tag to the CVSROOT/val-tags cache.  Establishes a write lock and
 * reverifies that the tag does not exist before adding it.
 */
static void add_to_val_tags (const char *name)
{
    DBM *db;
    datum mytag;
    datum value;

    if (noexec) return;

    val_tags_lock (current_parsed_root->directory);

    /* Check for presence again since we have a lock now.  */
    if (is_in_val_tags (&db, name)) return;

    /* Casting out const should be safe here - input datums are not
     * written to by the myndbm functions.
     */
    mytag.dptr = (char *)name;
    mytag.dsize = strlen (name);
    value.dptr = "y";
    value.dsize = 1;

    if (dbm_store (db, mytag, value, DBM_REPLACE) < 0)
	error (0, errno, "failed to store %s into val-tags", name);
    dbm_close (db);

    clear_val_tags_lock ();
}



static Dtype
val_direntproc (void *callerdat, const char *dir, const char *repository,
                const char *update_dir, List *entries)
{
    /* This is not quite right--it doesn't get right the case of "cvs
       update -d -r foobar" where foobar is a tag which exists only in
       files in a directory which does not exist yet, but which is
       about to be created.  */
    if (isdir (dir))
	return R_PROCESS;
    return R_SKIP_ALL;
}



/* With VALID set, insert NAME into val-tags if it is not already present
 * there.
 *
 * Without VALID set, check to see whether NAME is a valid tag.  If so, return.
 * If not print an error message and exit.
 *
 * INPUTS
 *
 *   ARGC, ARGV, LOCAL, and AFLAG specify which files we will be operating on.
 *
 *   REPOSITORY is the repository if we need to cd into it, or NULL if
 *     we are already there, or "" if we should do a W_LOCAL recursion.
 *     Sorry for three cases, but the "" case is needed in case the
 *     working directories come from diverse parts of the repository, the
 *     NULL case avoids an unneccesary chdir, and the non-NULL, non-""
 *     case is needed for checkout, where we don't want to chdir if the
 *     tag is found in CVSROOTADM_VALTAGS, but there is not (yet) any
 *     local directory.
 *
 * ERRORS
 *   Errors may be encountered opening and accessing the DBM file.  Write
 *   errors generate warnings and read errors are fatal.  When !VALID and NAME
 *   is not in val-tags, errors may also be generated as per start_recursion.
 *   When !VALID, non-existance of tags both in val-tags and in the archive
 *   files also causes a fatal error.
 *
 * RETURNS
 *   Nothing.
 */
void
tag_check_valid (const char *name, int argc, char **argv, int local, int aflag,
                 char *repository, bool valid)
{
    struct val_args the_val_args;
    struct saved_cwd cwd;
    int which;

#ifdef HAVE_PRINTF_PTR
    TRACE (TRACE_FUNCTION,
	   "tag_check_valid (name=%s, argc=%d, argv=%p, local=%d,\n"
      "                      aflag=%d, repository=%s, valid=%s)",
	   name ? name : "(name)", argc, (void *)argv, local, aflag,
	   repository ? repository : "(null)",
	   valid ? "true" : "false");
#else
    TRACE (TRACE_FUNCTION,
	   "tag_check_valid (name=%s, argc=%d, argv=%lx, local=%d,\n"
      "                      aflag=%d, repository=%s, valid=%s)",
	   name ? name : "(name)", argc, (unsigned long)argv, local, aflag,
	   repository ? repository : "(null)",
	   valid ? "true" : "false");
#endif

    /* Numeric tags require only a syntactic check.  */
    if (isdigit ((unsigned char) name[0]))
    {
	/* insert is not possible for numeric revisions */
	assert (!valid);
	if (RCS_valid_rev (name)) return;
	else
	    error (1, 0, "\
Numeric tag %s invalid.  Numeric tags should be of the form X[.X]...", name);
    }

    /* Special tags are always valid.  */
    if (strcmp (name, TAG_BASE) == 0
	|| strcmp (name, TAG_HEAD) == 0)
    {
	/* insert is not possible for numeric revisions */
	assert (!valid);
	return;
    }

    /* Verify that the tag is valid syntactically.  Some later code once made
     * assumptions about this.
     */
    RCS_check_tag (name);

    if (is_in_val_tags (NULL, name)) return;

    if (!valid)
    {
	/* We didn't find the tag in val-tags, so look through all the RCS files
	 * to see whether it exists there.  Yes, this is expensive, but there
	 * is no other way to cope with a tag which might have been created
	 * by an old version of CVS, from before val-tags was invented
	 */

	the_val_args.name = name;
	the_val_args.found = 0;
	which = W_REPOS | W_ATTIC;

	if (repository == NULL || repository[0] == '\0')
	    which |= W_LOCAL;
	else
	{
	    if (save_cwd (&cwd))
		error (1, errno, "Failed to save current directory.");
	    if (CVS_CHDIR (repository) < 0)
		error (1, errno, "cannot change to %s directory", repository);
	}

	start_recursion
	    (val_fileproc, NULL, val_direntproc, NULL,
	     &the_val_args, argc, argv, local, which, aflag,
	     CVS_LOCK_READ, NULL, 1, repository);
	if (repository != NULL && repository[0] != '\0')
	{
	    if (restore_cwd (&cwd))
		error (1, errno, "Failed to restore current directory, `%s'.",
		       cwd.name);
	    free_cwd (&cwd);
	}

	if (!the_val_args.found)
	    error (1, 0, "no such tag `%s'", name);
    }

    /* The tags is valid but not mentioned in val-tags.  Add it.  */
    add_to_val_tags (name);
}