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