/* -*-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: checkin.cc 1.22.1.2.1.9.1.23.1.1.1.6.1.15.1.2.1.49 Sun, 09 May 2004 18:21:12 -0700 jmacd $
 */

#include "prcs.h"

#include "setkeys.h"
#include "projdesc.h"
#include "fileent.h"
#include "repository.h"
#include "vc.h"
#include "checkout.h"
#include "checkin.h"
#include "misc.h"
#include "memseg.h"
#include "rebuild.h"
#include "system.h"
#include "quick.h"
#include "syscmd.h"
#include "diff.h"

static const char rlog_header[] = "checked in by PRCS version %d %d %d\n";

static PrVoidError write_new_prjfile(ProjectDescriptor* project, ProjectVersionData *parent_data);
static PrVoidError checkin_each_file(ProjectDescriptor*);
static PrVoidError maybe_query_user(ProjectDescriptor*);
static PrVoidError slow_eliminate_prepare(ProjectDescriptor*);
static PrVoidError checkin_cleanup_handler(void* data, bool);
static PrVoidError compute_partial_modified(ProjectDescriptor  *project,
					    ProjectDescriptor  *pred_project);

PrPrcsExitStatusError checkin_command()
{
    ProjectVersionData *new_project_data;         /* New version data. */
    ProjectDescriptor  *project;                  /* The working project. */
    ProjectVersionData *pred_project_data = NULL; /* The predecessor project, if different */
    ProjectDescriptor  *pred_project = NULL;      /* from working Project-Version. */

    Return_if_fail(project << read_project_file(cmd_root_project_full_name,
						cmd_root_project_file_path,
						true,
						(ProjectReadData)(KeepMergeParents)));

    install_cleanup_handler(checkin_cleanup_handler, project);

    Return_if_fail(project->init_repository_entry(cmd_root_project_name, true, true));

    /* remove files that were not named on the command line */
    eliminate_unnamed_files(project);

    /* warn the user about filenames on the command line that didn't match any files */
    Return_if_fail(warn_unused_files(true));

    /* select the new version and possibly warn the user of unwise checkins */
    Return_if_fail(new_project_data << project->resolve_checkin_version(cmd_version_specifier_major,
									&pred_project_data));

    prcsinfo << "Checking in project " << squote(cmd_root_project_name)
	     << " version " << new_project_data << dotendl;

    /* check that no unselected files have empty descriptors and that all selected
     * files exist */
    Return_if_fail(eliminate_working_files(project, QueryUserRemoveFromCommandLine));
    Return_if_fail(check_project(project));

    /* if the auxiliary database is present, read it and eliminate files that
     * have not been touched since the last checkout or rekey */
    project->read_quick_elim();
    project->quick_elim_unmodified();

    if (pred_project_data) {
	Return_if_fail (pred_project << project->repository_entry()->
			                   checkout_prj_file (project->project_full_name(),
							      pred_project_data->rcs_version(),
							      KeepNothing));
    }

    /* now eliminate files that have not changed by reading and unkeying files,
     * comparing the unkeyed length, then computing their checksum and comparing
     * the unkeyed checksum.  with the unkeyed file in memory, prepare the repository
     * file (possibly uncompress), and write it out into the temporary location
     * in the repository. */

    Return_if_fail(slow_eliminate_prepare(project));

    /* report changes to the user, and also make partial checkins use
     * the pred_project's versions for files omitted from the command
     * line of this checkin. */
    Return_if_fail(compute_partial_modified(project, pred_project));

    if(option_report_actions) {
	return ExitSuccess;
    }

    /* check if the user wants to enter a log. */
    Return_if_fail(maybe_query_user(project));

    project->repository_entry()->Rep_log() << "Checking in project version " << new_project_data << dotendl;

    Return_if_fail(checkin_each_file(project));

    foreach_fileent(fe_ptr, project) {
	FileEntry* fe = *fe_ptr;

	if(fe->real_on_cmd_line())
	    project->quick_elim_update_fe(fe);
    }

    project->quick_elim_update();

    Return_if_fail(project->update_attributes(new_project_data));

    Return_if_fail(write_new_prjfile(project, new_project_data));

    project->repository_entry()->Rep_log() << "Finished checking in" << dotendl;

    return ExitSuccess;
}

