/*
 * 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 <repos/Repository.h>
#include "opencmclient.h"

/* Stream format to use when storing workspace object */
#define SDR_WORKSPACE SDR_BINARY
#define CM_WORKSPACE_FSNAME "Workspace"

CommitInfo *CreateNewCommitInfo(const char *helpful_stuff,
				const char *changelist,
				const char *authorURI, 
				const char *branchURI, 
				uint64_t rev);

static void
ws_FindProjectRoot(WorkSpace *ws, struct command *cmd)
{
#ifdef __unix__
  const char *dir = path_curdir();
  const char *parent = path_parent_dir();

  ws->topDir = ws->startDir;
  
  ws->haveProject = FALSE;

  for(;;) {
    const char *opencm_dir = path_join(dir, CM_CONFIG_DIR);
    const char *opencm_ws = path_join(opencm_dir, CM_WORKSPACE_FSNAME);

    if (path_exists(opencm_dir) &&
	path_isdir(opencm_dir) &&
	path_isfile(opencm_ws)) {
      ws->haveProject = TRUE;
      break;
    }
    
    if (CMD_ISFLAG(cmd, CF_NOROOTSEARCH))
      break;

    /* Check for top of filesystem */
    if (path_same_dir(dir, parent))
      break;

    dir = parent;
    parent = path_join(path_parent_dir(), dir);
  }

  if (ws->haveProject) {
    SDR_stream *strm;
    
    chdir(dir);
    ws->topDir = path_current_directory();

    ws->projPath = path_join(ws->topDir, CM_CONFIG_DIR);
    ws->projPath = path_join(ws->projPath, CM_WORKSPACE_FSNAME);

    strm = stream_fromfile(ws->projPath, SDR_WORKSPACE);
    ws->pc = sdr_read("ws_nextChange", strm);
    stream_close(strm);

    /* User name on command line wins, but failing that, user name
       from workspace trumps environment and default. */
    if (opt_HaveUser == 0 && ws->pc->userName)
      opt_user = ws->pc->userName;
  }
  
  /* FIX: check for recovery/cleanup requirement! */
  
  {
    int contains = ! strncmp(ws->startDir, ws->topDir, strlen(ws->topDir));
    
    log_trace(DBG_STARTUP, "Project directory is \"%s\"\n", ws->topDir);
    log_trace(DBG_STARTUP, "Startup directory is \"%s\"\n", ws->startDir);
    log_trace(DBG_STARTUP, "Project contains startup? %c\n", contains ? 'y' : 'n');
  }

  if (strcmp(ws->startDir, ws->topDir) == 0)
    ws->relStartDir = path_curdir();
  else {
    /* topDir should always be shorter than startDir... */
    int topDirLen = strlen(ws->topDir);
    
    if (topDirLen > strlen(ws->startDir))
      THROW(ExMalformed, "topDir is bogus");

    ws->relStartDir = &ws->startDir[topDirLen];
    
    ws->relStartDir = path_canonical(path_join(path_curdir(),
					       ws->relStartDir));
  }

  log_trace(DBG_STARTUP, "Relative start dir is \"%s\"\n", ws->relStartDir);
#else
#  error "WorkSpace::FindAndChdir() not implemented"
#endif
}

const char *
ws_NormalizePath(WorkSpace *ws, const char *path)
{
  const char *wspath = NULL;
  size_t buffer_len;
  size_t advance_ptr;

  xassert(ws);
  xassert(path);

  if (path_isAbsolute(path))
    wspath = path;
  else
    wspath = path_join(ws->startDir, path);

  /* Resolve (and delete) any embedded '.' or '..' */
  wspath = path_canonical(wspath);
  buffer_len = strlen(wspath);

  /* Handle easy case first: */
  if (nmequal(ws->topDir, wspath))
    return path_curdir();

  /* Absolute path should begin with absolute path of user's workspace */
  if (!path_isprefix(ws->topDir, wspath))
    THROW(ExBadValue, format("Doesn't have Workspace path as "
			     "prefix: \"%s\"\n", wspath));

  /* Strip off absolute path of Workspace topdir */
  advance_ptr = strlen(ws->topDir) + 1;

  /* Try to prevent buffer overflow! */
  xassert(advance_ptr < buffer_len);

  wspath = wspath + advance_ptr;

  /* Everything in the Workspace begins with path_curdir() */
  return path_join(path_curdir(), wspath);

}

