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