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