/* -*-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: prcs.cc 1.23.1.1.1.14.1.18.1.8.1.16.1.1.1.7.1.41.1.2 Wed, 06 Feb 2002 20:57:16 -0800 jmacd $
 */

/* $Format: "static const char prcs_version_id[] = \"$ProjectVersion$ $ProjectAuthor$ $ProjectDate$\";"$ */
static const char prcs_version_id[] = "1.3.3-relase.1 jmacd Sun, 09 May 2004 18:34:01 -0700";

#include "fnmatch.h"

#include "prcs.h"
#include "lock.h"
#include "docs.h"
#include "vc.h"
#include "misc.h"
#include "projdesc.h"
#include "fileent.h"
#include "syscmd.h"
#include "repository.h"
#include "system.h"

#if defined(__BEOS__)
#include <KernelKit.h>
#endif

extern "C" {
#include <signal.h>
#include <sys/time.h>
#include "getopt.h"
}

/* $Format: "const int prcs_version_number[3] = { $ReleaseMajorVersion$, $ReleaseMinorVersion$, $ReleaseMicroVersion$ };" $ */
const int prcs_version_number[3] = { 1, 3, 3 };

/* $Format: "const char prcs_version_string[] = \"$ReleaseVersion$\";"$ */
const char prcs_version_string[] = "1.3.3";

/* The following classes are only used inside this file, and oraganize
 * several arrays of information used to select the command and
 * configuration. */
enum ProjectArgType {
    InsureProjectName,
    NoInsureProjectName,
    NoProjectRequired,
    OptionalProjectName
};

enum RevisionSpecifierArgType {
    NoRevisionArg,
    OneRevisionArg,
    TwoRevisionArgs
};

enum RevisionFlags {
    NoFlags        = 0,
    AllowWildCards = 1 << 1,
    NoDefault      = 1 << 2
};

enum DiffArgType {
    ReallyDiffArgs,
    ReallyExecuteArgs,
    NoDiffArgs
};

struct CommandOptionPair {
    char opt;
    const char* desc;
    int *addr;
};

struct CommandNamePair {
    SystemCommand* command;
    const char* name;
};

struct PrcsCommand {
    const char *command_name;
    const char *defmaj;
    const char *defmin;
    const char *defmin_maj_only;
    int revision_flags;
    PrPrcsExitStatusError (*command_function)();
    const char *help_string;
    RevisionSpecifierArgType revision_arg_type;
    ProjectArgType project_arg_type;
    const char* optstring;
    DiffArgType diff_arg_type;
    int n_files_allowed;
};

struct CleanupHandler {
    CleanupHandler(PrVoidError (*h)(void*, bool), void* d, struct CleanupHandler* n)
	:handler(h), data(d), next_handler(n) { }
    PrVoidError (*handler)(void*, bool);
    void *data;
    CleanupHandler* next_handler;
};

/* Static data */

static PrcsCommand *command = NULL;
static PrcsCommand *subcommand = NULL;
static bool illegal_option = false;
static struct CleanupHandler *cleanup_handler_list = NULL;
static struct CleanupHandler *alarm_handler_list = NULL;

static const char check_rcs_arg[] = "-V";
static const char check_rcs_expected[] = "*RCS version 5*";
static const CommandNamePair check_rcs = { &rcs_command, "rcs" };

static const char check_diff_arg[] = "-v";
static const char check_diff_expected[] = "*GNU diffutils*";
static const CommandNamePair check_diff = { &gdiff_command, "gdiff" };

static const char check_diff3_arg[] = "-v";
static const char check_diff3_expected[] = "*GNU diffutils*";
static const CommandNamePair check_diff3 = { &gdiff3_command, "gdiff3" };

static CommandNamePair const command_names[] = {
    { &rcs_command, "rcs" },
    { &ci_command, "ci" },
    { &co_command, "co" },
    { &rlog_command, "rlog" },
    { &gdiff_command, "diff" },
    { &gdiff3_command, "diff3" },
    { &gzip_command, "gzip" },
    { &ls_command, "ls" },
    { &tar_command, "tar" },
    { NULL, NULL }
};

#define MATCH_CHAR       '\300'
#define NOTMATCH_CHAR    '\301'
#define ALL_CHAR         '\302'
#define PRE_CHAR         '\303'
#define PIPE_CHAR        '\304'
#define PLAINFORMAT_CHAR '\305'
#define SORT_CHAR        '\306'
#define NOKEYW_CHAR      '\307'
#define VLOG_CHAR        '\310'

#define EXECUTE_STR      "\300\301\302\303\304"
#define PLAINFORMAT_STR  "\305"
#define SORT_STR         "\306"
#define NOKEYW_STR       "\307"
#define VLOG_STR         "\310"

static CommandOptionPair const command_options[] = {
    { 'i', "--immediate", &option_immediate_uncompression },
    { 'k', "--keywords", &option_diff_keywords },
    { 'N', "--new", &option_diff_new_file },
    { 'd', "--delete", &option_populate_delete },
    { 'z', "--compress", &option_package_compress },
    { 'p', "--preserve", &option_preserve_permissions },
    { 'P', "--exclude-project-file", &option_exclude_project_file },
    { 'u', "--unlink", &option_unlink },
    { 's', "--skilled-merge", &option_skilled_merge },
    { NOKEYW_CHAR, "--no-keywords", &option_nokeywords },
    { VLOG_CHAR, "--version-log", &option_version_log },
    { SORT_CHAR, "--sort", &option_sort },
    { MATCH_CHAR, "--match", &option_match_file },
    { NOTMATCH_CHAR, "--not", &option_not_match_file },
    { ALL_CHAR, "--all", &option_all_files },
    { PRE_CHAR, "--pre", &option_preorder },
    { PIPE_CHAR, "--pipe", &option_pipe },
    { 0, NULL }
};

