1 /* 2 * Copyright (c) 2008 The DragonFly Project. All rights reserved. 3 * 4 * This code is derived from software contributed to The DragonFly Project 5 * by Matthew Dillon <dillon@backplane.com> 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in 15 * the documentation and/or other materials provided with the 16 * distribution. 17 * 3. Neither the name of The DragonFly Project nor the names of its 18 * contributors may be used to endorse or promote products derived 19 * from this software without specific, prior written permission. 20 * 21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 26 * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, 27 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 31 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 32 * SUCH DAMAGE. 33 * 34 * $DragonFly: src/usr.bin/undo/undo.c,v 1.6 2008/07/17 21:34:47 thomas Exp $ 35 */ 36 /* 37 * UNDO - retrieve an older version of a file. 38 */ 39 40 #include <sys/types.h> 41 #include <sys/stat.h> 42 #include <sys/wait.h> 43 #include <sys/tree.h> 44 #include <stdio.h> 45 #include <stdlib.h> 46 #include <stdarg.h> 47 #include <string.h> 48 #include <unistd.h> 49 #include <fcntl.h> 50 #include <errno.h> 51 #include <vfs/hammer/hammer_disk.h> 52 #include <vfs/hammer/hammer_ioctl.h> 53 54 /* 55 * Sorted list of transaction ids 56 */ 57 struct undo_hist_entry; 58 RB_HEAD(undo_hist_entry_rb_tree, undo_hist_entry); 59 RB_PROTOTYPE2(undo_hist_entry_rb_tree, undo_hist_entry, rbnode, 60 undo_hist_entry_compare, hammer_tid_t); 61 62 struct undo_hist_entry { 63 RB_ENTRY(undo_hist_entry) rbnode; 64 struct hammer_ioc_hist_entry tse; 65 ino_t inum; 66 }; 67 68 enum undo_type { TYPE_FILE, TYPE_DIFF, TYPE_RDIFF, TYPE_HISTORY }; 69 enum undo_cmd { CMD_DUMP, CMD_ITERATEALL }; 70 71 #define UNDO_FLAG_MULT 0x0001 72 #define UNDO_FLAG_INOCHG 0x0002 73 #define UNDO_FLAG_TID_INDEX1 0x0004 74 #define UNDO_FLAG_TID_INDEX2 0x0008 75 76 static int undo_hist_entry_compare(struct undo_hist_entry *he1, 77 struct undo_hist_entry *he2); 78 static void doiterate(const char *filename, int flags, 79 struct hammer_ioc_hist_entry ts1, 80 struct hammer_ioc_hist_entry ts2, 81 enum undo_cmd cmd, enum undo_type type); 82 static int doiterate_dump(const char *filename, int flags, 83 struct undo_hist_entry_rb_tree *ptse_tree, 84 struct hammer_ioc_hist_entry ts1, 85 struct hammer_ioc_hist_entry ts2, 86 enum undo_type type); 87 static int doiterate_iterall(const char *filename, int flags, 88 struct undo_hist_entry_rb_tree *ptse_tree, 89 enum undo_type type); 90 static void dogenerate(const char *filename, int flags, 91 struct hammer_ioc_hist_entry ts1, 92 struct hammer_ioc_hist_entry ts2, 93 int idx, enum undo_type type); 94 static void __collect_history(int fd, int *error, 95 struct undo_hist_entry_rb_tree *tse_tree); 96 static void collect_history(const char *filename, int *errorp, 97 struct undo_hist_entry_rb_tree *dir_tree); 98 static void collect_dir_history(const char *filename, int *error, 99 struct undo_hist_entry_rb_tree *dir_tree); 100 static void clean_tree(struct undo_hist_entry_rb_tree *tree); 101 static hammer_tid_t parse_delta_time(const char *timeStr, int *flags, 102 int ind_flag); 103 static FILE *_fopen(const char *filename, const char *mode); 104 static void runcmd(int fd, const char *cmd, ...); 105 static char *timestamp(hammer_ioc_hist_entry_t hen); 106 static void usage(void); 107 108 static int VerboseOpt; 109 static const char *OutFileName = NULL; 110 static const char *OutFilePostfix = NULL; 111 112 RB_GENERATE2(undo_hist_entry_rb_tree, undo_hist_entry, rbnode, 113 undo_hist_entry_compare, hammer_tid_t, tse.tid); 114 115 116 int 117 main(int ac, char **av) 118 { 119 enum undo_cmd cmd; 120 enum undo_type type; 121 struct hammer_ioc_hist_entry ts1; 122 struct hammer_ioc_hist_entry ts2; 123 int c; 124 int count_t; 125 int flags; 126 127 bzero(&ts1, sizeof(ts1)); 128 bzero(&ts2, sizeof(ts2)); 129 130 cmd = CMD_DUMP; 131 type = TYPE_FILE; 132 count_t = 0; 133 flags = 0; 134 135 while ((c = getopt(ac, av, "adDiuvo:t:")) != -1) { 136 switch(c) { 137 case 'd': 138 type = TYPE_DIFF; 139 break; 140 case 'D': 141 type = TYPE_RDIFF; 142 break; 143 case 'i': 144 if (type != TYPE_FILE) 145 usage(); 146 type = TYPE_HISTORY; 147 cmd = CMD_ITERATEALL; 148 break; 149 case 'a': 150 cmd = CMD_ITERATEALL; 151 break; 152 case 'u': 153 OutFilePostfix = ".undo"; 154 break; 155 case 'v': 156 ++VerboseOpt; 157 break; 158 case 'o': 159 OutFileName = optarg; 160 break; 161 case 't': 162 /* 163 * Parse one or two -t options. If two are specified 164 * -d is implied (but may be overridden) 165 */ 166 ++count_t; 167 if (count_t == 1) { 168 ts1.tid = parse_delta_time(optarg, &flags, 169 UNDO_FLAG_TID_INDEX1); 170 } else if (count_t == 2) { 171 ts2.tid = parse_delta_time(optarg, &flags, 172 UNDO_FLAG_TID_INDEX2); 173 if (type == TYPE_FILE) 174 type = TYPE_DIFF; 175 } else { 176 usage(); 177 } 178 break; 179 default: 180 usage(); 181 /* NOT REACHED */ 182 break; 183 } 184 } 185 186 /* 187 * Option validation 188 */ 189 if (OutFileName && OutFilePostfix) { 190 fprintf(stderr, "The -o option may not be combined with -u\n"); 191 usage(); 192 } 193 194 ac -= optind; 195 av += optind; 196 if (ac > 1) 197 flags |= UNDO_FLAG_MULT; 198 199 if (ac == 0) 200 usage(); 201 202 /* 203 * Validate the output template, if specified. 204 */ 205 if (OutFileName && (flags & UNDO_FLAG_MULT)) { 206 const char *ptr = OutFileName; 207 int didStr = 0; 208 209 while ((ptr = strchr(ptr, '%')) != NULL) { 210 if (ptr[1] == 's') { 211 if (didStr) { 212 fprintf(stderr, "Malformed output " 213 "template\n"); 214 usage(); 215 } 216 didStr = 1; 217 ++ptr; 218 } else if (ptr[1] != '%') { 219 fprintf(stderr, "Malformed output template\n"); 220 usage(); 221 } else { 222 ptr += 2; 223 } 224 } 225 } 226 227 while (ac) { 228 doiterate(*av, flags, ts1, ts2, cmd, type); 229 ++av; 230 --ac; 231 } 232 return(0); 233 } 234 235 /* 236 * Iterate through a file's history. If cmd == CMD_DUMP we take the 237 * next-to-last transaction id, unless another given. Otherwise if 238 * cmd == CMD_ITERATEALL we scan all transaction ids. 239 * 240 * Also iterate through the directory's history to locate other inodes that 241 * used the particular file name. 242 */ 243 static 244 void 245 doiterate(const char *filename, int flags, 246 struct hammer_ioc_hist_entry ts1, 247 struct hammer_ioc_hist_entry ts2, 248 enum undo_cmd cmd, enum undo_type type) 249 { 250 struct undo_hist_entry_rb_tree dir_tree; 251 struct undo_hist_entry_rb_tree tse_tree; 252 struct undo_hist_entry *tse; 253 struct stat sb; 254 char *path = NULL; 255 int error; 256 257 RB_INIT(&dir_tree); 258 RB_INIT(&tse_tree); 259 260 /* 261 * Use the directory history to locate all possible versions of 262 * the file. 263 */ 264 collect_dir_history(filename, &error, &dir_tree); 265 RB_FOREACH(tse, undo_hist_entry_rb_tree, &dir_tree) { 266 asprintf(&path, "%s@@0x%016jx", filename, (uintmax_t)tse->tse.tid); 267 if (stat(path, &sb) == 0 && (sb.st_mode & S_IFIFO)) { 268 fprintf(stderr, "Warning: fake transaction id %s@@0x%016jx\n", 269 filename, 270 (uintmax_t)tse->tse.tid); 271 free(path); 272 continue; 273 } 274 collect_history(path, &error, &tse_tree); 275 free(path); 276 } 277 collect_history(filename, &error, &tse_tree); 278 279 switch (cmd) { 280 case CMD_DUMP: 281 if (doiterate_dump(filename, flags, &tse_tree, ts1, ts2, type) == -1) 282 printf("%s: No UNDO history found\n", filename); 283 break; 284 case CMD_ITERATEALL: 285 if (doiterate_iterall(filename, flags, &tse_tree, type) == -1) 286 printf("%s: No UNDO history found\n", filename); 287 break; 288 default: 289 fprintf(stderr, "Invalid command %d\n", cmd); 290 break; 291 } 292 293 clean_tree(&dir_tree); 294 clean_tree(&tse_tree); 295 } 296 297 static 298 int 299 doiterate_dump(const char *filename, int flags, 300 struct undo_hist_entry_rb_tree *ptse_tree, 301 struct hammer_ioc_hist_entry ts1, 302 struct hammer_ioc_hist_entry ts2, 303 enum undo_type type) 304 { 305 struct undo_hist_entry *tse1; 306 struct undo_hist_entry *tse2; 307 308 /* 309 * Find entry if tid set to placeholder index 310 */ 311 if (flags & UNDO_FLAG_TID_INDEX1) { 312 tse1 = RB_MAX(undo_hist_entry_rb_tree, ptse_tree); 313 while (tse1 && ts1.tid--) { 314 tse1 = RB_PREV(undo_hist_entry_rb_tree, 315 ptse_tree, tse1); 316 } 317 if (tse1) 318 ts1 = tse1->tse; 319 else 320 ts1.tid = 0; 321 } 322 if (flags & UNDO_FLAG_TID_INDEX2) { 323 tse2 = RB_MAX(undo_hist_entry_rb_tree, ptse_tree); 324 while (tse2 && ts2.tid--) { 325 tse2 = RB_PREV(undo_hist_entry_rb_tree, 326 ptse_tree, tse2); 327 } 328 if (tse2) 329 ts2 = tse2->tse; 330 else 331 ts2.tid = 0; 332 } 333 334 /* 335 * Single entry, most recent prior to current 336 */ 337 if (ts1.tid == 0) { 338 tse2 = RB_MAX(undo_hist_entry_rb_tree, ptse_tree); 339 if (tse2) { 340 ts2 = tse2->tse; 341 tse1 = RB_PREV(undo_hist_entry_rb_tree, 342 ptse_tree, tse2); 343 if (tse1) 344 ts1 = tse1->tse; 345 } 346 } 347 348 if (ts1.tid) { 349 dogenerate(filename, 0, ts1, ts2, 0, type); 350 return(0); 351 } 352 return(-1); 353 } 354 355 static 356 int 357 doiterate_iterall(const char *filename, int flags, 358 struct undo_hist_entry_rb_tree *ptse_tree, 359 enum undo_type type) 360 { 361 struct undo_hist_entry *tse1; 362 struct undo_hist_entry *tse2; 363 struct hammer_ioc_hist_entry tid_max; 364 int i; 365 366 if (RB_ROOT(ptse_tree) == NULL) 367 return(-1); 368 369 /* 370 * Iterate entire history 371 */ 372 printf("%s: ITERATE ENTIRE HISTORY\n", filename); 373 374 tse1 = NULL; 375 i = 0; 376 RB_FOREACH(tse2, undo_hist_entry_rb_tree, ptse_tree) { 377 if (tse1) { 378 dogenerate(filename, flags, tse1->tse, tse2->tse, i, type); 379 } 380 if (tse1 && tse2->inum != tse1->inum) 381 flags |= UNDO_FLAG_INOCHG; 382 else 383 flags &= ~UNDO_FLAG_INOCHG; 384 tse1 = tse2; 385 ++i; 386 } 387 388 /* 389 * There is no delta to print for the last pair, 390 * because they are identical. 391 */ 392 if (type != TYPE_DIFF && type != TYPE_RDIFF) { 393 tid_max.tid = HAMMER_MAX_TID; 394 tid_max.time32 = 0; 395 dogenerate(filename, flags, tse1->tse, tid_max, i, type); 396 } 397 return(0); 398 } 399 400 /* 401 * Generate output for a file as-of ts1 (ts1 may be 0!), if diffing then 402 * through ts2. 403 */ 404 static 405 void 406 dogenerate(const char *filename, int flags, 407 struct hammer_ioc_hist_entry ts1, 408 struct hammer_ioc_hist_entry ts2, 409 int idx, enum undo_type type) 410 { 411 struct stat st; 412 const char *elm; 413 char *ipath1 = NULL; 414 char *ipath2 = NULL; 415 FILE *fi; 416 FILE *fp; 417 char *buf; 418 char *path; 419 time_t t; 420 struct tm *tp; 421 char datestr[64]; 422 int n; 423 424 /* 425 * Open the input file. If ts1 is 0 try to locate the most recent 426 * version of the file prior to the current version. 427 */ 428 if (ts1.tid == 0) 429 asprintf(&ipath1, "%s", filename); 430 else 431 asprintf(&ipath1, "%s@@0x%016jx", filename, (uintmax_t)ts1.tid); 432 433 if (ts2.tid == 0) 434 asprintf(&ipath2, "%s", filename); 435 else 436 asprintf(&ipath2, "%s@@0x%016jx", filename, (uintmax_t)ts2.tid); 437 438 if (lstat(ipath1, &st) < 0 && lstat(ipath2, &st) < 0) { 439 if (idx == 0 || VerboseOpt) { 440 fprintf(stderr, "Unable to access either %s or %s\n", 441 ipath1, ipath2); 442 } 443 free(ipath1); 444 free(ipath2); 445 return; 446 } 447 448 /* 449 * elm is the last component of the input file name 450 */ 451 if ((elm = strrchr(filename, '/')) != NULL) 452 ++elm; 453 else 454 elm = filename; 455 456 /* 457 * Where do we stuff our output? 458 */ 459 if (OutFileName) { 460 if (flags & UNDO_FLAG_MULT) { 461 asprintf(&path, OutFileName, elm); 462 fp = _fopen(path, "w"); 463 free(path); 464 } else { 465 fp = _fopen(OutFileName, "w"); 466 } 467 } else if (OutFilePostfix) { 468 if (idx >= 0) { 469 asprintf(&path, "%s%s.%04d", filename, 470 OutFilePostfix, idx); 471 } else { 472 asprintf(&path, "%s%s", filename, OutFilePostfix); 473 } 474 fp = _fopen(path, "w"); 475 free(path); 476 } else { 477 if ((flags & UNDO_FLAG_MULT) && type == TYPE_FILE) { 478 if (idx >= 0) { 479 printf("\n>>> %s %04d 0x%016jx %s\n\n", 480 filename, idx, (uintmax_t)ts1.tid, 481 timestamp(&ts1)); 482 } else { 483 printf("\n>>> %s ---- 0x%016jx %s\n\n", 484 filename, (uintmax_t)ts1.tid, 485 timestamp(&ts1)); 486 } 487 } else if (idx >= 0 && type == TYPE_FILE) { 488 printf("\n>>> %s %04d 0x%016jx %s\n\n", 489 filename, idx, (uintmax_t)ts1.tid, 490 timestamp(&ts1)); 491 } 492 fp = stdout; 493 } 494 495 switch(type) { 496 case TYPE_FILE: 497 buf = malloc(8192); 498 if (buf == NULL) { 499 perror("malloc"); 500 exit(1); 501 } 502 if ((fi = fopen(ipath1, "r")) != NULL) { 503 while ((n = fread(buf, 1, 8192, fi)) > 0) 504 fwrite(buf, 1, n, fp); 505 fclose(fi); 506 } 507 free(buf); 508 break; 509 case TYPE_DIFF: 510 printf("diff -N -r -u %s %s (to %s)\n", 511 ipath1, ipath2, timestamp(&ts2)); 512 fflush(stdout); 513 runcmd(fileno(fp), "/usr/bin/diff", "diff", "-N", "-r", "-u", 514 ipath1, ipath2, NULL); 515 break; 516 case TYPE_RDIFF: 517 printf("diff -N -r -u %s %s\n", ipath2, ipath1); 518 fflush(stdout); 519 runcmd(fileno(fp), "/usr/bin/diff", "diff", "-N", "-r", "-u", 520 ipath2, ipath1, NULL); 521 break; 522 case TYPE_HISTORY: 523 t = (time_t)ts1.time32; 524 tp = localtime(&t); 525 strftime(datestr, sizeof(datestr), "%d-%b-%Y %H:%M:%S", tp); 526 printf("\t0x%016jx %s", (uintmax_t)ts1.tid, datestr); 527 if (flags & UNDO_FLAG_INOCHG) 528 printf(" inode-change"); 529 if (lstat(ipath1, &st) < 0) 530 printf(" file-deleted"); 531 printf("\n"); 532 break; 533 } 534 535 if (fp != stdout) 536 fclose(fp); 537 } 538 539 static 540 void 541 clean_tree(struct undo_hist_entry_rb_tree *tree) 542 { 543 struct undo_hist_entry *tse; 544 545 while ((tse = RB_ROOT(tree)) != NULL) { 546 RB_REMOVE(undo_hist_entry_rb_tree, tree, tse); 547 free(tse); 548 } 549 } 550 551 static 552 void 553 __collect_history(int fd, int *errorp, struct undo_hist_entry_rb_tree *tse_tree) 554 { 555 struct hammer_ioc_history hist; 556 struct undo_hist_entry *tse; 557 struct stat st; 558 int i; 559 560 /* 561 * Setup 562 */ 563 bzero(&hist, sizeof(hist)); 564 hist.beg_tid = HAMMER_MIN_TID; 565 hist.end_tid = HAMMER_MAX_TID; 566 hist.head.flags |= HAMMER_IOC_HISTORY_ATKEY; 567 hist.key = 0; 568 hist.nxt_key = HAMMER_MAX_KEY; 569 570 *errorp = 0; 571 572 /* 573 * Save the inode so inode changes can be reported. 574 */ 575 st.st_ino = 0; 576 fstat(fd, &st); 577 578 /* 579 * Collect a unique set of transaction ids 580 */ 581 if (ioctl(fd, HAMMERIOC_GETHISTORY, &hist) < 0) { 582 *errorp = errno; 583 return; 584 } 585 for (;;) { 586 for (i = 0; i < hist.count; ++i) { 587 tse = malloc(sizeof(*tse)); 588 tse->tse = hist.hist_ary[i]; 589 tse->inum = st.st_ino; 590 if (RB_INSERT(undo_hist_entry_rb_tree, tse_tree, tse)) { 591 free(tse); 592 } 593 } 594 if (hist.head.flags & HAMMER_IOC_HISTORY_EOF) 595 break; 596 if (hist.head.flags & HAMMER_IOC_HISTORY_NEXT_KEY) { 597 hist.key = hist.nxt_key; 598 hist.nxt_key = HAMMER_MAX_KEY; 599 } 600 if (hist.head.flags & HAMMER_IOC_HISTORY_NEXT_TID) 601 hist.beg_tid = hist.nxt_tid; 602 if (ioctl(fd, HAMMERIOC_GETHISTORY, &hist) < 0) { 603 *errorp = errno; 604 break; 605 } 606 } 607 } 608 609 static 610 void 611 collect_history(const char *filename, int *errorp, 612 struct undo_hist_entry_rb_tree *dir_tree) 613 { 614 int fd; 615 616 fd = open(filename, O_RDONLY); 617 if (fd == -1) { 618 *errorp = errno; 619 return; 620 } 621 __collect_history(fd, errorp, dir_tree); 622 close(fd); 623 } 624 625 static 626 void 627 collect_dir_history(const char *filename, int *errorp, 628 struct undo_hist_entry_rb_tree *dir_tree) 629 { 630 char *dirname; 631 632 if (strrchr(filename, '/')) { 633 dirname = strdup(filename); 634 *strrchr(dirname, '/') = 0; 635 } else { 636 dirname = strdup("."); 637 } 638 639 collect_history(dirname, errorp, dir_tree); 640 free(dirname); 641 } 642 643 static 644 hammer_tid_t 645 parse_delta_time(const char *timeStr, int *flags, int ind_flag) 646 { 647 hammer_tid_t tid; 648 649 tid = strtoull(timeStr, NULL, 0); 650 if (timeStr[0] == '+') 651 ++timeStr; 652 if (timeStr[0] >= '0' && timeStr[0] <= '9' && timeStr[1] != 'x') 653 *flags |= ind_flag; 654 return(tid); 655 } 656 657 static 658 FILE* 659 _fopen(const char *filename, const char *mode) 660 { 661 FILE *fp; 662 663 fp = fopen(filename, mode); 664 if (fp == NULL) { 665 perror(filename); 666 exit(1); 667 } 668 return(fp); 669 } 670 671 static void 672 runcmd(int fd, const char *cmd, ...) 673 { 674 va_list va; 675 pid_t pid; 676 char **av; 677 int ac; 678 int i; 679 680 va_start(va, cmd); 681 for (ac = 0; va_arg(va, void *) != NULL; ++ac) 682 ; 683 va_end(va); 684 685 av = malloc((ac + 1) * sizeof(char *)); 686 va_start(va, cmd); 687 for (i = 0; i < ac; ++i) 688 av[i] = va_arg(va, char *); 689 va_end(va); 690 av[i] = NULL; 691 692 if ((pid = fork()) < 0) { 693 perror("fork"); 694 exit(1); 695 } else if (pid == 0) { 696 if (fd != 1) { 697 dup2(fd, 1); 698 close(fd); 699 } 700 execv(cmd, av); 701 _exit(1); 702 } else { 703 while (waitpid(pid, NULL, 0) != pid) 704 ; 705 } 706 free(av); 707 } 708 709 /* 710 * Convert tid to timestamp. 711 */ 712 static char * 713 timestamp(hammer_ioc_hist_entry_t hen) 714 { 715 static char timebuf[64]; 716 time_t t = (time_t)hen->time32; 717 struct tm *tp; 718 719 tp = localtime(&t); 720 strftime(timebuf, sizeof(timebuf), "%d-%b-%Y %H:%M:%S", tp); 721 return(timebuf); 722 } 723 724 static 725 int 726 undo_hist_entry_compare(struct undo_hist_entry *he1, 727 struct undo_hist_entry *he2) 728 { 729 if (he1->tse.tid < he2->tse.tid) 730 return(-1); 731 if (he1->tse.tid > he2->tse.tid) 732 return(1); 733 return(0); 734 } 735 736 static void 737 usage(void) 738 { 739 fprintf(stderr, "undo [-adDiuv] [-o outfile] " 740 "[-t transaction-id] [-t transaction-id] path...\n" 741 " -a Iterate all historical segments\n" 742 " -d Forward diff\n" 743 " -D Reverse diff\n" 744 " -i Dump history transaction ids\n" 745 " -u Generate .undo files\n" 746 " -v Verbose\n" 747 " -o file Output to the specified file\n" 748 " -t TID Retrieve as of transaction-id, TID\n" 749 " (a second `-t TID' to diff two)\n" 750 " transaction ids must be prefixed with 0x, and\n" 751 " otherwise may specify an index starting at 0\n" 752 " and iterating backwards through the history.\n" 753 ); 754 exit(1); 755 } 756 757