/*
 * Copyright (c) 2002, The EROS Group, LLC and Johns Hopkins
 * University. All rights reserved.
 * 
 * This software was developed to support the EROS secure operating
 * system project (http://www.eros-os.org). The latest version of
 * the OpenCM software can be found at http://www.opencm.org.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 * 
 * 3. Neither the name of the The EROS Group, LLC nor the name of
 *    Johns Hopkins University, nor the names of its contributors
 *    may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <opencm.h>
#include <client/KeyGen.h>
#include <repos/Repository.h>
#include "opencmclient.h"
#include <signal.h>
#include <opencm-version.h>
#include <opencm-editor.h>

OC_bool do_Upgrade = FALSE;

SSL_CTX *ssl_ctx = NULL;
int opencmport = OPENCM_PORT;  /* unless proven otherwise */
PubKey *this_user = NULL;

/* Identifies repository of interest, especially if we're not
 * initializing a Workspace object */
Repository *this_repos = NULL;

/* Current buffer for exception handling */
catch_t *_curCatch     = NULL;

/* Cached user object for 'log' command */
User *modUser = NULL;
const char *modUserM     = NULL;
const char *modUserEmail = NULL;

/* Cached branch URI for 'log' command */
const char *logBranchURI = NULL;
const char *logBranchName = NULL;

#define TN_PREFIX "opencm://"
#define IsURI(name) (strncmp(name, TN_PREFIX, strlen(TN_PREFIX)) == 0)

Repository *CreateLocalRepos(void);
CommitInfo *CreateNewCommitInfo(const char *helpful_stuff,
				const char *changelist,
				const char *authorURI, 
				const char *branchURI, 
				uint64_t rev);
Group *CreateNewGroup(Repository *r);

void pendingchange_addNote(PendingChange *, const char *);

OC_bool load_config_files(OC_bool);

/* Used to prepend a tilde onto a user namespace path.
 * This makes calling the resolver a little more straightforward. */
static const char *
relative_to_homedir(const char *path)
{
  if (path == NULL)
    return "~";

  if (IsURI(path))
    return path;

  if (*path == '~')
    return path;

  return path_join("~", path);
}

/* Given command line args, convert them to pathnames (or "pathglobs")
   that are relative to the top of the Workspace.  This is needed
   because all client commands are currently executed with respect to
   the Workspace topdir. */
static StrVec *
normalize_args(WorkSpace *ws, int argc, char **argv)
{
  unsigned i;
  StrVec *nmlist = strvec_create();

  if (argc == 0)
    return NULL;

  for (i = 0; i < argc; i++) {
    const char *nm = NULL;

    TRY {
      nm = ws_NormalizePath(ws, argv[i]);
      if (!path_exists(nm))
	log_trace(TRC_WORKSPACE, "Warning:  \"%s\" does not exist.\n", argv[i]);
    }
    CATCH(ExBadValue) {
      log_trace(TRC_WORKSPACE, "Warning:  \"%s\" not in Workspace.\n", argv[i]);
    }
    END_CATCH;

    if (nm == NULL)
      continue;

    strvec_append(nmlist, nm);
  }

  return nmlist;
}

/* Validate user certificate based
 * on whether there is a valid email
 * address, etc. */
static void
client_validatePubKey(PubKey *x) 
{
  const char *email = pubkey_GetEmail(x);
  if (email == NULL) {
    THROW(ExBadValue, "No email address provided in certificate.");
  } else {
    if (strchr(email, '@') == NULL)
      THROW(ExBadValue, "Invalid email address in certificate.");
  }
}

Serializable *
client_GetMutableContent(Repository *r, const char *trueName)
{
  Serializable *s;
  Mutable *m = repos_GetMutable(r, trueName);

  s = repos_GetMutableContent(r, m);

  return s;
}

void
opencm_debugging_help(WorkSpace *ws, int argc, char **argv)
{
  log_help_debugging();
}

#ifdef DEBUG
void
opencm_resolve(WorkSpace *ws, int argc, char **argv)
{
  Repository *r = this_repos;

  while(argc) {
    ObVec *v;
    const char *arg = argv[0];
    unsigned u;

    report(3, "Expanding: %s\n", arg);

    v = resolve(r, arg, 0);
    if (v == NULL)
      THROW(ExBadValue, "No such object");

    for (u = 0; u < vec_size(v); u++) {
      Resolution *res = (Resolution *)vec_fetch(v, u);
      Serializable *s = res->tail ? res->tail : res->s;
      report(0, "tail[%s] s[%s] rest[%s] for %s (and fp_frag = %s)\n",
	     res->tail ? res->tail->ser_type->tyName : "<fsdir>",
             ser_getTrueName(s),
	     res->rest,
	     res->fullPath,
	     res->fp_frag);
    }

    argc--;
    argv++;
  }
}
#endif

static void
client_doReviseMutable(Repository *r, Mutable *m, Serializable *newContent)
{
  Revision *rv;
  Mutable *retMutable;
  const char *tn = ser_getTrueName(newContent);
  
  rv = (Revision *)repos_GetTopRev(r, m);

  if (rv && nmequal(rv->newObject, tn))
    return;

  repos_ReviseEntity(r, m->uri, rv ? rv->newObject : 0, newContent);
  retMutable = repos_ReviseMutable(r, m->uri, m->nRevisions,
				   newContent);

  memcpy(retMutable, m, sizeof(Mutable));
}

static void
client_doBind(Repository *r, const char *path, const char *value)
{
  char *cwd_str;
  Mutable *cwd_m;
  Directory *cwd;
  Resolution *res;
  ObVec *v;

  cwd_str = xstrndup(path, path_tail(path) - path);
  /* Call resolver in case path includes subdir's */
  v = resolve(r, relative_to_homedir(cwd_str), RESOLVER_OPT_SINGLE_RESULT);

  res = vec_fetch(v, 0, Resolution *);

  /* At this point, we should have a (sub)directory in the namespace.  If 
   * there's anything left over in the resolution, then we can't bind to
   * it.  (For example, all subdir's presented must already exist.) */
  if (GETTYPE(res->s) != TY_Directory)
    THROW(ExMalformed, 
	  format("Could not find a directory for %s", path));

  cwd_m = res->m;
  cwd = (Directory *)res->s;

  {
    const char *tail = path_tail(path);

    if (directory_Add(cwd, tail, value) < 0)
      THROW(ExObjectExists, 
	    format("Entry %s already exists in directory %s", 
		   tail, cwd_m->uri));
  }

  client_doReviseMutable(r, cwd_m, (Serializable *)cwd);
}

static void
client_doUnbind(Repository *r, const char *path)
{
  char *cwd_str;
  Mutable *cwd_m;
  Directory *cwd;
  Resolution *res;
  ObVec *v;

  cwd_str = xstrndup(path, path_tail(path) - path);
  /* Call resolver in case path includes subdir's */
  v = resolve(r, relative_to_homedir(cwd_str), RESOLVER_OPT_SINGLE_RESULT);

  res = vec_fetch(v, 0, Resolution *);

  cwd_m = res->m;
  
  xassert(GETTYPE(res->s) == TY_Directory);

  cwd = (Directory *)res->s;

  {
    const char *tail = path_tail(path);

    if (directory_Remove(cwd, tail) < 0)
      THROW(ExNoAccess, format("Error unbinding %s", tail));
  }

  /* make entry in directory */
  client_doReviseMutable(r, cwd_m, (Serializable *)cwd);
}

static Mutable *
client_doCreateProject(Repository *r, const char *synopsis, 
                       const char *desc, const char *bind_name)
{
  Buffer *ci_desc = buffer_create();
  Mutable *chg_m = NULL;
  Change *chg = NULL;
  CommitInfo *ci = NULL;

  /* Manually build the description for the commit info based
   * on the description for the new project */
  buffer_appendString(ci_desc, "Initial, empty configuration for ");
  buffer_appendString(ci_desc, synopsis);
  buffer_appendString(ci_desc, ":\n");
  buffer_appendString(ci_desc, desc);

  chg_m = repos_CreateMutable(r, synopsis, desc, NULL, 0x0);

  ci = commitinfo_create(ci_desc,
			 r->authMutable->uri,
                         chg_m->uri, 0);
  repos_ReviseEntity(r, chg_m->uri, 0, ci);
  chg = change_create(NULL, ci);

  client_doReviseMutable(r, chg_m, (Serializable *)chg);

  /* Bind the resulting branch at the name specified by the user. 
   * The project is internally referenced by the branch, but is not
   * bound. */
  client_doBind(r, bind_name, chg_m->uri);

  return chg_m;
}

static Mutable *
client_doDupBranchMut(Repository *r, Resolution *res, const char *name, 
		      unsigned mutFlags)
{
  Mutable *dupM = NULL;
  unsigned long version = 0;

  /* By default, we want to copy the ACLs when we duplicate the mutable.
   * The --private command line option overrides this and sets the ACLs of
   * dup'ed mutable to the executing user. */
  OC_bool keepACLs = (opt_Private == 0);
 
  switch(GETTYPE(res->tail)) {
  case TY_Version:
    {
      Version *z = (Version *)(res->tail);
      version = z->rev;
      break;
    }
  case TY_Change:
    /* Determine latest branch */
    {
      Revision *rev;
      void *obj;

      obj = repos_GetTopRev(r, res->m);
      if (obj == NULL)
	THROW(ExNoObject, 
	      format("Could not find branch object: %s.", res->m->uri));

      rev = (Revision *)obj;
      version = rev->seq_number;
      break;
    }
  default:
    {
      THROW(ExBadValue, "Can only fork or tag a branch");
      break;
    }
  }

  /* Duplicate the given branch mutable, setting flags/ACLs appropriately */
  dupM = repos_DupMutable(r, name, res->m->uri, keepACLs, version, mutFlags);

  return dupM;
}

/* FIX: This code needs to be killed, and opencm_ls rewritten based on the
   browser code. -JL 7/3/02
*/
void
opencm_ls(WorkSpace *ws, int argc, char **argv)
{
  Resolution *res;
  ObVec *v;
  unsigned u;
  OC_bool needBlankLine = FALSE;
  Repository *r = this_repos;
  const char *path = relative_to_homedir(NULL);

  if (argc > 0)
    path = relative_to_homedir(argv[0]);

  v = resolve(r, path, RESOLVER_OPT_MANY_RESULTS);

  for (u = 0; u < vec_size(v); u++) {
    res = vec_fetch(v, u, Resolution *);
    needBlankLine = FALSE;

    if (res->tail == NULL)
      continue;

    /* Check for any 'container' objects.  For those, call
     * the resolver one more time with an asterisk wildcard
     * appended. */
    switch (GETTYPE(res->tail)) {
    case TY_Directory:
    case TY_FsDir:
    case TY_Version:
    case TY_Change:
    case TY_Group: 
      {
	/* FIX: We resolved a particular version, so do not list the
	   whole branch! */
	unsigned k;
	Resolution *subres;
	ObVec *subv = 0;

	if (vec_size(v) > 1) {
	  report(0, "\n%s:\n", res->fullPath);
	  needBlankLine = TRUE;
	}
	/* FIX: If no access then the following leads to problems.
	 * Suggestion:  First check if client has access to
	 * path_join(res->fullPath, "*").  If not, just skip it. (?)
	 */

        TRY {
          subv = resolve(r, path_join(res->fullPath, "*"), 0);
        }
        CATCH(ExNoObject) {}
        END_CATCH;

        if(!subv)
          continue;

	for (k = 0; k < vec_size(subv); k++) {
	  subres = vec_fetch(subv, k, Resolution *);

	  /* Be careful here, because we might have an empty mutable
	   * (like a virgin branch) */
	  if (subres->tail) {
	    if (opt_LongListing) {
	      report(0, "[%s] %s %s\n", subres->tail->ser_type->tyName, 
		     subres->fp_frag ? subres->fp_frag : subres->fullPath,
		     subres->m->uri);
	    }
	    else {
	      report(0, "[%s] %s\n", subres->tail->ser_type->tyName, 
		     subres->fp_frag ? subres->fp_frag : subres->fullPath);
	    }
	  } else {
	    report(0, "[Empty] %s\n", 
		   subres->fp_frag ? subres->fp_frag : subres->fullPath );
	  }
	}

      }
      break;
      
    default:
      {
	if (opt_LongListing) {
	  report(0, "[%s] %s %s\n", res->tail->ser_type->tyName, 
		  res->fp_frag ? res->fp_frag : res->fullPath,
		  res->m->uri);
	}
	else {
	  report(0, "[%s] %s\n", res->tail->ser_type->tyName, 
		  res->fp_frag ? res->fp_frag : res->fullPath);
	}
      }
      break;

    }
    if (needBlankLine) {
      report(0, "\n");
    }
  }
}

