xref: /openbsd/usr.bin/rcs/co.c (revision 91f110e0)
1 /*	$OpenBSD: co.c,v 1.117 2013/04/16 20:24:45 deraadt 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[MAXPATHLEN];
54 	char *rev_str, *username;
55 	time_t rcs_mtime = -1;
56 
57 	flags = ret = 0;
58 	kflag = RCS_KWEXP_ERR;
59 	rev = RCS_HEAD_REV;
60 	rev_str = NULL;
61 	author = date = state = NULL;
62 
63 	while ((ch = rcs_getopt(argc, argv, CO_OPTSTRING)) != -1) {
64 		switch (ch) {
65 		case 'd':
66 			date = rcs_optarg;
67 			break;
68 		case 'f':
69 			rcs_setrevstr(&rev_str, rcs_optarg);
70 			flags |= FORCE;
71 			break;
72 		case 'I':
73 			rcs_setrevstr(&rev_str, rcs_optarg);
74 			flags |= INTERACTIVE;
75 			break;
76 
77 		case 'k':
78 			kflag = rcs_kflag_get(rcs_optarg);
79 			if (RCS_KWEXP_INVAL(kflag)) {
80 				warnx("invalid RCS keyword substitution mode");
81 				(usage)();
82 				exit(1);
83 			}
84 			break;
85 		case 'l':
86 			if (flags & CO_UNLOCK) {
87 				warnx("warning: -u overridden by -l");
88 				flags &= ~CO_UNLOCK;
89 			}
90 			rcs_setrevstr(&rev_str, rcs_optarg);
91 			flags |= CO_LOCK;
92 			break;
93 		case 'M':
94 			rcs_setrevstr(&rev_str, rcs_optarg);
95 			flags |= CO_REVDATE;
96 			break;
97 		case 'p':
98 			rcs_setrevstr(&rev_str, rcs_optarg);
99 			flags |= PIPEOUT;
100 			break;
101 		case 'q':
102 			rcs_setrevstr(&rev_str, rcs_optarg);
103 			flags |= QUIET;
104 			break;
105 		case 'r':
106 			rcs_setrevstr(&rev_str, rcs_optarg);
107 			break;
108 		case 's':
109 			state = rcs_optarg;
110 			flags |= CO_STATE;
111 			break;
112 		case 'T':
113 			flags |= PRESERVETIME;
114 			break;
115 		case 'u':
116 			rcs_setrevstr(&rev_str, rcs_optarg);
117 			if (flags & CO_LOCK) {
118 				warnx("warning: -l overridden by -u");
119 				flags &= ~CO_LOCK;
120 			}
121 			flags |= CO_UNLOCK;
122 			break;
123 		case 'V':
124 			printf("%s\n", rcs_version);
125 			exit(0);
126 		case 'w':
127 			/* if no argument, assume current user */
128 			if (rcs_optarg == NULL) {
129 				if ((author = getlogin()) == NULL)
130 					err(1, "getlogin");
131 			} else
132 				author = rcs_optarg;
133 			flags |= CO_AUTHOR;
134 			break;
135 		case 'x':
136 			/* Use blank extension if none given. */
137 			rcs_suffixes = rcs_optarg ? rcs_optarg : "";
138 			break;
139 		case 'z':
140 			timezone_flag = rcs_optarg;
141 			break;
142 		default:
143 			(usage)();
144 			exit(1);
145 		}
146 	}
147 
148 	argc -= rcs_optind;
149 	argv += rcs_optind;
150 
151 	if (argc == 0) {
152 		warnx("no input file");
153 		(usage)();
154 		exit (1);
155 	}
156 
157 	if ((username = getlogin()) == NULL)
158 		err(1, "getlogin");
159 
160 	for (i = 0; i < argc; i++) {
161 		fd = rcs_choosefile(argv[i], fpath, sizeof(fpath));
162 		if (fd < 0) {
163 			warn("%s", fpath);
164 			ret = 1;
165 			continue;
166 		}
167 		rcs_strip_suffix(argv[i]);
168 
169 		if (!(flags & QUIET))
170 			(void)fprintf(stderr, "%s  -->  %s\n", fpath,
171 			    (flags & PIPEOUT) ? "standard output" : argv[i]);
172 
173 		if ((flags & CO_LOCK) && (kflag & RCS_KWEXP_VAL)) {
174 			warnx("%s: cannot combine -kv and -l", fpath);
175 			(void)close(fd);
176 			continue;
177 		}
178 
179 		if ((file = rcs_open(fpath, fd,
180 		    RCS_RDWR|RCS_PARSE_FULLY)) == NULL)
181 			continue;
182 
183 		if (flags & PRESERVETIME)
184 			rcs_mtime = rcs_get_mtime(file);
185 
186 		rcs_kwexp_set(file, kflag);
187 
188 		if (rev_str != NULL) {
189 			if ((rev = rcs_getrevnum(rev_str, file)) == NULL)
190 				errx(1, "invalid revision: %s", rev_str);
191 		} else {
192 			/* no revisions in RCS file, generate empty 0.0 */
193 			if (file->rf_ndelta == 0) {
194 				rev = rcsnum_parse("0.0");
195 				if (rev == NULL)
196 					errx(1, "failed to generate rev 0.0");
197 			} else {
198 				rev = rcsnum_alloc();
199 				rcsnum_cpy(file->rf_head, rev, 0);
200 			}
201 		}
202 
203 		if (checkout_rev(file, rev, argv[i], flags,
204 		    username, author, state, date) < 0) {
205 			rcs_close(file);
206 			rcsnum_free(rev);
207 			ret = 1;
208 			continue;
209 		}
210 
211 		if (!(flags & QUIET))
212 			(void)fprintf(stderr, "done\n");
213 
214 		rcsnum_free(rev);
215 
216 		rcs_write(file);
217 		if (flags & PRESERVETIME)
218 			rcs_set_mtime(file, rcs_mtime);
219 		rcs_close(file);
220 	}
221 
222 	return (ret);
223 }
224 
225 void
226 checkout_usage(void)
227 {
228 	fprintf(stderr,
229 	    "usage: co [-TV] [-ddate] [-f[rev]] [-I[rev]] [-kmode] [-l[rev]]\n"
230 	    "          [-M[rev]] [-p[rev]] [-q[rev]] [-r[rev]] [-sstate]\n"
231 	    "          [-u[rev]] [-w[user]] [-xsuffixes] [-ztz] file ...\n");
232 }
233 
234 /*
235  * Checkout revision <rev> from RCSFILE <file>, writing it to the path <dst>
236  * Currently recognised <flags> are CO_LOCK, CO_UNLOCK and CO_REVDATE.
237  *
238  * Looks up revision based upon <lockname>, <author>, <state> and <date>
239  *
240  * Returns 0 on success, -1 on failure.
241  */
242 int
243 checkout_rev(RCSFILE *file, RCSNUM *frev, const char *dst, int flags,
244     const char *lockname, const char *author, const char *state,
245     const char *date)
246 {
247 	BUF *bp;
248 	u_int i;
249 	int fd, lcount;
250 	char buf[RCS_REV_BUFSZ];
251 	mode_t mode = DEFFILEMODE;
252 	struct stat st;
253 	struct rcs_delta *rdp;
254 	struct rcs_lock *lkp;
255 	char *fdate;
256 	const char *fstatus;
257 	time_t rcsdate, givendate;
258 	RCSNUM *rev;
259 
260 	rcsdate = givendate = -1;
261 	if (date != NULL && (givendate = date_parse(date)) == -1) {
262 		warnx("invalid date: %s", date);
263 		return -1;
264 	}
265 
266 	if (file->rf_ndelta == 0 && !(flags & QUIET))
267 		(void)fprintf(stderr,
268 		    "no revisions present; generating empty revision 0.0\n");
269 
270 	/* XXX rcsnum_cmp()
271 	 * Check out the latest revision if <frev> is greater than HEAD
272 	 */
273 	if (file->rf_ndelta != 0) {
274 		for (i = 0; i < file->rf_head->rn_len; i++) {
275 			if (file->rf_head->rn_id[i] < frev->rn_id[i]) {
276 				frev = file->rf_head;
277 				break;
278 			}
279 		}
280 	}
281 
282 	lcount = 0;
283 	TAILQ_FOREACH(lkp, &(file->rf_locks), rl_list) {
284 		if (!strcmp(lkp->rl_name, lockname))
285 			lcount++;
286 	}
287 
288 	/*
289 	 * If the user didn't specify any revision, we cycle through
290 	 * revisions to lookup the first one that matches what he specified.
291 	 *
292 	 * If we cannot find one, we return an error.
293 	 */
294 	rdp = NULL;
295 	if (file->rf_ndelta != 0 && frev == file->rf_head) {
296 		if (lcount > 1) {
297 			warnx("multiple revisions locked by %s; "
298 			    "please specify one", lockname);
299 			return (-1);
300 		}
301 
302 		TAILQ_FOREACH(rdp, &file->rf_delta, rd_list) {
303 			if (date != NULL) {
304 				fdate = asctime(&rdp->rd_date);
305 				if ((rcsdate = date_parse(fdate)) == -1) {
306 					warnx("invalid date: %s", fdate);
307 					return -1;
308 				}
309 				if (givendate <= rcsdate)
310 					continue;
311 			}
312 
313 			if (author != NULL &&
314 			    strcmp(rdp->rd_author, author))
315 				continue;
316 
317 			if (state != NULL &&
318 			    strcmp(rdp->rd_state, state))
319 				continue;
320 
321 			frev = rdp->rd_num;
322 			break;
323 		}
324 	} else if (file->rf_ndelta != 0) {
325 		rdp = rcs_findrev(file, frev);
326 	}
327 
328 	if (file->rf_ndelta != 0 && rdp == NULL) {
329 		checkout_err_nobranch(file, author, date, state, flags);
330 		return (-1);
331 	}
332 
333 	if (file->rf_ndelta == 0)
334 		rev = frev;
335 	else
336 		rev = rdp->rd_num;
337 
338 	rcsnum_tostr(rev, buf, sizeof(buf));
339 
340 	if (file->rf_ndelta != 0 && rdp->rd_locker != NULL) {
341 		if (strcmp(lockname, rdp->rd_locker)) {
342 			warnx("Revision %s is already locked by %s; %s",
343 			    buf, rdp->rd_locker,
344 			    (flags & CO_UNLOCK) ? "use co -r or rcs -u" : "");
345 			return (-1);
346 		}
347 	}
348 
349 	if (!(flags & QUIET) && !(flags & NEWFILE) &&
350 	    !(flags & CO_REVERT) && file->rf_ndelta != 0)
351 		(void)fprintf(stderr, "revision %s", buf);
352 
353 	if (file->rf_ndelta != 0) {
354 		if ((bp = rcs_getrev(file, rev)) == NULL) {
355 			warnx("cannot find revision `%s'", buf);
356 			return (-1);
357 		}
358 	} else {
359 		bp = buf_alloc(1);
360 	}
361 
362 	/*
363 	 * Do keyword expansion if required.
364 	 */
365 	if (file->rf_ndelta != 0)
366 		bp = rcs_kwexp_buf(bp, file, rev);
367 	/*
368 	 * File inherits permissions from its ,v file
369 	 */
370 	if (file->rf_file != NULL) {
371 		if (fstat(fileno(file->rf_file), &st) == -1)
372 			err(1, "%s", file->rf_path);
373 		file->rf_mode = mode = st.st_mode;
374 	} else {
375 		mode = file->rf_mode;
376 	}
377 
378 	if (flags & CO_LOCK) {
379 		if (file->rf_ndelta != 0) {
380 			if (lockname != NULL &&
381 			    rcs_lock_add(file, lockname, rev) < 0) {
382 				if (rcs_errno != RCS_ERR_DUPENT)
383 					return (-1);
384 			}
385 		}
386 
387 		/* File should only be writable by owner. */
388 		mode &= ~(S_IWGRP|S_IWOTH);
389 		mode |= S_IWUSR;
390 
391 		if (file->rf_ndelta != 0) {
392 			if (!(flags & QUIET) && !(flags & NEWFILE) &&
393 			    !(flags & CO_REVERT))
394 				(void)fprintf(stderr, " (locked)");
395 		}
396 	} else if (flags & CO_UNLOCK) {
397 		if (file->rf_ndelta != 0) {
398 			if (rcs_lock_remove(file, lockname, rev) < 0) {
399 				if (rcs_errno != RCS_ERR_NOENT)
400 					return (-1);
401 			}
402 		}
403 
404 		/* Strip all write bits from mode */
405 		mode &= ~(S_IWUSR|S_IWGRP|S_IWOTH);
406 
407 		if (file->rf_ndelta != 0) {
408 			if (!(flags & QUIET) && !(flags & NEWFILE) &&
409 			    !(flags & CO_REVERT))
410 				(void)fprintf(stderr, " (unlocked)");
411 		}
412 	}
413 
414 	/* If strict locking is disabled, make file writable by owner. */
415 	if (rcs_lock_getmode(file) == RCS_LOCK_LOOSE)
416 		mode |= S_IWUSR;
417 
418 	if (file->rf_ndelta == 0 && !(flags & QUIET) &&
419 	    ((flags & CO_LOCK) || (flags & CO_UNLOCK))) {
420 		(void)fprintf(stderr, "no revisions, so nothing can be %s\n",
421 		    (flags & CO_LOCK) ? "locked" : "unlocked");
422 	} else if (file->rf_ndelta != 0) {
423 		/* XXX - Not a good way to detect if a newline is needed. */
424 		if (!(flags & QUIET) && !(flags & NEWFILE) &&
425 		    !(flags & CO_REVERT))
426 			(void)fprintf(stderr, "\n");
427 	}
428 
429 	if (flags & CO_LOCK) {
430 		if (rcs_errno != RCS_ERR_DUPENT)
431 			lcount++;
432 		if (!(flags & QUIET) && lcount > 1 && !(flags & CO_REVERT))
433 			warnx("%s: warning: You now have %d locks.",
434 			    file->rf_path, lcount);
435 	}
436 
437 	if ((flags & (PIPEOUT|FORCE)) == 0 && stat(dst, &st) != -1) {
438 		/*
439 		 * Prompt the user if the file is writable or the file is
440 		 * not writable but is different from the RCS head version.
441 		 * This is different from GNU which will silently overwrite
442 		 * the file regardless of its contents so long as it is
443 		 * read-only.
444 		 */
445 		if (st.st_mode & (S_IWUSR|S_IWGRP|S_IWOTH))
446 			fstatus = "writable";
447 		else if (checkout_file_has_diffs(file, frev, dst) != D_SAME)
448 			fstatus = "modified";
449 		else
450 			fstatus = NULL;
451 		if (fstatus) {
452 			(void)fprintf(stderr, "%s %s exists%s; ", fstatus, dst,
453 			    (getuid() == st.st_uid) ? "" :
454 			    ", and you do not own it");
455 			(void)fprintf(stderr, "remove it? [ny](n): ");
456 			if (rcs_yesno('n') == 'n') {
457 				if (!(flags & QUIET) && isatty(STDIN_FILENO))
458 					warnx("%s %s exists; checkout aborted",
459 					    fstatus, dst);
460 				else
461 					warnx("checkout aborted");
462 				return (-1);
463 			}
464 		}
465 	}
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)) < 0)
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) < 0)
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.",
517 	    file->rf_path,
518 	    date ? " a date before " : "",
519 	    date ? date : "",
520 	    author ? " and author " + (date ? 0:4 ) : "",
521 	    author ? author : "",
522 	    state  ? " and state " + (date || author ? 0:4) : "",
523 	    state  ? state : "");
524 }
525 
526 /*
527  * checkout_file_has_diffs()
528  *
529  * Check for diffs between the working file and its current revision.
530  * Same return values as diffreg()
531  */
532 static int
533 checkout_file_has_diffs(RCSFILE *rfp, RCSNUM *frev, const char *dst)
534 {
535 	char *tempfile;
536 	BUF *bp;
537 	int ret;
538 
539 	tempfile = NULL;
540 
541 	if ((bp = rcs_getrev(rfp, frev)) == NULL) {
542 		warnx("failed to load revision");
543 		return (D_ERROR);
544 	}
545 	if ((bp = rcs_kwexp_buf(bp, rfp, frev)) == NULL) {
546 		warnx("failed to expand tags");
547 		return (D_ERROR);
548 	}
549 
550 	(void)xasprintf(&tempfile, "%s/diff.XXXXXXXXXX", rcs_tmpdir);
551 	buf_write_stmp(bp, tempfile);
552 	buf_empty(bp);
553 
554 	diff_format = D_RCSDIFF;
555 	ret = diffreg(dst, tempfile, bp, D_FORCEASCII);
556 
557 	buf_free(bp);
558 	unlink(tempfile);
559 	xfree(tempfile);
560 
561 	return (ret);
562 }
563