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