void
opencm_tag(WorkSpace *ws, int argc, char **argv)
{
  Resolution *res;
  ObVec *v;
  Mutable *dupM;
  const char *tag = argv[1];
  Repository *r = this_repos;

  xassert(opt_Name);

  if (!validate_pet_name(tag))
    THROW(ExBadValue, format("Invalid name %s", tag));

  v = resolve(r, relative_to_homedir(argv[0]), 0);
  if (vec_size(v) == 0)
    THROW(ExNoObject, format("Could not resolve %s", argv[0]));
  if (vec_size(v) > 1)
    THROW(ExNoObject, "Can only tag one branch at a time");

  res = vec_fetch(v, 0, Resolution *);

  dupM = client_doDupBranchMut(r, res, opt_Name, (MF_FROZEN | MF_NOTRAIL));
  client_doBind(r, tag, dupM->uri);
}

void
opencm_unbind(WorkSpace *ws, int argc, char **argv)
{
  client_doUnbind(this_repos, argv[0]);
}

void
opencm_bind(WorkSpace *ws, int argc, char **argv)
{
  Resolution *res;
  ObVec *v;
  const char *path = argv[0];
  const char *tname = argv[1];
  Repository *r = this_repos;

  if (!validate_pet_name(path))
    THROW(ExBadValue, format("Invalid name %s", path));

  if (IsURI(tname)) {
    client_doBind(r, path, tname);
  } else {
    v = resolve(r, relative_to_homedir(tname), 0);
    if (vec_size(v) != 1)
      THROW(ExNoObject, "Can only bind to one object at a time");

    res = vec_fetch(v, 0, Resolution *);

    if (res->m == NULL)
      THROW(ExMalformed, format("Corrupt mutable %s", tname));

    /* Check to ensure that user is not trying to bind anything other
     * than an absolute mutable uri.  EG. no versions or wildcards, etc. */
    xassert(res->s);
    xassert(res->tail);
    if (res->rest == NULL && nmequal(ser_getTrueName(res->s), 
				     ser_getTrueName(res->tail)))
      client_doBind(r, path, res->m->uri);
    else
      THROW(ExBadValue, "Can only bind to an absolute mutable.");
  }
}

void
opencm_mkdir(WorkSpace *ws, int argc, char **argv)
{
  Mutable *sub_m;
  Directory *sub;
  const char *path = argv[0];
  Repository *r = this_repos;
  const char *tail = path_tail(path);

  if (!validate_pet_name(path))
    THROW(ExBadValue, format("Invalid name %s", path));

  /* Create a whole new directory (new Mutable) */
  sub = directory_create();
  sub_m = repos_CreateMutable(r, tail, NULL, sub, MF_NOTRAIL);

  client_doBind(r, path, sub_m->uri);
}

void
opencm_set_group(WorkSpace *ws, int argc, char **argv)
{
  ObVec *vM;
  ObVec *vG;
  Resolution *resM;
  Resolution *resG;
  unsigned u;
  unsigned int flag = 0u;
  const char *permission = argv[2];
  Repository *r = this_repos;

  /* Resolve the mutable of interest first */
  vM = resolve(r, argv[0], RESOLVER_OPT_MANY_RESULTS);

  /* Then, resolve the new group/user to which we want to set the ACL */
  vG = resolve(r, argv[1], RESOLVER_OPT_SINGLE_RESULT);

  resG = vec_fetch(vG, 0, Resolution *);

  /* Get the permission bits */
  if (strlen(permission) > 2)
    THROW(ExBadValue,
	  "Must specify one of 'r', 'w', 'wr' or 'rw' for permission");

  if (strchr(permission, 'r')) 
    flag = flag | ACC_READ;

  if (strchr(permission, 'w')) 
    flag = flag | ACC_WRITE;

  /* Cycle through all resolved mutables and set ACL accordingly */
  for (u = 0; u < vec_size(vM); u++) {
    resM = vec_fetch(vM, u, Resolution *);
    resM->m = repos_SetMutableACL(r, resM->m->uri, flag, resG->m->uri);
  }
}

void
opencm_create_group(WorkSpace *ws, int argc, char **argv)
{
  char *description = NULL;
  Mutable *g_m;
  Group *g;
  Repository *r = this_repos;

  xassert(opt_Name);

  if (!validate_pet_name(argv[0]))
    THROW(ExBadValue, format("Invalid nickname %s", argv[0]));

  g = CreateNewGroup(r);

  /* Note that get_message() will take whatever's in opt_Message
   * if it's not NULL.  It opt_Message is NULL, then user gets
   * prompted. */
  description = get_message("Please enter description\n", 
			    xstrcat(COMMENT_LEADER, " New Group:"), 0);

  group_append(g, r->authMutable->uri);
  g_m = repos_CreateMutable(r, opt_Name, description, g, 0x0);
  
  client_doBind(r, argv[0], g_m->uri);
}

void
opencm_set_user(WorkSpace *ws, int argc, char **argv)
{
  Mutable *m;
  User *u;
  ObVec *v;
  Resolution *res;
  unsigned k;
  unsigned int access = 0u;
  const char *permission = argv[1];
  Repository *r = this_repos;

  if (strlen(permission) > 2)
    THROW(ExBadValue, 
	  "Must specify one of 'r', 'w', 'd', 'wr' or 'rw' for permissions");

  if (strchr(permission, 'r')) 
    access |= ACC_READ;

  if (strchr(permission, 'w')) 
    access |= ACC_WRITE;

  if (strchr(permission, 'd')) 
    access |= ACC_REVOKED;

  xassert(access != 0u);

  v = resolve(r, relative_to_homedir(argv[0]), RESOLVER_OPT_MANY_RESULTS);

  for (k = 0; k < vec_size(v); k++) {
    res = vec_fetch(v, k, Resolution *);
    if (GETTYPE(res->s) != TY_User)
      continue;

    u = (User *)res->s;
    m = repos_BindUser(r, u->pubKey, access);
  }
}

void
opencm_set_name(WorkSpace *ws, int argc, char **argv)
{
  ObVec *v = NULL;
  Resolution *res = NULL;
  Mutable *m = NULL;
  Repository *r = this_repos;

  xassert(opt_Name);

  /* FIX: This used to check only that there was at least one result, but only
     use the first one. Is SINGLE_RESULT the right thing to use here? JL 7/3/2002
  */
  v = resolve(r, relative_to_homedir(argv[0]), RESOLVER_OPT_SINGLE_RESULT);

  res = vec_fetch(v, 0, Resolution *);
  m = repos_SetMutableName(r, res->m->uri, opt_Name);
}

void
opencm_set_desc(WorkSpace *ws, int argc, char **argv)
{
  char *description = NULL;
  ObVec *v = NULL;
  Resolution *res = NULL;
  Mutable *m = NULL;
  Repository *r = this_repos;

  /* FIX: This used to check only that there was at least one result, but only
     use the first one. Is SINGLE_RESULT the right thing to use here? JL 7/3/2002
  */
  v = resolve(r, relative_to_homedir(argv[0]), RESOLVER_OPT_SINGLE_RESULT);

  res = vec_fetch(v, 0, Resolution *);

  /* Note that get_message() will take whatever's in opt_Message
   * if it's not NULL.  It opt_Message is NULL, then user gets
   * prompted. */
  description = get_message("Please enter description\n", COMMENT_LEADER, 0);

  m = repos_SetMutableDesc(r, res->m->uri, description);
}

void
opencm_create_user(WorkSpace *ws, int argc, char **argv)
{
  const char *keyFile;
  const char *certFile;
  const char *user_path;
  mode_t old_umask = umask(077);

  char *userFileName = argv[0];

  const char *path = opt_ConfigDir;
  if (!path_exists(path)) {
    log_trace(DBG_USER_CREATE, "Creating %s configuration directory \"%s\"\n",
           CM_APPNAME, path);
    path_mkdir(path);
  }

  user_path = path_join(path, CM_USER_SUBDIR);
  if (!path_exists(user_path)) {
    log_trace(DBG_USER_CREATE, "Creating %s users directory \"%s\"\n", CM_APPNAME, user_path);
    path_mkdir(user_path);
  }

  xassert (path_exists(user_path));

  user_path = path_join(user_path, userFileName);

  /* Invoke OpenSSL to create the key. */
  keyFile = xstrcat(user_path, ".key");
  certFile = xstrcat(user_path, ".pem");

  Generate_X509_Key(keyFile, certFile, TRUE, 1825); /* 1825 == 5 years */

  client_validatePubKey(pubkey_from_PemFile(certFile));

  log_trace(DBG_USER_CREATE,
	     "A key and certificate for \"%s\" have been created.\n",
	     userFileName);

  umask(old_umask);
}

void
opencm_set_userkey(WorkSpace *ws, int argc, char **argv)
{
  Mutable *m;
  PubKey *newpubkey;
  char *cfile = NULL;
  FILE *f = NULL;
  ObVec *v = NULL;
  Resolution *res;
  X509 *x;
  User *u;
  Repository *r = this_repos;

  cfile = argv[1];

  if (!path_exists(cfile))
    THROW(ExNoObject, format("Cannot find cert file %s", cfile));

  TRY {
    f = xfopen(cfile, 'r', 't');

    x = PEM_read_X509(f, NULL, NULL, NULL);
    xfclose(f);
  }
  DEFAULT(ex) {
    xfclose(f);
    RETHROW(ex);
  }
  END_CATCH;

  newpubkey = pubkey_from_X509(x);
  client_validatePubKey(newpubkey);

  /* Now get user */
  v = resolve(r, relative_to_homedir(argv[0]), RESOLVER_OPT_SINGLE_RESULT);
  res = vec_fetch(v, 0, Resolution *);

  /* Verify that it's a user */
  if (GETTYPE(res->s) != TY_User)
    THROW(ExNoObject, 
	  format("The name \"%s\" doesn't resolve to a user.", argv[0]));

  /* Currently, we store user's email address in 'name' field of
     Mutable.  If email address has changed in the new cert, update
     that first. */
  u = (User *)(res->s);
  if (!nmequal(res->m->name, pubkey_GetEmail(newpubkey)))
    res->m = repos_SetMutableName(r, res->m->uri, pubkey_GetEmail(newpubkey));

  /* Now update the user's public key */
  m = repos_RebindUser(r, res->m->uri, newpubkey);
}