static PrVoidError compute_partial_modified(ProjectDescriptor *project,
					    ProjectDescriptor *pred_project)
{
    /* This shares some structure with changes_command, not all of it
     * though.  We know that pred_project is not deleted. */
    bool project_modified = false;

    /* if pred_project is non-null, this function makes an ugly use of
     * the on_command_line() status bit to mean something else:
     * whether the preceding project versions' file was deleted. */

    foreach_fileent(fe_ptr, project) {
	FileEntry *fe = *fe_ptr;

	const char *name = fe->working_path();
	const char *lname;
	bool        modified = false;
	bool        new_file = false;
	FileEntry  *pred_fe = NULL;

	if (pred_project) {
	    Return_if_fail (pred_fe << pred_project->match_fileent (fe));
	}

	if (pred_fe) {
	    pred_fe->set_on_command_line (false);
	} else {
	    project_modified = true;
	    new_file = true;

	    if (option_long_format) {
		prcsoutput << format_type (fe->file_type(), true)
			   << " " << squote (name) << " was added" << dotendl;
	    }
	}

	if (pred_fe && fe->file_type() != pred_fe->file_type()) {
	    project_modified = true;

	    if (option_long_format) {
		prcsoutput << "File " << squote (name) << " changes from type "
			   << format_type (pred_fe->file_type()) << " to "
			   << format_type (fe->file_type()) << dotendl;
	    }
	}

	if (pred_fe) {

	    const char* a = pred_fe->working_path(), *b = fe->working_path();

	    if (!pathname_equal (a, b)) {
		project_modified = true;

		if (option_long_format) {
		    prcsoutput << "File " << squote (name) << " is renamed from "
			       << squote (pred_fe->working_path()) << dotendl;
		}
	    }
	}

	if (!fe->on_command_line()) {
	    if ((pred_fe != NULL) &&
		(pred_fe->file_type () == fe->file_type ()) &&
		(fe->file_type () == RealFile) &&
		(strcmp (fe->descriptor_name(), pred_fe->descriptor_name()) != 0 ||
		 strcmp (fe->descriptor_version_number(), pred_fe->descriptor_version_number()) != 0))
	    {
		if (option_long_format) {
		    prcsoutput << "Non-selected file " << squote (name) << " assumes pre-existing version" << dotendl;
		}

		/* Note: This is the side effect of this call. */
		fe->set_descriptor_name (pred_fe->descriptor_name ());
		fe->set_version_number  (pred_fe->descriptor_version_number ());
	    }
	    continue;
	}

	switch (fe->file_type()) {
	case Directory:
	    break;
	case SymLink:
	    Return_if_fail (lname << read_sym_link (name));

	    fe->set_link_name (lname);

	    if (!pred_fe ||
		!pred_fe->link_name() ||
		strcmp (lname, pred_fe->link_name()) != 0) {
		modified = true;
	    }

	    if (!new_file) {
		if (!modified && option_really_long_format) {
		    prcsoutput << "Symlink " << squote(name) << " is unmodified" << dotendl;
		} else if (modified && option_long_format) {
		    prcsoutput << "Symlink " << squote(name) << " is modified" << dotendl;
		}
	    }

	    break;
	case RealFile:
	    if (!fe->unmodified()) {
		modified = true;

		if (option_long_format && !new_file) {
		    prcsoutput << "New version of file " << squote(name) << dotendl;
		}
	    }

	    if (pred_fe &&
		fe->descriptor_name() &&
		fe->file_type() == pred_fe->file_type()) {

		if (strcmp (fe->descriptor_name(), pred_fe->descriptor_name()) != 0 ||
		    strcmp (fe->descriptor_version_number(),
			    pred_fe->descriptor_version_number()) != 0) {

		    modified = true;
		    if (option_long_format) {
		      prcsoutput << "File " << squote (name) << " changes versions" << dotendl;
		    }
		}
	    }

	    if (fe->file_mode() != fe->stat_permissions()) {

		if (option_long_format && !new_file) {
		    prcsoutput << "File " << squote(name) << " has new mode" << dotendl;
		}
		
		modified = true;

		fe->set_file_mode (fe->stat_permissions());
	    }

	    if (!new_file && !modified && option_really_long_format && option_long_format) {
		prcsoutput << "File " << squote(name) << " is unmodified" << dotendl;
	    }

	    break;
	}

	project_modified |= modified;
    }

    if (pred_project) {
	foreach_fileent (fe_ptr, pred_project) {
	    FileEntry *fe = *fe_ptr;

	    if (fe->on_command_line()) { 
		project_modified = true;

		if (option_long_format) {
		    prcsoutput << "File " << squote(fe->working_path()) << " was deleted" << dotendl;
		}
	    }
	}
    }

    if(!project_modified) {
	prcsquery << "No modifications were found.  "
		  << force("Aborting checkin")
		  << report("Abort checkin")
		  << option('n', "Continue, with no modified files in this version")
		  << deffail('y')
		  << query("Abort");

	Return_if_fail(prcsquery.result());
    }

    return NoError;
}

PrVoidError ProjectDescriptor::update_attributes(ProjectVersionData *new_project_data)
{
    /* The Versions. */
    if (strcmp (*project_version_minor(), "0") != 0) {
	parent_version_major()->assign(*project_version_major());
	parent_version_minor()->assign(*project_version_minor());
    }
    project_version_major()->assign(new_project_data->prcs_major());
    project_version_minor()->assign(new_project_data->prcs_minor());

    /* the Checkin-Time attribute */
    checkin_time()->sprintf("%s", get_utc_time());

    /* the Logs */
    if (option_version_log) {
	if (new_version_log()->length() != 0) {
	    prcsquery << "You have supplied a version log on the command line, but the project file contains a new version log already.  "
		      << force("Overriding")
		      << report("Override")
		      << optfail('n')
		      << defopt('y', "Override the project file")
		      << query("Override");

	    Return_if_fail(prcsquery.result());
	}
	version_log()->assign(option_version_log_string);
    } else {
	version_log()->assign(*new_version_log());
    }

    new_version_log()->truncate(0);

    /* the Checkin-Login attribute */
    checkin_login()->assign(get_login());

    set_full_version (true);

    return NoError;
}

