ra_plugin.c   [plain text]


/*
 * ra_plugin.c : the main RA module for local repository access
 *
 * ====================================================================
 * Copyright (c) 2000-2004 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */

#include "ra_local.h"
#include "svn_ra.h"
#include "svn_fs.h"
#include "svn_delta.h"
#include "svn_repos.h"
#include "svn_pools.h"
#include "svn_time.h"
#include "svn_props.h"
#include "svn_path.h"
#include "svn_private_config.h"
#include "../libsvn_ra/ra_loader.h"

#define APR_WANT_STRFUNC
#include <apr_want.h>

#include <assert.h>

/*----------------------------------------------------------------*/


/* The reporter vtable needed by do_update() */
typedef struct reporter_baton_t
{
  svn_ra_local__session_baton_t *session;
  void *report_baton;

} reporter_baton_t;


static void *
make_reporter_baton(svn_ra_local__session_baton_t *session,
                    void *report_baton,
                    apr_pool_t *pool)
{
  reporter_baton_t *rbaton = apr_palloc(pool, sizeof(*rbaton));
  rbaton->session = session;
  rbaton->report_baton = report_baton;
  return rbaton;
}


static svn_error_t *
reporter_set_path(void *reporter_baton,
                  const char *path,
                  svn_revnum_t revision,
                  svn_boolean_t start_empty,
                  const char *lock_token,
                  apr_pool_t *pool)
{
  reporter_baton_t *rbaton = reporter_baton;
  return svn_repos_set_path2(rbaton->report_baton, path,
                             revision, start_empty, lock_token, pool);
}


static svn_error_t *
reporter_delete_path(void *reporter_baton,
                     const char *path,
                     apr_pool_t *pool)
{
  reporter_baton_t *rbaton = reporter_baton;
  return svn_repos_delete_path(rbaton->report_baton, path, pool);
}


static svn_error_t *
reporter_link_path(void *reporter_baton,
                   const char *path,
                   const char *url,
                   svn_revnum_t revision,
                   svn_boolean_t start_empty,
                   const char *lock_token,
                   apr_pool_t *pool)
{
  reporter_baton_t *rbaton = reporter_baton;
  const char *fs_path = NULL;
  const char *repos_url_decoded;
  int repos_url_len;

  url = svn_path_uri_decode(url, pool);
  repos_url_decoded = svn_path_uri_decode(rbaton->session->repos_url, pool);
  repos_url_len = strlen(repos_url_decoded);
  if (strncmp(url, repos_url_decoded, repos_url_len) != 0)
    return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
                             _("'%s'\n"
                               "is not the same repository as\n"
                               "'%s'"), url, rbaton->session->repos_url);
  fs_path = url + repos_url_len;

  return svn_repos_link_path2(rbaton->report_baton, path, fs_path, revision,
                              start_empty, lock_token, pool);
}


static svn_error_t *
reporter_finish_report(void *reporter_baton,
                       apr_pool_t *pool)
{
  reporter_baton_t *rbaton = reporter_baton;
  return svn_repos_finish_report(rbaton->report_baton, pool);
}


static svn_error_t *
reporter_abort_report(void *reporter_baton,
                      apr_pool_t *pool)
{
  reporter_baton_t *rbaton = reporter_baton;
  return svn_repos_abort_report(rbaton->report_baton, pool);
}


static const svn_ra_reporter2_t ra_local_reporter = 
{
  reporter_set_path,
  reporter_delete_path,
  reporter_link_path,
  reporter_finish_report,
  reporter_abort_report
};

static svn_error_t *
svn_ra_local__get_file_revs(svn_ra_session_t *session,
                            const char *path,
                            svn_revnum_t start,
                            svn_revnum_t end,
                            svn_ra_file_rev_handler_t handler,
                            void *handler_baton,
                            apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sbaton = session->priv;
  const char *abs_path = sbaton->fs_path->data;

  /* Concatenate paths */
  abs_path = svn_path_join(abs_path, path, pool);

  return svn_repos_get_file_revs(sbaton->repos, abs_path, start, end, NULL,
                                 NULL, handler, handler_baton, pool);
}

/* Pool cleanup handler: Ensure that the access descriptor of the filesystem
   DATA is set to NULL. */
static apr_status_t
cleanup_access(void *data)
{
  svn_error_t *serr;
  svn_fs_t *fs = data;

  serr = svn_fs_set_access(fs, NULL);

  if (serr)
    {
      apr_status_t apr_err = serr->apr_err;
      svn_error_clear(serr);
      return apr_err;
    }

  return APR_SUCCESS;
}

