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