static struct option const long_options[] = {
  /* standard operands */
  {"version",                 no_argument, 0, 'v'},
  {"help",                    no_argument, 0, 'h'},
  {"long-long-format",        no_argument, 0, 'L'},
  {"long-format",             no_argument, 0, 'l'},
  {"no-action",               no_argument, 0, 'n'},
  {"quiet",                   no_argument, 0, 'q'},
  {"force",                   no_argument, 0, 'f'},
  {"repository",              required_argument, 0, 'R'},
  {"jobs",                    required_argument, 0, 'j'},
  {"plain-format",            no_argument, 0, PLAINFORMAT_CHAR },
#ifdef PRCS_DEVEL
  {"debug",                   no_argument, 0, 'D'},
  {"tune",                    required_argument, 0, 't'},
#endif

  /* non-standard operands */
  {"new",                     no_argument, 0, 'N'},
  {"keywords",                no_argument, 0, 'k'},
  {"immediate",               no_argument, 0, 'i'},
  {"delete",                  no_argument, 0, 'd'},
  {"compress",                no_argument, 0, 'z'},
  {"preserve-permissions",    no_argument, 0, 'p'},
  {"preserve",                no_argument, 0, 'p'},
  {"permissions",             no_argument, 0, 'p'},
  {"exclude-project-file",    no_argument, 0, 'P'},
  {"revision",                required_argument, 0, 'r'},
  {"unlink",                  no_argument, 0, 'u'},
  {"skilled-merge",           no_argument, 0, 's'},
  {"no-keywords",             no_argument, 0, NOKEYW_CHAR},
  {"version-log",             required_argument, 0, VLOG_CHAR},
  {"sort",                    required_argument, 0, SORT_CHAR},
  {"match",                   required_argument, 0, MATCH_CHAR},
  {"not",                     required_argument, 0, NOTMATCH_CHAR},
  {"all",                     no_argument, 0, ALL_CHAR},
  {"pre",                     no_argument, 0, PRE_CHAR},
  {"pipe",                    no_argument, 0, PIPE_CHAR},
  {0,0,0,0}};

/* It looks like a bug in the optstring that EXECUTE_STR members don't
 * have ':' characters, but GNU getopt doesn't seem to mind. */
static const char prcs_optstring[] = "+vhHLlnqfR:r:NkidzpPruj:s"
                                     EXECUTE_STR PLAINFORMAT_STR NOKEYW_STR VLOG_STR ":" SORT_STR ":"
#ifdef PRCS_DEVEL
"Dt:"
#endif
;

static PrcsCommand commands[] = {
    /* commands accepting -r */
    {"checkin", "", "@", "@", NoFlags,
     checkin_command, checkin_help_string,
     OneRevisionArg, InsureProjectName, "uj" VLOG_STR, NoDiffArgs, -1 },

    {"checkout", "@", "@", "@", NoFlags,
     checkout_command, checkout_help_string,
     OneRevisionArg, InsureProjectName, "upP", NoDiffArgs, -1 },

    {"diff", "", "", "@", NoFlags,
     diff_command, diff_help_string,
     TwoRevisionArgs, InsureProjectName, "NkP", ReallyDiffArgs, -1 },

    {"merge", "", "@", "@", NoFlags,
     merge_command, merge_help_string,
     OneRevisionArg, InsureProjectName, "us", ReallyDiffArgs, -1 },

    {"delete", "", "", "@", NoDefault,
     delete_command, delete_help_string,
     OneRevisionArg, InsureProjectName, NULL, NoDiffArgs, 0 },

    {"info", "*", "*", "*", AllowWildCards,
     info_command, info_help_string,
     OneRevisionArg, InsureProjectName, SORT_STR, NoDiffArgs, -1 },

    {"changes", "", "", "@", NoFlags,
     changes_command, changes_help_string,
     TwoRevisionArgs, InsureProjectName, "", NoDiffArgs, -1 },

    {"execute", "", "", "@", NoFlags,
     execute_command, execute_help_string,
     OneRevisionArg, InsureProjectName, "P" EXECUTE_STR, ReallyExecuteArgs, -1 },

    /* commands not accepting -r */
    {"populate", NULL, NULL, NULL, NoFlags,
     populate_command, populate_help_string,
     NoRevisionArg, InsureProjectName, "du" NOKEYW_STR, NoDiffArgs, -1 },

    {"depopulate", NULL, NULL, NULL, NoFlags,
     depopulate_command, depopulate_help_string,
     NoRevisionArg, InsureProjectName, "u", NoDiffArgs, -1 },

    {"package", NULL, NULL, NULL, NoFlags,
     package_command, package_help_string,
     NoRevisionArg, InsureProjectName, "z", NoDiffArgs, 1 },

    {"unpackage", NULL, NULL, NULL, NoFlags,
     unpackage_command, unpackage_help_string,
     NoRevisionArg, NoInsureProjectName, NULL, NoDiffArgs, -2 },

    {"rekey", NULL, NULL, NULL, NoFlags,
     rekey_command, rekey_help_string,
     NoRevisionArg, InsureProjectName, "u", NoDiffArgs, -1 },

    {"config", NULL, NULL, NULL, NoFlags,
     config_command, config_help_string,
     NoRevisionArg, NoProjectRequired, NULL, NoDiffArgs, 0 },

    /* fake command entries */
    {"admin", NULL, NULL, NULL, NoFlags,
     NULL, NULL, NoRevisionArg, InsureProjectName, NULL, NoDiffArgs, 0 },

    {NULL, NULL, NULL, NULL, NoFlags,
     NULL, NULL, NoRevisionArg, InsureProjectName, NULL, NoDiffArgs, 0 }
};

