admin.c revision 32785
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
18static Dtype admin_dirproc PROTO ((void *callerdat, char *dir,
19				   char *repos, char *update_dir,
20				   List *entries));
21static int admin_fileproc PROTO ((void *callerdat, struct file_info *finfo));
22
23static 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.  */
31struct 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).  */
82static void
83arg_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
110int
111admin (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 (p[1]))
379	    check_numeric (p + 1, argc, argv);
380	else if (p != NULL && p[1] == ':' && isdigit(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_file_names (argc, argv, SEND_EXPAND_WILD);
418	send_files (argc, argv, 0, 0, SEND_NO_CONTENTS);
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 */
456static int
457admin_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, 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	RCS_setbranch (rcs, (admin_data->branch[2] == '\0'
494			     ? NULL
495			     : admin_data->branch + 2));
496    if (admin_data->comment != NULL)
497    {
498	if (rcs->comment != NULL)
499	    free (rcs->comment);
500	rcs->comment = xstrdup (admin_data->comment + 2);
501    }
502    if (admin_data->set_strict)
503	rcs->strict_locks = 1;
504    if (admin_data->set_nonstrict)
505	rcs->strict_locks = 0;
506    if (admin_data->delete_revs != NULL)
507    {
508	char *s, *t, *rev1, *rev2;
509	/* Set for :, clear for ::.  */
510	int inclusive;
511	char *t2;
512
513	s = admin_data->delete_revs + 2;
514	inclusive = 1;
515	t = strchr (s, ':');
516	if (t != NULL)
517	{
518	    if (t[1] == ':')
519	    {
520		inclusive = 0;
521		t2 = t + 2;
522	    }
523	    else
524		t2 = t + 1;
525	}
526
527	/* Note that we don't support '-' for ranges.  RCS considers it
528	   obsolete and it is problematic with tags containing '-'.  "cvs log"
529	   has made the same decision.  */
530
531	if (t == NULL)
532	{
533	    /* -orev */
534	    rev1 = xstrdup (s);
535	    rev2 = xstrdup (s);
536	}
537	else if (t == s)
538	{
539	    /* -o:rev2 */
540	    rev1 = NULL;
541	    rev2 = xstrdup (t2);
542	}
543	else
544	{
545	    *t = '\0';
546	    rev1 = xstrdup (s);
547	    *t = ':';	/* probably unnecessary */
548	    if (*t2 == '\0')
549		/* -orev1: */
550		rev2 = NULL;
551	    else
552		/* -orev1:rev2 */
553		rev2 = xstrdup (t2);
554	}
555
556	if (rev1 == NULL && rev2 == NULL)
557	{
558	    /* RCS segfaults if `-o:' is given */
559	    error (0, 0, "no valid revisions specified in `%s' option",
560		   admin_data->delete_revs);
561	    status = 1;
562	}
563	else
564	{
565	    status |= RCS_delete_revs (rcs, rev1, rev2, inclusive);
566	    if (rev1)
567		free (rev1);
568	    if (rev2)
569		free (rev2);
570	}
571    }
572    if (admin_data->desc != NULL)
573    {
574	free (rcs->desc);
575	rcs->desc = NULL;
576	if (admin_data->desc[2] == '-')
577	    rcs->desc = xstrdup (admin_data->desc + 3);
578	else
579	{
580	    char *descfile = admin_data->desc + 2;
581	    size_t bufsize = 0;
582	    size_t len;
583
584	    /* If -t specified with no argument, read from stdin. */
585	    if (*descfile == '\0')
586		descfile = NULL;
587	    get_file (descfile, descfile, "r", &rcs->desc, &bufsize, &len);
588	}
589    }
590    if (admin_data->kflag != NULL)
591    {
592	char *kflag = admin_data->kflag + 2;
593	if (!rcs->expand || strcmp (rcs->expand, kflag) != 0)
594	{
595	    if (rcs->expand)
596		free (rcs->expand);
597	    rcs->expand = xstrdup (kflag);
598	}
599    }
600
601    /* Handle miscellaneous options.  TODO: decide whether any or all
602       of these should have their own fields in the admin_data
603       structure. */
604    for (i = 0; i < admin_data->ac; ++i)
605    {
606	char *arg;
607	char *p, *rev, *revnum, *tag, *msg;
608	char **users;
609	int argc, u;
610	Node *n;
611	RCSVers *delta;
612
613	arg = admin_data->av[i];
614	switch (arg[1])
615	{
616	    case 'a': /* fall through */
617	    case 'e':
618	        line2argv (&argc, &users, arg + 2, " ,\t\n");
619		if (arg[1] == 'a')
620		    for (u = 0; u < argc; ++u)
621			RCS_addaccess (rcs, users[u]);
622		else
623		    for (u = 0; u < argc; ++u)
624			RCS_delaccess (rcs, users[u]);
625		free_names (&argc, users);
626		break;
627	    case 'A':
628
629		/* See admin-19a-admin and friends in sanity.sh for
630		   relative pathnames.  It makes sense to think in
631		   terms of a syntax which give pathnames relative to
632		   the repository or repository corresponding to the
633		   current directory or some such (and perhaps don't
634		   include ,v), but trying to worry about such things
635		   is a little pointless unless you first worry about
636		   whether "cvs admin -A" as a whole makes any sense
637		   (currently probably not, as access lists don't
638		   affect the behavior of CVS).  */
639
640		rcs2 = RCS_parsercsfile (arg + 2);
641		if (rcs2 == NULL)
642		    error (1, 0, "cannot continue");
643
644		p = xstrdup (RCS_getaccess (rcs2));
645	        line2argv (&argc, &users, p, " \t\n");
646		free (p);
647		freercsnode (&rcs2);
648
649		for (u = 0; u < argc; ++u)
650		    RCS_addaccess (rcs, users[u]);
651		free_names (&argc, users);
652		break;
653	    case 'n': /* fall through */
654	    case 'N':
655		if (arg[2] == '\0')
656		{
657		    cvs_outerr ("missing symbolic name after ", 0);
658		    cvs_outerr (arg, 0);
659		    cvs_outerr ("\n", 1);
660		    break;
661		}
662		p = strchr (arg, ':');
663		if (p == NULL)
664		{
665		    if (RCS_deltag (rcs, arg + 2) != 0)
666		    {
667			error (0, 0, "%s: Symbolic name %s is undefined.",
668			       rcs->path,
669			       arg + 2);
670			status = 1;
671			continue;
672		    }
673		    break;
674		}
675		*p = '\0';
676		tag = xstrdup (arg + 2);
677		*p++ = ':';
678
679		/* Option `n' signals an error if this tag is already bound. */
680		if (arg[1] == 'n')
681		{
682		    n = findnode (RCS_symbols (rcs), tag);
683		    if (n != NULL)
684		    {
685			error (0, 0,
686			       "%s: symbolic name %s already bound to %s",
687			       rcs->path,
688			       tag, n->data);
689			status = 1;
690			continue;
691		    }
692		}
693
694		/* Expand rev if necessary. */
695		rev = RCS_gettag (rcs, p, 0, NULL);
696		RCS_settag (rcs, tag, rev);
697		if (rev != NULL)
698		    free (rev);
699		free (tag);
700		break;
701	    case 's':
702	        p = strchr (arg, ':');
703		if (p == NULL)
704		{
705		    tag = xstrdup (arg + 2);
706		    rev = RCS_head (rcs);
707		}
708		else
709		{
710		    *p = '\0';
711		    tag = xstrdup (arg + 2);
712		    *p++ = ':';
713		    rev = xstrdup (p);
714		}
715		revnum = RCS_gettag (rcs, rev, 0, NULL);
716		free (rev);
717		if (revnum != NULL)
718		    n = findnode (rcs->versions, revnum);
719		if (revnum == NULL || n == NULL)
720		{
721		    error (0, 0,
722			   "%s: can't set state of nonexisting revision %s",
723			   rcs->path,
724			   rev);
725		    if (revnum != NULL)
726			free (revnum);
727		    status = 1;
728		    continue;
729		}
730		delta = (RCSVers *) n->data;
731		free (delta->state);
732		delta->state = tag;
733		break;
734
735	    case 'm':
736	        p = strchr (arg, ':');
737		if (p == NULL)
738		{
739		    error (0, 0, "%s: -m option lacks revision number",
740			   rcs->path);
741		    status = 1;
742		    continue;
743		}
744		*p = '\0';
745		rev = RCS_gettag (rcs, arg + 2, 0, NULL);
746		if (rev == NULL)
747		{
748		    error (0, 0, "%s: no such revision %s", rcs->path, rev);
749		    status = 1;
750		    continue;
751		}
752		*p++ = ':';
753		msg = p;
754
755		n = findnode (rcs->versions, rev);
756		delta = (RCSVers *) n->data;
757		if (delta->text == NULL)
758		{
759		    delta->text = (Deltatext *) xmalloc (sizeof (Deltatext));
760		    memset ((void *) delta->text, 0, sizeof (Deltatext));
761		}
762		delta->text->version = xstrdup (delta->version);
763		delta->text->log = make_message_rcslegal (msg);
764		break;
765
766	    case 'l':
767	        status |= RCS_lock (rcs, arg[2] ? arg + 2 : NULL, 0);
768		break;
769	    case 'u':
770		status |= RCS_unlock (rcs, arg[2] ? arg + 2 : NULL, 0);
771		break;
772	    default: assert(0);	/* can't happen */
773	}
774    }
775
776    /* TODO: reconcile the weird discrepancies between
777       admin_data->quiet and quiet. */
778    if (status == 0)
779    {
780	RCS_rewrite (rcs, NULL, NULL);
781	if (!admin_data->quiet)
782	    cvs_output ("done\n", 5);
783    }
784    else
785    {
786	/* Note that this message should only occur after another
787	   message has given a more specific error.  The point of this
788	   additional message is to make it clear that the previous problems
789	   caused CVS to forget about the idea of modifying the RCS file.  */
790	error (0, 0, "cannot modify RCS file for `%s'", finfo->file);
791
792	/* Upon failure, we want to abandon any changes made to the
793	   RCS data structure.  Forcing a reparse does the trick,
794	   but leaks memory and is kludgey.  Should we export
795	   free_rcsnode_contents for this purpose? */
796	RCS_reparsercsfile (rcs, NULL);
797    }
798
799  exitfunc:
800    freevers_ts (&vers);
801    return status;
802}
803
804/*
805 * Print a warm fuzzy message
806 */
807/* ARGSUSED */
808static Dtype
809admin_dirproc (callerdat, dir, repos, update_dir, entries)
810    void *callerdat;
811    char *dir;
812    char *repos;
813    char *update_dir;
814    List *entries;
815{
816    if (!quiet)
817	error (0, 0, "Administrating %s", update_dir);
818    return (R_PROCESS);
819}
820