/* * 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]))) struct svn_cl__interactive_conflict_baton_t { svn_cl__accept_t accept_which; apr_hash_t *config; const char *editor_cmd; svn_boolean_t external_failed; svn_cmdline_prompt_baton_t *pb; const char *path_prefix; svn_boolean_t quit; svn_cl__conflict_stats_t *conflict_stats; }; svn_error_t * svn_cl__get_conflict_func_interactive_baton( svn_cl__interactive_conflict_baton_t **b, svn_cl__accept_t accept_which, apr_hash_t *config, const char *editor_cmd, svn_cl__conflict_stats_t *conflict_stats, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *result_pool) { svn_cmdline_prompt_baton_t *pb = apr_palloc(result_pool, sizeof(*pb)); pb->cancel_func = cancel_func; pb->cancel_baton = cancel_baton; *b = apr_palloc(result_pool, sizeof(**b)); (*b)->accept_which = accept_which; (*b)->config = config; (*b)->editor_cmd = editor_cmd; (*b)->external_failed = FALSE; (*b)->pb = pb; SVN_ERR(svn_dirent_get_absolute(&(*b)->path_prefix, "", result_pool)); (*b)->quit = FALSE; (*b)->conflict_stats = conflict_stats; return SVN_NO_ERROR; } svn_cl__accept_t svn_cl__accept_from_word(const char *word) { /* Shorthand options are consistent with svn_cl__conflict_handler(). */ if (strcmp(word, SVN_CL__ACCEPT_POSTPONE) == 0 || strcmp(word, "p") == 0 || strcmp(word, ":-P") == 0) return svn_cl__accept_postpone; if (strcmp(word, SVN_CL__ACCEPT_BASE) == 0) /* ### shorthand? */ return svn_cl__accept_base; if (strcmp(word, SVN_CL__ACCEPT_WORKING) == 0) /* ### shorthand? */ return svn_cl__accept_working; if (strcmp(word, SVN_CL__ACCEPT_MINE_CONFLICT) == 0 || strcmp(word, "mc") == 0 || strcmp(word, "X-)") == 0) return svn_cl__accept_mine_conflict; if (strcmp(word, SVN_CL__ACCEPT_THEIRS_CONFLICT) == 0 || strcmp(word, "tc") == 0 || strcmp(word, "X-(") == 0) return svn_cl__accept_theirs_conflict; if (strcmp(word, SVN_CL__ACCEPT_MINE_FULL) == 0 || strcmp(word, "mf") == 0 || strcmp(word, ":-)") == 0) return svn_cl__accept_mine_full; if (strcmp(word, SVN_CL__ACCEPT_THEIRS_FULL) == 0 || strcmp(word, "tf") == 0 || strcmp(word, ":-(") == 0) return svn_cl__accept_theirs_full; if (strcmp(word, SVN_CL__ACCEPT_EDIT) == 0 || strcmp(word, "e") == 0 || strcmp(word, ":-E") == 0) return svn_cl__accept_edit; if (strcmp(word, SVN_CL__ACCEPT_LAUNCH) == 0 || strcmp(word, "l") == 0 || strcmp(word, ":-l") == 0) return svn_cl__accept_launch; /* word is an invalid action. */ return svn_cl__accept_invalid; } /* Print on stdout a diff that shows incoming conflicting changes * corresponding to the conflict described in DESC. */ static svn_error_t * show_diff(const svn_wc_conflict_description2_t *desc, const char *path_prefix, apr_pool_t *pool) { const char *path1, *path2; const char *label1, *label2; svn_diff_t *diff; svn_stream_t *output; svn_diff_file_options_t *options; if (desc->merged_file) { /* For conflicts recorded by the 'merge' operation, show a diff between * 'mine' (the working version of the file as it appeared before the * 'merge' operation was run) and 'merged' (the version of the file * as it appears after the merge operation). * * For conflicts recorded by the 'update' and 'switch' operations, * show a diff beween 'theirs' (the new pristine version of the * file) and 'merged' (the version of the file as it appears with * local changes merged with the new pristine version). * * This way, the diff is always minimal and clearly identifies changes * brought into the working copy by the update/switch/merge operation. */ if (desc->operation == svn_wc_operation_merge) { path1 = desc->my_abspath; label1 = _("MINE"); } else { path1 = desc->their_abspath; label1 = _("THEIRS"); } path2 = desc->merged_file; label2 = _("MERGED"); } else { /* There's no merged file, but we can show the difference between mine and theirs. */ path1 = desc->their_abspath; label1 = _("THEIRS"); path2 = desc->my_abspath; label2 = _("MINE"); } label1 = apr_psprintf(pool, "%s\t- %s", svn_cl__local_style_skip_ancestor( path_prefix, path1, pool), label1); label2 = apr_psprintf(pool, "%s\t- %s", svn_cl__local_style_skip_ancestor( path_prefix, path2, pool), label2); options = svn_diff_file_options_create(pool); options->ignore_eol_style = TRUE; SVN_ERR(svn_stream_for_stdout(&output, pool)); SVN_ERR(svn_diff_file_diff_2(&diff, path1, path2, options, pool)); return svn_diff_file_output_unified3(output, diff, path1, path2, label1, label2, APR_LOCALE_CHARSET, NULL, FALSE, pool); } /* Print on stdout just the conflict hunks of a diff among the 'base', 'their' * and 'my' files of DESC. */ static svn_error_t * show_conflicts(const svn_wc_conflict_description2_t *desc, apr_pool_t *pool) { svn_diff_t *diff; svn_stream_t *output; svn_diff_file_options_t *options; options = svn_diff_file_options_create(pool); options->ignore_eol_style = TRUE; SVN_ERR(svn_stream_for_stdout(&output, pool)); SVN_ERR(svn_diff_file_diff3_2(&diff, desc->base_abspath, desc->my_abspath, desc->their_abspath, options, pool)); /* ### Consider putting the markers/labels from ### svn_wc__merge_internal in the conflict description. */ return svn_diff_file_output_merge2(output, diff, desc->base_abspath, desc->my_abspath, desc->their_abspath, _("||||||| ORIGINAL"), _("<<<<<<< MINE (select with 'mc')"), _(">>>>>>> THEIRS (select with 'tc')"), "=======", svn_diff_conflict_display_only_conflicts, pool); } /* Perform a 3-way merge of the conflicting values of a property, * and write the result to the OUTPUT stream. * * If MERGED_ABSPATH is non-NULL, use it as 'my' version instead of * DESC->MY_ABSPATH. * * Assume the values are printable UTF-8 text. */ static svn_error_t * merge_prop_conflict(svn_stream_t *output, const svn_wc_conflict_description2_t *desc, const char *merged_abspath, apr_pool_t *pool) { const char *base_abspath = desc->base_abspath; const char *my_abspath = desc->my_abspath; const char *their_abspath = desc->their_abspath; svn_diff_file_options_t *options = svn_diff_file_options_create(pool); svn_diff_t *diff; /* If any of the property values is missing, use an empty file instead * for the purpose of showing a diff. */ if (! base_abspath || ! my_abspath || ! their_abspath) { const char *empty_file; SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file, NULL, svn_io_file_del_on_pool_cleanup, pool, pool)); if (! base_abspath) base_abspath = empty_file; if (! my_abspath) my_abspath = empty_file; if (! their_abspath) their_abspath = empty_file; } options->ignore_eol_style = TRUE; SVN_ERR(svn_diff_file_diff3_2(&diff, base_abspath, merged_abspath ? merged_abspath : my_abspath, their_abspath, options, pool)); SVN_ERR(svn_diff_file_output_merge2(output, diff, base_abspath, merged_abspath ? merged_abspath : my_abspath, their_abspath, _("||||||| ORIGINAL"), _("<<<<<<< MINE"), _(">>>>>>> THEIRS"), "=======", svn_diff_conflict_display_modified_original_latest, pool)); return SVN_NO_ERROR; } /* Display the conflicting values of a property as a 3-way diff. * * If MERGED_ABSPATH is non-NULL, show it as 'my' version instead of * DESC->MY_ABSPATH. * * Assume the values are printable UTF-8 text. */ static svn_error_t * show_prop_conflict(const svn_wc_conflict_description2_t *desc, const char *merged_abspath, apr_pool_t *pool) { svn_stream_t *output; SVN_ERR(svn_stream_for_stdout(&output, pool)); SVN_ERR(merge_prop_conflict(output, desc, merged_abspath, pool)); return SVN_NO_ERROR; } /* Run an external editor, passing it the MERGED_FILE, or, if the * 'merged' file is null, return an error. The tool to use is determined by * B->editor_cmd, B->config and environment variables; see * svn_cl__edit_file_externally() for details. * * If the tool runs, set *PERFORMED_EDIT to true; if a tool is not * configured or cannot run, do not touch *PERFORMED_EDIT, report the error * on stderr, and return SVN_NO_ERROR; if any other error is encountered, * return that error. */ static svn_error_t * open_editor(svn_boolean_t *performed_edit, const char *merged_file, svn_cl__interactive_conflict_baton_t *b, apr_pool_t *pool) { svn_error_t *err; if (merged_file) { err = svn_cmdline__edit_file_externally(merged_file, b->editor_cmd, b->config, pool); if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR)) { svn_error_t *root_err = svn_error_root_cause(err); SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n", root_err->message ? root_err->message : _("No editor found."))); svn_error_clear(err); } else if (err && (err->apr_err == SVN_ERR_EXTERNAL_PROGRAM)) { svn_error_t *root_err = svn_error_root_cause(err); SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n", root_err->message ? root_err->message : _("Error running editor."))); svn_error_clear(err); } else if (err) return svn_error_trace(err); else *performed_edit = TRUE; } else SVN_ERR(svn_cmdline_fprintf(stderr, pool, _("Invalid option; there's no " "merged version to edit.\n\n"))); return SVN_NO_ERROR; } /* Run an external editor, passing it the 'merged' property in DESC. * The tool to use is determined by B->editor_cmd, B->config and * environment variables; see svn_cl__edit_file_externally() for details. */ static svn_error_t * edit_prop_conflict(const char **merged_file_path, const svn_wc_conflict_description2_t *desc, svn_cl__interactive_conflict_baton_t *b, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { apr_file_t *file; const char *file_path; svn_boolean_t performed_edit = FALSE; svn_stream_t *merged_prop; SVN_ERR(svn_io_open_unique_file3(&file, &file_path, NULL, svn_io_file_del_on_pool_cleanup, result_pool, scratch_pool)); merged_prop = svn_stream_from_aprfile2(file, TRUE /* disown */, scratch_pool); SVN_ERR(merge_prop_conflict(merged_prop, desc, NULL, scratch_pool)); SVN_ERR(svn_stream_close(merged_prop)); SVN_ERR(svn_io_file_flush_to_disk(file, scratch_pool)); SVN_ERR(open_editor(&performed_edit, file_path, b, scratch_pool)); *merged_file_path = (performed_edit ? file_path : NULL); return SVN_NO_ERROR; } /* Run an external merge tool, passing it the 'base', 'their', 'my' and * 'merged' files in DESC. The tool to use is determined by B->config and * environment variables; see svn_cl__merge_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 * launch_resolver(svn_boolean_t *performed_edit, const svn_wc_conflict_description2_t *desc, svn_cl__interactive_conflict_baton_t *b, apr_pool_t *pool) { svn_error_t *err; err = svn_cl__merge_file_externally(desc->base_abspath, desc->their_abspath, desc->my_abspath, desc->merged_file, desc->local_abspath, b->config, NULL, pool); if (err && err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL) { SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n", err->message ? err->message : _("No merge tool found, " "try '(m) merge' instead.\n"))); svn_error_clear(err); } else if (err && err->apr_err == SVN_ERR_EXTERNAL_PROGRAM) { SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n", err->message ? err->message : _("Error running merge tool, " "try '(m) merge' instead."))); svn_error_clear(err); } else if (err) return svn_error_trace(err); else if (performed_edit) *performed_edit = TRUE; return SVN_NO_ERROR; } /* Maximum line length for the prompt string. */ #define MAX_PROMPT_WIDTH 70 /* Description of a resolver option */ typedef struct resolver_option_t { const char *code; /* one or two characters */ const char *short_desc; /* label in prompt (localized) */ const char *long_desc; /* longer description (localized) */ svn_wc_conflict_choice_t choice; /* or -1 if not a simple choice */ } resolver_option_t; /* Resolver options for a text conflict */ /* (opt->code == "" causes a blank line break in help_string()) */ static const resolver_option_t text_conflict_options[] = { /* Translators: keep long_desc below 70 characters (wrap with a left margin of 9 spaces if needed); don't translate the words within square brackets. */ { "e", N_("edit file"), N_("change merged file in an editor" " [edit]"), -1 }, { "df", N_("show diff"), N_("show all changes made to merged file"), -1 }, { "r", N_("mark resolved"), N_("accept merged version of file"), svn_wc_conflict_choose_merged }, { "", "", "", svn_wc_conflict_choose_unspecified }, { "dc", N_("display conflict"), N_("show all conflicts " "(ignoring merged version)"), -1 }, { "mc", N_("my side of conflict"), N_("accept my version for all conflicts " "(same) [mine-conflict]"), svn_wc_conflict_choose_mine_conflict }, { "tc", N_("their side of conflict"), N_("accept their version for all " "conflicts (same)" " [theirs-conflict]"), svn_wc_conflict_choose_theirs_conflict }, { "", "", "", svn_wc_conflict_choose_unspecified }, { "mf", N_("my version"), N_("accept my version of entire file (even " "non-conflicts) [mine-full]"), svn_wc_conflict_choose_mine_full }, { "tf", N_("their version"), N_("accept their version of entire file " "(same) [theirs-full]"), svn_wc_conflict_choose_theirs_full }, { "", "", "", svn_wc_conflict_choose_unspecified }, { "m", N_("merge"), N_("use internal merge tool to resolve " "conflict"), -1 }, { "l", N_("launch tool"), N_("launch external tool to resolve " "conflict [launch]"), -1 }, { "p", N_("postpone"), N_("mark the conflict to be resolved later" " [postpone]"), svn_wc_conflict_choose_postpone }, { "q", N_("quit resolution"), N_("postpone all remaining conflicts"), svn_wc_conflict_choose_postpone }, { "s", N_("show all options"), N_("show this list (also 'h', '?')"), -1 }, { NULL } }; /* Resolver options for a property conflict */ static const resolver_option_t prop_conflict_options[] = { { "mf", N_("my version"), N_("accept my version of entire file (even " "non-conflicts) [mine-full]"), svn_wc_conflict_choose_mine_full }, { "tf", N_("their version"), N_("accept their version of entire file " "(same) [theirs-full]"), svn_wc_conflict_choose_theirs_full }, { "dc", N_("display conflict"), N_("show conflicts in this property"), -1 }, { "e", N_("edit property"), N_("change merged property value in an editor" " [edit]"), -1 }, { "r", N_("mark resolved"), N_("accept edited version of property"), svn_wc_conflict_choose_merged }, { "p", N_("postpone"), N_("mark the conflict to be resolved later" " [postpone]"), svn_wc_conflict_choose_postpone }, { "q", N_("quit resolution"), N_("postpone all remaining conflicts"), svn_wc_conflict_choose_postpone }, { "h", N_("help"), N_("show this help (also '?')"), -1 }, { NULL } }; /* Resolver options for an obstructued addition */ static const resolver_option_t obstructed_add_options[] = { { "mf", N_("my version"), N_("accept pre-existing item (ignore " "upstream addition) [mine-full]"), svn_wc_conflict_choose_mine_full }, { "tf", N_("their version"), N_("accept incoming item (overwrite " "pre-existing item) [theirs-full]"), svn_wc_conflict_choose_theirs_full }, { "p", N_("postpone"), N_("mark the conflict to be resolved later" " [postpone]"), svn_wc_conflict_choose_postpone }, { "q", N_("quit resolution"), N_("postpone all remaining conflicts"), svn_wc_conflict_choose_postpone }, { "h", N_("help"), N_("show this help (also '?')"), -1 }, { NULL } }; /* Resolver options for a tree conflict */ static const resolver_option_t tree_conflict_options[] = { { "r", N_("mark resolved"), N_("accept current working copy state"), svn_wc_conflict_choose_merged }, { "p", N_("postpone"), N_("resolve the conflict later [postpone]"), svn_wc_conflict_choose_postpone }, { "q", N_("quit resolution"), N_("postpone all remaining conflicts"), svn_wc_conflict_choose_postpone }, { "h", N_("help"), N_("show this help (also '?')"), -1 }, { NULL } }; static const resolver_option_t tree_conflict_options_update_moved_away[] = { { "mc", N_("apply update (recommended)"), N_("apply update to the move destination" " [mine-conflict]"), svn_wc_conflict_choose_mine_conflict }, { "r", N_("discard update (breaks move)"), N_("discard update, mark " "resolved, the move will " "will become a copy"), svn_wc_conflict_choose_merged }, { "p", N_("postpone"), N_("resolve the conflict later [postpone]"), svn_wc_conflict_choose_postpone }, { "q", N_("quit resolution"), N_("postpone all remaining conflicts"), svn_wc_conflict_choose_postpone }, { "h", N_("help"), N_("show this help (also '?')"), -1 }, { NULL } }; static const resolver_option_t tree_conflict_options_update_edit_moved_away[] = { { "mc", N_("apply update to move destination"), N_("apply incoming update to move destination" " [mine-conflict]"), svn_wc_conflict_choose_mine_conflict }, { "p", N_("postpone"), N_("resolve the conflict later [postpone]"), svn_wc_conflict_choose_postpone }, { "q", N_("quit resolution"), N_("postpone all remaining conflicts"), svn_wc_conflict_choose_postpone }, { "h", N_("help"), N_("show this help (also '?')"), -1 }, { NULL } }; static const resolver_option_t tree_conflict_options_update_deleted[] = { { "mc", N_("keep affected local moves"), N_("keep any local moves affected " "by this deletion [mine-conflict]"), svn_wc_conflict_choose_mine_conflict }, { "r", N_("mark resolved (breaks moves)"), N_("mark resolved, any affected " "moves will become copies"), svn_wc_conflict_choose_merged }, { "p", N_("postpone"), N_("resolve the conflict later [postpone]"), svn_wc_conflict_choose_postpone }, { "q", N_("quit resolution"), N_("postpone all remaining conflicts"), svn_wc_conflict_choose_postpone }, { "h", N_("help"), N_("show this help (also '?')"), -1 }, { NULL } }; static const resolver_option_t tree_conflict_options_update_replaced[] = { { "mc", N_("keep affected local moves"), N_("keep any moves affected by this " "replacement [mine-conflict]"), svn_wc_conflict_choose_mine_conflict }, { "r", N_("mark resolved (breaks moves)"), N_("mark resolved (any affected " "moves will become copies)"), svn_wc_conflict_choose_merged }, { "p", N_("postpone"), N_("resolve the conflict later [postpone]"), svn_wc_conflict_choose_postpone }, { "q", N_("quit resolution"), N_("postpone all remaining conflicts"), svn_wc_conflict_choose_postpone }, { "h", N_("help"), N_("show this help (also '?')"), -1 }, { NULL } }; /* Return a pointer to the option description in OPTIONS matching the * one- or two-character OPTION_CODE. Return NULL if not found. */ static const resolver_option_t * find_option(const resolver_option_t *options, const char *option_code) { const resolver_option_t *opt; for (opt = options; opt->code; opt++) { /* Ignore code "" (blank lines) which is not a valid answer. */ if (opt->code[0] && strcmp(opt->code, option_code) == 0) return opt; } return NULL; } /* Return a prompt string listing the options OPTIONS. If OPTION_CODES is * non-null, select only the options whose codes are mentioned in it. */ static const char * prompt_string(const resolver_option_t *options, const char *const *option_codes, apr_pool_t *pool) { const char *result = _("Select:"); int left_margin = svn_utf_cstring_utf8_width(result); const char *line_sep = apr_psprintf(pool, "\n%*s", left_margin, ""); int this_line_len = left_margin; svn_boolean_t first = TRUE; while (1) { const resolver_option_t *opt; const char *s; int slen; if (option_codes) { if (! *option_codes) break; opt = find_option(options, *option_codes++); } else { opt = options++; if (! opt->code) break; } if (! first) result = apr_pstrcat(pool, result, ",", (char *)NULL); s = apr_psprintf(pool, _(" (%s) %s"), opt->code, _(opt->short_desc)); slen = svn_utf_cstring_utf8_width(s); /* Break the line if adding the next option would make it too long */ if (this_line_len + slen > MAX_PROMPT_WIDTH) { result = apr_pstrcat(pool, result, line_sep, (char *)NULL); this_line_len = left_margin; } result = apr_pstrcat(pool, result, s, (char *)NULL); this_line_len += slen; first = FALSE; } return apr_pstrcat(pool, result, ": ", (char *)NULL); } /* Return a help string listing the OPTIONS. */ static const char * help_string(const resolver_option_t *options, apr_pool_t *pool) { const char *result = ""; const resolver_option_t *opt; for (opt = options; opt->code; opt++) { /* Append a line describing OPT, or a blank line if its code is "". */ if (opt->code[0]) { const char *s = apr_psprintf(pool, " (%s)", opt->code); result = apr_psprintf(pool, "%s%-6s - %s\n", result, s, _(opt->long_desc)); } else { result = apr_pstrcat(pool, result, "\n", (char *)NULL); } } result = apr_pstrcat(pool, result, _("Words in square brackets are the corresponding " "--accept option arguments.\n"), (char *)NULL); return result; } /* Prompt the user with CONFLICT_OPTIONS, restricted to the options listed * in OPTIONS_TO_SHOW if that is non-null. Set *OPT to point to the chosen * one of CONFLICT_OPTIONS (not necessarily one of OPTIONS_TO_SHOW), or to * NULL if the answer was not one of them. * * If the answer is the (globally recognized) 'help' option, then display * the help (on stderr) and return with *OPT == NULL. */ static svn_error_t * prompt_user(const resolver_option_t **opt, const resolver_option_t *conflict_options, const char *const *options_to_show, void *prompt_baton, apr_pool_t *scratch_pool) { const char *prompt = prompt_string(conflict_options, options_to_show, scratch_pool); const char *answer; SVN_ERR(svn_cmdline_prompt_user2(&answer, prompt, prompt_baton, scratch_pool)); if (strcmp(answer, "h") == 0 || strcmp(answer, "?") == 0) { SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n", help_string(conflict_options, scratch_pool))); *opt = NULL; } else { *opt = find_option(conflict_options, answer); if (! *opt) { SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, _("Unrecognized option.\n\n"))); } } return SVN_NO_ERROR; } /* Ask the user what to do about the text conflict described by DESC. * Return the answer in RESULT. B is the conflict baton for this * conflict resolution session. * SCRATCH_POOL is used for temporary allocations. */ static svn_error_t * handle_text_conflict(svn_wc_conflict_result_t *result, const svn_wc_conflict_description2_t *desc, svn_cl__interactive_conflict_baton_t *b, apr_pool_t *scratch_pool) { apr_pool_t *iterpool = svn_pool_create(scratch_pool); svn_boolean_t diff_allowed = FALSE; /* Have they done something that might have affected the merged file (so that we need to save a .edited copy)? */ 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; SVN_ERR_ASSERT(desc->kind == svn_wc_conflict_kind_text); SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, _("Conflict discovered in file '%s'.\n"), svn_cl__local_style_skip_ancestor( b->path_prefix, desc->local_abspath, scratch_pool))); /* Diffing can happen between base and merged, to show conflict markers to the user (this is the typical 3-way merge scenario), or if no base is available, we can show a diff between mine and theirs. */ if ((desc->merged_file && desc->base_abspath) || (!desc->base_abspath && desc->my_abspath && desc->their_abspath)) diff_allowed = TRUE; while (TRUE) { const char *options[ARRAY_LEN(text_conflict_options)]; const char **next_option = options; const resolver_option_t *opt; svn_pool_clear(iterpool); *next_option++ = "p"; if (diff_allowed) { *next_option++ = "df"; *next_option++ = "e"; *next_option++ = "m"; if (knows_something) *next_option++ = "r"; if (! desc->is_binary) { *next_option++ = "mc"; *next_option++ = "tc"; } } else { if (knows_something) *next_option++ = "r"; *next_option++ = "mf"; *next_option++ = "tf"; } *next_option++ = "s"; *next_option++ = NULL; SVN_ERR(prompt_user(&opt, text_conflict_options, options, b->pb, iterpool)); if (! opt) continue; if (strcmp(opt->code, "q") == 0) { result->choice = opt->choice; b->accept_which = svn_cl__accept_postpone; b->quit = TRUE; break; } else if (strcmp(opt->code, "s") == 0) { SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "\n%s\n", help_string(text_conflict_options, iterpool))); } else if (strcmp(opt->code, "dc") == 0) { if (desc->is_binary) { SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, _("Invalid option; cannot " "display conflicts for a " "binary file.\n\n"))); continue; } else if (! (desc->my_abspath && desc->base_abspath && desc->their_abspath)) { SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, _("Invalid option; original " "files not available.\n\n"))); continue; } SVN_ERR(show_conflicts(desc, iterpool)); knows_something = TRUE; } else if (strcmp(opt->code, "df") == 0) { if (! diff_allowed) { SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, _("Invalid option; there's no " "merged version to diff.\n\n"))); continue; } SVN_ERR(show_diff(desc, b->path_prefix, iterpool)); knows_something = TRUE; } else if (strcmp(opt->code, "e") == 0 || strcmp(opt->code, ":-E") == 0) { SVN_ERR(open_editor(&performed_edit, desc->merged_file, b, iterpool)); if (performed_edit) knows_something = TRUE; } else if (strcmp(opt->code, "m") == 0 || strcmp(opt->code, ":-g") == 0 || strcmp(opt->code, "=>-") == 0 || strcmp(opt->code, ":>.") == 0) { if (desc->kind != svn_wc_conflict_kind_text) { SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, _("Invalid option; can only " "resolve text conflicts with " "the internal merge tool." "\n\n"))); continue; } if (desc->base_abspath && desc->their_abspath && desc->my_abspath && desc->merged_file) { svn_boolean_t remains_in_conflict; SVN_ERR(svn_cl__merge_file(desc->base_abspath, desc->their_abspath, desc->my_abspath, desc->merged_file, desc->local_abspath, b->path_prefix, b->editor_cmd, b->config, &remains_in_conflict, iterpool)); knows_something = !remains_in_conflict; } else SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, _("Invalid option.\n\n"))); } else if (strcmp(opt->code, "l") == 0 || strcmp(opt->code, ":-l") == 0) { /* ### This check should be earlier as it's nasty to offer an option * and then when the user chooses it say 'Invalid option'. */ /* ### 'merged_file' shouldn't be necessary *before* we launch the * resolver: it should be the *result* of doing so. */ if (desc->base_abspath && desc->their_abspath && desc->my_abspath && desc->merged_file) { SVN_ERR(launch_resolver(&performed_edit, desc, b, iterpool)); if (performed_edit) knows_something = TRUE; } else SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, _("Invalid option.\n\n"))); } else if (opt->choice != -1) { if ((opt->choice == svn_wc_conflict_choose_mine_conflict || opt->choice == svn_wc_conflict_choose_theirs_conflict) && desc->is_binary) { SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, _("Invalid option; cannot choose " "based on conflicts in a " "binary file.\n\n"))); continue; } /* We only allow the user accept the merged version of the file if they've edited it, or at least looked at the diff. */ if (result->choice == svn_wc_conflict_choose_merged && ! knows_something) { SVN_ERR(svn_cmdline_fprintf( stderr, iterpool, _("Invalid option; use diff/edit/merge/launch " "before choosing 'mark resolved'.\n\n"))); continue; } result->choice = opt->choice; if (performed_edit) result->save_merged = TRUE; break; } } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } /* Ask the user what to do about the property conflict described by DESC. * Return the answer in RESULT. B is the conflict baton for this * conflict resolution session. * SCRATCH_POOL is used for temporary allocations. */ static svn_error_t * handle_prop_conflict(svn_wc_conflict_result_t *result, const svn_wc_conflict_description2_t *desc, svn_cl__interactive_conflict_baton_t *b, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { apr_pool_t *iterpool; const char *message; const char *merged_file_path = NULL; svn_boolean_t resolved_allowed = FALSE; /* ### Work around a historical bug in the provider: the path to the * conflict description file was put in the 'theirs' field, and * 'theirs' was put in the 'merged' field. */ ((svn_wc_conflict_description2_t *)desc)->their_abspath = desc->merged_file; ((svn_wc_conflict_description2_t *)desc)->merged_file = NULL; SVN_ERR_ASSERT(desc->kind == svn_wc_conflict_kind_property); SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, _("Conflict for property '%s' discovered" " on '%s'.\n"), desc->property_name, svn_cl__local_style_skip_ancestor( b->path_prefix, desc->local_abspath, scratch_pool))); SVN_ERR(svn_cl__get_human_readable_prop_conflict_description(&message, desc, scratch_pool)); SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n", message)); iterpool = svn_pool_create(scratch_pool); while (TRUE) { const resolver_option_t *opt; const char *options[ARRAY_LEN(prop_conflict_options)]; const char **next_option = options; *next_option++ = "p"; *next_option++ = "mf"; *next_option++ = "tf"; *next_option++ = "dc"; *next_option++ = "e"; if (resolved_allowed) *next_option++ = "r"; *next_option++ = "q"; *next_option++ = "h"; *next_option++ = NULL; svn_pool_clear(iterpool); SVN_ERR(prompt_user(&opt, prop_conflict_options, options, b->pb, iterpool)); if (! opt) continue; if (strcmp(opt->code, "q") == 0) { result->choice = opt->choice; b->accept_which = svn_cl__accept_postpone; b->quit = TRUE; break; } else if (strcmp(opt->code, "dc") == 0) { SVN_ERR(show_prop_conflict(desc, merged_file_path, scratch_pool)); } else if (strcmp(opt->code, "e") == 0) { SVN_ERR(edit_prop_conflict(&merged_file_path, desc, b, result_pool, scratch_pool)); resolved_allowed = (merged_file_path != NULL); } else if (strcmp(opt->code, "r") == 0) { if (! resolved_allowed) { SVN_ERR(svn_cmdline_fprintf(stderr, iterpool, _("Invalid option; please edit the property " "first.\n\n"))); continue; } result->merged_file = merged_file_path; result->choice = svn_wc_conflict_choose_merged; break; } else if (opt->choice != -1) { result->choice = opt->choice; break; } } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } /* Ask the user what to do about the tree conflict described by DESC. * Return the answer in RESULT. B is the conflict baton for this * conflict resolution session. * SCRATCH_POOL is used for temporary allocations. */ static svn_error_t * handle_tree_conflict(svn_wc_conflict_result_t *result, const svn_wc_conflict_description2_t *desc, svn_cl__interactive_conflict_baton_t *b, apr_pool_t *scratch_pool) { const char *readable_desc; apr_pool_t *iterpool; SVN_ERR(svn_cl__get_human_readable_tree_conflict_description( &readable_desc, desc, scratch_pool)); SVN_ERR(svn_cmdline_fprintf( stderr, scratch_pool, _("Tree conflict on '%s'\n > %s\n"), svn_cl__local_style_skip_ancestor(b->path_prefix, desc->local_abspath, scratch_pool), readable_desc)); iterpool = svn_pool_create(scratch_pool); while (1) { const resolver_option_t *opt; const resolver_option_t *tc_opts; svn_pool_clear(iterpool); if (desc->operation == svn_wc_operation_update || desc->operation == svn_wc_operation_switch) { if (desc->reason == svn_wc_conflict_reason_moved_away) { if (desc->action == svn_wc_conflict_action_edit) tc_opts = tree_conflict_options_update_edit_moved_away; else tc_opts = tree_conflict_options_update_moved_away; } else if (desc->reason == svn_wc_conflict_reason_deleted) tc_opts = tree_conflict_options_update_deleted; else if (desc->reason == svn_wc_conflict_reason_replaced) tc_opts = tree_conflict_options_update_replaced; else tc_opts = tree_conflict_options; } else tc_opts = tree_conflict_options; SVN_ERR(prompt_user(&opt, tc_opts, NULL, b->pb, iterpool)); if (! opt) continue; if (strcmp(opt->code, "q") == 0) { result->choice = opt->choice; b->accept_which = svn_cl__accept_postpone; b->quit = TRUE; break; } else if (opt->choice != -1) { result->choice = opt->choice; break; } } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } /* Ask the user what to do about the obstructed add described by DESC. * Return the answer in RESULT. B is the conflict baton for this * conflict resolution session. * SCRATCH_POOL is used for temporary allocations. */ static svn_error_t * handle_obstructed_add(svn_wc_conflict_result_t *result, const svn_wc_conflict_description2_t *desc, svn_cl__interactive_conflict_baton_t *b, apr_pool_t *scratch_pool) { apr_pool_t *iterpool; SVN_ERR(svn_cmdline_fprintf( stderr, scratch_pool, _("Conflict discovered when trying to add '%s'.\n" "An object of the same name already exists.\n"), svn_cl__local_style_skip_ancestor(b->path_prefix, desc->local_abspath, scratch_pool))); iterpool = svn_pool_create(scratch_pool); while (1) { const resolver_option_t *opt; svn_pool_clear(iterpool); SVN_ERR(prompt_user(&opt, obstructed_add_options, NULL, b->pb, iterpool)); if (! opt) continue; if (strcmp(opt->code, "q") == 0) { result->choice = opt->choice; b->accept_which = svn_cl__accept_postpone; b->quit = TRUE; break; } else if (opt->choice != -1) { result->choice = opt->choice; break; } } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } /* The body of svn_cl__conflict_func_interactive(). */ static svn_error_t * conflict_func_interactive(svn_wc_conflict_result_t **result, const svn_wc_conflict_description2_t *desc, void *baton, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { svn_cl__interactive_conflict_baton_t *b = baton; svn_error_t *err; /* Start out assuming we're going to postpone the conflict. */ *result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone, NULL, result_pool); switch (b->accept_which) { case svn_cl__accept_invalid: case svn_cl__accept_unspecified: /* No (or no valid) --accept option, fall through to prompting. */ break; case svn_cl__accept_postpone: (*result)->choice = svn_wc_conflict_choose_postpone; return SVN_NO_ERROR; case svn_cl__accept_base: (*result)->choice = svn_wc_conflict_choose_base; return SVN_NO_ERROR; case svn_cl__accept_working: /* If the caller didn't merge the property values, then I guess * 'choose working' means 'choose mine'... */ if (! desc->merged_file) (*result)->merged_file = desc->my_abspath; (*result)->choice = svn_wc_conflict_choose_merged; return SVN_NO_ERROR; case svn_cl__accept_mine_conflict: (*result)->choice = svn_wc_conflict_choose_mine_conflict; return SVN_NO_ERROR; case svn_cl__accept_theirs_conflict: (*result)->choice = svn_wc_conflict_choose_theirs_conflict; return SVN_NO_ERROR; case svn_cl__accept_mine_full: (*result)->choice = svn_wc_conflict_choose_mine_full; return SVN_NO_ERROR; case svn_cl__accept_theirs_full: (*result)->choice = svn_wc_conflict_choose_theirs_full; return SVN_NO_ERROR; case svn_cl__accept_edit: if (desc->merged_file) { if (b->external_failed) { (*result)->choice = svn_wc_conflict_choose_postpone; return SVN_NO_ERROR; } err = svn_cmdline__edit_file_externally(desc->merged_file, b->editor_cmd, b->config, scratch_pool); if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR)) { SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n", err->message ? err->message : _("No editor found;" " leaving all conflicts."))); svn_error_clear(err); b->external_failed = TRUE; } else if (err && (err->apr_err == SVN_ERR_EXTERNAL_PROGRAM)) { SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n", err->message ? err->message : _("Error running editor;" " leaving all conflicts."))); svn_error_clear(err); b->external_failed = TRUE; } else if (err) return svn_error_trace(err); (*result)->choice = svn_wc_conflict_choose_merged; return SVN_NO_ERROR; } /* else, fall through to prompting. */ break; case svn_cl__accept_launch: if (desc->base_abspath && desc->their_abspath && desc->my_abspath && desc->merged_file) { svn_boolean_t remains_in_conflict; if (b->external_failed) { (*result)->choice = svn_wc_conflict_choose_postpone; return SVN_NO_ERROR; } err = svn_cl__merge_file_externally(desc->base_abspath, desc->their_abspath, desc->my_abspath, desc->merged_file, desc->local_abspath, b->config, &remains_in_conflict, scratch_pool); if (err && err->apr_err == SVN_ERR_CL_NO_EXTERNAL_MERGE_TOOL) { SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n", err->message ? err->message : _("No merge tool found;" " leaving all conflicts."))); b->external_failed = TRUE; return svn_error_trace(err); } else if (err && err->apr_err == SVN_ERR_EXTERNAL_PROGRAM) { SVN_ERR(svn_cmdline_fprintf(stderr, scratch_pool, "%s\n", err->message ? err->message : _("Error running merge tool;" " leaving all conflicts."))); b->external_failed = TRUE; return svn_error_trace(err); } else if (err) return svn_error_trace(err); if (remains_in_conflict) (*result)->choice = svn_wc_conflict_choose_postpone; else (*result)->choice = svn_wc_conflict_choose_merged; return SVN_NO_ERROR; } /* else, fall through to prompting. */ break; } /* We're in interactive mode and either the user gave no --accept option or the option did not apply; let's prompt. */ /* Handle the most common cases, which is either: Conflicting edits on a file's text, or Conflicting edits on a property. */ if (((desc->kind == svn_wc_conflict_kind_text) && (desc->action == svn_wc_conflict_action_edit) && (desc->reason == svn_wc_conflict_reason_edited))) SVN_ERR(handle_text_conflict(*result, desc, b, scratch_pool)); else if (desc->kind == svn_wc_conflict_kind_property) SVN_ERR(handle_prop_conflict(*result, desc, b, result_pool, scratch_pool)); /* Dealing with obstruction of additions can be tricky. The obstructing item could be unversioned, versioned, or even schedule-add. Here's a matrix of how the caller should behave, based on results we return. Unversioned Versioned Schedule-Add choose_mine skip addition, skip addition skip addition add existing item choose_theirs destroy file, schedule-delete, revert add, add new item. add new item. rm file, add new item postpone [ bail out ] */ else if ((desc->action == svn_wc_conflict_action_add) && (desc->reason == svn_wc_conflict_reason_obstructed)) SVN_ERR(handle_obstructed_add(*result, desc, b, scratch_pool)); else if (desc->kind == svn_wc_conflict_kind_tree) SVN_ERR(handle_tree_conflict(*result, desc, b, scratch_pool)); else /* other types of conflicts -- do nothing about them. */ { (*result)->choice = svn_wc_conflict_choose_postpone; } return SVN_NO_ERROR; } svn_error_t * svn_cl__conflict_func_interactive(svn_wc_conflict_result_t **result, const svn_wc_conflict_description2_t *desc, void *baton, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { svn_cl__interactive_conflict_baton_t *b = baton; SVN_ERR(conflict_func_interactive(result, desc, baton, result_pool, scratch_pool)); /* If we are resolving a conflict, adjust the summary of conflicts. */ if ((*result)->choice != svn_wc_conflict_choose_postpone) { const char *local_path = svn_cl__local_style_skip_ancestor( b->path_prefix, desc->local_abspath, scratch_pool); svn_cl__conflict_stats_resolved(b->conflict_stats, local_path, desc->kind); } return SVN_NO_ERROR; }