WorkSpace *
ws_Init(struct command *cmd, const char *rootPath)
{
  /* Strategy:

     First, find and load the workspace's PendingChange structure
     if one exists. For most of the commands we will want this, and
     for the rest it will do no harm. Also, it will find for us the
     "top" of the active project tree.

     If the PendingChange structure exists, ensure that the executing
     user matches the creator of the PendingChange structure.

     Build a Repository object based on the Workspace and/or environment
     variables.
 */

  WorkSpace *ws = GC_MALLOC(sizeof(WorkSpace));

  /* Eventually we will support layered projects, and it will be
     possible for rootPath to be non-null. Until then, just barf if
     rootPath isn't what we are expecting. */
  if (rootPath != 0)
    THROW(ExMalformed, "Subordinate workspaces not (yet) supported");

  ws->startDir = path_current_directory();
  ws_FindProjectRoot(ws, cmd);

  ws->r = 0;

  if (CMD_ISFLAG(cmd, CF_NOCONNECT))
    return ws;

  /* Establish a repository object if we need to. 
   * Here's the logic:
   * 1)  Use of the --repository command line option always takes precedence.
   * 2)  Next is the Workspace repository, if there is a Workspace.
   * 2)  Last chance:  use the environment variable.
   */
  if (RepositoryURI) {
    if (opt_HaveRepository)
      ws->r = repository_open(RepositoryURI);
    else if (ws && ws->pc)
      ws->r = repository_open(ws->pc->reposURI);
    else if (ws == NULL || ws->pc == NULL) 
      ws->r = repository_open(RepositoryURI);
  }
  else if (ws && ws->pc) {
    ws->r = repository_open(ws->pc->reposURI);
  }

#undef WARN_ABOUT_CONFLICTING_REPOS
#ifdef WARN_ABOUT_CONFLICTING_REPOS
  if (ws && RepositoryURI && ws->pc && opt_HaveRepository == 0) {
    if (!nmequal(RepositoryURI, ws->pc->reposURI))
      report(0, "WARNING: The repository specified by your environment:\n"
	     "        %s\n"
	     "    does not match the one in the Workspace:\n"
	     "        %s\n",
	     RepositoryURI, ws->pc->reposURI);
  }
#endif

  if (ws->r == NULL && !CMD_ISFLAG(cmd, CF_OPTCONNECT))
    THROW(ExNoConnect, "Repository not found");

  return ws;
}

void
ws_Connect(WorkSpace *ws, PubKey *pk)
{
  if (ws->r == NULL)
    THROW(ExNoConnect, "No repository specified in Workspace.");

  repos_Connect(ws->r, pk);

  if (!ws->r->doesAccess)
    ws->r = authrepository_wrap(ws->r, pk);

  /* In the client case, a connected repository will need an
     authorized user: */
  if (ws->r->authMutable == 0 || ws->r->authUser == 0)
    THROW(ExNoAuth, "Could not authenticate");

  if (ws->r->authAccess & ACC_REVOKED)
    THROW(ExNoAuth, "Repos access denied");
}

void
ws_RewriteWorkspace(WorkSpace *ws)
{
  SER_MODIFIED(ws->pc);

  if (ws->projPath == 0) {
    const char *scratchPath;
    
    ws->projPath = path_join(ws->topDir, CM_CONFIG_DIR);
    scratchPath = path_join(ws->projPath, "scratch");

    if (!path_exists(ws->projPath))
      path_mkdir(ws->projPath);
    if (!path_isdir(ws->projPath))
      THROW(ExNoObject, 
	    format("%s is not a directory", CM_CONFIG_DIR));

    if (!path_exists(scratchPath))
      path_mkdir(scratchPath);
    if (!path_isdir(scratchPath))
      THROW(ExNoObject, 
	    format("%s/scratch is not a directory", CM_CONFIG_DIR));

    ws->projPath = path_join(ws->projPath, CM_WORKSPACE_FSNAME);
  }

  if (ws->pc == 0)
    THROW(ExBadValue, "No change to write");
  
  {
    SDR_stream *strm = NULL;
    const char *tmpPath = xstrcat(ws->projPath, "-tmp");
    onthrow_remove(tmpPath);
  
    TRY {
      strm = stream_createfile(tmpPath, SDR_WORKSPACE);
      sdr_write("ws_nextChange", strm, ws->pc);
      stream_close(strm);

      path_rename(tmpPath, ws->projPath);
    }
    DEFAULT(ex) {
      if (strm) stream_close(strm);
      RETHROW(ex);
    }
    END_CATCH;
  }
}

WsEntity *
ws_AddFile(WorkSpace *ws, const char *fnm)
{
  WsEntity *wse = pendingchange_FindEntity(ws->pc,fnm);

  if (wse) {
    /* If NEF_JADD and NEF_DELETED, then change it back to just NEF_JADD */
    if ((wse->flags & NEF_JADD) && (wse->flags & NEF_DELETED)) {
      wse->flags &= ~ NEF_DELETED;
      SER_MODIFIED(wse);
    }

    /* If NEF_MERGED and NEF_DELETED, then just override the deletion
       of this Entity: */
    else if ((wse->flags & NEF_MERGED) && (wse->flags & NEF_DELETED)) {
      wse->flags &= ~ (NEF_MERGED | NEF_DELETED);
      SER_MODIFIED(wse);
    }

    /* If just NEF_DELETED, cancel that: */
    else if (wse->flags & NEF_DELETED) {
      wse->flags &= ~ NEF_DELETED; 
      SER_MODIFIED(wse);
    }

    /* If just NEF_CONDDEL, cancel that: */
    else if (wse->flags & NEF_CONDDEL) {
      wse->flags &= ~ NEF_CONDDEL; /* cancel a previous CONDDEL set by
                                      'merge' */
      SER_MODIFIED(wse);
    }
    /* If the object is already in the workspace, silently ignore.
       This is needed because ws_EnumeratePath is promiscuous. */
  }
  /* Brand new file */
  else if (path_isfile(fnm)) {
    wse = wsentity_addFromFile(ws->pc, fnm, opt_eType);
    pendingchange_InsertEntity(ws->pc, wse);
    wsentity_ReportStatus(wse);
    SER_MODIFIED(wse);
  }
  /* Unknown files */
  else if (path_exists(fnm)) {
    report(0, "? %s unknown type\n", fnm);
  }
  else {
    report(0, "? %s not found\n", fnm);
  }

  return wse;
}