PrVoidError check_project(ProjectDescriptor *project)
{
    const char* name;

    foreach_fileent(fe_ptr, project) {
	FileEntry *fe = *fe_ptr;

        name = fe->working_path();

        if(!fe->on_command_line() && fe->empty_descriptor()) {
	    /* if the user is doing a partial operation, the file needs
	     * to have a non-empty internal descriptor, the descriptor
	     * is currently, empty, and init has asked to register new
	     * files for empty descriptors, complain. */

	    If_fail (fe->stat()) {
		pthrow prcserror << "The file " << squote(name)
				<< " has an empty descriptor and does not exist.  You may not "
		    "do a partial checkin excluding files with empty descriptors" << dotendl;
	    } else {
		static BangFlag bang;

		prcsquery << "The file " << squote(name)
			  << " has an empty descriptor and was excluded on the command line.  Partial checkins may not ignore files with empty descriptors.  "
			  << force("Continuing")
			  << report("Continue")
			  << allow_bang(bang)
			  << optfail('n')
			  << defopt('y', "Select this file, as if it were on the command line")
			  << query("Select this file");

		Return_if_fail(prcsquery.result());

		fe->set_on_command_line(true);
	    }
        } else if(fe->file_type() == RealFile && fe->descriptor_name()) {
	    RcsVersionData* data;

	    /* Check to see that the version exists, even if not on the
	     * command line */

	    If_fail(data << project->repository_entry()->
		    lookup_rcs_file_data(fe->descriptor_name(),
					 fe->descriptor_version_number())) {
		pthrow prcserror << "The missing file may have been in a deleted project version"
				 << dotendl;
	    }
	}
    }

    return NoError;
}

#ifndef NDEBUG
static PrVoidError invalid()
{
    fs_copy_filename(temp_file_1, bug_name());

    prcserror << "Generated an invalid project file, aborting" << dotendl;

    return bug ();
}
#endif

static PrVoidError write_new_prjfile(ProjectDescriptor *P, ProjectVersionData *parent)
{
#ifndef NDEBUG
    ProjectDescriptor* copy;

    Return_if_fail(P->write_project_file(temp_file_1));

    If_fail(copy << read_project_file(P->project_full_name(), temp_file_1, false, KeepNothing)) {
	return invalid();
    }

    foreach_fileent(fe_ptr, copy) {
	FileEntry* fe = *fe_ptr;

	if ( fe->empty_descriptor() ) {
	    /* Hopefully this will catch Gene's problem. */
	    return invalid();
	}
    }

    /*delete copy;*/
#endif

    Return_if_fail(P->write_project_file(cmd_root_project_file_path));

    Return_if_fail(P->checkin_project_file(parent));

    return NoError;
}

static void checkin_hook (void* v_fe, const char* new_version)
{
    FileEntry *fe = (FileEntry*) v_fe;

    fe->set_version_number (new_version);
}

static PrVoidError checkin_each_file(ProjectDescriptor *project)
{
    /* inconsistencies have been checked the Files attribute has been
     * modified already, excepting the new RCS version numbers */

    foreach_fileent(fe_ptr, project) {
	FileEntry *fe = *fe_ptr;

	if(!fe->real_on_cmd_line() || fe->unmodified())
	    continue;

	Return_if_fail(VC_checkin(fe->temp_file_path(),
				  fe->descriptor_version_number(),
				  fe->full_descriptor_name(),
				  NULL,
				  fe,
				  checkin_hook));
    }

    Dstring rlog;

    rlog.sprintf (rlog_header, prcs_version_number[0],
		  prcs_version_number[1],
		  prcs_version_number[2]);

    Return_if_fail(VC_checkin(NULL, NULL, NULL, rlog, NULL, checkin_hook));

    /* @@@ Really should do some verification here.  It seems that the
     * above chmod can reach an NFS server before the new RCS versions
     * do... Dan Bonachea's problem. */

    foreach_fileent(fe_ptr, project) {
	FileEntry *fe = *fe_ptr;

	if(!fe->real_on_cmd_line() || fe->unmodified())
	    continue;

	RcsVersionData ver;

	Return_if_fail (VC_get_one_version_data (fe->full_descriptor_name (),
						 fe->descriptor_version_number (),
						 & ver));

	fe->set_lines (ver.plus_lines (), ver.minus_lines ());

	/* RCS 5.7 does not preserve the mode of an empty version file
	 * created by rcs -i, so we ignore the umask when registering
	 * new files and just chmod the first version. */
	if(strcmp(fe->descriptor_version_number(), "1.1") == 0) {
	    Return_if_fail (project->repository_entry()->
			    Rep_chown_file(fe->descriptor_name()));
	}
    }

    return NoError;
}

