Index: build.conf =================================================================== --- build.conf (revision 20268) +++ build.conf (working copy) @@ -60,6 +60,7 @@ subversion/tests/libsvn_subr/target-test.py subversion/tests/cmdline/getopt_tests.py subversion/tests/cmdline/basic_tests.py + subversion/tests/cmdline/checkout_tests.py subversion/tests/cmdline/commit_tests.py subversion/tests/cmdline/update_tests.py subversion/tests/cmdline/switch_tests.py Index: subversion/include/svn_client.h =================================================================== --- subversion/include/svn_client.h (revision 20268) +++ subversion/include/svn_client.h (working copy) @@ -696,15 +696,40 @@ * just the directory represented by @a URL and its immediate * non-directory children, but none of its child directories (if any). * + * If @a force_checkout is @c TRUE then checkout tolerates an existing local + * tree rooted at @a PATH even if some paths in the local tree are identical + * to those in @a URL. File paths that collide will effectively treat the + * local file as a user modification to the pristine checkout. If @a force + * is @c FALSE then the checkout will abort if there are any colliding paths. + * * If @a URL refers to a file rather than a directory, return the * error @c SVN_ERR_UNSUPPORTED_FEATURE. If @a URL does not exist, * return the error @c SVN_ERR_RA_ILLEGAL_URL. * * Use @a pool for any temporary allocation. * - * @since New in 1.2. + * @since New in 1.5. */ svn_error_t * +svn_client_checkout3(svn_revnum_t *result_rev, + const char *URL, + const char *path, + const svn_opt_revision_t *peg_revision, + const svn_opt_revision_t *revision, + svn_boolean_t recurse, + svn_boolean_t ignore_externals, + svn_boolean_t force_checkout, + svn_client_ctx_t *ctx, + apr_pool_t *pool); + + +/** + * Similar to svn_client_checkout3() but with the force parameter always + * set to @c FALSE. + * + * @deprecated Provided for backward compatibility with the 1.4 API. + */ +svn_error_t * svn_client_checkout2(svn_revnum_t *result_rev, const char *URL, const char *path, @@ -759,6 +784,13 @@ * update just their immediate entries, but not their child * directories (if any). * + * If @a force_update is @c TRUE then the update tolerates existing + * unversioned paths rooted at @a PATH even if some of those paths obstruct + * added paths from @a URL. Obstructing file paths are left as-is, + * effectively treating them as a user modification after the update. + * If @a force is @c FALSE then the update will abort if there are any + * obstructing paths. + * * If @a ctx->notify_func2 is non-null, invoke @a ctx->notify_func2 with * @a ctx->notify_baton2 for each item handled by the update, and also for * files restored from text-base. If @a ctx->cancel_func is non-null, invoke @@ -766,9 +798,25 @@ * * Use @a pool for any temporary allocation. * - * @since New in 1.2. + * @since New in 1.5. */ svn_error_t * +svn_client_update3(apr_array_header_t **result_revs, + const apr_array_header_t *paths, + const svn_opt_revision_t *revision, + svn_boolean_t recurse, + svn_boolean_t ignore_externals, + svn_boolean_t force_update, + svn_client_ctx_t *ctx, + apr_pool_t *pool); + +/** + * Similar to svn_client_update3() but with the force_update parameter + * always set to @c FALSE. + * + * @deprecated Provided for backward compatibility with the 1.4 API. + */ +svn_error_t * svn_client_update2(apr_array_header_t **result_revs, const apr_array_header_t *paths, const svn_opt_revision_t *revision, @@ -811,13 +859,39 @@ * recursively; otherwise, switch just @a path and its immediate * entries, but not its child directories (if any). * + * If @a force_switch is @c TRUE then the switch tolerates existing + * unversioned paths rooted at @a PATH even if some of those paths obstruct + * added paths from @a URL. Obstructing file paths are left as-is, + * effectively treating them as a user modification after the switch. + * If @a force is @c FALSE then the switch will abort if there are any + * obstructing paths. + * * If @a ctx->notify_func2 is non-null, invoke it with @a ctx->notify_baton2 * on paths affected by the switch. Also invoke it for files may be restored * from the text-base because they were removed from the working copy. * * Use @a pool for any temporary allocation. + * + * @since New in 1.5. */ svn_error_t * +svn_client_switch2(svn_revnum_t *result_rev, + const char *path, + const char *url, + const svn_opt_revision_t *revision, + svn_boolean_t recurse, + svn_boolean_t force_switch, + svn_client_ctx_t *ctx, + apr_pool_t *pool); + + +/** + * Similar to svn_client_switch2() but with the force_switch parameter + * always set to @c FALSE. + * + * @deprecated Provided for backward compatibility with the 1.4 API. + */ +svn_error_t * svn_client_switch(svn_revnum_t *result_rev, const char *path, const char *url, Index: subversion/include/svn_wc.h =================================================================== --- subversion/include/svn_wc.h (revision 20268) +++ subversion/include/svn_wc.h (working copy) @@ -640,7 +640,11 @@ svn_wc_notify_failed_lock, /** Failed to unlock a path. @since New in 1.2. */ - svn_wc_notify_failed_unlock + svn_wc_notify_failed_unlock, + + /** Tried adding a path that already exists. @since New in 1.5. */ + svn_wc_notify_exists + } svn_wc_notify_action_t; @@ -1092,6 +1096,9 @@ * that if the text base is much longer than the working file, every * byte of the text base will still be examined.) * + * If @a use_tmp_base is TRUE, then use the text-base in the local tmp + * area, if @c FALSE use the 'normal' base revision. + * * If @a filename does not exist, consider it unmodified. If it exists * but is not under revision control (not even scheduled for * addition), return the error @c SVN_ERR_ENTRY_NOT_FOUND. @@ -1099,7 +1106,22 @@ * If @a filename is unmodified but has a timestamp variation then this * function may "repair" @a filename's text-time by setting it to * @a filename's last modification time. + * + * @since New in 1.5. */ +svn_error_t *svn_wc_text_modified_p2(svn_boolean_t *modified_p, + const char *filename, + svn_boolean_t force_comparison, + svn_boolean_t use_tmp_base, + svn_wc_adm_access_t *adm_access, + apr_pool_t *pool); + + +/** Similar to svn_wc_text_modified_p2() but with the use_tmp_base + * parameter always set to @c FALSE. + * + * @deprecated Provided for backward compatibility with the 1.4 API. + */ svn_error_t *svn_wc_text_modified_p(svn_boolean_t *modified_p, const char *filename, svn_boolean_t force_comparison, @@ -2476,8 +2498,34 @@ * have their working timestamp set to the last-committed-time. If * FALSE, the working files will be touched with the 'now' time. * - * @since New in 1.2. + * If @a allow_obstructions is @c TRUE, then allow unversioned obstructions + * when adding a path. + * + * @since New in 1.5. */ +svn_error_t *svn_wc_get_update_editor3(svn_revnum_t *target_revision, + svn_wc_adm_access_t *anchor, + const char *target, + svn_boolean_t use_commit_times, + svn_boolean_t recurse, + svn_boolean_t allow_obstructions, + svn_wc_notify_func2_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + const char *diff3_cmd, + const svn_delta_editor_t **editor, + void **edit_baton, + svn_wc_traversal_info_t *ti, + apr_pool_t *pool); + + +/** + * Similar to svn_wc_get_update_editor3() but with the allow_obstructions + * parameter always set to @c FALSE. + * + * @deprecated Provided for backward compatibility with the 1.4 API. + */ svn_error_t *svn_wc_get_update_editor2(svn_revnum_t *target_revision, svn_wc_adm_access_t *anchor, const char *target, @@ -2551,8 +2599,34 @@ * have their working timestamp set to the last-committed-time. If * FALSE, the working files will be touched with the 'now' time. * - * @since New in 1.2. + * If @a allow_obstructions is @c TRUE, then allow unversioned obstructions + * when adding a path. + * + * @since New in 1.5. */ +svn_error_t *svn_wc_get_switch_editor3(svn_revnum_t *target_revision, + svn_wc_adm_access_t *anchor, + const char *target, + const char *switch_url, + svn_boolean_t use_commit_times, + svn_boolean_t recurse, + svn_boolean_t allow_obstructions, + svn_wc_notify_func2_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + const char *diff3_cmd, + const svn_delta_editor_t **editor, + void **edit_baton, + svn_wc_traversal_info_t *ti, + apr_pool_t *pool); + +/** + * Similar to svn_wc_get_switch_editor3() but with the allow_obstructions + * parameter always set to @c FALSE. + * + * @deprecated Provided for backward compatibility with the 1.4 API. + */ svn_error_t *svn_wc_get_switch_editor2(svn_revnum_t *target_revision, svn_wc_adm_access_t *anchor, const char *target, Index: subversion/libsvn_client/checkout.c =================================================================== --- subversion/libsvn_client/checkout.c (revision 20268) +++ subversion/libsvn_client/checkout.c (working copy) @@ -2,7 +2,7 @@ * checkout.c: wrappers around wc checkout functionality * * ==================================================================== - * Copyright (c) 2000-2004 CollabNet. All rights reserved. + * Copyright (c) 2000-2006 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 @@ -49,6 +49,7 @@ const svn_opt_revision_t *revision, svn_boolean_t recurse, svn_boolean_t ignore_externals, + svn_boolean_t force_checkout, svn_boolean_t *timestamp_sleep, svn_client_ctx_t *ctx, apr_pool_t *pool) @@ -119,6 +120,7 @@ /* Have update fix the incompleteness. */ err = svn_client__update_internal(result_rev, path, revision, recurse, ignore_externals, + force_checkout, use_sleep, ctx, pool); } else if (kind == svn_node_dir) @@ -135,6 +137,7 @@ repos, revnum, pool)); err = svn_client__update_internal(result_rev, path, revision, recurse, ignore_externals, + force_checkout, use_sleep, ctx, pool); goto done; } @@ -153,7 +156,8 @@ { err = svn_client__update_internal(result_rev, path, revision, recurse, ignore_externals, - use_sleep, ctx, pool); + force_checkout, use_sleep, + ctx, pool); } else { @@ -204,6 +208,23 @@ } svn_error_t * +svn_client_checkout3(svn_revnum_t *result_rev, + const char *URL, + const char *path, + const svn_opt_revision_t *peg_revision, + const svn_opt_revision_t *revision, + svn_boolean_t recurse, + svn_boolean_t ignore_externals, + svn_boolean_t force_checkout, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + return svn_client__checkout_internal(result_rev, URL, path, peg_revision, + revision, recurse, ignore_externals, + force_checkout, NULL, ctx, pool); +} + +svn_error_t * svn_client_checkout2(svn_revnum_t *result_rev, const char *URL, const char *path, @@ -216,7 +237,7 @@ { return svn_client__checkout_internal(result_rev, URL, path, peg_revision, revision, recurse, ignore_externals, - NULL, ctx, pool); + FALSE, NULL, ctx, pool); } svn_error_t * @@ -233,6 +254,6 @@ peg_revision.kind = svn_opt_revision_unspecified; return svn_client__checkout_internal(result_rev, URL, path, &peg_revision, - revision, recurse, FALSE, NULL, + revision, recurse, FALSE, FALSE, NULL, ctx, pool); } Index: subversion/libsvn_client/client.h =================================================================== --- subversion/libsvn_client/client.h (revision 20268) +++ subversion/libsvn_client/client.h (working copy) @@ -323,13 +323,17 @@ function will sleep before returning to ensure timestamp integrity. If TIMESTAMP_SLEEP is not NULL then the function will not sleep but will set *TIMESTAMP_SLEEP to TRUE if a sleep is required, and will - not change *TIMESTAMP_SLEEP if no sleep is required. */ + not change *TIMESTAMP_SLEEP if no sleep is required. If FORCE_UPDATE + is TRUE, unversioned children of PATH that obstruct items added from + the repos are tolerated; if FALSE, these obstructions cause the update + to fail. */ svn_error_t * svn_client__update_internal(svn_revnum_t *result_rev, const char *path, const svn_opt_revision_t *revision, svn_boolean_t recurse, svn_boolean_t ignore_externals, + svn_boolean_t force_update, svn_boolean_t *timestamp_sleep, svn_client_ctx_t *ctx, apr_pool_t *pool); @@ -341,7 +345,9 @@ timestamp integrity. If TIMESTAMP_SLEEP is not NULL then the function will not sleep but will set *TIMESTAMP_SLEEP to TRUE if a sleep is required, and will not change *TIMESTAMP_SLEEP if no sleep - is required. */ + is required. If FORCE_CHECKOUT is TRUE, unversioned children of PATH + that obstruct items added from the repos are tolerated; if FALSE, + these obstructions cause the checkout to fail. */ svn_error_t * svn_client__checkout_internal(svn_revnum_t *result_rev, const char *URL, @@ -350,6 +356,7 @@ const svn_opt_revision_t *revision, svn_boolean_t recurse, svn_boolean_t ignore_externals, + svn_boolean_t force_checkout, svn_boolean_t *timestamp_sleep, svn_client_ctx_t *ctx, apr_pool_t *pool); @@ -360,7 +367,10 @@ returning to ensure timestamp integrity. If TIMESTAMP_SLEEP is not NULL then the function will not sleep but will set *TIMESTAMP_SLEEP to TRUE if a sleep is required, and will not change - *TIMESTAMP_SLEEP if no sleep is required. */ + *TIMESTAMP_SLEEP if no sleep is required. If FORCE_SWITCH + is TRUE, unversioned children of PATH that obstruct items added from + the repos are tolerated; if FALSE, these obstructions cause the switch + to fail. */ svn_error_t * svn_client__switch_internal(svn_revnum_t *result_rev, const char *path, @@ -368,6 +378,7 @@ const svn_opt_revision_t *revision, svn_boolean_t recurse, svn_boolean_t *timestamp_sleep, + svn_boolean_t force_switch, svn_client_ctx_t *ctx, apr_pool_t *pool); Index: subversion/libsvn_client/commit_util.c =================================================================== --- subversion/libsvn_client/commit_util.c (revision 20268) +++ subversion/libsvn_client/commit_util.c (working copy) @@ -432,9 +432,9 @@ prop was changed, we might have to send new text to the server to match the new newline style. */ if (state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY) - SVN_ERR(svn_wc_text_modified_p(&text_mod, path, - eol_prop_changed, adm_access, - pool)); + SVN_ERR(svn_wc_text_modified_p2(&text_mod, path, + eol_prop_changed, FALSE, + adm_access, pool)); else text_mod = TRUE; } @@ -458,8 +458,8 @@ changed, we might have to send new text to the server to match the new newline style. */ if (entry->kind == svn_node_file) - SVN_ERR(svn_wc_text_modified_p(&text_mod, path, eol_prop_changed, - adm_access, pool)); + SVN_ERR(svn_wc_text_modified_p2(&text_mod, path, eol_prop_changed, + FALSE, adm_access, pool)); } /* Set text/prop modification flags accordingly. */ Index: subversion/libsvn_client/copy.c =================================================================== --- subversion/libsvn_client/copy.c (revision 20268) +++ subversion/libsvn_client/copy.c (working copy) @@ -822,7 +822,7 @@ { SVN_ERR(svn_client__checkout_internal (NULL, src_url, dst_path, &revision, &revision, - TRUE, FALSE, NULL, ctx, pool)); + TRUE, FALSE, FALSE, NULL, ctx, pool)); if ((revision.kind == svn_opt_revision_head) && same_repositories) { Index: subversion/libsvn_client/diff.c =================================================================== --- subversion/libsvn_client/diff.c (revision 20268) +++ subversion/libsvn_client/diff.c (working copy) @@ -820,8 +820,8 @@ if (older) { - SVN_ERR(svn_wc_text_modified_p(&has_local_mods, mine, FALSE, - adm_access, subpool)); + SVN_ERR(svn_wc_text_modified_p2(&has_local_mods, mine, FALSE, + FALSE, adm_access, subpool)); /* Special case: if a binary file isn't locally modified, and is exactly identical to the 'left' side of the merge, then don't Index: subversion/libsvn_client/externals.c =================================================================== --- subversion/libsvn_client/externals.c (revision 20268) +++ subversion/libsvn_client/externals.c (working copy) @@ -198,8 +198,9 @@ if (strcmp(entry->url, url) == 0) { SVN_ERR(svn_client__update_internal(NULL, path, revision, - TRUE, FALSE, timestamp_sleep, - ctx, pool)); + TRUE, FALSE, FALSE, + timestamp_sleep, ctx, + pool)); return SVN_NO_ERROR; } else if (entry->repos) @@ -236,8 +237,8 @@ } SVN_ERR(svn_client__switch_internal(NULL, path, url, revision, - TRUE, timestamp_sleep, ctx, - subpool)); + TRUE, timestamp_sleep, + FALSE, ctx, subpool)); return SVN_NO_ERROR; } @@ -268,7 +269,7 @@ /* ... Hello, new hotness. */ SVN_ERR(svn_client__checkout_internal(NULL, url, path, revision, revision, - TRUE, FALSE, timestamp_sleep, + TRUE, FALSE, FALSE, timestamp_sleep, ctx, pool)); return SVN_NO_ERROR; @@ -362,7 +363,7 @@ SVN_ERR(svn_client__checkout_internal(NULL, new_item->url, path, &(new_item->revision), &(new_item->revision), - TRUE, FALSE, + TRUE, FALSE, FALSE, ib->timestamp_sleep, ib->ctx, ib->pool)); } Index: subversion/libsvn_client/switch.c =================================================================== --- subversion/libsvn_client/switch.c (revision 20268) +++ subversion/libsvn_client/switch.c (working copy) @@ -57,6 +57,7 @@ const svn_opt_revision_t *revision, svn_boolean_t recurse, svn_boolean_t *timestamp_sleep, + svn_boolean_t force_switch, svn_client_ctx_t *ctx, apr_pool_t *pool) { @@ -128,8 +129,9 @@ /* Fetch the switch (update) editor. If REVISION is invalid, that's okay; the RA driver will call editor->set_target_revision() later on. */ - SVN_ERR(svn_wc_get_switch_editor2(&revnum, adm_access, target, + SVN_ERR(svn_wc_get_switch_editor3(&revnum, adm_access, target, switch_url, use_commit_times, recurse, + force_switch, ctx->notify_func2, ctx->notify_baton2, ctx->cancel_func, ctx->cancel_baton, diff3_cmd, @@ -202,6 +204,20 @@ } svn_error_t * +svn_client_switch2(svn_revnum_t *result_rev, + const char *path, + const char *switch_url, + const svn_opt_revision_t *revision, + svn_boolean_t recurse, + svn_boolean_t force_switch, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + return svn_client__switch_internal(result_rev, path, switch_url, revision, + recurse, NULL, force_switch, ctx, pool); +} + +svn_error_t * svn_client_switch(svn_revnum_t *result_rev, const char *path, const char *switch_url, @@ -211,5 +227,5 @@ apr_pool_t *pool) { return svn_client__switch_internal(result_rev, path, switch_url, revision, - recurse, NULL, ctx, pool); + recurse, NULL, FALSE, ctx, pool); } Index: subversion/libsvn_client/update.c =================================================================== --- subversion/libsvn_client/update.c (revision 20268) +++ subversion/libsvn_client/update.c (working copy) @@ -2,7 +2,7 @@ * update.c: wrappers around wc update functionality * * ==================================================================== - * Copyright (c) 2000-2004 CollabNet. All rights reserved. + * Copyright (c) 2000-2006 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 @@ -44,6 +44,7 @@ const svn_opt_revision_t *revision, svn_boolean_t recurse, svn_boolean_t ignore_externals, + svn_boolean_t force_update, svn_boolean_t *timestamp_sleep, svn_client_ctx_t *ctx, apr_pool_t *pool) @@ -125,8 +126,9 @@ /* Fetch the update editor. If REVISION is invalid, that's okay; the RA driver will call editor->set_target_revision later on. */ - SVN_ERR(svn_wc_get_update_editor2(&revnum, adm_access, target, + SVN_ERR(svn_wc_get_update_editor3(&revnum, adm_access, target, use_commit_times, recurse, + force_update, ctx->notify_func2, ctx->notify_baton2, ctx->cancel_func, ctx->cancel_baton, diff3_cmd, @@ -194,11 +196,12 @@ } svn_error_t * -svn_client_update2(apr_array_header_t **result_revs, +svn_client_update3(apr_array_header_t **result_revs, const apr_array_header_t *paths, const svn_opt_revision_t *revision, svn_boolean_t recurse, svn_boolean_t ignore_externals, + svn_boolean_t force_update, svn_client_ctx_t *ctx, apr_pool_t *pool) { @@ -221,7 +224,7 @@ break; err = svn_client__update_internal(&result_rev, path, revision, - recurse, ignore_externals, + recurse, ignore_externals, force_update, &sleep, ctx, subpool); if (err && err->apr_err != SVN_ERR_WC_NOT_DIRECTORY) { @@ -250,6 +253,19 @@ } svn_error_t * +svn_client_update2(apr_array_header_t **result_revs, + const apr_array_header_t *paths, + const svn_opt_revision_t *revision, + svn_boolean_t recurse, + svn_boolean_t ignore_externals, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + return svn_client_update3(result_revs, paths, revision, recurse, + ignore_externals, FALSE, ctx, pool); +} + +svn_error_t * svn_client_update(svn_revnum_t *result_rev, const char *path, const svn_opt_revision_t *revision, @@ -258,5 +274,5 @@ apr_pool_t *pool) { return svn_client__update_internal(result_rev, path, revision, recurse, - FALSE, NULL, ctx, pool); + FALSE, FALSE, NULL, ctx, pool); } Index: subversion/libsvn_wc/adm_ops.c =================================================================== --- subversion/libsvn_wc/adm_ops.c (revision 20268) +++ subversion/libsvn_wc/adm_ops.c (working copy) @@ -1486,7 +1486,7 @@ if (! reinstall_working) SVN_ERR(svn_wc__text_modified_internal_p(&reinstall_working, fullpath, FALSE, adm_access, - FALSE, pool)); + FALSE, FALSE, pool)); if (reinstall_working) { @@ -1865,8 +1865,8 @@ full_path = svn_path_join(full_path, name, pool); /* Check for local mods. before removing entry */ - SVN_ERR(svn_wc_text_modified_p(&text_modified_p, full_path, - FALSE, adm_access, pool)); + SVN_ERR(svn_wc_text_modified_p2(&text_modified_p, full_path, + FALSE, FALSE, adm_access, pool)); if (text_modified_p && instant_error) return svn_error_createf(SVN_ERR_WC_LEFT_LOCAL_MOD, NULL, _("File '%s' has local modifications"), Index: subversion/libsvn_wc/diff.c =================================================================== --- subversion/libsvn_wc/diff.c (revision 20268) +++ subversion/libsvn_wc/diff.c (working copy) @@ -577,12 +577,12 @@ break; default: - SVN_ERR(svn_wc_text_modified_p(&modified, path, FALSE, - adm_access, pool)); + SVN_ERR(svn_wc_text_modified_p2(&modified, path, FALSE, + FALSE, adm_access, pool)); if (modified) { /* Note that this might be the _second_ time we translate - the file, as svn_wc_text_modified_p() might have used a + the file, as svn_wc_text_modified_p2() might have used a tmp translated copy too. But what the heck, diff is already expensive, translating twice for the sake of code modularity is liveable. */ @@ -1421,8 +1421,8 @@ (BASE:WORKING) modifications. */ modified = (b->temp_file_path != NULL); if (!modified && !eb->use_text_base) - SVN_ERR(svn_wc_text_modified_p(&modified, b->path, FALSE, - adm_access, pool)); + SVN_ERR(svn_wc_text_modified_p2(&modified, b->path, FALSE, + FALSE, adm_access, pool)); if (modified) { Index: subversion/libsvn_wc/log.c =================================================================== --- subversion/libsvn_wc/log.c (revision 20268) +++ subversion/libsvn_wc/log.c (working copy) @@ -2352,8 +2352,8 @@ SVN_ERR(svn_wc_props_modified_p(&modified, entry_path, adm_access, subpool)); if (entry->kind == svn_node_file) - SVN_ERR(svn_wc_text_modified_p(&modified, entry_path, FALSE, - adm_access, subpool)); + SVN_ERR(svn_wc_text_modified_p2(&modified, entry_path, FALSE, + FALSE, adm_access, subpool)); } } svn_pool_destroy(subpool); Index: subversion/libsvn_wc/props.c =================================================================== --- subversion/libsvn_wc/props.c (revision 20268) +++ subversion/libsvn_wc/props.c (working copy) @@ -1649,7 +1649,7 @@ svn_wc_entry_t tmp_entry; /* If we changed the keywords or newlines, void the entry - timestamp for this file, so svn_wc_text_modified_p() does + timestamp for this file, so svn_wc_text_modified_p2() does a real (albeit slow) check later on. */ tmp_entry.kind = svn_node_file; tmp_entry.text_time = 0; Index: subversion/libsvn_wc/questions.c =================================================================== --- subversion/libsvn_wc/questions.c (revision 20268) +++ subversion/libsvn_wc/questions.c (working copy) @@ -135,15 +135,19 @@ -/*** svn_wc_text_modified_p ***/ +/*** svn_wc_text_modified_p2 ***/ -/* svn_wc_text_modified_p answers the question: +/* svn_wc_text_modified_p2 answers the question: "Are the contents of F different than the contents of - .svn/text-base/F.svn-base?" + .svn/text-base/F.svn-base or .svn/tmp/text-base/F.svn-base?" - In other words, we're looking to see if a user has made local - modifications to a file since the last update or commit. + In the first case, we're looking to see if a user has made local + modifications to a file since the last update or commit. In the + second, the file may not be versioned yet (it doesn't exist in + entries). Support for the latter case came about to facilitate + forced checkouts, updates, and switches, where an unversioned file + may obstruct a file about to be added. Note: Assuming that F lives in a directory D at revision V, please notice that we are *NOT* answering the question, "are the contents @@ -382,6 +386,7 @@ svn_boolean_t force_comparison, svn_wc_adm_access_t *adm_access, svn_boolean_t compare_textbases, + svn_boolean_t use_tmp_textbase, apr_pool_t *pool) { const char *textbase_filename; @@ -430,7 +435,8 @@ /* If there's no text-base file, we have to assume the working file is modified. For example, a file scheduled for addition but not yet committed. */ - textbase_filename = svn_wc__text_base_path(filename, 0, subpool); + textbase_filename = svn_wc__text_base_path(filename, use_tmp_textbase, + subpool); SVN_ERR(svn_io_check_path(textbase_filename, &kind, subpool)); if (kind != svn_node_file) { @@ -471,15 +477,28 @@ svn_error_t * +svn_wc_text_modified_p2(svn_boolean_t *modified_p, + const char *filename, + svn_boolean_t force_comparison, + svn_boolean_t use_tmp_base, + svn_wc_adm_access_t *adm_access, + apr_pool_t *pool) +{ + return svn_wc__text_modified_internal_p(modified_p, filename, + force_comparison, adm_access, + TRUE, use_tmp_base, pool); +} + + +svn_error_t * svn_wc_text_modified_p(svn_boolean_t *modified_p, const char *filename, svn_boolean_t force_comparison, svn_wc_adm_access_t *adm_access, apr_pool_t *pool) { - return svn_wc__text_modified_internal_p(modified_p, filename, - force_comparison, adm_access, - TRUE, pool); + return svn_wc_text_modified_p2(modified_p, filename, force_comparison, + FALSE, adm_access, pool); } Index: subversion/libsvn_wc/status.c =================================================================== --- subversion/libsvn_wc/status.c (revision 20268) +++ subversion/libsvn_wc/status.c (working copy) @@ -371,8 +371,8 @@ && (wc_special == path_special) #endif /* HAVE_SYMLINK */ ) - SVN_ERR(svn_wc_text_modified_p(&text_modified_p, path, FALSE, - adm_access, pool)); + SVN_ERR(svn_wc_text_modified_p2(&text_modified_p, path, FALSE, + FALSE, adm_access, pool)); if (text_modified_p) final_text_status = svn_wc_status_modified; Index: subversion/libsvn_wc/update_editor.c =================================================================== --- subversion/libsvn_wc/update_editor.c (revision 20268) +++ subversion/libsvn_wc/update_editor.c (working copy) @@ -84,6 +84,9 @@ /* Was the update-target deleted? This is a special situation. */ svn_boolean_t target_deleted; + + /* Allow unversioned obstructions when adding a path. */ + svn_boolean_t allow_obstructions; /* Non-null if this is a 'switch' operation. */ const char *switch_url; @@ -515,6 +518,10 @@ /* Set if this file is new. */ svn_boolean_t added; + /* Set if an unversioned file of the same name already existed in + this directory. */ + svn_boolean_t existed; + /* This gets set if the file underwent a text change, which guides the code that syncs up the adm dir and working copy. */ svn_boolean_t text_changed; @@ -577,6 +584,7 @@ f->propchanges = apr_array_make(pool, 1, sizeof(svn_prop_t)); f->bump_info = pb->bump_info; f->added = adding; + f->existed = FALSE; f->dir_baton = pb; /* No need to initialize f->digest, since we used pcalloc(). */ @@ -1050,6 +1058,7 @@ struct edit_baton *eb = pb->edit_baton; struct dir_baton *db = make_dir_baton(path, eb, pb, TRUE, pool); svn_node_kind_t kind; + svn_boolean_t exists = FALSE; /* Semantic check. Either both "copyfrom" args are valid, or they're NULL and SVN_INVALID_REVNUM. A mixture is illegal semantics. */ @@ -1057,15 +1066,65 @@ || ((! copyfrom_path) && (SVN_IS_VALID_REVNUM(copyfrom_revision)))) abort(); - /* There should be nothing with this name. */ SVN_ERR(svn_io_check_path(db->path, &kind, db->pool)); - if (kind != svn_node_none) - return svn_error_createf - (SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, - _("Failed to add directory '%s': " - "object of the same name already exists"), - svn_path_local_style(db->path, pool)); + /* Check for obstructions. */ + if (eb->allow_obstructions) + { + /* The path can exist, but it must be a directory... */ + if (kind == svn_node_file || kind == svn_node_unknown) + return svn_error_createf + (SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, + _("Failed to add directory '%s': a non-directory object of the " + "same name already exists"), + svn_path_local_style(db->path, pool)); + else if (kind == svn_node_dir) + { + /* ...Ok, it's a directory but it can't be versioned. We don't + handle that, yet... */ + svn_wc_adm_access_t *adm_access; + const char *obstructing_path = svn_path_join( + pb->path, svn_path_basename(path, pool), pool); + + /* Test the obstructing dir to see if it's versioned. */ + svn_error_t *err = svn_wc_adm_open3(&adm_access, NULL, + obstructing_path, FALSE, 0, + NULL, NULL, pool); + if (err && err->apr_err != SVN_ERR_WC_NOT_DIRECTORY) + { + /* Something quite unexepected has happened. */ + return err; + } + else if (err) + { + /* Obstructing dir is not versioned, just need to flag it as + existing then we are done here. */ + exists = TRUE; + svn_error_clear(err); + } + else + { + /* Obstructing dir *is* versioned. */ + return svn_error_createf + (SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, + _("Failed to forcibly add directory '%s': a versioned " + "directory of the same name already exists"), + svn_path_local_style(db->path, pool)); + } + } + } + else + { + /* There should be nothing with this name if obstructions are + not allowed. */ + if (kind != svn_node_none) + return svn_error_createf + (SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, + _("Failed to add directory '%s': " + "object of the same name already exists"), + svn_path_local_style(db->path, pool)); + } + /* It may not be named the same as the administrative directory. */ if (svn_wc_is_adm_dir(svn_path_basename(path, pool), pool)) return svn_error_createf @@ -1135,9 +1194,10 @@ if (eb->notify_func) { - svn_wc_notify_t *notify = svn_wc_create_notify(db->path, - svn_wc_notify_update_add, - pool); + svn_wc_notify_t *notify = svn_wc_create_notify( + db->path, + exists ? svn_wc_notify_exists : svn_wc_notify_update_add, + pool); notify->kind = svn_node_dir; (*eb->notify_func)(eb->notify_baton, notify, pool); } @@ -1461,13 +1521,33 @@ /* Sanity checks. */ - /* If adding, there should be nothing with this name. */ + /* If adding, there should be nothing with this name unless obstructions + are permitted. */ if (adding && (kind != svn_node_none)) - return svn_error_createf - (SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, - _("Failed to add file '%s': object of the same name already exists"), - svn_path_local_style(fb->path, pool)); - + { + if (eb->allow_obstructions) + { + /* The name can exist, but it better *really* be a file. If + it is treat the existing file as a WC modification by the + user. */ + if (kind != svn_node_file) + return svn_error_createf(SVN_ERR_WC_OBSTRUCTED_UPDATE, + NULL, + _("Failed to add file '%s': " + "a non-file object of the same " + "name already exists"), + svn_path_local_style(fb->path, + pool)); + fb->existed = TRUE; + } + else + { + return svn_error_createf + (SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, + _("Failed to add file '%s': object of the same name " + "already exists"), svn_path_local_style(fb->path, pool)); + } + } /* sussman sez: If we're trying to add a file that's already in `entries' (but not on disk), that's okay. It's probably because the user deleted the working version and ran 'svn up' as a means @@ -1684,9 +1764,9 @@ applied. */ fb->prop_changed = 1; - /* Special case: if the file is added during a checkout, cache the - last-changed-date propval for future use. */ - if (eb->use_commit_times + /* Special case: if the file is added during a checkout or existed, cache + the last-changed-date propval for future use. */ + if ((eb->use_commit_times || fb->existed) && (strcmp(name, SVN_PROP_ENTRY_COMMITTED_DATE) == 0) && value) fb->last_changed_date = apr_pstrdup(fb->pool, value->data); @@ -1847,6 +1927,10 @@ * generated log commands. If there is no text base, HAS_NEW_TEXT_BASE * must be FALSE. * + * If USE_OBSTRUCTION is TRUE, FILE_PATH exists but is not already under + * version control, and FILE_PATH differs from temporary text-base then + * don't merge any textual changes, instead leave FILE_PATH as-is. + * * The caller also provides the property changes for the file in the * PROP_CHANGES array; if there are no prop changes, then the caller must pass * NULL instead. This argument is an array of svn_prop_t structures, @@ -1892,6 +1976,7 @@ const char *file_path, svn_revnum_t new_revision, svn_boolean_t has_new_text_base, + svn_boolean_t use_obstruction, const apr_array_header_t *prop_changes, const char *new_URL, const char *diff3_cmd, @@ -1948,8 +2033,9 @@ file_path, prop_changes, pool)); /* Has the user made local mods to the working file? */ - SVN_ERR(svn_wc_text_modified_p(&is_locally_modified, file_path, - FALSE, adm_access, pool)); + SVN_ERR(svn_wc_text_modified_p2(&is_locally_modified, file_path, + FALSE, use_obstruction, adm_access, + pool)); /* In the case where the user has replaced a file with a copy it may * not show as locally modified. So if the file isn't listed as @@ -2011,15 +2097,18 @@ /* For 'textual' merging, we implement this matrix. - Text file Binary File - ----------------------------------------------- - Local Mods | svn_wc_merge uses diff3, | svn_wc_merge | - | possibly makes backups & | makes backups, | - | marks file as conflicted.| marks conflicted | - ----------------------------------------------- - No Mods | Just overwrite working file. | - | | - ----------------------------------------------- + Text file Binary File + ----------------------------------------------- + Local Mods && | svn_wc_merge uses diff3, | svn_wc_merge | + !USE_OBSTRUCTION | possibly makes backups & | makes backups, | + | marks file as conflicted.| marks conflicted | + ----------------------------------------------- + Local Mods && | Just leave obstructing file as-is. | + USE_OBSTRUCTION | | + ----------------------------------------------- + No Mods | Just overwrite working file. | + | | + ----------------------------------------------- So the first thing we do is figure out where we are in the matrix. */ @@ -2048,6 +2137,17 @@ svn_wc__copy_translate, tmp_txtb, base_name, FALSE, pool)); } + else if (use_obstruction) /* Leave obstruction as-is. */ + { + svn_wc_entry_t tmp_entry; + + SVN_ERR (svn_time_from_cstring(&(tmp_entry.text_time), + timestamp_string,pool)); + + SVN_ERR(svn_wc__loggy_entry_modify( + &log_accum, adm_access, base_name, &tmp_entry, + SVN_WC__ENTRY_MODIFY_TEXT_TIME, pool)); + } else /* working file exists, and has local mods.*/ { /* Now we need to let loose svn_wc_merge2() to merge the @@ -2139,8 +2239,9 @@ /* Log commands to handle text-timestamp */ if (!is_locally_modified) { - if (timestamp_string) - /* Adjust working copy file */ + if (timestamp_string && !use_obstruction) + /* Adjust working copy file unless this file is an allowed + obstruction. */ SVN_ERR(svn_wc__loggy_set_timestamp(&log_accum, adm_access, base_name, timestamp_string, pool)); @@ -2229,6 +2330,7 @@ fb->path, (*eb->target_revision), fb->text_changed, + fb->existed, propchanges, fb->new_URL, eb->diff3_cmd, @@ -2251,8 +2353,12 @@ { svn_wc_notify_t *notify = svn_wc_create_notify(fb->path, - fb->added ? svn_wc_notify_update_add - : svn_wc_notify_update_update, pool); + (fb->existed + ? svn_wc_notify_exists + : (fb->added + ? svn_wc_notify_update_add + : svn_wc_notify_update_update)), + pool); notify->kind = svn_node_file; notify->content_state = content_state; notify->prop_state = prop_state; @@ -2342,6 +2448,7 @@ svn_boolean_t use_commit_times, const char *switch_url, svn_boolean_t recurse, + svn_boolean_t allow_obstructions, svn_wc_notify_func2_t notify_func, void *notify_baton, svn_cancel_func_t cancel_func, @@ -2372,21 +2479,22 @@ /* Construct an edit baton. */ eb = apr_pcalloc(subpool, sizeof(*eb)); - eb->pool = subpool; - eb->use_commit_times= use_commit_times; - eb->target_revision = target_revision; - eb->switch_url = switch_url; - eb->repos = entry ? entry->repos : NULL; - eb->adm_access = adm_access; - eb->anchor = anchor; - eb->target = target; - eb->recurse = recurse; - eb->notify_func = notify_func; - eb->notify_baton = notify_baton; - eb->traversal_info = traversal_info; - eb->diff3_cmd = diff3_cmd; - eb->cancel_func = cancel_func; - eb->cancel_baton = cancel_baton; + eb->pool = subpool; + eb->use_commit_times = use_commit_times; + eb->target_revision = target_revision; + eb->switch_url = switch_url; + eb->repos = entry ? entry->repos : NULL; + eb->adm_access = adm_access; + eb->anchor = anchor; + eb->target = target; + eb->recurse = recurse; + eb->notify_func = notify_func; + eb->notify_baton = notify_baton; + eb->traversal_info = traversal_info; + eb->diff3_cmd = diff3_cmd; + eb->cancel_func = cancel_func; + eb->cancel_baton = cancel_baton; + eb->allow_obstructions = allow_obstructions; /* Construct an editor. */ tree_editor->set_target_revision = set_target_revision; @@ -2418,11 +2526,12 @@ svn_error_t * -svn_wc_get_update_editor2(svn_revnum_t *target_revision, +svn_wc_get_update_editor3(svn_revnum_t *target_revision, svn_wc_adm_access_t *anchor, const char *target, svn_boolean_t use_commit_times, svn_boolean_t recurse, + svn_boolean_t allow_obstructions, svn_wc_notify_func2_t notify_func, void *notify_baton, svn_cancel_func_t cancel_func, @@ -2434,12 +2543,37 @@ apr_pool_t *pool) { return make_editor(target_revision, anchor, svn_wc_adm_access_path(anchor), - target, use_commit_times, NULL, recurse, notify_func, - notify_baton, cancel_func, cancel_baton, diff3_cmd, - editor, edit_baton, traversal_info, pool); + target, use_commit_times, NULL, recurse, + allow_obstructions, notify_func, notify_baton, + cancel_func, cancel_baton, diff3_cmd, editor, + edit_baton, traversal_info, pool); } + svn_error_t * +svn_wc_get_update_editor2(svn_revnum_t *target_revision, + svn_wc_adm_access_t *anchor, + const char *target, + svn_boolean_t use_commit_times, + svn_boolean_t recurse, + svn_wc_notify_func2_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + const char *diff3_cmd, + const svn_delta_editor_t **editor, + void **edit_baton, + svn_wc_traversal_info_t *traversal_info, + apr_pool_t *pool) +{ + return svn_wc_get_update_editor3(target_revision, anchor, target, + use_commit_times, recurse, FALSE, + notify_func, notify_baton, cancel_func, + cancel_baton, diff3_cmd, editor, + edit_baton, traversal_info, pool); +} + +svn_error_t * svn_wc_get_update_editor(svn_revnum_t *target_revision, svn_wc_adm_access_t *anchor, const char *target, @@ -2459,20 +2593,21 @@ nb->func = notify_func; nb->baton = notify_baton; - return svn_wc_get_update_editor2(target_revision, anchor, target, - use_commit_times, recurse, + return svn_wc_get_update_editor3(target_revision, anchor, target, + use_commit_times, recurse, FALSE, svn_wc__compat_call_notify_func, nb, cancel_func, cancel_baton, diff3_cmd, editor, edit_baton, traversal_info, pool); } svn_error_t * -svn_wc_get_switch_editor2(svn_revnum_t *target_revision, +svn_wc_get_switch_editor3(svn_revnum_t *target_revision, svn_wc_adm_access_t *anchor, const char *target, const char *switch_url, svn_boolean_t use_commit_times, svn_boolean_t recurse, + svn_boolean_t allow_obstructions, svn_wc_notify_func2_t notify_func, void *notify_baton, svn_cancel_func_t cancel_func, @@ -2486,12 +2621,40 @@ assert(switch_url); return make_editor(target_revision, anchor, svn_wc_adm_access_path(anchor), - target, use_commit_times, switch_url, recurse, - notify_func, notify_baton, cancel_func, cancel_baton, - diff3_cmd, editor, edit_baton, traversal_info, pool); + target, use_commit_times, switch_url, recurse, + allow_obstructions, notify_func, notify_baton, + cancel_func, cancel_baton, diff3_cmd, editor, + edit_baton, traversal_info, pool); } svn_error_t * +svn_wc_get_switch_editor2(svn_revnum_t *target_revision, + svn_wc_adm_access_t *anchor, + const char *target, + const char *switch_url, + svn_boolean_t use_commit_times, + svn_boolean_t recurse, + svn_wc_notify_func2_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + const char *diff3_cmd, + const svn_delta_editor_t **editor, + void **edit_baton, + svn_wc_traversal_info_t *traversal_info, + apr_pool_t *pool) +{ + assert(switch_url); + + return svn_wc_get_switch_editor3(target_revision, anchor, target, + switch_url, use_commit_times, recurse, + FALSE, notify_func, notify_baton, + cancel_func, cancel_baton, diff3_cmd, + editor, edit_baton, traversal_info, + pool); +} + +svn_error_t * svn_wc_get_switch_editor(svn_revnum_t *target_revision, svn_wc_adm_access_t *anchor, const char *target, @@ -2512,9 +2675,9 @@ nb->func = notify_func; nb->baton = notify_baton; - return svn_wc_get_switch_editor2(target_revision, anchor, target, + return svn_wc_get_switch_editor3(target_revision, anchor, target, switch_url, use_commit_times, recurse, - svn_wc__compat_call_notify_func, nb, + FALSE, svn_wc__compat_call_notify_func, nb, cancel_func, cancel_baton, diff3_cmd, editor, edit_baton, traversal_info, pool); } Index: subversion/libsvn_wc/wc.h =================================================================== --- subversion/libsvn_wc/wc.h (revision 20268) +++ subversion/libsvn_wc/wc.h (working copy) @@ -185,6 +185,9 @@ * detranslated version of *FILENAME and the text base, otherwise, a * translated version of the text base and *FILENAME will be compared. * + * If USE_TMP_TEXTBASE is true the temporary text base is used in the + * comparison. + * * If FILENAME does not exist, consider it unmodified. If it exists * but is not under revision control (not even scheduled for * addition), return the error SVN_ERR_ENTRY_NOT_FOUND. @@ -195,6 +198,7 @@ svn_boolean_t force_comparison, svn_wc_adm_access_t *adm_access, svn_boolean_t compare_textbases, + svn_boolean_t use_tmp_textbase, apr_pool_t *pool); #ifdef __cplusplus Index: subversion/svn/checkout-cmd.c =================================================================== --- subversion/svn/checkout-cmd.c (revision 20268) +++ subversion/svn/checkout-cmd.c (working copy) @@ -156,11 +156,12 @@ revision.kind = svn_opt_revision_head; } - SVN_ERR(svn_client_checkout2(NULL, true_url, target_dir, + SVN_ERR(svn_client_checkout3(NULL, true_url, target_dir, &peg_revision, &revision, opt_state->nonrecursive ? FALSE : TRUE, opt_state->ignore_externals, + opt_state->force, ctx, subpool)); } svn_pool_destroy(subpool); Index: subversion/svn/main.c =================================================================== --- subversion/svn/main.c (revision 20268) +++ subversion/svn/main.c (working copy) @@ -230,9 +230,19 @@ " If PATH is omitted, the basename of the URL will be used as\n" " the destination. If multiple URLs are given each will be checked\n" " out into a sub-directory of PATH, with the name of the sub-directory\n" - " being the basename of the URL.\n"), - {'r', 'q', 'N', SVN_CL__AUTH_OPTIONS, svn_cl__config_dir_opt, - svn_cl__ignore_externals_opt} }, + " being the basename of the URL.\n" + "\n" + " If --force is used, unversioned obstructing paths in the working\n" + " copy destination do not automatically cause the check out to fail.\n" + " If the obstructing path is the same type (file or directory) as the\n" + " corresponding path in the repository it will be left 'as-is' in the\n" + " working copy. For directories this simply means the obstruction is\n" + " tolerated. For files, any content differences between the\n" + " obstruction and the repository are treated like a local modification\n" + " to the working copy. All properties from the repository are applied\n" + " to the obstructing path.\n"), + {'r', 'q', 'N', svn_cl__force_opt, SVN_CL__AUTH_OPTIONS, + svn_cl__config_dir_opt, svn_cl__ignore_externals_opt} }, { "cleanup", svn_cl__cleanup, {0}, N_ ("Recursively clean up the working copy, removing locks, resuming\n" @@ -704,9 +714,19 @@ " 2. Rewrite working copy URL metadata to reflect a syntactic change only.\n" " This is used when repository's root URL changes (such as a scheme\n" " or hostname change) but your working copy still reflects the same\n" - " directory within the same repository.\n"), + " directory within the same repository.\n" + "\n" + " If --force is used, unversioned obstructing paths in the working\n" + " copy do not automatically cause a failure if the switch attempts to\n" + " add the same path. If the obstructing path is the same type (file or\n" + " directory) as the corresponding path in the new URL it will be left\n" + " 'as-is' in the working copy. For directories this simply means the\n" + " obstruction is tolerated. For files, any content differences between\n" + " the obstruction and the repository are treated like a local modification\n" + " to the working copy. All properties from the repository are applied to\n" + " the obstructing path.\n"), { 'r', 'N', 'q', svn_cl__merge_cmd_opt, svn_cl__relocate_opt, - SVN_CL__AUTH_OPTIONS, svn_cl__config_dir_opt} }, + SVN_CL__AUTH_OPTIONS, svn_cl__config_dir_opt, svn_cl__force_opt} }, { "unlock", svn_cl__unlock, {0}, N_ ("Unlock working copy paths or URLs.\n" @@ -731,13 +751,26 @@ " U Updated\n" " C Conflict\n" " G Merged\n" + " E Existed\n" "\n" " A character in the first column signifies an update to the actual file,\n" " while updates to the file's properties are shown in the second column.\n" " A 'B' in the third column signifies that the lock for the file has\n" - " been broken or stolen.\n"), - {'r', 'N', 'q', svn_cl__merge_cmd_opt, SVN_CL__AUTH_OPTIONS, - svn_cl__config_dir_opt, svn_cl__ignore_externals_opt} }, + " been broken or stolen.\n" + "\n" + " If --force is used, unversioned obstructing paths in the working\n" + " copy do not automatically cause a failure if the update attempts to\n" + " add the same path. If the obstructing path is the same type (file or\n" + " directory) as the corresponding path in the repository it will be left\n" + " 'as-is' in the working copy. For directories this simply means the\n" + " obstruction is tolerated. For files, any content differences between\n" + " the obstruction and the repository are treated like a local modification\n" + " to the working copy. All properties from the repository are applied to\n" + " the obstructing path. Obstructing paths are reported in the first\n" + " column with code 'E':\n"), + {'r', 'N', 'q', svn_cl__merge_cmd_opt, svn_cl__force_opt, + SVN_CL__AUTH_OPTIONS, svn_cl__config_dir_opt, + svn_cl__ignore_externals_opt} }, { NULL, NULL, {0}, NULL, {0} } }; Index: subversion/svn/notify.c =================================================================== --- subversion/svn/notify.c (revision 20268) +++ subversion/svn/notify.c (working copy) @@ -90,6 +90,12 @@ goto print_error; break; + case svn_wc_notify_exists: + nb->received_some_change = TRUE; + if ((err = svn_cmdline_printf(pool, "E %s\n", path_local))) + goto print_error; + break; + case svn_wc_notify_restore: if ((err = svn_cmdline_printf(pool, _("Restored '%s'\n"), path_local))) Index: subversion/svn/switch-cmd.c =================================================================== --- subversion/svn/switch-cmd.c (revision 20268) +++ subversion/svn/switch-cmd.c (working copy) @@ -150,10 +150,11 @@ FALSE, FALSE, pool); /* Do the 'switch' update. */ - SVN_ERR(svn_client_switch(NULL, target, switch_url, - &(opt_state->start_revision), - opt_state->nonrecursive ? FALSE : TRUE, - ctx, pool)); + SVN_ERR(svn_client_switch2(NULL, target, switch_url, + &(opt_state->start_revision), + opt_state->nonrecursive ? FALSE : TRUE, + opt_state->force, + ctx, pool)); return SVN_NO_ERROR; } Index: subversion/svn/update-cmd.c =================================================================== --- subversion/svn/update-cmd.c (revision 20268) +++ subversion/svn/update-cmd.c (working copy) @@ -51,10 +51,11 @@ svn_cl__get_notifier(&ctx->notify_func2, &ctx->notify_baton2, FALSE, FALSE, FALSE, pool); - SVN_ERR(svn_client_update2(NULL, targets, + SVN_ERR(svn_client_update3(NULL, targets, &(opt_state->start_revision), opt_state->nonrecursive ? FALSE : TRUE, opt_state->ignore_externals, + opt_state->force, ctx, pool)); return SVN_NO_ERROR; Index: subversion/tests/cmdline/checkout_tests.py =================================================================== --- subversion/tests/cmdline/checkout_tests.py (revision 0) +++ subversion/tests/cmdline/checkout_tests.py (revision 0) @@ -0,0 +1,371 @@ +#!/usr/bin/env python +# +# checkout_tests.py: Testing checkout --force behavior when local +# tree already exits. +# +# Subversion is a tool for revision control. +# See http://subversion.tigris.org for more information. +# +# ==================================================================== +# Copyright (c) 2000-2006 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. +# +###################################################################### + +# General modules +import sys, re, os + +# Our testing module +import svntest +from svntest import wc, SVNAnyOutput + +# (abbreviation) +Item = wc.StateItem + +#---------------------------------------------------------------------- +# Helper function to set up an existing local tree that has paths which +# obstruct with the incoming WC. +# +# Build a sandbox SBOX without a WC. Created the following paths +# rooted at SBOX.WC_DIR: +# +# iota +# A/ +# A/mu +# +# If MOD_FILES is FALSE, 'iota' and 'A/mu' have the same contents as the +# standard greek tree. If TRUE the contents of each as set as follows: +# +# iota : contents == "This is the local version of the file 'iota'.\n" +# A/mu : contents == "This is the local version of the file 'mu'.\n" +# +# If ADD_UNVERSIONED is TRUE, add the following files and directories, +# rooted in SBOX.WC_DIR, that don't exist in the standard greek tree: +# +# 'sigma' +# 'A/upsilon' +# 'A/Z/' +# +# Return the expected output for svn co --force SBOX.REPO_URL SBOX.WC_DIR +# +def make_local_tree(sbox, mod_files=False, add_unversioned=False): + """Make a local unversioned tree to checkout into.""" + + sbox.build(create_wc = False) + + if os.path.exists(sbox.wc_dir): + svntest.main.safe_rmtree(sbox.wc_dir) + + export_target = sbox.wc_dir + expected_output = svntest.main.greek_state.copy() + expected_output.wc_dir = sbox.wc_dir + expected_output.desc[""] = Item() + expected_output.tweak(contents=None, status="A ") + + # Export an unversioned tree to sbox.wc_dir. + svntest.actions.run_and_verify_export(svntest.main.current_repo_url, + export_target, + expected_output, + svntest.main.greek_state.copy()) + + # Remove everything remaining except for 'iota', 'A/', and 'A/mu'. + svntest.main.safe_rmtree(os.path.join(sbox.wc_dir, "A", "B")) + svntest.main.safe_rmtree(os.path.join(sbox.wc_dir, "A", "C")) + svntest.main.safe_rmtree(os.path.join(sbox.wc_dir, "A", "D")) + + # Should obstructions differ from the standard greek tree? + if mod_files: + iota_path = os.path.join(sbox.wc_dir, "iota") + mu_path = os.path.join(sbox.wc_dir, "A", "mu") + svntest.main.file_write(iota_path, + "This is the local version of the file 'iota'.\n") + svntest.main.file_write(mu_path, + "This is the local version of the file 'mu'.\n") + + # Add some files that won't obstruct anything in standard greek tree? + if add_unversioned: + sigma_path = os.path.join(sbox.wc_dir, "sigma") + svntest.main.file_append(sigma_path, "unversioned sigma") + upsilon_path = os.path.join(sbox.wc_dir, "A", "upsilon") + svntest.main.file_append(upsilon_path, "unversioned upsilon") + Z_path = os.path.join(sbox.wc_dir, "A", "Z") + os.mkdir(Z_path) + + return wc.State(sbox.wc_dir, { + "A" : Item(status='E '), # Obstruction + "A/B" : Item(status='A '), + "A/B/lambda" : Item(status='A '), + "A/B/E" : Item(status='A '), + "A/B/E/alpha" : Item(status='A '), + "A/B/E/beta" : Item(status='A '), + "A/B/F" : Item(status='A '), + "A/mu" : Item(status='E '), # Obstruction + "A/C" : Item(status='A '), + "A/D" : Item(status='A '), + "A/D/gamma" : Item(status='A '), + "A/D/G" : Item(status='A '), + "A/D/G/pi" : Item(status='A '), + "A/D/G/rho" : Item(status='A '), + "A/D/G/tau" : Item(status='A '), + "A/D/H" : Item(status='A '), + "A/D/H/chi" : Item(status='A '), + "A/D/H/omega" : Item(status='A '), + "A/D/H/psi" : Item(status='A '), + "iota" : Item(status='E '), # Obstruction + }) + +###################################################################### +# Tests +# +# Each test must return on success or raise on failure. +#---------------------------------------------------------------------- + +def checkout_with_obstructions(sbox): + """co with obstructions should fail without --force""" + + make_local_tree(sbox, False, False) + + svntest.actions.run_and_verify_svn("No error where some expected", + None, SVNAnyOutput, "co", + svntest.main.current_repo_url, + sbox.wc_dir) + +#---------------------------------------------------------------------- + +def forced_checkout_of_file_with_dir_obstructions(sbox): + """forced co fails if a dir obstructs a file""" + + make_local_tree(sbox, False, False) + + # Make the "other" working copy + other_wc = sbox.add_wc_path('other') + os.mkdir(other_wc) + os.mkdir(os.path.join(other_wc, "iota")) + + # Checkout the standard greek repos into a directory that has a dir named + # "iota" obstructing the file "iota" in the repos. This should fail. + sout, serr = svntest.actions.run_and_verify_svn("Expected error during co", + None, SVNAnyOutput, "co", + "--force", sbox.repo_url, + other_wc) + + exp_err_re = re.compile(".*Failed to add file.*a non-file object of " + \ + "the same name already exists") + for line in serr: + if exp_err_re.search(line): + return + raise svntest.Failure("forced co failed but not in the expected way") + +#---------------------------------------------------------------------- + +def forced_checkout_of_dir_with_file_obstructions(sbox): + """forced co fails if a file obstructs a dir""" + + make_local_tree(sbox, False, False) + + # Make the "other" working copy + other_wc = sbox.add_wc_path('other') + os.mkdir(other_wc) + svntest.main.file_append(os.path.join(other_wc, "A"), "The file A") + + # Checkout the standard greek repos into a directory that has a file named + # "A" obstructing the dir "A" in the repos. This should fail. + sout, serr = svntest.actions.run_and_verify_svn("Expected error during co", + None, SVNAnyOutput, "co", + "--force", sbox.repo_url, + other_wc) + + exp_err_re = re.compile(".*Failed to add directory.*a non-directory " + \ + "object of the same name already exists") + for line in serr: + if exp_err_re.search(line): + return + raise svntest.Failure("forced co failed but not in the expected way") + +#---------------------------------------------------------------------- + +def forced_checkout_with_faux_obstructions(sbox): + """co with faux obstructions ok with --force""" + + # Make a local tree that partially obstructs the paths coming from the + # repos but has no true differences. + expected_output = make_local_tree(sbox, False, False) + + expected_wc = svntest.main.greek_state.copy() + + svntest.actions.run_and_verify_checkout(svntest.main.current_repo_url, + sbox.wc_dir, expected_output, + expected_wc, None, None, None, + None, '--force') + +#---------------------------------------------------------------------- + +def forced_checkout_with_real_obstructions(sbox): + """co with real obstructions ok with --force""" + + # Make a local tree that partially obstructs the paths coming from the + # repos and make the obstructing files different from the standard greek + # tree. + expected_output = make_local_tree(sbox, True, False) + + expected_wc = svntest.main.greek_state.copy() + expected_wc.tweak('A/mu', + contents="This is the local version of the file 'mu'.\n") + expected_wc.tweak('iota', + contents="This is the local version of the file 'iota'.\n") + + svntest.actions.run_and_verify_checkout(svntest.main.current_repo_url, + sbox.wc_dir, expected_output, + expected_wc, None, None, None, + None, '--force') + +#---------------------------------------------------------------------- + +def forced_checkout_with_real_obstructions_and_unversioned_files(sbox): + """co with real obstructions and unversioned files""" + + # Make a local tree that partially obstructs the paths coming from the + # repos, make the obstructing files different from the standard greek + # tree, and finally add some files that don't exist in the stardard tree. + expected_output = make_local_tree(sbox, True, True) + + expected_wc = svntest.main.greek_state.copy() + expected_wc.tweak('A/mu', + contents="This is the local version of the file 'mu'.\n") + expected_wc.tweak('iota', + contents="This is the local version of the file 'iota'.\n") + expected_wc.add({'sigma' : Item("unversioned sigma"), + 'A/upsilon' : Item("unversioned upsilon"), + 'A/Z' : Item(), + }) + + svntest.actions.run_and_verify_checkout(svntest.main.current_repo_url, + sbox.wc_dir, expected_output, + expected_wc, None, None, None, + None, '--force') + +#---------------------------------------------------------------------- + +def forced_checkout_with_versioned_obstruction(sbox): + """forced co with versioned obstruction""" + + # Make a greek tree working copy + sbox.build() + + # Create a second repository with the same greek tree + repo_dir = sbox.repo_dir + repo_url = sbox.repo_url + other_repo_dir, other_repo_url = sbox.add_repo_path("other") + svntest.main.copy_repos(repo_dir, other_repo_dir, 1, 1) + + other_wc_dir = sbox.add_wc_path("other") + os.mkdir(other_wc_dir) + + # Checkout "A/" from the other repos. + svntest.actions.run_and_verify_svn("Unexpected error during co", + SVNAnyOutput, [], "co", + other_repo_url + "/A", + os.path.join(other_wc_dir, "A")) + + # Checkout the first repos into "other/A". This should fail since the + # obstructing versioned directory points to a different URL. + sout, serr = svntest.actions.run_and_verify_svn("Expected error during co", + None, SVNAnyOutput, "co", + "--force", sbox.repo_url, + other_wc_dir) + + exp_err_re = re.compile(".*Failed to forcibly add directory.*a " + \ + "versioned directory of the same name " + \ + "already exists") + for line in serr: + if exp_err_re.search(line): + return + raise svntest.Failure("forced co failed but not in the expected way") + +#---------------------------------------------------------------------- +# Explicitly test Julian's import and co scenario +def import_and_checkout(sbox): + """import and checkout""" + + sbox.build() + + other_repo_dir, other_repo_url = sbox.add_repo_path("other") + import_from_dir = sbox.add_wc_path("other") + + # Export greek tree to import_from_dir + expected_output = svntest.main.greek_state.copy() + expected_output.wc_dir = import_from_dir + expected_output.desc[''] = Item() + expected_output.tweak(contents=None, status='A ') + svntest.actions.run_and_verify_export(svntest.main.current_repo_url, + import_from_dir, + expected_output, + svntest.main.greek_state.copy()) + + # Create the 'other' repos + svntest.main.run_svnadmin("create", other_repo_dir) + + # Import import_from_dir to the other repos + expected_output = svntest.wc.State(sbox.wc_dir, {}) + + svntest.actions.run_and_verify_svn(None, None, [], 'import', + '--username', svntest.main.wc_author, + '--password', svntest.main.wc_passwd, + '-m', 'Log message for new import', + import_from_dir, other_repo_url) + + expected_output = wc.State(import_from_dir, { + "A" : Item(status='E '), + "A/B" : Item(status='E '), + "A/B/lambda" : Item(status='E '), + "A/B/E" : Item(status='E '), + "A/B/E/alpha" : Item(status='E '), + "A/B/E/beta" : Item(status='E '), + "A/B/F" : Item(status='E '), + "A/mu" : Item(status='E '), + "A/C" : Item(status='E '), + "A/D" : Item(status='E '), + "A/D/gamma" : Item(status='E '), + "A/D/G" : Item(status='E '), + "A/D/G/pi" : Item(status='E '), + "A/D/G/rho" : Item(status='E '), + "A/D/G/tau" : Item(status='E '), + "A/D/H" : Item(status='E '), + "A/D/H/chi" : Item(status='E '), + "A/D/H/omega" : Item(status='E '), + "A/D/H/psi" : Item(status='E '), + "iota" : Item(status='E ') + }) + + expected_wc = svntest.main.greek_state.copy() + + svntest.actions.run_and_verify_checkout(other_repo_url, import_from_dir, + expected_output, expected_wc, + None, None, None, None, + '--force') + +#---------------------------------------------------------------------- + +# list all tests here, starting with None: +test_list = [ None, + checkout_with_obstructions, + forced_checkout_of_file_with_dir_obstructions, + forced_checkout_of_dir_with_file_obstructions, + forced_checkout_with_faux_obstructions, + forced_checkout_with_real_obstructions, + forced_checkout_with_real_obstructions_and_unversioned_files, + forced_checkout_with_versioned_obstruction, + import_and_checkout, + ] + +if __name__ == "__main__": + svntest.main.run_tests(test_list) + # NOTREACHED + + +### End of file. \ No newline at end of file Property changes on: subversion\tests\cmdline\checkout_tests.py ___________________________________________________________________ Name: svn:executable + * Name: svn:eol-style + native Index: subversion/tests/cmdline/getopt_tests_data/svn_help_log_switch_stdout =================================================================== --- subversion/tests/cmdline/getopt_tests_data/svn_help_log_switch_stdout (revision 20268) +++ subversion/tests/cmdline/getopt_tests_data/svn_help_log_switch_stdout (working copy) @@ -59,6 +59,16 @@ or hostname change) but your working copy still reflects the same directory within the same repository. + If --force is used, unversioned obstructing paths in the working + copy do not automatically cause a failure if the switch attempts to + add the same path. If the obstructing path is the same type (file or + directory) as the corresponding path in the new URL it will be left + 'as-is' in the working copy. For directories this simply means the + obstruction is tolerated. For files, any content differences between + the obstruction and the repository are treated like a local modification + to the working copy. All properties from the repository are applied to + the obstructing path. + Valid options: -r [--revision] arg : ARG (some commands also take ARG1:ARG2 range) A revision argument can be one of: @@ -77,4 +87,5 @@ --no-auth-cache : do not cache authentication tokens --non-interactive : do no interactive prompting --config-dir arg : read user configuration files from directory ARG + --force : force operation to run Index: subversion/tests/cmdline/svntest/actions.py =================================================================== --- subversion/tests/cmdline/svntest/actions.py (revision 20268) +++ subversion/tests/cmdline/svntest/actions.py (working copy) @@ -247,22 +247,35 @@ singleton_handler_a = None, a_baton = None, singleton_handler_b = None, - b_baton = None): - """Checkout the URL into a new directory WC_DIR_NAME. - + b_baton = None, + *args): + """Checkout the URL into a new directory WC_DIR_NAME. *ARGS are any + extra optional args to the checkout subcommand. + The subcommand output will be verified against OUTPUT_TREE, and the working copy itself will be verified against DISK_TREE. SINGLETON_HANDLER_A and SINGLETON_HANDLER_B will be passed to tree.compare_trees - see that function's doc string for more details. - Returns if successful and raise on failure.""" + Returns if successful and raise on failure. + WC_DIR_NAME is deleted if present unless the '--force' option is passed + in *ARGS.""" + if isinstance(output_tree, wc.State): output_tree = output_tree.old_tree() if isinstance(disk_tree, wc.State): disk_tree = disk_tree.old_tree() - # Remove dir if it's already there. - main.safe_rmtree(wc_dir_name) + # Remove dir if it's already there, unless this is a forced checkout. + # In that case assume we want to test a forced checkout's toleration + # of obstructing paths. + remove_wc = True + for arg in args: + if arg == '--force': + remove_wc = False + break + if remove_wc: + main.safe_rmtree(wc_dir_name) # Checkout and make a tree of the output, using l:foo/p:bar ### todo: svn should not be prompting for auth info when using @@ -270,7 +283,7 @@ output, errput = main.run_svn (None, 'co', '--username', main.wc_author, '--password', main.wc_passwd, - URL, wc_dir_name) + URL, wc_dir_name, *args) mytree = tree.build_tree_from_checkout (output) # Verify actual output against expected output. @@ -562,14 +575,21 @@ wc_target, switch_url, output_tree, disk_tree, status_tree, + error_re_string = None, singleton_handler_a = None, a_baton = None, singleton_handler_b = None, b_baton = None, - check_props = 0): + check_props = 0, + *args): """Switch WC_TARGET (in working copy dir WC_DIR_NAME) to SWITCH_URL. + If ERROR_RE_STRING, the switch must exit with error, and the error + message must match regular expression ERROR_RE_STRING. + + Else if ERROR_RE_STRING is None, then: + The subcommand output will be verified against OUTPUT_TREE, and the working copy itself will be verified against DISK_TREE. If optional STATUS_OUTPUT_TREE is given, then 'svn status' output will be @@ -587,10 +607,19 @@ status_tree = status_tree.old_tree() # Update and make a tree of the output. - output, errput = main.run_svn (None, 'switch', + output, errput = main.run_svn (error_re_string, 'switch', '--username', main.wc_author, '--password', main.wc_passwd, - switch_url, wc_target) + switch_url, wc_target, *args) + + if (error_re_string): + rm = re.compile(error_re_string) + for line in errput: + match = rm.search(line) + if match: + return + raise main.SVNUnmatchedError + mytree = tree.build_tree_from_checkout (output) verify_update (mytree, wc_dir_name, Index: subversion/tests/cmdline/svntest/tree.py =================================================================== --- subversion/tests/cmdline/svntest/tree.py (revision 20268) +++ subversion/tests/cmdline/svntest/tree.py (working copy) @@ -573,7 +573,7 @@ "Return a tree derived by parsing the output LINES from 'co' or 'up'." root = SVNTreeNode(root_node_name) - rm1 = re.compile ('^([MAGCUD_ ][MAGCUD_ ])([B ])\s+(.+)') + rm1 = re.compile ('^([MAGCUDE_ ][MAGCUDE_ ])([B ])\s+(.+)') # There may be other verbs we need to match, in addition to # "Restored". If so, add them as alternatives in the first match # group below. Index: subversion/tests/cmdline/switch_tests.py =================================================================== --- subversion/tests/cmdline/switch_tests.py (revision 20268) +++ subversion/tests/cmdline/switch_tests.py (working copy) @@ -1066,9 +1066,153 @@ # Make sure we didn't break the WC. expected_status = svntest.actions.get_virginal_state(wc_dir, 1) svntest.actions.run_and_verify_status(wc_dir, expected_status) - +def forced_switch(sbox): + "forced switch tolerates obstructions to adds" + sbox.build() + + # Dir obstruction + G_path = os.path.join(sbox.wc_dir, 'A', 'B', 'F', 'G') + os.mkdir(G_path) + + # Faux file obstructions + shutil.copyfile(os.path.join(sbox.wc_dir, 'A', 'D', 'gamma'), + os.path.join(sbox.wc_dir, 'A', 'B', 'F', 'gamma')) + shutil.copyfile(os.path.join(sbox.wc_dir, 'A', 'D', 'G', 'tau'), + os.path.join(sbox.wc_dir, 'A', 'B', 'F', 'G', 'tau')) + + # Real file obstruction + pi_path = os.path.join(sbox.wc_dir, 'A', 'B', 'F', 'G', 'pi') + svntest.main.file_write(pi_path, + "This is the OBSTRUCTING file 'pi'.\n") + + # Non-obstructing dir and file + I_path = os.path.join(sbox.wc_dir, 'A', 'B', 'F', 'I') + os.mkdir(I_path) + upsilon_path = os.path.join(G_path, 'upsilon') + svntest.main.file_write(upsilon_path, + "This is the unversioned file 'upsilon'.\n") + + # Setup expected results of switch. + expected_output = svntest.wc.State(sbox.wc_dir, { + "A/B/F/gamma" : Item(status='E '), + "A/B/F/G" : Item(status='E '), + "A/B/F/G/pi" : Item(status='E '), + "A/B/F/G/rho" : Item(status='A '), + "A/B/F/G/tau" : Item(status='E '), + "A/B/F/H" : Item(status='A '), + "A/B/F/H/chi" : Item(status='A '), + "A/B/F/H/omega" : Item(status='A '), + "A/B/F/H/psi" : Item(status='A '), + }) + expected_disk = svntest.main.greek_state.copy() + expected_disk.add({ + "A/B/F/gamma" : Item("This is the file 'gamma'.\n"), + "A/B/F/G" : Item(), + "A/B/F/G/pi" : Item("This is the OBSTRUCTING file 'pi'.\n"), + "A/B/F/G/rho" : Item("This is the file 'rho'.\n"), + "A/B/F/G/tau" : Item("This is the file 'tau'.\n"), + "A/B/F/G/upsilon" : Item("This is the unversioned file 'upsilon'.\n"), + "A/B/F/H" : Item(), + "A/B/F/H/chi" : Item("This is the file 'chi'.\n"), + "A/B/F/H/omega" : Item("This is the file 'omega'.\n"), + "A/B/F/H/psi" : Item("This is the file 'psi'.\n"), + "A/B/F/I" : Item(), + }) + expected_status = svntest.actions.get_virginal_state(sbox.wc_dir, 1) + expected_status.tweak('A/B/F', switched='S') + expected_status.add({ + "A/B/F/gamma" : Item(status=' ', wc_rev=1), + "A/B/F/G" : Item(status=' ', wc_rev=1), + "A/B/F/G/pi" : Item(status='M ', wc_rev=1), + "A/B/F/G/rho" : Item(status=' ', wc_rev=1), + "A/B/F/G/tau" : Item(status=' ', wc_rev=1), + "A/B/F/H" : Item(status=' ', wc_rev=1), + "A/B/F/H/chi" : Item(status=' ', wc_rev=1), + "A/B/F/H/omega" : Item(status=' ', wc_rev=1), + "A/B/F/H/psi" : Item(status=' ', wc_rev=1), + }) + + # Do the switch and check the results in three ways. + F_path = os.path.join(sbox.wc_dir, 'A', 'B', 'F') + AD_url = svntest.main.current_repo_url + '/A/D' + svntest.actions.run_and_verify_switch(sbox.wc_dir, F_path, AD_url, + expected_output, + expected_disk, + expected_status, None, + None, None, None, None, 0, + '--force') + + +def forced_switch_failures(sbox): + "forced switch fails with some types of obstuctions" + sbox.build() + + # Add a directory to obstruct a file. + pi_path = os.path.join(sbox.wc_dir, 'A', 'B', 'F', 'pi') + os.mkdir(pi_path) + + # Add a file to obstruct a directory. + H_path = os.path.join(sbox.wc_dir, 'A', 'C', 'H') + svntest.main.file_write(H_path, "The file 'H'\n") + + # Test three cases where forced switch should fail: + + # 1) A forced switch that tries to add a file when an unversioned + # directory of the same name already exists should fail. + svntest.actions.run_and_verify_switch(sbox.wc_dir, + os.path.join(sbox.wc_dir, 'A', 'C'), + sbox.repo_url + "/A/D", + None, None, None, + ".*Failed to add directory .*" + \ + ": a non-directory object of the" + \ + " same name already exists\n", + None, None, None, None, 0, '--force') + + # 2) A forced switch that tries to add a dir when a file of the same + # name already exists should fail. + svntest.actions.run_and_verify_switch(sbox.wc_dir, + os.path.join(sbox.wc_dir, + 'A', 'B', 'F'), + sbox.repo_url + "/A/D/G", + None, None, None, + ".*Failed to add file .*" + \ + ": a non-file object of the " + \ + "same name already exists\n", + None, None, None, None, 0, '--force') + + # 3) A forced update that tries to add a directory when a versioned + # directory of the same name already exists should fail. + + # Make dir A/D/H/I in repos. + I_url = sbox.repo_url + "/A/D/H/I" + so, se = svntest.actions.run_and_verify_svn("Unexpected error during mkdir", + ['\n', 'Committed revision 2.\n'], + [], "mkdir", I_url, + "-m", "Log Message") + + # Make A/D/G/I and co A/D/H/I into it. + I_path = os.path.join(sbox.wc_dir, 'A', 'D', 'G', 'I') + os.mkdir(I_path) + so, se = svntest.actions.run_and_verify_svn("Unexpected error during co", + ['Checked out revision 2.\n'], + [], "co", I_url, I_path) + + # Try the forced switch. A/D/G/I obstructs the dir A/D/G/I coming + # from the repos. Normally this isn't a problem, but A/D/G/I is already + # versioned so this should fail. + svntest.actions.run_and_verify_switch(sbox.wc_dir, + os.path.join(sbox.wc_dir, + 'A', 'D', 'G'), + sbox.repo_url + "/A/D/H", + None, None, None, + ".*Failed to forcibly add " + \ + "directory .*: a versioned " + \ + "directory of the same name " + \ + "already exists\n", + None, None, None, None, 0, '--force') + ######################################################################## # Run the tests @@ -1093,6 +1237,8 @@ relocate_beyond_repos_root, refresh_read_only_attribute, switch_change_repos_root, + forced_switch, + forced_switch_failures, ] if __name__ == '__main__': Index: subversion/tests/cmdline/update_tests.py =================================================================== --- subversion/tests/cmdline/update_tests.py (revision 20268) +++ subversion/tests/cmdline/update_tests.py (working copy) @@ -1674,7 +1674,234 @@ expected_output, expected_wc) +#---------------------------------------------------------------------- +def forced_update(sbox): + "forced update tolerates obstructions to adds" + + sbox.build() + wc_dir = sbox.wc_dir + # Make a backup copy of the working copy + wc_backup = sbox.add_wc_path('backup') + svntest.actions.duplicate_dir(wc_dir, wc_backup) + + # Make a couple of local mods to files + mu_path = os.path.join(wc_dir, 'A', 'mu') + rho_path = os.path.join(wc_dir, 'A', 'D', 'G', 'rho') + svntest.main.file_append (mu_path, 'appended mu text') + svntest.main.file_append (rho_path, 'new appended text for rho') + + # Add some files + nu_path = os.path.join(wc_dir, 'A', 'B', 'F', 'nu') + svntest.main.file_append(nu_path, "This is the file 'nu'\n") + svntest.main.run_svn(None, 'add', nu_path) + kappa_path = os.path.join(wc_dir, 'kappa') + svntest.main.file_append(kappa_path, "This is the file 'kappa'\n") + svntest.main.run_svn(None, 'add', kappa_path) + + # Add a dir with two files + I_path = os.path.join(wc_dir, 'A', 'C', 'I') + os.mkdir(I_path) + svntest.main.run_svn(None, 'add', I_path) + upsilon_path = os.path.join(I_path, 'upsilon') + svntest.main.file_append(upsilon_path, "This is the file 'upsilon'\n") + svntest.main.run_svn(None, 'add', upsilon_path) + zeta_path = os.path.join(I_path, 'zeta') + svntest.main.file_append(zeta_path, "This is the file 'zeta'\n") + svntest.main.run_svn(None, 'add', zeta_path) + + # Created expected output tree for 'svn ci' + expected_output = wc.State(wc_dir, { + 'A/mu' : Item(verb='Sending'), + 'A/D/G/rho' : Item(verb='Sending'), + 'A/B/F/nu' : Item(verb='Adding'), + 'kappa' : Item(verb='Adding'), + 'A/C/I' : Item(verb='Adding'), + 'A/C/I/upsilon' : Item(verb='Adding'), + 'A/C/I/zeta' : Item(verb='Adding'), + }) + + # Create expected status tree. + expected_status = svntest.actions.get_virginal_state(wc_dir, 2) + expected_status.tweak(wc_rev=1) + expected_status.add({ + 'A/B/F/nu' : Item(status=' ', wc_rev=2), + 'kappa' : Item(status=' ', wc_rev=2), + 'A/C/I' : Item(status=' ', wc_rev=2), + 'A/C/I/upsilon' : Item(status=' ', wc_rev=2), + 'A/C/I/zeta' : Item(status=' ', wc_rev=2), + }) + expected_status.tweak('A/mu', 'A/D/G/rho', wc_rev=2) + + # Commit. + svntest.actions.run_and_verify_commit (wc_dir, expected_output, + expected_status, None, + None, None, None, None, wc_dir) + + # Make a local mod to mu that will merge cleanly. + backup_mu_path = os.path.join(wc_backup, 'A', 'mu') + svntest.main.file_append (backup_mu_path, 'appended mu text') + + # Create unversioned files and dir that will obstruct A/B/F/nu, kappa, + # A/C/I, and A/C/I/upsilon coming from repos during update. + # The obstructing nu has the same contents as the repos, while kappa and + # upsilon differ, which means the latter two should show as modified after + # the forced update. + nu_path = os.path.join(wc_backup, 'A', 'B', 'F', 'nu') + svntest.main.file_append(nu_path, "This is the file 'nu'\n") + kappa_path = os.path.join(wc_backup, 'kappa') + svntest.main.file_append(kappa_path, + "This is the OBSTRUCTING file 'kappa'\n") + I_path = os.path.join(wc_backup, 'A', 'C', 'I') + os.mkdir(I_path) + upsilon_path = os.path.join(I_path, 'upsilon') + svntest.main.file_append(upsilon_path, + "This is the OBSTRUCTING file 'upsilon'\n") + + # Create expected output tree for an update of the wc_backup. + # mu and rho are run of the mill update operations; merge and update + # respectively. + # kappa, nu, I, and upsilon all 'E'xisted as unversioned items in the WC. + # While the dir I does exist, zeta does not so it's just an add. + expected_output = wc.State(wc_backup, { + 'A/mu' : Item(status='G '), + 'A/D/G/rho' : Item(status='U '), + 'kappa' : Item(status='E '), + 'A/B/F/nu' : Item(status='E '), + 'A/C/I' : Item(status='E '), + 'A/C/I/upsilon' : Item(status='E '), + 'A/C/I/zeta' : Item(status='A '), + }) + + # Create expected output tree for an update of the wc_backup. + # + # - mu and rho are run of the mill update operations; merge and update + # respectively. + # + # - kappa, nu, I, and upsilon all 'E'xisted as unversioned items in the WC. + # + # - While the dir I does exist, I/zeta does not so it's just an add. + expected_disk = svntest.main.greek_state.copy() + expected_disk.add({ + 'A/B/F/nu' : Item("This is the file 'nu'\n"), + 'kappa' : Item("This is the OBSTRUCTING file 'kappa'\n"), + 'A/C/I' : Item(), + 'A/C/I/upsilon' : Item("This is the OBSTRUCTING file 'upsilon'\n"), + 'A/C/I/zeta' : Item("This is the file 'zeta'\n"), + }) + expected_disk.tweak('A/mu', + contents=expected_disk.desc['A/mu'].contents + + 'appended mu text') + expected_disk.tweak('A/D/G/rho', + contents=expected_disk.desc['A/D/G/rho'].contents + + 'new appended text for rho') + + # Create expected status tree for the update. Since the obstructing + # kappa and upsilon differ from the repos, they should show as modified. + expected_status = svntest.actions.get_virginal_state(wc_backup, 2) + expected_status.add({ + 'A/B/F/nu' : Item(status=' ', wc_rev=2), + 'A/C/I' : Item(status=' ', wc_rev=2), + 'A/C/I/zeta' : Item(status=' ', wc_rev=2), + 'kappa' : Item(status='M ', wc_rev=2), + 'A/C/I/upsilon' : Item(status='M ', wc_rev=2), + }) + + # Perform forced update and check the results in three ways. + svntest.actions.run_and_verify_update(wc_backup, + expected_output, + expected_disk, + expected_status, + None, None, None, None, None, 0, + wc_backup, '--force') + +#---------------------------------------------------------------------- +def forced_update_failures(sbox): + "forced up fails with some types of obstructions" + + sbox.build() + wc_dir = sbox.wc_dir + + # Make a backup copy of the working copy + wc_backup = sbox.add_wc_path('backup') + svntest.actions.duplicate_dir(wc_dir, wc_backup) + + # Add a file + nu_path = os.path.join(wc_dir, 'A', 'B', 'F', 'nu') + svntest.main.file_append(nu_path, "This is the file 'nu'\n") + svntest.main.run_svn(None, 'add', nu_path) + + # Add a dir + I_path = os.path.join(wc_dir, 'A', 'C', 'I') + os.mkdir(I_path) + svntest.main.run_svn(None, 'add', I_path) + + # Created expected output tree for 'svn ci' + expected_output = wc.State(wc_dir, { + 'A/B/F/nu' : Item(verb='Adding'), + 'A/C/I' : Item(verb='Adding'), + }) + + # Create expected status tree. + expected_status = svntest.actions.get_virginal_state(wc_dir, 2) + expected_status.tweak(wc_rev=1) + expected_status.add({ + 'A/B/F/nu' : Item(status=' ', wc_rev=2), + 'A/C/I' : Item(status=' ', wc_rev=2), + }) + + # Commit. + svntest.actions.run_and_verify_commit(wc_dir, expected_output, + expected_status, None, + None, None, None, None, wc_dir) + + # Create an unversioned dir A/B/F/nu that will obstruct the file of the + # same name coming from the repository. Create an unversioned file A/C/I + # that will obstruct the dir of the same name. + nu_path = os.path.join(wc_backup, 'A', 'B', 'F', 'nu') + os.mkdir(nu_path) + I_path = os.path.join(wc_backup, 'A', 'C', 'I') + svntest.main.file_append(I_path, + "This is the file 'I'...shouldn't I be a dir?\n") + + # A forced update that tries to add a file when an unversioned directory + # of the same name already exists should fail. + F_Path = os.path.join(wc_backup, 'A', 'B', 'F') + svntest.actions.run_and_verify_update(F_Path, None, None, None, + ".*Failed to add file.*" + \ + "a non-file object of the " + \ + "same name already exists", + None, None, None, None, 0, F_Path, + '--force') + + # A forced update that tries to add a directory when an unversioned file + # of the same name already exists should fail. + C_Path = os.path.join(wc_backup, 'A', 'C') + svntest.actions.run_and_verify_update(C_Path, None, None, None, + ".*Failed to add directory.*" + \ + "a non-directory object of the " + \ + "same name already exists", + None, None, None, None, 0, C_Path, + '--force') + + # A forced update that tries to add a directory when a versioned directory + # of the same name already exists should fail. + + # Remove the file A/C/I and make it a versioned directory. + I_url = svntest.main.current_repo_url + "/A/C/I" + os.remove(I_path) + os.mkdir(I_path) + so, se = svntest.actions.run_and_verify_svn("Unexpected error during co", + ['Checked out revision 2.\n'], + [], "co", I_url, I_path) + + svntest.actions.run_and_verify_update(C_Path, None, None, None, + ".*Failed to forcibly add " + \ + "directory.*a versioned directory " + \ + "of the same name already exists", + None, None, None, None, 0, C_Path, + '--force') + ######################################################################## # Run the tests @@ -1708,6 +1935,8 @@ obstructed_update_alters_wc_props, update_xml_unsafe_dir, checkout_broken_eol, + forced_update, + forced_update_failures, ] if __name__ == '__main__':