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