/* -*-Mode: C++;-*-
 * PRCS - The Project Revision Control System
 * Copyright (C) 1997  Josh MacDonald
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: vc.cc 1.13.1.1.1.11.1.12.1.11.1.6.1.20 Wed, 06 Feb 2002 20:57:16 -0800 jmacd $
 */

extern "C" {
#include <signal.h>
}

#include "prcs.h"
#include "hash.h"
#include "vc.h"
#include "syscmd.h"
#include "system.h"
#include "misc.h"
#include "memseg.h"

EXTERN char* rcs_text;       /* declared in vc.l */
EXTERN int VC_get_token(FILE* is);
EXTERN int VC_init_seg_get_c(char* seg, size_t len);

static bool VC_expect_token(FILE* stream, int type, Dstring* result);
static PrVoidError fix_version_data(const ProjectVersionDataPtrArray *pvda,
				    const DstringPtrArray            *old_fmt_parents);

extern "C" void vc_lex_fatal_error(const char* msg)
{
    prcserror << msg;

    if (errno != 0)
	prcserror << perror;
    else
	prcserror << dotendl;

    kill (getpid(), SIGINT);
}

void VC_init_seg_get(MemorySegment* seg)
{
    seg->append_segment("\0\0", 2);

    VC_init_seg_get_c((char*)seg->segment(), seg->length());
}

/*
 * VC_checkin --
 *
 *     Buffers checkins of working files into RCS versionfiles.  It
 *     first calls upon RCS to lock the parent version.  Currently,
 *     one fork to RCS is required for each file.  This could possibly
 *     be replaced in the future by either some analysis of the time
 *     stamp on the file (and assume nothing has been checked in since
 *     the last was checked in) or by parsing the RCS file according
 *     to rcsfile(5).  This is the simplest and most transparent way
 *     of doing this, however, it only relies on RCS's external
 *     interface.
 *
 *     when VC_checkin is called with all arguments except the log
 *     NULL, the actual checkin takes place.  Also, if the argument
 *     size approaches sysconf(_SC_ARG_MAX), the buffer will be
 *     flushed.  The checkin unlocks the RCS file.
 * Results:
 *     true if everything is going okay.  false if an error has
 *     occured.  the new RCS version assigned to newfile is returned
 *     in the Dstring *parentversion.  If parent version was empty,
 *     then checkin will fill RCS version 1.1.
 * Side Effects:
 *     if sucessful, the RCS files will be modified.
 * Parameters:
 *     newfile -- path of the working file.
 *     parentversion -- return buffer for new version number
 *     versionfile -- path of the RCS version file.
 *     log -- a log message
 */
typedef void (* CheckinHook)(void* data, const char* new_version);

struct CheckinData {
    CheckinData(const char* file0,
		const char* ver0,
		const char* log0,
		void* data0,
		CheckinHook notify_func0)
	:file(file0), ver(ver0), log(log0), data(data0),
	 notify_func(notify_func0) { }
    const char* file;
    const char* ver;
    const char* log;
    void* data;
    CheckinHook notify_func;
};

struct CheckinData2 {
    CheckinData2 (VoidPtrList* l0, CheckinHook notify_func0, int n0)
	:l(l0), notify_func(notify_func0), n(n0) { }
    VoidPtrList *l;
    CheckinHook notify_func;
    int n;
};

PrVoidError VC_checkin_stage2(MemorySegment*, MemorySegment*, int exit_val, void* vdata);
PrVoidError VC_checkin_stage3(MemorySegment*, MemorySegment*, int exit_val, void* vdata);

PrVoidError VC_checkin(const char* newfile,
		       const char* parentversion,
		       const char* versionfile,
		       const char* log,
		       void* data,
		       CheckinHook notify_func)
{
    CheckinData* c_data = new CheckinData(newfile, versionfile,
					   log, data, notify_func);

    if( newfile != NULL) {
	Dstring ver;
	ArgList *argl;

	Return_if_fail(argl << rcs_command.new_arg_list());

	/* if the file being checked in has a parent version, then
	 * instruct RCS to pick a good new version number by locking the
	 * parent version
	 */
        if (parentversion != NULL && parentversion[0]) {

	    ver.append("-l");
	    ver.append(parentversion);

	    argl->append("-q");
	    argl->append("-T"); /* preserve time stamp */
	    argl->append("-u");
	    argl->append("-M");
	    argl->append(ver.cast());
	    argl->append(versionfile);

	} else if (parentversion != NULL && !parentversion[0]) {
	    /*
	     * if the version is empty, assume the file will be the *
	     * initial version.  We still must try to lock the version
	     * in the case the file is being checked in by a different
	     * owner and no locks are set (eg. the project file, which
	     * doesn't track RCS versions).   To prove this:
	     * user1% rcs -i -U foo,v
	     * # creates initial version without strict locking
	     * user2% co foo
	     * user2% chmod +w foo
	     * # essentially what PRCS does, since it checks out on stdout
	     * # modify foo
	     * user2% ci foo
	     * # no locks set by user2, fail
	     * user1% ci foo
	     * # since user1 owns it, it succeeds, I guess.
	     *
	     * The point is, you won't see why this is neccesary until
	     * you try using two users.
	     */

	    argl->append("-q");
	    argl->append("-T"); /* preserve time stamp */
	    argl->append("-u");
	    argl->append("-M");
	    argl->append("-l");
	    argl->append(versionfile);
	}

	Return_if_fail(rcs_command.open_delayed (VC_checkin_stage2, c_data, false, false));
    } else {
        Return_if_fail(DelayedJob::delay_wait(true));

	DEBUG ("starting ci processes");

	Return_if_fail(VC_checkin_stage2(NULL, NULL, 0, c_data));

	Return_if_fail(DelayedJob::delay_wait(true));
    }

    return NoError;
}

