just playing with tangled
at ig/vimdiffwarn 216 lines 7.4 kB view raw
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 copy_op = if self.target_tree.path_value(source)?.is_absent() { 170 CopyOperation::Rename 171 } else { 172 CopyOperation::Copy 173 }; 174 Ok((copy_op, (source_value, target_value))) 175 } 176} 177 178impl Stream for CopiesTreeDiffStream<'_> { 179 type Item = CopiesTreeDiffEntry; 180 181 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { 182 while let Some(diff_entry) = ready!(self.inner.as_mut().poll_next(cx)) { 183 let Some(CopyRecord { source, .. }) = self.copy_records.for_target(&diff_entry.path) 184 else { 185 let target_deleted = 186 matches!(&diff_entry.values, Ok((_, target_value)) if target_value.is_absent()); 187 if target_deleted && self.copy_records.has_source(&diff_entry.path) { 188 // Skip the "delete" entry when there is a rename. 189 continue; 190 } 191 return Poll::Ready(Some(CopiesTreeDiffEntry { 192 path: CopiesTreeDiffEntryPath { 193 source: None, 194 target: diff_entry.path, 195 }, 196 values: diff_entry.values, 197 })); 198 }; 199 200 let (copy_op, values) = match self.resolve_copy_source(source, diff_entry.values) { 201 Ok((copy_op, values)) => (copy_op, Ok(values)), 202 // Fall back to "copy" (= path still exists) if unknown. 203 Err(err) => (CopyOperation::Copy, Err(err)), 204 }; 205 return Poll::Ready(Some(CopiesTreeDiffEntry { 206 path: CopiesTreeDiffEntryPath { 207 source: Some((source.clone(), copy_op)), 208 target: diff_entry.path, 209 }, 210 values, 211 })); 212 } 213 214 Poll::Ready(None) 215 } 216}