blob: 53b726d74389c288d241229d6690552c2ce7db59 [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])))
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;
if (strcmp(word, SVN_CL__ACCEPT_RECOMMENDED) == 0
|| strcmp(word, "r") == 0)
return svn_cl__accept_recommended;
/* 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 CONFLICT. */
static svn_error_t *
show_diff(svn_client_conflict_t *conflict,
const char *merged_abspath,
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;
const char *my_abspath;
const char *their_abspath;
SVN_ERR(svn_client_conflict_text_get_contents(NULL, &my_abspath, NULL,
&their_abspath,
conflict, pool, pool));
if (merged_abspath)
{
/* 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 between '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 (svn_client_conflict_get_operation(conflict) == svn_wc_operation_merge)
{
path1 = my_abspath;
label1 = _("MINE");
}
else
{
path1 = their_abspath;
label1 = _("THEIRS");
}
path2 = merged_abspath;
label2 = _("MERGED");
}
else
{
/* There's no merged file, but we can show the
difference between mine and theirs. */
path1 = their_abspath;
label1 = _("THEIRS");
path2 = 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 CONFLICT. */
static svn_error_t *
show_conflicts(svn_client_conflict_t *conflict,
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;
const char *base_abspath;
const char *my_abspath;
const char *their_abspath;
SVN_ERR(svn_client_conflict_text_get_contents(NULL, &my_abspath,
&base_abspath, &their_abspath,
conflict, pool, pool));
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, base_abspath, my_abspath, 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, base_abspath, my_abspath, 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_PROPVAL is non-NULL, use it as 'my' version instead of
* MY_ABSPATH.
*
* Assume the values are printable UTF-8 text.
*/
static svn_error_t *
merge_prop_conflict(svn_stream_t *output,
const svn_string_t *base_propval,
const svn_string_t *my_propval,
const svn_string_t *their_propval,
const svn_string_t *merged_propval,
svn_cancel_func_t cancel_func,
void *cancel_baton,
apr_pool_t *pool)
{
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 value instead
* for the purpose of showing a diff. */
if (base_propval == NULL)
base_propval = svn_string_create_empty(pool);
if (my_propval == NULL)
my_propval = svn_string_create_empty(pool);
if (their_propval == NULL)
their_propval = svn_string_create_empty(pool);
options->ignore_eol_style = TRUE;
SVN_ERR(svn_diff_mem_string_diff3(&diff, base_propval,
merged_propval ?
merged_propval : my_propval,
their_propval, options, pool));
SVN_ERR(svn_diff_mem_string_output_merge3(
output, diff, base_propval,
merged_propval ? merged_propval : my_propval, their_propval,
_("||||||| 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_string_t *base_propval,
const svn_string_t *my_propval,
const svn_string_t *their_propval,
const svn_string_t *merged_propval,
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, base_propval, my_propval, their_propval,
merged_propval, cancel_func, cancel_baton, pool));
return SVN_NO_ERROR;
}
/* Run an external editor, passing it the MERGED_ABSPATH, 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_abspath,
const char *editor_cmd,
apr_hash_t *config,
apr_pool_t *pool)
{
svn_error_t *err;
if (merged_abspath)
{
err = svn_cmdline__edit_file_externally(merged_abspath, editor_cmd,
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 on the merged property value with conflict markers.
* Return the edited result in *MERGED_PROPVAL.
* If the edit is aborted, set *MERGED_ABSPATH and *MERGED_PROPVAL to NULL.
* 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 svn_string_t **merged_propval,
const svn_string_t *base_propval,
const svn_string_t *my_propval,
const svn_string_t *their_propval,
const char *editor_cmd,
apr_hash_t *config,
svn_cmdline_prompt_baton_t *pb,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const char *file_path;
svn_boolean_t performed_edit = FALSE;
svn_stream_t *merged_prop;
SVN_ERR(svn_stream_open_unique(&merged_prop, &file_path, NULL,
svn_io_file_del_on_pool_cleanup,
scratch_pool, scratch_pool));
SVN_ERR(merge_prop_conflict(merged_prop, base_propval, my_propval,
their_propval, NULL,
pb->cancel_func,
pb->cancel_baton,
scratch_pool));
SVN_ERR(svn_stream_close(merged_prop));
SVN_ERR(open_editor(&performed_edit, file_path, editor_cmd,
config, scratch_pool));
if (performed_edit && merged_propval)
{
svn_stringbuf_t *buf;
SVN_ERR(svn_stringbuf_from_file2(&buf, file_path, scratch_pool));
*merged_propval = svn_string_create_from_buf(buf, result_pool);
}
return SVN_NO_ERROR;
}
/* Maximum line length for the prompt string. */
#define MAX_PROMPT_WIDTH 70
/* Description of a resolver option.
* Resolver options are used to build the resolver's conflict prompt.
* The user types a code to select the corresponding conflict resolution option.
* Some resolver options have a corresponding --accept argument. */
typedef struct resolver_option_t
{
const char *code; /* one or two characters */
svn_client_conflict_option_id_t choice;
/* or ..._undefined if not from libsvn_client */
const char *accept_arg; /* --accept option argument (NOT localized) */
} resolver_option_t;
typedef struct client_option_t
{
const char *code; /* one or two characters */
const char *label; /* label in prompt (localized) */
const char *long_desc; /* longer description (localized) */
svn_client_conflict_option_id_t choice;
/* or ..._undefined if not from libsvn_client */
const char *accept_arg; /* --accept option argument (NOT localized) */
svn_boolean_t is_recommended; /* if TRUE, try this option before prompting */
} client_option_t;
/* Resolver options for conflict options offered by libsvn_client. */
static const resolver_option_t builtin_resolver_options[] =
{
{ "r", svn_client_conflict_option_merged_text,
SVN_CL__ACCEPT_WORKING },
{ "mc", svn_client_conflict_option_working_text_where_conflicted,
SVN_CL__ACCEPT_MINE_CONFLICT },
{ "tc", svn_client_conflict_option_incoming_text_where_conflicted,
SVN_CL__ACCEPT_THEIRS_CONFLICT },
{ "mf", svn_client_conflict_option_working_text,
SVN_CL__ACCEPT_MINE_FULL},
{ "tf", svn_client_conflict_option_incoming_text,
SVN_CL__ACCEPT_THEIRS_FULL },
{ "p", svn_client_conflict_option_postpone,
SVN_CL__ACCEPT_POSTPONE },
/* This option resolves a tree conflict to the current working copy state. */
{ "r", svn_client_conflict_option_accept_current_wc_state,
SVN_CL__ACCEPT_WORKING },
/* These options use the same code since they only occur in
* distinct conflict scenarios. */
{ "u", svn_client_conflict_option_update_move_destination },
{ "u", svn_client_conflict_option_update_any_moved_away_children },
/* Options for incoming add vs local add. */
{ "i", svn_client_conflict_option_incoming_add_ignore },
/* Options for incoming file add vs local file add upon merge. */
{ "m", svn_client_conflict_option_incoming_added_file_text_merge },
{ "M", svn_client_conflict_option_incoming_added_file_replace_and_merge },
/* Options for incoming dir add vs local dir add upon merge. */
{ "m", svn_client_conflict_option_incoming_added_dir_merge },
{ "R", svn_client_conflict_option_incoming_added_dir_replace },
{ "M", svn_client_conflict_option_incoming_added_dir_replace_and_merge },
/* Options for incoming delete vs any. */
{ "i", svn_client_conflict_option_incoming_delete_ignore },
{ "a", svn_client_conflict_option_incoming_delete_accept },
/* Options for incoming move vs local edit. */
{ "m", svn_client_conflict_option_incoming_move_file_text_merge },
{ "m", svn_client_conflict_option_incoming_move_dir_merge },
/* Options for local move vs incoming edit. */
{ "m", svn_client_conflict_option_local_move_file_text_merge },
{ "m", svn_client_conflict_option_local_move_dir_merge },
/* Options for local missing vs incoming edit. */
{ "m", svn_client_conflict_option_sibling_move_file_text_merge },
{ "m", svn_client_conflict_option_sibling_move_dir_merge },
/* Options for incoming move vs local move. */
{ "m", svn_client_conflict_option_both_moved_file_merge },
{ "M", svn_client_conflict_option_both_moved_file_move_merge },
{ "m", svn_client_conflict_option_both_moved_dir_merge },
{ "M", svn_client_conflict_option_both_moved_dir_move_merge },
{ NULL }
};
/* Extra resolver options offered by 'svn' for any conflict. */
static const client_option_t extra_resolver_options[] =
{
/* Translators: keep long_desc below 70 characters (wrap with a left
margin of 9 spaces if needed) */
{ "q", N_("Quit resolution"), N_("postpone all remaining conflicts"),
svn_client_conflict_option_postpone },
{ NULL }
};
/* Additional resolver options offered by 'svn' for a text conflict. */
static const client_option_t extra_resolver_options_text[] =
{
/* Translators: keep long_desc below 70 characters (wrap with a left
margin of 9 spaces if needed) */
{ "e", N_("Edit file"), N_("change merged file in an editor"),
svn_client_conflict_option_undefined,
SVN_CL__ACCEPT_EDIT },
{ "df", N_("Show diff"), N_("show all changes made to merged file"),
svn_client_conflict_option_undefined},
{ "dc", N_("Display conflict"), N_("show all conflicts "
"(ignoring merged version)"),
svn_client_conflict_option_undefined },
{ "m", N_("Merge"), N_("use merge tool to resolve conflict"),
svn_client_conflict_option_undefined },
{ "l", N_("Launch tool"), N_("launch external merge tool to resolve "
"conflict"),
svn_client_conflict_option_undefined,
SVN_CL__ACCEPT_LAUNCH },
{ "i", N_("Internal merge tool"), N_("use built-in merge tool to "
"resolve conflict"),
svn_client_conflict_option_undefined },
{ "s", N_("Show all options"), N_("show this list (also 'h', '?')"),
svn_client_conflict_option_undefined },
{ NULL }
};
/* Additional resolver options offered by 'svn' for a property conflict. */
static const client_option_t extra_resolver_options_prop[] =
{
/* Translators: keep long_desc below 70 characters (wrap with a left
margin of 9 spaces if needed) */
{ "dc", N_("Display conflict"), N_("show conflicts in this property"),
svn_client_conflict_option_undefined },
{ "e", N_("Edit property"), N_("change merged property value in an "
"editor"),
svn_client_conflict_option_undefined,
SVN_CL__ACCEPT_EDIT },
{ "h", N_("Help"), N_("show this help (also '?')"),
svn_client_conflict_option_undefined },
{ NULL }
};
/* Additional resolver options offered by 'svn' for a tree conflict. */
static const client_option_t extra_resolver_options_tree[] =
{
/* Translators: keep long_desc below 70 characters (wrap with a left
margin of 9 spaces if needed) */
{ "d", N_("Set repository move destination path"),
N_("pick repository move target from list of possible targets"),
svn_client_conflict_option_undefined },
{ "w", N_("Set working copy move destination path"),
N_("pick working copy move target from list of possible targets"),
svn_client_conflict_option_undefined },
{ "h", N_("Help"), N_("show this help (also '?')"),
svn_client_conflict_option_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 client_option_t *
find_option(const apr_array_header_t *options,
const char *option_code)
{
int i;
for (i = 0; i < options->nelts; i++)
{
const client_option_t *opt = APR_ARRAY_IDX(options, i, client_option_t *);
/* Ignore code "" (blank lines) which is not a valid answer. */
if (opt->code[0] && strcmp(opt->code, option_code) == 0)
return opt;
}
return NULL;
}
/* Find the first recommended option in OPTIONS. */
static const client_option_t *
find_recommended_option(const apr_array_header_t *options)
{
int i;
for (i = 0; i < options->nelts; i++)
{
const client_option_t *opt = APR_ARRAY_IDX(options, i, client_option_t *);
/* Ignore code "" (blank lines) which is not a valid answer. */
if (opt->code[0] && opt->is_recommended)
return opt;
}
return NULL;
}
/* Return a pointer to the client_option_t in OPTIONS matching the ID of
* conflict option BUILTIN_OPTION. @a out will be set to NULL if the
* option was not found. */
static svn_error_t *
find_option_by_builtin(client_option_t **out,
svn_client_conflict_t *conflict,
const resolver_option_t *options,
svn_client_conflict_option_t *builtin_option,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const resolver_option_t *opt;
svn_client_conflict_option_id_t id;
svn_client_conflict_option_id_t recommended_id;
id = svn_client_conflict_option_get_id(builtin_option);
recommended_id = svn_client_conflict_get_recommended_option_id(conflict);
for (opt = options; opt->code; opt++)
{
if (opt->choice == id)
{
client_option_t *client_opt;
client_opt = apr_pcalloc(result_pool, sizeof(*client_opt));
client_opt->choice = id;
client_opt->code = opt->code;
client_opt->label = svn_client_conflict_option_get_label(
builtin_option,
result_pool);
client_opt->long_desc = svn_client_conflict_option_get_description(
builtin_option,
result_pool);
client_opt->accept_arg = opt->accept_arg;
client_opt->is_recommended =
(recommended_id != svn_client_conflict_option_unspecified &&
id == recommended_id);
*out = client_opt;
return SVN_NO_ERROR;
}
}
*out = NULL;
return SVN_NO_ERROR;
}
/* 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 apr_array_header_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;
int i = 0;
while (1)
{
const client_option_t *opt;
const char *s;
int slen;
if (option_codes)
{
if (! *option_codes)
break;
opt = find_option(options, *option_codes++);
if (opt == NULL)
continue;
}
else
{
if (i >= options->nelts)
break;
opt = APR_ARRAY_IDX(options, i, client_option_t *);
i++;
}
if (! first)
result = apr_pstrcat(pool, result, ",", SVN_VA_NULL);
s = apr_psprintf(pool, " (%s) %s", opt->code,
opt->label ? opt->label : opt->long_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 svn_error_t *
help_string(const char **result,
const apr_array_header_t *options,
apr_pool_t *pool)
{
apr_pool_t *iterpool;
int i;
*result = "";
iterpool = svn_pool_create(pool);
for (i = 0; i < options->nelts; i++)
{
const client_option_t *opt;
svn_pool_clear(iterpool);
opt = APR_ARRAY_IDX(options, i,
client_option_t *);
/* 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);
if (opt->accept_arg)
*result = apr_psprintf(pool, "%s%-6s - %s [%s]\n",
*result, s, opt->long_desc,
opt->accept_arg);
else
*result = apr_psprintf(pool, "%s%-6s - %s\n", *result, s,
opt->long_desc);
}
else
{
*result = apr_pstrcat(pool, *result, "\n", SVN_VA_NULL);
}
}
svn_pool_destroy(iterpool);
*result = apr_pstrcat(pool, *result,
_("Words in square brackets are the corresponding "
"--accept option arguments.\n"),
SVN_VA_NULL);
return SVN_NO_ERROR;
}
/* 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
* CONFLICT_DESCRIPTION (if not NULL) and help (on stderr) and return with
* *OPT == NULL.
*/
static svn_error_t *
prompt_user(const client_option_t **opt,
const apr_array_header_t *conflict_options,
const char *const *options_to_show,
const char *conflict_description,
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)
{
const char *helpstr;
if (conflict_description)
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n",
conflict_description));
SVN_ERR(help_string(&helpstr, conflict_options, scratch_pool));
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n", helpstr));
*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;
}
/* Set *OPTIONS to an array of resolution options for CONFLICT. */
static svn_error_t *
build_text_conflict_options(apr_array_header_t **options,
svn_client_conflict_t *conflict,
svn_client_ctx_t *ctx,
svn_boolean_t is_binary,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const client_option_t *o;
apr_array_header_t *builtin_options;
int nopt;
int i;
apr_pool_t *iterpool;
SVN_ERR(svn_client_conflict_text_get_resolution_options(&builtin_options,
conflict, ctx,
scratch_pool,
scratch_pool));
nopt = builtin_options->nelts + ARRAY_LEN(extra_resolver_options);
if (!is_binary)
nopt += ARRAY_LEN(extra_resolver_options_text);
*options = apr_array_make(result_pool, nopt, sizeof(client_option_t *));
iterpool = svn_pool_create(scratch_pool);
for (i = 0; i < builtin_options->nelts; i++)
{
client_option_t *opt;
svn_client_conflict_option_t *builtin_option;
svn_pool_clear(iterpool);
builtin_option = APR_ARRAY_IDX(builtin_options, i,
svn_client_conflict_option_t *);
SVN_ERR(find_option_by_builtin(&opt, conflict,
builtin_resolver_options,
builtin_option,
result_pool,
iterpool));
if (opt == NULL)
continue; /* ### unknown option -- assign a code dynamically? */
APR_ARRAY_PUSH(*options, client_option_t *) = opt;
}
for (o = extra_resolver_options; o->code; o++)
APR_ARRAY_PUSH(*options, const client_option_t *) = o;
if (!is_binary)
{
for (o = extra_resolver_options_text; o->code; o++)
APR_ARRAY_PUSH(*options, const client_option_t *) = o;
}
svn_pool_destroy(iterpool);
return SVN_NO_ERROR;
}
/* Mark CONFLICT as resolved to resolution option with ID OPTION_ID.
* If TEXT_CONFLICTED is true, resolve text conflicts described by CONFLICT.
* IF PROPNAME is not NULL, mark the conflict in the specified property as
* resolved. If PROPNAME is "", mark all property conflicts described by
* CONFLICT as resolved.
* If TREE_CONFLICTED is true, resolve tree conflicts described by CONFLICT.
* Adjust CONFLICT_STATS as necessary (PATH_PREFIX is needed for this step). */
static svn_error_t *
mark_conflict_resolved(svn_client_conflict_t *conflict,
svn_client_conflict_option_id_t option_id,
svn_boolean_t text_conflicted,
const char *propname,
svn_boolean_t tree_conflicted,
const char *path_prefix,
svn_cl__conflict_stats_t *conflict_stats,
svn_client_ctx_t *ctx,
apr_pool_t *scratch_pool)
{
const char *local_relpath
= svn_cl__local_style_skip_ancestor(
path_prefix, svn_client_conflict_get_local_abspath(conflict),
scratch_pool);
if (text_conflicted)
{
SVN_ERR(svn_client_conflict_text_resolve_by_id(conflict, option_id,
ctx, scratch_pool));
svn_cl__conflict_stats_resolved(conflict_stats, local_relpath,
svn_wc_conflict_kind_text);
}
if (propname)
{
SVN_ERR(svn_client_conflict_prop_resolve_by_id(conflict, propname,
option_id, ctx,
scratch_pool));
svn_cl__conflict_stats_resolved(conflict_stats, local_relpath,
svn_wc_conflict_kind_property);
}
if (tree_conflicted)
{
SVN_ERR(svn_client_conflict_tree_resolve_by_id(conflict, option_id,
ctx, scratch_pool));
svn_cl__conflict_stats_resolved(conflict_stats, local_relpath,
svn_wc_conflict_kind_tree);
}
return SVN_NO_ERROR;
}
/* Ask the user what to do about the text conflict described by CONFLICT
* and either resolve the conflict accordingly or postpone resolution.
* SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_text_conflict(svn_boolean_t *resolved,
svn_boolean_t *postponed,
svn_boolean_t *quit,
svn_boolean_t *printed_description,
svn_client_conflict_t *conflict,
const char *path_prefix,
svn_cmdline_prompt_baton_t *pb,
const char *editor_cmd,
apr_hash_t *config,
svn_cl__conflict_stats_t *conflict_stats,
svn_client_ctx_t *ctx,
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? */
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;
const char *local_abspath = svn_client_conflict_get_local_abspath(conflict);
const char *mime_type = svn_client_conflict_text_get_mime_type(conflict);
svn_boolean_t is_binary = mime_type ? svn_mime_type_is_binary(mime_type)
: FALSE;
const char *base_abspath;
const char *my_abspath;
const char *their_abspath;
const char *merged_abspath = svn_client_conflict_get_local_abspath(conflict);
apr_array_header_t *text_conflict_options;
svn_client_conflict_option_id_t option_id;
option_id = svn_client_conflict_option_unspecified;
SVN_ERR(svn_client_conflict_text_get_contents(NULL, &my_abspath,
&base_abspath, &their_abspath,
conflict, scratch_pool,
scratch_pool));
local_relpath = svn_cl__local_style_skip_ancestor(path_prefix,
local_abspath,
scratch_pool);
if (!*printed_description)
{
if (is_binary)
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
_("Merge conflict discovered in binary "
"file '%s'.\n"),
local_relpath));
else
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
_("Merge conflict discovered in file '%s'.\n"),
local_relpath));
*printed_description = TRUE;
}
/* ### 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 (!is_binary &&
((merged_abspath && base_abspath)
|| (!base_abspath && my_abspath && their_abspath)))
diff_allowed = TRUE;
SVN_ERR(build_text_conflict_options(&text_conflict_options, conflict, ctx,
is_binary, scratch_pool, scratch_pool));
while (TRUE)
{
const char *suggested_options[9]; /* filled statically below */
const char **next_option = suggested_options;
const client_option_t *opt;
svn_pool_clear(iterpool);
*next_option++ = "p";
if (diff_allowed)
{
/* We need one more path for this feature. */
if (my_abspath)
*next_option++ = "df";
*next_option++ = "e";
/* We need one more path for this feature. */
if (my_abspath)
*next_option++ = "m";
if (knows_something)
*next_option++ = "r";
}
else
{
if (knows_something || is_binary)
*next_option++ = "r";
/* The 'mine-full' option selects the ".mine" file for texts or
* the current working directory file for binary files. */
if (my_abspath || is_binary)
*next_option++ = "mf";
*next_option++ = "tf";
}
*next_option++ = "s";
*next_option++ = NULL;
SVN_ERR(prompt_user(&opt, text_conflict_options, suggested_options,
NULL, pb, iterpool));
if (! opt)
continue;
if (strcmp(opt->code, "q") == 0)
{
option_id = opt->choice;
*quit = TRUE;
break;
}
else if (strcmp(opt->code, "s") == 0)
{
const char *helpstr;
SVN_ERR(help_string(&helpstr, text_conflict_options, iterpool));
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n",
helpstr));
}
else if (strcmp(opt->code, "dc") == 0)
{
if (is_binary)
{
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
_("Invalid option; cannot "
"display conflicts for a "
"binary file.\n\n")));
continue;
}
else if (! (my_abspath && base_abspath && their_abspath))
{
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool,
_("Invalid option; original "
"files not available.\n\n")));
continue;
}
SVN_ERR(show_conflicts(conflict,
pb->cancel_func,
pb->cancel_baton,
iterpool));
knows_something = TRUE;
}
else if (strcmp(opt->code, "df") == 0)
{
/* Re-check preconditions. */
if (! diff_allowed || ! 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(conflict, merged_abspath, path_prefix,
pb->cancel_func, 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, merged_abspath, editor_cmd,
config, 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 (! 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(base_abspath,
their_abspath,
my_abspath,
merged_abspath,
local_abspath, 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,
base_abspath,
their_abspath,
my_abspath,
merged_abspath,
local_abspath,
path_prefix,
editor_cmd,
config,
pb->cancel_func,
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_abspath' shouldn't be necessary *before* we launch the
* resolver: it should be the *result* of doing so. */
if (base_abspath && their_abspath && my_abspath && merged_abspath)
{
svn_error_t *err;
char buf[1024];
const char *message;
err = svn_cl__merge_file_externally(base_abspath,
their_abspath,
my_abspath,
merged_abspath,
local_abspath,
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,
base_abspath,
their_abspath,
my_abspath,
merged_abspath,
local_abspath,
path_prefix,
editor_cmd,
config,
pb->cancel_func,
pb->cancel_baton,
iterpool));
if (!remains_in_conflict)
knows_something = TRUE;
}
else if (opt->choice != svn_client_conflict_option_undefined)
{
if ((opt->choice == svn_client_conflict_option_working_text_where_conflicted
|| opt->choice == svn_client_conflict_option_incoming_text_where_conflicted)
&& 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_client_conflict_option_merged_text
&& ! 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;
}
option_id = opt->choice;
break;
}
}
svn_pool_destroy(iterpool);
if (option_id != svn_client_conflict_option_unspecified &&
option_id != svn_client_conflict_option_postpone)
{
SVN_ERR(mark_conflict_resolved(conflict, option_id,
TRUE, NULL, FALSE,
path_prefix, conflict_stats,
ctx, scratch_pool));
*resolved = TRUE;
}
else
{
*resolved = FALSE;
*postponed = (option_id == svn_client_conflict_option_postpone);
}
return SVN_NO_ERROR;
}
/* Set *OPTIONS to an array of resolution options for CONFLICT. */
static svn_error_t *
build_prop_conflict_options(apr_array_header_t **options,
svn_client_conflict_t *conflict,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const client_option_t *o;
apr_array_header_t *builtin_options;
int nopt;
int i;
apr_pool_t *iterpool;
SVN_ERR(svn_client_conflict_prop_get_resolution_options(&builtin_options,
conflict, ctx,
scratch_pool,
scratch_pool));
nopt = builtin_options->nelts + ARRAY_LEN(extra_resolver_options) +
ARRAY_LEN(extra_resolver_options_prop);
*options = apr_array_make(result_pool, nopt, sizeof(client_option_t *));
iterpool = svn_pool_create(scratch_pool);
for (i = 0; i < builtin_options->nelts; i++)
{
client_option_t *opt;
svn_client_conflict_option_t *builtin_option;
svn_pool_clear(iterpool);
builtin_option = APR_ARRAY_IDX(builtin_options, i,
svn_client_conflict_option_t *);
SVN_ERR(find_option_by_builtin(&opt, conflict,
builtin_resolver_options,
builtin_option,
result_pool,
iterpool));
if (opt == NULL)
continue; /* ### unknown option -- assign a code dynamically? */
APR_ARRAY_PUSH(*options, client_option_t *) = opt;
}
svn_pool_destroy(iterpool);
for (o = extra_resolver_options; o->code; o++)
APR_ARRAY_PUSH(*options, const client_option_t *) = o;
for (o = extra_resolver_options_prop; o->code; o++)
APR_ARRAY_PUSH(*options, const client_option_t *) = o;
return SVN_NO_ERROR;
}
/* Ask the user what to do about the conflicted property PROPNAME described
* by CONFLICT and return the corresponding resolution option in *OPTION.
* SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_one_prop_conflict(svn_client_conflict_option_t **option,
svn_boolean_t *quit,
const char *path_prefix,
svn_cmdline_prompt_baton_t *pb,
const char *editor_cmd,
apr_hash_t *config,
svn_client_conflict_t *conflict,
const char *propname,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
apr_pool_t *iterpool;
const char *description;
const svn_string_t *merged_propval = NULL;
svn_boolean_t resolved_allowed = FALSE;
const svn_string_t *base_propval;
const svn_string_t *my_propval;
const svn_string_t *their_propval;
apr_array_header_t *resolution_options;
apr_array_header_t *prop_conflict_options;
SVN_ERR(svn_client_conflict_prop_get_propvals(NULL, &my_propval,
&base_propval, &their_propval,
conflict, propname,
scratch_pool));
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
_("Conflict for property '%s' discovered"
" on '%s'.\n"),
propname,
svn_cl__local_style_skip_ancestor(
path_prefix,
svn_client_conflict_get_local_abspath(conflict),
scratch_pool)));
SVN_ERR(svn_client_conflict_prop_get_description(&description, conflict,
scratch_pool, scratch_pool));
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n", description));
SVN_ERR(svn_client_conflict_prop_get_resolution_options(&resolution_options,
conflict, ctx,
result_pool,
scratch_pool));
SVN_ERR(build_prop_conflict_options(&prop_conflict_options, conflict, ctx,
scratch_pool, scratch_pool));
iterpool = svn_pool_create(scratch_pool);
while (TRUE)
{
const client_option_t *opt;
const char *suggested_options[9]; /* filled statically below */
const char **next_option = suggested_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, suggested_options,
NULL, pb, iterpool));
if (! opt)
continue;
if (strcmp(opt->code, "q") == 0)
{
*option = svn_client_conflict_option_find_by_id(resolution_options,
opt->choice);
*quit = TRUE;
break;
}
else if (strcmp(opt->code, "dc") == 0)
{
SVN_ERR(show_prop_conflict(base_propval, my_propval, their_propval,
merged_propval,
pb->cancel_func, pb->cancel_baton,
scratch_pool));
}
else if (strcmp(opt->code, "e") == 0)
{
SVN_ERR(edit_prop_conflict(&merged_propval,
base_propval, my_propval, their_propval,
editor_cmd, config, pb,
result_pool, scratch_pool));
resolved_allowed = (merged_propval != 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;
}
*option = svn_client_conflict_option_find_by_id(
resolution_options,
svn_client_conflict_option_merged_text);
svn_client_conflict_option_set_merged_propval(*option,
merged_propval);
break;
}
else if (opt->choice != svn_client_conflict_option_undefined)
{
*option = svn_client_conflict_option_find_by_id(resolution_options,
opt->choice);
break;
}
}
svn_pool_destroy(iterpool);
return SVN_NO_ERROR;
}
/* Ask the user what to do about the property conflicts described by CONFLICT
* and either resolve them accordingly or postpone resolution.
* SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_prop_conflicts(svn_boolean_t *resolved,
svn_boolean_t *postponed,
svn_boolean_t *quit,
const svn_string_t **merged_value,
const char *path_prefix,
svn_cmdline_prompt_baton_t *pb,
const char *editor_cmd,
apr_hash_t *config,
svn_client_conflict_t *conflict,
svn_cl__conflict_stats_t *conflict_stats,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
apr_array_header_t *props_conflicted;
apr_pool_t *iterpool;
int i;
int nresolved = 0;
SVN_ERR(svn_client_conflict_get_conflicted(NULL, &props_conflicted, NULL,
conflict, scratch_pool,
scratch_pool));
iterpool = svn_pool_create(scratch_pool);
for (i = 0; i < props_conflicted->nelts; i++)
{
const char *propname = APR_ARRAY_IDX(props_conflicted, i, const char *);
svn_client_conflict_option_t *option;
svn_client_conflict_option_id_t option_id;
svn_pool_clear(iterpool);
SVN_ERR(handle_one_prop_conflict(&option, quit, path_prefix, pb,
editor_cmd, config, conflict, propname,
ctx,
iterpool, iterpool));
option_id = svn_client_conflict_option_get_id(option);
if (option_id != svn_client_conflict_option_unspecified &&
option_id != svn_client_conflict_option_postpone)
{
const char *local_relpath =
svn_cl__local_style_skip_ancestor(
path_prefix, svn_client_conflict_get_local_abspath(conflict),
iterpool);
SVN_ERR(svn_client_conflict_prop_resolve(conflict, propname, option,
ctx, iterpool));
svn_cl__conflict_stats_resolved(conflict_stats, local_relpath,
svn_wc_conflict_kind_property);
nresolved++;
*postponed = FALSE;
}
else
*postponed = (option_id == svn_client_conflict_option_postpone);
if (*quit)
break;
}
svn_pool_destroy(iterpool);
/* Indicate success if no property conflicts remain. */
*resolved = (nresolved == props_conflicted->nelts);
return SVN_NO_ERROR;
}
/* Set *OPTIONS to an array of resolution options for CONFLICT. */
static svn_error_t *
build_tree_conflict_options(
apr_array_header_t **options,
apr_array_header_t **possible_moved_to_repos_relpaths,
apr_array_header_t **possible_moved_to_abspaths,
svn_boolean_t *all_options_are_dumb,
svn_client_conflict_t *conflict,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const client_option_t *o;
apr_array_header_t *builtin_options;
int nopt;
int i;
int next_unknown_option_code = 1;
apr_pool_t *iterpool;
if (all_options_are_dumb != NULL)
*all_options_are_dumb = TRUE;
SVN_ERR(svn_client_conflict_tree_get_resolution_options(&builtin_options,
conflict, ctx,
scratch_pool,
scratch_pool));
nopt = builtin_options->nelts + ARRAY_LEN(extra_resolver_options_tree) +
ARRAY_LEN(extra_resolver_options);
*options = apr_array_make(result_pool, nopt, sizeof(client_option_t *));
*possible_moved_to_abspaths = NULL;
*possible_moved_to_repos_relpaths = NULL;
iterpool = svn_pool_create(scratch_pool);
for (i = 0; i < builtin_options->nelts; i++)
{
client_option_t *opt;
svn_client_conflict_option_t *builtin_option;
svn_client_conflict_option_id_t id;
svn_pool_clear(iterpool);
builtin_option = APR_ARRAY_IDX(builtin_options, i,
svn_client_conflict_option_t *);
SVN_ERR(find_option_by_builtin(&opt, conflict,
builtin_resolver_options,
builtin_option,
result_pool,
iterpool));
if (opt == NULL)
{
/* Unknown option. Assign a dynamic option code. */
opt = apr_pcalloc(result_pool, sizeof(*opt));
opt->code = apr_psprintf(result_pool, "%d", next_unknown_option_code);
next_unknown_option_code++;
opt->label = svn_client_conflict_option_get_label(builtin_option,
result_pool);
opt->long_desc = svn_client_conflict_option_get_description(
builtin_option, result_pool);
opt->choice = svn_client_conflict_option_get_id(builtin_option);
opt->accept_arg = NULL;
}
APR_ARRAY_PUSH(*options, client_option_t *) = opt;
id = svn_client_conflict_option_get_id(builtin_option);
/* Check if we got a "smart" tree conflict option. */
if (all_options_are_dumb != NULL &&
*all_options_are_dumb &&
id != svn_client_conflict_option_postpone &&
id != svn_client_conflict_option_accept_current_wc_state)
*all_options_are_dumb = FALSE;
if (*possible_moved_to_repos_relpaths == NULL)
SVN_ERR(
svn_client_conflict_option_get_moved_to_repos_relpath_candidates2(
possible_moved_to_repos_relpaths, builtin_option,
result_pool, iterpool));
if (*possible_moved_to_abspaths == NULL)
SVN_ERR(svn_client_conflict_option_get_moved_to_abspath_candidates2(
possible_moved_to_abspaths, builtin_option,
result_pool, iterpool));
}
svn_pool_destroy(iterpool);
for (o = extra_resolver_options_tree; o->code; o++)
{
/* Add move target choice options only if there are multiple
* move targets to choose from. */
if (strcmp(o->code, "d") == 0 &&
(*possible_moved_to_repos_relpaths == NULL ||
(*possible_moved_to_repos_relpaths)->nelts <= 1))
continue;
if (strcmp(o->code, "w") == 0 &&
(*possible_moved_to_abspaths == NULL ||
(*possible_moved_to_abspaths)->nelts <= 1))
continue;
APR_ARRAY_PUSH(*options, const client_option_t *) = o;
}
for (o = extra_resolver_options; o->code; o++)
APR_ARRAY_PUSH(*options, const client_option_t *) = o;
return SVN_NO_ERROR;
}
/* Make the user select a move target path for the moved-away VICTIM_ABSPATH. */
static svn_error_t *
prompt_move_target_path(int *preferred_move_target_idx,
apr_array_header_t *possible_moved_to_paths,
svn_boolean_t paths_are_local,
svn_cmdline_prompt_baton_t *pb,
const char *victim_abspath,
svn_client_ctx_t *ctx,
apr_pool_t *scratch_pool)
{
const char *move_targets_prompt = "";
const char *move_targets_list = "";
const char *wcroot_abspath;
const char *victim_relpath;
int i;
apr_int64_t idx;
apr_pool_t *iterpool;
SVN_ERR(svn_client_get_wc_root(&wcroot_abspath, victim_abspath,
ctx, scratch_pool, scratch_pool));
victim_relpath = svn_cl__local_style_skip_ancestor(wcroot_abspath,
victim_abspath,
scratch_pool),
iterpool = svn_pool_create(scratch_pool);
/* Build the prompt. */
for (i = 0; i < possible_moved_to_paths->nelts; i++)
{
svn_pool_clear(iterpool);
if (paths_are_local)
{
const char *moved_to_abspath;
const char *moved_to_relpath;
moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_paths, i,
const char *);
moved_to_relpath = svn_cl__local_style_skip_ancestor(
wcroot_abspath, moved_to_abspath, iterpool),
move_targets_list = apr_psprintf(scratch_pool, "%s (%d): '%s'\n",
move_targets_list, i + 1,
moved_to_relpath);
}
else
{
const char *moved_to_repos_relpath;
moved_to_repos_relpath = APR_ARRAY_IDX(possible_moved_to_paths, i,
const char *);
move_targets_list = apr_psprintf(scratch_pool, "%s (%d): '^/%s'\n",
move_targets_list, i + 1,
moved_to_repos_relpath);
}
}
if (paths_are_local)
move_targets_prompt =
apr_psprintf(scratch_pool,
_("Possible working copy destinations for moved-away '%s' "
"are:\n%s"
"Only one destination can be a move; the others are "
"copies.\n"
"Specify the correct move target path by number: "),
victim_relpath, move_targets_list);
else
move_targets_prompt =
apr_psprintf(scratch_pool,
_("Possible repository destinations for moved-away '%s' "
"are:\n%s"
"Only one destination can be a move; the others are "
"copies.\n"
"Specify the correct move target path by number: "),
victim_relpath, move_targets_list);
/* Keep asking the user until we got a valid choice. */
while (1)
{
const char *answer;
svn_error_t *err;
svn_pool_clear(iterpool);
SVN_ERR(svn_cmdline_prompt_user2(&answer, move_targets_prompt,
pb, iterpool));
err = svn_cstring_strtoi64(&idx, answer, 1,
possible_moved_to_paths->nelts, 10);
if (err)
{
char buf[1024];
SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, "%s\n",
svn_err_best_message(err, buf, sizeof(buf))));
svn_error_clear(err);
continue;
}
break;
}
svn_pool_destroy(iterpool);
SVN_ERR_ASSERT((idx - 1) == (int)(idx - 1));
*preferred_move_target_idx = (int)(idx - 1);
return SVN_NO_ERROR;
}
static svn_error_t *
find_conflict_option_with_repos_move_targets(
svn_client_conflict_option_t **option_with_move_targets,
apr_array_header_t *options,
apr_pool_t *scratch_pool)
{
apr_pool_t *iterpool = svn_pool_create(scratch_pool);
int i;
apr_array_header_t *possible_moved_to_repos_relpaths = NULL;
*option_with_move_targets = NULL;
for (i = 0; i < options->nelts; i++)
{
svn_client_conflict_option_t *option;
svn_pool_clear(iterpool);
option = APR_ARRAY_IDX(options, i, svn_client_conflict_option_t *);
SVN_ERR(svn_client_conflict_option_get_moved_to_repos_relpath_candidates2(
&possible_moved_to_repos_relpaths, option, iterpool, iterpool));
if (possible_moved_to_repos_relpaths)
{
*option_with_move_targets = option;
break;
}
}
svn_pool_destroy(iterpool);
return SVN_NO_ERROR;
}
static svn_error_t *
find_conflict_option_with_working_copy_move_targets(
svn_client_conflict_option_t **option_with_move_targets,
apr_array_header_t *options,
apr_pool_t *scratch_pool)
{
apr_pool_t *iterpool = svn_pool_create(scratch_pool);
int i;
apr_array_header_t *possible_moved_to_abspaths = NULL;
*option_with_move_targets = NULL;
for (i = 0; i < options->nelts; i++)
{
svn_client_conflict_option_t *option;
svn_pool_clear(iterpool);
option = APR_ARRAY_IDX(options, i, svn_client_conflict_option_t *);
SVN_ERR(svn_client_conflict_option_get_moved_to_abspath_candidates2(
&possible_moved_to_abspaths, option, scratch_pool,
iterpool));
if (possible_moved_to_abspaths)
{
*option_with_move_targets = option;
break;
}
}
svn_pool_destroy(iterpool);
return SVN_NO_ERROR;
}
/* Ask the user what to do about the tree conflict described by CONFLICT
* and either resolve the conflict accordingly or postpone resolution.
* SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
handle_tree_conflict(svn_boolean_t *resolved,
svn_boolean_t *postponed,
svn_boolean_t *quit,
svn_boolean_t *printed_description,
svn_client_conflict_t *conflict,
const char *path_prefix,
svn_cmdline_prompt_baton_t *pb,
svn_cl__conflict_stats_t *conflict_stats,
svn_client_ctx_t *ctx,
apr_pool_t *scratch_pool)
{
apr_pool_t *iterpool;
apr_array_header_t *tree_conflict_options;
svn_client_conflict_option_id_t option_id;
const char *local_abspath;
const char *conflict_description;
const char *local_change_description;
const char *incoming_change_description;
apr_array_header_t *possible_moved_to_repos_relpaths;
apr_array_header_t *possible_moved_to_abspaths;
svn_boolean_t all_options_are_dumb;
const struct client_option_t *recommended_option;
svn_boolean_t repos_move_target_chosen = FALSE;
svn_boolean_t wc_move_target_chosen = FALSE;
option_id = svn_client_conflict_option_unspecified;
local_abspath = svn_client_conflict_get_local_abspath(conflict);
/* Always show the best possible conflict description and options. */
SVN_ERR(svn_client_conflict_tree_get_details(conflict, ctx, scratch_pool));
SVN_ERR(svn_client_conflict_tree_get_description(
&incoming_change_description, &local_change_description,
conflict, ctx, scratch_pool, scratch_pool));
conflict_description = apr_psprintf(scratch_pool, "%s\n%s",
incoming_change_description,
local_change_description);
if (!*printed_description)
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
_("Tree conflict on '%s':\n%s\n"),
svn_cl__local_style_skip_ancestor(
path_prefix, local_abspath, scratch_pool),
conflict_description));
SVN_ERR(build_tree_conflict_options(&tree_conflict_options,
&possible_moved_to_repos_relpaths,
&possible_moved_to_abspaths,
&all_options_are_dumb,
conflict, ctx,
scratch_pool, scratch_pool));
/* Try a recommended resolution option before prompting. */
recommended_option = find_recommended_option(tree_conflict_options);
if (recommended_option)
{
svn_error_t *err;
apr_status_t root_cause;
SVN_ERR(svn_cmdline_printf(scratch_pool,
_("Applying recommended resolution '%s':\n"),
recommended_option->label));
err = mark_conflict_resolved(conflict, recommended_option->choice,
FALSE, NULL, TRUE,
path_prefix, conflict_stats,
ctx, scratch_pool);
if (!err)
{
*resolved = TRUE;
return SVN_NO_ERROR;
}
root_cause = svn_error_root_cause(err)->apr_err;
if (root_cause != SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE &&
root_cause != SVN_ERR_WC_OBSTRUCTED_UPDATE &&
root_cause != SVN_ERR_WC_FOUND_CONFLICT)
return svn_error_trace(err);
/* Fall back to interactive prompting. */
svn_error_clear(err);
}
if (all_options_are_dumb)
SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool,
_("\nSubversion is not smart enough to resolve "
"this tree conflict automatically!\nSee 'svn "
"help resolve' for more information.\n\n")));
iterpool = svn_pool_create(scratch_pool);
while (1)
{
const client_option_t *opt;
svn_pool_clear(iterpool);
if (!repos_move_target_chosen &&
possible_moved_to_repos_relpaths &&
possible_moved_to_repos_relpaths->nelts > 1)
SVN_ERR(svn_cmdline_printf(scratch_pool,
_("Ambiguous move destinations exist in the repository; "
"try the 'd' option\n")));
if (!wc_move_target_chosen && possible_moved_to_abspaths &&
possible_moved_to_abspaths->nelts > 1)
SVN_ERR(svn_cmdline_printf(scratch_pool,
_("Ambiguous move destinations exist in the working copy; "
"try the 'w' option\n")));
SVN_ERR(prompt_user(&opt, tree_conflict_options, NULL,
conflict_description, pb, iterpool));
*printed_description = TRUE;
if (! opt)
continue;
if (strcmp(opt->code, "q") == 0)
{
option_id = opt->choice;
*quit = TRUE;
break;
}
else if (strcmp(opt->code, "d") == 0)
{
int preferred_move_target_idx;
apr_array_header_t *options;
svn_client_conflict_option_t *option;
SVN_ERR(prompt_move_target_path(&preferred_move_target_idx,
possible_moved_to_repos_relpaths,
FALSE,
pb, local_abspath, ctx, iterpool));
/* Update preferred move target path. */
SVN_ERR(svn_client_conflict_tree_get_resolution_options(&options,
conflict,
ctx,
iterpool,
iterpool));
SVN_ERR(find_conflict_option_with_repos_move_targets(
&option, options, iterpool));
if (option)
{
SVN_ERR(svn_client_conflict_option_set_moved_to_repos_relpath2(
option, preferred_move_target_idx, ctx, iterpool));
repos_move_target_chosen = TRUE;
wc_move_target_chosen = FALSE;
/* Update option description. */
SVN_ERR(build_tree_conflict_options(
&tree_conflict_options,
&possible_moved_to_repos_relpaths,
&possible_moved_to_abspaths,
NULL, conflict, ctx,
scratch_pool, scratch_pool));
/* Update conflict description. */
SVN_ERR(svn_client_conflict_tree_get_description(
&incoming_change_description, &local_change_description,
conflict, ctx, scratch_pool, scratch_pool));
conflict_description = apr_psprintf(scratch_pool, "%s\n%s",
incoming_change_description,
local_change_description);
}
continue;
}
else if (strcmp(opt->code, "w") == 0)
{
int preferred_move_target_idx;
apr_array_header_t *options;
svn_client_conflict_option_t *option;
SVN_ERR(prompt_move_target_path(&preferred_move_target_idx,
possible_moved_to_abspaths, TRUE,
pb, local_abspath, ctx, iterpool));
/* Update preferred move target path. */
SVN_ERR(svn_client_conflict_tree_get_resolution_options(&options,
conflict,
ctx,
iterpool,
iterpool));
SVN_ERR(find_conflict_option_with_working_copy_move_targets(
&option, options, iterpool));
if (option)
{
SVN_ERR(svn_client_conflict_option_set_moved_to_abspath2(
option, preferred_move_target_idx, ctx, iterpool));
wc_move_target_chosen = TRUE;
/* Update option description. */
SVN_ERR(build_tree_conflict_options(
&tree_conflict_options,
&possible_moved_to_repos_relpaths,
&possible_moved_to_abspaths,
NULL, conflict, ctx,
scratch_pool, scratch_pool));
}
continue;
}
else if (opt->choice != svn_client_conflict_option_undefined)
{
option_id = opt->choice;
break;
}
}
svn_pool_destroy(iterpool);
if (option_id != svn_client_conflict_option_unspecified &&
option_id != svn_client_conflict_option_postpone)
{
SVN_ERR(mark_conflict_resolved(conflict, option_id,
FALSE, NULL, TRUE,
path_prefix, conflict_stats,
ctx, scratch_pool));
*resolved = TRUE;
}
else
{
*resolved = FALSE;
*postponed = (option_id == svn_client_conflict_option_postpone);
}
return SVN_NO_ERROR;
}
static svn_error_t *
resolve_conflict_interactively(svn_boolean_t *resolved,
svn_boolean_t *postponed,
svn_boolean_t *quit,
svn_boolean_t *external_failed,
svn_boolean_t *printed_summary,
svn_boolean_t *printed_description,
svn_client_conflict_t *conflict,
const char *editor_cmd,
apr_hash_t *config,
const char *path_prefix,
svn_cmdline_prompt_baton_t *pb,
svn_cl__conflict_stats_t *conflict_stats,
svn_client_ctx_t *ctx,
apr_pool_t *scratch_pool)
{
svn_boolean_t text_conflicted;
apr_array_header_t *props_conflicted;
svn_boolean_t tree_conflicted;
const svn_string_t *merged_propval;
SVN_ERR(svn_client_conflict_get_conflicted(&text_conflicted,
&props_conflicted,
&tree_conflicted,
conflict,
scratch_pool,
scratch_pool));
/* Print a summary of conflicts before starting interactive resolution */
if (! *printed_summary)
{
SVN_ERR(svn_cl__print_conflict_stats(conflict_stats, scratch_pool));
*printed_summary = TRUE;
}
*resolved = FALSE;
if (text_conflicted
&& (svn_client_conflict_get_incoming_change(conflict) ==
svn_wc_conflict_action_edit)
&& (svn_client_conflict_get_local_change(conflict) ==
svn_wc_conflict_reason_edited))
SVN_ERR(handle_text_conflict(resolved, postponed, quit, printed_description,
conflict, path_prefix, pb, editor_cmd, config,
conflict_stats, ctx, scratch_pool));
if (props_conflicted->nelts > 0)
SVN_ERR(handle_prop_conflicts(resolved, postponed, quit, &merged_propval,
path_prefix, pb, editor_cmd, config, conflict,
conflict_stats, ctx, scratch_pool, scratch_pool));
if (tree_conflicted)
SVN_ERR(handle_tree_conflict(resolved, postponed, quit, printed_description,
conflict, path_prefix, pb, conflict_stats, ctx,
scratch_pool));
return SVN_NO_ERROR;
}
svn_error_t *
svn_cl__resolve_conflict(svn_boolean_t *quit,
svn_boolean_t *external_failed,
svn_boolean_t *printed_summary,
svn_client_conflict_t *conflict,
svn_cl__accept_t accept_which,
const char *editor_cmd,
const char *path_prefix,
svn_cmdline_prompt_baton_t *pb,
svn_cl__conflict_stats_t *conflict_stats,
svn_client_ctx_t *ctx,
apr_pool_t *scratch_pool)
{
svn_boolean_t text_conflicted;
apr_array_header_t *props_conflicted;
svn_boolean_t tree_conflicted;
const char *local_abspath;
svn_client_conflict_option_id_t option_id;
SVN_ERR(svn_client_conflict_get_conflicted(&text_conflicted,
&props_conflicted,
&tree_conflicted,
conflict,
scratch_pool,
scratch_pool));
local_abspath = svn_client_conflict_get_local_abspath(conflict);
if (accept_which == svn_cl__accept_unspecified)
{
option_id = svn_client_conflict_option_unspecified;
}
else if (accept_which == svn_cl__accept_postpone)
{
option_id = svn_client_conflict_option_postpone;
}
else if (accept_which == svn_cl__accept_base)
{
option_id = svn_client_conflict_option_base_text;
}
else if (accept_which == svn_cl__accept_working)
{
option_id = svn_client_conflict_option_merged_text;
if (text_conflicted)
{
const char *mime_type =
svn_client_conflict_text_get_mime_type(conflict);
/* There is no merged text for binary conflicts, behave as
* if 'mine-full' was chosen. */
if (mime_type && svn_mime_type_is_binary(mime_type))
option_id = svn_client_conflict_option_working_text;
}
else if (tree_conflicted)
{
/* For tree conflicts, map 'working' to 'accept current working
* copy state'. */
option_id = svn_client_conflict_option_accept_current_wc_state;
}
}
else if (accept_which == svn_cl__accept_theirs_conflict)
{
option_id = svn_client_conflict_option_incoming_text_where_conflicted;
}
else if (accept_which == svn_cl__accept_mine_conflict)
{
option_id = svn_client_conflict_option_working_text_where_conflicted;
if (tree_conflicted)
{
svn_wc_operation_t operation;
operation = svn_client_conflict_get_operation(conflict);
if (operation == svn_wc_operation_update ||
operation == svn_wc_operation_switch)
{
svn_wc_conflict_reason_t reason;
reason = svn_client_conflict_get_local_change(conflict);
if (reason == svn_wc_conflict_reason_moved_away)
{
/* Map 'mine-conflict' to 'update move destination'. */
option_id =
svn_client_conflict_option_update_move_destination;
}
else if (reason == svn_wc_conflict_reason_deleted ||
reason == svn_wc_conflict_reason_replaced)
{
svn_wc_conflict_action_t action;
svn_node_kind_t node_kind;
action = svn_client_conflict_get_incoming_change(conflict);
node_kind =
svn_client_conflict_tree_get_victim_node_kind(conflict);
if (action == svn_wc_conflict_action_edit &&
node_kind == svn_node_dir)
{
/* Map 'mine-conflict' to 'update any moved away
* children'. */
option_id =
svn_client_conflict_option_update_any_moved_away_children;
}
}
}
}
}
else if (accept_which == svn_cl__accept_theirs_full)
{
option_id = svn_client_conflict_option_incoming_text;
}
else if (accept_which == svn_cl__accept_mine_full)
{
option_id = svn_client_conflict_option_working_text;
}
else if (accept_which == svn_cl__accept_edit)
{
option_id = svn_client_conflict_option_unspecified;
if (local_abspath)
{
if (*external_failed)
{
option_id = svn_client_conflict_option_postpone;
}
else
{
svn_error_t *err;
err = svn_cmdline__edit_file_externally(local_abspath,
editor_cmd,
ctx->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);
*external_failed = TRUE;
}
else if (err)
return svn_error_trace(err);
option_id = svn_client_conflict_option_merged_text;
}
}
}
else if (accept_which == svn_cl__accept_launch)
{
const char *base_abspath = NULL;
const char *my_abspath = NULL;
const char *their_abspath = NULL;
option_id = svn_client_conflict_option_unspecified;
if (text_conflicted)
SVN_ERR(svn_client_conflict_text_get_contents(NULL, &my_abspath,
&base_abspath,
&their_abspath,
conflict, scratch_pool,
scratch_pool));
if (base_abspath && their_abspath && my_abspath && local_abspath)
{
if (*external_failed)
{
option_id = svn_client_conflict_option_postpone;
}
else
{
svn_boolean_t remains_in_conflict;
svn_error_t *err;
err = svn_cl__merge_file_externally(base_abspath, their_abspath,
my_abspath, local_abspath,
local_abspath, ctx->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));
*external_failed = TRUE;
return svn_error_trace(err);
}
else if (err)
return svn_error_trace(err);
if (remains_in_conflict)
option_id = svn_client_conflict_option_postpone;
else
option_id = svn_client_conflict_option_merged_text;
}
}
}
else if (accept_which == svn_cl__accept_recommended)
{
svn_client_conflict_option_id_t recommended_id;
if (tree_conflicted)
SVN_ERR(svn_client_conflict_tree_get_details(conflict, ctx,
scratch_pool));
recommended_id = svn_client_conflict_get_recommended_option_id(conflict);
if (recommended_id != svn_client_conflict_option_unspecified)
option_id = recommended_id;
else
option_id = svn_client_conflict_option_postpone;
}
else
SVN_ERR_MALFUNCTION();
/* If we are in interactive mode and either the user gave no --accept
* option or the option did not apply, then prompt. */
if (option_id == svn_client_conflict_option_unspecified)
{
svn_boolean_t resolved = FALSE;
svn_boolean_t postponed = FALSE;
svn_boolean_t printed_description = FALSE;
svn_error_t *err;
apr_pool_t *iterpool;
*quit = FALSE;
iterpool = svn_pool_create(scratch_pool);
while (!resolved && !postponed && !*quit)
{
svn_pool_clear(iterpool);
err = resolve_conflict_interactively(&resolved, &postponed, quit,
external_failed,
printed_summary,
&printed_description,
conflict,
editor_cmd, ctx->config,
path_prefix, pb,
conflict_stats, ctx,
iterpool);
if (err && err->apr_err == SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE)
{
/* Conflict resolution has failed. Let the user try again.
* It is always possible to break out of this loop with
* the 'quit' or 'postpone' options. */
svn_handle_warning2(stderr, err, "svn: ");
svn_error_clear(err);
err = SVN_NO_ERROR;
}
SVN_ERR(err);
}
svn_pool_destroy(iterpool);
}
else if (option_id != svn_client_conflict_option_postpone)
SVN_ERR(mark_conflict_resolved(conflict, option_id,
text_conflicted,
props_conflicted->nelts > 0 ? "" : NULL,
tree_conflicted,
path_prefix, conflict_stats,
ctx, scratch_pool));
return SVN_NO_ERROR;
}