void
ws_RemoveFile(WorkSpace *ws, const char *fnm)
{
  WsEntity *wse = pendingchange_FindEntity(ws->pc,fnm);

  if (wse) {
    /* If this is a mergespace orphan that was just added to the
       Workspace from 'merge', (NEF_MERGED and NEF_JADD) change its
       state to (NEF_MERGED | NEF_JADD | NEF_DELETED).  This allows
       user to be wishy-washy and change this entity back to
       (NEF_MERGED | NEF_JADD) if he wants. */
    if (wse->flags & NEF_JADD && wse->flags & NEF_MERGED) {
      wse->flags |= NEF_DELETED;
      wsentity_ReportStatus(wse);
      SER_MODIFIED(wse);
    }
    /* If WsEntity is marked NEF_ADDED, remove it from PendingChange,
       but leave the actual file in place.  This will result in that
       file becoming "unknown" to the 'status' command. */
    else if (wse->flags & NEF_ADDED) {
      pendingchange_RemoveEntity(ws->pc, wse);
      report(0, "%s %10d %s\n",
	      "[ forget ]",
	      wse->lk_length,
	      wse->cur_fsName);
      SER_MODIFIED(ws->pc);
    }
    else if (wse->flags & NEF_DELETED)
      report(0, "File is already marked for deletion.\n");
    else {

      /* If WsEntity is marked NEF_CONDDEL, cancel that first */
      if (wse->flags & NEF_CONDDEL) 
	wse->flags &= ~ NEF_CONDDEL;

      /* Mark entity for deletion */
      wse->flags |= NEF_DELETED;

      SER_MODIFIED(wse);
      wsentity_ReportStatus(wse);
    }
  }
  else
    report(0, "Not in your workspace: %s.\n", fnm);

  /* Don't remove actual file... let commit do that! */

}

WsEntity *
ws_Lookup(WorkSpace *ws, const char *fnm)
{
  return pendingchange_FindEntity(ws->pc, fnm);
}

static void
ws_DoEnumeratePath(WorkSpace *ws, 
		   StrVec *names,
		   const char *path,
		   unsigned flags,
		   OC_bool (*filter)(WorkSpace *, const char *))
{

  if (path_isdir(path)) {
    FilterSet *localFilters = filterset_LoadFrom(path);
    OC_DIR* dir;
    const char* ent;
  
    /* If this is another opencm workspace that was checked out as a
       subtree, skip it. */
    if (!nmequal(path, path_curdir()) &&
	path_exists(path_join(path, CM_CONFIG_DIR)))
      return;      

    dir = path_opendir(path, TRUE);

    while ((ent = path_readdir(dir))) {
      const char *entpath;
    
      if (path_should_skip_dirent(ent))
	continue;

      entpath = path_join(path, ent);
    
      if (flags & WSE_FILTER) {
	OC_bool exclude = FALSE;

	/* Check user-specified filter: */
	if (filter && filter(ws, entpath))
	  continue;

	exclude = filterset_ShouldExclude(GlobalFilters, entpath, exclude);
	exclude = filterset_ShouldExclude(localFilters, entpath, exclude);

	if (exclude)
	  continue;
      }
  
      if (path_isdir(entpath) && (flags & WSE_DIRS))
	strvec_append(names, entpath);

      ws_DoEnumeratePath(ws, names, entpath, flags, filter);
    }

    path_closedir(dir);
  }
  else if (path_isfile(path) && (flags & WSE_FILES))
    strvec_append(names, path);
  else if (path_issymlink(path) && (flags & WSE_SYMLINKS)) {
    strvec_append(names, path);
  }
}

void
ws_EnumeratePath(WorkSpace *ws, 
		 StrVec *names,
		 const char *path,
		 unsigned flags,
		 OC_bool (*filter)(WorkSpace *, const char *))
{
  /* Make sure 'path' is relative to the Workspace top dir, since
     that's how everything is processed */
  if (flags & WSE_NORMALIZE)
    path = ws_NormalizePath(ws, path);

  /* If we are asked on the command line to enumerate a file, we need
     to apply the user-supplied filter in all cases. */

  if ((flags & WSE_FILTER) && filter && filter(ws, path))
    return;

  /* FIX: Open issue: should we apply the per-directory and/or the
     global filters to a file specified on the command line? I think the
     answer (for now) should be no. */

  ws_DoEnumeratePath(ws, names, path, flags, filter);
}

StrVec *
ws_EnumerateWsEnts(WorkSpace *ws, StrVec *regexpVec)
{
  unsigned u, j;
  StrVec *names = strvec_create();

  if (regexpVec) strvec_sort(regexpVec);

  for (u = 0; u < vec_size(ws->pc->entSet); u++) {
    WsEntity *wse = vec_fetch(ws->pc->entSet, u, WsEntity *);

    if (regexpVec == NULL || strvec_bsearch(regexpVec, wse->cur_fsName) >= 0) {
      strvec_append(names, wse->cur_fsName);
      continue;
    }

    /* In some cases, "names" can contain a glob expression, such as
       the name of a subdir (which may or may not exist).  Here's
       where we handle that. */
    for (j = 0; j < vec_size(regexpVec); j++) {
      const char *glob_expr = vec_fetch(regexpVec, j, const char *);

      if (glob_match(wse->cur_fsName, glob_expr, GM_FS_COMPONENT))
	strvec_append(names, wse->cur_fsName);
    }
  }
  
  strvec_sort(names);

  return names;
}