static PrcsCommand subcommands[] = {
    {"compress", NULL, NULL, NULL, NoFlags,
     admin_compress_command, admin_compress_help_string,
     NoRevisionArg, InsureProjectName, NULL, NoDiffArgs, 0 },

    {"uncompress", NULL, NULL, NULL, NoFlags,
     admin_uncompress_command, admin_uncompress_help_string,
     NoRevisionArg, InsureProjectName, "i", NoDiffArgs, 0 },

    {"rebuild",  NULL,  NULL, NULL, NoFlags,
     admin_rebuild_command, admin_rebuild_help_string,
     NoRevisionArg, InsureProjectName, NULL, NoDiffArgs, 0 },

    {"init",  NULL,  NULL, NULL, NoFlags,
     admin_init_command, admin_init_help_string,
     NoRevisionArg, InsureProjectName, NULL, NoDiffArgs, 0 },

    {"access", NULL,  NULL, NULL, NoFlags,
     admin_access_command, admin_access_help_string,
     NoRevisionArg, OptionalProjectName, NULL, NoDiffArgs, 0 },

    {"pdelete", NULL, NULL, NULL, NoFlags,
     admin_pdelete_command, admin_pdelete_help_string,
     NoRevisionArg, InsureProjectName, NULL, NoDiffArgs, 0 },

    {"pinfo", NULL, NULL, NULL, NoFlags,
     admin_pinfo_command, admin_pinfo_help_string,
     NoRevisionArg, NoProjectRequired, NULL, NoDiffArgs, 0 },

    {"prename", NULL, NULL, NULL, NoFlags,
     admin_prename_command, admin_prename_help_string,
     NoRevisionArg, InsureProjectName, NULL, NoDiffArgs, 1 },

    {NULL, NULL, NULL, NULL, NoFlags,
     NULL, NULL, NoRevisionArg, InsureProjectName, NULL, NoDiffArgs, 0 }
};

static const int friendly_signals[] = {
    SIGINT, SIGTERM, SIGPIPE, SIGHUP,
#ifdef SIGXCPU
    SIGXCPU,
#endif
#ifdef SIGXFSZ
    SIGXFSZ,
#endif
    0
};

static const int unfriendly_signals[] = {
    SIGBUS, SIGFPE, SIGILL, SIGQUIT, SIGSEGV, 0
};

#if defined(__BEOS__)
static thread_id beos_alarm_id = 0;
#endif

/* End static data */

static PrVoidError check_system_command(CommandNamePair com,
					const char* arg,
					const char* expected)
{
    Dstring output;

    ArgList *args;

    Return_if_fail(args << com.command->new_arg_list());

    args->append(arg);

    Return_if_fail(com.command->open(true, false));

    If_fail(read_string(com.command->standard_out(), &output))
	pthrow prcserror << "Read failed from " << com.name << perror;

    Return_if_fail_if_ne(com.command->close(), 0) {
	pthrow prcserror << com.name << ' ' << arg
			<< " failed, " << com.name
			<< " is probably too old for PRCS to use"
			<< dotendl;
    }

    if (fnmatch (expected, output.cast (), FNM_NOESCAPE) != 0) {
	pthrow prcserror << com.name
			 << " installation is too old, please install "
			 << expected << dotendl;
    }

    return NoError;
}

static PrVoidError check_system_commands()
{
    Return_if_fail(check_system_command(check_rcs,
					check_rcs_arg,
					check_rcs_expected));

    Return_if_fail(check_system_command(check_diff,
					check_diff_arg,
					check_diff_expected));

    Return_if_fail(check_system_command(check_diff3,
					check_diff3_arg,
					check_diff3_expected));

    return NoError;
}

/* Config checks and reports the paths of all the programs upon which
 * PRCS depends, and exits according to whether everything checked
 * out. */