/*
 * eliminate_unnamed_files --
 *
 *     this marks files that were not given on the command line so
 *     that they can be ignored in certain stages of the checkin (and
 *     other) commands.  if no files were given, it leaves them all
 *     marked, otherwise, it unmarks them all and marks all files (and
 *     recursively marks directories).  */
void eliminate_unnamed_files(ProjectDescriptor* project)
{
    if(cmd_filenames_count < 1) {
        if (! option_exclude_project_file)
	    cmd_prj_given_as_file = true;
        return;
    }

    for(int i = 0; i < cmd_filenames_count; i += 1) {
        /* special case, the project file */
        if(strcmp(cmd_root_project_file_path, cmd_corrected_filenames_given[i]) == 0) {
            cmd_filenames_found[i] = true;
            cmd_prj_given_as_file = true;
        }
    }

    foreach_fileent(fe_ptr, project) {
	FileEntry *fe = *fe_ptr;

        fe->set_on_command_line(false);

        for(int j = 0; j < cmd_filenames_count; j += 1) {
            const char *f, *w;

            w = fe->working_path();
            f = cmd_corrected_filenames_given[j];

            while(*f && *w && *f == *w) {
                while(f[1] == '/') { f += 1; }
                while(w[1] == '/') { w += 1; }
                f += 1;
                w += 1;
            }

            if (*f == 0 && (w[-1] == '/' || *w == 0 || f[-1] == '/')) {
                cmd_filenames_found[j] = true;
                fe->set_on_command_line(true);
            }
        }
    }
}


/*
 * warn_unused_files --
 *
 *     this optionally prompts the user to continue and warns if any files
 *     were not matched by a call to eliminate_unnamed_files.
 *
 *     if prompt_abort is true, returns an error if the user does not
 *     confirm ignoring unmatched filenames.
 *
 *     if prompt_abort is false, it will return an error if files are unmatched.
 */
PrVoidError warn_unused_files(bool prompt_abort)
{
    bool any = false, ret = true, need_prompt = false;

    if(cmd_filenames_count < 1)
        return NoError;

    for(int j = 0; j < cmd_filenames_count; j += 1) {
        any |= cmd_filenames_found[j];
        if(!cmd_filenames_found[j]) {
            if(prompt_abort) {
		need_prompt = true;
                prcswarning << "File or directory " << squote(cmd_filenames_given[j])
			    << " on command line does not match any working files" << dotendl;
            } else {
                ret = false;
                prcswarning << "File or directory " << squote(cmd_filenames_given[j])
			    << " on command line did not match any working files" << dotendl;
            }
        }
    }

    if(!any)
	pthrow prcserror << "No files on command line match working files" << dotendl;

    if(need_prompt) {
	prcsquery << "Files on the command line did not match files in the project.  "
		  << force("Continuing")
		  << report("Continue")
		  << optfail('n')
		  << defopt('y', "Continue, ignoring these files")
		  << query("Continue");

	Return_if_fail(prcsquery.result());
    }

    if(ret)
	return NoError;
    else
	return PrVoidError(NonFatalError);
}

static PrVoidError maybe_query_user(ProjectDescriptor* P)
{
    int tty, c, lc, llc;

    if(P->new_version_log()->length() > 0 ||
       !get_environ_var("PRCS_LOGQUERY") || /* either it has been edited or PRCS_LOGQUERY is not set */
       option_report_actions ||
       option_force_resolution ||
       option_version_log)
        return NoError;

    tty = isatty(STDIN_FILENO);

    if(tty) {
	prcsquery << "Empty New-Version-Log.  "
		  << option('e', "Enter version log now")
		  << defopt('p', "Proceed with an empty version log")
		  << query("Enter a log, or proceed");

	char c;

	Return_if_fail(c << prcsquery.result());

	if(c == 'p')
	    return NoError;

        prcsoutput << "Enter description terminated by a single '.' or EOF" << dotendl;

	fprintf(stdout, ">> ");
    }

    re_query_message = ">> ";
    re_query_len = 3;

    lc = '\n';
    llc = '\n';

    while(true) {
	If_fail(c << Err_fgetc(stdin))
	    pthrow prcserror << "Read failure on stdin" << perror;

	if (c == EOF) {
	    fprintf (stdout, "\n");
	    break;
	}

        if(tty && c == '\n') {
            if(lc == '.' && llc == '\n') {
                P->new_version_log()->truncate(P->new_version_log()->length() - 1);
                break;
            }
            fprintf(stdout, ">> ");
        }

        llc = lc;
        lc = c;

        P->new_version_log()->append(c);
    }

    if(tty) fprintf(stdout, "Done.\n");

    /* I don't think its possible that PRCS will ever need to query the
     * user after this, but I might as well reopen it. */
    if(freopen(ctermid(NULL), "r", stdin) == NULL)
	pthrow prcserror << "Couldn't reopen the standard input" << perror;

    return NoError;
}

