Skip to main content

jj_lib/
copies.rs

1// Copyright 2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Code for working with copies and renames.
16
17use std::collections::HashMap;
18use std::collections::HashSet;
19use std::pin::Pin;
20use std::task::Context;
21use std::task::Poll;
22use std::task::ready;
23
24use futures::Stream;
25use futures::StreamExt as _;
26use futures::future::BoxFuture;
27use futures::future::ready;
28use futures::future::try_join_all;
29use futures::stream::Fuse;
30use futures::stream::FuturesOrdered;
31use indexmap::IndexMap;
32use indexmap::IndexSet;
33use itertools::Itertools as _;
34use pollster::FutureExt as _;
35
36use crate::backend::BackendError;
37use crate::backend::BackendResult;
38use crate::backend::CopyHistory;
39use crate::backend::CopyId;
40use crate::backend::CopyRecord;
41use crate::backend::TreeValue;
42use crate::dag_walk;
43use crate::merge::Diff;
44use crate::merge::Merge;
45use crate::merge::MergedTreeValue;
46use crate::merge::SameChange;
47use crate::merged_tree::MergedTree;
48use crate::merged_tree::TreeDiffEntry;
49use crate::merged_tree::TreeDiffStream;
50use crate::repo_path::RepoPath;
51use crate::repo_path::RepoPathBuf;
52
53/// A collection of CopyRecords.
54#[derive(Default, Debug)]
55pub struct CopyRecords {
56    records: Vec<CopyRecord>,
57    // Maps from `source` or `target` to the index of the entry in `records`.
58    // Conflicts are excluded by keeping an out of range value.
59    sources: HashMap<RepoPathBuf, usize>,
60    targets: HashMap<RepoPathBuf, usize>,
61}
62
63impl CopyRecords {
64    /// Adds information about `CopyRecord`s to `self`. A target with multiple
65    /// conflicts is discarded and treated as not having an origin.
66    pub fn add_records(&mut self, copy_records: impl IntoIterator<Item = CopyRecord>) {
67        for r in copy_records {
68            self.sources
69                .entry(r.source.clone())
70                // TODO: handle conflicts instead of ignoring both sides.
71                .and_modify(|value| *value = usize::MAX)
72                .or_insert(self.records.len());
73            self.targets
74                .entry(r.target.clone())
75                // TODO: handle conflicts instead of ignoring both sides.
76                .and_modify(|value| *value = usize::MAX)
77                .or_insert(self.records.len());
78            self.records.push(r);
79        }
80    }
81
82    /// Returns true if there are copy records associated with a source path.
83    pub fn has_source(&self, source: &RepoPath) -> bool {
84        self.sources.contains_key(source)
85    }
86
87    /// Gets any copy record associated with a source path.
88    pub fn for_source(&self, source: &RepoPath) -> Option<&CopyRecord> {
89        self.sources.get(source).and_then(|&i| self.records.get(i))
90    }
91
92    /// Returns true if there are copy records associated with a target path.
93    pub fn has_target(&self, target: &RepoPath) -> bool {
94        self.targets.contains_key(target)
95    }
96
97    /// Gets any copy record associated with a target path.
98    pub fn for_target(&self, target: &RepoPath) -> Option<&CopyRecord> {
99        self.targets.get(target).and_then(|&i| self.records.get(i))
100    }
101
102    /// Gets all copy records.
103    pub fn iter(&self) -> impl Iterator<Item = &CopyRecord> {
104        self.records.iter()
105    }
106}
107
108/// Whether or not the source path was deleted.
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum CopyOperation {
111    /// The source path was not deleted.
112    Copy,
113    /// The source path was renamed to the destination.
114    Rename,
115}
116
117/// A `TreeDiffEntry` with copy information.
118#[derive(Debug)]
119pub struct CopiesTreeDiffEntry {
120    /// The path.
121    pub path: CopiesTreeDiffEntryPath,
122    /// The resolved tree values if available.
123    pub values: BackendResult<Diff<MergedTreeValue>>,
124}
125
126/// Path and copy information of `CopiesTreeDiffEntry`.
127#[derive(Clone, Debug, Eq, PartialEq)]
128pub struct CopiesTreeDiffEntryPath {
129    /// The source path and copy information if this is a copy or rename.
130    pub source: Option<(RepoPathBuf, CopyOperation)>,
131    /// The target path.
132    pub target: RepoPathBuf,
133}
134
135impl CopiesTreeDiffEntryPath {
136    /// The source path.
137    pub fn source(&self) -> &RepoPath {
138        self.source.as_ref().map_or(&self.target, |(path, _)| path)
139    }
140
141    /// The target path.
142    pub fn target(&self) -> &RepoPath {
143        &self.target
144    }
145
146    /// Whether this entry was copied or renamed from the source. Returns `None`
147    /// if the path is unchanged.
148    pub fn copy_operation(&self) -> Option<CopyOperation> {
149        self.source.as_ref().map(|(_, op)| *op)
150    }
151
152    /// Returns source/target paths as [`Diff`] if they differ.
153    pub fn to_diff(&self) -> Option<Diff<&RepoPath>> {
154        let (source, _) = self.source.as_ref()?;
155        Some(Diff::new(source, &self.target))
156    }
157}
158
159/// Wraps a `TreeDiffStream`, adding support for copies and renames.
160pub struct CopiesTreeDiffStream<'a> {
161    inner: TreeDiffStream<'a>,
162    source_tree: MergedTree,
163    target_tree: MergedTree,
164    copy_records: &'a CopyRecords,
165}
166
167impl<'a> CopiesTreeDiffStream<'a> {
168    /// Create a new diff stream with copy information.
169    pub fn new(
170        inner: TreeDiffStream<'a>,
171        source_tree: MergedTree,
172        target_tree: MergedTree,
173        copy_records: &'a CopyRecords,
174    ) -> Self {
175        Self {
176            inner,
177            source_tree,
178            target_tree,
179            copy_records,
180        }
181    }
182
183    async fn resolve_copy_source(
184        &self,
185        source: &RepoPath,
186        values: BackendResult<Diff<MergedTreeValue>>,
187    ) -> BackendResult<(CopyOperation, Diff<MergedTreeValue>)> {
188        let target_value = values?.after;
189        let source_value = self.source_tree.path_value(source).await?;
190        // If the source path is deleted in the target tree, it's a rename.
191        let source_value_at_target = self.target_tree.path_value(source).await?;
192        let copy_op = if source_value_at_target.is_absent() || source_value_at_target.is_tree() {
193            CopyOperation::Rename
194        } else {
195            CopyOperation::Copy
196        };
197        Ok((copy_op, Diff::new(source_value, target_value)))
198    }
199}
200
201impl Stream for CopiesTreeDiffStream<'_> {
202    type Item = CopiesTreeDiffEntry;
203
204    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
205        while let Some(diff_entry) = ready!(self.inner.as_mut().poll_next(cx)) {
206            let Some(CopyRecord { source, .. }) = self.copy_records.for_target(&diff_entry.path)
207            else {
208                let target_deleted =
209                    matches!(&diff_entry.values, Ok(diff) if diff.after.is_absent());
210                if target_deleted && self.copy_records.has_source(&diff_entry.path) {
211                    // Skip the "delete" entry when there is a rename.
212                    continue;
213                }
214                return Poll::Ready(Some(CopiesTreeDiffEntry {
215                    path: CopiesTreeDiffEntryPath {
216                        source: None,
217                        target: diff_entry.path,
218                    },
219                    values: diff_entry.values,
220                }));
221            };
222
223            let (copy_op, values) = match self
224                .resolve_copy_source(source, diff_entry.values)
225                .block_on()
226            {
227                Ok((copy_op, values)) => (copy_op, Ok(values)),
228                // Fall back to "copy" (= path still exists) if unknown.
229                Err(err) => (CopyOperation::Copy, Err(err)),
230            };
231            return Poll::Ready(Some(CopiesTreeDiffEntry {
232                path: CopiesTreeDiffEntryPath {
233                    source: Some((source.clone(), copy_op)),
234                    target: diff_entry.path,
235                },
236                values,
237            }));
238        }
239
240        Poll::Ready(None)
241    }
242}
243
244/// Maps `CopyId`s to `CopyHistory`s
245pub type CopyGraph = IndexMap<CopyId, CopyHistory>;
246
247fn collect_descendants(copy_graph: &CopyGraph) -> IndexMap<CopyId, IndexSet<CopyId>> {
248    let mut ancestor_map: IndexMap<CopyId, IndexSet<CopyId>> = IndexMap::new();
249
250    // Collect ancestors
251    //
252    // Keys in the map will be ordered with parents before children. The set of
253    // ancestors for a given key will also be ordered with parents before
254    // children.
255    let heads = dag_walk::heads(
256        copy_graph.keys(),
257        |id| *id,
258        |id| copy_graph[*id].parents.iter(),
259    )
260    .into_iter()
261    .sorted()
262    .collect_vec();
263    for id in dag_walk::topo_order_forward(
264        heads,
265        |id| *id,
266        |id| copy_graph[*id].parents.iter(),
267        |id| panic!("Cycle detected in copy history graph involving CopyId {id}"),
268    )
269    .expect("Could not walk CopyGraph")
270    {
271        // For each ID we visit, we should have visited all of its parents first.
272        let mut ancestors = IndexSet::new();
273        for parent in &copy_graph[id].parents {
274            ancestors.extend(ancestor_map[parent].iter().cloned());
275            ancestors.insert(parent.clone());
276        }
277        ancestor_map.insert(id.clone(), ancestors);
278    }
279
280    // Reverse ancestor map to descendant map
281    let mut result: IndexMap<CopyId, IndexSet<CopyId>> = IndexMap::new();
282    for (id, ancestors) in ancestor_map {
283        for ancestor in ancestors {
284            result.entry(ancestor).or_default().insert(id.clone());
285        }
286        // Make sure every CopyId in the graph has an entry in the descendants map, even
287        // if it has no descendants of its own.
288        result.entry(id.clone()).or_default();
289    }
290    result
291}
292
293/// Iterate over the ancestors of a starting CopyId, visiting children before
294/// parents. The `CopyGraph` argument should be sorted in topological order.
295fn iterate_ancestors<'a>(
296    copies: &'a CopyGraph,
297    initial_id: &'a CopyId,
298) -> impl Iterator<Item = &'a CopyId> {
299    let mut valid = HashSet::from([initial_id]);
300    copies.iter().filter_map(move |(id, history)| {
301        if valid.contains(id) {
302            valid.extend(history.parents.iter());
303            Some(id)
304        } else {
305            None
306        }
307    })
308}
309
310/// Returns whether `maybe_child` is a descendant of `parent`
311pub fn is_ancestor(copies: &CopyGraph, ancestor: &CopyId, descendant: &CopyId) -> bool {
312    for history in dag_walk::dfs(
313        [descendant],
314        |id| *id,
315        |id| copies.get(*id).unwrap().parents.iter(),
316    ) {
317        if history == ancestor {
318            return true;
319        }
320    }
321    false
322}
323
324/// Describes the source of a CopyHistoryDiffTerm
325#[derive(Clone, Debug, Eq, Hash, PartialEq)]
326pub enum CopyHistorySource {
327    /// The file was copied from a source at a different path
328    Copy(RepoPathBuf),
329    /// The file was renamed from a source at a different path
330    Rename(RepoPathBuf),
331    /// The source and target have the same path
332    Normal,
333}
334
335/// Describes a single term of a copy-aware diff
336#[derive(Debug, Eq, Hash, PartialEq)]
337pub struct CopyHistoryDiffTerm {
338    /// The current value of the target, if present
339    pub target_value: Option<TreeValue>,
340    /// List of sources, whether they were copied, renamed, or neither, and the
341    /// original value
342    pub sources: Vec<(CopyHistorySource, MergedTreeValue)>,
343}
344
345/// Like a `TreeDiffEntry`, but takes `CopyHistory`s into account
346#[derive(Debug)]
347pub struct CopyHistoryTreeDiffEntry {
348    /// The final source path (after copy/rename if applicable)
349    pub target_path: RepoPathBuf,
350    /// The resolved values for the target and source(s), if available
351    pub diffs: BackendResult<Merge<CopyHistoryDiffTerm>>,
352}
353
354impl CopyHistoryTreeDiffEntry {
355    // Simple conversion case where no copy tracing is needed
356    fn normal(diff_entry: TreeDiffEntry) -> Self {
357        let target_path = diff_entry.path;
358        let diffs = diff_entry.values.map(|diff| {
359            let sources = if diff.before.is_absent() {
360                vec![]
361            } else {
362                vec![(CopyHistorySource::Normal, diff.before)]
363            };
364            diff.after.into_map(|target_value| CopyHistoryDiffTerm {
365                target_value,
366                sources: sources.clone(),
367            })
368        });
369        Self { target_path, diffs }
370    }
371}
372
373/// Adapts a `TreeDiffStream` to follow copies / renames.
374pub struct CopyHistoryDiffStream<'a> {
375    inner: Fuse<TreeDiffStream<'a>>,
376    before_tree: &'a MergedTree,
377    after_tree: &'a MergedTree,
378    pending: FuturesOrdered<BoxFuture<'static, CopyHistoryTreeDiffEntry>>,
379}
380
381impl<'a> CopyHistoryDiffStream<'a> {
382    /// Creates an iterator over the differences between two trees, taking copy
383    /// history into account. Generally prefer
384    /// `MergedTree::diff_stream_with_copy_history()` instead of calling this
385    /// directly.
386    pub fn new(
387        inner: TreeDiffStream<'a>,
388        before_tree: &'a MergedTree,
389        after_tree: &'a MergedTree,
390    ) -> Self {
391        Self {
392            inner: inner.fuse(),
393            before_tree,
394            after_tree,
395            pending: FuturesOrdered::new(),
396        }
397    }
398}
399
400impl Stream for CopyHistoryDiffStream<'_> {
401    type Item = CopyHistoryTreeDiffEntry;
402
403    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
404        loop {
405            // First, check if we have newly-finished futures. If this returns Pending, we
406            // intentionally fall through to poll `self.inner`.
407            if let Poll::Ready(Some(next)) = self.pending.poll_next_unpin(cx) {
408                return Poll::Ready(Some(next));
409            }
410
411            // If we didn't have queued results above, we want to check our wrapped stream
412            // for the next non-copy-matched diff entry.
413            let next_diff_entry = match ready!(self.inner.poll_next_unpin(cx)) {
414                Some(diff_entry) => diff_entry,
415                None if self.pending.is_empty() => return Poll::Ready(None),
416                _ => return Poll::Pending,
417            };
418
419            let Ok(Diff { before, after }) = &next_diff_entry.values else {
420                self.pending
421                    .push_back(Box::pin(ready(CopyHistoryTreeDiffEntry::normal(
422                        next_diff_entry,
423                    ))));
424                continue;
425            };
426
427            // Don't try copy-tracing if we have conflicts on either side.
428            //
429            // TODO: consider accepting conflicts if the copy IDs can be resolved.
430            let Some(before) = before.as_resolved() else {
431                self.pending
432                    .push_back(Box::pin(ready(CopyHistoryTreeDiffEntry::normal(
433                        next_diff_entry,
434                    ))));
435                continue;
436            };
437            let Some(after) = after.as_resolved() else {
438                self.pending
439                    .push_back(Box::pin(ready(CopyHistoryTreeDiffEntry::normal(
440                        next_diff_entry,
441                    ))));
442                continue;
443            };
444
445            match (before, after) {
446                // If we have files with matching copy_ids, no need to do copy-tracing.
447                (
448                    Some(TreeValue::File { copy_id: id1, .. }),
449                    Some(TreeValue::File { copy_id: id2, .. }),
450                ) if id1 == id2 => {
451                    self.pending
452                        .push_back(Box::pin(ready(CopyHistoryTreeDiffEntry::normal(
453                            next_diff_entry,
454                        ))));
455                }
456
457                (other, Some(f @ TreeValue::File { .. })) => {
458                    if let Some(other) = other {
459                        // For files with non-matching copy-ids, or for a non-file that changes to a
460                        // file, mark the first as deleted and do copy-tracing on the second.
461                        //
462                        // NOTE[deletion-diff-entry]: this may emit two diff entries, where the old
463                        // diffstream would contain only one (even with gix's heuristic-based copy
464                        // detection).
465                        //
466                        // This may be desirable in some cases (such as replacing a file X with a
467                        // copy of some other file Y; the deletion entry makes it more clear that
468                        // the original X was replaced by a formerly unrelated file). It is less
469                        // desirable in cases where the new file shares some actual relation to the
470                        // old one.
471                        //
472                        // We plan to improve this in the near future, but for now we'll keep the
473                        // simpler implementation since this behavior is not visible outside of
474                        // tests yet.
475                        self.pending
476                            .push_back(Box::pin(ready(CopyHistoryTreeDiffEntry {
477                                target_path: next_diff_entry.path.clone(),
478                                diffs: Ok(Merge::resolved(CopyHistoryDiffTerm {
479                                    target_value: None,
480                                    sources: vec![(
481                                        CopyHistorySource::Normal,
482                                        Merge::resolved(Some(other.clone())),
483                                    )],
484                                })),
485                            })));
486                    }
487
488                    let future = tree_diff_entry_from_copies(
489                        self.before_tree.clone(),
490                        self.after_tree.clone(),
491                        f.clone(),
492                        next_diff_entry.path.clone(),
493                    );
494                    self.pending.push_back(Box::pin(future));
495                }
496
497                // Anything else (e.g. file => non-file non-tree), issue a simple diff entry.
498                //
499                // NOTE[deletion-diff-entry2]: this is another point where a spurious deletion entry
500                // can be generated; we have a planned fix in the works.
501                _ => self
502                    .pending
503                    .push_back(Box::pin(ready(CopyHistoryTreeDiffEntry::normal(
504                        next_diff_entry,
505                    )))),
506            }
507        }
508    }
509}
510
511async fn tree_diff_entry_from_copies(
512    before_tree: MergedTree,
513    after_tree: MergedTree,
514    file: TreeValue,
515    target_path: RepoPathBuf,
516) -> CopyHistoryTreeDiffEntry {
517    CopyHistoryTreeDiffEntry {
518        target_path,
519        diffs: diffs_from_copies(before_tree, after_tree, file).await,
520    }
521}
522
523async fn diffs_from_copies(
524    before_tree: MergedTree,
525    after_tree: MergedTree,
526    after_file: TreeValue,
527) -> BackendResult<Merge<CopyHistoryDiffTerm>> {
528    let copy_id = after_file.copy_id().ok_or(BackendError::Other(
529        "Expected TreeValue::File with a CopyId".into(),
530    ))?;
531    let copy_graph: CopyGraph = before_tree
532        .store()
533        .backend()
534        .get_related_copies(copy_id)
535        .await?
536        .into_iter()
537        .map(|related| (related.id, related.history))
538        .collect();
539
540    let descendants = collect_descendants(&copy_graph);
541    let copies =
542        find_diff_sources_from_copies(&before_tree, copy_id, &copy_graph, &descendants).await?;
543
544    try_join_all(copies.into_iter().map(async |(before_path, before_val)| {
545        classify_source(
546            &after_tree,
547            copy_id,
548            before_path,
549            before_val
550                .copy_id()
551                .expect("expected TreeValue::File with a CopyId"),
552            &copy_graph,
553        )
554        .await
555        .map(|source| (source, Merge::resolved(Some(before_val))))
556    }))
557    .await
558    .map(|sources| {
559        Merge::resolved(CopyHistoryDiffTerm {
560            target_value: Some(after_file),
561            sources,
562        })
563    })
564}
565
566async fn classify_source(
567    after_tree: &MergedTree,
568    after_id: &CopyId,
569    before_path: RepoPathBuf,
570    before_id: &CopyId,
571    copy_graph: &CopyGraph,
572) -> BackendResult<CopyHistorySource> {
573    let history = copy_graph
574        .get(after_id)
575        .expect("copy_graph should already include after_id");
576    let after_path = &history.current_path;
577
578    // First, check to see if we're looking at the same path with different copy
579    // IDs, but an ancestor relationship between the histories. If so, this is a
580    // "normal" diff source.
581    if *after_path == before_path
582        && (is_ancestor(copy_graph, after_id, before_id)
583            || is_ancestor(copy_graph, before_id, after_id))
584    {
585        return Ok(CopyHistorySource::Normal);
586    }
587
588    let after_tree_before_path_val = after_tree.path_value(&before_path).await?;
589    // We're getting our arguments from `find_diff_sources_from_copies`, so we
590    // shouldn't have to worry about missing paths or conflicts. So let's just
591    // be lazy and `.expect()` our way out of all the `Option`s.
592    let Some(after_tree_before_path_id) = after_tree_before_path_val
593        .to_copy_id_merge()
594        .expect("expected merge of `TreeValue::File`s")
595        .resolve_trivial(SameChange::Accept)
596        .expect("expected no CopyId conflicts")
597        .clone()
598    else {
599        // before_path is no longer present in after_tree
600        return Ok(CopyHistorySource::Rename(before_path));
601    };
602
603    if is_ancestor(copy_graph, before_id, &after_tree_before_path_id)
604        || is_ancestor(copy_graph, &after_tree_before_path_id, before_id)
605    {
606        Ok(CopyHistorySource::Copy(before_path))
607    } else {
608        //  before_path in before_tree & after_tree are not ancestors/descendants of
609        //  each other
610        Ok(CopyHistorySource::Rename(before_path))
611    }
612}
613
614async fn find_diff_sources_from_copies(
615    tree: &MergedTree,
616    copy_id: &CopyId,
617    copy_graph: &CopyGraph,
618    descendants: &IndexMap<CopyId, IndexSet<CopyId>>,
619) -> BackendResult<Vec<(RepoPathBuf, TreeValue)>> {
620    // Related copies MUST contain ancestors AND descendants. It may also contain
621    // unrelated copies.
622    let history = copy_graph.get(copy_id).ok_or(BackendError::Other(
623        "CopyId should be present in `get_related_copies()` result".into(),
624    ))?;
625
626    if history.parents.is_empty() {
627        // If there are no parents, let's look for a descendant (this handles
628        // the reverse-diff case of a file rename.
629        for descendant_id in &descendants[copy_id] {
630            if let Some(descendant) = tree.copy_value(descendant_id).await? {
631                return Ok(vec![(
632                    copy_graph[descendant_id].current_path.clone(),
633                    descendant,
634                )]);
635            }
636        }
637    }
638
639    let mut sources = vec![];
640
641    // Finds at most one related TreeValue::File present in `tree` per parent listed
642    // in `file`'s CopyHistory.
643    //
644    // TODO: this correctly finds the shallowest relative, but it only finds
645    // one. I'm not sure what is the best thing to do when one of our parents
646    // itself has multiple parents. E.g., if we have a CopyHistory graph like
647    //
648    //      D
649    //      |
650    //      C
651    //     / \
652    //    A   B
653    //
654    // where D is `file`, C is its parent but is not present in `tree`, but both A
655    // and B are present, this will find either A or B, not both. Should we
656    // return both A and B instead? I don't think there's a way to do that with
657    // the current dag_walk functions. Do we care enough to implement something
658    // new there that pays more attention to the depth in the DAG? Perhaps
659    // a variant of closest_common_nodes?
660    'parents: for parent_copy_id in &history.parents {
661        let mut absent_ancestors = vec![];
662
663        // First, try to find the parent or a direct ancestor in the tree
664        for ancestor_id in iterate_ancestors(copy_graph, parent_copy_id) {
665            let ancestor_history = copy_graph.get(ancestor_id).ok_or(BackendError::Other(
666                "Ancestor CopyId should be present in `get_related_copies()` result".into(),
667            ))?;
668            if let Some(ancestor) = tree.copy_value(ancestor_id).await? {
669                sources.push((ancestor_history.current_path.clone(), ancestor));
670                continue 'parents;
671            } else {
672                absent_ancestors.push(ancestor_id);
673            }
674        }
675
676        // If not, then try descendants of the parent
677        //
678        // TODO: This will find a relative, when what we really want is probably the
679        // "closest" relative.
680        for descendant_id in &descendants[parent_copy_id] {
681            if let Some(descendant) = tree.copy_value(descendant_id).await? {
682                sources.push((copy_graph[descendant_id].current_path.clone(), descendant));
683                continue 'parents;
684            }
685        }
686
687        // Finally, try descendants of any ancestor
688        //
689        // TODO: This will find a relative, when what we really want is probably the
690        // "closest" relative.
691        for ancestor_id in absent_ancestors {
692            for descendant_id in descendants[ancestor_id].difference(&descendants[parent_copy_id]) {
693                if let Some(descendant) = tree.copy_value(descendant_id).await? {
694                    sources.push((copy_graph[descendant_id].current_path.clone(), descendant));
695                    continue 'parents;
696                }
697            }
698        }
699    }
700    Ok(sources)
701}