void
ws_PruneWorkspace(WorkSpace *ws) 
{
  /* Grab an unfiltered list of directories: */

  StrVec *s = strvec_create();
  unsigned u;

  ws_EnumeratePath(ws, s, path_curdir(), WSE_DIRS, 0);

  strvec_sort(s);

  /* This is sleazy. We simply rely on the fact that rmdir() fails
     when applied to a non-empty directory. The sort above guarantees
     that foo/x always appears later than foo/ in the list, so we will
     simply proceed through it backwards. */

  u = vec_size(s);

  while (u) {
    const char *dirName;
    u--;

    dirName = vec_fetch(s, u, const char *);

    TRY {
      path_rmdir(dirName);
    }
    DEFAULT(ex) {
    }
    END_CATCH;
  }
}

void
ws_SetModTimes(const WorkSpace *ws) 
{
  int i;
  ObVec *wsEntVec = ws->pc->entSet;

  for (i = 0; i < vec_size(wsEntVec); i++) {
    WsEntity *went = vec_fetch(wsEntVec, i, WsEntity *);
    const char *modTime = went->old->modTime;

    /* FIX: If we later implement a --modtime option, the calculation of 
       modTime needs to deal specially with the case where the thing
       in the workspace has been modified or merged. For now, we are
       only calling ws_SetModTimes() from the checkout() routine, so
       there should only exist a parent (i.e. not a mergeParent). 
   
       Similarly, WsEntity should not be modified or otherwise
       flagged. */
    if (went->cur_mergeParent != 0 || went->flags)
      THROW(ExAssert, "Setting mod times in unhandled case.");

    if (path_exists(went->cur_fsName) && modTime != 0)
      path_set_mod_time(went->cur_fsName, modTime);
  }
}

void
ws_BuildPendingChange(WorkSpace *ws,
		      Repository *r,
		      const char *branchURI,
		      uint64_t nRevs)
{
  ws->pc = pendingchange_create(r, branchURI, nRevs);
  SER_MODIFIED(ws->pc);
}

static int
wsent_fsname_cmp(const void *v1, const void *v2)
{
  const WsEntity *e1 = *((const WsEntity **)v1);
  const WsEntity *e2 = *((const WsEntity **)v2);

  return strcmp(e1->cur_fsName, e2->cur_fsName);
}

/* If you check for collisions and there are any, this aborts. If you
 * do NOT check for collisions, colliding files will not be modified. */
void
ws_RestoreFiles(WorkSpace *ws, unsigned flags)
{
  int i = 0;
  Repository *r = ws->r;
  ObVec *wsEntVec;
  OC_bool canUpdate = TRUE;

  wsEntVec = obvec_shallow_copy(ws->pc->entSet);
  vec_sort_using(wsEntVec, wsent_fsname_cmp);

  if (flags & WSR_CHECKCOLLIDE) {
    for (i = 0; i < vec_size(wsEntVec); i++) {
      WsEntity *went = vec_fetch(wsEntVec, i, WsEntity *);

      if (path_exists(went->cur_fsName) && went->old != NULL) {
	Buffer *content = buffer_FromFile(went->cur_fsName, went->cur_entityType);
	if (nmequal(ser_getTrueName(content), went->old->contentTrueName))
	  continue;

	if (flags & WSR_FORCE) {
	  path_remove(went->cur_fsName);
	}
	else {
	  report(0, "Remove \"%s\" -- it is in the way\n", went->cur_fsName);
	  canUpdate = FALSE;
	}
      }
    }
  }

  if (canUpdate == FALSE)
    THROW(ExObjectExists, "Colliding files were found in the workspace.");

  /* Iterate over the Workspace PendingChange object */
  for (i = 0; i < vec_size(wsEntVec); i++) {

    WsEntity *went = vec_fetch(wsEntVec, i, WsEntity *);
	    
    if (path_exists(went->cur_fsName) && (flags & WSR_CHECKCOLLIDE) == 0)
      continue;

    if (path_exists(went->cur_fsName) && went->old && (flags & WSR_CHECKCOLLIDE) == 0)
      path_remove(went->cur_fsName);

    if (path_exists(went->cur_fsName) && went->old) {
      Buffer *content = buffer_FromFile(went->cur_fsName, went->cur_entityType);
      if (nmequal(ser_getTrueName(content), went->old->contentTrueName))
	continue;
    }

    if (went->old != NULL) {
      Buffer *content;

      content = (Buffer *)repos_GetEntity(r, ws->pc->branchURI, 
					  went->old->contentTrueName);
      assert(GETTYPE(content) == TY_Buffer);

      buffer_ToFile(content, went->cur_fsName, went->old->entityType);
      report(0, "U %s\n", went->cur_fsName);
    }

    {
      portstat_t ps;
      path_portstat(went->cur_fsName, &ps);
      went->lk_modTime = 0;	/* force modify recomputation */
      SER_MODIFIED(went);
      SER_MODIFIED(ws->pc);
    }
  }

  for (i = 0; i < vec_size(wsEntVec); i++) {
    WsEntity *went = vec_fetch(wsEntVec, i, WsEntity *);

    /* File may still be missing if it was NEF_ADDED to begin with: */
    if (path_exists(went->cur_fsName))
      path_mkexecutable(went->cur_fsName, 
			(went->cur_entityPerms & EPRM_EXEC) ? TRUE : FALSE);
  }
}