static svn_error_t *
get_username(svn_ra_session_t *session,
             apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton = session->priv;
  svn_auth_iterstate_t *iterstate;
  svn_fs_access_t *access_ctx;

  /* If we've already found the username don't ask for it again. */
  if (! baton->username)
    {
      /* Get a username somehow, so we have some svn:author property to
         attach to a commit. */
      if (baton->callbacks->auth_baton)
        {
          void *creds;
          svn_auth_cred_username_t *username_creds;
          SVN_ERR(svn_auth_first_credentials(&creds, &iterstate,
                                             SVN_AUTH_CRED_USERNAME,
                                             baton->uuid, /* realmstring */
                                             baton->callbacks->auth_baton,
                                             pool));
          
          /* No point in calling next_creds(), since that assumes that the
             first_creds() somehow failed to authenticate.  But there's no
             challenge going on, so we use whatever creds we get back on
             the first try. */
          username_creds = creds;
          if (username_creds && username_creds->username)
            {
              baton->username = apr_pstrdup(session->pool,
                                            username_creds->username);
              svn_error_clear(svn_auth_save_credentials(iterstate, pool));
            }
          else
            baton->username = "";
        }
      else
        baton->username = "";
    }

  /* If we have a real username, attach it to the filesystem so that it can
     be used to validate locks.  Even if there already is a user context
     associated, it may contain irrelevant lock tokens, so always create a new.
  */
  if (*baton->username)
    {
      SVN_ERR(svn_fs_create_access(&access_ctx, baton->username,
                                   pool));
      SVN_ERR(svn_fs_set_access(baton->fs, access_ctx));

      /* Make sure this context is disassociated when the pool gets
         destroyed. */
      apr_pool_cleanup_register(pool, baton->fs, cleanup_access,
                                apr_pool_cleanup_null);
    }

  return SVN_NO_ERROR;
}


/*----------------------------------------------------------------*/

/** The RA vtable routines **/

#define RA_LOCAL_DESCRIPTION \
        N_("Module for accessing a repository on local disk.")

static const char *
svn_ra_local__get_description(void)
{
  return _(RA_LOCAL_DESCRIPTION);
}

static const char * const *
svn_ra_local__get_schemes(apr_pool_t *pool)
{
  static const char *schemes[] = { "file", NULL };

  return schemes;
}

static svn_error_t *
svn_ra_local__open(svn_ra_session_t *session,
                   const char *repos_URL,
                   const svn_ra_callbacks2_t *callbacks,
                   void *callback_baton,
                   apr_hash_t *config,
                   apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton;
  const char *fs_path;
  
  /* Allocate and stash the session_baton args we have already. */
  baton = apr_pcalloc(pool, sizeof(*baton));
  baton->callbacks = callbacks;
  baton->callback_baton = callback_baton;
  
  /* Look through the URL, figure out which part points to the
     repository, and which part is the path *within* the
     repository. */
  SVN_ERR_W(svn_ra_local__split_URL(&(baton->repos),
                                    &(baton->repos_url),
                                    &fs_path,
                                    repos_URL,
                                    session->pool),
            _("Unable to open an ra_local session to URL"));
  baton->fs_path = svn_stringbuf_create(fs_path, session->pool);

  /* Cache the filesystem object from the repos here for
     convenience. */
  baton->fs = svn_repos_fs(baton->repos);

  /* Cache the repository UUID as well */
  SVN_ERR(svn_fs_get_uuid(baton->fs, &baton->uuid, session->pool));

  /* Be sure username is NULL so we know to look it up / ask for it */
  baton->username = NULL;

  session->priv = baton;
  return SVN_NO_ERROR;
}

static svn_error_t *
svn_ra_local__reparent(svn_ra_session_t *session,
                       const char *url,
                       apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton = session->priv;

  svn_stringbuf_set(baton->fs_path,
                    svn_path_uri_decode(url + strlen(baton->repos_url),
                                        pool));

  return SVN_NO_ERROR;
}

static svn_error_t *
svn_ra_local__get_latest_revnum(svn_ra_session_t *session,
                                svn_revnum_t *latest_revnum,
                                apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton = session->priv;

  SVN_ERR(svn_fs_youngest_rev(latest_revnum, baton->fs, pool));

  return SVN_NO_ERROR;
}



static svn_error_t *
svn_ra_local__get_dated_revision(svn_ra_session_t *session,
                                 svn_revnum_t *revision,
                                 apr_time_t tm,
                                 apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton = session->priv;

  SVN_ERR(svn_repos_dated_revision(revision, baton->repos, tm, pool));

  return SVN_NO_ERROR;
}


static svn_error_t *
svn_ra_local__change_rev_prop(svn_ra_session_t *session,
                              svn_revnum_t rev,
                              const char *name,
                              const svn_string_t *value,
                              apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton = session->priv;

  SVN_ERR(get_username(session, pool));

  SVN_ERR(svn_repos_fs_change_rev_prop2(baton->repos, rev, baton->username,
                                        name, value, NULL, NULL, pool));

  return SVN_NO_ERROR;
}


static svn_error_t *
svn_ra_local__get_uuid(svn_ra_session_t *session,
                       const char **uuid,
                       apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton = session->priv;

  *uuid = baton->uuid;

  return SVN_NO_ERROR;
}

static svn_error_t *
svn_ra_local__get_repos_root(svn_ra_session_t *session,
                             const char **url,
                             apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton = session->priv;

  *url = baton->repos_url;

  return SVN_NO_ERROR;
}

