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