| /* |
| * conflicts.c: conflict resolver implementation |
| * |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| */ |
| |
| /* ==================================================================== */ |
| |
| |
| |
| /*** Includes. ***/ |
| |
| #include "svn_types.h" |
| #include "svn_wc.h" |
| #include "svn_client.h" |
| #include "svn_error.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_path.h" |
| #include "svn_pools.h" |
| #include "svn_props.h" |
| #include "svn_hash.h" |
| #include "svn_sorts.h" |
| #include "svn_subst.h" |
| #include "client.h" |
| |
| #include "private/svn_diff_tree.h" |
| #include "private/svn_ra_private.h" |
| #include "private/svn_sorts_private.h" |
| #include "private/svn_token.h" |
| #include "private/svn_wc_private.h" |
| |
| #include "svn_private_config.h" |
| |
| #define ARRAY_LEN(ary) ((sizeof (ary)) / (sizeof ((ary)[0]))) |
| |
| |
| /*** Dealing with conflicts. ***/ |
| |
| /* Describe a tree conflict. */ |
| typedef svn_error_t *(*tree_conflict_get_description_func_t)( |
| const char **change_description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool); |
| |
| /* Get more information about a tree conflict. |
| * This function may contact the repository. */ |
| typedef svn_error_t *(*tree_conflict_get_details_func_t)( |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool); |
| |
| struct svn_client_conflict_t |
| { |
| const char *local_abspath; |
| apr_hash_t *prop_conflicts; |
| |
| /* Indicate which options were chosen to resolve a text or tree conflict |
| * on the conflicted node. */ |
| svn_client_conflict_option_id_t resolution_text; |
| svn_client_conflict_option_id_t resolution_tree; |
| |
| /* A mapping from const char* property name to pointers to |
| * svn_client_conflict_option_t for all properties which had their |
| * conflicts resolved. Indicates which options were chosen to resolve |
| * the property conflicts. */ |
| apr_hash_t *resolved_props; |
| |
| /* Ask a tree conflict to describe itself. */ |
| tree_conflict_get_description_func_t |
| tree_conflict_get_incoming_description_func; |
| tree_conflict_get_description_func_t |
| tree_conflict_get_local_description_func; |
| |
| /* Ask a tree conflict to find out more information about itself |
| * by contacting the repository. */ |
| tree_conflict_get_details_func_t tree_conflict_get_incoming_details_func; |
| tree_conflict_get_details_func_t tree_conflict_get_local_details_func; |
| |
| /* Any additional information found can be stored here and may be used |
| * when describing a tree conflict. */ |
| void *tree_conflict_incoming_details; |
| void *tree_conflict_local_details; |
| |
| /* The pool this conflict was allocated from. */ |
| apr_pool_t *pool; |
| |
| /* Conflict data provided by libsvn_wc. */ |
| const svn_wc_conflict_description2_t *legacy_text_conflict; |
| const char *legacy_prop_conflict_propname; |
| const svn_wc_conflict_description2_t *legacy_tree_conflict; |
| |
| /* The recommended resolution option's ID. */ |
| svn_client_conflict_option_id_t recommended_option_id; |
| }; |
| |
| /* Resolves conflict to OPTION and sets CONFLICT->RESOLUTION accordingly. |
| * |
| * May raise an error in case the conflict could not be resolved. A common |
| * case would be a tree conflict the resolution of which depends on other |
| * tree conflicts to be resolved first. */ |
| typedef svn_error_t *(*conflict_option_resolve_func_t)( |
| svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool); |
| |
| struct svn_client_conflict_option_t |
| { |
| svn_client_conflict_option_id_t id; |
| const char *label; |
| const char *description; |
| |
| svn_client_conflict_t *conflict; |
| conflict_option_resolve_func_t do_resolve_func; |
| |
| /* The pool this option was allocated from. */ |
| apr_pool_t *pool; |
| |
| /* Data which is specific to particular conflicts and options. */ |
| union { |
| struct { |
| /* Indicates the property to resolve in case of a property conflict. |
| * If set to "", all properties are resolved to this option. */ |
| const char *propname; |
| |
| /* A merged property value, if supplied by the API user, else NULL. */ |
| const svn_string_t *merged_propval; |
| } prop; |
| } type_data; |
| |
| }; |
| |
| /* |
| * Return a legacy conflict choice corresponding to OPTION_ID. |
| * Return svn_wc_conflict_choose_undefined if no corresponding |
| * legacy conflict choice exists. |
| */ |
| static svn_wc_conflict_choice_t |
| conflict_option_id_to_wc_conflict_choice( |
| svn_client_conflict_option_id_t option_id) |
| { |
| |
| switch (option_id) |
| { |
| case svn_client_conflict_option_undefined: |
| return svn_wc_conflict_choose_undefined; |
| |
| case svn_client_conflict_option_postpone: |
| return svn_wc_conflict_choose_postpone; |
| |
| case svn_client_conflict_option_base_text: |
| return svn_wc_conflict_choose_base; |
| |
| case svn_client_conflict_option_incoming_text: |
| return svn_wc_conflict_choose_theirs_full; |
| |
| case svn_client_conflict_option_working_text: |
| return svn_wc_conflict_choose_mine_full; |
| |
| case svn_client_conflict_option_incoming_text_where_conflicted: |
| return svn_wc_conflict_choose_theirs_conflict; |
| |
| case svn_client_conflict_option_working_text_where_conflicted: |
| return svn_wc_conflict_choose_mine_conflict; |
| |
| case svn_client_conflict_option_merged_text: |
| return svn_wc_conflict_choose_merged; |
| |
| case svn_client_conflict_option_unspecified: |
| return svn_wc_conflict_choose_unspecified; |
| |
| default: |
| break; |
| } |
| |
| return svn_wc_conflict_choose_undefined; |
| } |
| |
| static void |
| add_legacy_desc_to_conflict(const svn_wc_conflict_description2_t *desc, |
| svn_client_conflict_t *conflict, |
| apr_pool_t *result_pool) |
| { |
| switch (desc->kind) |
| { |
| case svn_wc_conflict_kind_text: |
| conflict->legacy_text_conflict = desc; |
| break; |
| |
| case svn_wc_conflict_kind_property: |
| if (conflict->prop_conflicts == NULL) |
| conflict->prop_conflicts = apr_hash_make(result_pool); |
| svn_hash_sets(conflict->prop_conflicts, desc->property_name, desc); |
| conflict->legacy_prop_conflict_propname = desc->property_name; |
| break; |
| |
| case svn_wc_conflict_kind_tree: |
| conflict->legacy_tree_conflict = desc; |
| break; |
| |
| default: |
| SVN_ERR_ASSERT_NO_RETURN(FALSE); /* unknown kind of conflict */ |
| } |
| } |
| |
| /* A map for svn_wc_conflict_action_t values to strings */ |
| static const svn_token_map_t map_conflict_action[] = |
| { |
| { "edit", svn_wc_conflict_action_edit }, |
| { "delete", svn_wc_conflict_action_delete }, |
| { "add", svn_wc_conflict_action_add }, |
| { "replace", svn_wc_conflict_action_replace }, |
| { NULL, 0 } |
| }; |
| |
| /* A map for svn_wc_conflict_reason_t values to strings */ |
| static const svn_token_map_t map_conflict_reason[] = |
| { |
| { "edit", svn_wc_conflict_reason_edited }, |
| { "delete", svn_wc_conflict_reason_deleted }, |
| { "missing", svn_wc_conflict_reason_missing }, |
| { "obstruction", svn_wc_conflict_reason_obstructed }, |
| { "add", svn_wc_conflict_reason_added }, |
| { "replace", svn_wc_conflict_reason_replaced }, |
| { "unversioned", svn_wc_conflict_reason_unversioned }, |
| { "moved-away", svn_wc_conflict_reason_moved_away }, |
| { "moved-here", svn_wc_conflict_reason_moved_here }, |
| { NULL, 0 } |
| }; |
| |
| /* Describes a server-side move (really a copy+delete within the same |
| * revision) which was identified by scanning the revision log. |
| * This structure can represent one or more "chains" of moves, i.e. |
| * multiple move operations which occurred across a range of revisions. */ |
| struct repos_move_info { |
| /* The revision in which this move was committed. */ |
| svn_revnum_t rev; |
| |
| /* The author who committed the revision in which this move was committed. */ |
| const char *rev_author; |
| |
| /* The repository relpath the node was moved from in this revision. */ |
| const char *moved_from_repos_relpath; |
| |
| /* The repository relpath the node was moved to in this revision. */ |
| const char *moved_to_repos_relpath; |
| |
| /* The copyfrom revision of the moved-to path. */ |
| svn_revnum_t copyfrom_rev; |
| |
| /* The node kind of the item being moved. */ |
| svn_node_kind_t node_kind; |
| |
| /* Prev pointer. NULL if no prior move exists in the chain. */ |
| struct repos_move_info *prev; |
| |
| /* An array of struct repos_move_info * elements, each representing |
| * a possible way forward in the move chain. NULL if no next move |
| * exists in this chain. If the deleted node was copied only once in |
| * this revision, then this array has only one element and the move |
| * chain does not fork. But if this revision contains multiple copies of |
| * the deleted node, each of these copies appears as an element of this |
| * array, and each element represents a different path the next move |
| * might have taken. */ |
| apr_array_header_t *next; |
| }; |
| |
| static svn_revnum_t |
| rev_below(svn_revnum_t rev) |
| { |
| SVN_ERR_ASSERT_NO_RETURN(rev != SVN_INVALID_REVNUM); |
| SVN_ERR_ASSERT_NO_RETURN(rev > 0); |
| |
| return rev == 1 ? 1 : rev - 1; |
| } |
| |
| /* Set *RELATED to true if the deleted node DELETED_REPOS_RELPATH@DELETED_REV |
| * is an ancestor of the copied node COPYFROM_PATH@COPYFROM_REV. |
| * If CHECK_LAST_CHANGED_REV is non-zero, also ensure that the copied node |
| * is a copy of the deleted node's last-changed revision's content, rather |
| * than a copy of some older content. If it's not, set *RELATED to false. */ |
| static svn_error_t * |
| check_move_ancestry(svn_boolean_t *related, |
| svn_ra_session_t *ra_session, |
| const char *repos_root_url, |
| const char *deleted_repos_relpath, |
| svn_revnum_t deleted_rev, |
| const char *copyfrom_path, |
| svn_revnum_t copyfrom_rev, |
| svn_boolean_t check_last_changed_rev, |
| apr_pool_t *scratch_pool) |
| { |
| apr_hash_t *locations; |
| const char *deleted_url; |
| const char *deleted_location; |
| apr_array_header_t *location_revisions; |
| const char *old_session_url; |
| |
| location_revisions = apr_array_make(scratch_pool, 1, sizeof(svn_revnum_t)); |
| APR_ARRAY_PUSH(location_revisions, svn_revnum_t) = copyfrom_rev; |
| deleted_url = svn_uri_canonicalize(apr_pstrcat(scratch_pool, |
| repos_root_url, "/", |
| deleted_repos_relpath, |
| NULL), |
| scratch_pool); |
| SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session, |
| deleted_url, scratch_pool)); |
| SVN_ERR(svn_ra_get_locations(ra_session, &locations, "", |
| rev_below(deleted_rev), location_revisions, |
| scratch_pool)); |
| |
| deleted_location = apr_hash_get(locations, ©from_rev, |
| sizeof(svn_revnum_t)); |
| if (deleted_location) |
| { |
| if (deleted_location[0] == '/') |
| deleted_location++; |
| if (strcmp(deleted_location, copyfrom_path) != 0) |
| { |
| *related = FALSE; |
| return SVN_NO_ERROR; |
| } |
| } |
| else |
| { |
| *related = FALSE; |
| return SVN_NO_ERROR; |
| } |
| |
| if (check_last_changed_rev) |
| { |
| svn_dirent_t *dirent; |
| |
| /* Verify that copyfrom_rev >= last-changed revision of the |
| * deleted node. */ |
| SVN_ERR(svn_ra_stat(ra_session, "", rev_below(deleted_rev), &dirent, |
| scratch_pool)); |
| if (dirent == NULL || copyfrom_rev < dirent->created_rev) |
| { |
| *related = FALSE; |
| return SVN_NO_ERROR; |
| } |
| } |
| |
| *related = TRUE; |
| return SVN_NO_ERROR; |
| } |
| |
| struct copy_info { |
| const char *copyto_path; |
| const char *copyfrom_path; |
| svn_revnum_t copyfrom_rev; |
| svn_node_kind_t node_kind; |
| }; |
| |
| /* Allocate and return a NEW_MOVE, and update MOVED_PATHS with this new move. */ |
| static svn_error_t * |
| add_new_move(struct repos_move_info **new_move, |
| const char *deleted_repos_relpath, |
| const char *copyto_path, |
| svn_revnum_t copyfrom_rev, |
| svn_node_kind_t node_kind, |
| svn_revnum_t revision, |
| const char *author, |
| apr_hash_t *moved_paths, |
| svn_ra_session_t *ra_session, |
| const char *repos_root_url, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| struct repos_move_info *move; |
| struct repos_move_info *next_move; |
| |
| move = apr_pcalloc(result_pool, sizeof(*move)); |
| move->moved_from_repos_relpath = apr_pstrdup(result_pool, |
| deleted_repos_relpath); |
| move->moved_to_repos_relpath = apr_pstrdup(result_pool, copyto_path); |
| move->rev = revision; |
| move->rev_author = apr_pstrdup(result_pool, author); |
| move->copyfrom_rev = copyfrom_rev; |
| move->node_kind = node_kind; |
| |
| /* Link together multiple moves of the same node. |
| * Note that we're traversing history backwards, so moves already |
| * present in the list happened in younger revisions. */ |
| next_move = svn_hash_gets(moved_paths, move->moved_to_repos_relpath); |
| if (next_move) |
| { |
| svn_boolean_t related; |
| |
| /* Tracing back history of the delete-half of the next move |
| * to the copyfrom-revision of the prior move we must end up |
| * at the delete-half of the prior move. */ |
| SVN_ERR(check_move_ancestry(&related, ra_session, repos_root_url, |
| next_move->moved_from_repos_relpath, |
| next_move->rev, |
| move->moved_from_repos_relpath, |
| move->copyfrom_rev, |
| FALSE, scratch_pool)); |
| if (related) |
| { |
| SVN_ERR_ASSERT(move->rev < next_move->rev); |
| |
| /* Prepend this move to the linked list. */ |
| if (move->next == NULL) |
| move->next = apr_array_make(result_pool, 1, |
| sizeof (struct repos_move_info *)); |
| APR_ARRAY_PUSH(move->next, struct repos_move_info *) = next_move; |
| next_move->prev = move; |
| } |
| } |
| |
| /* Make this move the head of our next-move linking map. */ |
| svn_hash_sets(moved_paths, move->moved_from_repos_relpath, move); |
| |
| *new_move = move; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Push a MOVE into the MOVES_TABLE. */ |
| static void |
| push_move(struct repos_move_info *move, apr_hash_t *moves_table, |
| apr_pool_t *result_pool) |
| { |
| apr_array_header_t *moves; |
| |
| /* Add this move to the list of moves in the revision. */ |
| moves = apr_hash_get(moves_table, &move->rev, sizeof(svn_revnum_t)); |
| if (moves == NULL) |
| { |
| /* It is the first move in this revision. Create the list. */ |
| moves = apr_array_make(result_pool, 1, sizeof(struct repos_move_info *)); |
| apr_hash_set(moves_table, &move->rev, sizeof(svn_revnum_t), moves); |
| } |
| APR_ARRAY_PUSH(moves, struct repos_move_info *) = move; |
| } |
| |
| /* Find the youngest common ancestor of REPOS_RELPATH1@PEG_REV1 and |
| * REPOS_RELPATH2@PEG_REV2. Return the result in *YCA_LOC. |
| * Set *YCA_LOC to NULL if no common ancestor exists. */ |
| static svn_error_t * |
| find_yca(svn_client__pathrev_t **yca_loc, |
| const char *repos_relpath1, |
| svn_revnum_t peg_rev1, |
| const char *repos_relpath2, |
| svn_revnum_t peg_rev2, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| svn_ra_session_t *ra_session, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client__pathrev_t *loc1; |
| svn_client__pathrev_t *loc2; |
| |
| *yca_loc = NULL; |
| |
| loc1 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid, |
| peg_rev1, repos_relpath1, |
| scratch_pool); |
| loc2 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid, |
| peg_rev2, repos_relpath2, |
| scratch_pool); |
| SVN_ERR(svn_client__get_youngest_common_ancestor(yca_loc, loc1, loc2, |
| ra_session, ctx, |
| result_pool, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Like find_yca, expect that a YCA could also be found via a brute-force |
| * search of parents of REPOS_RELPATH1 and REPOS_RELPATH2, if no "direct" |
| * YCA exists. An implicit assumption is that some parent of REPOS_RELPATH1 |
| * is a branch of some parent of REPOS_RELPATH2. |
| * |
| * This function can guess a "good enough" YCA for 'missing nodes' which do |
| * not exist in the working copy, e.g. when a file edit is merged to a path |
| * which does not exist in the working copy. |
| */ |
| static svn_error_t * |
| find_nearest_yca(svn_client__pathrev_t **yca_locp, |
| const char *repos_relpath1, |
| svn_revnum_t peg_rev1, |
| const char *repos_relpath2, |
| svn_revnum_t peg_rev2, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| svn_ra_session_t *ra_session, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client__pathrev_t *yca_loc; |
| svn_error_t *err; |
| apr_pool_t *iterpool; |
| const char *p1, *p2; |
| apr_size_t c1, c2; |
| |
| *yca_locp = NULL; |
| |
| iterpool = svn_pool_create(scratch_pool); |
| |
| p1 = repos_relpath1; |
| c1 = svn_path_component_count(repos_relpath1); |
| while (c1--) |
| { |
| svn_pool_clear(iterpool); |
| |
| p2 = repos_relpath2; |
| c2 = svn_path_component_count(repos_relpath2); |
| while (c2--) |
| { |
| err = find_yca(&yca_loc, p1, peg_rev1, p2, peg_rev2, |
| repos_root_url, repos_uuid, ra_session, ctx, |
| result_pool, iterpool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_FS_NOT_FOUND) |
| { |
| svn_error_clear(err); |
| yca_loc = NULL; |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| if (yca_loc) |
| { |
| *yca_locp = yca_loc; |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| p2 = svn_relpath_dirname(p2, scratch_pool); |
| } |
| |
| p1 = svn_relpath_dirname(p1, scratch_pool); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Check if the copied node described by COPY and the DELETED_PATH@DELETED_REV |
| * share a common ancestor. If so, return new repos_move_info in *MOVE which |
| * describes a move from the deleted path to that copy's destination. */ |
| static svn_error_t * |
| find_related_move(struct repos_move_info **move, |
| struct copy_info *copy, |
| const char *deleted_repos_relpath, |
| svn_revnum_t deleted_rev, |
| const char *author, |
| apr_hash_t *moved_paths, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| svn_client_ctx_t *ctx, |
| svn_ra_session_t *ra_session, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client__pathrev_t *yca_loc; |
| svn_error_t *err; |
| |
| *move = NULL; |
| err = find_yca(&yca_loc, copy->copyfrom_path, copy->copyfrom_rev, |
| deleted_repos_relpath, rev_below(deleted_rev), |
| repos_root_url, repos_uuid, ra_session, ctx, |
| scratch_pool, scratch_pool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_FS_NOT_FOUND) |
| { |
| svn_error_clear(err); |
| yca_loc = NULL; |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| if (yca_loc) |
| SVN_ERR(add_new_move(move, deleted_repos_relpath, |
| copy->copyto_path, copy->copyfrom_rev, |
| copy->node_kind, deleted_rev, author, |
| moved_paths, ra_session, repos_root_url, |
| result_pool, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Detect moves by matching DELETED_REPOS_RELPATH@DELETED_REV to the copies |
| * in COPIES. Add any moves found to MOVES_TABLE and update MOVED_PATHS. */ |
| static svn_error_t * |
| match_copies_to_deletion(const char *deleted_repos_relpath, |
| svn_revnum_t deleted_rev, |
| const char *author, |
| apr_hash_t *copies, |
| apr_hash_t *moves_table, |
| apr_hash_t *moved_paths, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| svn_ra_session_t *ra_session, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_hash_index_t *hi; |
| apr_pool_t *iterpool; |
| |
| iterpool = svn_pool_create(scratch_pool); |
| for (hi = apr_hash_first(scratch_pool, copies); |
| hi != NULL; |
| hi = apr_hash_next(hi)) |
| { |
| const char *copyfrom_path = apr_hash_this_key(hi); |
| apr_array_header_t *copies_with_same_source_path; |
| int i; |
| |
| svn_pool_clear(iterpool); |
| |
| copies_with_same_source_path = apr_hash_this_val(hi); |
| |
| if (strcmp(copyfrom_path, deleted_repos_relpath) == 0) |
| { |
| /* We found a copyfrom path which matches a deleted node. |
| * Check if the deleted node is an ancestor of the copied node. */ |
| for (i = 0; i < copies_with_same_source_path->nelts; i++) |
| { |
| struct copy_info *copy; |
| svn_boolean_t related; |
| struct repos_move_info *move; |
| |
| copy = APR_ARRAY_IDX(copies_with_same_source_path, i, |
| struct copy_info *); |
| SVN_ERR(check_move_ancestry(&related, |
| ra_session, repos_root_url, |
| deleted_repos_relpath, |
| deleted_rev, |
| copy->copyfrom_path, |
| copy->copyfrom_rev, |
| TRUE, iterpool)); |
| if (!related) |
| continue; |
| |
| /* Remember details of this move. */ |
| SVN_ERR(add_new_move(&move, deleted_repos_relpath, |
| copy->copyto_path, copy->copyfrom_rev, |
| copy->node_kind, deleted_rev, author, |
| moved_paths, ra_session, repos_root_url, |
| result_pool, iterpool)); |
| push_move(move, moves_table, result_pool); |
| } |
| } |
| else |
| { |
| /* Check if this deleted node is related to any copies in this |
| * revision. These could be moves of the deleted node which |
| * were merged here from other lines of history. */ |
| for (i = 0; i < copies_with_same_source_path->nelts; i++) |
| { |
| struct copy_info *copy; |
| struct repos_move_info *move = NULL; |
| |
| copy = APR_ARRAY_IDX(copies_with_same_source_path, i, |
| struct copy_info *); |
| SVN_ERR(find_related_move(&move, copy, deleted_repos_relpath, |
| deleted_rev, author, |
| moved_paths, |
| repos_root_url, repos_uuid, |
| ctx, ra_session, |
| result_pool, iterpool)); |
| if (move) |
| push_move(move, moves_table, result_pool); |
| } |
| } |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Update MOVES_TABLE and MOVED_PATHS based on information from |
| * revision data in LOG_ENTRY, COPIES, and DELETED_PATHS. |
| * Use RA_SESSION to perform the necessary requests. */ |
| static svn_error_t * |
| find_moves_in_revision(svn_ra_session_t *ra_session, |
| apr_hash_t *moves_table, |
| apr_hash_t *moved_paths, |
| svn_log_entry_t *log_entry, |
| apr_hash_t *copies, |
| apr_array_header_t *deleted_paths, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_pool_t *iterpool; |
| int i; |
| const svn_string_t *author; |
| |
| author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR); |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < deleted_paths->nelts; i++) |
| { |
| const char *deleted_repos_relpath; |
| |
| svn_pool_clear(iterpool); |
| |
| deleted_repos_relpath = APR_ARRAY_IDX(deleted_paths, i, const char *); |
| SVN_ERR(match_copies_to_deletion(deleted_repos_relpath, |
| log_entry->revision, |
| author ? author->data |
| : _("unknown author"), |
| copies, moves_table, moved_paths, |
| repos_root_url, repos_uuid, ra_session, |
| ctx, result_pool, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| struct find_deleted_rev_baton |
| { |
| /* Variables below are arguments provided by the caller of |
| * svn_ra_get_log2(). */ |
| const char *deleted_repos_relpath; |
| const char *related_repos_relpath; |
| svn_revnum_t related_peg_rev; |
| const char *repos_root_url; |
| const char *repos_uuid; |
| svn_client_ctx_t *ctx; |
| const char *victim_abspath; /* for notifications */ |
| |
| /* Variables below are results for the caller of svn_ra_get_log2(). */ |
| svn_revnum_t deleted_rev; |
| const char *deleted_rev_author; |
| svn_node_kind_t replacing_node_kind; |
| apr_pool_t *result_pool; |
| |
| apr_hash_t *moves_table; /* Obtained from find_moves_in_revision(). */ |
| struct repos_move_info *move; /* Last known move which affected the node. */ |
| |
| /* Extra RA session that can be used to make additional requests. */ |
| svn_ra_session_t *extra_ra_session; |
| }; |
| |
| /* If DELETED_RELPATH matches the moved-from path of a move in MOVES, |
| * or if DELETED_RELPATH is a child of a moved-to path in MOVES, return |
| * a struct move_info for the corresponding move. Else, return NULL. */ |
| static struct repos_move_info * |
| map_deleted_path_to_move(const char *deleted_relpath, |
| apr_array_header_t *moves, |
| apr_pool_t *scratch_pool) |
| { |
| struct repos_move_info *closest_move = NULL; |
| apr_size_t min_components = 0; |
| int i; |
| |
| for (i = 0; i < moves->nelts; i++) |
| { |
| const char *relpath; |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(moves, i, struct repos_move_info *); |
| if (strcmp(move->moved_from_repos_relpath, deleted_relpath) == 0) |
| return move; |
| |
| relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, |
| deleted_relpath); |
| if (relpath) |
| { |
| /* This could be a nested move. Return the path-wise closest move. */ |
| const apr_size_t c = svn_path_component_count(relpath); |
| if (c == 0) |
| return move; |
| else if (min_components == 0 || c < min_components) |
| { |
| min_components = c; |
| closest_move = move; |
| } |
| } |
| } |
| |
| if (closest_move) |
| { |
| const char *relpath; |
| |
| /* See if we can find an even closer move for this moved-along path. */ |
| relpath = svn_relpath_skip_ancestor(closest_move->moved_to_repos_relpath, |
| deleted_relpath); |
| if (relpath && relpath[0] != '\0') |
| { |
| struct repos_move_info *move; |
| const char *moved_along_path = |
| svn_relpath_join(closest_move->moved_from_repos_relpath, relpath, |
| scratch_pool); |
| move = map_deleted_path_to_move(moved_along_path, moves, scratch_pool); |
| if (move) |
| return move; |
| } |
| } |
| |
| return closest_move; |
| } |
| |
| /* Search for nested moves in REVISION, given the already found MOVES, |
| * all DELETED_PATHS, and all COPIES, from the same revision. |
| * Append any nested moves to the MOVES array. */ |
| static svn_error_t * |
| find_nested_moves(apr_array_header_t *moves, |
| apr_hash_t *copies, |
| apr_array_header_t *deleted_paths, |
| apr_hash_t *moved_paths, |
| svn_revnum_t revision, |
| const char *author, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| svn_ra_session_t *ra_session, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *nested_moves; |
| int i; |
| apr_pool_t *iterpool; |
| |
| nested_moves = apr_array_make(result_pool, 0, |
| sizeof(struct repos_move_info *)); |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < deleted_paths->nelts; i++) |
| { |
| const char *deleted_path; |
| const char *child_relpath; |
| const char *moved_along_repos_relpath; |
| struct repos_move_info *move; |
| apr_array_header_t *copies_with_same_source_path; |
| int j; |
| svn_boolean_t related; |
| |
| svn_pool_clear(iterpool); |
| |
| deleted_path = APR_ARRAY_IDX(deleted_paths, i, const char *); |
| move = map_deleted_path_to_move(deleted_path, moves, iterpool); |
| if (move == NULL) |
| continue; |
| child_relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, |
| deleted_path); |
| if (child_relpath == NULL || child_relpath[0] == '\0') |
| continue; /* not a nested move */ |
| |
| /* Consider: svn mv A B; svn mv B/foo C/foo |
| * Copyfrom for C/foo is A/foo, even though C/foo was moved here from |
| * B/foo. A/foo was not deleted. It is B/foo which was deleted. |
| * We now know about the move A->B and moved-along child_relpath "foo". |
| * Try to detect an ancestral relationship between A/foo and the |
| * moved-along path. */ |
| moved_along_repos_relpath = |
| svn_relpath_join(move->moved_from_repos_relpath, child_relpath, |
| iterpool); |
| copies_with_same_source_path = svn_hash_gets(copies, |
| moved_along_repos_relpath); |
| if (copies_with_same_source_path == NULL) |
| continue; /* not a nested move */ |
| |
| for (j = 0; j < copies_with_same_source_path->nelts; j++) |
| { |
| struct copy_info *copy; |
| |
| copy = APR_ARRAY_IDX(copies_with_same_source_path, j, |
| struct copy_info *); |
| SVN_ERR(check_move_ancestry(&related, ra_session, repos_root_url, |
| moved_along_repos_relpath, |
| revision, |
| copy->copyfrom_path, |
| copy->copyfrom_rev, |
| TRUE, iterpool)); |
| if (related) |
| { |
| struct repos_move_info *nested_move; |
| |
| /* Remember details of this move. */ |
| SVN_ERR(add_new_move(&nested_move, moved_along_repos_relpath, |
| copy->copyto_path, copy->copyfrom_rev, |
| copy->node_kind, |
| revision, author, moved_paths, |
| ra_session, repos_root_url, |
| result_pool, iterpool)); |
| |
| /* Add this move to the list of nested moves in this revision. */ |
| APR_ARRAY_PUSH(nested_moves, struct repos_move_info *) = |
| nested_move; |
| } |
| } |
| } |
| svn_pool_destroy(iterpool); |
| |
| /* Add all nested moves found to the list of all moves in this revision. */ |
| apr_array_cat(moves, nested_moves); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Make a shallow copy of the copied LOG_ITEM in COPIES. */ |
| static void |
| cache_copied_item(apr_hash_t *copies, const char *changed_path, |
| svn_log_changed_path2_t *log_item) |
| { |
| apr_pool_t *result_pool = apr_hash_pool_get(copies); |
| struct copy_info *copy = apr_palloc(result_pool, sizeof(*copy)); |
| apr_array_header_t *copies_with_same_source_path; |
| |
| copy->copyfrom_path = log_item->copyfrom_path; |
| if (log_item->copyfrom_path[0] == '/') |
| copy->copyfrom_path++; |
| copy->copyto_path = changed_path; |
| copy->copyfrom_rev = log_item->copyfrom_rev; |
| copy->node_kind = log_item->node_kind; |
| |
| copies_with_same_source_path = apr_hash_get(copies, copy->copyfrom_path, |
| APR_HASH_KEY_STRING); |
| if (copies_with_same_source_path == NULL) |
| { |
| copies_with_same_source_path = apr_array_make(result_pool, 1, |
| sizeof(struct copy_info *)); |
| apr_hash_set(copies, copy->copyfrom_path, APR_HASH_KEY_STRING, |
| copies_with_same_source_path); |
| } |
| APR_ARRAY_PUSH(copies_with_same_source_path, struct copy_info *) = copy; |
| } |
| |
| /* Implements svn_log_entry_receiver_t. |
| * |
| * Find the revision in which a node, optionally ancestrally related to the |
| * node specified via find_deleted_rev_baton, was deleted, When the revision |
| * was found, store it in BATON->DELETED_REV and abort the log operation |
| * by raising SVN_ERR_CEASE_INVOCATION. |
| * |
| * If no such revision can be found, leave BATON->DELETED_REV and |
| * BATON->REPLACING_NODE_KIND alone. |
| * |
| * If the node was replaced, set BATON->REPLACING_NODE_KIND to the node |
| * kind of the node which replaced the original node. If the node was not |
| * replaced, set BATON->REPLACING_NODE_KIND to svn_node_none. |
| * |
| * This function answers the same question as svn_ra_get_deleted_rev() but |
| * works in cases where we do not already know a revision in which the deleted |
| * node once used to exist. |
| * |
| * If the node was moved, rather than deleted, return move information |
| * in BATON->MOVE. |
| */ |
| static svn_error_t * |
| find_deleted_rev(void *baton, |
| svn_log_entry_t *log_entry, |
| apr_pool_t *scratch_pool) |
| { |
| struct find_deleted_rev_baton *b = baton; |
| apr_hash_index_t *hi; |
| apr_pool_t *iterpool; |
| svn_boolean_t deleted_node_found = FALSE; |
| svn_node_kind_t replacing_node_kind = svn_node_none; |
| |
| if (b->ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify( |
| b->victim_abspath, |
| svn_wc_notify_tree_conflict_details_progress, |
| scratch_pool), |
| notify->revision = log_entry->revision; |
| b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| /* No paths were changed in this revision. Nothing to do. */ |
| if (! log_entry->changed_paths2) |
| return SVN_NO_ERROR; |
| |
| iterpool = svn_pool_create(scratch_pool); |
| for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); |
| hi != NULL; |
| hi = apr_hash_next(hi)) |
| { |
| const char *changed_path = apr_hash_this_key(hi); |
| svn_log_changed_path2_t *log_item = apr_hash_this_val(hi); |
| |
| svn_pool_clear(iterpool); |
| |
| /* ### Remove leading slash from paths in log entries. */ |
| if (changed_path[0] == '/') |
| changed_path++; |
| |
| /* Check if we already found the deleted node we're looking for. */ |
| if (!deleted_node_found && |
| svn_path_compare_paths(b->deleted_repos_relpath, changed_path) == 0 && |
| (log_item->action == 'D' || log_item->action == 'R')) |
| { |
| deleted_node_found = TRUE; |
| |
| if (b->related_repos_relpath != NULL && |
| b->related_peg_rev != SVN_INVALID_REVNUM) |
| { |
| svn_client__pathrev_t *yca_loc; |
| svn_error_t *err; |
| |
| /* We found a deleted node which occupies the correct path. |
| * To be certain that this is the deleted node we're looking for, |
| * we must establish whether it is ancestrally related to the |
| * "related node" specified in our baton. */ |
| err = find_yca(&yca_loc, |
| b->related_repos_relpath, |
| b->related_peg_rev, |
| b->deleted_repos_relpath, |
| rev_below(log_entry->revision), |
| b->repos_root_url, b->repos_uuid, |
| b->extra_ra_session, b->ctx, iterpool, iterpool); |
| if (err) |
| { |
| /* ### Happens for moves within other moves and copies. */ |
| if (err->apr_err == SVN_ERR_FS_NOT_FOUND) |
| { |
| svn_error_clear(err); |
| yca_loc = NULL; |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| deleted_node_found = (yca_loc != NULL); |
| } |
| |
| if (deleted_node_found && log_item->action == 'R') |
| replacing_node_kind = log_item->node_kind; |
| } |
| } |
| svn_pool_destroy(iterpool); |
| |
| if (!deleted_node_found) |
| { |
| apr_array_header_t *moves; |
| |
| if (b->moves_table == NULL) |
| return SVN_NO_ERROR; |
| |
| moves = apr_hash_get(b->moves_table, &log_entry->revision, |
| sizeof(svn_revnum_t)); |
| if (moves) |
| { |
| struct repos_move_info *move; |
| |
| move = map_deleted_path_to_move(b->deleted_repos_relpath, |
| moves, scratch_pool); |
| if (move) |
| { |
| const char *relpath; |
| |
| /* The node was moved. Update our search path accordingly. */ |
| b->move = move; |
| relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, |
| b->deleted_repos_relpath); |
| if (relpath) |
| b->deleted_repos_relpath = |
| svn_relpath_join(move->moved_from_repos_relpath, relpath, |
| b->result_pool); |
| } |
| } |
| } |
| else |
| { |
| svn_string_t *author; |
| |
| b->deleted_rev = log_entry->revision; |
| author = svn_hash_gets(log_entry->revprops, |
| SVN_PROP_REVISION_AUTHOR); |
| if (author) |
| b->deleted_rev_author = apr_pstrdup(b->result_pool, author->data); |
| else |
| b->deleted_rev_author = _("unknown author"); |
| |
| b->replacing_node_kind = replacing_node_kind; |
| |
| /* We're done. Abort the log operation. */ |
| return svn_error_create(SVN_ERR_CEASE_INVOCATION, NULL, NULL); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return a localised string representation of the local part of a tree |
| conflict on a file. */ |
| static svn_error_t * |
| describe_local_file_node_change(const char **description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_conflict_reason_t local_change; |
| svn_wc_operation_t operation; |
| |
| local_change = svn_client_conflict_get_local_change(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| |
| switch (local_change) |
| { |
| case svn_wc_conflict_reason_edited: |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| *description = _("A file containing uncommitted changes was " |
| "found in the working copy."); |
| else if (operation == svn_wc_operation_merge) |
| *description = _("A file which differs from the corresponding " |
| "file on the merge source branch was found " |
| "in the working copy."); |
| break; |
| case svn_wc_conflict_reason_obstructed: |
| *description = _("A file which already occupies this path was found " |
| "in the working copy."); |
| break; |
| case svn_wc_conflict_reason_unversioned: |
| *description = _("An unversioned file was found in the working " |
| "copy."); |
| break; |
| case svn_wc_conflict_reason_deleted: |
| *description = _("A deleted file was found in the working copy."); |
| break; |
| case svn_wc_conflict_reason_missing: |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| *description = _("No such file was found in the working copy."); |
| else if (operation == svn_wc_operation_merge) |
| { |
| /* ### display deleted revision */ |
| *description = _("No such file was found in the merge target " |
| "working copy.\nPerhaps the file has been " |
| "deleted or moved away in the repository's " |
| "history?"); |
| } |
| break; |
| case svn_wc_conflict_reason_added: |
| case svn_wc_conflict_reason_replaced: |
| { |
| /* ### show more details about copies or replacements? */ |
| *description = _("A file scheduled to be added to the " |
| "repository in the next commit was found in " |
| "the working copy."); |
| } |
| break; |
| case svn_wc_conflict_reason_moved_away: |
| { |
| const char *moved_to_abspath; |
| svn_error_t *err; |
| |
| err = svn_wc__node_was_moved_away(&moved_to_abspath, NULL, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) |
| { |
| moved_to_abspath = NULL; |
| svn_error_clear(err); |
| } |
| else |
| return svn_error_trace(err); |
| } |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| if (moved_to_abspath == NULL) |
| { |
| /* The move no longer exists. */ |
| *description = _("The file in the working copy had " |
| "been moved away at the time this " |
| "conflict was recorded."); |
| } |
| else |
| { |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| *description = apr_psprintf( |
| result_pool, |
| _("The file in the working copy was " |
| "moved away to\n'%s'."), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| moved_to_abspath), |
| scratch_pool)); |
| } |
| } |
| else if (operation == svn_wc_operation_merge) |
| { |
| if (moved_to_abspath == NULL) |
| { |
| /* The move probably happened in branch history. |
| * This case cannot happen until we detect incoming |
| * moves, which we currently don't do. */ |
| /* ### find deleted/moved revision? */ |
| *description = _("The file in the working copy had " |
| "been moved away at the time this " |
| "conflict was recorded."); |
| } |
| else |
| { |
| /* This is a local move in the working copy. */ |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| *description = apr_psprintf( |
| result_pool, |
| _("The file in the working copy was " |
| "moved away to\n'%s'."), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| moved_to_abspath), |
| scratch_pool)); |
| } |
| } |
| break; |
| } |
| case svn_wc_conflict_reason_moved_here: |
| { |
| const char *moved_from_abspath; |
| |
| SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| if (moved_from_abspath == NULL) |
| { |
| /* The move no longer exists. */ |
| *description = _("A file had been moved here in the " |
| "working copy at the time this " |
| "conflict was recorded."); |
| } |
| else |
| { |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| *description = apr_psprintf( |
| result_pool, |
| _("A file was moved here in the " |
| "working copy from\n'%s'."), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| moved_from_abspath), |
| scratch_pool)); |
| } |
| } |
| else if (operation == svn_wc_operation_merge) |
| { |
| if (moved_from_abspath == NULL) |
| { |
| /* The move probably happened in branch history. |
| * This case cannot happen until we detect incoming |
| * moves, which we currently don't do. */ |
| /* ### find deleted/moved revision? */ |
| *description = _("A file had been moved here in the " |
| "working copy at the time this " |
| "conflict was recorded."); |
| } |
| else |
| { |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| /* This is a local move in the working copy. */ |
| *description = apr_psprintf( |
| result_pool, |
| _("A file was moved here in the " |
| "working copy from\n'%s'."), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| moved_from_abspath), |
| scratch_pool)); |
| } |
| } |
| break; |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return a localised string representation of the local part of a tree |
| conflict on a directory. */ |
| static svn_error_t * |
| describe_local_dir_node_change(const char **description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_conflict_reason_t local_change; |
| svn_wc_operation_t operation; |
| |
| local_change = svn_client_conflict_get_local_change(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| |
| switch (local_change) |
| { |
| case svn_wc_conflict_reason_edited: |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| *description = _("A directory containing uncommitted changes " |
| "was found in the working copy."); |
| else if (operation == svn_wc_operation_merge) |
| *description = _("A directory which differs from the " |
| "corresponding directory on the merge source " |
| "branch was found in the working copy."); |
| break; |
| case svn_wc_conflict_reason_obstructed: |
| *description = _("A directory which already occupies this path was " |
| "found in the working copy."); |
| break; |
| case svn_wc_conflict_reason_unversioned: |
| *description = _("An unversioned directory was found in the " |
| "working copy."); |
| break; |
| case svn_wc_conflict_reason_deleted: |
| *description = _("A deleted directory was found in the " |
| "working copy."); |
| break; |
| case svn_wc_conflict_reason_missing: |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| *description = _("No such directory was found in the working copy."); |
| else if (operation == svn_wc_operation_merge) |
| { |
| /* ### display deleted revision */ |
| *description = _("No such directory was found in the merge " |
| "target working copy.\nPerhaps the " |
| "directory has been deleted or moved away " |
| "in the repository's history?"); |
| } |
| break; |
| case svn_wc_conflict_reason_added: |
| case svn_wc_conflict_reason_replaced: |
| { |
| /* ### show more details about copies or replacements? */ |
| *description = _("A directory scheduled to be added to the " |
| "repository in the next commit was found in " |
| "the working copy."); |
| } |
| break; |
| case svn_wc_conflict_reason_moved_away: |
| { |
| const char *moved_to_abspath; |
| svn_error_t *err; |
| |
| err = svn_wc__node_was_moved_away(&moved_to_abspath, NULL, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) |
| { |
| moved_to_abspath = NULL; |
| svn_error_clear(err); |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| if (moved_to_abspath == NULL) |
| { |
| /* The move no longer exists. */ |
| *description = _("The directory in the working copy " |
| "had been moved away at the time " |
| "this conflict was recorded."); |
| } |
| else |
| { |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| *description = apr_psprintf( |
| result_pool, |
| _("The directory in the working copy " |
| "was moved away to\n'%s'."), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| moved_to_abspath), |
| scratch_pool)); |
| } |
| } |
| else if (operation == svn_wc_operation_merge) |
| { |
| if (moved_to_abspath == NULL) |
| { |
| /* The move probably happened in branch history. |
| * This case cannot happen until we detect incoming |
| * moves, which we currently don't do. */ |
| /* ### find deleted/moved revision? */ |
| *description = _("The directory had been moved away " |
| "at the time this conflict was " |
| "recorded."); |
| } |
| else |
| { |
| /* This is a local move in the working copy. */ |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| *description = apr_psprintf( |
| result_pool, |
| _("The directory was moved away to\n" |
| "'%s'."), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| moved_to_abspath), |
| scratch_pool)); |
| } |
| } |
| } |
| break; |
| case svn_wc_conflict_reason_moved_here: |
| { |
| const char *moved_from_abspath; |
| |
| SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| if (moved_from_abspath == NULL) |
| { |
| /* The move no longer exists. */ |
| *description = _("A directory had been moved here at " |
| "the time this conflict was " |
| "recorded."); |
| } |
| else |
| { |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| *description = apr_psprintf( |
| result_pool, |
| _("A directory was moved here from\n" |
| "'%s'."), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| moved_from_abspath), |
| scratch_pool)); |
| } |
| } |
| else if (operation == svn_wc_operation_merge) |
| { |
| if (moved_from_abspath == NULL) |
| { |
| /* The move probably happened in branch history. |
| * This case cannot happen until we detect incoming |
| * moves, which we currently don't do. */ |
| /* ### find deleted/moved revision? */ |
| *description = _("A directory had been moved here at " |
| "the time this conflict was " |
| "recorded."); |
| } |
| else |
| { |
| /* This is a local move in the working copy. */ |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| *description = apr_psprintf( |
| result_pool, |
| _("A directory was moved here in " |
| "the working copy from\n'%s'."), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| moved_from_abspath), |
| scratch_pool)); |
| } |
| } |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| struct find_moves_baton |
| { |
| /* Variables below are arguments provided by the caller of |
| * svn_ra_get_log2(). */ |
| const char *repos_root_url; |
| const char *repos_uuid; |
| svn_client_ctx_t *ctx; |
| const char *victim_abspath; /* for notifications */ |
| apr_pool_t *result_pool; |
| |
| /* A hash table mapping a revision number to an array of struct |
| * repos_move_info * elements, describing moves. |
| * |
| * Must be allocated in RESULT_POOL by the caller of svn_ra_get_log2(). |
| * |
| * If the node was moved, the DELETED_REV is present in this table, |
| * perhaps along with additional revisions. |
| * |
| * Given a sequence of moves which happened in the repository, such as: |
| * rA: mv x->z |
| * rA: mv a->b |
| * rB: mv b->c |
| * rC: mv c->d |
| * we map each revision number to all the moves which happened in the |
| * revision, which looks as follows: |
| * rA : [(x->z), (a->b)] |
| * rB : [(b->c)] |
| * rC : [(c->d)] |
| * This allows us to later find relevant moves based on a revision number. |
| * |
| * Additionally, we embed the number of the revision in which a move was |
| * found inside the repos_move_info structure: |
| * rA : [(rA, x->z), (rA, a->b)] |
| * rB : [(rB, b->c)] |
| * rC : [(rC, c->d)] |
| * And also, all moves pertaining to the same node are chained into a |
| * doubly-linked list via 'next' and 'prev' pointers (see definition of |
| * struct repos_move_info). This can be visualized as follows: |
| * rA : [(rA, x->z, prev=>NULL, next=>NULL), |
| * (rA, a->b, prev=>NULL, next=>(rB, b->c))] |
| * rB : [(rB, b->c), prev=>(rA, a->b), next=>(rC, c->d)] |
| * rC : [(rC, c->d), prev=>(rB, c->d), next=>NULL] |
| * This way, we can look up all moves relevant to a node, forwards and |
| * backwards in history, once we have located one move in the chain. |
| * |
| * In the above example, the data tells us that within the revision |
| * range rA:C, a was moved to d. However, within the revision range |
| * rA;B, a was moved to b. |
| */ |
| apr_hash_t *moves_table; |
| |
| /* Variables below hold state for find_moves() and are not |
| * intended to be used by the caller of svn_ra_get_log2(). |
| * Like all other variables, they must be initialized, however. */ |
| |
| /* Temporary map of moved paths to struct repos_move_info. |
| * Used to link multiple moves of the same node across revisions. */ |
| apr_hash_t *moved_paths; |
| |
| /* Extra RA session that can be used to make additional requests. */ |
| svn_ra_session_t *extra_ra_session; |
| }; |
| |
| /* Implements svn_log_entry_receiver_t. */ |
| static svn_error_t * |
| find_moves(void *baton, svn_log_entry_t *log_entry, apr_pool_t *scratch_pool) |
| { |
| struct find_moves_baton *b = baton; |
| apr_hash_index_t *hi; |
| apr_pool_t *iterpool; |
| apr_array_header_t *deleted_paths; |
| apr_hash_t *copies; |
| apr_array_header_t *moves; |
| |
| if (b->ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify( |
| b->victim_abspath, |
| svn_wc_notify_tree_conflict_details_progress, |
| scratch_pool), |
| notify->revision = log_entry->revision; |
| b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| /* No paths were changed in this revision. Nothing to do. */ |
| if (! log_entry->changed_paths2) |
| return SVN_NO_ERROR; |
| |
| copies = apr_hash_make(scratch_pool); |
| deleted_paths = apr_array_make(scratch_pool, 0, sizeof(const char *)); |
| iterpool = svn_pool_create(scratch_pool); |
| for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); |
| hi != NULL; |
| hi = apr_hash_next(hi)) |
| { |
| const char *changed_path = apr_hash_this_key(hi); |
| svn_log_changed_path2_t *log_item = apr_hash_this_val(hi); |
| |
| svn_pool_clear(iterpool); |
| |
| /* ### Remove leading slash from paths in log entries. */ |
| if (changed_path[0] == '/') |
| changed_path++; |
| |
| /* For move detection, scan for copied nodes in this revision. */ |
| if (log_item->action == 'A' && log_item->copyfrom_path) |
| cache_copied_item(copies, changed_path, log_item); |
| |
| /* For move detection, store all deleted_paths. */ |
| if (log_item->action == 'D' || log_item->action == 'R') |
| APR_ARRAY_PUSH(deleted_paths, const char *) = |
| apr_pstrdup(scratch_pool, changed_path); |
| } |
| svn_pool_destroy(iterpool); |
| |
| /* Check for moves in this revision */ |
| SVN_ERR(find_moves_in_revision(b->extra_ra_session, |
| b->moves_table, b->moved_paths, |
| log_entry, copies, deleted_paths, |
| b->repos_root_url, b->repos_uuid, |
| b->ctx, b->result_pool, scratch_pool)); |
| |
| moves = apr_hash_get(b->moves_table, &log_entry->revision, |
| sizeof(svn_revnum_t)); |
| if (moves) |
| { |
| const svn_string_t *author; |
| |
| author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR); |
| SVN_ERR(find_nested_moves(moves, copies, deleted_paths, |
| b->moved_paths, log_entry->revision, |
| author ? author->data : _("unknown author"), |
| b->repos_root_url, |
| b->repos_uuid, |
| b->extra_ra_session, b->ctx, |
| b->result_pool, scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Find all moves which occurred in repository history starting at |
| * REPOS_RELPATH@START_REV until END_REV (where START_REV > END_REV). |
| * Return results in *MOVES_TABLE (see struct find_moves_baton for details). */ |
| static svn_error_t * |
| find_moves_in_revision_range(struct apr_hash_t **moves_table, |
| const char *repos_relpath, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| const char *victim_abspath, |
| svn_revnum_t start_rev, |
| svn_revnum_t end_rev, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_ra_session_t *ra_session; |
| const char *url; |
| const char *corrected_url; |
| apr_array_header_t *paths; |
| apr_array_header_t *revprops; |
| struct find_moves_baton b = { 0 }; |
| |
| SVN_ERR_ASSERT(start_rev > end_rev); |
| |
| url = svn_path_url_add_component2(repos_root_url, repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| url, NULL, NULL, FALSE, FALSE, |
| ctx, scratch_pool, |
| scratch_pool)); |
| |
| paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); |
| APR_ARRAY_PUSH(paths, const char *) = ""; |
| |
| revprops = apr_array_make(scratch_pool, 1, sizeof(const char *)); |
| APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; |
| |
| b.repos_root_url = repos_root_url; |
| b.repos_uuid = repos_uuid; |
| b.ctx = ctx; |
| b.victim_abspath = victim_abspath; |
| b.moves_table = apr_hash_make(result_pool); |
| b.moved_paths = apr_hash_make(scratch_pool); |
| b.result_pool = result_pool; |
| SVN_ERR(svn_ra__dup_session(&b.extra_ra_session, ra_session, NULL, |
| scratch_pool, scratch_pool)); |
| |
| SVN_ERR(svn_ra_get_log2(ra_session, paths, start_rev, end_rev, |
| 0, /* no limit */ |
| TRUE, /* need the changed paths list */ |
| FALSE, /* need to traverse copies */ |
| FALSE, /* no need for merged revisions */ |
| revprops, |
| find_moves, &b, |
| scratch_pool)); |
| |
| *moves_table = b.moves_table; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return new move information for a moved-along child MOVED_ALONG_RELPATH. |
| * Set MOVE->NODE_KIND to MOVED_ALONG_NODE_KIND. |
| * Do not copy MOVE->NEXT and MOVE-PREV. |
| * If MOVED_ALONG_RELPATH is empty, this effectively copies MOVE to |
| * RESULT_POOL with NEXT and PREV pointers cleared. */ |
| static struct repos_move_info * |
| new_path_adjusted_move(struct repos_move_info *move, |
| const char *moved_along_relpath, |
| svn_node_kind_t moved_along_node_kind, |
| apr_pool_t *result_pool) |
| { |
| struct repos_move_info *new_move; |
| |
| new_move = apr_pcalloc(result_pool, sizeof(*new_move)); |
| new_move->moved_from_repos_relpath = |
| svn_relpath_join(move->moved_from_repos_relpath, moved_along_relpath, |
| result_pool); |
| new_move->moved_to_repos_relpath = |
| svn_relpath_join(move->moved_to_repos_relpath, moved_along_relpath, |
| result_pool); |
| new_move->rev = move->rev; |
| new_move->rev_author = apr_pstrdup(result_pool, move->rev_author); |
| new_move->copyfrom_rev = move->copyfrom_rev; |
| new_move->node_kind = moved_along_node_kind; |
| /* Ignore prev and next pointers. Caller will set them if needed. */ |
| |
| return new_move; |
| } |
| |
| /* Given a list of MOVES_IN_REVISION, figure out which of these moves again |
| * move the node which was already moved by PREV_MOVE in the past . */ |
| static svn_error_t * |
| find_next_moves_in_revision(apr_array_header_t **next_moves, |
| apr_array_header_t *moves_in_revision, |
| struct repos_move_info *prev_move, |
| svn_ra_session_t *ra_session, |
| const char *repos_root_url, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| int i; |
| apr_pool_t *iterpool; |
| |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < moves_in_revision->nelts; i++) |
| { |
| struct repos_move_info *move; |
| const char *relpath; |
| const char *deleted_repos_relpath; |
| svn_boolean_t related; |
| svn_error_t *err; |
| |
| svn_pool_clear(iterpool); |
| |
| /* Check if this move affects the current known path of our node. */ |
| move = APR_ARRAY_IDX(moves_in_revision, i, struct repos_move_info *); |
| relpath = svn_relpath_skip_ancestor(move->moved_from_repos_relpath, |
| prev_move->moved_to_repos_relpath); |
| if (relpath == NULL) |
| continue; |
| |
| /* It does. So our node must have been deleted again. */ |
| deleted_repos_relpath = svn_relpath_join(move->moved_from_repos_relpath, |
| relpath, iterpool); |
| |
| /* Tracing back history of the delete-half of this move to the |
| * copyfrom-revision of the prior move we must end up at the |
| * delete-half of the prior move. */ |
| err = check_move_ancestry(&related, ra_session, repos_root_url, |
| deleted_repos_relpath, move->rev, |
| prev_move->moved_from_repos_relpath, |
| prev_move->copyfrom_rev, |
| FALSE, scratch_pool); |
| if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) |
| { |
| svn_error_clear(err); |
| continue; |
| } |
| else |
| SVN_ERR(err); |
| |
| if (related) |
| { |
| struct repos_move_info *new_move; |
| |
| /* We have a winner. */ |
| new_move = new_path_adjusted_move(move, relpath, prev_move->node_kind, |
| result_pool); |
| if (*next_moves == NULL) |
| *next_moves = apr_array_make(result_pool, 1, |
| sizeof(struct repos_move_info *)); |
| APR_ARRAY_PUSH(*next_moves, struct repos_move_info *) = new_move; |
| } |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static int |
| compare_items_as_revs(const svn_sort__item_t *a, const svn_sort__item_t *b) |
| { |
| return svn_sort_compare_revisions(a->key, b->key); |
| } |
| |
| /* Starting at MOVE->REV, loop over future revisions which contain moves, |
| * and look for matching next moves in each. Once found, return a list of |
| * (ambiguous, if more than one) moves in *NEXT_MOVES. */ |
| static svn_error_t * |
| find_next_moves(apr_array_header_t **next_moves, |
| apr_hash_t *moves_table, |
| struct repos_move_info *move, |
| svn_ra_session_t *ra_session, |
| const char *repos_root_url, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *moves; |
| apr_array_header_t *revisions; |
| apr_pool_t *iterpool; |
| int i; |
| |
| *next_moves = NULL; |
| revisions = svn_sort__hash(moves_table, compare_items_as_revs, scratch_pool); |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < revisions->nelts; i++) |
| { |
| svn_sort__item_t item = APR_ARRAY_IDX(revisions, i, svn_sort__item_t); |
| svn_revnum_t rev = *(svn_revnum_t *)item.key; |
| |
| svn_pool_clear(iterpool); |
| |
| if (rev <= move->rev) |
| continue; |
| |
| moves = apr_hash_get(moves_table, &rev, sizeof(rev)); |
| SVN_ERR(find_next_moves_in_revision(next_moves, moves, move, |
| ra_session, repos_root_url, |
| result_pool, iterpool)); |
| if (*next_moves) |
| break; |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Trace all future moves of the node moved by MOVE. |
| * Update MOVE->PREV and MOVE->NEXT accordingly. */ |
| static svn_error_t * |
| trace_moved_node(apr_hash_t *moves_table, |
| struct repos_move_info *move, |
| svn_ra_session_t *ra_session, |
| const char *repos_root_url, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *next_moves; |
| |
| SVN_ERR(find_next_moves(&next_moves, moves_table, move, |
| ra_session, repos_root_url, |
| result_pool, scratch_pool)); |
| if (next_moves) |
| { |
| int i; |
| apr_pool_t *iterpool; |
| |
| move->next = next_moves; |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < next_moves->nelts; i++) |
| { |
| struct repos_move_info *next_move; |
| |
| svn_pool_clear(iterpool); |
| next_move = APR_ARRAY_IDX(next_moves, i, struct repos_move_info *); |
| next_move->prev = move; |
| SVN_ERR(trace_moved_node(moves_table, next_move, |
| ra_session, repos_root_url, |
| result_pool, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Given a list of MOVES_IN_REVISION, figure out which of these moves |
| * move the node which was later on moved by NEXT_MOVE. */ |
| static svn_error_t * |
| find_prev_move_in_revision(struct repos_move_info **prev_move, |
| apr_array_header_t *moves_in_revision, |
| struct repos_move_info *next_move, |
| svn_ra_session_t *ra_session, |
| const char *repos_root_url, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| int i; |
| apr_pool_t *iterpool; |
| |
| *prev_move = NULL; |
| |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < moves_in_revision->nelts; i++) |
| { |
| struct repos_move_info *move; |
| const char *relpath; |
| const char *deleted_repos_relpath; |
| svn_boolean_t related; |
| svn_error_t *err; |
| |
| svn_pool_clear(iterpool); |
| |
| /* Check if this move affects the current known path of our node. */ |
| move = APR_ARRAY_IDX(moves_in_revision, i, struct repos_move_info *); |
| relpath = svn_relpath_skip_ancestor(next_move->moved_from_repos_relpath, |
| move->moved_to_repos_relpath); |
| if (relpath == NULL) |
| continue; |
| |
| /* It does. So our node must have been deleted. */ |
| deleted_repos_relpath = svn_relpath_join( |
| next_move->moved_from_repos_relpath, |
| relpath, iterpool); |
| |
| /* Tracing back history of the delete-half of the next move to the |
| * copyfrom-revision of the prior move we must end up at the |
| * delete-half of the prior move. */ |
| err = check_move_ancestry(&related, ra_session, repos_root_url, |
| deleted_repos_relpath, next_move->rev, |
| move->moved_from_repos_relpath, |
| move->copyfrom_rev, |
| FALSE, scratch_pool); |
| if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) |
| { |
| svn_error_clear(err); |
| continue; |
| } |
| else |
| SVN_ERR(err); |
| |
| if (related) |
| { |
| /* We have a winner. */ |
| *prev_move = new_path_adjusted_move(move, relpath, |
| next_move->node_kind, |
| result_pool); |
| break; |
| } |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static int |
| compare_items_as_revs_reverse(const svn_sort__item_t *a, |
| const svn_sort__item_t *b) |
| { |
| int c = svn_sort_compare_revisions(a->key, b->key); |
| if (c < 0) |
| return 1; |
| if (c > 0) |
| return -1; |
| return c; |
| } |
| |
| /* Starting at MOVE->REV, loop over past revisions which contain moves, |
| * and look for a matching previous move in each. Once found, return |
| * it in *PREV_MOVE */ |
| static svn_error_t * |
| find_prev_move(struct repos_move_info **prev_move, |
| apr_hash_t *moves_table, |
| struct repos_move_info *move, |
| svn_ra_session_t *ra_session, |
| const char *repos_root_url, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *moves; |
| apr_array_header_t *revisions; |
| apr_pool_t *iterpool; |
| int i; |
| |
| *prev_move = NULL; |
| revisions = svn_sort__hash(moves_table, compare_items_as_revs_reverse, |
| scratch_pool); |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < revisions->nelts; i++) |
| { |
| svn_sort__item_t item = APR_ARRAY_IDX(revisions, i, svn_sort__item_t); |
| svn_revnum_t rev = *(svn_revnum_t *)item.key; |
| |
| svn_pool_clear(iterpool); |
| |
| if (rev >= move->rev) |
| continue; |
| |
| moves = apr_hash_get(moves_table, &rev, sizeof(rev)); |
| SVN_ERR(find_prev_move_in_revision(prev_move, moves, move, |
| ra_session, repos_root_url, |
| result_pool, iterpool)); |
| if (*prev_move) |
| break; |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Trace all past moves of the node moved by MOVE. |
| * Update MOVE->PREV and MOVE->NEXT accordingly. */ |
| static svn_error_t * |
| trace_moved_node_backwards(apr_hash_t *moves_table, |
| struct repos_move_info *move, |
| svn_ra_session_t *ra_session, |
| const char *repos_root_url, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| struct repos_move_info *prev_move; |
| |
| SVN_ERR(find_prev_move(&prev_move, moves_table, move, |
| ra_session, repos_root_url, |
| result_pool, scratch_pool)); |
| if (prev_move) |
| { |
| move->prev = prev_move; |
| prev_move->next = apr_array_make(result_pool, 1, |
| sizeof(struct repos_move_info *)); |
| APR_ARRAY_PUSH(prev_move->next, struct repos_move_info *) = move; |
| |
| SVN_ERR(trace_moved_node_backwards(moves_table, prev_move, |
| ra_session, repos_root_url, |
| result_pool, scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Scan MOVES_TABLE for moves which affect a particular deleted node, and |
| * build a set of new move information for this node. |
| * Return heads of all possible move chains in *MOVES. |
| * |
| * MOVES_TABLE describes moves which happened at arbitrary paths in the |
| * repository. DELETED_REPOS_RELPATH may have been moved directly or it |
| * may have been moved along with a parent path. Move information returned |
| * from this function represents how DELETED_REPOS_RELPATH itself was moved |
| * from one path to another, effectively "zooming in" on the effective move |
| * operations which occurred for this particular node. */ |
| static svn_error_t * |
| find_operative_moves(apr_array_header_t **moves, |
| apr_hash_t *moves_table, |
| const char *deleted_repos_relpath, |
| svn_revnum_t deleted_rev, |
| svn_ra_session_t *ra_session, |
| const char *repos_root_url, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *moves_in_deleted_rev; |
| int i; |
| apr_pool_t *iterpool; |
| const char *session_url, *url = NULL; |
| |
| moves_in_deleted_rev = apr_hash_get(moves_table, &deleted_rev, |
| sizeof(deleted_rev)); |
| if (moves_in_deleted_rev == NULL) |
| { |
| *moves = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, scratch_pool)); |
| |
| /* Look for operative moves in the revision where the node was deleted. */ |
| *moves = apr_array_make(scratch_pool, 0, sizeof(struct repos_move_info *)); |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < moves_in_deleted_rev->nelts; i++) |
| { |
| struct repos_move_info *move; |
| const char *relpath; |
| |
| svn_pool_clear(iterpool); |
| |
| move = APR_ARRAY_IDX(moves_in_deleted_rev, i, struct repos_move_info *); |
| if (strcmp(move->moved_from_repos_relpath, deleted_repos_relpath) == 0) |
| { |
| APR_ARRAY_PUSH(*moves, struct repos_move_info *) = move; |
| continue; |
| } |
| |
| /* Test for an operative nested move. */ |
| relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, |
| deleted_repos_relpath); |
| if (relpath && relpath[0] != '\0') |
| { |
| struct repos_move_info *nested_move; |
| const char *actual_deleted_repos_relpath; |
| |
| actual_deleted_repos_relpath = |
| svn_relpath_join(move->moved_from_repos_relpath, relpath, |
| iterpool); |
| nested_move = map_deleted_path_to_move(actual_deleted_repos_relpath, |
| moves_in_deleted_rev, |
| iterpool); |
| if (nested_move) |
| APR_ARRAY_PUSH(*moves, struct repos_move_info *) = nested_move; |
| } |
| } |
| |
| if (url != NULL) |
| SVN_ERR(svn_ra_reparent(ra_session, session_url, scratch_pool)); |
| |
| /* If we didn't find any applicable moves, return NULL. */ |
| if ((*moves)->nelts == 0) |
| { |
| *moves = NULL; |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Figure out what happened to these moves in future revisions. */ |
| for (i = 0; i < (*moves)->nelts; i++) |
| { |
| struct repos_move_info *move; |
| |
| svn_pool_clear(iterpool); |
| |
| move = APR_ARRAY_IDX(*moves, i, struct repos_move_info *); |
| SVN_ERR(trace_moved_node(moves_table, move, ra_session, repos_root_url, |
| result_pool, iterpool)); |
| } |
| |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Try to find a revision older than START_REV, and its author, which deleted |
| * DELETED_BASENAME in the directory PARENT_REPOS_RELPATH. Assume the deleted |
| * node is ancestrally related to RELATED_REPOS_RELPATH@RELATED_PEG_REV. |
| * If no such revision can be found, set *DELETED_REV to SVN_INVALID_REVNUM |
| * and *DELETED_REV_AUTHOR to NULL. |
| * If the node was replaced rather than deleted, set *REPLACING_NODE_KIND to |
| * the node kind of the replacing node. Else, set it to svn_node_unknown. |
| * Only request the log for revisions up to END_REV from the server. |
| * If MOVES it not NULL, and the deleted node was moved, provide heads of |
| * move chains in *MOVES, or, if the node was not moved, set *MOVES to NULL. |
| */ |
| static svn_error_t * |
| find_revision_for_suspected_deletion(svn_revnum_t *deleted_rev, |
| const char **deleted_rev_author, |
| svn_node_kind_t *replacing_node_kind, |
| struct apr_array_header_t **moves, |
| svn_client_conflict_t *conflict, |
| const char *deleted_basename, |
| const char *parent_repos_relpath, |
| svn_revnum_t start_rev, |
| svn_revnum_t end_rev, |
| const char *related_repos_relpath, |
| svn_revnum_t related_peg_rev, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_ra_session_t *ra_session; |
| const char *url; |
| const char *corrected_url; |
| apr_array_header_t *paths; |
| apr_array_header_t *revprops; |
| const char *repos_root_url; |
| const char *repos_uuid; |
| struct find_deleted_rev_baton b = { 0 }; |
| const char *victim_abspath; |
| svn_error_t *err; |
| apr_hash_t *moves_table; |
| |
| SVN_ERR_ASSERT(start_rev > end_rev); |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, &repos_uuid, |
| conflict, scratch_pool, |
| scratch_pool)); |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| if (moves) |
| SVN_ERR(find_moves_in_revision_range(&moves_table, parent_repos_relpath, |
| repos_root_url, repos_uuid, |
| victim_abspath, start_rev, end_rev, |
| ctx, result_pool, scratch_pool)); |
| |
| url = svn_path_url_add_component2(repos_root_url, parent_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| url, NULL, NULL, FALSE, FALSE, |
| ctx, scratch_pool, |
| scratch_pool)); |
| |
| paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); |
| APR_ARRAY_PUSH(paths, const char *) = ""; |
| |
| revprops = apr_array_make(scratch_pool, 1, sizeof(const char *)); |
| APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; |
| |
| b.victim_abspath = victim_abspath; |
| b.deleted_repos_relpath = svn_relpath_join(parent_repos_relpath, |
| deleted_basename, scratch_pool); |
| b.related_repos_relpath = related_repos_relpath; |
| b.related_peg_rev = related_peg_rev; |
| b.deleted_rev = SVN_INVALID_REVNUM; |
| b.replacing_node_kind = svn_node_unknown; |
| b.repos_root_url = repos_root_url; |
| b.repos_uuid = repos_uuid; |
| b.ctx = ctx; |
| if (moves) |
| b.moves_table = moves_table; |
| b.result_pool = result_pool; |
| SVN_ERR(svn_ra__dup_session(&b.extra_ra_session, ra_session, NULL, |
| scratch_pool, scratch_pool)); |
| |
| err = svn_ra_get_log2(ra_session, paths, start_rev, end_rev, |
| 0, /* no limit */ |
| TRUE, /* need the changed paths list */ |
| FALSE, /* need to traverse copies */ |
| FALSE, /* no need for merged revisions */ |
| revprops, |
| find_deleted_rev, &b, |
| scratch_pool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_CEASE_INVOCATION && |
| b.deleted_rev != SVN_INVALID_REVNUM) |
| |
| { |
| /* Log operation was aborted because we found deleted rev. */ |
| svn_error_clear(err); |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| if (b.deleted_rev == SVN_INVALID_REVNUM) |
| { |
| struct repos_move_info *move = b.move; |
| |
| if (moves && move) |
| { |
| *deleted_rev = move->rev; |
| *deleted_rev_author = move->rev_author; |
| *replacing_node_kind = b.replacing_node_kind; |
| SVN_ERR(find_operative_moves(moves, moves_table, |
| b.deleted_repos_relpath, |
| move->rev, |
| ra_session, repos_root_url, |
| result_pool, scratch_pool)); |
| } |
| else |
| { |
| /* We could not determine the revision in which the node was |
| * deleted. */ |
| *deleted_rev = SVN_INVALID_REVNUM; |
| *deleted_rev_author = NULL; |
| *replacing_node_kind = svn_node_unknown; |
| if (moves) |
| *moves = NULL; |
| } |
| return SVN_NO_ERROR; |
| } |
| else |
| { |
| *deleted_rev = b.deleted_rev; |
| *deleted_rev_author = b.deleted_rev_author; |
| *replacing_node_kind = b.replacing_node_kind; |
| if (moves) |
| SVN_ERR(find_operative_moves(moves, moves_table, |
| b.deleted_repos_relpath, b.deleted_rev, |
| ra_session, repos_root_url, |
| result_pool, scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Details for tree conflicts involving a locally missing node. */ |
| struct conflict_tree_local_missing_details |
| { |
| /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. */ |
| svn_revnum_t deleted_rev; |
| |
| /* ### Add 'added_rev', like in conflict_tree_incoming_delete_details? */ |
| |
| /* Author who committed DELETED_REV. */ |
| const char *deleted_rev_author; |
| |
| /* The path which was deleted relative to the repository root. */ |
| const char *deleted_repos_relpath; |
| |
| /* Move information about the conflict victim. If not NULL, this is an |
| * array of 'struct repos_move_info *' elements. Each element is the |
| * head of a move chain which starts in DELETED_REV. */ |
| apr_array_header_t *moves; |
| |
| /* If moves is not NULL, a map of repos_relpaths and working copy nodes. |
| * |
| * Each key is a "const char *" repository relpath corresponding to a |
| * possible repository-side move destination node in the revision which |
| * is the merge-right revision in case of a merge. |
| * |
| * Each value is an apr_array_header_t *. |
| * Each array consists of "const char *" absolute paths to working copy |
| * nodes which correspond to the repository node selected by the map key. |
| * Each such working copy node is a potential local move target which can |
| * be chosen to find a suitable merge target when resolving a tree conflict. |
| * |
| * This may be an empty hash map in case if there is no move target path |
| * in the working copy. */ |
| apr_hash_t *wc_move_targets; |
| |
| /* If not NULL, the preferred move target repository relpath. This is our key |
| * into the WC_MOVE_TARGETS map above (can be overridden by the user). */ |
| const char *move_target_repos_relpath; |
| |
| /* The current index into the list of working copy nodes corresponding to |
| * MOVE_TARGET_REPOS_REPLATH (can be overridden by the user). */ |
| int wc_move_target_idx; |
| |
| /* Move information about siblings. Siblings are nodes which share |
| * a youngest common ancestor with the conflict victim. E.g. in case |
| * of a merge operation they are part of the merge source branch. |
| * If not NULL, this is an array of 'struct repos_move_info *' elements. |
| * Each element is the head of a move chain, which starts at some |
| * point in history after siblings and conflict victim forked off |
| * their common ancestor. */ |
| apr_array_header_t *sibling_moves; |
| |
| /* List of nodes in the WC which are suitable merge targets for changes |
| * merged from any moved sibling. Array elements are 'const char *' |
| * absolute paths of working copy nodes. This array contains multiple |
| * elements only if ambiguous matches were found in the WC. */ |
| apr_array_header_t *wc_siblings; |
| int preferred_sibling_idx; |
| }; |
| |
| static svn_error_t * |
| find_related_node(const char **related_repos_relpath, |
| svn_revnum_t *related_peg_rev, |
| const char *younger_related_repos_relpath, |
| svn_revnum_t younger_related_peg_rev, |
| const char *older_repos_relpath, |
| svn_revnum_t older_peg_rev, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *repos_root_url; |
| const char *related_url; |
| const char *corrected_url; |
| svn_node_kind_t related_node_kind; |
| svn_ra_session_t *ra_session; |
| |
| *related_repos_relpath = NULL; |
| *related_peg_rev = SVN_INVALID_REVNUM; |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, |
| scratch_pool, scratch_pool)); |
| related_url = svn_path_url_add_component2(repos_root_url, |
| younger_related_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, |
| &corrected_url, |
| related_url, NULL, |
| NULL, |
| FALSE, |
| FALSE, |
| ctx, |
| scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_ra_check_path(ra_session, "", younger_related_peg_rev, |
| &related_node_kind, scratch_pool)); |
| if (related_node_kind == svn_node_none) |
| { |
| svn_revnum_t related_deleted_rev; |
| const char *related_deleted_rev_author; |
| svn_node_kind_t related_replacing_node_kind; |
| const char *related_basename; |
| const char *related_parent_repos_relpath; |
| apr_array_header_t *related_moves; |
| |
| /* Looks like the younger node, which we'd like to use as our |
| * 'related node', was deleted. Try to find its deleted revision |
| * so we can calculate a peg revision at which it exists. |
| * The younger node is related to the older node, so we can use |
| * the older node to guide us in our search. */ |
| related_basename = svn_relpath_basename(younger_related_repos_relpath, |
| scratch_pool); |
| related_parent_repos_relpath = |
| svn_relpath_dirname(younger_related_repos_relpath, scratch_pool); |
| SVN_ERR(find_revision_for_suspected_deletion( |
| &related_deleted_rev, &related_deleted_rev_author, |
| &related_replacing_node_kind, &related_moves, |
| conflict, related_basename, |
| related_parent_repos_relpath, |
| younger_related_peg_rev, 0, |
| older_repos_relpath, older_peg_rev, |
| ctx, conflict->pool, scratch_pool)); |
| |
| /* If we can't find a related node, bail. */ |
| if (related_deleted_rev == SVN_INVALID_REVNUM) |
| return SVN_NO_ERROR; |
| |
| /* The node should exist in the revision before it was deleted. */ |
| *related_repos_relpath = younger_related_repos_relpath; |
| *related_peg_rev = rev_below(related_deleted_rev); |
| } |
| else |
| { |
| *related_repos_relpath = younger_related_repos_relpath; |
| *related_peg_rev = younger_related_peg_rev; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Determine if REPOS_RELPATH@PEG_REV was moved at some point in its history. |
| * History's range of interest ends at END_REV which must be older than PEG_REV. |
| * |
| * VICTIM_ABSPATH is the abspath of a conflict victim in the working copy and |
| * will be used in notifications. |
| * |
| * Return any applicable move chain heads in *MOVES. |
| * If no moves can be found, set *MOVES to NULL. */ |
| static svn_error_t * |
| find_moves_in_natural_history(apr_array_header_t **moves, |
| const char *repos_relpath, |
| svn_revnum_t peg_rev, |
| svn_node_kind_t node_kind, |
| svn_revnum_t end_rev, |
| const char *victim_abspath, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| svn_ra_session_t *ra_session, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_hash_t *moves_table; |
| apr_array_header_t *revs; |
| apr_array_header_t *most_recent_moves = NULL; |
| int i; |
| apr_pool_t *iterpool; |
| |
| *moves = NULL; |
| |
| SVN_ERR(find_moves_in_revision_range(&moves_table, repos_relpath, |
| repos_root_url, repos_uuid, |
| victim_abspath, peg_rev, end_rev, |
| ctx, scratch_pool, scratch_pool)); |
| |
| iterpool = svn_pool_create(scratch_pool); |
| |
| /* Scan the moves table for applicable moves. */ |
| revs = svn_sort__hash(moves_table, compare_items_as_revs, scratch_pool); |
| for (i = revs->nelts - 1; i >= 0; i--) |
| { |
| svn_sort__item_t item = APR_ARRAY_IDX(revs, i, svn_sort__item_t); |
| apr_array_header_t *moves_in_rev = apr_hash_get(moves_table, item.key, |
| sizeof(svn_revnum_t)); |
| int j; |
| |
| svn_pool_clear(iterpool); |
| |
| /* Was repos relpath moved to its location in this revision? */ |
| for (j = 0; j < moves_in_rev->nelts; j++) |
| { |
| struct repos_move_info *move; |
| const char *relpath; |
| |
| move = APR_ARRAY_IDX(moves_in_rev, j, struct repos_move_info *); |
| relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, |
| repos_relpath); |
| if (relpath) |
| { |
| /* If the move did not happen in our peg revision, make |
| * sure this move happened on the same line of history. */ |
| if (move->rev != peg_rev) |
| { |
| svn_client__pathrev_t *yca_loc; |
| svn_error_t *err; |
| |
| err = find_yca(&yca_loc, repos_relpath, peg_rev, |
| repos_relpath, move->rev, |
| repos_root_url, repos_uuid, |
| NULL, ctx, iterpool, iterpool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_FS_NOT_FOUND) |
| { |
| svn_error_clear(err); |
| yca_loc = NULL; |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| if (yca_loc == NULL || yca_loc->rev != move->rev) |
| continue; |
| } |
| |
| if (most_recent_moves == NULL) |
| most_recent_moves = |
| apr_array_make(result_pool, 1, |
| sizeof(struct repos_move_info *)); |
| |
| /* Copy the move to result pool (even if relpath is ""). */ |
| move = new_path_adjusted_move(move, relpath, node_kind, |
| result_pool); |
| APR_ARRAY_PUSH(most_recent_moves, |
| struct repos_move_info *) = move; |
| } |
| } |
| |
| /* If we found one move, or several ambiguous moves, we're done. */ |
| if (most_recent_moves) |
| break; |
| } |
| |
| if (most_recent_moves && most_recent_moves->nelts > 0) |
| { |
| *moves = apr_array_make(result_pool, 1, |
| sizeof(struct repos_move_info *)); |
| |
| /* Figure out what happened to the most recent moves in prior |
| * revisions and build move chains. */ |
| for (i = 0; i < most_recent_moves->nelts; i++) |
| { |
| struct repos_move_info *move; |
| |
| svn_pool_clear(iterpool); |
| |
| move = APR_ARRAY_IDX(most_recent_moves, i, struct repos_move_info *); |
| SVN_ERR(trace_moved_node_backwards(moves_table, move, |
| ra_session, repos_root_url, |
| result_pool, iterpool)); |
| /* Follow the move chain backwards. */ |
| while (move->prev) |
| move = move->prev; |
| |
| /* Return move heads. */ |
| APR_ARRAY_PUSH(*moves, struct repos_move_info *) = move; |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| collect_sibling_move_candidates(apr_array_header_t *candidates, |
| const char *victim_abspath, |
| svn_node_kind_t victim_kind, |
| struct repos_move_info *move, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *basename; |
| apr_array_header_t *abspaths; |
| int i; |
| |
| basename = svn_relpath_basename(move->moved_from_repos_relpath, scratch_pool); |
| SVN_ERR(svn_wc__find_working_nodes_with_basename(&abspaths, victim_abspath, |
| basename, victim_kind, |
| ctx->wc_ctx, result_pool, |
| scratch_pool)); |
| apr_array_cat(candidates, abspaths); |
| |
| if (move->next) |
| { |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < move->next->nelts; i++) |
| { |
| struct repos_move_info *next_move; |
| next_move = APR_ARRAY_IDX(move->next, i, struct repos_move_info *); |
| SVN_ERR(collect_sibling_move_candidates(candidates, victim_abspath, |
| victim_kind, next_move, ctx, |
| result_pool, iterpool)); |
| svn_pool_clear(iterpool); |
| } |
| svn_pool_destroy(iterpool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Follow each move chain starting a MOVE all the way to the end to find |
| * the possible working copy locations for VICTIM_ABSPATH which corresponds |
| * to VICTIM_REPOS_REPLATH@VICTIM_REVISION. |
| * Add each such location to the WC_MOVE_TARGETS hash table, keyed on the |
| * repos_relpath which is the corresponding move destination in the repository. |
| * This function is recursive. */ |
| static svn_error_t * |
| follow_move_chains(apr_hash_t *wc_move_targets, |
| struct repos_move_info *move, |
| svn_client_ctx_t *ctx, |
| const char *victim_abspath, |
| svn_node_kind_t victim_node_kind, |
| const char *victim_repos_relpath, |
| svn_revnum_t victim_revision, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *candidate_abspaths; |
| |
| /* Gather candidate nodes which represent this moved_to_repos_relpath. */ |
| SVN_ERR(svn_wc__guess_incoming_move_target_nodes( |
| &candidate_abspaths, ctx->wc_ctx, |
| victim_abspath, victim_node_kind, |
| move->moved_to_repos_relpath, |
| scratch_pool, scratch_pool)); |
| |
| if (candidate_abspaths->nelts > 0) |
| { |
| apr_array_header_t *moved_to_abspaths; |
| int i; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| |
| moved_to_abspaths = apr_array_make(result_pool, 1, |
| sizeof (const char *)); |
| |
| for (i = 0; i < candidate_abspaths->nelts; i++) |
| { |
| const char *candidate_abspath; |
| const char *repos_root_url; |
| const char *repos_uuid; |
| const char *candidate_repos_relpath; |
| svn_revnum_t candidate_revision; |
| |
| svn_pool_clear(iterpool); |
| |
| candidate_abspath = APR_ARRAY_IDX(candidate_abspaths, i, |
| const char *); |
| SVN_ERR(svn_wc__node_get_origin(NULL, &candidate_revision, |
| &candidate_repos_relpath, |
| &repos_root_url, |
| &repos_uuid, |
| NULL, NULL, |
| ctx->wc_ctx, |
| candidate_abspath, |
| FALSE, |
| iterpool, iterpool)); |
| |
| if (candidate_revision == SVN_INVALID_REVNUM) |
| continue; |
| |
| /* If the conflict victim and the move target candidate |
| * are not from the same revision we must ensure that |
| * they are related. */ |
| if (candidate_revision != victim_revision) |
| { |
| svn_client__pathrev_t *yca_loc; |
| svn_error_t *err; |
| |
| err = find_yca(&yca_loc, victim_repos_relpath, |
| victim_revision, |
| candidate_repos_relpath, |
| candidate_revision, |
| repos_root_url, repos_uuid, |
| NULL, ctx, iterpool, iterpool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_FS_NOT_FOUND) |
| { |
| svn_error_clear(err); |
| yca_loc = NULL; |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| if (yca_loc == NULL) |
| continue; |
| } |
| |
| APR_ARRAY_PUSH(moved_to_abspaths, const char *) = |
| apr_pstrdup(result_pool, candidate_abspath); |
| } |
| svn_pool_destroy(iterpool); |
| |
| svn_hash_sets(wc_move_targets, move->moved_to_repos_relpath, |
| moved_to_abspaths); |
| } |
| |
| if (move->next) |
| { |
| int i; |
| apr_pool_t *iterpool; |
| |
| /* Recurse into each of the possible move chains. */ |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < move->next->nelts; i++) |
| { |
| struct repos_move_info *next_move; |
| |
| svn_pool_clear(iterpool); |
| |
| next_move = APR_ARRAY_IDX(move->next, i, struct repos_move_info *); |
| SVN_ERR(follow_move_chains(wc_move_targets, next_move, |
| ctx, victim_abspath, victim_node_kind, |
| victim_repos_relpath, victim_revision, |
| result_pool, iterpool)); |
| |
| } |
| svn_pool_destroy(iterpool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements tree_conflict_get_details_func_t. */ |
| static svn_error_t * |
| conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *old_repos_relpath; |
| const char *new_repos_relpath; |
| const char *parent_repos_relpath; |
| svn_revnum_t parent_peg_rev; |
| svn_revnum_t old_rev; |
| svn_revnum_t new_rev; |
| svn_revnum_t deleted_rev; |
| svn_node_kind_t old_kind; |
| svn_node_kind_t new_kind; |
| const char *deleted_rev_author; |
| svn_node_kind_t replacing_node_kind; |
| const char *deleted_basename; |
| struct conflict_tree_local_missing_details *details; |
| apr_array_header_t *moves = NULL; |
| apr_array_header_t *sibling_moves = NULL; |
| apr_array_header_t *wc_siblings = NULL; |
| const char *related_repos_relpath; |
| svn_revnum_t related_peg_rev; |
| const char *repos_root_url; |
| const char *repos_uuid; |
| const char *url, *corrected_url; |
| svn_ra_session_t *ra_session; |
| svn_client__pathrev_t *yca_loc; |
| svn_revnum_t end_rev; |
| |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &old_repos_relpath, &old_rev, &old_kind, conflict, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &new_repos_relpath, &new_rev, &new_kind, conflict, |
| scratch_pool, scratch_pool)); |
| |
| /* Scan the conflict victim's parent's log to find a revision which |
| * deleted the node. */ |
| deleted_basename = svn_dirent_basename(conflict->local_abspath, |
| scratch_pool); |
| SVN_ERR(svn_wc__node_get_repos_info(&parent_peg_rev, &parent_repos_relpath, |
| &repos_root_url, &repos_uuid, |
| ctx->wc_ctx, |
| svn_dirent_dirname( |
| conflict->local_abspath, |
| scratch_pool), |
| scratch_pool, |
| scratch_pool)); |
| |
| /* If the parent is not part of the repository-side tree checked out |
| * into this working copy, then bail. We do not support this case yet. */ |
| if (parent_peg_rev == SVN_INVALID_REVNUM) |
| return SVN_NO_ERROR; |
| |
| /* Pick the younger incoming node as our 'related node' which helps |
| * pin-pointing the deleted conflict victim in history. */ |
| related_repos_relpath = |
| (old_rev < new_rev ? new_repos_relpath : old_repos_relpath); |
| related_peg_rev = (old_rev < new_rev ? new_rev : old_rev); |
| |
| /* Make sure we're going to search the related node in a revision where |
| * it exists. The younger incoming node might have been deleted in HEAD. */ |
| if (related_repos_relpath != NULL && related_peg_rev != SVN_INVALID_REVNUM) |
| SVN_ERR(find_related_node( |
| &related_repos_relpath, &related_peg_rev, |
| related_repos_relpath, related_peg_rev, |
| (old_rev < new_rev ? old_repos_relpath : new_repos_relpath), |
| (old_rev < new_rev ? old_rev : new_rev), |
| conflict, ctx, scratch_pool, scratch_pool)); |
| |
| /* Set END_REV to our best guess of the nearest YCA revision. */ |
| url = svn_path_url_add_component2(repos_root_url, related_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, |
| &corrected_url, |
| url, NULL, NULL, |
| FALSE, |
| FALSE, |
| ctx, |
| scratch_pool, |
| scratch_pool)); |
| SVN_ERR(find_nearest_yca(&yca_loc, related_repos_relpath, related_peg_rev, |
| parent_repos_relpath, parent_peg_rev, |
| repos_root_url, repos_uuid, ra_session, ctx, |
| scratch_pool, scratch_pool)); |
| if (yca_loc) |
| { |
| end_rev = yca_loc->rev; |
| |
| /* END_REV must be smaller than PARENT_PEG_REV, else the call to |
| * find_revision_for_suspected_deletion() below will abort. */ |
| if (end_rev >= parent_peg_rev) |
| end_rev = parent_peg_rev > 0 ? parent_peg_rev - 1 : 0; |
| } |
| else |
| end_rev = 0; /* ### We might walk through all of history... */ |
| |
| SVN_ERR(find_revision_for_suspected_deletion( |
| &deleted_rev, &deleted_rev_author, &replacing_node_kind, |
| yca_loc ? &moves : NULL, |
| conflict, deleted_basename, parent_repos_relpath, |
| parent_peg_rev, end_rev, related_repos_relpath, related_peg_rev, |
| ctx, conflict->pool, scratch_pool)); |
| |
| /* If the victim was not deleted then check if the related path was moved. */ |
| if (deleted_rev == SVN_INVALID_REVNUM) |
| { |
| const char *victim_abspath; |
| svn_node_kind_t related_node_kind; |
| apr_array_header_t *candidates; |
| int i; |
| apr_pool_t *iterpool; |
| |
| /* ### The following describes all moves in terms of forward-merges, |
| * should do we something else for reverse-merges? */ |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| if (yca_loc) |
| { |
| end_rev = yca_loc->rev; |
| |
| /* END_REV must be smaller than RELATED_PEG_REV, else the call |
| to find_moves_in_natural_history() below will error out. */ |
| if (end_rev >= related_peg_rev) |
| end_rev = related_peg_rev > 0 ? related_peg_rev - 1 : 0; |
| } |
| else |
| end_rev = 0; /* ### We might walk through all of history... */ |
| |
| SVN_ERR(svn_ra_check_path(ra_session, "", related_peg_rev, |
| &related_node_kind, scratch_pool)); |
| SVN_ERR(find_moves_in_natural_history(&sibling_moves, |
| related_repos_relpath, |
| related_peg_rev, |
| related_node_kind, |
| end_rev, |
| victim_abspath, |
| repos_root_url, repos_uuid, |
| ra_session, ctx, |
| conflict->pool, scratch_pool)); |
| |
| if (sibling_moves == NULL) |
| return SVN_NO_ERROR; |
| |
| /* Find the missing node in the WC. In theory, this requires tracing |
| * back history of every node in the WC to check for a YCA with the |
| * conflict victim. This operation would obviously be quite expensive. |
| * |
| * However, assuming that the victim was not moved in the merge target, |
| * we can take a short-cut: The basename of the node cannot have changed, |
| * so we can limit history tracing to nodes with a matching basename. |
| * |
| * This approach solves the conflict case where an edit to a file which |
| * was moved on one branch is cherry-picked to another branch where the |
| * corresponding file has not been moved (yet). It does not solve move |
| * vs. move conflicts, but such conflicts are not yet supported by the |
| * resolver anyway and are hard to solve without server-side support. */ |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < sibling_moves->nelts; i++) |
| { |
| struct repos_move_info *move; |
| int j; |
| |
| svn_pool_clear(iterpool); |
| |
| move = APR_ARRAY_IDX(sibling_moves, i, struct repos_move_info *); |
| candidates = apr_array_make(iterpool, 1, sizeof(const char *)); |
| SVN_ERR(collect_sibling_move_candidates(candidates, victim_abspath, |
| old_rev < new_rev |
| ? new_kind : old_kind, |
| move, ctx, iterpool, |
| iterpool)); |
| |
| /* Determine whether a candidate node shares a YCA with the victim. */ |
| for (j = 0; j < candidates->nelts; j++) |
| { |
| const char *candidate_abspath; |
| const char *candidate_repos_relpath; |
| svn_revnum_t candidate_revision; |
| svn_error_t *err; |
| |
| candidate_abspath = APR_ARRAY_IDX(candidates, j, const char *); |
| SVN_ERR(svn_wc__node_get_origin(NULL, &candidate_revision, |
| &candidate_repos_relpath, |
| NULL, NULL, NULL, NULL, |
| ctx->wc_ctx, |
| candidate_abspath, |
| FALSE, |
| iterpool, iterpool)); |
| err = find_yca(&yca_loc, |
| old_rev < new_rev |
| ? new_repos_relpath : old_repos_relpath, |
| old_rev < new_rev ? new_rev : old_rev, |
| candidate_repos_relpath, |
| candidate_revision, |
| repos_root_url, repos_uuid, |
| NULL, ctx, iterpool, iterpool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_FS_NOT_FOUND) |
| { |
| svn_error_clear(err); |
| yca_loc = NULL; |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| if (yca_loc) |
| { |
| if (wc_siblings == NULL) |
| wc_siblings = apr_array_make(conflict->pool, 1, |
| sizeof(const char *)); |
| APR_ARRAY_PUSH(wc_siblings, const char *) = |
| apr_pstrdup(conflict->pool, candidate_abspath); |
| } |
| } |
| } |
| svn_pool_destroy(iterpool); |
| } |
| |
| details = apr_pcalloc(conflict->pool, sizeof(*details)); |
| details->deleted_rev = deleted_rev; |
| details->deleted_rev_author = deleted_rev_author; |
| if (deleted_rev != SVN_INVALID_REVNUM) |
| details->deleted_repos_relpath = svn_relpath_join(parent_repos_relpath, |
| deleted_basename, |
| conflict->pool); |
| details->moves = moves; |
| if (details->moves != NULL) |
| { |
| apr_pool_t *iterpool; |
| int i; |
| |
| details->wc_move_targets = apr_hash_make(conflict->pool); |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < details->moves->nelts; i++) |
| { |
| struct repos_move_info *move; |
| |
| svn_pool_clear(iterpool); |
| move = APR_ARRAY_IDX(details->moves, i, struct repos_move_info *); |
| SVN_ERR(follow_move_chains(details->wc_move_targets, move, ctx, |
| conflict->local_abspath, |
| new_kind, |
| new_repos_relpath, |
| new_rev, |
| scratch_pool, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| if (apr_hash_count(details->wc_move_targets) > 0) |
| { |
| apr_array_header_t *move_target_repos_relpaths; |
| const svn_sort__item_t *item; |
| |
| /* Initialize to the first possible move target. Hopefully, |
| * in most cases there will only be one candidate anyway. */ |
| move_target_repos_relpaths = svn_sort__hash( |
| details->wc_move_targets, |
| svn_sort_compare_items_as_paths, |
| scratch_pool); |
| item = &APR_ARRAY_IDX(move_target_repos_relpaths, |
| 0, svn_sort__item_t); |
| details->move_target_repos_relpath = item->key; |
| details->wc_move_target_idx = 0; |
| } |
| else |
| { |
| details->move_target_repos_relpath = NULL; |
| details->wc_move_target_idx = 0; |
| } |
| } |
| |
| details->sibling_moves = sibling_moves; |
| details->wc_siblings = wc_siblings; |
| if (details->wc_move_targets && apr_hash_count(details->wc_move_targets) == 1) |
| { |
| apr_array_header_t *wc_abspaths; |
| |
| wc_abspaths = svn_hash_gets(details->wc_move_targets, |
| details->move_target_repos_relpath); |
| if (wc_abspaths->nelts == 1) |
| { |
| svn_node_kind_t kind = old_rev < new_rev ? new_kind : old_kind; |
| |
| if (kind == svn_node_file) |
| conflict->recommended_option_id = |
| svn_client_conflict_option_local_move_file_text_merge; |
| else if (kind == svn_node_dir) |
| conflict->recommended_option_id = |
| svn_client_conflict_option_local_move_dir_merge; |
| } |
| } |
| else if (details->wc_siblings && details->wc_siblings->nelts == 1) |
| { |
| svn_node_kind_t kind = old_rev < new_rev ? new_kind : old_kind; |
| |
| if (kind == svn_node_file) |
| conflict->recommended_option_id = |
| svn_client_conflict_option_sibling_move_file_text_merge; |
| else if (kind == svn_node_dir) |
| conflict->recommended_option_id = |
| svn_client_conflict_option_sibling_move_dir_merge; |
| } |
| |
| conflict->tree_conflict_local_details = details; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return a localised string representation of the local part of a tree |
| conflict on a non-existent node. */ |
| static svn_error_t * |
| describe_local_none_node_change(const char **description, |
| svn_client_conflict_t *conflict, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_conflict_reason_t local_change; |
| svn_wc_operation_t operation; |
| |
| local_change = svn_client_conflict_get_local_change(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| |
| switch (local_change) |
| { |
| case svn_wc_conflict_reason_edited: |
| *description = _("An item containing uncommitted changes was " |
| "found in the working copy."); |
| break; |
| case svn_wc_conflict_reason_obstructed: |
| *description = _("An item which already occupies this path was found in " |
| "the working copy."); |
| break; |
| case svn_wc_conflict_reason_deleted: |
| *description = _("A deleted item was found in the working copy."); |
| break; |
| case svn_wc_conflict_reason_missing: |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| *description = _("No such file or directory was found in the " |
| "working copy."); |
| else if (operation == svn_wc_operation_merge) |
| { |
| /* ### display deleted revision */ |
| *description = _("No such file or directory was found in the " |
| "merge target working copy.\nThe item may " |
| "have been deleted or moved away in the " |
| "repository's history."); |
| } |
| break; |
| case svn_wc_conflict_reason_unversioned: |
| *description = _("An unversioned item was found in the working " |
| "copy."); |
| break; |
| case svn_wc_conflict_reason_added: |
| case svn_wc_conflict_reason_replaced: |
| *description = _("An item scheduled to be added to the repository " |
| "in the next commit was found in the working " |
| "copy."); |
| break; |
| case svn_wc_conflict_reason_moved_away: |
| *description = _("The item in the working copy had been moved " |
| "away at the time this conflict was recorded."); |
| break; |
| case svn_wc_conflict_reason_moved_here: |
| *description = _("An item had been moved here in the working copy " |
| "at the time this conflict was recorded."); |
| break; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Append a description of a move chain beginning at NEXT to DESCRIPTION. */ |
| static const char * |
| append_moved_to_chain_description(const char *description, |
| apr_array_header_t *next, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| if (next == NULL) |
| return description; |
| |
| while (next) |
| { |
| struct repos_move_info *move; |
| |
| /* Describe the first possible move chain only. Adding multiple chains |
| * to the description would just be confusing. The user may select a |
| * different move destination while resolving the conflict. */ |
| move = APR_ARRAY_IDX(next, 0, struct repos_move_info *); |
| |
| description = apr_psprintf(scratch_pool, |
| _("%s\nAnd then moved away to '^/%s' by " |
| "%s in r%ld."), |
| description, move->moved_to_repos_relpath, |
| move->rev_author, move->rev); |
| next = move->next; |
| } |
| |
| return apr_pstrdup(result_pool, description); |
| } |
| |
| /* Implements tree_conflict_get_description_func_t. */ |
| static svn_error_t * |
| conflict_tree_get_local_description_generic(const char **description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_node_kind_t victim_node_kind; |
| |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| |
| *description = NULL; |
| |
| switch (victim_node_kind) |
| { |
| case svn_node_file: |
| case svn_node_symlink: |
| SVN_ERR(describe_local_file_node_change(description, conflict, ctx, |
| result_pool, scratch_pool)); |
| break; |
| case svn_node_dir: |
| SVN_ERR(describe_local_dir_node_change(description, conflict, ctx, |
| result_pool, scratch_pool)); |
| break; |
| case svn_node_none: |
| case svn_node_unknown: |
| SVN_ERR(describe_local_none_node_change(description, conflict, |
| result_pool, scratch_pool)); |
| break; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements tree_conflict_get_description_func_t. */ |
| static svn_error_t * |
| conflict_tree_get_description_local_missing(const char **description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| struct conflict_tree_local_missing_details *details; |
| |
| details = conflict->tree_conflict_local_details; |
| if (details == NULL) |
| return svn_error_trace(conflict_tree_get_local_description_generic( |
| description, conflict, ctx, |
| result_pool, scratch_pool)); |
| |
| if (details->moves || details->sibling_moves) |
| { |
| struct repos_move_info *move; |
| |
| *description = _("No such file or directory was found in the " |
| "merge target working copy.\n"); |
| |
| if (details->moves) |
| { |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| if (move->node_kind == svn_node_file) |
| *description = apr_psprintf( |
| result_pool, |
| _("%sThe file was moved to '^/%s' in r%ld by %s."), |
| *description, move->moved_to_repos_relpath, |
| move->rev, move->rev_author); |
| else if (move->node_kind == svn_node_dir) |
| *description = apr_psprintf( |
| result_pool, |
| _("%sThe directory was moved to '^/%s' in " |
| "r%ld by %s."), |
| *description, move->moved_to_repos_relpath, |
| move->rev, move->rev_author); |
| else |
| *description = apr_psprintf( |
| result_pool, |
| _("%sThe item was moved to '^/%s' in r%ld by %s."), |
| *description, move->moved_to_repos_relpath, |
| move->rev, move->rev_author); |
| *description = append_moved_to_chain_description(*description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| |
| if (details->sibling_moves) |
| { |
| move = APR_ARRAY_IDX(details->sibling_moves, 0, |
| struct repos_move_info *); |
| if (move->node_kind == svn_node_file) |
| *description = apr_psprintf( |
| result_pool, |
| _("%sThe file '^/%s' was moved to '^/%s' " |
| "in r%ld by %s."), |
| *description, move->moved_from_repos_relpath, |
| move->moved_to_repos_relpath, |
| move->rev, move->rev_author); |
| else if (move->node_kind == svn_node_dir) |
| *description = apr_psprintf( |
| result_pool, |
| _("%sThe directory '^/%s' was moved to '^/%s' " |
| "in r%ld by %s."), |
| *description, move->moved_from_repos_relpath, |
| move->moved_to_repos_relpath, |
| move->rev, move->rev_author); |
| else |
| *description = apr_psprintf( |
| result_pool, |
| _("%sThe item '^/%s' was moved to '^/%s' " |
| "in r%ld by %s."), |
| *description, move->moved_from_repos_relpath, |
| move->moved_to_repos_relpath, |
| move->rev, move->rev_author); |
| *description = append_moved_to_chain_description(*description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| } |
| else |
| *description = apr_psprintf( |
| result_pool, |
| _("No such file or directory was found in the " |
| "merge target working copy.\n'^/%s' was deleted " |
| "in r%ld by %s."), |
| details->deleted_repos_relpath, |
| details->deleted_rev, details->deleted_rev_author); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return a localised string representation of the incoming part of a |
| conflict; NULL for non-localised odd cases. */ |
| static const char * |
| describe_incoming_change(svn_node_kind_t kind, svn_wc_conflict_action_t action, |
| svn_wc_operation_t operation) |
| { |
| switch (kind) |
| { |
| case svn_node_file: |
| case svn_node_symlink: |
| if (operation == svn_wc_operation_update) |
| { |
| switch (action) |
| { |
| case svn_wc_conflict_action_edit: |
| return _("An update operation tried to edit a file."); |
| case svn_wc_conflict_action_add: |
| return _("An update operation tried to add a file."); |
| case svn_wc_conflict_action_delete: |
| return _("An update operation tried to delete or move " |
| "a file."); |
| case svn_wc_conflict_action_replace: |
| return _("An update operation tried to replace a file."); |
| } |
| } |
| else if (operation == svn_wc_operation_switch) |
| { |
| switch (action) |
| { |
| case svn_wc_conflict_action_edit: |
| return _("A switch operation tried to edit a file."); |
| case svn_wc_conflict_action_add: |
| return _("A switch operation tried to add a file."); |
| case svn_wc_conflict_action_delete: |
| return _("A switch operation tried to delete or move " |
| "a file."); |
| case svn_wc_conflict_action_replace: |
| return _("A switch operation tried to replace a file."); |
| } |
| } |
| else if (operation == svn_wc_operation_merge) |
| { |
| switch (action) |
| { |
| case svn_wc_conflict_action_edit: |
| return _("A merge operation tried to edit a file."); |
| case svn_wc_conflict_action_add: |
| return _("A merge operation tried to add a file."); |
| case svn_wc_conflict_action_delete: |
| return _("A merge operation tried to delete or move " |
| "a file."); |
| case svn_wc_conflict_action_replace: |
| return _("A merge operation tried to replace a file."); |
| } |
| } |
| break; |
| case svn_node_dir: |
| if (operation == svn_wc_operation_update) |
| { |
| switch (action) |
| { |
| case svn_wc_conflict_action_edit: |
| return _("An update operation tried to change a directory."); |
| case svn_wc_conflict_action_add: |
| return _("An update operation tried to add a directory."); |
| case svn_wc_conflict_action_delete: |
| return _("An update operation tried to delete or move " |
| "a directory."); |
| case svn_wc_conflict_action_replace: |
| return _("An update operation tried to replace a directory."); |
| } |
| } |
| else if (operation == svn_wc_operation_switch) |
| { |
| switch (action) |
| { |
| case svn_wc_conflict_action_edit: |
| return _("A switch operation tried to edit a directory."); |
| case svn_wc_conflict_action_add: |
| return _("A switch operation tried to add a directory."); |
| case svn_wc_conflict_action_delete: |
| return _("A switch operation tried to delete or move " |
| "a directory."); |
| case svn_wc_conflict_action_replace: |
| return _("A switch operation tried to replace a directory."); |
| } |
| } |
| else if (operation == svn_wc_operation_merge) |
| { |
| switch (action) |
| { |
| case svn_wc_conflict_action_edit: |
| return _("A merge operation tried to edit a directory."); |
| case svn_wc_conflict_action_add: |
| return _("A merge operation tried to add a directory."); |
| case svn_wc_conflict_action_delete: |
| return _("A merge operation tried to delete or move " |
| "a directory."); |
| case svn_wc_conflict_action_replace: |
| return _("A merge operation tried to replace a directory."); |
| } |
| } |
| break; |
| case svn_node_none: |
| case svn_node_unknown: |
| if (operation == svn_wc_operation_update) |
| { |
| switch (action) |
| { |
| case svn_wc_conflict_action_edit: |
| return _("An update operation tried to edit an item."); |
| case svn_wc_conflict_action_add: |
| return _("An update operation tried to add an item."); |
| case svn_wc_conflict_action_delete: |
| return _("An update operation tried to delete or move " |
| "an item."); |
| case svn_wc_conflict_action_replace: |
| return _("An update operation tried to replace an item."); |
| } |
| } |
| else if (operation == svn_wc_operation_switch) |
| { |
| switch (action) |
| { |
| case svn_wc_conflict_action_edit: |
| return _("A switch operation tried to edit an item."); |
| case svn_wc_conflict_action_add: |
| return _("A switch operation tried to add an item."); |
| case svn_wc_conflict_action_delete: |
| return _("A switch operation tried to delete or move " |
| "an item."); |
| case svn_wc_conflict_action_replace: |
| return _("A switch operation tried to replace an item."); |
| } |
| } |
| else if (operation == svn_wc_operation_merge) |
| { |
| switch (action) |
| { |
| case svn_wc_conflict_action_edit: |
| return _("A merge operation tried to edit an item."); |
| case svn_wc_conflict_action_add: |
| return _("A merge operation tried to add an item."); |
| case svn_wc_conflict_action_delete: |
| return _("A merge operation tried to delete or move " |
| "an item."); |
| case svn_wc_conflict_action_replace: |
| return _("A merge operation tried to replace an item."); |
| } |
| } |
| break; |
| } |
| |
| return NULL; |
| } |
| |
| /* Return a localised string representation of the operation part of a |
| conflict. */ |
| static const char * |
| operation_str(svn_wc_operation_t operation) |
| { |
| switch (operation) |
| { |
| case svn_wc_operation_update: return _("upon update"); |
| case svn_wc_operation_switch: return _("upon switch"); |
| case svn_wc_operation_merge: return _("upon merge"); |
| case svn_wc_operation_none: return _("upon none"); |
| } |
| SVN_ERR_MALFUNCTION_NO_RETURN(); |
| return NULL; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_prop_get_description(const char **description, |
| svn_client_conflict_t *conflict, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *reason_str, *action_str; |
| |
| /* We provide separately translatable strings for the values that we |
| * know about, and a fall-back in case any other values occur. */ |
| switch (svn_client_conflict_get_local_change(conflict)) |
| { |
| case svn_wc_conflict_reason_edited: |
| reason_str = _("local edit"); |
| break; |
| case svn_wc_conflict_reason_added: |
| reason_str = _("local add"); |
| break; |
| case svn_wc_conflict_reason_deleted: |
| reason_str = _("local delete"); |
| break; |
| case svn_wc_conflict_reason_obstructed: |
| reason_str = _("local obstruction"); |
| break; |
| default: |
| reason_str = apr_psprintf( |
| scratch_pool, _("local %s"), |
| svn_token__to_word( |
| map_conflict_reason, |
| svn_client_conflict_get_local_change(conflict))); |
| break; |
| } |
| switch (svn_client_conflict_get_incoming_change(conflict)) |
| { |
| case svn_wc_conflict_action_edit: |
| action_str = _("incoming edit"); |
| break; |
| case svn_wc_conflict_action_add: |
| action_str = _("incoming add"); |
| break; |
| case svn_wc_conflict_action_delete: |
| action_str = _("incoming delete"); |
| break; |
| default: |
| action_str = apr_psprintf( |
| scratch_pool, _("incoming %s"), |
| svn_token__to_word( |
| map_conflict_action, |
| svn_client_conflict_get_incoming_change(conflict))); |
| break; |
| } |
| SVN_ERR_ASSERT(reason_str && action_str); |
| |
| *description = apr_psprintf(result_pool, _("%s, %s %s"), |
| reason_str, action_str, |
| operation_str( |
| svn_client_conflict_get_operation(conflict))); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements tree_conflict_get_description_func_t. */ |
| static svn_error_t * |
| conflict_tree_get_incoming_description_generic( |
| const char **incoming_change_description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *action; |
| svn_node_kind_t incoming_kind; |
| svn_wc_conflict_action_t conflict_action; |
| svn_wc_operation_t conflict_operation; |
| |
| conflict_action = svn_client_conflict_get_incoming_change(conflict); |
| conflict_operation = svn_client_conflict_get_operation(conflict); |
| |
| /* Determine the node kind of the incoming change. */ |
| incoming_kind = svn_node_unknown; |
| if (conflict_action == svn_wc_conflict_action_edit || |
| conflict_action == svn_wc_conflict_action_delete) |
| { |
| /* Change is acting on 'src_left' version of the node. */ |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| NULL, NULL, &incoming_kind, conflict, scratch_pool, |
| scratch_pool)); |
| } |
| else if (conflict_action == svn_wc_conflict_action_add || |
| conflict_action == svn_wc_conflict_action_replace) |
| { |
| /* Change is acting on 'src_right' version of the node. |
| * |
| * ### For 'replace', the node kind is ambiguous. However, src_left |
| * ### is NULL for replace, so we must use src_right. */ |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| NULL, NULL, &incoming_kind, conflict, scratch_pool, |
| scratch_pool)); |
| } |
| |
| action = describe_incoming_change(incoming_kind, conflict_action, |
| conflict_operation); |
| if (action) |
| { |
| *incoming_change_description = apr_pstrdup(result_pool, action); |
| } |
| else |
| { |
| /* A catch-all message for very rare or nominally impossible cases. |
| It will not be pretty, but is closer to an internal error than |
| an ordinary user-facing string. */ |
| *incoming_change_description = apr_psprintf(result_pool, |
| _("incoming %s %s"), |
| svn_node_kind_to_word(incoming_kind), |
| svn_token__to_word(map_conflict_action, |
| conflict_action)); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| /* Details for tree conflicts involving incoming deletions and replacements. */ |
| struct conflict_tree_incoming_delete_details |
| { |
| /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. */ |
| svn_revnum_t deleted_rev; |
| |
| /* If not SVN_INVALID_REVNUM, the node was added in ADDED_REV. The incoming |
| * delete is the result of a reverse application of this addition. */ |
| svn_revnum_t added_rev; |
| |
| /* The path which was deleted/added relative to the repository root. */ |
| const char *repos_relpath; |
| |
| /* Author who committed DELETED_REV/ADDED_REV. */ |
| const char *rev_author; |
| |
| /* New node kind for a replaced node. This is svn_node_none for deletions. */ |
| svn_node_kind_t replacing_node_kind; |
| |
| /* Move information. If not NULL, this is an array of repos_move_info * |
| * elements. Each element is the head of a move chain which starts in |
| * DELETED_REV or in ADDED_REV (in which case moves should be interpreted |
| * in reverse). */ |
| apr_array_header_t *moves; |
| |
| /* A map of repos_relpaths and working copy nodes for an incoming move. |
| * |
| * Each key is a "const char *" repository relpath corresponding to a |
| * possible repository-side move destination node in the revision which |
| * is the target revision in case of update and switch, or the merge-right |
| * revision in case of a merge. |
| * |
| * Each value is an apr_array_header_t *. |
| * Each array consists of "const char *" absolute paths to working copy |
| * nodes which correspond to the repository node selected by the map key. |
| * Each such working copy node is a potential local move target which can |
| * be chosen to "follow" the incoming move when resolving a tree conflict. |
| * |
| * This may be an empty hash map in case if there is no move target path |
| * in the working copy. */ |
| apr_hash_t *wc_move_targets; |
| |
| /* The preferred move target repository relpath. This is our key into |
| * the WC_MOVE_TARGETS map above (can be overridden by the user). */ |
| const char *move_target_repos_relpath; |
| |
| /* The current index into the list of working copy nodes corresponding to |
| * MOVE_TARGET_REPOS_REPLATH (can be overridden by the user). */ |
| int wc_move_target_idx; |
| }; |
| |
| /* Get the currently selected repository-side move target path. |
| * If none was selected yet, determine and return a default one. */ |
| static const char * |
| get_moved_to_repos_relpath( |
| struct conflict_tree_incoming_delete_details *details, |
| apr_pool_t *scratch_pool) |
| { |
| struct repos_move_info *move; |
| |
| if (details->move_target_repos_relpath) |
| return details->move_target_repos_relpath; |
| |
| if (details->wc_move_targets && apr_hash_count(details->wc_move_targets) > 0) |
| { |
| svn_sort__item_t item; |
| apr_array_header_t *repos_relpaths; |
| |
| repos_relpaths = svn_sort__hash(details->wc_move_targets, |
| svn_sort_compare_items_as_paths, |
| scratch_pool); |
| item = APR_ARRAY_IDX(repos_relpaths, 0, svn_sort__item_t); |
| return (const char *)item.key; |
| } |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| return move->moved_to_repos_relpath; |
| } |
| |
| static const char * |
| describe_incoming_deletion_upon_update( |
| struct conflict_tree_incoming_delete_details *details, |
| svn_node_kind_t victim_node_kind, |
| svn_revnum_t old_rev, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| if (details->replacing_node_kind == svn_node_file || |
| details->replacing_node_kind == svn_node_symlink) |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Directory updated from r%ld to r%ld was " |
| "replaced with a file by %s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced directory was moved to " |
| "'^/%s'."), description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("File updated from r%ld to r%ld was replaced " |
| "with a file from another line of history by " |
| "%s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced file was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Item updated from r%ld to r%ld was replaced " |
| "with a file by %s in r%ld."), old_rev, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced item was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| } |
| else if (details->replacing_node_kind == svn_node_dir) |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Directory updated from r%ld to r%ld was " |
| "replaced with a directory from another line " |
| "of history by %s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced directory was moved to " |
| "'^/%s'."), description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("File updated from r%ld to r%ld was " |
| "replaced with a directory by %s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced file was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Item updated from r%ld to r%ld was replaced " |
| "by %s in r%ld."), old_rev, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced item was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| } |
| else |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| if (details->moves) |
| { |
| const char *description; |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("Directory updated from r%ld to r%ld was " |
| "moved to '^/%s' by %s in r%ld."), |
| old_rev, new_rev, |
| get_moved_to_repos_relpath(details, scratch_pool), |
| details->rev_author, details->deleted_rev); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("Directory updated from r%ld to r%ld was " |
| "deleted by %s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| const char *description; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("File updated from r%ld to r%ld was moved " |
| "to '^/%s' by %s in r%ld."), old_rev, new_rev, |
| get_moved_to_repos_relpath(details, scratch_pool), |
| details->rev_author, details->deleted_rev); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("File updated from r%ld to r%ld was " |
| "deleted by %s in r%ld."), old_rev, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| else |
| { |
| if (details->moves) |
| { |
| const char *description; |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("Item updated from r%ld to r%ld was moved " |
| "to '^/%s' by %s in r%ld."), old_rev, new_rev, |
| get_moved_to_repos_relpath(details, scratch_pool), |
| details->rev_author, details->deleted_rev); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("Item updated from r%ld to r%ld was " |
| "deleted by %s in r%ld."), old_rev, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| } |
| } |
| |
| static const char * |
| describe_incoming_reverse_addition_upon_update( |
| struct conflict_tree_incoming_delete_details *details, |
| svn_node_kind_t victim_node_kind, |
| svn_revnum_t old_rev, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool) |
| { |
| if (details->replacing_node_kind == svn_node_file || |
| details->replacing_node_kind == svn_node_symlink) |
| { |
| if (victim_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Directory updated backwards from r%ld to r%ld " |
| "was a file before the replacement made by %s " |
| "in r%ld."), old_rev, new_rev, |
| details->rev_author, details->added_rev); |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("File updated backwards from r%ld to r%ld was a " |
| "file from another line of history before the " |
| "replacement made by %s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Item updated backwards from r%ld to r%ld was " |
| "replaced with a file by %s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->added_rev); |
| } |
| else if (details->replacing_node_kind == svn_node_dir) |
| { |
| if (victim_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Directory updated backwards from r%ld to r%ld " |
| "was a directory from another line of history " |
| "before the replacement made by %s in " |
| "r%ld."), old_rev, new_rev, |
| details->rev_author, details->added_rev); |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("File updated backwards from r%ld to r%ld was a " |
| "directory before the replacement made by %s " |
| "in r%ld."), old_rev, new_rev, |
| details->rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Item updated backwards from r%ld to r%ld was " |
| "replaced with a directory by %s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->added_rev); |
| } |
| else |
| { |
| if (victim_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Directory updated backwards from r%ld to r%ld " |
| "did not exist before it was added by %s in " |
| "r%ld."), old_rev, new_rev, |
| details->rev_author, details->added_rev); |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("File updated backwards from r%ld to r%ld did " |
| "not exist before it was added by %s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Item updated backwards from r%ld to r%ld did " |
| "not exist before it was added by %s in r%ld."), |
| old_rev, new_rev, |
| details->rev_author, details->added_rev); |
| } |
| } |
| |
| static const char * |
| describe_incoming_deletion_upon_switch( |
| struct conflict_tree_incoming_delete_details *details, |
| svn_node_kind_t victim_node_kind, |
| const char *old_repos_relpath, |
| svn_revnum_t old_rev, |
| const char *new_repos_relpath, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| if (details->replacing_node_kind == svn_node_file || |
| details->replacing_node_kind == svn_node_symlink) |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Directory switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was replaced with a file by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced directory was moved " |
| "to '^/%s'."), description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("File switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "replaced with a file from another line of " |
| "history by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced file was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Item switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "replaced with a file by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced item was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| } |
| else if (details->replacing_node_kind == svn_node_dir) |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Directory switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was replaced with a directory from another " |
| "line of history by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced directory was moved to " |
| "'^/%s'."), description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("File switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was replaced with a directory by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced file was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Item switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "replaced with a directory by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced item was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| } |
| else |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| const char *description; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("Directory switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was moved to '^/%s' by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| get_moved_to_repos_relpath(details, scratch_pool), |
| details->rev_author, details->deleted_rev); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("Directory switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was deleted by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| const char *description; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("File switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "moved to '^/%s' by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| get_moved_to_repos_relpath(details, scratch_pool), |
| details->rev_author, details->deleted_rev); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("File switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "deleted by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| else |
| { |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| const char *description; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("Item switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "moved to '^/%s' by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| get_moved_to_repos_relpath(details, scratch_pool), |
| details->rev_author, details->deleted_rev); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("Item switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "deleted by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| } |
| } |
| |
| static const char * |
| describe_incoming_reverse_addition_upon_switch( |
| struct conflict_tree_incoming_delete_details *details, |
| svn_node_kind_t victim_node_kind, |
| const char *old_repos_relpath, |
| svn_revnum_t old_rev, |
| const char *new_repos_relpath, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool) |
| { |
| if (details->replacing_node_kind == svn_node_file || |
| details->replacing_node_kind == svn_node_symlink) |
| { |
| if (victim_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Directory switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was a file before the replacement made by %s " |
| "in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("File switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas a " |
| "file from another line of history before the " |
| "replacement made by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Item switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "replaced with a file by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| } |
| else if (details->replacing_node_kind == svn_node_dir) |
| { |
| if (victim_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Directory switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was a directory from another line of history " |
| "before the replacement made by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("Directory switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was a file before the replacement made by %s " |
| "in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Item switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "replaced with a directory by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| } |
| else |
| { |
| if (victim_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Directory switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "did not exist before it was added by %s in " |
| "r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("File switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\ndid " |
| "not exist before it was added by %s in " |
| "r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Item switched from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\ndid " |
| "not exist before it was added by %s in " |
| "r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| } |
| } |
| |
| static const char * |
| describe_incoming_deletion_upon_merge( |
| struct conflict_tree_incoming_delete_details *details, |
| svn_node_kind_t victim_node_kind, |
| const char *old_repos_relpath, |
| svn_revnum_t old_rev, |
| const char *new_repos_relpath, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| if (details->replacing_node_kind == svn_node_file || |
| details->replacing_node_kind == svn_node_symlink) |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Directory merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was replaced with a file by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced directory was moved to " |
| "'^/%s'."), description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("File merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "replaced with a file from another line of " |
| "history by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced file was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("Item merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "replaced with a file by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| else if (details->replacing_node_kind == svn_node_dir) |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Directory merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was replaced with a directory from another " |
| "line of history by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced directory was moved to " |
| "'^/%s'."), description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("File merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was replaced with a directory by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced file was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| else |
| { |
| const char *description = |
| apr_psprintf(result_pool, |
| _("Item merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "replaced with a directory by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("%s\nThe replaced item was moved to '^/%s'."), |
| description, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| return description; |
| } |
| } |
| else |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| const char *description; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("Directory merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "moved to '^/%s' by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| get_moved_to_repos_relpath(details, scratch_pool), |
| details->rev_author, details->deleted_rev); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("Directory merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "deleted by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| const char *description; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("File merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "moved to '^/%s' by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| get_moved_to_repos_relpath(details, scratch_pool), |
| details->rev_author, details->deleted_rev); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("File merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "deleted by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| else |
| { |
| if (details->moves) |
| { |
| struct repos_move_info *move; |
| const char *description; |
| |
| move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); |
| description = |
| apr_psprintf(result_pool, |
| _("Item merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "moved to '^/%s' by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| get_moved_to_repos_relpath(details, scratch_pool), |
| details->rev_author, details->deleted_rev); |
| return append_moved_to_chain_description(description, |
| move->next, |
| result_pool, |
| scratch_pool); |
| } |
| else |
| return apr_psprintf(result_pool, |
| _("Item merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " |
| "deleted by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->deleted_rev); |
| } |
| } |
| } |
| |
| static const char * |
| describe_incoming_reverse_addition_upon_merge( |
| struct conflict_tree_incoming_delete_details *details, |
| svn_node_kind_t victim_node_kind, |
| const char *old_repos_relpath, |
| svn_revnum_t old_rev, |
| const char *new_repos_relpath, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool) |
| { |
| if (details->replacing_node_kind == svn_node_file || |
| details->replacing_node_kind == svn_node_symlink) |
| { |
| if (victim_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Directory reverse-merged from\n'^/%s@%ld'\nto " |
| "^/%s@%ld was a file before the replacement " |
| "made by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("File reverse-merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was a file from another line of history before " |
| "the replacement made by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Item reverse-merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was replaced with a file by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| } |
| else if (details->replacing_node_kind == svn_node_dir) |
| { |
| if (victim_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Directory reverse-merged from\n'^/%s@%ld'\nto " |
| "^/%s@%ld was a directory from another line " |
| "of history before the replacement made by %s " |
| "in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("Directory reverse-merged from\n'^/%s@%ld'\nto " |
| "^/%s@%ld was a file before the replacement " |
| "made by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Item reverse-merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "was replaced with a directory by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| } |
| else |
| { |
| if (victim_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Directory reverse-merged from\n'^/%s@%ld'\nto " |
| "^/%s@%ld did not exist before it was added " |
| "by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("File reverse-merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "did not exist before it was added by %s in " |
| "r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Item reverse-merged from\n" |
| "'^/%s@%ld'\nto\n'^/%s@%ld'\n" |
| "did not exist before it was added by %s in " |
| "r%ld."), |
| old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, |
| details->rev_author, details->added_rev); |
| } |
| } |
| |
| /* Implements tree_conflict_get_description_func_t. */ |
| static svn_error_t * |
| conflict_tree_get_description_incoming_delete( |
| const char **incoming_change_description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *action; |
| svn_node_kind_t victim_node_kind; |
| svn_wc_operation_t conflict_operation; |
| const char *old_repos_relpath; |
| svn_revnum_t old_rev; |
| const char *new_repos_relpath; |
| svn_revnum_t new_rev; |
| struct conflict_tree_incoming_delete_details *details; |
| |
| if (conflict->tree_conflict_incoming_details == NULL) |
| return svn_error_trace(conflict_tree_get_incoming_description_generic( |
| incoming_change_description, |
| conflict, ctx, result_pool, scratch_pool)); |
| |
| conflict_operation = svn_client_conflict_get_operation(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &old_repos_relpath, &old_rev, NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &new_repos_relpath, &new_rev, NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| details = conflict->tree_conflict_incoming_details; |
| |
| if (conflict_operation == svn_wc_operation_update) |
| { |
| if (details->deleted_rev != SVN_INVALID_REVNUM) |
| { |
| action = describe_incoming_deletion_upon_update(details, |
| victim_node_kind, |
| old_rev, |
| new_rev, |
| result_pool, |
| scratch_pool); |
| } |
| else /* details->added_rev != SVN_INVALID_REVNUM */ |
| { |
| /* This deletion is really the reverse change of an addition. */ |
| action = describe_incoming_reverse_addition_upon_update( |
| details, victim_node_kind, old_rev, new_rev, result_pool); |
| } |
| } |
| else if (conflict_operation == svn_wc_operation_switch) |
| { |
| if (details->deleted_rev != SVN_INVALID_REVNUM) |
| { |
| action = describe_incoming_deletion_upon_switch(details, |
| victim_node_kind, |
| old_repos_relpath, |
| old_rev, |
| new_repos_relpath, |
| new_rev, |
| result_pool, |
| scratch_pool); |
| } |
| else /* details->added_rev != SVN_INVALID_REVNUM */ |
| { |
| /* This deletion is really the reverse change of an addition. */ |
| action = describe_incoming_reverse_addition_upon_switch( |
| details, victim_node_kind, old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, result_pool); |
| |
| } |
| } |
| else if (conflict_operation == svn_wc_operation_merge) |
| { |
| if (details->deleted_rev != SVN_INVALID_REVNUM) |
| { |
| action = describe_incoming_deletion_upon_merge(details, |
| victim_node_kind, |
| old_repos_relpath, |
| old_rev, |
| new_repos_relpath, |
| new_rev, |
| result_pool, |
| scratch_pool); |
| } |
| else /* details->added_rev != SVN_INVALID_REVNUM */ |
| { |
| /* This deletion is really the reverse change of an addition. */ |
| action = describe_incoming_reverse_addition_upon_merge( |
| details, victim_node_kind, old_repos_relpath, old_rev, |
| new_repos_relpath, new_rev, result_pool); |
| } |
| } |
| |
| *incoming_change_description = apr_pstrdup(result_pool, action); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Baton for find_added_rev(). */ |
| struct find_added_rev_baton |
| { |
| const char *victim_abspath; |
| svn_client_ctx_t *ctx; |
| svn_revnum_t added_rev; |
| const char *repos_relpath; |
| const char *parent_repos_relpath; |
| apr_pool_t *pool; |
| }; |
| |
| /* Implements svn_location_segment_receiver_t. |
| * Finds the revision in which a node was added by tracing 'start' |
| * revisions in location segments reported for the node. |
| * If the PARENT_REPOS_RELPATH in the baton is not NULL, only consider |
| * segments in which the node existed somewhere beneath this path. */ |
| static svn_error_t * |
| find_added_rev(svn_location_segment_t *segment, |
| void *baton, |
| apr_pool_t *scratch_pool) |
| { |
| struct find_added_rev_baton *b = baton; |
| |
| if (b->ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify( |
| b->victim_abspath, |
| svn_wc_notify_tree_conflict_details_progress, |
| scratch_pool), |
| notify->revision = segment->range_start; |
| b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| if (segment->path) /* not interested in gaps */ |
| { |
| if (b->parent_repos_relpath == NULL || |
| svn_relpath_skip_ancestor(b->parent_repos_relpath, |
| segment->path) != NULL) |
| { |
| b->added_rev = segment->range_start; |
| b->repos_relpath = apr_pstrdup(b->pool, segment->path); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Find conflict details in the case where a revision which added a node was |
| * applied in reverse, resulting in an incoming deletion. */ |
| static svn_error_t * |
| get_incoming_delete_details_for_reverse_addition( |
| struct conflict_tree_incoming_delete_details **details, |
| const char *repos_root_url, |
| const char *old_repos_relpath, |
| svn_revnum_t old_rev, |
| svn_revnum_t new_rev, |
| svn_client_ctx_t *ctx, |
| const char *victim_abspath, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_ra_session_t *ra_session; |
| const char *url; |
| const char *corrected_url; |
| svn_string_t *author_revprop; |
| struct find_added_rev_baton b = { 0 }; |
| |
| url = svn_path_url_add_component2(repos_root_url, old_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, |
| &corrected_url, |
| url, NULL, NULL, |
| FALSE, |
| FALSE, |
| ctx, |
| scratch_pool, |
| scratch_pool)); |
| |
| *details = apr_pcalloc(result_pool, sizeof(**details)); |
| b.ctx = ctx; |
| b.victim_abspath = victim_abspath; |
| b.added_rev = SVN_INVALID_REVNUM; |
| b.repos_relpath = NULL; |
| b.parent_repos_relpath = NULL; |
| b.pool = scratch_pool; |
| |
| /* Figure out when this node was added. */ |
| SVN_ERR(svn_ra_get_location_segments(ra_session, "", old_rev, |
| old_rev, new_rev, |
| find_added_rev, &b, |
| scratch_pool)); |
| |
| SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev, |
| SVN_PROP_REVISION_AUTHOR, |
| &author_revprop, scratch_pool)); |
| (*details)->deleted_rev = SVN_INVALID_REVNUM; |
| (*details)->added_rev = b.added_rev; |
| (*details)->repos_relpath = apr_pstrdup(result_pool, b.repos_relpath); |
| if (author_revprop) |
| (*details)->rev_author = apr_pstrdup(result_pool, author_revprop->data); |
| else |
| (*details)->rev_author = _("unknown author"); |
| |
| /* Check for replacement. */ |
| (*details)->replacing_node_kind = svn_node_none; |
| if ((*details)->added_rev > 0) |
| { |
| svn_node_kind_t replaced_node_kind; |
| |
| SVN_ERR(svn_ra_check_path(ra_session, "", |
| rev_below((*details)->added_rev), |
| &replaced_node_kind, scratch_pool)); |
| if (replaced_node_kind != svn_node_none) |
| SVN_ERR(svn_ra_check_path(ra_session, "", (*details)->added_rev, |
| &(*details)->replacing_node_kind, |
| scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| init_wc_move_targets(struct conflict_tree_incoming_delete_details *details, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| int i; |
| const char *victim_abspath; |
| svn_node_kind_t victim_node_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| /* ### Should we get the old location in case of reverse-merges? */ |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, |
| scratch_pool, scratch_pool)); |
| details->wc_move_targets = apr_hash_make(conflict->pool); |
| for (i = 0; i < details->moves->nelts; i++) |
| { |
| struct repos_move_info *move; |
| |
| move = APR_ARRAY_IDX(details->moves, i, struct repos_move_info *); |
| SVN_ERR(follow_move_chains(details->wc_move_targets, move, |
| ctx, victim_abspath, |
| victim_node_kind, |
| incoming_new_repos_relpath, |
| incoming_new_pegrev, |
| conflict->pool, scratch_pool)); |
| } |
| |
| /* Initialize to the first possible move target. Hopefully, |
| * in most cases there will only be one candidate anyway. */ |
| details->move_target_repos_relpath = |
| get_moved_to_repos_relpath(details, scratch_pool); |
| details->wc_move_target_idx = 0; |
| |
| /* If only one move target exists recommend a resolution option. */ |
| if (apr_hash_count(details->wc_move_targets) == 1) |
| { |
| apr_array_header_t *wc_abspaths; |
| |
| wc_abspaths = svn_hash_gets(details->wc_move_targets, |
| details->move_target_repos_relpath); |
| if (wc_abspaths->nelts == 1) |
| { |
| svn_client_conflict_option_id_t recommended[] = |
| { |
| /* Only one of these will be present for any given conflict. */ |
| svn_client_conflict_option_incoming_move_file_text_merge, |
| svn_client_conflict_option_incoming_move_dir_merge, |
| svn_client_conflict_option_local_move_file_text_merge, |
| svn_client_conflict_option_local_move_dir_merge, |
| svn_client_conflict_option_sibling_move_file_text_merge, |
| svn_client_conflict_option_sibling_move_dir_merge, |
| }; |
| apr_array_header_t *options; |
| |
| SVN_ERR(svn_client_conflict_tree_get_resolution_options( |
| &options, conflict, ctx, scratch_pool, scratch_pool)); |
| for (i = 0; i < (sizeof(recommended) / sizeof(recommended[0])); i++) |
| { |
| svn_client_conflict_option_id_t option_id = recommended[i]; |
| |
| if (svn_client_conflict_option_find_by_id(options, option_id)) |
| { |
| conflict->recommended_option_id = option_id; |
| break; |
| } |
| } |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements tree_conflict_get_details_func_t. |
| * Find the revision in which the victim was deleted in the repository. */ |
| static svn_error_t * |
| conflict_tree_get_details_incoming_delete(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *old_repos_relpath; |
| const char *new_repos_relpath; |
| const char *repos_root_url; |
| svn_revnum_t old_rev; |
| svn_revnum_t new_rev; |
| svn_node_kind_t old_kind; |
| svn_node_kind_t new_kind; |
| struct conflict_tree_incoming_delete_details *details; |
| svn_wc_operation_t operation; |
| |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &old_repos_relpath, &old_rev, &old_kind, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &new_repos_relpath, &new_rev, &new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, |
| scratch_pool, scratch_pool)); |
| operation = svn_client_conflict_get_operation(conflict); |
| |
| if (operation == svn_wc_operation_update) |
| { |
| if (old_rev < new_rev) |
| { |
| const char *parent_repos_relpath; |
| svn_revnum_t parent_peg_rev; |
| svn_revnum_t deleted_rev; |
| svn_revnum_t end_rev; |
| const char *deleted_rev_author; |
| svn_node_kind_t replacing_node_kind; |
| apr_array_header_t *moves; |
| const char *related_repos_relpath; |
| svn_revnum_t related_peg_rev; |
| |
| /* The update operation went forward in history. */ |
| SVN_ERR(svn_wc__node_get_repos_info(&parent_peg_rev, |
| &parent_repos_relpath, |
| NULL, NULL, |
| ctx->wc_ctx, |
| svn_dirent_dirname( |
| conflict->local_abspath, |
| scratch_pool), |
| scratch_pool, |
| scratch_pool)); |
| if (new_kind == svn_node_none) |
| { |
| SVN_ERR(find_related_node(&related_repos_relpath, |
| &related_peg_rev, |
| new_repos_relpath, new_rev, |
| old_repos_relpath, old_rev, |
| conflict, ctx, |
| scratch_pool, scratch_pool)); |
| } |
| else |
| { |
| /* related to self */ |
| related_repos_relpath = NULL; |
| related_peg_rev = SVN_INVALID_REVNUM; |
| } |
| |
| end_rev = (new_kind == svn_node_none ? 0 : old_rev); |
| if (end_rev >= parent_peg_rev) |
| end_rev = (parent_peg_rev > 0 ? parent_peg_rev - 1 : 0); |
| |
| SVN_ERR(find_revision_for_suspected_deletion( |
| &deleted_rev, &deleted_rev_author, &replacing_node_kind, |
| &moves, conflict, |
| svn_dirent_basename(conflict->local_abspath, scratch_pool), |
| parent_repos_relpath, parent_peg_rev, end_rev, |
| related_repos_relpath, related_peg_rev, |
| ctx, conflict->pool, scratch_pool)); |
| if (deleted_rev == SVN_INVALID_REVNUM) |
| { |
| /* We could not determine the revision in which the node was |
| * deleted. We cannot provide the required details so the best |
| * we can do is fall back to the default description. */ |
| return SVN_NO_ERROR; |
| } |
| |
| details = apr_pcalloc(conflict->pool, sizeof(*details)); |
| details->deleted_rev = deleted_rev; |
| details->added_rev = SVN_INVALID_REVNUM; |
| details->repos_relpath = apr_pstrdup(conflict->pool, |
| new_repos_relpath); |
| details->rev_author = deleted_rev_author; |
| details->replacing_node_kind = replacing_node_kind; |
| details->moves = moves; |
| } |
| else /* new_rev < old_rev */ |
| { |
| /* The update operation went backwards in history. |
| * Figure out when this node was added. */ |
| SVN_ERR(get_incoming_delete_details_for_reverse_addition( |
| &details, repos_root_url, old_repos_relpath, |
| old_rev, new_rev, ctx, |
| svn_client_conflict_get_local_abspath(conflict), |
| conflict->pool, scratch_pool)); |
| } |
| } |
| else if (operation == svn_wc_operation_switch || |
| operation == svn_wc_operation_merge) |
| { |
| if (old_rev < new_rev) |
| { |
| svn_revnum_t deleted_rev; |
| const char *deleted_rev_author; |
| svn_node_kind_t replacing_node_kind; |
| apr_array_header_t *moves; |
| |
| /* The switch/merge operation went forward in history. |
| * |
| * The deletion of the node happened on the branch we switched to |
| * or merged from. Scan new_repos_relpath's parent's log to find |
| * the revision which deleted the node. */ |
| SVN_ERR(find_revision_for_suspected_deletion( |
| &deleted_rev, &deleted_rev_author, &replacing_node_kind, |
| &moves, conflict, |
| svn_relpath_basename(new_repos_relpath, scratch_pool), |
| svn_relpath_dirname(new_repos_relpath, scratch_pool), |
| new_rev, old_rev, old_repos_relpath, old_rev, ctx, |
| conflict->pool, scratch_pool)); |
| if (deleted_rev == SVN_INVALID_REVNUM) |
| { |
| /* We could not determine the revision in which the node was |
| * deleted. We cannot provide the required details so the best |
| * we can do is fall back to the default description. */ |
| return SVN_NO_ERROR; |
| } |
| |
| details = apr_pcalloc(conflict->pool, sizeof(*details)); |
| details->deleted_rev = deleted_rev; |
| details->added_rev = SVN_INVALID_REVNUM; |
| details->repos_relpath = apr_pstrdup(conflict->pool, |
| new_repos_relpath); |
| details->rev_author = apr_pstrdup(conflict->pool, |
| deleted_rev_author); |
| details->replacing_node_kind = replacing_node_kind; |
| details->moves = moves; |
| } |
| else /* new_rev < old_rev */ |
| { |
| /* The switch/merge operation went backwards in history. |
| * Figure out when the node we switched away from, or merged |
| * from another branch, was added. */ |
| SVN_ERR(get_incoming_delete_details_for_reverse_addition( |
| &details, repos_root_url, old_repos_relpath, |
| old_rev, new_rev, ctx, |
| svn_client_conflict_get_local_abspath(conflict), |
| conflict->pool, scratch_pool)); |
| } |
| } |
| else |
| { |
| details = NULL; |
| } |
| |
| conflict->tree_conflict_incoming_details = details; |
| |
| if (details && details->moves) |
| SVN_ERR(init_wc_move_targets(details, conflict, ctx, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Details for tree conflicts involving incoming additions. */ |
| struct conflict_tree_incoming_add_details |
| { |
| /* If not SVN_INVALID_REVNUM, the node was added in ADDED_REV. */ |
| svn_revnum_t added_rev; |
| |
| /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. |
| * Note that both ADDED_REV and DELETED_REV may be valid for update/switch. |
| * See comment in conflict_tree_get_details_incoming_add() for details. */ |
| svn_revnum_t deleted_rev; |
| |
| /* The path which was added/deleted relative to the repository root. */ |
| const char *repos_relpath; |
| |
| /* Authors who committed ADDED_REV/DELETED_REV. */ |
| const char *added_rev_author; |
| const char *deleted_rev_author; |
| |
| /* Move information. If not NULL, this is an array of repos_move_info * |
| * elements. Each element is the head of a move chain which starts in |
| * ADDED_REV or in DELETED_REV (in which case moves should be interpreted |
| * in reverse). */ |
| apr_array_header_t *moves; |
| }; |
| |
| /* Implements tree_conflict_get_details_func_t. |
| * Find the revision in which the victim was added in the repository. */ |
| static svn_error_t * |
| conflict_tree_get_details_incoming_add(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *old_repos_relpath; |
| const char *new_repos_relpath; |
| const char *repos_root_url; |
| svn_revnum_t old_rev; |
| svn_revnum_t new_rev; |
| struct conflict_tree_incoming_add_details *details = NULL; |
| svn_wc_operation_t operation; |
| |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &old_repos_relpath, &old_rev, NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &new_repos_relpath, &new_rev, NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, |
| scratch_pool, scratch_pool)); |
| operation = svn_client_conflict_get_operation(conflict); |
| |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| /* Only the new repository location is recorded for the node which |
| * caused an incoming addition. There is no pre-update/pre-switch |
| * revision to be recorded for the node since it does not exist in |
| * the repository at that revision. |
| * The implication is that we cannot know whether the operation went |
| * forward or backwards in history. So always try to find an added |
| * and a deleted revision for the node. Users must figure out by whether |
| * the addition or deletion caused the conflict. */ |
| const char *url; |
| const char *corrected_url; |
| svn_string_t *author_revprop; |
| struct find_added_rev_baton b = { 0 }; |
| svn_ra_session_t *ra_session; |
| svn_revnum_t deleted_rev; |
| svn_revnum_t head_rev; |
| |
| url = svn_path_url_add_component2(repos_root_url, new_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, |
| &corrected_url, |
| url, NULL, NULL, |
| FALSE, |
| FALSE, |
| ctx, |
| scratch_pool, |
| scratch_pool)); |
| |
| details = apr_pcalloc(conflict->pool, sizeof(*details)); |
| b.ctx = ctx, |
| b.victim_abspath = svn_client_conflict_get_local_abspath(conflict), |
| b.added_rev = SVN_INVALID_REVNUM; |
| b.repos_relpath = NULL; |
| b.parent_repos_relpath = NULL; |
| b.pool = scratch_pool; |
| |
| /* Figure out when this node was added. */ |
| SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev, |
| new_rev, SVN_INVALID_REVNUM, |
| find_added_rev, &b, |
| scratch_pool)); |
| |
| SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev, |
| SVN_PROP_REVISION_AUTHOR, |
| &author_revprop, scratch_pool)); |
| details->repos_relpath = apr_pstrdup(conflict->pool, b.repos_relpath); |
| details->added_rev = b.added_rev; |
| if (author_revprop) |
| details->added_rev_author = apr_pstrdup(conflict->pool, |
| author_revprop->data); |
| else |
| details->added_rev_author = _("unknown author"); |
| details->deleted_rev = SVN_INVALID_REVNUM; |
| details->deleted_rev_author = NULL; |
| |
| /* Figure out whether this node was deleted later. |
| * ### Could probably optimize by inferring both addition and deletion |
| * ### from svn_ra_get_location_segments() call above. */ |
| SVN_ERR(svn_ra_get_latest_revnum(ra_session, &head_rev, scratch_pool)); |
| if (new_rev < head_rev) |
| { |
| SVN_ERR(svn_ra_get_deleted_rev(ra_session, "", new_rev, head_rev, |
| &deleted_rev, scratch_pool)); |
| if (SVN_IS_VALID_REVNUM(deleted_rev)) |
| { |
| SVN_ERR(svn_ra_rev_prop(ra_session, deleted_rev, |
| SVN_PROP_REVISION_AUTHOR, |
| &author_revprop, scratch_pool)); |
| details->deleted_rev = deleted_rev; |
| if (author_revprop) |
| details->deleted_rev_author = apr_pstrdup(conflict->pool, |
| author_revprop->data); |
| else |
| details->deleted_rev_author = _("unknown author"); |
| } |
| } |
| } |
| else if (operation == svn_wc_operation_merge && |
| strcmp(old_repos_relpath, new_repos_relpath) == 0) |
| { |
| if (old_rev < new_rev) |
| { |
| /* The merge operation went forwards in history. |
| * The addition of the node happened on the branch we merged form. |
| * Scan the nodes's history to find the revision which added it. */ |
| const char *url; |
| const char *corrected_url; |
| svn_string_t *author_revprop; |
| struct find_added_rev_baton b = { 0 }; |
| svn_ra_session_t *ra_session; |
| |
| url = svn_path_url_add_component2(repos_root_url, new_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, |
| &corrected_url, |
| url, NULL, NULL, |
| FALSE, |
| FALSE, |
| ctx, |
| scratch_pool, |
| scratch_pool)); |
| |
| details = apr_pcalloc(conflict->pool, sizeof(*details)); |
| b.victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| b.ctx = ctx; |
| b.added_rev = SVN_INVALID_REVNUM; |
| b.repos_relpath = NULL; |
| b.parent_repos_relpath = NULL; |
| b.pool = scratch_pool; |
| |
| /* Figure out when this node was added. */ |
| SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev, |
| new_rev, old_rev, |
| find_added_rev, &b, |
| scratch_pool)); |
| |
| SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev, |
| SVN_PROP_REVISION_AUTHOR, |
| &author_revprop, scratch_pool)); |
| details->repos_relpath = apr_pstrdup(conflict->pool, b.repos_relpath); |
| details->added_rev = b.added_rev; |
| if (author_revprop) |
| details->added_rev_author = apr_pstrdup(conflict->pool, |
| author_revprop->data); |
| else |
| details->added_rev_author = _("unknown author"); |
| details->deleted_rev = SVN_INVALID_REVNUM; |
| details->deleted_rev_author = NULL; |
| } |
| else if (old_rev > new_rev) |
| { |
| /* The merge operation was a reverse-merge. |
| * This addition is in fact a deletion, applied in reverse, |
| * which happened on the branch we merged from. |
| * Find the revision which deleted the node. */ |
| svn_revnum_t deleted_rev; |
| const char *deleted_rev_author; |
| svn_node_kind_t replacing_node_kind; |
| apr_array_header_t *moves; |
| |
| SVN_ERR(find_revision_for_suspected_deletion( |
| &deleted_rev, &deleted_rev_author, &replacing_node_kind, |
| &moves, conflict, |
| svn_relpath_basename(old_repos_relpath, scratch_pool), |
| svn_relpath_dirname(old_repos_relpath, scratch_pool), |
| old_rev, new_rev, |
| NULL, SVN_INVALID_REVNUM, /* related to self */ |
| ctx, |
| conflict->pool, scratch_pool)); |
| if (deleted_rev == SVN_INVALID_REVNUM) |
| { |
| /* We could not determine the revision in which the node was |
| * deleted. We cannot provide the required details so the best |
| * we can do is fall back to the default description. */ |
| return SVN_NO_ERROR; |
| } |
| |
| details = apr_pcalloc(conflict->pool, sizeof(*details)); |
| details->repos_relpath = apr_pstrdup(conflict->pool, |
| new_repos_relpath); |
| details->deleted_rev = deleted_rev; |
| details->deleted_rev_author = apr_pstrdup(conflict->pool, |
| deleted_rev_author); |
| |
| details->added_rev = SVN_INVALID_REVNUM; |
| details->added_rev_author = NULL; |
| details->moves = moves; |
| } |
| } |
| |
| conflict->tree_conflict_incoming_details = details; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static const char * |
| describe_incoming_add_upon_update( |
| struct conflict_tree_incoming_add_details *details, |
| svn_node_kind_t new_node_kind, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool) |
| { |
| if (new_node_kind == svn_node_dir) |
| { |
| if (SVN_IS_VALID_REVNUM(details->added_rev) && |
| SVN_IS_VALID_REVNUM(details->deleted_rev)) |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during update to r%ld; " |
| "it was added by %s in r%ld and later deleted " |
| "by %s in r%ld."), new_rev, |
| details->added_rev_author, details->added_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| else if (SVN_IS_VALID_REVNUM(details->added_rev)) |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during update to r%ld; " |
| "it was added by %s in r%ld."), new_rev, |
| details->added_rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during update to r%ld; " |
| "it was deleted by %s in r%ld."), new_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| } |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| { |
| if (SVN_IS_VALID_REVNUM(details->added_rev) && |
| SVN_IS_VALID_REVNUM(details->deleted_rev)) |
| return apr_psprintf(result_pool, |
| _("A new file appeared during update to r%ld; " |
| "it was added by %s in r%ld and later deleted " |
| "by %s in r%ld."), new_rev, |
| details->added_rev_author, details->added_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| else if (SVN_IS_VALID_REVNUM(details->added_rev)) |
| return apr_psprintf(result_pool, |
| _("A new file appeared during update to r%ld; " |
| "it was added by %s in r%ld."), new_rev, |
| details->added_rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new file appeared during update to r%ld; " |
| "it was deleted by %s in r%ld."), new_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| } |
| else |
| { |
| if (SVN_IS_VALID_REVNUM(details->added_rev) && |
| SVN_IS_VALID_REVNUM(details->deleted_rev)) |
| return apr_psprintf(result_pool, |
| _("A new item appeared during update to r%ld; " |
| "it was added by %s in r%ld and later deleted " |
| "by %s in r%ld."), new_rev, |
| details->added_rev_author, details->added_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| else if (SVN_IS_VALID_REVNUM(details->added_rev)) |
| return apr_psprintf(result_pool, |
| _("A new item appeared during update to r%ld; " |
| "it was added by %s in r%ld."), new_rev, |
| details->added_rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new item appeared during update to r%ld; " |
| "it was deleted by %s in r%ld."), new_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| } |
| } |
| |
| static const char * |
| describe_incoming_add_upon_switch( |
| struct conflict_tree_incoming_add_details *details, |
| svn_node_kind_t victim_node_kind, |
| const char *new_repos_relpath, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool) |
| { |
| if (victim_node_kind == svn_node_dir) |
| { |
| if (SVN_IS_VALID_REVNUM(details->added_rev) && |
| SVN_IS_VALID_REVNUM(details->deleted_rev)) |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during switch to\n" |
| "'^/%s@%ld'.\n" |
| "It was added by %s in r%ld and later deleted " |
| "by %s in r%ld."), new_repos_relpath, new_rev, |
| details->added_rev_author, details->added_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| else if (SVN_IS_VALID_REVNUM(details->added_rev)) |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during switch to\n" |
| "'^/%s@%ld'.\nIt was added by %s in r%ld."), |
| new_repos_relpath, new_rev, |
| details->added_rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during switch to\n" |
| "'^/%s@%ld'.\nIt was deleted by %s in r%ld."), |
| new_repos_relpath, new_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| } |
| else if (victim_node_kind == svn_node_file || |
| victim_node_kind == svn_node_symlink) |
| { |
| if (SVN_IS_VALID_REVNUM(details->added_rev) && |
| SVN_IS_VALID_REVNUM(details->deleted_rev)) |
| return apr_psprintf(result_pool, |
| _("A new file appeared during switch to\n" |
| "'^/%s@%ld'.\n" |
| "It was added by %s in r%ld and later deleted " |
| "by %s in r%ld."), new_repos_relpath, new_rev, |
| details->added_rev_author, details->added_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| else if (SVN_IS_VALID_REVNUM(details->added_rev)) |
| return apr_psprintf(result_pool, |
| _("A new file appeared during switch to\n" |
| "'^/%s@%ld'.\n" |
| "It was added by %s in r%ld."), |
| new_repos_relpath, new_rev, |
| details->added_rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new file appeared during switch to\n" |
| "'^/%s@%ld'.\n" |
| "It was deleted by %s in r%ld."), |
| new_repos_relpath, new_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| } |
| else |
| { |
| if (SVN_IS_VALID_REVNUM(details->added_rev) && |
| SVN_IS_VALID_REVNUM(details->deleted_rev)) |
| return apr_psprintf(result_pool, |
| _("A new item appeared during switch to\n" |
| "'^/%s@%ld'.\n" |
| "It was added by %s in r%ld and later deleted " |
| "by %s in r%ld."), new_repos_relpath, new_rev, |
| details->added_rev_author, details->added_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| else if (SVN_IS_VALID_REVNUM(details->added_rev)) |
| return apr_psprintf(result_pool, |
| _("A new item appeared during switch to\n" |
| "'^/%s@%ld'.\n" |
| "It was added by %s in r%ld."), |
| new_repos_relpath, new_rev, |
| details->added_rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new item appeared during switch to\n" |
| "'^/%s@%ld'.\n" |
| "It was deleted by %s in r%ld."), |
| new_repos_relpath, new_rev, |
| details->deleted_rev_author, details->deleted_rev); |
| } |
| } |
| |
| static const char * |
| describe_incoming_add_upon_merge( |
| struct conflict_tree_incoming_add_details *details, |
| svn_node_kind_t new_node_kind, |
| svn_revnum_t old_rev, |
| const char *new_repos_relpath, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool) |
| { |
| if (new_node_kind == svn_node_dir) |
| { |
| if (old_rev + 1 == new_rev) |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during merge of\n" |
| "'^/%s:%ld'.\nIt was added by %s in r%ld."), |
| new_repos_relpath, new_rev, |
| details->added_rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during merge of\n" |
| "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."), |
| new_repos_relpath, old_rev + 1, new_rev, |
| details->added_rev_author, details->added_rev); |
| } |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| { |
| if (old_rev + 1 == new_rev) |
| return apr_psprintf(result_pool, |
| _("A new file appeared during merge of\n" |
| "'^/%s:%ld'.\nIt was added by %s in r%ld."), |
| new_repos_relpath, new_rev, |
| details->added_rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new file appeared during merge of\n" |
| "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."), |
| new_repos_relpath, old_rev + 1, new_rev, |
| details->added_rev_author, details->added_rev); |
| } |
| else |
| { |
| if (old_rev + 1 == new_rev) |
| return apr_psprintf(result_pool, |
| _("A new item appeared during merge of\n" |
| "'^/%s:%ld'.\nIt was added by %s in r%ld."), |
| new_repos_relpath, new_rev, |
| details->added_rev_author, details->added_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new item appeared during merge of\n" |
| "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."), |
| new_repos_relpath, old_rev + 1, new_rev, |
| details->added_rev_author, details->added_rev); |
| } |
| } |
| |
| static const char * |
| describe_incoming_reverse_deletion_upon_merge( |
| struct conflict_tree_incoming_add_details *details, |
| svn_node_kind_t new_node_kind, |
| const char *old_repos_relpath, |
| svn_revnum_t old_rev, |
| svn_revnum_t new_rev, |
| apr_pool_t *result_pool) |
| { |
| if (new_node_kind == svn_node_dir) |
| { |
| if (new_rev + 1 == old_rev) |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during reverse-merge of" |
| "\n'^/%s:%ld'.\nIt was deleted by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| details->deleted_rev_author, |
| details->deleted_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new directory appeared during reverse-merge " |
| "of\n'^/%s:%ld-%ld'.\n" |
| "It was deleted by %s in r%ld."), |
| old_repos_relpath, new_rev, rev_below(old_rev), |
| details->deleted_rev_author, |
| details->deleted_rev); |
| } |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| { |
| if (new_rev + 1 == old_rev) |
| return apr_psprintf(result_pool, |
| _("A new file appeared during reverse-merge of\n" |
| "'^/%s:%ld'.\nIt was deleted by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| details->deleted_rev_author, |
| details->deleted_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new file appeared during reverse-merge of\n" |
| "'^/%s:%ld-%ld'.\nIt was deleted by %s in r%ld."), |
| old_repos_relpath, new_rev + 1, old_rev, |
| details->deleted_rev_author, |
| details->deleted_rev); |
| } |
| else |
| { |
| if (new_rev + 1 == old_rev) |
| return apr_psprintf(result_pool, |
| _("A new item appeared during reverse-merge of\n" |
| "'^/%s:%ld'.\nIt was deleted by %s in r%ld."), |
| old_repos_relpath, old_rev, |
| details->deleted_rev_author, |
| details->deleted_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("A new item appeared during reverse-merge of\n" |
| "'^/%s:%ld-%ld'.\nIt was deleted by %s in r%ld."), |
| old_repos_relpath, new_rev + 1, old_rev, |
| details->deleted_rev_author, |
| details->deleted_rev); |
| } |
| } |
| |
| /* Implements tree_conflict_get_description_func_t. */ |
| static svn_error_t * |
| conflict_tree_get_description_incoming_add( |
| const char **incoming_change_description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *action; |
| svn_node_kind_t victim_node_kind; |
| svn_wc_operation_t conflict_operation; |
| const char *old_repos_relpath; |
| svn_revnum_t old_rev; |
| svn_node_kind_t old_node_kind; |
| const char *new_repos_relpath; |
| svn_revnum_t new_rev; |
| svn_node_kind_t new_node_kind; |
| struct conflict_tree_incoming_add_details *details; |
| |
| if (conflict->tree_conflict_incoming_details == NULL) |
| return svn_error_trace(conflict_tree_get_incoming_description_generic( |
| incoming_change_description, conflict, ctx, |
| result_pool, scratch_pool)); |
| |
| conflict_operation = svn_client_conflict_get_operation(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &old_repos_relpath, &old_rev, &old_node_kind, conflict, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &new_repos_relpath, &new_rev, &new_node_kind, conflict, |
| scratch_pool, scratch_pool)); |
| |
| details = conflict->tree_conflict_incoming_details; |
| |
| if (conflict_operation == svn_wc_operation_update) |
| { |
| action = describe_incoming_add_upon_update(details, |
| new_node_kind, |
| new_rev, |
| result_pool); |
| } |
| else if (conflict_operation == svn_wc_operation_switch) |
| { |
| action = describe_incoming_add_upon_switch(details, |
| victim_node_kind, |
| new_repos_relpath, |
| new_rev, |
| result_pool); |
| } |
| else if (conflict_operation == svn_wc_operation_merge) |
| { |
| if (old_rev < new_rev) |
| action = describe_incoming_add_upon_merge(details, |
| new_node_kind, |
| old_rev, |
| new_repos_relpath, |
| new_rev, |
| result_pool); |
| else |
| action = describe_incoming_reverse_deletion_upon_merge( |
| details, new_node_kind, old_repos_relpath, |
| old_rev, new_rev, result_pool); |
| } |
| |
| *incoming_change_description = apr_pstrdup(result_pool, action); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Details for tree conflicts involving incoming edits. |
| * Note that we store an array of these. Each element corresponds to a |
| * revision within the old/new range in which a modification occurred. */ |
| struct conflict_tree_incoming_edit_details |
| { |
| /* The revision in which the edit occurred. */ |
| svn_revnum_t rev; |
| |
| /* The author of the revision. */ |
| const char *author; |
| |
| /** Is the text modified? May be svn_tristate_unknown. */ |
| svn_tristate_t text_modified; |
| |
| /** Are properties modified? May be svn_tristate_unknown. */ |
| svn_tristate_t props_modified; |
| |
| /** For directories, are children modified? |
| * May be svn_tristate_unknown. */ |
| svn_tristate_t children_modified; |
| |
| /* The path which was edited, relative to the repository root. */ |
| const char *repos_relpath; |
| }; |
| |
| /* Baton for find_modified_rev(). */ |
| struct find_modified_rev_baton { |
| const char *victim_abspath; |
| svn_client_ctx_t *ctx; |
| apr_array_header_t *edits; |
| const char *repos_relpath; |
| svn_node_kind_t node_kind; |
| apr_pool_t *result_pool; |
| apr_pool_t *scratch_pool; |
| }; |
| |
| /* Implements svn_log_entry_receiver_t. */ |
| static svn_error_t * |
| find_modified_rev(void *baton, |
| svn_log_entry_t *log_entry, |
| apr_pool_t *scratch_pool) |
| { |
| struct find_modified_rev_baton *b = baton; |
| struct conflict_tree_incoming_edit_details *details = NULL; |
| svn_string_t *author; |
| apr_hash_index_t *hi; |
| apr_pool_t *iterpool; |
| |
| if (b->ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify( |
| b->victim_abspath, |
| svn_wc_notify_tree_conflict_details_progress, |
| scratch_pool), |
| notify->revision = log_entry->revision; |
| b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| /* No paths were changed in this revision. Nothing to do. */ |
| if (! log_entry->changed_paths2) |
| return SVN_NO_ERROR; |
| |
| details = apr_pcalloc(b->result_pool, sizeof(*details)); |
| details->rev = log_entry->revision; |
| author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR); |
| if (author) |
| details->author = apr_pstrdup(b->result_pool, author->data); |
| else |
| details->author = _("unknown author"); |
| |
| details->text_modified = svn_tristate_unknown; |
| details->props_modified = svn_tristate_unknown; |
| details->children_modified = svn_tristate_unknown; |
| |
| iterpool = svn_pool_create(scratch_pool); |
| for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); |
| hi != NULL; |
| hi = apr_hash_next(hi)) |
| { |
| void *val; |
| const char *path; |
| svn_log_changed_path2_t *log_item; |
| |
| svn_pool_clear(iterpool); |
| |
| apr_hash_this(hi, (void *) &path, NULL, &val); |
| log_item = val; |
| |
| /* ### Remove leading slash from paths in log entries. */ |
| if (path[0] == '/') |
| path = svn_relpath_canonicalize(path, iterpool); |
| |
| if (svn_path_compare_paths(b->repos_relpath, path) == 0 && |
| (log_item->action == 'M' || log_item->action == 'A')) |
| { |
| details->text_modified = log_item->text_modified; |
| details->props_modified = log_item->props_modified; |
| details->repos_relpath = apr_pstrdup(b->result_pool, path); |
| |
| if (log_item->copyfrom_path) |
| b->repos_relpath = apr_pstrdup(b->scratch_pool, |
| /* ### remove leading slash */ |
| svn_relpath_canonicalize( |
| log_item->copyfrom_path, |
| iterpool)); |
| } |
| else if (b->node_kind == svn_node_dir && |
| svn_relpath_skip_ancestor(b->repos_relpath, path) != NULL) |
| details->children_modified = svn_tristate_true; |
| } |
| |
| if (b->node_kind == svn_node_dir && |
| details->children_modified == svn_tristate_unknown) |
| details->children_modified = svn_tristate_false; |
| |
| APR_ARRAY_PUSH(b->edits, struct conflict_tree_incoming_edit_details *) = |
| details; |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements tree_conflict_get_details_func_t. |
| * Find one or more revisions in which the victim was modified in the |
| * repository. */ |
| static svn_error_t * |
| conflict_tree_get_details_incoming_edit(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *old_repos_relpath; |
| const char *new_repos_relpath; |
| const char *repos_root_url; |
| svn_revnum_t old_rev; |
| svn_revnum_t new_rev; |
| svn_node_kind_t old_node_kind; |
| svn_node_kind_t new_node_kind; |
| svn_wc_operation_t operation; |
| const char *url; |
| const char *corrected_url; |
| svn_ra_session_t *ra_session; |
| apr_array_header_t *paths; |
| apr_array_header_t *revprops; |
| struct find_modified_rev_baton b = { 0 }; |
| |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &old_repos_relpath, &old_rev, &old_node_kind, conflict, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &new_repos_relpath, &new_rev, &new_node_kind, conflict, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, |
| scratch_pool, scratch_pool)); |
| operation = svn_client_conflict_get_operation(conflict); |
| if (operation == svn_wc_operation_update) |
| { |
| b.node_kind = old_rev < new_rev ? new_node_kind : old_node_kind; |
| |
| /* If there is no node then we cannot find any edits. */ |
| if (b.node_kind == svn_node_none) |
| return SVN_NO_ERROR; |
| |
| url = svn_path_url_add_component2(repos_root_url, |
| old_rev < new_rev ? new_repos_relpath |
| : old_repos_relpath, |
| scratch_pool); |
| |
| b.repos_relpath = old_rev < new_rev ? new_repos_relpath |
| : old_repos_relpath; |
| } |
| else if (operation == svn_wc_operation_switch || |
| operation == svn_wc_operation_merge) |
| { |
| url = svn_path_url_add_component2(repos_root_url, new_repos_relpath, |
| scratch_pool); |
| |
| b.repos_relpath = new_repos_relpath; |
| b.node_kind = new_node_kind; |
| } |
| |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, |
| &corrected_url, |
| url, NULL, NULL, |
| FALSE, |
| FALSE, |
| ctx, |
| scratch_pool, |
| scratch_pool)); |
| |
| paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); |
| APR_ARRAY_PUSH(paths, const char *) = ""; |
| |
| revprops = apr_array_make(scratch_pool, 1, sizeof(const char *)); |
| APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; |
| |
| b.ctx = ctx; |
| b.victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| b.result_pool = conflict->pool; |
| b.scratch_pool = scratch_pool; |
| b.edits = apr_array_make( |
| conflict->pool, 0, |
| sizeof(struct conflict_tree_incoming_edit_details *)); |
| |
| SVN_ERR(svn_ra_get_log2(ra_session, paths, |
| old_rev < new_rev ? old_rev : new_rev, |
| old_rev < new_rev ? new_rev : old_rev, |
| 0, /* no limit */ |
| TRUE, /* need the changed paths list */ |
| FALSE, /* need to traverse copies */ |
| FALSE, /* no need for merged revisions */ |
| revprops, |
| find_modified_rev, &b, |
| scratch_pool)); |
| |
| conflict->tree_conflict_incoming_details = b.edits; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static const char * |
| describe_incoming_edit_upon_update(svn_revnum_t old_rev, |
| svn_revnum_t new_rev, |
| svn_node_kind_t old_node_kind, |
| svn_node_kind_t new_node_kind, |
| apr_pool_t *result_pool) |
| { |
| if (old_rev < new_rev) |
| { |
| if (new_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Changes destined for a directory arrived " |
| "via the following revisions during update " |
| "from r%ld to r%ld."), old_rev, new_rev); |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("Changes destined for a file arrived " |
| "via the following revisions during update " |
| "from r%ld to r%ld"), old_rev, new_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Changes from the following revisions arrived " |
| "during update from r%ld to r%ld"), |
| old_rev, new_rev); |
| } |
| else |
| { |
| if (new_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Changes destined for a directory arrived " |
| "via the following revisions during backwards " |
| "update from r%ld to r%ld"), |
| old_rev, new_rev); |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("Changes destined for a file arrived " |
| "via the following revisions during backwards " |
| "update from r%ld to r%ld"), |
| old_rev, new_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Changes from the following revisions arrived " |
| "during backwards update from r%ld to r%ld"), |
| old_rev, new_rev); |
| } |
| } |
| |
| static const char * |
| describe_incoming_edit_upon_switch(const char *new_repos_relpath, |
| svn_revnum_t new_rev, |
| svn_node_kind_t new_node_kind, |
| apr_pool_t *result_pool) |
| { |
| if (new_node_kind == svn_node_dir) |
| return apr_psprintf(result_pool, |
| _("Changes destined for a directory arrived via " |
| "the following revisions during switch to\n" |
| "'^/%s@r%ld'"), |
| new_repos_relpath, new_rev); |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| return apr_psprintf(result_pool, |
| _("Changes destined for a directory arrived via " |
| "the following revisions during switch to\n" |
| "'^/%s@r%ld'"), |
| new_repos_relpath, new_rev); |
| else |
| return apr_psprintf(result_pool, |
| _("Changes from the following revisions arrived " |
| "during switch to\n'^/%s@r%ld'"), |
| new_repos_relpath, new_rev); |
| } |
| |
| /* Return a string showing the list of revisions in EDITS, ensuring |
| * the string won't grow too large for display. */ |
| static const char * |
| describe_incoming_edit_list_modified_revs(apr_array_header_t *edits, |
| apr_pool_t *result_pool) |
| { |
| int num_revs_to_skip; |
| static const int min_revs_for_skipping = 5; |
| static const int max_revs_to_display = 8; |
| const char *s = ""; |
| int i; |
| |
| if (edits->nelts == 0) |
| return _(" (no revisions found)"); |
| |
| if (edits->nelts <= max_revs_to_display) |
| num_revs_to_skip = 0; |
| else |
| { |
| /* Check if we should insert a placeholder for some revisions because |
| * the string would grow too long for display otherwise. */ |
| num_revs_to_skip = edits->nelts - max_revs_to_display; |
| if (num_revs_to_skip < min_revs_for_skipping) |
| { |
| /* Don't bother with the placeholder. Just list all revisions. */ |
| num_revs_to_skip = 0; |
| } |
| } |
| |
| for (i = 0; i < edits->nelts; i++) |
| { |
| struct conflict_tree_incoming_edit_details *details; |
| |
| details = APR_ARRAY_IDX(edits, i, |
| struct conflict_tree_incoming_edit_details *); |
| if (num_revs_to_skip > 0) |
| { |
| /* Insert a placeholder for revisions falling into the middle of |
| * the range so we'll get something that looks like: |
| * 1, 2, 3, 4, 5 [ placeholder ] 95, 96, 97, 98, 99 */ |
| if (i < max_revs_to_display / 2) |
| s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s, |
| details->rev, details->author, |
| i < edits->nelts - 1 ? "," : ""); |
| else if (i >= max_revs_to_display / 2 && |
| i < edits->nelts - (max_revs_to_display / 2)) |
| continue; |
| else |
| { |
| if (i == edits->nelts - (max_revs_to_display / 2)) |
| s = apr_psprintf(result_pool, |
| Q_("%s\n [%d revision omitted for " |
| "brevity],\n", |
| "%s\n [%d revisions omitted for " |
| "brevity],\n", |
| num_revs_to_skip), |
| s, num_revs_to_skip); |
| |
| s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s, |
| details->rev, details->author, |
| i < edits->nelts - 1 ? "," : ""); |
| } |
| } |
| else |
| s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s, |
| details->rev, details->author, |
| i < edits->nelts - 1 ? "," : ""); |
| } |
| |
| return s; |
| } |
| |
| /* Implements tree_conflict_get_description_func_t. */ |
| static svn_error_t * |
| conflict_tree_get_description_incoming_edit( |
| const char **incoming_change_description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *action; |
| svn_wc_operation_t conflict_operation; |
| const char *old_repos_relpath; |
| svn_revnum_t old_rev; |
| svn_node_kind_t old_node_kind; |
| const char *new_repos_relpath; |
| svn_revnum_t new_rev; |
| svn_node_kind_t new_node_kind; |
| apr_array_header_t *edits; |
| |
| if (conflict->tree_conflict_incoming_details == NULL) |
| return svn_error_trace(conflict_tree_get_incoming_description_generic( |
| incoming_change_description, conflict, ctx, |
| result_pool, scratch_pool)); |
| |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &old_repos_relpath, &old_rev, &old_node_kind, conflict, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &new_repos_relpath, &new_rev, &new_node_kind, conflict, |
| scratch_pool, scratch_pool)); |
| |
| conflict_operation = svn_client_conflict_get_operation(conflict); |
| |
| edits = conflict->tree_conflict_incoming_details; |
| |
| if (conflict_operation == svn_wc_operation_update) |
| action = describe_incoming_edit_upon_update(old_rev, new_rev, |
| old_node_kind, new_node_kind, |
| scratch_pool); |
| else if (conflict_operation == svn_wc_operation_switch) |
| action = describe_incoming_edit_upon_switch(new_repos_relpath, new_rev, |
| new_node_kind, scratch_pool); |
| else if (conflict_operation == svn_wc_operation_merge) |
| { |
| /* Handle merge inline because it returns early sometimes. */ |
| if (old_rev < new_rev) |
| { |
| if (old_rev + 1 == new_rev) |
| { |
| if (new_node_kind == svn_node_dir) |
| action = apr_psprintf(scratch_pool, |
| _("Changes destined for a directory " |
| "arrived during merge of\n" |
| "'^/%s:%ld'."), |
| new_repos_relpath, new_rev); |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| action = apr_psprintf(scratch_pool, |
| _("Changes destined for a file " |
| "arrived during merge of\n" |
| "'^/%s:%ld'."), |
| new_repos_relpath, new_rev); |
| else |
| action = apr_psprintf(scratch_pool, |
| _("Changes arrived during merge of\n" |
| "'^/%s:%ld'."), |
| new_repos_relpath, new_rev); |
| |
| *incoming_change_description = apr_pstrdup(result_pool, action); |
| |
| return SVN_NO_ERROR; |
| } |
| else |
| { |
| if (new_node_kind == svn_node_dir) |
| action = apr_psprintf(scratch_pool, |
| _("Changes destined for a directory " |
| "arrived via the following revisions " |
| "during merge of\n'^/%s:%ld-%ld'"), |
| new_repos_relpath, old_rev + 1, new_rev); |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| action = apr_psprintf(scratch_pool, |
| _("Changes destined for a file " |
| "arrived via the following revisions " |
| "during merge of\n'^/%s:%ld-%ld'"), |
| new_repos_relpath, old_rev + 1, new_rev); |
| else |
| action = apr_psprintf(scratch_pool, |
| _("Changes from the following revisions " |
| "arrived during merge of\n" |
| "'^/%s:%ld-%ld'"), |
| new_repos_relpath, old_rev + 1, new_rev); |
| } |
| } |
| else |
| { |
| if (new_rev + 1 == old_rev) |
| { |
| if (new_node_kind == svn_node_dir) |
| action = apr_psprintf(scratch_pool, |
| _("Changes destined for a directory " |
| "arrived during reverse-merge of\n" |
| "'^/%s:%ld'."), |
| new_repos_relpath, old_rev); |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| action = apr_psprintf(scratch_pool, |
| _("Changes destined for a file " |
| "arrived during reverse-merge of\n" |
| "'^/%s:%ld'."), |
| new_repos_relpath, old_rev); |
| else |
| action = apr_psprintf(scratch_pool, |
| _("Changes arrived during reverse-merge " |
| "of\n'^/%s:%ld'."), |
| new_repos_relpath, old_rev); |
| |
| *incoming_change_description = apr_pstrdup(result_pool, action); |
| |
| return SVN_NO_ERROR; |
| } |
| else |
| { |
| if (new_node_kind == svn_node_dir) |
| action = apr_psprintf(scratch_pool, |
| _("Changes destined for a directory " |
| "arrived via the following revisions " |
| "during reverse-merge of\n" |
| "'^/%s:%ld-%ld'"), |
| new_repos_relpath, new_rev + 1, old_rev); |
| else if (new_node_kind == svn_node_file || |
| new_node_kind == svn_node_symlink) |
| action = apr_psprintf(scratch_pool, |
| _("Changes destined for a file " |
| "arrived via the following revisions " |
| "during reverse-merge of\n" |
| "'^/%s:%ld-%ld'"), |
| new_repos_relpath, new_rev + 1, old_rev); |
| |
| else |
| action = apr_psprintf(scratch_pool, |
| _("Changes from the following revisions " |
| "arrived during reverse-merge of\n" |
| "'^/%s:%ld-%ld'"), |
| new_repos_relpath, new_rev + 1, old_rev); |
| } |
| } |
| } |
| |
| action = apr_psprintf(scratch_pool, "%s:\n%s", action, |
| describe_incoming_edit_list_modified_revs( |
| edits, scratch_pool)); |
| *incoming_change_description = apr_pstrdup(result_pool, action); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_tree_get_description( |
| const char **incoming_change_description, |
| const char **local_change_description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| SVN_ERR(conflict->tree_conflict_get_incoming_description_func( |
| incoming_change_description, |
| conflict, ctx, result_pool, scratch_pool)); |
| |
| SVN_ERR(conflict->tree_conflict_get_local_description_func( |
| local_change_description, |
| conflict, ctx, result_pool, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| void |
| svn_client_conflict_option_set_merged_propval( |
| svn_client_conflict_option_t *option, |
| const svn_string_t *merged_propval) |
| { |
| option->type_data.prop.merged_propval = svn_string_dup(merged_propval, |
| option->pool); |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_postpone(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| return SVN_NO_ERROR; /* Nothing to do. */ |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_text_conflict(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_wc_conflict_choice_t conflict_choice; |
| svn_error_t *err; |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| conflict_choice = conflict_option_id_to_wc_conflict_choice(option_id); |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| err = svn_wc__conflict_text_mark_resolved(ctx->wc_ctx, |
| local_abspath, |
| conflict_choice, |
| ctx->cancel_func, |
| ctx->cancel_baton, |
| ctx->notify_func2, |
| ctx->notify_baton2, |
| scratch_pool); |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| svn_io_sleep_for_timestamps(local_abspath, scratch_pool); |
| SVN_ERR(err); |
| |
| conflict->resolution_text = option_id; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_prop_conflict(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| svn_wc_conflict_choice_t conflict_choice; |
| const char *local_abspath; |
| const char *lock_abspath; |
| const char *propname = option->type_data.prop.propname; |
| svn_error_t *err; |
| const svn_string_t *merged_value; |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| conflict_choice = conflict_option_id_to_wc_conflict_choice(option_id); |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| if (option_id == svn_client_conflict_option_merged_text) |
| merged_value = option->type_data.prop.merged_propval; |
| else |
| merged_value = NULL; |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| err = svn_wc__conflict_prop_mark_resolved(ctx->wc_ctx, local_abspath, |
| propname, conflict_choice, |
| merged_value, |
| ctx->notify_func2, |
| ctx->notify_baton2, |
| scratch_pool); |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| svn_io_sleep_for_timestamps(local_abspath, scratch_pool); |
| SVN_ERR(err); |
| |
| if (propname[0] == '\0') |
| { |
| apr_hash_index_t *hi; |
| |
| /* All properties have been resolved to the same option. */ |
| for (hi = apr_hash_first(scratch_pool, conflict->prop_conflicts); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| const char *this_propname = apr_hash_this_key(hi); |
| |
| svn_hash_sets(conflict->resolved_props, |
| apr_pstrdup(apr_hash_pool_get(conflict->resolved_props), |
| this_propname), |
| option); |
| svn_hash_sets(conflict->prop_conflicts, this_propname, NULL); |
| } |
| |
| conflict->legacy_prop_conflict_propname = NULL; |
| } |
| else |
| { |
| svn_hash_sets(conflict->resolved_props, |
| apr_pstrdup(apr_hash_pool_get(conflict->resolved_props), |
| propname), |
| option); |
| svn_hash_sets(conflict->prop_conflicts, propname, NULL); |
| |
| if (apr_hash_count(conflict->prop_conflicts) > 0) |
| conflict->legacy_prop_conflict_propname = |
| apr_hash_this_key(apr_hash_first(scratch_pool, |
| conflict->prop_conflicts)); |
| else |
| conflict->legacy_prop_conflict_propname = NULL; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_accept_current_wc_state(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_error_t *err; |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| if (option_id != svn_client_conflict_option_accept_current_wc_state) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Tree conflict on '%s' can only be resolved " |
| "to the current working copy state"), |
| svn_dirent_local_style(local_abspath, |
| scratch_pool)); |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* Resolve to current working copy state. */ |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); |
| |
| /* svn_wc__del_tree_conflict doesn't handle notification for us */ |
| if (ctx->notify_func2) |
| ctx->notify_func2(ctx->notify_baton2, |
| svn_wc_create_notify(local_abspath, |
| svn_wc_notify_resolved_tree, |
| scratch_pool), |
| scratch_pool); |
| |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| conflict->resolution_tree = option_id; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_update_break_moved_away(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_error_t *err; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| err = svn_wc__conflict_tree_update_break_moved_away(ctx->wc_ctx, |
| local_abspath, |
| ctx->cancel_func, |
| ctx->cancel_baton, |
| ctx->notify_func2, |
| ctx->notify_baton2, |
| scratch_pool); |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_update_raise_moved_away(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_error_t *err; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| err = svn_wc__conflict_tree_update_raise_moved_away(ctx->wc_ctx, |
| local_abspath, |
| ctx->cancel_func, |
| ctx->cancel_baton, |
| ctx->notify_func2, |
| ctx->notify_baton2, |
| scratch_pool); |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_update_moved_away_node(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_error_t *err; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| err = svn_wc__conflict_tree_update_moved_away_node(ctx->wc_ctx, |
| local_abspath, |
| ctx->cancel_func, |
| ctx->cancel_baton, |
| ctx->notify_func2, |
| ctx->notify_baton2, |
| scratch_pool); |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| svn_io_sleep_for_timestamps(local_abspath, scratch_pool); |
| SVN_ERR(err); |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Verify the local working copy state matches what we expect when an |
| * incoming add vs add tree conflict exists after an update operation. |
| * We assume the update operation leaves the working copy in a state which |
| * prefers the local change and cancels the incoming addition. |
| * Run a quick sanity check and error out if it looks as if the |
| * working copy was modified since, even though it's not easy to make |
| * such modifications without also clearing the conflict marker. */ |
| static svn_error_t * |
| verify_local_state_for_incoming_add_upon_update( |
| svn_client_conflict_t *conflict, |
| svn_client_conflict_option_t *option, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *local_abspath; |
| svn_client_conflict_option_id_t option_id; |
| const char *wcroot_abspath; |
| svn_wc_operation_t operation; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| const char *base_repos_relpath; |
| svn_revnum_t base_rev; |
| svn_node_kind_t base_kind; |
| const char *local_style_relpath; |
| svn_boolean_t is_added; |
| svn_error_t *err; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| option_id = svn_client_conflict_option_get_id(option); |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| local_abspath, scratch_pool, |
| scratch_pool)); |
| operation = svn_client_conflict_get_operation(conflict); |
| SVN_ERR_ASSERT(operation == svn_wc_operation_update); |
| |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| local_style_relpath = svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, |
| local_abspath), |
| scratch_pool); |
| |
| /* Check if a local addition addition replaces the incoming new node. */ |
| err = svn_wc__node_get_base(&base_kind, &base_rev, &base_repos_relpath, |
| NULL, NULL, NULL, ctx->wc_ctx, local_abspath, |
| FALSE, scratch_pool, scratch_pool); |
| if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) |
| { |
| if (option_id == svn_client_conflict_option_incoming_add_ignore) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected a base node but found none)"), |
| local_style_relpath); |
| else if (option_id == |
| svn_client_conflict_option_incoming_added_dir_replace) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected a base node but found none)"), |
| local_style_relpath); |
| else |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, |
| _("Unexpected option id '%d'"), option_id); |
| } |
| else if (err) |
| return svn_error_trace(err); |
| |
| if (base_kind != incoming_new_kind) |
| { |
| if (option_id == svn_client_conflict_option_incoming_add_ignore) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected base node kind '%s', " |
| "but found '%s')"), |
| local_style_relpath, |
| svn_node_kind_to_word(incoming_new_kind), |
| svn_node_kind_to_word(base_kind)); |
| else if (option_id == |
| svn_client_conflict_option_incoming_added_dir_replace) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected base node kind '%s', " |
| "but found '%s')"), |
| local_style_relpath, |
| svn_node_kind_to_word(incoming_new_kind), |
| svn_node_kind_to_word(base_kind)); |
| else |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Unexpected option id '%d'"), option_id); |
| } |
| |
| if (strcmp(base_repos_relpath, incoming_new_repos_relpath) != 0 || |
| base_rev != incoming_new_pegrev) |
| { |
| if (option_id == svn_client_conflict_option_incoming_add_ignore) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected base node from '^/%s@%ld', " |
| "but found '^/%s@%ld')"), |
| local_style_relpath, |
| incoming_new_repos_relpath, |
| incoming_new_pegrev, |
| base_repos_relpath, base_rev); |
| else if (option_id == |
| svn_client_conflict_option_incoming_added_dir_replace) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected base node from '^/%s@%ld', " |
| "but found '^/%s@%ld')"), |
| local_style_relpath, |
| incoming_new_repos_relpath, |
| incoming_new_pegrev, |
| base_repos_relpath, base_rev); |
| else |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Unexpected option id '%d'"), option_id); |
| } |
| |
| SVN_ERR(svn_wc__node_is_added(&is_added, ctx->wc_ctx, local_abspath, |
| scratch_pool)); |
| if (!is_added) |
| { |
| if (option_id == svn_client_conflict_option_incoming_add_ignore) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected an added item, but the item " |
| "is not added)"), |
| local_style_relpath); |
| |
| else if (option_id == |
| svn_client_conflict_option_incoming_added_dir_replace) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected an added item, but the item " |
| "is not added)"), |
| local_style_relpath); |
| else |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Unexpected option id '%d'"), option_id); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_incoming_add_ignore(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_wc_operation_t operation; |
| svn_error_t *err; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| if (operation == svn_wc_operation_update) |
| { |
| err = verify_local_state_for_incoming_add_upon_update(conflict, option, |
| ctx, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| |
| /* All other options for this conflict actively fetch the incoming |
| * new node. We can ignore the incoming new node by doing nothing. */ |
| |
| /* Resolve to current working copy state. */ |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); |
| |
| /* svn_wc__del_tree_conflict doesn't handle notification for us */ |
| if (ctx->notify_func2) |
| ctx->notify_func2(ctx->notify_baton2, |
| svn_wc_create_notify(local_abspath, |
| svn_wc_notify_resolved_tree, |
| scratch_pool), |
| scratch_pool); |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Delete entry and wc props from a set of properties. */ |
| static void |
| filter_props(apr_hash_t *props, apr_pool_t *scratch_pool) |
| { |
| apr_hash_index_t *hi; |
| |
| for (hi = apr_hash_first(scratch_pool, props); |
| hi != NULL; |
| hi = apr_hash_next(hi)) |
| { |
| const char *propname = apr_hash_this_key(hi); |
| |
| if (!svn_wc_is_normal_prop(propname)) |
| svn_hash_sets(props, propname, NULL); |
| } |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_merge_incoming_added_file_text_update( |
| svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *wc_tmpdir; |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_wc_merge_outcome_t merge_content_outcome; |
| svn_wc_notify_state_t merge_props_outcome; |
| const char *empty_file_abspath; |
| const char *working_file_tmp_abspath; |
| svn_stream_t *working_file_stream; |
| svn_stream_t *working_file_tmp_stream; |
| apr_hash_t *working_props; |
| apr_array_header_t *propdiffs; |
| svn_error_t *err; |
| svn_wc_conflict_reason_t local_change; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| /* Set up temporary storage for the working version of file. */ |
| SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_stream_open_unique(&working_file_tmp_stream, |
| &working_file_tmp_abspath, wc_tmpdir, |
| /* Don't delete automatically! */ |
| svn_io_file_del_none, |
| scratch_pool, scratch_pool)); |
| |
| if (local_change == svn_wc_conflict_reason_unversioned) |
| { |
| /* Copy the unversioned file to temporary storage. */ |
| SVN_ERR(svn_stream_open_readonly(&working_file_stream, local_abspath, |
| scratch_pool, scratch_pool)); |
| /* Unversioned files have no properties. */ |
| working_props = apr_hash_make(scratch_pool); |
| } |
| else |
| { |
| /* Copy the detranslated working file to temporary storage. */ |
| SVN_ERR(svn_wc__translated_stream(&working_file_stream, ctx->wc_ctx, |
| local_abspath, local_abspath, |
| SVN_WC_TRANSLATE_TO_NF, |
| scratch_pool, scratch_pool)); |
| /* Get a copy of the working file's properties. */ |
| SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| filter_props(working_props, scratch_pool); |
| } |
| |
| SVN_ERR(svn_stream_copy3(working_file_stream, working_file_tmp_stream, |
| ctx->cancel_func, ctx->cancel_baton, |
| scratch_pool)); |
| |
| /* Create an empty file as fake "merge-base" for the two added files. |
| * The files are not ancestrally related so this is the best we can do. */ |
| SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file_abspath, NULL, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| |
| /* Create a property diff which shows all props as added. */ |
| SVN_ERR(svn_prop_diffs(&propdiffs, working_props, |
| apr_hash_make(scratch_pool), scratch_pool)); |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* Revert the path in order to restore the repository's line of |
| * history, which is part of the BASE tree. This revert operation |
| * is why are being careful about not losing the temporary copy. */ |
| err = svn_wc_revert6(ctx->wc_ctx, local_abspath, svn_depth_empty, |
| FALSE, NULL, TRUE, FALSE, |
| TRUE /*added_keep_local*/, |
| NULL, NULL, /* no cancellation */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Perform the file merge. ### Merge into tempfile and then rename on top? */ |
| err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, |
| ctx->wc_ctx, empty_file_abspath, |
| working_file_tmp_abspath, local_abspath, |
| NULL, NULL, NULL, /* labels */ |
| NULL, NULL, /* conflict versions */ |
| FALSE, /* dry run */ |
| NULL, NULL, /* diff3_cmd, merge_options */ |
| NULL, propdiffs, |
| NULL, NULL, /* conflict func/baton */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool); |
| |
| unlock_wc: |
| if (err) |
| err = svn_error_quick_wrapf( |
| err, _("If needed, a backup copy of '%s' can be found at '%s'"), |
| svn_dirent_local_style(local_abspath, scratch_pool), |
| svn_dirent_local_style(working_file_tmp_abspath, scratch_pool)); |
| err = svn_error_compose_create(err, |
| svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| svn_io_sleep_for_timestamps(local_abspath, scratch_pool); |
| SVN_ERR(err); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| /* Tell the world about the file merge that just happened. */ |
| notify = svn_wc_create_notify(local_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| if (merge_content_outcome == svn_wc_merge_conflict) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->prop_state = merge_props_outcome; |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| |
| /* And also about the successfully resolved tree conflict. */ |
| notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| /* All is good -- remove temporary copy of the working file. */ |
| SVN_ERR(svn_io_remove_file2(working_file_tmp_abspath, TRUE, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_merge_incoming_added_file_text_merge( |
| svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_ra_session_t *ra_session; |
| const char *url; |
| const char *corrected_url; |
| const char *repos_root_url; |
| const char *wc_tmpdir; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_wc_merge_outcome_t merge_content_outcome; |
| svn_wc_notify_state_t merge_props_outcome; |
| apr_file_t *incoming_new_file; |
| const char *incoming_new_tmp_abspath; |
| const char *empty_file_abspath; |
| svn_stream_t *incoming_new_stream; |
| apr_hash_t *incoming_new_props; |
| apr_array_header_t *propdiffs; |
| svn_error_t *err; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| /* Set up temporary storage for the repository version of file. */ |
| SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_io_open_unique_file3(&incoming_new_file, |
| &incoming_new_tmp_abspath, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| incoming_new_stream = svn_stream_from_aprfile2(incoming_new_file, TRUE, |
| scratch_pool); |
| |
| /* Fetch the incoming added file from the repository. */ |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| url, NULL, NULL, FALSE, FALSE, |
| ctx, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, |
| incoming_new_stream, NULL, /* fetched_rev */ |
| &incoming_new_props, scratch_pool)); |
| |
| /* Flush file to disk. */ |
| SVN_ERR(svn_stream_close(incoming_new_stream)); |
| SVN_ERR(svn_io_file_flush(incoming_new_file, scratch_pool)); |
| |
| filter_props(incoming_new_props, scratch_pool); |
| |
| /* Create an empty file as fake "merge-base" for the two added files. |
| * The files are not ancestrally related so this is the best we can do. */ |
| SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file_abspath, NULL, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| |
| /* Create a property diff which shows all props as added. */ |
| SVN_ERR(svn_prop_diffs(&propdiffs, incoming_new_props, |
| apr_hash_make(scratch_pool), scratch_pool)); |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| /* Resolve to current working copy state. svn_wc_merge5() requires this. */ |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); |
| if (err) |
| return svn_error_compose_create(err, |
| svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| /* Perform the file merge. ### Merge into tempfile and then rename on top? */ |
| err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, |
| ctx->wc_ctx, empty_file_abspath, |
| incoming_new_tmp_abspath, local_abspath, |
| NULL, NULL, NULL, /* labels */ |
| NULL, NULL, /* conflict versions */ |
| FALSE, /* dry run */ |
| NULL, NULL, /* diff3_cmd, merge_options */ |
| NULL, propdiffs, |
| NULL, NULL, /* conflict func/baton */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool); |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| svn_io_sleep_for_timestamps(local_abspath, scratch_pool); |
| SVN_ERR(err); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| /* Tell the world about the file merge that just happened. */ |
| notify = svn_wc_create_notify(local_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| if (merge_content_outcome == svn_wc_merge_conflict) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->prop_state = merge_props_outcome; |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| |
| /* And also about the successfully resolved tree conflict. */ |
| notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_merge_incoming_added_file_replace_and_merge( |
| svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_ra_session_t *ra_session; |
| const char *url; |
| const char *corrected_url; |
| const char *repos_root_url; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| apr_file_t *incoming_new_file; |
| svn_stream_t *incoming_new_stream; |
| apr_hash_t *incoming_new_props; |
| const char *local_abspath; |
| const char *lock_abspath; |
| const char *wc_tmpdir; |
| svn_stream_t *working_file_tmp_stream; |
| const char *working_file_tmp_abspath; |
| svn_stream_t *working_file_stream; |
| apr_hash_t *working_props; |
| svn_error_t *err; |
| svn_wc_merge_outcome_t merge_content_outcome; |
| svn_wc_notify_state_t merge_props_outcome; |
| apr_file_t *empty_file; |
| const char *empty_file_abspath; |
| apr_array_header_t *propdiffs; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| /* Set up temporary storage for the working version of file. */ |
| SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_stream_open_unique(&working_file_tmp_stream, |
| &working_file_tmp_abspath, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| |
| /* Copy the detranslated working file to temporary storage. */ |
| SVN_ERR(svn_wc__translated_stream(&working_file_stream, ctx->wc_ctx, |
| local_abspath, local_abspath, |
| SVN_WC_TRANSLATE_TO_NF, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_stream_copy3(working_file_stream, working_file_tmp_stream, |
| ctx->cancel_func, ctx->cancel_baton, |
| scratch_pool)); |
| |
| /* Get a copy of the working file's properties. */ |
| SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* Fetch the incoming added file from the repository. */ |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| url, NULL, NULL, FALSE, FALSE, |
| ctx, scratch_pool, |
| scratch_pool)); |
| if (corrected_url) |
| url = corrected_url; |
| SVN_ERR(svn_io_open_unique_file3(&incoming_new_file, NULL, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| incoming_new_stream = svn_stream_from_aprfile2(incoming_new_file, TRUE, |
| scratch_pool); |
| SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, |
| incoming_new_stream, NULL, /* fetched_rev */ |
| &incoming_new_props, scratch_pool)); |
| /* Flush file to disk. */ |
| SVN_ERR(svn_io_file_flush(incoming_new_file, scratch_pool)); |
| |
| /* Reset the stream in preparation for adding its content to WC. */ |
| SVN_ERR(svn_stream_reset(incoming_new_stream)); |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* ### The following WC modifications should be atomic. */ |
| |
| /* Replace the working file with the file from the repository. */ |
| err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, |
| NULL, NULL, /* don't allow user to cancel here */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| err = svn_wc_add_repos_file4(ctx->wc_ctx, local_abspath, |
| incoming_new_stream, |
| NULL, /* ### could we merge first, then set |
| ### the merged content here? */ |
| incoming_new_props, |
| NULL, /* ### merge props first, set here? */ |
| url, incoming_new_pegrev, |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath, |
| svn_wc_notify_add, |
| scratch_pool); |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| /* Resolve to current working copy state. svn_wc_merge5() requires this. */ |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Create an empty file as fake "merge-base" for the two added files. |
| * The files are not ancestrally related so this is the best we can do. */ |
| err = svn_io_open_unique_file3(&empty_file, &empty_file_abspath, NULL, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| filter_props(incoming_new_props, scratch_pool); |
| |
| /* Create a property diff for the files. */ |
| err = svn_prop_diffs(&propdiffs, incoming_new_props, |
| working_props, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Perform the file merge. */ |
| err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, |
| ctx->wc_ctx, empty_file_abspath, |
| working_file_tmp_abspath, local_abspath, |
| NULL, NULL, NULL, /* labels */ |
| NULL, NULL, /* conflict versions */ |
| FALSE, /* dry run */ |
| NULL, NULL, /* diff3_cmd, merge_options */ |
| NULL, propdiffs, |
| NULL, NULL, /* conflict func/baton */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify = svn_wc_create_notify( |
| local_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| |
| if (merge_content_outcome == svn_wc_merge_conflict) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->prop_state = merge_props_outcome; |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| svn_io_sleep_for_timestamps(local_abspath, scratch_pool); |
| SVN_ERR(err); |
| |
| SVN_ERR(svn_stream_close(incoming_new_stream)); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify = svn_wc_create_notify( |
| local_abspath, |
| svn_wc_notify_resolved_tree, |
| scratch_pool); |
| |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| raise_tree_conflict(const char *local_abspath, |
| svn_wc_conflict_action_t incoming_change, |
| svn_wc_conflict_reason_t local_change, |
| svn_node_kind_t local_node_kind, |
| svn_node_kind_t merge_left_kind, |
| svn_node_kind_t merge_right_kind, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| const char *repos_relpath, |
| svn_revnum_t merge_left_rev, |
| svn_revnum_t merge_right_rev, |
| svn_wc_context_t *wc_ctx, |
| svn_wc_notify_func2_t notify_func2, |
| void *notify_baton2, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_conflict_description2_t *conflict; |
| const svn_wc_conflict_version_t *left_version; |
| const svn_wc_conflict_version_t *right_version; |
| |
| left_version = svn_wc_conflict_version_create2(repos_root_url, |
| repos_uuid, |
| repos_relpath, |
| merge_left_rev, |
| merge_left_kind, |
| scratch_pool); |
| right_version = svn_wc_conflict_version_create2(repos_root_url, |
| repos_uuid, |
| repos_relpath, |
| merge_right_rev, |
| merge_right_kind, |
| scratch_pool); |
| conflict = svn_wc_conflict_description_create_tree2(local_abspath, |
| local_node_kind, |
| svn_wc_operation_merge, |
| left_version, |
| right_version, |
| scratch_pool); |
| conflict->action = incoming_change; |
| conflict->reason = local_change; |
| |
| SVN_ERR(svn_wc__add_tree_conflict(wc_ctx, conflict, scratch_pool)); |
| |
| if (notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(local_abspath, svn_wc_notify_tree_conflict, |
| scratch_pool); |
| notify->kind = local_node_kind; |
| notify_func2(notify_baton2, notify, scratch_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| struct merge_newly_added_dir_baton { |
| const char *target_abspath; |
| svn_client_ctx_t *ctx; |
| const char *repos_root_url; |
| const char *repos_uuid; |
| const char *added_repos_relpath; |
| svn_revnum_t merge_left_rev; |
| svn_revnum_t merge_right_rev; |
| }; |
| |
| static svn_error_t * |
| merge_added_dir_props(const char *target_abspath, |
| const char *added_repos_relpath, |
| apr_hash_t *added_props, |
| const char *repos_root_url, |
| const char *repos_uuid, |
| svn_revnum_t merge_left_rev, |
| svn_revnum_t merge_right_rev, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_notify_state_t property_state; |
| apr_array_header_t *propchanges; |
| const svn_wc_conflict_version_t *left_version; |
| const svn_wc_conflict_version_t *right_version; |
| apr_hash_index_t *hi; |
| |
| left_version = svn_wc_conflict_version_create2( |
| repos_root_url, repos_uuid, added_repos_relpath, |
| merge_left_rev, svn_node_none, scratch_pool); |
| |
| right_version = svn_wc_conflict_version_create2( |
| repos_root_url, repos_uuid, added_repos_relpath, |
| merge_right_rev, svn_node_dir, scratch_pool); |
| |
| propchanges = apr_array_make(scratch_pool, apr_hash_count(added_props), |
| sizeof(svn_prop_t)); |
| for (hi = apr_hash_first(scratch_pool, added_props); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| svn_prop_t prop; |
| |
| prop.name = apr_hash_this_key(hi); |
| prop.value = apr_hash_this_val(hi); |
| |
| if (svn_wc_is_normal_prop(prop.name)) |
| APR_ARRAY_PUSH(propchanges, svn_prop_t) = prop; |
| } |
| |
| SVN_ERR(svn_wc_merge_props3(&property_state, ctx->wc_ctx, |
| target_abspath, |
| left_version, right_version, |
| apr_hash_make(scratch_pool), |
| propchanges, |
| FALSE, /* not a dry-run */ |
| NULL, NULL, NULL, NULL, |
| scratch_pool)); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(target_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| notify->kind = svn_node_dir; |
| notify->content_state = svn_wc_notify_state_unchanged;; |
| notify->prop_state = property_state; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* An svn_diff_tree_processor_t callback. */ |
| static svn_error_t * |
| diff_dir_added(const char *relpath, |
| const svn_diff_source_t *copyfrom_source, |
| const svn_diff_source_t *right_source, |
| apr_hash_t *copyfrom_props, |
| apr_hash_t *right_props, |
| void *dir_baton, |
| const struct svn_diff_tree_processor_t *processor, |
| apr_pool_t *scratch_pool) |
| { |
| struct merge_newly_added_dir_baton *b = processor->baton; |
| const char *local_abspath; |
| const char *copyfrom_url; |
| svn_node_kind_t db_kind; |
| svn_node_kind_t on_disk_kind; |
| apr_hash_index_t *hi; |
| |
| /* Handle the root of the added directory tree. */ |
| if (relpath[0] == '\0') |
| { |
| /* ### svn_wc_merge_props3() requires this... */ |
| SVN_ERR(svn_wc__del_tree_conflict(b->ctx->wc_ctx, b->target_abspath, |
| scratch_pool)); |
| SVN_ERR(merge_added_dir_props(b->target_abspath, |
| b->added_repos_relpath, right_props, |
| b->repos_root_url, b->repos_uuid, |
| b->merge_left_rev, b->merge_right_rev, |
| b->ctx, scratch_pool)); |
| return SVN_NO_ERROR; |
| |
| } |
| |
| local_abspath = svn_dirent_join(b->target_abspath, relpath, scratch_pool); |
| |
| SVN_ERR(svn_wc_read_kind2(&db_kind, b->ctx->wc_ctx, local_abspath, |
| FALSE, FALSE, scratch_pool)); |
| SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool)); |
| |
| if (db_kind == svn_node_dir && on_disk_kind == svn_node_dir) |
| { |
| SVN_ERR(merge_added_dir_props(svn_dirent_join(b->target_abspath, relpath, |
| scratch_pool), |
| b->added_repos_relpath, right_props, |
| b->repos_root_url, b->repos_uuid, |
| b->merge_left_rev, b->merge_right_rev, |
| b->ctx, scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| if (db_kind != svn_node_none && db_kind != svn_node_unknown) |
| { |
| SVN_ERR(raise_tree_conflict( |
| local_abspath, svn_wc_conflict_action_add, |
| svn_wc_conflict_reason_obstructed, |
| db_kind, svn_node_none, svn_node_dir, |
| b->repos_root_url, b->repos_uuid, |
| svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), |
| b->merge_left_rev, b->merge_right_rev, |
| b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, |
| scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| if (on_disk_kind != svn_node_none) |
| { |
| SVN_ERR(raise_tree_conflict( |
| local_abspath, svn_wc_conflict_action_add, |
| svn_wc_conflict_reason_obstructed, db_kind, |
| svn_node_none, svn_node_dir, b->repos_root_url, b->repos_uuid, |
| svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), |
| b->merge_left_rev, b->merge_right_rev, |
| b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, |
| scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| SVN_ERR(svn_io_dir_make(local_abspath, APR_OS_DEFAULT, scratch_pool)); |
| copyfrom_url = apr_pstrcat(scratch_pool, b->repos_root_url, "/", |
| right_source->repos_relpath, SVN_VA_NULL); |
| SVN_ERR(svn_wc_add4(b->ctx->wc_ctx, local_abspath, svn_depth_infinity, |
| copyfrom_url, right_source->revision, |
| NULL, NULL, /* cancel func/baton */ |
| b->ctx->notify_func2, b->ctx->notify_baton2, |
| scratch_pool)); |
| |
| for (hi = apr_hash_first(scratch_pool, right_props); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| const char *propname = apr_hash_this_key(hi); |
| const svn_string_t *propval = apr_hash_this_val(hi); |
| |
| SVN_ERR(svn_wc_prop_set4(b->ctx->wc_ctx, local_abspath, |
| propname, propval, svn_depth_empty, |
| FALSE, NULL /* do not skip checks */, |
| NULL, NULL, /* cancel func/baton */ |
| b->ctx->notify_func2, b->ctx->notify_baton2, |
| scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| merge_added_files(const char *local_abspath, |
| const char *incoming_added_file_abspath, |
| apr_hash_t *incoming_added_file_props, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_merge_outcome_t merge_content_outcome; |
| svn_wc_notify_state_t merge_props_outcome; |
| apr_file_t *empty_file; |
| const char *empty_file_abspath; |
| apr_array_header_t *propdiffs; |
| apr_hash_t *working_props; |
| |
| /* Create an empty file as fake "merge-base" for the two added files. |
| * The files are not ancestrally related so this is the best we can do. */ |
| SVN_ERR(svn_io_open_unique_file3(&empty_file, &empty_file_abspath, NULL, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| |
| /* Get a copy of the working file's properties. */ |
| SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* Create a property diff for the files. */ |
| SVN_ERR(svn_prop_diffs(&propdiffs, incoming_added_file_props, |
| working_props, scratch_pool)); |
| |
| /* Perform the file merge. */ |
| SVN_ERR(svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, |
| ctx->wc_ctx, empty_file_abspath, |
| incoming_added_file_abspath, local_abspath, |
| NULL, NULL, NULL, /* labels */ |
| NULL, NULL, /* conflict versions */ |
| FALSE, /* dry run */ |
| NULL, NULL, /* diff3_cmd, merge_options */ |
| NULL, propdiffs, |
| NULL, NULL, /* conflict func/baton */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool)); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify = svn_wc_create_notify( |
| local_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| |
| if (merge_content_outcome == svn_wc_merge_conflict) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->prop_state = merge_props_outcome; |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* An svn_diff_tree_processor_t callback. */ |
| static svn_error_t * |
| diff_file_added(const char *relpath, |
| const svn_diff_source_t *copyfrom_source, |
| const svn_diff_source_t *right_source, |
| const char *copyfrom_file, |
| const char *right_file, |
| apr_hash_t *copyfrom_props, |
| apr_hash_t *right_props, |
| void *file_baton, |
| const struct svn_diff_tree_processor_t *processor, |
| apr_pool_t *scratch_pool) |
| { |
| struct merge_newly_added_dir_baton *b = processor->baton; |
| const char *local_abspath; |
| svn_node_kind_t db_kind; |
| svn_node_kind_t on_disk_kind; |
| apr_array_header_t *propsarray; |
| apr_array_header_t *regular_props; |
| |
| local_abspath = svn_dirent_join(b->target_abspath, relpath, scratch_pool); |
| |
| SVN_ERR(svn_wc_read_kind2(&db_kind, b->ctx->wc_ctx, local_abspath, |
| FALSE, FALSE, scratch_pool)); |
| SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool)); |
| |
| if (db_kind == svn_node_file && on_disk_kind == svn_node_file) |
| { |
| propsarray = svn_prop_hash_to_array(right_props, scratch_pool); |
| SVN_ERR(svn_categorize_props(propsarray, NULL, NULL, ®ular_props, |
| scratch_pool)); |
| SVN_ERR(merge_added_files(local_abspath, right_file, |
| svn_prop_array_to_hash(regular_props, |
| scratch_pool), |
| b->ctx, scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| if (db_kind != svn_node_none && db_kind != svn_node_unknown) |
| { |
| SVN_ERR(raise_tree_conflict( |
| local_abspath, svn_wc_conflict_action_add, |
| svn_wc_conflict_reason_obstructed, |
| db_kind, svn_node_none, svn_node_file, |
| b->repos_root_url, b->repos_uuid, |
| svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), |
| b->merge_left_rev, b->merge_right_rev, |
| b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, |
| scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| if (on_disk_kind != svn_node_none) |
| { |
| SVN_ERR(raise_tree_conflict( |
| local_abspath, svn_wc_conflict_action_add, |
| svn_wc_conflict_reason_obstructed, db_kind, |
| svn_node_none, svn_node_file, b->repos_root_url, b->repos_uuid, |
| svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), |
| b->merge_left_rev, b->merge_right_rev, |
| b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, |
| scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| propsarray = svn_prop_hash_to_array(right_props, scratch_pool); |
| SVN_ERR(svn_categorize_props(propsarray, NULL, NULL, ®ular_props, |
| scratch_pool)); |
| SVN_ERR(svn_io_copy_file(right_file, local_abspath, FALSE, scratch_pool)); |
| SVN_ERR(svn_wc_add_from_disk3(b->ctx->wc_ctx, local_abspath, |
| svn_prop_array_to_hash(regular_props, |
| scratch_pool), |
| FALSE, b->ctx->notify_func2, |
| b->ctx->notify_baton2, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Merge a newly added directory into TARGET_ABSPATH in the working copy. |
| * |
| * This uses a diff-tree processor because our standard merge operation |
| * is not set up for merges where the merge-source anchor is itself an |
| * added directory (i.e. does not exist on one side of the diff). |
| * The standard merge will only merge additions of children of a path |
| * that exists across the entire revision range being merged. |
| * But in our case, SOURCE1 does not yet exist in REV1, but SOURCE2 |
| * does exist in REV2. Thus we use a diff processor. |
| */ |
| static svn_error_t * |
| merge_newly_added_dir(const char *added_repos_relpath, |
| const char *source1, |
| svn_revnum_t rev1, |
| const char *source2, |
| svn_revnum_t rev2, |
| const char *target_abspath, |
| svn_boolean_t reverse_merge, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_diff_tree_processor_t *processor; |
| struct merge_newly_added_dir_baton baton = { 0 }; |
| const svn_diff_tree_processor_t *diff_processor; |
| svn_ra_session_t *ra_session; |
| const char *corrected_url; |
| svn_ra_session_t *extra_ra_session; |
| const svn_ra_reporter3_t *reporter; |
| void *reporter_baton; |
| const svn_delta_editor_t *diff_editor; |
| void *diff_edit_baton; |
| const char *anchor1; |
| const char *anchor2; |
| const char *target1; |
| const char *target2; |
| |
| svn_uri_split(&anchor1, &target1, source1, scratch_pool); |
| svn_uri_split(&anchor2, &target2, source2, scratch_pool); |
| |
| baton.target_abspath = target_abspath; |
| baton.ctx = ctx; |
| baton.added_repos_relpath = added_repos_relpath; |
| SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL, |
| &baton.repos_root_url, &baton.repos_uuid, |
| ctx->wc_ctx, target_abspath, |
| scratch_pool, scratch_pool)); |
| baton.merge_left_rev = rev1; |
| baton.merge_right_rev = rev2; |
| |
| processor = svn_diff__tree_processor_create(&baton, scratch_pool); |
| processor->dir_added = diff_dir_added; |
| processor->file_added = diff_file_added; |
| |
| diff_processor = processor; |
| if (reverse_merge) |
| diff_processor = svn_diff__tree_processor_reverse_create(diff_processor, |
| scratch_pool); |
| |
| /* Filter the first path component using a filter processor, until we fixed |
| the diff processing to handle this directly */ |
| diff_processor = svn_diff__tree_processor_filter_create( |
| diff_processor, target1, scratch_pool); |
| |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| anchor2, NULL, NULL, FALSE, |
| FALSE, ctx, |
| scratch_pool, scratch_pool)); |
| if (corrected_url) |
| anchor2 = corrected_url; |
| |
| /* Extra RA session is used during the editor calls to fetch file contents. */ |
| SVN_ERR(svn_ra__dup_session(&extra_ra_session, ra_session, anchor2, |
| scratch_pool, scratch_pool)); |
| |
| /* Create a repos-repos diff editor. */ |
| SVN_ERR(svn_client__get_diff_editor2( |
| &diff_editor, &diff_edit_baton, |
| extra_ra_session, svn_depth_infinity, rev1, TRUE, |
| diff_processor, ctx->cancel_func, ctx->cancel_baton, |
| scratch_pool)); |
| |
| /* We want to switch our txn into URL2 */ |
| SVN_ERR(svn_ra_do_diff3(ra_session, &reporter, &reporter_baton, |
| rev2, target1, svn_depth_infinity, TRUE, TRUE, |
| source2, diff_editor, diff_edit_baton, scratch_pool)); |
| |
| /* Drive the reporter; do the diff. */ |
| SVN_ERR(reporter->set_path(reporter_baton, "", rev1, |
| svn_depth_infinity, |
| FALSE, NULL, |
| scratch_pool)); |
| |
| SVN_ERR(reporter->finish_report(reporter_baton, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_merge_incoming_added_dir_merge(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *repos_root_url; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *local_abspath; |
| const char *lock_abspath; |
| struct conflict_tree_incoming_add_details *details; |
| const char *added_repos_relpath; |
| const char *source1; |
| svn_revnum_t rev1; |
| const char *source2; |
| svn_revnum_t rev2; |
| svn_error_t *err; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Conflict resolution option '%d' requires " |
| "details for tree conflict at '%s' to be " |
| "fetched from the repository"), |
| option->id, |
| svn_dirent_local_style(local_abspath, |
| scratch_pool)); |
| |
| /* Set up merge sources to merge the entire incoming added directory tree. */ |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| source1 = svn_path_url_add_component2(repos_root_url, |
| details->repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, scratch_pool)); |
| if (incoming_old_pegrev < incoming_new_pegrev) /* forward merge */ |
| { |
| if (details->added_rev == SVN_INVALID_REVNUM) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Could not determine when '%s' was " |
| "added the repository"), |
| svn_dirent_local_style(local_abspath, |
| scratch_pool)); |
| rev1 = rev_below(details->added_rev); |
| source2 = svn_path_url_add_component2(repos_root_url, |
| incoming_new_repos_relpath, |
| scratch_pool); |
| rev2 = incoming_new_pegrev; |
| added_repos_relpath = incoming_new_repos_relpath; |
| } |
| else /* reverse-merge */ |
| { |
| if (details->deleted_rev == SVN_INVALID_REVNUM) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Could not determine when '%s' was " |
| "deleted from the repository"), |
| svn_dirent_local_style(local_abspath, |
| scratch_pool)); |
| rev1 = details->deleted_rev; |
| source2 = svn_path_url_add_component2(repos_root_url, |
| incoming_old_repos_relpath, |
| scratch_pool); |
| rev2 = incoming_old_pegrev; |
| added_repos_relpath = incoming_new_repos_relpath; |
| } |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* ### wrap in a transaction */ |
| err = merge_newly_added_dir(added_repos_relpath, |
| source1, rev1, source2, rev2, |
| local_abspath, |
| (incoming_old_pegrev > incoming_new_pegrev), |
| ctx, scratch_pool, scratch_pool); |
| if (!err) |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); |
| |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| svn_io_sleep_for_timestamps(local_abspath, scratch_pool); |
| SVN_ERR(err); |
| |
| if (ctx->notify_func2) |
| ctx->notify_func2(ctx->notify_baton2, |
| svn_wc_create_notify(local_abspath, |
| svn_wc_notify_resolved_tree, |
| scratch_pool), |
| scratch_pool); |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_update_incoming_added_dir_merge(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_error_t *err; |
| svn_wc_conflict_reason_t local_change; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| if (local_change == svn_wc_conflict_reason_unversioned) |
| { |
| char *parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool); |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, parent_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* The update/switch operation has added the incoming versioned |
| * directory as a deleted op-depth layer. We can revert this layer |
| * to make the incoming tree appear in the working copy. |
| * This meta-data-only revert operation effecively merges the |
| * versioned and unversioned trees but leaves all unversioned files as |
| * they were. This is the best we can do; 3-way merging of unversioned |
| * files with files from the repository is impossible because there is |
| * no known merge base. No unversioned data will be lost, and any |
| * differences to files in the repository will show up in 'svn diff'. */ |
| err = svn_wc_revert6(ctx->wc_ctx, local_abspath, svn_depth_infinity, |
| FALSE, NULL, TRUE, TRUE /* metadata_only */, |
| TRUE /*added_keep_local*/, |
| NULL, NULL, /* no cancellation */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| } |
| else |
| { |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| err = svn_wc__conflict_tree_update_local_add(ctx->wc_ctx, |
| local_abspath, |
| ctx->cancel_func, |
| ctx->cancel_baton, |
| ctx->notify_func2, |
| ctx->notify_baton2, |
| scratch_pool); |
| } |
| |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Resolve a dir/dir "incoming add vs local obstruction" tree conflict by |
| * replacing the local directory with the incoming directory. |
| * If MERGE_DIRS is set, also merge the directories after replacing. */ |
| static svn_error_t * |
| merge_incoming_added_dir_replace(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| svn_boolean_t merge_dirs, |
| apr_pool_t *scratch_pool) |
| { |
| svn_ra_session_t *ra_session; |
| const char *url; |
| const char *corrected_url; |
| const char *repos_root_url; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_error_t *err; |
| svn_boolean_t timestamp_sleep; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| /* Find the URL of the incoming added directory in the repository. */ |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| url, NULL, NULL, FALSE, FALSE, |
| ctx, scratch_pool, |
| scratch_pool)); |
| if (corrected_url) |
| url = corrected_url; |
| |
| /* ### The following WC modifications should be atomic. */ |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| svn_dirent_dirname( |
| local_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| /* Remove the working directory. */ |
| err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, |
| NULL, NULL, /* don't allow user to cancel here */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_client__repos_to_wc_copy_by_editor(×tamp_sleep, |
| svn_node_dir, |
| url, incoming_new_pegrev, |
| local_abspath, |
| ra_session, ctx, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath, |
| svn_wc_notify_add, |
| scratch_pool); |
| notify->kind = svn_node_dir; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| /* Resolve to current working copy state. |
| * svn_client__merge_locked() requires this. */ |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (merge_dirs) |
| { |
| svn_revnum_t base_revision; |
| const char *base_repos_relpath; |
| struct find_added_rev_baton b = { 0 }; |
| |
| /* Find the URL and revision of the directory we have just replaced. */ |
| err = svn_wc__node_get_base(NULL, &base_revision, &base_repos_relpath, |
| NULL, NULL, NULL, ctx->wc_ctx, local_abspath, |
| FALSE, scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| url = svn_path_url_add_component2(repos_root_url, base_repos_relpath, |
| scratch_pool); |
| |
| /* Trace the replaced directory's history to its origin. */ |
| err = svn_ra_reparent(ra_session, url, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| b.victim_abspath = local_abspath; |
| b.ctx = ctx; |
| b.added_rev = SVN_INVALID_REVNUM; |
| b.repos_relpath = NULL; |
| b.parent_repos_relpath = svn_relpath_dirname(base_repos_relpath, |
| scratch_pool); |
| b.pool = scratch_pool; |
| |
| err = svn_ra_get_location_segments(ra_session, "", base_revision, |
| base_revision, SVN_INVALID_REVNUM, |
| find_added_rev, &b, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (b.added_rev == SVN_INVALID_REVNUM) |
| { |
| err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Could not determine the revision in " |
| "which '^/%s' was added to the " |
| "repository.\n"), |
| base_repos_relpath); |
| goto unlock_wc; |
| } |
| |
| /* Merge the replaced directory into the directory which replaced it. |
| * We do not need to consider a reverse-merge here since the source of |
| * this merge was part of the merge target working copy, not a branch |
| * in the repository. */ |
| err = merge_newly_added_dir(base_repos_relpath, |
| url, rev_below(b.added_rev), url, |
| base_revision, local_abspath, FALSE, |
| ctx, scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| svn_io_sleep_for_timestamps(local_abspath, scratch_pool); |
| SVN_ERR(err); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify = svn_wc_create_notify( |
| local_abspath, |
| svn_wc_notify_resolved_tree, |
| scratch_pool); |
| |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_merge_incoming_added_dir_replace(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| return svn_error_trace(merge_incoming_added_dir_replace(option, |
| conflict, |
| ctx, |
| FALSE, |
| scratch_pool)); |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_merge_incoming_added_dir_replace_and_merge( |
| svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| return svn_error_trace(merge_incoming_added_dir_replace(option, |
| conflict, |
| ctx, |
| TRUE, |
| scratch_pool)); |
| } |
| |
| /* Ensure the conflict victim is a copy of itself from before it was deleted. |
| * Update and switch are supposed to set this up when flagging the conflict. */ |
| static svn_error_t * |
| ensure_local_edit_vs_incoming_deletion_copied_state( |
| struct conflict_tree_incoming_delete_details *details, |
| svn_wc_operation_t operation, |
| const char *wcroot_abspath, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| |
| svn_boolean_t is_copy; |
| svn_revnum_t copyfrom_rev; |
| const char *copyfrom_repos_relpath; |
| |
| SVN_ERR_ASSERT(operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch); |
| |
| SVN_ERR(svn_wc__node_get_origin(&is_copy, ©from_rev, |
| ©from_repos_relpath, |
| NULL, NULL, NULL, NULL, |
| ctx->wc_ctx, conflict->local_abspath, |
| FALSE, scratch_pool, scratch_pool)); |
| if (!is_copy) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected a copied item, but the item " |
| "is not a copy)"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool)); |
| else if (details->deleted_rev != SVN_INVALID_REVNUM && |
| copyfrom_rev >= details->deleted_rev) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected an item copied from a revision " |
| "smaller than r%ld, but the item was " |
| "copied from r%ld)"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, conflict->local_abspath), |
| scratch_pool), |
| details->deleted_rev, copyfrom_rev); |
| else if (details->added_rev != SVN_INVALID_REVNUM && |
| copyfrom_rev < details->added_rev) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected an item copied from a revision " |
| "larger than r%ld, but the item was " |
| "copied from r%ld)"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, conflict->local_abspath), |
| scratch_pool), |
| details->added_rev, copyfrom_rev); |
| else if (operation == svn_wc_operation_update) |
| { |
| const char *old_repos_relpath; |
| |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &old_repos_relpath, NULL, NULL, conflict, |
| scratch_pool, scratch_pool)); |
| if (strcmp(copyfrom_repos_relpath, details->repos_relpath) != 0 && |
| strcmp(copyfrom_repos_relpath, old_repos_relpath) != 0) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected an item copied from '^/%s' " |
| "or from '^/%s' but the item was " |
| "copied from '^/%s@%ld')"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, conflict->local_abspath), |
| scratch_pool), |
| details->repos_relpath, |
| old_repos_relpath, |
| copyfrom_repos_relpath, copyfrom_rev); |
| } |
| else if (operation == svn_wc_operation_switch) |
| { |
| const char *old_repos_relpath; |
| |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &old_repos_relpath, NULL, NULL, conflict, |
| scratch_pool, scratch_pool)); |
| |
| if (strcmp(copyfrom_repos_relpath, old_repos_relpath) != 0) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected an item copied from '^/%s', " |
| "but the item was copied from " |
| "'^/%s@%ld')"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool), |
| old_repos_relpath, |
| copyfrom_repos_relpath, copyfrom_rev); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Verify the local working copy state matches what we expect when an |
| * incoming deletion tree conflict exists. |
| * We assume update/merge/switch operations leave the working copy in a |
| * state which prefers the local change and cancels the deletion. |
| * Run a quick sanity check and error out if it looks as if the |
| * working copy was modified since, even though it's not easy to make |
| * such modifications without also clearing the conflict marker. */ |
| static svn_error_t * |
| verify_local_state_for_incoming_delete(svn_client_conflict_t *conflict, |
| svn_client_conflict_option_t *option, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *local_abspath; |
| const char *wcroot_abspath; |
| svn_wc_operation_t operation; |
| svn_wc_conflict_reason_t local_change; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| local_abspath, scratch_pool, |
| scratch_pool)); |
| operation = svn_client_conflict_get_operation(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| struct conflict_tree_incoming_delete_details *details; |
| |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Conflict resolution option '%d' requires " |
| "details for tree conflict at '%s' to be " |
| "fetched from the repository."), |
| option->id, |
| svn_dirent_local_style(local_abspath, |
| scratch_pool)); |
| |
| if (details->deleted_rev == SVN_INVALID_REVNUM && |
| details->added_rev == SVN_INVALID_REVNUM) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Could not find the revision in which '%s' " |
| "was deleted from the repository"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool)); |
| |
| if (local_change == svn_wc_conflict_reason_edited) |
| SVN_ERR(ensure_local_edit_vs_incoming_deletion_copied_state( |
| details, operation, wcroot_abspath, conflict, ctx, |
| scratch_pool)); |
| } |
| else if (operation == svn_wc_operation_merge) |
| { |
| svn_node_kind_t victim_node_kind; |
| svn_node_kind_t on_disk_kind; |
| |
| /* For merge, all we can do is ensure that the item still exists. */ |
| victim_node_kind = |
| svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool)); |
| |
| if (victim_node_kind != on_disk_kind) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected node kind '%s' but found '%s')"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, conflict->local_abspath), |
| scratch_pool), |
| svn_node_kind_to_word(victim_node_kind), |
| svn_node_kind_to_word(on_disk_kind)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_incoming_delete_ignore(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *local_abspath; |
| const char *lock_abspath; |
| svn_error_t *err; |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| err = verify_local_state_for_incoming_delete(conflict, option, ctx, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Resolve to the current working copy state. */ |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); |
| |
| /* svn_wc__del_tree_conflict doesn't handle notification for us */ |
| if (ctx->notify_func2) |
| ctx->notify_func2(ctx->notify_baton2, |
| svn_wc_create_notify(local_abspath, |
| svn_wc_notify_resolved_tree, |
| scratch_pool), |
| scratch_pool); |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| conflict->resolution_tree = option_id; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_incoming_delete_accept(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *local_abspath; |
| const char *parent_abspath; |
| const char *lock_abspath; |
| svn_error_t *err; |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| |
| /* Deleting a node requires a lock on the node's parent. */ |
| parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool); |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, |
| parent_abspath, |
| scratch_pool, scratch_pool)); |
| |
| err = verify_local_state_for_incoming_delete(conflict, option, ctx, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Delete the tree conflict victim. Marks the conflict resolved. */ |
| err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, |
| NULL, NULL, /* don't allow user to cancel here */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) |
| { |
| /* Not a versioned path. This can happen if the victim has already |
| * been deleted in our branche's history, for example. Either way, |
| * the item is gone, which is what we want, so don't treat this as |
| * a fatal error. */ |
| svn_error_clear(err); |
| |
| /* Resolve to current working copy state. */ |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, |
| scratch_pool); |
| } |
| |
| if (err) |
| goto unlock_wc; |
| } |
| |
| if (ctx->notify_func2) |
| ctx->notify_func2(ctx->notify_baton2, |
| svn_wc_create_notify(local_abspath, |
| svn_wc_notify_resolved_tree, |
| scratch_pool), |
| scratch_pool); |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| conflict->resolution_tree = option_id; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *victim_abspath; |
| const char *merge_source_abspath; |
| svn_wc_conflict_reason_t local_change; |
| svn_wc_operation_t operation; |
| const char *lock_abspath; |
| svn_error_t *err; |
| const char *repos_root_url; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *wc_tmpdir; |
| const char *ancestor_abspath; |
| svn_stream_t *ancestor_stream; |
| apr_hash_t *ancestor_props; |
| apr_hash_t *victim_props; |
| apr_hash_t *move_target_props; |
| const char *ancestor_url; |
| const char *corrected_url; |
| svn_ra_session_t *ra_session; |
| svn_wc_merge_outcome_t merge_content_outcome; |
| svn_wc_notify_state_t merge_props_outcome; |
| apr_array_header_t *propdiffs; |
| struct conflict_tree_incoming_delete_details *details; |
| apr_array_header_t *possible_moved_to_abspaths; |
| const char *moved_to_abspath; |
| const char *incoming_abspath = NULL; |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL || details->moves == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("The specified conflict resolution option " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first."), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| if (operation == svn_wc_operation_none) |
| return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL, |
| _("Invalid operation code '%d' recorded for " |
| "conflict at '%s'"), operation, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| SVN_ERR_ASSERT(option_id == |
| svn_client_conflict_option_incoming_move_file_text_merge || |
| option_id == |
| svn_client_conflict_option_both_moved_file_move_merge); |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| /* Set up temporary storage for the common ancestor version of the file. */ |
| SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, victim_abspath, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_stream_open_unique(&ancestor_stream, |
| &ancestor_abspath, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| |
| /* Fetch the ancestor file's content. */ |
| ancestor_url = svn_path_url_add_component2(repos_root_url, |
| incoming_old_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| ancestor_url, NULL, NULL, |
| FALSE, FALSE, ctx, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, |
| ancestor_stream, NULL, /* fetched_rev */ |
| &ancestor_props, scratch_pool)); |
| filter_props(ancestor_props, scratch_pool); |
| |
| /* Close stream to flush ancestor file to disk. */ |
| SVN_ERR(svn_stream_close(ancestor_stream)); |
| |
| possible_moved_to_abspaths = |
| svn_hash_gets(details->wc_move_targets, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, |
| details->wc_move_target_idx, |
| const char *); |
| |
| if (local_change == svn_wc_conflict_reason_missing) |
| { |
| /* This is an incoming move vs local move conflict. |
| * Merge from the local move's target location to the |
| * incoming move's target location. */ |
| struct conflict_tree_local_missing_details *local_details; |
| apr_array_header_t *moves; |
| |
| local_details = conflict->tree_conflict_local_details; |
| moves = svn_hash_gets(local_details->wc_move_targets, |
| local_details->move_target_repos_relpath); |
| merge_source_abspath = |
| APR_ARRAY_IDX(moves, local_details->wc_move_target_idx, const char *); |
| } |
| else |
| merge_source_abspath = victim_abspath; |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, |
| svn_dirent_get_longest_ancestor(victim_abspath, |
| moved_to_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| if (local_change != svn_wc_conflict_reason_missing) |
| { |
| err = verify_local_state_for_incoming_delete(conflict, option, ctx, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| |
| /* Get a copy of the conflict victim's properties. */ |
| err = svn_wc_prop_list2(&victim_props, ctx->wc_ctx, merge_source_abspath, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Get a copy of the move target's properties. */ |
| err = svn_wc_prop_list2(&move_target_props, ctx->wc_ctx, |
| moved_to_abspath, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Create a property diff for the files. */ |
| err = svn_prop_diffs(&propdiffs, move_target_props, victim_props, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| svn_stream_t *moved_to_stream; |
| svn_stream_t *incoming_stream; |
| |
| /* Create a temporary copy of the moved file in repository-normal form. |
| * Set up this temporary file to be automatically removed. */ |
| err = svn_stream_open_unique(&incoming_stream, |
| &incoming_abspath, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_wc__translated_stream(&moved_to_stream, ctx->wc_ctx, |
| moved_to_abspath, |
| moved_to_abspath, |
| SVN_WC_TRANSLATE_TO_NF, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_stream_copy3(moved_to_stream, incoming_stream, |
| NULL, NULL, /* no cancellation */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Overwrite the moved file with the conflict victim's content. |
| * Incoming changes will be merged in from the temporary file created |
| * above. This is required to correctly make local changes show up as |
| * 'mine' during the three-way text merge between the ancestor file, |
| * the conflict victim ('mine'), and the moved file ('theirs') which |
| * was brought in by the update/switch operation and occupies the path |
| * of the merge target. */ |
| err = svn_io_copy_file(merge_source_abspath, moved_to_abspath, FALSE, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| else if (operation == svn_wc_operation_merge) |
| { |
| svn_stream_t *incoming_stream; |
| svn_stream_t *move_target_stream; |
| |
| /* Set aside the current move target file. This is required to apply |
| * the move, and only then perform a three-way text merge between |
| * the ancestor's file, our working file (which we would move to |
| * the destination), and the file that we have set aside, which |
| * contains the incoming fulltext. |
| * Set up this temporary file to NOT be automatically removed. */ |
| err = svn_stream_open_unique(&incoming_stream, |
| &incoming_abspath, wc_tmpdir, |
| svn_io_file_del_none, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_wc__translated_stream(&move_target_stream, ctx->wc_ctx, |
| moved_to_abspath, moved_to_abspath, |
| SVN_WC_TRANSLATE_TO_NF, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_stream_copy3(move_target_stream, incoming_stream, |
| NULL, NULL, /* no cancellation */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Apply the incoming move. */ |
| err = svn_io_remove_file2(moved_to_abspath, FALSE, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| err = svn_wc__move2(ctx->wc_ctx, merge_source_abspath, moved_to_abspath, |
| FALSE, /* ordinary (not meta-data only) move */ |
| FALSE, /* mixed-revisions don't apply to files */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| NULL, NULL, /* no extra notification */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| else |
| SVN_ERR_MALFUNCTION(); |
| |
| /* Perform the file merge. */ |
| err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, |
| ctx->wc_ctx, ancestor_abspath, |
| incoming_abspath, moved_to_abspath, |
| NULL, NULL, NULL, /* labels */ |
| NULL, NULL, /* conflict versions */ |
| FALSE, /* dry run */ |
| NULL, NULL, /* diff3_cmd, merge_options */ |
| apr_hash_count(ancestor_props) ? ancestor_props : NULL, |
| propdiffs, |
| NULL, NULL, /* conflict func/baton */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool); |
| svn_io_sleep_for_timestamps(moved_to_abspath, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (operation == svn_wc_operation_merge && incoming_abspath) |
| { |
| err = svn_io_remove_file2(incoming_abspath, TRUE, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| incoming_abspath = NULL; |
| } |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| /* Tell the world about the file merge that just happened. */ |
| notify = svn_wc_create_notify(moved_to_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| if (merge_content_outcome == svn_wc_merge_conflict) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->prop_state = merge_props_outcome; |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| /* Delete the tree conflict victim (clears the tree conflict marker). */ |
| err = svn_wc_delete4(ctx->wc_ctx, victim_abspath, FALSE, FALSE, |
| NULL, NULL, /* don't allow user to cancel here */ |
| NULL, NULL, /* no extra notification */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| else if (local_change == svn_wc_conflict_reason_missing) |
| { |
| /* Clear tree conflict marker. */ |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| conflict->resolution_tree = option_id; |
| |
| unlock_wc: |
| if (err && operation == svn_wc_operation_merge && incoming_abspath) |
| err = svn_error_quick_wrapf( |
| err, _("If needed, a backup copy of '%s' can be found at '%s'"), |
| svn_dirent_local_style(moved_to_abspath, scratch_pool), |
| svn_dirent_local_style(incoming_abspath, scratch_pool)); |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. |
| * Resolve an incoming move vs local move conflict by merging from the |
| * incoming move's target location to the local move's target location, |
| * overriding the incoming move. */ |
| static svn_error_t * |
| resolve_both_moved_file_text_merge(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *victim_abspath; |
| const char *local_moved_to_abspath; |
| svn_wc_operation_t operation; |
| const char *lock_abspath; |
| svn_error_t *err; |
| const char *repos_root_url; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *wc_tmpdir; |
| const char *ancestor_abspath; |
| svn_stream_t *ancestor_stream; |
| apr_hash_t *ancestor_props; |
| apr_hash_t *incoming_props; |
| apr_hash_t *local_props; |
| const char *ancestor_url; |
| const char *corrected_url; |
| svn_ra_session_t *ra_session; |
| svn_wc_merge_outcome_t merge_content_outcome; |
| svn_wc_notify_state_t merge_props_outcome; |
| apr_array_header_t *propdiffs; |
| struct conflict_tree_incoming_delete_details *incoming_details; |
| apr_array_header_t *possible_moved_to_abspaths; |
| const char *incoming_moved_to_abspath; |
| struct conflict_tree_local_missing_details *local_details; |
| apr_array_header_t *local_moves; |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_details = conflict->tree_conflict_incoming_details; |
| if (incoming_details == NULL || incoming_details->moves == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("The specified conflict resolution option " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first."), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| if (operation == svn_wc_operation_none) |
| return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL, |
| _("Invalid operation code '%d' recorded for " |
| "conflict at '%s'"), operation, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_file_merge); |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| /* Set up temporary storage for the common ancestor version of the file. */ |
| SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, victim_abspath, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_stream_open_unique(&ancestor_stream, |
| &ancestor_abspath, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| |
| /* Fetch the ancestor file's content. */ |
| ancestor_url = svn_path_url_add_component2(repos_root_url, |
| incoming_old_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| ancestor_url, NULL, NULL, |
| FALSE, FALSE, ctx, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, |
| ancestor_stream, NULL, /* fetched_rev */ |
| &ancestor_props, scratch_pool)); |
| filter_props(ancestor_props, scratch_pool); |
| |
| /* Close stream to flush ancestor file to disk. */ |
| SVN_ERR(svn_stream_close(ancestor_stream)); |
| |
| possible_moved_to_abspaths = |
| svn_hash_gets(incoming_details->wc_move_targets, |
| get_moved_to_repos_relpath(incoming_details, scratch_pool)); |
| incoming_moved_to_abspath = |
| APR_ARRAY_IDX(possible_moved_to_abspaths, |
| incoming_details->wc_move_target_idx, const char *); |
| |
| local_details = conflict->tree_conflict_local_details; |
| local_moves = svn_hash_gets(local_details->wc_move_targets, |
| local_details->move_target_repos_relpath); |
| local_moved_to_abspath = |
| APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, const char *); |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, |
| svn_dirent_get_longest_ancestor(victim_abspath, |
| local_moved_to_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| /* Get a copy of the incoming moved item's properties. */ |
| err = svn_wc_prop_list2(&incoming_props, ctx->wc_ctx, |
| incoming_moved_to_abspath, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Get a copy of the local move target's properties. */ |
| err = svn_wc_prop_list2(&local_props, ctx->wc_ctx, |
| local_moved_to_abspath, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Create a property diff for the files. */ |
| err = svn_prop_diffs(&propdiffs, incoming_props, local_props, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Perform the file merge. */ |
| err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, |
| ctx->wc_ctx, ancestor_abspath, |
| incoming_moved_to_abspath, local_moved_to_abspath, |
| NULL, NULL, NULL, /* labels */ |
| NULL, NULL, /* conflict versions */ |
| FALSE, /* dry run */ |
| NULL, NULL, /* diff3_cmd, merge_options */ |
| apr_hash_count(ancestor_props) ? ancestor_props : NULL, |
| propdiffs, |
| NULL, NULL, /* conflict func/baton */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| /* Tell the world about the file merge that just happened. */ |
| notify = svn_wc_create_notify(local_moved_to_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| if (merge_content_outcome == svn_wc_merge_conflict) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->prop_state = merge_props_outcome; |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| /* Revert local addition of the incoming move's target. */ |
| err = svn_wc_revert6(ctx->wc_ctx, incoming_moved_to_abspath, |
| svn_depth_infinity, FALSE, NULL, TRUE, FALSE, |
| FALSE /*added_keep_local*/, |
| NULL, NULL, /* no cancellation */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool); |
| |
| conflict->resolution_tree = option_id; |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. |
| * Resolve an incoming move vs local move conflict by moving the locally moved |
| * directory to the incoming move target location, and then merging changes. */ |
| static svn_error_t * |
| resolve_both_moved_dir_merge(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *victim_abspath; |
| const char *local_moved_to_abspath; |
| svn_wc_operation_t operation; |
| const char *lock_abspath; |
| svn_error_t *err; |
| const char *repos_root_url; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *incoming_moved_repos_relpath; |
| struct conflict_tree_incoming_delete_details *incoming_details; |
| apr_array_header_t *possible_moved_to_abspaths; |
| const char *incoming_moved_to_abspath; |
| struct conflict_tree_local_missing_details *local_details; |
| apr_array_header_t *local_moves; |
| svn_client__conflict_report_t *conflict_report; |
| const char *incoming_old_url; |
| const char *incoming_moved_url; |
| svn_opt_revision_t incoming_old_opt_rev; |
| svn_opt_revision_t incoming_moved_opt_rev; |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_details = conflict->tree_conflict_incoming_details; |
| if (incoming_details == NULL || incoming_details->moves == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("The specified conflict resolution option " |
| "requires details for tree conflict at '%s' " |
| |
| "to be fetched from the repository first."), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| if (operation == svn_wc_operation_none) |
| return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL, |
| _("Invalid operation code '%d' recorded for " |
| "conflict at '%s'"), operation, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_dir_merge); |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| possible_moved_to_abspaths = |
| svn_hash_gets(incoming_details->wc_move_targets, |
| get_moved_to_repos_relpath(incoming_details, scratch_pool)); |
| incoming_moved_to_abspath = |
| APR_ARRAY_IDX(possible_moved_to_abspaths, |
| incoming_details->wc_move_target_idx, const char *); |
| |
| local_details = conflict->tree_conflict_local_details; |
| local_moves = svn_hash_gets(local_details->wc_move_targets, |
| local_details->move_target_repos_relpath); |
| local_moved_to_abspath = |
| APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, const char *); |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, |
| svn_dirent_get_longest_ancestor(victim_abspath, |
| local_moved_to_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| /* Perform the merge. */ |
| incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/", |
| incoming_old_repos_relpath, SVN_VA_NULL); |
| incoming_old_opt_rev.kind = svn_opt_revision_number; |
| incoming_old_opt_rev.value.number = incoming_old_pegrev; |
| |
| incoming_moved_repos_relpath = |
| get_moved_to_repos_relpath(incoming_details, scratch_pool); |
| incoming_moved_url = apr_pstrcat(scratch_pool, repos_root_url, "/", |
| incoming_moved_repos_relpath, SVN_VA_NULL); |
| incoming_moved_opt_rev.kind = svn_opt_revision_number; |
| incoming_moved_opt_rev.value.number = incoming_new_pegrev; |
| err = svn_client__merge_locked(&conflict_report, |
| incoming_old_url, &incoming_old_opt_rev, |
| incoming_moved_url, &incoming_moved_opt_rev, |
| local_moved_to_abspath, svn_depth_infinity, |
| TRUE, TRUE, /* do a no-ancestry merge */ |
| FALSE, FALSE, FALSE, |
| TRUE, /* Allow mixed-rev just in case, |
| * since conflict victims can't be |
| * updated to straighten out |
| * mixed-rev trees. */ |
| NULL, ctx, scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Revert local addition of the incoming move's target. */ |
| err = svn_wc_revert6(ctx->wc_ctx, incoming_moved_to_abspath, |
| svn_depth_infinity, FALSE, NULL, TRUE, FALSE, |
| FALSE /*added_keep_local*/, |
| NULL, NULL, /* no cancellation */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool); |
| |
| conflict->resolution_tree = option_id; |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. |
| * Resolve an incoming move vs local move conflict by merging from the |
| * incoming move's target location to the local move's target location, |
| * overriding the incoming move. */ |
| static svn_error_t * |
| resolve_both_moved_dir_move_merge(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *victim_abspath; |
| const char *local_moved_to_abspath; |
| svn_wc_operation_t operation; |
| const char *lock_abspath; |
| svn_error_t *err; |
| const char *repos_root_url; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| struct conflict_tree_incoming_delete_details *incoming_details; |
| apr_array_header_t *possible_moved_to_abspaths; |
| const char *incoming_moved_to_abspath; |
| struct conflict_tree_local_missing_details *local_details; |
| apr_array_header_t *local_moves; |
| svn_client__conflict_report_t *conflict_report; |
| const char *incoming_old_url; |
| const char *incoming_moved_url; |
| svn_opt_revision_t incoming_old_opt_rev; |
| svn_opt_revision_t incoming_moved_opt_rev; |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_details = conflict->tree_conflict_incoming_details; |
| if (incoming_details == NULL || incoming_details->moves == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("The specified conflict resolution option " |
| "requires details for tree conflict at '%s' " |
| |
| "to be fetched from the repository first."), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| if (operation == svn_wc_operation_none) |
| return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL, |
| _("Invalid operation code '%d' recorded for " |
| "conflict at '%s'"), operation, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| SVN_ERR_ASSERT(option_id == |
| svn_client_conflict_option_both_moved_dir_move_merge); |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| possible_moved_to_abspaths = |
| svn_hash_gets(incoming_details->wc_move_targets, |
| get_moved_to_repos_relpath(incoming_details, scratch_pool)); |
| incoming_moved_to_abspath = |
| APR_ARRAY_IDX(possible_moved_to_abspaths, |
| incoming_details->wc_move_target_idx, const char *); |
| |
| local_details = conflict->tree_conflict_local_details; |
| local_moves = svn_hash_gets(local_details->wc_move_targets, |
| local_details->move_target_repos_relpath); |
| local_moved_to_abspath = |
| APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, const char *); |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, |
| svn_dirent_get_longest_ancestor(victim_abspath, |
| local_moved_to_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| /* Revert the incoming move target directory. */ |
| err = svn_wc_revert6(ctx->wc_ctx, incoming_moved_to_abspath, |
| svn_depth_infinity, |
| FALSE, NULL, TRUE, FALSE, |
| TRUE /*added_keep_local*/, |
| NULL, NULL, /* no cancellation */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* The move operation is not part of natural history. We must replicate |
| * this move in our history. Record a move in the working copy. */ |
| err = svn_wc__move2(ctx->wc_ctx, local_moved_to_abspath, |
| incoming_moved_to_abspath, |
| FALSE, /* this is not a meta-data only move */ |
| TRUE, /* allow mixed-revisions just in case */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Merge INCOMING_OLD_URL@MERGE_LEFT->INCOMING_MOVED_URL@MERGE_RIGHT |
| * into the locally moved merge target. */ |
| incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/", |
| incoming_old_repos_relpath, SVN_VA_NULL); |
| incoming_old_opt_rev.kind = svn_opt_revision_number; |
| incoming_old_opt_rev.value.number = incoming_old_pegrev; |
| |
| incoming_moved_url = apr_pstrcat(scratch_pool, repos_root_url, "/", |
| incoming_details->move_target_repos_relpath, |
| SVN_VA_NULL); |
| incoming_moved_opt_rev.kind = svn_opt_revision_number; |
| incoming_moved_opt_rev.value.number = incoming_new_pegrev; |
| err = svn_client__merge_locked(&conflict_report, |
| incoming_old_url, &incoming_old_opt_rev, |
| incoming_moved_url, &incoming_moved_opt_rev, |
| incoming_moved_to_abspath, svn_depth_infinity, |
| TRUE, TRUE, /* do a no-ancestry merge */ |
| FALSE, FALSE, FALSE, |
| TRUE, /* Allow mixed-rev just in case, |
| * since conflict victims can't be |
| * updated to straighten out |
| * mixed-rev trees. */ |
| NULL, ctx, scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool); |
| |
| conflict->resolution_tree = option_id; |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_incoming_move_dir_merge(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *local_abspath; |
| svn_wc_operation_t operation; |
| const char *lock_abspath; |
| svn_error_t *err; |
| const char *repos_root_url; |
| const char *repos_uuid; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *victim_repos_relpath; |
| svn_revnum_t victim_peg_rev; |
| const char *moved_to_repos_relpath; |
| svn_revnum_t moved_to_peg_rev; |
| struct conflict_tree_incoming_delete_details *details; |
| apr_array_header_t *possible_moved_to_abspaths; |
| const char *moved_to_abspath; |
| const char *incoming_old_url; |
| svn_opt_revision_t incoming_old_opt_rev; |
| svn_client__conflict_report_t *conflict_report; |
| svn_boolean_t is_copy; |
| svn_boolean_t is_modified; |
| |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL || details->moves == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("The specified conflict resolution option " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first."), |
| svn_dirent_local_style(local_abspath, |
| scratch_pool)); |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| SVN_ERR_ASSERT(option_id == |
| svn_client_conflict_option_incoming_move_dir_merge); |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, &repos_uuid, |
| conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| /* Get repository location of the moved-away node (the conflict victim). */ |
| if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| victim_repos_relpath = incoming_old_repos_relpath; |
| victim_peg_rev = incoming_old_pegrev; |
| } |
| else if (operation == svn_wc_operation_merge) |
| SVN_ERR(svn_wc__node_get_repos_info(&victim_peg_rev, &victim_repos_relpath, |
| NULL, NULL, ctx->wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* Get repository location of the moved-here node (incoming move). */ |
| possible_moved_to_abspaths = |
| svn_hash_gets(details->wc_move_targets, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, |
| details->wc_move_target_idx, |
| const char *); |
| |
| /* ### The following WC modifications should be atomic. */ |
| |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, |
| svn_dirent_get_longest_ancestor(local_abspath, |
| moved_to_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| err = svn_wc__node_get_origin(&is_copy, &moved_to_peg_rev, |
| &moved_to_repos_relpath, |
| NULL, NULL, NULL, NULL, |
| ctx->wc_ctx, moved_to_abspath, FALSE, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| if (!is_copy && operation == svn_wc_operation_merge) |
| { |
| err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(expected a copied item at '%s', but the " |
| "item is not a copy)"), |
| svn_dirent_local_style(local_abspath, |
| scratch_pool), |
| svn_dirent_local_style(moved_to_abspath, |
| scratch_pool)); |
| goto unlock_wc; |
| } |
| |
| if (moved_to_repos_relpath == NULL || moved_to_peg_rev == SVN_INVALID_REVNUM) |
| { |
| err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Cannot resolve tree conflict on '%s' " |
| "(could not determine origin of '%s')"), |
| svn_dirent_local_style(local_abspath, |
| scratch_pool), |
| svn_dirent_local_style(moved_to_abspath, |
| scratch_pool)); |
| goto unlock_wc; |
| } |
| |
| err = verify_local_state_for_incoming_delete(conflict, option, ctx, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (operation == svn_wc_operation_merge) |
| { |
| const char *move_target_url; |
| svn_opt_revision_t incoming_new_opt_rev; |
| |
| /* Revert the incoming move target directory. */ |
| err = svn_wc_revert6(ctx->wc_ctx, moved_to_abspath, svn_depth_infinity, |
| FALSE, NULL, TRUE, FALSE, |
| TRUE /*added_keep_local*/, |
| NULL, NULL, /* no cancellation */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* The move operation is not part of natural history. We must replicate |
| * this move in our history. Record a move in the working copy. */ |
| err = svn_wc__move2(ctx->wc_ctx, local_abspath, moved_to_abspath, |
| FALSE, /* this is not a meta-data only move */ |
| TRUE, /* allow mixed-revisions just in case */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Merge INCOMING_OLD_URL@MERGE_LEFT->MOVE_TARGET_URL@MERGE_RIGHT |
| * into move target. */ |
| incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/", |
| incoming_old_repos_relpath, SVN_VA_NULL); |
| incoming_old_opt_rev.kind = svn_opt_revision_number; |
| incoming_old_opt_rev.value.number = incoming_old_pegrev; |
| move_target_url = apr_pstrcat(scratch_pool, repos_root_url, "/", |
| get_moved_to_repos_relpath(details, |
| scratch_pool), |
| SVN_VA_NULL); |
| incoming_new_opt_rev.kind = svn_opt_revision_number; |
| incoming_new_opt_rev.value.number = incoming_new_pegrev; |
| err = svn_client__merge_locked(&conflict_report, |
| incoming_old_url, &incoming_old_opt_rev, |
| move_target_url, &incoming_new_opt_rev, |
| moved_to_abspath, svn_depth_infinity, |
| TRUE, TRUE, /* do a no-ancestry merge */ |
| FALSE, FALSE, FALSE, |
| TRUE, /* Allow mixed-rev just in case, |
| * since conflict victims can't be |
| * updated to straighten out |
| * mixed-rev trees. */ |
| NULL, ctx, scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| else |
| { |
| SVN_ERR_ASSERT(operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch); |
| |
| /* Merge local modifications into the incoming move target dir. */ |
| err = svn_wc__has_local_mods(&is_modified, ctx->wc_ctx, local_abspath, |
| TRUE, ctx->cancel_func, ctx->cancel_baton, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (is_modified) |
| { |
| err = svn_wc__conflict_tree_update_incoming_move(ctx->wc_ctx, |
| local_abspath, |
| moved_to_abspath, |
| ctx->cancel_func, |
| ctx->cancel_baton, |
| ctx->notify_func2, |
| ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| |
| /* The move operation is part of our natural history. |
| * Delete the tree conflict victim (clears the tree conflict marker). */ |
| err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, |
| NULL, NULL, /* don't allow user to cancel here */ |
| NULL, NULL, /* no extra notification */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| } |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| conflict->resolution_tree = option_id; |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. |
| * Handles svn_client_conflict_option_local_move_file_text_merge |
| * and svn_client_conflict_option_sibling_move_file_text_merge. */ |
| static svn_error_t * |
| resolve_local_move_file_merge(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *lock_abspath; |
| svn_error_t *err; |
| const char *repos_root_url; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *wc_tmpdir; |
| const char *ancestor_tmp_abspath; |
| const char *incoming_tmp_abspath; |
| apr_hash_t *ancestor_props; |
| apr_hash_t *incoming_props; |
| svn_stream_t *stream; |
| const char *url; |
| const char *corrected_url; |
| const char *old_session_url; |
| svn_ra_session_t *ra_session; |
| svn_wc_merge_outcome_t merge_content_outcome; |
| svn_wc_notify_state_t merge_props_outcome; |
| apr_array_header_t *propdiffs; |
| struct conflict_tree_local_missing_details *details; |
| const char *merge_target_abspath; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| |
| details = conflict->tree_conflict_local_details; |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (details->wc_siblings) |
| { |
| merge_target_abspath = APR_ARRAY_IDX(details->wc_siblings, |
| details->preferred_sibling_idx, |
| const char *); |
| } |
| else if (details->wc_move_targets && details->move_target_repos_relpath) |
| { |
| apr_array_header_t *moves; |
| moves = svn_hash_gets(details->wc_move_targets, |
| details->move_target_repos_relpath); |
| merge_target_abspath = APR_ARRAY_IDX(moves, details->wc_move_target_idx, |
| const char *); |
| } |
| else |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Corresponding working copy node not found " |
| "for '%s'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor( |
| wcroot_abspath, conflict->local_abspath), |
| scratch_pool)); |
| |
| SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, |
| merge_target_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* Fetch the common ancestor file's content. */ |
| SVN_ERR(svn_stream_open_unique(&stream, &ancestor_tmp_abspath, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| url = svn_path_url_add_component2(repos_root_url, |
| incoming_old_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| url, NULL, NULL, |
| FALSE, FALSE, ctx, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, stream, NULL, |
| &ancestor_props, scratch_pool)); |
| filter_props(ancestor_props, scratch_pool); |
| |
| /* Close stream to flush the file to disk. */ |
| SVN_ERR(svn_stream_close(stream)); |
| |
| /* Do the same for the incoming file's content. */ |
| SVN_ERR(svn_stream_open_unique(&stream, &incoming_tmp_abspath, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| url = svn_path_url_add_component2(repos_root_url, |
| incoming_new_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session, |
| url, scratch_pool)); |
| SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, stream, NULL, |
| &incoming_props, scratch_pool)); |
| /* Close stream to flush the file to disk. */ |
| SVN_ERR(svn_stream_close(stream)); |
| |
| filter_props(incoming_props, scratch_pool); |
| |
| /* Create a property diff for the files. */ |
| SVN_ERR(svn_prop_diffs(&propdiffs, incoming_props, ancestor_props, |
| scratch_pool)); |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, |
| svn_dirent_get_longest_ancestor(conflict->local_abspath, |
| merge_target_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| /* Perform the file merge. */ |
| err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, |
| ctx->wc_ctx, |
| ancestor_tmp_abspath, incoming_tmp_abspath, |
| merge_target_abspath, |
| NULL, NULL, NULL, /* labels */ |
| NULL, NULL, /* conflict versions */ |
| FALSE, /* dry run */ |
| NULL, NULL, /* diff3_cmd, merge_options */ |
| apr_hash_count(ancestor_props) ? ancestor_props : NULL, |
| propdiffs, |
| NULL, NULL, /* conflict func/baton */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool); |
| svn_io_sleep_for_timestamps(merge_target_abspath, scratch_pool); |
| if (err) |
| return svn_error_compose_create(err, |
| svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, conflict->local_abspath, |
| scratch_pool); |
| err = svn_error_compose_create(err, |
| svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| if (err) |
| return svn_error_trace(err); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| /* Tell the world about the file merge that just happened. */ |
| notify = svn_wc_create_notify(merge_target_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| if (merge_content_outcome == svn_wc_merge_conflict) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->prop_state = merge_props_outcome; |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| |
| /* And also about the successfully resolved tree conflict. */ |
| notify = svn_wc_create_notify(conflict->local_abspath, |
| svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. */ |
| static svn_error_t * |
| resolve_local_move_dir_merge(svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *lock_abspath; |
| svn_error_t *err; |
| const char *repos_root_url; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| struct conflict_tree_local_missing_details *details; |
| const char *merge_target_abspath; |
| const char *incoming_old_url; |
| const char *incoming_new_url; |
| svn_opt_revision_t incoming_old_opt_rev; |
| svn_opt_revision_t incoming_new_opt_rev; |
| svn_client__conflict_report_t *conflict_report; |
| |
| details = conflict->tree_conflict_local_details; |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (details->wc_move_targets) |
| { |
| apr_array_header_t *moves; |
| |
| moves = svn_hash_gets(details->wc_move_targets, |
| details->move_target_repos_relpath); |
| merge_target_abspath = |
| APR_ARRAY_IDX(moves, details->wc_move_target_idx, const char *); |
| } |
| else |
| merge_target_abspath = APR_ARRAY_IDX(details->wc_siblings, |
| details->preferred_sibling_idx, |
| const char *); |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, |
| svn_dirent_get_longest_ancestor(conflict->local_abspath, |
| merge_target_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| /* Resolve to current working copy state. |
| * svn_client__merge_locked() requires this. */ |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, conflict->local_abspath, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Merge outstanding changes to the merge target. */ |
| incoming_old_url = apr_pstrcat(scratch_pool, repos_root_url, "/", |
| incoming_old_repos_relpath, SVN_VA_NULL); |
| incoming_old_opt_rev.kind = svn_opt_revision_number; |
| incoming_old_opt_rev.value.number = incoming_old_pegrev; |
| incoming_new_url = apr_pstrcat(scratch_pool, repos_root_url, "/", |
| incoming_new_repos_relpath, SVN_VA_NULL); |
| incoming_new_opt_rev.kind = svn_opt_revision_number; |
| incoming_new_opt_rev.value.number = incoming_new_pegrev; |
| err = svn_client__merge_locked(&conflict_report, |
| incoming_old_url, &incoming_old_opt_rev, |
| incoming_new_url, &incoming_new_opt_rev, |
| merge_target_abspath, svn_depth_infinity, |
| TRUE, TRUE, /* do a no-ancestry merge */ |
| FALSE, FALSE, FALSE, |
| TRUE, /* Allow mixed-rev just in case, |
| * since conflict victims can't be |
| * updated to straighten out |
| * mixed-rev trees. */ |
| NULL, ctx, scratch_pool, scratch_pool); |
| unlock_wc: |
| svn_io_sleep_for_timestamps(merge_target_abspath, scratch_pool); |
| err = svn_error_compose_create(err, |
| svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| if (err) |
| return svn_error_trace(err); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| /* Tell the world about the file merge that just happened. */ |
| notify = svn_wc_create_notify(merge_target_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| if (conflict_report) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->kind = svn_node_dir; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| |
| /* And also about the successfully resolved tree conflict. */ |
| notify = svn_wc_create_notify(conflict->local_abspath, |
| svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| conflict->resolution_tree = svn_client_conflict_option_get_id(option); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| assert_text_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) |
| { |
| svn_boolean_t text_conflicted; |
| |
| SVN_ERR(svn_client_conflict_get_conflicted(&text_conflicted, NULL, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| |
| SVN_ERR_ASSERT(text_conflicted); /* ### return proper error? */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| assert_prop_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *props_conflicted; |
| |
| SVN_ERR(svn_client_conflict_get_conflicted(NULL, &props_conflicted, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| |
| /* ### return proper error? */ |
| SVN_ERR_ASSERT(props_conflicted && props_conflicted->nelts > 0); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| assert_tree_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) |
| { |
| svn_boolean_t tree_conflicted; |
| |
| SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted, |
| conflict, scratch_pool, |
| scratch_pool)); |
| |
| SVN_ERR_ASSERT(tree_conflicted); /* ### return proper error? */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Helper to add to conflict resolution option to array of OPTIONS. |
| * Resolution option object will be allocated from OPTIONS->POOL |
| * and DESCRIPTION will be copied to this pool. |
| * Returns pointer to the created conflict resolution option. */ |
| static svn_client_conflict_option_t * |
| add_resolution_option(apr_array_header_t *options, |
| svn_client_conflict_t *conflict, |
| svn_client_conflict_option_id_t id, |
| const char *label, |
| const char *description, |
| conflict_option_resolve_func_t resolve_func) |
| { |
| svn_client_conflict_option_t *option; |
| |
| option = apr_pcalloc(options->pool, sizeof(*option)); |
| option->pool = options->pool; |
| option->id = id; |
| option->label = apr_pstrdup(option->pool, label); |
| option->description = apr_pstrdup(option->pool, description); |
| option->conflict = conflict; |
| option->do_resolve_func = resolve_func; |
| |
| APR_ARRAY_PUSH(options, const svn_client_conflict_option_t *) = option; |
| |
| return option; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_text_get_resolution_options(apr_array_header_t **options, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *mime_type; |
| |
| SVN_ERR(assert_text_conflict(conflict, scratch_pool)); |
| |
| *options = apr_array_make(result_pool, 7, |
| sizeof(svn_client_conflict_option_t *)); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_postpone, |
| _("Postpone"), |
| _("skip this conflict and leave it unresolved"), |
| resolve_postpone); |
| |
| mime_type = svn_client_conflict_text_get_mime_type(conflict); |
| if (mime_type && svn_mime_type_is_binary(mime_type)) |
| { |
| /* Resolver options for a binary file conflict. */ |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_base_text, |
| _("Accept base"), |
| _("discard local and incoming changes for this binary file"), |
| resolve_text_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_incoming_text, |
| _("Accept incoming"), |
| _("accept incoming version of binary file"), |
| resolve_text_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_working_text, |
| _("Mark as resolved"), |
| _("accept binary file as it appears in the working copy"), |
| resolve_text_conflict); |
| } |
| else |
| { |
| /* Resolver options for a text file conflict. */ |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_base_text, |
| _("Accept base"), |
| _("discard local and incoming changes for this file"), |
| resolve_text_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_incoming_text, |
| _("Accept incoming"), |
| _("accept incoming version of entire file"), |
| resolve_text_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_working_text, |
| _("Reject incoming"), |
| _("reject all incoming changes for this file"), |
| resolve_text_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_incoming_text_where_conflicted, |
| _("Accept incoming for conflicts"), |
| _("accept incoming changes only where they conflict"), |
| resolve_text_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_working_text_where_conflicted, |
| _("Reject conflicts"), |
| _("reject incoming changes which conflict and accept the rest"), |
| resolve_text_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_merged_text, |
| _("Mark as resolved"), |
| _("accept the file as it appears in the working copy"), |
| resolve_text_conflict); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_prop_get_resolution_options(apr_array_header_t **options, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| SVN_ERR(assert_prop_conflict(conflict, scratch_pool)); |
| |
| *options = apr_array_make(result_pool, 7, |
| sizeof(svn_client_conflict_option_t *)); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_postpone, |
| _("Postpone"), |
| _("skip this conflict and leave it unresolved"), |
| resolve_postpone); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_base_text, |
| _("Accept base"), |
| _("discard local and incoming changes for this property"), |
| resolve_prop_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_incoming_text, |
| _("Accept incoming"), |
| _("accept incoming version of entire property value"), |
| resolve_prop_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_working_text, |
| _("Mark as resolved"), |
| _("accept working copy version of entire property value"), |
| resolve_prop_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_incoming_text_where_conflicted, |
| _("Accept incoming for conflicts"), |
| _("accept incoming changes only where they conflict"), |
| resolve_prop_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_working_text_where_conflicted, |
| _("Reject conflicts"), |
| _("reject changes which conflict and accept the rest"), |
| resolve_prop_conflict); |
| |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_merged_text, |
| _("Accept merged"), |
| _("accept merged version of property value"), |
| resolve_prop_conflict); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'accept current wc state' resolution option for a tree conflict. */ |
| static svn_error_t * |
| configure_option_accept_current_wc_state(svn_client_conflict_t *conflict, |
| apr_array_header_t *options) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| conflict_option_resolve_func_t do_resolve_func; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| if ((operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) && |
| (local_change == svn_wc_conflict_reason_moved_away || |
| local_change == svn_wc_conflict_reason_deleted || |
| local_change == svn_wc_conflict_reason_replaced) && |
| incoming_change == svn_wc_conflict_action_edit) |
| { |
| /* We must break moves if the user accepts the current working copy |
| * state instead of updating a moved-away node or updating children |
| * moved outside of deleted or replaced directory nodes. |
| * Else such moves would be left in an invalid state. */ |
| do_resolve_func = resolve_update_break_moved_away; |
| } |
| else |
| do_resolve_func = resolve_accept_current_wc_state; |
| |
| add_resolution_option(options, conflict, |
| svn_client_conflict_option_accept_current_wc_state, |
| _("Mark as resolved"), |
| _("accept current working copy state"), |
| do_resolve_func); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'update move destination' resolution option for a tree conflict. */ |
| static svn_error_t * |
| configure_option_update_move_destination(svn_client_conflict_t *conflict, |
| apr_array_header_t *options) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| if ((operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) && |
| incoming_change == svn_wc_conflict_action_edit && |
| local_change == svn_wc_conflict_reason_moved_away) |
| { |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_update_move_destination, |
| _("Update move destination"), |
| _("apply incoming changes to move destination"), |
| resolve_update_moved_away_node); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'update raise moved away children' resolution option for a tree |
| * conflict. */ |
| static svn_error_t * |
| configure_option_update_raise_moved_away_children( |
| svn_client_conflict_t *conflict, |
| apr_array_header_t *options) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| svn_node_kind_t victim_node_kind; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| |
| if ((operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) && |
| incoming_change == svn_wc_conflict_action_edit && |
| (local_change == svn_wc_conflict_reason_deleted || |
| local_change == svn_wc_conflict_reason_replaced) && |
| victim_node_kind == svn_node_dir) |
| { |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_update_any_moved_away_children, |
| _("Update any moved-away children"), |
| _("prepare for updating moved-away children, if any"), |
| resolve_update_raise_moved_away); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming add ignore' resolution option for a tree conflict. */ |
| static svn_error_t * |
| configure_option_incoming_add_ignore(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t victim_node_kind; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| /* This option is only available for directories. */ |
| if (victim_node_kind == svn_node_dir && |
| incoming_change == svn_wc_conflict_action_add && |
| (local_change == svn_wc_conflict_reason_obstructed || |
| local_change == svn_wc_conflict_reason_added)) |
| { |
| const char *description; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| if (operation == svn_wc_operation_merge) |
| description = |
| apr_psprintf(scratch_pool, |
| _("ignore and do not add '^/%s@%ld' here"), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| else if (operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) |
| { |
| if (victim_node_kind == svn_node_file) |
| description = |
| apr_psprintf(scratch_pool, |
| _("replace '^/%s@%ld' with the locally added file"), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| else if (victim_node_kind == svn_node_dir) |
| description = |
| apr_psprintf(scratch_pool, |
| _("replace '^/%s@%ld' with the locally added " |
| "directory"), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| else |
| description = |
| apr_psprintf(scratch_pool, |
| _("replace '^/%s@%ld' with the locally added item"), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| } |
| else |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("unexpected operation code '%d'"), |
| operation); |
| add_resolution_option( |
| options, conflict, svn_client_conflict_option_incoming_add_ignore, |
| _("Ignore incoming addition"), description, resolve_incoming_add_ignore); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming added file text merge' resolution option for a tree |
| * conflict. */ |
| static svn_error_t * |
| configure_option_incoming_added_file_text_merge(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| svn_node_kind_t victim_node_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (victim_node_kind == svn_node_file && |
| incoming_new_kind == svn_node_file && |
| incoming_change == svn_wc_conflict_action_add && |
| (local_change == svn_wc_conflict_reason_obstructed || |
| local_change == svn_wc_conflict_reason_unversioned || |
| local_change == svn_wc_conflict_reason_added)) |
| { |
| const char *description; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| |
| if (operation == svn_wc_operation_merge) |
| description = |
| apr_psprintf(scratch_pool, _("merge '^/%s@%ld' into '%s'"), |
| incoming_new_repos_relpath, incoming_new_pegrev, |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool)); |
| else |
| description = |
| apr_psprintf(scratch_pool, _("merge local '%s' and '^/%s@%ld'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_incoming_added_file_text_merge, |
| _("Merge the files"), description, |
| operation == svn_wc_operation_merge |
| ? resolve_merge_incoming_added_file_text_merge |
| : resolve_merge_incoming_added_file_text_update); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming added file replace and merge' resolution option for a |
| * tree conflict. */ |
| static svn_error_t * |
| configure_option_incoming_added_file_replace_and_merge( |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| svn_node_kind_t victim_node_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (operation == svn_wc_operation_merge && |
| victim_node_kind == svn_node_file && |
| incoming_new_kind == svn_node_file && |
| incoming_change == svn_wc_conflict_action_add && |
| local_change == svn_wc_conflict_reason_obstructed) |
| { |
| const char *wcroot_abspath; |
| const char *description; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| description = |
| apr_psprintf(scratch_pool, |
| _("delete '%s', copy '^/%s@%ld' here, and merge the files"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_incoming_added_file_replace_and_merge, |
| _("Replace and merge"), |
| description, resolve_merge_incoming_added_file_replace_and_merge); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming added dir merge' resolution option for a tree |
| * conflict. */ |
| static svn_error_t * |
| configure_option_incoming_added_dir_merge(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| svn_node_kind_t victim_node_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (victim_node_kind == svn_node_dir && |
| incoming_new_kind == svn_node_dir && |
| incoming_change == svn_wc_conflict_action_add && |
| (local_change == svn_wc_conflict_reason_added || |
| (operation == svn_wc_operation_merge && |
| local_change == svn_wc_conflict_reason_obstructed) || |
| (operation != svn_wc_operation_merge && |
| local_change == svn_wc_conflict_reason_unversioned))) |
| { |
| const char *description; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| if (operation == svn_wc_operation_merge) |
| { |
| if (conflict->tree_conflict_incoming_details == NULL) |
| return SVN_NO_ERROR; |
| |
| description = |
| apr_psprintf(scratch_pool, _("merge '^/%s@%ld' into '%s'"), |
| incoming_new_repos_relpath, incoming_new_pegrev, |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool)); |
| } |
| else |
| description = |
| apr_psprintf(scratch_pool, _("merge local '%s' and '^/%s@%ld'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| |
| add_resolution_option(options, conflict, |
| svn_client_conflict_option_incoming_added_dir_merge, |
| _("Merge the directories"), description, |
| operation == svn_wc_operation_merge |
| ? resolve_merge_incoming_added_dir_merge |
| : resolve_update_incoming_added_dir_merge); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming added dir replace' resolution option for a tree |
| * conflict. */ |
| static svn_error_t * |
| configure_option_incoming_added_dir_replace(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| svn_node_kind_t victim_node_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (operation == svn_wc_operation_merge && |
| victim_node_kind == svn_node_dir && |
| incoming_new_kind == svn_node_dir && |
| incoming_change == svn_wc_conflict_action_add && |
| local_change == svn_wc_conflict_reason_obstructed) |
| { |
| const char *description; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| description = |
| apr_psprintf(scratch_pool, _("delete '%s' and copy '^/%s@%ld' here"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_incoming_added_dir_replace, |
| _("Delete my directory and replace it with incoming directory"), |
| description, resolve_merge_incoming_added_dir_replace); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming added dir replace and merge' resolution option |
| * for a tree conflict. */ |
| static svn_error_t * |
| configure_option_incoming_added_dir_replace_and_merge( |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| svn_node_kind_t victim_node_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (operation == svn_wc_operation_merge && |
| victim_node_kind == svn_node_dir && |
| incoming_new_kind == svn_node_dir && |
| incoming_change == svn_wc_conflict_action_add && |
| local_change == svn_wc_conflict_reason_obstructed) |
| { |
| const char *description; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| description = |
| apr_psprintf(scratch_pool, |
| _("delete '%s', copy '^/%s@%ld' here, and merge the directories"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, |
| conflict->local_abspath), |
| scratch_pool), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_incoming_added_dir_replace_and_merge, |
| _("Replace and merge"), |
| description, resolve_merge_incoming_added_dir_replace_and_merge); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming delete ignore' resolution option for a tree conflict. */ |
| static svn_error_t * |
| configure_option_incoming_delete_ignore(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (incoming_change == svn_wc_conflict_action_delete) |
| { |
| const char *description; |
| struct conflict_tree_incoming_delete_details *incoming_details; |
| svn_boolean_t is_incoming_move; |
| |
| incoming_details = conflict->tree_conflict_incoming_details; |
| is_incoming_move = (incoming_details != NULL && |
| incoming_details->moves != NULL); |
| if (local_change == svn_wc_conflict_reason_moved_away || |
| local_change == svn_wc_conflict_reason_edited) |
| { |
| /* An option which ignores the incoming deletion makes no sense |
| * if we know there was a local move and/or an incoming move. */ |
| if (is_incoming_move) |
| return SVN_NO_ERROR; |
| } |
| else if (local_change == svn_wc_conflict_reason_deleted) |
| { |
| /* If the local item was deleted and conflict details were fetched |
| * and indicate that there was no move, then this is an actual |
| * 'delete vs delete' situation. An option which ignores the incoming |
| * deletion makes no sense in that case because there is no local |
| * node to preserve. */ |
| if (!is_incoming_move) |
| return SVN_NO_ERROR; |
| } |
| else if (local_change == svn_wc_conflict_reason_missing && |
| operation == svn_wc_operation_merge) |
| { |
| struct conflict_tree_local_missing_details *local_details; |
| svn_boolean_t is_local_move; /* "local" to branch history */ |
| |
| local_details = conflict->tree_conflict_local_details; |
| is_local_move = (local_details != NULL && |
| local_details->moves != NULL); |
| |
| if (is_incoming_move || is_local_move) |
| return SVN_NO_ERROR; |
| } |
| |
| description = |
| apr_psprintf(scratch_pool, _("ignore the deletion of '^/%s@%ld'"), |
| incoming_new_repos_relpath, incoming_new_pegrev); |
| |
| add_resolution_option(options, conflict, |
| svn_client_conflict_option_incoming_delete_ignore, |
| _("Ignore incoming deletion"), description, |
| resolve_incoming_delete_ignore); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming delete accept' resolution option for a tree conflict. */ |
| static svn_error_t * |
| configure_option_incoming_delete_accept(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (incoming_change == svn_wc_conflict_action_delete) |
| { |
| struct conflict_tree_incoming_delete_details *incoming_details; |
| svn_boolean_t is_incoming_move; |
| |
| incoming_details = conflict->tree_conflict_incoming_details; |
| is_incoming_move = (incoming_details != NULL && |
| incoming_details->moves != NULL); |
| if (is_incoming_move && |
| (local_change == svn_wc_conflict_reason_edited || |
| local_change == svn_wc_conflict_reason_moved_away || |
| local_change == svn_wc_conflict_reason_missing)) |
| { |
| /* An option which accepts the incoming deletion makes no sense |
| * if we know there was a local move and/or an incoming move. */ |
| return SVN_NO_ERROR; |
| } |
| else |
| { |
| const char *description; |
| const char *wcroot_abspath; |
| const char *local_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| local_abspath = svn_client_conflict_get_local_abspath(conflict); |
| description = |
| apr_psprintf(scratch_pool, _("accept the deletion of '%s'"), |
| svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, |
| local_abspath), |
| scratch_pool)); |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_incoming_delete_accept, |
| _("Accept incoming deletion"), description, |
| resolve_incoming_delete_accept); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| describe_incoming_move_merge_conflict_option( |
| const char **description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| const char *moved_to_abspath, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| const char *victim_abspath; |
| svn_node_kind_t victim_node_kind; |
| const char *wcroot_abspath; |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| victim_abspath, scratch_pool, |
| scratch_pool)); |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| if (operation == svn_wc_operation_merge) |
| { |
| const char *incoming_moved_abspath = NULL; |
| |
| if (victim_node_kind == svn_node_none) |
| { |
| /* This is an incoming move vs local move conflict. */ |
| struct conflict_tree_incoming_delete_details *details; |
| |
| details = conflict->tree_conflict_incoming_details; |
| if (details->wc_move_targets) |
| { |
| apr_array_header_t *moves; |
| |
| moves = svn_hash_gets(details->wc_move_targets, |
| details->move_target_repos_relpath); |
| incoming_moved_abspath = |
| APR_ARRAY_IDX(moves, details->wc_move_target_idx, |
| const char *); |
| } |
| } |
| |
| if (incoming_moved_abspath) |
| { |
| /* The 'move and merge' option follows the incoming move; note that |
| * moved_to_abspath points to the current location of an item which |
| * was moved in the history of our merge target branch. If the user |
| * chooses 'move and merge', that item will be moved again (i.e. it |
| * will be moved to and merged with incoming_moved_abspath's item). */ |
| *description = |
| apr_psprintf( |
| result_pool, _("move '%s' to '%s' and merge"), |
| svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, |
| moved_to_abspath), |
| scratch_pool), |
| svn_dirent_local_style(svn_dirent_skip_ancestor( |
| wcroot_abspath, |
| incoming_moved_abspath), |
| scratch_pool)); |
| } |
| else |
| { |
| *description = |
| apr_psprintf( |
| result_pool, _("move '%s' to '%s' and merge"), |
| svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, |
| victim_abspath), |
| scratch_pool), |
| svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, |
| moved_to_abspath), |
| scratch_pool)); |
| } |
| } |
| else |
| *description = |
| apr_psprintf( |
| result_pool, _("move and merge local changes from '%s' into '%s'"), |
| svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, |
| victim_abspath), |
| scratch_pool), |
| svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, |
| moved_to_abspath), |
| scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming move file merge' resolution option for |
| * a tree conflict. */ |
| static svn_error_t * |
| configure_option_incoming_move_file_merge(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_node_kind_t victim_node_kind; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| svn_node_kind_t incoming_old_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| &incoming_old_kind, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (victim_node_kind == svn_node_file && |
| incoming_old_kind == svn_node_file && |
| incoming_new_kind == svn_node_none && |
| incoming_change == svn_wc_conflict_action_delete && |
| local_change == svn_wc_conflict_reason_edited) |
| { |
| struct conflict_tree_incoming_delete_details *details; |
| const char *description; |
| apr_array_header_t *move_target_wc_abspaths; |
| const char *moved_to_abspath; |
| |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL || details->moves == NULL) |
| return SVN_NO_ERROR; |
| |
| if (apr_hash_count(details->wc_move_targets) == 0) |
| return SVN_NO_ERROR; |
| |
| move_target_wc_abspaths = |
| svn_hash_gets(details->wc_move_targets, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, |
| details->wc_move_target_idx, |
| const char *); |
| SVN_ERR(describe_incoming_move_merge_conflict_option(&description, |
| conflict, ctx, |
| moved_to_abspath, |
| scratch_pool, |
| scratch_pool)); |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_incoming_move_file_text_merge, |
| _("Move and merge"), description, |
| resolve_incoming_move_file_text_merge); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'incoming move dir merge' resolution option for |
| * a tree conflict. */ |
| static svn_error_t * |
| configure_option_incoming_dir_merge(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_node_kind_t victim_node_kind; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| svn_node_kind_t incoming_old_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| &incoming_old_kind, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (victim_node_kind == svn_node_dir && |
| incoming_old_kind == svn_node_dir && |
| incoming_new_kind == svn_node_none && |
| incoming_change == svn_wc_conflict_action_delete && |
| local_change == svn_wc_conflict_reason_edited) |
| { |
| struct conflict_tree_incoming_delete_details *details; |
| const char *description; |
| apr_array_header_t *move_target_wc_abspaths; |
| const char *moved_to_abspath; |
| |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL || details->moves == NULL) |
| return SVN_NO_ERROR; |
| |
| if (apr_hash_count(details->wc_move_targets) == 0) |
| return SVN_NO_ERROR; |
| |
| move_target_wc_abspaths = |
| svn_hash_gets(details->wc_move_targets, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, |
| details->wc_move_target_idx, |
| const char *); |
| SVN_ERR(describe_incoming_move_merge_conflict_option(&description, |
| conflict, ctx, |
| moved_to_abspath, |
| scratch_pool, |
| scratch_pool)); |
| add_resolution_option(options, conflict, |
| svn_client_conflict_option_incoming_move_dir_merge, |
| _("Move and merge"), description, |
| resolve_incoming_move_dir_merge); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'local move file merge' resolution option for |
| * a tree conflict. */ |
| static svn_error_t * |
| configure_option_local_move_file_or_dir_merge( |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| svn_node_kind_t incoming_old_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| &incoming_old_kind, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (operation == svn_wc_operation_merge && |
| incoming_change == svn_wc_conflict_action_edit && |
| local_change == svn_wc_conflict_reason_missing) |
| { |
| struct conflict_tree_local_missing_details *details; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| details = conflict->tree_conflict_local_details; |
| if (details != NULL && details->moves != NULL && |
| details->move_target_repos_relpath != NULL) |
| { |
| apr_array_header_t *moves; |
| const char *moved_to_abspath; |
| const char *description; |
| |
| moves = svn_hash_gets(details->wc_move_targets, |
| details->move_target_repos_relpath); |
| moved_to_abspath = |
| APR_ARRAY_IDX(moves, details->wc_move_target_idx, const char *); |
| |
| description = |
| apr_psprintf( |
| scratch_pool, _("apply changes to move destination '%s'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, moved_to_abspath), |
| scratch_pool)); |
| |
| if ((incoming_old_kind == svn_node_file || |
| incoming_old_kind == svn_node_none) && |
| (incoming_new_kind == svn_node_file || |
| incoming_new_kind == svn_node_none)) |
| { |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_local_move_file_text_merge, |
| _("Apply to move destination"), |
| description, resolve_local_move_file_merge); |
| } |
| else |
| { |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_local_move_dir_merge, |
| _("Apply to move destination"), |
| description, resolve_local_move_dir_merge); |
| } |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'sibling move file/dir merge' resolution option for |
| * a tree conflict. */ |
| static svn_error_t * |
| configure_option_sibling_move_merge(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| svn_node_kind_t incoming_old_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| &incoming_old_kind, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (operation == svn_wc_operation_merge && |
| incoming_change == svn_wc_conflict_action_edit && |
| local_change == svn_wc_conflict_reason_missing) |
| { |
| struct conflict_tree_local_missing_details *details; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, scratch_pool)); |
| |
| details = conflict->tree_conflict_local_details; |
| if (details != NULL && details->wc_siblings != NULL) |
| { |
| const char *description; |
| const char *sibling; |
| |
| sibling = |
| apr_pstrdup(conflict->pool, |
| APR_ARRAY_IDX(details->wc_siblings, |
| details->preferred_sibling_idx, |
| const char *)); |
| description = |
| apr_psprintf( |
| scratch_pool, _("apply changes to '%s'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, sibling), |
| scratch_pool)); |
| |
| if ((incoming_old_kind == svn_node_file || |
| incoming_old_kind == svn_node_none) && |
| (incoming_new_kind == svn_node_file || |
| incoming_new_kind == svn_node_none)) |
| { |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_sibling_move_file_text_merge, |
| _("Apply to corresponding local location"), |
| description, resolve_local_move_file_merge); |
| } |
| else |
| { |
| add_resolution_option( |
| options, conflict, |
| svn_client_conflict_option_sibling_move_dir_merge, |
| _("Apply to corresponding local location"), |
| description, resolve_local_move_dir_merge); |
| } |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| struct conflict_tree_update_local_moved_away_details { |
| /* |
| * This array consists of "const char *" absolute paths to working copy |
| * nodes which are uncommitted copies and correspond to the repository path |
| * of the conflict victim. |
| * Each such working copy node is a potential local move target which can |
| * be chosen to find a suitable merge target when resolving a tree conflict. |
| * |
| * This may be an empty array in case if there is no move target path in |
| * the working copy. */ |
| apr_array_header_t *wc_move_targets; |
| |
| /* Current index into the list of working copy paths in WC_MOVE_TARGETS. */ |
| int preferred_move_target_idx; |
| }; |
| |
| /* Implements conflict_option_resolve_func_t. |
| * Resolve an incoming move vs local move conflict by merging from the |
| * incoming move's target location to the local move's target location, |
| * overriding the incoming move. The original local move was broken during |
| * update/switch, so overriding the incoming move involves recording a new |
| * move from the incoming move's target location to the local move's target |
| * location. */ |
| static svn_error_t * |
| resolve_both_moved_file_update_keep_local_move( |
| svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *victim_abspath; |
| const char *local_moved_to_abspath; |
| svn_wc_operation_t operation; |
| const char *lock_abspath; |
| svn_error_t *err; |
| const char *repos_root_url; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *wc_tmpdir; |
| const char *ancestor_abspath; |
| svn_stream_t *ancestor_stream; |
| apr_hash_t *ancestor_props; |
| apr_hash_t *incoming_props; |
| apr_hash_t *local_props; |
| const char *ancestor_url; |
| const char *corrected_url; |
| svn_ra_session_t *ra_session; |
| svn_wc_merge_outcome_t merge_content_outcome; |
| svn_wc_notify_state_t merge_props_outcome; |
| apr_array_header_t *propdiffs; |
| struct conflict_tree_incoming_delete_details *incoming_details; |
| apr_array_header_t *possible_moved_to_abspaths; |
| const char *incoming_moved_to_abspath; |
| struct conflict_tree_update_local_moved_away_details *local_details; |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_details = conflict->tree_conflict_incoming_details; |
| if (incoming_details == NULL || incoming_details->moves == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("The specified conflict resolution option " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first."), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| if (operation == svn_wc_operation_none) |
| return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL, |
| _("Invalid operation code '%d' recorded for " |
| "conflict at '%s'"), operation, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| SVN_ERR_ASSERT(option_id == svn_client_conflict_option_both_moved_file_merge); |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| /* Set up temporary storage for the common ancestor version of the file. */ |
| SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, victim_abspath, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_stream_open_unique(&ancestor_stream, |
| &ancestor_abspath, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| |
| /* Fetch the ancestor file's content. */ |
| ancestor_url = svn_path_url_add_component2(repos_root_url, |
| incoming_old_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| ancestor_url, NULL, NULL, |
| FALSE, FALSE, ctx, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, |
| ancestor_stream, NULL, /* fetched_rev */ |
| &ancestor_props, scratch_pool)); |
| filter_props(ancestor_props, scratch_pool); |
| |
| /* Close stream to flush ancestor file to disk. */ |
| SVN_ERR(svn_stream_close(ancestor_stream)); |
| |
| possible_moved_to_abspaths = |
| svn_hash_gets(incoming_details->wc_move_targets, |
| get_moved_to_repos_relpath(incoming_details, scratch_pool)); |
| incoming_moved_to_abspath = |
| APR_ARRAY_IDX(possible_moved_to_abspaths, |
| incoming_details->wc_move_target_idx, const char *); |
| |
| local_details = conflict->tree_conflict_local_details; |
| local_moved_to_abspath = |
| APR_ARRAY_IDX(local_details->wc_move_targets, |
| local_details->preferred_move_target_idx, const char *); |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, |
| svn_dirent_get_longest_ancestor(victim_abspath, |
| local_moved_to_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| /* Get a copy of the incoming moved item's properties. */ |
| err = svn_wc_prop_list2(&incoming_props, ctx->wc_ctx, |
| incoming_moved_to_abspath, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Get a copy of the local move target's properties. */ |
| err = svn_wc_prop_list2(&local_props, ctx->wc_ctx, |
| local_moved_to_abspath, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Create a property diff for the files. */ |
| err = svn_prop_diffs(&propdiffs, incoming_props, local_props, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Perform the file merge. */ |
| err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, |
| ctx->wc_ctx, ancestor_abspath, |
| incoming_moved_to_abspath, local_moved_to_abspath, |
| NULL, NULL, NULL, /* labels */ |
| NULL, NULL, /* conflict versions */ |
| FALSE, /* dry run */ |
| NULL, NULL, /* diff3_cmd, merge_options */ |
| apr_hash_count(ancestor_props) ? ancestor_props : NULL, |
| propdiffs, |
| NULL, NULL, /* conflict func/baton */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| /* Tell the world about the file merge that just happened. */ |
| notify = svn_wc_create_notify(local_moved_to_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| if (merge_content_outcome == svn_wc_merge_conflict) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->prop_state = merge_props_outcome; |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| /* Record a new move which overrides the incoming move. */ |
| err = svn_wc__move2(ctx->wc_ctx, incoming_moved_to_abspath, |
| local_moved_to_abspath, |
| TRUE, /* meta-data only move */ |
| FALSE, /* mixed-revisions don't apply to files */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| NULL, NULL, /* no extra notification */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Remove moved-away file from disk. */ |
| err = svn_io_remove_file2(incoming_moved_to_abspath, TRUE, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool); |
| |
| conflict->resolution_tree = option_id; |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements conflict_option_resolve_func_t. |
| * Resolve an incoming move vs local move conflict by merging from the |
| * local move's target location to the incoming move's target location, |
| * and reverting the local move. */ |
| static svn_error_t * |
| resolve_both_moved_file_update_keep_incoming_move( |
| svn_client_conflict_option_t *option, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_option_id_t option_id; |
| const char *victim_abspath; |
| const char *local_moved_to_abspath; |
| svn_wc_operation_t operation; |
| const char *lock_abspath; |
| svn_error_t *err; |
| const char *repos_root_url; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| const char *wc_tmpdir; |
| const char *ancestor_abspath; |
| svn_stream_t *ancestor_stream; |
| apr_hash_t *ancestor_props; |
| apr_hash_t *incoming_props; |
| apr_hash_t *local_props; |
| const char *ancestor_url; |
| const char *corrected_url; |
| svn_ra_session_t *ra_session; |
| svn_wc_merge_outcome_t merge_content_outcome; |
| svn_wc_notify_state_t merge_props_outcome; |
| apr_array_header_t *propdiffs; |
| struct conflict_tree_incoming_delete_details *incoming_details; |
| apr_array_header_t *possible_moved_to_abspaths; |
| const char *incoming_moved_to_abspath; |
| struct conflict_tree_update_local_moved_away_details *local_details; |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_details = conflict->tree_conflict_incoming_details; |
| if (incoming_details == NULL || incoming_details->moves == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("The specified conflict resolution option " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first."), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| if (operation == svn_wc_operation_none) |
| return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL, |
| _("Invalid operation code '%d' recorded for " |
| "conflict at '%s'"), operation, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| option_id = svn_client_conflict_option_get_id(option); |
| SVN_ERR_ASSERT(option_id == |
| svn_client_conflict_option_both_moved_file_move_merge); |
| |
| SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, |
| conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| NULL, conflict, scratch_pool, |
| scratch_pool)); |
| |
| /* Set up temporary storage for the common ancestor version of the file. */ |
| SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, victim_abspath, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_stream_open_unique(&ancestor_stream, |
| &ancestor_abspath, wc_tmpdir, |
| svn_io_file_del_on_pool_cleanup, |
| scratch_pool, scratch_pool)); |
| |
| /* Fetch the ancestor file's content. */ |
| ancestor_url = svn_path_url_add_component2(repos_root_url, |
| incoming_old_repos_relpath, |
| scratch_pool); |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, |
| ancestor_url, NULL, NULL, |
| FALSE, FALSE, ctx, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, |
| ancestor_stream, NULL, /* fetched_rev */ |
| &ancestor_props, scratch_pool)); |
| filter_props(ancestor_props, scratch_pool); |
| |
| /* Close stream to flush ancestor file to disk. */ |
| SVN_ERR(svn_stream_close(ancestor_stream)); |
| |
| possible_moved_to_abspaths = |
| svn_hash_gets(incoming_details->wc_move_targets, |
| get_moved_to_repos_relpath(incoming_details, scratch_pool)); |
| incoming_moved_to_abspath = |
| APR_ARRAY_IDX(possible_moved_to_abspaths, |
| incoming_details->wc_move_target_idx, const char *); |
| |
| local_details = conflict->tree_conflict_local_details; |
| local_moved_to_abspath = |
| APR_ARRAY_IDX(local_details->wc_move_targets, |
| local_details->preferred_move_target_idx, const char *); |
| |
| /* ### The following WC modifications should be atomic. */ |
| SVN_ERR(svn_wc__acquire_write_lock_for_resolve( |
| &lock_abspath, ctx->wc_ctx, |
| svn_dirent_get_longest_ancestor(victim_abspath, |
| local_moved_to_abspath, |
| scratch_pool), |
| scratch_pool, scratch_pool)); |
| |
| /* Get a copy of the incoming moved item's properties. */ |
| err = svn_wc_prop_list2(&incoming_props, ctx->wc_ctx, |
| incoming_moved_to_abspath, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Get a copy of the local move target's properties. */ |
| err = svn_wc_prop_list2(&local_props, ctx->wc_ctx, |
| local_moved_to_abspath, |
| scratch_pool, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Create a property diff for the files. */ |
| err = svn_prop_diffs(&propdiffs, incoming_props, local_props, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| /* Perform the file merge. */ |
| err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, |
| ctx->wc_ctx, ancestor_abspath, |
| local_moved_to_abspath, incoming_moved_to_abspath, |
| NULL, NULL, NULL, /* labels */ |
| NULL, NULL, /* conflict versions */ |
| FALSE, /* dry run */ |
| NULL, NULL, /* diff3_cmd, merge_options */ |
| apr_hash_count(ancestor_props) ? ancestor_props : NULL, |
| propdiffs, |
| NULL, NULL, /* conflict func/baton */ |
| NULL, NULL, /* don't allow user to cancel here */ |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| /* Tell the world about the file merge that just happened. */ |
| notify = svn_wc_create_notify(local_moved_to_abspath, |
| svn_wc_notify_update_update, |
| scratch_pool); |
| if (merge_content_outcome == svn_wc_merge_conflict) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| else |
| notify->content_state = svn_wc_notify_state_merged; |
| notify->prop_state = merge_props_outcome; |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| /* Revert the copy-half of the local move. The delete-half of this move |
| * has already been deleted during the update/switch operation. */ |
| err = svn_wc_revert6(ctx->wc_ctx, local_moved_to_abspath, svn_depth_empty, |
| FALSE, NULL, TRUE, FALSE, |
| TRUE /*added_keep_local*/, |
| NULL, NULL, /* no cancellation */ |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| err = svn_wc__del_tree_conflict(ctx->wc_ctx, victim_abspath, scratch_pool); |
| if (err) |
| goto unlock_wc; |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, |
| scratch_pool); |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| svn_io_sleep_for_timestamps(local_moved_to_abspath, scratch_pool); |
| |
| conflict->resolution_tree = option_id; |
| |
| unlock_wc: |
| err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, |
| lock_abspath, |
| scratch_pool)); |
| SVN_ERR(err); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements tree_conflict_get_details_func_t. */ |
| static svn_error_t * |
| conflict_tree_get_details_update_local_moved_away( |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| struct conflict_tree_update_local_moved_away_details *details; |
| const char *incoming_old_repos_relpath; |
| svn_node_kind_t incoming_old_kind; |
| |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, NULL, &incoming_old_kind, |
| conflict, scratch_pool, scratch_pool)); |
| |
| details = apr_pcalloc(conflict->pool, sizeof(*details)); |
| |
| details->wc_move_targets = apr_array_make(conflict->pool, 1, |
| sizeof(const char *)); |
| |
| /* Search the WC for copies of the conflict victim. */ |
| SVN_ERR(svn_wc__find_copies_of_repos_path(&details->wc_move_targets, |
| conflict->local_abspath, |
| incoming_old_repos_relpath, |
| incoming_old_kind, |
| ctx->wc_ctx, |
| conflict->pool, |
| scratch_pool)); |
| |
| conflict->tree_conflict_local_details = details; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| get_both_moved_file_paths(const char **incoming_moved_to_abspath, |
| const char **local_moved_to_abspath, |
| svn_client_conflict_t *conflict, |
| apr_pool_t *scratch_pool) |
| { |
| struct conflict_tree_incoming_delete_details *incoming_details; |
| apr_array_header_t *incoming_move_target_wc_abspaths; |
| svn_wc_operation_t operation; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| |
| *incoming_moved_to_abspath = NULL; |
| *local_moved_to_abspath = NULL; |
| |
| incoming_details = conflict->tree_conflict_incoming_details; |
| if (incoming_details == NULL || incoming_details->moves == NULL || |
| apr_hash_count(incoming_details->wc_move_targets) == 0) |
| return SVN_NO_ERROR; |
| |
| incoming_move_target_wc_abspaths = |
| svn_hash_gets(incoming_details->wc_move_targets, |
| get_moved_to_repos_relpath(incoming_details, |
| scratch_pool)); |
| *incoming_moved_to_abspath = |
| APR_ARRAY_IDX(incoming_move_target_wc_abspaths, |
| incoming_details->wc_move_target_idx, const char *); |
| |
| if (operation == svn_wc_operation_merge) |
| { |
| struct conflict_tree_local_missing_details *local_details; |
| apr_array_header_t *local_moves; |
| |
| local_details = conflict->tree_conflict_local_details; |
| if (local_details == NULL || |
| apr_hash_count(local_details->wc_move_targets) == 0) |
| return SVN_NO_ERROR; |
| |
| local_moves = svn_hash_gets(local_details->wc_move_targets, |
| local_details->move_target_repos_relpath); |
| *local_moved_to_abspath = |
| APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, |
| const char *); |
| } |
| else |
| { |
| struct conflict_tree_update_local_moved_away_details *local_details; |
| |
| local_details = conflict->tree_conflict_local_details; |
| if (local_details == NULL || |
| local_details->wc_move_targets->nelts == 0) |
| return SVN_NO_ERROR; |
| |
| *local_moved_to_abspath = |
| APR_ARRAY_IDX(local_details->wc_move_targets, |
| local_details->preferred_move_target_idx, |
| const char *); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| conflict_tree_get_description_update_both_moved_file_merge( |
| const char **description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *incoming_moved_to_abspath; |
| const char *local_moved_to_abspath; |
| svn_wc_operation_t operation; |
| const char *wcroot_abspath; |
| |
| *description = NULL; |
| |
| SVN_ERR(get_both_moved_file_paths(&incoming_moved_to_abspath, |
| &local_moved_to_abspath, |
| conflict, scratch_pool)); |
| if (incoming_moved_to_abspath == NULL || local_moved_to_abspath == NULL) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| |
| if (operation == svn_wc_operation_merge) |
| { |
| /* In case of a merge, the incoming move has A+ (copied) status... */ |
| *description = |
| apr_psprintf( |
| scratch_pool, |
| _("apply changes to '%s' and revert addition of '%s'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath), |
| scratch_pool), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath), |
| scratch_pool)); |
| } |
| else |
| { |
| /* ...but in case of update/switch the local move has "A+" status. */ |
| *description = |
| apr_psprintf( |
| scratch_pool, |
| _("override incoming move and merge incoming changes from '%s' " |
| "to '%s'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath), |
| scratch_pool), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath), |
| scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| conflict_tree_get_description_update_both_moved_file_move_merge( |
| const char **description, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const char *incoming_moved_to_abspath; |
| const char *local_moved_to_abspath; |
| svn_wc_operation_t operation; |
| const char *wcroot_abspath; |
| |
| *description = NULL; |
| |
| SVN_ERR(get_both_moved_file_paths(&incoming_moved_to_abspath, |
| &local_moved_to_abspath, |
| conflict, scratch_pool)); |
| if (incoming_moved_to_abspath == NULL || local_moved_to_abspath == NULL) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| |
| if (operation == svn_wc_operation_merge) |
| { |
| SVN_ERR(describe_incoming_move_merge_conflict_option( |
| description, conflict, ctx, local_moved_to_abspath, |
| scratch_pool, scratch_pool)); |
| } |
| else |
| { |
| *description = |
| apr_psprintf( |
| scratch_pool, |
| _("accept incoming move and merge local changes from " |
| "'%s' to '%s'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath), |
| scratch_pool), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath), |
| scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'both moved file merge' resolution options for a tree conflict. */ |
| static svn_error_t * |
| configure_option_both_moved_file_merge(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_node_kind_t victim_node_kind; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| svn_node_kind_t incoming_old_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| &incoming_old_kind, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| /* ### what about the switch operation? */ |
| if (((operation == svn_wc_operation_merge && |
| victim_node_kind == svn_node_none) || |
| (operation == svn_wc_operation_update && |
| victim_node_kind == svn_node_file)) && |
| incoming_old_kind == svn_node_file && |
| incoming_new_kind == svn_node_none && |
| ((operation == svn_wc_operation_merge && |
| local_change == svn_wc_conflict_reason_missing) || |
| (operation == svn_wc_operation_update && |
| local_change == svn_wc_conflict_reason_moved_away)) && |
| incoming_change == svn_wc_conflict_action_delete) |
| { |
| const char *description; |
| |
| SVN_ERR(conflict_tree_get_description_update_both_moved_file_merge( |
| &description, conflict, ctx, conflict->pool, scratch_pool)); |
| |
| if (description == NULL) /* details not fetched yet */ |
| return SVN_NO_ERROR; |
| |
| add_resolution_option( |
| options, conflict, svn_client_conflict_option_both_moved_file_merge, |
| _("Merge to corresponding local location"), |
| description, |
| operation == svn_wc_operation_merge ? |
| resolve_both_moved_file_text_merge : |
| resolve_both_moved_file_update_keep_local_move); |
| |
| SVN_ERR(conflict_tree_get_description_update_both_moved_file_move_merge( |
| &description, conflict, ctx, conflict->pool, scratch_pool)); |
| |
| add_resolution_option(options, conflict, |
| svn_client_conflict_option_both_moved_file_move_merge, |
| _("Move and merge"), description, |
| operation == svn_wc_operation_merge ? |
| resolve_incoming_move_file_text_merge : |
| resolve_both_moved_file_update_keep_incoming_move); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Configure 'both moved dir merge' resolution options for a tree conflict. */ |
| static svn_error_t * |
| configure_option_both_moved_dir_merge(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_array_header_t *options, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_operation_t operation; |
| svn_node_kind_t victim_node_kind; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| const char *incoming_old_repos_relpath; |
| svn_revnum_t incoming_old_pegrev; |
| svn_node_kind_t incoming_old_kind; |
| const char *incoming_new_repos_relpath; |
| svn_revnum_t incoming_new_pegrev; |
| svn_node_kind_t incoming_new_kind; |
| const char *wcroot_abspath; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, |
| conflict->local_abspath, scratch_pool, |
| scratch_pool)); |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( |
| &incoming_old_repos_relpath, &incoming_old_pegrev, |
| &incoming_old_kind, conflict, scratch_pool, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( |
| &incoming_new_repos_relpath, &incoming_new_pegrev, |
| &incoming_new_kind, conflict, scratch_pool, |
| scratch_pool)); |
| |
| if (operation == svn_wc_operation_merge && |
| victim_node_kind == svn_node_none && |
| incoming_old_kind == svn_node_dir && |
| incoming_new_kind == svn_node_none && |
| local_change == svn_wc_conflict_reason_missing && |
| incoming_change == svn_wc_conflict_action_delete) |
| { |
| struct conflict_tree_incoming_delete_details *incoming_details; |
| struct conflict_tree_local_missing_details *local_details; |
| const char *description; |
| apr_array_header_t *local_moves; |
| const char *local_moved_to_abspath; |
| const char *incoming_moved_to_abspath; |
| apr_array_header_t *incoming_move_target_wc_abspaths; |
| |
| incoming_details = conflict->tree_conflict_incoming_details; |
| if (incoming_details == NULL || incoming_details->moves == NULL || |
| apr_hash_count(incoming_details->wc_move_targets) == 0) |
| return SVN_NO_ERROR; |
| |
| local_details = conflict->tree_conflict_local_details; |
| if (local_details == NULL || |
| apr_hash_count(local_details->wc_move_targets) == 0) |
| return SVN_NO_ERROR; |
| |
| local_moves = svn_hash_gets(local_details->wc_move_targets, |
| local_details->move_target_repos_relpath); |
| local_moved_to_abspath = |
| APR_ARRAY_IDX(local_moves, local_details->wc_move_target_idx, |
| const char *); |
| |
| incoming_move_target_wc_abspaths = |
| svn_hash_gets(incoming_details->wc_move_targets, |
| get_moved_to_repos_relpath(incoming_details, |
| scratch_pool)); |
| incoming_moved_to_abspath = |
| APR_ARRAY_IDX(incoming_move_target_wc_abspaths, |
| incoming_details->wc_move_target_idx, const char *); |
| |
| description = |
| apr_psprintf( |
| scratch_pool, _("apply changes to '%s' and revert addition of '%s'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, local_moved_to_abspath), |
| scratch_pool), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, incoming_moved_to_abspath), |
| scratch_pool)); |
| add_resolution_option( |
| options, conflict, svn_client_conflict_option_both_moved_dir_merge, |
| _("Merge to corresponding local location"), |
| description, resolve_both_moved_dir_merge); |
| |
| SVN_ERR(describe_incoming_move_merge_conflict_option( |
| &description, conflict, ctx, local_moved_to_abspath, |
| scratch_pool, scratch_pool)); |
| add_resolution_option(options, conflict, |
| svn_client_conflict_option_both_moved_dir_move_merge, |
| _("Move and merge"), description, |
| resolve_both_moved_dir_move_merge); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return a copy of the repos replath candidate list. */ |
| static svn_error_t * |
| get_repos_relpath_candidates( |
| apr_array_header_t **possible_moved_to_repos_relpaths, |
| apr_hash_t *wc_move_targets, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *sorted_repos_relpaths; |
| int i; |
| |
| sorted_repos_relpaths = svn_sort__hash(wc_move_targets, |
| svn_sort_compare_items_as_paths, |
| scratch_pool); |
| |
| *possible_moved_to_repos_relpaths = |
| apr_array_make(result_pool, sorted_repos_relpaths->nelts, |
| sizeof (const char *)); |
| for (i = 0; i < sorted_repos_relpaths->nelts; i++) |
| { |
| svn_sort__item_t item; |
| const char *repos_relpath; |
| |
| item = APR_ARRAY_IDX(sorted_repos_relpaths, i, svn_sort__item_t); |
| repos_relpath = item.key; |
| APR_ARRAY_PUSH(*possible_moved_to_repos_relpaths, const char *) = |
| apr_pstrdup(result_pool, repos_relpath); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_option_get_moved_to_repos_relpath_candidates2( |
| apr_array_header_t **possible_moved_to_repos_relpaths, |
| svn_client_conflict_option_t *option, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_t *conflict = option->conflict; |
| const char *victim_abspath; |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| svn_client_conflict_option_id_t id; |
| |
| id = svn_client_conflict_option_get_id(option); |
| if (id != svn_client_conflict_option_incoming_move_file_text_merge && |
| id != svn_client_conflict_option_incoming_move_dir_merge && |
| id != svn_client_conflict_option_local_move_file_text_merge && |
| id != svn_client_conflict_option_local_move_dir_merge && |
| id != svn_client_conflict_option_sibling_move_file_text_merge && |
| id != svn_client_conflict_option_sibling_move_dir_merge && |
| id != svn_client_conflict_option_both_moved_file_merge && |
| id != svn_client_conflict_option_both_moved_file_move_merge && |
| id != svn_client_conflict_option_both_moved_dir_merge && |
| id != svn_client_conflict_option_both_moved_dir_move_merge) |
| { |
| /* We cannot operate on this option. */ |
| *possible_moved_to_repos_relpaths = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| if (operation == svn_wc_operation_merge && |
| incoming_change == svn_wc_conflict_action_edit && |
| local_change == svn_wc_conflict_reason_missing) |
| { |
| struct conflict_tree_local_missing_details *details; |
| |
| details = conflict->tree_conflict_local_details; |
| if (details == NULL || |
| (details->wc_move_targets == NULL && details->wc_siblings == NULL)) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Getting a list of possible move targets " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| if (details->wc_move_targets) |
| SVN_ERR(get_repos_relpath_candidates(possible_moved_to_repos_relpaths, |
| details->wc_move_targets, |
| result_pool, scratch_pool)); |
| else |
| *possible_moved_to_repos_relpaths = NULL; |
| } |
| else |
| { |
| struct conflict_tree_incoming_delete_details *details; |
| |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL || details->wc_move_targets == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Getting a list of possible move targets " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| SVN_ERR(get_repos_relpath_candidates(possible_moved_to_repos_relpaths, |
| details->wc_move_targets, |
| result_pool, scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_option_get_moved_to_repos_relpath_candidates( |
| apr_array_header_t **possible_moved_to_repos_relpaths, |
| svn_client_conflict_option_t *option, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| /* The only difference to API version 2 is an assertion failure if |
| * an unexpected option is passed. |
| * We do not emulate this old behaviour since clients written against |
| * the previous API will just keep working. */ |
| return svn_error_trace( |
| svn_client_conflict_option_get_moved_to_repos_relpath_candidates2( |
| possible_moved_to_repos_relpaths, option, result_pool, scratch_pool)); |
| } |
| |
| static svn_error_t * |
| set_wc_move_target(const char **new_hash_key, |
| apr_hash_t *wc_move_targets, |
| int preferred_move_target_idx, |
| const char *victim_abspath, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *move_target_repos_relpaths; |
| svn_sort__item_t item; |
| const char *move_target_repos_relpath; |
| apr_hash_index_t *hi; |
| |
| if (preferred_move_target_idx < 0 || |
| preferred_move_target_idx >= apr_hash_count(wc_move_targets)) |
| return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, |
| _("Index '%d' is out of bounds of the possible " |
| "move target list for '%s'"), |
| preferred_move_target_idx, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| /* Translate the index back into a hash table key. */ |
| move_target_repos_relpaths = svn_sort__hash(wc_move_targets, |
| svn_sort_compare_items_as_paths, |
| scratch_pool); |
| item = APR_ARRAY_IDX(move_target_repos_relpaths, preferred_move_target_idx, |
| svn_sort__item_t); |
| move_target_repos_relpath = item.key; |
| /* Find our copy of the hash key and remember the user's preference. */ |
| for (hi = apr_hash_first(scratch_pool, wc_move_targets); |
| hi != NULL; |
| hi = apr_hash_next(hi)) |
| { |
| const char *repos_relpath = apr_hash_this_key(hi); |
| |
| if (strcmp(move_target_repos_relpath, repos_relpath) == 0) |
| { |
| *new_hash_key = repos_relpath; |
| return SVN_NO_ERROR; |
| } |
| } |
| |
| return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, |
| _("Repository path '%s' not found in list of " |
| "possible move targets for '%s'"), |
| move_target_repos_relpath, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| } |
| |
| svn_error_t * |
| svn_client_conflict_option_set_moved_to_repos_relpath2( |
| svn_client_conflict_option_t *option, |
| int preferred_move_target_idx, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_t *conflict = option->conflict; |
| const char *victim_abspath; |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| svn_client_conflict_option_id_t id; |
| |
| id = svn_client_conflict_option_get_id(option); |
| if (id != svn_client_conflict_option_incoming_move_file_text_merge && |
| id != svn_client_conflict_option_incoming_move_dir_merge && |
| id != svn_client_conflict_option_local_move_file_text_merge && |
| id != svn_client_conflict_option_local_move_dir_merge && |
| id != svn_client_conflict_option_sibling_move_file_text_merge && |
| id != svn_client_conflict_option_sibling_move_dir_merge && |
| id != svn_client_conflict_option_both_moved_file_merge && |
| id != svn_client_conflict_option_both_moved_file_move_merge && |
| id != svn_client_conflict_option_both_moved_dir_merge && |
| id != svn_client_conflict_option_both_moved_dir_move_merge) |
| return SVN_NO_ERROR; /* We cannot operate on this option. Nothing to do. */ |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| if (operation == svn_wc_operation_merge && |
| incoming_change == svn_wc_conflict_action_edit && |
| local_change == svn_wc_conflict_reason_missing) |
| { |
| struct conflict_tree_local_missing_details *details; |
| |
| details = conflict->tree_conflict_local_details; |
| if (details == NULL || details->wc_move_targets == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Setting a move target requires details " |
| "for tree conflict at '%s' to be fetched " |
| "from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| SVN_ERR(set_wc_move_target(&details->move_target_repos_relpath, |
| details->wc_move_targets, |
| preferred_move_target_idx, |
| victim_abspath, scratch_pool)); |
| details->wc_move_target_idx = 0; |
| |
| /* Update option description. */ |
| SVN_ERR(conflict_tree_get_description_local_missing( |
| &option->description, conflict, ctx, |
| conflict->pool, scratch_pool)); |
| } |
| else |
| { |
| struct conflict_tree_incoming_delete_details *details; |
| apr_array_header_t *move_target_wc_abspaths; |
| const char *moved_to_abspath; |
| |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL || details->wc_move_targets == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Setting a move target requires details " |
| "for tree conflict at '%s' to be fetched " |
| "from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| SVN_ERR(set_wc_move_target(&details->move_target_repos_relpath, |
| details->wc_move_targets, |
| preferred_move_target_idx, |
| victim_abspath, scratch_pool)); |
| details->wc_move_target_idx = 0; |
| |
| /* Update option description. */ |
| move_target_wc_abspaths = |
| svn_hash_gets(details->wc_move_targets, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, |
| details->wc_move_target_idx, |
| const char *); |
| SVN_ERR(describe_incoming_move_merge_conflict_option( |
| &option->description, |
| conflict, ctx, |
| moved_to_abspath, |
| conflict->pool, |
| scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_option_set_moved_to_repos_relpath( |
| svn_client_conflict_option_t *option, |
| int preferred_move_target_idx, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| /* The only difference to API version 2 is an assertion failure if |
| * an unexpected option is passed. |
| * We do not emulate this old behaviour since clients written against |
| * the previous API will just keep working. */ |
| return svn_error_trace( |
| svn_client_conflict_option_set_moved_to_repos_relpath2(option, |
| preferred_move_target_idx, ctx, scratch_pool)); |
| } |
| |
| svn_error_t * |
| svn_client_conflict_option_get_moved_to_abspath_candidates2( |
| apr_array_header_t **possible_moved_to_abspaths, |
| svn_client_conflict_option_t *option, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_t *conflict = option->conflict; |
| const char *victim_abspath; |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| int i; |
| svn_client_conflict_option_id_t id; |
| |
| id = svn_client_conflict_option_get_id(option); |
| if (id != svn_client_conflict_option_incoming_move_file_text_merge && |
| id != svn_client_conflict_option_incoming_move_dir_merge && |
| id != svn_client_conflict_option_local_move_file_text_merge && |
| id != svn_client_conflict_option_local_move_dir_merge && |
| id != svn_client_conflict_option_sibling_move_file_text_merge && |
| id != svn_client_conflict_option_sibling_move_dir_merge && |
| id != svn_client_conflict_option_both_moved_file_merge && |
| id != svn_client_conflict_option_both_moved_file_move_merge && |
| id != svn_client_conflict_option_both_moved_dir_merge && |
| id != svn_client_conflict_option_both_moved_dir_move_merge) |
| { |
| /* We cannot operate on this option. */ |
| *possible_moved_to_abspaths = NULL; |
| return NULL; |
| } |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| if (operation == svn_wc_operation_merge && |
| incoming_change == svn_wc_conflict_action_edit && |
| local_change == svn_wc_conflict_reason_missing) |
| { |
| struct conflict_tree_local_missing_details *details; |
| |
| details = conflict->tree_conflict_local_details; |
| if (details == NULL || |
| (details->wc_move_targets == NULL && details->wc_siblings == NULL)) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Getting a list of possible move siblings " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| *possible_moved_to_abspaths = apr_array_make(result_pool, 1, |
| sizeof (const char *)); |
| if (details->wc_move_targets) |
| { |
| apr_array_header_t *move_target_wc_abspaths; |
| move_target_wc_abspaths = |
| svn_hash_gets(details->wc_move_targets, |
| details->move_target_repos_relpath); |
| for (i = 0; i < move_target_wc_abspaths->nelts; i++) |
| { |
| const char *moved_to_abspath; |
| |
| moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, i, |
| const char *); |
| APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) = |
| apr_pstrdup(result_pool, moved_to_abspath); |
| } |
| } |
| |
| /* ### Siblings are actually 'corresponding nodes', not 'move targets'. |
| ### But we provide them here to avoid another API function. */ |
| if (details->wc_siblings) |
| { |
| for (i = 0; i < details->wc_siblings->nelts; i++) |
| { |
| const char *sibling_abspath; |
| |
| sibling_abspath = APR_ARRAY_IDX(details->wc_siblings, i, |
| const char *); |
| APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) = |
| apr_pstrdup(result_pool, sibling_abspath); |
| } |
| } |
| } |
| else if ((operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) && |
| incoming_change == svn_wc_conflict_action_delete && |
| local_change == svn_wc_conflict_reason_moved_away) |
| { |
| struct conflict_tree_update_local_moved_away_details *details; |
| |
| details = conflict->tree_conflict_local_details; |
| if (details == NULL || details->wc_move_targets == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Getting a list of possible move targets " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| /* Return a copy of the option's move target candidate list. */ |
| *possible_moved_to_abspaths = |
| apr_array_make(result_pool, details->wc_move_targets->nelts, |
| sizeof (const char *)); |
| for (i = 0; i < details->wc_move_targets->nelts; i++) |
| { |
| const char *moved_to_abspath; |
| |
| moved_to_abspath = APR_ARRAY_IDX(details->wc_move_targets, i, |
| const char *); |
| APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) = |
| apr_pstrdup(result_pool, moved_to_abspath); |
| } |
| } |
| else |
| { |
| struct conflict_tree_incoming_delete_details *details; |
| apr_array_header_t *move_target_wc_abspaths; |
| |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL || details->wc_move_targets == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Getting a list of possible move targets " |
| "requires details for tree conflict at '%s' " |
| "to be fetched from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| move_target_wc_abspaths = |
| svn_hash_gets(details->wc_move_targets, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| |
| /* Return a copy of the option's move target candidate list. */ |
| *possible_moved_to_abspaths = |
| apr_array_make(result_pool, move_target_wc_abspaths->nelts, |
| sizeof (const char *)); |
| for (i = 0; i < move_target_wc_abspaths->nelts; i++) |
| { |
| const char *moved_to_abspath; |
| |
| moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, i, |
| const char *); |
| APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) = |
| apr_pstrdup(result_pool, moved_to_abspath); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_option_get_moved_to_abspath_candidates( |
| apr_array_header_t **possible_moved_to_abspaths, |
| svn_client_conflict_option_t *option, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| /* The only difference to API version 2 is an assertion failure if |
| * an unexpected option is passed. |
| * We do not emulate this old behaviour since clients written against |
| * the previous API will just keep working. */ |
| return svn_error_trace( |
| svn_client_conflict_option_get_moved_to_abspath_candidates2( |
| possible_moved_to_abspaths, option, result_pool, scratch_pool)); |
| } |
| |
| svn_error_t * |
| svn_client_conflict_option_set_moved_to_abspath2( |
| svn_client_conflict_option_t *option, |
| int preferred_move_target_idx, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_client_conflict_t *conflict = option->conflict; |
| const char *victim_abspath; |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| svn_client_conflict_option_id_t id; |
| |
| id = svn_client_conflict_option_get_id(option); |
| if (id != svn_client_conflict_option_incoming_move_file_text_merge && |
| id != svn_client_conflict_option_incoming_move_dir_merge && |
| id != svn_client_conflict_option_local_move_file_text_merge && |
| id != svn_client_conflict_option_local_move_dir_merge && |
| id != svn_client_conflict_option_sibling_move_file_text_merge && |
| id != svn_client_conflict_option_sibling_move_dir_merge && |
| id != svn_client_conflict_option_both_moved_file_merge && |
| id != svn_client_conflict_option_both_moved_file_move_merge && |
| id != svn_client_conflict_option_both_moved_dir_merge && |
| id != svn_client_conflict_option_both_moved_dir_move_merge) |
| return NULL; /* We cannot operate on this option. Nothing to do. */ |
| |
| victim_abspath = svn_client_conflict_get_local_abspath(conflict); |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| if (operation == svn_wc_operation_merge && |
| incoming_change == svn_wc_conflict_action_edit && |
| local_change == svn_wc_conflict_reason_missing) |
| { |
| struct conflict_tree_local_missing_details *details; |
| const char *wcroot_abspath; |
| const char *preferred_sibling; |
| |
| SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, |
| ctx->wc_ctx, |
| conflict->local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| |
| details = conflict->tree_conflict_local_details; |
| if (details == NULL || (details->wc_siblings == NULL && |
| details->wc_move_targets == NULL)) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Setting a move target requires details " |
| "for tree conflict at '%s' to be fetched " |
| "from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| if (details->wc_siblings) |
| { |
| if (preferred_move_target_idx < 0 || |
| preferred_move_target_idx > details->wc_siblings->nelts) |
| return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, |
| _("Index '%d' is out of bounds of the " |
| "possible move sibling list for '%s'"), |
| preferred_move_target_idx, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| /* Record the user's preference. */ |
| details->preferred_sibling_idx = preferred_move_target_idx; |
| |
| /* Update option description. */ |
| preferred_sibling = APR_ARRAY_IDX(details->wc_siblings, |
| details->preferred_sibling_idx, |
| const char *); |
| option->description = |
| apr_psprintf( |
| conflict->pool, _("apply changes to '%s'"), |
| svn_dirent_local_style( |
| svn_dirent_skip_ancestor(wcroot_abspath, preferred_sibling), |
| scratch_pool)); |
| } |
| else if (details->wc_move_targets) |
| { |
| apr_array_header_t *move_target_wc_abspaths; |
| move_target_wc_abspaths = |
| svn_hash_gets(details->wc_move_targets, |
| details->move_target_repos_relpath); |
| |
| if (preferred_move_target_idx < 0 || |
| preferred_move_target_idx > move_target_wc_abspaths->nelts) |
| return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, |
| _("Index '%d' is out of bounds of the possible " |
| "move target list for '%s'"), |
| preferred_move_target_idx, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| /* Record the user's preference. */ |
| details->wc_move_target_idx = preferred_move_target_idx; |
| |
| /* Update option description. */ |
| SVN_ERR(conflict_tree_get_description_local_missing( |
| &option->description, conflict, ctx, |
| conflict->pool, scratch_pool)); |
| } |
| } |
| else if ((operation == svn_wc_operation_update || |
| operation == svn_wc_operation_switch) && |
| incoming_change == svn_wc_conflict_action_delete && |
| local_change == svn_wc_conflict_reason_moved_away) |
| { |
| struct conflict_tree_update_local_moved_away_details *details; |
| |
| details = conflict->tree_conflict_local_details; |
| if (details == NULL || details->wc_move_targets == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Setting a move target requires details " |
| "for tree conflict at '%s' to be fetched " |
| "from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| if (preferred_move_target_idx < 0 || |
| preferred_move_target_idx > details->wc_move_targets->nelts) |
| return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, |
| _("Index '%d' is out of bounds of the " |
| "possible move target list for '%s'"), |
| preferred_move_target_idx, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| /* Record the user's preference. */ |
| details->preferred_move_target_idx = preferred_move_target_idx; |
| |
| /* Update option description. */ |
| if (id == svn_client_conflict_option_both_moved_file_merge) |
| SVN_ERR(conflict_tree_get_description_update_both_moved_file_merge( |
| &option->description, conflict, ctx, conflict->pool, |
| scratch_pool)); |
| else if (id == svn_client_conflict_option_both_moved_file_move_merge) |
| SVN_ERR(conflict_tree_get_description_update_both_moved_file_move_merge( |
| &option->description, conflict, ctx, conflict->pool, scratch_pool)); |
| #if 0 /* ### TODO: Also handle options for directories! */ |
| else if (id == svn_client_conflict_option_both_moved_dir_merge) |
| { |
| } |
| else if (id == svn_client_conflict_option_both_moved_dir_move_merge) |
| { |
| } |
| #endif |
| else |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Unexpected option id '%d'"), id); |
| } |
| else |
| { |
| struct conflict_tree_incoming_delete_details *details; |
| apr_array_header_t *move_target_wc_abspaths; |
| const char *moved_to_abspath; |
| |
| details = conflict->tree_conflict_incoming_details; |
| if (details == NULL || details->wc_move_targets == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Setting a move target requires details " |
| "for tree conflict at '%s' to be fetched " |
| "from the repository first"), |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| move_target_wc_abspaths = |
| svn_hash_gets(details->wc_move_targets, |
| get_moved_to_repos_relpath(details, scratch_pool)); |
| |
| if (preferred_move_target_idx < 0 || |
| preferred_move_target_idx > move_target_wc_abspaths->nelts) |
| return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, |
| _("Index '%d' is out of bounds of the possible " |
| "move target list for '%s'"), |
| preferred_move_target_idx, |
| svn_dirent_local_style(victim_abspath, |
| scratch_pool)); |
| |
| /* Record the user's preference. */ |
| details->wc_move_target_idx = preferred_move_target_idx; |
| |
| /* Update option description. */ |
| moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, |
| details->wc_move_target_idx, |
| const char *); |
| SVN_ERR(describe_incoming_move_merge_conflict_option(&option->description, |
| conflict, ctx, |
| moved_to_abspath, |
| conflict->pool, |
| scratch_pool)); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_option_set_moved_to_abspath( |
| svn_client_conflict_option_t *option, |
| int preferred_move_target_idx, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| /* The only difference to API version 2 is an assertion failure if |
| * an unexpected option is passed. |
| * We do not emulate this old behaviour since clients written against |
| * the previous API will just keep working. */ |
| return svn_error_trace( |
| svn_client_conflict_option_set_moved_to_abspath2(option, |
| preferred_move_target_idx, ctx, scratch_pool)); |
| } |
| |
| svn_error_t * |
| svn_client_conflict_tree_get_resolution_options(apr_array_header_t **options, |
| svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| SVN_ERR(assert_tree_conflict(conflict, scratch_pool)); |
| |
| *options = apr_array_make(result_pool, 2, |
| sizeof(svn_client_conflict_option_t *)); |
| |
| /* Add postpone option. */ |
| add_resolution_option(*options, conflict, |
| svn_client_conflict_option_postpone, |
| _("Postpone"), |
| _("skip this conflict and leave it unresolved"), |
| resolve_postpone); |
| |
| /* Add an option which marks the conflict resolved. */ |
| SVN_ERR(configure_option_accept_current_wc_state(conflict, *options)); |
| |
| /* Configure options which offer automatic resolution. */ |
| SVN_ERR(configure_option_update_move_destination(conflict, *options)); |
| SVN_ERR(configure_option_update_raise_moved_away_children(conflict, |
| *options)); |
| SVN_ERR(configure_option_incoming_add_ignore(conflict, ctx, *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_incoming_added_file_text_merge(conflict, ctx, |
| *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_incoming_added_file_replace_and_merge(conflict, |
| ctx, |
| *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_incoming_added_dir_merge(conflict, ctx, |
| *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_incoming_added_dir_replace(conflict, ctx, |
| *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_incoming_added_dir_replace_and_merge(conflict, |
| ctx, |
| *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_incoming_delete_ignore(conflict, ctx, *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_incoming_delete_accept(conflict, ctx, *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_incoming_move_file_merge(conflict, ctx, *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_incoming_dir_merge(conflict, ctx, *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_local_move_file_or_dir_merge(conflict, ctx, |
| *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_sibling_move_merge(conflict, ctx, *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_both_moved_file_merge(conflict, ctx, *options, |
| scratch_pool)); |
| SVN_ERR(configure_option_both_moved_dir_merge(conflict, ctx, *options, |
| scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Swallow authz failures and return SVN_NO_ERROR in that case. |
| * Otherwise, return ERR unchanged. */ |
| static svn_error_t * |
| ignore_authz_failures(svn_error_t *err) |
| { |
| if (err && ( svn_error_find_cause(err, SVN_ERR_AUTHZ_UNREADABLE) |
| || svn_error_find_cause(err, SVN_ERR_RA_NOT_AUTHORIZED) |
| || svn_error_find_cause(err, SVN_ERR_RA_DAV_FORBIDDEN))) |
| { |
| svn_error_clear(err); |
| err = SVN_NO_ERROR; |
| } |
| |
| return err; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_tree_get_details(svn_client_conflict_t *conflict, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| SVN_ERR(assert_tree_conflict(conflict, scratch_pool)); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify( |
| svn_client_conflict_get_local_abspath(conflict), |
| svn_wc_notify_begin_search_tree_conflict_details, |
| scratch_pool), |
| ctx->notify_func2(ctx->notify_baton2, notify, |
| scratch_pool); |
| } |
| |
| /* Collecting conflict details may fail due to insufficient access rights. |
| * This is not a failure but simply restricts our future options. */ |
| if (conflict->tree_conflict_get_incoming_details_func) |
| SVN_ERR(ignore_authz_failures( |
| conflict->tree_conflict_get_incoming_details_func(conflict, ctx, |
| scratch_pool))); |
| |
| |
| if (conflict->tree_conflict_get_local_details_func) |
| SVN_ERR(ignore_authz_failures( |
| conflict->tree_conflict_get_local_details_func(conflict, ctx, |
| scratch_pool))); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify( |
| svn_client_conflict_get_local_abspath(conflict), |
| svn_wc_notify_end_search_tree_conflict_details, |
| scratch_pool), |
| ctx->notify_func2(ctx->notify_baton2, notify, |
| scratch_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_client_conflict_option_id_t |
| svn_client_conflict_option_get_id(svn_client_conflict_option_t *option) |
| { |
| return option->id; |
| } |
| |
| const char * |
| svn_client_conflict_option_get_label(svn_client_conflict_option_t *option, |
| apr_pool_t *result_pool) |
| { |
| return apr_pstrdup(result_pool, option->label); |
| } |
| |
| const char * |
| svn_client_conflict_option_get_description(svn_client_conflict_option_t *option, |
| apr_pool_t *result_pool) |
| { |
| return apr_pstrdup(result_pool, option->description); |
| } |
| |
| svn_client_conflict_option_id_t |
| svn_client_conflict_get_recommended_option_id(svn_client_conflict_t *conflict) |
| { |
| return conflict->recommended_option_id; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_text_resolve(svn_client_conflict_t *conflict, |
| svn_client_conflict_option_t *option, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| SVN_ERR(assert_text_conflict(conflict, scratch_pool)); |
| SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_client_conflict_option_t * |
| svn_client_conflict_option_find_by_id(apr_array_header_t *options, |
| svn_client_conflict_option_id_t option_id) |
| { |
| int i; |
| |
| for (i = 0; i < options->nelts; i++) |
| { |
| svn_client_conflict_option_t *this_option; |
| svn_client_conflict_option_id_t this_option_id; |
| |
| this_option = APR_ARRAY_IDX(options, i, svn_client_conflict_option_t *); |
| this_option_id = svn_client_conflict_option_get_id(this_option); |
| |
| if (this_option_id == option_id) |
| return this_option; |
| } |
| |
| return NULL; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_text_resolve_by_id( |
| svn_client_conflict_t *conflict, |
| svn_client_conflict_option_id_t option_id, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *resolution_options; |
| svn_client_conflict_option_t *option; |
| |
| SVN_ERR(svn_client_conflict_text_get_resolution_options( |
| &resolution_options, conflict, ctx, |
| scratch_pool, scratch_pool)); |
| option = svn_client_conflict_option_find_by_id(resolution_options, |
| option_id); |
| if (option == NULL) |
| return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE, |
| NULL, |
| _("Inapplicable conflict resolution option " |
| "given for conflicted path '%s'"), |
| svn_dirent_local_style(conflict->local_abspath, |
| scratch_pool)); |
| |
| SVN_ERR(svn_client_conflict_text_resolve(conflict, option, ctx, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_client_conflict_option_id_t |
| svn_client_conflict_text_get_resolution(svn_client_conflict_t *conflict) |
| { |
| return conflict->resolution_text; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_prop_resolve(svn_client_conflict_t *conflict, |
| const char *propname, |
| svn_client_conflict_option_t *option, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| SVN_ERR(assert_prop_conflict(conflict, scratch_pool)); |
| option->type_data.prop.propname = propname; |
| SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_prop_resolve_by_id( |
| svn_client_conflict_t *conflict, |
| const char *propname, |
| svn_client_conflict_option_id_t option_id, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *resolution_options; |
| svn_client_conflict_option_t *option; |
| |
| SVN_ERR(svn_client_conflict_prop_get_resolution_options( |
| &resolution_options, conflict, ctx, |
| scratch_pool, scratch_pool)); |
| option = svn_client_conflict_option_find_by_id(resolution_options, |
| option_id); |
| if (option == NULL) |
| return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE, |
| NULL, |
| _("Inapplicable conflict resolution option " |
| "given for conflicted path '%s'"), |
| svn_dirent_local_style(conflict->local_abspath, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_prop_resolve(conflict, propname, option, ctx, |
| scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_client_conflict_option_id_t |
| svn_client_conflict_prop_get_resolution(svn_client_conflict_t *conflict, |
| const char *propname) |
| { |
| svn_client_conflict_option_t *option; |
| |
| option = svn_hash_gets(conflict->resolved_props, propname); |
| if (option == NULL) |
| return svn_client_conflict_option_unspecified; |
| |
| return svn_client_conflict_option_get_id(option); |
| } |
| |
| svn_error_t * |
| svn_client_conflict_tree_resolve(svn_client_conflict_t *conflict, |
| svn_client_conflict_option_t *option, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| SVN_ERR(assert_tree_conflict(conflict, scratch_pool)); |
| SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_tree_resolve_by_id( |
| svn_client_conflict_t *conflict, |
| svn_client_conflict_option_id_t option_id, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| apr_array_header_t *resolution_options; |
| svn_client_conflict_option_t *option; |
| |
| SVN_ERR(svn_client_conflict_tree_get_resolution_options( |
| &resolution_options, conflict, ctx, |
| scratch_pool, scratch_pool)); |
| option = svn_client_conflict_option_find_by_id(resolution_options, |
| option_id); |
| if (option == NULL) |
| return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE, |
| NULL, |
| _("Inapplicable conflict resolution option " |
| "given for conflicted path '%s'"), |
| svn_dirent_local_style(conflict->local_abspath, |
| scratch_pool)); |
| SVN_ERR(svn_client_conflict_tree_resolve(conflict, option, ctx, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_client_conflict_option_id_t |
| svn_client_conflict_tree_get_resolution(svn_client_conflict_t *conflict) |
| { |
| return conflict->resolution_tree; |
| } |
| |
| /* Return the legacy conflict descriptor which is wrapped by CONFLICT. */ |
| static const svn_wc_conflict_description2_t * |
| get_conflict_desc2_t(svn_client_conflict_t *conflict) |
| { |
| if (conflict->legacy_text_conflict) |
| return conflict->legacy_text_conflict; |
| |
| if (conflict->legacy_tree_conflict) |
| return conflict->legacy_tree_conflict; |
| |
| if (conflict->prop_conflicts && conflict->legacy_prop_conflict_propname) |
| return svn_hash_gets(conflict->prop_conflicts, |
| conflict->legacy_prop_conflict_propname); |
| |
| return NULL; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_get_conflicted(svn_boolean_t *text_conflicted, |
| apr_array_header_t **props_conflicted, |
| svn_boolean_t *tree_conflicted, |
| svn_client_conflict_t *conflict, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| if (text_conflicted) |
| *text_conflicted = (conflict->legacy_text_conflict != NULL); |
| |
| if (props_conflicted) |
| { |
| if (conflict->prop_conflicts) |
| SVN_ERR(svn_hash_keys(props_conflicted, conflict->prop_conflicts, |
| result_pool)); |
| else |
| *props_conflicted = apr_array_make(result_pool, 0, |
| sizeof(const char*)); |
| } |
| |
| if (tree_conflicted) |
| *tree_conflicted = (conflict->legacy_tree_conflict != NULL); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| const char * |
| svn_client_conflict_get_local_abspath(svn_client_conflict_t *conflict) |
| { |
| return conflict->local_abspath; |
| } |
| |
| svn_wc_operation_t |
| svn_client_conflict_get_operation(svn_client_conflict_t *conflict) |
| { |
| return get_conflict_desc2_t(conflict)->operation; |
| } |
| |
| svn_wc_conflict_action_t |
| svn_client_conflict_get_incoming_change(svn_client_conflict_t *conflict) |
| { |
| return get_conflict_desc2_t(conflict)->action; |
| } |
| |
| svn_wc_conflict_reason_t |
| svn_client_conflict_get_local_change(svn_client_conflict_t *conflict) |
| { |
| return get_conflict_desc2_t(conflict)->reason; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_get_repos_info(const char **repos_root_url, |
| const char **repos_uuid, |
| svn_client_conflict_t *conflict, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| if (repos_root_url) |
| { |
| if (get_conflict_desc2_t(conflict)->src_left_version) |
| *repos_root_url = |
| get_conflict_desc2_t(conflict)->src_left_version->repos_url; |
| else if (get_conflict_desc2_t(conflict)->src_right_version) |
| *repos_root_url = |
| get_conflict_desc2_t(conflict)->src_right_version->repos_url; |
| else |
| *repos_root_url = NULL; |
| } |
| |
| if (repos_uuid) |
| { |
| if (get_conflict_desc2_t(conflict)->src_left_version) |
| *repos_uuid = |
| get_conflict_desc2_t(conflict)->src_left_version->repos_uuid; |
| else if (get_conflict_desc2_t(conflict)->src_right_version) |
| *repos_uuid = |
| get_conflict_desc2_t(conflict)->src_right_version->repos_uuid; |
| else |
| *repos_uuid = NULL; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_get_incoming_old_repos_location( |
| const char **incoming_old_repos_relpath, |
| svn_revnum_t *incoming_old_pegrev, |
| svn_node_kind_t *incoming_old_node_kind, |
| svn_client_conflict_t *conflict, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| if (incoming_old_repos_relpath) |
| { |
| if (get_conflict_desc2_t(conflict)->src_left_version) |
| *incoming_old_repos_relpath = |
| get_conflict_desc2_t(conflict)->src_left_version->path_in_repos; |
| else |
| *incoming_old_repos_relpath = NULL; |
| } |
| |
| if (incoming_old_pegrev) |
| { |
| if (get_conflict_desc2_t(conflict)->src_left_version) |
| *incoming_old_pegrev = |
| get_conflict_desc2_t(conflict)->src_left_version->peg_rev; |
| else |
| *incoming_old_pegrev = SVN_INVALID_REVNUM; |
| } |
| |
| if (incoming_old_node_kind) |
| { |
| if (get_conflict_desc2_t(conflict)->src_left_version) |
| *incoming_old_node_kind = |
| get_conflict_desc2_t(conflict)->src_left_version->node_kind; |
| else |
| *incoming_old_node_kind = svn_node_none; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_get_incoming_new_repos_location( |
| const char **incoming_new_repos_relpath, |
| svn_revnum_t *incoming_new_pegrev, |
| svn_node_kind_t *incoming_new_node_kind, |
| svn_client_conflict_t *conflict, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| if (incoming_new_repos_relpath) |
| { |
| if (get_conflict_desc2_t(conflict)->src_right_version) |
| *incoming_new_repos_relpath = |
| get_conflict_desc2_t(conflict)->src_right_version->path_in_repos; |
| else |
| *incoming_new_repos_relpath = NULL; |
| } |
| |
| if (incoming_new_pegrev) |
| { |
| if (get_conflict_desc2_t(conflict)->src_right_version) |
| *incoming_new_pegrev = |
| get_conflict_desc2_t(conflict)->src_right_version->peg_rev; |
| else |
| *incoming_new_pegrev = SVN_INVALID_REVNUM; |
| } |
| |
| if (incoming_new_node_kind) |
| { |
| if (get_conflict_desc2_t(conflict)->src_right_version) |
| *incoming_new_node_kind = |
| get_conflict_desc2_t(conflict)->src_right_version->node_kind; |
| else |
| *incoming_new_node_kind = svn_node_none; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_node_kind_t |
| svn_client_conflict_tree_get_victim_node_kind(svn_client_conflict_t *conflict) |
| { |
| SVN_ERR_ASSERT_NO_RETURN(assert_tree_conflict(conflict, conflict->pool) |
| == SVN_NO_ERROR); |
| |
| return get_conflict_desc2_t(conflict)->node_kind; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_prop_get_propvals(const svn_string_t **base_propval, |
| const svn_string_t **working_propval, |
| const svn_string_t **incoming_old_propval, |
| const svn_string_t **incoming_new_propval, |
| svn_client_conflict_t *conflict, |
| const char *propname, |
| apr_pool_t *result_pool) |
| { |
| const svn_wc_conflict_description2_t *desc; |
| |
| SVN_ERR(assert_prop_conflict(conflict, conflict->pool)); |
| |
| desc = svn_hash_gets(conflict->prop_conflicts, propname); |
| if (desc == NULL) |
| return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, |
| _("Property '%s' is not in conflict."), propname); |
| |
| if (base_propval) |
| *base_propval = |
| svn_string_dup(desc->prop_value_base, result_pool); |
| |
| if (working_propval) |
| *working_propval = |
| svn_string_dup(desc->prop_value_working, result_pool); |
| |
| if (incoming_old_propval) |
| *incoming_old_propval = |
| svn_string_dup(desc->prop_value_incoming_old, result_pool); |
| |
| if (incoming_new_propval) |
| *incoming_new_propval = |
| svn_string_dup(desc->prop_value_incoming_new, result_pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| const char * |
| svn_client_conflict_prop_get_reject_abspath(svn_client_conflict_t *conflict) |
| { |
| SVN_ERR_ASSERT_NO_RETURN(assert_prop_conflict(conflict, conflict->pool) |
| == SVN_NO_ERROR); |
| |
| /* svn_wc_conflict_description2_t stores this path in 'their_abspath' */ |
| return get_conflict_desc2_t(conflict)->their_abspath; |
| } |
| |
| const char * |
| svn_client_conflict_text_get_mime_type(svn_client_conflict_t *conflict) |
| { |
| SVN_ERR_ASSERT_NO_RETURN(assert_text_conflict(conflict, conflict->pool) |
| == SVN_NO_ERROR); |
| |
| return get_conflict_desc2_t(conflict)->mime_type; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_text_get_contents(const char **base_abspath, |
| const char **working_abspath, |
| const char **incoming_old_abspath, |
| const char **incoming_new_abspath, |
| svn_client_conflict_t *conflict, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| SVN_ERR(assert_text_conflict(conflict, scratch_pool)); |
| |
| if (base_abspath) |
| { |
| if (svn_client_conflict_get_operation(conflict) == |
| svn_wc_operation_merge) |
| *base_abspath = NULL; /* ### WC base contents not available yet */ |
| else /* update/switch */ |
| *base_abspath = get_conflict_desc2_t(conflict)->base_abspath; |
| } |
| |
| if (working_abspath) |
| *working_abspath = get_conflict_desc2_t(conflict)->my_abspath; |
| |
| if (incoming_old_abspath) |
| *incoming_old_abspath = get_conflict_desc2_t(conflict)->base_abspath; |
| |
| if (incoming_new_abspath) |
| *incoming_new_abspath = get_conflict_desc2_t(conflict)->their_abspath; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Set up type-specific data for a new conflict object. */ |
| static svn_error_t * |
| conflict_type_specific_setup(svn_client_conflict_t *conflict, |
| apr_pool_t *scratch_pool) |
| { |
| svn_boolean_t tree_conflicted; |
| svn_wc_operation_t operation; |
| svn_wc_conflict_action_t incoming_change; |
| svn_wc_conflict_reason_t local_change; |
| |
| /* For now, we only deal with tree conflicts here. */ |
| SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted, |
| conflict, scratch_pool, |
| scratch_pool)); |
| if (!tree_conflicted) |
| return SVN_NO_ERROR; |
| |
| /* Set a default description function. */ |
| conflict->tree_conflict_get_incoming_description_func = |
| conflict_tree_get_incoming_description_generic; |
| conflict->tree_conflict_get_local_description_func = |
| conflict_tree_get_local_description_generic; |
| |
| operation = svn_client_conflict_get_operation(conflict); |
| incoming_change = svn_client_conflict_get_incoming_change(conflict); |
| local_change = svn_client_conflict_get_local_change(conflict); |
| |
| /* Set type-specific description and details functions. */ |
| if (incoming_change == svn_wc_conflict_action_delete || |
| incoming_change == svn_wc_conflict_action_replace) |
| { |
| conflict->tree_conflict_get_incoming_description_func = |
| conflict_tree_get_description_incoming_delete; |
| conflict->tree_conflict_get_incoming_details_func = |
| conflict_tree_get_details_incoming_delete; |
| } |
| else if (incoming_change == svn_wc_conflict_action_add) |
| { |
| conflict->tree_conflict_get_incoming_description_func = |
| conflict_tree_get_description_incoming_add; |
| conflict->tree_conflict_get_incoming_details_func = |
| conflict_tree_get_details_incoming_add; |
| } |
| else if (incoming_change == svn_wc_conflict_action_edit) |
| { |
| conflict->tree_conflict_get_incoming_description_func = |
| conflict_tree_get_description_incoming_edit; |
| conflict->tree_conflict_get_incoming_details_func = |
| conflict_tree_get_details_incoming_edit; |
| } |
| |
| if (local_change == svn_wc_conflict_reason_missing) |
| { |
| conflict->tree_conflict_get_local_description_func = |
| conflict_tree_get_description_local_missing; |
| conflict->tree_conflict_get_local_details_func = |
| conflict_tree_get_details_local_missing; |
| } |
| else if (local_change == svn_wc_conflict_reason_moved_away && |
| operation == svn_wc_operation_update /* ### what about switch? */) |
| { |
| conflict->tree_conflict_get_local_details_func = |
| conflict_tree_get_details_update_local_moved_away; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_get(svn_client_conflict_t **conflict, |
| const char *local_abspath, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| const apr_array_header_t *descs; |
| int i; |
| |
| *conflict = apr_pcalloc(result_pool, sizeof(**conflict)); |
| |
| (*conflict)->local_abspath = apr_pstrdup(result_pool, local_abspath); |
| (*conflict)->resolution_text = svn_client_conflict_option_unspecified; |
| (*conflict)->resolution_tree = svn_client_conflict_option_unspecified; |
| (*conflict)->resolved_props = apr_hash_make(result_pool); |
| (*conflict)->recommended_option_id = svn_client_conflict_option_unspecified; |
| (*conflict)->pool = result_pool; |
| |
| /* Add all legacy conflict descriptors we can find. Eventually, this code |
| * path should stop relying on svn_wc_conflict_description2_t entirely. */ |
| SVN_ERR(svn_wc__read_conflict_descriptions2_t(&descs, ctx->wc_ctx, |
| local_abspath, |
| result_pool, scratch_pool)); |
| for (i = 0; i < descs->nelts; i++) |
| { |
| const svn_wc_conflict_description2_t *desc; |
| |
| desc = APR_ARRAY_IDX(descs, i, const svn_wc_conflict_description2_t *); |
| add_legacy_desc_to_conflict(desc, *conflict, result_pool); |
| } |
| |
| SVN_ERR(conflict_type_specific_setup(*conflict, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Baton for conflict_status_walker */ |
| struct conflict_status_walker_baton |
| { |
| svn_client_conflict_walk_func_t conflict_walk_func; |
| void *conflict_walk_func_baton; |
| svn_client_ctx_t *ctx; |
| svn_wc_notify_func2_t notify_func; |
| void *notify_baton; |
| svn_boolean_t resolved_a_tree_conflict; |
| apr_hash_t *unresolved_tree_conflicts; |
| }; |
| |
| /* Implements svn_wc_notify_func2_t to collect new conflicts caused by |
| resolving a tree conflict. */ |
| static void |
| tree_conflict_collector(void *baton, |
| const svn_wc_notify_t *notify, |
| apr_pool_t *pool) |
| { |
| struct conflict_status_walker_baton *cswb = baton; |
| |
| if (cswb->notify_func) |
| cswb->notify_func(cswb->notify_baton, notify, pool); |
| |
| if (cswb->unresolved_tree_conflicts |
| && (notify->action == svn_wc_notify_tree_conflict |
| || notify->prop_state == svn_wc_notify_state_conflicted |
| || notify->content_state == svn_wc_notify_state_conflicted)) |
| { |
| if (!svn_hash_gets(cswb->unresolved_tree_conflicts, notify->path)) |
| { |
| const char *tc_abspath; |
| apr_pool_t *hash_pool; |
| |
| hash_pool = apr_hash_pool_get(cswb->unresolved_tree_conflicts); |
| tc_abspath = apr_pstrdup(hash_pool, notify->path); |
| svn_hash_sets(cswb->unresolved_tree_conflicts, tc_abspath, ""); |
| } |
| } |
| } |
| |
| /* |
| * Record a tree conflict resolution failure due to error condition ERR |
| * in the RESOLVE_LATER hash table. If the hash table is not available |
| * (meaning the caller does not wish to retry resolution later), or if |
| * the error condition does not indicate circumstances where another |
| * existing tree conflict is blocking the resolution attempt, then |
| * return the error ERR itself. |
| */ |
| static svn_error_t * |
| handle_tree_conflict_resolution_failure(const char *local_abspath, |
| svn_error_t *err, |
| apr_hash_t *unresolved_tree_conflicts) |
| { |
| const char *tc_abspath; |
| |
| if (!unresolved_tree_conflicts |
| || (err->apr_err != SVN_ERR_WC_OBSTRUCTED_UPDATE |
| && err->apr_err != SVN_ERR_WC_FOUND_CONFLICT)) |
| return svn_error_trace(err); /* Give up. Do not retry resolution later. */ |
| |
| svn_error_clear(err); |
| tc_abspath = apr_pstrdup(apr_hash_pool_get(unresolved_tree_conflicts), |
| local_abspath); |
| |
| svn_hash_sets(unresolved_tree_conflicts, tc_abspath, ""); |
| |
| return SVN_NO_ERROR; /* Caller may retry after resolving other conflicts. */ |
| } |
| |
| /* Implements svn_wc_status4_t to walk all conflicts to resolve. |
| */ |
| static svn_error_t * |
| conflict_status_walker(void *baton, |
| const char *local_abspath, |
| const svn_wc_status3_t *status, |
| apr_pool_t *scratch_pool) |
| { |
| struct conflict_status_walker_baton *cswb = baton; |
| svn_client_conflict_t *conflict; |
| svn_error_t *err; |
| svn_boolean_t tree_conflicted; |
| |
| if (!status->conflicted) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(svn_client_conflict_get(&conflict, local_abspath, cswb->ctx, |
| scratch_pool, scratch_pool)); |
| SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted, |
| conflict, scratch_pool, |
| scratch_pool)); |
| err = cswb->conflict_walk_func(cswb->conflict_walk_func_baton, |
| conflict, scratch_pool); |
| if (err) |
| { |
| if (tree_conflicted) |
| SVN_ERR(handle_tree_conflict_resolution_failure( |
| local_abspath, err, cswb->unresolved_tree_conflicts)); |
| |
| else |
| return svn_error_trace(err); |
| } |
| |
| if (tree_conflicted) |
| { |
| svn_client_conflict_option_id_t resolution; |
| |
| resolution = svn_client_conflict_tree_get_resolution(conflict); |
| if (resolution != svn_client_conflict_option_unspecified && |
| resolution != svn_client_conflict_option_postpone) |
| cswb->resolved_a_tree_conflict = TRUE; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_conflict_walk(const char *local_abspath, |
| svn_depth_t depth, |
| svn_client_conflict_walk_func_t conflict_walk_func, |
| void *conflict_walk_func_baton, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| struct conflict_status_walker_baton cswb; |
| apr_pool_t *iterpool = NULL; |
| svn_error_t *err = SVN_NO_ERROR; |
| |
| if (depth == svn_depth_unknown) |
| depth = svn_depth_infinity; |
| |
| cswb.conflict_walk_func = conflict_walk_func; |
| cswb.conflict_walk_func_baton = conflict_walk_func_baton; |
| cswb.ctx = ctx; |
| cswb.resolved_a_tree_conflict = FALSE; |
| cswb.unresolved_tree_conflicts = apr_hash_make(scratch_pool); |
| |
| if (ctx->notify_func2) |
| ctx->notify_func2(ctx->notify_baton2, |
| svn_wc_create_notify( |
| local_abspath, |
| svn_wc_notify_conflict_resolver_starting, |
| scratch_pool), |
| scratch_pool); |
| |
| /* Swap in our notify_func wrapper. We must revert this before returning! */ |
| cswb.notify_func = ctx->notify_func2; |
| cswb.notify_baton = ctx->notify_baton2; |
| ctx->notify_func2 = tree_conflict_collector; |
| ctx->notify_baton2 = &cswb; |
| |
| err = svn_wc_walk_status(ctx->wc_ctx, |
| local_abspath, |
| depth, |
| FALSE /* get_all */, |
| FALSE /* no_ignore */, |
| TRUE /* ignore_text_mods */, |
| NULL /* ignore_patterns */, |
| conflict_status_walker, &cswb, |
| ctx->cancel_func, ctx->cancel_baton, |
| scratch_pool); |
| |
| /* If we got new tree conflicts (or delayed conflicts) during the initial |
| walk, we now walk them one by one as closure. */ |
| while (!err && cswb.unresolved_tree_conflicts && |
| apr_hash_count(cswb.unresolved_tree_conflicts)) |
| { |
| apr_hash_index_t *hi; |
| svn_wc_status3_t *status = NULL; |
| const char *tc_abspath = NULL; |
| |
| if (iterpool) |
| svn_pool_clear(iterpool); |
| else |
| iterpool = svn_pool_create(scratch_pool); |
| |
| hi = apr_hash_first(scratch_pool, cswb.unresolved_tree_conflicts); |
| cswb.unresolved_tree_conflicts = apr_hash_make(scratch_pool); |
| cswb.resolved_a_tree_conflict = FALSE; |
| |
| for (; hi && !err; hi = apr_hash_next(hi)) |
| { |
| svn_pool_clear(iterpool); |
| |
| tc_abspath = apr_hash_this_key(hi); |
| |
| if (ctx->cancel_func) |
| { |
| err = ctx->cancel_func(ctx->cancel_baton); |
| if (err) |
| break; |
| } |
| |
| err = svn_error_trace(svn_wc_status3(&status, ctx->wc_ctx, |
| tc_abspath, |
| iterpool, iterpool)); |
| if (err) |
| break; |
| |
| err = svn_error_trace(conflict_status_walker(&cswb, tc_abspath, |
| status, scratch_pool)); |
| if (err) |
| break; |
| } |
| |
| if (!err && !cswb.resolved_a_tree_conflict && tc_abspath && |
| apr_hash_count(cswb.unresolved_tree_conflicts)) |
| { |
| /* None of the remaining conflicts got resolved, without any error. |
| * Disable the 'unresolved_tree_conflicts' cache and try again. */ |
| cswb.unresolved_tree_conflicts = NULL; |
| |
| /* Run the most recent resolve operation again. |
| * We still have status and tc_abspath for that one. |
| * This should uncover the error which prevents resolution. */ |
| err = svn_error_trace(conflict_status_walker(&cswb, tc_abspath, |
| status, scratch_pool)); |
| SVN_ERR_ASSERT(err != NULL); |
| |
| err = svn_error_createf( |
| SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, |
| _("Unable to resolve pending conflict on '%s'"), |
| svn_dirent_local_style(tc_abspath, scratch_pool)); |
| break; |
| } |
| } |
| |
| if (iterpool) |
| svn_pool_destroy(iterpool); |
| |
| ctx->notify_func2 = cswb.notify_func; |
| ctx->notify_baton2 = cswb.notify_baton; |
| |
| if (!err && ctx->notify_func2) |
| ctx->notify_func2(ctx->notify_baton2, |
| svn_wc_create_notify(local_abspath, |
| svn_wc_notify_conflict_resolver_done, |
| scratch_pool), |
| scratch_pool); |
| |
| return svn_error_trace(err); |
| } |