| /* |
| * props.c : routines dealing with properties in the working copy |
| * |
| * ==================================================================== |
| * Copyright (c) 2000-2007 CollabNet. All rights reserved. |
| * |
| * This software is licensed as described in the file COPYING, which |
| * you should have received as part of this distribution. The terms |
| * are also available at http://subversion.tigris.org/license-1.html. |
| * If newer versions of this license are posted there, you may use a |
| * newer version instead, at your option. |
| * |
| * This software consists of voluntary contributions made by many |
| * individuals. For exact contribution history, see the revision |
| * history and logs, available at http://subversion.tigris.org/. |
| * ==================================================================== |
| */ |
| |
| |
| |
| #include <stdlib.h> |
| #include <string.h> |
| #include <apr_pools.h> |
| #include <apr_hash.h> |
| #include <apr_tables.h> |
| #include <apr_file_io.h> |
| #include <apr_strings.h> |
| #include <apr_general.h> |
| #include "svn_types.h" |
| #include "svn_string.h" |
| #include "svn_pools.h" |
| #include "svn_path.h" |
| #include "svn_xml.h" |
| #include "svn_error.h" |
| #include "svn_props.h" |
| #include "svn_io.h" |
| #include "svn_hash.h" |
| #include "svn_mergeinfo.h" |
| #include "svn_wc.h" |
| #include "svn_utf.h" |
| #include "svn_diff.h" |
| |
| #include "private/svn_wc_private.h" |
| #include "private/svn_mergeinfo_private.h" |
| |
| #include "wc.h" |
| #include "log.h" |
| #include "adm_files.h" |
| #include "entries.h" |
| #include "props.h" |
| #include "translate.h" |
| #include "questions.h" |
| #include "lock.h" |
| |
| #include "svn_private_config.h" |
| |
| /*---------------------------------------------------------------------*/ |
| |
| /*** Deducing local changes to properties ***/ |
| |
| /*---------------------------------------------------------------------*/ |
| |
| /*** Reading/writing property hashes from disk ***/ |
| |
| /* The real functionality here is part of libsvn_subr, in hashdump.c. |
| But these are convenience routines for use in libsvn_wc. */ |
| |
| static svn_error_t * |
| get_prop_path(const char **ppath, |
| const char *path, |
| svn_wc__props_kind_t props_kind, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| const svn_wc_entry_t *entry; |
| |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, TRUE, pool)); |
| return svn_wc__prop_path(ppath, path, entry->kind, |
| props_kind, FALSE, pool); |
| } |
| |
| /* If PROPFILE_PATH exists (and is a file), assume it's full of |
| properties and load this file into HASH. Otherwise, leave HASH |
| untouched. */ |
| static svn_error_t * |
| load_prop_file(const char *propfile_path, |
| apr_hash_t *hash, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| svn_stream_t *stream; |
| apr_finfo_t finfo; |
| |
| /* We shouldn't be calling load_prop_file() with an empty file, but |
| we do. This check makes sure that we don't call svn_hash_read2() |
| on an empty stream. Ugly, hacky and crude. */ |
| err = svn_io_stat(&finfo, propfile_path, APR_FINFO_SIZE, pool); |
| if (err && (APR_STATUS_IS_ENOENT(err->apr_err) |
| || APR_STATUS_IS_ENOTDIR(err->apr_err))) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| SVN_ERR(err); |
| if (finfo.size == 0) |
| return SVN_NO_ERROR; |
| |
| err = svn_stream_open_readonly(&stream, propfile_path, pool, pool); |
| |
| if (err && (APR_STATUS_IS_ENOENT(err->apr_err) |
| || APR_STATUS_IS_ENOTDIR(err->apr_err))) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| |
| SVN_ERR(err); |
| |
| SVN_ERR_W(svn_hash_read2(hash, stream, SVN_HASH_TERMINATOR, pool), |
| apr_psprintf(pool, _("Can't parse '%s'"), |
| svn_path_local_style(propfile_path, pool))); |
| |
| return svn_stream_close(stream); |
| } |
| |
| |
| |
| /* Given a HASH full of property name/values, write them to a file |
| located at PROPFILE_PATH. If WRITE_EMPTY is TRUE then writing |
| an emtpy property hash will result in an actual empty property |
| file on disk, otherwise an empty hash will result in no file |
| being written at all. */ |
| static svn_error_t * |
| save_prop_file(const char *propfile_path, |
| apr_hash_t *hash, |
| svn_boolean_t write_empty, |
| apr_pool_t *pool) |
| { |
| apr_file_t *prop_tmp; |
| svn_stream_t *stream; |
| |
| SVN_ERR(svn_io_file_open(&prop_tmp, propfile_path, |
| (APR_WRITE | APR_CREATE | APR_TRUNCATE |
| | APR_BUFFERED), |
| APR_OS_DEFAULT, pool)); |
| stream = svn_stream_from_aprfile2(prop_tmp, FALSE, pool); |
| |
| if (apr_hash_count(hash) != 0 || write_empty) |
| SVN_ERR_W(svn_hash_write2(hash, stream, SVN_HASH_TERMINATOR, pool), |
| apr_psprintf(pool, |
| _("Can't write property hash to '%s'"), |
| svn_path_local_style(propfile_path, pool))); |
| |
| return svn_stream_close(stream); |
| } |
| |
| |
| /*---------------------------------------------------------------------*/ |
| |
| /*** Misc ***/ |
| |
| /* Opens reject temporary file for FULL_PATH. */ |
| static svn_error_t * |
| open_reject_tmp_file(apr_file_t **fp, const char **reject_tmp_path, |
| const char *full_path, |
| svn_wc_adm_access_t *adm_access, |
| svn_boolean_t is_dir, apr_pool_t *pool) |
| { |
| const char *tmp_path; |
| |
| /* Get path to /temporary/ local prop file */ |
| SVN_ERR(svn_wc__prop_path(&tmp_path, full_path, |
| is_dir ? svn_node_dir : svn_node_file, |
| svn_wc__props_working, TRUE, pool)); |
| |
| /* Reserve a .prej file based on it. */ |
| return svn_io_open_unique_file2(fp, reject_tmp_path, tmp_path, |
| SVN_WC__PROP_REJ_EXT, |
| svn_io_file_del_none, pool); |
| } |
| |
| |
| /* Assuming FP is a filehandle already open for appending, write |
| CONFLICT_DESCRIPTION to file, plus a trailing EOL sequence. */ |
| static svn_error_t * |
| append_prop_conflict(apr_file_t *fp, |
| const svn_string_t *conflict_description, |
| apr_pool_t *pool) |
| { |
| /* TODO: someday, perhaps prefix each conflict_description with a |
| timestamp or something? */ |
| apr_size_t written; |
| const char *native_text = |
| svn_utf_cstring_from_utf8_fuzzy(conflict_description->data, pool); |
| SVN_ERR(svn_io_file_write_full(fp, native_text, strlen(native_text), |
| &written, pool)); |
| |
| native_text = svn_utf_cstring_from_utf8_fuzzy(APR_EOL_STR, pool); |
| return svn_io_file_write_full(fp, native_text, strlen(native_text), |
| &written, pool); |
| } |
| |
| |
| /* Look up the entry for PATH within ADM_ACCESS and see if it has a `current' |
| reject file describing a state of conflict. Set *REJECT_FILE to the |
| name of that file, or to NULL if no such file exists. */ |
| static svn_error_t * |
| get_existing_prop_reject_file(const char **reject_file, |
| svn_wc_adm_access_t *adm_access, |
| const char *path, |
| apr_pool_t *pool) |
| { |
| const svn_wc_entry_t *entry; |
| |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, FALSE, pool)); |
| |
| *reject_file = entry->prejfile |
| ? apr_pstrcat(pool, svn_wc_adm_access_path(adm_access), |
| entry->prejfile, NULL) |
| : NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| /*---------------------------------------------------------------------*/ |
| |
| |
| /* Build a space separated list of properties that are contained in |
| the hash PROPS and which we want to cache. |
| The string is allocated in POOL. */ |
| static const char * |
| build_present_props(apr_hash_t *props, apr_pool_t *pool) |
| { |
| apr_array_header_t *cachable; |
| svn_stringbuf_t *present_props = svn_stringbuf_create("", pool); |
| int i; |
| |
| if (apr_hash_count(props) == 0) |
| return present_props->data; |
| |
| cachable = svn_cstring_split(SVN_WC__CACHABLE_PROPS, " ", TRUE, pool); |
| for (i = 0; i < cachable->nelts; i++) |
| { |
| const char *proptolookfor = APR_ARRAY_IDX(cachable, i, |
| const char *); |
| |
| if (apr_hash_get(props, proptolookfor, APR_HASH_KEY_STRING) != NULL) |
| { |
| svn_stringbuf_appendcstr(present_props, proptolookfor); |
| svn_stringbuf_appendcstr(present_props, " "); |
| } |
| } |
| |
| /* Avoid returning a string with a trailing space. */ |
| svn_stringbuf_chop(present_props, 1); |
| return present_props->data; |
| } |
| |
| /*** Loading regular properties. ***/ |
| svn_error_t * |
| svn_wc__load_props(apr_hash_t **base_props_p, |
| apr_hash_t **props_p, |
| apr_hash_t **revert_props_p, |
| svn_wc_adm_access_t *adm_access, |
| const char *path, |
| apr_pool_t *pool) |
| { |
| svn_node_kind_t kind; |
| svn_boolean_t has_propcaching = |
| svn_wc__adm_wc_format(adm_access) > SVN_WC__NO_PROPCACHING_VERSION; |
| const svn_wc_entry_t *entry; |
| apr_hash_t *base_props = NULL; /* Silence uninitialized warning. */ |
| |
| SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, pool)); |
| /* If there is no entry, we just return empty hashes, since the |
| property merging can use this function when there is no entry. */ |
| if (! entry) |
| { |
| if (base_props_p) |
| *base_props_p = apr_hash_make(pool); |
| if (props_p) |
| *props_p = apr_hash_make(pool); |
| if (revert_props_p) |
| *revert_props_p = apr_hash_make(pool); |
| return SVN_NO_ERROR; |
| } |
| |
| kind = entry->kind; |
| /* We will need the base props if the user requested them, OR, |
| our WC has prop caching, the user requested working props and there are no |
| prop mods. */ |
| if (base_props_p |
| || (has_propcaching && ! entry->has_prop_mods && entry->has_props)) |
| { |
| const char *prop_base_path; |
| |
| SVN_ERR(svn_wc__prop_path(&prop_base_path, |
| path, kind, svn_wc__props_base, FALSE, pool)); |
| base_props = apr_hash_make(pool); |
| SVN_ERR(load_prop_file(prop_base_path, base_props, pool)); |
| |
| if (base_props_p) |
| *base_props_p = base_props; |
| } |
| |
| if (props_p) |
| { |
| if (has_propcaching && ! entry->has_prop_mods && entry->has_props) |
| *props_p = apr_hash_copy(pool, base_props); |
| else if (! has_propcaching || entry->has_props) |
| { |
| const char *prop_path; |
| |
| SVN_ERR(svn_wc__prop_path(&prop_path, path, kind, |
| svn_wc__props_working, FALSE, pool)); |
| *props_p = apr_hash_make(pool); |
| SVN_ERR(load_prop_file(prop_path, *props_p, pool)); |
| } |
| else |
| *props_p = apr_hash_make(pool); |
| } |
| |
| if (revert_props_p) |
| { |
| *revert_props_p = apr_hash_make(pool); |
| |
| if (entry->schedule == svn_wc_schedule_replace) |
| { |
| const char *revert_prop_path; |
| |
| SVN_ERR(svn_wc__prop_path(&revert_prop_path, path, kind, |
| svn_wc__props_revert, FALSE, pool)); |
| SVN_ERR(load_prop_file(revert_prop_path, *revert_props_p, pool)); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /*---------------------------------------------------------------------*/ |
| |
| /*** Installing new properties. ***/ |
| |
| /* Extend LOG_ACCUM with log commands to write the properties PROPS into |
| * the admin file specified by WC_PROP_KIND. ADM_ACCESS and PATH specify |
| * the WC item with which this file should be associated. */ |
| static svn_error_t * |
| install_props_file(svn_stringbuf_t **log_accum, |
| svn_wc_adm_access_t *adm_access, |
| const char *path, |
| apr_hash_t *props, |
| svn_wc__props_kind_t wc_prop_kind, |
| apr_pool_t *pool) |
| { |
| svn_node_kind_t node_kind; |
| const char *propfile_path; |
| const char *propfile_tmp_path; |
| |
| if (! svn_path_is_child(svn_wc_adm_access_path(adm_access), path, NULL)) |
| node_kind = svn_node_dir; |
| else |
| node_kind = svn_node_file; |
| |
| SVN_ERR(svn_wc__prop_path(&propfile_path, path, |
| node_kind, wc_prop_kind, FALSE, pool)); |
| |
| /* Write the property hash into a temporary file. */ |
| SVN_ERR(svn_wc__prop_path(&propfile_tmp_path, path, |
| node_kind, wc_prop_kind, TRUE, pool)); |
| SVN_ERR(save_prop_file(propfile_tmp_path, props, |
| FALSE, pool)); |
| |
| /* Write a log entry to move tmp file to real file. */ |
| SVN_ERR(svn_wc__loggy_move(log_accum, adm_access, |
| propfile_tmp_path, |
| propfile_path, |
| pool)); |
| |
| /* Make the props file read-only */ |
| return svn_wc__loggy_set_readonly(log_accum, adm_access, |
| propfile_path, pool); |
| } |
| |
| svn_error_t * |
| svn_wc__install_props(svn_stringbuf_t **log_accum, |
| svn_wc_adm_access_t *adm_access, |
| const char *path, |
| apr_hash_t *base_props, |
| apr_hash_t *working_props, |
| svn_boolean_t write_base_props, |
| apr_pool_t *pool) |
| { |
| apr_array_header_t *prop_diffs; |
| const svn_wc_entry_t *entry; |
| svn_wc_entry_t tmp_entry; |
| svn_node_kind_t kind; |
| svn_boolean_t has_propcaching = |
| svn_wc__adm_wc_format(adm_access) > SVN_WC__NO_PROPCACHING_VERSION; |
| |
| if (! svn_path_is_child(svn_wc_adm_access_path(adm_access), path, NULL)) |
| kind = svn_node_dir; |
| else |
| kind = svn_node_file; |
| |
| /* Check if the props are modified, and update the entry. */ |
| SVN_ERR(svn_prop_diffs(&prop_diffs, working_props, base_props, pool)); |
| tmp_entry.has_prop_mods = (prop_diffs->nelts > 0); |
| tmp_entry.has_props = (apr_hash_count(working_props) > 0); |
| tmp_entry.cachable_props = SVN_WC__CACHABLE_PROPS; |
| tmp_entry.present_props = build_present_props(working_props, pool); |
| |
| SVN_ERR(svn_wc__loggy_entry_modify(log_accum, adm_access, |
| path, &tmp_entry, |
| SVN_WC__ENTRY_MODIFY_HAS_PROPS |
| | SVN_WC__ENTRY_MODIFY_HAS_PROP_MODS |
| | SVN_WC__ENTRY_MODIFY_CACHABLE_PROPS |
| | SVN_WC__ENTRY_MODIFY_PRESENT_PROPS, |
| pool)); |
| |
| if (has_propcaching) |
| SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, pool)); |
| else |
| entry = NULL; |
| |
| /* Save the working properties file if it differs from base. */ |
| if (tmp_entry.has_prop_mods) |
| { |
| SVN_ERR(install_props_file(log_accum, adm_access, path, working_props, |
| svn_wc__props_working, pool)); |
| } |
| else |
| { |
| /* No property modifications, remove the file instead. */ |
| const char *working_propfile_path; |
| |
| SVN_ERR(svn_wc__prop_path(&working_propfile_path, path, |
| kind, svn_wc__props_working, FALSE, pool)); |
| |
| if (! has_propcaching || (entry && entry->has_prop_mods)) |
| SVN_ERR(svn_wc__loggy_remove(log_accum, adm_access, |
| working_propfile_path, pool)); |
| } |
| |
| /* Repeat the above steps for the base properties if required. */ |
| if (write_base_props) |
| { |
| if (apr_hash_count(base_props) > 0) |
| { |
| SVN_ERR(install_props_file(log_accum, adm_access, path, base_props, |
| svn_wc__props_base, pool)); |
| } |
| else |
| { |
| const char *base_propfile_path; |
| |
| SVN_ERR(svn_wc__prop_path(&base_propfile_path, path, |
| kind, svn_wc__props_base, FALSE, pool)); |
| |
| if (! has_propcaching || (entry && entry->has_props)) |
| SVN_ERR(svn_wc__loggy_remove(log_accum, adm_access, |
| base_propfile_path, pool)); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| |
| svn_error_t * |
| svn_wc__working_props_committed(const char *path, |
| svn_wc_adm_access_t *adm_access, |
| svn_boolean_t sync_entries, |
| apr_pool_t *pool) |
| { |
| const char *working; |
| const char *base; |
| const svn_wc_entry_t *entry; |
| svn_wc_entry_t mod_entry; |
| svn_wc_adm_access_t *mod_access; |
| |
| |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, TRUE, pool)); |
| |
| SVN_ERR(svn_wc__prop_path(&working, path, entry->kind, |
| svn_wc__props_working, FALSE, pool)); |
| SVN_ERR(svn_wc__prop_path(&base, path, entry->kind, |
| svn_wc__props_base, FALSE, pool)); |
| |
| /* svn_io_file_rename() retains a read-only bit, so there's no |
| need to explicitly set it. */ |
| SVN_ERR(svn_io_file_rename(working, base, pool)); |
| |
| SVN_ERR(svn_wc_adm_probe_retrieve(&mod_access, adm_access, path, pool)); |
| mod_entry.has_prop_mods = FALSE; |
| return svn_wc__entry_modify(mod_access, entry->name, &mod_entry, |
| SVN_WC__ENTRY_MODIFY_HAS_PROP_MODS, |
| sync_entries, pool); |
| } |
| |
| |
| svn_error_t * |
| svn_wc__props_last_modified(apr_time_t *mod_time, |
| const char *path, |
| svn_wc__props_kind_t props_kind, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| const char *props_file; |
| |
| SVN_ERR(get_prop_path(&props_file, path, props_kind, adm_access, pool)); |
| |
| err = svn_io_file_affected_time(mod_time, props_file, pool); |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| svn_error_clear(err); |
| *mod_time = 0; |
| } |
| else |
| SVN_ERR_W(err, |
| apr_psprintf(pool, |
| _("Error getting 'affected time' on '%s'"), |
| svn_path_local_style(props_file, pool))); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| remove_file_if_present(const char *file, apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| |
| /* Try to remove the file. */ |
| err = svn_io_remove_file(file, pool); |
| |
| /* Ignore file not found error. */ |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| svn_error_clear(err); |
| err = SVN_NO_ERROR; |
| } |
| |
| return err; |
| } |
| |
| |
| /* If wcprops are stored in a single file in this working copy, read that file |
| and store it in the cache of ADM_ACCESS. Use POOL for temporary |
| allocations. */ |
| static svn_error_t * |
| read_wcprops(svn_wc_adm_access_t *adm_access, apr_pool_t *pool) |
| { |
| apr_pool_t *cache_pool = svn_wc_adm_access_pool(adm_access); |
| apr_hash_t *all_wcprops; |
| apr_hash_t *proplist; |
| svn_stream_t *stream; |
| svn_error_t *err; |
| |
| /* If the WC format is too old, there is nothing to cache. */ |
| if (svn_wc__adm_wc_format(adm_access) <= SVN_WC__WCPROPS_MANY_FILES_VERSION) |
| return SVN_NO_ERROR; |
| |
| all_wcprops = apr_hash_make(cache_pool); |
| |
| err = svn_wc__open_adm_stream(&stream, svn_wc_adm_access_path(adm_access), |
| SVN_WC__ADM_ALL_WCPROPS, pool, pool); |
| |
| /* A non-existent file means there are no props. */ |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| svn_error_clear(err); |
| svn_wc__adm_access_set_wcprops(adm_access, all_wcprops); |
| return SVN_NO_ERROR; |
| } |
| SVN_ERR(err); |
| |
| /* Read the proplist for THIS_DIR. */ |
| proplist = apr_hash_make(cache_pool); |
| SVN_ERR(svn_hash_read2(proplist, stream, SVN_HASH_TERMINATOR, cache_pool)); |
| apr_hash_set(all_wcprops, SVN_WC_ENTRY_THIS_DIR, APR_HASH_KEY_STRING, |
| proplist); |
| |
| /* And now, the children. */ |
| while (1729) |
| { |
| svn_stringbuf_t *line; |
| svn_boolean_t eof; |
| |
| SVN_ERR(svn_stream_readline(stream, &line, "\n", &eof, cache_pool)); |
| if (eof) |
| { |
| if (line->len > 0) |
| return svn_error_createf |
| (SVN_ERR_WC_CORRUPT, NULL, |
| _("Missing end of line in wcprops file for '%s'"), |
| svn_path_local_style(svn_wc_adm_access_path(adm_access), pool)); |
| break; |
| } |
| proplist = apr_hash_make(cache_pool); |
| SVN_ERR(svn_hash_read2(proplist, stream, SVN_HASH_TERMINATOR, |
| cache_pool)); |
| apr_hash_set(all_wcprops, line->data, APR_HASH_KEY_STRING, proplist); |
| } |
| |
| svn_wc__adm_access_set_wcprops(adm_access, all_wcprops); |
| |
| return svn_stream_close(stream); |
| } |
| |
| static svn_error_t * |
| write_wcprops(svn_wc_adm_access_t *adm_access, apr_pool_t *pool) |
| { |
| apr_hash_t *wcprops = svn_wc__adm_access_wcprops(adm_access); |
| svn_stream_t *stream; |
| apr_hash_t *proplist; |
| apr_hash_index_t *hi; |
| apr_pool_t *subpool = svn_pool_create(pool); |
| svn_boolean_t any_props = FALSE; |
| const char *temp_file_path; |
| |
| /* If there are no cached wcprops, there is nothing to do. */ |
| if (! wcprops) |
| return SVN_NO_ERROR; |
| |
| /* Check if there are any properties at all. */ |
| for (hi = apr_hash_first(pool, wcprops); hi && ! any_props; |
| hi = apr_hash_next(hi)) |
| { |
| void *val; |
| |
| apr_hash_this(hi, NULL, NULL, &val); |
| proplist = val; |
| if (apr_hash_count(proplist) > 0) |
| any_props = TRUE; |
| } |
| |
| /* If there are no props, remove the file. */ |
| if (! any_props) |
| { |
| svn_error_t *err; |
| |
| err = svn_wc__remove_adm_file(adm_access, SVN_WC__ADM_ALL_WCPROPS, |
| subpool); |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| else |
| return err; |
| } |
| |
| SVN_ERR(svn_wc__open_adm_writable(&stream, &temp_file_path, |
| svn_wc_adm_access_path(adm_access), |
| SVN_WC__ADM_ALL_WCPROPS, pool, subpool)); |
| |
| /* First, the props for this_dir. */ |
| proplist = apr_hash_get(wcprops, SVN_WC_ENTRY_THIS_DIR, APR_HASH_KEY_STRING); |
| if (! proplist) |
| proplist = apr_hash_make(subpool); |
| SVN_ERR(svn_hash_write2(proplist, stream, SVN_HASH_TERMINATOR, subpool)); |
| |
| /* Write children. */ |
| for (hi = apr_hash_first(pool, wcprops); hi; hi = apr_hash_next(hi)) |
| { |
| const void *key; |
| void *val; |
| const char *name; |
| |
| apr_hash_this(hi, &key, NULL, &val); |
| name = key; |
| proplist = val; |
| |
| /* We already wrote this_dir, and writing empty hashes makes me |
| feel silly... */ |
| if (strcmp(SVN_WC_ENTRY_THIS_DIR, name) == 0 |
| || apr_hash_count(proplist) == 0) |
| continue; |
| |
| svn_pool_clear(subpool); |
| |
| SVN_ERR(svn_stream_printf(stream, subpool, "%s\n", name)); |
| SVN_ERR(svn_hash_write2(proplist, stream, SVN_HASH_TERMINATOR, subpool)); |
| } |
| |
| return svn_wc__close_adm_stream(stream, temp_file_path, |
| svn_wc_adm_access_path(adm_access), |
| SVN_WC__ADM_ALL_WCPROPS, pool); |
| } |
| |
| |
| svn_error_t * |
| svn_wc__wcprops_flush(svn_wc_adm_access_t *adm_access, |
| apr_pool_t *scratch_pool) |
| { |
| return write_wcprops(adm_access, scratch_pool); |
| } |
| |
| |
| svn_error_t * |
| svn_wc__loggy_props_delete(svn_stringbuf_t **log_accum, |
| const char *path, |
| svn_wc__props_kind_t props_kind, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| const char *props_file; |
| |
| if (props_kind == svn_wc__props_wcprop) |
| { |
| /* We use 1 file for all wcprops in a directory, |
| use a helper to remove them from that file */ |
| apr_hash_t *props; |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| apr_hash_index_t *hi; |
| |
| SVN_ERR(svn_wc__wcprop_list(&props, path, adm_access, pool)); |
| /* ### TODO: There's no log command to delete all wcprops |
| from a file at once. Removing all props should do it though. */ |
| |
| for (hi = apr_hash_first(pool, props); hi; hi = apr_hash_next(hi)) |
| { |
| const void *key; |
| const char *name; |
| |
| svn_pool_clear(iterpool); |
| |
| apr_hash_this(hi, &key, NULL, NULL); |
| name = key; |
| |
| SVN_ERR(svn_wc__loggy_modify_wcprop(log_accum, |
| adm_access, path, |
| name, NULL, iterpool)); |
| } |
| |
| svn_pool_destroy(iterpool); |
| } |
| else |
| { |
| SVN_ERR(get_prop_path(&props_file, path, props_kind, adm_access, pool)); |
| SVN_ERR(svn_wc__loggy_remove(log_accum, adm_access, props_file, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_wc__props_delete(const char *path, |
| svn_wc__props_kind_t props_kind, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| const char *props_file; |
| |
| if (props_kind == svn_wc__props_wcprop) |
| { |
| /* We use 1 file for all wcprops in a directory, |
| use a helper to remove them from that file */ |
| |
| svn_wc_adm_access_t *path_access; |
| const char *filename; |
| svn_boolean_t write_needed = FALSE; |
| apr_hash_t *all_wcprops; |
| |
| SVN_ERR(svn_wc_adm_probe_retrieve(&path_access, adm_access, |
| path, pool)); |
| |
| all_wcprops = svn_wc__adm_access_wcprops(path_access); |
| |
| /* If PATH is a directory, then FILENAME will be NULL. |
| If PATH is a file, then FILENAME will be the BASE_NAME of PATH. */ |
| filename = svn_path_is_child(svn_wc_adm_access_path(path_access), |
| path, NULL); |
| if (! filename) |
| { |
| /* There is no point in reading the props just to determine if we |
| need to rewrite them:-), so assume a write is needed if the props |
| aren't already cached. */ |
| if (! all_wcprops || apr_hash_count(all_wcprops) > 0) |
| { |
| apr_pool_t *adm_pool = svn_wc_adm_access_pool(path_access); |
| |
| svn_wc__adm_access_set_wcprops(path_access, |
| apr_hash_make(adm_pool)); |
| write_needed = TRUE; |
| } |
| } |
| else |
| { |
| apr_hash_t *wcprops; |
| |
| if (! all_wcprops) |
| { |
| SVN_ERR(read_wcprops(path_access, pool)); |
| all_wcprops = svn_wc__adm_access_wcprops(path_access); |
| } |
| if (all_wcprops) |
| wcprops = apr_hash_get(all_wcprops, filename, APR_HASH_KEY_STRING); |
| else |
| wcprops = NULL; |
| if (wcprops && apr_hash_count(wcprops) > 0) |
| { |
| apr_hash_set(all_wcprops, filename, APR_HASH_KEY_STRING, NULL); |
| write_needed = TRUE; |
| } |
| } |
| if (write_needed) |
| SVN_ERR(write_wcprops(path_access, pool)); |
| } |
| else |
| { |
| SVN_ERR(get_prop_path(&props_file, path, props_kind, adm_access, pool)); |
| SVN_ERR(remove_file_if_present(props_file, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_wc__loggy_revert_props_create(svn_stringbuf_t **log_accum, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| svn_boolean_t destroy_baseprops, |
| apr_pool_t *pool) |
| { |
| const svn_wc_entry_t *entry; |
| const char *dst_rprop; |
| const char *dst_bprop; |
| const char *tmp_rprop; |
| svn_node_kind_t kind; |
| |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, TRUE, pool)); |
| |
| SVN_ERR(svn_wc__prop_path(&dst_rprop, path, |
| entry->kind, svn_wc__props_revert, FALSE, pool)); |
| SVN_ERR(svn_wc__prop_path(&tmp_rprop, path, |
| entry->kind, svn_wc__props_revert, TRUE, pool)); |
| SVN_ERR(svn_wc__prop_path(&dst_bprop, path, |
| entry->kind, svn_wc__props_base, FALSE, pool)); |
| |
| /* If prop base exist, copy it to revert base. */ |
| SVN_ERR(svn_io_check_path(dst_bprop, &kind, pool)); |
| if (kind == svn_node_file) |
| { |
| if (destroy_baseprops) |
| SVN_ERR(svn_wc__loggy_move(log_accum, |
| adm_access, dst_bprop, dst_rprop, |
| pool)); |
| else |
| { |
| SVN_ERR(svn_io_copy_file(dst_bprop, tmp_rprop, TRUE, pool)); |
| SVN_ERR(svn_wc__loggy_move(log_accum, adm_access, |
| tmp_rprop, dst_rprop, pool)); |
| } |
| } |
| else if (kind == svn_node_none) |
| { |
| /* If there wasn't any prop base we still need an empty revert |
| propfile, otherwise a revert won't know that a change to the |
| props needs to be made (it'll just see no file, and do nothing). |
| So manufacture an empty propfile and force it to be written out. */ |
| |
| SVN_ERR(svn_wc__prop_path(&dst_bprop, path, entry->kind, |
| svn_wc__props_revert, TRUE, pool)); |
| |
| SVN_ERR(save_prop_file(dst_bprop, apr_hash_make(pool), TRUE, pool)); |
| |
| SVN_ERR(svn_wc__loggy_move(log_accum, |
| adm_access, dst_bprop, dst_rprop, |
| pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| #if 0 |
| /*### Some day, when we get better log primitives, |
| we probably want to stat() less, which can be done coding |
| 'calls' to functions like the one below into as a log command.*/ |
| svn_error_t * |
| svn_wc__revert_props_create(const char *path, |
| svn_wc_adm_access_t *adm_access, |
| svn_boolean_t destroy_baseprops, |
| svn_boolean_t maybe_rerun, |
| apr_pool_t *pool) |
| { |
| const svn_wc_entry_t *entry; |
| const char *revert_file, *base_file; |
| const char *tmp_revert_file; |
| svn_error_t *err; |
| |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, TRUE, pool)); |
| |
| SVN_ERR(svn_wc__prop_path(&base_file, path, entry->kind, svn_wc__props_base, |
| FALSE, pool)); |
| SVN_ERR(svn_wc__prop_path(&revert_file, path, entry->kind, |
| svn_wc__props_revert, FALSE, pool)); |
| SVN_ERR(svn_wc__prop_path(&tmp_revert_file, path, entry->kind, |
| svn_wc__props_revert, TRUE, pool)); |
| |
| |
| if (destroy_baseprops) |
| err = svn_io_file_rename(base_file, revert_file, pool); |
| else |
| { |
| err = svn_io_copy_file(base_file, tmp_revert_file, TRUE, pool); |
| if (! err) |
| SVN_ERR(svn_io_file_rename(tmp_revert_file, revert_file, pool)); |
| } |
| |
| if (err && APR_STATUS_IS_ENOENT(err->apr_err)) |
| /* If there's no file to move or copy, create one. */ |
| { |
| svn_node_kind_t kind = svn_node_none; |
| |
| svn_error_clear(err); |
| |
| if (maybe_rerun) |
| SVN_ERR(svn_io_check_path(revert_file, &kind, pool)); |
| |
| if (kind == svn_node_none) |
| { |
| SVN_ERR(save_prop_file(tmp_revert_file, |
| apr_hash_make(pool), TRUE, pool)); |
| SVN_ERR(svn_io_file_rename(base_file, revert_file, pool)); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| #endif |
| |
| svn_error_t * |
| svn_wc__loggy_revert_props_restore(svn_stringbuf_t **log_accum, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| const svn_wc_entry_t *entry; |
| const char *revert_file, *base_file; |
| |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, TRUE, pool)); |
| |
| SVN_ERR(svn_wc__prop_path(&base_file, path, entry->kind, svn_wc__props_base, |
| FALSE, pool)); |
| SVN_ERR(svn_wc__prop_path(&revert_file, path, entry->kind, |
| svn_wc__props_revert, FALSE, pool)); |
| |
| return svn_wc__loggy_move(log_accum, adm_access, |
| revert_file, base_file, pool); |
| } |
| |
| |
| #if 0 |
| /*### Some day, when we get better log primitives, |
| we probably want to stat() less, which can be done coding |
| 'calls' to functions like the one below into as a log command.*/ |
| svn_error_t * |
| svn_wc__revert_props_restore(const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| const svn_wc_entry_t *entry; |
| const char *revert_file, *base_file; |
| |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, TRUE, pool)); |
| |
| SVN_ERR(svn_wc__prop_path(&base_file, path, entry->kind, svn_wc__props_base, |
| FALSE, pool)); |
| SVN_ERR(svn_wc__prop_path(&revert_file, path, entry->kind, |
| svn_wc__props_revert, FALSE, pool)); |
| |
| SVN_ERR(svn_io_file_rename(revert_file, base_file, pool)); |
| return SVN_NO_ERROR; |
| } |
| #endif |
| |
| /*---------------------------------------------------------------------*/ |
| |
| /*** Merging propchanges into the working copy ***/ |
| |
| |
| /* Parse FROM_PROP_VAL and TO_PROP_VAL into mergeinfo hashes, and |
| calculate the deltas between them. */ |
| static svn_error_t * |
| diff_mergeinfo_props(svn_mergeinfo_t *deleted, svn_mergeinfo_t *added, |
| const svn_string_t *from_prop_val, |
| const svn_string_t *to_prop_val, apr_pool_t *pool) |
| { |
| if (svn_string_compare(from_prop_val, to_prop_val)) |
| { |
| /* Don't bothering parsing identical mergeinfo. */ |
| *deleted = apr_hash_make(pool); |
| *added = apr_hash_make(pool); |
| } |
| else |
| { |
| svn_mergeinfo_t from, to; |
| SVN_ERR(svn_mergeinfo_parse(&from, from_prop_val->data, pool)); |
| SVN_ERR(svn_mergeinfo_parse(&to, to_prop_val->data, pool)); |
| SVN_ERR(svn_mergeinfo_diff(deleted, added, from, to, |
| FALSE, pool)); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| /* Parse the mergeinfo from PROP_VAL1 and PROP_VAL2, combine it, then |
| reconstitute it into *OUTPUT. Call when the WC's mergeinfo has |
| been modified to combine it with incoming mergeinfo from the |
| repos. */ |
| static svn_error_t * |
| combine_mergeinfo_props(const svn_string_t **output, |
| const svn_string_t *prop_val1, |
| const svn_string_t *prop_val2, |
| apr_pool_t *pool) |
| { |
| svn_mergeinfo_t mergeinfo1, mergeinfo2; |
| SVN_ERR(svn_mergeinfo_parse(&mergeinfo1, prop_val1->data, pool)); |
| SVN_ERR(svn_mergeinfo_parse(&mergeinfo2, prop_val2->data, pool)); |
| SVN_ERR(svn_mergeinfo_merge(mergeinfo1, mergeinfo2, pool)); |
| return svn_mergeinfo_to_string((svn_string_t **)output, mergeinfo1, pool); |
| } |
| |
| /* Perform a 3-way merge operation on mergeinfo. FROM_PROP_VAL is |
| the "base" property value, WORKING_PROP_VAL is the current value, |
| and TO_PROP_VAL is the new value. */ |
| static svn_error_t * |
| combine_forked_mergeinfo_props(const svn_string_t **output, |
| const svn_string_t *from_prop_val, |
| const svn_string_t *working_prop_val, |
| const svn_string_t *to_prop_val, |
| apr_pool_t *pool) |
| { |
| svn_mergeinfo_t from_mergeinfo, l_deleted, l_added, r_deleted, r_added; |
| |
| /* ### OPTIMIZE: Use from_mergeinfo when diff'ing. */ |
| SVN_ERR(diff_mergeinfo_props(&l_deleted, &l_added, from_prop_val, |
| working_prop_val, pool)); |
| SVN_ERR(diff_mergeinfo_props(&r_deleted, &r_added, from_prop_val, |
| to_prop_val, pool)); |
| SVN_ERR(svn_mergeinfo_merge(l_deleted, r_deleted, pool)); |
| SVN_ERR(svn_mergeinfo_merge(l_added, r_added, pool)); |
| |
| /* Apply the combined deltas to the base. */ |
| SVN_ERR(svn_mergeinfo_parse(&from_mergeinfo, from_prop_val->data, pool)); |
| SVN_ERR(svn_mergeinfo_merge(from_mergeinfo, l_added, pool)); |
| |
| SVN_ERR(svn_mergeinfo_remove(&from_mergeinfo, l_deleted, |
| from_mergeinfo, pool)); |
| |
| return svn_mergeinfo_to_string((svn_string_t **)output, from_mergeinfo, pool); |
| } |
| |
| |
| svn_error_t * |
| svn_wc_merge_props(svn_wc_notify_state_t *state, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_hash_t *baseprops, |
| const apr_array_header_t *propchanges, |
| svn_boolean_t base_merge, |
| svn_boolean_t dry_run, |
| apr_pool_t *pool) |
| { |
| return svn_wc_merge_props2(state, path, adm_access, baseprops, propchanges, |
| base_merge, dry_run, NULL, NULL, pool); |
| } |
| |
| |
| svn_error_t * |
| svn_wc_merge_props2(svn_wc_notify_state_t *state, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_hash_t *baseprops, |
| const apr_array_header_t *propchanges, |
| svn_boolean_t base_merge, |
| svn_boolean_t dry_run, |
| svn_wc_conflict_resolver_func_t conflict_func, |
| void *conflict_baton, |
| apr_pool_t *pool) |
| { |
| const svn_wc_entry_t *entry; |
| svn_stringbuf_t *log_accum; |
| |
| /* IMPORTANT: svn_wc_merge_prop_diffs relies on the fact that baseprops |
| may be NULL. */ |
| |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, FALSE, pool)); |
| |
| /* Notice that we're not using svn_path_split_if_file(), because |
| that looks at the actual working file. Its existence shouldn't |
| matter, so we're looking at entry->kind instead. */ |
| switch (entry->kind) |
| { |
| case svn_node_dir: |
| case svn_node_file: |
| break; |
| default: |
| return SVN_NO_ERROR; /* ### svn_node_none or svn_node_unknown */ |
| } |
| |
| if (! dry_run) |
| log_accum = svn_stringbuf_create("", pool); |
| |
| /* Note that while this routine does the "real" work, it's only |
| prepping tempfiles and writing log commands. */ |
| SVN_ERR(svn_wc__merge_props(state, adm_access, path, baseprops, NULL, NULL, |
| propchanges, base_merge, dry_run, |
| conflict_func, conflict_baton, pool, &log_accum)); |
| |
| if (! dry_run) |
| { |
| SVN_ERR(svn_wc__write_log(adm_access, 0, log_accum, pool)); |
| SVN_ERR(svn_wc__run_log(adm_access, NULL, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| |
| /* Set the value of *STATE to NEW_VALUE if STATE is not NULL |
| * and NEW_VALUE is a higer order value than *STATE's current value |
| * using this ordering (lower order first): |
| * |
| * - unknown, unchanged, inapplicable |
| * - changed |
| * - merged |
| * - missing |
| * - obstructed |
| * - conflicted |
| * |
| */ |
| static void |
| set_prop_merge_state(svn_wc_notify_state_t *state, |
| svn_wc_notify_state_t new_value) |
| { |
| static char ordering[] = |
| { svn_wc_notify_state_unknown, |
| svn_wc_notify_state_unchanged, |
| svn_wc_notify_state_inapplicable, |
| svn_wc_notify_state_changed, |
| svn_wc_notify_state_merged, |
| svn_wc_notify_state_obstructed, |
| svn_wc_notify_state_conflicted }; |
| int state_pos = 0, i; |
| |
| if (! state) |
| return; |
| |
| /* Find *STATE in our ordering */ |
| for (i = 0; i < sizeof(ordering); i++) |
| { |
| if (*state == ordering[i]) |
| { |
| state_pos = i; |
| break; |
| } |
| } |
| |
| /* Find NEW_VALUE in our ordering |
| * We don't need to look further than where we found *STATE though: |
| * If we find our value, it's order is too low. |
| * If we don't find it, we'll want to set it, no matter its order. |
| */ |
| |
| for (i = 0; i <= state_pos; i++) |
| { |
| if (new_value == ordering[i]) |
| return; |
| } |
| |
| *state = new_value; |
| } |
| |
| /* Helper function for the three apply_* functions below, used when |
| * merging properties together. |
| * |
| * Given property PROPNAME on PATH, and four possible property values, |
| * generate four tmpfiles and pass them to CONFLICT_FUNC callback. |
| * This gives the client an opportunity to interactively resolve the |
| * property conflict. (ADM_ACCESS provides the ability to examine |
| * PATH's entries.) |
| * |
| * BASE_VAL/WORKING_VAL represent the current state of the working |
| * copy, and OLD_VAL/NEW_VAL represents the incoming propchange. Any |
| * of these values might be NULL, indicating either non-existence or |
| * intent-to-delete. |
| * |
| * If the callback isn't available, or if it responds with |
| * 'choose_postpone', then set *CONFLICT_REMAINS to true and return. |
| * |
| * If the callback responds with a choice of 'base', 'theirs', 'mine', |
| * or 'merged', then install the proper value into WORKING_PROPS and |
| * set *CONFLICT_REMAINS to false. |
| * |
| */ |
| static svn_error_t * |
| maybe_generate_propconflict(svn_boolean_t *conflict_remains, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| svn_boolean_t is_dir, |
| const char *propname, |
| apr_hash_t *working_props, |
| const svn_string_t *old_val, |
| const svn_string_t *new_val, |
| const svn_string_t *base_val, |
| const svn_string_t *working_val, |
| svn_wc_conflict_resolver_func_t conflict_func, |
| void *conflict_baton, |
| apr_pool_t *pool) |
| { |
| svn_wc_conflict_result_t *result = NULL; |
| svn_string_t *mime_propval = NULL; |
| apr_pool_t *filepool = svn_pool_create(pool); |
| svn_wc_conflict_description_t *cdesc; |
| |
| if (! conflict_func) |
| { |
| /* Just postpone the conflict. */ |
| *conflict_remains = TRUE; |
| return SVN_NO_ERROR; |
| } |
| |
| cdesc = svn_wc_conflict_description_create_prop( |
| path, adm_access, is_dir ? svn_node_dir : svn_node_file, propname, pool); |
| |
| /* Create a tmpfile for each of the string_t's we've got. */ |
| if (working_val) |
| SVN_ERR(svn_io_write_unique(&cdesc->my_file, path, working_val->data, |
| working_val->len, |
| svn_io_file_del_on_pool_cleanup, filepool)); |
| |
| if (new_val) |
| SVN_ERR(svn_io_write_unique(&cdesc->their_file, path, new_val->data, |
| new_val->len, svn_io_file_del_on_pool_cleanup, |
| filepool)); |
| |
| if (!base_val && !old_val) |
| { |
| /* If base and old are both NULL, then that's fine, we just let |
| base_file stay NULL as-is. Both agents are attempting to add a |
| new property. */ |
| } |
| |
| else if ((base_val && !old_val) |
| || (!base_val && old_val)) |
| { |
| /* If only one of base and old are defined, then we've got a |
| situation where one agent is attempting to add the property |
| for the first time, and the other agent is changing a |
| property it thinks already exists. In this case, we return |
| whichever older-value happens to be defined, so that the |
| conflict-callback can still attempt a 3-way merge. */ |
| |
| const svn_string_t *the_val = base_val ? base_val : old_val; |
| |
| SVN_ERR(svn_io_write_unique(&cdesc->base_file, path, the_val->data, |
| the_val->len, svn_io_file_del_on_pool_cleanup, |
| filepool)); |
| } |
| |
| else /* base and old are both non-NULL */ |
| { |
| const svn_string_t *the_val; |
| |
| if (! svn_string_compare(base_val, old_val)) |
| { |
| /* What happens if 'base' and 'old' don't match up? In an |
| ideal situation, they would. But if they don't, this is |
| a classic example of a patch 'hunk' failing to apply due |
| to a lack of context. For example: imagine that the user |
| is busy changing the property from a value of "cat" to |
| "dog", but the incoming propchange wants to change the |
| same property value from "red" to "green". Total context |
| mismatch. |
| |
| HOWEVER: we can still pass one of the two base values as |
| 'base_file' to the callback anyway. It's still useful to |
| present the working and new values to the user to |
| compare. */ |
| |
| if (working_val && svn_string_compare(base_val, working_val)) |
| the_val = old_val; |
| else |
| the_val = base_val; |
| } |
| else |
| { |
| the_val = base_val; |
| } |
| |
| SVN_ERR(svn_io_write_unique(&cdesc->base_file, path, the_val->data, |
| the_val->len, svn_io_file_del_on_pool_cleanup, |
| filepool)); |
| |
| if (working_val && new_val) |
| { |
| svn_stream_t *mergestream; |
| svn_diff_t *diff; |
| svn_diff_file_options_t *options = |
| svn_diff_file_options_create(filepool); |
| |
| SVN_ERR(svn_stream_open_unique(&mergestream, &cdesc->merged_file, |
| NULL, svn_io_file_del_on_pool_cleanup, |
| filepool, pool)); |
| SVN_ERR(svn_diff_mem_string_diff3(&diff, the_val, working_val, |
| new_val, options, filepool)); |
| SVN_ERR(svn_diff_mem_string_output_merge2 |
| (mergestream, diff, the_val, working_val, new_val, |
| NULL, NULL, NULL, NULL, |
| svn_diff_conflict_display_modified_latest, filepool)); |
| svn_stream_close(mergestream); |
| } |
| } |
| |
| /* Build the rest of the description object: */ |
| if (!is_dir && working_props) |
| mime_propval = apr_hash_get(working_props, SVN_PROP_MIME_TYPE, |
| APR_HASH_KEY_STRING); |
| cdesc->mime_type = mime_propval ? mime_propval->data : NULL; |
| cdesc->is_binary = mime_propval ? |
| svn_mime_type_is_binary(mime_propval->data) : FALSE; |
| |
| if (!old_val && new_val) |
| cdesc->action = svn_wc_conflict_action_add; |
| else if (old_val && !new_val) |
| cdesc->action = svn_wc_conflict_action_delete; |
| else |
| cdesc->action = svn_wc_conflict_action_edit; |
| |
| if (base_val && !working_val) |
| cdesc->reason = svn_wc_conflict_reason_deleted; |
| else if (!base_val && working_val) |
| cdesc->reason = svn_wc_conflict_reason_obstructed; |
| else |
| cdesc->reason = svn_wc_conflict_reason_edited; |
| |
| /* Invoke the interactive conflict callback. */ |
| SVN_ERR(conflict_func(&result, cdesc, conflict_baton, pool)); |
| if (result == NULL) |
| { |
| *conflict_remains = TRUE; |
| return svn_error_create(SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, |
| NULL, _("Conflict callback violated API:" |
| " returned no results.")); |
| } |
| |
| switch (result->choice) |
| { |
| default: |
| case svn_wc_conflict_choose_postpone: |
| { |
| *conflict_remains = TRUE; |
| break; |
| } |
| case svn_wc_conflict_choose_mine_full: |
| { |
| /* No need to change working_props; it already contains working_val */ |
| *conflict_remains = FALSE; |
| break; |
| } |
| /* I think _mine_full and _theirs_full are appropriate for prop |
| behavior as well as the text behavior. There should even be |
| analogous behaviors for _mine and _theirs when those are |
| ready, namely: fold in all non-conflicting prop changes, and |
| then choose _mine side or _theirs side for conflicting ones. */ |
| case svn_wc_conflict_choose_theirs_full: |
| { |
| apr_hash_set(working_props, propname, APR_HASH_KEY_STRING, new_val); |
| *conflict_remains = FALSE; |
| break; |
| } |
| case svn_wc_conflict_choose_base: |
| { |
| apr_hash_set(working_props, propname, APR_HASH_KEY_STRING, base_val); |
| *conflict_remains = FALSE; |
| break; |
| } |
| case svn_wc_conflict_choose_merged: |
| { |
| if (!cdesc->merged_file && !result->merged_file) |
| return svn_error_create |
| (SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE, |
| NULL, _("Conflict callback violated API:" |
| " returned no merged file.")); |
| else |
| { |
| svn_stringbuf_t *merged_stringbuf; |
| svn_string_t *merged_string; |
| |
| SVN_ERR(svn_stringbuf_from_file2(&merged_stringbuf, |
| result->merged_file ? |
| result->merged_file : |
| cdesc->merged_file, |
| pool)); |
| merged_string = svn_string_create_from_buf(merged_stringbuf, pool); |
| apr_hash_set(working_props, propname, |
| APR_HASH_KEY_STRING, merged_string); |
| *conflict_remains = FALSE; |
| } |
| break; |
| } |
| } |
| |
| /* Delete any tmpfiles we made. */ |
| svn_pool_destroy(filepool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Add the property with name PROPNAME to the set of WORKING_PROPS on |
| * PATH, setting *STATE or *CONFLICT according to merge outcomes. |
| * |
| * *STATE is an input and output parameter, its value is to be |
| * set using set_merge_prop_state(). |
| * |
| * BASE_VAL contains the working copy base property value |
| * |
| * NEW_VAL contains the value to be set. |
| * |
| * CONFLICT_FUNC/BATON is a callback to be called before declaring a |
| * property conflict; it gives the client a chance to resolve the |
| * conflict interactively. It uses ADM_ACCESS to possibly examine |
| * PATH's entries. |
| */ |
| static svn_error_t * |
| apply_single_prop_add(svn_wc_notify_state_t *state, |
| const char *path, |
| svn_boolean_t is_dir, |
| apr_hash_t *working_props, |
| svn_string_t **conflict, |
| const char *propname, |
| const svn_string_t *base_val, |
| const svn_string_t *new_val, |
| svn_wc_conflict_resolver_func_t conflict_func, |
| void *conflict_baton, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| |
| { |
| svn_boolean_t got_conflict = FALSE; |
| svn_string_t *working_val |
| = apr_hash_get(working_props, propname, APR_HASH_KEY_STRING); |
| |
| if (working_val) |
| { |
| /* the property already exists in working_props... */ |
| |
| if (svn_string_compare(working_val, new_val)) |
| /* The value we want is already there, so it's a merge. */ |
| set_prop_merge_state(state, svn_wc_notify_state_merged); |
| |
| else |
| { |
| /* The WC difference doesn't match the new value. |
| We only merge mergeinfo; other props conflict */ |
| if (strcmp(propname, SVN_PROP_MERGEINFO) == 0) |
| { |
| SVN_ERR(combine_mergeinfo_props(&new_val, working_val, |
| new_val, pool)); |
| apr_hash_set(working_props, propname, |
| APR_HASH_KEY_STRING, new_val); |
| set_prop_merge_state(state, svn_wc_notify_state_merged); |
| } |
| else |
| { |
| SVN_ERR(maybe_generate_propconflict(&got_conflict, path, |
| adm_access, is_dir, |
| propname, working_props, |
| NULL, new_val, |
| base_val, working_val, |
| conflict_func, conflict_baton, |
| pool)); |
| if (got_conflict) |
| *conflict = svn_string_createf |
| (pool, |
| _("Trying to add new property '%s' with value " |
| "'%s',\nbut property already exists with value '%s'."), |
| propname, new_val->data, working_val->data); |
| } |
| } |
| } |
| else if (base_val) |
| { |
| SVN_ERR(maybe_generate_propconflict(&got_conflict, path, adm_access, |
| is_dir, propname, |
| working_props, NULL, new_val, |
| base_val, NULL, |
| conflict_func, conflict_baton, pool)); |
| if (got_conflict) |
| *conflict = svn_string_createf |
| (pool, _("Trying to create property '%s' with value '%s',\n" |
| "but it has been locally deleted."), |
| propname, new_val->data); |
| } |
| else /* property doesn't yet exist in working_props... */ |
| /* so just set it */ |
| apr_hash_set(working_props, propname, APR_HASH_KEY_STRING, new_val); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Delete the property with name PROPNAME from the set of |
| * WORKING_PROPS on PATH, setting *STATE or *CONFLICT according to |
| * merge outcomes. |
| * |
| * *STATE is an input and output parameter, its value is to be |
| * set using set_merge_prop_state(). |
| * |
| * BASE_VAL contains the working copy base property value |
| * |
| * OLD_VAL contains the value the of the property the server |
| * thinks it's deleting. |
| * |
| * CONFLICT_FUNC/BATON is a callback to be called before declaring a |
| * property conflict; it gives the client a chance to resolve the |
| * conflict interactively. It uses ADM_ACCESS to possibly examine |
| * PATH's entries. |
| */ |
| static svn_error_t * |
| apply_single_prop_delete(svn_wc_notify_state_t *state, |
| const char *path, |
| svn_boolean_t is_dir, |
| apr_hash_t *working_props, |
| svn_string_t **conflict, |
| const char *propname, |
| const svn_string_t *base_val, |
| const svn_string_t *old_val, |
| svn_wc_conflict_resolver_func_t conflict_func, |
| void *conflict_baton, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| svn_boolean_t got_conflict = FALSE; |
| svn_string_t *working_val |
| = apr_hash_get(working_props, propname, APR_HASH_KEY_STRING); |
| |
| if (! base_val) |
| { |
| apr_hash_set(working_props, propname, APR_HASH_KEY_STRING, NULL); |
| if (old_val) |
| /* This is a merge, merging a delete into non-existent */ |
| set_prop_merge_state(state, svn_wc_notify_state_merged); |
| } |
| |
| else if (svn_string_compare(base_val, old_val)) |
| { |
| if (working_val) |
| { |
| if (svn_string_compare(working_val, old_val)) |
| /* they have the same values, so it's an update */ |
| apr_hash_set(working_props, propname, APR_HASH_KEY_STRING, NULL); |
| else |
| { |
| SVN_ERR(maybe_generate_propconflict(&got_conflict, path, |
| adm_access, is_dir, |
| propname, working_props, |
| old_val, NULL, |
| base_val, working_val, |
| conflict_func, conflict_baton, |
| pool)); |
| if (got_conflict) |
| *conflict = svn_string_createf |
| (pool, |
| _("Trying to delete property '%s' with value '%s'\n" |
| "but it has been modified from '%s' to '%s'."), |
| propname, old_val->data, |
| base_val->data, working_val->data); |
| } |
| } |
| else |
| /* The property is locally deleted, so it's a merge */ |
| set_prop_merge_state(state, svn_wc_notify_state_merged); |
| } |
| |
| else |
| { |
| SVN_ERR(maybe_generate_propconflict(&got_conflict, path, adm_access, |
| is_dir, propname, |
| working_props, old_val, NULL, |
| base_val, working_val, |
| conflict_func, conflict_baton, pool)); |
| if (got_conflict) |
| *conflict = svn_string_createf |
| (pool, |
| _("Trying to delete property '%s' with value '%s'\n" |
| "but the local value is '%s'."), |
| propname, base_val->data, working_val->data); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Merge a change to the mergeinfo property. The same as |
| apply_single_prop_change(), except that the PROPNAME is always |
| SVN_PROP_MERGEINFO. */ |
| /* ### This function is extracted straight from the previous all-in-one |
| version of apply_single_prop_change() by removing the code paths that |
| were not followed for this property, but with no attempt to rationalize |
| the remainder. */ |
| static svn_error_t * |
| apply_single_mergeinfo_prop_change(svn_wc_notify_state_t *state, |
| const char *path, |
| svn_boolean_t is_dir, |
| apr_hash_t *working_props, |
| svn_string_t **conflict, |
| const char *propname, |
| const svn_string_t *base_val, |
| const svn_string_t *old_val, |
| const svn_string_t *new_val, |
| svn_wc_conflict_resolver_func_t conflict_func, |
| void *conflict_baton, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| svn_boolean_t got_conflict = FALSE; |
| svn_string_t *working_val |
| = apr_hash_get(working_props, propname, APR_HASH_KEY_STRING); |
| |
| if ((working_val && ! base_val) |
| || (! working_val && base_val) |
| || (working_val && base_val |
| && !svn_string_compare(working_val, base_val))) |
| { |
| /* Locally changed property */ |
| if (working_val) |
| { |
| if (svn_string_compare(working_val, new_val)) |
| /* The new value equals the changed value: a no-op merge */ |
| set_prop_merge_state(state, svn_wc_notify_state_merged); |
| else |
| { |
| /* We have base, WC, and new values. Discover |
| deltas between base <-> WC, and base <-> |
| incoming. Combine those deltas, and apply |
| them to base to get the new value. */ |
| SVN_ERR(combine_forked_mergeinfo_props(&new_val, old_val, |
| working_val, |
| new_val, pool)); |
| apr_hash_set(working_props, propname, |
| APR_HASH_KEY_STRING, new_val); |
| set_prop_merge_state(state, svn_wc_notify_state_merged); |
| } |
| } |
| |
| else |
| { |
| /* There is a base_val but no working_val */ |
| SVN_ERR(maybe_generate_propconflict(&got_conflict, path, adm_access, |
| is_dir, propname, working_props, |
| old_val, new_val, |
| base_val, working_val, |
| conflict_func, conflict_baton, |
| pool)); |
| if (got_conflict) |
| *conflict = svn_string_createf |
| (pool, |
| _("Trying to change property '%s' from '%s' to '%s',\n" |
| "but it has been locally deleted."), |
| propname, old_val->data, new_val->data); |
| } |
| } |
| |
| else if (! working_val) /* means !working_val && !base_val due |
| to conditions above: no prop at all */ |
| { |
| /* Discover any mergeinfo additions in the |
| incoming value relative to the base, and |
| "combine" those with the empty WC value. */ |
| svn_mergeinfo_t deleted_mergeinfo, added_mergeinfo; |
| SVN_ERR(diff_mergeinfo_props(&deleted_mergeinfo, |
| &added_mergeinfo, |
| old_val, new_val, pool)); |
| SVN_ERR(svn_mergeinfo_to_string((svn_string_t **)&new_val, |
| added_mergeinfo, pool)); |
| apr_hash_set(working_props, propname, APR_HASH_KEY_STRING, new_val); |
| } |
| |
| else /* means working && base && svn_string_compare(working, base) */ |
| { |
| if (svn_string_compare(old_val, base_val)) |
| apr_hash_set(working_props, propname, APR_HASH_KEY_STRING, new_val); |
| |
| else |
| { |
| /* We have base, WC, and new values. Discover |
| deltas between base <-> WC, and base <-> |
| incoming. Combine those deltas, and apply |
| them to base to get the new value. */ |
| SVN_ERR(combine_forked_mergeinfo_props(&new_val, old_val, |
| working_val, |
| new_val, pool)); |
| apr_hash_set(working_props, propname, |
| APR_HASH_KEY_STRING, new_val); |
| set_prop_merge_state(state, svn_wc_notify_state_merged); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Merge a change to a property, using the rule that if the working value |
| is the same as OLD_VAL then apply the change as a simple update |
| (replacement), otherwise invoke maybe_generate_propconflict(). |
| The definition of the arguments and behaviour is the same as |
| apply_single_prop_change(). */ |
| static svn_error_t * |
| apply_single_generic_prop_change(svn_wc_notify_state_t *state, |
| const char *path, |
| svn_boolean_t is_dir, |
| apr_hash_t *working_props, |
| svn_string_t **conflict, |
| const char *propname, |
| const svn_string_t *base_val, |
| const svn_string_t *old_val, |
| const svn_string_t *new_val, |
| svn_wc_conflict_resolver_func_t conflict_func, |
| void *conflict_baton, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| svn_boolean_t got_conflict = FALSE; |
| svn_string_t *working_val |
| = apr_hash_get(working_props, propname, APR_HASH_KEY_STRING); |
| |
| /* If working_val is the same as old_val... */ |
| if ((!working_val && !old_val) |
| || (working_val && old_val |
| && svn_string_compare(working_val, old_val))) |
| { |
| /* A trivial update: change it to new_val. */ |
| apr_hash_set(working_props, propname, APR_HASH_KEY_STRING, new_val); |
| } |
| else |
| { |
| /* Merge the change. */ |
| SVN_ERR(maybe_generate_propconflict(&got_conflict, path, adm_access, |
| is_dir, propname, working_props, |
| old_val, new_val, |
| base_val, working_val, |
| conflict_func, conflict_baton, |
| pool)); |
| if (got_conflict) |
| { |
| /* Describe the conflict, referring to base_val as well as |
| working_val for the user's convenience. */ |
| if (working_val && base_val |
| && svn_string_compare(working_val, base_val)) |
| *conflict = svn_string_createf |
| (pool, |
| _("Trying to change property '%s' from '%s' to '%s',\n" |
| "but property already exists with value '%s'."), |
| propname, old_val->data, new_val->data, working_val->data); |
| else if (working_val && base_val) |
| *conflict = svn_string_createf |
| (pool, |
| _("Trying to change property '%s' from '%s' to '%s',\n" |
| "but the property has been locally changed from '%s' to " |
| "'%s'."), |
| propname, old_val->data, new_val->data, |
| base_val->data, working_val->data); |
| else if (working_val) |
| *conflict = svn_string_createf |
| (pool, |
| _("Trying to change property '%s' from '%s' to '%s',\n" |
| "but property has been locally added with value " |
| "'%s'."), |
| propname, old_val->data, new_val->data, working_val->data); |
| else if (base_val) |
| *conflict = svn_string_createf |
| (pool, |
| _("Trying to change property '%s' from '%s' to '%s',\n" |
| "but it has been locally deleted."), |
| propname, old_val->data, new_val->data); |
| else |
| *conflict = svn_string_createf |
| (pool, |
| _("Trying to change property '%s' from '%s' to '%s',\n" |
| "but the property does not exist."), |
| propname, old_val->data, new_val->data); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Change the property with name PROPNAME in the set of WORKING_PROPS |
| * on PATH, setting *STATE or *CONFLICT according to the merge outcome. |
| * |
| * *STATE is an input and output parameter, its value is to be |
| * set using set_prop_merge_state(). (May be null.). |
| * |
| * BASE_VAL contains the working copy base property value. (May be null.) |
| * |
| * OLD_VAL contains the value of the property the server |
| * thinks it's overwriting. (Not null.) |
| * |
| * NEW_VAL contains the value to be set. (Not null.) |
| * |
| * CONFLICT_FUNC/BATON is a callback to be called before declaring a |
| * property conflict; it gives the client a chance to resolve the |
| * conflict interactively. It uses ADM_ACCESS to possibly examine the |
| * path's entries. |
| */ |
| static svn_error_t * |
| apply_single_prop_change(svn_wc_notify_state_t *state, |
| const char *path, |
| svn_boolean_t is_dir, |
| apr_hash_t *working_props, |
| svn_string_t **conflict, |
| const char *propname, |
| const svn_string_t *base_val, |
| const svn_string_t *old_val, |
| const svn_string_t *new_val, |
| svn_wc_conflict_resolver_func_t conflict_func, |
| void *conflict_baton, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| /* Note: The purpose is to apply the change (old_val -> new_val) onto |
| (working_val). There is no need for base_val to be involved in the |
| process except as a bit of context to help the user understand and |
| resolve any conflict. */ |
| |
| /* Decide how to merge, based on whether we know anything special about |
| the property. */ |
| if (strcmp(propname, SVN_PROP_MERGEINFO) == 0) |
| { |
| /* We know how to merge any mergeinfo property change. */ |
| |
| SVN_ERR(apply_single_mergeinfo_prop_change(state, path, is_dir, |
| working_props, conflict, |
| propname, base_val, old_val, |
| new_val, conflict_func, |
| conflict_baton, adm_access, |
| pool)); |
| } |
| else |
| { |
| /* The standard method: perform a simple update automatically, but |
| pass any other kind of merge to maybe_generate_propconflict(). */ |
| |
| SVN_ERR(apply_single_generic_prop_change(state, path, is_dir, |
| working_props, conflict, |
| propname, base_val, old_val, |
| new_val, conflict_func, |
| conflict_baton, adm_access, |
| pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_wc__merge_props(svn_wc_notify_state_t *state, |
| svn_wc_adm_access_t *adm_access, |
| const char *path, |
| apr_hash_t *server_baseprops, |
| apr_hash_t *base_props, |
| apr_hash_t *working_props, |
| const apr_array_header_t *propchanges, |
| svn_boolean_t base_merge, |
| svn_boolean_t dry_run, |
| svn_wc_conflict_resolver_func_t conflict_func, |
| void *conflict_baton, |
| apr_pool_t *pool, |
| svn_stringbuf_t **entry_accum) |
| { |
| int i; |
| svn_boolean_t is_dir; |
| |
| const char *reject_path = NULL; |
| apr_file_t *reject_tmp_fp = NULL; /* the temporary conflicts file */ |
| const char *reject_tmp_path = NULL; |
| |
| if (! svn_path_is_child(svn_wc_adm_access_path(adm_access), path, NULL)) |
| is_dir = TRUE; |
| else |
| is_dir = FALSE; |
| |
| /* If not provided, load the base & working property files into hashes */ |
| if (! base_props || ! working_props) |
| SVN_ERR(svn_wc__load_props(base_props ? NULL : &base_props, |
| working_props ? NULL : &working_props, |
| NULL, adm_access, path, pool)); |
| if (!server_baseprops) |
| server_baseprops = base_props; |
| |
| if (state) |
| { |
| /* Start out assuming no changes or conflicts. Don't bother to |
| examine propchanges->nelts yet; even if we knew there were |
| propchanges, we wouldn't yet know if they are "normal" props, |
| as opposed wc or entry props. */ |
| *state = svn_wc_notify_state_unchanged; |
| } |
| |
| /* Looping over the array of incoming propchanges we want to apply: */ |
| for (i = 0; i < propchanges->nelts; i++) |
| { |
| const char *propname; |
| svn_string_t *conflict = NULL; |
| const svn_prop_t *incoming_change; |
| const svn_string_t *from_val, *to_val, *working_val, *base_val; |
| svn_boolean_t is_normal; |
| |
| /* For the incoming propchange, figure out the TO and FROM values. */ |
| incoming_change = &APR_ARRAY_IDX(propchanges, i, svn_prop_t); |
| propname = incoming_change->name; |
| is_normal = svn_wc_is_normal_prop(propname); |
| to_val = incoming_change->value |
| ? svn_string_dup(incoming_change->value, pool) : NULL; |
| from_val = apr_hash_get(server_baseprops, propname, APR_HASH_KEY_STRING); |
| |
| working_val = apr_hash_get(working_props, propname, APR_HASH_KEY_STRING); |
| base_val = apr_hash_get(base_props, propname, APR_HASH_KEY_STRING); |
| |
| if (base_merge) |
| apr_hash_set(base_props, propname, APR_HASH_KEY_STRING, to_val); |
| |
| /* We already know that state is at least `changed', so mark |
| that, but remember that we may later upgrade to `merged' or |
| even `conflicted'. */ |
| if (is_normal) |
| set_prop_merge_state(state, svn_wc_notify_state_changed); |
| |
| if (! from_val) /* adding a new property */ |
| SVN_ERR(apply_single_prop_add(is_normal ? state : NULL, path, is_dir, |
| working_props, &conflict, |
| propname, base_val, to_val, |
| conflict_func, conflict_baton, |
| adm_access, pool)); |
| |
| else if (! to_val) /* delete an existing property */ |
| SVN_ERR(apply_single_prop_delete(is_normal ? state : NULL, path, is_dir, |
| working_props, &conflict, |
| propname, base_val, from_val, |
| conflict_func, conflict_baton, |
| adm_access, pool)); |
| |
| else /* changing an existing property */ |
| SVN_ERR(apply_single_prop_change(is_normal ? state : NULL, path, is_dir, |
| working_props, &conflict, |
| propname, base_val, from_val, to_val, |
| conflict_func, conflict_baton, |
| adm_access, pool)); |
| |
| |
| /* merging logic complete, now we need to possibly log conflict |
| data to tmpfiles. */ |
| |
| if (conflict) |
| { |
| if (is_normal) |
| set_prop_merge_state(state, svn_wc_notify_state_conflicted); |
| |
| if (dry_run) |
| continue; /* skip to next incoming change */ |
| |
| if (! reject_tmp_fp) |
| /* This is the very first prop conflict found on this item. */ |
| SVN_ERR(open_reject_tmp_file(&reject_tmp_fp, &reject_tmp_path, |
| path, adm_access, is_dir, |
| pool)); |
| |
| /* Append the conflict to the open tmp/PROPS/---.prej file */ |
| SVN_ERR(append_prop_conflict(reject_tmp_fp, conflict, pool)); |
| } |
| |
| } /* foreach propchange ... */ |
| |
| /* Finished applying all incoming propchanges to our hashes! */ |
| |
| if (dry_run) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR(svn_wc__install_props(entry_accum, adm_access, path, |
| base_props, working_props, base_merge, |
| pool)); |
| |
| if (reject_tmp_fp) |
| { |
| /* There's a .prej file sitting in .svn/tmp/ somewhere. Deal |
| with the conflicts. */ |
| |
| /* First, _close_ this temporary conflicts file. We've been |
| appending to it all along. */ |
| SVN_ERR(svn_io_file_close(reject_tmp_fp, pool)); |
| |
| /* Now try to get the name of a pre-existing .prej file from the |
| entries file */ |
| SVN_ERR(get_existing_prop_reject_file(&reject_path, |
| adm_access, path, pool)); |
| |
| if (! reject_path) |
| { |
| /* Reserve a new .prej file *above* the .svn/ directory by |
| opening and closing it. */ |
| const char *full_reject_path; |
| |
| full_reject_path = (!is_dir) ? path : |
| svn_path_join(path, SVN_WC__THIS_DIR_PREJ, pool); |
| |
| SVN_ERR(svn_io_open_unique_file2(NULL, &reject_path, |
| full_reject_path, |
| SVN_WC__PROP_REJ_EXT, |
| svn_io_file_del_none, pool)); |
| |
| /* This file will be overwritten when the log is run; that's |
| ok, because at least now we have a reservation on |
| disk. */ |
| } |
| |
| /* We've now guaranteed that some kind of .prej file exists |
| above the .svn/ dir. We write log entries to append our |
| conflicts to it. */ |
| SVN_ERR(svn_wc__loggy_append(entry_accum, adm_access, |
| reject_tmp_path, reject_path, pool)); |
| |
| /* And of course, delete the temporary reject file. */ |
| SVN_ERR(svn_wc__loggy_remove(entry_accum, adm_access, |
| reject_tmp_path, pool)); |
| |
| /* Mark entry as "conflicted" with a particular .prej file. */ |
| { |
| svn_wc_entry_t entry; |
| |
| entry.prejfile = svn_path_is_child(svn_wc_adm_access_path(adm_access), |
| reject_path, NULL); |
| SVN_ERR(svn_wc__loggy_entry_modify(entry_accum, |
| adm_access, |
| path, |
| &entry, |
| SVN_WC__ENTRY_MODIFY_PREJFILE, |
| pool)); |
| } |
| |
| } /* if (reject_tmp_fp) */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* This is DEPRECATED, use svn_wc_merge_props2() instead. */ |
| svn_error_t * |
| svn_wc_merge_prop_diffs(svn_wc_notify_state_t *state, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| const apr_array_header_t *propchanges, |
| svn_boolean_t base_merge, |
| svn_boolean_t dry_run, |
| apr_pool_t *pool) |
| { |
| /* NOTE: Here, we use implementation knowledge. The public |
| svn_wc_merge_props2 doesn't allow NULL as baseprops argument, but we know |
| that it works. */ |
| return svn_wc_merge_props2(state, path, adm_access, NULL, propchanges, |
| base_merge, dry_run, NULL, NULL, pool); |
| } |
| |
| |
| |
| /*------------------------------------------------------------------*/ |
| |
| /*** Private 'wc prop' functions ***/ |
| |
| |
| svn_error_t * |
| svn_wc__wcprop_list(apr_hash_t **wcprops, |
| const char *entryname, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| const char *prop_path; |
| const svn_wc_entry_t *entry; |
| apr_hash_t *all_wcprops; |
| apr_pool_t *cache_pool = svn_wc_adm_access_pool(adm_access); |
| const char *path = svn_path_join(svn_wc_adm_access_path(adm_access), |
| entryname, pool); |
| |
| SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, pool)); |
| if (! entry) |
| { |
| /* No entry exists, therefore no wcprop-file can exist */ |
| *wcprops = apr_hash_make(pool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Try the cache first. */ |
| all_wcprops = svn_wc__adm_access_wcprops(adm_access); |
| if (! all_wcprops) |
| { |
| SVN_ERR(read_wcprops(adm_access, pool)); |
| all_wcprops = svn_wc__adm_access_wcprops(adm_access); |
| } |
| if (all_wcprops) |
| { |
| *wcprops = apr_hash_get(all_wcprops, entryname, APR_HASH_KEY_STRING); |
| /* The cache contains no hash tables for empty proplist, so we just |
| create one here if that's the case. */ |
| if (! *wcprops) |
| { |
| *wcprops = apr_hash_make(cache_pool); |
| entryname = apr_pstrdup(cache_pool, entryname); |
| apr_hash_set(all_wcprops, entryname, APR_HASH_KEY_STRING, *wcprops); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| /* Fall back on individual files for backwards compatibility. */ |
| |
| /* Construct a path to the relevant property file */ |
| SVN_ERR(svn_wc__prop_path(&prop_path, path, entry->kind, |
| svn_wc__props_wcprop, FALSE, pool)); |
| *wcprops = apr_hash_make(pool); |
| return load_prop_file(prop_path, *wcprops, pool); |
| } |
| |
| |
| /* Get a single 'wcprop' NAME for versioned object PATH, return in |
| *VALUE. ADM_ACCESS is an access baton set that contains PATH. */ |
| static svn_error_t * |
| wcprop_get(const svn_string_t **value, |
| const char *name, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *prophash; |
| const svn_wc_entry_t *entry; |
| |
| SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, pool)); |
| if (! entry) |
| { |
| *value = NULL; |
| return SVN_NO_ERROR; |
| } |
| if (entry->kind == svn_node_dir) |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, path, pool)); |
| else |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, |
| svn_path_dirname(path, pool), pool)); |
| |
| SVN_ERR_W(svn_wc__wcprop_list(&prophash, entry->name, adm_access, pool), |
| _("Failed to load properties from disk")); |
| |
| *value = apr_hash_get(prophash, name, APR_HASH_KEY_STRING); |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_wc__wcprop_set(const char *name, |
| const svn_string_t *value, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| svn_boolean_t force_write, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *prophash; |
| apr_pool_t *cache_pool = svn_wc_adm_access_pool(adm_access); |
| const svn_wc_entry_t *entry; |
| |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, FALSE, pool)); |
| |
| if (entry->kind == svn_node_dir) |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, path, pool)); |
| else |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, |
| svn_path_dirname(path, pool), pool)); |
| SVN_ERR_W(svn_wc__wcprop_list(&prophash, entry->name, adm_access, pool), |
| _("Failed to load properties from disk")); |
| |
| /* Now we have all the properties in our hash. Simply merge the new |
| property into it. */ |
| name = apr_pstrdup(cache_pool, name); |
| if (value) |
| value = svn_string_dup(value, cache_pool); |
| apr_hash_set(prophash, name, APR_HASH_KEY_STRING, value); |
| |
| if (svn_wc__adm_wc_format(adm_access) > SVN_WC__WCPROPS_MANY_FILES_VERSION) |
| { |
| if (force_write) |
| SVN_ERR(write_wcprops(adm_access, pool)); |
| } |
| else |
| { |
| SVN_ERR(svn_wc__write_old_wcprops(path, prophash, entry->kind, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /*------------------------------------------------------------------*/ |
| |
| |
| /*** Public Functions ***/ |
| |
| |
| svn_error_t * |
| svn_wc_prop_list(apr_hash_t **props, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| const svn_wc_entry_t *entry; |
| |
| SVN_ERR(svn_wc_entry(&entry, path, adm_access, TRUE, pool)); |
| |
| /* if there is no entry, 'path' is not under version control and |
| therefore has no props */ |
| if (! entry) |
| { |
| *props = apr_hash_make(pool); |
| return SVN_NO_ERROR; |
| } |
| |
| if (entry->kind == svn_node_dir) |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, path, pool)); |
| else |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, |
| svn_path_dirname(path, pool), pool)); |
| |
| return svn_wc__load_props(NULL, props, NULL, adm_access, path, pool); |
| } |
| |
| /* Determine if PROPNAME is contained in the list of space separated |
| values STRING. */ |
| |
| static svn_boolean_t |
| string_contains_prop(const char *string, const char *propname) |
| { |
| const char *place = strstr(string, propname); |
| int proplen = strlen(propname); |
| |
| if (!place) |
| return FALSE; |
| |
| while (place) |
| { |
| if (place[proplen] == ' ' || place[proplen] == 0) |
| return TRUE; |
| place = strstr(place + 1, propname); |
| } |
| return FALSE; |
| } |
| |
| svn_error_t * |
| svn_wc_prop_get(const svn_string_t **value, |
| const char *name, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *prophash; |
| enum svn_prop_kind kind = svn_property_kind(NULL, name); |
| const svn_wc_entry_t *entry; |
| |
| SVN_ERR(svn_wc_entry(&entry, path, adm_access, TRUE, pool)); |
| |
| if (entry == NULL) |
| { |
| *value = NULL; |
| return SVN_NO_ERROR; |
| } |
| |
| if (entry->cachable_props |
| && string_contains_prop(entry->cachable_props, name)) |
| { |
| /* We separate these two cases so that we can return the correct |
| value for booleans if they exist in the string. */ |
| if (!entry->present_props |
| || !string_contains_prop(entry->present_props, name)) |
| { |
| *value = NULL; |
| return SVN_NO_ERROR; |
| } |
| if (svn_prop_is_boolean(name)) |
| { |
| *value = svn_string_create(SVN_PROP_BOOLEAN_TRUE, pool); |
| SVN_ERR_ASSERT(*value != NULL); |
| return SVN_NO_ERROR; |
| } |
| } |
| |
| if (kind == svn_prop_wc_kind) |
| { |
| return wcprop_get(value, name, path, adm_access, pool); |
| } |
| if (kind == svn_prop_entry_kind) |
| { |
| return svn_error_createf /* we don't do entry properties here */ |
| (SVN_ERR_BAD_PROP_KIND, NULL, |
| _("Property '%s' is an entry property"), name); |
| } |
| else /* regular prop */ |
| { |
| SVN_ERR_W(svn_wc_prop_list(&prophash, path, adm_access, pool), |
| _("Failed to load properties from disk")); |
| |
| *value = apr_hash_get(prophash, name, APR_HASH_KEY_STRING); |
| |
| return SVN_NO_ERROR; |
| } |
| } |
| |
| |
| /* The special Subversion properties are not valid for all node kinds. |
| Return an error if NAME is an invalid Subversion property for PATH which |
| is of kind NODE_KIND. */ |
| static svn_error_t * |
| validate_prop_against_node_kind(const char *name, |
| const char *path, |
| svn_node_kind_t node_kind, |
| apr_pool_t *pool) |
| { |
| |
| const char *file_prohibit[] = { SVN_PROP_IGNORE, |
| SVN_PROP_EXTERNALS, |
| NULL }; |
| const char *dir_prohibit[] = { SVN_PROP_EXECUTABLE, |
| SVN_PROP_KEYWORDS, |
| SVN_PROP_EOL_STYLE, |
| SVN_PROP_MIME_TYPE, |
| SVN_PROP_NEEDS_LOCK, |
| NULL }; |
| const char **node_kind_prohibit; |
| const char *path_display |
| = svn_path_is_url(path) ? path : svn_path_local_style(path, pool); |
| |
| switch (node_kind) |
| { |
| case svn_node_dir: |
| node_kind_prohibit = dir_prohibit; |
| while (*node_kind_prohibit) |
| if (strcmp(name, *node_kind_prohibit++) == 0) |
| return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("Cannot set '%s' on a directory ('%s')"), |
| name, path_display); |
| break; |
| case svn_node_file: |
| node_kind_prohibit = file_prohibit; |
| while (*node_kind_prohibit) |
| if (strcmp(name, *node_kind_prohibit++) == 0) |
| return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("Cannot set '%s' on a file ('%s')"), |
| name, |
| path_display); |
| break; |
| default: |
| return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, |
| _("'%s' is not a file or directory"), |
| path_display); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| struct getter_baton { |
| const char *path; |
| svn_wc_adm_access_t *adm_access; |
| }; |
| |
| |
| static svn_error_t * |
| get_file_for_validation(const svn_string_t **mime_type, |
| svn_stream_t *stream, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| struct getter_baton *gb = baton; |
| |
| if (mime_type) |
| SVN_ERR(svn_wc_prop_get(mime_type, SVN_PROP_MIME_TYPE, |
| gb->path, gb->adm_access, pool)); |
| |
| if (stream) |
| { |
| svn_stream_t *read_stream; |
| |
| /* Open PATH. */ |
| SVN_ERR(svn_stream_open_readonly(&read_stream, gb->path, |
| pool, pool)); |
| |
| /* Copy from the file into the translating stream. */ |
| SVN_ERR(svn_stream_copy3(read_stream, svn_stream_disown(stream, pool), |
| NULL, NULL, pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static svn_error_t * |
| validate_eol_prop_against_file(const char *path, |
| svn_wc_canonicalize_svn_prop_get_file_t getter, |
| void *getter_baton, |
| apr_pool_t *pool) |
| { |
| svn_stream_t *translating_stream; |
| svn_error_t *err; |
| const svn_string_t *mime_type; |
| const char *path_display |
| = svn_path_is_url(path) ? path : svn_path_local_style(path, pool); |
| |
| /* First just ask the "getter" for the MIME type. */ |
| SVN_ERR(getter(&mime_type, NULL, getter_baton, pool)); |
| |
| /* See if this file has been determined to be binary. */ |
| if (mime_type && svn_mime_type_is_binary(mime_type->data)) |
| return svn_error_createf |
| (SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("File '%s' has binary mime type property"), |
| path_display); |
| |
| /* Now ask the getter for the contents of the file; this will do a |
| newline translation. All we really care about here is whether or |
| not the function fails on inconsistent line endings. The |
| function is "translating" to an empty stream. This is |
| sneeeeeeeeeeeaky. */ |
| translating_stream = svn_subst_stream_translated(svn_stream_empty(pool), |
| "", FALSE, NULL, FALSE, |
| pool); |
| |
| err = getter(NULL, translating_stream, getter_baton, pool); |
| |
| if (!err) |
| err = svn_stream_close(translating_stream); |
| |
| if (err && err->apr_err == SVN_ERR_IO_INCONSISTENT_EOL) |
| return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, err, |
| _("File '%s' has inconsistent newlines"), |
| path_display); |
| else if (err) |
| return err; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_wc_prop_set2(const char *name, |
| const svn_string_t *value, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| svn_boolean_t skip_checks, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *prophash, *base_prophash; |
| enum svn_prop_kind prop_kind = svn_property_kind(NULL, name); |
| svn_stringbuf_t *log_accum = svn_stringbuf_create("", pool); |
| const svn_wc_entry_t *entry; |
| |
| if (prop_kind == svn_prop_wc_kind) |
| return svn_wc__wcprop_set(name, value, path, adm_access, TRUE, pool); |
| else if (prop_kind == svn_prop_entry_kind) |
| return svn_error_createf /* we don't do entry properties here */ |
| (SVN_ERR_BAD_PROP_KIND, NULL, |
| _("Property '%s' is an entry property"), name); |
| |
| /* Else, handle a regular property: */ |
| |
| /* Get the entry and name for this path. */ |
| SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, FALSE, pool)); |
| |
| /* Get the access baton for the entry's directory. */ |
| if (entry->kind == svn_node_dir) |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, path, pool)); |
| else |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, |
| svn_path_dirname(path, pool), pool)); |
| |
| /* Setting an inappropriate property is not allowed (unless |
| overridden by 'skip_checks', in some circumstances). Deleting an |
| inappropriate property is allowed, however, since older clients |
| allowed (and other clients possibly still allow) setting it in |
| the first place. */ |
| if (value && svn_prop_is_svn_prop(name)) |
| { |
| const svn_string_t *new_value; |
| struct getter_baton *gb = apr_pcalloc(pool, sizeof(*gb)); |
| |
| gb->path = path; |
| gb->adm_access = adm_access; |
| |
| SVN_ERR(svn_wc_canonicalize_svn_prop(&new_value, name, value, path, |
| entry->kind, skip_checks, |
| get_file_for_validation, gb, pool)); |
| value = new_value; |
| } |
| |
| if (entry->kind == svn_node_file && strcmp(name, SVN_PROP_EXECUTABLE) == 0) |
| { |
| /* If the svn:executable property was set, then chmod +x. |
| If the svn:executable property was deleted (NULL value passed |
| in), then chmod -x. */ |
| if (value == NULL) |
| SVN_ERR(svn_io_set_file_executable(path, FALSE, TRUE, pool)); |
| else |
| SVN_ERR(svn_io_set_file_executable(path, TRUE, TRUE, pool)); |
| } |
| |
| if (entry->kind == svn_node_file && strcmp(name, SVN_PROP_NEEDS_LOCK) == 0) |
| { |
| /* If the svn:needs-lock property was set to NULL, set the file |
| to read-write */ |
| if (value == NULL) |
| SVN_ERR(svn_io_set_file_read_write(path, FALSE, pool)); |
| |
| /* If not, we'll set the file to read-only at commit time. */ |
| } |
| |
| SVN_ERR_W(svn_wc__load_props(&base_prophash, &prophash, NULL, |
| adm_access, path, pool), |
| _("Failed to load properties from disk")); |
| |
| /* If we're changing this file's list of expanded keywords, then |
| * we'll need to invalidate its text timestamp, since keyword |
| * expansion affects the comparison of working file to text base. |
| * |
| * Here we retrieve the old list of expanded keywords; after the |
| * property is set, we'll grab the new list and see if it differs |
| * from the old one. |
| */ |
| if (entry->kind == svn_node_file && strcmp(name, SVN_PROP_KEYWORDS) == 0) |
| { |
| svn_string_t *old_value = apr_hash_get(prophash, SVN_PROP_KEYWORDS, |
| APR_HASH_KEY_STRING); |
| apr_hash_t *old_keywords, *new_keywords; |
| |
| SVN_ERR(svn_wc__get_keywords(&old_keywords, path, adm_access, |
| old_value ? old_value->data : "", pool)); |
| SVN_ERR(svn_wc__get_keywords(&new_keywords, path, adm_access, |
| value ? value->data : "", pool)); |
| |
| if (svn_subst_keywords_differ2(old_keywords, new_keywords, FALSE, pool)) |
| { |
| svn_wc_entry_t tmp_entry; |
| |
| /* If we changed the keywords or newlines, void the entry |
| timestamp for this file, so svn_wc_text_modified_p() does |
| a real (albeit slow) check later on. */ |
| tmp_entry.kind = svn_node_file; |
| tmp_entry.text_time = 0; |
| SVN_ERR(svn_wc__loggy_entry_modify(&log_accum, adm_access, |
| path, &tmp_entry, |
| SVN_WC__ENTRY_MODIFY_TEXT_TIME, |
| pool)); |
| } |
| } |
| |
| /* Now we have all the properties in our hash. Simply merge the new |
| property into it. */ |
| apr_hash_set(prophash, name, APR_HASH_KEY_STRING, value); |
| |
| SVN_ERR(svn_wc__install_props(&log_accum, adm_access, path, |
| base_prophash, prophash, FALSE, pool)); |
| SVN_ERR(svn_wc__write_log(adm_access, 0, log_accum, pool)); |
| return svn_wc__run_log(adm_access, NULL, pool); |
| } |
| |
| |
| svn_error_t * |
| svn_wc_prop_set(const char *name, |
| const svn_string_t *value, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| return svn_wc_prop_set2(name, value, path, adm_access, FALSE, pool); |
| } |
| |
| |
| svn_error_t * |
| svn_wc_canonicalize_svn_prop(const svn_string_t **propval_p, |
| const char *propname, |
| const svn_string_t *propval, |
| const char *path, |
| svn_node_kind_t kind, |
| svn_boolean_t skip_some_checks, |
| svn_wc_canonicalize_svn_prop_get_file_t getter, |
| void *getter_baton, |
| apr_pool_t *pool) |
| { |
| svn_stringbuf_t *new_value = NULL; |
| |
| /* Keep this static, it may get stored (for read-only purposes) in a |
| hash that outlives this function. */ |
| static const svn_string_t boolean_value = |
| { |
| SVN_PROP_BOOLEAN_TRUE, |
| sizeof(SVN_PROP_BOOLEAN_TRUE) - 1 |
| }; |
| |
| SVN_ERR(validate_prop_against_node_kind(propname, path, kind, pool)); |
| |
| if (!skip_some_checks && (strcmp(propname, SVN_PROP_EOL_STYLE) == 0)) |
| { |
| svn_subst_eol_style_t eol_style; |
| const char *ignored_eol; |
| new_value = svn_stringbuf_create_from_string(propval, pool); |
| svn_stringbuf_strip_whitespace(new_value); |
| svn_subst_eol_style_from_value(&eol_style, &ignored_eol, new_value->data); |
| if (eol_style == svn_subst_eol_style_unknown) |
| return svn_error_createf(SVN_ERR_IO_UNKNOWN_EOL, NULL, |
| _("Unrecognized line ending style for '%s'"), |
| svn_path_local_style(path, pool)); |
| SVN_ERR(validate_eol_prop_against_file(path, getter, getter_baton, |
| pool)); |
| } |
| else if (!skip_some_checks && (strcmp(propname, SVN_PROP_MIME_TYPE) == 0)) |
| { |
| new_value = svn_stringbuf_create_from_string(propval, pool); |
| svn_stringbuf_strip_whitespace(new_value); |
| SVN_ERR(svn_mime_type_validate(new_value->data, pool)); |
| } |
| else if (strcmp(propname, SVN_PROP_IGNORE) == 0 |
| || strcmp(propname, SVN_PROP_EXTERNALS) == 0) |
| { |
| /* Make sure that the last line ends in a newline */ |
| if (propval->data[propval->len - 1] != '\n') |
| { |
| new_value = svn_stringbuf_create_from_string(propval, pool); |
| svn_stringbuf_appendbytes(new_value, "\n", 1); |
| } |
| |
| /* Make sure this is a valid externals property. Do not |
| allow 'skip_some_checks' to override, as there is no circumstance in |
| which this is proper (because there is no circumstance in |
| which Subversion can handle it). */ |
| if (strcmp(propname, SVN_PROP_EXTERNALS) == 0) |
| { |
| /* We don't allow "." nor ".." as target directories in |
| an svn:externals line. As it happens, our parse code |
| checks for this, so all we have to is invoke it -- |
| we're not interested in the parsed result, only in |
| whether or the parsing errored. */ |
| SVN_ERR(svn_wc_parse_externals_description3 |
| (NULL, path, propval->data, FALSE, pool)); |
| } |
| } |
| else if (strcmp(propname, SVN_PROP_KEYWORDS) == 0) |
| { |
| new_value = svn_stringbuf_create_from_string(propval, pool); |
| svn_stringbuf_strip_whitespace(new_value); |
| } |
| else if (strcmp(propname, SVN_PROP_EXECUTABLE) == 0 |
| || strcmp(propname, SVN_PROP_NEEDS_LOCK) == 0) |
| { |
| new_value = svn_stringbuf_create_from_string(&boolean_value, pool); |
| } |
| else if (strcmp(propname, SVN_PROP_MERGEINFO) == 0) |
| { |
| apr_hash_t *mergeinfo; |
| SVN_ERR(svn_mergeinfo_parse(&mergeinfo, propval->data, pool)); |
| } |
| |
| if (new_value) |
| *propval_p = svn_string_create_from_buf(new_value, pool); |
| else |
| *propval_p = propval; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_boolean_t |
| svn_wc_is_normal_prop(const char *name) |
| { |
| enum svn_prop_kind kind = svn_property_kind(NULL, name); |
| return (kind == svn_prop_regular_kind); |
| } |
| |
| |
| svn_boolean_t |
| svn_wc_is_wc_prop(const char *name) |
| { |
| enum svn_prop_kind kind = svn_property_kind(NULL, name); |
| return (kind == svn_prop_wc_kind); |
| } |
| |
| |
| svn_boolean_t |
| svn_wc_is_entry_prop(const char *name) |
| { |
| enum svn_prop_kind kind = svn_property_kind(NULL, name); |
| return (kind == svn_prop_entry_kind); |
| } |
| |
| |
| /* Helper to optimize svn_wc_props_modified_p(). |
| |
| If PATH_TO_PROP_FILE is nonexistent, is empty, or is of size 4 bytes |
| ("END\n"), then set EMPTY_P to true. Otherwise set EMPTY_P to false, |
| which means that the file must contain real properties. */ |
| static svn_error_t * |
| empty_props_p(svn_boolean_t *empty_p, |
| const char *path_to_prop_file, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| apr_finfo_t finfo; |
| |
| err = svn_io_stat(&finfo, path_to_prop_file, APR_FINFO_MIN | APR_FINFO_TYPE, |
| pool); |
| if (err) |
| { |
| if (! APR_STATUS_IS_ENOENT(err->apr_err) |
| && ! APR_STATUS_IS_ENOTDIR(err->apr_err)) |
| return err; |
| |
| /* nonexistent */ |
| svn_error_clear(err); |
| *empty_p = TRUE; |
| } |
| else |
| { |
| |
| |
| /* If we remove props from a propfile, eventually the file will |
| be empty, or, for working copies written by pre-1.3 libraries, will |
| contain nothing but "END\n" */ |
| if (finfo.filetype == APR_REG && (finfo.size == 4 || finfo.size == 0)) |
| *empty_p = TRUE; |
| else |
| *empty_p = FALSE; |
| |
| /* If the size is between 1 and 4, then something is corrupt. |
| If the size is between 4 and 16, then something is corrupt, |
| because 16 is the -smallest- the file can possibly be if it |
| contained only one property. So long as we say it is "not |
| empty", we will discover such corruption later when we try |
| to read the properties from the file. */ |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Simple wrapper around empty_props_p, and inversed. */ |
| svn_error_t * |
| svn_wc__has_props(svn_boolean_t *has_props, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| svn_boolean_t is_empty; |
| const char *prop_path; |
| const svn_wc_entry_t *entry; |
| svn_boolean_t has_propcaching = |
| svn_wc__adm_wc_format(adm_access) > SVN_WC__NO_PROPCACHING_VERSION; |
| |
| SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, pool)); |
| |
| /*### Maybe assert (entry); calling svn_wc__has_props |
| for an unversioned path is bogus */ |
| if (! entry) |
| { |
| *has_props = FALSE; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Use the flag in the entry if the WC is recent enough. */ |
| if (has_propcaching) |
| { |
| *has_props = entry->has_props; |
| return SVN_NO_ERROR; |
| } |
| |
| /* The rest is for compatibility with WCs that don't have propcaching. */ |
| |
| SVN_ERR(svn_wc__prop_path(&prop_path, path, entry->kind, |
| svn_wc__props_working, FALSE, pool)); |
| SVN_ERR(empty_props_p(&is_empty, prop_path, pool)); |
| |
| if (is_empty) |
| *has_props = FALSE; |
| else |
| *has_props = TRUE; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Common implementation for svn_wc_props_modified_p() |
| and svn_wc__has_prop_mods(). |
| |
| Set *MODIFIED_P to true if PATH's properties are modified |
| with regard to the base revision, else set MODIFIED_P to false. |
| |
| If WHICH_PROPS is non-null and there are prop mods then set |
| *WHICH_PROPS to a (const char *propname) -> |
| (const svn_string_t *propvalue) key:value mapping of only |
| the modified properties. */ |
| static svn_error_t * |
| modified_props(svn_boolean_t *modified_p, |
| const char *path, |
| apr_hash_t **which_props, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| const char *prop_path; |
| const char *prop_base_path; |
| const svn_wc_entry_t *entry; |
| apr_pool_t *subpool = svn_pool_create(pool); |
| int wc_format = svn_wc__adm_wc_format(adm_access); |
| svn_boolean_t want_props = which_props ? TRUE : FALSE; |
| |
| if (want_props) |
| *which_props = apr_hash_make(pool); |
| |
| SVN_ERR(svn_wc_entry(&entry, path, adm_access, TRUE, subpool)); |
| |
| /* If we have no entry, we can't have any prop mods. */ |
| if (! entry) |
| { |
| *modified_p = FALSE; |
| goto cleanup; |
| } |
| |
| /* For newer WCs, if there is an entry for the path, we have a fast |
| * and nice way to retrieve the information from the entry. */ |
| if (wc_format > SVN_WC__NO_PROPCACHING_VERSION) |
| { |
| /* Only continue if there are prop mods |
| and we want to know the details. */ |
| *modified_p = entry->has_prop_mods; |
| if (!*modified_p || !want_props) |
| goto cleanup; |
| } |
| |
| /* So, we have a WC in an older format or we have propcaching |
| but need to find the specific prop changes. Either way we |
| have some work to do... */ |
| |
| /* First, get the paths of the working and 'base' prop files. */ |
| SVN_ERR(svn_wc__prop_path(&prop_path, path, entry->kind, |
| svn_wc__props_working, FALSE, subpool)); |
| SVN_ERR(svn_wc__prop_path(&prop_base_path, path, entry->kind, |
| svn_wc__props_base, FALSE, subpool)); |
| |
| /* Check for numerous easy outs on older WC formats before we |
| resort to svn_prop_diffs(). */ |
| if (wc_format <= SVN_WC__NO_PROPCACHING_VERSION) |
| { |
| svn_boolean_t bempty, wempty; |
| /* Decide if either path is "empty" of properties. */ |
| SVN_ERR(empty_props_p(&wempty, prop_path, subpool)); |
| SVN_ERR(empty_props_p(&bempty, prop_base_path, subpool)); |
| |
| /* If something is scheduled for replacement, we do *not* want to |
| pay attention to any base-props; they might be residual from the |
| old deleted file. */ |
| if (entry->schedule == svn_wc_schedule_replace) |
| { |
| *modified_p = wempty ? FALSE : TRUE; |
| |
| /* Only continue if there are prop mods |
| and we want to know the details. */ |
| if (!*modified_p || !want_props) |
| goto cleanup; |
| } |
| |
| /* Easy out: if the base file is empty, we know the answer |
| immediately. */ |
| if (bempty) |
| { |
| if (! wempty) |
| { |
| /* base is empty, but working is not */ |
| *modified_p = TRUE; |
| |
| /* Only continue if we want to know the details. */ |
| if (!want_props) |
| goto cleanup; |
| } |
| else |
| { |
| /* base and working are both empty */ |
| *modified_p = FALSE; |
| goto cleanup; |
| } |
| } |
| /* OK, so the base file is non-empty. One more easy out: */ |
| else if (wempty) |
| { |
| /* base exists, working is empty */ |
| *modified_p = TRUE; |
| |
| /* Only continue if we want to know the details. */ |
| if (!want_props) |
| goto cleanup; |
| } |
| else |
| { |
| svn_boolean_t different_filesizes; |
| |
| /* At this point, we know both files exists. Therefore we have no |
| choice but to start checking their contents. */ |
| |
| /* There are at least three tests we can try in succession. */ |
| |
| /* Easy-answer attempt #1: (### this stat's the files again) */ |
| |
| /* Check if the local and prop-base file have *definitely* |
| different filesizes. */ |
| SVN_ERR(svn_io_filesizes_different_p(&different_filesizes, |
| prop_path, |
| prop_base_path, |
| subpool)); |
| if (different_filesizes) |
| { |
| *modified_p = TRUE; |
| |
| /* Only continue if we want to know the details. */ |
| if (!want_props) |
| goto cleanup; |
| } |
| else |
| { |
| svn_boolean_t equal_timestamps; |
| |
| /* Easy-answer attempt #2: (### this stat's the files again) */ |
| |
| /* See if the local file's prop timestamp is the same as the |
| one recorded in the administrative directory. */ |
| SVN_ERR(svn_wc__timestamps_equal_p(&equal_timestamps, path, |
| adm_access, |
| svn_wc__prop_time, |
| subpool)); |
| if (equal_timestamps) |
| { |
| *modified_p = FALSE; |
| goto cleanup; |
| } |
| } |
| } |
| } /* wc_format <= SVN_WC__NO_PROPCACHING_VERSION */ |
| |
| /* If we get here, then we either known we have prop changes and want |
| the specific changed props or we have a pre-propcaching WC version |
| and still haven't figured out if we even have changes. Regardless, |
| our approach is the same in both cases. |
| |
| In the pre-propcaching case: |
| |
| We know that the filesizes are the same, |
| but the timestamps are different. That's still not enough |
| evidence to make a correct decision; we need to look at the |
| files' contents directly. |
| |
| However, doing a byte-for-byte comparison won't work. The two |
| properties files may have the *exact* same name/value pairs, but |
| arranged in a different order. (Our hashdump format makes no |
| guarantees about ordering.) |
| |
| Therefore, rather than use contents_identical_p(), we use |
| svn_prop_diffs(). */ |
| { |
| apr_array_header_t *local_propchanges; |
| apr_hash_t *localprops = apr_hash_make(subpool); |
| apr_hash_t *baseprops = apr_hash_make(subpool); |
| |
| /* ### Amazingly, this stats the files again! */ |
| SVN_ERR(load_prop_file(prop_path, localprops, subpool)); |
| SVN_ERR(load_prop_file(prop_base_path, baseprops, subpool)); |
| |
| /* Don't use the subpool is we are hanging on to the changed props. */ |
| SVN_ERR(svn_prop_diffs(&local_propchanges, localprops, |
| baseprops, |
| want_props ? pool : subpool)); |
| |
| if (local_propchanges->nelts == 0) |
| { |
| *modified_p = FALSE; |
| } |
| else |
| { |
| *modified_p = TRUE; |
| |
| /* Record the changed props if that's what we want. */ |
| if (want_props) |
| { |
| int i; |
| for (i = 0; i < local_propchanges->nelts; i++) |
| { |
| svn_prop_t *propt = &APR_ARRAY_IDX(local_propchanges, i, |
| svn_prop_t); |
| apr_hash_set(*which_props, propt->name, |
| APR_HASH_KEY_STRING, propt->value); |
| } |
| } |
| } |
| } |
| |
| cleanup: |
| svn_pool_destroy(subpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_wc_props_modified_p(svn_boolean_t *modified_p, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| return modified_props(modified_p, path, NULL, adm_access, pool); |
| } |
| |
| |
| svn_error_t * |
| svn_wc__has_prop_mods(svn_boolean_t *prop_mods, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| return modified_props(prop_mods, path, NULL, adm_access, pool); |
| } |
| |
| |
| svn_error_t * |
| svn_wc_get_prop_diffs(apr_array_header_t **propchanges, |
| apr_hash_t **original_props, |
| const char *path, |
| svn_wc_adm_access_t *adm_access, |
| apr_pool_t *pool) |
| { |
| const svn_wc_entry_t *entry; |
| apr_hash_t *baseprops, *props; |
| const char *entryname; |
| |
| /*### Maybe assert (entry); calling svn_wc_get_prop_diffs |
| for an unversioned path is bogus */ |
| SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, pool)); |
| |
| if (! entry) |
| { |
| if (original_props) |
| *original_props = apr_hash_make(pool); |
| |
| if (propchanges) |
| *propchanges = apr_array_make(pool, 0, sizeof(svn_prop_t)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| if (entry->kind == svn_node_dir) |
| { |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, path, pool)); |
| entryname = SVN_WC_ENTRY_THIS_DIR; |
| } |
| else |
| { |
| const char *dirname; |
| svn_path_split(path, &dirname, &entryname, pool); |
| SVN_ERR(svn_wc_adm_retrieve(&adm_access, adm_access, dirname, pool)); |
| } |
| |
| SVN_ERR(svn_wc__load_props(&baseprops, propchanges ? &props : NULL, NULL, |
| adm_access, path, pool)); |
| |
| if (original_props != NULL) |
| *original_props = baseprops; |
| |
| if (propchanges != NULL) |
| SVN_ERR(svn_prop_diffs(propchanges, props, baseprops, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| |
| /** Externals **/ |
| |
| /* |
| * Look for either |
| * |
| * -r N |
| * -rN |
| * |
| * in the LINE_PARTS array and update the revision field in ITEM with |
| * the revision if the revision is found. Set REV_IDX to the index in |
| * LINE_PARTS where the revision specification starts. Remove from |
| * LINE_PARTS the element(s) that specify the revision. |
| * PARENT_DIRECTORY_DISPLAY and LINE are given to return a nice error |
| * string. |
| * |
| * If this function returns successfully, then LINE_PARTS will have |
| * only two elements in it. |
| */ |
| static svn_error_t * |
| find_and_remove_externals_revision(int *rev_idx, |
| const char **line_parts, |
| int num_line_parts, |
| svn_wc_external_item2_t *item, |
| const char *parent_directory_display, |
| const char *line, |
| apr_pool_t *pool) |
| { |
| int i; |
| |
| for (i = 0; i < 2; ++i) |
| { |
| const char *token = line_parts[i]; |
| |
| if (token[0] == '-' && token[1] == 'r') |
| { |
| svn_opt_revision_t end_revision = { svn_opt_revision_unspecified }; |
| const char *digits_ptr; |
| int shift_count; |
| int j; |
| |
| *rev_idx = i; |
| |
| if (token[2] == '\0') |
| { |
| /* There must be a total of four elements in the line if |
| -r N is used. */ |
| if (num_line_parts != 4) |
| goto parse_error; |
| |
| shift_count = 2; |
| digits_ptr = line_parts[i+1]; |
| } |
| else |
| { |
| /* There must be a total of three elements in the line |
| if -rN is used. */ |
| if (num_line_parts != 3) |
| goto parse_error; |
| |
| shift_count = 1; |
| digits_ptr = token+2; |
| } |
| |
| if (svn_opt_parse_revision(&item->revision, |
| &end_revision, |
| digits_ptr, pool) != 0) |
| goto parse_error; |
| /* We want a single revision, not a range. */ |
| if (end_revision.kind != svn_opt_revision_unspecified) |
| goto parse_error; |
| /* Allow only numbers and dates, not keywords. */ |
| if (item->revision.kind != svn_opt_revision_number |
| && item->revision.kind != svn_opt_revision_date) |
| goto parse_error; |
| |
| /* Shift any line elements past the revision specification |
| down over the revision specification. */ |
| for (j = i; j < num_line_parts-shift_count; ++j) |
| line_parts[j] = line_parts[j+shift_count]; |
| line_parts[num_line_parts-shift_count] = NULL; |
| |
| /* Found the revision, so leave the function immediately, do |
| * not continue looking for additional revisions. */ |
| return SVN_NO_ERROR; |
| } |
| } |
| |
| /* No revision was found, so there must be exactly two items in the |
| line array. */ |
| if (num_line_parts == 2) |
| return SVN_NO_ERROR; |
| |
| parse_error: |
| return svn_error_createf |
| (SVN_ERR_CLIENT_INVALID_EXTERNALS_DESCRIPTION, NULL, |
| _("Error parsing %s property on '%s': '%s'"), |
| SVN_PROP_EXTERNALS, |
| parent_directory_display, |
| line); |
| } |
| |
| svn_error_t * |
| svn_wc_parse_externals_description3(apr_array_header_t **externals_p, |
| const char *parent_directory, |
| const char *desc, |
| svn_boolean_t canonicalize_url, |
| apr_pool_t *pool) |
| { |
| apr_array_header_t *lines = svn_cstring_split(desc, "\n\r", TRUE, pool); |
| int i; |
| const char *parent_directory_display = svn_path_is_url(parent_directory) ? |
| parent_directory : svn_path_local_style(parent_directory, pool); |
| |
| if (externals_p) |
| *externals_p = apr_array_make(pool, 1, sizeof(svn_wc_external_item2_t *)); |
| |
| for (i = 0; i < lines->nelts; i++) |
| { |
| const char *line = APR_ARRAY_IDX(lines, i, const char *); |
| apr_status_t status; |
| char **line_parts; |
| int num_line_parts; |
| svn_wc_external_item2_t *item; |
| const char *token0; |
| const char *token1; |
| svn_boolean_t token0_is_url; |
| svn_boolean_t token1_is_url; |
| |
| /* Index into line_parts where the revision specification |
| started. */ |
| int rev_idx = -1; |
| |
| if ((! line) || (line[0] == '#')) |
| continue; |
| |
| /* else proceed */ |
| |
| status = apr_tokenize_to_argv(line, &line_parts, pool); |
| if (status) |
| return svn_error_wrap_apr(status, |
| _("Can't split line into components: '%s'"), |
| line); |
| /* Count the number of tokens. */ |
| for (num_line_parts = 0; line_parts[num_line_parts]; num_line_parts++) |
| ; |
| |
| SVN_ERR(svn_wc_external_item_create |
| ((const svn_wc_external_item2_t **) &item, pool)); |
| item->revision.kind = svn_opt_revision_unspecified; |
| item->peg_revision.kind = svn_opt_revision_unspecified; |
| |
| /* |
| * There are six different formats of externals: |
| * |
| * 1) DIR URL |
| * 2) DIR -r N URL |
| * 3) DIR -rN URL |
| * 4) URL DIR |
| * 5) -r N URL DIR |
| * 6) -rN URL DIR |
| * |
| * The last three allow peg revisions in the URL. |
| * |
| * With relative URLs and no '-rN' or '-r N', there is no way to |
| * distinguish between 'DIR URL' and 'URL DIR' when URL is a |
| * relative URL like /svn/repos/trunk, so this case is taken as |
| * case 4). |
| */ |
| if (num_line_parts < 2 || num_line_parts > 4) |
| return svn_error_createf |
| (SVN_ERR_CLIENT_INVALID_EXTERNALS_DESCRIPTION, NULL, |
| _("Error parsing %s property on '%s': '%s'"), |
| SVN_PROP_EXTERNALS, |
| parent_directory_display, |
| line); |
| |
| /* To make it easy to check for the forms, find and remove -r N |
| or -rN from the line item array. If it is found, rev_idx |
| contains the index into line_parts where '-r' was found and |
| set item->revision to the parsed revision. */ |
| /* ### ugh. stupid cast. */ |
| SVN_ERR(find_and_remove_externals_revision(&rev_idx, |
| (const char **)line_parts, |
| num_line_parts, item, |
| parent_directory_display, |
| line, pool)); |
| |
| token0 = line_parts[0]; |
| token1 = line_parts[1]; |
| |
| token0_is_url = svn_path_is_url(token0); |
| token1_is_url = svn_path_is_url(token1); |
| |
| if (token0_is_url && token1_is_url) |
| return svn_error_createf |
| (SVN_ERR_CLIENT_INVALID_EXTERNALS_DESCRIPTION, NULL, |
| _("Invalid %s property on '%s': " |
| "cannot use two absolute URLs ('%s' and '%s') in an external; " |
| "one must be a path where an absolute or relative URL is " |
| "checked out to"), |
| SVN_PROP_EXTERNALS, parent_directory_display, token0, token1); |
| |
| if (0 == rev_idx && token1_is_url) |
| return svn_error_createf |
| (SVN_ERR_CLIENT_INVALID_EXTERNALS_DESCRIPTION, NULL, |
| _("Invalid %s property on '%s': " |
| "cannot use a URL '%s' as the target directory for an external " |
| "definition"), |
| SVN_PROP_EXTERNALS, parent_directory_display, token1); |
| |
| if (1 == rev_idx && token0_is_url) |
| return svn_error_createf |
| (SVN_ERR_CLIENT_INVALID_EXTERNALS_DESCRIPTION, NULL, |
| _("Invalid %s property on '%s': " |
| "cannot use a URL '%s' as the target directory for an external " |
| "definition"), |
| SVN_PROP_EXTERNALS, parent_directory_display, token0); |
| |
| /* The appearence of -r N or -rN forces the type of external. |
| If -r is at the beginning of the line or the first token is |
| an absolute URL or if the second token is not an absolute |
| URL, then the URL supports peg revisions. */ |
| if (0 == rev_idx || |
| (-1 == rev_idx && (token0_is_url || ! token1_is_url))) |
| { |
| /* The URL is passed to svn_opt_parse_path in |
| uncanonicalized form so that the scheme relative URL |
| //hostname/foo is not collapsed to a server root relative |
| URL /hostname/foo. */ |
| SVN_ERR(svn_opt_parse_path(&item->peg_revision, &item->url, |
| token0, pool)); |
| item->target_dir = token1; |
| } |
| else |
| { |
| item->target_dir = token0; |
| item->url = token1; |
| item->peg_revision = item->revision; |
| } |
| |
| SVN_ERR(svn_opt_resolve_revisions(&item->peg_revision, |
| &item->revision, TRUE, FALSE, |
| pool)); |
| |
| item->target_dir = svn_path_canonicalize |
| (svn_path_internal_style(item->target_dir, pool), pool); |
| |
| if (item->target_dir[0] == '\0' || item->target_dir[0] == '/' |
| || svn_path_is_backpath_present(item->target_dir)) |
| return svn_error_createf |
| (SVN_ERR_CLIENT_INVALID_EXTERNALS_DESCRIPTION, NULL, |
| _("Invalid %s property on '%s': " |
| "target '%s' is an absolute path or involves '..'"), |
| SVN_PROP_EXTERNALS, |
| parent_directory_display, |
| item->target_dir); |
| |
| if (canonicalize_url) |
| item->url = svn_path_canonicalize(item->url, pool); |
| |
| if (externals_p) |
| APR_ARRAY_PUSH(*externals_p, svn_wc_external_item2_t *) = item; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_boolean_t |
| svn_wc__has_special_property(apr_hash_t *props) |
| { |
| return apr_hash_get(props, SVN_PROP_SPECIAL, APR_HASH_KEY_STRING) != NULL; |
| } |
| |
| svn_boolean_t |
| svn_wc__has_magic_property(const apr_array_header_t *properties) |
| { |
| int i; |
| |
| for (i = 0; i < properties->nelts; i++) |
| { |
| const svn_prop_t *property = &APR_ARRAY_IDX(properties, i, svn_prop_t); |
| |
| if (strcmp(property->name, SVN_PROP_EXECUTABLE) == 0 |
| || strcmp(property->name, SVN_PROP_KEYWORDS) == 0 |
| || strcmp(property->name, SVN_PROP_EOL_STYLE) == 0 |
| || strcmp(property->name, SVN_PROP_SPECIAL) == 0 |
| || strcmp(property->name, SVN_PROP_NEEDS_LOCK) == 0) |
| return TRUE; |
| } |
| return FALSE; |
| } |