diff options
-rw-r--r-- | usr.bin/cvs/repo.c | 638 | ||||
-rw-r--r-- | usr.bin/cvs/repo.h | 156 |
2 files changed, 794 insertions, 0 deletions
diff --git a/usr.bin/cvs/repo.c b/usr.bin/cvs/repo.c new file mode 100644 index 00000000000..013f3538441 --- /dev/null +++ b/usr.bin/cvs/repo.c @@ -0,0 +1,638 @@ +/* $OpenBSD: repo.c,v 1.1 2005/02/16 15:41:15 jfb Exp $ */ +/* + * Copyright (c) 2005 Jean-Francois Brousseau <jfb@openbsd.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <sys/param.h> +#include <sys/queue.h> +#include <sys/time.h> +#include <sys/stat.h> + +#include <errno.h> +#include <stdio.h> +#include <fcntl.h> +#include <stdlib.h> +#include <unistd.h> +#include <dirent.h> +#include <string.h> +#include <libgen.h> + +#include "log.h" +#include "repo.h" +#include "cvsd.h" + + + +static CVSRPENT* cvs_repo_loadrec (CVSREPO *, const char *); + + +/* + * cvs_repo_load() + * + * Load the information for a specific CVS repository whose base directory + * is specified in <base>. + */ + +CVSREPO* +cvs_repo_load(const char *base, int flags) +{ + struct stat st; + CVSREPO *repo; + + cvs_log(LP_DEBUG, "loading repository %s", base); + + if (stat(base, &st) == -1) { + cvs_log(LP_ERRNO, "failed to stat %s", base); + return (NULL); + } + + if (!S_ISDIR(st.st_mode)) { + cvs_log(LP_ERR, "%s: repository path is not a directory", base); + return (NULL); + } + + repo = (struct cvs_repo *)malloc(sizeof(*repo)); + if (repo == NULL) { + cvs_log(LP_ERRNO, "failed to allocate repository data"); + return (NULL); + } + memset(repo, 0, sizeof(*repo)); + + TAILQ_INIT(&(repo->cr_modules)); + + repo->cr_path = strdup(base); + if (repo->cr_path == NULL) { + cvs_log(LP_ERRNO, "failed to copy repository path"); + free(repo); + return (NULL); + } + + repo->cr_tree = cvs_repo_loadrec(repo, repo->cr_path); + if (repo->cr_tree == NULL) { + cvs_repo_free(repo); + return (NULL); + } + + return (repo); +} + + +/* + * cvs_repo_free() + * + * Free the data associated to a repository. + */ + +void +cvs_repo_free(CVSREPO *repo) +{ + CVSMODULE *mod; + + if (repo != NULL) { + if (repo->cr_path != NULL) + free(repo->cr_path); + + while ((mod = TAILQ_FIRST(&(repo->cr_modules))) != NULL) { + TAILQ_REMOVE(&(repo->cr_modules), mod, cm_link); + cvs_repo_modfree(mod); + } + + if (repo->cr_tree != NULL) + cvs_repo_entfree(repo->cr_tree); + + free(repo); + } +} + + +/* + * cvs_repo_lockdir() + * + * Obtain a lock on the directory <dir> which is relative to the root of + * the repository <repo>. The owner of the lock becomes <pid>. + * Returns 0 on success, or -1 on failure. + */ +int +cvs_repo_lockdir(CVSREPO *repo, const char *dir, int type, pid_t owner) +{ + CVSRPENT *ent; + + if ((ent = cvs_repo_find(repo, dir)) == NULL) { + return (-1); + } + + return cvs_repo_lockent(ent, type, owner); +} + + +/* + * cvs_repo_unlockdir() + * + * Attempt to unlock the directory <dir> in the repository <repo>. The <owner> + * argument is used to make sure that the caller really owns the lock it is + * trying to release. + * Returns 0 on success, or -1 on failure. + */ +int +cvs_repo_unlockdir(CVSREPO *repo, const char *dir, pid_t owner) +{ + CVSRPENT *ent; + + if ((ent = cvs_repo_find(repo, dir)) == NULL) { + return (-1); + } + + return cvs_repo_unlockent(ent, owner); +} + + +/* + * cvs_repo_lockent() + * + * Obtain a lock on the entry <ent>. The owner of the lock becomes <pid>. + * Returns 0 on success, or -1 on failure. + */ +int +cvs_repo_lockent(CVSRPENT *ent, int type, pid_t owner) +{ + struct cvs_lock *lk; + struct cvs_lklist *list; + + if ((type != CVS_LOCK_READ) && (type != CVS_LOCK_WRITE)) { + cvs_log(LP_ERR, "invalid lock type (%d) requested"); + return (-1); + } + + lk = (struct cvs_lock *)malloc(sizeof(*lk)); + if (lk == NULL) { + cvs_log(LP_ERRNO, "failed to allocate repository lock"); + return (-1); + } + lk->lk_owner = owner; + lk->lk_type = type; + lk->lk_ent = ent; + + if ((ent->cr_wlock != NULL) && (ent->cr_wlock->lk_owner != 0)) { + /* + * Another process has already locked the entry with a write + * lock, so regardless of the type of lock we are requesting, + * we'll have to wait in the pending requests queue. + */ + if (ent->cr_wlock->lk_owner == owner) { + cvs_log(LP_WARN, "double-lock attempt"); + free(lk); + } else + TAILQ_INSERT_TAIL(&(ent->cr_lkreq), lk, lk_link); + } else { + if (type == CVS_LOCK_READ) { + /* + * If there are any pending write lock requests, + * add the read lock request at the tail of the queue + * instead of assigning it right away. Otherwise, + * we could end up with a write lock request never + * being obtained if other processes make overlapping + * read lock requests. + */ + if (TAILQ_EMPTY(&(ent->cr_lkreq))) + list = &(ent->cr_rlocks); + else + list = &(ent->cr_lkreq); + TAILQ_INSERT_TAIL(list, lk, lk_link); + } else if (type == CVS_LOCK_WRITE) { + if (TAILQ_EMPTY(&(ent->cr_rlocks))) + ent->cr_wlock = lk; + else + TAILQ_INSERT_TAIL(&(ent->cr_lkreq), lk, lk_link); + } + } + + return (0); +} + + +/* + * cvs_repo_unlockent() + * + * Attempt to unlock the entry <ent>. The <owner> argument is used to make + * sure that the caller really owns the lock it is trying to release. + * Returns 0 on success, or -1 on failure. + */ +int +cvs_repo_unlockent(CVSRPENT *ent, pid_t owner) +{ + struct cvs_lock *lk; + + if ((ent->cr_wlock != NULL) && (ent->cr_wlock->lk_owner != 0)) { + if (ent->cr_wlock->lk_owner != owner) { + cvs_log(LP_ERR, "child %d attempted to unlock write " + "lock owned by %d", ent->cr_wlock->lk_owner); + return (-1); + } + + free(ent->cr_wlock); + ent->cr_wlock = NULL; + } else { + TAILQ_FOREACH(lk, &(ent->cr_rlocks), lk_link) { + if (lk->lk_owner == owner) { + TAILQ_REMOVE(&(ent->cr_rlocks), lk, lk_link); + free(lk); + break; + } + } + } + +#ifdef notyet + /* assign lock to any process with a pending request */ + while ((lk = TAILQ_FIRST(&(ent->cr_lkreq))) != NULL) { + TAILQ_REMOVE(&(ent->cr_lkreq), lk, lk_link); + /* XXX send message to process */ + child = cvsd_child_find(lk->lk_owner); + if (child == NULL) + continue; + + break; + } +#endif + + return (0); +} + + +/* + * cvs_repo_alias() + * + * Add a new module entry with name <alias> in the repository <repo>, which + * points to the path <path> within the repository. + * Returns 0 on success, or -1 on failure. + */ +int +cvs_repo_alias(CVSREPO *repo, const char *path, const char *alias) +{ + CVSMODULE *mod; + + mod = (CVSMODULE *)malloc(sizeof(*mod)); + if (mod == NULL) { + cvs_log(LP_ERRNO, "failed to allocate module alias"); + return (-1); + } + memset(mod, 0, sizeof(*mod)); + + mod->cm_name = strdup(alias); + if (mod->cm_name == NULL) { + cvs_log(LP_ERRNO, "failed to allocate module alias"); + free(mod); + return (-1); + } + mod->cm_flags |= CVS_MODULE_ISALIAS; + + mod->cm_path = strdup(path); + if (mod->cm_path == NULL) { + cvs_log(LP_ERRNO, "failed to allocate module alias"); + free(mod->cm_name); + free(mod); + return (-1); + } + + TAILQ_INSERT_TAIL(&(repo->cr_modules), mod, cm_link); + + return (0); +} + + +/* + * cvs_repo_unalias() + * + * Remove the module alias <alias> from the repository <repo>. + * Returns 0 on success, or -1 on failure. + */ +int +cvs_repo_unalias(CVSREPO *repo, const char *alias) +{ + CVSMODULE *mod; + + TAILQ_FOREACH(mod, &(repo->cr_modules), cm_link) { + if (strcmp(mod->cm_name, alias) == 0) { + if (!(mod->cm_flags & CVS_MODULE_ISALIAS)) { + cvs_log(LP_ERR, + "attempt to remove non-aliased module `%s'", + mod->cm_name); + return (-1); + } + + break; + } + } + if (mod == NULL) + return (-1); + + TAILQ_REMOVE(&(repo->cr_modules), mod, cm_link); + return (0); +} + + +/* + * cvs_repo_find() + * + * Find the pointer to a CVS file entry within the file hierarchy <hier>. + * The file's pathname <path> must be relative to the base of <hier>. + * Returns the entry on success, or NULL on failure. + */ +CVSRPENT* +cvs_repo_find(CVSREPO *repo, const char *path) +{ + size_t len; + char *pp, *sp, pbuf[MAXPATHLEN]; + CVSRPENT *sf, *cf; + + if ((len = strlcpy(pbuf, path, sizeof(pbuf))) >= sizeof(pbuf)) { + cvs_log(LP_ERR, "path %s too long", path); + return (NULL); + } + + /* remove any trailing slashes */ + while ((len > 0) && (pbuf[len - 1] == '/')) + pbuf[--len] = '\0'; + + cf = repo->cr_tree; + pp = pbuf; + do { + if (cf->cr_type != CVS_RPENT_DIR) { + cvs_log(LP_ERR, + "part of the path %s is not a directory", path); + return (NULL); + } + sp = strchr(pp, '/'); + if (sp != NULL) + *(sp++) = '\0'; + + /* special case */ + if (*pp == '.') { + if ((*(pp + 1) == '.') && (*(pp + 2) == '\0')) { + /* request to go back to parent */ + if (cf->cr_parent == NULL) { + cvs_log(LP_NOTICE, + "path %s goes back too far", path); + return (NULL); + } + cf = cf->cr_parent; + continue; + } else if (*(pp + 1) == '\0') + continue; + } + + TAILQ_FOREACH(sf, &(cf->cr_files), cr_link) { + if (strcmp(pp, sf->cr_name) == 0) + break; + } + if (sf == NULL) + return (NULL); + + cf = sf; + pp = sp; + } while (sp != NULL); + + return (cf); +} + + +#if 0 +/* + * cvs_repo_getpath() + * + * Get the full path of the file <file> and store it in <buf>, which is of + * size <len>. For portability, it is recommended that <buf> always be + * at least MAXPATHLEN bytes long. + * Returns a pointer to the start of the path on success, or NULL on failure. + */ +char* +cvs_repo_getpath(CVSRPENT *file, char *buf, size_t len) +{ + u_int i; + char *fp, *namevec[CVS_FILE_MAXDEPTH]; + CVSRPENT *top; + + buf[0] = '\0'; + i = CVS_FILE_MAXDEPTH; + memset(namevec, 0, sizeof(namevec)); + + /* find the top node */ + for (top = file; (top != NULL) && (i > 0); top = top->cr_parent) { + fp = top->cr_name; + + /* skip self-references */ + if ((fp[0] == '.') && (fp[1] == '\0')) + continue; + namevec[--i] = fp; + } + + if (i == 0) + return (NULL); + else if (i == CVS_FILE_MAXDEPTH) { + strlcpy(buf, ".", len); + return (buf); + } + + while (i < CVS_FILE_MAXDEPTH - 1) { + strlcat(buf, namevec[i++], len); + strlcat(buf, "/", len); + } + strlcat(buf, namevec[i], len); + + return (buf); +} +#endif + + +/* + * cvs_repo_loadrec() + * + * Recursively load the repository structure + */ +static CVSRPENT* +cvs_repo_loadrec(CVSREPO *repo, const char *path) +{ + int ret, fd; + long base; + u_char *dp, *ep; + mode_t fmode; + char fbuf[2048], pbuf[MAXPATHLEN]; + struct dirent *ent; + CVSRPENT *cfp, *cr_ent; + struct stat st; + + cvs_log(LP_NOTICE, "loading %s", path); + if (stat(path, &st) == -1) { + cvs_log(LP_ERRNO, "failed to stat %s", path); + return (NULL); + } + + cfp = (CVSRPENT *)malloc(sizeof(*cfp)); + if (cfp == NULL) { + cvs_log(LP_ERRNO, "failed to allocate repository entry"); + return (NULL); + } + memset(cfp, 0, sizeof(*cfp)); + TAILQ_INIT(&(cfp->cr_rlocks)); + TAILQ_INIT(&(cfp->cr_lkreq)); + + cfp->cr_name = strdup(basename(path)); + if (cfp->cr_name == NULL) { + cvs_log(LP_ERRNO, "failed to copy entry name"); + free(cfp); + return (NULL); + } + + if (repo->cr_flags & CVS_REPO_CHKPERM) { + if (S_ISDIR(st.st_mode)) + fmode = CVSD_DPERM; + else + fmode = CVSD_FPERM; + /* perform permission checks on the file */ + if (st.st_uid != cvsd_uid) { + cvs_log(LP_WARN, "owner of `%s' is not %s", + path, CVSD_USER); + } + + if (st.st_gid != cvsd_gid) { + cvs_log(LP_WARN, "group of `%s' is not %s", + path, CVSD_GROUP); + } + + if (st.st_mode & S_IWGRP) { + cvs_log(LP_WARN, "file `%s' is group-writable", + path, fmode); + } + + if (st.st_mode & S_IWOTH) { + cvs_log(LP_WARN, "file `%s' is world-writable", + path, fmode); + } + } + + if (S_ISREG(st.st_mode)) + cfp->cr_type = CVS_RPENT_RCSFILE; + else if (S_ISDIR(st.st_mode)) { + cfp->cr_type = CVS_RPENT_DIR; + + TAILQ_INIT(&(cfp->cr_files)); + + if ((fd = open(path, O_RDONLY)) == -1) { + cvs_log(LP_ERRNO, "failed to open `%s'", path); + cvs_repo_entfree(cfp); + return (NULL); + } + + do { + ret = getdirentries(fd, fbuf, sizeof(fbuf), &base); + if (ret == -1) { + cvs_log(LP_ERRNO, + "failed to get directory entries"); + cvs_repo_entfree(cfp); + (void)close(fd); + return (NULL); + } + + dp = fbuf; + ep = fbuf + (size_t)ret; + while (dp < ep) { + ent = (struct dirent *)dp; + dp += ent->d_reclen; + if (ent->d_fileno == 0) + continue; + + if (((ent->d_namlen == 1) && + (ent->d_name[0] == '.')) || + ((ent->d_namlen == 2) && + (ent->d_name[0] == '.') && + (ent->d_name[1] == '.'))) + continue; + + snprintf(pbuf, sizeof(pbuf), "%s/%s", path, + ent->d_name); + + if ((ent->d_type != DT_DIR) && + (ent->d_type != DT_REG)) { + cvs_log(LP_NOTICE, "skipping non-" + "regular file `%s'", pbuf); + continue; + } + + cr_ent = cvs_repo_loadrec(repo, pbuf); + if (cr_ent == NULL) { + cvs_repo_entfree(cfp); + (void)close(fd); + return (NULL); + } + + cr_ent->cr_parent = cfp; + TAILQ_INSERT_TAIL(&(cfp->cr_files), cr_ent, cr_link); + } + } while (ret > 0); + + (void)close(fd); + } + + return (cfp); +} + + +/* + * cvs_repo_entfree() + * + * Free a repository entry structure and all underlying data. In the case of + * directories, any child entries are also freed recursively. + */ +void +cvs_repo_entfree(CVSRPENT *ent) +{ + CVSRPENT *ch_ent; + + if (ent->cr_type == CVS_RPENT_DIR) { + while ((ch_ent = TAILQ_FIRST(&(ent->cr_files))) != NULL) { + TAILQ_REMOVE(&(ent->cr_files), ch_ent, cr_link); + cvs_repo_entfree(ch_ent); + } + + } + + if (ent->cr_name != NULL) + free(ent->cr_name); + free(ent); +} + + +/* + * cvs_repo_modfree() + * + * Free a CVS module structure. + */ +void +cvs_repo_modfree(CVSMODULE *mod) +{ + if (mod->cm_name != NULL) + free(mod->cm_name); + if (mod->cm_path != NULL) + free(mod->cm_path); + free(mod); +} diff --git a/usr.bin/cvs/repo.h b/usr.bin/cvs/repo.h new file mode 100644 index 00000000000..a641097ea6d --- /dev/null +++ b/usr.bin/cvs/repo.h @@ -0,0 +1,156 @@ +/* $OpenBSD: repo.h,v 1.1 2005/02/16 15:41:15 jfb Exp $ */ +/* + * Copyright (c) 2005 Jean-Francois Brousseau <jfb@openbsd.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef REPO_H +#define REPO_H + +#include <sys/types.h> +#include <sys/queue.h> + + +#define CVS_MODULE_ISALIAS 0x01 + +typedef struct cvs_module { + char *cm_name; + int cm_flags; + char *cm_path; /* subpath for aliases, NULL otherwise */ + + TAILQ_ENTRY(cvs_module) cm_link; +} CVSMODULE; + + + +#define CVS_RPENT_UNKNOWN 0 +#define CVS_RPENT_DIR 1 +#define CVS_RPENT_RCSFILE 2 + +typedef struct cvs_repoent CVSRPENT; + +/* + * Repository locks + * ================ + * + * OpenCVS derives from the standard CVS mechanism in the way it manages locks + * on the repository. GNU CVS uses files with 'rfl' and 'wfl' extensions for + * read and write locks on particular directories. + * Using the filesystem for locking semantics has one major drawback: a lock + * can stay even after the process that created it is gone, if it didn't + * perform the appropriate cleanup. This stale lock problem has been known + * to happen with GNU CVS and an intervention from one of the repository + * administrators is required before anyone else can access parts of the + * repository. + * In OpenCVS, a child cvsd needing to access a particular part of the tree + * must first request a lock on that part of the tree by issuing a + * CVS_MSG_LOCK message with the appropriate path. Although the code + * supports locking at the file level, it should only be applied to the + * directory level to avoid extra overhead. Both read and write locks can be + * obtained, though with different behaviour. Multiple simultaneous read locks + * can be obtained on the same entry, but there can only be one active write + * lock. In the case where the directory + * is already locked by another child, a lock wait is added to that entry + * and the child requesting the lock will get a CVSD_MSG_LOCKPEND reply, + * meaning that the lock has not been obtained but the child should block + * until it receives a CVSD_MSG_OK or CVSD_MSG_ERR telling it whether it + * obtained the lock or not. When a child is done modifying the locked portion + * it should release its lock using the CVSD_MSG_UNLOCK request with the path. + * + * NOTES: + * * The current locking mechanism allows a lock to be obtained on a + * subportion of a part that has already been locked by another process. + * * A lock on a directory only allows the owner to modify RCS files found + * within that directory. Any modifications on subdirectories require the + * process to lock those subdirectories as well. + */ + +#define CVS_LOCK_READ 1 +#define CVS_LOCK_WRITE 2 + + +struct cvs_lock { + pid_t lk_owner; + int lk_type; + CVSRPENT *lk_ent; /* backpointer to the entry */ + + TAILQ_ENTRY(cvs_lock) lk_link; + TAILQ_ENTRY(cvs_lock) lk_chlink; +}; + +TAILQ_HEAD(cvs_lklist, cvs_lock); + +struct cvs_repoent { + char *cr_name; + int cr_type; + CVSRPENT *cr_parent; + + union { + TAILQ_HEAD(, cvs_repoent) files; + } cr_data; + + struct cvs_lock *cr_wlock; /* write lock, NULL if none */ + struct cvs_lklist cr_rlocks; /* read locks */ + struct cvs_lklist cr_lkreq; /* pending lock requests */ + + TAILQ_ENTRY(cvs_repoent) cr_link; +}; + +#define cr_files cr_data.files + + + +#define CVS_REPO_LOCKED 0x01 +#define CVS_REPO_READONLY 0x02 +#define CVS_REPO_CHKPERM 0x04 + +TAILQ_HEAD(cvs_modlist, cvs_module); + +typedef struct cvs_repo { + char *cr_path; + int cr_flags; + CVSRPENT *cr_tree; + + struct cvs_modlist cr_modules; + TAILQ_ENTRY(cvs_repo) cr_link; +} CVSREPO; + + + + +CVSREPO* cvs_repo_load (const char *, int); +void cvs_repo_free (CVSREPO *); +int cvs_repo_alias (CVSREPO *, const char *, const char *); +int cvs_repo_unalias (CVSREPO *, const char *); +int cvs_repo_lockdir (CVSREPO *, const char *, int, pid_t); +int cvs_repo_unlockdir (CVSREPO *, const char *, pid_t); +int cvs_repo_lockent (CVSRPENT *, int, pid_t); +int cvs_repo_unlockent (CVSRPENT *, pid_t); +void cvs_repo_entfree (CVSRPENT *); +void cvs_repo_modfree (CVSMODULE *); + +CVSRPENT* cvs_repo_find (CVSREPO *, const char *); + + + +#endif /* REPO_H */ |