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