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