PrVoidError VC_checkin_stage2(MemorySegment*, MemorySegment*, int exit_val, void* v_data)
{
    static VoidPtrList *accumulate = NULL;
    static ArgList     *args = NULL;

    CheckinData *c_data = (CheckinData*) v_data;

    const char* newfile = c_data->file;
    const char* log = c_data->log;
    const char* versionfile = c_data->ver;
    void* data = c_data->data;
    CheckinHook func = c_data->notify_func;

    delete c_data;

    if (exit_val != 0)
      pthrow prcserror << "RCS failed on file " << versionfile << dotendl;;

    if (!args && !accumulate && !newfile)
	return NoError;

    if (!args)
	args = new ArgList;

    if (newfile == NULL) {
	ArgList* ci_args;

	int file_count     = args->length() / 2;
	int ratio          = file_count / option_jobs;
	int files_per_proc = 4 > ratio ? 4 : ratio;
	int max_arg_size   = sysconf(_SC_ARG_MAX) - (4 * MAXPATHLEN);

#ifdef __BEOS__
        /* ci sometimes freezes when given more than one file,
         * and is remarkably slow when given more then 32 files in
         * any case. *sigh*
         */
        files_per_proc = 1;
#endif

	DEBUG ("using " << files_per_proc << " files per child");

	VoidPtrList* acc_end;

	accumulate = accumulate->nreverse();

        Dstring logmes;
	Dstring date;

	date.append("-d");
	date.append(get_utc_time());

	if (log) {
	    logmes.append("-m");
	    logmes.append(log);
	}

	for (int j = 0; j < file_count;) {

	    int arg_list_size = 0;
	    int this_round = 0;

	    acc_end = accumulate;

	    Return_if_fail(ci_args << ci_command.new_arg_list());

	    if (log)
		ci_args->append(logmes.cast());
	    ci_args->append(date.cast());

	    ci_args->append("-T");
	    ci_args->append("-f");

	    /*if (keep)
		ci_args->append("-u");*/

	    for (; j < file_count &&
		     arg_list_size < max_arg_size &&
		     this_round < files_per_proc; j += 1, this_round += 1) {
		ci_args->append(args->index(2*j));
		ci_args->append(args->index(2*j+1));

		arg_list_size += 2;
		arg_list_size += strlen(args->index(2*j));
		arg_list_size += strlen(args->index(2*j+1));

		acc_end = acc_end->tail();
	    }

	    DEBUG("ci with " << this_round << " jobs");

	    Return_if_fail(ci_command.open_delayed(VC_checkin_stage3,
						   new CheckinData2(accumulate, func, this_round),
						   false, true));

	    accumulate = acc_end;
	}

	delete args;

	args = new ArgList;

	accumulate = NULL;
    } else {
        accumulate = new VoidPtrList(data, accumulate);

        args->append(newfile);
        args->append(versionfile);
    }

    return NoError;
}

PrVoidError VC_checkin_stage3(MemorySegment*, MemorySegment* err_seg, int exit_val, void* v_data)
{
    CheckinData2* data = (CheckinData2*) v_data;

    int n            = data->n;
    CheckinHook func = data->notify_func;
    VoidPtrList *l   = data->l;
    VoidPtrList *t;

    delete data;

    if (exit_val != 0) {
	cerr.write(err_seg->segment(), err_seg->length());
	pthrow prcserror << "RCS ci failed on batch command" << dotendl;
    }

    VC_init_seg_get(err_seg);

    for (; n > 0; n -= 1) {

	int vclex = VC_get_token(NULL);

	if(vclex != NewRevision &&
	   vclex != InitialRevision)
	    pthrow prcserror << "Error scanning RCS ci output" << dotendl;

	if (l->head() != NULL)
	    (* func) (l->head(), rcs_text);

	t = l;

	l = l->tail();

	delete t;
    }

    return NoError;
}