PrProjectVersionDataPtrError
ProjectDescriptor::resolve_checkin_version(const char          *maj,
					   ProjectVersionData **pred_project_data)
{
    ProjectVersionData *cur_project_data = new ProjectVersionData(-1);
    ProjectVersionData *first_parent_project_data = NULL;
    Dstring *new_major = new Dstring; /* Most of this stuff leaks. */
    Dstring *new_minor = new Dstring;
    int prev_highest_minor;

    /* Move New-Merge-Parents to Merge-Parents. */
    Return_if_fail(adjust_merge_parents());

    /* Find first parent version, if any. */
    if(strcmp(*project_version_minor(), "0") != 0) {
	/* Don't beleive what's in the project file, make sure its in the
	 * repository. */

	/* @@@ I am concerned that all callers of lookup_version are
	 * forced to check the same error conditions (NULL &&
	 * ->deleted()), but I don't know why it should be this way.
	 */
	first_parent_project_data = repository_entry()->lookup_version(this);

	if(!first_parent_project_data) {
	    pthrow prcserror << "Illegal version " << full_version() << " in working project file" << dotendl;
	}

	if(first_parent_project_data->deleted ()) {
	    pthrow prcserror << "Project version " << first_parent_project_data << " has been deleted" << dotendl;
	}

	cur_project_data->new_parent(first_parent_project_data->version_index());

	if (_merge_parents->length() > 0) {

	    /* See if the previous merge was complete. */
	    if (_merge_parents->last_index()->state & MergeStateIncomplete) {
		/* Last merge is incomplete, ask to abort this one and finish last one. */
		Return_if_fail (merge_help_query_incomplete(_merge_parents->last_index()));
	    }

	    /* Add in any merge parents. */
	    foreach (ent_ptr, _merge_parents, MergeParentEntryPtrArray::ArrayIterator) {
		MergeParentEntry *ent = *ent_ptr;

		if (ent->state & MergeStateParent) {
		    cur_project_data->new_parent(ent->project_data->version_index());
		}
	    }
	}
    } else {
	/* We assume that the first parent is a the Parent-Version field, so we can't
	 * add Merge-Parents if we didn't add a Parent-Version. */
	if (_merge_parents->length() > 0) {
	    pthrow prcserror << "Kind of strange to have merge parents when you're an empty "
		"version, isn't it?" << prcsendl;
	}
    }

    /* Compute the new major version. */
    if(!maj[0]) {
	new_major->assign(*project_version_major());
    } else if(strcmp(maj, "@") == 0) {
	int highest_major;

	if(repository_entry()->version_count() == 0)
	    pthrow prcserror << "No versions in the repository, can't resolve major version '@'" << dotendl;

	highest_major = repository_entry()->highest_major_version();

	if(highest_major >= 0) {
	    new_major->assign_int(highest_major);
	} else
	    pthrow prcserror << "No numeric major versions in the repository, "
		"can't resolve major version '@'" << dotendl;
    } else {
	new_major->assign(maj);
    }

    /* Compute the new minor version. */
    prev_highest_minor = repository_entry()->highest_minor_version(*new_major, true);
    new_minor->assign_int(prev_highest_minor + 1);

    cur_project_data->prcs_major (*new_major);
    cur_project_data->prcs_minor (*new_minor);

    /* Warn if the minor version specified on cmd line differs. */
    if (cmd_version_specifier_minor &&
	strcmp (cmd_version_specifier_minor, "@") != 0 &&
	strcmp (cmd_version_specifier_minor, *new_minor) != 0) {

	prcsquery << "Minor version "
		  << squote(cmd_version_specifier_minor)
		  << " will be ignored, new minor is "
		  << new_minor << ".  "
		  << force("Continuing")
		  << report("Continue")
		  << optfail('n')
		  << defopt('y', "Continue and ignore supplied minor")
		  << query("Continue");

	Return_if_fail(prcsquery.result());
    }

    if(!repository_entry()->major_version_exists(*new_major)) {
	/* If the branch is new, no need to check if the checkin is okay. */

	if(repository_entry()->version_count() != 0) {
	    /* Other majors do exist, confirm creation. */
	    prcsquery << "No previous major version named "
		      << squote(new_major) << ".  "
		      << force("Creating")
		      << report("Create")
		      << optfail('n')
		      << defopt('y', "Create the new branch and continue")
		      << query("Create");

	    Return_if_fail(prcsquery.result());
	}

	*pred_project_data = first_parent_project_data;

    } else {
	/* We've already got a ProjectVersionData entry for the working
	 * version, its nameless.  Now we check the common ancestor of
	 * it and the previous highest minor version on the branch we're
	 * checking into.  If this common ancestor is equal to the
	 * previous highest, then things are cool.  If they are not, then
	 * we warn the user and ask them what to do. */
	ProjectVersionDataPtrArray *ancestors;
	ProjectVersionData *new_data;

	new_data = repository_entry()->lookup_version (*new_major, prev_highest_minor);

	ASSERT (new_data, "the minor number was already verified.");

	ancestors = repository_entry()-> common_version(cur_project_data, new_data);

	bool ancestor_unique = ancestors->length() == 1;
	bool ancestor_diff = !ancestor_unique || ancestors->index(0) != new_data;

	if (! ancestor_unique) {
	    /* The common ancestor is troublesome.  Query now. */
	    if (ancestors->length() == 0) {
		prcsquery << "No common ancestor between new version and working version.  ";
	    } else {
		prcsquery << "Ambiguous common ancestor between new version and working version.  ";
	    }
	} else if (ancestor_diff) {
	    /* The common ancestor is unique, but there are versions
	     * in the way, so query. */

	    prcsquery << "Intervening versions have been checked in between the target version, "
		      << cur_project_data << ", and the working version's common ancestor, "
		      << ancestors->index(0) << ".  ";
	}

	if (!ancestor_unique || ancestor_diff) {
	    prcsquery << report ("Query Abort/Force Continue")
		      << force ("Continuing", 'n')
		      << option ('n', "Continue, ignoring warning")
		      << deffail ('y')
		      << query ("Abort");

	    Return_if_fail (prcsquery.result());
	}

	/* You're allowed to checkin past the deleted end of a branch.  In that case its
	 * as if the ancestry starts fresh with this checkin. */
	if (!new_data->deleted()) {
	    *pred_project_data = new_data;
	}

	delete ancestors;
    }

    return cur_project_data;
}

