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