diff options
Diffstat (limited to 'subversion/libsvn_client')
23 files changed, 14066 insertions, 1082 deletions
diff --git a/subversion/libsvn_client/checkout.c b/subversion/libsvn_client/checkout.c index 0d20e24e117a..b9138b0b6606 100644 --- a/subversion/libsvn_client/checkout.c +++ b/subversion/libsvn_client/checkout.c @@ -81,6 +81,7 @@ svn_client__checkout_internal(svn_revnum_t *result_rev, { svn_node_kind_t kind; svn_client__pathrev_t *pathrev; + svn_opt_revision_t resolved_rev = { svn_opt_revision_number }; /* Sanity check. Without these, the checkout is meaningless. */ SVN_ERR_ASSERT(local_abspath != NULL); @@ -125,6 +126,7 @@ svn_client__checkout_internal(svn_revnum_t *result_rev, } SVN_ERR(svn_ra_check_path(ra_session, "", pathrev->rev, &kind, scratch_pool)); + resolved_rev.value.number = pathrev->rev; if (kind == svn_node_none) return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, @@ -185,8 +187,8 @@ svn_client__checkout_internal(svn_revnum_t *result_rev, /* Have update fix the incompleteness. */ SVN_ERR(svn_client__update_internal(result_rev, timestamp_sleep, - local_abspath, revision, depth, TRUE, - ignore_externals, + local_abspath, &resolved_rev, depth, + TRUE, ignore_externals, allow_unver_obstructions, TRUE /* adds_as_modification */, FALSE, FALSE, ra_session, diff --git a/subversion/libsvn_client/client.h b/subversion/libsvn_client/client.h index 58354cf9b2c4..c0a794712f3f 100644 --- a/subversion/libsvn_client/client.h +++ b/subversion/libsvn_client/client.h @@ -1072,9 +1072,13 @@ svn_client__ensure_revprop_table(apr_hash_t **revprop_table_out, EXPAND_KEYWORDS operates as per the EXPAND argument to svn_subst_stream_translated, which see. If NORMALIZE_EOLS is TRUE and LOCAL_ABSPATH requires translation, then normalize the line endings in - *NORMAL_STREAM. + *NORMAL_STREAM to "\n" if the stream has svn:eol-style set. - Uses SCRATCH_POOL for temporary allocations. */ + Note that this IS NOT the repository normal form of the stream as that + would use "\r\n" if set to CRLF and "\r" if set to CR. + + The stream is allocated in RESULT_POOL and temporary SCRATCH_POOL is + used for temporary allocations. */ svn_error_t * svn_client__get_normalized_stream(svn_stream_t **normal_stream, svn_wc_context_t *wc_ctx, @@ -1181,6 +1185,88 @@ svn_client__remote_propget(apr_hash_t *props, apr_pool_t *result_pool, apr_pool_t *scratch_pool); +/* */ +typedef struct merge_source_t +{ + /* "left" side URL and revision (inclusive iff youngest) */ + const svn_client__pathrev_t *loc1; + + /* "right" side URL and revision (inclusive iff youngest) */ + const svn_client__pathrev_t *loc2; + + /* True iff LOC1 is an ancestor of LOC2 or vice-versa (history-wise). */ + svn_boolean_t ancestral; +} merge_source_t; + +/* Description of the merge target root node (a WC working node) */ +typedef struct merge_target_t +{ + /* Absolute path to the WC node */ + const char *abspath; + + /* The repository location of the base node of the target WC. If the node + * is locally added, then URL & REV are NULL & SVN_INVALID_REVNUM. + * REPOS_ROOT_URL and REPOS_UUID are always valid. */ + svn_client__pathrev_t loc; + +} merge_target_t; + +/* + * Similar API to svn_client_merge_peg5(). + */ +svn_error_t * +svn_client__merge_elements(svn_boolean_t *use_sleep, + apr_array_header_t *merge_sources, + merge_target_t *target, + svn_ra_session_t *ra_session, + svn_boolean_t diff_ignore_ancestry, + svn_boolean_t force_delete, + svn_boolean_t dry_run, + const apr_array_header_t *merge_options, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + +/* Data for reporting when a merge aborted because of raising conflicts. + * + * ### TODO: More info, including the ranges (or other parameters) the user + * needs to complete the merge. + */ +typedef struct svn_client__conflict_report_t +{ + const char *target_abspath; + /* The revision range during which conflicts were raised */ + const merge_source_t *conflicted_range; + /* Was the conflicted range the last range in the whole requested merge? */ + svn_boolean_t was_last_range; +} svn_client__conflict_report_t; + +/* Create and return an error structure appropriate for the unmerged + revisions range(s). */ +svn_error_t * +svn_client__make_merge_conflict_error(svn_client__conflict_report_t *report, + apr_pool_t *scratch_pool); + +/* The body of svn_client_merge5(), which see for details. */ +svn_error_t * +svn_client__merge_locked(svn_client__conflict_report_t **conflict_report, + const char *source1, + const svn_opt_revision_t *revision1, + const char *source2, + const svn_opt_revision_t *revision2, + const char *target_abspath, + svn_depth_t depth, + svn_boolean_t ignore_mergeinfo, + svn_boolean_t diff_ignore_ancestry, + svn_boolean_t force_delete, + svn_boolean_t record_only, + svn_boolean_t dry_run, + svn_boolean_t allow_mixed_rev, + const apr_array_header_t *merge_options, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/subversion/libsvn_client/conflicts.c b/subversion/libsvn_client/conflicts.c new file mode 100644 index 000000000000..0fd9a2bbec5e --- /dev/null +++ b/subversion/libsvn_client/conflicts.c @@ -0,0 +1,11207 @@ +/* + * conflicts.c: conflict resolver implementation + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ + +/* ==================================================================== */ + + + +/*** Includes. ***/ + +#include "svn_types.h" +#include "svn_wc.h" +#include "svn_client.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_pools.h" +#include "svn_props.h" +#include "svn_hash.h" +#include "svn_sorts.h" +#include "svn_subst.h" +#include "client.h" + +#include "private/svn_diff_tree.h" +#include "private/svn_ra_private.h" +#include "private/svn_sorts_private.h" +#include "private/svn_token.h" +#include "private/svn_wc_private.h" + +#include "svn_private_config.h" + +#define ARRAY_LEN(ary) ((sizeof (ary)) / (sizeof ((ary)[0]))) + + +/*** Dealing with conflicts. ***/ + +/* Describe a tree conflict. */ +typedef svn_error_t *(*tree_conflict_get_description_func_t)( + const char **change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + +/* Get more information about a tree conflict. + * This function may contact the repository. */ +typedef svn_error_t *(*tree_conflict_get_details_func_t)( + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool); + +struct svn_client_conflict_t +{ + const char *local_abspath; + apr_hash_t *prop_conflicts; + + /* Indicate which options were chosen to resolve a text or tree conflict + * on the conflicted node. */ + svn_client_conflict_option_id_t resolution_text; + svn_client_conflict_option_id_t resolution_tree; + + /* A mapping from const char* property name to pointers to + * svn_client_conflict_option_t for all properties which had their + * conflicts resolved. Indicates which options were chosen to resolve + * the property conflicts. */ + apr_hash_t *resolved_props; + + /* Ask a tree conflict to describe itself. */ + tree_conflict_get_description_func_t + tree_conflict_get_incoming_description_func; + tree_conflict_get_description_func_t + tree_conflict_get_local_description_func; + + /* Ask a tree conflict to find out more information about itself + * by contacting the repository. */ + tree_conflict_get_details_func_t tree_conflict_get_incoming_details_func; + tree_conflict_get_details_func_t tree_conflict_get_local_details_func; + + /* Any additional information found can be stored here and may be used + * when describing a tree conflict. */ + void *tree_conflict_incoming_details; + void *tree_conflict_local_details; + + /* The pool this conflict was allocated from. */ + apr_pool_t *pool; + + /* Conflict data provided by libsvn_wc. */ + const svn_wc_conflict_description2_t *legacy_text_conflict; + const char *legacy_prop_conflict_propname; + const svn_wc_conflict_description2_t *legacy_tree_conflict; + + /* The recommended resolution option's ID. */ + svn_client_conflict_option_id_t recommended_option_id; +}; + +/* Resolves conflict to OPTION and sets CONFLICT->RESOLUTION accordingly. + * + * May raise an error in case the conflict could not be resolved. A common + * case would be a tree conflict the resolution of which depends on other + * tree conflicts to be resolved first. */ +typedef svn_error_t *(*conflict_option_resolve_func_t)( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool); + +struct svn_client_conflict_option_t +{ + svn_client_conflict_option_id_t id; + const char *label; + const char *description; + + svn_client_conflict_t *conflict; + conflict_option_resolve_func_t do_resolve_func; + + /* The pool this option was allocated from. */ + apr_pool_t *pool; + + /* Data which is specific to particular conflicts and options. */ + union { + struct { + /* Indicates the property to resolve in case of a property conflict. + * If set to "", all properties are resolved to this option. */ + const char *propname; + + /* A merged property value, if supplied by the API user, else NULL. */ + const svn_string_t *merged_propval; + } prop; + } type_data; + +}; + +/* + * Return a legacy conflict choice corresponding to OPTION_ID. + * Return svn_wc_conflict_choose_undefined if no corresponding + * legacy conflict choice exists. + */ +static svn_wc_conflict_choice_t +conflict_option_id_to_wc_conflict_choice( + svn_client_conflict_option_id_t option_id) +{ + + switch (option_id) + { + case svn_client_conflict_option_undefined: + return svn_wc_conflict_choose_undefined; + + case svn_client_conflict_option_postpone: + return svn_wc_conflict_choose_postpone; + + case svn_client_conflict_option_base_text: + return svn_wc_conflict_choose_base; + + case svn_client_conflict_option_incoming_text: + return svn_wc_conflict_choose_theirs_full; + + case svn_client_conflict_option_working_text: + return svn_wc_conflict_choose_mine_full; + + case svn_client_conflict_option_incoming_text_where_conflicted: + return svn_wc_conflict_choose_theirs_conflict; + + case svn_client_conflict_option_working_text_where_conflicted: + return svn_wc_conflict_choose_mine_conflict; + + case svn_client_conflict_option_merged_text: + return svn_wc_conflict_choose_merged; + + case svn_client_conflict_option_unspecified: + return svn_wc_conflict_choose_unspecified; + + default: + break; + } + + return svn_wc_conflict_choose_undefined; +} + +static void +add_legacy_desc_to_conflict(const svn_wc_conflict_description2_t *desc, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool) +{ + switch (desc->kind) + { + case svn_wc_conflict_kind_text: + conflict->legacy_text_conflict = desc; + break; + + case svn_wc_conflict_kind_property: + if (conflict->prop_conflicts == NULL) + conflict->prop_conflicts = apr_hash_make(result_pool); + svn_hash_sets(conflict->prop_conflicts, desc->property_name, desc); + conflict->legacy_prop_conflict_propname = desc->property_name; + break; + + case svn_wc_conflict_kind_tree: + conflict->legacy_tree_conflict = desc; + break; + + default: + SVN_ERR_ASSERT_NO_RETURN(FALSE); /* unknown kind of conflict */ + } +} + +/* A map for svn_wc_conflict_action_t values to strings */ +static const svn_token_map_t map_conflict_action[] = +{ + { "edit", svn_wc_conflict_action_edit }, + { "delete", svn_wc_conflict_action_delete }, + { "add", svn_wc_conflict_action_add }, + { "replace", svn_wc_conflict_action_replace }, + { NULL, 0 } +}; + +/* A map for svn_wc_conflict_reason_t values to strings */ +static const svn_token_map_t map_conflict_reason[] = +{ + { "edit", svn_wc_conflict_reason_edited }, + { "delete", svn_wc_conflict_reason_deleted }, + { "missing", svn_wc_conflict_reason_missing }, + { "obstruction", svn_wc_conflict_reason_obstructed }, + { "add", svn_wc_conflict_reason_added }, + { "replace", svn_wc_conflict_reason_replaced }, + { "unversioned", svn_wc_conflict_reason_unversioned }, + { "moved-away", svn_wc_conflict_reason_moved_away }, + { "moved-here", svn_wc_conflict_reason_moved_here }, + { NULL, 0 } +}; + +/* Describes a server-side move (really a copy+delete within the same + * revision) which was identified by scanning the revision log. + * This structure can represent one or more "chains" of moves, i.e. + * multiple move operations which occurred across a range of revisions. */ +struct repos_move_info { + /* The revision in which this move was committed. */ + svn_revnum_t rev; + + /* The author who commited the revision in which this move was committed. */ + const char *rev_author; + + /* The repository relpath the node was moved from in this revision. */ + const char *moved_from_repos_relpath; + + /* The repository relpath the node was moved to in this revision. */ + const char *moved_to_repos_relpath; + + /* The copyfrom revision of the moved-to path. */ + svn_revnum_t copyfrom_rev; + + /* The node kind of the item being moved. */ + svn_node_kind_t node_kind; + + /* Prev pointer. NULL if no prior move exists in the chain. */ + struct repos_move_info *prev; + + /* An array of struct repos_move_info * elements, each representing + * a possible way forward in the move chain. NULL if no next move + * exists in this chain. If the deleted node was copied only once in + * this revision, then this array has only one element and the move + * chain does not fork. But if this revision contains multiple copies of + * the deleted node, each of these copies appears as an element of this + * array, and each element represents a different path the next move + * might have taken. */ + apr_array_header_t *next; +}; + +static svn_revnum_t +rev_below(svn_revnum_t rev) +{ + SVN_ERR_ASSERT_NO_RETURN(rev != SVN_INVALID_REVNUM); + SVN_ERR_ASSERT_NO_RETURN(rev > 0); + + return rev == 1 ? 1 : rev - 1; +} + +/* Set *RELATED to true if the deleted node DELETED_REPOS_RELPATH@DELETED_REV + * is an ancestor of the copied node COPYFROM_PATH@COPYFROM_REV. + * If CHECK_LAST_CHANGED_REV is non-zero, also ensure that the copied node + * is a copy of the deleted node's last-changed revision's content, rather + * than a copy of some older content. If it's not, set *RELATED to false. */ +static svn_error_t * +check_move_ancestry(svn_boolean_t *related, + svn_ra_session_t *ra_session, + const char *repos_root_url, + const char *deleted_repos_relpath, + svn_revnum_t deleted_rev, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + svn_boolean_t check_last_changed_rev, + apr_pool_t *scratch_pool) +{ + apr_hash_t *locations; + const char *deleted_url; + const char *deleted_location; + apr_array_header_t *location_revisions; + const char *old_session_url; + + location_revisions = apr_array_make(scratch_pool, 1, sizeof(svn_revnum_t)); + APR_ARRAY_PUSH(location_revisions, svn_revnum_t) = copyfrom_rev; + deleted_url = svn_uri_canonicalize(apr_pstrcat(scratch_pool, + repos_root_url, "/", + deleted_repos_relpath, + NULL), + scratch_pool); + SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session, + deleted_url, scratch_pool)); + SVN_ERR(svn_ra_get_locations(ra_session, &locations, "", + rev_below(deleted_rev), location_revisions, + scratch_pool)); + + deleted_location = apr_hash_get(locations, ©from_rev, + sizeof(svn_revnum_t)); + if (deleted_location) + { + if (deleted_location[0] == '/') + deleted_location++; + if (strcmp(deleted_location, copyfrom_path) != 0) + { + *related = FALSE; + return SVN_NO_ERROR; + } + } + else + { + *related = FALSE; + return SVN_NO_ERROR; + } + + if (check_last_changed_rev) + { + svn_dirent_t *dirent; + + /* Verify that copyfrom_rev >= last-changed revision of the + * deleted node. */ + SVN_ERR(svn_ra_stat(ra_session, "", rev_below(deleted_rev), &dirent, + scratch_pool)); + if (dirent == NULL || copyfrom_rev < dirent->created_rev) + { + *related = FALSE; + return SVN_NO_ERROR; + } + } + + *related = TRUE; + return SVN_NO_ERROR; +} + +struct copy_info { + const char *copyto_path; + const char *copyfrom_path; + svn_revnum_t copyfrom_rev; + svn_node_kind_t node_kind; +}; + +/* Allocate and return a NEW_MOVE, and update MOVED_PATHS with this new move. */ +static svn_error_t * +add_new_move(struct repos_move_info **new_move, + const char *deleted_repos_relpath, + const char *copyto_path, + svn_revnum_t copyfrom_rev, + svn_node_kind_t node_kind, + svn_revnum_t revision, + const char *author, + apr_hash_t *moved_paths, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct repos_move_info *move; + struct repos_move_info *next_move; + + move = apr_pcalloc(result_pool, sizeof(*move)); + move->moved_from_repos_relpath = apr_pstrdup(result_pool, + deleted_repos_relpath); + move->moved_to_repos_relpath = apr_pstrdup(result_pool, copyto_path); + move->rev = revision; + move->rev_author = apr_pstrdup(result_pool, author); + move->copyfrom_rev = copyfrom_rev; + move->node_kind = node_kind; + + /* Link together multiple moves of the same node. + * Note that we're traversing history backwards, so moves already + * present in the list happened in younger revisions. */ + next_move = svn_hash_gets(moved_paths, move->moved_to_repos_relpath); + if (next_move) + { + svn_boolean_t related; + + /* Tracing back history of the delete-half of the next move + * to the copyfrom-revision of the prior move we must end up + * at the delete-half of the prior move. */ + SVN_ERR(check_move_ancestry(&related, ra_session, repos_root_url, + next_move->moved_from_repos_relpath, + next_move->rev, + move->moved_from_repos_relpath, + move->copyfrom_rev, + FALSE, scratch_pool)); + if (related) + { + SVN_ERR_ASSERT(move->rev < next_move->rev); + + /* Prepend this move to the linked list. */ + if (move->next == NULL) + move->next = apr_array_make(result_pool, 1, + sizeof (struct repos_move_info *)); + APR_ARRAY_PUSH(move->next, struct repos_move_info *) = next_move; + next_move->prev = move; + } + } + + /* Make this move the head of our next-move linking map. */ + svn_hash_sets(moved_paths, move->moved_from_repos_relpath, move); + + *new_move = move; + return SVN_NO_ERROR; +} + +/* Push a MOVE into the MOVES_TABLE. */ +static void +push_move(struct repos_move_info *move, apr_hash_t *moves_table, + apr_pool_t *result_pool) +{ + apr_array_header_t *moves; + + /* Add this move to the list of moves in the revision. */ + moves = apr_hash_get(moves_table, &move->rev, sizeof(svn_revnum_t)); + if (moves == NULL) + { + /* It is the first move in this revision. Create the list. */ + moves = apr_array_make(result_pool, 1, sizeof(struct repos_move_info *)); + apr_hash_set(moves_table, &move->rev, sizeof(svn_revnum_t), moves); + } + APR_ARRAY_PUSH(moves, struct repos_move_info *) = move; +} + +/* Find the youngest common ancestor of REPOS_RELPATH1@PEG_REV1 and + * REPOS_RELPATH2@PEG_REV2. Return the result in *YCA_LOC. + * Set *YCA_LOC to NULL if no common ancestor exists. */ +static svn_error_t * +find_yca(svn_client__pathrev_t **yca_loc, + const char *repos_relpath1, + svn_revnum_t peg_rev1, + const char *repos_relpath2, + svn_revnum_t peg_rev2, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client__pathrev_t *loc1; + svn_client__pathrev_t *loc2; + + *yca_loc = NULL; + + loc1 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid, + peg_rev1, repos_relpath1, + scratch_pool); + loc2 = svn_client__pathrev_create_with_relpath(repos_root_url, repos_uuid, + peg_rev2, repos_relpath2, + scratch_pool); + SVN_ERR(svn_client__get_youngest_common_ancestor(yca_loc, loc1, loc2, + ra_session, ctx, + result_pool, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Like find_yca, expect that a YCA could also be found via a brute-force + * search of parents of REPOS_RELPATH1 and REPOS_RELPATH2, if no "direct" + * YCA exists. An implicit assumption is that some parent of REPOS_RELPATH1 + * is a branch of some parent of REPOS_RELPATH2. + * + * This function can guess a "good enough" YCA for 'missing nodes' which do + * not exist in the working copy, e.g. when a file edit is merged to a path + * which does not exist in the working copy. + */ +static svn_error_t * +find_nearest_yca(svn_client__pathrev_t **yca_locp, + const char *repos_relpath1, + svn_revnum_t peg_rev1, + const char *repos_relpath2, + svn_revnum_t peg_rev2, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + apr_pool_t *iterpool; + const char *p1, *p2; + apr_size_t c1, c2; + + *yca_locp = NULL; + + iterpool = svn_pool_create(scratch_pool); + + p1 = repos_relpath1; + c1 = svn_path_component_count(repos_relpath1); + while (c1--) + { + svn_pool_clear(iterpool); + + p2 = repos_relpath2; + c2 = svn_path_component_count(repos_relpath2); + while (c2--) + { + err = find_yca(&yca_loc, p1, peg_rev1, p2, peg_rev2, + repos_root_url, repos_uuid, ra_session, ctx, + result_pool, iterpool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + if (yca_loc) + { + *yca_locp = yca_loc; + svn_pool_destroy(iterpool); + return SVN_NO_ERROR; + } + + p2 = svn_relpath_dirname(p2, scratch_pool); + } + + p1 = svn_relpath_dirname(p1, scratch_pool); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Check if the copied node described by COPY and the DELETED_PATH@DELETED_REV + * share a common ancestor. If so, return new repos_move_info in *MOVE which + * describes a move from the deleted path to that copy's destination. */ +static svn_error_t * +find_related_move(struct repos_move_info **move, + struct copy_info *copy, + const char *deleted_repos_relpath, + svn_revnum_t deleted_rev, + const char *author, + apr_hash_t *moved_paths, + const char *repos_root_url, + const char *repos_uuid, + svn_client_ctx_t *ctx, + svn_ra_session_t *ra_session, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + + *move = NULL; + err = find_yca(&yca_loc, copy->copyfrom_path, copy->copyfrom_rev, + deleted_repos_relpath, rev_below(deleted_rev), + repos_root_url, repos_uuid, ra_session, ctx, + scratch_pool, scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + if (yca_loc) + SVN_ERR(add_new_move(move, deleted_repos_relpath, + copy->copyto_path, copy->copyfrom_rev, + copy->node_kind, deleted_rev, author, + moved_paths, ra_session, repos_root_url, + result_pool, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Detect moves by matching DELETED_REPOS_RELPATH@DELETED_REV to the copies + * in COPIES. Add any moves found to MOVES_TABLE and update MOVED_PATHS. */ +static svn_error_t * +match_copies_to_deletion(const char *deleted_repos_relpath, + svn_revnum_t deleted_rev, + const char *author, + apr_hash_t *copies, + apr_hash_t *moves_table, + apr_hash_t *moved_paths, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_hash_index_t *hi; + apr_pool_t *iterpool; + + iterpool = svn_pool_create(scratch_pool); + for (hi = apr_hash_first(scratch_pool, copies); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *copyfrom_path = apr_hash_this_key(hi); + apr_array_header_t *copies_with_same_source_path; + int i; + + svn_pool_clear(iterpool); + + copies_with_same_source_path = apr_hash_this_val(hi); + + if (strcmp(copyfrom_path, deleted_repos_relpath) == 0) + { + /* We found a copyfrom path which matches a deleted node. + * Check if the deleted node is an ancestor of the copied node. */ + for (i = 0; i < copies_with_same_source_path->nelts; i++) + { + struct copy_info *copy; + svn_boolean_t related; + struct repos_move_info *move; + + copy = APR_ARRAY_IDX(copies_with_same_source_path, i, + struct copy_info *); + SVN_ERR(check_move_ancestry(&related, + ra_session, repos_root_url, + deleted_repos_relpath, + deleted_rev, + copy->copyfrom_path, + copy->copyfrom_rev, + TRUE, iterpool)); + if (!related) + continue; + + /* Remember details of this move. */ + SVN_ERR(add_new_move(&move, deleted_repos_relpath, + copy->copyto_path, copy->copyfrom_rev, + copy->node_kind, deleted_rev, author, + moved_paths, ra_session, repos_root_url, + result_pool, iterpool)); + push_move(move, moves_table, result_pool); + } + } + else + { + /* Check if this deleted node is related to any copies in this + * revision. These could be moves of the deleted node which + * were merged here from other lines of history. */ + for (i = 0; i < copies_with_same_source_path->nelts; i++) + { + struct copy_info *copy; + struct repos_move_info *move = NULL; + + copy = APR_ARRAY_IDX(copies_with_same_source_path, i, + struct copy_info *); + SVN_ERR(find_related_move(&move, copy, deleted_repos_relpath, + deleted_rev, author, + moved_paths, + repos_root_url, repos_uuid, + ctx, ra_session, + result_pool, iterpool)); + if (move) + push_move(move, moves_table, result_pool); + } + } + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Update MOVES_TABLE and MOVED_PATHS based on information from + * revision data in LOG_ENTRY, COPIES, and DELETED_PATHS. + * Use RA_SESSION to perform the necessary requests. */ +static svn_error_t * +find_moves_in_revision(svn_ra_session_t *ra_session, + apr_hash_t *moves_table, + apr_hash_t *moved_paths, + svn_log_entry_t *log_entry, + apr_hash_t *copies, + apr_array_header_t *deleted_paths, + const char *repos_root_url, + const char *repos_uuid, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_pool_t *iterpool; + int i; + const svn_string_t *author; + + author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < deleted_paths->nelts; i++) + { + const char *deleted_repos_relpath; + + svn_pool_clear(iterpool); + + deleted_repos_relpath = APR_ARRAY_IDX(deleted_paths, i, const char *); + SVN_ERR(match_copies_to_deletion(deleted_repos_relpath, + log_entry->revision, + author ? author->data + : _("unknown author"), + copies, moves_table, moved_paths, + repos_root_url, repos_uuid, ra_session, + ctx, result_pool, iterpool)); + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +struct find_deleted_rev_baton +{ + /* Variables below are arguments provided by the caller of + * svn_ra_get_log2(). */ + const char *deleted_repos_relpath; + const char *related_repos_relpath; + svn_revnum_t related_peg_rev; + const char *repos_root_url; + const char *repos_uuid; + svn_client_ctx_t *ctx; + const char *victim_abspath; /* for notifications */ + + /* Variables below are results for the caller of svn_ra_get_log2(). */ + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + apr_pool_t *result_pool; + + apr_hash_t *moves_table; /* Obtained from find_moves_in_revision(). */ + struct repos_move_info *move; /* Last known move which affected the node. */ + + /* Extra RA session that can be used to make additional requests. */ + svn_ra_session_t *extra_ra_session; +}; + +/* If DELETED_RELPATH matches the moved-from path of a move in MOVES, + * or if DELETED_RELPATH is a child of a moved-to path in MOVES, return + * a struct move_info for the corresponding move. Else, return NULL. */ +static struct repos_move_info * +map_deleted_path_to_move(const char *deleted_relpath, + apr_array_header_t *moves, + apr_pool_t *scratch_pool) +{ + struct repos_move_info *closest_move = NULL; + apr_size_t min_components = 0; + int i; + + for (i = 0; i < moves->nelts; i++) + { + const char *relpath; + struct repos_move_info *move; + + move = APR_ARRAY_IDX(moves, i, struct repos_move_info *); + if (strcmp(move->moved_from_repos_relpath, deleted_relpath) == 0) + return move; + + relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, + deleted_relpath); + if (relpath) + { + /* This could be a nested move. Return the path-wise closest move. */ + const apr_size_t c = svn_path_component_count(relpath); + if (c == 0) + return move; + else if (min_components == 0 || c < min_components) + { + min_components = c; + closest_move = move; + } + } + } + + if (closest_move) + { + const char *relpath; + const char *moved_along_path; + struct repos_move_info *move; + + /* See if we can find an even closer move for this moved-along path. */ + relpath = svn_relpath_skip_ancestor(closest_move->moved_to_repos_relpath, + deleted_relpath); + moved_along_path = + svn_relpath_join(closest_move->moved_from_repos_relpath, relpath, + scratch_pool); + move = map_deleted_path_to_move(moved_along_path, moves, scratch_pool); + if (move) + return move; + } + + return closest_move; +} + +/* Search for nested moves in REVISION, given the already found MOVES, + * all DELETED_PATHS, and all COPIES, from the same revision. + * Append any nested moves to the MOVES array. */ +static svn_error_t * +find_nested_moves(apr_array_header_t *moves, + apr_hash_t *copies, + apr_array_header_t *deleted_paths, + apr_hash_t *moved_paths, + svn_revnum_t revision, + const char *author, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *nested_moves; + int i; + apr_pool_t *iterpool; + + nested_moves = apr_array_make(result_pool, 0, + sizeof(struct repos_move_info *)); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < deleted_paths->nelts; i++) + { + const char *deleted_path; + const char *child_relpath; + const char *moved_along_repos_relpath; + struct repos_move_info *move; + apr_array_header_t *copies_with_same_source_path; + int j; + svn_boolean_t related; + + svn_pool_clear(iterpool); + + deleted_path = APR_ARRAY_IDX(deleted_paths, i, const char *); + move = map_deleted_path_to_move(deleted_path, moves, iterpool); + if (move == NULL) + continue; + child_relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, + deleted_path); + if (child_relpath == NULL || child_relpath[0] == '\0') + continue; /* not a nested move */ + + /* Consider: svn mv A B; svn mv B/foo C/foo + * Copyfrom for C/foo is A/foo, even though C/foo was moved here from + * B/foo. A/foo was not deleted. It is B/foo which was deleted. + * We now know about the move A->B and moved-along child_relpath "foo". + * Try to detect an ancestral relationship between A/foo and the + * moved-along path. */ + moved_along_repos_relpath = + svn_relpath_join(move->moved_from_repos_relpath, child_relpath, + iterpool); + copies_with_same_source_path = svn_hash_gets(copies, + moved_along_repos_relpath); + if (copies_with_same_source_path == NULL) + continue; /* not a nested move */ + + for (j = 0; j < copies_with_same_source_path->nelts; j++) + { + struct copy_info *copy; + + copy = APR_ARRAY_IDX(copies_with_same_source_path, j, + struct copy_info *); + SVN_ERR(check_move_ancestry(&related, ra_session, repos_root_url, + moved_along_repos_relpath, + revision, + copy->copyfrom_path, + copy->copyfrom_rev, + TRUE, iterpool)); + if (related) + { + struct repos_move_info *nested_move; + + /* Remember details of this move. */ + SVN_ERR(add_new_move(&nested_move, moved_along_repos_relpath, + copy->copyto_path, copy->copyfrom_rev, + copy->node_kind, + revision, author, moved_paths, + ra_session, repos_root_url, + result_pool, iterpool)); + + /* Add this move to the list of nested moves in this revision. */ + APR_ARRAY_PUSH(nested_moves, struct repos_move_info *) = + nested_move; + } + } + } + svn_pool_destroy(iterpool); + + /* Add all nested moves found to the list of all moves in this revision. */ + apr_array_cat(moves, nested_moves); + + return SVN_NO_ERROR; +} + +/* Make a shallow copy of the copied LOG_ITEM in COPIES. */ +static void +cache_copied_item(apr_hash_t *copies, const char *changed_path, + svn_log_changed_path2_t *log_item) +{ + apr_pool_t *result_pool = apr_hash_pool_get(copies); + struct copy_info *copy = apr_palloc(result_pool, sizeof(*copy)); + apr_array_header_t *copies_with_same_source_path; + + copy->copyfrom_path = log_item->copyfrom_path; + if (log_item->copyfrom_path[0] == '/') + copy->copyfrom_path++; + copy->copyto_path = changed_path; + copy->copyfrom_rev = log_item->copyfrom_rev; + copy->node_kind = log_item->node_kind; + + copies_with_same_source_path = apr_hash_get(copies, copy->copyfrom_path, + APR_HASH_KEY_STRING); + if (copies_with_same_source_path == NULL) + { + copies_with_same_source_path = apr_array_make(result_pool, 1, + sizeof(struct copy_info *)); + apr_hash_set(copies, copy->copyfrom_path, APR_HASH_KEY_STRING, + copies_with_same_source_path); + } + APR_ARRAY_PUSH(copies_with_same_source_path, struct copy_info *) = copy; +} + +/* Implements svn_log_entry_receiver_t. + * + * Find the revision in which a node, optionally ancestrally related to the + * node specified via find_deleted_rev_baton, was deleted, When the revision + * was found, store it in BATON->DELETED_REV and abort the log operation + * by raising SVN_ERR_CEASE_INVOCATION. + * + * If no such revision can be found, leave BATON->DELETED_REV and + * BATON->REPLACING_NODE_KIND alone. + * + * If the node was replaced, set BATON->REPLACING_NODE_KIND to the node + * kind of the node which replaced the original node. If the node was not + * replaced, set BATON->REPLACING_NODE_KIND to svn_node_none. + * + * This function answers the same question as svn_ra_get_deleted_rev() but + * works in cases where we do not already know a revision in which the deleted + * node once used to exist. + * + * If the node was moved, rather than deleted, return move information + * in BATON->MOVE. + */ +static svn_error_t * +find_deleted_rev(void *baton, + svn_log_entry_t *log_entry, + apr_pool_t *scratch_pool) +{ + struct find_deleted_rev_baton *b = baton; + apr_hash_index_t *hi; + apr_pool_t *iterpool; + svn_boolean_t deleted_node_found = FALSE; + svn_node_kind_t replacing_node_kind = svn_node_none; + + if (b->ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + b->victim_abspath, + svn_wc_notify_tree_conflict_details_progress, + scratch_pool), + notify->revision = log_entry->revision; + b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); + } + + /* No paths were changed in this revision. Nothing to do. */ + if (! log_entry->changed_paths2) + return SVN_NO_ERROR; + + iterpool = svn_pool_create(scratch_pool); + for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *changed_path = apr_hash_this_key(hi); + svn_log_changed_path2_t *log_item = apr_hash_this_val(hi); + + svn_pool_clear(iterpool); + + /* ### Remove leading slash from paths in log entries. */ + if (changed_path[0] == '/') + changed_path++; + + /* Check if we already found the deleted node we're looking for. */ + if (!deleted_node_found && + svn_path_compare_paths(b->deleted_repos_relpath, changed_path) == 0 && + (log_item->action == 'D' || log_item->action == 'R')) + { + deleted_node_found = TRUE; + + if (b->related_repos_relpath != NULL && + b->related_peg_rev != SVN_INVALID_REVNUM) + { + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + + /* We found a deleted node which occupies the correct path. + * To be certain that this is the deleted node we're looking for, + * we must establish whether it is ancestrally related to the + * "related node" specified in our baton. */ + err = find_yca(&yca_loc, + b->related_repos_relpath, + b->related_peg_rev, + b->deleted_repos_relpath, + rev_below(log_entry->revision), + b->repos_root_url, b->repos_uuid, + b->extra_ra_session, b->ctx, iterpool, iterpool); + if (err) + { + /* ### Happens for moves within other moves and copies. */ + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + deleted_node_found = (yca_loc != NULL); + } + + if (deleted_node_found && log_item->action == 'R') + replacing_node_kind = log_item->node_kind; + } + } + svn_pool_destroy(iterpool); + + if (!deleted_node_found) + { + apr_array_header_t *moves; + + moves = apr_hash_get(b->moves_table, &log_entry->revision, + sizeof(svn_revnum_t)); + if (moves) + { + struct repos_move_info *move; + + move = map_deleted_path_to_move(b->deleted_repos_relpath, + moves, scratch_pool); + if (move) + { + const char *relpath; + + /* The node was moved. Update our search path accordingly. */ + b->move = move; + relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, + b->deleted_repos_relpath); + if (relpath) + b->deleted_repos_relpath = + svn_relpath_join(move->moved_from_repos_relpath, relpath, + b->result_pool); + } + } + } + else + { + svn_string_t *author; + + b->deleted_rev = log_entry->revision; + author = svn_hash_gets(log_entry->revprops, + SVN_PROP_REVISION_AUTHOR); + if (author) + b->deleted_rev_author = apr_pstrdup(b->result_pool, author->data); + else + b->deleted_rev_author = _("unknown author"); + + b->replacing_node_kind = replacing_node_kind; + + /* We're done. Abort the log operation. */ + return svn_error_create(SVN_ERR_CEASE_INVOCATION, NULL, NULL); + } + + return SVN_NO_ERROR; +} + +/* Return a localised string representation of the local part of a tree + conflict on a file. */ +static svn_error_t * +describe_local_file_node_change(const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_reason_t local_change; + svn_wc_operation_t operation; + + local_change = svn_client_conflict_get_local_change(conflict); + operation = svn_client_conflict_get_operation(conflict); + + switch (local_change) + { + case svn_wc_conflict_reason_edited: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("A file containing uncommitted changes was " + "found in the working copy."); + else if (operation == svn_wc_operation_merge) + *description = _("A file which differs from the corresponding " + "file on the merge source branch was found " + "in the working copy."); + break; + case svn_wc_conflict_reason_obstructed: + *description = _("A file which already occupies this path was found " + "in the working copy."); + break; + case svn_wc_conflict_reason_unversioned: + *description = _("An unversioned file was found in the working " + "copy."); + break; + case svn_wc_conflict_reason_deleted: + *description = _("A deleted file was found in the working copy."); + break; + case svn_wc_conflict_reason_missing: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("No such file was found in the working copy."); + else if (operation == svn_wc_operation_merge) + { + /* ### display deleted revision */ + *description = _("No such file was found in the merge target " + "working copy.\nPerhaps the file has been " + "deleted or moved away in the repository's " + "history?"); + } + break; + case svn_wc_conflict_reason_added: + case svn_wc_conflict_reason_replaced: + { + /* ### show more details about copies or replacements? */ + *description = _("A file scheduled to be added to the " + "repository in the next commit was found in " + "the working copy."); + } + break; + case svn_wc_conflict_reason_moved_away: + { + const char *moved_to_abspath; + svn_error_t *err; + + err = svn_wc__node_was_moved_away(&moved_to_abspath, NULL, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) + { + moved_to_abspath = NULL; + svn_error_clear(err); + } + else + return svn_error_trace(err); + } + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (moved_to_abspath == NULL) + { + /* The move no longer exists. */ + *description = _("The file in the working copy had " + "been moved away at the time this " + "conflict was recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("The file in the working copy was " + "moved away to\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_to_abspath), + scratch_pool)); + } + } + else if (operation == svn_wc_operation_merge) + { + if (moved_to_abspath == NULL) + { + /* The move probably happened in branch history. + * This case cannot happen until we detect incoming + * moves, which we currently don't do. */ + /* ### find deleted/moved revision? */ + *description = _("The file in the working copy had " + "been moved away at the time this " + "conflict was recorded."); + } + else + { + /* This is a local move in the working copy. */ + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("The file in the working copy was " + "moved away to\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_to_abspath), + scratch_pool)); + } + } + break; + } + case svn_wc_conflict_reason_moved_here: + { + const char *moved_from_abspath; + + SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (moved_from_abspath == NULL) + { + /* The move no longer exists. */ + *description = _("A file had been moved here in the " + "working copy at the time this " + "conflict was recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("A file was moved here in the " + "working copy from\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_from_abspath), + scratch_pool)); + } + } + else if (operation == svn_wc_operation_merge) + { + if (moved_from_abspath == NULL) + { + /* The move probably happened in branch history. + * This case cannot happen until we detect incoming + * moves, which we currently don't do. */ + /* ### find deleted/moved revision? */ + *description = _("A file had been moved here in the " + "working copy at the time this " + "conflict was recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + /* This is a local move in the working copy. */ + *description = apr_psprintf( + result_pool, + _("A file was moved here in the " + "working copy from\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_from_abspath), + scratch_pool)); + } + } + break; + } + } + + return SVN_NO_ERROR; +} + +/* Return a localised string representation of the local part of a tree + conflict on a directory. */ +static svn_error_t * +describe_local_dir_node_change(const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_reason_t local_change; + svn_wc_operation_t operation; + + local_change = svn_client_conflict_get_local_change(conflict); + operation = svn_client_conflict_get_operation(conflict); + + switch (local_change) + { + case svn_wc_conflict_reason_edited: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("A directory containing uncommitted changes " + "was found in the working copy."); + else if (operation == svn_wc_operation_merge) + *description = _("A directory which differs from the " + "corresponding directory on the merge source " + "branch was found in the working copy."); + break; + case svn_wc_conflict_reason_obstructed: + *description = _("A directory which already occupies this path was " + "found in the working copy."); + break; + case svn_wc_conflict_reason_unversioned: + *description = _("An unversioned directory was found in the " + "working copy."); + break; + case svn_wc_conflict_reason_deleted: + *description = _("A deleted directory was found in the " + "working copy."); + break; + case svn_wc_conflict_reason_missing: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("No such directory was found in the working copy."); + else if (operation == svn_wc_operation_merge) + { + /* ### display deleted revision */ + *description = _("No such directory was found in the merge " + "target working copy.\nPerhaps the " + "directory has been deleted or moved away " + "in the repository's history?"); + } + break; + case svn_wc_conflict_reason_added: + case svn_wc_conflict_reason_replaced: + { + /* ### show more details about copies or replacements? */ + *description = _("A directory scheduled to be added to the " + "repository in the next commit was found in " + "the working copy."); + } + break; + case svn_wc_conflict_reason_moved_away: + { + const char *moved_to_abspath; + svn_error_t *err; + + err = svn_wc__node_was_moved_away(&moved_to_abspath, NULL, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) + { + moved_to_abspath = NULL; + svn_error_clear(err); + } + else + return svn_error_trace(err); + } + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (moved_to_abspath == NULL) + { + /* The move no longer exists. */ + *description = _("The directory in the working copy " + "had been moved away at the time " + "this conflict was recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("The directory in the working copy " + "was moved away to\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_to_abspath), + scratch_pool)); + } + } + else if (operation == svn_wc_operation_merge) + { + if (moved_to_abspath == NULL) + { + /* The move probably happened in branch history. + * This case cannot happen until we detect incoming + * moves, which we currently don't do. */ + /* ### find deleted/moved revision? */ + *description = _("The directory had been moved away " + "at the time this conflict was " + "recorded."); + } + else + { + /* This is a local move in the working copy. */ + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("The directory was moved away to\n" + "'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_to_abspath), + scratch_pool)); + } + } + } + break; + case svn_wc_conflict_reason_moved_here: + { + const char *moved_from_abspath; + + SVN_ERR(svn_wc__node_was_moved_here(&moved_from_abspath, NULL, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (moved_from_abspath == NULL) + { + /* The move no longer exists. */ + *description = _("A directory had been moved here at " + "the time this conflict was " + "recorded."); + } + else + { + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("A directory was moved here from\n" + "'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_from_abspath), + scratch_pool)); + } + } + else if (operation == svn_wc_operation_merge) + { + if (moved_from_abspath == NULL) + { + /* The move probably happened in branch history. + * This case cannot happen until we detect incoming + * moves, which we currently don't do. */ + /* ### find deleted/moved revision? */ + *description = _("A directory had been moved here at " + "the time this conflict was " + "recorded."); + } + else + { + /* This is a local move in the working copy. */ + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, + ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, + scratch_pool)); + *description = apr_psprintf( + result_pool, + _("A directory was moved here in " + "the working copy from\n'%s'."), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + moved_from_abspath), + scratch_pool)); + } + } + } + } + + return SVN_NO_ERROR; +} + +struct find_moves_baton +{ + /* Variables below are arguments provided by the caller of + * svn_ra_get_log2(). */ + const char *repos_root_url; + const char *repos_uuid; + svn_client_ctx_t *ctx; + const char *victim_abspath; /* for notifications */ + apr_pool_t *result_pool; + + /* A hash table mapping a revision number to an array of struct + * repos_move_info * elements, describing moves. + * + * Must be allocated in RESULT_POOL by the caller of svn_ra_get_log2(). + * + * If the node was moved, the DELETED_REV is present in this table, + * perhaps along with additional revisions. + * + * Given a sequence of moves which happened in the repository, such as: + * rA: mv x->z + * rA: mv a->b + * rB: mv b->c + * rC: mv c->d + * we map each revision number to all the moves which happened in the + * revision, which looks as follows: + * rA : [(x->z), (a->b)] + * rB : [(b->c)] + * rC : [(c->d)] + * This allows us to later find relevant moves based on a revision number. + * + * Additionally, we embed the number of the revision in which a move was + * found inside the repos_move_info structure: + * rA : [(rA, x->z), (rA, a->b)] + * rB : [(rB, b->c)] + * rC : [(rC, c->d)] + * And also, all moves pertaining to the same node are chained into a + * doubly-linked list via 'next' and 'prev' pointers (see definition of + * struct repos_move_info). This can be visualized as follows: + * rA : [(rA, x->z, prev=>NULL, next=>NULL), + * (rA, a->b, prev=>NULL, next=>(rB, b->c))] + * rB : [(rB, b->c), prev=>(rA, a->b), next=>(rC, c->d)] + * rC : [(rC, c->d), prev=>(rB, c->d), next=>NULL] + * This way, we can look up all moves relevant to a node, forwards and + * backwards in history, once we have located one move in the chain. + * + * In the above example, the data tells us that within the revision + * range rA:C, a was moved to d. However, within the revision range + * rA;B, a was moved to b. + */ + apr_hash_t *moves_table; + + /* Variables below hold state for find_moves() and are not + * intended to be used by the caller of svn_ra_get_log2(). + * Like all other variables, they must be initialized, however. */ + + /* Temporary map of moved paths to struct repos_move_info. + * Used to link multiple moves of the same node across revisions. */ + apr_hash_t *moved_paths; + + /* Extra RA session that can be used to make additional requests. */ + svn_ra_session_t *extra_ra_session; +}; + +/* Implements svn_log_entry_receiver_t. */ +static svn_error_t * +find_moves(void *baton, svn_log_entry_t *log_entry, apr_pool_t *scratch_pool) +{ + struct find_moves_baton *b = baton; + apr_hash_index_t *hi; + apr_pool_t *iterpool; + apr_array_header_t *deleted_paths; + apr_hash_t *copies; + apr_array_header_t *moves; + + if (b->ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + b->victim_abspath, + svn_wc_notify_tree_conflict_details_progress, + scratch_pool), + notify->revision = log_entry->revision; + b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); + } + + /* No paths were changed in this revision. Nothing to do. */ + if (! log_entry->changed_paths2) + return SVN_NO_ERROR; + + copies = apr_hash_make(scratch_pool); + deleted_paths = apr_array_make(scratch_pool, 0, sizeof(const char *)); + iterpool = svn_pool_create(scratch_pool); + for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *changed_path = apr_hash_this_key(hi); + svn_log_changed_path2_t *log_item = apr_hash_this_val(hi); + + svn_pool_clear(iterpool); + + /* ### Remove leading slash from paths in log entries. */ + if (changed_path[0] == '/') + changed_path++; + + /* For move detection, scan for copied nodes in this revision. */ + if (log_item->action == 'A' && log_item->copyfrom_path) + cache_copied_item(copies, changed_path, log_item); + + /* For move detection, store all deleted_paths. */ + if (log_item->action == 'D' || log_item->action == 'R') + APR_ARRAY_PUSH(deleted_paths, const char *) = + apr_pstrdup(scratch_pool, changed_path); + } + svn_pool_destroy(iterpool); + + /* Check for moves in this revision */ + SVN_ERR(find_moves_in_revision(b->extra_ra_session, + b->moves_table, b->moved_paths, + log_entry, copies, deleted_paths, + b->repos_root_url, b->repos_uuid, + b->ctx, b->result_pool, scratch_pool)); + + moves = apr_hash_get(b->moves_table, &log_entry->revision, + sizeof(svn_revnum_t)); + if (moves) + { + const svn_string_t *author; + + author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR); + SVN_ERR(find_nested_moves(moves, copies, deleted_paths, + b->moved_paths, log_entry->revision, + author ? author->data : _("unknown author"), + b->repos_root_url, + b->repos_uuid, + b->extra_ra_session, b->ctx, + b->result_pool, scratch_pool)); + } + + return SVN_NO_ERROR; +} + +/* Find all moves which occured in repository history starting at + * REPOS_RELPATH@START_REV until END_REV (where START_REV > END_REV). + * Return results in *MOVES_TABLE (see struct find_moves_baton for details). */ +static svn_error_t * +find_moves_in_revision_range(struct apr_hash_t **moves_table, + const char *repos_relpath, + const char *repos_root_url, + const char *repos_uuid, + const char *victim_abspath, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + apr_array_header_t *paths; + apr_array_header_t *revprops; + struct find_moves_baton b = { 0 }; + + SVN_ERR_ASSERT(start_rev > end_rev); + + url = svn_path_url_add_component2(repos_root_url, repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + + paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(paths, const char *) = ""; + + revprops = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; + + b.repos_root_url = repos_root_url; + b.repos_uuid = repos_uuid; + b.ctx = ctx; + b.victim_abspath = victim_abspath; + b.moves_table = apr_hash_make(result_pool); + b.moved_paths = apr_hash_make(scratch_pool); + b.result_pool = result_pool; + SVN_ERR(svn_ra__dup_session(&b.extra_ra_session, ra_session, NULL, + scratch_pool, scratch_pool)); + + SVN_ERR(svn_ra_get_log2(ra_session, paths, start_rev, end_rev, + 0, /* no limit */ + TRUE, /* need the changed paths list */ + FALSE, /* need to traverse copies */ + FALSE, /* no need for merged revisions */ + revprops, + find_moves, &b, + scratch_pool)); + + *moves_table = b.moves_table; + + return SVN_NO_ERROR; +} + +/* Return new move information for a moved-along child MOVED_ALONG_RELPATH. + * Set MOVE->NODE_KIND to MOVED_ALONG_NODE_KIND. + * Do not copy MOVE->NEXT and MOVE-PREV. + * If MOVED_ALONG_RELPATH is empty, this effectively copies MOVE to + * RESULT_POOL with NEXT and PREV pointers cleared. */ +static struct repos_move_info * +new_path_adjusted_move(struct repos_move_info *move, + const char *moved_along_relpath, + svn_node_kind_t moved_along_node_kind, + apr_pool_t *result_pool) +{ + struct repos_move_info *new_move; + + new_move = apr_pcalloc(result_pool, sizeof(*new_move)); + new_move->moved_from_repos_relpath = + svn_relpath_join(move->moved_from_repos_relpath, moved_along_relpath, + result_pool); + new_move->moved_to_repos_relpath = + svn_relpath_join(move->moved_to_repos_relpath, moved_along_relpath, + result_pool); + new_move->rev = move->rev; + new_move->rev_author = apr_pstrdup(result_pool, move->rev_author); + new_move->copyfrom_rev = move->copyfrom_rev; + new_move->node_kind = moved_along_node_kind; + /* Ignore prev and next pointers. Caller will set them if needed. */ + + return new_move; +} + +/* Given a list of MOVES_IN_REVISION, figure out which of these moves again + * move the node which was already moved by PREV_MOVE in the past . */ +static svn_error_t * +find_next_moves_in_revision(apr_array_header_t **next_moves, + apr_array_header_t *moves_in_revision, + struct repos_move_info *prev_move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + int i; + apr_pool_t *iterpool; + + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < moves_in_revision->nelts; i++) + { + struct repos_move_info *move; + const char *relpath; + const char *deleted_repos_relpath; + svn_boolean_t related; + svn_error_t *err; + + svn_pool_clear(iterpool); + + /* Check if this move affects the current known path of our node. */ + move = APR_ARRAY_IDX(moves_in_revision, i, struct repos_move_info *); + relpath = svn_relpath_skip_ancestor(move->moved_from_repos_relpath, + prev_move->moved_to_repos_relpath); + if (relpath == NULL) + continue; + + /* It does. So our node must have been deleted again. */ + deleted_repos_relpath = svn_relpath_join(move->moved_from_repos_relpath, + relpath, iterpool); + + /* Tracing back history of the delete-half of this move to the + * copyfrom-revision of the prior move we must end up at the + * delete-half of the prior move. */ + err = check_move_ancestry(&related, ra_session, repos_root_url, + deleted_repos_relpath, move->rev, + prev_move->moved_from_repos_relpath, + prev_move->copyfrom_rev, + FALSE, scratch_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + continue; + } + else + SVN_ERR(err); + + if (related) + { + struct repos_move_info *new_move; + + /* We have a winner. */ + new_move = new_path_adjusted_move(move, relpath, prev_move->node_kind, + result_pool); + if (*next_moves == NULL) + *next_moves = apr_array_make(result_pool, 1, + sizeof(struct repos_move_info *)); + APR_ARRAY_PUSH(*next_moves, struct repos_move_info *) = new_move; + } + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +static int +compare_items_as_revs(const svn_sort__item_t *a, const svn_sort__item_t *b) +{ + return svn_sort_compare_revisions(a->key, b->key); +} + +/* Starting at MOVE->REV, loop over future revisions which contain moves, + * and look for matching next moves in each. Once found, return a list of + * (ambiguous, if more than one) moves in *NEXT_MOVES. */ +static svn_error_t * +find_next_moves(apr_array_header_t **next_moves, + apr_hash_t *moves_table, + struct repos_move_info *move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *moves; + apr_array_header_t *revisions; + apr_pool_t *iterpool; + int i; + + *next_moves = NULL; + revisions = svn_sort__hash(moves_table, compare_items_as_revs, scratch_pool); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < revisions->nelts; i++) + { + svn_sort__item_t item = APR_ARRAY_IDX(revisions, i, svn_sort__item_t); + svn_revnum_t rev = *(svn_revnum_t *)item.key; + + svn_pool_clear(iterpool); + + if (rev <= move->rev) + continue; + + moves = apr_hash_get(moves_table, &rev, sizeof(rev)); + SVN_ERR(find_next_moves_in_revision(next_moves, moves, move, + ra_session, repos_root_url, + result_pool, iterpool)); + if (*next_moves) + break; + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Trace all future moves of the node moved by MOVE. + * Update MOVE->PREV and MOVE->NEXT accordingly. */ +static svn_error_t * +trace_moved_node(apr_hash_t *moves_table, + struct repos_move_info *move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *next_moves; + + SVN_ERR(find_next_moves(&next_moves, moves_table, move, + ra_session, repos_root_url, + result_pool, scratch_pool)); + if (next_moves) + { + int i; + apr_pool_t *iterpool; + + move->next = next_moves; + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < next_moves->nelts; i++) + { + struct repos_move_info *next_move; + + svn_pool_clear(iterpool); + next_move = APR_ARRAY_IDX(next_moves, i, struct repos_move_info *); + next_move->prev = move; + SVN_ERR(trace_moved_node(moves_table, next_move, + ra_session, repos_root_url, + result_pool, iterpool)); + } + svn_pool_destroy(iterpool); + } + + return SVN_NO_ERROR; +} + +/* Given a list of MOVES_IN_REVISION, figure out which of these moves + * move the node which was later on moved by NEXT_MOVE. */ +static svn_error_t * +find_prev_move_in_revision(struct repos_move_info **prev_move, + apr_array_header_t *moves_in_revision, + struct repos_move_info *next_move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + int i; + apr_pool_t *iterpool; + + *prev_move = NULL; + + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < moves_in_revision->nelts; i++) + { + struct repos_move_info *move; + const char *relpath; + const char *deleted_repos_relpath; + svn_boolean_t related; + svn_error_t *err; + + svn_pool_clear(iterpool); + + /* Check if this move affects the current known path of our node. */ + move = APR_ARRAY_IDX(moves_in_revision, i, struct repos_move_info *); + relpath = svn_relpath_skip_ancestor(next_move->moved_from_repos_relpath, + move->moved_to_repos_relpath); + if (relpath == NULL) + continue; + + /* It does. So our node must have been deleted. */ + deleted_repos_relpath = svn_relpath_join( + next_move->moved_from_repos_relpath, + relpath, iterpool); + + /* Tracing back history of the delete-half of the next move to the + * copyfrom-revision of the prior move we must end up at the + * delete-half of the prior move. */ + err = check_move_ancestry(&related, ra_session, repos_root_url, + deleted_repos_relpath, next_move->rev, + move->moved_from_repos_relpath, + move->copyfrom_rev, + FALSE, scratch_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + continue; + } + else + SVN_ERR(err); + + if (related) + { + /* We have a winner. */ + *prev_move = new_path_adjusted_move(move, relpath, + next_move->node_kind, + result_pool); + break; + } + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +static int +compare_items_as_revs_reverse(const svn_sort__item_t *a, + const svn_sort__item_t *b) +{ + int c = svn_sort_compare_revisions(a->key, b->key); + if (c < 0) + return 1; + if (c > 0) + return -1; + return c; +} + +/* Starting at MOVE->REV, loop over past revisions which contain moves, + * and look for a matching previous move in each. Once found, return + * it in *PREV_MOVE */ +static svn_error_t * +find_prev_move(struct repos_move_info **prev_move, + apr_hash_t *moves_table, + struct repos_move_info *move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *moves; + apr_array_header_t *revisions; + apr_pool_t *iterpool; + int i; + + *prev_move = NULL; + revisions = svn_sort__hash(moves_table, compare_items_as_revs_reverse, + scratch_pool); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < revisions->nelts; i++) + { + svn_sort__item_t item = APR_ARRAY_IDX(revisions, i, svn_sort__item_t); + svn_revnum_t rev = *(svn_revnum_t *)item.key; + + svn_pool_clear(iterpool); + + if (rev >= move->rev) + continue; + + moves = apr_hash_get(moves_table, &rev, sizeof(rev)); + SVN_ERR(find_prev_move_in_revision(prev_move, moves, move, + ra_session, repos_root_url, + result_pool, iterpool)); + if (*prev_move) + break; + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + + +/* Trace all past moves of the node moved by MOVE. + * Update MOVE->PREV and MOVE->NEXT accordingly. */ +static svn_error_t * +trace_moved_node_backwards(apr_hash_t *moves_table, + struct repos_move_info *move, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct repos_move_info *prev_move; + + SVN_ERR(find_prev_move(&prev_move, moves_table, move, + ra_session, repos_root_url, + result_pool, scratch_pool)); + if (prev_move) + { + move->prev = prev_move; + prev_move->next = apr_array_make(result_pool, 1, + sizeof(struct repos_move_info *)); + APR_ARRAY_PUSH(prev_move->next, struct repos_move_info *) = move; + + SVN_ERR(trace_moved_node_backwards(moves_table, prev_move, + ra_session, repos_root_url, + result_pool, scratch_pool)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +reparent_session_and_fetch_node_kind(svn_node_kind_t *node_kind, + svn_ra_session_t *ra_session, + const char *url, + svn_revnum_t peg_rev, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + + err = svn_ra_reparent(ra_session, url, scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_RA_ILLEGAL_URL) + { + svn_error_clear(err); + *node_kind = svn_node_unknown; + return SVN_NO_ERROR; + } + + return svn_error_trace(err); + } + + SVN_ERR(svn_ra_check_path(ra_session, "", peg_rev, node_kind, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Scan MOVES_TABLE for moves which affect a particular deleted node, and + * build a set of new move information for this node. + * Return heads of all possible move chains in *MOVES. + * + * MOVES_TABLE describes moves which happened at arbitrary paths in the + * repository. DELETED_REPOS_RELPATH may have been moved directly or it + * may have been moved along with a parent path. Move information returned + * from this function represents how DELETED_REPOS_RELPATH itself was moved + * from one path to another, effectively "zooming in" on the effective move + * operations which occurred for this particular node. */ +static svn_error_t * +find_operative_moves(apr_array_header_t **moves, + apr_hash_t *moves_table, + const char *deleted_repos_relpath, + svn_revnum_t deleted_rev, + svn_ra_session_t *ra_session, + const char *repos_root_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *moves_in_deleted_rev; + int i; + apr_pool_t *iterpool; + const char *session_url, *url = NULL; + + moves_in_deleted_rev = apr_hash_get(moves_table, &deleted_rev, + sizeof(deleted_rev)); + if (moves_in_deleted_rev == NULL) + { + *moves = NULL; + return SVN_NO_ERROR; + } + + SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, scratch_pool)); + + /* Look for operative moves in the revision where the node was deleted. */ + *moves = apr_array_make(scratch_pool, 0, sizeof(struct repos_move_info *)); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < moves_in_deleted_rev->nelts; i++) + { + struct repos_move_info *move; + const char *relpath; + + svn_pool_clear(iterpool); + + move = APR_ARRAY_IDX(moves_in_deleted_rev, i, struct repos_move_info *); + relpath = svn_relpath_skip_ancestor(move->moved_from_repos_relpath, + deleted_repos_relpath); + if (relpath && relpath[0] != '\0') + { + svn_node_kind_t node_kind; + + url = svn_path_url_add_component2(repos_root_url, + deleted_repos_relpath, + iterpool); + SVN_ERR(reparent_session_and_fetch_node_kind(&node_kind, + ra_session, url, + rev_below(deleted_rev), + iterpool)); + move = new_path_adjusted_move(move, relpath, node_kind, result_pool); + } + APR_ARRAY_PUSH(*moves, struct repos_move_info *) = move; + } + + if (url != NULL) + SVN_ERR(svn_ra_reparent(ra_session, session_url, scratch_pool)); + + /* If we didn't find any applicable moves, return NULL. */ + if ((*moves)->nelts == 0) + { + *moves = NULL; + svn_pool_destroy(iterpool); + return SVN_NO_ERROR; + } + + /* Figure out what happened to these moves in future revisions. */ + for (i = 0; i < (*moves)->nelts; i++) + { + struct repos_move_info *move; + + svn_pool_clear(iterpool); + + move = APR_ARRAY_IDX(*moves, i, struct repos_move_info *); + SVN_ERR(trace_moved_node(moves_table, move, ra_session, repos_root_url, + result_pool, iterpool)); + } + + svn_pool_destroy(iterpool); + return SVN_NO_ERROR; +} + +/* Try to find a revision older than START_REV, and its author, which deleted + * DELETED_BASENAME in the directory PARENT_REPOS_RELPATH. Assume the deleted + * node is ancestrally related to RELATED_REPOS_RELPATH@RELATED_PEG_REV. + * If no such revision can be found, set *DELETED_REV to SVN_INVALID_REVNUM + * and *DELETED_REV_AUTHOR to NULL. + * If the node was replaced rather than deleted, set *REPLACING_NODE_KIND to + * the node kind of the replacing node. Else, set it to svn_node_unknown. + * Only request the log for revisions up to END_REV from the server. + * If the deleted node was moved, provide heads of move chains in *MOVES. + * If the node was not moved,set *MOVES to NULL. + */ +static svn_error_t * +find_revision_for_suspected_deletion(svn_revnum_t *deleted_rev, + const char **deleted_rev_author, + svn_node_kind_t *replacing_node_kind, + struct apr_array_header_t **moves, + svn_client_conflict_t *conflict, + const char *deleted_basename, + const char *parent_repos_relpath, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + const char *related_repos_relpath, + svn_revnum_t related_peg_rev, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + apr_array_header_t *paths; + apr_array_header_t *revprops; + const char *repos_root_url; + const char *repos_uuid; + struct find_deleted_rev_baton b = { 0 }; + const char *victim_abspath; + svn_error_t *err; + apr_hash_t *moves_table; + + SVN_ERR_ASSERT(start_rev > end_rev); + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, &repos_uuid, + conflict, scratch_pool, + scratch_pool)); + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(find_moves_in_revision_range(&moves_table, parent_repos_relpath, + repos_root_url, repos_uuid, + victim_abspath, start_rev, end_rev, + ctx, result_pool, scratch_pool)); + + url = svn_path_url_add_component2(repos_root_url, parent_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + + paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(paths, const char *) = ""; + + revprops = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; + + b.victim_abspath = victim_abspath; + b.deleted_repos_relpath = svn_relpath_join(parent_repos_relpath, + deleted_basename, scratch_pool); + b.related_repos_relpath = related_repos_relpath; + b.related_peg_rev = related_peg_rev; + b.deleted_rev = SVN_INVALID_REVNUM; + b.replacing_node_kind = svn_node_unknown; + b.repos_root_url = repos_root_url; + b.repos_uuid = repos_uuid; + b.ctx = ctx; + b.moves_table = moves_table; + b.result_pool = result_pool; + SVN_ERR(svn_ra__dup_session(&b.extra_ra_session, ra_session, NULL, + scratch_pool, scratch_pool)); + + err = svn_ra_get_log2(ra_session, paths, start_rev, end_rev, + 0, /* no limit */ + TRUE, /* need the changed paths list */ + FALSE, /* need to traverse copies */ + FALSE, /* no need for merged revisions */ + revprops, + find_deleted_rev, &b, + scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_CEASE_INVOCATION && + b.deleted_rev != SVN_INVALID_REVNUM) + + { + /* Log operation was aborted because we found deleted rev. */ + svn_error_clear(err); + } + else + return svn_error_trace(err); + } + + if (b.deleted_rev == SVN_INVALID_REVNUM) + { + struct repos_move_info *move = b.move; + + if (move) + { + *deleted_rev = move->rev; + *deleted_rev_author = move->rev_author; + *replacing_node_kind = b.replacing_node_kind; + SVN_ERR(find_operative_moves(moves, moves_table, + b.deleted_repos_relpath, + move->rev, + ra_session, repos_root_url, + result_pool, scratch_pool)); + } + else + { + /* We could not determine the revision in which the node was + * deleted. */ + *deleted_rev = SVN_INVALID_REVNUM; + *deleted_rev_author = NULL; + *replacing_node_kind = svn_node_unknown; + *moves = NULL; + } + return SVN_NO_ERROR; + } + else + { + *deleted_rev = b.deleted_rev; + *deleted_rev_author = b.deleted_rev_author; + *replacing_node_kind = b.replacing_node_kind; + SVN_ERR(find_operative_moves(moves, moves_table, + b.deleted_repos_relpath, b.deleted_rev, + ra_session, repos_root_url, + result_pool, scratch_pool)); + } + + return SVN_NO_ERROR; +} + +/* Details for tree conflicts involving a locally missing node. */ +struct conflict_tree_local_missing_details +{ + /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. */ + svn_revnum_t deleted_rev; + + /* Author who committed DELETED_REV. */ + const char *deleted_rev_author; + + /* The path which was deleted relative to the repository root. */ + const char *deleted_repos_relpath; + + /* Move information about the conflict victim. If not NULL, this is an + * array of repos_move_info elements. Each element is the head of a + * move chain which starts in DELETED_REV. */ + apr_array_header_t *moves; + + /* Move information about siblings. Siblings are nodes which share + * a youngest common ancestor with the conflict victim. E.g. in case + * of a merge operation they are part of the merge source branch. + * If not NULL, this is an array of repos_move_info elements. + * Each element is the head of a move chain, which starts at some + * point in history after siblings and conflict victim forked off + * their common ancestor. */ + apr_array_header_t *sibling_moves; + + /* If not NULL, this is the move target abspath. */ + const char *moved_to_abspath; +}; + +static svn_error_t * +find_related_node(const char **related_repos_relpath, + svn_revnum_t *related_peg_rev, + const char *younger_related_repos_relpath, + svn_revnum_t younger_related_peg_rev, + const char *older_repos_relpath, + svn_revnum_t older_peg_rev, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *repos_root_url; + const char *related_url; + const char *corrected_url; + svn_node_kind_t related_node_kind; + svn_ra_session_t *ra_session; + + *related_repos_relpath = NULL; + *related_peg_rev = SVN_INVALID_REVNUM; + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, + scratch_pool, scratch_pool)); + related_url = svn_path_url_add_component2(repos_root_url, + younger_related_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + related_url, NULL, + NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + SVN_ERR(svn_ra_check_path(ra_session, "", younger_related_peg_rev, + &related_node_kind, scratch_pool)); + if (related_node_kind == svn_node_none) + { + svn_revnum_t related_deleted_rev; + const char *related_deleted_rev_author; + svn_node_kind_t related_replacing_node_kind; + const char *related_basename; + const char *related_parent_repos_relpath; + apr_array_header_t *related_moves; + + /* Looks like the younger node, which we'd like to use as our + * 'related node', was deleted. Try to find its deleted revision + * so we can calculate a peg revision at which it exists. + * The younger node is related to the older node, so we can use + * the older node to guide us in our search. */ + related_basename = svn_relpath_basename(younger_related_repos_relpath, + scratch_pool); + related_parent_repos_relpath = + svn_relpath_dirname(younger_related_repos_relpath, scratch_pool); + SVN_ERR(find_revision_for_suspected_deletion( + &related_deleted_rev, &related_deleted_rev_author, + &related_replacing_node_kind, &related_moves, + conflict, related_basename, + related_parent_repos_relpath, + younger_related_peg_rev, 0, + older_repos_relpath, older_peg_rev, + ctx, conflict->pool, scratch_pool)); + + /* If we can't find a related node, bail. */ + if (related_deleted_rev == SVN_INVALID_REVNUM) + return SVN_NO_ERROR; + + /* The node should exist in the revision before it was deleted. */ + *related_repos_relpath = younger_related_repos_relpath; + *related_peg_rev = rev_below(related_deleted_rev); + } + else + { + *related_repos_relpath = younger_related_repos_relpath; + *related_peg_rev = younger_related_peg_rev; + } + + return SVN_NO_ERROR; +} + +/* Determine if REPOS_RELPATH@PEG_REV was moved at some point in its history. + * History's range of interest ends at END_REV which must be older than PEG_REV. + * + * VICTIM_ABSPATH is the abspath of a conflict victim in the working copy and + * will be used in notifications. + * + * Return any applicable move chain heads in *MOVES. + * If no moves can be found, set *MOVES to NULL. */ +static svn_error_t * +find_moves_in_natural_history(apr_array_header_t **moves, + const char *repos_relpath, + svn_revnum_t peg_rev, + svn_node_kind_t node_kind, + svn_revnum_t end_rev, + const char *victim_abspath, + const char *repos_root_url, + const char *repos_uuid, + svn_ra_session_t *ra_session, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_hash_t *moves_table; + apr_array_header_t *revs; + apr_array_header_t *most_recent_moves = NULL; + int i; + apr_pool_t *iterpool; + + *moves = NULL; + + SVN_ERR(find_moves_in_revision_range(&moves_table, repos_relpath, + repos_root_url, repos_uuid, + victim_abspath, peg_rev, end_rev, + ctx, scratch_pool, scratch_pool)); + + iterpool = svn_pool_create(scratch_pool); + + /* Scan the moves table for applicable moves. */ + revs = svn_sort__hash(moves_table, compare_items_as_revs, scratch_pool); + for (i = revs->nelts - 1; i >= 0; i--) + { + svn_sort__item_t item = APR_ARRAY_IDX(revs, i, svn_sort__item_t); + apr_array_header_t *moves_in_rev = apr_hash_get(moves_table, item.key, + sizeof(svn_revnum_t)); + int j; + + svn_pool_clear(iterpool); + + /* Was repos relpath moved to its location in this revision? */ + for (j = 0; j < moves_in_rev->nelts; j++) + { + struct repos_move_info *move; + const char *relpath; + + move = APR_ARRAY_IDX(moves_in_rev, j, struct repos_move_info *); + relpath = svn_relpath_skip_ancestor(move->moved_to_repos_relpath, + repos_relpath); + if (relpath) + { + /* If the move did not happen in our peg revision, make + * sure this move happened on the same line of history. */ + if (move->rev != peg_rev) + { + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + + err = find_yca(&yca_loc, repos_relpath, peg_rev, + repos_relpath, move->rev, + repos_root_url, repos_uuid, + NULL, ctx, iterpool, iterpool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + if (yca_loc == NULL || yca_loc->rev != move->rev) + continue; + } + + if (most_recent_moves == NULL) + most_recent_moves = + apr_array_make(result_pool, 1, + sizeof(struct repos_move_info *)); + + /* Copy the move to result pool (even if relpath is ""). */ + move = new_path_adjusted_move(move, relpath, node_kind, + result_pool); + APR_ARRAY_PUSH(most_recent_moves, + struct repos_move_info *) = move; + } + } + + /* If we found one move, or several ambiguous moves, we're done. */ + if (most_recent_moves) + break; + } + + if (most_recent_moves && most_recent_moves->nelts > 0) + { + *moves = apr_array_make(result_pool, 1, + sizeof(struct repos_move_info *)); + + /* Figure out what happened to the most recent moves in prior + * revisions and build move chains. */ + for (i = 0; i < most_recent_moves->nelts; i++) + { + struct repos_move_info *move; + + svn_pool_clear(iterpool); + + move = APR_ARRAY_IDX(most_recent_moves, i, struct repos_move_info *); + SVN_ERR(trace_moved_node_backwards(moves_table, move, + ra_session, repos_root_url, + result_pool, iterpool)); + /* Follow the move chain backwards. */ + while (move->prev) + move = move->prev; + + /* Return move heads. */ + APR_ARRAY_PUSH(*moves, struct repos_move_info *) = move; + } + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_details_func_t. */ +static svn_error_t * +conflict_tree_get_details_local_missing(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *old_repos_relpath; + const char *new_repos_relpath; + const char *parent_repos_relpath; + svn_revnum_t parent_peg_rev; + svn_revnum_t old_rev; + svn_revnum_t new_rev; + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + const char *deleted_basename; + struct conflict_tree_local_missing_details *details; + apr_array_header_t *moves = NULL; + apr_array_header_t *sibling_moves = NULL; + const char *related_repos_relpath; + svn_revnum_t related_peg_rev; + const char *repos_root_url; + const char *repos_uuid; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, NULL, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, NULL, conflict, + scratch_pool, scratch_pool)); + + /* Scan the conflict victim's parent's log to find a revision which + * deleted the node. */ + deleted_basename = svn_dirent_basename(conflict->local_abspath, + scratch_pool); + SVN_ERR(svn_wc__node_get_repos_info(&parent_peg_rev, &parent_repos_relpath, + &repos_root_url, &repos_uuid, + ctx->wc_ctx, + svn_dirent_dirname( + conflict->local_abspath, + scratch_pool), + scratch_pool, + scratch_pool)); + + /* Pick the younger incoming node as our 'related node' which helps + * pin-pointing the deleted conflict victim in history. */ + related_repos_relpath = + (old_rev < new_rev ? new_repos_relpath : old_repos_relpath); + related_peg_rev = (old_rev < new_rev ? new_rev : old_rev); + + /* Make sure we're going to search the related node in a revision where + * it exists. The younger incoming node might have been deleted in HEAD. */ + if (related_repos_relpath != NULL && related_peg_rev != SVN_INVALID_REVNUM) + SVN_ERR(find_related_node( + &related_repos_relpath, &related_peg_rev, + related_repos_relpath, related_peg_rev, + (old_rev < new_rev ? old_repos_relpath : new_repos_relpath), + (old_rev < new_rev ? old_rev : new_rev), + conflict, ctx, scratch_pool, scratch_pool)); + + SVN_ERR(find_revision_for_suspected_deletion( + &deleted_rev, &deleted_rev_author, &replacing_node_kind, &moves, + conflict, deleted_basename, parent_repos_relpath, + parent_peg_rev, 0, related_repos_relpath, related_peg_rev, + ctx, conflict->pool, scratch_pool)); + + /* If the victim was not deleted then check if the related path was moved. */ + if (deleted_rev == SVN_INVALID_REVNUM) + { + const char *victim_abspath; + svn_ra_session_t *ra_session; + const char *url, *corrected_url; + svn_client__pathrev_t *yca_loc; + svn_revnum_t end_rev; + svn_node_kind_t related_node_kind; + + /* ### The following describes all moves in terms of forward-merges, + * should do we something else for reverse-merges? */ + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + url = svn_path_url_add_component2(repos_root_url, related_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + /* Set END_REV to our best guess of the nearest YCA revision. */ + SVN_ERR(find_nearest_yca(&yca_loc, related_repos_relpath, related_peg_rev, + parent_repos_relpath, parent_peg_rev, + repos_root_url, repos_uuid, ra_session, ctx, + scratch_pool, scratch_pool)); + if (yca_loc == NULL) + return SVN_NO_ERROR; + end_rev = yca_loc->rev; + + /* END_REV must be smaller than RELATED_PEG_REV, else the call + to find_moves_in_natural_history() below will error out. */ + if (end_rev >= related_peg_rev) + end_rev = related_peg_rev > 0 ? related_peg_rev - 1 : 0; + + SVN_ERR(svn_ra_check_path(ra_session, "", related_peg_rev, + &related_node_kind, scratch_pool)); + SVN_ERR(find_moves_in_natural_history(&sibling_moves, + related_repos_relpath, + related_peg_rev, + related_node_kind, + end_rev, + victim_abspath, + repos_root_url, repos_uuid, + ra_session, ctx, + conflict->pool, scratch_pool)); + + if (sibling_moves == NULL) + return SVN_NO_ERROR; + + /* ## TODO: Find the missing node in the WC. */ + } + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + details->deleted_rev = deleted_rev; + details->deleted_rev_author = deleted_rev_author; + if (deleted_rev != SVN_INVALID_REVNUM) + details->deleted_repos_relpath = svn_relpath_join(parent_repos_relpath, + deleted_basename, + conflict->pool); + details->moves = moves; + details->sibling_moves = sibling_moves; + + conflict->tree_conflict_local_details = details; + + return SVN_NO_ERROR; +} + +/* Return a localised string representation of the local part of a tree + conflict on a non-existent node. */ +static svn_error_t * +describe_local_none_node_change(const char **description, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_reason_t local_change; + svn_wc_operation_t operation; + + local_change = svn_client_conflict_get_local_change(conflict); + operation = svn_client_conflict_get_operation(conflict); + + switch (local_change) + { + case svn_wc_conflict_reason_edited: + *description = _("An item containing uncommitted changes was " + "found in the working copy."); + break; + case svn_wc_conflict_reason_obstructed: + *description = _("An item which already occupies this path was found in " + "the working copy."); + break; + case svn_wc_conflict_reason_deleted: + *description = _("A deleted item was found in the working copy."); + break; + case svn_wc_conflict_reason_missing: + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + *description = _("No such file or directory was found in the " + "working copy."); + else if (operation == svn_wc_operation_merge) + { + /* ### display deleted revision */ + *description = _("No such file or directory was found in the " + "merge target working copy.\nThe item may " + "have been deleted or moved away in the " + "repository's history."); + } + break; + case svn_wc_conflict_reason_unversioned: + *description = _("An unversioned item was found in the working " + "copy."); + break; + case svn_wc_conflict_reason_added: + case svn_wc_conflict_reason_replaced: + *description = _("An item scheduled to be added to the repository " + "in the next commit was found in the working " + "copy."); + break; + case svn_wc_conflict_reason_moved_away: + *description = _("The item in the working copy had been moved " + "away at the time this conflict was recorded."); + break; + case svn_wc_conflict_reason_moved_here: + *description = _("An item had been moved here in the working copy " + "at the time this conflict was recorded."); + break; + } + + return SVN_NO_ERROR; +} + +/* Append a description of a move chain beginning at NEXT to DESCRIPTION. */ +static const char * +append_moved_to_chain_description(const char *description, + apr_array_header_t *next, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (next == NULL) + return description; + + while (next) + { + struct repos_move_info *move; + + /* Describe the first possible move chain only. Adding multiple chains + * to the description would just be confusing. The user may select a + * different move destination while resolving the conflict. */ + move = APR_ARRAY_IDX(next, 0, struct repos_move_info *); + + description = apr_psprintf(scratch_pool, + _("%s\nAnd then moved away to '^/%s' by " + "%s in r%ld."), + description, move->moved_to_repos_relpath, + move->rev_author, move->rev); + next = move->next; + } + + return apr_pstrdup(result_pool, description); +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_local_description_generic(const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_node_kind_t victim_node_kind; + + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + + *description = NULL; + + switch (victim_node_kind) + { + case svn_node_file: + case svn_node_symlink: + SVN_ERR(describe_local_file_node_change(description, conflict, ctx, + result_pool, scratch_pool)); + break; + case svn_node_dir: + SVN_ERR(describe_local_dir_node_change(description, conflict, ctx, + result_pool, scratch_pool)); + break; + case svn_node_none: + case svn_node_unknown: + SVN_ERR(describe_local_none_node_change(description, conflict, + result_pool, scratch_pool)); + break; + } + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_description_local_missing(const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct conflict_tree_local_missing_details *details; + + details = conflict->tree_conflict_local_details; + if (details == NULL) + return svn_error_trace(conflict_tree_get_local_description_generic( + description, conflict, ctx, + result_pool, scratch_pool)); + + if (details->moves || details->sibling_moves) + { + struct repos_move_info *move; + + *description = _("No such file or directory was found in the " + "merge target working copy.\n"); + + if (details->moves) + { + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + if (move->node_kind == svn_node_file) + *description = apr_psprintf( + result_pool, + _("%sThe file was moved to '^/%s' in r%ld by %s."), + *description, move->moved_to_repos_relpath, + move->rev, move->rev_author); + else if (move->node_kind == svn_node_dir) + *description = apr_psprintf( + result_pool, + _("%sThe directory was moved to '^/%s' in " + "r%ld by %s."), + *description, move->moved_to_repos_relpath, + move->rev, move->rev_author); + else + *description = apr_psprintf( + result_pool, + _("%sThe item was moved to '^/%s' in r%ld by %s."), + *description, move->moved_to_repos_relpath, + move->rev, move->rev_author); + *description = append_moved_to_chain_description(*description, + move->next, + result_pool, + scratch_pool); + } + + if (details->sibling_moves) + { + move = APR_ARRAY_IDX(details->sibling_moves, 0, + struct repos_move_info *); + if (move->node_kind == svn_node_file) + *description = apr_psprintf( + result_pool, + _("%sThe file '^/%s' was moved to '^/%s' " + "in r%ld by %s."), + *description, move->moved_from_repos_relpath, + move->moved_to_repos_relpath, + move->rev, move->rev_author); + else if (move->node_kind == svn_node_dir) + *description = apr_psprintf( + result_pool, + _("%sThe directory '^/%s' was moved to '^/%s' " + "in r%ld by %s."), + *description, move->moved_from_repos_relpath, + move->moved_to_repos_relpath, + move->rev, move->rev_author); + else + *description = apr_psprintf( + result_pool, + _("%sThe item '^/%s' was moved to '^/%s' " + "in r%ld by %s."), + *description, move->moved_from_repos_relpath, + move->moved_to_repos_relpath, + move->rev, move->rev_author); + *description = append_moved_to_chain_description(*description, + move->next, + result_pool, + scratch_pool); + } + } + else + *description = apr_psprintf( + result_pool, + _("No such file or directory was found in the " + "merge target working copy.\n'^/%s' was deleted " + "in r%ld by %s."), + details->deleted_repos_relpath, + details->deleted_rev, details->deleted_rev_author); + + return SVN_NO_ERROR; +} + +/* Return a localised string representation of the incoming part of a + conflict; NULL for non-localised odd cases. */ +static const char * +describe_incoming_change(svn_node_kind_t kind, svn_wc_conflict_action_t action, + svn_wc_operation_t operation) +{ + switch (kind) + { + case svn_node_file: + case svn_node_symlink: + if (operation == svn_wc_operation_update) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("An update operation tried to edit a file."); + case svn_wc_conflict_action_add: + return _("An update operation tried to add a file."); + case svn_wc_conflict_action_delete: + return _("An update operation tried to delete or move " + "a file."); + case svn_wc_conflict_action_replace: + return _("An update operation tried to replace a file."); + } + } + else if (operation == svn_wc_operation_switch) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A switch operation tried to edit a file."); + case svn_wc_conflict_action_add: + return _("A switch operation tried to add a file."); + case svn_wc_conflict_action_delete: + return _("A switch operation tried to delete or move " + "a file."); + case svn_wc_conflict_action_replace: + return _("A switch operation tried to replace a file."); + } + } + else if (operation == svn_wc_operation_merge) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A merge operation tried to edit a file."); + case svn_wc_conflict_action_add: + return _("A merge operation tried to add a file."); + case svn_wc_conflict_action_delete: + return _("A merge operation tried to delete or move " + "a file."); + case svn_wc_conflict_action_replace: + return _("A merge operation tried to replace a file."); + } + } + break; + case svn_node_dir: + if (operation == svn_wc_operation_update) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("An update operation tried to change a directory."); + case svn_wc_conflict_action_add: + return _("An update operation tried to add a directory."); + case svn_wc_conflict_action_delete: + return _("An update operation tried to delete or move " + "a directory."); + case svn_wc_conflict_action_replace: + return _("An update operation tried to replace a directory."); + } + } + else if (operation == svn_wc_operation_switch) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A switch operation tried to edit a directory."); + case svn_wc_conflict_action_add: + return _("A switch operation tried to add a directory."); + case svn_wc_conflict_action_delete: + return _("A switch operation tried to delete or move " + "a directory."); + case svn_wc_conflict_action_replace: + return _("A switch operation tried to replace a directory."); + } + } + else if (operation == svn_wc_operation_merge) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A merge operation tried to edit a directory."); + case svn_wc_conflict_action_add: + return _("A merge operation tried to add a directory."); + case svn_wc_conflict_action_delete: + return _("A merge operation tried to delete or move " + "a directory."); + case svn_wc_conflict_action_replace: + return _("A merge operation tried to replace a directory."); + } + } + break; + case svn_node_none: + case svn_node_unknown: + if (operation == svn_wc_operation_update) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("An update operation tried to edit an item."); + case svn_wc_conflict_action_add: + return _("An update operation tried to add an item."); + case svn_wc_conflict_action_delete: + return _("An update operation tried to delete or move " + "an item."); + case svn_wc_conflict_action_replace: + return _("An update operation tried to replace an item."); + } + } + else if (operation == svn_wc_operation_switch) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A switch operation tried to edit an item."); + case svn_wc_conflict_action_add: + return _("A switch operation tried to add an item."); + case svn_wc_conflict_action_delete: + return _("A switch operation tried to delete or move " + "an item."); + case svn_wc_conflict_action_replace: + return _("A switch operation tried to replace an item."); + } + } + else if (operation == svn_wc_operation_merge) + { + switch (action) + { + case svn_wc_conflict_action_edit: + return _("A merge operation tried to edit an item."); + case svn_wc_conflict_action_add: + return _("A merge operation tried to add an item."); + case svn_wc_conflict_action_delete: + return _("A merge operation tried to delete or move " + "an item."); + case svn_wc_conflict_action_replace: + return _("A merge operation tried to replace an item."); + } + } + break; + } + + return NULL; +} + +/* Return a localised string representation of the operation part of a + conflict. */ +static const char * +operation_str(svn_wc_operation_t operation) +{ + switch (operation) + { + case svn_wc_operation_update: return _("upon update"); + case svn_wc_operation_switch: return _("upon switch"); + case svn_wc_operation_merge: return _("upon merge"); + case svn_wc_operation_none: return _("upon none"); + } + SVN_ERR_MALFUNCTION_NO_RETURN(); + return NULL; +} + +svn_error_t * +svn_client_conflict_prop_get_description(const char **description, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *reason_str, *action_str; + + /* We provide separately translatable strings for the values that we + * know about, and a fall-back in case any other values occur. */ + switch (svn_client_conflict_get_local_change(conflict)) + { + case svn_wc_conflict_reason_edited: + reason_str = _("local edit"); + break; + case svn_wc_conflict_reason_added: + reason_str = _("local add"); + break; + case svn_wc_conflict_reason_deleted: + reason_str = _("local delete"); + break; + case svn_wc_conflict_reason_obstructed: + reason_str = _("local obstruction"); + break; + default: + reason_str = apr_psprintf( + scratch_pool, _("local %s"), + svn_token__to_word( + map_conflict_reason, + svn_client_conflict_get_local_change(conflict))); + break; + } + switch (svn_client_conflict_get_incoming_change(conflict)) + { + case svn_wc_conflict_action_edit: + action_str = _("incoming edit"); + break; + case svn_wc_conflict_action_add: + action_str = _("incoming add"); + break; + case svn_wc_conflict_action_delete: + action_str = _("incoming delete"); + break; + default: + action_str = apr_psprintf( + scratch_pool, _("incoming %s"), + svn_token__to_word( + map_conflict_action, + svn_client_conflict_get_incoming_change(conflict))); + break; + } + SVN_ERR_ASSERT(reason_str && action_str); + + *description = apr_psprintf(result_pool, _("%s, %s %s"), + reason_str, action_str, + operation_str( + svn_client_conflict_get_operation(conflict))); + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_incoming_description_generic( + const char **incoming_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *action; + svn_node_kind_t incoming_kind; + svn_wc_conflict_action_t conflict_action; + svn_wc_operation_t conflict_operation; + + conflict_action = svn_client_conflict_get_incoming_change(conflict); + conflict_operation = svn_client_conflict_get_operation(conflict); + + /* Determine the node kind of the incoming change. */ + incoming_kind = svn_node_unknown; + if (conflict_action == svn_wc_conflict_action_edit || + conflict_action == svn_wc_conflict_action_delete) + { + /* Change is acting on 'src_left' version of the node. */ + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + NULL, NULL, &incoming_kind, conflict, scratch_pool, + scratch_pool)); + } + else if (conflict_action == svn_wc_conflict_action_add || + conflict_action == svn_wc_conflict_action_replace) + { + /* Change is acting on 'src_right' version of the node. + * + * ### For 'replace', the node kind is ambiguous. However, src_left + * ### is NULL for replace, so we must use src_right. */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + NULL, NULL, &incoming_kind, conflict, scratch_pool, + scratch_pool)); + } + + action = describe_incoming_change(incoming_kind, conflict_action, + conflict_operation); + if (action) + { + *incoming_change_description = apr_pstrdup(result_pool, action); + } + else + { + /* A catch-all message for very rare or nominally impossible cases. + It will not be pretty, but is closer to an internal error than + an ordinary user-facing string. */ + *incoming_change_description = apr_psprintf(result_pool, + _("incoming %s %s"), + svn_node_kind_to_word(incoming_kind), + svn_token__to_word(map_conflict_action, + conflict_action)); + } + return SVN_NO_ERROR; +} + +/* Details for tree conflicts involving incoming deletions and replacements. */ +struct conflict_tree_incoming_delete_details +{ + /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. */ + svn_revnum_t deleted_rev; + + /* If not SVN_INVALID_REVNUM, the node was added in ADDED_REV. The incoming + * delete is the result of a reverse application of this addition. */ + svn_revnum_t added_rev; + + /* The path which was deleted/added relative to the repository root. */ + const char *repos_relpath; + + /* Author who committed DELETED_REV/ADDED_REV. */ + const char *rev_author; + + /* New node kind for a replaced node. This is svn_node_none for deletions. */ + svn_node_kind_t replacing_node_kind; + + /* Move information. If not NULL, this is an array of repos_move_info * + * elements. Each element is the head of a move chain which starts in + * DELETED_REV or in ADDED_REV (in which case moves should be interpreted + * in reverse). */ + apr_array_header_t *moves; + + /* A map of repos_relpaths and working copy nodes for an incoming move. + * + * Each key is a "const char *" repository relpath corresponding to a + * possible repository-side move destination node in the revision which + * is the target revision in case of update and switch, or the merge-right + * revision in case of a merge. + * + * Each value is an apr_array_header_t *. + * Each array consists of "const char *" absolute paths to working copy + * nodes which correspond to the repository node selected by the map key. + * Each such working copy node is a potential local move target which can + * be chosen to "follow" the incoming move when resolving a tree conflict. + * + * This may be an empty hash map in case if there is no move target path + * in the working copy. */ + apr_hash_t *wc_move_targets; + + /* The preferred move target repository relpath. This is our key into + * the WC_MOVE_TARGETS map above (can be overridden by the user). */ + const char *move_target_repos_relpath; + + /* The current index into the list of working copy nodes corresponding to + * MOVE_TARGET_REPOS_REPLATH (can be overridden by the user). */ + int wc_move_target_idx; +}; + +/* Get the currently selected repository-side move target path. + * If none was selected yet, determine and return a default one. */ +static const char * +get_moved_to_repos_relpath( + struct conflict_tree_incoming_delete_details *details, + apr_pool_t *scratch_pool) +{ + struct repos_move_info *move; + + if (details->move_target_repos_relpath) + return details->move_target_repos_relpath; + + if (details->wc_move_targets && apr_hash_count(details->wc_move_targets) > 0) + { + svn_sort__item_t item; + apr_array_header_t *repos_relpaths; + + repos_relpaths = svn_sort__hash(details->wc_move_targets, + svn_sort_compare_items_as_paths, + scratch_pool); + item = APR_ARRAY_IDX(repos_relpaths, 0, svn_sort__item_t); + return (const char *)item.key; + } + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + return move->moved_to_repos_relpath; +} + +static const char * +describe_incoming_deletion_upon_update( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + svn_revnum_t old_rev, + svn_revnum_t new_rev, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory updated from r%ld to r%ld was " + "replaced with a file by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File updated from r%ld to r%ld was replaced " + "with a file from another line of history by " + "%s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item updated from r%ld to r%ld was replaced " + "with a file by %s in r%ld."), old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory updated from r%ld to r%ld was " + "replaced with a directory from another line " + "of history by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File updated from r%ld to r%ld was " + "replaced with a directory by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item updated from r%ld to r%ld was replaced " + "by %s in r%ld."), old_rev, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else + { + if (victim_node_kind == svn_node_dir) + { + if (details->moves) + { + const char *description; + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Directory updated from r%ld to r%ld was " + "moved to '^/%s' by %s in r%ld."), + old_rev, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Directory updated from r%ld to r%ld was " + "deleted by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->deleted_rev); + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("File updated from r%ld to r%ld was moved " + "to '^/%s' by %s in r%ld."), old_rev, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("File updated from r%ld to r%ld was " + "deleted by %s in r%ld."), old_rev, new_rev, + details->rev_author, details->deleted_rev); + } + else + { + if (details->moves) + { + const char *description; + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Item updated from r%ld to r%ld was moved " + "to '^/%s' by %s in r%ld."), old_rev, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Item updated from r%ld to r%ld was " + "deleted by %s in r%ld."), old_rev, new_rev, + details->rev_author, details->deleted_rev); + } + } +} + +static const char * +describe_incoming_reverse_addition_upon_update( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + svn_revnum_t old_rev, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory updated backwards from r%ld to r%ld " + "was a file before the replacement made by %s " + "in r%ld."), old_rev, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File updated backwards from r%ld to r%ld was a " + "file from another line of history before the " + "replacement made by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item updated backwards from r%ld to r%ld was " + "replaced with a file by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory updated backwards from r%ld to r%ld " + "was a directory from another line of history " + "before the replacement made by %s in " + "r%ld."), old_rev, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File updated backwards from r%ld to r%ld was a " + "directory before the replacement made by %s " + "in r%ld."), old_rev, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item updated backwards from r%ld to r%ld was " + "replaced with a directory by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + } + else + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory updated backwards from r%ld to r%ld " + "did not exist before it was added by %s in " + "r%ld."), old_rev, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File updated backwards from r%ld to r%ld did " + "not exist before it was added by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item updated backwards from r%ld to r%ld did " + "not exist before it was added by %s in r%ld."), + old_rev, new_rev, + details->rev_author, details->added_rev); + } +} + +static const char * +describe_incoming_deletion_upon_switch( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved " + "to '^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file from another line of " + "history by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory from another " + "line of history by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else + { + if (victim_node_kind == svn_node_dir) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + } +} + +static const char * +describe_incoming_reverse_addition_upon_switch( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was a file before the replacement made by %s " + "in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas a " + "file from another line of history before the " + "replacement made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was a directory from another line of history " + "before the replacement made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was a file before the replacement made by %s " + "in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } + else + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "did not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\ndid " + "not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item switched from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\ndid " + "not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } +} + +static const char * +describe_incoming_deletion_upon_merge( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file from another line of " + "history by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + return apr_psprintf(result_pool, + _("Item merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + { + const char *description = + apr_psprintf(result_pool, + _("Directory merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory from another " + "line of history by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced directory was moved to " + "'^/%s'."), description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + const char *description = + apr_psprintf(result_pool, + _("File merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced file was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + else + { + const char *description = + apr_psprintf(result_pool, + _("Item merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + if (details->moves) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("%s\nThe replaced item was moved to '^/%s'."), + description, + get_moved_to_repos_relpath(details, scratch_pool)); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + return description; + } + } + else + { + if (victim_node_kind == svn_node_dir) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Directory merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Directory merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("File merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("File merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + else + { + if (details->moves) + { + struct repos_move_info *move; + const char *description; + + move = APR_ARRAY_IDX(details->moves, 0, struct repos_move_info *); + description = + apr_psprintf(result_pool, + _("Item merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "moved to '^/%s' by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + get_moved_to_repos_relpath(details, scratch_pool), + details->rev_author, details->deleted_rev); + return append_moved_to_chain_description(description, + move->next, + result_pool, + scratch_pool); + } + else + return apr_psprintf(result_pool, + _("Item merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\nwas " + "deleted by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->deleted_rev); + } + } +} + +static const char * +describe_incoming_reverse_addition_upon_merge( + struct conflict_tree_incoming_delete_details *details, + svn_node_kind_t victim_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (details->replacing_node_kind == svn_node_file || + details->replacing_node_kind == svn_node_symlink) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory reverse-merged from\n'^/%s@%ld'\nto " + "^/%s@%ld was a file before the replacement " + "made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was a file from another line of history before " + "the replacement made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a file by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } + else if (details->replacing_node_kind == svn_node_dir) + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory reverse-merged from\n'^/%s@%ld'\nto " + "^/%s@%ld was a directory from another line " + "of history before the replacement made by %s " + "in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Directory reverse-merged from\n'^/%s@%ld'\nto " + "^/%s@%ld was a file before the replacement " + "made by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "was replaced with a directory by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } + else + { + if (victim_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Directory reverse-merged from\n'^/%s@%ld'\nto " + "^/%s@%ld did not exist before it was added " + "by %s in r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("File reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "did not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("Item reverse-merged from\n" + "'^/%s@%ld'\nto\n'^/%s@%ld'\n" + "did not exist before it was added by %s in " + "r%ld."), + old_repos_relpath, old_rev, + new_repos_relpath, new_rev, + details->rev_author, details->added_rev); + } +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_description_incoming_delete( + const char **incoming_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *action; + svn_node_kind_t victim_node_kind; + svn_wc_operation_t conflict_operation; + const char *old_repos_relpath; + svn_revnum_t old_rev; + const char *new_repos_relpath; + svn_revnum_t new_rev; + struct conflict_tree_incoming_delete_details *details; + + if (conflict->tree_conflict_incoming_details == NULL) + return svn_error_trace(conflict_tree_get_incoming_description_generic( + incoming_change_description, + conflict, ctx, result_pool, scratch_pool)); + + conflict_operation = svn_client_conflict_get_operation(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, NULL, conflict, scratch_pool, + scratch_pool)); + + details = conflict->tree_conflict_incoming_details; + + if (conflict_operation == svn_wc_operation_update) + { + if (details->deleted_rev != SVN_INVALID_REVNUM) + { + action = describe_incoming_deletion_upon_update(details, + victim_node_kind, + old_rev, + new_rev, + result_pool, + scratch_pool); + } + else /* details->added_rev != SVN_INVALID_REVNUM */ + { + /* This deletion is really the reverse change of an addition. */ + action = describe_incoming_reverse_addition_upon_update( + details, victim_node_kind, old_rev, new_rev, result_pool); + } + } + else if (conflict_operation == svn_wc_operation_switch) + { + if (details->deleted_rev != SVN_INVALID_REVNUM) + { + action = describe_incoming_deletion_upon_switch(details, + victim_node_kind, + old_repos_relpath, + old_rev, + new_repos_relpath, + new_rev, + result_pool, + scratch_pool); + } + else /* details->added_rev != SVN_INVALID_REVNUM */ + { + /* This deletion is really the reverse change of an addition. */ + action = describe_incoming_reverse_addition_upon_switch( + details, victim_node_kind, old_repos_relpath, old_rev, + new_repos_relpath, new_rev, result_pool); + + } + } + else if (conflict_operation == svn_wc_operation_merge) + { + if (details->deleted_rev != SVN_INVALID_REVNUM) + { + action = describe_incoming_deletion_upon_merge(details, + victim_node_kind, + old_repos_relpath, + old_rev, + new_repos_relpath, + new_rev, + result_pool, + scratch_pool); + } + else /* details->added_rev != SVN_INVALID_REVNUM */ + { + /* This deletion is really the reverse change of an addition. */ + action = describe_incoming_reverse_addition_upon_merge( + details, victim_node_kind, old_repos_relpath, old_rev, + new_repos_relpath, new_rev, result_pool); + } + } + + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; +} + +/* Baton for find_added_rev(). */ +struct find_added_rev_baton +{ + const char *victim_abspath; + svn_client_ctx_t *ctx; + svn_revnum_t added_rev; + const char *repos_relpath; + const char *parent_repos_relpath; + apr_pool_t *pool; +}; + +/* Implements svn_location_segment_receiver_t. + * Finds the revision in which a node was added by tracing 'start' + * revisions in location segments reported for the node. + * If the PARENT_REPOS_RELPATH in the baton is not NULL, only consider + * segments in which the node existed somwhere beneath this path. */ +static svn_error_t * +find_added_rev(svn_location_segment_t *segment, + void *baton, + apr_pool_t *scratch_pool) +{ + struct find_added_rev_baton *b = baton; + + if (b->ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + b->victim_abspath, + svn_wc_notify_tree_conflict_details_progress, + scratch_pool), + notify->revision = segment->range_start; + b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); + } + + if (segment->path) /* not interested in gaps */ + { + if (b->parent_repos_relpath == NULL || + svn_relpath_skip_ancestor(b->parent_repos_relpath, + segment->path) != NULL) + { + b->added_rev = segment->range_start; + b->repos_relpath = apr_pstrdup(b->pool, segment->path); + } + } + + return SVN_NO_ERROR; +} + +/* Find conflict details in the case where a revision which added a node was + * applied in reverse, resulting in an incoming deletion. */ +static svn_error_t * +get_incoming_delete_details_for_reverse_addition( + struct conflict_tree_incoming_delete_details **details, + const char *repos_root_url, + const char *old_repos_relpath, + svn_revnum_t old_rev, + svn_revnum_t new_rev, + svn_client_ctx_t *ctx, + const char *victim_abspath, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + svn_string_t *author_revprop; + struct find_added_rev_baton b = { 0 }; + + url = svn_path_url_add_component2(repos_root_url, old_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + *details = apr_pcalloc(result_pool, sizeof(**details)); + b.ctx = ctx; + b.victim_abspath = victim_abspath; + b.added_rev = SVN_INVALID_REVNUM; + b.repos_relpath = NULL; + b.parent_repos_relpath = NULL; + b.pool = scratch_pool; + + /* Figure out when this node was added. */ + SVN_ERR(svn_ra_get_location_segments(ra_session, "", old_rev, + old_rev, new_rev, + find_added_rev, &b, + scratch_pool)); + + SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev, + SVN_PROP_REVISION_AUTHOR, + &author_revprop, scratch_pool)); + (*details)->deleted_rev = SVN_INVALID_REVNUM; + (*details)->added_rev = b.added_rev; + (*details)->repos_relpath = apr_pstrdup(result_pool, b.repos_relpath); + if (author_revprop) + (*details)->rev_author = apr_pstrdup(result_pool, author_revprop->data); + else + (*details)->rev_author = _("unknown author"); + + /* Check for replacement. */ + (*details)->replacing_node_kind = svn_node_none; + if ((*details)->added_rev > 0) + { + svn_node_kind_t replaced_node_kind; + + SVN_ERR(svn_ra_check_path(ra_session, "", + rev_below((*details)->added_rev), + &replaced_node_kind, scratch_pool)); + if (replaced_node_kind != svn_node_none) + SVN_ERR(svn_ra_check_path(ra_session, "", (*details)->added_rev, + &(*details)->replacing_node_kind, + scratch_pool)); + } + + return SVN_NO_ERROR; +} + +/* Follow each move chain starting a MOVE all the way to the end to find + * the possible working copy locations for VICTIM_ABSPATH which corresponds + * to VICTIM_REPOS_REPLATH@VICTIM_REVISION. + * Add each such location to the WC_MOVE_TARGETS hash table, keyed on the + * repos_relpath which is the corresponding move destination in the repository. + * This function is recursive. */ +static svn_error_t * +follow_move_chains(apr_hash_t *wc_move_targets, + struct repos_move_info *move, + svn_client_ctx_t *ctx, + const char *victim_abspath, + svn_node_kind_t victim_node_kind, + const char *victim_repos_relpath, + svn_revnum_t victim_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + /* If this is the end of a move chain, look for matching paths in + * the working copy and add them to our collection if found. */ + if (move->next == NULL) + { + apr_array_header_t *candidate_abspaths; + + /* Gather candidate nodes which represent this moved_to_repos_relpath. */ + SVN_ERR(svn_wc__guess_incoming_move_target_nodes( + &candidate_abspaths, ctx->wc_ctx, + victim_abspath, victim_node_kind, + move->moved_to_repos_relpath, + scratch_pool, scratch_pool)); + if (candidate_abspaths->nelts > 0) + { + apr_array_header_t *moved_to_abspaths; + int i; + apr_pool_t *iterpool = svn_pool_create(scratch_pool); + + moved_to_abspaths = apr_array_make(result_pool, 1, + sizeof (const char *)); + + for (i = 0; i < candidate_abspaths->nelts; i++) + { + const char *candidate_abspath; + const char *repos_root_url; + const char *repos_uuid; + const char *candidate_repos_relpath; + svn_revnum_t candidate_revision; + + svn_pool_clear(iterpool); + + candidate_abspath = APR_ARRAY_IDX(candidate_abspaths, i, + const char *); + SVN_ERR(svn_wc__node_get_origin(NULL, &candidate_revision, + &candidate_repos_relpath, + &repos_root_url, + &repos_uuid, + NULL, NULL, + ctx->wc_ctx, + candidate_abspath, + FALSE, + iterpool, iterpool)); + + if (candidate_revision == SVN_INVALID_REVNUM) + continue; + + /* If the conflict victim and the move target candidate + * are not from the same revision we must ensure that + * they are related. */ + if (candidate_revision != victim_revision) + { + svn_client__pathrev_t *yca_loc; + svn_error_t *err; + + err = find_yca(&yca_loc, victim_repos_relpath, + victim_revision, + candidate_repos_relpath, + candidate_revision, + repos_root_url, repos_uuid, + NULL, ctx, iterpool, iterpool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + yca_loc = NULL; + } + else + return svn_error_trace(err); + } + + if (yca_loc == NULL) + continue; + } + + APR_ARRAY_PUSH(moved_to_abspaths, const char *) = + apr_pstrdup(result_pool, candidate_abspath); + } + svn_pool_destroy(iterpool); + + svn_hash_sets(wc_move_targets, move->moved_to_repos_relpath, + moved_to_abspaths); + } + } + else + { + int i; + apr_pool_t *iterpool; + + /* Recurse into each of the possible move chains. */ + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < move->next->nelts; i++) + { + struct repos_move_info *next_move; + + svn_pool_clear(iterpool); + + next_move = APR_ARRAY_IDX(move->next, i, struct repos_move_info *); + SVN_ERR(follow_move_chains(wc_move_targets, next_move, + ctx, victim_abspath, victim_node_kind, + victim_repos_relpath, victim_revision, + result_pool, iterpool)); + + } + svn_pool_destroy(iterpool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +init_wc_move_targets(struct conflict_tree_incoming_delete_details *details, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + int i; + const char *victim_abspath; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_wc_operation_t operation; + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + operation = svn_client_conflict_get_operation(conflict); + /* ### Should we get the old location in case of reverse-merges? */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, + scratch_pool, scratch_pool)); + details->wc_move_targets = apr_hash_make(conflict->pool); + for (i = 0; i < details->moves->nelts; i++) + { + struct repos_move_info *move; + + move = APR_ARRAY_IDX(details->moves, i, struct repos_move_info *); + SVN_ERR(follow_move_chains(details->wc_move_targets, move, + ctx, victim_abspath, + victim_node_kind, + incoming_new_repos_relpath, + incoming_new_pegrev, + conflict->pool, scratch_pool)); + } + + /* Initialize to the first possible move target. Hopefully, + * in most cases there will only be one candidate anyway. */ + details->move_target_repos_relpath = + get_moved_to_repos_relpath(details, scratch_pool); + details->wc_move_target_idx = 0; + + /* If only one move target exists after an update or switch, + * recommend a resolution option which follows the incoming move. */ + if (apr_hash_count(details->wc_move_targets) == 1 && + (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch)) + { + apr_array_header_t *wc_abspaths; + + wc_abspaths = svn_hash_gets(details->wc_move_targets, + details->move_target_repos_relpath); + if (wc_abspaths->nelts == 1) + { + svn_client_conflict_option_id_t recommended[] = + { + /* Only one of these will be present for any given conflict. */ + svn_client_conflict_option_incoming_move_file_text_merge, + svn_client_conflict_option_incoming_move_dir_merge, + svn_client_conflict_option_local_move_file_text_merge + }; + apr_array_header_t *options; + + SVN_ERR(svn_client_conflict_tree_get_resolution_options( + &options, conflict, ctx, scratch_pool, scratch_pool)); + for (i = 0; i < (sizeof(recommended) / sizeof(recommended[0])); i++) + { + svn_client_conflict_option_id_t option_id = recommended[i]; + + if (svn_client_conflict_option_find_by_id(options, option_id)) + { + conflict->recommended_option_id = option_id; + break; + } + } + } + } + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_details_func_t. + * Find the revision in which the victim was deleted in the repository. */ +static svn_error_t * +conflict_tree_get_details_incoming_delete(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *old_repos_relpath; + const char *new_repos_relpath; + const char *repos_root_url; + svn_revnum_t old_rev; + svn_revnum_t new_rev; + svn_node_kind_t old_kind; + svn_node_kind_t new_kind; + struct conflict_tree_incoming_delete_details *details; + svn_wc_operation_t operation; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, &old_kind, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, &new_kind, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, + scratch_pool, scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + + if (operation == svn_wc_operation_update) + { + if (old_rev < new_rev) + { + const char *parent_repos_relpath; + svn_revnum_t parent_peg_rev; + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + apr_array_header_t *moves; + const char *related_repos_relpath; + svn_revnum_t related_peg_rev; + + /* The update operation went forward in history. */ + SVN_ERR(svn_wc__node_get_repos_info(&parent_peg_rev, + &parent_repos_relpath, + NULL, NULL, + ctx->wc_ctx, + svn_dirent_dirname( + conflict->local_abspath, + scratch_pool), + scratch_pool, + scratch_pool)); + if (new_kind == svn_node_none) + { + SVN_ERR(find_related_node(&related_repos_relpath, + &related_peg_rev, + new_repos_relpath, new_rev, + old_repos_relpath, old_rev, + conflict, ctx, + scratch_pool, scratch_pool)); + } + else + { + /* related to self */ + related_repos_relpath = NULL; + related_peg_rev = SVN_INVALID_REVNUM; + } + + SVN_ERR(find_revision_for_suspected_deletion( + &deleted_rev, &deleted_rev_author, &replacing_node_kind, + &moves, conflict, + svn_dirent_basename(conflict->local_abspath, scratch_pool), + parent_repos_relpath, parent_peg_rev, + new_kind == svn_node_none ? 0 : old_rev, + related_repos_relpath, related_peg_rev, + ctx, conflict->pool, scratch_pool)); + if (deleted_rev == SVN_INVALID_REVNUM) + { + /* We could not determine the revision in which the node was + * deleted. We cannot provide the required details so the best + * we can do is fall back to the default description. */ + return SVN_NO_ERROR; + } + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + details->deleted_rev = deleted_rev; + details->added_rev = SVN_INVALID_REVNUM; + details->repos_relpath = apr_pstrdup(conflict->pool, + new_repos_relpath); + details->rev_author = deleted_rev_author; + details->replacing_node_kind = replacing_node_kind; + details->moves = moves; + } + else /* new_rev < old_rev */ + { + /* The update operation went backwards in history. + * Figure out when this node was added. */ + SVN_ERR(get_incoming_delete_details_for_reverse_addition( + &details, repos_root_url, old_repos_relpath, + old_rev, new_rev, ctx, + svn_client_conflict_get_local_abspath(conflict), + conflict->pool, scratch_pool)); + } + } + else if (operation == svn_wc_operation_switch || + operation == svn_wc_operation_merge) + { + if (old_rev < new_rev) + { + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + apr_array_header_t *moves; + + /* The switch/merge operation went forward in history. + * + * The deletion of the node happened on the branch we switched to + * or merged from. Scan new_repos_relpath's parent's log to find + * the revision which deleted the node. */ + SVN_ERR(find_revision_for_suspected_deletion( + &deleted_rev, &deleted_rev_author, &replacing_node_kind, + &moves, conflict, + svn_relpath_basename(new_repos_relpath, scratch_pool), + svn_relpath_dirname(new_repos_relpath, scratch_pool), + new_rev, old_rev, old_repos_relpath, old_rev, ctx, + conflict->pool, scratch_pool)); + if (deleted_rev == SVN_INVALID_REVNUM) + { + /* We could not determine the revision in which the node was + * deleted. We cannot provide the required details so the best + * we can do is fall back to the default description. */ + return SVN_NO_ERROR; + } + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + details->deleted_rev = deleted_rev; + details->added_rev = SVN_INVALID_REVNUM; + details->repos_relpath = apr_pstrdup(conflict->pool, + new_repos_relpath); + details->rev_author = apr_pstrdup(conflict->pool, + deleted_rev_author); + details->replacing_node_kind = replacing_node_kind; + details->moves = moves; + } + else /* new_rev < old_rev */ + { + /* The switch/merge operation went backwards in history. + * Figure out when the node we switched away from, or merged + * from another branch, was added. */ + SVN_ERR(get_incoming_delete_details_for_reverse_addition( + &details, repos_root_url, old_repos_relpath, + old_rev, new_rev, ctx, + svn_client_conflict_get_local_abspath(conflict), + conflict->pool, scratch_pool)); + } + } + else + { + details = NULL; + } + + conflict->tree_conflict_incoming_details = details; + + if (details && details->moves) + SVN_ERR(init_wc_move_targets(details, conflict, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Details for tree conflicts involving incoming additions. */ +struct conflict_tree_incoming_add_details +{ + /* If not SVN_INVALID_REVNUM, the node was added in ADDED_REV. */ + svn_revnum_t added_rev; + + /* If not SVN_INVALID_REVNUM, the node was deleted in DELETED_REV. + * Note that both ADDED_REV and DELETED_REV may be valid for update/switch. + * See comment in conflict_tree_get_details_incoming_add() for details. */ + svn_revnum_t deleted_rev; + + /* The path which was added/deleted relative to the repository root. */ + const char *repos_relpath; + + /* Authors who committed ADDED_REV/DELETED_REV. */ + const char *added_rev_author; + const char *deleted_rev_author; + + /* Move information. If not NULL, this is an array of repos_move_info * + * elements. Each element is the head of a move chain which starts in + * ADDED_REV or in DELETED_REV (in which case moves should be interpreted + * in reverse). */ + apr_array_header_t *moves; +}; + +/* Implements tree_conflict_get_details_func_t. + * Find the revision in which the victim was added in the repository. */ +static svn_error_t * +conflict_tree_get_details_incoming_add(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *old_repos_relpath; + const char *new_repos_relpath; + const char *repos_root_url; + svn_revnum_t old_rev; + svn_revnum_t new_rev; + struct conflict_tree_incoming_add_details *details; + svn_wc_operation_t operation; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, + scratch_pool, scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + /* Only the new repository location is recorded for the node which + * caused an incoming addition. There is no pre-update/pre-switch + * revision to be recorded for the node since it does not exist in + * the repository at that revision. + * The implication is that we cannot know whether the operation went + * forward or backwards in history. So always try to find an added + * and a deleted revision for the node. Users must figure out by whether + * the addition or deletion caused the conflict. */ + const char *url; + const char *corrected_url; + svn_string_t *author_revprop; + struct find_added_rev_baton b = { 0 }; + svn_ra_session_t *ra_session; + svn_revnum_t deleted_rev; + svn_revnum_t head_rev; + + url = svn_path_url_add_component2(repos_root_url, new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + b.ctx = ctx, + b.victim_abspath = svn_client_conflict_get_local_abspath(conflict), + b.added_rev = SVN_INVALID_REVNUM; + b.repos_relpath = NULL; + b.parent_repos_relpath = NULL; + b.pool = scratch_pool; + + /* Figure out when this node was added. */ + SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev, + new_rev, SVN_INVALID_REVNUM, + find_added_rev, &b, + scratch_pool)); + + SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev, + SVN_PROP_REVISION_AUTHOR, + &author_revprop, scratch_pool)); + details->repos_relpath = apr_pstrdup(conflict->pool, b.repos_relpath); + details->added_rev = b.added_rev; + if (author_revprop) + details->added_rev_author = apr_pstrdup(conflict->pool, + author_revprop->data); + else + details->added_rev_author = _("unknown author"); + details->deleted_rev = SVN_INVALID_REVNUM; + details->deleted_rev_author = NULL; + + /* Figure out whether this node was deleted later. + * ### Could probably optimize by infering both addition and deletion + * ### from svn_ra_get_location_segments() call above. */ + SVN_ERR(svn_ra_get_latest_revnum(ra_session, &head_rev, scratch_pool)); + if (new_rev < head_rev) + { + SVN_ERR(svn_ra_get_deleted_rev(ra_session, "", new_rev, head_rev, + &deleted_rev, scratch_pool)); + if (SVN_IS_VALID_REVNUM(deleted_rev)) + { + SVN_ERR(svn_ra_rev_prop(ra_session, deleted_rev, + SVN_PROP_REVISION_AUTHOR, + &author_revprop, scratch_pool)); + details->deleted_rev = deleted_rev; + if (author_revprop) + details->deleted_rev_author = apr_pstrdup(conflict->pool, + author_revprop->data); + else + details->deleted_rev_author = _("unknown author"); + } + } + } + else if (operation == svn_wc_operation_merge) + { + if (old_rev < new_rev) + { + /* The merge operation went forwards in history. + * The addition of the node happened on the branch we merged form. + * Scan the nodes's history to find the revision which added it. */ + const char *url; + const char *corrected_url; + svn_string_t *author_revprop; + struct find_added_rev_baton b = { 0 }; + svn_ra_session_t *ra_session; + + url = svn_path_url_add_component2(repos_root_url, new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + b.victim_abspath = svn_client_conflict_get_local_abspath(conflict); + b.ctx = ctx; + b.added_rev = SVN_INVALID_REVNUM; + b.repos_relpath = NULL; + b.parent_repos_relpath = NULL; + b.pool = scratch_pool; + + /* Figure out when this node was added. */ + SVN_ERR(svn_ra_get_location_segments(ra_session, "", new_rev, + new_rev, old_rev, + find_added_rev, &b, + scratch_pool)); + + SVN_ERR(svn_ra_rev_prop(ra_session, b.added_rev, + SVN_PROP_REVISION_AUTHOR, + &author_revprop, scratch_pool)); + details->repos_relpath = apr_pstrdup(conflict->pool, b.repos_relpath); + details->added_rev = b.added_rev; + if (author_revprop) + details->added_rev_author = apr_pstrdup(conflict->pool, + author_revprop->data); + else + details->added_rev_author = _("unknown author"); + details->deleted_rev = SVN_INVALID_REVNUM; + details->deleted_rev_author = NULL; + } + else + { + /* The merge operation was a reverse-merge. + * This addition is in fact a deletion, applied in reverse, + * which happened on the branch we merged from. + * Find the revision which deleted the node. */ + svn_revnum_t deleted_rev; + const char *deleted_rev_author; + svn_node_kind_t replacing_node_kind; + apr_array_header_t *moves; + + SVN_ERR(find_revision_for_suspected_deletion( + &deleted_rev, &deleted_rev_author, &replacing_node_kind, + &moves, conflict, + svn_relpath_basename(old_repos_relpath, scratch_pool), + svn_relpath_dirname(old_repos_relpath, scratch_pool), + old_rev, new_rev, + NULL, SVN_INVALID_REVNUM, /* related to self */ + ctx, + conflict->pool, scratch_pool)); + if (deleted_rev == SVN_INVALID_REVNUM) + { + /* We could not determine the revision in which the node was + * deleted. We cannot provide the required details so the best + * we can do is fall back to the default description. */ + return SVN_NO_ERROR; + } + + details = apr_pcalloc(conflict->pool, sizeof(*details)); + details->repos_relpath = apr_pstrdup(conflict->pool, + new_repos_relpath); + details->deleted_rev = deleted_rev; + details->deleted_rev_author = apr_pstrdup(conflict->pool, + deleted_rev_author); + + details->added_rev = SVN_INVALID_REVNUM; + details->added_rev_author = NULL; + details->moves = moves; + } + } + else + { + details = NULL; + } + + conflict->tree_conflict_incoming_details = details; + + return SVN_NO_ERROR; +} + +static const char * +describe_incoming_add_upon_update( + struct conflict_tree_incoming_add_details *details, + svn_node_kind_t new_node_kind, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (new_node_kind == svn_node_dir) + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new directory appeared during update to r%ld; " + "it was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new directory appeared during update to r%ld; " + "it was added by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new directory appeared during update to r%ld; " + "it was deleted by %s in r%ld."), new_rev, + details->deleted_rev_author, details->deleted_rev); + } + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new file appeared during update to r%ld; " + "it was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new file appeared during update to r%ld; " + "it was added by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new file appeared during update to r%ld; " + "it was deleted by %s in r%ld."), new_rev, + details->deleted_rev_author, details->deleted_rev); + } + else + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new item appeared during update to r%ld; " + "it was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new item appeared during update to r%ld; " + "it was added by %s in r%ld."), new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new item appeared during update to r%ld; " + "it was deleted by %s in r%ld."), new_rev, + details->deleted_rev_author, details->deleted_rev); + } +} + +static const char * +describe_incoming_add_upon_switch( + struct conflict_tree_incoming_add_details *details, + svn_node_kind_t victim_node_kind, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (victim_node_kind == svn_node_dir) + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new directory appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new directory appeared during switch to\n" + "'^/%s@%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new directory appeared during switch to\n" + "'^/%s@%ld'.\nIt was deleted by %s in r%ld."), + new_repos_relpath, new_rev, + details->deleted_rev_author, details->deleted_rev); + } + else if (victim_node_kind == svn_node_file || + victim_node_kind == svn_node_symlink) + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new file appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new file appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new file appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was deleted by %s in r%ld."), + new_repos_relpath, new_rev, + details->deleted_rev_author, details->deleted_rev); + } + else + { + if (SVN_IS_VALID_REVNUM(details->added_rev) && + SVN_IS_VALID_REVNUM(details->deleted_rev)) + return apr_psprintf(result_pool, + _("A new item appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld and later deleted " + "by %s in r%ld."), new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev, + details->deleted_rev_author, details->deleted_rev); + else if (SVN_IS_VALID_REVNUM(details->added_rev)) + return apr_psprintf(result_pool, + _("A new item appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new item appeared during switch to\n" + "'^/%s@%ld'.\n" + "It was deleted by %s in r%ld."), + new_repos_relpath, new_rev, + details->deleted_rev_author, details->deleted_rev); + } +} + +static const char * +describe_incoming_add_upon_merge( + struct conflict_tree_incoming_add_details *details, + svn_node_kind_t new_node_kind, + svn_revnum_t old_rev, + const char *new_repos_relpath, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (new_node_kind == svn_node_dir) + { + if (old_rev + 1 == new_rev) + return apr_psprintf(result_pool, + _("A new directory appeared during merge of\n" + "'^/%s:%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new directory appeared during merge of\n" + "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, old_rev + 1, new_rev, + details->added_rev_author, details->added_rev); + } + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + { + if (old_rev + 1 == new_rev) + return apr_psprintf(result_pool, + _("A new file appeared during merge of\n" + "'^/%s:%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new file appeared during merge of\n" + "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, old_rev + 1, new_rev, + details->added_rev_author, details->added_rev); + } + else + { + if (old_rev + 1 == new_rev) + return apr_psprintf(result_pool, + _("A new item appeared during merge of\n" + "'^/%s:%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, new_rev, + details->added_rev_author, details->added_rev); + else + return apr_psprintf(result_pool, + _("A new item appeared during merge of\n" + "'^/%s:%ld-%ld'.\nIt was added by %s in r%ld."), + new_repos_relpath, old_rev + 1, new_rev, + details->added_rev_author, details->added_rev); + } +} + +static const char * +describe_incoming_reverse_deletion_upon_merge( + struct conflict_tree_incoming_add_details *details, + svn_node_kind_t new_node_kind, + const char *old_repos_relpath, + svn_revnum_t old_rev, + svn_revnum_t new_rev, + apr_pool_t *result_pool) +{ + if (new_node_kind == svn_node_dir) + { + if (new_rev + 1 == old_rev) + return apr_psprintf(result_pool, + _("A new directory appeared during reverse-merge of" + "\n'^/%s:%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, old_rev, + details->deleted_rev_author, + details->deleted_rev); + else + return apr_psprintf(result_pool, + _("A new directory appeared during reverse-merge " + "of\n'^/%s:%ld-%ld'.\n" + "It was deleted by %s in r%ld."), + old_repos_relpath, new_rev, rev_below(old_rev), + details->deleted_rev_author, + details->deleted_rev); + } + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + { + if (new_rev + 1 == old_rev) + return apr_psprintf(result_pool, + _("A new file appeared during reverse-merge of\n" + "'^/%s:%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, old_rev, + details->deleted_rev_author, + details->deleted_rev); + else + return apr_psprintf(result_pool, + _("A new file appeared during reverse-merge of\n" + "'^/%s:%ld-%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, new_rev + 1, old_rev, + details->deleted_rev_author, + details->deleted_rev); + } + else + { + if (new_rev + 1 == old_rev) + return apr_psprintf(result_pool, + _("A new item appeared during reverse-merge of\n" + "'^/%s:%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, old_rev, + details->deleted_rev_author, + details->deleted_rev); + else + return apr_psprintf(result_pool, + _("A new item appeared during reverse-merge of\n" + "'^/%s:%ld-%ld'.\nIt was deleted by %s in r%ld."), + old_repos_relpath, new_rev + 1, old_rev, + details->deleted_rev_author, + details->deleted_rev); + } +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_description_incoming_add( + const char **incoming_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *action; + svn_node_kind_t victim_node_kind; + svn_wc_operation_t conflict_operation; + const char *old_repos_relpath; + svn_revnum_t old_rev; + svn_node_kind_t old_node_kind; + const char *new_repos_relpath; + svn_revnum_t new_rev; + svn_node_kind_t new_node_kind; + struct conflict_tree_incoming_add_details *details; + + if (conflict->tree_conflict_incoming_details == NULL) + return svn_error_trace(conflict_tree_get_incoming_description_generic( + incoming_change_description, conflict, ctx, + result_pool, scratch_pool)); + + conflict_operation = svn_client_conflict_get_operation(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, &old_node_kind, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, &new_node_kind, conflict, + scratch_pool, scratch_pool)); + + details = conflict->tree_conflict_incoming_details; + + if (conflict_operation == svn_wc_operation_update) + { + action = describe_incoming_add_upon_update(details, + new_node_kind, + new_rev, + result_pool); + } + else if (conflict_operation == svn_wc_operation_switch) + { + action = describe_incoming_add_upon_switch(details, + victim_node_kind, + new_repos_relpath, + new_rev, + result_pool); + } + else if (conflict_operation == svn_wc_operation_merge) + { + if (old_rev < new_rev) + action = describe_incoming_add_upon_merge(details, + new_node_kind, + old_rev, + new_repos_relpath, + new_rev, + result_pool); + else + action = describe_incoming_reverse_deletion_upon_merge( + details, new_node_kind, old_repos_relpath, + old_rev, new_rev, result_pool); + } + + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; +} + +/* Details for tree conflicts involving incoming edits. + * Note that we store an array of these. Each element corresponds to a + * revision within the old/new range in which a modification occured. */ +struct conflict_tree_incoming_edit_details +{ + /* The revision in which the edit ocurred. */ + svn_revnum_t rev; + + /* The author of the revision. */ + const char *author; + + /** Is the text modified? May be svn_tristate_unknown. */ + svn_tristate_t text_modified; + + /** Are properties modified? May be svn_tristate_unknown. */ + svn_tristate_t props_modified; + + /** For directories, are children modified? + * May be svn_tristate_unknown. */ + svn_tristate_t children_modified; + + /* The path which was edited, relative to the repository root. */ + const char *repos_relpath; +}; + +/* Baton for find_modified_rev(). */ +struct find_modified_rev_baton { + const char *victim_abspath; + svn_client_ctx_t *ctx; + apr_array_header_t *edits; + const char *repos_relpath; + svn_node_kind_t node_kind; + apr_pool_t *result_pool; + apr_pool_t *scratch_pool; +}; + +/* Implements svn_log_entry_receiver_t. */ +static svn_error_t * +find_modified_rev(void *baton, + svn_log_entry_t *log_entry, + apr_pool_t *scratch_pool) +{ + struct find_modified_rev_baton *b = baton; + struct conflict_tree_incoming_edit_details *details = NULL; + svn_string_t *author; + apr_hash_index_t *hi; + apr_pool_t *iterpool; + + if (b->ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + b->victim_abspath, + svn_wc_notify_tree_conflict_details_progress, + scratch_pool), + notify->revision = log_entry->revision; + b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); + } + + /* No paths were changed in this revision. Nothing to do. */ + if (! log_entry->changed_paths2) + return SVN_NO_ERROR; + + details = apr_pcalloc(b->result_pool, sizeof(*details)); + details->rev = log_entry->revision; + author = svn_hash_gets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR); + if (author) + details->author = apr_pstrdup(b->result_pool, author->data); + else + details->author = _("unknown author"); + + details->text_modified = svn_tristate_unknown; + details->props_modified = svn_tristate_unknown; + details->children_modified = svn_tristate_unknown; + + iterpool = svn_pool_create(scratch_pool); + for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); + hi != NULL; + hi = apr_hash_next(hi)) + { + void *val; + const char *path; + svn_log_changed_path2_t *log_item; + + svn_pool_clear(iterpool); + + apr_hash_this(hi, (void *) &path, NULL, &val); + log_item = val; + + /* ### Remove leading slash from paths in log entries. */ + if (path[0] == '/') + path = svn_relpath_canonicalize(path, iterpool); + + if (svn_path_compare_paths(b->repos_relpath, path) == 0 && + (log_item->action == 'M' || log_item->action == 'A')) + { + details->text_modified = log_item->text_modified; + details->props_modified = log_item->props_modified; + details->repos_relpath = apr_pstrdup(b->result_pool, path); + + if (log_item->copyfrom_path) + b->repos_relpath = apr_pstrdup(b->scratch_pool, + log_item->copyfrom_path); + } + else if (b->node_kind == svn_node_dir && + svn_relpath_skip_ancestor(b->repos_relpath, path) != NULL) + details->children_modified = svn_tristate_true; + } + + if (b->node_kind == svn_node_dir && + details->children_modified == svn_tristate_unknown) + details->children_modified = svn_tristate_false; + + APR_ARRAY_PUSH(b->edits, struct conflict_tree_incoming_edit_details *) = + details; + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* Implements tree_conflict_get_details_func_t. + * Find one or more revisions in which the victim was modified in the + * repository. */ +static svn_error_t * +conflict_tree_get_details_incoming_edit(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *old_repos_relpath; + const char *new_repos_relpath; + const char *repos_root_url; + svn_revnum_t old_rev; + svn_revnum_t new_rev; + svn_node_kind_t old_node_kind; + svn_node_kind_t new_node_kind; + svn_wc_operation_t operation; + const char *url; + const char *corrected_url; + svn_ra_session_t *ra_session; + apr_array_header_t *paths; + apr_array_header_t *revprops; + struct find_modified_rev_baton b = { 0 }; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, &old_node_kind, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, &new_node_kind, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, + scratch_pool, scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + if (operation == svn_wc_operation_update) + { + b.node_kind = old_rev < new_rev ? new_node_kind : old_node_kind; + + /* If there is no node then we cannot find any edits. */ + if (b.node_kind == svn_node_none) + return SVN_NO_ERROR; + + url = svn_path_url_add_component2(repos_root_url, + old_rev < new_rev ? new_repos_relpath + : old_repos_relpath, + scratch_pool); + + b.repos_relpath = old_rev < new_rev ? new_repos_relpath + : old_repos_relpath; + } + else if (operation == svn_wc_operation_switch || + operation == svn_wc_operation_merge) + { + url = svn_path_url_add_component2(repos_root_url, new_repos_relpath, + scratch_pool); + + b.repos_relpath = new_repos_relpath; + b.node_kind = new_node_kind; + } + + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, + &corrected_url, + url, NULL, NULL, + FALSE, + FALSE, + ctx, + scratch_pool, + scratch_pool)); + + paths = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(paths, const char *) = ""; + + revprops = apr_array_make(scratch_pool, 1, sizeof(const char *)); + APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; + + b.ctx = ctx; + b.victim_abspath = svn_client_conflict_get_local_abspath(conflict); + b.result_pool = conflict->pool; + b.scratch_pool = scratch_pool; + b.edits = apr_array_make( + conflict->pool, 0, + sizeof(struct conflict_tree_incoming_edit_details *)); + + SVN_ERR(svn_ra_get_log2(ra_session, paths, + old_rev < new_rev ? old_rev : new_rev, + old_rev < new_rev ? new_rev : old_rev, + 0, /* no limit */ + TRUE, /* need the changed paths list */ + FALSE, /* need to traverse copies */ + FALSE, /* no need for merged revisions */ + revprops, + find_modified_rev, &b, + scratch_pool)); + + conflict->tree_conflict_incoming_details = b.edits; + + return SVN_NO_ERROR; +} + +static const char * +describe_incoming_edit_upon_update(svn_revnum_t old_rev, + svn_revnum_t new_rev, + svn_node_kind_t old_node_kind, + svn_node_kind_t new_node_kind, + apr_pool_t *result_pool) +{ + if (old_rev < new_rev) + { + if (new_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Changes destined for a directory arrived " + "via the following revisions during update " + "from r%ld to r%ld."), old_rev, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Changes destined for a file arrived " + "via the following revisions during update " + "from r%ld to r%ld"), old_rev, new_rev); + else + return apr_psprintf(result_pool, + _("Changes from the following revisions arrived " + "during update from r%ld to r%ld"), + old_rev, new_rev); + } + else + { + if (new_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Changes destined for a directory arrived " + "via the following revisions during backwards " + "update from r%ld to r%ld"), + old_rev, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Changes destined for a file arrived " + "via the following revisions during backwards " + "update from r%ld to r%ld"), + old_rev, new_rev); + else + return apr_psprintf(result_pool, + _("Changes from the following revisions arrived " + "during backwards update from r%ld to r%ld"), + old_rev, new_rev); + } +} + +static const char * +describe_incoming_edit_upon_switch(const char *new_repos_relpath, + svn_revnum_t new_rev, + svn_node_kind_t new_node_kind, + apr_pool_t *result_pool) +{ + if (new_node_kind == svn_node_dir) + return apr_psprintf(result_pool, + _("Changes destined for a directory arrived via " + "the following revisions during switch to\n" + "'^/%s@r%ld'"), + new_repos_relpath, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + return apr_psprintf(result_pool, + _("Changes destined for a directory arrived via " + "the following revisions during switch to\n" + "'^/%s@r%ld'"), + new_repos_relpath, new_rev); + else + return apr_psprintf(result_pool, + _("Changes from the following revisions arrived " + "during switch to\n'^/%s@r%ld'"), + new_repos_relpath, new_rev); +} + +/* Return a string showing the list of revisions in EDITS, ensuring + * the string won't grow too large for display. */ +static const char * +describe_incoming_edit_list_modified_revs(apr_array_header_t *edits, + apr_pool_t *result_pool) +{ + int num_revs_to_skip; + static const int min_revs_for_skipping = 5; + static const int max_revs_to_display = 8; + const char *s = ""; + int i; + + if (edits->nelts <= max_revs_to_display) + num_revs_to_skip = 0; + else + { + /* Check if we should insert a placeholder for some revisions because + * the string would grow too long for display otherwise. */ + num_revs_to_skip = edits->nelts - max_revs_to_display; + if (num_revs_to_skip < min_revs_for_skipping) + { + /* Don't bother with the placeholder. Just list all revisions. */ + num_revs_to_skip = 0; + } + } + + for (i = 0; i < edits->nelts; i++) + { + struct conflict_tree_incoming_edit_details *details; + + details = APR_ARRAY_IDX(edits, i, + struct conflict_tree_incoming_edit_details *); + if (num_revs_to_skip > 0) + { + /* Insert a placeholder for revisions falling into the middle of + * the range so we'll get something that looks like: + * 1, 2, 3, 4, 5 [ placeholder ] 95, 96, 97, 98, 99 */ + if (i < max_revs_to_display / 2) + s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s, + details->rev, details->author, + i < edits->nelts - 1 ? "," : ""); + else if (i >= max_revs_to_display / 2 && + i < edits->nelts - (max_revs_to_display / 2)) + continue; + else + { + if (i == edits->nelts - (max_revs_to_display / 2)) + s = apr_psprintf(result_pool, + _("%s\n [%d revisions omitted for " + "brevity],\n"), + s, num_revs_to_skip); + + s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s, + details->rev, details->author, + i < edits->nelts - 1 ? "," : ""); + } + } + else + s = apr_psprintf(result_pool, _("%s r%ld by %s%s"), s, + details->rev, details->author, + i < edits->nelts - 1 ? "," : ""); + } + + return s; +} + +/* Implements tree_conflict_get_description_func_t. */ +static svn_error_t * +conflict_tree_get_description_incoming_edit( + const char **incoming_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *action; + svn_wc_operation_t conflict_operation; + const char *old_repos_relpath; + svn_revnum_t old_rev; + svn_node_kind_t old_node_kind; + const char *new_repos_relpath; + svn_revnum_t new_rev; + svn_node_kind_t new_node_kind; + apr_array_header_t *edits; + + if (conflict->tree_conflict_incoming_details == NULL) + return svn_error_trace(conflict_tree_get_incoming_description_generic( + incoming_change_description, conflict, ctx, + result_pool, scratch_pool)); + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, &old_rev, &old_node_kind, conflict, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &new_repos_relpath, &new_rev, &new_node_kind, conflict, + scratch_pool, scratch_pool)); + + conflict_operation = svn_client_conflict_get_operation(conflict); + + edits = conflict->tree_conflict_incoming_details; + + if (conflict_operation == svn_wc_operation_update) + action = describe_incoming_edit_upon_update(old_rev, new_rev, + old_node_kind, new_node_kind, + scratch_pool); + else if (conflict_operation == svn_wc_operation_switch) + action = describe_incoming_edit_upon_switch(new_repos_relpath, new_rev, + new_node_kind, scratch_pool); + else if (conflict_operation == svn_wc_operation_merge) + { + /* Handle merge inline because it returns early sometimes. */ + if (old_rev < new_rev) + { + if (old_rev + 1 == new_rev) + { + if (new_node_kind == svn_node_dir) + action = apr_psprintf(scratch_pool, + _("Changes destined for a directory " + "arrived during merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + action = apr_psprintf(scratch_pool, + _("Changes destined for a file " + "arrived during merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, new_rev); + else + action = apr_psprintf(scratch_pool, + _("Changes arrived during merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, new_rev); + + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; + } + else + { + if (new_node_kind == svn_node_dir) + action = apr_psprintf(scratch_pool, + _("Changes destined for a directory " + "arrived via the following revisions " + "during merge of\n'^/%s:%ld-%ld'"), + new_repos_relpath, old_rev + 1, new_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + action = apr_psprintf(scratch_pool, + _("Changes destined for a file " + "arrived via the following revisions " + "during merge of\n'^/%s:%ld-%ld'"), + new_repos_relpath, old_rev + 1, new_rev); + else + action = apr_psprintf(scratch_pool, + _("Changes from the following revisions " + "arrived during merge of\n" + "'^/%s:%ld-%ld'"), + new_repos_relpath, old_rev + 1, new_rev); + } + } + else + { + if (new_rev + 1 == old_rev) + { + if (new_node_kind == svn_node_dir) + action = apr_psprintf(scratch_pool, + _("Changes destined for a directory " + "arrived during reverse-merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, old_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + action = apr_psprintf(scratch_pool, + _("Changes destined for a file " + "arrived during reverse-merge of\n" + "'^/%s:%ld'."), + new_repos_relpath, old_rev); + else + action = apr_psprintf(scratch_pool, + _("Changes arrived during reverse-merge " + "of\n'^/%s:%ld'."), + new_repos_relpath, old_rev); + + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; + } + else + { + if (new_node_kind == svn_node_dir) + action = apr_psprintf(scratch_pool, + _("Changes destined for a directory " + "arrived via the following revisions " + "during reverse-merge of\n" + "'^/%s:%ld-%ld'"), + new_repos_relpath, new_rev + 1, old_rev); + else if (new_node_kind == svn_node_file || + new_node_kind == svn_node_symlink) + action = apr_psprintf(scratch_pool, + _("Changes destined for a file " + "arrived via the following revisions " + "during reverse-merge of\n" + "'^/%s:%ld-%ld'"), + new_repos_relpath, new_rev + 1, old_rev); + + else + action = apr_psprintf(scratch_pool, + _("Changes from the following revisions " + "arrived during reverse-merge of\n" + "'^/%s:%ld-%ld'"), + new_repos_relpath, new_rev + 1, old_rev); + } + } + } + + action = apr_psprintf(scratch_pool, "%s:\n%s", action, + describe_incoming_edit_list_modified_revs( + edits, scratch_pool)); + *incoming_change_description = apr_pstrdup(result_pool, action); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_tree_get_description( + const char **incoming_change_description, + const char **local_change_description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + SVN_ERR(conflict->tree_conflict_get_incoming_description_func( + incoming_change_description, + conflict, ctx, result_pool, scratch_pool)); + + SVN_ERR(conflict->tree_conflict_get_local_description_func( + local_change_description, + conflict, ctx, result_pool, scratch_pool)); + + return SVN_NO_ERROR; +} + +void +svn_client_conflict_option_set_merged_propval( + svn_client_conflict_option_t *option, + const svn_string_t *merged_propval) +{ + option->type_data.prop.merged_propval = svn_string_dup(merged_propval, + option->pool); +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_postpone(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + return SVN_NO_ERROR; /* Nothing to do. */ +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_text_conflict(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + const char *lock_abspath; + svn_wc_conflict_choice_t conflict_choice; + svn_error_t *err; + + option_id = svn_client_conflict_option_get_id(option); + conflict_choice = conflict_option_id_to_wc_conflict_choice(option_id); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_text_mark_resolved(ctx->wc_ctx, + local_abspath, + conflict_choice, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + conflict->resolution_text = option_id; + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_prop_conflict(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + svn_wc_conflict_choice_t conflict_choice; + const char *local_abspath; + const char *lock_abspath; + const char *propname = option->type_data.prop.propname; + svn_error_t *err; + const svn_string_t *merged_value; + + option_id = svn_client_conflict_option_get_id(option); + conflict_choice = conflict_option_id_to_wc_conflict_choice(option_id); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + if (option_id == svn_client_conflict_option_merged_text) + merged_value = option->type_data.prop.merged_propval; + else + merged_value = NULL; + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_prop_mark_resolved(ctx->wc_ctx, local_abspath, + propname, conflict_choice, + merged_value, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (propname[0] == '\0') + { + apr_hash_index_t *hi; + + /* All properties have been resolved to the same option. */ + for (hi = apr_hash_first(scratch_pool, conflict->prop_conflicts); + hi; + hi = apr_hash_next(hi)) + { + const char *this_propname = apr_hash_this_key(hi); + + svn_hash_sets(conflict->resolved_props, + apr_pstrdup(apr_hash_pool_get(conflict->resolved_props), + this_propname), + option); + svn_hash_sets(conflict->prop_conflicts, this_propname, NULL); + } + + conflict->legacy_prop_conflict_propname = NULL; + } + else + { + svn_hash_sets(conflict->resolved_props, + apr_pstrdup(apr_hash_pool_get(conflict->resolved_props), + propname), + option); + svn_hash_sets(conflict->prop_conflicts, propname, NULL); + + if (apr_hash_count(conflict->prop_conflicts) > 0) + conflict->legacy_prop_conflict_propname = + apr_hash_this_key(apr_hash_first(scratch_pool, + conflict->prop_conflicts)); + else + conflict->legacy_prop_conflict_propname = NULL; + } + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_accept_current_wc_state(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + option_id = svn_client_conflict_option_get_id(option); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + if (option_id != svn_client_conflict_option_accept_current_wc_state) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Tree conflict on '%s' can only be resolved " + "to the current working copy state"), + svn_dirent_local_style(local_abspath, + scratch_pool)); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + /* Resolve to current working copy state. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + + /* svn_wc__del_tree_conflict doesn't handle notification for us */ + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = option_id; + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_update_break_moved_away(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_tree_update_break_moved_away(ctx->wc_ctx, + local_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_update_raise_moved_away(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_tree_update_raise_moved_away(ctx->wc_ctx, + local_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_update_moved_away_node(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + err = svn_wc__conflict_tree_update_moved_away_node(ctx->wc_ctx, + local_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Verify the local working copy state matches what we expect when an + * incoming add vs add tree conflict exists after an update operation. + * We assume the update operation leaves the working copy in a state which + * prefers the local change and cancels the incoming addition. + * Run a quick sanity check and error out if it looks as if the + * working copy was modified since, even though it's not easy to make + * such modifications without also clearing the conflict marker. */ +static svn_error_t * +verify_local_state_for_incoming_add_upon_update( + svn_client_conflict_t *conflict, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + svn_client_conflict_option_id_t option_id; + const char *wcroot_abspath; + svn_wc_operation_t operation; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + const char *base_repos_relpath; + svn_revnum_t base_rev; + svn_node_kind_t base_kind; + const char *local_style_relpath; + svn_boolean_t is_added; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + option_id = svn_client_conflict_option_get_id(option); + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + local_abspath, scratch_pool, + scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + SVN_ERR_ASSERT(operation == svn_wc_operation_update); + + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + local_style_relpath = svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + local_abspath), + scratch_pool); + + /* Check if a local addition addition replaces the incoming new node. */ + err = svn_wc__node_get_base(&base_kind, &base_rev, &base_repos_relpath, + NULL, NULL, NULL, ctx->wc_ctx, local_abspath, + FALSE, scratch_pool, scratch_pool); + if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) + { + if (option_id == svn_client_conflict_option_incoming_add_ignore) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, + _("Cannot resolve tree conflict on '%s' " + "(expected a base node but found none)"), + local_style_relpath); + else if (option_id == + svn_client_conflict_option_incoming_added_dir_replace) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, + _("Cannot resolve tree conflict on '%s' " + "(expected a base node but found none)"), + local_style_relpath); + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, + _("Unexpected option id '%d'"), option_id); + } + else if (err) + return svn_error_trace(err); + + if (base_kind != incoming_new_kind) + { + if (option_id == svn_client_conflict_option_incoming_add_ignore) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected base node kind '%s', " + "but found '%s')"), + local_style_relpath, + svn_node_kind_to_word(incoming_new_kind), + svn_node_kind_to_word(base_kind)); + else if (option_id == + svn_client_conflict_option_incoming_added_dir_replace) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected base node kind '%s', " + "but found '%s')"), + local_style_relpath, + svn_node_kind_to_word(incoming_new_kind), + svn_node_kind_to_word(base_kind)); + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Unexpected option id '%d'"), option_id); + } + + if (strcmp(base_repos_relpath, incoming_new_repos_relpath) != 0 || + base_rev != incoming_new_pegrev) + { + if (option_id == svn_client_conflict_option_incoming_add_ignore) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected base node from '^/%s@%ld', " + "but found '^/%s@%ld')"), + local_style_relpath, + incoming_new_repos_relpath, + incoming_new_pegrev, + base_repos_relpath, base_rev); + else if (option_id == + svn_client_conflict_option_incoming_added_dir_replace) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected base node from '^/%s@%ld', " + "but found '^/%s@%ld')"), + local_style_relpath, + incoming_new_repos_relpath, + incoming_new_pegrev, + base_repos_relpath, base_rev); + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Unexpected option id '%d'"), option_id); + } + + SVN_ERR(svn_wc__node_is_added(&is_added, ctx->wc_ctx, local_abspath, + scratch_pool)); + if (!is_added) + { + if (option_id == svn_client_conflict_option_incoming_add_ignore) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an added item, but the item " + "is not added)"), + local_style_relpath); + + else if (option_id == + svn_client_conflict_option_incoming_added_dir_replace) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an added item, but the item " + "is not added)"), + local_style_relpath); + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Unexpected option id '%d'"), option_id); + } + + return SVN_NO_ERROR; +} + + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_add_ignore(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_wc_operation_t operation; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + operation = svn_client_conflict_get_operation(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + if (operation == svn_wc_operation_update) + { + err = verify_local_state_for_incoming_add_upon_update(conflict, option, + ctx, scratch_pool); + if (err) + goto unlock_wc; + } + + /* All other options for this conflict actively fetch the incoming + * new node. We can ignore the incoming new node by doing nothing. */ + + /* Resolve to current working copy state. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + + /* svn_wc__del_tree_conflict doesn't handle notification for us */ + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Delete entry and wc props from a set of properties. */ +static void +filter_props(apr_hash_t *props, apr_pool_t *scratch_pool) +{ + apr_hash_index_t *hi; + + for (hi = apr_hash_first(scratch_pool, props); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *propname = apr_hash_this_key(hi); + + if (!svn_wc_is_normal_prop(propname)) + svn_hash_sets(props, propname, NULL); + } +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_file_text_update( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *wc_tmpdir; + const char *local_abspath; + const char *lock_abspath; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + const char *empty_file_abspath; + const char *working_file_tmp_abspath; + svn_stream_t *working_file_stream; + svn_stream_t *working_file_tmp_stream; + apr_hash_t *working_props; + apr_array_header_t *propdiffs; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Set up tempory storage for the working version of file. */ + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_open_unique(&working_file_tmp_stream, + &working_file_tmp_abspath, wc_tmpdir, + /* Don't delete automatically! */ + svn_io_file_del_none, + scratch_pool, scratch_pool)); + + /* Copy the detranslated working file to temporary storage. */ + SVN_ERR(svn_wc__translated_stream(&working_file_stream, ctx->wc_ctx, + local_abspath, local_abspath, + SVN_WC_TRANSLATE_TO_NF, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_copy3(working_file_stream, working_file_tmp_stream, + ctx->cancel_func, ctx->cancel_baton, + scratch_pool)); + + /* Get a copy of the working file's properties. */ + SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + filter_props(working_props, scratch_pool); + + /* Create an empty file as fake "merge-base" for the two added files. + * The files are not ancestrally related so this is the best we can do. */ + SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file_abspath, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Create a property diff which shows all props as added. */ + SVN_ERR(svn_prop_diffs(&propdiffs, working_props, + apr_hash_make(scratch_pool), scratch_pool)); + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + /* Revert the path in order to restore the repository's line of + * history, which is part of the BASE tree. This revert operation + * is why are being careful about not losing the temporary copy. */ + err = svn_wc_revert5(ctx->wc_ctx, local_abspath, svn_depth_empty, + FALSE, NULL, TRUE, FALSE, + NULL, NULL, /* no cancellation */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + + /* Perform the file merge. ### Merge into tempfile and then rename on top? */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, empty_file_abspath, + working_file_tmp_abspath, local_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + NULL, propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + +unlock_wc: + if (err) + err = svn_error_quick_wrapf( + err, _("If needed, a backup copy of '%s' can be found at '%s'"), + svn_dirent_local_style(local_abspath, scratch_pool), + svn_dirent_local_style(working_file_tmp_abspath, scratch_pool)); + err = svn_error_compose_create(err, + svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + /* Tell the world about the file merge that just happened. */ + notify = svn_wc_create_notify(local_abspath, + svn_wc_notify_update_update, + scratch_pool); + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + + /* And also about the successfully resolved tree conflict. */ + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + /* All is good -- remove temporary copy of the working file. */ + SVN_ERR(svn_io_remove_file2(working_file_tmp_abspath, TRUE, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_file_text_merge( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + const char *repos_root_url; + const char *wc_tmpdir; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *local_abspath; + const char *lock_abspath; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_file_t *incoming_new_file; + const char *incoming_new_tmp_abspath; + const char *empty_file_abspath; + svn_stream_t *incoming_new_stream; + apr_hash_t *incoming_new_props; + apr_array_header_t *propdiffs; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Set up temporary storage for the repository version of file. */ + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_io_open_unique_file3(&incoming_new_file, + &incoming_new_tmp_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + incoming_new_stream = svn_stream_from_aprfile2(incoming_new_file, TRUE, + scratch_pool); + + /* Fetch the incoming added file from the repository. */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, + incoming_new_stream, NULL, /* fetched_rev */ + &incoming_new_props, scratch_pool)); + + /* Flush file to disk. */ + SVN_ERR(svn_stream_close(incoming_new_stream)); + SVN_ERR(svn_io_file_flush(incoming_new_file, scratch_pool)); + + filter_props(incoming_new_props, scratch_pool); + + /* Create an empty file as fake "merge-base" for the two added files. + * The files are not ancestrally related so this is the best we can do. */ + SVN_ERR(svn_io_open_unique_file3(NULL, &empty_file_abspath, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Create a property diff which shows all props as added. */ + SVN_ERR(svn_prop_diffs(&propdiffs, incoming_new_props, + apr_hash_make(scratch_pool), scratch_pool)); + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + /* Resolve to current working copy state. svn_wc_merge5() requires this. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + if (err) + return svn_error_compose_create(err, + svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + /* Perform the file merge. ### Merge into tempfile and then rename on top? */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, empty_file_abspath, + incoming_new_tmp_abspath, local_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + NULL, propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + /* Tell the world about the file merge that just happened. */ + notify = svn_wc_create_notify(local_abspath, + svn_wc_notify_update_update, + scratch_pool); + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + + /* And also about the successfully resolved tree conflict. */ + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_file_replace_and_merge( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + const char *repos_root_url; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + apr_file_t *incoming_new_file; + svn_stream_t *incoming_new_stream; + apr_hash_t *incoming_new_props; + const char *local_abspath; + const char *lock_abspath; + const char *wc_tmpdir; + svn_stream_t *working_file_tmp_stream; + const char *working_file_tmp_abspath; + svn_stream_t *working_file_stream; + apr_hash_t *working_props; + svn_error_t *err; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_file_t *empty_file; + const char *empty_file_abspath; + apr_array_header_t *propdiffs; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Set up tempory storage for the working version of file. */ + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_open_unique(&working_file_tmp_stream, + &working_file_tmp_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Copy the detranslated working file to temporary storage. */ + SVN_ERR(svn_wc__translated_stream(&working_file_stream, ctx->wc_ctx, + local_abspath, local_abspath, + SVN_WC_TRANSLATE_TO_NF, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_copy3(working_file_stream, working_file_tmp_stream, + ctx->cancel_func, ctx->cancel_baton, + scratch_pool)); + + /* Get a copy of the working file's properties. */ + SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + /* Fetch the incoming added file from the repository. */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + if (corrected_url) + url = corrected_url; + SVN_ERR(svn_io_open_unique_file3(&incoming_new_file, NULL, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + incoming_new_stream = svn_stream_from_aprfile2(incoming_new_file, TRUE, + scratch_pool); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, + incoming_new_stream, NULL, /* fetched_rev */ + &incoming_new_props, scratch_pool)); + /* Flush file to disk. */ + SVN_ERR(svn_io_file_flush(incoming_new_file, scratch_pool)); + + /* Reset the stream in preparation for adding its content to WC. */ + SVN_ERR(svn_stream_reset(incoming_new_stream)); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + /* ### The following WC modifications should be atomic. */ + + /* Replace the working file with the file from the repository. */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + err = svn_wc_add_repos_file4(ctx->wc_ctx, local_abspath, + incoming_new_stream, + NULL, /* ### could we merge first, then set + ### the merged content here? */ + incoming_new_props, + NULL, /* ### merge props first, set here? */ + url, incoming_new_pegrev, + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + if (err) + goto unlock_wc; + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath, + svn_wc_notify_add, + scratch_pool); + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + /* Resolve to current working copy state. svn_wc_merge5() requires this. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + if (err) + goto unlock_wc; + + /* Create an empty file as fake "merge-base" for the two added files. + * The files are not ancestrally related so this is the best we can do. */ + err = svn_io_open_unique_file3(&empty_file, &empty_file_abspath, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + filter_props(incoming_new_props, scratch_pool); + + /* Create a property diff for the files. */ + err = svn_prop_diffs(&propdiffs, incoming_new_props, + working_props, scratch_pool); + if (err) + goto unlock_wc; + + /* Perform the file merge. */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, empty_file_abspath, + working_file_tmp_abspath, local_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + NULL, propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + if (err) + goto unlock_wc; + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify( + local_abspath, + svn_wc_notify_update_update, + scratch_pool); + + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + SVN_ERR(svn_stream_close(incoming_new_stream)); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify( + local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool); + + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +static svn_error_t * +raise_tree_conflict(const char *local_abspath, + svn_wc_conflict_action_t incoming_change, + svn_wc_conflict_reason_t local_change, + svn_node_kind_t local_node_kind, + svn_node_kind_t merge_left_kind, + svn_node_kind_t merge_right_kind, + const char *repos_root_url, + const char *repos_uuid, + const char *repos_relpath, + svn_revnum_t merge_left_rev, + svn_revnum_t merge_right_rev, + svn_wc_context_t *wc_ctx, + svn_wc_notify_func2_t notify_func2, + void *notify_baton2, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_description2_t *conflict; + const svn_wc_conflict_version_t *left_version; + const svn_wc_conflict_version_t *right_version; + + left_version = svn_wc_conflict_version_create2(repos_root_url, + repos_uuid, + repos_relpath, + merge_left_rev, + merge_left_kind, + scratch_pool); + right_version = svn_wc_conflict_version_create2(repos_root_url, + repos_uuid, + repos_relpath, + merge_right_rev, + merge_right_kind, + scratch_pool); + conflict = svn_wc_conflict_description_create_tree2(local_abspath, + local_node_kind, + svn_wc_operation_merge, + left_version, + right_version, + scratch_pool); + conflict->action = incoming_change; + conflict->reason = local_change; + + SVN_ERR(svn_wc__add_tree_conflict(wc_ctx, conflict, scratch_pool)); + + if (notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_tree_conflict, + scratch_pool); + notify->kind = local_node_kind; + notify_func2(notify_baton2, notify, scratch_pool); + } + + return SVN_NO_ERROR; +} + +struct merge_newly_added_dir_baton { + const char *target_abspath; + svn_client_ctx_t *ctx; + const char *repos_root_url; + const char *repos_uuid; + const char *added_repos_relpath; + svn_revnum_t merge_left_rev; + svn_revnum_t merge_right_rev; +}; + +static svn_error_t * +merge_added_dir_props(const char *target_abspath, + const char *added_repos_relpath, + apr_hash_t *added_props, + const char *repos_root_url, + const char *repos_uuid, + svn_revnum_t merge_left_rev, + svn_revnum_t merge_right_rev, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_wc_notify_state_t property_state; + apr_array_header_t *propchanges; + const svn_wc_conflict_version_t *left_version; + const svn_wc_conflict_version_t *right_version; + apr_hash_index_t *hi; + + left_version = svn_wc_conflict_version_create2( + repos_root_url, repos_uuid, added_repos_relpath, + merge_left_rev, svn_node_none, scratch_pool); + + right_version = svn_wc_conflict_version_create2( + repos_root_url, repos_uuid, added_repos_relpath, + merge_right_rev, svn_node_dir, scratch_pool); + + propchanges = apr_array_make(scratch_pool, apr_hash_count(added_props), + sizeof(svn_prop_t)); + for (hi = apr_hash_first(scratch_pool, added_props); + hi; + hi = apr_hash_next(hi)) + { + svn_prop_t prop; + + prop.name = apr_hash_this_key(hi); + prop.value = apr_hash_this_val(hi); + + if (svn_wc_is_normal_prop(prop.name)) + APR_ARRAY_PUSH(propchanges, svn_prop_t) = prop; + } + + SVN_ERR(svn_wc_merge_props3(&property_state, ctx->wc_ctx, + target_abspath, + left_version, right_version, + apr_hash_make(scratch_pool), + propchanges, + FALSE, /* not a dry-run */ + NULL, NULL, NULL, NULL, + scratch_pool)); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify(target_abspath, + svn_wc_notify_update_update, + scratch_pool); + notify->kind = svn_node_dir; + notify->content_state = svn_wc_notify_state_unchanged;; + notify->prop_state = property_state; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + return SVN_NO_ERROR; +} + +/* An svn_diff_tree_processor_t callback. */ +static svn_error_t * +diff_dir_added(const char *relpath, + const svn_diff_source_t *copyfrom_source, + const svn_diff_source_t *right_source, + apr_hash_t *copyfrom_props, + apr_hash_t *right_props, + void *dir_baton, + const struct svn_diff_tree_processor_t *processor, + apr_pool_t *scratch_pool) +{ + struct merge_newly_added_dir_baton *b = processor->baton; + const char *local_abspath; + const char *copyfrom_url; + svn_node_kind_t db_kind; + svn_node_kind_t on_disk_kind; + apr_hash_index_t *hi; + + /* Handle the root of the added directory tree. */ + if (relpath[0] == '\0') + { + /* ### svn_wc_merge_props3() requires this... */ + SVN_ERR(svn_wc__del_tree_conflict(b->ctx->wc_ctx, b->target_abspath, + scratch_pool)); + SVN_ERR(merge_added_dir_props(b->target_abspath, + b->added_repos_relpath, right_props, + b->repos_root_url, b->repos_uuid, + b->merge_left_rev, b->merge_right_rev, + b->ctx, scratch_pool)); + return SVN_NO_ERROR; + + } + + local_abspath = svn_dirent_join(b->target_abspath, relpath, scratch_pool); + + SVN_ERR(svn_wc_read_kind2(&db_kind, b->ctx->wc_ctx, local_abspath, + FALSE, FALSE, scratch_pool)); + SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool)); + + if (db_kind == svn_node_dir && on_disk_kind == svn_node_dir) + { + SVN_ERR(merge_added_dir_props(svn_dirent_join(b->target_abspath, relpath, + scratch_pool), + b->added_repos_relpath, right_props, + b->repos_root_url, b->repos_uuid, + b->merge_left_rev, b->merge_right_rev, + b->ctx, scratch_pool)); + return SVN_NO_ERROR; + } + + if (db_kind != svn_node_none && db_kind != svn_node_unknown) + { + SVN_ERR(raise_tree_conflict( + local_abspath, svn_wc_conflict_action_add, + svn_wc_conflict_reason_obstructed, + db_kind, svn_node_none, svn_node_dir, + b->repos_root_url, b->repos_uuid, + svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), + b->merge_left_rev, b->merge_right_rev, + b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + return SVN_NO_ERROR; + } + + if (on_disk_kind != svn_node_none) + { + SVN_ERR(raise_tree_conflict( + local_abspath, svn_wc_conflict_action_add, + svn_wc_conflict_reason_obstructed, db_kind, + svn_node_none, svn_node_dir, b->repos_root_url, b->repos_uuid, + svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), + b->merge_left_rev, b->merge_right_rev, + b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + return SVN_NO_ERROR; + } + + SVN_ERR(svn_io_dir_make(local_abspath, APR_OS_DEFAULT, scratch_pool)); + copyfrom_url = apr_pstrcat(scratch_pool, b->repos_root_url, "/", + right_source->repos_relpath, SVN_VA_NULL); + SVN_ERR(svn_wc_add4(b->ctx->wc_ctx, local_abspath, svn_depth_infinity, + copyfrom_url, right_source->revision, + NULL, NULL, /* cancel func/baton */ + b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + + for (hi = apr_hash_first(scratch_pool, right_props); + hi; + hi = apr_hash_next(hi)) + { + const char *propname = apr_hash_this_key(hi); + const svn_string_t *propval = apr_hash_this_val(hi); + + SVN_ERR(svn_wc_prop_set4(b->ctx->wc_ctx, local_abspath, + propname, propval, svn_depth_empty, + FALSE, NULL /* do not skip checks */, + NULL, NULL, /* cancel func/baton */ + b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +merge_added_files(const char *local_abspath, + const char *incoming_added_file_abspath, + apr_hash_t *incoming_added_file_props, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_file_t *empty_file; + const char *empty_file_abspath; + apr_array_header_t *propdiffs; + apr_hash_t *working_props; + + /* Create an empty file as fake "merge-base" for the two added files. + * The files are not ancestrally related so this is the best we can do. */ + SVN_ERR(svn_io_open_unique_file3(&empty_file, &empty_file_abspath, NULL, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Get a copy of the working file's properties. */ + SVN_ERR(svn_wc_prop_list2(&working_props, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + /* Create a property diff for the files. */ + SVN_ERR(svn_prop_diffs(&propdiffs, incoming_added_file_props, + working_props, scratch_pool)); + + /* Perform the file merge. */ + SVN_ERR(svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, empty_file_abspath, + incoming_added_file_abspath, local_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + NULL, propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool)); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify( + local_abspath, + svn_wc_notify_update_update, + scratch_pool); + + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + return SVN_NO_ERROR; +} + +/* An svn_diff_tree_processor_t callback. */ +static svn_error_t * +diff_file_added(const char *relpath, + const svn_diff_source_t *copyfrom_source, + const svn_diff_source_t *right_source, + const char *copyfrom_file, + const char *right_file, + apr_hash_t *copyfrom_props, + apr_hash_t *right_props, + void *file_baton, + const struct svn_diff_tree_processor_t *processor, + apr_pool_t *scratch_pool) +{ + struct merge_newly_added_dir_baton *b = processor->baton; + const char *local_abspath; + svn_node_kind_t db_kind; + svn_node_kind_t on_disk_kind; + apr_array_header_t *propsarray; + apr_array_header_t *regular_props; + + local_abspath = svn_dirent_join(b->target_abspath, relpath, scratch_pool); + + SVN_ERR(svn_wc_read_kind2(&db_kind, b->ctx->wc_ctx, local_abspath, + FALSE, FALSE, scratch_pool)); + SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool)); + + if (db_kind == svn_node_file && on_disk_kind == svn_node_file) + { + propsarray = svn_prop_hash_to_array(right_props, scratch_pool); + SVN_ERR(svn_categorize_props(propsarray, NULL, NULL, ®ular_props, + scratch_pool)); + SVN_ERR(merge_added_files(local_abspath, right_file, + svn_prop_array_to_hash(regular_props, + scratch_pool), + b->ctx, scratch_pool)); + return SVN_NO_ERROR; + } + + if (db_kind != svn_node_none && db_kind != svn_node_unknown) + { + SVN_ERR(raise_tree_conflict( + local_abspath, svn_wc_conflict_action_add, + svn_wc_conflict_reason_obstructed, + db_kind, svn_node_none, svn_node_file, + b->repos_root_url, b->repos_uuid, + svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), + b->merge_left_rev, b->merge_right_rev, + b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + return SVN_NO_ERROR; + } + + if (on_disk_kind != svn_node_none) + { + SVN_ERR(raise_tree_conflict( + local_abspath, svn_wc_conflict_action_add, + svn_wc_conflict_reason_obstructed, db_kind, + svn_node_none, svn_node_file, b->repos_root_url, b->repos_uuid, + svn_relpath_join(b->added_repos_relpath, relpath, scratch_pool), + b->merge_left_rev, b->merge_right_rev, + b->ctx->wc_ctx, b->ctx->notify_func2, b->ctx->notify_baton2, + scratch_pool)); + return SVN_NO_ERROR; + } + + propsarray = svn_prop_hash_to_array(right_props, scratch_pool); + SVN_ERR(svn_categorize_props(propsarray, NULL, NULL, ®ular_props, + scratch_pool)); + SVN_ERR(svn_io_copy_file(right_file, local_abspath, FALSE, scratch_pool)); + SVN_ERR(svn_wc_add_from_disk3(b->ctx->wc_ctx, local_abspath, + svn_prop_array_to_hash(regular_props, + scratch_pool), + FALSE, b->ctx->notify_func2, + b->ctx->notify_baton2, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Merge a newly added directory into TARGET_ABSPATH in the working copy. + * + * This uses a diff-tree processor because our standard merge operation + * is not set up for merges where the merge-source anchor is itself an + * added directory (i.e. does not exist on one side of the diff). + * The standard merge will only merge additions of children of a path + * that exists across the entire revision range being merged. + * But in our case, SOURCE1 does not yet exist in REV1, but SOURCE2 + * does exist in REV2. Thus we use a diff processor. + */ +static svn_error_t * +merge_newly_added_dir(const char *added_repos_relpath, + const char *source1, + svn_revnum_t rev1, + const char *source2, + svn_revnum_t rev2, + const char *target_abspath, + svn_boolean_t reverse_merge, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_diff_tree_processor_t *processor; + struct merge_newly_added_dir_baton baton = { 0 }; + const svn_diff_tree_processor_t *diff_processor; + svn_ra_session_t *ra_session; + const char *corrected_url; + svn_ra_session_t *extra_ra_session; + const svn_ra_reporter3_t *reporter; + void *reporter_baton; + const svn_delta_editor_t *diff_editor; + void *diff_edit_baton; + const char *anchor1; + const char *anchor2; + const char *target1; + const char *target2; + + svn_uri_split(&anchor1, &target1, source1, scratch_pool); + svn_uri_split(&anchor2, &target2, source2, scratch_pool); + + baton.target_abspath = target_abspath; + baton.ctx = ctx; + baton.added_repos_relpath = added_repos_relpath; + SVN_ERR(svn_wc__node_get_repos_info(NULL, NULL, + &baton.repos_root_url, &baton.repos_uuid, + ctx->wc_ctx, target_abspath, + scratch_pool, scratch_pool)); + baton.merge_left_rev = rev1; + baton.merge_right_rev = rev2; + + processor = svn_diff__tree_processor_create(&baton, scratch_pool); + processor->dir_added = diff_dir_added; + processor->file_added = diff_file_added; + + diff_processor = processor; + if (reverse_merge) + diff_processor = svn_diff__tree_processor_reverse_create(diff_processor, + NULL, + scratch_pool); + + /* Filter the first path component using a filter processor, until we fixed + the diff processing to handle this directly */ + diff_processor = svn_diff__tree_processor_filter_create( + diff_processor, target1, scratch_pool); + + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + anchor2, NULL, NULL, FALSE, + FALSE, ctx, + scratch_pool, scratch_pool)); + if (corrected_url) + anchor2 = corrected_url; + + /* Extra RA session is used during the editor calls to fetch file contents. */ + SVN_ERR(svn_ra__dup_session(&extra_ra_session, ra_session, anchor2, + scratch_pool, scratch_pool)); + + /* Create a repos-repos diff editor. */ + SVN_ERR(svn_client__get_diff_editor2( + &diff_editor, &diff_edit_baton, + extra_ra_session, svn_depth_infinity, rev1, TRUE, + diff_processor, ctx->cancel_func, ctx->cancel_baton, + scratch_pool)); + + /* We want to switch our txn into URL2 */ + SVN_ERR(svn_ra_do_diff3(ra_session, &reporter, &reporter_baton, + rev2, target1, svn_depth_infinity, TRUE, TRUE, + source2, diff_editor, diff_edit_baton, scratch_pool)); + + /* Drive the reporter; do the diff. */ + SVN_ERR(reporter->set_path(reporter_baton, "", rev1, + svn_depth_infinity, + FALSE, NULL, + scratch_pool)); + + SVN_ERR(reporter->finish_report(reporter_baton, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_dir_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *repos_root_url; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *local_abspath; + const char *lock_abspath; + struct conflict_tree_incoming_add_details *details; + const char *added_repos_relpath; + const char *source1; + svn_revnum_t rev1; + const char *source2; + svn_revnum_t rev2; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + details = conflict->tree_conflict_incoming_details; + if (details == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Conflict resolution option '%d' requires " + "details for tree conflict at '%s' to be " + "fetched from the repository"), + option->id, + svn_dirent_local_style(local_abspath, + scratch_pool)); + + /* Set up merge sources to merge the entire incoming added directory tree. */ + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + source1 = svn_path_url_add_component2(repos_root_url, + details->repos_relpath, + scratch_pool); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + NULL, conflict, scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, scratch_pool)); + if (incoming_old_pegrev < incoming_new_pegrev) /* forward merge */ + { + if (details->added_rev == SVN_INVALID_REVNUM) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Could not determine when '%s' was " + "added the repository"), + svn_dirent_local_style(local_abspath, + scratch_pool)); + rev1 = rev_below(details->added_rev); + source2 = svn_path_url_add_component2(repos_root_url, + incoming_new_repos_relpath, + scratch_pool); + rev2 = incoming_new_pegrev; + added_repos_relpath = incoming_new_repos_relpath; + } + else /* reverse-merge */ + { + if (details->deleted_rev == SVN_INVALID_REVNUM) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Could not determine when '%s' was " + "deleted from the repository"), + svn_dirent_local_style(local_abspath, + scratch_pool)); + rev1 = details->deleted_rev; + source2 = svn_path_url_add_component2(repos_root_url, + incoming_old_repos_relpath, + scratch_pool); + rev2 = incoming_old_pegrev; + added_repos_relpath = incoming_new_repos_relpath; + } + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + /* ### wrap in a transaction */ + err = merge_newly_added_dir(added_repos_relpath, + source1, rev1, source2, rev2, + local_abspath, + (incoming_old_pegrev > incoming_new_pegrev), + ctx, scratch_pool, scratch_pool); + if (!err) + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_update_incoming_added_dir_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve( + &lock_abspath, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + err = svn_wc__conflict_tree_update_local_add(ctx->wc_ctx, + local_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + return SVN_NO_ERROR; +} + +/* A baton for notification_adjust_func(). */ +struct notification_adjust_baton +{ + svn_wc_notify_func2_t inner_func; + void *inner_baton; + const char *checkout_abspath; + const char *final_abspath; +}; + +/* A svn_wc_notify_func2_t function that wraps BATON->inner_func (whose + * baton is BATON->inner_baton) and adjusts the notification paths that + * start with BATON->checkout_abspath to start instead with + * BATON->final_abspath. */ +static void +notification_adjust_func(void *baton, + const svn_wc_notify_t *notify, + apr_pool_t *pool) +{ + struct notification_adjust_baton *nb = baton; + svn_wc_notify_t *inner_notify = svn_wc_dup_notify(notify, pool); + const char *relpath; + + relpath = svn_dirent_skip_ancestor(nb->checkout_abspath, notify->path); + inner_notify->path = svn_dirent_join(nb->final_abspath, relpath, pool); + + if (nb->inner_func) + nb->inner_func(nb->inner_baton, inner_notify, pool); +} + +/* Resolve a dir/dir "incoming add vs local obstruction" tree conflict by + * replacing the local directory with the incoming directory. + * If MERGE_DIRS is set, also merge the directories after replacing. */ +static svn_error_t * +merge_incoming_added_dir_replace(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + svn_boolean_t merge_dirs, + apr_pool_t *scratch_pool) +{ + svn_ra_session_t *ra_session; + const char *url; + const char *corrected_url; + const char *repos_root_url; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *local_abspath; + const char *lock_abspath; + const char *tmpdir_abspath, *tmp_abspath; + svn_error_t *err; + svn_revnum_t copy_src_revnum; + svn_opt_revision_t copy_src_peg_revision; + svn_boolean_t timestamp_sleep; + svn_wc_notify_func2_t old_notify_func2 = ctx->notify_func2; + void *old_notify_baton2 = ctx->notify_baton2; + struct notification_adjust_baton nb; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Find the URL of the incoming added directory in the repository. */ + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, incoming_new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, FALSE, FALSE, + ctx, scratch_pool, + scratch_pool)); + if (corrected_url) + url = corrected_url; + + + /* Find a temporary location in which to check out the copy source. */ + SVN_ERR(svn_wc__get_tmpdir(&tmpdir_abspath, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + SVN_ERR(svn_io_open_unique_file3(NULL, &tmp_abspath, tmpdir_abspath, + svn_io_file_del_on_close, + scratch_pool, scratch_pool)); + + /* Make a new checkout of the requested source. While doing so, + * resolve copy_src_revnum to an actual revision number in case it + * was until now 'invalid' meaning 'head'. Ask this function not to + * sleep for timestamps, by passing a sleep_needed output param. + * Send notifications for all nodes except the root node, and adjust + * them to refer to the destination rather than this temporary path. */ + + nb.inner_func = ctx->notify_func2; + nb.inner_baton = ctx->notify_baton2; + nb.checkout_abspath = tmp_abspath; + nb.final_abspath = local_abspath; + ctx->notify_func2 = notification_adjust_func; + ctx->notify_baton2 = &nb; + + copy_src_peg_revision.kind = svn_opt_revision_number; + copy_src_peg_revision.value.number = incoming_new_pegrev; + + err = svn_client__checkout_internal(©_src_revnum, ×tamp_sleep, + url, tmp_abspath, + ©_src_peg_revision, + ©_src_peg_revision, + svn_depth_infinity, + TRUE, /* we want to ignore externals */ + FALSE, /* we don't allow obstructions */ + ra_session, ctx, scratch_pool); + + ctx->notify_func2 = old_notify_func2; + ctx->notify_baton2 = old_notify_baton2; + + SVN_ERR(err); + + /* ### The following WC modifications should be atomic. */ + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + svn_dirent_dirname( + local_abspath, + scratch_pool), + scratch_pool, scratch_pool)); + + /* Remove the working directory. */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + + /* Schedule dst_path for addition in parent, with copy history. + Don't send any notification here. + Then remove the temporary checkout's .svn dir in preparation for + moving the rest of it into the final destination. */ + err = svn_wc_copy3(ctx->wc_ctx, tmp_abspath, local_abspath, + TRUE /* metadata_only */, + NULL, NULL, /* don't allow user to cancel here */ + NULL, NULL, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_wc__acquire_write_lock(NULL, ctx->wc_ctx, tmp_abspath, + FALSE, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + err = svn_wc_remove_from_revision_control2(ctx->wc_ctx, + tmp_abspath, + FALSE, FALSE, + NULL, NULL, /* don't cancel */ + scratch_pool); + if (err) + goto unlock_wc; + + /* Move the temporary disk tree into place. */ + err = svn_io_file_rename2(tmp_abspath, local_abspath, FALSE, scratch_pool); + if (err) + goto unlock_wc; + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify(local_abspath, + svn_wc_notify_add, + scratch_pool); + notify->kind = svn_node_dir; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + /* Resolve to current working copy state. + * svn_client__merge_locked() requires this. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + if (err) + goto unlock_wc; + + if (merge_dirs) + { + svn_revnum_t base_revision; + const char *base_repos_relpath; + struct find_added_rev_baton b = { 0 }; + + /* Find the URL and revision of the directory we have just replaced. */ + err = svn_wc__node_get_base(NULL, &base_revision, &base_repos_relpath, + NULL, NULL, NULL, ctx->wc_ctx, local_abspath, + FALSE, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + url = svn_path_url_add_component2(repos_root_url, base_repos_relpath, + scratch_pool); + + /* Trace the replaced directory's history to its origin. */ + err = svn_ra_reparent(ra_session, url, scratch_pool); + if (err) + goto unlock_wc; + b.victim_abspath = local_abspath; + b.ctx = ctx; + b.added_rev = SVN_INVALID_REVNUM; + b.repos_relpath = NULL; + b.parent_repos_relpath = svn_relpath_dirname(base_repos_relpath, + scratch_pool); + b.pool = scratch_pool; + + err = svn_ra_get_location_segments(ra_session, "", base_revision, + base_revision, SVN_INVALID_REVNUM, + find_added_rev, &b, + scratch_pool); + if (err) + goto unlock_wc; + + if (b.added_rev == SVN_INVALID_REVNUM) + { + err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Could not determine the revision in " + "which '^/%s' was added to the " + "repository.\n"), + base_repos_relpath); + goto unlock_wc; + } + + /* Merge the replaced directory into the directory which replaced it. + * We do not need to consider a reverse-merge here since the source of + * this merge was part of the merge target working copy, not a branch + * in the repository. */ + err = merge_newly_added_dir(base_repos_relpath, + url, rev_below(b.added_rev), url, + base_revision, local_abspath, FALSE, + ctx, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + } + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + svn_io_sleep_for_timestamps(local_abspath, scratch_pool); + SVN_ERR(err); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify = svn_wc_create_notify( + local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool); + + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_dir_replace(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + return svn_error_trace(merge_incoming_added_dir_replace(option, + conflict, + ctx, + FALSE, + scratch_pool)); +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_merge_incoming_added_dir_replace_and_merge( + svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + return svn_error_trace(merge_incoming_added_dir_replace(option, + conflict, + ctx, + TRUE, + scratch_pool)); +} + +/* Verify the local working copy state matches what we expect when an + * incoming deletion tree conflict exists. + * We assume update/merge/switch operations leave the working copy in a + * state which prefers the local change and cancels the deletion. + * Run a quick sanity check and error out if it looks as if the + * working copy was modified since, even though it's not easy to make + * such modifications without also clearing the conflict marker. */ +static svn_error_t * +verify_local_state_for_incoming_delete(svn_client_conflict_t *conflict, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *local_abspath; + const char *wcroot_abspath; + svn_wc_operation_t operation; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + local_abspath, scratch_pool, + scratch_pool)); + operation = svn_client_conflict_get_operation(conflict); + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + struct conflict_tree_incoming_delete_details *details; + svn_boolean_t is_copy; + svn_revnum_t copyfrom_rev; + const char *copyfrom_repos_relpath; + + details = conflict->tree_conflict_incoming_details; + if (details == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Conflict resolution option '%d' requires " + "details for tree conflict at '%s' to be " + "fetched from the repository."), + option->id, + svn_dirent_local_style(local_abspath, + scratch_pool)); + + /* Ensure that the item is a copy of itself from before it was deleted. + * Update and switch are supposed to set this up when flagging the + * conflict. */ + SVN_ERR(svn_wc__node_get_origin(&is_copy, ©from_rev, + ©from_repos_relpath, + NULL, NULL, NULL, NULL, + ctx->wc_ctx, local_abspath, FALSE, + scratch_pool, scratch_pool)); + if (!is_copy) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected a copied item, but the item " + "is not a copy)"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + conflict->local_abspath), + scratch_pool)); + else if (details->deleted_rev == SVN_INVALID_REVNUM && + details->added_rev == SVN_INVALID_REVNUM) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Could not find the revision in which '%s' " + "was deleted from the repository"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + conflict->local_abspath), + scratch_pool)); + else if (details->deleted_rev != SVN_INVALID_REVNUM && + copyfrom_rev >= details->deleted_rev) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an item copied from a revision " + "smaller than r%ld, but the item was " + "copied from r%ld)"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, conflict->local_abspath), + scratch_pool), + details->deleted_rev, copyfrom_rev); + + else if (details->added_rev != SVN_INVALID_REVNUM && + copyfrom_rev < details->added_rev) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an item copied from a revision " + "larger than r%ld, but the item was " + "copied from r%ld)"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, conflict->local_abspath), + scratch_pool), + details->added_rev, copyfrom_rev); + else if (operation == svn_wc_operation_update) + { + const char *old_repos_relpath; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, NULL, NULL, conflict, + scratch_pool, scratch_pool)); + if (strcmp(copyfrom_repos_relpath, details->repos_relpath) != 0 && + strcmp(copyfrom_repos_relpath, old_repos_relpath) != 0) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an item copied from '^/%s' " + "or from '^/%s' but the item was " + "copied from '^/%s@%ld')"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, conflict->local_abspath), + scratch_pool), + details->repos_relpath, + old_repos_relpath, + copyfrom_repos_relpath, copyfrom_rev); + } + else if (operation == svn_wc_operation_switch) + { + const char *old_repos_relpath; + + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &old_repos_relpath, NULL, NULL, conflict, + scratch_pool, scratch_pool)); + + if (strcmp(copyfrom_repos_relpath, old_repos_relpath) != 0) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected an item copied from '^/%s', " + "but the item was copied from " + "'^/%s@%ld')"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, + conflict->local_abspath), + scratch_pool), + old_repos_relpath, + copyfrom_repos_relpath, copyfrom_rev); + } + } + else if (operation == svn_wc_operation_merge) + { + svn_node_kind_t victim_node_kind; + svn_node_kind_t on_disk_kind; + + /* For merge, all we can do is ensure that the item still exists. */ + victim_node_kind = + svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_io_check_path(local_abspath, &on_disk_kind, scratch_pool)); + + if (victim_node_kind != on_disk_kind) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected node kind '%s' but found '%s')"), + svn_dirent_local_style( + svn_dirent_skip_ancestor( + wcroot_abspath, conflict->local_abspath), + scratch_pool), + svn_node_kind_to_word(victim_node_kind), + svn_node_kind_to_word(on_disk_kind)); + } + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_delete_ignore(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + const char *lock_abspath; + svn_error_t *err; + + option_id = svn_client_conflict_option_get_id(option); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + local_abspath, + scratch_pool, scratch_pool)); + + err = verify_local_state_for_incoming_delete(conflict, option, ctx, + scratch_pool); + if (err) + goto unlock_wc; + + /* Resolve to the current working copy state. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, scratch_pool); + + /* svn_wc__del_tree_conflict doesn't handle notification for us */ + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = option_id; + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_delete_accept(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + const char *parent_abspath; + const char *lock_abspath; + svn_error_t *err; + + option_id = svn_client_conflict_option_get_id(option); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + + /* Deleting a node requires a lock on the node's parent. */ + parent_abspath = svn_dirent_dirname(local_abspath, scratch_pool); + SVN_ERR(svn_wc__acquire_write_lock_for_resolve(&lock_abspath, ctx->wc_ctx, + parent_abspath, + scratch_pool, scratch_pool)); + + err = verify_local_state_for_incoming_delete(conflict, option, ctx, + scratch_pool); + if (err) + goto unlock_wc; + + /* Delete the tree conflict victim. Marks the conflict resolved. */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) + { + /* Not a versioned path. This can happen if the victim has already + * been deleted in our branche's history, for example. Either way, + * the item is gone, which is what we want, so don't treat this as + * a fatal error. */ + svn_error_clear(err); + + /* Resolve to current working copy state. */ + err = svn_wc__del_tree_conflict(ctx->wc_ctx, local_abspath, + scratch_pool); + } + + if (err) + goto unlock_wc; + } + + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool), + scratch_pool); + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + conflict->resolution_tree = option_id; + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_move_file_text_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + svn_wc_operation_t operation; + const char *lock_abspath; + svn_error_t *err; + const char *repos_root_url; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *wc_tmpdir; + const char *ancestor_abspath; + svn_stream_t *ancestor_stream; + apr_hash_t *ancestor_props; + apr_hash_t *victim_props; + apr_hash_t *move_target_props; + const char *ancestor_url; + const char *corrected_url; + svn_ra_session_t *ra_session; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_array_header_t *propdiffs; + struct conflict_tree_incoming_delete_details *details; + apr_array_header_t *possible_moved_to_abspaths; + const char *moved_to_abspath; + const char *incoming_abspath = NULL; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + operation = svn_client_conflict_get_operation(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->moves == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("The specified conflict resolution option " + "requires details for tree conflict at '%s' " + "to be fetched from the repository first."), + svn_dirent_local_style(local_abspath, + scratch_pool)); + if (operation == svn_wc_operation_none) + return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL, + _("Invalid operation code '%d' recorded for " + "conflict at '%s'"), operation, + svn_dirent_local_style(local_abspath, + scratch_pool)); + + option_id = svn_client_conflict_option_get_id(option); + SVN_ERR_ASSERT(option_id == + svn_client_conflict_option_incoming_move_file_text_merge || + option_id == + svn_client_conflict_option_incoming_move_dir_merge); + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + /* Set up temporary storage for the common ancestor version of the file. */ + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stream_open_unique(&ancestor_stream, + &ancestor_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + /* Fetch the ancestor file's content. */ + ancestor_url = svn_path_url_add_component2(repos_root_url, + incoming_old_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + ancestor_url, NULL, NULL, + FALSE, FALSE, ctx, + scratch_pool, scratch_pool)); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, + ancestor_stream, NULL, /* fetched_rev */ + &ancestor_props, scratch_pool)); + filter_props(ancestor_props, scratch_pool); + + /* Close stream to flush ancestor file to disk. */ + SVN_ERR(svn_stream_close(ancestor_stream)); + + possible_moved_to_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, + details->wc_move_target_idx, + const char *); + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve( + &lock_abspath, ctx->wc_ctx, + svn_dirent_get_longest_ancestor(local_abspath, + moved_to_abspath, + scratch_pool), + scratch_pool, scratch_pool)); + + err = verify_local_state_for_incoming_delete(conflict, option, ctx, + scratch_pool); + if (err) + goto unlock_wc; + + /* Get a copy of the conflict victim's properties. */ + err = svn_wc_prop_list2(&victim_props, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + /* Get a copy of the move target's properties. */ + err = svn_wc_prop_list2(&move_target_props, ctx->wc_ctx, + moved_to_abspath, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + /* Create a property diff for the files. */ + err = svn_prop_diffs(&propdiffs, move_target_props, victim_props, + scratch_pool); + if (err) + goto unlock_wc; + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + svn_stream_t *working_stream; + svn_stream_t *incoming_stream; + + /* Create a temporary copy of the working file in repository-normal form. + * Set up this temporary file to be automatically removed. */ + err = svn_stream_open_unique(&incoming_stream, + &incoming_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_wc__translated_stream(&working_stream, ctx->wc_ctx, + local_abspath, local_abspath, + SVN_WC_TRANSLATE_TO_NF, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_stream_copy3(working_stream, incoming_stream, + NULL, NULL, /* no cancellation */ + scratch_pool); + if (err) + goto unlock_wc; + } + else if (operation == svn_wc_operation_merge) + { + svn_stream_t *incoming_stream; + svn_stream_t *move_target_stream; + + /* Set aside the current move target file. This is required to apply + * the move, and only then perform a three-way text merge between + * the ancestor's file, our working file (which we would move to + * the destination), and the file that we have set aside, which + * contains the incoming fulltext. + * Set up this temporary file to NOT be automatically removed. */ + err = svn_stream_open_unique(&incoming_stream, + &incoming_abspath, wc_tmpdir, + svn_io_file_del_none, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_wc__translated_stream(&move_target_stream, ctx->wc_ctx, + moved_to_abspath, moved_to_abspath, + SVN_WC_TRANSLATE_TO_NF, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + err = svn_stream_copy3(move_target_stream, incoming_stream, + NULL, NULL, /* no cancellation */ + scratch_pool); + if (err) + goto unlock_wc; + + /* Apply the incoming move. */ + err = svn_io_remove_file2(moved_to_abspath, FALSE, scratch_pool); + if (err) + goto unlock_wc; + err = svn_wc__move2(ctx->wc_ctx, local_abspath, moved_to_abspath, + FALSE, /* ordinary (not meta-data only) move */ + FALSE, /* mixed-revisions don't apply to files */ + NULL, NULL, /* don't allow user to cancel here */ + NULL, NULL, /* no extra notification */ + scratch_pool); + if (err) + goto unlock_wc; + } + else + SVN_ERR_MALFUNCTION(); + + /* Perform the file merge. */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, ancestor_abspath, + incoming_abspath, moved_to_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + apr_hash_count(ancestor_props) ? ancestor_props : NULL, + propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + svn_io_sleep_for_timestamps(moved_to_abspath, scratch_pool); + if (err) + goto unlock_wc; + + if (operation == svn_wc_operation_merge && incoming_abspath) + { + err = svn_io_remove_file2(incoming_abspath, TRUE, scratch_pool); + if (err) + goto unlock_wc; + incoming_abspath = NULL; + } + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + /* Tell the world about the file merge that just happened. */ + notify = svn_wc_create_notify(moved_to_abspath, + svn_wc_notify_update_update, + scratch_pool); + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + /* Delete the tree conflict victim (clears the tree conflict marker). */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + NULL, NULL, /* no extra notification */ + scratch_pool); + if (err) + goto unlock_wc; + } + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = option_id; + +unlock_wc: + if (err && operation == svn_wc_operation_merge && incoming_abspath) + err = svn_error_quick_wrapf( + err, _("If needed, a backup copy of '%s' can be found at '%s'"), + svn_dirent_local_style(moved_to_abspath, scratch_pool), + svn_dirent_local_style(incoming_abspath, scratch_pool)); + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_incoming_move_dir_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_option_id_t option_id; + const char *local_abspath; + svn_wc_operation_t operation; + const char *lock_abspath; + svn_error_t *err; + const char *repos_root_url; + const char *repos_uuid; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *victim_repos_relpath; + svn_revnum_t victim_peg_rev; + const char *moved_to_repos_relpath; + svn_revnum_t moved_to_peg_rev; + struct conflict_tree_incoming_delete_details *details; + apr_array_header_t *possible_moved_to_abspaths; + const char *moved_to_abspath; + svn_client__pathrev_t *yca_loc; + svn_opt_revision_t yca_opt_rev; + svn_client__conflict_report_t *conflict_report; + svn_boolean_t is_copy; + svn_boolean_t is_modified; + + local_abspath = svn_client_conflict_get_local_abspath(conflict); + operation = svn_client_conflict_get_operation(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->moves == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("The specified conflict resolution option " + "requires details for tree conflict at '%s' " + "to be fetched from the repository first."), + svn_dirent_local_style(local_abspath, + scratch_pool)); + + option_id = svn_client_conflict_option_get_id(option); + SVN_ERR_ASSERT(option_id == + svn_client_conflict_option_incoming_move_dir_merge); + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, &repos_uuid, + conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + /* Get repository location of the moved-away node (the conflict victim). */ + if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + victim_repos_relpath = incoming_old_repos_relpath; + victim_peg_rev = incoming_old_pegrev; + } + else if (operation == svn_wc_operation_merge) + SVN_ERR(svn_wc__node_get_repos_info(&victim_peg_rev, &victim_repos_relpath, + NULL, NULL, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + + /* Get repository location of the moved-here node (incoming move). */ + possible_moved_to_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + moved_to_abspath = APR_ARRAY_IDX(possible_moved_to_abspaths, + details->wc_move_target_idx, + const char *); + + /* ### The following WC modifications should be atomic. */ + + SVN_ERR(svn_wc__acquire_write_lock_for_resolve( + &lock_abspath, ctx->wc_ctx, + svn_dirent_get_longest_ancestor(local_abspath, + moved_to_abspath, + scratch_pool), + scratch_pool, scratch_pool)); + + err = svn_wc__node_get_origin(&is_copy, &moved_to_peg_rev, + &moved_to_repos_relpath, + NULL, NULL, NULL, NULL, + ctx->wc_ctx, moved_to_abspath, FALSE, + scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + if (!is_copy && operation == svn_wc_operation_merge) + { + err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(expected a copied item at '%s', but the " + "item is not a copy)"), + svn_dirent_local_style(local_abspath, + scratch_pool), + svn_dirent_local_style(moved_to_abspath, + scratch_pool)); + goto unlock_wc; + } + + if (moved_to_repos_relpath == NULL || moved_to_peg_rev == SVN_INVALID_REVNUM) + { + err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(could not determine origin of '%s')"), + svn_dirent_local_style(local_abspath, + scratch_pool), + svn_dirent_local_style(moved_to_abspath, + scratch_pool)); + goto unlock_wc; + } + + /* Now find the youngest common ancestor of these nodes. */ + err = find_yca(&yca_loc, victim_repos_relpath, victim_peg_rev, + moved_to_repos_relpath, moved_to_peg_rev, + repos_root_url, repos_uuid, + NULL, ctx, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + + if (yca_loc == NULL) + { + err = svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Cannot resolve tree conflict on '%s' " + "(could not find common ancestor of '^/%s@%ld' " + " and '^/%s@%ld')"), + svn_dirent_local_style(local_abspath, + scratch_pool), + victim_repos_relpath, victim_peg_rev, + moved_to_repos_relpath, moved_to_peg_rev); + goto unlock_wc; + } + + yca_opt_rev.kind = svn_opt_revision_number; + yca_opt_rev.value.number = yca_loc->rev; + + err = verify_local_state_for_incoming_delete(conflict, option, ctx, + scratch_pool); + if (err) + goto unlock_wc; + + if (operation == svn_wc_operation_merge) + { + const char *move_target_url; + svn_opt_revision_t incoming_new_opt_rev; + + /* Revert the incoming move target directory. */ + SVN_ERR(svn_wc_revert5(ctx->wc_ctx, moved_to_abspath, svn_depth_infinity, + FALSE, NULL, TRUE, FALSE, + NULL, NULL, /* no cancellation */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool)); + + /* The move operation is not part of natural history. We must replicate + * this move in our history. Record a move in the working copy. */ + err = svn_wc__move2(ctx->wc_ctx, local_abspath, moved_to_abspath, + FALSE, /* this is not a meta-data only move */ + TRUE, /* allow mixed-revisions just in case */ + NULL, NULL, /* don't allow user to cancel here */ + ctx->notify_func2, ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + + /* Merge YCA_URL@YCA_REV->MOVE_TARGET_URL@MERGE_RIGHT into move target. */ + move_target_url = apr_pstrcat(scratch_pool, repos_root_url, "/", + get_moved_to_repos_relpath(details, + scratch_pool), + SVN_VA_NULL); + incoming_new_opt_rev.kind = svn_opt_revision_number; + incoming_new_opt_rev.value.number = incoming_new_pegrev; + err = svn_client__merge_locked(&conflict_report, + yca_loc->url, &yca_opt_rev, + move_target_url, &incoming_new_opt_rev, + moved_to_abspath, svn_depth_infinity, + TRUE, TRUE, /* do a no-ancestry merge */ + FALSE, FALSE, FALSE, + TRUE, /* Allow mixed-rev just in case, + * since conflict victims can't be + * updated to straighten out + * mixed-rev trees. */ + NULL, ctx, scratch_pool, scratch_pool); + if (err) + goto unlock_wc; + } + else + { + SVN_ERR_ASSERT(operation == svn_wc_operation_update || + operation == svn_wc_operation_switch); + + /* Merge local modifications into the incoming move target dir. */ + err = svn_wc__has_local_mods(&is_modified, ctx->wc_ctx, local_abspath, + TRUE, ctx->cancel_func, ctx->cancel_baton, + scratch_pool); + if (err) + goto unlock_wc; + + if (is_modified) + { + err = svn_wc__conflict_tree_update_incoming_move(ctx->wc_ctx, + local_abspath, + moved_to_abspath, + ctx->cancel_func, + ctx->cancel_baton, + ctx->notify_func2, + ctx->notify_baton2, + scratch_pool); + if (err) + goto unlock_wc; + } + + /* The move operation is part of our natural history. + * Delete the tree conflict victim (clears the tree conflict marker). */ + err = svn_wc_delete4(ctx->wc_ctx, local_abspath, FALSE, FALSE, + NULL, NULL, /* don't allow user to cancel here */ + NULL, NULL, /* no extra notification */ + scratch_pool); + if (err) + goto unlock_wc; + } + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify(local_abspath, svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = option_id; + +unlock_wc: + err = svn_error_compose_create(err, svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + SVN_ERR(err); + + return SVN_NO_ERROR; +} + +/* Implements conflict_option_resolve_func_t. */ +static svn_error_t * +resolve_local_move_file_merge(svn_client_conflict_option_t *option, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + const char *lock_abspath; + svn_error_t *err; + const char *repos_root_url; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + const char *wc_tmpdir; + const char *ancestor_tmp_abspath; + const char *incoming_tmp_abspath; + apr_hash_t *ancestor_props; + apr_hash_t *incoming_props; + svn_stream_t *stream; + const char *url; + const char *corrected_url; + const char *old_session_url; + svn_ra_session_t *ra_session; + svn_wc_merge_outcome_t merge_content_outcome; + svn_wc_notify_state_t merge_props_outcome; + apr_array_header_t *propdiffs; + struct conflict_tree_local_missing_details *details; + + details = conflict->tree_conflict_local_details; + + SVN_ERR(svn_client_conflict_get_repos_info(&repos_root_url, NULL, + conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + SVN_ERR(svn_wc__get_tmpdir(&wc_tmpdir, ctx->wc_ctx, + details->moved_to_abspath, + scratch_pool, scratch_pool)); + + /* Fetch the common ancestor file's content. */ + SVN_ERR(svn_stream_open_unique(&stream, &ancestor_tmp_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, + incoming_old_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__open_ra_session_internal(&ra_session, &corrected_url, + url, NULL, NULL, + FALSE, FALSE, ctx, + scratch_pool, scratch_pool)); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_old_pegrev, stream, NULL, + &ancestor_props, scratch_pool)); + filter_props(ancestor_props, scratch_pool); + + /* Close stream to flush the file to disk. */ + SVN_ERR(svn_stream_close(stream)); + + /* Do the same for the incoming file's content. */ + SVN_ERR(svn_stream_open_unique(&stream, &incoming_tmp_abspath, wc_tmpdir, + svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + url = svn_path_url_add_component2(repos_root_url, + incoming_new_repos_relpath, + scratch_pool); + SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session, + url, scratch_pool)); + SVN_ERR(svn_ra_get_file(ra_session, "", incoming_new_pegrev, stream, NULL, + &incoming_props, scratch_pool)); + /* Close stream to flush the file to disk. */ + SVN_ERR(svn_stream_close(stream)); + + filter_props(incoming_props, scratch_pool); + + /* Create a property diff for the files. */ + SVN_ERR(svn_prop_diffs(&propdiffs, incoming_props, ancestor_props, + scratch_pool)); + + /* ### The following WC modifications should be atomic. */ + SVN_ERR(svn_wc__acquire_write_lock_for_resolve( + &lock_abspath, ctx->wc_ctx, + svn_dirent_get_longest_ancestor(conflict->local_abspath, + details->moved_to_abspath, + scratch_pool), + scratch_pool, scratch_pool)); + + /* Perform the file merge. */ + err = svn_wc_merge5(&merge_content_outcome, &merge_props_outcome, + ctx->wc_ctx, + ancestor_tmp_abspath, incoming_tmp_abspath, + details->moved_to_abspath, + NULL, NULL, NULL, /* labels */ + NULL, NULL, /* conflict versions */ + FALSE, /* dry run */ + NULL, NULL, /* diff3_cmd, merge_options */ + apr_hash_count(ancestor_props) ? ancestor_props : NULL, + propdiffs, + NULL, NULL, /* conflict func/baton */ + NULL, NULL, /* don't allow user to cancel here */ + scratch_pool); + svn_io_sleep_for_timestamps(details->moved_to_abspath, scratch_pool); + if (err) + return svn_error_compose_create(err, + svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + + err = svn_wc__del_tree_conflict(ctx->wc_ctx, conflict->local_abspath, + scratch_pool); + err = svn_error_compose_create(err, + svn_wc__release_write_lock(ctx->wc_ctx, + lock_abspath, + scratch_pool)); + if (err) + return svn_error_trace(err); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + /* Tell the world about the file merge that just happened. */ + notify = svn_wc_create_notify(details->moved_to_abspath, + svn_wc_notify_update_update, + scratch_pool); + if (merge_content_outcome == svn_wc_merge_conflict) + notify->content_state = svn_wc_notify_state_conflicted; + else + notify->content_state = svn_wc_notify_state_merged; + notify->prop_state = merge_props_outcome; + notify->kind = svn_node_file; + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + + /* And also about the successfully resolved tree conflict. */ + notify = svn_wc_create_notify(conflict->local_abspath, + svn_wc_notify_resolved_tree, + scratch_pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); + } + + conflict->resolution_tree = svn_client_conflict_option_get_id(option); + + return SVN_NO_ERROR; +} + +static svn_error_t * +assert_text_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) +{ + svn_boolean_t text_conflicted; + + SVN_ERR(svn_client_conflict_get_conflicted(&text_conflicted, NULL, NULL, + conflict, scratch_pool, + scratch_pool)); + + SVN_ERR_ASSERT(text_conflicted); /* ### return proper error? */ + + return SVN_NO_ERROR; +} + +static svn_error_t * +assert_prop_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) +{ + apr_array_header_t *props_conflicted; + + SVN_ERR(svn_client_conflict_get_conflicted(NULL, &props_conflicted, NULL, + conflict, scratch_pool, + scratch_pool)); + + /* ### return proper error? */ + SVN_ERR_ASSERT(props_conflicted && props_conflicted->nelts > 0); + + return SVN_NO_ERROR; +} + +static svn_error_t * +assert_tree_conflict(svn_client_conflict_t *conflict, apr_pool_t *scratch_pool) +{ + svn_boolean_t tree_conflicted; + + SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted, + conflict, scratch_pool, + scratch_pool)); + + SVN_ERR_ASSERT(tree_conflicted); /* ### return proper error? */ + + return SVN_NO_ERROR; +} + +/* Helper to add to conflict resolution option to array of OPTIONS. + * Resolution option object will be allocated from OPTIONS->POOL + * and DESCRIPTION will be copied to this pool. + * Returns pointer to the created conflict resolution option. */ +static svn_client_conflict_option_t * +add_resolution_option(apr_array_header_t *options, + svn_client_conflict_t *conflict, + svn_client_conflict_option_id_t id, + const char *label, + const char *description, + conflict_option_resolve_func_t resolve_func) +{ + svn_client_conflict_option_t *option; + + option = apr_pcalloc(options->pool, sizeof(*option)); + option->pool = options->pool; + option->id = id; + option->label = apr_pstrdup(option->pool, label); + option->description = apr_pstrdup(option->pool, description); + option->conflict = conflict; + option->do_resolve_func = resolve_func; + + APR_ARRAY_PUSH(options, const svn_client_conflict_option_t *) = option; + + return option; +} + +svn_error_t * +svn_client_conflict_text_get_resolution_options(apr_array_header_t **options, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *mime_type; + + SVN_ERR(assert_text_conflict(conflict, scratch_pool)); + + *options = apr_array_make(result_pool, 7, + sizeof(svn_client_conflict_option_t *)); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_postpone, + _("Postpone"), + _("skip this conflict and leave it unresolved"), + resolve_postpone); + + mime_type = svn_client_conflict_text_get_mime_type(conflict); + if (mime_type && svn_mime_type_is_binary(mime_type)) + { + /* Resolver options for a binary file conflict. */ + add_resolution_option(*options, conflict, + svn_client_conflict_option_base_text, + _("Accept base"), + _("discard local and incoming changes for this binary file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text, + _("Accept incoming"), + _("accept incoming version of binary file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text, + _("Mark as resolved"), + _("accept binary file as it appears in the working copy"), + resolve_text_conflict); + } + else + { + /* Resolver options for a text file conflict. */ + add_resolution_option(*options, conflict, + svn_client_conflict_option_base_text, + _("Accept base"), + _("discard local and incoming changes for this file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text, + _("Accept incoming"), + _("accept incoming version of entire file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text, + _("Reject incoming"), + _("reject all incoming changes for this file"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text_where_conflicted, + _("Accept incoming for conflicts"), + _("accept changes only where they conflict"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text_where_conflicted, + _("Reject conflicts"), + _("reject changes which conflict and accept the rest"), + resolve_text_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_merged_text, + _("Mark as resolved"), + _("accept the file as it appears in the working copy"), + resolve_text_conflict); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_prop_get_resolution_options(apr_array_header_t **options, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_prop_conflict(conflict, scratch_pool)); + + *options = apr_array_make(result_pool, 7, + sizeof(svn_client_conflict_option_t *)); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_postpone, + _("Postpone"), + _("skip this conflict and leave it unresolved"), + resolve_postpone); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_base_text, + _("Accept base"), + _("discard local and incoming changes for this property"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text, + _("Accept incoming"), + _("accept incoming version of entire property value"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text, + _("Mark as resolved"), + _("accept working copy version of entire property value"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_incoming_text_where_conflicted, + _("Accept incoming for conflicts"), + _("accept incoming changes only where they conflict"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_working_text_where_conflicted, + _("Reject conflicts"), + _("reject changes which conflict and accept the rest"), + resolve_prop_conflict); + + add_resolution_option(*options, conflict, + svn_client_conflict_option_merged_text, + _("Accept merged"), + _("accept merged version of property value"), + resolve_prop_conflict); + + return SVN_NO_ERROR; +} + +/* Configure 'accept current wc state' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_accept_current_wc_state(svn_client_conflict_t *conflict, + apr_array_header_t *options) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + conflict_option_resolve_func_t do_resolve_func; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + + if ((operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) && + (local_change == svn_wc_conflict_reason_moved_away || + local_change == svn_wc_conflict_reason_deleted || + local_change == svn_wc_conflict_reason_replaced) && + incoming_change == svn_wc_conflict_action_edit) + { + /* We must break moves if the user accepts the current working copy + * state instead of updating a moved-away node or updating children + * moved outside of deleted or replaced directory nodes. + * Else such moves would be left in an invalid state. */ + do_resolve_func = resolve_update_break_moved_away; + } + else + do_resolve_func = resolve_accept_current_wc_state; + + add_resolution_option(options, conflict, + svn_client_conflict_option_accept_current_wc_state, + _("Mark as resolved"), + _("accept current working copy state"), + do_resolve_func); + + return SVN_NO_ERROR; +} + +/* Configure 'update move destination' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_update_move_destination(svn_client_conflict_t *conflict, + apr_array_header_t *options) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + + if ((operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) && + incoming_change == svn_wc_conflict_action_edit && + local_change == svn_wc_conflict_reason_moved_away) + { + add_resolution_option( + options, conflict, + svn_client_conflict_option_update_move_destination, + _("Update move destination"), + _("apply incoming changes to move destination"), + resolve_update_moved_away_node); + } + + return SVN_NO_ERROR; +} + +/* Configure 'update raise moved away children' resolution option for a tree + * conflict. */ +static svn_error_t * +configure_option_update_raise_moved_away_children( + svn_client_conflict_t *conflict, + apr_array_header_t *options) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + + if ((operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) && + incoming_change == svn_wc_conflict_action_edit && + (local_change == svn_wc_conflict_reason_deleted || + local_change == svn_wc_conflict_reason_replaced) && + victim_node_kind == svn_node_dir) + { + add_resolution_option( + options, conflict, + svn_client_conflict_option_update_any_moved_away_children, + _("Update any moved-away children"), + _("prepare for updating moved-away children, if any"), + resolve_update_raise_moved_away); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming add ignore' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_incoming_add_ignore(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t victim_node_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + /* This option is only available for directories. */ + if (victim_node_kind == svn_node_dir && + incoming_change == svn_wc_conflict_action_add && + (local_change == svn_wc_conflict_reason_obstructed || + local_change == svn_wc_conflict_reason_added)) + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + if (operation == svn_wc_operation_merge) + description = + apr_psprintf(scratch_pool, + _("ignore and do not add '^/%s@%ld' here"), + incoming_new_repos_relpath, incoming_new_pegrev); + else if (operation == svn_wc_operation_update || + operation == svn_wc_operation_switch) + { + if (victim_node_kind == svn_node_file) + description = + apr_psprintf(scratch_pool, + _("replace '^/%s@%ld' with the locally added file"), + incoming_new_repos_relpath, incoming_new_pegrev); + else if (victim_node_kind == svn_node_dir) + description = + apr_psprintf(scratch_pool, + _("replace '^/%s@%ld' with the locally added " + "directory"), + incoming_new_repos_relpath, incoming_new_pegrev); + else + description = + apr_psprintf(scratch_pool, + _("replace '^/%s@%ld' with the locally added item"), + incoming_new_repos_relpath, incoming_new_pegrev); + } + else + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("unexpected operation code '%d'"), + operation); + add_resolution_option( + options, conflict, svn_client_conflict_option_incoming_add_ignore, + _("Ignore incoming addition"), description, resolve_incoming_add_ignore); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added file text merge' resolution option for a tree + * conflict. */ +static svn_error_t * +configure_option_incoming_added_file_text_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (victim_node_kind == svn_node_file && + incoming_new_kind == svn_node_file && + incoming_change == svn_wc_conflict_action_add && + (local_change == svn_wc_conflict_reason_obstructed || + local_change == svn_wc_conflict_reason_added)) + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge) + description = + apr_psprintf(scratch_pool, _("merge '^/%s@%ld' into '%s'"), + incoming_new_repos_relpath, incoming_new_pegrev, + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool)); + else + description = + apr_psprintf(scratch_pool, _("merge local '%s' and '^/%s@%ld'"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_added_file_text_merge, + _("Merge the files"), description, + operation == svn_wc_operation_merge + ? resolve_merge_incoming_added_file_text_merge + : resolve_merge_incoming_added_file_text_update); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added file replace and merge' resolution option for a + * tree conflict. */ +static svn_error_t * +configure_option_incoming_added_file_replace_and_merge( + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge && + victim_node_kind == svn_node_file && + incoming_new_kind == svn_node_file && + incoming_change == svn_wc_conflict_action_add && + local_change == svn_wc_conflict_reason_obstructed) + { + const char *wcroot_abspath; + const char *description; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + description = + apr_psprintf(scratch_pool, + _("delete '%s', copy '^/%s@%ld' here, and merge the files"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_added_file_replace_and_merge, + _("Replace and merge"), + description, resolve_merge_incoming_added_file_replace_and_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added dir merge' resolution option for a tree + * conflict. */ +static svn_error_t * +configure_option_incoming_added_dir_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (victim_node_kind == svn_node_dir && + incoming_new_kind == svn_node_dir && + incoming_change == svn_wc_conflict_action_add && + (local_change == svn_wc_conflict_reason_added || + (operation == svn_wc_operation_merge && + local_change == svn_wc_conflict_reason_obstructed))) + + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + if (operation == svn_wc_operation_merge) + description = + apr_psprintf(scratch_pool, _("merge '^/%s@%ld' into '%s'"), + incoming_new_repos_relpath, incoming_new_pegrev, + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool)); + else + description = + apr_psprintf(scratch_pool, _("merge local '%s' and '^/%s@%ld'"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option(options, conflict, + svn_client_conflict_option_incoming_added_dir_merge, + _("Merge the directories"), description, + operation == svn_wc_operation_merge + ? resolve_merge_incoming_added_dir_merge + : resolve_update_incoming_added_dir_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added dir replace' resolution option for a tree + * conflict. */ +static svn_error_t * +configure_option_incoming_added_dir_replace(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge && + victim_node_kind == svn_node_dir && + incoming_new_kind == svn_node_dir && + incoming_change == svn_wc_conflict_action_add && + local_change == svn_wc_conflict_reason_obstructed) + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + description = + apr_psprintf(scratch_pool, _("delete '%s' and copy '^/%s@%ld' here"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_added_dir_replace, + _("Delete my directory and replace it with incoming directory"), + description, resolve_merge_incoming_added_dir_replace); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming added dir replace and merge' resolution option + * for a tree conflict. */ +static svn_error_t * +configure_option_incoming_added_dir_replace_and_merge( + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + svn_node_kind_t victim_node_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge && + victim_node_kind == svn_node_dir && + incoming_new_kind == svn_node_dir && + incoming_change == svn_wc_conflict_action_add && + local_change == svn_wc_conflict_reason_obstructed) + { + const char *description; + const char *wcroot_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + description = + apr_psprintf(scratch_pool, + _("delete '%s', copy '^/%s@%ld' here, and merge the directories"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + conflict->local_abspath), + scratch_pool), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_added_dir_replace_and_merge, + _("Replace and merge"), + description, resolve_merge_incoming_added_dir_replace_and_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming delete ignore' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_incoming_delete_ignore(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + if (incoming_change == svn_wc_conflict_action_delete) + { + const char *description; + struct conflict_tree_incoming_delete_details *incoming_details; + svn_boolean_t is_incoming_move; + + incoming_details = conflict->tree_conflict_incoming_details; + is_incoming_move = (incoming_details != NULL && + incoming_details->moves != NULL); + if (local_change == svn_wc_conflict_reason_moved_away || + local_change == svn_wc_conflict_reason_edited) + { + /* An option which ignores the incoming deletion makes no sense + * if we know there was a local move and/or an incoming move. */ + if (is_incoming_move) + return SVN_NO_ERROR; + } + else if (local_change == svn_wc_conflict_reason_deleted) + { + /* If the local item was deleted and conflict details were fetched + * and indicate that there was no move, then this is an actual + * 'delete vs delete' situation. An option which ignores the incoming + * deletion makes no sense in that case because there is no local + * node to preserve. */ + if (!is_incoming_move) + return SVN_NO_ERROR; + } + else if (local_change == svn_wc_conflict_reason_missing && + operation == svn_wc_operation_merge) + { + struct conflict_tree_local_missing_details *local_details; + svn_boolean_t is_local_move; /* "local" to branch history */ + + local_details = conflict->tree_conflict_local_details; + is_local_move = (local_details != NULL && + local_details->moves != NULL); + + if (!is_incoming_move && !is_local_move) + return SVN_NO_ERROR; + } + + description = + apr_psprintf(scratch_pool, _("ignore the deletion of '^/%s@%ld'"), + incoming_new_repos_relpath, incoming_new_pegrev); + + add_resolution_option(options, conflict, + svn_client_conflict_option_incoming_delete_ignore, + _("Ignore incoming deletion"), description, + resolve_incoming_delete_ignore); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming delete accept' resolution option for a tree conflict. */ +static svn_error_t * +configure_option_incoming_delete_accept(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + if (incoming_change == svn_wc_conflict_action_delete) + { + struct conflict_tree_incoming_delete_details *incoming_details; + svn_boolean_t is_incoming_move; + + incoming_details = conflict->tree_conflict_incoming_details; + is_incoming_move = (incoming_details != NULL && + incoming_details->moves != NULL); + if (is_incoming_move && + (local_change == svn_wc_conflict_reason_edited || + local_change == svn_wc_conflict_reason_moved_away)) + { + /* An option which accepts the incoming deletion makes no sense + * if we know there was a local move and/or an incoming move. */ + return SVN_NO_ERROR; + } + else + { + const char *description; + const char *wcroot_abspath; + const char *local_abspath; + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, scratch_pool, + scratch_pool)); + local_abspath = svn_client_conflict_get_local_abspath(conflict); + description = + apr_psprintf(scratch_pool, _("accept the deletion of '%s'"), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + local_abspath), + scratch_pool)); + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_delete_accept, + _("Accept incoming deletion"), description, + resolve_incoming_delete_accept); + } + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +describe_incoming_move_merge_conflict_option( + const char **description, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + struct conflict_tree_incoming_delete_details *details, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *move_target_wc_abspaths; + svn_wc_operation_t operation; + const char *victim_abspath; + const char *moved_to_abspath; + const char *wcroot_abspath; + + move_target_wc_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, + details->wc_move_target_idx, + const char *); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + victim_abspath, scratch_pool, + scratch_pool)); + + operation = svn_client_conflict_get_operation(conflict); + if (operation == svn_wc_operation_merge) + *description = + apr_psprintf( + result_pool, _("move '%s' to '%s' and merge"), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + victim_abspath), + scratch_pool), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + moved_to_abspath), + scratch_pool)); + else + *description = + apr_psprintf( + result_pool, _("move and merge local changes from '%s' into '%s'"), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + victim_abspath), + scratch_pool), + svn_dirent_local_style(svn_dirent_skip_ancestor(wcroot_abspath, + moved_to_abspath), + scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Configure 'incoming move file merge' resolution option for + * a tree conflict. */ +static svn_error_t * +configure_option_incoming_move_file_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_node_kind_t victim_node_kind; + svn_wc_conflict_action_t incoming_change; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + svn_node_kind_t incoming_old_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + incoming_change = svn_client_conflict_get_incoming_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + &incoming_old_kind, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (victim_node_kind == svn_node_file && + incoming_old_kind == svn_node_file && + incoming_new_kind == svn_node_none && + incoming_change == svn_wc_conflict_action_delete) + { + struct conflict_tree_incoming_delete_details *details; + const char *description; + + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->moves == NULL) + return SVN_NO_ERROR; + + if (apr_hash_count(details->wc_move_targets) == 0) + return SVN_NO_ERROR; + + SVN_ERR(describe_incoming_move_merge_conflict_option(&description, + conflict, ctx, + details, + scratch_pool, + scratch_pool)); + add_resolution_option( + options, conflict, + svn_client_conflict_option_incoming_move_file_text_merge, + _("Move and merge"), description, + resolve_incoming_move_file_text_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'incoming move dir merge' resolution option for + * a tree conflict. */ +static svn_error_t * +configure_option_incoming_dir_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_node_kind_t victim_node_kind; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_old_repos_relpath; + svn_revnum_t incoming_old_pegrev; + svn_node_kind_t incoming_old_kind; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + svn_node_kind_t incoming_new_kind; + + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + victim_node_kind = svn_client_conflict_tree_get_victim_node_kind(conflict); + SVN_ERR(svn_client_conflict_get_incoming_old_repos_location( + &incoming_old_repos_relpath, &incoming_old_pegrev, + &incoming_old_kind, conflict, scratch_pool, + scratch_pool)); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + &incoming_new_kind, conflict, scratch_pool, + scratch_pool)); + + if (victim_node_kind == svn_node_dir && + incoming_old_kind == svn_node_dir && + incoming_new_kind == svn_node_none && + incoming_change == svn_wc_conflict_action_delete && + local_change == svn_wc_conflict_reason_edited) + { + struct conflict_tree_incoming_delete_details *details; + const char *description; + + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->moves == NULL) + return SVN_NO_ERROR; + + if (apr_hash_count(details->wc_move_targets) == 0) + return SVN_NO_ERROR; + + SVN_ERR(describe_incoming_move_merge_conflict_option(&description, + conflict, ctx, + details, + scratch_pool, + scratch_pool)); + add_resolution_option(options, conflict, + svn_client_conflict_option_incoming_move_dir_merge, + _("Move and merge"), description, + resolve_incoming_move_dir_merge); + } + + return SVN_NO_ERROR; +} + +/* Configure 'local move file merge' resolution option for + * a tree conflict. */ +static svn_error_t * +configure_option_local_move_file_merge(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_array_header_t *options, + apr_pool_t *scratch_pool) +{ + svn_wc_operation_t operation; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + const char *incoming_new_repos_relpath; + svn_revnum_t incoming_new_pegrev; + + operation = svn_client_conflict_get_operation(conflict); + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + SVN_ERR(svn_client_conflict_get_incoming_new_repos_location( + &incoming_new_repos_relpath, &incoming_new_pegrev, + NULL, conflict, scratch_pool, + scratch_pool)); + + if (operation == svn_wc_operation_merge && + incoming_change == svn_wc_conflict_action_edit && + local_change == svn_wc_conflict_reason_missing) + { + struct conflict_tree_local_missing_details *details; + + details = conflict->tree_conflict_local_details; + if (details != NULL && details->moves != NULL) + { + apr_hash_t *wc_move_targets = apr_hash_make(scratch_pool); + apr_pool_t *iterpool; + int i; + + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < details->moves->nelts; i++) + { + struct repos_move_info *move; + + svn_pool_clear(iterpool); + move = APR_ARRAY_IDX(details->moves, i, struct repos_move_info *); + SVN_ERR(follow_move_chains(wc_move_targets, move, ctx, + conflict->local_abspath, + svn_node_file, + incoming_new_repos_relpath, + incoming_new_pegrev, + scratch_pool, iterpool)); + } + svn_pool_destroy(iterpool); + + if (apr_hash_count(wc_move_targets) > 0) + { + apr_array_header_t *move_target_repos_relpaths; + const svn_sort__item_t *item; + apr_array_header_t *moved_to_abspaths; + const char *description; + const char *wcroot_abspath; + + /* Initialize to the first possible move target. Hopefully, + * in most cases there will only be one candidate anyway. */ + move_target_repos_relpaths = svn_sort__hash( + wc_move_targets, + svn_sort_compare_items_as_paths, + scratch_pool); + item = &APR_ARRAY_IDX(move_target_repos_relpaths, + 0, svn_sort__item_t); + moved_to_abspaths = item->value; + details->moved_to_abspath = + apr_pstrdup(conflict->pool, + APR_ARRAY_IDX(moved_to_abspaths, 0, const char *)); + + SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, + conflict->local_abspath, + scratch_pool, scratch_pool)); + description = + apr_psprintf( + scratch_pool, _("apply changes to move destination '%s'"), + svn_dirent_local_style( + svn_dirent_skip_ancestor(wcroot_abspath, + details->moved_to_abspath), + scratch_pool)); + + add_resolution_option( + options, conflict, + svn_client_conflict_option_local_move_file_text_merge, + _("Apply to move destination"), + description, resolve_local_move_file_merge); + } + else + details->moved_to_abspath = NULL; + } + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_option_get_moved_to_repos_relpath_candidates( + apr_array_header_t **possible_moved_to_repos_relpaths, + svn_client_conflict_option_t *option, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_t *conflict = option->conflict; + struct conflict_tree_incoming_delete_details *details; + const char *victim_abspath; + apr_array_header_t *sorted_repos_relpaths; + int i; + + SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_file_text_merge || + svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_dir_merge); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->wc_move_targets == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Getting a list of possible move targets " + "requires details for tree conflict at '%s' " + "to be fetched from the repository first"), + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + /* Return a copy of the repos replath candidate list. */ + sorted_repos_relpaths = svn_sort__hash(details->wc_move_targets, + svn_sort_compare_items_as_paths, + scratch_pool); + + *possible_moved_to_repos_relpaths = apr_array_make( + result_pool, + sorted_repos_relpaths->nelts, + sizeof (const char *)); + for (i = 0; i < sorted_repos_relpaths->nelts; i++) + { + svn_sort__item_t item; + const char *repos_relpath; + + item = APR_ARRAY_IDX(sorted_repos_relpaths, i, svn_sort__item_t); + repos_relpath = item.key; + APR_ARRAY_PUSH(*possible_moved_to_repos_relpaths, const char *) = + apr_pstrdup(result_pool, repos_relpath); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_option_set_moved_to_repos_relpath( + svn_client_conflict_option_t *option, + int preferred_move_target_idx, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_t *conflict = option->conflict; + struct conflict_tree_incoming_delete_details *details; + const char *victim_abspath; + apr_array_header_t *move_target_repos_relpaths; + svn_sort__item_t item; + const char *move_target_repos_relpath; + apr_hash_index_t *hi; + + SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_file_text_merge || + svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_dir_merge); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->wc_move_targets == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Setting a move target requires details " + "for tree conflict at '%s' to be fetched " + "from the repository first"), + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + if (preferred_move_target_idx < 0 || + preferred_move_target_idx >= apr_hash_count(details->wc_move_targets)) + return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Index '%d' is out of bounds of the possible " + "move target list for '%s'"), + preferred_move_target_idx, + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + /* Translate the index back into a hash table key. */ + move_target_repos_relpaths = + svn_sort__hash(details->wc_move_targets, + svn_sort_compare_items_as_paths, + scratch_pool); + item = APR_ARRAY_IDX(move_target_repos_relpaths, preferred_move_target_idx, + svn_sort__item_t); + move_target_repos_relpath = item.key; + /* Find our copy of the hash key and remember the user's preference. */ + for (hi = apr_hash_first(scratch_pool, details->wc_move_targets); + hi != NULL; + hi = apr_hash_next(hi)) + { + const char *repos_relpath = apr_hash_this_key(hi); + + if (strcmp(move_target_repos_relpath, repos_relpath) == 0) + { + details->move_target_repos_relpath = repos_relpath; + /* Update option description. */ + SVN_ERR(describe_incoming_move_merge_conflict_option( + &option->description, + conflict, ctx, + details, + conflict->pool, + scratch_pool)); + + return SVN_NO_ERROR; + } + } + + return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Repository path '%s' not found in list of " + "possible move targets for '%s'"), + move_target_repos_relpath, + svn_dirent_local_style(victim_abspath, + scratch_pool)); +} + +svn_error_t * +svn_client_conflict_option_get_moved_to_abspath_candidates( + apr_array_header_t **possible_moved_to_abspaths, + svn_client_conflict_option_t *option, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_t *conflict = option->conflict; + struct conflict_tree_incoming_delete_details *details; + const char *victim_abspath; + apr_array_header_t *move_target_wc_abspaths; + int i; + + SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_file_text_merge || + svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_dir_merge); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->wc_move_targets == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Getting a list of possible move targets " + "requires details for tree conflict at '%s' " + "to be fetched from the repository first"), + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + move_target_wc_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + + /* Return a copy of the option's move target candidate list. */ + *possible_moved_to_abspaths = + apr_array_make(result_pool, move_target_wc_abspaths->nelts, + sizeof (const char *)); + for (i = 0; i < move_target_wc_abspaths->nelts; i++) + { + const char *moved_to_abspath; + + moved_to_abspath = APR_ARRAY_IDX(move_target_wc_abspaths, i, + const char *); + APR_ARRAY_PUSH(*possible_moved_to_abspaths, const char *) = + apr_pstrdup(result_pool, moved_to_abspath); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_option_set_moved_to_abspath( + svn_client_conflict_option_t *option, + int preferred_move_target_idx, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_client_conflict_t *conflict = option->conflict; + struct conflict_tree_incoming_delete_details *details; + const char *victim_abspath; + apr_array_header_t *move_target_wc_abspaths; + + SVN_ERR_ASSERT(svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_file_text_merge || + svn_client_conflict_option_get_id(option) == + svn_client_conflict_option_incoming_move_dir_merge); + + victim_abspath = svn_client_conflict_get_local_abspath(conflict); + details = conflict->tree_conflict_incoming_details; + if (details == NULL || details->wc_move_targets == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Setting a move target requires details " + "for tree conflict at '%s' to be fetched " + "from the repository first"), + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + move_target_wc_abspaths = + svn_hash_gets(details->wc_move_targets, + get_moved_to_repos_relpath(details, scratch_pool)); + + if (preferred_move_target_idx < 0 || + preferred_move_target_idx > move_target_wc_abspaths->nelts) + return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, + _("Index '%d' is out of bounds of the possible " + "move target list for '%s'"), + preferred_move_target_idx, + svn_dirent_local_style(victim_abspath, + scratch_pool)); + + /* Record the user's preference. */ + details->wc_move_target_idx = preferred_move_target_idx; + + /* Update option description. */ + SVN_ERR(describe_incoming_move_merge_conflict_option(&option->description, + conflict, ctx, + details, + conflict->pool, + scratch_pool)); + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_tree_get_resolution_options(apr_array_header_t **options, + svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_tree_conflict(conflict, scratch_pool)); + + *options = apr_array_make(result_pool, 2, + sizeof(svn_client_conflict_option_t *)); + + /* Add postpone option. */ + add_resolution_option(*options, conflict, + svn_client_conflict_option_postpone, + _("Postpone"), + _("skip this conflict and leave it unresolved"), + resolve_postpone); + + /* Add an option which marks the conflict resolved. */ + SVN_ERR(configure_option_accept_current_wc_state(conflict, *options)); + + /* Configure options which offer automatic resolution. */ + SVN_ERR(configure_option_update_move_destination(conflict, *options)); + SVN_ERR(configure_option_update_raise_moved_away_children(conflict, + *options)); + SVN_ERR(configure_option_incoming_add_ignore(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_file_text_merge(conflict, ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_file_replace_and_merge(conflict, + ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_dir_merge(conflict, ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_dir_replace(conflict, ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_added_dir_replace_and_merge(conflict, + ctx, + *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_delete_ignore(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_delete_accept(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_move_file_merge(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_incoming_dir_merge(conflict, ctx, *options, + scratch_pool)); + SVN_ERR(configure_option_local_move_file_merge(conflict, ctx, *options, + scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Swallow authz failures and return SVN_NO_ERROR in that case. + * Otherwise, return ERR unchanged. */ +static svn_error_t * +ignore_authz_failures(svn_error_t *err) +{ + if (err && ( svn_error_find_cause(err, SVN_ERR_AUTHZ_UNREADABLE) + || svn_error_find_cause(err, SVN_ERR_RA_NOT_AUTHORIZED) + || svn_error_find_cause(err, SVN_ERR_RA_DAV_FORBIDDEN))) + { + svn_error_clear(err); + err = SVN_NO_ERROR; + } + + return err; +} + +svn_error_t * +svn_client_conflict_tree_get_details(svn_client_conflict_t *conflict, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_tree_conflict(conflict, scratch_pool)); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + svn_client_conflict_get_local_abspath(conflict), + svn_wc_notify_begin_search_tree_conflict_details, + scratch_pool), + ctx->notify_func2(ctx->notify_baton2, notify, + scratch_pool); + } + + /* Collecting conflict details may fail due to insufficient access rights. + * This is not a failure but simply restricts our future options. */ + if (conflict->tree_conflict_get_incoming_details_func) + SVN_ERR(ignore_authz_failures( + conflict->tree_conflict_get_incoming_details_func(conflict, ctx, + scratch_pool))); + + + if (conflict->tree_conflict_get_local_details_func) + SVN_ERR(ignore_authz_failures( + conflict->tree_conflict_get_local_details_func(conflict, ctx, + scratch_pool))); + + if (ctx->notify_func2) + { + svn_wc_notify_t *notify; + + notify = svn_wc_create_notify( + svn_client_conflict_get_local_abspath(conflict), + svn_wc_notify_end_search_tree_conflict_details, + scratch_pool), + ctx->notify_func2(ctx->notify_baton2, notify, + scratch_pool); + } + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_id_t +svn_client_conflict_option_get_id(svn_client_conflict_option_t *option) +{ + return option->id; +} + +const char * +svn_client_conflict_option_get_label(svn_client_conflict_option_t *option, + apr_pool_t *result_pool) +{ + return apr_pstrdup(result_pool, option->label); +} + +const char * +svn_client_conflict_option_get_description(svn_client_conflict_option_t *option, + apr_pool_t *result_pool) +{ + return apr_pstrdup(result_pool, option->description); +} + +svn_client_conflict_option_id_t +svn_client_conflict_get_recommended_option_id(svn_client_conflict_t *conflict) +{ + return conflict->recommended_option_id; +} + +svn_error_t * +svn_client_conflict_text_resolve(svn_client_conflict_t *conflict, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_text_conflict(conflict, scratch_pool)); + SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_t * +svn_client_conflict_option_find_by_id(apr_array_header_t *options, + svn_client_conflict_option_id_t option_id) +{ + int i; + + for (i = 0; i < options->nelts; i++) + { + svn_client_conflict_option_t *this_option; + svn_client_conflict_option_id_t this_option_id; + + this_option = APR_ARRAY_IDX(options, i, svn_client_conflict_option_t *); + this_option_id = svn_client_conflict_option_get_id(this_option); + + if (this_option_id == option_id) + return this_option; + } + + return NULL; +} + +svn_error_t * +svn_client_conflict_text_resolve_by_id( + svn_client_conflict_t *conflict, + svn_client_conflict_option_id_t option_id, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *resolution_options; + svn_client_conflict_option_t *option; + + SVN_ERR(svn_client_conflict_text_get_resolution_options( + &resolution_options, conflict, ctx, + scratch_pool, scratch_pool)); + option = svn_client_conflict_option_find_by_id(resolution_options, + option_id); + if (option == NULL) + return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE, + NULL, + _("Inapplicable conflict resolution option " + "given for conflicted path '%s'"), + svn_dirent_local_style(conflict->local_abspath, + scratch_pool)); + + SVN_ERR(svn_client_conflict_text_resolve(conflict, option, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_id_t +svn_client_conflict_text_get_resolution(svn_client_conflict_t *conflict) +{ + return conflict->resolution_text; +} + +svn_error_t * +svn_client_conflict_prop_resolve(svn_client_conflict_t *conflict, + const char *propname, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_prop_conflict(conflict, scratch_pool)); + option->type_data.prop.propname = propname; + SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_prop_resolve_by_id( + svn_client_conflict_t *conflict, + const char *propname, + svn_client_conflict_option_id_t option_id, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *resolution_options; + svn_client_conflict_option_t *option; + + SVN_ERR(svn_client_conflict_prop_get_resolution_options( + &resolution_options, conflict, ctx, + scratch_pool, scratch_pool)); + option = svn_client_conflict_option_find_by_id(resolution_options, + option_id); + if (option == NULL) + return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE, + NULL, + _("Inapplicable conflict resolution option " + "given for conflicted path '%s'"), + svn_dirent_local_style(conflict->local_abspath, + scratch_pool)); + SVN_ERR(svn_client_conflict_prop_resolve(conflict, propname, option, ctx, + scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_id_t +svn_client_conflict_prop_get_resolution(svn_client_conflict_t *conflict, + const char *propname) +{ + svn_client_conflict_option_t *option; + + option = svn_hash_gets(conflict->resolved_props, propname); + if (option == NULL) + return svn_client_conflict_option_unspecified; + + return svn_client_conflict_option_get_id(option); +} + +svn_error_t * +svn_client_conflict_tree_resolve(svn_client_conflict_t *conflict, + svn_client_conflict_option_t *option, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_tree_conflict(conflict, scratch_pool)); + SVN_ERR(option->do_resolve_func(option, conflict, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_tree_resolve_by_id( + svn_client_conflict_t *conflict, + svn_client_conflict_option_id_t option_id, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_array_header_t *resolution_options; + svn_client_conflict_option_t *option; + + SVN_ERR(svn_client_conflict_tree_get_resolution_options( + &resolution_options, conflict, ctx, + scratch_pool, scratch_pool)); + option = svn_client_conflict_option_find_by_id(resolution_options, + option_id); + if (option == NULL) + return svn_error_createf(SVN_ERR_CLIENT_CONFLICT_OPTION_NOT_APPLICABLE, + NULL, + _("Inapplicable conflict resolution option " + "given for conflicted path '%s'"), + svn_dirent_local_style(conflict->local_abspath, + scratch_pool)); + SVN_ERR(svn_client_conflict_tree_resolve(conflict, option, ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_client_conflict_option_id_t +svn_client_conflict_tree_get_resolution(svn_client_conflict_t *conflict) +{ + return conflict->resolution_tree; +} + +/* Return the legacy conflict descriptor which is wrapped by CONFLICT. */ +static const svn_wc_conflict_description2_t * +get_conflict_desc2_t(svn_client_conflict_t *conflict) +{ + if (conflict->legacy_text_conflict) + return conflict->legacy_text_conflict; + + if (conflict->legacy_tree_conflict) + return conflict->legacy_tree_conflict; + + if (conflict->prop_conflicts && conflict->legacy_prop_conflict_propname) + return svn_hash_gets(conflict->prop_conflicts, + conflict->legacy_prop_conflict_propname); + + return NULL; +} + +svn_error_t * +svn_client_conflict_get_conflicted(svn_boolean_t *text_conflicted, + apr_array_header_t **props_conflicted, + svn_boolean_t *tree_conflicted, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (text_conflicted) + *text_conflicted = (conflict->legacy_text_conflict != NULL); + + if (props_conflicted) + { + if (conflict->prop_conflicts) + SVN_ERR(svn_hash_keys(props_conflicted, conflict->prop_conflicts, + result_pool)); + else + *props_conflicted = apr_array_make(result_pool, 0, + sizeof(const char*)); + } + + if (tree_conflicted) + *tree_conflicted = (conflict->legacy_tree_conflict != NULL); + + return SVN_NO_ERROR; +} + +const char * +svn_client_conflict_get_local_abspath(svn_client_conflict_t *conflict) +{ + return conflict->local_abspath; +} + +svn_wc_operation_t +svn_client_conflict_get_operation(svn_client_conflict_t *conflict) +{ + return get_conflict_desc2_t(conflict)->operation; +} + +svn_wc_conflict_action_t +svn_client_conflict_get_incoming_change(svn_client_conflict_t *conflict) +{ + return get_conflict_desc2_t(conflict)->action; +} + +svn_wc_conflict_reason_t +svn_client_conflict_get_local_change(svn_client_conflict_t *conflict) +{ + return get_conflict_desc2_t(conflict)->reason; +} + +svn_error_t * +svn_client_conflict_get_repos_info(const char **repos_root_url, + const char **repos_uuid, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (repos_root_url) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *repos_root_url = + get_conflict_desc2_t(conflict)->src_left_version->repos_url; + else if (get_conflict_desc2_t(conflict)->src_right_version) + *repos_root_url = + get_conflict_desc2_t(conflict)->src_right_version->repos_url; + else + *repos_root_url = NULL; + } + + if (repos_uuid) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *repos_uuid = + get_conflict_desc2_t(conflict)->src_left_version->repos_uuid; + else if (get_conflict_desc2_t(conflict)->src_right_version) + *repos_uuid = + get_conflict_desc2_t(conflict)->src_right_version->repos_uuid; + else + *repos_uuid = NULL; + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_get_incoming_old_repos_location( + const char **incoming_old_repos_relpath, + svn_revnum_t *incoming_old_pegrev, + svn_node_kind_t *incoming_old_node_kind, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (incoming_old_repos_relpath) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *incoming_old_repos_relpath = + get_conflict_desc2_t(conflict)->src_left_version->path_in_repos; + else + *incoming_old_repos_relpath = NULL; + } + + if (incoming_old_pegrev) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *incoming_old_pegrev = + get_conflict_desc2_t(conflict)->src_left_version->peg_rev; + else + *incoming_old_pegrev = SVN_INVALID_REVNUM; + } + + if (incoming_old_node_kind) + { + if (get_conflict_desc2_t(conflict)->src_left_version) + *incoming_old_node_kind = + get_conflict_desc2_t(conflict)->src_left_version->node_kind; + else + *incoming_old_node_kind = svn_node_none; + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_get_incoming_new_repos_location( + const char **incoming_new_repos_relpath, + svn_revnum_t *incoming_new_pegrev, + svn_node_kind_t *incoming_new_node_kind, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (incoming_new_repos_relpath) + { + if (get_conflict_desc2_t(conflict)->src_right_version) + *incoming_new_repos_relpath = + get_conflict_desc2_t(conflict)->src_right_version->path_in_repos; + else + *incoming_new_repos_relpath = NULL; + } + + if (incoming_new_pegrev) + { + if (get_conflict_desc2_t(conflict)->src_right_version) + *incoming_new_pegrev = + get_conflict_desc2_t(conflict)->src_right_version->peg_rev; + else + *incoming_new_pegrev = SVN_INVALID_REVNUM; + } + + if (incoming_new_node_kind) + { + if (get_conflict_desc2_t(conflict)->src_right_version) + *incoming_new_node_kind = + get_conflict_desc2_t(conflict)->src_right_version->node_kind; + else + *incoming_new_node_kind = svn_node_none; + } + + return SVN_NO_ERROR; +} + +svn_node_kind_t +svn_client_conflict_tree_get_victim_node_kind(svn_client_conflict_t *conflict) +{ + SVN_ERR_ASSERT_NO_RETURN(assert_tree_conflict(conflict, conflict->pool) + == SVN_NO_ERROR); + + return get_conflict_desc2_t(conflict)->node_kind; +} + +svn_error_t * +svn_client_conflict_prop_get_propvals(const svn_string_t **base_propval, + const svn_string_t **working_propval, + const svn_string_t **incoming_old_propval, + const svn_string_t **incoming_new_propval, + svn_client_conflict_t *conflict, + const char *propname, + apr_pool_t *result_pool) +{ + const svn_wc_conflict_description2_t *desc; + + SVN_ERR(assert_prop_conflict(conflict, conflict->pool)); + + desc = svn_hash_gets(conflict->prop_conflicts, propname); + if (desc == NULL) + return svn_error_createf(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, NULL, + _("Property '%s' is not in conflict."), propname); + + if (base_propval) + *base_propval = + svn_string_dup(desc->prop_value_base, result_pool); + + if (working_propval) + *working_propval = + svn_string_dup(desc->prop_value_working, result_pool); + + if (incoming_old_propval) + *incoming_old_propval = + svn_string_dup(desc->prop_value_incoming_old, result_pool); + + if (incoming_new_propval) + *incoming_new_propval = + svn_string_dup(desc->prop_value_incoming_new, result_pool); + + return SVN_NO_ERROR; +} + +const char * +svn_client_conflict_prop_get_reject_abspath(svn_client_conflict_t *conflict) +{ + SVN_ERR_ASSERT_NO_RETURN(assert_prop_conflict(conflict, conflict->pool) + == SVN_NO_ERROR); + + /* svn_wc_conflict_description2_t stores this path in 'their_abspath' */ + return get_conflict_desc2_t(conflict)->their_abspath; +} + +const char * +svn_client_conflict_text_get_mime_type(svn_client_conflict_t *conflict) +{ + SVN_ERR_ASSERT_NO_RETURN(assert_text_conflict(conflict, conflict->pool) + == SVN_NO_ERROR); + + return get_conflict_desc2_t(conflict)->mime_type; +} + +svn_error_t * +svn_client_conflict_text_get_contents(const char **base_abspath, + const char **working_abspath, + const char **incoming_old_abspath, + const char **incoming_new_abspath, + svn_client_conflict_t *conflict, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + SVN_ERR(assert_text_conflict(conflict, scratch_pool)); + + if (base_abspath) + { + if (svn_client_conflict_get_operation(conflict) == + svn_wc_operation_merge) + *base_abspath = NULL; /* ### WC base contents not available yet */ + else /* update/switch */ + *base_abspath = get_conflict_desc2_t(conflict)->base_abspath; + } + + if (working_abspath) + *working_abspath = get_conflict_desc2_t(conflict)->my_abspath; + + if (incoming_old_abspath) + *incoming_old_abspath = get_conflict_desc2_t(conflict)->base_abspath; + + if (incoming_new_abspath) + *incoming_new_abspath = get_conflict_desc2_t(conflict)->their_abspath; + + return SVN_NO_ERROR; +} + +/* Set up type-specific data for a new conflict object. */ +static svn_error_t * +conflict_type_specific_setup(svn_client_conflict_t *conflict, + apr_pool_t *scratch_pool) +{ + svn_boolean_t tree_conflicted; + svn_wc_conflict_action_t incoming_change; + svn_wc_conflict_reason_t local_change; + + /* For now, we only deal with tree conflicts here. */ + SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted, + conflict, scratch_pool, + scratch_pool)); + if (!tree_conflicted) + return SVN_NO_ERROR; + + /* Set a default description function. */ + conflict->tree_conflict_get_incoming_description_func = + conflict_tree_get_incoming_description_generic; + conflict->tree_conflict_get_local_description_func = + conflict_tree_get_local_description_generic; + + incoming_change = svn_client_conflict_get_incoming_change(conflict); + local_change = svn_client_conflict_get_local_change(conflict); + + /* Set type-specific description and details functions. */ + if (incoming_change == svn_wc_conflict_action_delete || + incoming_change == svn_wc_conflict_action_replace) + { + conflict->tree_conflict_get_incoming_description_func = + conflict_tree_get_description_incoming_delete; + conflict->tree_conflict_get_incoming_details_func = + conflict_tree_get_details_incoming_delete; + } + else if (incoming_change == svn_wc_conflict_action_add) + { + conflict->tree_conflict_get_incoming_description_func = + conflict_tree_get_description_incoming_add; + conflict->tree_conflict_get_incoming_details_func = + conflict_tree_get_details_incoming_add; + } + else if (incoming_change == svn_wc_conflict_action_edit) + { + conflict->tree_conflict_get_incoming_description_func = + conflict_tree_get_description_incoming_edit; + conflict->tree_conflict_get_incoming_details_func = + conflict_tree_get_details_incoming_edit; + } + + if (local_change == svn_wc_conflict_reason_missing) + { + conflict->tree_conflict_get_local_description_func = + conflict_tree_get_description_local_missing; + conflict->tree_conflict_get_local_details_func = + conflict_tree_get_details_local_missing; + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_get(svn_client_conflict_t **conflict, + const char *local_abspath, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const apr_array_header_t *descs; + int i; + + *conflict = apr_pcalloc(result_pool, sizeof(**conflict)); + + (*conflict)->local_abspath = apr_pstrdup(result_pool, local_abspath); + (*conflict)->resolution_text = svn_client_conflict_option_unspecified; + (*conflict)->resolution_tree = svn_client_conflict_option_unspecified; + (*conflict)->resolved_props = apr_hash_make(result_pool); + (*conflict)->recommended_option_id = svn_client_conflict_option_unspecified; + (*conflict)->pool = result_pool; + + /* Add all legacy conflict descriptors we can find. Eventually, this code + * path should stop relying on svn_wc_conflict_description2_t entirely. */ + SVN_ERR(svn_wc__read_conflict_descriptions2_t(&descs, ctx->wc_ctx, + local_abspath, + result_pool, scratch_pool)); + for (i = 0; i < descs->nelts; i++) + { + const svn_wc_conflict_description2_t *desc; + + desc = APR_ARRAY_IDX(descs, i, const svn_wc_conflict_description2_t *); + add_legacy_desc_to_conflict(desc, *conflict, result_pool); + } + + SVN_ERR(conflict_type_specific_setup(*conflict, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Baton for conflict_status_walker */ +struct conflict_status_walker_baton +{ + svn_client_conflict_walk_func_t conflict_walk_func; + void *conflict_walk_func_baton; + svn_client_ctx_t *ctx; + svn_wc_notify_func2_t notify_func; + void *notify_baton; + svn_boolean_t resolved_a_tree_conflict; + apr_hash_t *unresolved_tree_conflicts; +}; + +/* Implements svn_wc_notify_func2_t to collect new conflicts caused by + resolving a tree conflict. */ +static void +tree_conflict_collector(void *baton, + const svn_wc_notify_t *notify, + apr_pool_t *pool) +{ + struct conflict_status_walker_baton *cswb = baton; + + if (cswb->notify_func) + cswb->notify_func(cswb->notify_baton, notify, pool); + + if (cswb->unresolved_tree_conflicts + && (notify->action == svn_wc_notify_tree_conflict + || notify->prop_state == svn_wc_notify_state_conflicted + || notify->content_state == svn_wc_notify_state_conflicted)) + { + if (!svn_hash_gets(cswb->unresolved_tree_conflicts, notify->path)) + { + const char *tc_abspath; + apr_pool_t *hash_pool; + + hash_pool = apr_hash_pool_get(cswb->unresolved_tree_conflicts); + tc_abspath = apr_pstrdup(hash_pool, notify->path); + svn_hash_sets(cswb->unresolved_tree_conflicts, tc_abspath, ""); + } + } +} + +/* + * Record a tree conflict resolution failure due to error condition ERR + * in the RESOLVE_LATER hash table. If the hash table is not available + * (meaning the caller does not wish to retry resolution later), or if + * the error condition does not indicate circumstances where another + * existing tree conflict is blocking the resolution attempt, then + * return the error ERR itself. + */ +static svn_error_t * +handle_tree_conflict_resolution_failure(const char *local_abspath, + svn_error_t *err, + apr_hash_t *unresolved_tree_conflicts) +{ + const char *tc_abspath; + + if (!unresolved_tree_conflicts + || (err->apr_err != SVN_ERR_WC_OBSTRUCTED_UPDATE + && err->apr_err != SVN_ERR_WC_FOUND_CONFLICT)) + return svn_error_trace(err); /* Give up. Do not retry resolution later. */ + + svn_error_clear(err); + tc_abspath = apr_pstrdup(apr_hash_pool_get(unresolved_tree_conflicts), + local_abspath); + + svn_hash_sets(unresolved_tree_conflicts, tc_abspath, ""); + + return SVN_NO_ERROR; /* Caller may retry after resolving other conflicts. */ +} + +/* Implements svn_wc_status4_t to walk all conflicts to resolve. + */ +static svn_error_t * +conflict_status_walker(void *baton, + const char *local_abspath, + const svn_wc_status3_t *status, + apr_pool_t *scratch_pool) +{ + struct conflict_status_walker_baton *cswb = baton; + svn_client_conflict_t *conflict; + svn_error_t *err; + svn_boolean_t tree_conflicted; + + if (!status->conflicted) + return SVN_NO_ERROR; + + SVN_ERR(svn_client_conflict_get(&conflict, local_abspath, cswb->ctx, + scratch_pool, scratch_pool)); + SVN_ERR(svn_client_conflict_get_conflicted(NULL, NULL, &tree_conflicted, + conflict, scratch_pool, + scratch_pool)); + err = cswb->conflict_walk_func(cswb->conflict_walk_func_baton, + conflict, scratch_pool); + if (err) + { + if (tree_conflicted) + SVN_ERR(handle_tree_conflict_resolution_failure( + local_abspath, err, cswb->unresolved_tree_conflicts)); + + else + return svn_error_trace(err); + } + + if (tree_conflicted) + { + svn_client_conflict_option_id_t resolution; + + resolution = svn_client_conflict_tree_get_resolution(conflict); + if (resolution != svn_client_conflict_option_unspecified && + resolution != svn_client_conflict_option_postpone) + cswb->resolved_a_tree_conflict = TRUE; + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_conflict_walk(const char *local_abspath, + svn_depth_t depth, + svn_client_conflict_walk_func_t conflict_walk_func, + void *conflict_walk_func_baton, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + struct conflict_status_walker_baton cswb; + apr_pool_t *iterpool = NULL; + svn_error_t *err = SVN_NO_ERROR; + + if (depth == svn_depth_unknown) + depth = svn_depth_infinity; + + cswb.conflict_walk_func = conflict_walk_func; + cswb.conflict_walk_func_baton = conflict_walk_func_baton; + cswb.ctx = ctx; + cswb.resolved_a_tree_conflict = FALSE; + cswb.unresolved_tree_conflicts = apr_hash_make(scratch_pool); + + if (ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify( + local_abspath, + svn_wc_notify_conflict_resolver_starting, + scratch_pool), + scratch_pool); + + /* Swap in our notify_func wrapper. We must revert this before returning! */ + cswb.notify_func = ctx->notify_func2; + cswb.notify_baton = ctx->notify_baton2; + ctx->notify_func2 = tree_conflict_collector; + ctx->notify_baton2 = &cswb; + + err = svn_wc_walk_status(ctx->wc_ctx, + local_abspath, + depth, + FALSE /* get_all */, + FALSE /* no_ignore */, + TRUE /* ignore_text_mods */, + NULL /* ignore_patterns */, + conflict_status_walker, &cswb, + ctx->cancel_func, ctx->cancel_baton, + scratch_pool); + + /* If we got new tree conflicts (or delayed conflicts) during the initial + walk, we now walk them one by one as closure. */ + while (!err && cswb.unresolved_tree_conflicts && + apr_hash_count(cswb.unresolved_tree_conflicts)) + { + apr_hash_index_t *hi; + svn_wc_status3_t *status = NULL; + const char *tc_abspath = NULL; + + if (iterpool) + svn_pool_clear(iterpool); + else + iterpool = svn_pool_create(scratch_pool); + + hi = apr_hash_first(scratch_pool, cswb.unresolved_tree_conflicts); + cswb.unresolved_tree_conflicts = apr_hash_make(scratch_pool); + cswb.resolved_a_tree_conflict = FALSE; + + for (; hi && !err; hi = apr_hash_next(hi)) + { + svn_pool_clear(iterpool); + + tc_abspath = apr_hash_this_key(hi); + + if (ctx->cancel_func) + { + err = ctx->cancel_func(ctx->cancel_baton); + if (err) + break; + } + + err = svn_error_trace(svn_wc_status3(&status, ctx->wc_ctx, + tc_abspath, + iterpool, iterpool)); + if (err) + break; + + err = svn_error_trace(conflict_status_walker(&cswb, tc_abspath, + status, scratch_pool)); + if (err) + break; + } + + if (!err && !cswb.resolved_a_tree_conflict && tc_abspath && + apr_hash_count(cswb.unresolved_tree_conflicts)) + { + /* None of the remaining conflicts got resolved, without any error. + * Disable the 'unresolved_tree_conflicts' cache and try again. */ + cswb.unresolved_tree_conflicts = NULL; + + /* Run the most recent resolve operation again. + * We still have status and tc_abspath for that one. + * This should uncover the error which prevents resolution. */ + err = svn_error_trace(conflict_status_walker(&cswb, tc_abspath, + status, scratch_pool)); + SVN_ERR_ASSERT(err != NULL); + + err = svn_error_createf( + SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, err, + _("Unable to resolve pending conflict on '%s'"), + svn_dirent_local_style(tc_abspath, scratch_pool)); + break; + } + } + + if (iterpool) + svn_pool_destroy(iterpool); + + ctx->notify_func2 = cswb.notify_func; + ctx->notify_baton2 = cswb.notify_baton; + + if (!err && ctx->notify_func2) + ctx->notify_func2(ctx->notify_baton2, + svn_wc_create_notify(local_abspath, + svn_wc_notify_conflict_resolver_done, + scratch_pool), + scratch_pool); + + return svn_error_trace(err); +} diff --git a/subversion/libsvn_client/copy.c b/subversion/libsvn_client/copy.c index af6a75b83698..b2e3a44d797b 100644 --- a/subversion/libsvn_client/copy.c +++ b/subversion/libsvn_client/copy.c @@ -1711,13 +1711,9 @@ repos_to_repos_copy(const apr_array_header_t *copy_pairs, SVN_ERR(svn_ra_check_path(ra_session, dst_rel, SVN_INVALID_REVNUM, &dst_kind, pool)); if (dst_kind != svn_node_none) - { - const char *path = svn_uri_skip_ancestor(repos_root, - pair->dst_abspath_or_url, - pool); - return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL, - _("Path '/%s' already exists"), path); - } + return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL, + _("Path '%s' already exists"), + pair->dst_abspath_or_url); /* More info for our INFO structure. */ info->src_path = src_rel; /* May be NULL, if outside RA session scope */ @@ -2433,10 +2429,10 @@ repos_to_wc_copy_single(svn_boolean_t *timestamp_sleep, *timestamp_sleep = TRUE; - /* 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. */ + /* 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. */ SVN_ERR(svn_wc_copy3(ctx->wc_ctx, tmp_abspath, dst_abspath, TRUE /* metadata_only */, ctx->cancel_func, ctx->cancel_baton, @@ -2451,7 +2447,7 @@ repos_to_wc_copy_single(svn_boolean_t *timestamp_sleep, pool)); /* Move the temporary disk tree into place. */ - SVN_ERR(svn_io_file_rename(tmp_abspath, dst_abspath, pool)); + SVN_ERR(svn_io_file_rename2(tmp_abspath, dst_abspath, FALSE, pool)); } else { @@ -2580,7 +2576,7 @@ repos_to_wc_copy_single(svn_boolean_t *timestamp_sleep, static svn_error_t * repos_to_wc_copy_locked(svn_boolean_t *timestamp_sleep, const apr_array_header_t *copy_pairs, - const char *top_dst_path, + const char *top_dst_abspath, svn_boolean_t ignore_externals, svn_boolean_t pin_externals, const apr_hash_t *externals_to_pin, @@ -2600,39 +2596,25 @@ repos_to_wc_copy_locked(svn_boolean_t *timestamp_sleep, /* Decide whether the two repositories are the same or not. */ { - svn_error_t *src_err, *dst_err; - const char *parent; const char *parent_abspath; const char *src_uuid, *dst_uuid; /* Get the repository uuid of SRC_URL */ - src_err = svn_ra_get_uuid2(ra_session, &src_uuid, iterpool); - if (src_err && src_err->apr_err != SVN_ERR_RA_NO_REPOS_UUID) - return svn_error_trace(src_err); + SVN_ERR(svn_ra_get_uuid2(ra_session, &src_uuid, iterpool)); /* Get repository uuid of dst's parent directory, since dst may not exist. ### TODO: we should probably walk up the wc here, in case the parent dir has an imaginary URL. */ if (copy_pairs->nelts == 1) - parent = svn_dirent_dirname(top_dst_path, scratch_pool); + parent_abspath = svn_dirent_dirname(top_dst_abspath, scratch_pool); else - parent = top_dst_path; - - SVN_ERR(svn_dirent_get_absolute(&parent_abspath, parent, scratch_pool)); - dst_err = svn_client_get_repos_root(NULL /* root_url */, &dst_uuid, - parent_abspath, ctx, - iterpool, iterpool); - if (dst_err && dst_err->apr_err != SVN_ERR_RA_NO_REPOS_UUID) - return dst_err; - - /* If either of the UUIDs are nonexistent, then at least one of - the repositories must be very old. Rather than punish the - user, just assume the repositories are different, so no - copy-history is attempted. */ - if (src_err || dst_err || (! src_uuid) || (! dst_uuid)) - same_repositories = FALSE; - else - same_repositories = (strcmp(src_uuid, dst_uuid) == 0); + parent_abspath = top_dst_abspath; + + SVN_ERR(svn_client_get_repos_root(NULL /* root_url */, &dst_uuid, + parent_abspath, ctx, + iterpool, iterpool)); + /* ### Also check repos_root_url? */ + same_repositories = (strcmp(src_uuid, dst_uuid) == 0); } /* Perform the move for each of the copy_pairs. */ @@ -2668,7 +2650,7 @@ repos_to_wc_copy(svn_boolean_t *timestamp_sleep, apr_pool_t *pool) { svn_ra_session_t *ra_session; - const char *top_src_url, *top_dst_path; + const char *top_src_url, *top_dst_abspath; apr_pool_t *iterpool = svn_pool_create(pool); const char *lock_abspath; int i; @@ -2693,13 +2675,13 @@ repos_to_wc_copy(svn_boolean_t *timestamp_sleep, pair->src_abspath_or_url = apr_pstrdup(pool, src); } - SVN_ERR(get_copy_pair_ancestors(copy_pairs, &top_src_url, &top_dst_path, + SVN_ERR(get_copy_pair_ancestors(copy_pairs, &top_src_url, &top_dst_abspath, NULL, pool)); - lock_abspath = top_dst_path; + lock_abspath = top_dst_abspath; if (copy_pairs->nelts == 1) { top_src_url = svn_uri_dirname(top_src_url, pool); - lock_abspath = svn_dirent_dirname(top_dst_path, pool); + lock_abspath = svn_dirent_dirname(top_dst_abspath, pool); } /* Open a repository session to the longest common src ancestor. We do not @@ -2771,7 +2753,7 @@ repos_to_wc_copy(svn_boolean_t *timestamp_sleep, SVN_WC__CALL_WITH_WRITE_LOCK( repos_to_wc_copy_locked(timestamp_sleep, - copy_pairs, top_dst_path, ignore_externals, + copy_pairs, top_dst_abspath, ignore_externals, pin_externals, externals_to_pin, ra_session, ctx, pool), ctx->wc_ctx, lock_abspath, FALSE, pool); diff --git a/subversion/libsvn_client/deprecated.c b/subversion/libsvn_client/deprecated.c index b1760a4e3738..dc20b2772286 100644 --- a/subversion/libsvn_client/deprecated.c +++ b/subversion/libsvn_client/deprecated.c @@ -1330,6 +1330,26 @@ svn_client_export(svn_revnum_t *result_rev, /*** From list.c ***/ +svn_error_t * +svn_client_list3(const char *path_or_url, + const svn_opt_revision_t *peg_revision, + const svn_opt_revision_t *revision, + svn_depth_t depth, + apr_uint32_t dirent_fields, + svn_boolean_t fetch_locks, + svn_boolean_t include_externals, + svn_client_list_func2_t list_func, + void *baton, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + return svn_error_trace(svn_client_list4(path_or_url, peg_revision, + revision, NULL, depth, + dirent_fields, fetch_locks, + include_externals, + list_func, baton, ctx, pool)); +} + /* Baton for use with wrap_list_func */ struct list_func_wrapper_baton { void *list_func1_baton; diff --git a/subversion/libsvn_client/diff.c b/subversion/libsvn_client/diff.c index 4817ffdf1016..ba6e91eb2e7b 100644 --- a/subversion/libsvn_client/diff.c +++ b/subversion/libsvn_client/diff.c @@ -230,113 +230,158 @@ adjust_paths_for_diff_labels(const char **index_path, /* Generate a label for the diff output for file PATH at revision REVNUM. If REVNUM is invalid then it is assumed to be the current working copy. Assumes the paths are already in the desired style (local - vs internal). Allocate the label in POOL. */ + vs internal). Allocate the label in RESULT-POOL. */ static const char * diff_label(const char *path, svn_revnum_t revnum, - apr_pool_t *pool) + apr_pool_t *result_pool) { const char *label; if (revnum >= 0) - label = apr_psprintf(pool, _("%s\t(revision %ld)"), path, revnum); + label = apr_psprintf(result_pool, _("%s\t(revision %ld)"), path, revnum); else if (revnum == DIFF_REVNUM_NONEXISTENT) - label = apr_psprintf(pool, _("%s\t(nonexistent)"), path); + label = apr_psprintf(result_pool, _("%s\t(nonexistent)"), path); else /* SVN_INVALID_REVNUM */ - label = apr_psprintf(pool, _("%s\t(working copy)"), path); + label = apr_psprintf(result_pool, _("%s\t(working copy)"), path); return label; } +/* Standard modes produced in git style diffs */ +static const int exec_mode = 0755; +static const int noexec_mode = 0644; +static const int kind_file_mode = 0100000; +/*static const kind_dir_mode = 0040000;*/ +static const int kind_symlink_mode = 0120000; + /* Print a git diff header for an addition within a diff between PATH1 and - * PATH2 to the stream OS using HEADER_ENCODING. - * All allocations are done in RESULT_POOL. */ + * PATH2 to the stream OS using HEADER_ENCODING. */ static svn_error_t * print_git_diff_header_added(svn_stream_t *os, const char *header_encoding, const char *path1, const char *path2, - apr_pool_t *result_pool) + svn_boolean_t exec_bit, + svn_boolean_t symlink_bit, + apr_pool_t *scratch_pool) { - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + int new_mode = (exec_bit ? exec_mode : noexec_mode) + | (symlink_bit ? kind_symlink_mode : kind_file_mode); + + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "diff --git a/%s b/%s%s", path1, path2, APR_EOL_STR)); - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, - "new file mode 10644" APR_EOL_STR)); + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, + "new file mode %06o" APR_EOL_STR, + new_mode)); return SVN_NO_ERROR; } /* Print a git diff header for a deletion within a diff between PATH1 and - * PATH2 to the stream OS using HEADER_ENCODING. - * All allocations are done in RESULT_POOL. */ + * PATH2 to the stream OS using HEADER_ENCODING. */ static svn_error_t * print_git_diff_header_deleted(svn_stream_t *os, const char *header_encoding, const char *path1, const char *path2, - apr_pool_t *result_pool) + svn_boolean_t exec_bit, + svn_boolean_t symlink_bit, + apr_pool_t *scratch_pool) { - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + int old_mode = (exec_bit ? exec_mode : noexec_mode) + | (symlink_bit ? kind_symlink_mode : kind_file_mode); + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "diff --git a/%s b/%s%s", path1, path2, APR_EOL_STR)); - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, - "deleted file mode 10644" - APR_EOL_STR)); + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, + "deleted file mode %06o" APR_EOL_STR, + old_mode)); return SVN_NO_ERROR; } /* Print a git diff header for a copy from COPYFROM_PATH to PATH to the stream - * OS using HEADER_ENCODING. All allocations are done in RESULT_POOL. */ + * OS using HEADER_ENCODING. */ static svn_error_t * print_git_diff_header_copied(svn_stream_t *os, const char *header_encoding, const char *copyfrom_path, svn_revnum_t copyfrom_rev, const char *path, - apr_pool_t *result_pool) + apr_pool_t *scratch_pool) { - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "diff --git a/%s b/%s%s", copyfrom_path, path, APR_EOL_STR)); if (copyfrom_rev != SVN_INVALID_REVNUM) - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "copy from %s@%ld%s", copyfrom_path, copyfrom_rev, APR_EOL_STR)); else - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "copy from %s%s", copyfrom_path, APR_EOL_STR)); - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "copy to %s%s", path, APR_EOL_STR)); return SVN_NO_ERROR; } /* Print a git diff header for a rename from COPYFROM_PATH to PATH to the - * stream OS using HEADER_ENCODING. All allocations are done in RESULT_POOL. */ + * stream OS using HEADER_ENCODING. */ static svn_error_t * print_git_diff_header_renamed(svn_stream_t *os, const char *header_encoding, const char *copyfrom_path, const char *path, - apr_pool_t *result_pool) + apr_pool_t *scratch_pool) { - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "diff --git a/%s b/%s%s", copyfrom_path, path, APR_EOL_STR)); - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "rename from %s%s", copyfrom_path, APR_EOL_STR)); - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "rename to %s%s", path, APR_EOL_STR)); return SVN_NO_ERROR; } /* Print a git diff header for a modification within a diff between PATH1 and - * PATH2 to the stream OS using HEADER_ENCODING. - * All allocations are done in RESULT_POOL. */ + * PATH2 to the stream OS using HEADER_ENCODING. */ static svn_error_t * print_git_diff_header_modified(svn_stream_t *os, const char *header_encoding, const char *path1, const char *path2, - apr_pool_t *result_pool) + apr_pool_t *scratch_pool) { - SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, result_pool, + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, "diff --git a/%s b/%s%s", path1, path2, APR_EOL_STR)); return SVN_NO_ERROR; } +/* Helper function for print_git_diff_header */ +static svn_error_t * +maybe_print_mode_change(svn_stream_t *os, + const char *header_encoding, + svn_boolean_t exec_bit1, + svn_boolean_t exec_bit2, + svn_boolean_t symlink_bit1, + svn_boolean_t symlink_bit2, + const char *git_index_shas, + apr_pool_t *scratch_pool) +{ + int old_mode = (exec_bit1 ? exec_mode : noexec_mode) + | (symlink_bit1 ? kind_symlink_mode : kind_file_mode); + int new_mode = (exec_bit2 ? exec_mode : noexec_mode) + | (symlink_bit2 ? kind_symlink_mode : kind_file_mode); + if (old_mode == new_mode) + { + if (git_index_shas) + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, + "index %s %06o" APR_EOL_STR, + git_index_shas, old_mode)); + return SVN_NO_ERROR; + } + + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, + "old mode %06o" APR_EOL_STR, old_mode)); + SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool, + "new mode %06o" APR_EOL_STR, new_mode)); + return SVN_NO_ERROR; +} + /* Print a git diff header showing the OPERATION to the stream OS using * HEADER_ENCODING. Return suitable diff labels for the git diff in *LABEL1 * and *LABEL2. REPOS_RELPATH1 and REPOS_RELPATH2 are relative to reposroot. @@ -354,17 +399,31 @@ print_git_diff_header(svn_stream_t *os, svn_revnum_t rev2, const char *copyfrom_path, svn_revnum_t copyfrom_rev, + apr_hash_t *left_props, + apr_hash_t *right_props, + const char *git_index_shas, const char *header_encoding, apr_pool_t *scratch_pool) { + svn_boolean_t exec_bit1 = (svn_prop_get_value(left_props, + SVN_PROP_EXECUTABLE) != NULL); + svn_boolean_t exec_bit2 = (svn_prop_get_value(right_props, + SVN_PROP_EXECUTABLE) != NULL); + svn_boolean_t symlink_bit1 = (svn_prop_get_value(left_props, + SVN_PROP_SPECIAL) != NULL); + svn_boolean_t symlink_bit2 = (svn_prop_get_value(right_props, + SVN_PROP_SPECIAL) != NULL); + if (operation == svn_diff_op_deleted) { SVN_ERR(print_git_diff_header_deleted(os, header_encoding, repos_relpath1, repos_relpath2, + exec_bit1, symlink_bit1, scratch_pool)); *label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", repos_relpath1), rev1, scratch_pool); - *label2 = diff_label("/dev/null", rev2, scratch_pool); + *label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2), + rev2, scratch_pool); } else if (operation == svn_diff_op_copied) @@ -377,13 +436,20 @@ print_git_diff_header(svn_stream_t *os, rev1, scratch_pool); *label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2), rev2, scratch_pool); + SVN_ERR(maybe_print_mode_change(os, header_encoding, + exec_bit1, exec_bit2, + symlink_bit1, symlink_bit2, + git_index_shas, + scratch_pool)); } else if (operation == svn_diff_op_added) { SVN_ERR(print_git_diff_header_added(os, header_encoding, repos_relpath1, repos_relpath2, + exec_bit2, symlink_bit2, scratch_pool)); - *label1 = diff_label("/dev/null", rev1, scratch_pool); + *label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", repos_relpath1), + rev1, scratch_pool); *label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2), rev2, scratch_pool); } @@ -396,6 +462,11 @@ print_git_diff_header(svn_stream_t *os, rev1, scratch_pool); *label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2), rev2, scratch_pool); + SVN_ERR(maybe_print_mode_change(os, header_encoding, + exec_bit1, exec_bit2, + symlink_bit1, symlink_bit2, + git_index_shas, + scratch_pool)); } else if (operation == svn_diff_op_moved) { @@ -406,6 +477,11 @@ print_git_diff_header(svn_stream_t *os, rev1, scratch_pool); *label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2), rev2, scratch_pool); + SVN_ERR(maybe_print_mode_change(os, header_encoding, + exec_bit1, exec_bit2, + symlink_bit1, symlink_bit2, + git_index_shas, + scratch_pool)); } return SVN_NO_ERROR; @@ -425,7 +501,8 @@ print_git_diff_header(svn_stream_t *os, ANCHOR is the local path where the diff editor is anchored. */ static svn_error_t * display_prop_diffs(const apr_array_header_t *propchanges, - apr_hash_t *original_props, + apr_hash_t *left_props, + apr_hash_t *right_props, const char *diff_relpath, const char *anchor, const char *orig_path1, @@ -487,6 +564,9 @@ display_prop_diffs(const apr_array_header_t *propchanges, repos_relpath1, repos_relpath2, rev1, rev2, NULL, SVN_INVALID_REVNUM, + left_props, + right_props, + NULL, encoding, scratch_pool)); /* --- label1 @@ -496,18 +576,18 @@ display_prop_diffs(const apr_array_header_t *propchanges, } SVN_ERR(svn_stream_printf_from_utf8(outstream, encoding, scratch_pool, - _("%sProperty changes on: %s%s"), + APR_EOL_STR + "Property changes on: %s" APR_EOL_STR, use_git_diff_format ? repos_relpath1 - : index_path, - APR_EOL_STR)); + : index_path)); SVN_ERR(svn_stream_printf_from_utf8(outstream, encoding, scratch_pool, SVN_DIFF__UNDER_STRING APR_EOL_STR)); SVN_ERR(svn_diff__display_prop_diffs( - outstream, encoding, propchanges, original_props, + outstream, encoding, propchanges, left_props, TRUE /* pretty_print_mergeinfo */, -1 /* context_size */, cancel_func, cancel_baton, scratch_pool)); @@ -606,7 +686,8 @@ diff_props_changed(const char *diff_relpath, svn_revnum_t rev1, svn_revnum_t rev2, const apr_array_header_t *propchanges, - apr_hash_t *original_props, + apr_hash_t *left_props, + apr_hash_t *right_props, svn_boolean_t show_diff_header, diff_writer_info_t *dwi, apr_pool_t *scratch_pool) @@ -625,7 +706,7 @@ diff_props_changed(const char *diff_relpath, /* We're using the revnums from the dwi since there's * no revision argument to the svn_wc_diff_callback_t * dir_props_changed(). */ - SVN_ERR(display_prop_diffs(props, original_props, + SVN_ERR(display_prop_diffs(props, left_props, right_props, diff_relpath, dwi->ddi.anchor, dwi->ddi.orig_path_1, @@ -647,6 +728,62 @@ diff_props_changed(const char *diff_relpath, return SVN_NO_ERROR; } +/* Given a file ORIG_TMPFILE, return a path to a temporary file that lives at + * least as long as RESULT_POOL, containing the git-like represention of + * ORIG_TMPFILE */ +static svn_error_t * +transform_link_to_git(const char **new_tmpfile, + const char **git_sha1, + const char *orig_tmpfile, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_file_t *orig; + apr_file_t *gitlike; + svn_stringbuf_t *line; + + *git_sha1 = NULL; + + SVN_ERR(svn_io_file_open(&orig, orig_tmpfile, APR_READ, APR_OS_DEFAULT, + scratch_pool)); + SVN_ERR(svn_io_open_unique_file3(&gitlike, new_tmpfile, NULL, + svn_io_file_del_on_pool_cleanup, + result_pool, scratch_pool)); + + SVN_ERR(svn_io_file_readline(orig, &line, NULL, NULL, 2 * APR_PATH_MAX + 2, + scratch_pool, scratch_pool)); + + if (line->len > 5 && !strncmp(line->data, "link ", 5)) + { + const char *sz_str; + svn_checksum_t *checksum; + + svn_stringbuf_remove(line, 0, 5); + + SVN_ERR(svn_io_file_write_full(gitlike, line->data, line->len, + NULL, scratch_pool)); + + /* git calculates the sha over "blob X\0" + the actual data, + where X is the decimal size of the blob. */ + sz_str = apr_psprintf(scratch_pool, "blob %u", (unsigned int)line->len); + svn_stringbuf_insert(line, 0, sz_str, strlen(sz_str) + 1); + + SVN_ERR(svn_checksum(&checksum, svn_checksum_sha1, + line->data, line->len, scratch_pool)); + + *git_sha1 = svn_checksum_to_cstring(checksum, result_pool); + } + else + { + /* Not a link... so can't convert */ + *new_tmpfile = apr_pstrdup(result_pool, orig_tmpfile); + } + + SVN_ERR(svn_io_file_close(orig, scratch_pool)); + SVN_ERR(svn_io_file_close(gitlike, scratch_pool)); + return SVN_NO_ERROR; +} + /* Show differences between TMPFILE1 and TMPFILE2. DIFF_RELPATH, REV1, and REV2 are used in the headers to indicate the file and revisions. If either MIMETYPE1 or MIMETYPE2 indicate binary content, don't show a diff, @@ -662,8 +799,8 @@ diff_content_changed(svn_boolean_t *wrote_header, const char *tmpfile2, svn_revnum_t rev1, svn_revnum_t rev2, - const char *mimetype1, - const char *mimetype2, + apr_hash_t *left_props, + apr_hash_t *right_props, svn_diff_operation_kind_t operation, svn_boolean_t force_diff, const char *copyfrom_path, @@ -678,6 +815,9 @@ diff_content_changed(svn_boolean_t *wrote_header, const char *index_path = diff_relpath; const char *path1 = dwi->ddi.orig_path_1; const char *path2 = dwi->ddi.orig_path_2; + const char *mimetype1 = svn_prop_get_value(left_props, SVN_PROP_MIME_TYPE); + const char *mimetype2 = svn_prop_get_value(right_props, SVN_PROP_MIME_TYPE); + const char *index_shas = NULL; /* If only property differences are shown, there's nothing to do. */ if (dwi->properties_only) @@ -699,6 +839,33 @@ diff_content_changed(svn_boolean_t *wrote_header, if (mimetype2) mt2_binary = svn_mime_type_is_binary(mimetype2); + if (dwi->use_git_diff_format) + { + const char *l_hash = NULL; + const char *r_hash = NULL; + + /* Change symlinks to their 'git like' plain format */ + if (svn_prop_get_value(left_props, SVN_PROP_SPECIAL)) + SVN_ERR(transform_link_to_git(&tmpfile1, &l_hash, tmpfile1, + scratch_pool, scratch_pool)); + if (svn_prop_get_value(right_props, SVN_PROP_SPECIAL)) + SVN_ERR(transform_link_to_git(&tmpfile2, &r_hash, tmpfile2, + scratch_pool, scratch_pool)); + + if (l_hash && r_hash) + { + /* The symlink has changed. But we can't tell the user of the + diff whether we are writing git diffs or svn diffs of the + symlink... except when we add a git-like index line */ + + l_hash = apr_pstrndup(scratch_pool, l_hash, 8); + r_hash = apr_pstrndup(scratch_pool, r_hash, 8); + + index_shas = apr_psprintf(scratch_pool, "%8s..%8s", + l_hash, r_hash); + } + } + if (! dwi->force_binary && (mt1_binary || mt2_binary)) { /* Print out the diff header. */ @@ -708,6 +875,8 @@ diff_content_changed(svn_boolean_t *wrote_header, SVN_DIFF__EQUAL_STRING APR_EOL_STR, index_path)); + *wrote_header = TRUE; + /* ### Print git diff headers. */ if (dwi->use_git_diff_format) @@ -716,6 +885,7 @@ diff_content_changed(svn_boolean_t *wrote_header, svn_stream_t *right_stream; const char *repos_relpath1; const char *repos_relpath2; + const char *copyfrom_repos_relpath = NULL; SVN_ERR(make_repos_relpath(&repos_relpath1, diff_relpath, dwi->ddi.orig_path_1, @@ -729,12 +899,22 @@ diff_content_changed(svn_boolean_t *wrote_header, dwi->wc_ctx, dwi->ddi.anchor, scratch_pool, scratch_pool)); + if (copyfrom_path) + SVN_ERR(make_repos_relpath(©from_repos_relpath, copyfrom_path, + dwi->ddi.orig_path_2, + dwi->ddi.session_relpath, + dwi->wc_ctx, + dwi->ddi.anchor, + scratch_pool, scratch_pool)); SVN_ERR(print_git_diff_header(outstream, &label1, &label2, operation, repos_relpath1, repos_relpath2, rev1, rev2, - copyfrom_path, + copyfrom_repos_relpath, copyfrom_rev, + left_props, + right_props, + index_shas, dwi->header_encoding, scratch_pool)); @@ -878,6 +1058,8 @@ diff_content_changed(svn_boolean_t *wrote_header, { const char *repos_relpath1; const char *repos_relpath2; + const char *copyfrom_repos_relpath = NULL; + SVN_ERR(make_repos_relpath(&repos_relpath1, diff_relpath, dwi->ddi.orig_path_1, dwi->ddi.session_relpath, @@ -890,12 +1072,23 @@ diff_content_changed(svn_boolean_t *wrote_header, dwi->wc_ctx, dwi->ddi.anchor, scratch_pool, scratch_pool)); + if (copyfrom_path) + SVN_ERR(make_repos_relpath(©from_repos_relpath, + copyfrom_path, + dwi->ddi.orig_path_2, + dwi->ddi.session_relpath, + dwi->wc_ctx, + dwi->ddi.anchor, + scratch_pool, scratch_pool)); SVN_ERR(print_git_diff_header(outstream, &label1, &label2, operation, repos_relpath1, repos_relpath2, rev1, rev2, - copyfrom_path, + copyfrom_repos_relpath, copyfrom_rev, + left_props, + right_props, + index_shas, dwi->header_encoding, scratch_pool)); } @@ -916,10 +1109,6 @@ diff_content_changed(svn_boolean_t *wrote_header, } } - /* ### todo: someday we'll need to worry about whether we're going - to need to write a diff plug-in mechanism that makes use of the - two paths, instead of just blindly running SVN_CLIENT_DIFF. */ - return SVN_NO_ERROR; } @@ -946,10 +1135,7 @@ diff_file_changed(const char *relpath, left_file, right_file, left_source->revision, right_source->revision, - svn_prop_get_value(left_props, - SVN_PROP_MIME_TYPE), - svn_prop_get_value(right_props, - SVN_PROP_MIME_TYPE), + left_props, right_props, svn_diff_op_modified, FALSE, NULL, SVN_INVALID_REVNUM, dwi, @@ -958,7 +1144,7 @@ diff_file_changed(const char *relpath, SVN_ERR(diff_props_changed(relpath, left_source->revision, right_source->revision, prop_changes, - left_props, !wrote_header, + left_props, right_props, !wrote_header, dwi, scratch_pool)); return SVN_NO_ERROR; } @@ -986,6 +1172,23 @@ diff_file_added(const char *relpath, apr_hash_t *left_props; apr_array_header_t *prop_changes; + if (dwi->no_diff_added) + { + const char *index_path = relpath; + + if (dwi->ddi.anchor) + index_path = svn_dirent_join(dwi->ddi.anchor, relpath, + scratch_pool); + + SVN_ERR(svn_stream_printf_from_utf8(dwi->outstream, + dwi->header_encoding, scratch_pool, + "Index: %s (added)" APR_EOL_STR + SVN_DIFF__EQUAL_STRING APR_EOL_STR, + index_path)); + wrote_header = TRUE; + return SVN_NO_ERROR; + } + /* During repos->wc diff of a copy revision numbers obtained * from the working copy are always SVN_INVALID_REVNUM. */ if (copyfrom_source && !dwi->show_copies_as_adds) @@ -1009,33 +1212,19 @@ diff_file_added(const char *relpath, SVN_ERR(svn_prop_diffs(&prop_changes, right_props, left_props, scratch_pool)); - if (dwi->no_diff_added) - { - const char *index_path = relpath; - - if (dwi->ddi.anchor) - index_path = svn_dirent_join(dwi->ddi.anchor, relpath, - scratch_pool); - - SVN_ERR(svn_stream_printf_from_utf8(dwi->outstream, - dwi->header_encoding, scratch_pool, - "Index: %s (added)" APR_EOL_STR - SVN_DIFF__EQUAL_STRING APR_EOL_STR, - index_path)); - wrote_header = TRUE; - } - else if (copyfrom_source && right_file) + if (copyfrom_source && right_file) SVN_ERR(diff_content_changed(&wrote_header, relpath, left_file, right_file, copyfrom_source->revision, right_source->revision, - svn_prop_get_value(left_props, - SVN_PROP_MIME_TYPE), - svn_prop_get_value(right_props, - SVN_PROP_MIME_TYPE), - svn_diff_op_copied, + left_props, right_props, + copyfrom_source->moved_from_relpath + ? svn_diff_op_moved + : svn_diff_op_copied, TRUE /* force diff output */, - copyfrom_source->repos_relpath, + copyfrom_source->moved_from_relpath + ? copyfrom_source->moved_from_relpath + : copyfrom_source->repos_relpath, copyfrom_source->revision, dwi, scratch_pool)); else if (right_file) @@ -1043,10 +1232,7 @@ diff_file_added(const char *relpath, left_file, right_file, DIFF_REVNUM_NONEXISTENT, right_source->revision, - svn_prop_get_value(left_props, - SVN_PROP_MIME_TYPE), - svn_prop_get_value(right_props, - SVN_PROP_MIME_TYPE), + left_props, right_props, svn_diff_op_added, TRUE /* force diff output */, NULL, SVN_INVALID_REVNUM, @@ -1058,8 +1244,8 @@ diff_file_added(const char *relpath, : DIFF_REVNUM_NONEXISTENT, right_source->revision, prop_changes, - left_props, ! wrote_header, - dwi, scratch_pool)); + left_props, right_props, + ! wrote_header, dwi, scratch_pool)); return SVN_NO_ERROR; } @@ -1104,8 +1290,7 @@ diff_file_deleted(const char *relpath, left_file, dwi->empty_file, left_source->revision, DIFF_REVNUM_NONEXISTENT, - svn_prop_get_value(left_props, - SVN_PROP_MIME_TYPE), + left_props, NULL, svn_diff_op_deleted, FALSE, NULL, SVN_INVALID_REVNUM, @@ -1123,8 +1308,8 @@ diff_file_deleted(const char *relpath, left_source->revision, DIFF_REVNUM_NONEXISTENT, prop_changes, - left_props, ! wrote_header, - dwi, scratch_pool)); + left_props, NULL, + ! wrote_header, dwi, scratch_pool)); } } @@ -1149,7 +1334,7 @@ diff_dir_changed(const char *relpath, left_source->revision, right_source->revision, prop_changes, - left_props, + left_props, right_props, TRUE /* show_diff_header */, dwi, scratch_pool)); @@ -1194,7 +1379,7 @@ diff_dir_added(const char *relpath, : DIFF_REVNUM_NONEXISTENT, right_source->revision, prop_changes, - left_props, + left_props, right_props, TRUE /* show_diff_header */, dwi, scratch_pool)); @@ -1211,19 +1396,20 @@ diff_dir_deleted(const char *relpath, { diff_writer_info_t *dwi = processor->baton; apr_array_header_t *prop_changes; + apr_hash_t *right_props; if (dwi->no_diff_deleted) return SVN_NO_ERROR; - - SVN_ERR(svn_prop_diffs(&prop_changes, apr_hash_make(scratch_pool), + right_props = apr_hash_make(scratch_pool); + SVN_ERR(svn_prop_diffs(&prop_changes, right_props, left_props, scratch_pool)); SVN_ERR(diff_props_changed(relpath, left_source->revision, DIFF_REVNUM_NONEXISTENT, prop_changes, - left_props, + left_props, right_props, TRUE /* show_diff_header */, dwi, scratch_pool)); @@ -1852,11 +2038,15 @@ diff_repos_repos(const char **root_relpath, /* Perform a diff between a repository path and a working-copy path. PATH_OR_URL1 may be either a URL or a working copy path. PATH2 is a - working copy path. REVISION1 and REVISION2 are their respective - revisions. If REVERSE is TRUE, the diff will be done in reverse. - If PEG_REVISION is specified, then PATH_OR_URL1 is the path in the peg - revision, and the actual repository path to be compared is - determined by following copy history. + working copy path. REVISION1 is the revision of URL1. If PEG_REVISION1 + is specified, then PATH_OR_URL1 is the path in the peg revision, and the + actual repository path to be compared is determined by following copy + history. + + REVISION_KIND2 specifies which revision should be reported from the + working copy (BASE or WORKING) + + If REVERSE is TRUE, the diff will be reported in reverse. All other options are the same as those passed to svn_client_diff6(). */ static svn_error_t * @@ -1865,9 +2055,9 @@ diff_repos_wc(const char **root_relpath, struct diff_driver_info_t *ddi, const char *path_or_url1, const svn_opt_revision_t *revision1, - const svn_opt_revision_t *peg_revision, + const svn_opt_revision_t *peg_revision1, const char *path2, - const svn_opt_revision_t *revision2, + enum svn_opt_revision_kind revision2_kind, svn_boolean_t reverse, svn_depth_t depth, svn_boolean_t ignore_ancestry, @@ -1884,7 +2074,7 @@ diff_repos_wc(const char **root_relpath, void *reporter_baton; const svn_delta_editor_t *diff_editor; void *diff_edit_baton; - svn_boolean_t rev2_is_base = (revision2->kind == svn_opt_revision_base); + svn_boolean_t rev2_is_base = (revision2_kind == svn_opt_revision_base); svn_boolean_t server_supports_depth; const char *abspath_or_url1; const char *abspath2; @@ -1924,10 +2114,10 @@ diff_repos_wc(const char **root_relpath, SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc1, path_or_url1, abspath2, - peg_revision, revision1, + peg_revision1, revision1, ctx, scratch_pool)); - if (revision2->kind == svn_opt_revision_base || !is_copy) + if (revision2_kind == svn_opt_revision_base || !is_copy) { /* Convert path_or_url1 to a URL to feed to do_diff. */ SVN_ERR(svn_wc_get_actual_target2(&anchor, &target, ctx->wc_ctx, path2, @@ -2069,22 +2259,20 @@ diff_repos_wc(const char **root_relpath, else diff_depth = svn_depth_unknown; - - - if (is_copy && revision2->kind != svn_opt_revision_base) + /* Tell the RA layer we want a delta to change our txn to URL1 */ + SVN_ERR(svn_ra_do_diff3(ra_session, + &reporter, &reporter_baton, + loc1->rev, + target, + diff_depth, + ignore_ancestry, + TRUE, /* text_deltas */ + loc1->url, + diff_editor, diff_edit_baton, + scratch_pool)); + + if (is_copy && revision2_kind != svn_opt_revision_base) { - /* Tell the RA layer we want a delta to change our txn to URL1 */ - SVN_ERR(svn_ra_do_diff3(ra_session, - &reporter, &reporter_baton, - loc1->rev, - target, - diff_depth, - ignore_ancestry, - TRUE, /* text_deltas */ - loc1->url, - diff_editor, diff_edit_baton, - scratch_pool)); - /* Report the copy source. */ if (cf_depth == svn_depth_unknown) cf_depth = svn_depth_infinity; @@ -2108,18 +2296,6 @@ diff_repos_wc(const char **root_relpath, } else { - /* Tell the RA layer we want a delta to change our txn to URL1 */ - SVN_ERR(svn_ra_do_diff3(ra_session, - &reporter, &reporter_baton, - loc1->rev, - target, - diff_depth, - ignore_ancestry, - TRUE, /* text_deltas */ - loc1->url, - diff_editor, diff_edit_baton, - scratch_pool)); - /* Create a txn mirror of path2; the diff editor will print diffs in reverse. :-) */ SVN_ERR(svn_wc_crawl_revisions5(ctx->wc_ctx, abspath2, @@ -2146,6 +2322,7 @@ do_diff(const char **root_relpath, const svn_opt_revision_t *revision1, const svn_opt_revision_t *revision2, const svn_opt_revision_t *peg_revision, + svn_boolean_t no_peg_revision, svn_depth_t depth, svn_boolean_t ignore_ancestry, const apr_array_header_t *changelists, @@ -2166,7 +2343,7 @@ do_diff(const char **root_relpath, { if (is_repos2) { - /* ### Ignores 'show_copies_as_adds'. */ + /* Ignores changelists. */ SVN_ERR(diff_repos_repos(root_relpath, root_is_dir, ddi, path_or_url1, path_or_url2, @@ -2179,8 +2356,11 @@ do_diff(const char **root_relpath, else /* path_or_url2 is a working copy path */ { SVN_ERR(diff_repos_wc(root_relpath, root_is_dir, ddi, - path_or_url1, revision1, peg_revision, - path_or_url2, revision2, FALSE, depth, + path_or_url1, revision1, + no_peg_revision ? revision1 + : peg_revision, + path_or_url2, revision2->kind, + FALSE, depth, ignore_ancestry, changelists, diff_processor, ctx, result_pool, scratch_pool)); @@ -2191,8 +2371,12 @@ do_diff(const char **root_relpath, if (is_repos2) { SVN_ERR(diff_repos_wc(root_relpath, root_is_dir, ddi, - path_or_url2, revision2, peg_revision, - path_or_url1, revision1, TRUE, depth, + path_or_url2, revision2, + no_peg_revision ? revision2 + : peg_revision, + path_or_url1, + revision1->kind, + TRUE, depth, ignore_ancestry, changelists, diff_processor, ctx, result_pool, scratch_pool)); @@ -2211,7 +2395,7 @@ do_diff(const char **root_relpath, scratch_pool)); /* ### What about ddi? */ - + /* Ignores changelists, ignore_ancestry */ SVN_ERR(svn_client__arbitrary_nodes_diff(root_relpath, root_is_dir, abspath1, abspath2, depth, @@ -2415,7 +2599,8 @@ svn_client_diff6(const apr_array_header_t *options, return svn_error_trace(do_diff(NULL, NULL, &dwi.ddi, path_or_url1, path_or_url2, - revision1, revision2, &peg_revision, + revision1, revision2, + &peg_revision, TRUE /* no_peg_revision */, depth, ignore_ancestry, changelists, TRUE /* text_deltas */, diff_processor, ctx, pool, pool)); @@ -2498,7 +2683,8 @@ svn_client_diff_peg6(const apr_array_header_t *options, return svn_error_trace(do_diff(NULL, NULL, &dwi.ddi, path_or_url, path_or_url, - start_revision, end_revision, peg_revision, + start_revision, end_revision, + peg_revision, FALSE /* no_peg_revision */, depth, ignore_ancestry, changelists, TRUE /* text_deltas */, diff_processor, ctx, pool, pool)); @@ -2531,7 +2717,8 @@ svn_client_diff_summarize2(const char *path_or_url1, return svn_error_trace(do_diff(p_root_relpath, NULL, NULL, path_or_url1, path_or_url2, - revision1, revision2, &peg_revision, + revision1, revision2, + &peg_revision, TRUE /* no_peg_revision */, depth, ignore_ancestry, changelists, FALSE /* text_deltas */, diff_processor, ctx, pool, pool)); @@ -2560,7 +2747,8 @@ svn_client_diff_summarize_peg2(const char *path_or_url, return svn_error_trace(do_diff(p_root_relpath, NULL, NULL, path_or_url, path_or_url, - start_revision, end_revision, peg_revision, + start_revision, end_revision, + peg_revision, FALSE /* no_peg_revision */, depth, ignore_ancestry, changelists, FALSE /* text_deltas */, diff_processor, ctx, pool, pool)); diff --git a/subversion/libsvn_client/diff_local.c b/subversion/libsvn_client/diff_local.c index df6bc0a8c009..056ee53e8f28 100644 --- a/subversion/libsvn_client/diff_local.c +++ b/subversion/libsvn_client/diff_local.c @@ -179,7 +179,7 @@ inner_dir_diff(const char *left_abspath, { svn_error_clear(err); right_dirents = apr_hash_make(scratch_pool); - right_only = TRUE; + left_only = TRUE; } else SVN_ERR(err); @@ -669,9 +669,6 @@ svn_client__arbitrary_nodes_diff(const char **root_relpath, SVN_ERR(svn_io_check_resolved_path(left_abspath, &left_kind, scratch_pool)); SVN_ERR(svn_io_check_resolved_path(right_abspath, &right_kind, scratch_pool)); - if (depth == svn_depth_unknown) - depth = svn_depth_infinity; - if (left_kind == svn_node_dir && right_kind == svn_node_dir) { left_root_abspath = left_abspath; diff --git a/subversion/libsvn_client/export.c b/subversion/libsvn_client/export.c index 63cd87a921b0..2fc2dc8ffd53 100644 --- a/subversion/libsvn_client/export.c +++ b/subversion/libsvn_client/export.c @@ -432,7 +432,7 @@ export_node(void *baton, scratch_pool)); /* Now that dst_tmp contains the translated data, do the atomic rename. */ - SVN_ERR(svn_io_file_rename(dst_tmp, to_abspath, scratch_pool)); + SVN_ERR(svn_io_file_rename2(dst_tmp, to_abspath, FALSE, scratch_pool)); if (eib->notify_func) { @@ -816,7 +816,7 @@ close_file(void *file_baton, if ((! fb->eol_style_val) && (! fb->keywords_val) && (! fb->special)) { - SVN_ERR(svn_io_file_rename(fb->tmppath, fb->path, pool)); + SVN_ERR(svn_io_file_rename2(fb->tmppath, fb->path, FALSE, pool)); } else { @@ -1035,7 +1035,7 @@ add_file_ev2(void *baton, eb->cancel_baton, scratch_pool)); /* Move the file into place. */ - SVN_ERR(svn_io_file_rename(tmppath, full_path, scratch_pool)); + SVN_ERR(svn_io_file_rename2(tmppath, full_path, FALSE, scratch_pool)); } if (executable_val) @@ -1148,7 +1148,7 @@ get_editor_ev2(const svn_delta_editor_t **export_editor, } static svn_error_t * -export_file_ev2(const char *from_path_or_url, +export_file_ev2(const char *from_url, const char *to_path, struct edit_baton *eb, svn_client__pathrev_t *loc, @@ -1156,23 +1156,21 @@ export_file_ev2(const char *from_path_or_url, svn_boolean_t overwrite, apr_pool_t *scratch_pool) { - svn_boolean_t from_is_url = svn_path_is_url(from_path_or_url); apr_hash_t *props; svn_stream_t *tmp_stream; svn_node_kind_t to_kind; + SVN_ERR_ASSERT(svn_path_is_url(from_url)); + if (svn_path_is_empty(to_path)) { - if (from_is_url) - to_path = svn_uri_basename(from_path_or_url, scratch_pool); - else - to_path = svn_dirent_basename(from_path_or_url, NULL); + to_path = svn_uri_basename(from_url, scratch_pool); eb->root_path = to_path; } else { - SVN_ERR(append_basename_if_dir(&to_path, from_path_or_url, - from_is_url, scratch_pool)); + SVN_ERR(append_basename_if_dir(&to_path, from_url, + TRUE, scratch_pool)); eb->root_path = to_path; } @@ -1204,7 +1202,7 @@ export_file_ev2(const char *from_path_or_url, } static svn_error_t * -export_file(const char *from_path_or_url, +export_file(const char *from_url, const char *to_path, struct edit_baton *eb, svn_client__pathrev_t *loc, @@ -1216,20 +1214,18 @@ export_file(const char *from_path_or_url, apr_hash_index_t *hi; struct file_baton *fb = apr_pcalloc(scratch_pool, sizeof(*fb)); svn_node_kind_t to_kind; - svn_boolean_t from_is_url = svn_path_is_url(from_path_or_url); + + SVN_ERR_ASSERT(svn_path_is_url(from_url)); if (svn_path_is_empty(to_path)) { - if (from_is_url) - to_path = svn_uri_basename(from_path_or_url, scratch_pool); - else - to_path = svn_dirent_basename(from_path_or_url, NULL); + to_path = svn_uri_basename(from_url, scratch_pool); eb->root_path = to_path; } else { - SVN_ERR(append_basename_if_dir(&to_path, from_path_or_url, - from_is_url, scratch_pool)); + SVN_ERR(append_basename_if_dir(&to_path, from_url, + TRUE, scratch_pool)); eb->root_path = to_path; } @@ -1288,7 +1284,7 @@ export_file(const char *from_path_or_url, } static svn_error_t * -export_directory(const char *from_path_or_url, +export_directory(const char *from_url, const char *to_path, struct edit_baton *eb, svn_client__pathrev_t *loc, @@ -1307,6 +1303,8 @@ export_directory(const char *from_path_or_url, void *report_baton; svn_node_kind_t kind; + SVN_ERR_ASSERT(svn_path_is_url(from_url)); + if (!ENABLE_EV2_IMPL) SVN_ERR(get_editor_ev1(&export_editor, &edit_baton, eb, ctx, scratch_pool, scratch_pool)); @@ -1355,7 +1353,7 @@ export_directory(const char *from_path_or_url, SVN_ERR(svn_dirent_get_absolute(&to_abspath, to_path, scratch_pool)); SVN_ERR(svn_client__export_externals(eb->externals, - from_path_or_url, + from_url, to_abspath, eb->repos_root_url, depth, native_eol, ignore_keywords, @@ -1402,8 +1400,12 @@ svn_client_export5(svn_revnum_t *result_rev, svn_client__pathrev_t *loc; svn_ra_session_t *ra_session; svn_node_kind_t kind; + const char *from_url; struct edit_baton *eb = apr_pcalloc(pool, sizeof(*eb)); + SVN_ERR(svn_client_url_from_path2(&from_url, from_path_or_url, + ctx, pool, pool)); + /* Get the RA connection. */ SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc, from_path_or_url, NULL, @@ -1428,15 +1430,15 @@ svn_client_export5(svn_revnum_t *result_rev, if (kind == svn_node_file) { if (!ENABLE_EV2_IMPL) - SVN_ERR(export_file(from_path_or_url, to_path, eb, loc, ra_session, + SVN_ERR(export_file(from_url, to_path, eb, loc, ra_session, overwrite, pool)); else - SVN_ERR(export_file_ev2(from_path_or_url, to_path, eb, loc, + SVN_ERR(export_file_ev2(from_url, to_path, eb, loc, ra_session, overwrite, pool)); } else if (kind == svn_node_dir) { - SVN_ERR(export_directory(from_path_or_url, to_path, + SVN_ERR(export_directory(from_url, to_path, eb, loc, ra_session, overwrite, ignore_externals, ignore_keywords, depth, native_eol, ctx, pool)); @@ -1508,7 +1510,7 @@ svn_client_export5(svn_revnum_t *result_rev, eib.ignore_keywords = ignore_keywords; eib.wc_ctx = ctx->wc_ctx; eib.native_eol = native_eol; - eib.notify_func = ctx->notify_func2;; + eib.notify_func = ctx->notify_func2; eib.notify_baton = ctx->notify_baton2; eib.origin_abspath = from_path_or_url; eib.exported = FALSE; diff --git a/subversion/libsvn_client/externals.c b/subversion/libsvn_client/externals.c index 851b260dec4c..7ff1ed25d80d 100644 --- a/subversion/libsvn_client/externals.c +++ b/subversion/libsvn_client/externals.c @@ -120,7 +120,7 @@ relegate_dir_external(svn_wc_context_t *wc_ctx, /* And if it is no longer a working copy, we should just rename it */ - err = svn_io_file_rename(local_abspath, new_path, scratch_pool); + err = svn_io_file_rename2(local_abspath, new_path, FALSE, scratch_pool); } /* ### TODO: We should notify the user about the rename */ @@ -534,7 +534,7 @@ switch_file_external(const char *local_abspath, SVN_ERR(svn_io_check_path(local_abspath, &disk_kind, scratch_pool)); - if (kind == svn_node_file || kind == svn_node_dir) + if (disk_kind == svn_node_file || disk_kind == svn_node_dir) return svn_error_createf(SVN_ERR_WC_PATH_FOUND, NULL, _("The file external '%s' can not be " "created because the node exists."), @@ -574,6 +574,8 @@ switch_file_external(const char *local_abspath, record_url, record_peg_revision, record_revision, + ctx->conflict_func2, + ctx->conflict_baton2, ctx->cancel_func, ctx->cancel_baton, ctx->notify_func2, diff --git a/subversion/libsvn_client/import.c b/subversion/libsvn_client/import.c index b5ee0776def7..ae67e9ad43b5 100644 --- a/subversion/libsvn_client/import.c +++ b/subversion/libsvn_client/import.c @@ -73,32 +73,59 @@ typedef struct import_ctx_t apr_hash_t *autoprops; } import_ctx_t; +typedef struct open_txdelta_stream_baton_t +{ + svn_boolean_t need_reset; + svn_stream_t *stream; +} open_txdelta_stream_baton_t; + +/* Implements svn_txdelta_stream_open_func_t */ +static svn_error_t * +open_txdelta_stream(svn_txdelta_stream_t **txdelta_stream_p, + void *baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + open_txdelta_stream_baton_t *b = baton; + + if (b->need_reset) + { + /* Under rare circumstances, we can be restarted and would need to + * supply the delta stream again. In this case, reset the base + * stream. */ + SVN_ERR(svn_stream_reset(b->stream)); + } + + /* Get the delta stream (delta against the empty string). */ + svn_txdelta2(txdelta_stream_p, svn_stream_empty(result_pool), + b->stream, FALSE, result_pool); + b->need_reset = TRUE; + return SVN_NO_ERROR; +} /* Apply LOCAL_ABSPATH's contents (as a delta against the empty string) to FILE_BATON in EDITOR. Use POOL for any temporary allocation. PROPERTIES is the set of node properties set on this file. - Fill DIGEST with the md5 checksum of the sent file; DIGEST must be - at least APR_MD5_DIGESTSIZE bytes long. */ + Return the resulting checksum in *RESULT_MD5_CHECKSUM_P. */ /* ### how does this compare against svn_wc_transmit_text_deltas2() ??? */ static svn_error_t * -send_file_contents(const char *local_abspath, +send_file_contents(svn_checksum_t **result_md5_checksum_p, + const char *local_abspath, void *file_baton, const svn_delta_editor_t *editor, apr_hash_t *properties, - unsigned char *digest, apr_pool_t *pool) { svn_stream_t *contents; - svn_txdelta_window_handler_t handler; - void *handler_baton; const svn_string_t *eol_style_val = NULL, *keywords_val = NULL; svn_boolean_t special = FALSE; svn_subst_eol_style_t eol_style; const char *eol; apr_hash_t *keywords; + open_txdelta_stream_baton_t baton = { 0 }; /* If there are properties, look for EOL-style and keywords ones. */ if (properties) @@ -111,10 +138,6 @@ send_file_contents(const char *local_abspath, special = TRUE; } - /* Get an editor func that wants to consume the delta stream. */ - SVN_ERR(editor->apply_textdelta(file_baton, NULL, pool, - &handler, &handler_baton)); - if (eol_style_val) svn_subst_eol_style_from_value(&eol_style, &eol, eol_style_val->data); else @@ -168,10 +191,17 @@ send_file_contents(const char *local_abspath, } } - /* Send the file's contents to the delta-window handler. */ - return svn_error_trace(svn_txdelta_send_stream(contents, handler, - handler_baton, digest, - pool)); + /* Arrange the stream to calculate the resulting MD5. */ + contents = svn_stream_checksummed2(contents, result_md5_checksum_p, NULL, + svn_checksum_md5, TRUE, pool); + /* Send the contents. */ + baton.need_reset = FALSE; + baton.stream = svn_stream_disown(contents, pool); + SVN_ERR(editor->apply_textdelta_stream(editor, file_baton, NULL, + open_txdelta_stream, &baton, pool)); + SVN_ERR(svn_stream_close(contents)); + + return SVN_NO_ERROR; } @@ -198,7 +228,7 @@ import_file(const svn_delta_editor_t *editor, { void *file_baton; const char *mimetype = NULL; - unsigned char digest[APR_MD5_DIGESTSIZE]; + svn_checksum_t *result_md5_checksum; const char *text_checksum; apr_hash_t* properties; apr_hash_index_t *hi; @@ -262,13 +292,11 @@ import_file(const svn_delta_editor_t *editor, } /* Now, transmit the file contents. */ - SVN_ERR(send_file_contents(local_abspath, file_baton, editor, - properties, digest, pool)); + SVN_ERR(send_file_contents(&result_md5_checksum, local_abspath, + file_baton, editor, properties, pool)); /* Finally, close the file. */ - text_checksum = - svn_checksum_to_cstring(svn_checksum__from_digest_md5(digest, pool), pool); - + text_checksum = svn_checksum_to_cstring(result_md5_checksum, pool); return svn_error_trace(editor->close_file(file_baton, text_checksum, pool)); } @@ -577,26 +605,26 @@ import_dir(const svn_delta_editor_t *editor, } -/* Recursively import PATH to a repository using EDITOR and - * EDIT_BATON. PATH can be a file or directory. +/* Recursively import LOCAL_ABSPATH to a repository using EDITOR and + * EDIT_BATON. LOCAL_ABSPATH can be a file or directory. * * Sets *UPDATED_REPOSITORY to TRUE when the repository was modified by * a successfull commit, otherwise to FALSE. * - * DEPTH is the depth at which to import PATH; it behaves as for - * svn_client_import4(). + * DEPTH is the depth at which to import LOCAL_ABSPATH; it behaves as for + * svn_client_import5(). * * BASE_REV is the revision to use for the root of the commit. We * checked the preconditions against this revision. * * NEW_ENTRIES is an ordered array of path components that must be * created in the repository (where the ordering direction is - * parent-to-child). If PATH is a directory, NEW_ENTRIES may be empty + * parent-to-child). If LOCAL_ABSPATH is a directory, NEW_ENTRIES may be empty * -- the result is an import which creates as many new entries in the * top repository target directory as there are importable entries in - * the top of PATH; but if NEW_ENTRIES is not empty, its last item is + * the top of LOCAL_ABSPATH; but if NEW_ENTRIES is not empty, its last item is * the name of a new subdirectory in the repository to hold the - * import. If PATH is a file, NEW_ENTRIES may not be empty, and its + * import. If LOCAL_ABSPATH is a file, NEW_ENTRIES may not be empty, and its * last item is the name used for the file in the repository. If * NEW_ENTRIES contains more than one item, all but the last item are * the names of intermediate directories that are created before the @@ -624,6 +652,8 @@ import_dir(const svn_delta_editor_t *editor, * If CTX->NOTIFY_FUNC is non-null, invoke it with CTX->NOTIFY_BATON for * each imported path, passing actions svn_wc_notify_commit_added. * + * URL is used only in the 'commit_finalizing' notification. + * * Use POOL for any temporary allocation. * * Note: the repository directory receiving the import was specified diff --git a/subversion/libsvn_client/info.c b/subversion/libsvn_client/info.c index 39b5eb112bab..3331647c9587 100644 --- a/subversion/libsvn_client/info.c +++ b/subversion/libsvn_client/info.c @@ -253,17 +253,17 @@ same_resource_in_head(svn_boolean_t *same_p, apr_pool_t *pool) { svn_error_t *err; - svn_opt_revision_t start_rev, peg_rev; + svn_opt_revision_t operative_rev, peg_rev; const char *head_url; - start_rev.kind = svn_opt_revision_head; - peg_rev.kind = svn_opt_revision_number; - peg_rev.value.number = rev; + peg_rev.kind = svn_opt_revision_head; + operative_rev.kind = svn_opt_revision_number; + operative_rev.value.number = rev; err = svn_client__repos_locations(&head_url, NULL, NULL, NULL, ra_session, url, &peg_rev, - &start_rev, NULL, + &operative_rev, NULL, ctx, pool); if (err && ((err->apr_err == SVN_ERR_CLIENT_UNRELATED_RESOURCES) || diff --git a/subversion/libsvn_client/list.c b/subversion/libsvn_client/list.c index c29b8bd68dc4..78433c337e58 100644 --- a/subversion/libsvn_client/list.c +++ b/subversion/libsvn_client/list.c @@ -21,6 +21,8 @@ * ==================================================================== */ +#include <apr_fnmatch.h> + #include "svn_client.h" #include "svn_dirent_uri.h" #include "svn_hash.h" @@ -35,12 +37,14 @@ #include "private/svn_fspath.h" #include "private/svn_ra_private.h" #include "private/svn_sorts_private.h" +#include "private/svn_utf_private.h" #include "private/svn_wc_private.h" #include "svn_private_config.h" /* Prototypes for referencing before declaration */ static svn_error_t * list_externals(apr_hash_t *externals, + const apr_array_header_t *patterns, svn_depth_t depth, apr_uint32_t dirent_fields, svn_boolean_t fetch_locks, @@ -53,6 +57,7 @@ static svn_error_t * list_internal(const char *path_or_url, const svn_opt_revision_t *peg_revision, const svn_opt_revision_t *revision, + const apr_array_header_t *patterns, svn_depth_t depth, apr_uint32_t dirent_fields, svn_boolean_t fetch_locks, @@ -64,6 +69,18 @@ list_internal(const char *path_or_url, svn_client_ctx_t *ctx, apr_pool_t *pool); +/* Return TRUE if S matches any of the const char * in PATTERNS. + * Note that any S will match if PATTERNS is empty. + * Use SCRATCH_BUFFER for temporary string contents. */ +static svn_boolean_t +match_patterns(const char *s, + const apr_array_header_t *patterns, + svn_membuf_t *scratch_buffer) +{ + return patterns + ? svn_utf__fuzzy_glob_match(s, patterns, scratch_buffer) + : TRUE; +} /* Get the directory entries of DIR at REV (relative to the root of RA_SESSION), getting at least the fields specified by DIRENT_FIELDS. @@ -75,6 +92,10 @@ list_internal(const char *path_or_url, if svn_depth_infinity, invoke it on file and directory entries and recurse into the directory entries with the same depth. + If PATTERNS is not empty, the last path segments must match at least + one of const char * patterns in it or the respective dirent will not + be reported. + LOCKS, if non-NULL, is a hash mapping const char * paths to svn_lock_t objects and FS_PATH is the absolute filesystem path of the RA session. Use SCRATCH_POOL for temporary allocations. @@ -86,6 +107,8 @@ list_internal(const char *path_or_url, EXTERNAL_PARENT_URL and EXTERNAL_TARGET are set when external items are listed, otherwise both are set to NULL by the caller. + + Use SCRATCH_BUFFER for temporary string contents. */ static svn_error_t * get_dir_contents(apr_uint32_t dirent_fields, @@ -94,6 +117,7 @@ get_dir_contents(apr_uint32_t dirent_fields, svn_ra_session_t *ra_session, apr_hash_t *locks, const char *fs_path, + const apr_array_header_t *patterns, svn_depth_t depth, svn_client_ctx_t *ctx, apr_hash_t *externals, @@ -101,6 +125,7 @@ get_dir_contents(apr_uint32_t dirent_fields, const char *external_target, svn_client_list_func2_t list_func, void *baton, + svn_membuf_t *scratch_buffer, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { @@ -175,23 +200,71 @@ get_dir_contents(apr_uint32_t dirent_fields, if (the_ent->kind == svn_node_file || depth == svn_depth_immediates || depth == svn_depth_infinity) - SVN_ERR(list_func(baton, path, the_ent, lock, fs_path, - external_parent_url, external_target, iterpool)); + if (match_patterns(item->key, patterns, scratch_buffer)) + SVN_ERR(list_func(baton, path, the_ent, lock, fs_path, + external_parent_url, external_target, iterpool)); /* If externals is non-NULL, populate the externals hash table recursively for all directory entries. */ if (depth == svn_depth_infinity && the_ent->kind == svn_node_dir) - SVN_ERR(get_dir_contents(dirent_fields, path, rev, - ra_session, locks, fs_path, depth, ctx, + SVN_ERR(get_dir_contents(dirent_fields, path, rev, ra_session, + locks, fs_path, patterns, depth, ctx, externals, external_parent_url, external_target, list_func, baton, - result_pool, iterpool)); + scratch_buffer, result_pool, iterpool)); } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } +/* Baton type to be used with list_receiver. */ +typedef struct receiver_baton_t +{ + /* Wrapped callback function to invoke. */ + svn_client_list_func2_t list_func; + + /* Baton to be used with LIST_FUNC. */ + void *list_baton; + + /* Client context providing cancellation support. */ + svn_client_ctx_t *ctx; + + /* All locks found for the whole tree; pick yours. */ + apr_hash_t *locks; + + /* Start path of the operation. */ + const char *fs_base_path; +} receiver_baton_t; + +/* Implement svn_ra_dirent_receiver_t. + The BATON type must be a receiver_baton_t. */ +static svn_error_t * +list_receiver(const char *rel_path, + svn_dirent_t *dirent, + void *baton, + apr_pool_t *pool) +{ + receiver_baton_t *b = baton; + const svn_lock_t *lock = NULL; + + /* We only report the path relative to the start path. */ + rel_path = svn_dirent_skip_ancestor(b->fs_base_path, rel_path); + + if (b->locks) + { + const char *abs_path = svn_dirent_join(b->fs_base_path, rel_path, pool); + lock = svn_hash_gets(b->locks, abs_path); + } + + if (b->ctx->cancel_func) + SVN_ERR(b->ctx->cancel_func(b->ctx->cancel_baton)); + + SVN_ERR(b->list_func(b->list_baton, rel_path, dirent, lock, + b->fs_base_path, NULL, NULL, pool)); + + return SVN_NO_ERROR; +} /* List the file/directory entries for PATH_OR_URL at REVISION. The actual node revision selected is determined by the path as @@ -204,6 +277,10 @@ get_dir_contents(apr_uint32_t dirent_fields, subdirectories (at svn_depth_empty). Else if DEPTH is svn_depth_empty, just list PATH_OR_URL with none of its entries. + If PATTERNS is not NULL, the last path segments must match at least + one of const char * patterns in it or the respective dirent will not + be reported. + DIRENT_FIELDS controls which fields in the svn_dirent_t's are filled in. To have them totally filled in use SVN_DIRENT_ALL, otherwise simply bitwise OR together the combination of SVN_DIRENT_* @@ -230,6 +307,7 @@ static svn_error_t * list_internal(const char *path_or_url, const svn_opt_revision_t *peg_revision, const svn_opt_revision_t *revision, + const apr_array_header_t *patterns, svn_depth_t depth, apr_uint32_t dirent_fields, svn_boolean_t fetch_locks, @@ -248,6 +326,7 @@ list_internal(const char *path_or_url, svn_error_t *err; apr_hash_t *locks; apr_hash_t *externals; + svn_membuf_t scratch_buffer; if (include_externals) externals = apr_hash_make(pool); @@ -266,12 +345,6 @@ list_internal(const char *path_or_url, fs_path = svn_client__pathrev_fspath(loc, pool); - SVN_ERR(svn_ra_stat(ra_session, "", loc->rev, &dirent, pool)); - if (! dirent) - return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, - _("URL '%s' non-existent in revision %ld"), - loc->url, loc->rev); - /* Maybe get all locks under url. */ if (fetch_locks) { @@ -290,20 +363,52 @@ list_internal(const char *path_or_url, else locks = NULL; + /* Try to use the efficient and fully authz-filtered code path. */ + if (!include_externals) + { + receiver_baton_t receiver_baton; + receiver_baton.list_baton = baton; + receiver_baton.ctx = ctx; + receiver_baton.list_func = list_func; + receiver_baton.locks = locks; + receiver_baton.fs_base_path = fs_path; + + err = svn_ra_list(ra_session, "", loc->rev, patterns, depth, + dirent_fields, list_receiver, &receiver_baton, pool); + + if (svn_error_find_cause(err, SVN_ERR_UNSUPPORTED_FEATURE)) + svn_error_clear(err); + else + return svn_error_trace(err); + } + + /* Stat for the file / directory node itself. */ + SVN_ERR(svn_ra_stat(ra_session, "", loc->rev, &dirent, pool)); + if (! dirent) + return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, + _("URL '%s' non-existent in revision %ld"), + loc->url, loc->rev); + + /* We need a scratch buffer for temporary string data. + * Create one with a reasonable initial size. */ + svn_membuf__create(&scratch_buffer, 256, pool); + /* Report the dirent for the target. */ - SVN_ERR(list_func(baton, "", dirent, locks - ? (svn_hash_gets(locks, fs_path)) - : NULL, fs_path, external_parent_url, - external_target, pool)); + if (match_patterns(svn_dirent_dirname(fs_path, pool), patterns, + &scratch_buffer)) + SVN_ERR(list_func(baton, "", dirent, locks + ? (svn_hash_gets(locks, fs_path)) + : NULL, fs_path, external_parent_url, + external_target, pool)); if (dirent->kind == svn_node_dir && (depth == svn_depth_files || depth == svn_depth_immediates || depth == svn_depth_infinity)) SVN_ERR(get_dir_contents(dirent_fields, "", loc->rev, ra_session, locks, - fs_path, depth, ctx, externals, + fs_path, patterns, depth, ctx, externals, external_parent_url, external_target, list_func, - baton, pool, pool)); + baton, &scratch_buffer, pool, pool)); /* We handle externals after listing entries under path_or_url, so that handling external items (and any errors therefrom) doesn't delay @@ -312,7 +417,7 @@ list_internal(const char *path_or_url, { /* The 'externals' hash populated by get_dir_contents() is processed here. */ - SVN_ERR(list_externals(externals, depth, dirent_fields, + SVN_ERR(list_externals(externals, patterns, depth, dirent_fields, fetch_locks, list_func, baton, ctx, pool)); } @@ -349,6 +454,7 @@ wrap_list_error(const svn_client_ctx_t *ctx, static svn_error_t * list_external_items(apr_array_header_t *external_items, const char *externals_parent_url, + const apr_array_header_t *patterns, svn_depth_t depth, apr_uint32_t dirent_fields, svn_boolean_t fetch_locks, @@ -389,6 +495,7 @@ list_external_items(apr_array_header_t *external_items, list_internal(resolved_url, &item->peg_revision, &item->revision, + patterns, depth, dirent_fields, fetch_locks, TRUE, @@ -411,6 +518,7 @@ list_external_items(apr_array_header_t *external_items, passed to svn_client_list(). */ static svn_error_t * list_externals(apr_hash_t *externals, + const apr_array_header_t *patterns, svn_depth_t depth, apr_uint32_t dirent_fields, svn_boolean_t fetch_locks, @@ -440,9 +548,10 @@ list_externals(apr_hash_t *externals, if (! external_items->nelts) continue; - SVN_ERR(list_external_items(external_items, externals_parent_url, depth, - dirent_fields, fetch_locks, list_func, - baton, ctx, iterpool)); + SVN_ERR(list_external_items(external_items, externals_parent_url, + patterns, depth, dirent_fields, + fetch_locks, list_func, baton, ctx, + iterpool)); } svn_pool_destroy(iterpool); @@ -452,9 +561,10 @@ list_externals(apr_hash_t *externals, svn_error_t * -svn_client_list3(const char *path_or_url, +svn_client_list4(const char *path_or_url, const svn_opt_revision_t *peg_revision, const svn_opt_revision_t *revision, + const apr_array_header_t *patterns, svn_depth_t depth, apr_uint32_t dirent_fields, svn_boolean_t fetch_locks, @@ -462,14 +572,14 @@ svn_client_list3(const char *path_or_url, svn_client_list_func2_t list_func, void *baton, svn_client_ctx_t *ctx, - apr_pool_t *pool) + apr_pool_t *scratch_pool) { return svn_error_trace(list_internal(path_or_url, peg_revision, - revision, + revision, patterns, depth, dirent_fields, fetch_locks, include_externals, NULL, NULL, list_func, - baton, ctx, pool)); + baton, ctx, scratch_pool)); } diff --git a/subversion/libsvn_client/merge.c b/subversion/libsvn_client/merge.c index aaab4e002cf5..21341b9b0a7b 100644 --- a/subversion/libsvn_client/merge.c +++ b/subversion/libsvn_client/merge.c @@ -215,32 +215,6 @@ /*** Repos-Diff Editor Callbacks ***/ -/* */ -typedef struct merge_source_t -{ - /* "left" side URL and revision (inclusive iff youngest) */ - const svn_client__pathrev_t *loc1; - - /* "right" side URL and revision (inclusive iff youngest) */ - const svn_client__pathrev_t *loc2; - - /* True iff LOC1 is an ancestor of LOC2 or vice-versa (history-wise). */ - svn_boolean_t ancestral; -} merge_source_t; - -/* Description of the merge target root node (a WC working node) */ -typedef struct merge_target_t -{ - /* Absolute path to the WC node */ - const char *abspath; - - /* The repository location of the base node of the target WC. If the node - * is locally added, then URL & REV are NULL & SVN_INVALID_REVNUM. - * REPOS_ROOT_URL and REPOS_UUID are always valid. */ - svn_client__pathrev_t loc; - -} merge_target_t; - typedef struct merge_cmd_baton_t { svn_boolean_t force_delete; /* Delete a file/dir even if modified */ svn_boolean_t dry_run; @@ -1236,7 +1210,7 @@ struct merge_file_baton_t /* If a tree conflict will be installed once edited, it's reason. If a skip should be produced its reason. Some special values are defined. See the - merge_tree_baton_t for an explanation. */ + merge_dir_baton_t for an explanation. */ svn_wc_conflict_reason_t tree_conflict_reason; svn_wc_conflict_action_t tree_conflict_action; svn_node_kind_t tree_conflict_local_node_kind; @@ -2266,13 +2240,9 @@ merge_file_added(const char *relpath, Otherwise, we'll use a pure add. */ if (merge_b->same_repos) { - const char *child = - svn_dirent_skip_ancestor(merge_b->target->abspath, - local_abspath); - SVN_ERR_ASSERT(child != NULL); copyfrom_url = svn_path_url_add_component2( merge_b->merge_source.loc2->url, - child, scratch_pool); + relpath, scratch_pool); copyfrom_rev = right_source->revision; SVN_ERR(check_repos_match(merge_b->target, local_abspath, copyfrom_url, scratch_pool)); @@ -5524,29 +5494,16 @@ single_range_conflict_report_create(const merge_source_t *conflicted_range, return report; } -/* Data for reporting when a merge aborted because of raising conflicts. - * - * ### TODO: More info, including the ranges (or other parameters) the user - * needs to complete the merge. - */ -typedef struct conflict_report_t -{ - const char *target_abspath; - /* The revision range during which conflicts were raised */ - const merge_source_t *conflicted_range; - /* Was the conflicted range the last range in the whole requested merge? */ - svn_boolean_t was_last_range; -} conflict_report_t; - -/* Return a new conflict_report_t containing deep copies of the parameters, - * allocated in RESULT_POOL. */ -static conflict_report_t * +/* Return a new svn_client__conflict_report_t containing deep copies of the + * parameters, allocated in RESULT_POOL. */ +static svn_client__conflict_report_t * conflict_report_create(const char *target_abspath, const merge_source_t *conflicted_range, svn_boolean_t was_last_range, apr_pool_t *result_pool) { - conflict_report_t *report = apr_palloc(result_pool, sizeof(*report)); + svn_client__conflict_report_t *report = apr_palloc(result_pool, + sizeof(*report)); report->target_abspath = apr_pstrdup(result_pool, target_abspath); report->conflicted_range = merge_source_dup(conflicted_range, result_pool); @@ -5555,11 +5512,12 @@ conflict_report_create(const char *target_abspath, } /* Return a deep copy of REPORT, allocated in RESULT_POOL. */ -static conflict_report_t * -conflict_report_dup(const conflict_report_t *report, +static svn_client__conflict_report_t * +conflict_report_dup(const svn_client__conflict_report_t *report, apr_pool_t *result_pool) { - conflict_report_t *new = apr_pmemdup(result_pool, report, sizeof(*new)); + svn_client__conflict_report_t *new = apr_pmemdup(result_pool, report, + sizeof(*new)); new->target_abspath = apr_pstrdup(result_pool, report->target_abspath); new->conflicted_range = merge_source_dup(report->conflicted_range, @@ -5567,11 +5525,9 @@ conflict_report_dup(const conflict_report_t *report, return new; } -/* Create and return an error structure appropriate for the unmerged - revisions range(s). */ -static APR_INLINE svn_error_t * -make_merge_conflict_error(conflict_report_t *report, - apr_pool_t *scratch_pool) +svn_error_t * +svn_client__make_merge_conflict_error(svn_client__conflict_report_t *report, + apr_pool_t *scratch_pool) { assert(!report || svn_dirent_is_absolute(report->target_abspath)); @@ -6512,10 +6468,7 @@ get_mergeinfo_paths(apr_array_header_t *children_with_mergeinfo, /* Sort CHILDREN_WITH_MERGEINFO by each child's path (i.e. as per compare_merge_path_t_as_paths). Any subsequent insertions of new children with insert_child_to_merge() require this ordering. */ - qsort(children_with_mergeinfo->elts, - children_with_mergeinfo->nelts, - children_with_mergeinfo->elt_size, - compare_merge_path_t_as_paths); + svn_sort__array(children_with_mergeinfo, compare_merge_path_t_as_paths); } svn_pool_destroy(swmi_pool); @@ -7125,8 +7078,7 @@ combine_range_with_segments(apr_array_header_t **merge_source_ts_p, /* If this was a subtractive merge, and we created more than one merge source, we need to reverse the sort ordering of our sources. */ if (subtractive && (merge_source_ts->nelts > 1)) - qsort(merge_source_ts->elts, merge_source_ts->nelts, - merge_source_ts->elt_size, compare_merge_source_ts); + svn_sort__array(merge_source_ts, compare_merge_source_ts); *merge_source_ts_p = merge_source_ts; return SVN_NO_ERROR; @@ -9814,7 +9766,7 @@ ensure_ra_session_url(svn_ra_session_t **ra_session, static svn_error_t * do_merge(apr_hash_t **modified_subtrees, svn_mergeinfo_catalog_t result_catalog, - conflict_report_t **conflict_report, + svn_client__conflict_report_t **conflict_report, svn_boolean_t *use_sleep, const apr_array_header_t *merge_sources, const merge_target_t *target, @@ -10137,23 +10089,24 @@ do_merge(apr_hash_t **modified_subtrees, SCRATCH_POOL is used for all temporary allocations. */ static svn_error_t * -merge_cousins_and_supplement_mergeinfo(conflict_report_t **conflict_report, - svn_boolean_t *use_sleep, - const merge_target_t *target, - svn_ra_session_t *URL1_ra_session, - svn_ra_session_t *URL2_ra_session, - const merge_source_t *source, - const svn_client__pathrev_t *yca, - svn_boolean_t same_repos, - svn_depth_t depth, - svn_boolean_t diff_ignore_ancestry, - svn_boolean_t force_delete, - svn_boolean_t record_only, - svn_boolean_t dry_run, - const apr_array_header_t *merge_options, - svn_client_ctx_t *ctx, - apr_pool_t *result_pool, - apr_pool_t *scratch_pool) +merge_cousins_and_supplement_mergeinfo( + svn_client__conflict_report_t **conflict_report, + svn_boolean_t *use_sleep, + const merge_target_t *target, + svn_ra_session_t *URL1_ra_session, + svn_ra_session_t *URL2_ra_session, + const merge_source_t *source, + const svn_client__pathrev_t *yca, + svn_boolean_t same_repos, + svn_depth_t depth, + svn_boolean_t diff_ignore_ancestry, + svn_boolean_t force_delete, + svn_boolean_t record_only, + svn_boolean_t dry_run, + const apr_array_header_t *merge_options, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) { apr_array_header_t *remove_sources, *add_sources; apr_hash_t *modified_subtrees = NULL; @@ -10477,24 +10430,24 @@ open_target_wc(merge_target_t **target_p, * * IGNORE_MERGEINFO and DIFF_IGNORE_ANCESTRY are as in do_merge(). */ -static svn_error_t * -merge_locked(conflict_report_t **conflict_report, - const char *source1, - const svn_opt_revision_t *revision1, - const char *source2, - const svn_opt_revision_t *revision2, - const char *target_abspath, - svn_depth_t depth, - svn_boolean_t ignore_mergeinfo, - svn_boolean_t diff_ignore_ancestry, - svn_boolean_t force_delete, - svn_boolean_t record_only, - svn_boolean_t dry_run, - svn_boolean_t allow_mixed_rev, - const apr_array_header_t *merge_options, - svn_client_ctx_t *ctx, - apr_pool_t *result_pool, - apr_pool_t *scratch_pool) +svn_error_t * +svn_client__merge_locked(svn_client__conflict_report_t **conflict_report, + const char *source1, + const svn_opt_revision_t *revision1, + const char *source2, + const svn_opt_revision_t *revision2, + const char *target_abspath, + svn_depth_t depth, + svn_boolean_t ignore_mergeinfo, + svn_boolean_t diff_ignore_ancestry, + svn_boolean_t force_delete, + svn_boolean_t record_only, + svn_boolean_t dry_run, + svn_boolean_t allow_mixed_rev, + const apr_array_header_t *merge_options, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) { merge_target_t *target; svn_client__pathrev_t *source1_loc, *source2_loc; @@ -10691,7 +10644,7 @@ svn_client_merge5(const char *source1, apr_pool_t *pool) { const char *target_abspath, *lock_abspath; - conflict_report_t *conflict_report; + svn_client__conflict_report_t *conflict_report; /* Sanity check our input -- we require specified revisions, * and either 2 paths or 2 URLs. */ @@ -10714,22 +10667,23 @@ svn_client_merge5(const char *source1, if (!dry_run) SVN_WC__CALL_WITH_WRITE_LOCK( - merge_locked(&conflict_report, - source1, revision1, source2, revision2, - target_abspath, depth, ignore_mergeinfo, - diff_ignore_ancestry, - force_delete, record_only, dry_run, - allow_mixed_rev, merge_options, ctx, pool, pool), + svn_client__merge_locked(&conflict_report, + source1, revision1, source2, revision2, + target_abspath, depth, ignore_mergeinfo, + diff_ignore_ancestry, + force_delete, record_only, dry_run, + allow_mixed_rev, merge_options, ctx, pool, pool), ctx->wc_ctx, lock_abspath, FALSE /* lock_anchor */, pool); else - SVN_ERR(merge_locked(&conflict_report, - source1, revision1, source2, revision2, - target_abspath, depth, ignore_mergeinfo, - diff_ignore_ancestry, - force_delete, record_only, dry_run, - allow_mixed_rev, merge_options, ctx, pool, pool)); - - SVN_ERR(make_merge_conflict_error(conflict_report, pool)); + SVN_ERR(svn_client__merge_locked(&conflict_report, + source1, revision1, source2, revision2, + target_abspath, depth, ignore_mergeinfo, + diff_ignore_ancestry, + force_delete, record_only, dry_run, + allow_mixed_rev, merge_options, ctx, pool, + pool)); + + SVN_ERR(svn_client__make_merge_conflict_error(conflict_report, pool)); return SVN_NO_ERROR; } @@ -11756,7 +11710,7 @@ open_reintegrate_source_and_target(svn_ra_session_t **source_ra_session_p, /* The body of svn_client_merge_reintegrate(), which see for details. */ static svn_error_t * -merge_reintegrate_locked(conflict_report_t **conflict_report, +merge_reintegrate_locked(svn_client__conflict_report_t **conflict_report, const char *source_path_or_url, const svn_opt_revision_t *source_peg_revision, const char *target_abspath, @@ -11831,7 +11785,7 @@ svn_client_merge_reintegrate(const char *source_path_or_url, apr_pool_t *pool) { const char *target_abspath, *lock_abspath; - conflict_report_t *conflict_report; + svn_client__conflict_report_t *conflict_report; SVN_ERR(get_target_and_lock_abspath(&target_abspath, &lock_abspath, target_wcpath, ctx, pool)); @@ -11851,7 +11805,7 @@ svn_client_merge_reintegrate(const char *source_path_or_url, FALSE /*diff_ignore_ancestry*/, dry_run, merge_options, ctx, pool, pool)); - SVN_ERR(make_merge_conflict_error(conflict_report, pool)); + SVN_ERR(svn_client__make_merge_conflict_error(conflict_report, pool)); return SVN_NO_ERROR; } @@ -11861,7 +11815,7 @@ svn_client_merge_reintegrate(const char *source_path_or_url, * IGNORE_MERGEINFO and DIFF_IGNORE_ANCESTRY are as in do_merge(). */ static svn_error_t * -merge_peg_locked(conflict_report_t **conflict_report, +merge_peg_locked(svn_client__conflict_report_t **conflict_report, const char *source_path_or_url, const svn_opt_revision_t *source_peg_revision, const svn_rangelist_t *ranges_to_merge, @@ -11913,6 +11867,21 @@ merge_peg_locked(conflict_report_t **conflict_report, /* Do the real merge! (We say with confidence that our merge sources are both ancestral and related.) */ + if (getenv("SVN_ELEMENT_MERGE") + && same_repos + && (depth == svn_depth_infinity || depth == svn_depth_unknown) + && ignore_mergeinfo + && !record_only) + { + err = svn_client__merge_elements(&use_sleep, + merge_sources, target, ra_session, + diff_ignore_ancestry, force_delete, + dry_run, merge_options, + ctx, result_pool, scratch_pool); + /* ### Currently this merge just errors out on any conflicts */ + *conflict_report = NULL; + } + else err = do_merge(NULL, NULL, conflict_report, &use_sleep, merge_sources, target, ra_session, TRUE /*sources_related*/, same_repos, ignore_mergeinfo, @@ -11951,7 +11920,7 @@ client_find_automatic_merge(automatic_merge_t **merge_p, apr_pool_t *scratch_pool); static svn_error_t * -do_automatic_merge_locked(conflict_report_t **conflict_report, +do_automatic_merge_locked(svn_client__conflict_report_t **conflict_report, const automatic_merge_t *merge, const char *target_abspath, svn_depth_t depth, @@ -11981,7 +11950,7 @@ svn_client_merge_peg5(const char *source_path_or_url, apr_pool_t *pool) { const char *target_abspath, *lock_abspath; - conflict_report_t *conflict_report; + svn_client__conflict_report_t *conflict_report; /* No ranges to merge? No problem. */ if (ranges_to_merge != NULL && ranges_to_merge->nelts == 0) @@ -12046,7 +12015,7 @@ svn_client_merge_peg5(const char *source_path_or_url, force_delete, record_only, dry_run, allow_mixed_rev, merge_options, ctx, pool, pool)); - SVN_ERR(make_merge_conflict_error(conflict_report, pool)); + SVN_ERR(svn_client__make_merge_conflict_error(conflict_report, pool)); return SVN_NO_ERROR; } @@ -12731,7 +12700,7 @@ client_find_automatic_merge(automatic_merge_t **merge_p, * eliminate already-cherry-picked revisions from the source. */ static svn_error_t * -do_automatic_merge_locked(conflict_report_t **conflict_report, +do_automatic_merge_locked(svn_client__conflict_report_t **conflict_report, const automatic_merge_t *merge, const char *target_abspath, svn_depth_t depth, diff --git a/subversion/libsvn_client/merge_elements.c b/subversion/libsvn_client/merge_elements.c new file mode 100644 index 000000000000..66c21b640593 --- /dev/null +++ b/subversion/libsvn_client/merge_elements.c @@ -0,0 +1,248 @@ +/* + * merge_elements.c: element-based merging + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ + +#include <assert.h> +#include <apr_strings.h> +#include <apr_tables.h> +#include <apr_hash.h> +#include "svn_types.h" +#include "svn_error.h" +#include "svn_pools.h" +#include "svn_hash.h" +#include "svn_wc.h" +#include "svn_client.h" +#include "svn_dirent_uri.h" + +#include "client.h" +#include "private/svn_element.h" + +#include "svn_private_config.h" + + +/* Print a notification. + * ### TODO: Send notifications through ctx->notify_func2(). + * ### TODO: Only when 'verbose' output is requested. + */ +static +__attribute__((format(printf, 1, 2))) +void +verbose_notify(const char *fmt, + ...) +{ + va_list ap; + + va_start(ap, fmt); + vprintf(fmt, ap); + if (fmt[strlen(fmt) - 1] != '\n') + printf("\n"); + va_end(ap); +} + +/* Return a string representation of PATHREV. */ +static const char * +pathrev_str(const svn_client__pathrev_t *pathrev, + apr_pool_t *pool) +{ + const char *rrpath + = svn_uri_skip_ancestor(pathrev->repos_root_url, pathrev->url, pool); + + return apr_psprintf(pool, "^/%s@%ld", rrpath, pathrev->rev); +} + +/* Element matching info. + */ +typedef struct element_matching_info_t +{ + void *info; +} element_matching_info_t; + +/* Return a string representation of INFO. */ +static const char * +element_matching_info_str(const element_matching_info_t *info, + apr_pool_t *result_pool) +{ + /* ### */ + const char *str = "{...}"; + + return str; +} + +/* Assign EIDs (in memory) to the source-left, source-right and target + * trees. + */ +static svn_error_t * +assign_eids_to_trees(svn_element__tree_t **tree_left_p, + svn_element__tree_t **tree_right_p, + svn_element__tree_t **tree_target_p, + const svn_client__pathrev_t *src_left, + const svn_client__pathrev_t *src_right, + merge_target_t *target, + svn_ra_session_t *ra_session, + element_matching_info_t *element_matching_info, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + verbose_notify("--- Assigning EIDs to trees"); + + /* ### */ + return SVN_NO_ERROR; +} + +/* Perform a three-way tree merge. Write the result to *MERGE_RESULT_P. + * + * Set *CONFLICTS_P to describe any conflicts, or set *CONFLICTS_P to + * null if there are none. + */ +static svn_error_t * +merge_trees(svn_element__tree_t **merge_result_p, + void **conflicts_p, + svn_element__tree_t *tree_left, + svn_element__tree_t *tree_right, + svn_element__tree_t *tree_target, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + verbose_notify("--- Merging trees"); + + /* ### */ + *merge_result_p = NULL; + *conflicts_p = NULL; + return SVN_NO_ERROR; +} + +/* Convert the MERGE_RESULT to a series of WC edits and apply those to + * the WC described in TARGET. + */ +static svn_error_t * +apply_merge_result_to_wc(merge_target_t *target, + svn_element__tree_t *merge_result, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + verbose_notify("--- Writing merge result to WC"); + + return SVN_NO_ERROR; +} + +/* Do a three-way element-based merge for one merge source range, + * SRC_LEFT:SRC_RIGHT. If there are no conflicts, write the result to the + * WC described in TARGET. + */ +static svn_error_t * +merge_elements_one_source(svn_boolean_t *use_sleep, + const svn_client__pathrev_t *src_left, + const svn_client__pathrev_t *src_right, + merge_target_t *target, + svn_ra_session_t *ra_session, + element_matching_info_t *element_matching_info, + svn_boolean_t diff_ignore_ancestry, + svn_boolean_t force_delete, + svn_boolean_t dry_run, + const apr_array_header_t *merge_options, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_element__tree_t *tree_left, *tree_right, *tree_target; + svn_element__tree_t *merge_result; + void *conflicts; + + verbose_notify("--- Merging by elements: " + "left=%s, right=%s, matching=%s", + pathrev_str(src_left, scratch_pool), + pathrev_str(src_right, scratch_pool), + element_matching_info_str(element_matching_info, + scratch_pool)); + + /* assign EIDs (in memory) to the source-left, source-right and target + trees */ + SVN_ERR(assign_eids_to_trees(&tree_left, &tree_right, &tree_target, + src_left, src_right, target, ra_session, + element_matching_info, + ctx, scratch_pool, scratch_pool)); + + /* perform a tree merge, creating a temporary result (in memory) */ + SVN_ERR(merge_trees(&merge_result, &conflicts, + tree_left, tree_right, tree_target, + scratch_pool, scratch_pool)); + + /* check for (new style) conflicts in the result; if any, bail out */ + if (conflicts) + { + return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("Merge had conflicts; " + "this is not yet supported")); + } + + /* convert the result to a series of WC edits and apply those to the WC */ + if (dry_run) + { + verbose_notify("--- Dry run; not writing merge result to WC"); + } + else + { + SVN_ERR(apply_merge_result_to_wc(target, merge_result, + ctx, scratch_pool)); + *use_sleep = TRUE; + } + + /* forget all the EID metadata */ + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client__merge_elements(svn_boolean_t *use_sleep, + apr_array_header_t *merge_sources, + merge_target_t *target, + svn_ra_session_t *ra_session, + svn_boolean_t diff_ignore_ancestry, + svn_boolean_t force_delete, + svn_boolean_t dry_run, + const apr_array_header_t *merge_options, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + int i; + + /* Merge each source range in turn */ + for (i = 0; i < merge_sources->nelts; i++) + { + merge_source_t *source + = APR_ARRAY_IDX(merge_sources, i, void *); + element_matching_info_t *element_matching_info; + + /* ### TODO: get element matching info from the user */ + element_matching_info = NULL; + + SVN_ERR(merge_elements_one_source(use_sleep, + source->loc1, source->loc2, + target, ra_session, + element_matching_info, + diff_ignore_ancestry, + force_delete, dry_run, merge_options, + ctx, scratch_pool)); + } + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_client/mergeinfo.c b/subversion/libsvn_client/mergeinfo.c index 622dbbca2261..f4f5acd6c850 100644 --- a/subversion/libsvn_client/mergeinfo.c +++ b/subversion/libsvn_client/mergeinfo.c @@ -1946,12 +1946,12 @@ svn_client__mergeinfo_log(svn_boolean_t finding_merged, SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo( &subtree_source_history, source_history, - subtree_rel_path, scratch_pool, scratch_pool)); + subtree_rel_path, scratch_pool, iterpool)); if (!finding_merged) SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo( &subtree_history, target_history, - subtree_rel_path, scratch_pool, scratch_pool)); + subtree_rel_path, scratch_pool, iterpool)); } else { @@ -1969,7 +1969,7 @@ svn_client__mergeinfo_log(svn_boolean_t finding_merged, scratch_pool, iterpool)); SVN_ERR(svn_mergeinfo_merge2(subtree_mergeinfo, merged_via_history, - scratch_pool, scratch_pool)); + scratch_pool, iterpool)); } SVN_ERR(svn_mergeinfo_inheritable2(&subtree_inheritable_mergeinfo, @@ -1995,7 +1995,7 @@ svn_client__mergeinfo_log(svn_boolean_t finding_merged, subtree_source_history, FALSE, scratch_pool, iterpool)); svn_mergeinfo__set_inheritance(merged_noninheritable, FALSE, - scratch_pool); + iterpool); /* Keep track of all ranges partially merged to any and all subtrees. */ diff --git a/subversion/libsvn_client/mtcc.c b/subversion/libsvn_client/mtcc.c index e0fc1e9441b0..75889cad2746 100644 --- a/subversion/libsvn_client/mtcc.c +++ b/subversion/libsvn_client/mtcc.c @@ -568,10 +568,44 @@ svn_client__mtcc_add_copy(const char *src_relpath, return SVN_NO_ERROR; } -svn_error_t * -svn_client__mtcc_add_delete(const char *relpath, - svn_client__mtcc_t *mtcc, - apr_pool_t *scratch_pool) +/* Check if this operation contains at least one change that is not a + plain delete */ +static svn_boolean_t +mtcc_op_contains_non_delete(const mtcc_op_t *op) +{ + if (op->kind != OP_OPEN_DIR && op->kind != OP_OPEN_FILE + && op->kind != OP_DELETE) + { + return TRUE; + } + + if (op->prop_mods && op->prop_mods->nelts) + return TRUE; + + if (op->src_stream) + return TRUE; + + if (op->children) + { + int i; + + for (i = 0; i < op->children->nelts; i++) + { + const mtcc_op_t *c_op = APR_ARRAY_IDX(op->children, i, + const mtcc_op_t *); + + if (mtcc_op_contains_non_delete(c_op)) + return TRUE; + } + } + return FALSE; +} + +static svn_error_t * +mtcc_add_delete(const char *relpath, + svn_boolean_t for_move, + svn_client__mtcc_t *mtcc, + apr_pool_t *scratch_pool) { mtcc_op_t *op; svn_boolean_t created; @@ -598,6 +632,20 @@ svn_client__mtcc_add_delete(const char *relpath, SVN_ERR(mtcc_op_find(&op, &created, relpath, mtcc->root_op, FALSE, TRUE, TRUE, mtcc->pool, scratch_pool)); + if (!for_move && !op && !created) + { + /* Allow deleting directories, that are unmodified except for + one or more deleted descendants */ + + SVN_ERR(mtcc_op_find(&op, &created, relpath, mtcc->root_op, TRUE, + FALSE, FALSE, mtcc->pool, scratch_pool)); + + if (op && mtcc_op_contains_non_delete(op)) + op = NULL; + else + created = TRUE; + } + if (!op || !created) { return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, @@ -614,6 +662,14 @@ svn_client__mtcc_add_delete(const char *relpath, } svn_error_t * +svn_client__mtcc_add_delete(const char *relpath, + svn_client__mtcc_t *mtcc, + apr_pool_t *scratch_pool) +{ + return svn_error_trace(mtcc_add_delete(relpath, FALSE, mtcc, scratch_pool)); +} + +svn_error_t * svn_client__mtcc_add_mkdir(const char *relpath, svn_client__mtcc_t *mtcc, apr_pool_t *scratch_pool) @@ -662,7 +718,7 @@ svn_client__mtcc_add_move(const char *src_relpath, SVN_ERR(svn_client__mtcc_add_copy(src_relpath, mtcc->base_revision, dst_relpath, mtcc, scratch_pool)); - SVN_ERR(svn_client__mtcc_add_delete(src_relpath, mtcc, scratch_pool)); + SVN_ERR(mtcc_add_delete(src_relpath, TRUE, mtcc, scratch_pool)); return SVN_NO_ERROR; } @@ -712,6 +768,7 @@ mtcc_prop_getter(const svn_string_t **mime_type, { *mime_type = svn_string_dup(mod->value, pool); mime_type = NULL; + break; } } } diff --git a/subversion/libsvn_client/patch.c b/subversion/libsvn_client/patch.c index 6d8d0a4632c8..1b2d86b1da94 100644 --- a/subversion/libsvn_client/patch.c +++ b/subversion/libsvn_client/patch.c @@ -46,6 +46,7 @@ #include "private/svn_eol_private.h" #include "private/svn_wc_private.h" #include "private/svn_dep_compat.h" +#include "private/svn_diff_private.h" #include "private/svn_string_private.h" #include "private/svn_subr_private.h" #include "private/svn_sorts_private.h" @@ -66,7 +67,10 @@ typedef struct hunk_info_t { /* The fuzz factor used when matching this hunk, i.e. how many * lines of leading and trailing context to ignore during matching. */ - svn_linenum_t fuzz; + svn_linenum_t match_fuzz; + + /* match_fuzz + the penalty caused by bad patch files */ + svn_linenum_t report_fuzz; } hunk_info_t; /* A struct carrying information related to the patched and unpatched @@ -152,6 +156,9 @@ typedef struct prop_patch_target_t { * ### Should we use flags instead since we're not using all enum values? */ svn_diff_operation_kind_t operation; + /* When true the property change won't be applied */ + svn_boolean_t skipped; + /* ### Here we'll add flags telling if the prop was added, deleted, * ### had_rejects, had_local_mods prior to patching and so on. */ } prop_patch_target_t; @@ -190,8 +197,8 @@ typedef struct patch_target_t { /* Path to the patched file. */ const char *patched_path; - /* Hunks that are rejected will be written to this file. */ - apr_file_t *reject_file; + /* Hunks that are rejected will be written to this stream. */ + svn_stream_t *reject_stream; /* Path to the reject file. */ const char *reject_path; @@ -209,15 +216,23 @@ typedef struct patch_target_t { /* True if the target had to be skipped for some reason. */ svn_boolean_t skipped; + /* True if the reason for skipping is a local obstruction */ + svn_boolean_t obstructed; + /* True if at least one hunk was rejected. */ svn_boolean_t had_rejects; /* True if at least one property hunk was rejected. */ svn_boolean_t had_prop_rejects; - /* True if the target file had local modifications before the - * patch was applied to it. */ - svn_boolean_t local_mods; + /* True if at least one hunk was handled as already applied */ + svn_boolean_t had_already_applied; + + /* True if at least one property hunk was handled as already applied */ + svn_boolean_t had_prop_already_applied; + + /* The operation on the target as set in the patch file */ + svn_diff_operation_kind_t operation; /* True if the target was added by the patch, which means that it did * not exist on disk before patching and has content after patching. */ @@ -226,10 +241,6 @@ typedef struct patch_target_t { /* True if the target ended up being deleted by the patch. */ svn_boolean_t deleted; - /* True if the target ended up being replaced by the patch - * (i.e. a new file was added on top locally deleted node). */ - svn_boolean_t replaced; - /* Set if the target is supposed to be moved by the patch. * This applies to --git diffs which carry "rename from/to" headers. */ const char *move_target_abspath; @@ -252,6 +263,10 @@ typedef struct patch_target_t { /* A hash table of prop_patch_target_t objects keyed by property names. */ apr_hash_t *prop_targets; + /* When TRUE, this patch uses the raw git symlink format instead of the + Subversion internal style format where links start with 'link '. */ + svn_boolean_t git_symlink_format; + } patch_target_t; @@ -261,8 +276,58 @@ typedef struct patch_target_t { typedef struct patch_target_info_t { const char *local_abspath; svn_boolean_t deleted; + svn_boolean_t added; } patch_target_info_t; +/* Check if LOCAL_ABSPATH is recorded as added in TARGETS_INFO */ +static svn_boolean_t +target_is_added(const apr_array_header_t *targets_info, + const char *local_abspath, + apr_pool_t *scratch_pool) +{ + int i; + + for (i = targets_info->nelts - 1; i >= 0; i--) + { + const patch_target_info_t *target_info = + APR_ARRAY_IDX(targets_info, i, const patch_target_info_t *); + + const char *info = svn_dirent_skip_ancestor(target_info->local_abspath, + local_abspath); + + if (info && !*info) + return target_info->added; + else if (info) + return FALSE; + } + + return FALSE; +} + +/* Check if LOCAL_ABSPATH or an ancestor is recorded as deleted in + TARGETS_INFO */ +static svn_boolean_t +target_is_deleted(const apr_array_header_t *targets_info, + const char *local_abspath, + apr_pool_t *scratch_pool) +{ + int i; + + for (i = targets_info->nelts - 1; i >= 0; i--) + { + const patch_target_info_t *target_info = + APR_ARRAY_IDX(targets_info, i, const patch_target_info_t *); + + const char *info = svn_dirent_skip_ancestor(target_info->local_abspath, + local_abspath); + + if (info) + return target_info->deleted; + } + + return FALSE; +} + /* Strip STRIP_COUNT components from the front of PATH, returning * the result in *RESULT, allocated in RESULT_POOL. @@ -368,18 +433,20 @@ obtain_eol_and_keywords_for_file(apr_hash_t **keywords, * Indicate in TARGET->SKIPPED whether the target should be skipped. * STRIP_COUNT specifies the number of leading path components * which should be stripped from target paths in the patch. - * PROP_CHANGES_ONLY specifies whether the target path is allowed to have - * only property changes, and no content changes (in which case the target - * must be a directory). + * HAS_TEXT_CHANGES specifies whether the target path will have some text + * changes applied, implying that the target should be a file and not a + * directory. * Use RESULT_POOL for allocations of fields in TARGET. * Use SCRATCH_POOL for all other allocations. */ static svn_error_t * resolve_target_path(patch_target_t *target, const char *path_from_patchfile, - const char *wcroot_abspath, + const char *root_abspath, int strip_count, - svn_boolean_t prop_changes_only, + svn_boolean_t has_text_changes, + svn_boolean_t follow_moves, svn_wc_context_t *wc_ctx, + const apr_array_header_t *targets_info, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { @@ -391,8 +458,8 @@ resolve_target_path(patch_target_t *target, target->canon_path_from_patchfile = svn_dirent_internal_style( path_from_patchfile, result_pool); - /* We allow properties to be set on the wc root dir. */ - if (! prop_changes_only && target->canon_path_from_patchfile[0] == '\0') + /* We can't handle text changes on the patch root dir. */ + if (has_text_changes && target->canon_path_from_patchfile[0] == '\0') { /* An empty patch target path? What gives? Skip this. */ target->skipped = TRUE; @@ -409,14 +476,14 @@ resolve_target_path(patch_target_t *target, if (svn_dirent_is_absolute(stripped_path)) { - target->local_relpath = svn_dirent_is_child(wcroot_abspath, + target->local_relpath = svn_dirent_is_child(root_abspath, stripped_path, result_pool); if (! target->local_relpath) { /* The target path is either outside of the working copy - * or it is the working copy itself. Skip it. */ + * or it is the patch root itself. Skip it. */ target->skipped = TRUE; target->local_abspath = NULL; target->local_relpath = stripped_path; @@ -429,9 +496,9 @@ resolve_target_path(patch_target_t *target, } /* Make sure the path is secure to use. We want the target to be inside - * of the working copy and not be fooled by symlinks it might contain. */ + * the locked tree and not be fooled by symlinks it might contain. */ SVN_ERR(svn_dirent_is_under_root(&under_root, - &target->local_abspath, wcroot_abspath, + &target->local_abspath, root_abspath, target->local_relpath, result_pool)); if (! under_root) @@ -442,6 +509,13 @@ resolve_target_path(patch_target_t *target, return SVN_NO_ERROR; } + if (target_is_deleted(targets_info, target->local_abspath, scratch_pool)) + { + target->locally_deleted = TRUE; + target->db_kind = svn_node_none; + return SVN_NO_ERROR; + } + /* Skip things we should not be messing with. */ err = svn_wc_status3(&status, wc_ctx, target->local_abspath, result_pool, scratch_pool); @@ -463,6 +537,7 @@ resolve_target_path(patch_target_t *target, status->conflicted) { target->skipped = TRUE; + target->obstructed = TRUE; return SVN_NO_ERROR; } else if (status->node_status == svn_wc_status_deleted) @@ -481,20 +556,29 @@ resolve_target_path(patch_target_t *target, if (target->locally_deleted) { - const char *moved_to_abspath; + const char *moved_to_abspath = NULL; + + if (follow_moves + && !target_is_added(targets_info, target->local_abspath, + scratch_pool)) + { + SVN_ERR(svn_wc__node_was_moved_away(&moved_to_abspath, NULL, + wc_ctx, target->local_abspath, + result_pool, scratch_pool)); + } - SVN_ERR(svn_wc__node_was_moved_away(&moved_to_abspath, NULL, - wc_ctx, target->local_abspath, - result_pool, scratch_pool)); - /* ### BUG: moved_to_abspath contains the target where the op-root was - ### moved to... not the target itself! */ if (moved_to_abspath) { target->local_abspath = moved_to_abspath; - target->local_relpath = svn_dirent_skip_ancestor(wcroot_abspath, - moved_to_abspath); - SVN_ERR_ASSERT(target->local_relpath && - target->local_relpath[0] != '\0'); + target->local_relpath = svn_dirent_skip_ancestor(root_abspath, + moved_to_abspath); + + if (!target->local_relpath || target->local_relpath[0] == '\0') + { + /* The target path is outside of the patch area. Skip it. */ + target->skipped = TRUE; + return SVN_NO_ERROR; + } /* As far as we are concerned this target is not locally deleted. */ target->locally_deleted = FALSE; @@ -511,6 +595,22 @@ resolve_target_path(patch_target_t *target, } } +#ifndef HAVE_SYMLINK + if (target->kind_on_disk == svn_node_file + && !target->is_symlink + && !target->locally_deleted + && status->prop_status != svn_wc_status_none) + { + const svn_string_t *value; + + SVN_ERR(svn_wc_prop_get2(&value, wc_ctx, target->local_abspath, + SVN_PROP_SPECIAL, scratch_pool, scratch_pool)); + + if (value) + target->is_symlink = TRUE; + } +#endif + return SVN_NO_ERROR; } @@ -540,7 +640,7 @@ readline_prop(void *baton, svn_stringbuf_t **line, const char **eol_str, svn_boolean_t *eof, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { - prop_read_baton_t *b = (prop_read_baton_t *)baton; + prop_read_baton_t *b = baton; svn_stringbuf_t *str = NULL; const char *c; svn_boolean_t found_eof; @@ -593,7 +693,7 @@ readline_prop(void *baton, svn_stringbuf_t **line, const char **eol_str, while (c < b->value->data + b->value->len); if (eof) - *eof = found_eof; + *eof = found_eof && !(str && str->len > 0); *line = str; return SVN_NO_ERROR; @@ -605,7 +705,8 @@ readline_prop(void *baton, svn_stringbuf_t **line, const char **eol_str, static svn_error_t * tell_prop(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool) { - prop_read_baton_t *b = (prop_read_baton_t *)baton; + prop_read_baton_t *b = baton; + *offset = b->offset; return SVN_NO_ERROR; } @@ -615,7 +716,8 @@ tell_prop(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool) static svn_error_t * seek_prop(void *baton, apr_off_t offset, apr_pool_t *scratch_pool) { - prop_read_baton_t *b = (prop_read_baton_t *)baton; + prop_read_baton_t *b = baton; + b->offset = offset; return SVN_NO_ERROR; } @@ -626,7 +728,8 @@ static svn_error_t * write_prop(void *baton, const char *buf, apr_size_t len, apr_pool_t *scratch_pool) { - svn_stringbuf_t *patched_value = (svn_stringbuf_t *)baton; + svn_stringbuf_t *patched_value = baton; + svn_stringbuf_appendbytes(patched_value, buf, len); return SVN_NO_ERROR; } @@ -638,6 +741,7 @@ write_prop(void *baton, const char *buf, apr_size_t len, * Use SCRATCH_POOL for temporary allocations. */ static svn_error_t * init_prop_target(prop_patch_target_t **prop_target, + const patch_target_t *target, const char *prop_name, svn_diff_operation_kind_t operation, svn_wc_context_t *wc_ctx, @@ -647,7 +751,6 @@ init_prop_target(prop_patch_target_t **prop_target, prop_patch_target_t *new_prop_target; target_content_t *content; const svn_string_t *value; - svn_error_t *err; prop_read_baton_t *prop_read_baton; content = apr_pcalloc(result_pool, sizeof(*content)); @@ -664,18 +767,12 @@ init_prop_target(prop_patch_target_t **prop_target, new_prop_target->operation = operation; new_prop_target->content = content; - err = svn_wc_prop_get2(&value, wc_ctx, local_abspath, prop_name, - result_pool, scratch_pool); - if (err) - { - if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) - { - svn_error_clear(err); - value = NULL; - } - else - return svn_error_trace(err); - } + if (!(target->deleted || target->db_kind == svn_node_none)) + SVN_ERR(svn_wc_prop_get2(&value, wc_ctx, local_abspath, prop_name, + result_pool, scratch_pool)); + else + value = NULL; + content->existed = (value != NULL); new_prop_target->value = value; new_prop_target->patched_value = svn_stringbuf_create_empty(result_pool); @@ -716,71 +813,15 @@ readline_file(void *baton, svn_stringbuf_t **line, const char **eol_str, svn_boolean_t *eof, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { - apr_file_t *file = (apr_file_t *)baton; - svn_stringbuf_t *str = NULL; - apr_size_t numbytes; - char c; - svn_boolean_t found_eof; - - /* Read bytes into STR up to and including, but not storing, - * the next EOL sequence. */ - *eol_str = NULL; - numbytes = 1; - found_eof = FALSE; - while (!found_eof) - { - SVN_ERR(svn_io_file_read_full2(file, &c, sizeof(c), &numbytes, - &found_eof, scratch_pool)); - if (numbytes != 1) - { - found_eof = TRUE; - break; - } - - if (c == '\n') - { - *eol_str = "\n"; - } - else if (c == '\r') - { - *eol_str = "\r"; - - if (!found_eof) - { - apr_off_t pos; - - /* Check for "\r\n" by peeking at the next byte. */ - pos = 0; - SVN_ERR(svn_io_file_seek(file, APR_CUR, &pos, scratch_pool)); - SVN_ERR(svn_io_file_read_full2(file, &c, sizeof(c), &numbytes, - &found_eof, scratch_pool)); - if (numbytes == 1 && c == '\n') - { - *eol_str = "\r\n"; - } - else - { - /* Pretend we never peeked. */ - SVN_ERR(svn_io_file_seek(file, APR_SET, &pos, scratch_pool)); - found_eof = FALSE; - numbytes = 1; - } - } - } - else - { - if (str == NULL) - str = svn_stringbuf_create_ensure(80, result_pool); - svn_stringbuf_appendbyte(str, c); - } + apr_file_t *file = baton; - if (*eol_str) - break; - } + SVN_ERR(svn_io_file_readline(file, line, eol_str, eof, APR_SIZE_MAX, + result_pool, scratch_pool)); - if (eof) - *eof = found_eof; - *line = str; + if (!(*line)->len) + *line = NULL; + else + *eof = FALSE; return SVN_NO_ERROR; } @@ -791,9 +832,9 @@ readline_file(void *baton, svn_stringbuf_t **line, const char **eol_str, static svn_error_t * tell_file(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool) { - apr_file_t *file = (apr_file_t *)baton; - *offset = 0; - SVN_ERR(svn_io_file_seek(file, APR_CUR, offset, scratch_pool)); + apr_file_t *file = baton; + + SVN_ERR(svn_io_file_get_offset(offset, file, scratch_pool)); return SVN_NO_ERROR; } @@ -802,7 +843,8 @@ tell_file(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool) static svn_error_t * seek_file(void *baton, apr_off_t offset, apr_pool_t *scratch_pool) { - apr_file_t *file = (apr_file_t *)baton; + apr_file_t *file = baton; + SVN_ERR(svn_io_file_seek(file, APR_SET, &offset, scratch_pool)); return SVN_NO_ERROR; } @@ -813,27 +855,15 @@ static svn_error_t * write_file(void *baton, const char *buf, apr_size_t len, apr_pool_t *scratch_pool) { - apr_file_t *file = (apr_file_t *)baton; + apr_file_t *file = baton; + SVN_ERR(svn_io_file_write_full(file, buf, len, &len, scratch_pool)); return SVN_NO_ERROR; } -/* Handling symbolic links: - * - * In Subversion, symlinks can be represented on disk in two distinct ways. - * On systems which support symlinks, a symlink is created on disk. - * On systems which do not support symlink, a file is created on disk - * which contains the "normal form" of the symlink, which looks like: - * link TARGET - * where TARGET is the file the symlink points to. - * - * When reading symlinks (i.e. the link itself, not the file the symlink - * is pointing to) through the svn_subst_create_specialfile() function - * into a buffer, the buffer always contains the "normal form" of the symlink. - * Due to this representation symlinks always contain a single line of text. - * - * The functions below are needed to deal with the case where a patch - * wants to change the TARGET that a symlink points to. +/* Symlinks appear in patches in their repository normal form, abstracted by + * the svn_subst_* module. The functions below enable patches to change the + * targets of symlinks. */ /* Baton for the (readline|tell|seek|write)_symlink functions. */ @@ -869,16 +899,36 @@ readline_symlink(void *baton, svn_stringbuf_t **line, const char **eol_str, } else { - svn_string_t *dest; + svn_stream_t *stream; + const apr_size_t len_hint = 64; /* arbitrary */ - SVN_ERR(svn_io_read_link(&dest, sb->local_abspath, scratch_pool)); - *line = svn_stringbuf_createf(result_pool, "link %s", dest->data); + SVN_ERR(svn_subst_read_specialfile(&stream, sb->local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_stringbuf_from_stream(line, stream, len_hint, result_pool)); + *eof = FALSE; sb->at_eof = TRUE; } return SVN_NO_ERROR; } +/* Identical to readline_symlink(), but returns symlink in raw format to + * allow patching links in git-style. + */ +static svn_error_t * +readline_symlink_git(void *baton, svn_stringbuf_t **line, const char **eol_str, + svn_boolean_t *eof, apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + SVN_ERR(readline_symlink(baton, line, eol_str, eof, + result_pool, scratch_pool)); + + if (*line && (*line)->len > 5 && !strncmp((*line)->data, "link ", 5)) + svn_stringbuf_remove(*line, 0, 5); /* Skip "link " */ + + return SVN_NO_ERROR; +} + /* Set *OFFSET to 1 or 0 depending on whether the "normal form" of * the symlink has already been read. */ static svn_error_t * @@ -901,35 +951,6 @@ seek_symlink(void *baton, apr_off_t offset, apr_pool_t *scratch_pool) return SVN_NO_ERROR; } - -/* Set the target of the symlink accessed via BATON. - * The contents of BUF must be a valid "normal form" of a symlink. */ -static svn_error_t * -write_symlink(void *baton, const char *buf, apr_size_t len, - apr_pool_t *scratch_pool) -{ - const char *target_abspath = baton; - const char *new_name; - const char *link = apr_pstrndup(scratch_pool, buf, len); - - if (strncmp(link, "link ", 5) != 0) - return svn_error_create(SVN_ERR_IO_WRITE_ERROR, NULL, - _("Invalid link representation")); - - link += 5; /* Skip "link " */ - - /* We assume the entire symlink is written at once, as the patch - format is line based */ - - SVN_ERR(svn_io_create_unique_link(&new_name, target_abspath, link, - ".tmp", scratch_pool)); - - SVN_ERR(svn_io_file_rename(new_name, target_abspath, scratch_pool)); - - return SVN_NO_ERROR; -} - - /* Return a suitable filename for the target of PATCH. * Examine the ``old'' and ``new'' file names, and choose the file name * with the fewest path components, the shortest basename, and the shortest @@ -987,32 +1008,19 @@ choose_target_filename(const svn_patch_t *patch) static svn_error_t * init_patch_target(patch_target_t **patch_target, const svn_patch_t *patch, - const char *wcroot_abspath, + const char *root_abspath, svn_wc_context_t *wc_ctx, int strip_count, svn_boolean_t remove_tempfiles, + const apr_array_header_t *targets_info, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { patch_target_t *target; target_content_t *content; - svn_boolean_t has_prop_changes = FALSE; - svn_boolean_t prop_changes_only = FALSE; - - { - apr_hash_index_t *hi; - - for (hi = apr_hash_first(scratch_pool, patch->prop_patches); - hi; - hi = apr_hash_next(hi)) - { - svn_prop_patch_t *prop_patch = apr_hash_this_val(hi); - if (! has_prop_changes) - has_prop_changes = prop_patch->hunks->nelts > 0; - else - break; - } - } + svn_boolean_t has_text_changes = FALSE; + svn_boolean_t follow_moves; - prop_changes_only = has_prop_changes && patch->hunks->nelts == 0; + has_text_changes = ((patch->hunks && patch->hunks->nelts > 0) + || patch->binary_patch); content = apr_pcalloc(result_pool, sizeof(*content)); @@ -1030,57 +1038,35 @@ init_patch_target(patch_target_t **patch_target, target->kind_on_disk = svn_node_none; target->content = content; target->prop_targets = apr_hash_make(result_pool); + target->operation = patch->operation; + + if (patch->operation == svn_diff_op_added /* Allow replacing */ + || patch->operation == svn_diff_op_moved) + { + follow_moves = FALSE; + } + else if (patch->operation == svn_diff_op_unchanged + && patch->hunks && patch->hunks->nelts == 1) + { + svn_diff_hunk_t *hunk = APR_ARRAY_IDX(patch->hunks, 0, + svn_diff_hunk_t *); + + follow_moves = (svn_diff_hunk_get_original_start(hunk) != 0); + } + else + follow_moves = TRUE; SVN_ERR(resolve_target_path(target, choose_target_filename(patch), - wcroot_abspath, strip_count, prop_changes_only, - wc_ctx, result_pool, scratch_pool)); + root_abspath, strip_count, has_text_changes, + follow_moves, wc_ctx, targets_info, + result_pool, scratch_pool)); *patch_target = target; if (! target->skipped) { - const char *diff_header; - apr_size_t len; - - /* Create a temporary file to write the patched result to. - * Also grab various bits of information about the file. */ - if (target->is_symlink) - { - struct symlink_baton_t *sb = apr_pcalloc(result_pool, sizeof(*sb)); - content->existed = TRUE; - - sb->local_abspath = target->local_abspath; - - /* Wire up the read callbacks. */ - content->read_baton = sb; - - content->readline = readline_symlink; - content->seek = seek_symlink; - content->tell = tell_symlink; - } - else if (target->kind_on_disk == svn_node_file) + if (patch->old_symlink_bit == svn_tristate_true + || patch->new_symlink_bit == svn_tristate_true) { - SVN_ERR(svn_io_file_open(&target->file, target->local_abspath, - APR_READ | APR_BUFFERED, - APR_OS_DEFAULT, result_pool)); - SVN_ERR(svn_wc_text_modified_p2(&target->local_mods, wc_ctx, - target->local_abspath, FALSE, - scratch_pool)); - SVN_ERR(svn_io_is_file_executable(&target->executable, - target->local_abspath, - scratch_pool)); - SVN_ERR(obtain_eol_and_keywords_for_file(&content->keywords, - &content->eol_style, - &content->eol_str, - wc_ctx, - target->local_abspath, - result_pool, - scratch_pool)); - content->existed = TRUE; - - /* Wire up the read callbacks. */ - content->readline = readline_file; - content->seek = seek_file; - content->tell = tell_file; - content->read_baton = target->file; + target->git_symlink_format = TRUE; } /* ### Is it ok to set the operation of the target already here? Isn't @@ -1098,6 +1084,7 @@ init_patch_target(patch_target_t **patch_target, const char *move_target_path; const char *move_target_relpath; svn_boolean_t under_root; + svn_boolean_t is_special; svn_node_kind_t kind_on_disk; svn_node_kind_t wc_kind; @@ -1110,7 +1097,7 @@ init_patch_target(patch_target_t **patch_target, if (svn_dirent_is_absolute(move_target_path)) { - move_target_relpath = svn_dirent_is_child(wcroot_abspath, + move_target_relpath = svn_dirent_is_child(root_abspath, move_target_path, scratch_pool); if (! move_target_relpath) @@ -1118,7 +1105,6 @@ init_patch_target(patch_target_t **patch_target, /* The move target path is either outside of the working * copy or it is the working copy itself. Skip it. */ target->skipped = TRUE; - target->local_abspath = NULL; return SVN_NO_ERROR; } } @@ -1128,76 +1114,133 @@ init_patch_target(patch_target_t **patch_target, /* Make sure the move target path is secure to use. */ SVN_ERR(svn_dirent_is_under_root(&under_root, &target->move_target_abspath, - wcroot_abspath, + root_abspath, move_target_relpath, result_pool)); if (! under_root) { /* The target path is outside of the working copy. Skip it. */ target->skipped = TRUE; - target->local_abspath = NULL; + target->move_target_abspath = NULL; return SVN_NO_ERROR; } - SVN_ERR(svn_io_check_path(target->move_target_abspath, - &kind_on_disk, scratch_pool)); + SVN_ERR(svn_io_check_special_path(target->move_target_abspath, + &kind_on_disk, &is_special, + scratch_pool)); SVN_ERR(svn_wc_read_kind2(&wc_kind, wc_ctx, target->move_target_abspath, FALSE, FALSE, scratch_pool)); - if (kind_on_disk != svn_node_none || wc_kind != svn_node_none) + if (wc_kind == svn_node_file || wc_kind == svn_node_dir) { - /* The move target path already exists on disk. Skip target. */ - target->skipped = TRUE; - target->move_target_abspath = NULL; - return SVN_NO_ERROR; + /* The move target path already exists on disk. */ + svn_error_t *err; + const char *moved_from_abspath; + + err = svn_wc__node_was_moved_here(&moved_from_abspath, NULL, + wc_ctx, + target->move_target_abspath, + scratch_pool, scratch_pool); + + if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) + { + svn_error_clear(err); + err = NULL; + moved_from_abspath = NULL; + } + else + SVN_ERR(err); + + if (moved_from_abspath && (strcmp(moved_from_abspath, + target->local_abspath) == 0)) + { + target->local_abspath = target->move_target_abspath; + target->move_target_abspath = NULL; + target->operation = svn_diff_op_modified; + target->locally_deleted = FALSE; + target->db_kind = wc_kind; + target->kind_on_disk = kind_on_disk; + target->is_special = is_special; + + target->had_already_applied = TRUE; /* Make sure we notify */ + } + else + { + target->skipped = TRUE; + target->move_target_abspath = NULL; + return SVN_NO_ERROR; + } + + } + else if (kind_on_disk != svn_node_none + || target_is_added(targets_info, target->move_target_abspath, + scratch_pool)) + { + target->skipped = TRUE; + target->move_target_abspath = NULL; + return SVN_NO_ERROR; } } - if (! target->is_symlink) + /* Create a temporary file to write the patched result to. + * Also grab various bits of information about the file. */ + if (target->is_symlink) { - /* Open a temporary file to write the patched result to. */ - SVN_ERR(svn_io_open_unique_file3(&target->patched_file, - &target->patched_path, NULL, - remove_tempfiles ? - svn_io_file_del_on_pool_cleanup : - svn_io_file_del_none, - result_pool, scratch_pool)); + struct symlink_baton_t *sb = apr_pcalloc(result_pool, sizeof(*sb)); + content->existed = TRUE; + + sb->local_abspath = target->local_abspath; - /* Put the write callback in place. */ - content->write = write_file; - content->write_baton = target->patched_file; + /* Wire up the read callbacks. */ + content->read_baton = sb; + + content->readline = target->git_symlink_format ? readline_symlink_git + : readline_symlink; + content->seek = seek_symlink; + content->tell = tell_symlink; } - else + else if (target->kind_on_disk == svn_node_file) { - /* Put the write callback in place. */ - SVN_ERR(svn_io_open_unique_file3(NULL, - &target->patched_path, NULL, - remove_tempfiles ? - svn_io_file_del_on_pool_cleanup : - svn_io_file_del_none, - result_pool, scratch_pool)); - - content->write_baton = (void*)target->patched_path; + SVN_ERR(svn_io_file_open(&target->file, target->local_abspath, + APR_READ | APR_BUFFERED, + APR_OS_DEFAULT, result_pool)); + SVN_ERR(svn_io_is_file_executable(&target->executable, + target->local_abspath, + scratch_pool)); + SVN_ERR(obtain_eol_and_keywords_for_file(&content->keywords, + &content->eol_style, + &content->eol_str, + wc_ctx, + target->local_abspath, + result_pool, + scratch_pool)); + content->existed = TRUE; - content->write = write_symlink; + /* Wire up the read callbacks. */ + content->readline = readline_file; + content->seek = seek_file; + content->tell = tell_file; + content->read_baton = target->file; } - /* Open a temporary file to write rejected hunks to. */ - SVN_ERR(svn_io_open_unique_file3(&target->reject_file, - &target->reject_path, NULL, + /* Open a temporary file to write the patched result to. */ + SVN_ERR(svn_io_open_unique_file3(&target->patched_file, + &target->patched_path, NULL, remove_tempfiles ? svn_io_file_del_on_pool_cleanup : svn_io_file_del_none, result_pool, scratch_pool)); - /* The reject file needs a diff header. */ - diff_header = apr_psprintf(scratch_pool, "--- %s%s+++ %s%s", - target->canon_path_from_patchfile, - APR_EOL_STR, - target->canon_path_from_patchfile, - APR_EOL_STR); - len = strlen(diff_header); - SVN_ERR(svn_io_file_write_full(target->reject_file, diff_header, len, - &len, scratch_pool)); + /* Put the write callback in place. */ + content->write = write_file; + content->write_baton = target->patched_file; + + /* Open a temporary stream to write rejected hunks to. */ + SVN_ERR(svn_stream_open_unique(&target->reject_stream, + &target->reject_path, NULL, + remove_tempfiles ? + svn_io_file_del_on_pool_cleanup : + svn_io_file_del_none, + result_pool, scratch_pool)); /* Handle properties. */ if (! target->skipped) @@ -1213,13 +1256,185 @@ init_patch_target(patch_target_t **patch_target, prop_patch_target_t *prop_target; SVN_ERR(init_prop_target(&prop_target, - prop_name, + target, prop_name, prop_patch->operation, wc_ctx, target->local_abspath, result_pool, scratch_pool)); svn_hash_sets(target->prop_targets, prop_name, prop_target); } + + /* Now, check for out-of-band mode changes and convert these in + their Subversion equivalent properties. */ + if (patch->new_executable_bit != svn_tristate_unknown + && patch->new_executable_bit != patch->old_executable_bit) + { + svn_diff_operation_kind_t operation; + + if (patch->new_executable_bit == svn_tristate_true) + operation = svn_diff_op_added; + else if (patch->new_executable_bit == svn_tristate_false) + { + /* Made non-executable. */ + if (patch->old_executable_bit == svn_tristate_true) + operation = svn_diff_op_deleted; + else + operation = svn_diff_op_unchanged; + } + else + operation = svn_diff_op_unchanged; + + if (operation != svn_diff_op_unchanged) + { + prop_patch_target_t *prop_target; + + prop_target = svn_hash_gets(target->prop_targets, + SVN_PROP_EXECUTABLE); + + if (prop_target && operation != prop_target->operation) + { + return svn_error_createf(SVN_ERR_INVALID_INPUT, NULL, + _("Invalid patch: specifies " + "contradicting mode changes and " + "%s changes (for '%s')"), + SVN_PROP_EXECUTABLE, + target->local_abspath); + } + else if (!prop_target) + { + SVN_ERR(init_prop_target(&prop_target, + target, SVN_PROP_EXECUTABLE, + operation, + wc_ctx, target->local_abspath, + result_pool, scratch_pool)); + svn_hash_sets(target->prop_targets, SVN_PROP_EXECUTABLE, + prop_target); + } + } + } + + if (patch->new_symlink_bit != svn_tristate_unknown + && patch->new_symlink_bit != patch->old_symlink_bit) + { + svn_diff_operation_kind_t operation; + + if (patch->new_symlink_bit == svn_tristate_true) + operation = svn_diff_op_added; + else if (patch->new_symlink_bit == svn_tristate_false) + { + /* Made non-symlink. */ + if (patch->old_symlink_bit == svn_tristate_true) + operation = svn_diff_op_deleted; + else + operation = svn_diff_op_unchanged; + } + else + operation = svn_diff_op_unchanged; + + if (operation != svn_diff_op_unchanged) + { + prop_patch_target_t *prop_target; + prop_target = svn_hash_gets(target->prop_targets, + SVN_PROP_SPECIAL); + + if (prop_target && operation != prop_target->operation) + { + return svn_error_createf(SVN_ERR_INVALID_INPUT, NULL, + _("Invalid patch: specifies " + "contradicting mode changes and " + "%s changes (for '%s')"), + SVN_PROP_SPECIAL, + target->local_abspath); + } + else if (!prop_target) + { + SVN_ERR(init_prop_target(&prop_target, + target, SVN_PROP_SPECIAL, + operation, + wc_ctx, target->local_abspath, + result_pool, scratch_pool)); + svn_hash_sets(target->prop_targets, SVN_PROP_SPECIAL, + prop_target); + } + } + } + } + } + + if ((target->locally_deleted || target->db_kind == svn_node_none) + && !target->added + && target->operation == svn_diff_op_unchanged) + { + svn_boolean_t maybe_add = FALSE; + + if (patch->hunks && patch->hunks->nelts == 1) + { + svn_diff_hunk_t *hunk = APR_ARRAY_IDX(patch->hunks, 0, + svn_diff_hunk_t *); + + if (svn_diff_hunk_get_original_start(hunk) == 0) + maybe_add = TRUE; + } + else if (patch->prop_patches && apr_hash_count(patch->prop_patches)) + { + apr_hash_index_t *hi; + svn_boolean_t all_add = TRUE; + + for (hi = apr_hash_first(result_pool, patch->prop_patches); + hi; + hi = apr_hash_next(hi)) + { + svn_prop_patch_t *prop_patch = apr_hash_this_val(hi); + + if (prop_patch->operation != svn_diff_op_added) + { + all_add = FALSE; + break; + } + } + + maybe_add = all_add; + } + /* Other implied types */ + + if (maybe_add) + target->added = TRUE; + } + else if (!target->deleted && !target->added + && target->operation == svn_diff_op_unchanged) + { + svn_boolean_t maybe_delete = FALSE; + + if (patch->hunks && patch->hunks->nelts == 1) + { + svn_diff_hunk_t *hunk = APR_ARRAY_IDX(patch->hunks, 0, + svn_diff_hunk_t *); + + if (svn_diff_hunk_get_modified_start(hunk) == 0) + maybe_delete = TRUE; } + + /* Other implied types */ + + if (maybe_delete) + target->deleted = TRUE; + } + + if (target->reject_stream != NULL) + { + /* The reject file needs a diff header. */ + const char *left_src = target->canon_path_from_patchfile; + const char *right_src = target->canon_path_from_patchfile; + + /* Handle moves specifically? */ + if (target->added) + left_src = "/dev/null"; + if (target->deleted) + right_src = "/dev/null"; + + SVN_ERR(svn_stream_printf(target->reject_stream, scratch_pool, + "--- %s" APR_EOL_STR + "+++ %s" APR_EOL_STR, + left_src, right_src)); } return SVN_NO_ERROR; @@ -1354,12 +1569,20 @@ match_hunk(svn_boolean_t *matched, target_content_t *content, svn_linenum_t hunk_length; svn_linenum_t leading_context; svn_linenum_t trailing_context; + svn_linenum_t fuzz_penalty; *matched = FALSE; if (content->eof) return SVN_NO_ERROR; + fuzz_penalty = svn_diff_hunk__get_fuzz_penalty(hunk); + + if (fuzz_penalty > fuzz) + return SVN_NO_ERROR; + else + fuzz -= fuzz_penalty; + saved_line = content->current_line; lines_read = 0; lines_matched = FALSE; @@ -1568,14 +1791,14 @@ match_existing_target(svn_boolean_t *match, *match = FALSE; return SVN_NO_ERROR; } - } - while (lines_matched && ! content->eof && ! hunk_eof); - svn_pool_destroy(iterpool); + } + while (lines_matched && ! content->eof && ! hunk_eof); + svn_pool_destroy(iterpool); - *match = (lines_matched && content->eof == hunk_eof); - SVN_ERR(seek_to_line(content, saved_line, scratch_pool)); + *match = (lines_matched && content->eof == hunk_eof); + SVN_ERR(seek_to_line(content, saved_line, scratch_pool)); - return SVN_NO_ERROR; + return SVN_NO_ERROR; } /* Determine the line at which a HUNK applies to CONTENT of the TARGET @@ -1636,8 +1859,6 @@ get_hunk_info(hunk_info_t **hi, patch_target_t *target, { svn_boolean_t file_matches; - /* ### I can't reproduce anything but a no-match here. - The content is already at eof, so any hunk fails */ SVN_ERR(match_existing_target(&file_matches, content, hunk, scratch_pool)); if (file_matches) @@ -1682,8 +1903,11 @@ get_hunk_info(hunk_info_t **hi, patch_target_t *target, } else if (original_start > 0 && content->existed) { + svn_linenum_t modified_start; svn_linenum_t saved_line = content->current_line; + modified_start = svn_diff_hunk_get_modified_start(hunk); + /* Scan for a match at the line where the hunk thinks it * should be going. */ SVN_ERR(seek_to_line(content, original_start, scratch_pool)); @@ -1707,20 +1931,24 @@ get_hunk_info(hunk_info_t **hi, patch_target_t *target, * check would be ambiguous. */ if (fuzz == 0) { - svn_linenum_t modified_start; - - modified_start = svn_diff_hunk_get_modified_start(hunk); - if (modified_start == 0) + if (modified_start == 0 + && (target->operation == svn_diff_op_unchanged + || target->operation == svn_diff_op_deleted)) { - /* Patch wants to delete the file. + /* Patch wants to delete the file. */ - ### locally_deleted is always false here? */ already_applied = target->locally_deleted; } else { - SVN_ERR(seek_to_line(content, modified_start, - scratch_pool)); + svn_linenum_t seek_to; + + if (modified_start == 0) + seek_to = 1; /* Empty file case */ + else + seek_to = modified_start; + + SVN_ERR(seek_to_line(content, seek_to, scratch_pool)); SVN_ERR(scan_for_match(&matched_line, content, hunk, TRUE, modified_start + 1, @@ -1817,12 +2045,42 @@ get_hunk_info(hunk_info_t **hi, patch_target_t *target, } } } + else if (matched_line > 0 + && fuzz == 0 + && (svn_diff_hunk_get_leading_context(hunk) == 0 + || svn_diff_hunk_get_trailing_context(hunk) == 0) + && (svn_diff_hunk_get_modified_length(hunk) > + svn_diff_hunk_get_original_length(hunk))) + { + /* Check that we are not applying the same change that just adds some + lines again, when we don't have enough context to see the + difference */ + svn_linenum_t reverse_matched_line; + + SVN_ERR(seek_to_line(content, modified_start, scratch_pool)); + SVN_ERR(scan_for_match(&reverse_matched_line, content, + hunk, TRUE, + modified_start + 1, + fuzz, ignore_whitespace, TRUE, + cancel_func, cancel_baton, + scratch_pool)); + + /* We might want to check that we are actually at the start or the + end of the file. Having no context implies that we should be. */ + already_applied = (reverse_matched_line == modified_start); + } SVN_ERR(seek_to_line(content, saved_line, scratch_pool)); } + else if (!content->existed && svn_diff_hunk_get_modified_start(hunk) == 0) + { + /* The hunk wants to delete a file or property which doesn't exist. */ + matched_line = 0; + already_applied = TRUE; + } else { - /* The hunk wants to modify a file which doesn't exist. */ + /* The hunk wants to modify a file or property which doesn't exist. */ matched_line = 0; } @@ -1831,7 +2089,8 @@ get_hunk_info(hunk_info_t **hi, patch_target_t *target, (*hi)->matched_line = matched_line; (*hi)->rejected = (matched_line == 0); (*hi)->already_applied = already_applied; - (*hi)->fuzz = fuzz; + (*hi)->report_fuzz = fuzz; + (*hi)->match_fuzz = fuzz - svn_diff_hunk__get_fuzz_penalty(hunk); return SVN_NO_ERROR; } @@ -1877,8 +2136,6 @@ reject_hunk(patch_target_t *target, target_content_t *content, svn_diff_hunk_t *hunk, const char *prop_name, apr_pool_t *pool) { - const char *hunk_header; - apr_size_t len; svn_boolean_t eof; static const char * const text_atat = "@@"; static const char * const prop_atat = "##"; @@ -1887,14 +2144,9 @@ reject_hunk(patch_target_t *target, target_content_t *content, if (prop_name) { - const char *prop_header; - - /* ### Print 'Added', 'Deleted' or 'Modified' instead of 'Property'. - */ - prop_header = apr_psprintf(pool, "Property: %s\n", prop_name); - len = strlen(prop_header); - SVN_ERR(svn_io_file_write_full(target->reject_file, prop_header, - len, &len, pool)); + /* ### Print 'Added', 'Deleted' or 'Modified' instead of 'Property'. */ + svn_stream_printf(target->reject_stream, + pool, "Property: %s" APR_EOL_STR, prop_name); atat = prop_atat; } else @@ -1902,17 +2154,14 @@ reject_hunk(patch_target_t *target, target_content_t *content, atat = text_atat; } - hunk_header = apr_psprintf(pool, "%s -%lu,%lu +%lu,%lu %s%s", - atat, - svn_diff_hunk_get_original_start(hunk), - svn_diff_hunk_get_original_length(hunk), - svn_diff_hunk_get_modified_start(hunk), - svn_diff_hunk_get_modified_length(hunk), - atat, - APR_EOL_STR); - len = strlen(hunk_header); - SVN_ERR(svn_io_file_write_full(target->reject_file, hunk_header, len, - &len, pool)); + SVN_ERR(svn_stream_printf(target->reject_stream, pool, + "%s -%lu,%lu +%lu,%lu %s" APR_EOL_STR, + atat, + svn_diff_hunk_get_original_start(hunk), + svn_diff_hunk_get_original_length(hunk), + svn_diff_hunk_get_modified_start(hunk), + svn_diff_hunk_get_modified_length(hunk), + atat)); iterpool = svn_pool_create(pool); do @@ -1928,17 +2177,15 @@ reject_hunk(patch_target_t *target, target_content_t *content, { if (hunk_line->len >= 1) { - len = hunk_line->len; - SVN_ERR(svn_io_file_write_full(target->reject_file, - hunk_line->data, len, &len, - iterpool)); + apr_size_t len = hunk_line->len; + + SVN_ERR(svn_stream_write(target->reject_stream, + hunk_line->data, &len)); } if (eol_str) { - len = strlen(eol_str); - SVN_ERR(svn_io_file_write_full(target->reject_file, eol_str, - len, &len, iterpool)); + SVN_ERR(svn_stream_puts(target->reject_stream, eol_str)); } } } @@ -1965,6 +2212,7 @@ apply_hunk(patch_target_t *target, target_content_t *content, svn_linenum_t lines_read; svn_boolean_t eof; apr_pool_t *iterpool; + svn_linenum_t fuzz = hi->match_fuzz; /* ### Is there a cleaner way to describe if we have an existing target? */ @@ -1976,13 +2224,13 @@ apply_hunk(patch_target_t *target, target_content_t *content, * Also copy leading lines of context which matched with fuzz. * The target has changed on the fuzzy-matched lines, * so we should retain the target's version of those lines. */ - SVN_ERR(copy_lines_to_target(content, hi->matched_line + hi->fuzz, + SVN_ERR(copy_lines_to_target(content, hi->matched_line + fuzz, pool)); /* Skip the target's version of the hunk. * Don't skip trailing lines which matched with fuzz. */ line = content->current_line + - svn_diff_hunk_get_original_length(hi->hunk) - (2 * hi->fuzz); + svn_diff_hunk_get_original_length(hi->hunk) - (2 * fuzz); SVN_ERR(seek_to_line(content, line, pool)); if (content->current_line != line && ! content->eof) { @@ -2009,8 +2257,8 @@ apply_hunk(patch_target_t *target, target_content_t *content, &eol_str, &eof, iterpool, iterpool)); lines_read++; - if (lines_read > hi->fuzz && - lines_read <= svn_diff_hunk_get_modified_length(hi->hunk) - hi->fuzz) + if (lines_read > fuzz && + lines_read <= svn_diff_hunk_get_modified_length(hi->hunk) - fuzz) { apr_size_t len; @@ -2079,7 +2327,7 @@ send_hunk_notification(const hunk_info_t *hi, notify->hunk_modified_length = svn_diff_hunk_get_modified_length(hi->hunk); notify->hunk_matched_line = hi->matched_line; - notify->hunk_fuzz = hi->fuzz; + notify->hunk_fuzz = hi->report_fuzz; notify->prop_name = prop_name; ctx->notify_func2(ctx->notify_baton2, notify, pool); @@ -2092,7 +2340,7 @@ send_hunk_notification(const hunk_info_t *hi, static svn_error_t * send_patch_notification(const patch_target_t *target, const svn_client_ctx_t *ctx, - apr_pool_t *pool) + apr_pool_t *scratch_pool) { svn_wc_notify_t *notify; svn_wc_notify_action_t action; @@ -2105,7 +2353,7 @@ send_patch_notification(const patch_target_t *target, action = svn_wc_notify_skip; else if (target->deleted) action = svn_wc_notify_delete; - else if (target->added || target->replaced || target->move_target_abspath) + else if (target->added || target->move_target_abspath) action = svn_wc_notify_add; else action = svn_wc_notify_patch; @@ -2116,16 +2364,17 @@ send_patch_notification(const patch_target_t *target, notify_path = target->local_abspath ? target->local_abspath : target->local_relpath; - notify = svn_wc_create_notify(notify_path, action, pool); - notify->kind = svn_node_file; + notify = svn_wc_create_notify(notify_path, action, scratch_pool); + notify->kind = (target->db_kind == svn_node_dir) ? svn_node_dir + : svn_node_file; if (action == svn_wc_notify_skip) { - if (target->db_kind == svn_node_none || - target->db_kind == svn_node_unknown) - notify->content_state = svn_wc_notify_state_missing; - else if (target->db_kind == svn_node_dir) + if (target->obstructed) notify->content_state = svn_wc_notify_state_obstructed; + else if (target->db_kind == svn_node_none || + target->db_kind == svn_node_unknown) + notify->content_state = svn_wc_notify_state_missing; else notify->content_state = svn_wc_notify_state_unknown; } @@ -2133,26 +2382,32 @@ send_patch_notification(const patch_target_t *target, { if (target->had_rejects) notify->content_state = svn_wc_notify_state_conflicted; - else if (target->local_mods) - notify->content_state = svn_wc_notify_state_merged; else if (target->has_text_changes) notify->content_state = svn_wc_notify_state_changed; + else if (target->had_already_applied) + notify->content_state = svn_wc_notify_state_merged; + else + notify->content_state = svn_wc_notify_state_unchanged; if (target->had_prop_rejects) notify->prop_state = svn_wc_notify_state_conflicted; else if (target->has_prop_changes) notify->prop_state = svn_wc_notify_state_changed; + else if (target->had_prop_already_applied) + notify->prop_state = svn_wc_notify_state_merged; + else + notify->prop_state = svn_wc_notify_state_unchanged; } - ctx->notify_func2(ctx->notify_baton2, notify, pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); if (action == svn_wc_notify_patch) { int i; apr_pool_t *iterpool; - apr_hash_index_t *hash_index; + apr_array_header_t *prop_targets; - iterpool = svn_pool_create(pool); + iterpool = svn_pool_create(scratch_pool); for (i = 0; i < target->content->hunks->nelts; i++) { const hunk_info_t *hi; @@ -2165,26 +2420,30 @@ send_patch_notification(const patch_target_t *target, ctx, iterpool)); } - for (hash_index = apr_hash_first(pool, target->prop_targets); - hash_index; - hash_index = apr_hash_next(hash_index)) + prop_targets = svn_sort__hash(target->prop_targets, + svn_sort_compare_items_lexically, + scratch_pool); + for (i = 0; i < prop_targets->nelts; i++) { - prop_patch_target_t *prop_target; + int j; + svn_sort__item_t item = APR_ARRAY_IDX(prop_targets, i, + svn_sort__item_t); - prop_target = apr_hash_this_val(hash_index); + prop_patch_target_t *prop_target = item.value; - for (i = 0; i < prop_target->content->hunks->nelts; i++) + for (j = 0; j < prop_target->content->hunks->nelts; j++) { const hunk_info_t *hi; svn_pool_clear(iterpool); - hi = APR_ARRAY_IDX(prop_target->content->hunks, i, + hi = APR_ARRAY_IDX(prop_target->content->hunks, j, hunk_info_t *); /* Don't notify on the hunk level for added or deleted props. */ - if (prop_target->operation != svn_diff_op_added && + if ((prop_target->operation != svn_diff_op_added && prop_target->operation != svn_diff_op_deleted) + || hi->rejected || hi->already_applied) SVN_ERR(send_hunk_notification(hi, target, prop_target->name, ctx, iterpool)); } @@ -2192,13 +2451,13 @@ send_patch_notification(const patch_target_t *target, svn_pool_destroy(iterpool); } - if (target->move_target_abspath) + if (!target->skipped && target->move_target_abspath) { /* Notify about deletion of move source. */ notify = svn_wc_create_notify(target->local_abspath, - svn_wc_notify_delete, pool); + svn_wc_notify_delete, scratch_pool); notify->kind = svn_node_file; - ctx->notify_func2(ctx->notify_baton2, notify, pool); + ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); } return SVN_NO_ERROR; @@ -2252,7 +2511,8 @@ sort_matched_hunks(const void *a, const void *b) * in RESULT_POOL. Use WC_CTX as the working copy context. * STRIP_COUNT specifies the number of leading path components * which should be stripped from target paths in the patch. - * REMOVE_TEMPFILES, PATCH_FUNC, and PATCH_BATON as in svn_client_patch(). + * REMOVE_TEMPFILES is as in svn_client_patch(). + * TARGETS_INFO is for preserving info across calls. * IGNORE_WHITESPACE tells whether whitespace should be considered when * doing the matching. * Call cancel CANCEL_FUNC with baton CANCEL_BATON to trigger cancellation. @@ -2263,6 +2523,7 @@ apply_one_patch(patch_target_t **patch_target, svn_patch_t *patch, int strip_count, svn_boolean_t ignore_whitespace, svn_boolean_t remove_tempfiles, + const apr_array_header_t *targets_info, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *result_pool, apr_pool_t *scratch_pool) @@ -2273,9 +2534,11 @@ apply_one_patch(patch_target_t **patch_target, svn_patch_t *patch, static const svn_linenum_t MAX_FUZZ = 2; apr_hash_index_t *hash_index; svn_linenum_t previous_offset = 0; + apr_array_header_t *prop_targets; SVN_ERR(init_patch_target(&target, patch, abs_wc_path, wc_ctx, strip_count, - remove_tempfiles, result_pool, scratch_pool)); + remove_tempfiles, targets_info, + result_pool, scratch_pool)); if (target->skipped) { *patch_target = target; @@ -2283,79 +2546,196 @@ apply_one_patch(patch_target_t **patch_target, svn_patch_t *patch, } iterpool = svn_pool_create(scratch_pool); - /* Match hunks. */ - for (i = 0; i < patch->hunks->nelts; i++) + + if (patch->hunks && patch->hunks->nelts) { - svn_diff_hunk_t *hunk; - hunk_info_t *hi; - svn_linenum_t fuzz = 0; + /* Match hunks. */ + for (i = 0; i < patch->hunks->nelts; i++) + { + svn_diff_hunk_t *hunk; + hunk_info_t *hi; + svn_linenum_t fuzz = 0; - svn_pool_clear(iterpool); + svn_pool_clear(iterpool); - if (cancel_func) - SVN_ERR(cancel_func(cancel_baton)); + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); - hunk = APR_ARRAY_IDX(patch->hunks, i, svn_diff_hunk_t *); + hunk = APR_ARRAY_IDX(patch->hunks, i, svn_diff_hunk_t *); - /* Determine the line the hunk should be applied at. - * If no match is found initially, try with fuzz. */ - do - { - SVN_ERR(get_hunk_info(&hi, target, target->content, hunk, fuzz, - previous_offset, - ignore_whitespace, - FALSE /* is_prop_hunk */, - cancel_func, cancel_baton, - result_pool, iterpool)); - fuzz++; + /* Determine the line the hunk should be applied at. + * If no match is found initially, try with fuzz. */ + do + { + SVN_ERR(get_hunk_info(&hi, target, target->content, hunk, fuzz, + previous_offset, + ignore_whitespace, + FALSE /* is_prop_hunk */, + cancel_func, cancel_baton, + result_pool, iterpool)); + fuzz++; + } + while (hi->rejected && fuzz <= MAX_FUZZ && ! hi->already_applied); + + if (hi->matched_line) + previous_offset + = hi->matched_line - svn_diff_hunk_get_original_start(hunk); + + APR_ARRAY_PUSH(target->content->hunks, hunk_info_t *) = hi; } - while (hi->rejected && fuzz <= MAX_FUZZ && ! hi->already_applied); - if (hi->matched_line) - previous_offset - = hi->matched_line - svn_diff_hunk_get_original_start(hunk); + /* Hunks are applied in the order determined by the matched line and + this may be different from the order of the original lines. */ + svn_sort__array(target->content->hunks, sort_matched_hunks); - APR_ARRAY_PUSH(target->content->hunks, hunk_info_t *) = hi; - } + /* Apply or reject hunks. */ + for (i = 0; i < target->content->hunks->nelts; i++) + { + hunk_info_t *hi; + + svn_pool_clear(iterpool); + + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); - /* Hunks are applied in the order determined by the matched line and - this may be different from the order of the original lines. */ - svn_sort__array(target->content->hunks, sort_matched_hunks); + hi = APR_ARRAY_IDX(target->content->hunks, i, hunk_info_t *); + if (hi->already_applied) + { + target->had_already_applied = TRUE; + continue; + } + else if (hi->rejected) + SVN_ERR(reject_hunk(target, target->content, hi->hunk, + NULL /* prop_name */, + iterpool)); + else + SVN_ERR(apply_hunk(target, target->content, hi, + NULL /* prop_name */, iterpool)); + } - /* Apply or reject hunks. */ - for (i = 0; i < target->content->hunks->nelts; i++) + if (target->kind_on_disk == svn_node_file) + { + /* Copy any remaining lines to target. */ + SVN_ERR(copy_lines_to_target(target->content, 0, scratch_pool)); + if (! target->content->eof) + { + /* We could not copy the entire target file to the temporary + * file, and would truncate the target if we copied the + * temporary file on top of it. Skip this target. */ + target->skipped = TRUE; + } + } + } + else if (patch->binary_patch) { - hunk_info_t *hi; + svn_stream_t *orig_stream; + svn_boolean_t same; + + if (target->file) + orig_stream = svn_stream_from_aprfile2(target->file, TRUE, iterpool); + else + orig_stream = svn_stream_empty(iterpool); + SVN_ERR(svn_stream_contents_same2( + &same, orig_stream, + svn_diff_get_binary_diff_original_stream(patch->binary_patch, + iterpool), + iterpool)); svn_pool_clear(iterpool); - if (cancel_func) - SVN_ERR(cancel_func(cancel_baton)); + if (same) + { + /* The file in the working copy is identical to the one expected by + the patch... So we can write the result stream; no fuzz, + just a 100% match */ - hi = APR_ARRAY_IDX(target->content->hunks, i, hunk_info_t *); - if (hi->already_applied) - continue; - else if (hi->rejected) - SVN_ERR(reject_hunk(target, target->content, hi->hunk, - NULL /* prop_name */, - iterpool)); + target->has_text_changes = TRUE; + } else - SVN_ERR(apply_hunk(target, target->content, hi, - NULL /* prop_name */, iterpool)); - } + { + /* Perhaps the file is identical to the resulting version, implying + that the patch has already been applied */ + if (target->file) + { + apr_off_t start = 0; - if (target->kind_on_disk == svn_node_file) - { - /* Copy any remaining lines to target. */ - SVN_ERR(copy_lines_to_target(target->content, 0, scratch_pool)); - if (! target->content->eof) + SVN_ERR(svn_io_file_seek(target->file, APR_SET, &start, iterpool)); + + orig_stream = svn_stream_from_aprfile2(target->file, TRUE, iterpool); + } + else + orig_stream = svn_stream_empty(iterpool); + + SVN_ERR(svn_stream_contents_same2( + &same, orig_stream, + svn_diff_get_binary_diff_result_stream(patch->binary_patch, + iterpool), + iterpool)); + svn_pool_clear(iterpool); + + if (same) + target->had_already_applied = TRUE; + } + + if (same) { - /* We could not copy the entire target file to the temporary file, - * and would truncate the target if we copied the temporary file - * on top of it. Skip this target. */ + SVN_ERR(svn_stream_copy3( + svn_diff_get_binary_diff_result_stream(patch->binary_patch, + iterpool), + svn_stream_from_aprfile2(target->patched_file, TRUE, + iterpool), + cancel_func, cancel_baton, + iterpool)); + } + else + { + /* ### TODO: Implement a proper reject of a binary patch + + This should at least setup things for a proper notification, + and perhaps install a normal text conflict. Unlike normal unified + diff based patches we have all the versions we would need for + that in a much easier format than can be obtained from the patch + file. */ target->skipped = TRUE; } } + else if (target->move_target_abspath) + { + /* ### Why do we do this? + BH: I don't know, but if we don't do this some tests + on git style patches break. + + ### It would be much better to really move the actual file instead + of copying to a temporary file; move that to target and then + delete the original file + + ### BH: I have absolutely no idea if moving directories would work. + */ + if (target->kind_on_disk == svn_node_file) + { + /* Copy any remaining lines to target. (read: all lines) */ + SVN_ERR(copy_lines_to_target(target->content, 0, scratch_pool)); + if (!target->content->eof) + { + /* We could not copy the entire target file to the temporary + * file, and would truncate the target if we copied the + * temporary file on top of it. Skip this target. */ + target->skipped = TRUE; + } + } + } + + if (target->had_rejects || target->locally_deleted) + target->deleted = FALSE; + + if (target->added + && !(target->locally_deleted || target->db_kind == svn_node_none)) + { + target->added = FALSE; + } + + /* Assume nothing changed. Will be updated via property hunks */ + target->is_special = target->is_symlink; /* Match property hunks. */ for (hash_index = apr_hash_first(scratch_pool, patch->prop_patches); @@ -2369,8 +2749,8 @@ apply_one_patch(patch_target_t **patch_target, svn_patch_t *patch, prop_name = apr_hash_this_key(hash_index); prop_patch = apr_hash_this_val(hash_index); - if (! strcmp(prop_name, SVN_PROP_SPECIAL)) - target->is_special = TRUE; + if (!strcmp(prop_name, SVN_PROP_SPECIAL)) + target->is_special = (prop_patch->operation != svn_diff_op_deleted); /* We'll store matched hunks in prop_content. */ prop_target = svn_hash_gets(target->prop_targets, prop_name); @@ -2406,48 +2786,174 @@ apply_one_patch(patch_target_t **patch_target, svn_patch_t *patch, } } - /* Apply or reject property hunks. */ - for (hash_index = apr_hash_first(scratch_pool, target->prop_targets); - hash_index; - hash_index = apr_hash_next(hash_index)) + /* Match implied property hunks. */ + if (patch->new_executable_bit != svn_tristate_unknown + && patch->new_executable_bit != patch->old_executable_bit + && svn_hash_gets(target->prop_targets, SVN_PROP_EXECUTABLE) + && !svn_hash_gets(patch->prop_patches, SVN_PROP_EXECUTABLE)) { - prop_patch_target_t *prop_target; + hunk_info_t *hi; + svn_diff_hunk_t *hunk; + prop_patch_target_t *prop_target = svn_hash_gets(target->prop_targets, + SVN_PROP_EXECUTABLE); + + if (patch->new_executable_bit == svn_tristate_true) + SVN_ERR(svn_diff_hunk__create_adds_single_line( + &hunk, + SVN_PROP_EXECUTABLE_VALUE, + patch, + result_pool, + iterpool)); + else + SVN_ERR(svn_diff_hunk__create_deletes_single_line( + &hunk, + SVN_PROP_EXECUTABLE_VALUE, + patch, + result_pool, + iterpool)); + + /* Derive a hunk_info from hunk. */ + SVN_ERR(get_hunk_info(&hi, target, prop_target->content, + hunk, 0 /* fuzz */, 0 /* previous_offset */, + ignore_whitespace, + TRUE /* is_prop_hunk */, + cancel_func, cancel_baton, + result_pool, iterpool)); + APR_ARRAY_PUSH(prop_target->content->hunks, hunk_info_t *) = hi; + } + + if (patch->new_symlink_bit != svn_tristate_unknown + && patch->new_symlink_bit != patch->old_symlink_bit + && svn_hash_gets(target->prop_targets, SVN_PROP_SPECIAL) + && !svn_hash_gets(patch->prop_patches, SVN_PROP_SPECIAL)) + { + hunk_info_t *hi; + svn_diff_hunk_t *hunk; + + prop_patch_target_t *prop_target = svn_hash_gets(target->prop_targets, + SVN_PROP_SPECIAL); + + if (patch->new_symlink_bit == svn_tristate_true) + { + SVN_ERR(svn_diff_hunk__create_adds_single_line( + &hunk, + SVN_PROP_SPECIAL_VALUE, + patch, + result_pool, + iterpool)); + target->is_special = TRUE; + } + else + { + SVN_ERR(svn_diff_hunk__create_deletes_single_line( + &hunk, + SVN_PROP_SPECIAL_VALUE, + patch, + result_pool, + iterpool)); + target->is_special = FALSE; + } + + /* Derive a hunk_info from hunk. */ + SVN_ERR(get_hunk_info(&hi, target, prop_target->content, + hunk, 0 /* fuzz */, 0 /* previous_offset */, + ignore_whitespace, + TRUE /* is_prop_hunk */, + cancel_func, cancel_baton, + result_pool, iterpool)); + APR_ARRAY_PUSH(prop_target->content->hunks, hunk_info_t *) = hi; + } + + /* When the node is deleted or does not exist after the patch is applied + we should reject a few more property hunks that can't be applied even + though the source matched */ + if (target->deleted + || (!target->added && + (target->locally_deleted || target->db_kind == svn_node_none))) + { + for (hash_index = apr_hash_first(scratch_pool, target->prop_targets); + hash_index; + hash_index = apr_hash_next(hash_index)) + { + prop_patch_target_t *prop_target = apr_hash_this_val(hash_index); + + if (prop_target->operation == svn_diff_op_deleted) + continue; + + for (i = 0; i < prop_target->content->hunks->nelts; i++) + { + hunk_info_t *hi; + + hi = APR_ARRAY_IDX(prop_target->content->hunks, i, hunk_info_t*); + + if (hi->already_applied || hi->rejected) + continue; + else + { + hi->rejected = TRUE; + prop_target->skipped = TRUE; + + if (!target->deleted && !target->added) + target->skipped = TRUE; + } + } + } + } - prop_target = apr_hash_this_val(hash_index); + /* Apply or reject property hunks. */ - for (i = 0; i < prop_target->content->hunks->nelts; i++) + prop_targets = svn_sort__hash(target->prop_targets, + svn_sort_compare_items_lexically, + scratch_pool); + for (i = 0; i < prop_targets->nelts; i++) + { + svn_sort__item_t item = APR_ARRAY_IDX(prop_targets, i, svn_sort__item_t); + prop_patch_target_t *prop_target = item.value; + svn_boolean_t applied_one = FALSE; + int j; + + for (j = 0; j < prop_target->content->hunks->nelts; j++) { hunk_info_t *hi; svn_pool_clear(iterpool); - hi = APR_ARRAY_IDX(prop_target->content->hunks, i, + hi = APR_ARRAY_IDX(prop_target->content->hunks, j, hunk_info_t *); if (hi->already_applied) - continue; + { + target->had_prop_already_applied = TRUE; + continue; + } else if (hi->rejected) SVN_ERR(reject_hunk(target, prop_target->content, hi->hunk, prop_target->name, iterpool)); else - SVN_ERR(apply_hunk(target, prop_target->content, hi, - prop_target->name, - iterpool)); + { + SVN_ERR(apply_hunk(target, prop_target->content, hi, + prop_target->name, + iterpool)); + applied_one = TRUE; + } } - if (prop_target->content->existed) - { - /* Copy any remaining lines to target. */ - SVN_ERR(copy_lines_to_target(prop_target->content, 0, - scratch_pool)); - if (! prop_target->content->eof) - { - /* We could not copy the entire target property to the - * temporary file, and would truncate the target if we - * copied the temporary file on top of it. Skip this target. */ - target->skipped = TRUE; - } - } + if (!applied_one) + prop_target->skipped = TRUE; + + if (applied_one && prop_target->content->existed) + { + /* Copy any remaining lines to target. */ + SVN_ERR(copy_lines_to_target(prop_target->content, 0, + scratch_pool)); + if (! prop_target->content->eof) + { + /* We could not copy the entire target property to the + * temporary stream, and would truncate the target if we + * copied the temporary stream on top of it. Skip this target. */ + prop_target->skipped = TRUE; + } + } } svn_pool_destroy(iterpool); @@ -2460,57 +2966,9 @@ apply_one_patch(patch_target_t **patch_target, svn_patch_t *patch, * will be closed later in write_out_rejected_hunks(). */ if (target->kind_on_disk == svn_node_file) SVN_ERR(svn_io_file_close(target->file, scratch_pool)); - - SVN_ERR(svn_io_file_close(target->patched_file, scratch_pool)); } - if (! target->skipped) - { - apr_finfo_t working_file; - apr_finfo_t patched_file; - - /* Get sizes of the patched temporary file and the working file. - * We'll need those to figure out whether we should delete the - * patched file. */ - SVN_ERR(svn_io_stat(&patched_file, target->patched_path, - APR_FINFO_SIZE | APR_FINFO_LINK, scratch_pool)); - if (target->kind_on_disk == svn_node_file) - SVN_ERR(svn_io_stat(&working_file, target->local_abspath, - APR_FINFO_SIZE | APR_FINFO_LINK, scratch_pool)); - else - working_file.size = 0; - - if (patched_file.size == 0 && working_file.size > 0) - { - /* If a unidiff removes all lines from a file, that usually - * means deletion, so we can confidently schedule the target - * for deletion. In the rare case where the unidiff was really - * meant to replace a file with an empty one, this may not - * be desirable. But the deletion can easily be reverted and - * creating an empty file manually is not exactly hard either. */ - target->deleted = (target->db_kind == svn_node_file); - } - else if (patched_file.size == 0 && working_file.size == 0) - { - /* The target was empty or non-existent to begin with - * and no content was changed by patching. - * Report this as skipped if it didn't exist, unless in the special - * case of adding an empty file which has properties set on it or - * adding an empty file with a 'git diff' */ - if (target->kind_on_disk == svn_node_none - && ! target->has_prop_changes - && ! target->added) - target->skipped = TRUE; - } - else if (patched_file.size > 0 && working_file.size == 0) - { - /* The patch has created a file. */ - if (target->locally_deleted) - target->replaced = TRUE; - else if (target->db_kind == svn_node_none) - target->added = TRUE; - } - } + SVN_ERR(svn_io_file_close(target->patched_file, scratch_pool)); *patch_target = target; @@ -2520,6 +2978,9 @@ apply_one_patch(patch_target_t **patch_target, svn_patch_t *patch, /* Try to create missing parent directories for TARGET in the working copy * rooted at ABS_WC_PATH, and add the parents to version control. * If the parents cannot be created, mark the target as skipped. + * + * In dry run mode record missing parents in ALREADY_ADDED + * * Use client context CTX. If DRY_RUN is true, do not create missing * parents but issue notifications only. * Use SCRATCH_POOL for temporary allocations. */ @@ -2528,6 +2989,7 @@ create_missing_parents(patch_target_t *target, const char *abs_wc_path, svn_client_ctx_t *ctx, svn_boolean_t dry_run, + apr_array_header_t *targets_info, apr_pool_t *scratch_pool) { const char *local_abspath; @@ -2608,12 +3070,28 @@ create_missing_parents(patch_target_t *target, for (i = present_components; i < components->nelts - 1; i++) { const char *component; + patch_target_info_t *pti; svn_pool_clear(iterpool); + if (ctx->cancel_func) + SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); + component = APR_ARRAY_IDX(components, i, const char *); local_abspath = svn_dirent_join(local_abspath, component, scratch_pool); + + if (target_is_added(targets_info, local_abspath, iterpool)) + continue; + + pti = apr_pcalloc(targets_info->pool, sizeof(*pti)); + + pti->local_abspath = apr_pstrdup(targets_info->pool, + local_abspath); + pti->added = TRUE; + + APR_ARRAY_PUSH(targets_info, patch_target_info_t *) = pti; + if (dry_run) { if (ctx->notify_func2) @@ -2634,10 +3112,6 @@ create_missing_parents(patch_target_t *target, * to version control. Allow cancellation since we * have not modified the working copy yet for this * target. */ - - if (ctx->cancel_func) - SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); - SVN_ERR(svn_wc_add_from_disk3(ctx->wc_ctx, local_abspath, NULL /*props*/, FALSE /* skip checks */, @@ -2653,11 +3127,17 @@ create_missing_parents(patch_target_t *target, /* Install a patched TARGET into the working copy at ABS_WC_PATH. * Use client context CTX to retrieve WC_CTX, and possibly doing - * notifications. If DRY_RUN is TRUE, don't modify the working copy. + * notifications. + * + * Pass on ALREADY_ADDED to allow recording already added ancestors + * in dry-run mode. + * + * If DRY_RUN is TRUE, don't modify the working copy. * Do temporary allocations in POOL. */ static svn_error_t * install_patched_target(patch_target_t *target, const char *abs_wc_path, svn_client_ctx_t *ctx, svn_boolean_t dry_run, + apr_array_header_t *targets_info, apr_pool_t *pool) { if (target->deleted) @@ -2671,13 +3151,15 @@ install_patched_target(patch_target_t *target, const char *abs_wc_path, * notify about what we did before aborting. */ SVN_ERR(svn_wc_delete4(ctx->wc_ctx, target->local_abspath, FALSE /* keep_local */, FALSE, - NULL, NULL, NULL, NULL, pool)); + ctx->cancel_func, ctx->cancel_baton, + NULL, NULL /* notify */, + pool)); } } else { svn_node_kind_t parent_db_kind; - if (target->added || target->replaced) + if (target->added) { const char *parent_abspath; @@ -2707,7 +3189,7 @@ install_patched_target(patch_target_t *target, const char *abs_wc_path, } else SVN_ERR(create_missing_parents(target, abs_wc_path, ctx, - dry_run, pool)); + dry_run, targets_info, pool)); } else @@ -2723,6 +3205,8 @@ install_patched_target(patch_target_t *target, const char *abs_wc_path, || wc_kind != target->kind_on_disk) { target->skipped = TRUE; + if (wc_kind != target->kind_on_disk) + target->obstructed = TRUE; } } @@ -2739,6 +3223,8 @@ install_patched_target(patch_target_t *target, const char *abs_wc_path, SVN_ERR(svn_subst_create_specialfile(&stream, target->local_abspath, pool, pool)); + if (target->git_symlink_format) + SVN_ERR(svn_stream_puts(stream, "link ")); SVN_ERR(svn_stream_copy3(patched_stream, stream, ctx->cancel_func, ctx->cancel_baton, pool)); @@ -2766,7 +3252,7 @@ install_patched_target(patch_target_t *target, const char *abs_wc_path, ctx->cancel_func, ctx->cancel_baton, pool)); } - if (target->added || target->replaced) + if (target->added) { /* The target file didn't exist previously, * so add it to version control. @@ -2800,7 +3286,8 @@ install_patched_target(patch_target_t *target, const char *abs_wc_path, target->move_target_abspath, TRUE, /* metadata_only */ FALSE, /* allow_mixed_revisions */ - NULL, NULL, NULL, NULL, + ctx->cancel_func, ctx->cancel_baton, + NULL, NULL, pool)); /* Delete the patch target's old location from disk. */ @@ -2818,20 +3305,54 @@ install_patched_target(patch_target_t *target, const char *abs_wc_path, */ static svn_error_t * write_out_rejected_hunks(patch_target_t *target, + const char *root_abspath, svn_boolean_t dry_run, - apr_pool_t *pool) + apr_pool_t *scratch_pool) { - SVN_ERR(svn_io_file_close(target->reject_file, pool)); - if (! dry_run && (target->had_rejects || target->had_prop_rejects)) { /* Write out rejected hunks, if any. */ - SVN_ERR(svn_io_copy_file(target->reject_path, - apr_psprintf(pool, "%s.svnpatch.rej", - target->local_abspath), - FALSE, pool)); + apr_file_t *reject_file; + svn_error_t *err; + + err = svn_io_open_uniquely_named(&reject_file, NULL, + svn_dirent_dirname(target->local_abspath, + scratch_pool), + svn_dirent_basename( + target->local_abspath, + NULL), + ".svnpatch.rej", + svn_io_file_del_none, + scratch_pool, scratch_pool); + if (err && APR_STATUS_IS_ENOENT(err->apr_err)) + { + /* The hunk applies to a file in a directory which does not exist. + * Put the reject file into the working copy root instead. */ + svn_error_clear(err); + SVN_ERR(svn_io_open_uniquely_named(&reject_file, NULL, + root_abspath, + svn_dirent_basename( + target->local_abspath, + NULL), + ".svnpatch.rej", + svn_io_file_del_none, + scratch_pool, scratch_pool)); + } + else + SVN_ERR(err); + + SVN_ERR(svn_stream_reset(target->reject_stream)); + + /* svn_stream_copy3() closes the files for us */ + SVN_ERR(svn_stream_copy3(target->reject_stream, + svn_stream_from_aprfile2(reject_file, FALSE, + scratch_pool), + NULL, NULL, scratch_pool)); /* ### TODO mark file as conflicted. */ } + else + SVN_ERR(svn_stream_close(target->reject_stream)); + return SVN_NO_ERROR; } @@ -2845,6 +3366,13 @@ install_patched_prop_targets(patch_target_t *target, { apr_hash_index_t *hi; apr_pool_t *iterpool; + const char *local_abspath; + + /* Apply properties to a move target if there is one */ + if (target->move_target_abspath) + local_abspath = target->move_target_abspath; + else + local_abspath = target->local_abspath; iterpool = svn_pool_create(scratch_pool); @@ -2861,11 +3389,14 @@ install_patched_prop_targets(patch_target_t *target, if (ctx->cancel_func) SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); + if (prop_target->skipped) + continue; + /* For a deleted prop we only set the value to NULL. */ if (prop_target->operation == svn_diff_op_deleted) { if (! dry_run) - SVN_ERR(svn_wc_prop_set4(ctx->wc_ctx, target->local_abspath, + SVN_ERR(svn_wc_prop_set4(ctx->wc_ctx, local_abspath, prop_target->name, NULL, svn_depth_empty, TRUE /* skip_checks */, NULL /* changelist_filter */, @@ -2875,31 +3406,6 @@ install_patched_prop_targets(patch_target_t *target, continue; } - /* If the patch target doesn't exist yet, the patch wants to add an - * empty file with properties set on it. So create an empty file and - * add it to version control. But if the patch was in the 'git format' - * then the file has already been added. - * - * ### How can we tell whether the patch really wanted to create - * ### an empty directory? */ - if (! target->has_text_changes - && target->kind_on_disk == svn_node_none - && ! target->added) - { - if (! dry_run) - { - SVN_ERR(svn_io_file_create_empty(target->local_abspath, - scratch_pool)); - SVN_ERR(svn_wc_add_from_disk3(ctx->wc_ctx, target->local_abspath, - NULL /*props*/, - FALSE /* skip checks */, - /* suppress notification */ - NULL, NULL, - iterpool)); - } - target->added = TRUE; - } - /* Attempt to set the property, and reject all hunks if this fails. If the property had a non-empty value, but now has an empty one, we'll just delete the property altogether. */ @@ -2915,7 +3421,7 @@ install_patched_prop_targets(patch_target_t *target, err = svn_wc_canonicalize_svn_prop(&canon_propval, prop_target->name, - prop_val, target->local_abspath, + prop_val, local_abspath, target->db_kind, TRUE, /* ### Skipping checks */ NULL, NULL, @@ -2923,7 +3429,7 @@ install_patched_prop_targets(patch_target_t *target, } else { - err = svn_wc_prop_set4(ctx->wc_ctx, target->local_abspath, + err = svn_wc_prop_set4(ctx->wc_ctx, local_abspath, prop_target->name, prop_val, svn_depth_empty, TRUE /* skip_checks */, NULL /* changelist_filter */, @@ -3105,7 +3611,7 @@ static svn_error_t * apply_patches(/* The path to the patch file. */ const char *patch_abspath, /* The abspath to the working copy the patch should be applied to. */ - const char *abs_wc_path, + const char *root_abspath, /* Indicates whether we're doing a dry run. */ svn_boolean_t dry_run, /* Number of leading components to strip from patch target paths. */ @@ -3150,9 +3656,10 @@ apply_patches(/* The path to the patch file. */ patch_target_t *target; svn_boolean_t filtered = FALSE; - SVN_ERR(apply_one_patch(&target, patch, abs_wc_path, + SVN_ERR(apply_one_patch(&target, patch, root_abspath, ctx->wc_ctx, strip_count, ignore_whitespace, remove_tempfiles, + targets_info, ctx->cancel_func, ctx->cancel_baton, iterpool, iterpool)); @@ -3172,31 +3679,34 @@ apply_patches(/* The path to the patch file. */ target_info->local_abspath = apr_pstrdup(scratch_pool, target->local_abspath); target_info->deleted = target->deleted; + target_info->added = target->added; if (! target->skipped) { - APR_ARRAY_PUSH(targets_info, - patch_target_info_t *) = target_info; - if (target->has_text_changes || target->added || target->move_target_abspath || target->deleted) - SVN_ERR(install_patched_target(target, abs_wc_path, - ctx, dry_run, iterpool)); + SVN_ERR(install_patched_target(target, root_abspath, + ctx, dry_run, + targets_info, iterpool)); if (target->has_prop_changes && (!target->deleted)) SVN_ERR(install_patched_prop_targets(target, ctx, dry_run, iterpool)); - SVN_ERR(write_out_rejected_hunks(target, dry_run, iterpool)); - } + SVN_ERR(write_out_rejected_hunks(target, root_abspath, + dry_run, iterpool)); + + APR_ARRAY_PUSH(targets_info, + patch_target_info_t *) = target_info; + } SVN_ERR(send_patch_notification(target, ctx, iterpool)); if (target->deleted && !target->skipped) { SVN_ERR(check_ancestor_delete(target_info->local_abspath, - targets_info, abs_wc_path, + targets_info, root_abspath, dry_run, ctx, scratch_pool, iterpool)); } diff --git a/subversion/libsvn_client/ra.c b/subversion/libsvn_client/ra.c index f98b4b0708ee..d50e720bd0f1 100644 --- a/subversion/libsvn_client/ra.c +++ b/subversion/libsvn_client/ra.c @@ -727,7 +727,7 @@ repos_locations(const char **start_url, /* We'd better have all the paths we were looking for! */ if (start_url) { - start_path = apr_hash_get(rev_locs, &start_revnum, sizeof(svn_revnum_t)); + start_path = apr_hash_get(rev_locs, &start_revnum, sizeof(start_revnum)); if (! start_path) return svn_error_createf (SVN_ERR_CLIENT_UNRELATED_RESOURCES, NULL, @@ -739,7 +739,7 @@ repos_locations(const char **start_url, if (end_url) { - end_path = apr_hash_get(rev_locs, &end_revnum, sizeof(svn_revnum_t)); + end_path = apr_hash_get(rev_locs, &end_revnum, sizeof(end_revnum)); if (! end_path) return svn_error_createf (SVN_ERR_CLIENT_UNRELATED_RESOURCES, NULL, diff --git a/subversion/libsvn_client/relocate.c b/subversion/libsvn_client/relocate.c index dcd9017c75ca..2e5c1170f1ae 100644 --- a/subversion/libsvn_client/relocate.c +++ b/subversion/libsvn_client/relocate.c @@ -141,6 +141,8 @@ svn_client_relocate2(const char *wcroot_dir, apr_hash_index_t *hi; apr_pool_t *iterpool = NULL; const char *old_repos_root_url, *new_repos_root_url; + char *sig_from_prefix, *sig_to_prefix; + apr_size_t index_from, index_to; /* Populate our validator callback baton, and call the relocate code. */ vb.ctx = ctx; @@ -183,6 +185,21 @@ svn_client_relocate2(const char *wcroot_dir, if (! apr_hash_count(externals_hash)) return SVN_NO_ERROR; + /* A valid prefix for the main working copy may be too long to be + valid for an external. Trim any common trailing characters to + leave the significant part that changes. */ + sig_from_prefix = apr_pstrdup(pool, from_prefix); + sig_to_prefix = apr_pstrdup(pool, to_prefix); + index_from = strlen(sig_from_prefix); + index_to = strlen(sig_to_prefix); + while (index_from && index_to + && sig_from_prefix[index_from] == sig_to_prefix[index_to]) + { + sig_from_prefix[index_from] = sig_to_prefix[index_to] = '\0'; + --index_from; + --index_to; + } + iterpool = svn_pool_create(pool); for (hi = apr_hash_first(pool, externals_hash); @@ -218,7 +235,8 @@ svn_client_relocate2(const char *wcroot_dir, SVN_ERR(err); if (strcmp(old_repos_root_url, this_repos_root_url) == 0) - SVN_ERR(svn_client_relocate2(this_abspath, from_prefix, to_prefix, + SVN_ERR(svn_client_relocate2(this_abspath, + sig_from_prefix, sig_to_prefix, FALSE /* ignore_externals */, ctx, iterpool)); } diff --git a/subversion/libsvn_client/resolved.c b/subversion/libsvn_client/resolved.c index 8b94707ec89b..6cb5caef54b9 100644 --- a/subversion/libsvn_client/resolved.c +++ b/subversion/libsvn_client/resolved.c @@ -1,5 +1,5 @@ /* - * resolved.c: wrapper around wc resolved functionality. + * resolved.c: wrapper around Subversion <=1.9 wc resolved functionality * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one @@ -41,6 +41,7 @@ #include "private/svn_wc_private.h" #include "svn_private_config.h" + /*** Code. ***/ diff --git a/subversion/libsvn_client/revisions.c b/subversion/libsvn_client/revisions.c index e61e7d475f86..4bfbfc3fc32a 100644 --- a/subversion/libsvn_client/revisions.c +++ b/subversion/libsvn_client/revisions.c @@ -89,7 +89,9 @@ svn_client__get_revision_number(svn_revnum_t *revnum, /* The BASE, COMMITTED, and PREV revision keywords do not apply to URLs. */ if (svn_path_is_url(local_abspath)) - goto invalid_rev_arg; + return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, + _("PREV, BASE, or COMMITTED revision " + "keywords are invalid for URL")); err = svn_wc__node_get_origin(NULL, revnum, NULL, NULL, NULL, NULL, NULL, @@ -129,7 +131,9 @@ svn_client__get_revision_number(svn_revnum_t *revnum, /* The BASE, COMMITTED, and PREV revision keywords do not apply to URLs. */ if (svn_path_is_url(local_abspath)) - goto invalid_rev_arg; + return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, + _("PREV, BASE, or COMMITTED revision " + "keywords are invalid for URL")); SVN_ERR(svn_wc__node_get_changed_info(revnum, NULL, NULL, wc_ctx, local_abspath, @@ -183,10 +187,4 @@ svn_client__get_revision_number(svn_revnum_t *revnum, *revnum = *youngest_rev; return SVN_NO_ERROR; - - invalid_rev_arg: - return svn_error_create( - SVN_ERR_CLIENT_BAD_REVISION, NULL, - _("PREV, BASE, or COMMITTED revision keywords are invalid for URL")); - } diff --git a/subversion/libsvn_client/shelve.c b/subversion/libsvn_client/shelve.c new file mode 100644 index 000000000000..af8dd67bc605 --- /dev/null +++ b/subversion/libsvn_client/shelve.c @@ -0,0 +1,552 @@ +/* + * shelve.c: implementation of the 'shelve' commands + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + */ + +/* ==================================================================== */ + +/* We define this here to remove any further warnings about the usage of + experimental functions in this file. */ +#define SVN_EXPERIMENTAL + +#include "svn_client.h" +#include "svn_wc.h" +#include "svn_pools.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_hash.h" +#include "svn_utf.h" +#include "svn_ctype.h" + +#include "client.h" +#include "private/svn_client_private.h" +#include "private/svn_wc_private.h" +#include "svn_private_config.h" + + +static svn_error_t * +shelf_name_encode(char **encoded_name_p, + const char *name, + apr_pool_t *result_pool) +{ + char *encoded_name + = apr_palloc(result_pool, strlen(name) * 2 + 1); + char *out_pos = encoded_name; + + if (name[0] == '\0') + return svn_error_create(SVN_ERR_BAD_CHANGELIST_NAME, NULL, + _("Shelf name cannot be the empty string")); + + while (*name) + { + apr_snprintf(out_pos, 3, "%02x", (unsigned char)(*name++)); + out_pos += 2; + } + *encoded_name_p = encoded_name; + return SVN_NO_ERROR; +} + +static svn_error_t * +shelf_name_decode(char **decoded_name_p, + const char *codename, + apr_pool_t *result_pool) +{ + svn_stringbuf_t *sb + = svn_stringbuf_create_ensure(strlen(codename) / 2, result_pool); + const char *input = codename; + + while (*input) + { + int c; + int nchars; + int nitems = sscanf(input, "%02x%n", &c, &nchars); + + if (nitems != 1 || nchars != 2) + return svn_error_createf(SVN_ERR_BAD_CHANGELIST_NAME, NULL, + _("Shelve: Bad encoded name '%s'"), codename); + svn_stringbuf_appendbyte(sb, c); + input += 2; + } + *decoded_name_p = sb->data; + return SVN_NO_ERROR; +} + +/* Set *NAME to the shelf name from FILENAME. */ +static svn_error_t * +shelf_name_from_filename(char **name, + const char *filename, + apr_pool_t *result_pool) +{ + size_t len = strlen(filename); + + if (len > 6 && strcmp(filename + len - 6, ".patch") == 0) + { + char *codename = apr_pstrndup(result_pool, filename, len - 6); + SVN_ERR(shelf_name_decode(name, codename, result_pool)); + } + return SVN_NO_ERROR; +} + +/* Set *PATCH_ABSPATH to the abspath of the patch file for shelved change + * NAME, no matter whether it exists. + */ +static svn_error_t * +get_patch_abspath(char **patch_abspath, + const char *name, + const char *wc_root_abspath, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + char *dir; + char *filename; + + SVN_ERR(svn_wc__get_shelves_dir(&dir, ctx->wc_ctx, wc_root_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(shelf_name_encode(&filename, name, scratch_pool)); + filename = apr_pstrcat(scratch_pool, filename, ".patch", SVN_VA_NULL); + *patch_abspath = svn_dirent_join(dir, filename, result_pool); + return SVN_NO_ERROR; +} + +/** Write local changes to a patch file for shelved change @a name. + * + * @a message: An optional log message. + * + * @a wc_root_abspath: The WC root dir. + * + * @a overwrite_existing: If a file at @a patch_abspath exists, overwrite it. + * + * @a paths, @a depth, @a changelists: The selection of local paths to diff. + */ +static svn_error_t * +shelf_write_patch(const char *name, + const char *message, + const char *wc_root_abspath, + svn_boolean_t overwrite_existing, + const apr_array_header_t *paths, + svn_depth_t depth, + const apr_array_header_t *changelists, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + char *patch_abspath; + apr_int32_t flag; + apr_file_t *outfile; + svn_stream_t *outstream; + svn_stream_t *errstream; + apr_pool_t *iterpool = svn_pool_create(scratch_pool); + int i; + svn_opt_revision_t peg_revision = {svn_opt_revision_unspecified, {0}}; + svn_opt_revision_t start_revision = {svn_opt_revision_base, {0}}; + svn_opt_revision_t end_revision = {svn_opt_revision_working, {0}}; + + SVN_ERR(get_patch_abspath(&patch_abspath, name, wc_root_abspath, + ctx, scratch_pool, scratch_pool)); + + /* Get streams for the output and any error output of the diff. */ + /* ### svn_stream_open_writable() doesn't work here: the buffering + goes wrong so that diff headers appear after their hunks. + For now, fix by opening the file without APR_BUFFERED. */ + flag = APR_FOPEN_WRITE | APR_FOPEN_CREATE | APR_FOPEN_TRUNCATE; + if (! overwrite_existing) + flag |= APR_FOPEN_EXCL; + SVN_ERR(svn_io_file_open(&outfile, patch_abspath, + flag, APR_FPROT_OS_DEFAULT, scratch_pool)); + outstream = svn_stream_from_aprfile2(outfile, FALSE /*disown*/, scratch_pool); + SVN_ERR(svn_stream_for_stderr(&errstream, scratch_pool)); + + /* Write the patch file header (log message, etc.) */ + if (message) + { + SVN_ERR(svn_stream_printf(outstream, scratch_pool, "%s\n", + message)); + } + SVN_ERR(svn_stream_printf(outstream, scratch_pool, + "--This line, and those below, will be ignored--\n\n")); + SVN_ERR(svn_stream_printf(outstream, scratch_pool, + "--This patch was generated by 'svn shelve'--\n\n")); + + for (i = 0; i < paths->nelts; i++) + { + const char *path = APR_ARRAY_IDX(paths, i, const char *); + + if (svn_path_is_url(path)) + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + _("'%s' is not a local path"), path); + SVN_ERR(svn_dirent_get_absolute(&path, path, scratch_pool)); + + SVN_ERR(svn_client_diff_peg6( + NULL /*options*/, + path, + &peg_revision, + &start_revision, + &end_revision, + wc_root_abspath, + depth, + TRUE /*notice_ancestry*/, + FALSE /*no_diff_added*/, + FALSE /*no_diff_deleted*/, + TRUE /*show_copies_as_adds*/, + FALSE /*ignore_content_type: FALSE -> omit binary files*/, + FALSE /*ignore_properties*/, + FALSE /*properties_only*/, + FALSE /*use_git_diff_format*/, + SVN_APR_LOCALE_CHARSET, + outstream, + errstream, + changelists, + ctx, iterpool)); + } + SVN_ERR(svn_stream_close(outstream)); + SVN_ERR(svn_stream_close(errstream)); + + return SVN_NO_ERROR; +} + +/** Apply the patch file for shelved change @a name to the WC. + * + * @a wc_root_abspath: The WC root dir. + * + * @a reverse: Apply the patch in reverse. + * + * @a dry_run: Don't really apply the changes, just notify what would be done. + */ +static svn_error_t * +shelf_apply_patch(const char *name, + const char *wc_root_abspath, + svn_boolean_t reverse, + svn_boolean_t dry_run, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + char *patch_abspath; + + SVN_ERR(get_patch_abspath(&patch_abspath, name, wc_root_abspath, + ctx, scratch_pool, scratch_pool)); + SVN_ERR(svn_client_patch(patch_abspath, wc_root_abspath, + dry_run, 0 /*strip*/, + reverse, + FALSE /*ignore_whitespace*/, + TRUE /*remove_tempfiles*/, NULL, NULL, + ctx, scratch_pool)); + + return SVN_NO_ERROR; +} + +/** Delete the patch file for shelved change @a name. + * + * @a wc_root_abspath: The WC root dir. + */ +static svn_error_t * +shelf_delete_patch(const char *name, + const char *wc_root_abspath, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + char *patch_abspath, *to_abspath; + + SVN_ERR(get_patch_abspath(&patch_abspath, name, wc_root_abspath, + ctx, scratch_pool, scratch_pool)); + to_abspath = apr_pstrcat(scratch_pool, patch_abspath, ".bak", SVN_VA_NULL); + + /* remove any previous backup */ + SVN_ERR(svn_io_remove_file2(to_abspath, TRUE /*ignore_enoent*/, + scratch_pool)); + + /* move the patch to a backup file */ + SVN_ERR(svn_io_file_rename2(patch_abspath, to_abspath, FALSE /*flush_to_disk*/, + scratch_pool)); + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_shelve(const char *name, + const apr_array_header_t *paths, + svn_depth_t depth, + const apr_array_header_t *changelists, + svn_boolean_t keep_local, + svn_boolean_t dry_run, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + const char *local_abspath; + const char *wc_root_abspath; + const char *message = ""; + svn_error_t *err; + + /* ### TODO: check all paths are in same WC; for now use first path */ + SVN_ERR(svn_dirent_get_absolute(&local_abspath, + APR_ARRAY_IDX(paths, 0, char *), pool)); + SVN_ERR(svn_client_get_wc_root(&wc_root_abspath, + local_abspath, ctx, pool, pool)); + + /* Fetch the log message and any other revprops */ + if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx)) + { + const char *tmp_file; + apr_array_header_t *commit_items = apr_array_make(pool, 1, sizeof(void *)); + + SVN_ERR(svn_client__get_log_msg(&message, &tmp_file, commit_items, + ctx, pool)); + if (! message) + return SVN_NO_ERROR; + } + + err = shelf_write_patch(name, message, wc_root_abspath, + FALSE /*overwrite_existing*/, + paths, depth, changelists, + ctx, pool); + if (err && APR_STATUS_IS_EEXIST(err->apr_err)) + { + return svn_error_quick_wrapf(err, + "Shelved change '%s' already exists", + name); + } + else + SVN_ERR(err); + + if (!keep_local) + { + /* Reverse-apply the patch. This should be a safer way to remove those + changes from the WC than running a 'revert' operation. */ + SVN_ERR(shelf_apply_patch(name, wc_root_abspath, + TRUE /*reverse*/, dry_run, + ctx, pool)); + } + + if (dry_run) + { + SVN_ERR(shelf_delete_patch(name, wc_root_abspath, + ctx, pool)); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_unshelve(const char *name, + const char *local_abspath, + svn_boolean_t keep, + svn_boolean_t dry_run, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + const char *wc_root_abspath; + svn_error_t *err; + + SVN_ERR(svn_client_get_wc_root(&wc_root_abspath, + local_abspath, ctx, pool, pool)); + + /* Apply the patch. */ + err = shelf_apply_patch(name, wc_root_abspath, + FALSE /*reverse*/, dry_run, + ctx, pool); + if (err && err->apr_err == SVN_ERR_ILLEGAL_TARGET) + { + return svn_error_quick_wrapf(err, + "Shelved change '%s' not found", + name); + } + else + SVN_ERR(err); + + /* Remove the patch. */ + if (! keep && ! dry_run) + { + SVN_ERR(shelf_delete_patch(name, wc_root_abspath, + ctx, pool)); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_shelves_delete(const char *name, + const char *local_abspath, + svn_boolean_t dry_run, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + const char *wc_root_abspath; + + SVN_ERR(svn_client_get_wc_root(&wc_root_abspath, + local_abspath, ctx, pool, pool)); + + /* Remove the patch. */ + if (! dry_run) + { + svn_error_t *err; + + err = shelf_delete_patch(name, wc_root_abspath, + ctx, pool); + if (err && APR_STATUS_IS_ENOENT(err->apr_err)) + { + return svn_error_quick_wrapf(err, + "Shelved change '%s' not found", + name); + } + else + SVN_ERR(err); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_shelf_get_paths(apr_hash_t **affected_paths, + const char *name, + const char *local_abspath, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *wc_root_abspath; + char *patch_abspath; + svn_patch_file_t *patch_file; + apr_pool_t *iterpool = svn_pool_create(scratch_pool); + apr_hash_t *paths = apr_hash_make(result_pool); + + SVN_ERR(svn_client_get_wc_root(&wc_root_abspath, + local_abspath, ctx, scratch_pool, scratch_pool)); + SVN_ERR(get_patch_abspath(&patch_abspath, name, wc_root_abspath, + ctx, scratch_pool, scratch_pool)); + SVN_ERR(svn_diff_open_patch_file(&patch_file, patch_abspath, result_pool)); + + while (1) + { + svn_patch_t *patch; + + svn_pool_clear(iterpool); + SVN_ERR(svn_diff_parse_next_patch(&patch, patch_file, + FALSE /*reverse*/, + FALSE /*ignore_whitespace*/, + iterpool, iterpool)); + if (! patch) + break; + svn_hash_sets(paths, + apr_pstrdup(result_pool, patch->old_filename), + apr_pstrdup(result_pool, patch->new_filename)); + } + SVN_ERR(svn_diff_close_patch_file(patch_file, iterpool)); + svn_pool_destroy(iterpool); + + *affected_paths = paths; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_shelf_has_changes(svn_boolean_t *has_changes, + const char *name, + const char *local_abspath, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_hash_t *patch_paths; + + SVN_ERR(svn_client_shelf_get_paths(&patch_paths, name, local_abspath, + ctx, scratch_pool, scratch_pool)); + *has_changes = (apr_hash_count(patch_paths) != 0); + return SVN_NO_ERROR; +} + +/* Set *LOGMSG to the log message stored in the file PATCH_ABSPATH. + * + * ### Currently just reads the first line. + */ +static svn_error_t * +read_logmsg_from_patch(const char **logmsg, + const char *patch_abspath, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_file_t *file; + svn_stream_t *stream; + svn_boolean_t eof; + svn_stringbuf_t *line; + + SVN_ERR(svn_io_file_open(&file, patch_abspath, + APR_FOPEN_READ, APR_FPROT_OS_DEFAULT, scratch_pool)); + stream = svn_stream_from_aprfile2(file, FALSE /*disown*/, scratch_pool); + SVN_ERR(svn_stream_readline(stream, &line, "\n", &eof, result_pool)); + SVN_ERR(svn_stream_close(stream)); + *logmsg = line->data; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_shelves_list(apr_hash_t **shelved_patch_infos, + const char *local_abspath, + svn_client_ctx_t *ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + char *shelves_dir; + apr_hash_t *dirents; + apr_hash_index_t *hi; + + SVN_ERR(svn_wc__get_shelves_dir(&shelves_dir, ctx->wc_ctx, local_abspath, + scratch_pool, scratch_pool)); + SVN_ERR(svn_io_get_dirents3(&dirents, shelves_dir, FALSE /*only_check_type*/, + result_pool, scratch_pool)); + + *shelved_patch_infos = apr_hash_make(result_pool); + + /* Remove non-shelves */ + for (hi = apr_hash_first(scratch_pool, dirents); hi; hi = apr_hash_next(hi)) + { + const char *filename = apr_hash_this_key(hi); + svn_io_dirent2_t *dirent = apr_hash_this_val(hi); + char *name = NULL; + + svn_error_clear(shelf_name_from_filename(&name, filename, result_pool)); + if (name && dirent->kind == svn_node_file) + { + svn_client_shelved_patch_info_t *info + = apr_palloc(result_pool, sizeof(*info)); + + info->dirent = dirent; + info->mtime = info->dirent->mtime; + info->patch_path + = svn_dirent_join(shelves_dir, filename, result_pool); + SVN_ERR(read_logmsg_from_patch(&info->message, info->patch_path, + result_pool, scratch_pool)); + + svn_hash_sets(*shelved_patch_infos, name, info); + } + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client_shelves_any(svn_boolean_t *any_shelved, + const char *local_abspath, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_hash_t *shelved_patch_infos; + + SVN_ERR(svn_client_shelves_list(&shelved_patch_infos, local_abspath, + ctx, scratch_pool, scratch_pool)); + *any_shelved = apr_hash_count(shelved_patch_infos) != 0; + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_client/upgrade.c b/subversion/libsvn_client/upgrade.c index 5677b1cf59b1..741443af2ddc 100644 --- a/subversion/libsvn_client/upgrade.c +++ b/subversion/libsvn_client/upgrade.c @@ -179,6 +179,122 @@ svn_client_upgrade(const char *path, return SVN_NO_ERROR; } +/* Helper for upgrade_externals_from_properties: upgrades one external ITEM + in EXTERNALS_PARENT. Uses SCRATCH_POOL for temporary allocations. */ +static svn_error_t * +upgrade_external_item(svn_client_ctx_t *ctx, + const char *externals_parent_abspath, + const char *externals_parent_url, + const char *externals_parent_repos_root_url, + svn_wc_external_item2_t *item, + struct repos_info_baton *info_baton, + apr_pool_t *scratch_pool) +{ + const char *resolved_url; + const char *external_abspath; + const char *repos_relpath; + const char *repos_root_url; + const char *repos_uuid; + svn_node_kind_t external_kind; + svn_revnum_t peg_revision; + svn_revnum_t revision; + svn_error_t *err; + + external_abspath = svn_dirent_join(externals_parent_abspath, + item->target_dir, + scratch_pool); + + SVN_ERR(svn_wc__resolve_relative_external_url( + &resolved_url, + item, + externals_parent_repos_root_url, + externals_parent_url, + scratch_pool, scratch_pool)); + + /* This is a hack. We only need to call svn_wc_upgrade() on external + * dirs, as file externals are upgraded along with their defining + * WC. Reading the kind will throw an exception on an external dir, + * saying that the wc must be upgraded. If it's a file, the lookup + * is done in an adm_dir belonging to the defining wc (which has + * already been upgraded) and no error is returned. If it doesn't + * exist (external that isn't checked out yet), we'll just get + * svn_node_none. */ + err = svn_wc_read_kind2(&external_kind, ctx->wc_ctx, + external_abspath, TRUE, FALSE, scratch_pool); + if (err && err->apr_err == SVN_ERR_WC_UPGRADE_REQUIRED) + { + svn_error_clear(err); + + SVN_ERR(svn_client_upgrade(external_abspath, ctx, scratch_pool)); + } + else if (err) + return svn_error_trace(err); + + /* The upgrade of any dir should be done now, get the now reliable + * kind. */ + SVN_ERR(svn_wc_read_kind2(&external_kind, ctx->wc_ctx, external_abspath, + TRUE, FALSE, scratch_pool)); + + /* Update the EXTERNALS table according to the root URL, + * relpath and uuid known in the upgraded external WC. */ + + /* We should probably have a function that provides all three + * of root URL, repos relpath and uuid at once, but here goes... */ + + /* First get the relpath, as that returns SVN_ERR_WC_PATH_NOT_FOUND + * when the node is not present in the file system. + * svn_wc__node_get_repos_info() would try to derive the URL. */ + SVN_ERR(svn_wc__node_get_repos_info(NULL, + &repos_relpath, + &repos_root_url, + &repos_uuid, + ctx->wc_ctx, + external_abspath, + scratch_pool, scratch_pool)); + + /* If we haven't got any information from the checked out external, + * or if the URL information mismatches the external's definition, + * ask fetch_repos_info() to find out the repos root. */ + if (0 != strcmp(resolved_url, + svn_path_url_add_component2(repos_root_url, + repos_relpath, + scratch_pool))) + { + SVN_ERR(fetch_repos_info(&repos_root_url, &repos_uuid, info_baton, + resolved_url, scratch_pool, scratch_pool)); + + repos_relpath = svn_uri_skip_ancestor(repos_root_url, + resolved_url, + scratch_pool); + + /* There's just the URL, no idea what kind the external is. + * That's fine, as the external isn't even checked out yet. + * The kind will be set during the next 'update'. */ + external_kind = svn_node_unknown; + } + + peg_revision = (item->peg_revision.kind == svn_opt_revision_number + ? item->peg_revision.value.number + : SVN_INVALID_REVNUM); + + revision = (item->revision.kind == svn_opt_revision_number + ? item->revision.value.number + : SVN_INVALID_REVNUM); + + SVN_ERR(svn_wc__upgrade_add_external_info(ctx->wc_ctx, + external_abspath, + external_kind, + externals_parent_abspath, + repos_relpath, + repos_root_url, + repos_uuid, + peg_revision, + revision, + scratch_pool)); + + return SVN_NO_ERROR; +} + static svn_error_t * upgrade_externals_from_properties(svn_client_ctx_t *ctx, const char *local_abspath, @@ -207,34 +323,32 @@ upgrade_externals_from_properties(svn_client_ctx_t *ctx, hi = apr_hash_next(hi)) { int i; - const char *externals_parent_abspath; const char *externals_parent_url; const char *externals_parent_repos_root_url; const char *externals_parent_repos_relpath; - const char *externals_parent = apr_hash_this_key(hi); + const char *externals_parent_abspath = apr_hash_this_key(hi); svn_string_t *external_desc = apr_hash_this_val(hi); apr_array_header_t *externals_p; svn_error_t *err; svn_pool_clear(iterpool); + + /* svn_client_propget5() has API promise to return absolute paths. */ + SVN_ERR_ASSERT(svn_dirent_is_absolute(externals_parent_abspath)); + externals_p = apr_array_make(iterpool, 1, sizeof(svn_wc_external_item2_t*)); /* In this loop, an error causes the respective externals definition, or * the external (inner loop), to be skipped, so that upgrade carries on * with the other externals. */ - - err = svn_dirent_get_absolute(&externals_parent_abspath, - externals_parent, iterpool); - - if (!err) - err = svn_wc__node_get_repos_info(NULL, - &externals_parent_repos_relpath, - &externals_parent_repos_root_url, - NULL, - ctx->wc_ctx, - externals_parent_abspath, - iterpool, iterpool); + err = svn_wc__node_get_repos_info(NULL, + &externals_parent_repos_relpath, + &externals_parent_repos_root_url, + NULL, + ctx->wc_ctx, + externals_parent_abspath, + iterpool, iterpool); if (!err) externals_parent_url = svn_path_url_add_component2( @@ -248,7 +362,7 @@ upgrade_externals_from_properties(svn_client_ctx_t *ctx, if (err) { svn_wc_notify_t *notify = - svn_wc_create_notify(externals_parent, + svn_wc_create_notify(externals_parent_abspath, svn_wc_notify_failed_external, scratch_pool); notify->err = err; @@ -265,130 +379,21 @@ upgrade_externals_from_properties(svn_client_ctx_t *ctx, for (i = 0; i < externals_p->nelts; i++) { svn_wc_external_item2_t *item; - const char *resolved_url; - const char *external_abspath; - const char *repos_relpath; - const char *repos_root_url; - const char *repos_uuid; - svn_node_kind_t external_kind; - svn_revnum_t peg_revision; - svn_revnum_t revision; item = APR_ARRAY_IDX(externals_p, i, svn_wc_external_item2_t*); svn_pool_clear(iterpool2); - external_abspath = svn_dirent_join(externals_parent_abspath, - item->target_dir, - iterpool2); - - err = svn_wc__resolve_relative_external_url( - &resolved_url, - item, - externals_parent_repos_root_url, - externals_parent_url, - scratch_pool, scratch_pool); - if (err) - goto handle_error; - - /* This is a hack. We only need to call svn_wc_upgrade() on external - * dirs, as file externals are upgraded along with their defining - * WC. Reading the kind will throw an exception on an external dir, - * saying that the wc must be upgraded. If it's a file, the lookup - * is done in an adm_dir belonging to the defining wc (which has - * already been upgraded) and no error is returned. If it doesn't - * exist (external that isn't checked out yet), we'll just get - * svn_node_none. */ - err = svn_wc_read_kind2(&external_kind, ctx->wc_ctx, - external_abspath, TRUE, FALSE, iterpool2); - if (err && err->apr_err == SVN_ERR_WC_UPGRADE_REQUIRED) - { - svn_error_clear(err); + err = upgrade_external_item(ctx, externals_parent_abspath, + externals_parent_url, + externals_parent_repos_root_url, + item, info_baton, iterpool2); - err = svn_client_upgrade(external_abspath, ctx, iterpool2); - if (err) - goto handle_error; - } - else if (err) - goto handle_error; - - /* The upgrade of any dir should be done now, get the now reliable - * kind. */ - err = svn_wc_read_kind2(&external_kind, ctx->wc_ctx, external_abspath, - TRUE, FALSE, iterpool2); - if (err) - goto handle_error; - - /* Update the EXTERNALS table according to the root URL, - * relpath and uuid known in the upgraded external WC. */ - - /* We should probably have a function that provides all three - * of root URL, repos relpath and uuid at once, but here goes... */ - - /* First get the relpath, as that returns SVN_ERR_WC_PATH_NOT_FOUND - * when the node is not present in the file system. - * svn_wc__node_get_repos_info() would try to derive the URL. */ - err = svn_wc__node_get_repos_info(NULL, - &repos_relpath, - &repos_root_url, - &repos_uuid, - ctx->wc_ctx, - external_abspath, - iterpool2, iterpool2); - if (err) - goto handle_error; - - /* If we haven't got any information from the checked out external, - * or if the URL information mismatches the external's definition, - * ask fetch_repos_info() to find out the repos root. */ - if (0 != strcmp(resolved_url, - svn_path_url_add_component2(repos_root_url, - repos_relpath, - scratch_pool))) - { - err = fetch_repos_info(&repos_root_url, - &repos_uuid, - info_baton, - resolved_url, - scratch_pool, scratch_pool); - if (err) - goto handle_error; - - repos_relpath = svn_uri_skip_ancestor(repos_root_url, - resolved_url, - iterpool2); - - /* There's just the URL, no idea what kind the external is. - * That's fine, as the external isn't even checked out yet. - * The kind will be set during the next 'update'. */ - external_kind = svn_node_unknown; - } - - if (err) - goto handle_error; - - peg_revision = (item->peg_revision.kind == svn_opt_revision_number - ? item->peg_revision.value.number - : SVN_INVALID_REVNUM); - - revision = (item->revision.kind == svn_opt_revision_number - ? item->revision.value.number - : SVN_INVALID_REVNUM); - - err = svn_wc__upgrade_add_external_info(ctx->wc_ctx, - external_abspath, - external_kind, - externals_parent, - repos_relpath, - repos_root_url, - repos_uuid, - peg_revision, - revision, - iterpool2); -handle_error: if (err) { svn_wc_notify_t *notify = - svn_wc_create_notify(external_abspath, + svn_wc_create_notify(svn_dirent_join(externals_parent_abspath, + item->target_dir, + iterpool2), svn_wc_notify_failed_external, scratch_pool); notify->err = err; |