void
ws_Revert(WorkSpace *ws)
{
  unsigned i;
  WsEntity *wse;

  /* First remove the files that need to go because they have been
     modified. Make a half-hearted attempt to undo renames while we
     are at it. */
  for (i = 0; i < vec_size(ws->pc->entSet); i++) {
    wse = vec_fetch(ws->pc->entSet, i, WsEntity *);

    if (wse->flags & (NEF_JADD|NEF_ADDED|NEF_MERGED|NEF_MODIFIED)) {
      path_remove(wse->cur_fsName);
      continue;
    }
    if (wse->cur_mergeParent != 0 || wse->old == NULL) {
      path_remove(wse->cur_fsName);
      continue;
    }
    if (wse->flags & NEF_RENAMED) {
      const char *origName = wse->old->fsName;
      if (path_exists(origName)) {
	path_remove(wse->cur_fsName); 
	continue;
      }
      else
	path_rename(wse->cur_fsName, origName);
    }

    /* And don't forget about the PERMS status! Undo it manually here
       so we don't have to waste a roundtrip on the wire refetching
       the file contents.  If NEF_PERMS is set, then either the user
       set or unset the exec bit.  We need to check both cases. */
    if (wse->flags & NEF_PERMS) {
      if (wse->cur_entityPerms & EPRM_EXEC) { /* bit is set */
	wse->cur_entityPerms = 0;
	wse->flags &= ~ NEF_PERMS;
      }
      else {			/* bit is NOT set */
	wse->cur_entityPerms = EPRM_EXEC;
	wse->flags &= ~ NEF_PERMS;
      }
    }
  }

  {
    const char *branchURI = ws->pc->branchURI;
    uint64_t nRevs = ws->pc->nRevisions;

    ws->pc = pendingchange_create(ws->r, branchURI, nRevs);
    SER_MODIFIED(ws->pc);
  }

  ws_RestoreFiles(ws, WSR_CHECKCOLLIDE|WSR_FORCE);

  ws->pc->mergedChange = 0;
  SER_MODIFIED(ws->pc);
}

void
ws_Populate(WorkSpace *ws)
{
  unsigned i;
  
  StrVec *names = strvec_create();

  ws_EnumeratePath(ws, names, path_curdir(), 
                   WSE_FILES | WSE_FILTER, /* don't normalize and NO
                                              symlinks! */
                   0); 

  for (i = 0; i < vec_size(names); i++) {
    const char *relpath = vec_fetch(names,i, const char *);
    
    if (ws_Lookup(ws, relpath)) {
      if (opt_Verbosity > 0)
	report(0, "X %s\n", relpath);
    }
    else {
      ws_AddFile(ws, relpath);
      report(0, "N %s\n", relpath);
    }
  }

  SER_MODIFIED(ws->pc);
}

static OC_bool
ws_hasFsDir(WorkSpace *ws, const char *path)
{
  unsigned u;
  ObVec *wsvec = ws->pc->entSet;

  /* The following pass is probably unnecessary. The idea is to be
     really sure that none of the result names will collide. */
  for (u = 0; u < vec_size(wsvec); u++) {
    WsEntity *wse = vec_fetch(wsvec, u, WsEntity *);
    const char *rest;

    rest = glob_match(wse->cur_fsName, path, GM_FS_COMPONENT);

    if (rest == 0)
      continue;

    if (*rest == '/')
      return TRUE;

    if (*rest == '0' && wse->cur_entityType == 'd')
      return TRUE;
  }

  return FALSE;
}

