locking_commands.c revision 289180
150476Speter/* 215903Swosch * locking_commands.c: Implementation of lock and unlock. 315903Swosch * 415903Swosch * ==================================================================== 515903Swosch * Licensed to the Apache Software Foundation (ASF) under one 615903Swosch * or more contributor license agreements. See the NOTICE file 715903Swosch * distributed with this work for additional information 8105327Sru * regarding copyright ownership. The ASF licenses this file 9105327Sru * to you under the Apache License, Version 2.0 (the 10105327Sru * "License"); you may not use this file except in compliance 11105327Sru * with the License. You may obtain a copy of the License at 1215903Swosch * 1315903Swosch * http://www.apache.org/licenses/LICENSE-2.0 14105327Sru * 15105327Sru * Unless required by applicable law or agreed to in writing, 16105327Sru * software distributed under the License is distributed on an 17105327Sru * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 1815903Swosch * KIND, either express or implied. See the License for the 1915903Swosch * specific language governing permissions and limitations 2015903Swosch * under the License. 21139761Skrion * ==================================================================== 2215903Swosch */ 2315903Swosch 24124490Sru/* ==================================================================== */ 25124490Sru 2615903Swosch 2715903Swosch 2815903Swosch/*** Includes. ***/ 2915903Swosch 3015903Swosch#include "svn_client.h" 3115903Swosch#include "svn_hash.h" 3215903Swosch#include "client.h" 3315903Swosch#include "svn_dirent_uri.h" 3415903Swosch#include "svn_path.h" 3515903Swosch#include "svn_xml.h" 3615903Swosch#include "svn_pools.h" 37105327Sru 38105327Sru#include "svn_private_config.h" 39105327Sru#include "private/svn_client_private.h" 40105327Sru#include "private/svn_wc_private.h" 411845Swollman 4295255Sru 4395255Sru/*** Code. ***/ 4495255Sru 4515903Swosch/* For use with store_locks_callback, below. */ 46105327Srustruct lock_baton 47105327Sru{ 48105327Sru const char *base_dir_abspath; 49105327Sru apr_hash_t *urls_to_paths; /* url -> abspath */ 50105327Sru const char *base_url; 5194768Sru svn_client_ctx_t *ctx; 5296132Sbde apr_pool_t *pool; 5394768Sru}; 5494768Sru 5594768Sru 5614986Swosch/* This callback is called by the ra_layer for each path locked. 5714986Swosch * BATON is a 'struct lock_baton *', PATH is the path being locked, 58105327Sru * and LOCK is the lock itself. 59139108Sru * 60105327Sru * If BATON->urls_to_paths is not null, then this function either 61202579Sru * stores the LOCK on REL_URL or removes any lock tokens from REL_URL 62105327Sru * (depending on whether DO_LOCK is true or false respectively), but 63105327Sru * only if RA_ERR is null, or (in the unlock case) is something other 64105327Sru * than SVN_ERR_FS_LOCK_OWNER_MISMATCH. 65105327Sru * 66202579Sru * Implements svn_ra_lock_callback_t. 67202579Sru */ 68202579Srustatic svn_error_t * 69105327Srustore_locks_callback(void *baton, 70105327Sru const char *rel_url, 71105327Sru svn_boolean_t do_lock, 7235951Sbde const svn_lock_t *lock, 7335951Sbde svn_error_t *ra_err, apr_pool_t *pool) 7435951Sbde{ 75124637Sru struct lock_baton *lb = baton; 76124637Sru svn_wc_notify_t *notify; 77124637Sru const char *local_abspath = lb->urls_to_paths 78124637Sru ? svn_hash_gets(lb->urls_to_paths, rel_url) 79124637Sru : NULL; 80124637Sru 8135951Sbde /* Create the notify struct first, so we can tweak it below. */ 82124435Sru notify = svn_wc_create_notify(local_abspath ? local_abspath : rel_url, 8335951Sbde do_lock 8435951Sbde ? (ra_err 85124637Sru ? svn_wc_notify_failed_lock 86124637Sru : svn_wc_notify_locked) 87124637Sru : (ra_err 8835951Sbde ? svn_wc_notify_failed_unlock 89124435Sru : svn_wc_notify_unlocked), 9035951Sbde pool); 9135951Sbde notify->lock = lock; 9235951Sbde notify->err = ra_err; 9335951Sbde 94124435Sru if (local_abspath) 9535951Sbde { 96124435Sru /* Notify a valid working copy path */ 97125119Sru notify->path_prefix = lb->base_dir_abspath; 98241790Smarcel 99241790Smarcel if (do_lock) 10035951Sbde { 10135951Sbde if (!ra_err && lock) 102124435Sru { 103125119Sru SVN_ERR(svn_wc_add_lock2(lb->ctx->wc_ctx, local_abspath, lock, 104124435Sru lb->pool)); 105241790Smarcel notify->lock_state = svn_wc_notify_lock_state_locked; 106241790Smarcel } 10735951Sbde else 108124435Sru notify->lock_state = svn_wc_notify_lock_state_unchanged; 109124435Sru } 11035951Sbde else /* unlocking */ 11135951Sbde { 11235951Sbde /* Remove our wc lock token either a) if we got no error, or b) if 11335951Sbde we got any error except for owner mismatch or hook failure (the 11435951Sbde hook would be pre-unlock rather than post-unlock). Note that the 115124637Sru only errors that are handed to this callback will be 116124637Sru locking-related errors. */ 117124637Sru 11835951Sbde if (!ra_err || 11935951Sbde (ra_err && (ra_err->apr_err != SVN_ERR_FS_LOCK_OWNER_MISMATCH 12035951Sbde && ra_err->apr_err != SVN_ERR_REPOS_HOOK_FAILURE))) 12135951Sbde { 1221845Swollman SVN_ERR(svn_wc_remove_lock2(lb->ctx->wc_ctx, local_abspath, 12330113Sjkh lb->pool)); 12495306Sru notify->lock_state = svn_wc_notify_lock_state_unlocked; 12514986Swosch } 126241298Smarcel else 127241298Smarcel notify->lock_state = svn_wc_notify_lock_state_unchanged; 128241298Smarcel } 12934181Sbde } 13034181Sbde else 131239613Sdim notify->url = svn_path_url_add_component2(lb->base_url, rel_url, pool); 132239613Sdim 133239613Sdim if (lb->ctx->notify_func2) 134239613Sdim lb->ctx->notify_func2(lb->ctx->notify_baton2, notify, pool); 135126890Strhodes 136124490Sru return SVN_NO_ERROR; 137124490Sru} 13814986Swosch 139125119Sru 14034181Sbde/* This is a wrapper around svn_uri_condense_targets() and 141126890Strhodes * svn_dirent_condense_targets() (the choice of which is made based on 14214986Swosch * the value of TARGETS_ARE_URIS) which takes care of the 143125119Sru * single-target special case. 144125119Sru * 14514986Swosch * Callers are expected to check for an empty *COMMON_PARENT (which 146126890Strhodes * means, "there was nothing common") for themselves. 14736673Sdt */ 14814986Swoschstatic svn_error_t * 14924750Sbdecondense_targets(const char **common_parent, 15094922Sru apr_array_header_t **target_relpaths, 15194841Sru const apr_array_header_t *targets, 15224750Sbde svn_boolean_t targets_are_uris, 15314986Swosch svn_boolean_t remove_redundancies, 15430113Sjkh apr_pool_t *result_pool, 1551845Swollman apr_pool_t *scratch_pool) 15695306Sru{ 1571845Swollman if (targets_are_uris) 1581845Swollman { 1591845Swollman SVN_ERR(svn_uri_condense_targets(common_parent, target_relpaths, 16030113Sjkh targets, remove_redundancies, 16130113Sjkh result_pool, scratch_pool)); 16230113Sjkh } 1631845Swollman else 1641845Swollman { 1651845Swollman SVN_ERR(svn_dirent_condense_targets(common_parent, target_relpaths, 1661845Swollman targets, remove_redundancies, 1671845Swollman result_pool, scratch_pool)); 1681845Swollman } 16916663Sjkh 17095306Sru /* svn_*_condense_targets leaves *TARGET_RELPATHS empty if TARGETS only 17128806Sbde had 1 member, so we special case that. */ 172202579Sru if (apr_is_empty_array(*target_relpaths)) 173105327Sru { 17424861Sjkh const char *base_name; 175105327Sru 17616663Sjkh if (targets_are_uris) 177202578Sru { 178202579Sru svn_uri_split(common_parent, &base_name, 17916663Sjkh *common_parent, result_pool); 18024861Sjkh } 181105327Sru else 18299344Sru { 18399344Sru svn_dirent_split(common_parent, &base_name, 184117195Sbde *common_parent, result_pool); 185117195Sbde } 18699344Sru APR_ARRAY_PUSH(*target_relpaths, const char *) = base_name; 187117195Sbde } 188117195Sbde 189117195Sbde return SVN_NO_ERROR; 190117195Sbde} 19199344Sru 192117195Sbde/* Lock info. Used in organize_lock_targets. 193 ### Maybe return this instead of the ugly hashes? */ 194struct wc_lock_item_t 195{ 196 svn_revnum_t revision; 197 const char *lock_token; 198 const char *url; 199}; 200 201/* 202 * Sets LOCK_PATHS to an array of working copy paths that this function 203 * has obtained lock on. The caller is responsible to release the locks 204 * EVEN WHEN THIS FUNCTION RETURNS AN ERROR. 205 * 206 * Set *COMMON_PARENT_URL to the nearest common parent URL of all TARGETS. 207 * If TARGETS are local paths, then the entry for each path is examined 208 * and *COMMON_PARENT is set to the common parent URL for all the 209 * targets (as opposed to the common local path). 210 * 211 * If there is no common parent, either because the targets are a 212 * mixture of URLs and local paths, or because they simply do not 213 * share a common parent, then return SVN_ERR_UNSUPPORTED_FEATURE. 214 * 215 * DO_LOCK is TRUE for locking TARGETS, and FALSE for unlocking them. 216 * FORCE is TRUE for breaking or stealing locks, and FALSE otherwise. 217 * 218 * Each key stored in *REL_TARGETS_P is a path relative to 219 * *COMMON_PARENT. If TARGETS are local paths, then: if DO_LOCK is 220 * true, the value is a pointer to the corresponding base_revision 221 * (allocated in POOL) for the path, else the value is the lock token 222 * (or "" if no token found in the wc). 223 * 224 * If TARGETS is an array of urls, REL_FS_PATHS_P is set to NULL. 225 * Otherwise each key in REL_FS_PATHS_P is an repository path (relative to 226 * COMMON_PARENT) mapped to the absolute path for TARGET. 227 * 228 * If *COMMON_PARENT is a URL, then the values are a pointer to 229 * SVN_INVALID_REVNUM (allocated in pool) if DO_LOCK, else "". 230 * 231 * TARGETS may not be empty. 232 */ 233static svn_error_t * 234organize_lock_targets(apr_array_header_t **lock_paths, 235 const char **common_parent_url, 236 const char **base_dir_abspath, 237 apr_hash_t **rel_targets_p, 238 apr_hash_t **rel_fs_paths_p, 239 const apr_array_header_t *targets, 240 svn_boolean_t do_lock, 241 svn_boolean_t force, 242 svn_wc_context_t *wc_ctx, 243 apr_pool_t *result_pool, 244 apr_pool_t *scratch_pool) 245{ 246 const char *common_url = NULL; 247 apr_hash_t *rel_targets_ret = apr_hash_make(result_pool); 248 apr_hash_t *rel_fs_paths = NULL; 249 apr_hash_t *wc_info = apr_hash_make(scratch_pool); 250 svn_boolean_t url_mode; 251 252 *lock_paths = NULL; 253 254 SVN_ERR_ASSERT(targets->nelts); 255 SVN_ERR(svn_client__assert_homogeneous_target_type(targets)); 256 257 url_mode = svn_path_is_url(APR_ARRAY_IDX(targets, 0, const char *)); 258 259 if (url_mode) 260 { 261 apr_array_header_t *rel_targets; 262 int i; 263 svn_revnum_t *invalid_revnum = 264 apr_palloc(result_pool, sizeof(*invalid_revnum)); 265 266 *invalid_revnum = SVN_INVALID_REVNUM; 267 268 /* Get the common parent URL and a bunch of relpaths, one per target. */ 269 SVN_ERR(condense_targets(&common_url, &rel_targets, targets, 270 TRUE, TRUE, result_pool, scratch_pool)); 271 if (! (common_url && *common_url)) 272 return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, 273 _("No common parent found, unable to operate " 274 "on disjoint arguments")); 275 276 /* Create mapping of the target relpaths to either 277 SVN_INVALID_REVNUM (if our caller is locking) or to an empty 278 lock token string (if the caller is unlocking). */ 279 for (i = 0; i < rel_targets->nelts; i++) 280 { 281 svn_hash_sets(rel_targets_ret, 282 APR_ARRAY_IDX(rel_targets, i, const char *), 283 do_lock 284 ? (const void *)invalid_revnum 285 : (const void *)""); 286 } 287 } 288 else 289 { 290 apr_array_header_t *rel_urls, *target_urls; 291 apr_hash_t *wcroot_target = apr_hash_make(scratch_pool); 292 apr_pool_t *iterpool = svn_pool_create(scratch_pool); 293 apr_hash_index_t *hi; 294 int i; 295 296 *lock_paths = apr_array_make(result_pool, 1, sizeof(const char *)); 297 298 for (i = 0; i < targets->nelts; i++) 299 { 300 const char *target_abspath; 301 const char *wcroot_abspath; 302 apr_array_header_t *wc_targets; 303 304 svn_pool_clear(iterpool); 305 306 SVN_ERR(svn_dirent_get_absolute(&target_abspath, 307 APR_ARRAY_IDX(targets, i, const char*), 308 result_pool)); 309 310 SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, wc_ctx, target_abspath, 311 iterpool, iterpool)); 312 313 wc_targets = svn_hash_gets(wcroot_target, wcroot_abspath); 314 315 if (!wc_targets) 316 { 317 wc_targets = apr_array_make(scratch_pool, 1, sizeof(const char*)); 318 svn_hash_sets(wcroot_target, apr_pstrdup(scratch_pool, wcroot_abspath), 319 wc_targets); 320 } 321 322 APR_ARRAY_PUSH(wc_targets, const char *) = target_abspath; 323 } 324 325 for (hi = apr_hash_first(scratch_pool, wcroot_target); 326 hi; 327 hi = apr_hash_next(hi)) 328 { 329 const char *lock_abspath; 330 apr_array_header_t *paths = apr_hash_this_val(hi); 331 332 /* Use parent dir of a single file target */ 333 if (paths->nelts == 1) 334 lock_abspath = svn_dirent_dirname( 335 APR_ARRAY_IDX(paths, 0, const char *), 336 result_pool); 337 else 338 SVN_ERR(svn_dirent_condense_targets(&lock_abspath, NULL, paths, 339 FALSE, result_pool, 340 scratch_pool)); 341 342 SVN_ERR(svn_wc__acquire_write_lock(&lock_abspath, 343 wc_ctx, lock_abspath, FALSE, 344 result_pool, scratch_pool)); 345 346 APR_ARRAY_PUSH(*lock_paths, const char *) = lock_abspath; 347 } 348 349 /* Get the URL for each target (which also serves to verify that 350 the dirent targets are sane). */ 351 target_urls = apr_array_make(scratch_pool, targets->nelts, 352 sizeof(const char *)); 353 for (hi = apr_hash_first(scratch_pool, wcroot_target); 354 hi; 355 hi = apr_hash_next(hi)) 356 { 357 apr_array_header_t *wc_targets = apr_hash_this_val(hi); 358 359 for (i = 0; i < wc_targets->nelts; i++) 360 { 361 const char *repos_relpath; 362 const char *repos_root_url; 363 struct wc_lock_item_t *wli; 364 const char *local_abspath; 365 svn_node_kind_t kind; 366 367 svn_pool_clear(iterpool); 368 369 local_abspath = APR_ARRAY_IDX(wc_targets, i, const char *); 370 wli = apr_pcalloc(scratch_pool, sizeof(*wli)); 371 372 SVN_ERR(svn_wc__node_get_base(&kind, &wli->revision, 373 &repos_relpath, 374 &repos_root_url, NULL, 375 &wli->lock_token, 376 wc_ctx, local_abspath, 377 FALSE /* ignore_enoent */, 378 result_pool, iterpool)); 379 380 if (kind != svn_node_file) 381 return svn_error_createf(SVN_ERR_WC_NOT_FILE, NULL, 382 _("The node '%s' is not a file"), 383 svn_dirent_local_style(local_abspath, 384 iterpool)); 385 386 wli->url = svn_path_url_add_component2(repos_root_url, 387 repos_relpath, 388 scratch_pool); 389 svn_hash_sets(wc_info, local_abspath, wli); 390 391 APR_ARRAY_PUSH(target_urls, const char *) = wli->url; 392 } 393 } 394 395 /* Now that we have a bunch of URLs for our dirent targets, 396 condense those into a single common parent URL and a bunch of 397 paths relative to that. */ 398 SVN_ERR(condense_targets(&common_url, &rel_urls, target_urls, 399 TRUE, FALSE, result_pool, scratch_pool)); 400 if (! (common_url && *common_url)) 401 return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, 402 _("Unable to lock/unlock across multiple " 403 "repositories")); 404 405 /* Now we need to create our mapping. */ 406 rel_fs_paths = apr_hash_make(result_pool); 407 408 for (hi = apr_hash_first(scratch_pool, wc_info); 409 hi; 410 hi = apr_hash_next(hi)) 411 { 412 const char *local_abspath = apr_hash_this_key(hi); 413 struct wc_lock_item_t *wli = apr_hash_this_val(hi); 414 const char *rel_url; 415 416 svn_pool_clear(iterpool); 417 418 rel_url = svn_uri_skip_ancestor(common_url, wli->url, result_pool); 419 420 svn_hash_sets(rel_fs_paths, rel_url, 421 apr_pstrdup(result_pool, local_abspath)); 422 423 if (do_lock) /* Lock. */ 424 { 425 svn_revnum_t *revnum; 426 revnum = apr_palloc(result_pool, sizeof(* revnum)); 427 428 *revnum = wli->revision; 429 430 svn_hash_sets(rel_targets_ret, rel_url, revnum); 431 } 432 else /* Unlock. */ 433 { 434 const char *lock_token; 435 436 /* If not forcing the unlock, get the lock token. */ 437 if (! force) 438 { 439 if (! wli->lock_token) 440 return svn_error_createf( 441 SVN_ERR_CLIENT_MISSING_LOCK_TOKEN, NULL, 442 _("'%s' is not locked in this working copy"), 443 svn_dirent_local_style(local_abspath, 444 scratch_pool)); 445 446 lock_token = wli->lock_token 447 ? apr_pstrdup(result_pool, wli->lock_token) 448 : NULL; 449 } 450 else 451 lock_token = NULL; 452 453 /* If breaking a lock, we shouldn't pass any lock token. */ 454 svn_hash_sets(rel_targets_ret, rel_url, 455 lock_token ? lock_token : ""); 456 } 457 } 458 459 svn_pool_destroy(iterpool); 460 } 461 462 /* Set our return variables. */ 463 *common_parent_url = common_url; 464 if (*lock_paths && (*lock_paths)->nelts == 1) 465 *base_dir_abspath = APR_ARRAY_IDX(*lock_paths, 0, const char*); 466 else 467 *base_dir_abspath = NULL; 468 *rel_targets_p = rel_targets_ret; 469 *rel_fs_paths_p = rel_fs_paths; 470 471 return SVN_NO_ERROR; 472} 473 474/* Fetch lock tokens from the repository for the paths in PATH_TOKENS, 475 setting the values to the fetched tokens, allocated in pool. */ 476static svn_error_t * 477fetch_tokens(svn_ra_session_t *ra_session, apr_hash_t *path_tokens, 478 apr_pool_t *pool) 479{ 480 apr_hash_index_t *hi; 481 apr_pool_t *iterpool = svn_pool_create(pool); 482 483 for (hi = apr_hash_first(pool, path_tokens); hi; hi = apr_hash_next(hi)) 484 { 485 const char *path = apr_hash_this_key(hi); 486 svn_lock_t *lock; 487 488 svn_pool_clear(iterpool); 489 490 SVN_ERR(svn_ra_get_lock(ra_session, &lock, path, iterpool)); 491 492 if (! lock) 493 return svn_error_createf 494 (SVN_ERR_CLIENT_MISSING_LOCK_TOKEN, NULL, 495 _("'%s' is not locked"), path); 496 497 svn_hash_sets(path_tokens, path, apr_pstrdup(pool, lock->token)); 498 } 499 500 svn_pool_destroy(iterpool); 501 return SVN_NO_ERROR; 502} 503 504 505svn_error_t * 506svn_client_lock(const apr_array_header_t *targets, 507 const char *comment, 508 svn_boolean_t steal_lock, 509 svn_client_ctx_t *ctx, 510 apr_pool_t *pool) 511{ 512 const char *base_dir_abspath = NULL; 513 const char *common_parent_url; 514 svn_ra_session_t *ra_session; 515 apr_hash_t *path_revs, *urls_to_paths; 516 struct lock_baton cb; 517 apr_array_header_t *lock_abspaths; 518 svn_error_t *err; 519 520 if (apr_is_empty_array(targets)) 521 return SVN_NO_ERROR; 522 523 /* Enforce that the comment be xml-escapable. */ 524 if (comment) 525 { 526 if (! svn_xml_is_xml_safe(comment, strlen(comment))) 527 return svn_error_create 528 (SVN_ERR_XML_UNESCAPABLE_DATA, NULL, 529 _("Lock comment contains illegal characters")); 530 } 531 532 err = organize_lock_targets(&lock_abspaths, &common_parent_url, 533 &base_dir_abspath, &path_revs, 534 &urls_to_paths, 535 targets, TRUE, steal_lock, 536 ctx->wc_ctx, pool, pool); 537 538 if (err) 539 goto release_locks; 540 541 /* Open an RA session to the common parent URL of TARGETS. */ 542 err = svn_client_open_ra_session2(&ra_session, common_parent_url, 543 base_dir_abspath, ctx, pool, pool); 544 545 if (err) 546 goto release_locks; 547 548 cb.base_dir_abspath = base_dir_abspath; 549 cb.base_url = common_parent_url; 550 cb.urls_to_paths = urls_to_paths; 551 cb.ctx = ctx; 552 cb.pool = pool; 553 554 /* Lock the paths. */ 555 err = svn_ra_lock(ra_session, path_revs, comment, 556 steal_lock, store_locks_callback, &cb, pool); 557 558release_locks: 559 if (lock_abspaths) 560 { 561 int i; 562 563 for (i = 0; i < lock_abspaths->nelts; i++) 564 { 565 err = svn_error_compose_create( 566 err, 567 svn_wc__release_write_lock(ctx->wc_ctx, 568 APR_ARRAY_IDX(lock_abspaths, i, 569 const char *), 570 pool)); 571 } 572 } 573 574 return svn_error_trace(err); 575} 576 577svn_error_t * 578svn_client_unlock(const apr_array_header_t *targets, 579 svn_boolean_t break_lock, 580 svn_client_ctx_t *ctx, 581 apr_pool_t *pool) 582{ 583 const char *base_dir_abspath = NULL; 584 const char *common_parent_url; 585 svn_ra_session_t *ra_session; 586 apr_hash_t *path_tokens, *urls_to_paths; 587 apr_array_header_t *lock_abspaths; 588 struct lock_baton cb; 589 svn_error_t *err; 590 591 if (apr_is_empty_array(targets)) 592 return SVN_NO_ERROR; 593 594 err = organize_lock_targets(&lock_abspaths, &common_parent_url, 595 &base_dir_abspath, &path_tokens, &urls_to_paths, 596 targets, FALSE, break_lock, 597 ctx->wc_ctx, pool, pool); 598 599 if (err) 600 goto release_locks; 601 602 /* Open an RA session to the common parent URL of TARGETS. */ 603 err = svn_client_open_ra_session2(&ra_session, common_parent_url, 604 base_dir_abspath, ctx, pool, pool); 605 606 if (err) 607 goto release_locks; 608 609 /* If break_lock is not set, lock tokens are required by the server. 610 If the targets were all URLs, ensure that we provide lock tokens, 611 so the repository will only check that the user owns the 612 locks. */ 613 if (! lock_abspaths && !break_lock) 614 { 615 err = fetch_tokens(ra_session, path_tokens, pool); 616 617 if (err) 618 goto release_locks; 619 } 620 621 cb.base_dir_abspath = base_dir_abspath; 622 cb.base_url = common_parent_url; 623 cb.urls_to_paths = urls_to_paths; 624 cb.ctx = ctx; 625 cb.pool = pool; 626 627 /* Unlock the paths. */ 628 err = svn_ra_unlock(ra_session, path_tokens, break_lock, 629 store_locks_callback, &cb, pool); 630 631release_locks: 632 if (lock_abspaths) 633 { 634 int i; 635 636 for (i = 0; i < lock_abspaths->nelts; i++) 637 { 638 err = svn_error_compose_create( 639 err, 640 svn_wc__release_write_lock(ctx->wc_ctx, 641 APR_ARRAY_IDX(lock_abspaths, i, 642 const char *), 643 pool)); 644 } 645 } 646 647 return svn_error_trace(err); 648} 649 650