1/* 2 * util.c: Subversion command line client utility functions. Any 3 * functions that need to be shared across subcommands should be put 4 * in here. 5 * 6 * ==================================================================== 7 * Licensed to the Apache Software Foundation (ASF) under one 8 * or more contributor license agreements. See the NOTICE file 9 * distributed with this work for additional information 10 * regarding copyright ownership. The ASF licenses this file 11 * to you under the Apache License, Version 2.0 (the 12 * "License"); you may not use this file except in compliance 13 * with the License. You may obtain a copy of the License at 14 * 15 * http://www.apache.org/licenses/LICENSE-2.0 16 * 17 * Unless required by applicable law or agreed to in writing, 18 * software distributed under the License is distributed on an 19 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 * KIND, either express or implied. See the License for the 21 * specific language governing permissions and limitations 22 * under the License. 23 * ==================================================================== 24 */ 25 26/* ==================================================================== */ 27 28 29 30/*** Includes. ***/ 31 32#include <string.h> 33#include <ctype.h> 34#include <assert.h> 35 36#include <apr_env.h> 37#include <apr_errno.h> 38#include <apr_file_info.h> 39#include <apr_strings.h> 40#include <apr_tables.h> 41#include <apr_general.h> 42#include <apr_lib.h> 43 44#include "svn_pools.h" 45#include "svn_error.h" 46#include "svn_ctype.h" 47#include "svn_client.h" 48#include "svn_cmdline.h" 49#include "svn_string.h" 50#include "svn_dirent_uri.h" 51#include "svn_path.h" 52#include "svn_hash.h" 53#include "svn_io.h" 54#include "svn_utf.h" 55#include "svn_subst.h" 56#include "svn_config.h" 57#include "svn_wc.h" 58#include "svn_xml.h" 59#include "svn_time.h" 60#include "svn_props.h" 61#include "svn_private_config.h" 62#include "cl.h" 63 64#include "private/svn_token.h" 65#include "private/svn_opt_private.h" 66#include "private/svn_client_private.h" 67#include "private/svn_cmdline_private.h" 68#include "private/svn_string_private.h" 69#ifdef HAS_ORGANIZATION_NAME 70#include "freebsd-organization.h" 71#endif 72 73 74 75 76svn_error_t * 77svn_cl__print_commit_info(const svn_commit_info_t *commit_info, 78 void *baton, 79 apr_pool_t *pool) 80{ 81 /* Be very careful with returning errors from this callback as those 82 will be returned as errors from editor->close_edit(...), which may 83 cause callers to assume that the commit itself failed. 84 85 See log message of r1659867 and the svn_ra_get_commit_editor3 86 documentation for details on error scenarios. */ 87 88 if (SVN_IS_VALID_REVNUM(commit_info->revision)) 89 SVN_ERR(svn_cmdline_printf(pool, _("Committed revision %ld%s.\n"), 90 commit_info->revision, 91 commit_info->revision == 42 && 92 getenv("SVN_I_LOVE_PANGALACTIC_GARGLE_BLASTERS") 93 ? _(" (the answer to life, the universe, " 94 "and everything)") 95 : "")); 96 97 /* Writing to stdout, as there maybe systems that consider the 98 * presence of stderr as an indication of commit failure. 99 * OTOH, this is only of informational nature to the user as 100 * the commit has succeeded. */ 101 if (commit_info->post_commit_err) 102 SVN_ERR(svn_cmdline_printf(pool, _("\nWarning: %s\n"), 103 commit_info->post_commit_err)); 104 105 return SVN_NO_ERROR; 106} 107 108 109svn_error_t * 110svn_cl__merge_file_externally(const char *base_path, 111 const char *their_path, 112 const char *my_path, 113 const char *merged_path, 114 const char *wc_path, 115 apr_hash_t *config, 116 svn_boolean_t *remains_in_conflict, 117 apr_pool_t *pool) 118{ 119 char *merge_tool; 120 /* Error if there is no editor specified */ 121 if (apr_env_get(&merge_tool, "SVN_MERGE", pool) != APR_SUCCESS) 122 { 123 struct svn_config_t *cfg; 124 merge_tool = NULL; 125 cfg = config ? svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG) : NULL; 126 /* apr_env_get wants char **, this wants const char ** */ 127 svn_config_get(cfg, (const char **)&merge_tool, 128 SVN_CONFIG_SECTION_HELPERS, 129 SVN_CONFIG_OPTION_MERGE_TOOL_CMD, NULL); 130 } 131 132 if (merge_tool) 133 { 134 const char *c; 135 136 for (c = merge_tool; *c; c++) 137 if (!svn_ctype_isspace(*c)) 138 break; 139 140 if (! *c) 141 return svn_error_create 142 (SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL, NULL, 143 _("The SVN_MERGE environment variable is empty or " 144 "consists solely of whitespace. Expected a shell command.\n")); 145 } 146 else 147 return svn_error_create 148 (SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL, NULL, 149 _("The environment variable SVN_MERGE and the merge-tool-cmd run-time " 150 "configuration option were not set.\n")); 151 152 { 153 const char *arguments[7] = { 0 }; 154 char *cwd; 155 int exitcode; 156 157 apr_status_t status = apr_filepath_get(&cwd, APR_FILEPATH_NATIVE, pool); 158 if (status != 0) 159 return svn_error_wrap_apr(status, NULL); 160 161 arguments[0] = merge_tool; 162 arguments[1] = base_path; 163 arguments[2] = their_path; 164 arguments[3] = my_path; 165 arguments[4] = merged_path; 166 arguments[5] = wc_path; 167 arguments[6] = NULL; 168 169 /* Presumably apr_filepath_get() returns a valid path, so we don't have 170 to use the safe version of svn_dirent_internal_style() here. */ 171 SVN_ERR(svn_io_run_cmd(svn_dirent_internal_style(cwd, pool), merge_tool, 172 arguments, &exitcode, NULL, TRUE, NULL, NULL, NULL, 173 pool)); 174 /* Exit code 0 means the merge was successful. 175 * Exit code 1 means the file was left in conflict but it 176 * is OK to continue with the merge. 177 * Any other exit code means there was a real problem. */ 178 if (exitcode != 0 && exitcode != 1) 179 return svn_error_createf(SVN_ERR_EXTERNAL_PROGRAM, NULL, 180 _("The external merge tool '%s' exited with exit code %d."), 181 merge_tool, exitcode); 182 else if (remains_in_conflict) 183 *remains_in_conflict = exitcode == 1; 184 } 185 return SVN_NO_ERROR; 186} 187 188 189/* A svn_client_ctx_t's log_msg_baton3, for use with 190 svn_cl__make_log_msg_baton(). */ 191struct log_msg_baton 192{ 193 const char *editor_cmd; /* editor specified via --editor-cmd, else NULL */ 194 const char *message; /* the message. */ 195 const char *message_encoding; /* the locale/encoding of the message. */ 196 const char *base_dir; /* the base directory for an external edit. UTF-8! */ 197 const char *tmpfile_left; /* the tmpfile left by an external edit. UTF-8! */ 198 svn_boolean_t non_interactive; /* if true, don't pop up an editor */ 199 apr_hash_t *config; /* client configuration hash */ 200 svn_boolean_t keep_locks; /* Keep repository locks? */ 201 apr_pool_t *pool; /* a pool. */ 202}; 203 204 205svn_error_t * 206svn_cl__make_log_msg_baton(void **baton, 207 svn_cl__opt_state_t *opt_state, 208 const char *base_dir /* UTF-8! */, 209 apr_hash_t *config, 210 apr_pool_t *pool) 211{ 212 struct log_msg_baton *lmb = apr_pcalloc(pool, sizeof(*lmb)); 213 214 if (opt_state->filedata) 215 { 216 if (strlen(opt_state->filedata->data) < opt_state->filedata->len) 217 { 218 /* The data contains a zero byte, and therefore can't be 219 represented as a C string. Punt now; it's probably not 220 a deliberate encoding, and even if it is, we still 221 can't handle it. */ 222 return svn_error_create(SVN_ERR_CL_BAD_LOG_MESSAGE, NULL, 223 _("Log message contains a zero byte")); 224 } 225 lmb->message = opt_state->filedata->data; 226 } 227 else 228 { 229 lmb->message = opt_state->message; 230 } 231 232 lmb->editor_cmd = opt_state->editor_cmd; 233 if (opt_state->encoding) 234 { 235 lmb->message_encoding = opt_state->encoding; 236 } 237 else if (config) 238 { 239 svn_config_t *cfg = svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG); 240 svn_config_get(cfg, &(lmb->message_encoding), 241 SVN_CONFIG_SECTION_MISCELLANY, 242 SVN_CONFIG_OPTION_LOG_ENCODING, 243 NULL); 244 } 245 else 246 lmb->message_encoding = NULL; 247 248 lmb->base_dir = base_dir; 249 lmb->tmpfile_left = NULL; 250 lmb->config = config; 251 lmb->keep_locks = opt_state->no_unlock; 252 lmb->non_interactive = opt_state->non_interactive; 253 lmb->pool = pool; 254 *baton = lmb; 255 return SVN_NO_ERROR; 256} 257 258 259svn_error_t * 260svn_cl__cleanup_log_msg(void *log_msg_baton, 261 svn_error_t *commit_err, 262 apr_pool_t *pool) 263{ 264 struct log_msg_baton *lmb = log_msg_baton; 265 svn_error_t *err; 266 267 /* If there was no tmpfile left, or there is no log message baton, 268 return COMMIT_ERR. */ 269 if ((! lmb) || (! lmb->tmpfile_left)) 270 return commit_err; 271 272 /* If there was no commit error, cleanup the tmpfile and return. */ 273 if (! commit_err) 274 return svn_io_remove_file2(lmb->tmpfile_left, FALSE, lmb->pool); 275 276 /* There was a commit error; there is a tmpfile. Leave the tmpfile 277 around, and add message about its presence to the commit error 278 chain. Then return COMMIT_ERR. If the conversion from UTF-8 to 279 native encoding fails, we have to compose that error with the 280 commit error chain, too. */ 281 282 err = svn_error_createf(commit_err->apr_err, NULL, 283 _(" '%s'"), 284 svn_dirent_local_style(lmb->tmpfile_left, pool)); 285 svn_error_compose(commit_err, 286 svn_error_create(commit_err->apr_err, err, 287 _("Your commit message was left in " 288 "a temporary file:"))); 289 return commit_err; 290} 291 292 293/* Remove line-starting PREFIX and everything after it from BUFFER. 294 If NEW_LEN is non-NULL, return the new length of BUFFER in 295 *NEW_LEN. */ 296static void 297truncate_buffer_at_prefix(apr_size_t *new_len, 298 char *buffer, 299 const char *prefix) 300{ 301 char *substring = buffer; 302 303 assert(buffer && prefix); 304 305 /* Initialize *NEW_LEN. */ 306 if (new_len) 307 *new_len = strlen(buffer); 308 309 while (1) 310 { 311 /* Find PREFIX in BUFFER. */ 312 substring = strstr(substring, prefix); 313 if (! substring) 314 return; 315 316 /* We found PREFIX. Is it really a PREFIX? Well, if it's the first 317 thing in the file, or if the character before it is a 318 line-terminator character, it sure is. */ 319 if ((substring == buffer) 320 || (*(substring - 1) == '\r') 321 || (*(substring - 1) == '\n')) 322 { 323 *substring = '\0'; 324 if (new_len) 325 *new_len = substring - buffer; 326 } 327 else if (substring) 328 { 329 /* Well, it wasn't really a prefix, so just advance by 1 330 character and continue. */ 331 substring++; 332 } 333 } 334 335 /* NOTREACHED */ 336} 337 338 339/* 340 * Since we're adding freebsd-specific tokens to the log message, 341 * clean out any leftovers to avoid accidently sending them to other 342 * projects that won't be expecting them. 343 */ 344 345static const char *prefixes[] = { 346 "PR:", 347 "Submitted by:", 348 "Reported by:", 349 "Reviewed by:", 350 "Approved by:", 351 "Obtained from:", 352 "MFC after:", 353 "MFH:", 354 "Relnotes:", 355 "Security:", 356 "Sponsored by:", 357 "Pull Request:", 358 "Differential Revision:", 359}; 360 361void 362cleanmsg(apr_size_t *l, char *s) 363{ 364 int i; 365 char *pos; 366 char *kw; 367 char *p; 368 int empty; 369 370 for (i = 0; i < sizeof(prefixes) / sizeof(prefixes[0]); i++) { 371 pos = s; 372 while ((kw = strstr(pos, prefixes[i])) != NULL) { 373 /* Check to see if keyword is at start of line (or buffer) */ 374 if (!(kw == s || kw[-1] == '\r' || kw[-1] == '\n')) { 375 pos = kw + 1; 376 continue; 377 } 378 p = kw + strlen(prefixes[i]); 379 empty = 1; 380 while (1) { 381 if (*p == ' ' || *p == '\t') { 382 p++; 383 continue; 384 } 385 if (*p == '\0' || *p == '\r' || *p == '\n') 386 break; 387 empty = 0; 388 break; 389 } 390 if (empty && (*p == '\r' || *p == '\n')) { 391 memmove(kw, p + 1, strlen(p + 1) + 1); 392 if (l) 393 *l -= (p + 1 - kw); 394 } else if (empty) { 395 *kw = '\0'; 396 if (l) 397 *l -= (p - kw); 398 } else { 399 pos = p; 400 } 401 } 402 } 403} 404 405#define EDITOR_EOF_PREFIX _("--This line, and those below, will be ignored--") 406 407svn_error_t * 408svn_cl__get_log_message(const char **log_msg, 409 const char **tmp_file, 410 const apr_array_header_t *commit_items, 411 void *baton, 412 apr_pool_t *pool) 413{ 414 svn_stringbuf_t *default_msg = NULL; 415 struct log_msg_baton *lmb = baton; 416 svn_stringbuf_t *message = NULL; 417 svn_config_t *cfg; 418 const char *mfc_after, *sponsored_by; 419 420 cfg = lmb->config ? svn_hash_gets(lmb->config, SVN_CONFIG_CATEGORY_CONFIG) : NULL; 421 422 /* Set default message. */ 423 default_msg = svn_stringbuf_create(APR_EOL_STR, pool); 424 svn_stringbuf_appendcstr(default_msg, APR_EOL_STR); 425 svn_stringbuf_appendcstr(default_msg, "PR:\t\t" APR_EOL_STR); 426 svn_stringbuf_appendcstr(default_msg, "Submitted by:\t" APR_EOL_STR); 427 svn_stringbuf_appendcstr(default_msg, "Reported by:\t" APR_EOL_STR); 428 svn_stringbuf_appendcstr(default_msg, "Reviewed by:\t" APR_EOL_STR); 429 svn_stringbuf_appendcstr(default_msg, "Approved by:\t" APR_EOL_STR); 430 svn_stringbuf_appendcstr(default_msg, "Obtained from:\t" APR_EOL_STR); 431 svn_stringbuf_appendcstr(default_msg, "MFC after:\t"); 432 svn_config_get(cfg, &mfc_after, SVN_CONFIG_SECTION_MISCELLANY, "freebsd-mfc-after", NULL); 433 if (mfc_after != NULL) 434 svn_stringbuf_appendcstr(default_msg, mfc_after); 435 svn_stringbuf_appendcstr(default_msg, APR_EOL_STR); 436 svn_stringbuf_appendcstr(default_msg, "MFH:\t\t" APR_EOL_STR); 437 svn_stringbuf_appendcstr(default_msg, "Relnotes:\t" APR_EOL_STR); 438 svn_stringbuf_appendcstr(default_msg, "Security:\t" APR_EOL_STR); 439 svn_stringbuf_appendcstr(default_msg, "Sponsored by:\t"); 440 svn_config_get(cfg, &sponsored_by, SVN_CONFIG_SECTION_MISCELLANY, "freebsd-sponsored-by", 441#ifdef HAS_ORGANIZATION_NAME 442 ORGANIZATION_NAME); 443#else 444 NULL); 445#endif 446 if (sponsored_by != NULL) 447 svn_stringbuf_appendcstr(default_msg, sponsored_by); 448 svn_stringbuf_appendcstr(default_msg, APR_EOL_STR); 449 svn_stringbuf_appendcstr(default_msg, "Pull Request:\t" APR_EOL_STR); 450 svn_stringbuf_appendcstr(default_msg, "Differential Revision:\t" APR_EOL_STR); 451 svn_stringbuf_appendcstr(default_msg, EDITOR_EOF_PREFIX); 452 svn_stringbuf_appendcstr(default_msg, APR_EOL_STR); 453 svn_stringbuf_appendcstr(default_msg, "> Description of fields to fill in above: 76 columns --|" APR_EOL_STR); 454 svn_stringbuf_appendcstr(default_msg, "> PR: If and which Problem Report is related." APR_EOL_STR); 455 svn_stringbuf_appendcstr(default_msg, "> Submitted by: If someone else sent in the change." APR_EOL_STR); 456 svn_stringbuf_appendcstr(default_msg, "> Reported by: If someone else reported the issue." APR_EOL_STR); 457 svn_stringbuf_appendcstr(default_msg, "> Reviewed by: If someone else reviewed your modification." APR_EOL_STR); 458 svn_stringbuf_appendcstr(default_msg, "> Approved by: If you needed approval for this commit." APR_EOL_STR); 459 svn_stringbuf_appendcstr(default_msg, "> Obtained from: If the change is from a third party." APR_EOL_STR); 460 svn_stringbuf_appendcstr(default_msg, "> MFC after: N [day[s]|week[s]|month[s]]. Request a reminder email." APR_EOL_STR); 461 svn_stringbuf_appendcstr(default_msg, "> MFH: Ports tree branch name. Request approval for merge." APR_EOL_STR); 462 svn_stringbuf_appendcstr(default_msg, "> Relnotes: Set to 'yes' for mention in release notes." APR_EOL_STR); 463 svn_stringbuf_appendcstr(default_msg, "> Security: Vulnerability reference (one per line) or description." APR_EOL_STR); 464 svn_stringbuf_appendcstr(default_msg, "> Sponsored by: If the change was sponsored by an organization." APR_EOL_STR); 465 svn_stringbuf_appendcstr(default_msg, "> Pull Request: https://github.com/freebsd/freebsd/pull/### (*full* GitHub URL needed)." APR_EOL_STR); 466 svn_stringbuf_appendcstr(default_msg, "> Differential Revision: https://reviews.freebsd.org/D### (*full* phabric URL needed)." APR_EOL_STR); 467 svn_stringbuf_appendcstr(default_msg, "> Empty fields above will be automatically removed." APR_EOL_STR); 468 svn_stringbuf_appendcstr(default_msg, APR_EOL_STR); 469 470 *tmp_file = NULL; 471 if (lmb->message) 472 { 473 svn_string_t *log_msg_str = svn_string_create(lmb->message, pool); 474 475 SVN_ERR_W(svn_subst_translate_string2(&log_msg_str, NULL, NULL, 476 log_msg_str, lmb->message_encoding, 477 FALSE, pool, pool), 478 _("Error normalizing log message to internal format")); 479 480 /* Strip off the EOF marker text and the junk that follows it. */ 481 truncate_buffer_at_prefix(&(log_msg_str->len), (char *)log_msg_str->data, 482 EDITOR_EOF_PREFIX); 483 484 cleanmsg(&(log_msg_str->len), (char*)log_msg_str->data); 485 486 *log_msg = log_msg_str->data; 487 return SVN_NO_ERROR; 488 } 489 490 if (! commit_items->nelts) 491 { 492 *log_msg = ""; 493 return SVN_NO_ERROR; 494 } 495 496 while (! message) 497 { 498 /* We still don't have a valid commit message. Use $EDITOR to 499 get one. Note that svn_cl__edit_string_externally will still 500 return a UTF-8'ized log message. */ 501 int i; 502 svn_stringbuf_t *tmp_message = svn_stringbuf_dup(default_msg, pool); 503 svn_error_t *err = SVN_NO_ERROR; 504 svn_string_t *msg_string = svn_string_create_empty(pool); 505 506 for (i = 0; i < commit_items->nelts; i++) 507 { 508 svn_client_commit_item3_t *item 509 = APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *); 510 const char *path = item->path; 511 char text_mod = '_', prop_mod = ' ', unlock = ' '; 512 513 if (! path) 514 path = item->url; 515 else if (lmb->base_dir) 516 path = svn_dirent_is_child(lmb->base_dir, path, pool); 517 518 /* If still no path, then just use current directory. */ 519 if (! path || !*path) 520 path = "."; 521 522 if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE) 523 && (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)) 524 text_mod = 'R'; 525 else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD) 526 text_mod = 'A'; 527 else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE) 528 text_mod = 'D'; 529 else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS) 530 text_mod = 'M'; 531 532 if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS) 533 prop_mod = 'M'; 534 535 if (! lmb->keep_locks 536 && item->state_flags & SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN) 537 unlock = 'U'; 538 539 svn_stringbuf_appendbyte(tmp_message, text_mod); 540 svn_stringbuf_appendbyte(tmp_message, prop_mod); 541 svn_stringbuf_appendbyte(tmp_message, unlock); 542 if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY) 543 /* History included via copy/move. */ 544 svn_stringbuf_appendcstr(tmp_message, "+ "); 545 else 546 svn_stringbuf_appendcstr(tmp_message, " "); 547 svn_stringbuf_appendcstr(tmp_message, path); 548 svn_stringbuf_appendcstr(tmp_message, APR_EOL_STR); 549 } 550 551 msg_string->data = tmp_message->data; 552 msg_string->len = tmp_message->len; 553 554 /* Use the external edit to get a log message. */ 555 if (! lmb->non_interactive) 556 { 557 err = svn_cmdline__edit_string_externally(&msg_string, &lmb->tmpfile_left, 558 lmb->editor_cmd, 559 lmb->base_dir ? lmb->base_dir : "", 560 msg_string, "svn-commit", 561 lmb->config, TRUE, 562 lmb->message_encoding, 563 pool); 564 } 565 else /* non_interactive flag says we can't pop up an editor, so error */ 566 { 567 return svn_error_create 568 (SVN_ERR_CL_INSUFFICIENT_ARGS, NULL, 569 _("Cannot invoke editor to get log message " 570 "when non-interactive")); 571 } 572 573 /* Dup the tmpfile path into its baton's pool. */ 574 *tmp_file = lmb->tmpfile_left = apr_pstrdup(lmb->pool, 575 lmb->tmpfile_left); 576 577 /* If the edit returned an error, handle it. */ 578 if (err) 579 { 580 if (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR) 581 err = svn_error_quick_wrap 582 (err, _("Could not use external editor to fetch log message; " 583 "consider setting the $SVN_EDITOR environment variable " 584 "or using the --message (-m) or --file (-F) options")); 585 return svn_error_trace(err); 586 } 587 588 if (msg_string) 589 message = svn_stringbuf_create_from_string(msg_string, pool); 590 591 /* Strip off the EOF marker text and the junk that follows it. */ 592 if (message) 593 truncate_buffer_at_prefix(&message->len, message->data, 594 EDITOR_EOF_PREFIX); 595 /* 596 * Since we're adding freebsd-specific tokens to the log message, 597 * clean out any leftovers to avoid accidently sending them to other 598 * projects that won't be expecting them. 599 */ 600 if (message) 601 cleanmsg(&message->len, message->data); 602 603 if (message) 604 { 605 /* We did get message, now check if it is anything more than just 606 white space as we will consider white space only as empty */ 607 apr_size_t len; 608 609 for (len = 0; len < message->len; len++) 610 { 611 /* FIXME: should really use an UTF-8 whitespace test 612 rather than svn_ctype_isspace, which is ASCII only */ 613 if (! svn_ctype_isspace(message->data[len])) 614 break; 615 } 616 if (len == message->len) 617 message = NULL; 618 } 619 620 if (! message) 621 { 622 const char *reply; 623 SVN_ERR(svn_cmdline_prompt_user2 624 (&reply, 625 _("\nLog message unchanged or not specified\n" 626 "(a)bort, (c)ontinue, (e)dit:\n"), NULL, pool)); 627 if (reply) 628 { 629 int letter = apr_tolower(reply[0]); 630 631 /* If the user chooses to abort, we cleanup the 632 temporary file and exit the loop with a NULL 633 message. */ 634 if ('a' == letter) 635 { 636 SVN_ERR(svn_io_remove_file2(lmb->tmpfile_left, FALSE, pool)); 637 *tmp_file = lmb->tmpfile_left = NULL; 638 break; 639 } 640 641 /* If the user chooses to continue, we make an empty 642 message, which will cause us to exit the loop. We 643 also cleanup the temporary file. */ 644 if ('c' == letter) 645 { 646 SVN_ERR(svn_io_remove_file2(lmb->tmpfile_left, FALSE, pool)); 647 *tmp_file = lmb->tmpfile_left = NULL; 648 message = svn_stringbuf_create_empty(pool); 649 } 650 651 /* If the user chooses anything else, the loop will 652 continue on the NULL message. */ 653 } 654 } 655 } 656 657 *log_msg = message ? message->data : NULL; 658 return SVN_NO_ERROR; 659} 660 661 662/* ### The way our error wrapping currently works, the error returned 663 * from here will look as though it originates in this source file, 664 * instead of in the caller's source file. This can be a bit 665 * misleading, until one starts debugging. Ideally, there'd be a way 666 * to wrap an error while preserving its FILE/LINE info. 667 */ 668svn_error_t * 669svn_cl__may_need_force(svn_error_t *err) 670{ 671 if (err 672 && (err->apr_err == SVN_ERR_UNVERSIONED_RESOURCE || 673 err->apr_err == SVN_ERR_CLIENT_MODIFIED)) 674 { 675 /* Should this svn_error_compose a new error number? Probably not, 676 the error hasn't changed. */ 677 err = svn_error_quick_wrap 678 (err, _("Use --force to override this restriction (local modifications " 679 "may be lost)")); 680 } 681 682 return svn_error_trace(err); 683} 684 685 686svn_error_t * 687svn_cl__error_checked_fputs(const char *string, FILE* stream) 688{ 689 /* On POSIX systems, errno will be set on an error in fputs, but this might 690 not be the case on other platforms. We reset errno and only 691 use it if it was set by the below fputs call. Else, we just return 692 a generic error. */ 693 errno = 0; 694 695 if (fputs(string, stream) == EOF) 696 { 697 if (apr_get_os_error()) /* is errno on POSIX */ 698 return svn_error_wrap_apr(apr_get_os_error(), _("Write error")); 699 else 700 return svn_error_create(SVN_ERR_IO_WRITE_ERROR, NULL, NULL); 701 } 702 703 return SVN_NO_ERROR; 704} 705 706 707svn_error_t * 708svn_cl__try(svn_error_t *err, 709 apr_array_header_t *errors_seen, 710 svn_boolean_t quiet, 711 ...) 712{ 713 if (err) 714 { 715 apr_status_t apr_err; 716 va_list ap; 717 718 va_start(ap, quiet); 719 while ((apr_err = va_arg(ap, apr_status_t)) != APR_SUCCESS) 720 { 721 if (errors_seen) 722 { 723 int i; 724 svn_boolean_t add = TRUE; 725 726 /* Don't report duplicate error codes. */ 727 for (i = 0; i < errors_seen->nelts; i++) 728 { 729 if (APR_ARRAY_IDX(errors_seen, i, 730 apr_status_t) == err->apr_err) 731 { 732 add = FALSE; 733 break; 734 } 735 } 736 if (add) 737 APR_ARRAY_PUSH(errors_seen, apr_status_t) = err->apr_err; 738 } 739 if (err->apr_err == apr_err) 740 { 741 if (! quiet) 742 svn_handle_warning2(stderr, err, "svn: "); 743 svn_error_clear(err); 744 va_end(ap); 745 return SVN_NO_ERROR; 746 } 747 } 748 va_end(ap); 749 } 750 751 return svn_error_trace(err); 752} 753 754 755void 756svn_cl__xml_tagged_cdata(svn_stringbuf_t **sb, 757 apr_pool_t *pool, 758 const char *tagname, 759 const char *string) 760{ 761 if (string) 762 { 763 svn_xml_make_open_tag(sb, pool, svn_xml_protect_pcdata, 764 tagname, SVN_VA_NULL); 765 svn_xml_escape_cdata_cstring(sb, string, pool); 766 svn_xml_make_close_tag(sb, pool, tagname); 767 } 768} 769 770 771void 772svn_cl__print_xml_commit(svn_stringbuf_t **sb, 773 svn_revnum_t revision, 774 const char *author, 775 const char *date, 776 apr_pool_t *pool) 777{ 778 /* "<commit ...>" */ 779 svn_xml_make_open_tag(sb, pool, svn_xml_normal, "commit", 780 "revision", 781 apr_psprintf(pool, "%ld", revision), SVN_VA_NULL); 782 783 /* "<author>xx</author>" */ 784 if (author) 785 svn_cl__xml_tagged_cdata(sb, pool, "author", author); 786 787 /* "<date>xx</date>" */ 788 if (date) 789 svn_cl__xml_tagged_cdata(sb, pool, "date", date); 790 791 /* "</commit>" */ 792 svn_xml_make_close_tag(sb, pool, "commit"); 793} 794 795 796void 797svn_cl__print_xml_lock(svn_stringbuf_t **sb, 798 const svn_lock_t *lock, 799 apr_pool_t *pool) 800{ 801 /* "<lock>" */ 802 svn_xml_make_open_tag(sb, pool, svn_xml_normal, "lock", SVN_VA_NULL); 803 804 /* "<token>xx</token>" */ 805 svn_cl__xml_tagged_cdata(sb, pool, "token", lock->token); 806 807 /* "<owner>xx</owner>" */ 808 svn_cl__xml_tagged_cdata(sb, pool, "owner", lock->owner); 809 810 /* "<comment>xx</comment>" */ 811 svn_cl__xml_tagged_cdata(sb, pool, "comment", lock->comment); 812 813 /* "<created>xx</created>" */ 814 svn_cl__xml_tagged_cdata(sb, pool, "created", 815 svn_time_to_cstring(lock->creation_date, pool)); 816 817 /* "<expires>xx</expires>" */ 818 if (lock->expiration_date != 0) 819 svn_cl__xml_tagged_cdata(sb, pool, "expires", 820 svn_time_to_cstring(lock->expiration_date, pool)); 821 822 /* "</lock>" */ 823 svn_xml_make_close_tag(sb, pool, "lock"); 824} 825 826 827svn_error_t * 828svn_cl__xml_print_header(const char *tagname, 829 apr_pool_t *pool) 830{ 831 svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool); 832 833 /* <?xml version="1.0" encoding="UTF-8"?> */ 834 svn_xml_make_header2(&sb, "UTF-8", pool); 835 836 /* "<TAGNAME>" */ 837 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, tagname, SVN_VA_NULL); 838 839 return svn_cl__error_checked_fputs(sb->data, stdout); 840} 841 842 843svn_error_t * 844svn_cl__xml_print_footer(const char *tagname, 845 apr_pool_t *pool) 846{ 847 svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool); 848 849 /* "</TAGNAME>" */ 850 svn_xml_make_close_tag(&sb, pool, tagname); 851 return svn_cl__error_checked_fputs(sb->data, stdout); 852} 853 854 855/* A map for svn_node_kind_t values to XML strings */ 856static const svn_token_map_t map_node_kind_xml[] = 857{ 858 { "none", svn_node_none }, 859 { "file", svn_node_file }, 860 { "dir", svn_node_dir }, 861 { "", svn_node_unknown }, 862 { NULL, 0 } 863}; 864 865/* A map for svn_node_kind_t values to human-readable strings */ 866static const svn_token_map_t map_node_kind_human[] = 867{ 868 { N_("none"), svn_node_none }, 869 { N_("file"), svn_node_file }, 870 { N_("dir"), svn_node_dir }, 871 { "", svn_node_unknown }, 872 { NULL, 0 } 873}; 874 875const char * 876svn_cl__node_kind_str_xml(svn_node_kind_t kind) 877{ 878 return svn_token__to_word(map_node_kind_xml, kind); 879} 880 881const char * 882svn_cl__node_kind_str_human_readable(svn_node_kind_t kind) 883{ 884 return _(svn_token__to_word(map_node_kind_human, kind)); 885} 886 887 888/* A map for svn_wc_operation_t values to XML strings */ 889static const svn_token_map_t map_wc_operation_xml[] = 890{ 891 { "none", svn_wc_operation_none }, 892 { "update", svn_wc_operation_update }, 893 { "switch", svn_wc_operation_switch }, 894 { "merge", svn_wc_operation_merge }, 895 { NULL, 0 } 896}; 897 898/* A map for svn_wc_operation_t values to human-readable strings */ 899static const svn_token_map_t map_wc_operation_human[] = 900{ 901 { N_("none"), svn_wc_operation_none }, 902 { N_("update"), svn_wc_operation_update }, 903 { N_("switch"), svn_wc_operation_switch }, 904 { N_("merge"), svn_wc_operation_merge }, 905 { NULL, 0 } 906}; 907 908const char * 909svn_cl__operation_str_xml(svn_wc_operation_t operation, apr_pool_t *pool) 910{ 911 return svn_token__to_word(map_wc_operation_xml, operation); 912} 913 914const char * 915svn_cl__operation_str_human_readable(svn_wc_operation_t operation, 916 apr_pool_t *pool) 917{ 918 return _(svn_token__to_word(map_wc_operation_human, operation)); 919} 920 921 922svn_error_t * 923svn_cl__args_to_target_array_print_reserved(apr_array_header_t **targets, 924 apr_getopt_t *os, 925 const apr_array_header_t *known_targets, 926 svn_client_ctx_t *ctx, 927 svn_boolean_t keep_last_origpath_on_truepath_collision, 928 apr_pool_t *pool) 929{ 930 svn_error_t *err = svn_client_args_to_target_array2(targets, 931 os, 932 known_targets, 933 ctx, 934 keep_last_origpath_on_truepath_collision, 935 pool); 936 if (err) 937 { 938 if (err->apr_err == SVN_ERR_RESERVED_FILENAME_SPECIFIED) 939 { 940 svn_handle_error2(err, stderr, FALSE, "svn: Skipping argument: "); 941 svn_error_clear(err); 942 } 943 else 944 return svn_error_trace(err); 945 } 946 return SVN_NO_ERROR; 947} 948 949 950/* Helper for svn_cl__get_changelist(); implements 951 svn_changelist_receiver_t. */ 952static svn_error_t * 953changelist_receiver(void *baton, 954 const char *path, 955 const char *changelist, 956 apr_pool_t *pool) 957{ 958 /* No need to check CHANGELIST; our caller only asked about one of them. */ 959 apr_array_header_t *paths = baton; 960 APR_ARRAY_PUSH(paths, const char *) = apr_pstrdup(paths->pool, path); 961 return SVN_NO_ERROR; 962} 963 964 965svn_error_t * 966svn_cl__changelist_paths(apr_array_header_t **paths, 967 const apr_array_header_t *changelists, 968 const apr_array_header_t *targets, 969 svn_depth_t depth, 970 svn_client_ctx_t *ctx, 971 apr_pool_t *result_pool, 972 apr_pool_t *scratch_pool) 973{ 974 apr_array_header_t *found; 975 apr_hash_t *paths_hash; 976 apr_pool_t *iterpool; 977 int i; 978 979 if (! (changelists && changelists->nelts)) 980 { 981 *paths = (apr_array_header_t *)targets; 982 return SVN_NO_ERROR; 983 } 984 985 found = apr_array_make(scratch_pool, 8, sizeof(const char *)); 986 iterpool = svn_pool_create(scratch_pool); 987 for (i = 0; i < targets->nelts; i++) 988 { 989 const char *target = APR_ARRAY_IDX(targets, i, const char *); 990 svn_pool_clear(iterpool); 991 SVN_ERR(svn_client_get_changelists(target, changelists, depth, 992 changelist_receiver, found, 993 ctx, iterpool)); 994 } 995 svn_pool_destroy(iterpool); 996 997 SVN_ERR(svn_hash_from_cstring_keys(&paths_hash, found, result_pool)); 998 return svn_error_trace(svn_hash_keys(paths, paths_hash, result_pool)); 999} 1000 1001svn_cl__show_revs_t 1002svn_cl__show_revs_from_word(const char *word) 1003{ 1004 if (strcmp(word, SVN_CL__SHOW_REVS_MERGED) == 0) 1005 return svn_cl__show_revs_merged; 1006 if (strcmp(word, SVN_CL__SHOW_REVS_ELIGIBLE) == 0) 1007 return svn_cl__show_revs_eligible; 1008 /* word is an invalid flavor. */ 1009 return svn_cl__show_revs_invalid; 1010} 1011 1012 1013svn_error_t * 1014svn_cl__time_cstring_to_human_cstring(const char **human_cstring, 1015 const char *data, 1016 apr_pool_t *pool) 1017{ 1018 svn_error_t *err; 1019 apr_time_t when; 1020 1021 err = svn_time_from_cstring(&when, data, pool); 1022 if (err && err->apr_err == SVN_ERR_BAD_DATE) 1023 { 1024 svn_error_clear(err); 1025 1026 *human_cstring = _("(invalid date)"); 1027 return SVN_NO_ERROR; 1028 } 1029 else if (err) 1030 return svn_error_trace(err); 1031 1032 *human_cstring = svn_time_to_human_cstring(when, pool); 1033 1034 return SVN_NO_ERROR; 1035} 1036 1037const char * 1038svn_cl__node_description(const char *repos_root_url, 1039 const char *repos_relpath, 1040 svn_revnum_t peg_rev, 1041 svn_node_kind_t node_kind, 1042 const char *wc_repos_root_URL, 1043 apr_pool_t *pool) 1044{ 1045 const char *root_str = "^"; 1046 const char *path_str = "..."; 1047 1048 if (!repos_root_url || !repos_relpath || !SVN_IS_VALID_REVNUM(peg_rev)) 1049 /* Printing "(none)" the harder way to ensure conformity (mostly with 1050 * translations). */ 1051 return apr_psprintf(pool, "(%s)", 1052 svn_cl__node_kind_str_human_readable(svn_node_none)); 1053 1054 /* Construct a "caret notation" ^/URL if NODE matches WC_REPOS_ROOT_URL. 1055 * Otherwise show the complete URL, and if we can't, show dots. */ 1056 1057 if (repos_root_url && 1058 (wc_repos_root_URL == NULL || 1059 strcmp(repos_root_url, wc_repos_root_URL) != 0)) 1060 root_str = repos_root_url; 1061 1062 if (repos_relpath) 1063 path_str = repos_relpath; 1064 1065 return apr_psprintf(pool, "(%s) %s@%ld", 1066 svn_cl__node_kind_str_human_readable(node_kind), 1067 svn_path_url_add_component2(root_str, path_str, pool), 1068 peg_rev); 1069} 1070 1071svn_error_t * 1072svn_cl__eat_peg_revisions(apr_array_header_t **true_targets_p, 1073 const apr_array_header_t *targets, 1074 apr_pool_t *pool) 1075{ 1076 int i; 1077 apr_array_header_t *true_targets; 1078 1079 true_targets = apr_array_make(pool, targets->nelts, sizeof(const char *)); 1080 1081 for (i = 0; i < targets->nelts; i++) 1082 { 1083 const char *target = APR_ARRAY_IDX(targets, i, const char *); 1084 const char *true_target, *peg; 1085 1086 SVN_ERR(svn_opt__split_arg_at_peg_revision(&true_target, &peg, 1087 target, pool)); 1088 if (peg[0] && peg[1]) 1089 return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, 1090 _("'%s': a peg revision is not allowed here"), 1091 target); 1092 APR_ARRAY_PUSH(true_targets, const char *) = true_target; 1093 } 1094 1095 SVN_ERR_ASSERT(true_targets_p); 1096 *true_targets_p = true_targets; 1097 1098 return SVN_NO_ERROR; 1099} 1100 1101svn_error_t * 1102svn_cl__assert_homogeneous_target_type(const apr_array_header_t *targets) 1103{ 1104 svn_error_t *err; 1105 1106 err = svn_client__assert_homogeneous_target_type(targets); 1107 if (err && err->apr_err == SVN_ERR_ILLEGAL_TARGET) 1108 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, err, NULL); 1109 return err; 1110} 1111 1112svn_error_t * 1113svn_cl__check_target_is_local_path(const char *target) 1114{ 1115 if (svn_path_is_url(target)) 1116 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 1117 _("'%s' is not a local path"), target); 1118 return SVN_NO_ERROR; 1119} 1120 1121svn_error_t * 1122svn_cl__check_targets_are_local_paths(const apr_array_header_t *targets) 1123{ 1124 int i; 1125 1126 for (i = 0; i < targets->nelts; i++) 1127 { 1128 const char *target = APR_ARRAY_IDX(targets, i, const char *); 1129 1130 SVN_ERR(svn_cl__check_target_is_local_path(target)); 1131 } 1132 return SVN_NO_ERROR; 1133} 1134 1135const char * 1136svn_cl__local_style_skip_ancestor(const char *parent_path, 1137 const char *path, 1138 apr_pool_t *pool) 1139{ 1140 const char *relpath = NULL; 1141 1142 if (parent_path) 1143 relpath = svn_dirent_skip_ancestor(parent_path, path); 1144 1145 return svn_dirent_local_style(relpath ? relpath : path, pool); 1146} 1147 1148svn_error_t * 1149svn_cl__propset_print_binary_mime_type_warning(apr_array_header_t *targets, 1150 const char *propname, 1151 const svn_string_t *propval, 1152 apr_pool_t *scratch_pool) 1153{ 1154 if (strcmp(propname, SVN_PROP_MIME_TYPE) == 0) 1155 { 1156 apr_pool_t *iterpool = svn_pool_create(scratch_pool); 1157 int i; 1158 1159 for (i = 0; i < targets->nelts; i++) 1160 { 1161 const char *detected_mimetype; 1162 const char *target = APR_ARRAY_IDX(targets, i, const char *); 1163 const char *local_abspath; 1164 const svn_string_t *canon_propval; 1165 svn_node_kind_t node_kind; 1166 1167 svn_pool_clear(iterpool); 1168 1169 SVN_ERR(svn_dirent_get_absolute(&local_abspath, target, iterpool)); 1170 SVN_ERR(svn_io_check_path(local_abspath, &node_kind, iterpool)); 1171 if (node_kind != svn_node_file) 1172 continue; 1173 1174 SVN_ERR(svn_wc_canonicalize_svn_prop(&canon_propval, 1175 propname, propval, 1176 local_abspath, 1177 svn_node_file, 1178 FALSE, NULL, NULL, 1179 iterpool)); 1180 1181 if (svn_mime_type_is_binary(canon_propval->data)) 1182 { 1183 SVN_ERR(svn_io_detect_mimetype2(&detected_mimetype, 1184 local_abspath, NULL, 1185 iterpool)); 1186 if (detected_mimetype == NULL || 1187 !svn_mime_type_is_binary(detected_mimetype)) 1188 svn_error_clear(svn_cmdline_fprintf(stderr, iterpool, 1189 _("svn: warning: '%s' is a binary mime-type but file '%s' " 1190 "looks like text; diff, merge, blame, and other " 1191 "operations will stop working on this file\n"), 1192 canon_propval->data, 1193 svn_dirent_local_style(local_abspath, iterpool))); 1194 1195 } 1196 } 1197 svn_pool_destroy(iterpool); 1198 } 1199 1200 return SVN_NO_ERROR; 1201} 1202 1203