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