void checkin_prj_hook (void* v_new_rcs_version, const char* new_version)
{
    ((Dstring*)v_new_rcs_version)->assign(new_version);
}

PrVoidError ProjectDescriptor::checkin_project_file(ProjectVersionData *new_project_data)
{
    struct stat buf;
    Dstring version_log, new_rcs_version;

    foreach_fileent(fe_ptr, this)
        (*fe_ptr)->update_repository(repository_entry());

    If_fail(Err_stat(project_file_path(), &buf))
	pthrow prcserror << "Stat failed on newly written project file " << project_file_path() << perror;

    new_project_data->date(get_utc_time_t());
    new_project_data->author(get_login());
    new_project_data->length(buf.st_size);

    format_version_log(new_project_data, version_log);

    Dstring tmp_name (repository_entry()->Rep_name_of_version_file());
    const char* abs_path;

    Return_if_fail (abs_path << absolute_path (project_file_path()));

    tmp_name.truncate (tmp_name.length() - 2);

    unlink (tmp_name);

    If_fail (Err_symlink (abs_path, tmp_name))
      pthrow prcserror << "Error creating symbolic link "
		      << squote(tmp_name) << perror;

    If_fail(VC_checkin(tmp_name,
		       "",
		       repository_entry()->Rep_name_of_version_file(),
		       NULL,
		       &new_rcs_version,
		       checkin_prj_hook))
	pthrow prcserror << "Failure checking in project file" << dotendl;

    If_fail(VC_checkin(NULL, NULL, NULL, version_log, NULL, checkin_prj_hook))
	pthrow prcserror << "Failure checking in project file" << dotendl;

    unlink (tmp_name);

    // @@@ Really should do some verification here to work around NFS bugs

    if (strcmp (new_rcs_version, "1.1") == 0) {
        Dstring desc_name (project_name());
	desc_name.append (".prj");

	/* RCS 5.7 does not preserve the mode of an empty version file
	 * created by rcs -i, so we ignore the umask when registering
	 * new files and just chmod the first version. */
	Return_if_fail(repository_entry()->Rep_chown_file(desc_name));
    }

    new_project_data->rcs_version(new_rcs_version);

    /* Keith Owens 1.3.0 bug: if we crash right here then the RCS file
     * has been updated but not the prcs_data file.  This causes
     * future checkins to get the wrong parent version index.  This is
     * now checked during RepEntry::init. */

    repository_entry()->add_project_data(new_project_data);

    Return_if_fail(repository_entry()->close_repository());

    return NoError;
}

void format_version_log(ProjectVersionData* project_data, Dstring& log)
{
    static const char prcs_version_format[] =
	"PRCS major version: %s\n"
	"PRCS minor version: %s\n";

    log.sprintf(rlog_header, prcs_version_number[0],
		prcs_version_number[1],
		prcs_version_number[2]);
    log.sprintfa(prcs_version_format,
		 project_data->prcs_major(),
		 project_data->prcs_minor());

    if (project_data->parent_count() > 0) {
	log.sprintfa("PRCS parent indices: %d", project_data->parent_index(0));

	if (project_data->parent_count() > 1) {
	    for (int i = 1; i < project_data->parent_count(); i += 1)
		log.sprintfa(":%d", project_data->parent_index(i));
	}

	log.sprintfa("\n");
    }

    if(project_data->deleted())
	log.append("PRCS version deleted\n");
}

void ProjectDescriptor::quick_elim_unmodified()
{
    foreach_fileent(fe_ptr, this) {
	FileEntry* fe = *fe_ptr;

	if(!fe->real_on_cmd_line() || !fe->descriptor_name())
	    continue;

	if(_quick_elim && _quick_elim->unchanged(fe)) {
	    DEBUG("Quick elim succeeds for file " << squote(fe->working_path()));
	    fe->set_unmodified(true);
	}
    }
}

