xref: /openbsd/gnu/usr.bin/cvs/src/admin.c (revision 404b540a)
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 
18 static Dtype admin_dirproc PROTO ((void *callerdat, char *dir,
19 				   char *repos, char *update_dir,
20 				   List *entries));
21 static int admin_fileproc PROTO ((void *callerdat, struct file_info *finfo));
22 
23 static 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) Substitue keyword and value.\n",
35     "\t   kvl  Substitue keyword, value, and locker (if any).\n",
36     "\t   k    Substitue keyword only.\n",
37     "\t   o    Preserve original string.\n",
38     "\t   b    Like o, but mark file as binary.\n",
39     "\t   v    Substitue 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.  */
69 struct 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).  */
114 static void
115 arg_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 
142 int
143 admin (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')
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 */
538 static int
539 admin_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 == NULL)
563     {
564 	error (0, 0, "lost revision control file for `%s'", finfo->file);
565 	goto exitfunc;
566     }
567     if (rcs->flags & PARTIAL)
568 	RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL);
569 
570     status = 0;
571 
572     if (!really_quiet)
573     {
574 	cvs_output ("RCS file: ", 0);
575 	cvs_output (rcs->path, 0);
576 	cvs_output ("\n", 1);
577     }
578 
579     if (admin_data->branch != NULL)
580     {
581 	char *branch = &admin_data->branch[2];
582 	if (*branch != '\0' && ! isdigit ((unsigned char) *branch))
583 	{
584 	    branch = RCS_whatbranch (rcs, admin_data->branch + 2);
585 	    if (branch == NULL)
586 	    {
587 		error (0, 0, "%s: Symbolic name %s is undefined.",
588 				rcs->path, admin_data->branch + 2);
589 		status = 1;
590 	    }
591 	}
592 	if (status == 0)
593 	    RCS_setbranch (rcs, branch);
594 	if (branch != NULL && branch != &admin_data->branch[2])
595 	    free (branch);
596     }
597     if (admin_data->comment != NULL)
598     {
599 	if (rcs->comment != NULL)
600 	    free (rcs->comment);
601 	rcs->comment = xstrdup (admin_data->comment + 2);
602     }
603     if (admin_data->set_strict)
604 	rcs->strict_locks = 1;
605     if (admin_data->set_nonstrict)
606 	rcs->strict_locks = 0;
607     if (admin_data->delete_revs != NULL)
608     {
609 	char *s, *t, *rev1, *rev2;
610 	/* Set for :, clear for ::.  */
611 	int inclusive;
612 	char *t2;
613 
614 	s = admin_data->delete_revs + 2;
615 	inclusive = 1;
616 	t = strchr (s, ':');
617 	if (t != NULL)
618 	{
619 	    if (t[1] == ':')
620 	    {
621 		inclusive = 0;
622 		t2 = t + 2;
623 	    }
624 	    else
625 		t2 = t + 1;
626 	}
627 
628 	/* Note that we don't support '-' for ranges.  RCS considers it
629 	   obsolete and it is problematic with tags containing '-'.  "cvs log"
630 	   has made the same decision.  */
631 
632 	if (t == NULL)
633 	{
634 	    /* -orev */
635 	    rev1 = xstrdup (s);
636 	    rev2 = xstrdup (s);
637 	}
638 	else if (t == s)
639 	{
640 	    /* -o:rev2 */
641 	    rev1 = NULL;
642 	    rev2 = xstrdup (t2);
643 	}
644 	else
645 	{
646 	    *t = '\0';
647 	    rev1 = xstrdup (s);
648 	    *t = ':';	/* probably unnecessary */
649 	    if (*t2 == '\0')
650 		/* -orev1: */
651 		rev2 = NULL;
652 	    else
653 		/* -orev1:rev2 */
654 		rev2 = xstrdup (t2);
655 	}
656 
657 	if (rev1 == NULL && rev2 == NULL)
658 	{
659 	    /* RCS segfaults if `-o:' is given */
660 	    error (0, 0, "no valid revisions specified in `%s' option",
661 		   admin_data->delete_revs);
662 	    status = 1;
663 	}
664 	else
665 	{
666 	    status |= RCS_delete_revs (rcs, rev1, rev2, inclusive);
667 	    if (rev1)
668 		free (rev1);
669 	    if (rev2)
670 		free (rev2);
671 	}
672     }
673     if (admin_data->desc != NULL)
674     {
675 	free (rcs->desc);
676 	rcs->desc = xstrdup (admin_data->desc);
677     }
678     if (admin_data->kflag != NULL)
679     {
680 	char *kflag = admin_data->kflag + 2;
681 	char *oldexpand = RCS_getexpand (rcs);
682 	if (oldexpand == NULL || strcmp (oldexpand, kflag) != 0)
683 	    RCS_setexpand (rcs, kflag);
684     }
685 
686     /* Handle miscellaneous options.  TODO: decide whether any or all
687        of these should have their own fields in the admin_data
688        structure. */
689     for (i = 0; i < admin_data->ac; ++i)
690     {
691 	char *arg;
692 	char *p, *rev, *revnum, *tag, *msg;
693 	char **users;
694 	int argc, u;
695 	Node *n;
696 	RCSVers *delta;
697 
698 	arg = admin_data->av[i];
699 	switch (arg[1])
700 	{
701 	    case 'a': /* fall through */
702 	    case 'e':
703 	        line2argv (&argc, &users, arg + 2, " ,\t\n");
704 		if (arg[1] == 'a')
705 		    for (u = 0; u < argc; ++u)
706 			RCS_addaccess (rcs, users[u]);
707 		else if (argc == 0)
708 		    RCS_delaccess (rcs, NULL);
709 		else
710 		    for (u = 0; u < argc; ++u)
711 			RCS_delaccess (rcs, users[u]);
712 		free_names (&argc, users);
713 		break;
714 	    case 'A':
715 
716 		/* See admin-19a-admin and friends in sanity.sh for
717 		   relative pathnames.  It makes sense to think in
718 		   terms of a syntax which give pathnames relative to
719 		   the repository or repository corresponding to the
720 		   current directory or some such (and perhaps don't
721 		   include ,v), but trying to worry about such things
722 		   is a little pointless unless you first worry about
723 		   whether "cvs admin -A" as a whole makes any sense
724 		   (currently probably not, as access lists don't
725 		   affect the behavior of CVS).  */
726 
727 		rcs2 = RCS_parsercsfile (arg + 2);
728 		if (rcs2 == NULL)
729 		    error (1, 0, "cannot continue");
730 
731 		p = xstrdup (RCS_getaccess (rcs2));
732 	        line2argv (&argc, &users, p, " \t\n");
733 		free (p);
734 		freercsnode (&rcs2);
735 
736 		for (u = 0; u < argc; ++u)
737 		    RCS_addaccess (rcs, users[u]);
738 		free_names (&argc, users);
739 		break;
740 	    case 'n': /* fall through */
741 	    case 'N':
742 		if (arg[2] == '\0')
743 		{
744 		    cvs_outerr ("missing symbolic name after ", 0);
745 		    cvs_outerr (arg, 0);
746 		    cvs_outerr ("\n", 1);
747 		    break;
748 		}
749 		p = strchr (arg, ':');
750 		if (p == NULL)
751 		{
752 		    if (RCS_deltag (rcs, arg + 2) != 0)
753 		    {
754 			error (0, 0, "%s: Symbolic name %s is undefined.",
755 			       rcs->path,
756 			       arg + 2);
757 			status = 1;
758 			continue;
759 		    }
760 		    break;
761 		}
762 		*p = '\0';
763 		tag = xstrdup (arg + 2);
764 		*p++ = ':';
765 
766 		/* Option `n' signals an error if this tag is already bound. */
767 		if (arg[1] == 'n')
768 		{
769 		    n = findnode (RCS_symbols (rcs), tag);
770 		    if (n != NULL)
771 		    {
772 			error (0, 0,
773 			       "%s: symbolic name %s already bound to %s",
774 			       rcs->path,
775 			       tag, n->data);
776 			status = 1;
777 			free (tag);
778 			continue;
779 		    }
780 		}
781 
782                 /* Attempt to perform the requested tagging.  */
783 
784 		if ((*p == 0 && (rev = RCS_head (rcs)))
785                     || (rev = RCS_tag2rev (rcs, p))) /* tag2rev may exit */
786 		{
787 		    RCS_check_tag (tag); /* exit if not a valid tag */
788 		    RCS_settag (rcs, tag, rev);
789 		    free (rev);
790 		}
791                 else
792 		{
793 		    if (!really_quiet)
794 			error (0, 0,
795 			       "%s: Symbolic name or revision %s is undefined.",
796 			       rcs->path, p);
797 		    status = 1;
798 		}
799 		free (tag);
800 		break;
801 	    case 's':
802 	        p = strchr (arg, ':');
803 		if (p == NULL)
804 		{
805 		    tag = xstrdup (arg + 2);
806 		    rev = RCS_head (rcs);
807 		}
808 		else
809 		{
810 		    *p = '\0';
811 		    tag = xstrdup (arg + 2);
812 		    *p++ = ':';
813 		    rev = xstrdup (p);
814 		}
815 		revnum = RCS_gettag (rcs, rev, 0, NULL);
816 		if (revnum != NULL)
817 		{
818 		    n = findnode (rcs->versions, revnum);
819 		    free (revnum);
820 		}
821 		else
822 		    n = NULL;
823 		if (n == NULL)
824 		{
825 		    error (0, 0,
826 			   "%s: can't set state of nonexisting revision %s",
827 			   rcs->path,
828 			   rev);
829 		    free (rev);
830 		    status = 1;
831 		    continue;
832 		}
833 		free (rev);
834 		delta = (RCSVers *) n->data;
835 		free (delta->state);
836 		delta->state = tag;
837 		break;
838 
839 	    case 'm':
840 	        p = strchr (arg, ':');
841 		if (p == NULL)
842 		{
843 		    error (0, 0, "%s: -m option lacks revision number",
844 			   rcs->path);
845 		    status = 1;
846 		    continue;
847 		}
848 		*p = '\0';
849 		rev = RCS_gettag (rcs, arg + 2, 0, NULL);
850 		if (rev == NULL)
851 		{
852 		    error (0, 0, "%s: no such revision %s", rcs->path, rev);
853 		    status = 1;
854 		    continue;
855 		}
856 		*p++ = ':';
857 		msg = p;
858 
859 		n = findnode (rcs->versions, rev);
860 		free (rev);
861 		delta = (RCSVers *) n->data;
862 		if (delta->text == NULL)
863 		{
864 		    delta->text = (Deltatext *) xmalloc (sizeof (Deltatext));
865 		    memset ((void *) delta->text, 0, sizeof (Deltatext));
866 		}
867 		delta->text->version = xstrdup (delta->version);
868 		delta->text->log = make_message_rcslegal (msg);
869 		break;
870 
871 	    case 'l':
872 	        status |= RCS_lock (rcs, arg[2] ? arg + 2 : NULL, 0);
873 		break;
874 	    case 'u':
875 		status |= RCS_unlock (rcs, arg[2] ? arg + 2 : NULL, 0);
876 		break;
877 	    default: assert(0);	/* can't happen */
878 	}
879     }
880 
881     if (status == 0)
882     {
883 	RCS_rewrite (rcs, NULL, NULL);
884 	if (!really_quiet)
885 	    cvs_output ("done\n", 5);
886     }
887     else
888     {
889 	/* Note that this message should only occur after another
890 	   message has given a more specific error.  The point of this
891 	   additional message is to make it clear that the previous problems
892 	   caused CVS to forget about the idea of modifying the RCS file.  */
893 	if (!really_quiet)
894 	    error (0, 0, "RCS file for `%s' not modified.", finfo->file);
895 	RCS_abandon (rcs);
896     }
897 
898   exitfunc:
899     freevers_ts (&vers);
900     return status;
901 }
902 
903 /*
904  * Print a warm fuzzy message
905  */
906 /* ARGSUSED */
907 static Dtype
908 admin_dirproc (callerdat, dir, repos, update_dir, entries)
909     void *callerdat;
910     char *dir;
911     char *repos;
912     char *update_dir;
913     List *entries;
914 {
915     if (!quiet)
916 	error (0, 0, "Administrating %s", update_dir);
917     return (R_PROCESS);
918 }
919