commit.c revision 299742
1/* commit.c --- editor for committing changes to a filesystem. 2 * 3 * ==================================================================== 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 * ==================================================================== 21 */ 22 23 24#include <string.h> 25 26#include <apr_pools.h> 27#include <apr_file_io.h> 28 29#include "svn_hash.h" 30#include "svn_compat.h" 31#include "svn_pools.h" 32#include "svn_error.h" 33#include "svn_dirent_uri.h" 34#include "svn_path.h" 35#include "svn_delta.h" 36#include "svn_fs.h" 37#include "svn_repos.h" 38#include "svn_checksum.h" 39#include "svn_ctype.h" 40#include "svn_props.h" 41#include "svn_mergeinfo.h" 42#include "svn_private_config.h" 43 44#include "repos.h" 45 46#include "private/svn_fspath.h" 47#include "private/svn_fs_private.h" 48#include "private/svn_repos_private.h" 49#include "private/svn_editor.h" 50 51 52 53/*** Editor batons. ***/ 54 55struct edit_baton 56{ 57 apr_pool_t *pool; 58 59 /** Supplied when the editor is created: **/ 60 61 /* Revision properties to set for this commit. */ 62 apr_hash_t *revprop_table; 63 64 /* Callback to run when the commit is done. */ 65 svn_commit_callback2_t commit_callback; 66 void *commit_callback_baton; 67 68 /* Callback to check authorizations on paths. */ 69 svn_repos_authz_callback_t authz_callback; 70 void *authz_baton; 71 72 /* The already-open svn repository to commit to. */ 73 svn_repos_t *repos; 74 75 /* URL to the root of the open repository. */ 76 const char *repos_url_decoded; 77 78 /* The name of the repository (here for convenience). */ 79 const char *repos_name; 80 81 /* The filesystem associated with the REPOS above (here for 82 convenience). */ 83 svn_fs_t *fs; 84 85 /* Location in fs where the edit will begin. */ 86 const char *base_path; 87 88 /* Does this set of interfaces 'own' the commit transaction? */ 89 svn_boolean_t txn_owner; 90 91 /* svn transaction associated with this edit (created in 92 open_root, or supplied by the public API caller). */ 93 svn_fs_txn_t *txn; 94 95 /** Filled in during open_root: **/ 96 97 /* The name of the transaction. */ 98 const char *txn_name; 99 100 /* The object representing the root directory of the svn txn. */ 101 svn_fs_root_t *txn_root; 102 103 /* Avoid aborting an fs transaction more than once */ 104 svn_boolean_t txn_aborted; 105 106 /** Filled in when the edit is closed: **/ 107 108 /* The new revision created by this commit. */ 109 svn_revnum_t *new_rev; 110 111 /* The date (according to the repository) of this commit. */ 112 const char **committed_date; 113 114 /* The author (also according to the repository) of this commit. */ 115 const char **committed_author; 116}; 117 118 119struct dir_baton 120{ 121 struct edit_baton *edit_baton; 122 struct dir_baton *parent; 123 const char *path; /* the -absolute- path to this dir in the fs */ 124 svn_revnum_t base_rev; /* the revision I'm based on */ 125 svn_boolean_t was_copied; /* was this directory added with history? */ 126 apr_pool_t *pool; /* my personal pool, in which I am allocated. */ 127}; 128 129 130struct file_baton 131{ 132 struct edit_baton *edit_baton; 133 const char *path; /* the -absolute- path to this file in the fs */ 134}; 135 136 137struct ev2_baton 138{ 139 /* The repository we are editing. */ 140 svn_repos_t *repos; 141 142 /* The authz baton for checks; NULL to skip authz. */ 143 svn_authz_t *authz; 144 145 /* The repository name and user for performing authz checks. */ 146 const char *authz_repos_name; 147 const char *authz_user; 148 149 /* Callback to provide info about the committed revision. */ 150 svn_commit_callback2_t commit_cb; 151 void *commit_baton; 152 153 /* The FS txn editor */ 154 svn_editor_t *inner; 155 156 /* The name of the open transaction (so we know what to commit) */ 157 const char *txn_name; 158}; 159 160 161/* Create and return a generic out-of-dateness error. */ 162static svn_error_t * 163out_of_date(const char *path, svn_node_kind_t kind) 164{ 165 return svn_error_createf(SVN_ERR_FS_TXN_OUT_OF_DATE, NULL, 166 (kind == svn_node_dir 167 ? _("Directory '%s' is out of date") 168 : kind == svn_node_file 169 ? _("File '%s' is out of date") 170 : _("'%s' is out of date")), 171 path); 172} 173 174 175static svn_error_t * 176invoke_commit_cb(svn_commit_callback2_t commit_cb, 177 void *commit_baton, 178 svn_fs_t *fs, 179 svn_revnum_t revision, 180 const char *post_commit_errstr, 181 apr_pool_t *scratch_pool) 182{ 183 /* FS interface returns non-const values. */ 184 /* const */ svn_string_t *date; 185 /* const */ svn_string_t *author; 186 svn_commit_info_t *commit_info; 187 188 if (commit_cb == NULL) 189 return SVN_NO_ERROR; 190 191 SVN_ERR(svn_fs_revision_prop(&date, fs, revision, SVN_PROP_REVISION_DATE, 192 scratch_pool)); 193 SVN_ERR(svn_fs_revision_prop(&author, fs, revision, 194 SVN_PROP_REVISION_AUTHOR, 195 scratch_pool)); 196 197 commit_info = svn_create_commit_info(scratch_pool); 198 199 /* fill up the svn_commit_info structure */ 200 commit_info->revision = revision; 201 commit_info->date = date ? date->data : NULL; 202 commit_info->author = author ? author->data : NULL; 203 commit_info->post_commit_err = post_commit_errstr; 204 /* commit_info->repos_root is not set by the repos layer, only by RA layers */ 205 206 return svn_error_trace(commit_cb(commit_info, commit_baton, scratch_pool)); 207} 208 209 210 211/* If EDITOR_BATON contains a valid authz callback, verify that the 212 REQUIRED access to PATH in ROOT is authorized. Return an error 213 appropriate for throwing out of the commit editor with SVN_ERR. If 214 no authz callback is present in EDITOR_BATON, then authorize all 215 paths. Use POOL for temporary allocation only. */ 216static svn_error_t * 217check_authz(struct edit_baton *editor_baton, const char *path, 218 svn_fs_root_t *root, svn_repos_authz_access_t required, 219 apr_pool_t *pool) 220{ 221 if (editor_baton->authz_callback) 222 { 223 svn_boolean_t allowed; 224 225 SVN_ERR(editor_baton->authz_callback(required, &allowed, root, path, 226 editor_baton->authz_baton, pool)); 227 if (!allowed) 228 return svn_error_create(required & svn_authz_write ? 229 SVN_ERR_AUTHZ_UNWRITABLE : 230 SVN_ERR_AUTHZ_UNREADABLE, 231 NULL, "Access denied"); 232 } 233 234 return SVN_NO_ERROR; 235} 236 237 238/* Return a directory baton allocated in POOL which represents 239 FULL_PATH, which is the immediate directory child of the directory 240 represented by PARENT_BATON. EDIT_BATON is the commit editor 241 baton. WAS_COPIED reveals whether or not this directory is the 242 result of a copy operation. BASE_REVISION is the base revision of 243 the directory. */ 244static struct dir_baton * 245make_dir_baton(struct edit_baton *edit_baton, 246 struct dir_baton *parent_baton, 247 const char *full_path, 248 svn_boolean_t was_copied, 249 svn_revnum_t base_revision, 250 apr_pool_t *pool) 251{ 252 struct dir_baton *db; 253 db = apr_pcalloc(pool, sizeof(*db)); 254 db->edit_baton = edit_baton; 255 db->parent = parent_baton; 256 db->pool = pool; 257 db->path = full_path; 258 db->was_copied = was_copied; 259 db->base_rev = base_revision; 260 return db; 261} 262 263/* This function is the shared guts of add_file() and add_directory(), 264 which see for the meanings of the parameters. The only extra 265 parameter here is IS_DIR, which is TRUE when adding a directory, 266 and FALSE when adding a file. 267 268 COPY_PATH must be a full URL, not a relative path. */ 269static svn_error_t * 270add_file_or_directory(const char *path, 271 void *parent_baton, 272 const char *copy_path, 273 svn_revnum_t copy_revision, 274 svn_boolean_t is_dir, 275 apr_pool_t *pool, 276 void **return_baton) 277{ 278 struct dir_baton *pb = parent_baton; 279 struct edit_baton *eb = pb->edit_baton; 280 apr_pool_t *subpool = svn_pool_create(pool); 281 svn_boolean_t was_copied = FALSE; 282 const char *full_path; 283 284 /* Reject paths which contain control characters (related to issue #4340). */ 285 SVN_ERR(svn_path_check_valid(path, pool)); 286 287 full_path = svn_fspath__join(eb->base_path, 288 svn_relpath_canonicalize(path, pool), pool); 289 290 /* Sanity check. */ 291 if (copy_path && (! SVN_IS_VALID_REVNUM(copy_revision))) 292 return svn_error_createf 293 (SVN_ERR_FS_GENERAL, NULL, 294 _("Got source path but no source revision for '%s'"), full_path); 295 296 if (copy_path) 297 { 298 const char *fs_path; 299 svn_fs_root_t *copy_root; 300 svn_node_kind_t kind; 301 size_t repos_url_len; 302 svn_repos_authz_access_t required; 303 304 /* Copy requires recursive write access to the destination path 305 and write access to the parent path. */ 306 required = svn_authz_write | (is_dir ? svn_authz_recursive : 0); 307 SVN_ERR(check_authz(eb, full_path, eb->txn_root, 308 required, subpool)); 309 SVN_ERR(check_authz(eb, pb->path, eb->txn_root, 310 svn_authz_write, subpool)); 311 312 /* Check PATH in our transaction. Make sure it does not exist 313 unless its parent directory was copied (in which case, the 314 thing might have been copied in as well), else return an 315 out-of-dateness error. */ 316 SVN_ERR(svn_fs_check_path(&kind, eb->txn_root, full_path, subpool)); 317 if ((kind != svn_node_none) && (! pb->was_copied)) 318 return svn_error_trace(out_of_date(full_path, kind)); 319 320 /* For now, require that the url come from the same repository 321 that this commit is operating on. */ 322 copy_path = svn_path_uri_decode(copy_path, subpool); 323 repos_url_len = strlen(eb->repos_url_decoded); 324 if (strncmp(copy_path, eb->repos_url_decoded, repos_url_len) != 0) 325 return svn_error_createf 326 (SVN_ERR_FS_GENERAL, NULL, 327 _("Source url '%s' is from different repository"), copy_path); 328 329 fs_path = apr_pstrdup(subpool, copy_path + repos_url_len); 330 331 /* Now use the "fs_path" as an absolute path within the 332 repository to make the copy from. */ 333 SVN_ERR(svn_fs_revision_root(©_root, eb->fs, 334 copy_revision, subpool)); 335 336 /* Copy also requires (recursive) read access to the source */ 337 required = svn_authz_read | (is_dir ? svn_authz_recursive : 0); 338 SVN_ERR(check_authz(eb, fs_path, copy_root, required, subpool)); 339 340 SVN_ERR(svn_fs_copy(copy_root, fs_path, 341 eb->txn_root, full_path, subpool)); 342 was_copied = TRUE; 343 } 344 else 345 { 346 /* No ancestry given, just make a new directory or empty file. 347 Note that we don't perform an existence check here like the 348 copy-from case does -- that's because svn_fs_make_*() 349 already errors out if the file already exists. Verify write 350 access to the full path and to the parent. */ 351 SVN_ERR(check_authz(eb, full_path, eb->txn_root, 352 svn_authz_write, subpool)); 353 SVN_ERR(check_authz(eb, pb->path, eb->txn_root, 354 svn_authz_write, subpool)); 355 if (is_dir) 356 SVN_ERR(svn_fs_make_dir(eb->txn_root, full_path, subpool)); 357 else 358 SVN_ERR(svn_fs_make_file(eb->txn_root, full_path, subpool)); 359 } 360 361 /* Cleanup our temporary subpool. */ 362 svn_pool_destroy(subpool); 363 364 /* Build a new child baton. */ 365 if (is_dir) 366 { 367 *return_baton = make_dir_baton(eb, pb, full_path, was_copied, 368 SVN_INVALID_REVNUM, pool); 369 } 370 else 371 { 372 struct file_baton *new_fb = apr_pcalloc(pool, sizeof(*new_fb)); 373 new_fb->edit_baton = eb; 374 new_fb->path = full_path; 375 *return_baton = new_fb; 376 } 377 378 return SVN_NO_ERROR; 379} 380 381 382 383/*** Editor functions ***/ 384 385static svn_error_t * 386open_root(void *edit_baton, 387 svn_revnum_t base_revision, 388 apr_pool_t *pool, 389 void **root_baton) 390{ 391 struct dir_baton *dirb; 392 struct edit_baton *eb = edit_baton; 393 svn_revnum_t youngest; 394 395 /* Ignore BASE_REVISION. We always build our transaction against 396 HEAD. However, we will keep it in our dir baton for out of 397 dateness checks. */ 398 SVN_ERR(svn_fs_youngest_rev(&youngest, eb->fs, eb->pool)); 399 400 if (base_revision > youngest) 401 return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, 402 _("No such revision %ld (HEAD is %ld)"), 403 base_revision, youngest); 404 405 /* Unless we've been instructed to use a specific transaction, we'll 406 make our own. */ 407 if (eb->txn_owner) 408 { 409 SVN_ERR(svn_repos_fs_begin_txn_for_commit2(&(eb->txn), 410 eb->repos, 411 youngest, 412 eb->revprop_table, 413 eb->pool)); 414 } 415 else /* Even if we aren't the owner of the transaction, we might 416 have been instructed to set some properties. */ 417 { 418 apr_array_header_t *props = svn_prop_hash_to_array(eb->revprop_table, 419 pool); 420 SVN_ERR(svn_repos_fs_change_txn_props(eb->txn, props, pool)); 421 } 422 SVN_ERR(svn_fs_txn_name(&(eb->txn_name), eb->txn, eb->pool)); 423 SVN_ERR(svn_fs_txn_root(&(eb->txn_root), eb->txn, eb->pool)); 424 425 /* Create a root dir baton. The `base_path' field is an -absolute- 426 path in the filesystem, upon which all further editor paths are 427 based. */ 428 dirb = apr_pcalloc(pool, sizeof(*dirb)); 429 dirb->edit_baton = edit_baton; 430 dirb->parent = NULL; 431 dirb->pool = pool; 432 dirb->was_copied = FALSE; 433 dirb->path = apr_pstrdup(pool, eb->base_path); 434 dirb->base_rev = base_revision; 435 436 *root_baton = dirb; 437 return SVN_NO_ERROR; 438} 439 440 441 442static svn_error_t * 443delete_entry(const char *path, 444 svn_revnum_t revision, 445 void *parent_baton, 446 apr_pool_t *pool) 447{ 448 struct dir_baton *parent = parent_baton; 449 struct edit_baton *eb = parent->edit_baton; 450 svn_node_kind_t kind; 451 svn_revnum_t cr_rev; 452 svn_repos_authz_access_t required = svn_authz_write; 453 const char *full_path; 454 455 full_path = svn_fspath__join(eb->base_path, 456 svn_relpath_canonicalize(path, pool), pool); 457 458 /* Check PATH in our transaction. */ 459 SVN_ERR(svn_fs_check_path(&kind, eb->txn_root, full_path, pool)); 460 461 /* Deletion requires a recursive write access, as well as write 462 access to the parent directory. */ 463 if (kind == svn_node_dir) 464 required |= svn_authz_recursive; 465 SVN_ERR(check_authz(eb, full_path, eb->txn_root, 466 required, pool)); 467 SVN_ERR(check_authz(eb, parent->path, eb->txn_root, 468 svn_authz_write, pool)); 469 470 /* If PATH doesn't exist in the txn, the working copy is out of date. */ 471 if (kind == svn_node_none) 472 return svn_error_trace(out_of_date(full_path, kind)); 473 474 /* Now, make sure we're deleting the node we *think* we're 475 deleting, else return an out-of-dateness error. */ 476 SVN_ERR(svn_fs_node_created_rev(&cr_rev, eb->txn_root, full_path, pool)); 477 if (SVN_IS_VALID_REVNUM(revision) && (revision < cr_rev)) 478 return svn_error_trace(out_of_date(full_path, kind)); 479 480 /* This routine is a mindless wrapper. We call svn_fs_delete() 481 because that will delete files and recursively delete 482 directories. */ 483 return svn_fs_delete(eb->txn_root, full_path, pool); 484} 485 486 487static svn_error_t * 488add_directory(const char *path, 489 void *parent_baton, 490 const char *copy_path, 491 svn_revnum_t copy_revision, 492 apr_pool_t *pool, 493 void **child_baton) 494{ 495 return add_file_or_directory(path, parent_baton, copy_path, copy_revision, 496 TRUE /* is_dir */, pool, child_baton); 497} 498 499 500static svn_error_t * 501open_directory(const char *path, 502 void *parent_baton, 503 svn_revnum_t base_revision, 504 apr_pool_t *pool, 505 void **child_baton) 506{ 507 struct dir_baton *pb = parent_baton; 508 struct edit_baton *eb = pb->edit_baton; 509 svn_node_kind_t kind; 510 const char *full_path; 511 512 full_path = svn_fspath__join(eb->base_path, 513 svn_relpath_canonicalize(path, pool), pool); 514 515 /* Check PATH in our transaction. If it does not exist, 516 return a 'Path not present' error. */ 517 SVN_ERR(svn_fs_check_path(&kind, eb->txn_root, full_path, pool)); 518 if (kind == svn_node_none) 519 return svn_error_createf(SVN_ERR_FS_NOT_DIRECTORY, NULL, 520 _("Path '%s' not present"), 521 path); 522 523 /* Build a new dir baton for this directory. */ 524 *child_baton = make_dir_baton(eb, pb, full_path, pb->was_copied, 525 base_revision, pool); 526 return SVN_NO_ERROR; 527} 528 529 530static svn_error_t * 531apply_textdelta(void *file_baton, 532 const char *base_checksum, 533 apr_pool_t *pool, 534 svn_txdelta_window_handler_t *handler, 535 void **handler_baton) 536{ 537 struct file_baton *fb = file_baton; 538 539 /* Check for write authorization. */ 540 SVN_ERR(check_authz(fb->edit_baton, fb->path, 541 fb->edit_baton->txn_root, 542 svn_authz_write, pool)); 543 544 return svn_fs_apply_textdelta(handler, handler_baton, 545 fb->edit_baton->txn_root, 546 fb->path, 547 base_checksum, 548 NULL, 549 pool); 550} 551 552 553static svn_error_t * 554add_file(const char *path, 555 void *parent_baton, 556 const char *copy_path, 557 svn_revnum_t copy_revision, 558 apr_pool_t *pool, 559 void **file_baton) 560{ 561 return add_file_or_directory(path, parent_baton, copy_path, copy_revision, 562 FALSE /* is_dir */, pool, file_baton); 563} 564 565 566static svn_error_t * 567open_file(const char *path, 568 void *parent_baton, 569 svn_revnum_t base_revision, 570 apr_pool_t *pool, 571 void **file_baton) 572{ 573 struct file_baton *new_fb; 574 struct dir_baton *pb = parent_baton; 575 struct edit_baton *eb = pb->edit_baton; 576 svn_revnum_t cr_rev; 577 apr_pool_t *subpool = svn_pool_create(pool); 578 const char *full_path; 579 580 full_path = svn_fspath__join(eb->base_path, 581 svn_relpath_canonicalize(path, pool), pool); 582 583 /* Check for read authorization. */ 584 SVN_ERR(check_authz(eb, full_path, eb->txn_root, 585 svn_authz_read, subpool)); 586 587 /* Get this node's creation revision (doubles as an existence check). */ 588 SVN_ERR(svn_fs_node_created_rev(&cr_rev, eb->txn_root, full_path, 589 subpool)); 590 591 /* If the node our caller has is an older revision number than the 592 one in our transaction, return an out-of-dateness error. */ 593 if (SVN_IS_VALID_REVNUM(base_revision) && (base_revision < cr_rev)) 594 return svn_error_trace(out_of_date(full_path, svn_node_file)); 595 596 /* Build a new file baton */ 597 new_fb = apr_pcalloc(pool, sizeof(*new_fb)); 598 new_fb->edit_baton = eb; 599 new_fb->path = full_path; 600 601 *file_baton = new_fb; 602 603 /* Destory the work subpool. */ 604 svn_pool_destroy(subpool); 605 606 return SVN_NO_ERROR; 607} 608 609 610static svn_error_t * 611change_file_prop(void *file_baton, 612 const char *name, 613 const svn_string_t *value, 614 apr_pool_t *pool) 615{ 616 struct file_baton *fb = file_baton; 617 struct edit_baton *eb = fb->edit_baton; 618 619 /* Check for write authorization. */ 620 SVN_ERR(check_authz(eb, fb->path, eb->txn_root, 621 svn_authz_write, pool)); 622 623 return svn_repos_fs_change_node_prop(eb->txn_root, fb->path, 624 name, value, pool); 625} 626 627 628static svn_error_t * 629close_file(void *file_baton, 630 const char *text_digest, 631 apr_pool_t *pool) 632{ 633 struct file_baton *fb = file_baton; 634 635 if (text_digest) 636 { 637 svn_checksum_t *checksum; 638 svn_checksum_t *text_checksum; 639 640 SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, 641 fb->edit_baton->txn_root, fb->path, 642 TRUE, pool)); 643 SVN_ERR(svn_checksum_parse_hex(&text_checksum, svn_checksum_md5, 644 text_digest, pool)); 645 646 if (!svn_checksum_match(text_checksum, checksum)) 647 return svn_checksum_mismatch_err(text_checksum, checksum, pool, 648 _("Checksum mismatch for resulting fulltext\n(%s)"), 649 fb->path); 650 } 651 652 return SVN_NO_ERROR; 653} 654 655 656static svn_error_t * 657change_dir_prop(void *dir_baton, 658 const char *name, 659 const svn_string_t *value, 660 apr_pool_t *pool) 661{ 662 struct dir_baton *db = dir_baton; 663 struct edit_baton *eb = db->edit_baton; 664 665 /* Check for write authorization. */ 666 SVN_ERR(check_authz(eb, db->path, eb->txn_root, 667 svn_authz_write, pool)); 668 669 if (SVN_IS_VALID_REVNUM(db->base_rev)) 670 { 671 /* Subversion rule: propchanges can only happen on a directory 672 which is up-to-date. */ 673 svn_revnum_t created_rev; 674 SVN_ERR(svn_fs_node_created_rev(&created_rev, 675 eb->txn_root, db->path, pool)); 676 677 if (db->base_rev < created_rev) 678 return svn_error_trace(out_of_date(db->path, svn_node_dir)); 679 } 680 681 return svn_repos_fs_change_node_prop(eb->txn_root, db->path, 682 name, value, pool); 683} 684 685const char * 686svn_repos__post_commit_error_str(svn_error_t *err, 687 apr_pool_t *pool) 688{ 689 svn_error_t *hook_err1, *hook_err2; 690 const char *msg; 691 692 if (! err) 693 return _("(no error)"); 694 695 err = svn_error_purge_tracing(err); 696 697 /* hook_err1 is the SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED wrapped 698 error from the post-commit script, if any, and hook_err2 should 699 be the original error, but be defensive and handle a case where 700 SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED doesn't wrap an error. */ 701 hook_err1 = svn_error_find_cause(err, SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED); 702 if (hook_err1 && hook_err1->child) 703 hook_err2 = hook_err1->child; 704 else 705 hook_err2 = hook_err1; 706 707 /* This implementation counts on svn_repos_fs_commit_txn() and 708 libsvn_repos/commit.c:complete_cb() returning 709 svn_fs_commit_txn() as the parent error with a child 710 SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED error. If the parent error 711 is SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED then there was no error 712 in svn_fs_commit_txn(). 713 714 The post-commit hook error message is already self describing, so 715 it can be dropped into an error message without any additional 716 text. */ 717 if (hook_err1) 718 { 719 if (err == hook_err1) 720 { 721 if (hook_err2->message) 722 msg = apr_pstrdup(pool, hook_err2->message); 723 else 724 msg = _("post-commit hook failed with no error message."); 725 } 726 else 727 { 728 msg = hook_err2->message 729 ? apr_pstrdup(pool, hook_err2->message) 730 : _("post-commit hook failed with no error message."); 731 msg = apr_psprintf( 732 pool, 733 _("post commit FS processing had error:\n%s\n%s"), 734 err->message ? err->message : _("(no error message)"), 735 msg); 736 } 737 } 738 else 739 { 740 msg = apr_psprintf(pool, 741 _("post commit FS processing had error:\n%s"), 742 err->message ? err->message 743 : _("(no error message)")); 744 } 745 746 return msg; 747} 748 749static svn_error_t * 750close_edit(void *edit_baton, 751 apr_pool_t *pool) 752{ 753 struct edit_baton *eb = edit_baton; 754 svn_revnum_t new_revision = SVN_INVALID_REVNUM; 755 svn_error_t *err; 756 const char *conflict; 757 const char *post_commit_err = NULL; 758 759 /* If no transaction has been created (ie. if open_root wasn't 760 called before close_edit), abort the operation here with an 761 error. */ 762 if (! eb->txn) 763 return svn_error_create(SVN_ERR_REPOS_BAD_ARGS, NULL, 764 "No valid transaction supplied to close_edit"); 765 766 /* Commit. */ 767 err = svn_repos_fs_commit_txn(&conflict, eb->repos, 768 &new_revision, eb->txn, pool); 769 770 if (SVN_IS_VALID_REVNUM(new_revision)) 771 { 772 /* The actual commit succeeded, i.e. the transaction does no longer 773 exist and we can't use txn_root for conflict resolution etc. 774 775 Since close_edit is supposed to release resources, do it now. */ 776 if (eb->txn_root) 777 svn_fs_close_root(eb->txn_root); 778 779 if (err) 780 { 781 /* If the error was in post-commit, then the commit itself 782 succeeded. In which case, save the post-commit warning 783 (to be reported back to the client, who will probably 784 display it as a warning) and clear the error. */ 785 post_commit_err = svn_repos__post_commit_error_str(err, pool); 786 svn_error_clear(err); 787 } 788 789 /* Make sure a future abort doesn't perform 790 any work. This may occur if the commit 791 callback returns an error! */ 792 793 eb->txn = NULL; 794 eb->txn_root = NULL; 795 } 796 else 797 { 798 /* ### todo: we should check whether it really was a conflict, 799 and return the conflict info if so? */ 800 801 /* If the commit failed, it's *probably* due to a conflict -- 802 that is, the txn being out-of-date. The filesystem gives us 803 the ability to continue diddling the transaction and try 804 again; but let's face it: that's not how the cvs or svn works 805 from a user interface standpoint. Thus we don't make use of 806 this fs feature (for now, at least.) 807 808 So, in a nutshell: svn commits are an all-or-nothing deal. 809 Each commit creates a new fs txn which either succeeds or is 810 aborted completely. No second chances; the user simply 811 needs to update and commit again :) */ 812 813 eb->txn_aborted = TRUE; 814 815 return svn_error_trace( 816 svn_error_compose_create(err, 817 svn_fs_abort_txn(eb->txn, pool))); 818 } 819 820 /* At this point, the post-commit error has been converted to a string. 821 That information will be passed to a callback, if provided. If the 822 callback invocation fails in some way, that failure is returned here. 823 IOW, the post-commit error information is low priority compared to 824 other gunk here. */ 825 826 /* Pass new revision information to the caller's callback. */ 827 return svn_error_trace(invoke_commit_cb(eb->commit_callback, 828 eb->commit_callback_baton, 829 eb->repos->fs, 830 new_revision, 831 post_commit_err, 832 pool)); 833} 834 835 836static svn_error_t * 837abort_edit(void *edit_baton, 838 apr_pool_t *pool) 839{ 840 struct edit_baton *eb = edit_baton; 841 if ((! eb->txn) || (! eb->txn_owner) || eb->txn_aborted) 842 return SVN_NO_ERROR; 843 844 eb->txn_aborted = TRUE; 845 846 /* Since abort_edit is supposed to release resources, do it now. */ 847 if (eb->txn_root) 848 svn_fs_close_root(eb->txn_root); 849 850 return svn_error_trace(svn_fs_abort_txn(eb->txn, pool)); 851} 852 853 854static svn_error_t * 855fetch_props_func(apr_hash_t **props, 856 void *baton, 857 const char *path, 858 svn_revnum_t base_revision, 859 apr_pool_t *result_pool, 860 apr_pool_t *scratch_pool) 861{ 862 struct edit_baton *eb = baton; 863 svn_fs_root_t *fs_root; 864 svn_error_t *err; 865 866 SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, 867 svn_fs_txn_base_revision(eb->txn), 868 scratch_pool)); 869 err = svn_fs_node_proplist(props, fs_root, path, result_pool); 870 if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) 871 { 872 svn_error_clear(err); 873 *props = apr_hash_make(result_pool); 874 return SVN_NO_ERROR; 875 } 876 else if (err) 877 return svn_error_trace(err); 878 879 return SVN_NO_ERROR; 880} 881 882static svn_error_t * 883fetch_kind_func(svn_node_kind_t *kind, 884 void *baton, 885 const char *path, 886 svn_revnum_t base_revision, 887 apr_pool_t *scratch_pool) 888{ 889 struct edit_baton *eb = baton; 890 svn_fs_root_t *fs_root; 891 892 if (!SVN_IS_VALID_REVNUM(base_revision)) 893 base_revision = svn_fs_txn_base_revision(eb->txn); 894 895 SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, base_revision, scratch_pool)); 896 897 SVN_ERR(svn_fs_check_path(kind, fs_root, path, scratch_pool)); 898 899 return SVN_NO_ERROR; 900} 901 902static svn_error_t * 903fetch_base_func(const char **filename, 904 void *baton, 905 const char *path, 906 svn_revnum_t base_revision, 907 apr_pool_t *result_pool, 908 apr_pool_t *scratch_pool) 909{ 910 struct edit_baton *eb = baton; 911 svn_stream_t *contents; 912 svn_stream_t *file_stream; 913 const char *tmp_filename; 914 svn_fs_root_t *fs_root; 915 svn_error_t *err; 916 917 if (!SVN_IS_VALID_REVNUM(base_revision)) 918 base_revision = svn_fs_txn_base_revision(eb->txn); 919 920 SVN_ERR(svn_fs_revision_root(&fs_root, eb->fs, base_revision, scratch_pool)); 921 922 err = svn_fs_file_contents(&contents, fs_root, path, scratch_pool); 923 if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) 924 { 925 svn_error_clear(err); 926 *filename = NULL; 927 return SVN_NO_ERROR; 928 } 929 else if (err) 930 return svn_error_trace(err); 931 SVN_ERR(svn_stream_open_unique(&file_stream, &tmp_filename, NULL, 932 svn_io_file_del_on_pool_cleanup, 933 scratch_pool, scratch_pool)); 934 SVN_ERR(svn_stream_copy3(contents, file_stream, NULL, NULL, scratch_pool)); 935 936 *filename = apr_pstrdup(result_pool, tmp_filename); 937 938 return SVN_NO_ERROR; 939} 940 941 942 943/*** Public interfaces. ***/ 944 945svn_error_t * 946svn_repos_get_commit_editor5(const svn_delta_editor_t **editor, 947 void **edit_baton, 948 svn_repos_t *repos, 949 svn_fs_txn_t *txn, 950 const char *repos_url_decoded, 951 const char *base_path, 952 apr_hash_t *revprop_table, 953 svn_commit_callback2_t commit_callback, 954 void *commit_baton, 955 svn_repos_authz_callback_t authz_callback, 956 void *authz_baton, 957 apr_pool_t *pool) 958{ 959 svn_delta_editor_t *e; 960 apr_pool_t *subpool = svn_pool_create(pool); 961 struct edit_baton *eb; 962 svn_delta_shim_callbacks_t *shim_callbacks = 963 svn_delta_shim_callbacks_default(pool); 964 const char *repos_url = svn_path_uri_encode(repos_url_decoded, pool); 965 966 /* Do a global authz access lookup. Users with no write access 967 whatsoever to the repository don't get a commit editor. */ 968 if (authz_callback) 969 { 970 svn_boolean_t allowed; 971 972 SVN_ERR(authz_callback(svn_authz_write, &allowed, NULL, NULL, 973 authz_baton, pool)); 974 if (!allowed) 975 return svn_error_create(SVN_ERR_AUTHZ_UNWRITABLE, NULL, 976 "Not authorized to open a commit editor."); 977 } 978 979 /* Allocate the structures. */ 980 e = svn_delta_default_editor(pool); 981 eb = apr_pcalloc(subpool, sizeof(*eb)); 982 983 /* Set up the editor. */ 984 e->open_root = open_root; 985 e->delete_entry = delete_entry; 986 e->add_directory = add_directory; 987 e->open_directory = open_directory; 988 e->change_dir_prop = change_dir_prop; 989 e->add_file = add_file; 990 e->open_file = open_file; 991 e->close_file = close_file; 992 e->apply_textdelta = apply_textdelta; 993 e->change_file_prop = change_file_prop; 994 e->close_edit = close_edit; 995 e->abort_edit = abort_edit; 996 997 /* Set up the edit baton. */ 998 eb->pool = subpool; 999 eb->revprop_table = svn_prop_hash_dup(revprop_table, subpool); 1000 eb->commit_callback = commit_callback; 1001 eb->commit_callback_baton = commit_baton; 1002 eb->authz_callback = authz_callback; 1003 eb->authz_baton = authz_baton; 1004 eb->base_path = svn_fspath__canonicalize(base_path, subpool); 1005 eb->repos = repos; 1006 eb->repos_url_decoded = repos_url_decoded; 1007 eb->repos_name = svn_dirent_basename(svn_repos_path(repos, subpool), 1008 subpool); 1009 eb->fs = svn_repos_fs(repos); 1010 eb->txn = txn; 1011 eb->txn_owner = txn == NULL; 1012 1013 *edit_baton = eb; 1014 *editor = e; 1015 1016 shim_callbacks->fetch_props_func = fetch_props_func; 1017 shim_callbacks->fetch_kind_func = fetch_kind_func; 1018 shim_callbacks->fetch_base_func = fetch_base_func; 1019 shim_callbacks->fetch_baton = eb; 1020 1021 SVN_ERR(svn_editor__insert_shims(editor, edit_baton, *editor, *edit_baton, 1022 repos_url, eb->base_path, 1023 shim_callbacks, pool, pool)); 1024 1025 return SVN_NO_ERROR; 1026} 1027 1028 1029#if 0 1030static svn_error_t * 1031ev2_check_authz(const struct ev2_baton *eb, 1032 const char *relpath, 1033 svn_repos_authz_access_t required, 1034 apr_pool_t *scratch_pool) 1035{ 1036 const char *fspath; 1037 svn_boolean_t allowed; 1038 1039 if (eb->authz == NULL) 1040 return SVN_NO_ERROR; 1041 1042 if (relpath) 1043 fspath = apr_pstrcat(scratch_pool, "/", relpath, SVN_VA_NULL); 1044 else 1045 fspath = NULL; 1046 1047 SVN_ERR(svn_repos_authz_check_access(eb->authz, eb->authz_repos_name, fspath, 1048 eb->authz_user, required, 1049 &allowed, scratch_pool)); 1050 if (!allowed) 1051 return svn_error_create(required & svn_authz_write 1052 ? SVN_ERR_AUTHZ_UNWRITABLE 1053 : SVN_ERR_AUTHZ_UNREADABLE, 1054 NULL, "Access denied"); 1055 1056 return SVN_NO_ERROR; 1057} 1058#endif 1059 1060 1061/* This implements svn_editor_cb_add_directory_t */ 1062static svn_error_t * 1063add_directory_cb(void *baton, 1064 const char *relpath, 1065 const apr_array_header_t *children, 1066 apr_hash_t *props, 1067 svn_revnum_t replaces_rev, 1068 apr_pool_t *scratch_pool) 1069{ 1070 struct ev2_baton *eb = baton; 1071 1072 SVN_ERR(svn_editor_add_directory(eb->inner, relpath, children, props, 1073 replaces_rev)); 1074 return SVN_NO_ERROR; 1075} 1076 1077 1078/* This implements svn_editor_cb_add_file_t */ 1079static svn_error_t * 1080add_file_cb(void *baton, 1081 const char *relpath, 1082 const svn_checksum_t *checksum, 1083 svn_stream_t *contents, 1084 apr_hash_t *props, 1085 svn_revnum_t replaces_rev, 1086 apr_pool_t *scratch_pool) 1087{ 1088 struct ev2_baton *eb = baton; 1089 1090 SVN_ERR(svn_editor_add_file(eb->inner, relpath, checksum, contents, props, 1091 replaces_rev)); 1092 return SVN_NO_ERROR; 1093} 1094 1095 1096/* This implements svn_editor_cb_add_symlink_t */ 1097static svn_error_t * 1098add_symlink_cb(void *baton, 1099 const char *relpath, 1100 const char *target, 1101 apr_hash_t *props, 1102 svn_revnum_t replaces_rev, 1103 apr_pool_t *scratch_pool) 1104{ 1105 struct ev2_baton *eb = baton; 1106 1107 SVN_ERR(svn_editor_add_symlink(eb->inner, relpath, target, props, 1108 replaces_rev)); 1109 return SVN_NO_ERROR; 1110} 1111 1112 1113/* This implements svn_editor_cb_add_absent_t */ 1114static svn_error_t * 1115add_absent_cb(void *baton, 1116 const char *relpath, 1117 svn_node_kind_t kind, 1118 svn_revnum_t replaces_rev, 1119 apr_pool_t *scratch_pool) 1120{ 1121 struct ev2_baton *eb = baton; 1122 1123 SVN_ERR(svn_editor_add_absent(eb->inner, relpath, kind, replaces_rev)); 1124 return SVN_NO_ERROR; 1125} 1126 1127 1128/* This implements svn_editor_cb_alter_directory_t */ 1129static svn_error_t * 1130alter_directory_cb(void *baton, 1131 const char *relpath, 1132 svn_revnum_t revision, 1133 const apr_array_header_t *children, 1134 apr_hash_t *props, 1135 apr_pool_t *scratch_pool) 1136{ 1137 struct ev2_baton *eb = baton; 1138 1139 SVN_ERR(svn_editor_alter_directory(eb->inner, relpath, revision, 1140 children, props)); 1141 return SVN_NO_ERROR; 1142} 1143 1144 1145/* This implements svn_editor_cb_alter_file_t */ 1146static svn_error_t * 1147alter_file_cb(void *baton, 1148 const char *relpath, 1149 svn_revnum_t revision, 1150 const svn_checksum_t *checksum, 1151 svn_stream_t *contents, 1152 apr_hash_t *props, 1153 apr_pool_t *scratch_pool) 1154{ 1155 struct ev2_baton *eb = baton; 1156 1157 SVN_ERR(svn_editor_alter_file(eb->inner, relpath, revision, 1158 checksum, contents, props)); 1159 return SVN_NO_ERROR; 1160} 1161 1162 1163/* This implements svn_editor_cb_alter_symlink_t */ 1164static svn_error_t * 1165alter_symlink_cb(void *baton, 1166 const char *relpath, 1167 svn_revnum_t revision, 1168 const char *target, 1169 apr_hash_t *props, 1170 apr_pool_t *scratch_pool) 1171{ 1172 struct ev2_baton *eb = baton; 1173 1174 SVN_ERR(svn_editor_alter_symlink(eb->inner, relpath, revision, 1175 target, props)); 1176 return SVN_NO_ERROR; 1177} 1178 1179 1180/* This implements svn_editor_cb_delete_t */ 1181static svn_error_t * 1182delete_cb(void *baton, 1183 const char *relpath, 1184 svn_revnum_t revision, 1185 apr_pool_t *scratch_pool) 1186{ 1187 struct ev2_baton *eb = baton; 1188 1189 SVN_ERR(svn_editor_delete(eb->inner, relpath, revision)); 1190 return SVN_NO_ERROR; 1191} 1192 1193 1194/* This implements svn_editor_cb_copy_t */ 1195static svn_error_t * 1196copy_cb(void *baton, 1197 const char *src_relpath, 1198 svn_revnum_t src_revision, 1199 const char *dst_relpath, 1200 svn_revnum_t replaces_rev, 1201 apr_pool_t *scratch_pool) 1202{ 1203 struct ev2_baton *eb = baton; 1204 1205 SVN_ERR(svn_editor_copy(eb->inner, src_relpath, src_revision, dst_relpath, 1206 replaces_rev)); 1207 return SVN_NO_ERROR; 1208} 1209 1210 1211/* This implements svn_editor_cb_move_t */ 1212static svn_error_t * 1213move_cb(void *baton, 1214 const char *src_relpath, 1215 svn_revnum_t src_revision, 1216 const char *dst_relpath, 1217 svn_revnum_t replaces_rev, 1218 apr_pool_t *scratch_pool) 1219{ 1220 struct ev2_baton *eb = baton; 1221 1222 SVN_ERR(svn_editor_move(eb->inner, src_relpath, src_revision, dst_relpath, 1223 replaces_rev)); 1224 return SVN_NO_ERROR; 1225} 1226 1227 1228/* This implements svn_editor_cb_complete_t */ 1229static svn_error_t * 1230complete_cb(void *baton, 1231 apr_pool_t *scratch_pool) 1232{ 1233 struct ev2_baton *eb = baton; 1234 svn_revnum_t revision; 1235 svn_error_t *post_commit_err; 1236 const char *conflict_path; 1237 svn_error_t *err; 1238 const char *post_commit_errstr; 1239 apr_hash_t *hooks_env; 1240 1241 /* Parse the hooks-env file (if any). */ 1242 SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, eb->repos->hooks_env_path, 1243 scratch_pool, scratch_pool)); 1244 1245 /* The transaction has been fully edited. Let the pre-commit hook 1246 have a look at the thing. */ 1247 SVN_ERR(svn_repos__hooks_pre_commit(eb->repos, hooks_env, 1248 eb->txn_name, scratch_pool)); 1249 1250 /* Hook is done. Let's do the actual commit. */ 1251 SVN_ERR(svn_fs__editor_commit(&revision, &post_commit_err, &conflict_path, 1252 eb->inner, scratch_pool, scratch_pool)); 1253 1254 /* Did a conflict occur during the commit process? */ 1255 if (conflict_path != NULL) 1256 return svn_error_createf(SVN_ERR_FS_CONFLICT, NULL, 1257 _("Conflict at '%s'"), conflict_path); 1258 1259 /* Since did not receive an error during the commit process, and no 1260 conflict was specified... we committed a revision. Run the hooks. 1261 Other errors may have occurred within the FS (specified by the 1262 POST_COMMIT_ERR localvar), but we need to run the hooks. */ 1263 SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(revision)); 1264 err = svn_repos__hooks_post_commit(eb->repos, hooks_env, revision, 1265 eb->txn_name, scratch_pool); 1266 if (err) 1267 err = svn_error_create(SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED, err, 1268 _("Commit succeeded, but post-commit hook failed")); 1269 1270 /* Combine the FS errors with the hook errors, and stringify. */ 1271 err = svn_error_compose_create(post_commit_err, err); 1272 if (err) 1273 { 1274 post_commit_errstr = svn_repos__post_commit_error_str(err, scratch_pool); 1275 svn_error_clear(err); 1276 } 1277 else 1278 { 1279 post_commit_errstr = NULL; 1280 } 1281 1282 return svn_error_trace(invoke_commit_cb(eb->commit_cb, eb->commit_baton, 1283 eb->repos->fs, revision, 1284 post_commit_errstr, 1285 scratch_pool)); 1286} 1287 1288 1289/* This implements svn_editor_cb_abort_t */ 1290static svn_error_t * 1291abort_cb(void *baton, 1292 apr_pool_t *scratch_pool) 1293{ 1294 struct ev2_baton *eb = baton; 1295 1296 SVN_ERR(svn_editor_abort(eb->inner)); 1297 return SVN_NO_ERROR; 1298} 1299 1300 1301static svn_error_t * 1302apply_revprops(svn_fs_t *fs, 1303 const char *txn_name, 1304 apr_hash_t *revprops, 1305 apr_pool_t *scratch_pool) 1306{ 1307 svn_fs_txn_t *txn; 1308 const apr_array_header_t *revprops_array; 1309 1310 /* The FS editor has a TXN inside it, but we can't access it. Open another 1311 based on the TXN_NAME. */ 1312 SVN_ERR(svn_fs_open_txn(&txn, fs, txn_name, scratch_pool)); 1313 1314 /* Validate and apply the revision properties. */ 1315 revprops_array = svn_prop_hash_to_array(revprops, scratch_pool); 1316 SVN_ERR(svn_repos_fs_change_txn_props(txn, revprops_array, scratch_pool)); 1317 1318 /* ### do we need to force the txn to close, or is it enough to wait 1319 ### for the pool to be cleared? */ 1320 return SVN_NO_ERROR; 1321} 1322 1323 1324svn_error_t * 1325svn_repos__get_commit_ev2(svn_editor_t **editor, 1326 svn_repos_t *repos, 1327 svn_authz_t *authz, 1328 const char *authz_repos_name, 1329 const char *authz_user, 1330 apr_hash_t *revprops, 1331 svn_commit_callback2_t commit_cb, 1332 void *commit_baton, 1333 svn_cancel_func_t cancel_func, 1334 void *cancel_baton, 1335 apr_pool_t *result_pool, 1336 apr_pool_t *scratch_pool) 1337{ 1338 static const svn_editor_cb_many_t editor_cbs = { 1339 add_directory_cb, 1340 add_file_cb, 1341 add_symlink_cb, 1342 add_absent_cb, 1343 alter_directory_cb, 1344 alter_file_cb, 1345 alter_symlink_cb, 1346 delete_cb, 1347 copy_cb, 1348 move_cb, 1349 complete_cb, 1350 abort_cb 1351 }; 1352 struct ev2_baton *eb; 1353 const svn_string_t *author; 1354 apr_hash_t *hooks_env; 1355 1356 /* Parse the hooks-env file (if any). */ 1357 SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, repos->hooks_env_path, 1358 scratch_pool, scratch_pool)); 1359 1360 /* Can the user modify the repository at all? */ 1361 /* ### check against AUTHZ. */ 1362 1363 author = svn_hash_gets(revprops, SVN_PROP_REVISION_AUTHOR); 1364 1365 eb = apr_palloc(result_pool, sizeof(*eb)); 1366 eb->repos = repos; 1367 eb->authz = authz; 1368 eb->authz_repos_name = authz_repos_name; 1369 eb->authz_user = authz_user; 1370 eb->commit_cb = commit_cb; 1371 eb->commit_baton = commit_baton; 1372 1373 SVN_ERR(svn_fs__editor_create(&eb->inner, &eb->txn_name, 1374 repos->fs, SVN_FS_TXN_CHECK_LOCKS, 1375 cancel_func, cancel_baton, 1376 result_pool, scratch_pool)); 1377 1378 /* The TXN has been created. Go ahead and apply all revision properties. */ 1379 SVN_ERR(apply_revprops(repos->fs, eb->txn_name, revprops, scratch_pool)); 1380 1381 /* Okay... some access is allowed. Let's run the start-commit hook. */ 1382 SVN_ERR(svn_repos__hooks_start_commit(repos, hooks_env, 1383 author ? author->data : NULL, 1384 repos->client_capabilities, 1385 eb->txn_name, scratch_pool)); 1386 1387 /* Wrap the FS editor within our editor. */ 1388 SVN_ERR(svn_editor_create(editor, eb, cancel_func, cancel_baton, 1389 result_pool, scratch_pool)); 1390 SVN_ERR(svn_editor_setcb_many(*editor, &editor_cbs, scratch_pool)); 1391 1392 return SVN_NO_ERROR; 1393} 1394