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