/*
 * VC_register --
 *
 *     Used to create an initial RCS versionfile with no versions.  This
 *     avoids a condition in VC_checkin where RCS would be confused by
 *     a missing file.
 * Results:
 *     If sucessful, file has been registered.  Files are not actually
 *     created via a fork/exec to rcs until VC_register is called with
 *     NULL as its filename.  It will not register the same file twice
 *     even if the buffered files have not been created.  That is, two
 *     sucessive calls to VC_register with the same name will fail until
 *     VC_register(NULL) has been called.  After that, it will succeed
 *     again.  It is someone elses duty to check that the file doesn't
 *     already exist.
 *
 *     Should the buffer size approach sysconf(_SC_ARG_MAX), it will
 *     automatically flush the buffer.
 * Side Effects:
 *     when VC_register(NULL) gets called, files will be created.
 * Parameters:
 *     file names a file, or flushes the buffer if NULL.
 */
#define REGISTER_COUNT 64 /* I determined experimentally that this
			   * is reasonably optimal for both single
			   * and multi-process checkins. */
PrVoidError VC_register_callback (MemorySegment*,
				  MemorySegment*,
				  int exit_val,
				  void* data)
{
    bool *n_success = (bool*) data;

    if (exit_val != 0)
	*n_success = true;

    return NoError;
}

PrVoidError VC_register(const char* file)
{
    static ArgList *accumulate = NULL;
    static bool n_success;

    if ((accumulate == NULL || accumulate->length() == 0) && file == NULL)
        return NoError;

    if (accumulate == NULL)
        accumulate = new ArgList;

    if (file == NULL || accumulate->length() == REGISTER_COUNT) {
	ArgList *argl;

        Return_if_fail(argl << rcs_command.new_arg_list());

        argl->append("-i");          /* RCS init */
        argl->append("-t/dev/null"); /* No initial description message. */
        argl->append("-U");          /* unset strict locking */
        argl->append("-q");          /* be quiet */

	for(int i = 0; i < accumulate->length(); i += 1)
	    argl->append(accumulate->index(i));

        Return_if_fail(rcs_command.open_delayed(VC_register_callback,
						&n_success,
						false,
						false));

	accumulate->truncate(0);
    }

    if(file != NULL) {
	accumulate->append(file);
    } else {
	Return_if_fail(DelayedJob::delay_wait(true));

	bool tmp = n_success;
	n_success = false;

	if (tmp)
	    pthrow prcserror << "RCS -i failed on batch init" << dotendl;
    }

    return NoError;
}

/*
 * VC_expect_token --
 *
 *     Used when a certain token type is expected from the token stream.
 * Results:
 *     Returns true when the expected token type is found, false otherwise.
 *     the external variable rcs_text will also contain the token text.
 * Side Effects:
 *     One token is read from the stream via a call to VC_get_token.
 * Parameters:
 *     stream is the argument passed to VC_get_token, possibly NULL to
 *     continue reading the last stream passed. type is one of the
 *     #defined types in include/vc.h.  result gets the value of the
 *     token text if succesful, if non-null.
 */
static bool VC_expect_token(FILE* stream, int type, Dstring* result)
{
    if(type != VC_get_token(stream))
        return false;
    else if(result)
        result->assign(rcs_text);
    return true;
}

/*
 * VC_checkout_stream --
 *
 *     Retrieves version from revisionfile, into a stream.
 * Results:
 *     returns a FILE* opened for reading or NULL upon failure.
 * Side Effects:
 *     a file descriptor is open.  VC_close_checkout_stream should
 *     be called.
 * Parameters:
 *     versionfile names the pathname of an RCS file.  version is
 *     retrieved.
 */
PrCFilePtrError VC_checkout_stream(const char* version,
				   const char* revisionfile)
{
    ArgList *argl;
    Dstring ver("-p");

    Return_if_fail(argl << co_command.new_arg_list());

    ver.append(version);

    argl->append("-ko");
    argl->append(ver);
    argl->append(revisionfile);

    If_fail(co_command.open(true, true))
        pthrow prcserror << "Failed checking out version " << version
			 << " from file " << squote(revisionfile) << dotendl;

    return co_command.standard_out();
}

/*
 * VC_close_checkout_stream --
 *
 *     cleans up after a VC_checkout_stream command and checks that RCS
 *     did not report any errors.
 * Results:
 *     true if everything went okay.
 * Side effects:
 *     FILE *f will be closed.
 * Parameters:
 *     RCS versionfile file and version.
 */
#ifdef NDEBUG
#define UNUSED(x)
#else
#define UNUSED(x) x
#endif
PrVoidError VC_close_checkout_stream(FILE* UNUSED(f), const char* version, const char* file)
{
    if(!VC_expect_token(co_command.standard_err(), RlogVersion, NULL) ||
       strcmp(rcs_text, version) != 0)
        pthrow prcserror << "Consistency check failed checking out version "
			 << version << " from file " << squote(file) << dotendl;

    ASSERT(f == co_command.standard_out(), "no way, man!");

    Return_if_fail_if_ne(co_command.close(), 0)
	pthrow prcserror << "co exited with non-zero status" << dotendl;

    return NoError;
}

