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