static svn_error_t *
svn_ra_local__rev_proplist(svn_ra_session_t *session,
                           svn_revnum_t rev,
                           apr_hash_t **props,
                           apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton = session->priv;

  SVN_ERR(svn_repos_fs_revision_proplist(props, baton->repos, rev,
                                         NULL, NULL, pool));

  return SVN_NO_ERROR;
}


static svn_error_t *
svn_ra_local__rev_prop(svn_ra_session_t *session,
                       svn_revnum_t rev,
                       const char *name,
                       svn_string_t **value,
                       apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *baton = session->priv;

  SVN_ERR(svn_repos_fs_revision_prop(value, baton->repos, rev, name,
                                     NULL, NULL, pool));

  return SVN_NO_ERROR;
}


struct deltify_etc_baton
{
  svn_fs_t *fs;                    /* the fs to deltify in */
  svn_repos_t *repos;              /* repos for unlocking */
  const char *fs_path;             /* fs-path part of split session URL */
  apr_hash_t *lock_tokens;         /* tokens to unlock, if any */
  apr_pool_t *pool;                /* pool for scratch work */
  svn_commit_callback2_t callback;  /* the original callback */
  void *callback_baton;            /* the original callback's baton */
};

/* This implements 'svn_commit_callback_t'.  Its invokes the original
   (wrapped) callback, but also does deltification on the new revision and
   possibly unlocks committed paths.
   BATON is 'struct deltify_etc_baton *'. */
static svn_error_t * 
deltify_etc(const svn_commit_info_t *commit_info,
            void *baton, apr_pool_t *pool)
{
  struct deltify_etc_baton *db = baton;
  svn_error_t *err1, *err2;
  apr_hash_index_t *hi;
  apr_pool_t *iterpool;

  /* Invoke the original callback first, in case someone's waiting to
     know the revision number so they can go off and annotate an
     issue or something. */
  err1 = (*db->callback)(commit_info, db->callback_baton, pool);

  /* Maybe unlock the paths. */
  if (db->lock_tokens)
    {
      iterpool = svn_pool_create(db->pool);
      for (hi = apr_hash_first(db->pool, db->lock_tokens); hi;
           hi = apr_hash_next(hi))
        {
          const void *rel_path;
          void *val;
          const char *abs_path, *token;

          svn_pool_clear(iterpool);
          apr_hash_this(hi, &rel_path, NULL, &val);
          token = val;
          abs_path = svn_path_join(db->fs_path, rel_path, iterpool);
          /* We may get errors here if the lock was broken or stolen
             after the commit succeeded.  This is fine and should be
             ignored. */
          svn_error_clear(svn_repos_fs_unlock(db->repos, abs_path, token,
                                              FALSE, iterpool));
        }
      svn_pool_destroy(iterpool);
    }

  /* But, deltification shouldn't be stopped just because someone's
     random callback failed, so proceed unconditionally on to
     deltification. */
  err2 = svn_fs_deltify_revision(db->fs, commit_info->revision, db->pool);

  /* It's more interesting if the original callback failed, so let
     that one dominate. */
  if (err1)
    {
      svn_error_clear(err2);
      return err1;
    }

  return err2;
}


static svn_error_t *
svn_ra_local__get_commit_editor(svn_ra_session_t *session,
                                const svn_delta_editor_t **editor,
                                void **edit_baton,
                                const char *log_msg,
                                svn_commit_callback2_t callback,
                                void *callback_baton,
                                apr_hash_t *lock_tokens,
                                svn_boolean_t keep_locks,
                                apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sess_baton = session->priv;
  struct deltify_etc_baton *db = apr_palloc(pool, sizeof(*db));
  apr_hash_index_t *hi;
  svn_fs_access_t *fs_access;

  db->fs = sess_baton->fs;
  db->repos = sess_baton->repos;
  db->fs_path = sess_baton->fs_path->data;
  if (! keep_locks)
    db->lock_tokens = lock_tokens;
  else
    db->lock_tokens = NULL;
  db->pool = pool;
  db->callback = callback;
  db->callback_baton = callback_baton;

  SVN_ERR(get_username(session, pool));

  /* If there are lock tokens to add, do so. */
  if (lock_tokens)
    {
      SVN_ERR(svn_fs_get_access(&fs_access, sess_baton->fs));

      /* If there is no access context, the filesystem will scream if a
         lock is needed. */      
      if (fs_access)
        {
          for (hi = apr_hash_first(pool, lock_tokens); hi;
               hi = apr_hash_next(hi))
            {
              void *val;
              const char *token;

              apr_hash_this(hi, NULL, NULL, &val);
              token = val;
              SVN_ERR(svn_fs_access_add_lock_token(fs_access, token));
            }
        }
    }
              
  /* Get the repos commit-editor */     
  SVN_ERR(svn_repos_get_commit_editor4
          (editor, edit_baton, sess_baton->repos, NULL,
           svn_path_uri_decode(sess_baton->repos_url, pool),
           sess_baton->fs_path->data,
           sess_baton->username, log_msg,
           deltify_etc, db, NULL, NULL, pool));

  return SVN_NO_ERROR;
}