/*
 * VC_checkout_file --
 *
 *     checks out a file and places it in newfile.
 * Results:
 *     true if everything went okay.
 * Side effects:
 *     None.
 * Parameters:
 *     RCS versionfile file and version.  File is placed in newfile.
 */
PrVoidError VC_checkout_file(const char* newfile,
			     const char* version,
			     const char* revisionfile)
{
    FILE *infile, *outfile;

    Return_if_fail(infile << VC_checkout_stream(version, revisionfile));

    If_fail(outfile << Err_fopen(newfile, "w"))
        pthrow prcserror << "Can't open file " << squote(newfile)
		         << " for writing" << perror;

    Return_if_fail(write_file(infile, outfile));
    Return_if_fail(VC_close_checkout_stream(infile, version, revisionfile));

    If_fail(Err_fclose(outfile))
	pthrow prcserror << "Write failed on file " << squote(newfile) << perror;

    return NoError;
}

/*
 * VC_get_project_version_data --
 *
 *     Retrieves all the log information for a PRCS project file. (which
 *     is used to map PRCS versions to RCS versions) from an RCS versionfile.
 * Results:
 *     If return is successful, return value is a heap allocated array of
 *     ProjectVersionData.
 * Side Effects:
 *     Memory is allocated for the return value.
 * Parameters:
 *     versionfile names the pathname of an RCS file.
 */
PrProjectVersionDataPtrArrayPtrError
VC_get_project_version_data(const char* versionfile)
{
    ArgList *argl;
    int rev_count, i;

    Return_if_fail(argl << rlog_command.new_arg_list());

    argl->append(versionfile);

    Return_if_fail(rlog_command.open(true, false));

    if(VC_get_token(rlog_command.standard_out()) == RlogTotalRevisions)
	rev_count = atoi(rcs_text);
    else
	pthrow prcserror << "Unrecognized output from RCS rlog command on RCS file "
			<< squote(versionfile) << dotendl;

    ProjectVersionDataPtrArray* pvda = new ProjectVersionDataPtrArray (rev_count);

    if(rev_count == 0)
	return pvda;

    /* This crap is here for backward compatiblity.  The new formats
     * require knowing the index of the parent, not the version name,
     * for simplicity, but the versions arrive out of order, so the
     * parent names or NULL are stored until all versions arrive. */
    DstringPtrArray* old_fmt_parents = new DstringPtrArray (rev_count);
    DstringPtrArray* old_fmt_descends = new DstringPtrArray (rev_count);

    pvda->expand (rev_count, (ProjectVersionData *) 0);
    old_fmt_parents->expand (rev_count, (Dstring *) 0);
    old_fmt_descends->expand (rev_count, (Dstring *) 0);

    if(VC_get_token(NULL) != RlogVersion)
	goto error;

    for(i = 0; i < rev_count; i += 1) {
	RcsToken vclex;
	Dstring *par_ver_maj, *desc_ver_maj;
	ProjectVersionData* pvd = new ProjectVersionData(rev_count - 1 - i);

	pvd->rcs_version(p_strdup(rcs_text));

	pvda->index(rev_count - 1 - i, pvd);

	while(true) {
	    vclex = (RcsToken)VC_get_token(NULL);
	    if(vclex == RlogVersion || vclex == NoTokenMatch)
		break;
	    switch(vclex) {
	    case RlogLines:
		/* Don't care for the PVD lines */
		break;
	    case RlogDate:
		pvd->date(timestr_to_time_t(rcs_text));
		break;
	    case RlogAuthor:
		pvd->author(p_strdup(rcs_text));
		break;
	    case PrcsMajorVersion:
		pvd->prcs_major(p_strdup(rcs_text));
		break;
	    case PrcsMinorVersion:
		pvd->prcs_minor(p_strdup(rcs_text));
		break;
	    case PrcsDescendsMajorVersion:
		desc_ver_maj = new Dstring (rcs_text);

		vclex = (RcsToken)VC_get_token(NULL);

		if (vclex != PrcsDescendsMinorVersion)
		    goto error;

		if (strcmp (rcs_text, "0") == 0) {
		    /* Its a empty parent version, no parents. */
		    delete desc_ver_maj;
		    break;
		}

		desc_ver_maj->append ('.');
		desc_ver_maj->append (rcs_text);

		old_fmt_descends->index(rev_count - 1 - i, desc_ver_maj);

		break;
	    case PrcsParentMajorVersion:
		par_ver_maj = new Dstring (rcs_text);

		vclex = (RcsToken)VC_get_token(NULL);

		if (vclex != PrcsParentMinorVersion)
		    goto error;

		if (strcmp (rcs_text, "0") == 0) {
		    /* Its a empty parent version, no parents. */
		    delete par_ver_maj;
		    break;
		}

		par_ver_maj->append ('.');
		par_ver_maj->append (rcs_text);

		old_fmt_parents->index(rev_count - 1 - i, par_ver_maj);

		break;
	    case PrcsVersionDeleted:
		pvd->deleted(true);
		break;
	    case PrcsParents:

		if (rcs_text[0]) {
		    const char* pis = strtok (rcs_text, ":");
		    int pi = atoi (pis);

		    if (!pis[0] || pi < 0 || pi >= (rev_count - 1 - i))
			pthrow prcserror << "Invalid parent version index " << squote (rcs_text)
					<< " in " << squote (versionfile)
					<< " RCS version 1." << (rev_count - i) << dotendl;

		    pvd->new_parent(pi);

		    while ((pis = strtok (NULL, ":")) != NULL) {
			pi = atoi (pis);

			if (!pis[0] || pi < 0 || pi >= (rev_count - 1 - i))
			    pthrow prcserror << "Invalid parent version index " << squote (rcs_text)
					    << " in " << squote (versionfile)
					    << " RCS version 1." << (rev_count - i) << dotendl;

			pvd->new_parent(pi);
		    }
		}

		break;

	    case NoTokenMatch:
	    case RlogTotalRevisions:
	    case RlogVersion:
	    case PrevRevision:
	    case NewRevision:
	    case InitialRevision:
	    case PrcsParentMinorVersion:
	    case PrcsDescendsMinorVersion:
	    case RcsAbort:
		goto error;
	    }
	}

	if (!pvd->OK())
	    pthrow prcserror << "Incomplete version log information in RCS file "
			    << squote(versionfile)
			    << " version 1." << (rev_count - i) << dotendl;
    }

    Return_if_fail_if_ne(rlog_command.close(), 0)
	pthrow prcserror << "RCS rlog command returned non-zero status on RCS file "
			<< squote(versionfile) << dotendl;

    Return_if_fail(fix_version_data(pvda, old_fmt_parents));
    Return_if_fail(fix_version_data(pvda, old_fmt_descends));

    foreach (ds_ptr, old_fmt_parents, DstringPtrArray::ArrayIterator) {
	if (*ds_ptr) {
	    delete *ds_ptr;
	}
    }

    delete old_fmt_parents;

    return pvda;

error:
    delete old_fmt_parents;
    delete pvda;

    pthrow prcserror << "Error scanning RCS rlog output on RCS file "
		    << squote(versionfile) << dotendl;

}

