/*
* Copyright 2001, 2002, 2003 David Mansfield and Cobite, Inc.
* See COPYING file for license information
*/
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <limits.h>
#include <stdarg.h>
#include <zlib.h>
#include <sys/socket.h>
#include <cbtcommon/debug.h>
#include <cbtcommon/text_util.h>
#include <cbtcommon/tcpsocket.h>
#include <cbtcommon/sio.h>
#include "cvs_direct.h"
#include "util.h"
#define RD_BUFF_SIZE 4096
struct _CvsServerCtx
{
int read_fd;
int write_fd;
char root[PATH_MAX];
int is_pserver;
/* buffered reads from descriptor */
char read_buff[RD_BUFF_SIZE];
char * head;
char * tail;
int compressed;
z_stream zout;
z_stream zin;
/* when reading compressed data, the compressed data buffer */
char zread_buff[RD_BUFF_SIZE];
};
static void get_cvspass(char *, const char *);
static void send_string(CvsServerCtx *, const char *, ...);
static int read_response(CvsServerCtx *, const char *);
static void ctx_to_fp(CvsServerCtx * ctx, FILE * fp);
static int read_line(CvsServerCtx * ctx, char * p);
static CvsServerCtx * open_ctx_pserver(CvsServerCtx *, const char *);
static CvsServerCtx * open_ctx_forked(CvsServerCtx *, const char *);
CvsServerCtx * open_cvs_server(char * p_root, int compress)
{
CvsServerCtx * ctx = (CvsServerCtx*)malloc(sizeof(*ctx));
char root[PATH_MAX];
char * p = root, *tok;
if (!ctx)
return NULL;
ctx->head = ctx->tail = ctx->read_buff;
ctx->read_fd = ctx->write_fd = -1;
ctx->compressed = 0;
ctx->is_pserver = 0;
if (compress)
{
memset(&ctx->zout, 0, sizeof(z_stream));
memset(&ctx->zin, 0, sizeof(z_stream));
/*
* to 'prime' the reads, make it look like there was output
* room available (i.e. we have processed all pending compressed
* data
*/
ctx->zin.avail_out = 1;
if (deflateInit(&ctx->zout, compress) != Z_OK)
{
free(ctx);
return NULL;
}
if (inflateInit(&ctx->zin) != Z_OK)
{
deflateEnd(&ctx->zout);
free(ctx);
return NULL;
}
}
strcpy(root, p_root);
tok = strsep(&p, ":");
/* if root string looks like :pserver:... then the first token will be empty */
if (strlen(tok) == 0)
{
char * method = strsep(&p, ":");
if (strcmp(method, "pserver") == 0)
{
ctx = open_ctx_pserver(ctx, p);
}
else if (strstr("local:ext:fork:server", method))
{
/* handle all of these via fork, even local */
ctx = open_ctx_forked(ctx, p);
}
else
{
debug(DEBUG_APPERROR, "cvs_direct: unsupported cvs access method: %s", method);
free(ctx);
ctx = NULL;
}
}
else
{
ctx = open_ctx_forked(ctx, p_root);
}
if (ctx)
{
char buff[BUFSIZ];
send_string(ctx, "Root %s\n", ctx->root);
/* this is taken from 1.11.1p1 trace - but with Mbinary removed. we can't handle it (yet!) */
send_string(ctx, "Valid-responses ok error Valid-requests Checked-in New-entry Checksum Copy-file Updated Created Update-existing Merged Patched Rcs-diff Mode Mod-time Removed Remove-entry Set-static-directory Clear-static-directory Set-sticky Clear-sticky Template Set-checkin-prog Set-update-prog Notified Module-expansion Wrapper-rcsOption M E F\n", ctx->root);
send_string(ctx, "valid-requests\n");
/* check for the commands we will issue */
read_line(ctx, buff);
if (strncmp(buff, "Valid-requests", 14) != 0)
{
debug(DEBUG_APPERROR, "cvs_direct: bad response to valid-requests command");
close_cvs_server(ctx);
return NULL;
}
if (!strstr(buff, " version") ||
!strstr(buff, " rlog") ||
!strstr(buff, " rdiff") ||
!strstr(buff, " diff") ||
!strstr(buff, " co"))
{
debug(DEBUG_APPERROR, "cvs_direct: cvs server too old for cvs_direct");
close_cvs_server(ctx);
return NULL;
}
read_line(ctx, buff);
if (strcmp(buff, "ok") != 0)
{
debug(DEBUG_APPERROR, "cvs_direct: bad ok trailer to valid-requests command");
close_cvs_server(ctx);
return NULL;
}
/* this is myterious but 'mandatory' */
send_string(ctx, "UseUnchanged\n");
if (compress)
{
send_string(ctx, "Gzip-stream %d\n", compress);
ctx->compressed = 1;
}
debug(DEBUG_APPMSG1, "cvs_direct initialized to CVSROOT %s", ctx->root);
}
return ctx;
}
static CvsServerCtx * open_ctx_pserver(CvsServerCtx * ctx, const char * p_root)
{
char root[PATH_MAX];
char full_root[PATH_MAX];
char * p = root, *tok, *tok2;
char user[BUFSIZ];
char server[BUFSIZ];
char pass[BUFSIZ];
char port[8];
strcpy(root, p_root);
tok = strsep(&p, ":");
if (strlen(tok) == 0 || !p)
{
debug(DEBUG_APPERROR, "parse error on third token");
goto out_free_err;
}
tok2 = strsep(&tok, "@");
if (!strlen(tok2) || (!tok || !strlen(tok)))
{
debug(DEBUG_APPERROR, "parse error on user@server in pserver");
goto out_free_err;
}
strcpy(user, tok2);
strcpy(server, tok);
if (*p != '/')
{
tok = strchr(p, '/');
if (!tok)
{
debug(DEBUG_APPERROR, "parse error: expecting / in root");
goto out_free_err;
}
memset(port, 0, sizeof(port));
memcpy(port, p, tok - p);
p = tok;
}
else
{
strcpy(port, "2401");
}
/* the line from .cvspass is fully qualified, so rebuild */
snprintf(full_root, PATH_MAX, ":pserver:%s@%s:%s%s", user, server, port, p);
get_cvspass(pass, full_root);
debug(DEBUG_TCP, "user:%s server:%s port:%s pass:%s full_root:%s", user, server, port, pass, full_root);
if ((ctx->read_fd = tcp_create_socket(REUSE_ADDR)) < 0)
goto out_free_err;
ctx->write_fd = dup(ctx->read_fd);
if (tcp_connect(ctx->read_fd, server, atoi(port)) < 0)
goto out_close_err;
send_string(ctx, "BEGIN AUTH REQUEST\n");
send_string(ctx, "%s\n", p);
send_string(ctx, "%s\n", user);
send_string(ctx, "%s\n", pass);
send_string(ctx, "END AUTH REQUEST\n");
if (!read_response(ctx, "I LOVE YOU"))
goto out_close_err;
strcpy(ctx->root, p);
ctx->is_pserver = 1;
return ctx;
out_close_err:
close(ctx->read_fd);
out_free_err:
free(ctx);
return NULL;
}
static CvsServerCtx * open_ctx_forked(CvsServerCtx * ctx, const char * p_root)
{
char root[PATH_MAX];
char * p = root, *tok, *tok2, *rep;
char execcmd[PATH_MAX];
int to_cvs[2];
int from_cvs[2];
pid_t pid;
const char * cvs_server = getenv("CVS_SERVER");
if (!cvs_server)
cvs_server = "cvs";
strcpy(root, p_root);
/* if there's a ':', it's remote */
tok = strsep(&p, ":");
if (p)
{
const char * cvs_rsh = getenv("CVS_RSH");
if (!cvs_rsh)
cvs_rsh = "rsh";
tok2 = strsep(&tok, "@");
if (tok)
snprintf(execcmd, PATH_MAX, "%s -l %s %s %s server", cvs_rsh, tok2, tok, cvs_server);
else
snprintf(execcmd, PATH_MAX, "%s %s %s server", cvs_rsh, tok2, cvs_server);
rep = p;
}
else
{
snprintf(execcmd, PATH_MAX, "%s server", cvs_server);
rep = tok;
}
if (pipe(to_cvs) < 0)
{
debug(DEBUG_SYSERROR, "cvs_direct: failed to create pipe to_cvs");
goto out_free_err;
}
if (pipe(from_cvs) < 0)
{
debug(DEBUG_SYSERROR, "cvs_direct: failed to create pipe from_cvs");
goto out_close_err;
}
debug(DEBUG_TCP, "forked cmdline: %s", execcmd);
if ((pid = fork()) < 0)
{
debug(DEBUG_SYSERROR, "cvs_direct: can't fork");
goto out_close2_err;
}
else if (pid == 0) /* child */
{
char * argp[4];
argp[0] = "sh";
argp[1] = "-c";
argp[2] = execcmd;
argp[3] = NULL;
close(to_cvs[1]);
close(from_cvs[0]);
close(0);
dup(to_cvs[0]);
close(1);
dup(from_cvs[1]);
execv("/bin/sh",argp);
debug(DEBUG_APPERROR, "cvs_direct: fatal: shouldn't be reached");
exit(1);
}
close(to_cvs[0]);
close(from_cvs[1]);
ctx->read_fd = from_cvs[0];
ctx->write_fd = to_cvs[1];
strcpy(ctx->root, rep);
return ctx;
out_close2_err:
close(from_cvs[0]);
close(from_cvs[1]);
out_close_err:
close(to_cvs[0]);
close(to_cvs[1]);
out_free_err:
free(ctx);
return NULL;
}
void close_cvs_server(CvsServerCtx * ctx)
{
/* FIXME: some sort of flushing should be done for non-compressed case */
if (ctx->compressed)
{
int ret, len;
char buff[BUFSIZ];
/*
* there shouldn't be anything left, but we do want
* to send an 'end of stream' marker, (if such a thing
* actually exists..)
*/
do
{
ctx->zout.next_out = buff;
ctx->zout.avail_out = BUFSIZ;
ret = deflate(&ctx->zout, Z_FINISH);
if ((ret == Z_OK || ret == Z_STREAM_END) && ctx->zout.avail_out != BUFSIZ)
{
len = BUFSIZ - ctx->zout.avail_out;
if (writen(ctx->write_fd, buff, len) != len)
debug(DEBUG_APPERROR, "cvs_direct: zout: error writing final state");
//hexdump(buff, len, "cvs_direct: zout: sending unsent data");
}
} while (ret == Z_OK);
if ((ret = deflateEnd(&ctx->zout)) != Z_OK)
debug(DEBUG_APPERROR, "cvs_direct: zout: deflateEnd error: %s: %s",
(ret == Z_STREAM_ERROR) ? "Z_STREAM_ERROR":"Z_DATA_ERROR", ctx->zout.msg);
}
/* we're done writing now */
debug(DEBUG_TCP, "cvs_direct: closing cvs server write connection %d", ctx->write_fd);
close(ctx->write_fd);
/*
* if this is pserver, then read_fd is a bi-directional socket.
* we want to shutdown the write side, just to make sure the
* server get's eof
*/
if (ctx->is_pserver)
{
debug(DEBUG_TCP, "cvs_direct: shutdown on read socket");
if (shutdown(ctx->read_fd, SHUT_WR) < 0)
debug(DEBUG_SYSERROR, "cvs_direct: error with shutdown on pserver socket");
}
if (ctx->compressed)
{
int ret = Z_OK, len, eof = 0;
char buff[BUFSIZ];
/* read to the 'eof'/'eos' marker. there are two states we
* track, looking for Z_STREAM_END (application level EOS)
* and EOF on socket. Both should happen at the same time,
* but we need to do the read first, the first time through
* the loop, but we want to do one read after getting Z_STREAM_END
* too. so this loop has really ugly exit conditions.
*/
for(;;)
{
/*
* if there's nothing in the avail_in, and we
* inflated everything last pass (avail_out != 0)
* then slurp some more from the descriptor,
* if we get EOF, exit the loop
*/
if (ctx->zin.avail_in == 0 && ctx->zin.avail_out != 0)
{
debug(DEBUG_TCP, "cvs_direct: doing final slurp");
len = read(ctx->read_fd, ctx->zread_buff, RD_BUFF_SIZE);
debug(DEBUG_TCP, "cvs_direct: did final slurp: %d", len);
if (len <= 0)
{
eof = 1;
break;
}
/* put the data into the inflate input stream */
ctx->zin.next_in = ctx->zread_buff;
ctx->zin.avail_in = len;
}
/*
* if the last time through we got Z_STREAM_END, and we
* get back here, it means we should've gotten EOF but
* didn't
*/
if (ret == Z_STREAM_END)
break;
ctx->zin.next_out = buff;
ctx->zin.avail_out = BUFSIZ;
ret = inflate(&ctx->zin, Z_SYNC_FLUSH);
len = BUFSIZ - ctx->zin.avail_out;
if (ret == Z_BUF_ERROR)
debug(DEBUG_APPERROR, "Z_BUF_ERROR");
if (ret == Z_OK && len == 0)
debug(DEBUG_TCP, "cvs_direct: no data out of inflate");
if (ret == Z_STREAM_END)
debug(DEBUG_TCP, "cvs_direct: got Z_STREAM_END");
if ((ret == Z_OK || ret == Z_STREAM_END) && len > 0)
hexdump(buff, BUFSIZ - ctx->zin.avail_out, "cvs_direct: zin: unread data at close");
}
if (ret != Z_STREAM_END)
debug(DEBUG_APPERROR, "cvs_direct: zin: Z_STREAM_END not encountered (premature EOF?)");
if (eof == 0)
debug(DEBUG_APPERROR, "cvs_direct: zin: EOF not encountered (premature Z_STREAM_END?)");
if ((ret = inflateEnd(&ctx->zin)) != Z_OK)
debug(DEBUG_APPERROR, "cvs_direct: zin: inflateEnd error: %s: %s",
(ret == Z_STREAM_ERROR) ? "Z_STREAM_ERROR":"Z_DATA_ERROR", ctx->zin.msg ? ctx->zin.msg : "");
}
debug(DEBUG_TCP, "cvs_direct: closing cvs server read connection %d", ctx->read_fd);
close(ctx->read_fd);
free(ctx);
}
static void get_cvspass(char * pass, const char * root)
{
char cvspass[PATH_MAX];
const char * home;
FILE * fp;
pass[0] = 0;
if (!(home = getenv("HOME")))
{
debug(DEBUG_APPERROR, "HOME environment variable not set");
exit(1);
}
if (snprintf(cvspass, PATH_MAX, "%s/.cvspass", home) >= PATH_MAX)
{
debug(DEBUG_APPERROR, "prefix buffer overflow");
exit(1);
}
if ((fp = fopen(cvspass, "r")))
{
char buff[BUFSIZ];
int len = strlen(root);
while (fgets(buff, BUFSIZ, fp))
{
/* FIXME: what does /1 mean? */
if (strncmp(buff, "/1 ", 3) != 0)
continue;
if (strncmp(buff + 3, root, len) == 0)
{
strcpy(pass, buff + 3 + len + 1);
chop(pass);
break;
}
}
fclose(fp);
}
if (!pass[0])
pass[0] = 'A';
}
static void send_string(CvsServerCtx * ctx, const char * str, ...)
{
int len;
char buff[BUFSIZ];
va_list ap;
va_start(ap, str);
len = vsnprintf(buff, BUFSIZ, str, ap);
if (len >= BUFSIZ)
{
debug(DEBUG_APPERROR, "cvs_direct: command send string overflow");
exit(1);
}
if (ctx->compressed)
{
char zbuff[BUFSIZ];
if (ctx->zout.avail_in != 0)
{
debug(DEBUG_APPERROR, "cvs_direct: zout: last output command not flushed");
exit(1);
}
ctx->zout.next_in = buff;
ctx->zout.avail_in = len;
ctx->zout.avail_out = 0;
while (ctx->zout.avail_in > 0 || ctx->zout.avail_out == 0)
{
int ret;
ctx->zout.next_out = zbuff;
ctx->zout.avail_out = BUFSIZ;
/* FIXME: for the arguments before a command, flushing is counterproductive */
ret = deflate(&ctx->zout, Z_SYNC_FLUSH);
if (ret == Z_OK)
{
len = BUFSIZ - ctx->zout.avail_out;
if (writen(ctx->write_fd, zbuff, len) != len)
{
debug(DEBUG_SYSERROR, "cvs_direct: zout: can't write");
exit(1);
}
}
else
{
debug(DEBUG_APPERROR, "cvs_direct: zout: error %d %s", ret, ctx->zout.msg);
}
}
}
else
{
if (writen(ctx->write_fd, buff, len) != len)
{
debug(DEBUG_SYSERROR, "cvs_direct: can't send command");
exit(1);
}
}
debug(DEBUG_TCP, "string: '%s' sent", buff);
}
static int refill_buffer(CvsServerCtx * ctx)
{
int len;
if (ctx->head != ctx->tail)
{
debug(DEBUG_APPERROR, "cvs_direct: refill_buffer called on non-empty buffer");
exit(1);
}
ctx->head = ctx->read_buff;
len = RD_BUFF_SIZE;
if (ctx->compressed)
{
int zlen, ret;
/* if there was leftover buffer room, it's time to slurp more data */
do
{
if (ctx->zin.avail_out > 0)
{
if (ctx->zin.avail_in != 0)
{
debug(DEBUG_APPERROR, "cvs_direct: zin: expect 0 avail_in");
exit(1);
}
zlen = read(ctx->read_fd, ctx->zread_buff, RD_BUFF_SIZE);
ctx->zin.next_in = ctx->zread_buff;
ctx->zin.avail_in = zlen;
}
ctx->zin.next_out = ctx->head;
ctx->zin.avail_out = len;
/* FIXME: we don't always need Z_SYNC_FLUSH, do we? */
ret = inflate(&ctx->zin, Z_SYNC_FLUSH);
}
while (ctx->zin.avail_out == len);
if (ret == Z_OK)
{
ctx->tail = ctx->head + (len - ctx->zin.avail_out);
}
else
{
debug(DEBUG_APPERROR, "cvs_direct: zin: error %d %s", ret, ctx->zin.msg);
exit(1);
}
}
else
{
len = read(ctx->read_fd, ctx->head, len);
ctx->tail = (len <= 0) ? ctx->head : ctx->head + len;
}
return len;
}
static int read_line(CvsServerCtx * ctx, char * p)
{
int len = 0;
while (1)
{
if (ctx->head == ctx->tail)
if (refill_buffer(ctx) <= 0)
return -1;
*p = *ctx->head++;
if (*p == '\n')
{
*p = 0;
break;
}
p++;
len++;
}
return len;
}
static int read_response(CvsServerCtx * ctx, const char * str)
{
/* FIXME: more than 1 char at a time */
char resp[BUFSIZ];
if (read_line(ctx, resp) < 0)
return 0;
debug(DEBUG_TCP, "response '%s' read", resp);
return (strcmp(resp, str) == 0);
}
static void ctx_to_fp(CvsServerCtx * ctx, FILE * fp)
{
char line[BUFSIZ];
while (1)
{
read_line(ctx, line);
debug(DEBUG_TCP, "ctx_to_fp: %s", line);
if (memcmp(line, "M ", 2) == 0)
{
if (fp)
fprintf(fp, "%s\n", line + 2);
}
else if (memcmp(line, "E ", 2) == 0)
{
debug(DEBUG_APPMSG1, "%s", line + 2);
}
else if (strncmp(line, "ok", 2) == 0 || strncmp(line, "error", 5) == 0)
{
break;
}
}
if (fp)
fflush(fp);
}
void cvs_rdiff(CvsServerCtx * ctx,
const char * rep, const char * file,
const char * rev1, const char * rev2)
{
/* NOTE: opts are ignored for rdiff, '-u' is always used */
send_string(ctx, "Argument -u\n");
send_string(ctx, "Argument -r\n");
send_string(ctx, "Argument %s\n", rev1);
send_string(ctx, "Argument -r\n");
send_string(ctx, "Argument %s\n", rev2);
send_string(ctx, "Argument %s%s\n", rep, file);
send_string(ctx, "rdiff\n");
ctx_to_fp(ctx, stdout);
}
void cvs_rupdate(CvsServerCtx * ctx, const char * rep, const char * file, const char * rev, int create, const char * opts)
{
FILE * fp;
char cmdbuff[BUFSIZ];
snprintf(cmdbuff, BUFSIZ, "diff %s %s /dev/null %s | sed -e '%s s|^\\([+-][+-][+-]\\) -|\\1 %s/%s|g'",
opts, create?"":"-", create?"-":"", create?"2":"1", rep, file);
debug(DEBUG_TCP, "cmdbuff: %s", cmdbuff);
if (!(fp = popen(cmdbuff, "w")))
{
debug(DEBUG_APPERROR, "cvs_direct: popen for diff failed: %s", cmdbuff);
exit(1);
}
send_string(ctx, "Argument -p\n");
send_string(ctx, "Argument -r\n");
send_string(ctx, "Argument %s\n", rev);
send_string(ctx, "Argument %s/%s\n", rep, file);
send_string(ctx, "co\n");
ctx_to_fp(ctx, fp);
pclose(fp);
}
static int parse_patch_arg(char * arg, char ** str)
{
char *tok, *tok2 = "";
tok = strsep(str, " ");
if (!tok)
return 0;
if (!*tok == '-')
{
debug(DEBUG_APPERROR, "diff_opts parse error: no '-' starting argument: %s", *str);
return 0;
}
/* if it's not 'long format' argument, we can process it efficiently */
if (tok[1] == '-')
{
debug(DEBUG_APPERROR, "diff_opts parse_error: long format args not supported");
return 0;
}
/* see if command wants two args and they're separated by ' ' */
if (tok[2] == 0 && strchr("BdDFgiorVxYz", tok[1]))
{
tok2 = strsep(str, " ");
if (!tok2)
{
debug(DEBUG_APPERROR, "diff_opts parse_error: argument %s requires two arguments", tok);
return 0;
}
}
snprintf(arg, 32, "%s%s", tok, tok2);
return 1;
}
void cvs_diff(CvsServerCtx * ctx,
const char * rep, const char * file,
const char * rev1, const char * rev2, const char * opts)
{
char argstr[BUFSIZ], *p = argstr;
char arg[32];
char file_buff[PATH_MAX], *basename;
strzncpy(argstr, opts, BUFSIZ);
while (parse_patch_arg(arg, &p))
send_string(ctx, "Argument %s\n", arg);
send_string(ctx, "Argument -r\n");
send_string(ctx, "Argument %s\n", rev1);
send_string(ctx, "Argument -r\n");
send_string(ctx, "Argument %s\n", rev2);
/*
* we need to separate the 'basename' of file in order to
* generate the Directory directive(s)
*/
strzncpy(file_buff, file, PATH_MAX);
if ((basename = strrchr(file_buff, '/')))
{
*basename = 0;
send_string(ctx, "Directory %s/%s\n", rep, file_buff);
send_string(ctx, "%s/%s/%s\n", ctx->root, rep, file_buff);
}
else
{
send_string(ctx, "Directory %s\n", rep, file_buff);
send_string(ctx, "%s/%s\n", ctx->root, rep);
}
send_string(ctx, "Directory .\n");
send_string(ctx, "%s\n", ctx->root);
send_string(ctx, "Argument %s/%s\n", rep, file);
send_string(ctx, "diff\n");
ctx_to_fp(ctx, stdout);
}
/*
* FIXME: the design of this sucks. It was originally designed to fork a subprocess
* which read the cvs response and send it back through a pipe the main process,
* which fdopen(3)ed the other end, and juts used regular fgets. This however
* didn't work because the reads of compressed data in the child process altered
* the compression state, and there was no way to resynchronize that state with
* the parent process. We could use threads...
*/
FILE * cvs_rlog_open(CvsServerCtx * ctx, const char * rep, const char * date_str)
{
/* note: use of the date_str is handled in a non-standard, cvsps specific way */
if (date_str && date_str[0])
{
send_string(ctx, "Argument -d\n", rep);
send_string(ctx, "Argument %s<1 Jan 2038 05:00:00 -0000\n", date_str);
send_string(ctx, "Argument -d\n", rep);
send_string(ctx, "Argument %s\n", date_str);
}
send_string(ctx, "Argument %s\n", rep);
send_string(ctx, "rlog\n");
/*
* FIXME: is it possible to create a 'fake' FILE * whose 'refill'
* function is below?
*/
return (FILE*)ctx;
}
char * cvs_rlog_fgets(char * buff, int buflen, CvsServerCtx * ctx)
{
char lbuff[BUFSIZ];
int len;
len = read_line(ctx, lbuff);
debug(DEBUG_TCP, "cvs_direct: rlog: read %s", lbuff);
if (memcmp(lbuff, "M ", 2) == 0)
{
memcpy(buff, lbuff + 2, len - 2);
buff[len - 2 ] = '\n';
buff[len - 1 ] = 0;
}
else if (memcmp(lbuff, "E ", 2) == 0)
{
debug(DEBUG_APPMSG1, "%s", lbuff + 2);
}
else if (strcmp(lbuff, "ok") == 0 ||strcmp(lbuff, "error") == 0)
{
debug(DEBUG_TCP, "cvs_direct: rlog: got command completion");
return NULL;
}
return buff;
}
void cvs_rlog_close(CvsServerCtx * ctx)
{
}
void cvs_version(CvsServerCtx * ctx, char * client_version, char * server_version)
{
char lbuff[BUFSIZ];
strcpy(client_version, "Client: Concurrent Versions System (CVS) 99.99.99 (client/server) cvs-direct");
send_string(ctx, "version\n");
read_line(ctx, lbuff);
if (memcmp(lbuff, "M ", 2) == 0)
sprintf(server_version, "Server: %s", lbuff + 2);
else
debug(DEBUG_APPERROR, "cvs_direct: didn't read version: %s", lbuff);
read_line(ctx, lbuff);
if (strcmp(lbuff, "ok") != 0)
debug(DEBUG_APPERROR, "cvs_direct: protocol error reading version");
debug(DEBUG_TCP, "cvs_direct: client version %s", client_version);
debug(DEBUG_TCP, "cvs_direct: server version %s", server_version);
}
syntax highlighted by Code2HTML, v. 0.9.1