static svn_error_t *
make_reporter(svn_ra_session_t *session,
              const svn_ra_reporter2_t **reporter,
              void **report_baton,
              svn_revnum_t revision,
              const char *target,
              const char *other_url,
              svn_boolean_t text_deltas,
              svn_boolean_t recurse,
              svn_boolean_t ignore_ancestry,
              const svn_delta_editor_t *editor,
              void *edit_baton,
              apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sbaton = session->priv;
  void *rbaton;
  int repos_url_len;
  const char *other_fs_path = NULL;
  const char *repos_url_decoded;

  /* Get the HEAD revision if one is not supplied. */
  if (! SVN_IS_VALID_REVNUM(revision))
    SVN_ERR(svn_ra_local__get_latest_revnum(session, &revision, pool));

  /* If OTHER_URL was provided, validate it and convert it into a
     regular filesystem path. */
  if (other_url)
    {
      other_url = svn_path_uri_decode(other_url, pool);
      repos_url_decoded = svn_path_uri_decode(sbaton->repos_url, pool);
      repos_url_len = strlen(repos_url_decoded);
      
      /* Sanity check:  the other_url better be in the same repository as
         the original session url! */
      if (strncmp(other_url, repos_url_decoded, repos_url_len) != 0)
        return svn_error_createf 
          (SVN_ERR_RA_ILLEGAL_URL, NULL,
           _("'%s'\n"
             "is not the same repository as\n"
             "'%s'"), other_url, sbaton->repos_url);

      other_fs_path = other_url + repos_url_len;
    }

  /* Pass back our reporter */
  *reporter = &ra_local_reporter;

  SVN_ERR(get_username(session, pool));
  
  /* Build a reporter baton. */
  SVN_ERR(svn_repos_begin_report(&rbaton,
                                 revision,
                                 sbaton->username,
                                 sbaton->repos, 
                                 sbaton->fs_path->data,
                                 target, 
                                 other_fs_path,
                                 text_deltas,
                                 recurse,
                                 ignore_ancestry,
                                 editor, 
                                 edit_baton,
                                 NULL,
                                 NULL,
                                 pool));
  
  /* Wrap the report baton given us by the repos layer with our own
     reporter baton. */
  *report_baton = make_reporter_baton(sbaton, rbaton, pool);

  return SVN_NO_ERROR;
}


static svn_error_t *
svn_ra_local__do_update(svn_ra_session_t *session,
                        const svn_ra_reporter2_t **reporter,
                        void **report_baton,
                        svn_revnum_t update_revision,
                        const char *update_target,
                        svn_boolean_t recurse,
                        const svn_delta_editor_t *update_editor,
                        void *update_baton,
                        apr_pool_t *pool)
{
  return make_reporter(session,
                       reporter,
                       report_baton,
                       update_revision,
                       update_target,
                       NULL,
                       TRUE,
                       recurse,
                       FALSE,
                       update_editor,
                       update_baton,
                       pool);
}


static svn_error_t *
svn_ra_local__do_switch(svn_ra_session_t *session,
                        const svn_ra_reporter2_t **reporter,
                        void **report_baton,
                        svn_revnum_t update_revision,
                        const char *update_target,
                        svn_boolean_t recurse,
                        const char *switch_url,
                        const svn_delta_editor_t *update_editor,
                        void *update_baton,
                        apr_pool_t *pool)
{
  return make_reporter(session,
                       reporter,
                       report_baton,
                       update_revision,
                       update_target,
                       switch_url,
                       TRUE,
                       recurse,
                       TRUE,
                       update_editor,
                       update_baton,
                       pool);
}


static svn_error_t *
svn_ra_local__do_status(svn_ra_session_t *session,
                        const svn_ra_reporter2_t **reporter,
                        void **report_baton,
                        const char *status_target,
                        svn_revnum_t revision,
                        svn_boolean_t recurse,
                        const svn_delta_editor_t *status_editor,
                        void *status_baton,
                        apr_pool_t *pool)
{
  return make_reporter(session,
                       reporter,
                       report_baton,
                       revision,
                       status_target,
                       NULL,
                       FALSE,
                       recurse,
                       FALSE,
                       status_editor,
                       status_baton,
                       pool);
}


static svn_error_t *
svn_ra_local__do_diff(svn_ra_session_t *session,
                      const svn_ra_reporter2_t **reporter,
                      void **report_baton,
                      svn_revnum_t update_revision,
                      const char *update_target,
                      svn_boolean_t recurse,
                      svn_boolean_t ignore_ancestry,
                      svn_boolean_t text_deltas,
                      const char *switch_url,
                      const svn_delta_editor_t *update_editor,
                      void *update_baton,
                      apr_pool_t *pool)
{
  return make_reporter(session,
                       reporter,
                       report_baton,
                       update_revision,
                       update_target,
                       switch_url,
                       text_deltas,
                       recurse,
                       ignore_ancestry,
                       update_editor,
                       update_baton,
                       pool);
}


