diff options
Diffstat (limited to 'subversion/libsvn_client/conflicts.c')
-rw-r--r-- | subversion/libsvn_client/conflicts.c | 3201 |
1 files changed, 2723 insertions, 478 deletions
diff --git a/subversion/libsvn_client/conflicts.c b/subversion/libsvn_client/conflicts.c index d1a2ae2..920c55e 100644 --- a/subversion/libsvn_client/conflicts.c +++ b/subversion/libsvn_client/conflicts.c @@ -255,7 +255,7 @@ struct repos_move_info { /* The revision in which this move was committed. */ svn_revnum_t rev; - /* The author who commited the revision in which this move was committed. */ + /* 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. */ @@ -2350,6 +2350,8 @@ 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; @@ -2357,21 +2359,49 @@ struct conflict_tree_local_missing_details const char *deleted_repos_relpath; /* Move information about the conflict victim. If not NULL, this is an - * array of repos_move_info elements. Each element is the head of a - * move chain which starts in DELETED_REV. */ + * 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 repos_move_info elements. + * 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; - /* If not NULL, this is the move target abspath. */ - const char *moved_to_abspath; + /* 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 * @@ -2590,6 +2620,168 @@ find_moves_in_natural_history(apr_array_header_t **moves, 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, @@ -2603,12 +2795,15 @@ conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict, 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; @@ -2619,10 +2814,10 @@ conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict, svn_revnum_t end_rev; SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( - &old_repos_relpath, &old_rev, NULL, conflict, + &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, NULL, conflict, + &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 @@ -2638,6 +2833,11 @@ conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict, 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 = @@ -2693,6 +2893,9 @@ conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict, { 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? */ @@ -2726,7 +2929,81 @@ conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict, if (sibling_moves == NULL) return SVN_NO_ERROR; - /* ## TODO: Find the missing node in the WC. */ + /* 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)); @@ -2737,8 +3014,83 @@ conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict, deleted_basename, conflict->pool); details->moves = moves; - details->sibling_moves = sibling_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; @@ -4647,133 +4999,6 @@ get_incoming_delete_details_for_reverse_addition( return SVN_NO_ERROR; } -/* Follow each move chain starting a MOVE all the way to the end to find - * the possible working copy locations for VICTIM_ABSPATH which corresponds - * to VICTIM_REPOS_REPLATH@VICTIM_REVISION. - * Add each such location to the WC_MOVE_TARGETS hash table, keyed on the - * repos_relpath which is the corresponding move destination in the repository. - * This function is recursive. */ -static svn_error_t * -follow_move_chains(apr_hash_t *wc_move_targets, - struct repos_move_info *move, - svn_client_ctx_t *ctx, - const char *victim_abspath, - svn_node_kind_t victim_node_kind, - const char *victim_repos_relpath, - svn_revnum_t victim_revision, - apr_pool_t *result_pool, - apr_pool_t *scratch_pool) -{ - /* If this is the end of a move chain, look for matching paths in - * the working copy and add them to our collection if found. */ - if (move->next == NULL) - { - apr_array_header_t *candidate_abspaths; - - /* Gather candidate nodes which represent this moved_to_repos_relpath. */ - SVN_ERR(svn_wc__guess_incoming_move_target_nodes( - &candidate_abspaths, ctx->wc_ctx, - victim_abspath, victim_node_kind, - move->moved_to_repos_relpath, - scratch_pool, scratch_pool)); - if (candidate_abspaths->nelts > 0) - { - apr_array_header_t *moved_to_abspaths; - int i; - apr_pool_t *iterpool = svn_pool_create(scratch_pool); - - moved_to_abspaths = apr_array_make(result_pool, 1, - sizeof (const char *)); - - for (i = 0; i < candidate_abspaths->nelts; i++) - { - const char *candidate_abspath; - const char *repos_root_url; - const char *repos_uuid; - const char *candidate_repos_relpath; - svn_revnum_t candidate_revision; - - svn_pool_clear(iterpool); - - candidate_abspath = APR_ARRAY_IDX(candidate_abspaths, i, - const char *); - SVN_ERR(svn_wc__node_get_origin(NULL, &candidate_revision, - &candidate_repos_relpath, - &repos_root_url, - &repos_uuid, - NULL, NULL, - ctx->wc_ctx, - candidate_abspath, - FALSE, - iterpool, iterpool)); - - if (candidate_revision == SVN_INVALID_REVNUM) - continue; - - /* If the conflict victim and the move target candidate - * are not from the same revision we must ensure that - * they are related. */ - if (candidate_revision != victim_revision) - { - svn_client__pathrev_t *yca_loc; - svn_error_t *err; - - err = find_yca(&yca_loc, victim_repos_relpath, - victim_revision, - candidate_repos_relpath, - candidate_revision, - repos_root_url, repos_uuid, - NULL, ctx, iterpool, iterpool); - if (err) - { - if (err->apr_err == SVN_ERR_FS_NOT_FOUND) - { - svn_error_clear(err); - yca_loc = NULL; - } - else - return svn_error_trace(err); - } - - if (yca_loc == NULL) - continue; - } - - APR_ARRAY_PUSH(moved_to_abspaths, const char *) = - apr_pstrdup(result_pool, candidate_abspath); - } - svn_pool_destroy(iterpool); - - svn_hash_sets(wc_move_targets, move->moved_to_repos_relpath, - moved_to_abspaths); - } - } - else - { - int i; - apr_pool_t *iterpool; - - /* Recurse into each of the possible move chains. */ - iterpool = svn_pool_create(scratch_pool); - for (i = 0; i < move->next->nelts; i++) - { - struct repos_move_info *next_move; - - svn_pool_clear(iterpool); - - next_move = APR_ARRAY_IDX(move->next, i, struct repos_move_info *); - SVN_ERR(follow_move_chains(wc_move_targets, next_move, - ctx, victim_abspath, victim_node_kind, - victim_repos_relpath, victim_revision, - result_pool, iterpool)); - - } - svn_pool_destroy(iterpool); - } - - return SVN_NO_ERROR; -} - static svn_error_t * init_wc_move_targets(struct conflict_tree_incoming_delete_details *details, svn_client_conflict_t *conflict, @@ -4785,11 +5010,9 @@ init_wc_move_targets(struct conflict_tree_incoming_delete_details *details, svn_node_kind_t victim_node_kind; const char *incoming_new_repos_relpath; svn_revnum_t incoming_new_pegrev; - svn_wc_operation_t operation; victim_abspath = svn_client_conflict_get_local_abspath(conflict); victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); - operation = svn_client_conflict_get_operation(conflict); /* ### Should we get the old location in case of reverse-merges? */ SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( &incoming_new_repos_relpath, &incoming_new_pegrev, @@ -4815,11 +5038,8 @@ init_wc_move_targets(struct conflict_tree_incoming_delete_details *details, get_moved_to_repos_relpath(details, scratch_pool); details->wc_move_target_idx = 0; - /* If only one move target exists after an update or switch, - * recommend a resolution option which follows the incoming move. */ - if (apr_hash_count(details->wc_move_targets) == 1 && - (operation == svn_wc_operation_update || - operation == svn_wc_operation_switch)) + /* 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; @@ -4832,7 +5052,10 @@ init_wc_move_targets(struct conflict_tree_incoming_delete_details *details, /* 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_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; @@ -4889,6 +5112,7 @@ conflict_tree_get_details_incoming_delete(svn_client_conflict_t *conflict, 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; @@ -4921,12 +5145,15 @@ conflict_tree_get_details_incoming_delete(svn_client_conflict_t *conflict, 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, - new_kind == svn_node_none ? 0 : old_rev, + 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) @@ -5952,8 +6179,11 @@ describe_incoming_edit_list_modified_revs(apr_array_header_t *edits, { if (i == edits->nelts - (max_revs_to_display / 2)) s = apr_psprintf(result_pool, - _("%s\n [%d revisions omitted for " - "brevity],\n"), + 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, @@ -6725,8 +6955,9 @@ resolve_merge_incoming_added_file_text_update( /* Revert the path in order to restore the repository's line of * history, which is part of the BASE tree. This revert operation * is why are being careful about not losing the temporary copy. */ - err = svn_wc_revert5(ctx->wc_ctx, local_abspath, svn_depth_empty, + 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); @@ -7543,7 +7774,6 @@ merge_newly_added_dir(const char *added_repos_relpath, diff_processor = processor; if (reverse_merge) diff_processor = svn_diff__tree_processor_reverse_create(diff_processor, - NULL, scratch_pool); /* Filter the first path component using a filter processor, until we fixed @@ -7726,8 +7956,9 @@ resolve_update_incoming_added_dir_merge(svn_client_conflict_option_t *option, * 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_revert5(ctx->wc_ctx, local_abspath, svn_depth_infinity, + 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); @@ -7754,35 +7985,6 @@ resolve_update_incoming_added_dir_merge(svn_client_conflict_option_t *option, return SVN_NO_ERROR; } -/* A baton for notification_adjust_func(). */ -struct notification_adjust_baton -{ - svn_wc_notify_func2_t inner_func; - void *inner_baton; - const char *checkout_abspath; - const char *final_abspath; -}; - -/* A svn_wc_notify_func2_t function that wraps BATON->inner_func (whose - * baton is BATON->inner_baton) and adjusts the notification paths that - * start with BATON->checkout_abspath to start instead with - * BATON->final_abspath. */ -static void -notification_adjust_func(void *baton, - const svn_wc_notify_t *notify, - apr_pool_t *pool) -{ - struct notification_adjust_baton *nb = baton; - svn_wc_notify_t *inner_notify = svn_wc_dup_notify(notify, pool); - const char *relpath; - - relpath = svn_dirent_skip_ancestor(nb->checkout_abspath, notify->path); - inner_notify->path = svn_dirent_join(nb->final_abspath, relpath, pool); - - if (nb->inner_func) - nb->inner_func(nb->inner_baton, inner_notify, pool); -} - /* Resolve a dir/dir "incoming add vs local obstruction" tree conflict by * replacing the local directory with the incoming directory. * If MERGE_DIRS is set, also merge the directories after replacing. */ @@ -7801,14 +8003,8 @@ merge_incoming_added_dir_replace(svn_client_conflict_option_t *option, svn_revnum_t incoming_new_pegrev; const char *local_abspath; const char *lock_abspath; - const char *tmpdir_abspath, *tmp_abspath; svn_error_t *err; - svn_revnum_t copy_src_revnum; - svn_opt_revision_t copy_src_peg_revision; svn_boolean_t timestamp_sleep; - svn_wc_notify_func2_t old_notify_func2 = ctx->notify_func2; - void *old_notify_baton2 = ctx->notify_baton2; - struct notification_adjust_baton nb; local_abspath = svn_client_conflict_get_local_abspath(conflict); @@ -7829,46 +8025,6 @@ merge_incoming_added_dir_replace(svn_client_conflict_option_t *option, if (corrected_url) url = corrected_url; - - /* Find a temporary location in which to check out the copy source. */ - SVN_ERR(svn_wc__get_tmpdir(&tmpdir_abspath, ctx->wc_ctx, local_abspath, - scratch_pool, scratch_pool)); - - SVN_ERR(svn_io_open_unique_file3(NULL, &tmp_abspath, tmpdir_abspath, - svn_io_file_del_on_close, - scratch_pool, scratch_pool)); - - /* Make a new checkout of the requested source. While doing so, - * resolve copy_src_revnum to an actual revision number in case it - * was until now 'invalid' meaning 'head'. Ask this function not to - * sleep for timestamps, by passing a sleep_needed output param. - * Send notifications for all nodes except the root node, and adjust - * them to refer to the destination rather than this temporary path. */ - - nb.inner_func = ctx->notify_func2; - nb.inner_baton = ctx->notify_baton2; - nb.checkout_abspath = tmp_abspath; - nb.final_abspath = local_abspath; - ctx->notify_func2 = notification_adjust_func; - ctx->notify_baton2 = &nb; - - copy_src_peg_revision.kind = svn_opt_revision_number; - copy_src_peg_revision.value.number = incoming_new_pegrev; - - err = svn_client__checkout_internal(©_src_revnum, ×tamp_sleep, - url, tmp_abspath, - ©_src_peg_revision, - ©_src_peg_revision, - svn_depth_infinity, - TRUE, /* we want to ignore externals */ - FALSE, /* we don't allow obstructions */ - ra_session, ctx, scratch_pool); - - ctx->notify_func2 = old_notify_func2; - ctx->notify_baton2 = old_notify_baton2; - - SVN_ERR(err); - /* ### The following WC modifications should be atomic. */ SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, @@ -7885,31 +8041,11 @@ merge_incoming_added_dir_replace(svn_client_conflict_option_t *option, if (err) goto unlock_wc; - /* Schedule dst_path for addition in parent, with copy history. - Don't send any notification here. - Then remove the temporary checkout's .svn dir in preparation for - moving the rest of it into the final destination. */ - err = svn_wc_copy3(ctx->wc_ctx, tmp_abspath, local_abspath, - TRUE /* metadata_only */, - NULL, NULL, /* don't allow user to cancel here */ - NULL, NULL, scratch_pool); - if (err) - goto unlock_wc; - - err = svn_wc__acquire_write_lock(NULL, ctx->wc_ctx, tmp_abspath, - FALSE, scratch_pool, scratch_pool); - if (err) - goto unlock_wc; - err = svn_wc_remove_from_revision_control2(ctx->wc_ctx, - tmp_abspath, - FALSE, FALSE, - NULL, NULL, /* don't cancel */ - scratch_pool); - if (err) - goto unlock_wc; - - /* Move the temporary disk tree into place. */ - err = svn_io_file_rename2(tmp_abspath, local_abspath, FALSE, scratch_pool); + 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; @@ -8346,7 +8482,9 @@ resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, apr_pool_t *scratch_pool) { svn_client_conflict_option_id_t option_id; - const char *local_abspath; + 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; @@ -8372,7 +8510,8 @@ resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, const char *moved_to_abspath; const char *incoming_abspath = NULL; - local_abspath = svn_client_conflict_get_local_abspath(conflict); + 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) @@ -8380,20 +8519,20 @@ resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, _("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, + 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(local_abspath, + 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_incoming_move_dir_merge); + svn_client_conflict_option_both_moved_file_move_merge); SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, conflict, scratch_pool, @@ -8408,7 +8547,7 @@ resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, scratch_pool)); /* Set up temporary storage for the common ancestor version of the file. */ - SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, + 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, @@ -8438,21 +8577,41 @@ resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, 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(local_abspath, + svn_dirent_get_longest_ancestor(victim_abspath, moved_to_abspath, scratch_pool), scratch_pool, scratch_pool)); - err = verify_local_state_for_incoming_delete(conflict, option, ctx, - scratch_pool); - if (err) - goto unlock_wc; + 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, local_abspath, + err = svn_wc_prop_list2(&victim_props, ctx->wc_ctx, merge_source_abspath, scratch_pool, scratch_pool); if (err) goto unlock_wc; @@ -8506,7 +8665,7 @@ resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, * 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(local_abspath, moved_to_abspath, FALSE, + err = svn_io_copy_file(merge_source_abspath, moved_to_abspath, FALSE, scratch_pool); if (err) goto unlock_wc; @@ -8546,7 +8705,7 @@ resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, err = svn_io_remove_file2(moved_to_abspath, FALSE, scratch_pool); if (err) goto unlock_wc; - err = svn_wc__move2(ctx->wc_ctx, local_abspath, moved_to_abspath, + 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 */ @@ -8604,19 +8763,27 @@ resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, operation == svn_wc_operation_switch) { /* Delete the tree conflict victim (clears the tree conflict marker). */ - err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + 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(local_abspath, svn_wc_notify_resolved_tree, + notify = svn_wc_create_notify(victim_abspath, svn_wc_notify_resolved_tree, scratch_pool); ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); } @@ -8637,6 +8804,521 @@ unlock_wc: 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, @@ -8767,11 +9449,14 @@ resolve_incoming_move_dir_merge(svn_client_conflict_option_t *option, svn_opt_revision_t incoming_new_opt_rev; /* Revert the incoming move target directory. */ - SVN_ERR(svn_wc_revert5(ctx->wc_ctx, moved_to_abspath, svn_depth_infinity, - FALSE, NULL, TRUE, FALSE, - NULL, NULL, /* no cancellation */ - ctx->notify_func2, ctx->notify_baton2, - scratch_pool)); + 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. */ @@ -8866,7 +9551,9 @@ unlock_wc: return SVN_NO_ERROR; } -/* Implements conflict_option_resolve_func_t. */ +/* 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, @@ -8894,6 +9581,12 @@ resolve_local_move_file_merge(svn_client_conflict_option_t *option, 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; @@ -8909,8 +9602,31 @@ resolve_local_move_file_merge(svn_client_conflict_option_t *option, 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, - details->moved_to_abspath, + merge_target_abspath, scratch_pool, scratch_pool)); /* Fetch the common ancestor file's content. */ @@ -8955,7 +9671,7 @@ resolve_local_move_file_merge(svn_client_conflict_option_t *option, SVN_ERR(svn_wc__acquire_write_lock_for_resolve( &lock_abspath, ctx->wc_ctx, svn_dirent_get_longest_ancestor(conflict->local_abspath, - details->moved_to_abspath, + merge_target_abspath, scratch_pool), scratch_pool, scratch_pool)); @@ -8963,7 +9679,7 @@ resolve_local_move_file_merge(svn_client_conflict_option_t *option, err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, ctx->wc_ctx, ancestor_tmp_abspath, incoming_tmp_abspath, - details->moved_to_abspath, + merge_target_abspath, NULL, NULL, NULL, /* labels */ NULL, NULL, /* conflict versions */ FALSE, /* dry run */ @@ -8973,7 +9689,7 @@ resolve_local_move_file_merge(svn_client_conflict_option_t *option, NULL, NULL, /* conflict func/baton */ NULL, NULL, /* don't allow user to cancel here */ scratch_pool); - svn_io_sleep_for_timestamps(details->moved_to_abspath, scratch_pool); + 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, @@ -8994,7 +9710,7 @@ resolve_local_move_file_merge(svn_client_conflict_option_t *option, svn_wc_notify_t *notify; /* Tell the world about the file merge that just happened. */ - notify = svn_wc_create_notify(details->moved_to_abspath, + notify = svn_wc_create_notify(merge_target_abspath, svn_wc_notify_update_update, scratch_pool); if (merge_content_outcome == svn_wc_merge_conflict) @@ -9017,6 +9733,127 @@ resolve_local_move_file_merge(svn_client_conflict_option_t *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) { @@ -9153,13 +9990,13 @@ svn_client_conflict_text_get_resolution_options(apr_array_header_t **options, add_resolution_option(*options, conflict, svn_client_conflict_option_incoming_text_where_conflicted, _("Accept incoming for conflicts"), - _("accept changes only where they conflict"), + _("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 changes which conflict and accept the rest"), + _("reject incoming changes which conflict and accept the rest"), resolve_text_conflict); add_resolution_option(*options, conflict, @@ -9771,7 +10608,7 @@ configure_option_incoming_delete_ignore(svn_client_conflict_t *conflict, is_local_move = (local_details != NULL && local_details->moves != NULL); - if (!is_incoming_move && !is_local_move) + if (is_incoming_move || is_local_move) return SVN_NO_ERROR; } @@ -9817,7 +10654,8 @@ configure_option_incoming_delete_accept(svn_client_conflict_t *conflict, 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_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. */ @@ -9854,39 +10692,75 @@ describe_incoming_move_merge_conflict_option( const char **description, svn_client_conflict_t *conflict, svn_client_ctx_t *ctx, - struct conflict_tree_incoming_delete_details *details, + const char *moved_to_abspath, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { - apr_array_header_t *move_target_wc_abspaths; svn_wc_operation_t operation; const char *victim_abspath; - const char *moved_to_abspath; + svn_node_kind_t victim_node_kind; const char *wcroot_abspath; - move_target_wc_abspaths = - svn_hash_gets(details->wc_move_targets, - get_moved_to_repos_relpath(details, scratch_pool)); - moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, - details->wc_move_target_idx, - const char *); - victim_abspath = svn_client_conflict_get_local_abspath(conflict); + 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) - *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)); + { + 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( @@ -9918,6 +10792,7 @@ configure_option_incoming_move_file_merge(svn_client_conflict_t *conflict, 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); @@ -9938,6 +10813,8 @@ configure_option_incoming_move_file_merge(svn_client_conflict_t *conflict, { 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) @@ -9946,9 +10823,15 @@ configure_option_incoming_move_file_merge(svn_client_conflict_t *conflict, 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, - details, + moved_to_abspath, scratch_pool, scratch_pool)); add_resolution_option( @@ -9999,6 +10882,8 @@ configure_option_incoming_dir_merge(svn_client_conflict_t *conflict, { 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) @@ -10007,9 +10892,15 @@ configure_option_incoming_dir_merge(svn_client_conflict_t *conflict, 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, - details, + moved_to_abspath, scratch_pool, scratch_pool)); add_resolution_option(options, conflict, @@ -10024,23 +10915,32 @@ configure_option_incoming_dir_merge(svn_client_conflict_t *conflict, /* Configure 'local move file merge' resolution option for * a tree conflict. */ static svn_error_t * -configure_option_local_move_file_merge(svn_client_conflict_t *conflict, - svn_client_ctx_t *ctx, - apr_array_header_t *options, - apr_pool_t *scratch_pool) +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, - NULL, conflict, scratch_pool, + &incoming_new_kind, conflict, scratch_pool, scratch_pool)); if (operation == svn_wc_operation_merge && @@ -10048,62 +10948,37 @@ configure_option_local_move_file_merge(svn_client_conflict_t *conflict, 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) + if (details != NULL && details->moves != NULL && + details->move_target_repos_relpath != NULL) { - apr_hash_t *wc_move_targets = apr_hash_make(scratch_pool); - apr_pool_t *iterpool; - int i; + apr_array_header_t *moves; + const char *moved_to_abspath; + const char *description; - iterpool = svn_pool_create(scratch_pool); - for (i = 0; i < details->moves->nelts; i++) - { - struct repos_move_info *move; + 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 *); - svn_pool_clear(iterpool); - move = APR_ARRAY_IDX(details->moves, i, struct repos_move_info *); - SVN_ERR(follow_move_chains(wc_move_targets, move, ctx, - conflict->local_abspath, - svn_node_file, - incoming_new_repos_relpath, - incoming_new_pegrev, - scratch_pool, iterpool)); - } - svn_pool_destroy(iterpool); + 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 (apr_hash_count(wc_move_targets) > 0) + 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)) { - apr_array_header_t *move_target_repos_relpaths; - const svn_sort__item_t *item; - apr_array_header_t *moved_to_abspaths; - const char *description; - const char *wcroot_abspath; - - /* Initialize to the first possible move target. Hopefully, - * in most cases there will only be one candidate anyway. */ - move_target_repos_relpaths = svn_sort__hash( - wc_move_targets, - svn_sort_compare_items_as_paths, - scratch_pool); - item = &APR_ARRAY_IDX(move_target_repos_relpaths, - 0, svn_sort__item_t); - moved_to_abspaths = item->value; - details->moved_to_abspath = - apr_pstrdup(conflict->pool, - APR_ARRAY_IDX(moved_to_abspaths, 0, const char *)); - - SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, - conflict->local_abspath, - scratch_pool, scratch_pool)); - description = - apr_psprintf( - scratch_pool, _("apply changes to move destination '%s'"), - svn_dirent_local_style( - svn_dirent_skip_ancestor(wcroot_abspath, - details->moved_to_abspath), - scratch_pool)); - add_resolution_option( options, conflict, svn_client_conflict_option_local_move_file_text_merge, @@ -10111,50 +10986,943 @@ configure_option_local_move_file_merge(svn_client_conflict_t *conflict, description, resolve_local_move_file_merge); } else - details->moved_to_abspath = NULL; + { + 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; } -svn_error_t * -svn_client_conflict_option_get_moved_to_repos_relpath_candidates( - apr_array_header_t **possible_moved_to_repos_relpaths, +/* Configure 'sibling move file/dir merge' resolution option for + * a tree conflict. */ +static svn_error_t * +configure_option_sibling_move_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + svn_node_kind_t incoming_old_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + &incoming_old_kind, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge && + incoming_change == svn_wc_conflict_action_edit && + local_change == svn_wc_conflict_reason_missing) + { + struct conflict_tree_local_missing_details *details; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, scratch_pool)); + + details = conflict->tree_conflict_local_details; + if (details != NULL && details->wc_siblings != NULL) + { + const char *description; + const char *sibling; + + sibling = + apr_pstrdup(conflict->pool, + APR_ARRAY_IDX(details->wc_siblings, + details->preferred_sibling_idx, + const char *)); + description = + apr_psprintf( + scratch_pool, _("apply changes to '%s'"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, sibling), + scratch_pool)); + + if ((incoming_old_kind == svn_node_file || + incoming_old_kind == svn_node_none) && + (incoming_new_kind == svn_node_file || + incoming_new_kind == svn_node_none)) + { + add_resolution_option( + options, conflict, + svn_client_conflict_option_sibling_move_file_text_merge, + _("Apply to corresponding local location"), + description, resolve_local_move_file_merge); + } + else + { + add_resolution_option( + options, conflict, + svn_client_conflict_option_sibling_move_dir_merge, + _("Apply to corresponding local location"), + description, resolve_local_move_dir_merge); + } + } + } + + return SVN_NO_ERROR; +} + +struct conflict_tree_update_local_moved_away_details { + /* + * This array consists of "const char *" absolute paths to working copy + * nodes which are uncomitted copies and correspond to the repository path + * of the conflict victim. + * Each such working copy node is a potential local move target which can + * be chosen to find a suitable merge target when resolving a tree conflict. + * + * This may be an empty array in case if there is no move target path in + * the working copy. */ + apr_array_header_t *wc_move_targets; + + /* Current index into the list of working copy paths in WC_MOVE_TARGETS. */ + int preferred_move_target_idx; +}; + +/* Implements conflict_option_resolve_func_t. + * Resolve an incoming move vs local move conflict by merging from the + * incoming move's target location to the local move's target location, + * overriding the incoming move. The original local move was broken during + * update/switch, so overriding the incoming move involves recording a new + * move from the incoming move's target location to the local move's target + * location. */ +static svn_error_t * +resolve_both_moved_file_update_keep_local_move( svn_client_conflict_option_t *option, - apr_pool_t *result_pool, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, apr_pool_t *scratch_pool) { - svn_client_conflict_t *conflict = option->conflict; - struct conflict_tree_incoming_delete_details *details; + svn_client_conflict_option_id_t option_id; const char *victim_abspath; - apr_array_header_t *sorted_repos_relpaths; - int i; + 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; - SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == - svn_client_conflict_option_incoming_move_file_text_merge || - svn_client_conflict_option_get_id(option) == - svn_client_conflict_option_incoming_move_dir_merge); + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + 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); - details = conflict->tree_conflict_incoming_details; - if (details == NULL || details->wc_move_targets == NULL) + 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, - _("Getting a list of possible move targets " + _("The specified conflict resolution option " "requires details for tree conflict at '%s' " - "to be fetched from the repository first"), + "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); - /* Return a copy of the repos replath candidate list. */ - sorted_repos_relpaths = svn_sort__hash(details->wc_move_targets, + *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 *)); + *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; @@ -10170,37 +11938,115 @@ svn_client_conflict_option_get_moved_to_repos_relpath_candidates( } svn_error_t * -svn_client_conflict_option_set_moved_to_repos_relpath( +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, - int preferred_move_target_idx, - svn_client_ctx_t *ctx, + apr_pool_t *result_pool, apr_pool_t *scratch_pool) { svn_client_conflict_t *conflict = option->conflict; - struct conflict_tree_incoming_delete_details *details; const char *victim_abspath; + 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; - SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == - svn_client_conflict_option_incoming_move_file_text_merge || - svn_client_conflict_option_get_id(option) == - svn_client_conflict_option_incoming_move_dir_merge); - - victim_abspath = svn_client_conflict_get_local_abspath(conflict); - details = conflict->tree_conflict_incoming_details; - if (details == NULL || details->wc_move_targets == NULL) - return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, - _("Setting a move target requires details " - "for tree conflict at '%s' to be fetched " - "from the repository first"), - svn_dirent_local_style(victim_abspath, - scratch_pool)); - if (preferred_move_target_idx < 0 || - preferred_move_target_idx >= apr_hash_count(details->wc_move_targets)) + 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'"), @@ -10209,15 +12055,14 @@ svn_client_conflict_option_set_moved_to_repos_relpath( scratch_pool)); /* Translate the index back into a hash table key. */ - move_target_repos_relpaths = - svn_sort__hash(details->wc_move_targets, - svn_sort_compare_items_as_paths, - scratch_pool); + 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, details->wc_move_targets); + for (hi = apr_hash_first(scratch_pool, wc_move_targets); hi != NULL; hi = apr_hash_next(hi)) { @@ -10225,16 +12070,7 @@ svn_client_conflict_option_set_moved_to_repos_relpath( if (strcmp(move_target_repos_relpath, repos_relpath) == 0) { - details->move_target_repos_relpath = repos_relpath; - details->wc_move_target_idx = 0; - /* Update option description. */ - SVN_ERR(describe_incoming_move_merge_conflict_option( - &option->description, - conflict, ctx, - details, - conflict->pool, - scratch_pool)); - + *new_hash_key = repos_relpath; return SVN_NO_ERROR; } } @@ -10248,107 +12084,501 @@ svn_client_conflict_option_set_moved_to_repos_relpath( } svn_error_t * -svn_client_conflict_option_get_moved_to_abspath_candidates( +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; - struct conflict_tree_incoming_delete_details *details; const char *victim_abspath; - apr_array_header_t *move_target_wc_abspaths; + 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; - SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == - svn_client_conflict_option_incoming_move_file_text_merge || - svn_client_conflict_option_get_id(option) == - svn_client_conflict_option_incoming_move_dir_merge); + 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); - 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)); + 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); - move_target_wc_abspaths = - svn_hash_gets(details->wc_move_targets, - get_moved_to_repos_relpath(details, 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; - /* 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++) + 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) { - const char *moved_to_abspath; + struct conflict_tree_update_local_moved_away_details *details; - 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); + 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_set_moved_to_abspath( +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; - struct conflict_tree_incoming_delete_details *details; const char *victim_abspath; - apr_array_header_t *move_target_wc_abspaths; + 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; - SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == - svn_client_conflict_option_incoming_move_file_text_merge || - svn_client_conflict_option_get_id(option) == - svn_client_conflict_option_incoming_move_dir_merge); + 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); - 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)); + 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); - move_target_wc_abspaths = - svn_hash_gets(details->wc_move_targets, - get_moved_to_repos_relpath(details, 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; + const char *preferred_sibling; - 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)); + 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)); - /* Record the user's preference. */ - details->wc_move_target_idx = preferred_move_target_idx; + 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. */ - SVN_ERR(describe_incoming_move_merge_conflict_option(&option->description, - conflict, ctx, - details, - conflict->pool, + /* 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, @@ -10401,8 +12631,15 @@ svn_client_conflict_tree_get_resolution_options(apr_array_header_t **options, scratch_pool)); SVN_ERR(configure_option_incoming_dir_merge(conflict, ctx, *options, scratch_pool)); - SVN_ERR(configure_option_local_move_file_merge(conflict, ctx, *options, + 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; } @@ -10947,6 +13184,7 @@ 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; @@ -10963,6 +13201,7 @@ conflict_type_specific_setup(svn_client_conflict_t *conflict, 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); @@ -10997,6 +13236,12 @@ conflict_type_specific_setup(svn_client_conflict_t *conflict, 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; } |