xref: /netbsd-src/external/gpl2/xcvs/dist/src/admin.c (revision 274254cdae52594c1aa480a736aef78313d15c9c)
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->UserAdminGroup == NULL)
268 	return 1;
269 
270     if ((grp = getgrnam(CVS_ADMIN_GROUP)) == 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 }
303 
304 int
305 admin (int argc, char **argv)
306 {
307     int err;
308     struct admin_data admin_data;
309     int c;
310     int i;
311     bool only_allowed_options;
312 
313     if (argc <= 1)
314 	usage (admin_usage);
315 
316     wrap_setup ();
317 
318     memset (&admin_data, 0, sizeof admin_data);
319     admin_data.cmdline = makecmdline (argc, argv);
320 
321     /* TODO: get rid of `-' switch notation in admin_data.  For
322        example, admin_data->branch should be not `-bfoo' but simply `foo'. */
323 
324     optind = 0;
325     only_allowed_options = true;
326     while ((c = getopt (argc, argv,
327 			"+ib::c:a:A:e::l::u::LUn:N:m:o:s:t::IqxV:k:")) != -1)
328     {
329 	if (c != 'q' && !strchr (config->UserAdminOptions, c))
330 	    only_allowed_options = false;
331 
332 	switch (c)
333 	{
334 	    case 'i':
335 		/* This has always been documented as useless in cvs.texinfo
336 		   and it really is--admin_fileproc silently does nothing
337 		   if vers->vn_user is NULL. */
338 		error (0, 0, "the -i option to admin is not supported");
339 		error (0, 0, "run add or import to create an RCS file");
340 		goto usage_error;
341 
342 	    case 'b':
343 		if (admin_data.branch != NULL)
344 		{
345 		    error (0, 0, "duplicate 'b' option");
346 		    goto usage_error;
347 		}
348 		if (optarg == NULL)
349 		    admin_data.branch = xstrdup ("-b");
350 		else
351 		    admin_data.branch = Xasprintf ("-b%s", optarg);
352 		break;
353 
354 	    case 'c':
355 		if (admin_data.comment != NULL)
356 		{
357 		    error (0, 0, "duplicate 'c' option");
358 		    goto usage_error;
359 		}
360 		admin_data.comment = Xasprintf ("-c%s", optarg);
361 		break;
362 
363 	    case 'a':
364 		arg_add (&admin_data, 'a', optarg);
365 		break;
366 
367 	    case 'A':
368 		/* In the client/server case, this is cheesy because
369 		   we just pass along the name of the RCS file, which
370 		   then will want to exist on the server.  This is
371 		   accidental; having the client specify a pathname on
372 		   the server is not a design feature of the protocol.  */
373 		arg_add (&admin_data, 'A', optarg);
374 		break;
375 
376 	    case 'e':
377 		arg_add (&admin_data, 'e', optarg);
378 		break;
379 
380 	    case 'l':
381 		/* Note that multiple -l options are valid.  */
382 		arg_add (&admin_data, 'l', optarg);
383 		break;
384 
385 	    case 'u':
386 		/* Note that multiple -u options are valid.  */
387 		arg_add (&admin_data, 'u', optarg);
388 		break;
389 
390 	    case 'L':
391 		/* Probably could also complain if -L is specified multiple
392 		   times, although RCS doesn't and I suppose it is reasonable
393 		   just to have it mean the same as a single -L.  */
394 		if (admin_data.set_nonstrict)
395 		{
396 		    error (0, 0, "-U and -L are incompatible");
397 		    goto usage_error;
398 		}
399 		admin_data.set_strict = 1;
400 		break;
401 
402 	    case 'U':
403 		/* Probably could also complain if -U is specified multiple
404 		   times, although RCS doesn't and I suppose it is reasonable
405 		   just to have it mean the same as a single -U.  */
406 		if (admin_data.set_strict)
407 		{
408 		    error (0, 0, "-U and -L are incompatible");
409 		    goto usage_error;
410 		}
411 		admin_data.set_nonstrict = 1;
412 		break;
413 
414 	    case 'n':
415 		/* Mostly similar to cvs tag.  Could also be parsing
416 		   the syntax of optarg, although for now we just pass
417 		   it to rcs as-is.  Note that multiple -n options are
418 		   valid.  */
419 		arg_add (&admin_data, 'n', optarg);
420 		break;
421 
422 	    case 'N':
423 		/* Mostly similar to cvs tag.  Could also be parsing
424 		   the syntax of optarg, although for now we just pass
425 		   it to rcs as-is.  Note that multiple -N options are
426 		   valid.  */
427 		arg_add (&admin_data, 'N', optarg);
428 		break;
429 
430 	    case 'm':
431 		/* Change log message.  Could also be parsing the syntax
432 		   of optarg, although for now we just pass it to rcs
433 		   as-is.  Note that multiple -m options are valid.  */
434 		arg_add (&admin_data, 'm', optarg);
435 		break;
436 
437 	    case 'o':
438 		/* Delete revisions.  Probably should also be parsing the
439 		   syntax of optarg, so that the client can give errors
440 		   rather than making the server take care of that.
441 		   Other than that I'm not sure whether it matters much
442 		   whether we parse it here or in admin_fileproc.
443 
444 		   Note that multiple -o options are invalid, in RCS
445 		   as well as here.  */
446 
447 		if (admin_data.delete_revs != NULL)
448 		{
449 		    error (0, 0, "duplicate '-o' option");
450 		    goto usage_error;
451 		}
452 		admin_data.delete_revs = Xasprintf ("-o%s", optarg);
453 		break;
454 
455 	    case 's':
456 		/* Note that multiple -s options are valid.  */
457 		arg_add (&admin_data, 's', optarg);
458 		break;
459 
460 	    case 't':
461 		if (admin_data.desc != NULL)
462 		{
463 		    error (0, 0, "duplicate 't' option");
464 		    goto usage_error;
465 		}
466 		if (optarg != NULL && optarg[0] == '-')
467 		    admin_data.desc = xstrdup (optarg + 1);
468 		else
469 		{
470 		    size_t bufsize = 0;
471 		    size_t len;
472 
473 		    get_file (optarg, optarg, "r", &admin_data.desc,
474 			      &bufsize, &len);
475 		}
476 		break;
477 
478 	    case 'I':
479 		/* At least in RCS this can be specified several times,
480 		   with the same meaning as being specified once.  */
481 		admin_data.interactive = 1;
482 		break;
483 
484 	    case 'q':
485 		/* Silently set the global really_quiet flag.  This keeps admin in
486 		 * sync with the RCS man page and allows us to silently support
487 		 * older servers when necessary.
488 		 *
489 		 * Some logic says we might want to output a deprecation warning
490 		 * here, but I'm opting not to in order to stay quietly in sync
491 		 * with the RCS man page.
492 		 */
493 		really_quiet = 1;
494 		break;
495 
496 	    case 'x':
497 		error (0, 0, "the -x option has never done anything useful");
498 		error (0, 0, "RCS files in CVS always end in ,v");
499 		goto usage_error;
500 
501 	    case 'V':
502 		/* No longer supported. */
503 		error (0, 0, "the `-V' option is obsolete");
504 		break;
505 
506 	    case 'k':
507 		if (admin_data.kflag != NULL)
508 		{
509 		    error (0, 0, "duplicate '-k' option");
510 		    goto usage_error;
511 		}
512 		admin_data.kflag = RCS_check_kflag (optarg);
513 		break;
514 	    default:
515 	    case '?':
516 		/* getopt will have printed an error message.  */
517 
518 	    usage_error:
519 		/* Don't use cvs_cmd_name; it might be "server".  */
520 	        error (1, 0, "specify %s -H admin for usage information",
521 		       program_name);
522 	}
523     }
524     argc -= optind;
525     argv += optind;
526 
527     /* The use of `cvs admin -k' is unrestricted.  However, any other
528        option is restricted if the group CVS_ADMIN_GROUP exists on the
529        server.  */
530     /* This is only "secure" on the server, since the user could edit the
531      * RCS file on a local host, but some people like this kind of
532      * check anyhow.  The alternative would be to check only when
533      * (server_active) rather than when not on the client.
534      */
535     if (!only_allowed_options && !admin_group_member())
536 	error (1, 0, "usage is restricted to members of the group %s",
537 	       CVS_ADMIN_GROUP);
538 
539     for (i = 0; i < admin_data.ac; ++i)
540     {
541 	assert (admin_data.av[i][0] == '-');
542 	switch (admin_data.av[i][1])
543 	{
544 	    case 'm':
545 	    case 'l':
546 	    case 'u':
547 		check_numeric (&admin_data.av[i][2], argc, argv);
548 		break;
549 	    default:
550 		break;
551 	}
552     }
553     if (admin_data.branch != NULL)
554 	check_numeric (admin_data.branch + 2, argc, argv);
555     if (admin_data.delete_revs != NULL)
556     {
557 	char *p;
558 
559 	check_numeric (admin_data.delete_revs + 2, argc, argv);
560 	p = strchr (admin_data.delete_revs + 2, ':');
561 	if (p != NULL && isdigit ((unsigned char) p[1]))
562 	    check_numeric (p + 1, argc, argv);
563 	else if (p != NULL && p[1] == ':' && isdigit ((unsigned char) p[2]))
564 	    check_numeric (p + 2, argc, argv);
565     }
566 
567 #ifdef CLIENT_SUPPORT
568     if (current_parsed_root->isremote)
569     {
570 	/* We're the client side.  Fire up the remote server.  */
571 	start_server ();
572 
573 	ign_setup ();
574 
575 	/* Note that option_with_arg does not work for us, because some
576 	   of the options must be sent without a space between the option
577 	   and its argument.  */
578 	if (admin_data.interactive)
579 	    error (1, 0, "-I option not useful with client/server");
580 	if (admin_data.branch != NULL)
581 	    send_arg (admin_data.branch);
582 	if (admin_data.comment != NULL)
583 	    send_arg (admin_data.comment);
584 	if (admin_data.set_strict)
585 	    send_arg ("-L");
586 	if (admin_data.set_nonstrict)
587 	    send_arg ("-U");
588 	if (admin_data.delete_revs != NULL)
589 	    send_arg (admin_data.delete_revs);
590 	if (admin_data.desc != NULL)
591 	{
592 	    char *p = admin_data.desc;
593 	    send_to_server ("Argument -t-", 0);
594 	    while (*p)
595 	    {
596 		if (*p == '\n')
597 		{
598 		    send_to_server ("\012Argumentx ", 0);
599 		    ++p;
600 		}
601 		else
602 		{
603 		    char *q = strchr (p, '\n');
604 		    if (q == NULL) q = p + strlen (p);
605 		    send_to_server (p, q - p);
606 		    p = q;
607 		}
608 	    }
609 	    send_to_server ("\012", 1);
610 	}
611 	/* Send this for all really_quiets since we know that it will be silently
612 	 * ignored when unneeded.  This supports old servers.
613 	 */
614 	if (really_quiet)
615 	    send_arg ("-q");
616 	if (admin_data.kflag != NULL)
617 	    send_arg (admin_data.kflag);
618 
619 	for (i = 0; i < admin_data.ac; ++i)
620 	    send_arg (admin_data.av[i]);
621 
622 	send_arg ("--");
623 	send_files (argc, argv, 0, 0, SEND_NO_CONTENTS);
624 	send_file_names (argc, argv, SEND_EXPAND_WILD);
625 	send_to_server ("admin\012", 0);
626         err = get_responses_and_close ();
627 	goto return_it;
628     }
629 #endif /* CLIENT_SUPPORT */
630 
631     lock_tree_promotably (argc, argv, 0, W_LOCAL, 0);
632 
633     err = start_recursion
634 	    (admin_fileproc, admin_filesdoneproc, admin_dirproc,
635 	     NULL, &admin_data,
636 	     argc, argv, 0,
637 	     W_LOCAL, 0, CVS_LOCK_WRITE, NULL, 1, NULL);
638 
639     Lock_Cleanup ();
640 
641 /* This just suppresses a warning from -Wall.  */
642 #ifdef CLIENT_SUPPORT
643  return_it:
644 #endif /* CLIENT_SUPPORT */
645     if (admin_data.cmdline != NULL)
646 	free (admin_data.cmdline);
647     if (admin_data.branch != NULL)
648 	free (admin_data.branch);
649     if (admin_data.comment != NULL)
650 	free (admin_data.comment);
651     if (admin_data.delete_revs != NULL)
652 	free (admin_data.delete_revs);
653     if (admin_data.kflag != NULL)
654 	free (admin_data.kflag);
655     if (admin_data.desc != NULL)
656 	free (admin_data.desc);
657     for (i = 0; i < admin_data.ac; ++i)
658 	free (admin_data.av[i]);
659     if (admin_data.av != NULL)
660 	free (admin_data.av);
661 
662     return err;
663 }
664 
665 
666 
667 /*
668  * Called to run "rcs" on a particular file.
669  */
670 /* ARGSUSED */
671 static int
672 admin_fileproc (void *callerdat, struct file_info *finfo)
673 {
674     struct admin_data *admin_data = (struct admin_data *) callerdat;
675     Vers_TS *vers;
676     char *version;
677     int i;
678     int status = 0;
679     RCSNode *rcs, *rcs2;
680 
681     vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0);
682 
683     version = vers->vn_user;
684     if (version != NULL && strcmp (version, "0") == 0)
685     {
686 	error (0, 0, "cannot admin newly added file `%s'", finfo->file);
687 	status = 1;
688 	goto exitfunc;
689     }
690 
691     history_write ('X', finfo->update_dir, admin_data->cmdline, finfo->file,
692 	finfo->repository);
693     rcs = vers->srcfile;
694     if (rcs == NULL)
695     {
696 	if (!really_quiet)
697 	    error (0, 0, "nothing known about %s", finfo->file);
698 	status = 1;
699 	goto exitfunc;
700     }
701 
702     if (rcs->flags & PARTIAL)
703 	RCS_reparsercsfile (rcs, NULL, NULL);
704 
705     if (!really_quiet)
706     {
707 	cvs_output ("RCS file: ", 0);
708 	cvs_output (rcs->path, 0);
709 	cvs_output ("\n", 1);
710     }
711 
712     if (admin_data->branch != NULL)
713     {
714 	char *branch = &admin_data->branch[2];
715 	if (*branch != '\0' && ! isdigit ((unsigned char) *branch))
716 	{
717 	    branch = RCS_whatbranch (rcs, admin_data->branch + 2);
718 	    if (branch == NULL)
719 	    {
720 		error (0, 0, "%s: Symbolic name %s is undefined.",
721 				rcs->path, admin_data->branch + 2);
722 		status = 1;
723 	    }
724 	}
725 	if (status == 0)
726 	    RCS_setbranch (rcs, branch);
727 	if (branch != NULL && branch != &admin_data->branch[2])
728 	    free (branch);
729     }
730     if (admin_data->comment != NULL)
731     {
732 	if (rcs->comment != NULL)
733 	    free (rcs->comment);
734 	rcs->comment = xstrdup (admin_data->comment + 2);
735     }
736     if (admin_data->set_strict)
737 	rcs->strict_locks = 1;
738     if (admin_data->set_nonstrict)
739 	rcs->strict_locks = 0;
740     if (admin_data->delete_revs != NULL)
741     {
742 	char *s, *t, *rev1, *rev2;
743 	/* Set for :, clear for ::.  */
744 	int inclusive;
745 	char *t2;
746 
747 	s = admin_data->delete_revs + 2;
748 	inclusive = 1;
749 	t = strchr (s, ':');
750 	if (t != NULL)
751 	{
752 	    if (t[1] == ':')
753 	    {
754 		inclusive = 0;
755 		t2 = t + 2;
756 	    }
757 	    else
758 		t2 = t + 1;
759 	}
760 
761 	/* Note that we don't support '-' for ranges.  RCS considers it
762 	   obsolete and it is problematic with tags containing '-'.  "cvs log"
763 	   has made the same decision.  */
764 
765 	if (t == NULL)
766 	{
767 	    /* -orev */
768 	    rev1 = xstrdup (s);
769 	    rev2 = xstrdup (s);
770 	}
771 	else if (t == s)
772 	{
773 	    /* -o:rev2 */
774 	    rev1 = NULL;
775 	    rev2 = xstrdup (t2);
776 	}
777 	else
778 	{
779 	    *t = '\0';
780 	    rev1 = xstrdup (s);
781 	    *t = ':';	/* probably unnecessary */
782 	    if (*t2 == '\0')
783 		/* -orev1: */
784 		rev2 = NULL;
785 	    else
786 		/* -orev1:rev2 */
787 		rev2 = xstrdup (t2);
788 	}
789 
790 	if (rev1 == NULL && rev2 == NULL)
791 	{
792 	    /* RCS segfaults if `-o:' is given */
793 	    error (0, 0, "no valid revisions specified in `%s' option",
794 		   admin_data->delete_revs);
795 	    status = 1;
796 	}
797 	else
798 	{
799 	    status |= RCS_delete_revs (rcs, rev1, rev2, inclusive);
800 	    if (rev1)
801 		free (rev1);
802 	    if (rev2)
803 		free (rev2);
804 	}
805     }
806     if (admin_data->desc != NULL)
807     {
808 	free (rcs->desc);
809 	rcs->desc = xstrdup (admin_data->desc);
810     }
811     if (admin_data->kflag != NULL)
812     {
813 	char *kflag = admin_data->kflag + 2;
814 	char *oldexpand = RCS_getexpand (rcs);
815 	if (oldexpand == NULL || strcmp (oldexpand, kflag) != 0)
816 	    RCS_setexpand (rcs, kflag);
817     }
818 
819     /* Handle miscellaneous options.  TODO: decide whether any or all
820        of these should have their own fields in the admin_data
821        structure. */
822     for (i = 0; i < admin_data->ac; ++i)
823     {
824 	char *arg;
825 	char *p, *rev, *revnum, *tag, *msg;
826 	char **users;
827 	int argc, u;
828 	Node *n;
829 	RCSVers *delta;
830 
831 	arg = admin_data->av[i];
832 	switch (arg[1])
833 	{
834 	    case 'a': /* fall through */
835 	    case 'e':
836 	        line2argv (&argc, &users, arg + 2, " ,\t\n");
837 		if (arg[1] == 'a')
838 		    for (u = 0; u < argc; ++u)
839 			RCS_addaccess (rcs, users[u]);
840 		else if (argc == 0)
841 		    RCS_delaccess (rcs, NULL);
842 		else
843 		    for (u = 0; u < argc; ++u)
844 			RCS_delaccess (rcs, users[u]);
845 		free_names (&argc, users);
846 		break;
847 	    case 'A':
848 
849 		/* See admin-19a-admin and friends in sanity.sh for
850 		   relative pathnames.  It makes sense to think in
851 		   terms of a syntax which give pathnames relative to
852 		   the repository or repository corresponding to the
853 		   current directory or some such (and perhaps don't
854 		   include ,v), but trying to worry about such things
855 		   is a little pointless unless you first worry about
856 		   whether "cvs admin -A" as a whole makes any sense
857 		   (currently probably not, as access lists don't
858 		   affect the behavior of CVS).  */
859 
860 		rcs2 = RCS_parsercsfile (arg + 2);
861 		if (rcs2 == NULL)
862 		    error (1, 0, "cannot continue");
863 
864 		p = xstrdup (RCS_getaccess (rcs2));
865 	        line2argv (&argc, &users, p, " \t\n");
866 		free (p);
867 		freercsnode (&rcs2);
868 
869 		for (u = 0; u < argc; ++u)
870 		    RCS_addaccess (rcs, users[u]);
871 		free_names (&argc, users);
872 		break;
873 	    case 'n': /* fall through */
874 	    case 'N':
875 		if (arg[2] == '\0')
876 		{
877 		    cvs_outerr ("missing symbolic name after ", 0);
878 		    cvs_outerr (arg, 0);
879 		    cvs_outerr ("\n", 1);
880 		    break;
881 		}
882 		p = strchr (arg, ':');
883 		if (p == NULL)
884 		{
885 		    if (RCS_deltag (rcs, arg + 2) != 0)
886 		    {
887 			error (0, 0, "%s: Symbolic name %s is undefined.",
888 			       rcs->path,
889 			       arg + 2);
890 			status = 1;
891 			continue;
892 		    }
893 		    break;
894 		}
895 		*p = '\0';
896 		tag = xstrdup (arg + 2);
897 		*p++ = ':';
898 
899 		/* Option `n' signals an error if this tag is already bound. */
900 		if (arg[1] == 'n')
901 		{
902 		    n = findnode (RCS_symbols (rcs), tag);
903 		    if (n != NULL)
904 		    {
905 			error (0, 0,
906 			       "%s: symbolic name %s already bound to %s",
907 			       rcs->path,
908 			       tag, (char *)n->data);
909 			status = 1;
910 			free (tag);
911 			continue;
912 		    }
913 		}
914 
915                 /* Attempt to perform the requested tagging.  */
916 
917 		if ((*p == 0 && (rev = RCS_head (rcs)))
918                     || (rev = RCS_tag2rev (rcs, p))) /* tag2rev may exit */
919 		{
920 		    RCS_check_tag (tag); /* exit if not a valid tag */
921 		    RCS_settag (rcs, tag, rev);
922 		    free (rev);
923 		}
924                 else
925 		{
926 		    if (!really_quiet)
927 			error (0, 0,
928 			       "%s: Symbolic name or revision %s is undefined.",
929 			       rcs->path, p);
930 		    status = 1;
931 		}
932 		free (tag);
933 		break;
934 	    case 's':
935 	        p = strchr (arg, ':');
936 		if (p == NULL)
937 		{
938 		    tag = xstrdup (arg + 2);
939 		    rev = RCS_head (rcs);
940 		    if (!rev)
941 		    {
942 			error (0, 0, "No head revision in archive file `%s'.",
943 			       rcs->path);
944 			status = 1;
945 			continue;
946 		    }
947 		}
948 		else
949 		{
950 		    *p = '\0';
951 		    tag = xstrdup (arg + 2);
952 		    *p++ = ':';
953 		    rev = xstrdup (p);
954 		}
955 		revnum = RCS_gettag (rcs, rev, 0, NULL);
956 		if (revnum != NULL)
957 		{
958 		    n = findnode (rcs->versions, revnum);
959 		    free (revnum);
960 		}
961 		else
962 		    n = NULL;
963 		if (n == NULL)
964 		{
965 		    error (0, 0,
966 			   "%s: can't set state of nonexisting revision %s",
967 			   rcs->path,
968 			   rev);
969 		    free (rev);
970 		    status = 1;
971 		    continue;
972 		}
973 		free (rev);
974 		delta = n->data;
975 		free (delta->state);
976 		delta->state = tag;
977 		break;
978 
979 	    case 'm':
980 	        p = strchr (arg, ':');
981 		if (p == NULL)
982 		{
983 		    error (0, 0, "%s: -m option lacks revision number",
984 			   rcs->path);
985 		    status = 1;
986 		    continue;
987 		}
988 		*p = '\0';	/* temporarily make arg+2 its own string */
989 		rev = RCS_gettag (rcs, arg + 2, 1, NULL); /* Force tag match */
990 		if (rev == NULL)
991 		{
992 		    error (0, 0, "%s: no such revision %s", rcs->path, arg+2);
993 		    status = 1;
994 		    *p = ':';	/* restore the full text of the -m argument */
995 		    continue;
996 		}
997 		msg = p+1;
998 
999 		n = findnode (rcs->versions, rev);
1000 		/* tags may exist against non-existing versions */
1001 		if (n == NULL)
1002 		{
1003 		     error (0, 0, "%s: no such revision %s: %s",
1004 			    rcs->path, arg+2, rev);
1005 		    status = 1;
1006 		    *p = ':';	/* restore the full text of the -m argument */
1007 		    free (rev);
1008 		    continue;
1009 		}
1010 		*p = ':';	/* restore the full text of the -m argument */
1011 		free (rev);
1012 
1013 		delta = n->data;
1014 		if (delta->text == NULL)
1015 		{
1016 		    delta->text = xmalloc (sizeof (Deltatext));
1017 		    memset (delta->text, 0, sizeof (Deltatext));
1018 		}
1019 		delta->text->version = xstrdup (delta->version);
1020 		delta->text->log = make_message_rcsvalid (msg);
1021 		break;
1022 
1023 	    case 'l':
1024 	        status |= RCS_lock (rcs, arg[2] ? arg + 2 : NULL, 0);
1025 		break;
1026 	    case 'u':
1027 		status |= RCS_unlock (rcs, arg[2] ? arg + 2 : NULL, 0);
1028 		break;
1029 	    default: assert(0);	/* can't happen */
1030 	}
1031     }
1032 
1033     if (status == 0)
1034     {
1035 	RCS_rewrite (rcs, NULL, NULL);
1036 	if (!really_quiet)
1037 	    cvs_output ("done\n", 5);
1038     }
1039     else
1040     {
1041 	/* Note that this message should only occur after another
1042 	   message has given a more specific error.  The point of this
1043 	   additional message is to make it clear that the previous problems
1044 	   caused CVS to forget about the idea of modifying the RCS file.  */
1045 	if (!really_quiet)
1046 	    error (0, 0, "RCS file for `%s' not modified.", finfo->file);
1047 	RCS_abandon (rcs);
1048     }
1049 
1050   exitfunc:
1051     freevers_ts (&vers);
1052     return status;
1053 }
1054 
1055 
1056 
1057 /*
1058  * Print a warm fuzzy message
1059  */
1060 /* ARGSUSED */
1061 static Dtype
1062 admin_dirproc (void *callerdat, const char *dir, const char *repos,
1063                const char *update_dir, List *entries)
1064 {
1065     if (!quiet)
1066 	error (0, 0, "Administrating %s", update_dir);
1067     return R_PROCESS;
1068 }
1069