##// END OF EJS Templates
dirstate-item: implement `drop_merge_data` on the Rust DirstateItem...
marmoute -
r48946:3c7db97c default
parent child Browse files
Show More
@@ -1,400 +1,427 b''
1 1 use crate::errors::HgError;
2 2 use bitflags::bitflags;
3 3 use std::convert::TryFrom;
4 4
5 5 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
6 6 pub enum EntryState {
7 7 Normal,
8 8 Added,
9 9 Removed,
10 10 Merged,
11 11 }
12 12
13 13 /// The C implementation uses all signed types. This will be an issue
14 14 /// either when 4GB+ source files are commonplace or in 2038, whichever
15 15 /// comes first.
16 16 #[derive(Debug, PartialEq, Copy, Clone)]
17 17 pub struct DirstateEntry {
18 18 flags: Flags,
19 19 mode: i32,
20 20 size: i32,
21 21 mtime: i32,
22 22 }
23 23
24 24 bitflags! {
25 25 pub struct Flags: u8 {
26 26 const WDIR_TRACKED = 1 << 0;
27 27 const P1_TRACKED = 1 << 1;
28 28 const P2_TRACKED = 1 << 2;
29 29 const POSSIBLY_DIRTY = 1 << 3;
30 30 const MERGED = 1 << 4;
31 31 const CLEAN_P1 = 1 << 5;
32 32 const CLEAN_P2 = 1 << 6;
33 33 const ENTRYLESS_TREE_NODE = 1 << 7;
34 34 }
35 35 }
36 36
37 37 pub const V1_RANGEMASK: i32 = 0x7FFFFFFF;
38 38
39 39 pub const MTIME_UNSET: i32 = -1;
40 40
41 41 /// A `DirstateEntry` with a size of `-2` means that it was merged from the
42 42 /// other parent. This allows revert to pick the right status back during a
43 43 /// merge.
44 44 pub const SIZE_FROM_OTHER_PARENT: i32 = -2;
45 45 /// A special value used for internal representation of special case in
46 46 /// dirstate v1 format.
47 47 pub const SIZE_NON_NORMAL: i32 = -1;
48 48
49 49 impl DirstateEntry {
50 50 pub fn new(
51 51 flags: Flags,
52 52 mode_size_mtime: Option<(i32, i32, i32)>,
53 53 ) -> Self {
54 54 let (mode, size, mtime) =
55 55 mode_size_mtime.unwrap_or((0, SIZE_NON_NORMAL, MTIME_UNSET));
56 56 Self {
57 57 flags,
58 58 mode,
59 59 size,
60 60 mtime,
61 61 }
62 62 }
63 63
64 64 pub fn from_v1_data(
65 65 state: EntryState,
66 66 mode: i32,
67 67 size: i32,
68 68 mtime: i32,
69 69 ) -> Self {
70 70 match state {
71 71 EntryState::Normal => {
72 72 if size == SIZE_FROM_OTHER_PARENT {
73 73 Self::new_from_p2()
74 74 } else if size == SIZE_NON_NORMAL {
75 75 Self::new_possibly_dirty()
76 76 } else if mtime == MTIME_UNSET {
77 77 Self {
78 78 flags: Flags::WDIR_TRACKED
79 79 | Flags::P1_TRACKED
80 80 | Flags::POSSIBLY_DIRTY,
81 81 mode,
82 82 size,
83 83 mtime: 0,
84 84 }
85 85 } else {
86 86 Self::new_normal(mode, size, mtime)
87 87 }
88 88 }
89 89 EntryState::Added => Self::new_added(),
90 90 EntryState::Removed => Self {
91 91 flags: if size == SIZE_NON_NORMAL {
92 92 Flags::P1_TRACKED // might not be true because of rename ?
93 93 | Flags::P2_TRACKED // might not be true because of rename ?
94 94 | Flags::MERGED
95 95 } else if size == SIZE_FROM_OTHER_PARENT {
96 96 // We don’t know if P1_TRACKED should be set (file history)
97 97 Flags::P2_TRACKED | Flags::CLEAN_P2
98 98 } else {
99 99 Flags::P1_TRACKED
100 100 },
101 101 mode: 0,
102 102 size: 0,
103 103 mtime: 0,
104 104 },
105 105 EntryState::Merged => Self::new_merged(),
106 106 }
107 107 }
108 108
109 109 pub fn new_from_p2() -> Self {
110 110 Self {
111 111 // might be missing P1_TRACKED
112 112 flags: Flags::WDIR_TRACKED | Flags::P2_TRACKED | Flags::CLEAN_P2,
113 113 mode: 0,
114 114 size: SIZE_FROM_OTHER_PARENT,
115 115 mtime: MTIME_UNSET,
116 116 }
117 117 }
118 118
119 119 pub fn new_possibly_dirty() -> Self {
120 120 Self {
121 121 flags: Flags::WDIR_TRACKED
122 122 | Flags::P1_TRACKED
123 123 | Flags::POSSIBLY_DIRTY,
124 124 mode: 0,
125 125 size: SIZE_NON_NORMAL,
126 126 mtime: MTIME_UNSET,
127 127 }
128 128 }
129 129
130 130 pub fn new_added() -> Self {
131 131 Self {
132 132 flags: Flags::WDIR_TRACKED,
133 133 mode: 0,
134 134 size: SIZE_NON_NORMAL,
135 135 mtime: MTIME_UNSET,
136 136 }
137 137 }
138 138
139 139 pub fn new_merged() -> Self {
140 140 Self {
141 141 flags: Flags::WDIR_TRACKED
142 142 | Flags::P1_TRACKED // might not be true because of rename ?
143 143 | Flags::P2_TRACKED // might not be true because of rename ?
144 144 | Flags::MERGED,
145 145 mode: 0,
146 146 size: SIZE_NON_NORMAL,
147 147 mtime: MTIME_UNSET,
148 148 }
149 149 }
150 150
151 151 pub fn new_normal(mode: i32, size: i32, mtime: i32) -> Self {
152 152 Self {
153 153 flags: Flags::WDIR_TRACKED | Flags::P1_TRACKED,
154 154 mode,
155 155 size,
156 156 mtime,
157 157 }
158 158 }
159 159
160 160 /// Creates a new entry in "removed" state.
161 161 ///
162 162 /// `size` is expected to be zero, `SIZE_NON_NORMAL`, or
163 163 /// `SIZE_FROM_OTHER_PARENT`
164 164 pub fn new_removed(size: i32) -> Self {
165 165 Self::from_v1_data(EntryState::Removed, 0, size, 0)
166 166 }
167 167
168 168 pub fn tracked(&self) -> bool {
169 169 self.flags.contains(Flags::WDIR_TRACKED)
170 170 }
171 171
172 172 fn tracked_in_any_parent(&self) -> bool {
173 173 self.flags.intersects(Flags::P1_TRACKED | Flags::P2_TRACKED)
174 174 }
175 175
176 176 pub fn removed(&self) -> bool {
177 177 self.tracked_in_any_parent()
178 178 && !self.flags.contains(Flags::WDIR_TRACKED)
179 179 }
180 180
181 181 pub fn merged(&self) -> bool {
182 182 self.flags.contains(Flags::WDIR_TRACKED | Flags::MERGED)
183 183 }
184 184
185 185 pub fn added(&self) -> bool {
186 186 self.flags.contains(Flags::WDIR_TRACKED)
187 187 && !self.tracked_in_any_parent()
188 188 }
189 189
190 190 pub fn from_p2(&self) -> bool {
191 191 self.flags.contains(Flags::WDIR_TRACKED | Flags::CLEAN_P2)
192 192 }
193 193
194 194 pub fn maybe_clean(&self) -> bool {
195 195 if !self.flags.contains(Flags::WDIR_TRACKED) {
196 196 false
197 197 } else if self.added() {
198 198 false
199 199 } else if self.flags.contains(Flags::MERGED) {
200 200 false
201 201 } else if self.flags.contains(Flags::CLEAN_P2) {
202 202 false
203 203 } else {
204 204 true
205 205 }
206 206 }
207 207
208 208 pub fn any_tracked(&self) -> bool {
209 209 self.flags.intersects(
210 210 Flags::WDIR_TRACKED | Flags::P1_TRACKED | Flags::P2_TRACKED,
211 211 )
212 212 }
213 213
214 214 pub fn state(&self) -> EntryState {
215 215 if self.removed() {
216 216 EntryState::Removed
217 217 } else if self.merged() {
218 218 EntryState::Merged
219 219 } else if self.added() {
220 220 EntryState::Added
221 221 } else {
222 222 EntryState::Normal
223 223 }
224 224 }
225 225
226 226 pub fn mode(&self) -> i32 {
227 227 self.mode
228 228 }
229 229
230 230 pub fn size(&self) -> i32 {
231 231 if self.removed() && self.flags.contains(Flags::MERGED) {
232 232 SIZE_NON_NORMAL
233 233 } else if self.removed() && self.flags.contains(Flags::CLEAN_P2) {
234 234 SIZE_FROM_OTHER_PARENT
235 235 } else if self.removed() {
236 236 0
237 237 } else if self.merged() {
238 238 SIZE_FROM_OTHER_PARENT
239 239 } else if self.added() {
240 240 SIZE_NON_NORMAL
241 241 } else if self.from_p2() {
242 242 SIZE_FROM_OTHER_PARENT
243 243 } else if self.flags.contains(Flags::POSSIBLY_DIRTY) {
244 244 self.size // TODO: SIZE_NON_NORMAL ?
245 245 } else {
246 246 self.size
247 247 }
248 248 }
249 249
250 250 pub fn mtime(&self) -> i32 {
251 251 if self.removed() {
252 252 0
253 253 } else if self.flags.contains(Flags::POSSIBLY_DIRTY) {
254 254 MTIME_UNSET
255 255 } else if self.merged() {
256 256 MTIME_UNSET
257 257 } else if self.added() {
258 258 MTIME_UNSET
259 259 } else if self.from_p2() {
260 260 MTIME_UNSET
261 261 } else {
262 262 self.mtime
263 263 }
264 264 }
265 265
266 pub fn drop_merge_data(&mut self) {
267 if self.flags.contains(Flags::CLEAN_P1)
268 || self.flags.contains(Flags::CLEAN_P2)
269 || self.flags.contains(Flags::MERGED)
270 || self.flags.contains(Flags::P2_TRACKED)
271 {
272 if self.flags.contains(Flags::MERGED) {
273 self.flags.insert(Flags::P1_TRACKED);
274 } else {
275 self.flags.remove(Flags::P1_TRACKED);
276 }
277 self.flags.remove(
278 Flags::MERGED
279 | Flags::CLEAN_P1
280 | Flags::CLEAN_P2
281 | Flags::P2_TRACKED,
282 );
283 self.flags.insert(Flags::POSSIBLY_DIRTY);
284 self.mode = 0;
285 self.mtime = 0;
286 // size = None on the python size turn into size = NON_NORMAL when
287 // accessed. So the next line is currently required, but a some
288 // future clean up would be welcome.
289 self.size = SIZE_NON_NORMAL;
290 }
291 }
292
266 293 pub fn set_possibly_dirty(&mut self) {
267 294 self.flags.insert(Flags::POSSIBLY_DIRTY)
268 295 }
269 296
270 297 pub fn set_clean(&mut self, mode: i32, size: i32, mtime: i32) {
271 298 self.flags.insert(Flags::WDIR_TRACKED | Flags::P1_TRACKED);
272 299 self.flags.remove(
273 300 Flags::P2_TRACKED // This might be wrong
274 301 | Flags::MERGED
275 302 | Flags::CLEAN_P2
276 303 | Flags::POSSIBLY_DIRTY,
277 304 );
278 305 self.mode = mode;
279 306 self.size = size;
280 307 self.mtime = mtime;
281 308 }
282 309
283 310 pub fn set_tracked(&mut self) {
284 311 self.flags
285 312 .insert(Flags::WDIR_TRACKED | Flags::POSSIBLY_DIRTY);
286 313 // size = None on the python size turn into size = NON_NORMAL when
287 314 // accessed. So the next line is currently required, but a some future
288 315 // clean up would be welcome.
289 316 self.size = SIZE_NON_NORMAL;
290 317 }
291 318
292 319 pub fn set_untracked(&mut self) {
293 320 self.flags.remove(Flags::WDIR_TRACKED);
294 321 self.mode = 0;
295 322 self.size = 0;
296 323 self.mtime = 0;
297 324 }
298 325
299 326 /// Returns `(state, mode, size, mtime)` for the puprose of serialization
300 327 /// in the dirstate-v1 format.
301 328 ///
302 329 /// This includes marker values such as `mtime == -1`. In the future we may
303 330 /// want to not represent these cases that way in memory, but serialization
304 331 /// will need to keep the same format.
305 332 pub fn v1_data(&self) -> (u8, i32, i32, i32) {
306 333 (self.state().into(), self.mode(), self.size(), self.mtime())
307 334 }
308 335
309 336 pub(crate) fn is_from_other_parent(&self) -> bool {
310 337 self.state() == EntryState::Normal
311 338 && self.size() == SIZE_FROM_OTHER_PARENT
312 339 }
313 340
314 341 // TODO: other platforms
315 342 #[cfg(unix)]
316 343 pub fn mode_changed(
317 344 &self,
318 345 filesystem_metadata: &std::fs::Metadata,
319 346 ) -> bool {
320 347 use std::os::unix::fs::MetadataExt;
321 348 const EXEC_BIT_MASK: u32 = 0o100;
322 349 let dirstate_exec_bit = (self.mode() as u32) & EXEC_BIT_MASK;
323 350 let fs_exec_bit = filesystem_metadata.mode() & EXEC_BIT_MASK;
324 351 dirstate_exec_bit != fs_exec_bit
325 352 }
326 353
327 354 /// Returns a `(state, mode, size, mtime)` tuple as for
328 355 /// `DirstateMapMethods::debug_iter`.
329 356 pub fn debug_tuple(&self) -> (u8, i32, i32, i32) {
330 357 let state = if self.flags.contains(Flags::ENTRYLESS_TREE_NODE) {
331 358 b' '
332 359 } else {
333 360 self.state().into()
334 361 };
335 362 (state, self.mode(), self.size(), self.mtime())
336 363 }
337 364
338 365 pub fn mtime_is_ambiguous(&self, now: i32) -> bool {
339 366 self.state() == EntryState::Normal && self.mtime() == now
340 367 }
341 368
342 369 pub fn clear_ambiguous_mtime(&mut self, now: i32) -> bool {
343 370 let ambiguous = self.mtime_is_ambiguous(now);
344 371 if ambiguous {
345 372 // The file was last modified "simultaneously" with the current
346 373 // write to dirstate (i.e. within the same second for file-
347 374 // systems with a granularity of 1 sec). This commonly happens
348 375 // for at least a couple of files on 'update'.
349 376 // The user could change the file without changing its size
350 377 // within the same second. Invalidate the file's mtime in
351 378 // dirstate, forcing future 'status' calls to compare the
352 379 // contents of the file if the size is the same. This prevents
353 380 // mistakenly treating such files as clean.
354 381 self.clear_mtime()
355 382 }
356 383 ambiguous
357 384 }
358 385
359 386 pub fn clear_mtime(&mut self) {
360 387 self.mtime = -1;
361 388 }
362 389 }
363 390
364 391 impl EntryState {
365 392 pub fn is_tracked(self) -> bool {
366 393 use EntryState::*;
367 394 match self {
368 395 Normal | Added | Merged => true,
369 396 Removed => false,
370 397 }
371 398 }
372 399 }
373 400
374 401 impl TryFrom<u8> for EntryState {
375 402 type Error = HgError;
376 403
377 404 fn try_from(value: u8) -> Result<Self, Self::Error> {
378 405 match value {
379 406 b'n' => Ok(EntryState::Normal),
380 407 b'a' => Ok(EntryState::Added),
381 408 b'r' => Ok(EntryState::Removed),
382 409 b'm' => Ok(EntryState::Merged),
383 410 _ => Err(HgError::CorruptedRepository(format!(
384 411 "Incorrect dirstate entry state {}",
385 412 value
386 413 ))),
387 414 }
388 415 }
389 416 }
390 417
391 418 impl Into<u8> for EntryState {
392 419 fn into(self) -> u8 {
393 420 match self {
394 421 EntryState::Normal => b'n',
395 422 EntryState::Added => b'a',
396 423 EntryState::Removed => b'r',
397 424 EntryState::Merged => b'm',
398 425 }
399 426 }
400 427 }
@@ -1,213 +1,218 b''
1 1 use cpython::exc;
2 2 use cpython::PyBytes;
3 3 use cpython::PyErr;
4 4 use cpython::PyNone;
5 5 use cpython::PyObject;
6 6 use cpython::PyResult;
7 7 use cpython::Python;
8 8 use cpython::PythonObject;
9 9 use hg::dirstate::entry::Flags;
10 10 use hg::dirstate::DirstateEntry;
11 11 use hg::dirstate::EntryState;
12 12 use std::cell::Cell;
13 13 use std::convert::TryFrom;
14 14
15 15 py_class!(pub class DirstateItem |py| {
16 16 data entry: Cell<DirstateEntry>;
17 17
18 18 def __new__(
19 19 _cls,
20 20 wc_tracked: bool = false,
21 21 p1_tracked: bool = false,
22 22 p2_tracked: bool = false,
23 23 merged: bool = false,
24 24 clean_p1: bool = false,
25 25 clean_p2: bool = false,
26 26 possibly_dirty: bool = false,
27 27 parentfiledata: Option<(i32, i32, i32)> = None,
28 28
29 29 ) -> PyResult<DirstateItem> {
30 30 let mut flags = Flags::empty();
31 31 flags.set(Flags::WDIR_TRACKED, wc_tracked);
32 32 flags.set(Flags::P1_TRACKED, p1_tracked);
33 33 flags.set(Flags::P2_TRACKED, p2_tracked);
34 34 flags.set(Flags::MERGED, merged);
35 35 flags.set(Flags::CLEAN_P1, clean_p1);
36 36 flags.set(Flags::CLEAN_P2, clean_p2);
37 37 flags.set(Flags::POSSIBLY_DIRTY, possibly_dirty);
38 38 let entry = DirstateEntry::new(flags, parentfiledata);
39 39 DirstateItem::create_instance(py, Cell::new(entry))
40 40 }
41 41
42 42 @property
43 43 def state(&self) -> PyResult<PyBytes> {
44 44 let state_byte: u8 = self.entry(py).get().state().into();
45 45 Ok(PyBytes::new(py, &[state_byte]))
46 46 }
47 47
48 48 @property
49 49 def mode(&self) -> PyResult<i32> {
50 50 Ok(self.entry(py).get().mode())
51 51 }
52 52
53 53 @property
54 54 def size(&self) -> PyResult<i32> {
55 55 Ok(self.entry(py).get().size())
56 56 }
57 57
58 58 @property
59 59 def mtime(&self) -> PyResult<i32> {
60 60 Ok(self.entry(py).get().mtime())
61 61 }
62 62
63 63 @property
64 64 def tracked(&self) -> PyResult<bool> {
65 65 Ok(self.entry(py).get().tracked())
66 66 }
67 67
68 68 @property
69 69 def added(&self) -> PyResult<bool> {
70 70 Ok(self.entry(py).get().added())
71 71 }
72 72
73 73 @property
74 74 def merged(&self) -> PyResult<bool> {
75 75 Ok(self.entry(py).get().merged())
76 76 }
77 77
78 78 @property
79 79 def removed(&self) -> PyResult<bool> {
80 80 Ok(self.entry(py).get().removed())
81 81 }
82 82
83 83 @property
84 84 def from_p2(&self) -> PyResult<bool> {
85 85 Ok(self.entry(py).get().from_p2())
86 86 }
87 87
88 88 @property
89 89 def maybe_clean(&self) -> PyResult<bool> {
90 90 Ok(self.entry(py).get().maybe_clean())
91 91 }
92 92
93 93 @property
94 94 def any_tracked(&self) -> PyResult<bool> {
95 95 Ok(self.entry(py).get().any_tracked())
96 96 }
97 97
98 98 def v1_state(&self) -> PyResult<PyBytes> {
99 99 let (state, _mode, _size, _mtime) = self.entry(py).get().v1_data();
100 100 let state_byte: u8 = state.into();
101 101 Ok(PyBytes::new(py, &[state_byte]))
102 102 }
103 103
104 104 def v1_mode(&self) -> PyResult<i32> {
105 105 let (_state, mode, _size, _mtime) = self.entry(py).get().v1_data();
106 106 Ok(mode)
107 107 }
108 108
109 109 def v1_size(&self) -> PyResult<i32> {
110 110 let (_state, _mode, size, _mtime) = self.entry(py).get().v1_data();
111 111 Ok(size)
112 112 }
113 113
114 114 def v1_mtime(&self) -> PyResult<i32> {
115 115 let (_state, _mode, _size, mtime) = self.entry(py).get().v1_data();
116 116 Ok(mtime)
117 117 }
118 118
119 119 def need_delay(&self, now: i32) -> PyResult<bool> {
120 120 Ok(self.entry(py).get().mtime_is_ambiguous(now))
121 121 }
122 122
123 123 @classmethod
124 124 def from_v1_data(
125 125 _cls,
126 126 state: PyBytes,
127 127 mode: i32,
128 128 size: i32,
129 129 mtime: i32,
130 130 ) -> PyResult<Self> {
131 131 let state = <[u8; 1]>::try_from(state.data(py))
132 132 .ok()
133 133 .and_then(|state| EntryState::try_from(state[0]).ok())
134 134 .ok_or_else(|| PyErr::new::<exc::ValueError, _>(py, "invalid state"))?;
135 135 let entry = DirstateEntry::from_v1_data(state, mode, size, mtime);
136 136 DirstateItem::create_instance(py, Cell::new(entry))
137 137 }
138 138
139 139 @classmethod
140 140 def new_added(_cls) -> PyResult<Self> {
141 141 let entry = DirstateEntry::new_added();
142 142 DirstateItem::create_instance(py, Cell::new(entry))
143 143 }
144 144
145 145 @classmethod
146 146 def new_merged(_cls) -> PyResult<Self> {
147 147 let entry = DirstateEntry::new_merged();
148 148 DirstateItem::create_instance(py, Cell::new(entry))
149 149 }
150 150
151 151 @classmethod
152 152 def new_from_p2(_cls) -> PyResult<Self> {
153 153 let entry = DirstateEntry::new_from_p2();
154 154 DirstateItem::create_instance(py, Cell::new(entry))
155 155 }
156 156
157 157 @classmethod
158 158 def new_possibly_dirty(_cls) -> PyResult<Self> {
159 159 let entry = DirstateEntry::new_possibly_dirty();
160 160 DirstateItem::create_instance(py, Cell::new(entry))
161 161 }
162 162
163 163 @classmethod
164 164 def new_normal(_cls, mode: i32, size: i32, mtime: i32) -> PyResult<Self> {
165 165 let entry = DirstateEntry::new_normal(mode, size, mtime);
166 166 DirstateItem::create_instance(py, Cell::new(entry))
167 167 }
168 168
169 def drop_merge_data(&self) -> PyResult<PyNone> {
170 self.update(py, |entry| entry.drop_merge_data());
171 Ok(PyNone)
172 }
173
169 174 def set_clean(
170 175 &self,
171 176 mode: i32,
172 177 size: i32,
173 178 mtime: i32,
174 179 ) -> PyResult<PyNone> {
175 180 self.update(py, |entry| entry.set_clean(mode, size, mtime));
176 181 Ok(PyNone)
177 182 }
178 183
179 184 def set_possibly_dirty(&self) -> PyResult<PyNone> {
180 185 self.update(py, |entry| entry.set_possibly_dirty());
181 186 Ok(PyNone)
182 187 }
183 188
184 189 def set_tracked(&self) -> PyResult<PyNone> {
185 190 self.update(py, |entry| entry.set_tracked());
186 191 Ok(PyNone)
187 192 }
188 193
189 194 def set_untracked(&self) -> PyResult<PyNone> {
190 195 self.update(py, |entry| entry.set_untracked());
191 196 Ok(PyNone)
192 197 }
193 198 });
194 199
195 200 impl DirstateItem {
196 201 pub fn new_as_pyobject(
197 202 py: Python<'_>,
198 203 entry: DirstateEntry,
199 204 ) -> PyResult<PyObject> {
200 205 Ok(DirstateItem::create_instance(py, Cell::new(entry))?.into_object())
201 206 }
202 207
203 208 pub fn get_entry(&self, py: Python<'_>) -> DirstateEntry {
204 209 self.entry(py).get()
205 210 }
206 211
207 212 // TODO: Use https://doc.rust-lang.org/std/cell/struct.Cell.html#method.update instead when it’s stable
208 213 pub fn update(&self, py: Python<'_>, f: impl FnOnce(&mut DirstateEntry)) {
209 214 let mut entry = self.entry(py).get();
210 215 f(&mut entry);
211 216 self.entry(py).set(entry)
212 217 }
213 218 }
General Comments 0
You need to be logged in to leave comments. Login now