| /* |
| * scanlog.c: scanning the log for moves |
| * |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| */ |
| |
| /* ==================================================================== */ |
| |
| |
| |
| /*** Includes. ***/ |
| |
| #include "svn_hash.h" |
| #include "svn_wc.h" |
| #include "svn_client.h" |
| #include "svn_error.h" |
| #include "svn_config.h" |
| #include "svn_time.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_path.h" |
| #include "svn_pools.h" |
| #include "svn_io.h" |
| |
| #include "private/svn_client_private.h" |
| #include "private/svn_wc_private.h" |
| #include "private/svn_ra_private.h" |
| |
| #include "svnmover.h" |
| |
| #include "svn_private_config.h" |
| |
| |
| /* From moves-scan-log branch */ |
| |
| svn_repos_move_info_t * |
| svn_repos_move_info_create(const char *moved_from_repos_relpath, |
| const char *moved_to_repos_relpath, |
| svn_revnum_t revision, |
| svn_revnum_t copyfrom_rev, |
| svn_repos_move_info_t *prev, |
| svn_repos_move_info_t *next, |
| apr_pool_t *result_pool) |
| { |
| svn_repos_move_info_t *move = apr_palloc(result_pool, sizeof(*move)); |
| |
| move->moved_from_repos_relpath = moved_from_repos_relpath; |
| move->moved_to_repos_relpath = moved_to_repos_relpath; |
| move->revision = revision; |
| move->copyfrom_rev = copyfrom_rev; |
| move->prev = prev; |
| move->next = next; |
| |
| return move; |
| } |
| |
| const char * |
| svn_client__format_move_chain_for_display(svn_repos_move_info_t *first_move, |
| const char *indent, |
| apr_pool_t *result_pool) |
| { |
| const char *s; |
| svn_repos_move_info_t *last_move; |
| |
| last_move = first_move; |
| while (last_move->next) |
| last_move = last_move->next; |
| |
| if (last_move != first_move) |
| { |
| svn_repos_move_info_t *this_move; |
| |
| s = apr_psprintf(result_pool, |
| _("Combined move:\n%s %s@%ld -> %s\n" |
| "%sIndividual moves:\n"), |
| indent, first_move->moved_from_repos_relpath, |
| first_move->copyfrom_rev, |
| last_move->moved_to_repos_relpath, indent); |
| |
| this_move = first_move; |
| do |
| { |
| s = apr_pstrcat(result_pool, s, |
| apr_psprintf( |
| result_pool, _("%s [r%ld] %s@%ld -> %s\n"), |
| indent, |
| this_move->revision, |
| this_move->moved_from_repos_relpath, |
| this_move->copyfrom_rev, |
| this_move->moved_to_repos_relpath), |
| (char *)NULL); |
| this_move = this_move->next; |
| } |
| while (this_move); |
| } |
| else |
| s = apr_psprintf(result_pool, _(" [r%ld] %s@%ld -> %s\n"), |
| first_move->revision, |
| first_move->moved_from_repos_relpath, |
| first_move->copyfrom_rev, |
| first_move->moved_to_repos_relpath); |
| |
| return s; |
| } |
| |
| struct scan_moves_log_receiver_baton { |
| /*const char *anchor_abspath;*/ |
| svn_client_ctx_t *ctx; |
| svn_revnum_t start; |
| svn_revnum_t end; |
| svn_ra_session_t *ra_session; |
| |
| /* The moved nodes hash to be populated. |
| * Maps a revision number to an array of svn_repos_move_info_t |
| * objects describing moves which happened in the revision. |
| * |
| * Given a sequence of moves which happened in given revisions, such as: |
| * rA: mv x->z |
| * rA: mv a->b |
| * rB: mv b->c |
| * rC: mv c->d |
| * we map each revision number to all moves which happened in the |
| * revision, which looks as follows: |
| * rA : [(rA, x->z), (rA, a->b)] |
| * rB : [(rB, b->c)] |
| * rC : [(rC, c->d)] |
| * This allows an update to find relevant moves based on the base |
| * revision of a node (during updates the base revision of each node |
| * in the working copy is arbitrary so we might not know the nodes 'a' |
| * and 'x' under these names). |
| * Additionally, all moves pertaining to the same node are chained into a |
| * doubly-linked list via 'next' and 'prev' pointers (see definition of |
| * svn_repos_move_info_t). |
| * This way, an update can look up all moves relevant to a node, forwards |
| * or backwards in history, once it has located a relevant move in the chain. |
| * This can be visualized as follows: |
| * rA : [(rA, x->z, prev=>NULL, next=>NULL), |
| * (rA, a->b, prev=>NULL, next=>(rB, b->c))] |
| * rB : [(rB, b->c), prev=>(rA, a->b), next=>(rC, c->d)] |
| * rC : [(rC, c->d), prev=>(rB, c->d), next=>NULL] |
| */ |
| apr_hash_t *moves; |
| |
| /* Temporary map of move-target paths to repos_move_info_t. |
| * Used to link multiple moves of the same node across revisions. */ |
| apr_hash_t *moves_by_target_path; |
| } scan_moves_log_receiver_baton; |
| |
| struct copy_info { |
| const char *copyto_path; |
| const char *copyfrom_path; |
| svn_revnum_t copyfrom_rev; |
| } copy_info; |
| |
| |
| /* Set *RELATED to true if the deleted node at repository relpath |
| * DELETED_PATH@DELETED_REV is ancestrally related to the node at |
| * repository relpath COPYFROM_PATH@COPYFROM_REV, else set it to false. |
| * |
| * ### JAF: In practice this attempts to trace back, starting from |
| * DELETED_PATH@(DELETED_REV-1). What if that does not exist? |
| */ |
| static svn_error_t * |
| check_ancestry(svn_boolean_t *related, |
| const char *session_url, |
| const char *repos_root_url, |
| const char *deleted_path, |
| svn_revnum_t deleted_rev, |
| const char *copyfrom_path, |
| svn_revnum_t copyfrom_rev, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| apr_hash_t *locations; |
| const char *old_url; |
| const char *old_location; |
| const char *relpath; |
| svn_ra_session_t *ra_session2; |
| apr_array_header_t *location_revisions; |
| |
| *related = FALSE; |
| |
| location_revisions = apr_array_make(scratch_pool, 1, sizeof(svn_revnum_t)); |
| APR_ARRAY_PUSH(location_revisions, svn_revnum_t) = copyfrom_rev; |
| old_url = svn_uri_canonicalize(apr_pstrcat(scratch_pool, |
| repos_root_url, "/", |
| deleted_path, NULL), |
| scratch_pool); |
| relpath = svn_uri_skip_ancestor(session_url, old_url, scratch_pool); |
| SVN_ERR(svn_client_open_ra_session2(&ra_session2, session_url, NULL, |
| ctx, scratch_pool, scratch_pool)); |
| if (relpath == NULL) |
| { |
| svn_error_t *err; |
| |
| /* The deleted path is outside of the baton's RA session URL. |
| * Try to open the new RA session to the repository root. */ |
| SVN_ERR(svn_ra_reparent(ra_session2, repos_root_url, scratch_pool)); |
| relpath = svn_uri_skip_ancestor(repos_root_url, old_url, scratch_pool); |
| if (relpath == NULL) |
| return SVN_NO_ERROR; |
| err = svn_ra_get_locations(ra_session2, &locations, relpath, |
| deleted_rev - 1, location_revisions, |
| scratch_pool); |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_RA_NOT_AUTHORIZED || |
| err->apr_err == SVN_ERR_RA_DAV_FORBIDDEN) |
| { |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| else |
| return svn_error_trace(err); |
| } |
| } |
| else |
| SVN_ERR(svn_ra_get_locations(ra_session2, &locations, relpath, |
| deleted_rev - 1, location_revisions, |
| scratch_pool)); |
| |
| old_location = apr_hash_get(locations, ©from_rev, sizeof(svn_revnum_t)); |
| *related = (old_location && |
| strcmp(old_location[0] == '/' ? old_location + 1 : old_location, |
| copyfrom_path) == 0); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| scan_moves_log_receiver(void *baton, |
| svn_log_entry_t *log_entry, |
| apr_pool_t *scratch_pool) |
| { |
| apr_hash_index_t *hi; |
| apr_hash_t *copies; |
| apr_array_header_t *deleted_paths; |
| struct scan_moves_log_receiver_baton *b = baton; |
| apr_pool_t *result_pool = apr_hash_pool_get(b->moves); |
| apr_pool_t *iterpool; |
| int i; |
| const char *session_url; |
| const char *repos_root_url; |
| apr_array_header_t *moves; |
| |
| if (b->ctx->notify_func2) |
| { |
| #if 0 |
| svn_wc_notify_t *notify; |
| notify = svn_wc_create_notify(b->anchor_abspath, |
| svn_wc_notify_moves_scan_log_in_progress, |
| scratch_pool); |
| notify->moves_scan_log_start_rev = b->start; |
| notify->moves_scan_log_end_rev = b->end; |
| notify->moves_scan_log_current_rev = log_entry->revision; |
| b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); |
| #endif |
| } |
| |
| if (log_entry->changed_paths2 == NULL) |
| return SVN_NO_ERROR; |
| |
| copies = apr_hash_make(scratch_pool); |
| deleted_paths = apr_array_make(scratch_pool, 0, sizeof(const char *)); |
| |
| /* Scan for copied and deleted nodes in this revision. */ |
| for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths2); |
| hi; hi = apr_hash_next(hi)) |
| { |
| const char *path = apr_hash_this_key(hi); |
| svn_log_changed_path2_t *data = apr_hash_this_val(hi); |
| |
| if ((data->action == 'A' || data->action == 'R') && data->copyfrom_path) |
| { |
| struct copy_info *copy; |
| apr_array_header_t *copies_with_same_source_path; |
| |
| SVN_ERR_ASSERT(path[0] == '/'); |
| |
| if (data->copyfrom_path[0] == '/') |
| data->copyfrom_path++; |
| |
| copy = apr_palloc(scratch_pool, sizeof(*copy)); |
| copy->copyto_path = path + 1; /* Strip leading '/' */ |
| copy->copyfrom_path = data->copyfrom_path; |
| copy->copyfrom_rev = data->copyfrom_rev; |
| copies_with_same_source_path = apr_hash_get(copies, |
| data->copyfrom_path, |
| APR_HASH_KEY_STRING); |
| if (copies_with_same_source_path == NULL) |
| { |
| copies_with_same_source_path = apr_array_make( |
| result_pool, 1, |
| sizeof(struct copy_info *)); |
| apr_hash_set(copies, copy->copyfrom_path, APR_HASH_KEY_STRING, |
| copies_with_same_source_path); |
| } |
| APR_ARRAY_PUSH(copies_with_same_source_path, |
| struct copy_info *) = copy; |
| } |
| |
| if (data->action == 'D' || data->action == 'R') |
| { |
| const char *parent_path; |
| |
| /* ### Is this true? What does the API guarantee? Is it |
| ### true that copyfrom_path is a relpath? */ |
| SVN_ERR_ASSERT(path[0] == '/'); |
| |
| /* When a delete is within a copy the deleted path in the |
| changed_paths2 hash is the copied path, but for the purposes |
| of move detection we want the pre-copy path. |
| |
| ### Not sure if this is the correct thing to do. Yes, it |
| ### allows us to detect moves in copies/moves but will it |
| ### lead to false positives? Does it matter that the |
| ### adjusted path may not have been committed? Does it |
| ### matter that the adjusted path may be the same as |
| ### another committed path? */ |
| parent_path = svn_dirent_dirname(path, scratch_pool); |
| while(strcmp(parent_path, "/")) |
| { |
| svn_log_changed_path2_t *data2 |
| = apr_hash_get(log_entry->changed_paths2, parent_path, |
| APR_HASH_KEY_STRING); |
| |
| if (data2 && data2->action == 'A') |
| { |
| const char *relpath = svn_dirent_skip_ancestor(parent_path, |
| path); |
| path = svn_dirent_join_many(scratch_pool, "/", |
| data2->copyfrom_path, relpath, |
| NULL); |
| break; |
| } |
| else |
| parent_path = svn_dirent_dirname(parent_path, scratch_pool); |
| } |
| APR_ARRAY_PUSH(deleted_paths, const char *) = path + 1; |
| } |
| } |
| |
| /* If a node was deleted at one location and copied from the deleted |
| * location to a new location within the same revision, put the node |
| * on the moved-nodes list. */ |
| SVN_ERR(svn_ra_get_session_url(b->ra_session, &session_url, scratch_pool)); |
| SVN_ERR(svn_ra_get_repos_root2(b->ra_session, &repos_root_url, scratch_pool)); |
| iterpool = svn_pool_create(scratch_pool); |
| for (i = 0; i < deleted_paths->nelts; i++) |
| { |
| const char *deleted_path; |
| apr_array_header_t *copies_with_same_source_path; |
| svn_repos_move_info_t *new_move; |
| svn_repos_move_info_t *prior_move; |
| svn_boolean_t related; |
| int j; |
| |
| deleted_path = APR_ARRAY_IDX(deleted_paths, i, const char *); |
| copies_with_same_source_path = apr_hash_get(copies, deleted_path, |
| APR_HASH_KEY_STRING); |
| if (copies_with_same_source_path == NULL) |
| continue; |
| |
| svn_pool_clear(iterpool); |
| |
| for (j = 0; j < copies_with_same_source_path->nelts; j++) |
| { |
| struct copy_info *copy; |
| |
| copy = APR_ARRAY_IDX(copies_with_same_source_path, j, |
| struct copy_info *); |
| |
| /* We found a deleted node which matches the copyfrom path of |
| * a copied node. Verify that the deleted node is an ancestor |
| * of the copied node. Tracing back history of the deleted node |
| * from revision log_entry->revision-1 to the copyfrom-revision |
| * we must end up at the copyfrom-path. */ |
| SVN_ERR(check_ancestry(&related, session_url, repos_root_url, |
| deleted_path, log_entry->revision, |
| copy->copyfrom_path, |
| copy->copyfrom_rev, |
| b->ctx, iterpool)); |
| if (!related) |
| continue; |
| |
| /* ### TODO: |
| * If the node was not copied from the most recent last-changed |
| * revision of the deleted node, this is not a move but a |
| * "copy from the past + delete". */ |
| |
| /* Remember details of this move. */ |
| new_move = svn_repos_move_info_create( |
| apr_pstrdup(result_pool, deleted_path), |
| apr_pstrdup(result_pool, copy->copyto_path), |
| log_entry->revision, copy->copyfrom_rev, |
| NULL, NULL, result_pool); |
| |
| /* Link together multiple moves of the same node. */ |
| prior_move = apr_hash_get(b->moves_by_target_path, |
| new_move->moved_from_repos_relpath, |
| APR_HASH_KEY_STRING); |
| if (prior_move) |
| { |
| /* Tracing back history of the delete-half of the new move |
| * to the copyfrom-revision of the prior move we must end up |
| * at the delete-half of the prior move. */ |
| SVN_ERR(check_ancestry(&related, session_url, repos_root_url, |
| new_move->moved_from_repos_relpath, |
| new_move->revision, |
| prior_move->moved_from_repos_relpath, |
| prior_move->copyfrom_rev, |
| b->ctx, iterpool)); |
| if (related) |
| { |
| prior_move->next = new_move; |
| new_move->prev = prior_move; |
| } |
| } |
| apr_hash_set(b->moves_by_target_path, |
| new_move->moved_to_repos_relpath, |
| APR_HASH_KEY_STRING, new_move); |
| |
| /* Add this move to the list of moves in this revision. */ |
| moves = apr_hash_get(b->moves, &new_move->revision, |
| sizeof(svn_revnum_t)); |
| if (moves == NULL) |
| { |
| moves = apr_array_make(result_pool, 1, |
| sizeof(svn_repos_move_info_t *)); |
| APR_ARRAY_PUSH(moves, svn_repos_move_info_t *) = new_move; |
| apr_hash_set(b->moves, &new_move->revision, sizeof(svn_revnum_t), |
| moves); |
| } |
| else |
| APR_ARRAY_PUSH(moves, svn_repos_move_info_t *) = new_move; |
| } |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client__get_repos_moves(apr_hash_t **moves, |
| const char *anchor_abspath, |
| svn_ra_session_t *ra_session, |
| svn_revnum_t start, |
| svn_revnum_t end, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| struct scan_moves_log_receiver_baton lrb; |
| |
| /*lrb.anchor_abspath = anchor_abspath;*/ |
| lrb.ctx = ctx; |
| lrb.moves = apr_hash_make(result_pool); |
| lrb.start = start; |
| lrb.end = end; |
| lrb.ra_session = ra_session; |
| lrb.moves_by_target_path = apr_hash_make(scratch_pool); |
| |
| if (ctx->notify_func2) |
| { |
| #if 0 |
| svn_wc_notify_t *notify; |
| notify = svn_wc_create_notify(b->anchor_abspath, |
| svn_wc_notify_moves_scan_log_start, |
| scratch_pool); |
| notify->moves_scan_log_start_rev = start; |
| notify->moves_scan_log_end_rev = end; |
| notify->moves_scan_log_current_rev = start; |
| ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); |
| #endif |
| } |
| |
| SVN_ERR(svn_ra_get_log2(ra_session, NULL, start, end, 0, TRUE, FALSE, |
| FALSE, apr_array_make(scratch_pool, 0, |
| sizeof(const char *)), |
| scan_moves_log_receiver, &lrb, scratch_pool)); |
| |
| if (ctx->notify_func2) |
| { |
| #if 0 |
| svn_wc_notify_t *notify; |
| notify = svn_wc_create_notify(b->anchor_abspath, |
| svn_wc_notify_moves_scan_log_done, |
| scratch_pool); |
| notify->moves_scan_log_start_rev = start; |
| notify->moves_scan_log_end_rev = end; |
| notify->moves_scan_log_current_rev = end; |
| b->ctx->notify_func2(b->ctx->notify_baton2, notify, scratch_pool); |
| #endif |
| } |
| |
| if (moves) |
| *moves = lrb.moves; |
| |
| return SVN_NO_ERROR; |
| } |
| |