Index: subversion/libsvn_wc/copy.c =================================================================== --- subversion/libsvn_wc/copy.c (revision 21139) +++ subversion/libsvn_wc/copy.c (working copy) @@ -39,6 +39,228 @@ /*** Code. ***/ +/* Helper function for svn_wc_copy2() which handles WC->WC copying of + files which are scheduled for addition or unversioned. + + Copy file SRC_PATH to DST_BASENAME in DST_PARENT_ACCESS. + + DST_PARENT_ACCESS is a 0 depth locked access for a versioned directory + in the same WC as SRC_PATH. + + If SRC_IS_ADDED is true then SRC_PATH is scheduled for addition and + DST_BASENAME will also be scheduled for addition. + + If SRC_IS_ADDED is false then SRC_PATH is the unversioned child + file of a versioned or added parent and DST_BASENAME is simply copied. + + Use POOL for all necessary allocations. +*/ +static svn_error_t * +copy_added_file_administratively(const char *src_path, + svn_boolean_t src_is_added, + svn_wc_adm_access_t *dst_parent_access, + const char *dst_basename, + svn_cancel_func_t cancel_func, + void *cancel_baton, + svn_wc_notify_func2_t notify_func, + void *notify_baton, + apr_pool_t *pool) +{ + const char *dst_path + = svn_path_join(svn_wc_adm_access_path(dst_parent_access), + dst_basename, pool); + + /* Copy this file and possibly put it under version control. */ + SVN_ERR(svn_io_copy_file(src_path, dst_path, TRUE, pool)); + + if (src_is_added) + { + SVN_ERR(svn_wc_add2(dst_path, dst_parent_access, NULL, + SVN_INVALID_REVNUM, cancel_func, + cancel_baton, notify_func, + notify_baton, pool)); + } + + return SVN_NO_ERROR; +} + + +/* Helper function for svn_wc_copy2() which handles WC->WC copying of + directories which are scheduled for addition or unversioned. + + Recursively copy directory SRC_PATH and its children, excluding + administrative directories, to DST_BASENAME in DST_PARENT_ACCESS. + + DST_PARENT_ACCESS is a 0 depth locked access for a versioned directory + in the same WC as SRC_PATH. + + SRC_ACCESS is a -1 depth access for SRC_PATH + + If SRC_IS_ADDED is true then SRC_PATH is scheduled for addition and + DST_BASENAME will also be scheduled for addition. + + If SRC_IS_ADDED is false then SRC_PATH is the unversioned child + directory of a versioned or added parent and DST_BASENAME is simply + copied. + + Use POOL for all necessary allocations. +*/ +static svn_error_t * +copy_added_dir_administratively(const char *src_path, + svn_boolean_t src_is_added, + svn_wc_adm_access_t *dst_parent_access, + svn_wc_adm_access_t *src_access, + const char *dst_basename, + svn_cancel_func_t cancel_func, + void *cancel_baton, + svn_wc_notify_func2_t notify_func, + void *notify_baton, + apr_pool_t *pool) +{ + /* The 'dst_path' is simply dst_parent/dst_basename */ + const char *dst_path + = svn_path_join(svn_wc_adm_access_path(dst_parent_access), + dst_basename, pool); + + if (! src_is_added) + { + /* src_path is the top of an unversioned tree, just copy + the whole thing and we are done. */ + SVN_ERR(svn_io_copy_dir_recursively(src_path, + svn_wc_adm_access_path(dst_parent_access), + dst_basename, + TRUE, cancel_func, cancel_baton, + pool)); + } + else + { + char *src_parent, *dst_parent; + const svn_wc_entry_t *entry; + svn_wc_adm_access_t *dst_child_dir_access; + svn_wc_adm_access_t *src_child_dir_access; + apr_dir_t *dir; + apr_finfo_t this_entry; + svn_error_t *err; + apr_pool_t *subpool; + apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME; + + /* Check cancellation; note that this catches recursive calls too. */ + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + src_parent = svn_path_dirname(src_path, pool); + dst_parent = svn_path_dirname(dst_path, pool); + + /* "Copy" the dir dst_path and schedule it, and possibly + it's children, for addition. */ + SVN_ERR(svn_io_dir_make(dst_path, APR_OS_DEFAULT, pool)); + + /* Add the directory, adding locking access for dst_path + to dst_parent_access at the same time. */ + SVN_ERR(svn_wc_add2(dst_path, dst_parent_access, NULL, + SVN_INVALID_REVNUM, cancel_func, cancel_baton, + notify_func, notify_baton, pool)); + + /* Get the accesses for the newly added dir and its source, we'll + need both to process any of SRC_PATHS's children below. */ + SVN_ERR(svn_wc_adm_retrieve(&dst_child_dir_access, dst_parent_access, + dst_path, pool)); + SVN_ERR(svn_wc_adm_retrieve(&src_child_dir_access, src_access, + src_path, pool)); + + /* Create a subpool for iterative memory control. */ + subpool = svn_pool_create(pool); + + /* Read src_path's entries one by one. */ + SVN_ERR(svn_io_dir_open(&dir, src_path, pool)); + for (err = svn_io_dir_read(&this_entry, flags, dir, subpool); + err == SVN_NO_ERROR; + err = svn_io_dir_read(&this_entry, flags, dir, subpool)) + { + const char *src_fullpath, *dst_fullpath; + + /* Skip entries for this dir and its parent. */ + if (this_entry.name[0] == '.' + && (this_entry.name[1] == '\0' + || (this_entry.name[1] == '.' + && this_entry.name[2] == '\0'))) + continue; + + /* Check cancellation so you can cancel during an + * add of a directory with lots of files. */ + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Skip over SVN admin directories. */ + if (svn_wc_is_adm_dir(this_entry.name, subpool)) + continue; + + /* Construct the full path of the entry. */ + src_fullpath = svn_path_join(src_path, this_entry.name, subpool); + dst_fullpath = svn_path_join(dst_path, this_entry.name, subpool); + + SVN_ERR(svn_wc_entry(&entry, src_fullpath, src_child_dir_access, + TRUE, subpool)); + + /* Recurse on directories; add files; ignore the rest. */ + if (this_entry.filetype == APR_DIR) + { + SVN_ERR(copy_added_dir_administratively(src_fullpath, + entry ? TRUE : FALSE, + dst_child_dir_access, + src_child_dir_access, + this_entry.name, + cancel_func, + cancel_baton, + notify_func, + notify_baton, + subpool)); + } + else if (this_entry.filetype != APR_UNKFILE) + { + SVN_ERR(copy_added_file_administratively(src_fullpath, + entry ? TRUE : FALSE, + dst_child_dir_access, + this_entry.name, + cancel_func, + cancel_baton, + notify_func, + notify_baton, + subpool)); + } + + /* Clean out the per-iteration pool. */ + svn_pool_clear(subpool); + + } /* End for loop */ + + /* Check that the loop exited cleanly. */ + if (! (APR_STATUS_IS_ENOENT(err->apr_err))) + { + return svn_error_createf(err->apr_err, err, + _("Error during recursive copy of '%s'"), + svn_path_local_style(src_path, + subpool)); + } + else /* Yes, it exited cleanly, so close the dir. */ + { + apr_status_t apr_err; + + svn_error_clear(err); + apr_err = apr_dir_close(dir); + if (apr_err) + return svn_error_wrap_apr(apr_err, + _("Can't close directory '%s'"), + svn_path_local_style(src_path, + subpool)); + } + + } /* End else src_is_added. */ + + return SVN_NO_ERROR; +} + + /* Helper function for copy_file_administratively() and copy_dir_administratively(). Determines the COPYFROM_URL and COPYFROM_REV of a file or directory SRC_PATH which is the descendant @@ -588,15 +810,46 @@ SVN_ERR(svn_io_check_path(src_path, &src_kind, pool)); if (src_kind == svn_node_file) - SVN_ERR(copy_file_administratively(src_path, adm_access, - dst_parent, dst_basename, - notify_func, notify_baton, pool)); - + { + /* Check if we are copying a file scheduled for addition, + these require special handling. */ + if (src_entry->schedule == svn_wc_schedule_add + && (! src_entry->copied)) + { + SVN_ERR(copy_added_file_administratively(src_path, TRUE, + dst_parent, dst_basename, + cancel_func, cancel_baton, + notify_func, notify_baton, + pool)); + } + else + { + SVN_ERR(copy_file_administratively(src_path, adm_access, + dst_parent, dst_basename, + notify_func, notify_baton, pool)); + } + } else if (src_kind == svn_node_dir) - SVN_ERR(copy_dir_administratively(src_path, adm_access, - dst_parent, dst_basename, - cancel_func, cancel_baton, - notify_func, notify_baton, pool)); + { + /* Check if we are copying a directory scheduled for addition, + these require special handling. */ + if (src_entry->schedule == svn_wc_schedule_add + && (! src_entry->copied)) + { + SVN_ERR(copy_added_dir_administratively(src_path, TRUE, + dst_parent, adm_access, + dst_basename, cancel_func, + cancel_baton, notify_func, + notify_baton, pool)); + } + else + { + SVN_ERR(copy_dir_administratively(src_path, adm_access, + dst_parent, dst_basename, + cancel_func, cancel_baton, + notify_func, notify_baton, pool)); + } + } SVN_ERR(svn_wc_adm_close(adm_access)); Index: subversion/tests/cmdline/copy_tests.py =================================================================== --- subversion/tests/cmdline/copy_tests.py (revision 21139) +++ subversion/tests/cmdline/copy_tests.py (working copy) @@ -2431,6 +2431,280 @@ 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) + + # Scatter some unversioned files and an unversioned dir within + # in the added dir L. + unversioned_path_1 = os.path.join(I_path, 'unversioned1') + unversioned_path_2 = os.path.join(J_path, 'unversioned2') + L_path = os.path.join(I_path, "L_UNVERSIONED") + unversioned_path_3 = os.path.join(L_path, 'unversioned3') + svntest.main.file_write(unversioned_path_1, "An unversioned file\n") + svntest.main.file_write(unversioned_path_2, "An unversioned file\n") + os.mkdir(L_path) + svntest.main.file_write(unversioned_path_3, "An unversioned file\n") + + # 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) + + # Confirm unversioned paths got copied and moved too. + unversioned_paths = [ + os.path.join(wc_dir, 'A', 'D', 'H', 'I', 'unversioned1'), + os.path.join(wc_dir, 'A', 'D', 'H', 'I', 'L_UNVERSIONED'), + os.path.join(wc_dir, 'A', 'D', 'H', 'I', 'L_UNVERSIONED', + 'unversioned3'), + os.path.join(wc_dir, 'A', 'D', 'H', 'I', 'J', 'unversioned2'), + os.path.join(wc_dir, 'K', 'I', 'unversioned1'), + os.path.join(wc_dir, 'K', 'I', 'L_UNVERSIONED'), + os.path.join(wc_dir, 'K', 'I', 'L_UNVERSIONED', 'unversioned3'), + os.path.join(wc_dir, 'K', 'I', 'J', 'unversioned2')] + for path in unversioned_paths: + if not os.path.exists(path): + raise svntest.Failure("Unversioned path '%s' not found." % path) + + +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) + + # Add various unversioned files and dirs. These don't get copied + # in a WC->URL copy obviously. + unversioned_path_1 = os.path.join(I_path, 'unversioned1') + unversioned_path_2 = os.path.join(J_path, 'unversioned2') + svntest.main.file_write(unversioned_path_1, "An unversioned file\n") + svntest.main.file_write(unversioned_path_2, "An unversioned file\n") + os.mkdir(os.path.join(I_path, 'L')) + unversioned_path_3 = os.path.join(wc_dir, 'unversioned3') + svntest.main.file_write(unversioned_path_3, "An unversioned file\n") + + # 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"), + 'unversioned3' : Item("An unversioned file\n"), #unversioned + 'A/D/I/L' : Item(), #unversioned + 'A/D/I/unversioned1' : Item("An unversioned file\n"), #unversioned + 'A/D/I/J/unversioned2' : Item("An unversioned file\n"), #unversioned + }) + + # 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 +2757,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__':