1/* 2 * info-cmd.c -- Display information about a resource 3 * 4 * ==================================================================== 5 * Licensed to the Apache Software Foundation (ASF) under one 6 * or more contributor license agreements. See the NOTICE file 7 * distributed with this work for additional information 8 * regarding copyright ownership. The ASF licenses this file 9 * to you under the Apache License, Version 2.0 (the 10 * "License"); you may not use this file except in compliance 11 * with the License. You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, 16 * software distributed under the License is distributed on an 17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 * KIND, either express or implied. See the License for the 19 * specific language governing permissions and limitations 20 * under the License. 21 * ==================================================================== 22 */ 23 24/* ==================================================================== */ 25 26 27 28/*** Includes. ***/ 29 30#include "svn_string.h" 31#include "svn_cmdline.h" 32#include "svn_wc.h" 33#include "svn_pools.h" 34#include "svn_error_codes.h" 35#include "svn_error.h" 36#include "svn_dirent_uri.h" 37#include "svn_path.h" 38#include "svn_time.h" 39#include "svn_xml.h" 40#include "cl.h" 41 42#include "svn_private_config.h" 43#include "cl-conflicts.h" 44 45 46/*** Code. ***/ 47 48static svn_error_t * 49svn_cl__info_print_time(apr_time_t atime, 50 const char *desc, 51 apr_pool_t *pool) 52{ 53 const char *time_utf8; 54 55 time_utf8 = svn_time_to_human_cstring(atime, pool); 56 return svn_cmdline_printf(pool, "%s: %s\n", desc, time_utf8); 57} 58 59 60/* Return string representation of SCHEDULE */ 61static const char * 62schedule_str(svn_wc_schedule_t schedule) 63{ 64 switch (schedule) 65 { 66 case svn_wc_schedule_normal: 67 return "normal"; 68 case svn_wc_schedule_add: 69 return "add"; 70 case svn_wc_schedule_delete: 71 return "delete"; 72 case svn_wc_schedule_replace: 73 return "replace"; 74 default: 75 return "none"; 76 } 77} 78 79 80/* A callback of type svn_client_info_receiver2_t. 81 Prints svn info in xml mode to standard out */ 82static svn_error_t * 83print_info_xml(void *baton, 84 const char *target, 85 const svn_client_info2_t *info, 86 apr_pool_t *pool) 87{ 88 svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool); 89 const char *rev_str; 90 const char *path_prefix = baton; 91 92 if (SVN_IS_VALID_REVNUM(info->rev)) 93 rev_str = apr_psprintf(pool, "%ld", info->rev); 94 else 95 rev_str = apr_pstrdup(pool, _("Resource is not under version control.")); 96 97 /* "<entry ...>" */ 98 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "entry", 99 "path", svn_cl__local_style_skip_ancestor( 100 path_prefix, target, pool), 101 "kind", svn_cl__node_kind_str_xml(info->kind), 102 "revision", rev_str, 103 NULL); 104 105 /* "<url> xx </url>" */ 106 svn_cl__xml_tagged_cdata(&sb, pool, "url", info->URL); 107 108 if (info->repos_root_URL && info->URL) 109 { 110 /* "<relative-url> xx </relative-url>" */ 111 svn_cl__xml_tagged_cdata(&sb, pool, "relative-url", 112 apr_pstrcat(pool, "^/", 113 svn_path_uri_encode( 114 svn_uri_skip_ancestor( 115 info->repos_root_URL, 116 info->URL, pool), 117 pool), 118 NULL)); 119 } 120 121 if (info->repos_root_URL || info->repos_UUID) 122 { 123 /* "<repository>" */ 124 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "repository", NULL); 125 126 /* "<root> xx </root>" */ 127 svn_cl__xml_tagged_cdata(&sb, pool, "root", info->repos_root_URL); 128 129 /* "<uuid> xx </uuid>" */ 130 svn_cl__xml_tagged_cdata(&sb, pool, "uuid", info->repos_UUID); 131 132 /* "</repository>" */ 133 svn_xml_make_close_tag(&sb, pool, "repository"); 134 } 135 136 if (info->wc_info) 137 { 138 /* "<wc-info>" */ 139 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "wc-info", NULL); 140 141 /* "<wcroot-abspath> xx </wcroot-abspath>" */ 142 if (info->wc_info->wcroot_abspath) 143 svn_cl__xml_tagged_cdata(&sb, pool, "wcroot-abspath", 144 info->wc_info->wcroot_abspath); 145 146 /* "<schedule> xx </schedule>" */ 147 svn_cl__xml_tagged_cdata(&sb, pool, "schedule", 148 schedule_str(info->wc_info->schedule)); 149 150 /* "<depth> xx </depth>" */ 151 { 152 svn_depth_t depth = info->wc_info->depth; 153 154 /* In the entries world info just passed depth infinity for files */ 155 if (depth == svn_depth_unknown && info->kind == svn_node_file) 156 depth = svn_depth_infinity; 157 158 svn_cl__xml_tagged_cdata(&sb, pool, "depth", svn_depth_to_word(depth)); 159 } 160 161 /* "<copy-from-url> xx </copy-from-url>" */ 162 svn_cl__xml_tagged_cdata(&sb, pool, "copy-from-url", 163 info->wc_info->copyfrom_url); 164 165 /* "<copy-from-rev> xx </copy-from-rev>" */ 166 if (SVN_IS_VALID_REVNUM(info->wc_info->copyfrom_rev)) 167 svn_cl__xml_tagged_cdata(&sb, pool, "copy-from-rev", 168 apr_psprintf(pool, "%ld", 169 info->wc_info->copyfrom_rev)); 170 171 /* "<text-updated> xx </text-updated>" */ 172 if (info->wc_info->recorded_time) 173 svn_cl__xml_tagged_cdata(&sb, pool, "text-updated", 174 svn_time_to_cstring( 175 info->wc_info->recorded_time, 176 pool)); 177 178 /* "<checksum> xx </checksum>" */ 179 /* ### Print the checksum kind. */ 180 svn_cl__xml_tagged_cdata(&sb, pool, "checksum", 181 svn_checksum_to_cstring(info->wc_info->checksum, 182 pool)); 183 184 if (info->wc_info->changelist) 185 /* "<changelist> xx </changelist>" */ 186 svn_cl__xml_tagged_cdata(&sb, pool, "changelist", 187 info->wc_info->changelist); 188 189 if (info->wc_info->moved_from_abspath) 190 { 191 const char *relpath; 192 193 relpath = svn_dirent_skip_ancestor(info->wc_info->wcroot_abspath, 194 info->wc_info->moved_from_abspath); 195 196 /* <moved-from> xx </moved-from> */ 197 if (relpath && relpath[0] != '\0') 198 svn_cl__xml_tagged_cdata(&sb, pool, "moved-from", relpath); 199 else 200 svn_cl__xml_tagged_cdata(&sb, pool, "moved-from", 201 info->wc_info->moved_from_abspath); 202 } 203 204 if (info->wc_info->moved_to_abspath) 205 { 206 const char *relpath; 207 208 relpath = svn_dirent_skip_ancestor(info->wc_info->wcroot_abspath, 209 info->wc_info->moved_to_abspath); 210 /* <moved-to> xx </moved-to> */ 211 if (relpath && relpath[0] != '\0') 212 svn_cl__xml_tagged_cdata(&sb, pool, "moved-to", relpath); 213 else 214 svn_cl__xml_tagged_cdata(&sb, pool, "moved-to", 215 info->wc_info->moved_to_abspath); 216 } 217 218 /* "</wc-info>" */ 219 svn_xml_make_close_tag(&sb, pool, "wc-info"); 220 } 221 222 if (info->last_changed_author 223 || SVN_IS_VALID_REVNUM(info->last_changed_rev) 224 || info->last_changed_date) 225 { 226 svn_cl__print_xml_commit(&sb, info->last_changed_rev, 227 info->last_changed_author, 228 svn_time_to_cstring(info->last_changed_date, 229 pool), 230 pool); 231 } 232 233 if (info->wc_info && info->wc_info->conflicts) 234 { 235 int i; 236 237 for (i = 0; i < info->wc_info->conflicts->nelts; i++) 238 { 239 const svn_wc_conflict_description2_t *conflict = 240 APR_ARRAY_IDX(info->wc_info->conflicts, i, 241 const svn_wc_conflict_description2_t *); 242 243 SVN_ERR(svn_cl__append_conflict_info_xml(sb, conflict, pool)); 244 } 245 } 246 247 if (info->lock) 248 svn_cl__print_xml_lock(&sb, info->lock, pool); 249 250 /* "</entry>" */ 251 svn_xml_make_close_tag(&sb, pool, "entry"); 252 253 return svn_cl__error_checked_fputs(sb->data, stdout); 254} 255 256 257/* A callback of type svn_client_info_receiver2_t. */ 258static svn_error_t * 259print_info(void *baton, 260 const char *target, 261 const svn_client_info2_t *info, 262 apr_pool_t *pool) 263{ 264 const char *path_prefix = baton; 265 266 SVN_ERR(svn_cmdline_printf(pool, _("Path: %s\n"), 267 svn_cl__local_style_skip_ancestor( 268 path_prefix, target, pool))); 269 270 /* ### remove this someday: it's only here for cmdline output 271 compatibility with svn 1.1 and older. */ 272 if (info->kind != svn_node_dir) 273 SVN_ERR(svn_cmdline_printf(pool, _("Name: %s\n"), 274 svn_dirent_basename(target, pool))); 275 276 if (info->wc_info && info->wc_info->wcroot_abspath) 277 SVN_ERR(svn_cmdline_printf(pool, _("Working Copy Root Path: %s\n"), 278 svn_dirent_local_style( 279 info->wc_info->wcroot_abspath, 280 pool))); 281 282 if (info->URL) 283 SVN_ERR(svn_cmdline_printf(pool, _("URL: %s\n"), info->URL)); 284 285 if (info->URL && info->repos_root_URL) 286 SVN_ERR(svn_cmdline_printf(pool, _("Relative URL: ^/%s\n"), 287 svn_path_uri_encode( 288 svn_uri_skip_ancestor(info->repos_root_URL, 289 info->URL, pool), 290 pool))); 291 292 if (info->repos_root_URL) 293 SVN_ERR(svn_cmdline_printf(pool, _("Repository Root: %s\n"), 294 info->repos_root_URL)); 295 296 if (info->repos_UUID) 297 SVN_ERR(svn_cmdline_printf(pool, _("Repository UUID: %s\n"), 298 info->repos_UUID)); 299 300 if (SVN_IS_VALID_REVNUM(info->rev)) 301 SVN_ERR(svn_cmdline_printf(pool, _("Revision: %ld\n"), info->rev)); 302 303 switch (info->kind) 304 { 305 case svn_node_file: 306 SVN_ERR(svn_cmdline_printf(pool, _("Node Kind: file\n"))); 307 break; 308 309 case svn_node_dir: 310 SVN_ERR(svn_cmdline_printf(pool, _("Node Kind: directory\n"))); 311 break; 312 313 case svn_node_none: 314 SVN_ERR(svn_cmdline_printf(pool, _("Node Kind: none\n"))); 315 break; 316 317 case svn_node_unknown: 318 default: 319 SVN_ERR(svn_cmdline_printf(pool, _("Node Kind: unknown\n"))); 320 break; 321 } 322 323 if (info->wc_info) 324 { 325 switch (info->wc_info->schedule) 326 { 327 case svn_wc_schedule_normal: 328 SVN_ERR(svn_cmdline_printf(pool, _("Schedule: normal\n"))); 329 break; 330 331 case svn_wc_schedule_add: 332 SVN_ERR(svn_cmdline_printf(pool, _("Schedule: add\n"))); 333 break; 334 335 case svn_wc_schedule_delete: 336 SVN_ERR(svn_cmdline_printf(pool, _("Schedule: delete\n"))); 337 break; 338 339 case svn_wc_schedule_replace: 340 SVN_ERR(svn_cmdline_printf(pool, _("Schedule: replace\n"))); 341 break; 342 343 default: 344 break; 345 } 346 347 switch (info->wc_info->depth) 348 { 349 case svn_depth_unknown: 350 /* Unknown depth is the norm for remote directories anyway 351 (although infinity would be equally appropriate). Let's 352 not bother to print it. */ 353 break; 354 355 case svn_depth_empty: 356 SVN_ERR(svn_cmdline_printf(pool, _("Depth: empty\n"))); 357 break; 358 359 case svn_depth_files: 360 SVN_ERR(svn_cmdline_printf(pool, _("Depth: files\n"))); 361 break; 362 363 case svn_depth_immediates: 364 SVN_ERR(svn_cmdline_printf(pool, _("Depth: immediates\n"))); 365 break; 366 367 case svn_depth_exclude: 368 SVN_ERR(svn_cmdline_printf(pool, _("Depth: exclude\n"))); 369 break; 370 371 case svn_depth_infinity: 372 /* Infinity is the default depth for working copy 373 directories. Let's not print it, it's not special enough 374 to be worth mentioning. */ 375 break; 376 377 default: 378 /* Other depths should never happen here. */ 379 SVN_ERR(svn_cmdline_printf(pool, _("Depth: INVALID\n"))); 380 } 381 382 if (info->wc_info->copyfrom_url) 383 SVN_ERR(svn_cmdline_printf(pool, _("Copied From URL: %s\n"), 384 info->wc_info->copyfrom_url)); 385 386 if (SVN_IS_VALID_REVNUM(info->wc_info->copyfrom_rev)) 387 SVN_ERR(svn_cmdline_printf(pool, _("Copied From Rev: %ld\n"), 388 info->wc_info->copyfrom_rev)); 389 if (info->wc_info->moved_from_abspath) 390 { 391 const char *relpath; 392 393 relpath = svn_dirent_skip_ancestor(info->wc_info->wcroot_abspath, 394 info->wc_info->moved_from_abspath); 395 if (relpath && relpath[0] != '\0') 396 SVN_ERR(svn_cmdline_printf(pool, _("Moved From: %s\n"), relpath)); 397 else 398 SVN_ERR(svn_cmdline_printf(pool, _("Moved From: %s\n"), 399 info->wc_info->moved_from_abspath)); 400 } 401 402 if (info->wc_info->moved_to_abspath) 403 { 404 const char *relpath; 405 406 relpath = svn_dirent_skip_ancestor(info->wc_info->wcroot_abspath, 407 info->wc_info->moved_to_abspath); 408 if (relpath && relpath[0] != '\0') 409 SVN_ERR(svn_cmdline_printf(pool, _("Moved To: %s\n"), relpath)); 410 else 411 SVN_ERR(svn_cmdline_printf(pool, _("Moved To: %s\n"), 412 info->wc_info->moved_to_abspath)); 413 } 414 } 415 416 if (info->last_changed_author) 417 SVN_ERR(svn_cmdline_printf(pool, _("Last Changed Author: %s\n"), 418 info->last_changed_author)); 419 420 if (SVN_IS_VALID_REVNUM(info->last_changed_rev)) 421 SVN_ERR(svn_cmdline_printf(pool, _("Last Changed Rev: %ld\n"), 422 info->last_changed_rev)); 423 424 if (info->last_changed_date) 425 SVN_ERR(svn_cl__info_print_time(info->last_changed_date, 426 _("Last Changed Date"), pool)); 427 428 if (info->wc_info) 429 { 430 if (info->wc_info->recorded_time) 431 SVN_ERR(svn_cl__info_print_time(info->wc_info->recorded_time, 432 _("Text Last Updated"), pool)); 433 434 if (info->wc_info->checksum) 435 SVN_ERR(svn_cmdline_printf(pool, _("Checksum: %s\n"), 436 svn_checksum_to_cstring( 437 info->wc_info->checksum, pool))); 438 439 if (info->wc_info->conflicts) 440 { 441 svn_boolean_t printed_prop_conflict_file = FALSE; 442 int i; 443 444 for (i = 0; i < info->wc_info->conflicts->nelts; i++) 445 { 446 const svn_wc_conflict_description2_t *conflict = 447 APR_ARRAY_IDX(info->wc_info->conflicts, i, 448 const svn_wc_conflict_description2_t *); 449 const char *desc; 450 451 switch (conflict->kind) 452 { 453 case svn_wc_conflict_kind_text: 454 if (conflict->base_abspath) 455 SVN_ERR(svn_cmdline_printf(pool, 456 _("Conflict Previous Base File: %s\n"), 457 svn_cl__local_style_skip_ancestor( 458 path_prefix, conflict->base_abspath, 459 pool))); 460 461 if (conflict->my_abspath) 462 SVN_ERR(svn_cmdline_printf(pool, 463 _("Conflict Previous Working File: %s\n"), 464 svn_cl__local_style_skip_ancestor( 465 path_prefix, conflict->my_abspath, 466 pool))); 467 468 if (conflict->their_abspath) 469 SVN_ERR(svn_cmdline_printf(pool, 470 _("Conflict Current Base File: %s\n"), 471 svn_cl__local_style_skip_ancestor( 472 path_prefix, conflict->their_abspath, 473 pool))); 474 break; 475 476 case svn_wc_conflict_kind_property: 477 if (! printed_prop_conflict_file) 478 SVN_ERR(svn_cmdline_printf(pool, 479 _("Conflict Properties File: %s\n"), 480 svn_dirent_local_style(conflict->their_abspath, 481 pool))); 482 printed_prop_conflict_file = TRUE; 483 break; 484 485 case svn_wc_conflict_kind_tree: 486 SVN_ERR( 487 svn_cl__get_human_readable_tree_conflict_description( 488 &desc, conflict, pool)); 489 490 SVN_ERR(svn_cmdline_printf(pool, "%s: %s\n", 491 _("Tree conflict"), desc)); 492 break; 493 } 494 } 495 496 /* We only store one left and right version for all conflicts, which is 497 referenced from all conflicts. 498 Print it after the conflicts to match the 1.6/1.7 output where it is 499 only available for tree conflicts */ 500 { 501 const char *src_left_version; 502 const char *src_right_version; 503 const svn_wc_conflict_description2_t *conflict = 504 APR_ARRAY_IDX(info->wc_info->conflicts, 0, 505 const svn_wc_conflict_description2_t *); 506 507 src_left_version = 508 svn_cl__node_description(conflict->src_left_version, 509 info->repos_root_URL, pool); 510 511 src_right_version = 512 svn_cl__node_description(conflict->src_right_version, 513 info->repos_root_URL, pool); 514 515 if (src_left_version) 516 SVN_ERR(svn_cmdline_printf(pool, " %s: %s\n", 517 _("Source left"), /* (1) */ 518 src_left_version)); 519 /* (1): Sneaking in a space in "Source left" so that 520 * it is the same length as "Source right" while it still 521 * starts in the same column. That's just a tiny tweak in 522 * the English `svn'. */ 523 524 if (src_right_version) 525 SVN_ERR(svn_cmdline_printf(pool, " %s: %s\n", 526 _("Source right"), 527 src_right_version)); 528 } 529 } 530 } 531 532 if (info->lock) 533 { 534 if (info->lock->token) 535 SVN_ERR(svn_cmdline_printf(pool, _("Lock Token: %s\n"), 536 info->lock->token)); 537 538 if (info->lock->owner) 539 SVN_ERR(svn_cmdline_printf(pool, _("Lock Owner: %s\n"), 540 info->lock->owner)); 541 542 if (info->lock->creation_date) 543 SVN_ERR(svn_cl__info_print_time(info->lock->creation_date, 544 _("Lock Created"), pool)); 545 546 if (info->lock->expiration_date) 547 SVN_ERR(svn_cl__info_print_time(info->lock->expiration_date, 548 _("Lock Expires"), pool)); 549 550 if (info->lock->comment) 551 { 552 int comment_lines; 553 /* NOTE: The stdio will handle newline translation. */ 554 comment_lines = svn_cstring_count_newlines(info->lock->comment) + 1; 555 SVN_ERR(svn_cmdline_printf(pool, 556 Q_("Lock Comment (%i line):\n%s\n", 557 "Lock Comment (%i lines):\n%s\n", 558 comment_lines), 559 comment_lines, 560 info->lock->comment)); 561 } 562 } 563 564 if (info->wc_info && info->wc_info->changelist) 565 SVN_ERR(svn_cmdline_printf(pool, _("Changelist: %s\n"), 566 info->wc_info->changelist)); 567 568 /* Print extra newline separator. */ 569 return svn_cmdline_printf(pool, "\n"); 570} 571 572 573/* This implements the `svn_opt_subcommand_t' interface. */ 574svn_error_t * 575svn_cl__info(apr_getopt_t *os, 576 void *baton, 577 apr_pool_t *pool) 578{ 579 svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state; 580 svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx; 581 apr_array_header_t *targets = NULL; 582 apr_pool_t *subpool = svn_pool_create(pool); 583 int i; 584 svn_error_t *err; 585 svn_boolean_t seen_nonexistent_target = FALSE; 586 svn_opt_revision_t peg_revision; 587 svn_client_info_receiver2_t receiver; 588 const char *path_prefix; 589 590 SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os, 591 opt_state->targets, 592 ctx, FALSE, pool)); 593 594 /* Add "." if user passed 0 arguments. */ 595 svn_opt_push_implicit_dot_target(targets, pool); 596 597 if (opt_state->xml) 598 { 599 receiver = print_info_xml; 600 601 /* If output is not incremental, output the XML header and wrap 602 everything in a top-level element. This makes the output in 603 its entirety a well-formed XML document. */ 604 if (! opt_state->incremental) 605 SVN_ERR(svn_cl__xml_print_header("info", pool)); 606 } 607 else 608 { 609 receiver = print_info; 610 611 if (opt_state->incremental) 612 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 613 _("'incremental' option only valid in XML " 614 "mode")); 615 } 616 617 if (opt_state->depth == svn_depth_unknown) 618 opt_state->depth = svn_depth_empty; 619 620 SVN_ERR(svn_dirent_get_absolute(&path_prefix, "", pool)); 621 622 for (i = 0; i < targets->nelts; i++) 623 { 624 const char *truepath; 625 const char *target = APR_ARRAY_IDX(targets, i, const char *); 626 627 svn_pool_clear(subpool); 628 SVN_ERR(svn_cl__check_cancel(ctx->cancel_baton)); 629 630 /* Get peg revisions. */ 631 SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, target, subpool)); 632 633 /* If no peg-rev was attached to a URL target, then assume HEAD. */ 634 if (svn_path_is_url(truepath)) 635 { 636 if (peg_revision.kind == svn_opt_revision_unspecified) 637 peg_revision.kind = svn_opt_revision_head; 638 } 639 else 640 { 641 SVN_ERR(svn_dirent_get_absolute(&truepath, truepath, subpool)); 642 } 643 644 err = svn_client_info3(truepath, 645 &peg_revision, &(opt_state->start_revision), 646 opt_state->depth, TRUE, TRUE, 647 opt_state->changelists, 648 receiver, (void *) path_prefix, 649 ctx, subpool); 650 651 if (err) 652 { 653 /* If one of the targets is a non-existent URL or wc-entry, 654 don't bail out. Just warn and move on to the next target. */ 655 if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND || 656 err->apr_err == SVN_ERR_RA_ILLEGAL_URL) 657 { 658 svn_handle_warning2(stderr, err, "svn: "); 659 svn_error_clear(svn_cmdline_fprintf(stderr, subpool, "\n")); 660 } 661 else 662 { 663 return svn_error_trace(err); 664 } 665 666 svn_error_clear(err); 667 err = NULL; 668 seen_nonexistent_target = TRUE; 669 } 670 } 671 svn_pool_destroy(subpool); 672 673 if (opt_state->xml && (! opt_state->incremental)) 674 SVN_ERR(svn_cl__xml_print_footer("info", pool)); 675 676 if (seen_nonexistent_target) 677 return svn_error_create( 678 SVN_ERR_ILLEGAL_TARGET, NULL, 679 _("Could not display info for all targets because some " 680 "targets don't exist")); 681 else 682 return SVN_NO_ERROR; 683} 684