diff options
Diffstat (limited to 'subversion/libsvn_client/conflicts.c')
-rw-r--r-- | subversion/libsvn_client/conflicts.c | 11207 |
1 files changed, 11207 insertions, 0 deletions
diff --git a/subversion/libsvn_client/conflicts.c b/subversion/libsvn_client/conflicts.c new file mode 100644 index 000000000000..0fd9a2bbec5e --- /dev/null +++ b/subversion/libsvn_client/conflicts.c @@ -0,0 +1,11207 @@ +/* + * conflicts.c: conflict resolver implementation + * + * ==================================================================== + * 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. + * ==================================================================== + */ + +/* ==================================================================== */ + + + +/*** Includes. ***/ + +#include "svn_types.h" +#include "svn_wc.h" +#include "svn_client.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_pools.h" +#include "svn_props.h" +#include "svn_hash.h" +#include "svn_sorts.h" +#include "svn_subst.h" +#include "client.h" + +#include "private/svn_diff_tree.h" +#include "private/svn_ra_private.h" +#include "private/svn_sorts_private.h" +#include "private/svn_token.h" +#include "private/svn_wc_private.h" + +#include "svn_private_config.h" + +#define ARRAY_LEN(ary) ((sizeof (ary)) / (sizeof ((ary)[0]))) + + +/*** Dealing with conflicts. ***/ + +/* Describe a tree conflict. */ +typedef svn_error_t *(*tree_conflict_get_description_func_t)( + const char **change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + +/* Get more information about a tree conflict. + * This function may contact the repository. */ +typedef svn_error_t *(*tree_conflict_get_details_func_t)( + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool); + +struct svn_client_conflict_t +{ + const char *local_abspath; + apr_hash_t *prop_conflicts; + + /* Indicate which options were chosen to resolve a text or tree conflict + * on the conflicted node. */ + svn_client_conflict_option_id_t resolution_text; + svn_client_conflict_option_id_t resolution_tree; + + /* A mapping from const char* property name to pointers to + * svn_client_conflict_option_t for all properties which had their + * conflicts resolved. Indicates which options were chosen to resolve + * the property conflicts. */ + apr_hash_t *resolved_props; + + /* Ask a tree conflict to describe itself. */ + tree_conflict_get_description_func_t + tree_conflict_get_incoming_description_func; + tree_conflict_get_description_func_t + tree_conflict_get_local_description_func; + + /* Ask a tree conflict to find out more information about itself + * by contacting the repository. */ + tree_conflict_get_details_func_t tree_conflict_get_incoming_details_func; + tree_conflict_get_details_func_t tree_conflict_get_local_details_func; + + /* Any additional information found can be stored here and may be used + * when describing a tree conflict. */ + void *tree_conflict_incoming_details; + void *tree_conflict_local_details; + + /* The pool this conflict was allocated from. */ + apr_pool_t *pool; + + /* Conflict data provided by libsvn_wc. */ + const svn_wc_conflict_description2_t *legacy_text_conflict; + const char *legacy_prop_conflict_propname; + const svn_wc_conflict_description2_t *legacy_tree_conflict; + + /* The recommended resolution option's ID. */ + svn_client_conflict_option_id_t recommended_option_id; +}; + +/* Resolves conflict to OPTION and sets CONFLICT->RESOLUTION accordingly. + * + * May raise an error in case the conflict could not be resolved. A common + * case would be a tree conflict the resolution of which depends on other + * tree conflicts to be resolved first. */ +typedef svn_error_t *(*conflict_option_resolve_func_t)( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool); + +struct svn_client_conflict_option_t +{ + svn_client_conflict_option_id_t id; + const char *label; + const char *description; + + svn_client_conflict_t *conflict; + conflict_option_resolve_func_t do_resolve_func; + + /* The pool this option was allocated from. */ + apr_pool_t *pool; + + /* Data which is specific to particular conflicts and options. */ + union { + struct { + /* Indicates the property to resolve in case of a property conflict. + * If set to "", all properties are resolved to this option. */ + const char *propname; + + /* A merged property value, if supplied by the API user, else NULL. */ + const svn_string_t *merged_propval; + } prop; + } type_data; + +}; + +/* + * Return a legacy conflict choice corresponding to OPTION_ID. + * Return svn_wc_conflict_choose_undefined if no corresponding + * legacy conflict choice exists. + */ +static svn_wc_conflict_choice_t +conflict_option_id_to_wc_conflict_choice( + svn_client_conflict_option_id_t option_id) +{ + + switch (option_id) + { + case svn_client_conflict_option_undefined: + return svn_wc_conflict_choose_undefined; + + case svn_client_conflict_option_postpone: + return svn_wc_conflict_choose_postpone; + + case svn_client_conflict_option_base_text: + return svn_wc_conflict_choose_base; + + case svn_client_conflict_option_incoming_text: + return svn_wc_conflict_choose_theirs_full; + + case svn_client_conflict_option_working_text: + return svn_wc_conflict_choose_mine_full; + + case svn_client_conflict_option_incoming_text_where_conflicted: + return svn_wc_conflict_choose_theirs_conflict; + + case svn_client_conflict_option_working_text_where_conflicted: + return svn_wc_conflict_choose_mine_conflict; + + case svn_client_conflict_option_merged_text: + return svn_wc_conflict_choose_merged; + + case svn_client_conflict_option_unspecified: + return svn_wc_conflict_choose_unspecified; + + default: + break; + } + + return svn_wc_conflict_choose_undefined; +} + +static void +add_legacy_desc_to_conflict(const svn_wc_conflict_description2_t *desc, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool) +{ + switch (desc->kind) + { + case svn_wc_conflict_kind_text: + conflict->legacy_text_conflict = desc; + break; + + case svn_wc_conflict_kind_property: + if (conflict->prop_conflicts == NULL) + conflict->prop_conflicts = apr_hash_make(result_pool); + svn_hash_sets(conflict->prop_conflicts, desc->property_name, desc); + conflict->legacy_prop_conflict_propname = desc->property_name; + break; + + case svn_wc_conflict_kind_tree: + conflict->legacy_tree_conflict = desc; + break; + + default: + SVN_ERR_ASSERT_NO_RETURN(FALSE); /* unknown kind of conflict */ + } +} + +/* A map for svn_wc_conflict_action_t values to strings */ +static const svn_token_map_t map_conflict_action[] = +{ + { "edit", svn_wc_conflict_action_edit }, + { "delete", svn_wc_conflict_action_delete }, + { "add", svn_wc_conflict_action_add }, + { "replace", svn_wc_conflict_action_replace }, + { NULL, 0 } +}; + +/* A map for svn_wc_conflict_reason_t values to strings */ +static const svn_token_map_t map_conflict_reason[] = +{ + { "edit", svn_wc_conflict_reason_edited }, + { "delete", svn_wc_conflict_reason_deleted }, + { "missing", svn_wc_conflict_reason_missing }, + { "obstruction", svn_wc_conflict_reason_obstructed }, + { "add", svn_wc_conflict_reason_added }, + { "replace", svn_wc_conflict_reason_replaced }, + { "unversioned", svn_wc_conflict_reason_unversioned }, + { "moved-away", svn_wc_conflict_reason_moved_away }, + { "moved-here", svn_wc_conflict_reason_moved_here }, + { NULL, 0 } +}; + +/* Describes a server-side move (really a copy+delete within the same + * revision) which was identified by scanning the revision log. + * This structure can represent one or more "chains" of moves, i.e. + * multiple move operations which occurred across a range of revisions. */ +struct repos_move_info { + /* The revision in which this move was committed. */ + svn_revnum_t rev; + + /* The author who commited the revision in which this move was committed. */ + const char *rev_author; + + /* The repository relpath the node was moved from in this revision. */ + const char *moved_from_repos_relpath; + + /* The repository relpath the node was moved to in this revision. */ + const char *moved_to_repos_relpath; + + /* The copyfrom revision of the moved-to path. */ + svn_revnum_t copyfrom_rev; + + /* The node kind of the item being moved. */ + svn_node_kind_t node_kind; + + /* Prev pointer. NULL if no prior move exists in the chain. */ + struct repos_move_info *prev; + + /* An array of struct repos_move_info * elements, each representing + * a possible way forward in the move chain. NULL if no next move + * exists in this chain. If the deleted node was copied only once in + * this revision, then this array has only one element and the move + * chain does not fork. But if this revision contains multiple copies of + * the deleted node, each of these copies appears as an element of this + * array, and each element represents a different path the next move + * might have taken. */ + apr_array_header_t *next; +}; + +static svn_revnum_t +rev_below(svn_revnum_t rev) +{ + SVN_ERR_ASSERT_NO_RETURN(rev != SVN_INVALID_REVNUM); + SVN_ERR_ASSERT_NO_RETURN(rev > 0); + + return rev == 1 ? 1 : rev - 1; +} + +/* Set *RELATED to true if the deleted node DELETED_REPOS_RELPATH@DELETED_REV + * is an ancestor of the copied node COPYFROM_PATH@COPYFROM_REV. + * If CHECK_LAST_CHANGED_REV is non-zero, also ensure that the copied node + * is a copy of the deleted node's last-changed revision's content, rather + * than a copy of some older content. If it's not, set *RELATED to false. */ +static svn_error_t * +check_move_ancestry(svn_boolean_t *related, + svn_ra_session_t *ra_session, + const char *repos_root_url, + const char *deleted_repos_relpath, + svn_revnum_t deleted_rev, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + svn_boolean_t check_last_changed_rev, + apr_pool_t *scratch_pool) +{ + apr_hash_t *locations; + const char *deleted_url; + const char *deleted_location; + apr_array_header_t *location_revisions; + const char *old_session_url; + + location_revisions = apr_array_make(scratch_pool, 1, sizeof(svn_revnum_t)); + APR_ARRAY_PUSH(location_revisions, svn_revnum_t) = copyfrom_rev; + deleted_url = svn_uri_canonicalize(apr_pstrcat(scratch_pool, + repos_root_url, "/", + deleted_repos_relpath, + NULL), + scratch_pool); + SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session, + deleted_url, scratch_pool)); + SVN_ERR(svn_ra_get_locations(ra_session, &locations, "", + rev_below(deleted_rev), location_revisions, + scratch_pool)); + + deleted_location = apr_hash_get(locations, ©from_rev, + sizeof(svn_revnum_t)); + if (deleted_location) + { + if (deleted_location[0] == '/') + deleted_location++; + if (strcmp(deleted_location, copyfrom_path) != 0) + { + *related = FALSE; + return SVN_NO_ERROR; + } + } + else + { + *related = FALSE; + return SVN_NO_ERROR; + } + + if (check_last_changed_rev) + { + svn_dirent_t *dirent; + + /* Verify that copyfrom_rev >= last-changed revision of the + * deleted node. */ + SVN_ERR(svn_ra_stat(ra_session, "", rev_below(deleted_rev), &dirent, + scratch_pool)); + if (dirent == NULL || copyfrom_rev < dirent->created_rev) + { + *related = FALSE; + return SVN_NO_ERROR; + } + } + + *related = TRUE; + return SVN_NO_ERROR; +} + +struct copy_info { + const char *copyto_path; + const char *copyfrom_path; + svn_revnum_t copyfrom_rev; + svn_node_kind_t node_kind; +}; + +/* Allocate and return a NEW_MOVE, and update MOVED_PATHS with this new move. */ +static svn_error_t * +add_new_move(struct repos_move_info **new_move, + const char *deleted_repos_relpath, + const char *copyto_path, + svn_revnum_t copyfrom_rev, + svn_node_kind_t node_kind, + svn_revnum_t revision, + const char *author, + apr_hash_t *moved_paths, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct repos_move_info *move; + struct repos_move_info *next_move; + + move = apr_pcalloc(result_pool, sizeof(*move)); + move->moved_from_repos_relpath = apr_pstrdup(result_pool, + deleted_repos_relpath); + move->moved_to_repos_relpath = apr_pstrdup(result_pool, copyto_path); + move->rev = revision; + move->rev_author = apr_pstrdup(result_pool, author); + move->copyfrom_rev = copyfrom_rev; + move->node_kind = node_kind; + + /* Link together multiple moves of the same node. + * Note that we're traversing history backwards, so moves already + * present in the list happened in younger revisions. */ + next_move = svn_hash_gets(moved_paths, move->moved_to_repos_relpath); + if (next_move) + { + svn_boolean_t related; + + /* Tracing back history of the delete-half of the next move + * to the copyfrom-revision of the prior move we must end up + * at the delete-half of the prior move. */ + SVN_ERR(check_move_ancestry(&related, ra_session, repos_root_url, + next_move->moved_from_repos_relpath, + next_move->rev, + move->moved_from_repos_relpath, + move->copyfrom_rev, + FALSE, scratch_pool)); + if (related) + { + SVN_ERR_ASSERT(move->rev < next_move->rev); + + /* Prepend this move to the linked list. */ + if (move->next == NULL) + move->next = apr_array_make(result_pool, 1, + sizeof (struct repos_move_info *)); + APR_ARRAY_PUSH(move->next, struct repos_move_info *) = next_move; + next_move->prev = move; + } + } + + /* Make this move the head of our next-move linking map. */ + svn_hash_sets(moved_paths, move->moved_from_repos_relpath, move); + + *new_move = move; + return SVN_NO_ERROR; +} + +/* Push a MOVE into the MOVES_TABLE. */ +static void +push_move(struct repos_move_info *move, apr_hash_t *moves_table, + apr_pool_t *result_pool) +{ + apr_array_header_t *moves; + + /* Add this move to the list of moves in the revision. */ + moves = apr_hash_get(moves_table, &move->rev, sizeof(svn_revnum_t)); + if (moves == NULL) + { + /* It is the first move in this revision. Create the list. */ + moves = apr_array_make(result_pool, 1, sizeof(struct repos_move_info *)); + apr_hash_set(moves_table, &move->rev, sizeof(svn_revnum_t), moves); + } + APR_ARRAY_PUSH(moves, struct repos_move_info *) = move; +} + +/* Find the youngest common ancestor of REPOS_RELPATH1@PEG_REV1 and + * REPOS_RELPATH2@PEG_REV2. Return the result in *YCA_LOC. + * Set *YCA_LOC to NULL if no common ancestor exists. */ +static svn_error_t * +find_yca(svn_client__pathrev_t **yca_loc, + const char *repos_relpath1, + svn_revnum_t peg_rev1, + const char *repos_relpath2, + svn_revnum_t peg_rev2, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client__pathrev_t *loc1; + svn_client__pathrev_t *loc2; + + *yca_loc = NULL; + + loc1 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid, + peg_rev1, repos_relpath1, + scratch_pool); + loc2 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid, + peg_rev2, repos_relpath2, + scratch_pool); + SVN_ERR(svn_client__get_youngest_common_ancestor(yca_loc, loc1, loc2, + ra_session, ctx, + result_pool, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Like find_yca, expect that a YCA could also be found via a brute-force + * search of parents of REPOS_RELPATH1 and REPOS_RELPATH2, if no "direct" + * YCA exists. An implicit assumption is that some parent of REPOS_RELPATH1 + * is a branch of some parent of REPOS_RELPATH2. + * + * This function can guess a "good enough" YCA for 'missing nodes' which do + * not exist in the working copy, e.g. when a file edit is merged to a path + * which does not exist in the working copy. + */ +static svn_error_t * +find_nearest_yca(svn_client__pathrev_t **yca_locp, + const char *repos_relpath1, + svn_revnum_t peg_rev1, + const char *repos_relpath2, + svn_revnum_t peg_rev2, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + apr_pool_t *iterpool; + const char *p1, *p2; + apr_size_t c1, c2; + + *yca_locp = NULL; + + iterpool = svn_pool_create(scratch_pool); + + p1 = repos_relpath1; + c1 = svn_path_component_count(repos_relpath1); + while (c1--) + { + svn_pool_clear(iterpool); + + p2 = repos_relpath2; + c2 = svn_path_component_count(repos_relpath2); + while (c2--) + { + err = find_yca(&yca_loc, p1, peg_rev1, p2, peg_rev2, + repos_root_url, repos_uuid, ra_session, ctx, + result_pool, iterpool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + if (yca_loc) + { + *yca_locp = yca_loc; + svn_pool_destroy(iterpool); + return SVN_NO_ERROR; + } + + p2 = svn_relpath_dirname(p2, scratch_pool); + } + + p1 = svn_relpath_dirname(p1, scratch_pool); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Check if the copied node described by COPY and the DELETED_PATH@DELETED_REV + * share a common ancestor. If so, return new repos_move_info in *MOVE which + * describes a move from the deleted path to that copy's destination. */ +static svn_error_t * +find_related_move(struct repos_move_info **move, + struct copy_info *copy, + const char *deleted_repos_relpath, + svn_revnum_t deleted_rev, + const char *author, + apr_hash_t *moved_paths, + const char *repos_root_url, + const char *repos_uuid, + svn_client_ctx_t *ctx, + svn_ra_session_t *ra_session, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + + *move = NULL; + err = find_yca(&yca_loc, copy->copyfrom_path, copy->copyfrom_rev, + deleted_repos_relpath, rev_below(deleted_rev), + repos_root_url, repos_uuid, ra_session, ctx, + scratch_pool, scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + if (yca_loc) + SVN_ERR(add_new_move(move, deleted_repos_relpath, + copy->copyto_path, copy->copyfrom_rev, + copy->node_kind, deleted_rev, author, + moved_paths, ra_session, repos_root_url, + result_pool, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Detect moves by matching DELETED_REPOS_RELPATH@DELETED_REV to the copies + * in COPIES. Add any moves found to MOVES_TABLE and update MOVED_PATHS. */ +static svn_error_t * +match_copies_to_deletion(const char *deleted_repos_relpath, + svn_revnum_t deleted_rev, + const char *author, + apr_hash_t *copies, + apr_hash_t *moves_table, + apr_hash_t *moved_paths, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_hash_index_t *hi; + apr_pool_t *iterpool; + + iterpool = svn_pool_create(scratch_pool); + for (hi = apr_hash_first(scratch_pool, copies); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *copyfrom_path = apr_hash_this_key(hi); + apr_array_header_t *copies_with_same_source_path; + int i; + + svn_pool_clear(iterpool); + + copies_with_same_source_path = apr_hash_this_val(hi); + + if (strcmp(copyfrom_path, deleted_repos_relpath) == 0) + { + /* We found a copyfrom path which matches a deleted node. + * Check if the deleted node is an ancestor of the copied node. */ + for (i = 0; i < copies_with_same_source_path->nelts; i++) + { + struct copy_info *copy; + svn_boolean_t related; + struct repos_move_info *move; + + copy = APR_ARRAY_IDX(copies_with_same_source_path, i, + struct copy_info *); + SVN_ERR(check_move_ancestry(&related, + ra_session, repos_root_url, + deleted_repos_relpath, + deleted_rev, + copy->copyfrom_path, + copy->copyfrom_rev, + TRUE, iterpool)); + if (!related) + continue; + + /* Remember details of this move. */ + SVN_ERR(add_new_move(&move, deleted_repos_relpath, + copy->copyto_path, copy->copyfrom_rev, + copy->node_kind, deleted_rev, author, + moved_paths, ra_session, repos_root_url, + result_pool, iterpool)); + push_move(move, moves_table, result_pool); + } + } + else + { + /* Check if this deleted node is related to any copies in this + * revision. These could be moves of the deleted node which + * were merged here from other lines of history. */ + for (i = 0; i < copies_with_same_source_path->nelts; i++) + { + struct copy_info *copy; + struct repos_move_info *move = NULL; + + copy = APR_ARRAY_IDX(copies_with_same_source_path, i, + struct copy_info *); + SVN_ERR(find_related_move(&move, copy, deleted_repos_relpath, + deleted_rev, author, + moved_paths, + repos_root_url, repos_uuid, + ctx, ra_session, + result_pool, iterpool)); + if (move) + push_move(move, moves_table, result_pool); + } + } + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Update MOVES_TABLE and MOVED_PATHS based on information from + * revision data in LOG_ENTRY, COPIES, and DELETED_PATHS. + * Use RA_SESSION to perform the necessary requests. */ +static svn_error_t * +find_moves_in_revision(svn_ra_session_t *ra_session, + apr_hash_t *moves_table, + apr_hash_t *moved_paths, + svn_log_entry_t *log_entry, + apr_hash_t *copies, + apr_array_header_t *deleted_paths, + const char *repos_root_url, + const char *repos_uuid, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_pool_t *iterpool; + int i; + const svn_string_t *author; + + author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < deleted_paths->nelts; i++) + { + const char *deleted_repos_relpath; + + svn_pool_clear(iterpool); + + deleted_repos_relpath = APR_ARRAY_IDX(deleted_paths, i, const char *); + SVN_ERR(match_copies_to_deletion(deleted_repos_relpath, + log_entry->revision, + author ? author->data + : _("unknown author"), + copies, moves_table, moved_paths, + repos_root_url, repos_uuid, ra_session, + ctx, result_pool, iterpool)); + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +struct find_deleted_rev_baton +{ + /* Variables below are arguments provided by the caller of + * svn_ra_get_log2(). */ + const char *deleted_repos_relpath; + const char *related_repos_relpath; + svn_revnum_t related_peg_rev; + const char *repos_root_url; + const char *repos_uuid; + svn_client_ctx_t *ctx; + const char *victim_abspath; /* for notifications */ + + /* Variables below are results for the caller of svn_ra_get_log2(). */ + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + apr_pool_t *result_pool; + + apr_hash_t *moves_table; /* Obtained from find_moves_in_revision(). */ + struct repos_move_info *move; /* Last known move which affected the node. */ + + /* Extra RA session that can be used to make additional requests. */ + svn_ra_session_t *extra_ra_session; +}; + +/* If DELETED_RELPATH matches the moved-from path of a move in MOVES, + * or if DELETED_RELPATH is a child of a moved-to path in MOVES, return + * a struct move_info for the corresponding move. Else, return NULL. */ +static struct repos_move_info * +map_deleted_path_to_move(const char *deleted_relpath, + apr_array_header_t *moves, + apr_pool_t *scratch_pool) +{ + struct repos_move_info *closest_move = NULL; + apr_size_t min_components = 0; + int i; + + for (i = 0; i < moves->nelts; i++) + { + const char *relpath; + struct repos_move_info *move; + + move = APR_ARRAY_IDX(moves, i, struct repos_move_info *); + if (strcmp(move->moved_from_repos_relpath, deleted_relpath) == 0) + return move; + + relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, + deleted_relpath); + if (relpath) + { + /* This could be a nested move. Return the path-wise closest move. */ + const apr_size_t c = svn_path_component_count(relpath); + if (c == 0) + return move; + else if (min_components == 0 || c < min_components) + { + min_components = c; + closest_move = move; + } + } + } + + if (closest_move) + { + const char *relpath; + const char *moved_along_path; + struct repos_move_info *move; + + /* See if we can find an even closer move for this moved-along path. */ + relpath = svn_relpath_skip_ancestor(closest_move->moved_to_repos_relpath, + deleted_relpath); + moved_along_path = + svn_relpath_join(closest_move->moved_from_repos_relpath, relpath, + scratch_pool); + move = map_deleted_path_to_move(moved_along_path, moves, scratch_pool); + if (move) + return move; + } + + return closest_move; +} + +/* Search for nested moves in REVISION, given the already found MOVES, + * all DELETED_PATHS, and all COPIES, from the same revision. + * Append any nested moves to the MOVES array. */ +static svn_error_t * +find_nested_moves(apr_array_header_t *moves, + apr_hash_t *copies, + apr_array_header_t *deleted_paths, + apr_hash_t *moved_paths, + svn_revnum_t revision, + const char *author, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *nested_moves; + int i; + apr_pool_t *iterpool; + + nested_moves = apr_array_make(result_pool, 0, + sizeof(struct repos_move_info *)); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < deleted_paths->nelts; i++) + { + const char *deleted_path; + const char *child_relpath; + const char *moved_along_repos_relpath; + struct repos_move_info *move; + apr_array_header_t *copies_with_same_source_path; + int j; + svn_boolean_t related; + + svn_pool_clear(iterpool); + + deleted_path = APR_ARRAY_IDX(deleted_paths, i, const char *); + move = map_deleted_path_to_move(deleted_path, moves, iterpool); + if (move == NULL) + continue; + child_relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, + deleted_path); + if (child_relpath == NULL || child_relpath[0] == '\0') + continue; /* not a nested move */ + + /* Consider: svn mv A B; svn mv B/foo C/foo + * Copyfrom for C/foo is A/foo, even though C/foo was moved here from + * B/foo. A/foo was not deleted. It is B/foo which was deleted. + * We now know about the move A->B and moved-along child_relpath "foo". + * Try to detect an ancestral relationship between A/foo and the + * moved-along path. */ + moved_along_repos_relpath = + svn_relpath_join(move->moved_from_repos_relpath, child_relpath, + iterpool); + copies_with_same_source_path = svn_hash_gets(copies, + moved_along_repos_relpath); + if (copies_with_same_source_path == NULL) + continue; /* not a nested move */ + + for (j = 0; j < copies_with_same_source_path->nelts; j++) + { + struct copy_info *copy; + + copy = APR_ARRAY_IDX(copies_with_same_source_path, j, + struct copy_info *); + SVN_ERR(check_move_ancestry(&related, ra_session, repos_root_url, + moved_along_repos_relpath, + revision, + copy->copyfrom_path, + copy->copyfrom_rev, + TRUE, iterpool)); + if (related) + { + struct repos_move_info *nested_move; + + /* Remember details of this move. */ + SVN_ERR(add_new_move(&nested_move, moved_along_repos_relpath, + copy->copyto_path, copy->copyfrom_rev, + copy->node_kind, + revision, author, moved_paths, + ra_session, repos_root_url, + result_pool, iterpool)); + + /* Add this move to the list of nested moves in this revision. */ + APR_ARRAY_PUSH(nested_moves, struct repos_move_info *) = + nested_move; + } + } + } + svn_pool_destroy(iterpool); + + /* Add all nested moves found to the list of all moves in this revision. */ + apr_array_cat(moves, nested_moves); + + return SVN_NO_ERROR; +} + +/* Make a shallow copy of the copied LOG_ITEM in COPIES. */ +static void +cache_copied_item(apr_hash_t *copies, const char *changed_path, + svn_log_changed_path2_t *log_item) +{ + apr_pool_t *result_pool = apr_hash_pool_get(copies); + struct copy_info *copy = apr_palloc(result_pool, sizeof(*copy)); + apr_array_header_t *copies_with_same_source_path; + + copy->copyfrom_path = log_item->copyfrom_path; + if (log_item->copyfrom_path[0] == '/') + copy->copyfrom_path++; + copy->copyto_path = changed_path; + copy->copyfrom_rev = log_item->copyfrom_rev; + copy->node_kind = log_item->node_kind; + + copies_with_same_source_path = apr_hash_get(copies, copy->copyfrom_path, + APR_HASH_KEY_STRING); + if (copies_with_same_source_path == NULL) + { + copies_with_same_source_path = apr_array_make(result_pool, 1, + sizeof(struct copy_info *)); + apr_hash_set(copies, copy->copyfrom_path, APR_HASH_KEY_STRING, + copies_with_same_source_path); + } + APR_ARRAY_PUSH(copies_with_same_source_path, struct copy_info *) = copy; +} + +/* Implements svn_log_entry_receiver_t. + * + * Find the revision in which a node, optionally ancestrally related to the + * node specified via find_deleted_rev_baton, was deleted, When the revision + * was found, store it in BATON->DELETED_REV and abort the log operation + * by raising SVN_ERR_CEASE_INVOCATION. + * + * If no such revision can be found, leave BATON->DELETED_REV and + * BATON->REPLACING_NODE_KIND alone. + * + * If the node was replaced, set BATON->REPLACING_NODE_KIND to the node + * kind of the node which replaced the original node. If the node was not + * replaced, set BATON->REPLACING_NODE_KIND to svn_node_none. + * + * This function answers the same question as svn_ra_get_deleted_rev() but + * works in cases where we do not already know a revision in which the deleted + * node once used to exist. + * + * If the node was moved, rather than deleted, return move information + * in BATON->MOVE. + */ +static svn_error_t * +find_deleted_rev(void *baton, + svn_log_entry_t *log_entry, + apr_pool_t *scratch_pool) +{ + struct find_deleted_rev_baton *b = baton; + apr_hash_index_t *hi; + apr_pool_t *iterpool; + svn_boolean_t deleted_node_found = FALSE; + svn_node_kind_t replacing_node_kind = svn_node_none; + + if (b->ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + b->victim_abspath, + svn_wc_notify_tree_conflict_details_progress, + scratch_pool), + notify->revision = log_entry->revision; + b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); + } + + /* No paths were changed in this revision. Nothing to do. */ + if (! log_entry->changed_paths2) + return SVN_NO_ERROR; + + iterpool = svn_pool_create(scratch_pool); + for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *changed_path = apr_hash_this_key(hi); + svn_log_changed_path2_t *log_item = apr_hash_this_val(hi); + + svn_pool_clear(iterpool); + + /* ### Remove leading slash from paths in log entries. */ + if (changed_path[0] == '/') + changed_path++; + + /* Check if we already found the deleted node we're looking for. */ + if (!deleted_node_found && + svn_path_compare_paths(b->deleted_repos_relpath, changed_path) == 0 && + (log_item->action == 'D' || log_item->action == 'R')) + { + deleted_node_found = TRUE; + + if (b->related_repos_relpath != NULL && + b->related_peg_rev != SVN_INVALID_REVNUM) + { + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + + /* We found a deleted node which occupies the correct path. + * To be certain that this is the deleted node we're looking for, + * we must establish whether it is ancestrally related to the + * "related node" specified in our baton. */ + err = find_yca(&yca_loc, + b->related_repos_relpath, + b->related_peg_rev, + b->deleted_repos_relpath, + rev_below(log_entry->revision), + b->repos_root_url, b->repos_uuid, + b->extra_ra_session, b->ctx, iterpool, iterpool); + if (err) + { + /* ### Happens for moves within other moves and copies. */ + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + deleted_node_found = (yca_loc != NULL); + } + + if (deleted_node_found && log_item->action == 'R') + replacing_node_kind = log_item->node_kind; + } + } + svn_pool_destroy(iterpool); + + if (!deleted_node_found) + { + apr_array_header_t *moves; + + moves = apr_hash_get(b->moves_table, &log_entry->revision, + sizeof(svn_revnum_t)); + if (moves) + { + struct repos_move_info *move; + + move = map_deleted_path_to_move(b->deleted_repos_relpath, + moves, scratch_pool); + if (move) + { + const char *relpath; + + /* The node was moved. Update our search path accordingly. */ + b->move = move; + relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, + b->deleted_repos_relpath); + if (relpath) + b->deleted_repos_relpath = + svn_relpath_join(move->moved_from_repos_relpath, relpath, + b->result_pool); + } + } + } + else + { + svn_string_t *author; + + b->deleted_rev = log_entry->revision; + author = svn_hash_gets(log_entry->revprops, + SVN_PROP_REVISION_AUTHOR); + if (author) + b->deleted_rev_author = apr_pstrdup(b->result_pool, author->data); + else + b->deleted_rev_author = _("unknown author"); + + b->replacing_node_kind = replacing_node_kind; + + /* We're done. Abort the log operation. */ + return svn_error_create(SVN_ERR_CEASE_INVOCATION, NULL, NULL); + } + + return SVN_NO_ERROR; +} + +/* Return a localised string representation of the local part of a tree + conflict on a file. */ +static svn_error_t * +describe_local_file_node_change(const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_reason_t local_change; + svn_wc_operation_t operation; + + local_change = svn_client_conflict_get_local_change(conflict); + operation = svn_client_conflict_get_operation(conflict); + + switch (local_change) + { + case svn_wc_conflict_reason_edited: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("A file containing uncommitted changes was " + "found in the working copy."); + else if (operation == svn_wc_operation_merge) + *description = _("A file which differs from the corresponding " + "file on the merge source branch was found " + "in the working copy."); + break; + case svn_wc_conflict_reason_obstructed: + *description = _("A file which already occupies this path was found " + "in the working copy."); + break; + case svn_wc_conflict_reason_unversioned: + *description = _("An unversioned file was found in the working " + "copy."); + break; + case svn_wc_conflict_reason_deleted: + *description = _("A deleted file was found in the working copy."); + break; + case svn_wc_conflict_reason_missing: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("No such file was found in the working copy."); + else if (operation == svn_wc_operation_merge) + { + /* ### display deleted revision */ + *description = _("No such file was found in the merge target " + "working copy.\nPerhaps the file has been " + "deleted or moved away in the repository's " + "history?"); + } + break; + case svn_wc_conflict_reason_added: + case svn_wc_conflict_reason_replaced: + { + /* ### show more details about copies or replacements? */ + *description = _("A file scheduled to be added to the " + "repository in the next commit was found in " + "the working copy."); + } + break; + case svn_wc_conflict_reason_moved_away: + { + const char *moved_to_abspath; + svn_error_t *err; + + err = svn_wc__node_was_moved_away(&moved_to_abspath, NULL, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) + { + moved_to_abspath = NULL; + svn_error_clear(err); + } + else + return svn_error_trace(err); + } + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (moved_to_abspath == NULL) + { + /* The move no longer exists. */ + *description = _("The file in the working copy had " + "been moved away at the time this " + "conflict was recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("The file in the working copy was " + "moved away to\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_to_abspath), + scratch_pool)); + } + } + else if (operation == svn_wc_operation_merge) + { + if (moved_to_abspath == NULL) + { + /* The move probably happened in branch history. + * This case cannot happen until we detect incoming + * moves, which we currently don't do. */ + /* ### find deleted/moved revision? */ + *description = _("The file in the working copy had " + "been moved away at the time this " + "conflict was recorded."); + } + else + { + /* This is a local move in the working copy. */ + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("The file in the working copy was " + "moved away to\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_to_abspath), + scratch_pool)); + } + } + break; + } + case svn_wc_conflict_reason_moved_here: + { + const char *moved_from_abspath; + + SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (moved_from_abspath == NULL) + { + /* The move no longer exists. */ + *description = _("A file had been moved here in the " + "working copy at the time this " + "conflict was recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("A file was moved here in the " + "working copy from\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_from_abspath), + scratch_pool)); + } + } + else if (operation == svn_wc_operation_merge) + { + if (moved_from_abspath == NULL) + { + /* The move probably happened in branch history. + * This case cannot happen until we detect incoming + * moves, which we currently don't do. */ + /* ### find deleted/moved revision? */ + *description = _("A file had been moved here in the " + "working copy at the time this " + "conflict was recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + /* This is a local move in the working copy. */ + *description = apr_psprintf( + result_pool, + _("A file was moved here in the " + "working copy from\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_from_abspath), + scratch_pool)); + } + } + break; + } + } + + return SVN_NO_ERROR; +} + +/* Return a localised string representation of the local part of a tree + conflict on a directory. */ +static svn_error_t * +describe_local_dir_node_change(const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_reason_t local_change; + svn_wc_operation_t operation; + + local_change = svn_client_conflict_get_local_change(conflict); + operation = svn_client_conflict_get_operation(conflict); + + switch (local_change) + { + case svn_wc_conflict_reason_edited: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("A directory containing uncommitted changes " + "was found in the working copy."); + else if (operation == svn_wc_operation_merge) + *description = _("A directory which differs from the " + "corresponding directory on the merge source " + "branch was found in the working copy."); + break; + case svn_wc_conflict_reason_obstructed: + *description = _("A directory which already occupies this path was " + "found in the working copy."); + break; + case svn_wc_conflict_reason_unversioned: + *description = _("An unversioned directory was found in the " + "working copy."); + break; + case svn_wc_conflict_reason_deleted: + *description = _("A deleted directory was found in the " + "working copy."); + break; + case svn_wc_conflict_reason_missing: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("No such directory was found in the working copy."); + else if (operation == svn_wc_operation_merge) + { + /* ### display deleted revision */ + *description = _("No such directory was found in the merge " + "target working copy.\nPerhaps the " + "directory has been deleted or moved away " + "in the repository's history?"); + } + break; + case svn_wc_conflict_reason_added: + case svn_wc_conflict_reason_replaced: + { + /* ### show more details about copies or replacements? */ + *description = _("A directory scheduled to be added to the " + "repository in the next commit was found in " + "the working copy."); + } + break; + case svn_wc_conflict_reason_moved_away: + { + const char *moved_to_abspath; + svn_error_t *err; + + err = svn_wc__node_was_moved_away(&moved_to_abspath, NULL, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) + { + moved_to_abspath = NULL; + svn_error_clear(err); + } + else + return svn_error_trace(err); + } + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (moved_to_abspath == NULL) + { + /* The move no longer exists. */ + *description = _("The directory in the working copy " + "had been moved away at the time " + "this conflict was recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("The directory in the working copy " + "was moved away to\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_to_abspath), + scratch_pool)); + } + } + else if (operation == svn_wc_operation_merge) + { + if (moved_to_abspath == NULL) + { + /* The move probably happened in branch history. + * This case cannot happen until we detect incoming + * moves, which we currently don't do. */ + /* ### find deleted/moved revision? */ + *description = _("The directory had been moved away " + "at the time this conflict was " + "recorded."); + } + else + { + /* This is a local move in the working copy. */ + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("The directory was moved away to\n" + "'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_to_abspath), + scratch_pool)); + } + } + } + break; + case svn_wc_conflict_reason_moved_here: + { + const char *moved_from_abspath; + + SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (moved_from_abspath == NULL) + { + /* The move no longer exists. */ + *description = _("A directory had been moved here at " + "the time this conflict was " + "recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("A directory was moved here from\n" + "'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_from_abspath), + scratch_pool)); + } + } + else if (operation == svn_wc_operation_merge) + { + if (moved_from_abspath == NULL) + { + /* The move probably happened in branch history. + * This case cannot happen until we detect incoming + * moves, which we currently don't do. */ + /* ### find deleted/moved revision? */ + *description = _("A directory had been moved here at " + "the time this conflict was " + "recorded."); + } + else + { + /* This is a local move in the working copy. */ + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("A directory was moved here in " + "the working copy from\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_from_abspath), + scratch_pool)); + } + } + } + } + + return SVN_NO_ERROR; +} + +struct find_moves_baton +{ + /* Variables below are arguments provided by the caller of + * svn_ra_get_log2(). */ + const char *repos_root_url; + const char *repos_uuid; + svn_client_ctx_t *ctx; + const char *victim_abspath; /* for notifications */ + apr_pool_t *result_pool; + + /* A hash table mapping a revision number to an array of struct + * repos_move_info * elements, describing moves. + * + * Must be allocated in RESULT_POOL by the caller of svn_ra_get_log2(). + * + * If the node was moved, the DELETED_REV is present in this table, + * perhaps along with additional revisions. + * + * Given a sequence of moves which happened in the repository, such as: + * rA: mv x->z + * rA: mv a->b + * rB: mv b->c + * rC: mv c->d + * we map each revision number to all the moves which happened in the + * revision, which looks as follows: + * rA : [(x->z), (a->b)] + * rB : [(b->c)] + * rC : [(c->d)] + * This allows us to later find relevant moves based on a revision number. + * + * Additionally, we embed the number of the revision in which a move was + * found inside the repos_move_info structure: + * rA : [(rA, x->z), (rA, a->b)] + * rB : [(rB, b->c)] + * rC : [(rC, c->d)] + * And also, all moves pertaining to the same node are chained into a + * doubly-linked list via 'next' and 'prev' pointers (see definition of + * struct repos_move_info). This can be visualized as follows: + * rA : [(rA, x->z, prev=>NULL, next=>NULL), + * (rA, a->b, prev=>NULL, next=>(rB, b->c))] + * rB : [(rB, b->c), prev=>(rA, a->b), next=>(rC, c->d)] + * rC : [(rC, c->d), prev=>(rB, c->d), next=>NULL] + * This way, we can look up all moves relevant to a node, forwards and + * backwards in history, once we have located one move in the chain. + * + * In the above example, the data tells us that within the revision + * range rA:C, a was moved to d. However, within the revision range + * rA;B, a was moved to b. + */ + apr_hash_t *moves_table; + + /* Variables below hold state for find_moves() and are not + * intended to be used by the caller of svn_ra_get_log2(). + * Like all other variables, they must be initialized, however. */ + + /* Temporary map of moved paths to struct repos_move_info. + * Used to link multiple moves of the same node across revisions. */ + apr_hash_t *moved_paths; + + /* Extra RA session that can be used to make additional requests. */ + svn_ra_session_t *extra_ra_session; +}; + +/* Implements svn_log_entry_receiver_t. */ +static svn_error_t * +find_moves(void *baton, svn_log_entry_t *log_entry, apr_pool_t *scratch_pool) +{ + struct find_moves_baton *b = baton; + apr_hash_index_t *hi; + apr_pool_t *iterpool; + apr_array_header_t *deleted_paths; + apr_hash_t *copies; + apr_array_header_t *moves; + + if (b->ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + b->victim_abspath, + svn_wc_notify_tree_conflict_details_progress, + scratch_pool), + notify->revision = log_entry->revision; + b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); + } + + /* No paths were changed in this revision. Nothing to do. */ + if (! log_entry->changed_paths2) + return SVN_NO_ERROR; + + copies = apr_hash_make(scratch_pool); + deleted_paths = apr_array_make(scratch_pool, 0, sizeof(const char *)); + iterpool = svn_pool_create(scratch_pool); + for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *changed_path = apr_hash_this_key(hi); + svn_log_changed_path2_t *log_item = apr_hash_this_val(hi); + + svn_pool_clear(iterpool); + + /* ### Remove leading slash from paths in log entries. */ + if (changed_path[0] == '/') + changed_path++; + + /* For move detection, scan for copied nodes in this revision. */ + if (log_item->action == 'A' && log_item->copyfrom_path) + cache_copied_item(copies, changed_path, log_item); + + /* For move detection, store all deleted_paths. */ + if (log_item->action == 'D' || log_item->action == 'R') + APR_ARRAY_PUSH(deleted_paths, const char *) = + apr_pstrdup(scratch_pool, changed_path); + } + svn_pool_destroy(iterpool); + + /* Check for moves in this revision */ + SVN_ERR(find_moves_in_revision(b->extra_ra_session, + b->moves_table, b->moved_paths, + log_entry, copies, deleted_paths, + b->repos_root_url, b->repos_uuid, + b->ctx, b->result_pool, scratch_pool)); + + moves = apr_hash_get(b->moves_table, &log_entry->revision, + sizeof(svn_revnum_t)); + if (moves) + { + const svn_string_t *author; + + author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR); + SVN_ERR(find_nested_moves(moves, copies, deleted_paths, + b->moved_paths, log_entry->revision, + author ? author->data : _("unknown author"), + b->repos_root_url, + b->repos_uuid, + b->extra_ra_session, b->ctx, + b->result_pool, scratch_pool)); + } + + return SVN_NO_ERROR; +} + +/* Find all moves which occured in repository history starting at + * REPOS_RELPATH@START_REV until END_REV (where START_REV > END_REV). + * Return results in *MOVES_TABLE (see struct find_moves_baton for details). */ +static svn_error_t * +find_moves_in_revision_range(struct apr_hash_t **moves_table, + const char *repos_relpath, + const char *repos_root_url, + const char *repos_uuid, + const char *victim_abspath, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + apr_array_header_t *paths; + apr_array_header_t *revprops; + struct find_moves_baton b = { 0 }; + + SVN_ERR_ASSERT(start_rev > end_rev); + + url = svn_path_url_add_component2(repos_root_url, repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + + paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(paths, const char *) = ""; + + revprops = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; + + b.repos_root_url = repos_root_url; + b.repos_uuid = repos_uuid; + b.ctx = ctx; + b.victim_abspath = victim_abspath; + b.moves_table = apr_hash_make(result_pool); + b.moved_paths = apr_hash_make(scratch_pool); + b.result_pool = result_pool; + SVN_ERR(svn_ra__dup_session(&b.extra_ra_session, ra_session, NULL, + scratch_pool, scratch_pool)); + + SVN_ERR(svn_ra_get_log2(ra_session, paths, start_rev, end_rev, + 0, /* no limit */ + TRUE, /* need the changed paths list */ + FALSE, /* need to traverse copies */ + FALSE, /* no need for merged revisions */ + revprops, + find_moves, &b, + scratch_pool)); + + *moves_table = b.moves_table; + + return SVN_NO_ERROR; +} + +/* Return new move information for a moved-along child MOVED_ALONG_RELPATH. + * Set MOVE->NODE_KIND to MOVED_ALONG_NODE_KIND. + * Do not copy MOVE->NEXT and MOVE-PREV. + * If MOVED_ALONG_RELPATH is empty, this effectively copies MOVE to + * RESULT_POOL with NEXT and PREV pointers cleared. */ +static struct repos_move_info * +new_path_adjusted_move(struct repos_move_info *move, + const char *moved_along_relpath, + svn_node_kind_t moved_along_node_kind, + apr_pool_t *result_pool) +{ + struct repos_move_info *new_move; + + new_move = apr_pcalloc(result_pool, sizeof(*new_move)); + new_move->moved_from_repos_relpath = + svn_relpath_join(move->moved_from_repos_relpath, moved_along_relpath, + result_pool); + new_move->moved_to_repos_relpath = + svn_relpath_join(move->moved_to_repos_relpath, moved_along_relpath, + result_pool); + new_move->rev = move->rev; + new_move->rev_author = apr_pstrdup(result_pool, move->rev_author); + new_move->copyfrom_rev = move->copyfrom_rev; + new_move->node_kind = moved_along_node_kind; + /* Ignore prev and next pointers. Caller will set them if needed. */ + + return new_move; +} + +/* Given a list of MOVES_IN_REVISION, figure out which of these moves again + * move the node which was already moved by PREV_MOVE in the past . */ +static svn_error_t * +find_next_moves_in_revision(apr_array_header_t **next_moves, + apr_array_header_t *moves_in_revision, + struct repos_move_info *prev_move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + int i; + apr_pool_t *iterpool; + + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < moves_in_revision->nelts; i++) + { + struct repos_move_info *move; + const char *relpath; + const char *deleted_repos_relpath; + svn_boolean_t related; + svn_error_t *err; + + svn_pool_clear(iterpool); + + /* Check if this move affects the current known path of our node. */ + move = APR_ARRAY_IDX(moves_in_revision, i, struct repos_move_info *); + relpath = svn_relpath_skip_ancestor(move->moved_from_repos_relpath, + prev_move->moved_to_repos_relpath); + if (relpath == NULL) + continue; + + /* It does. So our node must have been deleted again. */ + deleted_repos_relpath = svn_relpath_join(move->moved_from_repos_relpath, + relpath, iterpool); + + /* Tracing back history of the delete-half of this move to the + * copyfrom-revision of the prior move we must end up at the + * delete-half of the prior move. */ + err = check_move_ancestry(&related, ra_session, repos_root_url, + deleted_repos_relpath, move->rev, + prev_move->moved_from_repos_relpath, + prev_move->copyfrom_rev, + FALSE, scratch_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + continue; + } + else + SVN_ERR(err); + + if (related) + { + struct repos_move_info *new_move; + + /* We have a winner. */ + new_move = new_path_adjusted_move(move, relpath, prev_move->node_kind, + result_pool); + if (*next_moves == NULL) + *next_moves = apr_array_make(result_pool, 1, + sizeof(struct repos_move_info *)); + APR_ARRAY_PUSH(*next_moves, struct repos_move_info *) = new_move; + } + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +static int +compare_items_as_revs(const svn_sort__item_t *a, const svn_sort__item_t *b) +{ + return svn_sort_compare_revisions(a->key, b->key); +} + +/* Starting at MOVE->REV, loop over future revisions which contain moves, + * and look for matching next moves in each. Once found, return a list of + * (ambiguous, if more than one) moves in *NEXT_MOVES. */ +static svn_error_t * +find_next_moves(apr_array_header_t **next_moves, + apr_hash_t *moves_table, + struct repos_move_info *move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *moves; + apr_array_header_t *revisions; + apr_pool_t *iterpool; + int i; + + *next_moves = NULL; + revisions = svn_sort__hash(moves_table, compare_items_as_revs, scratch_pool); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < revisions->nelts; i++) + { + svn_sort__item_t item = APR_ARRAY_IDX(revisions, i, svn_sort__item_t); + svn_revnum_t rev = *(svn_revnum_t *)item.key; + + svn_pool_clear(iterpool); + + if (rev <= move->rev) + continue; + + moves = apr_hash_get(moves_table, &rev, sizeof(rev)); + SVN_ERR(find_next_moves_in_revision(next_moves, moves, move, + ra_session, repos_root_url, + result_pool, iterpool)); + if (*next_moves) + break; + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Trace all future moves of the node moved by MOVE. + * Update MOVE->PREV and MOVE->NEXT accordingly. */ +static svn_error_t * +trace_moved_node(apr_hash_t *moves_table, + struct repos_move_info *move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *next_moves; + + SVN_ERR(find_next_moves(&next_moves, moves_table, move, + ra_session, repos_root_url, + result_pool, scratch_pool)); + if (next_moves) + { + int i; + apr_pool_t *iterpool; + + move->next = next_moves; + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < next_moves->nelts; i++) + { + struct repos_move_info *next_move; + + svn_pool_clear(iterpool); + next_move = APR_ARRAY_IDX(next_moves, i, struct repos_move_info *); + next_move->prev = move; + SVN_ERR(trace_moved_node(moves_table, next_move, + ra_session, repos_root_url, + result_pool, iterpool)); + } + svn_pool_destroy(iterpool); + } + + return SVN_NO_ERROR; +} + +/* Given a list of MOVES_IN_REVISION, figure out which of these moves + * move the node which was later on moved by NEXT_MOVE. */ +static svn_error_t * +find_prev_move_in_revision(struct repos_move_info **prev_move, + apr_array_header_t *moves_in_revision, + struct repos_move_info *next_move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + int i; + apr_pool_t *iterpool; + + *prev_move = NULL; + + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < moves_in_revision->nelts; i++) + { + struct repos_move_info *move; + const char *relpath; + const char *deleted_repos_relpath; + svn_boolean_t related; + svn_error_t *err; + + svn_pool_clear(iterpool); + + /* Check if this move affects the current known path of our node. */ + move = APR_ARRAY_IDX(moves_in_revision, i, struct repos_move_info *); + relpath = svn_relpath_skip_ancestor(next_move->moved_from_repos_relpath, + move->moved_to_repos_relpath); + if (relpath == NULL) + continue; + + /* It does. So our node must have been deleted. */ + deleted_repos_relpath = svn_relpath_join( + next_move->moved_from_repos_relpath, + relpath, iterpool); + + /* Tracing back history of the delete-half of the next move to the + * copyfrom-revision of the prior move we must end up at the + * delete-half of the prior move. */ + err = check_move_ancestry(&related, ra_session, repos_root_url, + deleted_repos_relpath, next_move->rev, + move->moved_from_repos_relpath, + move->copyfrom_rev, + FALSE, scratch_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + continue; + } + else + SVN_ERR(err); + + if (related) + { + /* We have a winner. */ + *prev_move = new_path_adjusted_move(move, relpath, + next_move->node_kind, + result_pool); + break; + } + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +static int +compare_items_as_revs_reverse(const svn_sort__item_t *a, + const svn_sort__item_t *b) +{ + int c = svn_sort_compare_revisions(a->key, b->key); + if (c < 0) + return 1; + if (c > 0) + return -1; + return c; +} + +/* Starting at MOVE->REV, loop over past revisions which contain moves, + * and look for a matching previous move in each. Once found, return + * it in *PREV_MOVE */ +static svn_error_t * +find_prev_move(struct repos_move_info **prev_move, + apr_hash_t *moves_table, + struct repos_move_info *move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *moves; + apr_array_header_t *revisions; + apr_pool_t *iterpool; + int i; + + *prev_move = NULL; + revisions = svn_sort__hash(moves_table, compare_items_as_revs_reverse, + scratch_pool); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < revisions->nelts; i++) + { + svn_sort__item_t item = APR_ARRAY_IDX(revisions, i, svn_sort__item_t); + svn_revnum_t rev = *(svn_revnum_t *)item.key; + + svn_pool_clear(iterpool); + + if (rev >= move->rev) + continue; + + moves = apr_hash_get(moves_table, &rev, sizeof(rev)); + SVN_ERR(find_prev_move_in_revision(prev_move, moves, move, + ra_session, repos_root_url, + result_pool, iterpool)); + if (*prev_move) + break; + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + + +/* Trace all past moves of the node moved by MOVE. + * Update MOVE->PREV and MOVE->NEXT accordingly. */ +static svn_error_t * +trace_moved_node_backwards(apr_hash_t *moves_table, + struct repos_move_info *move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct repos_move_info *prev_move; + + SVN_ERR(find_prev_move(&prev_move, moves_table, move, + ra_session, repos_root_url, + result_pool, scratch_pool)); + if (prev_move) + { + move->prev = prev_move; + prev_move->next = apr_array_make(result_pool, 1, + sizeof(struct repos_move_info *)); + APR_ARRAY_PUSH(prev_move->next, struct repos_move_info *) = move; + + SVN_ERR(trace_moved_node_backwards(moves_table, prev_move, + ra_session, repos_root_url, + result_pool, scratch_pool)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +reparent_session_and_fetch_node_kind(svn_node_kind_t *node_kind, + svn_ra_session_t *ra_session, + const char *url, + svn_revnum_t peg_rev, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + + err = svn_ra_reparent(ra_session, url, scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_RA_ILLEGAL_URL) + { + svn_error_clear(err); + *node_kind = svn_node_unknown; + return SVN_NO_ERROR; + } + + return svn_error_trace(err); + } + + SVN_ERR(svn_ra_check_path(ra_session, "", peg_rev, node_kind, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Scan MOVES_TABLE for moves which affect a particular deleted node, and + * build a set of new move information for this node. + * Return heads of all possible move chains in *MOVES. + * + * MOVES_TABLE describes moves which happened at arbitrary paths in the + * repository. DELETED_REPOS_RELPATH may have been moved directly or it + * may have been moved along with a parent path. Move information returned + * from this function represents how DELETED_REPOS_RELPATH itself was moved + * from one path to another, effectively "zooming in" on the effective move + * operations which occurred for this particular node. */ +static svn_error_t * +find_operative_moves(apr_array_header_t **moves, + apr_hash_t *moves_table, + const char *deleted_repos_relpath, + svn_revnum_t deleted_rev, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *moves_in_deleted_rev; + int i; + apr_pool_t *iterpool; + const char *session_url, *url = NULL; + + moves_in_deleted_rev = apr_hash_get(moves_table, &deleted_rev, + sizeof(deleted_rev)); + if (moves_in_deleted_rev == NULL) + { + *moves = NULL; + return SVN_NO_ERROR; + } + + SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, scratch_pool)); + + /* Look for operative moves in the revision where the node was deleted. */ + *moves = apr_array_make(scratch_pool, 0, sizeof(struct repos_move_info *)); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < moves_in_deleted_rev->nelts; i++) + { + struct repos_move_info *move; + const char *relpath; + + svn_pool_clear(iterpool); + + move = APR_ARRAY_IDX(moves_in_deleted_rev, i, struct repos_move_info *); + relpath = svn_relpath_skip_ancestor(move->moved_from_repos_relpath, + deleted_repos_relpath); + if (relpath && relpath[0] != '\0') + { + svn_node_kind_t node_kind; + + url = svn_path_url_add_component2(repos_root_url, + deleted_repos_relpath, + iterpool); + SVN_ERR(reparent_session_and_fetch_node_kind(&node_kind, + ra_session, url, + rev_below(deleted_rev), + iterpool)); + move = new_path_adjusted_move(move, relpath, node_kind, result_pool); + } + APR_ARRAY_PUSH(*moves, struct repos_move_info *) = move; + } + + if (url != NULL) + SVN_ERR(svn_ra_reparent(ra_session, session_url, scratch_pool)); + + /* If we didn't find any applicable moves, return NULL. */ + if ((*moves)->nelts == 0) + { + *moves = NULL; + svn_pool_destroy(iterpool); + return SVN_NO_ERROR; + } + + /* Figure out what happened to these moves in future revisions. */ + for (i = 0; i < (*moves)->nelts; i++) + { + struct repos_move_info *move; + + svn_pool_clear(iterpool); + + move = APR_ARRAY_IDX(*moves, i, struct repos_move_info *); + SVN_ERR(trace_moved_node(moves_table, move, ra_session, repos_root_url, + result_pool, iterpool)); + } + + svn_pool_destroy(iterpool); + return SVN_NO_ERROR; +} + +/* Try to find a revision older than START_REV, and its author, which deleted + * DELETED_BASENAME in the directory PARENT_REPOS_RELPATH. Assume the deleted + * node is ancestrally related to RELATED_REPOS_RELPATH@RELATED_PEG_REV. + * If no such revision can be found, set *DELETED_REV to SVN_INVALID_REVNUM + * and *DELETED_REV_AUTHOR to NULL. + * If the node was replaced rather than deleted, set *REPLACING_NODE_KIND to + * the node kind of the replacing node. Else, set it to svn_node_unknown. + * Only request the log for revisions up to END_REV from the server. + * If the deleted node was moved, provide heads of move chains in *MOVES. + * If the node was not moved,set *MOVES to NULL. + */ +static svn_error_t * +find_revision_for_suspected_deletion(svn_revnum_t *deleted_rev, + const char **deleted_rev_author, + svn_node_kind_t *replacing_node_kind, + struct apr_array_header_t **moves, + svn_client_conflict_t *conflict, + const char *deleted_basename, + const char *parent_repos_relpath, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + const char *related_repos_relpath, + svn_revnum_t related_peg_rev, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + apr_array_header_t *paths; + apr_array_header_t *revprops; + const char *repos_root_url; + const char *repos_uuid; + struct find_deleted_rev_baton b = { 0 }; + const char *victim_abspath; + svn_error_t *err; + apr_hash_t *moves_table; + + SVN_ERR_ASSERT(start_rev > end_rev); + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, &repos_uuid, + conflict, scratch_pool, + scratch_pool)); + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(find_moves_in_revision_range(&moves_table, parent_repos_relpath, + repos_root_url, repos_uuid, + victim_abspath, start_rev, end_rev, + ctx, result_pool, scratch_pool)); + + url = svn_path_url_add_component2(repos_root_url, parent_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + + paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(paths, const char *) = ""; + + revprops = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; + + b.victim_abspath = victim_abspath; + b.deleted_repos_relpath = svn_relpath_join(parent_repos_relpath, + deleted_basename, scratch_pool); + b.related_repos_relpath = related_repos_relpath; + b.related_peg_rev = related_peg_rev; + b.deleted_rev = SVN_INVALID_REVNUM; + b.replacing_node_kind = svn_node_unknown; + b.repos_root_url = repos_root_url; + b.repos_uuid = repos_uuid; + b.ctx = ctx; + b.moves_table = moves_table; + b.result_pool = result_pool; + SVN_ERR(svn_ra__dup_session(&b.extra_ra_session, ra_session, NULL, + scratch_pool, scratch_pool)); + + err = svn_ra_get_log2(ra_session, paths, start_rev, end_rev, + 0, /* no limit */ + TRUE, /* need the changed paths list */ + FALSE, /* need to traverse copies */ + FALSE, /* no need for merged revisions */ + revprops, + find_deleted_rev, &b, + scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_CEASE_INVOCATION && + b.deleted_rev != SVN_INVALID_REVNUM) + + { + /* Log operation was aborted because we found deleted rev. */ + svn_error_clear(err); + } + else + return svn_error_trace(err); + } + + if (b.deleted_rev == SVN_INVALID_REVNUM) + { + struct repos_move_info *move = b.move; + + if (move) + { + *deleted_rev = move->rev; + *deleted_rev_author = move->rev_author; + *replacing_node_kind = b.replacing_node_kind; + SVN_ERR(find_operative_moves(moves, moves_table, + b.deleted_repos_relpath, + move->rev, + ra_session, repos_root_url, + result_pool, scratch_pool)); + } + else + { + /* We could not determine the revision in which the node was + * deleted. */ + *deleted_rev = SVN_INVALID_REVNUM; + *deleted_rev_author = NULL; + *replacing_node_kind = svn_node_unknown; + *moves = NULL; + } + return SVN_NO_ERROR; + } + else + { + *deleted_rev = b.deleted_rev; + *deleted_rev_author = b.deleted_rev_author; + *replacing_node_kind = b.replacing_node_kind; + SVN_ERR(find_operative_moves(moves, moves_table, + b.deleted_repos_relpath, b.deleted_rev, + ra_session, repos_root_url, + result_pool, scratch_pool)); + } + + return SVN_NO_ERROR; +} + +/* Details for tree conflicts involving a locally missing node. */ +struct conflict_tree_local_missing_details +{ + /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. */ + svn_revnum_t deleted_rev; + + /* Author who committed DELETED_REV. */ + const char *deleted_rev_author; + + /* The path which was deleted relative to the repository root. */ + const char *deleted_repos_relpath; + + /* Move information about the conflict victim. If not NULL, this is an + * array of repos_move_info elements. Each element is the head of a + * move chain which starts in DELETED_REV. */ + apr_array_header_t *moves; + + /* Move information about siblings. Siblings are nodes which share + * a youngest common ancestor with the conflict victim. E.g. in case + * of a merge operation they are part of the merge source branch. + * If not NULL, this is an array of repos_move_info elements. + * Each element is the head of a move chain, which starts at some + * point in history after siblings and conflict victim forked off + * their common ancestor. */ + apr_array_header_t *sibling_moves; + + /* If not NULL, this is the move target abspath. */ + const char *moved_to_abspath; +}; + +static svn_error_t * +find_related_node(const char **related_repos_relpath, + svn_revnum_t *related_peg_rev, + const char *younger_related_repos_relpath, + svn_revnum_t younger_related_peg_rev, + const char *older_repos_relpath, + svn_revnum_t older_peg_rev, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *repos_root_url; + const char *related_url; + const char *corrected_url; + svn_node_kind_t related_node_kind; + svn_ra_session_t *ra_session; + + *related_repos_relpath = NULL; + *related_peg_rev = SVN_INVALID_REVNUM; + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, + scratch_pool, scratch_pool)); + related_url = svn_path_url_add_component2(repos_root_url, + younger_related_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + related_url, NULL, + NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + SVN_ERR(svn_ra_check_path(ra_session, "", younger_related_peg_rev, + &related_node_kind, scratch_pool)); + if (related_node_kind == svn_node_none) + { + svn_revnum_t related_deleted_rev; + const char *related_deleted_rev_author; + svn_node_kind_t related_replacing_node_kind; + const char *related_basename; + const char *related_parent_repos_relpath; + apr_array_header_t *related_moves; + + /* Looks like the younger node, which we'd like to use as our + * 'related node', was deleted. Try to find its deleted revision + * so we can calculate a peg revision at which it exists. + * The younger node is related to the older node, so we can use + * the older node to guide us in our search. */ + related_basename = svn_relpath_basename(younger_related_repos_relpath, + scratch_pool); + related_parent_repos_relpath = + svn_relpath_dirname(younger_related_repos_relpath, scratch_pool); + SVN_ERR(find_revision_for_suspected_deletion( + &related_deleted_rev, &related_deleted_rev_author, + &related_replacing_node_kind, &related_moves, + conflict, related_basename, + related_parent_repos_relpath, + younger_related_peg_rev, 0, + older_repos_relpath, older_peg_rev, + ctx, conflict->pool, scratch_pool)); + + /* If we can't find a related node, bail. */ + if (related_deleted_rev == SVN_INVALID_REVNUM) + return SVN_NO_ERROR; + + /* The node should exist in the revision before it was deleted. */ + *related_repos_relpath = younger_related_repos_relpath; + *related_peg_rev = rev_below(related_deleted_rev); + } + else + { + *related_repos_relpath = younger_related_repos_relpath; + *related_peg_rev = younger_related_peg_rev; + } + + return SVN_NO_ERROR; +} + +/* Determine if REPOS_RELPATH@PEG_REV was moved at some point in its history. + * History's range of interest ends at END_REV which must be older than PEG_REV. + * + * VICTIM_ABSPATH is the abspath of a conflict victim in the working copy and + * will be used in notifications. + * + * Return any applicable move chain heads in *MOVES. + * If no moves can be found, set *MOVES to NULL. */ +static svn_error_t * +find_moves_in_natural_history(apr_array_header_t **moves, + const char *repos_relpath, + svn_revnum_t peg_rev, + svn_node_kind_t node_kind, + svn_revnum_t end_rev, + const char *victim_abspath, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_hash_t *moves_table; + apr_array_header_t *revs; + apr_array_header_t *most_recent_moves = NULL; + int i; + apr_pool_t *iterpool; + + *moves = NULL; + + SVN_ERR(find_moves_in_revision_range(&moves_table, repos_relpath, + repos_root_url, repos_uuid, + victim_abspath, peg_rev, end_rev, + ctx, scratch_pool, scratch_pool)); + + iterpool = svn_pool_create(scratch_pool); + + /* Scan the moves table for applicable moves. */ + revs = svn_sort__hash(moves_table, compare_items_as_revs, scratch_pool); + for (i = revs->nelts - 1; i >= 0; i--) + { + svn_sort__item_t item = APR_ARRAY_IDX(revs, i, svn_sort__item_t); + apr_array_header_t *moves_in_rev = apr_hash_get(moves_table, item.key, + sizeof(svn_revnum_t)); + int j; + + svn_pool_clear(iterpool); + + /* Was repos relpath moved to its location in this revision? */ + for (j = 0; j < moves_in_rev->nelts; j++) + { + struct repos_move_info *move; + const char *relpath; + + move = APR_ARRAY_IDX(moves_in_rev, j, struct repos_move_info *); + relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, + repos_relpath); + if (relpath) + { + /* If the move did not happen in our peg revision, make + * sure this move happened on the same line of history. */ + if (move->rev != peg_rev) + { + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + + err = find_yca(&yca_loc, repos_relpath, peg_rev, + repos_relpath, move->rev, + repos_root_url, repos_uuid, + NULL, ctx, iterpool, iterpool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + if (yca_loc == NULL || yca_loc->rev != move->rev) + continue; + } + + if (most_recent_moves == NULL) + most_recent_moves = + apr_array_make(result_pool, 1, + sizeof(struct repos_move_info *)); + + /* Copy the move to result pool (even if relpath is ""). */ + move = new_path_adjusted_move(move, relpath, node_kind, + result_pool); + APR_ARRAY_PUSH(most_recent_moves, + struct repos_move_info *) = move; + } + } + + /* If we found one move, or several ambiguous moves, we're done. */ + if (most_recent_moves) + break; + } + + if (most_recent_moves && most_recent_moves->nelts > 0) + { + *moves = apr_array_make(result_pool, 1, + sizeof(struct repos_move_info *)); + + /* Figure out what happened to the most recent moves in prior + * revisions and build move chains. */ + for (i = 0; i < most_recent_moves->nelts; i++) + { + struct repos_move_info *move; + + svn_pool_clear(iterpool); + + move = APR_ARRAY_IDX(most_recent_moves, i, struct repos_move_info *); + SVN_ERR(trace_moved_node_backwards(moves_table, move, + ra_session, repos_root_url, + result_pool, iterpool)); + /* Follow the move chain backwards. */ + while (move->prev) + move = move->prev; + + /* Return move heads. */ + APR_ARRAY_PUSH(*moves, struct repos_move_info *) = move; + } + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_details_func_t. */ +static svn_error_t * +conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *old_repos_relpath; + const char *new_repos_relpath; + const char *parent_repos_relpath; + svn_revnum_t parent_peg_rev; + svn_revnum_t old_rev; + svn_revnum_t new_rev; + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + const char *deleted_basename; + struct conflict_tree_local_missing_details *details; + apr_array_header_t *moves = NULL; + apr_array_header_t *sibling_moves = NULL; + const char *related_repos_relpath; + svn_revnum_t related_peg_rev; + const char *repos_root_url; + const char *repos_uuid; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, NULL, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, NULL, conflict, + scratch_pool, scratch_pool)); + + /* Scan the conflict victim's parent's log to find a revision which + * deleted the node. */ + deleted_basename = svn_dirent_basename(conflict->local_abspath, + scratch_pool); + SVN_ERR(svn_wc__node_get_repos_info(&parent_peg_rev, &parent_repos_relpath, + &repos_root_url, &repos_uuid, + ctx->wc_ctx, + svn_dirent_dirname( + conflict->local_abspath, + scratch_pool), + scratch_pool, + scratch_pool)); + + /* Pick the younger incoming node as our 'related node' which helps + * pin-pointing the deleted conflict victim in history. */ + related_repos_relpath = + (old_rev < new_rev ? new_repos_relpath : old_repos_relpath); + related_peg_rev = (old_rev < new_rev ? new_rev : old_rev); + + /* Make sure we're going to search the related node in a revision where + * it exists. The younger incoming node might have been deleted in HEAD. */ + if (related_repos_relpath != NULL && related_peg_rev != SVN_INVALID_REVNUM) + SVN_ERR(find_related_node( + &related_repos_relpath, &related_peg_rev, + related_repos_relpath, related_peg_rev, + (old_rev < new_rev ? old_repos_relpath : new_repos_relpath), + (old_rev < new_rev ? old_rev : new_rev), + conflict, ctx, scratch_pool, scratch_pool)); + + SVN_ERR(find_revision_for_suspected_deletion( + &deleted_rev, &deleted_rev_author, &replacing_node_kind, &moves, + conflict, deleted_basename, parent_repos_relpath, + parent_peg_rev, 0, related_repos_relpath, related_peg_rev, + ctx, conflict->pool, scratch_pool)); + + /* If the victim was not deleted then check if the related path was moved. */ + if (deleted_rev == SVN_INVALID_REVNUM) + { + const char *victim_abspath; + svn_ra_session_t *ra_session; + const char *url, *corrected_url; + svn_client__pathrev_t *yca_loc; + svn_revnum_t end_rev; + svn_node_kind_t related_node_kind; + + /* ### The following describes all moves in terms of forward-merges, + * should do we something else for reverse-merges? */ + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + url = svn_path_url_add_component2(repos_root_url, related_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + /* Set END_REV to our best guess of the nearest YCA revision. */ + SVN_ERR(find_nearest_yca(&yca_loc, related_repos_relpath, related_peg_rev, + parent_repos_relpath, parent_peg_rev, + repos_root_url, repos_uuid, ra_session, ctx, + scratch_pool, scratch_pool)); + if (yca_loc == NULL) + return SVN_NO_ERROR; + end_rev = yca_loc->rev; + + /* END_REV must be smaller than RELATED_PEG_REV, else the call + to find_moves_in_natural_history() below will error out. */ + if (end_rev >= related_peg_rev) + end_rev = related_peg_rev > 0 ? related_peg_rev - 1 : 0; + + SVN_ERR(svn_ra_check_path(ra_session, "", related_peg_rev, + &related_node_kind, scratch_pool)); + SVN_ERR(find_moves_in_natural_history(&sibling_moves, + related_repos_relpath, + related_peg_rev, + related_node_kind, + end_rev, + victim_abspath, + repos_root_url, repos_uuid, + ra_session, ctx, + conflict->pool, scratch_pool)); + + if (sibling_moves == NULL) + return SVN_NO_ERROR; + + /* ## TODO: Find the missing node in the WC. */ + } + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + details->deleted_rev = deleted_rev; + details->deleted_rev_author = deleted_rev_author; + if (deleted_rev != SVN_INVALID_REVNUM) + details->deleted_repos_relpath = svn_relpath_join(parent_repos_relpath, + deleted_basename, + conflict->pool); + details->moves = moves; + details->sibling_moves = sibling_moves; + + conflict->tree_conflict_local_details = details; + + return SVN_NO_ERROR; +} + +/* Return a localised string representation of the local part of a tree + conflict on a non-existent node. */ +static svn_error_t * +describe_local_none_node_change(const char **description, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_reason_t local_change; + svn_wc_operation_t operation; + + local_change = svn_client_conflict_get_local_change(conflict); + operation = svn_client_conflict_get_operation(conflict); + + switch (local_change) + { + case svn_wc_conflict_reason_edited: + *description = _("An item containing uncommitted changes was " + "found in the working copy."); + break; + case svn_wc_conflict_reason_obstructed: + *description = _("An item which already occupies this path was found in " + "the working copy."); + break; + case svn_wc_conflict_reason_deleted: + *description = _("A deleted item was found in the working copy."); + break; + case svn_wc_conflict_reason_missing: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("No such file or directory was found in the " + "working copy."); + else if (operation == svn_wc_operation_merge) + { + /* ### display deleted revision */ + *description = _("No such file or directory was found in the " + "merge target working copy.\nThe item may " + "have been deleted or moved away in the " + "repository's history."); + } + break; + case svn_wc_conflict_reason_unversioned: + *description = _("An unversioned item was found in the working " + "copy."); + break; + case svn_wc_conflict_reason_added: + case svn_wc_conflict_reason_replaced: + *description = _("An item scheduled to be added to the repository " + "in the next commit was found in the working " + "copy."); + break; + case svn_wc_conflict_reason_moved_away: + *description = _("The item in the working copy had been moved " + "away at the time this conflict was recorded."); + break; + case svn_wc_conflict_reason_moved_here: + *description = _("An item had been moved here in the working copy " + "at the time this conflict was recorded."); + break; + } + + return SVN_NO_ERROR; +} + +/* Append a description of a move chain beginning at NEXT to DESCRIPTION. */ +static const char * +append_moved_to_chain_description(const char *description, + apr_array_header_t *next, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (next == NULL) + return description; + + while (next) + { + struct repos_move_info *move; + + /* Describe the first possible move chain only. Adding multiple chains + * to the description would just be confusing. The user may select a + * different move destination while resolving the conflict. */ + move = APR_ARRAY_IDX(next, 0, struct repos_move_info *); + + description = apr_psprintf(scratch_pool, + _("%s\nAnd then moved away to '^/%s' by " + "%s in r%ld."), + description, move->moved_to_repos_relpath, + move->rev_author, move->rev); + next = move->next; + } + + return apr_pstrdup(result_pool, description); +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_local_description_generic(const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_node_kind_t victim_node_kind; + + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + + *description = NULL; + + switch (victim_node_kind) + { + case svn_node_file: + case svn_node_symlink: + SVN_ERR(describe_local_file_node_change(description, conflict, ctx, + result_pool, scratch_pool)); + break; + case svn_node_dir: + SVN_ERR(describe_local_dir_node_change(description, conflict, ctx, + result_pool, scratch_pool)); + break; + case svn_node_none: + case svn_node_unknown: + SVN_ERR(describe_local_none_node_change(description, conflict, + result_pool, scratch_pool)); + break; + } + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_description_local_missing(const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct conflict_tree_local_missing_details *details; + + details = conflict->tree_conflict_local_details; + if (details == NULL) + return svn_error_trace(conflict_tree_get_local_description_generic( + description, conflict, ctx, + result_pool, scratch_pool)); + + if (details->moves || details->sibling_moves) + { + struct repos_move_info *move; + + *description = _("No such file or directory was found in the " + "merge target working copy.\n"); + + if (details->moves) + { + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + if (move->node_kind == svn_node_file) + *description = apr_psprintf( + result_pool, + _("%sThe file was moved to '^/%s' in r%ld by %s."), + *description, move->moved_to_repos_relpath, + move->rev, move->rev_author); + else if (move->node_kind == svn_node_dir) + *description = apr_psprintf( + result_pool, + _("%sThe directory was moved to '^/%s' in " + "r%ld by %s."), + *description, move->moved_to_repos_relpath, + move->rev, move->rev_author); + else + *description = apr_psprintf( + result_pool, + _("%sThe item was moved to '^/%s' in r%ld by %s."), + *description, move->moved_to_repos_relpath, + move->rev, move->rev_author); + *description = append_moved_to_chain_description(*description, + move->next, + result_pool, + scratch_pool); + } + + if (details->sibling_moves) + { + move = APR_ARRAY_IDX(details->sibling_moves, 0, + struct repos_move_info *); + if (move->node_kind == svn_node_file) + *description = apr_psprintf( + result_pool, + _("%sThe file '^/%s' was moved to '^/%s' " + "in r%ld by %s."), + *description, move->moved_from_repos_relpath, + move->moved_to_repos_relpath, + move->rev, move->rev_author); + else if (move->node_kind == svn_node_dir) + *description = apr_psprintf( + result_pool, + _("%sThe directory '^/%s' was moved to '^/%s' " + "in r%ld by %s."), + *description, move->moved_from_repos_relpath, + move->moved_to_repos_relpath, + move->rev, move->rev_author); + else + *description = apr_psprintf( + result_pool, + _("%sThe item '^/%s' was moved to '^/%s' " + "in r%ld by %s."), + *description, move->moved_from_repos_relpath, + move->moved_to_repos_relpath, + move->rev, move->rev_author); + *description = append_moved_to_chain_description(*description, + move->next, + result_pool, + scratch_pool); + } + } + else + *description = apr_psprintf( + result_pool, + _("No such file or directory was found in the " + "merge target working copy.\n'^/%s' was deleted " + "in r%ld by %s."), + details->deleted_repos_relpath, + details->deleted_rev, details->deleted_rev_author); + + return SVN_NO_ERROR; +} + +/* Return a localised string representation of the incoming part of a + conflict; NULL for non-localised odd cases. */ +static const char * +describe_incoming_change(svn_node_kind_t kind, svn_wc_conflict_action_t action, + svn_wc_operation_t operation) +{ + switch (kind) + { + case svn_node_file: + case svn_node_symlink: + if (operation == svn_wc_operation_update) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("An update operation tried to edit a file."); + case svn_wc_conflict_action_add: + return _("An update operation tried to add a file."); + case svn_wc_conflict_action_delete: + return _("An update operation tried to delete or move " + "a file."); + case svn_wc_conflict_action_replace: + return _("An update operation tried to replace a file."); + } + } + else if (operation == svn_wc_operation_switch) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A switch operation tried to edit a file."); + case svn_wc_conflict_action_add: + return _("A switch operation tried to add a file."); + case svn_wc_conflict_action_delete: + return _("A switch operation tried to delete or move " + "a file."); + case svn_wc_conflict_action_replace: + return _("A switch operation tried to replace a file."); + } + } + else if (operation == svn_wc_operation_merge) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A merge operation tried to edit a file."); + case svn_wc_conflict_action_add: + return _("A merge operation tried to add a file."); + case svn_wc_conflict_action_delete: + return _("A merge operation tried to delete or move " + "a file."); + case svn_wc_conflict_action_replace: + return _("A merge operation tried to replace a file."); + } + } + break; + case svn_node_dir: + if (operation == svn_wc_operation_update) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("An update operation tried to change a directory."); + case svn_wc_conflict_action_add: + return _("An update operation tried to add a directory."); + case svn_wc_conflict_action_delete: + return _("An update operation tried to delete or move " + "a directory."); + case svn_wc_conflict_action_replace: + return _("An update operation tried to replace a directory."); + } + } + else if (operation == svn_wc_operation_switch) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A switch operation tried to edit a directory."); + case svn_wc_conflict_action_add: + return _("A switch operation tried to add a directory."); + case svn_wc_conflict_action_delete: + return _("A switch operation tried to delete or move " + "a directory."); + case svn_wc_conflict_action_replace: + return _("A switch operation tried to replace a directory."); + } + } + else if (operation == svn_wc_operation_merge) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A merge operation tried to edit a directory."); + case svn_wc_conflict_action_add: + return _("A merge operation tried to add a directory."); + case svn_wc_conflict_action_delete: + return _("A merge operation tried to delete or move " + "a directory."); + case svn_wc_conflict_action_replace: + return _("A merge operation tried to replace a directory."); + } + } + break; + case svn_node_none: + case svn_node_unknown: + if (operation == svn_wc_operation_update) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("An update operation tried to edit an item."); + case svn_wc_conflict_action_add: + return _("An update operation tried to add an item."); + case svn_wc_conflict_action_delete: + return _("An update operation tried to delete or move " + "an item."); + case svn_wc_conflict_action_replace: + return _("An update operation tried to replace an item."); + } + } + else if (operation == svn_wc_operation_switch) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A switch operation tried to edit an item."); + case svn_wc_conflict_action_add: + return _("A switch operation tried to add an item."); + case svn_wc_conflict_action_delete: + return _("A switch operation tried to delete or move " + "an item."); + case svn_wc_conflict_action_replace: + return _("A switch operation tried to replace an item."); + } + } + else if (operation == svn_wc_operation_merge) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A merge operation tried to edit an item."); + case svn_wc_conflict_action_add: + return _("A merge operation tried to add an item."); + case svn_wc_conflict_action_delete: + return _("A merge operation tried to delete or move " + "an item."); + case svn_wc_conflict_action_replace: + return _("A merge operation tried to replace an item."); + } + } + break; + } + + return NULL; +} + +/* Return a localised string representation of the operation part of a + conflict. */ +static const char * +operation_str(svn_wc_operation_t operation) +{ + switch (operation) + { + case svn_wc_operation_update: return _("upon update"); + case svn_wc_operation_switch: return _("upon switch"); + case svn_wc_operation_merge: return _("upon merge"); + case svn_wc_operation_none: return _("upon none"); + } + SVN_ERR_MALFUNCTION_NO_RETURN(); + return NULL; +} + +svn_error_t * +svn_client_conflict_prop_get_description(const char **description, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *reason_str, *action_str; + + /* We provide separately translatable strings for the values that we + * know about, and a fall-back in case any other values occur. */ + switch (svn_client_conflict_get_local_change(conflict)) + { + case svn_wc_conflict_reason_edited: + reason_str = _("local edit"); + break; + case svn_wc_conflict_reason_added: + reason_str = _("local add"); + break; + case svn_wc_conflict_reason_deleted: + reason_str = _("local delete"); + break; + case svn_wc_conflict_reason_obstructed: + reason_str = _("local obstruction"); + break; + default: + reason_str = apr_psprintf( + scratch_pool, _("local %s"), + svn_token__to_word( + map_conflict_reason, + svn_client_conflict_get_local_change(conflict))); + break; + } + switch (svn_client_conflict_get_incoming_change(conflict)) + { + case svn_wc_conflict_action_edit: + action_str = _("incoming edit"); + break; + case svn_wc_conflict_action_add: + action_str = _("incoming add"); + break; + case svn_wc_conflict_action_delete: + action_str = _("incoming delete"); + break; + default: + action_str = apr_psprintf( + scratch_pool, _("incoming %s"), + svn_token__to_word( + map_conflict_action, + svn_client_conflict_get_incoming_change(conflict))); + break; + } + SVN_ERR_ASSERT(reason_str && action_str); + + *description = apr_psprintf(result_pool, _("%s, %s %s"), + reason_str, action_str, + operation_str( + svn_client_conflict_get_operation(conflict))); + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_incoming_description_generic( + const char **incoming_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *action; + svn_node_kind_t incoming_kind; + svn_wc_conflict_action_t conflict_action; + svn_wc_operation_t conflict_operation; + + conflict_action = svn_client_conflict_get_incoming_change(conflict); + conflict_operation = svn_client_conflict_get_operation(conflict); + + /* Determine the node kind of the incoming change. */ + incoming_kind = svn_node_unknown; + if (conflict_action == svn_wc_conflict_action_edit || + conflict_action == svn_wc_conflict_action_delete) + { + /* Change is acting on 'src_left' version of the node. */ + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + NULL, NULL, &incoming_kind, conflict, scratch_pool, + scratch_pool)); + } + else if (conflict_action == svn_wc_conflict_action_add || + conflict_action == svn_wc_conflict_action_replace) + { + /* Change is acting on 'src_right' version of the node. + * + * ### For 'replace', the node kind is ambiguous. However, src_left + * ### is NULL for replace, so we must use src_right. */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + NULL, NULL, &incoming_kind, conflict, scratch_pool, + scratch_pool)); + } + + action = describe_incoming_change(incoming_kind, conflict_action, + conflict_operation); + if (action) + { + *incoming_change_description = apr_pstrdup(result_pool, action); + } + else + { + /* A catch-all message for very rare or nominally impossible cases. + It will not be pretty, but is closer to an internal error than + an ordinary user-facing string. */ + *incoming_change_description = apr_psprintf(result_pool, + _("incoming %s %s"), + svn_node_kind_to_word(incoming_kind), + svn_token__to_word(map_conflict_action, + conflict_action)); + } + return SVN_NO_ERROR; +} + +/* Details for tree conflicts involving incoming deletions and replacements. */ +struct conflict_tree_incoming_delete_details +{ + /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. */ + svn_revnum_t deleted_rev; + + /* If not SVN_INVALID_REVNUM, the node was added in ADDED_REV. The incoming + * delete is the result of a reverse application of this addition. */ + svn_revnum_t added_rev; + + /* The path which was deleted/added relative to the repository root. */ + const char *repos_relpath; + + /* Author who committed DELETED_REV/ADDED_REV. */ + const char *rev_author; + + /* New node kind for a replaced node. This is svn_node_none for deletions. */ + svn_node_kind_t replacing_node_kind; + + /* Move information. If not NULL, this is an array of repos_move_info * + * elements. Each element is the head of a move chain which starts in + * DELETED_REV or in ADDED_REV (in which case moves should be interpreted + * in reverse). */ + apr_array_header_t *moves; + + /* A map of repos_relpaths and working copy nodes for an incoming move. + * + * Each key is a "const char *" repository relpath corresponding to a + * possible repository-side move destination node in the revision which + * is the target revision in case of update and switch, or the merge-right + * revision in case of a merge. + * + * Each value is an apr_array_header_t *. + * Each array consists of "const char *" absolute paths to working copy + * nodes which correspond to the repository node selected by the map key. + * Each such working copy node is a potential local move target which can + * be chosen to "follow" the incoming move when resolving a tree conflict. + * + * This may be an empty hash map in case if there is no move target path + * in the working copy. */ + apr_hash_t *wc_move_targets; + + /* The preferred move target repository relpath. This is our key into + * the WC_MOVE_TARGETS map above (can be overridden by the user). */ + const char *move_target_repos_relpath; + + /* The current index into the list of working copy nodes corresponding to + * MOVE_TARGET_REPOS_REPLATH (can be overridden by the user). */ + int wc_move_target_idx; +}; + +/* Get the currently selected repository-side move target path. + * If none was selected yet, determine and return a default one. */ +static const char * +get_moved_to_repos_relpath( + struct conflict_tree_incoming_delete_details *details, + apr_pool_t *scratch_pool) +{ + struct repos_move_info *move; + + if (details->move_target_repos_relpath) + return details->move_target_repos_relpath; + + if (details->wc_move_targets && apr_hash_count(details->wc_move_targets) > 0) + { + svn_sort__item_t item; + apr_array_header_t *repos_relpaths; + + repos_relpaths = svn_sort__hash(details->wc_move_targets, + svn_sort_compare_items_as_paths, + scratch_pool); + item = APR_ARRAY_IDX(repos_relpaths, 0, svn_sort__item_t); + return (const char *)item.key; + } + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + return move->moved_to_repos_relpath; +} + +static const char * +describe_incoming_deletion_upon_update( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + svn_revnum_t old_rev, + svn_revnum_t new_rev, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory updated from r%ld to r%ld was " + "replaced with a file by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File updated from r%ld to r%ld was replaced " + "with a file from another line of history by " + "%s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item updated from r%ld to r%ld was replaced " + "with a file by %s in r%ld."), old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory updated from r%ld to r%ld was " + "replaced with a directory from another line " + "of history by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File updated from r%ld to r%ld was " + "replaced with a directory by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item updated from r%ld to r%ld was replaced " + "by %s in r%ld."), old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else + { + if (victim_node_kind == svn_node_dir) + { + if (details->moves) + { + const char *description; + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Directory updated from r%ld to r%ld was " + "moved to '^/%s' by %s in r%ld."), + old_rev, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Directory updated from r%ld to r%ld was " + "deleted by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("File updated from r%ld to r%ld was moved " + "to '^/%s' by %s in r%ld."), old_rev, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("File updated from r%ld to r%ld was " + "deleted by %s in r%ld."), old_rev, new_rev, + details->rev_author, details->deleted_rev); + } + else + { + if (details->moves) + { + const char *description; + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Item updated from r%ld to r%ld was moved " + "to '^/%s' by %s in r%ld."), old_rev, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Item updated from r%ld to r%ld was " + "deleted by %s in r%ld."), old_rev, new_rev, + details->rev_author, details->deleted_rev); + } + } +} + +static const char * +describe_incoming_reverse_addition_upon_update( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + svn_revnum_t old_rev, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory updated backwards from r%ld to r%ld " + "was a file before the replacement made by %s " + "in r%ld."), old_rev, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File updated backwards from r%ld to r%ld was a " + "file from another line of history before the " + "replacement made by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item updated backwards from r%ld to r%ld was " + "replaced with a file by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory updated backwards from r%ld to r%ld " + "was a directory from another line of history " + "before the replacement made by %s in " + "r%ld."), old_rev, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File updated backwards from r%ld to r%ld was a " + "directory before the replacement made by %s " + "in r%ld."), old_rev, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item updated backwards from r%ld to r%ld was " + "replaced with a directory by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + } + else + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory updated backwards from r%ld to r%ld " + "did not exist before it was added by %s in " + "r%ld."), old_rev, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File updated backwards from r%ld to r%ld did " + "not exist before it was added by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item updated backwards from r%ld to r%ld did " + "not exist before it was added by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + } +} + +static const char * +describe_incoming_deletion_upon_switch( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved " + "to '^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file from another line of " + "history by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory from another " + "line of history by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else + { + if (victim_node_kind == svn_node_dir) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + } +} + +static const char * +describe_incoming_reverse_addition_upon_switch( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was a file before the replacement made by %s " + "in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas a " + "file from another line of history before the " + "replacement made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was a directory from another line of history " + "before the replacement made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was a file before the replacement made by %s " + "in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } + else + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "did not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\ndid " + "not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\ndid " + "not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } +} + +static const char * +describe_incoming_deletion_upon_merge( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file from another line of " + "history by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + return apr_psprintf(result_pool, + _("Item merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory from another " + "line of history by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else + { + if (victim_node_kind == svn_node_dir) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Directory merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Directory merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("File merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("File merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Item merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Item merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + } +} + +static const char * +describe_incoming_reverse_addition_upon_merge( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory reverse-merged from\n'^/%s@%ld'\nto " + "^/%s@%ld was a file before the replacement " + "made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was a file from another line of history before " + "the replacement made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory reverse-merged from\n'^/%s@%ld'\nto " + "^/%s@%ld was a directory from another line " + "of history before the replacement made by %s " + "in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Directory reverse-merged from\n'^/%s@%ld'\nto " + "^/%s@%ld was a file before the replacement " + "made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } + else + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory reverse-merged from\n'^/%s@%ld'\nto " + "^/%s@%ld did not exist before it was added " + "by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "did not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "did not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_description_incoming_delete( + const char **incoming_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *action; + svn_node_kind_t victim_node_kind; + svn_wc_operation_t conflict_operation; + const char *old_repos_relpath; + svn_revnum_t old_rev; + const char *new_repos_relpath; + svn_revnum_t new_rev; + struct conflict_tree_incoming_delete_details *details; + + if (conflict->tree_conflict_incoming_details == NULL) + return svn_error_trace(conflict_tree_get_incoming_description_generic( + incoming_change_description, + conflict, ctx, result_pool, scratch_pool)); + + conflict_operation = svn_client_conflict_get_operation(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, NULL, conflict, scratch_pool, + scratch_pool)); + + details = conflict->tree_conflict_incoming_details; + + if (conflict_operation == svn_wc_operation_update) + { + if (details->deleted_rev != SVN_INVALID_REVNUM) + { + action = describe_incoming_deletion_upon_update(details, + victim_node_kind, + old_rev, + new_rev, + result_pool, + scratch_pool); + } + else /* details->added_rev != SVN_INVALID_REVNUM */ + { + /* This deletion is really the reverse change of an addition. */ + action = describe_incoming_reverse_addition_upon_update( + details, victim_node_kind, old_rev, new_rev, result_pool); + } + } + else if (conflict_operation == svn_wc_operation_switch) + { + if (details->deleted_rev != SVN_INVALID_REVNUM) + { + action = describe_incoming_deletion_upon_switch(details, + victim_node_kind, + old_repos_relpath, + old_rev, + new_repos_relpath, + new_rev, + result_pool, + scratch_pool); + } + else /* details->added_rev != SVN_INVALID_REVNUM */ + { + /* This deletion is really the reverse change of an addition. */ + action = describe_incoming_reverse_addition_upon_switch( + details, victim_node_kind, old_repos_relpath, old_rev, + new_repos_relpath, new_rev, result_pool); + + } + } + else if (conflict_operation == svn_wc_operation_merge) + { + if (details->deleted_rev != SVN_INVALID_REVNUM) + { + action = describe_incoming_deletion_upon_merge(details, + victim_node_kind, + old_repos_relpath, + old_rev, + new_repos_relpath, + new_rev, + result_pool, + scratch_pool); + } + else /* details->added_rev != SVN_INVALID_REVNUM */ + { + /* This deletion is really the reverse change of an addition. */ + action = describe_incoming_reverse_addition_upon_merge( + details, victim_node_kind, old_repos_relpath, old_rev, + new_repos_relpath, new_rev, result_pool); + } + } + + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; +} + +/* Baton for find_added_rev(). */ +struct find_added_rev_baton +{ + const char *victim_abspath; + svn_client_ctx_t *ctx; + svn_revnum_t added_rev; + const char *repos_relpath; + const char *parent_repos_relpath; + apr_pool_t *pool; +}; + +/* Implements svn_location_segment_receiver_t. + * Finds the revision in which a node was added by tracing 'start' + * revisions in location segments reported for the node. + * If the PARENT_REPOS_RELPATH in the baton is not NULL, only consider + * segments in which the node existed somwhere beneath this path. */ +static svn_error_t * +find_added_rev(svn_location_segment_t *segment, + void *baton, + apr_pool_t *scratch_pool) +{ + struct find_added_rev_baton *b = baton; + + if (b->ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + b->victim_abspath, + svn_wc_notify_tree_conflict_details_progress, + scratch_pool), + notify->revision = segment->range_start; + b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); + } + + if (segment->path) /* not interested in gaps */ + { + if (b->parent_repos_relpath == NULL || + svn_relpath_skip_ancestor(b->parent_repos_relpath, + segment->path) != NULL) + { + b->added_rev = segment->range_start; + b->repos_relpath = apr_pstrdup(b->pool, segment->path); + } + } + + return SVN_NO_ERROR; +} + +/* Find conflict details in the case where a revision which added a node was + * applied in reverse, resulting in an incoming deletion. */ +static svn_error_t * +get_incoming_delete_details_for_reverse_addition( + struct conflict_tree_incoming_delete_details **details, + const char *repos_root_url, + const char *old_repos_relpath, + svn_revnum_t old_rev, + svn_revnum_t new_rev, + svn_client_ctx_t *ctx, + const char *victim_abspath, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + svn_string_t *author_revprop; + struct find_added_rev_baton b = { 0 }; + + url = svn_path_url_add_component2(repos_root_url, old_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + *details = apr_pcalloc(result_pool, sizeof(**details)); + b.ctx = ctx; + b.victim_abspath = victim_abspath; + b.added_rev = SVN_INVALID_REVNUM; + b.repos_relpath = NULL; + b.parent_repos_relpath = NULL; + b.pool = scratch_pool; + + /* Figure out when this node was added. */ + SVN_ERR(svn_ra_get_location_segments(ra_session, "", old_rev, + old_rev, new_rev, + find_added_rev, &b, + scratch_pool)); + + SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev, + SVN_PROP_REVISION_AUTHOR, + &author_revprop, scratch_pool)); + (*details)->deleted_rev = SVN_INVALID_REVNUM; + (*details)->added_rev = b.added_rev; + (*details)->repos_relpath = apr_pstrdup(result_pool, b.repos_relpath); + if (author_revprop) + (*details)->rev_author = apr_pstrdup(result_pool, author_revprop->data); + else + (*details)->rev_author = _("unknown author"); + + /* Check for replacement. */ + (*details)->replacing_node_kind = svn_node_none; + if ((*details)->added_rev > 0) + { + svn_node_kind_t replaced_node_kind; + + SVN_ERR(svn_ra_check_path(ra_session, "", + rev_below((*details)->added_rev), + &replaced_node_kind, scratch_pool)); + if (replaced_node_kind != svn_node_none) + SVN_ERR(svn_ra_check_path(ra_session, "", (*details)->added_rev, + &(*details)->replacing_node_kind, + scratch_pool)); + } + + return SVN_NO_ERROR; +} + +/* Follow each move chain starting a MOVE all the way to the end to find + * the possible working copy locations for VICTIM_ABSPATH which corresponds + * to VICTIM_REPOS_REPLATH@VICTIM_REVISION. + * Add each such location to the WC_MOVE_TARGETS hash table, keyed on the + * repos_relpath which is the corresponding move destination in the repository. + * This function is recursive. */ +static svn_error_t * +follow_move_chains(apr_hash_t *wc_move_targets, + struct repos_move_info *move, + svn_client_ctx_t *ctx, + const char *victim_abspath, + svn_node_kind_t victim_node_kind, + const char *victim_repos_relpath, + svn_revnum_t victim_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + /* If this is the end of a move chain, look for matching paths in + * the working copy and add them to our collection if found. */ + if (move->next == NULL) + { + apr_array_header_t *candidate_abspaths; + + /* Gather candidate nodes which represent this moved_to_repos_relpath. */ + SVN_ERR(svn_wc__guess_incoming_move_target_nodes( + &candidate_abspaths, ctx->wc_ctx, + victim_abspath, victim_node_kind, + move->moved_to_repos_relpath, + scratch_pool, scratch_pool)); + if (candidate_abspaths->nelts > 0) + { + apr_array_header_t *moved_to_abspaths; + int i; + apr_pool_t *iterpool = svn_pool_create(scratch_pool); + + moved_to_abspaths = apr_array_make(result_pool, 1, + sizeof (const char *)); + + for (i = 0; i < candidate_abspaths->nelts; i++) + { + const char *candidate_abspath; + const char *repos_root_url; + const char *repos_uuid; + const char *candidate_repos_relpath; + svn_revnum_t candidate_revision; + + svn_pool_clear(iterpool); + + candidate_abspath = APR_ARRAY_IDX(candidate_abspaths, i, + const char *); + SVN_ERR(svn_wc__node_get_origin(NULL, &candidate_revision, + &candidate_repos_relpath, + &repos_root_url, + &repos_uuid, + NULL, NULL, + ctx->wc_ctx, + candidate_abspath, + FALSE, + iterpool, iterpool)); + + if (candidate_revision == SVN_INVALID_REVNUM) + continue; + + /* If the conflict victim and the move target candidate + * are not from the same revision we must ensure that + * they are related. */ + if (candidate_revision != victim_revision) + { + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + + err = find_yca(&yca_loc, victim_repos_relpath, + victim_revision, + candidate_repos_relpath, + candidate_revision, + repos_root_url, repos_uuid, + NULL, ctx, iterpool, iterpool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + if (yca_loc == NULL) + continue; + } + + APR_ARRAY_PUSH(moved_to_abspaths, const char *) = + apr_pstrdup(result_pool, candidate_abspath); + } + svn_pool_destroy(iterpool); + + svn_hash_sets(wc_move_targets, move->moved_to_repos_relpath, + moved_to_abspaths); + } + } + else + { + int i; + apr_pool_t *iterpool; + + /* Recurse into each of the possible move chains. */ + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < move->next->nelts; i++) + { + struct repos_move_info *next_move; + + svn_pool_clear(iterpool); + + next_move = APR_ARRAY_IDX(move->next, i, struct repos_move_info *); + SVN_ERR(follow_move_chains(wc_move_targets, next_move, + ctx, victim_abspath, victim_node_kind, + victim_repos_relpath, victim_revision, + result_pool, iterpool)); + + } + svn_pool_destroy(iterpool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +init_wc_move_targets(struct conflict_tree_incoming_delete_details *details, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + int i; + const char *victim_abspath; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_wc_operation_t operation; + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + operation = svn_client_conflict_get_operation(conflict); + /* ### Should we get the old location in case of reverse-merges? */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, + scratch_pool, scratch_pool)); + details->wc_move_targets = apr_hash_make(conflict->pool); + for (i = 0; i < details->moves->nelts; i++) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, i, struct repos_move_info *); + SVN_ERR(follow_move_chains(details->wc_move_targets, move, + ctx, victim_abspath, + victim_node_kind, + incoming_new_repos_relpath, + incoming_new_pegrev, + conflict->pool, scratch_pool)); + } + + /* Initialize to the first possible move target. Hopefully, + * in most cases there will only be one candidate anyway. */ + details->move_target_repos_relpath = + get_moved_to_repos_relpath(details, scratch_pool); + details->wc_move_target_idx = 0; + + /* If only one move target exists after an update or switch, + * recommend a resolution option which follows the incoming move. */ + if (apr_hash_count(details->wc_move_targets) == 1 && + (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch)) + { + apr_array_header_t *wc_abspaths; + + wc_abspaths = svn_hash_gets(details->wc_move_targets, + details->move_target_repos_relpath); + if (wc_abspaths->nelts == 1) + { + svn_client_conflict_option_id_t recommended[] = + { + /* Only one of these will be present for any given conflict. */ + svn_client_conflict_option_incoming_move_file_text_merge, + svn_client_conflict_option_incoming_move_dir_merge, + svn_client_conflict_option_local_move_file_text_merge + }; + apr_array_header_t *options; + + SVN_ERR(svn_client_conflict_tree_get_resolution_options( + &options, conflict, ctx, scratch_pool, scratch_pool)); + for (i = 0; i < (sizeof(recommended) / sizeof(recommended[0])); i++) + { + svn_client_conflict_option_id_t option_id = recommended[i]; + + if (svn_client_conflict_option_find_by_id(options, option_id)) + { + conflict->recommended_option_id = option_id; + break; + } + } + } + } + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_details_func_t. + * Find the revision in which the victim was deleted in the repository. */ +static svn_error_t * +conflict_tree_get_details_incoming_delete(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *old_repos_relpath; + const char *new_repos_relpath; + const char *repos_root_url; + svn_revnum_t old_rev; + svn_revnum_t new_rev; + svn_node_kind_t old_kind; + svn_node_kind_t new_kind; + struct conflict_tree_incoming_delete_details *details; + svn_wc_operation_t operation; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, &old_kind, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, &new_kind, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, + scratch_pool, scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + + if (operation == svn_wc_operation_update) + { + if (old_rev < new_rev) + { + const char *parent_repos_relpath; + svn_revnum_t parent_peg_rev; + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + apr_array_header_t *moves; + const char *related_repos_relpath; + svn_revnum_t related_peg_rev; + + /* The update operation went forward in history. */ + SVN_ERR(svn_wc__node_get_repos_info(&parent_peg_rev, + &parent_repos_relpath, + NULL, NULL, + ctx->wc_ctx, + svn_dirent_dirname( + conflict->local_abspath, + scratch_pool), + scratch_pool, + scratch_pool)); + if (new_kind == svn_node_none) + { + SVN_ERR(find_related_node(&related_repos_relpath, + &related_peg_rev, + new_repos_relpath, new_rev, + old_repos_relpath, old_rev, + conflict, ctx, + scratch_pool, scratch_pool)); + } + else + { + /* related to self */ + related_repos_relpath = NULL; + related_peg_rev = SVN_INVALID_REVNUM; + } + + SVN_ERR(find_revision_for_suspected_deletion( + &deleted_rev, &deleted_rev_author, &replacing_node_kind, + &moves, conflict, + svn_dirent_basename(conflict->local_abspath, scratch_pool), + parent_repos_relpath, parent_peg_rev, + new_kind == svn_node_none ? 0 : old_rev, + related_repos_relpath, related_peg_rev, + ctx, conflict->pool, scratch_pool)); + if (deleted_rev == SVN_INVALID_REVNUM) + { + /* We could not determine the revision in which the node was + * deleted. We cannot provide the required details so the best + * we can do is fall back to the default description. */ + return SVN_NO_ERROR; + } + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + details->deleted_rev = deleted_rev; + details->added_rev = SVN_INVALID_REVNUM; + details->repos_relpath = apr_pstrdup(conflict->pool, + new_repos_relpath); + details->rev_author = deleted_rev_author; + details->replacing_node_kind = replacing_node_kind; + details->moves = moves; + } + else /* new_rev < old_rev */ + { + /* The update operation went backwards in history. + * Figure out when this node was added. */ + SVN_ERR(get_incoming_delete_details_for_reverse_addition( + &details, repos_root_url, old_repos_relpath, + old_rev, new_rev, ctx, + svn_client_conflict_get_local_abspath(conflict), + conflict->pool, scratch_pool)); + } + } + else if (operation == svn_wc_operation_switch || + operation == svn_wc_operation_merge) + { + if (old_rev < new_rev) + { + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + apr_array_header_t *moves; + + /* The switch/merge operation went forward in history. + * + * The deletion of the node happened on the branch we switched to + * or merged from. Scan new_repos_relpath's parent's log to find + * the revision which deleted the node. */ + SVN_ERR(find_revision_for_suspected_deletion( + &deleted_rev, &deleted_rev_author, &replacing_node_kind, + &moves, conflict, + svn_relpath_basename(new_repos_relpath, scratch_pool), + svn_relpath_dirname(new_repos_relpath, scratch_pool), + new_rev, old_rev, old_repos_relpath, old_rev, ctx, + conflict->pool, scratch_pool)); + if (deleted_rev == SVN_INVALID_REVNUM) + { + /* We could not determine the revision in which the node was + * deleted. We cannot provide the required details so the best + * we can do is fall back to the default description. */ + return SVN_NO_ERROR; + } + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + details->deleted_rev = deleted_rev; + details->added_rev = SVN_INVALID_REVNUM; + details->repos_relpath = apr_pstrdup(conflict->pool, + new_repos_relpath); + details->rev_author = apr_pstrdup(conflict->pool, + deleted_rev_author); + details->replacing_node_kind = replacing_node_kind; + details->moves = moves; + } + else /* new_rev < old_rev */ + { + /* The switch/merge operation went backwards in history. + * Figure out when the node we switched away from, or merged + * from another branch, was added. */ + SVN_ERR(get_incoming_delete_details_for_reverse_addition( + &details, repos_root_url, old_repos_relpath, + old_rev, new_rev, ctx, + svn_client_conflict_get_local_abspath(conflict), + conflict->pool, scratch_pool)); + } + } + else + { + details = NULL; + } + + conflict->tree_conflict_incoming_details = details; + + if (details && details->moves) + SVN_ERR(init_wc_move_targets(details, conflict, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Details for tree conflicts involving incoming additions. */ +struct conflict_tree_incoming_add_details +{ + /* If not SVN_INVALID_REVNUM, the node was added in ADDED_REV. */ + svn_revnum_t added_rev; + + /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. + * Note that both ADDED_REV and DELETED_REV may be valid for update/switch. + * See comment in conflict_tree_get_details_incoming_add() for details. */ + svn_revnum_t deleted_rev; + + /* The path which was added/deleted relative to the repository root. */ + const char *repos_relpath; + + /* Authors who committed ADDED_REV/DELETED_REV. */ + const char *added_rev_author; + const char *deleted_rev_author; + + /* Move information. If not NULL, this is an array of repos_move_info * + * elements. Each element is the head of a move chain which starts in + * ADDED_REV or in DELETED_REV (in which case moves should be interpreted + * in reverse). */ + apr_array_header_t *moves; +}; + +/* Implements tree_conflict_get_details_func_t. + * Find the revision in which the victim was added in the repository. */ +static svn_error_t * +conflict_tree_get_details_incoming_add(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *old_repos_relpath; + const char *new_repos_relpath; + const char *repos_root_url; + svn_revnum_t old_rev; + svn_revnum_t new_rev; + struct conflict_tree_incoming_add_details *details; + svn_wc_operation_t operation; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, + scratch_pool, scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + /* Only the new repository location is recorded for the node which + * caused an incoming addition. There is no pre-update/pre-switch + * revision to be recorded for the node since it does not exist in + * the repository at that revision. + * The implication is that we cannot know whether the operation went + * forward or backwards in history. So always try to find an added + * and a deleted revision for the node. Users must figure out by whether + * the addition or deletion caused the conflict. */ + const char *url; + const char *corrected_url; + svn_string_t *author_revprop; + struct find_added_rev_baton b = { 0 }; + svn_ra_session_t *ra_session; + svn_revnum_t deleted_rev; + svn_revnum_t head_rev; + + url = svn_path_url_add_component2(repos_root_url, new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + b.ctx = ctx, + b.victim_abspath = svn_client_conflict_get_local_abspath(conflict), + b.added_rev = SVN_INVALID_REVNUM; + b.repos_relpath = NULL; + b.parent_repos_relpath = NULL; + b.pool = scratch_pool; + + /* Figure out when this node was added. */ + SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev, + new_rev, SVN_INVALID_REVNUM, + find_added_rev, &b, + scratch_pool)); + + SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev, + SVN_PROP_REVISION_AUTHOR, + &author_revprop, scratch_pool)); + details->repos_relpath = apr_pstrdup(conflict->pool, b.repos_relpath); + details->added_rev = b.added_rev; + if (author_revprop) + details->added_rev_author = apr_pstrdup(conflict->pool, + author_revprop->data); + else + details->added_rev_author = _("unknown author"); + details->deleted_rev = SVN_INVALID_REVNUM; + details->deleted_rev_author = NULL; + + /* Figure out whether this node was deleted later. + * ### Could probably optimize by infering both addition and deletion + * ### from svn_ra_get_location_segments() call above. */ + SVN_ERR(svn_ra_get_latest_revnum(ra_session, &head_rev, scratch_pool)); + if (new_rev < head_rev) + { + SVN_ERR(svn_ra_get_deleted_rev(ra_session, "", new_rev, head_rev, + &deleted_rev, scratch_pool)); + if (SVN_IS_VALID_REVNUM(deleted_rev)) + { + SVN_ERR(svn_ra_rev_prop(ra_session, deleted_rev, + SVN_PROP_REVISION_AUTHOR, + &author_revprop, scratch_pool)); + details->deleted_rev = deleted_rev; + if (author_revprop) + details->deleted_rev_author = apr_pstrdup(conflict->pool, + author_revprop->data); + else + details->deleted_rev_author = _("unknown author"); + } + } + } + else if (operation == svn_wc_operation_merge) + { + if (old_rev < new_rev) + { + /* The merge operation went forwards in history. + * The addition of the node happened on the branch we merged form. + * Scan the nodes's history to find the revision which added it. */ + const char *url; + const char *corrected_url; + svn_string_t *author_revprop; + struct find_added_rev_baton b = { 0 }; + svn_ra_session_t *ra_session; + + url = svn_path_url_add_component2(repos_root_url, new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + b.victim_abspath = svn_client_conflict_get_local_abspath(conflict); + b.ctx = ctx; + b.added_rev = SVN_INVALID_REVNUM; + b.repos_relpath = NULL; + b.parent_repos_relpath = NULL; + b.pool = scratch_pool; + + /* Figure out when this node was added. */ + SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev, + new_rev, old_rev, + find_added_rev, &b, + scratch_pool)); + + SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev, + SVN_PROP_REVISION_AUTHOR, + &author_revprop, scratch_pool)); + details->repos_relpath = apr_pstrdup(conflict->pool, b.repos_relpath); + details->added_rev = b.added_rev; + if (author_revprop) + details->added_rev_author = apr_pstrdup(conflict->pool, + author_revprop->data); + else + details->added_rev_author = _("unknown author"); + details->deleted_rev = SVN_INVALID_REVNUM; + details->deleted_rev_author = NULL; + } + else + { + /* The merge operation was a reverse-merge. + * This addition is in fact a deletion, applied in reverse, + * which happened on the branch we merged from. + * Find the revision which deleted the node. */ + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + apr_array_header_t *moves; + + SVN_ERR(find_revision_for_suspected_deletion( + &deleted_rev, &deleted_rev_author, &replacing_node_kind, + &moves, conflict, + svn_relpath_basename(old_repos_relpath, scratch_pool), + svn_relpath_dirname(old_repos_relpath, scratch_pool), + old_rev, new_rev, + NULL, SVN_INVALID_REVNUM, /* related to self */ + ctx, + conflict->pool, scratch_pool)); + if (deleted_rev == SVN_INVALID_REVNUM) + { + /* We could not determine the revision in which the node was + * deleted. We cannot provide the required details so the best + * we can do is fall back to the default description. */ + return SVN_NO_ERROR; + } + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + details->repos_relpath = apr_pstrdup(conflict->pool, + new_repos_relpath); + details->deleted_rev = deleted_rev; + details->deleted_rev_author = apr_pstrdup(conflict->pool, + deleted_rev_author); + + details->added_rev = SVN_INVALID_REVNUM; + details->added_rev_author = NULL; + details->moves = moves; + } + } + else + { + details = NULL; + } + + conflict->tree_conflict_incoming_details = details; + + return SVN_NO_ERROR; +} + +static const char * +describe_incoming_add_upon_update( + struct conflict_tree_incoming_add_details *details, + svn_node_kind_t new_node_kind, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (new_node_kind == svn_node_dir) + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new directory appeared during update to r%ld; " + "it was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new directory appeared during update to r%ld; " + "it was added by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new directory appeared during update to r%ld; " + "it was deleted by %s in r%ld."), new_rev, + details->deleted_rev_author, details->deleted_rev); + } + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new file appeared during update to r%ld; " + "it was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new file appeared during update to r%ld; " + "it was added by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new file appeared during update to r%ld; " + "it was deleted by %s in r%ld."), new_rev, + details->deleted_rev_author, details->deleted_rev); + } + else + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new item appeared during update to r%ld; " + "it was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new item appeared during update to r%ld; " + "it was added by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new item appeared during update to r%ld; " + "it was deleted by %s in r%ld."), new_rev, + details->deleted_rev_author, details->deleted_rev); + } +} + +static const char * +describe_incoming_add_upon_switch( + struct conflict_tree_incoming_add_details *details, + svn_node_kind_t victim_node_kind, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (victim_node_kind == svn_node_dir) + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new directory appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new directory appeared during switch to\n" + "'^/%s@%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new directory appeared during switch to\n" + "'^/%s@%ld'.\nIt was deleted by %s in r%ld."), + new_repos_relpath, new_rev, + details->deleted_rev_author, details->deleted_rev); + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new file appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new file appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new file appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was deleted by %s in r%ld."), + new_repos_relpath, new_rev, + details->deleted_rev_author, details->deleted_rev); + } + else + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new item appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new item appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new item appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was deleted by %s in r%ld."), + new_repos_relpath, new_rev, + details->deleted_rev_author, details->deleted_rev); + } +} + +static const char * +describe_incoming_add_upon_merge( + struct conflict_tree_incoming_add_details *details, + svn_node_kind_t new_node_kind, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (new_node_kind == svn_node_dir) + { + if (old_rev + 1 == new_rev) + return apr_psprintf(result_pool, + _("A new directory appeared during merge of\n" + "'^/%s:%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new directory appeared during merge of\n" + "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, old_rev + 1, new_rev, + details->added_rev_author, details->added_rev); + } + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + { + if (old_rev + 1 == new_rev) + return apr_psprintf(result_pool, + _("A new file appeared during merge of\n" + "'^/%s:%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new file appeared during merge of\n" + "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, old_rev + 1, new_rev, + details->added_rev_author, details->added_rev); + } + else + { + if (old_rev + 1 == new_rev) + return apr_psprintf(result_pool, + _("A new item appeared during merge of\n" + "'^/%s:%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new item appeared during merge of\n" + "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, old_rev + 1, new_rev, + details->added_rev_author, details->added_rev); + } +} + +static const char * +describe_incoming_reverse_deletion_upon_merge( + struct conflict_tree_incoming_add_details *details, + svn_node_kind_t new_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (new_node_kind == svn_node_dir) + { + if (new_rev + 1 == old_rev) + return apr_psprintf(result_pool, + _("A new directory appeared during reverse-merge of" + "\n'^/%s:%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, old_rev, + details->deleted_rev_author, + details->deleted_rev); + else + return apr_psprintf(result_pool, + _("A new directory appeared during reverse-merge " + "of\n'^/%s:%ld-%ld'.\n" + "It was deleted by %s in r%ld."), + old_repos_relpath, new_rev, rev_below(old_rev), + details->deleted_rev_author, + details->deleted_rev); + } + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + { + if (new_rev + 1 == old_rev) + return apr_psprintf(result_pool, + _("A new file appeared during reverse-merge of\n" + "'^/%s:%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, old_rev, + details->deleted_rev_author, + details->deleted_rev); + else + return apr_psprintf(result_pool, + _("A new file appeared during reverse-merge of\n" + "'^/%s:%ld-%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, new_rev + 1, old_rev, + details->deleted_rev_author, + details->deleted_rev); + } + else + { + if (new_rev + 1 == old_rev) + return apr_psprintf(result_pool, + _("A new item appeared during reverse-merge of\n" + "'^/%s:%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, old_rev, + details->deleted_rev_author, + details->deleted_rev); + else + return apr_psprintf(result_pool, + _("A new item appeared during reverse-merge of\n" + "'^/%s:%ld-%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, new_rev + 1, old_rev, + details->deleted_rev_author, + details->deleted_rev); + } +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_description_incoming_add( + const char **incoming_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *action; + svn_node_kind_t victim_node_kind; + svn_wc_operation_t conflict_operation; + const char *old_repos_relpath; + svn_revnum_t old_rev; + svn_node_kind_t old_node_kind; + const char *new_repos_relpath; + svn_revnum_t new_rev; + svn_node_kind_t new_node_kind; + struct conflict_tree_incoming_add_details *details; + + if (conflict->tree_conflict_incoming_details == NULL) + return svn_error_trace(conflict_tree_get_incoming_description_generic( + incoming_change_description, conflict, ctx, + result_pool, scratch_pool)); + + conflict_operation = svn_client_conflict_get_operation(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, &old_node_kind, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, &new_node_kind, conflict, + scratch_pool, scratch_pool)); + + details = conflict->tree_conflict_incoming_details; + + if (conflict_operation == svn_wc_operation_update) + { + action = describe_incoming_add_upon_update(details, + new_node_kind, + new_rev, + result_pool); + } + else if (conflict_operation == svn_wc_operation_switch) + { + action = describe_incoming_add_upon_switch(details, + victim_node_kind, + new_repos_relpath, + new_rev, + result_pool); + } + else if (conflict_operation == svn_wc_operation_merge) + { + if (old_rev < new_rev) + action = describe_incoming_add_upon_merge(details, + new_node_kind, + old_rev, + new_repos_relpath, + new_rev, + result_pool); + else + action = describe_incoming_reverse_deletion_upon_merge( + details, new_node_kind, old_repos_relpath, + old_rev, new_rev, result_pool); + } + + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; +} + +/* Details for tree conflicts involving incoming edits. + * Note that we store an array of these. Each element corresponds to a + * revision within the old/new range in which a modification occured. */ +struct conflict_tree_incoming_edit_details +{ + /* The revision in which the edit ocurred. */ + svn_revnum_t rev; + + /* The author of the revision. */ + const char *author; + + /** Is the text modified? May be svn_tristate_unknown. */ + svn_tristate_t text_modified; + + /** Are properties modified? May be svn_tristate_unknown. */ + svn_tristate_t props_modified; + + /** For directories, are children modified? + * May be svn_tristate_unknown. */ + svn_tristate_t children_modified; + + /* The path which was edited, relative to the repository root. */ + const char *repos_relpath; +}; + +/* Baton for find_modified_rev(). */ +struct find_modified_rev_baton { + const char *victim_abspath; + svn_client_ctx_t *ctx; + apr_array_header_t *edits; + const char *repos_relpath; + svn_node_kind_t node_kind; + apr_pool_t *result_pool; + apr_pool_t *scratch_pool; +}; + +/* Implements svn_log_entry_receiver_t. */ +static svn_error_t * +find_modified_rev(void *baton, + svn_log_entry_t *log_entry, + apr_pool_t *scratch_pool) +{ + struct find_modified_rev_baton *b = baton; + struct conflict_tree_incoming_edit_details *details = NULL; + svn_string_t *author; + apr_hash_index_t *hi; + apr_pool_t *iterpool; + + if (b->ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + b->victim_abspath, + svn_wc_notify_tree_conflict_details_progress, + scratch_pool), + notify->revision = log_entry->revision; + b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); + } + + /* No paths were changed in this revision. Nothing to do. */ + if (! log_entry->changed_paths2) + return SVN_NO_ERROR; + + details = apr_pcalloc(b->result_pool, sizeof(*details)); + details->rev = log_entry->revision; + author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR); + if (author) + details->author = apr_pstrdup(b->result_pool, author->data); + else + details->author = _("unknown author"); + + details->text_modified = svn_tristate_unknown; + details->props_modified = svn_tristate_unknown; + details->children_modified = svn_tristate_unknown; + + iterpool = svn_pool_create(scratch_pool); + for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); + hi != NULL; + hi = apr_hash_next(hi)) + { + void *val; + const char *path; + svn_log_changed_path2_t *log_item; + + svn_pool_clear(iterpool); + + apr_hash_this(hi, (void *) &path, NULL, &val); + log_item = val; + + /* ### Remove leading slash from paths in log entries. */ + if (path[0] == '/') + path = svn_relpath_canonicalize(path, iterpool); + + if (svn_path_compare_paths(b->repos_relpath, path) == 0 && + (log_item->action == 'M' || log_item->action == 'A')) + { + details->text_modified = log_item->text_modified; + details->props_modified = log_item->props_modified; + details->repos_relpath = apr_pstrdup(b->result_pool, path); + + if (log_item->copyfrom_path) + b->repos_relpath = apr_pstrdup(b->scratch_pool, + log_item->copyfrom_path); + } + else if (b->node_kind == svn_node_dir && + svn_relpath_skip_ancestor(b->repos_relpath, path) != NULL) + details->children_modified = svn_tristate_true; + } + + if (b->node_kind == svn_node_dir && + details->children_modified == svn_tristate_unknown) + details->children_modified = svn_tristate_false; + + APR_ARRAY_PUSH(b->edits, struct conflict_tree_incoming_edit_details *) = + details; + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_details_func_t. + * Find one or more revisions in which the victim was modified in the + * repository. */ +static svn_error_t * +conflict_tree_get_details_incoming_edit(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *old_repos_relpath; + const char *new_repos_relpath; + const char *repos_root_url; + svn_revnum_t old_rev; + svn_revnum_t new_rev; + svn_node_kind_t old_node_kind; + svn_node_kind_t new_node_kind; + svn_wc_operation_t operation; + const char *url; + const char *corrected_url; + svn_ra_session_t *ra_session; + apr_array_header_t *paths; + apr_array_header_t *revprops; + struct find_modified_rev_baton b = { 0 }; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, &old_node_kind, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, &new_node_kind, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, + scratch_pool, scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + if (operation == svn_wc_operation_update) + { + b.node_kind = old_rev < new_rev ? new_node_kind : old_node_kind; + + /* If there is no node then we cannot find any edits. */ + if (b.node_kind == svn_node_none) + return SVN_NO_ERROR; + + url = svn_path_url_add_component2(repos_root_url, + old_rev < new_rev ? new_repos_relpath + : old_repos_relpath, + scratch_pool); + + b.repos_relpath = old_rev < new_rev ? new_repos_relpath + : old_repos_relpath; + } + else if (operation == svn_wc_operation_switch || + operation == svn_wc_operation_merge) + { + url = svn_path_url_add_component2(repos_root_url, new_repos_relpath, + scratch_pool); + + b.repos_relpath = new_repos_relpath; + b.node_kind = new_node_kind; + } + + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(paths, const char *) = ""; + + revprops = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; + + b.ctx = ctx; + b.victim_abspath = svn_client_conflict_get_local_abspath(conflict); + b.result_pool = conflict->pool; + b.scratch_pool = scratch_pool; + b.edits = apr_array_make( + conflict->pool, 0, + sizeof(struct conflict_tree_incoming_edit_details *)); + + SVN_ERR(svn_ra_get_log2(ra_session, paths, + old_rev < new_rev ? old_rev : new_rev, + old_rev < new_rev ? new_rev : old_rev, + 0, /* no limit */ + TRUE, /* need the changed paths list */ + FALSE, /* need to traverse copies */ + FALSE, /* no need for merged revisions */ + revprops, + find_modified_rev, &b, + scratch_pool)); + + conflict->tree_conflict_incoming_details = b.edits; + + return SVN_NO_ERROR; +} + +static const char * +describe_incoming_edit_upon_update(svn_revnum_t old_rev, + svn_revnum_t new_rev, + svn_node_kind_t old_node_kind, + svn_node_kind_t new_node_kind, + apr_pool_t *result_pool) +{ + if (old_rev < new_rev) + { + if (new_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Changes destined for a directory arrived " + "via the following revisions during update " + "from r%ld to r%ld."), old_rev, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Changes destined for a file arrived " + "via the following revisions during update " + "from r%ld to r%ld"), old_rev, new_rev); + else + return apr_psprintf(result_pool, + _("Changes from the following revisions arrived " + "during update from r%ld to r%ld"), + old_rev, new_rev); + } + else + { + if (new_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Changes destined for a directory arrived " + "via the following revisions during backwards " + "update from r%ld to r%ld"), + old_rev, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Changes destined for a file arrived " + "via the following revisions during backwards " + "update from r%ld to r%ld"), + old_rev, new_rev); + else + return apr_psprintf(result_pool, + _("Changes from the following revisions arrived " + "during backwards update from r%ld to r%ld"), + old_rev, new_rev); + } +} + +static const char * +describe_incoming_edit_upon_switch(const char *new_repos_relpath, + svn_revnum_t new_rev, + svn_node_kind_t new_node_kind, + apr_pool_t *result_pool) +{ + if (new_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Changes destined for a directory arrived via " + "the following revisions during switch to\n" + "'^/%s@r%ld'"), + new_repos_relpath, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Changes destined for a directory arrived via " + "the following revisions during switch to\n" + "'^/%s@r%ld'"), + new_repos_relpath, new_rev); + else + return apr_psprintf(result_pool, + _("Changes from the following revisions arrived " + "during switch to\n'^/%s@r%ld'"), + new_repos_relpath, new_rev); +} + +/* Return a string showing the list of revisions in EDITS, ensuring + * the string won't grow too large for display. */ +static const char * +describe_incoming_edit_list_modified_revs(apr_array_header_t *edits, + apr_pool_t *result_pool) +{ + int num_revs_to_skip; + static const int min_revs_for_skipping = 5; + static const int max_revs_to_display = 8; + const char *s = ""; + int i; + + if (edits->nelts <= max_revs_to_display) + num_revs_to_skip = 0; + else + { + /* Check if we should insert a placeholder for some revisions because + * the string would grow too long for display otherwise. */ + num_revs_to_skip = edits->nelts - max_revs_to_display; + if (num_revs_to_skip < min_revs_for_skipping) + { + /* Don't bother with the placeholder. Just list all revisions. */ + num_revs_to_skip = 0; + } + } + + for (i = 0; i < edits->nelts; i++) + { + struct conflict_tree_incoming_edit_details *details; + + details = APR_ARRAY_IDX(edits, i, + struct conflict_tree_incoming_edit_details *); + if (num_revs_to_skip > 0) + { + /* Insert a placeholder for revisions falling into the middle of + * the range so we'll get something that looks like: + * 1, 2, 3, 4, 5 [ placeholder ] 95, 96, 97, 98, 99 */ + if (i < max_revs_to_display / 2) + s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s, + details->rev, details->author, + i < edits->nelts - 1 ? "," : ""); + else if (i >= max_revs_to_display / 2 && + i < edits->nelts - (max_revs_to_display / 2)) + continue; + else + { + if (i == edits->nelts - (max_revs_to_display / 2)) + s = apr_psprintf(result_pool, + _("%s\n [%d revisions omitted for " + "brevity],\n"), + s, num_revs_to_skip); + + s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s, + details->rev, details->author, + i < edits->nelts - 1 ? "," : ""); + } + } + else + s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s, + details->rev, details->author, + i < edits->nelts - 1 ? "," : ""); + } + + return s; +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_description_incoming_edit( + const char **incoming_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *action; + svn_wc_operation_t conflict_operation; + const char *old_repos_relpath; + svn_revnum_t old_rev; + svn_node_kind_t old_node_kind; + const char *new_repos_relpath; + svn_revnum_t new_rev; + svn_node_kind_t new_node_kind; + apr_array_header_t *edits; + + if (conflict->tree_conflict_incoming_details == NULL) + return svn_error_trace(conflict_tree_get_incoming_description_generic( + incoming_change_description, conflict, ctx, + result_pool, scratch_pool)); + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, &old_node_kind, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, &new_node_kind, conflict, + scratch_pool, scratch_pool)); + + conflict_operation = svn_client_conflict_get_operation(conflict); + + edits = conflict->tree_conflict_incoming_details; + + if (conflict_operation == svn_wc_operation_update) + action = describe_incoming_edit_upon_update(old_rev, new_rev, + old_node_kind, new_node_kind, + scratch_pool); + else if (conflict_operation == svn_wc_operation_switch) + action = describe_incoming_edit_upon_switch(new_repos_relpath, new_rev, + new_node_kind, scratch_pool); + else if (conflict_operation == svn_wc_operation_merge) + { + /* Handle merge inline because it returns early sometimes. */ + if (old_rev < new_rev) + { + if (old_rev + 1 == new_rev) + { + if (new_node_kind == svn_node_dir) + action = apr_psprintf(scratch_pool, + _("Changes destined for a directory " + "arrived during merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + action = apr_psprintf(scratch_pool, + _("Changes destined for a file " + "arrived during merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, new_rev); + else + action = apr_psprintf(scratch_pool, + _("Changes arrived during merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, new_rev); + + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; + } + else + { + if (new_node_kind == svn_node_dir) + action = apr_psprintf(scratch_pool, + _("Changes destined for a directory " + "arrived via the following revisions " + "during merge of\n'^/%s:%ld-%ld'"), + new_repos_relpath, old_rev + 1, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + action = apr_psprintf(scratch_pool, + _("Changes destined for a file " + "arrived via the following revisions " + "during merge of\n'^/%s:%ld-%ld'"), + new_repos_relpath, old_rev + 1, new_rev); + else + action = apr_psprintf(scratch_pool, + _("Changes from the following revisions " + "arrived during merge of\n" + "'^/%s:%ld-%ld'"), + new_repos_relpath, old_rev + 1, new_rev); + } + } + else + { + if (new_rev + 1 == old_rev) + { + if (new_node_kind == svn_node_dir) + action = apr_psprintf(scratch_pool, + _("Changes destined for a directory " + "arrived during reverse-merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, old_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + action = apr_psprintf(scratch_pool, + _("Changes destined for a file " + "arrived during reverse-merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, old_rev); + else + action = apr_psprintf(scratch_pool, + _("Changes arrived during reverse-merge " + "of\n'^/%s:%ld'."), + new_repos_relpath, old_rev); + + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; + } + else + { + if (new_node_kind == svn_node_dir) + action = apr_psprintf(scratch_pool, + _("Changes destined for a directory " + "arrived via the following revisions " + "during reverse-merge of\n" + "'^/%s:%ld-%ld'"), + new_repos_relpath, new_rev + 1, old_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + action = apr_psprintf(scratch_pool, + _("Changes destined for a file " + "arrived via the following revisions " + "during reverse-merge of\n" + "'^/%s:%ld-%ld'"), + new_repos_relpath, new_rev + 1, old_rev); + + else + action = apr_psprintf(scratch_pool, + _("Changes from the following revisions " + "arrived during reverse-merge of\n" + "'^/%s:%ld-%ld'"), + new_repos_relpath, new_rev + 1, old_rev); + } + } + } + + action = apr_psprintf(scratch_pool, "%s:\n%s", action, + describe_incoming_edit_list_modified_revs( + edits, scratch_pool)); + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_tree_get_description( + const char **incoming_change_description, + const char **local_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + SVN_ERR(conflict->tree_conflict_get_incoming_description_func( + incoming_change_description, + conflict, ctx, result_pool, scratch_pool)); + + SVN_ERR(conflict->tree_conflict_get_local_description_func( + local_change_description, + conflict, ctx, result_pool, scratch_pool)); + + return SVN_NO_ERROR; +} + +void +svn_client_conflict_option_set_merged_propval( + svn_client_conflict_option_t *option, + const svn_string_t *merged_propval) +{ + option->type_data.prop.merged_propval = svn_string_dup(merged_propval, + option->pool); +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_postpone(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + return SVN_NO_ERROR; /* Nothing to do. */ +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_text_conflict(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + const char *lock_abspath; + svn_wc_conflict_choice_t conflict_choice; + svn_error_t *err; + + option_id = svn_client_conflict_option_get_id(option); + conflict_choice = conflict_option_id_to_wc_conflict_choice(option_id); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_text_mark_resolved(ctx->wc_ctx, + local_abspath, + conflict_choice, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + conflict->resolution_text = option_id; + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_prop_conflict(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + svn_wc_conflict_choice_t conflict_choice; + const char *local_abspath; + const char *lock_abspath; + const char *propname = option->type_data.prop.propname; + svn_error_t *err; + const svn_string_t *merged_value; + + option_id = svn_client_conflict_option_get_id(option); + conflict_choice = conflict_option_id_to_wc_conflict_choice(option_id); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + if (option_id == svn_client_conflict_option_merged_text) + merged_value = option->type_data.prop.merged_propval; + else + merged_value = NULL; + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_prop_mark_resolved(ctx->wc_ctx, local_abspath, + propname, conflict_choice, + merged_value, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (propname[0] == '\0') + { + apr_hash_index_t *hi; + + /* All properties have been resolved to the same option. */ + for (hi = apr_hash_first(scratch_pool, conflict->prop_conflicts); + hi; + hi = apr_hash_next(hi)) + { + const char *this_propname = apr_hash_this_key(hi); + + svn_hash_sets(conflict->resolved_props, + apr_pstrdup(apr_hash_pool_get(conflict->resolved_props), + this_propname), + option); + svn_hash_sets(conflict->prop_conflicts, this_propname, NULL); + } + + conflict->legacy_prop_conflict_propname = NULL; + } + else + { + svn_hash_sets(conflict->resolved_props, + apr_pstrdup(apr_hash_pool_get(conflict->resolved_props), + propname), + option); + svn_hash_sets(conflict->prop_conflicts, propname, NULL); + + if (apr_hash_count(conflict->prop_conflicts) > 0) + conflict->legacy_prop_conflict_propname = + apr_hash_this_key(apr_hash_first(scratch_pool, + conflict->prop_conflicts)); + else + conflict->legacy_prop_conflict_propname = NULL; + } + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_accept_current_wc_state(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + option_id = svn_client_conflict_option_get_id(option); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + if (option_id != svn_client_conflict_option_accept_current_wc_state) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Tree conflict on '%s' can only be resolved " + "to the current working copy state"), + svn_dirent_local_style(local_abspath, + scratch_pool)); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + /* Resolve to current working copy state. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + + /* svn_wc__del_tree_conflict doesn't handle notification for us */ + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = option_id; + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_update_break_moved_away(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_tree_update_break_moved_away(ctx->wc_ctx, + local_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_update_raise_moved_away(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_tree_update_raise_moved_away(ctx->wc_ctx, + local_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_update_moved_away_node(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_tree_update_moved_away_node(ctx->wc_ctx, + local_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Verify the local working copy state matches what we expect when an + * incoming add vs add tree conflict exists after an update operation. + * We assume the update operation leaves the working copy in a state which + * prefers the local change and cancels the incoming addition. + * Run a quick sanity check and error out if it looks as if the + * working copy was modified since, even though it's not easy to make + * such modifications without also clearing the conflict marker. */ +static svn_error_t * +verify_local_state_for_incoming_add_upon_update( + svn_client_conflict_t *conflict, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + svn_client_conflict_option_id_t option_id; + const char *wcroot_abspath; + svn_wc_operation_t operation; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + const char *base_repos_relpath; + svn_revnum_t base_rev; + svn_node_kind_t base_kind; + const char *local_style_relpath; + svn_boolean_t is_added; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + option_id = svn_client_conflict_option_get_id(option); + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + local_abspath, scratch_pool, + scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + SVN_ERR_ASSERT(operation == svn_wc_operation_update); + + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + local_style_relpath = svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + local_abspath), + scratch_pool); + + /* Check if a local addition addition replaces the incoming new node. */ + err = svn_wc__node_get_base(&base_kind, &base_rev, &base_repos_relpath, + NULL, NULL, NULL, ctx->wc_ctx, local_abspath, + FALSE, scratch_pool, scratch_pool); + if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) + { + if (option_id == svn_client_conflict_option_incoming_add_ignore) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, + _("Cannot resolve tree conflict on '%s' " + "(expected a base node but found none)"), + local_style_relpath); + else if (option_id == + svn_client_conflict_option_incoming_added_dir_replace) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, + _("Cannot resolve tree conflict on '%s' " + "(expected a base node but found none)"), + local_style_relpath); + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, + _("Unexpected option id '%d'"), option_id); + } + else if (err) + return svn_error_trace(err); + + if (base_kind != incoming_new_kind) + { + if (option_id == svn_client_conflict_option_incoming_add_ignore) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected base node kind '%s', " + "but found '%s')"), + local_style_relpath, + svn_node_kind_to_word(incoming_new_kind), + svn_node_kind_to_word(base_kind)); + else if (option_id == + svn_client_conflict_option_incoming_added_dir_replace) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected base node kind '%s', " + "but found '%s')"), + local_style_relpath, + svn_node_kind_to_word(incoming_new_kind), + svn_node_kind_to_word(base_kind)); + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Unexpected option id '%d'"), option_id); + } + + if (strcmp(base_repos_relpath, incoming_new_repos_relpath) != 0 || + base_rev != incoming_new_pegrev) + { + if (option_id == svn_client_conflict_option_incoming_add_ignore) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected base node from '^/%s@%ld', " + "but found '^/%s@%ld')"), + local_style_relpath, + incoming_new_repos_relpath, + incoming_new_pegrev, + base_repos_relpath, base_rev); + else if (option_id == + svn_client_conflict_option_incoming_added_dir_replace) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected base node from '^/%s@%ld', " + "but found '^/%s@%ld')"), + local_style_relpath, + incoming_new_repos_relpath, + incoming_new_pegrev, + base_repos_relpath, base_rev); + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Unexpected option id '%d'"), option_id); + } + + SVN_ERR(svn_wc__node_is_added(&is_added, ctx->wc_ctx, local_abspath, + scratch_pool)); + if (!is_added) + { + if (option_id == svn_client_conflict_option_incoming_add_ignore) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an added item, but the item " + "is not added)"), + local_style_relpath); + + else if (option_id == + svn_client_conflict_option_incoming_added_dir_replace) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an added item, but the item " + "is not added)"), + local_style_relpath); + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Unexpected option id '%d'"), option_id); + } + + return SVN_NO_ERROR; +} + + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_add_ignore(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_wc_operation_t operation; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + operation = svn_client_conflict_get_operation(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + if (operation == svn_wc_operation_update) + { + err = verify_local_state_for_incoming_add_upon_update(conflict, option, + ctx, scratch_pool); + if (err) + goto unlock_wc; + } + + /* All other options for this conflict actively fetch the incoming + * new node. We can ignore the incoming new node by doing nothing. */ + + /* Resolve to current working copy state. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + + /* svn_wc__del_tree_conflict doesn't handle notification for us */ + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Delete entry and wc props from a set of properties. */ +static void +filter_props(apr_hash_t *props, apr_pool_t *scratch_pool) +{ + apr_hash_index_t *hi; + + for (hi = apr_hash_first(scratch_pool, props); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *propname = apr_hash_this_key(hi); + + if (!svn_wc_is_normal_prop(propname)) + svn_hash_sets(props, propname, NULL); + } +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_file_text_update( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *wc_tmpdir; + const char *local_abspath; + const char *lock_abspath; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + const char *empty_file_abspath; + const char *working_file_tmp_abspath; + svn_stream_t *working_file_stream; + svn_stream_t *working_file_tmp_stream; + apr_hash_t *working_props; + apr_array_header_t *propdiffs; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Set up tempory storage for the working version of file. */ + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_open_unique(&working_file_tmp_stream, + &working_file_tmp_abspath, wc_tmpdir, + /* Don't delete automatically! */ + svn_io_file_del_none, + scratch_pool, scratch_pool)); + + /* Copy the detranslated working file to temporary storage. */ + SVN_ERR(svn_wc__translated_stream(&working_file_stream, ctx->wc_ctx, + local_abspath, local_abspath, + SVN_WC_TRANSLATE_TO_NF, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_copy3(working_file_stream, working_file_tmp_stream, + ctx->cancel_func, ctx->cancel_baton, + scratch_pool)); + + /* Get a copy of the working file's properties. */ + SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + filter_props(working_props, scratch_pool); + + /* Create an empty file as fake "merge-base" for the two added files. + * The files are not ancestrally related so this is the best we can do. */ + SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file_abspath, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Create a property diff which shows all props as added. */ + SVN_ERR(svn_prop_diffs(&propdiffs, working_props, + apr_hash_make(scratch_pool), scratch_pool)); + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + /* Revert the path in order to restore the repository's line of + * history, which is part of the BASE tree. This revert operation + * is why are being careful about not losing the temporary copy. */ + err = svn_wc_revert5(ctx->wc_ctx, local_abspath, svn_depth_empty, + FALSE, NULL, TRUE, FALSE, + NULL, NULL, /* no cancellation */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + + /* Perform the file merge. ### Merge into tempfile and then rename on top? */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, empty_file_abspath, + working_file_tmp_abspath, local_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + NULL, propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + +unlock_wc: + if (err) + err = svn_error_quick_wrapf( + err, _("If needed, a backup copy of '%s' can be found at '%s'"), + svn_dirent_local_style(local_abspath, scratch_pool), + svn_dirent_local_style(working_file_tmp_abspath, scratch_pool)); + err = svn_error_compose_create(err, + svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + /* Tell the world about the file merge that just happened. */ + notify = svn_wc_create_notify(local_abspath, + svn_wc_notify_update_update, + scratch_pool); + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + + /* And also about the successfully resolved tree conflict. */ + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + /* All is good -- remove temporary copy of the working file. */ + SVN_ERR(svn_io_remove_file2(working_file_tmp_abspath, TRUE, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_file_text_merge( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + const char *repos_root_url; + const char *wc_tmpdir; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *local_abspath; + const char *lock_abspath; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_file_t *incoming_new_file; + const char *incoming_new_tmp_abspath; + const char *empty_file_abspath; + svn_stream_t *incoming_new_stream; + apr_hash_t *incoming_new_props; + apr_array_header_t *propdiffs; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Set up temporary storage for the repository version of file. */ + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_io_open_unique_file3(&incoming_new_file, + &incoming_new_tmp_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + incoming_new_stream = svn_stream_from_aprfile2(incoming_new_file, TRUE, + scratch_pool); + + /* Fetch the incoming added file from the repository. */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, + incoming_new_stream, NULL, /* fetched_rev */ + &incoming_new_props, scratch_pool)); + + /* Flush file to disk. */ + SVN_ERR(svn_stream_close(incoming_new_stream)); + SVN_ERR(svn_io_file_flush(incoming_new_file, scratch_pool)); + + filter_props(incoming_new_props, scratch_pool); + + /* Create an empty file as fake "merge-base" for the two added files. + * The files are not ancestrally related so this is the best we can do. */ + SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file_abspath, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Create a property diff which shows all props as added. */ + SVN_ERR(svn_prop_diffs(&propdiffs, incoming_new_props, + apr_hash_make(scratch_pool), scratch_pool)); + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + /* Resolve to current working copy state. svn_wc_merge5() requires this. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + if (err) + return svn_error_compose_create(err, + svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + /* Perform the file merge. ### Merge into tempfile and then rename on top? */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, empty_file_abspath, + incoming_new_tmp_abspath, local_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + NULL, propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + /* Tell the world about the file merge that just happened. */ + notify = svn_wc_create_notify(local_abspath, + svn_wc_notify_update_update, + scratch_pool); + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + + /* And also about the successfully resolved tree conflict. */ + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_file_replace_and_merge( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + const char *repos_root_url; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + apr_file_t *incoming_new_file; + svn_stream_t *incoming_new_stream; + apr_hash_t *incoming_new_props; + const char *local_abspath; + const char *lock_abspath; + const char *wc_tmpdir; + svn_stream_t *working_file_tmp_stream; + const char *working_file_tmp_abspath; + svn_stream_t *working_file_stream; + apr_hash_t *working_props; + svn_error_t *err; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_file_t *empty_file; + const char *empty_file_abspath; + apr_array_header_t *propdiffs; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Set up tempory storage for the working version of file. */ + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_open_unique(&working_file_tmp_stream, + &working_file_tmp_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Copy the detranslated working file to temporary storage. */ + SVN_ERR(svn_wc__translated_stream(&working_file_stream, ctx->wc_ctx, + local_abspath, local_abspath, + SVN_WC_TRANSLATE_TO_NF, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_copy3(working_file_stream, working_file_tmp_stream, + ctx->cancel_func, ctx->cancel_baton, + scratch_pool)); + + /* Get a copy of the working file's properties. */ + SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + /* Fetch the incoming added file from the repository. */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + if (corrected_url) + url = corrected_url; + SVN_ERR(svn_io_open_unique_file3(&incoming_new_file, NULL, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + incoming_new_stream = svn_stream_from_aprfile2(incoming_new_file, TRUE, + scratch_pool); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, + incoming_new_stream, NULL, /* fetched_rev */ + &incoming_new_props, scratch_pool)); + /* Flush file to disk. */ + SVN_ERR(svn_io_file_flush(incoming_new_file, scratch_pool)); + + /* Reset the stream in preparation for adding its content to WC. */ + SVN_ERR(svn_stream_reset(incoming_new_stream)); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + /* ### The following WC modifications should be atomic. */ + + /* Replace the working file with the file from the repository. */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + err = svn_wc_add_repos_file4(ctx->wc_ctx, local_abspath, + incoming_new_stream, + NULL, /* ### could we merge first, then set + ### the merged content here? */ + incoming_new_props, + NULL, /* ### merge props first, set here? */ + url, incoming_new_pegrev, + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + if (err) + goto unlock_wc; + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath, + svn_wc_notify_add, + scratch_pool); + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + /* Resolve to current working copy state. svn_wc_merge5() requires this. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + if (err) + goto unlock_wc; + + /* Create an empty file as fake "merge-base" for the two added files. + * The files are not ancestrally related so this is the best we can do. */ + err = svn_io_open_unique_file3(&empty_file, &empty_file_abspath, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + filter_props(incoming_new_props, scratch_pool); + + /* Create a property diff for the files. */ + err = svn_prop_diffs(&propdiffs, incoming_new_props, + working_props, scratch_pool); + if (err) + goto unlock_wc; + + /* Perform the file merge. */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, empty_file_abspath, + working_file_tmp_abspath, local_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + NULL, propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + if (err) + goto unlock_wc; + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify( + local_abspath, + svn_wc_notify_update_update, + scratch_pool); + + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + SVN_ERR(svn_stream_close(incoming_new_stream)); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify( + local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool); + + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +static svn_error_t * +raise_tree_conflict(const char *local_abspath, + svn_wc_conflict_action_t incoming_change, + svn_wc_conflict_reason_t local_change, + svn_node_kind_t local_node_kind, + svn_node_kind_t merge_left_kind, + svn_node_kind_t merge_right_kind, + const char *repos_root_url, + const char *repos_uuid, + const char *repos_relpath, + svn_revnum_t merge_left_rev, + svn_revnum_t merge_right_rev, + svn_wc_context_t *wc_ctx, + svn_wc_notify_func2_t notify_func2, + void *notify_baton2, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_description2_t *conflict; + const svn_wc_conflict_version_t *left_version; + const svn_wc_conflict_version_t *right_version; + + left_version = svn_wc_conflict_version_create2(repos_root_url, + repos_uuid, + repos_relpath, + merge_left_rev, + merge_left_kind, + scratch_pool); + right_version = svn_wc_conflict_version_create2(repos_root_url, + repos_uuid, + repos_relpath, + merge_right_rev, + merge_right_kind, + scratch_pool); + conflict = svn_wc_conflict_description_create_tree2(local_abspath, + local_node_kind, + svn_wc_operation_merge, + left_version, + right_version, + scratch_pool); + conflict->action = incoming_change; + conflict->reason = local_change; + + SVN_ERR(svn_wc__add_tree_conflict(wc_ctx, conflict, scratch_pool)); + + if (notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_tree_conflict, + scratch_pool); + notify->kind = local_node_kind; + notify_func2(notify_baton2, notify, scratch_pool); + } + + return SVN_NO_ERROR; +} + +struct merge_newly_added_dir_baton { + const char *target_abspath; + svn_client_ctx_t *ctx; + const char *repos_root_url; + const char *repos_uuid; + const char *added_repos_relpath; + svn_revnum_t merge_left_rev; + svn_revnum_t merge_right_rev; +}; + +static svn_error_t * +merge_added_dir_props(const char *target_abspath, + const char *added_repos_relpath, + apr_hash_t *added_props, + const char *repos_root_url, + const char *repos_uuid, + svn_revnum_t merge_left_rev, + svn_revnum_t merge_right_rev, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_wc_notify_state_t property_state; + apr_array_header_t *propchanges; + const svn_wc_conflict_version_t *left_version; + const svn_wc_conflict_version_t *right_version; + apr_hash_index_t *hi; + + left_version = svn_wc_conflict_version_create2( + repos_root_url, repos_uuid, added_repos_relpath, + merge_left_rev, svn_node_none, scratch_pool); + + right_version = svn_wc_conflict_version_create2( + repos_root_url, repos_uuid, added_repos_relpath, + merge_right_rev, svn_node_dir, scratch_pool); + + propchanges = apr_array_make(scratch_pool, apr_hash_count(added_props), + sizeof(svn_prop_t)); + for (hi = apr_hash_first(scratch_pool, added_props); + hi; + hi = apr_hash_next(hi)) + { + svn_prop_t prop; + + prop.name = apr_hash_this_key(hi); + prop.value = apr_hash_this_val(hi); + + if (svn_wc_is_normal_prop(prop.name)) + APR_ARRAY_PUSH(propchanges, svn_prop_t) = prop; + } + + SVN_ERR(svn_wc_merge_props3(&property_state, ctx->wc_ctx, + target_abspath, + left_version, right_version, + apr_hash_make(scratch_pool), + propchanges, + FALSE, /* not a dry-run */ + NULL, NULL, NULL, NULL, + scratch_pool)); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify(target_abspath, + svn_wc_notify_update_update, + scratch_pool); + notify->kind = svn_node_dir; + notify->content_state = svn_wc_notify_state_unchanged;; + notify->prop_state = property_state; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + return SVN_NO_ERROR; +} + +/* An svn_diff_tree_processor_t callback. */ +static svn_error_t * +diff_dir_added(const char *relpath, + const svn_diff_source_t *copyfrom_source, + const svn_diff_source_t *right_source, + apr_hash_t *copyfrom_props, + apr_hash_t *right_props, + void *dir_baton, + const struct svn_diff_tree_processor_t *processor, + apr_pool_t *scratch_pool) +{ + struct merge_newly_added_dir_baton *b = processor->baton; + const char *local_abspath; + const char *copyfrom_url; + svn_node_kind_t db_kind; + svn_node_kind_t on_disk_kind; + apr_hash_index_t *hi; + + /* Handle the root of the added directory tree. */ + if (relpath[0] == '\0') + { + /* ### svn_wc_merge_props3() requires this... */ + SVN_ERR(svn_wc__del_tree_conflict(b->ctx->wc_ctx, b->target_abspath, + scratch_pool)); + SVN_ERR(merge_added_dir_props(b->target_abspath, + b->added_repos_relpath, right_props, + b->repos_root_url, b->repos_uuid, + b->merge_left_rev, b->merge_right_rev, + b->ctx, scratch_pool)); + return SVN_NO_ERROR; + + } + + local_abspath = svn_dirent_join(b->target_abspath, relpath, scratch_pool); + + SVN_ERR(svn_wc_read_kind2(&db_kind, b->ctx->wc_ctx, local_abspath, + FALSE, FALSE, scratch_pool)); + SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool)); + + if (db_kind == svn_node_dir && on_disk_kind == svn_node_dir) + { + SVN_ERR(merge_added_dir_props(svn_dirent_join(b->target_abspath, relpath, + scratch_pool), + b->added_repos_relpath, right_props, + b->repos_root_url, b->repos_uuid, + b->merge_left_rev, b->merge_right_rev, + b->ctx, scratch_pool)); + return SVN_NO_ERROR; + } + + if (db_kind != svn_node_none && db_kind != svn_node_unknown) + { + SVN_ERR(raise_tree_conflict( + local_abspath, svn_wc_conflict_action_add, + svn_wc_conflict_reason_obstructed, + db_kind, svn_node_none, svn_node_dir, + b->repos_root_url, b->repos_uuid, + svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), + b->merge_left_rev, b->merge_right_rev, + b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + return SVN_NO_ERROR; + } + + if (on_disk_kind != svn_node_none) + { + SVN_ERR(raise_tree_conflict( + local_abspath, svn_wc_conflict_action_add, + svn_wc_conflict_reason_obstructed, db_kind, + svn_node_none, svn_node_dir, b->repos_root_url, b->repos_uuid, + svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), + b->merge_left_rev, b->merge_right_rev, + b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + return SVN_NO_ERROR; + } + + SVN_ERR(svn_io_dir_make(local_abspath, APR_OS_DEFAULT, scratch_pool)); + copyfrom_url = apr_pstrcat(scratch_pool, b->repos_root_url, "/", + right_source->repos_relpath, SVN_VA_NULL); + SVN_ERR(svn_wc_add4(b->ctx->wc_ctx, local_abspath, svn_depth_infinity, + copyfrom_url, right_source->revision, + NULL, NULL, /* cancel func/baton */ + b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + + for (hi = apr_hash_first(scratch_pool, right_props); + hi; + hi = apr_hash_next(hi)) + { + const char *propname = apr_hash_this_key(hi); + const svn_string_t *propval = apr_hash_this_val(hi); + + SVN_ERR(svn_wc_prop_set4(b->ctx->wc_ctx, local_abspath, + propname, propval, svn_depth_empty, + FALSE, NULL /* do not skip checks */, + NULL, NULL, /* cancel func/baton */ + b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +merge_added_files(const char *local_abspath, + const char *incoming_added_file_abspath, + apr_hash_t *incoming_added_file_props, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_file_t *empty_file; + const char *empty_file_abspath; + apr_array_header_t *propdiffs; + apr_hash_t *working_props; + + /* Create an empty file as fake "merge-base" for the two added files. + * The files are not ancestrally related so this is the best we can do. */ + SVN_ERR(svn_io_open_unique_file3(&empty_file, &empty_file_abspath, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Get a copy of the working file's properties. */ + SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + /* Create a property diff for the files. */ + SVN_ERR(svn_prop_diffs(&propdiffs, incoming_added_file_props, + working_props, scratch_pool)); + + /* Perform the file merge. */ + SVN_ERR(svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, empty_file_abspath, + incoming_added_file_abspath, local_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + NULL, propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool)); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify( + local_abspath, + svn_wc_notify_update_update, + scratch_pool); + + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + return SVN_NO_ERROR; +} + +/* An svn_diff_tree_processor_t callback. */ +static svn_error_t * +diff_file_added(const char *relpath, + const svn_diff_source_t *copyfrom_source, + const svn_diff_source_t *right_source, + const char *copyfrom_file, + const char *right_file, + apr_hash_t *copyfrom_props, + apr_hash_t *right_props, + void *file_baton, + const struct svn_diff_tree_processor_t *processor, + apr_pool_t *scratch_pool) +{ + struct merge_newly_added_dir_baton *b = processor->baton; + const char *local_abspath; + svn_node_kind_t db_kind; + svn_node_kind_t on_disk_kind; + apr_array_header_t *propsarray; + apr_array_header_t *regular_props; + + local_abspath = svn_dirent_join(b->target_abspath, relpath, scratch_pool); + + SVN_ERR(svn_wc_read_kind2(&db_kind, b->ctx->wc_ctx, local_abspath, + FALSE, FALSE, scratch_pool)); + SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool)); + + if (db_kind == svn_node_file && on_disk_kind == svn_node_file) + { + propsarray = svn_prop_hash_to_array(right_props, scratch_pool); + SVN_ERR(svn_categorize_props(propsarray, NULL, NULL, ®ular_props, + scratch_pool)); + SVN_ERR(merge_added_files(local_abspath, right_file, + svn_prop_array_to_hash(regular_props, + scratch_pool), + b->ctx, scratch_pool)); + return SVN_NO_ERROR; + } + + if (db_kind != svn_node_none && db_kind != svn_node_unknown) + { + SVN_ERR(raise_tree_conflict( + local_abspath, svn_wc_conflict_action_add, + svn_wc_conflict_reason_obstructed, + db_kind, svn_node_none, svn_node_file, + b->repos_root_url, b->repos_uuid, + svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), + b->merge_left_rev, b->merge_right_rev, + b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + return SVN_NO_ERROR; + } + + if (on_disk_kind != svn_node_none) + { + SVN_ERR(raise_tree_conflict( + local_abspath, svn_wc_conflict_action_add, + svn_wc_conflict_reason_obstructed, db_kind, + svn_node_none, svn_node_file, b->repos_root_url, b->repos_uuid, + svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), + b->merge_left_rev, b->merge_right_rev, + b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + return SVN_NO_ERROR; + } + + propsarray = svn_prop_hash_to_array(right_props, scratch_pool); + SVN_ERR(svn_categorize_props(propsarray, NULL, NULL, ®ular_props, + scratch_pool)); + SVN_ERR(svn_io_copy_file(right_file, local_abspath, FALSE, scratch_pool)); + SVN_ERR(svn_wc_add_from_disk3(b->ctx->wc_ctx, local_abspath, + svn_prop_array_to_hash(regular_props, + scratch_pool), + FALSE, b->ctx->notify_func2, + b->ctx->notify_baton2, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Merge a newly added directory into TARGET_ABSPATH in the working copy. + * + * This uses a diff-tree processor because our standard merge operation + * is not set up for merges where the merge-source anchor is itself an + * added directory (i.e. does not exist on one side of the diff). + * The standard merge will only merge additions of children of a path + * that exists across the entire revision range being merged. + * But in our case, SOURCE1 does not yet exist in REV1, but SOURCE2 + * does exist in REV2. Thus we use a diff processor. + */ +static svn_error_t * +merge_newly_added_dir(const char *added_repos_relpath, + const char *source1, + svn_revnum_t rev1, + const char *source2, + svn_revnum_t rev2, + const char *target_abspath, + svn_boolean_t reverse_merge, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_diff_tree_processor_t *processor; + struct merge_newly_added_dir_baton baton = { 0 }; + const svn_diff_tree_processor_t *diff_processor; + svn_ra_session_t *ra_session; + const char *corrected_url; + svn_ra_session_t *extra_ra_session; + const svn_ra_reporter3_t *reporter; + void *reporter_baton; + const svn_delta_editor_t *diff_editor; + void *diff_edit_baton; + const char *anchor1; + const char *anchor2; + const char *target1; + const char *target2; + + svn_uri_split(&anchor1, &target1, source1, scratch_pool); + svn_uri_split(&anchor2, &target2, source2, scratch_pool); + + baton.target_abspath = target_abspath; + baton.ctx = ctx; + baton.added_repos_relpath = added_repos_relpath; + SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL, + &baton.repos_root_url, &baton.repos_uuid, + ctx->wc_ctx, target_abspath, + scratch_pool, scratch_pool)); + baton.merge_left_rev = rev1; + baton.merge_right_rev = rev2; + + processor = svn_diff__tree_processor_create(&baton, scratch_pool); + processor->dir_added = diff_dir_added; + processor->file_added = diff_file_added; + + diff_processor = processor; + if (reverse_merge) + diff_processor = svn_diff__tree_processor_reverse_create(diff_processor, + NULL, + scratch_pool); + + /* Filter the first path component using a filter processor, until we fixed + the diff processing to handle this directly */ + diff_processor = svn_diff__tree_processor_filter_create( + diff_processor, target1, scratch_pool); + + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + anchor2, NULL, NULL, FALSE, + FALSE, ctx, + scratch_pool, scratch_pool)); + if (corrected_url) + anchor2 = corrected_url; + + /* Extra RA session is used during the editor calls to fetch file contents. */ + SVN_ERR(svn_ra__dup_session(&extra_ra_session, ra_session, anchor2, + scratch_pool, scratch_pool)); + + /* Create a repos-repos diff editor. */ + SVN_ERR(svn_client__get_diff_editor2( + &diff_editor, &diff_edit_baton, + extra_ra_session, svn_depth_infinity, rev1, TRUE, + diff_processor, ctx->cancel_func, ctx->cancel_baton, + scratch_pool)); + + /* We want to switch our txn into URL2 */ + SVN_ERR(svn_ra_do_diff3(ra_session, &reporter, &reporter_baton, + rev2, target1, svn_depth_infinity, TRUE, TRUE, + source2, diff_editor, diff_edit_baton, scratch_pool)); + + /* Drive the reporter; do the diff. */ + SVN_ERR(reporter->set_path(reporter_baton, "", rev1, + svn_depth_infinity, + FALSE, NULL, + scratch_pool)); + + SVN_ERR(reporter->finish_report(reporter_baton, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_dir_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *repos_root_url; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *local_abspath; + const char *lock_abspath; + struct conflict_tree_incoming_add_details *details; + const char *added_repos_relpath; + const char *source1; + svn_revnum_t rev1; + const char *source2; + svn_revnum_t rev2; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + details = conflict->tree_conflict_incoming_details; + if (details == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Conflict resolution option '%d' requires " + "details for tree conflict at '%s' to be " + "fetched from the repository"), + option->id, + svn_dirent_local_style(local_abspath, + scratch_pool)); + + /* Set up merge sources to merge the entire incoming added directory tree. */ + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + source1 = svn_path_url_add_component2(repos_root_url, + details->repos_relpath, + scratch_pool); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + NULL, conflict, scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, scratch_pool)); + if (incoming_old_pegrev < incoming_new_pegrev) /* forward merge */ + { + if (details->added_rev == SVN_INVALID_REVNUM) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Could not determine when '%s' was " + "added the repository"), + svn_dirent_local_style(local_abspath, + scratch_pool)); + rev1 = rev_below(details->added_rev); + source2 = svn_path_url_add_component2(repos_root_url, + incoming_new_repos_relpath, + scratch_pool); + rev2 = incoming_new_pegrev; + added_repos_relpath = incoming_new_repos_relpath; + } + else /* reverse-merge */ + { + if (details->deleted_rev == SVN_INVALID_REVNUM) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Could not determine when '%s' was " + "deleted from the repository"), + svn_dirent_local_style(local_abspath, + scratch_pool)); + rev1 = details->deleted_rev; + source2 = svn_path_url_add_component2(repos_root_url, + incoming_old_repos_relpath, + scratch_pool); + rev2 = incoming_old_pegrev; + added_repos_relpath = incoming_new_repos_relpath; + } + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + /* ### wrap in a transaction */ + err = merge_newly_added_dir(added_repos_relpath, + source1, rev1, source2, rev2, + local_abspath, + (incoming_old_pegrev > incoming_new_pegrev), + ctx, scratch_pool, scratch_pool); + if (!err) + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_update_incoming_added_dir_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve( + &lock_abspath, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + err = svn_wc__conflict_tree_update_local_add(ctx->wc_ctx, + local_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + return SVN_NO_ERROR; +} + +/* A baton for notification_adjust_func(). */ +struct notification_adjust_baton +{ + svn_wc_notify_func2_t inner_func; + void *inner_baton; + const char *checkout_abspath; + const char *final_abspath; +}; + +/* A svn_wc_notify_func2_t function that wraps BATON->inner_func (whose + * baton is BATON->inner_baton) and adjusts the notification paths that + * start with BATON->checkout_abspath to start instead with + * BATON->final_abspath. */ +static void +notification_adjust_func(void *baton, + const svn_wc_notify_t *notify, + apr_pool_t *pool) +{ + struct notification_adjust_baton *nb = baton; + svn_wc_notify_t *inner_notify = svn_wc_dup_notify(notify, pool); + const char *relpath; + + relpath = svn_dirent_skip_ancestor(nb->checkout_abspath, notify->path); + inner_notify->path = svn_dirent_join(nb->final_abspath, relpath, pool); + + if (nb->inner_func) + nb->inner_func(nb->inner_baton, inner_notify, pool); +} + +/* Resolve a dir/dir "incoming add vs local obstruction" tree conflict by + * replacing the local directory with the incoming directory. + * If MERGE_DIRS is set, also merge the directories after replacing. */ +static svn_error_t * +merge_incoming_added_dir_replace(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + svn_boolean_t merge_dirs, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + const char *repos_root_url; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *local_abspath; + const char *lock_abspath; + const char *tmpdir_abspath, *tmp_abspath; + svn_error_t *err; + svn_revnum_t copy_src_revnum; + svn_opt_revision_t copy_src_peg_revision; + svn_boolean_t timestamp_sleep; + svn_wc_notify_func2_t old_notify_func2 = ctx->notify_func2; + void *old_notify_baton2 = ctx->notify_baton2; + struct notification_adjust_baton nb; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Find the URL of the incoming added directory in the repository. */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + if (corrected_url) + url = corrected_url; + + + /* Find a temporary location in which to check out the copy source. */ + SVN_ERR(svn_wc__get_tmpdir(&tmpdir_abspath, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + SVN_ERR(svn_io_open_unique_file3(NULL, &tmp_abspath, tmpdir_abspath, + svn_io_file_del_on_close, + scratch_pool, scratch_pool)); + + /* Make a new checkout of the requested source. While doing so, + * resolve copy_src_revnum to an actual revision number in case it + * was until now 'invalid' meaning 'head'. Ask this function not to + * sleep for timestamps, by passing a sleep_needed output param. + * Send notifications for all nodes except the root node, and adjust + * them to refer to the destination rather than this temporary path. */ + + nb.inner_func = ctx->notify_func2; + nb.inner_baton = ctx->notify_baton2; + nb.checkout_abspath = tmp_abspath; + nb.final_abspath = local_abspath; + ctx->notify_func2 = notification_adjust_func; + ctx->notify_baton2 = &nb; + + copy_src_peg_revision.kind = svn_opt_revision_number; + copy_src_peg_revision.value.number = incoming_new_pegrev; + + err = svn_client__checkout_internal(©_src_revnum, ×tamp_sleep, + url, tmp_abspath, + ©_src_peg_revision, + ©_src_peg_revision, + svn_depth_infinity, + TRUE, /* we want to ignore externals */ + FALSE, /* we don't allow obstructions */ + ra_session, ctx, scratch_pool); + + ctx->notify_func2 = old_notify_func2; + ctx->notify_baton2 = old_notify_baton2; + + SVN_ERR(err); + + /* ### The following WC modifications should be atomic. */ + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + svn_dirent_dirname( + local_abspath, + scratch_pool), + scratch_pool, scratch_pool)); + + /* Remove the working directory. */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + + /* Schedule dst_path for addition in parent, with copy history. + Don't send any notification here. + Then remove the temporary checkout's .svn dir in preparation for + moving the rest of it into the final destination. */ + err = svn_wc_copy3(ctx->wc_ctx, tmp_abspath, local_abspath, + TRUE /* metadata_only */, + NULL, NULL, /* don't allow user to cancel here */ + NULL, NULL, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_wc__acquire_write_lock(NULL, ctx->wc_ctx, tmp_abspath, + FALSE, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + err = svn_wc_remove_from_revision_control2(ctx->wc_ctx, + tmp_abspath, + FALSE, FALSE, + NULL, NULL, /* don't cancel */ + scratch_pool); + if (err) + goto unlock_wc; + + /* Move the temporary disk tree into place. */ + err = svn_io_file_rename2(tmp_abspath, local_abspath, FALSE, scratch_pool); + if (err) + goto unlock_wc; + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath, + svn_wc_notify_add, + scratch_pool); + notify->kind = svn_node_dir; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + /* Resolve to current working copy state. + * svn_client__merge_locked() requires this. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + if (err) + goto unlock_wc; + + if (merge_dirs) + { + svn_revnum_t base_revision; + const char *base_repos_relpath; + struct find_added_rev_baton b = { 0 }; + + /* Find the URL and revision of the directory we have just replaced. */ + err = svn_wc__node_get_base(NULL, &base_revision, &base_repos_relpath, + NULL, NULL, NULL, ctx->wc_ctx, local_abspath, + FALSE, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + url = svn_path_url_add_component2(repos_root_url, base_repos_relpath, + scratch_pool); + + /* Trace the replaced directory's history to its origin. */ + err = svn_ra_reparent(ra_session, url, scratch_pool); + if (err) + goto unlock_wc; + b.victim_abspath = local_abspath; + b.ctx = ctx; + b.added_rev = SVN_INVALID_REVNUM; + b.repos_relpath = NULL; + b.parent_repos_relpath = svn_relpath_dirname(base_repos_relpath, + scratch_pool); + b.pool = scratch_pool; + + err = svn_ra_get_location_segments(ra_session, "", base_revision, + base_revision, SVN_INVALID_REVNUM, + find_added_rev, &b, + scratch_pool); + if (err) + goto unlock_wc; + + if (b.added_rev == SVN_INVALID_REVNUM) + { + err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Could not determine the revision in " + "which '^/%s' was added to the " + "repository.\n"), + base_repos_relpath); + goto unlock_wc; + } + + /* Merge the replaced directory into the directory which replaced it. + * We do not need to consider a reverse-merge here since the source of + * this merge was part of the merge target working copy, not a branch + * in the repository. */ + err = merge_newly_added_dir(base_repos_relpath, + url, rev_below(b.added_rev), url, + base_revision, local_abspath, FALSE, + ctx, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + } + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify( + local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool); + + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_dir_replace(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + return svn_error_trace(merge_incoming_added_dir_replace(option, + conflict, + ctx, + FALSE, + scratch_pool)); +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_dir_replace_and_merge( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + return svn_error_trace(merge_incoming_added_dir_replace(option, + conflict, + ctx, + TRUE, + scratch_pool)); +} + +/* Verify the local working copy state matches what we expect when an + * incoming deletion tree conflict exists. + * We assume update/merge/switch operations leave the working copy in a + * state which prefers the local change and cancels the deletion. + * Run a quick sanity check and error out if it looks as if the + * working copy was modified since, even though it's not easy to make + * such modifications without also clearing the conflict marker. */ +static svn_error_t * +verify_local_state_for_incoming_delete(svn_client_conflict_t *conflict, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *wcroot_abspath; + svn_wc_operation_t operation; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + local_abspath, scratch_pool, + scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + struct conflict_tree_incoming_delete_details *details; + svn_boolean_t is_copy; + svn_revnum_t copyfrom_rev; + const char *copyfrom_repos_relpath; + + details = conflict->tree_conflict_incoming_details; + if (details == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Conflict resolution option '%d' requires " + "details for tree conflict at '%s' to be " + "fetched from the repository."), + option->id, + svn_dirent_local_style(local_abspath, + scratch_pool)); + + /* Ensure that the item is a copy of itself from before it was deleted. + * Update and switch are supposed to set this up when flagging the + * conflict. */ + SVN_ERR(svn_wc__node_get_origin(&is_copy, ©from_rev, + ©from_repos_relpath, + NULL, NULL, NULL, NULL, + ctx->wc_ctx, local_abspath, FALSE, + scratch_pool, scratch_pool)); + if (!is_copy) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected a copied item, but the item " + "is not a copy)"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + conflict->local_abspath), + scratch_pool)); + else if (details->deleted_rev == SVN_INVALID_REVNUM && + details->added_rev == SVN_INVALID_REVNUM) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Could not find the revision in which '%s' " + "was deleted from the repository"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + conflict->local_abspath), + scratch_pool)); + else if (details->deleted_rev != SVN_INVALID_REVNUM && + copyfrom_rev >= details->deleted_rev) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an item copied from a revision " + "smaller than r%ld, but the item was " + "copied from r%ld)"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, conflict->local_abspath), + scratch_pool), + details->deleted_rev, copyfrom_rev); + + else if (details->added_rev != SVN_INVALID_REVNUM && + copyfrom_rev < details->added_rev) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an item copied from a revision " + "larger than r%ld, but the item was " + "copied from r%ld)"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, conflict->local_abspath), + scratch_pool), + details->added_rev, copyfrom_rev); + else if (operation == svn_wc_operation_update) + { + const char *old_repos_relpath; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, NULL, NULL, conflict, + scratch_pool, scratch_pool)); + if (strcmp(copyfrom_repos_relpath, details->repos_relpath) != 0 && + strcmp(copyfrom_repos_relpath, old_repos_relpath) != 0) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an item copied from '^/%s' " + "or from '^/%s' but the item was " + "copied from '^/%s@%ld')"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, conflict->local_abspath), + scratch_pool), + details->repos_relpath, + old_repos_relpath, + copyfrom_repos_relpath, copyfrom_rev); + } + else if (operation == svn_wc_operation_switch) + { + const char *old_repos_relpath; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, NULL, NULL, conflict, + scratch_pool, scratch_pool)); + + if (strcmp(copyfrom_repos_relpath, old_repos_relpath) != 0) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an item copied from '^/%s', " + "but the item was copied from " + "'^/%s@%ld')"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + conflict->local_abspath), + scratch_pool), + old_repos_relpath, + copyfrom_repos_relpath, copyfrom_rev); + } + } + else if (operation == svn_wc_operation_merge) + { + svn_node_kind_t victim_node_kind; + svn_node_kind_t on_disk_kind; + + /* For merge, all we can do is ensure that the item still exists. */ + victim_node_kind = + svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool)); + + if (victim_node_kind != on_disk_kind) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected node kind '%s' but found '%s')"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, conflict->local_abspath), + scratch_pool), + svn_node_kind_to_word(victim_node_kind), + svn_node_kind_to_word(on_disk_kind)); + } + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_delete_ignore(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + option_id = svn_client_conflict_option_get_id(option); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + err = verify_local_state_for_incoming_delete(conflict, option, ctx, + scratch_pool); + if (err) + goto unlock_wc; + + /* Resolve to the current working copy state. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + + /* svn_wc__del_tree_conflict doesn't handle notification for us */ + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = option_id; + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_delete_accept(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + const char *parent_abspath; + const char *lock_abspath; + svn_error_t *err; + + option_id = svn_client_conflict_option_get_id(option); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Deleting a node requires a lock on the node's parent. */ + parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool); + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + parent_abspath, + scratch_pool, scratch_pool)); + + err = verify_local_state_for_incoming_delete(conflict, option, ctx, + scratch_pool); + if (err) + goto unlock_wc; + + /* Delete the tree conflict victim. Marks the conflict resolved. */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) + { + /* Not a versioned path. This can happen if the victim has already + * been deleted in our branche's history, for example. Either way, + * the item is gone, which is what we want, so don't treat this as + * a fatal error. */ + svn_error_clear(err); + + /* Resolve to current working copy state. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, + scratch_pool); + } + + if (err) + goto unlock_wc; + } + + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = option_id; + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + svn_wc_operation_t operation; + const char *lock_abspath; + svn_error_t *err; + const char *repos_root_url; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *wc_tmpdir; + const char *ancestor_abspath; + svn_stream_t *ancestor_stream; + apr_hash_t *ancestor_props; + apr_hash_t *victim_props; + apr_hash_t *move_target_props; + const char *ancestor_url; + const char *corrected_url; + svn_ra_session_t *ra_session; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_array_header_t *propdiffs; + struct conflict_tree_incoming_delete_details *details; + apr_array_header_t *possible_moved_to_abspaths; + const char *moved_to_abspath; + const char *incoming_abspath = NULL; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + operation = svn_client_conflict_get_operation(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->moves == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("The specified conflict resolution option " + "requires details for tree conflict at '%s' " + "to be fetched from the repository first."), + svn_dirent_local_style(local_abspath, + scratch_pool)); + if (operation == svn_wc_operation_none) + return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL, + _("Invalid operation code '%d' recorded for " + "conflict at '%s'"), operation, + svn_dirent_local_style(local_abspath, + scratch_pool)); + + option_id = svn_client_conflict_option_get_id(option); + SVN_ERR_ASSERT(option_id == + svn_client_conflict_option_incoming_move_file_text_merge || + option_id == + svn_client_conflict_option_incoming_move_dir_merge); + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + /* Set up temporary storage for the common ancestor version of the file. */ + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_open_unique(&ancestor_stream, + &ancestor_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Fetch the ancestor file's content. */ + ancestor_url = svn_path_url_add_component2(repos_root_url, + incoming_old_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + ancestor_url, NULL, NULL, + FALSE, FALSE, ctx, + scratch_pool, scratch_pool)); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, + ancestor_stream, NULL, /* fetched_rev */ + &ancestor_props, scratch_pool)); + filter_props(ancestor_props, scratch_pool); + + /* Close stream to flush ancestor file to disk. */ + SVN_ERR(svn_stream_close(ancestor_stream)); + + possible_moved_to_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, + details->wc_move_target_idx, + const char *); + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve( + &lock_abspath, ctx->wc_ctx, + svn_dirent_get_longest_ancestor(local_abspath, + moved_to_abspath, + scratch_pool), + scratch_pool, scratch_pool)); + + err = verify_local_state_for_incoming_delete(conflict, option, ctx, + scratch_pool); + if (err) + goto unlock_wc; + + /* Get a copy of the conflict victim's properties. */ + err = svn_wc_prop_list2(&victim_props, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + /* Get a copy of the move target's properties. */ + err = svn_wc_prop_list2(&move_target_props, ctx->wc_ctx, + moved_to_abspath, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + /* Create a property diff for the files. */ + err = svn_prop_diffs(&propdiffs, move_target_props, victim_props, + scratch_pool); + if (err) + goto unlock_wc; + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + svn_stream_t *working_stream; + svn_stream_t *incoming_stream; + + /* Create a temporary copy of the working file in repository-normal form. + * Set up this temporary file to be automatically removed. */ + err = svn_stream_open_unique(&incoming_stream, + &incoming_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_wc__translated_stream(&working_stream, ctx->wc_ctx, + local_abspath, local_abspath, + SVN_WC_TRANSLATE_TO_NF, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_stream_copy3(working_stream, incoming_stream, + NULL, NULL, /* no cancellation */ + scratch_pool); + if (err) + goto unlock_wc; + } + else if (operation == svn_wc_operation_merge) + { + svn_stream_t *incoming_stream; + svn_stream_t *move_target_stream; + + /* Set aside the current move target file. This is required to apply + * the move, and only then perform a three-way text merge between + * the ancestor's file, our working file (which we would move to + * the destination), and the file that we have set aside, which + * contains the incoming fulltext. + * Set up this temporary file to NOT be automatically removed. */ + err = svn_stream_open_unique(&incoming_stream, + &incoming_abspath, wc_tmpdir, + svn_io_file_del_none, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_wc__translated_stream(&move_target_stream, ctx->wc_ctx, + moved_to_abspath, moved_to_abspath, + SVN_WC_TRANSLATE_TO_NF, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_stream_copy3(move_target_stream, incoming_stream, + NULL, NULL, /* no cancellation */ + scratch_pool); + if (err) + goto unlock_wc; + + /* Apply the incoming move. */ + err = svn_io_remove_file2(moved_to_abspath, FALSE, scratch_pool); + if (err) + goto unlock_wc; + err = svn_wc__move2(ctx->wc_ctx, local_abspath, moved_to_abspath, + FALSE, /* ordinary (not meta-data only) move */ + FALSE, /* mixed-revisions don't apply to files */ + NULL, NULL, /* don't allow user to cancel here */ + NULL, NULL, /* no extra notification */ + scratch_pool); + if (err) + goto unlock_wc; + } + else + SVN_ERR_MALFUNCTION(); + + /* Perform the file merge. */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, ancestor_abspath, + incoming_abspath, moved_to_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + apr_hash_count(ancestor_props) ? ancestor_props : NULL, + propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + svn_io_sleep_for_timestamps(moved_to_abspath, scratch_pool); + if (err) + goto unlock_wc; + + if (operation == svn_wc_operation_merge && incoming_abspath) + { + err = svn_io_remove_file2(incoming_abspath, TRUE, scratch_pool); + if (err) + goto unlock_wc; + incoming_abspath = NULL; + } + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + /* Tell the world about the file merge that just happened. */ + notify = svn_wc_create_notify(moved_to_abspath, + svn_wc_notify_update_update, + scratch_pool); + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + /* Delete the tree conflict victim (clears the tree conflict marker). */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + NULL, NULL, /* no extra notification */ + scratch_pool); + if (err) + goto unlock_wc; + } + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = option_id; + +unlock_wc: + if (err && operation == svn_wc_operation_merge && incoming_abspath) + err = svn_error_quick_wrapf( + err, _("If needed, a backup copy of '%s' can be found at '%s'"), + svn_dirent_local_style(moved_to_abspath, scratch_pool), + svn_dirent_local_style(incoming_abspath, scratch_pool)); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_move_dir_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + svn_wc_operation_t operation; + const char *lock_abspath; + svn_error_t *err; + const char *repos_root_url; + const char *repos_uuid; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *victim_repos_relpath; + svn_revnum_t victim_peg_rev; + const char *moved_to_repos_relpath; + svn_revnum_t moved_to_peg_rev; + struct conflict_tree_incoming_delete_details *details; + apr_array_header_t *possible_moved_to_abspaths; + const char *moved_to_abspath; + svn_client__pathrev_t *yca_loc; + svn_opt_revision_t yca_opt_rev; + svn_client__conflict_report_t *conflict_report; + svn_boolean_t is_copy; + svn_boolean_t is_modified; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + operation = svn_client_conflict_get_operation(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->moves == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("The specified conflict resolution option " + "requires details for tree conflict at '%s' " + "to be fetched from the repository first."), + svn_dirent_local_style(local_abspath, + scratch_pool)); + + option_id = svn_client_conflict_option_get_id(option); + SVN_ERR_ASSERT(option_id == + svn_client_conflict_option_incoming_move_dir_merge); + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, &repos_uuid, + conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + /* Get repository location of the moved-away node (the conflict victim). */ + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + victim_repos_relpath = incoming_old_repos_relpath; + victim_peg_rev = incoming_old_pegrev; + } + else if (operation == svn_wc_operation_merge) + SVN_ERR(svn_wc__node_get_repos_info(&victim_peg_rev, &victim_repos_relpath, + NULL, NULL, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + /* Get repository location of the moved-here node (incoming move). */ + possible_moved_to_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, + details->wc_move_target_idx, + const char *); + + /* ### The following WC modifications should be atomic. */ + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve( + &lock_abspath, ctx->wc_ctx, + svn_dirent_get_longest_ancestor(local_abspath, + moved_to_abspath, + scratch_pool), + scratch_pool, scratch_pool)); + + err = svn_wc__node_get_origin(&is_copy, &moved_to_peg_rev, + &moved_to_repos_relpath, + NULL, NULL, NULL, NULL, + ctx->wc_ctx, moved_to_abspath, FALSE, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + if (!is_copy && operation == svn_wc_operation_merge) + { + err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected a copied item at '%s', but the " + "item is not a copy)"), + svn_dirent_local_style(local_abspath, + scratch_pool), + svn_dirent_local_style(moved_to_abspath, + scratch_pool)); + goto unlock_wc; + } + + if (moved_to_repos_relpath == NULL || moved_to_peg_rev == SVN_INVALID_REVNUM) + { + err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(could not determine origin of '%s')"), + svn_dirent_local_style(local_abspath, + scratch_pool), + svn_dirent_local_style(moved_to_abspath, + scratch_pool)); + goto unlock_wc; + } + + /* Now find the youngest common ancestor of these nodes. */ + err = find_yca(&yca_loc, victim_repos_relpath, victim_peg_rev, + moved_to_repos_relpath, moved_to_peg_rev, + repos_root_url, repos_uuid, + NULL, ctx, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + if (yca_loc == NULL) + { + err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(could not find common ancestor of '^/%s@%ld' " + " and '^/%s@%ld')"), + svn_dirent_local_style(local_abspath, + scratch_pool), + victim_repos_relpath, victim_peg_rev, + moved_to_repos_relpath, moved_to_peg_rev); + goto unlock_wc; + } + + yca_opt_rev.kind = svn_opt_revision_number; + yca_opt_rev.value.number = yca_loc->rev; + + err = verify_local_state_for_incoming_delete(conflict, option, ctx, + scratch_pool); + if (err) + goto unlock_wc; + + if (operation == svn_wc_operation_merge) + { + const char *move_target_url; + svn_opt_revision_t incoming_new_opt_rev; + + /* Revert the incoming move target directory. */ + SVN_ERR(svn_wc_revert5(ctx->wc_ctx, moved_to_abspath, svn_depth_infinity, + FALSE, NULL, TRUE, FALSE, + NULL, NULL, /* no cancellation */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool)); + + /* The move operation is not part of natural history. We must replicate + * this move in our history. Record a move in the working copy. */ + err = svn_wc__move2(ctx->wc_ctx, local_abspath, moved_to_abspath, + FALSE, /* this is not a meta-data only move */ + TRUE, /* allow mixed-revisions just in case */ + NULL, NULL, /* don't allow user to cancel here */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + + /* Merge YCA_URL@YCA_REV->MOVE_TARGET_URL@MERGE_RIGHT into move target. */ + move_target_url = apr_pstrcat(scratch_pool, repos_root_url, "/", + get_moved_to_repos_relpath(details, + scratch_pool), + SVN_VA_NULL); + incoming_new_opt_rev.kind = svn_opt_revision_number; + incoming_new_opt_rev.value.number = incoming_new_pegrev; + err = svn_client__merge_locked(&conflict_report, + yca_loc->url, &yca_opt_rev, + move_target_url, &incoming_new_opt_rev, + moved_to_abspath, svn_depth_infinity, + TRUE, TRUE, /* do a no-ancestry merge */ + FALSE, FALSE, FALSE, + TRUE, /* Allow mixed-rev just in case, + * since conflict victims can't be + * updated to straighten out + * mixed-rev trees. */ + NULL, ctx, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + } + else + { + SVN_ERR_ASSERT(operation == svn_wc_operation_update || + operation == svn_wc_operation_switch); + + /* Merge local modifications into the incoming move target dir. */ + err = svn_wc__has_local_mods(&is_modified, ctx->wc_ctx, local_abspath, + TRUE, ctx->cancel_func, ctx->cancel_baton, + scratch_pool); + if (err) + goto unlock_wc; + + if (is_modified) + { + err = svn_wc__conflict_tree_update_incoming_move(ctx->wc_ctx, + local_abspath, + moved_to_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + } + + /* The move operation is part of our natural history. + * Delete the tree conflict victim (clears the tree conflict marker). */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + NULL, NULL, /* no extra notification */ + scratch_pool); + if (err) + goto unlock_wc; + } + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = option_id; + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_local_move_file_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *lock_abspath; + svn_error_t *err; + const char *repos_root_url; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *wc_tmpdir; + const char *ancestor_tmp_abspath; + const char *incoming_tmp_abspath; + apr_hash_t *ancestor_props; + apr_hash_t *incoming_props; + svn_stream_t *stream; + const char *url; + const char *corrected_url; + const char *old_session_url; + svn_ra_session_t *ra_session; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_array_header_t *propdiffs; + struct conflict_tree_local_missing_details *details; + + details = conflict->tree_conflict_local_details; + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, + details->moved_to_abspath, + scratch_pool, scratch_pool)); + + /* Fetch the common ancestor file's content. */ + SVN_ERR(svn_stream_open_unique(&stream, &ancestor_tmp_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, + incoming_old_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, + FALSE, FALSE, ctx, + scratch_pool, scratch_pool)); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, stream, NULL, + &ancestor_props, scratch_pool)); + filter_props(ancestor_props, scratch_pool); + + /* Close stream to flush the file to disk. */ + SVN_ERR(svn_stream_close(stream)); + + /* Do the same for the incoming file's content. */ + SVN_ERR(svn_stream_open_unique(&stream, &incoming_tmp_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, + incoming_new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session, + url, scratch_pool)); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, stream, NULL, + &incoming_props, scratch_pool)); + /* Close stream to flush the file to disk. */ + SVN_ERR(svn_stream_close(stream)); + + filter_props(incoming_props, scratch_pool); + + /* Create a property diff for the files. */ + SVN_ERR(svn_prop_diffs(&propdiffs, incoming_props, ancestor_props, + scratch_pool)); + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve( + &lock_abspath, ctx->wc_ctx, + svn_dirent_get_longest_ancestor(conflict->local_abspath, + details->moved_to_abspath, + scratch_pool), + scratch_pool, scratch_pool)); + + /* Perform the file merge. */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, + ancestor_tmp_abspath, incoming_tmp_abspath, + details->moved_to_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + apr_hash_count(ancestor_props) ? ancestor_props : NULL, + propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + svn_io_sleep_for_timestamps(details->moved_to_abspath, scratch_pool); + if (err) + return svn_error_compose_create(err, + svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + + err = svn_wc__del_tree_conflict(ctx->wc_ctx, conflict->local_abspath, + scratch_pool); + err = svn_error_compose_create(err, + svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + if (err) + return svn_error_trace(err); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + /* Tell the world about the file merge that just happened. */ + notify = svn_wc_create_notify(details->moved_to_abspath, + svn_wc_notify_update_update, + scratch_pool); + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + + /* And also about the successfully resolved tree conflict. */ + notify = svn_wc_create_notify(conflict->local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +static svn_error_t * +assert_text_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) +{ + svn_boolean_t text_conflicted; + + SVN_ERR(svn_client_conflict_get_conflicted(&text_conflicted, NULL, NULL, + conflict, scratch_pool, + scratch_pool)); + + SVN_ERR_ASSERT(text_conflicted); /* ### return proper error? */ + + return SVN_NO_ERROR; +} + +static svn_error_t * +assert_prop_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) +{ + apr_array_header_t *props_conflicted; + + SVN_ERR(svn_client_conflict_get_conflicted(NULL, &props_conflicted, NULL, + conflict, scratch_pool, + scratch_pool)); + + /* ### return proper error? */ + SVN_ERR_ASSERT(props_conflicted && props_conflicted->nelts > 0); + + return SVN_NO_ERROR; +} + +static svn_error_t * +assert_tree_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) +{ + svn_boolean_t tree_conflicted; + + SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted, + conflict, scratch_pool, + scratch_pool)); + + SVN_ERR_ASSERT(tree_conflicted); /* ### return proper error? */ + + return SVN_NO_ERROR; +} + +/* Helper to add to conflict resolution option to array of OPTIONS. + * Resolution option object will be allocated from OPTIONS->POOL + * and DESCRIPTION will be copied to this pool. + * Returns pointer to the created conflict resolution option. */ +static svn_client_conflict_option_t * +add_resolution_option(apr_array_header_t *options, + svn_client_conflict_t *conflict, + svn_client_conflict_option_id_t id, + const char *label, + const char *description, + conflict_option_resolve_func_t resolve_func) +{ + svn_client_conflict_option_t *option; + + option = apr_pcalloc(options->pool, sizeof(*option)); + option->pool = options->pool; + option->id = id; + option->label = apr_pstrdup(option->pool, label); + option->description = apr_pstrdup(option->pool, description); + option->conflict = conflict; + option->do_resolve_func = resolve_func; + + APR_ARRAY_PUSH(options, const svn_client_conflict_option_t *) = option; + + return option; +} + +svn_error_t * +svn_client_conflict_text_get_resolution_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 char *mime_type; + + SVN_ERR(assert_text_conflict(conflict, scratch_pool)); + + *options = apr_array_make(result_pool, 7, + sizeof(svn_client_conflict_option_t *)); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_postpone, + _("Postpone"), + _("skip this conflict and leave it unresolved"), + resolve_postpone); + + mime_type = svn_client_conflict_text_get_mime_type(conflict); + if (mime_type && svn_mime_type_is_binary(mime_type)) + { + /* Resolver options for a binary file conflict. */ + add_resolution_option(*options, conflict, + svn_client_conflict_option_base_text, + _("Accept base"), + _("discard local and incoming changes for this binary file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text, + _("Accept incoming"), + _("accept incoming version of binary file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text, + _("Mark as resolved"), + _("accept binary file as it appears in the working copy"), + resolve_text_conflict); + } + else + { + /* Resolver options for a text file conflict. */ + add_resolution_option(*options, conflict, + svn_client_conflict_option_base_text, + _("Accept base"), + _("discard local and incoming changes for this file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text, + _("Accept incoming"), + _("accept incoming version of entire file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text, + _("Reject incoming"), + _("reject all incoming changes for this file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text_where_conflicted, + _("Accept incoming for conflicts"), + _("accept changes only where they conflict"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text_where_conflicted, + _("Reject conflicts"), + _("reject changes which conflict and accept the rest"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_merged_text, + _("Mark as resolved"), + _("accept the file as it appears in the working copy"), + resolve_text_conflict); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_prop_get_resolution_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) +{ + SVN_ERR(assert_prop_conflict(conflict, scratch_pool)); + + *options = apr_array_make(result_pool, 7, + sizeof(svn_client_conflict_option_t *)); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_postpone, + _("Postpone"), + _("skip this conflict and leave it unresolved"), + resolve_postpone); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_base_text, + _("Accept base"), + _("discard local and incoming changes for this property"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text, + _("Accept incoming"), + _("accept incoming version of entire property value"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text, + _("Mark as resolved"), + _("accept working copy version of entire property value"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text_where_conflicted, + _("Accept incoming for conflicts"), + _("accept incoming changes only where they conflict"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text_where_conflicted, + _("Reject conflicts"), + _("reject changes which conflict and accept the rest"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_merged_text, + _("Accept merged"), + _("accept merged version of property value"), + resolve_prop_conflict); + + return SVN_NO_ERROR; +} + +/* Configure 'accept current wc state' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_accept_current_wc_state(svn_client_conflict_t *conflict, + apr_array_header_t *options) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + conflict_option_resolve_func_t do_resolve_func; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + + if ((operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) && + (local_change == svn_wc_conflict_reason_moved_away || + local_change == svn_wc_conflict_reason_deleted || + local_change == svn_wc_conflict_reason_replaced) && + incoming_change == svn_wc_conflict_action_edit) + { + /* We must break moves if the user accepts the current working copy + * state instead of updating a moved-away node or updating children + * moved outside of deleted or replaced directory nodes. + * Else such moves would be left in an invalid state. */ + do_resolve_func = resolve_update_break_moved_away; + } + else + do_resolve_func = resolve_accept_current_wc_state; + + add_resolution_option(options, conflict, + svn_client_conflict_option_accept_current_wc_state, + _("Mark as resolved"), + _("accept current working copy state"), + do_resolve_func); + + return SVN_NO_ERROR; +} + +/* Configure 'update move destination' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_update_move_destination(svn_client_conflict_t *conflict, + apr_array_header_t *options) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + + if ((operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) && + incoming_change == svn_wc_conflict_action_edit && + local_change == svn_wc_conflict_reason_moved_away) + { + add_resolution_option( + options, conflict, + svn_client_conflict_option_update_move_destination, + _("Update move destination"), + _("apply incoming changes to move destination"), + resolve_update_moved_away_node); + } + + return SVN_NO_ERROR; +} + +/* Configure 'update raise moved away children' resolution option for a tree + * conflict. */ +static svn_error_t * +configure_option_update_raise_moved_away_children( + svn_client_conflict_t *conflict, + apr_array_header_t *options) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + + if ((operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) && + incoming_change == svn_wc_conflict_action_edit && + (local_change == svn_wc_conflict_reason_deleted || + local_change == svn_wc_conflict_reason_replaced) && + victim_node_kind == svn_node_dir) + { + add_resolution_option( + options, conflict, + svn_client_conflict_option_update_any_moved_away_children, + _("Update any moved-away children"), + _("prepare for updating moved-away children, if any"), + resolve_update_raise_moved_away); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming add ignore' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_incoming_add_ignore(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t victim_node_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + /* This option is only available for directories. */ + if (victim_node_kind == svn_node_dir && + incoming_change == svn_wc_conflict_action_add && + (local_change == svn_wc_conflict_reason_obstructed || + local_change == svn_wc_conflict_reason_added)) + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + if (operation == svn_wc_operation_merge) + description = + apr_psprintf(scratch_pool, + _("ignore and do not add '^/%s@%ld' here"), + incoming_new_repos_relpath, incoming_new_pegrev); + else if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (victim_node_kind == svn_node_file) + description = + apr_psprintf(scratch_pool, + _("replace '^/%s@%ld' with the locally added file"), + incoming_new_repos_relpath, incoming_new_pegrev); + else if (victim_node_kind == svn_node_dir) + description = + apr_psprintf(scratch_pool, + _("replace '^/%s@%ld' with the locally added " + "directory"), + incoming_new_repos_relpath, incoming_new_pegrev); + else + description = + apr_psprintf(scratch_pool, + _("replace '^/%s@%ld' with the locally added item"), + incoming_new_repos_relpath, incoming_new_pegrev); + } + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("unexpected operation code '%d'"), + operation); + add_resolution_option( + options, conflict, svn_client_conflict_option_incoming_add_ignore, + _("Ignore incoming addition"), description, resolve_incoming_add_ignore); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added file text merge' resolution option for a tree + * conflict. */ +static svn_error_t * +configure_option_incoming_added_file_text_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (victim_node_kind == svn_node_file && + incoming_new_kind == svn_node_file && + incoming_change == svn_wc_conflict_action_add && + (local_change == svn_wc_conflict_reason_obstructed || + local_change == svn_wc_conflict_reason_added)) + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge) + description = + apr_psprintf(scratch_pool, _("merge '^/%s@%ld' into '%s'"), + incoming_new_repos_relpath, incoming_new_pegrev, + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool)); + else + description = + apr_psprintf(scratch_pool, _("merge local '%s' and '^/%s@%ld'"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_added_file_text_merge, + _("Merge the files"), description, + operation == svn_wc_operation_merge + ? resolve_merge_incoming_added_file_text_merge + : resolve_merge_incoming_added_file_text_update); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added file replace and merge' resolution option for a + * tree conflict. */ +static svn_error_t * +configure_option_incoming_added_file_replace_and_merge( + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge && + victim_node_kind == svn_node_file && + incoming_new_kind == svn_node_file && + incoming_change == svn_wc_conflict_action_add && + local_change == svn_wc_conflict_reason_obstructed) + { + const char *wcroot_abspath; + const char *description; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + description = + apr_psprintf(scratch_pool, + _("delete '%s', copy '^/%s@%ld' here, and merge the files"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_added_file_replace_and_merge, + _("Replace and merge"), + description, resolve_merge_incoming_added_file_replace_and_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added dir merge' resolution option for a tree + * conflict. */ +static svn_error_t * +configure_option_incoming_added_dir_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (victim_node_kind == svn_node_dir && + incoming_new_kind == svn_node_dir && + incoming_change == svn_wc_conflict_action_add && + (local_change == svn_wc_conflict_reason_added || + (operation == svn_wc_operation_merge && + local_change == svn_wc_conflict_reason_obstructed))) + + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + if (operation == svn_wc_operation_merge) + description = + apr_psprintf(scratch_pool, _("merge '^/%s@%ld' into '%s'"), + incoming_new_repos_relpath, incoming_new_pegrev, + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool)); + else + description = + apr_psprintf(scratch_pool, _("merge local '%s' and '^/%s@%ld'"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option(options, conflict, + svn_client_conflict_option_incoming_added_dir_merge, + _("Merge the directories"), description, + operation == svn_wc_operation_merge + ? resolve_merge_incoming_added_dir_merge + : resolve_update_incoming_added_dir_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added dir replace' resolution option for a tree + * conflict. */ +static svn_error_t * +configure_option_incoming_added_dir_replace(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge && + victim_node_kind == svn_node_dir && + incoming_new_kind == svn_node_dir && + incoming_change == svn_wc_conflict_action_add && + local_change == svn_wc_conflict_reason_obstructed) + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + description = + apr_psprintf(scratch_pool, _("delete '%s' and copy '^/%s@%ld' here"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_added_dir_replace, + _("Delete my directory and replace it with incoming directory"), + description, resolve_merge_incoming_added_dir_replace); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added dir replace and merge' resolution option + * for a tree conflict. */ +static svn_error_t * +configure_option_incoming_added_dir_replace_and_merge( + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge && + victim_node_kind == svn_node_dir && + incoming_new_kind == svn_node_dir && + incoming_change == svn_wc_conflict_action_add && + local_change == svn_wc_conflict_reason_obstructed) + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + description = + apr_psprintf(scratch_pool, + _("delete '%s', copy '^/%s@%ld' here, and merge the directories"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_added_dir_replace_and_merge, + _("Replace and merge"), + description, resolve_merge_incoming_added_dir_replace_and_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming delete ignore' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_incoming_delete_ignore(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + if (incoming_change == svn_wc_conflict_action_delete) + { + const char *description; + struct conflict_tree_incoming_delete_details *incoming_details; + svn_boolean_t is_incoming_move; + + incoming_details = conflict->tree_conflict_incoming_details; + is_incoming_move = (incoming_details != NULL && + incoming_details->moves != NULL); + if (local_change == svn_wc_conflict_reason_moved_away || + local_change == svn_wc_conflict_reason_edited) + { + /* An option which ignores the incoming deletion makes no sense + * if we know there was a local move and/or an incoming move. */ + if (is_incoming_move) + return SVN_NO_ERROR; + } + else if (local_change == svn_wc_conflict_reason_deleted) + { + /* If the local item was deleted and conflict details were fetched + * and indicate that there was no move, then this is an actual + * 'delete vs delete' situation. An option which ignores the incoming + * deletion makes no sense in that case because there is no local + * node to preserve. */ + if (!is_incoming_move) + return SVN_NO_ERROR; + } + else if (local_change == svn_wc_conflict_reason_missing && + operation == svn_wc_operation_merge) + { + struct conflict_tree_local_missing_details *local_details; + svn_boolean_t is_local_move; /* "local" to branch history */ + + local_details = conflict->tree_conflict_local_details; + is_local_move = (local_details != NULL && + local_details->moves != NULL); + + if (!is_incoming_move && !is_local_move) + return SVN_NO_ERROR; + } + + description = + apr_psprintf(scratch_pool, _("ignore the deletion of '^/%s@%ld'"), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option(options, conflict, + svn_client_conflict_option_incoming_delete_ignore, + _("Ignore incoming deletion"), description, + resolve_incoming_delete_ignore); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming delete accept' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_incoming_delete_accept(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + if (incoming_change == svn_wc_conflict_action_delete) + { + struct conflict_tree_incoming_delete_details *incoming_details; + svn_boolean_t is_incoming_move; + + incoming_details = conflict->tree_conflict_incoming_details; + is_incoming_move = (incoming_details != NULL && + incoming_details->moves != NULL); + if (is_incoming_move && + (local_change == svn_wc_conflict_reason_edited || + local_change == svn_wc_conflict_reason_moved_away)) + { + /* An option which accepts the incoming deletion makes no sense + * if we know there was a local move and/or an incoming move. */ + return SVN_NO_ERROR; + } + else + { + const char *description; + const char *wcroot_abspath; + const char *local_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + description = + apr_psprintf(scratch_pool, _("accept the deletion of '%s'"), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + local_abspath), + scratch_pool)); + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_delete_accept, + _("Accept incoming deletion"), description, + resolve_incoming_delete_accept); + } + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +describe_incoming_move_merge_conflict_option( + const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + struct conflict_tree_incoming_delete_details *details, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *move_target_wc_abspaths; + svn_wc_operation_t operation; + const char *victim_abspath; + const char *moved_to_abspath; + const char *wcroot_abspath; + + move_target_wc_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, + details->wc_move_target_idx, + const char *); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + victim_abspath, scratch_pool, + scratch_pool)); + + operation = svn_client_conflict_get_operation(conflict); + if (operation == svn_wc_operation_merge) + *description = + apr_psprintf( + result_pool, _("move '%s' to '%s' and merge"), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + victim_abspath), + scratch_pool), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + moved_to_abspath), + scratch_pool)); + else + *description = + apr_psprintf( + result_pool, _("move and merge local changes from '%s' into '%s'"), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + victim_abspath), + scratch_pool), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + moved_to_abspath), + scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Configure 'incoming move file merge' resolution option for + * a tree conflict. */ +static svn_error_t * +configure_option_incoming_move_file_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_node_kind_t victim_node_kind; + svn_wc_conflict_action_t incoming_change; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + svn_node_kind_t incoming_old_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + incoming_change = svn_client_conflict_get_incoming_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + &incoming_old_kind, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (victim_node_kind == svn_node_file && + incoming_old_kind == svn_node_file && + incoming_new_kind == svn_node_none && + incoming_change == svn_wc_conflict_action_delete) + { + struct conflict_tree_incoming_delete_details *details; + const char *description; + + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->moves == NULL) + return SVN_NO_ERROR; + + if (apr_hash_count(details->wc_move_targets) == 0) + return SVN_NO_ERROR; + + SVN_ERR(describe_incoming_move_merge_conflict_option(&description, + conflict, ctx, + details, + scratch_pool, + scratch_pool)); + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_move_file_text_merge, + _("Move and merge"), description, + resolve_incoming_move_file_text_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming move dir merge' resolution option for + * a tree conflict. */ +static svn_error_t * +configure_option_incoming_dir_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_node_kind_t victim_node_kind; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + svn_node_kind_t incoming_old_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + &incoming_old_kind, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (victim_node_kind == svn_node_dir && + incoming_old_kind == svn_node_dir && + incoming_new_kind == svn_node_none && + incoming_change == svn_wc_conflict_action_delete && + local_change == svn_wc_conflict_reason_edited) + { + struct conflict_tree_incoming_delete_details *details; + const char *description; + + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->moves == NULL) + return SVN_NO_ERROR; + + if (apr_hash_count(details->wc_move_targets) == 0) + return SVN_NO_ERROR; + + SVN_ERR(describe_incoming_move_merge_conflict_option(&description, + conflict, ctx, + details, + scratch_pool, + scratch_pool)); + add_resolution_option(options, conflict, + svn_client_conflict_option_incoming_move_dir_merge, + _("Move and merge"), description, + resolve_incoming_move_dir_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'local move file merge' resolution option for + * a tree conflict. */ +static svn_error_t * +configure_option_local_move_file_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge && + incoming_change == svn_wc_conflict_action_edit && + local_change == svn_wc_conflict_reason_missing) + { + struct conflict_tree_local_missing_details *details; + + details = conflict->tree_conflict_local_details; + if (details != NULL && details->moves != NULL) + { + apr_hash_t *wc_move_targets = apr_hash_make(scratch_pool); + apr_pool_t *iterpool; + int i; + + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < details->moves->nelts; i++) + { + struct repos_move_info *move; + + svn_pool_clear(iterpool); + move = APR_ARRAY_IDX(details->moves, i, struct repos_move_info *); + SVN_ERR(follow_move_chains(wc_move_targets, move, ctx, + conflict->local_abspath, + svn_node_file, + incoming_new_repos_relpath, + incoming_new_pegrev, + scratch_pool, iterpool)); + } + svn_pool_destroy(iterpool); + + if (apr_hash_count(wc_move_targets) > 0) + { + apr_array_header_t *move_target_repos_relpaths; + const svn_sort__item_t *item; + apr_array_header_t *moved_to_abspaths; + const char *description; + const char *wcroot_abspath; + + /* Initialize to the first possible move target. Hopefully, + * in most cases there will only be one candidate anyway. */ + move_target_repos_relpaths = svn_sort__hash( + wc_move_targets, + svn_sort_compare_items_as_paths, + scratch_pool); + item = &APR_ARRAY_IDX(move_target_repos_relpaths, + 0, svn_sort__item_t); + moved_to_abspaths = item->value; + details->moved_to_abspath = + apr_pstrdup(conflict->pool, + APR_ARRAY_IDX(moved_to_abspaths, 0, const char *)); + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, scratch_pool)); + description = + apr_psprintf( + scratch_pool, _("apply changes to move destination '%s'"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + details->moved_to_abspath), + scratch_pool)); + + add_resolution_option( + options, conflict, + svn_client_conflict_option_local_move_file_text_merge, + _("Apply to move destination"), + description, resolve_local_move_file_merge); + } + else + details->moved_to_abspath = NULL; + } + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_option_get_moved_to_repos_relpath_candidates( + apr_array_header_t **possible_moved_to_repos_relpaths, + svn_client_conflict_option_t *option, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_t *conflict = option->conflict; + struct conflict_tree_incoming_delete_details *details; + const char *victim_abspath; + apr_array_header_t *sorted_repos_relpaths; + int i; + + SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_file_text_merge || + svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_dir_merge); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->wc_move_targets == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Getting a list of possible move targets " + "requires details for tree conflict at '%s' " + "to be fetched from the repository first"), + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + /* Return a copy of the repos replath candidate list. */ + sorted_repos_relpaths = svn_sort__hash(details->wc_move_targets, + svn_sort_compare_items_as_paths, + scratch_pool); + + *possible_moved_to_repos_relpaths = apr_array_make( + result_pool, + sorted_repos_relpaths->nelts, + sizeof (const char *)); + for (i = 0; i < sorted_repos_relpaths->nelts; i++) + { + svn_sort__item_t item; + const char *repos_relpath; + + item = APR_ARRAY_IDX(sorted_repos_relpaths, i, svn_sort__item_t); + repos_relpath = item.key; + APR_ARRAY_PUSH(*possible_moved_to_repos_relpaths, const char *) = + apr_pstrdup(result_pool, repos_relpath); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_option_set_moved_to_repos_relpath( + svn_client_conflict_option_t *option, + int preferred_move_target_idx, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_t *conflict = option->conflict; + struct conflict_tree_incoming_delete_details *details; + const char *victim_abspath; + apr_array_header_t *move_target_repos_relpaths; + svn_sort__item_t item; + const char *move_target_repos_relpath; + apr_hash_index_t *hi; + + SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_file_text_merge || + svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_dir_merge); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->wc_move_targets == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Setting a move target requires details " + "for tree conflict at '%s' to be fetched " + "from the repository first"), + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + if (preferred_move_target_idx < 0 || + preferred_move_target_idx >= apr_hash_count(details->wc_move_targets)) + return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Index '%d' is out of bounds of the possible " + "move target list for '%s'"), + preferred_move_target_idx, + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + /* Translate the index back into a hash table key. */ + move_target_repos_relpaths = + svn_sort__hash(details->wc_move_targets, + svn_sort_compare_items_as_paths, + scratch_pool); + item = APR_ARRAY_IDX(move_target_repos_relpaths, preferred_move_target_idx, + svn_sort__item_t); + move_target_repos_relpath = item.key; + /* Find our copy of the hash key and remember the user's preference. */ + for (hi = apr_hash_first(scratch_pool, details->wc_move_targets); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *repos_relpath = apr_hash_this_key(hi); + + if (strcmp(move_target_repos_relpath, repos_relpath) == 0) + { + details->move_target_repos_relpath = repos_relpath; + /* Update option description. */ + SVN_ERR(describe_incoming_move_merge_conflict_option( + &option->description, + conflict, ctx, + details, + conflict->pool, + scratch_pool)); + + return SVN_NO_ERROR; + } + } + + return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Repository path '%s' not found in list of " + "possible move targets for '%s'"), + move_target_repos_relpath, + svn_dirent_local_style(victim_abspath, + scratch_pool)); +} + +svn_error_t * +svn_client_conflict_option_get_moved_to_abspath_candidates( + apr_array_header_t **possible_moved_to_abspaths, + svn_client_conflict_option_t *option, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_t *conflict = option->conflict; + struct conflict_tree_incoming_delete_details *details; + const char *victim_abspath; + apr_array_header_t *move_target_wc_abspaths; + int i; + + SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_file_text_merge || + svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_dir_merge); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->wc_move_targets == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Getting a list of possible move targets " + "requires details for tree conflict at '%s' " + "to be fetched from the repository first"), + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + move_target_wc_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + + /* Return a copy of the option's move target candidate list. */ + *possible_moved_to_abspaths = + apr_array_make(result_pool, move_target_wc_abspaths->nelts, + sizeof (const char *)); + for (i = 0; i < move_target_wc_abspaths->nelts; i++) + { + const char *moved_to_abspath; + + moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, i, + const char *); + APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) = + apr_pstrdup(result_pool, moved_to_abspath); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_option_set_moved_to_abspath( + svn_client_conflict_option_t *option, + int preferred_move_target_idx, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_t *conflict = option->conflict; + struct conflict_tree_incoming_delete_details *details; + const char *victim_abspath; + apr_array_header_t *move_target_wc_abspaths; + + SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_file_text_merge || + svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_dir_merge); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->wc_move_targets == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Setting a move target requires details " + "for tree conflict at '%s' to be fetched " + "from the repository first"), + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + move_target_wc_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + + if (preferred_move_target_idx < 0 || + preferred_move_target_idx > move_target_wc_abspaths->nelts) + return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Index '%d' is out of bounds of the possible " + "move target list for '%s'"), + preferred_move_target_idx, + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + /* Record the user's preference. */ + details->wc_move_target_idx = preferred_move_target_idx; + + /* Update option description. */ + SVN_ERR(describe_incoming_move_merge_conflict_option(&option->description, + conflict, ctx, + details, + conflict->pool, + scratch_pool)); + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_tree_get_resolution_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) +{ + SVN_ERR(assert_tree_conflict(conflict, scratch_pool)); + + *options = apr_array_make(result_pool, 2, + sizeof(svn_client_conflict_option_t *)); + + /* Add postpone option. */ + add_resolution_option(*options, conflict, + svn_client_conflict_option_postpone, + _("Postpone"), + _("skip this conflict and leave it unresolved"), + resolve_postpone); + + /* Add an option which marks the conflict resolved. */ + SVN_ERR(configure_option_accept_current_wc_state(conflict, *options)); + + /* Configure options which offer automatic resolution. */ + SVN_ERR(configure_option_update_move_destination(conflict, *options)); + SVN_ERR(configure_option_update_raise_moved_away_children(conflict, + *options)); + SVN_ERR(configure_option_incoming_add_ignore(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_file_text_merge(conflict, ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_file_replace_and_merge(conflict, + ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_dir_merge(conflict, ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_dir_replace(conflict, ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_dir_replace_and_merge(conflict, + ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_delete_ignore(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_delete_accept(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_move_file_merge(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_dir_merge(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_local_move_file_merge(conflict, ctx, *options, + scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Swallow authz failures and return SVN_NO_ERROR in that case. + * Otherwise, return ERR unchanged. */ +static svn_error_t * +ignore_authz_failures(svn_error_t *err) +{ + if (err && ( svn_error_find_cause(err, SVN_ERR_AUTHZ_UNREADABLE) + || svn_error_find_cause(err, SVN_ERR_RA_NOT_AUTHORIZED) + || svn_error_find_cause(err, SVN_ERR_RA_DAV_FORBIDDEN))) + { + svn_error_clear(err); + err = SVN_NO_ERROR; + } + + return err; +} + +svn_error_t * +svn_client_conflict_tree_get_details(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_tree_conflict(conflict, scratch_pool)); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + svn_client_conflict_get_local_abspath(conflict), + svn_wc_notify_begin_search_tree_conflict_details, + scratch_pool), + ctx->notify_func2(ctx->notify_baton2, notify, + scratch_pool); + } + + /* Collecting conflict details may fail due to insufficient access rights. + * This is not a failure but simply restricts our future options. */ + if (conflict->tree_conflict_get_incoming_details_func) + SVN_ERR(ignore_authz_failures( + conflict->tree_conflict_get_incoming_details_func(conflict, ctx, + scratch_pool))); + + + if (conflict->tree_conflict_get_local_details_func) + SVN_ERR(ignore_authz_failures( + conflict->tree_conflict_get_local_details_func(conflict, ctx, + scratch_pool))); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + svn_client_conflict_get_local_abspath(conflict), + svn_wc_notify_end_search_tree_conflict_details, + scratch_pool), + ctx->notify_func2(ctx->notify_baton2, notify, + scratch_pool); + } + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_id_t +svn_client_conflict_option_get_id(svn_client_conflict_option_t *option) +{ + return option->id; +} + +const char * +svn_client_conflict_option_get_label(svn_client_conflict_option_t *option, + apr_pool_t *result_pool) +{ + return apr_pstrdup(result_pool, option->label); +} + +const char * +svn_client_conflict_option_get_description(svn_client_conflict_option_t *option, + apr_pool_t *result_pool) +{ + return apr_pstrdup(result_pool, option->description); +} + +svn_client_conflict_option_id_t +svn_client_conflict_get_recommended_option_id(svn_client_conflict_t *conflict) +{ + return conflict->recommended_option_id; +} + +svn_error_t * +svn_client_conflict_text_resolve(svn_client_conflict_t *conflict, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_text_conflict(conflict, scratch_pool)); + SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_t * +svn_client_conflict_option_find_by_id(apr_array_header_t *options, + svn_client_conflict_option_id_t option_id) +{ + int i; + + for (i = 0; i < options->nelts; i++) + { + svn_client_conflict_option_t *this_option; + svn_client_conflict_option_id_t this_option_id; + + this_option = APR_ARRAY_IDX(options, i, svn_client_conflict_option_t *); + this_option_id = svn_client_conflict_option_get_id(this_option); + + if (this_option_id == option_id) + return this_option; + } + + return NULL; +} + +svn_error_t * +svn_client_conflict_text_resolve_by_id( + svn_client_conflict_t *conflict, + svn_client_conflict_option_id_t option_id, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *resolution_options; + svn_client_conflict_option_t *option; + + SVN_ERR(svn_client_conflict_text_get_resolution_options( + &resolution_options, conflict, ctx, + scratch_pool, scratch_pool)); + option = svn_client_conflict_option_find_by_id(resolution_options, + option_id); + if (option == NULL) + return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE, + NULL, + _("Inapplicable conflict resolution option " + "given for conflicted path '%s'"), + svn_dirent_local_style(conflict->local_abspath, + scratch_pool)); + + SVN_ERR(svn_client_conflict_text_resolve(conflict, option, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_id_t +svn_client_conflict_text_get_resolution(svn_client_conflict_t *conflict) +{ + return conflict->resolution_text; +} + +svn_error_t * +svn_client_conflict_prop_resolve(svn_client_conflict_t *conflict, + const char *propname, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_prop_conflict(conflict, scratch_pool)); + option->type_data.prop.propname = propname; + SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_prop_resolve_by_id( + svn_client_conflict_t *conflict, + const char *propname, + svn_client_conflict_option_id_t option_id, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *resolution_options; + svn_client_conflict_option_t *option; + + SVN_ERR(svn_client_conflict_prop_get_resolution_options( + &resolution_options, conflict, ctx, + scratch_pool, scratch_pool)); + option = svn_client_conflict_option_find_by_id(resolution_options, + option_id); + if (option == NULL) + return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE, + NULL, + _("Inapplicable conflict resolution option " + "given for conflicted path '%s'"), + svn_dirent_local_style(conflict->local_abspath, + scratch_pool)); + SVN_ERR(svn_client_conflict_prop_resolve(conflict, propname, option, ctx, + scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_id_t +svn_client_conflict_prop_get_resolution(svn_client_conflict_t *conflict, + const char *propname) +{ + svn_client_conflict_option_t *option; + + option = svn_hash_gets(conflict->resolved_props, propname); + if (option == NULL) + return svn_client_conflict_option_unspecified; + + return svn_client_conflict_option_get_id(option); +} + +svn_error_t * +svn_client_conflict_tree_resolve(svn_client_conflict_t *conflict, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_tree_conflict(conflict, scratch_pool)); + SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_tree_resolve_by_id( + svn_client_conflict_t *conflict, + svn_client_conflict_option_id_t option_id, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *resolution_options; + svn_client_conflict_option_t *option; + + SVN_ERR(svn_client_conflict_tree_get_resolution_options( + &resolution_options, conflict, ctx, + scratch_pool, scratch_pool)); + option = svn_client_conflict_option_find_by_id(resolution_options, + option_id); + if (option == NULL) + return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE, + NULL, + _("Inapplicable conflict resolution option " + "given for conflicted path '%s'"), + svn_dirent_local_style(conflict->local_abspath, + scratch_pool)); + SVN_ERR(svn_client_conflict_tree_resolve(conflict, option, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_id_t +svn_client_conflict_tree_get_resolution(svn_client_conflict_t *conflict) +{ + return conflict->resolution_tree; +} + +/* Return the legacy conflict descriptor which is wrapped by CONFLICT. */ +static const svn_wc_conflict_description2_t * +get_conflict_desc2_t(svn_client_conflict_t *conflict) +{ + if (conflict->legacy_text_conflict) + return conflict->legacy_text_conflict; + + if (conflict->legacy_tree_conflict) + return conflict->legacy_tree_conflict; + + if (conflict->prop_conflicts && conflict->legacy_prop_conflict_propname) + return svn_hash_gets(conflict->prop_conflicts, + conflict->legacy_prop_conflict_propname); + + return NULL; +} + +svn_error_t * +svn_client_conflict_get_conflicted(svn_boolean_t *text_conflicted, + apr_array_header_t **props_conflicted, + svn_boolean_t *tree_conflicted, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (text_conflicted) + *text_conflicted = (conflict->legacy_text_conflict != NULL); + + if (props_conflicted) + { + if (conflict->prop_conflicts) + SVN_ERR(svn_hash_keys(props_conflicted, conflict->prop_conflicts, + result_pool)); + else + *props_conflicted = apr_array_make(result_pool, 0, + sizeof(const char*)); + } + + if (tree_conflicted) + *tree_conflicted = (conflict->legacy_tree_conflict != NULL); + + return SVN_NO_ERROR; +} + +const char * +svn_client_conflict_get_local_abspath(svn_client_conflict_t *conflict) +{ + return conflict->local_abspath; +} + +svn_wc_operation_t +svn_client_conflict_get_operation(svn_client_conflict_t *conflict) +{ + return get_conflict_desc2_t(conflict)->operation; +} + +svn_wc_conflict_action_t +svn_client_conflict_get_incoming_change(svn_client_conflict_t *conflict) +{ + return get_conflict_desc2_t(conflict)->action; +} + +svn_wc_conflict_reason_t +svn_client_conflict_get_local_change(svn_client_conflict_t *conflict) +{ + return get_conflict_desc2_t(conflict)->reason; +} + +svn_error_t * +svn_client_conflict_get_repos_info(const char **repos_root_url, + const char **repos_uuid, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (repos_root_url) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *repos_root_url = + get_conflict_desc2_t(conflict)->src_left_version->repos_url; + else if (get_conflict_desc2_t(conflict)->src_right_version) + *repos_root_url = + get_conflict_desc2_t(conflict)->src_right_version->repos_url; + else + *repos_root_url = NULL; + } + + if (repos_uuid) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *repos_uuid = + get_conflict_desc2_t(conflict)->src_left_version->repos_uuid; + else if (get_conflict_desc2_t(conflict)->src_right_version) + *repos_uuid = + get_conflict_desc2_t(conflict)->src_right_version->repos_uuid; + else + *repos_uuid = NULL; + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_get_incoming_old_repos_location( + const char **incoming_old_repos_relpath, + svn_revnum_t *incoming_old_pegrev, + svn_node_kind_t *incoming_old_node_kind, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (incoming_old_repos_relpath) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *incoming_old_repos_relpath = + get_conflict_desc2_t(conflict)->src_left_version->path_in_repos; + else + *incoming_old_repos_relpath = NULL; + } + + if (incoming_old_pegrev) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *incoming_old_pegrev = + get_conflict_desc2_t(conflict)->src_left_version->peg_rev; + else + *incoming_old_pegrev = SVN_INVALID_REVNUM; + } + + if (incoming_old_node_kind) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *incoming_old_node_kind = + get_conflict_desc2_t(conflict)->src_left_version->node_kind; + else + *incoming_old_node_kind = svn_node_none; + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_get_incoming_new_repos_location( + const char **incoming_new_repos_relpath, + svn_revnum_t *incoming_new_pegrev, + svn_node_kind_t *incoming_new_node_kind, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (incoming_new_repos_relpath) + { + if (get_conflict_desc2_t(conflict)->src_right_version) + *incoming_new_repos_relpath = + get_conflict_desc2_t(conflict)->src_right_version->path_in_repos; + else + *incoming_new_repos_relpath = NULL; + } + + if (incoming_new_pegrev) + { + if (get_conflict_desc2_t(conflict)->src_right_version) + *incoming_new_pegrev = + get_conflict_desc2_t(conflict)->src_right_version->peg_rev; + else + *incoming_new_pegrev = SVN_INVALID_REVNUM; + } + + if (incoming_new_node_kind) + { + if (get_conflict_desc2_t(conflict)->src_right_version) + *incoming_new_node_kind = + get_conflict_desc2_t(conflict)->src_right_version->node_kind; + else + *incoming_new_node_kind = svn_node_none; + } + + return SVN_NO_ERROR; +} + +svn_node_kind_t +svn_client_conflict_tree_get_victim_node_kind(svn_client_conflict_t *conflict) +{ + SVN_ERR_ASSERT_NO_RETURN(assert_tree_conflict(conflict, conflict->pool) + == SVN_NO_ERROR); + + return get_conflict_desc2_t(conflict)->node_kind; +} + +svn_error_t * +svn_client_conflict_prop_get_propvals(const svn_string_t **base_propval, + const svn_string_t **working_propval, + const svn_string_t **incoming_old_propval, + const svn_string_t **incoming_new_propval, + svn_client_conflict_t *conflict, + const char *propname, + apr_pool_t *result_pool) +{ + const svn_wc_conflict_description2_t *desc; + + SVN_ERR(assert_prop_conflict(conflict, conflict->pool)); + + desc = svn_hash_gets(conflict->prop_conflicts, propname); + if (desc == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Property '%s' is not in conflict."), propname); + + if (base_propval) + *base_propval = + svn_string_dup(desc->prop_value_base, result_pool); + + if (working_propval) + *working_propval = + svn_string_dup(desc->prop_value_working, result_pool); + + if (incoming_old_propval) + *incoming_old_propval = + svn_string_dup(desc->prop_value_incoming_old, result_pool); + + if (incoming_new_propval) + *incoming_new_propval = + svn_string_dup(desc->prop_value_incoming_new, result_pool); + + return SVN_NO_ERROR; +} + +const char * +svn_client_conflict_prop_get_reject_abspath(svn_client_conflict_t *conflict) +{ + SVN_ERR_ASSERT_NO_RETURN(assert_prop_conflict(conflict, conflict->pool) + == SVN_NO_ERROR); + + /* svn_wc_conflict_description2_t stores this path in 'their_abspath' */ + return get_conflict_desc2_t(conflict)->their_abspath; +} + +const char * +svn_client_conflict_text_get_mime_type(svn_client_conflict_t *conflict) +{ + SVN_ERR_ASSERT_NO_RETURN(assert_text_conflict(conflict, conflict->pool) + == SVN_NO_ERROR); + + return get_conflict_desc2_t(conflict)->mime_type; +} + +svn_error_t * +svn_client_conflict_text_get_contents(const char **base_abspath, + const char **working_abspath, + const char **incoming_old_abspath, + const char **incoming_new_abspath, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_text_conflict(conflict, scratch_pool)); + + if (base_abspath) + { + if (svn_client_conflict_get_operation(conflict) == + svn_wc_operation_merge) + *base_abspath = NULL; /* ### WC base contents not available yet */ + else /* update/switch */ + *base_abspath = get_conflict_desc2_t(conflict)->base_abspath; + } + + if (working_abspath) + *working_abspath = get_conflict_desc2_t(conflict)->my_abspath; + + if (incoming_old_abspath) + *incoming_old_abspath = get_conflict_desc2_t(conflict)->base_abspath; + + if (incoming_new_abspath) + *incoming_new_abspath = get_conflict_desc2_t(conflict)->their_abspath; + + return SVN_NO_ERROR; +} + +/* Set up type-specific data for a new conflict object. */ +static svn_error_t * +conflict_type_specific_setup(svn_client_conflict_t *conflict, + apr_pool_t *scratch_pool) +{ + svn_boolean_t tree_conflicted; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + + /* For now, we only deal with tree conflicts here. */ + SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted, + conflict, scratch_pool, + scratch_pool)); + if (!tree_conflicted) + return SVN_NO_ERROR; + + /* Set a default description function. */ + conflict->tree_conflict_get_incoming_description_func = + conflict_tree_get_incoming_description_generic; + conflict->tree_conflict_get_local_description_func = + conflict_tree_get_local_description_generic; + + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + + /* Set type-specific description and details functions. */ + if (incoming_change == svn_wc_conflict_action_delete || + incoming_change == svn_wc_conflict_action_replace) + { + conflict->tree_conflict_get_incoming_description_func = + conflict_tree_get_description_incoming_delete; + conflict->tree_conflict_get_incoming_details_func = + conflict_tree_get_details_incoming_delete; + } + else if (incoming_change == svn_wc_conflict_action_add) + { + conflict->tree_conflict_get_incoming_description_func = + conflict_tree_get_description_incoming_add; + conflict->tree_conflict_get_incoming_details_func = + conflict_tree_get_details_incoming_add; + } + else if (incoming_change == svn_wc_conflict_action_edit) + { + conflict->tree_conflict_get_incoming_description_func = + conflict_tree_get_description_incoming_edit; + conflict->tree_conflict_get_incoming_details_func = + conflict_tree_get_details_incoming_edit; + } + + if (local_change == svn_wc_conflict_reason_missing) + { + conflict->tree_conflict_get_local_description_func = + conflict_tree_get_description_local_missing; + conflict->tree_conflict_get_local_details_func = + conflict_tree_get_details_local_missing; + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_get(svn_client_conflict_t **conflict, + const char *local_abspath, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const apr_array_header_t *descs; + int i; + + *conflict = apr_pcalloc(result_pool, sizeof(**conflict)); + + (*conflict)->local_abspath = apr_pstrdup(result_pool, local_abspath); + (*conflict)->resolution_text = svn_client_conflict_option_unspecified; + (*conflict)->resolution_tree = svn_client_conflict_option_unspecified; + (*conflict)->resolved_props = apr_hash_make(result_pool); + (*conflict)->recommended_option_id = svn_client_conflict_option_unspecified; + (*conflict)->pool = result_pool; + + /* Add all legacy conflict descriptors we can find. Eventually, this code + * path should stop relying on svn_wc_conflict_description2_t entirely. */ + SVN_ERR(svn_wc__read_conflict_descriptions2_t(&descs, ctx->wc_ctx, + local_abspath, + result_pool, scratch_pool)); + for (i = 0; i < descs->nelts; i++) + { + const svn_wc_conflict_description2_t *desc; + + desc = APR_ARRAY_IDX(descs, i, const svn_wc_conflict_description2_t *); + add_legacy_desc_to_conflict(desc, *conflict, result_pool); + } + + SVN_ERR(conflict_type_specific_setup(*conflict, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Baton for conflict_status_walker */ +struct conflict_status_walker_baton +{ + svn_client_conflict_walk_func_t conflict_walk_func; + void *conflict_walk_func_baton; + svn_client_ctx_t *ctx; + svn_wc_notify_func2_t notify_func; + void *notify_baton; + svn_boolean_t resolved_a_tree_conflict; + apr_hash_t *unresolved_tree_conflicts; +}; + +/* Implements svn_wc_notify_func2_t to collect new conflicts caused by + resolving a tree conflict. */ +static void +tree_conflict_collector(void *baton, + const svn_wc_notify_t *notify, + apr_pool_t *pool) +{ + struct conflict_status_walker_baton *cswb = baton; + + if (cswb->notify_func) + cswb->notify_func(cswb->notify_baton, notify, pool); + + if (cswb->unresolved_tree_conflicts + && (notify->action == svn_wc_notify_tree_conflict + || notify->prop_state == svn_wc_notify_state_conflicted + || notify->content_state == svn_wc_notify_state_conflicted)) + { + if (!svn_hash_gets(cswb->unresolved_tree_conflicts, notify->path)) + { + const char *tc_abspath; + apr_pool_t *hash_pool; + + hash_pool = apr_hash_pool_get(cswb->unresolved_tree_conflicts); + tc_abspath = apr_pstrdup(hash_pool, notify->path); + svn_hash_sets(cswb->unresolved_tree_conflicts, tc_abspath, ""); + } + } +} + +/* + * Record a tree conflict resolution failure due to error condition ERR + * in the RESOLVE_LATER hash table. If the hash table is not available + * (meaning the caller does not wish to retry resolution later), or if + * the error condition does not indicate circumstances where another + * existing tree conflict is blocking the resolution attempt, then + * return the error ERR itself. + */ +static svn_error_t * +handle_tree_conflict_resolution_failure(const char *local_abspath, + svn_error_t *err, + apr_hash_t *unresolved_tree_conflicts) +{ + const char *tc_abspath; + + if (!unresolved_tree_conflicts + || (err->apr_err != SVN_ERR_WC_OBSTRUCTED_UPDATE + && err->apr_err != SVN_ERR_WC_FOUND_CONFLICT)) + return svn_error_trace(err); /* Give up. Do not retry resolution later. */ + + svn_error_clear(err); + tc_abspath = apr_pstrdup(apr_hash_pool_get(unresolved_tree_conflicts), + local_abspath); + + svn_hash_sets(unresolved_tree_conflicts, tc_abspath, ""); + + return SVN_NO_ERROR; /* Caller may retry after resolving other conflicts. */ +} + +/* Implements svn_wc_status4_t to walk all conflicts to resolve. + */ +static svn_error_t * +conflict_status_walker(void *baton, + const char *local_abspath, + const svn_wc_status3_t *status, + apr_pool_t *scratch_pool) +{ + struct conflict_status_walker_baton *cswb = baton; + svn_client_conflict_t *conflict; + svn_error_t *err; + svn_boolean_t tree_conflicted; + + if (!status->conflicted) + return SVN_NO_ERROR; + + SVN_ERR(svn_client_conflict_get(&conflict, local_abspath, cswb->ctx, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted, + conflict, scratch_pool, + scratch_pool)); + err = cswb->conflict_walk_func(cswb->conflict_walk_func_baton, + conflict, scratch_pool); + if (err) + { + if (tree_conflicted) + SVN_ERR(handle_tree_conflict_resolution_failure( + local_abspath, err, cswb->unresolved_tree_conflicts)); + + else + return svn_error_trace(err); + } + + if (tree_conflicted) + { + svn_client_conflict_option_id_t resolution; + + resolution = svn_client_conflict_tree_get_resolution(conflict); + if (resolution != svn_client_conflict_option_unspecified && + resolution != svn_client_conflict_option_postpone) + cswb->resolved_a_tree_conflict = TRUE; + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_walk(const char *local_abspath, + svn_depth_t depth, + svn_client_conflict_walk_func_t conflict_walk_func, + void *conflict_walk_func_baton, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + struct conflict_status_walker_baton cswb; + apr_pool_t *iterpool = NULL; + svn_error_t *err = SVN_NO_ERROR; + + if (depth == svn_depth_unknown) + depth = svn_depth_infinity; + + cswb.conflict_walk_func = conflict_walk_func; + cswb.conflict_walk_func_baton = conflict_walk_func_baton; + cswb.ctx = ctx; + cswb.resolved_a_tree_conflict = FALSE; + cswb.unresolved_tree_conflicts = apr_hash_make(scratch_pool); + + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify( + local_abspath, + svn_wc_notify_conflict_resolver_starting, + scratch_pool), + scratch_pool); + + /* Swap in our notify_func wrapper. We must revert this before returning! */ + cswb.notify_func = ctx->notify_func2; + cswb.notify_baton = ctx->notify_baton2; + ctx->notify_func2 = tree_conflict_collector; + ctx->notify_baton2 = &cswb; + + err = svn_wc_walk_status(ctx->wc_ctx, + local_abspath, + depth, + FALSE /* get_all */, + FALSE /* no_ignore */, + TRUE /* ignore_text_mods */, + NULL /* ignore_patterns */, + conflict_status_walker, &cswb, + ctx->cancel_func, ctx->cancel_baton, + scratch_pool); + + /* If we got new tree conflicts (or delayed conflicts) during the initial + walk, we now walk them one by one as closure. */ + while (!err && cswb.unresolved_tree_conflicts && + apr_hash_count(cswb.unresolved_tree_conflicts)) + { + apr_hash_index_t *hi; + svn_wc_status3_t *status = NULL; + const char *tc_abspath = NULL; + + if (iterpool) + svn_pool_clear(iterpool); + else + iterpool = svn_pool_create(scratch_pool); + + hi = apr_hash_first(scratch_pool, cswb.unresolved_tree_conflicts); + cswb.unresolved_tree_conflicts = apr_hash_make(scratch_pool); + cswb.resolved_a_tree_conflict = FALSE; + + for (; hi && !err; hi = apr_hash_next(hi)) + { + svn_pool_clear(iterpool); + + tc_abspath = apr_hash_this_key(hi); + + if (ctx->cancel_func) + { + err = ctx->cancel_func(ctx->cancel_baton); + if (err) + break; + } + + err = svn_error_trace(svn_wc_status3(&status, ctx->wc_ctx, + tc_abspath, + iterpool, iterpool)); + if (err) + break; + + err = svn_error_trace(conflict_status_walker(&cswb, tc_abspath, + status, scratch_pool)); + if (err) + break; + } + + if (!err && !cswb.resolved_a_tree_conflict && tc_abspath && + apr_hash_count(cswb.unresolved_tree_conflicts)) + { + /* None of the remaining conflicts got resolved, without any error. + * Disable the 'unresolved_tree_conflicts' cache and try again. */ + cswb.unresolved_tree_conflicts = NULL; + + /* Run the most recent resolve operation again. + * We still have status and tc_abspath for that one. + * This should uncover the error which prevents resolution. */ + err = svn_error_trace(conflict_status_walker(&cswb, tc_abspath, + status, scratch_pool)); + SVN_ERR_ASSERT(err != NULL); + + err = svn_error_createf( + SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, + _("Unable to resolve pending conflict on '%s'"), + svn_dirent_local_style(tc_abspath, scratch_pool)); + break; + } + } + + if (iterpool) + svn_pool_destroy(iterpool); + + ctx->notify_func2 = cswb.notify_func; + ctx->notify_baton2 = cswb.notify_baton; + + if (!err && ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_conflict_resolver_done, + scratch_pool), + scratch_pool); + + return svn_error_trace(err); +} |