blob: a158576ffe008d6096734d6bec07908f60b41154 [file] [log] [blame]
/*
* conflict-callbacks.c: conflict resolution callbacks specific to the
* commandline client.
*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*/
#include <apr_xlate.h> /* for APR_LOCALE_CHARSET */
#define APR_WANT_STDIO
#define APR_WANT_STRFUNC
#include <apr_want.h>
#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)
{
/* 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 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 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 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,
desc->path, 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.\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 svn_error_trace(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 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_file && desc->their_file
&& desc->my_file && 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_file,
desc->their_file,
desc->my_file,
desc->merged_file,
desc->path,
b->config,
&remains_in_conflict,
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.")));
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, 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. */
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"),
(char *)NULL);
if (knows_something)
prompt = apr_pstrcat(subpool, prompt, _(", (r) resolved"),
(char *)NULL);
if (! desc->is_binary &&
desc->kind != svn_wc_conflict_kind_property)
prompt = apr_pstrcat(subpool, prompt,
_(",\n (mc) mine-conflict, "
"(tc) theirs-conflict"),
(char *)NULL);
}
else
{
if (knows_something)
prompt = apr_pstrcat(subpool, prompt, _(", (r) resolved"),
(char *)NULL);
prompt = apr_pstrcat(subpool, prompt,
_(",\n "
"(mf) mine-full, (tf) theirs-full"),
(char *)NULL);
}
prompt = apr_pstrcat(subpool, prompt, ",\n ", (char *)NULL);
prompt = apr_pstrcat(subpool, prompt,
_("(s) show all options: "),
(char *)NULL);
SVN_ERR(svn_cmdline_prompt_user2(&answer, prompt, b->pb, subpool));
if (strcmp(answer, "s") == 0)
{
/* These are used in svn_cl__accept_from_word(). */
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 || 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 || strcmp(answer, "X-)") == 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 || strcmp(answer, "X-(") == 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 || strcmp(answer, ":-)") == 0)
{
(*result)->choice = svn_wc_conflict_choose_mine_full;
if (performed_edit)
(*result)->save_merged = TRUE;
break;
}
else if (strcmp(answer, "tf") == 0 || strcmp(answer, ":-(") == 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 || 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 || strcmp(answer, ":-l") == 0)
{
if (desc->kind == svn_wc_conflict_kind_property)
{
SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
_("Invalid option; cannot "
"resolve property conflicts "
"with an external merge tool."
"\n\n")));
continue;
}
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 || strcmp(answer, ":-P") == 0)
{
(*result)->choice = svn_wc_conflict_choose_postpone;
break;
}
if (strcmp(answer, "mf") == 0 || strcmp(answer, ":-)") == 0)
{
(*result)->choice = svn_wc_conflict_choose_mine_full;
break;
}
if (strcmp(answer, "tf") == 0 || strcmp(answer, ":-(") == 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;
}