/* * adm_ops.c: routines for affecting working copy administrative * information. NOTE: this code doesn't know where the adm * info is actually stored. Instead, generic handles to * adm data are requested via a reference to some PATH * (PATH being a regular, non-administrative directory or * file in the working copy). * * ==================================================================== * Copyright (c) 2000-2008 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 #include #include #include #include #include #include #include "svn_types.h" #include "svn_pools.h" #include "svn_string.h" #include "svn_error.h" #include "svn_path.h" #include "svn_hash.h" #include "svn_wc.h" #include "svn_io.h" #include "svn_xml.h" #include "svn_time.h" #include "svn_diff.h" #include "wc.h" #include "log.h" #include "adm_files.h" #include "adm_ops.h" #include "entries.h" #include "lock.h" #include "props.h" #include "translate.h" #include "svn_private_config.h" #include "private/svn_wc_private.h" /*** Finishing updates and commits. ***/ /* The main body of svn_wc__do_update_cleanup. */ static svn_error_t * tweak_entries(svn_wc_adm_access_t *dirpath, const char *base_url, const char *repos, svn_revnum_t new_rev, svn_wc_notify_func2_t notify_func, void *notify_baton, svn_boolean_t remove_missing_dirs, svn_depth_t depth, apr_hash_t *exclude_paths, apr_pool_t *pool) { apr_hash_t *entries; apr_hash_index_t *hi; apr_pool_t *subpool = svn_pool_create(pool); svn_boolean_t write_required = FALSE; svn_wc_notify_t *notify; /* Read DIRPATH's entries. */ SVN_ERR(svn_wc_entries_read(&entries, dirpath, TRUE, pool)); /* Tweak "this_dir" */ if (! apr_hash_get(exclude_paths, svn_wc_adm_access_path(dirpath), APR_HASH_KEY_STRING)) SVN_ERR(svn_wc__tweak_entry(entries, SVN_WC_ENTRY_THIS_DIR, base_url, repos, new_rev, FALSE, &write_required, svn_wc_adm_access_pool(dirpath))); if (depth == svn_depth_unknown) depth = svn_depth_infinity; if (depth > svn_depth_empty) { for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) { const void *key; void *val; const char *name; svn_wc_entry_t *current_entry; const char *child_path; const char *child_url = NULL; svn_boolean_t excluded; svn_pool_clear(subpool); apr_hash_this(hi, &key, NULL, &val); name = key; current_entry = val; /* Ignore the "this dir" entry. */ if (! strcmp(name, SVN_WC_ENTRY_THIS_DIR)) continue; /* Derive the new URL for the current (child) entry */ if (base_url) child_url = svn_path_url_add_component(base_url, name, subpool); child_path = svn_path_join(svn_wc_adm_access_path(dirpath), name, subpool); excluded = (apr_hash_get(exclude_paths, child_path, APR_HASH_KEY_STRING) != NULL); /* If a file, or deleted or absent dir, then tweak the entry but don't recurse. */ if ((current_entry->kind == svn_node_file) || (current_entry->deleted || current_entry->absent)) { if (! excluded) SVN_ERR(svn_wc__tweak_entry(entries, name, child_url, repos, new_rev, TRUE, &write_required, svn_wc_adm_access_pool(dirpath))); } /* If a directory and recursive... */ else if ((depth == svn_depth_infinity || depth == svn_depth_immediates) && (current_entry->kind == svn_node_dir)) { svn_depth_t depth_below_here = depth; if (depth == svn_depth_immediates) depth_below_here = svn_depth_empty; /* If the directory is 'missing', remove it. This is safe as long as this function is only called as a helper to svn_wc__do_update_cleanup, since the update will already have restored any missing items that it didn't want to delete. */ if (remove_missing_dirs && svn_wc__adm_missing(dirpath, child_path)) { if (current_entry->schedule != svn_wc_schedule_add && !excluded) { svn_wc__entry_remove(entries, name); if (notify_func) { notify = svn_wc_create_notify(child_path, svn_wc_notify_delete, subpool); notify->kind = current_entry->kind; (* notify_func)(notify_baton, notify, subpool); } } /* Else if missing item is schedule-add, do nothing. */ } /* Not missing, deleted, or absent, so recurse. */ else { svn_wc_adm_access_t *child_access; SVN_ERR(svn_wc_adm_retrieve(&child_access, dirpath, child_path, subpool)); SVN_ERR(tweak_entries (child_access, child_url, repos, new_rev, notify_func, notify_baton, remove_missing_dirs, depth_below_here, exclude_paths, subpool)); } } } } /* Write a shiny new entries file to disk. */ if (write_required) SVN_ERR(svn_wc__entries_write(entries, dirpath, subpool)); /* Cleanup */ svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* Helper for svn_wc_process_committed2. */ static svn_error_t * remove_revert_file(svn_stringbuf_t **logtags, svn_wc_adm_access_t *adm_access, const char *path, svn_boolean_t is_prop, apr_pool_t * pool) { const char *revert_file; svn_node_kind_t kind; if (is_prop) SVN_ERR(svn_wc__loggy_props_delete(logtags, path, svn_wc__props_revert, adm_access, pool)); else { revert_file = svn_wc__text_revert_path(path, FALSE, pool); SVN_ERR(svn_io_check_path(revert_file, &kind, pool)); if (kind == svn_node_file) SVN_ERR(svn_wc__loggy_remove(logtags, adm_access, revert_file, pool)); } return SVN_NO_ERROR; } svn_error_t * svn_wc__do_update_cleanup(const char *path, svn_wc_adm_access_t *adm_access, svn_depth_t depth, const char *base_url, const char *repos, svn_revnum_t new_revision, svn_wc_notify_func2_t notify_func, void *notify_baton, svn_boolean_t remove_missing_dirs, apr_hash_t *exclude_paths, apr_pool_t *pool) { apr_hash_t *entries; const svn_wc_entry_t *entry; SVN_ERR(svn_wc_entry(&entry, path, adm_access, TRUE, pool)); if (entry == NULL) return SVN_NO_ERROR; if (entry->kind == svn_node_file || (entry->kind == svn_node_dir && (entry->deleted || entry->absent))) { const char *parent, *base_name; svn_wc_adm_access_t *dir_access; svn_boolean_t write_required = FALSE; if (apr_hash_get(exclude_paths, path, APR_HASH_KEY_STRING)) return SVN_NO_ERROR; svn_path_split(path, &parent, &base_name, pool); SVN_ERR(svn_wc_adm_retrieve(&dir_access, adm_access, parent, pool)); SVN_ERR(svn_wc_entries_read(&entries, dir_access, TRUE, pool)); SVN_ERR(svn_wc__tweak_entry(entries, base_name, base_url, repos, new_revision, FALSE, /* Parent not updated so don't remove PATH entry */ &write_required, svn_wc_adm_access_pool(dir_access))); if (write_required) SVN_ERR(svn_wc__entries_write(entries, dir_access, pool)); } else if (entry->kind == svn_node_dir) { svn_wc_adm_access_t *dir_access; SVN_ERR(svn_wc_adm_retrieve(&dir_access, adm_access, path, pool)); SVN_ERR(tweak_entries(dir_access, base_url, repos, new_revision, notify_func, notify_baton, remove_missing_dirs, depth, exclude_paths, pool)); } else return svn_error_createf(SVN_ERR_NODE_UNKNOWN_KIND, NULL, _("Unrecognized node kind: '%s'"), svn_path_local_style(path, pool)); return SVN_NO_ERROR; } svn_error_t * svn_wc_maybe_set_repos_root(svn_wc_adm_access_t *adm_access, const char *path, const char *repos, apr_pool_t *pool) { apr_hash_t *entries; svn_boolean_t write_required = FALSE; const svn_wc_entry_t *entry; const char *base_name; svn_wc_adm_access_t *dir_access; SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, pool)); if (! entry) return SVN_NO_ERROR; if (entry->kind == svn_node_file) { const char *parent; svn_path_split(path, &parent, &base_name, pool); SVN_ERR(svn_wc__adm_retrieve_internal(&dir_access, adm_access, parent, pool)); } else { base_name = SVN_WC_ENTRY_THIS_DIR; SVN_ERR(svn_wc__adm_retrieve_internal(&dir_access, adm_access, path, pool)); } if (! dir_access) return SVN_NO_ERROR; SVN_ERR(svn_wc_entries_read(&entries, dir_access, TRUE, pool)); SVN_ERR(svn_wc__tweak_entry(entries, base_name, NULL, repos, SVN_INVALID_REVNUM, FALSE, &write_required, svn_wc_adm_access_pool(dir_access))); if (write_required) SVN_ERR(svn_wc__entries_write(entries, dir_access, pool)); return SVN_NO_ERROR; } static svn_error_t * process_committed_leaf(int log_number, const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t *recurse, svn_revnum_t new_revnum, const char *rev_date, const char *rev_author, apr_array_header_t *wcprop_changes, svn_boolean_t remove_lock, svn_boolean_t remove_changelist, const unsigned char *digest, apr_pool_t *pool) { const char *base_name; const char *hex_digest = NULL; svn_wc_entry_t tmp_entry; apr_uint64_t modify_flags = 0; svn_stringbuf_t *logtags = svn_stringbuf_create("", pool); svn_checksum_t *checksum = NULL; SVN_ERR(svn_wc__adm_write_check(adm_access)); if (digest) { checksum = svn_checksum_create(svn_checksum_md5, pool); checksum->digest = digest; } /* Set PATH's working revision to NEW_REVNUM; if REV_DATE and REV_AUTHOR are both non-NULL, then set the 'committed-rev', 'committed-date', and 'last-author' entry values; and set the checksum if a file. */ base_name = svn_path_is_child(svn_wc_adm_access_path(adm_access), path, pool); if (base_name) { /* If the props or text revert file exists it needs to be deleted when * the file is committed. */ SVN_ERR(remove_revert_file(&logtags, adm_access, path, FALSE, pool)); SVN_ERR(remove_revert_file(&logtags, adm_access, path, TRUE, pool)); if (checksum) hex_digest = svn_checksum_to_cstring(checksum, pool); else { /* There may be a new text base sitting in the adm tmp area by now, because the commit succeeded. A file that is copied, but not otherwise modified, doesn't have a new text base, so we use the unmodified text base. ### Does this mean that a file committed with only prop mods ### will still get its text base checksum recomputed? Yes it ### does, sadly. But it's not enough to just check for that ### condition, because in the case of an added file, there ### may not be a pre-existing checksum in the entry. ### Probably the best solution is to compute (or copy) the ### checksum at 'svn add' (or 'svn cp') time, instead of ### waiting until commit time. */ const char *latest_base; svn_error_t *err; svn_checksum_t *local_checksum; latest_base = svn_wc__text_base_path(path, TRUE, pool); err = svn_io_file_checksum2(&local_checksum, latest_base, svn_checksum_md5, pool); if (err && APR_STATUS_IS_ENOENT(err->apr_err)) { svn_error_clear(err); latest_base = svn_wc__text_base_path(path, FALSE, pool); err = svn_io_file_checksum2(&local_checksum, latest_base, svn_checksum_md5, pool); } if (! err) hex_digest = svn_checksum_to_cstring(local_checksum, pool); else if (APR_STATUS_IS_ENOENT(err->apr_err)) svn_error_clear(err); else return err; } /* Oh, and recursing at this point isn't really sensible. */ if (recurse) *recurse = FALSE; } else { /* PATH must be a dir */ base_name = SVN_WC_ENTRY_THIS_DIR; } /* Append a log command to set (overwrite) the 'committed-rev', 'committed-date', 'last-author', and possibly 'checksum' attributes in the entry. Note: it's important that this log command come *before* the LOG_COMMITTED command, because log_do_committed() might actually remove the entry! */ if (rev_date) { tmp_entry.cmt_rev = new_revnum; SVN_ERR(svn_time_from_cstring(&tmp_entry.cmt_date, rev_date, pool)); modify_flags |= SVN_WC__ENTRY_MODIFY_CMT_REV | SVN_WC__ENTRY_MODIFY_CMT_DATE; } if (rev_author) { tmp_entry.cmt_rev = new_revnum; tmp_entry.cmt_author = rev_author; modify_flags |= SVN_WC__ENTRY_MODIFY_CMT_REV | SVN_WC__ENTRY_MODIFY_CMT_AUTHOR; } if (hex_digest) { tmp_entry.checksum = hex_digest; modify_flags |= SVN_WC__ENTRY_MODIFY_CHECKSUM; } SVN_ERR(svn_wc__loggy_entry_modify(&logtags, adm_access, path, &tmp_entry, modify_flags, pool)); if (remove_lock) SVN_ERR(svn_wc__loggy_delete_lock(&logtags, adm_access, path, pool)); if (remove_changelist) SVN_ERR(svn_wc__loggy_delete_changelist(&logtags, adm_access, path, pool)); /* Regardless of whether it's a file or dir, the "main" logfile contains a command to bump the revision attribute (and timestamp). */ SVN_ERR(svn_wc__loggy_committed(&logtags, adm_access, path, new_revnum, pool)); /* Do wcprops in the same log txn as revision, etc. */ if (wcprop_changes && (wcprop_changes->nelts > 0)) { int i; for (i = 0; i < wcprop_changes->nelts; i++) { svn_prop_t *prop = APR_ARRAY_IDX(wcprop_changes, i, svn_prop_t *); SVN_ERR(svn_wc__loggy_modify_wcprop (&logtags, adm_access, path, prop->name, prop->value ? prop->value->data : NULL, pool)); } } /* Write our accumulation of log entries into a log file */ return svn_wc__write_log(adm_access, log_number, logtags, pool); } static svn_error_t * process_committed_internal(int *log_number, const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t recurse, svn_revnum_t new_revnum, const char *rev_date, const char *rev_author, apr_array_header_t *wcprop_changes, svn_boolean_t remove_lock, svn_boolean_t remove_changelist, const unsigned char *digest, apr_pool_t *pool) { SVN_ERR(process_committed_leaf((*log_number)++, path, adm_access, &recurse, new_revnum, rev_date, rev_author, wcprop_changes, remove_lock, remove_changelist, digest, pool)); if (recurse) { apr_hash_t *entries; apr_hash_index_t *hi; apr_pool_t *subpool = svn_pool_create(pool); /* Read PATH's entries; this is the absolute path. */ SVN_ERR(svn_wc_entries_read(&entries, adm_access, TRUE, pool)); /* Recursively loop over all children. */ for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) { const void *key; void *val; const char *name; const svn_wc_entry_t *current_entry; const char *this_path; svn_wc_adm_access_t *child_access; svn_pool_clear(subpool); apr_hash_this(hi, &key, NULL, &val); name = key; current_entry = val; /* Ignore the "this dir" entry. */ if (! strcmp(name, SVN_WC_ENTRY_THIS_DIR)) continue; /* Create child path by telescoping the main path. */ this_path = svn_path_join(path, name, subpool); if (current_entry->kind == svn_node_dir) SVN_ERR(svn_wc_adm_retrieve(&child_access, adm_access, this_path, subpool)); else child_access = adm_access; /* Recurse, but only allow further recursion if the child is a directory. Pass null for wcprop_changes, because the ones present in the current call are only applicable to this one committed item. */ if (current_entry->kind == svn_node_dir) SVN_ERR(svn_wc_process_committed4 (this_path, child_access, TRUE, new_revnum, rev_date, rev_author, NULL, FALSE, remove_changelist, NULL, subpool)); else { /* Suppress log creation for deleted entries in a replaced directory. By the time any log we create here is run, those entries will already have been removed (as a result of running the log for the replaced directory that was created at the start of this function). */ if (current_entry->schedule == svn_wc_schedule_delete) { svn_wc_entry_t *parent_entry; parent_entry = apr_hash_get(entries, SVN_WC_ENTRY_THIS_DIR, APR_HASH_KEY_STRING); if (parent_entry->schedule == svn_wc_schedule_replace) continue; } SVN_ERR(process_committed_leaf ((*log_number)++, this_path, adm_access, NULL, new_revnum, rev_date, rev_author, NULL, FALSE, remove_changelist, NULL, subpool)); } } svn_pool_destroy(subpool); } return SVN_NO_ERROR; } struct svn_wc_committed_queue_t { apr_pool_t *pool; apr_array_header_t *queue; }; typedef struct committed_queue_item_t { const char *path; svn_wc_adm_access_t *adm_access; svn_boolean_t recurse; svn_boolean_t remove_lock; svn_boolean_t remove_changelist; apr_array_header_t *wcprop_changes; const unsigned char *digest; } committed_queue_item_t; svn_wc_committed_queue_t * svn_wc_committed_queue_create(apr_pool_t *pool) { svn_wc_committed_queue_t *q; q = apr_palloc(pool, sizeof(*q)); q->pool = pool; q->queue = apr_array_make(pool, 1, sizeof(committed_queue_item_t *)); return q; } svn_error_t * svn_wc_queue_committed(svn_wc_committed_queue_t **queue, const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t recurse, apr_array_header_t *wcprop_changes, svn_boolean_t remove_lock, svn_boolean_t remove_changelist, const unsigned char *digest, apr_pool_t *pool) { committed_queue_item_t *cqi; /* Use the same pool as the one *QUEUE was allocated in, to prevent lifetime issues. Intermediate operations should use POOL. */ /* Add to the array with paths and options */ cqi = apr_palloc((*queue)->pool, sizeof(*cqi)); cqi->path = path; cqi->adm_access = adm_access; cqi->recurse = recurse; cqi->remove_lock = remove_lock; cqi->remove_changelist = remove_changelist; cqi->wcprop_changes = wcprop_changes; cqi->digest = digest; APR_ARRAY_PUSH((*queue)->queue, committed_queue_item_t *) = cqi; return SVN_NO_ERROR; } typedef struct affected_adm_t { int next_log; svn_wc_adm_access_t *adm_access; } affected_adm_t; /* Return TRUE if any item of QUEUE is a parent of ITEM and will be processed recursively, return FALSE otherwise. If HAVE_ANY_RECURSIVE is FALSE, exit early returning FALSE. Recalculate its value otherwise, changing it to FALSE iff no recursive items are found. */ static svn_boolean_t have_recursive_parent(svn_boolean_t *have_any_recursive, apr_array_header_t *queue, int item, apr_pool_t *pool) { int i; svn_boolean_t found_recursive = FALSE; const char *path = APR_ARRAY_IDX(queue, item, committed_queue_item_t *)->path; if (! *have_any_recursive) return FALSE; for (i = 0; i < queue->nelts; i++) { committed_queue_item_t *qi = APR_ARRAY_IDX(queue, i, committed_queue_item_t *); found_recursive |= qi->recurse; if (i == item) continue; if (qi->recurse && svn_path_is_child(qi->path, path, pool)) return TRUE; } /* Now we walked the entire array, change the cached value to reflect what we found. */ *have_any_recursive = found_recursive; return FALSE; } svn_error_t * svn_wc_process_committed_queue(svn_wc_committed_queue_t *queue, svn_wc_adm_access_t *adm_access, svn_revnum_t new_revnum, const char *rev_date, const char *rev_author, apr_pool_t *pool) { int i; apr_hash_index_t *hi; apr_hash_t *updated_adms = apr_hash_make(pool); apr_pool_t *iterpool = svn_pool_create(pool); svn_boolean_t have_any_recursive = TRUE; /* Assume we do have recursive items queued: we need to search for recursive parents until proven otherwise */ /* Now, we write all log files, collecting the affected adms in the process ... */ for (i = 0; i < queue->queue->nelts; i++) { affected_adm_t *affected_adm; const char *adm_path; committed_queue_item_t *cqi = APR_ARRAY_IDX(queue->queue, i, committed_queue_item_t *); svn_pool_clear(iterpool); if (have_recursive_parent(&have_any_recursive, queue->queue, i, iterpool)) continue; adm_path = svn_wc_adm_access_path(cqi->adm_access); affected_adm = apr_hash_get(updated_adms, adm_path, APR_HASH_KEY_STRING); if (! affected_adm) { /* allocate in pool instead of iterpool: we don't want this cleared at the next iteration */ affected_adm = apr_palloc(pool, sizeof(*affected_adm)); affected_adm->next_log = 0; affected_adm->adm_access = cqi->adm_access; apr_hash_set(updated_adms, adm_path, APR_HASH_KEY_STRING, affected_adm); } SVN_ERR(process_committed_internal(&affected_adm->next_log, cqi->path, cqi->adm_access, cqi->recurse, new_revnum, rev_date, rev_author, cqi->wcprop_changes, cqi->remove_lock, cqi->remove_changelist, cqi->digest, iterpool)); } /* ... and then we run them; all at once. This prevents writing the entries file more than once per adm area */ for (hi = apr_hash_first(pool, updated_adms); hi; hi = apr_hash_next(hi)) { void *val; affected_adm_t *this_adm; svn_pool_clear(iterpool); apr_hash_this(hi, NULL, NULL, &val); this_adm = val; SVN_ERR(svn_wc__run_log(this_adm->adm_access, NULL, iterpool)); } queue->queue->nelts = 0; svn_pool_destroy(iterpool); return SVN_NO_ERROR; } svn_error_t * svn_wc_process_committed4(const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t recurse, svn_revnum_t new_revnum, const char *rev_date, const char *rev_author, apr_array_header_t *wcprop_changes, svn_boolean_t remove_lock, svn_boolean_t remove_changelist, const unsigned char *digest, apr_pool_t *pool) { int log_number = 0; SVN_ERR(process_committed_internal(&log_number, path, adm_access, recurse, new_revnum, rev_date, rev_author, wcprop_changes, remove_lock, remove_changelist, digest, pool)); /* Run the log file(s) we just created. */ return svn_wc__run_log(adm_access, NULL, pool); } svn_error_t * svn_wc_process_committed3(const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t recurse, svn_revnum_t new_revnum, const char *rev_date, const char *rev_author, apr_array_header_t *wcprop_changes, svn_boolean_t remove_lock, const unsigned char *digest, apr_pool_t *pool) { return svn_wc_process_committed4(path, adm_access, recurse, new_revnum, rev_date, rev_author, wcprop_changes, remove_lock, FALSE, digest, pool); } svn_error_t * svn_wc_process_committed2(const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t recurse, svn_revnum_t new_revnum, const char *rev_date, const char *rev_author, apr_array_header_t *wcprop_changes, svn_boolean_t remove_lock, apr_pool_t *pool) { return svn_wc_process_committed3(path, adm_access, recurse, new_revnum, rev_date, rev_author, wcprop_changes, remove_lock, NULL, pool); } svn_error_t * svn_wc_process_committed(const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t recurse, svn_revnum_t new_revnum, const char *rev_date, const char *rev_author, apr_array_header_t *wcprop_changes, apr_pool_t *pool) { return svn_wc_process_committed2(path, adm_access, recurse, new_revnum, rev_date, rev_author, wcprop_changes, FALSE, pool); } /* Remove FILE if it exists and is a file. If it does not exist, do nothing. If it is not a file, 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; } /* Recursively mark a tree ADM_ACCESS with a SCHEDULE, COPIED and/or KEEP_LOCAL flag, depending on the state of MODIFY_FLAGS (which may contain only a subset of the possible modification flags, namely, those indicating a change to one of the three flags mentioned above). */ static svn_error_t * mark_tree(svn_wc_adm_access_t *adm_access, apr_uint64_t modify_flags, svn_wc_schedule_t schedule, svn_boolean_t copied, svn_boolean_t keep_local, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { apr_pool_t *subpool = svn_pool_create(pool); apr_hash_t *entries; apr_hash_index_t *hi; const svn_wc_entry_t *entry; svn_wc_entry_t tmp_entry; apr_uint64_t this_dir_flags; /* Read the entries file for this directory. */ SVN_ERR(svn_wc_entries_read(&entries, adm_access, FALSE, pool)); /* Mark each entry in the entries file. */ for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) { const char *fullpath; const void *key; void *val; const char *base_name; /* Clear our per-iteration pool. */ svn_pool_clear(subpool); /* Get the next entry */ apr_hash_this(hi, &key, NULL, &val); entry = val; /* Skip "this dir". */ if (! strcmp((const char *)key, SVN_WC_ENTRY_THIS_DIR)) continue; base_name = key; fullpath = svn_path_join(svn_wc_adm_access_path(adm_access), base_name, subpool); /* If this is a directory, recurse. */ if (entry->kind == svn_node_dir) { svn_wc_adm_access_t *child_access; SVN_ERR(svn_wc_adm_retrieve(&child_access, adm_access, fullpath, subpool)); SVN_ERR(mark_tree(child_access, modify_flags, schedule, copied, keep_local, cancel_func, cancel_baton, notify_func, notify_baton, subpool)); } tmp_entry.schedule = schedule; tmp_entry.copied = copied; SVN_ERR(svn_wc__entry_modify (adm_access, base_name, &tmp_entry, modify_flags & (SVN_WC__ENTRY_MODIFY_SCHEDULE | SVN_WC__ENTRY_MODIFY_COPIED), TRUE, subpool)); if (copied) /* Remove now obsolete wcprops */ SVN_ERR(svn_wc__props_delete(fullpath, svn_wc__props_wcprop, adm_access, subpool)); /* Tell someone what we've done. */ if (schedule == svn_wc_schedule_delete && notify_func != NULL) (*notify_func)(notify_baton, svn_wc_create_notify(fullpath, svn_wc_notify_delete, subpool), pool); } /* Handle "this dir" for states that need it done post-recursion. */ entry = apr_hash_get(entries, SVN_WC_ENTRY_THIS_DIR, APR_HASH_KEY_STRING); this_dir_flags = 0; /* Uncommitted directories (schedule add) that are to be scheduled for deletion are a special case, they don't need to be changed as they will be removed from their parent's entry list. */ if (! (entry->schedule == svn_wc_schedule_add && schedule == svn_wc_schedule_delete)) { if (modify_flags & SVN_WC__ENTRY_MODIFY_SCHEDULE) { tmp_entry.schedule = schedule; this_dir_flags |= SVN_WC__ENTRY_MODIFY_SCHEDULE; } if (modify_flags & SVN_WC__ENTRY_MODIFY_COPIED) { tmp_entry.copied = copied; this_dir_flags |= SVN_WC__ENTRY_MODIFY_COPIED; } } /* Set keep_local on the "this dir", if requested. */ if (modify_flags & SVN_WC__ENTRY_MODIFY_KEEP_LOCAL) { tmp_entry.keep_local = keep_local; this_dir_flags |= SVN_WC__ENTRY_MODIFY_KEEP_LOCAL; } /* Modify this_dir entry if requested. */ if (this_dir_flags) SVN_ERR(svn_wc__entry_modify(adm_access, NULL, &tmp_entry, this_dir_flags, TRUE, subpool)); /* Destroy our per-iteration pool. */ svn_pool_destroy(subpool); return SVN_NO_ERROR; } /* Remove/erase PATH from the working copy. This involves deleting PATH * from the physical filesystem. PATH is assumed to be an unversioned file * or directory. * * If CANCEL_FUNC is non-null, invoke it with CANCEL_BATON at various * points, return any error immediately. */ static svn_error_t * erase_unversioned_from_wc(const char *path, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *pool) { svn_error_t *err; /* Optimize the common case: try to delete the file */ err = svn_io_remove_file(path, pool); if (err) { /* Then maybe it was a directory? */ svn_error_clear(err); err = svn_io_remove_dir2(path, FALSE, cancel_func, cancel_baton, pool); if (err) { /* We're unlikely to end up here. But we need this fallback to make sure we report the right error *and* try the correct deletion at least once. */ svn_node_kind_t kind; svn_error_clear(err); SVN_ERR(svn_io_check_path(path, &kind, pool)); if (kind == svn_node_file) SVN_ERR(svn_io_remove_file(path, pool)); else if (kind == svn_node_dir) SVN_ERR(svn_io_remove_dir2(path, FALSE, cancel_func, cancel_baton, pool)); else if (kind == svn_node_none) return svn_error_createf(SVN_ERR_BAD_FILENAME, NULL, _("'%s' does not exist"), svn_path_local_style(path, pool)); else return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, _("Unsupported node kind for path '%s'"), svn_path_local_style(path, pool)); } } return SVN_NO_ERROR; } /* Remove/erase PATH from the working copy. For files this involves * deletion from the physical filesystem. For directories it involves the * deletion from the filesystem of all unversioned children, and all * versioned children that are files. By the time we get here, added but * not committed items will have been scheduled for deletion which means * they have become unversioned. * * The result is that all that remains are versioned directories, each with * its .svn directory and .svn contents. * * If CANCEL_FUNC is non-null, invoke it with CANCEL_BATON at various * points, return any error immediately. * * KIND is the node kind appropriate for PATH */ static svn_error_t * erase_from_wc(const char *path, svn_wc_adm_access_t *adm_access, svn_node_kind_t kind, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *pool) { const svn_wc_entry_t *entry; if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); if (kind == svn_node_file) SVN_ERR(remove_file_if_present(path, pool)); else if (kind == svn_node_dir) /* This must be a directory or absent */ { apr_hash_t *ver, *unver; apr_hash_index_t *hi; svn_wc_adm_access_t *dir_access; svn_error_t *err; /* ### Suspect that an iteration or recursion subpool would be good here. */ /* First handle the versioned items, this is better (probably) than simply using svn_io_get_dirents2 for everything as it avoids the need to do svn_io_check_path on each versioned item */ err = svn_wc_adm_retrieve(&dir_access, adm_access, path, pool); /* If there's no on-disk item, be sure to exit early and not to return an error */ if (err) { svn_node_kind_t wc_kind; svn_error_t *err2 = svn_io_check_path(path, &wc_kind, pool); if (err2) { svn_error_clear(err); return err2; } if (wc_kind != svn_node_none) return err; svn_error_clear(err); return SVN_NO_ERROR; } SVN_ERR(svn_wc_entries_read(&ver, dir_access, FALSE, pool)); for (hi = apr_hash_first(pool, ver); hi; hi = apr_hash_next(hi)) { const void *key; void *val; const char *name; const char *down_path; apr_hash_this(hi, &key, NULL, &val); name = key; entry = val; if (!strcmp(name, SVN_WC_ENTRY_THIS_DIR)) continue; down_path = svn_path_join(path, name, pool); SVN_ERR(erase_from_wc(down_path, adm_access, entry->kind, cancel_func, cancel_baton, pool)); } /* Now handle any remaining unversioned items */ SVN_ERR(svn_io_get_dirents2(&unver, path, pool)); for (hi = apr_hash_first(pool, unver); hi; hi = apr_hash_next(hi)) { const void *key; const char *name; const char *down_path; apr_hash_this(hi, &key, NULL, NULL); name = key; /* The admin directory will show up, we don't want to delete it */ if (svn_wc_is_adm_dir(name, pool)) continue; /* Versioned directories will show up, don't delete those either */ if (apr_hash_get(ver, name, APR_HASH_KEY_STRING)) continue; down_path = svn_path_join(path, name, pool); SVN_ERR(erase_unversioned_from_wc (down_path, cancel_func, cancel_baton, pool)); } } return SVN_NO_ERROR; } svn_error_t * svn_wc_delete3(const char *path, svn_wc_adm_access_t *adm_access, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, svn_boolean_t keep_local, apr_pool_t *pool) { svn_wc_adm_access_t *dir_access; const svn_wc_entry_t *entry; const char *parent, *base_name; svn_boolean_t was_schedule; svn_node_kind_t was_kind; svn_boolean_t was_copied; svn_boolean_t was_deleted = FALSE; /* Silence a gcc uninitialized warning */ SVN_ERR(svn_wc_adm_probe_try3(&dir_access, adm_access, path, TRUE, -1, cancel_func, cancel_baton, pool)); if (dir_access) SVN_ERR(svn_wc_entry(&entry, path, dir_access, FALSE, pool)); else entry = NULL; if (!entry) return erase_unversioned_from_wc(path, cancel_func, cancel_baton, pool); /* A file external should not be deleted since the file external is implemented as a switched file and it would delete the file the file external is switched to, which is not the behavior the user would probably want. */ if (entry->file_external_path) return svn_error_createf(SVN_ERR_WC_CANNOT_DELETE_FILE_EXTERNAL, NULL, _("Cannot remove the file external at '%s'; " "please propedit or propdel the svn:externals " "description that created it"), svn_path_local_style(path, pool)); /* Note: Entries caching? What happens to this entry when the entries file is updated? Lets play safe and copy the values */ was_schedule = entry->schedule; was_kind = entry->kind; was_copied = entry->copied; svn_path_split(path, &parent, &base_name, pool); if (was_kind == svn_node_dir) { svn_wc_adm_access_t *parent_access; apr_hash_t *entries; const svn_wc_entry_t *entry_in_parent; /* The deleted state is only available in the entry in parent's entries file */ SVN_ERR(svn_wc_adm_retrieve(&parent_access, adm_access, parent, pool)); SVN_ERR(svn_wc_entries_read(&entries, parent_access, TRUE, pool)); entry_in_parent = apr_hash_get(entries, base_name, APR_HASH_KEY_STRING); was_deleted = entry_in_parent ? entry_in_parent->deleted : FALSE; if (was_schedule == svn_wc_schedule_add && !was_deleted) { /* Deleting a directory that has been added but not yet committed is easy, just remove the administrative dir. */ if (dir_access != adm_access) { SVN_ERR(svn_wc_remove_from_revision_control (dir_access, SVN_WC_ENTRY_THIS_DIR, FALSE, FALSE, cancel_func, cancel_baton, pool)); } else { /* adm_probe_retrieve returned the parent access baton, which is the same access baton that we came in here with! this means we're dealing with a missing item that's scheduled for addition. Easiest to just remove the entry. */ svn_wc__entry_remove(entries, base_name); SVN_ERR(svn_wc__entries_write(entries, parent_access, pool)); } } else { /* if adm_probe_retrieve returned the parent access baton, (which is the same access baton that we came in here with), this means we're dealing with a missing directory. So there's no tree to mark for deletion. Instead, the next phase of code will simply schedule the directory for deletion in its parent. */ if (dir_access != adm_access) { /* Recursively mark a whole tree for deletion. */ SVN_ERR(mark_tree(dir_access, SVN_WC__ENTRY_MODIFY_SCHEDULE | SVN_WC__ENTRY_MODIFY_KEEP_LOCAL, svn_wc_schedule_delete, FALSE, keep_local, cancel_func, cancel_baton, notify_func, notify_baton, pool)); } } } if (!(was_kind == svn_node_dir && was_schedule == svn_wc_schedule_add && !was_deleted)) { /* We need to mark this entry for deletion in its parent's entries file, so we split off base_name from the parent path, then fold in the addition of a delete flag. */ svn_stringbuf_t *log_accum = svn_stringbuf_create("", pool); svn_wc_entry_t tmp_entry; /* Edit the entry to reflect the now deleted state. entries.c:fold_entry() clears the values of copied, copyfrom_rev and copyfrom_url. */ tmp_entry.schedule = svn_wc_schedule_delete; SVN_ERR(svn_wc__loggy_entry_modify(&log_accum, adm_access, path, &tmp_entry, SVN_WC__ENTRY_MODIFY_SCHEDULE, pool)); /* is it a replacement with history? */ if (was_schedule == svn_wc_schedule_replace && was_copied) { const char *text_base = svn_wc__text_base_path(path, FALSE, pool); const char *text_revert = svn_wc__text_revert_path(path, FALSE, pool); if (was_kind != svn_node_dir) /* Dirs don't have text-bases */ /* Restore the original text-base */ SVN_ERR(svn_wc__loggy_move(&log_accum, NULL, adm_access, text_revert, text_base, FALSE, pool)); SVN_ERR(svn_wc__loggy_revert_props_restore(&log_accum, path, adm_access, pool)); } if (was_schedule == svn_wc_schedule_add) SVN_ERR(svn_wc__loggy_props_delete(&log_accum, path, svn_wc__props_base, adm_access, pool)); SVN_ERR(svn_wc__write_log(adm_access, 0, log_accum, pool)); SVN_ERR(svn_wc__run_log(adm_access, NULL, pool)); } /* Report the deletion to the caller. */ if (notify_func != NULL) (*notify_func)(notify_baton, svn_wc_create_notify(path, svn_wc_notify_delete, pool), pool); /* By the time we get here, anything that was scheduled to be added has become unversioned */ if (!keep_local) { if (was_schedule == svn_wc_schedule_add) SVN_ERR(erase_unversioned_from_wc (path, cancel_func, cancel_baton, pool)); else SVN_ERR(erase_from_wc(path, adm_access, was_kind, cancel_func, cancel_baton, pool)); } return SVN_NO_ERROR; } svn_error_t * svn_wc_delete2(const char *path, svn_wc_adm_access_t *adm_access, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { return svn_wc_delete3(path, adm_access, cancel_func, cancel_baton, notify_func, notify_baton, FALSE, pool); } svn_error_t * svn_wc_delete(const char *path, svn_wc_adm_access_t *adm_access, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func_t notify_func, void *notify_baton, apr_pool_t *pool) { svn_wc__compat_notify_baton_t nb; nb.func = notify_func; nb.baton = notify_baton; return svn_wc_delete2(path, adm_access, cancel_func, cancel_baton, svn_wc__compat_call_notify_func, &nb, pool); } svn_error_t * svn_wc_get_ancestry(char **url, svn_revnum_t *rev, const char *path, svn_wc_adm_access_t *adm_access, apr_pool_t *pool) { const svn_wc_entry_t *ent; SVN_ERR(svn_wc__entry_versioned(&ent, path, adm_access, FALSE, pool)); if (url) *url = apr_pstrdup(pool, ent->url); if (rev) *rev = ent->revision; return SVN_NO_ERROR; } svn_error_t * svn_wc_add3(const char *path, svn_wc_adm_access_t *parent_access, svn_depth_t depth, const char *copyfrom_url, svn_revnum_t copyfrom_rev, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { const char *parent_dir, *base_name; const svn_wc_entry_t *orig_entry, *parent_entry; svn_wc_entry_t tmp_entry; svn_boolean_t is_replace = FALSE; svn_node_kind_t kind; apr_uint64_t modify_flags = 0; svn_wc_adm_access_t *adm_access; SVN_ERR(svn_path_check_valid(path, pool)); /* Make sure something's there. */ SVN_ERR(svn_io_check_path(path, &kind, pool)); if (kind == svn_node_none) return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL, _("'%s' not found"), svn_path_local_style(path, pool)); if (kind == svn_node_unknown) return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, _("Unsupported node kind for path '%s'"), svn_path_local_style(path, pool)); /* Get the original entry for this path if one exists (perhaps this is actually a replacement of a previously deleted thing). Note that this is one of the few functions that is allowed to see 'deleted' entries; it's totally fine to have an entry that is scheduled for addition and still previously 'deleted'. */ SVN_ERR(svn_wc_adm_probe_try3(&adm_access, parent_access, path, TRUE, copyfrom_url != NULL ? -1 : 0, cancel_func, cancel_baton, pool)); if (adm_access) SVN_ERR(svn_wc_entry(&orig_entry, path, adm_access, TRUE, pool)); else orig_entry = NULL; /* You can only add something that is not in revision control, or that is slated for deletion from revision control, or has been previously 'deleted', unless, of course, you're specifying an addition with -history-; then it's okay for the object to be under version control already; it's not really new. */ if (orig_entry) { if ((! copyfrom_url) && (orig_entry->schedule != svn_wc_schedule_delete) && (! orig_entry->deleted)) { return svn_error_createf (SVN_ERR_ENTRY_EXISTS, NULL, _("'%s' is already under version control"), svn_path_local_style(path, pool)); } else if (orig_entry->kind != kind) { /* ### todo: At some point, we obviously don't want to block replacements where the node kind changes. When this happens, svn_wc_revert3() needs to learn how to revert this situation. At present we are using a specific node-change error so that clients can detect it. */ return svn_error_createf (SVN_ERR_WC_NODE_KIND_CHANGE, NULL, _("Can't replace '%s' with a node of a differing type; " "the deletion must be committed and the parent updated " "before adding '%s'"), svn_path_local_style(path, pool), svn_path_local_style(path, pool)); } if (orig_entry->schedule == svn_wc_schedule_delete) is_replace = TRUE; } /* Split off the base_name from the parent directory. */ svn_path_split(path, &parent_dir, &base_name, pool); SVN_ERR(svn_wc_entry(&parent_entry, parent_dir, parent_access, FALSE, pool)); if (! parent_entry) return svn_error_createf (SVN_ERR_ENTRY_NOT_FOUND, NULL, _("Can't find parent directory's entry while trying to add '%s'"), svn_path_local_style(path, pool)); if (parent_entry->schedule == svn_wc_schedule_delete) return svn_error_createf (SVN_ERR_WC_SCHEDULE_CONFLICT, NULL, _("Can't add '%s' to a parent directory scheduled for deletion"), svn_path_local_style(path, pool)); /* Init the modify flags. */ modify_flags = SVN_WC__ENTRY_MODIFY_SCHEDULE | SVN_WC__ENTRY_MODIFY_KIND; if (! (is_replace || copyfrom_url)) modify_flags |= SVN_WC__ENTRY_MODIFY_REVISION; /* If a copy ancestor was given, make sure the copyfrom URL is in the same repository (if possible) and put the proper ancestry info in the new entry */ if (copyfrom_url) { if (parent_entry->repos && ! svn_path_is_ancestor(parent_entry->repos, copyfrom_url)) return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, _("The URL '%s' has a different repository " "root than its parent"), copyfrom_url); tmp_entry.copyfrom_url = copyfrom_url; tmp_entry.copyfrom_rev = copyfrom_rev; tmp_entry.copied = TRUE; modify_flags |= SVN_WC__ENTRY_MODIFY_COPYFROM_URL; modify_flags |= SVN_WC__ENTRY_MODIFY_COPYFROM_REV; modify_flags |= SVN_WC__ENTRY_MODIFY_COPIED; } /* If this is a replacement we want to remove the checksum and the property flags so they are not set to their respective old values. */ if (is_replace) { tmp_entry.checksum = NULL; modify_flags |= SVN_WC__ENTRY_MODIFY_CHECKSUM; tmp_entry.has_props = FALSE; tmp_entry.has_prop_mods = FALSE; modify_flags |= SVN_WC__ENTRY_MODIFY_HAS_PROPS; modify_flags |= SVN_WC__ENTRY_MODIFY_HAS_PROP_MODS; } tmp_entry.revision = 0; tmp_entry.kind = kind; tmp_entry.schedule = svn_wc_schedule_add; /* Now, add the entry for this item to the parent_dir's entries file, marking it for addition. */ SVN_ERR(svn_wc__entry_modify(parent_access, base_name, &tmp_entry, modify_flags, TRUE, pool)); /* If this is a replacement without history, we need to reset the properties for PATH. */ if (orig_entry && (! copyfrom_url)) SVN_ERR(svn_wc__props_delete(path, svn_wc__props_working, adm_access, pool)); if (is_replace) { /* We don't want the old base text (if any) and base props to be mistakenly used as the bases for the new, replacement object. So, move them out of the way. */ /* ### TODO: In an ideal world, this whole function would be loggy. * ### But the directory recursion code below is already tangled * ### enough, and re-doing the code above would require setting * ### up more of tmp_entry. It's more than a SMOP. For now, * ### I'm leaving it be, though we set up the revert base(s) * ### loggily because that's Just How It's Done. */ svn_stringbuf_t *log_accum = svn_stringbuf_create("", pool); if (orig_entry->kind == svn_node_file) { const char *textb = svn_wc__text_base_path(path, FALSE, pool); const char *rtextb = svn_wc__text_revert_path(path, FALSE, pool); SVN_ERR(svn_wc__loggy_move(&log_accum, NULL, adm_access, textb, rtextb, FALSE, pool)); } SVN_ERR(svn_wc__loggy_revert_props_create(&log_accum, path, adm_access, TRUE, pool)); SVN_ERR(svn_wc__write_log(adm_access, 0, log_accum, pool)); SVN_ERR(svn_wc__run_log(adm_access, NULL, pool)); } if (kind == svn_node_dir) /* scheduling a directory for addition */ { if (! copyfrom_url) { const svn_wc_entry_t *p_entry; /* ### why not use parent_entry? */ const char *new_url; /* Get the entry for this directory's parent. We need to snatch the ancestor path out of there. */ SVN_ERR(svn_wc_entry(&p_entry, parent_dir, parent_access, FALSE, pool)); /* Derive the parent path for our new addition here. */ new_url = svn_path_url_add_component(p_entry->url, base_name, pool); /* Make sure this new directory has an admistrative subdirectory created inside of it */ SVN_ERR(svn_wc_ensure_adm3(path, NULL, new_url, p_entry->repos, 0, depth, pool)); } else { /* When we are called with the copyfrom arguments set and with the admin directory already in existence, then the dir will contain the copyfrom settings. So we need to pass the copyfrom arguments to the ensure call. */ SVN_ERR(svn_wc_ensure_adm3(path, NULL, copyfrom_url, parent_entry->repos, copyfrom_rev, depth, pool)); } /* We want the locks to persist, so use the access baton's pool */ if (! orig_entry || orig_entry->deleted) { apr_pool_t* access_pool = svn_wc_adm_access_pool(parent_access); SVN_ERR(svn_wc_adm_open3(&adm_access, parent_access, path, TRUE, copyfrom_url != NULL ? -1 : 0, cancel_func, cancel_baton, access_pool)); } /* We're making the same mods we made above, but this time we'll force the scheduling. Also make sure to undo the 'incomplete' flag which svn_wc_ensure_adm3 sets by default. */ modify_flags |= SVN_WC__ENTRY_MODIFY_FORCE; modify_flags |= SVN_WC__ENTRY_MODIFY_INCOMPLETE; tmp_entry.schedule = is_replace ? svn_wc_schedule_replace : svn_wc_schedule_add; tmp_entry.incomplete = FALSE; SVN_ERR(svn_wc__entry_modify(adm_access, NULL, &tmp_entry, modify_flags, TRUE, pool)); if (copyfrom_url) { /* If this new directory has ancestry, it's not enough to schedule it for addition with copyfrom args. We also need to rewrite its ancestor-url, and rewrite the ancestor-url of ALL its children! We're doing this because our current commit model (for hysterical raisins, presumably) assumes an entry's URL is correct before commit -- i.e. the URL is not tweaked in the post-commit bumping process. We might want to change this model someday. */ /* Figure out what the new url should be. */ const char *new_url = svn_path_url_add_component(parent_entry->url, base_name, pool); /* Change the entry urls recursively (but not the working rev). */ SVN_ERR(svn_wc__do_update_cleanup(path, adm_access, depth, new_url, parent_entry->repos, SVN_INVALID_REVNUM, NULL, NULL, FALSE, apr_hash_make(pool), pool)); /* Recursively add the 'copied' existence flag as well! */ SVN_ERR(mark_tree(adm_access, SVN_WC__ENTRY_MODIFY_COPIED, svn_wc_schedule_normal, TRUE, FALSE, cancel_func, cancel_baton, NULL, NULL, /* N/A cuz we aren't deleting */ pool)); /* Clean out the now-obsolete wcprops. */ SVN_ERR(svn_wc__props_delete(path, svn_wc__props_wcprop, adm_access, pool)); } } /* Report the addition to the caller. */ if (notify_func != NULL) { svn_wc_notify_t *notify = svn_wc_create_notify(path, svn_wc_notify_add, pool); notify->kind = kind; (*notify_func)(notify_baton, notify, pool); } return SVN_NO_ERROR; } svn_error_t * svn_wc_add2(const char *path, svn_wc_adm_access_t *parent_access, const char *copyfrom_url, svn_revnum_t copyfrom_rev, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { return svn_wc_add3(path, parent_access, svn_depth_infinity, copyfrom_url, copyfrom_rev, cancel_func, cancel_baton, notify_func, notify_baton, pool); } svn_error_t * svn_wc_add(const char *path, svn_wc_adm_access_t *parent_access, const char *copyfrom_url, svn_revnum_t copyfrom_rev, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func_t notify_func, void *notify_baton, apr_pool_t *pool) { svn_wc__compat_notify_baton_t nb; nb.func = notify_func; nb.baton = notify_baton; return svn_wc_add2(path, parent_access, copyfrom_url, copyfrom_rev, cancel_func, cancel_baton, svn_wc__compat_call_notify_func, &nb, pool); } /* Thoughts on Reversion. What does is mean to revert a given PATH in a tree? We'll consider things by their modifications. Adds - For files, svn_wc_remove_from_revision_control(), baby. - Added directories may contain nothing but added children, and reverting the addition of a directory necessarily means reverting the addition of all the directory's children. Again, svn_wc_remove_from_revision_control() should do the trick. Deletes - Restore properties to their unmodified state. - For files, restore the pristine contents, and reset the schedule to 'normal'. - For directories, reset the schedule to 'normal'. All children of a directory marked for deletion must also be marked for deletion, but it's okay for those children to remain deleted even if their parent directory is restored. That's what the recursive flag is for. Replaces - Restore properties to their unmodified state. - For files, restore the pristine contents, and reset the schedule to 'normal'. - For directories, reset the schedule to normal. A replaced directory can have deleted children (left over from the initial deletion), replaced children (children of the initial deletion now re-added), and added children (new entries under the replaced directory). Since this is technically an addition, it necessitates recursion. Modifications - Restore properties and, for files, contents to their unmodified state. */ /* Revert ENTRY for NAME in directory represented by ADM_ACCESS. Set *REVERTED to TRUE if something (text or props or both) is reverted, FALSE otherwise. If something is reverted and USE_COMMIT_TIMES is true, then update the entry's timestamp to the last-committed-time; otherwise don't do that. Use SVN_WC_ENTRY_THIS_DIR as NAME for reverting ADM_ACCESS directory itself. Use POOL for any temporary allocations.*/ static svn_error_t * revert_admin_things(svn_wc_adm_access_t *adm_access, const char *name, const svn_wc_entry_t *entry, svn_boolean_t *reverted, svn_boolean_t use_commit_times, apr_pool_t *pool) { const char *fullpath; svn_boolean_t reinstall_working = FALSE; /* force working file reinstall? */ svn_wc_entry_t tmp_entry; apr_uint64_t flags = 0; svn_stringbuf_t *log_accum = svn_stringbuf_create("", pool); apr_hash_t *baseprops = NULL; svn_boolean_t revert_base = FALSE; /* By default, assume no action; we'll see what happens later. */ *reverted = FALSE; /* Build the full path of the thing we're reverting. */ fullpath = svn_wc_adm_access_path(adm_access); if (strcmp(name, SVN_WC_ENTRY_THIS_DIR) != 0) fullpath = svn_path_join(fullpath, name, pool); /* Deal with properties. */ if (entry->schedule == svn_wc_schedule_replace) { /* Refer to the original base, before replacement. */ revert_base = TRUE; /* Use the revertpath as the new propsbase if it exists. */ baseprops = apr_hash_make(pool); SVN_ERR(svn_wc__load_props(NULL, NULL, &baseprops, adm_access, fullpath, pool)); /* Ensure the revert propfile gets removed. */ SVN_ERR(svn_wc__loggy_props_delete(&log_accum, fullpath, svn_wc__props_revert, adm_access, pool)); *reverted = TRUE; } /* If not schedule replace, or no revert props, use the normal base-props and working props. */ if (! baseprops) { svn_boolean_t modified; /* Check for prop changes. */ SVN_ERR(svn_wc_props_modified_p(&modified, fullpath, adm_access, pool)); if (modified) { apr_array_header_t *propchanges; /* Get the full list of property changes and see if any magic properties were changed. */ SVN_ERR(svn_wc_get_prop_diffs(&propchanges, &baseprops, fullpath, adm_access, pool)); /* Determine if any of the propchanges are the "magic" ones that might require changing the working file. */ reinstall_working = svn_wc__has_magic_property(propchanges); } } /* Reinstall props if we need to. Only rewrite the baseprops, if we're reverting a replacement. This is just an optimization. */ if (baseprops) { SVN_ERR(svn_wc__install_props(&log_accum, adm_access, fullpath, baseprops, baseprops, revert_base, pool)); *reverted = TRUE; } /* Deal with the contents. */ if (entry->kind == svn_node_file) { svn_node_kind_t kind; const char *regular_base_path = svn_wc__text_base_path(fullpath, FALSE, pool); /* This becomes NULL if there is no revert-base. */ const char *revert_base_path = svn_wc__text_revert_path(fullpath, FALSE, pool); if (! reinstall_working) { /* If the working file is missing, we need to reinstall it. */ SVN_ERR(svn_io_check_path(fullpath, &kind, pool)); if (kind == svn_node_none) reinstall_working = TRUE; } /* Whether or not the working file was missing, if there is a revert text-base, we'll need to put it back to the regular text-base and then reinstall the working file. (Strictly speaking, the working file could be unmodified w.r.t. the revert-base, but discovering that would be costly and chances are it's not the case anyway. So we just assume. */ SVN_ERR(svn_io_check_path(revert_base_path, &kind, pool)); if (kind == svn_node_file) { reinstall_working = TRUE; } else if (kind == svn_node_none) { SVN_ERR(svn_io_check_path(regular_base_path, &kind, pool)); if (kind != svn_node_file) { /* A real file must have either a regular or a revert text-base. If it has neither, we could be looking at the situation described in issue #2101, in which case all we can do is deliver the expected error. */ return svn_error_createf(APR_ENOENT, NULL, _("Error restoring text for '%s'"), svn_path_local_style(fullpath, pool)); } else { revert_base_path = NULL; } } else { return svn_error_createf (SVN_ERR_NODE_UNKNOWN_KIND, NULL, _("unexpected kind for revert-base '%s'"), svn_path_local_style(revert_base_path, pool)); } /* You'd think we could just write out one log command to move the revert base (if any) to the regular base, then another to copy-and-translate the regular base to the working file. Unfortunately, svn_wc__loggy_copy() doesn't actually write out a copy instruction if the src file for the copy isn't present *at the time the log is being composed*. See that function's documentation for details. So instead, we write out a log command to copy-and-translate the revert text-base to the working file, then another log command to move the revert text-base to the regular text-base. */ if (revert_base_path) { SVN_ERR(svn_wc__loggy_copy (&log_accum, NULL, adm_access, svn_wc__copy_translate, revert_base_path, fullpath, FALSE, pool)); SVN_ERR(svn_wc__loggy_move (&log_accum, NULL, adm_access, revert_base_path, regular_base_path, FALSE, pool)); *reverted = TRUE; } else { /* No revert-base -- so don't assume reinstall_working either. */ if (! reinstall_working) { SVN_ERR(svn_wc__text_modified_internal_p (&reinstall_working, fullpath, FALSE, adm_access, FALSE, pool)); } if (reinstall_working) { SVN_ERR(svn_wc__loggy_copy (&log_accum, NULL, adm_access, svn_wc__copy_translate, regular_base_path, fullpath, FALSE, pool)); *reverted = TRUE; } } /* If we reinstalled the working file, then maybe update the text timestamp in the entries file. */ if (reinstall_working) { /* Possibly set the timestamp to last-commit-time, rather than the 'now' time that already exists. */ if (use_commit_times && entry->cmt_date) SVN_ERR(svn_wc__loggy_set_timestamp (&log_accum, adm_access, fullpath, svn_time_to_cstring(entry->cmt_date, pool), pool)); SVN_ERR(svn_wc__loggy_set_entry_timestamp_from_wc (&log_accum, adm_access, fullpath, SVN_WC__ENTRY_ATTR_TEXT_TIME, pool)); SVN_ERR(svn_wc__loggy_set_entry_working_size_from_wc (&log_accum, adm_access, fullpath, pool)); } } /* Remove conflict state (and conflict files), if any. Handle the three possible text conflict files. */ if (entry->conflict_old) { flags |= SVN_WC__ENTRY_MODIFY_CONFLICT_OLD; tmp_entry.conflict_old = NULL; SVN_ERR(svn_wc__loggy_remove (&log_accum, adm_access, svn_path_join(svn_wc_adm_access_path(adm_access), entry->conflict_old, pool), pool)); } if (entry->conflict_new) { flags |= SVN_WC__ENTRY_MODIFY_CONFLICT_NEW; tmp_entry.conflict_new = NULL; SVN_ERR(svn_wc__loggy_remove (&log_accum, adm_access, svn_path_join(svn_wc_adm_access_path(adm_access), entry->conflict_new, pool), pool)); } if (entry->conflict_wrk) { flags |= SVN_WC__ENTRY_MODIFY_CONFLICT_WRK; tmp_entry.conflict_wrk = NULL; SVN_ERR(svn_wc__loggy_remove (&log_accum, adm_access, svn_path_join(svn_wc_adm_access_path(adm_access), entry->conflict_wrk, pool), pool)); } /* Remove the property conflict file if the entry lists one (and it exists) */ if (entry->prejfile) { flags |= SVN_WC__ENTRY_MODIFY_PREJFILE; tmp_entry.prejfile = NULL; SVN_ERR(svn_wc__loggy_remove (&log_accum, adm_access, svn_path_join(svn_wc_adm_access_path(adm_access), entry->prejfile, pool), pool)); } /* Clean up the copied state if this is a replacement. */ if (entry->schedule == svn_wc_schedule_replace) { flags |= SVN_WC__ENTRY_MODIFY_COPIED | SVN_WC__ENTRY_MODIFY_COPYFROM_URL | SVN_WC__ENTRY_MODIFY_COPYFROM_REV; tmp_entry.copied = FALSE; /* Reset the checksum if this is a replace-with-history. */ if (entry->kind == svn_node_file && entry->copyfrom_url) { const char *base_path; svn_checksum_t *checksum; base_path = svn_wc__text_revert_path(fullpath, FALSE, pool); SVN_ERR(svn_io_file_checksum2(&checksum, base_path, svn_checksum_md5, pool)); tmp_entry.checksum = svn_checksum_to_cstring(checksum, pool); flags |= SVN_WC__ENTRY_MODIFY_CHECKSUM; } /* Set this to the empty string, because NULL values will disappear in the XML log file. */ tmp_entry.copyfrom_url = ""; tmp_entry.copyfrom_rev = SVN_INVALID_REVNUM; } /* Reset schedule attribute to svn_wc_schedule_normal. */ if (entry->schedule != svn_wc_schedule_normal) { flags |= SVN_WC__ENTRY_MODIFY_SCHEDULE; tmp_entry.schedule = svn_wc_schedule_normal; *reverted = TRUE; } /* If the entry is for this_dir, delete tree conflict data. */ if ((strcmp(name, SVN_WC_ENTRY_THIS_DIR) == 0) && entry->tree_conflict_data) { flags |= SVN_WC__ENTRY_MODIFY_TREE_CONFLICT_DATA; tmp_entry.tree_conflict_data = NULL; *reverted = TRUE; } /* Modify the entry, loggily. */ SVN_ERR(svn_wc__loggy_entry_modify(&log_accum, adm_access, fullpath, &tmp_entry, flags, pool)); /* Don't run log if nothing to change. */ if (! svn_stringbuf_isempty(log_accum)) { 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; } /* Revert PATH of on-disk KIND. ENTRY is the working copy entry for PATH. *DEPTH is the depth of the reversion crawl the caller is using; this function may choose to override that value as needed. See svn_wc_revert3() for the interpretations of PARENT_ACCESS, USE_COMMIT_TIMES, CANCEL_FUNC, CANCEL_BATON, NOTIFY_FUNC, and NOTIFY_BATON. Use POOL for allocations. */ static svn_error_t * revert_entry(svn_depth_t *depth, const char *path, svn_node_kind_t kind, const svn_wc_entry_t *entry, svn_wc_adm_access_t *parent_access, svn_boolean_t use_commit_times, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { const char *bname; svn_boolean_t is_wc_root = FALSE; svn_wc_adm_access_t *dir_access; /* Initialize this even though revert_admin_things() is guaranteed to set it, because we don't know that revert_admin_things() will be called. */ svn_boolean_t reverted = FALSE; /* Fetch the access baton for this path. */ SVN_ERR(svn_wc_adm_probe_retrieve(&dir_access, parent_access, path, pool)); /* For directories, determine if PATH is a WC root so that we can tell if it is safe to split PATH into a parent directory and basename. For files, we always do this split. */ if (kind == svn_node_dir) SVN_ERR(svn_wc_is_wc_root(&is_wc_root, path, dir_access, pool)); bname = is_wc_root ? NULL : svn_path_basename(path, pool); /* Additions. */ if (entry->schedule == svn_wc_schedule_add) { /* Before removing item from revision control, notice if the entry is in a 'deleted' state; this is critical for directories, where this state only exists in its parent's entry. */ svn_boolean_t was_deleted = FALSE; const char *parent, *basey; svn_path_split(path, &parent, &basey, pool); if (entry->kind == svn_node_file) { was_deleted = entry->deleted; SVN_ERR(svn_wc_remove_from_revision_control(parent_access, bname, FALSE, FALSE, cancel_func, cancel_baton, pool)); } else if (entry->kind == svn_node_dir) { apr_hash_t *entries; const svn_wc_entry_t *parents_entry; /* We are trying to revert the current directory which is scheduled for addition. This is supposed to fail (Issue #854) */ if (path[0] == '\0') return svn_error_create(SVN_ERR_WC_INVALID_OP_ON_CWD, NULL, _("Cannot revert addition of current " "directory; please try again from the " "parent directory")); SVN_ERR(svn_wc_entries_read(&entries, parent_access, TRUE, pool)); parents_entry = apr_hash_get(entries, basey, APR_HASH_KEY_STRING); if (parents_entry) was_deleted = parents_entry->deleted; if (kind == svn_node_none || svn_wc__adm_missing(parent_access, path)) { /* Schedule add but missing, just remove the entry or it's missing an adm area in which case svn_wc_adm_probe_retrieve() returned the parent's adm_access, for which we definitely can't use the 'else' code path (as it will remove the parent from version control... (See issue 2425) */ svn_wc__entry_remove(entries, basey); SVN_ERR(svn_wc__entries_write(entries, parent_access, pool)); } else { SVN_ERR(svn_wc_remove_from_revision_control (dir_access, SVN_WC_ENTRY_THIS_DIR, FALSE, FALSE, cancel_func, cancel_baton, pool)); } } else /* Else it's `none', or something exotic like a symlink... */ { return svn_error_createf(SVN_ERR_NODE_UNKNOWN_KIND, NULL, _("Unknown or unexpected kind for path " "'%s'"), svn_path_local_style(path, pool)); } /* Recursivity is taken care of by svn_wc_remove_from_revision_control, and we've definitely reverted PATH at this point. */ *depth = svn_depth_empty; reverted = TRUE; /* If the removed item was *also* in a 'deleted' state, make sure we leave just a plain old 'deleted' entry behind in the parent. */ if (was_deleted) { svn_wc_entry_t *tmpentry; /* ### FIXME: Why the heap alloc? */ tmpentry = apr_pcalloc(pool, sizeof(*tmpentry)); tmpentry->kind = entry->kind; tmpentry->deleted = TRUE; if (entry->kind == svn_node_dir) SVN_ERR(svn_wc__entry_modify(parent_access, basey, tmpentry, SVN_WC__ENTRY_MODIFY_KIND | SVN_WC__ENTRY_MODIFY_DELETED, TRUE, pool)); else SVN_ERR(svn_wc__entry_modify(parent_access, bname, tmpentry, SVN_WC__ENTRY_MODIFY_KIND | SVN_WC__ENTRY_MODIFY_DELETED, TRUE, pool)); } } /* Regular prop and text edit. */ /* Deletions and replacements. */ else if (entry->schedule == svn_wc_schedule_normal || entry->schedule == svn_wc_schedule_delete || entry->schedule == svn_wc_schedule_replace) { /* Revert the prop and text mods (if any). */ switch (entry->kind) { case svn_node_file: SVN_ERR(revert_admin_things(parent_access, bname, entry, &reverted, use_commit_times, pool)); break; case svn_node_dir: SVN_ERR(revert_admin_things(dir_access, SVN_WC_ENTRY_THIS_DIR, entry, &reverted, use_commit_times, pool)); /* Also revert the entry in the parent (issue #2804). */ if (reverted && bname) { svn_boolean_t dummy_reverted; svn_wc_entry_t *entry_in_parent; apr_hash_t *entries; SVN_ERR(svn_wc_entries_read(&entries, parent_access, TRUE, pool)); entry_in_parent = apr_hash_get(entries, bname, APR_HASH_KEY_STRING); SVN_ERR(revert_admin_things(parent_access, bname, entry_in_parent, &dummy_reverted, use_commit_times, pool)); } /* Force recursion on replaced directories. */ if (entry->schedule == svn_wc_schedule_replace) *depth = svn_depth_infinity; break; default: /* No op? */ break; } } /* If PATH was reverted, tell our client that. */ if ((notify_func != NULL) && reverted) (*notify_func)(notify_baton, svn_wc_create_notify(path, svn_wc_notify_revert, pool), pool); return SVN_NO_ERROR; } /* This is just the guts of svn_wc_revert3() save that it accepts a hash CHANGELIST_HASH whose keys are changelist names instead of an array of said names. See svn_wc_revert3() for additional documentation. */ static svn_error_t * revert_internal(const char *path, svn_wc_adm_access_t *parent_access, svn_depth_t depth, svn_boolean_t use_commit_times, apr_hash_t *changelist_hash, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { svn_node_kind_t kind; const svn_wc_entry_t *entry; svn_wc_adm_access_t *dir_access; /* Check cancellation here, so recursive calls get checked early. */ if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); /* Fetch the access baton for this path. */ SVN_ERR(svn_wc_adm_probe_retrieve(&dir_access, parent_access, path, pool)); /* Safeguard 1: is this a versioned resource? */ SVN_ERR_W(svn_wc__entry_versioned(&entry, path, dir_access, FALSE, pool), _("Cannot revert")); /* Safeguard 1.5: is this a missing versioned directory? */ if (entry->kind == svn_node_dir) { svn_node_kind_t disk_kind; SVN_ERR(svn_io_check_path(path, &disk_kind, pool)); if ((disk_kind != svn_node_dir) && (entry->schedule != svn_wc_schedule_add)) { /* When the directory itself is missing, we can't revert without hitting the network. Someday a '--force' option will make this happen. For now, send notification of the failure. */ if (notify_func != NULL) { svn_wc_notify_t *notify = svn_wc_create_notify(path, svn_wc_notify_failed_revert, pool); notify_func(notify_baton, notify, pool); } return SVN_NO_ERROR; } } /* Safeguard 2: can we handle this entry's recorded kind? */ if ((entry->kind != svn_node_file) && (entry->kind != svn_node_dir)) return svn_error_createf (SVN_ERR_UNSUPPORTED_FEATURE, NULL, _("Cannot revert '%s': unsupported entry node kind"), svn_path_local_style(path, pool)); /* Safeguard 3: can we deal with the node kind of PATH currently in the working copy? */ SVN_ERR(svn_io_check_path(path, &kind, pool)); if ((kind != svn_node_none) && (kind != svn_node_file) && (kind != svn_node_dir)) return svn_error_createf (SVN_ERR_UNSUPPORTED_FEATURE, NULL, _("Cannot revert '%s': unsupported node kind in working copy"), svn_path_local_style(path, pool)); /* If the entry passes changelist filtering, revert it! */ if (SVN_WC__CL_MATCH(changelist_hash, entry)) { /* Actually revert this entry. If this is a working copy root, we provide a base_name from the parent path. */ SVN_ERR(revert_entry(&depth, path, kind, entry, parent_access, use_commit_times, cancel_func, cancel_baton, notify_func, notify_baton, pool)); } /* Finally, recurse if requested. */ if (entry->kind == svn_node_dir && depth > svn_depth_empty) { apr_hash_t *entries; apr_hash_index_t *hi; apr_pool_t *subpool = svn_pool_create(pool); SVN_ERR(svn_wc_entries_read(&entries, dir_access, FALSE, pool)); for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) { const void *key; const char *keystring; void *val; const char *full_entry_path; svn_depth_t depth_under_here = depth; svn_wc_entry_t *child_entry; if (depth == svn_depth_files || depth == svn_depth_immediates) depth_under_here = svn_depth_empty; svn_pool_clear(subpool); /* Get the next entry */ apr_hash_this(hi, &key, NULL, &val); keystring = key; child_entry = val; /* Skip "this dir" */ if (! strcmp(keystring, SVN_WC_ENTRY_THIS_DIR)) continue; /* Skip subdirectories if we're called with depth-files. */ if ((depth == svn_depth_files) && (child_entry->kind != svn_node_file)) continue; /* Add the entry name to FULL_ENTRY_PATH. */ full_entry_path = svn_path_join(path, keystring, subpool); /* Revert the entry. */ SVN_ERR(revert_internal(full_entry_path, dir_access, depth_under_here, use_commit_times, changelist_hash, cancel_func, cancel_baton, notify_func, notify_baton, subpool)); } svn_pool_destroy(subpool); } return SVN_NO_ERROR; } svn_error_t * svn_wc_revert3(const char *path, svn_wc_adm_access_t *parent_access, svn_depth_t depth, svn_boolean_t use_commit_times, const apr_array_header_t *changelists, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { apr_hash_t *changelist_hash = NULL; if (changelists && changelists->nelts) SVN_ERR(svn_hash_from_cstring_keys(&changelist_hash, changelists, pool)); return revert_internal(path, parent_access, depth, use_commit_times, changelist_hash, cancel_func, cancel_baton, notify_func, notify_baton, pool); } svn_error_t * svn_wc_revert2(const char *path, svn_wc_adm_access_t *parent_access, svn_boolean_t recursive, svn_boolean_t use_commit_times, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { return svn_wc_revert3(path, parent_access, SVN_DEPTH_INFINITY_OR_EMPTY(recursive), use_commit_times, NULL, cancel_func, cancel_baton, notify_func, notify_baton, pool); } svn_error_t * svn_wc_revert(const char *path, svn_wc_adm_access_t *parent_access, svn_boolean_t recursive, svn_boolean_t use_commit_times, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func_t notify_func, void *notify_baton, apr_pool_t *pool) { svn_wc__compat_notify_baton_t nb; nb.func = notify_func; nb.baton = notify_baton; return svn_wc_revert2(path, parent_access, recursive, use_commit_times, cancel_func, cancel_baton, svn_wc__compat_call_notify_func, &nb, pool); } svn_error_t * svn_wc_get_pristine_copy_path(const char *path, const char **pristine_path, apr_pool_t *pool) { *pristine_path = svn_wc__text_base_path(path, FALSE, pool); return SVN_NO_ERROR; } svn_error_t * svn_wc_get_pristine_contents(svn_stream_t **contents, const char *path, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { const char *text_base = svn_wc__text_base_path(path, FALSE, scratch_pool); if (text_base == NULL) { *contents = NULL; return SVN_NO_ERROR; } return svn_stream_open_readonly(contents, text_base, result_pool, scratch_pool); } svn_error_t * svn_wc_remove_from_revision_control(svn_wc_adm_access_t *adm_access, const char *name, svn_boolean_t destroy_wf, svn_boolean_t instant_error, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *pool) { svn_error_t *err; svn_boolean_t is_file; svn_boolean_t left_something = FALSE; apr_hash_t *entries = NULL; const char *full_path = apr_pstrdup(pool, svn_wc_adm_access_path(adm_access)); /* Check cancellation here, so recursive calls get checked early. */ if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); /* NAME is either a file's basename or SVN_WC_ENTRY_THIS_DIR. */ is_file = (strcmp(name, SVN_WC_ENTRY_THIS_DIR)) ? TRUE : FALSE; if (is_file) { svn_node_kind_t kind; svn_boolean_t wc_special, local_special; svn_boolean_t text_modified_p; full_path = svn_path_join(full_path, name, pool); /* Only check if the file was modified when it wasn't overwritten with a special file */ SVN_ERR(svn_wc__get_special(&wc_special, full_path, adm_access, pool)); SVN_ERR(svn_io_check_special_path(full_path, &kind, &local_special, pool)); if (wc_special || ! local_special) { /* Check for local mods. before removing entry */ SVN_ERR(svn_wc_text_modified_p(&text_modified_p, full_path, FALSE, adm_access, pool)); if (text_modified_p && instant_error) return svn_error_createf(SVN_ERR_WC_LEFT_LOCAL_MOD, NULL, _("File '%s' has local modifications"), svn_path_local_style(full_path, pool)); } /* Remove the wcprops. */ SVN_ERR(svn_wc__props_delete(full_path, svn_wc__props_wcprop, adm_access, pool)); /* Remove prop/NAME, prop-base/NAME.svn-base. */ SVN_ERR(svn_wc__props_delete(full_path, svn_wc__props_working, adm_access, pool)); SVN_ERR(svn_wc__props_delete(full_path, svn_wc__props_base, adm_access, pool)); /* Remove NAME from PATH's entries file: */ SVN_ERR(svn_wc_entries_read(&entries, adm_access, TRUE, pool)); svn_wc__entry_remove(entries, name); SVN_ERR(svn_wc__entries_write(entries, adm_access, pool)); /* Remove text-base/NAME.svn-base */ SVN_ERR(remove_file_if_present(svn_wc__text_base_path(full_path, 0, pool), pool)); /* If we were asked to destroy the working file, do so unless it has local mods. */ if (destroy_wf) { /* Don't kill local mods. */ if (text_modified_p || (! wc_special && local_special)) return svn_error_create(SVN_ERR_WC_LEFT_LOCAL_MOD, NULL, NULL); else /* The working file is still present; remove it. */ SVN_ERR(remove_file_if_present(full_path, pool)); } } /* done with file case */ else /* looking at THIS_DIR */ { apr_pool_t *subpool = svn_pool_create(pool); apr_hash_index_t *hi; svn_wc_entry_t incomplete_entry; /* ### sanity check: check 2 places for DELETED flag? */ /* Before we start removing entries from this dir's entries file, mark this directory as "incomplete". This allows this function to be interruptible and the wc recoverable by 'svn up' later on. */ incomplete_entry.incomplete = TRUE; SVN_ERR(svn_wc__entry_modify(adm_access, SVN_WC_ENTRY_THIS_DIR, &incomplete_entry, SVN_WC__ENTRY_MODIFY_INCOMPLETE, TRUE, /* sync to disk immediately */ pool)); /* Get rid of all the wcprops in this directory. This avoids rewriting the wcprops file over and over (meaning O(n^2) complexity) below. */ SVN_ERR(svn_wc__props_delete(full_path, svn_wc__props_wcprop, adm_access, pool)); /* Walk over every entry. */ SVN_ERR(svn_wc_entries_read(&entries, adm_access, FALSE, pool)); for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) { const void *key; void *val; const char *current_entry_name; const svn_wc_entry_t *current_entry; svn_pool_clear(subpool); apr_hash_this(hi, &key, NULL, &val); current_entry = val; if (! strcmp(key, SVN_WC_ENTRY_THIS_DIR)) current_entry_name = NULL; else current_entry_name = key; if (current_entry->kind == svn_node_file) { err = svn_wc_remove_from_revision_control (adm_access, current_entry_name, destroy_wf, instant_error, cancel_func, cancel_baton, subpool); if (err && (err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)) { if (instant_error) { return err; } else { svn_error_clear(err); left_something = TRUE; } } else if (err) return err; } else if (current_entry_name && (current_entry->kind == svn_node_dir)) { svn_wc_adm_access_t *entry_access; const char *entrypath = svn_path_join(svn_wc_adm_access_path(adm_access), current_entry_name, subpool); if (svn_wc__adm_missing(adm_access, entrypath)) { /* The directory is already missing, so don't try to recurse, just delete the entry in the parent directory. */ svn_wc__entry_remove(entries, current_entry_name); } else { SVN_ERR(svn_wc_adm_retrieve(&entry_access, adm_access, entrypath, subpool)); err = svn_wc_remove_from_revision_control (entry_access, SVN_WC_ENTRY_THIS_DIR, destroy_wf, instant_error, cancel_func, cancel_baton, subpool); if (err && (err->apr_err == SVN_ERR_WC_LEFT_LOCAL_MOD)) { if (instant_error) { return err; } else { svn_error_clear(err); left_something = TRUE; } } else if (err) return err; } } } /* At this point, every directory below this one has been removed from revision control. */ /* Remove self from parent's entries file, but only if parent is a working copy. If it's not, that's fine, we just move on. */ { const char *parent_dir, *base_name; svn_boolean_t is_root; SVN_ERR(svn_wc_is_wc_root(&is_root, full_path, adm_access, pool)); /* If full_path is not the top of a wc, then its parent directory is also a working copy and has an entry for full_path. We need to remove that entry: */ if (! is_root) { apr_hash_t *parent_entries; svn_wc_adm_access_t *parent_access; svn_path_split(full_path, &parent_dir, &base_name, pool); SVN_ERR(svn_wc_adm_retrieve(&parent_access, adm_access, parent_dir, pool)); SVN_ERR(svn_wc_entries_read(&parent_entries, parent_access, TRUE, pool)); svn_wc__entry_remove(parent_entries, base_name); SVN_ERR(svn_wc__entries_write(parent_entries, parent_access, pool)); } } /* Remove the entire administrative .svn area, thereby removing _this_ dir from revision control too. */ SVN_ERR(svn_wc__adm_destroy(adm_access, subpool)); /* If caller wants us to recursively nuke everything on disk, go ahead, provided that there are no dangling local-mod files below */ if (destroy_wf && (! left_something)) { /* If the dir is *truly* empty (i.e. has no unversioned resources, all versioned files are gone, all .svn dirs are gone, and contains nothing but empty dirs), then a *non*-recursive dir_remove should work. If it doesn't, no big deal. Just assume there are unversioned items in there and set "left_something" */ err = svn_io_dir_remove_nonrecursive (svn_wc_adm_access_path(adm_access), subpool); if (err) { left_something = TRUE; svn_error_clear(err); } } svn_pool_destroy(subpool); } /* end of directory case */ if (left_something) return svn_error_create(SVN_ERR_WC_LEFT_LOCAL_MOD, NULL, NULL); else return SVN_NO_ERROR; } /*** Resolving a conflict automatically ***/ /* Helper for resolve_conflict_on_entry. Delete the file BASE_NAME in PARENT_DIR if it exists. Set WAS_PRESENT to TRUE if the file existed, and to FALSE otherwise. */ static svn_error_t * attempt_deletion(const char *parent_dir, const char *base_name, svn_boolean_t *was_present, apr_pool_t *pool) { const char *full_path = svn_path_join(parent_dir, base_name, pool); svn_error_t *err = svn_io_remove_file(full_path, pool); *was_present = ! err || ! APR_STATUS_IS_ENOENT(err->apr_err); if (*was_present) return err; svn_error_clear(err); return SVN_NO_ERROR; } /* Conflict resolution involves removing the conflict files, if they exist, and clearing the conflict filenames from the entry. The latter needs to be done whether or not the conflict files exist. PATH is the path to the item to be resolved, BASE_NAME is the basename of PATH, and CONFLICT_DIR is the access baton for PATH. ORIG_ENTRY is the entry prior to resolution. RESOLVE_TEXT, RESOLVE_PROPS and RESOLVE_TREE are TRUE if text, property and tree conflicts respectively are to be resolved. See svn_wc_resolved_conflict3() for how CONFLICT_CHOICE behaves. */ static svn_error_t * resolve_conflict_on_entry(const char *path, const svn_wc_entry_t *orig_entry, svn_wc_adm_access_t *conflict_dir, const char *base_name, svn_boolean_t resolve_text, svn_boolean_t resolve_props, svn_boolean_t resolve_tree, svn_wc_conflict_choice_t conflict_choice, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { svn_boolean_t was_present, need_feedback = FALSE; apr_uint64_t modify_flags = 0; svn_wc_entry_t *entry = svn_wc_entry_dup(orig_entry, pool); if (resolve_text) { const char *auto_resolve_src; /* Handle automatic conflict resolution before the temporary files are * deleted, if necessary. */ switch (conflict_choice) { case svn_wc_conflict_choose_base: auto_resolve_src = entry->conflict_old; break; case svn_wc_conflict_choose_mine_full: auto_resolve_src = entry->conflict_wrk; break; case svn_wc_conflict_choose_theirs_full: auto_resolve_src = entry->conflict_new; break; case svn_wc_conflict_choose_merged: auto_resolve_src = NULL; break; case svn_wc_conflict_choose_theirs_conflict: case svn_wc_conflict_choose_mine_conflict: { if (entry->conflict_old && entry->conflict_wrk && entry->conflict_new) { apr_file_t *tmp_f; svn_stream_t *tmp_stream; svn_diff_t *diff; svn_diff_conflict_display_style_t style = conflict_choice == svn_wc_conflict_choose_theirs_conflict ? svn_diff_conflict_display_latest : svn_diff_conflict_display_modified; SVN_ERR(svn_wc_create_tmp_file2(&tmp_f, &auto_resolve_src, svn_wc_adm_access_path(conflict_dir), svn_io_file_del_none, pool)); tmp_stream = svn_stream_from_aprfile2(tmp_f, FALSE, pool); SVN_ERR(svn_diff_file_diff3_2(&diff, entry->conflict_old, entry->conflict_wrk, entry->conflict_new, svn_diff_file_options_create(pool), pool)); SVN_ERR(svn_diff_file_output_merge2(tmp_stream, diff, entry->conflict_old, entry->conflict_wrk, entry->conflict_new, /* markers ignored */ NULL, NULL, NULL, NULL, style, pool)); SVN_ERR(svn_stream_close(tmp_stream)); } else auto_resolve_src = NULL; break; } default: return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, _("Invalid 'conflict_result' argument")); } if (auto_resolve_src) SVN_ERR(svn_io_copy_file( svn_path_join(svn_wc_adm_access_path(conflict_dir), auto_resolve_src, pool), path, TRUE, pool)); } /* Yes indeed, being able to map a function over a list would be nice. */ if (resolve_text && entry->conflict_old) { SVN_ERR(attempt_deletion(svn_wc_adm_access_path(conflict_dir), entry->conflict_old, &was_present, pool)); modify_flags |= SVN_WC__ENTRY_MODIFY_CONFLICT_OLD; entry->conflict_old = NULL; need_feedback |= was_present; } if (resolve_text && entry->conflict_new) { SVN_ERR(attempt_deletion(svn_wc_adm_access_path(conflict_dir), entry->conflict_new, &was_present, pool)); modify_flags |= SVN_WC__ENTRY_MODIFY_CONFLICT_NEW; entry->conflict_new = NULL; need_feedback |= was_present; } if (resolve_text && entry->conflict_wrk) { SVN_ERR(attempt_deletion(svn_wc_adm_access_path(conflict_dir), entry->conflict_wrk, &was_present, pool)); modify_flags |= SVN_WC__ENTRY_MODIFY_CONFLICT_WRK; entry->conflict_wrk = NULL; need_feedback |= was_present; } if (resolve_props && entry->prejfile) { SVN_ERR(attempt_deletion(svn_wc_adm_access_path(conflict_dir), entry->prejfile, &was_present, pool)); modify_flags |= SVN_WC__ENTRY_MODIFY_PREJFILE; entry->prejfile = NULL; need_feedback |= was_present; } if (resolve_tree && (entry->kind == svn_node_dir) && entry->tree_conflict_data) { modify_flags |= SVN_WC__ENTRY_MODIFY_TREE_CONFLICT_DATA; entry->tree_conflict_data = NULL; need_feedback = TRUE; } if (modify_flags) { /* Although removing the files is sufficient to indicate that the conflict is resolved, if we update the entry as well future checks for conflict state will be more efficient. */ SVN_ERR(svn_wc__entry_modify (conflict_dir, (entry->kind == svn_node_dir ? NULL : base_name), entry, modify_flags, TRUE, pool)); /* No feedback if no files were deleted and all we did was change the entry, such a file did not appear as a conflict */ if (need_feedback && notify_func) { /* Sanity check: see if libsvn_wc *still* thinks this item is in a state of conflict that we have asked to resolve. If not, report the successful resolution. */ svn_boolean_t text_conflict, prop_conflict, tree_conflict; SVN_ERR(svn_wc_conflicted_p2(&text_conflict, &prop_conflict, &tree_conflict, svn_wc_adm_access_path(conflict_dir), entry, pool)); if ((! (resolve_text && text_conflict)) && (! (resolve_props && prop_conflict)) && (! (resolve_tree && tree_conflict))) (*notify_func)(notify_baton, svn_wc_create_notify(path, svn_wc_notify_resolved, pool), pool); } } return SVN_NO_ERROR; } /* Machinery for an automated entries walk... */ struct resolve_callback_baton { /* TRUE if text conflicts are to be resolved. */ svn_boolean_t resolve_text; /* TRUE if property conflicts are to be resolved. */ svn_boolean_t resolve_props; /* TRUE if tree conflicts are to be resolved. */ svn_boolean_t resolve_tree; /* The type of automatic conflict resolution to perform */ svn_wc_conflict_choice_t conflict_choice; /* An access baton for the tree, with write access */ svn_wc_adm_access_t *adm_access; /* Notification function and baton */ svn_wc_notify_func2_t notify_func; void *notify_baton; }; static svn_error_t * resolve_found_entry_callback(const char *path, const svn_wc_entry_t *entry, void *walk_baton, apr_pool_t *pool) { struct resolve_callback_baton *baton = walk_baton; const char *conflict_dir, *base_name = NULL; svn_wc_adm_access_t *adm_access; /* We're going to receive dirents twice; we want to ignore the first one (where it's a child of a parent dir), and only print the second one (where we're looking at THIS_DIR). */ if ((entry->kind == svn_node_dir) && (strcmp(entry->name, SVN_WC_ENTRY_THIS_DIR))) return SVN_NO_ERROR; /* Figger out the directory in which the conflict resides. */ if (entry->kind == svn_node_dir) conflict_dir = path; else svn_path_split(path, &conflict_dir, &base_name, pool); SVN_ERR(svn_wc_adm_retrieve(&adm_access, baton->adm_access, conflict_dir, pool)); return resolve_conflict_on_entry(path, entry, adm_access, base_name, baton->resolve_text, baton->resolve_props, baton->resolve_tree, baton->conflict_choice, baton->notify_func, baton->notify_baton, pool); } static const svn_wc_entry_callbacks2_t resolve_walk_callbacks = { resolve_found_entry_callback, svn_wc__walker_default_error_handler }; /* The public function */ svn_error_t * svn_wc_resolved_conflict(const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t resolve_text, svn_boolean_t resolve_props, svn_boolean_t recurse, svn_wc_notify_func_t notify_func, void *notify_baton, apr_pool_t *pool) { svn_wc__compat_notify_baton_t nb; nb.func = notify_func; nb.baton = notify_baton; return svn_wc_resolved_conflict2(path, adm_access, resolve_text, resolve_props, recurse, svn_wc__compat_call_notify_func, &nb, NULL, NULL, pool); } svn_error_t * svn_wc_resolved_conflict2(const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t resolve_text, svn_boolean_t resolve_props, svn_boolean_t recurse, svn_wc_notify_func2_t notify_func, void *notify_baton, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *pool) { return svn_wc_resolved_conflict3(path, adm_access, resolve_text, resolve_props, SVN_DEPTH_INFINITY_OR_EMPTY(recurse), svn_wc_conflict_choose_merged, notify_func, notify_baton, cancel_func, cancel_baton, pool); } svn_error_t * svn_wc_resolved_conflict3(const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t resolve_text, svn_boolean_t resolve_props, svn_depth_t depth, svn_wc_conflict_choice_t conflict_choice, svn_wc_notify_func2_t notify_func, void *notify_baton, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *pool) { return svn_wc_resolved_conflict4(path, adm_access, resolve_text, resolve_props, FALSE, depth, svn_wc_conflict_choose_merged, notify_func, notify_baton, cancel_func, cancel_baton, pool); } svn_error_t * svn_wc_resolved_conflict4(const char *path, svn_wc_adm_access_t *adm_access, svn_boolean_t resolve_text, svn_boolean_t resolve_props, svn_boolean_t resolve_tree, svn_depth_t depth, svn_wc_conflict_choice_t conflict_choice, svn_wc_notify_func2_t notify_func, void *notify_baton, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *pool) { struct resolve_callback_baton *baton = apr_pcalloc(pool, sizeof(*baton)); baton->resolve_text = resolve_text; baton->resolve_props = resolve_props; baton->resolve_tree = resolve_tree; baton->adm_access = adm_access; baton->notify_func = notify_func; baton->notify_baton = notify_baton; baton->conflict_choice = conflict_choice; return svn_wc_walk_entries3(path, adm_access, &resolve_walk_callbacks, baton, depth, FALSE, cancel_func, cancel_baton, pool); } svn_error_t *svn_wc_add_lock(const char *path, const svn_lock_t *lock, svn_wc_adm_access_t *adm_access, apr_pool_t *pool) { const svn_wc_entry_t *entry; svn_wc_entry_t newentry; SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, FALSE, pool)); newentry.lock_token = lock->token; newentry.lock_owner = lock->owner; newentry.lock_comment = lock->comment; newentry.lock_creation_date = lock->creation_date; SVN_ERR(svn_wc__entry_modify(adm_access, entry->name, &newentry, SVN_WC__ENTRY_MODIFY_LOCK_TOKEN | SVN_WC__ENTRY_MODIFY_LOCK_OWNER | SVN_WC__ENTRY_MODIFY_LOCK_COMMENT | SVN_WC__ENTRY_MODIFY_LOCK_CREATION_DATE, TRUE, pool)); { /* if svn:needs-lock is present, then make the file read-write. */ const svn_string_t *needs_lock; SVN_ERR(svn_wc_prop_get(&needs_lock, SVN_PROP_NEEDS_LOCK, path, adm_access, pool)); if (needs_lock) SVN_ERR(svn_io_set_file_read_write(path, FALSE, pool)); } return SVN_NO_ERROR; } svn_error_t *svn_wc_remove_lock(const char *path, svn_wc_adm_access_t *adm_access, apr_pool_t *pool) { const svn_wc_entry_t *entry; svn_wc_entry_t newentry; SVN_ERR(svn_wc__entry_versioned(&entry, path, adm_access, FALSE, pool)); newentry.lock_token = newentry.lock_owner = newentry.lock_comment = NULL; newentry.lock_creation_date = 0; SVN_ERR(svn_wc__entry_modify(adm_access, entry->name, &newentry, SVN_WC__ENTRY_MODIFY_LOCK_TOKEN | SVN_WC__ENTRY_MODIFY_LOCK_OWNER | SVN_WC__ENTRY_MODIFY_LOCK_COMMENT | SVN_WC__ENTRY_MODIFY_LOCK_CREATION_DATE, TRUE, pool)); { /* if svn:needs-lock is present, then make the file read-only. */ const svn_string_t *needs_lock; SVN_ERR(svn_wc_prop_get(&needs_lock, SVN_PROP_NEEDS_LOCK, path, adm_access, pool)); if (needs_lock) SVN_ERR(svn_io_set_file_read_only(path, FALSE, pool)); } return SVN_NO_ERROR; } svn_error_t * svn_wc_set_changelist(const char *path, const char *changelist, svn_wc_adm_access_t *adm_access, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *pool) { const svn_wc_entry_t *entry; svn_wc_entry_t newentry; svn_wc_notify_t *notify; SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, pool)); if (! entry) return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL, _("'%s' is not under version control"), path); /* We can't do changelists on directories. */ if (entry->kind == svn_node_dir) return svn_error_createf(SVN_ERR_CLIENT_IS_DIRECTORY, NULL, _("'%s' is a directory, and thus cannot" " be a member of a changelist"), path); /* If the path has no changelist and we're removing changelist, skip it. */ if (! (changelist || entry->changelist)) return SVN_NO_ERROR; /* If the path is already assigned to the changelist we're trying to assign, skip it. */ if (entry->changelist && changelist && strcmp(entry->changelist, changelist) == 0) return SVN_NO_ERROR; /* If the path is already a member of a changelist, warn the user about this, but still allow the reassignment to happen. */ if (entry->changelist && changelist && notify_func) { svn_error_t *reassign_err = svn_error_createf(SVN_ERR_WC_CHANGELIST_MOVE, NULL, _("Removing '%s' from changelist '%s'."), path, entry->changelist); notify = svn_wc_create_notify(path, svn_wc_notify_changelist_moved, pool); notify->err = reassign_err; notify_func(notify_baton, notify, pool); svn_error_clear(notify->err); } /* Tweak the entry. */ newentry.changelist = changelist; SVN_ERR(svn_wc__entry_modify(adm_access, entry->name, &newentry, SVN_WC__ENTRY_MODIFY_CHANGELIST, TRUE, pool)); /* And tell someone what we've done. */ if (notify_func) { notify = svn_wc_create_notify(path, changelist ? svn_wc_notify_changelist_set : svn_wc_notify_changelist_clear, pool); notify->changelist_name = changelist; notify_func(notify_baton, notify, pool); } return SVN_NO_ERROR; } svn_error_t * svn_wc__set_file_external_location(svn_wc_adm_access_t *adm_access, const char *name, const char *url, const svn_opt_revision_t *peg_rev, const svn_opt_revision_t *rev, const char *repos_root_url, apr_pool_t *pool) { apr_hash_t *entries; svn_wc_entry_t entry = { 0 }; SVN_ERR(svn_wc_entries_read(&entries, adm_access, FALSE, pool)); if (url) { /* A repository root relative path is stored in the entry. */ SVN_ERR_ASSERT(peg_rev); SVN_ERR_ASSERT(rev); entry.file_external_path = url + strlen(repos_root_url); entry.file_external_peg_rev = *peg_rev; entry.file_external_rev = *rev; } else { entry.file_external_path = NULL; entry.file_external_peg_rev.kind = svn_opt_revision_unspecified; entry.file_external_rev.kind = svn_opt_revision_unspecified; } SVN_ERR(svn_wc__entry_modify(adm_access, name, &entry, SVN_WC__ENTRY_MODIFY_FILE_EXTERNAL, TRUE, pool)); return SVN_NO_ERROR; }