conflict-callbacks.c revision 269847
1/*
2 * conflict-callbacks.c: conflict resolution callbacks specific to the
3 * commandline client.
4 *
5 * ====================================================================
6 *    Licensed to the Apache Software Foundation (ASF) under one
7 *    or more contributor license agreements.  See the NOTICE file
8 *    distributed with this work for additional information
9 *    regarding copyright ownership.  The ASF licenses this file
10 *    to you under the Apache License, Version 2.0 (the
11 *    "License"); you may not use this file except in compliance
12 *    with the License.  You may obtain a copy of the License at
13 *
14 *      http://www.apache.org/licenses/LICENSE-2.0
15 *
16 *    Unless required by applicable law or agreed to in writing,
17 *    software distributed under the License is distributed on an
18 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
19 *    KIND, either express or implied.  See the License for the
20 *    specific language governing permissions and limitations
21 *    under the License.
22 * ====================================================================
23 */
24
25#include <apr_xlate.h>  /* for APR_LOCALE_CHARSET */
26
27#define APR_WANT_STRFUNC
28#include <apr_want.h>
29
30#include "svn_hash.h"
31#include "svn_cmdline.h"
32#include "svn_client.h"
33#include "svn_dirent_uri.h"
34#include "svn_types.h"
35#include "svn_pools.h"
36#include "svn_sorts.h"
37#include "svn_utf.h"
38
39#include "cl.h"
40#include "cl-conflicts.h"
41
42#include "private/svn_cmdline_private.h"
43
44#include "svn_private_config.h"
45
46#define ARRAY_LEN(ary) ((sizeof (ary)) / (sizeof ((ary)[0])))
47
48
49
50struct svn_cl__interactive_conflict_baton_t {
51  svn_cl__accept_t accept_which;
52  apr_hash_t *config;
53  const char *editor_cmd;
54  svn_boolean_t external_failed;
55  svn_cmdline_prompt_baton_t *pb;
56  const char *path_prefix;
57  svn_boolean_t quit;
58  svn_cl__conflict_stats_t *conflict_stats;
59};
60
61svn_error_t *
62svn_cl__get_conflict_func_interactive_baton(
63  svn_cl__interactive_conflict_baton_t **b,
64  svn_cl__accept_t accept_which,
65  apr_hash_t *config,
66  const char *editor_cmd,
67  svn_cl__conflict_stats_t *conflict_stats,
68  svn_cancel_func_t cancel_func,
69  void *cancel_baton,
70  apr_pool_t *result_pool)
71{
72  svn_cmdline_prompt_baton_t *pb = apr_palloc(result_pool, sizeof(*pb));
73  pb->cancel_func = cancel_func;
74  pb->cancel_baton = cancel_baton;
75
76  *b = apr_palloc(result_pool, sizeof(**b));
77  (*b)->accept_which = accept_which;
78  (*b)->config = config;
79  (*b)->editor_cmd = editor_cmd;
80  (*b)->external_failed = FALSE;
81  (*b)->pb = pb;
82  SVN_ERR(svn_dirent_get_absolute(&(*b)->path_prefix, "", result_pool));
83  (*b)->quit = FALSE;
84  (*b)->conflict_stats = conflict_stats;
85
86  return SVN_NO_ERROR;
87}
88
89svn_cl__accept_t
90svn_cl__accept_from_word(const char *word)
91{
92  /* Shorthand options are consistent with  svn_cl__conflict_handler(). */
93  if (strcmp(word, SVN_CL__ACCEPT_POSTPONE) == 0
94      || strcmp(word, "p") == 0 || strcmp(word, ":-P") == 0)
95    return svn_cl__accept_postpone;
96  if (strcmp(word, SVN_CL__ACCEPT_BASE) == 0)
97    /* ### shorthand? */
98    return svn_cl__accept_base;
99  if (strcmp(word, SVN_CL__ACCEPT_WORKING) == 0)
100    /* ### shorthand? */
101    return svn_cl__accept_working;
102  if (strcmp(word, SVN_CL__ACCEPT_MINE_CONFLICT) == 0
103      || strcmp(word, "mc") == 0 || strcmp(word, "X-)") == 0)
104    return svn_cl__accept_mine_conflict;
105  if (strcmp(word, SVN_CL__ACCEPT_THEIRS_CONFLICT) == 0
106      || strcmp(word, "tc") == 0 || strcmp(word, "X-(") == 0)
107    return svn_cl__accept_theirs_conflict;
108  if (strcmp(word, SVN_CL__ACCEPT_MINE_FULL) == 0
109      || strcmp(word, "mf") == 0 || strcmp(word, ":-)") == 0)
110    return svn_cl__accept_mine_full;
111  if (strcmp(word, SVN_CL__ACCEPT_THEIRS_FULL) == 0
112      || strcmp(word, "tf") == 0 || strcmp(word, ":-(") == 0)
113    return svn_cl__accept_theirs_full;
114  if (strcmp(word, SVN_CL__ACCEPT_EDIT) == 0
115      || strcmp(word, "e") == 0 || strcmp(word, ":-E") == 0)
116    return svn_cl__accept_edit;
117  if (strcmp(word, SVN_CL__ACCEPT_LAUNCH) == 0
118      || strcmp(word, "l") == 0 || strcmp(word, ":-l") == 0)
119    return svn_cl__accept_launch;
120  /* word is an invalid action. */
121  return svn_cl__accept_invalid;
122}
123
124
125/* Print on stdout a diff that shows incoming conflicting changes
126 * corresponding to the conflict described in DESC. */
127static svn_error_t *
128show_diff(const svn_wc_conflict_description2_t *desc,
129          const char *path_prefix,
130          apr_pool_t *pool)
131{
132  const char *path1, *path2;
133  const char *label1, *label2;
134  svn_diff_t *diff;
135  svn_stream_t *output;
136  svn_diff_file_options_t *options;
137
138  if (desc->merged_file)
139    {
140      /* For conflicts recorded by the 'merge' operation, show a diff between
141       * 'mine' (the working version of the file as it appeared before the
142       * 'merge' operation was run) and 'merged' (the version of the file
143       * as it appears after the merge operation).
144       *
145       * For conflicts recorded by the 'update' and 'switch' operations,
146       * show a diff beween 'theirs' (the new pristine version of the
147       * file) and 'merged' (the version of the file as it appears with
148       * local changes merged with the new pristine version).
149       *
150       * This way, the diff is always minimal and clearly identifies changes
151       * brought into the working copy by the update/switch/merge operation. */
152      if (desc->operation == svn_wc_operation_merge)
153        {
154          path1 = desc->my_abspath;
155          label1 = _("MINE");
156        }
157      else
158        {
159          path1 = desc->their_abspath;
160          label1 = _("THEIRS");
161        }
162      path2 = desc->merged_file;
163      label2 = _("MERGED");
164    }
165  else
166    {
167      /* There's no merged file, but we can show the
168         difference between mine and theirs. */
169      path1 = desc->their_abspath;
170      label1 = _("THEIRS");
171      path2 = desc->my_abspath;
172      label2 = _("MINE");
173    }
174
175  label1 = apr_psprintf(pool, "%s\t- %s",
176                        svn_cl__local_style_skip_ancestor(
177                          path_prefix, path1, pool), label1);
178  label2 = apr_psprintf(pool, "%s\t- %s",
179                        svn_cl__local_style_skip_ancestor(
180                          path_prefix, path2, pool), label2);
181
182  options = svn_diff_file_options_create(pool);
183  options->ignore_eol_style = TRUE;
184  SVN_ERR(svn_stream_for_stdout(&output, pool));
185  SVN_ERR(svn_diff_file_diff_2(&diff, path1, path2,
186                               options, pool));
187  return svn_diff_file_output_unified3(output, diff,
188                                       path1, path2,
189                                       label1, label2,
190                                       APR_LOCALE_CHARSET,
191                                       NULL, FALSE,
192                                       pool);
193}
194
195
196/* Print on stdout just the conflict hunks of a diff among the 'base', 'their'
197 * and 'my' files of DESC. */
198static svn_error_t *
199show_conflicts(const svn_wc_conflict_description2_t *desc,
200               apr_pool_t *pool)
201{
202  svn_diff_t *diff;
203  svn_stream_t *output;
204  svn_diff_file_options_t *options;
205
206  options = svn_diff_file_options_create(pool);
207  options->ignore_eol_style = TRUE;
208  SVN_ERR(svn_stream_for_stdout(&output, pool));
209  SVN_ERR(svn_diff_file_diff3_2(&diff,
210                                desc->base_abspath,
211                                desc->my_abspath,
212                                desc->their_abspath,
213                                options, pool));
214  /* ### Consider putting the markers/labels from
215     ### svn_wc__merge_internal in the conflict description. */
216  return svn_diff_file_output_merge2(output, diff,
217                                     desc->base_abspath,
218                                     desc->my_abspath,
219                                     desc->their_abspath,
220                                     _("||||||| ORIGINAL"),
221                                     _("<<<<<<< MINE (select with 'mc')"),
222                                     _(">>>>>>> THEIRS (select with 'tc')"),
223                                     "=======",
224                                     svn_diff_conflict_display_only_conflicts,
225                                     pool);
226}
227
228/* Perform a 3-way merge of the conflicting values of a property,
229 * and write the result to the OUTPUT stream.
230 *
231 * If MERGED_ABSPATH is non-NULL, use it as 'my' version instead of
232 * DESC->MY_ABSPATH.
233 *
234 * Assume the values are printable UTF-8 text.
235 */
236static svn_error_t *
237merge_prop_conflict(svn_stream_t *output,
238                    const svn_wc_conflict_description2_t *desc,
239                    const char *merged_abspath,
240                    apr_pool_t *pool)
241{
242  const char *base_abspath = desc->base_abspath;
243  const char *my_abspath = desc->my_abspath;
244  const char *their_abspath = desc->their_abspath;
245  svn_diff_file_options_t *options = svn_diff_file_options_create(pool);
246  svn_diff_t *diff;
247
248  /* If any of the property values is missing, use an empty file instead
249   * for the purpose of showing a diff. */
250  if (! base_abspath || ! my_abspath || ! their_abspath)
251    {
252      const char *empty_file;
253
254      SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file,
255                                       NULL, svn_io_file_del_on_pool_cleanup,
256                                       pool, pool));
257      if (! base_abspath)
258        base_abspath = empty_file;
259      if (! my_abspath)
260        my_abspath = empty_file;
261      if (! their_abspath)
262        their_abspath = empty_file;
263    }
264
265  options->ignore_eol_style = TRUE;
266  SVN_ERR(svn_diff_file_diff3_2(&diff,
267                                base_abspath,
268                                merged_abspath ? merged_abspath : my_abspath,
269                                their_abspath,
270                                options, pool));
271  SVN_ERR(svn_diff_file_output_merge2(output, diff,
272                                      base_abspath,
273                                      merged_abspath ? merged_abspath
274                                                     : my_abspath,
275                                      their_abspath,
276                                      _("||||||| ORIGINAL"),
277                                      _("<<<<<<< MINE"),
278                                      _(">>>>>>> THEIRS"),
279                                      "=======",
280                                      svn_diff_conflict_display_modified_original_latest,
281                                      pool));
282
283  return SVN_NO_ERROR;
284}
285
286/* Display the conflicting values of a property as a 3-way diff.
287 *
288 * If MERGED_ABSPATH is non-NULL, show it as 'my' version instead of
289 * DESC->MY_ABSPATH.
290 *
291 * Assume the values are printable UTF-8 text.
292 */
293static svn_error_t *
294show_prop_conflict(const svn_wc_conflict_description2_t *desc,
295                   const char *merged_abspath,
296                   apr_pool_t *pool)
297{
298  svn_stream_t *output;
299
300  SVN_ERR(svn_stream_for_stdout(&output, pool));
301  SVN_ERR(merge_prop_conflict(output, desc, merged_abspath, pool));
302
303  return SVN_NO_ERROR;
304}
305
306/* Run an external editor, passing it the MERGED_FILE, or, if the
307 * 'merged' file is null, return an error. The tool to use is determined by
308 * B->editor_cmd, B->config and environment variables; see
309 * svn_cl__edit_file_externally() for details.
310 *
311 * If the tool runs, set *PERFORMED_EDIT to true; if a tool is not
312 * configured or cannot run, do not touch *PERFORMED_EDIT, report the error
313 * on stderr, and return SVN_NO_ERROR; if any other error is encountered,
314 * return that error. */
315static svn_error_t *
316open_editor(svn_boolean_t *performed_edit,
317            const char *merged_file,
318            svn_cl__interactive_conflict_baton_t *b,
319            apr_pool_t *pool)
320{
321  svn_error_t *err;
322
323  if (merged_file)
324    {
325      err = svn_cmdline__edit_file_externally(merged_file, b->editor_cmd,
326                                              b->config, pool);
327      if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR))
328        {
329          svn_error_t *root_err = svn_error_root_cause(err);
330
331          SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n",
332                                      root_err->message ? root_err->message :
333                                      _("No editor found.")));
334          svn_error_clear(err);
335        }
336      else if (err && (err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
337        {
338          svn_error_t *root_err = svn_error_root_cause(err);
339
340          SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n",
341                                      root_err->message ? root_err->message :
342                                      _("Error running editor.")));
343          svn_error_clear(err);
344        }
345      else if (err)
346        return svn_error_trace(err);
347      else
348        *performed_edit = TRUE;
349    }
350  else
351    SVN_ERR(svn_cmdline_fprintf(stderr, pool,
352                                _("Invalid option; there's no "
353                                  "merged version to edit.\n\n")));
354
355  return SVN_NO_ERROR;
356}
357
358/* Run an external editor, passing it the 'merged' property in DESC.
359 * The tool to use is determined by B->editor_cmd, B->config and
360 * environment variables; see svn_cl__edit_file_externally() for details. */
361static svn_error_t *
362edit_prop_conflict(const char **merged_file_path,
363                   const svn_wc_conflict_description2_t *desc,
364                   svn_cl__interactive_conflict_baton_t *b,
365                   apr_pool_t *result_pool,
366                   apr_pool_t *scratch_pool)
367{
368  apr_file_t *file;
369  const char *file_path;
370  svn_boolean_t performed_edit = FALSE;
371  svn_stream_t *merged_prop;
372
373  SVN_ERR(svn_io_open_unique_file3(&file, &file_path, NULL,
374                                   svn_io_file_del_on_pool_cleanup,
375                                   result_pool, scratch_pool));
376  merged_prop = svn_stream_from_aprfile2(file, TRUE /* disown */,
377                                         scratch_pool);
378  SVN_ERR(merge_prop_conflict(merged_prop, desc, NULL, scratch_pool));
379  SVN_ERR(svn_stream_close(merged_prop));
380  SVN_ERR(svn_io_file_flush_to_disk(file, scratch_pool));
381  SVN_ERR(open_editor(&performed_edit, file_path, b, scratch_pool));
382  *merged_file_path = (performed_edit ? file_path : NULL);
383
384  return SVN_NO_ERROR;
385}
386
387/* Run an external merge tool, passing it the 'base', 'their', 'my' and
388 * 'merged' files in DESC. The tool to use is determined by B->config and
389 * environment variables; see svn_cl__merge_file_externally() for details.
390 *
391 * If the tool runs, set *PERFORMED_EDIT to true; if a tool is not
392 * configured or cannot run, do not touch *PERFORMED_EDIT, report the error
393 * on stderr, and return SVN_NO_ERROR; if any other error is encountered,
394 * return that error.  */
395static svn_error_t *
396launch_resolver(svn_boolean_t *performed_edit,
397                const svn_wc_conflict_description2_t *desc,
398                svn_cl__interactive_conflict_baton_t *b,
399                apr_pool_t *pool)
400{
401  svn_error_t *err;
402
403  err = svn_cl__merge_file_externally(desc->base_abspath, desc->their_abspath,
404                                      desc->my_abspath, desc->merged_file,
405                                      desc->local_abspath, b->config, NULL,
406                                      pool);
407  if (err && err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL)
408    {
409      SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n",
410                                  err->message ? err->message :
411                                  _("No merge tool found, "
412                                    "try '(m) merge' instead.\n")));
413      svn_error_clear(err);
414    }
415  else if (err && err->apr_err == SVN_ERR_EXTERNAL_PROGRAM)
416    {
417      SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n",
418                                  err->message ? err->message :
419                             _("Error running merge tool, "
420                               "try '(m) merge' instead.")));
421      svn_error_clear(err);
422    }
423  else if (err)
424    return svn_error_trace(err);
425  else if (performed_edit)
426    *performed_edit = TRUE;
427
428  return SVN_NO_ERROR;
429}
430
431
432/* Maximum line length for the prompt string. */
433#define MAX_PROMPT_WIDTH 70
434
435/* Description of a resolver option */
436typedef struct resolver_option_t
437{
438  const char *code;        /* one or two characters */
439  const char *short_desc;  /* label in prompt (localized) */
440  const char *long_desc;   /* longer description (localized) */
441  svn_wc_conflict_choice_t choice;  /* or -1 if not a simple choice */
442} resolver_option_t;
443
444/* Resolver options for a text conflict */
445/* (opt->code == "" causes a blank line break in help_string()) */
446static const resolver_option_t text_conflict_options[] =
447{
448  /* Translators: keep long_desc below 70 characters (wrap with a left
449     margin of 9 spaces if needed); don't translate the words within square
450     brackets. */
451  { "e",  N_("edit file"),        N_("change merged file in an editor"
452                                     "  [edit]"),
453                                  -1 },
454  { "df", N_("show diff"),        N_("show all changes made to merged file"),
455                                  -1 },
456  { "r",  N_("mark resolved"),   N_("accept merged version of file"),
457                                  svn_wc_conflict_choose_merged },
458  { "",   "",                     "", svn_wc_conflict_choose_unspecified },
459  { "dc", N_("display conflict"), N_("show all conflicts "
460                                     "(ignoring merged version)"), -1 },
461  { "mc", N_("my side of conflict"), N_("accept my version for all conflicts "
462                                        "(same)  [mine-conflict]"),
463                                  svn_wc_conflict_choose_mine_conflict },
464  { "tc", N_("their side of conflict"), N_("accept their version for all "
465                                           "conflicts (same)"
466                                           "  [theirs-conflict]"),
467                                  svn_wc_conflict_choose_theirs_conflict },
468  { "",   "",                     "", svn_wc_conflict_choose_unspecified },
469  { "mf", N_("my version"),       N_("accept my version of entire file (even "
470                                     "non-conflicts)  [mine-full]"),
471                                  svn_wc_conflict_choose_mine_full },
472  { "tf", N_("their version"),    N_("accept their version of entire file "
473                                     "(same)  [theirs-full]"),
474                                  svn_wc_conflict_choose_theirs_full },
475  { "",   "",                     "", svn_wc_conflict_choose_unspecified },
476  { "m",  N_("merge"),            N_("use internal merge tool to resolve "
477                                     "conflict"), -1 },
478  { "l",  N_("launch tool"),      N_("launch external tool to resolve "
479                                     "conflict  [launch]"), -1 },
480  { "p",  N_("postpone"),         N_("mark the conflict to be resolved later"
481                                     "  [postpone]"),
482                                  svn_wc_conflict_choose_postpone },
483  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
484                                  svn_wc_conflict_choose_postpone },
485  { "s",  N_("show all options"), N_("show this list (also 'h', '?')"), -1 },
486  { NULL }
487};
488
489/* Resolver options for a property conflict */
490static const resolver_option_t prop_conflict_options[] =
491{
492  { "mf", N_("my version"),       N_("accept my version of entire property (even "
493                                     "non-conflicts)  [mine-full]"),
494                                  svn_wc_conflict_choose_mine_full },
495  { "tf", N_("their version"),    N_("accept their version of entire property "
496                                     "(same)  [theirs-full]"),
497                                  svn_wc_conflict_choose_theirs_full },
498  { "dc", N_("display conflict"), N_("show conflicts in this property"), -1 },
499  { "e",  N_("edit property"),    N_("change merged property value in an editor"
500                                     "  [edit]"), -1 },
501  { "r",  N_("mark resolved"),    N_("accept edited version of property"),
502                                  svn_wc_conflict_choose_merged },
503  { "p",  N_("postpone"),         N_("mark the conflict to be resolved later"
504                                     "  [postpone]"),
505                                  svn_wc_conflict_choose_postpone },
506  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
507                                  svn_wc_conflict_choose_postpone },
508  { "h",  N_("help"),             N_("show this help (also '?')"), -1 },
509  { NULL }
510};
511
512/* Resolver options for an obstructued addition */
513static const resolver_option_t obstructed_add_options[] =
514{
515  { "mf", N_("my version"),       N_("accept pre-existing item (ignore "
516                                     "upstream addition)  [mine-full]"),
517                                  svn_wc_conflict_choose_mine_full },
518  { "tf", N_("their version"),    N_("accept incoming item (overwrite "
519                                     "pre-existing item)  [theirs-full]"),
520                                  svn_wc_conflict_choose_theirs_full },
521  { "p",  N_("postpone"),         N_("mark the conflict to be resolved later"
522                                     "  [postpone]"),
523                                  svn_wc_conflict_choose_postpone },
524  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
525                                  svn_wc_conflict_choose_postpone },
526  { "h",  N_("help"),             N_("show this help (also '?')"), -1 },
527  { NULL }
528};
529
530/* Resolver options for a tree conflict */
531static const resolver_option_t tree_conflict_options[] =
532{
533  { "r",  N_("mark resolved"),    N_("accept current working copy state"),
534                                  svn_wc_conflict_choose_merged },
535  { "p",  N_("postpone"),         N_("resolve the conflict later  [postpone]"),
536                                  svn_wc_conflict_choose_postpone },
537  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
538                                  svn_wc_conflict_choose_postpone },
539  { "h",  N_("help"),             N_("show this help (also '?')"), -1 },
540  { NULL }
541};
542
543static const resolver_option_t tree_conflict_options_update_moved_away[] =
544{
545  { "mc", N_("apply update (recommended)"),
546                                  N_("apply update to the move destination"
547                                     "  [mine-conflict]"),
548                                  svn_wc_conflict_choose_mine_conflict },
549  { "r",  N_("discard update (breaks move)"), N_("discard update, mark "
550                                                 "resolved, the move will "
551                                                 "will become a copy"),
552                                  svn_wc_conflict_choose_merged },
553  { "p",  N_("postpone"),         N_("resolve the conflict later  [postpone]"),
554                                  svn_wc_conflict_choose_postpone },
555  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
556                                  svn_wc_conflict_choose_postpone },
557  { "h",  N_("help"),             N_("show this help (also '?')"), -1 },
558  { NULL }
559};
560
561static const resolver_option_t tree_conflict_options_update_edit_moved_away[] =
562{
563  { "mc", N_("apply update to move destination"),
564                                  N_("apply incoming update to move destination"
565                                     "  [mine-conflict]"),
566                                  svn_wc_conflict_choose_mine_conflict },
567  { "p",  N_("postpone"),         N_("resolve the conflict later  [postpone]"),
568                                  svn_wc_conflict_choose_postpone },
569  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
570                                  svn_wc_conflict_choose_postpone },
571  { "h",  N_("help"),             N_("show this help (also '?')"), -1 },
572  { NULL }
573};
574
575static const resolver_option_t tree_conflict_options_update_deleted[] =
576{
577  { "mc", N_("keep affected local moves"), N_("keep any local moves affected "
578                                              "by this deletion  [mine-conflict]"),
579                                  svn_wc_conflict_choose_mine_conflict },
580  { "r",  N_("mark resolved (breaks moves)"),  N_("mark resolved, any affected "
581                                                  "moves will become copies"),
582                                  svn_wc_conflict_choose_merged },
583  { "p",  N_("postpone"),         N_("resolve the conflict later  [postpone]"),
584                                  svn_wc_conflict_choose_postpone },
585  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
586                                  svn_wc_conflict_choose_postpone },
587  { "h",  N_("help"),             N_("show this help (also '?')"), -1 },
588  { NULL }
589};
590
591static const resolver_option_t tree_conflict_options_update_replaced[] =
592{
593  { "mc", N_("keep affected local moves"), N_("keep any moves affected by this "
594                                              "replacement  [mine-conflict]"),
595                                  svn_wc_conflict_choose_mine_conflict },
596  { "r",  N_("mark resolved (breaks moves)"), N_("mark resolved (any affected "
597                                                 "moves will become copies)"),
598                                  svn_wc_conflict_choose_merged },
599  { "p",  N_("postpone"),         N_("resolve the conflict later  [postpone]"),
600                                  svn_wc_conflict_choose_postpone },
601  { "q",  N_("quit resolution"),  N_("postpone all remaining conflicts"),
602                                  svn_wc_conflict_choose_postpone },
603  { "h",  N_("help"),             N_("show this help (also '?')"), -1 },
604  { NULL }
605};
606
607
608/* Return a pointer to the option description in OPTIONS matching the
609 * one- or two-character OPTION_CODE.  Return NULL if not found. */
610static const resolver_option_t *
611find_option(const resolver_option_t *options,
612            const char *option_code)
613{
614  const resolver_option_t *opt;
615
616  for (opt = options; opt->code; opt++)
617    {
618      /* Ignore code "" (blank lines) which is not a valid answer. */
619      if (opt->code[0] && strcmp(opt->code, option_code) == 0)
620        return opt;
621    }
622  return NULL;
623}
624
625/* Return a prompt string listing the options OPTIONS. If OPTION_CODES is
626 * non-null, select only the options whose codes are mentioned in it. */
627static const char *
628prompt_string(const resolver_option_t *options,
629              const char *const *option_codes,
630              apr_pool_t *pool)
631{
632  const char *result = _("Select:");
633  int left_margin = svn_utf_cstring_utf8_width(result);
634  const char *line_sep = apr_psprintf(pool, "\n%*s", left_margin, "");
635  int this_line_len = left_margin;
636  svn_boolean_t first = TRUE;
637
638  while (1)
639    {
640      const resolver_option_t *opt;
641      const char *s;
642      int slen;
643
644      if (option_codes)
645        {
646          if (! *option_codes)
647            break;
648          opt = find_option(options, *option_codes++);
649        }
650      else
651        {
652          opt = options++;
653          if (! opt->code)
654            break;
655        }
656
657      if (! first)
658        result = apr_pstrcat(pool, result, ",", (char *)NULL);
659      s = apr_psprintf(pool, _(" (%s) %s"),
660                       opt->code, _(opt->short_desc));
661      slen = svn_utf_cstring_utf8_width(s);
662      /* Break the line if adding the next option would make it too long */
663      if (this_line_len + slen > MAX_PROMPT_WIDTH)
664        {
665          result = apr_pstrcat(pool, result, line_sep, (char *)NULL);
666          this_line_len = left_margin;
667        }
668      result = apr_pstrcat(pool, result, s, (char *)NULL);
669      this_line_len += slen;
670      first = FALSE;
671    }
672  return apr_pstrcat(pool, result, ": ", (char *)NULL);
673}
674
675/* Return a help string listing the OPTIONS. */
676static const char *
677help_string(const resolver_option_t *options,
678            apr_pool_t *pool)
679{
680  const char *result = "";
681  const resolver_option_t *opt;
682
683  for (opt = options; opt->code; opt++)
684    {
685      /* Append a line describing OPT, or a blank line if its code is "". */
686      if (opt->code[0])
687        {
688          const char *s = apr_psprintf(pool, "  (%s)", opt->code);
689
690          result = apr_psprintf(pool, "%s%-6s - %s\n",
691                                result, s, _(opt->long_desc));
692        }
693      else
694        {
695          result = apr_pstrcat(pool, result, "\n", (char *)NULL);
696        }
697    }
698  result = apr_pstrcat(pool, result,
699                       _("Words in square brackets are the corresponding "
700                         "--accept option arguments.\n"),
701                       (char *)NULL);
702  return result;
703}
704
705/* Prompt the user with CONFLICT_OPTIONS, restricted to the options listed
706 * in OPTIONS_TO_SHOW if that is non-null.  Set *OPT to point to the chosen
707 * one of CONFLICT_OPTIONS (not necessarily one of OPTIONS_TO_SHOW), or to
708 * NULL if the answer was not one of them.
709 *
710 * If the answer is the (globally recognized) 'help' option, then display
711 * the help (on stderr) and return with *OPT == NULL.
712 */
713static svn_error_t *
714prompt_user(const resolver_option_t **opt,
715            const resolver_option_t *conflict_options,
716            const char *const *options_to_show,
717            void *prompt_baton,
718            apr_pool_t *scratch_pool)
719{
720  const char *prompt
721    = prompt_string(conflict_options, options_to_show, scratch_pool);
722  const char *answer;
723
724  SVN_ERR(svn_cmdline_prompt_user2(&answer, prompt, prompt_baton, scratch_pool));
725  if (strcmp(answer, "h") == 0 || strcmp(answer, "?") == 0)
726    {
727      SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n",
728                                  help_string(conflict_options,
729                                              scratch_pool)));
730      *opt = NULL;
731    }
732  else
733    {
734      *opt = find_option(conflict_options, answer);
735      if (! *opt)
736        {
737          SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
738                                      _("Unrecognized option.\n\n")));
739        }
740    }
741  return SVN_NO_ERROR;
742}
743
744/* Ask the user what to do about the text conflict described by DESC.
745 * Return the answer in RESULT. B is the conflict baton for this
746 * conflict resolution session.
747 * SCRATCH_POOL is used for temporary allocations. */
748static svn_error_t *
749handle_text_conflict(svn_wc_conflict_result_t *result,
750                     const svn_wc_conflict_description2_t *desc,
751                     svn_cl__interactive_conflict_baton_t *b,
752                     apr_pool_t *scratch_pool)
753{
754  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
755  svn_boolean_t diff_allowed = FALSE;
756  /* Have they done something that might have affected the merged
757     file (so that we need to save a .edited copy)? */
758  svn_boolean_t performed_edit = FALSE;
759  /* Have they done *something* (edit, look at diff, etc) to
760     give them a rational basis for choosing (r)esolved? */
761  svn_boolean_t knows_something = FALSE;
762
763  SVN_ERR_ASSERT(desc->kind == svn_wc_conflict_kind_text);
764
765  SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
766                              _("Conflict discovered in file '%s'.\n"),
767                              svn_cl__local_style_skip_ancestor(
768                                b->path_prefix, desc->local_abspath,
769                                scratch_pool)));
770
771  /* Diffing can happen between base and merged, to show conflict
772     markers to the user (this is the typical 3-way merge
773     scenario), or if no base is available, we can show a diff
774     between mine and theirs. */
775  if ((desc->merged_file && desc->base_abspath)
776      || (!desc->base_abspath && desc->my_abspath && desc->their_abspath))
777    diff_allowed = TRUE;
778
779  while (TRUE)
780    {
781      const char *options[ARRAY_LEN(text_conflict_options)];
782      const char **next_option = options;
783      const resolver_option_t *opt;
784
785      svn_pool_clear(iterpool);
786
787      *next_option++ = "p";
788      if (diff_allowed)
789        {
790          *next_option++ = "df";
791          *next_option++ = "e";
792          *next_option++ = "m";
793
794          if (knows_something)
795            *next_option++ = "r";
796
797          if (! desc->is_binary)
798            {
799              *next_option++ = "mc";
800              *next_option++ = "tc";
801            }
802        }
803      else
804        {
805          if (knows_something)
806            *next_option++ = "r";
807          *next_option++ = "mf";
808          *next_option++ = "tf";
809        }
810      *next_option++ = "s";
811      *next_option++ = NULL;
812
813      SVN_ERR(prompt_user(&opt, text_conflict_options, options, b->pb,
814                          iterpool));
815      if (! opt)
816        continue;
817
818      if (strcmp(opt->code, "q") == 0)
819        {
820          result->choice = opt->choice;
821          b->accept_which = svn_cl__accept_postpone;
822          b->quit = TRUE;
823          break;
824        }
825      else if (strcmp(opt->code, "s") == 0)
826        {
827          SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n",
828                                      help_string(text_conflict_options,
829                                                  iterpool)));
830        }
831      else if (strcmp(opt->code, "dc") == 0)
832        {
833          if (desc->is_binary)
834            {
835              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
836                                          _("Invalid option; cannot "
837                                            "display conflicts for a "
838                                            "binary file.\n\n")));
839              continue;
840            }
841          else if (! (desc->my_abspath && desc->base_abspath &&
842                      desc->their_abspath))
843            {
844              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
845                                          _("Invalid option; original "
846                                            "files not available.\n\n")));
847              continue;
848            }
849          SVN_ERR(show_conflicts(desc, iterpool));
850          knows_something = TRUE;
851        }
852      else if (strcmp(opt->code, "df") == 0)
853        {
854          if (! diff_allowed)
855            {
856              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
857                             _("Invalid option; there's no "
858                                "merged version to diff.\n\n")));
859              continue;
860            }
861
862          SVN_ERR(show_diff(desc, b->path_prefix, iterpool));
863          knows_something = TRUE;
864        }
865      else if (strcmp(opt->code, "e") == 0 || strcmp(opt->code, ":-E") == 0)
866        {
867          SVN_ERR(open_editor(&performed_edit, desc->merged_file, b, iterpool));
868          if (performed_edit)
869            knows_something = TRUE;
870        }
871      else if (strcmp(opt->code, "m") == 0 || strcmp(opt->code, ":-g") == 0 ||
872               strcmp(opt->code, "=>-") == 0 || strcmp(opt->code, ":>.") == 0)
873        {
874          if (desc->kind != svn_wc_conflict_kind_text)
875            {
876              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
877                                          _("Invalid option; can only "
878                                            "resolve text conflicts with "
879                                            "the internal merge tool."
880                                            "\n\n")));
881              continue;
882            }
883
884          if (desc->base_abspath && desc->their_abspath &&
885              desc->my_abspath && desc->merged_file)
886            {
887              svn_boolean_t remains_in_conflict;
888
889              SVN_ERR(svn_cl__merge_file(desc->base_abspath,
890                                         desc->their_abspath,
891                                         desc->my_abspath,
892                                         desc->merged_file,
893                                         desc->local_abspath,
894                                         b->path_prefix,
895                                         b->editor_cmd,
896                                         b->config,
897                                         &remains_in_conflict,
898                                         iterpool));
899              knows_something = !remains_in_conflict;
900            }
901          else
902            SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
903                                        _("Invalid option.\n\n")));
904        }
905      else if (strcmp(opt->code, "l") == 0 || strcmp(opt->code, ":-l") == 0)
906        {
907          /* ### This check should be earlier as it's nasty to offer an option
908           *     and then when the user chooses it say 'Invalid option'. */
909          /* ### 'merged_file' shouldn't be necessary *before* we launch the
910           *     resolver: it should be the *result* of doing so. */
911          if (desc->base_abspath && desc->their_abspath &&
912              desc->my_abspath && desc->merged_file)
913            {
914              SVN_ERR(launch_resolver(&performed_edit, desc, b, iterpool));
915              if (performed_edit)
916                knows_something = TRUE;
917            }
918          else
919            SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
920                                        _("Invalid option.\n\n")));
921        }
922      else if (opt->choice != -1)
923        {
924          if ((opt->choice == svn_wc_conflict_choose_mine_conflict
925               || opt->choice == svn_wc_conflict_choose_theirs_conflict)
926              && desc->is_binary)
927            {
928              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
929                                          _("Invalid option; cannot choose "
930                                            "based on conflicts in a "
931                                            "binary file.\n\n")));
932              continue;
933            }
934
935          /* We only allow the user accept the merged version of
936             the file if they've edited it, or at least looked at
937             the diff. */
938          if (opt->choice == svn_wc_conflict_choose_merged
939              && ! knows_something)
940            {
941              SVN_ERR(svn_cmdline_fprintf(
942                        stderr, iterpool,
943                        _("Invalid option; use diff/edit/merge/launch "
944                          "before choosing 'mark resolved'.\n\n")));
945              continue;
946            }
947
948          result->choice = opt->choice;
949          if (performed_edit)
950            result->save_merged = TRUE;
951          break;
952        }
953    }
954  svn_pool_destroy(iterpool);
955
956  return SVN_NO_ERROR;
957}
958
959/* Ask the user what to do about the property conflict described by DESC.
960 * Return the answer in RESULT. B is the conflict baton for this
961 * conflict resolution session.
962 * SCRATCH_POOL is used for temporary allocations. */
963static svn_error_t *
964handle_prop_conflict(svn_wc_conflict_result_t *result,
965                     const svn_wc_conflict_description2_t *desc,
966                     svn_cl__interactive_conflict_baton_t *b,
967                     apr_pool_t *result_pool,
968                     apr_pool_t *scratch_pool)
969{
970  apr_pool_t *iterpool;
971  const char *message;
972  const char *merged_file_path = NULL;
973  svn_boolean_t resolved_allowed = FALSE;
974
975  /* ### Work around a historical bug in the provider: the path to the
976   *     conflict description file was put in the 'theirs' field, and
977   *     'theirs' was put in the 'merged' field. */
978  ((svn_wc_conflict_description2_t *)desc)->their_abspath = desc->merged_file;
979  ((svn_wc_conflict_description2_t *)desc)->merged_file = NULL;
980
981  SVN_ERR_ASSERT(desc->kind == svn_wc_conflict_kind_property);
982
983  SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
984                              _("Conflict for property '%s' discovered"
985                                " on '%s'.\n"),
986                              desc->property_name,
987                              svn_cl__local_style_skip_ancestor(
988                                b->path_prefix, desc->local_abspath,
989                                scratch_pool)));
990
991  SVN_ERR(svn_cl__get_human_readable_prop_conflict_description(&message, desc,
992                                                               scratch_pool));
993  SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n", message));
994
995  iterpool = svn_pool_create(scratch_pool);
996  while (TRUE)
997    {
998      const resolver_option_t *opt;
999      const char *options[ARRAY_LEN(prop_conflict_options)];
1000      const char **next_option = options;
1001
1002      *next_option++ = "p";
1003      *next_option++ = "mf";
1004      *next_option++ = "tf";
1005      *next_option++ = "dc";
1006      *next_option++ = "e";
1007      if (resolved_allowed)
1008        *next_option++ = "r";
1009      *next_option++ = "q";
1010      *next_option++ = "h";
1011      *next_option++ = NULL;
1012
1013      svn_pool_clear(iterpool);
1014
1015      SVN_ERR(prompt_user(&opt, prop_conflict_options, options, b->pb,
1016                          iterpool));
1017      if (! opt)
1018        continue;
1019
1020      if (strcmp(opt->code, "q") == 0)
1021        {
1022          result->choice = opt->choice;
1023          b->accept_which = svn_cl__accept_postpone;
1024          b->quit = TRUE;
1025          break;
1026        }
1027      else if (strcmp(opt->code, "dc") == 0)
1028        {
1029          SVN_ERR(show_prop_conflict(desc, merged_file_path, scratch_pool));
1030        }
1031      else if (strcmp(opt->code, "e") == 0)
1032        {
1033          SVN_ERR(edit_prop_conflict(&merged_file_path, desc, b,
1034                                     result_pool, scratch_pool));
1035          resolved_allowed = (merged_file_path != NULL);
1036        }
1037      else if (strcmp(opt->code, "r") == 0)
1038        {
1039          if (! resolved_allowed)
1040            {
1041              SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
1042                             _("Invalid option; please edit the property "
1043                               "first.\n\n")));
1044              continue;
1045            }
1046
1047          result->merged_file = merged_file_path;
1048          result->choice = svn_wc_conflict_choose_merged;
1049          break;
1050        }
1051      else if (opt->choice != -1)
1052        {
1053          result->choice = opt->choice;
1054          break;
1055        }
1056    }
1057  svn_pool_destroy(iterpool);
1058
1059  return SVN_NO_ERROR;
1060}
1061
1062/* Ask the user what to do about the tree conflict described by DESC.
1063 * Return the answer in RESULT. B is the conflict baton for this
1064 * conflict resolution session.
1065 * SCRATCH_POOL is used for temporary allocations. */
1066static svn_error_t *
1067handle_tree_conflict(svn_wc_conflict_result_t *result,
1068                     const svn_wc_conflict_description2_t *desc,
1069                     svn_cl__interactive_conflict_baton_t *b,
1070                     apr_pool_t *scratch_pool)
1071{
1072  const char *readable_desc;
1073  apr_pool_t *iterpool;
1074
1075  SVN_ERR(svn_cl__get_human_readable_tree_conflict_description(
1076           &readable_desc, desc, scratch_pool));
1077  SVN_ERR(svn_cmdline_fprintf(
1078               stderr, scratch_pool,
1079               _("Tree conflict on '%s'\n   > %s\n"),
1080               svn_cl__local_style_skip_ancestor(b->path_prefix,
1081                                                 desc->local_abspath,
1082                                                 scratch_pool),
1083               readable_desc));
1084
1085  iterpool = svn_pool_create(scratch_pool);
1086  while (1)
1087    {
1088      const resolver_option_t *opt;
1089      const resolver_option_t *tc_opts;
1090
1091      svn_pool_clear(iterpool);
1092
1093      if (desc->operation == svn_wc_operation_update ||
1094          desc->operation == svn_wc_operation_switch)
1095        {
1096          if (desc->reason == svn_wc_conflict_reason_moved_away)
1097            {
1098              if (desc->action == svn_wc_conflict_action_edit)
1099                tc_opts = tree_conflict_options_update_edit_moved_away;
1100              else
1101                tc_opts = tree_conflict_options_update_moved_away;
1102            }
1103          else if (desc->reason == svn_wc_conflict_reason_deleted)
1104            tc_opts = tree_conflict_options_update_deleted;
1105          else if (desc->reason == svn_wc_conflict_reason_replaced)
1106            tc_opts = tree_conflict_options_update_replaced;
1107          else
1108            tc_opts = tree_conflict_options;
1109        }
1110      else
1111        tc_opts = tree_conflict_options;
1112
1113      SVN_ERR(prompt_user(&opt, tc_opts, NULL, b->pb, iterpool));
1114      if (! opt)
1115        continue;
1116
1117      if (strcmp(opt->code, "q") == 0)
1118        {
1119          result->choice = opt->choice;
1120          b->accept_which = svn_cl__accept_postpone;
1121          b->quit = TRUE;
1122          break;
1123        }
1124      else if (opt->choice != -1)
1125        {
1126          result->choice = opt->choice;
1127          break;
1128        }
1129    }
1130  svn_pool_destroy(iterpool);
1131
1132  return SVN_NO_ERROR;
1133}
1134
1135/* Ask the user what to do about the obstructed add described by DESC.
1136 * Return the answer in RESULT. B is the conflict baton for this
1137 * conflict resolution session.
1138 * SCRATCH_POOL is used for temporary allocations. */
1139static svn_error_t *
1140handle_obstructed_add(svn_wc_conflict_result_t *result,
1141                      const svn_wc_conflict_description2_t *desc,
1142                      svn_cl__interactive_conflict_baton_t *b,
1143                      apr_pool_t *scratch_pool)
1144{
1145  apr_pool_t *iterpool;
1146
1147  SVN_ERR(svn_cmdline_fprintf(
1148               stderr, scratch_pool,
1149               _("Conflict discovered when trying to add '%s'.\n"
1150                 "An object of the same name already exists.\n"),
1151               svn_cl__local_style_skip_ancestor(b->path_prefix,
1152                                                 desc->local_abspath,
1153                                                 scratch_pool)));
1154
1155  iterpool = svn_pool_create(scratch_pool);
1156  while (1)
1157    {
1158      const resolver_option_t *opt;
1159
1160      svn_pool_clear(iterpool);
1161
1162      SVN_ERR(prompt_user(&opt, obstructed_add_options, NULL, b->pb,
1163                          iterpool));
1164      if (! opt)
1165        continue;
1166
1167      if (strcmp(opt->code, "q") == 0)
1168        {
1169          result->choice = opt->choice;
1170          b->accept_which = svn_cl__accept_postpone;
1171          b->quit = TRUE;
1172          break;
1173        }
1174      else if (opt->choice != -1)
1175        {
1176          result->choice = opt->choice;
1177          break;
1178        }
1179    }
1180  svn_pool_destroy(iterpool);
1181
1182  return SVN_NO_ERROR;
1183}
1184
1185/* The body of svn_cl__conflict_func_interactive(). */
1186static svn_error_t *
1187conflict_func_interactive(svn_wc_conflict_result_t **result,
1188                          const svn_wc_conflict_description2_t *desc,
1189                          void *baton,
1190                          apr_pool_t *result_pool,
1191                          apr_pool_t *scratch_pool)
1192{
1193  svn_cl__interactive_conflict_baton_t *b = baton;
1194  svn_error_t *err;
1195
1196  /* Start out assuming we're going to postpone the conflict. */
1197  *result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone,
1198                                          NULL, result_pool);
1199
1200  switch (b->accept_which)
1201    {
1202    case svn_cl__accept_invalid:
1203    case svn_cl__accept_unspecified:
1204      /* No (or no valid) --accept option, fall through to prompting. */
1205      break;
1206    case svn_cl__accept_postpone:
1207      (*result)->choice = svn_wc_conflict_choose_postpone;
1208      return SVN_NO_ERROR;
1209    case svn_cl__accept_base:
1210      (*result)->choice = svn_wc_conflict_choose_base;
1211      return SVN_NO_ERROR;
1212    case svn_cl__accept_working:
1213      /* If the caller didn't merge the property values, then I guess
1214       * 'choose working' means 'choose mine'... */
1215      if (! desc->merged_file)
1216        (*result)->merged_file = desc->my_abspath;
1217      (*result)->choice = svn_wc_conflict_choose_merged;
1218      return SVN_NO_ERROR;
1219    case svn_cl__accept_mine_conflict:
1220      (*result)->choice = svn_wc_conflict_choose_mine_conflict;
1221      return SVN_NO_ERROR;
1222    case svn_cl__accept_theirs_conflict:
1223      (*result)->choice = svn_wc_conflict_choose_theirs_conflict;
1224      return SVN_NO_ERROR;
1225    case svn_cl__accept_mine_full:
1226      (*result)->choice = svn_wc_conflict_choose_mine_full;
1227      return SVN_NO_ERROR;
1228    case svn_cl__accept_theirs_full:
1229      (*result)->choice = svn_wc_conflict_choose_theirs_full;
1230      return SVN_NO_ERROR;
1231    case svn_cl__accept_edit:
1232      if (desc->merged_file)
1233        {
1234          if (b->external_failed)
1235            {
1236              (*result)->choice = svn_wc_conflict_choose_postpone;
1237              return SVN_NO_ERROR;
1238            }
1239
1240          err = svn_cmdline__edit_file_externally(desc->merged_file,
1241                                                  b->editor_cmd, b->config,
1242                                                  scratch_pool);
1243          if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR))
1244            {
1245              SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n",
1246                                          err->message ? err->message :
1247                                          _("No editor found;"
1248                                            " leaving all conflicts.")));
1249              svn_error_clear(err);
1250              b->external_failed = TRUE;
1251            }
1252          else if (err && (err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
1253            {
1254              SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n",
1255                                          err->message ? err->message :
1256                                          _("Error running editor;"
1257                                            " leaving all conflicts.")));
1258              svn_error_clear(err);
1259              b->external_failed = TRUE;
1260            }
1261          else if (err)
1262            return svn_error_trace(err);
1263          (*result)->choice = svn_wc_conflict_choose_merged;
1264          return SVN_NO_ERROR;
1265        }
1266      /* else, fall through to prompting. */
1267      break;
1268    case svn_cl__accept_launch:
1269      if (desc->base_abspath && desc->their_abspath
1270          && desc->my_abspath && desc->merged_file)
1271        {
1272          svn_boolean_t remains_in_conflict;
1273
1274          if (b->external_failed)
1275            {
1276              (*result)->choice = svn_wc_conflict_choose_postpone;
1277              return SVN_NO_ERROR;
1278            }
1279
1280          err = svn_cl__merge_file_externally(desc->base_abspath,
1281                                              desc->their_abspath,
1282                                              desc->my_abspath,
1283                                              desc->merged_file,
1284                                              desc->local_abspath,
1285                                              b->config,
1286                                              &remains_in_conflict,
1287                                              scratch_pool);
1288          if (err && err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL)
1289            {
1290              SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n",
1291                                          err->message ? err->message :
1292                                          _("No merge tool found;"
1293                                            " leaving all conflicts.")));
1294              b->external_failed = TRUE;
1295              return svn_error_trace(err);
1296            }
1297          else if (err && err->apr_err == SVN_ERR_EXTERNAL_PROGRAM)
1298            {
1299              SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n",
1300                                          err->message ? err->message :
1301                                          _("Error running merge tool;"
1302                                            " leaving all conflicts.")));
1303              b->external_failed = TRUE;
1304              return svn_error_trace(err);
1305            }
1306          else if (err)
1307            return svn_error_trace(err);
1308
1309          if (remains_in_conflict)
1310            (*result)->choice = svn_wc_conflict_choose_postpone;
1311          else
1312            (*result)->choice = svn_wc_conflict_choose_merged;
1313          return SVN_NO_ERROR;
1314        }
1315      /* else, fall through to prompting. */
1316      break;
1317    }
1318
1319  /* We're in interactive mode and either the user gave no --accept
1320     option or the option did not apply; let's prompt. */
1321
1322  /* Handle the most common cases, which is either:
1323
1324     Conflicting edits on a file's text, or
1325     Conflicting edits on a property.
1326  */
1327  if (((desc->kind == svn_wc_conflict_kind_text)
1328       && (desc->action == svn_wc_conflict_action_edit)
1329       && (desc->reason == svn_wc_conflict_reason_edited)))
1330    SVN_ERR(handle_text_conflict(*result, desc, b, scratch_pool));
1331  else if (desc->kind == svn_wc_conflict_kind_property)
1332    SVN_ERR(handle_prop_conflict(*result, desc, b, result_pool, scratch_pool));
1333
1334  /*
1335    Dealing with obstruction of additions can be tricky.  The
1336    obstructing item could be unversioned, versioned, or even
1337    schedule-add.  Here's a matrix of how the caller should behave,
1338    based on results we return.
1339
1340                         Unversioned       Versioned       Schedule-Add
1341
1342      choose_mine       skip addition,    skip addition     skip addition
1343                        add existing item
1344
1345      choose_theirs     destroy file,    schedule-delete,   revert add,
1346                        add new item.    add new item.      rm file,
1347                                                            add new item
1348
1349      postpone               [              bail out                 ]
1350
1351   */
1352  else if ((desc->action == svn_wc_conflict_action_add)
1353           && (desc->reason == svn_wc_conflict_reason_obstructed))
1354    SVN_ERR(handle_obstructed_add(*result, desc, b, scratch_pool));
1355
1356  else if (desc->kind == svn_wc_conflict_kind_tree)
1357    SVN_ERR(handle_tree_conflict(*result, desc, b, scratch_pool));
1358
1359  else /* other types of conflicts -- do nothing about them. */
1360    {
1361      (*result)->choice = svn_wc_conflict_choose_postpone;
1362    }
1363
1364  return SVN_NO_ERROR;
1365}
1366
1367svn_error_t *
1368svn_cl__conflict_func_interactive(svn_wc_conflict_result_t **result,
1369                                  const svn_wc_conflict_description2_t *desc,
1370                                  void *baton,
1371                                  apr_pool_t *result_pool,
1372                                  apr_pool_t *scratch_pool)
1373{
1374  svn_cl__interactive_conflict_baton_t *b = baton;
1375
1376  SVN_ERR(conflict_func_interactive(result, desc, baton,
1377                                    result_pool, scratch_pool));
1378
1379  /* If we are resolving a conflict, adjust the summary of conflicts. */
1380  if ((*result)->choice != svn_wc_conflict_choose_postpone)
1381    {
1382      const char *local_path
1383        = svn_cl__local_style_skip_ancestor(
1384            b->path_prefix, desc->local_abspath, scratch_pool);
1385
1386      svn_cl__conflict_stats_resolved(b->conflict_stats, local_path,
1387                                             desc->kind);
1388    }
1389  return SVN_NO_ERROR;
1390}
1391