svnmucc.c revision 251886
198184Sgordon/* 278344Sobrien * svnmucc.c: Subversion Multiple URL Client 3265420Simp * 4156813Sru * ==================================================================== 5228541Spjd * Licensed to the Apache Software Foundation (ASF) under one 6228541Spjd * or more contributor license agreements. See the NOTICE file 7228541Spjd * distributed with this work for additional information 8228541Spjd * regarding copyright ownership. The ASF licenses this file 9228541Spjd * to you under the Apache License, Version 2.0 (the 10228541Spjd * "License"); you may not use this file except in compliance 11228541Spjd * with the License. You may obtain a copy of the License at 12228541Spjd * 13228541Spjd * http://www.apache.org/licenses/LICENSE-2.0 14228541Spjd * 15228541Spjd * Unless required by applicable law or agreed to in writing, 16228541Spjd * software distributed under the License is distributed on an 17228541Spjd * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18228541Spjd * KIND, either express or implied. See the License for the 19243752Srwatson * specific language governing permissions and limitations 20228541Spjd * under the License. 21256022Sgjb * ==================================================================== 22228541Spjd * 23256022Sgjb */ 24259682Sgjb 25228541Spjd/* Multiple URL Command Client 26228541Spjd 27228541Spjd Combine a list of mv, cp and rm commands on URLs into a single commit. 28255570Strasz 29228541Spjd How it works: the command line arguments are parsed into an array of 30228541Spjd action structures. The action structures are interpreted to build a 31228541Spjd tree of operation structures. The tree of operation structures is 32228541Spjd used to drive an RA commit editor to produce a single commit. 33228541Spjd 34228541Spjd To build this client, type 'make svnmucc' from the root of your 35228541Spjd Subversion source directory. 36228541Spjd*/ 37228541Spjd 38228541Spjd#include <stdio.h> 39228541Spjd#include <string.h> 40228541Spjd 41273955Sjmg#include <apr_lib.h> 42228541Spjd 43256022Sgjb#include "svn_hash.h" 44228541Spjd#include "svn_client.h" 45228541Spjd#include "svn_cmdline.h" 46228541Spjd#include "svn_config.h" 47279463Srstone#include "svn_error.h" 48228541Spjd#include "svn_path.h" 49228541Spjd#include "svn_pools.h" 50228541Spjd#include "svn_props.h" 51228541Spjd#include "svn_ra.h" 52228541Spjd#include "svn_string.h" 53228541Spjd#include "svn_subst.h" 54228541Spjd#include "svn_utf.h" 55273285Shrs#include "svn_version.h" 56273285Shrs 57273285Shrs#include "private/svn_cmdline_private.h" 58228541Spjd#include "private/svn_ra_private.h" 59228541Spjd#include "private/svn_string_private.h" 60273285Shrs 61228541Spjd#include "svn_private_config.h" 62228541Spjd 63228541Spjdstatic void handle_error(svn_error_t *err, apr_pool_t *pool) 64228541Spjd{ 65228541Spjd if (err) 66228541Spjd svn_handle_error2(err, stderr, FALSE, "svnmucc: "); 67228541Spjd svn_error_clear(err); 68228541Spjd if (pool) 69228541Spjd svn_pool_destroy(pool); 70228541Spjd exit(EXIT_FAILURE); 71228541Spjd} 72228541Spjd 73228541Spjdstatic apr_pool_t * 74228541Spjdinit(const char *application) 75228541Spjd{ 76228541Spjd svn_error_t *err; 77228541Spjd const svn_version_checklist_t checklist[] = { 78228541Spjd {"svn_client", svn_client_version}, 79228541Spjd {"svn_subr", svn_subr_version}, 80228541Spjd {"svn_ra", svn_ra_version}, 81228541Spjd {NULL, NULL} 82228541Spjd }; 83228541Spjd SVN_VERSION_DEFINE(my_version); 84228541Spjd 85228541Spjd if (svn_cmdline_init(application, stderr)) 86228541Spjd exit(EXIT_FAILURE); 87228541Spjd 88228541Spjd err = svn_ver_check_list(&my_version, checklist); 89228541Spjd if (err) 90228541Spjd handle_error(err, NULL); 91228541Spjd 92228541Spjd return apr_allocator_owner_get(svn_pool_create_allocator(FALSE)); 93228541Spjd} 94228541Spjd 95118224Smtmstatic svn_error_t * 96228541Spjdopen_tmp_file(apr_file_t **fp, 97228541Spjd void *callback_baton, 98228541Spjd apr_pool_t *pool) 99228541Spjd{ 100228541Spjd /* Open a unique file; use APR_DELONCLOSE. */ 101228541Spjd return svn_io_open_unique_file3(fp, NULL, NULL, svn_io_file_del_on_close, 102228541Spjd pool, pool); 103228541Spjd} 104228541Spjd 105228541Spjdstatic svn_error_t * 106228541Spjdcreate_ra_callbacks(svn_ra_callbacks2_t **callbacks, 107228541Spjd const char *username, 108228541Spjd const char *password, 109228541Spjd const char *config_dir, 110228541Spjd svn_config_t *cfg_config, 111228541Spjd svn_boolean_t non_interactive, 112228541Spjd svn_boolean_t trust_server_cert, 113228541Spjd svn_boolean_t no_auth_cache, 114228541Spjd apr_pool_t *pool) 115228541Spjd{ 116228541Spjd SVN_ERR(svn_ra_create_callbacks(callbacks, pool)); 117252310Shrs 118252310Shrs SVN_ERR(svn_cmdline_create_auth_baton(&(*callbacks)->auth_baton, 119228541Spjd non_interactive, 120228541Spjd username, password, config_dir, 121228541Spjd no_auth_cache, 122228541Spjd trust_server_cert, 123153430Siedowse cfg_config, NULL, NULL, pool)); 124255809Sdes 125231534Sed (*callbacks)->open_tmp_file = open_tmp_file; 126228541Spjd 127228541Spjd return SVN_NO_ERROR; 128228541Spjd} 129228541Spjd 130228541Spjd 131228541Spjd 132228541Spjdstatic svn_error_t * 133228541Spjdcommit_callback(const svn_commit_info_t *commit_info, 134228541Spjd void *baton, 135228541Spjd apr_pool_t *pool) 136150490Swollman{ 137277736Sngie SVN_ERR(svn_cmdline_printf(pool, "r%ld committed by %s at %s\n", 138277736Sngie commit_info->revision, 139277736Sngie (commit_info->author 140277736Sngie ? commit_info->author : "(no author)"), 141277730Sngie commit_info->date)); 142277730Sngie return SVN_NO_ERROR; 143277730Sngie} 144277730Sngie 145278249Sngietypedef enum action_code_t { 146278249Sngie ACTION_MV, 147278249Sngie ACTION_MKDIR, 148278249Sngie ACTION_CP, 149277733Sngie ACTION_PROPSET, 150277733Sngie ACTION_PROPSETF, 151277733Sngie ACTION_PROPDEL, 152277733Sngie ACTION_PUT, 153277732Sngie ACTION_RM 154277732Sngie} action_code_t; 155277732Sngie 156277732Sngiestruct operation { 157277732Sngie enum { 158277728Sngie OP_OPEN, 159277728Sngie OP_DELETE, 160277728Sngie OP_ADD, 161277728Sngie OP_REPLACE, 162277728Sngie OP_PROPSET /* only for files for which no other operation is 163277728Sngie occuring; directories are OP_OPEN with non-empty 164271892Sngie props */ 165271892Sngie } operation; 166271892Sngie svn_node_kind_t kind; /* to copy, mkdir, put or set revprops */ 167271892Sngie svn_revnum_t rev; /* to copy, valid for add and replace */ 168271892Sngie const char *url; /* to copy, valid for add and replace */ 169219820Sjeff const char *src_file; /* for put, the source file for contents */ 170219820Sjeff apr_hash_t *children; /* const char *path -> struct operation * */ 171278249Sngie apr_hash_t *prop_mods; /* const char *prop_name -> 172278249Sngie const svn_string_t *prop_value */ 173278249Sngie apr_array_header_t *prop_dels; /* const char *prop_name deletions */ 174278249Sngie void *baton; /* as returned by the commit editor */ 175277686Sngie}; 176277686Sngie 177277686Sngie 178277686Sngie/* An iterator (for use via apr_table_do) which sets node properties. 179271892Sngie REC is a pointer to a struct driver_state. */ 180271892Sngiestatic svn_error_t * 181150490Swollmanchange_props(const svn_delta_editor_t *editor, 182150490Swollman void *baton, 183277678Sngie struct operation *child, 184277678Sngie apr_pool_t *pool) 185277678Sngie{ 186277678Sngie apr_pool_t *iterpool = svn_pool_create(pool); 187278249Sngie 188278249Sngie if (child->prop_dels) 189278249Sngie { 190278249Sngie int i; 191277725Sngie for (i = 0; i < child->prop_dels->nelts; i++) 192277725Sngie { 193277725Sngie const char *prop_name; 194277725Sngie 195278249Sngie svn_pool_clear(iterpool); 196278282Sngie prop_name = APR_ARRAY_IDX(child->prop_dels, i, const char *); 197278249Sngie if (child->kind == svn_node_dir) 198278249Sngie SVN_ERR(editor->change_dir_prop(baton, prop_name, 199277675Sngie NULL, iterpool)); 200277675Sngie else 201277675Sngie SVN_ERR(editor->change_file_prop(baton, prop_name, 202277675Sngie NULL, iterpool)); 203277675Sngie } 204277739Sngie } 205277739Sngie if (apr_hash_count(child->prop_mods)) 206277739Sngie { 207277739Sngie apr_hash_index_t *hi; 208278249Sngie for (hi = apr_hash_first(pool, child->prop_mods); 209278249Sngie hi; hi = apr_hash_next(hi)) 210278249Sngie { 211278249Sngie const char *propname = svn__apr_hash_index_key(hi); 212278249Sngie const svn_string_t *val = svn__apr_hash_index_val(hi); 213277731Sngie 214277731Sngie svn_pool_clear(iterpool); 215277731Sngie if (child->kind == svn_node_dir) 216277731Sngie SVN_ERR(editor->change_dir_prop(baton, propname, val, iterpool)); 217273285Shrs else 218277741Sngie SVN_ERR(editor->change_file_prop(baton, propname, val, iterpool)); 219277741Sngie } 220273285Shrs } 221273285Shrs 222273285Shrs svn_pool_destroy(iterpool); 223273285Shrs return SVN_NO_ERROR; 224273285Shrs} 225273285Shrs 226278249Sngie 227278249Sngie/* Drive EDITOR to affect the change represented by OPERATION. HEAD 228278249Sngie is the last-known youngest revision in the repository. */ 229278249Sngiestatic svn_error_t * 230278249Sngiedrive(struct operation *operation, 231278249Sngie svn_revnum_t head, 232278249Sngie const svn_delta_editor_t *editor, 233278249Sngie apr_pool_t *pool) 234278249Sngie{ 235278249Sngie apr_pool_t *subpool = svn_pool_create(pool); 236278249Sngie apr_hash_index_t *hi; 237278249Sngie 238271892Sngie for (hi = apr_hash_first(pool, operation->children); 239271892Sngie hi; hi = apr_hash_next(hi)) 240206706Srpaulo { 241206706Srpaulo const char *key = svn__apr_hash_index_key(hi); 242272043Sngie struct operation *child = svn__apr_hash_index_val(hi); 243272043Sngie void *file_baton = NULL; 244272043Sngie 245272043Sngie svn_pool_clear(subpool); 246271892Sngie 247271892Sngie /* Deletes and replacements are simple -- delete something. */ 248259682Sgjb if (child->operation == OP_DELETE || child->operation == OP_REPLACE) 249259682Sgjb { 250271895Sngie SVN_ERR(editor->delete_entry(key, head, operation->baton, subpool)); 251271895Sngie } 252271895Sngie /* Opens could be for directories or files. */ 253271895Sngie if (child->operation == OP_OPEN || child->operation == OP_PROPSET) 254271893Sngie { 255271893Sngie if (child->kind == svn_node_dir) 256271893Sngie { 257271893Sngie SVN_ERR(editor->open_directory(key, operation->baton, head, 258278249Sngie subpool, &child->baton)); 259278249Sngie } 260278249Sngie else 261278249Sngie { 262278249Sngie SVN_ERR(editor->open_file(key, operation->baton, head, 263278249Sngie subpool, &file_baton)); 264278249Sngie } 265278249Sngie } 266278249Sngie /* Adds and replacements could also be for directories or files. */ 267278249Sngie if (child->operation == OP_ADD || child->operation == OP_REPLACE) 268278249Sngie { 269278249Sngie if (child->kind == svn_node_dir) 270255809Sdes { 271255809Sdes SVN_ERR(editor->add_directory(key, operation->baton, 272255809Sdes child->url, child->rev, 273255809Sdes subpool, &child->baton)); 274231534Sed } 275231534Sed else 276231534Sed { 277231534Sed SVN_ERR(editor->add_file(key, operation->baton, child->url, 278278249Sngie child->rev, subpool, &file_baton)); 279278249Sngie } 280278249Sngie } 281278249Sngie /* If there's a source file and an open file baton, we get to 282277740Sngie change textual contents. */ 283277740Sngie if ((child->src_file) && (file_baton)) 284277740Sngie { 285277740Sngie svn_txdelta_window_handler_t handler; 286277740Sngie void *handler_baton; 28778344Sobrien svn_stream_t *contents; 28878344Sobrien 28978344Sobrien SVN_ERR(editor->apply_textdelta(file_baton, NULL, subpool, 29078344Sobrien &handler, &handler_baton)); 291 if (strcmp(child->src_file, "-") != 0) 292 { 293 SVN_ERR(svn_stream_open_readonly(&contents, child->src_file, 294 pool, pool)); 295 } 296 else 297 { 298 SVN_ERR(svn_stream_for_stdin(&contents, pool)); 299 } 300 SVN_ERR(svn_txdelta_send_stream(contents, handler, 301 handler_baton, NULL, pool)); 302 } 303 /* If we opened a file, we need to apply outstanding propmods, 304 then close it. */ 305 if (file_baton) 306 { 307 if (child->kind == svn_node_file) 308 { 309 SVN_ERR(change_props(editor, file_baton, child, subpool)); 310 } 311 SVN_ERR(editor->close_file(file_baton, NULL, subpool)); 312 } 313 /* If we opened, added, or replaced a directory, we need to 314 recurse, apply outstanding propmods, and then close it. */ 315 if ((child->kind == svn_node_dir) 316 && child->operation != OP_DELETE) 317 { 318 SVN_ERR(change_props(editor, child->baton, child, subpool)); 319 320 SVN_ERR(drive(child, head, editor, subpool)); 321 322 SVN_ERR(editor->close_directory(child->baton, subpool)); 323 } 324 } 325 svn_pool_destroy(subpool); 326 return SVN_NO_ERROR; 327} 328 329 330/* Find the operation associated with PATH, which is a single-path 331 component representing a child of the path represented by 332 OPERATION. If no such child operation exists, create a new one of 333 type OP_OPEN. */ 334static struct operation * 335get_operation(const char *path, 336 struct operation *operation, 337 apr_pool_t *pool) 338{ 339 struct operation *child = svn_hash_gets(operation->children, path); 340 if (! child) 341 { 342 child = apr_pcalloc(pool, sizeof(*child)); 343 child->children = apr_hash_make(pool); 344 child->operation = OP_OPEN; 345 child->rev = SVN_INVALID_REVNUM; 346 child->kind = svn_node_dir; 347 child->prop_mods = apr_hash_make(pool); 348 child->prop_dels = apr_array_make(pool, 1, sizeof(const char *)); 349 svn_hash_sets(operation->children, path, child); 350 } 351 return child; 352} 353 354/* Return the portion of URL that is relative to ANCHOR (URI-decoded). */ 355static const char * 356subtract_anchor(const char *anchor, const char *url, apr_pool_t *pool) 357{ 358 return svn_uri_skip_ancestor(anchor, url, pool); 359} 360 361/* Add PATH to the operations tree rooted at OPERATION, creating any 362 intermediate nodes that are required. Here's what's expected for 363 each action type: 364 365 ACTION URL REV SRC-FILE PROPNAME 366 ------------ ----- ------- -------- -------- 367 ACTION_MKDIR NULL invalid NULL NULL 368 ACTION_CP valid valid NULL NULL 369 ACTION_PUT NULL invalid valid NULL 370 ACTION_RM NULL invalid NULL NULL 371 ACTION_PROPSET valid invalid NULL valid 372 ACTION_PROPDEL valid invalid NULL valid 373 374 Node type information is obtained for any copy source (to determine 375 whether to create a file or directory) and for any deleted path (to 376 ensure it exists since svn_delta_editor_t->delete_entry doesn't 377 return an error on non-existent nodes). */ 378static svn_error_t * 379build(action_code_t action, 380 const char *path, 381 const char *url, 382 svn_revnum_t rev, 383 const char *prop_name, 384 const svn_string_t *prop_value, 385 const char *src_file, 386 svn_revnum_t head, 387 const char *anchor, 388 svn_ra_session_t *session, 389 struct operation *operation, 390 apr_pool_t *pool) 391{ 392 apr_array_header_t *path_bits = svn_path_decompose(path, pool); 393 const char *path_so_far = ""; 394 const char *copy_src = NULL; 395 svn_revnum_t copy_rev = SVN_INVALID_REVNUM; 396 int i; 397 398 /* Look for any previous operations we've recognized for PATH. If 399 any of PATH's ancestors have not yet been traversed, we'll be 400 creating OP_OPEN operations for them as we walk down PATH's path 401 components. */ 402 for (i = 0; i < path_bits->nelts; ++i) 403 { 404 const char *path_bit = APR_ARRAY_IDX(path_bits, i, const char *); 405 path_so_far = svn_relpath_join(path_so_far, path_bit, pool); 406 operation = get_operation(path_so_far, operation, pool); 407 408 /* If we cross a replace- or add-with-history, remember the 409 source of those things in case we need to lookup the node kind 410 of one of their children. And if this isn't such a copy, 411 but we've already seen one in of our parent paths, we just need 412 to extend that copy source path by our current path 413 component. */ 414 if (operation->url 415 && SVN_IS_VALID_REVNUM(operation->rev) 416 && (operation->operation == OP_REPLACE 417 || operation->operation == OP_ADD)) 418 { 419 copy_src = subtract_anchor(anchor, operation->url, pool); 420 copy_rev = operation->rev; 421 } 422 else if (copy_src) 423 { 424 copy_src = svn_relpath_join(copy_src, path_bit, pool); 425 } 426 } 427 428 /* Handle property changes. */ 429 if (prop_name) 430 { 431 if (operation->operation == OP_DELETE) 432 return svn_error_createf(SVN_ERR_BAD_URL, NULL, 433 "cannot set properties on a location being" 434 " deleted ('%s')", path); 435 /* If we're not adding this thing ourselves, check for existence. */ 436 if (! ((operation->operation == OP_ADD) || 437 (operation->operation == OP_REPLACE))) 438 { 439 SVN_ERR(svn_ra_check_path(session, 440 copy_src ? copy_src : path, 441 copy_src ? copy_rev : head, 442 &operation->kind, pool)); 443 if (operation->kind == svn_node_none) 444 return svn_error_createf(SVN_ERR_BAD_URL, NULL, 445 "propset: '%s' not found", path); 446 else if ((operation->kind == svn_node_file) 447 && (operation->operation == OP_OPEN)) 448 operation->operation = OP_PROPSET; 449 } 450 if (! prop_value) 451 APR_ARRAY_PUSH(operation->prop_dels, const char *) = prop_name; 452 else 453 svn_hash_sets(operation->prop_mods, prop_name, prop_value); 454 if (!operation->rev) 455 operation->rev = rev; 456 return SVN_NO_ERROR; 457 } 458 459 /* We won't fuss about multiple operations on the same path in the 460 following cases: 461 462 - the prior operation was, in fact, a no-op (open) 463 - the prior operation was a propset placeholder 464 - the prior operation was a deletion 465 466 Note: while the operation structure certainly supports the 467 ability to do a copy of a file followed by a put of new contents 468 for the file, we don't let that happen (yet). 469 */ 470 if (operation->operation != OP_OPEN 471 && operation->operation != OP_PROPSET 472 && operation->operation != OP_DELETE) 473 return svn_error_createf(SVN_ERR_BAD_URL, NULL, 474 "unsupported multiple operations on '%s'", path); 475 476 /* For deletions, we validate that there's actually something to 477 delete. If this is a deletion of the child of a copied 478 directory, we need to remember to look in the copy source tree to 479 verify that this thing actually exists. */ 480 if (action == ACTION_RM) 481 { 482 operation->operation = OP_DELETE; 483 SVN_ERR(svn_ra_check_path(session, 484 copy_src ? copy_src : path, 485 copy_src ? copy_rev : head, 486 &operation->kind, pool)); 487 if (operation->kind == svn_node_none) 488 { 489 if (copy_src && strcmp(path, copy_src)) 490 return svn_error_createf(SVN_ERR_BAD_URL, NULL, 491 "'%s' (from '%s:%ld') not found", 492 path, copy_src, copy_rev); 493 else 494 return svn_error_createf(SVN_ERR_BAD_URL, NULL, "'%s' not found", 495 path); 496 } 497 } 498 /* Handle copy operations (which can be adds or replacements). */ 499 else if (action == ACTION_CP) 500 { 501 if (rev > head) 502 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 503 "Copy source revision cannot be younger " 504 "than base revision"); 505 operation->operation = 506 operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD; 507 if (operation->operation == OP_ADD) 508 { 509 /* There is a bug in the current version of mod_dav_svn 510 which incorrectly replaces existing directories. 511 Therefore we need to check if the target exists 512 and raise an error here. */ 513 SVN_ERR(svn_ra_check_path(session, 514 copy_src ? copy_src : path, 515 copy_src ? copy_rev : head, 516 &operation->kind, pool)); 517 if (operation->kind != svn_node_none) 518 { 519 if (copy_src && strcmp(path, copy_src)) 520 return svn_error_createf(SVN_ERR_BAD_URL, NULL, 521 "'%s' (from '%s:%ld') already exists", 522 path, copy_src, copy_rev); 523 else 524 return svn_error_createf(SVN_ERR_BAD_URL, NULL, 525 "'%s' already exists", path); 526 } 527 } 528 SVN_ERR(svn_ra_check_path(session, subtract_anchor(anchor, url, pool), 529 rev, &operation->kind, pool)); 530 if (operation->kind == svn_node_none) 531 return svn_error_createf(SVN_ERR_BAD_URL, NULL, 532 "'%s' not found", 533 subtract_anchor(anchor, url, pool)); 534 operation->url = url; 535 operation->rev = rev; 536 } 537 /* Handle mkdir operations (which can be adds or replacements). */ 538 else if (action == ACTION_MKDIR) 539 { 540 operation->operation = 541 operation->operation == OP_DELETE ? OP_REPLACE : OP_ADD; 542 operation->kind = svn_node_dir; 543 } 544 /* Handle put operations (which can be adds, replacements, or opens). */ 545 else if (action == ACTION_PUT) 546 { 547 if (operation->operation == OP_DELETE) 548 { 549 operation->operation = OP_REPLACE; 550 } 551 else 552 { 553 SVN_ERR(svn_ra_check_path(session, 554 copy_src ? copy_src : path, 555 copy_src ? copy_rev : head, 556 &operation->kind, pool)); 557 if (operation->kind == svn_node_file) 558 operation->operation = OP_OPEN; 559 else if (operation->kind == svn_node_none) 560 operation->operation = OP_ADD; 561 else 562 return svn_error_createf(SVN_ERR_BAD_URL, NULL, 563 "'%s' is not a file", path); 564 } 565 operation->kind = svn_node_file; 566 operation->src_file = src_file; 567 } 568 else 569 { 570 /* We shouldn't get here. */ 571 SVN_ERR_MALFUNCTION(); 572 } 573 574 return SVN_NO_ERROR; 575} 576 577struct action { 578 action_code_t action; 579 580 /* revision (copy-from-rev of path[0] for cp; base-rev for put) */ 581 svn_revnum_t rev; 582 583 /* action path[0] path[1] 584 * ------ ------- ------- 585 * mv source target 586 * mkdir target (null) 587 * cp source target 588 * put target source 589 * rm target (null) 590 * propset target (null) 591 */ 592 const char *path[2]; 593 594 /* property name/value */ 595 const char *prop_name; 596 const svn_string_t *prop_value; 597}; 598 599struct fetch_baton 600{ 601 svn_ra_session_t *session; 602 svn_revnum_t head; 603}; 604 605static svn_error_t * 606fetch_base_func(const char **filename, 607 void *baton, 608 const char *path, 609 svn_revnum_t base_revision, 610 apr_pool_t *result_pool, 611 apr_pool_t *scratch_pool) 612{ 613 struct fetch_baton *fb = baton; 614 svn_stream_t *fstream; 615 svn_error_t *err; 616 617 if (! SVN_IS_VALID_REVNUM(base_revision)) 618 base_revision = fb->head; 619 620 SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL, 621 svn_io_file_del_on_pool_cleanup, 622 result_pool, scratch_pool)); 623 624 err = svn_ra_get_file(fb->session, path, base_revision, fstream, NULL, NULL, 625 scratch_pool); 626 if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) 627 { 628 svn_error_clear(err); 629 SVN_ERR(svn_stream_close(fstream)); 630 631 *filename = NULL; 632 return SVN_NO_ERROR; 633 } 634 else if (err) 635 return svn_error_trace(err); 636 637 SVN_ERR(svn_stream_close(fstream)); 638 639 return SVN_NO_ERROR; 640} 641 642static svn_error_t * 643fetch_props_func(apr_hash_t **props, 644 void *baton, 645 const char *path, 646 svn_revnum_t base_revision, 647 apr_pool_t *result_pool, 648 apr_pool_t *scratch_pool) 649{ 650 struct fetch_baton *fb = baton; 651 svn_node_kind_t node_kind; 652 653 if (! SVN_IS_VALID_REVNUM(base_revision)) 654 base_revision = fb->head; 655 656 SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, &node_kind, 657 scratch_pool)); 658 659 if (node_kind == svn_node_file) 660 { 661 SVN_ERR(svn_ra_get_file(fb->session, path, base_revision, NULL, NULL, 662 props, result_pool)); 663 } 664 else if (node_kind == svn_node_dir) 665 { 666 apr_array_header_t *tmp_props; 667 668 SVN_ERR(svn_ra_get_dir2(fb->session, NULL, NULL, props, path, 669 base_revision, 0 /* Dirent fields */, 670 result_pool)); 671 tmp_props = svn_prop_hash_to_array(*props, result_pool); 672 SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props, 673 result_pool)); 674 *props = svn_prop_array_to_hash(tmp_props, result_pool); 675 } 676 else 677 { 678 *props = apr_hash_make(result_pool); 679 } 680 681 return SVN_NO_ERROR; 682} 683 684static svn_error_t * 685fetch_kind_func(svn_node_kind_t *kind, 686 void *baton, 687 const char *path, 688 svn_revnum_t base_revision, 689 apr_pool_t *scratch_pool) 690{ 691 struct fetch_baton *fb = baton; 692 693 if (! SVN_IS_VALID_REVNUM(base_revision)) 694 base_revision = fb->head; 695 696 SVN_ERR(svn_ra_check_path(fb->session, path, base_revision, kind, 697 scratch_pool)); 698 699 return SVN_NO_ERROR; 700} 701 702static svn_delta_shim_callbacks_t * 703get_shim_callbacks(svn_ra_session_t *session, 704 svn_revnum_t head, 705 apr_pool_t *result_pool) 706{ 707 svn_delta_shim_callbacks_t *callbacks = 708 svn_delta_shim_callbacks_default(result_pool); 709 struct fetch_baton *fb = apr_pcalloc(result_pool, sizeof(*fb)); 710 711 fb->session = session; 712 fb->head = head; 713 714 callbacks->fetch_props_func = fetch_props_func; 715 callbacks->fetch_kind_func = fetch_kind_func; 716 callbacks->fetch_base_func = fetch_base_func; 717 callbacks->fetch_baton = fb; 718 719 return callbacks; 720} 721 722static svn_error_t * 723execute(const apr_array_header_t *actions, 724 const char *anchor, 725 apr_hash_t *revprops, 726 const char *username, 727 const char *password, 728 const char *config_dir, 729 const apr_array_header_t *config_options, 730 svn_boolean_t non_interactive, 731 svn_boolean_t trust_server_cert, 732 svn_boolean_t no_auth_cache, 733 svn_revnum_t base_revision, 734 apr_pool_t *pool) 735{ 736 svn_ra_session_t *session; 737 svn_ra_session_t *aux_session; 738 const char *repos_root; 739 svn_revnum_t head; 740 const svn_delta_editor_t *editor; 741 svn_ra_callbacks2_t *ra_callbacks; 742 void *editor_baton; 743 struct operation root; 744 svn_error_t *err; 745 apr_hash_t *config; 746 svn_config_t *cfg_config; 747 int i; 748 749 SVN_ERR(svn_config_get_config(&config, config_dir, pool)); 750 SVN_ERR(svn_cmdline__apply_config_options(config, config_options, 751 "svnmucc: ", "--config-option")); 752 cfg_config = svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG); 753 754 if (! svn_hash_gets(revprops, SVN_PROP_REVISION_LOG)) 755 { 756 svn_string_t *msg = svn_string_create("", pool); 757 758 /* If we can do so, try to pop up $EDITOR to fetch a log message. */ 759 if (non_interactive) 760 { 761 return svn_error_create 762 (SVN_ERR_CL_INSUFFICIENT_ARGS, NULL, 763 _("Cannot invoke editor to get log message " 764 "when non-interactive")); 765 } 766 else 767 { 768 SVN_ERR(svn_cmdline__edit_string_externally( 769 &msg, NULL, NULL, "", msg, "svnmucc-commit", config, 770 TRUE, NULL, apr_hash_pool_get(revprops))); 771 } 772 773 svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, msg); 774 } 775 776 SVN_ERR(create_ra_callbacks(&ra_callbacks, username, password, config_dir, 777 cfg_config, non_interactive, trust_server_cert, 778 no_auth_cache, pool)); 779 SVN_ERR(svn_ra_open4(&session, NULL, anchor, NULL, ra_callbacks, 780 NULL, config, pool)); 781 /* Open, then reparent to avoid AUTHZ errors when opening the reposroot */ 782 SVN_ERR(svn_ra_open4(&aux_session, NULL, anchor, NULL, ra_callbacks, 783 NULL, config, pool)); 784 SVN_ERR(svn_ra_get_repos_root2(aux_session, &repos_root, pool)); 785 SVN_ERR(svn_ra_reparent(aux_session, repos_root, pool)); 786 SVN_ERR(svn_ra_get_latest_revnum(session, &head, pool)); 787 788 /* Reparent to ANCHOR's dir, if ANCHOR is not a directory. */ 789 { 790 svn_node_kind_t kind; 791 792 SVN_ERR(svn_ra_check_path(aux_session, 793 svn_uri_skip_ancestor(repos_root, anchor, pool), 794 head, &kind, pool)); 795 if (kind != svn_node_dir) 796 { 797 anchor = svn_uri_dirname(anchor, pool); 798 SVN_ERR(svn_ra_reparent(session, anchor, pool)); 799 } 800 } 801 802 if (SVN_IS_VALID_REVNUM(base_revision)) 803 { 804 if (base_revision > head) 805 return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, 806 "No such revision %ld (youngest is %ld)", 807 base_revision, head); 808 head = base_revision; 809 } 810 811 memset(&root, 0, sizeof(root)); 812 root.children = apr_hash_make(pool); 813 root.operation = OP_OPEN; 814 root.kind = svn_node_dir; /* For setting properties */ 815 root.prop_mods = apr_hash_make(pool); 816 root.prop_dels = apr_array_make(pool, 1, sizeof(const char *)); 817 818 for (i = 0; i < actions->nelts; ++i) 819 { 820 struct action *action = APR_ARRAY_IDX(actions, i, struct action *); 821 const char *path1, *path2; 822 switch (action->action) 823 { 824 case ACTION_MV: 825 path1 = subtract_anchor(anchor, action->path[0], pool); 826 path2 = subtract_anchor(anchor, action->path[1], pool); 827 SVN_ERR(build(ACTION_RM, path1, NULL, 828 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor, 829 session, &root, pool)); 830 SVN_ERR(build(ACTION_CP, path2, action->path[0], 831 head, NULL, NULL, NULL, head, anchor, 832 session, &root, pool)); 833 break; 834 case ACTION_CP: 835 path2 = subtract_anchor(anchor, action->path[1], pool); 836 if (action->rev == SVN_INVALID_REVNUM) 837 action->rev = head; 838 SVN_ERR(build(ACTION_CP, path2, action->path[0], 839 action->rev, NULL, NULL, NULL, head, anchor, 840 session, &root, pool)); 841 break; 842 case ACTION_RM: 843 path1 = subtract_anchor(anchor, action->path[0], pool); 844 SVN_ERR(build(ACTION_RM, path1, NULL, 845 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor, 846 session, &root, pool)); 847 break; 848 case ACTION_MKDIR: 849 path1 = subtract_anchor(anchor, action->path[0], pool); 850 SVN_ERR(build(ACTION_MKDIR, path1, action->path[0], 851 SVN_INVALID_REVNUM, NULL, NULL, NULL, head, anchor, 852 session, &root, pool)); 853 break; 854 case ACTION_PUT: 855 path1 = subtract_anchor(anchor, action->path[0], pool); 856 SVN_ERR(build(ACTION_PUT, path1, action->path[0], 857 SVN_INVALID_REVNUM, NULL, NULL, action->path[1], 858 head, anchor, session, &root, pool)); 859 break; 860 case ACTION_PROPSET: 861 case ACTION_PROPDEL: 862 path1 = subtract_anchor(anchor, action->path[0], pool); 863 SVN_ERR(build(action->action, path1, action->path[0], 864 SVN_INVALID_REVNUM, 865 action->prop_name, action->prop_value, 866 NULL, head, anchor, session, &root, pool)); 867 break; 868 case ACTION_PROPSETF: 869 default: 870 SVN_ERR_MALFUNCTION_NO_RETURN(); 871 } 872 } 873 874 SVN_ERR(svn_ra__register_editor_shim_callbacks(session, 875 get_shim_callbacks(aux_session, head, pool))); 876 SVN_ERR(svn_ra_get_commit_editor3(session, &editor, &editor_baton, revprops, 877 commit_callback, NULL, NULL, FALSE, pool)); 878 879 SVN_ERR(editor->open_root(editor_baton, head, pool, &root.baton)); 880 err = change_props(editor, root.baton, &root, pool); 881 if (!err) 882 err = drive(&root, head, editor, pool); 883 if (!err) 884 err = editor->close_directory(root.baton, pool); 885 if (!err) 886 err = editor->close_edit(editor_baton, pool); 887 888 if (err) 889 err = svn_error_compose_create(err, 890 editor->abort_edit(editor_baton, pool)); 891 892 return err; 893} 894 895static svn_error_t * 896read_propvalue_file(const svn_string_t **value_p, 897 const char *filename, 898 apr_pool_t *pool) 899{ 900 svn_stringbuf_t *value; 901 apr_pool_t *scratch_pool = svn_pool_create(pool); 902 903 SVN_ERR(svn_stringbuf_from_file2(&value, filename, scratch_pool)); 904 *value_p = svn_string_create_from_buf(value, pool); 905 svn_pool_destroy(scratch_pool); 906 return SVN_NO_ERROR; 907} 908 909/* Perform the typical suite of manipulations for user-provided URLs 910 on URL, returning the result (allocated from POOL): IRI-to-URI 911 conversion, auto-escaping, and canonicalization. */ 912static const char * 913sanitize_url(const char *url, 914 apr_pool_t *pool) 915{ 916 url = svn_path_uri_from_iri(url, pool); 917 url = svn_path_uri_autoescape(url, pool); 918 return svn_uri_canonicalize(url, pool); 919} 920 921static void 922usage(apr_pool_t *pool, int exit_val) 923{ 924 FILE *stream = exit_val == EXIT_SUCCESS ? stdout : stderr; 925 svn_error_clear(svn_cmdline_fputs( 926 _("Subversion multiple URL command client\n" 927 "usage: svnmucc ACTION...\n" 928 "\n" 929 " Perform one or more Subversion repository URL-based ACTIONs, committing\n" 930 " the result as a (single) new revision.\n" 931 "\n" 932 "Actions:\n" 933 " cp REV SRC-URL DST-URL : copy SRC-URL@REV to DST-URL\n" 934 " mkdir URL : create new directory URL\n" 935 " mv SRC-URL DST-URL : move SRC-URL to DST-URL\n" 936 " rm URL : delete URL\n" 937 " put SRC-FILE URL : add or modify file URL with contents copied from\n" 938 " SRC-FILE (use \"-\" to read from standard input)\n" 939 " propset NAME VALUE URL : set property NAME on URL to VALUE\n" 940 " propsetf NAME FILE URL : set property NAME on URL to value read from FILE\n" 941 " propdel NAME URL : delete property NAME from URL\n" 942 "\n" 943 "Valid options:\n" 944 " -h, -? [--help] : display this text\n" 945 " -m [--message] ARG : use ARG as a log message\n" 946 " -F [--file] ARG : read log message from file ARG\n" 947 " -u [--username] ARG : commit the changes as username ARG\n" 948 " -p [--password] ARG : use ARG as the password\n" 949 " -U [--root-url] ARG : interpret all action URLs relative to ARG\n" 950 " -r [--revision] ARG : use revision ARG as baseline for changes\n" 951 " --with-revprop ARG : set revision property in the following format:\n" 952 " NAME[=VALUE]\n" 953 " --non-interactive : do no interactive prompting (default is to\n" 954 " prompt only if standard input is a terminal)\n" 955 " --force-interactive : do interactive prompting even if standard\n" 956 " input is not a terminal\n" 957 " --trust-server-cert : accept SSL server certificates from unknown\n" 958 " certificate authorities without prompting (but\n" 959 " only with '--non-interactive')\n" 960 " -X [--extra-args] ARG : append arguments from file ARG (one per line;\n" 961 " use \"-\" to read from standard input)\n" 962 " --config-dir ARG : use ARG to override the config directory\n" 963 " --config-option ARG : use ARG to override a configuration option\n" 964 " --no-auth-cache : do not cache authentication tokens\n" 965 " --version : print version information\n"), 966 stream, pool)); 967 svn_pool_destroy(pool); 968 exit(exit_val); 969} 970 971static void 972insufficient(apr_pool_t *pool) 973{ 974 handle_error(svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, 975 "insufficient arguments"), 976 pool); 977} 978 979static svn_error_t * 980display_version(apr_getopt_t *os, apr_pool_t *pool) 981{ 982 const char *ra_desc_start 983 = "The following repository access (RA) modules are available:\n\n"; 984 svn_stringbuf_t *version_footer; 985 986 version_footer = svn_stringbuf_create(ra_desc_start, pool); 987 SVN_ERR(svn_ra_print_modules(version_footer, pool)); 988 989 SVN_ERR(svn_opt_print_help4(os, "svnmucc", TRUE, FALSE, FALSE, 990 version_footer->data, 991 NULL, NULL, NULL, NULL, NULL, pool)); 992 993 return SVN_NO_ERROR; 994} 995 996/* Return an error about the mutual exclusivity of the -m, -F, and 997 --with-revprop=svn:log command-line options. */ 998static svn_error_t * 999mutually_exclusive_logs_error(void) 1000{ 1001 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 1002 _("--message (-m), --file (-F), and " 1003 "--with-revprop=svn:log are mutually " 1004 "exclusive")); 1005} 1006 1007/* Ensure that the REVPROPS hash contains a command-line-provided log 1008 message, if any, and that there was but one source of such a thing 1009 provided on that command-line. */ 1010static svn_error_t * 1011sanitize_log_sources(apr_hash_t *revprops, 1012 const char *message, 1013 svn_stringbuf_t *filedata) 1014{ 1015 apr_pool_t *hash_pool = apr_hash_pool_get(revprops); 1016 1017 /* If we already have a log message in the revprop hash, then just 1018 make sure the user didn't try to also use -m or -F. Otherwise, 1019 we need to consult -m or -F to find a log message, if any. */ 1020 if (svn_hash_gets(revprops, SVN_PROP_REVISION_LOG)) 1021 { 1022 if (filedata || message) 1023 return mutually_exclusive_logs_error(); 1024 } 1025 else if (filedata) 1026 { 1027 if (message) 1028 return mutually_exclusive_logs_error(); 1029 1030 SVN_ERR(svn_utf_cstring_to_utf8(&message, filedata->data, hash_pool)); 1031 svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, 1032 svn_stringbuf__morph_into_string(filedata)); 1033 } 1034 else if (message) 1035 { 1036 svn_hash_sets(revprops, SVN_PROP_REVISION_LOG, 1037 svn_string_create(message, hash_pool)); 1038 } 1039 1040 return SVN_NO_ERROR; 1041} 1042 1043int 1044main(int argc, const char **argv) 1045{ 1046 apr_pool_t *pool = init("svnmucc"); 1047 apr_array_header_t *actions = apr_array_make(pool, 1, 1048 sizeof(struct action *)); 1049 const char *anchor = NULL; 1050 svn_error_t *err = SVN_NO_ERROR; 1051 apr_getopt_t *opts; 1052 enum { 1053 config_dir_opt = SVN_OPT_FIRST_LONGOPT_ID, 1054 config_inline_opt, 1055 no_auth_cache_opt, 1056 version_opt, 1057 with_revprop_opt, 1058 non_interactive_opt, 1059 force_interactive_opt, 1060 trust_server_cert_opt 1061 }; 1062 static const apr_getopt_option_t options[] = { 1063 {"message", 'm', 1, ""}, 1064 {"file", 'F', 1, ""}, 1065 {"username", 'u', 1, ""}, 1066 {"password", 'p', 1, ""}, 1067 {"root-url", 'U', 1, ""}, 1068 {"revision", 'r', 1, ""}, 1069 {"with-revprop", with_revprop_opt, 1, ""}, 1070 {"extra-args", 'X', 1, ""}, 1071 {"help", 'h', 0, ""}, 1072 {NULL, '?', 0, ""}, 1073 {"non-interactive", non_interactive_opt, 0, ""}, 1074 {"force-interactive", force_interactive_opt, 0, ""}, 1075 {"trust-server-cert", trust_server_cert_opt, 0, ""}, 1076 {"config-dir", config_dir_opt, 1, ""}, 1077 {"config-option", config_inline_opt, 1, ""}, 1078 {"no-auth-cache", no_auth_cache_opt, 0, ""}, 1079 {"version", version_opt, 0, ""}, 1080 {NULL, 0, 0, NULL} 1081 }; 1082 const char *message = NULL; 1083 svn_stringbuf_t *filedata = NULL; 1084 const char *username = NULL, *password = NULL; 1085 const char *root_url = NULL, *extra_args_file = NULL; 1086 const char *config_dir = NULL; 1087 apr_array_header_t *config_options; 1088 svn_boolean_t non_interactive = FALSE; 1089 svn_boolean_t force_interactive = FALSE; 1090 svn_boolean_t trust_server_cert = FALSE; 1091 svn_boolean_t no_auth_cache = FALSE; 1092 svn_revnum_t base_revision = SVN_INVALID_REVNUM; 1093 apr_array_header_t *action_args; 1094 apr_hash_t *revprops = apr_hash_make(pool); 1095 int i; 1096 1097 config_options = apr_array_make(pool, 0, 1098 sizeof(svn_cmdline__config_argument_t*)); 1099 1100 apr_getopt_init(&opts, pool, argc, argv); 1101 opts->interleave = 1; 1102 while (1) 1103 { 1104 int opt; 1105 const char *arg; 1106 const char *opt_arg; 1107 1108 apr_status_t status = apr_getopt_long(opts, options, &opt, &arg); 1109 if (APR_STATUS_IS_EOF(status)) 1110 break; 1111 if (status != APR_SUCCESS) 1112 handle_error(svn_error_wrap_apr(status, "getopt failure"), pool); 1113 switch(opt) 1114 { 1115 case 'm': 1116 err = svn_utf_cstring_to_utf8(&message, arg, pool); 1117 if (err) 1118 handle_error(err, pool); 1119 break; 1120 case 'F': 1121 { 1122 const char *arg_utf8; 1123 err = svn_utf_cstring_to_utf8(&arg_utf8, arg, pool); 1124 if (! err) 1125 err = svn_stringbuf_from_file2(&filedata, arg, pool); 1126 if (err) 1127 handle_error(err, pool); 1128 } 1129 break; 1130 case 'u': 1131 username = apr_pstrdup(pool, arg); 1132 break; 1133 case 'p': 1134 password = apr_pstrdup(pool, arg); 1135 break; 1136 case 'U': 1137 err = svn_utf_cstring_to_utf8(&root_url, arg, pool); 1138 if (err) 1139 handle_error(err, pool); 1140 if (! svn_path_is_url(root_url)) 1141 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, 1142 "'%s' is not a URL\n", root_url), 1143 pool); 1144 root_url = sanitize_url(root_url, pool); 1145 break; 1146 case 'r': 1147 { 1148 char *digits_end = NULL; 1149 base_revision = strtol(arg, &digits_end, 10); 1150 if ((! SVN_IS_VALID_REVNUM(base_revision)) 1151 || (! digits_end) 1152 || *digits_end) 1153 handle_error(svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, 1154 NULL, "Invalid revision number"), 1155 pool); 1156 } 1157 break; 1158 case with_revprop_opt: 1159 err = svn_opt_parse_revprop(&revprops, arg, pool); 1160 if (err != SVN_NO_ERROR) 1161 handle_error(err, pool); 1162 break; 1163 case 'X': 1164 extra_args_file = apr_pstrdup(pool, arg); 1165 break; 1166 case non_interactive_opt: 1167 non_interactive = TRUE; 1168 break; 1169 case force_interactive_opt: 1170 force_interactive = TRUE; 1171 break; 1172 case trust_server_cert_opt: 1173 trust_server_cert = TRUE; 1174 break; 1175 case config_dir_opt: 1176 err = svn_utf_cstring_to_utf8(&config_dir, arg, pool); 1177 if (err) 1178 handle_error(err, pool); 1179 break; 1180 case config_inline_opt: 1181 err = svn_utf_cstring_to_utf8(&opt_arg, arg, pool); 1182 if (err) 1183 handle_error(err, pool); 1184 1185 err = svn_cmdline__parse_config_option(config_options, opt_arg, 1186 pool); 1187 if (err) 1188 handle_error(err, pool); 1189 break; 1190 case no_auth_cache_opt: 1191 no_auth_cache = TRUE; 1192 break; 1193 case version_opt: 1194 SVN_INT_ERR(display_version(opts, pool)); 1195 exit(EXIT_SUCCESS); 1196 break; 1197 case 'h': 1198 case '?': 1199 usage(pool, EXIT_SUCCESS); 1200 break; 1201 } 1202 } 1203 1204 if (non_interactive && force_interactive) 1205 { 1206 err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 1207 _("--non-interactive and --force-interactive " 1208 "are mutually exclusive")); 1209 return svn_cmdline_handle_exit_error(err, pool, "svnmucc: "); 1210 } 1211 else 1212 non_interactive = !svn_cmdline__be_interactive(non_interactive, 1213 force_interactive); 1214 1215 if (trust_server_cert && !non_interactive) 1216 { 1217 err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 1218 _("--trust-server-cert requires " 1219 "--non-interactive")); 1220 return svn_cmdline_handle_exit_error(err, pool, "svnmucc: "); 1221 } 1222 1223 /* Make sure we have a log message to use. */ 1224 err = sanitize_log_sources(revprops, message, filedata); 1225 if (err) 1226 handle_error(err, pool); 1227 1228 /* Copy the rest of our command-line arguments to an array, 1229 UTF-8-ing them along the way. */ 1230 action_args = apr_array_make(pool, opts->argc, sizeof(const char *)); 1231 while (opts->ind < opts->argc) 1232 { 1233 const char *arg = opts->argv[opts->ind++]; 1234 if ((err = svn_utf_cstring_to_utf8(&(APR_ARRAY_PUSH(action_args, 1235 const char *)), 1236 arg, pool))) 1237 handle_error(err, pool); 1238 } 1239 1240 /* If there are extra arguments in a supplementary file, tack those 1241 on, too (again, in UTF8 form). */ 1242 if (extra_args_file) 1243 { 1244 const char *extra_args_file_utf8; 1245 svn_stringbuf_t *contents, *contents_utf8; 1246 1247 err = svn_utf_cstring_to_utf8(&extra_args_file_utf8, 1248 extra_args_file, pool); 1249 if (! err) 1250 err = svn_stringbuf_from_file2(&contents, extra_args_file_utf8, pool); 1251 if (! err) 1252 err = svn_utf_stringbuf_to_utf8(&contents_utf8, contents, pool); 1253 if (err) 1254 handle_error(err, pool); 1255 svn_cstring_split_append(action_args, contents_utf8->data, "\n\r", 1256 FALSE, pool); 1257 } 1258 1259 /* Now, we iterate over the combined set of arguments -- our actions. */ 1260 for (i = 0; i < action_args->nelts; ) 1261 { 1262 int j, num_url_args; 1263 const char *action_string = APR_ARRAY_IDX(action_args, i, const char *); 1264 struct action *action = apr_pcalloc(pool, sizeof(*action)); 1265 1266 /* First, parse the action. */ 1267 if (! strcmp(action_string, "mv")) 1268 action->action = ACTION_MV; 1269 else if (! strcmp(action_string, "cp")) 1270 action->action = ACTION_CP; 1271 else if (! strcmp(action_string, "mkdir")) 1272 action->action = ACTION_MKDIR; 1273 else if (! strcmp(action_string, "rm")) 1274 action->action = ACTION_RM; 1275 else if (! strcmp(action_string, "put")) 1276 action->action = ACTION_PUT; 1277 else if (! strcmp(action_string, "propset")) 1278 action->action = ACTION_PROPSET; 1279 else if (! strcmp(action_string, "propsetf")) 1280 action->action = ACTION_PROPSETF; 1281 else if (! strcmp(action_string, "propdel")) 1282 action->action = ACTION_PROPDEL; 1283 else if (! strcmp(action_string, "?") || ! strcmp(action_string, "h") 1284 || ! strcmp(action_string, "help")) 1285 usage(pool, EXIT_SUCCESS); 1286 else 1287 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, 1288 "'%s' is not an action\n", 1289 action_string), pool); 1290 if (++i == action_args->nelts) 1291 insufficient(pool); 1292 1293 /* For copies, there should be a revision number next. */ 1294 if (action->action == ACTION_CP) 1295 { 1296 const char *rev_str = APR_ARRAY_IDX(action_args, i, const char *); 1297 if (strcmp(rev_str, "head") == 0) 1298 action->rev = SVN_INVALID_REVNUM; 1299 else if (strcmp(rev_str, "HEAD") == 0) 1300 action->rev = SVN_INVALID_REVNUM; 1301 else 1302 { 1303 char *end; 1304 1305 while (*rev_str == 'r') 1306 ++rev_str; 1307 1308 action->rev = strtol(rev_str, &end, 0); 1309 if (*end) 1310 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, 1311 "'%s' is not a revision\n", 1312 rev_str), pool); 1313 } 1314 if (++i == action_args->nelts) 1315 insufficient(pool); 1316 } 1317 else 1318 { 1319 action->rev = SVN_INVALID_REVNUM; 1320 } 1321 1322 /* For puts, there should be a local file next. */ 1323 if (action->action == ACTION_PUT) 1324 { 1325 action->path[1] = 1326 svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i, 1327 const char *), pool); 1328 if (++i == action_args->nelts) 1329 insufficient(pool); 1330 } 1331 1332 /* For propset, propsetf, and propdel, a property name (and 1333 maybe a property value or file which contains one) comes next. */ 1334 if ((action->action == ACTION_PROPSET) 1335 || (action->action == ACTION_PROPSETF) 1336 || (action->action == ACTION_PROPDEL)) 1337 { 1338 action->prop_name = APR_ARRAY_IDX(action_args, i, const char *); 1339 if (++i == action_args->nelts) 1340 insufficient(pool); 1341 1342 if (action->action == ACTION_PROPDEL) 1343 { 1344 action->prop_value = NULL; 1345 } 1346 else if (action->action == ACTION_PROPSET) 1347 { 1348 action->prop_value = 1349 svn_string_create(APR_ARRAY_IDX(action_args, i, 1350 const char *), pool); 1351 if (++i == action_args->nelts) 1352 insufficient(pool); 1353 } 1354 else 1355 { 1356 const char *propval_file = 1357 svn_dirent_internal_style(APR_ARRAY_IDX(action_args, i, 1358 const char *), pool); 1359 1360 if (++i == action_args->nelts) 1361 insufficient(pool); 1362 1363 err = read_propvalue_file(&(action->prop_value), 1364 propval_file, pool); 1365 if (err) 1366 handle_error(err, pool); 1367 1368 action->action = ACTION_PROPSET; 1369 } 1370 1371 if (action->prop_value 1372 && svn_prop_needs_translation(action->prop_name)) 1373 { 1374 svn_string_t *translated_value; 1375 err = svn_subst_translate_string2(&translated_value, NULL, 1376 NULL, action->prop_value, NULL, 1377 FALSE, pool, pool); 1378 if (err) 1379 handle_error( 1380 svn_error_quick_wrap(err, 1381 "Error normalizing property value"), 1382 pool); 1383 action->prop_value = translated_value; 1384 } 1385 } 1386 1387 /* How many URLs does this action expect? */ 1388 if (action->action == ACTION_RM 1389 || action->action == ACTION_MKDIR 1390 || action->action == ACTION_PUT 1391 || action->action == ACTION_PROPSET 1392 || action->action == ACTION_PROPSETF /* shouldn't see this one */ 1393 || action->action == ACTION_PROPDEL) 1394 num_url_args = 1; 1395 else 1396 num_url_args = 2; 1397 1398 /* Parse the required number of URLs. */ 1399 for (j = 0; j < num_url_args; ++j) 1400 { 1401 const char *url = APR_ARRAY_IDX(action_args, i, const char *); 1402 1403 /* If there's a ROOT_URL, we expect URL to be a path 1404 relative to ROOT_URL (and we build a full url from the 1405 combination of the two). Otherwise, it should be a full 1406 url. */ 1407 if (! svn_path_is_url(url)) 1408 { 1409 if (! root_url) 1410 handle_error(svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, 1411 "'%s' is not a URL, and " 1412 "--root-url (-U) not provided\n", 1413 url), pool); 1414 /* ### These relpaths are already URI-encoded. */ 1415 url = apr_pstrcat(pool, root_url, "/", 1416 svn_relpath_canonicalize(url, pool), 1417 (char *)NULL); 1418 } 1419 url = sanitize_url(url, pool); 1420 action->path[j] = url; 1421 1422 /* The first URL arguments to 'cp', 'pd', 'ps' could be the anchor, 1423 but the other URLs should be children of the anchor. */ 1424 if (! (action->action == ACTION_CP && j == 0) 1425 && action->action != ACTION_PROPDEL 1426 && action->action != ACTION_PROPSET 1427 && action->action != ACTION_PROPSETF) 1428 url = svn_uri_dirname(url, pool); 1429 if (! anchor) 1430 anchor = url; 1431 else 1432 anchor = svn_uri_get_longest_ancestor(anchor, url, pool); 1433 1434 if ((++i == action_args->nelts) && (j + 1 < num_url_args)) 1435 insufficient(pool); 1436 } 1437 APR_ARRAY_PUSH(actions, struct action *) = action; 1438 } 1439 1440 if (! actions->nelts) 1441 usage(pool, EXIT_FAILURE); 1442 1443 if ((err = execute(actions, anchor, revprops, username, password, 1444 config_dir, config_options, non_interactive, 1445 trust_server_cert, no_auth_cache, base_revision, pool))) 1446 { 1447 if (err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive) 1448 err = svn_error_quick_wrap(err, 1449 _("Authentication failed and interactive" 1450 " prompting is disabled; see the" 1451 " --force-interactive option")); 1452 handle_error(err, pool); 1453 } 1454 1455 /* Ensure that stdout is flushed, so the user will see all results. */ 1456 svn_error_clear(svn_cmdline_fflush(stdout)); 1457 1458 svn_pool_destroy(pool); 1459 return EXIT_SUCCESS; 1460} 1461