1/* 2 * externals.c: handle the svn:externals property 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_uri.h> 31#include "svn_hash.h" 32#include "svn_wc.h" 33#include "svn_pools.h" 34#include "svn_client.h" 35#include "svn_types.h" 36#include "svn_error.h" 37#include "svn_dirent_uri.h" 38#include "svn_path.h" 39#include "svn_props.h" 40#include "svn_config.h" 41#include "client.h" 42 43#include "svn_private_config.h" 44#include "private/svn_wc_private.h" 45 46 47/* Remove the directory at LOCAL_ABSPATH from revision control, and do the 48 * same to any revision controlled directories underneath LOCAL_ABSPATH 49 * (including directories not referred to by parent svn administrative areas); 50 * then if LOCAL_ABSPATH is empty afterwards, remove it, else rename it to a 51 * unique name in the same parent directory. 52 * 53 * Pass CANCEL_FUNC, CANCEL_BATON to svn_wc_remove_from_revision_control. 54 * 55 * Use SCRATCH_POOL for all temporary allocation. 56 */ 57static svn_error_t * 58relegate_dir_external(svn_wc_context_t *wc_ctx, 59 const char *wri_abspath, 60 const char *local_abspath, 61 svn_cancel_func_t cancel_func, 62 void *cancel_baton, 63 svn_wc_notify_func2_t notify_func, 64 void *notify_baton, 65 apr_pool_t *scratch_pool) 66{ 67 svn_error_t *err = SVN_NO_ERROR; 68 69 SVN_ERR(svn_wc__acquire_write_lock(NULL, wc_ctx, local_abspath, 70 FALSE, scratch_pool, scratch_pool)); 71 72 err = svn_wc__external_remove(wc_ctx, wri_abspath, local_abspath, FALSE, 73 cancel_func, cancel_baton, scratch_pool); 74 if (err && (err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)) 75 { 76 const char *parent_dir; 77 const char *dirname; 78 const char *new_path; 79 80 svn_error_clear(err); 81 err = SVN_NO_ERROR; 82 83 svn_dirent_split(&parent_dir, &dirname, local_abspath, scratch_pool); 84 85 /* Reserve the new dir name. */ 86 SVN_ERR(svn_io_open_uniquely_named(NULL, &new_path, 87 parent_dir, dirname, ".OLD", 88 svn_io_file_del_none, 89 scratch_pool, scratch_pool)); 90 91 /* Sigh... We must fall ever so slightly from grace. 92 93 Ideally, there would be no window, however brief, when we 94 don't have a reservation on the new name. Unfortunately, 95 at least in the Unix (Linux?) version of apr_file_rename(), 96 you can't rename a directory over a file, because it's just 97 calling stdio rename(), which says: 98 99 ENOTDIR 100 A component used as a directory in oldpath or newpath 101 path is not, in fact, a directory. Or, oldpath is 102 a directory, and newpath exists but is not a directory 103 104 So instead, we get the name, then remove the file (ugh), then 105 rename the directory, hoping that nobody has gotten that name 106 in the meantime -- which would never happen in real life, so 107 no big deal. 108 */ 109 /* Do our best, but no biggy if it fails. The rename will fail. */ 110 svn_error_clear(svn_io_remove_file2(new_path, TRUE, scratch_pool)); 111 112 /* Rename. If this is still a working copy we should use the working 113 copy rename function (to release open handles) */ 114 err = svn_wc__rename_wc(wc_ctx, local_abspath, new_path, 115 scratch_pool); 116 117 if (err && err->apr_err == SVN_ERR_WC_PATH_UNEXPECTED_STATUS) 118 { 119 svn_error_clear(err); 120 121 /* And if it is no longer a working copy, we should just rename 122 it */ 123 err = svn_io_file_rename2(local_abspath, new_path, FALSE, scratch_pool); 124 } 125 126 /* ### TODO: We should notify the user about the rename */ 127 if (notify_func) 128 { 129 svn_wc_notify_t *notify; 130 131 notify = svn_wc_create_notify(err ? local_abspath : new_path, 132 svn_wc_notify_left_local_modifications, 133 scratch_pool); 134 notify->kind = svn_node_dir; 135 notify->err = err; 136 137 notify_func(notify_baton, notify, scratch_pool); 138 } 139 } 140 141 return svn_error_trace(err); 142} 143 144/* Try to update a directory external at PATH to URL at REVISION. 145 Use POOL for temporary allocations, and use the client context CTX. */ 146static svn_error_t * 147switch_dir_external(const char *local_abspath, 148 const char *url, 149 const char *url_from_externals_definition, 150 const svn_opt_revision_t *peg_revision, 151 const svn_opt_revision_t *revision, 152 const char *defining_abspath, 153 svn_boolean_t *timestamp_sleep, 154 svn_ra_session_t *ra_session, 155 svn_client_ctx_t *ctx, 156 apr_pool_t *pool) 157{ 158 svn_node_kind_t kind; 159 svn_error_t *err; 160 svn_revnum_t external_peg_rev = SVN_INVALID_REVNUM; 161 svn_revnum_t external_rev = SVN_INVALID_REVNUM; 162 apr_pool_t *subpool = svn_pool_create(pool); 163 const char *repos_root_url; 164 const char *repos_uuid; 165 166 SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); 167 168 if (peg_revision->kind == svn_opt_revision_number) 169 external_peg_rev = peg_revision->value.number; 170 171 if (revision->kind == svn_opt_revision_number) 172 external_rev = revision->value.number; 173 174 /* 175 * The code below assumes existing versioned paths are *not* part of 176 * the external's defining working copy. 177 * The working copy library does not support registering externals 178 * on top of existing BASE nodes and will error out if we try. 179 * So if the external target is part of the defining working copy's 180 * BASE tree, don't attempt to create the external. Doing so would 181 * leave behind a switched path instead of an external (since the 182 * switch succeeds but registration of the external in the DB fails). 183 * The working copy then cannot be updated until the path is switched back. 184 * See issue #4085. 185 */ 186 SVN_ERR(svn_wc__node_get_base(&kind, NULL, NULL, 187 &repos_root_url, &repos_uuid, 188 NULL, ctx->wc_ctx, local_abspath, 189 TRUE, /* ignore_enoent */ 190 pool, pool)); 191 if (kind != svn_node_unknown) 192 { 193 const char *wcroot_abspath; 194 const char *defining_wcroot_abspath; 195 196 SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, 197 local_abspath, pool, pool)); 198 SVN_ERR(svn_wc__get_wcroot(&defining_wcroot_abspath, ctx->wc_ctx, 199 defining_abspath, pool, pool)); 200 if (strcmp(wcroot_abspath, defining_wcroot_abspath) == 0) 201 return svn_error_createf(SVN_ERR_WC_PATH_UNEXPECTED_STATUS, NULL, 202 _("The external '%s' defined in %s at '%s' " 203 "cannot be checked out because '%s' is " 204 "already a versioned path."), 205 url_from_externals_definition, 206 SVN_PROP_EXTERNALS, 207 svn_dirent_local_style(defining_abspath, 208 pool), 209 svn_dirent_local_style(local_abspath, 210 pool)); 211 } 212 213 /* If path is a directory, try to update/switch to the correct URL 214 and revision. */ 215 SVN_ERR(svn_io_check_path(local_abspath, &kind, pool)); 216 if (kind == svn_node_dir) 217 { 218 const char *node_url; 219 220 /* Doubles as an "is versioned" check. */ 221 err = svn_wc__node_get_url(&node_url, ctx->wc_ctx, local_abspath, 222 pool, subpool); 223 if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) 224 { 225 svn_error_clear(err); 226 err = SVN_NO_ERROR; 227 goto relegate; 228 } 229 else if (err) 230 return svn_error_trace(err); 231 232 if (node_url) 233 { 234 svn_boolean_t is_wcroot; 235 236 SVN_ERR(svn_wc__is_wcroot(&is_wcroot, ctx->wc_ctx, local_abspath, 237 pool)); 238 239 if (! is_wcroot) 240 { 241 /* This can't be a directory external! */ 242 243 err = svn_wc__external_remove(ctx->wc_ctx, defining_abspath, 244 local_abspath, 245 TRUE /* declaration_only */, 246 ctx->cancel_func, ctx->cancel_baton, 247 pool); 248 249 if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) 250 { 251 /* New external... No problem that we can't remove it */ 252 svn_error_clear(err); 253 err = NULL; 254 } 255 else if (err) 256 return svn_error_trace(err); 257 258 return svn_error_createf(SVN_ERR_WC_PATH_UNEXPECTED_STATUS, NULL, 259 _("The external '%s' defined in %s at '%s' " 260 "cannot be checked out because '%s' is " 261 "already a versioned path."), 262 url_from_externals_definition, 263 SVN_PROP_EXTERNALS, 264 svn_dirent_local_style(defining_abspath, 265 pool), 266 svn_dirent_local_style(local_abspath, 267 pool)); 268 } 269 270 /* If we have what appears to be a version controlled 271 subdir, and its top-level URL matches that of our 272 externals definition, perform an update. */ 273 if (strcmp(node_url, url) == 0) 274 { 275 SVN_ERR(svn_client__update_internal(NULL, timestamp_sleep, 276 local_abspath, 277 revision, svn_depth_unknown, 278 FALSE, FALSE, FALSE, TRUE, 279 FALSE, TRUE, 280 ra_session, ctx, subpool)); 281 282 /* We just decided that this existing directory is an external, 283 so update the external registry with this information, like 284 when checking out an external */ 285 SVN_ERR(svn_wc__external_register(ctx->wc_ctx, 286 defining_abspath, 287 local_abspath, svn_node_dir, 288 repos_root_url, repos_uuid, 289 svn_uri_skip_ancestor(repos_root_url, 290 url, pool), 291 external_peg_rev, 292 external_rev, 293 pool)); 294 295 svn_pool_destroy(subpool); 296 goto cleanup; 297 } 298 299 /* We'd really prefer not to have to do a brute-force 300 relegation -- blowing away the current external working 301 copy and checking it out anew -- so we'll first see if we 302 can get away with a generally cheaper relocation (if 303 required) and switch-style update. 304 305 To do so, we need to know the repository root URL of the 306 external working copy as it currently sits. */ 307 err = svn_wc__node_get_repos_info(NULL, NULL, 308 &repos_root_url, &repos_uuid, 309 ctx->wc_ctx, local_abspath, 310 pool, subpool); 311 if (err) 312 { 313 if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND 314 && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY) 315 return svn_error_trace(err); 316 317 svn_error_clear(err); 318 repos_root_url = NULL; 319 repos_uuid = NULL; 320 } 321 322 if (repos_root_url) 323 { 324 /* If the new external target URL is not obviously a 325 child of the external working copy's current 326 repository root URL... */ 327 if (! svn_uri__is_ancestor(repos_root_url, url)) 328 { 329 const char *repos_root; 330 331 /* ... then figure out precisely which repository 332 root URL that target URL *is* a child of ... */ 333 SVN_ERR(svn_client_get_repos_root(&repos_root, NULL, url, 334 ctx, subpool, subpool)); 335 336 /* ... and use that to try to relocate the external 337 working copy to the target location. */ 338 err = svn_client_relocate2(local_abspath, repos_root_url, 339 repos_root, FALSE, ctx, subpool); 340 341 /* If the relocation failed because the new URL 342 points to a totally different repository, we've 343 no choice but to relegate and check out a new WC. */ 344 if (err 345 && (err->apr_err == SVN_ERR_WC_INVALID_RELOCATION 346 || (err->apr_err 347 == SVN_ERR_CLIENT_INVALID_RELOCATION))) 348 { 349 svn_error_clear(err); 350 goto relegate; 351 } 352 else if (err) 353 return svn_error_trace(err); 354 355 /* If the relocation went without a hitch, we should 356 have a new repository root URL. */ 357 repos_root_url = repos_root; 358 } 359 360 SVN_ERR(svn_client__switch_internal(NULL, local_abspath, url, 361 peg_revision, revision, 362 svn_depth_infinity, 363 TRUE, FALSE, FALSE, 364 TRUE /* ignore_ancestry */, 365 timestamp_sleep, 366 ctx, subpool)); 367 368 SVN_ERR(svn_wc__external_register(ctx->wc_ctx, 369 defining_abspath, 370 local_abspath, svn_node_dir, 371 repos_root_url, repos_uuid, 372 svn_uri_skip_ancestor( 373 repos_root_url, 374 url, subpool), 375 external_peg_rev, 376 external_rev, 377 subpool)); 378 379 svn_pool_destroy(subpool); 380 goto cleanup; 381 } 382 } 383 } 384 385 relegate: 386 387 /* Fall back on removing the WC and checking out a new one. */ 388 389 /* Ensure that we don't have any RA sessions or WC locks from failed 390 operations above. */ 391 svn_pool_destroy(subpool); 392 393 if (kind == svn_node_dir) 394 { 395 /* Buh-bye, old and busted ... */ 396 SVN_ERR(relegate_dir_external(ctx->wc_ctx, defining_abspath, 397 local_abspath, 398 ctx->cancel_func, ctx->cancel_baton, 399 ctx->notify_func2, ctx->notify_baton2, 400 pool)); 401 } 402 else 403 { 404 /* The target dir might have multiple components. Guarantee 405 the path leading down to the last component. */ 406 const char *parent = svn_dirent_dirname(local_abspath, pool); 407 SVN_ERR(svn_io_make_dir_recursively(parent, pool)); 408 } 409 410 /* ... Hello, new hotness. */ 411 SVN_ERR(svn_client__checkout_internal(NULL, timestamp_sleep, 412 url, local_abspath, peg_revision, 413 revision, svn_depth_infinity, 414 FALSE, FALSE, 415 ra_session, 416 ctx, pool)); 417 418 SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL, 419 &repos_root_url, 420 &repos_uuid, 421 ctx->wc_ctx, local_abspath, 422 pool, pool)); 423 424 SVN_ERR(svn_wc__external_register(ctx->wc_ctx, 425 defining_abspath, 426 local_abspath, svn_node_dir, 427 repos_root_url, repos_uuid, 428 svn_uri_skip_ancestor(repos_root_url, 429 url, pool), 430 external_peg_rev, 431 external_rev, 432 pool)); 433 434 cleanup: 435 /* Issues #4123 and #4130: We don't need to keep the newly checked 436 out external's DB open. */ 437 SVN_ERR(svn_wc__close_db(local_abspath, ctx->wc_ctx, pool)); 438 439 return SVN_NO_ERROR; 440} 441 442/* Try to update a file external at LOCAL_ABSPATH to SWITCH_LOC. This function 443 assumes caller has a write lock in CTX. Use SCRATCH_POOL for temporary 444 allocations, and use the client context CTX. */ 445static svn_error_t * 446switch_file_external(const char *local_abspath, 447 const svn_client__pathrev_t *switch_loc, 448 const char *record_url, 449 const svn_opt_revision_t *record_peg_revision, 450 const svn_opt_revision_t *record_revision, 451 const char *def_dir_abspath, 452 svn_ra_session_t *ra_session, 453 svn_client_ctx_t *ctx, 454 apr_pool_t *scratch_pool) 455{ 456 svn_config_t *cfg = ctx->config 457 ? svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG) 458 : NULL; 459 svn_boolean_t use_commit_times; 460 const char *diff3_cmd; 461 const char *preserved_exts_str; 462 const apr_array_header_t *preserved_exts; 463 svn_node_kind_t kind, external_kind; 464 465 SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); 466 467 /* See if the user wants last-commit timestamps instead of current ones. */ 468 SVN_ERR(svn_config_get_bool(cfg, &use_commit_times, 469 SVN_CONFIG_SECTION_MISCELLANY, 470 SVN_CONFIG_OPTION_USE_COMMIT_TIMES, FALSE)); 471 472 /* Get the external diff3, if any. */ 473 svn_config_get(cfg, &diff3_cmd, SVN_CONFIG_SECTION_HELPERS, 474 SVN_CONFIG_OPTION_DIFF3_CMD, NULL); 475 476 if (diff3_cmd != NULL) 477 SVN_ERR(svn_path_cstring_to_utf8(&diff3_cmd, diff3_cmd, scratch_pool)); 478 479 /* See which files the user wants to preserve the extension of when 480 conflict files are made. */ 481 svn_config_get(cfg, &preserved_exts_str, SVN_CONFIG_SECTION_MISCELLANY, 482 SVN_CONFIG_OPTION_PRESERVED_CF_EXTS, ""); 483 preserved_exts = *preserved_exts_str 484 ? svn_cstring_split(preserved_exts_str, "\n\r\t\v ", FALSE, scratch_pool) 485 : NULL; 486 487 { 488 const char *wcroot_abspath; 489 490 SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, local_abspath, 491 scratch_pool, scratch_pool)); 492 493 /* File externals can only be installed inside the current working copy. 494 So verify if the working copy that contains/will contain the target 495 is the defining abspath, or one of its ancestors */ 496 497 if (!svn_dirent_is_ancestor(wcroot_abspath, def_dir_abspath)) 498 return svn_error_createf( 499 SVN_ERR_WC_BAD_PATH, NULL, 500 _("Cannot insert a file external defined on '%s' " 501 "into the working copy '%s'."), 502 svn_dirent_local_style(def_dir_abspath, 503 scratch_pool), 504 svn_dirent_local_style(wcroot_abspath, 505 scratch_pool)); 506 } 507 508 SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath, 509 TRUE, FALSE, scratch_pool)); 510 511 SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL, NULL, 512 ctx->wc_ctx, local_abspath, local_abspath, 513 TRUE, scratch_pool, scratch_pool)); 514 515 /* If there is a versioned item with this name, ensure it's a file 516 external before working with it. If there is no entry in the 517 working copy, then create an empty file and add it to the working 518 copy. */ 519 if (kind != svn_node_none && kind != svn_node_unknown) 520 { 521 if (external_kind != svn_node_file) 522 { 523 return svn_error_createf( 524 SVN_ERR_CLIENT_FILE_EXTERNAL_OVERWRITE_VERSIONED, 0, 525 _("The file external from '%s' cannot overwrite the existing " 526 "versioned item at '%s'"), 527 switch_loc->url, 528 svn_dirent_local_style(local_abspath, scratch_pool)); 529 } 530 } 531 else 532 { 533 svn_node_kind_t disk_kind; 534 535 SVN_ERR(svn_io_check_path(local_abspath, &disk_kind, scratch_pool)); 536 537 if (disk_kind == svn_node_file || disk_kind == svn_node_dir) 538 return svn_error_createf(SVN_ERR_WC_PATH_FOUND, NULL, 539 _("The file external '%s' can not be " 540 "created because the node exists."), 541 svn_dirent_local_style(local_abspath, 542 scratch_pool)); 543 } 544 545 { 546 const svn_ra_reporter3_t *reporter; 547 void *report_baton; 548 const svn_delta_editor_t *switch_editor; 549 void *switch_baton; 550 svn_revnum_t revnum; 551 apr_array_header_t *inherited_props; 552 const char *target = svn_dirent_basename(local_abspath, scratch_pool); 553 554 /* Get the external file's iprops. */ 555 SVN_ERR(svn_ra_get_inherited_props(ra_session, &inherited_props, "", 556 switch_loc->rev, 557 scratch_pool, scratch_pool)); 558 559 SVN_ERR(svn_ra_reparent(ra_session, 560 svn_uri_dirname(switch_loc->url, scratch_pool), 561 scratch_pool)); 562 563 SVN_ERR(svn_wc__get_file_external_editor(&switch_editor, &switch_baton, 564 &revnum, ctx->wc_ctx, 565 local_abspath, 566 def_dir_abspath, 567 switch_loc->url, 568 switch_loc->repos_root_url, 569 switch_loc->repos_uuid, 570 inherited_props, 571 use_commit_times, 572 diff3_cmd, preserved_exts, 573 def_dir_abspath, 574 record_url, 575 record_peg_revision, 576 record_revision, 577 ctx->conflict_func2, 578 ctx->conflict_baton2, 579 ctx->cancel_func, 580 ctx->cancel_baton, 581 ctx->notify_func2, 582 ctx->notify_baton2, 583 scratch_pool, scratch_pool)); 584 585 /* Tell RA to do an update of URL+TARGET to REVISION; if we pass an 586 invalid revnum, that means RA will use the latest revision. */ 587 SVN_ERR(svn_ra_do_switch3(ra_session, &reporter, &report_baton, 588 switch_loc->rev, 589 target, svn_depth_unknown, switch_loc->url, 590 FALSE /* send_copyfrom */, 591 TRUE /* ignore_ancestry */, 592 switch_editor, switch_baton, 593 scratch_pool, scratch_pool)); 594 595 SVN_ERR(svn_wc__crawl_file_external(ctx->wc_ctx, local_abspath, 596 reporter, report_baton, 597 TRUE, use_commit_times, 598 ctx->cancel_func, ctx->cancel_baton, 599 ctx->notify_func2, ctx->notify_baton2, 600 scratch_pool)); 601 602 if (ctx->notify_func2) 603 { 604 svn_wc_notify_t *notify 605 = svn_wc_create_notify(local_abspath, svn_wc_notify_update_completed, 606 scratch_pool); 607 notify->kind = svn_node_none; 608 notify->content_state = notify->prop_state 609 = svn_wc_notify_state_inapplicable; 610 notify->lock_state = svn_wc_notify_lock_state_inapplicable; 611 notify->revision = revnum; 612 ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); 613 } 614 } 615 616 return SVN_NO_ERROR; 617} 618 619/* Wrappers around svn_wc__external_remove, obtaining and releasing a lock for 620 directory externals */ 621static svn_error_t * 622remove_external2(svn_boolean_t *removed, 623 svn_wc_context_t *wc_ctx, 624 const char *wri_abspath, 625 const char *local_abspath, 626 svn_node_kind_t external_kind, 627 svn_cancel_func_t cancel_func, 628 void *cancel_baton, 629 apr_pool_t *scratch_pool) 630{ 631 SVN_ERR(svn_wc__external_remove(wc_ctx, wri_abspath, 632 local_abspath, 633 (external_kind == svn_node_none), 634 cancel_func, cancel_baton, 635 scratch_pool)); 636 637 *removed = TRUE; 638 return SVN_NO_ERROR; 639} 640 641 642static svn_error_t * 643remove_external(svn_boolean_t *removed, 644 svn_wc_context_t *wc_ctx, 645 const char *wri_abspath, 646 const char *local_abspath, 647 svn_node_kind_t external_kind, 648 svn_cancel_func_t cancel_func, 649 void *cancel_baton, 650 apr_pool_t *scratch_pool) 651{ 652 *removed = FALSE; 653 switch (external_kind) 654 { 655 case svn_node_dir: 656 SVN_WC__CALL_WITH_WRITE_LOCK( 657 remove_external2(removed, 658 wc_ctx, wri_abspath, 659 local_abspath, external_kind, 660 cancel_func, cancel_baton, 661 scratch_pool), 662 wc_ctx, local_abspath, FALSE, scratch_pool); 663 break; 664 case svn_node_file: 665 default: 666 SVN_ERR(remove_external2(removed, 667 wc_ctx, wri_abspath, 668 local_abspath, external_kind, 669 cancel_func, cancel_baton, 670 scratch_pool)); 671 break; 672 } 673 674 return SVN_NO_ERROR; 675} 676 677/* Called when an external that is in the EXTERNALS table is no longer 678 referenced from an svn:externals property */ 679static svn_error_t * 680handle_external_item_removal(const svn_client_ctx_t *ctx, 681 const char *defining_abspath, 682 const char *local_abspath, 683 apr_pool_t *scratch_pool) 684{ 685 svn_error_t *err; 686 svn_node_kind_t external_kind; 687 svn_node_kind_t kind; 688 svn_boolean_t removed = FALSE; 689 690 /* local_abspath should be a wcroot or a file external */ 691 SVN_ERR(svn_wc__read_external_info(&external_kind, NULL, NULL, NULL, NULL, 692 ctx->wc_ctx, defining_abspath, 693 local_abspath, FALSE, 694 scratch_pool, scratch_pool)); 695 696 SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, local_abspath, TRUE, FALSE, 697 scratch_pool)); 698 699 if (external_kind != kind) 700 external_kind = svn_node_none; /* Only remove the registration */ 701 702 err = remove_external(&removed, 703 ctx->wc_ctx, defining_abspath, local_abspath, 704 external_kind, 705 ctx->cancel_func, ctx->cancel_baton, 706 scratch_pool); 707 708 if (err && err->apr_err == SVN_ERR_WC_NOT_LOCKED && removed) 709 { 710 svn_error_clear(err); 711 err = NULL; /* We removed the working copy, so we can't release the 712 lock that was stored inside */ 713 } 714 715 if (ctx->notify_func2) 716 { 717 svn_wc_notify_t *notify = 718 svn_wc_create_notify(local_abspath, 719 svn_wc_notify_update_external_removed, 720 scratch_pool); 721 722 notify->kind = kind; 723 notify->err = err; 724 725 ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); 726 727 if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD) 728 { 729 notify = svn_wc_create_notify(local_abspath, 730 svn_wc_notify_left_local_modifications, 731 scratch_pool); 732 notify->kind = svn_node_dir; 733 notify->err = err; 734 735 ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); 736 } 737 } 738 739 if (err && err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD) 740 { 741 svn_error_clear(err); 742 err = NULL; 743 } 744 745 return svn_error_trace(err); 746} 747 748static svn_error_t * 749handle_external_item_change(svn_client_ctx_t *ctx, 750 const char *repos_root_url, 751 const char *parent_dir_abspath, 752 const char *parent_dir_url, 753 const char *local_abspath, 754 const char *old_defining_abspath, 755 const svn_wc_external_item2_t *new_item, 756 svn_ra_session_t *ra_session, 757 svn_boolean_t *timestamp_sleep, 758 apr_pool_t *scratch_pool) 759{ 760 svn_client__pathrev_t *new_loc; 761 const char *new_url; 762 svn_node_kind_t ext_kind; 763 764 SVN_ERR_ASSERT(repos_root_url && parent_dir_url); 765 SVN_ERR_ASSERT(new_item != NULL); 766 767 /* Don't bother to check status, since we'll get that for free by 768 attempting to retrieve the hash values anyway. */ 769 770 /* When creating the absolute URL, use the pool and not the 771 iterpool, since the hash table values outlive the iterpool and 772 any pointers they have should also outlive the iterpool. */ 773 774 SVN_ERR(svn_wc__resolve_relative_external_url(&new_url, 775 new_item, repos_root_url, 776 parent_dir_url, 777 scratch_pool, scratch_pool)); 778 779 /* Determine if the external is a file or directory. */ 780 /* Get the RA connection, if needed. */ 781 if (ra_session) 782 { 783 svn_error_t *err = svn_ra_reparent(ra_session, new_url, scratch_pool); 784 785 if (err) 786 { 787 if (err->apr_err == SVN_ERR_RA_ILLEGAL_URL) 788 { 789 svn_error_clear(err); 790 ra_session = NULL; 791 } 792 else 793 return svn_error_trace(err); 794 } 795 else 796 { 797 SVN_ERR(svn_client__resolve_rev_and_url(&new_loc, 798 ra_session, new_url, 799 &(new_item->peg_revision), 800 &(new_item->revision), ctx, 801 scratch_pool)); 802 803 SVN_ERR(svn_ra_reparent(ra_session, new_loc->url, scratch_pool)); 804 } 805 } 806 807 if (!ra_session) 808 SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc, 809 new_url, NULL, 810 &(new_item->peg_revision), 811 &(new_item->revision), ctx, 812 scratch_pool)); 813 814 SVN_ERR(svn_ra_check_path(ra_session, "", new_loc->rev, &ext_kind, 815 scratch_pool)); 816 817 if (svn_node_none == ext_kind) 818 return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, 819 _("URL '%s' at revision %ld doesn't exist"), 820 new_loc->url, new_loc->rev); 821 822 if (svn_node_dir != ext_kind && svn_node_file != ext_kind) 823 return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, 824 _("URL '%s' at revision %ld is not a file " 825 "or a directory"), 826 new_loc->url, new_loc->rev); 827 828 829 /* Not protecting against recursive externals. Detecting them in 830 the global case is hard, and it should be pretty obvious to a 831 user when it happens. Worst case: your disk fills up :-). */ 832 833 /* First notify that we're about to handle an external. */ 834 if (ctx->notify_func2) 835 { 836 ctx->notify_func2( 837 ctx->notify_baton2, 838 svn_wc_create_notify(local_abspath, 839 svn_wc_notify_update_external, 840 scratch_pool), 841 scratch_pool); 842 } 843 844 if (! old_defining_abspath) 845 { 846 /* The target dir might have multiple components. Guarantee the path 847 leading down to the last component. */ 848 SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(local_abspath, 849 scratch_pool), 850 scratch_pool)); 851 } 852 853 switch (ext_kind) 854 { 855 case svn_node_dir: 856 SVN_ERR(switch_dir_external(local_abspath, new_loc->url, 857 new_item->url, 858 &(new_item->peg_revision), 859 &(new_item->revision), 860 parent_dir_abspath, 861 timestamp_sleep, ra_session, ctx, 862 scratch_pool)); 863 break; 864 case svn_node_file: 865 if (strcmp(repos_root_url, new_loc->repos_root_url)) 866 { 867 const char *local_repos_root_url; 868 const char *local_repos_uuid; 869 const char *ext_repos_relpath; 870 svn_error_t *err; 871 872 /* 873 * The working copy library currently requires that all files 874 * in the working copy have the same repository root URL. 875 * The URL from the file external's definition differs from the 876 * one used by the working copy. As a workaround, replace the 877 * root URL portion of the file external's URL, after making 878 * sure both URLs point to the same repository. See issue #4087. 879 */ 880 881 err = svn_wc__node_get_repos_info(NULL, NULL, 882 &local_repos_root_url, 883 &local_repos_uuid, 884 ctx->wc_ctx, parent_dir_abspath, 885 scratch_pool, scratch_pool); 886 if (err) 887 { 888 if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND 889 && err->apr_err != SVN_ERR_WC_NOT_WORKING_COPY) 890 return svn_error_trace(err); 891 892 svn_error_clear(err); 893 local_repos_root_url = NULL; 894 local_repos_uuid = NULL; 895 } 896 897 ext_repos_relpath = svn_uri_skip_ancestor(new_loc->repos_root_url, 898 new_url, scratch_pool); 899 if (local_repos_uuid == NULL || local_repos_root_url == NULL || 900 ext_repos_relpath == NULL || 901 strcmp(local_repos_uuid, new_loc->repos_uuid) != 0) 902 return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, 903 _("Unsupported external: URL of file external '%s' " 904 "is not in repository '%s'"), 905 new_url, repos_root_url); 906 907 new_url = svn_path_url_add_component2(local_repos_root_url, 908 ext_repos_relpath, 909 scratch_pool); 910 SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &new_loc, 911 new_url, 912 NULL, 913 &(new_item->peg_revision), 914 &(new_item->revision), 915 ctx, scratch_pool)); 916 } 917 918 SVN_ERR(switch_file_external(local_abspath, 919 new_loc, 920 new_url, 921 &new_item->peg_revision, 922 &new_item->revision, 923 parent_dir_abspath, 924 ra_session, 925 ctx, 926 scratch_pool)); 927 break; 928 929 default: 930 SVN_ERR_MALFUNCTION(); 931 break; 932 } 933 934 return SVN_NO_ERROR; 935} 936 937static svn_error_t * 938wrap_external_error(const svn_client_ctx_t *ctx, 939 const char *target_abspath, 940 svn_error_t *err, 941 apr_pool_t *scratch_pool) 942{ 943 if (err && err->apr_err != SVN_ERR_CANCELLED) 944 { 945 if (ctx->notify_func2) 946 { 947 svn_wc_notify_t *notifier = svn_wc_create_notify( 948 target_abspath, 949 svn_wc_notify_failed_external, 950 scratch_pool); 951 notifier->err = err; 952 ctx->notify_func2(ctx->notify_baton2, notifier, scratch_pool); 953 } 954 svn_error_clear(err); 955 return SVN_NO_ERROR; 956 } 957 958 return err; 959} 960 961static svn_error_t * 962handle_externals_change(svn_client_ctx_t *ctx, 963 const char *repos_root_url, 964 svn_boolean_t *timestamp_sleep, 965 const char *local_abspath, 966 const char *new_desc_text, 967 apr_hash_t *old_externals, 968 svn_depth_t ambient_depth, 969 svn_depth_t requested_depth, 970 svn_ra_session_t *ra_session, 971 apr_pool_t *scratch_pool) 972{ 973 apr_array_header_t *new_desc; 974 int i; 975 apr_pool_t *iterpool; 976 const char *url; 977 978 iterpool = svn_pool_create(scratch_pool); 979 980 SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); 981 982 /* Bag out if the depth here is too shallow for externals action. */ 983 if ((requested_depth < svn_depth_infinity 984 && requested_depth != svn_depth_unknown) 985 || (ambient_depth < svn_depth_infinity 986 && requested_depth < svn_depth_infinity)) 987 return SVN_NO_ERROR; 988 989 if (new_desc_text) 990 SVN_ERR(svn_wc_parse_externals_description3(&new_desc, local_abspath, 991 new_desc_text, 992 FALSE, scratch_pool)); 993 else 994 new_desc = NULL; 995 996 SVN_ERR(svn_wc__node_get_url(&url, ctx->wc_ctx, local_abspath, 997 scratch_pool, iterpool)); 998 999 SVN_ERR_ASSERT(url); 1000 1001 for (i = 0; new_desc && (i < new_desc->nelts); i++) 1002 { 1003 const char *old_defining_abspath; 1004 svn_wc_external_item2_t *new_item; 1005 const char *target_abspath; 1006 svn_boolean_t under_root; 1007 1008 new_item = APR_ARRAY_IDX(new_desc, i, svn_wc_external_item2_t *); 1009 1010 svn_pool_clear(iterpool); 1011 1012 if (ctx->cancel_func) 1013 SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); 1014 1015 SVN_ERR(svn_dirent_is_under_root(&under_root, &target_abspath, 1016 local_abspath, new_item->target_dir, 1017 iterpool)); 1018 1019 if (! under_root) 1020 { 1021 return svn_error_createf( 1022 SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, 1023 _("Path '%s' is not in the working copy"), 1024 svn_dirent_local_style( 1025 svn_dirent_join(local_abspath, new_item->target_dir, 1026 iterpool), 1027 iterpool)); 1028 } 1029 1030 old_defining_abspath = svn_hash_gets(old_externals, target_abspath); 1031 1032 SVN_ERR(wrap_external_error( 1033 ctx, target_abspath, 1034 handle_external_item_change(ctx, 1035 repos_root_url, 1036 local_abspath, url, 1037 target_abspath, 1038 old_defining_abspath, 1039 new_item, ra_session, 1040 timestamp_sleep, 1041 iterpool), 1042 iterpool)); 1043 1044 /* And remove already processed items from the to-remove hash */ 1045 if (old_defining_abspath) 1046 svn_hash_sets(old_externals, target_abspath, NULL); 1047 } 1048 1049 svn_pool_destroy(iterpool); 1050 1051 return SVN_NO_ERROR; 1052} 1053 1054 1055svn_error_t * 1056svn_client__handle_externals(apr_hash_t *externals_new, 1057 apr_hash_t *ambient_depths, 1058 const char *repos_root_url, 1059 const char *target_abspath, 1060 svn_depth_t requested_depth, 1061 svn_boolean_t *timestamp_sleep, 1062 svn_ra_session_t *ra_session, 1063 svn_client_ctx_t *ctx, 1064 apr_pool_t *scratch_pool) 1065{ 1066 apr_hash_t *old_external_defs; 1067 apr_hash_index_t *hi; 1068 apr_pool_t *iterpool; 1069 1070 SVN_ERR_ASSERT(repos_root_url); 1071 1072 iterpool = svn_pool_create(scratch_pool); 1073 1074 SVN_ERR(svn_wc__externals_defined_below(&old_external_defs, 1075 ctx->wc_ctx, target_abspath, 1076 scratch_pool, iterpool)); 1077 1078 for (hi = apr_hash_first(scratch_pool, externals_new); 1079 hi; 1080 hi = apr_hash_next(hi)) 1081 { 1082 const char *local_abspath = apr_hash_this_key(hi); 1083 const char *desc_text = apr_hash_this_val(hi); 1084 svn_depth_t ambient_depth = svn_depth_infinity; 1085 1086 svn_pool_clear(iterpool); 1087 1088 if (ambient_depths) 1089 { 1090 const char *ambient_depth_w; 1091 1092 ambient_depth_w = apr_hash_get(ambient_depths, local_abspath, 1093 apr_hash_this_key_len(hi)); 1094 1095 if (ambient_depth_w == NULL) 1096 { 1097 return svn_error_createf( 1098 SVN_ERR_WC_CORRUPT, NULL, 1099 _("Traversal of '%s' found no ambient depth"), 1100 svn_dirent_local_style(local_abspath, scratch_pool)); 1101 } 1102 else 1103 { 1104 ambient_depth = svn_depth_from_word(ambient_depth_w); 1105 } 1106 } 1107 1108 SVN_ERR(handle_externals_change(ctx, repos_root_url, timestamp_sleep, 1109 local_abspath, 1110 desc_text, old_external_defs, 1111 ambient_depth, requested_depth, 1112 ra_session, iterpool)); 1113 } 1114 1115 /* Remove the remaining externals */ 1116 for (hi = apr_hash_first(scratch_pool, old_external_defs); 1117 hi; 1118 hi = apr_hash_next(hi)) 1119 { 1120 const char *item_abspath = apr_hash_this_key(hi); 1121 const char *defining_abspath = apr_hash_this_val(hi); 1122 const char *parent_abspath; 1123 1124 svn_pool_clear(iterpool); 1125 1126 SVN_ERR(wrap_external_error( 1127 ctx, item_abspath, 1128 handle_external_item_removal(ctx, defining_abspath, 1129 item_abspath, iterpool), 1130 iterpool)); 1131 1132 /* Are there any unversioned directories between the removed 1133 * external and the DEFINING_ABSPATH which we can remove? */ 1134 parent_abspath = item_abspath; 1135 do { 1136 svn_node_kind_t kind; 1137 1138 parent_abspath = svn_dirent_dirname(parent_abspath, iterpool); 1139 SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, parent_abspath, 1140 FALSE /* show_deleted*/, 1141 FALSE /* show_hidden */, 1142 iterpool)); 1143 if (kind == svn_node_none) 1144 { 1145 svn_error_t *err; 1146 1147 err = svn_io_dir_remove_nonrecursive(parent_abspath, iterpool); 1148 if (err) 1149 { 1150 if (APR_STATUS_IS_ENOTEMPTY(err->apr_err)) 1151 { 1152 svn_error_clear(err); 1153 break; /* No parents to delete */ 1154 } 1155 else if (APR_STATUS_IS_ENOENT(err->apr_err) 1156 || APR_STATUS_IS_ENOTDIR(err->apr_err)) 1157 { 1158 svn_error_clear(err); 1159 /* Fall through; parent dir might be unversioned */ 1160 } 1161 else 1162 return svn_error_trace(err); 1163 } 1164 } 1165 } while (strcmp(parent_abspath, defining_abspath) != 0); 1166 } 1167 1168 1169 svn_pool_destroy(iterpool); 1170 return SVN_NO_ERROR; 1171} 1172 1173 1174svn_error_t * 1175svn_client__export_externals(apr_hash_t *externals, 1176 const char *from_url, 1177 const char *to_abspath, 1178 const char *repos_root_url, 1179 svn_depth_t requested_depth, 1180 const char *native_eol, 1181 svn_boolean_t ignore_keywords, 1182 svn_client_ctx_t *ctx, 1183 apr_pool_t *scratch_pool) 1184{ 1185 apr_pool_t *iterpool = svn_pool_create(scratch_pool); 1186 apr_pool_t *sub_iterpool = svn_pool_create(scratch_pool); 1187 apr_hash_index_t *hi; 1188 1189 SVN_ERR_ASSERT(svn_dirent_is_absolute(to_abspath)); 1190 1191 for (hi = apr_hash_first(scratch_pool, externals); 1192 hi; 1193 hi = apr_hash_next(hi)) 1194 { 1195 const char *local_abspath = apr_hash_this_key(hi); 1196 const char *desc_text = apr_hash_this_val(hi); 1197 const char *local_relpath; 1198 const char *dir_url; 1199 apr_array_header_t *items; 1200 int i; 1201 1202 svn_pool_clear(iterpool); 1203 1204 SVN_ERR(svn_wc_parse_externals_description3(&items, local_abspath, 1205 desc_text, FALSE, 1206 iterpool)); 1207 1208 if (! items->nelts) 1209 continue; 1210 1211 local_relpath = svn_dirent_skip_ancestor(to_abspath, local_abspath); 1212 1213 dir_url = svn_path_url_add_component2(from_url, local_relpath, 1214 scratch_pool); 1215 1216 for (i = 0; i < items->nelts; i++) 1217 { 1218 const char *item_abspath; 1219 const char *new_url; 1220 svn_boolean_t under_root; 1221 svn_wc_external_item2_t *item = APR_ARRAY_IDX(items, i, 1222 svn_wc_external_item2_t *); 1223 1224 svn_pool_clear(sub_iterpool); 1225 1226 SVN_ERR(svn_dirent_is_under_root(&under_root, &item_abspath, 1227 local_abspath, item->target_dir, 1228 sub_iterpool)); 1229 1230 if (! under_root) 1231 { 1232 return svn_error_createf( 1233 SVN_ERR_WC_OBSTRUCTED_UPDATE, NULL, 1234 _("Path '%s' is not in the working copy"), 1235 svn_dirent_local_style( 1236 svn_dirent_join(local_abspath, item->target_dir, 1237 sub_iterpool), 1238 sub_iterpool)); 1239 } 1240 1241 SVN_ERR(svn_wc__resolve_relative_external_url(&new_url, item, 1242 repos_root_url, 1243 dir_url, sub_iterpool, 1244 sub_iterpool)); 1245 1246 /* The target dir might have multiple components. Guarantee 1247 the path leading down to the last component. */ 1248 SVN_ERR(svn_io_make_dir_recursively(svn_dirent_dirname(item_abspath, 1249 sub_iterpool), 1250 sub_iterpool)); 1251 1252 /* First notify that we're about to handle an external. */ 1253 if (ctx->notify_func2) 1254 { 1255 ctx->notify_func2( 1256 ctx->notify_baton2, 1257 svn_wc_create_notify(item_abspath, 1258 svn_wc_notify_update_external, 1259 sub_iterpool), 1260 sub_iterpool); 1261 } 1262 1263 SVN_ERR(wrap_external_error( 1264 ctx, item_abspath, 1265 svn_client_export5(NULL, new_url, item_abspath, 1266 &item->peg_revision, 1267 &item->revision, 1268 TRUE, FALSE, ignore_keywords, 1269 svn_depth_infinity, 1270 native_eol, 1271 ctx, sub_iterpool), 1272 sub_iterpool)); 1273 } 1274 } 1275 1276 svn_pool_destroy(sub_iterpool); 1277 svn_pool_destroy(iterpool); 1278 1279 return SVN_NO_ERROR; 1280} 1281 1282