Initial revision
[alioth/cvs.git] / src / history.c
1 /*
2  * Copyright (C) 1994-2005 The Free Software Foundation, Inc.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2, or (at your option)
7  * any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  */
14
15 /* **************** History of Users and Module ****************
16  *
17  * LOGGING:  Append record to "${CVSROOT}/CVSROOTADM/CVSROOTADM_HISTORY".
18  *
19  * On For each Tag, Add, Checkout, Commit, Update or Release command,
20  * one line of text is written to a History log.
21  *
22  *      X date | user | CurDir | special | rev(s) | argument '\n'
23  *
24  * where: [The spaces in the example line above are not in the history file.]
25  *
26  *  X           is a single character showing the type of event:
27  *              T       "Tag" cmd.
28  *              O       "Checkout" cmd.
29  *              E       "Export" cmd.
30  *              F       "Release" cmd.
31  *              W       "Update" cmd - No User file, Remove from Entries file.
32  *              U       "Update" cmd - File was checked out over User file.
33  *              P       "Update" cmd - User file was patched.
34  *              G       "Update" cmd - File was merged successfully.
35  *              C       "Update" cmd - File was merged and shows overlaps.
36  *              M       "Commit" cmd - "Modified" file.
37  *              A       "Commit" cmd - "Added" file.
38  *              R       "Commit" cmd - "Removed" file.
39  *
40  *  date        is a fixed length 8-char hex representation of a Unix time_t.
41  *              [Starting here, variable fields are delimited by '|' chars.]
42  *
43  *  user        is the username of the person who typed the command.
44  *
45  *  CurDir      The directory where the action occurred.  This should be the
46  *              absolute path of the directory which is at the same level as
47  *              the "Repository" field (for W,U,P,G,C & M,A,R).
48  *
49  *  Repository  For record types [W,U,P,G,C,M,A,R] this field holds the
50  *              repository read from the administrative data where the
51  *              command was typed.
52  *              T       "A" --> New Tag, "D" --> Delete Tag
53  *                      Otherwise it is the Tag or Date to modify.
54  *              O,F,E   A "" (null field)
55  *
56  *  rev(s)      Revision number or tag.
57  *              T       The Tag to apply.
58  *              O,E     The Tag or Date, if specified, else "" (null field).
59  *              F       "" (null field)
60  *              W       The Tag or Date, if specified, else "" (null field).
61  *              U,P     The Revision checked out over the User file.
62  *              G,C     The Revision(s) involved in merge.
63  *              M,A,R   RCS Revision affected.
64  *
65  *  argument    The module (for [TOEF]) or file (for [WUPGCMAR]) affected.
66  *
67  *
68  *** Report categories: "User" and "Since" modifiers apply to all reports.
69  *                      [For "sort" ordering see the "sort_order" routine.]
70  *
71  *   Extract list of record types
72  *
73  *      -e, -x [TOEFWUPGCMAR]
74  *
75  *              Extracted records are simply printed, No analysis is performed.
76  *              All "field" modifiers apply.  -e chooses all types.
77  *
78  *   Checked 'O'ut modules
79  *
80  *      -o, -w
81  *              Checked out modules.  'F' and 'O' records are examined and if
82  *              the last record for a repository/file is an 'O', a line is
83  *              printed.  "-w" forces the "working dir" to be used in the
84  *              comparison instead of the repository.
85  *
86  *   Committed (Modified) files
87  *
88  *      -c, -l, -w
89  *              All 'M'odified, 'A'dded and 'R'emoved records are examined.
90  *              "Field" modifiers apply.  -l forces a sort by file within user
91  *              and shows only the last modifier.  -w works as in Checkout.
92  *
93  *              Warning: Be careful with what you infer from the output of
94  *                       "cvs hi -c -l".  It means the last time *you*
95  *                       changed the file, not the list of files for which
96  *                       you were the last changer!!!
97  *
98  *   Module history for named modules.
99  *      -m module, -l
100  *
101  *              This is special.  If one or more modules are specified, the
102  *              module names are remembered and the files making up the
103  *              modules are remembered.  Only records matching exactly those
104  *              files and repositories are shown.  Sorting by "module", then
105  *              filename, is implied.  If -l ("last modified") is specified,
106  *              then "update" records (types WUPCG), tag and release records
107  *              are ignored and the last (by date) "modified" record.
108  *
109  *   TAG history
110  *
111  *      -T      All Tag records are displayed.
112  *
113  *** Modifiers.
114  *
115  *   Since ...          [All records contain a timestamp, so any report
116  *                       category can be limited by date.]
117  *
118  *      -D date         - The "date" is parsed into a Unix "time_t" and
119  *                        records with an earlier time stamp are ignored.
120  *      -r rev/tag      - A "rev" begins with a digit.  A "tag" does not.  If
121  *                        you use this option, every file is searched for the
122  *                        indicated rev/tag.
123  *      -t tag          - The "tag" is searched for in the history file and no
124  *                        record is displayed before the tag is found.  An
125  *                        error is printed if the tag is never found.
126  *      -b string       - Records are printed only back to the last reference
127  *                        to the string in the "module", "file" or
128  *                        "repository" fields.
129  *
130  *   Field Selections   [Simple comparisons on existing fields.  All field
131  *                       selections are repeatable.]
132  *
133  *      -a              - All users.
134  *      -u user         - If no user is given and '-a' is not given, only
135  *                        records for the user typing the command are shown.
136  *                        ==> If -a or -u is not specified, just use "self".
137  *
138  *      -f filematch    - Only records in which the "file" field contains the
139  *                        string "filematch" are considered.
140  *
141  *      -p repository   - Only records in which the "repository" string is a
142  *                        prefix of the "repos" field are considered.
143  *
144  *      -n modulename   - Only records which contain "modulename" in the
145  *                        "module" field are considered.
146  *
147  *
148  * EXAMPLES: ("cvs history", "cvs his" or "cvs hi")
149  *
150  *** Checked out files for username.  (default self, e.g. "dgg")
151  *      cvs hi                  [equivalent to: "cvs hi -o -u dgg"]
152  *      cvs hi -u user          [equivalent to: "cvs hi -o -u user"]
153  *      cvs hi -o               [equivalent to: "cvs hi -o -u dgg"]
154  *
155  *** Committed (modified) files from the beginning of the file.
156  *      cvs hi -c [-u user]
157  *
158  *** Committed (modified) files since Midnight, January 1, 1990:
159  *      cvs hi -c -D 'Jan 1 1990' [-u user]
160  *
161  *** Committed (modified) files since tag "TAG" was stored in the history file:
162  *      cvs hi -c -t TAG [-u user]
163  *
164  *** Committed (modified) files since tag "TAG" was placed on the files:
165  *      cvs hi -c -r TAG [-u user]
166  *
167  *** Who last committed file/repository X?
168  *      cvs hi -c -l -[fp] X
169  *
170  *** Modified files since tag/date/file/repos?
171  *      cvs hi -c {-r TAG | -D Date | -b string}
172  *
173  *** Tag history
174  *      cvs hi -T
175  *
176  *** History of file/repository/module X.
177  *      cvs hi -[fpn] X
178  *
179  *** History of user "user".
180  *      cvs hi -e -u user
181  *
182  *** Dump (eXtract) specified record types
183  *      cvs hi -x [TOEFWUPGCMAR]
184  *
185  *
186  * FUTURE:              J[Join], I[Import]  (Not currently implemented.)
187  *
188  */
189
190 #include "cvs.h"
191 #include "history.h"
192 #include "save-cwd.h"
193
194 static struct hrec
195 {
196     char *type;         /* Type of record (In history record) */
197     char *user;         /* Username (In history record) */
198     char *dir;          /* "Compressed" Working dir (In history record) */
199     char *repos;        /* (Tag is special.) Repository (In history record) */
200     char *rev;          /* Revision affected (In history record) */
201     char *file;         /* Filename (In history record) */
202     char *end;          /* Ptr into repository to copy at end of workdir */
203     char *mod;          /* The module within which the file is contained */
204     time_t date;        /* Calculated from date stored in record */
205     long idx;           /* Index of record, for "stable" sort. */
206 } *hrec_head;
207 static long hrec_idx;
208
209
210 static void fill_hrec (char *line, struct hrec * hr);
211 static int accept_hrec (struct hrec * hr, struct hrec * lr);
212 static int select_hrec (struct hrec * hr);
213 static int sort_order (const void *l, const void *r);
214 static int within (char *find, char *string);
215 static void expand_modules (void);
216 static void read_hrecs (List *flist);
217 static void report_hrecs (void);
218 static void save_file (char *dir, char *name, char *module);
219 static void save_module (char *module);
220 static void save_user (char *name);
221
222 #define USER_INCREMENT  2
223 #define FILE_INCREMENT  128
224 #define MODULE_INCREMENT 5
225 #define HREC_INCREMENT  128
226
227 static short report_count;
228
229 static short extract;
230 static short extract_all;
231 static short v_checkout;
232 static short modified;
233 static short tag_report;
234 static short module_report;
235 static short working;
236 static short last_entry;
237 static short all_users;
238
239 static short user_sort;
240 static short repos_sort;
241 static short file_sort;
242 static short module_sort;
243
244 static short tz_local;
245 static time_t tz_seconds_east_of_GMT;
246 static char *tz_name = "+0000";
247
248 /* -r, -t, or -b options, malloc'd.  These are "" if the option in
249    question is not specified or is overridden by another option.  The
250    main reason for using "" rather than NULL is historical.  Together
251    with since_date, these are a mutually exclusive set; one overrides the
252    others.  */
253 static char *since_rev;
254 static char *since_tag;
255 static char *backto;
256 /* -D option, or 0 if not specified.  RCS format.  */
257 static char * since_date;
258
259 static struct hrec *last_since_tag;
260 static struct hrec *last_backto;
261
262 /* Record types to look for, malloc'd.  Probably could be statically
263    allocated, but only if we wanted to check for duplicates more than
264    we do.  */
265 static char *rec_types;
266
267 static int hrec_count;
268 static int hrec_max;
269
270 static char **user_list;        /* Ptr to array of ptrs to user names */
271 static int user_max;            /* Number of elements allocated */
272 static int user_count;          /* Number of elements used */
273
274 static struct file_list_str
275 {
276     char *l_file;
277     char *l_module;
278 } *file_list;                   /* Ptr to array file name structs */
279 static int file_max;            /* Number of elements allocated */
280 static int file_count;          /* Number of elements used */
281
282 static char **mod_list;         /* Ptr to array of ptrs to module names */
283 static int mod_max;             /* Number of elements allocated */
284 static int mod_count;           /* Number of elements used */
285
286 /* This is pretty unclear.  First of all, separating "flags" vs.
287    "options" (I think the distinction is that "options" take arguments)
288    is nonstandard, and not something we do elsewhere in CVS.  Second of
289    all, what does "reports" mean?  I think it means that you can only
290    supply one of those options, but "reports" hardly has that meaning in
291    a self-explanatory way.  */
292 static const char *const history_usg[] =
293 {
294     "Usage: %s %s [-report] [-flags] [-options args] [files...]\n\n",
295     "   Reports:\n",
296     "        -T              Produce report on all TAGs\n",
297     "        -c              Committed (Modified) files\n",
298     "        -o              Checked out modules\n",
299     "        -m <module>     Look for specified module (repeatable)\n",
300     "        -x [" ALL_HISTORY_REC_TYPES "] Extract by record type\n",
301     "        -e              Everything (same as -x, but all record types)\n",
302     "   Flags:\n",
303     "        -a              All users (Default is self)\n",
304     "        -l              Last modified (committed or modified report)\n",
305     "        -w              Working directory must match\n",
306     "   Options:\n",
307     "        -D <date>       Since date (Many formats)\n",
308     "        -b <str>        Back to record with str in module/file/repos field\n",
309     "        -f <file>       Specified file (same as command line) (repeatable)\n",
310     "        -n <modulename> In module (repeatable)\n",
311     "        -p <repos>      In repository (repeatable)\n",
312     "        -r <rev/tag>    Since rev or tag (looks inside RCS files!)\n",
313     "        -t <tag>        Since tag record placed in history file (by anyone).\n",
314     "        -u <user>       For user name (repeatable)\n",
315     "        -z <tz>         Output for time zone <tz> (e.g. -z -0700)\n",
316     NULL};
317
318 /* Sort routine for qsort:
319    - If a user is selected at all, sort it first. User-within-file is useless.
320    - If a module was selected explicitly, sort next on module.
321    - Then sort by file.  "File" is "repository/file" unless "working" is set,
322      then it is "workdir/file".  (Revision order should always track date.)
323    - Always sort timestamp last.
324 */
325 static int
326 sort_order (const void *l, const void *r)
327 {
328     int i;
329     const struct hrec *left = l;
330     const struct hrec *right = r;
331
332     if (user_sort)      /* If Sort by username, compare users */
333     {
334         if ((i = strcmp (left->user, right->user)) != 0)
335             return i;
336     }
337     if (module_sort)    /* If sort by modules, compare module names */
338     {
339         if (left->mod && right->mod)
340             if ((i = strcmp (left->mod, right->mod)) != 0)
341                 return i;
342     }
343     if (repos_sort)     /* If sort by repository, compare them. */
344     {
345         if ((i = strcmp (left->repos, right->repos)) != 0)
346             return i;
347     }
348     if (file_sort)      /* If sort by filename, compare files, NOT dirs. */
349     {
350         if ((i = strcmp (left->file, right->file)) != 0)
351             return i;
352
353         if (working)
354         {
355             if ((i = strcmp (left->dir, right->dir)) != 0)
356                 return i;
357
358             if ((i = strcmp (left->end, right->end)) != 0)
359                 return i;
360         }
361     }
362
363     /*
364      * By default, sort by date, time
365      * XXX: This fails after 2030 when date slides into sign bit
366      */
367     if ((i = ((long) (left->date) - (long) (right->date))) != 0)
368         return i;
369
370     /* For matching dates, keep the sort stable by using record index */
371     return left->idx - right->idx;
372 }
373
374
375
376 /* Get the name of the history log, either from CVSROOT/config, or via the
377  * hard-coded default.
378  */
379 static const char *
380 get_history_log_name (time_t now)
381 {
382     char *log_name;
383
384     if (config->HistoryLogPath)
385     {
386         /* ~, $VARs, and were expanded via expand_path() when CVSROOT/config
387          * was parsed.
388          */
389         log_name = xmalloc (PATH_MAX);
390         if (!now) now = time (NULL);
391         if (!strftime (log_name, PATH_MAX, config->HistoryLogPath,
392                        localtime (&now)))
393         {
394             error (0, 0, "Invalid date format in HistoryLogPath.");
395             free (config->HistoryLogPath);
396             config->HistoryLogPath = NULL;
397         }
398     }
399
400     if (!config->HistoryLogPath)
401     {
402         /* Use the default.  */
403         log_name = xmalloc (strlen (current_parsed_root->directory)
404                             + sizeof (CVSROOTADM)
405                             + sizeof (CVSROOTADM_HISTORY) + 3);
406         sprintf (log_name, "%s/%s/%s", current_parsed_root->directory,
407                  CVSROOTADM, CVSROOTADM_HISTORY);
408     }
409
410     return log_name;
411 }
412
413
414
415 int
416 history (int argc, char **argv)
417 {
418     int i, c;
419     const char *fname = NULL;
420     List *flist;
421
422     if (argc == -1)
423         usage (history_usg);
424
425     since_rev = xstrdup ("");
426     since_tag = xstrdup ("");
427     backto = xstrdup ("");
428     rec_types = xstrdup ("");
429     optind = 0;
430     while ((c = getopt (argc, argv, "+Tacelow?D:b:f:m:n:p:r:t:u:x:X:z:")) != -1)
431     {
432         switch (c)
433         {
434             case 'T':                   /* Tag list */
435                 report_count++;
436                 tag_report++;
437                 break;
438             case 'a':                   /* For all usernames */
439                 all_users++;
440                 break;
441             case 'c':
442                 report_count++;
443                 modified = 1;
444                 break;
445             case 'e':
446                 report_count++;
447                 extract_all++;
448                 free (rec_types);
449                 rec_types = xstrdup (ALL_HISTORY_REC_TYPES);
450                 break;
451             case 'l':                   /* Find Last file record */
452                 last_entry = 1;
453                 break;
454             case 'o':
455                 report_count++;
456                 v_checkout = 1;
457                 break;
458             case 'w':                   /* Match Working Dir (CurDir) fields */
459                 working = 1;
460                 break;
461             case 'X':                   /* Undocumented debugging flag */
462 #ifdef DEBUG
463                 fname = optarg;
464 #endif
465                 break;
466
467             case 'D':                   /* Since specified date */
468                 if (*since_rev || *since_tag || *backto)
469                 {
470                     error (0, 0, "date overriding rev/tag/backto");
471                     *since_rev = *since_tag = *backto = '\0';
472                 }
473                 since_date = Make_Date (optarg);
474                 break;
475             case 'b':                   /* Since specified file/Repos */
476                 if (since_date || *since_rev || *since_tag)
477                 {
478                     error (0, 0, "backto overriding date/rev/tag");
479                     *since_rev = *since_tag = '\0';
480                     if (since_date != NULL)
481                         free (since_date);
482                     since_date = NULL;
483                 }
484                 free (backto);
485                 backto = xstrdup (optarg);
486                 break;
487             case 'f':                   /* For specified file */
488                 save_file (NULL, optarg, NULL);
489                 break;
490             case 'm':                   /* Full module report */
491                 if (!module_report++) report_count++;
492                 /* fall through */
493             case 'n':                   /* Look for specified module */
494                 save_module (optarg);
495                 break;
496             case 'p':                   /* For specified directory */
497                 save_file (optarg, NULL, NULL);
498                 break;
499             case 'r':                   /* Since specified Tag/Rev */
500                 if (since_date || *since_tag || *backto)
501                 {
502                     error (0, 0, "rev overriding date/tag/backto");
503                     *since_tag = *backto = '\0';
504                     if (since_date != NULL)
505                         free (since_date);
506                     since_date = NULL;
507                 }
508                 free (since_rev);
509                 since_rev = xstrdup (optarg);
510                 break;
511             case 't':                   /* Since specified Tag/Rev */
512                 if (since_date || *since_rev || *backto)
513                 {
514                     error (0, 0, "tag overriding date/marker/file/repos");
515                     *since_rev = *backto = '\0';
516                     if (since_date != NULL)
517                         free (since_date);
518                     since_date = NULL;
519                 }
520                 free (since_tag);
521                 since_tag = xstrdup (optarg);
522                 break;
523             case 'u':                   /* For specified username */
524                 save_user (optarg);
525                 break;
526             case 'x':
527                 report_count++;
528                 extract++;
529                 {
530                     char *cp;
531
532                     for (cp = optarg; *cp; cp++)
533                         if (!strchr (ALL_HISTORY_REC_TYPES, *cp))
534                             error (1, 0, "%c is not a valid report type", *cp);
535                 }
536                 free (rec_types);
537                 rec_types = xstrdup (optarg);
538                 break;
539             case 'z':
540                 tz_local = 
541                     (optarg[0] == 'l' || optarg[0] == 'L')
542                     && (optarg[1] == 't' || optarg[1] == 'T')
543                     && !optarg[2];
544                 if (tz_local)
545                     tz_name = optarg;
546                 else
547                 {
548                     /*
549                      * Convert a known time with the given timezone to time_t.
550                      * Use the epoch + 23 hours, so timezones east of GMT work.
551                      */
552                     struct timespec t;
553                     char *buf = Xasprintf ("1/1/1970 23:00 %s", optarg);
554                     if (get_date (&t, buf, NULL))
555                     {
556                         /*
557                          * Convert to seconds east of GMT, removing the
558                          * 23-hour offset mentioned above.
559                          */
560                         tz_seconds_east_of_GMT = (time_t)23 * 60 * 60
561                                                  - t.tv_sec;
562                         tz_name = optarg;
563                     }
564                     else
565                         error (0, 0, "%s is not a known time zone", optarg);
566                     free (buf);
567                 }
568                 break;
569             case '?':
570             default:
571                 usage (history_usg);
572                 break;
573         }
574     }
575     argc -= optind;
576     argv += optind;
577     for (i = 0; i < argc; i++)
578         save_file (NULL, argv[i], NULL);
579
580
581     /* ================ Now analyze the arguments a bit */
582     if (!report_count)
583         v_checkout++;
584     else if (report_count > 1)
585         error (1, 0, "Only one report type allowed from: \"-Tcomxe\".");
586
587 #ifdef CLIENT_SUPPORT
588     if (current_parsed_root->isremote)
589     {
590         struct file_list_str *f1;
591         char **mod;
592
593         /* We're the client side.  Fire up the remote server.  */
594         start_server ();
595         
596         ign_setup ();
597
598         if (tag_report)
599             send_arg ("-T");
600         if (all_users)
601             send_arg ("-a");
602         if (modified)
603             send_arg ("-c");
604         if (last_entry)
605             send_arg ("-l");
606         if (v_checkout)
607             send_arg ("-o");
608         if (working)
609             send_arg ("-w");
610         if (fname)
611             option_with_arg ("-X", fname);
612         if (since_date)
613             client_senddate (since_date);
614         if (backto[0] != '\0')
615             option_with_arg ("-b", backto);
616         for (f1 = file_list; f1 < &file_list[file_count]; ++f1)
617         {
618             if (f1->l_file[0] == '*')
619                 option_with_arg ("-p", f1->l_file + 1);
620             else
621                 option_with_arg ("-f", f1->l_file);
622         }
623         if (module_report)
624             send_arg ("-m");
625         for (mod = mod_list; mod < &mod_list[mod_count]; ++mod)
626             option_with_arg ("-n", *mod);
627         if (*since_rev)
628             option_with_arg ("-r", since_rev);
629         if (*since_tag)
630             option_with_arg ("-t", since_tag);
631         for (mod = user_list; mod < &user_list[user_count]; ++mod)
632             option_with_arg ("-u", *mod);
633         if (extract_all)
634             send_arg ("-e");
635         if (extract)
636             option_with_arg ("-x", rec_types);
637         option_with_arg ("-z", tz_name);
638
639         send_to_server ("history\012", 0);
640         return get_responses_and_close ();
641     }
642 #endif
643
644     if (all_users)
645         save_user ("");
646
647     if (mod_list)
648         expand_modules ();
649
650     if (tag_report)
651     {
652         if (!strchr (rec_types, 'T'))
653         {
654             rec_types = xrealloc (rec_types, strlen (rec_types) + 5);
655             (void) strcat (rec_types, "T");
656         }
657     }
658     else if (extract || extract_all)
659     {
660         if (user_list)
661             user_sort++;
662     }
663     else if (modified)
664     {
665         free (rec_types);
666         rec_types = xstrdup ("MAR");
667         /*
668          * If the user has not specified a date oriented flag ("Since"), sort
669          * by Repository/file before date.  Default is "just" date.
670          */
671         if (last_entry
672             || (!since_date && !*since_rev && !*since_tag && !*backto))
673         {
674             repos_sort++;
675             file_sort++;
676             /*
677              * If we are not looking for last_modified and the user specified
678              * one or more users to look at, sort by user before filename.
679              */
680             if (!last_entry && user_list)
681                 user_sort++;
682         }
683     }
684     else if (module_report)
685     {
686         free (rec_types);
687         rec_types = xstrdup (last_entry ? "OMAR" : ALL_HISTORY_REC_TYPES);
688         module_sort++;
689         repos_sort++;
690         file_sort++;
691         working = 0;                    /* User's workdir doesn't count here */
692     }
693     else
694         /* Must be "checkout" or default */
695     {
696         free (rec_types);
697         rec_types = xstrdup ("OF");
698         /* See comments in "modified" above */
699         if (!last_entry && user_list)
700             user_sort++;
701         if (last_entry
702             || (!since_date && !*since_rev && !*since_tag && !*backto))
703             file_sort++;
704     }
705
706     /* If no users were specified, use self (-a saves a universal ("") user) */
707     if (!user_list)
708         save_user (getcaller ());
709
710     /* If we're looking back to a Tag value, must consider "Tag" records */
711     if (*since_tag && !strchr (rec_types, 'T'))
712     {
713         rec_types = xrealloc (rec_types, strlen (rec_types) + 5);
714         (void) strcat (rec_types, "T");
715     }
716
717     if (fname)
718     {
719         Node *p;
720
721         flist = getlist ();
722         p = getnode ();
723         p->type = FILES;
724         p->key = Xasprintf ("%s/%s/%s",
725                             current_parsed_root->directory, CVSROOTADM, fname);
726         addnode (flist, p);
727     }
728     else
729     {
730         char *pat;
731
732         if (config->HistorySearchPath)
733             pat = config->HistorySearchPath;
734         else
735             pat = Xasprintf ("%s/%s/%s",
736                              current_parsed_root->directory, CVSROOTADM,
737                              CVSROOTADM_HISTORY);
738
739         flist = find_files (NULL, pat);
740         if (pat != config->HistorySearchPath) free (pat);
741     }
742
743     read_hrecs (flist);
744     if (hrec_count > 0)
745         qsort (hrec_head, hrec_count, sizeof (struct hrec), sort_order);
746     report_hrecs ();
747     if (since_date != NULL)
748         free (since_date);
749     free (since_rev);
750     free (since_tag);
751     free (backto);
752     free (rec_types);
753
754     return 0;
755 }
756
757
758
759 /* An empty LogHistory string in CVSROOT/config will turn logging off.
760  */
761 void
762 history_write (int type, const char *update_dir, const char *revs,
763                const char *name, const char *repository)
764 {
765     const char *fname;
766     char *workdir;
767     char *username = getcaller ();
768     int fd;
769     char *line;
770     char *slash = "", *cp;
771     const char *cp2, *repos;
772     int i;
773     static char *tilde = "";
774     static char *PrCurDir = NULL;
775     time_t now;
776
777     if (logoff)                 /* History is turned off by noexec or
778                                  * readonlyfs.
779                                  */
780         return;
781     if (!strchr (config->logHistory, type))     
782         return;
783
784     if (noexec)
785         goto out;
786
787     repos = Short_Repository (repository);
788
789     if (!PrCurDir)
790     {
791         char *pwdir;
792
793         pwdir = get_homedir ();
794         PrCurDir = CurDir;
795         if (pwdir != NULL)
796         {
797             /* Assumes neither CurDir nor pwdir ends in '/' */
798             i = strlen (pwdir);
799             if (!strncmp (CurDir, pwdir, i))
800             {
801                 PrCurDir += i;          /* Point to '/' separator */
802                 tilde = "~";
803             }
804             else
805             {
806                 /* Try harder to find a "homedir" */
807                 struct saved_cwd cwd;
808                 char *homedir;
809
810                 if (save_cwd (&cwd))
811                     error (1, errno, "Failed to save current directory.");
812
813                 if (CVS_CHDIR (pwdir) < 0 || (homedir = xgetcwd ()) == NULL)
814                     homedir = pwdir;
815
816                 if (restore_cwd (&cwd))
817                     error (1, errno,
818                            "Failed to restore current directory, `%s'.",
819                            cwd.name);
820                 free_cwd (&cwd);
821
822                 i = strlen (homedir);
823                 if (!strncmp (CurDir, homedir, i))
824                 {
825                     PrCurDir += i;      /* Point to '/' separator */
826                     tilde = "~";
827                 }
828
829                 if (homedir != pwdir)
830                     free (homedir);
831             }
832         }
833     }
834
835     if (type == 'T')
836     {
837         repos = update_dir;
838         update_dir = "";
839     }
840     else if (update_dir && *update_dir)
841         slash = "/";
842     else
843         update_dir = "";
844
845     workdir = Xasprintf ("%s%s%s%s", tilde, PrCurDir, slash, update_dir);
846
847     /*
848      * "workdir" is the directory where the file "name" is. ("^~" == $HOME)
849      * "repos"  is the Repository, relative to $CVSROOT where the RCS file is.
850      *
851      * "$workdir/$name" is the working file name.
852      * "$CVSROOT/$repos/$name,v" is the RCS file in the Repository.
853      *
854      * First, note that the history format was intended to save space, not
855      * to be human readable.
856      *
857      * The working file directory ("workdir") and the Repository ("repos")
858      * usually end with the same one or more directory elements.  To avoid
859      * duplication (and save space), the "workdir" field ends with
860      * an integer offset into the "repos" field.  This offset indicates the
861      * beginning of the "tail" of "repos", after which all characters are
862      * duplicates.
863      *
864      * In other words, if the "workdir" field has a '*' (a very stupid thing
865      * to put in a filename) in it, then every thing following the last '*'
866      * is a hex offset into "repos" of the first character from "repos" to
867      * append to "workdir" to finish the pathname.
868      *
869      * It might be easier to look at an example:
870      *
871      *  M273b3463|dgg|~/work*9|usr/local/cvs/examples|1.2|loginfo
872      *
873      * Indicates that the workdir is really "~/work/cvs/examples", saving
874      * 10 characters, where "~/work*d" would save 6 characters and mean that
875      * the workdir is really "~/work/examples".  It will mean more on
876      * directories like: usr/local/gnu/emacs/dist-19.17/lisp/term
877      *
878      * "workdir" is always an absolute pathname (~/xxx is an absolute path)
879      * "repos" is always a relative pathname.  So we can assume that we will
880      * never run into the top of "workdir" -- there will always be a '/' or
881      * a '~' at the head of "workdir" that is not matched by anything in
882      * "repos".  On the other hand, we *can* run off the top of "repos".
883      *
884      * Only "compress" if we save characters.
885      */
886
887     cp = workdir + strlen (workdir) - 1;
888     cp2 = repos + strlen (repos) - 1;
889     for (i = 0; cp2 >= repos && cp > workdir && *cp == *cp2--; cp--)
890         i++;
891
892     if (i > 2)
893     {
894         i = strlen (repos) - i;
895         (void) sprintf ((cp + 1), "*%x", i);
896     }
897
898     if (!revs)
899         revs = "";
900     now = time (NULL);
901     line = Xasprintf ("%c%08lx|%s|%s|%s|%s|%s\n", type, (long) now,
902                       username, workdir, repos, revs, name);
903
904     fname = get_history_log_name (now);
905
906     if (!history_lock (current_parsed_root->directory))
907         /* history_lock() will already have printed an error on failure.  */
908         goto out;
909
910     fd = CVS_OPEN (fname, O_WRONLY | O_APPEND | O_CREAT | OPEN_BINARY, 0666);
911     if (fd < 0)
912     {
913         if (!really_quiet)
914             error (0, errno,
915                    "warning: cannot open history file `%s' for write", fname);
916         goto out;
917     }
918
919     TRACE (TRACE_FUNCTION, "open (`%s', a)", fname);
920
921     /* Lessen some race conditions on non-Posix-compliant hosts.
922      *
923      * FIXME:  I'm guessing the following was necessary for NFS when multiple
924      * simultaneous writes to the same file are possible, since NFS does not
925      * natively support append mode and it must be emulated via lseek().  Now
926      * that the history file is locked for write, the following lseek() may be
927      * unnecessary.
928      */
929     if (lseek (fd, (off_t) 0, SEEK_END) == -1)
930         error (1, errno, "cannot seek to end of history file: %s", fname);
931
932     if (write (fd, line, strlen (line)) < 0)
933         error (1, errno, "cannot write to history file: %s", fname);
934     free (line);
935     if (close (fd) != 0)
936         error (1, errno, "cannot close history file: %s", fname);
937     free (workdir);
938  out:
939     clear_history_lock ();
940 }
941
942
943
944 /*
945  * save_user() adds a user name to the user list to select.  Zero-length
946  *              username ("") matches any user.
947  */
948 static void
949 save_user (char *name)
950 {
951     if (user_count == user_max)
952     {
953         user_max = xsum (user_max, USER_INCREMENT);
954         if (size_overflow_p (xtimes (user_max, sizeof (char *))))
955         {
956             error (0, 0, "save_user: too many users");
957             return;
958         }
959         user_list = xnrealloc (user_list, user_max, sizeof (char *));
960     }
961     user_list[user_count++] = xstrdup (name);
962 }
963
964 /*
965  * save_file() adds file name and associated module to the file list to select.
966  *
967  * If "dir" is null, store a file name as is.
968  * If "name" is null, store a directory name with a '*' on the front.
969  * Else, store concatenated "dir/name".
970  *
971  * Later, in the "select" stage:
972  *      - if it starts with '*', it is prefix-matched against the repository.
973  *      - if it has a '/' in it, it is matched against the repository/file.
974  *      - else it is matched against the file name.
975  */
976 static void
977 save_file (char *dir, char *name, char *module)
978 {
979     struct file_list_str *fl;
980
981     if (file_count == file_max)
982     {
983         file_max = xsum (file_max, FILE_INCREMENT);
984         if (size_overflow_p (xtimes (file_max, sizeof (*fl))))
985         {
986             error (0, 0, "save_file: too many files");
987             return;
988         }
989         file_list = xnrealloc (file_list, file_max, sizeof (*fl));
990     }
991     fl = &file_list[file_count++];
992     fl->l_module = module;
993
994     if (dir && *dir)
995     {
996         if (name && *name)
997             fl->l_file = Xasprintf ("%s/%s", dir, name);
998         else
999             fl->l_file = Xasprintf ("*%s", dir);
1000     }
1001     else
1002     {
1003         if (name && *name)
1004             fl->l_file = xstrdup (name);
1005         else
1006             error (0, 0, "save_file: null dir and file name");
1007     }
1008 }
1009
1010 static void
1011 save_module (char *module)
1012 {
1013     if (mod_count == mod_max)
1014     {
1015         mod_max = xsum (mod_max, MODULE_INCREMENT);
1016         if (size_overflow_p (xtimes (mod_max, sizeof (char *))))
1017         {
1018             error (0, 0, "save_module: too many modules");
1019             return;
1020         }
1021         mod_list = xnrealloc (mod_list, mod_max, sizeof (char *));
1022     }
1023     mod_list[mod_count++] = xstrdup (module);
1024 }
1025
1026 static void
1027 expand_modules (void)
1028 {
1029 }
1030
1031 /* fill_hrec
1032  *
1033  * Take a ptr to 7-part history line, ending with a newline, for example:
1034  *
1035  *      M273b3463|dgg|~/work*9|usr/local/cvs/examples|1.2|loginfo
1036  *
1037  * Split it into 7 parts and drop the parts into a "struct hrec".
1038  * Return a pointer to the character following the newline.
1039  * 
1040  */
1041
1042 #define NEXT_BAR(here) do { \
1043         while (isspace (*line)) line++; \
1044         hr->here = line; \
1045         while ((c = *line++) && c != '|') ; \
1046         if (!c) return; line[-1] = '\0'; \
1047         } while (0)
1048
1049 static void
1050 fill_hrec (char *line, struct hrec *hr)
1051 {
1052     char *cp;
1053     int c;
1054
1055     hr->type = hr->user = hr->dir = hr->repos = hr->rev = hr->file =
1056         hr->end = hr->mod = NULL;
1057     hr->date = -1;
1058     hr->idx = ++hrec_idx;
1059
1060     while (isspace ((unsigned char) *line))
1061         line++;
1062
1063     hr->type = line++;
1064     hr->date = strtoul (line, &cp, 16);
1065     if (cp == line || *cp != '|')
1066         return;
1067     line = cp + 1;
1068     NEXT_BAR (user);
1069     NEXT_BAR (dir);
1070     if ((cp = strrchr (hr->dir, '*')) != NULL)
1071     {
1072         *cp++ = '\0';
1073         hr->end = line + strtoul (cp, NULL, 16);
1074     }
1075     else
1076         hr->end = line - 1;             /* A handy pointer to '\0' */
1077     NEXT_BAR (repos);
1078     NEXT_BAR (rev);
1079     if (strchr ("FOET", *(hr->type)))
1080         hr->mod = line;
1081
1082     NEXT_BAR (file);
1083 }
1084
1085
1086 #ifndef STAT_BLOCKSIZE
1087 #if HAVE_STRUCT_STAT_ST_BLKSIZE
1088 #define STAT_BLOCKSIZE(s) (s).st_blksize
1089 #else
1090 #define STAT_BLOCKSIZE(s) (4 * 1024)
1091 #endif
1092 #endif
1093
1094
1095 /* read_hrecs_file's job is to read a history file and fill in new "hrec"
1096  * (history record) array elements with the ones we need to print.
1097  *
1098  * Logic:
1099  * - Read a block from the file. 
1100  * - Walk through the block parsing line into hr records. 
1101  * - if the hr isn't used, free its strings, if it is, bump the hrec counter
1102  * - at the end of a block, copy the end of the current block to the start 
1103  * of space for the next block, then read in the next block.  If we get less
1104  * than the whole block, we're done. 
1105  */
1106 static int
1107 read_hrecs_file (Node *p, void *closure)
1108 {
1109     char *cpstart, *cpend, *cp, *nl;
1110     char *hrline;
1111     int i;
1112     int fd;
1113     struct stat st_buf;
1114     const char *fname = p->key;
1115
1116     if ((fd = CVS_OPEN (fname, O_RDONLY | OPEN_BINARY)) < 0)
1117     {
1118         error (0, errno, "cannot open history file `%s'", fname);
1119         return 0;
1120     }
1121
1122     if (fstat (fd, &st_buf) < 0)
1123     {
1124         error (0, errno, "can't stat history file `%s'", fname);
1125         return 0;
1126     }
1127
1128     if (!(st_buf.st_size))
1129     {
1130         error (0, 0, "history file `%s' is empty", fname);
1131         return 0;
1132     }
1133
1134     cpstart = xnmalloc (2, STAT_BLOCKSIZE (st_buf));
1135     cpstart[0] = '\0';
1136     cp = cpend = cpstart;
1137
1138     for (;;)
1139     {
1140         for (nl = cp; nl < cpend && *nl != '\n'; nl++)
1141             if (!isprint (*nl)) *nl = ' ';
1142
1143         if (nl >= cpend)
1144         {
1145             if (nl - cp >= STAT_BLOCKSIZE (st_buf))
1146             {
1147                 error(1, 0, "history line %ld too long (> %lu)", hrec_idx + 1,
1148                       (unsigned long) STAT_BLOCKSIZE(st_buf));
1149             }
1150             if (nl > cp)
1151                 memmove (cpstart, cp, nl - cp);
1152             nl = cpstart + (nl - cp);
1153             cp = cpstart;
1154             i = read (fd, nl, STAT_BLOCKSIZE(st_buf));
1155             if (i > 0)
1156             {
1157                 cpend = nl + i;
1158                 *cpend = '\0';
1159                 continue;
1160             }
1161             if (i < 0)
1162             {
1163                 error (0, errno, "error reading history file `%s'", fname);
1164                 return 0;
1165             }
1166             if (nl == cp) break;
1167             error (0, 0, "warning: no newline at end of history file `%s'",
1168                    fname);
1169         }
1170         *nl = '\0';
1171
1172         if (hrec_count == hrec_max)
1173         {
1174             struct hrec *old_head = hrec_head;
1175
1176             hrec_max += HREC_INCREMENT;
1177             hrec_head = xnrealloc (hrec_head, hrec_max, sizeof (struct hrec));
1178             if (last_since_tag)
1179                 last_since_tag = hrec_head + (last_since_tag - old_head);
1180             if (last_backto)
1181                 last_backto = hrec_head + (last_backto - old_head);
1182         }
1183
1184         /* fill_hrec dates from when history read the entire 
1185            history file in one chunk, and then records were pulled out
1186            by pointing to the various parts of this big chunk.  This is
1187            why there are ugly hacks here:  I don't want to completely
1188            re-write the whole history stuff right now.  */
1189
1190         hrline = xstrdup (cp);
1191         fill_hrec (hrline, &hrec_head[hrec_count]);
1192         if (select_hrec (&hrec_head[hrec_count]))
1193             hrec_count++;
1194         else 
1195             free (hrline);
1196
1197         cp = nl + 1;
1198     }
1199     free (cpstart);
1200     close (fd);
1201     return 1;
1202 }
1203
1204
1205
1206 /* Read the history records in from a list of history files.  */
1207 static void
1208 read_hrecs (List *flist)
1209 {
1210     int files_read;
1211
1212     /* The global history records are already initialized to 0 according to
1213      * ANSI C.
1214      */
1215     hrec_max = HREC_INCREMENT;
1216     hrec_head = xmalloc (hrec_max * sizeof (struct hrec));
1217     hrec_idx = 0;
1218
1219     files_read = walklist (flist, read_hrecs_file, NULL);
1220     if (!files_read)
1221         error (1, 0, "No history files read.");
1222
1223     /* Special selection problem: If "since_tag" is set, we have saved every
1224      * record from the 1st occurrence of "since_tag", when we want to save
1225      * records since the *last* occurrence of "since_tag".  So what we have
1226      * to do is bump hrec_head forward and reduce hrec_count accordingly.
1227      */
1228     if (last_since_tag)
1229     {
1230         hrec_count -= (last_since_tag - hrec_head);
1231         hrec_head = last_since_tag;
1232     }
1233
1234     /* Much the same thing is necessary for the "backto" option. */
1235     if (last_backto)
1236     {
1237         hrec_count -= (last_backto - hrec_head);
1238         hrec_head = last_backto;
1239     }
1240 }
1241
1242
1243
1244 /* Utility program for determining whether "find" is inside "string" */
1245 static int
1246 within (char *find, char *string)
1247 {
1248     int c, len;
1249
1250     if (!find || !string)
1251         return 0;
1252
1253     c = *find++;
1254     len = strlen (find);
1255
1256     while (*string)
1257     {
1258         if (!(string = strchr (string, c)))
1259             return 0;
1260         string++;
1261         if (!strncmp (find, string, len))
1262             return 1;
1263     }
1264     return 0;
1265 }
1266
1267 /* The purpose of "select_hrec" is to apply the selection criteria based on
1268  * the command arguments and defaults and return a flag indicating whether
1269  * this record should be remembered for printing.
1270  */
1271 static int
1272 select_hrec (struct hrec *hr)
1273 {
1274     char **cpp, *cp, *cp2;
1275     struct file_list_str *fl;
1276     int count;
1277
1278     /* basic validity checking */
1279     if (!hr->type || !hr->user || !hr->dir || !hr->repos || !hr->rev ||
1280         !hr->file || !hr->end)
1281     {
1282         error (0, 0, "warning: history line %ld invalid", hr->idx);
1283         return 0;
1284     }
1285
1286     /* "Since" checking:  The argument parser guarantees that only one of the
1287      *                    following four choices is set:
1288      *
1289      * 1. If "since_date" is set, it contains the date specified on the
1290      *    command line. hr->date fields earlier than "since_date" are ignored.
1291      * 2. If "since_rev" is set, it contains either an RCS "dotted" revision
1292      *    number (which is of limited use) or a symbolic TAG.  Each RCS file
1293      *    is examined and the date on the specified revision (or the revision
1294      *    corresponding to the TAG) in the RCS file (CVSROOT/repos/file) is
1295      *    compared against hr->date as in 1. above.
1296      * 3. If "since_tag" is set, matching tag records are saved.  The field
1297      *    "last_since_tag" is set to the last one of these.  Since we don't
1298      *    know where the last one will be, all records are saved from the
1299      *    first occurrence of the TAG.  Later, at the end of "select_hrec"
1300      *    records before the last occurrence of "since_tag" are skipped.
1301      * 4. If "backto" is set, all records with a module name or file name
1302      *    matching "backto" are saved.  In addition, all records with a
1303      *    repository field with a *prefix* matching "backto" are saved.
1304      *    The field "last_backto" is set to the last one of these.  As in
1305      *    3. above, "select_hrec" adjusts to include the last one later on.
1306      */
1307     if (since_date)
1308     {
1309         char *ourdate = date_from_time_t (hr->date);
1310         count = RCS_datecmp (ourdate, since_date);
1311         free (ourdate);
1312         if (count < 0)
1313             return 0;
1314     }
1315     else if (*since_rev)
1316     {
1317         Vers_TS *vers;
1318         time_t t;
1319         struct file_info finfo;
1320
1321         memset (&finfo, 0, sizeof finfo);
1322         finfo.file = hr->file;
1323         /* Not used, so don't worry about it.  */
1324         finfo.update_dir = NULL;
1325         finfo.fullname = finfo.file;
1326         finfo.repository = hr->repos;
1327         finfo.entries = NULL;
1328         finfo.rcs = NULL;
1329
1330         vers = Version_TS (&finfo, NULL, since_rev, NULL, 1, 0);
1331         if (vers->vn_rcs)
1332         {
1333             if ((t = RCS_getrevtime (vers->srcfile, vers->vn_rcs, NULL, 0))
1334                 != (time_t) 0)
1335             {
1336                 if (hr->date < t)
1337                 {
1338                     freevers_ts (&vers);
1339                     return 0;
1340                 }
1341             }
1342         }
1343         freevers_ts (&vers);
1344     }
1345     else if (*since_tag)
1346     {
1347         if (*(hr->type) == 'T')
1348         {
1349             /*
1350              * A 'T'ag record, the "rev" field holds the tag to be set,
1351              * while the "repos" field holds "D"elete, "A"dd or a rev.
1352              */
1353             if (within (since_tag, hr->rev))
1354             {
1355                 last_since_tag = hr;
1356                 return 1;
1357             }
1358             else
1359                 return 0;
1360         }
1361         if (!last_since_tag)
1362             return 0;
1363     }
1364     else if (*backto)
1365     {
1366         if (within (backto, hr->file) || within (backto, hr->mod) ||
1367             within (backto, hr->repos))
1368             last_backto = hr;
1369         else
1370             return 0;
1371     }
1372
1373     /* User checking:
1374      *
1375      * Run down "user_list", match username ("" matches anything)
1376      * If "" is not there and actual username is not there, return failure.
1377      */
1378     if (user_list && hr->user)
1379     {
1380         for (cpp = user_list, count = user_count; count; cpp++, count--)
1381         {
1382             if (!**cpp)
1383                 break;                  /* null user == accept */
1384             if (!strcmp (hr->user, *cpp))       /* found listed user */
1385                 break;
1386         }
1387         if (!count)
1388             return 0;                   /* Not this user */
1389     }
1390
1391     /* Record type checking:
1392      *
1393      * 1. If Record type is not in rec_types field, skip it.
1394      * 2. If mod_list is null, keep everything.  Otherwise keep only modules
1395      *    on mod_list.
1396      * 3. If neither a 'T', 'F' nor 'O' record, run through "file_list".  If
1397      *    file_list is null, keep everything.  Otherwise, keep only files on
1398      *    file_list, matched appropriately.
1399      */
1400     if (!strchr (rec_types, *(hr->type)))
1401         return 0;
1402     if (!strchr ("TFOE", *(hr->type)))  /* Don't bother with "file" if "TFOE" */
1403     {
1404         if (file_list)                  /* If file_list is null, accept all */
1405         {
1406             for (fl = file_list, count = file_count; count; fl++, count--)
1407             {
1408                 /* 1. If file_list entry starts with '*', skip the '*' and
1409                  *    compare it against the repository in the hrec.
1410                  * 2. If file_list entry has a '/' in it, compare it against
1411                  *    the concatenation of the repository and file from hrec.
1412                  * 3. Else compare the file_list entry against the hrec file.
1413                  */
1414                 char *cmpfile = NULL;
1415
1416                 if (*(cp = fl->l_file) == '*')
1417                 {
1418                     cp++;
1419                     /* if argument to -p is a prefix of repository */
1420                     if (!strncmp (cp, hr->repos, strlen (cp)))
1421                     {
1422                         hr->mod = fl->l_module;
1423                         break;
1424                     }
1425                 }
1426                 else
1427                 {
1428                     if (strchr (cp, '/'))
1429                     {
1430                         cmpfile = Xasprintf ("%s/%s", hr->repos, hr->file);
1431                         cp2 = cmpfile;
1432                     }
1433                     else
1434                     {
1435                         cp2 = hr->file;
1436                     }
1437
1438                     /* if requested file is found within {repos}/file fields */
1439                     if (within (cp, cp2))
1440                     {
1441                         hr->mod = fl->l_module;
1442                         if (cmpfile != NULL)
1443                             free (cmpfile);
1444                         break;
1445                     }
1446                     if (cmpfile != NULL)
1447                         free (cmpfile);
1448                 }
1449             }
1450             if (!count)
1451                 return 0;               /* String specified and no match */
1452         }
1453     }
1454     if (mod_list)
1455     {
1456         for (cpp = mod_list, count = mod_count; count; cpp++, count--)
1457         {
1458             if (hr->mod && !strcmp (hr->mod, *cpp))     /* found module */
1459                 break;
1460         }
1461         if (!count)
1462             return 0;   /* Module specified & this record is not one of them. */
1463     }
1464
1465     return 1;           /* Select this record unless rejected above. */
1466 }
1467
1468 /* The "sort_order" routine (when handed to qsort) has arranged for the
1469  * hrecs files to be in the right order for the report.
1470  *
1471  * Most of the "selections" are done in the select_hrec routine, but some
1472  * selections are more easily done after the qsort by "accept_hrec".
1473  */
1474 static void
1475 report_hrecs (void)
1476 {
1477     struct hrec *hr, *lr;
1478     struct tm *tm;
1479     int i, count, ty;
1480     char *cp;
1481     int user_len, file_len, rev_len, mod_len, repos_len;
1482
1483     if (*since_tag && !last_since_tag)
1484     {
1485         (void) printf ("No tag found: %s\n", since_tag);
1486         return;
1487     }
1488     else if (*backto && !last_backto)
1489     {
1490         (void) printf ("No module, file or repository with: %s\n", backto);
1491         return;
1492     }
1493     else if (hrec_count < 1)
1494     {
1495         (void) printf ("No records selected.\n");
1496         return;
1497     }
1498
1499     user_len = file_len = rev_len = mod_len = repos_len = 0;
1500
1501     /* Run through lists and find maximum field widths */
1502     hr = lr = hrec_head;
1503     hr++;
1504     for (count = hrec_count; count--; lr = hr, hr++)
1505     {
1506         char *repos;
1507
1508         if (!count)
1509             hr = NULL;
1510         if (!accept_hrec (lr, hr))
1511             continue;
1512
1513         ty = *(lr->type);
1514         repos = xstrdup (lr->repos);
1515         if ((cp = strrchr (repos, '/')) != NULL)
1516         {
1517             if (lr->mod && !strcmp (++cp, lr->mod))
1518             {
1519                 (void) strcpy (cp, "*");
1520             }
1521         }
1522         if ((i = strlen (lr->user)) > user_len)
1523             user_len = i;
1524         if ((i = strlen (lr->file)) > file_len)
1525             file_len = i;
1526         if (ty != 'T' && (i = strlen (repos)) > repos_len)
1527             repos_len = i;
1528         if (ty != 'T' && (i = strlen (lr->rev)) > rev_len)
1529             rev_len = i;
1530         if (lr->mod && (i = strlen (lr->mod)) > mod_len)
1531             mod_len = i;
1532         free (repos);
1533     }
1534
1535     /* Walk through hrec array setting "lr" (Last Record) to each element.
1536      * "hr" points to the record following "lr" -- It is NULL in the last
1537      * pass.
1538      *
1539      * There are two sections in the loop below:
1540      * 1. Based on the report type (e.g. extract, checkout, tag, etc.),
1541      *    decide whether the record should be printed.
1542      * 2. Based on the record type, format and print the data.
1543      */
1544     for (lr = hrec_head, hr = (lr + 1); hrec_count--; lr = hr, hr++)
1545     {
1546         char *workdir;
1547         char *repos;
1548
1549         if (!hrec_count)
1550             hr = NULL;
1551         if (!accept_hrec (lr, hr))
1552             continue;
1553
1554         ty = *(lr->type);
1555         if (!tz_local)
1556         {
1557             time_t t = lr->date + tz_seconds_east_of_GMT;
1558             tm = gmtime (&t);
1559         }
1560         else
1561             tm = localtime (&(lr->date));
1562
1563         (void) printf ("%c %04d-%02d-%02d %02d:%02d %s %-*s", ty,
1564                   tm->tm_year+1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour,
1565                   tm->tm_min, tz_name, user_len, lr->user);
1566
1567         workdir = xmalloc (strlen (lr->dir) + strlen (lr->end) + 10);
1568         (void) sprintf (workdir, "%s%s", lr->dir, lr->end);
1569         if ((cp = strrchr (workdir, '/')) != NULL)
1570         {
1571             if (lr->mod && !strcmp (++cp, lr->mod))
1572             {
1573                 (void) strcpy (cp, "*");
1574             }
1575         }
1576         repos = xmalloc (strlen (lr->repos) + 10);
1577         (void) strcpy (repos, lr->repos);
1578         if ((cp = strrchr (repos, '/')) != NULL)
1579         {
1580             if (lr->mod && !strcmp (++cp, lr->mod))
1581             {
1582                 (void) strcpy (cp, "*");
1583             }
1584         }
1585
1586         switch (ty)
1587         {
1588             case 'T':
1589                 /* 'T'ag records: repository is a "tag type", rev is the tag */
1590                 (void) printf (" %-*s [%s:%s]", mod_len, lr->mod, lr->rev,
1591                                repos);
1592                 if (working)
1593                     (void) printf (" {%s}", workdir);
1594                 break;
1595             case 'F':
1596             case 'E':
1597             case 'O':
1598                 if (lr->rev && *(lr->rev))
1599                     (void) printf (" [%s]", lr->rev);
1600                 (void) printf (" %-*s =%s%-*s %s", repos_len, repos, lr->mod,
1601                                mod_len + 1 - (int) strlen (lr->mod),
1602                                "=", workdir);
1603                 break;
1604             case 'W':
1605             case 'U':
1606             case 'P':
1607             case 'C':
1608             case 'G':
1609             case 'M':
1610             case 'A':
1611             case 'R':
1612                 (void) printf (" %-*s %-*s %-*s =%s= %s", rev_len, lr->rev,
1613                                file_len, lr->file, repos_len, repos,
1614                                lr->mod ? lr->mod : "", workdir);
1615                 break;
1616             default:
1617                 (void) printf ("Hey! What is this junk? RecType[0x%2.2x]", ty);
1618                 break;
1619         }
1620         (void) putchar ('\n');
1621         free (workdir);
1622         free (repos);
1623     }
1624 }
1625
1626 static int
1627 accept_hrec (struct hrec *lr, struct hrec *hr)
1628 {
1629     int ty;
1630
1631     ty = *(lr->type);
1632
1633     if (last_since_tag && ty == 'T')
1634         return 1;
1635
1636     if (v_checkout)
1637     {
1638         if (ty != 'O')
1639             return 0;                   /* Only interested in 'O' records */
1640
1641         /* We want to identify all the states that cause the next record
1642          * ("hr") to be different from the current one ("lr") and only
1643          * print a line at the allowed boundaries.
1644          */
1645
1646         if (!hr ||                      /* The last record */
1647             strcmp (hr->user, lr->user) ||      /* User has changed */
1648             strcmp (hr->mod, lr->mod) ||/* Module has changed */
1649             (working &&                 /* If must match "workdir" */
1650              (strcmp (hr->dir, lr->dir) ||      /*    and the 1st parts or */
1651               strcmp (hr->end, lr->end))))      /*    the 2nd parts differ */
1652
1653             return 1;
1654     }
1655     else if (modified)
1656     {
1657         if (!last_entry ||              /* Don't want only last rec */
1658             !hr ||                      /* Last entry is a "last entry" */
1659             strcmp (hr->repos, lr->repos) ||    /* Repository has changed */
1660             strcmp (hr->file, lr->file))/* File has changed */
1661             return 1;
1662
1663         if (working)
1664         {                               /* If must match "workdir" */
1665             if (strcmp (hr->dir, lr->dir) ||    /*    and the 1st parts or */
1666                 strcmp (hr->end, lr->end))      /*    the 2nd parts differ */
1667                 return 1;
1668         }
1669     }
1670     else if (module_report)
1671     {
1672         if (!last_entry ||              /* Don't want only last rec */
1673             !hr ||                      /* Last entry is a "last entry" */
1674             strcmp (hr->mod, lr->mod) ||/* Module has changed */
1675             strcmp (hr->repos, lr->repos) ||    /* Repository has changed */
1676             strcmp (hr->file, lr->file))/* File has changed */
1677             return 1;
1678     }
1679     else
1680     {
1681         /* "extract" and "tag_report" always print selected records. */
1682         return 1;
1683     }
1684
1685     return 0;
1686 }