void
ws_Rename(WorkSpace *ws, const char *old, const char *new)
{
  StrVec *fsvec;
  const char *orig_new = new;
  
  /* Don't allow user to rename config dir! (... or any
   * contents thereof, for that matter) */
  if (path_cdr(old) && nmequal(path_car(path_cdr(old)), CM_CONFIG_DIR))
    THROW(ExBadValue, "Can't move the config directory or its contents!");

  if (path_cdr(new) && nmequal(path_car(path_cdr(new)), CM_CONFIG_DIR))
    THROW(ExBadValue, "Don't try to move a file into the config directory!");

  /* There are three cases to handle:

     1. mv file1 file2
     2. mv file1 dir1
     3. mv dir1 dir2

     It is unclear whether the mv commands should be applied only to
     those names that exist in the user's workspace (i.e. to files) or
     to all of the matching names in the Workspace object. I have
     (perhaps counterintuitively) decided to go with the latter.

     No, on second thought it is very clear, which is really yucky.
  */

  if ( !pendingchange_FindEntity(ws->pc, old) &&
       !path_exists(old) &&
       !ws_hasFsDir(ws, old) )
    THROW(ExNoObject, 
	  format("File/directory \"%s\" not found", old));

  /* Make sure we won't overwrite something. Note that if the target
     is a directory name then "old" will be moved into "target". */
  if (pendingchange_FindEntity(ws->pc, new) || ws_hasFsDir(ws, new) || 
      (!path_isdir(new) && path_exists(new)) )
    THROW(ExObjectExists, 
	  format("Rename would clobber \"%s\"", orig_new));

  /* Special case if dest is a directory and source is a file: */
  {
    WsEntity *wse = pendingchange_FindEntity(ws->pc, old);

    if ( (path_isdir(new) || ws_hasFsDir(ws, new)) 
	 && (path_isfile(old) || (wse && wse->cur_entityType != 'd')) )
      new = path_join(new, path_tail(old));
  }

  /* At this point, either old and new are both files or old and new
     are both directories. Oddly enough, the following logic works in
     both cases. */

  /* This is a real pain in the butt, because we are going to move
     non-OpenCM files as well as workspace entities, and it's quite
     astonishing how many ways there are for these to collide. */

  fsvec = strvec_create();
  ws_EnumeratePath(ws, fsvec, path_curdir(), WSE_FILES|WSE_SYMLINKS, 0);
  strvec_sort(fsvec);

  /* First, go through the OpenCM workspace entries: */
  {
    ObVec * wsvec = ws->pc->entSet;
    unsigned u;

    /* IMPORTANT: note that we could not get through the tests above
       without calling pendingchange_FindEntity() at least once. This
       means that the entity set is already sorted, and that further
       calls to FindEntity() will not change its sort order. It is
       therefore safe to call FindEntity() within the following
       iteration: */

    /* The following pass is probably unnecessary. The idea is to be
       really sure that none of the result names will collide. */
    for (u = 0; u < vec_size(wsvec); u++) {
      WsEntity *wse = vec_fetch(wsvec, u, WsEntity *);
      const char *rest;
      const char *target;

      strvec_bremove(fsvec, wse->cur_fsName);

      rest = glob_match(wse->cur_fsName, old, GM_FS_COMPONENT);

      if (rest == 0)
	continue;

      target = (*rest == '\0') ? new : path_join(new, rest);

      if (path_exists(target) || pendingchange_FindEntity(ws->pc, new))
	THROW(ExObjectExists, 
	      format("Rename would clobber \"%s\"", target));
    }
  }

  /* Now, go through the residual files: */
  {
    unsigned u;

    for (u = 0; u < vec_size(fsvec); u++) {
      const char *curName = vec_fetch(fsvec, u, const char *);
      const char *rest;
      const char *target;

      rest = glob_match(curName, old, GM_FS_COMPONENT);

      if (rest == 0)
	continue;

      target = (*rest == '\0') ? new : path_join(new, rest);

      if (path_exists(target) || pendingchange_FindEntity(ws->pc, new))
	THROW(ExObjectExists, 
	      format("Rename would clobber \"%s\"", target));
    }
  }

  /* Rename all of the OpenCM entities as requested. */
  {
    ObVec * wsvec = ws->pc->entSet;
    unsigned u;

    for (u = 0; u < vec_size(wsvec); u++) {
      WsEntity *wse = vec_fetch(wsvec, u, WsEntity *);
      const char *rest;
      const char *target;

      rest = glob_match(wse->cur_fsName, old, GM_FS_COMPONENT);

      if (rest == 0)
	continue;

      target = (*rest == '\0') ? new : path_join(new, rest);

      report(0, "%s -> %s\n", wse->cur_fsName, target);

      path_smkdir(path_dirname(target));
      path_rename(wse->cur_fsName, target);

      wse->cur_fsName = target;
      wse->flags |= NEF_RENAMED;
  
      /* If user renamed it prior to committing it initially, or
       * if new name is same as current name then just reset the
       * flag */
      if (wse->old == NULL || nmequal(wse->old->fsName, wse->cur_fsName))
	wse->flags &= ~NEF_RENAMED;

      SER_MODIFIED(wse);
    }
  }

  /* Rename the residual files: */
  {
    unsigned u;

    for (u = 0; u < vec_size(fsvec); u++) {
      const char *curName = vec_fetch(fsvec, u, const char *);
      const char *rest;
      const char *target;

      rest = glob_match(curName, old, GM_FS_COMPONENT);

      if (rest == 0)
	continue;

      target = (*rest == '\0') ? new : path_join(new, rest);

      report(0, "%s -> %s\n", curName, target);

      path_smkdir(path_dirname(target));
      path_rename(curName, target);
    }
  }
}

