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