/*
 * 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);
}
