/* * 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 #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 #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; }