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
76 svn_error_t *
svn_cl__print_commit_info(const svn_commit_info_t * commit_info,void * baton,apr_pool_t * pool)77 svn_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
109 svn_error_t *
svn_cl__merge_file_externally(const char * base_path,const char * their_path,const char * my_path,const char * merged_path,const char * wc_path,apr_hash_t * config,svn_boolean_t * remains_in_conflict,apr_pool_t * pool)110 svn_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(). */
191 struct 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
205 svn_error_t *
svn_cl__make_log_msg_baton(void ** baton,svn_cl__opt_state_t * opt_state,const char * base_dir,apr_hash_t * config,apr_pool_t * pool)206 svn_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
259 svn_error_t *
svn_cl__cleanup_log_msg(void * log_msg_baton,svn_error_t * commit_err,apr_pool_t * pool)260 svn_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. */
296 static void
truncate_buffer_at_prefix(apr_size_t * new_len,char * buffer,const char * prefix)297 truncate_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
345 static 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
361 void
cleanmsg(apr_size_t * l,char * s)362 cleanmsg(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
407 svn_error_t *
svn_cl__get_log_message(const char ** log_msg,const char ** tmp_file,const apr_array_header_t * commit_items,void * baton,apr_pool_t * pool)408 svn_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 */
668 svn_error_t *
svn_cl__may_need_force(svn_error_t * err)669 svn_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
686 svn_error_t *
svn_cl__error_checked_fputs(const char * string,FILE * stream)687 svn_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
707 svn_error_t *
svn_cl__try(svn_error_t * err,apr_array_header_t * errors_seen,svn_boolean_t quiet,...)708 svn_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
755 void
svn_cl__xml_tagged_cdata(svn_stringbuf_t ** sb,apr_pool_t * pool,const char * tagname,const char * string)756 svn_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
771 void
svn_cl__print_xml_commit(svn_stringbuf_t ** sb,svn_revnum_t revision,const char * author,const char * date,apr_pool_t * pool)772 svn_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
796 void
svn_cl__print_xml_lock(svn_stringbuf_t ** sb,const svn_lock_t * lock,apr_pool_t * pool)797 svn_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
827 svn_error_t *
svn_cl__xml_print_header(const char * tagname,apr_pool_t * pool)828 svn_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
843 svn_error_t *
svn_cl__xml_print_footer(const char * tagname,apr_pool_t * pool)844 svn_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 */
856 static 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 */
866 static 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
875 const char *
svn_cl__node_kind_str_xml(svn_node_kind_t kind)876 svn_cl__node_kind_str_xml(svn_node_kind_t kind)
877 {
878 return svn_token__to_word(map_node_kind_xml, kind);
879 }
880
881 const char *
svn_cl__node_kind_str_human_readable(svn_node_kind_t kind)882 svn_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 */
889 static 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 */
899 static 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
908 const char *
svn_cl__operation_str_xml(svn_wc_operation_t operation,apr_pool_t * pool)909 svn_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
914 const char *
svn_cl__operation_str_human_readable(svn_wc_operation_t operation,apr_pool_t * pool)915 svn_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
922 svn_error_t *
svn_cl__args_to_target_array_print_reserved(apr_array_header_t ** targets,apr_getopt_t * os,const apr_array_header_t * known_targets,svn_client_ctx_t * ctx,svn_boolean_t keep_last_origpath_on_truepath_collision,apr_pool_t * pool)923 svn_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. */
952 static svn_error_t *
changelist_receiver(void * baton,const char * path,const char * changelist,apr_pool_t * pool)953 changelist_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
965 svn_error_t *
svn_cl__changelist_paths(apr_array_header_t ** paths,const apr_array_header_t * changelists,const apr_array_header_t * targets,svn_depth_t depth,svn_client_ctx_t * ctx,apr_pool_t * result_pool,apr_pool_t * scratch_pool)966 svn_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
1001 svn_cl__show_revs_t
svn_cl__show_revs_from_word(const char * word)1002 svn_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
1013 svn_error_t *
svn_cl__time_cstring_to_human_cstring(const char ** human_cstring,const char * data,apr_pool_t * pool)1014 svn_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
1037 const char *
svn_cl__node_description(const char * repos_root_url,const char * repos_relpath,svn_revnum_t peg_rev,svn_node_kind_t node_kind,const char * wc_repos_root_URL,apr_pool_t * pool)1038 svn_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
1071 svn_error_t *
svn_cl__eat_peg_revisions(apr_array_header_t ** true_targets_p,const apr_array_header_t * targets,apr_pool_t * pool)1072 svn_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
1101 svn_error_t *
svn_cl__assert_homogeneous_target_type(const apr_array_header_t * targets)1102 svn_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
1112 svn_error_t *
svn_cl__check_target_is_local_path(const char * target)1113 svn_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
1121 svn_error_t *
svn_cl__check_targets_are_local_paths(const apr_array_header_t * targets)1122 svn_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
1135 const char *
svn_cl__local_style_skip_ancestor(const char * parent_path,const char * path,apr_pool_t * pool)1136 svn_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
1148 svn_error_t *
svn_cl__propset_print_binary_mime_type_warning(apr_array_header_t * targets,const char * propname,const svn_string_t * propval,apr_pool_t * scratch_pool)1149 svn_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