/* This is here mostly for backward compatibility. */
static PrVoidError
fix_version_data(const ProjectVersionDataPtrArray *pvda,
		 const DstringPtrArray            *old_fmt_parents)
{
    /* Use a slow (O(n^2) in the number of versions) search here, it
     * avoids a lot of complexity, C++ hassle, and is only required
     * for versions prior to 1.2 during rebuild.  In fact, avert your
     * eyes, if I could start over, I would. */

    for (int i = 0; i < pvda->length(); i += 1) {
	if (old_fmt_parents->index(i)) {
	    for (int j = 0; j < i; j += 1) {
		const char* par_name = *old_fmt_parents->index(i);

		if (strcmp (pvda->index(j)->prcs_minor(), minor_version_of (par_name)) == 0 &&
		    strcmp (pvda->index(j)->prcs_major(), major_version_of (par_name)) == 0) {

		    pvda->index(i)->new_parent(j);
		    break;
		}
	    }
	}
    }

    return NoError;
}

/*
 * VC_get_version_data --
 *
 *     Retrieves all the log information from an RCS file.
 * Results:
 *     If return is successful, return value is a heap allocated array of
 *     RcsVersionData.
 * Side Effects:
 *     Memory is allocated for the return value.
 * Parameters:
 *     versionfile names the pathname of an RCS file.
 */