static PrVoidError slow_eliminate_prepare(ProjectDescriptor *project)
{
    static MemorySegment file_buffer(false);
    static MemorySegment setkey_buffer(false);

    foreach_fileent(fe_ptr, project) {
	FileEntry      *fe = *fe_ptr;
	const char     *output_buffer;
	int             output_len;
	bool            setkeys;
	char*           checksum;

	if(!fe->real_on_cmd_line() || fe->unmodified())
	    continue;

	Return_if_fail(file_buffer.map_file(fe->working_path()));

	if(fe->keyword_sub()) {
	    If_fail( setkeys << setkeys_inoutbuf(file_buffer.segment(),
						 file_buffer.length(),
						 &setkey_buffer,
						 fe,
						 Unsetkeys))
		pthrow prcserror << "Keyword replacement failed on file "
			        << squote(fe->working_path()) << perror;

	    output_buffer = setkey_buffer.segment();
	    output_len = setkey_buffer.length();
	} else {
	    output_buffer = file_buffer.segment();
	    output_len = file_buffer.length();
	}

	checksum = md5_buffer(output_buffer, output_len);

	fe->set_checksum(checksum);
	fe->set_unkeyed_length(output_len);

	if(fe->descriptor_name()) {
	    /* If it doesn't have an empty descriptor. */
	    RcsVersionData *old_data;

	    Return_if_fail(old_data << project->repository_entry() ->
			   lookup_rcs_file_data(fe->descriptor_name(),
						fe->descriptor_version_number()));

	    if (output_len == old_data->length() &&
	        memcmp(checksum, old_data->unkeyed_checksum(), 16) == 0) {

		DEBUG("Slow elim succeeds for file " << squote(fe->working_path()));

		fe->set_unmodified (true);

		continue;
	    }
	}

	Return_if_fail(fe->initialize_descriptor(project->repository_entry(),
						 !option_report_actions,
						 true));
	/* Can't free the rep_comp_cache for these initializations, because
	 * of the batch nature of this checkin. If you ever change that, be
	 * sure to clean up. */

	if(!option_report_actions) {
	    unlink(fe->temp_file_path());

	    const char* fullpath;

	    if(fe->keyword_sub() && setkeys) {
		FILE* temp_out;

		fullpath = make_temp_file("");

		If_fail(temp_out << Err_fopen(fullpath, "w"))
		    pthrow prcserror << "Failed opening temp file for writing "
				    << squote(fullpath) << perror;

		If_fail (Err_fwrite(output_buffer, output_len, temp_out))
		    pthrow prcserror << "Failed writing temp file "
				    << squote(fullpath) << perror;

		If_fail (Err_fclose(temp_out))
		    pthrow prcserror << "Failed writing temp file "
				    << squote(fullpath) << perror;
	    } else {
		Return_if_fail(fullpath << name_in_cwd(fe->working_path()));
	    }

	    If_fail (Err_symlink(fullpath, fe->temp_file_path())) {
		pthrow prcserror << "Error creating symbolic link "
				<< squote(fe->temp_file_path()) << perror;
	    }

#if defined (PRCS_DEVEL) && ! defined(NDEBUG)
	    if (option_debug) {
		struct stat sbuf;
		/* The link broke once on scheme.xcf, while running as
		 * root with -j32 on a project consisting of the gcc-2.7.2.2
		 * source tree with all files zeroed. */

		If_fail (Err_stat (fe->temp_file_path(), &sbuf)) {
		    prcserror << "Newly created symlink " << squote(fe->temp_file_path())
			      << " doesn't stat" << dotendl;
		    abort();
		}
	    }
#endif
	}
    }

    if(!option_report_actions)
	Return_if_fail(VC_register(NULL));

    return NoError;
}

void FileEntry::update_repository(RepEntry* rep_entry) const
{
    if(real_on_cmd_line() && !unmodified()) {
	RcsVersionData ver;
	ver.date             (get_utc_time_t());
	ver.author           (get_login());
	ver.length           (unkeyed_length());
	ver.unkeyed_checksum (checksum());
	ver.rcs_version      (descriptor_version_number());
	ver.set_plus_lines   (plus_lines ());
	ver.set_minus_lines  (minus_lines ());

	rep_entry->add_rcs_file_data(descriptor_name(), &ver);
    }
}

static PrVoidError checkin_cleanup_handler(void* data, bool sig)
{
    if (!sig)
        return NoError;

    ProjectDescriptor* project = (ProjectDescriptor* ) data;

    foreach_fileent(fe_ptr, project) {
	FileEntry* fe = *fe_ptr;

	if(!fe->on_command_line())
	    continue;

	if(fe->temp_file_path() != NULL)
	    unlink(fe->temp_file_path());
    }

    return NoError;
}

/* Given an input string, it modiifes the string to contain the parent path.
 * Returns false if there are no more entries.  Path may not be empty. */
static bool parent_path_of(Dstring &string)
{
    int inlen = string.length();

    ASSERT(inlen > 0, "");

    /* First remove any trailing slash, then remove the trailing component. */
    if (string.index(inlen-1) == '/') {
	string.truncate(inlen-1);
    }

    /* Earlier steps avoid the case of a "/" path name, so the path should
     * still be non-empty. */
    ASSERT(string.length() > 0, "");

    /* The length of the A/B/C/D/ compent, minus the length of E. */
    const char *p = string.cast();
    int len = strip_leading_path(p) - p;

    if (len == 0) {
	return false;
    }

    /* Leave a trailing '/'.  This is important for the strncmp() in
     * maybe_void_file_entry_subdir. */
    string.truncate(len);
    ASSERT (string.index(string.length() - 1) == '/', "");
    return true;
}

