Index: subversion/tests/cmdline/patch_tests.py =================================================================== --- subversion/tests/cmdline/patch_tests.py (revision 916918) +++ subversion/tests/cmdline/patch_tests.py (arbetskopia) @@ -911,7 +911,108 @@ None, # expected err 1, # check-props 1) # dry-run +def patch_remove_empty_dir(sbox): + "delete the dir if all children is deleted" + sbox.build() + wc_dir = sbox.wc_dir + + patch_file_path = tempfile.mkstemp(dir=os.path.abspath(svntest.main.temp_dir))[1] + + # Contents of B: + # A/B/lamba + # A/B/F + # A/B/E/{alpha,beta} + # Before patching we've deleted F, which means that B is empty after patching and + # should be removed. + # + # Contents of H: + # A/D/H/{chi,psi,omega} + # Before patching, chi has been removed by a non-svn operation which means it has + # status missing. The patch deletes the other two files but should not delete H. + + unidiff_patch = [ + "Index: psi\n", + "===================================================================\n", + "--- A/D/H/psi\t(revision 0)\n", + "+++ A/D/H/psi\t(revision 0)\n", + "@@ -1 +0,0 @@\n", + "-This is the file 'psi'.\n", + "Index: omega\n", + "===================================================================\n", + "--- A/D/H/omega\t(revision 0)\n", + "+++ A/D/H/omega\t(revision 0)\n", + "@@ -1 +0,0 @@\n", + "-This is the file 'omega'.\n", + "Index: lambda\n", + "===================================================================\n", + "--- A/B/lambda\t(revision 0)\n", + "+++ A/B/lambda\t(revision 0)\n", + "@@ -1 +0,0 @@\n", + "-This is the file 'lambda'.\n", + "Index: alpha\n", + "===================================================================\n", + "--- A/B/E/alpha\t(revision 0)\n", + "+++ A/B/E/alpha\t(revision 0)\n", + "@@ -1 +0,0 @@\n", + "-This is the file 'alpha'.\n", + "Index: beta\n", + "===================================================================\n", + "--- A/B/E/beta\t(revision 0)\n", + "+++ A/B/E/beta\t(revision 0)\n", + "@@ -1 +0,0 @@\n", + "-This is the file 'beta'.\n", + ] + + svntest.main.file_write(patch_file_path, ''.join(unidiff_patch)) + + F_path = os.path.join(wc_dir, 'A', 'B', 'F') + svntest.actions.run_and_verify_svn("Deleting F failed", None, [], + 'rm', F_path) + svntest.actions.run_and_verify_svn("Update failed", None, [], + 'up', wc_dir) + + # We should be able to handle one path beeing missing. + os.remove(os.path.join(wc_dir, 'A', 'D', 'H', 'chi')) + + expected_output = [ + 'D %s\n' % os.path.join(wc_dir, 'A', 'B'), + 'D %s\n' % os.path.join(wc_dir, 'A', 'D', 'H', 'psi'), + 'D %s\n' % os.path.join(wc_dir, 'A', 'D', 'H', 'omega'), + ] + + expected_disk = svntest.main.greek_state.copy() + expected_disk.remove('A/D/H/chi') + expected_disk.remove('A/D/H/psi') + expected_disk.remove('A/D/H/omega') + expected_disk.remove('A/B/lambda') + expected_disk.remove('A/B/E/alpha') + expected_disk.remove('A/B/E/beta') + + expected_status = svntest.actions.get_virginal_state(wc_dir, 1) + expected_status.add({'A/D/H/chi' : Item(status='! ', wc_rev=1)}) + expected_status.add({'A/D/H/omega' : Item(status='D ', wc_rev=1)}) + expected_status.add({'A/D/H/psi' : Item(status='D ', wc_rev=1)}) + expected_status.add({'A/B' : Item(status='D ', wc_rev=1)}) + expected_status.add({'A/B/E' : Item(status='D ', wc_rev=1)}) + expected_status.add({'A/B/E/beta' : Item(status='D ', wc_rev=1)}) + expected_status.add({'A/B/E/alpha' : Item(status='D ', wc_rev=1)}) + expected_status.add({'A/B/lambda' : Item(status='D ', wc_rev=1)}) + expected_status.add({'A/B/F' : Item(status='D ', wc_rev=1)}) + + expected_skip = wc.State('', { }) + + svntest.actions.run_and_verify_patch(wc_dir, + os.path.abspath(patch_file_path), + expected_output, + expected_disk, + expected_status, + expected_skip, + None, # expected err + 1, # check-props + 1) # dry-run + + def patch_reject(sbox): "apply a patch which is rejected" @@ -1363,6 +1464,7 @@ patch_chopped_leading_spaces, patch_strip1, patch_add_new_dir, + patch_remove_empty_dir, patch_reject, patch_keywords, patch_with_fuzz, Index: subversion/libsvn_client/patch.c =================================================================== --- subversion/libsvn_client/patch.c (revision 916918) +++ subversion/libsvn_client/patch.c (arbetskopia) @@ -34,6 +34,7 @@ #include "svn_path.h" #include "svn_pools.h" #include "svn_props.h" +#include "svn_sorts.h" #include "svn_subst.h" #include "svn_wc.h" #include "client.h" @@ -1159,6 +1160,244 @@ return SVN_NO_ERROR; } +/* Baton for find_existing_children() */ +struct status_baton +{ + apr_array_header_t *existing_targets; + const char *parent_path; + apr_pool_t *result_pool; +}; + +/* Implements svn_wc_status_func4_t. */ +static svn_error_t * +find_existing_children(void *baton, + const char *abspath, + const svn_wc_status2_t *status, + apr_pool_t *pool) +{ + struct status_baton *btn = baton; + + if (status->text_status != svn_wc_status_none + && status->text_status != svn_wc_status_deleted + && strcmp(abspath, btn->parent_path)) + { + APR_ARRAY_PUSH(btn->existing_targets, + const char *) = apr_pstrdup(btn->result_pool, + abspath); + } + + return SVN_NO_ERROR; +} + +/* Check if PARENT_DIR_ABSPATH has any versioned or unversioned children if + * we ignore the ones in TARGETS_TO_BE_DELETED. Return the answer in + * EMPTY. */ +static svn_error_t * +is_dir_empty(svn_boolean_t *empty, const char *parent_dir_abspath, + svn_client_ctx_t *ctx, + apr_array_header_t *targets_to_be_deleted, + apr_pool_t *result_pool, apr_pool_t *scratch_pool) +{ + struct status_baton btn; + svn_opt_revision_t revision; + int i; + + btn.existing_targets = apr_array_make(scratch_pool, 0, + sizeof(patch_target_t *)); + btn.parent_path = parent_dir_abspath; + btn.result_pool = scratch_pool; + revision.kind = svn_opt_revision_unspecified; + + SVN_ERR(svn_client_status5(NULL, parent_dir_abspath, &revision, + find_existing_children, &btn, + svn_depth_immediates, TRUE, FALSE, TRUE, + FALSE, NULL, ctx, scratch_pool)); + + /* Do we delete all targets? */ + for (i = 0; i < btn.existing_targets->nelts; i++) + { + int j; + const char *found = APR_ARRAY_IDX(btn.existing_targets, i, const char *); + svn_boolean_t deleted = FALSE; + + for (j = 0; j < targets_to_be_deleted->nelts; j++) + { + patch_target_t *to_be_del; + to_be_del = APR_ARRAY_IDX(targets_to_be_deleted, j, patch_target_t *); + if (! strcmp(found, to_be_del->abs_path)) + deleted = TRUE; + } + if (! deleted) + { + *empty = FALSE; + break; + } + } + + return SVN_NO_ERROR; +} + +/* Add TARGET to the array of targets keyed by their parent dir in + * TARGETS_TO_BE_DELETED. If there is no array for the parent dir a new one + * is created. All allocations are done in RESULT_POOL. */ +static svn_error_t * +add_target_to_hash_keyed_by_parent_dir(apr_hash_t *targets_to_be_deleted, + patch_target_t *target, + apr_pool_t *result_pool) +{ + apr_array_header_t * deleted_targets_in_dir; + + /* We're using the abs_path of the target. The abs_path is not + * present if the path is a symlink pointing outside the wc but we + * know, that's not the case. */ + const char *dirname = svn_dirent_dirname(target->abs_path, + result_pool); + + deleted_targets_in_dir = apr_hash_get(targets_to_be_deleted, + dirname, APR_HASH_KEY_STRING); + + if (deleted_targets_in_dir) + { + APR_ARRAY_PUSH(deleted_targets_in_dir, patch_target_t *) = target; + + apr_hash_set(targets_to_be_deleted, dirname, + APR_HASH_KEY_STRING, deleted_targets_in_dir); + } + else + { + apr_array_header_t *new_array; + + new_array = apr_array_make(result_pool, 0, sizeof(patch_target_t *)); + APR_ARRAY_PUSH(new_array, patch_target_t *) = target; + apr_hash_set(targets_to_be_deleted, + dirname, APR_HASH_KEY_STRING, new_array); + } + return SVN_NO_ERROR; +} + +/* Compare A and B and return an integer greater than, equal to, or less + * than 0, according to whether A has less subdirs, just as many or more + * subdir than B. */ +static int +sort_compare_nr_of_path_elements(const svn_sort__item_t *a, + const svn_sort__item_t *b) +{ + const char *astr, *bstr; + int n_a, n_b, i; + + astr = a->key; + bstr = b->key; + + for (i = 0; i < a->klen; i++) + if (astr[i] == '/') + n_a++; + + for (i = 0; i < b->klen; i++) + if (bstr[i] == '/') + n_b++; + + if (n_a > n_b) + return -1; + else if (n_a == n_b) + return 0; + else + return 1; +} + +/* If all TARGETS in a dir is to be deleted, we condense those targets into + * one, representing their parent dirs. A new array is allocated from + * RESULT_POOL and returned in TARGETS. */ +static svn_error_t * +maybe_condense_deleted_targets(apr_array_header_t **targets, + svn_client_ctx_t *ctx, apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + int i; + apr_array_header_t *new_targets; + apr_array_header_t *deleted_targets; + apr_array_header_t *sorted_keys; + apr_hash_t *targets_to_be_deleted; + + new_targets = apr_array_make(result_pool, 0, sizeof(patch_target_t *)); + targets_to_be_deleted = apr_hash_make(result_pool); + + /* First we collect the targets that should be deleted ... */ + for (i = 0; i < (*targets)->nelts; i++) + { + patch_target_t *target = APR_ARRAY_IDX(*targets, i, patch_target_t *); + + if (! target->deleted) + APR_ARRAY_PUSH(new_targets, patch_target_t *) = target; + else + SVN_ERR(add_target_to_hash_keyed_by_parent_dir(targets_to_be_deleted, + target, result_pool)); + } + + /* ... Then we sort the keys to allow us to detect when multiple subdirs + * should be deleted. */ + sorted_keys = svn_sort__hash(targets_to_be_deleted, + sort_compare_nr_of_path_elements, + scratch_pool); + + for (i = 0; i < sorted_keys->nelts ; i++) + { + svn_sort__item_t *item; + svn_boolean_t empty; + char *key; + apr_array_header_t *targets_array; + int j; + + item = &APR_ARRAY_IDX(sorted_keys, i, svn_sort__item_t); + key = (char *) item->key; + targets_array = (apr_array_header_t *) item->value; + + SVN_ERR(is_dir_empty(&empty, key, ctx, targets_array, + result_pool, scratch_pool)); + if (empty) + { + patch_target_t *target = apr_palloc(result_pool, sizeof(*target)); + target->deleted = TRUE; + target->kind = svn_node_dir; + target->abs_path = apr_pstrdup(result_pool, key); + + /* Since the dir was empty we'll use a parent_dir target instead + * of the child targets. Time to close unused streams! */ + for (j = 0; j < targets_array->nelts; j++) + { + patch_target_t *child_target; + + child_target = APR_ARRAY_IDX(targets_array, j, patch_target_t *); + + if (child_target->patch) + SVN_ERR(svn_diff__close_patch(child_target->patch)); + } + + SVN_ERR(add_target_to_hash_keyed_by_parent_dir(targets_to_be_deleted, + target, result_pool)); + + /* Hack! Since the added target of the parent dir has a shorter + * path, we're guarenteed that it will be inserted later. We do + * the sort and just continue our iteration. */ + sorted_keys = svn_sort__hash(targets_to_be_deleted, + sort_compare_nr_of_path_elements, + scratch_pool); + } + else + { + /* No empty dir, just add the targets to be deleted */ + for (j = 0; j < targets_array->nelts; j++) + { + patch_target_t *target = APR_ARRAY_IDX(targets_array, j, + patch_target_t *); + APR_ARRAY_PUSH(new_targets, patch_target_t *) = target; + } + } + } + *targets = new_targets; + + return SVN_NO_ERROR; +} + /* Install a patched TARGET into the working copy at ABS_WC_PATH. * Use client context CTX to retrieve WC_CTX, and possibly doing * notifications. If DRY_RUN is TRUE, don't modify the working copy. @@ -1413,6 +1652,9 @@ } while (patch); + SVN_ERR(maybe_condense_deleted_targets(&targets, btn->ctx, scratch_pool, + result_pool)); + /* Install patched targets into the working copy. */ for (i = 0; i < targets->nelts; i++) { @@ -1428,7 +1670,8 @@ SVN_ERR(install_patched_target(target, btn->abs_wc_path, btn->ctx, btn->dry_run, iterpool)); SVN_ERR(send_patch_notification(target, btn->ctx, iterpool)); - SVN_ERR(svn_diff__close_patch(target->patch)); + if (target->patch) + SVN_ERR(svn_diff__close_patch(target->patch)); } svn_pool_destroy(iterpool);