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