xref: /openbsd/gnu/usr.bin/cvs/src/admin.c (revision 0971b67f)
11e72d8d2Sderaadt /*
21e72d8d2Sderaadt  * Copyright (c) 1992, Brian Berliner and Jeff Polk
31e72d8d2Sderaadt  * Copyright (c) 1989-1992, Brian Berliner
41e72d8d2Sderaadt  *
51e72d8d2Sderaadt  * You may distribute under the terms of the GNU General Public License as
62286d8edStholo  * specified in the README file that comes with the CVS source distribution.
71e72d8d2Sderaadt  *
82286d8edStholo  * Administration ("cvs admin")
91e72d8d2Sderaadt  *
101e72d8d2Sderaadt  */
111e72d8d2Sderaadt 
121e72d8d2Sderaadt #include "cvs.h"
1313571821Stholo #ifdef CVS_ADMIN_GROUP
1413571821Stholo #include <grp.h>
1513571821Stholo #endif
162286d8edStholo #include <assert.h>
171e72d8d2Sderaadt 
1850bf276cStholo static Dtype admin_dirproc PROTO ((void *callerdat, char *dir,
1950bf276cStholo 				   char *repos, char *update_dir,
2050bf276cStholo 				   List *entries));
2150bf276cStholo static int admin_fileproc PROTO ((void *callerdat, struct file_info *finfo));
221e72d8d2Sderaadt 
231e72d8d2Sderaadt static const char *const admin_usage[] =
241e72d8d2Sderaadt {
25e77048c1Stholo     "Usage: %s %s [options] files...\n",
26e77048c1Stholo     "\t-a users   Append (comma-separated) user names to access list.\n",
27e77048c1Stholo     "\t-A file    Append another file's access list.\n",
28e77048c1Stholo     "\t-b[rev]    Set default branch (highest branch on trunk if omitted).\n",
29e77048c1Stholo     "\t-c string  Set comment leader.\n",
30f523375bSjcs     "\t-C rev:id  Set revision's commit id.\n",
31e77048c1Stholo     "\t-e[users]  Remove (comma-separated) user names from access list\n",
32e77048c1Stholo     "\t           (all names if omitted).\n",
33e77048c1Stholo     "\t-I         Run interactively.\n",
34e77048c1Stholo     "\t-k subst   Set keyword substitution mode:\n",
35e77048c1Stholo     "\t   kv   (Default) Substitue keyword and value.\n",
36e77048c1Stholo     "\t   kvl  Substitue keyword, value, and locker (if any).\n",
37e77048c1Stholo     "\t   k    Substitue keyword only.\n",
38e77048c1Stholo     "\t   o    Preserve original string.\n",
39e77048c1Stholo     "\t   b    Like o, but mark file as binary.\n",
40e77048c1Stholo     "\t   v    Substitue value only.\n",
41e77048c1Stholo     "\t-l[rev]    Lock revision (latest revision on branch,\n",
42e77048c1Stholo     "\t           latest revision on trunk if omitted).\n",
43e77048c1Stholo     "\t-L         Set strict locking.\n",
44e77048c1Stholo     "\t-m rev:msg  Replace revision's log message.\n",
45e77048c1Stholo     "\t-n tag[:[rev]]  Tag branch or revision.  If :rev is omitted,\n",
46e77048c1Stholo     "\t                delete the tag; if rev is omitted, tag the latest\n",
47e77048c1Stholo     "\t                revision on the default branch.\n",
48e77048c1Stholo     "\t-N tag[:[rev]]  Same as -n except override existing tag.\n",
49e77048c1Stholo     "\t-o range   Delete (outdate) specified range of revisions:\n",
5043c1707eStholo     "\t   rev1:rev2   Between rev1 and rev2, including rev1 and rev2.\n",
51e77048c1Stholo     "\t   rev1::rev2  Between rev1 and rev2, excluding rev1 and rev2.\n",
5243c1707eStholo     "\t   rev:        rev and following revisions on the same branch.\n",
53e77048c1Stholo     "\t   rev::       After rev on the same branch.\n",
5443c1707eStholo     "\t   :rev        rev and previous revisions on the same branch.\n",
55e77048c1Stholo     "\t   ::rev       Before rev on the same branch.\n",
56e77048c1Stholo     "\t   rev         Just rev.\n",
57e77048c1Stholo     "\t-q         Run quietly.\n",
58e77048c1Stholo     "\t-s state[:rev]  Set revision state (latest revision on branch,\n",
59e77048c1Stholo     "\t                latest revision on trunk if omitted).\n",
60e77048c1Stholo     "\t-t[file]   Get descriptive text from file (stdin if omitted).\n",
61e77048c1Stholo     "\t-t-string  Set descriptive text.\n",
62e77048c1Stholo     "\t-u[rev]    Unlock the revision (latest revision on branch,\n",
63e77048c1Stholo     "\t           latest revision on trunk if omitted).\n",
64e77048c1Stholo     "\t-U         Unset strict locking.\n",
652286d8edStholo     "(Specify the --help global option for a list of other help options)\n",
661e72d8d2Sderaadt     NULL
671e72d8d2Sderaadt };
681e72d8d2Sderaadt 
692286d8edStholo /* This structure is used to pass information through start_recursion.  */
702286d8edStholo struct admin_data
712286d8edStholo {
722286d8edStholo     /* Set default branch (-b).  It is "-b" followed by the value
732286d8edStholo        given, or NULL if not specified, or merely "-b" if -b is
742286d8edStholo        specified without a value.  */
752286d8edStholo     char *branch;
762286d8edStholo 
772286d8edStholo     /* Set comment leader (-c).  It is "-c" followed by the value
782286d8edStholo        given, or NULL if not specified.  The comment leader is
792286d8edStholo        relevant only for old versions of RCS, but we let people set it
802286d8edStholo        anyway.  */
812286d8edStholo     char *comment;
822286d8edStholo 
832286d8edStholo     /* Set strict locking (-L).  */
842286d8edStholo     int set_strict;
852286d8edStholo 
862286d8edStholo     /* Set nonstrict locking (-U).  */
872286d8edStholo     int set_nonstrict;
882286d8edStholo 
892286d8edStholo     /* Delete revisions (-o).  It is "-o" followed by the value specified.  */
902286d8edStholo     char *delete_revs;
912286d8edStholo 
922286d8edStholo     /* Keyword substitution mode (-k), e.g. "-kb".  */
932286d8edStholo     char *kflag;
942286d8edStholo 
95e77048c1Stholo     /* Description (-t).  */
962286d8edStholo     char *desc;
972286d8edStholo 
982286d8edStholo     /* Interactive (-I).  Problematic with client/server.  */
992286d8edStholo     int interactive;
1002286d8edStholo 
1012286d8edStholo     /* This is the cheesy part.  It is a vector with the options which
1022286d8edStholo        we don't deal with above (e.g. "-afoo" "-abar,baz").  In the future
1032286d8edStholo        this presumably will be replaced by other variables which break
1042286d8edStholo        out the data in a more convenient fashion.  AV as well as each of
1052286d8edStholo        the strings it points to is malloc'd.  */
1062286d8edStholo     int ac;
1072286d8edStholo     char **av;
1082286d8edStholo     int av_alloc;
1092286d8edStholo };
1102286d8edStholo 
1112286d8edStholo /* Add an argument.  OPT is the option letter, e.g. 'a'.  ARG is the
1122286d8edStholo    argument to that option, or NULL if omitted (whether NULL can actually
1132286d8edStholo    happen depends on whether the option was specified as optional to
1142286d8edStholo    getopt).  */
1152286d8edStholo static void
arg_add(dat,opt,arg)1162286d8edStholo arg_add (dat, opt, arg)
1172286d8edStholo     struct admin_data *dat;
1182286d8edStholo     int opt;
1192286d8edStholo     char *arg;
1202286d8edStholo {
1212286d8edStholo     char *newelt = xmalloc ((arg == NULL ? 0 : strlen (arg)) + 3);
1222286d8edStholo     strcpy (newelt, "-");
1232286d8edStholo     newelt[1] = opt;
1242286d8edStholo     if (arg == NULL)
1252286d8edStholo 	newelt[2] = '\0';
1262286d8edStholo     else
1272286d8edStholo 	strcpy (newelt + 2, arg);
1282286d8edStholo 
1292286d8edStholo     if (dat->av_alloc == 0)
1302286d8edStholo     {
1312286d8edStholo 	dat->av_alloc = 1;
1322286d8edStholo 	dat->av = (char **) xmalloc (dat->av_alloc * sizeof (*dat->av));
1332286d8edStholo     }
1342286d8edStholo     else if (dat->ac >= dat->av_alloc)
1352286d8edStholo     {
1362286d8edStholo 	dat->av_alloc *= 2;
1372286d8edStholo 	dat->av = (char **) xrealloc (dat->av,
1382286d8edStholo 				      dat->av_alloc * sizeof (*dat->av));
1392286d8edStholo     }
1402286d8edStholo     dat->av[dat->ac++] = newelt;
1412286d8edStholo }
1421e72d8d2Sderaadt 
1431e72d8d2Sderaadt int
admin(argc,argv)1441e72d8d2Sderaadt admin (argc, argv)
1451e72d8d2Sderaadt     int argc;
1461e72d8d2Sderaadt     char **argv;
1471e72d8d2Sderaadt {
1481e72d8d2Sderaadt     int err;
14913571821Stholo #ifdef CVS_ADMIN_GROUP
15013571821Stholo     struct group *grp;
151780d15dfStholo     struct group *getgrnam();
15213571821Stholo #endif
1532286d8edStholo     struct admin_data admin_data;
1542286d8edStholo     int c;
1552286d8edStholo     int i;
156892c0aadStholo     int only_k_option;
1572286d8edStholo 
1581e72d8d2Sderaadt     if (argc <= 1)
1591e72d8d2Sderaadt 	usage (admin_usage);
1601e72d8d2Sderaadt 
1611e72d8d2Sderaadt     wrap_setup ();
1621e72d8d2Sderaadt 
1632286d8edStholo     memset (&admin_data, 0, sizeof admin_data);
1642286d8edStholo 
1652286d8edStholo     /* TODO: get rid of `-' switch notation in admin_data.  For
1662286d8edStholo        example, admin_data->branch should be not `-bfoo' but simply `foo'. */
1672286d8edStholo 
1682286d8edStholo     optind = 0;
169892c0aadStholo     only_k_option = 1;
1702286d8edStholo     while ((c = getopt (argc, argv,
171f523375bSjcs 			"+ib::c:C:a:A:e::l::u::LUn:N:m:o:s:t::IqxV:k:")) != -1)
1722286d8edStholo     {
173892c0aadStholo 	if (c != 'k')
174892c0aadStholo 	    only_k_option = 0;
175892c0aadStholo 
1762286d8edStholo 	switch (c)
1772286d8edStholo 	{
1782286d8edStholo 	    case 'i':
1792286d8edStholo 		/* This has always been documented as useless in cvs.texinfo
1802286d8edStholo 		   and it really is--admin_fileproc silently does nothing
1812286d8edStholo 		   if vers->vn_user is NULL. */
1822286d8edStholo 		error (0, 0, "the -i option to admin is not supported");
1832286d8edStholo 		error (0, 0, "run add or import to create an RCS file");
1842286d8edStholo 		goto usage_error;
1852286d8edStholo 
1862286d8edStholo 	    case 'b':
1872286d8edStholo 		if (admin_data.branch != NULL)
1882286d8edStholo 		{
1892286d8edStholo 		    error (0, 0, "duplicate 'b' option");
1902286d8edStholo 		    goto usage_error;
1912286d8edStholo 		}
1922286d8edStholo 		if (optarg == NULL)
1932286d8edStholo 		    admin_data.branch = xstrdup ("-b");
1942286d8edStholo 		else
1952286d8edStholo 		{
1962286d8edStholo 		    admin_data.branch = xmalloc (strlen (optarg) + 5);
1972286d8edStholo 		    strcpy (admin_data.branch, "-b");
1982286d8edStholo 		    strcat (admin_data.branch, optarg);
1992286d8edStholo 		}
2001e72d8d2Sderaadt 		break;
2012286d8edStholo 
2022286d8edStholo 	    case 'c':
2032286d8edStholo 		if (admin_data.comment != NULL)
2042286d8edStholo 		{
2052286d8edStholo 		    error (0, 0, "duplicate 'c' option");
2062286d8edStholo 		    goto usage_error;
2072286d8edStholo 		}
2082286d8edStholo 		admin_data.comment = xmalloc (strlen (optarg) + 5);
2092286d8edStholo 		strcpy (admin_data.comment, "-c");
2102286d8edStholo 		strcat (admin_data.comment, optarg);
2112286d8edStholo 		break;
2122286d8edStholo 
213f523375bSjcs 	    case 'C':
214f523375bSjcs 		/* Add commitid. */
215f523375bSjcs 		arg_add (&admin_data, 'C', optarg);
216f523375bSjcs 		break;
217f523375bSjcs 
2182286d8edStholo 	    case 'a':
2192286d8edStholo 		arg_add (&admin_data, 'a', optarg);
2202286d8edStholo 		break;
2212286d8edStholo 
2222286d8edStholo 	    case 'A':
2232286d8edStholo 		/* In the client/server case, this is cheesy because
2242286d8edStholo 		   we just pass along the name of the RCS file, which
2252286d8edStholo 		   then will want to exist on the server.  This is
2262286d8edStholo 		   accidental; having the client specify a pathname on
2272286d8edStholo 		   the server is not a design feature of the protocol.  */
2282286d8edStholo 		arg_add (&admin_data, 'A', optarg);
2292286d8edStholo 		break;
2302286d8edStholo 
2312286d8edStholo 	    case 'e':
2322286d8edStholo 		arg_add (&admin_data, 'e', optarg);
2332286d8edStholo 		break;
2342286d8edStholo 
2352286d8edStholo 	    case 'l':
2362286d8edStholo 		/* Note that multiple -l options are legal.  */
2372286d8edStholo 		arg_add (&admin_data, 'l', optarg);
2382286d8edStholo 		break;
2392286d8edStholo 
2402286d8edStholo 	    case 'u':
2412286d8edStholo 		/* Note that multiple -u options are legal.  */
2422286d8edStholo 		arg_add (&admin_data, 'u', optarg);
2432286d8edStholo 		break;
2442286d8edStholo 
2452286d8edStholo 	    case 'L':
2462286d8edStholo 		/* Probably could also complain if -L is specified multiple
2472286d8edStholo 		   times, although RCS doesn't and I suppose it is reasonable
2482286d8edStholo 		   just to have it mean the same as a single -L.  */
2492286d8edStholo 		if (admin_data.set_nonstrict)
2502286d8edStholo 		{
2512286d8edStholo 		    error (0, 0, "-U and -L are incompatible");
2522286d8edStholo 		    goto usage_error;
2532286d8edStholo 		}
2542286d8edStholo 		admin_data.set_strict = 1;
2552286d8edStholo 		break;
2562286d8edStholo 
2572286d8edStholo 	    case 'U':
2582286d8edStholo 		/* Probably could also complain if -U is specified multiple
2592286d8edStholo 		   times, although RCS doesn't and I suppose it is reasonable
2602286d8edStholo 		   just to have it mean the same as a single -U.  */
2612286d8edStholo 		if (admin_data.set_strict)
2622286d8edStholo 		{
2632286d8edStholo 		    error (0, 0, "-U and -L are incompatible");
2642286d8edStholo 		    goto usage_error;
2652286d8edStholo 		}
2662286d8edStholo 		admin_data.set_nonstrict = 1;
2672286d8edStholo 		break;
2682286d8edStholo 
2692286d8edStholo 	    case 'n':
2702286d8edStholo 		/* Mostly similar to cvs tag.  Could also be parsing
2712286d8edStholo 		   the syntax of optarg, although for now we just pass
2722286d8edStholo 		   it to rcs as-is.  Note that multiple -n options are
2732286d8edStholo 		   legal.  */
2742286d8edStholo 		arg_add (&admin_data, 'n', optarg);
2752286d8edStholo 		break;
2762286d8edStholo 
2772286d8edStholo 	    case 'N':
2782286d8edStholo 		/* Mostly similar to cvs tag.  Could also be parsing
2792286d8edStholo 		   the syntax of optarg, although for now we just pass
2802286d8edStholo 		   it to rcs as-is.  Note that multiple -N options are
2812286d8edStholo 		   legal.  */
2822286d8edStholo 		arg_add (&admin_data, 'N', optarg);
2832286d8edStholo 		break;
2842286d8edStholo 
2852286d8edStholo 	    case 'm':
2862286d8edStholo 		/* Change log message.  Could also be parsing the syntax
2872286d8edStholo 		   of optarg, although for now we just pass it to rcs
2882286d8edStholo 		   as-is.  Note that multiple -m options are legal.  */
2892286d8edStholo 		arg_add (&admin_data, 'm', optarg);
2902286d8edStholo 		break;
2912286d8edStholo 
2922286d8edStholo 	    case 'o':
2932286d8edStholo 		/* Delete revisions.  Probably should also be parsing the
2942286d8edStholo 		   syntax of optarg, so that the client can give errors
2952286d8edStholo 		   rather than making the server take care of that.
2962286d8edStholo 		   Other than that I'm not sure whether it matters much
2972286d8edStholo 		   whether we parse it here or in admin_fileproc.
2982286d8edStholo 
2992286d8edStholo 		   Note that multiple -o options are illegal, in RCS
3002286d8edStholo 		   as well as here.  */
3012286d8edStholo 
3022286d8edStholo 		if (admin_data.delete_revs != NULL)
3032286d8edStholo 		{
3042286d8edStholo 		    error (0, 0, "duplicate '-o' option");
3052286d8edStholo 		    goto usage_error;
3062286d8edStholo 		}
3072286d8edStholo 		admin_data.delete_revs = xmalloc (strlen (optarg) + 5);
3082286d8edStholo 		strcpy (admin_data.delete_revs, "-o");
3092286d8edStholo 		strcat (admin_data.delete_revs, optarg);
3102286d8edStholo 		break;
3112286d8edStholo 
3122286d8edStholo 	    case 's':
3132286d8edStholo 		/* Note that multiple -s options are legal.  */
3142286d8edStholo 		arg_add (&admin_data, 's', optarg);
3152286d8edStholo 		break;
3162286d8edStholo 
3172286d8edStholo 	    case 't':
3182286d8edStholo 		if (admin_data.desc != NULL)
3192286d8edStholo 		{
3202286d8edStholo 		    error (0, 0, "duplicate 't' option");
3212286d8edStholo 		    goto usage_error;
3222286d8edStholo 		}
323e77048c1Stholo 		if (optarg != NULL && optarg[0] == '-')
324e77048c1Stholo 		    admin_data.desc = xstrdup (optarg + 1);
3252286d8edStholo 		else
3262286d8edStholo 		{
327e77048c1Stholo 		    size_t bufsize = 0;
328e77048c1Stholo 		    size_t len;
329e77048c1Stholo 
330e77048c1Stholo 		    get_file (optarg, optarg, "r", &admin_data.desc,
331e77048c1Stholo 			      &bufsize, &len);
3322286d8edStholo 		}
3332286d8edStholo 		break;
3342286d8edStholo 
3352286d8edStholo 	    case 'I':
3362286d8edStholo 		/* At least in RCS this can be specified several times,
3372286d8edStholo 		   with the same meaning as being specified once.  */
3382286d8edStholo 		admin_data.interactive = 1;
3392286d8edStholo 		break;
3402286d8edStholo 
3412286d8edStholo 	    case 'q':
34243c1707eStholo 		/* Silently set the global really_quiet flag.  This keeps admin in
34343c1707eStholo 		 * sync with the RCS man page and allows us to silently support
34443c1707eStholo 		 * older servers when necessary.
34543c1707eStholo 		 *
34643c1707eStholo 		 * Some logic says we might want to output a deprecation warning
34743c1707eStholo 		 * here, but I'm opting not to in order to stay quietly in sync
34843c1707eStholo 		 * with the RCS man page.
34943c1707eStholo 		 */
35043c1707eStholo 		really_quiet = 1;
3512286d8edStholo 		break;
3522286d8edStholo 
3532286d8edStholo 	    case 'x':
3542286d8edStholo 		error (0, 0, "the -x option has never done anything useful");
3552286d8edStholo 		error (0, 0, "RCS files in CVS always end in ,v");
3562286d8edStholo 		goto usage_error;
3572286d8edStholo 
3582286d8edStholo 	    case 'V':
3592286d8edStholo 		/* No longer supported. */
3602286d8edStholo 		error (0, 0, "the `-V' option is obsolete");
3612286d8edStholo 		break;
3622286d8edStholo 
3632286d8edStholo 	    case 'k':
3642286d8edStholo 		if (admin_data.kflag != NULL)
3652286d8edStholo 		{
3662286d8edStholo 		    error (0, 0, "duplicate '-k' option");
3672286d8edStholo 		    goto usage_error;
3682286d8edStholo 		}
3692286d8edStholo 		admin_data.kflag = RCS_check_kflag (optarg);
3702286d8edStholo 		break;
3712286d8edStholo 	    default:
3722286d8edStholo 	    case '?':
3732286d8edStholo 		/* getopt will have printed an error message.  */
3742286d8edStholo 
3752286d8edStholo 	    usage_error:
3762286d8edStholo 		/* Don't use command_name; it might be "server".  */
3772286d8edStholo 	        error (1, 0, "specify %s -H admin for usage information",
3782286d8edStholo 		       program_name);
3792286d8edStholo 	}
3802286d8edStholo     }
3812286d8edStholo     argc -= optind;
3822286d8edStholo     argv += optind;
3832286d8edStholo 
384892c0aadStholo #ifdef CVS_ADMIN_GROUP
38543c1707eStholo     /* The use of `cvs admin -k' is unrestricted.  However, any other
38643c1707eStholo        option is restricted if the group CVS_ADMIN_GROUP exists.  */
38743c1707eStholo     if (!only_k_option &&
38843c1707eStholo 	(grp = getgrnam(CVS_ADMIN_GROUP)) != NULL)
389892c0aadStholo     {
39043c1707eStholo #ifdef HAVE_GETGROUPS
39143c1707eStholo 	gid_t *grps;
39243c1707eStholo 	int n;
393892c0aadStholo 
39443c1707eStholo 	/* get number of auxiliary groups */
39543c1707eStholo 	n = getgroups (0, NULL);
39643c1707eStholo 	if (n < 0)
39743c1707eStholo 	    error (1, errno, "unable to get number of auxiliary groups");
39843c1707eStholo 	grps = (gid_t *) xmalloc((n + 1) * sizeof *grps);
39943c1707eStholo 	n = getgroups (n, grps);
40043c1707eStholo 	if (n < 0)
40143c1707eStholo 	    error (1, errno, "unable to get list of auxiliary groups");
40243c1707eStholo 	grps[n] = getgid();
40343c1707eStholo 	for (i = 0; i <= n; i++)
40443c1707eStholo 	    if (grps[i] == grp->gr_gid) break;
40543c1707eStholo 	free (grps);
40643c1707eStholo 	if (i > n)
407892c0aadStholo 	    error (1, 0, "usage is restricted to members of the group %s",
408892c0aadStholo 		   CVS_ADMIN_GROUP);
40943c1707eStholo #else
41043c1707eStholo 	char *me = getcaller();
41143c1707eStholo 	char **grnam;
41243c1707eStholo 
41343c1707eStholo 	for (grnam = grp->gr_mem; *grnam; grnam++)
41443c1707eStholo 	    if (strcmp (*grnam, me) == 0) break;
41543c1707eStholo 	if (!*grnam && getgid() != grp->gr_gid)
41643c1707eStholo 	    error (1, 0, "usage is restricted to members of the group %s",
41743c1707eStholo 		   CVS_ADMIN_GROUP);
41843c1707eStholo #endif
419892c0aadStholo     }
420892c0aadStholo #endif
421892c0aadStholo 
4222286d8edStholo     for (i = 0; i < admin_data.ac; ++i)
4232286d8edStholo     {
4242286d8edStholo 	assert (admin_data.av[i][0] == '-');
4252286d8edStholo 	switch (admin_data.av[i][1])
4262286d8edStholo 	{
4272286d8edStholo 	    case 'm':
4282286d8edStholo 	    case 'l':
4292286d8edStholo 	    case 'u':
4302286d8edStholo 		check_numeric (&admin_data.av[i][2], argc, argv);
4312286d8edStholo 		break;
4322286d8edStholo 	    default:
4332286d8edStholo 		break;
4342286d8edStholo 	}
4352286d8edStholo     }
4362286d8edStholo     if (admin_data.branch != NULL)
4372286d8edStholo 	check_numeric (admin_data.branch + 2, argc, argv);
4382286d8edStholo     if (admin_data.delete_revs != NULL)
4392286d8edStholo     {
4402286d8edStholo 	char *p;
4412286d8edStholo 
4422286d8edStholo 	check_numeric (admin_data.delete_revs + 2, argc, argv);
4432286d8edStholo 	p = strchr (admin_data.delete_revs + 2, ':');
444c71bc7e2Stholo 	if (p != NULL && isdigit ((unsigned char) p[1]))
4452286d8edStholo 	    check_numeric (p + 1, argc, argv);
446c71bc7e2Stholo 	else if (p != NULL && p[1] == ':' && isdigit ((unsigned char) p[2]))
4472286d8edStholo 	    check_numeric (p + 2, argc, argv);
4482286d8edStholo     }
4491e72d8d2Sderaadt 
4501e72d8d2Sderaadt #ifdef CLIENT_SUPPORT
45143c1707eStholo     if (current_parsed_root->isremote)
4521e72d8d2Sderaadt     {
4531e72d8d2Sderaadt 	/* We're the client side.  Fire up the remote server.  */
4541e72d8d2Sderaadt 	start_server ();
4551e72d8d2Sderaadt 
4561e72d8d2Sderaadt 	ign_setup ();
4571e72d8d2Sderaadt 
4582286d8edStholo 	/* Note that option_with_arg does not work for us, because some
4592286d8edStholo 	   of the options must be sent without a space between the option
4602286d8edStholo 	   and its argument.  */
4612286d8edStholo 	if (admin_data.interactive)
4622286d8edStholo 	    error (1, 0, "-I option not useful with client/server");
4632286d8edStholo 	if (admin_data.branch != NULL)
4642286d8edStholo 	    send_arg (admin_data.branch);
4652286d8edStholo 	if (admin_data.comment != NULL)
4662286d8edStholo 	    send_arg (admin_data.comment);
4672286d8edStholo 	if (admin_data.set_strict)
4682286d8edStholo 	    send_arg ("-L");
4692286d8edStholo 	if (admin_data.set_nonstrict)
4702286d8edStholo 	    send_arg ("-U");
4712286d8edStholo 	if (admin_data.delete_revs != NULL)
4722286d8edStholo 	    send_arg (admin_data.delete_revs);
4732286d8edStholo 	if (admin_data.desc != NULL)
474e77048c1Stholo 	{
475e77048c1Stholo 	    char *p = admin_data.desc;
476e77048c1Stholo 	    send_to_server ("Argument -t-", 0);
477e77048c1Stholo 	    while (*p)
478e77048c1Stholo 	    {
479e77048c1Stholo 		if (*p == '\n')
480e77048c1Stholo 		{
481e77048c1Stholo 		    send_to_server ("\012Argumentx ", 0);
482e77048c1Stholo 		    ++p;
483e77048c1Stholo 		}
484e77048c1Stholo 		else
485e77048c1Stholo 		{
486e77048c1Stholo 		    char *q = strchr (p, '\n');
487e77048c1Stholo 		    if (q == NULL) q = p + strlen (p);
488e77048c1Stholo 		    send_to_server (p, q - p);
489e77048c1Stholo 		    p = q;
490e77048c1Stholo 		}
491e77048c1Stholo 	    }
492e77048c1Stholo 	    send_to_server ("\012", 1);
493e77048c1Stholo 	}
49443c1707eStholo 	/* Send this for all really_quiets since we know that it will be silently
49543c1707eStholo 	 * ignored when unneeded.  This supports old servers.
49643c1707eStholo 	 */
49743c1707eStholo 	if (really_quiet)
4982286d8edStholo 	    send_arg ("-q");
4992286d8edStholo 	if (admin_data.kflag != NULL)
5002286d8edStholo 	    send_arg (admin_data.kflag);
5012286d8edStholo 
5022286d8edStholo 	for (i = 0; i < admin_data.ac; ++i)
5032286d8edStholo 	    send_arg (admin_data.av[i]);
5041e72d8d2Sderaadt 
505b6c02222Stholo 	send_files (argc, argv, 0, 0, SEND_NO_CONTENTS);
506c71bc7e2Stholo 	send_file_names (argc, argv, SEND_EXPAND_WILD);
50713571821Stholo 	send_to_server ("admin\012", 0);
5082286d8edStholo         err = get_responses_and_close ();
5092286d8edStholo 	goto return_it;
5101e72d8d2Sderaadt     }
5111e72d8d2Sderaadt #endif /* CLIENT_SUPPORT */
5121e72d8d2Sderaadt 
51343c1707eStholo     lock_tree_for_write (argc, argv, 0, W_LOCAL, 0);
5142286d8edStholo 
51513571821Stholo     err = start_recursion (admin_fileproc, (FILESDONEPROC) NULL, admin_dirproc,
5162286d8edStholo 			   (DIRLEAVEPROC) NULL, (void *)&admin_data,
5172286d8edStholo 			   argc, argv, 0,
5182286d8edStholo 			   W_LOCAL, 0, 0, (char *) NULL, 1);
5192286d8edStholo     Lock_Cleanup ();
5202286d8edStholo 
5212286d8edStholo  return_it:
5222286d8edStholo     if (admin_data.branch != NULL)
5232286d8edStholo 	free (admin_data.branch);
5242286d8edStholo     if (admin_data.comment != NULL)
5252286d8edStholo 	free (admin_data.comment);
5262286d8edStholo     if (admin_data.delete_revs != NULL)
5272286d8edStholo 	free (admin_data.delete_revs);
5282286d8edStholo     if (admin_data.kflag != NULL)
5292286d8edStholo 	free (admin_data.kflag);
5302286d8edStholo     if (admin_data.desc != NULL)
5312286d8edStholo 	free (admin_data.desc);
5322286d8edStholo     for (i = 0; i < admin_data.ac; ++i)
5332286d8edStholo 	free (admin_data.av[i]);
5342286d8edStholo     if (admin_data.av != NULL)
5352286d8edStholo 	free (admin_data.av);
5362286d8edStholo 
5371e72d8d2Sderaadt     return (err);
5381e72d8d2Sderaadt }
5391e72d8d2Sderaadt 
5401e72d8d2Sderaadt /*
5411e72d8d2Sderaadt  * Called to run "rcs" on a particular file.
5421e72d8d2Sderaadt  */
5431e72d8d2Sderaadt /* ARGSUSED */
5441e72d8d2Sderaadt static int
admin_fileproc(callerdat,finfo)54550bf276cStholo admin_fileproc (callerdat, finfo)
54650bf276cStholo     void *callerdat;
547c26070a5Stholo     struct file_info *finfo;
5481e72d8d2Sderaadt {
5492286d8edStholo     struct admin_data *admin_data = (struct admin_data *) callerdat;
5501e72d8d2Sderaadt     Vers_TS *vers;
5511e72d8d2Sderaadt     char *version;
5522286d8edStholo     int i;
55313571821Stholo     int status = 0;
5542286d8edStholo     RCSNode *rcs, *rcs2;
5551e72d8d2Sderaadt 
55650bf276cStholo     vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0);
5571e72d8d2Sderaadt 
5581e72d8d2Sderaadt     version = vers->vn_user;
5591e72d8d2Sderaadt     if (version == NULL)
56013571821Stholo 	goto exitfunc;
5611e72d8d2Sderaadt     else if (strcmp (version, "0") == 0)
5621e72d8d2Sderaadt     {
563c26070a5Stholo 	error (0, 0, "cannot admin newly added file `%s'", finfo->file);
56413571821Stholo 	goto exitfunc;
5651e72d8d2Sderaadt     }
5661e72d8d2Sderaadt 
5672286d8edStholo     rcs = vers->srcfile;
568518886fbStobias     if (rcs == NULL)
569518886fbStobias     {
570518886fbStobias 	error (0, 0, "lost revision control file for `%s'", finfo->file);
571518886fbStobias 	goto exitfunc;
572518886fbStobias     }
5732286d8edStholo     if (rcs->flags & PARTIAL)
5745e617892Stholo 	RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL);
5752286d8edStholo 
5762286d8edStholo     status = 0;
5772286d8edStholo 
57843c1707eStholo     if (!really_quiet)
5791e72d8d2Sderaadt     {
5802286d8edStholo 	cvs_output ("RCS file: ", 0);
5812286d8edStholo 	cvs_output (rcs->path, 0);
5822286d8edStholo 	cvs_output ("\n", 1);
5831e72d8d2Sderaadt     }
5842286d8edStholo 
5852286d8edStholo     if (admin_data->branch != NULL)
5865e617892Stholo     {
5875e617892Stholo 	char *branch = &admin_data->branch[2];
588c71bc7e2Stholo 	if (*branch != '\0' && ! isdigit ((unsigned char) *branch))
5895e617892Stholo 	{
5905e617892Stholo 	    branch = RCS_whatbranch (rcs, admin_data->branch + 2);
5915e617892Stholo 	    if (branch == NULL)
5925e617892Stholo 	    {
5935e617892Stholo 		error (0, 0, "%s: Symbolic name %s is undefined.",
5945e617892Stholo 				rcs->path, admin_data->branch + 2);
5955e617892Stholo 		status = 1;
5965e617892Stholo 	    }
5975e617892Stholo 	}
5985e617892Stholo 	if (status == 0)
5995e617892Stholo 	    RCS_setbranch (rcs, branch);
6005e617892Stholo 	if (branch != NULL && branch != &admin_data->branch[2])
6015e617892Stholo 	    free (branch);
6025e617892Stholo     }
6032286d8edStholo     if (admin_data->comment != NULL)
6042286d8edStholo     {
6052286d8edStholo 	if (rcs->comment != NULL)
6062286d8edStholo 	    free (rcs->comment);
6072286d8edStholo 	rcs->comment = xstrdup (admin_data->comment + 2);
6082286d8edStholo     }
6092286d8edStholo     if (admin_data->set_strict)
6102286d8edStholo 	rcs->strict_locks = 1;
6112286d8edStholo     if (admin_data->set_nonstrict)
6122286d8edStholo 	rcs->strict_locks = 0;
6132286d8edStholo     if (admin_data->delete_revs != NULL)
6142286d8edStholo     {
6152286d8edStholo 	char *s, *t, *rev1, *rev2;
6162286d8edStholo 	/* Set for :, clear for ::.  */
6172286d8edStholo 	int inclusive;
6182286d8edStholo 	char *t2;
6192286d8edStholo 
6202286d8edStholo 	s = admin_data->delete_revs + 2;
6212286d8edStholo 	inclusive = 1;
6222286d8edStholo 	t = strchr (s, ':');
6232286d8edStholo 	if (t != NULL)
6242286d8edStholo 	{
6252286d8edStholo 	    if (t[1] == ':')
6262286d8edStholo 	    {
6272286d8edStholo 		inclusive = 0;
6282286d8edStholo 		t2 = t + 2;
6292286d8edStholo 	    }
6302286d8edStholo 	    else
6312286d8edStholo 		t2 = t + 1;
6322286d8edStholo 	}
6332286d8edStholo 
6342286d8edStholo 	/* Note that we don't support '-' for ranges.  RCS considers it
6352286d8edStholo 	   obsolete and it is problematic with tags containing '-'.  "cvs log"
6362286d8edStholo 	   has made the same decision.  */
6372286d8edStholo 
6382286d8edStholo 	if (t == NULL)
6392286d8edStholo 	{
6402286d8edStholo 	    /* -orev */
6412286d8edStholo 	    rev1 = xstrdup (s);
6422286d8edStholo 	    rev2 = xstrdup (s);
6432286d8edStholo 	}
6442286d8edStholo 	else if (t == s)
6452286d8edStholo 	{
6462286d8edStholo 	    /* -o:rev2 */
6472286d8edStholo 	    rev1 = NULL;
6482286d8edStholo 	    rev2 = xstrdup (t2);
6492286d8edStholo 	}
6502286d8edStholo 	else
6512286d8edStholo 	{
6522286d8edStholo 	    *t = '\0';
6532286d8edStholo 	    rev1 = xstrdup (s);
6542286d8edStholo 	    *t = ':';	/* probably unnecessary */
6552286d8edStholo 	    if (*t2 == '\0')
6562286d8edStholo 		/* -orev1: */
6572286d8edStholo 		rev2 = NULL;
6582286d8edStholo 	    else
6592286d8edStholo 		/* -orev1:rev2 */
6602286d8edStholo 		rev2 = xstrdup (t2);
6612286d8edStholo 	}
6622286d8edStholo 
6632286d8edStholo 	if (rev1 == NULL && rev2 == NULL)
6642286d8edStholo 	{
6652286d8edStholo 	    /* RCS segfaults if `-o:' is given */
6662286d8edStholo 	    error (0, 0, "no valid revisions specified in `%s' option",
6672286d8edStholo 		   admin_data->delete_revs);
6682286d8edStholo 	    status = 1;
6692286d8edStholo 	}
6702286d8edStholo 	else
6712286d8edStholo 	{
6722286d8edStholo 	    status |= RCS_delete_revs (rcs, rev1, rev2, inclusive);
6732286d8edStholo 	    if (rev1)
6742286d8edStholo 		free (rev1);
6752286d8edStholo 	    if (rev2)
6762286d8edStholo 		free (rev2);
6772286d8edStholo 	}
6782286d8edStholo     }
6792286d8edStholo     if (admin_data->desc != NULL)
6802286d8edStholo     {
6812286d8edStholo 	free (rcs->desc);
682e77048c1Stholo 	rcs->desc = xstrdup (admin_data->desc);
6832286d8edStholo     }
6842286d8edStholo     if (admin_data->kflag != NULL)
6852286d8edStholo     {
6862286d8edStholo 	char *kflag = admin_data->kflag + 2;
687c71bc7e2Stholo 	char *oldexpand = RCS_getexpand (rcs);
688c71bc7e2Stholo 	if (oldexpand == NULL || strcmp (oldexpand, kflag) != 0)
689c71bc7e2Stholo 	    RCS_setexpand (rcs, kflag);
6902286d8edStholo     }
6912286d8edStholo 
6922286d8edStholo     /* Handle miscellaneous options.  TODO: decide whether any or all
6932286d8edStholo        of these should have their own fields in the admin_data
6942286d8edStholo        structure. */
6952286d8edStholo     for (i = 0; i < admin_data->ac; ++i)
6962286d8edStholo     {
6972286d8edStholo 	char *arg;
698f523375bSjcs 	char *p, *rev, *revnum, *tag, *msg, *commitid;
6992286d8edStholo 	char **users;
7002286d8edStholo 	int argc, u;
7012286d8edStholo 	Node *n;
7022286d8edStholo 	RCSVers *delta;
7032286d8edStholo 
7042286d8edStholo 	arg = admin_data->av[i];
7052286d8edStholo 	switch (arg[1])
7062286d8edStholo 	{
7072286d8edStholo 	    case 'a': /* fall through */
7082286d8edStholo 	    case 'e':
7092286d8edStholo 	        line2argv (&argc, &users, arg + 2, " ,\t\n");
7102286d8edStholo 		if (arg[1] == 'a')
7112286d8edStholo 		    for (u = 0; u < argc; ++u)
7122286d8edStholo 			RCS_addaccess (rcs, users[u]);
713e77048c1Stholo 		else if (argc == 0)
714e77048c1Stholo 		    RCS_delaccess (rcs, NULL);
7152286d8edStholo 		else
7162286d8edStholo 		    for (u = 0; u < argc; ++u)
7172286d8edStholo 			RCS_delaccess (rcs, users[u]);
7182286d8edStholo 		free_names (&argc, users);
7192286d8edStholo 		break;
7202286d8edStholo 	    case 'A':
7212286d8edStholo 
7222286d8edStholo 		/* See admin-19a-admin and friends in sanity.sh for
7232286d8edStholo 		   relative pathnames.  It makes sense to think in
7242286d8edStholo 		   terms of a syntax which give pathnames relative to
7252286d8edStholo 		   the repository or repository corresponding to the
7262286d8edStholo 		   current directory or some such (and perhaps don't
7272286d8edStholo 		   include ,v), but trying to worry about such things
7282286d8edStholo 		   is a little pointless unless you first worry about
7292286d8edStholo 		   whether "cvs admin -A" as a whole makes any sense
7302286d8edStholo 		   (currently probably not, as access lists don't
7312286d8edStholo 		   affect the behavior of CVS).  */
7322286d8edStholo 
7332286d8edStholo 		rcs2 = RCS_parsercsfile (arg + 2);
7342286d8edStholo 		if (rcs2 == NULL)
7352286d8edStholo 		    error (1, 0, "cannot continue");
7362286d8edStholo 
7372286d8edStholo 		p = xstrdup (RCS_getaccess (rcs2));
7382286d8edStholo 	        line2argv (&argc, &users, p, " \t\n");
7392286d8edStholo 		free (p);
7402286d8edStholo 		freercsnode (&rcs2);
7412286d8edStholo 
7422286d8edStholo 		for (u = 0; u < argc; ++u)
7432286d8edStholo 		    RCS_addaccess (rcs, users[u]);
7442286d8edStholo 		free_names (&argc, users);
7452286d8edStholo 		break;
746f523375bSjcs 	    case 'C':
747f523375bSjcs 	        p = strchr (arg, ':');
748f523375bSjcs 		if (p == NULL)
749f523375bSjcs 		{
750f523375bSjcs 		    error (0, 0, "%s: -C option lacks commitid", rcs->path);
751f523375bSjcs 		    status = 1;
752f523375bSjcs 		    continue;
753f523375bSjcs 		}
754f523375bSjcs 		*p = '\0';
755f523375bSjcs 		rev = RCS_gettag (rcs, arg + 2, 1, NULL);
756f523375bSjcs 		if (rev == NULL)
757f523375bSjcs 		{
758f523375bSjcs 		    error (0, 0, "%s: no such revision %s", rcs->path, arg + 2);
759f523375bSjcs 		    status = 1;
760f523375bSjcs 		    continue;
761f523375bSjcs 		}
762f523375bSjcs 		*p++ = ':';
763f523375bSjcs 		commitid = p;
764f523375bSjcs 
765f523375bSjcs 		if (*commitid == '\0')
766f523375bSjcs 		{
767f523375bSjcs 		    error (0, 0, "%s: -C option lacks commitid", rcs->path);
768f523375bSjcs 		    free (rev);
769f523375bSjcs 		    status = 1;
770f523375bSjcs 		    continue;
771f523375bSjcs 		}
772f523375bSjcs 
773f523375bSjcs 		n = findnode (rcs->versions, rev);
774f523375bSjcs 		delta = (RCSVers *) n->data;
775f523375bSjcs 
776f523375bSjcs 		if (delta->other_delta == NULL)
777f523375bSjcs     		    delta->other_delta = getlist();
778f523375bSjcs 
779*0971b67fStb 		if ((n = findnode (delta->other_delta, "commitid")))
780f523375bSjcs 		{
781f523375bSjcs 		    error (0, 0, "%s: revision %s already has commitid %s",
782f523375bSjcs 		        rcs->path, rev, n->data);
783f523375bSjcs 		    free (rev);
784f523375bSjcs 		    status = 1;
785f523375bSjcs 		    continue;
786f523375bSjcs 		}
787f523375bSjcs 
788f523375bSjcs 		n = getnode();
789f523375bSjcs 		n->type = RCSFIELD;
790f523375bSjcs 		n->key = xstrdup ("commitid");
791f523375bSjcs 		n->data = xstrdup(commitid);
792f523375bSjcs 		addnode (delta->other_delta, n);
793f523375bSjcs 
794f523375bSjcs 		free (rev);
795f523375bSjcs 
796f523375bSjcs 		break;
7972286d8edStholo 	    case 'n': /* fall through */
7982286d8edStholo 	    case 'N':
7992286d8edStholo 		if (arg[2] == '\0')
8002286d8edStholo 		{
8012286d8edStholo 		    cvs_outerr ("missing symbolic name after ", 0);
8022286d8edStholo 		    cvs_outerr (arg, 0);
8032286d8edStholo 		    cvs_outerr ("\n", 1);
8042286d8edStholo 		    break;
8052286d8edStholo 		}
8062286d8edStholo 		p = strchr (arg, ':');
8072286d8edStholo 		if (p == NULL)
8082286d8edStholo 		{
8092286d8edStholo 		    if (RCS_deltag (rcs, arg + 2) != 0)
8102286d8edStholo 		    {
8112286d8edStholo 			error (0, 0, "%s: Symbolic name %s is undefined.",
8122286d8edStholo 			       rcs->path,
8132286d8edStholo 			       arg + 2);
8142286d8edStholo 			status = 1;
8152286d8edStholo 			continue;
8162286d8edStholo 		    }
8172286d8edStholo 		    break;
8182286d8edStholo 		}
8192286d8edStholo 		*p = '\0';
8202286d8edStholo 		tag = xstrdup (arg + 2);
8212286d8edStholo 		*p++ = ':';
8222286d8edStholo 
8232286d8edStholo 		/* Option `n' signals an error if this tag is already bound. */
8242286d8edStholo 		if (arg[1] == 'n')
8252286d8edStholo 		{
8262286d8edStholo 		    n = findnode (RCS_symbols (rcs), tag);
8272286d8edStholo 		    if (n != NULL)
8282286d8edStholo 		    {
8292286d8edStholo 			error (0, 0,
8302286d8edStholo 			       "%s: symbolic name %s already bound to %s",
8312286d8edStholo 			       rcs->path,
8322286d8edStholo 			       tag, n->data);
8332286d8edStholo 			status = 1;
834b6f6614eStholo 			free (tag);
8352286d8edStholo 			continue;
8362286d8edStholo 		    }
8372286d8edStholo 		}
8382286d8edStholo 
839b6f6614eStholo                 /* Attempt to perform the requested tagging.  */
840b6f6614eStholo 
841b6f6614eStholo 		if ((*p == 0 && (rev = RCS_head (rcs)))
842b6f6614eStholo                     || (rev = RCS_tag2rev (rcs, p))) /* tag2rev may exit */
843b6f6614eStholo 		{
844b6f6614eStholo 		    RCS_check_tag (tag); /* exit if not a valid tag */
8452286d8edStholo 		    RCS_settag (rcs, tag, rev);
8462286d8edStholo 		    free (rev);
847b6f6614eStholo 		}
848b6f6614eStholo                 else
849b6f6614eStholo 		{
85043c1707eStholo 		    if (!really_quiet)
851b6f6614eStholo 			error (0, 0,
85243c1707eStholo 			       "%s: Symbolic name or revision %s is undefined.",
853b6f6614eStholo 			       rcs->path, p);
854b6f6614eStholo 		    status = 1;
855b6f6614eStholo 		}
8562286d8edStholo 		free (tag);
8572286d8edStholo 		break;
8582286d8edStholo 	    case 's':
8592286d8edStholo 	        p = strchr (arg, ':');
8602286d8edStholo 		if (p == NULL)
8612286d8edStholo 		{
8622286d8edStholo 		    tag = xstrdup (arg + 2);
8632286d8edStholo 		    rev = RCS_head (rcs);
8642286d8edStholo 		}
8652286d8edStholo 		else
8662286d8edStholo 		{
8672286d8edStholo 		    *p = '\0';
8682286d8edStholo 		    tag = xstrdup (arg + 2);
8692286d8edStholo 		    *p++ = ':';
8702286d8edStholo 		    rev = xstrdup (p);
8712286d8edStholo 		}
8722286d8edStholo 		revnum = RCS_gettag (rcs, rev, 0, NULL);
8732286d8edStholo 		if (revnum != NULL)
874e77048c1Stholo 		{
8752286d8edStholo 		    n = findnode (rcs->versions, revnum);
876e77048c1Stholo 		    free (revnum);
877e77048c1Stholo 		}
878e77048c1Stholo 		else
879e77048c1Stholo 		    n = NULL;
880e77048c1Stholo 		if (n == NULL)
8812286d8edStholo 		{
8822286d8edStholo 		    error (0, 0,
8832286d8edStholo 			   "%s: can't set state of nonexisting revision %s",
8842286d8edStholo 			   rcs->path,
8852286d8edStholo 			   rev);
886e77048c1Stholo 		    free (rev);
8872286d8edStholo 		    status = 1;
8882286d8edStholo 		    continue;
8892286d8edStholo 		}
890e77048c1Stholo 		free (rev);
8912286d8edStholo 		delta = (RCSVers *) n->data;
8922286d8edStholo 		free (delta->state);
8932286d8edStholo 		delta->state = tag;
8942286d8edStholo 		break;
8952286d8edStholo 
8962286d8edStholo 	    case 'm':
8972286d8edStholo 	        p = strchr (arg, ':');
8982286d8edStholo 		if (p == NULL)
8992286d8edStholo 		{
9002286d8edStholo 		    error (0, 0, "%s: -m option lacks revision number",
9012286d8edStholo 			   rcs->path);
9022286d8edStholo 		    status = 1;
9032286d8edStholo 		    continue;
9042286d8edStholo 		}
9052286d8edStholo 		*p = '\0';
9062286d8edStholo 		rev = RCS_gettag (rcs, arg + 2, 0, NULL);
9072286d8edStholo 		if (rev == NULL)
9082286d8edStholo 		{
9092286d8edStholo 		    error (0, 0, "%s: no such revision %s", rcs->path, rev);
9102286d8edStholo 		    status = 1;
9112286d8edStholo 		    continue;
9122286d8edStholo 		}
9132286d8edStholo 		*p++ = ':';
9142286d8edStholo 		msg = p;
9152286d8edStholo 
9162286d8edStholo 		n = findnode (rcs->versions, rev);
917e77048c1Stholo 		free (rev);
9182286d8edStholo 		delta = (RCSVers *) n->data;
9192286d8edStholo 		if (delta->text == NULL)
9202286d8edStholo 		{
9212286d8edStholo 		    delta->text = (Deltatext *) xmalloc (sizeof (Deltatext));
9222286d8edStholo 		    memset ((void *) delta->text, 0, sizeof (Deltatext));
9232286d8edStholo 		}
9242286d8edStholo 		delta->text->version = xstrdup (delta->version);
9252286d8edStholo 		delta->text->log = make_message_rcslegal (msg);
9262286d8edStholo 		break;
9272286d8edStholo 
9282286d8edStholo 	    case 'l':
9292286d8edStholo 	        status |= RCS_lock (rcs, arg[2] ? arg + 2 : NULL, 0);
9302286d8edStholo 		break;
9312286d8edStholo 	    case 'u':
9322286d8edStholo 		status |= RCS_unlock (rcs, arg[2] ? arg + 2 : NULL, 0);
9332286d8edStholo 		break;
9342286d8edStholo 	    default: assert(0);	/* can't happen */
9352286d8edStholo 	}
9362286d8edStholo     }
9372286d8edStholo 
9382286d8edStholo     if (status == 0)
9392286d8edStholo     {
9402286d8edStholo 	RCS_rewrite (rcs, NULL, NULL);
94143c1707eStholo 	if (!really_quiet)
9422286d8edStholo 	    cvs_output ("done\n", 5);
9432286d8edStholo     }
9442286d8edStholo     else
9452286d8edStholo     {
9462286d8edStholo 	/* Note that this message should only occur after another
9472286d8edStholo 	   message has given a more specific error.  The point of this
9482286d8edStholo 	   additional message is to make it clear that the previous problems
9492286d8edStholo 	   caused CVS to forget about the idea of modifying the RCS file.  */
95043c1707eStholo 	if (!really_quiet)
95143c1707eStholo 	    error (0, 0, "RCS file for `%s' not modified.", finfo->file);
952e77048c1Stholo 	RCS_abandon (rcs);
9532286d8edStholo     }
9542286d8edStholo 
95513571821Stholo   exitfunc:
95613571821Stholo     freevers_ts (&vers);
95713571821Stholo     return status;
9581e72d8d2Sderaadt }
9591e72d8d2Sderaadt 
9601e72d8d2Sderaadt /*
9611e72d8d2Sderaadt  * Print a warm fuzzy message
9621e72d8d2Sderaadt  */
9631e72d8d2Sderaadt /* ARGSUSED */
9641e72d8d2Sderaadt static Dtype
admin_dirproc(callerdat,dir,repos,update_dir,entries)96550bf276cStholo admin_dirproc (callerdat, dir, repos, update_dir, entries)
96650bf276cStholo     void *callerdat;
9671e72d8d2Sderaadt     char *dir;
9681e72d8d2Sderaadt     char *repos;
9691e72d8d2Sderaadt     char *update_dir;
97050bf276cStholo     List *entries;
9711e72d8d2Sderaadt {
9721e72d8d2Sderaadt     if (!quiet)
9731e72d8d2Sderaadt 	error (0, 0, "Administrating %s", update_dir);
9741e72d8d2Sderaadt     return (R_PROCESS);
9751e72d8d2Sderaadt }
976