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
checkout_main(int argc,char ** argv)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
checkout_usage(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
checkout_rev(RCSFILE * file,RCSNUM * frev,const char * dst,int flags,const char * lockname,const char * author,const char * state,const char * date)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
checkout_err_nobranch(RCSFILE * file,const char * author,const char * date,const char * state,int flags)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
checkout_file_has_diffs(RCSFILE * rfp,RCSNUM * frev,const char * dst)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