/* * conflict-callbacks.c: conflict resolution callbacks specific to the * commandline client. * * ==================================================================== * Copyright (c) 2000-2008 CollabNet. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://subversion.tigris.org/license-1.html. * If newer versions of this license are posted there, you may use a * newer version instead, at your option. * * This software consists of voluntary contributions made by many * individuals. For exact contribution history, see the revision * history and logs, available at http://subversion.tigris.org/. * ==================================================================== */ /* ==================================================================== */ /*** Includes. ***/ #define APR_WANT_STDIO #define APR_WANT_STRFUNC #include #include "svn_cmdline.h" #include "svn_client.h" #include "svn_types.h" #include "svn_pools.h" #include "cl.h" #include "svn_private_config.h" svn_cl__conflict_baton_t * svn_cl__conflict_baton_make(svn_cl__accept_t accept_which, apr_hash_t *config, const char *editor_cmd, svn_cmdline_prompt_baton_t *pb, apr_pool_t *pool) { svn_cl__conflict_baton_t *b = apr_palloc(pool, sizeof(*b)); b->accept_which = accept_which; b->config = config; b->editor_cmd = editor_cmd; b->external_failed = FALSE; b->pb = pb; return b; } svn_cl__accept_t svn_cl__accept_from_word(const char *word) { if (strcmp(word, SVN_CL__ACCEPT_POSTPONE) == 0) return svn_cl__accept_postpone; if (strcmp(word, SVN_CL__ACCEPT_BASE) == 0) return svn_cl__accept_base; if (strcmp(word, SVN_CL__ACCEPT_WORKING) == 0) return svn_cl__accept_working; if (strcmp(word, SVN_CL__ACCEPT_MINE_CONFLICT) == 0) return svn_cl__accept_mine_conflict; if (strcmp(word, SVN_CL__ACCEPT_THEIRS_CONFLICT) == 0) return svn_cl__accept_theirs_conflict; if (strcmp(word, SVN_CL__ACCEPT_MINE_FULL) == 0) return svn_cl__accept_mine_full; if (strcmp(word, SVN_CL__ACCEPT_THEIRS_FULL) == 0) return svn_cl__accept_theirs_full; if (strcmp(word, SVN_CL__ACCEPT_EDIT) == 0) return svn_cl__accept_edit; if (strcmp(word, SVN_CL__ACCEPT_LAUNCH) == 0) return svn_cl__accept_launch; /* word is an invalid action. */ return svn_cl__accept_invalid; } /* Print on stdout a diff between the 'base' and 'merged' files, if both of * those are available, else between 'their' and 'my' files, of DESC. */ static svn_error_t * show_diff(const svn_wc_conflict_description_t *desc, apr_pool_t *pool) { const char *path1, *path2; svn_diff_t *diff; svn_stream_t *output; svn_diff_file_options_t *options; if (desc->merged_file && desc->base_file) { /* Show the conflict markers to the user */ path1 = desc->base_file; path2 = desc->merged_file; } else { /* There's no base file, but we can show the difference between mine and theirs. */ path1 = desc->their_file; path2 = desc->my_file; } 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, NULL, NULL, 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_description_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_file, desc->my_file, desc->their_file, 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_file, desc->my_file, desc->their_file, _("||||||| ORIGINAL"), _("<<<<<<< MINE (select with 'mc')"), _(">>>>>>> THEIRS (select with 'tc')"), "=======", svn_diff_conflict_display_only_conflicts, pool); } /* Run an external editor, passing it the 'merged' file in DESC, 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 svn_wc_conflict_description_t *desc, svn_cl__conflict_baton_t *b, apr_pool_t *pool) { svn_error_t *err; if (desc->merged_file) { err = svn_cl__edit_file_externally(desc->merged_file, b->editor_cmd, b->config, pool); if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR)) { SVN_ERR(svn_cmdline_fprintf(stderr, pool, "%s\n", err->message ? err->message : _("No editor found."))); 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 editor."))); svn_error_clear(err); } else if (err) return 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 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_description_t *desc, svn_cl__conflict_baton_t *b, apr_pool_t *pool) { svn_error_t *err; err = svn_cl__merge_file_externally(desc->base_file, desc->their_file, desc->my_file, desc->merged_file, b->config, 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.\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."))); svn_error_clear(err); } else if (err) return err; else if (performed_edit) *performed_edit = TRUE; return SVN_NO_ERROR; } /* Implement svn_wc_conflict_resolver_func_t; resolves based on --accept option if given, else by prompting. */ svn_error_t * svn_cl__conflict_handler(svn_wc_conflict_result_t **result, const svn_wc_conflict_description_t *desc, void *baton, apr_pool_t *pool) { svn_cl__conflict_baton_t *b = baton; svn_error_t *err; apr_pool_t *subpool; /* Start out assuming we're going to postpone the conflict. */ *result = svn_wc_create_conflict_result(svn_wc_conflict_choose_postpone, NULL, 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: (*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_cl__edit_file_externally(desc->merged_file, b->editor_cmd, b->config, pool); if (err && (err->apr_err == SVN_ERR_CL_NO_EXTERNAL_EDITOR)) { SVN_ERR(svn_cmdline_fprintf(stderr, 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, 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 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_file && desc->their_file && desc->my_file && desc->merged_file) { if (b->external_failed) { (*result)->choice = svn_wc_conflict_choose_postpone; return SVN_NO_ERROR; } err = svn_cl__merge_file_externally(desc->base_file, desc->their_file, desc->my_file, desc->merged_file, b->config, 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;" " 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, pool, "%s\n", err->message ? err->message : _("Error running merge tool;" " leaving all conflicts."))); svn_error_clear(err); b->external_failed = TRUE; } else if (err) return err; (*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. */ subpool = svn_pool_create(pool); /* Handle the most common cases, which is either: Conflicting edits on a file's text, or Conflicting edits on a property. */ if (((desc->node_kind == svn_node_file) && (desc->action == svn_wc_conflict_action_edit) && (desc->reason == svn_wc_conflict_reason_edited)) || (desc->kind == svn_wc_conflict_kind_property)) { const char *answer; char *prompt; 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; if (desc->kind == svn_wc_conflict_kind_text) SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Conflict discovered in '%s'.\n"), desc->path)); else if (desc->kind == svn_wc_conflict_kind_property) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Conflict for property '%s' discovered" " on '%s'.\n"), desc->property_name, desc->path)); if ((!desc->my_file && desc->their_file) || (desc->my_file && !desc->their_file)) { /* One agent wants to change the property, one wants to delete it. This is not something we can diff, so we just tell the user. */ svn_stringbuf_t *myval = NULL, *theirval = NULL; if (desc->my_file) { SVN_ERR(svn_stringbuf_from_file2(&myval, desc->my_file, subpool)); SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("They want to delete the property, " "you want to change the value to '%s'.\n"), myval->data)); } else { SVN_ERR(svn_stringbuf_from_file2(&theirval, desc->their_file, subpool)); SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("They want to change the property value to '%s', " "you want to delete the property.\n"), theirval->data)); } } } else /* We don't recognize any other sort of conflict yet */ return SVN_NO_ERROR; /* 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_file) || (!desc->base_file && desc->my_file && desc->their_file)) diff_allowed = TRUE; while (TRUE) { svn_pool_clear(subpool); prompt = apr_pstrdup(subpool, _("Select: (p) postpone")); if (diff_allowed) { prompt = apr_pstrcat(subpool, prompt, _(", (df) diff-full, (e) edit"), NULL); if (knows_something) prompt = apr_pstrcat(subpool, prompt, _(", (r) resolved"), NULL); if (! desc->is_binary && desc->kind != svn_wc_conflict_kind_property) prompt = apr_pstrcat(subpool, prompt, _(",\n (mc) mine-conflict, " "(tc) theirs-conflict"), NULL); } else { if (knows_something) prompt = apr_pstrcat(subpool, prompt, _(", (r) resolved"), NULL); prompt = apr_pstrcat(subpool, prompt, _(",\n " "(mf) mine-full, (tf) theirs-full"), NULL); } prompt = apr_pstrcat(subpool, prompt, ",\n ", NULL); prompt = apr_pstrcat(subpool, prompt, _("(s) show all options: "), NULL); SVN_ERR(svn_cmdline_prompt_user2(&answer, prompt, b->pb, subpool)); if (strcmp(answer, "s") == 0) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("\n" " (e) edit - change merged file in an editor\n" " (df) diff-full - show all changes made to merged " "file\n" " (r) resolved - accept merged version of file\n" "\n" " (dc) display-conflict - show all conflicts " "(ignoring merged version)\n" " (mc) mine-conflict - accept my version for all " "conflicts (same)\n" " (tc) theirs-conflict - accept their version for all " "conflicts (same)\n" "\n" " (mf) mine-full - accept my version of entire file " "(even non-conflicts)\n" " (tf) theirs-full - accept their version of entire " "file (same)\n" "\n" " (p) postpone - mark the conflict to be " "resolved later\n" " (l) launch - launch external tool to " "resolve conflict\n" " (s) show all - show this list\n\n"))); } else if (strcmp(answer, "p") == 0) { /* Do nothing, let file be marked conflicted. */ (*result)->choice = svn_wc_conflict_choose_postpone; break; } else if (strcmp(answer, "mc") == 0) { if (desc->is_binary) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option; cannot choose " "based on conflicts in a " "binary file.\n\n"))); continue; } else if (desc->kind == svn_wc_conflict_kind_property) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option; cannot choose " "based on conflicts for " "properties.\n\n"))); continue; } (*result)->choice = svn_wc_conflict_choose_mine_conflict; if (performed_edit) (*result)->save_merged = TRUE; break; } else if (strcmp(answer, "tc") == 0) { if (desc->is_binary) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option; cannot choose " "based on conflicts in a " "binary file.\n\n"))); continue; } else if (desc->kind == svn_wc_conflict_kind_property) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option; cannot choose " "based on conflicts for " "properties.\n\n"))); continue; } (*result)->choice = svn_wc_conflict_choose_theirs_conflict; if (performed_edit) (*result)->save_merged = TRUE; break; } else if (strcmp(answer, "mf") == 0) { (*result)->choice = svn_wc_conflict_choose_mine_full; if (performed_edit) (*result)->save_merged = TRUE; break; } else if (strcmp(answer, "tf") == 0) { (*result)->choice = svn_wc_conflict_choose_theirs_full; if (performed_edit) (*result)->save_merged = TRUE; break; } else if (strcmp(answer, "dc") == 0) { if (desc->is_binary) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option; cannot " "display conflicts for a " "binary file.\n\n"))); continue; } else if (desc->kind == svn_wc_conflict_kind_property) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option; cannot " "display conflicts for " "properties.\n\n"))); continue; } else if (! (desc->my_file && desc->base_file && desc->their_file)) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option; original " "files not available.\n\n"))); continue; } SVN_ERR(show_conflicts(desc, subpool)); knows_something = TRUE; } else if (strcmp(answer, "df") == 0) { if (! diff_allowed) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option; there's no " "merged version to diff.\n\n"))); continue; } SVN_ERR(show_diff(desc, subpool)); knows_something = TRUE; } else if (strcmp(answer, "e") == 0) { SVN_ERR(open_editor(&performed_edit, desc, b, subpool)); if (performed_edit) knows_something = TRUE; } else if (strcmp(answer, "l") == 0) { if (desc->base_file && desc->their_file && desc->my_file && desc->merged_file) { SVN_ERR(launch_resolver(&performed_edit, desc, b, subpool)); if (performed_edit) knows_something = TRUE; } else SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option.\n\n"))); } else if (strcmp(answer, "r") == 0) { /* 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 (knows_something) { (*result)->choice = svn_wc_conflict_choose_merged; break; } else SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _("Invalid option.\n\n"))); } } } /* 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)) { const char *answer; const char *prompt; SVN_ERR(svn_cmdline_fprintf( stderr, subpool, _("Conflict discovered when trying to add '%s'.\n" "An object of the same name already exists.\n"), desc->path)); prompt = _("Select: (p) postpone, (mf) mine-full, " "(tf) theirs-full, (h) help:"); while (1) { svn_pool_clear(subpool); SVN_ERR(svn_cmdline_prompt_user2(&answer, prompt, b->pb, subpool)); if (strcmp(answer, "h") == 0 || strcmp(answer, "?") == 0) { SVN_ERR(svn_cmdline_fprintf(stderr, subpool, _(" (p) postpone - resolve the conflict later\n" " (mf) mine-full - accept pre-existing item " "(ignore upstream addition)\n" " (tf) theirs-full - accept incoming item " "(overwrite pre-existing item)\n" " (h) help - show this help\n\n"))); } if (strcmp(answer, "p") == 0) { (*result)->choice = svn_wc_conflict_choose_postpone; break; } if (strcmp(answer, "mf") == 0) { (*result)->choice = svn_wc_conflict_choose_mine_full; break; } if (strcmp(answer, "tf") == 0) { (*result)->choice = svn_wc_conflict_choose_theirs_full; break; } } } else /* other types of conflicts -- do nothing about them. */ { (*result)->choice = svn_wc_conflict_choose_postpone; } svn_pool_destroy(subpool); return SVN_NO_ERROR; }