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