void
ws_Commit(WorkSpace *ws, StrVec *names)
{
  ObVec *wsentVec = obvec_create();
  unsigned u;
  Change *new_chg;
  Change *cur_chg;  /* Used for wsentity_UploadTo() call */
  const char *cur_chg_name;
  Revision *oldrev;
  Mutable *chgM;
  /* const char *trueName; */
  CommitInfo *ci;
  StrVec *chgDescrip = 0;
  Repository *r = ws->r;
  
  /* FIX: There are preconditions to a commit that need to be checked
   * here and are not!
   */

  /* For example:  here's a hack to make sure we have something
   * to commit */
  OC_bool something_changed = FALSE;

  /* If user specified files to commit, then just search those for changes */
  if (names) {
    unsigned j;

    /* Need to ensure that user is not trying to partially commit
       results of a merge.  So, to be absolutely paranoid, we don't
       proceed if ws->pc->mergedChange is set (which indicates an
       uncommitted merge) */
    if (ws->pc->mergedChange)
      THROW(ExObjectExists, 
	    "Only full commits (no arguments) are allowed after a merge.");

    for (j = 0; j < vec_size(names); j++) {
      WsEntity *wsentity = ws_Lookup(ws, vec_fetch(names, j, const char *));
      if (wsentity && wsentity->flags != 0) {
	something_changed = TRUE;
	break;
      }
    }
  } else {
    /* Otherwise, need to iterate through entire Workspace's PendingChange */
    for (u = 0; u < vec_size(ws->pc->entSet); u++) {
      WsEntity *wse_u = vec_fetch(ws->pc->entSet, u, WsEntity *);
      if (wse_u->flags != 0) {
	something_changed = TRUE;
	break;
      }
    }
  }

  if (!something_changed && !ws->pc->mergedChange) {
    report(0, "Nothing to commit.\n");
    return;
  }

  if (opt_Message && ws->pc->notes) {
    /* FIX: It is not clear what the right thing to do is here. For
     * the moment, I go ahead and treat the message as an added note,
     * and set opt_Message to point to the revised value of
     * pc->notes. This preserves silence, but deprives the user of the
     * opportunity to edit the notes. Of course, if they use -m they
     * are probably one of those expert users.
     *
     * Note that if the commit fails the workspace will not be
     * rewritten, and in consequence the user-supplied message will
     * not get saved to the notes string, which is probably what we
     * want.
     */

    pendingchange_addNote(ws->pc, opt_Message);
    SER_MODIFIED(ws->pc);

    /* Smash opt_Message to point to the result of the combination in
     * order to preserve the silent commit behavior:
     */
    opt_Message = xstrdup(ws->pc->notes);
  }
  
  /* We always ensure before writing it that pc->notes contains a
   * terminating newline. Therefore, it is always safe to simply pass
   * it t strip_comments() (as happens within CreateNewCommitInfo(),
   * because the logic to newline-terminate the string will strip the
   * existing newline before re-appending it. This ensures that we
   * won't ever write off the end of the string.
   *
   * We need to make a separate pass to build a texty description of
   * the upload because we need to freeze the CommitInfo record before
   * we can upload any of the implicated entities.
   */

  chgDescrip = pendingchange_build_description(ws->pc);
  
  /* We now have a good description of the upload captured in
   * chgDescrip */

  /* We're going to optimistically guess what the branch number will be
   * so that we can go ahead and create the CommitInfo object. It is
   * possible that this guess will prove wrong because there is an
   * inherent race in the commit. In that case, the commit will fail
   * at the end when ReviseMutable() finds the wrong top revision
   * number.
  */

  chgM = repos_GetMutable(r, ws->pc->branchURI);
  oldrev = repos_GetTopRev(r, chgM);


  /* This is for caching info needed by wsentity_UploadTo() */
  cur_chg = (Change *)repos_GetMutableContent(r, chgM);
  cur_chg_name = ser_getTrueName(cur_chg);

  ci = CreateNewCommitInfo(ws->pc->notes, strvec_flatten(chgDescrip),
			   r->authMutable->uri,
			   ws->pc->branchURI, chgM->nRevisions);

  report(0, "Uploading CommitInfo Record... \n");

  repos_ReviseEntity(r, chgM->uri, ws->pc->baseCmtInfoName, ci);

  new_chg = change_create(cur_chg_name, ci);

  /* Go through all of the entities and ONLY upload to the server
   * entities that have changed state since last logical checkout.
   * While we are at it, construct a new Change record, and a text
   * description of what happened. For the text description, use a
   * strvec to limit the total number of string copies involved: */
  for (u = 0; u < vec_size(ws->pc->entSet); u++) {
    WsEntity *wse_u = vec_fetch(ws->pc->entSet, u, WsEntity *);

    /* Regardless of user specified fsnames, we need to build a new
       Change object that comprises ALL the Workspace objects. So, if
       any Workspace object hasn't changed, add its "old" version to
       the result set. */
    if (wse_u->flags == 0) {
      change_append_entity(new_chg, wse_u->old);
      obvec_append(wsentVec, wse_u);

      continue;
    }

    /* If a Workspace entity was added by 'merge' (NEF_JADD) and no
       modifications have been done locally (either to content or
       metadata) then that entity doesn't need to be uploaded to
       server either! */
    if (wse_u->flags & NEF_JADD) {
      if ((wse_u->flags & NEF_RENAMED) == 0 && 
	  (wse_u->flags & NEF_MODIFIED) == 0 && 
	  (wse_u->flags & NEF_DELETED) == 0 &&
	  (wse_u->flags & NEF_PERMS) == 0) {

	/* By definition, after a 'commit' all WsEntity objects in the
           PendingChange have all status flags cleared. (After all,
           the status flags only reflect the state since the last
           logical checkout. A logical checkout means 'checkout',
           'update' or 'commit'.) Usually, we call ws_UploadTo() for
           any WsEntity whose state has changed.  However, in this
           case the entity represented by the WsEntity is bit-wise
           identical to the mergespace entity and there's nothing new
           to upload.  Thus, we need to clear its status flags
           here. */
	wse_u->flags = 0;

	change_append_entity(new_chg, wse_u->old);
	obvec_append(wsentVec, wse_u);

	continue;
      }
    }

    /* Now, only act on user supplied fsnames */
    if (names) {

      /* Skip entities that aren't of interest, but make sure they're
         still kept in the new Change object.  This has the result of
         maintaining the local Workspace status, but maintaining the
         "old" version in our new Change object */
      if (strvec_bsearch(names, wse_u->cur_fsName) < 0) {
	/* Issue: If this is an added object, then it does not HAVE an
	   old entity. If the following check is NOT performed, and
	   there is an added entity that is NOT being uploaded, then
	   the change_append_entity() call gets passed a null
	   wse_u->old pointer, which causes a null pointer to get
	   added to the new change obvec, leading (thankfully) to a
	   core dump in the sort routine rather than a bad commit -- I
	   found this out the hard way. */
	if (wse_u->old)
	  change_append_entity(new_chg, wse_u->old);
	obvec_append(wsentVec, wse_u);

	continue;
      }
    }

    /* If a Workspace object is marked for deletion, just delete it
       and continue.  No need to keep track of it anymore in the
       Workspace. */
    if (wse_u->flags & NEF_DELETED) {
      path_remove(wse_u->cur_fsName);

      continue;
    }
    
    
    /* The upload has the side effect of recomputing the correct true
     * name for the revised entity. The library upload logic will
     * eliminate uploads of content that is already present on the
     * server.
     */

    assert(wse_u->flags);
    assert((wse_u->flags & NEF_DELETED) == 0);

    report(0, "Uploading \"%s\"\n", wse_u->cur_fsName);

    /* FIX: Verify that the entity we are uploading is in the right
     * branch generation!
     */
    if (wse_u->flags & NEF_JADD)
      wsentity_UploadTo(wse_u, r, chgM, ci, ws->pc->mergedChange);
    else
      wsentity_UploadTo(wse_u, r, chgM, ci, cur_chg_name);

    /* UploadTo has also updated wse_u->old to point to the
       newly uploaded entity, which now must be added to the entity
       set that is under construction: */
#if 0
    if (wse_u->flags & NEF_MODIFIED)
      assert(wse_u->cur_parent);
#endif

    assert(wse_u->cur_mergeParent == 0);

    /* wse_u is updated in place! */
    change_append_entity(new_chg, wse_u->old);
    obvec_append(wsentVec, wse_u);
  }

  /* Create a new Change record from this entity set: */
  if (ws->pc->mergedChange) {
    /* If there was a merge, there should have been a base! */
    assert(cur_chg_name);

    CMSET(new_chg, mergeParent, ws->pc->mergedChange);
    CMSET(new_chg, isPartialMerge, ws->pc->isPartialMerge);
  }

  repos_ReviseEntity(r, chgM->uri, cur_chg_name, new_chg);
  
  chgM = repos_ReviseMutable(r, chgM->uri, chgM->nRevisions, new_chg);

  /* Ensure new version of Branch was created successfully */
  chgM = repos_GetMutable(r, ws->pc->branchURI);

  /* Make sure the new rev number that we guessed earlier is correct */
  assert(chgM->nRevisions == CMGET(ci,branchVersion) + 1);

  /* Update the PendingChange object in the WorkSpace */
  ws->pc->nRevisions = chgM->nRevisions;
  ws->pc->baseChangeName = ser_getTrueName(new_chg);
  ws->pc->baseCmtInfoName = CMGET(new_chg, commitInfoTrueName);
  ws->pc->mergedChange = 0;
  ws->pc->entSet = wsentVec;

  ws->pc->notes = 0;
  ws->pc->isSorted = FALSE;

  SER_MODIFIED(ws->pc);
}