void
opencm_add_user(WorkSpace *ws, int argc, char **argv)
{
  PubKey *pk;
  char *cfile = NULL;
  char *permission = NULL;
  FILE *f = NULL;
  unsigned int access = 0u;
  X509 *x;
  Repository *r = this_repos;

  Mutable *user_m;

  cfile = argv[0];
  permission = argv[1];

  if (!path_exists(cfile))
    THROW(ExNoObject, format("Cannot find cert file %s", cfile));

  if (strlen(permission) > 2)
    THROW(ExBadValue, 
	  "Must specify permissions (as 'r', 'w', 'd', or combination)");

  if (strchr(permission, 'r')) 
    access |= ACC_READ;

  if (strchr(permission, 'w')) 
    access |= ACC_WRITE;

  if (strchr(permission, 'd')) 
    access |= ACC_REVOKED;

  xassert(access != 0u);

  TRY {
    f = xfopen(cfile, 'r', 't');

    x = PEM_read_X509(f, NULL, NULL, NULL);
    xfclose(f);
  }
  DEFAULT(ex) {
    xfclose(f);
    RETHROW(ex);
  }
  END_CATCH;

  pk = pubkey_from_X509(x);
  client_validatePubKey(pk);

  /* Check if user already exists.  If not, proceed */
  TRY {
    Mutable *exists __attribute__ ((unused)) = repos_GetUser(r, pk);
    THROW(ExObjectExists, "User already exists.");
  }
  CATCH(ExNoObject) {
    user_m = repos_BindUser(r, pk, access);
  }
  END_CATCH;
}

void
opencm_add_member(WorkSpace *ws, int argc, char **argv)
{
  Group *g;
  Mutable *g_m;
  ObVec *v;
  ObVec *v_member;
  unsigned k, n;
  Resolution *res;
  Resolution *res_member;
  Repository *r = this_repos;

  v = resolve(r, relative_to_homedir(argv[0]), RESOLVER_OPT_MANY_RESULTS);

  v_member = resolve(r, relative_to_homedir(argv[1]), RESOLVER_OPT_MANY_RESULTS);

  for (k = 0; k < vec_size(v); k++) {
    res = vec_fetch(v, k, Resolution *);
    g_m = res->m;
    if (GETTYPE(res->s) != TY_Group)
      THROW(ExBadValue, format("%s is not a Group", res->fullPath));

    g = (Group *)res->s;
    for (n = 0; n < vec_size(v_member); n++) {
      res_member = vec_fetch(v_member, n, Resolution *);
      if ((GETTYPE(res_member->s) != TY_Group) &&
	  (GETTYPE(res_member->s) != TY_User)) {
	log_trace(TRC_BUG, "Skipping bad mutable -- not user or group.\n");
	continue;
      }
      group_append(g, res_member->m->uri);
    }
    client_doReviseMutable(r, g_m, (Serializable *)g); 

  }
}

/* Remove a user or group from a group.  This does not remove
 * nor revoke the user from/on the repository. */
void
opencm_remove_member(WorkSpace *ws, int argc, char **argv)
{
  Group *g;
  Mutable *g_m;
  ObVec *v;
  ObVec *v_member;
  Resolution *res;
  Resolution *res_member;
  unsigned k, n;
  Repository *r = this_repos;

  v = resolve(r, relative_to_homedir(argv[0]), RESOLVER_OPT_MANY_RESULTS);

  v_member = resolve(r, relative_to_homedir(argv[1]), RESOLVER_OPT_MANY_RESULTS);

  for (k = 0; k < vec_size(v); k++) {
    res = vec_fetch(v, k, Resolution *);
    g_m = res->m;
    if (GETTYPE(res->s) != TY_Group)
      THROW(ExBadValue, format("%s is not a Group", res->fullPath));

    g = (Group *)res->s;
    for (n = 0; n < vec_size(v_member); n++) {
      res_member = vec_fetch(v_member, n, Resolution *);
      if ((GETTYPE(res_member->s) != TY_Group) &&
	  (GETTYPE(res_member->s) != TY_User)) {
	log_trace(TRC_BUG, "Skipping bad mutable -- not user or group.\n");
	continue;
      }
      group_remove(g, res_member->m->uri);
    }
    client_doReviseMutable(r, g_m, (Serializable *)g); 

  }
}

void
opencm_create_repository(WorkSpace *ws, int argc, char **argv) 
{
  Repository *r;
  PubKey *adminKey;
  char *path   = argv[0];
  char *fstype = argv[1];
  char *admin  = argv[2];

  if (!path_exists(admin))
    THROW(ExNoObject, format("File %s not found", admin));

  adminKey = pubkey_from_PemFile(admin);

  r = fsrepos_create(path, fstype, adminKey);
} 

void
opencm_create_project(WorkSpace *ws, int argc, char **argv)
{
  char *description = NULL;
  Mutable *m = NULL;
  Repository *r = this_repos;
  
  xassert(opt_Name);
  
  if (!validate_pet_name(path_tail(argv[0])))
    THROW(ExBadValue, format("Invalid name: %s", argv[0]));
  
  /* Note that get_message() will take whatever's in opt_Message if
     it's not NULL.  It opt_Message is NULL, then user gets
     prompted. */
  description = get_message("Please enter description\n", 
                            xstrcat(COMMENT_LEADER, " New Project:"), 0);
  
  m = client_doCreateProject(r, opt_Name, description, argv[0]); 
}

void
opencm_create_branch(WorkSpace *ws, int argc, char **argv)
{
  char *description = NULL;
  Change *orig_chg = NULL;
  Mutable *m;
  ObVec *v;
  Resolution *res;
  Repository *r = this_repos;

  xassert(opt_Name);

  if (!validate_pet_name(argv[1]))
    THROW(ExBadValue, format("Invalid name %s", argv[1]));

  v = resolve(r, relative_to_homedir(argv[0]), RESOLVER_OPT_SINGLE_RESULT);

  res = vec_fetch(v, 0, Resolution *);
  if (GETTYPE(res->tail) == TY_Change) {
    orig_chg = (Change *)res->tail;
  } else if (GETTYPE(res->tail) == TY_Version) {
    Version *ver = (Version *)res->tail;
    orig_chg = (Change *) repos_GetEntity(r, res->m->uri, ver->cfgName);
  } else {
    THROW(ExBadValue, format("%s is not a branch or version", argv[0]));
  }

  /* User has requested creation of a new branch. This is unlike
   * creation of a virgin main branch, because new branches arrive
   * with their first "change" identical to the change that they were
   * derived from.  We do this by duplicating the original branch Mutable.
   */
  description = get_message("Please enter description\n", 
			    xstrcat(COMMENT_LEADER, " New Branch:"), 0);


  m = client_doDupBranchMut(r, res, opt_Name, 0x0);
  m = repos_SetMutableDesc(r, m->uri, description);

  client_doBind(r, argv[1], m->uri);

  /* FIX: If the current workspace pointed to the baseline change, we
   * should offer to update it to point to the branch.
   */
}
  
void
opencm_import(WorkSpace *ws, int argc, char **argv)
{
  char *description = NULL;
  Mutable *chg_m = NULL;
  Repository *r = ws->r;
  
  if (ws->haveProject) {
    report(0, "You already imported.\n");
    return;
  }

  xassert(opt_Name);

  if (!validate_pet_name(path_tail(argv[0])))
    THROW(ExBadValue, format("Invalid name %s", argv[0]));

  /* Note that get_message() will take whatever's in opt_Message
   * if it's not NULL.  It opt_Message is NULL, then user gets
   * prompted. */
  description = get_message("Please enter description\n", 
			    xstrcat(COMMENT_LEADER, " New Project:"), 0);

  chg_m = client_doCreateProject(r, opt_Name, description, argv[0]);

  /* Now create the WorkSpace (with number of revs = 1 since this is
   * the first rev of the new branch)*/
  ws_BuildPendingChange(ws, r, chg_m->uri, 1llu);
  ws_Populate(ws);
  ws_RewriteWorkspace(ws);
  ws_PruneWorkspace(ws);

  report(0, "The project has been created.\n");
  report(0, "You should now run \"%s commit\" to commit this import.\n",
	  path_tail(appInvokedName));
}

#ifdef DEBUG
void
opencm_preds(WorkSpace *ws, int argc, char **argv)
{
  int i;
  Entity *ent;
  Repository *r = ws->r;
  void *v;

  for (i = 0; i < argc; i++) {
    TRY {
      /* This is wrong, but at least it compiles now. Since I don't know what
         this command is for, it's hard to know what should be done here.
           - JL (8/20/2002)
      */
      v = repos_GetEntity(r, 0, argv[i]);
    }
    DEFAULT(AnyException) {
      v = NULL;
    }
    END_CATCH;
    if (v == NULL)
      continue;

    ent = (Entity*)v;
    report(0, "Entity:        %s\n", ser_getTrueName(ent));
    report(0, "  parent:      %s\n",
	    ent->parent ? ent->parent : "<none>");
    report(0, "  mergeParent: %s\n",
	    ent->mergeParent ? ent->mergeParent : "<none>");
  }
}
#endif

/* FIX:  need to review this code */
void
opencm_show_object(WorkSpace *ws, int argc, char **argv)
{
  ObVec *v;
  Resolution *res;
  unsigned u;
  Repository *r = this_repos;
  int i;  

  for (i = 0; i < argc; i++) {
    v = resolve(r, argv[i], 0);

    for (u = 0; u < vec_size(v); u++) {
      res = vec_fetch(v, u, Resolution *);
      sdr_show(res->m);
      switch(GETTYPE(res->tail)) {
      case TY_Change:
      case TY_Group:
      case TY_User:
	{
	  report(0, "\n[%s]\n", res->tail->ser_type->tyName);
	  sdr_show(res->tail);
	  break;
	}
      case TY_Version:
	{
	  Version *v = (Version *) res->tail;
	  Revision *rev = (Revision *) repos_GetEntity(r, res->m->uri, v->cfgName);

	  report(0, "\n[%s]\n", rev->serType->tyName);

	  sdr_show(rev);
	  break;
	}
      default:
	{
	  sdr_show(res->tail);
	  break;
	}
      }
    }
  }
}

void
opencm_show_entity(WorkSpace *ws, int argc, char **argv)
{
  Repository *r = this_repos;
  int i;  

  for (i = 0; i < argc; i++) {
    void *ent = repos_GetEntity(r, r->authMutable->uri, argv[i]);
    sdr_nshow(ent);
  }
}

