diff options
Diffstat (limited to 'subversion/libsvn_client/export.c')
-rw-r--r-- | subversion/libsvn_client/export.c | 1589 |
1 files changed, 1589 insertions, 0 deletions
diff --git a/subversion/libsvn_client/export.c b/subversion/libsvn_client/export.c new file mode 100644 index 0000000..d6022ed --- /dev/null +++ b/subversion/libsvn_client/export.c @@ -0,0 +1,1589 @@ +/* + * export.c: export a tree. + * + * ==================================================================== + * 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 <apr_file_io.h> +#include <apr_md5.h> +#include "svn_types.h" +#include "svn_client.h" +#include "svn_string.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_hash.h" +#include "svn_path.h" +#include "svn_pools.h" +#include "svn_subst.h" +#include "svn_time.h" +#include "svn_props.h" +#include "client.h" + +#include "svn_private_config.h" +#include "private/svn_subr_private.h" +#include "private/svn_delta_private.h" +#include "private/svn_wc_private.h" + +#ifndef ENABLE_EV2_IMPL +#define ENABLE_EV2_IMPL 0 +#endif + + +/*** Code. ***/ + +/* Add EXTERNALS_PROP_VAL for the export destination path PATH to + TRAVERSAL_INFO. */ +static svn_error_t * +add_externals(apr_hash_t *externals, + const char *path, + const svn_string_t *externals_prop_val) +{ + apr_pool_t *pool = apr_hash_pool_get(externals); + const char *local_abspath; + + if (! externals_prop_val) + return SVN_NO_ERROR; + + SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, pool)); + + svn_hash_sets(externals, local_abspath, + apr_pstrmemdup(pool, externals_prop_val->data, + externals_prop_val->len)); + + return SVN_NO_ERROR; +} + +/* Helper function that gets the eol style and optionally overrides the + EOL marker for files marked as native with the EOL marker matching + the string specified in requested_value which is of the same format + as the svn:eol-style property values. */ +static svn_error_t * +get_eol_style(svn_subst_eol_style_t *style, + const char **eol, + const char *value, + const char *requested_value) +{ + svn_subst_eol_style_from_value(style, eol, value); + if (requested_value && *style == svn_subst_eol_style_native) + { + svn_subst_eol_style_t requested_style; + const char *requested_eol; + + svn_subst_eol_style_from_value(&requested_style, &requested_eol, + requested_value); + + if (requested_style == svn_subst_eol_style_fixed) + *eol = requested_eol; + else + return svn_error_createf(SVN_ERR_IO_UNKNOWN_EOL, NULL, + _("'%s' is not a valid EOL value"), + requested_value); + } + return SVN_NO_ERROR; +} + +/* If *APPENDABLE_DIRENT_P represents an existing directory, then append + * to it the basename of BASENAME_OF and return the result in + * *APPENDABLE_DIRENT_P. The kind of BASENAME_OF is either dirent or uri, + * as given by IS_URI. + */ +static svn_error_t * +append_basename_if_dir(const char **appendable_dirent_p, + const char *basename_of, + svn_boolean_t is_uri, + apr_pool_t *pool) +{ + svn_node_kind_t local_kind; + SVN_ERR(svn_io_check_resolved_path(*appendable_dirent_p, &local_kind, pool)); + if (local_kind == svn_node_dir) + { + const char *base_name; + + if (is_uri) + base_name = svn_uri_basename(basename_of, pool); + else + base_name = svn_dirent_basename(basename_of, NULL); + + *appendable_dirent_p = svn_dirent_join(*appendable_dirent_p, + base_name, pool); + } + + return SVN_NO_ERROR; +} + +/* Make an unversioned copy of the versioned file at FROM_ABSPATH. Copy it + * to the destination path TO_ABSPATH. + * + * If REVISION is svn_opt_revision_working, copy the working version, + * otherwise copy the base version. + * + * Expand the file's keywords according to the source file's 'svn:keywords' + * property, if present. If copying a locally modified working version, + * append 'M' to the revision number and use '(local)' for the author. + * + * Translate the file's line endings according to the source file's + * 'svn:eol-style' property, if present. If NATIVE_EOL is not NULL, use it + * in place of the native EOL style. Throw an error if the source file has + * inconsistent line endings and EOL translation is attempted. + * + * Set the destination file's modification time to the source file's + * modification time if copying the working version and the working version + * is locally modified; otherwise set it to the versioned file's last + * changed time. + * + * Set the destination file's 'executable' flag according to the source + * file's 'svn:executable' property. + */ + +/* baton for export_node */ +struct export_info_baton +{ + const char *to_path; + const svn_opt_revision_t *revision; + svn_boolean_t ignore_keywords; + svn_boolean_t overwrite; + svn_wc_context_t *wc_ctx; + const char *native_eol; + svn_wc_notify_func2_t notify_func; + void *notify_baton; + const char *origin_abspath; + svn_boolean_t exported; +}; + +/* Export a file or directory. Implements svn_wc_status_func4_t */ +static svn_error_t * +export_node(void *baton, + const char *local_abspath, + const svn_wc_status3_t *status, + apr_pool_t *scratch_pool) +{ + struct export_info_baton *eib = baton; + svn_wc_context_t *wc_ctx = eib->wc_ctx; + apr_hash_t *kw = NULL; + svn_subst_eol_style_t style; + apr_hash_t *props; + svn_string_t *eol_style, *keywords, *executable, *special; + const char *eol = NULL; + svn_boolean_t local_mod = FALSE; + apr_time_t tm; + svn_stream_t *source; + svn_stream_t *dst_stream; + const char *dst_tmp; + svn_error_t *err; + + const char *to_abspath = svn_dirent_join( + eib->to_path, + svn_dirent_skip_ancestor(eib->origin_abspath, + local_abspath), + scratch_pool); + + eib->exported = TRUE; + + /* Don't export 'deleted' files and directories unless it's a + revision other than WORKING. These files and directories + don't really exist in WORKING. */ + if (eib->revision->kind == svn_opt_revision_working + && status->node_status == svn_wc_status_deleted) + return SVN_NO_ERROR; + + if (status->kind == svn_node_dir) + { + apr_fileperms_t perm = APR_OS_DEFAULT; + + /* Try to make the new directory. If this fails because the + directory already exists, check our FORCE flag to see if we + care. */ + + /* Keep the source directory's permissions if applicable. + Skip retrieving the umask on windows. Apr does not implement setting + filesystem privileges on Windows. + Retrieving the file permissions with APR_FINFO_PROT | APR_FINFO_OWNER + is documented to be 'incredibly expensive' */ +#ifndef WIN32 + if (eib->revision->kind == svn_opt_revision_working) + { + apr_finfo_t finfo; + SVN_ERR(svn_io_stat(&finfo, local_abspath, APR_FINFO_PROT, + scratch_pool)); + perm = finfo.protection; + } +#endif + err = svn_io_dir_make(to_abspath, perm, scratch_pool); + if (err) + { + if (! APR_STATUS_IS_EEXIST(err->apr_err)) + return svn_error_trace(err); + if (! eib->overwrite) + SVN_ERR_W(err, _("Destination directory exists, and will not be " + "overwritten unless forced")); + else + svn_error_clear(err); + } + + if (eib->notify_func + && (strcmp(eib->origin_abspath, local_abspath) != 0)) + { + svn_wc_notify_t *notify = + svn_wc_create_notify(to_abspath, + svn_wc_notify_update_add, scratch_pool); + + notify->kind = svn_node_dir; + (eib->notify_func)(eib->notify_baton, notify, scratch_pool); + } + + return SVN_NO_ERROR; + } + else if (status->kind != svn_node_file) + { + if (strcmp(eib->origin_abspath, local_abspath) != 0) + return SVN_NO_ERROR; + + return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL, + _("The node '%s' was not found."), + svn_dirent_local_style(local_abspath, + scratch_pool)); + } + + if (status->file_external) + return SVN_NO_ERROR; + + /* Produce overwrite errors for the export root */ + if (strcmp(local_abspath, eib->origin_abspath) == 0) + { + svn_node_kind_t to_kind; + + SVN_ERR(svn_io_check_path(to_abspath, &to_kind, scratch_pool)); + + if ((to_kind == svn_node_file || to_kind == svn_node_unknown) + && !eib->overwrite) + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + _("Destination file '%s' exists, and " + "will not be overwritten unless forced"), + svn_dirent_local_style(to_abspath, + scratch_pool)); + else if (to_kind == svn_node_dir) + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + _("Destination '%s' exists. Cannot " + "overwrite directory with non-directory"), + svn_dirent_local_style(to_abspath, + scratch_pool)); + } + + if (eib->revision->kind != svn_opt_revision_working) + { + /* Only export 'added' files when the revision is WORKING. This is not + WORKING, so skip the 'added' files, since they didn't exist + in the BASE revision and don't have an associated text-base. + + 'replaced' files are technically the same as 'added' files. + ### TODO: Handle replaced nodes properly. + ### svn_opt_revision_base refers to the "new" + ### base of the node. That means, if a node is locally + ### replaced, export skips this node, as if it was locally + ### added, because svn_opt_revision_base refers to the base + ### of the added node, not to the node that was deleted. + ### In contrast, when the node is copied-here or moved-here, + ### the copy/move source's content will be exported. + ### It is currently not possible to export the revert-base + ### when a node is locally replaced. We need a new + ### svn_opt_revision_ enum value for proper distinction + ### between revert-base and commit-base. + + Copied-/moved-here nodes have a base, so export both added and + replaced files when they involve a copy-/move-here. + + We get all this for free from evaluating SOURCE == NULL: + */ + SVN_ERR(svn_wc_get_pristine_contents2(&source, wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + if (source == NULL) + return SVN_NO_ERROR; + + SVN_ERR(svn_wc_get_pristine_props(&props, wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + } + else + { + /* ### hmm. this isn't always a specialfile. this will simply open + ### the file readonly if it is a regular file. */ + SVN_ERR(svn_subst_read_specialfile(&source, local_abspath, scratch_pool, + scratch_pool)); + + SVN_ERR(svn_wc_prop_list2(&props, wc_ctx, local_abspath, scratch_pool, + scratch_pool)); + if (status->node_status != svn_wc_status_normal) + local_mod = TRUE; + } + + /* We can early-exit if we're creating a special file. */ + special = svn_hash_gets(props, SVN_PROP_SPECIAL); + if (special != NULL) + { + /* Create the destination as a special file, and copy the source + details into the destination stream. */ + /* ### And forget the notification */ + SVN_ERR(svn_subst_create_specialfile(&dst_stream, to_abspath, + scratch_pool, scratch_pool)); + return svn_error_trace( + svn_stream_copy3(source, dst_stream, NULL, NULL, scratch_pool)); + } + + + eol_style = svn_hash_gets(props, SVN_PROP_EOL_STYLE); + keywords = svn_hash_gets(props, SVN_PROP_KEYWORDS); + executable = svn_hash_gets(props, SVN_PROP_EXECUTABLE); + + if (eol_style) + SVN_ERR(get_eol_style(&style, &eol, eol_style->data, eib->native_eol)); + + if (local_mod) + { + /* Use the modified time from the working copy of + the file */ + SVN_ERR(svn_io_file_affected_time(&tm, local_abspath, scratch_pool)); + } + else + { + tm = status->changed_date; + } + + if (keywords) + { + svn_revnum_t changed_rev = status->changed_rev; + const char *suffix; + const char *url = svn_path_url_add_component2(status->repos_root_url, + status->repos_relpath, + scratch_pool); + const char *author = status->changed_author; + if (local_mod) + { + /* For locally modified files, we'll append an 'M' + to the revision number, and set the author to + "(local)" since we can't always determine the + current user's username */ + suffix = "M"; + author = _("(local)"); + } + else + { + suffix = ""; + } + + SVN_ERR(svn_subst_build_keywords3(&kw, keywords->data, + apr_psprintf(scratch_pool, "%ld%s", + changed_rev, suffix), + url, status->repos_root_url, tm, + author, scratch_pool)); + } + + /* For atomicity, we translate to a tmp file and then rename the tmp file + over the real destination. */ + SVN_ERR(svn_stream_open_unique(&dst_stream, &dst_tmp, + svn_dirent_dirname(to_abspath, scratch_pool), + svn_io_file_del_none, scratch_pool, + scratch_pool)); + + /* If some translation is needed, then wrap the output stream (this is + more efficient than wrapping the input). */ + if (eol || (kw && (apr_hash_count(kw) > 0))) + dst_stream = svn_subst_stream_translated(dst_stream, + eol, + FALSE /* repair */, + kw, + ! eib->ignore_keywords /* expand */, + scratch_pool); + + /* ###: use cancel func/baton in place of NULL/NULL below. */ + err = svn_stream_copy3(source, dst_stream, NULL, NULL, scratch_pool); + + if (!err && executable) + err = svn_io_set_file_executable(dst_tmp, TRUE, FALSE, scratch_pool); + + if (!err) + err = svn_io_set_file_affected_time(tm, dst_tmp, scratch_pool); + + if (err) + return svn_error_compose_create(err, svn_io_remove_file2(dst_tmp, FALSE, + scratch_pool)); + + /* Now that dst_tmp contains the translated data, do the atomic rename. */ + SVN_ERR(svn_io_file_rename(dst_tmp, to_abspath, scratch_pool)); + + if (eib->notify_func) + { + svn_wc_notify_t *notify = svn_wc_create_notify(to_abspath, + svn_wc_notify_update_add, scratch_pool); + notify->kind = svn_node_file; + (eib->notify_func)(eib->notify_baton, notify, scratch_pool); + } + + return SVN_NO_ERROR; +} + +/* Abstraction of open_root. + * + * Create PATH if it does not exist and is not obstructed, and invoke + * NOTIFY_FUNC with NOTIFY_BATON on PATH. + * + * If PATH exists but is a file, then error with SVN_ERR_WC_NOT_WORKING_COPY. + * + * If PATH is a already a directory, then error with + * SVN_ERR_WC_OBSTRUCTED_UPDATE, unless FORCE, in which case just + * export into PATH with no error. + */ +static svn_error_t * +open_root_internal(const char *path, + svn_boolean_t force, + svn_wc_notify_func2_t notify_func, + void *notify_baton, + apr_pool_t *pool) +{ + svn_node_kind_t kind; + + SVN_ERR(svn_io_check_path(path, &kind, pool)); + if (kind == svn_node_none) + SVN_ERR(svn_io_make_dir_recursively(path, pool)); + else if (kind == svn_node_file) + return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL, + _("'%s' exists and is not a directory"), + svn_dirent_local_style(path, pool)); + else if ((kind != svn_node_dir) || (! force)) + return svn_error_createf(SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, + _("'%s' already exists"), + svn_dirent_local_style(path, pool)); + + if (notify_func) + { + svn_wc_notify_t *notify = svn_wc_create_notify(path, + svn_wc_notify_update_add, + pool); + notify->kind = svn_node_dir; + (*notify_func)(notify_baton, notify, pool); + } + + return SVN_NO_ERROR; +} + + +/* ---------------------------------------------------------------------- */ + + +/*** A dedicated 'export' editor, which does no .svn/ accounting. ***/ + + +struct edit_baton +{ + const char *repos_root_url; + const char *root_path; + const char *root_url; + svn_boolean_t force; + svn_revnum_t *target_revision; + apr_hash_t *externals; + const char *native_eol; + svn_boolean_t ignore_keywords; + + svn_cancel_func_t cancel_func; + void *cancel_baton; + svn_wc_notify_func2_t notify_func; + void *notify_baton; +}; + + +struct dir_baton +{ + struct edit_baton *edit_baton; + const char *path; +}; + + +struct file_baton +{ + struct edit_baton *edit_baton; + + const char *path; + const char *tmppath; + + /* We need to keep this around so we can explicitly close it in close_file, + thus flushing its output to disk so we can copy and translate it. */ + svn_stream_t *tmp_stream; + + /* The MD5 digest of the file's fulltext. This is all zeros until + the last textdelta window handler call returns. */ + unsigned char text_digest[APR_MD5_DIGESTSIZE]; + + /* The three svn: properties we might actually care about. */ + const svn_string_t *eol_style_val; + const svn_string_t *keywords_val; + const svn_string_t *executable_val; + svn_boolean_t special; + + /* Any keyword vals to be substituted */ + const char *revision; + const char *url; + const char *repos_root_url; + const char *author; + apr_time_t date; + + /* Pool associated with this baton. */ + apr_pool_t *pool; +}; + + +struct handler_baton +{ + svn_txdelta_window_handler_t apply_handler; + void *apply_baton; + apr_pool_t *pool; + const char *tmppath; +}; + + +static svn_error_t * +set_target_revision(void *edit_baton, + svn_revnum_t target_revision, + apr_pool_t *pool) +{ + struct edit_baton *eb = edit_baton; + + /* Stashing a target_revision in the baton */ + *(eb->target_revision) = target_revision; + return SVN_NO_ERROR; +} + + + +/* Just ensure that the main export directory exists. */ +static svn_error_t * +open_root(void *edit_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **root_baton) +{ + struct edit_baton *eb = edit_baton; + struct dir_baton *db = apr_pcalloc(pool, sizeof(*db)); + + SVN_ERR(open_root_internal(eb->root_path, eb->force, + eb->notify_func, eb->notify_baton, pool)); + + /* Build our dir baton. */ + db->path = eb->root_path; + db->edit_baton = eb; + *root_baton = db; + + return SVN_NO_ERROR; +} + + +/* Ensure the directory exists, and send feedback. */ +static svn_error_t * +add_directory(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_revision, + apr_pool_t *pool, + void **baton) +{ + struct dir_baton *pb = parent_baton; + struct dir_baton *db = apr_pcalloc(pool, sizeof(*db)); + struct edit_baton *eb = pb->edit_baton; + const char *full_path = svn_dirent_join(eb->root_path, path, pool); + svn_node_kind_t kind; + + SVN_ERR(svn_io_check_path(full_path, &kind, pool)); + if (kind == svn_node_none) + SVN_ERR(svn_io_dir_make(full_path, APR_OS_DEFAULT, pool)); + else if (kind == svn_node_file) + return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL, + _("'%s' exists and is not a directory"), + svn_dirent_local_style(full_path, pool)); + else if (! (kind == svn_node_dir && eb->force)) + return svn_error_createf(SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, + _("'%s' already exists"), + svn_dirent_local_style(full_path, pool)); + + if (eb->notify_func) + { + svn_wc_notify_t *notify = svn_wc_create_notify(full_path, + svn_wc_notify_update_add, + pool); + notify->kind = svn_node_dir; + (*eb->notify_func)(eb->notify_baton, notify, pool); + } + + /* Build our dir baton. */ + db->path = full_path; + db->edit_baton = eb; + *baton = db; + + return SVN_NO_ERROR; +} + + +/* Build a file baton. */ +static svn_error_t * +add_file(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_revision, + apr_pool_t *pool, + void **baton) +{ + struct dir_baton *pb = parent_baton; + struct edit_baton *eb = pb->edit_baton; + struct file_baton *fb = apr_pcalloc(pool, sizeof(*fb)); + const char *full_path = svn_dirent_join(eb->root_path, path, pool); + + /* PATH is not canonicalized, i.e. it may still contain spaces etc. + * but EB->root_url is. */ + const char *full_url = svn_path_url_add_component2(eb->root_url, + path, + pool); + + fb->edit_baton = eb; + fb->path = full_path; + fb->url = full_url; + fb->repos_root_url = eb->repos_root_url; + fb->pool = pool; + + *baton = fb; + return SVN_NO_ERROR; +} + + +static svn_error_t * +window_handler(svn_txdelta_window_t *window, void *baton) +{ + struct handler_baton *hb = baton; + svn_error_t *err; + + err = hb->apply_handler(window, hb->apply_baton); + if (err) + { + /* We failed to apply the patch; clean up the temporary file. */ + err = svn_error_compose_create( + err, + svn_io_remove_file2(hb->tmppath, TRUE, hb->pool)); + } + + return svn_error_trace(err); +} + + + +/* Write incoming data into the tmpfile stream */ +static svn_error_t * +apply_textdelta(void *file_baton, + const char *base_checksum, + apr_pool_t *pool, + svn_txdelta_window_handler_t *handler, + void **handler_baton) +{ + struct file_baton *fb = file_baton; + struct handler_baton *hb = apr_palloc(pool, sizeof(*hb)); + + /* Create a temporary file in the same directory as the file. We're going + to rename the thing into place when we're done. */ + SVN_ERR(svn_stream_open_unique(&fb->tmp_stream, &fb->tmppath, + svn_dirent_dirname(fb->path, pool), + svn_io_file_del_none, fb->pool, fb->pool)); + + hb->pool = pool; + hb->tmppath = fb->tmppath; + + /* svn_txdelta_apply() closes the stream, but we want to close it in the + close_file() function, so disown it here. */ + /* ### contrast to when we call svn_ra_get_file() which does NOT close the + ### tmp_stream. we *should* be much more consistent! */ + svn_txdelta_apply(svn_stream_empty(pool), + svn_stream_disown(fb->tmp_stream, pool), + fb->text_digest, NULL, pool, + &hb->apply_handler, &hb->apply_baton); + + *handler_baton = hb; + *handler = window_handler; + return SVN_NO_ERROR; +} + + +static svn_error_t * +change_file_prop(void *file_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + struct file_baton *fb = file_baton; + + if (! value) + return SVN_NO_ERROR; + + /* Store only the magic three properties. */ + if (strcmp(name, SVN_PROP_EOL_STYLE) == 0) + fb->eol_style_val = svn_string_dup(value, fb->pool); + + else if (! fb->edit_baton->ignore_keywords && + strcmp(name, SVN_PROP_KEYWORDS) == 0) + fb->keywords_val = svn_string_dup(value, fb->pool); + + else if (strcmp(name, SVN_PROP_EXECUTABLE) == 0) + fb->executable_val = svn_string_dup(value, fb->pool); + + /* Try to fill out the baton's keywords-structure too. */ + else if (strcmp(name, SVN_PROP_ENTRY_COMMITTED_REV) == 0) + fb->revision = apr_pstrdup(fb->pool, value->data); + + else if (strcmp(name, SVN_PROP_ENTRY_COMMITTED_DATE) == 0) + SVN_ERR(svn_time_from_cstring(&fb->date, value->data, fb->pool)); + + else if (strcmp(name, SVN_PROP_ENTRY_LAST_AUTHOR) == 0) + fb->author = apr_pstrdup(fb->pool, value->data); + + else if (strcmp(name, SVN_PROP_SPECIAL) == 0) + fb->special = TRUE; + + return SVN_NO_ERROR; +} + + +static svn_error_t * +change_dir_prop(void *dir_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + struct dir_baton *db = dir_baton; + struct edit_baton *eb = db->edit_baton; + + if (value && (strcmp(name, SVN_PROP_EXTERNALS) == 0)) + SVN_ERR(add_externals(eb->externals, db->path, value)); + + return SVN_NO_ERROR; +} + + +/* Move the tmpfile to file, and send feedback. */ +static svn_error_t * +close_file(void *file_baton, + const char *text_digest, + apr_pool_t *pool) +{ + struct file_baton *fb = file_baton; + struct edit_baton *eb = fb->edit_baton; + svn_checksum_t *text_checksum; + svn_checksum_t *actual_checksum; + + /* Was a txdelta even sent? */ + if (! fb->tmppath) + return SVN_NO_ERROR; + + SVN_ERR(svn_stream_close(fb->tmp_stream)); + + SVN_ERR(svn_checksum_parse_hex(&text_checksum, svn_checksum_md5, text_digest, + pool)); + actual_checksum = svn_checksum__from_digest_md5(fb->text_digest, pool); + + /* Note that text_digest can be NULL when talking to certain repositories. + In that case text_checksum will be NULL and the following match code + will note that the checksums match */ + if (!svn_checksum_match(text_checksum, actual_checksum)) + return svn_checksum_mismatch_err(text_checksum, actual_checksum, pool, + _("Checksum mismatch for '%s'"), + svn_dirent_local_style(fb->path, pool)); + + if ((! fb->eol_style_val) && (! fb->keywords_val) && (! fb->special)) + { + SVN_ERR(svn_io_file_rename(fb->tmppath, fb->path, pool)); + } + else + { + svn_subst_eol_style_t style; + const char *eol = NULL; + svn_boolean_t repair = FALSE; + apr_hash_t *final_kw = NULL; + + if (fb->eol_style_val) + { + SVN_ERR(get_eol_style(&style, &eol, fb->eol_style_val->data, + eb->native_eol)); + repair = TRUE; + } + + if (fb->keywords_val) + SVN_ERR(svn_subst_build_keywords3(&final_kw, fb->keywords_val->data, + fb->revision, fb->url, + fb->repos_root_url, fb->date, + fb->author, pool)); + + SVN_ERR(svn_subst_copy_and_translate4(fb->tmppath, fb->path, + eol, repair, final_kw, + TRUE, /* expand */ + fb->special, + eb->cancel_func, eb->cancel_baton, + pool)); + + SVN_ERR(svn_io_remove_file2(fb->tmppath, FALSE, pool)); + } + + if (fb->executable_val) + SVN_ERR(svn_io_set_file_executable(fb->path, TRUE, FALSE, pool)); + + if (fb->date && (! fb->special)) + SVN_ERR(svn_io_set_file_affected_time(fb->date, fb->path, pool)); + + if (fb->edit_baton->notify_func) + { + svn_wc_notify_t *notify = svn_wc_create_notify(fb->path, + svn_wc_notify_update_add, + pool); + notify->kind = svn_node_file; + (*fb->edit_baton->notify_func)(fb->edit_baton->notify_baton, notify, + pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_props_func(apr_hash_t **props, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + /* Always use empty props, since the node won't have pre-existing props + (This is an export, remember?) */ + *props = apr_hash_make(result_pool); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_base_func(const char **filename, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + /* An export always gets text against the empty stream (i.e, full texts). */ + *filename = NULL; + + return SVN_NO_ERROR; +} + +static svn_error_t * +get_editor_ev1(const svn_delta_editor_t **export_editor, + void **edit_baton, + struct edit_baton *eb, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_delta_editor_t *editor = svn_delta_default_editor(result_pool); + + editor->set_target_revision = set_target_revision; + editor->open_root = open_root; + editor->add_directory = add_directory; + editor->add_file = add_file; + editor->apply_textdelta = apply_textdelta; + editor->close_file = close_file; + editor->change_file_prop = change_file_prop; + editor->change_dir_prop = change_dir_prop; + + SVN_ERR(svn_delta_get_cancellation_editor(ctx->cancel_func, + ctx->cancel_baton, + editor, + eb, + export_editor, + edit_baton, + result_pool)); + + return SVN_NO_ERROR; +} + + +/*** The Ev2 Implementation ***/ + +static svn_error_t * +add_file_ev2(void *baton, + const char *relpath, + const svn_checksum_t *checksum, + svn_stream_t *contents, + apr_hash_t *props, + svn_revnum_t replaces_rev, + apr_pool_t *scratch_pool) +{ + struct edit_baton *eb = baton; + const char *full_path = svn_dirent_join(eb->root_path, relpath, + scratch_pool); + /* RELPATH is not canonicalized, i.e. it may still contain spaces etc. + * but EB->root_url is. */ + const char *full_url = svn_path_url_add_component2(eb->root_url, + relpath, + scratch_pool); + const svn_string_t *val; + /* The four svn: properties we might actually care about. */ + const svn_string_t *eol_style_val = NULL; + const svn_string_t *keywords_val = NULL; + const svn_string_t *executable_val = NULL; + svn_boolean_t special = FALSE; + /* Any keyword vals to be substituted */ + const char *revision = NULL; + const char *author = NULL; + apr_time_t date = 0; + + /* Look at any properties for additional information. */ + if ( (val = svn_hash_gets(props, SVN_PROP_EOL_STYLE)) ) + eol_style_val = val; + + if ( !eb->ignore_keywords && (val = svn_hash_gets(props, SVN_PROP_KEYWORDS)) ) + keywords_val = val; + + if ( (val = svn_hash_gets(props, SVN_PROP_EXECUTABLE)) ) + executable_val = val; + + /* Try to fill out the baton's keywords-structure too. */ + if ( (val = svn_hash_gets(props, SVN_PROP_ENTRY_COMMITTED_REV)) ) + revision = val->data; + + if ( (val = svn_hash_gets(props, SVN_PROP_ENTRY_COMMITTED_DATE)) ) + SVN_ERR(svn_time_from_cstring(&date, val->data, scratch_pool)); + + if ( (val = svn_hash_gets(props, SVN_PROP_ENTRY_LAST_AUTHOR)) ) + author = val->data; + + if ( (val = svn_hash_gets(props, SVN_PROP_SPECIAL)) ) + special = TRUE; + + if (special) + { + svn_stream_t *tmp_stream; + + SVN_ERR(svn_subst_create_specialfile(&tmp_stream, full_path, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_copy3(contents, tmp_stream, eb->cancel_func, + eb->cancel_baton, scratch_pool)); + } + else + { + svn_stream_t *tmp_stream; + const char *tmppath; + + /* Create a temporary file in the same directory as the file. We're going + to rename the thing into place when we're done. */ + SVN_ERR(svn_stream_open_unique(&tmp_stream, &tmppath, + svn_dirent_dirname(full_path, + scratch_pool), + svn_io_file_del_none, + scratch_pool, scratch_pool)); + + /* Possibly wrap the stream to be translated, as dictated by + the props. */ + if (eol_style_val || keywords_val) + { + svn_subst_eol_style_t style; + const char *eol = NULL; + svn_boolean_t repair = FALSE; + apr_hash_t *final_kw = NULL; + + if (eol_style_val) + { + SVN_ERR(get_eol_style(&style, &eol, eol_style_val->data, + eb->native_eol)); + repair = TRUE; + } + + if (keywords_val) + SVN_ERR(svn_subst_build_keywords3(&final_kw, keywords_val->data, + revision, full_url, + eb->repos_root_url, + date, author, scratch_pool)); + + /* Writing through a translated stream is more efficient than + reading through one, so we wrap TMP_STREAM and not CONTENTS. */ + tmp_stream = svn_subst_stream_translated(tmp_stream, eol, repair, + final_kw, TRUE, /* expand */ + scratch_pool); + } + + SVN_ERR(svn_stream_copy3(contents, tmp_stream, eb->cancel_func, + eb->cancel_baton, scratch_pool)); + + /* Move the file into place. */ + SVN_ERR(svn_io_file_rename(tmppath, full_path, scratch_pool)); + } + + if (executable_val) + SVN_ERR(svn_io_set_file_executable(full_path, TRUE, FALSE, scratch_pool)); + + if (date && (! special)) + SVN_ERR(svn_io_set_file_affected_time(date, full_path, scratch_pool)); + + if (eb->notify_func) + { + svn_wc_notify_t *notify = svn_wc_create_notify(full_path, + svn_wc_notify_update_add, + scratch_pool); + notify->kind = svn_node_file; + (*eb->notify_func)(eb->notify_baton, notify, scratch_pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +add_directory_ev2(void *baton, + const char *relpath, + const apr_array_header_t *children, + apr_hash_t *props, + svn_revnum_t replaces_rev, + apr_pool_t *scratch_pool) +{ + struct edit_baton *eb = baton; + svn_node_kind_t kind; + const char *full_path = svn_dirent_join(eb->root_path, relpath, + scratch_pool); + svn_string_t *val; + + SVN_ERR(svn_io_check_path(full_path, &kind, scratch_pool)); + if (kind == svn_node_none) + SVN_ERR(svn_io_dir_make(full_path, APR_OS_DEFAULT, scratch_pool)); + else if (kind == svn_node_file) + return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL, + _("'%s' exists and is not a directory"), + svn_dirent_local_style(full_path, scratch_pool)); + else if (! (kind == svn_node_dir && eb->force)) + return svn_error_createf(SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, + _("'%s' already exists"), + svn_dirent_local_style(full_path, scratch_pool)); + + if ( (val = svn_hash_gets(props, SVN_PROP_EXTERNALS)) ) + SVN_ERR(add_externals(eb->externals, full_path, val)); + + if (eb->notify_func) + { + svn_wc_notify_t *notify = svn_wc_create_notify(full_path, + svn_wc_notify_update_add, + scratch_pool); + notify->kind = svn_node_dir; + (*eb->notify_func)(eb->notify_baton, notify, scratch_pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +target_revision_func(void *baton, + svn_revnum_t target_revision, + apr_pool_t *scratch_pool) +{ + struct edit_baton *eb = baton; + + *eb->target_revision = target_revision; + + return SVN_NO_ERROR; +} + +static svn_error_t * +get_editor_ev2(const svn_delta_editor_t **export_editor, + void **edit_baton, + struct edit_baton *eb, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_editor_t *editor; + struct svn_delta__extra_baton *exb = apr_pcalloc(result_pool, sizeof(*exb)); + svn_boolean_t *found_abs_paths = apr_palloc(result_pool, + sizeof(*found_abs_paths)); + + exb->baton = eb; + exb->target_revision = target_revision_func; + + SVN_ERR(svn_editor_create(&editor, eb, ctx->cancel_func, ctx->cancel_baton, + result_pool, scratch_pool)); + SVN_ERR(svn_editor_setcb_add_directory(editor, add_directory_ev2, + scratch_pool)); + SVN_ERR(svn_editor_setcb_add_file(editor, add_file_ev2, scratch_pool)); + + *found_abs_paths = TRUE; + + SVN_ERR(svn_delta__delta_from_editor(export_editor, edit_baton, + editor, NULL, NULL, found_abs_paths, + NULL, NULL, + fetch_props_func, eb, + fetch_base_func, eb, + exb, result_pool)); + + /* Create the root of the export. */ + SVN_ERR(open_root_internal(eb->root_path, eb->force, eb->notify_func, + eb->notify_baton, scratch_pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +export_file_ev2(const char *from_path_or_url, + const char *to_path, + struct edit_baton *eb, + svn_client__pathrev_t *loc, + svn_ra_session_t *ra_session, + svn_boolean_t overwrite, + apr_pool_t *scratch_pool) +{ + svn_boolean_t from_is_url = svn_path_is_url(from_path_or_url); + apr_hash_t *props; + svn_stream_t *tmp_stream; + svn_node_kind_t to_kind; + + if (svn_path_is_empty(to_path)) + { + if (from_is_url) + to_path = svn_uri_basename(from_path_or_url, scratch_pool); + else + to_path = svn_dirent_basename(from_path_or_url, NULL); + eb->root_path = to_path; + } + else + { + SVN_ERR(append_basename_if_dir(&to_path, from_path_or_url, + from_is_url, scratch_pool)); + eb->root_path = to_path; + } + + SVN_ERR(svn_io_check_path(to_path, &to_kind, scratch_pool)); + + if ((to_kind == svn_node_file || to_kind == svn_node_unknown) && + ! overwrite) + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + _("Destination file '%s' exists, and " + "will not be overwritten unless forced"), + svn_dirent_local_style(to_path, scratch_pool)); + else if (to_kind == svn_node_dir) + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + _("Destination '%s' exists. Cannot " + "overwrite directory with non-directory"), + svn_dirent_local_style(to_path, scratch_pool)); + + tmp_stream = svn_stream_buffered(scratch_pool); + + SVN_ERR(svn_ra_get_file(ra_session, "", loc->rev, + tmp_stream, NULL, &props, scratch_pool)); + + /* Since you cannot actually root an editor at a file, we manually drive + * a function of our editor. */ + SVN_ERR(add_file_ev2(eb, "", NULL, tmp_stream, props, SVN_INVALID_REVNUM, + scratch_pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +export_file(const char *from_path_or_url, + const char *to_path, + struct edit_baton *eb, + svn_client__pathrev_t *loc, + svn_ra_session_t *ra_session, + svn_boolean_t overwrite, + apr_pool_t *scratch_pool) +{ + apr_hash_t *props; + apr_hash_index_t *hi; + struct file_baton *fb = apr_pcalloc(scratch_pool, sizeof(*fb)); + svn_node_kind_t to_kind; + svn_boolean_t from_is_url = svn_path_is_url(from_path_or_url); + + if (svn_path_is_empty(to_path)) + { + if (from_is_url) + to_path = svn_uri_basename(from_path_or_url, scratch_pool); + else + to_path = svn_dirent_basename(from_path_or_url, NULL); + eb->root_path = to_path; + } + else + { + SVN_ERR(append_basename_if_dir(&to_path, from_path_or_url, + from_is_url, scratch_pool)); + eb->root_path = to_path; + } + + SVN_ERR(svn_io_check_path(to_path, &to_kind, scratch_pool)); + + if ((to_kind == svn_node_file || to_kind == svn_node_unknown) && + ! overwrite) + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + _("Destination file '%s' exists, and " + "will not be overwritten unless forced"), + svn_dirent_local_style(to_path, scratch_pool)); + else if (to_kind == svn_node_dir) + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + _("Destination '%s' exists. Cannot " + "overwrite directory with non-directory"), + svn_dirent_local_style(to_path, scratch_pool)); + + /* Since you cannot actually root an editor at a file, we + * manually drive a few functions of our editor. */ + + /* This is the equivalent of a parentless add_file(). */ + fb->edit_baton = eb; + fb->path = eb->root_path; + fb->url = eb->root_url; + fb->pool = scratch_pool; + fb->repos_root_url = eb->repos_root_url; + + /* Copied from apply_textdelta(). */ + SVN_ERR(svn_stream_open_unique(&fb->tmp_stream, &fb->tmppath, + svn_dirent_dirname(fb->path, scratch_pool), + svn_io_file_del_none, + fb->pool, fb->pool)); + + /* Step outside the editor-likeness for a moment, to actually talk + * to the repository. */ + /* ### note: the stream will not be closed */ + SVN_ERR(svn_ra_get_file(ra_session, "", loc->rev, + fb->tmp_stream, + NULL, &props, scratch_pool)); + + /* Push the props into change_file_prop(), to update the file_baton + * with information. */ + for (hi = apr_hash_first(scratch_pool, props); hi; hi = apr_hash_next(hi)) + { + const char *propname = svn__apr_hash_index_key(hi); + const svn_string_t *propval = svn__apr_hash_index_val(hi); + + SVN_ERR(change_file_prop(fb, propname, propval, scratch_pool)); + } + + /* And now just use close_file() to do all the keyword and EOL + * work, and put the file into place. */ + SVN_ERR(close_file(fb, NULL, scratch_pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +export_directory(const char *from_path_or_url, + const char *to_path, + struct edit_baton *eb, + svn_client__pathrev_t *loc, + svn_ra_session_t *ra_session, + svn_boolean_t overwrite, + svn_boolean_t ignore_externals, + svn_boolean_t ignore_keywords, + svn_depth_t depth, + const char *native_eol, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + void *edit_baton; + const svn_delta_editor_t *export_editor; + const svn_ra_reporter3_t *reporter; + void *report_baton; + svn_node_kind_t kind; + + if (!ENABLE_EV2_IMPL) + SVN_ERR(get_editor_ev1(&export_editor, &edit_baton, eb, ctx, + scratch_pool, scratch_pool)); + else + SVN_ERR(get_editor_ev2(&export_editor, &edit_baton, eb, ctx, + scratch_pool, scratch_pool)); + + /* Manufacture a basic 'report' to the update reporter. */ + SVN_ERR(svn_ra_do_update3(ra_session, + &reporter, &report_baton, + loc->rev, + "", /* no sub-target */ + depth, + FALSE, /* don't want copyfrom-args */ + FALSE, /* don't want ignore_ancestry */ + export_editor, edit_baton, + scratch_pool, scratch_pool)); + + SVN_ERR(reporter->set_path(report_baton, "", loc->rev, + /* Depth is irrelevant, as we're + passing start_empty=TRUE anyway. */ + svn_depth_infinity, + TRUE, /* "help, my dir is empty!" */ + NULL, scratch_pool)); + + SVN_ERR(reporter->finish_report(report_baton, scratch_pool)); + + /* Special case: Due to our sly export/checkout method of updating an + * empty directory, no target will have been created if the exported + * item is itself an empty directory (export_editor->open_root never + * gets called, because there are no "changes" to make to the empty + * dir we reported to the repository). + * + * So we just create the empty dir manually; but we do it via + * open_root_internal(), in order to get proper notification. + */ + SVN_ERR(svn_io_check_path(to_path, &kind, scratch_pool)); + if (kind == svn_node_none) + SVN_ERR(open_root_internal + (to_path, overwrite, ctx->notify_func2, + ctx->notify_baton2, scratch_pool)); + + if (! ignore_externals && depth == svn_depth_infinity) + { + const char *to_abspath; + + SVN_ERR(svn_dirent_get_absolute(&to_abspath, to_path, scratch_pool)); + SVN_ERR(svn_client__export_externals(eb->externals, + from_path_or_url, + to_abspath, eb->repos_root_url, + depth, native_eol, + ignore_keywords, + ctx, scratch_pool)); + } + + return SVN_NO_ERROR; +} + + + +/*** Public Interfaces ***/ + +svn_error_t * +svn_client_export5(svn_revnum_t *result_rev, + const char *from_path_or_url, + const char *to_path, + const svn_opt_revision_t *peg_revision, + const svn_opt_revision_t *revision, + svn_boolean_t overwrite, + svn_boolean_t ignore_externals, + svn_boolean_t ignore_keywords, + svn_depth_t depth, + const char *native_eol, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + svn_revnum_t edit_revision = SVN_INVALID_REVNUM; + svn_boolean_t from_is_url = svn_path_is_url(from_path_or_url); + + SVN_ERR_ASSERT(peg_revision != NULL); + SVN_ERR_ASSERT(revision != NULL); + + if (svn_path_is_url(to_path)) + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + _("'%s' is not a local path"), to_path); + + peg_revision = svn_cl__rev_default_to_head_or_working(peg_revision, + from_path_or_url); + revision = svn_cl__rev_default_to_peg(revision, peg_revision); + + if (from_is_url || ! SVN_CLIENT__REVKIND_IS_LOCAL_TO_WC(revision->kind)) + { + svn_client__pathrev_t *loc; + svn_ra_session_t *ra_session; + svn_node_kind_t kind; + struct edit_baton *eb = apr_pcalloc(pool, sizeof(*eb)); + + /* Get the RA connection. */ + SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc, + from_path_or_url, NULL, + peg_revision, + revision, ctx, pool)); + + SVN_ERR(svn_ra_get_repos_root2(ra_session, &eb->repos_root_url, pool)); + eb->root_path = to_path; + eb->root_url = loc->url; + eb->force = overwrite; + eb->target_revision = &edit_revision; + eb->externals = apr_hash_make(pool); + eb->native_eol = native_eol; + eb->ignore_keywords = ignore_keywords; + eb->cancel_func = ctx->cancel_func; + eb->cancel_baton = ctx->cancel_baton; + eb->notify_func = ctx->notify_func2; + eb->notify_baton = ctx->notify_baton2; + + SVN_ERR(svn_ra_check_path(ra_session, "", loc->rev, &kind, pool)); + + if (kind == svn_node_file) + { + if (!ENABLE_EV2_IMPL) + SVN_ERR(export_file(from_path_or_url, to_path, eb, loc, ra_session, + overwrite, pool)); + else + SVN_ERR(export_file_ev2(from_path_or_url, to_path, eb, loc, + ra_session, overwrite, pool)); + } + else if (kind == svn_node_dir) + { + SVN_ERR(export_directory(from_path_or_url, to_path, + eb, loc, ra_session, overwrite, + ignore_externals, ignore_keywords, depth, + native_eol, ctx, pool)); + } + else if (kind == svn_node_none) + { + return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, + _("URL '%s' doesn't exist"), + from_path_or_url); + } + /* kind == svn_node_unknown not handled */ + } + else + { + struct export_info_baton eib; + svn_node_kind_t kind; + apr_hash_t *externals = NULL; + + /* This is a working copy export. */ + /* just copy the contents of the working copy into the target path. */ + SVN_ERR(svn_dirent_get_absolute(&from_path_or_url, from_path_or_url, + pool)); + + SVN_ERR(svn_dirent_get_absolute(&to_path, to_path, pool)); + + SVN_ERR(svn_io_check_path(from_path_or_url, &kind, pool)); + + /* ### [JAF] If something already exists on disk at the destination path, + * the behaviour depends on the node kinds of the source and destination + * and on the FORCE flag. The intention (I guess) is to follow the + * semantics of svn_client_export5(), semantics that are not fully + * documented but would be something like: + * + * -----------+--------------------------------------------------------- + * Src | DIR FILE SPECIAL + * Dst (disk) +--------------------------------------------------------- + * NONE | simple copy simple copy (as src=file?) + * DIR | merge if forced [2] inside if root [1] (as src=file?) + * FILE | err overwr if forced[3] (as src=file?) + * SPECIAL | ??? ??? ??? + * -----------+--------------------------------------------------------- + * + * [1] FILE onto DIR case: If this file is the root of the copy and thus + * the only node to be copied, then copy it as a child of the + * directory TO, applying these same rules again except that if this + * case occurs again (the child path is already a directory) then + * error out. If this file is not the root of the copy (it is + * reached by recursion), then error out. + * + * [2] DIR onto DIR case. If the 'FORCE' flag is true then copy the + * source's children inside the target dir, else error out. When + * copying the children, apply the same set of rules, except in the + * FILE onto DIR case error out like in note [1]. + * + * [3] If the 'FORCE' flag is true then overwrite the destination file + * else error out. + * + * The reality (apparently, looking at the code) is somewhat different. + * For a start, to detect the source kind, it looks at what is on disk + * rather than the versioned working or base node. + */ + if (kind == svn_node_file) + SVN_ERR(append_basename_if_dir(&to_path, from_path_or_url, FALSE, + pool)); + + eib.to_path = to_path; + eib.revision = revision; + eib.overwrite = overwrite; + eib.ignore_keywords = ignore_keywords; + eib.wc_ctx = ctx->wc_ctx; + eib.native_eol = native_eol; + eib.notify_func = ctx->notify_func2;; + eib.notify_baton = ctx->notify_baton2; + eib.origin_abspath = from_path_or_url; + eib.exported = FALSE; + + SVN_ERR(svn_wc_walk_status(ctx->wc_ctx, from_path_or_url, depth, + TRUE /* get_all */, + TRUE /* no_ignore */, + FALSE /* ignore_text_mods */, + NULL, + export_node, &eib, + ctx->cancel_func, ctx->cancel_baton, + pool)); + + if (!eib.exported) + return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL, + _("The node '%s' was not found."), + svn_dirent_local_style(from_path_or_url, + pool)); + + if (!ignore_externals) + SVN_ERR(svn_wc__externals_defined_below(&externals, ctx->wc_ctx, + from_path_or_url, + pool, pool)); + + if (externals && apr_hash_count(externals)) + { + apr_pool_t *iterpool = svn_pool_create(pool); + apr_hash_index_t *hi; + + for (hi = apr_hash_first(pool, externals); + hi; + hi = apr_hash_next(hi)) + { + const char *external_abspath = svn__apr_hash_index_key(hi); + const char *relpath; + const char *target_abspath; + + svn_pool_clear(iterpool); + + relpath = svn_dirent_skip_ancestor(from_path_or_url, + external_abspath); + + target_abspath = svn_dirent_join(to_path, relpath, + iterpool); + + /* Ensure that the parent directory exists */ + SVN_ERR(svn_io_make_dir_recursively( + svn_dirent_dirname(target_abspath, iterpool), + iterpool)); + + SVN_ERR(svn_client_export5(NULL, + svn_dirent_join(from_path_or_url, + relpath, + iterpool), + target_abspath, + peg_revision, revision, + TRUE, ignore_externals, + ignore_keywords, depth, native_eol, + ctx, iterpool)); + } + + svn_pool_destroy(iterpool); + } + } + + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify + = svn_wc_create_notify(to_path, + svn_wc_notify_update_completed, pool); + notify->revision = edit_revision; + (*ctx->notify_func2)(ctx->notify_baton2, notify, pool); + } + + if (result_rev) + *result_rev = edit_revision; + + return SVN_NO_ERROR; +} |