Index: subversion/libsvn_client/copy.c =================================================================== --- subversion/libsvn_client/copy.c (revision 21012) +++ subversion/libsvn_client/copy.c (working copy) @@ -30,6 +30,7 @@ #include "svn_path.h" #include "svn_opt.h" #include "svn_time.h" +#include "svn_pools.h" #include "client.h" @@ -38,6 +39,104 @@ /*** Code. ***/ +/* SRC_PATH is a WC file or dir scheduled for addition. If SRC_PATH is a + file simply copy it to DST_PATH. If SRC_PATH is a directory make + DST_PATH and recursively call this function to copy SRC_PATH's children. + Use POOL for all necessary allocations. +*/ +static svn_error_t * +copy_added_path(const char *src_path, + const char *dst_path, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + const svn_wc_entry_t *entry; + const char *dst_parent, *base_name; + apr_finfo_t finfo; + apr_pool_t *iterpool; + apr_hash_t *entries; + apr_hash_index_t *hi; + svn_wc_adm_access_t *src_access; + + SVN_ERR(svn_wc_adm_probe_open3(&src_access, NULL, src_path, FALSE, + 0, ctx->cancel_func, ctx->cancel_baton, + pool)); + + svn_path_split(dst_path, &dst_parent, &base_name, pool); + + SVN_ERR(svn_wc_entry(&entry, src_path, src_access, FALSE, pool)); + + /* Bail if we're trying to copy something that isn't + scheduled for addition or doesn't exist. */ + if (!entry || entry->schedule != svn_wc_schedule_add) + { + SVN_ERR(svn_wc_adm_close(src_access)); + return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL, + _("'%s' is not scheduled for addition " + "or doesn't exist"), + svn_path_local_style(src_path, pool)); + } + + if (entry->kind == svn_node_file) + { + SVN_ERR(svn_io_copy_file(src_path, dst_path, TRUE, pool)); + } + else /* Copy added dir */ + { + /* Try to make the new directory. */ + SVN_ERR(svn_io_stat(&finfo, src_path, APR_FINFO_PROT, pool)); + SVN_ERR(svn_io_dir_make(dst_path, finfo.protection, pool)); + SVN_ERR(svn_wc_entries_read(&entries, src_access, FALSE, pool)); + + iterpool = svn_pool_create(pool); + for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) + { + const char *item; + const void *key; + void *val; + + svn_pool_clear(iterpool); + apr_hash_this(hi, &key, NULL, &val); + item = key; + entry = val; + + if (ctx->cancel_func) + SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); + + if (entry->kind == svn_node_dir) + { + if (strcmp(item, SVN_WC_ENTRY_THIS_DIR) == 0) + { + ; /* skip this, it's the current directory that we're + handling now. */ + } + else + { + const char *new_from = svn_path_join(src_path, item, + iterpool); + const char *new_to = svn_path_join(dst_path, item, + iterpool); + + SVN_ERR(copy_added_path(new_from, new_to, ctx, iterpool)); + } + } + else if (entry->kind == svn_node_file) + { + const char *new_from = svn_path_join(src_path, item, + iterpool); + const char *new_to = svn_path_join(dst_path, item, iterpool); + + SVN_ERR(svn_io_copy_file(new_from, new_to, TRUE, iterpool)); + } + } + svn_pool_destroy(iterpool); + } + + SVN_ERR(svn_wc_adm_close(src_access)); + return SVN_NO_ERROR; +} + + /* * if (not exist src_path) * return ERR_BAD_SRC error @@ -53,14 +152,16 @@ /* Copy SRC_PATH into DST_PATH as DST_BASENAME, deleting SRC_PATH - afterwards if IS_MOVE is TRUE. Use POOL for all necessary - allocations. + afterwards if IS_MOVE is TRUE. If IS_COPY_ADD is TRUE then SRC_PATH + is not under version control, but is scheduled for addition. + Use POOL for all necessary allocations. */ static svn_error_t * wc_to_wc_copy(const char *src_path, const char *dst_path, svn_boolean_t is_move, svn_boolean_t force, + svn_boolean_t is_copy_add, svn_client_ctx_t *ctx, apr_pool_t *pool) { @@ -148,10 +249,17 @@ /* ### If this is not a move, we won't have locked the source, so we ### won't detect any outstanding locks. If the source is locked and ### requires cleanup should we abort the copy? */ + if (is_copy_add) + { + err = copy_added_path(src_path, dst_path, ctx, pool); + } + else + { + err = svn_wc_copy2(src_path, adm_access, base_name, + ctx->cancel_func, ctx->cancel_baton, + ctx->notify_func2, ctx->notify_baton2, pool); + } - err = svn_wc_copy2(src_path, adm_access, base_name, - ctx->cancel_func, ctx->cancel_baton, - ctx->notify_func2, ctx->notify_baton2, pool); svn_sleep_for_timestamps(); SVN_ERR(err); @@ -171,6 +279,17 @@ SVN_ERR(svn_wc_adm_close(adm_access)); } + if (is_copy_add) + { + /* Recursively add the copied path. Don't force the addition since + copy/move doesn't allow any obstructions in the first place, + so if anything versioned found it's way into the dst_path tree + something is quite amiss. Lastly, no_ignore is true because if an + path was already schedule for addition in src_path and got copied + to dst_path we don't want to "un-add" it now. */ + SVN_ERR(svn_client_add3(dst_path, TRUE, FALSE, TRUE, ctx, pool)); + } + return SVN_NO_ERROR; } @@ -944,7 +1063,9 @@ svn_client_ctx_t *ctx, apr_pool_t *pool) { - svn_boolean_t src_is_url, dst_is_url; + svn_boolean_t src_is_url, dst_is_url, src_is_add; + svn_wc_adm_access_t *adm_access; + const svn_wc_entry_t *entry; /* Are either of our paths URLs? */ src_is_url = svn_path_is_url(src_path); @@ -958,6 +1079,17 @@ svn_path_local_style(src_path, pool), svn_path_local_style(dst_path, pool)); + if (!src_is_url) + { + /* Are we copying/moving a path that is scheduled for addition? */ + SVN_ERR(svn_wc_adm_probe_open3(&adm_access, NULL, src_path, FALSE, 0, + ctx->cancel_func, ctx->cancel_baton, + pool)); + SVN_ERR(svn_wc_entry(&entry, src_path, adm_access, FALSE, pool)); + SVN_ERR(svn_wc_adm_close(adm_access)); + src_is_add = (entry && entry->revision == 0) ? TRUE : FALSE; + } + if (is_move) { if (src_is_url == dst_is_url) @@ -985,8 +1117,6 @@ { /* We can convert the working copy path to a URL based on the entries file. */ - svn_wc_adm_access_t *adm_access; /* ### FIXME local */ - const svn_wc_entry_t *entry; SVN_ERR(svn_wc_adm_probe_open3(&adm_access, NULL, src_path, FALSE, 0, ctx->cancel_func, @@ -1018,7 +1148,7 @@ if ((! src_is_url) && (! dst_is_url)) { SVN_ERR(wc_to_wc_copy(src_path, dst_path, - is_move, force, + is_move, force, src_is_add, ctx, pool)); } Index: subversion/tests/cmdline/copy_tests.py =================================================================== --- subversion/tests/cmdline/copy_tests.py (revision 21012) +++ subversion/tests/cmdline/copy_tests.py (working copy) @@ -2431,6 +2431,240 @@ return raise svntest.Failure("mv failed but not in the expected way") + +def copy_move_added_paths(sbox): + "copy and move added paths without commits" + + sbox.build() + wc_dir = sbox.wc_dir + + # Create a new file and schedule it for addition + upsilon_path = os.path.join(wc_dir, 'A', 'D', 'upsilon') + svntest.main.file_write(upsilon_path, "This is the file 'upsilon'\n") + svntest.actions.run_and_verify_svn(None, + ["A " + upsilon_path + "\n"], + [], 'add', upsilon_path) + + # Create a dir with children and schedule it for addition + I_path = os.path.join(wc_dir, 'A', 'D', 'I') + J_path = os.path.join(I_path, 'J') + eta_path = os.path.join(I_path, 'eta') + theta_path = os.path.join(I_path, 'theta') + kappa_path = os.path.join(J_path, 'kappa') + os.mkdir(I_path) + os.mkdir(J_path) + svntest.main.file_write(eta_path, "This is the file 'eta'\n") + svntest.main.file_write(theta_path, "This is the file 'theta'\n") + svntest.main.file_write(kappa_path, "This is the file 'kappa'\n") + svntest.actions.run_and_verify_svn(None, + ["A " + I_path + "\n", + "A " + eta_path + "\n", + "A " + J_path + "\n", + "A " + kappa_path + "\n", + "A " + theta_path + "\n"], + [], 'add', I_path) + + # Create another dir and schedule it for addition + K_path = os.path.join(wc_dir, 'K') + os.mkdir(K_path) + svntest.actions.run_and_verify_svn(None, + ["A " + K_path + "\n"], + [], 'add', K_path) + + # Copy added dir A/D/I to added dir K/I + I_copy_path = os.path.join(K_path, 'I') + svntest.actions.run_and_verify_svn(None, None, [], 'cp', + I_path, I_copy_path) + + # Copy added file A/D/upsilon into added dir K + upsilon_copy_path = os.path.join(K_path, 'upsilon') + svntest.actions.run_and_verify_svn(None, None, [], 'cp', + upsilon_path, upsilon_copy_path) + + # Move added file A/D/upsilon to upsilon, + # then move it again to A/upsilon + upsilon_move_path = os.path.join(wc_dir, 'upsilon') + upsilon_move_path_2 = os.path.join(wc_dir, 'A', 'upsilon') + svntest.actions.run_and_verify_svn(None, None, [], 'mv', + upsilon_path, upsilon_move_path, + '--force') + svntest.actions.run_and_verify_svn(None, None, [], 'mv', + upsilon_move_path, upsilon_move_path_2, + '--force') + + # Move added dir A/D/I to A/B/I, + # then move it again to A/D/H/I + I_move_path = os.path.join(wc_dir, 'A', 'B', 'I') + I_move_path_2 = os.path.join(wc_dir, 'A', 'D', 'H', 'I') + svntest.actions.run_and_verify_svn(None, None, [], 'mv', + I_path, I_move_path, + '--force') + svntest.actions.run_and_verify_svn(None, None, [], 'mv', + I_move_path, I_move_path_2, + '--force') + + # Created expected output tree for 'svn ci' + expected_output = svntest.wc.State(wc_dir, { + 'A/D/H/I' : Item(verb='Adding'), + 'A/D/H/I/J' : Item(verb='Adding'), + 'A/D/H/I/J/kappa' : Item(verb='Adding'), + 'A/D/H/I/eta' : Item(verb='Adding'), + 'A/D/H/I/theta' : Item(verb='Adding'), + 'A/upsilon' : Item(verb='Adding'), + 'K' : Item(verb='Adding'), + 'K/I' : Item(verb='Adding'), + 'K/I/J' : Item(verb='Adding'), + 'K/I/J/kappa' : Item(verb='Adding'), + 'K/I/eta' : Item(verb='Adding'), + 'K/I/theta' : Item(verb='Adding'), + 'K/upsilon' : 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/D/H/I' : Item(status=' ', wc_rev=2), + 'A/D/H/I/J' : Item(status=' ', wc_rev=2), + 'A/D/H/I/J/kappa' : Item(status=' ', wc_rev=2), + 'A/D/H/I/eta' : Item(status=' ', wc_rev=2), + 'A/D/H/I/theta' : Item(status=' ', wc_rev=2), + 'A/upsilon' : Item(status=' ', wc_rev=2), + 'K' : Item(status=' ', wc_rev=2), + 'K/I' : Item(status=' ', wc_rev=2), + 'K/I/J' : Item(status=' ', wc_rev=2), + 'K/I/J/kappa' : Item(status=' ', wc_rev=2), + 'K/I/eta' : Item(status=' ', wc_rev=2), + 'K/I/theta' : Item(status=' ', wc_rev=2), + 'K/upsilon' : Item(status=' ', wc_rev=2), + }) + + svntest.actions.run_and_verify_commit(wc_dir, + expected_output, + expected_status, + None, + None, None, + None, None, + wc_dir) + + +def copy_added_paths_to_URL(sbox): + "copy added path to URL" + + sbox.build() + wc_dir = sbox.wc_dir + + # Create a new file and schedule it for addition + upsilon_path = os.path.join(wc_dir, 'A', 'D', 'upsilon') + svntest.main.file_write(upsilon_path, "This is the file 'upsilon'\n") + svntest.actions.run_and_verify_svn(None, + ["A " + upsilon_path + "\n"], + [], 'add', upsilon_path) + + # Create a dir with children and schedule it for addition + I_path = os.path.join(wc_dir, 'A', 'D', 'I') + J_path = os.path.join(I_path, 'J') + eta_path = os.path.join(I_path, 'eta') + theta_path = os.path.join(I_path, 'theta') + kappa_path = os.path.join(J_path, 'kappa') + os.mkdir(I_path) + os.mkdir(J_path) + svntest.main.file_write(eta_path, "This is the file 'eta'\n") + svntest.main.file_write(theta_path, "This is the file 'theta'\n") + svntest.main.file_write(kappa_path, "This is the file 'kappa'\n") + svntest.actions.run_and_verify_svn(None, + ["A " + I_path + "\n", + "A " + eta_path + "\n", + "A " + J_path + "\n", + "A " + kappa_path + "\n", + "A " + theta_path + "\n"], + [], 'add', I_path) + + # Copy added file A/D/upsilon to URL://A/C/upsilon + upsilon_copy_URL = sbox.repo_url + '/A/C/upsilon' + svntest.actions.run_and_verify_svn(None, None, [], 'cp', '-m', '', + upsilon_path, upsilon_copy_URL) + + # Copy added dir A/D/I to URL://A/D/G/I + I_copy_URL = sbox.repo_url + '/A/D/G/I' + svntest.actions.run_and_verify_svn(None, None, [], 'cp', '-m', '', + I_path, I_copy_URL) + + # Created expected output tree for 'svn ci' + expected_output = svntest.wc.State(wc_dir, { + 'A/D/I' : Item(verb='Adding'), + 'A/D/I/J' : Item(verb='Adding'), + 'A/D/I/J/kappa' : Item(verb='Adding'), + 'A/D/I/eta' : Item(verb='Adding'), + 'A/D/I/theta' : Item(verb='Adding'), + 'A/D/upsilon' : Item(verb='Adding'), + }) + + # Create expected status tree + expected_status = svntest.actions.get_virginal_state(wc_dir, 1) + expected_status.add({ + 'A/D/I' : Item(status=' ', wc_rev=4), + 'A/D/I/J' : Item(status=' ', wc_rev=4), + 'A/D/I/J/kappa' : Item(status=' ', wc_rev=4), + 'A/D/I/eta' : Item(status=' ', wc_rev=4), + 'A/D/I/theta' : Item(status=' ', wc_rev=4), + 'A/D/upsilon' : Item(status=' ', wc_rev=4), + }) + + svntest.actions.run_and_verify_commit(wc_dir, + expected_output, + expected_status, + None, + None, None, + None, None, + wc_dir) + + # Created expected output for update + expected_output = svntest.wc.State(wc_dir, { + 'A/D/G/I' : Item(status='A '), + 'A/D/G/I/theta' : Item(status='A '), + 'A/D/G/I/J' : Item(status='A '), + 'A/D/G/I/J/kappa' : Item(status='A '), + 'A/D/G/I/eta' : Item(status='A '), + 'A/C/upsilon' : Item(status='A '), + }) + + # Created expected disk for update + expected_disk = svntest.main.greek_state.copy() + expected_disk.add({ + 'A/D/G/I' : Item(), + 'A/D/G/I/theta' : Item("This is the file 'theta'\n"), + 'A/D/G/I/J' : Item(), + 'A/D/G/I/J/kappa' : Item("This is the file 'kappa'\n"), + 'A/D/G/I/eta' : Item("This is the file 'eta'\n"), + 'A/C/upsilon' : Item("This is the file 'upsilon'\n"), + 'A/D/I' : Item(), + 'A/D/I/J' : Item(), + 'A/D/I/J/kappa' : Item("This is the file 'kappa'\n"), + 'A/D/I/eta' : Item("This is the file 'eta'\n"), + 'A/D/I/theta' : Item("This is the file 'theta'\n"), + 'A/D/upsilon' : Item("This is the file 'upsilon'\n"), + }) + + # Some more changes to the expected_status to reflect post update WC + expected_status.tweak(wc_rev=4) + expected_status.add({ + 'A/C' : Item(status=' ', wc_rev=4), + 'A/C/upsilon' : Item(status=' ', wc_rev=4), + 'A/D/G' : Item(status=' ', wc_rev=4), + 'A/D/G/I' : Item(status=' ', wc_rev=4), + 'A/D/G/I/theta' : Item(status=' ', wc_rev=4), + 'A/D/G/I/J' : Item(status=' ', wc_rev=4), + 'A/D/G/I/J/kappa' : Item(status=' ', wc_rev=4), + 'A/D/G/I/eta' : Item(status=' ', wc_rev=4), + }) + + # Update WC, the WC->URL copies above should be added + svntest.actions.run_and_verify_update(wc_dir, + expected_output, + expected_disk, + expected_status) + ######################################################################## # Run the tests @@ -2483,6 +2717,8 @@ move_dir_out_of_moved_dir, move_file_back_and_forth, move_dir_back_and_forth, + copy_move_added_paths, + copy_added_paths_to_URL, ] if __name__ == '__main__':