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