static svn_error_t *
svn_ra_local__get_log(svn_ra_session_t *session,
                      const apr_array_header_t *paths,
                      svn_revnum_t start,
                      svn_revnum_t end,
                      int limit,
                      svn_boolean_t discover_changed_paths,
                      svn_boolean_t strict_node_history,
                      svn_log_message_receiver_t receiver,
                      void *receiver_baton,
                      apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sbaton = session->priv;
  int i;
  apr_array_header_t *abs_paths =
    apr_array_make(pool, 0, sizeof(const char *));

  if (paths)
    {
      for (i = 0; i < paths->nelts; i++)
        {
          const char *abs_path = "";
          const char *relative_path = (((const char **)(paths)->elts)[i]);
          
          /* Append the relative paths to the base FS path to get an
             absolute repository path. */
          abs_path = svn_path_join(sbaton->fs_path->data, relative_path,
                                   pool);
          (*((const char **)(apr_array_push(abs_paths)))) = abs_path;
        }
    }

  return svn_repos_get_logs3(sbaton->repos,
                             abs_paths,
                             start,
                             end,
                             limit,
                             discover_changed_paths,
                             strict_node_history,
                             NULL, NULL,
                             receiver,
                             receiver_baton,
                             pool);
}


static svn_error_t *
svn_ra_local__do_check_path(svn_ra_session_t *session,
                            const char *path,
                            svn_revnum_t revision,
                            svn_node_kind_t *kind,
                            apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sbaton = session->priv;
  svn_fs_root_t *root;
  const char *abs_path = sbaton->fs_path->data;

  /* ### Not sure if this counts as a workaround or not.  The
     session baton uses the empty string to mean root, and not
     sure that should change.  However, it would be better to use
     a path library function to add this separator -- hardcoding
     it is totally bogus.  See issue #559, though it may be only
     tangentially related. */
  if (abs_path[0] == '\0')
    abs_path = "/";

  /* If we were given a relative path to append, append it. */
  if (path)
    abs_path = svn_path_join(abs_path, path, pool);

  if (! SVN_IS_VALID_REVNUM(revision))
    SVN_ERR(svn_fs_youngest_rev(&revision, sbaton->fs, pool));
  SVN_ERR(svn_fs_revision_root(&root, sbaton->fs, revision, pool));
  SVN_ERR(svn_fs_check_path(kind, root, abs_path, pool));
  return SVN_NO_ERROR;
}


static svn_error_t *
svn_ra_local__stat(svn_ra_session_t *session,
                   const char *path,
                   svn_revnum_t revision,
                   svn_dirent_t **dirent,
                   apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sbaton = session->priv;
  svn_fs_root_t *root;
  const char *abs_path = sbaton->fs_path->data;
  
  /* ### see note above in __do_check_path() */
  if (abs_path[0] == '\0')
    abs_path = "/";

  if (path)
    abs_path = svn_path_join(abs_path, path, pool);

  if (! SVN_IS_VALID_REVNUM(revision))
    SVN_ERR(svn_fs_youngest_rev(&revision, sbaton->fs, pool));
  SVN_ERR(svn_fs_revision_root(&root, sbaton->fs, revision, pool));

  SVN_ERR(svn_repos_stat(dirent, root, abs_path, pool));

  return SVN_NO_ERROR;
}




static svn_error_t *
get_node_props(apr_hash_t **props,
               svn_ra_local__session_baton_t *sbaton,
               svn_fs_root_t *root,
               const char *path,
               apr_pool_t *pool)
{
  svn_revnum_t cmt_rev;
  const char *cmt_date, *cmt_author;

  /* Create a hash with props attached to the fs node. */
  SVN_ERR(svn_fs_node_proplist(props, root, path, pool));
      
  /* Now add some non-tweakable metadata to the hash as well... */
    
  /* The so-called 'entryprops' with info about CR & friends. */
  SVN_ERR(svn_repos_get_committed_info(&cmt_rev, &cmt_date,
                                       &cmt_author, root, path, pool));

  apr_hash_set(*props, 
               SVN_PROP_ENTRY_COMMITTED_REV, 
               APR_HASH_KEY_STRING, 
               svn_string_createf(pool, "%ld", cmt_rev));
  apr_hash_set(*props, 
               SVN_PROP_ENTRY_COMMITTED_DATE, 
               APR_HASH_KEY_STRING, 
               cmt_date ? svn_string_create(cmt_date, pool) : NULL);
  apr_hash_set(*props, 
               SVN_PROP_ENTRY_LAST_AUTHOR, 
               APR_HASH_KEY_STRING, 
               cmt_author ? svn_string_create(cmt_author, pool) : NULL);
  apr_hash_set(*props, 
               SVN_PROP_ENTRY_UUID,
               APR_HASH_KEY_STRING, 
               svn_string_create(sbaton->uuid, pool));

  /* We have no 'wcprops' in ra_local, but might someday. */
  
  return SVN_NO_ERROR;
}