void
opencm_checkout(WorkSpace *ws, int argc, char **argv)
{
  ObVec *v = NULL;
  Resolution *res = NULL;
  Repository *r = ws->r;

  if (ws->haveProject)
    THROW(ExObjectExists, "Workspace already has a project");
    
  v = resolve(r, relative_to_homedir(argv[0]), RESOLVER_OPT_SINGLE_RESULT);

  res = vec_fetch(v, 0, Resolution *);

  if (GETTYPE(res->tail) == TY_Change ) {

    ws_BuildPendingChange(ws, r, res->m->uri, res->m->nRevisions);

  } else if (GETTYPE(res->tail) == TY_Version) {
    Version *ver = (Version *)res->tail;

    ws_BuildPendingChange(ws, r, res->m->uri, ver->rev+1);
  } else {
    THROW(ExNoObject, "You can only checkout branches or "
	  "branch versions.\n");
  }

  ws_RestoreFiles(ws, WSR_CHECKCOLLIDE);
  pendingchange_RecomputeStatus(ws->pc, 0);
  ws_RewriteWorkspace(ws);
  ws_PruneWorkspace(ws);
  ws_SetModTimes(ws);
}

void
opencm_dump(WorkSpace *ws, int argc, char **argv)
{
  if (!ws || !ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  sdr_show(ws->pc);
}

void
opencm_revert(WorkSpace *ws, int argc, char **argv)
{
  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  pendingchange_RecomputeStatus(ws->pc, 0);
  ws_Revert(ws);
  ws_RewriteWorkspace(ws);
  ws_PruneWorkspace(ws);
  ws_SetModTimes(ws);
  report(0, "Timestamps may have changed. Consider running \"make clean\" or equivalent.\n");
}

static OC_bool
discard_wsentities(WorkSpace *ws, const char *relpath)
{
  /* definitely want to enumerate subdirectories, so do not filter those. */
  if (path_isdir(relpath))
    return FALSE;

  return (ws_Lookup(ws, relpath) ? TRUE : FALSE);
}

static OC_bool __attribute__ ((unused))
     keep_wsentities(WorkSpace *ws, const char *relpath)
{
  /* definitely want to enumerate subdirectories, so do not filter those. */
  if (path_isdir(relpath))
    return FALSE;

  return (ws_Lookup(ws, relpath) ? FALSE : TRUE);
}

static int
check_for_conditionals(WorkSpace *ws)
{
  WsEntity *wse;
  int i;
  int total = 0;

  pendingchange_RecomputeStatus(ws->pc, 0);
  for (i = 0; i < vec_size(ws->pc->entSet); i++) {
    wse = vec_fetch(ws->pc->entSet,i, WsEntity *);
    if (wse->flags & NEF_CONDDEL)
      total++;
  }
  return total;
}

static void
display_log(Repository *r, const CommitInfo *ci)
{
  Mutable *m = NULL;

  /* Get human readable name of current branch (if not cached) */
  if (!nmequal(CMGET(ci,branchURI), logBranchURI)) {
    logBranchURI = xstrdup(CMGET(ci,branchURI));
    m = repos_GetMutable(r, logBranchURI);
    logBranchName = xstrdup(m->name);
  }

  /* Get author info (if not cached) */
  if (!nmequal(CMGET(ci,authorURI), modUserM)) {
    modUserM = xstrdup(CMGET(ci,authorURI));
    modUser = (User *)client_GetMutableContent(r, CMGET(ci,authorURI));
    modUserEmail = pubkey_GetEmail(modUser->pubKey);
  }

  /* Pretty print it */
  report(0, "Branch version: %s/%s (%s)\n", CMGET(ci,branchURI), 
	 xunsigned64_str(CMGET(ci,branchVersion)),
	 logBranchName); 
  report(0, "date: %s \nauthor: %s\n", CMGET(ci,time), modUserEmail);
  report(0, "%s\n", buffer_asString(CMGET(ci,descrip)));
  report(0, "----------------------------\n");

}

static void
do_log_change(WorkSpace *ws, Change *chg, const char *uri_hint, uint64_t limit)
{
  const Change *cur_chg = chg;

  while (cur_chg && limit) {
    const CommitInfo *ci =
      (CommitInfo *)repos_GetEntity(ws->r, uri_hint,
				    CMGET(cur_chg,commitInfoTrueName));
    display_log(ws->r, ci);

    TRY {
      if (CMGET(cur_chg,parent))
        cur_chg = 
	  (Change *)repos_GetEntity(ws->r, uri_hint, CMGET(cur_chg,parent));
      else
	cur_chg = NULL;
    }
    DEFAULT(any) {
      cur_chg = NULL;
    }
    END_CATCH;

    limit--;
  }
}

static void
do_log_entity(WorkSpace *ws, Entity *ent)
{
  unsigned int count = 0;

  if (ent) {
    unsigned j;
    const char *ent_name = NULL;
    Entity *parent = NULL;
    Change *change_parent = NULL;
    OC_bool found = FALSE;
 
    {
      const CommitInfo *ci = NULL;

      report(0, "========================================\n"
	     "Workspace file: %s\n\n", ent->fsName);

      /* First, display commit info message from this latest entity. */
      ci = (CommitInfo *)repos_GetEntity(ws->r, ws->pc->branchURI, 
					 ent->commitInfoTrueName);
      display_log(ws->r, ci);

      /* Make sure we have a chain to walk: */
      if (ent->parent == NULL)
	return;

      /* Now, walk the chain of parent Entity objects and display each
         commit info message as we walk.  Realize that the actual Entity
         objects are embedded in the Change objects.  Thus, we need to extract
         each Change object as we walk and search through its Entity list to
         find the one we want. */
      ent_name = ent->parent;
      change_parent = (Change *)repos_GetEntity(ws->r, ws->pc->branchURI,
						ent->change_parent);

      while (ent_name && change_parent && count < 9) {

	found = FALSE;
	/* Extract actual parent Entity from parent Change object */
	for (j = 0; j < vec_size(CMGET(change_parent,entities)); j++) {
	  parent = vec_fetch(CMGET(change_parent,entities), j, Entity *);
	  if (nmequal(ser_getTrueName(parent), ent_name)) {
	    found = TRUE;
	    break;
	  } 
	}
	if (!found)
	  break;

	/* Now we have the parent Entity and can display its ci message */
	ci = (CommitInfo *)repos_GetEntity(ws->r, ws->pc->branchURI, 
					   parent->commitInfoTrueName);
	display_log(ws->r, ci);

	/* Now advance the pointers in the chain: */
	if (parent->change_parent == NULL)  /* end of the trail */
	  break;

	ent_name = parent->parent;
	TRY {
	  change_parent = (Change *)repos_GetEntity(ws->r, ws->pc->branchURI,
						    parent->change_parent);
	}
	DEFAULT(any) {
	  change_parent = NULL;
	}
	END_CATCH;

	if (!opt_LongListing)
	  count++;

      }
    }
  }
}

static void
do_remove(WorkSpace *ws, StrVec *argnames)
{
  unsigned u;

  assert(argnames);

  argnames = ws_EnumerateWsEnts(ws, argnames);

  if (vec_size(argnames) == 0)
    report(0, "Nothing to remove.\n");
  else {
    strvec_sort(argnames);

    for (u = 0; u < vec_size(argnames); u++) {
      ws_RemoveFile(ws, vec_fetch(argnames, u, const char *));
    }
  }
}

/* Returns number of unknown files. */
static int
do_status(WorkSpace *ws, StrVec *argnames)
{
  int i;
  StrVec *unknown = strvec_create();

  /* Build a list of known WsEntity names from 'argnames'.  Use this
     list for the pendingchange_XXX calls. */
  StrVec *names = ws_EnumerateWsEnts(ws, argnames);

  pendingchange_RecomputeStatus(ws->pc, names);
  pendingchange_ReportStatus(ws->pc, names);

  /* Now make a separate pass to report unknown files: */
  if (argnames) {
    unsigned u;

    for (u = 0; u < vec_size(argnames); u++) 
      /* Ignore SYMLINKS and no need to NORMALIZE again: */
      ws_EnumeratePath(ws, unknown, vec_fetch(argnames, u, const char *), 
		       WSE_FILES|WSE_FILTER, discard_wsentities);
  }
  else
    /* Ignore SYMLINKS and no need to NORMALIZE the ws->topDir */
    ws_EnumeratePath(ws, unknown, path_curdir(), 
                     WSE_FILES|WSE_FILTER, 
		      discard_wsentities);

  for (i = 0; i < vec_size(unknown); i++)
    report(0, "? %s\n", vec_fetch(unknown, i, const char *));

  return vec_size(unknown);
}

void
opencm_log(WorkSpace *ws, int argc, char **argv)
{
  ObVec *v = NULL;
  Resolution *res;
  Change *chg;
  uint64_t limit = ~0llu;

  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  if (argc == 2)
    limit = strtoul(argv[1], 0, 0);
    
  if (argc > 2)
    report(1, "Skipping extra command line arguments...\n");

  if (argc == 0) {
    chg = (Change *)client_GetMutableContent(ws->r, ws->pc->branchURI);
    do_log_change(ws, chg, ws->pc->branchURI, limit);
  } else {

    const char *tmppath = NULL;
    const char *resolve_me = NULL;
    const char *uri_prefix = path_join(ws->pc->branchURI, 
				       xunsigned64_str(ws->pc->nRevisions - 1));

    tmppath = ws_NormalizePath(ws, argv[0]);

    /* Resolver doesn't like leading './' hints */
    if (path_isprefix(path_curdir(), tmppath))
      tmppath = path_cdr(tmppath);

    resolve_me = path_join(uri_prefix, tmppath);

    /* Now call the Resolver.  If first attempt fails, try to 
     * resolve the actual raw argv.  If that fails, give up. */
    TRY {
      v = resolve(ws->r, resolve_me, RESOLVER_OPT_SINGLE_RESULT);
    }
    CATCH(ExNoObject) {
      TRY {
	v = resolve(ws->r, argv[0], RESOLVER_OPT_SINGLE_RESULT);
      }
      CATCH(ExNoObject) {
	TRY {
	  /* Workspace object might have been renamed (before commit): */
	  WsEntity *wsent = ws_Lookup(ws, path_join(path_curdir(), tmppath));
	  if (wsent) {
	    /* Resolver doesn't like leading './' hints */
	    if (path_isprefix(path_curdir(), wsent->old->fsName))
	      tmppath = path_cdr(wsent->old->fsName);

	    resolve_me = path_join(uri_prefix, tmppath);
	    v = resolve(ws->r, resolve_me, RESOLVER_OPT_SINGLE_RESULT);
	  } else
	    THROW(ExNoObject, "I give up.");
	}
	CATCH(ExNoObject) {
	  report(1, "Couldn't resolve as a loggable object: \"%s\".\n", argv[0]);
	  v = NULL;
	}
	END_CATCH;
      }
      END_CATCH;
    }
    END_CATCH;

    if (v) {
      res = vec_fetch(v, 0, Resolution *);
      switch(GETTYPE(res->tail)) {
      case TY_Change: 
	{
	  do_log_change(ws, (Change *)res->tail, res->m->uri, limit);
	  break;
	}
      case TY_Version:
	{
	  Version *ver = (Version *) res->tail;
	  Change *ch = (Change *)repos_GetEntity(ws->r, res->m->uri, 
						 ver->cfgName);
	  do_log_change(ws, ch, res->m->uri, 1);
	  break;
	}
      case TY_Entity: 
	{
	  do_log_entity(ws, (Entity *)res->tail);
	  break;
	}
      default:
	break;
      }
    }  
  }
}

void
opencm_merge(WorkSpace *ws, int argc, char **argv)
{
#define workSpaceChangeName  ws->pc->branchURI

  void *obj;

  Repository *r = ws->r;
  Change *workSpaceChange, *otherChange;
  Mutable *workSpaceChange_mut;
  Revision *workSpaceChangeRev;
  ObVec *v;
  Resolution *res;

  xassert(ws->pc);

  v = resolve(r, relative_to_homedir(argv[0]), RESOLVER_OPT_SINGLE_RESULT);
  res = vec_fetch(v, 0, Resolution *);

  switch(GETTYPE(res->tail)) {
  case TY_Version :
    {
      Version *ver = (Version *)(res->tail);
      otherChange = (Change *)repos_GetEntity(ws->r, res->m->uri, 
					      ver->cfgName);
    }
    break;
  case TY_Change :
    {
      otherChange = (Change *)(res->tail);
    }
    break;
  default:
    {
      THROW(ExBadValue, "You can only merge with other branches");
    }
  }

  workSpaceChange = (Change *)client_GetMutableContent(r, workSpaceChangeName);
  workSpaceChange_mut = repos_GetMutable(r, workSpaceChangeName);

  obj = repos_GetTopRev(r, workSpaceChange_mut);

  workSpaceChangeRev = (Revision *)obj;
  if (workSpaceChangeRev == NULL || GETTYPE(workSpaceChangeRev) != TY_Revision)
    THROW(ExMalformed, 
	  format("Couldn't retrieve the current revision for %s", argv[0]));

  report(1, "Merging: %s into your workspace.\n",argv[0]);

  /* Make sure the workspace is up to date with its branch: */
  if (workSpaceChangeRev->seq_number != ws->pc->nRevisions - 1)
    THROW(ExNoConnect,
	  format("Your workspace is not current.  Please run "
	         "\"%s update\" before trying to merge",
	         path_tail(appInvokedName)));

  /* From here on, we're essentially doing the equivalent of the
   * 'update' command:
   *
   * We have three configurations of interest here:
   *
   *   1) PendingChange specified by WorkSpace
   *   2) TopChange of "other branch"
   *   3) Mandatory common ancestor of the two above
   *
   * Extract the three, perform a 'diff3' on each Entity in the
   * Changes and then write the result to the local workspace.
   *
   * User must do a 'commit' in order for the new result to be
   * appended to the workspace's branch.  */

  /* Take into account any local changes not yet committed: */
  pendingchange_RecomputeStatus(ws->pc, 0);

  ws_mergeFrom(ws, r, res->m->uri, otherChange, FROM_MERGE);
  ws_RewriteWorkspace(ws);
  ws_PruneWorkspace(ws);
}

void
opencm_status(WorkSpace *ws, int argc, char **argv)
{
  StrVec *paths = strvec_create();

  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  paths = normalize_args(ws, argc, argv);
  (void) do_status(ws, paths);

  /* Now that status updates things we need to track, we need to
     rewrite the workspace after doing it. (the status, that is) */
  ws_RewriteWorkspace(ws);
}

void
opencm_update(WorkSpace *ws, int argc, char **argv)
{
  const char *branchURI;
  Change *chg;  
  Repository *r = ws->r;
  int i;
  StrVec *names;

  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  branchURI = ws->pc->branchURI;
  
  chg = (Change *)client_GetMutableContent(r, branchURI);
  xassert(chg);

  ws_Update(ws);

  pendingchange_ReportStatus(ws->pc, 0);

  names = strvec_create();
  ws_EnumeratePath(ws, names, path_curdir(), WSE_STANDARD, discard_wsentities);

  for (i = 0; i < vec_size(names); i++)
    report(0, "? %s\n", vec_fetch(names, i, const char *));

  ws_RewriteWorkspace(ws);
  ws_PruneWorkspace(ws);
}

void
opencm_show_notes(WorkSpace *ws, int argc, char **argv)
{
  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  /* suppress internal '%' interpretation! */
  if (ws->pc->notes)
    report(0, "%s", ws->pc->notes);
  else
    report(0, "No notes found.\n");
}

void
opencm_note(WorkSpace *ws, int argc, char **argv)
{
  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  pendingchange_addNote(ws->pc, opt_Message);
  ws_RewriteWorkspace(ws);
}

void
opencm_commit(WorkSpace *ws, int argc, char **argv)
{
  Mutable *brnM = NULL;
  Repository *r = ws->r;
  StrVec *names = NULL;

  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  if (check_for_conditionals(ws) > 0)
    THROW(ExNoConnect, "You must resolve conditional "
	  "deletes first");

  brnM = repos_GetMutable(r, ws->pc->branchURI);

  log_trace(DBG_COMMIT, "Before status");

  if (argc > 0)
    names = normalize_args(ws, argc, argv);

  if (do_status(ws, names) &&
      !opencm_confirm("Unknown files. Are you sure?", 1))
    THROW(ExNoConnect, "Commit aborted at user request");

  if (brnM->nRevisions != ws->pc->nRevisions)
    THROW(ExNoConnect,
	  format("This branch has been updated.  Please run "
	         "\"%s update\" before trying to commit",
		 path_tail(appInvokedName)));

  log_trace(DBG_COMMIT, "Before commit");

  ws_Commit(ws, names);

  log_trace(DBG_COMMIT, "After commit");

  ws_RewriteWorkspace(ws);
  ws_PruneWorkspace(ws);
}

void
opencm_add_file(WorkSpace *ws, int argc, char **argv)
{
  int i;
  StrVec *names = strvec_create();
  
  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  for (i = 0; i < argc; i++) {
    if (!path_exists(ws_NormalizePath(ws, argv[i])))
      THROW(ExNoObject, 
	    format("File/dir \"%s\" does not exist", argv[i]));

    ws_EnumeratePath(ws, names, argv[i], WSE_STANDARD, 0);
  }

  if (vec_size(names) == 0) {
    report(1, "Nothing to add.\n");
    return;
  }

  /* This is trickier than it looks, because argv[x] is always
   * relative to the startup directory, not the root directory.
   */

  for (i = 0; i < vec_size(names); i++) {
    const char *relpath = vec_fetch(names, i, const char *);

    /* FIX: Should run a filter check here and inquire whether the
     * user really means it if the filter says exclude. Should still
     * allow user to add even if the filter says exclude -- just want
     * to ask first!
     */
    ws_AddFile(ws, relpath); 
  }
  ws_RewriteWorkspace(ws);
}

void
opencm_remove_file(WorkSpace *ws, int argc, char **argv)
{
  StrVec *paths;

  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  paths = normalize_args(ws, argc, argv);

  do_remove(ws, paths);

  ws_RewriteWorkspace(ws);
}

void
opencm_wsenumerate(WorkSpace *ws, int argc, char **argv)
{
  unsigned int i;
  
  StrVec *names;
  StrVec *nmlist = nmlist_create(ws->relStartDir, argc, argv);

  names = ws_EnumerateWsEnts(ws, nmlist);

  for (i = 0; i < vec_size(names); i++) {
    const char *nm = vec_fetch(names, i, const char *);
    if (nmlist_matches(nmlist, nm))
      report(0, "%s\n", nm);
  }
}

void
opencm_diff(WorkSpace *ws, int argc, char **argv)
{
  Repository *r = ws->r;
  ObVec *v;
  int i;

  StrVec *names;

  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  names = strvec_create();

  if (argc) {
    for (i = 0; i < argc; i++) {
      if (!path_exists(ws_NormalizePath(ws, argv[i])))
	THROW(ExNoObject, 
	      format("File/dir \"%s\" does not exist", argv[i]));

      ws_EnumeratePath(ws, names, argv[i], WSE_STANDARD, 0);
    }
  }
  else
    ws_EnumeratePath(ws, names, path_curdir(), WSE_STANDARD, keep_wsentities);


  if (opt_Against) {
    Resolution *res;
    Change *chg = 0;
    v = resolve(r, opt_Against, RESOLVER_OPT_SINGLE_RESULT);

    res = vec_fetch(v, 0, Resolution *);

    if (GETTYPE(res->tail) == TY_Change) {
      chg = (Change *) res->tail;
    }
    else if (GETTYPE(res->tail) == TY_Version) {
      Version *ver = (Version *) res->tail;
      chg = (Change *) repos_GetEntity(r, res->m->uri, ver->cfgName);
    }
    else
      THROW(ExBadValue, "You can only diff against configurations");

    ws_diff_against_change(chg, r, res->m->uri, names);
  }
  else
    pendingchange_diff(ws->pc, r, names);
}

void
opencm_rebind(WorkSpace *ws, int argc, char **argv)
{
  const char *cur = argv[0];
  const char *new = argv[1];
  ObVec *v;
  Resolution *res;
  Repository *r = this_repos;

  /* Have to specify a userspace name for both args */
  if (!validate_pet_name(cur))
    THROW(ExBadValue, format("Invalid name %s", cur));

  if (!validate_pet_name(new))
    THROW(ExBadValue, format("Invalid name %s", new));

  v = resolve(r, cur, RESOLVER_OPT_SINGLE_RESULT);

  res = vec_fetch(v, 0, Resolution *);
  client_doBind(r, new, res->m->uri);
  client_doUnbind(r, cur);
}

void
opencm_rename(WorkSpace *ws, int argc, char **argv)
{
  const char *old = argv[0];
  const char *new = argv[1];

  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  ws_Rename(ws, ws_NormalizePath(ws, old), ws_NormalizePath(ws, new));

  ws_RewriteWorkspace(ws);
  ws_PruneWorkspace(ws);
}

/* This command is useful if you tend to use more than one
 * public key and/or you sometimes use an environment variable to specify
 * your public key and other times explicitly specify
 * it via the command line. */
void
opencm_whoami(WorkSpace *ws, int argc, char **argv)
{
  if (load_config_files(FALSE) == FALSE) {
    report(0, "User \"%s\" is unknown.\n", opt_user);
    return;
  }

  report(0, "%s (expires: %s)\n", pubkey_GetEmail(this_user), 
          pubkey_GetExpiration(this_user));
}

void
opencm_version(WorkSpace *ws, int argc, char **argv)
{
  const char *ver;

  fprintf(stdout,
	  "Client: %s version %s\n", appName, VERSION_STRING);
  if (this_repos) {
    TRY {
      ver = repos_GetVersion(this_repos);
      fprintf(stdout, "Repository: %s\n", ver);

    } DEFAULT(AnyException) {

      fprintf(stdout, "Repository version unknown (no repository specified)\n");
    }
    END_CATCH;
  }
}

#ifdef REPLICATION
static void
deepcopy_entities(Repository *fromR, Repository *toR, Branch *b) 
{
  int u,v;
  Change *change;
  void *commitInfo;
  EntitySet *entitySet;
  Entity *entity;
  void *bits;
  void *obj;

  xassert(fromR);
  xassert(toR);
  xassert(b);

  /* Now, we need to copy over all the Changes, CommitInfoRecords,
     EntitySets, and Entities: */
  for (u = 0; u < vec_size(b->changes); u++) {
    repos_GetEntity(fromR, vec_fetch(b->changes, u), &obj);
    change = (Change *)obj;
    repos_PutEntity(toR, change);
    
    repos_GetEntity(fromR, change->commitInfoTrueName, &commitInfo);
    repos_PutEntity(toR, commitInfo);

    repos_GetEntity(fromR, change->entSetTrueName, &obj);
    entitySet = (EntitySet *)obj;
    repos_PutEntity(toR, entitySet);
    for (v = 0; v < vec_size(entitySet->entNames); v++) {
      const char *nm = vec_fetch(entitySet->entNames, v);

      repos_GetEntity(fromR, nm, &obj);
      entity = (Entity *)obj;
			       
      repos_PutEntity(toR, entity);
      repos_GetEntity(fromR, ((Entity *)entity)->contentTrueName, &bits);
      repos_PutEntity(toR, bits);
    }
  }
} 
#endif

#ifdef REPLICATION
void
opencm_clone_branch(WorkSpace *ws, int argc, char **argv)
{
  Project *p;
  Branch *b;
  Repository *toR;
  char *petName;
  char *branch;
  char *proj;
  int u;

  petName = argv[0];
  branch  = strchr(argv[0], ':');

  if (branch == 0)
    THROW(ExBadValue, "Command requires branch name");
  
  proj = xstrndup(argv[0], branch - argv[0]);
  branch++;

  p = repos_PetGetProject(ws->r, proj);
  b = repos_PetGetBranch(ws->r, p, branch);

  toR = repository_open(argv[1],0);
  xassert(toR);

  /* Create all the necessary directories, etc.:*/
  repos_PutBranch(toR, p, b);

  /* This must be done here, because the two repositories are most likely
   * different:
   */
  deepcopy_entities(ws->r, toR, b); 
}
#endif

void 
opencm_xdcs_insert(WorkSpace *ws, int argc, char **argv)
{
  if (!path_exists(argv[2]))
    THROW(ExNoObject, format("No such file: \"%s\"", argv[2]));

  /* Need the try block to unwind-protect the path_mktmpnm() call */
  TRY {
    const char *tmpFile = 
      path_mktmpnm(path_dirname(argv[0]), "opencm-");

    SDR_stream *in = 
      path_exists(argv[0]) 
      ? stream_fromfile(argv[0], SDR_XDCS)
      : stream_fromfile("/dev/null", SDR_XDCS);

    SDR_stream *out = stream_createfile(tmpFile, SDR_XDCS);
    SDR_stream *theFile = stream_fromfile(argv[2], SDR_RAW);

    xdcs_insert(in, theFile, argv[1], out);

    stream_close(in);
    stream_close(theFile);
    stream_close(out);

    /* Only rename tmpFile if the insert command above completed.  For
    example, if one is trying to insert a name that already exists,
    xdcs_insert() will silently return and tmpFile will be empty. */
    if (path_file_length(tmpFile) > 0) 
      path_rename(tmpFile, argv[0]);
  }
  END_CATCH;
}

void 
opencm_xdcs_extract(WorkSpace *ws, int argc, char **argv)
{
  if (!path_exists(argv[0]))
    THROW(ExNoObject, format("No such archive: \"%s\"", argv[0]));

  {
    SDR_stream *archive = 0;
    SDR_stream *outFile = 0;

    TRY {
      XDeltaArchive_t *xda = 0;
      archive = stream_fromfile(argv[0], SDR_XDCS);
      outFile = stream_createfile(argv[2], SDR_RAW);

      xda = xda_fromStream(archive);
      xdcs_extract(xda, argv[1], outFile);
    }
    DEFAULT(ex) {
      if (archive) stream_close(archive);
      if (outFile) stream_close(outFile);
    }
    END_CATCH;

    stream_close(archive);
    stream_close(outFile);
  }
}

static void xdcs_ls_helper(XDeltaArchive_t *xda, xdirent_t * xde, void *aux)
{
  if (opt_LongListing)
    report(0, "%s %s\n", xde->name, (char *) aux);
  else
    report(0, "%s\n", xde->name);
}

void 
opencm_xdcs_ls(WorkSpace *ws, int argc, char **argv)
{
  int i;
  XDeltaArchive_t *xda;

  for (i = 0; i < argc; i++) {
    SDR_stream *s;

    if (!path_exists(argv[i]))
      report(0, format("No such archive: \"%s\"", argv[i]));

    s =  stream_fromfile(argv[i], SDR_XDCS);
    xda = xda_fromStream(s);
    xdcs_iterate(xda, xdcs_ls_helper, argv[i]);
    stream_close(s);
  }
}

void 
opencm_logmail(WorkSpace *ws, int argc, char **argv)
{
  ObVec *v = NULL;
  Repository *r = this_repos;
  Resolution *res = NULL;
  FILE *f = NULL;

  /* Outer try to unwind-protect the path-mktmpnm() call */
  TRY {
    const char *tmppath = path_mktmpnm(path_scratchdir(), "opencm-");

    SubProcess *logMail;

    TRY {
      f = xfopen(tmppath, 'w', 't');
    }
    DEFAULT(ex) {
      xfclose(f);
      f = NULL;
      RETHROW(ex);
    } END_CATCH;

    logMail = subprocess_create();
    subprocess_AddArg(logMail, "Mail");
    subprocess_AddArg(logMail, "-s");
    subprocess_AddArg(logMail, "OpenCM change notification");
    subprocess_AddArg(logMail, opt_Notify);

    /* We are now set up to do a subprocess. argv[0] is the newly
       introduced change object. What we need to do is fetch that object
       and then fetch the associated commitInfo record so we can email
       it out. */
    v = resolve(r, argv[0], RESOLVER_OPT_SINGLE_RESULT);

    res = vec_fetch(v, 0, Resolution *);

    /* Okay. If we got a kosher argument it should have resolved as a
       TY_Version object. */
    switch(GETTYPE(res->tail)) {
    case TY_Version: 
      {
        Version *ver = (Version *) res->tail;
        Change *chg = 
          (Change *)repos_GetEntity(r, res->m->uri, ver->cfgName);
        CommitInfo *ci =
          (CommitInfo *)repos_GetEntity(r, res->m->uri,
                                        CMGET(chg,commitInfoTrueName));
        User *u = (User *) client_GetMutableContent(r, CMGET(ci,authorURI));

        fprintf(f, "Project: %s\n", res->m->name);
        fprintf(f, "New version: %s/%s\n", CMGET(ci,branchURI), 
                xunsigned64_str(CMGET(ci,branchVersion)));

        fprintf(f, "Author: %s\n", pubkey_GetEmail(u->pubKey));
        fprintf(f, "Time: %s\n", CMGET(ci,time));
        fprintf(f, "Truename: %s\n", ver->cfgName);
        fprintf(f, "Description:\n");

        {
          ocmoff_t pos = 0;
          ocmoff_t end = buffer_length(CMGET(ci,descrip));

          while (pos < end) {
            BufferChunk bc = buffer_getChunk(CMGET(ci,descrip), pos, end - pos);
            fwrite(bc.ptr, 1, (size_t) bc.len, f);
            pos += bc.len;
          }
        }

        break;
      }
    default:
      THROW(ExBadValue, "URI references non-loggable object type");
    }

    xfclose(f);

    /* Ignore the error result, if any. If this fails, we cannot exactly
       log the problem, now can we? */
    subprocess_Run(logMail, tmppath, 0, 0, SPF_NORMAL);
  } END_CATCH;
}

#ifdef DEBUG
void 
opencm_vcmp(WorkSpace *ws, int argc, char **argv)
{
  int result = compare_rpm_versions(argv[0], argv[1]);

  report(0, "%s %s %s\n", 
	  argv[0],
	  (result < 0) ? "<" : ((result > 0) ? ">" : "=="),
	  argv[1]);
}
#endif

void 
opencm_set_workspace_repos(WorkSpace *ws, int argc, char **argv)
{
  URI *uri = uri_create(argv[0]);

  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  if (!uri->well_formed) {
    report(0, "Malformed URI.\n");
    return;
  }

#if 0
  /* Try to connect to the specified repository. */
  {
    Repository *new_repos;

    load_config_files(TRUE);

    new_repos = repository_open(argv[0], this_user);

    repos_Connect(new_repos, this_user);
    repos_Disconnect(new_repos);
  }
#endif

  ws->pc->reposURI = argv[0];

  ws_RewriteWorkspace(ws);
}

void 
opencm_show_workspace_mutable(WorkSpace *ws, int argc, char **argv)
{
  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  report(0, "%s\n", ws->pc->branchURI);
}

void 
opencm_show_workspace_repos(WorkSpace *ws, int argc, char **argv)
{
  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  report(0, "%s\n", ws->pc->reposURI);
}

void 
opencm_set_workspace_user(WorkSpace *ws, int argc, char **argv)
{
  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  ws->pc->userName = argv[0];

  ws_RewriteWorkspace(ws);
}

void 
opencm_show_workspace_user(WorkSpace *ws, int argc, char **argv)
{
  if (!ws->haveProject) {
    report(0, "No project found.\n");
    return;
  }

  if (ws->pc->userName)
    report(0, "%s\n", ws->pc->userName);
  else
    report(0, "No workspace user is recorded\n");
}

void
opencm_ndiff(WorkSpace *ws, int argc, char **argv)
{
  ObVec *lvec;
  Buffer *buf1 = buffer_FromFile(argv[0], 'T');
  Buffer *buf2 = buffer_FromFile(argv[1], 'T');

#if 0
  lvec = diff_extract_lines(buf);
  diff_dump_lines(lvec);
#endif

  lvec = diff2(buf1, buf2, 0);
}

#ifdef REPLICATION
void
opencm_clone_project(WorkSpace *ws, int argc, char **argv)
{
  Project *p, *saveP;
  Branch *b;
  Repository *toR;
  char *petName;
  char *proj;
  StrVec *branches;
  int u;

  petName = argv[0];

  p = repos_PetGetProject(ws->r, proj);
  saveP = repos_PetGetProject(ws->r, proj);

  toR = repository_open(argv[1],0);
  xassert(toR);

  /* Make sure the necessary directories exist or are created: */
  repos_PutProject(toR, p);

  /* Now copy every branch over to new project: */
  branches = repos_ListBranches(ws->r, saveP);
  for (u = 0; u < vec_size(branches); u++) {
    b = repos_GetBranch(ws->r, saveP, vec_fetch(branches, u));
    repos_PutBranch(toR, p, b);
    deepcopy_entities(ws->r, toR, b); 
  } 
} 
#endif

void
opencm_enum_entities(WorkSpace *ws, int argc, char **argv)
{
  Repository *r = this_repos;
  unsigned u;
  TnVec *tv = repos_Enumerate(r, 0, RENUM_ENTITIES, 0);

  for (u = 0; u < vec_size(tv); u++) {
    const char *tn = vec_fetch(tv, u, const char *);
    report(0, "%s\n", tn);
  }
}

void
opencm_enum_mutables(WorkSpace *ws, int argc, char **argv)
{
  Repository *r = this_repos;
  unsigned u;
  const char *host = (argc == 1) ? argv[0] : 0;

  TnVec *tv = repos_Enumerate(r, host, RENUM_MUTABLES, 0);

  for (u = 0; u < vec_size(tv); u++) {
    const char *tn = vec_fetch(tv, u, const char *);
    report(0, "%s\n", tn);
  }
}

void
opencm_enum_users(WorkSpace *ws, int argc, char **argv)
{
  Repository *r = this_repos;
  unsigned u;
  const char *host = (argc == 1) ? argv[0] : 0;

  TnVec *tv = repos_Enumerate(r, host, RENUM_USERS, 0);

  for (u = 0; u < vec_size(tv); u++) {
    const char *tn = vec_fetch(tv, u, const char *);
    report(0, "%s\n", tn);
  }
}

#if 0
static void 
markfn(Repository *r, const char *tn, void *state)
{
  MarkState *ms = state;

  rbkey lookupKey;

  if (tn == 0)
    return;

  lookupKey.vp = tn;
  lookupKey.w = 0;

  if (rbtree_find(ms->visited, &lookupKey) != TREE_NIL)
    return;

  rbtree_insert(ms->visited, rbnode_create(tn, 0, 0));

  if (strncmp(tn, "opencm://", strlen("opencm://")) == 0) {
    /* It's a mutable */
    Mutable *m = repos_GetMutable(r, tn);
    unsigned long long revno;

    report(0, "[%06d] %s has %s revisions\n", ms->depth, tn, 
	    xunsigned64_str(m->nRevisions));

#if 1
    for (revno = 0; revno < m->nRevisions; revno++) {
      Revision *rev = repos_GetRevision(r, tn, revno);
      report(0, "[%06d] revision %s \n", ms->depth+1, xunsigned64_str(revno));
      ms->depth += 2;
      ser_mark(r, rev, markfn, state);
      ms->depth -= 2;
    }
#else
    revno = m->nRevisions -1;
    for(;;) {
      Revision *rev = repos_GetRevision(r, tn, revno);
      report(0, "[%06ds]revision %s \n", ms->depth+1, xunsigned64_str(revno));
      ms->depth += 2;
      ser_mark(r, rev, markfn, state);
      ms->depth -= 2;

      if (revno == 0)
	break;

      revno--;
    }
#endif
  }
  else if (strncmp(tn, "sha1_", strlen("sha1_")) == 0) {
    /* It's an entity */
    Serializable *ent = repos_GetEntity(r, r->uri->URI, tn);
    const char *tyName = ent->ser_type->tyName;
    uint32_t tyVer = ent->ser_type->ver;

    report(0, "[%06d] Entity %s (type %s[%d])\n", ms->depth, tn, tyName, tyVer);
    ms->depth += 1;
    ser_mark(r, ent, markfn, state);
    ms->depth -= 1;
  }
  else
    THROW(ExBadValue, format("Unrecognized truename %s", tn));
}
#endif

void
mark_addmark(const void *container, struct rbtree *mo, const char *tn)
{
  rbkey lookupKey;

  if (tn == 0)
    return;

  lookupKey.vp = tn;
  lookupKey.w = 0;

  if (rbtree_find(mo, &lookupKey) != TREE_NIL)
    return;

#if 1
  if (GETTYPE(container) == TY_Mutable) {
    report(2, "Add M %s -> %s\n", ((const Mutable *)container)->uri, tn);
  }
  else if (GETTYPE(container) == TY_Revision) {
    report(2, "Add R %s/%s -> %s\n", 
	    ((const Revision *)container)->mutURI, 
	    xunsigned64_str( ((const Revision *)container)->seq_number ), 
	    tn);
  }
  else {
    static const void *last_container = 0;
    static const char *last_truename = 0;

    if (container != last_container) {
      last_truename = ser_getTrueName(container);
      last_container = container;
    }
    report(2, "Add E %s -> %s\n", last_truename, tn);
  }
#endif

  rbtree_insert(mo, rbnode_create(tn, 0, 0));
}

static int 
mark_sw_cmp(const rbnode *rn1, const rbnode *rn2)
{
  int delta = strcmp(rn1->value.vp , rn2->value.vp);

  if (delta != 0)
    return delta;

  if (rn1->value.w < rn2->value.w)
    return -1;
  else if (rn1->value.w > rn2->value.w)
    return 1;

  return 0;
}

static int 
mark_sw_cmpkey(const rbnode *rn1, const rbkey *rkey)
{
  int delta = strcmp(rn1->value.vp , rkey->vp);

  if (delta != 0)
    return delta;

  if (rn1->value.w < rkey->w)
    return -1;
  else if (rn1->value.w > rkey->w)
    return 1;

  return 0;
}

static void
mark_add_count(rbtree *mark_counts, Serializable *s)
{
  rbnode *rbn;
  rbkey lookupKey;

  lookupKey.vp = ser_find_type(GETTYPE(s));
  lookupKey.w = s->ser_ver;

  rbn = rbtree_find(mark_counts, &lookupKey);

  if (rbn == TREE_NIL) {
    unsigned *data = GC_MALLOC_ATOMIC(sizeof(unsigned));
    *data = 0;

    rbn = rbnode_create(lookupKey.vp, lookupKey.w, data);
    rbtree_insert(mark_counts, rbn);
  }

  *((unsigned *) rbn->data) += 1;
}

void 
opencm_mark(WorkSpace *ws, int argc, char **argv)
{
  unsigned long long count = 0;

  Repository* r = this_repos; 

  rbtree *visited = rbtree_create(rbtree_s_cmp, rbtree_s_cmpkey, FALSE);
  rbtree *marks = rbtree_create(rbtree_s_cmp, rbtree_s_cmpkey, FALSE);
  rbtree *mark_counts = rbtree_create(mark_sw_cmp, mark_sw_cmpkey, FALSE);

  StrVec *blackList = strvec_create();

  if (argc == 0) {
    unsigned u;
    StrVec *muts = repos_Enumerate(r, 0, RENUM_USERS, 0);

    for (u = 0; u < vec_size(muts); u++) {
      rbkey lookupKey;

      lookupKey.vp = vec_fetch(muts, u, const void *);
      lookupKey.w = 0;

      if (rbtree_find(marks, &lookupKey) == TREE_NIL)
	rbtree_insert(marks, rbnode_create(lookupKey.vp, 0, 0));
    }
  }

  while (argc) {
    ObVec *v;
    const char *arg = argv[0];
    unsigned u;

    log_trace(DBG_RESOLVER, "Expanding: %s\n", arg);

    v = resolve(r, arg, RESOLVER_OPT_MANY_RESULTS);
    if (v == NULL)
      THROW(ExBadValue, "No such object");

    for (u = 0; u < vec_size(v); u++) {
      Resolution *res = vec_fetch(v, u, Resolution *);
      rbkey lookupKey;

      lookupKey.vp = res->m->uri;
      lookupKey.w = 0;

      if (rbtree_find(marks, &lookupKey) == TREE_NIL)
	rbtree_insert(marks, rbnode_create(res->m->uri, 0, 0));
    }

    argc--;
    argv++;
  }

  while (!rbtree_isEmpty(marks)) {
    rbnode *nd;
    rbtree *newMarks = rbtree_create(rbtree_s_cmp, rbtree_s_cmpkey, FALSE);

    while ((nd = rbtree_root(marks)) != TREE_NIL) {
      rbkey lookupKey;
      const char *tn = nd->value.vp;

      assert (tn != 0);

      rbtree_remove(marks, nd);

      lookupKey.vp = tn;
      lookupKey.w = 0;

      if (rbtree_find(visited, &lookupKey) != TREE_NIL)
	continue;

      report(1, "Mark %s %s\n", xunsigned64_str(count), tn);
      count++;

      rbtree_insert(visited, rbnode_create(tn, 0, 0));

      if (strncmp(tn, "opencm://", strlen("opencm://")) == 0) {
	/* It's a mutable */
	Mutable *m = 0;
	unsigned long long revno;

	TRY {
	  m = repos_GetMutable(r, tn);
	}
	CATCH(ExNoObject) {
	  report(0, "Blacklisting mutable %s (no object)\n", tn);
	  strvec_append(blackList, tn);
	}
	CATCH(ExNoAccess) {
	  report(0, "Blacklisting mutable %s (no access)\n", tn);
	  strvec_append(blackList, tn);
	}
	END_CATCH;

	if (m) {
	  ser_mark(r, m, m, newMarks);
	  mark_add_count(mark_counts, (Serializable *) m);

	  report(1, "Mark mutable %s (%s revisions)\n", tn,
                 xunsigned64_str(m->nRevisions));

	  for (revno = 0; revno < m->nRevisions; revno++) {
	    Revision *rev = 0;

	    TRY {
	      rev = repos_GetRevision(r, tn, revno);
	    } DEFAULT(ex) {
	      /* Suppress */
	    } END_CATCH;

	    if (rev) {
	      report(1, "revision %s/%s \n", tn, xunsigned64_str(revno));
	      ser_mark(r, rev, rev, newMarks);
	      mark_add_count(mark_counts, (Serializable *) rev);
	    }
	  }
	}
      }
      else if (strncmp(tn, "sha1_", strlen("sha1_")) == 0) {
	/* It's an entity */
	Serializable *ent = 0;

	TRY {
	  ent = repos_GetEntity(r, r->uri->URI, tn);
	} CATCH(ExNoObject) {
	  report(0, "Blacklisting entity %s\n", tn);
	  strvec_append(blackList, tn);
	}
	END_CATCH;

	if (ent) {
	  const char *tyName = ent->ser_type->tyName;
	  uint32_t tyVer = ent->ser_type->ver;

	  report(1, "Entity %s (type %s[%d])\n", tn, tyName, tyVer);
	  ser_mark(r, ent, ent, newMarks);

	  mark_add_count(mark_counts, (Serializable *) ent);
	}
      }
      else
	THROW(ExBadValue, format("Unrecognized truename %s", tn));
    }

    marks = newMarks;
  }

  {
    rbnode *rbn = rbtree_min(mark_counts);

    while (rbn != TREE_NIL) {
      unsigned count = *((unsigned *)rbn->data);
      const SerialType *st = rbn->value.vp;

      report(0, "%-8d %s[%d]\n", count, st->tyName, rbn->value.w);
      
      rbn = rbtree_succ(rbn);
    }
  }
}

void 
opencm_upgrade(WorkSpace *ws, int argc, char **argv)
{
   Repository* r = repository_open(RepositoryURI);

   do_Upgrade = TRUE;

   load_config_files(TRUE);

   /* Upgrade is performed as a side effect. */
   repos_Connect(r, this_user);
   repos_Disconnect(r);
}

void 
opencm_gc(WorkSpace *ws, int argc, char **argv)
{
   Repository* r = this_repos;

   TRY {
     repos_GarbageCollect(r, RGC_START);
   }
   DEFAULT(ex) {
     report(0, "An incomplete garbage collection pass has already started\n");
   } END_CATCH;

   /* Some of the GC actions rely on side effects that are performed
      on the server side. Flush all caches to ensure that every
      request propagates at least once. */
   repos_FlushCache(r);

   /* Simply kick off the mark command with no arguments: */
   opencm_mark(ws, 0, 0);

   repos_GarbageCollect(r, RGC_END);
}

typedef struct config_files {
  const char *certfile;
  const char *keyfile;
} config_files;

static config_files *
find_config_files(OC_bool needThem)
{
  config_files *cfg = GC_MALLOC(sizeof(*cfg));

  const char *user_dir = path_join(opt_ConfigDir, CM_USER_SUBDIR);

  /* Following change made to support logging agent. Modify the -u
     option so that it can specify a full path to the key files. There
     is an odd constraint here that I find distasteful, but cannot
     determins a good workaround for. One would *like* the behavior to
     be as follows:

     1. first, try to locate the xxx.pem, xxx.key files by simply
        opening the command-line supplied path.
     2. Failing that, assume that it is a path relative to the user's
        $HOME/.opencm/users/ directory.

     Part (2) is fine, but part (1) can lead to both surprising
     behavior and potential security risks if the content in the the
     workspace includes PEM and KEY files -- in particular, a key
     substitution attack is possible in this way.

     We therefore apply case (1) only if the specified path is an
     absolute path. We do NOT attempt to fall back if the path is
     absolute.
  */

  if (path_isAbsolute(opt_user)) {
    cfg->keyfile    = xstrcat(opt_user, ".key");
    cfg->certfile   = xstrcat(opt_user, ".pem");
  }
  else {
    cfg->keyfile    = path_join(user_dir, xstrcat(opt_user, ".key"));
    cfg->certfile   = path_join(user_dir, xstrcat(opt_user, ".pem"));
  }

  if ( needThem == FALSE && 
       (!path_exists(cfg->keyfile) ||
	!path_exists(cfg->certfile) ||
	0
	))
    return 0;

  /* FIX: Isn't this CMD_ISFLAG test redundant? */
  if (!path_exists(cfg->keyfile))
    THROW(ExNoObject, 
	  format("User \"%s\" key file not found", opt_user));

  if (!path_exists(cfg->certfile))
    THROW(ExNoObject, 
	  format("User \"%s\" cert file not found", opt_user));

  return cfg;
}

OC_bool
load_config_files(OC_bool needThem)
{
  config_files *cfg = find_config_files(needThem);

  if ( cfg ) {
    ssl_ctx = ssl_init_ctx(cfg->keyfile, cfg->certfile);

    this_user = pubkey_from_PemFile(cfg->certfile);

    return TRUE;
  }
  return FALSE;
}

static void
process_environment()
{
  RepositoryURI = getenv(CM_VARPREFIX "_REPOSITORY");

  opt_Editor = getenv(CM_VARPREFIX "_EDITOR");
  if (opt_Editor == 0)
    opt_Editor = getenv("VISUAL");
  if (opt_Editor == 0)
    opt_Editor = getenv("EDITOR");
  if (opt_Editor == 0)
    opt_Editor = CM_EDITOR;

  if (opt_ConfigDir == 0)
    opt_ConfigDir = getenv(CM_VARPREFIX "_CONFIGDIR");

  if (opt_ConfigDir == 0)
    opt_ConfigDir = path_join(os_GetHomeDir(), CM_CONFIG_DIR);

  opt_user = getenv(CM_VARPREFIX "_USER");
  if (opt_user == 0)
    opt_user = CM_DEFAULT_USER;
}

static void* ssl_malloc(size_t howmuch)
{
  return GC_MALLOC(howmuch);
}

static void* ssl_realloc(void* ptr, size_t size)
{
  return GC_REALLOC(ptr, size);
}

static void ssl_free(void* ptr)
{
#if 1
   /* do nothing; let the GC handle it */
#else
  GC_FREE(ptr);
#endif
}

int
main(int argc, char *argv[])
{
  int dummy;
  struct found_command fc;
  WorkSpace *ws = 0;

  /* Set up the initial logging rules immediately */
  log_set_verbosity(0);

  GC_expand_hp (8 * 1024 * 1024);
  /* GC_use_entire_heap = 1; */
  /* GC_enable_incremental(); */

  /*
     OpenSSL has some leaks (and it looks like we hit them pretty hard at
     times). But by having it use the GC instead of the standard libc
     malloc/realloc/free, we can prevent the worst of it.
  */
#if USE_SSL_GC
  CRYPTO_set_mem_functions(ssl_malloc, ssl_realloc, ssl_free);
#endif

  appInvokedName = argv[0];
  appName = CM_APPNAME;

  TRY {
    top_of_stack = &dummy;

    /* Set up the right OpenCM port */
    {
      struct servent *service = os_GetServicePort("opencm", "tcp");
      if (service)
	opencmport = ntohs(service->s_port);
    }

    filterset_init();

    process_environment();

    process_options(argc, argv);

    /* Go ahead and initialize all the SSL stuff, because
     * even for just creating a repos, we need access to 
     * digest and signing algorithms */
    SSL_load_error_strings();
    SSL_library_init();
    initialize_PRNG();

    /* Find the command we will run, so we know how to
     * initialize */
    fc = opencm_find_command(argc - optind, argv + optind);

    /* If we have a valid command and we're not trying to
     * create a repository, go ahead and initialize an SSL
     * context and establish user identity */
    if (fc.cmd) {
      OC_bool haveConfig = FALSE;

      /* Now initialize the WorkSpace, if needed.
       * For CF_ADMIN commands, always bypass the Workspace. */
      if (CMD_ISFLAG(fc.cmd, (CF_WSINIT)) &&
	  ! CMD_ISFLAG(fc.cmd, (CF_ADMIN)))
	ws = ws_Init(fc.cmd, 0);

      this_repos = 0;

      if (! CMD_ISFLAG(fc.cmd, CF_NOCONNECT)) {
	/* We will need the config files unless (a) there is no
	   connection, or (b) the connection is optional. */

	OC_bool needThem = CMD_ISFLAG(fc.cmd, CF_OPTCONNECT) ? FALSE : TRUE;

	haveConfig = load_config_files(needThem);

	if (haveConfig == FALSE && needThem == TRUE)
	  THROW(ExNoObject, "Could not find config directory");
      }

      if (haveConfig) {
	/* Now connect to the repository, if needed */
	if (!CMD_ISFLAG(fc.cmd, (CF_NOCONNECT))) {

	  /* If we have a WorkSpace, use that as the basis for the
	     connection */
	  if (ws) {
	    xassert(CMD_ISFLAG(fc.cmd, (CF_WSINIT)));

	    /* Try connecting.  Catch any errors in case a successful
	     * repos connection is optional for this command */
	    TRY {
	      ws_Connect(ws, this_user);
	      this_repos = ws->r;
	    }
	    DEFAULT(caught) {
	      this_repos = NULL;

	      if ( !CMD_ISFLAG(fc.cmd, CF_OPTCONNECT|CF_NOCONNECT) )
		RETHROW(caught);
	    }
	    END_CATCH;
	  } 
	  else if (RepositoryURI) {
	    /* Need to connect without the Workspace's help */

	    /* Try connecting.  Catch any errors in case a successful
	     * repos connection is optional for this command */
	    TRY {
	      /* Repository was either spec'd on cmd line or env variable */
	      this_repos = repository_open(RepositoryURI);

	      repos_Connect(this_repos, this_user);
	      if (!this_repos->doesAccess)
		this_repos = authrepository_wrap(this_repos, this_user);
	    } 
	    DEFAULT(caught) {
	      this_repos = NULL;

	      if ( !CMD_ISFLAG(fc.cmd, CF_OPTCONNECT|CF_NOCONNECT) )
		RETHROW(caught);
	    }
	    END_CATCH;
	  }
	}
      }

      if (this_repos == 0 && !CMD_ISFLAG(fc.cmd, CF_OPTCONNECT|CF_NOCONNECT))
	THROW(ExNoConnect, "Repository not specified.");

      /* Now execute command */
      fc.cmd->fcn(ws, fc.argc, fc.argv);
    }
  }
  DEFAULT(AnyException) {
    syslog_error("Exception at %s:%d: %s\n", _catch.fname, _catch.line, 
	      _catch.str);
    syslog_error("Exception: %s.\n", EXCEPTION_NAME(AnyException));

    /* Issue a disconnect to release the repository lock file. */
    if (this_repos)
      repos_Disconnect(this_repos);

    do_exit(1);
  }
  END_CATCH;

  if (this_repos)
    repos_Disconnect(this_repos);

  do_exit(0);
}

#ifdef REPLICATION
/* These require user input and thus are only needed on the client side: */
Repository *
CreateLocalRepos(void)
{
  char *lpath = NULL;
  char *uri  = NULL;
  char *keyfile = NULL;
  char *certfile = NULL;
  char *scheme = "file://";
  char *admincert = NULL;

  Repository *r;

  while (lpath == NULL) 
    lpath = opencm_readline("Full local path for Repository", 0);
    
  while (keyfile == NULL) 
    keyfile = opencm_readline("Full local path for private key", 0);
    
  while (certfile == NULL) 
    certfile = opencm_readline("Full local path for certificate", 0);
    
  while (admincert == NULL) 
    admin = opencm_readline("Full local path for admin user's certificate", 0);
    
  if (!path_exists(keyfile))
    report_error(ExNoObject, "Couldn't find file: %s", keyfile);

  if (!path_exists(certfile))
    report_error(ExNoObject, "Couldn't find file: %s", certfile);

  if (!path_exists(admincert))
    report_error(ExNoObject, "Couldn't find file: %s", admincert);

  uri = xstrcat(scheme, lpath);

  r = repository_open(uri, NULL);

  if (!path_exists(lpath)) {
    repos_CreateRepository(r, keyfile, certfile, admincert):
  }
    
  return r;
}
#endif

void
pendingchange_addNote(PendingChange *pc, const char *msg)
{
  /* Okay -- this is a little convoluted. The idea is that the user
   * may give us a "quick append" by specifying a note using the
   * message option. If so, we will APPEND that to the existing notes
   * string. If no message has been passed in, we use the usual
   * message handling logic to let them edit the entire existing body
   * of notes.
     */

  if (msg) {
    /* No interaction: */
    char *s;
    unsigned notes_len = pc->notes ? strlen(pc->notes) : 0;
    unsigned msg_len = strlen(msg);

    /* add 3 for intervening LF, trailing LF,trailing NUL */
    s = GC_MALLOC_ATOMIC(notes_len + msg_len + 3);
    if (pc->notes) {
      strcpy(s, pc->notes);
      s[notes_len++] = '\n';
    }
    strcpy(&s[notes_len], msg);
    s[msg_len + notes_len] = '\n';	/* trailing newline */
    s[msg_len + notes_len + 1] = '\n';	/* null terminate */

    /* Call strip_comments() to ensure that there is at most one
     * trailing newline. It can be otherwise if the user used a quoted
     * argument to the -m option and ended it with several blank
     * lines. */
    strip_comments(s, 0);

    pc->notes = s;
  }
  else {
    pc->notes = get_message("Please enter your note.\n",
			    xstrcat(COMMENT_LEADER, " NOTES:"), pc->notes);
  }

  SER_MODIFIED(pc);
}

CommitInfo *
CreateNewCommitInfo(const char *helpful_stuff, const char *changelist,
		    const char *authorURI, 
		    const char *branchURI, 
		    uint64_t rev)
{
  Buffer *buf = buffer_create();
  char *descrip = NULL;

  if (changelist != 0) {
    descrip = get_message("Please enter a description for this change:\n",
			  xstrcat(COMMENT_LEADER, " Committing..."), 
			  helpful_stuff ? xstrcat(helpful_stuff, changelist) :
			  changelist);
    strip_comments(descrip, COMMENT_LEADER);
  } else {
    descrip = NULL;
  }

  buffer_appendString(buf, descrip);
  return commitinfo_create(buf, authorURI, branchURI, rev);
}

Group *
CreateNewGroup(Repository *r)
{
  Group *g;

  while (opt_Name == 0) {
    opt_Name = opencm_readline("Group name", 0);
  } 

  /* <mutname> */
  g = group_create();

  return g;
}
