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::pin::Pin;
19use std::task::Context;
20use std::task::Poll;
21use std::task::ready;
22
23use futures::Stream;
24
25use crate::backend::BackendResult;
26use crate::backend::CopyRecord;
27use crate::merge::Diff;
28use crate::merge::MergedTreeValue;
29use crate::merged_tree::MergedTree;
30use crate::merged_tree::TreeDiffStream;
31use crate::repo_path::RepoPath;
32use crate::repo_path::RepoPathBuf;
33
34/// A collection of CopyRecords.
35#[derive(Default, Debug)]
36pub struct CopyRecords {
37    records: Vec<CopyRecord>,
38    // Maps from `source` or `target` to the index of the entry in `records`.
39    // Conflicts are excluded by keeping an out of range value.
40    sources: HashMap<RepoPathBuf, usize>,
41    targets: HashMap<RepoPathBuf, usize>,
42}
43
44impl CopyRecords {
45    /// Adds information about `CopyRecord`s to `self`. A target with multiple
46    /// conflicts is discarded and treated as not having an origin.
47    pub fn add_records(
48        &mut self,
49        copy_records: impl IntoIterator<Item = BackendResult<CopyRecord>>,
50    ) -> BackendResult<()> {
51        for record in copy_records {
52            let r = record?;
53            self.sources
54                .entry(r.source.clone())
55                // TODO: handle conflicts instead of ignoring both sides.
56                .and_modify(|value| *value = usize::MAX)
57                .or_insert(self.records.len());
58            self.targets
59                .entry(r.target.clone())
60                // TODO: handle conflicts instead of ignoring both sides.
61                .and_modify(|value| *value = usize::MAX)
62                .or_insert(self.records.len());
63            self.records.push(r);
64        }
65        Ok(())
66    }
67
68    /// Returns true if there are copy records associated with a source path.
69    pub fn has_source(&self, source: &RepoPath) -> bool {
70        self.sources.contains_key(source)
71    }
72
73    /// Gets any copy record associated with a source path.
74    pub fn for_source(&self, source: &RepoPath) -> Option<&CopyRecord> {
75        self.sources.get(source).and_then(|&i| self.records.get(i))
76    }
77
78    /// Returns true if there are copy records associated with a target path.
79    pub fn has_target(&self, target: &RepoPath) -> bool {
80        self.targets.contains_key(target)
81    }
82
83    /// Gets any copy record associated with a target path.
84    pub fn for_target(&self, target: &RepoPath) -> Option<&CopyRecord> {
85        self.targets.get(target).and_then(|&i| self.records.get(i))
86    }
87
88    /// Gets all copy records.
89    pub fn iter(&self) -> impl Iterator<Item = &CopyRecord> {
90        self.records.iter()
91    }
92}
93
94/// Whether or not the source path was deleted.
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum CopyOperation {
97    /// The source path was not deleted.
98    Copy,
99    /// The source path was renamed to the destination.
100    Rename,
101}
102
103/// A `TreeDiffEntry` with copy information.
104#[derive(Debug)]
105pub struct CopiesTreeDiffEntry {
106    /// The path.
107    pub path: CopiesTreeDiffEntryPath,
108    /// The resolved tree values if available.
109    pub values: BackendResult<Diff<MergedTreeValue>>,
110}
111
112/// Path and copy information of `CopiesTreeDiffEntry`.
113#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct CopiesTreeDiffEntryPath {
115    /// The source path and copy information if this is a copy or rename.
116    pub source: Option<(RepoPathBuf, CopyOperation)>,
117    /// The target path.
118    pub target: RepoPathBuf,
119}
120
121impl CopiesTreeDiffEntryPath {
122    /// The source path.
123    pub fn source(&self) -> &RepoPath {
124        self.source.as_ref().map_or(&self.target, |(path, _)| path)
125    }
126
127    /// The target path.
128    pub fn target(&self) -> &RepoPath {
129        &self.target
130    }
131
132    /// Whether this entry was copied or renamed from the source. Returns `None`
133    /// if the path is unchanged.
134    pub fn copy_operation(&self) -> Option<CopyOperation> {
135        self.source.as_ref().map(|(_, op)| *op)
136    }
137}
138
139/// Wraps a `TreeDiffStream`, adding support for copies and renames.
140pub struct CopiesTreeDiffStream<'a> {
141    inner: TreeDiffStream<'a>,
142    source_tree: MergedTree,
143    target_tree: MergedTree,
144    copy_records: &'a CopyRecords,
145}
146
147impl<'a> CopiesTreeDiffStream<'a> {
148    /// Create a new diff stream with copy information.
149    pub fn new(
150        inner: TreeDiffStream<'a>,
151        source_tree: MergedTree,
152        target_tree: MergedTree,
153        copy_records: &'a CopyRecords,
154    ) -> Self {
155        Self {
156            inner,
157            source_tree,
158            target_tree,
159            copy_records,
160        }
161    }
162
163    fn resolve_copy_source(
164        &self,
165        source: &RepoPath,
166        values: BackendResult<Diff<MergedTreeValue>>,
167    ) -> BackendResult<(CopyOperation, Diff<MergedTreeValue>)> {
168        let target_value = values?.after;
169        let source_value = self.source_tree.path_value(source)?;
170        // If the source path is deleted in the target tree, it's a rename.
171        let source_value_at_target = self.target_tree.path_value(source)?;
172        let copy_op = if source_value_at_target.is_absent() || source_value_at_target.is_tree() {
173            CopyOperation::Rename
174        } else {
175            CopyOperation::Copy
176        };
177        Ok((copy_op, Diff::new(source_value, target_value)))
178    }
179}
180
181impl Stream for CopiesTreeDiffStream<'_> {
182    type Item = CopiesTreeDiffEntry;
183
184    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
185        while let Some(diff_entry) = ready!(self.inner.as_mut().poll_next(cx)) {
186            let Some(CopyRecord { source, .. }) = self.copy_records.for_target(&diff_entry.path)
187            else {
188                let target_deleted =
189                    matches!(&diff_entry.values, Ok(diff) if diff.after.is_absent());
190                if target_deleted && self.copy_records.has_source(&diff_entry.path) {
191                    // Skip the "delete" entry when there is a rename.
192                    continue;
193                }
194                return Poll::Ready(Some(CopiesTreeDiffEntry {
195                    path: CopiesTreeDiffEntryPath {
196                        source: None,
197                        target: diff_entry.path,
198                    },
199                    values: diff_entry.values,
200                }));
201            };
202
203            let (copy_op, values) = match self.resolve_copy_source(source, diff_entry.values) {
204                Ok((copy_op, values)) => (copy_op, Ok(values)),
205                // Fall back to "copy" (= path still exists) if unknown.
206                Err(err) => (CopyOperation::Copy, Err(err)),
207            };
208            return Poll::Ready(Some(CopiesTreeDiffEntry {
209                path: CopiesTreeDiffEntryPath {
210                    source: Some((source.clone(), copy_op)),
211                    target: diff_entry.path,
212                },
213                values,
214            }));
215        }
216
217        Poll::Ready(None)
218    }
219}