PrPrcsExitStatusError config_command()
{
    const int width = 40;
    const CommandNamePair* command = command_names;
    PrPrcsExitStatusError ret(ExitSuccess);

    prcsoutput << "Version " << prcs_version_string << prcsendl;

    prcsoutput << prcs_version_id << prcsendl;

    if(get_environ_var("RCS_PATH"))
	prcsinfo << "Using $RCS_PATH to find binaries" << dotendl;

    for(;command->command;command += 1) {
	If_fail(command->command->init()) {
	    ret = PrPrcsExitStatusError (FatalError);
	    continue;
	}

	prcsoutput << "System path for " << squote(command->name)
		   << " is: " << setcol(width) << command->command->path() << prcsendl;
    }

    const char* rep;

    If_fail(rep << Rep_guess_repository_path())
	rep = "-*- unavailable -*-";

    prcsoutput << "Repository path is: " << setcol(width) << rep << prcsendl;

    const char* log = get_environ_var ("PRCS_LOGQUERY");

    prcsoutput << "Log querying is: " << setcol(width)
	       << (log ? "on" : "off") << prcsendl;

    const char* editor = get_environ_var ("PRCS_CONFLICT_EDITOR");

    if (editor)
	prcsoutput << "Conflict editor is: " << setcol(width) << editor << prcsendl;

    const char* merge = get_environ_var ("PRCS_MERGE_COMMAND");

    if (merge)
	prcsoutput << "Merge command is: " << setcol(width) << merge << prcsendl;

    const char* diff = get_environ_var ("PRCS_DIFF_COMMAND");

    if (diff)
	prcsoutput << "Diff command is: " << setcol(width) << diff << prcsendl;

    prcsoutput << "Job number is: " << setcol(width) << option_jobs << prcsendl;

    diff = get_environ_var ("PRCS_DIFF_OPTIONS");

    if (diff)
	prcsoutput << "Diff options are: " << setcol(width) << diff << prcsendl;

    const char* tmpdir = get_environ_var ("TMPDIR");
    if (!tmpdir) tmpdir = "/tmp";
    prcsoutput << "Temp directory is: " << setcol(width) << tmpdir << prcsendl;

    prcsoutput << "Plain formatting is: " << setcol(width)
	       << (option_plain_format ? "on" : "off") << prcsendl;

    //Return_if_fail(check_system_commands());

    return ret;
}

static bool check_ignored_options(const char* optstring)
{
    const CommandOptionPair *p = command_options;
    bool warning = false;

    for(; p->opt; p += 1) {
	if(*p->addr && !strchr(optstring, p->opt)) {
	    prcsinfo << "Ignoring command line switch "
		     << p->desc << dotendl;
	    warning = true;
	    *p->addr = 0;
	}
    }

    if (option_all_files && option_pipe) {
	prcsinfo << "Ignoring --pipe with --all specified" << dotendl;
	option_pipe = 0;
	warning = true;
    }

    return warning;
}

static PrVoidError check_major_version_name(const char* name)
{
    if (name[0] && strcmp(name, "@") != 0 &&
	VC_check_token_match(name, "label") <= 0)
	pthrow prcserror << "Illegal major version name "
			 << squote(name)
			 << dotendl;

    return NoError;
}

static PrVoidError check_minor_version_name(const char* name)
{
    if (name[0] && strcmp(name, "@") != 0 &&
	VC_check_token_match(name, "number") <= 0)
	pthrow prcserror << "Illegal minor version name "
			 << squote(name)
			 << dotendl;

    return NoError;
}

static PrPrcsExitStatusError invoke_command(PrcsCommand *c)
{
    bool warning = false;

    if (c->n_files_allowed >= 0 && cmd_filenames_count != c->n_files_allowed) {
	pthrow prcserror << "This command requires " << c->n_files_allowed
			<< " file-or-dir arguments" << dotendl;
    }

    if(c->project_arg_type == InsureProjectName ||
       (c->project_arg_type == OptionalProjectName && cmd_root_project_name)) {
	if(VC_check_token_match(cmd_root_project_name, "label") <= 0 ||
	   /* this length is arbitrary */
	   strlen(cmd_root_project_name) > 200) {
	    prcserror << "Illegal project name " << squote(cmd_root_project_name)
		       << dotendl;
	    exit(2);
	}
    }

    if (c->revision_arg_type != NoRevisionArg) {
	if (c->revision_flags & NoDefault &&
	    !cmd_version_specifier_major &&
	    !cmd_version_specifier_minor)
	    pthrow prcserror << "You must specify a revision argument with "
			    << squote ("-r") << " for this command" << dotendl;

	if (!cmd_version_specifier_major && !cmd_version_specifier_minor) {
	    cmd_version_specifier_major = c->defmaj;
	    cmd_version_specifier_minor = c->defmin;
	}

	if (!cmd_version_specifier_minor)
	    cmd_version_specifier_minor = c->defmin_maj_only;

	if (!cmd_version_specifier_minor[0] && cmd_version_specifier_major[0])
	    pthrow prcserror << "It is illegal to specify a null minor "
		"version without a null major version" << dotendl;

	if (!(c->revision_flags & AllowWildCards)) {
	    Return_if_fail(check_major_version_name(cmd_version_specifier_major));
	    Return_if_fail(check_minor_version_name(cmd_version_specifier_minor));
	}

	if (c->revision_arg_type == TwoRevisionArgs &&
	    cmd_alt_version_specifier_major) {
	    if (!cmd_alt_version_specifier_minor)
		cmd_alt_version_specifier_minor = c->defmin_maj_only;

	    if (!(c->revision_flags & AllowWildCards)) {
		Return_if_fail(check_major_version_name(cmd_alt_version_specifier_major));
		Return_if_fail(check_minor_version_name(cmd_alt_version_specifier_minor));
	    }
	}
    }

    warning |= check_ignored_options(c->optstring ? c->optstring : "");

    if(warning) {
	prcsquery << "Command line options are being ignored.  "
		  << force("Continuing")
		  << report("Continue")
		  << optfail('n')
		  << defopt('y', "Continue, ignoring these options")
		  << query("Continue");

	Return_if_fail(prcsquery.result());
    }

    if (cmd_version_specifier_minor &&
	cmd_version_specifier_minor[0] &&
	strcmp(cmd_version_specifier_minor, "@") != 0) {
	cmd_version_specifier_minor_int = atoi(cmd_version_specifier_minor);
    }

    return c->command_function();
}

