xref: /openbsd/usr.bin/rcs/co.c (revision 6541b77c)
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