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