/* Returns whether to skip this file entry because, for example, we already
 * determined that its parent directory doesn't exist (and has been ignored).
 * It is given that stat() or lstat() failed on this file, and this performs
 * one of several possible actions: query the user, report to the user, issue
 * a report for forced operations, etc. */
static PrBoolError maybe_void_file_entry_subdir(FileEntry* fe, MissingFileAction action)
{
    static DstringPtrArray* eliminated_subdirs = NULL;
    struct stat buf;
    const char* name = fe->working_path();
    bool has_parent;

    if(!eliminated_subdirs)
	eliminated_subdirs = new DstringPtrArray;

    /* If it doesn't have any slashes in the path, then there are no parents
     * for this entry to be a subdirectory of. */
    if(!strchr(name, '/'))
	return false;

    /* The two path variables.  The first value for parent_path is the parent
     * of the argument.  Then we walk up the path using this_path and
     * parent_path. */
    Dstring parent_path (name);
    Dstring this_path;

    /* Now reduce parent_path to the last trailing slash. */
    has_parent = parent_path_of(parent_path);
    ASSERT (has_parent, "checked in strchr() above");

    /* Warning: this is linear search in a nested loop. */
    foreach(elim_ptr, eliminated_subdirs, DstringPtrArray::ArrayIterator) {
	Dstring* elim = *elim_ptr;

	if(strncmp(elim->cast(), name, elim->length()) == 0) {
	    switch (action) {
	    case QueryUserRemoveFromCommandLine:
		fe->set_on_command_line(false);
		break;
	    case NoQueryUserRemoveFromCommandLine:
		fe->set_on_command_line(false);
		break;
	    case SetNotPresentWarnUser:
		fe->set_present(false);
		break;
	    }

	    return true;
	}
    }

    if(stat(parent_path, &buf) >= 0 || errno != ENOENT)
	return false;

    for (;;) {

	this_path.assign(parent_path);

	has_parent = parent_path_of(parent_path);

	if (! has_parent) {
	    break;
	}
	
	const char* s = parent_path;

	if(stat(s[0] == 0 ? "." : s, &buf) >= 0) {
	    static BangFlag bang;

	    switch (action) {
	    case QueryUserRemoveFromCommandLine:
		prcsquery << "The directory " << squote(this_path)
			  << " does not exist.  "
			  << force("Ignoring all entries")
			  << report("Ignore all entries")
			  << optfail('n')
			  << defopt('y', "Ignore all entries of this directory")
			  << allow_bang (bang)
			  << query("Ignore it and descendants");

		Return_if_fail(prcsquery.result());

		fe->set_on_command_line(false);
		break;
	    case NoQueryUserRemoveFromCommandLine:
		prcswarning << "The directory " << squote(this_path)
			    << " does not exist.  Ignoring all entries" << dotendl;

		fe->set_on_command_line(false);
		break;
	    case SetNotPresentWarnUser:
		prcswarning << "The directory " << squote(this_path)
			    << " does not exist.  Assuming all entries are unmodified" << dotendl;

		fe->set_present(false);
		break;
	    }

	    eliminated_subdirs->append(new Dstring(this_path));

	    return true;

	} else if(errno != ENOENT) {
	    return false;
	}
    }

    return false;
}

PrVoidError eliminate_working_files(ProjectDescriptor* project, MissingFileAction action)
{
    foreach_fileent(fe_ptr, project) {
	FileEntry* fe = *fe_ptr;

        if(fe->on_command_line()) {
	    static BangFlag bang;
	    bool file_present;

	    Return_if_fail(file_present << fe->check_working_file());

	    if(file_present)
		continue;

	    bool ignore_subdir;

	    Return_if_fail(ignore_subdir << maybe_void_file_entry_subdir(fe, action));

	    if(ignore_subdir)
		continue;

	    switch (action) {
	    case QueryUserRemoveFromCommandLine:
		prcsquery << "File " << squote(fe->working_path()) << " is unavailable.  "
			  << force("Continuing")
			  << report("Continue")
			  << allow_bang(bang)
			  << defopt('y', "Ignore this file, as if it were not on the command line")
			  << optfail('n')
			  << query("Ignore this file");

		Return_if_fail(prcsquery.result());
		fe->set_on_command_line(false);
		break;
 	    case NoQueryUserRemoveFromCommandLine:
		prcswarning << "File " << squote(fe->working_path())
			    << " is unavailable.  Continuing" << dotendl;
		fe->set_on_command_line(false);
		break;
	    case SetNotPresentWarnUser:
		prcswarning << "File " << squote(fe->working_path())
			    << " is unavailable.  Assuming unmodified" << dotendl;
		fe->set_present(false);
		break;
	    }
        }
    }

    return NoError;
}
