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