1/*
2 * Copyright (C) 1986-2008 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 * Print Log Information
14 *
15 * Prints the RCS "log" (rlog) information for the specified files.  With no
16 * argument, prints the log information for all the files in the directory
17 * (recursive by default).
18 *
19 * $FreeBSD$
20 */
21
22#include "cvs.h"
23#include <assert.h>
24
25/* This structure holds information parsed from the -r option.  */
26
27struct option_revlist
28{
29    /* The next -r option.  */
30    struct option_revlist *next;
31    /* The first revision to print.  This is NULL if the range is
32       :rev, or if no revision is given.  */
33    char *first;
34    /* The last revision to print.  This is NULL if the range is rev:,
35       or if no revision is given.  If there is no colon, first and
36       last are the same.  */
37    char *last;
38    /* Nonzero if there was a trailing `.', which means to print only
39       the head revision of a branch.  */
40    int branchhead;
41    /* Nonzero if first and last are inclusive.  */
42    int inclusive;
43};
44
45/* This structure holds information derived from option_revlist given
46   a particular RCS file.  */
47
48struct revlist
49{
50    /* The next pair.  */
51    struct revlist *next;
52    /* The first numeric revision to print.  */
53    char *first;
54    /* The last numeric revision to print.  */
55    char *last;
56    /* The number of fields in these revisions (one more than
57       numdots).  */
58    int fields;
59    /* Whether first & last are to be included or excluded.  */
60    int inclusive;
61};
62
63/* This structure holds information parsed from the -d option.  */
64
65struct datelist
66{
67    /* The next date.  */
68    struct datelist *next;
69    /* The starting date.  */
70    char *start;
71    /* The ending date.  */
72    char *end;
73    /* Nonzero if the range is inclusive rather than exclusive.  */
74    int inclusive;
75};
76
77/* This structure is used to pass information through start_recursion.  */
78struct log_data
79{
80    /* Nonzero if the -R option was given, meaning that only the name
81       of the RCS file should be printed.  */
82    int nameonly;
83    /* Nonzero if the -h option was given, meaning that only header
84       information should be printed.  */
85    int header;
86    /* Nonzero if the -t option was given, meaning that only the
87       header and the descriptive text should be printed.  */
88    int long_header;
89    /* Nonzero if the -N option was seen, meaning that tag information
90       should not be printed.  */
91    int notags;
92    /* Nonzero if the -b option was seen, meaning that revisions
93       on the default branch should be printed.  */
94    int default_branch;
95    /* Nonzero if the -S option was seen, meaning that the header/name
96       should be suppressed if no revisions are selected.  */
97    int sup_header;
98    /* If not NULL, the value given for the -r option, which lists
99       sets of revisions to be printed.  */
100    struct option_revlist *revlist;
101    /* If not NULL, the date pairs given for the -d option, which
102       select date ranges to print.  */
103    struct datelist *datelist;
104    /* If not NULL, the single dates given for the -d option, which
105       select specific revisions to print based on a date.  */
106    struct datelist *singledatelist;
107    /* If not NULL, the list of states given for the -s option, which
108       only prints revisions of given states.  */
109    List *statelist;
110    /* If not NULL, the list of login names given for the -w option,
111       which only prints revisions checked in by given users.  */
112    List *authorlist;
113};
114
115/* This structure is used to pass information through walklist.  */
116struct log_data_and_rcs
117{
118    struct log_data *log_data;
119    struct revlist *revlist;
120    RCSNode *rcs;
121};
122
123static int rlog_proc PROTO((int argc, char **argv, char *xwhere,
124			    char *mwhere, char *mfile, int shorten,
125			    int local_specified, char *mname, char *msg));
126static Dtype log_dirproc PROTO ((void *callerdat, const char *dir,
127                                 const char *repository,
128                                 const char *update_dir,
129                                 List *entries));
130static int log_fileproc PROTO ((void *callerdat, struct file_info *finfo));
131static struct option_revlist *log_parse_revlist PROTO ((const char *));
132static void log_parse_date PROTO ((struct log_data *, const char *));
133static void log_parse_list PROTO ((List **, const char *));
134static struct revlist *log_expand_revlist PROTO ((RCSNode *, char *,
135						  struct option_revlist *,
136						  int));
137static void log_free_revlist PROTO ((struct revlist *));
138static int log_version_requested PROTO ((struct log_data *, struct revlist *,
139					 RCSNode *, RCSVers *));
140static int log_symbol PROTO ((Node *, void *));
141static int log_count PROTO ((Node *, void *));
142static int log_fix_singledate PROTO ((Node *, void *));
143static int log_count_print PROTO ((Node *, void *));
144static void log_tree PROTO ((struct log_data *, struct revlist *,
145			     RCSNode *, const char *));
146static void log_abranch PROTO ((struct log_data *, struct revlist *,
147				RCSNode *, const char *));
148static void log_version PROTO ((struct log_data *, struct revlist *,
149				RCSNode *, RCSVers *, int));
150static int log_branch PROTO ((Node *, void *));
151static int version_compare PROTO ((const char *, const char *, int));
152
153static struct log_data log_data;
154static int is_rlog;
155
156static const char *const log_usage[] =
157{
158    "Usage: %s %s [-lRhtNb] [-r[revisions]] [-d dates] [-s states]\n",
159    "    [-w[logins]] [files...]\n",
160    "\t-l\tLocal directory only, no recursion.\n",
161    "\t-b\tList revisions on the default branch.\n",
162    "\t-h\tOnly print header.\n",
163    "\t-R\tOnly print name of RCS file.\n",
164    "\t-t\tOnly print header and descriptive text.\n",
165    "\t-N\tDo not list tags.\n",
166    "\t-n\tList tags (default).\n",
167    "\t-S\tDo not print name/header if no revisions selected.  -d, -r,\n",
168    "\t\t-s, & -w have little effect in conjunction with -b, -h, -R, and\n",
169    "\t\t-t without this option.\n",
170    "\t-r[revisions]\tA comma-separated list of revisions to print:\n",
171    "\t   rev1:rev2   Between rev1 and rev2, including rev1 and rev2.\n",
172    "\t   rev1::rev2  Between rev1 and rev2, excluding rev1.\n",
173    "\t   rev:        rev and following revisions on the same branch.\n",
174    "\t   rev::       After rev on the same branch.\n",
175    "\t   :rev        rev and previous revisions on the same branch.\n",
176    "\t   ::rev       rev and previous revisions on the same branch.\n",
177    "\t   rev         Just rev.\n",
178    "\t   branch      All revisions on the branch.\n",
179    "\t   branch.     The last revision on the branch.\n",
180    "\t-d dates\tA semicolon-separated list of dates\n",
181    "\t        \t(D1<D2 for range, D for latest before).\n",
182    "\t-s states\tOnly list revisions with specified states.\n",
183    "\t-w[logins]\tOnly list revisions checked in by specified logins.\n",
184    "(Specify the --help global option for a list of other help options)\n",
185    NULL
186};
187
188#ifdef CLIENT_SUPPORT
189
190/* Helper function for send_arg_list.  */
191static int send_one PROTO ((Node *, void *));
192
193static int
194send_one (node, closure)
195    Node *node;
196    void *closure;
197{
198    char *option = (char *) closure;
199
200    send_to_server ("Argument ", 0);
201    send_to_server (option, 0);
202    if (strcmp (node->key, "@@MYSELF") == 0)
203	/* It is a bare -w option.  Note that we must send it as
204	   -w rather than messing with getcaller() or something (which on
205	   the client will return garbage).  */
206	;
207    else
208	send_to_server (node->key, 0);
209    send_to_server ("\012", 0);
210    return 0;
211}
212
213/* For each element in ARG, send an argument consisting of OPTION
214   concatenated with that element.  */
215static void send_arg_list PROTO ((char *, List *));
216
217static void
218send_arg_list (option, arg)
219    char *option;
220    List *arg;
221{
222    if (arg == NULL)
223	return;
224    walklist (arg, send_one, (void *)option);
225}
226
227#endif
228
229int
230cvslog (argc, argv)
231    int argc;
232    char **argv;
233{
234    int c;
235    int err = 0;
236    int local = 0;
237    struct option_revlist **prl;
238
239    is_rlog = (strcmp (cvs_cmd_name, "rlog") == 0);
240
241    if (argc == -1)
242	usage (log_usage);
243
244    memset (&log_data, 0, sizeof log_data);
245    prl = &log_data.revlist;
246
247    optind = 0;
248    while ((c = getopt (argc, argv, "+bd:hlNnSRr::s:tw::")) != -1)
249    {
250	switch (c)
251	{
252	    case 'b':
253		log_data.default_branch = 1;
254		break;
255	    case 'd':
256		log_parse_date (&log_data, optarg);
257		break;
258	    case 'h':
259		log_data.header = 1;
260		break;
261	    case 'l':
262		local = 1;
263		break;
264	    case 'N':
265		log_data.notags = 1;
266		break;
267	    case 'n':
268		log_data.notags = 0;
269		break;
270	    case 'S':
271		log_data.sup_header = 1;
272		break;
273	    case 'R':
274		log_data.nameonly = 1;
275		break;
276	    case 'r':
277		*prl = log_parse_revlist (optarg);
278		prl = &(*prl)->next;
279		break;
280	    case 's':
281		log_parse_list (&log_data.statelist, optarg);
282		break;
283	    case 't':
284		log_data.long_header = 1;
285		break;
286	    case 'w':
287		if (optarg != NULL)
288		    log_parse_list (&log_data.authorlist, optarg);
289		else
290		    log_parse_list (&log_data.authorlist, "@@MYSELF");
291		break;
292	    case '?':
293	    default:
294		usage (log_usage);
295		break;
296	}
297    }
298    argc -= optind;
299    argv += optind;
300
301    wrap_setup ();
302
303#ifdef CLIENT_SUPPORT
304    if (current_parsed_root->isremote)
305    {
306	struct datelist *p;
307	struct option_revlist *rp;
308	char datetmp[MAXDATELEN];
309
310	/* We're the local client.  Fire up the remote server.  */
311	start_server ();
312
313	if (is_rlog && !supported_request ("rlog"))
314	    error (1, 0, "server does not support rlog");
315
316	ign_setup ();
317
318	if (log_data.default_branch)
319	    send_arg ("-b");
320
321	while (log_data.datelist != NULL)
322	{
323	    p = log_data.datelist;
324	    log_data.datelist = p->next;
325	    assert (p->start != NULL && p->end != NULL);
326	    send_to_server ("Argument -d\012", 0);
327	    send_to_server ("Argument ", 0);
328	    date_to_internet (datetmp, p->start);
329	    send_to_server (datetmp, 0);
330	    if (p->inclusive)
331		send_to_server ("<=", 0);
332	    else
333		send_to_server ("<", 0);
334	    date_to_internet (datetmp, p->end);
335	    send_to_server (datetmp, 0);
336	    send_to_server ("\012", 0);
337	    free (p->start);
338	    free (p->end);
339	    free (p);
340	}
341	while (log_data.singledatelist != NULL)
342	{
343	    p = log_data.singledatelist;
344	    log_data.singledatelist = p->next;
345	    assert (p->end != NULL);
346	    send_to_server ("Argument -d\012", 0);
347	    send_to_server ("Argument ", 0);
348	    date_to_internet (datetmp, p->end);
349	    send_to_server (datetmp, 0);
350	    send_to_server ("\012", 0);
351	    free (p->end);
352	    free (p);
353	}
354
355	if (log_data.header)
356	    send_arg ("-h");
357	if (local)
358	    send_arg("-l");
359	if (log_data.notags)
360	    send_arg("-N");
361	if (log_data.sup_header)
362	    send_arg("-S");
363	if (log_data.nameonly)
364	    send_arg("-R");
365	if (log_data.long_header)
366	    send_arg("-t");
367
368	while (log_data.revlist != NULL)
369	{
370	    rp = log_data.revlist;
371	    log_data.revlist = rp->next;
372	    send_to_server ("Argument -r", 0);
373	    if (rp->branchhead)
374	    {
375		if (rp->first != NULL)
376		    send_to_server (rp->first, 0);
377		send_to_server (".", 1);
378	    }
379	    else
380	    {
381		if (rp->first != NULL)
382		    send_to_server (rp->first, 0);
383		send_to_server (":", 1);
384		if (!rp->inclusive)
385		    send_to_server (":", 1);
386		if (rp->last != NULL)
387		    send_to_server (rp->last, 0);
388	    }
389	    send_to_server ("\012", 0);
390	    if (rp->first)
391		free (rp->first);
392	    if (rp->last)
393		free (rp->last);
394	    free (rp);
395	}
396	send_arg_list ("-s", log_data.statelist);
397	dellist (&log_data.statelist);
398	send_arg_list ("-w", log_data.authorlist);
399	dellist (&log_data.authorlist);
400	send_arg ("--");
401
402	if (is_rlog)
403	{
404	    int i;
405	    for (i = 0; i < argc; i++)
406		send_arg (argv[i]);
407	    send_to_server ("rlog\012", 0);
408	}
409	else
410	{
411	    send_files (argc, argv, local, 0, SEND_NO_CONTENTS);
412	    send_file_names (argc, argv, SEND_EXPAND_WILD);
413	    send_to_server ("log\012", 0);
414	}
415        err = get_responses_and_close ();
416	return err;
417    }
418#endif
419
420    /* OK, now that we know we are local/server, we can resolve @@MYSELF
421       into our user name.  */
422    if (findnode (log_data.authorlist, "@@MYSELF") != NULL)
423	log_parse_list (&log_data.authorlist, getcaller ());
424
425    if (is_rlog)
426    {
427	DBM *db;
428	int i;
429	db = open_module ();
430	for (i = 0; i < argc; i++)
431	{
432	    err += do_module (db, argv[i], MISC, "Logging", rlog_proc,
433			     (char *) NULL, 0, local, 0, 0, (char *) NULL);
434	}
435	close_module (db);
436    }
437    else
438    {
439	err = rlog_proc (argc + 1, argv - 1, (char *) NULL,
440			 (char *) NULL, (char *) NULL, 0, local, (char *) NULL,
441			 (char *) NULL);
442    }
443
444    while (log_data.revlist)
445    {
446	struct option_revlist *rl = log_data.revlist->next;
447	if (log_data.revlist->first)
448	    free (log_data.revlist->first);
449	if (log_data.revlist->last)
450	    free (log_data.revlist->last);
451	free (log_data.revlist);
452	log_data.revlist = rl;
453    }
454    while (log_data.datelist)
455    {
456	struct datelist *nd = log_data.datelist->next;
457	if (log_data.datelist->start)
458	    free (log_data.datelist->start);
459	if (log_data.datelist->end)
460	    free (log_data.datelist->end);
461	free (log_data.datelist);
462	log_data.datelist = nd;
463    }
464    while (log_data.singledatelist)
465    {
466	struct datelist *nd = log_data.singledatelist->next;
467	if (log_data.singledatelist->start)
468	    free (log_data.singledatelist->start);
469	if (log_data.singledatelist->end)
470	    free (log_data.singledatelist->end);
471	free (log_data.singledatelist);
472	log_data.singledatelist = nd;
473    }
474    dellist (&log_data.statelist);
475    dellist (&log_data.authorlist);
476
477    return (err);
478}
479
480
481static int
482rlog_proc (argc, argv, xwhere, mwhere, mfile, shorten, local, mname, msg)
483    int argc;
484    char **argv;
485    char *xwhere;
486    char *mwhere;
487    char *mfile;
488    int shorten;
489    int local;
490    char *mname;
491    char *msg;
492{
493    /* Begin section which is identical to patch_proc--should this
494       be abstracted out somehow?  */
495    char *myargv[2];
496    int err = 0;
497    int which;
498    char *repository;
499    char *where;
500
501    if (is_rlog)
502    {
503	repository = xmalloc (strlen (current_parsed_root->directory)
504                              + strlen (argv[0])
505			      + (mfile == NULL ? 0 : strlen (mfile) + 1) + 2);
506	(void)sprintf (repository, "%s/%s",
507                       current_parsed_root->directory, argv[0]);
508	where = xmalloc (strlen (argv[0])
509                         + (mfile == NULL ? 0 : strlen (mfile) + 1)
510			 + 1);
511	(void) strcpy (where, argv[0]);
512
513	/* If mfile isn't null, we need to set up to do only part of theu
514         * module.
515         */
516	if (mfile != NULL)
517	{
518	    char *cp;
519	    char *path;
520
521	    /* If the portion of the module is a path, put the dir part on
522             * repos.
523             */
524	    if ((cp = strrchr (mfile, '/')) != NULL)
525	    {
526		*cp = '\0';
527		(void)strcat (repository, "/");
528		(void)strcat (repository, mfile);
529		(void)strcat (where, "/");
530		(void)strcat (where, mfile);
531		mfile = cp + 1;
532	    }
533
534	    /* take care of the rest */
535	    path = xmalloc (strlen (repository) + strlen (mfile) + 5);
536	    (void)sprintf (path, "%s/%s", repository, mfile);
537	    if (isdir (path))
538	    {
539		/* directory means repository gets the dir tacked on */
540		(void)strcpy (repository, path);
541		(void)strcat (where, "/");
542		(void)strcat (where, mfile);
543	    }
544	    else
545	    {
546		myargv[0] = argv[0];
547		myargv[1] = mfile;
548		argc = 2;
549		argv = myargv;
550	    }
551	    free (path);
552	}
553
554	/* cd to the starting repository */
555	if (CVS_CHDIR (repository) < 0)
556	{
557	    error (0, errno, "cannot chdir to %s", repository);
558	    free (repository);
559	    free (where);
560	    return 1;
561	}
562	/* End section which is identical to patch_proc.  */
563
564	which = W_REPOS | W_ATTIC;
565    }
566    else
567    {
568        repository = NULL;
569        where = NULL;
570        which = W_LOCAL | W_REPOS | W_ATTIC;
571    }
572
573    err = start_recursion (log_fileproc, (FILESDONEPROC) NULL, log_dirproc,
574			   (DIRLEAVEPROC) NULL, (void *) &log_data,
575			   argc - 1, argv + 1, local, which, 0, CVS_LOCK_READ,
576			   where, 1, repository);
577
578    if (!(which & W_LOCAL)) free (repository);
579    if (where) free (where);
580
581    return err;
582}
583
584
585
586/*
587 * Parse a revision list specification.
588 */
589static struct option_revlist *
590log_parse_revlist (argstring)
591    const char *argstring;
592{
593    char *orig_copy, *copy;
594    struct option_revlist *ret, **pr;
595
596    /* Unfortunately, rlog accepts -r without an argument to mean that
597       latest revision on the default branch, so we must support that
598       for compatibility.  */
599    if (argstring == NULL)
600	argstring = "";
601
602    ret = NULL;
603    pr = &ret;
604
605    /* Copy the argument into memory so that we can change it.  We
606       don't want to change the argument because, at least as of this
607       writing, we will use it if we send the arguments to the server.  */
608    orig_copy = copy = xstrdup (argstring);
609    while (copy != NULL)
610    {
611	char *comma;
612	struct option_revlist *r;
613
614	comma = strchr (copy, ',');
615	if (comma != NULL)
616	    *comma++ = '\0';
617
618	r = (struct option_revlist *) xmalloc (sizeof *r);
619	r->next = NULL;
620	r->first = copy;
621	r->branchhead = 0;
622	r->last = strchr (copy, ':');
623	if (r->last != NULL)
624	{
625	    *r->last++ = '\0';
626	    r->inclusive = (*r->last != ':');
627	    if (!r->inclusive)
628		r->last++;
629	}
630	else
631	{
632	    r->last = r->first;
633	    r->inclusive = 1;
634	    if (r->first[0] != '\0' && r->first[strlen (r->first) - 1] == '.')
635	    {
636		r->branchhead = 1;
637		r->first[strlen (r->first) - 1] = '\0';
638	    }
639	}
640
641	if (*r->first == '\0')
642	    r->first = NULL;
643	if (*r->last == '\0')
644	    r->last = NULL;
645
646	if (r->first != NULL)
647	    r->first = xstrdup (r->first);
648	if (r->last != NULL)
649	    r->last = xstrdup (r->last);
650
651	*pr = r;
652	pr = &r->next;
653
654	copy = comma;
655    }
656
657    free (orig_copy);
658    return ret;
659}
660
661/*
662 * Parse a date specification.
663 */
664static void
665log_parse_date (log_data, argstring)
666    struct log_data *log_data;
667    const char *argstring;
668{
669    char *orig_copy, *copy;
670
671    /* Copy the argument into memory so that we can change it.  We
672       don't want to change the argument because, at least as of this
673       writing, we will use it if we send the arguments to the server.  */
674    orig_copy = copy = xstrdup (argstring);
675    while (copy != NULL)
676    {
677	struct datelist *nd, **pd;
678	char *cpend, *cp, *ds, *de;
679
680	nd = (struct datelist *) xmalloc (sizeof *nd);
681
682	cpend = strchr (copy, ';');
683	if (cpend != NULL)
684	    *cpend++ = '\0';
685
686	pd = &log_data->datelist;
687	nd->inclusive = 0;
688
689	if ((cp = strchr (copy, '>')) != NULL)
690	{
691	    *cp++ = '\0';
692	    if (*cp == '=')
693	    {
694		++cp;
695		nd->inclusive = 1;
696	    }
697	    ds = cp;
698	    de = copy;
699	}
700	else if ((cp = strchr (copy, '<')) != NULL)
701	{
702	    *cp++ = '\0';
703	    if (*cp == '=')
704	    {
705		++cp;
706		nd->inclusive = 1;
707	    }
708	    ds = copy;
709	    de = cp;
710	}
711	else
712	{
713	    ds = NULL;
714	    de = copy;
715	    pd = &log_data->singledatelist;
716	}
717
718	if (ds == NULL)
719	    nd->start = NULL;
720	else if (*ds != '\0')
721	    nd->start = Make_Date (ds);
722	else
723	{
724	  /* 1970 was the beginning of time, as far as get_date and
725	     Make_Date are concerned.  FIXME: That is true only if time_t
726	     is a POSIX-style time and there is nothing in ANSI that
727	     mandates that.  It would be cleaner to set a flag saying
728	     whether or not there is a start date.  */
729	    nd->start = Make_Date ("1/1/1970 UTC");
730	}
731
732	if (*de != '\0')
733	    nd->end = Make_Date (de);
734	else
735	{
736	    /* We want to set the end date to some time sufficiently far
737	       in the future to pick up all revisions that have been
738	       created since the specified date and the time `cvs log'
739	       completes.  FIXME: The date in question only makes sense
740	       if time_t is a POSIX-style time and it is 32 bits
741	       and signed.  We should instead be setting a flag saying
742	       whether or not there is an end date.  Note that using
743	       something like "next week" would break the testsuite (and,
744	       perhaps less importantly, loses if the clock is set grossly
745	       wrong).  */
746	    nd->end = Make_Date ("2038-01-01");
747	}
748
749	nd->next = *pd;
750	*pd = nd;
751
752	copy = cpend;
753    }
754
755    free (orig_copy);
756}
757
758/*
759 * Parse a comma separated list of items, and add each one to *PLIST.
760 */
761static void
762log_parse_list (plist, argstring)
763    List **plist;
764    const char *argstring;
765{
766    while (1)
767    {
768	Node *p;
769	char *cp;
770
771	p = getnode ();
772
773	cp = strchr (argstring, ',');
774	if (cp == NULL)
775	    p->key = xstrdup (argstring);
776	else
777	{
778	    size_t len;
779
780	    len = cp - argstring;
781	    p->key = xmalloc (len + 1);
782	    strncpy (p->key, argstring, len);
783	    p->key[len] = '\0';
784	}
785
786	if (*plist == NULL)
787	    *plist = getlist ();
788	if (addnode (*plist, p) != 0)
789	    freenode (p);
790
791	if (cp == NULL)
792	    break;
793
794	argstring = cp + 1;
795    }
796}
797
798static int printlock_proc PROTO ((Node *, void *));
799
800static int
801printlock_proc (lock, foo)
802    Node *lock;
803    void *foo;
804{
805    cvs_output ("\n\t", 2);
806    cvs_output (lock->data, 0);
807    cvs_output (": ", 2);
808    cvs_output (lock->key, 0);
809    return 0;
810}
811
812
813
814/*
815 * Do an rlog on a file
816 */
817static int
818log_fileproc (callerdat, finfo)
819    void *callerdat;
820    struct file_info *finfo;
821{
822    struct log_data *log_data = (struct log_data *) callerdat;
823    Node *p;
824    char *baserev;
825    int selrev = -1;
826    RCSNode *rcsfile;
827    char buf[50];
828    struct revlist *revlist = NULL;
829    struct log_data_and_rcs log_data_and_rcs;
830
831    rcsfile = finfo->rcs;
832    p = findnode (finfo->entries, finfo->file);
833    if (p != NULL)
834    {
835	Entnode *e = p->data;
836	baserev = e->version;
837	if (baserev[0] == '-') ++baserev;
838    }
839    else
840	baserev = NULL;
841
842    if (rcsfile == NULL)
843    {
844	/* no rcs file.  What *do* we know about this file? */
845	if (baserev != NULL)
846	{
847	    if (baserev[0] == '0' && baserev[1] == '\0')
848	    {
849		if (!really_quiet)
850		    error (0, 0, "%s has been added, but not committed",
851			   finfo->file);
852		return 0;
853	    }
854	}
855
856	if (!really_quiet)
857	    error (0, 0, "nothing known about %s", finfo->file);
858
859	return 1;
860    }
861
862    if (log_data->sup_header || !log_data->nameonly)
863    {
864
865	/* We will need all the information in the RCS file.  */
866	RCS_fully_parse (rcsfile);
867
868	/* Turn any symbolic revisions in the revision list into numeric
869	   revisions.  */
870	revlist = log_expand_revlist (rcsfile, baserev, log_data->revlist,
871				      log_data->default_branch);
872	if (log_data->sup_header
873            || (!log_data->header && !log_data->long_header))
874	{
875	    log_data_and_rcs.log_data = log_data;
876	    log_data_and_rcs.revlist = revlist;
877	    log_data_and_rcs.rcs = rcsfile;
878
879	    /* If any single dates were specified, we need to identify the
880	       revisions they select.  Each one selects the single
881	       revision, which is otherwise selected, of that date or
882	       earlier.  The log_fix_singledate routine will fill in the
883	       start date for each specific revision.  */
884	    if (log_data->singledatelist != NULL)
885		walklist (rcsfile->versions, log_fix_singledate,
886			  (void *)&log_data_and_rcs);
887
888	    selrev = walklist (rcsfile->versions, log_count_print,
889			       (void *)&log_data_and_rcs);
890	    if (log_data->sup_header && selrev == 0)
891	    {
892		log_free_revlist (revlist);
893		return 0;
894	    }
895	}
896
897    }
898
899    if (log_data->nameonly)
900    {
901	cvs_output (rcsfile->path, 0);
902	cvs_output ("\n", 1);
903	log_free_revlist (revlist);
904	return 0;
905    }
906
907    /* The output here is intended to be exactly compatible with the
908       output of rlog.  I'm not sure whether this code should be here
909       or in rcs.c; I put it here because it is specific to the log
910       function, even though it uses information gathered by the
911       functions in rcs.c.  */
912
913    cvs_output ("\n", 1);
914
915    cvs_output ("RCS file: ", 0);
916    cvs_output (rcsfile->path, 0);
917
918    if (!is_rlog)
919    {
920	cvs_output ("\nWorking file: ", 0);
921	if (finfo->update_dir[0] != '\0')
922	{
923	    cvs_output (finfo->update_dir, 0);
924	    cvs_output ("/", 0);
925	}
926	cvs_output (finfo->file, 0);
927    }
928
929    cvs_output ("\nhead:", 0);
930    if (rcsfile->head != NULL)
931    {
932	cvs_output (" ", 1);
933	cvs_output (rcsfile->head, 0);
934    }
935
936    cvs_output ("\nbranch:", 0);
937    if (rcsfile->branch != NULL)
938    {
939	cvs_output (" ", 1);
940	cvs_output (rcsfile->branch, 0);
941    }
942
943    cvs_output ("\nlocks:", 0);
944    if (rcsfile->strict_locks)
945	cvs_output (" strict", 0);
946    walklist (RCS_getlocks (rcsfile), printlock_proc, NULL);
947
948    cvs_output ("\naccess list:", 0);
949    if (rcsfile->access != NULL)
950    {
951	const char *cp;
952
953	cp = rcsfile->access;
954	while (*cp != '\0')
955	{
956		const char *cp2;
957
958		cvs_output ("\n\t", 2);
959		cp2 = cp;
960		while (!isspace ((unsigned char) *cp2) && *cp2 != '\0')
961		    ++cp2;
962		cvs_output (cp, cp2 - cp);
963		cp = cp2;
964		while (isspace ((unsigned char) *cp) && *cp != '\0')
965		    ++cp;
966	}
967    }
968
969    if (!log_data->notags)
970    {
971	List *syms;
972
973	cvs_output ("\nsymbolic names:", 0);
974	syms = RCS_symbols (rcsfile);
975	walklist (syms, log_symbol, NULL);
976    }
977
978    cvs_output ("\nkeyword substitution: ", 0);
979    if (rcsfile->expand == NULL)
980	cvs_output ("kv", 2);
981    else
982	cvs_output (rcsfile->expand, 0);
983
984    cvs_output ("\ntotal revisions: ", 0);
985    sprintf (buf, "%d", walklist (rcsfile->versions, log_count, NULL));
986    cvs_output (buf, 0);
987
988    if (selrev >= 0)
989    {
990	cvs_output (";\tselected revisions: ", 0);
991	sprintf (buf, "%d", selrev);
992	cvs_output (buf, 0);
993    }
994
995    cvs_output ("\n", 1);
996
997    if (!log_data->header || log_data->long_header)
998    {
999	cvs_output ("description:\n", 0);
1000	if (rcsfile->desc != NULL)
1001	    cvs_output (rcsfile->desc, 0);
1002    }
1003
1004    if (!log_data->header && ! log_data->long_header && rcsfile->head != NULL)
1005    {
1006	p = findnode (rcsfile->versions, rcsfile->head);
1007	if (p == NULL)
1008	    error (1, 0, "can not find head revision in `%s'",
1009		   finfo->fullname);
1010	while (p != NULL)
1011	{
1012	    RCSVers *vers = p->data;
1013
1014	    log_version (log_data, revlist, rcsfile, vers, 1);
1015	    if (vers->next == NULL)
1016		p = NULL;
1017	    else
1018	    {
1019		p = findnode (rcsfile->versions, vers->next);
1020		if (p == NULL)
1021		    error (1, 0, "can not find next revision `%s' in `%s'",
1022			   vers->next, finfo->fullname);
1023	    }
1024	}
1025
1026	log_tree (log_data, revlist, rcsfile, rcsfile->head);
1027    }
1028
1029    cvs_output("\
1030=============================================================================\n",
1031	       0);
1032
1033    /* Free up the new revlist and restore the old one.  */
1034    log_free_revlist (revlist);
1035
1036    /* If singledatelist is not NULL, free up the start dates we added
1037       to it.  */
1038    if (log_data->singledatelist != NULL)
1039    {
1040	struct datelist *d;
1041
1042	for (d = log_data->singledatelist; d != NULL; d = d->next)
1043	{
1044	    if (d->start != NULL)
1045		free (d->start);
1046	    d->start = NULL;
1047	}
1048    }
1049
1050    return 0;
1051}
1052
1053
1054
1055/*
1056 * Fix up a revision list in order to compare it against versions.
1057 * Expand any symbolic revisions.
1058 */
1059static struct revlist *
1060log_expand_revlist (rcs, baserev, revlist, default_branch)
1061    RCSNode *rcs;
1062    char *baserev;
1063    struct option_revlist *revlist;
1064    int default_branch;
1065{
1066    struct option_revlist *r;
1067    struct revlist *ret, **pr;
1068
1069    ret = NULL;
1070    pr = &ret;
1071    for (r = revlist; r != NULL; r = r->next)
1072    {
1073	struct revlist *nr;
1074
1075	nr = (struct revlist *) xmalloc (sizeof *nr);
1076	nr->inclusive = r->inclusive;
1077
1078	if (r->first == NULL && r->last == NULL)
1079	{
1080	    /* If both first and last are NULL, it means that we want
1081	       just the head of the default branch, which is RCS_head.  */
1082	    nr->first = RCS_head (rcs);
1083	    if (!nr->first)
1084	    {
1085		if (!really_quiet)
1086		    error (0, 0, "No head revision in archive `%s'.",
1087		           rcs->path);
1088		nr->last = NULL;
1089		nr->fields = 0;
1090	    }
1091	    else
1092	    {
1093		nr->last = xstrdup (nr->first);
1094		nr->fields = numdots (nr->first) + 1;
1095	    }
1096	}
1097	else if (r->branchhead)
1098	{
1099	    char *branch;
1100
1101	    assert (r->first != NULL);
1102
1103	    /* Print just the head of the branch.  */
1104	    if (isdigit ((unsigned char) r->first[0]))
1105		nr->first = RCS_getbranch (rcs, r->first, 1);
1106	    else
1107	    {
1108		branch = RCS_whatbranch (rcs, r->first);
1109		if (branch == NULL)
1110		    nr->first = NULL;
1111		else
1112		{
1113		    nr->first = RCS_getbranch (rcs, branch, 1);
1114		    free (branch);
1115		}
1116	    }
1117	    if (!nr->first)
1118	    {
1119		if (!really_quiet)
1120		    error (0, 0, "warning: no branch `%s' in `%s'",
1121			   r->first, rcs->path);
1122		nr->last = NULL;
1123		nr->fields = 0;
1124	    }
1125	    else
1126	    {
1127		nr->last = xstrdup (nr->first);
1128		nr->fields = numdots (nr->first) + 1;
1129	    }
1130	}
1131	else
1132	{
1133	    if (r->first == NULL || isdigit ((unsigned char) r->first[0]))
1134		nr->first = xstrdup (r->first);
1135	    else
1136	    {
1137		if (baserev && strcmp (r->first, TAG_BASE) == 0)
1138		    nr->first = xstrdup (baserev);
1139		else if (RCS_nodeisbranch (rcs, r->first))
1140		    nr->first = RCS_whatbranch (rcs, r->first);
1141		else
1142		    nr->first = RCS_gettag (rcs, r->first, 1, (int *) NULL);
1143		if (nr->first == NULL && !really_quiet)
1144		{
1145		    error (0, 0, "warning: no revision `%s' in `%s'",
1146			   r->first, rcs->path);
1147		}
1148	    }
1149
1150	    if (r->last == r->first || (r->last != NULL && r->first != NULL &&
1151					strcmp (r->last, r->first) == 0))
1152		nr->last = xstrdup (nr->first);
1153	    else if (r->last == NULL || isdigit ((unsigned char) r->last[0]))
1154		nr->last = xstrdup (r->last);
1155	    else
1156	    {
1157		if (baserev && strcmp (r->last, TAG_BASE) == 0)
1158		    nr->last = xstrdup (baserev);
1159		else if (RCS_nodeisbranch (rcs, r->last))
1160		    nr->last = RCS_whatbranch (rcs, r->last);
1161		else
1162		    nr->last = RCS_gettag (rcs, r->last, 1, (int *) NULL);
1163		if (nr->last == NULL && !really_quiet)
1164		{
1165		    error (0, 0, "warning: no revision `%s' in `%s'",
1166			   r->last, rcs->path);
1167		}
1168	    }
1169
1170	    /* Process the revision numbers the same way that rlog
1171               does.  This code is a bit cryptic for my tastes, but
1172               keeping the same implementation as rlog ensures a
1173               certain degree of compatibility.  */
1174	    if (r->first == NULL && nr->last != NULL)
1175	    {
1176		nr->fields = numdots (nr->last) + 1;
1177		if (nr->fields < 2)
1178		    nr->first = xstrdup (".0");
1179		else
1180		{
1181		    char *cp;
1182
1183		    nr->first = xstrdup (nr->last);
1184		    cp = strrchr (nr->first, '.');
1185		    assert (cp);
1186		    strcpy (cp + 1, "0");
1187		}
1188	    }
1189	    else if (r->last == NULL && nr->first != NULL)
1190	    {
1191		nr->fields = numdots (nr->first) + 1;
1192		nr->last = xstrdup (nr->first);
1193		if (nr->fields < 2)
1194		    nr->last[0] = '\0';
1195		else
1196		{
1197		    char *cp;
1198
1199		    cp = strrchr (nr->last, '.');
1200		    assert (cp);
1201		    *cp = '\0';
1202		}
1203	    }
1204	    else if (nr->first == NULL || nr->last == NULL)
1205		nr->fields = 0;
1206	    else if (strcmp (nr->first, nr->last) == 0)
1207		nr->fields = numdots (nr->last) + 1;
1208	    else
1209	    {
1210		int ord;
1211		int dots1 = numdots (nr->first);
1212		int dots2 = numdots (nr->last);
1213		if (dots1 > dots2 || (dots1 == dots2 &&
1214		    version_compare (nr->first, nr->last, dots1 + 1) > 0))
1215		{
1216		    char *tmp = nr->first;
1217		    nr->first = nr->last;
1218		    nr->last = tmp;
1219		    nr->fields = dots2 + 1;
1220		    dots2 = dots1;
1221		    dots1 = nr->fields - 1;
1222		}
1223		else
1224		    nr->fields = dots1 + 1;
1225		dots1 += (nr->fields & 1);
1226		ord = version_compare (nr->first, nr->last, dots1);
1227		if (ord > 0 || (nr->fields > 2 && ord < 0))
1228		{
1229		    error (0, 0,
1230			   "invalid branch or revision pair %s:%s in `%s'",
1231			   r->first, r->last, rcs->path);
1232		    free (nr->first);
1233		    nr->first = NULL;
1234		    free (nr->last);
1235		    nr->last = NULL;
1236		    nr->fields = 0;
1237		}
1238		else
1239		{
1240		    if (nr->fields <= dots2 && (nr->fields & 1))
1241		    {
1242			char *p = xmalloc (strlen (nr->first) + 3);
1243			strcpy (p, nr->first);
1244			strcat (p, ".0");
1245			free (nr->first);
1246			nr->first = p;
1247			++nr->fields;
1248		    }
1249		    while (nr->fields <= dots2)
1250		    {
1251			char *p;
1252			int i;
1253
1254			nr->next = NULL;
1255			*pr = nr;
1256			nr = (struct revlist *) xmalloc (sizeof *nr);
1257			nr->inclusive = 1;
1258			nr->first = xstrdup ((*pr)->last);
1259			nr->last = xstrdup ((*pr)->last);
1260			nr->fields = (*pr)->fields;
1261			p = (*pr)->last;
1262			for (i = 0; i < nr->fields; i++)
1263			    p = strchr (p, '.') + 1;
1264			p[-1] = '\0';
1265			p = strchr (nr->first + (p - (*pr)->last), '.');
1266			if (p != NULL)
1267			{
1268			    *++p = '0';
1269			    *++p = '\0';
1270			    nr->fields += 2;
1271			}
1272			else
1273			    ++nr->fields;
1274			pr = &(*pr)->next;
1275		    }
1276		}
1277	    }
1278	}
1279
1280	nr->next = NULL;
1281	*pr = nr;
1282	pr = &nr->next;
1283    }
1284
1285    /* If the default branch was requested, add a revlist entry for
1286       it.  This is how rlog handles this option.  */
1287    if (default_branch
1288	&& (rcs->head != NULL || rcs->branch != NULL))
1289    {
1290	struct revlist *nr;
1291
1292	nr = (struct revlist *) xmalloc (sizeof *nr);
1293	if (rcs->branch != NULL)
1294	    nr->first = xstrdup (rcs->branch);
1295	else
1296	{
1297	    char *cp;
1298
1299	    nr->first = xstrdup (rcs->head);
1300	    assert (nr->first);
1301	    cp = strrchr (nr->first, '.');
1302	    assert (cp);
1303	    *cp = '\0';
1304	}
1305	nr->last = xstrdup (nr->first);
1306	nr->fields = numdots (nr->first) + 1;
1307	nr->inclusive = 1;
1308
1309	nr->next = NULL;
1310	*pr = nr;
1311    }
1312
1313    return ret;
1314}
1315
1316/*
1317 * Free a revlist created by log_expand_revlist.
1318 */
1319static void
1320log_free_revlist (revlist)
1321    struct revlist *revlist;
1322{
1323    struct revlist *r;
1324
1325    r = revlist;
1326    while (r != NULL)
1327    {
1328	struct revlist *next;
1329
1330	if (r->first != NULL)
1331	    free (r->first);
1332	if (r->last != NULL)
1333	    free (r->last);
1334	next = r->next;
1335	free (r);
1336	r = next;
1337    }
1338}
1339
1340/*
1341 * Return nonzero if a revision should be printed, based on the
1342 * options provided.
1343 */
1344static int
1345log_version_requested (log_data, revlist, rcs, vnode)
1346    struct log_data *log_data;
1347    struct revlist *revlist;
1348    RCSNode *rcs;
1349    RCSVers *vnode;
1350{
1351    /* Handle the list of states from the -s option.  */
1352    if (log_data->statelist != NULL
1353	&& findnode (log_data->statelist, vnode->state) == NULL)
1354    {
1355	return 0;
1356    }
1357
1358    /* Handle the list of authors from the -w option.  */
1359    if (log_data->authorlist != NULL)
1360    {
1361	if (vnode->author != NULL
1362	    && findnode (log_data->authorlist, vnode->author) == NULL)
1363	{
1364	    return 0;
1365	}
1366    }
1367
1368    /* rlog considers all the -d options together when it decides
1369       whether to print a revision, so we must be compatible.  */
1370    if (log_data->datelist != NULL || log_data->singledatelist != NULL)
1371    {
1372	struct datelist *d;
1373
1374	for (d = log_data->datelist; d != NULL; d = d->next)
1375	{
1376	    int cmp;
1377
1378	    cmp = RCS_datecmp (vnode->date, d->start);
1379	    if (cmp > 0 || (cmp == 0 && d->inclusive))
1380	    {
1381		cmp = RCS_datecmp (vnode->date, d->end);
1382		if (cmp < 0 || (cmp == 0 && d->inclusive))
1383		    break;
1384	    }
1385	}
1386
1387	if (d == NULL)
1388	{
1389	    /* Look through the list of specific dates.  We want to
1390	       select the revision with the exact date found in the
1391	       start field.  The commit code ensures that it is
1392	       impossible to check in multiple revisions of a single
1393	       file in a single second, so checking the date this way
1394	       should never select more than one revision.  */
1395	    for (d = log_data->singledatelist; d != NULL; d = d->next)
1396	    {
1397		if (d->start != NULL
1398		    && RCS_datecmp (vnode->date, d->start) == 0)
1399		{
1400		    break;
1401		}
1402	    }
1403
1404	    if (d == NULL)
1405		return 0;
1406	}
1407    }
1408
1409    /* If the -r or -b options were used, REVLIST will be non NULL,
1410       and we print the union of the specified revisions.  */
1411    if (revlist != NULL)
1412    {
1413	char *v;
1414	int vfields;
1415	struct revlist *r;
1416
1417	/* This code is taken from rlog.  */
1418	v = vnode->version;
1419	vfields = numdots (v) + 1;
1420	for (r = revlist; r != NULL; r = r->next)
1421	{
1422	    if (vfields == r->fields + (r->fields & 1) &&
1423		(r->inclusive ? version_compare (v, r->first, r->fields) >= 0 :
1424				version_compare (v, r->first, r->fields) > 0)
1425		    && version_compare (v, r->last, r->fields) <= 0)
1426	    {
1427		return 1;
1428	    }
1429	}
1430
1431	/* If we get here, then the -b and/or the -r option was used,
1432           but did not match this revision, so we reject it.  */
1433
1434	return 0;
1435    }
1436
1437    /* By default, we print all revisions.  */
1438    return 1;
1439}
1440
1441
1442
1443/*
1444 * Output a single symbol.  This is called via walklist.
1445 */
1446/*ARGSUSED*/
1447static int
1448log_symbol (p, closure)
1449    Node *p;
1450    void *closure;
1451{
1452    cvs_output ("\n\t", 2);
1453    cvs_output (p->key, 0);
1454    cvs_output (": ", 2);
1455    cvs_output (p->data, 0);
1456    return 0;
1457}
1458
1459
1460
1461/*
1462 * Count the number of entries on a list.  This is called via walklist.
1463 */
1464/*ARGSUSED*/
1465static int
1466log_count (p, closure)
1467    Node *p;
1468    void *closure;
1469{
1470    return 1;
1471}
1472
1473
1474
1475/*
1476 * Sort out a single date specification by narrowing down the date
1477 * until we find the specific selected revision.
1478 */
1479static int
1480log_fix_singledate (p, closure)
1481    Node *p;
1482    void *closure;
1483{
1484    struct log_data_and_rcs *data = (struct log_data_and_rcs *) closure;
1485    Node *pv;
1486    RCSVers *vnode;
1487    struct datelist *holdsingle, *holddate;
1488    int requested;
1489
1490    pv = findnode (data->rcs->versions, p->key);
1491    if (pv == NULL)
1492	error (1, 0, "missing version `%s' in RCS file `%s'",
1493	       p->key, data->rcs->path);
1494    vnode = pv->data;
1495
1496    /* We are only interested if this revision passes any other tests.
1497       Temporarily clear log_data->singledatelist to avoid confusing
1498       log_version_requested.  We also clear log_data->datelist,
1499       because rlog considers all the -d options together.  We don't
1500       want to reject a revision because it does not match a date pair
1501       if we are going to select it on the basis of the singledate.  */
1502    holdsingle = data->log_data->singledatelist;
1503    data->log_data->singledatelist = NULL;
1504    holddate = data->log_data->datelist;
1505    data->log_data->datelist = NULL;
1506    requested = log_version_requested (data->log_data, data->revlist,
1507				       data->rcs, vnode);
1508    data->log_data->singledatelist = holdsingle;
1509    data->log_data->datelist = holddate;
1510
1511    if (requested)
1512    {
1513	struct datelist *d;
1514
1515	/* For each single date, if this revision is before the
1516	   specified date, but is closer than the previously selected
1517	   revision, select it instead.  */
1518	for (d = data->log_data->singledatelist; d != NULL; d = d->next)
1519	{
1520	    if (RCS_datecmp (vnode->date, d->end) <= 0
1521		&& (d->start == NULL
1522		    || RCS_datecmp (vnode->date, d->start) > 0))
1523	    {
1524		if (d->start != NULL)
1525		    free (d->start);
1526		d->start = xstrdup (vnode->date);
1527	    }
1528	}
1529    }
1530
1531    return 0;
1532}
1533
1534
1535
1536/*
1537 * Count the number of revisions we are going to print.
1538 */
1539static int
1540log_count_print (p, closure)
1541    Node *p;
1542    void *closure;
1543{
1544    struct log_data_and_rcs *data = (struct log_data_and_rcs *) closure;
1545    Node *pv;
1546
1547    pv = findnode (data->rcs->versions, p->key);
1548    if (pv == NULL)
1549	error (1, 0, "missing version `%s' in RCS file `%s'",
1550	       p->key, data->rcs->path);
1551    if (log_version_requested (data->log_data, data->revlist, data->rcs,
1552			       pv->data))
1553	return 1;
1554    else
1555	return 0;
1556}
1557
1558/*
1559 * Print the list of changes, not including the trunk, in reverse
1560 * order for each branch.
1561 */
1562static void
1563log_tree (log_data, revlist, rcs, ver)
1564    struct log_data *log_data;
1565    struct revlist *revlist;
1566    RCSNode *rcs;
1567    const char *ver;
1568{
1569    Node *p;
1570    RCSVers *vnode;
1571
1572    p = findnode (rcs->versions, ver);
1573    if (p == NULL)
1574	error (1, 0, "missing version `%s' in RCS file `%s'",
1575	       ver, rcs->path);
1576    vnode = p->data;
1577    if (vnode->next != NULL)
1578	log_tree (log_data, revlist, rcs, vnode->next);
1579    if (vnode->branches != NULL)
1580    {
1581	Node *head, *branch;
1582
1583	/* We need to do the branches in reverse order.  This breaks
1584           the List abstraction, but so does most of the branch
1585           manipulation in rcs.c.  */
1586	head = vnode->branches->list;
1587	for (branch = head->prev; branch != head; branch = branch->prev)
1588	{
1589	    log_abranch (log_data, revlist, rcs, branch->key);
1590	    log_tree (log_data, revlist, rcs, branch->key);
1591	}
1592    }
1593}
1594
1595/*
1596 * Log the changes for a branch, in reverse order.
1597 */
1598static void
1599log_abranch (log_data, revlist, rcs, ver)
1600    struct log_data *log_data;
1601    struct revlist *revlist;
1602    RCSNode *rcs;
1603    const char *ver;
1604{
1605    Node *p;
1606    RCSVers *vnode;
1607
1608    p = findnode (rcs->versions, ver);
1609    if (p == NULL)
1610	error (1, 0, "missing version `%s' in RCS file `%s'",
1611	       ver, rcs->path);
1612    vnode = p->data;
1613    if (vnode->next != NULL)
1614	log_abranch (log_data, revlist, rcs, vnode->next);
1615    log_version (log_data, revlist, rcs, vnode, 0);
1616}
1617
1618/*
1619 * Print the log output for a single version.
1620 */
1621static void
1622log_version (log_data, revlist, rcs, ver, trunk)
1623    struct log_data *log_data;
1624    struct revlist *revlist;
1625    RCSNode *rcs;
1626    RCSVers *ver;
1627    int trunk;
1628{
1629    Node *p;
1630    int year, mon, mday, hour, min, sec;
1631    char buf[100];
1632    Node *padd, *pdel;
1633
1634    if (! log_version_requested (log_data, revlist, rcs, ver))
1635	return;
1636
1637    cvs_output ("----------------------------\nrevision ", 0);
1638    cvs_output (ver->version, 0);
1639
1640    p = findnode (RCS_getlocks (rcs), ver->version);
1641    if (p != NULL)
1642    {
1643	cvs_output ("\tlocked by: ", 0);
1644	cvs_output (p->data, 0);
1645	cvs_output (";", 1);
1646    }
1647
1648    cvs_output ("\ndate: ", 0);
1649    (void) sscanf (ver->date, SDATEFORM, &year, &mon, &mday, &hour, &min,
1650		   &sec);
1651    if (year < 1900)
1652	year += 1900;
1653    sprintf (buf, "%04d%c%02d%c%02d %02d:%02d:%02d",
1654	     year, datesep, mon, datesep, mday, hour, min, sec);
1655    cvs_output (buf, 0);
1656
1657    cvs_output (";  author: ", 0);
1658    cvs_output (ver->author, 0);
1659
1660    cvs_output (";  state: ", 0);
1661    cvs_output (ver->state, 0);
1662    cvs_output (";", 1);
1663
1664    if (! trunk)
1665    {
1666	padd = findnode (ver->other, ";add");
1667	pdel = findnode (ver->other, ";delete");
1668    }
1669    else if (ver->next == NULL)
1670    {
1671	padd = NULL;
1672	pdel = NULL;
1673    }
1674    else
1675    {
1676	Node *nextp;
1677	RCSVers *nextver;
1678
1679	nextp = findnode (rcs->versions, ver->next);
1680	if (nextp == NULL)
1681	    error (1, 0, "missing version `%s' in `%s'", ver->next,
1682		   rcs->path);
1683	nextver = nextp->data;
1684	pdel = findnode (nextver->other, ";add");
1685	padd = findnode (nextver->other, ";delete");
1686    }
1687
1688    if (padd != NULL)
1689    {
1690	assert (pdel);
1691	cvs_output ("  lines: +", 0);
1692	cvs_output (padd->data, 0);
1693	cvs_output (" -", 2);
1694	cvs_output (pdel->data, 0);
1695    }
1696
1697    if (ver->branches != NULL)
1698    {
1699	cvs_output ("\nbranches:", 0);
1700	walklist (ver->branches, log_branch, (void *) NULL);
1701    }
1702
1703    cvs_output ("\n", 1);
1704
1705    p = findnode (ver->other, "log");
1706    /* The p->date == NULL case is the normal one for an empty log
1707       message (rcs-14 in sanity.sh).  I don't think the case where
1708       p->data is "" can happen (getrcskey in rcs.c checks for an
1709       empty string and set the value to NULL in that case).  My guess
1710       would be the p == NULL case would mean an RCS file which was
1711       missing the "log" keyword (which is illegal according to
1712       rcsfile.5).  */
1713    if (p == NULL || p->data == NULL || *(char *)p->data == '\0')
1714	cvs_output ("*** empty log message ***\n", 0);
1715    else
1716    {
1717	/* FIXME: Technically, the log message could contain a null
1718           byte.  */
1719	cvs_output (p->data, 0);
1720	if (((char *)p->data)[strlen (p->data) - 1] != '\n')
1721	    cvs_output ("\n", 1);
1722    }
1723}
1724
1725/*
1726 * Output a branch version.  This is called via walklist.
1727 */
1728/*ARGSUSED*/
1729static int
1730log_branch (p, closure)
1731    Node *p;
1732    void *closure;
1733{
1734    cvs_output ("  ", 2);
1735    if ((numdots (p->key) & 1) == 0)
1736	cvs_output (p->key, 0);
1737    else
1738    {
1739	char *f, *cp;
1740
1741	f = xstrdup (p->key);
1742	cp = strrchr (f, '.');
1743	*cp = '\0';
1744	cvs_output (f, 0);
1745	free (f);
1746    }
1747    cvs_output (";", 1);
1748    return 0;
1749}
1750
1751/*
1752 * Print a warm fuzzy message
1753 */
1754/* ARGSUSED */
1755static Dtype
1756log_dirproc (callerdat, dir, repository, update_dir, entries)
1757    void *callerdat;
1758    const char *dir;
1759    const char *repository;
1760    const char *update_dir;
1761    List *entries;
1762{
1763    if (!isdir (dir))
1764	return (R_SKIP_ALL);
1765
1766    if (!quiet)
1767	error (0, 0, "Logging %s", update_dir);
1768    return (R_PROCESS);
1769}
1770
1771/*
1772 * Compare versions.  This is taken from RCS compartial.
1773 */
1774static int
1775version_compare (v1, v2, len)
1776    const char *v1;
1777    const char *v2;
1778    int len;
1779{
1780    while (1)
1781    {
1782	int d1, d2, r;
1783
1784	if (*v1 == '\0')
1785	    return 1;
1786	if (*v2 == '\0')
1787	    return -1;
1788
1789	while (*v1 == '0')
1790	    ++v1;
1791	for (d1 = 0; isdigit ((unsigned char) v1[d1]); ++d1)
1792	    ;
1793
1794	while (*v2 == '0')
1795	    ++v2;
1796	for (d2 = 0; isdigit ((unsigned char) v2[d2]); ++d2)
1797	    ;
1798
1799	if (d1 != d2)
1800	    return d1 < d2 ? -1 : 1;
1801
1802	r = memcmp (v1, v2, d1);
1803	if (r != 0)
1804	    return r;
1805
1806	--len;
1807	if (len == 0)
1808	    return 0;
1809
1810	v1 += d1;
1811	v2 += d1;
1812
1813	if (*v1 == '.')
1814	    ++v1;
1815	if (*v2 == '.')
1816	    ++v2;
1817    }
1818}
1819