1 /* $OpenBSD: co.c,v 1.127 2023/08/11 05:02:21 guenther Exp $ */ 2 /* 3 * Copyright (c) 2005 Joris Vink <joris@openbsd.org> 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions 8 * are met: 9 * 10 * 1. Redistributions of source code must retain the above copyright 11 * notice, this list of conditions and the following disclaimer. 12 * 2. The name of the author may not be used to endorse or promote products 13 * derived from this software without specific prior written permission. 14 * 15 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 17 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 18 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 21 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 22 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 23 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 24 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27 #include <sys/stat.h> 28 #include <sys/time.h> 29 30 #include <err.h> 31 #include <fcntl.h> 32 #include <stdio.h> 33 #include <stdlib.h> 34 #include <string.h> 35 #include <unistd.h> 36 37 #include "rcsprog.h" 38 #include "diff.h" 39 40 #define CO_OPTSTRING "d:f::I::k:l::M::p::q::r::s:Tu::Vw::x::z::" 41 42 static void checkout_err_nobranch(RCSFILE *, const char *, const char *, 43 const char *, int); 44 static int checkout_file_has_diffs(RCSFILE *, RCSNUM *, const char *); 45 46 int 47 checkout_main(int argc, char **argv) 48 { 49 int fd, i, ch, flags, kflag, ret; 50 RCSNUM *rev; 51 RCSFILE *file; 52 const char *author, *date, *state; 53 char fpath[PATH_MAX]; 54 char *rev_str, *username; 55 struct timespec rcs_mtime = { .tv_sec = 0, .tv_nsec = UTIME_OMIT }; 56 57 flags = ret = 0; 58 kflag = RCS_KWEXP_ERR; 59 rev_str = NULL; 60 author = date = state = NULL; 61 62 while ((ch = rcs_getopt(argc, argv, CO_OPTSTRING)) != -1) { 63 switch (ch) { 64 case 'd': 65 date = rcs_optarg; 66 break; 67 case 'f': 68 rcs_setrevstr(&rev_str, rcs_optarg); 69 flags |= FORCE; 70 break; 71 case 'I': 72 rcs_setrevstr(&rev_str, rcs_optarg); 73 flags |= INTERACTIVE; 74 break; 75 76 case 'k': 77 kflag = rcs_kflag_get(rcs_optarg); 78 if (RCS_KWEXP_INVAL(kflag)) { 79 warnx("invalid RCS keyword substitution mode"); 80 (usage)(); 81 } 82 break; 83 case 'l': 84 if (flags & CO_UNLOCK) { 85 warnx("warning: -u overridden by -l"); 86 flags &= ~CO_UNLOCK; 87 } 88 rcs_setrevstr(&rev_str, rcs_optarg); 89 flags |= CO_LOCK; 90 break; 91 case 'M': 92 rcs_setrevstr(&rev_str, rcs_optarg); 93 flags |= CO_REVDATE; 94 break; 95 case 'p': 96 rcs_setrevstr(&rev_str, rcs_optarg); 97 flags |= PIPEOUT; 98 break; 99 case 'q': 100 rcs_setrevstr(&rev_str, rcs_optarg); 101 flags |= QUIET; 102 break; 103 case 'r': 104 rcs_setrevstr(&rev_str, rcs_optarg); 105 break; 106 case 's': 107 state = rcs_optarg; 108 flags |= CO_STATE; 109 break; 110 case 'T': 111 flags |= PRESERVETIME; 112 break; 113 case 'u': 114 rcs_setrevstr(&rev_str, rcs_optarg); 115 if (flags & CO_LOCK) { 116 warnx("warning: -l overridden by -u"); 117 flags &= ~CO_LOCK; 118 } 119 flags |= CO_UNLOCK; 120 break; 121 case 'V': 122 printf("%s\n", rcs_version); 123 exit(0); 124 case 'w': 125 /* if no argument, assume current user */ 126 if (rcs_optarg == NULL) { 127 if ((author = getlogin()) == NULL) 128 err(1, "getlogin"); 129 } else 130 author = rcs_optarg; 131 flags |= CO_AUTHOR; 132 break; 133 case 'x': 134 /* Use blank extension if none given. */ 135 rcs_suffixes = rcs_optarg ? rcs_optarg : ""; 136 break; 137 case 'z': 138 timezone_flag = rcs_optarg; 139 break; 140 default: 141 (usage)(); 142 } 143 } 144 145 argc -= rcs_optind; 146 argv += rcs_optind; 147 148 if (argc == 0) { 149 warnx("no input file"); 150 (usage)(); 151 } 152 153 if ((username = getlogin()) == NULL) 154 err(1, "getlogin"); 155 156 for (i = 0; i < argc; i++) { 157 fd = rcs_choosefile(argv[i], fpath, sizeof(fpath)); 158 if (fd < 0) { 159 warn("%s", fpath); 160 ret = 1; 161 continue; 162 } 163 rcs_strip_suffix(argv[i]); 164 165 if (!(flags & QUIET)) 166 (void)fprintf(stderr, "%s --> %s\n", fpath, 167 (flags & PIPEOUT) ? "standard output" : argv[i]); 168 169 if ((flags & CO_LOCK) && (kflag & RCS_KWEXP_VAL)) { 170 warnx("%s: cannot combine -kv and -l", fpath); 171 (void)close(fd); 172 continue; 173 } 174 175 if ((file = rcs_open(fpath, fd, 176 RCS_RDWR|RCS_PARSE_FULLY)) == NULL) 177 continue; 178 179 if (flags & PRESERVETIME) 180 rcs_mtime = rcs_get_mtime(file); 181 182 rcs_kwexp_set(file, kflag); 183 184 if (rev_str != NULL) { 185 if ((rev = rcs_getrevnum(rev_str, file)) == NULL) 186 errx(1, "invalid revision: %s", rev_str); 187 } else { 188 /* no revisions in RCS file, generate empty 0.0 */ 189 if (file->rf_ndelta == 0) { 190 rev = rcsnum_parse("0.0"); 191 if (rev == NULL) 192 errx(1, "failed to generate rev 0.0"); 193 } else { 194 rev = rcsnum_alloc(); 195 rcsnum_cpy(file->rf_head, rev, 0); 196 } 197 } 198 199 if (checkout_rev(file, rev, argv[i], flags, 200 username, author, state, date) < 0) { 201 rcs_close(file); 202 rcsnum_free(rev); 203 ret = 1; 204 continue; 205 } 206 207 if (!(flags & QUIET)) 208 (void)fprintf(stderr, "done\n"); 209 210 rcsnum_free(rev); 211 212 rcs_write(file); 213 if (flags & PRESERVETIME) 214 rcs_set_mtime(file, rcs_mtime); 215 rcs_close(file); 216 } 217 218 return (ret); 219 } 220 221 __dead void 222 checkout_usage(void) 223 { 224 fprintf(stderr, 225 "usage: co [-TV] [-ddate] [-f[rev]] [-I[rev]] [-kmode] [-l[rev]]\n" 226 " [-M[rev]] [-p[rev]] [-q[rev]] [-r[rev]] [-sstate]\n" 227 " [-u[rev]] [-w[user]] [-xsuffixes] [-ztz] file ...\n"); 228 229 exit(1); 230 } 231 232 /* 233 * Checkout revision <rev> from RCSFILE <file>, writing it to the path <dst> 234 * Currently recognised <flags> are CO_LOCK, CO_UNLOCK and CO_REVDATE. 235 * 236 * Looks up revision based upon <lockname>, <author>, <state> and <date> 237 * 238 * Returns 0 on success, -1 on failure. 239 */ 240 int 241 checkout_rev(RCSFILE *file, RCSNUM *frev, const char *dst, int flags, 242 const char *lockname, const char *author, const char *state, 243 const char *date) 244 { 245 BUF *bp; 246 u_int i; 247 int fd, lcount; 248 char buf[RCS_REV_BUFSZ]; 249 mode_t mode = DEFFILEMODE; 250 struct stat st; 251 struct rcs_delta *rdp; 252 struct rcs_lock *lkp; 253 char *fdate; 254 const char *fstatus; 255 time_t rcsdate, givendate; 256 RCSNUM *rev; 257 258 givendate = -1; 259 if (date != NULL && (givendate = date_parse(date)) == -1) { 260 warnx("invalid date: %s", date); 261 return -1; 262 } 263 264 if (file->rf_ndelta == 0 && !(flags & QUIET)) 265 (void)fprintf(stderr, 266 "no revisions present; generating empty revision 0.0\n"); 267 268 /* XXX rcsnum_cmp() 269 * Check out the latest revision if <frev> is greater than HEAD 270 */ 271 if (file->rf_ndelta != 0) { 272 for (i = 0; i < file->rf_head->rn_len; i++) { 273 if (file->rf_head->rn_id[i] < frev->rn_id[i]) { 274 frev = file->rf_head; 275 break; 276 } 277 } 278 } 279 280 lcount = 0; 281 TAILQ_FOREACH(lkp, &(file->rf_locks), rl_list) { 282 if (!strcmp(lkp->rl_name, lockname)) 283 lcount++; 284 } 285 286 /* 287 * If the user didn't specify any revision, we cycle through 288 * revisions to lookup the first one that matches what he specified. 289 * 290 * If we cannot find one, we return an error. 291 */ 292 rdp = NULL; 293 if (file->rf_ndelta != 0 && frev == file->rf_head) { 294 if (lcount > 1) { 295 warnx("multiple revisions locked by %s; " 296 "please specify one", lockname); 297 return (-1); 298 } 299 300 TAILQ_FOREACH(rdp, &file->rf_delta, rd_list) { 301 if (date != NULL) { 302 fdate = asctime(&rdp->rd_date); 303 if ((rcsdate = date_parse(fdate)) == -1) { 304 warnx("invalid date: %s", fdate); 305 return -1; 306 } 307 if (givendate <= rcsdate) 308 continue; 309 } 310 311 if (author != NULL && 312 strcmp(rdp->rd_author, author)) 313 continue; 314 315 if (state != NULL && 316 strcmp(rdp->rd_state, state)) 317 continue; 318 319 frev = rdp->rd_num; 320 break; 321 } 322 } else if (file->rf_ndelta != 0) { 323 rdp = rcs_findrev(file, frev); 324 } 325 326 if (file->rf_ndelta != 0 && rdp == NULL) { 327 checkout_err_nobranch(file, author, date, state, flags); 328 return (-1); 329 } 330 331 if (file->rf_ndelta == 0) 332 rev = frev; 333 else 334 rev = rdp->rd_num; 335 336 rcsnum_tostr(rev, buf, sizeof(buf)); 337 338 if (file->rf_ndelta != 0 && rdp->rd_locker != NULL) { 339 if (strcmp(lockname, rdp->rd_locker)) { 340 warnx("Revision %s is already locked by %s; %s", 341 buf, rdp->rd_locker, 342 (flags & CO_UNLOCK) ? "use co -r or rcs -u" : ""); 343 return (-1); 344 } 345 } 346 347 if (!(flags & QUIET) && !(flags & NEWFILE) && 348 !(flags & CO_REVERT) && file->rf_ndelta != 0) 349 (void)fprintf(stderr, "revision %s", buf); 350 351 if (file->rf_ndelta != 0) { 352 if ((bp = rcs_getrev(file, rev)) == NULL) { 353 warnx("cannot find revision `%s'", buf); 354 return (-1); 355 } 356 } else { 357 bp = buf_alloc(1); 358 } 359 360 /* 361 * File inherits permissions from its ,v file 362 */ 363 if (file->rf_file != NULL) { 364 if (fstat(fileno(file->rf_file), &st) == -1) 365 err(1, "%s", file->rf_path); 366 file->rf_mode = mode = st.st_mode; 367 } else { 368 mode = file->rf_mode; 369 } 370 371 if (flags & CO_LOCK) { 372 /* File should only be writable by owner. */ 373 mode &= ~(S_IWGRP|S_IWOTH); 374 mode |= S_IWUSR; 375 376 if (file->rf_ndelta != 0) { 377 if (!(flags & QUIET) && !(flags & NEWFILE) && 378 !(flags & CO_REVERT)) 379 (void)fprintf(stderr, " (locked)\n"); 380 } 381 } else if (flags & CO_UNLOCK) { 382 if (file->rf_ndelta != 0) { 383 if (rcs_lock_remove(file, lockname, rev) < 0) { 384 if (rcs_errno != RCS_ERR_NOENT) 385 return (-1); 386 } 387 } 388 389 /* Strip all write bits from mode */ 390 mode &= ~(S_IWUSR|S_IWGRP|S_IWOTH); 391 392 if (file->rf_ndelta != 0) { 393 if (!(flags & QUIET) && !(flags & NEWFILE) && 394 !(flags & CO_REVERT)) 395 (void)fprintf(stderr, " (unlocked)\n"); 396 } 397 } else { 398 if (file->rf_ndelta != 0) { 399 if (!(flags & QUIET) && !(flags & NEWFILE) && 400 !(flags & CO_REVERT)) 401 (void)fprintf(stderr, "\n"); 402 } 403 } 404 405 if ((flags & (PIPEOUT|FORCE)) == 0 && stat(dst, &st) != -1) { 406 /* 407 * Prompt the user if the file is writable or the file is 408 * not writable but is different from the RCS head version. 409 * This is different from GNU which will silently overwrite 410 * the file regardless of its contents so long as it is 411 * read-only. 412 */ 413 if (st.st_mode & (S_IWUSR|S_IWGRP|S_IWOTH)) 414 fstatus = "writable"; 415 else if (checkout_file_has_diffs(file, frev, dst) != D_SAME) 416 fstatus = "modified"; 417 else 418 fstatus = NULL; 419 if (fstatus) { 420 (void)fprintf(stderr, "%s %s exists%s; ", fstatus, dst, 421 (getuid() == st.st_uid) ? "" : 422 ", and you do not own it"); 423 (void)fprintf(stderr, "remove it? [ny](n): "); 424 if (rcs_yesno('n') == 'n') { 425 if (!(flags & QUIET) && isatty(STDIN_FILENO)) 426 warnx("%s %s exists; checkout aborted", 427 fstatus, dst); 428 else 429 warnx("checkout aborted"); 430 return (-1); 431 } 432 } 433 } 434 435 if (flags & CO_LOCK) { 436 if (file->rf_ndelta != 0) { 437 if (lockname != NULL && 438 rcs_lock_add(file, lockname, rev) < 0) { 439 if (rcs_errno != RCS_ERR_DUPENT) 440 return (-1); 441 } 442 } 443 } 444 445 /* If strict locking is disabled, make file writable by owner. */ 446 if (rcs_lock_getmode(file) == RCS_LOCK_LOOSE) 447 mode |= S_IWUSR; 448 449 if (file->rf_ndelta == 0 && !(flags & QUIET) && 450 ((flags & CO_LOCK) || (flags & CO_UNLOCK))) { 451 (void)fprintf(stderr, "no revisions, so nothing can be %s\n", 452 (flags & CO_LOCK) ? "locked" : "unlocked"); 453 } 454 455 if (flags & CO_LOCK) { 456 if (rcs_errno != RCS_ERR_DUPENT) 457 lcount++; 458 if (!(flags & QUIET) && lcount > 1 && !(flags & CO_REVERT)) 459 warnx("%s: warning: You now have %d locks.", 460 file->rf_path, lcount); 461 } 462 463 /* Finally do keyword expansion if required. */ 464 if (file->rf_ndelta != 0) 465 bp = rcs_kwexp_buf(bp, file, rev); 466 467 if (flags & PIPEOUT) 468 buf_write_fd(bp, STDOUT_FILENO); 469 else { 470 (void)unlink(dst); 471 472 if ((fd = open(dst, O_WRONLY|O_CREAT|O_TRUNC, mode)) == -1) 473 err(1, "%s", dst); 474 475 if (buf_write_fd(bp, fd) < 0) { 476 warnx("failed to write revision to file"); 477 buf_free(bp); 478 (void)close(fd); 479 return (-1); 480 } 481 482 if (fchmod(fd, mode) == -1) 483 warn("%s", dst); 484 485 if (flags & CO_REVDATE) { 486 struct timeval tv[2]; 487 memset(&tv, 0, sizeof(tv)); 488 tv[0].tv_sec = rcs_rev_getdate(file, rev); 489 tv[1].tv_sec = tv[0].tv_sec; 490 if (futimes(fd, (const struct timeval *)&tv) == -1) 491 warn("utimes"); 492 } 493 494 (void)close(fd); 495 } 496 497 buf_free(bp); 498 499 return (0); 500 } 501 502 /* 503 * checkout_err_nobranch() 504 * 505 * XXX - should handle the dates too. 506 */ 507 static void 508 checkout_err_nobranch(RCSFILE *file, const char *author, const char *date, 509 const char *state, int flags) 510 { 511 if (!(flags & CO_AUTHOR)) 512 author = NULL; 513 if (!(flags & CO_STATE)) 514 state = NULL; 515 516 warnx("%s: No revision on branch has %s%s%s%s%s%s%s%s.", 517 file->rf_path, 518 date ? "a date before " : "", 519 date ? date : "", 520 (date && author) ? " and " : "", 521 author ? "author " : "", 522 author ? author : "", 523 ((date || author) && state) ? " and " : "", 524 state ? "state " : "", 525 state ? state : ""); 526 527 } 528 529 /* 530 * checkout_file_has_diffs() 531 * 532 * Check for diffs between the working file and its current revision. 533 * Same return values as diffreg() 534 */ 535 static int 536 checkout_file_has_diffs(RCSFILE *rfp, RCSNUM *frev, const char *dst) 537 { 538 char *tempfile; 539 BUF *bp; 540 int ret; 541 542 tempfile = NULL; 543 544 if ((bp = rcs_getrev(rfp, frev)) == NULL) { 545 warnx("failed to load revision"); 546 return (D_ERROR); 547 } 548 if ((bp = rcs_kwexp_buf(bp, rfp, frev)) == NULL) { 549 warnx("failed to expand tags"); 550 return (D_ERROR); 551 } 552 553 (void)xasprintf(&tempfile, "%s/diff.XXXXXXXXXX", rcs_tmpdir); 554 buf_write_stmp(bp, tempfile); 555 buf_empty(bp); 556 557 diff_format = D_RCSDIFF; 558 ret = diffreg(dst, tempfile, bp, D_FORCEASCII); 559 560 buf_free(bp); 561 unlink(tempfile); 562 free(tempfile); 563 564 return (ret); 565 } 566