/* * 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 /* for APR_LOCALE_CHARSET */ #define APR_WANT_STRFUNC #include #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) { /* Unkown 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; }