admin.c revision 102840
1/*
2 * Copyright (c) 1992, Brian Berliner and Jeff Polk
3 * Copyright (c) 1989-1992, Brian Berliner
4 *
5 * You may distribute under the terms of the GNU General Public License as
6 * specified in the README file that comes with the CVS source distribution.
7 *
8 * Administration ("cvs admin")
9 *
10 */
11
12#include "cvs.h"
13#ifdef CVS_ADMIN_GROUP
14#include <grp.h>
15#endif
16#include <assert.h>
17
18static Dtype admin_dirproc PROTO ((void *callerdat, char *dir,
19				   char *repos, char *update_dir,
20				   List *entries));
21static int admin_fileproc PROTO ((void *callerdat, struct file_info *finfo));
22
23static const char *const admin_usage[] =
24{
25    "Usage: %s %s [options] files...\n",
26    "\t-a users   Append (comma-separated) user names to access list.\n",
27    "\t-A file    Append another file's access list.\n",
28    "\t-b[rev]    Set default branch (highest branch on trunk if omitted).\n",
29    "\t-c string  Set comment leader.\n",
30    "\t-e[users]  Remove (comma-separated) user names from access list\n",
31    "\t           (all names if omitted).\n",
32    "\t-I         Run interactively.\n",
33    "\t-k subst   Set keyword substitution mode:\n",
34    "\t   kv   (Default) Substitute keyword and value.\n",
35    "\t   kvl  Substitute keyword, value, and locker (if any).\n",
36    "\t   k    Substitute keyword only.\n",
37    "\t   o    Preserve original string.\n",
38    "\t   b    Like o, but mark file as binary.\n",
39    "\t   v    Substitute value only.\n",
40    "\t-l[rev]    Lock revision (latest revision on branch,\n",
41    "\t           latest revision on trunk if omitted).\n",
42    "\t-L         Set strict locking.\n",
43    "\t-m rev:msg  Replace revision's log message.\n",
44    "\t-n tag[:[rev]]  Tag branch or revision.  If :rev is omitted,\n",
45    "\t                delete the tag; if rev is omitted, tag the latest\n",
46    "\t                revision on the default branch.\n",
47    "\t-N tag[:[rev]]  Same as -n except override existing tag.\n",
48    "\t-o range   Delete (outdate) specified range of revisions:\n",
49    "\t   rev1:rev2   Between rev1 and rev2, including rev1 and rev2.\n",
50    "\t   rev1::rev2  Between rev1 and rev2, excluding rev1 and rev2.\n",
51    "\t   rev:        rev and following revisions on the same branch.\n",
52    "\t   rev::       After rev on the same branch.\n",
53    "\t   :rev        rev and previous revisions on the same branch.\n",
54    "\t   ::rev       Before rev on the same branch.\n",
55    "\t   rev         Just rev.\n",
56    "\t-q         Run quietly.\n",
57    "\t-s state[:rev]  Set revision state (latest revision on branch,\n",
58    "\t                latest revision on trunk if omitted).\n",
59    "\t-t[file]   Get descriptive text from file (stdin if omitted).\n",
60    "\t-t-string  Set descriptive text.\n",
61    "\t-u[rev]    Unlock the revision (latest revision on branch,\n",
62    "\t           latest revision on trunk if omitted).\n",
63    "\t-U         Unset strict locking.\n",
64    "(Specify the --help global option for a list of other help options)\n",
65    NULL
66};
67
68/* This structure is used to pass information through start_recursion.  */
69struct admin_data
70{
71    /* Set default branch (-b).  It is "-b" followed by the value
72       given, or NULL if not specified, or merely "-b" if -b is
73       specified without a value.  */
74    char *branch;
75
76    /* Set comment leader (-c).  It is "-c" followed by the value
77       given, or NULL if not specified.  The comment leader is
78       relevant only for old versions of RCS, but we let people set it
79       anyway.  */
80    char *comment;
81
82    /* Set strict locking (-L).  */
83    int set_strict;
84
85    /* Set nonstrict locking (-U).  */
86    int set_nonstrict;
87
88    /* Delete revisions (-o).  It is "-o" followed by the value specified.  */
89    char *delete_revs;
90
91    /* Keyword substitution mode (-k), e.g. "-kb".  */
92    char *kflag;
93
94    /* Description (-t).  */
95    char *desc;
96
97    /* Interactive (-I).  Problematic with client/server.  */
98    int interactive;
99
100    /* This is the cheesy part.  It is a vector with the options which
101       we don't deal with above (e.g. "-afoo" "-abar,baz").  In the future
102       this presumably will be replaced by other variables which break
103       out the data in a more convenient fashion.  AV as well as each of
104       the strings it points to is malloc'd.  */
105    int ac;
106    char **av;
107    int av_alloc;
108};
109
110/* Add an argument.  OPT is the option letter, e.g. 'a'.  ARG is the
111   argument to that option, or NULL if omitted (whether NULL can actually
112   happen depends on whether the option was specified as optional to
113   getopt).  */
114static void
115arg_add (dat, opt, arg)
116    struct admin_data *dat;
117    int opt;
118    char *arg;
119{
120    char *newelt = xmalloc ((arg == NULL ? 0 : strlen (arg)) + 3);
121    strcpy (newelt, "-");
122    newelt[1] = opt;
123    if (arg == NULL)
124	newelt[2] = '\0';
125    else
126	strcpy (newelt + 2, arg);
127
128    if (dat->av_alloc == 0)
129    {
130	dat->av_alloc = 1;
131	dat->av = (char **) xmalloc (dat->av_alloc * sizeof (*dat->av));
132    }
133    else if (dat->ac >= dat->av_alloc)
134    {
135	dat->av_alloc *= 2;
136	dat->av = (char **) xrealloc (dat->av,
137				      dat->av_alloc * sizeof (*dat->av));
138    }
139    dat->av[dat->ac++] = newelt;
140}
141
142int
143admin (argc, argv)
144    int argc;
145    char **argv;
146{
147    int err;
148#ifdef CVS_ADMIN_GROUP
149    struct group *grp;
150    struct group *getgrnam();
151#endif
152    struct admin_data admin_data;
153    int c;
154    int i;
155    int only_k_option;
156
157    if (argc <= 1)
158	usage (admin_usage);
159
160    wrap_setup ();
161
162    memset (&admin_data, 0, sizeof admin_data);
163
164    /* TODO: get rid of `-' switch notation in admin_data.  For
165       example, admin_data->branch should be not `-bfoo' but simply `foo'. */
166
167    optind = 0;
168    only_k_option = 1;
169    while ((c = getopt (argc, argv,
170			"+ib::c:a:A:e::l::u::LUn:N:m:o:s:t::IqxV:k:")) != -1)
171    {
172	if (c != 'k' && c != 'q')
173	    only_k_option = 0;
174
175	switch (c)
176	{
177	    case 'i':
178		/* This has always been documented as useless in cvs.texinfo
179		   and it really is--admin_fileproc silently does nothing
180		   if vers->vn_user is NULL. */
181		error (0, 0, "the -i option to admin is not supported");
182		error (0, 0, "run add or import to create an RCS file");
183		goto usage_error;
184
185	    case 'b':
186		if (admin_data.branch != NULL)
187		{
188		    error (0, 0, "duplicate 'b' option");
189		    goto usage_error;
190		}
191		if (optarg == NULL)
192		    admin_data.branch = xstrdup ("-b");
193		else
194		{
195		    admin_data.branch = xmalloc (strlen (optarg) + 5);
196		    strcpy (admin_data.branch, "-b");
197		    strcat (admin_data.branch, optarg);
198		}
199		break;
200
201	    case 'c':
202		if (admin_data.comment != NULL)
203		{
204		    error (0, 0, "duplicate 'c' option");
205		    goto usage_error;
206		}
207		admin_data.comment = xmalloc (strlen (optarg) + 5);
208		strcpy (admin_data.comment, "-c");
209		strcat (admin_data.comment, optarg);
210		break;
211
212	    case 'a':
213		arg_add (&admin_data, 'a', optarg);
214		break;
215
216	    case 'A':
217		/* In the client/server case, this is cheesy because
218		   we just pass along the name of the RCS file, which
219		   then will want to exist on the server.  This is
220		   accidental; having the client specify a pathname on
221		   the server is not a design feature of the protocol.  */
222		arg_add (&admin_data, 'A', optarg);
223		break;
224
225	    case 'e':
226		arg_add (&admin_data, 'e', optarg);
227		break;
228
229	    case 'l':
230		/* Note that multiple -l options are legal.  */
231		arg_add (&admin_data, 'l', optarg);
232		break;
233
234	    case 'u':
235		/* Note that multiple -u options are legal.  */
236		arg_add (&admin_data, 'u', optarg);
237		break;
238
239	    case 'L':
240		/* Probably could also complain if -L is specified multiple
241		   times, although RCS doesn't and I suppose it is reasonable
242		   just to have it mean the same as a single -L.  */
243		if (admin_data.set_nonstrict)
244		{
245		    error (0, 0, "-U and -L are incompatible");
246		    goto usage_error;
247		}
248		admin_data.set_strict = 1;
249		break;
250
251	    case 'U':
252		/* Probably could also complain if -U is specified multiple
253		   times, although RCS doesn't and I suppose it is reasonable
254		   just to have it mean the same as a single -U.  */
255		if (admin_data.set_strict)
256		{
257		    error (0, 0, "-U and -L are incompatible");
258		    goto usage_error;
259		}
260		admin_data.set_nonstrict = 1;
261		break;
262
263	    case 'n':
264		/* Mostly similar to cvs tag.  Could also be parsing
265		   the syntax of optarg, although for now we just pass
266		   it to rcs as-is.  Note that multiple -n options are
267		   legal.  */
268		arg_add (&admin_data, 'n', optarg);
269		break;
270
271	    case 'N':
272		/* Mostly similar to cvs tag.  Could also be parsing
273		   the syntax of optarg, although for now we just pass
274		   it to rcs as-is.  Note that multiple -N options are
275		   legal.  */
276		arg_add (&admin_data, 'N', optarg);
277		break;
278
279	    case 'm':
280		/* Change log message.  Could also be parsing the syntax
281		   of optarg, although for now we just pass it to rcs
282		   as-is.  Note that multiple -m options are legal.  */
283		arg_add (&admin_data, 'm', optarg);
284		break;
285
286	    case 'o':
287		/* Delete revisions.  Probably should also be parsing the
288		   syntax of optarg, so that the client can give errors
289		   rather than making the server take care of that.
290		   Other than that I'm not sure whether it matters much
291		   whether we parse it here or in admin_fileproc.
292
293		   Note that multiple -o options are illegal, in RCS
294		   as well as here.  */
295
296		if (admin_data.delete_revs != NULL)
297		{
298		    error (0, 0, "duplicate '-o' option");
299		    goto usage_error;
300		}
301		admin_data.delete_revs = xmalloc (strlen (optarg) + 5);
302		strcpy (admin_data.delete_revs, "-o");
303		strcat (admin_data.delete_revs, optarg);
304		break;
305
306	    case 's':
307		/* Note that multiple -s options are legal.  */
308		arg_add (&admin_data, 's', optarg);
309		break;
310
311	    case 't':
312		if (admin_data.desc != NULL)
313		{
314		    error (0, 0, "duplicate 't' option");
315		    goto usage_error;
316		}
317		if (optarg != NULL && optarg[0] == '-')
318		    admin_data.desc = xstrdup (optarg + 1);
319		else
320		{
321		    size_t bufsize = 0;
322		    size_t len;
323
324		    get_file (optarg, optarg, "r", &admin_data.desc,
325			      &bufsize, &len);
326		}
327		break;
328
329	    case 'I':
330		/* At least in RCS this can be specified several times,
331		   with the same meaning as being specified once.  */
332		admin_data.interactive = 1;
333		break;
334
335	    case 'q':
336		/* Silently set the global really_quiet flag.  This keeps admin in
337		 * sync with the RCS man page and allows us to silently support
338		 * older servers when necessary.
339		 *
340		 * Some logic says we might want to output a deprecation warning
341		 * here, but I'm opting not to in order to stay quietly in sync
342		 * with the RCS man page.
343		 */
344		really_quiet = 1;
345		break;
346
347	    case 'x':
348		error (0, 0, "the -x option has never done anything useful");
349		error (0, 0, "RCS files in CVS always end in ,v");
350		goto usage_error;
351
352	    case 'V':
353		/* No longer supported. */
354		error (0, 0, "the `-V' option is obsolete");
355		break;
356
357	    case 'k':
358		if (admin_data.kflag != NULL)
359		{
360		    error (0, 0, "duplicate '-k' option");
361		    goto usage_error;
362		}
363		admin_data.kflag = RCS_check_kflag (optarg);
364		break;
365	    default:
366	    case '?':
367		/* getopt will have printed an error message.  */
368
369	    usage_error:
370		/* Don't use command_name; it might be "server".  */
371	        error (1, 0, "specify %s -H admin for usage information",
372		       program_name);
373	}
374    }
375    argc -= optind;
376    argv += optind;
377
378#ifdef CVS_ADMIN_GROUP
379    /* The use of `cvs admin -k' is unrestricted.  However, any other
380       option is restricted if the group CVS_ADMIN_GROUP exists.  */
381    if (!only_k_option &&
382	(grp = getgrnam(CVS_ADMIN_GROUP)) != NULL)
383    {
384#ifdef HAVE_GETGROUPS
385	gid_t *grps;
386	int n;
387
388	/* get number of auxiliary groups */
389	n = getgroups (0, NULL);
390	if (n < 0)
391	    error (1, errno, "unable to get number of auxiliary groups");
392	grps = (gid_t *) xmalloc((n + 1) * sizeof *grps);
393	n = getgroups (n, grps);
394	if (n < 0)
395	    error (1, errno, "unable to get list of auxiliary groups");
396	grps[n] = getgid();
397	for (i = 0; i <= n; i++)
398	    if (grps[i] == grp->gr_gid) break;
399	free (grps);
400	if (i > n)
401	    error (1, 0, "usage is restricted to members of the group %s",
402		   CVS_ADMIN_GROUP);
403#else
404	char *me = getcaller();
405	char **grnam;
406
407	for (grnam = grp->gr_mem; *grnam; grnam++)
408	    if (strcmp (*grnam, me) == 0) break;
409	if (!*grnam && getgid() != grp->gr_gid)
410	    error (1, 0, "usage is restricted to members of the group %s",
411		   CVS_ADMIN_GROUP);
412#endif
413    }
414#endif
415
416    for (i = 0; i < admin_data.ac; ++i)
417    {
418	assert (admin_data.av[i][0] == '-');
419	switch (admin_data.av[i][1])
420	{
421	    case 'm':
422	    case 'l':
423	    case 'u':
424		check_numeric (&admin_data.av[i][2], argc, argv);
425		break;
426	    default:
427		break;
428	}
429    }
430    if (admin_data.branch != NULL)
431	check_numeric (admin_data.branch + 2, argc, argv);
432    if (admin_data.delete_revs != NULL)
433    {
434	char *p;
435
436	check_numeric (admin_data.delete_revs + 2, argc, argv);
437	p = strchr (admin_data.delete_revs + 2, ':');
438	if (p != NULL && isdigit ((unsigned char) p[1]))
439	    check_numeric (p + 1, argc, argv);
440	else if (p != NULL && p[1] == ':' && isdigit ((unsigned char) p[2]))
441	    check_numeric (p + 2, argc, argv);
442    }
443
444#ifdef CLIENT_SUPPORT
445    if (current_parsed_root->isremote)
446    {
447	/* We're the client side.  Fire up the remote server.  */
448	start_server ();
449
450	ign_setup ();
451
452	/* Note that option_with_arg does not work for us, because some
453	   of the options must be sent without a space between the option
454	   and its argument.  */
455	if (admin_data.interactive)
456	    error (1, 0, "-I option not useful with client/server");
457	if (admin_data.branch != NULL)
458	    send_arg (admin_data.branch);
459	if (admin_data.comment != NULL)
460	    send_arg (admin_data.comment);
461	if (admin_data.set_strict)
462	    send_arg ("-L");
463	if (admin_data.set_nonstrict)
464	    send_arg ("-U");
465	if (admin_data.delete_revs != NULL)
466	    send_arg (admin_data.delete_revs);
467	if (admin_data.desc != NULL)
468	{
469	    char *p = admin_data.desc;
470	    send_to_server ("Argument -t-", 0);
471	    while (*p)
472	    {
473		if (*p == '\n')
474		{
475		    send_to_server ("\012Argumentx ", 0);
476		    ++p;
477		}
478		else
479		{
480		    char *q = strchr (p, '\n');
481		    if (q == NULL) q = p + strlen (p);
482		    send_to_server (p, q - p);
483		    p = q;
484		}
485	    }
486	    send_to_server ("\012", 1);
487	}
488	/* Send this for all really_quiets since we know that it will be silently
489	 * ignored when unneeded.  This supports old servers.
490	 */
491	if (really_quiet)
492	    send_arg ("-q");
493	if (admin_data.kflag != NULL)
494	    send_arg (admin_data.kflag);
495
496	for (i = 0; i < admin_data.ac; ++i)
497	    send_arg (admin_data.av[i]);
498
499	send_files (argc, argv, 0, 0, SEND_NO_CONTENTS);
500	send_file_names (argc, argv, SEND_EXPAND_WILD);
501	send_to_server ("admin\012", 0);
502        err = get_responses_and_close ();
503	goto return_it;
504    }
505#endif /* CLIENT_SUPPORT */
506
507    lock_tree_for_write (argc, argv, 0, W_LOCAL, 0);
508
509    err = start_recursion (admin_fileproc, (FILESDONEPROC) NULL, admin_dirproc,
510			   (DIRLEAVEPROC) NULL, (void *)&admin_data,
511			   argc, argv, 0,
512			   W_LOCAL, 0, 0, (char *) NULL, 1);
513    Lock_Cleanup ();
514
515 return_it:
516    if (admin_data.branch != NULL)
517	free (admin_data.branch);
518    if (admin_data.comment != NULL)
519	free (admin_data.comment);
520    if (admin_data.delete_revs != NULL)
521	free (admin_data.delete_revs);
522    if (admin_data.kflag != NULL)
523	free (admin_data.kflag);
524    if (admin_data.desc != NULL)
525	free (admin_data.desc);
526    for (i = 0; i < admin_data.ac; ++i)
527	free (admin_data.av[i]);
528    if (admin_data.av != NULL)
529	free (admin_data.av);
530
531    return (err);
532}
533
534/*
535 * Called to run "rcs" on a particular file.
536 */
537/* ARGSUSED */
538static int
539admin_fileproc (callerdat, finfo)
540    void *callerdat;
541    struct file_info *finfo;
542{
543    struct admin_data *admin_data = (struct admin_data *) callerdat;
544    Vers_TS *vers;
545    char *version;
546    int i;
547    int status = 0;
548    RCSNode *rcs, *rcs2;
549
550    vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0);
551
552    version = vers->vn_user;
553    if (version == NULL)
554	goto exitfunc;
555    else if (strcmp (version, "0") == 0)
556    {
557	error (0, 0, "cannot admin newly added file `%s'", finfo->file);
558	goto exitfunc;
559    }
560
561    rcs = vers->srcfile;
562    if (rcs->flags & PARTIAL)
563	RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL);
564
565    status = 0;
566
567    if (!really_quiet)
568    {
569	cvs_output ("RCS file: ", 0);
570	cvs_output (rcs->path, 0);
571	cvs_output ("\n", 1);
572    }
573
574    if (admin_data->branch != NULL)
575    {
576	char *branch = &admin_data->branch[2];
577	if (*branch != '\0' && ! isdigit ((unsigned char) *branch))
578	{
579	    branch = RCS_whatbranch (rcs, admin_data->branch + 2);
580	    if (branch == NULL)
581	    {
582		error (0, 0, "%s: Symbolic name %s is undefined.",
583				rcs->path, admin_data->branch + 2);
584		status = 1;
585	    }
586	}
587	if (status == 0)
588	    RCS_setbranch (rcs, branch);
589	if (branch != NULL && branch != &admin_data->branch[2])
590	    free (branch);
591    }
592    if (admin_data->comment != NULL)
593    {
594	if (rcs->comment != NULL)
595	    free (rcs->comment);
596	rcs->comment = xstrdup (admin_data->comment + 2);
597    }
598    if (admin_data->set_strict)
599	rcs->strict_locks = 1;
600    if (admin_data->set_nonstrict)
601	rcs->strict_locks = 0;
602    if (admin_data->delete_revs != NULL)
603    {
604	char *s, *t, *rev1, *rev2;
605	/* Set for :, clear for ::.  */
606	int inclusive;
607	char *t2;
608
609	s = admin_data->delete_revs + 2;
610	inclusive = 1;
611	t = strchr (s, ':');
612	if (t != NULL)
613	{
614	    if (t[1] == ':')
615	    {
616		inclusive = 0;
617		t2 = t + 2;
618	    }
619	    else
620		t2 = t + 1;
621	}
622
623	/* Note that we don't support '-' for ranges.  RCS considers it
624	   obsolete and it is problematic with tags containing '-'.  "cvs log"
625	   has made the same decision.  */
626
627	if (t == NULL)
628	{
629	    /* -orev */
630	    rev1 = xstrdup (s);
631	    rev2 = xstrdup (s);
632	}
633	else if (t == s)
634	{
635	    /* -o:rev2 */
636	    rev1 = NULL;
637	    rev2 = xstrdup (t2);
638	}
639	else
640	{
641	    *t = '\0';
642	    rev1 = xstrdup (s);
643	    *t = ':';	/* probably unnecessary */
644	    if (*t2 == '\0')
645		/* -orev1: */
646		rev2 = NULL;
647	    else
648		/* -orev1:rev2 */
649		rev2 = xstrdup (t2);
650	}
651
652	if (rev1 == NULL && rev2 == NULL)
653	{
654	    /* RCS segfaults if `-o:' is given */
655	    error (0, 0, "no valid revisions specified in `%s' option",
656		   admin_data->delete_revs);
657	    status = 1;
658	}
659	else
660	{
661	    status |= RCS_delete_revs (rcs, rev1, rev2, inclusive);
662	    if (rev1)
663		free (rev1);
664	    if (rev2)
665		free (rev2);
666	}
667    }
668    if (admin_data->desc != NULL)
669    {
670	free (rcs->desc);
671	rcs->desc = xstrdup (admin_data->desc);
672    }
673    if (admin_data->kflag != NULL)
674    {
675	char *kflag = admin_data->kflag + 2;
676	char *oldexpand = RCS_getexpand (rcs);
677	if (oldexpand == NULL || strcmp (oldexpand, kflag) != 0)
678	    RCS_setexpand (rcs, kflag);
679    }
680
681    /* Handle miscellaneous options.  TODO: decide whether any or all
682       of these should have their own fields in the admin_data
683       structure. */
684    for (i = 0; i < admin_data->ac; ++i)
685    {
686	char *arg;
687	char *p, *rev, *revnum, *tag, *msg;
688	char **users;
689	int argc, u;
690	Node *n;
691	RCSVers *delta;
692
693	arg = admin_data->av[i];
694	switch (arg[1])
695	{
696	    case 'a': /* fall through */
697	    case 'e':
698	        line2argv (&argc, &users, arg + 2, " ,\t\n");
699		if (arg[1] == 'a')
700		    for (u = 0; u < argc; ++u)
701			RCS_addaccess (rcs, users[u]);
702		else if (argc == 0)
703		    RCS_delaccess (rcs, NULL);
704		else
705		    for (u = 0; u < argc; ++u)
706			RCS_delaccess (rcs, users[u]);
707		free_names (&argc, users);
708		break;
709	    case 'A':
710
711		/* See admin-19a-admin and friends in sanity.sh for
712		   relative pathnames.  It makes sense to think in
713		   terms of a syntax which give pathnames relative to
714		   the repository or repository corresponding to the
715		   current directory or some such (and perhaps don't
716		   include ,v), but trying to worry about such things
717		   is a little pointless unless you first worry about
718		   whether "cvs admin -A" as a whole makes any sense
719		   (currently probably not, as access lists don't
720		   affect the behavior of CVS).  */
721
722		rcs2 = RCS_parsercsfile (arg + 2);
723		if (rcs2 == NULL)
724		    error (1, 0, "cannot continue");
725
726		p = xstrdup (RCS_getaccess (rcs2));
727	        line2argv (&argc, &users, p, " \t\n");
728		free (p);
729		freercsnode (&rcs2);
730
731		for (u = 0; u < argc; ++u)
732		    RCS_addaccess (rcs, users[u]);
733		free_names (&argc, users);
734		break;
735	    case 'n': /* fall through */
736	    case 'N':
737		if (arg[2] == '\0')
738		{
739		    cvs_outerr ("missing symbolic name after ", 0);
740		    cvs_outerr (arg, 0);
741		    cvs_outerr ("\n", 1);
742		    break;
743		}
744		p = strchr (arg, ':');
745		if (p == NULL)
746		{
747		    if (RCS_deltag (rcs, arg + 2) != 0)
748		    {
749			error (0, 0, "%s: Symbolic name %s is undefined.",
750			       rcs->path,
751			       arg + 2);
752			status = 1;
753			continue;
754		    }
755		    break;
756		}
757		*p = '\0';
758		tag = xstrdup (arg + 2);
759		*p++ = ':';
760
761		/* Option `n' signals an error if this tag is already bound. */
762		if (arg[1] == 'n')
763		{
764		    n = findnode (RCS_symbols (rcs), tag);
765		    if (n != NULL)
766		    {
767			error (0, 0,
768			       "%s: symbolic name %s already bound to %s",
769			       rcs->path,
770			       tag, n->data);
771			status = 1;
772			free (tag);
773			continue;
774		    }
775		}
776
777                /* Attempt to perform the requested tagging.  */
778
779		if ((*p == 0 && (rev = RCS_head (rcs)))
780                    || (rev = RCS_tag2rev (rcs, p))) /* tag2rev may exit */
781		{
782		    RCS_check_tag (tag); /* exit if not a valid tag */
783		    RCS_settag (rcs, tag, rev);
784		    free (rev);
785		}
786                else
787		{
788		    if (!really_quiet)
789			error (0, 0,
790			       "%s: Symbolic name or revision %s is undefined.",
791			       rcs->path, p);
792		    status = 1;
793		}
794		free (tag);
795		break;
796	    case 's':
797	        p = strchr (arg, ':');
798		if (p == NULL)
799		{
800		    tag = xstrdup (arg + 2);
801		    rev = RCS_head (rcs);
802		}
803		else
804		{
805		    *p = '\0';
806		    tag = xstrdup (arg + 2);
807		    *p++ = ':';
808		    rev = xstrdup (p);
809		}
810		revnum = RCS_gettag (rcs, rev, 0, NULL);
811		if (revnum != NULL)
812		{
813		    n = findnode (rcs->versions, revnum);
814		    free (revnum);
815		}
816		else
817		    n = NULL;
818		if (n == NULL)
819		{
820		    error (0, 0,
821			   "%s: can't set state of nonexisting revision %s",
822			   rcs->path,
823			   rev);
824		    free (rev);
825		    status = 1;
826		    continue;
827		}
828		free (rev);
829		delta = (RCSVers *) n->data;
830		free (delta->state);
831		delta->state = tag;
832		break;
833
834	    case 'm':
835	        p = strchr (arg, ':');
836		if (p == NULL)
837		{
838		    error (0, 0, "%s: -m option lacks revision number",
839			   rcs->path);
840		    status = 1;
841		    continue;
842		}
843		*p = '\0';
844		rev = RCS_gettag (rcs, arg + 2, 0, NULL);
845		if (rev == NULL)
846		{
847		    error (0, 0, "%s: no such revision %s", rcs->path, rev);
848		    status = 1;
849		    continue;
850		}
851		*p++ = ':';
852		msg = p;
853
854		n = findnode (rcs->versions, rev);
855		free (rev);
856		delta = (RCSVers *) n->data;
857		if (delta->text == NULL)
858		{
859		    delta->text = (Deltatext *) xmalloc (sizeof (Deltatext));
860		    memset ((void *) delta->text, 0, sizeof (Deltatext));
861		}
862		delta->text->version = xstrdup (delta->version);
863		delta->text->log = make_message_rcslegal (msg);
864		break;
865
866	    case 'l':
867	        status |= RCS_lock (rcs, arg[2] ? arg + 2 : NULL, 0);
868		break;
869	    case 'u':
870		status |= RCS_unlock (rcs, arg[2] ? arg + 2 : NULL, 0);
871		break;
872	    default: assert(0);	/* can't happen */
873	}
874    }
875
876    if (status == 0)
877    {
878	RCS_rewrite (rcs, NULL, NULL);
879	if (!really_quiet)
880	    cvs_output ("done\n", 5);
881    }
882    else
883    {
884	/* Note that this message should only occur after another
885	   message has given a more specific error.  The point of this
886	   additional message is to make it clear that the previous problems
887	   caused CVS to forget about the idea of modifying the RCS file.  */
888	if (!really_quiet)
889	    error (0, 0, "RCS file for `%s' not modified.", finfo->file);
890	RCS_abandon (rcs);
891    }
892
893  exitfunc:
894    freevers_ts (&vers);
895    return status;
896}
897
898/*
899 * Print a warm fuzzy message
900 */
901/* ARGSUSED */
902static Dtype
903admin_dirproc (callerdat, dir, repos, update_dir, entries)
904    void *callerdat;
905    char *dir;
906    char *repos;
907    char *update_dir;
908    List *entries;
909{
910    if (!quiet)
911	error (0, 0, "Administrating %s", update_dir);
912    return (R_PROCESS);
913}
914