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