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