xref: /dragonfly/contrib/cvs-1.12/src/history.c (revision 73610d44)
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
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 *
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
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
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
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
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
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
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
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
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
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
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
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
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
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