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