static PrVoidError set_project_name(const char* specifier0)
{
    /* There is ambiguity between interpreting "prcs" as a directory
     * or as a project name.  It will only be interpreted as a dir
     * if the directory is really a directory. */
    int len = strlen(specifier0);
    const char* last_slash = strrchr(specifier0, '/');

    Dstring* project_path = new Dstring;
    Dstring* project_name = new Dstring;

    if (len > 4 && strcmp (specifier0 + len - 4, ".prj") == 0) {

	if (last_slash) {
	    project_path->assign(specifier0);
	    project_path->truncate(last_slash - specifier0);

	    project_name->assign(last_slash + 1);
	} else {
	    /* project_path is correct */
	    project_name->assign(specifier0);
	}

	project_name->truncate(project_name->length() - 4);

    } else if (fs_is_directory(specifier0)) {

	Dstring* prj;

	Return_if_fail(prj << guess_prj_name(specifier0));

	if (prj == NULL) {
	    pthrow prcserror << "Cannot determine project name, there must be a single "
		"project file in the directory " << squote(specifier0) << dotendl;
	}

	project_path->assign(specifier0);

	project_name->assign(*prj);

	delete prj;

    } else if (specifier0[len - 1] == '/') {
	pthrow prcserror << "Directory " << squote(specifier0)
			<< " does not exist, cannot determine project name" << dotendl;
    } else if (last_slash) {
	project_path->assign(specifier0);
	project_path->truncate(last_slash - specifier0);
	project_name->assign(last_slash + 1);
    } else {
	project_name->assign(specifier0);
    }

    if (project_path->length() > 0 &&
	project_path->index(project_path->length() - 1) != '/')
	project_path->append('/');

    while (strncmp(*project_path, "./", 2) == 0)
	project_path->assign(project_path->cast() + 2);

    Dstring* project_file = new Dstring;
    Dstring* project_full = new Dstring;

    project_full->assign(*project_path);
    project_full->append(*project_name);

    project_file->assign(*project_full);
    project_file->append(".prj");

    /* These all leak. */
    cmd_root_project_name      = project_name->cast();
    cmd_root_project_file_path = project_file->cast();
    cmd_root_project_full_name = project_full->cast();
    cmd_root_project_path      = project_path->cast();
    cmd_root_project_path_len  = project_path->length();
#if 0
    prcsinfo << "cmd_root_project_name = " << cmd_root_project_name << prcsendl;
    prcsinfo << "cmd_root_project_file_path = " << cmd_root_project_file_path << prcsendl;
    prcsinfo << "cmd_root_project_full_name = " << cmd_root_project_full_name << prcsendl;
    prcsinfo << "cmd_root_project_path = " << cmd_root_project_path << prcsendl;
#endif
    return NoError;
}

static PrIntError determine_project(PrcsCommand* command, int argc, char** argv)
{
    if(command->project_arg_type != NoProjectRequired) {
	if(argc > 0 && strcmp(argv[0], "--") != 0) {

	    Return_if_fail(set_project_name(argv[0]));

	    return 1;
	} else if (command->project_arg_type == InsureProjectName) {

	    Return_if_fail(set_project_name("./"));

	} else if (command->project_arg_type != OptionalProjectName) {
	    pthrow prcserror << "Missing operand" << dotendl;
	}
    }

    return 0;
}

static const char* correct_cmd_filename(char* cmd_filename)
{
    Dstring correct(cmd_filename);

    while (strncmp(correct.cast(), "./", 2) == 0)
	correct.assign(correct.cast() + 2);

    /* let "foo/" be the root project path.  We want to accept anything prefixed
     * with "foo/" or "foo". */

    if (cmd_root_project_path_len > 0) {
	if (correct.length() < cmd_root_project_path_len - 1)
	    /* its too short */
	    goto nowayman;

	if (strncmp(correct, cmd_root_project_path, cmd_root_project_path_len - 1) != 0)
	    /* the prefix is incorrect */
	    goto nowayman;

	if (correct.length() == cmd_root_project_path_len - 1) {
	    /* its "foo", cool */
	    correct.append('/');
	} else if (strcmp(correct.cast() + cmd_root_project_path_len, ".") == 0)
	    /* its "foo/.", get rid of the "." */
	    correct.truncate(cmd_root_project_path_len);
    }

    while (correct.length() > 0 && correct.last_index() == '/')
      correct.truncate (correct.length() - 1);

    return p_strdup(correct);

nowayman:

    prcswarning << "Command line file-or-dir " << squote(cmd_filename)
		<< " is not prefixed with project path " << squote(cmd_root_project_path)
		<< dotendl;

    return NULL;
}

