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