/* * 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 committed 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; /* 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); if (relpath && relpath[0] != '\0') { struct repos_move_info *move; const char *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; if (b->moves_table == NULL) return SVN_NO_ERROR; 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; } /* 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 *); if (strcmp(move->moved_from_repos_relpath, deleted_repos_relpath) == 0) { APR_ARRAY_PUSH(*moves, struct repos_move_info *) = move; continue; } /* Test for an operative nested move. */ relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, deleted_repos_relpath); if (relpath && relpath[0] != '\0') { struct repos_move_info *nested_move; const char *actual_deleted_repos_relpath; actual_deleted_repos_relpath = svn_relpath_join(move->moved_from_repos_relpath, relpath, iterpool); nested_move = map_deleted_path_to_move(actual_deleted_repos_relpath, moves_in_deleted_rev, iterpool); if (nested_move) APR_ARRAY_PUSH(*moves, struct repos_move_info *) = nested_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 MOVES it not NULL, and the deleted node was moved, provide heads of * move chains in *MOVES, or, 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); if (moves) 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; if (moves) 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 (moves && 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; if (moves) *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; if (moves) 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; /* ### Add 'added_rev', like in conflict_tree_incoming_delete_details? */ /* 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 'struct repos_move_info *' elements. Each element is the * head of a move chain which starts in DELETED_REV. */ apr_array_header_t *moves; /* If moves is not NULL, a map of repos_relpaths and working copy nodes. * * Each key is a "const char *" repository relpath corresponding to a * possible repository-side move destination node in the revision which * is 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 find a suitable merge target 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; /* If not NULL, 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; /* 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 'struct 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; /* List of nodes in the WC which are suitable merge targets for changes * merged from any moved sibling. Array elements are 'const char *' * absolute paths of working copy nodes. This array contains multiple * elements only if ambiguous matches were found in the WC. */ apr_array_header_t *wc_siblings; int preferred_sibling_idx; }; 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; } static svn_error_t * collect_sibling_move_candidates(apr_array_header_t *candidates, const char *victim_abspath, svn_node_kind_t victim_kind, struct repos_move_info *move, svn_client_ctx_t *ctx, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const char *basename; apr_array_header_t *abspaths; int i; basename = svn_relpath_basename(move->moved_from_repos_relpath, scratch_pool); SVN_ERR(svn_wc__find_working_nodes_with_basename(&abspaths, victim_abspath, basename, victim_kind, ctx->wc_ctx, result_pool, scratch_pool)); apr_array_cat(candidates, abspaths); if (move->next) { apr_pool_t *iterpool = svn_pool_create(scratch_pool); for (i = 0; i < move->next->nelts; i++) { struct repos_move_info *next_move; next_move = APR_ARRAY_IDX(move->next, i, struct repos_move_info *); SVN_ERR(collect_sibling_move_candidates(candidates, victim_abspath, victim_kind, next_move, ctx, result_pool, iterpool)); svn_pool_clear(iterpool); } svn_pool_destroy(iterpool); } 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) { 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); } if (move->next) { 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; } /* 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; svn_node_kind_t old_kind; svn_node_kind_t new_kind; 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; apr_array_header_t *wc_siblings = NULL; const char *related_repos_relpath; svn_revnum_t related_peg_rev; const char *repos_root_url; const char *repos_uuid; const char *url, *corrected_url; svn_ra_session_t *ra_session; svn_client__pathrev_t *yca_loc; svn_revnum_t end_rev; 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)); /* 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)); /* If the parent is not part of the repository-side tree checked out * into this working copy, then bail. We do not support this case yet. */ if (parent_peg_rev == SVN_INVALID_REVNUM) return SVN_NO_ERROR; /* 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)); /* Set END_REV to our best guess of the nearest YCA revision. */ 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)); 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) { end_rev = yca_loc->rev; /* END_REV must be smaller than PARENT_PEG_REV, else the call to * find_revision_for_suspected_deletion() below will abort. */ if (end_rev >= parent_peg_rev) end_rev = parent_peg_rev > 0 ? parent_peg_rev - 1 : 0; } else end_rev = 0; /* ### We might walk through all of history... */ SVN_ERR(find_revision_for_suspected_deletion( &deleted_rev, &deleted_rev_author, &replacing_node_kind, yca_loc ? &moves : NULL, conflict, deleted_basename, parent_repos_relpath, parent_peg_rev, end_rev, 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_node_kind_t related_node_kind; apr_array_header_t *candidates; int i; apr_pool_t *iterpool; /* ### 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); if (yca_loc) { 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; } else end_rev = 0; /* ### We might walk through all of history... */ 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; /* Find the missing node in the WC. In theory, this requires tracing * back history of every node in the WC to check for a YCA with the * conflict victim. This operation would obviously be quite expensive. * * However, assuming that the victim was not moved in the merge target, * we can take a short-cut: The basename of the node cannot have changed, * so we can limit history tracing to nodes with a matching basename. * * This approach solves the conflict case where an edit to a file which * was moved on one branch is cherry-picked to another branch where the * corresponding file has not been moved (yet). It does not solve move * vs. move conflicts, but such conflicts are not yet supported by the * resolver anyway and are hard to solve without server-side support. */ iterpool = svn_pool_create(scratch_pool); for (i = 0; i < sibling_moves->nelts; i++) { struct repos_move_info *move; int j; svn_pool_clear(iterpool); move = APR_ARRAY_IDX(sibling_moves, i, struct repos_move_info *); candidates = apr_array_make(iterpool, 1, sizeof(const char *)); SVN_ERR(collect_sibling_move_candidates(candidates, victim_abspath, old_rev < new_rev ? new_kind : old_kind, move, ctx, iterpool, iterpool)); /* Determine whether a candidate node shares a YCA with the victim. */ for (j = 0; j < candidates->nelts; j++) { const char *candidate_abspath; const char *candidate_repos_relpath; svn_revnum_t candidate_revision; svn_error_t *err; candidate_abspath = APR_ARRAY_IDX(candidates, j, const char *); SVN_ERR(svn_wc__node_get_origin(NULL, &candidate_revision, &candidate_repos_relpath, NULL, NULL, NULL, NULL, ctx->wc_ctx, candidate_abspath, FALSE, iterpool, iterpool)); err = find_yca(&yca_loc, old_rev < new_rev ? new_repos_relpath : old_repos_relpath, old_rev < new_rev ? new_rev : old_rev, 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) { if (wc_siblings == NULL) wc_siblings = apr_array_make(conflict->pool, 1, sizeof(const char *)); APR_ARRAY_PUSH(wc_siblings, const char *) = apr_pstrdup(conflict->pool, candidate_abspath); } } } svn_pool_destroy(iterpool); } 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; if (details->moves != NULL) { apr_pool_t *iterpool; int i; details->wc_move_targets = apr_hash_make(conflict->pool); 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(details->wc_move_targets, move, ctx, conflict->local_abspath, new_kind, new_repos_relpath, new_rev, scratch_pool, iterpool)); } svn_pool_destroy(iterpool); if (apr_hash_count(details->wc_move_targets) > 0) { apr_array_header_t *move_target_repos_relpaths; const svn_sort__item_t *item; /* 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( details->wc_move_targets, svn_sort_compare_items_as_paths, scratch_pool); item = &APR_ARRAY_IDX(move_target_repos_relpaths, 0, svn_sort__item_t); details->move_target_repos_relpath = item->key; details->wc_move_target_idx = 0; } else { details->move_target_repos_relpath = NULL; details->wc_move_target_idx = 0; } } details->sibling_moves = sibling_moves; details->wc_siblings = wc_siblings; if (details->wc_move_targets && apr_hash_count(details->wc_move_targets) == 1) { 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_node_kind_t kind = old_rev < new_rev ? new_kind : old_kind; if (kind == svn_node_file) conflict->recommended_option_id = svn_client_conflict_option_local_move_file_text_merge; else if (kind == svn_node_dir) conflict->recommended_option_id = svn_client_conflict_option_local_move_dir_merge; } } else if (details->wc_siblings && details->wc_siblings->nelts == 1) { svn_node_kind_t kind = old_rev < new_rev ? new_kind : old_kind; if (kind == svn_node_file) conflict->recommended_option_id = svn_client_conflict_option_sibling_move_file_text_merge; else if (kind == svn_node_dir) conflict->recommended_option_id = svn_client_conflict_option_sibling_move_dir_merge; } 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; } 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; victim_abspath = svn_client_conflict_get_local_abspath(conflict); victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(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 recommend a resolution option. */ if (apr_hash_count(details->wc_move_targets) == 1) { 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, svn_client_conflict_option_local_move_dir_merge, svn_client_conflict_option_sibling_move_file_text_merge, svn_client_conflict_option_sibling_move_dir_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; svn_revnum_t end_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; } end_rev = (new_kind == svn_node_none ? 0 : old_rev); if (end_rev >= parent_peg_rev) end_rev = (parent_peg_rev > 0 ? parent_peg_rev - 1 : 0); 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, end_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 = NULL; 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 && strcmp(old_repos_relpath, new_repos_relpath) == 0) { 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 if (old_rev > new_rev) { /* 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; } } 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, /* ### remove leading slash */ svn_relpath_canonicalize( log_item->copyfrom_path, iterpool)); } 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 == 0) return _(" (no revisions found)"); 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, Q_("%s\n [%d revision omitted for " "brevity],\n", "%s\n [%d revisions omitted for " "brevity],\n", num_revs_to_skip), 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; svn_wc_conflict_reason_t local_change; local_abspath = svn_client_conflict_get_local_abspath(conflict); local_change = svn_client_conflict_get_local_change(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)); if (local_change == svn_wc_conflict_reason_unversioned) { /* Copy the unversioned file to temporary storage. */ SVN_ERR(svn_stream_open_readonly(&working_file_stream, local_abspath, scratch_pool, scratch_pool)); /* Unversioned files have no properties. */ working_props = apr_hash_make(scratch_pool); } else { /* 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)); /* 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); } SVN_ERR(svn_stream_copy3(working_file_stream, working_file_tmp_stream, ctx->cancel_func, ctx->cancel_baton, 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_revert6(ctx->wc_ctx, local_abspath, svn_depth_empty, FALSE, NULL, TRUE, FALSE, TRUE /*added_keep_local*/, 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, 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; svn_wc_conflict_reason_t local_change; local_abspath = svn_client_conflict_get_local_abspath(conflict); local_change = svn_client_conflict_get_local_change(conflict); if (local_change == svn_wc_conflict_reason_unversioned) { char *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)); /* The update/switch operation has added the incoming versioned * directory as a deleted op-depth layer. We can revert this layer * to make the incoming tree appear in the working copy. * This meta-data-only revert operation effecively merges the * versioned and unversioned trees but leaves all unversioned files as * they were. This is the best we can do; 3-way merging of unversioned * files with files from the repository is impossible because there is * no known merge base. No unversioned data will be lost, and any * differences to files in the repository will show up in 'svn diff'. */ err = svn_wc_revert6(ctx->wc_ctx, local_abspath, svn_depth_infinity, FALSE, NULL, TRUE, TRUE /* metadata_only */, TRUE /*added_keep_local*/, NULL, NULL, /* no cancellation */ ctx->notify_func2, ctx->notify_baton2, scratch_pool); } else { 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; } /* 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; svn_error_t *err; svn_boolean_t timestamp_sleep; 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; /* ### 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; err = svn_client__repos_to_wc_copy_by_editor(×tamp_sleep, svn_node_dir, url, incoming_new_pegrev, local_abspath, ra_session, ctx, 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)); } /* Ensure the conflict victim is a copy of itself from before it was deleted. * Update and switch are supposed to set this up when flagging the conflict. */ static svn_error_t * ensure_local_edit_vs_incoming_deletion_copied_state( struct conflict_tree_incoming_delete_details *details, svn_wc_operation_t operation, const char *wcroot_abspath, svn_client_conflict_t *conflict, svn_client_ctx_t *ctx, apr_pool_t *scratch_pool) { svn_boolean_t is_copy; svn_revnum_t copyfrom_rev; const char *copyfrom_repos_relpath; SVN_ERR_ASSERT(operation == svn_wc_operation_update || operation == svn_wc_operation_switch); SVN_ERR(svn_wc__node_get_origin(&is_copy, ©from_rev, ©from_repos_relpath, NULL, NULL, NULL, NULL, ctx->wc_ctx, conflict->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 && 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); } return SVN_NO_ERROR; } /* 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; svn_wc_conflict_reason_t local_change; 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); local_change = svn_client_conflict_get_local_change(conflict); if (operation == svn_wc_operation_update || operation == svn_wc_operation_switch) { struct conflict_tree_incoming_delete_details *details; 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)); 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)); if (local_change == svn_wc_conflict_reason_edited) SVN_ERR(ensure_local_edit_vs_incoming_deletion_copied_state( details, operation, wcroot_abspath, conflict, ctx, scratch_pool)); } 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 *victim_abspath; const char *merge_source_abspath; svn_wc_conflict_reason_t local_change; 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; victim_abspath = svn_client_conflict_get_local_abspath(conflict); local_change = svn_client_conflict_get_local_change(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(victim_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(victim_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_both_moved_file_move_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, victim_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 *); if (local_change == svn_wc_conflict_reason_missing) { /* This is an incoming move vs local move conflict. * Merge from the local move's target location to the * incoming move's target location. */ struct conflict_tree_local_missing_details *local_details; apr_array_header_t *moves; local_details = conflict->tree_conflict_local_details; moves = svn_hash_gets(local_details->wc_move_targets, local_details->move_target_repos_relpath); merge_source_abspath = APR_ARRAY_IDX(moves, local_details->wc_move_target_idx, const char *); } else merge_source_abspath = victim_abspath; /* ### 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(victim_abspath, moved_to_abspath, scratch_pool), scratch_pool, scratch_pool)); if (local_change != svn_wc_conflict_reason_missing) { 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, merge_source_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 *moved_to_stream; svn_stream_t *incoming_stream; /* Create a temporary copy of the moved 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(&moved_to_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(moved_to_stream, incoming_stream, NULL, NULL, /* no cancellation */ scratch_pool); if (err) goto unlock_wc; /* Overwrite the moved file with the conflict victim's content. * Incoming changes will be merged in from the temporary file created * above. This is required to correctly make local changes show up as * 'mine' during the three-way text merge between the ancestor file, * the conflict victim ('mine'), and the moved file ('theirs') which * was brought in by the update/switch operation and occupies the path * of the merge target. */ err = svn_io_copy_file(merge_source_abspath, moved_to_abspath, FALSE, 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, merge_source_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, victim_abspath, FALSE, FALSE, NULL, NULL, /* don't allow user to cancel here */ NULL, NULL, /* no extra notification */ scratch_pool); if (err) goto unlock_wc; } else if (local_change == svn_wc_conflict_reason_missing) { /* Clear tree conflict marker. */ err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); if (err) goto unlock_wc; } if (ctx->notify_func2) { svn_wc_notify_t *notify; notify = svn_wc_create_notify(victim_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. * Resolve an incoming move vs local move conflict by merging from the * incoming move's target location to the local move's target location, * overriding the incoming move. */ static svn_error_t * resolve_both_moved_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 *victim_abspath; const char *local_moved_to_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 *incoming_props; apr_hash_t *local_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 *incoming_details; apr_array_header_t *possible_moved_to_abspaths; const char *incoming_moved_to_abspath; struct conflict_tree_local_missing_details *local_details; apr_array_header_t *local_moves; victim_abspath = svn_client_conflict_get_local_abspath(conflict); operation = svn_client_conflict_get_operation(conflict); incoming_details = conflict->tree_conflict_incoming_details; if (incoming_details == NULL || incoming_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(victim_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(victim_abspath, scratch_pool)); option_id = svn_client_conflict_option_get_id(option); SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_file_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, victim_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(incoming_details->wc_move_targets, get_moved_to_repos_relpath(incoming_details, scratch_pool)); incoming_moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, incoming_details->wc_move_target_idx, const char *); local_details = conflict->tree_conflict_local_details; local_moves = svn_hash_gets(local_details->wc_move_targets, local_details->move_target_repos_relpath); local_moved_to_abspath = APR_ARRAY_IDX(local_moves, local_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(victim_abspath, local_moved_to_abspath, scratch_pool), scratch_pool, scratch_pool)); /* Get a copy of the incoming moved item's properties. */ err = svn_wc_prop_list2(&incoming_props, ctx->wc_ctx, incoming_moved_to_abspath, scratch_pool, scratch_pool); if (err) goto unlock_wc; /* Get a copy of the local move target's properties. */ err = svn_wc_prop_list2(&local_props, ctx->wc_ctx, local_moved_to_abspath, scratch_pool, scratch_pool); if (err) goto unlock_wc; /* Create a property diff for the files. */ err = svn_prop_diffs(&propdiffs, incoming_props, local_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, ancestor_abspath, incoming_moved_to_abspath, local_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); if (err) goto unlock_wc; 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_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); } /* Revert local addition of the incoming move's target. */ err = svn_wc_revert6(ctx->wc_ctx, incoming_moved_to_abspath, svn_depth_infinity, FALSE, NULL, TRUE, FALSE, FALSE /*added_keep_local*/, NULL, NULL, /* no cancellation */ ctx->notify_func2, ctx->notify_baton2, scratch_pool); if (err) goto unlock_wc; err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); if (err) goto unlock_wc; if (ctx->notify_func2) { svn_wc_notify_t *notify; notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, scratch_pool); ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); } svn_io_sleep_for_timestamps(local_moved_to_abspath, 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. * Resolve an incoming move vs local move conflict by moving the locally moved * directory to the incoming move target location, and then merging changes. */ static svn_error_t * resolve_both_moved_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 *victim_abspath; const char *local_moved_to_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 *incoming_moved_repos_relpath; struct conflict_tree_incoming_delete_details *incoming_details; apr_array_header_t *possible_moved_to_abspaths; const char *incoming_moved_to_abspath; struct conflict_tree_local_missing_details *local_details; apr_array_header_t *local_moves; svn_client__conflict_report_t *conflict_report; const char *incoming_old_url; const char *incoming_moved_url; svn_opt_revision_t incoming_old_opt_rev; svn_opt_revision_t incoming_moved_opt_rev; victim_abspath = svn_client_conflict_get_local_abspath(conflict); operation = svn_client_conflict_get_operation(conflict); incoming_details = conflict->tree_conflict_incoming_details; if (incoming_details == NULL || incoming_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(victim_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(victim_abspath, scratch_pool)); option_id = svn_client_conflict_option_get_id(option); SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_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)); possible_moved_to_abspaths = svn_hash_gets(incoming_details->wc_move_targets, get_moved_to_repos_relpath(incoming_details, scratch_pool)); incoming_moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, incoming_details->wc_move_target_idx, const char *); local_details = conflict->tree_conflict_local_details; local_moves = svn_hash_gets(local_details->wc_move_targets, local_details->move_target_repos_relpath); local_moved_to_abspath = APR_ARRAY_IDX(local_moves, local_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(victim_abspath, local_moved_to_abspath, scratch_pool), scratch_pool, scratch_pool)); /* Perform the merge. */ incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/", incoming_old_repos_relpath, SVN_VA_NULL); incoming_old_opt_rev.kind = svn_opt_revision_number; incoming_old_opt_rev.value.number = incoming_old_pegrev; incoming_moved_repos_relpath = get_moved_to_repos_relpath(incoming_details, scratch_pool); incoming_moved_url = apr_pstrcat(scratch_pool, repos_root_url, "/", incoming_moved_repos_relpath, SVN_VA_NULL); incoming_moved_opt_rev.kind = svn_opt_revision_number; incoming_moved_opt_rev.value.number = incoming_new_pegrev; err = svn_client__merge_locked(&conflict_report, incoming_old_url, &incoming_old_opt_rev, incoming_moved_url, &incoming_moved_opt_rev, local_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; /* Revert local addition of the incoming move's target. */ err = svn_wc_revert6(ctx->wc_ctx, incoming_moved_to_abspath, svn_depth_infinity, FALSE, NULL, TRUE, FALSE, FALSE /*added_keep_local*/, NULL, NULL, /* no cancellation */ ctx->notify_func2, ctx->notify_baton2, scratch_pool); if (err) goto unlock_wc; err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); if (err) goto unlock_wc; if (ctx->notify_func2) { svn_wc_notify_t *notify; notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, scratch_pool); ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); } svn_io_sleep_for_timestamps(local_moved_to_abspath, 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. * Resolve an incoming move vs local move conflict by merging from the * incoming move's target location to the local move's target location, * overriding the incoming move. */ static svn_error_t * resolve_both_moved_dir_move_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 *victim_abspath; const char *local_moved_to_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; struct conflict_tree_incoming_delete_details *incoming_details; apr_array_header_t *possible_moved_to_abspaths; const char *incoming_moved_to_abspath; struct conflict_tree_local_missing_details *local_details; apr_array_header_t *local_moves; svn_client__conflict_report_t *conflict_report; const char *incoming_old_url; const char *incoming_moved_url; svn_opt_revision_t incoming_old_opt_rev; svn_opt_revision_t incoming_moved_opt_rev; victim_abspath = svn_client_conflict_get_local_abspath(conflict); operation = svn_client_conflict_get_operation(conflict); incoming_details = conflict->tree_conflict_incoming_details; if (incoming_details == NULL || incoming_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(victim_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(victim_abspath, scratch_pool)); option_id = svn_client_conflict_option_get_id(option); SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_dir_move_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)); possible_moved_to_abspaths = svn_hash_gets(incoming_details->wc_move_targets, get_moved_to_repos_relpath(incoming_details, scratch_pool)); incoming_moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, incoming_details->wc_move_target_idx, const char *); local_details = conflict->tree_conflict_local_details; local_moves = svn_hash_gets(local_details->wc_move_targets, local_details->move_target_repos_relpath); local_moved_to_abspath = APR_ARRAY_IDX(local_moves, local_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(victim_abspath, local_moved_to_abspath, scratch_pool), scratch_pool, scratch_pool)); /* Revert the incoming move target directory. */ err = svn_wc_revert6(ctx->wc_ctx, incoming_moved_to_abspath, svn_depth_infinity, FALSE, NULL, TRUE, FALSE, TRUE /*added_keep_local*/, NULL, NULL, /* no cancellation */ ctx->notify_func2, ctx->notify_baton2, scratch_pool); if (err) goto unlock_wc; /* 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_moved_to_abspath, incoming_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 INCOMING_OLD_URL@MERGE_LEFT->INCOMING_MOVED_URL@MERGE_RIGHT * into the locally moved merge target. */ incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/", incoming_old_repos_relpath, SVN_VA_NULL); incoming_old_opt_rev.kind = svn_opt_revision_number; incoming_old_opt_rev.value.number = incoming_old_pegrev; incoming_moved_url = apr_pstrcat(scratch_pool, repos_root_url, "/", incoming_details->move_target_repos_relpath, SVN_VA_NULL); incoming_moved_opt_rev.kind = svn_opt_revision_number; incoming_moved_opt_rev.value.number = incoming_new_pegrev; err = svn_client__merge_locked(&conflict_report, incoming_old_url, &incoming_old_opt_rev, incoming_moved_url, &incoming_moved_opt_rev, incoming_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; err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); if (err) goto unlock_wc; if (ctx->notify_func2) { svn_wc_notify_t *notify; notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, scratch_pool); ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); } svn_io_sleep_for_timestamps(local_moved_to_abspath, 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_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; const char *incoming_old_url; svn_opt_revision_t incoming_old_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; } 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. */ err = svn_wc_revert6(ctx->wc_ctx, moved_to_abspath, svn_depth_infinity, FALSE, NULL, TRUE, FALSE, TRUE /*added_keep_local*/, NULL, NULL, /* no cancellation */ ctx->notify_func2, ctx->notify_baton2, scratch_pool); if (err) goto unlock_wc; /* 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 INCOMING_OLD_URL@MERGE_LEFT->MOVE_TARGET_URL@MERGE_RIGHT * into move target. */ incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/", incoming_old_repos_relpath, SVN_VA_NULL); incoming_old_opt_rev.kind = svn_opt_revision_number; incoming_old_opt_rev.value.number = incoming_old_pegrev; 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, incoming_old_url, &incoming_old_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. * Handles svn_client_conflict_option_local_move_file_text_merge * and svn_client_conflict_option_sibling_move_file_text_merge. */ 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; const char *merge_target_abspath; const char *wcroot_abspath; SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, conflict->local_abspath, scratch_pool, scratch_pool)); 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)); if (details->wc_siblings) { merge_target_abspath = APR_ARRAY_IDX(details->wc_siblings, details->preferred_sibling_idx, const char *); } else if (details->wc_move_targets && details->move_target_repos_relpath) { apr_array_header_t *moves; moves = svn_hash_gets(details->wc_move_targets, details->move_target_repos_relpath); merge_target_abspath = APR_ARRAY_IDX(moves, details->wc_move_target_idx, const char *); } else return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, _("Corresponding working copy node not found " "for '%s'"), svn_dirent_local_style( svn_dirent_skip_ancestor( wcroot_abspath, conflict->local_abspath), scratch_pool)); SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, merge_target_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, merge_target_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, merge_target_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(merge_target_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(merge_target_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; } /* Implements conflict_option_resolve_func_t. */ static svn_error_t * resolve_local_move_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 *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; struct conflict_tree_local_missing_details *details; const char *merge_target_abspath; const char *incoming_old_url; const char *incoming_new_url; svn_opt_revision_t incoming_old_opt_rev; svn_opt_revision_t incoming_new_opt_rev; svn_client__conflict_report_t *conflict_report; 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)); if (details->wc_move_targets) { apr_array_header_t *moves; moves = svn_hash_gets(details->wc_move_targets, details->move_target_repos_relpath); merge_target_abspath = APR_ARRAY_IDX(moves, details->wc_move_target_idx, const char *); } else merge_target_abspath = APR_ARRAY_IDX(details->wc_siblings, details->preferred_sibling_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(conflict->local_abspath, merge_target_abspath, scratch_pool), scratch_pool, scratch_pool)); /* Resolve to current working copy state. * svn_client__merge_locked() requires this. */ err = svn_wc__del_tree_conflict(ctx->wc_ctx, conflict->local_abspath, scratch_pool); if (err) goto unlock_wc; /* Merge outstanding changes to the merge target. */ incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/", incoming_old_repos_relpath, SVN_VA_NULL); incoming_old_opt_rev.kind = svn_opt_revision_number; incoming_old_opt_rev.value.number = incoming_old_pegrev; incoming_new_url = apr_pstrcat(scratch_pool, repos_root_url, "/", incoming_new_repos_relpath, 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, incoming_old_url, &incoming_old_opt_rev, incoming_new_url, &incoming_new_opt_rev, merge_target_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); unlock_wc: svn_io_sleep_for_timestamps(merge_target_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(merge_target_abspath, svn_wc_notify_update_update, scratch_pool); if (conflict_report) notify->content_state = svn_wc_notify_state_conflicted; else notify->content_state = svn_wc_notify_state_merged; notify->kind = svn_node_dir; 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 incoming changes only where they conflict"), resolve_text_conflict); add_resolution_option(*options, conflict, svn_client_conflict_option_working_text_where_conflicted, _("Reject conflicts"), _("reject incoming 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_unversioned || 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) || (operation != svn_wc_operation_merge && local_change == svn_wc_conflict_reason_unversioned))) { 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) { if (conflict->tree_conflict_incoming_details == NULL) return SVN_NO_ERROR; 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 || local_change == svn_wc_conflict_reason_missing)) { /* 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, const char *moved_to_abspath, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { svn_wc_operation_t operation; const char *victim_abspath; svn_node_kind_t victim_node_kind; const char *wcroot_abspath; victim_abspath = svn_client_conflict_get_local_abspath(conflict); victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(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) { const char *incoming_moved_abspath = NULL; if (victim_node_kind == svn_node_none) { /* This is an incoming move vs local move conflict. */ struct conflict_tree_incoming_delete_details *details; details = conflict->tree_conflict_incoming_details; if (details->wc_move_targets) { apr_array_header_t *moves; moves = svn_hash_gets(details->wc_move_targets, details->move_target_repos_relpath); incoming_moved_abspath = APR_ARRAY_IDX(moves, details->wc_move_target_idx, const char *); } } if (incoming_moved_abspath) { /* The 'move and merge' option follows the incoming move; note that * moved_to_abspath points to the current location of an item which * was moved in the history of our merge target branch. If the user * chooses 'move and merge', that item will be moved again (i.e. it * will be moved to and merged with incoming_moved_abspath's item). */ *description = apr_psprintf( result_pool, _("move '%s' to '%s' and merge"), svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, moved_to_abspath), scratch_pool), svn_dirent_local_style(svn_dirent_skip_ancestor( wcroot_abspath, incoming_moved_abspath), scratch_pool)); } else { *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; 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_file && incoming_old_kind == svn_node_file && 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; apr_array_header_t *move_target_wc_abspaths; const char *moved_to_abspath; 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; 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 *); SVN_ERR(describe_incoming_move_merge_conflict_option(&description, conflict, ctx, moved_to_abspath, 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; apr_array_header_t *move_target_wc_abspaths; const char *moved_to_abspath; 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; 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 *); SVN_ERR(describe_incoming_move_merge_conflict_option(&description, conflict, ctx, moved_to_abspath, 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_or_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; 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; 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_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 (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; const char *wcroot_abspath; SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, conflict->local_abspath, scratch_pool, scratch_pool)); details = conflict->tree_conflict_local_details; if (details != NULL && details->moves != NULL && details->move_target_repos_relpath != NULL) { apr_array_header_t *moves; const char *moved_to_abspath; const char *description; moves = svn_hash_gets(details->wc_move_targets, details->move_target_repos_relpath); moved_to_abspath = APR_ARRAY_IDX(moves, details->wc_move_target_idx, const char *); description = apr_psprintf( scratch_pool, _("apply changes to move destination '%s'"), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, moved_to_abspath), scratch_pool)); if ((incoming_old_kind == svn_node_file || incoming_old_kind == svn_node_none) && (incoming_new_kind == svn_node_file || incoming_new_kind == svn_node_none)) { 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 { add_resolution_option( options, conflict, svn_client_conflict_option_local_move_dir_merge, _("Apply to move destination"), description, resolve_local_move_dir_merge); } } } return SVN_NO_ERROR; } /* Configure 'sibling move file/dir merge' resolution option for * a tree conflict. */ static svn_error_t * configure_option_sibling_move_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_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; 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_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 (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; const char *wcroot_abspath; SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, conflict->local_abspath, scratch_pool, scratch_pool)); details = conflict->tree_conflict_local_details; if (details != NULL && details->wc_siblings != NULL) { const char *description; const char *sibling; sibling = apr_pstrdup(conflict->pool, APR_ARRAY_IDX(details->wc_siblings, details->preferred_sibling_idx, const char *)); description = apr_psprintf( scratch_pool, _("apply changes to '%s'"), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, sibling), scratch_pool)); if ((incoming_old_kind == svn_node_file || incoming_old_kind == svn_node_none) && (incoming_new_kind == svn_node_file || incoming_new_kind == svn_node_none)) { add_resolution_option( options, conflict, svn_client_conflict_option_sibling_move_file_text_merge, _("Apply to corresponding local location"), description, resolve_local_move_file_merge); } else { add_resolution_option( options, conflict, svn_client_conflict_option_sibling_move_dir_merge, _("Apply to corresponding local location"), description, resolve_local_move_dir_merge); } } } return SVN_NO_ERROR; } struct conflict_tree_update_local_moved_away_details { /* * This array consists of "const char *" absolute paths to working copy * nodes which are uncomitted copies and correspond to the repository path * of the conflict victim. * Each such working copy node is a potential local move target which can * be chosen to find a suitable merge target when resolving a tree conflict. * * This may be an empty array in case if there is no move target path in * the working copy. */ apr_array_header_t *wc_move_targets; /* Current index into the list of working copy paths in WC_MOVE_TARGETS. */ int preferred_move_target_idx; }; /* Implements conflict_option_resolve_func_t. * Resolve an incoming move vs local move conflict by merging from the * incoming move's target location to the local move's target location, * overriding the incoming move. The original local move was broken during * update/switch, so overriding the incoming move involves recording a new * move from the incoming move's target location to the local move's target * location. */ static svn_error_t * resolve_both_moved_file_update_keep_local_move( 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 *victim_abspath; const char *local_moved_to_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 *incoming_props; apr_hash_t *local_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 *incoming_details; apr_array_header_t *possible_moved_to_abspaths; const char *incoming_moved_to_abspath; struct conflict_tree_update_local_moved_away_details *local_details; victim_abspath = svn_client_conflict_get_local_abspath(conflict); operation = svn_client_conflict_get_operation(conflict); incoming_details = conflict->tree_conflict_incoming_details; if (incoming_details == NULL || incoming_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(victim_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(victim_abspath, scratch_pool)); option_id = svn_client_conflict_option_get_id(option); SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_file_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, victim_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(incoming_details->wc_move_targets, get_moved_to_repos_relpath(incoming_details, scratch_pool)); incoming_moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, incoming_details->wc_move_target_idx, const char *); local_details = conflict->tree_conflict_local_details; local_moved_to_abspath = APR_ARRAY_IDX(local_details->wc_move_targets, local_details->preferred_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(victim_abspath, local_moved_to_abspath, scratch_pool), scratch_pool, scratch_pool)); /* Get a copy of the incoming moved item's properties. */ err = svn_wc_prop_list2(&incoming_props, ctx->wc_ctx, incoming_moved_to_abspath, scratch_pool, scratch_pool); if (err) goto unlock_wc; /* Get a copy of the local move target's properties. */ err = svn_wc_prop_list2(&local_props, ctx->wc_ctx, local_moved_to_abspath, scratch_pool, scratch_pool); if (err) goto unlock_wc; /* Create a property diff for the files. */ err = svn_prop_diffs(&propdiffs, incoming_props, local_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, ancestor_abspath, incoming_moved_to_abspath, local_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); if (err) goto unlock_wc; 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_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); } /* Record a new move which overrides the incoming move. */ err = svn_wc__move2(ctx->wc_ctx, incoming_moved_to_abspath, local_moved_to_abspath, TRUE, /* 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; /* Remove moved-away file from disk. */ err = svn_io_remove_file2(incoming_moved_to_abspath, TRUE, scratch_pool); if (err) goto unlock_wc; err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); if (err) goto unlock_wc; if (ctx->notify_func2) { svn_wc_notify_t *notify; notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, scratch_pool); ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); } svn_io_sleep_for_timestamps(local_moved_to_abspath, 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. * Resolve an incoming move vs local move conflict by merging from the * local move's target location to the incoming move's target location, * and reverting the local move. */ static svn_error_t * resolve_both_moved_file_update_keep_incoming_move( 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 *victim_abspath; const char *local_moved_to_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 *incoming_props; apr_hash_t *local_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 *incoming_details; apr_array_header_t *possible_moved_to_abspaths; const char *incoming_moved_to_abspath; struct conflict_tree_update_local_moved_away_details *local_details; victim_abspath = svn_client_conflict_get_local_abspath(conflict); operation = svn_client_conflict_get_operation(conflict); incoming_details = conflict->tree_conflict_incoming_details; if (incoming_details == NULL || incoming_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(victim_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(victim_abspath, scratch_pool)); option_id = svn_client_conflict_option_get_id(option); SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_file_move_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, victim_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(incoming_details->wc_move_targets, get_moved_to_repos_relpath(incoming_details, scratch_pool)); incoming_moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, incoming_details->wc_move_target_idx, const char *); local_details = conflict->tree_conflict_local_details; local_moved_to_abspath = APR_ARRAY_IDX(local_details->wc_move_targets, local_details->preferred_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(victim_abspath, local_moved_to_abspath, scratch_pool), scratch_pool, scratch_pool)); /* Get a copy of the incoming moved item's properties. */ err = svn_wc_prop_list2(&incoming_props, ctx->wc_ctx, incoming_moved_to_abspath, scratch_pool, scratch_pool); if (err) goto unlock_wc; /* Get a copy of the local move target's properties. */ err = svn_wc_prop_list2(&local_props, ctx->wc_ctx, local_moved_to_abspath, scratch_pool, scratch_pool); if (err) goto unlock_wc; /* Create a property diff for the files. */ err = svn_prop_diffs(&propdiffs, incoming_props, local_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, ancestor_abspath, local_moved_to_abspath, incoming_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); if (err) goto unlock_wc; 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_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); } /* Revert the copy-half of the local move. The delete-half of this move * has already been deleted during the update/switch operation. */ err = svn_wc_revert6(ctx->wc_ctx, local_moved_to_abspath, svn_depth_empty, FALSE, NULL, TRUE, FALSE, TRUE /*added_keep_local*/, NULL, NULL, /* no cancellation */ ctx->notify_func2, ctx->notify_baton2, scratch_pool); if (err) goto unlock_wc; err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); if (err) goto unlock_wc; if (ctx->notify_func2) { svn_wc_notify_t *notify; notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, scratch_pool); ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); } svn_io_sleep_for_timestamps(local_moved_to_abspath, 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 tree_conflict_get_details_func_t. */ static svn_error_t * conflict_tree_get_details_update_local_moved_away( svn_client_conflict_t *conflict, svn_client_ctx_t *ctx, apr_pool_t *scratch_pool) { struct conflict_tree_update_local_moved_away_details *details; const char *incoming_old_repos_relpath; svn_node_kind_t incoming_old_kind; SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( &incoming_old_repos_relpath, NULL, &incoming_old_kind, conflict, scratch_pool, scratch_pool)); details = apr_pcalloc(conflict->pool, sizeof(*details)); details->wc_move_targets = apr_array_make(conflict->pool, 1, sizeof(const char *)); /* Search the WC for copies of the conflict victim. */ SVN_ERR(svn_wc__find_copies_of_repos_path(&details->wc_move_targets, conflict->local_abspath, incoming_old_repos_relpath, incoming_old_kind, ctx->wc_ctx, conflict->pool, scratch_pool)); conflict->tree_conflict_local_details = details; return SVN_NO_ERROR; } static svn_error_t * get_both_moved_file_paths(const char **incoming_moved_to_abspath, const char **local_moved_to_abspath, svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) { struct conflict_tree_incoming_delete_details *incoming_details; apr_array_header_t *incoming_move_target_wc_abspaths; svn_wc_operation_t operation; operation = svn_client_conflict_get_operation(conflict); *incoming_moved_to_abspath = NULL; *local_moved_to_abspath = NULL; incoming_details = conflict->tree_conflict_incoming_details; if (incoming_details == NULL || incoming_details->moves == NULL || apr_hash_count(incoming_details->wc_move_targets) == 0) return SVN_NO_ERROR; incoming_move_target_wc_abspaths = svn_hash_gets(incoming_details->wc_move_targets, get_moved_to_repos_relpath(incoming_details, scratch_pool)); *incoming_moved_to_abspath = APR_ARRAY_IDX(incoming_move_target_wc_abspaths, incoming_details->wc_move_target_idx, const char *); if (operation == svn_wc_operation_merge) { struct conflict_tree_local_missing_details *local_details; apr_array_header_t *local_moves; local_details = conflict->tree_conflict_local_details; if (local_details == NULL || apr_hash_count(local_details->wc_move_targets) == 0) return SVN_NO_ERROR; local_moves = svn_hash_gets(local_details->wc_move_targets, local_details->move_target_repos_relpath); *local_moved_to_abspath = APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, const char *); } else { struct conflict_tree_update_local_moved_away_details *local_details; local_details = conflict->tree_conflict_local_details; if (local_details == NULL || local_details->wc_move_targets->nelts == 0) return SVN_NO_ERROR; *local_moved_to_abspath = APR_ARRAY_IDX(local_details->wc_move_targets, local_details->preferred_move_target_idx, const char *); } return SVN_NO_ERROR; } static svn_error_t * conflict_tree_get_description_update_both_moved_file_merge( const char **description, svn_client_conflict_t *conflict, svn_client_ctx_t *ctx, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const char *incoming_moved_to_abspath; const char *local_moved_to_abspath; svn_wc_operation_t operation; const char *wcroot_abspath; *description = NULL; SVN_ERR(get_both_moved_file_paths(&incoming_moved_to_abspath, &local_moved_to_abspath, conflict, scratch_pool)); if (incoming_moved_to_abspath == NULL || local_moved_to_abspath == NULL) return SVN_NO_ERROR; SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, conflict->local_abspath, scratch_pool, scratch_pool)); operation = svn_client_conflict_get_operation(conflict); if (operation == svn_wc_operation_merge) { /* In case of a merge, the incoming move has A+ (copied) status... */ *description = apr_psprintf( scratch_pool, _("apply changes to '%s' and revert addition of '%s'"), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath), scratch_pool), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath), scratch_pool)); } else { /* ...but in case of update/switch the local move has "A+" status. */ *description = apr_psprintf( scratch_pool, _("override incoming move and merge incoming changes from '%s' " "to '%s'"), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath), scratch_pool), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath), scratch_pool)); } return SVN_NO_ERROR; } static svn_error_t * conflict_tree_get_description_update_both_moved_file_move_merge( const char **description, svn_client_conflict_t *conflict, svn_client_ctx_t *ctx, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const char *incoming_moved_to_abspath; const char *local_moved_to_abspath; svn_wc_operation_t operation; const char *wcroot_abspath; *description = NULL; SVN_ERR(get_both_moved_file_paths(&incoming_moved_to_abspath, &local_moved_to_abspath, conflict, scratch_pool)); if (incoming_moved_to_abspath == NULL || local_moved_to_abspath == NULL) return SVN_NO_ERROR; SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, conflict->local_abspath, scratch_pool, scratch_pool)); operation = svn_client_conflict_get_operation(conflict); if (operation == svn_wc_operation_merge) { SVN_ERR(describe_incoming_move_merge_conflict_option( description, conflict, ctx, local_moved_to_abspath, scratch_pool, scratch_pool)); } else { *description = apr_psprintf( scratch_pool, _("accept incoming move and merge local changes from " "'%s' to '%s'"), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath), scratch_pool), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath), scratch_pool)); } return SVN_NO_ERROR; } /* Configure 'both moved file merge' resolution options for a tree conflict. */ static svn_error_t * configure_option_both_moved_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_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; const char *wcroot_abspath; SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, conflict->local_abspath, scratch_pool, scratch_pool)); operation = svn_client_conflict_get_operation(conflict); victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(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_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)); /* ### what about the switch operation? */ if (((operation == svn_wc_operation_merge && victim_node_kind == svn_node_none) || (operation == svn_wc_operation_update && victim_node_kind == svn_node_file)) && incoming_old_kind == svn_node_file && incoming_new_kind == svn_node_none && ((operation == svn_wc_operation_merge && local_change == svn_wc_conflict_reason_missing) || (operation == svn_wc_operation_update && local_change == svn_wc_conflict_reason_moved_away)) && incoming_change == svn_wc_conflict_action_delete) { const char *description; SVN_ERR(conflict_tree_get_description_update_both_moved_file_merge( &description, conflict, ctx, conflict->pool, scratch_pool)); if (description == NULL) /* details not fetched yet */ return SVN_NO_ERROR; add_resolution_option( options, conflict, svn_client_conflict_option_both_moved_file_merge, _("Merge to corresponding local location"), description, operation == svn_wc_operation_merge ? resolve_both_moved_file_text_merge : resolve_both_moved_file_update_keep_local_move); SVN_ERR(conflict_tree_get_description_update_both_moved_file_move_merge( &description, conflict, ctx, conflict->pool, scratch_pool)); add_resolution_option(options, conflict, svn_client_conflict_option_both_moved_file_move_merge, _("Move and merge"), description, operation == svn_wc_operation_merge ? resolve_incoming_move_file_text_merge : resolve_both_moved_file_update_keep_incoming_move); } return SVN_NO_ERROR; } /* Configure 'both moved dir merge' resolution options for a tree conflict. */ static svn_error_t * configure_option_both_moved_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_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; const char *wcroot_abspath; SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, conflict->local_abspath, scratch_pool, scratch_pool)); operation = svn_client_conflict_get_operation(conflict); victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(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_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 (operation == svn_wc_operation_merge && victim_node_kind == svn_node_none && incoming_old_kind == svn_node_dir && incoming_new_kind == svn_node_none && local_change == svn_wc_conflict_reason_missing && incoming_change == svn_wc_conflict_action_delete) { struct conflict_tree_incoming_delete_details *incoming_details; struct conflict_tree_local_missing_details *local_details; const char *description; apr_array_header_t *local_moves; const char *local_moved_to_abspath; const char *incoming_moved_to_abspath; apr_array_header_t *incoming_move_target_wc_abspaths; incoming_details = conflict->tree_conflict_incoming_details; if (incoming_details == NULL || incoming_details->moves == NULL || apr_hash_count(incoming_details->wc_move_targets) == 0) return SVN_NO_ERROR; local_details = conflict->tree_conflict_local_details; if (local_details == NULL || apr_hash_count(local_details->wc_move_targets) == 0) return SVN_NO_ERROR; local_moves = svn_hash_gets(local_details->wc_move_targets, local_details->move_target_repos_relpath); local_moved_to_abspath = APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, const char *); incoming_move_target_wc_abspaths = svn_hash_gets(incoming_details->wc_move_targets, get_moved_to_repos_relpath(incoming_details, scratch_pool)); incoming_moved_to_abspath = APR_ARRAY_IDX(incoming_move_target_wc_abspaths, incoming_details->wc_move_target_idx, const char *); description = apr_psprintf( scratch_pool, _("apply changes to '%s' and revert addition of '%s'"), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath), scratch_pool), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath), scratch_pool)); add_resolution_option( options, conflict, svn_client_conflict_option_both_moved_dir_merge, _("Merge to corresponding local location"), description, resolve_both_moved_dir_merge); SVN_ERR(describe_incoming_move_merge_conflict_option( &description, conflict, ctx, local_moved_to_abspath, scratch_pool, scratch_pool)); add_resolution_option(options, conflict, svn_client_conflict_option_both_moved_dir_move_merge, _("Move and merge"), description, resolve_both_moved_dir_move_merge); } return SVN_NO_ERROR; } /* Return a copy of the repos replath candidate list. */ static svn_error_t * get_repos_relpath_candidates( apr_array_header_t **possible_moved_to_repos_relpaths, apr_hash_t *wc_move_targets, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { apr_array_header_t *sorted_repos_relpaths; int i; sorted_repos_relpaths = svn_sort__hash(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_get_moved_to_repos_relpath_candidates2( 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; const char *victim_abspath; svn_wc_operation_t operation; svn_wc_conflict_action_t incoming_change; svn_wc_conflict_reason_t local_change; svn_client_conflict_option_id_t id; id = svn_client_conflict_option_get_id(option); if (id != svn_client_conflict_option_incoming_move_file_text_merge && id != svn_client_conflict_option_incoming_move_dir_merge && id != svn_client_conflict_option_local_move_file_text_merge && id != svn_client_conflict_option_local_move_dir_merge && id != svn_client_conflict_option_sibling_move_file_text_merge && id != svn_client_conflict_option_sibling_move_dir_merge && id != svn_client_conflict_option_both_moved_file_merge && id != svn_client_conflict_option_both_moved_file_move_merge && id != svn_client_conflict_option_both_moved_dir_merge && id != svn_client_conflict_option_both_moved_dir_move_merge) { /* We cannot operate on this option. */ *possible_moved_to_repos_relpaths = NULL; return SVN_NO_ERROR; } victim_abspath = svn_client_conflict_get_local_abspath(conflict); 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_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->wc_move_targets == NULL && details->wc_siblings == 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)); if (details->wc_move_targets) SVN_ERR(get_repos_relpath_candidates(possible_moved_to_repos_relpaths, details->wc_move_targets, result_pool, scratch_pool)); else *possible_moved_to_repos_relpaths = NULL; } else { struct conflict_tree_incoming_delete_details *details; 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)); SVN_ERR(get_repos_relpath_candidates(possible_moved_to_repos_relpaths, details->wc_move_targets, result_pool, scratch_pool)); } 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) { /* The only difference to API version 2 is an assertion failure if * an unexpected option is passed. * We do not emulate this old behaviour since clients written against * the previous API will just keep working. */ return svn_error_trace( svn_client_conflict_option_get_moved_to_repos_relpath_candidates2( possible_moved_to_repos_relpaths, option, result_pool, scratch_pool)); } static svn_error_t * set_wc_move_target(const char **new_hash_key, apr_hash_t *wc_move_targets, int preferred_move_target_idx, const char *victim_abspath, apr_pool_t *scratch_pool) { apr_array_header_t *move_target_repos_relpaths; svn_sort__item_t item; const char *move_target_repos_relpath; apr_hash_index_t *hi; if (preferred_move_target_idx < 0 || preferred_move_target_idx >= apr_hash_count(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(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, 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) { *new_hash_key = repos_relpath; 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_set_moved_to_repos_relpath2( 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; const char *victim_abspath; svn_wc_operation_t operation; svn_wc_conflict_action_t incoming_change; svn_wc_conflict_reason_t local_change; svn_client_conflict_option_id_t id; id = svn_client_conflict_option_get_id(option); if (id != svn_client_conflict_option_incoming_move_file_text_merge && id != svn_client_conflict_option_incoming_move_dir_merge && id != svn_client_conflict_option_local_move_file_text_merge && id != svn_client_conflict_option_local_move_dir_merge && id != svn_client_conflict_option_sibling_move_file_text_merge && id != svn_client_conflict_option_sibling_move_dir_merge && id != svn_client_conflict_option_both_moved_file_merge && id != svn_client_conflict_option_both_moved_file_move_merge && id != svn_client_conflict_option_both_moved_dir_merge && id != svn_client_conflict_option_both_moved_dir_move_merge) return SVN_NO_ERROR; /* We cannot operate on this option. Nothing to do. */ victim_abspath = svn_client_conflict_get_local_abspath(conflict); 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_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->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)); SVN_ERR(set_wc_move_target(&details->move_target_repos_relpath, details->wc_move_targets, preferred_move_target_idx, victim_abspath, scratch_pool)); details->wc_move_target_idx = 0; /* Update option description. */ SVN_ERR(conflict_tree_get_description_local_missing( &option->description, conflict, ctx, conflict->pool, scratch_pool)); } else { struct conflict_tree_incoming_delete_details *details; apr_array_header_t *move_target_wc_abspaths; const char *moved_to_abspath; 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)); SVN_ERR(set_wc_move_target(&details->move_target_repos_relpath, details->wc_move_targets, preferred_move_target_idx, victim_abspath, scratch_pool)); details->wc_move_target_idx = 0; /* Update option description. */ 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 *); SVN_ERR(describe_incoming_move_merge_conflict_option( &option->description, conflict, ctx, moved_to_abspath, conflict->pool, scratch_pool)); } 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) { /* The only difference to API version 2 is an assertion failure if * an unexpected option is passed. * We do not emulate this old behaviour since clients written against * the previous API will just keep working. */ return svn_error_trace( svn_client_conflict_option_set_moved_to_repos_relpath2(option, preferred_move_target_idx, ctx, scratch_pool)); } svn_error_t * svn_client_conflict_option_get_moved_to_abspath_candidates2( 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; const char *victim_abspath; svn_wc_operation_t operation; svn_wc_conflict_action_t incoming_change; svn_wc_conflict_reason_t local_change; int i; svn_client_conflict_option_id_t id; id = svn_client_conflict_option_get_id(option); if (id != svn_client_conflict_option_incoming_move_file_text_merge && id != svn_client_conflict_option_incoming_move_dir_merge && id != svn_client_conflict_option_local_move_file_text_merge && id != svn_client_conflict_option_local_move_dir_merge && id != svn_client_conflict_option_sibling_move_file_text_merge && id != svn_client_conflict_option_sibling_move_dir_merge && id != svn_client_conflict_option_both_moved_file_merge && id != svn_client_conflict_option_both_moved_file_move_merge && id != svn_client_conflict_option_both_moved_dir_merge && id != svn_client_conflict_option_both_moved_dir_move_merge) { /* We cannot operate on this option. */ *possible_moved_to_abspaths = NULL; return NULL; } victim_abspath = svn_client_conflict_get_local_abspath(conflict); 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_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->wc_move_targets == NULL && details->wc_siblings == NULL)) return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, _("Getting a list of possible move siblings " "requires details for tree conflict at '%s' " "to be fetched from the repository first"), svn_dirent_local_style(victim_abspath, scratch_pool)); *possible_moved_to_abspaths = apr_array_make(result_pool, 1, sizeof (const char *)); if (details->wc_move_targets) { apr_array_header_t *move_target_wc_abspaths; move_target_wc_abspaths = svn_hash_gets(details->wc_move_targets, details->move_target_repos_relpath); 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); } } /* ### Siblings are actually 'corresponding nodes', not 'move targets'. ### But we provide them here to avoid another API function. */ if (details->wc_siblings) { for (i = 0; i < details->wc_siblings->nelts; i++) { const char *sibling_abspath; sibling_abspath = APR_ARRAY_IDX(details->wc_siblings, i, const char *); APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) = apr_pstrdup(result_pool, sibling_abspath); } } } else if ((operation == svn_wc_operation_update || operation == svn_wc_operation_switch) && incoming_change == svn_wc_conflict_action_delete && local_change == svn_wc_conflict_reason_moved_away) { struct conflict_tree_update_local_moved_away_details *details; details = conflict->tree_conflict_local_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 option's move target candidate list. */ *possible_moved_to_abspaths = apr_array_make(result_pool, details->wc_move_targets->nelts, sizeof (const char *)); for (i = 0; i < details->wc_move_targets->nelts; i++) { const char *moved_to_abspath; moved_to_abspath = APR_ARRAY_IDX(details->wc_move_targets, i, const char *); APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) = apr_pstrdup(result_pool, moved_to_abspath); } } else { struct conflict_tree_incoming_delete_details *details; apr_array_header_t *move_target_wc_abspaths; 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_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) { /* The only difference to API version 2 is an assertion failure if * an unexpected option is passed. * We do not emulate this old behaviour since clients written against * the previous API will just keep working. */ return svn_error_trace( svn_client_conflict_option_get_moved_to_abspath_candidates2( possible_moved_to_abspaths, option, result_pool, scratch_pool)); } svn_error_t * svn_client_conflict_option_set_moved_to_abspath2( 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; const char *victim_abspath; svn_wc_operation_t operation; svn_wc_conflict_action_t incoming_change; svn_wc_conflict_reason_t local_change; svn_client_conflict_option_id_t id; id = svn_client_conflict_option_get_id(option); if (id != svn_client_conflict_option_incoming_move_file_text_merge && id != svn_client_conflict_option_incoming_move_dir_merge && id != svn_client_conflict_option_local_move_file_text_merge && id != svn_client_conflict_option_local_move_dir_merge && id != svn_client_conflict_option_sibling_move_file_text_merge && id != svn_client_conflict_option_sibling_move_dir_merge && id != svn_client_conflict_option_both_moved_file_merge && id != svn_client_conflict_option_both_moved_file_move_merge && id != svn_client_conflict_option_both_moved_dir_merge && id != svn_client_conflict_option_both_moved_dir_move_merge) return NULL; /* We cannot operate on this option. Nothing to do. */ victim_abspath = svn_client_conflict_get_local_abspath(conflict); 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_merge && incoming_change == svn_wc_conflict_action_edit && local_change == svn_wc_conflict_reason_missing) { struct conflict_tree_local_missing_details *details; const char *wcroot_abspath; const char *preferred_sibling; SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, conflict->local_abspath, scratch_pool, scratch_pool)); details = conflict->tree_conflict_local_details; if (details == NULL || (details->wc_siblings == 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 (details->wc_siblings) { if (preferred_move_target_idx < 0 || preferred_move_target_idx > details->wc_siblings->nelts) return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, _("Index '%d' is out of bounds of the " "possible move sibling list for '%s'"), preferred_move_target_idx, svn_dirent_local_style(victim_abspath, scratch_pool)); /* Record the user's preference. */ details->preferred_sibling_idx = preferred_move_target_idx; /* Update option description. */ preferred_sibling = APR_ARRAY_IDX(details->wc_siblings, details->preferred_sibling_idx, const char *); option->description = apr_psprintf( conflict->pool, _("apply changes to '%s'"), svn_dirent_local_style( svn_dirent_skip_ancestor(wcroot_abspath, preferred_sibling), scratch_pool)); } else if (details->wc_move_targets) { apr_array_header_t *move_target_wc_abspaths; move_target_wc_abspaths = svn_hash_gets(details->wc_move_targets, details->move_target_repos_relpath); 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(conflict_tree_get_description_local_missing( &option->description, conflict, ctx, conflict->pool, scratch_pool)); } } else if ((operation == svn_wc_operation_update || operation == svn_wc_operation_switch) && incoming_change == svn_wc_conflict_action_delete && local_change == svn_wc_conflict_reason_moved_away) { struct conflict_tree_update_local_moved_away_details *details; details = conflict->tree_conflict_local_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 > details->wc_move_targets->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->preferred_move_target_idx = preferred_move_target_idx; /* Update option description. */ if (id == svn_client_conflict_option_both_moved_file_merge) SVN_ERR(conflict_tree_get_description_update_both_moved_file_merge( &option->description, conflict, ctx, conflict->pool, scratch_pool)); else if (id == svn_client_conflict_option_both_moved_file_move_merge) SVN_ERR(conflict_tree_get_description_update_both_moved_file_move_merge( &option->description, conflict, ctx, conflict->pool, scratch_pool)); #if 0 /* ### TODO: Also handle options for directories! */ else if (id == svn_client_conflict_option_both_moved_dir_merge) { } else if (id == svn_client_conflict_option_both_moved_dir_move_merge) { } #endif else return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, _("Unexpected option id '%d'"), id); } else { struct conflict_tree_incoming_delete_details *details; apr_array_header_t *move_target_wc_abspaths; const char *moved_to_abspath; 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. */ moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, details->wc_move_target_idx, const char *); SVN_ERR(describe_incoming_move_merge_conflict_option(&option->description, conflict, ctx, moved_to_abspath, conflict->pool, scratch_pool)); } 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) { /* The only difference to API version 2 is an assertion failure if * an unexpected option is passed. * We do not emulate this old behaviour since clients written against * the previous API will just keep working. */ return svn_error_trace( svn_client_conflict_option_set_moved_to_abspath2(option, preferred_move_target_idx, ctx, scratch_pool)); } 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_or_dir_merge(conflict, ctx, *options, scratch_pool)); SVN_ERR(configure_option_sibling_move_merge(conflict, ctx, *options, scratch_pool)); SVN_ERR(configure_option_both_moved_file_merge(conflict, ctx, *options, scratch_pool)); SVN_ERR(configure_option_both_moved_dir_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_operation_t operation; 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; 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); /* 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; } else if (local_change == svn_wc_conflict_reason_moved_away && operation == svn_wc_operation_update /* ### what about switch? */) { conflict->tree_conflict_get_local_details_func = conflict_tree_get_details_update_local_moved_away; } 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); }