PrRcsDeltaPtrError
VC_get_version_data(const char* versionfile)
{
    ArgList *argl;
    int rev_count, i;

    Return_if_fail(argl << rlog_command.new_arg_list());

    argl->append(versionfile);

    Return_if_fail(rlog_command.open(true, false));

    if(VC_get_token(rlog_command.standard_out()) == RlogTotalRevisions)
	rev_count = atoi(rcs_text);
    else
	pthrow prcserror << "Unrecognized output from RCS rlog command on RCS file "
			<< squote(versionfile) << dotendl;

    RcsDelta* pvda = new RcsDelta(NULL);

    if(rev_count == 0)
	return pvda;

    if(VC_get_token(NULL) != RlogVersion)
	goto error;

    for(i = 0; i < rev_count; i += 1) {
	RcsToken vclex;
	RcsVersionData* pvd = new RcsVersionData;

	pvd->rcs_version(p_strdup(rcs_text));

	pvda->insert(pvd->rcs_version(), pvd);

	while(true) {
	    vclex = (RcsToken)VC_get_token(NULL);
	    if(vclex == RlogVersion || vclex == NoTokenMatch)
		break;
	    switch(vclex) {
	    case RlogDate:
		pvd->date(timestr_to_time_t(rcs_text));
		break;
	    case RlogAuthor:
		pvd->author(p_strdup(rcs_text));
		break;
	    case RlogLines:
		pvd->set_lines (rcs_text);
		break;
	    case PrcsMajorVersion:
	    case PrcsMinorVersion:
	    case PrcsParentMajorVersion:
	    case PrcsParentMinorVersion:
	    case PrcsDescendsMajorVersion:
	    case PrcsDescendsMinorVersion:
		/* These are okay to make really really ancient repositories
		 * work, probably only the prcs repository. */
		break;
	    case PrcsVersionDeleted:
	    case PrcsParents:
	    case NoTokenMatch:
	    case RlogTotalRevisions:
	    case RlogVersion:
	    case PrevRevision:
	    case NewRevision:
	    case InitialRevision:
	    case RcsAbort:
		goto error;
	    }
	}

	if(!pvd->OK())
	    pthrow prcserror << "Incomplete version log information in RCS file "
			    << squote(versionfile) << dotendl;
    }

    Return_if_fail_if_ne(rlog_command.close(), 0)
	pthrow prcserror << "RCS rlog command returned non-zero status on RCS file "
			<< squote(versionfile) << dotendl;

    return pvda;

error:
    pthrow prcserror << "Error scanning RCS rlog output on RCS file "
		    << squote(versionfile) << dotendl;

}

PrVoidError
VC_get_one_version_data (const char* versionfile, const char *versionnum, RcsVersionData *rvd)
{
    ArgList *argl;
    RcsToken vclex;

    Return_if_fail(argl << rlog_command.new_arg_list());

    Dstring vstr;

    vstr.append ("-r");
    vstr.append (versionnum);

    argl->append(vstr.cast ());
    argl->append(versionfile);

    Return_if_fail(rlog_command.open(true, false));

    if (VC_get_token(rlog_command.standard_out()) != RlogTotalRevisions) {
	pthrow prcserror << "Unrecognized output from RCS rlog command on RCS file "
			<< squote(versionfile) << dotendl;
    }

    if (VC_get_token(NULL) != RlogVersion || strcmp(versionnum, rcs_text) != 0) {
	goto error;
    }

    rvd->rcs_version(p_strdup(rcs_text));

    while(true) {
	vclex = (RcsToken)VC_get_token(NULL);
	if(vclex == RlogVersion)
	    goto error;
	if (vclex == NoTokenMatch)
	    break;
	switch(vclex) {
	case RlogDate:
	    rvd->date(timestr_to_time_t(rcs_text));
	    break;
	case RlogAuthor:
	    rvd->author(p_strdup(rcs_text));
	    break;
	case RlogLines:
	    rvd->set_lines (rcs_text);
	    break;
	case PrcsMajorVersion:
	case PrcsMinorVersion:
	case PrcsParentMajorVersion:
	case PrcsParentMinorVersion:
	case PrcsDescendsMajorVersion:
	case PrcsDescendsMinorVersion:
	    /* These are okay to make really really ancient repositories
	     * work, probably only the prcs repository. */
	    break;
	case PrcsVersionDeleted:
	case PrcsParents:
	case NoTokenMatch:
	case RlogTotalRevisions:
	case RlogVersion:
	case PrevRevision:
	case NewRevision:
	case InitialRevision:
	case RcsAbort:
	    goto error;
	}
    }

    if(!rvd->OK()) {
	pthrow prcserror << "Incomplete version log information in RCS file "
			 << squote(versionfile) << dotendl;
    }

    Return_if_fail_if_ne(rlog_command.close(), 0) {
	pthrow prcserror << "RCS rlog command returned non-zero status on RCS file "
			 << squote(versionfile) << dotendl;
    }

    return NoError;

error:
    pthrow prcserror << "Error scanning RCS rlog output on RCS file "
		    << squote(versionfile) << dotendl;

}

PrIntError
VC_get_version_count(const char* versionfile)
{
    ArgList *argl;
    int rev_count;

    Return_if_fail(argl << rlog_command.new_arg_list());

    argl->append("-h");
    argl->append(versionfile);

    Return_if_fail(rlog_command.open(true, false));

    if(VC_get_token(rlog_command.standard_out()) == RlogTotalRevisions) {
	rev_count = atoi(rcs_text);
    } else {
	pthrow prcserror << "Unrecognized output from RCS 'rlog -h' command on RCS file "
			 << squote(versionfile) << dotendl;
    }

    Return_if_fail_if_ne(rlog_command.close(), 0) {
	pthrow prcserror << "RCS rlog command returned non-zero status on RCS file "
			 << squote(versionfile) << dotendl;
    }
    
    return rev_count;
}