void
ws_Update(WorkSpace *ws)
{
  Change *topChange;
  const char *topChangeName = NULL;
  Mutable *chgM;
#if 0
  Revision *rev;
#endif

  chgM = repos_GetMutable(ws->r, ws->pc->branchURI);
#if 0
  rev = repos_GetTopRev(ws->r, chgM);
#endif
  topChange = (Change *)repos_GetMutableContent(ws->r, chgM);

  /* GetMutableContent should have thrown if there wasn't a top
     change, but shap is paranoid. */
  assert(topChange);
  assert (ws->pc->nRevisions <= chgM->nRevisions);

  if (ws->pc->mergedChange)
    THROW(ExIntegrityFail, 
	  "Updates not permitted following a merge. See 'cm help merge'.\n");

  ws_RestoreFiles(ws, 0);

  /* FIX: Why not do this selectively in ws_RestoreMissingFiles()? */
  pendingchange_RecomputeStatus(ws->pc, 0);

  topChangeName = ser_getTrueName(topChange);
  if (nmequal(topChangeName, ws->pc->baseChangeName)) {
    report(0, "Workspace is up to date.\n");
    return;
  }

  if (!ws_mergeFrom(ws, ws->r, ws->pc->branchURI, topChange, FROM_UPDATE))
    unimplemented("Update with collisions is incomplete!\n");
  
  pendingchange_RecomputeStatus(ws->pc, 0);

  /* Fix up the PendingChange to reflect the new state: */
  ws->pc->nRevisions = chgM->nRevisions;
  ws->pc->baseChangeName = topChangeName;
  ws->pc->baseCmtInfoName = CMGET(topChange,commitInfoTrueName);

  SER_MODIFIED(ws->pc);
}

void
ws_diff_against_change(Change *chg, Repository *r, 
		       const char *branchURI, StrVec *nmList) 
{
  unsigned u;
  
  CommitInfo *ci = 
    (CommitInfo *) repos_GetEntity(r, branchURI, CMGET(chg,commitInfoTrueName));
  (void) &ci;

  for (u = 0; u < vec_size(CMGET(chg,entities)); u++) {
    Entity *e = vec_fetch(CMGET(chg,entities), u, Entity *);
    WsEntity *wse_u = wsentity_fromEntity(e);

    if (nmlist_matches(nmList, wse_u->cur_fsName) == 0)
      continue;
    
    wsentity_RecomputeStatus(wse_u);
    wsentity_dodiff(wse_u, r, branchURI);
  }
}

