##// END OF EJS Templates
rust-status: rename `StatusResult` to `DirstateStatus`...
Raphaël Gomès -
r45012:f13d1954 default
parent child Browse files
Show More
@@ -1,321 +1,321
1 1 // status.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Rust implementation of dirstate.status (dirstate.py).
9 9 //! It is currently missing a lot of functionality compared to the Python one
10 10 //! and will only be triggered in narrow cases.
11 11
12 12 use crate::{
13 13 dirstate::SIZE_FROM_OTHER_PARENT,
14 14 matchers::Matcher,
15 15 utils::{
16 16 files::HgMetadata,
17 17 hg_path::{
18 18 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
19 19 },
20 20 },
21 21 CopyMap, DirstateEntry, DirstateMap, EntryState,
22 22 };
23 23 use rayon::prelude::*;
24 24 use std::collections::HashSet;
25 25 use std::fs::{read_dir, DirEntry};
26 26 use std::path::Path;
27 27
28 28 /// Marker enum used to dispatch new status entries into the right collections.
29 29 /// Is similar to `crate::EntryState`, but represents the transient state of
30 30 /// entries during the lifetime of a command.
31 31 enum Dispatch {
32 32 Unsure,
33 33 Modified,
34 34 Added,
35 35 Removed,
36 36 Deleted,
37 37 Clean,
38 38 Unknown,
39 39 }
40 40
41 41 type IoResult<T> = std::io::Result<T>;
42 42
43 43 /// Dates and times that are outside the 31-bit signed range are compared
44 44 /// modulo 2^31. This should prevent hg from behaving badly with very large
45 45 /// files or corrupt dates while still having a high probability of detecting
46 46 /// changes. (issue2608)
47 47 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
48 48 /// is not defined for `i32`, and there is no `As` trait. This forces the
49 49 /// caller to cast `b` as `i32`.
50 50 fn mod_compare(a: i32, b: i32) -> bool {
51 51 a & i32::max_value() != b & i32::max_value()
52 52 }
53 53
54 54 /// Return a sorted list containing information about the entries
55 55 /// in the directory.
56 56 ///
57 57 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
58 58 fn list_directory(
59 59 path: impl AsRef<Path>,
60 60 skip_dot_hg: bool,
61 61 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
62 62 let mut results = vec![];
63 63 let entries = read_dir(path.as_ref())?;
64 64
65 65 for entry in entries {
66 66 let entry = entry?;
67 67 let filename = os_string_to_hg_path_buf(entry.file_name())?;
68 68 let file_type = entry.file_type()?;
69 69 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
70 70 return Ok(vec![]);
71 71 } else {
72 72 results.push((HgPathBuf::from(filename), entry))
73 73 }
74 74 }
75 75
76 76 results.sort_unstable_by_key(|e| e.0.clone());
77 77 Ok(results)
78 78 }
79 79
80 80 /// The file corresponding to the dirstate entry was found on the filesystem.
81 81 fn dispatch_found(
82 82 filename: impl AsRef<HgPath>,
83 83 entry: DirstateEntry,
84 84 metadata: HgMetadata,
85 85 copy_map: &CopyMap,
86 86 options: StatusOptions,
87 87 ) -> Dispatch {
88 88 let DirstateEntry {
89 89 state,
90 90 mode,
91 91 mtime,
92 92 size,
93 93 } = entry;
94 94
95 95 let HgMetadata {
96 96 st_mode,
97 97 st_size,
98 98 st_mtime,
99 99 ..
100 100 } = metadata;
101 101
102 102 match state {
103 103 EntryState::Normal => {
104 104 let size_changed = mod_compare(size, st_size as i32);
105 105 let mode_changed =
106 106 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
107 107 let metadata_changed = size >= 0 && (size_changed || mode_changed);
108 108 let other_parent = size == SIZE_FROM_OTHER_PARENT;
109 109 if metadata_changed
110 110 || other_parent
111 111 || copy_map.contains_key(filename.as_ref())
112 112 {
113 113 Dispatch::Modified
114 114 } else if mod_compare(mtime, st_mtime as i32) {
115 115 Dispatch::Unsure
116 116 } else if st_mtime == options.last_normal_time {
117 117 // the file may have just been marked as normal and
118 118 // it may have changed in the same second without
119 119 // changing its size. This can happen if we quickly
120 120 // do multiple commits. Force lookup, so we don't
121 121 // miss such a racy file change.
122 122 Dispatch::Unsure
123 123 } else if options.list_clean {
124 124 Dispatch::Clean
125 125 } else {
126 126 Dispatch::Unknown
127 127 }
128 128 }
129 129 EntryState::Merged => Dispatch::Modified,
130 130 EntryState::Added => Dispatch::Added,
131 131 EntryState::Removed => Dispatch::Removed,
132 132 EntryState::Unknown => Dispatch::Unknown,
133 133 }
134 134 }
135 135
136 136 /// The file corresponding to this Dirstate entry is missing.
137 137 fn dispatch_missing(state: EntryState) -> Dispatch {
138 138 match state {
139 139 // File was removed from the filesystem during commands
140 140 EntryState::Normal | EntryState::Merged | EntryState::Added => {
141 141 Dispatch::Deleted
142 142 }
143 143 // File was removed, everything is normal
144 144 EntryState::Removed => Dispatch::Removed,
145 145 // File is unknown to Mercurial, everything is normal
146 146 EntryState::Unknown => Dispatch::Unknown,
147 147 }
148 148 }
149 149
150 150 /// Get stat data about the files explicitly specified by match.
151 151 /// TODO subrepos
152 152 fn walk_explicit<'a>(
153 153 files: &'a HashSet<&HgPath>,
154 154 dmap: &'a DirstateMap,
155 155 root_dir: impl AsRef<Path> + Sync + Send,
156 156 options: StatusOptions,
157 157 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
158 158 files.par_iter().filter_map(move |filename| {
159 159 // TODO normalization
160 160 let normalized = filename.as_ref();
161 161
162 162 let buf = match hg_path_to_path_buf(normalized) {
163 163 Ok(x) => x,
164 164 Err(e) => return Some(Err(e.into())),
165 165 };
166 166 let target = root_dir.as_ref().join(buf);
167 167 let st = target.symlink_metadata();
168 168 match st {
169 169 Ok(meta) => {
170 170 let file_type = meta.file_type();
171 171 if file_type.is_file() || file_type.is_symlink() {
172 172 if let Some(entry) = dmap.get(normalized) {
173 173 return Some(Ok((
174 174 normalized,
175 175 dispatch_found(
176 176 &normalized,
177 177 *entry,
178 178 HgMetadata::from_metadata(meta),
179 179 &dmap.copy_map,
180 180 options,
181 181 ),
182 182 )));
183 183 }
184 184 } else {
185 185 if dmap.contains_key(normalized) {
186 186 return Some(Ok((normalized, Dispatch::Removed)));
187 187 }
188 188 }
189 189 }
190 190 Err(_) => {
191 191 if let Some(entry) = dmap.get(normalized) {
192 192 return Some(Ok((
193 193 normalized,
194 194 dispatch_missing(entry.state),
195 195 )));
196 196 }
197 197 }
198 198 };
199 199 None
200 200 })
201 201 }
202 202
203 203 #[derive(Debug, Copy, Clone)]
204 204 pub struct StatusOptions {
205 205 /// Remember the most recent modification timeslot for status, to make
206 206 /// sure we won't miss future size-preserving file content modifications
207 207 /// that happen within the same timeslot.
208 208 pub last_normal_time: i64,
209 209 /// Whether we are on a filesystem with UNIX-like exec flags
210 210 pub check_exec: bool,
211 211 pub list_clean: bool,
212 212 }
213 213
214 214 /// Stat all entries in the `DirstateMap` and mark them for dispatch into
215 215 /// the relevant collections.
216 216 fn stat_dmap_entries(
217 217 dmap: &DirstateMap,
218 218 root_dir: impl AsRef<Path> + Sync + Send,
219 219 options: StatusOptions,
220 220 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
221 221 dmap.par_iter().map(move |(filename, entry)| {
222 222 let filename: &HgPath = filename;
223 223 let filename_as_path = hg_path_to_path_buf(filename)?;
224 224 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
225 225
226 226 match meta {
227 227 Ok(ref m)
228 228 if !(m.file_type().is_file()
229 229 || m.file_type().is_symlink()) =>
230 230 {
231 231 Ok((filename, dispatch_missing(entry.state)))
232 232 }
233 233 Ok(m) => Ok((
234 234 filename,
235 235 dispatch_found(
236 236 filename,
237 237 *entry,
238 238 HgMetadata::from_metadata(m),
239 239 &dmap.copy_map,
240 240 options,
241 241 ),
242 242 )),
243 243 Err(ref e)
244 244 if e.kind() == std::io::ErrorKind::NotFound
245 245 || e.raw_os_error() == Some(20) =>
246 246 {
247 247 // Rust does not yet have an `ErrorKind` for
248 248 // `NotADirectory` (errno 20)
249 249 // It happens if the dirstate contains `foo/bar` and
250 250 // foo is not a directory
251 251 Ok((filename, dispatch_missing(entry.state)))
252 252 }
253 253 Err(e) => Err(e),
254 254 }
255 255 })
256 256 }
257 257
258 pub struct StatusResult<'a> {
258 pub struct DirstateStatus<'a> {
259 259 pub modified: Vec<&'a HgPath>,
260 260 pub added: Vec<&'a HgPath>,
261 261 pub removed: Vec<&'a HgPath>,
262 262 pub deleted: Vec<&'a HgPath>,
263 263 pub clean: Vec<&'a HgPath>,
264 264 /* TODO ignored
265 265 * TODO unknown */
266 266 }
267 267
268 268 fn build_response<'a>(
269 269 results: impl IntoIterator<Item = IoResult<(&'a HgPath, Dispatch)>>,
270 ) -> IoResult<(Vec<&'a HgPath>, StatusResult<'a>)> {
270 ) -> IoResult<(Vec<&'a HgPath>, DirstateStatus<'a>)> {
271 271 let mut lookup = vec![];
272 272 let mut modified = vec![];
273 273 let mut added = vec![];
274 274 let mut removed = vec![];
275 275 let mut deleted = vec![];
276 276 let mut clean = vec![];
277 277
278 278 for res in results.into_iter() {
279 279 let (filename, dispatch) = res?;
280 280 match dispatch {
281 281 Dispatch::Unknown => {}
282 282 Dispatch::Unsure => lookup.push(filename),
283 283 Dispatch::Modified => modified.push(filename),
284 284 Dispatch::Added => added.push(filename),
285 285 Dispatch::Removed => removed.push(filename),
286 286 Dispatch::Deleted => deleted.push(filename),
287 287 Dispatch::Clean => clean.push(filename),
288 288 }
289 289 }
290 290
291 291 Ok((
292 292 lookup,
293 StatusResult {
293 DirstateStatus {
294 294 modified,
295 295 added,
296 296 removed,
297 297 deleted,
298 298 clean,
299 299 },
300 300 ))
301 301 }
302 302
303 303 pub fn status<'a: 'c, 'b: 'c, 'c>(
304 304 dmap: &'a DirstateMap,
305 305 matcher: &'b impl Matcher,
306 306 root_dir: impl AsRef<Path> + Sync + Send + Copy,
307 307 options: StatusOptions,
308 ) -> IoResult<(Vec<&'c HgPath>, StatusResult<'c>)> {
308 ) -> IoResult<(Vec<&'c HgPath>, DirstateStatus<'c>)> {
309 309 let files = matcher.file_set();
310 310 let mut results = vec![];
311 311 if let Some(files) = files {
312 312 results.par_extend(walk_explicit(&files, &dmap, root_dir, options));
313 313 }
314 314
315 315 if !matcher.is_exact() {
316 316 let stat_results = stat_dmap_entries(&dmap, root_dir, options);
317 317 results.par_extend(stat_results);
318 318 }
319 319
320 320 build_response(results)
321 321 }
@@ -1,184 +1,184
1 1 // Copyright 2018-2020 Georges Racinet <georges.racinet@octobus.net>
2 2 // and Mercurial contributors
3 3 //
4 4 // This software may be used and distributed according to the terms of the
5 5 // GNU General Public License version 2 or any later version.
6 6 mod ancestors;
7 7 pub mod dagops;
8 8 pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors};
9 9 mod dirstate;
10 10 pub mod discovery;
11 11 pub mod testing; // unconditionally built, for use from integration tests
12 12 pub use dirstate::{
13 13 dirs_multiset::{DirsMultiset, DirsMultisetIter},
14 14 dirstate_map::DirstateMap,
15 15 parsers::{pack_dirstate, parse_dirstate, PARENT_SIZE},
16 status::{status, StatusOptions, StatusResult},
16 status::{status, DirstateStatus, StatusOptions},
17 17 CopyMap, CopyMapIter, DirstateEntry, DirstateParents, EntryState,
18 18 StateMap, StateMapIter,
19 19 };
20 20 mod filepatterns;
21 21 pub mod matchers;
22 22 pub mod revlog;
23 23 pub use revlog::*;
24 24 #[cfg(feature = "with-re2")]
25 25 pub mod re2;
26 26 pub mod utils;
27 27
28 28 use crate::utils::hg_path::{HgPathBuf, HgPathError};
29 29 pub use filepatterns::{
30 30 parse_pattern_syntax, read_pattern_file, IgnorePattern,
31 31 PatternFileWarning, PatternSyntax,
32 32 };
33 33 use std::collections::HashMap;
34 34 use twox_hash::RandomXxHashBuilder64;
35 35
36 36 pub type LineNumber = usize;
37 37
38 38 /// Rust's default hasher is too slow because it tries to prevent collision
39 39 /// attacks. We are not concerned about those: if an ill-minded person has
40 40 /// write access to your repository, you have other issues.
41 41 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
42 42
43 43 #[derive(Clone, Debug, PartialEq)]
44 44 pub enum DirstateParseError {
45 45 TooLittleData,
46 46 Overflow,
47 47 CorruptedEntry(String),
48 48 Damaged,
49 49 }
50 50
51 51 impl From<std::io::Error> for DirstateParseError {
52 52 fn from(e: std::io::Error) -> Self {
53 53 DirstateParseError::CorruptedEntry(e.to_string())
54 54 }
55 55 }
56 56
57 57 impl ToString for DirstateParseError {
58 58 fn to_string(&self) -> String {
59 59 use crate::DirstateParseError::*;
60 60 match self {
61 61 TooLittleData => "Too little data for dirstate.".to_string(),
62 62 Overflow => "Overflow in dirstate.".to_string(),
63 63 CorruptedEntry(e) => format!("Corrupted entry: {:?}.", e),
64 64 Damaged => "Dirstate appears to be damaged.".to_string(),
65 65 }
66 66 }
67 67 }
68 68
69 69 #[derive(Debug, PartialEq)]
70 70 pub enum DirstatePackError {
71 71 CorruptedEntry(String),
72 72 CorruptedParent,
73 73 BadSize(usize, usize),
74 74 }
75 75
76 76 impl From<std::io::Error> for DirstatePackError {
77 77 fn from(e: std::io::Error) -> Self {
78 78 DirstatePackError::CorruptedEntry(e.to_string())
79 79 }
80 80 }
81 81 #[derive(Debug, PartialEq)]
82 82 pub enum DirstateMapError {
83 83 PathNotFound(HgPathBuf),
84 84 EmptyPath,
85 85 InvalidPath(HgPathError),
86 86 }
87 87
88 88 impl ToString for DirstateMapError {
89 89 fn to_string(&self) -> String {
90 90 match self {
91 91 DirstateMapError::PathNotFound(_) => {
92 92 "expected a value, found none".to_string()
93 93 }
94 94 DirstateMapError::EmptyPath => "Overflow in dirstate.".to_string(),
95 95 DirstateMapError::InvalidPath(e) => e.to_string(),
96 96 }
97 97 }
98 98 }
99 99
100 100 pub enum DirstateError {
101 101 Parse(DirstateParseError),
102 102 Pack(DirstatePackError),
103 103 Map(DirstateMapError),
104 104 IO(std::io::Error),
105 105 }
106 106
107 107 impl From<DirstateParseError> for DirstateError {
108 108 fn from(e: DirstateParseError) -> Self {
109 109 DirstateError::Parse(e)
110 110 }
111 111 }
112 112
113 113 impl From<DirstatePackError> for DirstateError {
114 114 fn from(e: DirstatePackError) -> Self {
115 115 DirstateError::Pack(e)
116 116 }
117 117 }
118 118
119 119 #[derive(Debug)]
120 120 pub enum PatternError {
121 121 Path(HgPathError),
122 122 UnsupportedSyntax(String),
123 123 UnsupportedSyntaxInFile(String, String, usize),
124 124 TooLong(usize),
125 125 IO(std::io::Error),
126 126 /// Needed a pattern that can be turned into a regex but got one that
127 127 /// can't. This should only happen through programmer error.
128 128 NonRegexPattern(IgnorePattern),
129 129 /// This is temporary, see `re2/mod.rs`.
130 130 /// This will cause a fallback to Python.
131 131 Re2NotInstalled,
132 132 }
133 133
134 134 impl ToString for PatternError {
135 135 fn to_string(&self) -> String {
136 136 match self {
137 137 PatternError::UnsupportedSyntax(syntax) => {
138 138 format!("Unsupported syntax {}", syntax)
139 139 }
140 140 PatternError::UnsupportedSyntaxInFile(syntax, file_path, line) => {
141 141 format!(
142 142 "{}:{}: unsupported syntax {}",
143 143 file_path, line, syntax
144 144 )
145 145 }
146 146 PatternError::TooLong(size) => {
147 147 format!("matcher pattern is too long ({} bytes)", size)
148 148 }
149 149 PatternError::IO(e) => e.to_string(),
150 150 PatternError::Path(e) => e.to_string(),
151 151 PatternError::NonRegexPattern(pattern) => {
152 152 format!("'{:?}' cannot be turned into a regex", pattern)
153 153 }
154 154 PatternError::Re2NotInstalled => {
155 155 "Re2 is not installed, cannot use regex functionality."
156 156 .to_string()
157 157 }
158 158 }
159 159 }
160 160 }
161 161
162 162 impl From<DirstateMapError> for DirstateError {
163 163 fn from(e: DirstateMapError) -> Self {
164 164 DirstateError::Map(e)
165 165 }
166 166 }
167 167
168 168 impl From<std::io::Error> for DirstateError {
169 169 fn from(e: std::io::Error) -> Self {
170 170 DirstateError::IO(e)
171 171 }
172 172 }
173 173
174 174 impl From<std::io::Error> for PatternError {
175 175 fn from(e: std::io::Error) -> Self {
176 176 PatternError::IO(e)
177 177 }
178 178 }
179 179
180 180 impl From<HgPathError> for PatternError {
181 181 fn from(e: HgPathError) -> Self {
182 182 PatternError::Path(e)
183 183 }
184 184 }
@@ -1,129 +1,129
1 1 // status.rs
2 2 //
3 3 // Copyright 2019, Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Bindings for the `hg::status` module provided by the
9 9 //! `hg-core` crate. From Python, this will be seen as
10 10 //! `rustext.dirstate.status`.
11 11
12 12 use crate::dirstate::DirstateMap;
13 13 use cpython::exc::ValueError;
14 14 use cpython::{
15 15 ObjectProtocol, PyBytes, PyErr, PyList, PyObject, PyResult, PyTuple,
16 16 Python, PythonObject, ToPyObject,
17 17 };
18 18 use hg::utils::hg_path::HgPathBuf;
19 19 use hg::{
20 20 matchers::{AlwaysMatcher, FileMatcher},
21 21 status,
22 22 utils::{files::get_path_from_bytes, hg_path::HgPath},
23 StatusResult,
23 DirstateStatus,
24 24 };
25 25 use std::borrow::Borrow;
26 26
27 27 /// This will be useless once trait impls for collection are added to `PyBytes`
28 28 /// upstream.
29 29 fn collect_pybytes_list<P: AsRef<HgPath>>(
30 30 py: Python,
31 31 collection: &[P],
32 32 ) -> PyList {
33 33 let list = PyList::new(py, &[]);
34 34
35 35 for (i, path) in collection.iter().enumerate() {
36 36 list.insert(
37 37 py,
38 38 i,
39 39 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
40 40 )
41 41 }
42 42
43 43 list
44 44 }
45 45
46 46 pub fn status_wrapper(
47 47 py: Python,
48 48 dmap: DirstateMap,
49 49 matcher: PyObject,
50 50 root_dir: PyObject,
51 51 list_clean: bool,
52 52 last_normal_time: i64,
53 53 check_exec: bool,
54 54 ) -> PyResult<(PyList, PyList, PyList, PyList, PyList, PyList, PyList)> {
55 55 let bytes = root_dir.extract::<PyBytes>(py)?;
56 56 let root_dir = get_path_from_bytes(bytes.data(py));
57 57
58 58 let dmap: DirstateMap = dmap.to_py_object(py);
59 59 let dmap = dmap.get_inner(py);
60 60
61 61 match matcher.get_type(py).name(py).borrow() {
62 62 "alwaysmatcher" => {
63 63 let matcher = AlwaysMatcher;
64 64 let (lookup, status_res) = status(
65 65 &dmap,
66 66 &matcher,
67 67 &root_dir,
68 68 list_clean,
69 69 last_normal_time,
70 70 check_exec,
71 71 )
72 72 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
73 73 build_response(lookup, status_res, py)
74 74 }
75 75 "exactmatcher" => {
76 76 let files = matcher.call_method(
77 77 py,
78 78 "files",
79 79 PyTuple::new(py, &[]),
80 80 None,
81 81 )?;
82 82 let files: PyList = files.cast_into(py)?;
83 83 let files: PyResult<Vec<HgPathBuf>> = files
84 84 .iter(py)
85 85 .map(|f| {
86 86 Ok(HgPathBuf::from_bytes(
87 87 f.extract::<PyBytes>(py)?.data(py),
88 88 ))
89 89 })
90 90 .collect();
91 91
92 92 let files = files?;
93 93 let matcher = FileMatcher::new(&files)
94 94 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
95 95 let (lookup, status_res) = status(
96 96 &dmap,
97 97 &matcher,
98 98 &root_dir,
99 99 list_clean,
100 100 last_normal_time,
101 101 check_exec,
102 102 )
103 103 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
104 104 build_response(lookup, status_res, py)
105 105 }
106 106 e => {
107 107 return Err(PyErr::new::<ValueError, _>(
108 108 py,
109 109 format!("Unsupported matcher {}", e),
110 110 ));
111 111 }
112 112 }
113 113 }
114 114
115 115 fn build_response(
116 116 lookup: Vec<&HgPath>,
117 status_res: StatusResult,
117 status_res: DirstateStatus,
118 118 py: Python,
119 119 ) -> PyResult<(PyList, PyList, PyList, PyList, PyList, PyList, PyList)> {
120 120 let modified = collect_pybytes_list(py, status_res.modified.as_ref());
121 121 let added = collect_pybytes_list(py, status_res.added.as_ref());
122 122 let removed = collect_pybytes_list(py, status_res.removed.as_ref());
123 123 let deleted = collect_pybytes_list(py, status_res.deleted.as_ref());
124 124 let clean = collect_pybytes_list(py, status_res.clean.as_ref());
125 125 let lookup = collect_pybytes_list(py, lookup.as_ref());
126 126 let unknown = PyList::new(py, &[]);
127 127
128 128 Ok((lookup, modified, added, removed, deleted, unknown, clean))
129 129 }
General Comments 0
You need to be logged in to leave comments. Login now