xref: /openbsd-src/gnu/usr.bin/cvs/src/admin.c (revision 62a742911104f98b9185b2c6b6007d9b1c36396c)
1 /*
2  * Copyright (c) 1992, Brian Berliner and Jeff Polk
3  * Copyright (c) 1989-1992, Brian Berliner
4  *
5  * You may distribute under the terms of the GNU General Public License as
6  * specified in the README file that comes with the CVS source distribution.
7  *
8  * Administration ("cvs admin")
9  *
10  */
11 
12 #include "cvs.h"
13 #ifdef CVS_ADMIN_GROUP
14 #include <grp.h>
15 #endif
16 #include <assert.h>
17 
18 static Dtype admin_dirproc PROTO ((void *callerdat, char *dir,
19 				   char *repos, char *update_dir,
20 				   List *entries));
21 static int admin_fileproc PROTO ((void *callerdat, struct file_info *finfo));
22 
23 static const char *const admin_usage[] =
24 {
25     "Usage: %s %s rcs-options files...\n",
26     "(Specify the --help global option for a list of other help options)\n",
27     NULL
28 };
29 
30 /* This structure is used to pass information through start_recursion.  */
31 struct admin_data
32 {
33     /* Set default branch (-b).  It is "-b" followed by the value
34        given, or NULL if not specified, or merely "-b" if -b is
35        specified without a value.  */
36     char *branch;
37 
38     /* Set comment leader (-c).  It is "-c" followed by the value
39        given, or NULL if not specified.  The comment leader is
40        relevant only for old versions of RCS, but we let people set it
41        anyway.  */
42     char *comment;
43 
44     /* Set strict locking (-L).  */
45     int set_strict;
46 
47     /* Set nonstrict locking (-U).  */
48     int set_nonstrict;
49 
50     /* Delete revisions (-o).  It is "-o" followed by the value specified.  */
51     char *delete_revs;
52 
53     /* Keyword substitution mode (-k), e.g. "-kb".  */
54     char *kflag;
55 
56     /* Description (-t).  See sanity.sh for various moanings about
57        files and stdin and such.  "" if -t specified without an
58        argument.  It is "-t" followed by the argument.  */
59     char *desc;
60 
61     /* Interactive (-I).  Problematic with client/server.  */
62     int interactive;
63 
64     /* Quiet (-q).  Not the same as the global -q option, which is a bit
65        on the confusing side, perhaps.  */
66     int quiet;
67 
68     /* This is the cheesy part.  It is a vector with the options which
69        we don't deal with above (e.g. "-afoo" "-abar,baz").  In the future
70        this presumably will be replaced by other variables which break
71        out the data in a more convenient fashion.  AV as well as each of
72        the strings it points to is malloc'd.  */
73     int ac;
74     char **av;
75     int av_alloc;
76 };
77 
78 /* Add an argument.  OPT is the option letter, e.g. 'a'.  ARG is the
79    argument to that option, or NULL if omitted (whether NULL can actually
80    happen depends on whether the option was specified as optional to
81    getopt).  */
82 static void
83 arg_add (dat, opt, arg)
84     struct admin_data *dat;
85     int opt;
86     char *arg;
87 {
88     char *newelt = xmalloc ((arg == NULL ? 0 : strlen (arg)) + 3);
89     strcpy (newelt, "-");
90     newelt[1] = opt;
91     if (arg == NULL)
92 	newelt[2] = '\0';
93     else
94 	strcpy (newelt + 2, arg);
95 
96     if (dat->av_alloc == 0)
97     {
98 	dat->av_alloc = 1;
99 	dat->av = (char **) xmalloc (dat->av_alloc * sizeof (*dat->av));
100     }
101     else if (dat->ac >= dat->av_alloc)
102     {
103 	dat->av_alloc *= 2;
104 	dat->av = (char **) xrealloc (dat->av,
105 				      dat->av_alloc * sizeof (*dat->av));
106     }
107     dat->av[dat->ac++] = newelt;
108 }
109 
110 int
111 admin (argc, argv)
112     int argc;
113     char **argv;
114 {
115     int err;
116 #ifdef CVS_ADMIN_GROUP
117     struct group *grp;
118     struct group *getgrnam();
119 #endif
120     struct admin_data admin_data;
121     int c;
122     int i;
123 
124     if (argc <= 1)
125 	usage (admin_usage);
126 
127 #ifdef CVS_ADMIN_GROUP
128     grp = getgrnam(CVS_ADMIN_GROUP);
129      /* skip usage right check if group CVS_ADMIN_GROUP does not exist */
130     if (grp != NULL)
131     {
132 	char *me = getcaller();
133 	char **grnam = grp->gr_mem;
134 	int denied = 1;
135 
136 	while (*grnam)
137 	{
138 	    if (strcmp(*grnam, me) == 0)
139 	    {
140 		denied = 0;
141 		break;
142 	    }
143 	    grnam++;
144 	}
145 
146 	if (denied)
147 	    error (1, 0, "usage is restricted to members of the group %s",
148 		   CVS_ADMIN_GROUP);
149     }
150 #endif
151 
152     wrap_setup ();
153 
154     memset (&admin_data, 0, sizeof admin_data);
155 
156     /* TODO: get rid of `-' switch notation in admin_data.  For
157        example, admin_data->branch should be not `-bfoo' but simply `foo'. */
158 
159     optind = 0;
160     while ((c = getopt (argc, argv,
161 			"+ib::c:a:A:e:l::u::LUn:N:m:o:s:t::IqxV:k:")) != -1)
162     {
163 	switch (c)
164 	{
165 	    case 'i':
166 		/* This has always been documented as useless in cvs.texinfo
167 		   and it really is--admin_fileproc silently does nothing
168 		   if vers->vn_user is NULL. */
169 		error (0, 0, "the -i option to admin is not supported");
170 		error (0, 0, "run add or import to create an RCS file");
171 		goto usage_error;
172 
173 	    case 'b':
174 		if (admin_data.branch != NULL)
175 		{
176 		    error (0, 0, "duplicate 'b' option");
177 		    goto usage_error;
178 		}
179 		if (optarg == NULL)
180 		    admin_data.branch = xstrdup ("-b");
181 		else
182 		{
183 		    admin_data.branch = xmalloc (strlen (optarg) + 5);
184 		    strcpy (admin_data.branch, "-b");
185 		    strcat (admin_data.branch, optarg);
186 		}
187 		break;
188 
189 	    case 'c':
190 		if (admin_data.comment != NULL)
191 		{
192 		    error (0, 0, "duplicate 'c' option");
193 		    goto usage_error;
194 		}
195 		admin_data.comment = xmalloc (strlen (optarg) + 5);
196 		strcpy (admin_data.comment, "-c");
197 		strcat (admin_data.comment, optarg);
198 		break;
199 
200 	    case 'a':
201 		arg_add (&admin_data, 'a', optarg);
202 		break;
203 
204 	    case 'A':
205 		/* In the client/server case, this is cheesy because
206 		   we just pass along the name of the RCS file, which
207 		   then will want to exist on the server.  This is
208 		   accidental; having the client specify a pathname on
209 		   the server is not a design feature of the protocol.  */
210 		arg_add (&admin_data, 'A', optarg);
211 		break;
212 
213 	    case 'e':
214 		arg_add (&admin_data, 'e', optarg);
215 		break;
216 
217 	    case 'l':
218 		/* Note that multiple -l options are legal.  */
219 		arg_add (&admin_data, 'l', optarg);
220 		break;
221 
222 	    case 'u':
223 		/* Note that multiple -u options are legal.  */
224 		arg_add (&admin_data, 'u', optarg);
225 		break;
226 
227 	    case 'L':
228 		/* Probably could also complain if -L is specified multiple
229 		   times, although RCS doesn't and I suppose it is reasonable
230 		   just to have it mean the same as a single -L.  */
231 		if (admin_data.set_nonstrict)
232 		{
233 		    error (0, 0, "-U and -L are incompatible");
234 		    goto usage_error;
235 		}
236 		admin_data.set_strict = 1;
237 		break;
238 
239 	    case 'U':
240 		/* Probably could also complain if -U is specified multiple
241 		   times, although RCS doesn't and I suppose it is reasonable
242 		   just to have it mean the same as a single -U.  */
243 		if (admin_data.set_strict)
244 		{
245 		    error (0, 0, "-U and -L are incompatible");
246 		    goto usage_error;
247 		}
248 		admin_data.set_nonstrict = 1;
249 		break;
250 
251 	    case 'n':
252 		/* Mostly similar to cvs tag.  Could also be parsing
253 		   the syntax of optarg, although for now we just pass
254 		   it to rcs as-is.  Note that multiple -n options are
255 		   legal.  */
256 		arg_add (&admin_data, 'n', optarg);
257 		break;
258 
259 	    case 'N':
260 		/* Mostly similar to cvs tag.  Could also be parsing
261 		   the syntax of optarg, although for now we just pass
262 		   it to rcs as-is.  Note that multiple -N options are
263 		   legal.  */
264 		arg_add (&admin_data, 'N', optarg);
265 		break;
266 
267 	    case 'm':
268 		/* Change log message.  Could also be parsing the syntax
269 		   of optarg, although for now we just pass it to rcs
270 		   as-is.  Note that multiple -m options are legal.  */
271 		arg_add (&admin_data, 'm', optarg);
272 		break;
273 
274 	    case 'o':
275 		/* Delete revisions.  Probably should also be parsing the
276 		   syntax of optarg, so that the client can give errors
277 		   rather than making the server take care of that.
278 		   Other than that I'm not sure whether it matters much
279 		   whether we parse it here or in admin_fileproc.
280 
281 		   Note that multiple -o options are illegal, in RCS
282 		   as well as here.  */
283 
284 		if (admin_data.delete_revs != NULL)
285 		{
286 		    error (0, 0, "duplicate '-o' option");
287 		    goto usage_error;
288 		}
289 		admin_data.delete_revs = xmalloc (strlen (optarg) + 5);
290 		strcpy (admin_data.delete_revs, "-o");
291 		strcat (admin_data.delete_revs, optarg);
292 		break;
293 
294 	    case 's':
295 		/* Note that multiple -s options are legal.  */
296 		arg_add (&admin_data, 's', optarg);
297 		break;
298 
299 	    case 't':
300 		if (admin_data.desc != NULL)
301 		{
302 		    error (0, 0, "duplicate 't' option");
303 		    goto usage_error;
304 		}
305 		if (optarg == NULL)
306 		    admin_data.desc = xstrdup ("-t");
307 		else
308 		{
309 		    admin_data.desc = xmalloc (strlen (optarg) + 5);
310 		    strcpy (admin_data.desc, "-t");
311 		    strcat (admin_data.desc, optarg);
312 		}
313 		break;
314 
315 	    case 'I':
316 		/* At least in RCS this can be specified several times,
317 		   with the same meaning as being specified once.  */
318 		admin_data.interactive = 1;
319 		break;
320 
321 	    case 'q':
322 		admin_data.quiet = 1;
323 		break;
324 
325 	    case 'x':
326 		error (0, 0, "the -x option has never done anything useful");
327 		error (0, 0, "RCS files in CVS always end in ,v");
328 		goto usage_error;
329 
330 	    case 'V':
331 		/* No longer supported. */
332 		error (0, 0, "the `-V' option is obsolete");
333 		break;
334 
335 	    case 'k':
336 		if (admin_data.kflag != NULL)
337 		{
338 		    error (0, 0, "duplicate '-k' option");
339 		    goto usage_error;
340 		}
341 		admin_data.kflag = RCS_check_kflag (optarg);
342 		break;
343 	    default:
344 	    case '?':
345 		/* getopt will have printed an error message.  */
346 
347 	    usage_error:
348 		/* Don't use command_name; it might be "server".  */
349 	        error (1, 0, "specify %s -H admin for usage information",
350 		       program_name);
351 	}
352     }
353     argc -= optind;
354     argv += optind;
355 
356     for (i = 0; i < admin_data.ac; ++i)
357     {
358 	assert (admin_data.av[i][0] == '-');
359 	switch (admin_data.av[i][1])
360 	{
361 	    case 'm':
362 	    case 'l':
363 	    case 'u':
364 		check_numeric (&admin_data.av[i][2], argc, argv);
365 		break;
366 	    default:
367 		break;
368 	}
369     }
370     if (admin_data.branch != NULL)
371 	check_numeric (admin_data.branch + 2, argc, argv);
372     if (admin_data.delete_revs != NULL)
373     {
374 	char *p;
375 
376 	check_numeric (admin_data.delete_revs + 2, argc, argv);
377 	p = strchr (admin_data.delete_revs + 2, ':');
378 	if (p != NULL && isdigit ((unsigned char) p[1]))
379 	    check_numeric (p + 1, argc, argv);
380 	else if (p != NULL && p[1] == ':' && isdigit ((unsigned char) p[2]))
381 	    check_numeric (p + 2, argc, argv);
382     }
383 
384 #ifdef CLIENT_SUPPORT
385     if (client_active)
386     {
387 	/* We're the client side.  Fire up the remote server.  */
388 	start_server ();
389 
390 	ign_setup ();
391 
392 	/* Note that option_with_arg does not work for us, because some
393 	   of the options must be sent without a space between the option
394 	   and its argument.  */
395 	if (admin_data.interactive)
396 	    error (1, 0, "-I option not useful with client/server");
397 	if (admin_data.branch != NULL)
398 	    send_arg (admin_data.branch);
399 	if (admin_data.comment != NULL)
400 	    send_arg (admin_data.comment);
401 	if (admin_data.set_strict)
402 	    send_arg ("-L");
403 	if (admin_data.set_nonstrict)
404 	    send_arg ("-U");
405 	if (admin_data.delete_revs != NULL)
406 	    send_arg (admin_data.delete_revs);
407 	if (admin_data.desc != NULL)
408 	    send_arg (admin_data.desc);
409 	if (admin_data.quiet)
410 	    send_arg ("-q");
411 	if (admin_data.kflag != NULL)
412 	    send_arg (admin_data.kflag);
413 
414 	for (i = 0; i < admin_data.ac; ++i)
415 	    send_arg (admin_data.av[i]);
416 
417 	send_files (argc, argv, 0, 0, SEND_NO_CONTENTS);
418 	send_file_names (argc, argv, SEND_EXPAND_WILD);
419 	send_to_server ("admin\012", 0);
420         err = get_responses_and_close ();
421 	goto return_it;
422     }
423 #endif /* CLIENT_SUPPORT */
424 
425     lock_tree_for_write (argc, argv, 0, 0);
426 
427     err = start_recursion (admin_fileproc, (FILESDONEPROC) NULL, admin_dirproc,
428 			   (DIRLEAVEPROC) NULL, (void *)&admin_data,
429 			   argc, argv, 0,
430 			   W_LOCAL, 0, 0, (char *) NULL, 1);
431     Lock_Cleanup ();
432 
433  return_it:
434     if (admin_data.branch != NULL)
435 	free (admin_data.branch);
436     if (admin_data.comment != NULL)
437 	free (admin_data.comment);
438     if (admin_data.delete_revs != NULL)
439 	free (admin_data.delete_revs);
440     if (admin_data.kflag != NULL)
441 	free (admin_data.kflag);
442     if (admin_data.desc != NULL)
443 	free (admin_data.desc);
444     for (i = 0; i < admin_data.ac; ++i)
445 	free (admin_data.av[i]);
446     if (admin_data.av != NULL)
447 	free (admin_data.av);
448 
449     return (err);
450 }
451 
452 /*
453  * Called to run "rcs" on a particular file.
454  */
455 /* ARGSUSED */
456 static int
457 admin_fileproc (callerdat, finfo)
458     void *callerdat;
459     struct file_info *finfo;
460 {
461     struct admin_data *admin_data = (struct admin_data *) callerdat;
462     Vers_TS *vers;
463     char *version;
464     int i;
465     int status = 0;
466     RCSNode *rcs, *rcs2;
467 
468     vers = Version_TS (finfo, NULL, NULL, NULL, 0, 0);
469 
470     version = vers->vn_user;
471     if (version == NULL)
472 	goto exitfunc;
473     else if (strcmp (version, "0") == 0)
474     {
475 	error (0, 0, "cannot admin newly added file `%s'", finfo->file);
476 	goto exitfunc;
477     }
478 
479     rcs = vers->srcfile;
480     if (rcs->flags & PARTIAL)
481 	RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL);
482 
483     status = 0;
484 
485     if (!admin_data->quiet)
486     {
487 	cvs_output ("RCS file: ", 0);
488 	cvs_output (rcs->path, 0);
489 	cvs_output ("\n", 1);
490     }
491 
492     if (admin_data->branch != NULL)
493     {
494 	char *branch = &admin_data->branch[2];
495 	if (*branch != '\0' && ! isdigit ((unsigned char) *branch))
496 	{
497 	    branch = RCS_whatbranch (rcs, admin_data->branch + 2);
498 	    if (branch == NULL)
499 	    {
500 		error (0, 0, "%s: Symbolic name %s is undefined.",
501 				rcs->path, admin_data->branch + 2);
502 		status = 1;
503 	    }
504 	}
505 	if (status == 0)
506 	    RCS_setbranch (rcs, branch);
507 	if (branch != NULL && branch != &admin_data->branch[2])
508 	    free (branch);
509     }
510     if (admin_data->comment != NULL)
511     {
512 	if (rcs->comment != NULL)
513 	    free (rcs->comment);
514 	rcs->comment = xstrdup (admin_data->comment + 2);
515     }
516     if (admin_data->set_strict)
517 	rcs->strict_locks = 1;
518     if (admin_data->set_nonstrict)
519 	rcs->strict_locks = 0;
520     if (admin_data->delete_revs != NULL)
521     {
522 	char *s, *t, *rev1, *rev2;
523 	/* Set for :, clear for ::.  */
524 	int inclusive;
525 	char *t2;
526 
527 	s = admin_data->delete_revs + 2;
528 	inclusive = 1;
529 	t = strchr (s, ':');
530 	if (t != NULL)
531 	{
532 	    if (t[1] == ':')
533 	    {
534 		inclusive = 0;
535 		t2 = t + 2;
536 	    }
537 	    else
538 		t2 = t + 1;
539 	}
540 
541 	/* Note that we don't support '-' for ranges.  RCS considers it
542 	   obsolete and it is problematic with tags containing '-'.  "cvs log"
543 	   has made the same decision.  */
544 
545 	if (t == NULL)
546 	{
547 	    /* -orev */
548 	    rev1 = xstrdup (s);
549 	    rev2 = xstrdup (s);
550 	}
551 	else if (t == s)
552 	{
553 	    /* -o:rev2 */
554 	    rev1 = NULL;
555 	    rev2 = xstrdup (t2);
556 	}
557 	else
558 	{
559 	    *t = '\0';
560 	    rev1 = xstrdup (s);
561 	    *t = ':';	/* probably unnecessary */
562 	    if (*t2 == '\0')
563 		/* -orev1: */
564 		rev2 = NULL;
565 	    else
566 		/* -orev1:rev2 */
567 		rev2 = xstrdup (t2);
568 	}
569 
570 	if (rev1 == NULL && rev2 == NULL)
571 	{
572 	    /* RCS segfaults if `-o:' is given */
573 	    error (0, 0, "no valid revisions specified in `%s' option",
574 		   admin_data->delete_revs);
575 	    status = 1;
576 	}
577 	else
578 	{
579 	    status |= RCS_delete_revs (rcs, rev1, rev2, inclusive);
580 	    if (rev1)
581 		free (rev1);
582 	    if (rev2)
583 		free (rev2);
584 	}
585     }
586     if (admin_data->desc != NULL)
587     {
588 	free (rcs->desc);
589 	rcs->desc = NULL;
590 	if (admin_data->desc[2] == '-')
591 	    rcs->desc = xstrdup (admin_data->desc + 3);
592 	else
593 	{
594 	    char *descfile = admin_data->desc + 2;
595 	    size_t bufsize = 0;
596 	    size_t len;
597 
598 	    /* If -t specified with no argument, read from stdin. */
599 	    if (*descfile == '\0')
600 		descfile = NULL;
601 	    get_file (descfile, descfile, "r", &rcs->desc, &bufsize, &len);
602 	}
603     }
604     if (admin_data->kflag != NULL)
605     {
606 	char *kflag = admin_data->kflag + 2;
607 	char *oldexpand = RCS_getexpand (rcs);
608 	if (oldexpand == NULL || strcmp (oldexpand, kflag) != 0)
609 	    RCS_setexpand (rcs, kflag);
610     }
611 
612     /* Handle miscellaneous options.  TODO: decide whether any or all
613        of these should have their own fields in the admin_data
614        structure. */
615     for (i = 0; i < admin_data->ac; ++i)
616     {
617 	char *arg;
618 	char *p, *rev, *revnum, *tag, *msg;
619 	char **users;
620 	int argc, u;
621 	Node *n;
622 	RCSVers *delta;
623 
624 	arg = admin_data->av[i];
625 	switch (arg[1])
626 	{
627 	    case 'a': /* fall through */
628 	    case 'e':
629 	        line2argv (&argc, &users, arg + 2, " ,\t\n");
630 		if (arg[1] == 'a')
631 		    for (u = 0; u < argc; ++u)
632 			RCS_addaccess (rcs, users[u]);
633 		else
634 		    for (u = 0; u < argc; ++u)
635 			RCS_delaccess (rcs, users[u]);
636 		free_names (&argc, users);
637 		break;
638 	    case 'A':
639 
640 		/* See admin-19a-admin and friends in sanity.sh for
641 		   relative pathnames.  It makes sense to think in
642 		   terms of a syntax which give pathnames relative to
643 		   the repository or repository corresponding to the
644 		   current directory or some such (and perhaps don't
645 		   include ,v), but trying to worry about such things
646 		   is a little pointless unless you first worry about
647 		   whether "cvs admin -A" as a whole makes any sense
648 		   (currently probably not, as access lists don't
649 		   affect the behavior of CVS).  */
650 
651 		rcs2 = RCS_parsercsfile (arg + 2);
652 		if (rcs2 == NULL)
653 		    error (1, 0, "cannot continue");
654 
655 		p = xstrdup (RCS_getaccess (rcs2));
656 	        line2argv (&argc, &users, p, " \t\n");
657 		free (p);
658 		freercsnode (&rcs2);
659 
660 		for (u = 0; u < argc; ++u)
661 		    RCS_addaccess (rcs, users[u]);
662 		free_names (&argc, users);
663 		break;
664 	    case 'n': /* fall through */
665 	    case 'N':
666 		if (arg[2] == '\0')
667 		{
668 		    cvs_outerr ("missing symbolic name after ", 0);
669 		    cvs_outerr (arg, 0);
670 		    cvs_outerr ("\n", 1);
671 		    break;
672 		}
673 		p = strchr (arg, ':');
674 		if (p == NULL)
675 		{
676 		    if (RCS_deltag (rcs, arg + 2) != 0)
677 		    {
678 			error (0, 0, "%s: Symbolic name %s is undefined.",
679 			       rcs->path,
680 			       arg + 2);
681 			status = 1;
682 			continue;
683 		    }
684 		    break;
685 		}
686 		*p = '\0';
687 		tag = xstrdup (arg + 2);
688 		*p++ = ':';
689 
690 		/* Option `n' signals an error if this tag is already bound. */
691 		if (arg[1] == 'n')
692 		{
693 		    n = findnode (RCS_symbols (rcs), tag);
694 		    if (n != NULL)
695 		    {
696 			error (0, 0,
697 			       "%s: symbolic name %s already bound to %s",
698 			       rcs->path,
699 			       tag, n->data);
700 			status = 1;
701 			free (tag);
702 			continue;
703 		    }
704 		}
705 
706                 /* Attempt to perform the requested tagging.  */
707 
708 		if ((*p == 0 && (rev = RCS_head (rcs)))
709                     || (rev = RCS_tag2rev (rcs, p))) /* tag2rev may exit */
710 		{
711 		    RCS_check_tag (tag); /* exit if not a valid tag */
712 		    RCS_settag (rcs, tag, rev);
713 		    free (rev);
714 		}
715                 else
716 		{
717 		    error (0, 0,
718 			  "%s: Symbolic name or revision %s is undefined",
719 			   rcs->path, p);
720 		    status = 1;
721 		}
722 		free (tag);
723 		break;
724 	    case 's':
725 	        p = strchr (arg, ':');
726 		if (p == NULL)
727 		{
728 		    tag = xstrdup (arg + 2);
729 		    rev = RCS_head (rcs);
730 		}
731 		else
732 		{
733 		    *p = '\0';
734 		    tag = xstrdup (arg + 2);
735 		    *p++ = ':';
736 		    rev = xstrdup (p);
737 		}
738 		revnum = RCS_gettag (rcs, rev, 0, NULL);
739 		free (rev);
740 		if (revnum != NULL)
741 		    n = findnode (rcs->versions, revnum);
742 		if (revnum == NULL || n == NULL)
743 		{
744 		    error (0, 0,
745 			   "%s: can't set state of nonexisting revision %s",
746 			   rcs->path,
747 			   rev);
748 		    if (revnum != NULL)
749 			free (revnum);
750 		    status = 1;
751 		    continue;
752 		}
753 		delta = (RCSVers *) n->data;
754 		free (delta->state);
755 		delta->state = tag;
756 		break;
757 
758 	    case 'm':
759 	        p = strchr (arg, ':');
760 		if (p == NULL)
761 		{
762 		    error (0, 0, "%s: -m option lacks revision number",
763 			   rcs->path);
764 		    status = 1;
765 		    continue;
766 		}
767 		*p = '\0';
768 		rev = RCS_gettag (rcs, arg + 2, 0, NULL);
769 		if (rev == NULL)
770 		{
771 		    error (0, 0, "%s: no such revision %s", rcs->path, rev);
772 		    status = 1;
773 		    continue;
774 		}
775 		*p++ = ':';
776 		msg = p;
777 
778 		n = findnode (rcs->versions, rev);
779 		delta = (RCSVers *) n->data;
780 		if (delta->text == NULL)
781 		{
782 		    delta->text = (Deltatext *) xmalloc (sizeof (Deltatext));
783 		    memset ((void *) delta->text, 0, sizeof (Deltatext));
784 		}
785 		delta->text->version = xstrdup (delta->version);
786 		delta->text->log = make_message_rcslegal (msg);
787 		break;
788 
789 	    case 'l':
790 	        status |= RCS_lock (rcs, arg[2] ? arg + 2 : NULL, 0);
791 		break;
792 	    case 'u':
793 		status |= RCS_unlock (rcs, arg[2] ? arg + 2 : NULL, 0);
794 		break;
795 	    default: assert(0);	/* can't happen */
796 	}
797     }
798 
799     /* TODO: reconcile the weird discrepancies between
800        admin_data->quiet and quiet. */
801     if (status == 0)
802     {
803 	RCS_rewrite (rcs, NULL, NULL);
804 	if (!admin_data->quiet)
805 	    cvs_output ("done\n", 5);
806     }
807     else
808     {
809 	/* Note that this message should only occur after another
810 	   message has given a more specific error.  The point of this
811 	   additional message is to make it clear that the previous problems
812 	   caused CVS to forget about the idea of modifying the RCS file.  */
813 	error (0, 0, "cannot modify RCS file for `%s'", finfo->file);
814 
815 	/* Upon failure, we want to abandon any changes made to the
816 	   RCS data structure.  Forcing a reparse does the trick,
817 	   but leaks memory and is kludgey.  Should we export
818 	   free_rcsnode_contents for this purpose? */
819 	RCS_reparsercsfile (rcs, (FILE **) NULL, (struct rcsbuffer *) NULL);
820     }
821 
822   exitfunc:
823     freevers_ts (&vers);
824     return status;
825 }
826 
827 /*
828  * Print a warm fuzzy message
829  */
830 /* ARGSUSED */
831 static Dtype
832 admin_dirproc (callerdat, dir, repos, update_dir, entries)
833     void *callerdat;
834     char *dir;
835     char *repos;
836     char *update_dir;
837     List *entries;
838 {
839     if (!quiet)
840 	error (0, 0, "Administrating %s", update_dir);
841     return (R_PROCESS);
842 }
843