1 /*
2 * log.c -- Fetch log data and implement the log queries
3 *
4 * ====================================================================
5 * Licensed to the Apache Software Foundation (ASF) under one
6 * or more contributor license agreements. See the NOTICE file
7 * distributed with this work for additional information
8 * regarding copyright ownership. The ASF licenses this file
9 * to you under the Apache License, Version 2.0 (the
10 * "License"); you may not use this file except in compliance
11 * with the License. You may obtain a copy of the License at
12 *
13 * http://www.apache.org/licenses/LICENSE-2.0
14 *
15 * Unless required by applicable law or agreed to in writing,
16 * software distributed under the License is distributed on an
17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18 * KIND, either express or implied. See the License for the
19 * specific language governing permissions and limitations
20 * under the License.
21 * ====================================================================
22 */
23
24 #include "svn_cmdline.h"
25 #include "svn_client.h"
26 #include "svn_dirent_uri.h"
27 #include "svn_string.h"
28 #include "svn_path.h"
29 #include "svn_error.h"
30 #include "svn_sorts.h"
31 #include "svn_pools.h"
32 #include "svn_hash.h"
33
34 #include "private/svn_fspath.h"
35 #include "private/svn_subr_private.h"
36 #include "private/svn_sorts_private.h"
37
38 #include "mergeinfo-normalizer.h"
39
40 #include "svn_private_config.h"
41
42
43
44 /* Describes all changes of a single revision.
45 * Note that all strings are shared within a given svn_min__log_t instance.
46 */
47 typedef struct log_entry_t
48 {
49 /* Revision being described. */
50 svn_revnum_t revision;
51
52 /* FS path that is equal or a parent of any in PATHS. */
53 const char *common_base;
54
55 /* Sorted list of all FS paths touched. Elements are const char*. */
56 apr_array_header_t *paths;
57 } log_entry_t;
58
59 /* Describes a deletion.
60 * Note that replacements are treated as additions + deletions.
61 */
62 typedef struct deletion_t
63 {
64 /* Path being deleted (or replaced). */
65 const char *path;
66
67 /* Revision in which this deletion happened.*/
68 svn_revnum_t revision;
69 } deletion_t;
70
71 /* Note that all FS paths are internalized and shared within this object.
72 */
73 struct svn_min__log_t
74 {
75 /* Dictionary of all FS paths used in this log.
76 * The hash itself is only temporary and will be destroyed after the log
77 * has been constructed and all paths got internalized. */
78 apr_hash_t *unique_paths;
79
80 /* Oldest revision we received. */
81 svn_revnum_t first_rev;
82
83 /* Latest revision we received. */
84 svn_revnum_t head_rev;
85
86 /* Log contents we received. Entries are log_entry_t *. */
87 apr_array_header_t *entries;
88
89 /* List of all copy operations we encountered, sorted by target&rev. */
90 apr_array_header_t *copies;
91
92 /* Like COPIES but sorted by source&source-rev. */
93 apr_array_header_t *copies_by_source;
94
95 /* List of all deletions we encountered, sorted by path&rev. */
96 apr_array_header_t *deletions;
97
98 /* If set, don't show progress nor summary. */
99 svn_boolean_t quiet;
100 };
101
102 /* Comparison function defining the order in svn_min__log_t.COPIES. */
103 static int
copy_order(const void * lhs,const void * rhs)104 copy_order(const void *lhs,
105 const void *rhs)
106 {
107 const svn_min__copy_t *lhs_copy = *(const svn_min__copy_t * const *)lhs;
108 const svn_min__copy_t *rhs_copy = *(const svn_min__copy_t * const *)rhs;
109
110 int diff = strcmp(lhs_copy->path, rhs_copy->path);
111 if (diff)
112 return diff;
113
114 if (lhs_copy->revision < rhs_copy->revision)
115 return -1;
116
117 return lhs_copy->revision == rhs_copy->revision ? 0 : 1;
118 }
119
120 /* Comparison function defining the order in svn_min__log_t.COPIES_BY_SOURCE.
121 */
122 static int
copy_by_source_order(const void * lhs,const void * rhs)123 copy_by_source_order(const void *lhs,
124 const void *rhs)
125 {
126 const svn_min__copy_t *lhs_copy = *(const svn_min__copy_t * const *)lhs;
127 const svn_min__copy_t *rhs_copy = *(const svn_min__copy_t * const *)rhs;
128
129 int diff = strcmp(lhs_copy->copyfrom_path, rhs_copy->copyfrom_path);
130 if (diff)
131 return diff;
132
133 if (lhs_copy->copyfrom_revision < rhs_copy->copyfrom_revision)
134 return -1;
135
136 return lhs_copy->copyfrom_revision == rhs_copy->copyfrom_revision ? 0 : 1;
137 }
138
139 /* Comparison function defining the order in svn_min__log_t.DELETIONS. */
140 static int
deletion_order(const void * lhs,const void * rhs)141 deletion_order(const void *lhs,
142 const void *rhs)
143 {
144 const deletion_t *lhs_deletion = *(const deletion_t * const *)lhs;
145 const deletion_t *rhs_deletion = *(const deletion_t * const *)rhs;
146
147 int diff = strcmp(lhs_deletion->path, rhs_deletion->path);
148 if (diff)
149 return diff;
150
151 if (lhs_deletion->revision < rhs_deletion->revision)
152 return -1;
153
154 return lhs_deletion->revision == rhs_deletion->revision ? 0 : 1;
155 }
156
157 /* Return the string stored in UNIQUE_PATHS with the value PATH of PATH_LEN
158 * characters. If the hash does not have a matching entry, add one.
159 * Allocate all strings in RESULT_POOL. */
160 static const char *
internalize(apr_hash_t * unique_paths,const char * path,apr_ssize_t path_len,apr_pool_t * result_pool)161 internalize(apr_hash_t *unique_paths,
162 const char *path,
163 apr_ssize_t path_len,
164 apr_pool_t *result_pool)
165 {
166 const char *result = apr_hash_get(unique_paths, path, path_len);
167 if (result == NULL)
168 {
169 result = apr_pstrmemdup(result_pool, path, path_len);
170 apr_hash_set(unique_paths, result, path_len, result);
171 }
172
173 return result;
174 }
175
176 /* Implements svn_log_entry_receiver_t. Copies the info of LOG_ENTRY into
177 * (svn_min__log_t *)BATON. */
178 static svn_error_t *
log_entry_receiver(void * baton,svn_log_entry_t * log_entry,apr_pool_t * scratch_pool)179 log_entry_receiver(void *baton,
180 svn_log_entry_t *log_entry,
181 apr_pool_t *scratch_pool)
182 {
183 svn_min__log_t *log = baton;
184 apr_pool_t *result_pool = log->entries->pool;
185 log_entry_t *entry;
186 apr_hash_index_t *hi;
187 const char *common_base;
188 int count;
189
190 /* Don't care about empty revisions. Skip them. */
191 if (!log_entry->changed_paths || !apr_hash_count(log_entry->changed_paths))
192 return SVN_NO_ERROR;
193
194 /* Copy changed paths list. Collect deletions and copies. */
195 entry = apr_pcalloc(result_pool, sizeof(*entry));
196 entry->revision = log_entry->revision;
197 entry->paths = apr_array_make(result_pool,
198 apr_hash_count(log_entry->changed_paths),
199 sizeof(const char *));
200
201 for (hi = apr_hash_first(scratch_pool, log_entry->changed_paths);
202 hi;
203 hi = apr_hash_next(hi))
204 {
205 const char *path = apr_hash_this_key(hi);
206 svn_log_changed_path_t *change = apr_hash_this_val(hi);
207
208 path = internalize(log->unique_paths, path, apr_hash_this_key_len(hi),
209 result_pool);
210 APR_ARRAY_PUSH(entry->paths, const char *) = path;
211
212 if (change->action == 'D' || change->action == 'R')
213 {
214 deletion_t *deletion = apr_pcalloc(result_pool, sizeof(*deletion));
215 deletion->path = path;
216 deletion->revision = log_entry->revision;
217
218 APR_ARRAY_PUSH(log->deletions, deletion_t *) = deletion;
219 }
220
221 if (SVN_IS_VALID_REVNUM(change->copyfrom_rev))
222 {
223 svn_min__copy_t *copy = apr_pcalloc(result_pool, sizeof(*copy));
224 copy->path = path;
225 copy->revision = log_entry->revision;
226 copy->copyfrom_path = internalize(log->unique_paths,
227 change->copyfrom_path,
228 strlen(change->copyfrom_path),
229 result_pool);
230 copy->copyfrom_revision = change->copyfrom_rev;
231
232 APR_ARRAY_PUSH(log->copies, svn_min__copy_t *) = copy;
233 }
234 }
235
236 /* Determine the common base of all changed paths. */
237 count = entry->paths->nelts;
238 if (count == 1)
239 {
240 entry->common_base = APR_ARRAY_IDX(entry->paths, 0, const char *);
241 }
242 else
243 {
244 svn_sort__array(entry->paths, svn_sort_compare_paths);
245
246 common_base = svn_dirent_get_longest_ancestor(
247 APR_ARRAY_IDX(entry->paths, 0, const char *),
248 APR_ARRAY_IDX(entry->paths, count - 1, const char *),
249 scratch_pool);
250 entry->common_base = internalize(log->unique_paths, common_base,
251 strlen(common_base), result_pool);
252 }
253
254 /* Done with that reivison. */
255 APR_ARRAY_PUSH(log->entries, log_entry_t *) = entry;
256
257 /* Update log-global state. */
258 log->first_rev = log_entry->revision;
259 if (log->head_rev == SVN_INVALID_REVNUM)
260 log->head_rev = log_entry->revision;
261
262 /* Show progress. */
263 if (log->entries->nelts % 1000 == 0 && !log->quiet)
264 {
265 SVN_ERR(svn_cmdline_printf(scratch_pool, "."));
266 SVN_ERR(svn_cmdline_fflush(stdout));
267 }
268
269 return SVN_NO_ERROR;
270 }
271
272 /* Print some statistics about LOG to console. Use SCRATCH_POOL for
273 * temporary allocations. */
274 static svn_error_t *
print_log_stats(svn_min__log_t * log,apr_pool_t * scratch_pool)275 print_log_stats(svn_min__log_t *log,
276 apr_pool_t *scratch_pool)
277 {
278 int change_count = 0;
279
280 int i;
281 for (i = 0; i < log->entries->nelts; ++i)
282 {
283 const log_entry_t *entry = APR_ARRAY_IDX(log->entries, i,
284 const log_entry_t *);
285 change_count += entry->paths->nelts;
286 }
287
288 SVN_ERR(svn_cmdline_printf(scratch_pool,
289 _(" Received %d revisions from %ld to %ld.\n"),
290 log->entries->nelts, log->first_rev,
291 log->head_rev));
292 SVN_ERR(svn_cmdline_printf(scratch_pool,
293 _(" Received %d path changes.\n"),
294 change_count));
295 SVN_ERR(svn_cmdline_printf(scratch_pool,
296 _(" Pool has %u different paths.\n\n"),
297 apr_hash_count(log->unique_paths)));
298
299 return SVN_NO_ERROR;
300 }
301
302 svn_error_t *
svn_min__log(svn_min__log_t ** log,const char * url,svn_min__cmd_baton_t * baton,apr_pool_t * result_pool,apr_pool_t * scratch_pool)303 svn_min__log(svn_min__log_t **log,
304 const char *url,
305 svn_min__cmd_baton_t *baton,
306 apr_pool_t *result_pool,
307 apr_pool_t *scratch_pool)
308 {
309 svn_client_ctx_t *ctx = baton->ctx;
310 svn_min__log_t *result;
311
312 /* Prepare API parameters for fetching the full log for URL,
313 * including changed paths, excluding revprops.
314 */
315 apr_array_header_t *targets;
316 apr_array_header_t *revisions;
317 apr_array_header_t *revprops;
318 svn_opt_revision_t peg_revision = { svn_opt_revision_head };
319 svn_opt_revision_range_t range = { { svn_opt_revision_unspecified },
320 { svn_opt_revision_unspecified } };
321
322 targets = apr_array_make(scratch_pool, 1, sizeof(const char *));
323 APR_ARRAY_PUSH(targets, const char *) = url;
324
325 revisions = apr_array_make(scratch_pool, 1, sizeof(&range));
326 APR_ARRAY_PUSH(revisions, svn_opt_revision_range_t *) = ⦥
327
328 revprops = apr_array_make(scratch_pool, 0, sizeof(const char *));
329
330 /* The log object to fill. */
331 result = apr_pcalloc(result_pool, sizeof(*result));
332 result->unique_paths = svn_hash__make(scratch_pool);
333 result->first_rev = SVN_INVALID_REVNUM;
334 result->head_rev = SVN_INVALID_REVNUM;
335 result->entries = apr_array_make(result_pool, 1024, sizeof(log_entry_t *));
336 result->copies = apr_array_make(result_pool, 1024,
337 sizeof(svn_min__copy_t *));
338 result->deletions = apr_array_make(result_pool, 1024, sizeof(deletion_t *));
339 result->quiet = baton->opt_state->quiet;
340
341 if (!baton->opt_state->quiet)
342 {
343 SVN_ERR(svn_cmdline_printf(scratch_pool,
344 _("Fetching log for %s ..."),
345 url));
346 SVN_ERR(svn_cmdline_fflush(stdout));
347 }
348
349 SVN_ERR(svn_client_log5(targets,
350 &peg_revision,
351 revisions,
352 0, /* no limit */
353 TRUE, /* verbose */
354 TRUE, /* stop-on-copy */
355 FALSE, /* merge history */
356 revprops,
357 log_entry_receiver,
358 result,
359 ctx,
360 scratch_pool));
361
362 /* Complete arrays in RESULT. */
363 result->copies_by_source = apr_array_copy(result_pool, result->copies);
364
365 svn_sort__array_reverse(result->entries, scratch_pool);
366 svn_sort__array(result->copies, copy_order);
367 svn_sort__array(result->copies_by_source, copy_by_source_order);
368 svn_sort__array(result->deletions, deletion_order);
369
370 /* Show that we are done. */
371 if (!baton->opt_state->quiet)
372 {
373 SVN_ERR(svn_cmdline_printf(scratch_pool, "\n"));
374 SVN_ERR(print_log_stats(result, scratch_pool));
375 }
376
377 result->unique_paths = NULL;
378 *log = result;
379
380 return SVN_NO_ERROR;
381 }
382
383 /* Append REVISION with the INHERITABLE setting to RANGES. RANGES must be
384 * sorted and REVISION must be larger than the largest revision in RANGES. */
385 static void
append_rev_to_ranges(svn_rangelist_t * ranges,svn_revnum_t revision,svn_boolean_t inheritable)386 append_rev_to_ranges(svn_rangelist_t *ranges,
387 svn_revnum_t revision,
388 svn_boolean_t inheritable)
389 {
390 /* In many cases, we can save memory by simply extending the last range. */
391 svn_merge_range_t *range;
392 if (ranges->nelts)
393 {
394 range = APR_ARRAY_IDX(ranges, ranges->nelts - 1, svn_merge_range_t *);
395 if (range->end + 1 == revision && range->inheritable == inheritable)
396 {
397 range->end = revision;
398 return;
399 }
400 }
401
402 /* We need to add a new range. */
403 range = apr_pcalloc(ranges->pool, sizeof(*range));
404 range->start = revision - 1;
405 range->end = revision;
406 range->inheritable = inheritable;
407
408 APR_ARRAY_PUSH(ranges, svn_merge_range_t *) = range;
409 }
410
411 /* Comparison function comparing the log_entry_t * in *LHS with the
412 * svn_revnum_t in *rhs.
413 */
414 static int
compare_rev_log_entry(const void * lhs,const void * rhs)415 compare_rev_log_entry(const void *lhs,
416 const void *rhs)
417 {
418 const log_entry_t *entry = *(const log_entry_t * const *)lhs;
419 svn_revnum_t revision = *(const svn_revnum_t *)rhs;
420
421 if (entry->revision < revision)
422 return -1;
423
424 return entry->revision == revision ? 0 : 1;
425 }
426
427 /* Restrict RANGE to the range of revisions covered by LOG. Cut-off from
428 * both sides will be added to RANGES. */
429 static void
restrict_range(svn_min__log_t * log,svn_merge_range_t * range,svn_rangelist_t * ranges)430 restrict_range(svn_min__log_t *log,
431 svn_merge_range_t *range,
432 svn_rangelist_t *ranges)
433 {
434 /* Cut off at the earliest revision. */
435 if (range->start + 1 < log->first_rev)
436 {
437 svn_merge_range_t *new_range
438 = apr_pmemdup(ranges->pool, range, sizeof(*range));
439 new_range->end = MIN(new_range->end, log->first_rev - 1);
440
441 APR_ARRAY_PUSH(ranges, svn_merge_range_t *) = new_range;
442 range->start = new_range->end;
443 }
444
445 /* Cut off at log HEAD. */
446 if (range->end > log->head_rev)
447 {
448 svn_merge_range_t *new_range
449 = apr_pmemdup(ranges->pool, range, sizeof(*range));
450 new_range->start = log->head_rev;
451
452 APR_ARRAY_PUSH(ranges, svn_merge_range_t *) = new_range;
453 range->end = new_range->start;
454 }
455 }
456
457 /* Return TRUE if PATH is either equal to, a parent of or sub-path of
458 * CHANGED_PATH. */
459 static svn_boolean_t
is_relevant(const char * changed_path,const char * path)460 is_relevant(const char *changed_path,
461 const char *path)
462 {
463 return svn_dirent_is_ancestor(changed_path, path)
464 || svn_dirent_is_ancestor(path, changed_path);
465 }
466
467 /* Return TRUE if PATH is either equal to, a parent of or sub-path of
468 * SUB_TREE. Ignore REVISION and BATON but keep it for a unified signature
469 * to be used with filter_ranges. */
470 static svn_boolean_t
in_subtree(const char * changed_path,const char * sub_tree,svn_revnum_t revision,const void * baton)471 in_subtree(const char *changed_path,
472 const char *sub_tree,
473 svn_revnum_t revision,
474 const void *baton)
475 {
476 return svn_dirent_is_ancestor(sub_tree, changed_path);
477 }
478
479 /* Return TRUE if
480 * - CHANGED_PATH is is either equal to or a sub-node of PATH, and
481 * - CHNAGED_PATH is outside the sub-tree given as BATON.
482 * Ignore REVISION. */
483 static svn_boolean_t
below_path_outside_subtree(const char * changed_path,const char * path,svn_revnum_t revision,const void * baton)484 below_path_outside_subtree(const char *changed_path,
485 const char *path,
486 svn_revnum_t revision,
487 const void *baton)
488 {
489 const char *subtree = baton;
490
491 /* Is this a change _below_ PATH but not within SUBTREE? */
492 return !svn_dirent_is_ancestor(subtree, changed_path)
493 && svn_dirent_is_ancestor(path, changed_path)
494 && strcmp(path, changed_path);
495 }
496
497 /* Baton struct to be used with change_outside_all_subtree_ranges. */
498 typedef struct change_outside_baton_t
499 {
500 /* Maps FS path to revision range lists. */
501 apr_hash_t *sibling_ranges;
502
503 /* Pool for temporary allocations.
504 * Baton users may clear this at will. */
505 apr_pool_t *iterpool;
506 } change_outside_baton_t;
507
508 /* Comparison function comparing range *LHS to revision *RHS. */
509 static int
compare_range_rev(const void * lhs,const void * rhs)510 compare_range_rev(const void *lhs,
511 const void *rhs)
512 {
513 const svn_merge_range_t *range = *(const svn_merge_range_t * const *)lhs;
514 svn_revnum_t revision = *(const svn_revnum_t *)rhs;
515
516 if (range->start >= revision)
517 return 1;
518
519 return range->end < revision ? 1 : 0;
520 }
521
522 /* Return TRUE if CHANGED_PATH is either equal to or a sub-node of PATH,
523 * CHNAGED_PATH@REVISION is not covered by BATON->SIBLING_RANGES. */
524 static svn_boolean_t
change_outside_all_subtree_ranges(const char * changed_path,const char * path,svn_revnum_t revision,const void * baton)525 change_outside_all_subtree_ranges(const char *changed_path,
526 const char *path,
527 svn_revnum_t revision,
528 const void *baton)
529 {
530 const change_outside_baton_t *b = baton;
531 svn_boolean_t missing = TRUE;
532 apr_size_t len;
533 svn_rangelist_t *ranges;
534
535 /* Don't collect changes outside the subtree starting at PARENT_PATH. */
536 if (!svn_dirent_is_ancestor(path, changed_path))
537 return FALSE;
538
539 svn_pool_clear(b->iterpool);
540
541 /* All branches that contain CHANGED_PATH, i.e. match either it or one
542 * of its parents, must mention REVISION in their mergeinfo. */
543 for (len = strlen(changed_path);
544 !svn_fspath__is_root(changed_path, len);
545 len = strlen(changed_path))
546 {
547 ranges = apr_hash_get(b->sibling_ranges, changed_path, len);
548 if (ranges)
549 {
550 /* If any of the matching branches does not list REVISION
551 * as already merged, we found an "outside" change. */
552 if (!svn_sort__array_lookup(ranges, &revision, NULL,
553 compare_range_rev))
554 return TRUE;
555
556 /* Mergeinfo for this path has been found. */
557 missing = FALSE;
558 }
559
560 changed_path = svn_fspath__dirname(changed_path, b->iterpool);
561 }
562
563 /* Record, if no mergeinfo has been found for this CHANGED_PATH. */
564 return missing;
565 }
566
567 /* In LOG, scan the revisions given in RANGES and return the revision /
568 * ranges that are relevant to PATH with respect to the CHANGE_RELEVANT
569 * criterion using BATON. Keep revisions that lie outside what is covered
570 * by LOG. Allocate the result in RESULT_POOL. */
571 static svn_rangelist_t *
filter_ranges(svn_min__log_t * log,const char * path,svn_rangelist_t * ranges,svn_boolean_t (* change_relavent)(const char *,const char *,svn_revnum_t,const void *),const void * baton,apr_pool_t * result_pool)572 filter_ranges(svn_min__log_t *log,
573 const char *path,
574 svn_rangelist_t *ranges,
575 svn_boolean_t (*change_relavent)(const char *,
576 const char *,
577 svn_revnum_t,
578 const void *),
579 const void *baton,
580 apr_pool_t *result_pool)
581 {
582 svn_rangelist_t *result;
583 int i, k, l;
584
585 /* Auto-complete parameters. */
586 if (!SVN_IS_VALID_REVNUM(log->first_rev))
587 return svn_rangelist_dup(ranges, result_pool);
588
589 result = apr_array_make(result_pool, 0, ranges->elt_size);
590 for (i = 0; i < ranges->nelts; ++i)
591 {
592 /* Next revision range to scan. */
593 svn_merge_range_t range
594 = *APR_ARRAY_IDX(ranges, i, const svn_merge_range_t *);
595 restrict_range(log, &range, result);
596
597 /* Find the range start and scan the range linearly. */
598 ++range.start;
599 for (k = svn_sort__bsearch_lower_bound(log->entries, &range.start,
600 compare_rev_log_entry);
601 k < log->entries->nelts;
602 ++k)
603 {
604 const log_entry_t *entry = APR_ARRAY_IDX(log->entries, k,
605 const log_entry_t *);
606 if (entry->revision > range.end)
607 break;
608
609 /* Skip revisions no relevant to PATH. */
610 if (!is_relevant(entry->common_base, path))
611 continue;
612
613 /* Look for any changed path that meets the filter criterion. */
614 for (l = 0; l < entry->paths->nelts; ++l)
615 {
616 const char *changed_path
617 = APR_ARRAY_IDX(entry->paths, l, const char *);
618
619 if (change_relavent(changed_path, path, entry->revision, baton))
620 {
621 append_rev_to_ranges(result, entry->revision,
622 range.inheritable);
623 break;
624 }
625 }
626 }
627 }
628
629 return result;
630 }
631
632 svn_rangelist_t *
svn_min__operative(svn_min__log_t * log,const char * path,svn_rangelist_t * ranges,apr_pool_t * result_pool)633 svn_min__operative(svn_min__log_t *log,
634 const char *path,
635 svn_rangelist_t *ranges,
636 apr_pool_t *result_pool)
637 {
638 return filter_ranges(log, path, ranges, in_subtree, NULL, result_pool);
639 }
640
641 svn_rangelist_t *
svn_min__operative_outside_subtree(svn_min__log_t * log,const char * path,const char * subtree,svn_rangelist_t * ranges,apr_pool_t * result_pool)642 svn_min__operative_outside_subtree(svn_min__log_t *log,
643 const char *path,
644 const char *subtree,
645 svn_rangelist_t *ranges,
646 apr_pool_t *result_pool)
647 {
648 return filter_ranges(log, path, ranges, below_path_outside_subtree,
649 subtree, result_pool);
650 }
651
652 svn_rangelist_t *
svn_min__operative_outside_all_subtrees(svn_min__log_t * log,const char * path,svn_rangelist_t * ranges,apr_hash_t * sibling_ranges,apr_pool_t * result_pool,apr_pool_t * scratch_pool)653 svn_min__operative_outside_all_subtrees(svn_min__log_t *log,
654 const char *path,
655 svn_rangelist_t *ranges,
656 apr_hash_t *sibling_ranges,
657 apr_pool_t *result_pool,
658 apr_pool_t *scratch_pool)
659 {
660 svn_rangelist_t *result;
661 change_outside_baton_t baton;
662 baton.sibling_ranges = sibling_ranges;
663 baton.iterpool = svn_pool_create(scratch_pool);
664
665 result = filter_ranges(log, path, ranges, change_outside_all_subtree_ranges,
666 &baton, result_pool);
667 svn_pool_destroy(baton.iterpool);
668
669 return result;
670 }
671
672 svn_revnum_t
svn_min__find_deletion(svn_min__log_t * log,const char * path,svn_revnum_t start_rev,svn_revnum_t end_rev,apr_pool_t * scratch_pool)673 svn_min__find_deletion(svn_min__log_t *log,
674 const char *path,
675 svn_revnum_t start_rev,
676 svn_revnum_t end_rev,
677 apr_pool_t *scratch_pool)
678 {
679 svn_revnum_t latest = SVN_INVALID_REVNUM;
680
681 deletion_t *to_find = apr_pcalloc(scratch_pool, sizeof(*to_find));
682 to_find->path = path;
683 to_find->revision = end_rev;
684
685 /* Auto-complete parameters. */
686 if (!SVN_IS_VALID_REVNUM(start_rev))
687 start_rev = log->head_rev;
688
689 /* Walk up the tree and find the latest deletion of PATH or any of
690 * its parents. */
691 while (!svn_fspath__is_root(to_find->path, strlen(to_find->path)))
692 {
693 int i;
694 for (i = svn_sort__bsearch_lower_bound(log->deletions, &to_find,
695 deletion_order);
696 i < log->deletions->nelts;
697 ++i)
698 {
699 const deletion_t *deletion = APR_ARRAY_IDX(log->deletions, i,
700 const deletion_t *);
701 if (strcmp(deletion->path, to_find->path))
702 break;
703 if (deletion->revision > start_rev)
704 break;
705
706 latest = deletion->revision;
707 to_find->revision = deletion->revision;
708 }
709
710 to_find->path = svn_fspath__dirname(to_find->path, scratch_pool);
711 }
712
713 return latest;
714 }
715
716 apr_array_header_t *
svn_min__find_deletions(svn_min__log_t * log,const char * path,apr_pool_t * result_pool,apr_pool_t * scratch_pool)717 svn_min__find_deletions(svn_min__log_t *log,
718 const char *path,
719 apr_pool_t *result_pool,
720 apr_pool_t *scratch_pool)
721 {
722 apr_array_header_t *result = apr_array_make(result_pool, 0,
723 sizeof(svn_revnum_t));
724 int source, dest;
725
726 deletion_t *to_find = apr_pcalloc(scratch_pool, sizeof(*to_find));
727 to_find->path = path;
728 to_find->revision = 0;
729
730 /* Find deletions for PATH and its parents. */
731 if (!svn_fspath__is_root(to_find->path, strlen(to_find->path)))
732 {
733 int i;
734 for (i = svn_sort__bsearch_lower_bound(log->deletions, &to_find,
735 deletion_order);
736 i < log->deletions->nelts;
737 ++i)
738 {
739 const deletion_t *deletion = APR_ARRAY_IDX(log->deletions, i,
740 const deletion_t *);
741 if (strcmp(deletion->path, to_find->path))
742 break;
743
744 APR_ARRAY_PUSH(result, svn_revnum_t) = deletion->revision;
745 }
746
747 to_find->path = svn_fspath__dirname(to_find->path, scratch_pool);
748 }
749
750 /* Remove any duplicates (unlikely but possible). */
751 svn_sort__array(result, svn_sort_compare_revisions);
752 for (source = 1, dest = 0; source < result->nelts; ++source)
753 {
754 svn_revnum_t source_rev = APR_ARRAY_IDX(result, source, svn_revnum_t);
755 svn_revnum_t dest_rev = APR_ARRAY_IDX(result, dest, svn_revnum_t);
756 if (source_rev != dest_rev)
757 {
758 ++dest_rev;
759 APR_ARRAY_IDX(result, dest, svn_revnum_t) = source_rev;
760 }
761 }
762
763 if (result->nelts)
764 result->nelts = dest + 1;
765
766 return result;
767 }
768
769 /* Starting at REVISION, scan LOG for the next (in REVISION or older) copy
770 * that creates PATH explicitly or implicitly by creating a parent of it.
771 * Return the copy operation found or NULL if none exists. Use SCRATCH_POOL
772 * for temporary allocations. */
773 static const svn_min__copy_t *
next_copy(svn_min__log_t * log,const char * path,svn_revnum_t revision,apr_pool_t * scratch_pool)774 next_copy(svn_min__log_t *log,
775 const char *path,
776 svn_revnum_t revision,
777 apr_pool_t *scratch_pool)
778 {
779 const svn_min__copy_t *copy = NULL;
780 int idx;
781
782 svn_min__copy_t *to_find = apr_pcalloc(scratch_pool, sizeof(*to_find));
783 to_find->path = path;
784 to_find->revision = revision;
785
786 idx = svn_sort__bsearch_lower_bound(log->copies, &to_find, copy_order);
787 if (idx < log->copies->nelts)
788 {
789 /* Found an exact match? */
790 copy = APR_ARRAY_IDX(log->copies, idx, const svn_min__copy_t *);
791 if (copy->revision != revision || strcmp(copy->path, path))
792 copy = NULL;
793 }
794
795 if (!copy && idx > 0)
796 {
797 /* No exact match. The predecessor may be the closest copy. */
798 copy = APR_ARRAY_IDX(log->copies, idx - 1, const svn_min__copy_t *);
799 if (strcmp(copy->path, path))
800 copy = NULL;
801 }
802
803 /* Mabye, the parent folder got copied later, i.e. is the closest copy.
804 We implicitly recurse up the tree. */
805 if (!svn_fspath__is_root(to_find->path, strlen(to_find->path)))
806 {
807 const svn_min__copy_t *parent_copy;
808 to_find->path = svn_fspath__dirname(to_find->path, scratch_pool);
809
810 parent_copy = next_copy(log, to_find->path, revision, scratch_pool);
811 if (!copy)
812 copy = parent_copy;
813 else if (parent_copy && parent_copy->revision > copy->revision)
814 copy = parent_copy;
815 }
816
817 return copy;
818 }
819
820 svn_revnum_t
svn_min__find_copy(svn_min__log_t * log,const char * path,svn_revnum_t start_rev,svn_revnum_t end_rev,apr_pool_t * scratch_pool)821 svn_min__find_copy(svn_min__log_t *log,
822 const char *path,
823 svn_revnum_t start_rev,
824 svn_revnum_t end_rev,
825 apr_pool_t *scratch_pool)
826 {
827 const svn_min__copy_t *copy;
828
829 /* Auto-complete parameters. */
830 if (!SVN_IS_VALID_REVNUM(start_rev))
831 start_rev = log->head_rev;
832
833 /* The actual lookup. */
834 copy = next_copy(log, path, start_rev, scratch_pool);
835 if (copy && copy->revision >= end_rev)
836 return copy->revision;
837
838 return SVN_NO_ERROR;
839 }
840
841 apr_array_header_t *
svn_min__get_copies(svn_min__log_t * log,const char * path,svn_revnum_t start_rev,svn_revnum_t end_rev,apr_pool_t * result_pool,apr_pool_t * scratch_pool)842 svn_min__get_copies(svn_min__log_t *log,
843 const char *path,
844 svn_revnum_t start_rev,
845 svn_revnum_t end_rev,
846 apr_pool_t *result_pool,
847 apr_pool_t *scratch_pool)
848 {
849 apr_array_header_t *result = apr_array_make(result_pool, 0,
850 sizeof(svn_min__copy_t *));
851 const svn_min__copy_t **copies = (void *)log->copies_by_source->elts;
852 int idx;
853
854 /* Find all sub-tree copies, including PATH. */
855 svn_min__copy_t *to_find = apr_pcalloc(scratch_pool, sizeof(*to_find));
856 to_find->copyfrom_path = path;
857 to_find->copyfrom_revision = end_rev;
858
859 for (idx = svn_sort__bsearch_lower_bound(log->copies_by_source,
860 &to_find,
861 copy_by_source_order);
862 (idx < log->copies->nelts)
863 && svn_dirent_is_ancestor(path, copies[idx]->copyfrom_path);
864 ++idx)
865 {
866 if (copies[idx]->copyfrom_revision <= start_rev)
867 APR_ARRAY_PUSH(result, const svn_min__copy_t *) = copies[idx];
868 }
869
870 /* Find all parent copies. */
871 while (!svn_fspath__is_root(to_find->copyfrom_path,
872 strlen(to_find->copyfrom_path)))
873 {
874 to_find->copyfrom_path = svn_fspath__dirname(to_find->copyfrom_path,
875 scratch_pool);
876
877 for (idx = svn_sort__bsearch_lower_bound(log->copies_by_source,
878 &to_find,
879 copy_by_source_order);
880 (idx < log->copies->nelts)
881 && !strcmp(copies[idx]->copyfrom_path, to_find->copyfrom_path)
882 && (copies[idx]->copyfrom_revision <= start_rev);
883 ++idx)
884 {
885 APR_ARRAY_PUSH(result, const svn_min__copy_t *) = copies[idx];
886 }
887 }
888
889 return result;
890 }
891
892 /* A history segment. Simply a FS path plus the revision range that it is
893 * part of the history of the node. */
894 typedef struct segment_t
895 {
896 /* FS path at which the node lives in this segment */
897 const char *path;
898
899 /* Revision that it appears in or that the history was truncated to. */
900 svn_revnum_t start;
901
902 /* Revision from which the node was copied to the next segment or the
903 * revision that the history was truncated to. */
904 svn_revnum_t end;
905 } segment_t;
906
907 apr_array_header_t *
svn_min__get_history(svn_min__log_t * log,const char * path,svn_revnum_t start_rev,svn_revnum_t end_rev,apr_pool_t * result_pool,apr_pool_t * scratch_pool)908 svn_min__get_history(svn_min__log_t *log,
909 const char *path,
910 svn_revnum_t start_rev,
911 svn_revnum_t end_rev,
912 apr_pool_t *result_pool,
913 apr_pool_t *scratch_pool)
914 {
915 segment_t *segment;
916 const svn_min__copy_t *copy;
917 apr_array_header_t *result = apr_array_make(result_pool, 16,
918 sizeof(segment_t *));
919
920 /* Auto-complete parameters. */
921 if (!SVN_IS_VALID_REVNUM(start_rev))
922 start_rev = log->head_rev;
923
924 /* Simply follow all copies, each time adding a segment from "here" to
925 * the next copy. */
926 for (copy = next_copy(log, path, start_rev, scratch_pool);
927 copy && start_rev >= end_rev;
928 copy = next_copy(log, path, start_rev, scratch_pool))
929 {
930 segment = apr_pcalloc(result_pool, sizeof(*segment));
931 segment->start = MAX(end_rev, copy->revision);
932 segment->end = start_rev;
933 segment->path = apr_pstrdup(result_pool, path);
934
935 APR_ARRAY_PUSH(result, segment_t *) = segment;
936
937 start_rev = copy->copyfrom_revision;
938 path = svn_fspath__join(copy->copyfrom_path,
939 svn_fspath__skip_ancestor(copy->path, path),
940 scratch_pool);
941 }
942
943 /* The final segment has no copy-from. */
944 if (start_rev >= end_rev)
945 {
946 segment = apr_pcalloc(result_pool, sizeof(*segment));
947 segment->start = end_rev;
948 segment->end = start_rev;
949 segment->path = apr_pstrdup(result_pool, path);
950
951 APR_ARRAY_PUSH(result, segment_t *) = segment;
952 }
953
954 return result;
955 }
956
957 apr_array_header_t *
svn_min__intersect_history(apr_array_header_t * lhs,apr_array_header_t * rhs,apr_pool_t * result_pool)958 svn_min__intersect_history(apr_array_header_t *lhs,
959 apr_array_header_t *rhs,
960 apr_pool_t *result_pool)
961 {
962 apr_array_header_t *result = apr_array_make(result_pool, 16,
963 sizeof(segment_t *));
964
965 int lhs_idx = 0;
966 int rhs_idx = 0;
967
968 /* Careful: the segments are ordered latest to oldest. */
969 while (lhs_idx < lhs->nelts && rhs_idx < rhs->nelts)
970 {
971 segment_t *lhs_segment = APR_ARRAY_IDX(lhs, lhs_idx, segment_t *);
972 segment_t *rhs_segment = APR_ARRAY_IDX(rhs, rhs_idx, segment_t *);
973
974 /* Skip non-overlapping revision segments */
975 if (lhs_segment->start > rhs_segment->end)
976 {
977 ++lhs_idx;
978 continue;
979 }
980 else if (lhs_segment->end < rhs_segment->start)
981 {
982 ++rhs_idx;
983 continue;
984 }
985
986 /* Revision ranges overlap. Also the same path? */
987 if (!strcmp(lhs_segment->path, rhs_segment->path))
988 {
989 segment_t *segment = apr_pcalloc(result_pool, sizeof(*segment));
990 segment->start = MAX(lhs_segment->start, rhs_segment->start);
991 segment->end = MIN(lhs_segment->end, rhs_segment->end);
992 segment->path = apr_pstrdup(result_pool, lhs_segment->path);
993
994 APR_ARRAY_PUSH(result, segment_t *) = segment;
995 }
996
997 /* The segment that starts earlier may overlap with another one.
998 If they should start at the same rev, the next iteration will
999 skip the respective other segment. */
1000 if (lhs_segment->start > rhs_segment->start)
1001 ++lhs_idx;
1002 else
1003 ++rhs_idx;
1004 }
1005
1006 return result;
1007 }
1008
1009 svn_rangelist_t *
svn_min__history_ranges(apr_array_header_t * history,apr_pool_t * result_pool)1010 svn_min__history_ranges(apr_array_header_t *history,
1011 apr_pool_t *result_pool)
1012 {
1013 svn_rangelist_t *result = apr_array_make(result_pool, history->nelts,
1014 sizeof(svn_merge_range_t *));
1015
1016 int i;
1017 for (i = 0; i < history->nelts; ++i)
1018 {
1019 const segment_t *segment = APR_ARRAY_IDX(history, i, segment_t *);
1020
1021 /* Convert to merge ranges. Note that start+1 is the first rev
1022 actually in that range. */
1023 svn_merge_range_t *range = apr_pcalloc(result_pool, sizeof(*range));
1024 range->start = MAX(0, segment->start - 1);
1025 range->end = segment->end;
1026 range->inheritable = TRUE;
1027
1028 APR_ARRAY_PUSH(result, svn_merge_range_t *) = range;
1029 }
1030
1031 return result;
1032 }
1033