blob: a9cb39a2ade1652212f26d661c05646fc49ce616 [file] [log] [blame]
/*
* conflict-callbacks.c: conflict resolution callbacks specific to the
* commandline client.
*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*/
#include <apr_xlate.h> /* for APR_LOCALE_CHARSET */
#define APR_WANT_STRFUNC
#include <apr_want.h>
#include "svn_hash.h"
#include "svn_cmdline.h"
#include "svn_client.h"
#include "svn_dirent_uri.h"
#include "svn_types.h"
#include "svn_pools.h"
#include "svn_sorts.h"
#include "svn_utf.h"
#include "cl.h"
#include "cl-conflicts.h"
#include "private/svn_cmdline_private.h"
#include "svn_private_config.h"
#define ARRAY_LEN(ary) ((sizeof (ary)) / (sizeof ((ary)[0])))
#define MAX_ARRAY_LEN(aryx, aryz) \
(ARRAY_LEN((aryx)) > ARRAY_LEN((aryz)) \
? ARRAY_LEN((aryx)) : ARRAY_LEN((aryz)))
struct svn_cl__interactive_conflict_baton_t {
svn_cl__accept_t accept_which;
apr_hash_t *config;
const char *editor_cmd;
svn_boolean_t external_failed;
svn_cmdline_prompt_baton_t *pb;
const char *path_prefix;
svn_boolean_t quit;
svn_cl__conflict_stats_t *conflict_stats;
svn_boolean_t printed_summary;
};
svn_error_t *
svn_cl__get_conflict_func_interactive_baton(
svn_cl__interactive_conflict_baton_t **b,
svn_cl__accept_t accept_which,
apr_hash_t *config,
const char *editor_cmd,
svn_cl__conflict_stats_t *conflict_stats,
svn_cancel_func_t cancel_func,
void *cancel_baton,
apr_pool_t *result_pool)
{
svn_cmdline_prompt_baton_t *pb = apr_palloc(result_pool, sizeof(*pb));
pb->cancel_func = cancel_func;
pb->cancel_baton = cancel_baton;
*b = apr_palloc(result_pool, sizeof(**b));
(*b)->accept_which = accept_which;
(*b)->config = config;
(*b)->editor_cmd = editor_cmd;
(*b)->external_failed = FALSE;
(*b)->pb = pb;
SVN_ERR(svn_dirent_get_absolute(&(*b)->path_prefix, "", result_pool));
(*b)->quit = FALSE;
(*b)->conflict_stats = conflict_stats;
(*b)->printed_summary = FALSE;
return SVN_NO_ERROR;
}
svn_cl__accept_t
svn_cl__accept_from_word(const char *word)
{
/* Shorthand options are consistent with svn_cl__conflict_handler(). */
if (strcmp(word, SVN_CL__ACCEPT_POSTPONE) == 0
|| strcmp(word, "p") == 0 || strcmp(word, ":-P") == 0)
return svn_cl__accept_postpone;
if (strcmp(word, SVN_CL__ACCEPT_BASE) == 0)
/* ### shorthand? */
return svn_cl__accept_base;
if (strcmp(word, SVN_CL__ACCEPT_WORKING) == 0)
/* ### shorthand? */
return svn_cl__accept_working;
if (strcmp(word, SVN_CL__ACCEPT_MINE_CONFLICT) == 0
|| strcmp(word, "mc") == 0 || strcmp(word, "X-)") == 0)
return svn_cl__accept_mine_conflict;
if (strcmp(word, SVN_CL__ACCEPT_THEIRS_CONFLICT) == 0
|| strcmp(word, "tc") == 0 || strcmp(word, "X-(") == 0)
return svn_cl__accept_theirs_conflict;
if (strcmp(word, SVN_CL__ACCEPT_MINE_FULL) == 0
|| strcmp(word, "mf") == 0 || strcmp(word, ":-)") == 0)
return svn_cl__accept_mine_full;
if (strcmp(word, SVN_CL__ACCEPT_THEIRS_FULL) == 0
|| strcmp(word, "tf") == 0 || strcmp(word, ":-(") == 0)
return svn_cl__accept_theirs_full;
if (strcmp(word, SVN_CL__ACCEPT_EDIT) == 0
|| strcmp(word, "e") == 0 || strcmp(word, ":-E") == 0)
return svn_cl__accept_edit;
if (strcmp(word, SVN_CL__ACCEPT_LAUNCH) == 0
|| strcmp(word, "l") == 0 || strcmp(word, ":-l") == 0)
return svn_cl__accept_launch;
/* word is an invalid action. */
return svn_cl__accept_invalid;
}
/* Print on stdout a diff that shows incoming conflicting changes
* corresponding to the conflict described in DESC. */
static svn_error_t *
show_diff(const svn_wc_conflict_description2_t *desc,
const char *path_prefix,
svn_cancel_func_t cancel_func,
void *cancel_baton,
apr_pool_t *pool)
{
const char *path1, *path2;
const char *label1, *label2;
svn_diff_t *diff;
svn_stream_t *output;
svn_diff_file_options_t *options;
if (desc->merged_file)
{
/* For conflicts recorded by the 'merge' operation, show a diff between
* 'mine' (the working version of the file as it appeared before the
* 'merge' operation was run) and 'merged' (the version of the file
* as it appears after the merge operation).
*
* For conflicts recorded by the 'update' and 'switch' operations,
* show a diff beween 'theirs' (the new pristine version of the
* file) and 'merged' (the version of the file as it appears with
* local changes merged with the new pristine version).
*
* This way, the diff is always minimal and clearly identifies changes
* brought into the working copy by the update/switch/merge operation. */
if (desc->operation == svn_wc_operation_merge)
{
path1 = desc->my_abspath;
label1 = _("MINE");
}
else
{
path1 = desc->their_abspath;
label1 = _("THEIRS");
}
path2 = desc->merged_file;
label2 = _("MERGED");
}
else
{
/* There's no merged file, but we can show the
difference between mine and theirs. */
path1 = desc->their_abspath;
label1 = _("THEIRS");
path2 = desc->my_abspath;
label2 = _("MINE");
}
label1 = apr_psprintf(pool, "%s\t- %s",
svn_cl__local_style_skip_ancestor(
path_prefix, path1, pool), label1);
label2 = apr_psprintf(pool, "%s\t- %s",
svn_cl__local_style_skip_ancestor(
path_prefix, path2, pool), label2);
options = svn_diff_file_options_create(pool);
options->ignore_eol_style = TRUE;
SVN_ERR(svn_stream_for_stdout(&output, pool));
SVN_ERR(svn_diff_file_diff_2(&diff, path1, path2,
options, pool));
return svn_diff_file_output_unified4(output, diff,
path1, path2,
label1, label2,
APR_LOCALE_CHARSET,
NULL,
options->show_c_function,
options->context_size,
cancel_func, cancel_baton,
pool);
}
/* Print on stdout just the conflict hunks of a diff among the 'base', 'their'
* and 'my' files of DESC. */
static svn_error_t *
show_conflicts(const svn_wc_conflict_description2_t *desc,
svn_cancel_func_t cancel_func,
void *cancel_baton,
apr_pool_t *pool)
{
svn_diff_t *diff;
svn_stream_t *output;
svn_diff_file_options_t *options;
options = svn_diff_file_options_create(pool);
options->ignore_eol_style = TRUE;
SVN_ERR(svn_stream_for_stdout(&output, pool));
SVN_ERR(svn_diff_file_diff3_2(&diff,
desc->base_abspath,
desc->my_abspath,
desc->their_abspath,
options, pool));
/* ### Consider putting the markers/labels from
### svn_wc__merge_internal in the conflict description. */
return svn_diff_file_output_merge3(output, diff,
desc->base_abspath,
desc->my_abspath,
desc->their_abspath,
_("||||||| ORIGINAL"),
_("<<<<<<< MINE (select with 'mc')"),
_(">>>>>>> THEIRS (select with 'tc')"),
"=======",
svn_diff_conflict_display_only_conflicts,
cancel_func,
cancel_baton,
pool);
}
/* Perform a 3-way merge of the conflicting values of a property,
* and write the result to the OUTPUT stream.
*
* If MERGED_ABSPATH is non-NULL, use it as 'my' version instead of
* DESC->MY_ABSPATH.
*
* Assume the values are printable UTF-8 text.
*/
static svn_error_t *
merge_prop_conflict(svn_stream_t *output,
const svn_wc_conflict_description2_t *desc,
const char *merged_abspath,
svn_cancel_func_t cancel_func,
void *cancel_baton,
apr_pool_t *pool)
{
const char *base_abspath = desc->base_abspath;
const char *my_abspath = desc->my_abspath;
const char *their_abspath = desc->their_abspath;
svn_diff_file_options_t *options = svn_diff_file_options_create(pool);
svn_diff_t *diff;
/* If any of the property values is missing, use an empty file instead
* for the purpose of showing a diff. */
if (! base_abspath || ! my_abspath || ! their_abspath)
{
const char *empty_file;
SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file,
NULL, svn_io_file_del_on_pool_cleanup,
pool, pool));
if (! base_abspath)
base_abspath = empty_file;
if (! my_abspath)
my_abspath = empty_file;
if (! their_abspath)
their_abspath = empty_file;
}
options->ignore_eol_style = TRUE;
SVN_ERR(svn_diff_file_diff3_2(&diff,
base_abspath,
merged_abspath ? merged_abspath : my_abspath,
their_abspath,
options, pool));
SVN_ERR(svn_diff_file_output_merge3(output, diff,
base_abspath,
merged_abspath ? merged_abspath
: my_abspath,
their_abspath,
_("||||||| ORIGINAL"),
_("<<<<<<< MINE"),
_(">>>>>>> THEIRS"),
"=======",
svn_diff_conflict_display_modified_original_latest,
cancel_func,
cancel_baton,
pool));
return SVN_NO_ERROR;
}
/* Display the conflicting values of a property as a 3-way diff.
*
* If MERGED_ABSPATH is non-NULL, show it as 'my' version instead of
* DESC->MY_ABSPATH.
*
* Assume the values are printable UTF-8 text.
*/
static svn_error_t *
show_prop_conflict(const svn_wc_conflict_description2_t *desc,
const char *merged_abspath,
svn_cancel_func_t cancel_func,
void *cancel_baton,
apr_pool_t *pool)
{
svn_stream_t *output;
SVN_ERR(svn_stream_for_stdout(&output, pool));
SVN_ERR(merge_prop_conflict(output, desc, merged_abspath,
cancel_func, cancel_baton, pool));
return SVN_NO_ERROR;
}
/* Run an external editor, passing it the MERGED_FILE, or, if the
* 'merged' file is null, return an error. The tool to use is determined by
* B->editor_cmd, B->config and environment variables; see
* svn_cl__edit_file_externally() for details.
*
* If the tool runs, set *PERFORMED_EDIT to true; if a tool is not
* configured or cannot run, do not touch *PERFORMED_EDIT, report the error
* on stderr, and return SVN_NO_ERROR; if any other error is encountered,
* return that error. */
static svn_error_t *
open_editor(svn_boolean_t *performed_edit,
const char *merged_file,
svn_cl__interactive_conflict_baton_t *b,
apr_pool_t *pool)
{
svn_error_t *err;
if (merged_file)
{
err = svn_cmdline__edit_file_externally(merged_file, b->editor_cmd,
b->config, pool);
if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR ||
err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
{
char buf[1024];
const char *message;
message = svn_err_best_message(err, buf, sizeof(buf));
SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n", message));
svn_error_clear(err);
}
else if (err)
return svn_error_trace(err);
else
*performed_edit = TRUE;
}
else
SVN_ERR(svn_cmdline_fprintf(stderr, pool,
_("Invalid option; there's no "
"merged version to edit.\n\n")));
return SVN_NO_ERROR;
}
/* Run an external editor, passing it the 'merged' property in DESC.
* The tool to use is determined by B->editor_cmd, B->config and
* environment variables; see svn_cl__edit_file_externally() for details. */
static svn_error_t *
edit_prop_conflict(const char **merged_file_path,
const svn_wc_conflict_description2_t *desc,
svn_cl__interactive_conflict_baton_t *b,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
apr_file_t *file;
const char *file_path;
svn_boolean_t performed_edit = FALSE;
svn_stream_t *merged_prop;
SVN_ERR(svn_io_open_unique_file3(&file, &file_path, NULL,
svn_io_file_del_on_pool_cleanup,
result_pool, scratch_pool));
merged_prop = svn_stream_from_aprfile2(file, TRUE /* disown */,
scratch_pool);
SVN_ERR(merge_prop_conflict(merged_prop, desc, NULL,
b->pb->cancel_func,
b->pb->cancel_baton,
scratch_pool));
SVN_ERR(svn_stream_close(merged_prop));
SVN_ERR(svn_io_file_flush(file, scratch_pool));
SVN_ERR(open_editor(&performed_edit, file_path, b, scratch_pool));
*merged_file_path = (performed_edit ? file_path : NULL);
return SVN_NO_ERROR;
}
/* Maximum line length for the prompt string. */
#define MAX_PROMPT_WIDTH 70
/* Description of a resolver option */
typedef struct resolver_option_t
{
const char *code; /* one or two characters */
const char *short_desc; /* label in prompt (localized) */
const char *long_desc; /* longer description (localized) */
svn_wc_conflict_choice_t choice;
/* or ..._undefined if not a simple choice */
} resolver_option_t;
/* Resolver options for a text conflict */
/* (opt->code == "" causes a blank line break in help_string()) */
static const resolver_option_t text_conflict_options[] =
{
/* Translators: keep long_desc below 70 characters (wrap with a left
margin of 9 spaces if needed); don't translate the words within square
brackets. */
{ "e", N_("edit file"), N_("change merged file in an editor"
" [edit]"),
svn_wc_conflict_choose_undefined },
{ "df", N_("show diff"), N_("show all changes made to merged file"),
svn_wc_conflict_choose_undefined },
{ "r", N_("mark resolved"), N_("accept merged version of file [working]"),
svn_wc_conflict_choose_merged },
{ "", "", "", svn_wc_conflict_choose_unspecified },
{ "dc", N_("display conflict"), N_("show all conflicts "
"(ignoring merged version)"),
svn_wc_conflict_choose_undefined },
{ "mc", N_("my side of conflict"), N_("accept my version for all conflicts "
"(same) [mine-conflict]"),
svn_wc_conflict_choose_mine_conflict },
{ "tc", N_("their side of conflict"), N_("accept their version for all "
"conflicts (same)"
" [theirs-conflict]"),
svn_wc_conflict_choose_theirs_conflict },
{ "", "", "", svn_wc_conflict_choose_unspecified },
{ "mf", N_("my version"), N_("accept my version of entire file (even "
"non-conflicts) [mine-full]"),
svn_wc_conflict_choose_mine_full },
{ "tf", N_("their version"), N_("accept their version of entire file "
"(same) [theirs-full]"),
svn_wc_conflict_choose_theirs_full },
{ "", "", "", svn_wc_conflict_choose_unspecified },
{ "m", N_("merge"), N_("use merge tool to resolve conflict"),
svn_wc_conflict_choose_undefined },
{ "l", N_("launch tool"), N_("launch external merge tool to resolve "
"conflict [launch]"),
svn_wc_conflict_choose_undefined },
{ "i", N_("internal merge tool"), N_("use built-in merge tool to "
"resolve conflict"),
svn_wc_conflict_choose_undefined },
{ "p", N_("postpone"), N_("mark the conflict to be resolved later"
" [postpone]"),
svn_wc_conflict_choose_postpone },
{ "q", N_("quit resolution"), N_("postpone all remaining conflicts"),
svn_wc_conflict_choose_postpone },
{ "s", N_("show all options"), N_("show this list (also 'h', '?')"),
svn_wc_conflict_choose_undefined },
{ NULL }
};
/* Resolver options for a binary file conflict. */
static const resolver_option_t binary_conflict_options[] =
{
/* Translators: keep long_desc below 70 characters (wrap with a left
margin of 9 spaces if needed); don't translate the words within square
brackets. */
{ "r", N_("mark resolved"), N_("accept the working copy version of file "
" [working]"),
svn_wc_conflict_choose_merged },
{ "tf", N_("their version"), N_("accept the incoming version of file "
" [theirs-full]"),
svn_wc_conflict_choose_theirs_full },
{ "p", N_("postpone"), N_("mark the conflict to be resolved later "
" [postpone]"),
svn_wc_conflict_choose_postpone },
{ "q", N_("quit resolution"), N_("postpone all remaining conflicts"),
svn_wc_conflict_choose_postpone },
{ "s", N_("show all options"), N_("show this list (also 'h', '?')"),
svn_wc_conflict_choose_undefined },
{ NULL }
};
/* Resolver options for a property conflict */
static const resolver_option_t prop_conflict_options[] =
{
{ "mf", N_("my version"), N_("accept my version of entire property (even "
"non-conflicts) [mine-full]"),
svn_wc_conflict_choose_mine_full },
{ "tf", N_("their version"), N_("accept their version of entire property "
"(same) [theirs-full]"),
svn_wc_conflict_choose_theirs_full },
{ "dc", N_("display conflict"), N_("show conflicts in this property"),
svn_wc_conflict_choose_undefined },
{ "e", N_("edit property"), N_("change merged property value in an editor"
" [edit]"),
svn_wc_conflict_choose_undefined },
{ "r", N_("mark resolved"), N_("accept edited version of property"),
svn_wc_conflict_choose_merged },
{ "p", N_("postpone"), N_("mark the conflict to be resolved later"
" [postpone]"),
svn_wc_conflict_choose_postpone },
{ "q", N_("quit resolution"), N_("postpone all remaining conflicts"),
svn_wc_conflict_choose_postpone },
{ "h", N_("help"), N_("show this help (also '?')"),
svn_wc_conflict_choose_undefined },
{ NULL }
};
/* Resolver options for a tree conflict */
static const resolver_option_t tree_conflict_options[] =
{
{ "r", N_("mark resolved"), N_("accept current working copy state"),
svn_wc_conflict_choose_merged },
{ "p", N_("postpone"), N_("resolve the conflict later [postpone]"),
svn_wc_conflict_choose_postpone },
{ "q", N_("quit resolution"), N_("postpone all remaining conflicts"),
svn_wc_conflict_choose_postpone },
{ "h", N_("help"), N_("show this help (also '?')"),
svn_wc_conflict_choose_undefined },
{ NULL }
};
static const resolver_option_t tree_conflict_options_update_moved_away[] =
{
{ "mc", N_("apply update to move destination (recommended)"),
N_("apply incoming update to move destination"
" [mine-conflict]"),
svn_wc_conflict_choose_mine_conflict },
{ "p", N_("postpone"), N_("resolve the conflict later [postpone]"),
svn_wc_conflict_choose_postpone },
{ "q", N_("quit resolution"), N_("postpone all remaining conflicts"),
svn_wc_conflict_choose_postpone },
{ "h", N_("help"), N_("show this help (also '?')"),
svn_wc_conflict_choose_undefined },
{ NULL }
};
static const resolver_option_t tree_conflict_options_update_edit_deleted_dir[] =
{
{ "mc", N_("prepare for updating moved-away children, if any (recommended)"),
N_("allow updating moved-away children "
"with 'svn resolve' [mine-conflict]"),
svn_wc_conflict_choose_mine_conflict },
{ "p", N_("postpone"), N_("resolve the conflict later [postpone]"),
svn_wc_conflict_choose_postpone },
{ "q", N_("quit resolution"), N_("postpone all remaining conflicts"),
svn_wc_conflict_choose_postpone },
{ "h", N_("help"), N_("show this help (also '?')"),
svn_wc_conflict_choose_undefined },
{ NULL }
};
/* Return a pointer to the option description in OPTIONS matching the
* one- or two-character OPTION_CODE. Return NULL if not found. */
static const resolver_option_t *
find_option(const resolver_option_t *options,
const char *option_code)
{
const resolver_option_t *opt;
for (opt = options; opt->code; opt++)
{
/* Ignore code "" (blank lines) which is not a valid answer. */
if (opt->code[0] && strcmp(opt->code, option_code) == 0)
return opt;
}
return NULL;
}
/* Return a prompt string listing the options OPTIONS. If OPTION_CODES is
* non-null, select only the options whose codes are mentioned in it. */
static const char *
prompt_string(const resolver_option_t *options,
const char *const *option_codes,
apr_pool_t *pool)
{
const char *result = _("Select:");
int left_margin = svn_utf_cstring_utf8_width(result);
const char *line_sep = apr_psprintf(pool, "\n%*s", left_margin, "");
int this_line_len = left_margin;
svn_boolean_t first = TRUE;
while (1)
{
const resolver_option_t *opt;
const char *s;
int slen;
if (option_codes)
{
if (! *option_codes)
break;
opt = find_option(options, *option_codes++);
}
else
{
opt = options++;
if (! opt->code)
break;
}
if (! first)
result = apr_pstrcat(pool, result, ",", SVN_VA_NULL);
s = apr_psprintf(pool, _(" (%s) %s"),
opt->code, _(opt->short_desc));
slen = svn_utf_cstring_utf8_width(s);
/* Break the line if adding the next option would make it too long */
if (this_line_len + slen > MAX_PROMPT_WIDTH)
{
result = apr_pstrcat(pool, result, line_sep, SVN_VA_NULL);
this_line_len = left_margin;
}
result = apr_pstrcat(pool, result, s, SVN_VA_NULL);
this_line_len += slen;
first = FALSE;
}
return apr_pstrcat(pool, result, ": ", SVN_VA_NULL);
}
/* Return a help string listing the OPTIONS. */
static const char *
help_string(const resolver_option_t *options,
apr_pool_t *pool)
{
const char *result = "";
const resolver_option_t *opt;
for (opt = options; opt->code; opt++)
{
/* Append a line describing OPT, or a blank line if its code is "". */
if (opt->code[0])
{
const char *s = apr_psprintf(pool, " (%s)", opt->code);
result = apr_psprintf(pool, "%s%-6s - %s\n",
result, s, _(opt->long_desc));
}
else
{
result = apr_pstrcat(pool, result, "\n", SVN_VA_NULL);
}
}
result = apr_pstrcat(pool, result,
_("Words in square brackets are the corresponding "
"--accept option arguments.\n"),
SVN_VA_NULL);
return result;
}
/* Prompt the user with CONFLICT_OPTIONS, restricted to the options listed
* in OPTIONS_TO_SHOW if that is non-null. Set *OPT to point to the chosen
* one of CONFLICT_OPTIONS (not necessarily one of OPTIONS_TO_SHOW), or to
* NULL if the answer was not one of them.
*
* If the answer is the (globally recognized) 'help' option, then display
* the help (on stderr) and return with *OPT == NULL.
*/
static svn_error_t *
prompt_user(const resolver_option_t **opt,
const resolver_option_t *conflict_options,
const char *const *options_to_show,
void *prompt_baton,
apr_pool_t *scratch_pool)
{
const char *prompt
= prompt_string(conflict_options, options_to_show, scratch_pool);
const char *answer;
SVN_ERR(svn_cmdline_prompt_user2(&answer, prompt, prompt_baton, scratch_pool));
if (strcmp(answer, "h") == 0 || strcmp(answer, "?") == 0)
{
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n",
help_string(conflict_options,
scratch_pool)));
*opt = NULL;
}
else
{
*opt = find_option(conflict_options, answer);
if (! *opt)
{
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
_("Unrecognized option.\n\n")));
}
}
return SVN_NO_ERROR;
}
/* Ask the user what to do about the text conflict described by DESC.
* Return the answer in RESULT. B is the conflict baton for this
* conflict resolution session.
* SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_text_conflict(svn_wc_conflict_result_t *result,
const svn_wc_conflict_description2_t *desc,
svn_cl__interactive_conflict_baton_t *b,
apr_pool_t *scratch_pool)
{
apr_pool_t *iterpool = svn_pool_create(scratch_pool);
svn_boolean_t diff_allowed = FALSE;
/* Have they done something that might have affected the merged
file (so that we need to save a .edited copy by setting the
result->save_merge flag)? */
svn_boolean_t performed_edit = FALSE;
/* Have they done *something* (edit, look at diff, etc) to
give them a rational basis for choosing (r)esolved? */
svn_boolean_t knows_something = FALSE;
const char *local_relpath;
SVN_ERR_ASSERT(desc->kind == svn_wc_conflict_kind_text);
local_relpath = svn_cl__local_style_skip_ancestor(b->path_prefix,
desc->local_abspath,
scratch_pool);;
if (desc->is_binary)
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
_("Conflict discovered in binary file '%s'.\n"),
local_relpath));
else
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
_("Conflict discovered in file '%s'.\n"),
local_relpath));
/* ### TODO This whole feature availability check is grossly outdated.
DIFF_ALLOWED needs either to be redefined or to go away.
*/
/* Diffing can happen between base and merged, to show conflict
markers to the user (this is the typical 3-way merge
scenario), or if no base is available, we can show a diff
between mine and theirs. */
if (!desc->is_binary &&
((desc->merged_file && desc->base_abspath)
|| (!desc->base_abspath && desc->my_abspath && desc->their_abspath)))
diff_allowed = TRUE;
while (TRUE)
{
const char *options[1 + MAX_ARRAY_LEN(binary_conflict_options,
text_conflict_options)];
const resolver_option_t *conflict_options = desc->is_binary
? binary_conflict_options
: text_conflict_options;
const char **next_option = options;
const resolver_option_t *opt;
svn_pool_clear(iterpool);
*next_option++ = "p";
if (diff_allowed)
{
/* We need one more path for this feature. */
if (desc->my_abspath)
*next_option++ = "df";
*next_option++ = "e";
/* We need one more path for this feature. */
if (desc->my_abspath)
*next_option++ = "m";
if (knows_something)
*next_option++ = "r";
*next_option++ = "mc";
*next_option++ = "tc";
}
else
{
if (knows_something || desc->is_binary)
*next_option++ = "r";
/* The 'mine-full' option selects the ".mine" file so only offer
* it if that file exists. It does not exist for binary files,
* for example (questionable historical behaviour since 1.0). */
if (desc->my_abspath)
*next_option++ = "mf";
*next_option++ = "tf";
}
*next_option++ = "s";
*next_option++ = NULL;
SVN_ERR(prompt_user(&opt, conflict_options, options, b->pb, iterpool));
if (! opt)
continue;
if (strcmp(opt->code, "q") == 0)
{
result->choice = opt->choice;
b->accept_which = svn_cl__accept_postpone;
b->quit = TRUE;
break;
}
else if (strcmp(opt->code, "s") == 0)
{
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n",
help_string(conflict_options,
iterpool)));
}
else if (strcmp(opt->code, "dc") == 0)
{
if (desc->is_binary)
{
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
_("Invalid option; cannot "
"display conflicts for a "
"binary file.\n\n")));
continue;
}
else if (! (desc->my_abspath && desc->base_abspath &&
desc->their_abspath))
{
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
_("Invalid option; original "
"files not available.\n\n")));
continue;
}
SVN_ERR(show_conflicts(desc,
b->pb->cancel_func,
b->pb->cancel_baton,
iterpool));
knows_something = TRUE;
}
else if (strcmp(opt->code, "df") == 0)
{
/* Re-check preconditions. */
if (! diff_allowed || ! desc->my_abspath)
{
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
_("Invalid option; there's no "
"merged version to diff.\n\n")));
continue;
}
SVN_ERR(show_diff(desc, b->path_prefix,
b->pb->cancel_func, b->pb->cancel_baton,
iterpool));
knows_something = TRUE;
}
else if (strcmp(opt->code, "e") == 0 || strcmp(opt->code, ":-E") == 0)
{
SVN_ERR(open_editor(&performed_edit, desc->merged_file, b, iterpool));
if (performed_edit)
knows_something = TRUE;
}
else if (strcmp(opt->code, "m") == 0 || strcmp(opt->code, ":-g") == 0 ||
strcmp(opt->code, "=>-") == 0 || strcmp(opt->code, ":>.") == 0)
{
svn_error_t *err;
/* Re-check preconditions. */
if (! desc->my_abspath)
{
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
_("Invalid option; there's no "
"base path to merge.\n\n")));
continue;
}
err = svn_cl__merge_file_externally(desc->base_abspath,
desc->their_abspath,
desc->my_abspath,
desc->merged_file,
desc->local_abspath, b->config,
NULL, iterpool);
if (err)
{
if (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL)
{
svn_boolean_t remains_in_conflict = TRUE;
/* Try the internal merge tool. */
svn_error_clear(err);
SVN_ERR(svn_cl__merge_file(&remains_in_conflict,
desc->base_abspath,
desc->their_abspath,
desc->my_abspath,
desc->merged_file,
desc->local_abspath,
b->path_prefix,
b->editor_cmd,
b->config,
b->pb->cancel_func,
b->pb->cancel_baton,
iterpool));
knows_something = !remains_in_conflict;
}
else if (err->apr_err == SVN_ERR_EXTERNAL_PROGRAM)
{
char buf[1024];
const char *message;
message = svn_err_best_message(err, buf, sizeof(buf));
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
"%s\n", message));
svn_error_clear(err);
continue;
}
else
return svn_error_trace(err);
}
else
{
/* The external merge tool's exit code was either 0 or 1.
* The tool may leave the file conflicted by exiting with
* exit code 1, and we allow the user to mark the conflict
* resolved in this case. */
performed_edit = TRUE;
knows_something = TRUE;
}
}
else if (strcmp(opt->code, "l") == 0 || strcmp(opt->code, ":-l") == 0)
{
/* ### This check should be earlier as it's nasty to offer an option
* and then when the user chooses it say 'Invalid option'. */
/* ### 'merged_file' shouldn't be necessary *before* we launch the
* resolver: it should be the *result* of doing so. */
if (desc->base_abspath && desc->their_abspath &&
desc->my_abspath && desc->merged_file)
{
svn_error_t *err;
char buf[1024];
const char *message;
err = svn_cl__merge_file_externally(desc->base_abspath,
desc->their_abspath,
desc->my_abspath,
desc->merged_file,
desc->local_abspath,
b->config, NULL, iterpool);
if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL ||
err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
{
message = svn_err_best_message(err, buf, sizeof(buf));
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, "%s\n",
message));
svn_error_clear(err);
}
else if (err)
return svn_error_trace(err);
else
performed_edit = TRUE;
if (performed_edit)
knows_something = TRUE;
}
else
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
_("Invalid option.\n\n")));
}
else if (strcmp(opt->code, "i") == 0)
{
svn_boolean_t remains_in_conflict = TRUE;
SVN_ERR(svn_cl__merge_file(&remains_in_conflict,
desc->base_abspath,
desc->their_abspath,
desc->my_abspath,
desc->merged_file,
desc->local_abspath,
b->path_prefix,
b->editor_cmd,
b->config,
b->pb->cancel_func,
b->pb->cancel_baton,
iterpool));
if (!remains_in_conflict)
knows_something = TRUE;
}
else if (opt->choice != svn_wc_conflict_choose_undefined)
{
if ((opt->choice == svn_wc_conflict_choose_mine_conflict
|| opt->choice == svn_wc_conflict_choose_theirs_conflict)
&& desc->is_binary)
{
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
_("Invalid option; cannot choose "
"based on conflicts in a "
"binary file.\n\n")));
continue;
}
/* We only allow the user accept the merged version of
the file if they've edited it, or at least looked at
the diff. */
if (opt->choice == svn_wc_conflict_choose_merged
&& ! knows_something && diff_allowed)
{
SVN_ERR(svn_cmdline_fprintf(
stderr, iterpool,
_("Invalid option; use diff/edit/merge/launch "
"before choosing 'mark resolved'.\n\n")));
continue;
}
result->choice = opt->choice;
if (performed_edit)
result->save_merged = TRUE;
break;
}
}
svn_pool_destroy(iterpool);
return SVN_NO_ERROR;
}
/* Ask the user what to do about the property conflict described by DESC.
* Return the answer in RESULT. B is the conflict baton for this
* conflict resolution session.
* SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_prop_conflict(svn_wc_conflict_result_t *result,
const svn_wc_conflict_description2_t *desc,
svn_cl__interactive_conflict_baton_t *b,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
apr_pool_t *iterpool;
const char *message;
const char *merged_file_path = NULL;
svn_boolean_t resolved_allowed = FALSE;
/* ### Work around a historical bug in the provider: the path to the
* conflict description file was put in the 'theirs' field, and
* 'theirs' was put in the 'merged' field. */
((svn_wc_conflict_description2_t *)desc)->their_abspath = desc->merged_file;
((svn_wc_conflict_description2_t *)desc)->merged_file = NULL;
SVN_ERR_ASSERT(desc->kind == svn_wc_conflict_kind_property);
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
_("Conflict for property '%s' discovered"
" on '%s'.\n"),
desc->property_name,
svn_cl__local_style_skip_ancestor(
b->path_prefix, desc->local_abspath,
scratch_pool)));
SVN_ERR(svn_cl__get_human_readable_prop_conflict_description(&message, desc,
scratch_pool));
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n", message));
iterpool = svn_pool_create(scratch_pool);
while (TRUE)
{
const resolver_option_t *opt;
const char *options[ARRAY_LEN(prop_conflict_options)];
const char **next_option = options;
*next_option++ = "p";
*next_option++ = "mf";
*next_option++ = "tf";
*next_option++ = "dc";
*next_option++ = "e";
if (resolved_allowed)
*next_option++ = "r";
*next_option++ = "q";
*next_option++ = "h";
*next_option++ = NULL;
svn_pool_clear(iterpool);
SVN_ERR(prompt_user(&opt, prop_conflict_options, options, b->pb,
iterpool));
if (! opt)
continue;
if (strcmp(opt->code, "q") == 0)
{
result->choice = opt->choice;
b->accept_which = svn_cl__accept_postpone;
b->quit = TRUE;
break;
}
else if (strcmp(opt->code, "dc") == 0)
{
SVN_ERR(show_prop_conflict(desc, merged_file_path,
b->pb->cancel_func, b->pb->cancel_baton,
scratch_pool));
}
else if (strcmp(opt->code, "e") == 0)
{
SVN_ERR(edit_prop_conflict(&merged_file_path, desc, b,
result_pool, scratch_pool));
resolved_allowed = (merged_file_path != NULL);
}
else if (strcmp(opt->code, "r") == 0)
{
if (! resolved_allowed)
{
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
_("Invalid option; please edit the property "
"first.\n\n")));
continue;
}
result->merged_file = merged_file_path;
result->choice = svn_wc_conflict_choose_merged;
break;
}
else if (opt->choice != svn_wc_conflict_choose_undefined)
{
result->choice = opt->choice;
break;
}
}
svn_pool_destroy(iterpool);
return SVN_NO_ERROR;
}
/* Ask the user what to do about the tree conflict described by DESC.
* Return the answer in RESULT. B is the conflict baton for this
* conflict resolution session.
* SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_tree_conflict(svn_wc_conflict_result_t *result,
const svn_wc_conflict_description2_t *desc,
svn_cl__interactive_conflict_baton_t *b,
apr_pool_t *scratch_pool)
{
const char *readable_desc;
apr_pool_t *iterpool;
SVN_ERR(svn_cl__get_human_readable_tree_conflict_description(
&readable_desc, desc, scratch_pool));
SVN_ERR(svn_cmdline_fprintf(
stderr, scratch_pool,
_("Tree conflict on '%s'\n > %s\n"),
svn_cl__local_style_skip_ancestor(b->path_prefix,
desc->local_abspath,
scratch_pool),
readable_desc));
iterpool = svn_pool_create(scratch_pool);
while (1)
{
const resolver_option_t *opt;
const resolver_option_t *tc_opts;
svn_pool_clear(iterpool);
tc_opts = tree_conflict_options;
if (desc->operation == svn_wc_operation_update ||
desc->operation == svn_wc_operation_switch)
{
if (desc->reason == svn_wc_conflict_reason_moved_away)
{
tc_opts = tree_conflict_options_update_moved_away;
}
else if (desc->reason == svn_wc_conflict_reason_deleted ||
desc->reason == svn_wc_conflict_reason_replaced)
{
if (desc->action == svn_wc_conflict_action_edit &&
desc->node_kind == svn_node_dir)
tc_opts = tree_conflict_options_update_edit_deleted_dir;
}
}
SVN_ERR(prompt_user(&opt, tc_opts, NULL, b->pb, iterpool));
if (! opt)
continue;
if (strcmp(opt->code, "q") == 0)
{
result->choice = opt->choice;
b->accept_which = svn_cl__accept_postpone;
b->quit = TRUE;
break;
}
else if (opt->choice != svn_wc_conflict_choose_undefined)
{
result->choice = opt->choice;
break;
}
}
svn_pool_destroy(iterpool);
return SVN_NO_ERROR;
}
/* The body of svn_cl__conflict_func_interactive(). */
static svn_error_t *
conflict_func_interactive(svn_wc_conflict_result_t **result,
const svn_wc_conflict_description2_t *desc,
void *baton,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
svn_cl__interactive_conflict_baton_t *b = baton;
svn_error_t *err;
/* Start out assuming we're going to postpone the conflict. */
*result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone,
NULL, result_pool);
switch (b->accept_which)
{
case svn_cl__accept_invalid:
case svn_cl__accept_unspecified:
/* No (or no valid) --accept option, fall through to prompting. */
break;
case svn_cl__accept_postpone:
(*result)->choice = svn_wc_conflict_choose_postpone;
return SVN_NO_ERROR;
case svn_cl__accept_base:
(*result)->choice = svn_wc_conflict_choose_base;
return SVN_NO_ERROR;
case svn_cl__accept_working:
/* If the caller didn't merge the property values, then I guess
* 'choose working' means 'choose mine'... */
if (! desc->merged_file)
(*result)->merged_file = desc->my_abspath;
(*result)->choice = svn_wc_conflict_choose_merged;
return SVN_NO_ERROR;
case svn_cl__accept_mine_conflict:
(*result)->choice = svn_wc_conflict_choose_mine_conflict;
return SVN_NO_ERROR;
case svn_cl__accept_theirs_conflict:
(*result)->choice = svn_wc_conflict_choose_theirs_conflict;
return SVN_NO_ERROR;
case svn_cl__accept_mine_full:
(*result)->choice = svn_wc_conflict_choose_mine_full;
return SVN_NO_ERROR;
case svn_cl__accept_theirs_full:
(*result)->choice = svn_wc_conflict_choose_theirs_full;
return SVN_NO_ERROR;
case svn_cl__accept_edit:
if (desc->merged_file)
{
if (b->external_failed)
{
(*result)->choice = svn_wc_conflict_choose_postpone;
return SVN_NO_ERROR;
}
err = svn_cmdline__edit_file_externally(desc->merged_file,
b->editor_cmd, b->config,
scratch_pool);
if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR ||
err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
{
char buf[1024];
const char *message;
message = svn_err_best_message(err, buf, sizeof(buf));
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n",
message));
svn_error_clear(err);
b->external_failed = TRUE;
}
else if (err)
return svn_error_trace(err);
(*result)->choice = svn_wc_conflict_choose_merged;
return SVN_NO_ERROR;
}
/* else, fall through to prompting. */
break;
case svn_cl__accept_launch:
if (desc->base_abspath && desc->their_abspath
&& desc->my_abspath && desc->merged_file)
{
svn_boolean_t remains_in_conflict;
if (b->external_failed)
{
(*result)->choice = svn_wc_conflict_choose_postpone;
return SVN_NO_ERROR;
}
err = svn_cl__merge_file_externally(desc->base_abspath,
desc->their_abspath,
desc->my_abspath,
desc->merged_file,
desc->local_abspath,
b->config,
&remains_in_conflict,
scratch_pool);
if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL ||
err->apr_err == SVN_ERR_EXTERNAL_PROGRAM))
{
char buf[1024];
const char *message;
message = svn_err_best_message(err, buf, sizeof(buf));
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n",
message));
b->external_failed = TRUE;
return svn_error_trace(err);
}
else if (err)
return svn_error_trace(err);
if (remains_in_conflict)
(*result)->choice = svn_wc_conflict_choose_postpone;
else
(*result)->choice = svn_wc_conflict_choose_merged;
return SVN_NO_ERROR;
}
/* else, fall through to prompting. */
break;
}
/* Print a summary of conflicts before starting interactive resolution */
if (! b->printed_summary)
{
SVN_ERR(svn_cl__print_conflict_stats(b->conflict_stats, scratch_pool));
b->printed_summary = TRUE;
}
/* We're in interactive mode and either the user gave no --accept
option or the option did not apply; let's prompt. */
/* Handle the most common cases, which is either:
Conflicting edits on a file's text, or
Conflicting edits on a property.
*/
if (((desc->kind == svn_wc_conflict_kind_text)
&& (desc->action == svn_wc_conflict_action_edit)
&& (desc->reason == svn_wc_conflict_reason_edited)))
SVN_ERR(handle_text_conflict(*result, desc, b, scratch_pool));
else if (desc->kind == svn_wc_conflict_kind_property)
SVN_ERR(handle_prop_conflict(*result, desc, b, result_pool, scratch_pool));
else if (desc->kind == svn_wc_conflict_kind_tree)
SVN_ERR(handle_tree_conflict(*result, desc, b, scratch_pool));
else /* other types of conflicts -- do nothing about them. */
{
(*result)->choice = svn_wc_conflict_choose_postpone;
}
return SVN_NO_ERROR;
}
svn_error_t *
svn_cl__conflict_func_interactive(svn_wc_conflict_result_t **result,
const svn_wc_conflict_description2_t *desc,
void *baton,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
svn_cl__interactive_conflict_baton_t *b = baton;
SVN_ERR(conflict_func_interactive(result, desc, baton,
result_pool, scratch_pool));
/* If we are resolving a conflict, adjust the summary of conflicts. */
if ((*result)->choice != svn_wc_conflict_choose_postpone)
{
const char *local_path
= svn_cl__local_style_skip_ancestor(
b->path_prefix, desc->local_abspath, scratch_pool);
svn_cl__conflict_stats_resolved(b->conflict_stats, local_path,
desc->kind);
}
return SVN_NO_ERROR;
}