/* Getting just one file. */
static svn_error_t *
svn_ra_local__get_file(svn_ra_session_t *session,
                       const char *path,
                       svn_revnum_t revision,
                       svn_stream_t *stream,
                       svn_revnum_t *fetched_rev,
                       apr_hash_t **props,
                       apr_pool_t *pool)
{
  svn_fs_root_t *root;
  svn_stream_t *contents;
  svn_revnum_t youngest_rev;
  svn_ra_local__session_baton_t *sbaton = session->priv;
  const char *abs_path = sbaton->fs_path->data;

  /* ### Not sure if this counts as a workaround or not.  The
     session baton uses the empty string to mean root, and not
     sure that should change.  However, it would be better to use
     a path library function to add this separator -- hardcoding
     it is totally bogus.  See issue #559, though it may be only
     tangentially related. */
  if (abs_path[0] == '\0')
    abs_path = "/";

  /* If we were given a relative path to append, append it. */
  if (path)
    abs_path = svn_path_join(abs_path, path, pool);

  /* Open the revision's root. */
  if (! SVN_IS_VALID_REVNUM(revision))
    {
      SVN_ERR(svn_fs_youngest_rev(&youngest_rev, sbaton->fs, pool));
      SVN_ERR(svn_fs_revision_root(&root, sbaton->fs, youngest_rev, pool));
      if (fetched_rev != NULL)
        *fetched_rev = youngest_rev;
    }
  else
    SVN_ERR(svn_fs_revision_root(&root, sbaton->fs, revision, pool));

  if (stream)
    {
      /* Get a stream representing the file's contents. */
      SVN_ERR(svn_fs_file_contents(&contents, root, abs_path, pool));
      
      /* Now push data from the fs stream back at the caller's stream.
         Note that this particular RA layer does not computing a
         checksum as we go, and confirming it against the repository's
         checksum when done.  That's because it calls
         svn_fs_file_contents() directly, which already checks the
         stored checksum, and all we're doing here is writing bytes in
         a loop.  Truly, Nothing Can Go Wrong :-).  But RA layers that
         go over a network should confirm the checksum. */
      SVN_ERR(svn_stream_copy(contents, stream, pool));
      SVN_ERR(svn_stream_close(contents));
    }

  /* Handle props if requested. */
  if (props)
    SVN_ERR(get_node_props(props, sbaton, root, abs_path, pool));

  return SVN_NO_ERROR;
}



/* Getting a directory's entries */
static svn_error_t *
svn_ra_local__get_dir(svn_ra_session_t *session,
                      apr_hash_t **dirents,
                      svn_revnum_t *fetched_rev,
                      apr_hash_t **props,
                      const char *path,
                      svn_revnum_t revision,
                      apr_uint32_t dirent_fields,
                      apr_pool_t *pool)
{
  svn_fs_root_t *root;
  svn_revnum_t youngest_rev;
  apr_hash_t *entries;
  apr_hash_index_t *hi;
  svn_ra_local__session_baton_t *sbaton = session->priv;
  const char *abs_path = sbaton->fs_path->data;
  apr_pool_t *subpool;

  /* ### Not sure if this counts as a workaround or not.  The
     session baton uses the empty string to mean root, and not
     sure that should change.  However, it would be better to use
     a path library function to add this separator -- hardcoding
     it is totally bogus.  See issue #559, though it may be only
     tangentially related. */
  if (abs_path[0] == '\0')
    abs_path = "/";

  /* If we were given a relative path to append, append it. */
  if (path)
    abs_path = svn_path_join(abs_path, path, pool);

  /* Open the revision's root. */
  if (! SVN_IS_VALID_REVNUM(revision))
    {
      SVN_ERR(svn_fs_youngest_rev(&youngest_rev, sbaton->fs, pool));
      SVN_ERR(svn_fs_revision_root(&root, sbaton->fs, youngest_rev, pool));
      if (fetched_rev != NULL)
        *fetched_rev = youngest_rev;
    }
  else
    SVN_ERR(svn_fs_revision_root(&root, sbaton->fs, revision, pool));

  if (dirents)
    {
      /* Get the dir's entries. */
      SVN_ERR(svn_fs_dir_entries(&entries, root, abs_path, pool));
      
      /* Loop over the fs dirents, and build a hash of general
         svn_dirent_t's. */
      *dirents = apr_hash_make(pool);
      subpool = svn_pool_create(pool);
      for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi))
        {
          const void *key;
          void *val;
          apr_hash_t *prophash;
          const char *datestring, *entryname, *fullpath;
          svn_fs_dirent_t *fs_entry;
          svn_dirent_t *entry = apr_pcalloc(pool, sizeof(*entry));

          svn_pool_clear(subpool);

          apr_hash_this(hi, &key, NULL, &val);
          entryname = (const char *) key;
          fs_entry = (svn_fs_dirent_t *) val;

          fullpath = svn_path_join(abs_path, entryname, subpool);
        
          if (dirent_fields & SVN_DIRENT_KIND)
            {  
              /* node kind */
              entry->kind = fs_entry->kind;
            }

          if (dirent_fields & SVN_DIRENT_SIZE)
            {          
              /* size  */
              if (entry->kind == svn_node_dir)
                entry->size = 0;
              else
                SVN_ERR(svn_fs_file_length(&(entry->size), root,
                                           fullpath, subpool));
            }

          if (dirent_fields & SVN_DIRENT_HAS_PROPS)
            { 
              /* has_props? */
              SVN_ERR(svn_fs_node_proplist(&prophash, root, fullpath,
                                           subpool));
              entry->has_props = (apr_hash_count(prophash)) ? TRUE : FALSE;
            }

          if ((dirent_fields & SVN_DIRENT_TIME)
              || (dirent_fields & SVN_DIRENT_LAST_AUTHOR)
              || (dirent_fields & SVN_DIRENT_CREATED_REV))
            { 
              /* created_rev & friends */
              SVN_ERR(svn_repos_get_committed_info(&(entry->created_rev),
                                                   &datestring,
                                                   &(entry->last_author),
                                                   root, fullpath, subpool));
              if (datestring)
                SVN_ERR(svn_time_from_cstring(&(entry->time), datestring,
                                              pool));
              if (entry->last_author)
                entry->last_author = apr_pstrdup(pool, entry->last_author);
            }
 
          /* Store. */
          apr_hash_set(*dirents, entryname, APR_HASH_KEY_STRING, entry);
        }
      svn_pool_destroy(subpool);
    }

  /* Handle props if requested. */
  if (props)
    SVN_ERR(get_node_props(props, sbaton, root, abs_path, pool));

  return SVN_NO_ERROR;
}

