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