1 /* Print log messages and other information about RCS files.
2 
3    Copyright (C) 2010-2020 Thien-Thi Nguyen
4    Copyright (C) 1990, 1991, 1992, 1993, 1994, 1995 Paul Eggert
5    Copyright (C) 1982, 1988, 1989 Walter Tichy
6 
7    This file is part of GNU RCS.
8 
9    GNU RCS is free software: you can redistribute it and/or modify it
10    under the terms of the GNU General Public License as published by
11    the Free Software Foundation, either version 3 of the License, or
12    (at your option) any later version.
13 
14    GNU RCS is distributed in the hope that it will be useful,
15    but WITHOUT ANY WARRANTY; without even the implied warranty
16    of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17    See the GNU General Public License for more details.
18 
19    You should have received a copy of the GNU General Public License
20    along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 */
22 
23 #include "base.h"
24 #include <string.h>
25 #include <stdlib.h>
26 #include <errno.h>           /* temporary, to support ‘read_positive_integer’,
27                                 before it and ‘compute_a_d’ move to grok.  */
28 #include "rlog.help"
29 #include "b-complain.h"
30 #include "b-divvy.h"
31 #include "b-esds.h"
32 #include "b-excwho.h"
33 #include "b-fb.h"
34 #include "b-fro.h"
35 
36 static char const ks_delims[] = ", \t\n;";
37 
38 struct revrange
39 {
40   char const *beg;
41   char const *end;
42   int nfield;
43 };
44 
45 struct daterange
46 {
47   char beg[DATESIZE];
48   char end[DATESIZE];
49   bool open_end;
50 };
51 
52 struct date_selection
53 {
54   struct link *in;                      /* start - end */
55   struct link *by;                      /* end only */
56 };
57 
58 struct criteria
59 {
60   struct link *revs, *actual;
61   /* On the first pass (option processing), push onto ‘.revs’.
62      After grokking, walk ‘.revs’ and push onto ‘.actual’.  */
63 
64   struct link *authors;
65   struct link *lockers;
66   struct link *states;
67 };
68 
69 #define PUSH(x,ls)  ls = prepend (x, ls, PLEXUS)
70 
71 static bool
tokenize(char * argv,struct link ** chain)72 tokenize (char *argv, struct link **chain)
73 /* Tokenize (destructively) ‘argv’ using ‘ks_delims’.
74    Push tokens on ‘chain’ (LIFO).
75    Return true if any tokens were parsed.  */
76 {
77   struct link *before = *chain;
78   char *s, *save, *token;
79 
80   for (s = argv;
81        (token = strtok_r (s, ks_delims, &save));
82        s = NULL)
83     PUSH (token, *chain);
84 
85   return *chain != before;
86 }
87 
88 static void
cleanup(int * exitstatus)89 cleanup (int *exitstatus)
90 {
91   if (FLOW (erroneous))
92     *exitstatus = exit_failure;
93   fro_zclose (&FLOW (from));
94 }
95 
96 static void
getlocker(char * argv,struct criteria * criteria)97 getlocker (char *argv, struct criteria *criteria)
98 /* Parse logins from ‘argv’; store in ‘criteria->lockers’.  */
99 {
100   tokenize (argv, &criteria->lockers);
101 }
102 
103 /* TODO: Move into b-fro.c, since counting lines is not sensitive to
104    "@@" presence (we can work in-place, avoiding ‘string_from_atat’).  */
105 
106 static long
read_positive_integer(char const ** p)107 read_positive_integer (char const **p)
108 {
109   long rv;
110   char *end;
111 
112   errno = 0;
113   if (1 > (rv = strtol (*p, &end, 10)))
114     RFATAL ("non-positive integer");
115   if (ERANGE == errno)
116     RFATAL ("bad integer");
117   *p = end;
118   return rv;
119 }
120 
121 static void
count_a_d(long * a,long * d,struct atat * edits)122 count_a_d (long *a, long *d, struct atat *edits)
123 {
124   struct cbuf s = string_from_atat (SINGLE, edits);
125   char const *end = s.string + s.size;
126   long *totals = zlloc (SINGLE, 2 * sizeof (long));
127 
128   for (char const *p = s.string; p < end; p++)
129     {
130       bool add = ('a' == *p++);
131       long count;
132 
133       /* Skip the line number.  */
134       p = strchr (p, ' ');
135       count = read_positive_integer (&p);
136 
137       totals[add] += count;
138       if (add)
139         /* Ignore the actual lines.  */
140         while (count--)
141           {
142             size_t remaining = end - p;
143 
144             if (! (p = memchr (++p, '\n', remaining)))
145               /* No final newline; skip out.  */
146               goto done;
147           }
148     }
149  done:
150   *a = totals[1];
151   *d = totals[0];
152   brush_off (SINGLE, totals);
153 }
154 
155 static void
putadelta(struct delta const * node,struct delta const * editscript,char const * insDelFormat)156 putadelta (struct delta const *node, struct delta const *editscript,
157            char const *insDelFormat)
158 /* Print delta ‘node’.  ‘editscript’ indicates where the editscript is
159    stored; it equals ‘node’ if this node is not in trunk.  */
160 {
161   FILE *out = stdout;
162   char datebuf[FULLDATESIZE];
163   bool pre5 = BE (version) < VERSION (5);
164   struct atat *log;
165 
166   aprintf (out, "----------------------------\nrevision %s%s",
167            node->num,
168            pre5 ? "        " : "");
169   if (node->lockedby)
170     aprintf (out, pre5 + "\tlocked by: %s;", node->lockedby);
171 
172   aprintf (out, "\ndate: %s;  author: %s;  state: %s;",
173            date2str (node->date, datebuf), node->author, node->state);
174 
175   if (editscript && editscript != REPO (tip))
176     {
177       bool trunk = node != editscript;
178       long a, d;
179 
180       count_a_d (trunk ? &d : &a,
181                  trunk ? &a : &d,
182                  editscript->text);
183       aprintf (out, insDelFormat, a, d);
184     }
185 
186   if (node->branches)
187     {
188       aputs ("\nbranches:", out);
189       for (struct wlink *ls = node->branches; ls; ls = ls->next)
190         {
191           struct delta *delta = ls->entry;
192 
193           aprintf (out, "  %s;", BRANCHNO (delta->num));
194         }
195     }
196 
197   if (node->commitid)
198     aprintf (out, "%s commitid: %s",
199              editscript ? ";" : "",
200              node->commitid);
201 
202   newline (out);
203   if ((log = node->log)
204       && log->beg + 1 < ATAT_END (log))
205     atat_display (out, log, true);
206   else
207     awrite (EMPTYLOG "\n", sizeof (EMPTYLOG), out);
208 }
209 
210 static void
putrunk(const char * insDelFormat)211 putrunk (const char *insDelFormat)
212 /* Print revisions chosen, which are in trunk.  */
213 {
214   for (struct delta const *ptr = REPO (tip); ptr; ptr = ptr->ilk)
215     if (ptr->selector)
216       putadelta (ptr, ptr->ilk, insDelFormat);
217 }
218 
219 static struct delta const *putforest (struct wlink const *, const char *);
220 
221 static void
putree(struct delta const * root,const char * insDelFormat)222 putree (struct delta const *root, const char *insDelFormat)
223 /* Print delta tree from ‘root’ (not including trunk)
224    in reverse order on each branch.  */
225 {
226   while (root)
227     if (! root->branches)
228       root = root->ilk;
229     else
230       {
231         /* FIXME: This recurses deeply in the worst case.  */
232         putree (root->ilk, insDelFormat);
233         root = putforest (root->branches, insDelFormat);
234       }
235 }
236 
237 static void
putabranch(struct delta const * root,const char * insDelFormat)238 putabranch (struct delta const *root, const char *insDelFormat)
239 /* Print one branch from ‘root’.  */
240 {
241   while (!root->selector)
242     {
243       root = root->ilk;
244       if (!root)
245         return;
246     }
247 
248   /* FIXME: This recurses deeply in the worst case.  */
249   if (root->ilk)
250     putabranch (root->ilk, insDelFormat);
251 
252   putadelta (root, root, insDelFormat);
253 }
254 
255 static struct delta const *
putforest(struct wlink const * branchroot,const char * insDelFormat)256 putforest (struct wlink const *branchroot, const char *insDelFormat)
257 /* Print branches that have the same direct ancestor ‘branchroot’.  */
258 {
259   /* FIXME: This recurses deeply in the worst case.  */
260   if (branchroot->next)
261     putforest (branchroot->next, insDelFormat);
262 
263   putabranch (branchroot->entry, insDelFormat);
264   return branchroot->entry;
265 }
266 
267 static bool
extractdelta(struct delta const * pdelta,bool lockflag,struct criteria * criteria)268 extractdelta (struct delta const *pdelta, bool lockflag,
269               struct criteria *criteria)
270 /* Return true if ‘pdelta’ matches the selection critera.  */
271 {
272   struct link const *pstate;
273   struct link const *pauthor;
274   int length;
275 
276   /* Only certain authors wanted.  */
277   if ((pauthor = criteria->authors))
278     while (STR_DIFF (pauthor->entry, pdelta->author))
279       if (!(pauthor = pauthor->next))
280         return false;
281   /* Only certain states wanted.  */
282   if ((pstate = criteria->states))
283     while (STR_DIFF (pstate->entry, pdelta->state))
284       if (!(pstate = pstate->next))
285         return false;
286   /* Only locked revisions wanted.  */
287   if (lockflag && !lock_on (pdelta))
288     return false;
289   /* Only certain revs or branches wanted.  */
290   for (struct link *ls = criteria->actual; ls;)
291     {
292       struct revrange const *rr = ls->entry;
293 
294       length = rr->nfield;
295       if (countnumflds (pdelta->num) == length + ODDP (length)
296           && 0 <= compartial (pdelta->num, rr->beg, length)
297           && 0 <= compartial (rr->end, pdelta->num, length))
298         break;
299       if (! (ls = ls->next))
300         return false;
301     }
302   return true;
303 }
304 
305 static void
exttree(struct delta * root,bool lockflag,struct criteria * criteria)306 exttree (struct delta *root, bool lockflag,
307          struct criteria *criteria)
308 /* Select revisions, starting with ‘root’.  */
309 {
310   while (root)
311     {
312       root->selector = extractdelta (root, lockflag, criteria);
313       root->pretty_log.string = NULL;
314 
315       if (! root->branches)
316         root = root->ilk;
317       else
318         {
319           /* FIXME: This recurses deeply in the worst case.  */
320           struct wlink *ls;
321           exttree (root->ilk, lockflag, criteria);
322           for (ls = root->branches; ls->next; ls = ls->next)
323             exttree (ls->entry, lockflag, criteria);
324           root = ls->entry;
325         }
326     }
327 }
328 
329 static void
getauthor(char * argv,struct criteria * criteria)330 getauthor (char *argv, struct criteria *criteria)
331 /* Parse logins from ‘argv’; store in ‘criteria->authors’.
332    If none specified, default to the user login.  */
333 {
334   if (! tokenize (argv, &criteria->authors))
335     PUSH (getusername (false),
336           criteria->authors);
337 }
338 
339 static void
getstate(char * argv,struct criteria * criteria)340 getstate (char *argv, struct criteria *criteria)
341 /* Parse states from ‘argv’; store in ‘criteria->states’.
342    Signal error if none specified.  */
343 {
344   if (! tokenize (argv, &criteria->states))
345     PERR ("missing state attributes after -s option");
346 }
347 
348 static void
trunclocks(struct criteria * criteria)349 trunclocks (struct criteria *criteria)
350 /* Truncate the list of locks to those that are held by the id's on
351    ‘criteria->lockers’.  Do not truncate if ‘criteria->lockers’ empty.  */
352 {
353   struct link const *plocker;
354   struct link box, *tp;
355 
356   if (!criteria->lockers)
357     return;
358 
359   /* Shorten locks to those contained in ‘criteria->lockers’.  */
360   for (box.next = GROK (locks), tp = &box; tp->next;)
361     {
362       struct rcslock const *rl = tp->next->entry;
363 
364       for (plocker = criteria->lockers;;)
365         if (STR_SAME (plocker->entry, rl->login))
366           {
367             tp = tp->next;
368             break;
369           }
370         else if (!(plocker = plocker->next))
371           {
372             tp->next = tp->next->next;
373             GROK (locks) = box.next;
374             break;
375           }
376     }
377 }
378 
379 static void
recentdate(struct delta const * root,struct daterange * r)380 recentdate (struct delta const *root, struct daterange *r)
381 /* Find the delta that is closest to the cutoff date ‘pd’ among the
382    revisions selected by ‘exttree’.  Successively narrow down the
383    interval given by ‘pd’, and set the ‘strtdate’ of ‘pd’ to the date
384    of the selected delta.  */
385 {
386   while (root)
387     {
388       if (root->selector
389           && !DATE_LT (root->date, r->beg)
390           && !DATE_GT (root->date, r->end))
391         {
392           strncpy (r->beg, root->date, DATESIZE);
393           r->beg[DATESIZE - 1] = '\0';
394         }
395 
396       struct wlink *ls = root->branches;
397       if (!ls)
398         root = root->ilk;
399       else
400         {
401           /* FIXME: This recurses deeply in the worst case.  */
402           for (; ls->next; ls = ls->next)
403             recentdate (ls->entry, r);
404           root = ls->entry;
405         }
406     }
407 }
408 
409 static size_t
extdate(struct delta * root,struct date_selection * datesel)410 extdate (struct delta *root, struct date_selection *datesel)
411 /* Select revisions which are in the date range specified in ‘datesel->by’
412    and ‘datesel->in’, starting at ‘root’.  Return number of revisions
413    selected, including those already selected.  */
414 {
415   size_t revno = 0;
416 
417   for (; root; root = root->ilk)
418     {
419       if (datesel->in || datesel->by)
420         {
421           struct daterange const *r;
422           bool open_end, sel = false;
423 
424           for (struct link *ls = datesel->in; ls; ls = ls->next)
425             {
426               r = ls->entry;
427               open_end = r->open_end;
428               if ((sel = ((!r->beg[0]
429                            || (open_end
430                                ? DATE_LT (r->beg, root->date)
431                                : !DATE_GT (r->beg, root->date)))
432                           &&
433                           (!r->end[0]
434                            || (open_end
435                                ? DATE_LT (root->date, r->end)
436                                : !DATE_GT (root->date, r->end))))))
437                 break;
438             }
439           if (!sel)
440             {
441               for (struct link *ls = datesel->by; ls; ls = ls->next)
442                 {
443                   r = ls->entry;
444                   if ((sel = DATE_EQ (root->date, r->beg)))
445                     break;
446                 }
447               if (!sel)
448                 root->selector = false;
449             }
450         }
451 
452       revno += root->selector;
453 
454       /* FIXME: This recurses deeply in the worst case.  */
455       for (struct wlink *ls = root->branches; ls; ls = ls->next)
456         revno += extdate (ls->entry, datesel);
457     }
458 
459   return revno;
460 }
461 
462 static void
getdatepair(char * argv,struct date_selection * datesel)463 getdatepair (char *argv, struct date_selection *datesel)
464 /* Get time range from command line and store in ‘datesel->in’ if
465    a time range specified or in ‘datesel->by’ if a time spot specified.  */
466 {
467   register char c;
468   struct daterange *r;
469   char const *rawdate;
470   bool switchflag;
471 
472   argv--;
473   while ((c = *++argv) == ',' || c == ' ' || c == '\t' || c == '\n'
474          || c == ';')
475     continue;
476   if (c == '\0')
477     {
478       PERR ("missing date/time after -d");
479       return;
480     }
481 
482   while (c != '\0')
483     {
484       switchflag = false;
485       r = ZLLOC (1, struct daterange);
486       if (c == '<')                     /* <DATE */
487         {
488           c = *++argv;
489           if (!(r->open_end = c != '='))
490             c = *++argv;
491           r->beg[0] = '\0';
492         }
493       else if (c == '>')                /* >DATE */
494         {
495           c = *++argv;
496           if (!(r->open_end = c != '='))
497             c = *++argv;
498           r->end[0] = '\0';
499           switchflag = true;
500         }
501       else
502         {
503           rawdate = argv;
504           while (c != '<' && c != '>' && c != ';' && c != '\0')
505             c = *++argv;
506           *argv = '\0';
507           if (c == '>')
508             switchflag = true;
509           str2date (rawdate, switchflag ? r->end : r->beg);
510           if (c == ';' || c == '\0')    /* DATE */
511             {
512               memcpy (r->end, r->beg, DATESIZE);
513               PUSH (r, datesel->by);
514               goto end;
515             }
516           else                   /* DATE< or DATE> (see ‘switchflag’) */
517             {
518               bool eq = argv[1] == '=';
519 
520               r->open_end = !eq;
521               argv += eq;
522               while ((c = *++argv) == ' ' || c == '\t' || c == '\n')
523                 continue;
524               if (c == ';' || c == '\0')
525                 {
526                   /* Second date missing.  */
527                   (switchflag ? r->beg : r->end)[0] = '\0';
528                   PUSH (r, datesel->in);
529                   goto end;
530                 }
531             }
532         }
533       rawdate = argv;
534       while (c != '>' && c != '<' && c != ';' && c != '\0')
535         c = *++argv;
536       *argv = '\0';
537       str2date (rawdate, switchflag ? r->beg : r->end);
538       PUSH (r, datesel->in);
539     end:
540       if (BE (version) < VERSION (5))
541         r->open_end = false;
542       if (c == '\0')
543         return;
544       while ((c = *++argv) == ';' || c == ' ' || c == '\t' || c == '\n')
545         continue;
546     }
547 }
548 
549 static bool
checkrevpair(char const * num1,char const * num2)550 checkrevpair (char const *num1, char const *num2)
551 /* Check whether ‘num1’, ‘num2’ are a legal pair, i.e.
552    only the last field differs and have same number of
553    fields (if length <= 2, may be different if first field).  */
554 {
555   int length = countnumflds (num1);
556 
557   if (countnumflds (num2) != length
558       || (2 < length && compartial (num1, num2, length - 1) != 0))
559     {
560       RERR ("invalid branch or revision pair %s : %s", num1, num2);
561       return false;
562     }
563   return true;
564 }
565 
566 #define ZERODATE  "0.0.0.0.0.0"
567 
568 static bool
getnumericrev(bool branchflag,struct criteria * criteria)569 getnumericrev (bool branchflag, struct criteria *criteria)
570 /* Get the numeric name of revisions stored in ‘criteria->revs’; store
571    them in ‘criteria->actual’.  If ‘branchflag’, also add default branch.  */
572 {
573   struct link *ls;
574   struct revrange *rr;
575   int n;
576   struct cbuf s, e;
577   char const *lrev;
578   struct cbuf const *rstart, *rend;
579   struct delta *tip = REPO (tip);
580   char const *defbr = GROK (branch);
581 
582   criteria->actual = NULL;
583   for (ls = criteria->revs; ls; ls = ls->next)
584     {
585       struct revrange const *from = ls->entry;
586 
587       n = 0;
588       rstart = &s;
589       rend = &e;
590 
591       switch (from->nfield)
592         {
593 
594         case 1:                         /* -rREV */
595           if (!fully_numeric_no_k (&s, from->beg))
596             goto freebufs;
597           rend = &s;
598           n = countnumflds (s.string);
599           if (!n && (lrev = tiprev ()))
600             {
601               s.string = lrev;
602               n = countnumflds (lrev);
603             }
604           break;
605 
606         case 2:                         /* -rREV: */
607           if (!fully_numeric_no_k (&s, from->beg))
608             goto freebufs;
609           e.string = (2 > (n = countnumflds (s.string)))
610             ? ""
611             : SHSNIP (&e.size, s.string, strrchr (s.string, '.'));
612           break;
613 
614         case 3:                         /* -r:REV */
615           if (!fully_numeric_no_k (&e, from->end))
616             goto freebufs;
617           if ((n = countnumflds (e.string)) < 2)
618             s.string = ".0";
619           else
620             {
621               SHACCR (e.string, strrchr (e.string, '.'));
622               accf (PLEXUS, ".0");
623               s.string = SHSTR (&s.size);
624             }
625           break;
626 
627         default:                        /* -rREV1:REV2 */
628           if (!(fully_numeric_no_k (&s, from->beg)
629                 && fully_numeric_no_k (&e, from->end)
630                 && checkrevpair (s.string, e.string)))
631             goto freebufs;
632           n = countnumflds (s.string);
633           /* Swap if out of order.  */
634           if (compartial (s.string, e.string, n) > 0)
635             {
636               rstart = &e;
637               rend = &s;
638             }
639           break;
640         }
641 
642       if (n)
643         {
644           rr = FALLOC (struct revrange);
645           rr->nfield = n;
646           rr->beg = rstart->string;
647           rr->end = rend->string;
648           PUSH (rr, criteria->actual);
649         }
650     }
651   /* Now take care of ‘branchflag’.  */
652   if (branchflag && (defbr || tip))
653     {
654       rr = FALLOC (struct revrange);
655       rr->beg = rr->end = defbr
656         ? defbr
657         : TAKE (1, tip->num);
658       rr->nfield = countnumflds (rr->beg);
659       PUSH (rr, criteria->actual);
660     }
661 
662 freebufs:
663   return !ls;
664 }
665 
666 static void
putrevpairs(char const * b,char const * e,bool sawsep,void * data)667 putrevpairs (char const *b, char const *e, bool sawsep, void *data)
668 /* Store a revision or branch range into ‘creteria->revs’.  */
669 {
670   struct criteria *criteria = data;
671   struct revrange *rr = ZLLOC (1, struct revrange);
672 
673   rr->beg = b;
674   rr->end = e;
675   rr->nfield = (!sawsep
676                 ? 1                     /* -rREV */
677                 : (!e[0]
678                    ? 2                  /* -rREV: */
679                    : (!b[0]
680                       ? 3               /* -r:REV */
681                       : 4)));           /* -rREV1:REV2 */
682   PUSH (rr, criteria->revs);
683 }
684 
685 DECLARE_PROGRAM (rlog, TYAG_IMMEDIATE);
686 
687 static int
rlog_main(const char * cmd,int argc,char ** argv)688 rlog_main (const char *cmd, int argc, char **argv)
689 {
690   int exitstatus = EXIT_SUCCESS;
691   bool branchflag = false;
692   bool lockflag = false;
693   struct date_selection datesel = { .in = NULL, .by = NULL };
694   struct criteria criteria =
695     {
696       .revs = NULL,
697       .authors = NULL,
698       .lockers = NULL,
699       .states = NULL
700     };
701   char const *insDelFormat;
702   FILE *out;
703   char *a, **newargv;
704   char const *accessListString, *accessFormat;
705   char const *headFormat, *symbolFormat;
706   bool descflag, selectflag;
707   bool onlylockflag;                   /* print only files with locks */
708   bool onlyRCSflag;                    /* print only RCS filename */
709   bool pre5;
710   bool shownames;
711   size_t revno;
712 
713   CHECK_HV (cmd);
714   gnurcs_init (&program);
715 
716   descflag = selectflag = shownames = true;
717   onlylockflag = onlyRCSflag = false;
718   out = stdout;
719 
720   argc = getRCSINIT (argc, argv, &newargv);
721   argv = newargv;
722   while (a = *++argv, 0 < --argc && *a++ == '-')
723     {
724       switch (*a++)
725         {
726 
727         case 'L':
728           onlylockflag = true;
729           break;
730 
731         case 'N':
732           shownames = false;
733           break;
734 
735         case 'R':
736           onlyRCSflag = true;
737           break;
738 
739         case 'l':
740           lockflag = true;
741           getlocker (a, &criteria);
742           break;
743 
744         case 'b':
745           branchflag = true;
746           break;
747 
748         case 'r':
749           parse_revpairs ('r', a, &criteria, putrevpairs);
750           break;
751 
752         case 'd':
753           getdatepair (a, &datesel);
754           break;
755 
756         case 's':
757           getstate (a, &criteria);
758           break;
759 
760         case 'w':
761           getauthor (a, &criteria);
762           break;
763 
764         case 'h':
765           descflag = false;
766           break;
767 
768         case 't':
769           selectflag = false;
770           break;
771 
772         case 'q':
773           /* This has no effect; it's here for consistency.  */
774           BE (quiet) = true;
775           break;
776 
777         case 'x':
778           BE (pe) = a;
779           break;
780 
781         case 'z':
782           zone_set (a);
783           break;
784 
785         case 'T':
786           /* Ignore -T, so that RCSINIT can contain -T.  */
787           if (*a)
788             goto unknown;
789           break;
790 
791         case 'V':
792           setRCSversion (*argv);
793           break;
794 
795         default:
796         unknown:
797           bad_option (*argv);
798         };
799     }
800   /* (End of option processing.)  */
801 
802   if (!(descflag | selectflag))
803     {
804       PWARN ("-t overrides -h.");
805       descflag = true;
806     }
807 
808   pre5 = BE (version) < VERSION (5);
809   if (pre5)
810     {
811       accessListString = "\naccess list:   ";
812       accessFormat = "  %s";
813       headFormat =
814         "\nRCS file:        %s;   Working file:    %s\nhead:           %s%s\nbranch:         %s%s\nlocks:         ";
815       insDelFormat = "  lines added/del: %ld/%ld";
816       symbolFormat = "  %s: %s;";
817     }
818   else
819     {
820       accessListString = "\naccess list:";
821       accessFormat = "\n\t%s";
822       headFormat =
823         "\nRCS file: %s\nWorking file: %s\nhead:%s%s\nbranch:%s%s\nlocks:%s";
824       insDelFormat = "  lines: +%ld -%ld";
825       symbolFormat = "\n\t%s: %s";
826     }
827 
828   /* Now handle all filenames.  */
829   if (FLOW (erroneous))
830     cleanup (&exitstatus);
831   else if (argc < 1)
832     PFATAL ("no input file");
833   else
834     for (; 0 < argc; cleanup (&exitstatus), ++argv, --argc)
835       {
836         char const *repo_filename;
837         struct delta *tip;
838         char const *defbr;
839         bool strictly_locking;
840         int kws;
841         struct link *locks;
842 
843         ffree ();
844 
845         if (pairnames (argc, argv, rcsreadopen, true, false) <= 0)
846           continue;
847 
848         /* ‘REPO (filename)’ contains the name of the RCS file,
849            and ‘FLOW (from)’ the file descriptor;
850            ‘MANI (filename)’ contains the name of the working file.  */
851         repo_filename = REPO (filename);
852         tip = REPO (tip);
853         defbr = GROK (branch);
854         locks = GROK (locks);
855         strictly_locking = BE (strictly_locking);
856         kws = BE (kws);
857 
858         /* Keep only those locks given by ‘-l’.  */
859         if (lockflag)
860           trunclocks (&criteria);
861 
862         /* Do nothing if ‘-L’ is given and there are no locks.  */
863         if (onlylockflag && !locks)
864           continue;
865 
866         if (onlyRCSflag)
867           {
868             aprintf (out, "%s\n", repo_filename);
869             continue;
870           }
871 
872         if (!getnumericrev (branchflag, &criteria))
873           continue;
874 
875         /* Print RCS filename, working filename and optional
876            administrative information.  Could use ‘getfullRCSname’
877            here, but that is very slow.  */
878         aprintf (out, headFormat, repo_filename, MANI (filename),
879                  tip ? " " : "",
880                  tip ? tip->num : "",
881                  defbr ? " " : "",
882                  defbr ? defbr : "",
883                  strictly_locking ? " strict" : "");
884         format_locks (out, symbolFormat);
885         if (strictly_locking && pre5)
886           aputs ("  ;  strict" + (locks ? 3 : 0), out);
887 
888         /* Print access list.  */
889         aputs (accessListString, out);
890         for (struct link *ls = GROK (access); ls; ls = ls->next)
891           aprintf (out, accessFormat, ls->entry);
892 
893         if (shownames)
894           {
895             /* Print symbolic names.  */
896             aputs ("\nsymbolic names:", out);
897             format_assocs (out, symbolFormat);
898           }
899         if (pre5)
900           {
901             aputs ("\ncomment leader:  \"", out);
902             awrite (REPO (log_lead).string, REPO (log_lead).size, out);
903             afputc ('\"', out);
904           }
905         if (!pre5 || kws != kwsub_kv)
906           aprintf (out, "\nkeyword substitution: %s", kwsub_string (kws));
907 
908         aprintf (out, "\ntotal revisions: %zu", GROK (deltas_count));
909 
910         revno = 0;
911 
912         if (tip && selectflag & descflag)
913           {
914             exttree (tip, lockflag, &criteria);
915 
916             /* Get most recently date of the dates pointed by ‘duelst’.  */
917             for (struct link *ls = datesel.by; ls; ls = ls->next)
918               {
919                 struct daterange const *incomplete = ls->entry;
920                 struct daterange *r = ZLLOC (1, struct daterange);
921 
922                 /* Couldn't this have been done before?  --ttn  */
923                 *r = *incomplete;
924                 memcpy (r->beg, ZERODATE, sizeof ZERODATE);
925                 ls->entry = r;
926                 recentdate (tip, r);
927               }
928 
929             revno = extdate (tip, &datesel);
930 
931             aprintf (out, ";\tselected revisions: %zu", revno);
932           }
933 
934         newline (out);
935         if (descflag)
936           {
937             struct atat *desc = GROK (desc);
938 
939             aputs ("description:\n", out);
940             atat_display (out, desc, true);
941           }
942         if (revno)
943           {
944             putrunk (insDelFormat);
945             putree (tip, insDelFormat);
946           }
947         aputs (equal_line, out);
948       }
949   Ozclose (&out);
950   gnurcs_goodbye ();
951   return exitstatus;
952 }
953 
954 static const uint8_t rlog_aka[10] =
955 {
956   2 /* count */,
957   3,'l','o','g',
958   4,'r','l','o','g'
959 };
960 
961 YET_ANOTHER_COMMAND (rlog);
962 
963 /*:help
964 [options] file ...
965 Options:
966   -L            Ignore RCS files with no locks set.
967   -R            Print the RCS file name only.
968   -h            Print only the "header" information.
969   -t            Like -h, but also include the description.
970   -N            Omit symbolic names.
971   -b            Select the default branch.
972   -dDATES       Select revisions in the range DATES, with spec:
973                   D      -- single revision D or earlier
974                   D1<D2  -- between D1 and D2, exclusive
975                   D2>D1  -- likewise
976                   <D, D> -- before D
977                   >D, D< -- after D
978                 Use <= or >= to make ranges inclusive; DATES
979                 may also be a list of semicolon-separated specs.
980   -l[WHO]       Select revisions locked by WHO (comma-separated list)
981                 only, or by anyone if WHO is omitted.
982   -r[REVS]      Select revisions in REVS, a comma-separated list of
983                 range specs, one of: REV, REV:, :REV, REV1:REV2
984   -sSTATES      Select revisions with state in STATES (comma-separated list).
985   -w[WHO]       Select revisions checked in by WHO (comma-separated list),
986                 or by the user if WHO is omitted.
987   -T            No effect; included for compatibility with other commands.
988   -V            Obsolete; do not use.
989   -VN           Emulate RCS version N.
990   -xSUFF        Specify SUFF as a slash-separated list of suffixes
991                 used to identify RCS file names.
992   -zZONE        Specify date output format in keyword-substitution.
993   -q            No effect, included for consistency with other commands.
994 */
995 
996 /* rlog.c ends here */
997