cvs.c revision 1.21
1/*	$OpenBSD: cvs.c,v 1.21 2004/12/13 16:10:30 xsa Exp $	*/
2/*
3 * Copyright (c) 2004 Jean-Francois Brousseau <jfb@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/types.h>
28#include <sys/wait.h>
29
30#include <err.h>
31#include <pwd.h>
32#include <errno.h>
33#include <stdio.h>
34#include <stdlib.h>
35#include <unistd.h>
36#include <signal.h>
37#include <string.h>
38#include <sysexits.h>
39
40#include "cvs.h"
41#include "log.h"
42#include "file.h"
43
44
45extern char *__progname;
46
47
48/* verbosity level: 0 = really quiet, 1 = quiet, 2 = verbose */
49int verbosity = 2;
50
51/* compression level used with zlib, 0 meaning no compression taking place */
52int   cvs_compress = 0;
53int   cvs_readrc = 1;		/* read .cvsrc on startup */
54int   cvs_trace = 0;
55int   cvs_nolog = 0;
56int   cvs_readonly = 0;
57int   cvs_nocase = 0;   /* set to 1 to disable filename case sensitivity */
58
59char *cvs_defargs;		/* default global arguments from .cvsrc */
60char *cvs_command;		/* name of the command we are running */
61int   cvs_cmdop;
62char *cvs_rootstr;
63char *cvs_rsh = CVS_RSH_DEFAULT;
64char *cvs_editor = CVS_EDITOR_DEFAULT;
65
66char *cvs_msg = NULL;
67
68/* hierarchy of all the files affected by the command */
69CVSFILE *cvs_files;
70
71
72/*
73 * Command dispatch table
74 * ----------------------
75 *
76 * The synopsis field should only contain the list of arguments that the
77 * command supports, without the actual command's name.
78 *
79 * Command handlers are expected to return 0 if no error occured, or one of
80 * the values known in sysexits.h in case of an error.  In case the error
81 * returned is EX_USAGE, the command's usage string is printed to standard
82 * error before returning.
83 */
84static struct cvs_cmd {
85	int     cmd_op;
86	char    cmd_name[CVS_CMD_MAXNAMELEN];
87	char    cmd_alias[CVS_CMD_MAXALIAS][CVS_CMD_MAXNAMELEN];
88	int   (*cmd_hdlr)(int, char **);
89	char   *cmd_synopsis;
90	char   *cmd_opts;
91	char    cmd_descr[CVS_CMD_MAXDESCRLEN];
92	char   *cmd_defargs;
93} cvs_cdt[] = {
94	{
95		CVS_OP_ADD, "add",      { "ad",  "new" }, cvs_add,
96		"[-m msg] file ...",
97		"",
98		"Add a new file/directory to the repository",
99		NULL,
100	},
101	{
102		-1, "admin",    { "adm", "rcs" }, NULL,
103		"",
104		"",
105		"Administration front end for rcs",
106		NULL,
107	},
108	{
109		CVS_OP_ANNOTATE, "annotate", { "ann"        }, cvs_annotate,
110		"[-FflR] [-D date | -r rev] file ...",
111		"",
112		"Show last revision where each line was modified",
113		NULL,
114	},
115	{
116		CVS_OP_CHECKOUT, "checkout", { "co",  "get" }, cvs_checkout,
117		"",
118		"",
119		"Checkout sources for editing",
120		NULL,
121	},
122	{
123		CVS_OP_COMMIT, "commit",   { "ci",  "com" }, cvs_commit,
124		"[-flR] [-F logfile | -m msg] [-r rev] ...",
125		"F:flm:Rr:",
126		"Check files into the repository",
127		NULL,
128	},
129	{
130		CVS_OP_DIFF, "diff",     { "di",  "dif" }, cvs_diff,
131		"[-cilu] [-D date] [-r rev] ...",
132		"cD:ilur:",
133		"Show differences between revisions",
134		NULL,
135	},
136	{
137		-1, "edit",     {              }, NULL,
138		"",
139		"",
140		"Get ready to edit a watched file",
141		NULL,
142	},
143	{
144		-1, "editors",  {              }, NULL,
145		"",
146		"",
147		"See who is editing a watched file",
148		NULL,
149	},
150	{
151		-1, "export",   { "ex",  "exp" }, NULL,
152		"",
153		"",
154		"Export sources from CVS, similar to checkout",
155		NULL,
156	},
157	{
158		CVS_OP_HISTORY, "history",  { "hi",  "his" }, cvs_history,
159		"",
160		"",
161		"Show repository access history",
162		NULL,
163	},
164	{
165		CVS_OP_IMPORT, "import",   { "im",  "imp" }, NULL,
166		"[-d] [-b branch] [-I ign] [-k subst] [-m msg] "
167		"repository vendor-tag release-tags ...",
168		"b:dI:k:m:",
169		"Import sources into CVS, using vendor branches",
170		NULL,
171	},
172	{
173		CVS_OP_INIT, "init",     {              }, cvs_init,
174		"",
175		"",
176		"Create a CVS repository if it doesn't exist",
177		NULL,
178	},
179#if defined(HAVE_KERBEROS)
180	{
181		"kserver",  {}, NULL
182		"",
183		"",
184		"Start a Kerberos authentication CVS server",
185		NULL,
186	},
187#endif
188	{
189		CVS_OP_LOG, "log",      { "lo"         }, cvs_getlog,
190		"",
191		"",
192		"Print out history information for files",
193		NULL,
194	},
195	{
196		-1, "login",    {}, NULL,
197		"",
198		"",
199		"Prompt for password for authenticating server",
200		NULL,
201	},
202	{
203		-1, "logout",   {}, NULL,
204		"",
205		"",
206		"Removes entry in .cvspass for remote repository",
207		NULL,
208	},
209	{
210		-1, "rdiff",    {}, NULL,
211		"",
212		"",
213		"Create 'patch' format diffs between releases",
214		NULL,
215	},
216	{
217		-1, "release",  {}, NULL,
218		"",
219		"",
220		"Indicate that a Module is no longer in use",
221		NULL,
222	},
223	{
224		CVS_OP_REMOVE, "remove",   {}, NULL,
225		"",
226		"",
227		"Remove an entry from the repository",
228		NULL,
229	},
230	{
231		-1, "rlog",     {}, NULL,
232		"",
233		"",
234		"Print out history information for a module",
235		NULL,
236	},
237	{
238		-1, "rtag",     {}, NULL,
239		"",
240		"",
241		"Add a symbolic tag to a module",
242		NULL,
243	},
244	{
245		CVS_OP_SERVER, "server",   {}, cvs_server,
246		"",
247		"",
248		"Server mode",
249		NULL,
250	},
251	{
252		CVS_OP_STATUS, "status",   { "st", "stat" }, cvs_status,
253		"",
254		"",
255		"Display status information on checked out files",
256		NULL,
257	},
258	{
259		CVS_OP_TAG, "tag",      { "ta", "freeze" }, NULL,
260		"",
261		"",
262		"Add a symbolic tag to checked out version of files",
263		NULL,
264	},
265	{
266		-1, "unedit",   {}, NULL,
267		"",
268		"",
269		"Undo an edit command",
270		NULL,
271	},
272	{
273		CVS_OP_UPDATE, "update",   { "up", "upd" }, cvs_update,
274		"",
275		"",
276		"Bring work tree in sync with repository",
277		NULL,
278	},
279	{
280		CVS_OP_VERSION, "version",  { "ve", "ver" }, cvs_version,
281		"", "",
282		"Show current CVS version(s)",
283		NULL,
284	},
285	{
286		-1, "watch",    {}, NULL,
287		"",
288		"",
289		"Set watches",
290		NULL,
291	},
292	{
293		-1, "watchers", {}, NULL,
294		"",
295		"",
296		"See who is watching a file",
297		NULL,
298	},
299};
300
301#define CVS_NBCMD  (sizeof(cvs_cdt)/sizeof(cvs_cdt[0]))
302
303
304
305void             usage        (void);
306void             sigchld_hdlr (int);
307void             cvs_read_rcfile   (void);
308struct cvs_cmd*  cvs_findcmd  (const char *);
309int              cvs_getopt   (int, char **);
310
311
312/*
313 * usage()
314 *
315 * Display usage information.
316 */
317void
318usage(void)
319{
320	fprintf(stderr,
321	    "Usage: %s [-flQqtv] [-d root] [-e editor] [-z level] command [...]\n", __progname);
322}
323
324
325int
326main(int argc, char **argv)
327{
328	char *envstr, *cmd_argv[CVS_CMD_MAXARG], **targv;
329	int i, ret, cmd_argc;
330	struct cvs_cmd *cmdp;
331
332	if (cvs_log_init(LD_STD, 0) < 0)
333		err(1, "failed to initialize logging");
334
335	/* by default, be very verbose */
336	(void)cvs_log_filter(LP_FILTER_UNSET, LP_INFO);
337
338#ifdef DEBUG
339	(void)cvs_log_filter(LP_FILTER_UNSET, LP_DEBUG);
340#endif
341
342	/* check environment so command-line options override it */
343	if ((envstr = getenv("CVS_RSH")) != NULL)
344		cvs_rsh = envstr;
345
346	if (((envstr = getenv("CVSEDITOR")) != NULL) ||
347	    ((envstr = getenv("VISUAL")) != NULL) ||
348	    ((envstr = getenv("EDITOR")) != NULL))
349		cvs_editor = envstr;
350
351	ret = cvs_getopt(argc, argv);
352
353	argc -= ret;
354	argv += ret;
355	if (argc == 0) {
356		usage();
357		exit(EX_USAGE);
358	}
359	cvs_command = argv[0];
360
361	if (cvs_readrc) {
362		cvs_read_rcfile();
363
364		if (cvs_defargs != NULL) {
365			targv = cvs_makeargv(cvs_defargs, &i);
366			if (targv == NULL) {
367				cvs_log(LP_ERR,
368				    "failed to load default arguments to %s",
369				    __progname);
370				exit(EX_OSERR);
371			}
372
373			cvs_getopt(i, targv);
374			cvs_freeargv(targv, i);
375			free(targv);
376		}
377	}
378
379	/* setup signal handlers */
380	signal(SIGPIPE, SIG_IGN);
381
382	cvs_file_init();
383
384	ret = -1;
385
386	cmdp = cvs_findcmd(cvs_command);
387	if (cmdp == NULL) {
388		fprintf(stderr, "Unknown command: `%s'\n\n", cvs_command);
389		fprintf(stderr, "CVS commands are:\n");
390		for (i = 0; i < (int)CVS_NBCMD; i++)
391			fprintf(stderr, "\t%-16s%s\n",
392			    cvs_cdt[i].cmd_name, cvs_cdt[i].cmd_descr);
393		exit(EX_USAGE);
394	}
395
396	if (cmdp->cmd_hdlr == NULL) {
397		cvs_log(LP_ERR, "command `%s' not implemented", cvs_command);
398		exit(1);
399	}
400
401	cvs_cmdop = cmdp->cmd_op;
402
403	cmd_argc = 0;
404	memset(cmd_argv, 0, sizeof(cmd_argv));
405
406	cmd_argv[cmd_argc++] = argv[0];
407	if (cmdp->cmd_defargs != NULL) {
408		/* transform into a new argument vector */
409		ret = cvs_getargv(cmdp->cmd_defargs, cmd_argv + 1,
410		    CVS_CMD_MAXARG - 1);
411		if (ret < 0) {
412			cvs_log(LP_ERRNO, "failed to generate argument vector "
413			    "from default arguments");
414			exit(EX_DATAERR);
415		}
416		cmd_argc += ret;
417	}
418	for (ret = 1; ret < argc; ret++)
419		cmd_argv[cmd_argc++] = argv[ret];
420
421	ret = (*cmdp->cmd_hdlr)(cmd_argc, cmd_argv);
422	if (ret == EX_USAGE) {
423		fprintf(stderr, "Usage: %s %s %s\n", __progname, cvs_command,
424		    cmdp->cmd_synopsis);
425	}
426
427	if (cvs_files != NULL)
428		cvs_file_free(cvs_files);
429
430	return (ret);
431}
432
433
434int
435cvs_getopt(int argc, char **argv)
436{
437	int ret;
438	char *ep;
439
440	while ((ret = getopt(argc, argv, "b:d:e:fHlnQqrtvz:")) != -1) {
441		switch (ret) {
442		case 'b':
443			/*
444			 * We do not care about the bin directory for RCS files
445			 * as this program has no dependencies on RCS programs,
446			 * so it is only here for backwards compatibility.
447			 */
448			cvs_log(LP_NOTICE, "the -b argument is obsolete");
449			break;
450		case 'd':
451			cvs_rootstr = optarg;
452			break;
453		case 'e':
454			cvs_editor = optarg;
455			break;
456		case 'f':
457			cvs_readrc = 0;
458			break;
459		case 'l':
460			cvs_nolog = 1;
461			break;
462		case 'n':
463			break;
464		case 'Q':
465			verbosity = 0;
466			break;
467		case 'q':
468			/* don't override -Q */
469			if (verbosity > 1)
470				verbosity = 1;
471			break;
472		case 'r':
473			cvs_readonly = 1;
474			break;
475		case 't':
476			cvs_trace = 1;
477			break;
478		case 'v':
479			printf("%s\n", CVS_VERSION);
480			exit(0);
481			/* NOTREACHED */
482			break;
483		case 'x':
484			/*
485			 * Kerberos encryption support, kept for compatibility
486			 */
487			break;
488		case 'z':
489			cvs_compress = (int)strtol(optarg, &ep, 10);
490			if (*ep != '\0')
491				errx(1, "error parsing compression level");
492			if (cvs_compress < 0 || cvs_compress > 9)
493				errx(1, "gzip compression level must be "
494				    "between 0 and 9");
495			break;
496		default:
497			usage();
498			exit(EX_USAGE);
499		}
500	}
501
502	ret = optind;
503	optind = 1;
504	optreset = 1;	/* for next call */
505
506	return (ret);
507}
508
509
510/*
511 * cvs_findcmd()
512 *
513 * Find the entry in the command dispatch table whose name or one of its
514 * aliases matches <cmd>.
515 * Returns a pointer to the command entry on success, NULL on failure.
516 */
517struct cvs_cmd*
518cvs_findcmd(const char *cmd)
519{
520	u_int i, j;
521	struct cvs_cmd *cmdp;
522
523	cmdp = NULL;
524
525	for (i = 0; (i < CVS_NBCMD) && (cmdp == NULL); i++) {
526		if (strcmp(cmd, cvs_cdt[i].cmd_name) == 0)
527			cmdp = &cvs_cdt[i];
528		else {
529			for (j = 0; j < CVS_CMD_MAXALIAS; j++) {
530				if (strcmp(cmd, cvs_cdt[i].cmd_alias[j]) == 0) {
531					cmdp = &cvs_cdt[i];
532					break;
533				}
534			}
535		}
536	}
537
538	return (cmdp);
539}
540
541
542/*
543 * cvs_read_rcfile()
544 *
545 * Read the CVS `.cvsrc' file in the user's home directory.  If the file
546 * exists, it should contain a list of arguments that should always be given
547 * implicitly to the specified commands.
548 */
549void
550cvs_read_rcfile(void)
551{
552	char rcpath[MAXPATHLEN], linebuf[128], *lp;
553	size_t len;
554	struct cvs_cmd *cmdp;
555	struct passwd *pw;
556	FILE *fp;
557
558	pw = getpwuid(getuid());
559	if (pw == NULL) {
560		cvs_log(LP_NOTICE, "failed to get user's password entry");
561		return;
562	}
563
564	snprintf(rcpath, sizeof(rcpath), "%s/%s", pw->pw_dir, CVS_PATH_RC);
565
566	fp = fopen(rcpath, "r");
567	if (fp == NULL) {
568		if (errno != ENOENT)
569			cvs_log(LP_NOTICE, "failed to open `%s': %s", rcpath,
570			    strerror(errno));
571		return;
572	}
573
574	while (fgets(linebuf, sizeof(linebuf), fp) != NULL) {
575		if ((len = strlen(linebuf)) == 0)
576			continue;
577		if (linebuf[len - 1] != '\n') {
578			cvs_log(LP_WARN, ".cvsrc line too long");
579			break;
580		}
581		linebuf[--len] = '\0';
582
583		lp = strchr(linebuf, ' ');
584		if (lp == NULL)
585			continue;	/* ignore lines with no arguments */
586		*lp = '\0';
587		if (strcmp(linebuf, "cvs") == 0) {
588			/*
589			 * Global default options.  In the case of cvs only,
590			 * we keep the 'cvs' string as first argument because
591			 * getopt() does not like starting at index 0 for
592			 * argument processing.
593			 */
594			*lp = ' ';
595			cvs_defargs = strdup(linebuf);
596			if (cvs_defargs == NULL)
597				cvs_log(LP_ERRNO,
598				    "failed to copy global arguments");
599		} else {
600			lp++;
601			cmdp = cvs_findcmd(linebuf);
602			if (cmdp == NULL) {
603				cvs_log(LP_NOTICE,
604				    "unknown command `%s' in `%s'",
605				    linebuf, rcpath);
606				continue;
607			}
608
609			cmdp->cmd_defargs = strdup(lp);
610			if (cmdp->cmd_defargs == NULL)
611				cvs_log(LP_ERRNO,
612				    "failed to copy default arguments for %s",
613				    cmdp->cmd_name);
614		}
615	}
616	if (ferror(fp)) {
617		cvs_log(LP_NOTICE, "failed to read line from cvsrc");
618	}
619
620	(void)fclose(fp);
621}
622