1/* 2 * delete.c: wrappers around wc delete functionality. 3 * 4 * ==================================================================== 5 * Licensed to the Apache Software Foundation (ASF) under one 6 * or more contributor license agreements. See the NOTICE file 7 * distributed with this work for additional information 8 * regarding copyright ownership. The ASF licenses this file 9 * to you under the Apache License, Version 2.0 (the 10 * "License"); you may not use this file except in compliance 11 * with the License. You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, 16 * software distributed under the License is distributed on an 17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 * KIND, either express or implied. See the License for the 19 * specific language governing permissions and limitations 20 * under the License. 21 * ==================================================================== 22 */ 23 24/* ==================================================================== */ 25 26 27 28/*** Includes. ***/ 29 30#include <apr_file_io.h> 31#include "svn_hash.h" 32#include "svn_types.h" 33#include "svn_pools.h" 34#include "svn_wc.h" 35#include "svn_client.h" 36#include "svn_error.h" 37#include "svn_dirent_uri.h" 38#include "svn_path.h" 39#include "client.h" 40 41#include "private/svn_client_private.h" 42#include "private/svn_wc_private.h" 43#include "private/svn_ra_private.h" 44 45#include "svn_private_config.h" 46 47 48/*** Code. ***/ 49 50/* Baton for find_undeletables */ 51struct can_delete_baton_t 52{ 53 const char *root_abspath; 54 svn_boolean_t target_missing; 55}; 56 57/* An svn_wc_status_func4_t callback function for finding 58 status structures which are not safely deletable. */ 59static svn_error_t * 60find_undeletables(void *baton, 61 const char *local_abspath, 62 const svn_wc_status3_t *status, 63 apr_pool_t *pool) 64{ 65 if (status->node_status == svn_wc_status_missing) 66 { 67 struct can_delete_baton_t *cdt = baton; 68 69 if (strcmp(cdt->root_abspath, local_abspath) == 0) 70 cdt->target_missing = TRUE; 71 } 72 73 /* Check for error-ful states. */ 74 if (status->node_status == svn_wc_status_obstructed) 75 return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, 76 _("'%s' is in the way of the resource " 77 "actually under version control"), 78 svn_dirent_local_style(local_abspath, pool)); 79 else if (! status->versioned) 80 return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL, 81 _("'%s' is not under version control"), 82 svn_dirent_local_style(local_abspath, pool)); 83 else if ((status->node_status == svn_wc_status_added 84 || status->node_status == svn_wc_status_replaced) 85 && status->text_status == svn_wc_status_normal 86 && (status->prop_status == svn_wc_status_normal 87 || status->prop_status == svn_wc_status_none)) 88 { 89 /* Unmodified copy. Go ahead, remove it */ 90 } 91 else if (status->node_status != svn_wc_status_normal 92 && status->node_status != svn_wc_status_deleted 93 && status->node_status != svn_wc_status_missing) 94 return svn_error_createf(SVN_ERR_CLIENT_MODIFIED, NULL, 95 _("'%s' has local modifications -- commit or " 96 "revert them first"), 97 svn_dirent_local_style(local_abspath, pool)); 98 99 return SVN_NO_ERROR; 100} 101 102/* Check whether LOCAL_ABSPATH is an external and raise an error if it is. 103 104 A file external should not be deleted since the file external is 105 implemented as a switched file and it would delete the file the 106 file external is switched to, which is not the behavior the user 107 would probably want. 108 109 A directory external should not be deleted since it is the root 110 of a different working copy. */ 111static svn_error_t * 112check_external(const char *local_abspath, 113 svn_client_ctx_t *ctx, 114 apr_pool_t *scratch_pool) 115{ 116 svn_node_kind_t external_kind; 117 const char *defining_abspath; 118 119 SVN_ERR(svn_wc__read_external_info(&external_kind, &defining_abspath, NULL, 120 NULL, NULL, 121 ctx->wc_ctx, local_abspath, 122 local_abspath, TRUE, 123 scratch_pool, scratch_pool)); 124 125 if (external_kind != svn_node_none) 126 return svn_error_createf(SVN_ERR_WC_CANNOT_DELETE_FILE_EXTERNAL, NULL, 127 _("Cannot remove the external at '%s'; " 128 "please edit or delete the svn:externals " 129 "property on '%s'"), 130 svn_dirent_local_style(local_abspath, 131 scratch_pool), 132 svn_dirent_local_style(defining_abspath, 133 scratch_pool)); 134 135 return SVN_NO_ERROR; 136} 137 138/* Verify that the path can be deleted without losing stuff, 139 i.e. ensure that there are no modified or unversioned resources 140 under PATH. This is similar to checking the output of the status 141 command. CTX is used for the client's config options. POOL is 142 used for all temporary allocations. */ 143static svn_error_t * 144can_delete_node(svn_boolean_t *target_missing, 145 const char *local_abspath, 146 svn_client_ctx_t *ctx, 147 apr_pool_t *scratch_pool) 148{ 149 apr_array_header_t *ignores; 150 struct can_delete_baton_t cdt; 151 152 /* Use an infinite-depth status check to see if there's anything in 153 or under PATH which would make it unsafe for deletion. The 154 status callback function find_undeletables() makes the 155 determination, returning an error if it finds anything that shouldn't 156 be deleted. */ 157 158 SVN_ERR(svn_wc_get_default_ignores(&ignores, ctx->config, scratch_pool)); 159 160 cdt.root_abspath = local_abspath; 161 cdt.target_missing = FALSE; 162 163 SVN_ERR(svn_wc_walk_status(ctx->wc_ctx, 164 local_abspath, 165 svn_depth_infinity, 166 FALSE /* get_all */, 167 FALSE /* no_ignore */, 168 FALSE /* ignore_text_mod */, 169 ignores, 170 find_undeletables, &cdt, 171 ctx->cancel_func, 172 ctx->cancel_baton, 173 scratch_pool)); 174 175 if (target_missing) 176 *target_missing = cdt.target_missing; 177 178 return SVN_NO_ERROR; 179} 180 181 182static svn_error_t * 183path_driver_cb_func(void **dir_baton, 184 void *parent_baton, 185 void *callback_baton, 186 const char *path, 187 apr_pool_t *pool) 188{ 189 const svn_delta_editor_t *editor = callback_baton; 190 *dir_baton = NULL; 191 return editor->delete_entry(path, SVN_INVALID_REVNUM, parent_baton, pool); 192} 193 194static svn_error_t * 195single_repos_delete(svn_ra_session_t *ra_session, 196 const char *repos_root, 197 const apr_array_header_t *relpaths, 198 const apr_hash_t *revprop_table, 199 svn_commit_callback2_t commit_callback, 200 void *commit_baton, 201 svn_client_ctx_t *ctx, 202 apr_pool_t *pool) 203{ 204 const svn_delta_editor_t *editor; 205 apr_hash_t *commit_revprops; 206 void *edit_baton; 207 const char *log_msg; 208 int i; 209 svn_error_t *err; 210 211 /* Create new commit items and add them to the array. */ 212 if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx)) 213 { 214 svn_client_commit_item3_t *item; 215 const char *tmp_file; 216 apr_array_header_t *commit_items 217 = apr_array_make(pool, relpaths->nelts, sizeof(item)); 218 219 for (i = 0; i < relpaths->nelts; i++) 220 { 221 const char *relpath = APR_ARRAY_IDX(relpaths, i, const char *); 222 223 item = svn_client_commit_item3_create(pool); 224 item->url = svn_path_url_add_component2(repos_root, relpath, pool); 225 item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE; 226 APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item; 227 } 228 SVN_ERR(svn_client__get_log_msg(&log_msg, &tmp_file, commit_items, 229 ctx, pool)); 230 if (! log_msg) 231 return SVN_NO_ERROR; 232 } 233 else 234 log_msg = ""; 235 236 SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table, 237 log_msg, ctx, pool)); 238 239 /* Fetch RA commit editor */ 240 SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session, 241 svn_client__get_shim_callbacks(ctx->wc_ctx, 242 NULL, pool))); 243 SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton, 244 commit_revprops, 245 commit_callback, 246 commit_baton, 247 NULL, TRUE, /* No lock tokens */ 248 pool)); 249 250 /* Call the path-based editor driver. */ 251 err = svn_delta_path_driver2(editor, edit_baton, relpaths, TRUE, 252 path_driver_cb_func, (void *)editor, pool); 253 254 if (err) 255 { 256 return svn_error_trace( 257 svn_error_compose_create(err, 258 editor->abort_edit(edit_baton, pool))); 259 } 260 261 /* Close the edit. */ 262 return svn_error_trace(editor->close_edit(edit_baton, pool)); 263} 264 265 266/* Structure for tracking remote delete targets associated with a 267 specific repository. */ 268struct repos_deletables_t 269{ 270 svn_ra_session_t *ra_session; 271 apr_array_header_t *target_uris; 272}; 273 274 275static svn_error_t * 276delete_urls_multi_repos(const apr_array_header_t *uris, 277 const apr_hash_t *revprop_table, 278 svn_commit_callback2_t commit_callback, 279 void *commit_baton, 280 svn_client_ctx_t *ctx, 281 apr_pool_t *pool) 282{ 283 apr_hash_t *deletables = apr_hash_make(pool); 284 apr_pool_t *iterpool; 285 apr_hash_index_t *hi; 286 int i; 287 288 /* Create a hash mapping repository root URLs -> repos_deletables_t * 289 structures. */ 290 for (i = 0; i < uris->nelts; i++) 291 { 292 const char *uri = APR_ARRAY_IDX(uris, i, const char *); 293 struct repos_deletables_t *repos_deletables = NULL; 294 const char *repos_relpath; 295 svn_node_kind_t kind; 296 297 for (hi = apr_hash_first(pool, deletables); hi; hi = apr_hash_next(hi)) 298 { 299 const char *repos_root = svn__apr_hash_index_key(hi); 300 301 repos_relpath = svn_uri_skip_ancestor(repos_root, uri, pool); 302 if (repos_relpath) 303 { 304 /* Great! We've found another URI underneath this 305 session. We'll pick out the related RA session for 306 use later, store the new target, and move on. */ 307 repos_deletables = svn__apr_hash_index_val(hi); 308 APR_ARRAY_PUSH(repos_deletables->target_uris, const char *) = 309 apr_pstrdup(pool, uri); 310 break; 311 } 312 } 313 314 /* If we haven't created a repos_deletable structure for this 315 delete target, we need to do. That means opening up an RA 316 session and initializing its targets list. */ 317 if (!repos_deletables) 318 { 319 svn_ra_session_t *ra_session = NULL; 320 const char *repos_root; 321 apr_array_header_t *target_uris; 322 323 /* Open an RA session to (ultimately) the root of the 324 repository in which URI is found. */ 325 SVN_ERR(svn_client_open_ra_session2(&ra_session, uri, NULL, 326 ctx, pool, pool)); 327 SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root, pool)); 328 SVN_ERR(svn_ra_reparent(ra_session, repos_root, pool)); 329 repos_relpath = svn_uri_skip_ancestor(repos_root, uri, pool); 330 331 /* Make a new relpaths list for this repository, and add 332 this URI's relpath to it. */ 333 target_uris = apr_array_make(pool, 1, sizeof(const char *)); 334 APR_ARRAY_PUSH(target_uris, const char *) = apr_pstrdup(pool, uri); 335 336 /* Build our repos_deletables_t item and stash it in the 337 hash. */ 338 repos_deletables = apr_pcalloc(pool, sizeof(*repos_deletables)); 339 repos_deletables->ra_session = ra_session; 340 repos_deletables->target_uris = target_uris; 341 svn_hash_sets(deletables, repos_root, repos_deletables); 342 } 343 344 /* If we get here, we should have been able to calculate a 345 repos_relpath for this URI. Let's make sure. (We return an 346 RA error code otherwise for 1.6 compatibility.) */ 347 if (!repos_relpath || !*repos_relpath) 348 return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, 349 "URL '%s' not within a repository", uri); 350 351 /* Now, test to see if the thing actually exists in HEAD. */ 352 SVN_ERR(svn_ra_check_path(repos_deletables->ra_session, repos_relpath, 353 SVN_INVALID_REVNUM, &kind, pool)); 354 if (kind == svn_node_none) 355 return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, 356 "URL '%s' does not exist", uri); 357 } 358 359 /* Now we iterate over the DELETABLES hash, issuing a commit for 360 each repository with its associated collected targets. */ 361 iterpool = svn_pool_create(pool); 362 for (hi = apr_hash_first(pool, deletables); hi; hi = apr_hash_next(hi)) 363 { 364 const char *repos_root = svn__apr_hash_index_key(hi); 365 struct repos_deletables_t *repos_deletables = svn__apr_hash_index_val(hi); 366 const char *base_uri; 367 apr_array_header_t *target_relpaths; 368 369 svn_pool_clear(iterpool); 370 371 /* We want to anchor the commit on the longest common path 372 across the targets for this one repository. If, however, one 373 of our targets is that longest common path, we need instead 374 anchor the commit on that path's immediate parent. Because 375 we're asking svn_uri_condense_targets() to remove 376 redundancies, this situation should be detectable by their 377 being returned either a) only a single, empty-path, target 378 relpath, or b) no target relpaths at all. */ 379 SVN_ERR(svn_uri_condense_targets(&base_uri, &target_relpaths, 380 repos_deletables->target_uris, 381 TRUE, iterpool, iterpool)); 382 SVN_ERR_ASSERT(!svn_path_is_empty(base_uri)); 383 if (target_relpaths->nelts == 0) 384 { 385 const char *target_relpath; 386 387 svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool); 388 APR_ARRAY_PUSH(target_relpaths, const char *) = target_relpath; 389 } 390 else if ((target_relpaths->nelts == 1) 391 && (svn_path_is_empty(APR_ARRAY_IDX(target_relpaths, 0, 392 const char *)))) 393 { 394 const char *target_relpath; 395 396 svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool); 397 APR_ARRAY_IDX(target_relpaths, 0, const char *) = target_relpath; 398 } 399 400 SVN_ERR(svn_ra_reparent(repos_deletables->ra_session, base_uri, pool)); 401 SVN_ERR(single_repos_delete(repos_deletables->ra_session, repos_root, 402 target_relpaths, 403 revprop_table, commit_callback, 404 commit_baton, ctx, iterpool)); 405 } 406 svn_pool_destroy(iterpool); 407 408 return SVN_NO_ERROR; 409} 410 411svn_error_t * 412svn_client__wc_delete(const char *local_abspath, 413 svn_boolean_t force, 414 svn_boolean_t dry_run, 415 svn_boolean_t keep_local, 416 svn_wc_notify_func2_t notify_func, 417 void *notify_baton, 418 svn_client_ctx_t *ctx, 419 apr_pool_t *pool) 420{ 421 svn_boolean_t target_missing = FALSE; 422 423 SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); 424 425 SVN_ERR(check_external(local_abspath, ctx, pool)); 426 427 if (!force && !keep_local) 428 /* Verify that there are no "awkward" files */ 429 SVN_ERR(can_delete_node(&target_missing, local_abspath, ctx, pool)); 430 431 if (!dry_run) 432 /* Mark the entry for commit deletion and perform wc deletion */ 433 return svn_error_trace(svn_wc_delete4(ctx->wc_ctx, local_abspath, 434 keep_local || target_missing 435 /*keep_local */, 436 TRUE /* delete_unversioned */, 437 ctx->cancel_func, ctx->cancel_baton, 438 notify_func, notify_baton, pool)); 439 440 return SVN_NO_ERROR; 441} 442 443svn_error_t * 444svn_client__wc_delete_many(const apr_array_header_t *targets, 445 svn_boolean_t force, 446 svn_boolean_t dry_run, 447 svn_boolean_t keep_local, 448 svn_wc_notify_func2_t notify_func, 449 void *notify_baton, 450 svn_client_ctx_t *ctx, 451 apr_pool_t *pool) 452{ 453 int i; 454 svn_boolean_t has_non_missing = FALSE; 455 456 for (i = 0; i < targets->nelts; i++) 457 { 458 const char *local_abspath = APR_ARRAY_IDX(targets, i, const char *); 459 460 SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); 461 462 SVN_ERR(check_external(local_abspath, ctx, pool)); 463 464 if (!force && !keep_local) 465 { 466 svn_boolean_t missing; 467 /* Verify that there are no "awkward" files */ 468 469 SVN_ERR(can_delete_node(&missing, local_abspath, ctx, pool)); 470 471 if (! missing) 472 has_non_missing = TRUE; 473 } 474 else 475 has_non_missing = TRUE; 476 } 477 478 if (!dry_run) 479 { 480 /* Mark the entry for commit deletion and perform wc deletion */ 481 482 /* If none of the targets exists, pass keep local TRUE, to avoid 483 deleting case-different files. Detecting this in the generic case 484 from the delete code is expensive */ 485 return svn_error_trace(svn_wc__delete_many(ctx->wc_ctx, targets, 486 keep_local || !has_non_missing, 487 TRUE /* delete_unversioned_target */, 488 ctx->cancel_func, 489 ctx->cancel_baton, 490 notify_func, notify_baton, 491 pool)); 492 } 493 494 return SVN_NO_ERROR; 495} 496 497svn_error_t * 498svn_client_delete4(const apr_array_header_t *paths, 499 svn_boolean_t force, 500 svn_boolean_t keep_local, 501 const apr_hash_t *revprop_table, 502 svn_commit_callback2_t commit_callback, 503 void *commit_baton, 504 svn_client_ctx_t *ctx, 505 apr_pool_t *pool) 506{ 507 svn_boolean_t is_url; 508 509 if (! paths->nelts) 510 return SVN_NO_ERROR; 511 512 SVN_ERR(svn_client__assert_homogeneous_target_type(paths)); 513 is_url = svn_path_is_url(APR_ARRAY_IDX(paths, 0, const char *)); 514 515 if (is_url) 516 { 517 SVN_ERR(delete_urls_multi_repos(paths, revprop_table, commit_callback, 518 commit_baton, ctx, pool)); 519 } 520 else 521 { 522 const char *local_abspath; 523 apr_hash_t *wcroots; 524 apr_hash_index_t *hi; 525 int i; 526 int j; 527 apr_pool_t *iterpool; 528 svn_boolean_t is_new_target; 529 530 /* Build a map of wcroots and targets within them. */ 531 wcroots = apr_hash_make(pool); 532 iterpool = svn_pool_create(pool); 533 for (i = 0; i < paths->nelts; i++) 534 { 535 const char *wcroot_abspath; 536 apr_array_header_t *targets; 537 538 svn_pool_clear(iterpool); 539 540 /* See if the user wants us to stop. */ 541 if (ctx->cancel_func) 542 SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); 543 544 SVN_ERR(svn_dirent_get_absolute(&local_abspath, 545 APR_ARRAY_IDX(paths, i, 546 const char *), 547 pool)); 548 SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, 549 local_abspath, pool, iterpool)); 550 targets = svn_hash_gets(wcroots, wcroot_abspath); 551 if (targets == NULL) 552 { 553 targets = apr_array_make(pool, 1, sizeof(const char *)); 554 svn_hash_sets(wcroots, wcroot_abspath, targets); 555 } 556 557 /* Make sure targets are unique. */ 558 is_new_target = TRUE; 559 for (j = 0; j < targets->nelts; j++) 560 { 561 if (strcmp(APR_ARRAY_IDX(targets, j, const char *), 562 local_abspath) == 0) 563 { 564 is_new_target = FALSE; 565 break; 566 } 567 } 568 569 if (is_new_target) 570 APR_ARRAY_PUSH(targets, const char *) = local_abspath; 571 } 572 573 /* Delete the targets from each working copy in turn. */ 574 for (hi = apr_hash_first(pool, wcroots); hi; hi = apr_hash_next(hi)) 575 { 576 const char *root_abspath; 577 const apr_array_header_t *targets = svn__apr_hash_index_val(hi); 578 579 svn_pool_clear(iterpool); 580 581 SVN_ERR(svn_dirent_condense_targets(&root_abspath, NULL, targets, 582 FALSE, iterpool, iterpool)); 583 584 SVN_WC__CALL_WITH_WRITE_LOCK( 585 svn_client__wc_delete_many(targets, force, FALSE, keep_local, 586 ctx->notify_func2, ctx->notify_baton2, 587 ctx, iterpool), 588 ctx->wc_ctx, root_abspath, TRUE /* lock_anchor */, 589 iterpool); 590 } 591 svn_pool_destroy(iterpool); 592 } 593 594 return SVN_NO_ERROR; 595} 596