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