| /* |
| * patch.c: patch application support |
| * |
| * ==================================================================== |
| * 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 <apr_hash.h> |
| #include <apr_fnmatch.h> |
| #include "svn_client.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_diff.h" |
| #include "svn_hash.h" |
| #include "svn_io.h" |
| #include "svn_path.h" |
| #include "svn_pools.h" |
| #include "svn_props.h" |
| #include "svn_sorts.h" |
| #include "svn_subst.h" |
| #include "svn_wc.h" |
| #include "client.h" |
| |
| #include "svn_private_config.h" |
| #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" |
| |
| typedef struct hunk_info_t { |
| /* The hunk. */ |
| svn_diff_hunk_t *hunk; |
| |
| /* The line where the hunk matched in the target file. */ |
| svn_linenum_t matched_line; |
| |
| /* Whether this hunk has been rejected. */ |
| svn_boolean_t rejected; |
| |
| /* Whether this hunk has already been applied (either manually |
| * or by an earlier run of patch). */ |
| svn_boolean_t already_applied; |
| |
| /* 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 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 |
| * content of a target, be it a property or the text of a file. */ |
| typedef struct target_content_t { |
| /* Indicates whether unpatched content existed prior to patching. */ |
| svn_boolean_t existed; |
| |
| /* The line last read from the unpatched content. */ |
| svn_linenum_t current_line; |
| |
| /* The EOL-style of the unpatched content. Either 'none', 'fixed', |
| * or 'native'. See the documentation of svn_subst_eol_style_t. */ |
| svn_subst_eol_style_t eol_style; |
| |
| /* If the EOL_STYLE above is not 'none', this is the EOL string |
| * corresponding to the EOL-style. Else, it is the EOL string the |
| * last line read from the target file was using. */ |
| const char *eol_str; |
| |
| /* An array containing apr_off_t offsets marking the beginning of |
| * each line in the unpatched content. */ |
| apr_array_header_t *lines; |
| |
| /* An array containing hunk_info_t structures for hunks already matched. */ |
| apr_array_header_t *hunks; |
| |
| /* True if end-of-file was reached while reading from the unpatched |
| * content. */ |
| svn_boolean_t eof; |
| |
| /* The keywords of the target. They will be contracted when reading |
| * unpatched content and expanded when writing patched content. |
| * When patching properties this hash is always empty. */ |
| apr_hash_t *keywords; |
| |
| /* A callback, with an associated baton, to read a line of unpatched |
| * content. */ |
| svn_error_t *(*readline)(void *baton, svn_stringbuf_t **line, |
| const char **eol_str, svn_boolean_t *eof, |
| apr_pool_t *result_pool, apr_pool_t *scratch_pool); |
| void *read_baton; |
| |
| /* A callback to get the current byte offset within the unpatched |
| * content. Uses the read baton. */ |
| svn_error_t * (*tell)(void *baton, apr_off_t *offset, |
| apr_pool_t *scratch_pool); |
| |
| /* A callback to seek to an offset within the unpatched content. |
| * Uses the read baton. */ |
| svn_error_t * (*seek)(void *baton, apr_off_t offset, |
| apr_pool_t *scratch_pool); |
| |
| /* A callback to write data to the patched content, with an |
| * associated baton. */ |
| svn_error_t * (*write)(void *baton, const char *buf, apr_size_t len, |
| apr_pool_t *scratch_pool); |
| void *write_baton; |
| |
| } target_content_t; |
| |
| typedef struct prop_patch_target_t { |
| |
| /* The name of the property */ |
| const char *name; |
| |
| /* The property value. This is NULL in case the property did not exist |
| * prior to patch application (see also CONTENT->existed). |
| * Note that the patch implementation does not support binary properties, |
| * so this string is not expected to contain embedded NUL characters. */ |
| const svn_string_t *value; |
| |
| /* The patched property value. |
| * This is equivalent to the target, except that in appropriate |
| * places it contains the modified text as it appears in the patch file. */ |
| svn_stringbuf_t *patched_value; |
| |
| /* All information that is specific to the content of the property. */ |
| target_content_t *content; |
| |
| /* Represents the operation performed on the property. It can be added, |
| * deleted or modified. |
| * ### 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; |
| |
| typedef struct patch_target_t { |
| /* The target path as it appeared in the patch file, |
| * but in canonicalised form. */ |
| const char *canon_path_from_patchfile; |
| |
| /* The target path, relative to the working copy directory the |
| * patch is being applied to. A patch strip count applies to this |
| * and only this path. This is never NULL. */ |
| const char *local_relpath; |
| |
| /* The absolute path of the target on the filesystem. |
| * Any symlinks the path from the patch file may contain are resolved. |
| * Is not always known, so it may be NULL. */ |
| const char *local_abspath; |
| |
| /* The target file, read-only. This is NULL in case the target |
| * file did not exist prior to patch application (see also |
| * CONTENT->existed). */ |
| apr_file_t *file; |
| |
| /* The target file is a symlink */ |
| svn_boolean_t is_symlink; |
| |
| /* The patched file. |
| * This is equivalent to the target, except that in appropriate |
| * places it contains the modified text as it appears in the patch file. |
| * The data in this file is written in repository-normal form. |
| * EOL transformation and keyword contraction is performed when the |
| * patched result is installed in the working copy. */ |
| apr_file_t *patched_file; |
| |
| /* Path to the patched file. */ |
| const char *patched_path; |
| |
| /* Hunks that are rejected will be written to this stream. */ |
| svn_stream_t *reject_stream; |
| |
| /* Path to the reject file. */ |
| const char *reject_path; |
| |
| /* The node kind of the target as found in WC-DB prior |
| * to patch application. */ |
| svn_node_kind_t db_kind; |
| |
| /* The target's kind on disk prior to patch application. */ |
| svn_node_kind_t kind_on_disk; |
| |
| /* True if the target was locally deleted prior to patching. */ |
| svn_boolean_t locally_deleted; |
| |
| /* 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 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. */ |
| svn_boolean_t added; |
| |
| /* True if the target ended up being deleted by the patch. */ |
| svn_boolean_t deleted; |
| |
| /* 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; |
| |
| /* True if the target has the executable bit set. */ |
| svn_boolean_t executable; |
| |
| /* True if the patch changed the text of the target. */ |
| svn_boolean_t has_text_changes; |
| |
| /* True if the patch changed any of the properties of the target. */ |
| svn_boolean_t has_prop_changes; |
| |
| /* True if the patch contained a svn:special property. */ |
| svn_boolean_t is_special; |
| |
| /* All the information that is specific to the content of the target. */ |
| target_content_t *content; |
| |
| /* 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; |
| |
| |
| /* A smaller struct containing a subset of patch_target_t. |
| * Carries the minimal amount of information we still need for a |
| * target after we're done patching it so we can free other resources. */ |
| 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. |
| * Do temporary allocations in SCRATCH_POOL. */ |
| static svn_error_t * |
| strip_path(const char **result, const char *path, int strip_count, |
| apr_pool_t *result_pool, apr_pool_t *scratch_pool) |
| { |
| int i; |
| apr_array_header_t *components; |
| apr_array_header_t *stripped; |
| |
| components = svn_path_decompose(path, scratch_pool); |
| if (strip_count > components->nelts) |
| return svn_error_createf(SVN_ERR_CLIENT_PATCH_BAD_STRIP_COUNT, NULL, |
| Q_("Cannot strip %u component from '%s'", |
| "Cannot strip %u components from '%s'", |
| strip_count), |
| strip_count, |
| svn_dirent_local_style(path, scratch_pool)); |
| |
| stripped = apr_array_make(scratch_pool, components->nelts - strip_count, |
| sizeof(const char *)); |
| for (i = strip_count; i < components->nelts; i++) |
| { |
| const char *component; |
| |
| component = APR_ARRAY_IDX(components, i, const char *); |
| APR_ARRAY_PUSH(stripped, const char *) = component; |
| } |
| |
| *result = svn_path_compose(stripped, result_pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Obtain KEYWORDS, EOL_STYLE and EOL_STR for LOCAL_ABSPATH. |
| * WC_CTX is a context for the working copy the patch is applied to. |
| * Use RESULT_POOL for allocations of fields in TARGET. |
| * Use SCRATCH_POOL for all other allocations. */ |
| static svn_error_t * |
| obtain_eol_and_keywords_for_file(apr_hash_t **keywords, |
| svn_subst_eol_style_t *eol_style, |
| const char **eol_str, |
| svn_wc_context_t *wc_ctx, |
| const char *local_abspath, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_hash_t *props; |
| svn_string_t *keywords_val, *eol_style_val; |
| |
| SVN_ERR(svn_wc_prop_list2(&props, wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| keywords_val = svn_hash_gets(props, SVN_PROP_KEYWORDS); |
| if (keywords_val) |
| { |
| svn_revnum_t changed_rev; |
| apr_time_t changed_date; |
| const char *rev_str; |
| const char *author; |
| const char *url; |
| const char *repos_root_url; |
| const char *repos_relpath; |
| |
| SVN_ERR(svn_wc__node_get_changed_info(&changed_rev, |
| &changed_date, |
| &author, wc_ctx, |
| local_abspath, |
| scratch_pool, |
| scratch_pool)); |
| rev_str = apr_psprintf(scratch_pool, "%ld", changed_rev); |
| SVN_ERR(svn_wc__node_get_repos_info(NULL, &repos_relpath, &repos_root_url, |
| NULL, |
| wc_ctx, local_abspath, |
| scratch_pool, scratch_pool)); |
| url = svn_path_url_add_component2(repos_root_url, repos_relpath, |
| scratch_pool); |
| |
| SVN_ERR(svn_subst_build_keywords3(keywords, |
| keywords_val->data, |
| rev_str, url, repos_root_url, |
| changed_date, |
| author, result_pool)); |
| } |
| |
| eol_style_val = svn_hash_gets(props, SVN_PROP_EOL_STYLE); |
| if (eol_style_val) |
| { |
| svn_subst_eol_style_from_value(eol_style, |
| eol_str, |
| eol_style_val->data); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Resolve the exact path for a patch TARGET at path PATH_FROM_PATCHFILE, |
| * which is the path of the target as it appeared in the patch file. |
| * Put a canonicalized version of PATH_FROM_PATCHFILE into |
| * TARGET->CANON_PATH_FROM_PATCHFILE. |
| * WC_CTX is a context for the working copy the patch is applied to. |
| * If possible, determine TARGET->WC_PATH, TARGET->ABS_PATH, TARGET->KIND, |
| * TARGET->ADDED, and TARGET->PARENT_DIR_EXISTS. |
| * 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. |
| * 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 *root_abspath, |
| int strip_count, |
| 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) |
| { |
| const char *stripped_path; |
| svn_wc_status3_t *status; |
| svn_error_t *err; |
| svn_boolean_t under_root; |
| |
| target->canon_path_from_patchfile = svn_dirent_internal_style( |
| path_from_patchfile, result_pool); |
| |
| /* 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; |
| target->local_abspath = NULL; |
| target->local_relpath = ""; |
| return SVN_NO_ERROR; |
| } |
| |
| if (strip_count > 0) |
| SVN_ERR(strip_path(&stripped_path, target->canon_path_from_patchfile, |
| strip_count, result_pool, scratch_pool)); |
| else |
| stripped_path = target->canon_path_from_patchfile; |
| |
| if (svn_dirent_is_absolute(stripped_path)) |
| { |
| 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 patch root itself. Skip it. */ |
| target->skipped = TRUE; |
| target->local_abspath = NULL; |
| target->local_relpath = stripped_path; |
| return SVN_NO_ERROR; |
| } |
| } |
| else |
| { |
| target->local_relpath = stripped_path; |
| } |
| |
| /* Make sure the path is secure to use. We want the target to be inside |
| * the locked tree and not be fooled by symlinks it might contain. */ |
| SVN_ERR(svn_dirent_is_under_root(&under_root, |
| &target->local_abspath, root_abspath, |
| target->local_relpath, result_pool)); |
| |
| if (! under_root) |
| { |
| /* The target path is outside of the working copy. Skip it. */ |
| target->skipped = TRUE; |
| target->local_abspath = NULL; |
| 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); |
| if (err) |
| { |
| if (err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND) |
| return svn_error_trace(err); |
| |
| svn_error_clear(err); |
| |
| target->locally_deleted = TRUE; |
| target->db_kind = svn_node_none; |
| status = NULL; |
| } |
| else if (status->node_status == svn_wc_status_ignored || |
| status->node_status == svn_wc_status_unversioned || |
| status->node_status == svn_wc_status_missing || |
| status->node_status == svn_wc_status_obstructed || |
| status->conflicted) |
| { |
| target->skipped = TRUE; |
| target->obstructed = TRUE; |
| return SVN_NO_ERROR; |
| } |
| else if (status->node_status == svn_wc_status_deleted) |
| { |
| target->locally_deleted = TRUE; |
| } |
| |
| if (status && (status->kind != svn_node_unknown)) |
| target->db_kind = status->kind; |
| else |
| target->db_kind = svn_node_none; |
| |
| SVN_ERR(svn_io_check_special_path(target->local_abspath, |
| &target->kind_on_disk, &target->is_symlink, |
| scratch_pool)); |
| |
| if (target->locally_deleted) |
| { |
| 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)); |
| } |
| |
| if (moved_to_abspath) |
| { |
| target->local_abspath = moved_to_abspath; |
| 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; |
| |
| SVN_ERR(svn_io_check_special_path(target->local_abspath, |
| &target->kind_on_disk, |
| &target->is_symlink, |
| scratch_pool)); |
| } |
| else if (target->kind_on_disk != svn_node_none) |
| { |
| target->skipped = TRUE; |
| return SVN_NO_ERROR; |
| } |
| } |
| |
| #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; |
| } |
| |
| /* Baton for reading from properties. */ |
| typedef struct prop_read_baton_t { |
| const svn_string_t *value; |
| apr_off_t offset; |
| } prop_read_baton_t; |
| |
| /* Allocate *STRINGBUF in RESULT_POOL, and read into it one line from |
| * the unpatched property value accessed via BATON. |
| * Reading stops either after a line-terminator was found, or if |
| * the property value runs out in which case *EOF is set to TRUE. |
| * The line-terminator is not stored in *STRINGBUF. |
| * |
| * If the line is empty or could not be read, *line is set to NULL. |
| * |
| * The line-terminator is detected automatically and stored in *EOL |
| * if EOL is not NULL. If the end of the property value is reached |
| * and does not end with a newline character, and EOL is not NULL, |
| * *EOL is set to NULL. |
| * |
| * SCRATCH_POOL is used for temporary allocations. |
| */ |
| static svn_error_t * |
| 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 = baton; |
| svn_stringbuf_t *str = NULL; |
| const char *c; |
| svn_boolean_t found_eof; |
| |
| if ((apr_uint64_t)b->offset >= (apr_uint64_t)b->value->len) |
| { |
| *eol_str = NULL; |
| *eof = TRUE; |
| *line = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Read bytes into STR up to and including, but not storing, |
| * the next EOL sequence. */ |
| *eol_str = NULL; |
| found_eof = FALSE; |
| do |
| { |
| c = b->value->data + b->offset; |
| b->offset++; |
| |
| if (*c == '\0') |
| { |
| found_eof = TRUE; |
| break; |
| } |
| else if (*c == '\n') |
| { |
| *eol_str = "\n"; |
| } |
| else if (*c == '\r') |
| { |
| *eol_str = "\r"; |
| if (*(c + 1) == '\n') |
| { |
| *eol_str = "\r\n"; |
| b->offset++; |
| } |
| } |
| else |
| { |
| if (str == NULL) |
| str = svn_stringbuf_create_ensure(80, result_pool); |
| svn_stringbuf_appendbyte(str, *c); |
| } |
| |
| if (*eol_str) |
| break; |
| } |
| while (c < b->value->data + b->value->len); |
| |
| if (eof) |
| *eof = found_eof && !(str && str->len > 0); |
| *line = str; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return in *OFFSET the current byte offset for reading from the |
| * unpatched property value accessed via BATON. |
| * Use SCRATCH_POOL for temporary allocations. */ |
| static svn_error_t * |
| tell_prop(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool) |
| { |
| prop_read_baton_t *b = baton; |
| |
| *offset = b->offset; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Seek to the specified by OFFSET in the unpatched property value accessed |
| * via BATON. Use SCRATCH_POOL for temporary allocations. */ |
| static svn_error_t * |
| seek_prop(void *baton, apr_off_t offset, apr_pool_t *scratch_pool) |
| { |
| prop_read_baton_t *b = baton; |
| |
| b->offset = offset; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write LEN bytes from BUF into the patched property value accessed |
| * via BATON. Use SCRATCH_POOL for temporary allocations. */ |
| 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 = baton; |
| |
| svn_stringbuf_appendbytes(patched_value, buf, len); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Initialize a PROP_TARGET structure for PROP_NAME on the patch target |
| * at LOCAL_ABSPATH. OPERATION indicates the operation performed on the |
| * property. Use working copy context WC_CTX. |
| * Allocate results in RESULT_POOL. |
| * 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, |
| const char *local_abspath, |
| apr_pool_t *result_pool, apr_pool_t *scratch_pool) |
| { |
| prop_patch_target_t *new_prop_target; |
| target_content_t *content; |
| const svn_string_t *value; |
| prop_read_baton_t *prop_read_baton; |
| |
| content = apr_pcalloc(result_pool, sizeof(*content)); |
| |
| /* All other fields are FALSE or NULL due to apr_pcalloc(). */ |
| content->current_line = 1; |
| content->eol_style = svn_subst_eol_style_none; |
| content->lines = apr_array_make(result_pool, 0, sizeof(apr_off_t)); |
| content->hunks = apr_array_make(result_pool, 0, sizeof(hunk_info_t *)); |
| content->keywords = apr_hash_make(result_pool); |
| |
| new_prop_target = apr_pcalloc(result_pool, sizeof(*new_prop_target)); |
| new_prop_target->name = apr_pstrdup(result_pool, prop_name); |
| new_prop_target->operation = operation; |
| new_prop_target->content = content; |
| |
| 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); |
| |
| |
| /* Wire up the read and write callbacks. */ |
| prop_read_baton = apr_pcalloc(result_pool, sizeof(*prop_read_baton)); |
| prop_read_baton->value = value; |
| prop_read_baton->offset = 0; |
| content->readline = readline_prop; |
| content->tell = tell_prop; |
| content->seek = seek_prop; |
| content->read_baton = prop_read_baton; |
| content->write = write_prop; |
| content->write_baton = new_prop_target->patched_value; |
| |
| *prop_target = new_prop_target; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Allocate *STRINGBUF in RESULT_POOL, and read into it one line from |
| * the unpatched file content accessed via BATON. |
| * Reading stops either after a line-terminator was found, |
| * or if EOF is reached in which case *EOF is set to TRUE. |
| * The line-terminator is not stored in *STRINGBUF. |
| * |
| * If the line is empty or could not be read, *line is set to NULL. |
| * |
| * The line-terminator is detected automatically and stored in *EOL |
| * if EOL is not NULL. If EOF is reached and FILE does not end |
| * with a newline character, and EOL is not NULL, *EOL is set to NULL. |
| * |
| * SCRATCH_POOL is used for temporary allocations. |
| */ |
| static svn_error_t * |
| 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 = baton; |
| |
| SVN_ERR(svn_io_file_readline(file, line, eol_str, eof, APR_SIZE_MAX, |
| result_pool, scratch_pool)); |
| |
| if (!(*line)->len) |
| *line = NULL; |
| else |
| *eof = FALSE; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Return in *OFFSET the current byte offset for reading from the |
| * unpatched file content accessed via BATON. |
| * Use SCRATCH_POOL for temporary allocations. */ |
| static svn_error_t * |
| tell_file(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool) |
| { |
| apr_file_t *file = baton; |
| |
| SVN_ERR(svn_io_file_get_offset(offset, file, scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Seek to the specified by OFFSET in the unpatched file content accessed |
| * via BATON. Use SCRATCH_POOL for temporary allocations. */ |
| static svn_error_t * |
| seek_file(void *baton, apr_off_t offset, apr_pool_t *scratch_pool) |
| { |
| apr_file_t *file = baton; |
| |
| SVN_ERR(svn_io_file_seek(file, APR_SET, &offset, scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write LEN bytes from BUF into the patched file content accessed |
| * via BATON. Use SCRATCH_POOL for temporary allocations. */ |
| static svn_error_t * |
| write_file(void *baton, const char *buf, apr_size_t len, |
| apr_pool_t *scratch_pool) |
| { |
| apr_file_t *file = baton; |
| |
| SVN_ERR(svn_io_file_write_full(file, buf, len, &len, scratch_pool)); |
| return SVN_NO_ERROR; |
| } |
| |
| /* 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. */ |
| struct symlink_baton_t |
| { |
| /* The path to the symlink on disk (not the path to the target of the link) */ |
| const char *local_abspath; |
| |
| /* Indicates whether the "normal form" of the symlink has been read. */ |
| svn_boolean_t at_eof; |
| }; |
| |
| /* Allocate *STRINGBUF in RESULT_POOL, and store into it the "normal form" |
| * of the symlink accessed via BATON. |
| * |
| * Otherwise behaves like readline_file(), which see. |
| */ |
| static svn_error_t * |
| readline_symlink(void *baton, svn_stringbuf_t **line, const char **eol_str, |
| svn_boolean_t *eof, apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| struct symlink_baton_t *sb = baton; |
| |
| if (eof) |
| *eof = TRUE; |
| if (eol_str) |
| *eol_str = NULL; |
| |
| if (sb->at_eof) |
| { |
| *line = NULL; |
| } |
| else |
| { |
| svn_stream_t *stream; |
| const apr_size_t len_hint = 64; /* arbitrary */ |
| |
| 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 * |
| tell_symlink(void *baton, apr_off_t *offset, apr_pool_t *scratch_pool) |
| { |
| struct symlink_baton_t *sb = baton; |
| |
| *offset = sb->at_eof ? 1 : 0; |
| return SVN_NO_ERROR; |
| } |
| |
| /* If offset is non-zero, mark the symlink as having been read in its |
| * "normal form". Else, mark the symlink as not having been read yet. */ |
| static svn_error_t * |
| seek_symlink(void *baton, apr_off_t offset, apr_pool_t *scratch_pool) |
| { |
| struct symlink_baton_t *sb = baton; |
| |
| sb->at_eof = (offset != 0); |
| 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 |
| * total file name length (in that order). In case of a tie, return the new |
| * filename. This heuristic is also used by Larry Wall's UNIX patch (except |
| * that it prompts for a filename in case of a tie). |
| * Additionally, for compatibility with git, if one of the filenames |
| * is "/dev/null", use the other filename. */ |
| static const char * |
| choose_target_filename(const svn_patch_t *patch) |
| { |
| apr_size_t old; |
| apr_size_t new; |
| |
| if (strcmp(patch->old_filename, "/dev/null") == 0) |
| return patch->new_filename; |
| if (strcmp(patch->new_filename, "/dev/null") == 0) |
| return patch->old_filename; |
| |
| /* If the patch renames the target, use the old name while |
| * applying hunks. The target will be renamed to the new name |
| * after hunks have been applied. */ |
| if (patch->operation == svn_diff_op_moved) |
| return patch->old_filename; |
| |
| old = svn_path_component_count(patch->old_filename); |
| new = svn_path_component_count(patch->new_filename); |
| |
| if (old == new) |
| { |
| old = strlen(svn_dirent_basename(patch->old_filename, NULL)); |
| new = strlen(svn_dirent_basename(patch->new_filename, NULL)); |
| |
| if (old == new) |
| { |
| old = strlen(patch->old_filename); |
| new = strlen(patch->new_filename); |
| } |
| } |
| |
| return (old < new) ? patch->old_filename : patch->new_filename; |
| } |
| |
| /* Attempt to initialize a *PATCH_TARGET structure for a target file |
| * described by PATCH. Use working copy context WC_CTX. |
| * STRIP_COUNT specifies the number of leading path components |
| * which should be stripped from target paths in the patch. |
| * The patch target structure is allocated in RESULT_POOL, but if the target |
| * should be skipped, PATCH_TARGET->SKIPPED is set and the target should be |
| * treated as not fully initialized, e.g. the caller should not not do any |
| * further operations on the target if it is marked to be skipped. |
| * If REMOVE_TEMPFILES is TRUE, set up temporary files to be removed as |
| * soon as they are no longer needed. |
| * Use SCRATCH_POOL for all other allocations. */ |
| static svn_error_t * |
| init_patch_target(patch_target_t **patch_target, |
| const svn_patch_t *patch, |
| 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_text_changes = FALSE; |
| svn_boolean_t follow_moves; |
| const char *tempdir_abspath; |
| |
| has_text_changes = ((patch->hunks && patch->hunks->nelts > 0) |
| || patch->binary_patch); |
| |
| content = apr_pcalloc(result_pool, sizeof(*content)); |
| |
| /* All other fields in content are FALSE or NULL due to apr_pcalloc().*/ |
| content->current_line = 1; |
| content->eol_style = svn_subst_eol_style_none; |
| content->lines = apr_array_make(result_pool, 0, sizeof(apr_off_t)); |
| content->hunks = apr_array_make(result_pool, 0, sizeof(hunk_info_t *)); |
| content->keywords = apr_hash_make(result_pool); |
| |
| target = apr_pcalloc(result_pool, sizeof(*target)); |
| |
| /* All other fields in target are FALSE or NULL due to apr_pcalloc(). */ |
| target->db_kind = svn_node_none; |
| 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), |
| root_abspath, strip_count, has_text_changes, |
| follow_moves, wc_ctx, targets_info, |
| result_pool, scratch_pool)); |
| *patch_target = target; |
| if (! target->skipped) |
| { |
| if (patch->old_symlink_bit == svn_tristate_true |
| || patch->new_symlink_bit == svn_tristate_true) |
| { |
| target->git_symlink_format = TRUE; |
| } |
| |
| /* ### Is it ok to set the operation of the target already here? Isn't |
| * ### the target supposed to be marked with an operation after we have |
| * ### determined that the changes will apply cleanly to the WC? Maybe |
| * ### we should have kept the patch field in patch_target_t to be |
| * ### able to distinguish between 'what the patch says we should do' |
| * ### and 'what we can do with the given state of our WC'. */ |
| if (patch->operation == svn_diff_op_added) |
| target->added = TRUE; |
| else if (patch->operation == svn_diff_op_deleted) |
| target->deleted = TRUE; |
| else if (patch->operation == svn_diff_op_moved) |
| { |
| 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; |
| |
| move_target_path = svn_dirent_internal_style(patch->new_filename, |
| scratch_pool); |
| |
| if (strip_count > 0) |
| SVN_ERR(strip_path(&move_target_path, move_target_path, |
| strip_count, scratch_pool, scratch_pool)); |
| |
| if (svn_dirent_is_absolute(move_target_path)) |
| { |
| move_target_relpath = svn_dirent_is_child(root_abspath, |
| move_target_path, |
| scratch_pool); |
| if (! move_target_relpath) |
| { |
| /* The move target path is either outside of the working |
| * copy or it is the working copy itself. Skip it. */ |
| target->skipped = TRUE; |
| return SVN_NO_ERROR; |
| } |
| } |
| else |
| move_target_relpath = move_target_path; |
| |
| /* Make sure the move target path is secure to use. */ |
| SVN_ERR(svn_dirent_is_under_root(&under_root, |
| &target->move_target_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->move_target_abspath = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| 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 (wc_kind == svn_node_file || wc_kind == svn_node_dir) |
| { |
| /* 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; |
| } |
| } |
| |
| /* 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 = target->git_symlink_format ? readline_symlink_git |
| : readline_symlink; |
| content->seek = seek_symlink; |
| content->tell = tell_symlink; |
| } |
| else if (target->kind_on_disk == svn_node_file) |
| { |
| 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; |
| |
| /* 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 the patched result to. */ |
| SVN_ERR(svn_wc__get_tmpdir(&tempdir_abspath, wc_ctx, |
| target->local_abspath, scratch_pool, scratch_pool)); |
| SVN_ERR(svn_io_open_unique_file3(&target->patched_file, |
| &target->patched_path, tempdir_abspath, |
| remove_tempfiles ? |
| svn_io_file_del_on_pool_cleanup : |
| svn_io_file_del_none, |
| result_pool, 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, tempdir_abspath, |
| remove_tempfiles ? |
| svn_io_file_del_on_pool_cleanup : |
| svn_io_file_del_none, |
| result_pool, scratch_pool)); |
| |
| /* Handle properties. */ |
| if (! target->skipped) |
| { |
| apr_hash_index_t *hi; |
| |
| for (hi = apr_hash_first(result_pool, patch->prop_patches); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| const char *prop_name = apr_hash_this_key(hi); |
| svn_prop_patch_t *prop_patch = apr_hash_this_val(hi); |
| prop_patch_target_t *prop_target; |
| |
| SVN_ERR(init_prop_target(&prop_target, |
| 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; |
| } |
| |
| /* Read a *LINE from CONTENT. If the line has not been read before |
| * mark the line in CONTENT->LINES. |
| * If a line could be read successfully, increase CONTENT->CURRENT_LINE, |
| * and allocate *LINE in RESULT_POOL. |
| * Do temporary allocations in SCRATCH_POOL. |
| */ |
| static svn_error_t * |
| readline(target_content_t *content, |
| const char **line, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_stringbuf_t *line_raw; |
| const char *eol_str; |
| svn_linenum_t max_line = (svn_linenum_t)content->lines->nelts + 1; |
| |
| if (content->eof || content->readline == NULL) |
| { |
| *line = ""; |
| return SVN_NO_ERROR; |
| } |
| |
| SVN_ERR_ASSERT(content->current_line <= max_line); |
| if (content->current_line == max_line) |
| { |
| apr_off_t offset; |
| |
| SVN_ERR(content->tell(content->read_baton, &offset, |
| scratch_pool)); |
| APR_ARRAY_PUSH(content->lines, apr_off_t) = offset; |
| } |
| |
| SVN_ERR(content->readline(content->read_baton, &line_raw, |
| &eol_str, &content->eof, |
| result_pool, scratch_pool)); |
| if (content->eol_style == svn_subst_eol_style_none) |
| content->eol_str = eol_str; |
| |
| if (line_raw) |
| { |
| /* Contract keywords. */ |
| SVN_ERR(svn_subst_translate_cstring2(line_raw->data, line, |
| NULL, FALSE, |
| content->keywords, FALSE, |
| result_pool)); |
| } |
| else |
| *line = ""; |
| |
| if ((line_raw && line_raw->len > 0) || eol_str) |
| content->current_line++; |
| |
| SVN_ERR_ASSERT(content->current_line > 0); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Seek to the specified LINE in CONTENT. |
| * Mark any lines not read before in CONTENT->LINES. |
| * Do temporary allocations in SCRATCH_POOL. |
| */ |
| static svn_error_t * |
| seek_to_line(target_content_t *content, svn_linenum_t line, |
| apr_pool_t *scratch_pool) |
| { |
| svn_linenum_t saved_line; |
| svn_boolean_t saved_eof; |
| |
| SVN_ERR_ASSERT(line > 0); |
| |
| if (line == content->current_line) |
| return SVN_NO_ERROR; |
| |
| saved_line = content->current_line; |
| saved_eof = content->eof; |
| |
| if (line <= (svn_linenum_t)content->lines->nelts) |
| { |
| apr_off_t offset; |
| |
| offset = APR_ARRAY_IDX(content->lines, line - 1, apr_off_t); |
| SVN_ERR(content->seek(content->read_baton, offset, |
| scratch_pool)); |
| content->current_line = line; |
| } |
| else |
| { |
| const char *dummy; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| |
| while (! content->eof && content->current_line < line) |
| { |
| svn_pool_clear(iterpool); |
| SVN_ERR(readline(content, &dummy, iterpool, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| } |
| |
| /* After seeking backwards from EOF position clear EOF indicator. */ |
| if (saved_eof && saved_line > content->current_line) |
| content->eof = FALSE; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Indicate in *MATCHED whether the original text of HUNK matches the patch |
| * CONTENT at its current line. Lines within FUZZ lines of the start or |
| * end of HUNK will always match. If IGNORE_WHITESPACE is set, we ignore |
| * whitespace when doing the matching. When this function returns, neither |
| * CONTENT->CURRENT_LINE nor the file offset in the target file will |
| * have changed. If MATCH_MODIFIED is TRUE, match the modified hunk text, |
| * rather than the original hunk text. |
| * Do temporary allocations in POOL. */ |
| static svn_error_t * |
| match_hunk(svn_boolean_t *matched, target_content_t *content, |
| svn_diff_hunk_t *hunk, svn_linenum_t fuzz, |
| svn_boolean_t ignore_whitespace, |
| svn_boolean_t match_modified, apr_pool_t *pool) |
| { |
| svn_stringbuf_t *hunk_line; |
| const char *target_line; |
| svn_linenum_t lines_read; |
| svn_linenum_t saved_line; |
| svn_boolean_t hunk_eof; |
| svn_boolean_t lines_matched; |
| apr_pool_t *iterpool; |
| 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; |
| leading_context = svn_diff_hunk_get_leading_context(hunk); |
| trailing_context = svn_diff_hunk_get_trailing_context(hunk); |
| if (match_modified) |
| { |
| svn_diff_hunk_reset_modified_text(hunk); |
| hunk_length = svn_diff_hunk_get_modified_length(hunk); |
| } |
| else |
| { |
| svn_diff_hunk_reset_original_text(hunk); |
| hunk_length = svn_diff_hunk_get_original_length(hunk); |
| } |
| iterpool = svn_pool_create(pool); |
| do |
| { |
| const char *hunk_line_translated; |
| |
| svn_pool_clear(iterpool); |
| |
| if (match_modified) |
| SVN_ERR(svn_diff_hunk_readline_modified_text(hunk, &hunk_line, |
| NULL, &hunk_eof, |
| iterpool, iterpool)); |
| else |
| SVN_ERR(svn_diff_hunk_readline_original_text(hunk, &hunk_line, |
| NULL, &hunk_eof, |
| iterpool, iterpool)); |
| |
| /* Contract keywords, if any, before matching. */ |
| SVN_ERR(svn_subst_translate_cstring2(hunk_line->data, |
| &hunk_line_translated, |
| NULL, FALSE, |
| content->keywords, FALSE, |
| iterpool)); |
| SVN_ERR(readline(content, &target_line, iterpool, iterpool)); |
| |
| lines_read++; |
| |
| /* If the last line doesn't have a newline, we get EOF but still |
| * have a non-empty line to compare. */ |
| if ((hunk_eof && hunk_line->len == 0) || |
| (content->eof && *target_line == 0)) |
| break; |
| |
| /* Leading/trailing fuzzy lines always match. */ |
| if ((lines_read <= fuzz && leading_context > fuzz) || |
| (lines_read > hunk_length - fuzz && trailing_context > fuzz)) |
| lines_matched = TRUE; |
| else |
| { |
| if (ignore_whitespace) |
| { |
| char *hunk_line_trimmed; |
| char *target_line_trimmed; |
| |
| hunk_line_trimmed = apr_pstrdup(iterpool, hunk_line_translated); |
| target_line_trimmed = apr_pstrdup(iterpool, target_line); |
| apr_collapse_spaces(hunk_line_trimmed, hunk_line_trimmed); |
| apr_collapse_spaces(target_line_trimmed, target_line_trimmed); |
| lines_matched = ! strcmp(hunk_line_trimmed, target_line_trimmed); |
| } |
| else |
| lines_matched = ! strcmp(hunk_line_translated, target_line); |
| } |
| } |
| while (lines_matched); |
| |
| *matched = lines_matched && hunk_eof && hunk_line->len == 0; |
| SVN_ERR(seek_to_line(content, saved_line, iterpool)); |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Scan lines of CONTENT for a match of the original text of HUNK, |
| * up to but not including the specified UPPER_LINE. Use fuzz factor FUZZ. |
| * If UPPER_LINE is zero scan until EOF occurs when reading from TARGET. |
| * Return the line at which HUNK was matched in *MATCHED_LINE. |
| * If the hunk did not match at all, set *MATCHED_LINE to zero. |
| * If the hunk matched multiple times, and MATCH_FIRST is TRUE, |
| * return the line number at which the first match occurred in *MATCHED_LINE. |
| * If the hunk matched multiple times, and MATCH_FIRST is FALSE, |
| * return the line number at which the last match occurred in *MATCHED_LINE. |
| * If IGNORE_WHITESPACE is set, ignore whitespace during the matching. |
| * If MATCH_MODIFIED is TRUE, match the modified hunk text, |
| * rather than the original hunk text. |
| * Call cancel CANCEL_FUNC with baton CANCEL_BATON to trigger cancellation. |
| * Do all allocations in POOL. */ |
| static svn_error_t * |
| scan_for_match(svn_linenum_t *matched_line, |
| target_content_t *content, |
| svn_diff_hunk_t *hunk, svn_boolean_t match_first, |
| svn_linenum_t upper_line, svn_linenum_t fuzz, |
| svn_boolean_t ignore_whitespace, |
| svn_boolean_t match_modified, |
| svn_cancel_func_t cancel_func, void *cancel_baton, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *iterpool; |
| |
| *matched_line = 0; |
| iterpool = svn_pool_create(pool); |
| while ((content->current_line < upper_line || upper_line == 0) && |
| ! content->eof) |
| { |
| svn_boolean_t matched; |
| |
| svn_pool_clear(iterpool); |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| SVN_ERR(match_hunk(&matched, content, hunk, fuzz, ignore_whitespace, |
| match_modified, iterpool)); |
| if (matched) |
| { |
| svn_boolean_t taken = FALSE; |
| int i; |
| |
| /* Don't allow hunks to match at overlapping locations. */ |
| for (i = 0; i < content->hunks->nelts; i++) |
| { |
| const hunk_info_t *hi; |
| svn_linenum_t length; |
| |
| hi = APR_ARRAY_IDX(content->hunks, i, const hunk_info_t *); |
| |
| if (match_modified) |
| length = svn_diff_hunk_get_modified_length(hi->hunk); |
| else |
| length = svn_diff_hunk_get_original_length(hi->hunk); |
| |
| taken = (! hi->rejected && |
| content->current_line >= hi->matched_line && |
| content->current_line < (hi->matched_line + length)); |
| if (taken) |
| break; |
| } |
| |
| if (! taken) |
| { |
| *matched_line = content->current_line; |
| if (match_first) |
| break; |
| } |
| } |
| |
| if (! content->eof) |
| SVN_ERR(seek_to_line(content, content->current_line + 1, |
| iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Indicate in *MATCH whether the content described by CONTENT |
| * matches the modified text of HUNK. |
| * Use SCRATCH_POOL for temporary allocations. */ |
| static svn_error_t * |
| match_existing_target(svn_boolean_t *match, |
| target_content_t *content, |
| svn_diff_hunk_t *hunk, |
| apr_pool_t *scratch_pool) |
| { |
| svn_boolean_t lines_matched; |
| apr_pool_t *iterpool; |
| svn_boolean_t hunk_eof; |
| svn_linenum_t saved_line; |
| |
| svn_diff_hunk_reset_modified_text(hunk); |
| |
| saved_line = content->current_line; |
| |
| iterpool = svn_pool_create(scratch_pool); |
| do |
| { |
| const char *line; |
| svn_stringbuf_t *hunk_line; |
| const char *line_translated; |
| const char *hunk_line_translated; |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR(readline(content, &line, iterpool, iterpool)); |
| SVN_ERR(svn_diff_hunk_readline_modified_text(hunk, &hunk_line, |
| NULL, &hunk_eof, |
| iterpool, iterpool)); |
| /* Contract keywords. */ |
| SVN_ERR(svn_subst_translate_cstring2(line, &line_translated, |
| NULL, FALSE, |
| content->keywords, |
| FALSE, iterpool)); |
| SVN_ERR(svn_subst_translate_cstring2(hunk_line->data, |
| &hunk_line_translated, |
| NULL, FALSE, |
| content->keywords, |
| FALSE, iterpool)); |
| lines_matched = ! strcmp(line_translated, hunk_line_translated); |
| if (content->eof != hunk_eof) |
| { |
| svn_pool_destroy(iterpool); |
| *match = FALSE; |
| return SVN_NO_ERROR; |
| } |
| } |
| 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)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Determine the line at which a HUNK applies to CONTENT of the TARGET |
| * file, and return an appropriate hunk_info object in *HI, allocated from |
| * RESULT_POOL. Use fuzz factor FUZZ. Set HI->FUZZ to FUZZ. If no correct |
| * line can be determined, set HI->REJECTED to TRUE. PREVIOUS_OFFSET |
| * is the offset at which the previous matching hunk was applied, or zero. |
| * IGNORE_WHITESPACE tells whether whitespace should be considered when |
| * matching. IS_PROP_HUNK indicates whether the hunk patches file content |
| * or a property. |
| * When this function returns, neither CONTENT->CURRENT_LINE nor |
| * the file offset in the target file will have changed. |
| * Call cancel CANCEL_FUNC with baton CANCEL_BATON to trigger cancellation. |
| * Do temporary allocations in POOL. */ |
| static svn_error_t * |
| get_hunk_info(hunk_info_t **hi, patch_target_t *target, |
| target_content_t *content, |
| svn_diff_hunk_t *hunk, svn_linenum_t fuzz, |
| svn_linenum_t previous_offset, |
| svn_boolean_t ignore_whitespace, |
| svn_boolean_t is_prop_hunk, |
| svn_cancel_func_t cancel_func, void *cancel_baton, |
| apr_pool_t *result_pool, apr_pool_t *scratch_pool) |
| { |
| svn_linenum_t matched_line; |
| svn_linenum_t original_start; |
| svn_boolean_t already_applied; |
| |
| original_start = svn_diff_hunk_get_original_start(hunk) + previous_offset; |
| already_applied = FALSE; |
| |
| /* An original offset of zero means that this hunk wants to create |
| * a new file. Don't bother matching hunks in that case, since |
| * the hunk applies at line 1. If the file already exists, the hunk |
| * is rejected, unless the file is versioned and its content matches |
| * the file the patch wants to create. */ |
| if (original_start == 0 && fuzz > 0) |
| { |
| matched_line = 0; /* reject any fuzz for new files */ |
| } |
| else if (original_start == 0 && ! is_prop_hunk) |
| { |
| if (target->kind_on_disk == svn_node_file) |
| { |
| const svn_io_dirent2_t *dirent; |
| SVN_ERR(svn_io_stat_dirent2(&dirent, target->local_abspath, FALSE, |
| TRUE, scratch_pool, scratch_pool)); |
| |
| if (dirent->kind == svn_node_file |
| && !dirent->special |
| && dirent->filesize == 0) |
| { |
| matched_line = 1; /* Matched an on-disk empty file */ |
| } |
| else |
| { |
| if (target->db_kind == svn_node_file) |
| { |
| svn_boolean_t file_matches; |
| |
| SVN_ERR(match_existing_target(&file_matches, content, hunk, |
| scratch_pool)); |
| if (file_matches) |
| { |
| matched_line = 1; |
| already_applied = TRUE; |
| } |
| else |
| matched_line = 0; /* reject */ |
| } |
| else |
| matched_line = 0; /* reject */ |
| } |
| } |
| else |
| matched_line = 1; |
| } |
| /* Same conditions apply as for the file case above. |
| * |
| * ### Since the hunk says the prop should be added we just assume so for |
| * ### now and don't bother with storing the previous lines and such. When |
| * ### we have the diff operation available we can just check for adds. */ |
| else if (original_start == 0 && is_prop_hunk) |
| { |
| if (content->existed) |
| { |
| svn_boolean_t prop_matches; |
| |
| SVN_ERR(match_existing_target(&prop_matches, content, hunk, |
| scratch_pool)); |
| |
| if (prop_matches) |
| { |
| matched_line = 1; |
| already_applied = TRUE; |
| } |
| else |
| matched_line = 0; /* reject */ |
| } |
| else |
| matched_line = 1; |
| } |
| 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)); |
| if (content->current_line != original_start) |
| { |
| /* Seek failed. */ |
| matched_line = 0; |
| } |
| else |
| SVN_ERR(scan_for_match(&matched_line, content, hunk, TRUE, |
| original_start + 1, fuzz, |
| ignore_whitespace, FALSE, |
| cancel_func, cancel_baton, |
| scratch_pool)); |
| |
| if (matched_line != original_start) |
| { |
| /* Check if the hunk is already applied. |
| * We only check for an exact match here, and don't bother checking |
| * for already applied patches with offset/fuzz, because such a |
| * check would be ambiguous. */ |
| if (fuzz == 0) |
| { |
| if (modified_start == 0 |
| && (target->operation == svn_diff_op_unchanged |
| || target->operation == svn_diff_op_deleted)) |
| { |
| /* Patch wants to delete the file. */ |
| |
| already_applied = target->locally_deleted; |
| } |
| else |
| { |
| 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, |
| fuzz, ignore_whitespace, TRUE, |
| cancel_func, cancel_baton, |
| scratch_pool)); |
| already_applied = (matched_line == modified_start); |
| } |
| } |
| else |
| already_applied = FALSE; |
| |
| if (! already_applied) |
| { |
| int i; |
| svn_linenum_t search_start = 1, search_end = 0; |
| svn_linenum_t matched_line2; |
| |
| /* Search for closest match before or after original |
| start. We have no backward search so search forwards |
| from the previous match (or start of file) to the |
| original start looking for the last match. Then |
| search forwards from the original start looking for a |
| better match. Finally search forwards from the start |
| of file to the previous hunk if that could result in |
| a better match. */ |
| |
| for (i = content->hunks->nelts; i > 0; --i) |
| { |
| const hunk_info_t *prev |
| = APR_ARRAY_IDX(content->hunks, i - 1, const hunk_info_t *); |
| if (!prev->rejected) |
| { |
| svn_linenum_t length; |
| |
| length = svn_diff_hunk_get_original_length(prev->hunk); |
| search_start = prev->matched_line + length; |
| break; |
| } |
| } |
| |
| /* Search from the previous match, or start of file, |
| towards the original location. */ |
| SVN_ERR(seek_to_line(content, search_start, scratch_pool)); |
| SVN_ERR(scan_for_match(&matched_line, content, hunk, FALSE, |
| original_start, fuzz, |
| ignore_whitespace, FALSE, |
| cancel_func, cancel_baton, |
| scratch_pool)); |
| |
| /* If a match we only need to search forwards for a |
| better match, otherwise to the end of the file. */ |
| if (matched_line) |
| search_end = original_start + (original_start - matched_line); |
| |
| /* Search from original location, towards the end. */ |
| SVN_ERR(seek_to_line(content, original_start + 1, scratch_pool)); |
| SVN_ERR(scan_for_match(&matched_line2, content, hunk, |
| TRUE, search_end, fuzz, ignore_whitespace, |
| FALSE, cancel_func, cancel_baton, |
| scratch_pool)); |
| |
| /* Chose the forward match if it is closer than the |
| backward match or if there is no backward match. */ |
| if (matched_line2 |
| && (!matched_line |
| || (matched_line2 - original_start |
| < original_start - matched_line))) |
| matched_line = matched_line2; |
| |
| /* Search from before previous hunk if there could be a |
| better match. */ |
| if (search_start > 1 |
| && (!matched_line |
| || (matched_line > original_start |
| && (matched_line - original_start |
| > original_start - search_start)))) |
| { |
| svn_linenum_t search_start2 = 1; |
| |
| if (matched_line |
| && matched_line - original_start < original_start) |
| search_start2 |
| = original_start - (matched_line - original_start) + 1; |
| |
| SVN_ERR(seek_to_line(content, search_start2, scratch_pool)); |
| SVN_ERR(scan_for_match(&matched_line2, content, hunk, FALSE, |
| search_start - 1, fuzz, |
| ignore_whitespace, FALSE, |
| cancel_func, cancel_baton, |
| scratch_pool)); |
| if (matched_line2) |
| matched_line = matched_line2; |
| } |
| } |
| } |
| 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 or property which doesn't exist. */ |
| matched_line = 0; |
| } |
| |
| (*hi) = apr_pcalloc(result_pool, sizeof(hunk_info_t)); |
| (*hi)->hunk = hunk; |
| (*hi)->matched_line = matched_line; |
| (*hi)->rejected = (matched_line == 0); |
| (*hi)->already_applied = already_applied; |
| (*hi)->report_fuzz = fuzz; |
| (*hi)->match_fuzz = fuzz - svn_diff_hunk__get_fuzz_penalty(hunk); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Copy lines to the patched content until the specified LINE has been |
| * reached. Indicate in *EOF whether end-of-file was encountered while |
| * reading from the target. |
| * If LINE is zero, copy lines until end-of-file has been reached. |
| * Do all allocations in POOL. */ |
| static svn_error_t * |
| copy_lines_to_target(target_content_t *content, svn_linenum_t line, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *iterpool; |
| |
| iterpool = svn_pool_create(pool); |
| while ((content->current_line < line || line == 0) && ! content->eof) |
| { |
| const char *target_line; |
| apr_size_t len; |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR(readline(content, &target_line, iterpool, iterpool)); |
| if (! content->eof) |
| target_line = apr_pstrcat(iterpool, target_line, content->eol_str, |
| SVN_VA_NULL); |
| len = strlen(target_line); |
| SVN_ERR(content->write(content->write_baton, target_line, |
| len, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write the diff text of HUNK to TARGET's reject file, |
| * and mark TARGET as having had rejects. |
| * We don't expand keywords, nor normalise line-endings, in reject files. |
| * Do temporary allocations in SCRATCH_POOL. */ |
| static svn_error_t * |
| reject_hunk(patch_target_t *target, target_content_t *content, |
| svn_diff_hunk_t *hunk, const char *prop_name, |
| apr_pool_t *pool) |
| { |
| svn_boolean_t eof; |
| static const char * const text_atat = "@@"; |
| static const char * const prop_atat = "##"; |
| const char *atat; |
| apr_pool_t *iterpool; |
| |
| if (prop_name) |
| { |
| /* ### Print 'Added', 'Deleted' or 'Modified' instead of 'Property'. */ |
| SVN_ERR(svn_stream_printf(target->reject_stream, |
| pool, "Property: %s" APR_EOL_STR, prop_name)); |
| atat = prop_atat; |
| } |
| else |
| { |
| atat = text_atat; |
| } |
| |
| 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 |
| { |
| svn_stringbuf_t *hunk_line; |
| const char *eol_str; |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR(svn_diff_hunk_readline_diff_text(hunk, &hunk_line, &eol_str, |
| &eof, iterpool, iterpool)); |
| if (! eof) |
| { |
| if (hunk_line->len >= 1) |
| { |
| apr_size_t len = hunk_line->len; |
| |
| SVN_ERR(svn_stream_write(target->reject_stream, |
| hunk_line->data, &len)); |
| } |
| |
| if (eol_str) |
| { |
| SVN_ERR(svn_stream_puts(target->reject_stream, eol_str)); |
| } |
| } |
| } |
| while (! eof); |
| svn_pool_destroy(iterpool); |
| |
| if (prop_name) |
| target->had_prop_rejects = TRUE; |
| else |
| target->had_rejects = TRUE; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write the modified text of the hunk described by HI to the patched |
| * CONTENT. TARGET is the patch target. |
| * If PROP_NAME is not NULL, the hunk is assumed to be targeted for |
| * a property with the given name. |
| * Do temporary allocations in POOL. */ |
| static svn_error_t * |
| apply_hunk(patch_target_t *target, target_content_t *content, |
| hunk_info_t *hi, const char *prop_name, apr_pool_t *pool) |
| { |
| 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? |
| */ |
| if (target->kind_on_disk == svn_node_file || prop_name) |
| { |
| svn_linenum_t line; |
| |
| /* Move forward to the hunk's line, copying data as we go. |
| * 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 + 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 * fuzz); |
| SVN_ERR(seek_to_line(content, line, pool)); |
| if (content->current_line != line && ! content->eof) |
| { |
| /* Seek failed, reject this hunk. */ |
| hi->rejected = TRUE; |
| SVN_ERR(reject_hunk(target, content, hi->hunk, prop_name, pool)); |
| return SVN_NO_ERROR; |
| } |
| } |
| |
| /* Write the hunk's version to the patched result. |
| * Don't write the lines which matched with fuzz. */ |
| lines_read = 0; |
| svn_diff_hunk_reset_modified_text(hi->hunk); |
| iterpool = svn_pool_create(pool); |
| do |
| { |
| svn_stringbuf_t *hunk_line; |
| const char *eol_str; |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR(svn_diff_hunk_readline_modified_text(hi->hunk, &hunk_line, |
| &eol_str, &eof, |
| iterpool, iterpool)); |
| lines_read++; |
| if (lines_read > fuzz && |
| lines_read <= svn_diff_hunk_get_modified_length(hi->hunk) - fuzz) |
| { |
| apr_size_t len; |
| |
| if (hunk_line->len >= 1) |
| { |
| len = hunk_line->len; |
| SVN_ERR(content->write(content->write_baton, |
| hunk_line->data, len, iterpool)); |
| } |
| |
| if (eol_str) |
| { |
| /* Use the EOL as it was read from the patch file, |
| * unless the target's EOL style is set by svn:eol-style */ |
| if (content->eol_style != svn_subst_eol_style_none) |
| eol_str = content->eol_str; |
| |
| len = strlen(eol_str); |
| SVN_ERR(content->write(content->write_baton, |
| eol_str, len, iterpool)); |
| } |
| } |
| } |
| while (! eof); |
| svn_pool_destroy(iterpool); |
| |
| if (prop_name) |
| target->has_prop_changes = TRUE; |
| else |
| target->has_text_changes = TRUE; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Use client context CTX to send a suitable notification for hunk HI, |
| * using TARGET to determine the path. If the hunk is a property hunk, |
| * PROP_NAME must be the name of the property, else NULL. |
| * Use POOL for temporary allocations. */ |
| static svn_error_t * |
| send_hunk_notification(const hunk_info_t *hi, |
| const patch_target_t *target, |
| const char *prop_name, |
| const svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| svn_wc_notify_t *notify; |
| svn_wc_notify_action_t action; |
| |
| if (hi->already_applied) |
| action = svn_wc_notify_patch_hunk_already_applied; |
| else if (hi->rejected) |
| action = svn_wc_notify_patch_rejected_hunk; |
| else |
| action = svn_wc_notify_patch_applied_hunk; |
| |
| notify = svn_wc_create_notify(target->local_abspath |
| ? target->local_abspath |
| : target->local_relpath, |
| action, pool); |
| notify->hunk_original_start = |
| svn_diff_hunk_get_original_start(hi->hunk); |
| notify->hunk_original_length = |
| svn_diff_hunk_get_original_length(hi->hunk); |
| notify->hunk_modified_start = |
| svn_diff_hunk_get_modified_start(hi->hunk); |
| notify->hunk_modified_length = |
| svn_diff_hunk_get_modified_length(hi->hunk); |
| notify->hunk_matched_line = hi->matched_line; |
| notify->hunk_fuzz = hi->report_fuzz; |
| notify->prop_name = prop_name; |
| |
| ctx->notify_func2(ctx->notify_baton2, notify, pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Use client context CTX to send a suitable notification for a patch TARGET. |
| * Use POOL for temporary allocations. */ |
| static svn_error_t * |
| send_patch_notification(const patch_target_t *target, |
| const svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc_notify_t *notify; |
| svn_wc_notify_action_t action; |
| const char *notify_path; |
| |
| if (! ctx->notify_func2) |
| return SVN_NO_ERROR; |
| |
| if (target->skipped) |
| action = svn_wc_notify_skip; |
| else if (target->deleted) |
| action = svn_wc_notify_delete; |
| else if (target->added || target->move_target_abspath) |
| action = svn_wc_notify_add; |
| else |
| action = svn_wc_notify_patch; |
| |
| if (target->move_target_abspath) |
| notify_path = target->move_target_abspath; |
| else |
| notify_path = target->local_abspath ? target->local_abspath |
| : target->local_relpath; |
| |
| 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->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; |
| } |
| else |
| { |
| if (target->had_rejects) |
| notify->content_state = svn_wc_notify_state_conflicted; |
| 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, scratch_pool); |
| |
| if (action == svn_wc_notify_patch) |
| { |
| int i; |
| apr_pool_t *iterpool; |
| apr_array_header_t *prop_targets; |
| |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < target->content->hunks->nelts; i++) |
| { |
| const hunk_info_t *hi; |
| |
| svn_pool_clear(iterpool); |
| |
| hi = APR_ARRAY_IDX(target->content->hunks, i, hunk_info_t *); |
| |
| SVN_ERR(send_hunk_notification(hi, target, NULL /* prop_name */, |
| ctx, iterpool)); |
| } |
| |
| prop_targets = svn_sort__hash(target->prop_targets, |
| svn_sort_compare_items_lexically, |
| scratch_pool); |
| for (i = 0; i < prop_targets->nelts; i++) |
| { |
| int j; |
| svn_sort__item_t item = APR_ARRAY_IDX(prop_targets, i, |
| svn_sort__item_t); |
| |
| prop_patch_target_t *prop_target = item.value; |
| |
| 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, j, |
| hunk_info_t *); |
| |
| /* Don't notify on the hunk level for added or deleted props. */ |
| 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)); |
| } |
| } |
| svn_pool_destroy(iterpool); |
| } |
| |
| 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, scratch_pool); |
| notify->kind = svn_node_file; |
| ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements the callback for svn_sort__array. Puts hunks that match |
| before hunks that do not match, puts hunks that match in order |
| based on position matched, puts hunks that do not match in order |
| based on original position. */ |
| static int |
| sort_matched_hunks(const void *a, const void *b) |
| { |
| const hunk_info_t *item1 = *((const hunk_info_t * const *)a); |
| const hunk_info_t *item2 = *((const hunk_info_t * const *)b); |
| svn_boolean_t matched1 = !item1->rejected && !item1->already_applied; |
| svn_boolean_t matched2 = !item2->rejected && !item2->already_applied; |
| svn_linenum_t original1, original2; |
| |
| if (matched1 && matched2) |
| { |
| /* Both match so use order matched in file. */ |
| if (item1->matched_line > item2->matched_line) |
| return 1; |
| else if (item1->matched_line == item2->matched_line) |
| return 0; |
| else |
| return -1; |
| } |
| else if (matched2) |
| /* Only second matches, put it before first. */ |
| return 1; |
| else if (matched1) |
| /* Only first matches, put it before second. */ |
| return -1; |
| |
| /* Neither matches, sort by original_start. */ |
| original1 = svn_diff_hunk_get_original_start(item1->hunk); |
| original2 = svn_diff_hunk_get_original_start(item2->hunk); |
| if (original1 > original2) |
| return 1; |
| else if (original1 == original2) |
| return 0; |
| else |
| return -1; |
| } |
| |
| |
| /* Apply a PATCH to a working copy at ABS_WC_PATH and put the result |
| * into temporary files, to be installed in the working copy later. |
| * Return information about the patch target in *PATCH_TARGET, allocated |
| * 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 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. |
| * Do temporary allocations in SCRATCH_POOL. */ |
| static svn_error_t * |
| apply_one_patch(patch_target_t **patch_target, svn_patch_t *patch, |
| const char *abs_wc_path, svn_wc_context_t *wc_ctx, |
| 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) |
| { |
| patch_target_t *target; |
| apr_pool_t *iterpool; |
| int i; |
| 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, targets_info, |
| result_pool, scratch_pool)); |
| if (target->skipped) |
| { |
| *patch_target = target; |
| return SVN_NO_ERROR; |
| } |
| |
| iterpool = svn_pool_create(scratch_pool); |
| |
| if (patch->hunks && patch->hunks->nelts) |
| { |
| /* 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); |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| 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++; |
| } |
| 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; |
| } |
| |
| /* 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); |
| |
| /* 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)); |
| |
| 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)); |
| } |
| |
| 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) |
| { |
| 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 (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 */ |
| |
| target->has_text_changes = TRUE; |
| } |
| else |
| { |
| /* 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; |
| |
| 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) |
| { |
| 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); |
| hash_index; |
| hash_index = apr_hash_next(hash_index)) |
| { |
| svn_prop_patch_t *prop_patch; |
| const char *prop_name; |
| prop_patch_target_t *prop_target; |
| |
| 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 = (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); |
| |
| for (i = 0; i < prop_patch->hunks->nelts; i++) |
| { |
| svn_diff_hunk_t *hunk; |
| hunk_info_t *hi; |
| svn_linenum_t fuzz = 0; |
| |
| svn_pool_clear(iterpool); |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| hunk = APR_ARRAY_IDX(prop_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, prop_target->content, |
| hunk, fuzz, 0, |
| ignore_whitespace, |
| TRUE /* is_prop_hunk */, |
| cancel_func, cancel_baton, |
| result_pool, iterpool)); |
| fuzz++; |
| } |
| while (hi->rejected && fuzz <= MAX_FUZZ && ! hi->already_applied); |
| |
| APR_ARRAY_PUSH(prop_target->content->hunks, hunk_info_t *) = hi; |
| } |
| } |
| |
| /* 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)) |
| { |
| 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; |
| } |
| } |
| } |
| } |
| |
| /* Apply or reject property hunks. */ |
| |
| 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, j, |
| hunk_info_t *); |
| if (hi->already_applied) |
| { |
| 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)); |
| applied_one = 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); |
| |
| if (!target->is_symlink) |
| { |
| /* Now close files we don't need any longer to get their contents |
| * flushed to disk. |
| * But we're not closing the reject file -- it still needed and |
| * 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)); |
| |
| *patch_target = target; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* 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. */ |
| static svn_error_t * |
| 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; |
| apr_array_header_t *components; |
| int present_components; |
| int i; |
| apr_pool_t *iterpool; |
| |
| /* Check if we can safely create the target's parent. */ |
| local_abspath = abs_wc_path; |
| components = svn_path_decompose(target->local_relpath, scratch_pool); |
| present_components = 0; |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < components->nelts - 1; i++) |
| { |
| const char *component; |
| svn_node_kind_t wc_kind, disk_kind; |
| |
| svn_pool_clear(iterpool); |
| |
| component = APR_ARRAY_IDX(components, i, const char *); |
| local_abspath = svn_dirent_join(local_abspath, component, scratch_pool); |
| |
| SVN_ERR(svn_wc_read_kind2(&wc_kind, ctx->wc_ctx, local_abspath, |
| FALSE, TRUE, iterpool)); |
| |
| SVN_ERR(svn_io_check_path(local_abspath, &disk_kind, iterpool)); |
| |
| if (disk_kind == svn_node_file || wc_kind == svn_node_file) |
| { |
| /* on-disk files and missing files are obstructions */ |
| target->skipped = TRUE; |
| break; |
| } |
| else if (disk_kind == svn_node_dir) |
| { |
| if (wc_kind == svn_node_dir) |
| present_components++; |
| else |
| { |
| target->skipped = TRUE; |
| break; |
| } |
| } |
| else if (wc_kind != svn_node_none) |
| { |
| /* Node is missing */ |
| target->skipped = TRUE; |
| break; |
| } |
| else |
| { |
| /* It's not a file, it's not a dir... |
| Let's add a dir */ |
| break; |
| } |
| } |
| if (! target->skipped) |
| { |
| local_abspath = abs_wc_path; |
| for (i = 0; i < present_components; i++) |
| { |
| const char *component; |
| component = APR_ARRAY_IDX(components, i, const char *); |
| local_abspath = svn_dirent_join(local_abspath, |
| component, scratch_pool); |
| } |
| |
| if (!dry_run && present_components < components->nelts - 1) |
| SVN_ERR(svn_io_make_dir_recursively( |
| svn_dirent_join( |
| abs_wc_path, |
| svn_relpath_dirname(target->local_relpath, |
| scratch_pool), |
| scratch_pool), |
| scratch_pool)); |
| |
| 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) |
| { |
| /* Just do notification. */ |
| svn_wc_notify_t *notify; |
| notify = svn_wc_create_notify(local_abspath, |
| svn_wc_notify_add, |
| iterpool); |
| notify->kind = svn_node_dir; |
| ctx->notify_func2(ctx->notify_baton2, notify, |
| iterpool); |
| } |
| } |
| else |
| { |
| /* Create the missing component and add it |
| * to version control. Allow cancellation since we |
| * have not modified the working copy yet for this |
| * target. */ |
| SVN_ERR(svn_wc_add_from_disk3(ctx->wc_ctx, local_abspath, |
| NULL /*props*/, |
| FALSE /* skip checks */, |
| ctx->notify_func2, ctx->notify_baton2, |
| iterpool)); |
| } |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Install a patched TARGET into the working copy at ABS_WC_PATH. |
| * Use client context CTX to retrieve WC_CTX, and possibly doing |
| * 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) |
| { |
| if (! dry_run) |
| { |
| /* Schedule the target for deletion. Suppress |
| * notification, we'll do it manually in a minute |
| * because we also need to notify during dry-run. |
| * Also suppress cancellation, because we'd rather |
| * notify about what we did before aborting. */ |
| SVN_ERR(svn_wc_delete4(ctx->wc_ctx, target->local_abspath, |
| FALSE /* keep_local */, FALSE, |
| ctx->cancel_func, ctx->cancel_baton, |
| NULL, NULL /* notify */, |
| pool)); |
| } |
| } |
| else |
| { |
| svn_node_kind_t parent_db_kind; |
| if (target->added) |
| { |
| const char *parent_abspath; |
| |
| parent_abspath = svn_dirent_dirname(target->local_abspath, |
| pool); |
| /* If the target's parent directory does not yet exist |
| * we need to create it before we can copy the patched |
| * result in place. */ |
| SVN_ERR(svn_wc_read_kind2(&parent_db_kind, ctx->wc_ctx, |
| parent_abspath, FALSE, FALSE, pool)); |
| |
| /* We can't add targets under nodes scheduled for delete, so add |
| a new directory if needed. */ |
| if (parent_db_kind == svn_node_dir |
| || parent_db_kind == svn_node_file) |
| { |
| if (parent_db_kind != svn_node_dir) |
| target->skipped = TRUE; |
| else |
| { |
| svn_node_kind_t disk_kind; |
| |
| SVN_ERR(svn_io_check_path(parent_abspath, &disk_kind, pool)); |
| if (disk_kind != svn_node_dir) |
| target->skipped = TRUE; |
| } |
| } |
| else |
| SVN_ERR(create_missing_parents(target, abs_wc_path, ctx, |
| dry_run, targets_info, pool)); |
| |
| } |
| else |
| { |
| svn_node_kind_t wc_kind; |
| |
| /* The target should exist */ |
| SVN_ERR(svn_wc_read_kind2(&wc_kind, ctx->wc_ctx, |
| target->local_abspath, |
| FALSE, FALSE, pool)); |
| |
| if (target->kind_on_disk == svn_node_none |
| || wc_kind != target->kind_on_disk) |
| { |
| target->skipped = TRUE; |
| if (wc_kind != target->kind_on_disk) |
| target->obstructed = TRUE; |
| } |
| } |
| |
| if (! dry_run && ! target->skipped) |
| { |
| if (target->is_special) |
| { |
| svn_stream_t *stream; |
| svn_stream_t *patched_stream; |
| |
| SVN_ERR(svn_stream_open_readonly(&patched_stream, |
| target->patched_path, |
| pool, pool)); |
| 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)); |
| } |
| else |
| { |
| svn_boolean_t repair_eol; |
| |
| /* Copy the patched file on top of the target file. |
| * Always expand keywords in the patched file, but repair EOL |
| * only if svn:eol-style dictates a particular style. */ |
| repair_eol = (target->content->eol_style == |
| svn_subst_eol_style_fixed || |
| target->content->eol_style == |
| svn_subst_eol_style_native); |
| |
| SVN_ERR(svn_subst_copy_and_translate4( |
| target->patched_path, |
| target->move_target_abspath |
| ? target->move_target_abspath |
| : target->local_abspath, |
| target->content->eol_str, repair_eol, |
| target->content->keywords, |
| TRUE /* expand */, FALSE /* special */, |
| ctx->cancel_func, ctx->cancel_baton, pool)); |
| } |
| |
| if (target->added) |
| { |
| /* The target file didn't exist previously, |
| * so add it to version control. |
| * Suppress notification, we'll do that later (and also |
| * during dry-run). Don't allow cancellation because |
| * we'd rather notify about what we did before aborting. */ |
| SVN_ERR(svn_wc_add_from_disk3(ctx->wc_ctx, target->local_abspath, |
| NULL /*props*/, |
| FALSE /* skip checks */, |
| NULL, NULL, pool)); |
| } |
| |
| /* Restore the target's executable bit if necessary. */ |
| SVN_ERR(svn_io_set_file_executable(target->move_target_abspath |
| ? target->move_target_abspath |
| : target->local_abspath, |
| target->executable, |
| FALSE, pool)); |
| |
| if (target->move_target_abspath) |
| { |
| /* ### Copying the patched content to the move target location, |
| * performing the move in meta-data, and removing the file at |
| * the move source should be one atomic operation. */ |
| |
| /* ### Create missing parents. */ |
| |
| /* Perform the move in meta-data. */ |
| SVN_ERR(svn_wc__move2(ctx->wc_ctx, |
| target->local_abspath, |
| target->move_target_abspath, |
| TRUE, /* metadata_only */ |
| FALSE, /* allow_mixed_revisions */ |
| ctx->cancel_func, ctx->cancel_baton, |
| NULL, NULL, |
| pool)); |
| |
| /* Delete the patch target's old location from disk. */ |
| SVN_ERR(svn_io_remove_file2(target->local_abspath, FALSE, pool)); |
| } |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Write out rejected hunks, if any, to TARGET->REJECT_PATH. If DRY_RUN is |
| * TRUE, don't modify the working copy. |
| * Do temporary allocations in POOL. |
| */ |
| static svn_error_t * |
| write_out_rejected_hunks(patch_target_t *target, |
| const char *root_abspath, |
| svn_boolean_t dry_run, |
| apr_pool_t *scratch_pool) |
| { |
| if (! dry_run && (target->had_rejects || target->had_prop_rejects)) |
| { |
| /* Write out rejected hunks, if any. */ |
| 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; |
| } |
| |
| /* Install the patched properties for TARGET. Use client context CTX to |
| * retrieve WC_CTX. If DRY_RUN is TRUE, don't modify the working copy. |
| * Do temporary allocations in SCRATCH_POOL. */ |
| static svn_error_t * |
| install_patched_prop_targets(patch_target_t *target, |
| svn_client_ctx_t *ctx, svn_boolean_t dry_run, |
| apr_pool_t *scratch_pool) |
| { |
| 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); |
| |
| for (hi = apr_hash_first(scratch_pool, target->prop_targets); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| prop_patch_target_t *prop_target = apr_hash_this_val(hi); |
| const svn_string_t *prop_val; |
| svn_error_t *err; |
| |
| svn_pool_clear(iterpool); |
| |
| 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, local_abspath, |
| prop_target->name, NULL, svn_depth_empty, |
| TRUE /* skip_checks */, |
| NULL /* changelist_filter */, |
| NULL, NULL /* cancellation */, |
| NULL, NULL /* notification */, |
| iterpool)); |
| continue; |
| } |
| |
| /* 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. */ |
| if (prop_target->value && prop_target->value->len |
| && prop_target->patched_value && !prop_target->patched_value->len) |
| prop_val = NULL; |
| else |
| prop_val = svn_stringbuf__morph_into_string(prop_target->patched_value); |
| |
| if (dry_run) |
| { |
| const svn_string_t *canon_propval; |
| |
| err = svn_wc_canonicalize_svn_prop(&canon_propval, |
| prop_target->name, |
| prop_val, local_abspath, |
| target->db_kind, |
| TRUE, /* ### Skipping checks */ |
| NULL, NULL, |
| iterpool); |
| } |
| else |
| { |
| err = svn_wc_prop_set4(ctx->wc_ctx, local_abspath, |
| prop_target->name, prop_val, svn_depth_empty, |
| TRUE /* skip_checks */, |
| NULL /* changelist_filter */, |
| NULL, NULL /* cancellation */, |
| NULL, NULL /* notification */, |
| iterpool); |
| } |
| |
| if (err) |
| { |
| /* ### The errors which svn_wc_canonicalize_svn_prop() will |
| * ### return aren't documented. */ |
| if (err->apr_err == SVN_ERR_ILLEGAL_TARGET || |
| err->apr_err == SVN_ERR_NODE_UNEXPECTED_KIND || |
| err->apr_err == SVN_ERR_IO_UNKNOWN_EOL || |
| err->apr_err == SVN_ERR_BAD_MIME_TYPE || |
| err->apr_err == SVN_ERR_CLIENT_INVALID_EXTERNALS_DESCRIPTION) |
| { |
| int i; |
| |
| svn_error_clear(err); |
| |
| for (i = 0; i < prop_target->content->hunks->nelts; i++) |
| { |
| hunk_info_t *hunk_info; |
| |
| hunk_info = APR_ARRAY_IDX(prop_target->content->hunks, |
| i, hunk_info_t *); |
| hunk_info->rejected = TRUE; |
| SVN_ERR(reject_hunk(target, prop_target->content, |
| hunk_info->hunk, prop_target->name, |
| iterpool)); |
| } |
| } |
| else |
| return svn_error_trace(err); |
| } |
| |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Baton for can_delete_callback */ |
| struct can_delete_baton_t |
| { |
| svn_boolean_t must_keep; |
| const apr_array_header_t *targets_info; |
| const char *local_abspath; |
| }; |
| |
| /* Implements svn_wc_status_func4_t. */ |
| static svn_error_t * |
| can_delete_callback(void *baton, |
| const char *abspath, |
| const svn_wc_status3_t *status, |
| apr_pool_t *pool) |
| { |
| struct can_delete_baton_t *cb = baton; |
| int i; |
| |
| switch(status->node_status) |
| { |
| case svn_wc_status_none: |
| case svn_wc_status_deleted: |
| return SVN_NO_ERROR; |
| |
| default: |
| if (! strcmp(cb->local_abspath, abspath)) |
| return SVN_NO_ERROR; /* Only interested in descendants */ |
| |
| for (i = 0; i < cb->targets_info->nelts; i++) |
| { |
| const patch_target_info_t *target_info = |
| APR_ARRAY_IDX(cb->targets_info, i, const patch_target_info_t *); |
| |
| if (! strcmp(target_info->local_abspath, abspath)) |
| { |
| if (target_info->deleted) |
| return SVN_NO_ERROR; |
| |
| break; /* Cease invocation; must keep */ |
| } |
| } |
| |
| cb->must_keep = TRUE; |
| |
| return svn_error_create(SVN_ERR_CEASE_INVOCATION, NULL, NULL); |
| } |
| } |
| |
| static svn_error_t * |
| check_ancestor_delete(const char *deleted_target, |
| apr_array_header_t *targets_info, |
| const char *apply_root, |
| svn_boolean_t dry_run, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| struct can_delete_baton_t cb; |
| svn_error_t *err; |
| apr_array_header_t *ignores; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| |
| const char *dir_abspath = svn_dirent_dirname(deleted_target, scratch_pool); |
| |
| SVN_ERR(svn_wc_get_default_ignores(&ignores, ctx->config, scratch_pool)); |
| |
| while (svn_dirent_is_child(apply_root, dir_abspath, iterpool)) |
| { |
| svn_pool_clear(iterpool); |
| |
| cb.local_abspath = dir_abspath; |
| cb.must_keep = FALSE; |
| cb.targets_info = targets_info; |
| |
| err = svn_wc_walk_status(ctx->wc_ctx, dir_abspath, svn_depth_infinity, |
| TRUE, FALSE, FALSE, ignores, |
| can_delete_callback, &cb, |
| ctx->cancel_func, ctx->cancel_baton, |
| iterpool); |
| |
| if (err) |
| { |
| if (err->apr_err != SVN_ERR_CEASE_INVOCATION) |
| return svn_error_trace(err); |
| |
| svn_error_clear(err); |
| } |
| |
| if (cb.must_keep) |
| { |
| break; |
| } |
| |
| if (! dry_run) |
| { |
| SVN_ERR(svn_wc_delete4(ctx->wc_ctx, dir_abspath, FALSE, FALSE, |
| ctx->cancel_func, ctx->cancel_baton, |
| NULL, NULL, |
| scratch_pool)); |
| } |
| |
| { |
| patch_target_info_t *pti = apr_pcalloc(result_pool, sizeof(*pti)); |
| |
| pti->local_abspath = apr_pstrdup(result_pool, dir_abspath); |
| pti->deleted = TRUE; |
| |
| APR_ARRAY_PUSH(targets_info, patch_target_info_t *) = pti; |
| } |
| |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| |
| notify = svn_wc_create_notify(dir_abspath, svn_wc_notify_delete, |
| iterpool); |
| notify->kind = svn_node_dir; |
| |
| ctx->notify_func2(ctx->notify_baton2, notify, iterpool); |
| } |
| |
| /* And check if we must also delete the parent */ |
| dir_abspath = svn_dirent_dirname(dir_abspath, scratch_pool); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* This function is the main entry point into the patch code. */ |
| 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 *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. */ |
| int strip_count, |
| /* Whether to apply the patch in reverse. */ |
| svn_boolean_t reverse, |
| /* Whether to ignore whitespace when matching context lines. */ |
| svn_boolean_t ignore_whitespace, |
| /* As in svn_client_patch(). */ |
| svn_boolean_t remove_tempfiles, |
| /* As in svn_client_patch(). */ |
| svn_client_patch_func_t patch_func, |
| void *patch_baton, |
| /* The client context. */ |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_patch_t *patch; |
| apr_pool_t *iterpool; |
| svn_patch_file_t *patch_file; |
| apr_array_header_t *targets_info; |
| |
| /* Try to open the patch file. */ |
| SVN_ERR(svn_diff_open_patch_file(&patch_file, patch_abspath, scratch_pool)); |
| |
| /* Apply patches. */ |
| targets_info = apr_array_make(scratch_pool, 0, |
| sizeof(patch_target_info_t *)); |
| iterpool = svn_pool_create(scratch_pool); |
| do |
| { |
| svn_pool_clear(iterpool); |
| |
| if (ctx->cancel_func) |
| SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); |
| |
| SVN_ERR(svn_diff_parse_next_patch(&patch, patch_file, |
| reverse, ignore_whitespace, |
| iterpool, iterpool)); |
| if (patch) |
| { |
| patch_target_t *target; |
| svn_boolean_t filtered = FALSE; |
| |
| 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)); |
| |
| if (!target->skipped && patch_func) |
| { |
| SVN_ERR(patch_func(patch_baton, &filtered, |
| target->canon_path_from_patchfile, |
| target->patched_path, target->reject_path, |
| iterpool)); |
| } |
| |
| if (! filtered) |
| { |
| /* Save info we'll still need when we're done patching. */ |
| patch_target_info_t *target_info = |
| apr_pcalloc(scratch_pool, sizeof(patch_target_info_t)); |
| target_info->local_abspath = apr_pstrdup(scratch_pool, |
| target->local_abspath); |
| target_info->deleted = target->deleted; |
| target_info->added = target->added; |
| |
| if (! target->skipped) |
| { |
| if (target->has_text_changes |
| || target->added |
| || target->move_target_abspath |
| || target->deleted) |
| 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, 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, root_abspath, |
| dry_run, ctx, |
| scratch_pool, iterpool)); |
| } |
| } |
| } |
| } |
| while (patch); |
| |
| SVN_ERR(svn_diff_close_patch_file(patch_file, iterpool)); |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_patch(const char *patch_abspath, |
| const char *wc_dir_abspath, |
| svn_boolean_t dry_run, |
| int strip_count, |
| svn_boolean_t reverse, |
| svn_boolean_t ignore_whitespace, |
| svn_boolean_t remove_tempfiles, |
| svn_client_patch_func_t patch_func, |
| void *patch_baton, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| svn_node_kind_t kind; |
| |
| if (strip_count < 0) |
| return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, |
| _("strip count must be positive")); |
| |
| if (svn_path_is_url(wc_dir_abspath)) |
| return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("'%s' is not a local path"), |
| svn_dirent_local_style(wc_dir_abspath, |
| scratch_pool)); |
| |
| SVN_ERR(svn_io_check_path(patch_abspath, &kind, scratch_pool)); |
| if (kind == svn_node_none) |
| return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("'%s' does not exist"), |
| svn_dirent_local_style(patch_abspath, |
| scratch_pool)); |
| if (kind != svn_node_file) |
| return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("'%s' is not a file"), |
| svn_dirent_local_style(patch_abspath, |
| scratch_pool)); |
| |
| SVN_ERR(svn_io_check_path(wc_dir_abspath, &kind, scratch_pool)); |
| if (kind == svn_node_none) |
| return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("'%s' does not exist"), |
| svn_dirent_local_style(wc_dir_abspath, |
| scratch_pool)); |
| if (kind != svn_node_dir) |
| return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("'%s' is not a directory"), |
| svn_dirent_local_style(wc_dir_abspath, |
| scratch_pool)); |
| |
| SVN_WC__CALL_WITH_WRITE_LOCK( |
| apply_patches(patch_abspath, wc_dir_abspath, dry_run, strip_count, |
| reverse, ignore_whitespace, remove_tempfiles, |
| patch_func, patch_baton, ctx, scratch_pool), |
| ctx->wc_ctx, wc_dir_abspath, FALSE /* lock_anchor */, scratch_pool); |
| return SVN_NO_ERROR; |
| } |