blob: 145fce05650e3c81e9ac17aef8cdf6c8c699503f [file] [log] [blame]
/*
* wc_editor.c: editing the local modifications in the WC.
*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*/
/* ==================================================================== */
/*** Includes. ***/
#include <string.h>
#include "svn_hash.h"
#include "svn_client.h"
#include "svn_delta.h"
#include "svn_dirent_uri.h"
#include "svn_error.h"
#include "svn_error_codes.h"
#include "svn_pools.h"
#include "svn_props.h"
#include "svn_wc.h"
#include <apr_md5.h>
#include "client.h"
#include "private/svn_subr_private.h"
#include "private/svn_wc_private.h"
#include "svn_private_config.h"
/* ------------------------------------------------------------------ */
/* WC Modifications Editor.
*
* This editor applies incoming modifications onto the current working state
* of the working copy, to produce a new working state.
*
* Currently, it assumes the working state matches what the edit driver
* expects to find, and may throw an error if not.
*
* For simplicity, we apply incoming edits as they arrive, rather than
* queueing them up to apply in a batch.
*
* TODO:
* - tests
* - use for all existing scenarios ('svn add', 'svn propset', etc.)
* - Instead of 'root_dir_add' option, probably the driver should anchor
* at the parent dir.
* - Instead of 'ignore_mergeinfo' option, implement that as a wrapper.
* - Option to quietly accept changes that seem to be already applied
* in the versioned state and/or on disk.
* Consider 'svn add' which assumes items to be added are found on disk.
* - Notification.
*/
/* Everything we need to know about the edit session.
*/
struct edit_baton_t
{
const char *anchor_abspath;
svn_boolean_t manage_wc_write_lock;
const char *lock_root_abspath; /* the path locked, when locked */
/* True => 'open_root' method will act as 'add_directory' */
svn_boolean_t root_dir_add;
/* True => filter out any incoming svn:mergeinfo property changes */
svn_boolean_t ignore_mergeinfo_changes;
svn_ra_session_t *ra_session;
svn_wc_context_t *wc_ctx;
svn_client_ctx_t *ctx;
svn_wc_notify_func2_t notify_func;
void *notify_baton;
};
/* Everything we need to know about a directory that's open for edits.
*/
struct dir_baton_t
{
apr_pool_t *pool;
struct edit_baton_t *eb;
const char *local_abspath;
};
/* Join PATH onto ANCHOR_ABSPATH.
* Throw an error if the result is outside ANCHOR_ABSPATH.
*/
static svn_error_t *
get_path(const char **local_abspath_p,
const char *anchor_abspath,
const char *path,
apr_pool_t *result_pool)
{
svn_boolean_t under_root;
SVN_ERR(svn_dirent_is_under_root(&under_root, local_abspath_p,
anchor_abspath, path, result_pool));
if (! under_root)
{
return svn_error_createf(
SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL,
_("Path '%s' is not in the working copy"),
svn_dirent_local_style(path, result_pool));
}
return SVN_NO_ERROR;
}
/* Create a directory on disk and add it to version control,
* with no properties.
*/
static svn_error_t *
mkdir(const char *abspath,
struct edit_baton_t *eb,
apr_pool_t *scratch_pool)
{
SVN_ERR(svn_io_make_dir_recursively(abspath, scratch_pool));
SVN_ERR(svn_wc_add_from_disk3(eb->wc_ctx, abspath,
NULL /*properties*/,
TRUE /* skip checks */,
eb->notify_func, eb->notify_baton,
scratch_pool));
return SVN_NO_ERROR;
}
/* Prepare to open or add a directory: initialize a new dir baton.
*
* If PATH is "" and PB is null, it represents the root directory of
* the edit; otherwise PATH is not "" and PB is not null.
*/
static svn_error_t *
dir_open_or_add(struct dir_baton_t **child_dir_baton,
const char *path,
struct dir_baton_t *pb,
struct edit_baton_t *eb,
apr_pool_t *dir_pool)
{
struct dir_baton_t *db = apr_pcalloc(dir_pool, sizeof(*db));
db->pool = dir_pool;
db->eb = eb;
SVN_ERR(get_path(&db->local_abspath,
eb->anchor_abspath, path, dir_pool));
*child_dir_baton = db;
return SVN_NO_ERROR;
}
/* */
static svn_error_t *
release_write_lock(struct edit_baton_t *eb,
apr_pool_t *scratch_pool)
{
if (eb->lock_root_abspath)
{
SVN_ERR(svn_wc__release_write_lock(
eb->ctx->wc_ctx, eb->lock_root_abspath, scratch_pool));
eb->lock_root_abspath = NULL;
}
return SVN_NO_ERROR;
}
/* */
static apr_status_t
pool_cleanup_handler(void *root_baton)
{
struct dir_baton_t *db = root_baton;
struct edit_baton_t *eb = db->eb;
svn_error_clear(release_write_lock(eb, db->pool));
return APR_SUCCESS;
}
/* svn_delta_editor_t function */
static svn_error_t *
edit_open(void *edit_baton,
svn_revnum_t base_revision,
apr_pool_t *result_pool,
void **root_baton)
{
struct edit_baton_t *eb = edit_baton;
struct dir_baton_t *db;
SVN_ERR(dir_open_or_add(&db, "", NULL, eb, result_pool));
/* Acquire a WC write lock */
if (eb->manage_wc_write_lock)
{
apr_pool_cleanup_register(db->pool, db,
pool_cleanup_handler,
apr_pool_cleanup_null);
SVN_ERR(svn_wc__acquire_write_lock(&eb->lock_root_abspath,
eb->ctx->wc_ctx,
eb->anchor_abspath,
FALSE /*lock_anchor*/,
db->pool, db->pool));
}
if (eb->root_dir_add)
{
SVN_ERR(mkdir(db->local_abspath, eb, result_pool));
}
*root_baton = db;
return SVN_NO_ERROR;
}
/* svn_delta_editor_t function */
static svn_error_t *
edit_close_or_abort(void *edit_baton,
apr_pool_t *scratch_pool)
{
SVN_ERR(release_write_lock(edit_baton, scratch_pool));
return SVN_NO_ERROR;
}
/* */
static svn_error_t *
delete_entry(const char *path,
svn_revnum_t revision,
void *parent_baton,
apr_pool_t *scratch_pool)
{
struct dir_baton_t *pb = parent_baton;
struct edit_baton_t *eb = pb->eb;
const char *local_abspath;
SVN_ERR(get_path(&local_abspath,
eb->anchor_abspath, path, scratch_pool));
SVN_ERR(svn_wc_delete4(eb->wc_ctx, local_abspath,
FALSE /*keep_local*/,
TRUE /*delete_unversioned*/,
NULL, NULL, /*cancellation*/
eb->notify_func, eb->notify_baton, scratch_pool));
return SVN_NO_ERROR;
}
/* An svn_delta_editor_t function. */
static svn_error_t *
dir_open(const char *path,
void *parent_baton,
svn_revnum_t base_revision,
apr_pool_t *result_pool,
void **child_baton)
{
struct dir_baton_t *pb = parent_baton;
struct edit_baton_t *eb = pb->eb;
struct dir_baton_t *db;
SVN_ERR(dir_open_or_add(&db, path, pb, eb, result_pool));
*child_baton = db;
return SVN_NO_ERROR;
}
static svn_error_t *
dir_add(const char *path,
void *parent_baton,
const char *copyfrom_path,
svn_revnum_t copyfrom_revision,
apr_pool_t *result_pool,
void **child_baton)
{
struct dir_baton_t *pb = parent_baton;
struct edit_baton_t *eb = pb->eb;
struct dir_baton_t *db;
/* ### Our caller should be providing a scratch pool */
apr_pool_t *scratch_pool = svn_pool_create(result_pool);
SVN_ERR(dir_open_or_add(&db, path, pb, eb, result_pool));
if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_revision))
{
SVN_ERR(svn_client__repos_to_wc_copy_internal(NULL /*timestamp_sleep*/,
svn_node_dir,
copyfrom_path,
copyfrom_revision,
db->local_abspath,
db->eb->ra_session,
db->eb->ctx,
scratch_pool));
}
else
{
SVN_ERR(mkdir(db->local_abspath, eb, result_pool));
}
*child_baton = db;
svn_pool_destroy(scratch_pool);
return SVN_NO_ERROR;
}
static svn_error_t *
dir_change_prop(void *dir_baton,
const char *name,
const svn_string_t *value,
apr_pool_t *scratch_pool)
{
struct dir_baton_t *db = dir_baton;
struct edit_baton_t *eb = db->eb;
if (svn_property_kind2(name) != svn_prop_regular_kind
|| (eb->ignore_mergeinfo_changes && ! strcmp(name, SVN_PROP_MERGEINFO)))
{
/* We can't handle DAV, ENTRY and merge specific props here */
return SVN_NO_ERROR;
}
SVN_ERR(svn_wc_prop_set4(eb->wc_ctx, db->local_abspath, name, value,
svn_depth_empty, FALSE, NULL,
NULL, NULL, /* Cancellation */
NULL, NULL, /* Notification */
scratch_pool));
return SVN_NO_ERROR;
}
static svn_error_t *
dir_close(void *dir_baton,
apr_pool_t *scratch_pool)
{
return SVN_NO_ERROR;
}
/* Everything we need to know about a file that's open for edits.
*/
struct file_baton_t
{
apr_pool_t *pool;
struct edit_baton_t *eb;
const char *local_abspath;
/* fields for the transfer of text changes */
const char *writing_file;
unsigned char digest[APR_MD5_DIGESTSIZE]; /* MD5 digest of new fulltext */
svn_stream_t *wc_file_read_stream, *tmp_file_write_stream;
const char *tmp_path;
};
/* Create a new file on disk and add it to version control.
*
* The file is empty and has no properties.
*/
static svn_error_t *
mkfile(const char *abspath,
struct edit_baton_t *eb,
apr_pool_t *scratch_pool)
{
SVN_ERR(svn_io_file_create_empty(abspath, scratch_pool));
SVN_ERR(svn_wc_add_from_disk3(eb->wc_ctx, abspath,
NULL /*properties*/,
TRUE /* skip checks */,
eb->notify_func, eb->notify_baton,
scratch_pool));
return SVN_NO_ERROR;
}
/* */
static svn_error_t *
file_open_or_add(const char *path,
void *parent_baton,
struct file_baton_t **file_baton,
apr_pool_t *file_pool)
{
struct dir_baton_t *pb = parent_baton;
struct edit_baton_t *eb = pb->eb;
struct file_baton_t *fb = apr_pcalloc(file_pool, sizeof(*fb));
fb->pool = file_pool;
fb->eb = eb;
SVN_ERR(get_path(&fb->local_abspath,
eb->anchor_abspath, path, fb->pool));
*file_baton = fb;
return SVN_NO_ERROR;
}
static svn_error_t *
file_open(const char *path,
void *parent_baton,
svn_revnum_t base_revision,
apr_pool_t *result_pool,
void **file_baton)
{
struct file_baton_t *fb;
SVN_ERR(file_open_or_add(path, parent_baton, &fb, result_pool));
*file_baton = fb;
return SVN_NO_ERROR;
}
static svn_error_t *
file_add(const char *path,
void *parent_baton,
const char *copyfrom_path,
svn_revnum_t copyfrom_revision,
apr_pool_t *result_pool,
void **file_baton)
{
struct file_baton_t *fb;
SVN_ERR(file_open_or_add(path, parent_baton, &fb, result_pool));
if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_revision))
{
SVN_ERR(svn_client__repos_to_wc_copy_internal(NULL /*timestamp_sleep*/,
svn_node_file,
copyfrom_path,
copyfrom_revision,
fb->local_abspath,
fb->eb->ra_session,
fb->eb->ctx, fb->pool));
}
else
{
SVN_ERR(mkfile(fb->local_abspath, fb->eb, result_pool));
}
*file_baton = fb;
return SVN_NO_ERROR;
}
static svn_error_t *
file_change_prop(void *file_baton,
const char *name,
const svn_string_t *value,
apr_pool_t *scratch_pool)
{
struct file_baton_t *fb = file_baton;
struct edit_baton_t *eb = fb->eb;
if (svn_property_kind2(name) != svn_prop_regular_kind
|| (eb->ignore_mergeinfo_changes && ! strcmp(name, SVN_PROP_MERGEINFO)))
{
/* We can't handle DAV, ENTRY and merge specific props here */
return SVN_NO_ERROR;
}
SVN_ERR(svn_wc_prop_set4(eb->wc_ctx, fb->local_abspath, name, value,
svn_depth_empty, FALSE, NULL,
NULL, NULL, /* Cancellation */
NULL, NULL, /* Notification */
scratch_pool));
return SVN_NO_ERROR;
}
static svn_error_t *
file_textdelta(void *file_baton,
const char *base_checksum,
apr_pool_t *result_pool,
svn_txdelta_window_handler_t *handler,
void **handler_baton)
{
struct file_baton_t *fb = file_baton;
const char *target_dir = svn_dirent_dirname(fb->local_abspath, fb->pool);
svn_error_t *err;
SVN_ERR_ASSERT(! fb->writing_file);
err = svn_stream_open_readonly(&fb->wc_file_read_stream, fb->local_abspath,
fb->pool, fb->pool);
if (err && APR_STATUS_IS_ENOENT(err->apr_err))
{
svn_error_clear(err);
fb->wc_file_read_stream = svn_stream_empty(fb->pool);
}
else
SVN_ERR(err);
SVN_ERR(svn_stream_open_unique(&fb->tmp_file_write_stream, &fb->writing_file,
target_dir, svn_io_file_del_none,
fb->pool, fb->pool));
svn_txdelta_apply(fb->wc_file_read_stream,
fb->tmp_file_write_stream,
fb->digest,
fb->local_abspath,
fb->pool,
/* Provide the handler directly */
handler, handler_baton);
return SVN_NO_ERROR;
}
static svn_error_t *
file_close(void *file_baton,
const char *text_checksum,
apr_pool_t *scratch_pool)
{
struct file_baton_t *fb = file_baton;
/* If we have text changes, write them to disk */
if (fb->writing_file)
{
SVN_ERR(svn_stream_close(fb->wc_file_read_stream));
SVN_ERR(svn_io_file_rename2(fb->writing_file, fb->local_abspath,
FALSE /*flush*/, scratch_pool));
}
if (text_checksum)
{
svn_checksum_t *expected_checksum;
svn_checksum_t *actual_checksum;
SVN_ERR(svn_checksum_parse_hex(&expected_checksum, svn_checksum_md5,
text_checksum, fb->pool));
actual_checksum = svn_checksum__from_digest_md5(fb->digest, fb->pool);
if (! svn_checksum_match(expected_checksum, actual_checksum))
return svn_error_trace(
svn_checksum_mismatch_err(expected_checksum,
actual_checksum,
fb->pool,
_("Checksum mismatch for '%s'"),
svn_dirent_local_style(
fb->local_abspath,
fb->pool)));
}
return SVN_NO_ERROR;
}
svn_error_t *
svn_client__wc_editor_internal(const svn_delta_editor_t **editor_p,
void **edit_baton_p,
const char *dst_abspath,
svn_boolean_t root_dir_add,
svn_boolean_t ignore_mergeinfo_changes,
svn_boolean_t manage_wc_write_lock,
svn_wc_notify_func2_t notify_func,
void *notify_baton,
svn_ra_session_t *ra_session,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool)
{
svn_delta_editor_t *editor = svn_delta_default_editor(result_pool);
struct edit_baton_t *eb = apr_pcalloc(result_pool, sizeof(*eb));
eb->anchor_abspath = apr_pstrdup(result_pool, dst_abspath);
eb->manage_wc_write_lock = manage_wc_write_lock;
eb->lock_root_abspath = NULL;
eb->root_dir_add = root_dir_add;
eb->ignore_mergeinfo_changes = ignore_mergeinfo_changes;
eb->ra_session = ra_session;
eb->wc_ctx = ctx->wc_ctx;
eb->ctx = ctx;
eb->notify_func = notify_func;
eb->notify_baton = notify_baton;
editor->open_root = edit_open;
editor->close_edit = edit_close_or_abort;
editor->abort_edit = edit_close_or_abort;
editor->delete_entry = delete_entry;
editor->open_directory = dir_open;
editor->add_directory = dir_add;
editor->change_dir_prop = dir_change_prop;
editor->close_directory = dir_close;
editor->open_file = file_open;
editor->add_file = file_add;
editor->change_file_prop = file_change_prop;
editor->apply_textdelta = file_textdelta;
editor->close_file = file_close;
*editor_p = editor;
*edit_baton_p = eb;
return SVN_NO_ERROR;
}
svn_error_t *
svn_client__wc_editor(const svn_delta_editor_t **editor_p,
void **edit_baton_p,
const char *dst_abspath,
svn_wc_notify_func2_t notify_func,
void *notify_baton,
svn_ra_session_t *ra_session,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool)
{
SVN_ERR(svn_client__wc_editor_internal(editor_p, edit_baton_p,
dst_abspath,
FALSE /*root_dir_add*/,
FALSE /*ignore_mergeinfo_changes*/,
TRUE /*manage_wc_write_lock*/,
notify_func, notify_baton,
ra_session,
ctx, result_pool));
return SVN_NO_ERROR;
}
svn_error_t *
svn_client__wc_copy_mods(const char *src_wc_abspath,
const char *dst_wc_abspath,
svn_wc_notify_func2_t notify_func,
void *notify_baton,
svn_client_ctx_t *ctx,
apr_pool_t *scratch_pool)
{
svn_client__pathrev_t *base;
const char *dst_wc_url;
svn_ra_session_t *ra_session;
const svn_delta_editor_t *editor;
void *edit_baton;
apr_array_header_t *src_targets = apr_array_make(scratch_pool, 1,
sizeof(char *));
/* We'll need an RA session to obtain the base of any copies */
SVN_ERR(svn_client__wc_node_get_base(&base,
src_wc_abspath, ctx->wc_ctx,
scratch_pool, scratch_pool));
dst_wc_url = base->url;
SVN_ERR(svn_client_open_ra_session2(&ra_session,
dst_wc_url, dst_wc_abspath,
ctx, scratch_pool, scratch_pool));
SVN_ERR(svn_client__wc_editor(&editor, &edit_baton,
dst_wc_abspath,
NULL, NULL, /*notification*/
ra_session, ctx, scratch_pool));
APR_ARRAY_PUSH(src_targets, const char *) = src_wc_abspath;
SVN_ERR(svn_client__wc_replay(src_wc_abspath,
src_targets, svn_depth_infinity, NULL,
editor, edit_baton,
notify_func, notify_baton,
ctx, scratch_pool));
return SVN_NO_ERROR;
}