static svn_error_t *
svn_ra_local__get_locations(svn_ra_session_t *session,
                            apr_hash_t **locations,
                            const char *relative_path,
                            svn_revnum_t peg_revision,
                            apr_array_header_t *location_revisions,
                            apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sbaton = session->priv;
  const char *abs_path;

  /* Append the relative path to the base FS path to get an
     absolute repository path. */
  abs_path = svn_path_join(sbaton->fs_path->data, relative_path, pool);

  SVN_ERR(svn_repos_trace_node_locations(sbaton->fs, locations, abs_path,
                                         peg_revision, location_revisions,
                                         NULL, NULL, pool));

  return SVN_NO_ERROR;
}



static svn_error_t *
svn_ra_local__lock(svn_ra_session_t *session,
                   apr_hash_t *path_revs,
                   const char *comment,
                   svn_boolean_t force,
                   svn_ra_lock_callback_t lock_func, 
                   void *lock_baton,
                   apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sess = session->priv;
  apr_hash_index_t *hi;
  apr_pool_t *iterpool = svn_pool_create(pool);

  /* A username is absolutely required to lock a path. */
  SVN_ERR(get_username(session, pool));

  for (hi = apr_hash_first(pool, path_revs); hi; hi = apr_hash_next(hi))
    {
      svn_lock_t *lock;
      const void *key;
      const char *path;
      void *val;
      svn_revnum_t *revnum;
      const char *abs_path;
      svn_error_t *err, *callback_err = NULL;
 
      svn_pool_clear(iterpool);
      apr_hash_this(hi, &key, NULL, &val);
      path = key;
      revnum = val;

      abs_path = svn_path_join(sess->fs_path->data, path, iterpool);

      /* This wrapper will call pre- and post-lock hooks. */
      err = svn_repos_fs_lock(&lock, sess->repos, abs_path, NULL, comment,
                              FALSE /* not DAV comment */,
                              0 /* no expiration */, *revnum, force, 
                              iterpool);

      if (err && !SVN_ERR_IS_LOCK_ERROR(err))
        return err;

      if (lock_func)
        callback_err = lock_func(lock_baton, path, TRUE, err ? NULL : lock,
                                 err, iterpool);

      svn_error_clear(err);

      if (callback_err)
        return callback_err;
    }

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}


static svn_error_t *
svn_ra_local__unlock(svn_ra_session_t *session,
                     apr_hash_t *path_tokens,
                     svn_boolean_t force,
                     svn_ra_lock_callback_t lock_func, 
                     void *lock_baton,
                     apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sess = session->priv;
  apr_hash_index_t *hi;
  apr_pool_t *iterpool = svn_pool_create(pool);

  /* A username is absolutely required to unlock a path. */
  SVN_ERR(get_username(session, pool));

  for (hi = apr_hash_first(pool, path_tokens); hi; hi = apr_hash_next(hi))
    {
      const void *key;
      const char *path;
      void *val;
      const char *abs_path, *token;
      svn_error_t *err, *callback_err = NULL;
 
      svn_pool_clear(iterpool);
      apr_hash_this(hi, &key, NULL, &val);
      path = key;
      /* Since we can't store NULL values in a hash, we turn "" to
         NULL here. */
      if (strcmp(val, "") != 0)
        token = val;
      else
        token = NULL;

      abs_path = svn_path_join(sess->fs_path->data, path, iterpool);

      /* This wrapper will call pre- and post-unlock hooks. */
      err = svn_repos_fs_unlock(sess->repos, abs_path, token, force,
                                iterpool);

      if (err && !SVN_ERR_IS_UNLOCK_ERROR(err))
        return err;

      if (lock_func)
        callback_err = lock_func(lock_baton, path, FALSE, NULL, err, iterpool);

      svn_error_clear(err);

      if (callback_err)
        return callback_err;
    }

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}



