xref: /netbsd-src/external/gpl2/xcvs/dist/src/admin.c (revision 6a493d6bc668897c91594964a732d38505b70cbb)
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 /* cvsacl patch */
694 #ifdef SERVER_SUPPORT
695     if (use_cvs_acl /* && server_active */)
696     {
697 	if (!access_allowed (finfo->file, finfo->repository, NULL, 2,
698 			     NULL, NULL, 1))
699 	{
700 	    if (stop_at_first_permission_denied)
701 		error (1, 0, "permission denied for %s",
702 		       Short_Repository (finfo->repository));
703 	    else
704 		error (0, 0, "permission denied for %s/%s",
705 		       Short_Repository (finfo->repository), finfo->file);
706 
707 	    return (0);
708 	}
709     }
710 #endif
711 
712     version = vers->vn_user;
713     if (version != NULL && strcmp (version, "0") == 0)
714     {
715 	error (0, 0, "cannot admin newly added file `%s'", finfo->file);
716 	status = 1;
717 	goto exitfunc;
718     }
719 
720     history_write ('X', finfo->update_dir, admin_data->cmdline, finfo->file,
721 	finfo->repository);
722     rcs = vers->srcfile;
723     if (rcs == NULL)
724     {
725 	if (!really_quiet)
726 	    error (0, 0, "nothing known about %s", finfo->file);
727 	status = 1;
728 	goto exitfunc;
729     }
730 
731     if (rcs->flags & PARTIAL)
732 	RCS_reparsercsfile (rcs, NULL, NULL);
733 
734     if (!really_quiet)
735     {
736 	cvs_output ("RCS file: ", 0);
737 	cvs_output (rcs->path, 0);
738 	cvs_output ("\n", 1);
739     }
740 
741     if (admin_data->branch != NULL)
742     {
743 	char *branch = &admin_data->branch[2];
744 	if (*branch != '\0' && ! isdigit ((unsigned char) *branch))
745 	{
746 	    branch = RCS_whatbranch (rcs, admin_data->branch + 2);
747 	    if (branch == NULL)
748 	    {
749 		error (0, 0, "%s: Symbolic name %s is undefined.",
750 				rcs->path, admin_data->branch + 2);
751 		status = 1;
752 	    }
753 	}
754 	if (status == 0)
755 	    RCS_setbranch (rcs, branch);
756 	if (branch != NULL && branch != &admin_data->branch[2])
757 	    free (branch);
758     }
759     if (admin_data->comment != NULL)
760     {
761 	if (rcs->comment != NULL)
762 	    free (rcs->comment);
763 	rcs->comment = xstrdup (admin_data->comment + 2);
764     }
765     if (admin_data->set_strict)
766 	rcs->strict_locks = 1;
767     if (admin_data->set_nonstrict)
768 	rcs->strict_locks = 0;
769     if (admin_data->delete_revs != NULL)
770     {
771 	char *s, *t, *rev1, *rev2;
772 	/* Set for :, clear for ::.  */
773 	int inclusive;
774 	char *t2;
775 
776 	s = admin_data->delete_revs + 2;
777 	inclusive = 1;
778 	t = strchr (s, ':');
779 	if (t != NULL)
780 	{
781 	    if (t[1] == ':')
782 	    {
783 		inclusive = 0;
784 		t2 = t + 2;
785 	    }
786 	    else
787 		t2 = t + 1;
788 	}
789 
790 	/* Note that we don't support '-' for ranges.  RCS considers it
791 	   obsolete and it is problematic with tags containing '-'.  "cvs log"
792 	   has made the same decision.  */
793 
794 	if (t == NULL)
795 	{
796 	    /* -orev */
797 	    rev1 = xstrdup (s);
798 	    rev2 = xstrdup (s);
799 	}
800 	else if (t == s)
801 	{
802 	    /* -o:rev2 */
803 	    rev1 = NULL;
804 	    rev2 = xstrdup (t2);
805 	}
806 	else
807 	{
808 	    *t = '\0';
809 	    rev1 = xstrdup (s);
810 	    *t = ':';	/* probably unnecessary */
811 	    if (*t2 == '\0')
812 		/* -orev1: */
813 		rev2 = NULL;
814 	    else
815 		/* -orev1:rev2 */
816 		rev2 = xstrdup (t2);
817 	}
818 
819 	if (rev1 == NULL && rev2 == NULL)
820 	{
821 	    /* RCS segfaults if `-o:' is given */
822 	    error (0, 0, "no valid revisions specified in `%s' option",
823 		   admin_data->delete_revs);
824 	    status = 1;
825 	}
826 	else
827 	{
828 	    status |= RCS_delete_revs (rcs, rev1, rev2, inclusive);
829 	    if (rev1)
830 		free (rev1);
831 	    if (rev2)
832 		free (rev2);
833 	}
834     }
835     if (admin_data->desc != NULL)
836     {
837 	free (rcs->desc);
838 	rcs->desc = xstrdup (admin_data->desc);
839     }
840     if (admin_data->kflag != NULL)
841     {
842 	char *kflag = admin_data->kflag + 2;
843 	char *oldexpand = RCS_getexpand (rcs);
844 	if (oldexpand == NULL || strcmp (oldexpand, kflag) != 0)
845 	    RCS_setexpand (rcs, kflag);
846     }
847 
848     /* Handle miscellaneous options.  TODO: decide whether any or all
849        of these should have their own fields in the admin_data
850        structure. */
851     for (i = 0; i < admin_data->ac; ++i)
852     {
853 	char *arg;
854 	char *p, *rev, *revnum, *tag, *msg;
855 	char **users;
856 	int argc, u;
857 	Node *n;
858 	RCSVers *delta;
859 
860 	arg = admin_data->av[i];
861 	switch (arg[1])
862 	{
863 	    case 'a': /* fall through */
864 	    case 'e':
865 	        line2argv (&argc, &users, arg + 2, " ,\t\n");
866 		if (arg[1] == 'a')
867 		    for (u = 0; u < argc; ++u)
868 			RCS_addaccess (rcs, users[u]);
869 		else if (argc == 0)
870 		    RCS_delaccess (rcs, NULL);
871 		else
872 		    for (u = 0; u < argc; ++u)
873 			RCS_delaccess (rcs, users[u]);
874 		free_names (&argc, users);
875 		break;
876 	    case 'A':
877 
878 		/* See admin-19a-admin and friends in sanity.sh for
879 		   relative pathnames.  It makes sense to think in
880 		   terms of a syntax which give pathnames relative to
881 		   the repository or repository corresponding to the
882 		   current directory or some such (and perhaps don't
883 		   include ,v), but trying to worry about such things
884 		   is a little pointless unless you first worry about
885 		   whether "cvs admin -A" as a whole makes any sense
886 		   (currently probably not, as access lists don't
887 		   affect the behavior of CVS).  */
888 
889 		rcs2 = RCS_parsercsfile (arg + 2);
890 		if (rcs2 == NULL)
891 		    error (1, 0, "cannot continue");
892 
893 		p = xstrdup (RCS_getaccess (rcs2));
894 	        line2argv (&argc, &users, p, " \t\n");
895 		free (p);
896 		freercsnode (&rcs2);
897 
898 		for (u = 0; u < argc; ++u)
899 		    RCS_addaccess (rcs, users[u]);
900 		free_names (&argc, users);
901 		break;
902 	    case 'n': /* fall through */
903 	    case 'N':
904 		if (arg[2] == '\0')
905 		{
906 		    cvs_outerr ("missing symbolic name after ", 0);
907 		    cvs_outerr (arg, 0);
908 		    cvs_outerr ("\n", 1);
909 		    break;
910 		}
911 		p = strchr (arg, ':');
912 		if (p == NULL)
913 		{
914 		    if (RCS_deltag (rcs, arg + 2) != 0)
915 		    {
916 			error (0, 0, "%s: Symbolic name %s is undefined.",
917 			       rcs->path,
918 			       arg + 2);
919 			status = 1;
920 			continue;
921 		    }
922 		    break;
923 		}
924 		*p = '\0';
925 		tag = xstrdup (arg + 2);
926 		*p++ = ':';
927 
928 		/* Option `n' signals an error if this tag is already bound. */
929 		if (arg[1] == 'n')
930 		{
931 		    n = findnode (RCS_symbols (rcs), tag);
932 		    if (n != NULL)
933 		    {
934 			error (0, 0,
935 			       "%s: symbolic name %s already bound to %s",
936 			       rcs->path,
937 			       tag, (char *)n->data);
938 			status = 1;
939 			free (tag);
940 			continue;
941 		    }
942 		}
943 
944                 /* Attempt to perform the requested tagging.  */
945 
946 		if ((*p == 0 && (rev = RCS_head (rcs)))
947                     || (rev = RCS_tag2rev (rcs, p))) /* tag2rev may exit */
948 		{
949 		    RCS_check_tag (tag); /* exit if not a valid tag */
950 		    RCS_settag (rcs, tag, rev);
951 		    free (rev);
952 		}
953                 else
954 		{
955 		    if (!really_quiet)
956 			error (0, 0,
957 			       "%s: Symbolic name or revision %s is undefined.",
958 			       rcs->path, p);
959 		    status = 1;
960 		}
961 		free (tag);
962 		break;
963 	    case 's':
964 	        p = strchr (arg, ':');
965 		if (p == NULL)
966 		{
967 		    tag = xstrdup (arg + 2);
968 		    rev = RCS_head (rcs);
969 		    if (!rev)
970 		    {
971 			error (0, 0, "No head revision in archive file `%s'.",
972 			       rcs->path);
973 			status = 1;
974 			continue;
975 		    }
976 		}
977 		else
978 		{
979 		    *p = '\0';
980 		    tag = xstrdup (arg + 2);
981 		    *p++ = ':';
982 		    rev = xstrdup (p);
983 		}
984 		revnum = RCS_gettag (rcs, rev, 0, NULL);
985 		if (revnum != NULL)
986 		{
987 		    n = findnode (rcs->versions, revnum);
988 		    free (revnum);
989 		}
990 		else
991 		    n = NULL;
992 		if (n == NULL)
993 		{
994 		    error (0, 0,
995 			   "%s: can't set state of nonexisting revision %s",
996 			   rcs->path,
997 			   rev);
998 		    free (rev);
999 		    status = 1;
1000 		    continue;
1001 		}
1002 		free (rev);
1003 		delta = n->data;
1004 		free (delta->state);
1005 		delta->state = tag;
1006 		break;
1007 
1008 	    case 'm':
1009 	        p = strchr (arg, ':');
1010 		if (p == NULL)
1011 		{
1012 		    error (0, 0, "%s: -m option lacks revision number",
1013 			   rcs->path);
1014 		    status = 1;
1015 		    continue;
1016 		}
1017 		*p = '\0';	/* temporarily make arg+2 its own string */
1018 		rev = RCS_gettag (rcs, arg + 2, 1, NULL); /* Force tag match */
1019 		if (rev == NULL)
1020 		{
1021 		    error (0, 0, "%s: no such revision %s", rcs->path, arg+2);
1022 		    status = 1;
1023 		    *p = ':';	/* restore the full text of the -m argument */
1024 		    continue;
1025 		}
1026 		msg = p+1;
1027 
1028 		n = findnode (rcs->versions, rev);
1029 		/* tags may exist against non-existing versions */
1030 		if (n == NULL)
1031 		{
1032 		     error (0, 0, "%s: no such revision %s: %s",
1033 			    rcs->path, arg+2, rev);
1034 		    status = 1;
1035 		    *p = ':';	/* restore the full text of the -m argument */
1036 		    free (rev);
1037 		    continue;
1038 		}
1039 		*p = ':';	/* restore the full text of the -m argument */
1040 		free (rev);
1041 
1042 		delta = n->data;
1043 		if (delta->text == NULL)
1044 		{
1045 		    delta->text = xmalloc (sizeof (Deltatext));
1046 		    memset (delta->text, 0, sizeof (Deltatext));
1047 		}
1048 		delta->text->version = xstrdup (delta->version);
1049 		delta->text->log = make_message_rcsvalid (msg);
1050 		break;
1051 
1052 	    case 'l':
1053 	        status |= RCS_lock (rcs, arg[2] ? arg + 2 : NULL, 0);
1054 		break;
1055 	    case 'u':
1056 		status |= RCS_unlock (rcs, arg[2] ? arg + 2 : NULL, 0);
1057 		break;
1058 	    default: assert(0);	/* can't happen */
1059 	}
1060     }
1061 
1062     if (status == 0)
1063     {
1064 	RCS_rewrite (rcs, NULL, NULL);
1065 	if (!really_quiet)
1066 	    cvs_output ("done\n", 5);
1067     }
1068     else
1069     {
1070 	/* Note that this message should only occur after another
1071 	   message has given a more specific error.  The point of this
1072 	   additional message is to make it clear that the previous problems
1073 	   caused CVS to forget about the idea of modifying the RCS file.  */
1074 	if (!really_quiet)
1075 	    error (0, 0, "RCS file for `%s' not modified.", finfo->file);
1076 	RCS_abandon (rcs);
1077     }
1078 
1079   exitfunc:
1080     freevers_ts (&vers);
1081     return status;
1082 }
1083 
1084 
1085 
1086 /*
1087  * Print a warm fuzzy message
1088  */
1089 /* ARGSUSED */
1090 static Dtype
1091 admin_dirproc (void *callerdat, const char *dir, const char *repos,
1092                const char *update_dir, List *entries)
1093 {
1094     if (!quiet)
1095 	error (0, 0, "Administrating %s", update_dir);
1096     return R_PROCESS;
1097 }
1098