admin.c revision 128266
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, const char *dir,
19                                   const char *repos, const 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 cvs_cmd_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 on the
381       server.  */
382    if (
383# ifdef CLIENT_SUPPORT
384        /* This is only "secure" on the server, since the user could edit the
385	 * RCS file on a local host, but some people like this kind of
386	 * check anyhow.  The alternative would be to check only when
387	 * (server_active) rather than when not on the client.
388	 */
389        !current_parsed_root->isremote &&
390# endif	/* CLIENT_SUPPORT */
391        !only_k_option
392	&& (grp = getgrnam(CVS_ADMIN_GROUP)) != NULL)
393    {
394#ifdef HAVE_GETGROUPS
395	gid_t *grps;
396	int n;
397
398	/* get number of auxiliary groups */
399	n = getgroups (0, NULL);
400	if (n < 0)
401	    error (1, errno, "unable to get number of auxiliary groups");
402	grps = (gid_t *) xmalloc((n + 1) * sizeof *grps);
403	n = getgroups (n, grps);
404	if (n < 0)
405	    error (1, errno, "unable to get list of auxiliary groups");
406	grps[n] = getgid();
407	for (i = 0; i <= n; i++)
408	    if (grps[i] == grp->gr_gid) break;
409	free (grps);
410	if (i > n)
411	    error (1, 0, "usage is restricted to members of the group %s",
412		   CVS_ADMIN_GROUP);
413#else
414	char *me = getcaller();
415	char **grnam;
416
417	for (grnam = grp->gr_mem; *grnam; grnam++)
418	    if (strcmp (*grnam, me) == 0) break;
419	if (!*grnam && getgid() != grp->gr_gid)
420	    error (1, 0, "usage is restricted to members of the group %s",
421		   CVS_ADMIN_GROUP);
422#endif
423    }
424#endif /* defined CVS_ADMIN_GROUP */
425
426    for (i = 0; i < admin_data.ac; ++i)
427    {
428	assert (admin_data.av[i][0] == '-');
429	switch (admin_data.av[i][1])
430	{
431	    case 'm':
432	    case 'l':
433	    case 'u':
434		check_numeric (&admin_data.av[i][2], argc, argv);
435		break;
436	    default:
437		break;
438	}
439    }
440    if (admin_data.branch != NULL)
441	check_numeric (admin_data.branch + 2, argc, argv);
442    if (admin_data.delete_revs != NULL)
443    {
444	char *p;
445
446	check_numeric (admin_data.delete_revs + 2, argc, argv);
447	p = strchr (admin_data.delete_revs + 2, ':');
448	if (p != NULL && isdigit ((unsigned char) p[1]))
449	    check_numeric (p + 1, argc, argv);
450	else if (p != NULL && p[1] == ':' && isdigit ((unsigned char) p[2]))
451	    check_numeric (p + 2, argc, argv);
452    }
453
454#ifdef CLIENT_SUPPORT
455    if (current_parsed_root->isremote)
456    {
457	/* We're the client side.  Fire up the remote server.  */
458	start_server ();
459
460	ign_setup ();
461
462	/* Note that option_with_arg does not work for us, because some
463	   of the options must be sent without a space between the option
464	   and its argument.  */
465	if (admin_data.interactive)
466	    error (1, 0, "-I option not useful with client/server");
467	if (admin_data.branch != NULL)
468	    send_arg (admin_data.branch);
469	if (admin_data.comment != NULL)
470	    send_arg (admin_data.comment);
471	if (admin_data.set_strict)
472	    send_arg ("-L");
473	if (admin_data.set_nonstrict)
474	    send_arg ("-U");
475	if (admin_data.delete_revs != NULL)
476	    send_arg (admin_data.delete_revs);
477	if (admin_data.desc != NULL)
478	{
479	    char *p = admin_data.desc;
480	    send_to_server ("Argument -t-", 0);
481	    while (*p)
482	    {
483		if (*p == '\n')
484		{
485		    send_to_server ("\012Argumentx ", 0);
486		    ++p;
487		}
488		else
489		{
490		    char *q = strchr (p, '\n');
491		    if (q == NULL) q = p + strlen (p);
492		    send_to_server (p, q - p);
493		    p = q;
494		}
495	    }
496	    send_to_server ("\012", 1);
497	}
498	/* Send this for all really_quiets since we know that it will be silently
499	 * ignored when unneeded.  This supports old servers.
500	 */
501	if (really_quiet)
502	    send_arg ("-q");
503	if (admin_data.kflag != NULL)
504	    send_arg (admin_data.kflag);
505
506	for (i = 0; i < admin_data.ac; ++i)
507	    send_arg (admin_data.av[i]);
508
509	send_arg ("--");
510	send_files (argc, argv, 0, 0, SEND_NO_CONTENTS);
511	send_file_names (argc, argv, SEND_EXPAND_WILD);
512	send_to_server ("admin\012", 0);
513        err = get_responses_and_close ();
514	goto return_it;
515    }
516#endif /* CLIENT_SUPPORT */
517
518    lock_tree_for_write (argc, argv, 0, W_LOCAL, 0);
519
520    err = start_recursion (admin_fileproc, (FILESDONEPROC) NULL, admin_dirproc,
521			   (DIRLEAVEPROC) NULL, (void *)&admin_data,
522			   argc, argv, 0,
523			   W_LOCAL, 0, CVS_LOCK_NONE, (char *) NULL, 1,
524			   (char *) NULL);
525    Lock_Cleanup ();
526
527 return_it:
528    if (admin_data.branch != NULL)
529	free (admin_data.branch);
530    if (admin_data.comment != NULL)
531	free (admin_data.comment);
532    if (admin_data.delete_revs != NULL)
533	free (admin_data.delete_revs);
534    if (admin_data.kflag != NULL)
535	free (admin_data.kflag);
536    if (admin_data.desc != NULL)
537	free (admin_data.desc);
538    for (i = 0; i < admin_data.ac; ++i)
539	free (admin_data.av[i]);
540    if (admin_data.av != NULL)
541	free (admin_data.av);
542
543    return (err);
544}
545
546/*
547 * Called to run "rcs" on a particular file.
548 */
549/* ARGSUSED */
550static int
551admin_fileproc (callerdat, finfo)
552    void *callerdat;
553    struct file_info *finfo;
554{
555    struct admin_data *admin_data = (struct admin_data *) callerdat;
556    Vers_TS *vers;
557    char *version;
558    int i;
559    int status = 0;
560    RCSNode *rcs, *rcs2;
561
562    vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0);
563
564    version = vers->vn_user;
565    if (version != NULL && strcmp (version, "0") == 0)
566    {
567	error (0, 0, "cannot admin newly added file `%s'", finfo->file);
568	status = 1;
569	goto exitfunc;
570    }
571
572    rcs = vers->srcfile;
573    if (rcs == NULL)
574    {
575	if (!really_quiet)
576	    error (0, 0, "nothing known about %s", finfo->file);
577	status = 1;
578	goto exitfunc;
579    }
580
581    if (rcs->flags & PARTIAL)
582	RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL);
583
584    if (!really_quiet)
585    {
586	cvs_output ("RCS file: ", 0);
587	cvs_output (rcs->path, 0);
588	cvs_output ("\n", 1);
589    }
590
591    if (admin_data->branch != NULL)
592    {
593	char *branch = &admin_data->branch[2];
594	if (*branch != '\0' && ! isdigit ((unsigned char) *branch))
595	{
596	    branch = RCS_whatbranch (rcs, admin_data->branch + 2);
597	    if (branch == NULL)
598	    {
599		error (0, 0, "%s: Symbolic name %s is undefined.",
600				rcs->path, admin_data->branch + 2);
601		status = 1;
602	    }
603	}
604	if (status == 0)
605	    RCS_setbranch (rcs, branch);
606	if (branch != NULL && branch != &admin_data->branch[2])
607	    free (branch);
608    }
609    if (admin_data->comment != NULL)
610    {
611	if (rcs->comment != NULL)
612	    free (rcs->comment);
613	rcs->comment = xstrdup (admin_data->comment + 2);
614    }
615    if (admin_data->set_strict)
616	rcs->strict_locks = 1;
617    if (admin_data->set_nonstrict)
618	rcs->strict_locks = 0;
619    if (admin_data->delete_revs != NULL)
620    {
621	char *s, *t, *rev1, *rev2;
622	/* Set for :, clear for ::.  */
623	int inclusive;
624	char *t2;
625
626	s = admin_data->delete_revs + 2;
627	inclusive = 1;
628	t = strchr (s, ':');
629	if (t != NULL)
630	{
631	    if (t[1] == ':')
632	    {
633		inclusive = 0;
634		t2 = t + 2;
635	    }
636	    else
637		t2 = t + 1;
638	}
639
640	/* Note that we don't support '-' for ranges.  RCS considers it
641	   obsolete and it is problematic with tags containing '-'.  "cvs log"
642	   has made the same decision.  */
643
644	if (t == NULL)
645	{
646	    /* -orev */
647	    rev1 = xstrdup (s);
648	    rev2 = xstrdup (s);
649	}
650	else if (t == s)
651	{
652	    /* -o:rev2 */
653	    rev1 = NULL;
654	    rev2 = xstrdup (t2);
655	}
656	else
657	{
658	    *t = '\0';
659	    rev1 = xstrdup (s);
660	    *t = ':';	/* probably unnecessary */
661	    if (*t2 == '\0')
662		/* -orev1: */
663		rev2 = NULL;
664	    else
665		/* -orev1:rev2 */
666		rev2 = xstrdup (t2);
667	}
668
669	if (rev1 == NULL && rev2 == NULL)
670	{
671	    /* RCS segfaults if `-o:' is given */
672	    error (0, 0, "no valid revisions specified in `%s' option",
673		   admin_data->delete_revs);
674	    status = 1;
675	}
676	else
677	{
678	    status |= RCS_delete_revs (rcs, rev1, rev2, inclusive);
679	    if (rev1)
680		free (rev1);
681	    if (rev2)
682		free (rev2);
683	}
684    }
685    if (admin_data->desc != NULL)
686    {
687	free (rcs->desc);
688	rcs->desc = xstrdup (admin_data->desc);
689    }
690    if (admin_data->kflag != NULL)
691    {
692	char *kflag = admin_data->kflag + 2;
693	char *oldexpand = RCS_getexpand (rcs);
694	if (oldexpand == NULL || strcmp (oldexpand, kflag) != 0)
695	    RCS_setexpand (rcs, kflag);
696    }
697
698    /* Handle miscellaneous options.  TODO: decide whether any or all
699       of these should have their own fields in the admin_data
700       structure. */
701    for (i = 0; i < admin_data->ac; ++i)
702    {
703	char *arg;
704	char *p, *rev, *revnum, *tag, *msg;
705	char **users;
706	int argc, u;
707	Node *n;
708	RCSVers *delta;
709
710	arg = admin_data->av[i];
711	switch (arg[1])
712	{
713	    case 'a': /* fall through */
714	    case 'e':
715	        line2argv (&argc, &users, arg + 2, " ,\t\n");
716		if (arg[1] == 'a')
717		    for (u = 0; u < argc; ++u)
718			RCS_addaccess (rcs, users[u]);
719		else if (argc == 0)
720		    RCS_delaccess (rcs, NULL);
721		else
722		    for (u = 0; u < argc; ++u)
723			RCS_delaccess (rcs, users[u]);
724		free_names (&argc, users);
725		break;
726	    case 'A':
727
728		/* See admin-19a-admin and friends in sanity.sh for
729		   relative pathnames.  It makes sense to think in
730		   terms of a syntax which give pathnames relative to
731		   the repository or repository corresponding to the
732		   current directory or some such (and perhaps don't
733		   include ,v), but trying to worry about such things
734		   is a little pointless unless you first worry about
735		   whether "cvs admin -A" as a whole makes any sense
736		   (currently probably not, as access lists don't
737		   affect the behavior of CVS).  */
738
739		rcs2 = RCS_parsercsfile (arg + 2);
740		if (rcs2 == NULL)
741		    error (1, 0, "cannot continue");
742
743		p = xstrdup (RCS_getaccess (rcs2));
744	        line2argv (&argc, &users, p, " \t\n");
745		free (p);
746		freercsnode (&rcs2);
747
748		for (u = 0; u < argc; ++u)
749		    RCS_addaccess (rcs, users[u]);
750		free_names (&argc, users);
751		break;
752	    case 'n': /* fall through */
753	    case 'N':
754		if (arg[2] == '\0')
755		{
756		    cvs_outerr ("missing symbolic name after ", 0);
757		    cvs_outerr (arg, 0);
758		    cvs_outerr ("\n", 1);
759		    break;
760		}
761		p = strchr (arg, ':');
762		if (p == NULL)
763		{
764		    if (RCS_deltag (rcs, arg + 2) != 0)
765		    {
766			error (0, 0, "%s: Symbolic name %s is undefined.",
767			       rcs->path,
768			       arg + 2);
769			status = 1;
770			continue;
771		    }
772		    break;
773		}
774		*p = '\0';
775		tag = xstrdup (arg + 2);
776		*p++ = ':';
777
778		/* Option `n' signals an error if this tag is already bound. */
779		if (arg[1] == 'n')
780		{
781		    n = findnode (RCS_symbols (rcs), tag);
782		    if (n != NULL)
783		    {
784			error (0, 0,
785			       "%s: symbolic name %s already bound to %s",
786			       rcs->path,
787			       tag, (char *)n->data);
788			status = 1;
789			free (tag);
790			continue;
791		    }
792		}
793
794                /* Attempt to perform the requested tagging.  */
795
796		if ((*p == 0 && (rev = RCS_head (rcs)))
797                    || (rev = RCS_tag2rev (rcs, p))) /* tag2rev may exit */
798		{
799		    RCS_check_tag (tag); /* exit if not a valid tag */
800		    RCS_settag (rcs, tag, rev);
801		    free (rev);
802		}
803                else
804		{
805		    if (!really_quiet)
806			error (0, 0,
807			       "%s: Symbolic name or revision %s is undefined.",
808			       rcs->path, p);
809		    status = 1;
810		}
811		free (tag);
812		break;
813	    case 's':
814	        p = strchr (arg, ':');
815		if (p == NULL)
816		{
817		    tag = xstrdup (arg + 2);
818		    rev = RCS_head (rcs);
819		}
820		else
821		{
822		    *p = '\0';
823		    tag = xstrdup (arg + 2);
824		    *p++ = ':';
825		    rev = xstrdup (p);
826		}
827		revnum = RCS_gettag (rcs, rev, 0, NULL);
828		if (revnum != NULL)
829		{
830		    n = findnode (rcs->versions, revnum);
831		    free (revnum);
832		}
833		else
834		    n = NULL;
835		if (n == NULL)
836		{
837		    error (0, 0,
838			   "%s: can't set state of nonexisting revision %s",
839			   rcs->path,
840			   rev);
841		    free (rev);
842		    status = 1;
843		    continue;
844		}
845		free (rev);
846		delta = n->data;
847		free (delta->state);
848		delta->state = tag;
849		break;
850
851	    case 'm':
852	        p = strchr (arg, ':');
853		if (p == NULL)
854		{
855		    error (0, 0, "%s: -m option lacks revision number",
856			   rcs->path);
857		    status = 1;
858		    continue;
859		}
860		*p = '\0';	/* temporarily make arg+2 its own string */
861		rev = RCS_gettag (rcs, arg + 2, 1, NULL); /* Force tag match */
862		if (rev == NULL)
863		{
864		    error (0, 0, "%s: no such revision %s", rcs->path, arg+2);
865		    status = 1;
866		    *p = ':';	/* restore the full text of the -m argument */
867		    continue;
868		}
869		msg = p+1;
870
871		n = findnode (rcs->versions, rev);
872		/* tags may exist against non-existing versions */
873		if (n == NULL)
874		{
875		     error (0, 0, "%s: no such revision %s: %s",
876			    rcs->path, arg+2, rev);
877		    status = 1;
878		    *p = ':';	/* restore the full text of the -m argument */
879		    free (rev);
880		    continue;
881		}
882		*p = ':';	/* restore the full text of the -m argument */
883		free (rev);
884
885		delta = n->data;
886		if (delta->text == NULL)
887		{
888		    delta->text = (Deltatext *) xmalloc (sizeof (Deltatext));
889		    memset ((void *) delta->text, 0, sizeof (Deltatext));
890		}
891		delta->text->version = xstrdup (delta->version);
892		delta->text->log = make_message_rcslegal (msg);
893		break;
894
895	    case 'l':
896	        status |= RCS_lock (rcs, arg[2] ? arg + 2 : NULL, 0);
897		break;
898	    case 'u':
899		status |= RCS_unlock (rcs, arg[2] ? arg + 2 : NULL, 0);
900		break;
901	    default: assert(0);	/* can't happen */
902	}
903    }
904
905    if (status == 0)
906    {
907	RCS_rewrite (rcs, NULL, NULL);
908	if (!really_quiet)
909	    cvs_output ("done\n", 5);
910    }
911    else
912    {
913	/* Note that this message should only occur after another
914	   message has given a more specific error.  The point of this
915	   additional message is to make it clear that the previous problems
916	   caused CVS to forget about the idea of modifying the RCS file.  */
917	if (!really_quiet)
918	    error (0, 0, "RCS file for `%s' not modified.", finfo->file);
919	RCS_abandon (rcs);
920    }
921
922  exitfunc:
923    freevers_ts (&vers);
924    return status;
925}
926
927/*
928 * Print a warm fuzzy message
929 */
930/* ARGSUSED */
931static Dtype
932admin_dirproc (callerdat, dir, repos, update_dir, entries)
933    void *callerdat;
934    const char *dir;
935    const char *repos;
936    const char *update_dir;
937    List *entries;
938{
939    if (!quiet)
940	error (0, 0, "Administrating %s", update_dir);
941    return (R_PROCESS);
942}
943