static PrVoidError set_cmd_filenames()
{
    if(cmd_filenames_count > 0) {
       cmd_filenames_found = new bool[cmd_filenames_count];

       for(int i = 0; i < cmd_filenames_count; i += 1)
	  cmd_filenames_found[i] = false;

       cmd_corrected_filenames_given = new const char*[cmd_filenames_count];

       int j = 0;
       bool warn = false;
       for(int i = 0; i < cmd_filenames_count; i += 1) {
	   const char* new_name = correct_cmd_filename(cmd_filenames_given[i]);

	   if (!new_name) {
	       warn = true;
	       continue;
	   }

	   cmd_corrected_filenames_given[j] = new_name;
	   cmd_filenames_given[j++] = cmd_filenames_given[i];
       }

       cmd_filenames_count = j;

       for (int i = 0; i < cmd_filenames_count; i += 1) {
	   if (strcmp(cmd_corrected_filenames_given[i], cmd_root_project_path) == 0) {
	       	/* This is okay, there may be bogus file-or-dirs that won't be reported. */
#if 0
	       prcsinfo << "Command line file-or-dir " << squote(cmd_filenames_given[i])
			<< " matches project root, proceeding with all files selected"
			<< dotendl;
#endif

	       cmd_filenames_count = 0;
	   }
       }

       if (warn) {
	   prcsquery << "Command line file-or-dirs are being ignored.  "
		     << force("Continuing")
		     << report("Continue")
		     << optfail('n')
		     << defopt('y', "Proceed, ignore these filenames")
		     << query ("Proceed");

	   char c;

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

    return NoError;
}

static bool read_command_line(int argc, char** argv)
{
    int c;
    PrcsCommand *pc = commands;
    int num_revs = 0, i;
    int longind;
    bool saw_j = false;

    if(argc > 1)
	for(; pc->command_name != NULL; pc += 1)
	    if (strcmp(pc->command_name, argv[1]) == 0) {
		argv += 1;
		argc -= 1;
		command = pc;
		break;
	    }

    if(command && strcmp(command->command_name, "admin") == 0) {
	if(argc <= 1)
	    return false;

	for(pc = subcommands; pc->command_name != NULL; pc += 1)
	    if (strcmp(pc->command_name, argv[1]) == 0) {
		argv += 1;
		argc -= 1;
		subcommand = pc;
		break;
	    }

	if(subcommand == NULL)
	    return false;
    }

    while ((c = getopt_long(argc, argv, prcs_optstring, long_options, &longind)) != EOF)
    {
	char *min;
	switch (c) {
	case 'v':
	    prcsoutput << "Version " << prcs_version_string << prcsendl;
	    exit(0);
	    break;
	case 'h': case 'H': return false; break;
	case 'f': option_force_resolution = 1; break;
	case 'k': option_diff_keywords = 1; break;
	case 'l': option_long_format = 1; break;
	case 'i': option_immediate_uncompression = 1; break;
	case 'z': option_package_compress = 1; break;
	case 'd': option_populate_delete = 1; break;
	case 'q': option_be_silent = 1; break;
	case 'N': option_diff_new_file = 1; break;
	case 'L': option_really_long_format = 1; break;
	case 'n': option_report_actions = 1; break;
	case 'p': option_preserve_permissions = 1; break;
	case 'P': option_exclude_project_file = 1; break;
	case 'u': option_unlink = 1; break;
	case 'R': cmd_repository_path = optarg; break;
	case 'j':
	  {
	    char* end_ptr;
	    saw_j = true;

	    option_jobs = strtol (optarg, &end_ptr, 10);

	    if (end_ptr[0] != 0)
		option_jobs = -1;
	  }
	break;
	case MATCH_CHAR:
	    option_match_file = 1;
	    option_match_file_pattern = optarg;
	    break;
	case NOTMATCH_CHAR:
	    option_not_match_file = 1;
	    option_not_match_file_pattern = optarg;
	    break;
	case ALL_CHAR: option_all_files = 1; break;
	case PRE_CHAR: option_preorder = 1; break;
	case PIPE_CHAR: option_pipe = 1; break;
	case PLAINFORMAT_CHAR: option_plain_format = 1; break;
	case SORT_CHAR: option_sort = 1; option_sort_type = optarg; break;
	case NOKEYW_CHAR: option_nokeywords = 1; break;
	case VLOG_CHAR: option_version_log = 1; option_version_log_string = optarg; break;
#ifdef PRCS_DEVEL
	case 'D': option_n_debug = 0; break;
	case 't': option_tune = atoi (optarg); break;
#endif
	case 's': option_skilled_merge = 1; break;
	case 'r':
	    option_version_present = 1;

	    if(!command) break;

	    if (num_revs < 1 && !command->revision_arg_type == NoRevisionArg) {
		min = strrchr(optarg, '.'); /* last occurance of a period */
		if(min == NULL){
		    cmd_version_specifier_major = optarg;
		} else {
		    *min = '\0';
		    cmd_version_specifier_minor = min + 1;
		    cmd_version_specifier_major = optarg;
		}
		num_revs += 1;
	    } else if(num_revs < 2 && command->revision_arg_type == TwoRevisionArgs) {
		min = strrchr(optarg, '.'); /* last occurance of a period */
		if(min == NULL){
		    cmd_alt_version_specifier_major = optarg;
		} else {
		    *min = '\0';
		    cmd_alt_version_specifier_minor = min + 1;
		    cmd_alt_version_specifier_major = optarg;
		}
		num_revs += 1;
	    } else {
		prcserror << "Too many -r options given on command line" << dotendl;
		exit(2);
	    }

	    break;
	case '?':
	default:
	    illegal_option = true;
	    return false;
	    break;
	}
    }

    if (!option_sort)
	option_sort_type = "version";

    if (strcmp(option_sort_type, "version") != 0 &&
	strcmp(option_sort_type, "date") != 0) {
	prcserror << "Illegal sort type " << squote (option_sort_type) << dotendl;
	exit(2);
    }

    if (!saw_j) {
	const char* j = get_environ_var ("PRCS_JOB_NUMBER");
	char *je = NULL;

	if (j) {
	    option_jobs = strtol (j, &je, 10);

	    if (je[0] != 0)
		option_jobs = -1;
	}
    }


    if (option_jobs < 1 || option_jobs > 100 /* arbitrary */) {
	if (saw_j)
	    prcserror << "Illegal value for -j" << dotendl;
	else
	    prcserror << "Illegal value for PRCS_JOB_NUMBER" << dotendl;
	exit(2);
    }

    if(get_environ_var("PRCS_PLAIN_FORMAT"))
	option_plain_format = 1;

    if(option_report_actions)
	option_long_format = 1;

    if(subcommand)
	command = subcommand;

    if(command == NULL)
	return false;

    argc -= optind;
    argv += optind;

    int used;

    If_fail(used << determine_project(command, argc, argv))
	exit(2);

    argv += used;
    argc -= used;

    cmd_filenames_given = argv;
    cmd_filenames_count = argc;

    i = 0;
    cmd_diff_options_given = 0;
    cmd_diff_options_count = -1;

    while(i < cmd_filenames_count) {
	if(strcmp(cmd_filenames_given[i], "--") == 0) {
	    cmd_filenames_count = i;
	    cmd_diff_options_count = argc - i - 1;
	    cmd_diff_options_given = argv + i + 1;
	}
	i += 1;
    }

    if (command->diff_arg_type == NoDiffArgs && cmd_diff_options_count >= 0) {
	prcserror << "This command does not accept arguments following "
		  << squote ("--") << dotendl;
	exit(2);
    }

    if (cmd_diff_options_count < 0 && command->diff_arg_type == ReallyDiffArgs) {
	const char* pdo = get_environ_var ("PRCS_DIFF_OPTIONS");

	if (pdo) {
	    CharPtrArray *env_diff_options = new CharPtrArray;
	    char *p;

	    p = strtok (p_strdup (pdo), " \t\n");

	    while (p) {
		env_diff_options->append (p);
		p = strtok(NULL, " \t\n");
	    }

	    cmd_diff_options_given = (char**)env_diff_options->cast();
	    cmd_diff_options_count = env_diff_options->length();
	    /* @@@ leak */
	}
    }

    if (command->n_files_allowed == -1) {
	If_fail(set_cmd_filenames())
	    exit(2);
    }

    return true;
}

static void usage(char** argv)
{
    if (illegal_option) {
	cout << options_summary << "\n";
    } else if(command == NULL) {
	cout << "Usage: " << strip_leading_path(argv[0])
	     << " command [subcommand] [option ...] "
	    "[project [file-or-dir]]\n";
	cout << general_help_string;
    } else if(strcmp(command->command_name, "admin") == 0) {
	if(subcommand == NULL) {
	    cout << admin_help_string;
	} else {
	    cout << subcommand->help_string;
	}
    } else {
	cout << command->help_string;
    }

    exit(2);
}

/* Cleanup */

void install_cleanup_handler(PrVoidError (* handler)(void *, bool), void* data)
{
    cleanup_handler_list = new CleanupHandler(handler, data, cleanup_handler_list);
}

void install_alarm_handler(PrVoidError (* handler)(void *, bool), void* data)
{
    alarm_handler_list = new CleanupHandler(handler, data, alarm_handler_list);
}

static PrVoidError clean_up(bool signaled)
{
    sigset_t signal_mask;

    sigfillset(&signal_mask);
    sigdelset(&signal_mask, SIGABRT);

    sigprocmask(SIG_SETMASK, &signal_mask, NULL);

    CleanupHandler* last_handler;

    while(cleanup_handler_list) {
	last_handler = cleanup_handler_list;
	cleanup_handler_list = cleanup_handler_list->next_handler;

	Return_if_fail(last_handler->handler(last_handler->data, signaled));

	delete last_handler;
    }

    if(temp_directory
#ifdef PRCS_DEVEL
       && !option_debug
#endif
	) {
	fs_nuke_file(temp_directory);
	temp_directory = NULL;
    }

#if defined(__BEOS__)
    if (beos_alarm_id > 0) {
        kill_thread(beos_alarm_id);
        beos_alarm_id = 0;
    }
#endif
    return NoError;
}

static void bad_clean_up_handler(SIGNAL_ARG_TYPE)
{
    prcserror << "Internal error, attempting to drop core" << dotendl;

    bug ();

    clean_up(true);

    abort();
}

static void clean_up_handler(SIGNAL_ARG_TYPE)
{
    clean_up(true);

    exit(2);
}

static void alarm_handler(SIGNAL_ARG_TYPE)
{

    CleanupHandler* handler = alarm_handler_list;

    for (; handler; handler = handler->next_handler)
	handler->handler(handler->data, false);
}

#if defined(__BEOS__)
static int32 beos_alarm_thread (void * data)
{
    while (1) {
        snooze( (STALE_LOCK_TIMEOUT - 30) * 1000000 );
        alarm_handler(SIGALRM);
    }
}
#endif

static void chld_handler(SIGNAL_ARG_TYPE)
{
  /* Silly Solaris does not interrupt select() for SIGCHLD when there
   * is no handler!!! */
}

static void handle_signals()
{
    struct sigaction act;
    sigset_t signal_mask;
    int i;

    sigfillset(&signal_mask);
    sigdelset(&signal_mask, SIGABRT);

    act.sa_handler = clean_up_handler;
    act.sa_mask = signal_mask;
#ifdef SA_RESTART
    act.sa_flags = SA_RESTART;
#else
    act.sa_flags = 0;
#endif

    for(i = 0; friendly_signals[i]; i += 1)
	sigaction(friendly_signals[i], &act, NULL);

    act.sa_handler = bad_clean_up_handler;

    for(i = 0; unfriendly_signals[i]; i += 1)
	sigaction(unfriendly_signals[i], &act, NULL);

    sigemptyset(&signal_mask);

    /* SIGCONT */
    act.sa_handler = continue_handler;
    act.sa_mask = signal_mask;
    sigaction(SIGCONT, &act, NULL);

    /* SIGCHLD */
    act.sa_handler = chld_handler;
    act.sa_mask = signal_mask;
    sigaction (SIGCHLD, &act, NULL);

#if !defined(__BEOS__)
    /* SIGALRM */
    act.sa_handler = alarm_handler;
    act.sa_mask = signal_mask;
    sigaction (SIGALRM, &act, NULL);

    struct itimerval itval;

    itval.it_interval.tv_sec = STALE_LOCK_TIMEOUT - 30;
    itval.it_interval.tv_usec = 0;
    itval.it_value.tv_sec = STALE_LOCK_TIMEOUT - 30;
    itval.it_value.tv_usec = 0;

    setitimer (ITIMER_REAL, &itval, NULL);
#endif

}

/* Temp dirs */

static PrVoidError make_temp_dir()
{
    const char* tmpdir;
    Dstring *dir;
    int len, count = getpid();
    struct stat sbuf;

    if((tmpdir = get_environ_var ("TMPDIR")) == NULL)
	tmpdir = "/tmp";

    if(!fs_is_directory(tmpdir) || !fs_file_wrandex(tmpdir))
	pthrow prcserror << "Can't access temp directory "
		        << squote(tmpdir) << perror;

    if(tmpdir[0] != '/')
	pthrow prcserror << "TMPDIR must be an absolute path" << dotendl;

    dir = new Dstring(tmpdir);
    if (dir->index(dir->length() - 1) != '/')
	dir->append ('/');
    dir->append("prcs_temp.");
    len = dir->length();

    while(true) {
	dir->append_int(count);
	if(stat(*dir, &sbuf) < 0)
	    break;
	dir->truncate(len);
	count += 1;
    }

    temp_directory = *dir;

    If_fail(Err_mkdir(temp_directory, (mode_t)0700))
	pthrow prcserror << "Mkdir failed for file " << squote(temp_directory) << perror;

    return NoError;
}

static PrVoidError make_temp_files()
{
    /* all of this memory gets leaked */

    Return_if_fail(make_temp_dir());

    temp_file_1 = make_temp_file("");
    temp_file_2 = make_temp_file("");
    temp_file_3 = make_temp_file("");

    return NoError;
}

static void check_umask()
{
    mode_t user_file_creation_mask = get_umask();

    if(user_file_creation_mask & 0700) {
	prcswarning << "Your umask will prevent PRCS from functioning properly.  "
	    "Proceeding with user RWX permissions" << dotendl;
	umask(user_file_creation_mask & 0077);
    }
}

int main(int argc, char** argv)
{
    PrPrcsExitStatusError exitval(ExitSuccess);

    setup_streams(argc, argv);

    handle_signals();

#if defined(__BEOS__)
    /* We simulate the alarm() with a dedicated thread - this makes
     * executing the alarm signal handler much safer.
     */
    beos_alarm_id = spawn_thread( beos_alarm_thread, "prcs alarm thread"
                                , B_NORMAL_PRIORITY, NULL);
    if (beos_alarm_id < 0) {
        prcserror << "Can't create the alarm thread: "
                  << strerror(beos_alarm_id)
                  << dotendl;
        exit(2);
    }
    status_t beos_thread_state = resume_thread(beos_alarm_id);
    if (B_NO_ERROR != beos_thread_state) {
        prcserror << "Can't start the alarm thread: "
                  << strerror(beos_thread_state)
                  << dotendl;
        exit(2);
    }
#endif

    if(VC_check_token_match(get_login(), "login") <= 0) {
	prcserror << "Your login name, " << squote(get_login()) << ", may confuse the lexer" << dotendl;
	bug();
	exit(2);
    }

    check_umask();

    if (read_command_line(argc, argv) == false) {
	usage(argv);
	exit(2);
    }

    adjust_streams();

    If_fail(make_temp_files())
	exit(2);

    if(!option_force_resolution && !isatty(STDIN_FILENO) ) {
        prcserror << "Please specify -f to run PRCS without a controlling terminal"
		  << dotendl;
	exit(2);
    }

    If_fail(check_system_commands()) {
	exit(2);
    }

    if(subcommand == NULL) {
	exitval = invoke_command(command);
    } else { 
	exitval = invoke_command(subcommand);
    }

    bool clean_failed = false;

    If_fail(clean_up(false))
	clean_failed = true;

    if(Failure(exitval) || clean_failed) {
	if(exitval.error_val() != UserAbort || clean_failed) {
	    prcserror << "Command failed" << dotendl;
	}
	return 2;
    }

    switch(exitval.non_error_val()) {
    case ExitSuccess:
    case ExitNoDiffs:
	return 0;
    case ExitDiffs:
    default:
	return 1;
    }
}
