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
183static 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;
196static long hrec_idx;
197
198
199static void fill_hrec PROTO((char *line, struct hrec * hr));
200static int accept_hrec PROTO((struct hrec * hr, struct hrec * lr));
201static int select_hrec PROTO((struct hrec * hr));
202static int sort_order PROTO((const PTR l, const PTR r));
203static int within PROTO((char *find, char *string));
204static void expand_modules PROTO((void));
205static void read_hrecs PROTO((char *fname));
206static void report_hrecs PROTO((void));
207static void save_file PROTO((char *dir, char *name, char *module));
208static void save_module PROTO((char *module));
209static 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
217static short report_count;
218
219static short extract;
220static short v_checkout;
221static short modified;
222static short tag_report;
223static short module_report;
224static short working;
225static short last_entry;
226static short all_users;
227
228static short user_sort;
229static short repos_sort;
230static short file_sort;
231static short module_sort;
232
233static short tz_local;
234static time_t tz_seconds_east_of_GMT;
235static char *tz_name = "+0000";
236
237char *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.  */
244static char *since_rev;
245static char *since_tag;
246static char *backto;
247/* -D option, or 0 if not specified.  RCS format.  */
248static char * since_date;
249
250static struct hrec *last_since_tag;
251static 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.  */
256static char *rec_types;
257
258static int hrec_count;
259static int hrec_max;
260
261static char **user_list;	/* Ptr to array of ptrs to user names */
262static int user_max;		/* Number of elements allocated */
263static int user_count;		/* Number of elements used */
264
265static struct file_list_str
266{
267    char *l_file;
268    char *l_module;
269} *file_list;			/* Ptr to array file name structs */
270static int file_max;		/* Number of elements allocated */
271static int file_count;		/* Number of elements used */
272
273static char **mod_list;		/* Ptr to array of ptrs to module names */
274static int mod_max;		/* Number of elements allocated */
275static int mod_count;		/* Number of elements used */
276
277static 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.  */
285static 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*/
318static int
319sort_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
369int
370history (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
700void
701history_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 */
891static void
892save_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 */
920static void
921save_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
970static void
971save_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
987static void
988expand_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
1010static void
1011fill_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 */
1069static void
1070read_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" */
1177static int
1178within (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 */
1204static int
1205select_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 */
1411static void
1412report_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
1562static int
1563accept_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