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