PrBoolError VC_delete_version(const char* version,
			      const char* revisionfile)
{
    ArgList* args;
    Dstring outdate;

    Return_if_fail(args << rcs_command.new_arg_list());

    outdate.append("-o");
    outdate.append(version);

    args->append("-q");
    args->append(outdate);
    args->append(revisionfile);

    Return_if_fail(rcs_command.open(true, true));

    Return_if_fail_if_ne(rcs_command.close(), 0)
	return false;

    return true;
}

PrVoidError VC_set_log(const char* new_log_text,
		       const char* version,
		       const char* revisionfile)
{
    Dstring log;
    ArgList* args;

    log.append("-m");
    log.append(version);
    log.append(":");
    log.append(new_log_text);

    Return_if_fail(args << rcs_command.new_arg_list());

    args->append(log);
    args->append(revisionfile);

    Return_if_fail(rcs_command.open(true, true));

    Return_if_fail_if_ne(rcs_command.close(), 0) {
	pthrow prcserror << "RCS exited abnormally while updating log" << dotendl;
    }

    return NoError;
}

/* RcsDelta */

void RcsDelta::insert(const char* index, RcsVersionData* data)
{
    insertion_count += 1;

    if(index[0] == '\0') {
	rcs_data = data;
	return;
    }

    char* end_ptr;
    int delta_num = (int)strtol(index, &end_ptr, 10);

    if(!delta_array)
	delta_array = new RcsDeltaPtrArray;

    delta_array->expand(delta_num + 1, (RcsDelta*)0);

    if(delta_array->index(delta_num) == NULL)
	delta_array->index(delta_num, new RcsDelta(this));

    if(*end_ptr)
	delta_array->index(delta_num)->insert(end_ptr + 1, data);
    else
	delta_array->index(delta_num)->insert(end_ptr, data);
}

RcsVersionData* RcsDelta::lookup(const char* index)
{
    char* end_ptr;
    int delta_num = (int)strtol(index, &end_ptr, 10);

    if(!index[0])
	return rcs_data;
    else if(!delta_array || delta_array->length() <= delta_num)
       return NULL;
    else if(delta_array->index(delta_num)) {
	if(*end_ptr)
	    return delta_array->index(delta_num)->lookup(end_ptr + 1);
	else
	    return delta_array->index(delta_num)->lookup(end_ptr);

    } else
	return NULL;
}

void RcsDelta::remove(const char* index)
{
    insertion_count -= 1;

    char* end_ptr;
    int delta_num = (int)strtol(index, &end_ptr, 10);

    if(!index[0])
	rcs_data = NULL;
    else if(!delta_array || delta_array->length() <= delta_num)
       /* its not here? */
       return;
    else if(delta_array->index(delta_num)) {
	if(*end_ptr)
	    delta_array->index(delta_num)->remove(end_ptr + 1);
	else
	    delta_array->index(delta_num)->remove(end_ptr);

    }
    /* else its not here whats up with that */
}

/* This does an iterative table walk, you dont have to read it */
void RcsDelta::DeltaIterator::next()
{
    static int array_length; /* this doesnt need to eat stack space */

    _index += 1;

    if(!_current->delta_array) {
	array_length = 0;
    } else {
	array_length = _current->delta_array->length();
    }

    /* Its a leaf, 0 is the length of the non-existant array,
     * the data of a particular node is returned after the
     * children have been passed over. */
    if(_index == array_length) {
	_current_data = _current->rcs_data;
	/* _current_data may be NULL if this version was just deleted,
	 * if so, call next() again. */
	if(!_current_data)
	    next();
	return;
    } else if(_index < array_length) {
	if(_current->delta_array->index(_index)) {
	    _current->my_iter_index = _index;
	    _current = _current->delta_array->index(_index);
	    _index = -1;
	}

	next();
	return;
    } else {
	if(!_current->parent) {
	    /* this is the root, so we have covered all of them. */
	    _finished = true;
	    return;
	} else {
	    /* Restore the parents state */
	    _current = _current->parent;
	    _index = _current->my_iter_index;
	    next();
	    return;
	}
    }
}

RcsDelta::~RcsDelta()
{
    if(delta_array) {
	foreach(child, delta_array, RcsDeltaPtrArray::ArrayIterator) {
	    if(*child) {
		delete *child;
	    }
	}

	delete delta_array;
    }

    if(rcs_data) {
	delete rcs_data;
    }
}


RcsDelta::RcsDelta(RcsDelta* parent0)
    :rcs_data(NULL), delta_array(NULL),
     insertion_count(0),
     parent(parent0), my_iter_index(-1) { }

int RcsDelta::count() const { return insertion_count; }