static svn_error_t *
svn_ra_local__get_lock(svn_ra_session_t *session,
                       svn_lock_t **lock,
                       const char *path,
                       apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sess = session->priv;
  const char *abs_path;

  /* Get the absolute path. */
  abs_path = svn_path_join(sess->fs_path->data, path, pool);

  SVN_ERR(svn_fs_get_lock(lock, sess->fs, abs_path, pool));

  return SVN_NO_ERROR;
}



static svn_error_t *
svn_ra_local__get_locks(svn_ra_session_t *session,
                        apr_hash_t **locks,
                        const char *path,
                        apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sess = session->priv;
  const char *abs_path;

  /* Get the absolute path. */
  abs_path = svn_path_join(sess->fs_path->data, path, pool);

  /* Kinda silly to call the repos wrapper, since we have no authz
     func to give it.  But heck, why not. */
  SVN_ERR(svn_repos_fs_get_locks(locks, sess->repos, abs_path,
                                 NULL, NULL, pool));

  return SVN_NO_ERROR;
}


static svn_error_t *
svn_ra_local__replay(svn_ra_session_t *session,
                     svn_revnum_t revision,
                     svn_revnum_t low_water_mark,
                     svn_boolean_t send_deltas,
                     const svn_delta_editor_t *editor,
                     void *edit_baton,
                     apr_pool_t *pool)
{
  svn_ra_local__session_baton_t *sess = session->priv;
  svn_fs_root_t *root;

  SVN_ERR(svn_fs_revision_root(&root, svn_repos_fs(sess->repos),
                               revision, pool));

  SVN_ERR(svn_repos_replay2(root, sess->fs_path->data, low_water_mark,
                            send_deltas, editor, edit_baton, NULL, NULL,
                            pool));

  return SVN_NO_ERROR;
}


/*----------------------------------------------------------------*/

static const svn_version_t *
ra_local_version(void)
{
  SVN_VERSION_BODY;
}

/** The ra_vtable **/

static const svn_ra__vtable_t ra_local_vtable =
{
  ra_local_version,
  svn_ra_local__get_description,
  svn_ra_local__get_schemes,
  svn_ra_local__open,
  svn_ra_local__reparent,
  svn_ra_local__get_latest_revnum,
  svn_ra_local__get_dated_revision,
  svn_ra_local__change_rev_prop,
  svn_ra_local__rev_proplist,
  svn_ra_local__rev_prop,
  svn_ra_local__get_commit_editor,
  svn_ra_local__get_file,
  svn_ra_local__get_dir,
  svn_ra_local__do_update,
  svn_ra_local__do_switch,
  svn_ra_local__do_status,
  svn_ra_local__do_diff,
  svn_ra_local__get_log,
  svn_ra_local__do_check_path,
  svn_ra_local__stat,
  svn_ra_local__get_uuid,
  svn_ra_local__get_repos_root,
  svn_ra_local__get_locations,
  svn_ra_local__get_file_revs,
  svn_ra_local__lock,
  svn_ra_local__unlock,
  svn_ra_local__get_lock,
  svn_ra_local__get_locks,
  svn_ra_local__replay,
};


/*----------------------------------------------------------------*/

/** The One Public Routine, called by libsvn_ra **/

svn_error_t *
svn_ra_local__init(const svn_version_t *loader_version,
                   const svn_ra__vtable_t **vtable,
                   apr_pool_t *pool)
{
  static const svn_version_checklist_t checklist[] =
    {
      { "svn_subr",  svn_subr_version },
      { "svn_delta", svn_delta_version },
      { "svn_repos", svn_repos_version },
      { "svn_fs",    svn_fs_version },
      { NULL, NULL }
    };

  
  /* Simplified version check to make sure we can safely use the
     VTABLE parameter. The RA loader does a more exhaustive check. */
  if (loader_version->major != SVN_VER_MAJOR)
    return svn_error_createf(SVN_ERR_VERSION_MISMATCH, NULL,
                             _("Unsupported RA loader version (%d) for "
                               "ra_local"),
                             loader_version->major);

  SVN_ERR(svn_ver_check_list(ra_local_version(), checklist));

#ifndef SVN_LIBSVN_CLIENT_LINKS_RA_LOCAL
  /* This assumes that POOL was the pool used to load the dso. */
  SVN_ERR(svn_fs_initialize(pool));
#endif

  *vtable = &ra_local_vtable;

  return SVN_NO_ERROR;
}

/* Compatibility wrapper for the 1.1 and before API. */
#define NAME "ra_local"
#define DESCRIPTION RA_LOCAL_DESCRIPTION
#define VTBL ra_local_vtable
#define INITFUNC svn_ra_local__init
#define COMPAT_INITFUNC svn_ra_local_init
#include "../libsvn_ra/wrapper_template.h"