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