/*
* adm_files.c: helper routines for handling files & dirs in the
* working copy administrative area (creating,
* deleting, opening, and closing). This is the only
* code that actually knows where administrative
* information is kept.
*
* ====================================================================
* Copyright (c) 2000-2008 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 <stdarg.h>
#include <apr_pools.h>
#include <apr_file_io.h>
#include <apr_strings.h>
#include "svn_types.h"
#include "svn_error.h"
#include "svn_io.h"
#include "svn_path.h"
#include "svn_hash.h"
#include "wc.h"
#include "adm_files.h"
#include "entries.h"
#include "lock.h"
#include "svn_private_config.h"
#include "private/svn_wc_private.h"
/*** File names in the adm area. ***/
/* The default name of the WC admin directory. This name is always
checked by svn_wc_is_adm_dir. */
static const char default_adm_dir_name[] = ".svn";
/* The name that is actually used for the WC admin directory. The
commonest case where this won't be the default is in Windows
ASP.NET development environments, which choke on ".svn". */
static const char *adm_dir_name = default_adm_dir_name;
svn_boolean_t
svn_wc_is_adm_dir(const char *name, apr_pool_t *pool)
{
return (0 == strcmp(name, adm_dir_name)
|| 0 == strcmp(name, default_adm_dir_name));
}
const char *
svn_wc_get_adm_dir(apr_pool_t *pool)
{
return adm_dir_name;
}
svn_error_t *
svn_wc_set_adm_dir(const char *name, apr_pool_t *pool)
{
/* This is the canonical list of administrative directory names.
FIXME:
An identical list is used in
libsvn_subr/opt.c:svn_opt__args_to_target_array(),
but that function can't use this list, because that use would
create a circular dependency between libsvn_wc and libsvn_subr.
Make sure changes to the lists are always synchronized! */
static const char *valid_dir_names[] = {
default_adm_dir_name,
"_svn",
NULL
};
const char **dir_name;
for (dir_name = valid_dir_names; *dir_name; ++dir_name)
if (0 == strcmp(name, *dir_name))
{
/* Use the pointer to the statically allocated string
constant, to avoid potential pool lifetime issues. */
adm_dir_name = *dir_name;
return SVN_NO_ERROR;
}
return svn_error_createf
(SVN_ERR_BAD_FILENAME, NULL,
_("'%s' is not a valid administrative directory name"),
svn_path_local_style(name, pool));
}
/* Return the path to something in PATH's administrative area.
*
* First, the adm subdir is appended to PATH as a component, then the
* "tmp" directory is added iff USE_TMP is set, then each of the
* varargs in AP (char *'s) is appended as a path component. The list
* must be terminated with a NULL argument.
*
* Adding an empty component results in no effect (i.e., the separator
* char is not doubled).
*
* If EXTENSION is non-null, it will be appended to the final string
* without a separator character.
*/
static const char *
v_extend_with_adm_name(const char *path,
const char *extension,
svn_boolean_t use_tmp,
apr_pool_t *pool,
va_list ap)
{
const char *this;
/* Tack on the administrative subdirectory. */
path = svn_path_join(path, adm_dir_name, pool);
/* If this is a tmp file, name it into the tmp area. */
if (use_tmp)
path = svn_path_join(path, SVN_WC__ADM_TMP, pool);
/* Tack on everything else. */
while ((this = va_arg(ap, const char *)) != NULL)
{
if (this[0] == '\0')
continue;
path = svn_path_join(path, this, pool);
}
if (extension)
path = apr_pstrcat(pool, path, extension, NULL);
return path;
}
/* See v_extend_with_adm_name() for details. */
static const char *
extend_with_adm_name(const char *path,
const char *extension,
svn_boolean_t use_tmp,
apr_pool_t *pool,
...)
{
va_list ap;
va_start(ap, pool);
path = v_extend_with_adm_name(path, extension, use_tmp, pool, ap);
va_end(ap);
return path;
}
const char *svn_wc__adm_child(const char *path,
const char *child,
apr_pool_t *result_pool)
{
return extend_with_adm_name(path, NULL, FALSE, result_pool, child, NULL);
}
svn_boolean_t
svn_wc__adm_area_exists(const svn_wc_adm_access_t *adm_access,
apr_pool_t *pool)
{
const char *path = svn_wc__adm_child(svn_wc_adm_access_path(adm_access),
NULL, pool);
svn_node_kind_t kind;
svn_error_t *err;
err = svn_io_check_path(path, &kind, pool);
if (err)
{
svn_error_clear(err);
/* Return early, since kind is undefined in this case. */
return FALSE;
}
return kind != svn_node_none;
}
/*** Making and using files in the adm area. ***/
/* */
static svn_error_t *
make_adm_subdir(const char *path,
const char *subdir,
svn_boolean_t tmp,
apr_pool_t *pool)
{
const char *fullpath;
fullpath = extend_with_adm_name(path, NULL, tmp, pool, subdir, NULL);
return svn_io_dir_make(fullpath, APR_OS_DEFAULT, pool);
}
svn_error_t *
svn_wc__make_killme(svn_wc_adm_access_t *adm_access,
svn_boolean_t adm_only,
apr_pool_t *pool)
{
const char *path;
SVN_ERR(svn_wc__adm_write_check(adm_access, pool));
path = svn_wc__adm_child(svn_wc_adm_access_path(adm_access),
SVN_WC__ADM_KILLME, pool);
return svn_io_file_create(path, adm_only ? SVN_WC__KILL_ADM_ONLY : "", pool);
}
svn_error_t *
svn_wc__check_killme(svn_wc_adm_access_t *adm_access,
svn_boolean_t *exists,
svn_boolean_t *kill_adm_only,
apr_pool_t *pool)
{
const char *path;
svn_error_t *err;
svn_stringbuf_t *contents;
path = svn_wc__adm_child(svn_wc_adm_access_path(adm_access),
SVN_WC__ADM_KILLME, pool);
err = svn_stringbuf_from_file2(&contents, path, pool);
if (err)
{
if (APR_STATUS_IS_ENOENT(err->apr_err))
{
/* Killme file doesn't exist. */
*exists = FALSE;
svn_error_clear(err);
err = SVN_NO_ERROR;
}
return err;
}
*exists = TRUE;
/* If the killme file contains the string 'adm-only' then only the
administrative area should be removed. */
*kill_adm_only = strcmp(contents->data, SVN_WC__KILL_ADM_ONLY) == 0;
return SVN_NO_ERROR;
}
/*** Syncing files in the adm area. ***/
/* Rename a tmp text-base file to its real text-base name.
The file had better already be closed. */
svn_error_t *
svn_wc__sync_text_base(const char *path, apr_pool_t *pool)
{
const char *parent_path;
const char *base_name;
const char *tmp_path;
const char *base_path;
svn_path_split(path, &parent_path, &base_name, pool);
/* Extend tmp name. */
tmp_path = extend_with_adm_name(parent_path, SVN_WC__BASE_EXT, TRUE, pool,
SVN_WC__ADM_TEXT_BASE, base_name, NULL);
/* Extend real name. */
base_path = extend_with_adm_name(parent_path, SVN_WC__BASE_EXT, FALSE, pool,
SVN_WC__ADM_TEXT_BASE, base_name, NULL);
/* Rename. */
SVN_ERR(svn_io_file_rename(tmp_path, base_path, pool));
return svn_io_set_file_read_only(base_path, FALSE, pool);
}
const char *
svn_wc__text_base_path(const char *path,
svn_boolean_t tmp,
apr_pool_t *pool)
{
const char *newpath, *base_name;
svn_path_split(path, &newpath, &base_name, pool);
return extend_with_adm_name(newpath,
SVN_WC__BASE_EXT,
tmp,
pool,
SVN_WC__ADM_TEXT_BASE,
base_name,
NULL);
}
const char *
svn_wc__text_revert_path(const char *path,
apr_pool_t *pool)
{
const char *newpath, *base_name;
svn_path_split(path, &newpath, &base_name, pool);
return extend_with_adm_name(newpath,
SVN_WC__REVERT_EXT,
FALSE,
pool,
SVN_WC__ADM_TEXT_BASE,
base_name,
NULL);
}
svn_error_t *
svn_wc__get_revert_contents(svn_stream_t **contents,
const char *path,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const char *revert_base = svn_wc__text_revert_path(path, scratch_pool);
if (revert_base == NULL)
{
*contents = NULL;
return SVN_NO_ERROR;
}
return svn_stream_open_readonly(contents, revert_base, result_pool,
scratch_pool);
}
svn_error_t *
svn_wc__prop_path(const char **prop_path,
const char *path,
svn_node_kind_t node_kind,
svn_wc__props_kind_t props_kind,
apr_pool_t *pool)
{
if (node_kind == svn_node_dir) /* It's a working copy dir */
{
static const char * names[] = {
SVN_WC__ADM_DIR_PROP_BASE, /* svn_wc__props_base */
SVN_WC__ADM_DIR_PROP_REVERT, /* svn_wc__props_revert */
SVN_WC__ADM_DIR_WCPROPS, /* svn_wc__props_wcprop */
SVN_WC__ADM_DIR_PROPS /* svn_wc__props_working */
};
*prop_path = extend_with_adm_name
(path,
NULL,
FALSE,
pool,
names[props_kind],
NULL);
}
else /* It's a file */
{
static const char * extensions[] = {
SVN_WC__BASE_EXT, /* svn_wc__props_base */
SVN_WC__REVERT_EXT, /* svn_wc__props_revert */
SVN_WC__WORK_EXT, /* svn_wc__props_wcprop */
SVN_WC__WORK_EXT /* svn_wc__props_working */
};
static const char * dirs[] = {
SVN_WC__ADM_PROP_BASE, /* svn_wc__props_base */
SVN_WC__ADM_PROP_BASE, /* svn_wc__props_revert */
SVN_WC__ADM_WCPROPS, /* svn_wc__props_wcprop */
SVN_WC__ADM_PROPS /* svn_wc__props_working */
};
const char *base_name;
svn_path_split(path, prop_path, &base_name, pool);
*prop_path = extend_with_adm_name
(*prop_path,
extensions[props_kind],
FALSE,
pool,
dirs[props_kind],
base_name,
NULL);
}
return SVN_NO_ERROR;
}
/*** Opening and closing files in the adm area. ***/
/* Open a file somewhere in the adm area for directory PATH.
* First, add the adm subdir as the next component of PATH, then add
* each of the varargs (they are char *'s), then add EXTENSION if it
* is non-null, then open the resulting file as *STREAM.
*
* If FLAGS indicates writing, open the file in the adm tmp area.
* This means the file will probably need to be renamed from there,
* either by passing the sync flag to close_adm_file() later, or with
* an explicit call to sync_adm_file().
*/
static svn_error_t *
open_adm_file(svn_stream_t **stream,
const char **selected_path,
const char *path,
const char *extension,
svn_boolean_t for_writing,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool,
...)
{
svn_error_t *err;
va_list ap;
/* If we're writing, always do it to a tmp file. */
if (for_writing)
{
/* Extend with tmp name. */
va_start(ap, scratch_pool);
path = v_extend_with_adm_name(path, extension, TRUE, result_pool, ap);
va_end(ap);
err = svn_stream_open_writable(stream, path, result_pool, scratch_pool);
}
else
{
/* Extend with regular adm name. */
va_start(ap, scratch_pool);
path = v_extend_with_adm_name(path, extension, FALSE, result_pool, ap);
va_end(ap);
err = svn_stream_open_readonly(stream, path, result_pool, scratch_pool);
}
if (selected_path)
*selected_path = path; /* note: built in result_pool */
if (for_writing && err && APR_STATUS_IS_EEXIST(err->apr_err))
{
/* Exclusive open failed, delete and retry */
svn_error_clear(err);
SVN_ERR(svn_io_remove_file(path, scratch_pool));
err = svn_stream_open_writable(stream, path, result_pool, scratch_pool);
}
/* Examine the error from the first and/or second attempt at opening. */
if (for_writing && err && APR_STATUS_IS_ENOENT(err->apr_err))
{
/* If we receive a failure to open a file in our temporary directory,
* it may be because our temporary directories aren't created.
* Older SVN clients did not create these directories.
* 'svn cleanup' will fix this problem.
*/
err = svn_error_quick_wrap(err,
_("Your .svn/tmp directory may be missing or "
"corrupt; run 'svn cleanup' and try again"));
}
return err;
}
svn_error_t *
svn_wc__open_adm_writable(svn_stream_t **stream,
const char **temp_file_path,
const char *path,
const char *fname,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
return open_adm_file(stream, temp_file_path, path, NULL /* extension */,
TRUE /* for_writing */,
result_pool, scratch_pool,
fname, NULL);
}
svn_error_t *
svn_wc__close_adm_stream(svn_stream_t *stream,
const char *temp_file_path,
const char *path,
const char *fname,
apr_pool_t *scratch_pool)
{
const char *tmp_path = extend_with_adm_name(path, NULL, TRUE, scratch_pool,
fname, NULL);
const char *dst_path = extend_with_adm_name(path, NULL, FALSE, scratch_pool,
fname, NULL);
/* ### eventually, just use the parameter rather than compute tmp_path */
SVN_ERR_ASSERT(strcmp(temp_file_path, tmp_path) == 0);
SVN_ERR(svn_stream_close(stream));
/* Put the completed file into its intended location. */
SVN_ERR(svn_io_file_rename(tmp_path, dst_path, scratch_pool));
return svn_io_set_file_read_only(dst_path, FALSE, scratch_pool);
}
svn_error_t *
svn_wc__remove_adm_file(const svn_wc_adm_access_t *adm_access,
const char *filename,
apr_pool_t *scratch_pool)
{
const char *path = svn_wc__adm_child(svn_wc_adm_access_path(adm_access),
filename, scratch_pool);
return svn_io_remove_file(path, scratch_pool);
}
svn_error_t *
svn_wc__open_adm_stream(svn_stream_t **stream,
const char *path,
const char *fname,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
return open_adm_file(stream,
NULL /* selected_path */,
path,
NULL /* extension */,
FALSE /* for_writing */,
result_pool, scratch_pool,
fname,
NULL);
}
svn_error_t *
svn_wc__open_writable_base(svn_stream_t **stream,
const char **temp_base_path,
const char *path,
svn_boolean_t need_revert_base,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const char *parent_path;
const char *base_name;
svn_path_split(path, &parent_path, &base_name, scratch_pool);
return open_adm_file(stream, temp_base_path,
parent_path,
need_revert_base
? SVN_WC__REVERT_EXT
: SVN_WC__BASE_EXT,
TRUE /* for_writing */,
result_pool, scratch_pool,
SVN_WC__ADM_TEXT_BASE,
base_name,
NULL);
}
svn_error_t *
svn_wc__write_old_wcprops(const char *path,
apr_hash_t *prophash,
svn_node_kind_t kind,
apr_pool_t *scratch_pool)
{
apr_pool_t *pool = scratch_pool;
const char *parent_dir;
const char *base_name;
svn_stream_t *stream;
const char *temp_dir_path;
const char *temp_prop_path;
const char *prop_path;
int wc_format_version;
if (kind == svn_node_dir)
parent_dir = path;
else
svn_path_split(path, &parent_dir, &base_name, pool);
/* At this point, we know we need to open a file in the admin area
of parent_dir. First check that parent_dir is a working copy: */
SVN_ERR(svn_wc_check_wc(parent_dir, &wc_format_version, pool));
if (wc_format_version == 0)
return svn_error_createf(SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
_("'%s' is not a working copy"),
svn_path_local_style(parent_dir, pool));
/* Write to a temp file, then rename into place. */
temp_dir_path = svn_wc__adm_child(parent_dir, SVN_WC__ADM_TMP, pool);
SVN_ERR(svn_stream_open_unique(&stream, &temp_prop_path,
temp_dir_path,
svn_io_file_del_none,
pool, pool));
SVN_ERR_W(svn_hash_write2(prophash, stream, SVN_HASH_TERMINATOR,
pool),
apr_psprintf(pool,
_("Cannot write property hash for '%s'"),
svn_path_local_style(path, pool)));
svn_stream_close(stream);
/* Close file, then do an atomic "move". */
SVN_ERR(svn_wc__prop_path(&prop_path, path, kind, svn_wc__props_wcprop,
pool));
SVN_ERR(svn_io_file_rename(temp_prop_path, prop_path, pool));
return svn_io_set_file_read_only(prop_path, FALSE, pool);
}
/*** Checking for and creating administrative subdirs. ***/
static svn_error_t *
make_empty_adm(const char *path, apr_pool_t *pool)
{
path = svn_wc__adm_child(path, NULL, pool);
return svn_io_dir_make_hidden(path, APR_OS_DEFAULT, pool);
}
static svn_error_t *
init_adm_tmp_area(const svn_wc_adm_access_t *adm_access,
apr_pool_t *pool)
{
const char *path;
SVN_ERR(svn_wc__adm_write_check(adm_access, pool));
path = svn_wc_adm_access_path(adm_access);
/* SVN_WC__ADM_TMP */
SVN_ERR(make_adm_subdir(path, SVN_WC__ADM_TMP, FALSE, pool));
/* SVN_WC__ADM_TMP/SVN_WC__ADM_TEXT_BASE */
SVN_ERR(make_adm_subdir(path, SVN_WC__ADM_TEXT_BASE, TRUE, pool));
/* SVN_WC__ADM_TMP/SVN_WC__ADM_PROP_BASE */
SVN_ERR(make_adm_subdir(path, SVN_WC__ADM_PROP_BASE, TRUE, pool));
/* SVN_WC__ADM_TMP/SVN_WC__ADM_PROPS */
return make_adm_subdir(path, SVN_WC__ADM_PROPS, TRUE, pool);
}
/* Set up a new adm area for PATH, with URL as the ancestor url, and
INITIAL_REV as the starting revision. The entries file starts out
marked as 'incomplete. The adm area starts out locked; remember to
unlock it when done. */
static svn_error_t *
init_adm(const char *path,
const char *uuid,
const char *url,
const char *repos,
svn_revnum_t initial_rev,
svn_depth_t depth,
apr_pool_t *pool)
{
svn_wc_adm_access_t *adm_access;
/* First, make an empty administrative area. */
SVN_ERR(make_empty_adm(path, pool));
/* Lock it immediately. Theoretically, no compliant wc library
would ever consider this an adm area until a README file were
present... but locking it is still appropriately paranoid. */
SVN_ERR(svn_wc__adm_pre_open(&adm_access, path, pool));
/** Make subdirectories. ***/
/* SVN_WC__ADM_TEXT_BASE */
SVN_ERR(make_adm_subdir(path, SVN_WC__ADM_TEXT_BASE, FALSE, pool));
/* SVN_WC__ADM_PROP_BASE */
SVN_ERR(make_adm_subdir(path, SVN_WC__ADM_PROP_BASE, FALSE, pool));
/* SVN_WC__ADM_PROPS */
SVN_ERR(make_adm_subdir(path, SVN_WC__ADM_PROPS, FALSE, pool));
/** Init the tmp area. ***/
SVN_ERR(init_adm_tmp_area(adm_access, pool));
/** Initialize each administrative file. */
/* SVN_WC__ADM_ENTRIES */
/* THIS FILE MUST BE CREATED LAST:
After this exists, the dir is considered complete. */
SVN_ERR(svn_wc__entries_init(path, uuid, url, repos,
initial_rev, depth, pool));
/* Now unlock it. It's now a valid working copy directory, that
just happens to be at revision 0. */
return svn_wc_adm_close2(adm_access, pool);
}
svn_error_t *
svn_wc_ensure_adm3(const char *path,
const char *uuid,
const char *url,
const char *repos,
svn_revnum_t revision,
svn_depth_t depth,
apr_pool_t *pool)
{
svn_wc_adm_access_t *adm_access;
const svn_wc_entry_t *entry;
int format;
SVN_ERR(svn_wc_check_wc(path, &format, pool));
/* Early out: we know we're not dealing with an existing wc, so
just create one. */
if (format == 0)
return init_adm(path, uuid, url, repos, revision, depth, pool);
/* Now, get the existing url and repos for PATH. */
SVN_ERR(svn_wc_adm_open3(&adm_access, NULL, path, FALSE, 0,
NULL, NULL, pool));
SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, FALSE, pool));
SVN_ERR(svn_wc_adm_close2(adm_access, pool));
/* When the directory exists and is scheduled for deletion do not
* check the revision or the URL. The revision can be any
* arbitrary revision and the URL may differ if the add is
* being driven from a merge which will have a different URL. */
if (entry->schedule != svn_wc_schedule_delete)
{
if (entry->revision != revision)
return
svn_error_createf
(SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
_("Revision %ld doesn't match existing revision %ld in '%s'"),
revision, entry->revision, path);
/** ### comparing URLs, should they be canonicalized first? */
if (strcmp(entry->url, url) != 0)
return
svn_error_createf
(SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
_("URL '%s' doesn't match existing URL '%s' in '%s'"),
url, entry->url, path);
}
return SVN_NO_ERROR;
}
svn_error_t *
svn_wc__adm_destroy(svn_wc_adm_access_t *adm_access,
apr_pool_t *scratch_pool)
{
const char *path;
SVN_ERR(svn_wc__adm_write_check(adm_access, scratch_pool));
/* Well, the coast is clear for blowing away the administrative
directory, which also removes the lock file */
path = svn_wc__adm_child(svn_wc_adm_access_path(adm_access), NULL,
scratch_pool);
SVN_ERR(svn_io_remove_dir2(path, FALSE, NULL, NULL, scratch_pool));
return svn_wc_adm_close2(adm_access, scratch_pool);
}
svn_error_t *
svn_wc__adm_cleanup_tmp_area(const svn_wc_adm_access_t *adm_access,
apr_pool_t *scratch_pool)
{
const char *tmp_path;
/* If the admin area doesn't even *exist*, then the temp area is
definitely cleaned up. */
if (!svn_wc__adm_area_exists(adm_access, scratch_pool))
return SVN_NO_ERROR;
SVN_ERR(svn_wc__adm_write_check(adm_access, scratch_pool));
/* Get the path to the tmp area, and blow it away. */
tmp_path = svn_wc__adm_child(svn_wc_adm_access_path(adm_access),
SVN_WC__ADM_TMP, scratch_pool);
SVN_ERR(svn_io_remove_dir2(tmp_path, TRUE, NULL, NULL, scratch_pool));
/* Now, rebuild the tmp area. */
return init_adm_tmp_area(adm_access, scratch_pool);
}
svn_error_t *
svn_wc_create_tmp_file2(apr_file_t **fp,
const char **new_name,
const char *path,
svn_io_file_del_t delete_when,
apr_pool_t *pool)
{
const char *temp_dir;
apr_file_t *file;
SVN_ERR_ASSERT(fp || new_name);
temp_dir = svn_wc__adm_child(path, SVN_WC__ADM_TMP, pool);
SVN_ERR(svn_io_open_unique_file3(&file, new_name, temp_dir,
delete_when, pool, pool));
if (fp)
*fp = file;
else
SVN_ERR(svn_io_file_close(file, pool));
return SVN_NO_ERROR;
}