RcsDelta::DeltaIterator::DeltaIterator(RcsDelta* d0) :_current(d0), _finished(false), _index(-1) { next(); }
RcsVersionData* RcsDelta::DeltaIterator::operator*() const { return _current_data; }
bool RcsDelta::DeltaIterator::finished() const { return _finished; }

/* RcsVersionData */

RcsVersionData::RcsVersionData()
    :_date(0),
     _author(NULL),
     _rcs_version(NULL),
     _plus_lines (0),
     _minus_lines (0),
     _gc_mark(NotReferenced)  { }

RcsVersionData::~RcsVersionData() { }

void RcsVersionData::set_lines(const char *lines)
{
    ASSERT (lines[0] == '+', "the format is...");
    char *endp, *endp2;
    long pl = strtol (lines+1, &endp, 10);
    ASSERT (strncmp (endp, " -", 2) == 0, "the format is...");
    long ml = strtol (endp + 2, & endp2, 10);
    ASSERT (endp2[0] == '\n', "the format is...");
    _plus_lines = pl;
    _minus_lines = ml;
    //prcsoutput << "set lines: " << lines << " plus " << pl << " minus " << ml << prcsendl;
}

void RcsVersionData::set_plus_lines(int pls)
{
    _plus_lines = pls;
}

void RcsVersionData::set_minus_lines(int mls)
{
    _minus_lines = mls;
}

void RcsVersionData::set_plus_lines(const char *pls)
{
    char *endp;
    long  pl = strtol (pls, &endp, 10);
    ASSERT (endp[0] == 0, "bad format");
    _plus_lines = pl;
}

void RcsVersionData::set_minus_lines(const char *mls)
{
    char *endp;
    long  ml = strtol (mls, &endp, 10);
    ASSERT (endp[0] == 0, "bad format");
    _minus_lines = ml;
}

time_t RcsVersionData::date() const { return _date; }
int RcsVersionData::length() const { return _length; }
const char* RcsVersionData::author() const { return _author; }
const char* RcsVersionData::rcs_version() const { return _rcs_version; }
const char* RcsVersionData::unkeyed_checksum() const { return _unkeyed_checksum; }
int RcsVersionData::plus_lines() const { return _plus_lines; }
int RcsVersionData::minus_lines() const { return _minus_lines; }
RcsVersionData::GCMark RcsVersionData::referenced() const { return _gc_mark; }

void RcsVersionData::date(time_t t0) { _date = t0; }
void RcsVersionData::length(int l0) { _length = l0; }
void RcsVersionData::author(const char* a0) { _author = a0; }
void RcsVersionData::rcs_version(const char* rv0) { _rcs_version = rv0; }
void RcsVersionData::unkeyed_checksum(const char* ck0) { memcpy(_unkeyed_checksum, ck0, 16); }
void RcsVersionData::reference(GCMark gc0) { _gc_mark = gc0; }
bool RcsVersionData::OK() const { return _date > 0 && _author && _rcs_version; }

/* ProjectVersionData */

ProjectVersionData::ProjectVersionData(int index)
    :RcsVersionData(),
     _prcs_major(NULL), _prcs_minor(NULL),
     _prcs_minor_int(-1), _index(index),
     _deleted(false) { }
ProjectVersionData::~ProjectVersionData() { }

void ProjectVersionData::clear_flags() { _flag1 = false; _flag2 = false; }
bool ProjectVersionData::flag1(bool n) { bool o = _flag1; _flag1 = n; return o; }
bool ProjectVersionData::flag2(bool n) { bool o = _flag2; _flag2 = n; return o; }
bool ProjectVersionData::flag1() const { return _flag1; }
bool ProjectVersionData::flag2() const { return _flag2; }

bool ProjectVersionData::deleted() const { return _deleted; }
const char* ProjectVersionData::prcs_major() const { return _prcs_major; }
const char* ProjectVersionData::prcs_minor() const { return _prcs_minor; }
int ProjectVersionData::prcs_minor_int() const { return _prcs_minor_int; }
int ProjectVersionData::parent_count() const { return _parents.length(); }
int ProjectVersionData::parent_index(int p_num) const { return _parents.index(p_num); }
void ProjectVersionData::new_parent(int pi0) { _parents.append (pi0); }
int ProjectVersionData::version_index() const { return _index; }

void ProjectVersionData::deleted(bool del0) { _deleted = del0; }
void ProjectVersionData::prcs_major(const char* pm0) { _prcs_major = pm0; }
void ProjectVersionData::prcs_minor(const char* pm0) { _prcs_minor = pm0; _prcs_minor_int = atoi(pm0); }

bool ProjectVersionData::OK() const { return RcsVersionData::OK() && _prcs_major && _prcs_minor; }

ostream& operator<<(ostream& o, const ProjectVersionData* pvd)
{
    o << pvd->_prcs_major << "." << pvd->_prcs_minor;
    return o;
}
