##// END OF EJS Templates
dirstate: introduce a set_tracked method on "map" and "item"...
marmoute -
r48804:0d2a404f default
parent child Browse files
Show More
@@ -1,1320 +1,1333 b''
1 1 /*
2 2 parsers.c - efficient content parsing
3 3
4 4 Copyright 2008 Olivia Mackall <olivia@selenic.com> and others
5 5
6 6 This software may be used and distributed according to the terms of
7 7 the GNU General Public License, incorporated herein by reference.
8 8 */
9 9
10 10 #define PY_SSIZE_T_CLEAN
11 11 #include <Python.h>
12 12 #include <ctype.h>
13 13 #include <stddef.h>
14 14 #include <string.h>
15 15
16 16 #include "bitmanipulation.h"
17 17 #include "charencode.h"
18 18 #include "util.h"
19 19
20 20 #ifdef IS_PY3K
21 21 /* The mapping of Python types is meant to be temporary to get Python
22 22 * 3 to compile. We should remove this once Python 3 support is fully
23 23 * supported and proper types are used in the extensions themselves. */
24 24 #define PyInt_Check PyLong_Check
25 25 #define PyInt_FromLong PyLong_FromLong
26 26 #define PyInt_FromSsize_t PyLong_FromSsize_t
27 27 #define PyInt_AsLong PyLong_AsLong
28 28 #endif
29 29
30 30 static const char *const versionerrortext = "Python minor version mismatch";
31 31
32 32 static const int dirstate_v1_from_p2 = -2;
33 33 static const int dirstate_v1_nonnormal = -1;
34 34 static const int ambiguous_time = -1;
35 35
36 36 static PyObject *dict_new_presized(PyObject *self, PyObject *args)
37 37 {
38 38 Py_ssize_t expected_size;
39 39
40 40 if (!PyArg_ParseTuple(args, "n:make_presized_dict", &expected_size)) {
41 41 return NULL;
42 42 }
43 43
44 44 return _dict_new_presized(expected_size);
45 45 }
46 46
47 47 static PyObject *dirstate_item_new(PyTypeObject *subtype, PyObject *args,
48 48 PyObject *kwds)
49 49 {
50 50 /* We do all the initialization here and not a tp_init function because
51 51 * dirstate_item is immutable. */
52 52 dirstateItemObject *t;
53 53 int wc_tracked;
54 54 int p1_tracked;
55 55 int p2_tracked;
56 56 int merged;
57 57 int clean_p1;
58 58 int clean_p2;
59 59 int possibly_dirty;
60 60 PyObject *parentfiledata;
61 61 static char *keywords_name[] = {
62 62 "wc_tracked", "p1_tracked", "p2_tracked",
63 63 "merged", "clean_p1", "clean_p2",
64 64 "possibly_dirty", "parentfiledata", NULL,
65 65 };
66 66 wc_tracked = 0;
67 67 p1_tracked = 0;
68 68 p2_tracked = 0;
69 69 merged = 0;
70 70 clean_p1 = 0;
71 71 clean_p2 = 0;
72 72 possibly_dirty = 0;
73 73 parentfiledata = Py_None;
74 74 if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiiiiiiO", keywords_name,
75 75 &wc_tracked, &p1_tracked, &p2_tracked,
76 76 &merged, &clean_p1, &clean_p2,
77 77 &possibly_dirty, &parentfiledata
78 78
79 79 )) {
80 80 return NULL;
81 81 }
82 82 if (merged && (clean_p1 || clean_p2)) {
83 83 PyErr_SetString(PyExc_RuntimeError,
84 84 "`merged` argument incompatible with "
85 85 "`clean_p1`/`clean_p2`");
86 86 return NULL;
87 87 }
88 88 t = (dirstateItemObject *)subtype->tp_alloc(subtype, 1);
89 89 if (!t) {
90 90 return NULL;
91 91 }
92 92
93 93 t->flags = 0;
94 94 if (wc_tracked) {
95 95 t->flags |= dirstate_flag_wc_tracked;
96 96 }
97 97 if (p1_tracked) {
98 98 t->flags |= dirstate_flag_p1_tracked;
99 99 }
100 100 if (p2_tracked) {
101 101 t->flags |= dirstate_flag_p2_tracked;
102 102 }
103 103 if (possibly_dirty) {
104 104 t->flags |= dirstate_flag_possibly_dirty;
105 105 }
106 106 if (merged) {
107 107 t->flags |= dirstate_flag_merged;
108 108 }
109 109 if (clean_p1) {
110 110 t->flags |= dirstate_flag_clean_p1;
111 111 }
112 112 if (clean_p2) {
113 113 t->flags |= dirstate_flag_clean_p2;
114 114 }
115 115 t->mode = 0;
116 116 t->size = dirstate_v1_nonnormal;
117 117 t->mtime = ambiguous_time;
118 118 if (parentfiledata != Py_None) {
119 119 if (!PyTuple_CheckExact(parentfiledata)) {
120 120 PyErr_SetString(
121 121 PyExc_TypeError,
122 122 "parentfiledata should be a Tuple or None");
123 123 return NULL;
124 124 }
125 125 t->mode =
126 126 (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 0));
127 127 t->size =
128 128 (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 1));
129 129 t->mtime =
130 130 (int)PyLong_AsLong(PyTuple_GetItem(parentfiledata, 2));
131 131 }
132 132 return (PyObject *)t;
133 133 }
134 134
135 135 static void dirstate_item_dealloc(PyObject *o)
136 136 {
137 137 PyObject_Del(o);
138 138 }
139 139
140 140 static inline bool dirstate_item_c_tracked(dirstateItemObject *self)
141 141 {
142 142 return (self->flags & dirstate_flag_wc_tracked);
143 143 }
144 144
145 145 static inline bool dirstate_item_c_added(dirstateItemObject *self)
146 146 {
147 147 unsigned char mask =
148 148 (dirstate_flag_wc_tracked | dirstate_flag_p1_tracked |
149 149 dirstate_flag_p2_tracked);
150 150 unsigned char target = dirstate_flag_wc_tracked;
151 151 return (self->flags & mask) == target;
152 152 }
153 153
154 154 static inline bool dirstate_item_c_removed(dirstateItemObject *self)
155 155 {
156 156 if (self->flags & dirstate_flag_wc_tracked) {
157 157 return false;
158 158 }
159 159 return (self->flags &
160 160 (dirstate_flag_p1_tracked | dirstate_flag_p2_tracked));
161 161 }
162 162
163 163 static inline bool dirstate_item_c_merged(dirstateItemObject *self)
164 164 {
165 165 return ((self->flags & dirstate_flag_wc_tracked) &&
166 166 (self->flags & dirstate_flag_merged));
167 167 }
168 168
169 169 static inline bool dirstate_item_c_merged_removed(dirstateItemObject *self)
170 170 {
171 171 if (!dirstate_item_c_removed(self)) {
172 172 return false;
173 173 }
174 174 return (self->flags & dirstate_flag_merged);
175 175 }
176 176
177 177 static inline bool dirstate_item_c_from_p2(dirstateItemObject *self)
178 178 {
179 179 if (!dirstate_item_c_tracked(self)) {
180 180 return false;
181 181 }
182 182 return (self->flags & dirstate_flag_clean_p2);
183 183 }
184 184
185 185 static inline bool dirstate_item_c_from_p2_removed(dirstateItemObject *self)
186 186 {
187 187 if (!dirstate_item_c_removed(self)) {
188 188 return false;
189 189 }
190 190 return (self->flags & dirstate_flag_clean_p2);
191 191 }
192 192
193 193 static inline char dirstate_item_c_v1_state(dirstateItemObject *self)
194 194 {
195 195 if (self->flags & dirstate_flag_rust_special) {
196 196 return ' ';
197 197 } else if (dirstate_item_c_removed(self)) {
198 198 return 'r';
199 199 } else if (dirstate_item_c_merged(self)) {
200 200 return 'm';
201 201 } else if (dirstate_item_c_added(self)) {
202 202 return 'a';
203 203 } else {
204 204 return 'n';
205 205 }
206 206 }
207 207
208 208 static inline int dirstate_item_c_v1_mode(dirstateItemObject *self)
209 209 {
210 210 return self->mode;
211 211 }
212 212
213 213 static inline int dirstate_item_c_v1_size(dirstateItemObject *self)
214 214 {
215 215 if (self->flags & dirstate_flag_rust_special) {
216 216 return self->size;
217 217 } else if (dirstate_item_c_merged_removed(self)) {
218 218 return dirstate_v1_nonnormal;
219 219 } else if (dirstate_item_c_from_p2_removed(self)) {
220 220 return dirstate_v1_from_p2;
221 221 } else if (dirstate_item_c_removed(self)) {
222 222 return 0;
223 223 } else if (dirstate_item_c_merged(self)) {
224 224 return dirstate_v1_from_p2;
225 225 } else if (dirstate_item_c_added(self)) {
226 226 return dirstate_v1_nonnormal;
227 227 } else if (dirstate_item_c_from_p2(self)) {
228 228 return dirstate_v1_from_p2;
229 229 } else if (self->flags & dirstate_flag_possibly_dirty) {
230 230 return self->size; /* NON NORMAL ? */
231 231 } else {
232 232 return self->size;
233 233 }
234 234 }
235 235
236 236 static inline int dirstate_item_c_v1_mtime(dirstateItemObject *self)
237 237 {
238 238 if (self->flags & dirstate_flag_rust_special) {
239 239 return self->mtime;
240 240 } else if (dirstate_item_c_removed(self)) {
241 241 return 0;
242 242 } else if (self->flags & dirstate_flag_possibly_dirty) {
243 243 return ambiguous_time;
244 244 } else if (dirstate_item_c_merged(self)) {
245 245 return ambiguous_time;
246 246 } else if (dirstate_item_c_added(self)) {
247 247 return ambiguous_time;
248 248 } else if (dirstate_item_c_from_p2(self)) {
249 249 return ambiguous_time;
250 250 } else {
251 251 return self->mtime;
252 252 }
253 253 }
254 254
255 255 static PyObject *dirstate_item_v1_state(dirstateItemObject *self)
256 256 {
257 257 char state = dirstate_item_c_v1_state(self);
258 258 return PyBytes_FromStringAndSize(&state, 1);
259 259 };
260 260
261 261 static PyObject *dirstate_item_v1_mode(dirstateItemObject *self)
262 262 {
263 263 return PyInt_FromLong(dirstate_item_c_v1_mode(self));
264 264 };
265 265
266 266 static PyObject *dirstate_item_v1_size(dirstateItemObject *self)
267 267 {
268 268 return PyInt_FromLong(dirstate_item_c_v1_size(self));
269 269 };
270 270
271 271 static PyObject *dirstate_item_v1_mtime(dirstateItemObject *self)
272 272 {
273 273 return PyInt_FromLong(dirstate_item_c_v1_mtime(self));
274 274 };
275 275
276 276 static PyObject *dirstate_item_need_delay(dirstateItemObject *self,
277 277 PyObject *value)
278 278 {
279 279 long now;
280 280 if (!pylong_to_long(value, &now)) {
281 281 return NULL;
282 282 }
283 283 if (dirstate_item_c_v1_state(self) == 'n' &&
284 284 dirstate_item_c_v1_mtime(self) == now) {
285 285 Py_RETURN_TRUE;
286 286 } else {
287 287 Py_RETURN_FALSE;
288 288 }
289 289 };
290 290
291 291 /* This will never change since it's bound to V1
292 292 */
293 293 static inline dirstateItemObject *
294 294 dirstate_item_from_v1_data(char state, int mode, int size, int mtime)
295 295 {
296 296 dirstateItemObject *t =
297 297 PyObject_New(dirstateItemObject, &dirstateItemType);
298 298 if (!t) {
299 299 return NULL;
300 300 }
301 301
302 302 if (state == 'm') {
303 303 t->flags =
304 304 (dirstate_flag_wc_tracked | dirstate_flag_p1_tracked |
305 305 dirstate_flag_p2_tracked | dirstate_flag_merged);
306 306 t->mode = 0;
307 307 t->size = dirstate_v1_from_p2;
308 308 t->mtime = ambiguous_time;
309 309 } else if (state == 'a') {
310 310 t->flags = dirstate_flag_wc_tracked;
311 311 t->mode = 0;
312 312 t->size = dirstate_v1_nonnormal;
313 313 t->mtime = ambiguous_time;
314 314 } else if (state == 'r') {
315 315 t->mode = 0;
316 316 t->size = 0;
317 317 t->mtime = 0;
318 318 if (size == dirstate_v1_nonnormal) {
319 319 t->flags =
320 320 (dirstate_flag_p1_tracked |
321 321 dirstate_flag_p2_tracked | dirstate_flag_merged);
322 322 } else if (size == dirstate_v1_from_p2) {
323 323 t->flags =
324 324 (dirstate_flag_p2_tracked | dirstate_flag_clean_p2);
325 325 } else {
326 326 t->flags = dirstate_flag_p1_tracked;
327 327 }
328 328 } else if (state == 'n') {
329 329 if (size == dirstate_v1_from_p2) {
330 330 t->flags =
331 331 (dirstate_flag_wc_tracked |
332 332 dirstate_flag_p2_tracked | dirstate_flag_clean_p2);
333 333 t->mode = 0;
334 334 t->size = dirstate_v1_from_p2;
335 335 t->mtime = ambiguous_time;
336 336 } else if (size == dirstate_v1_nonnormal) {
337 337 t->flags = (dirstate_flag_wc_tracked |
338 338 dirstate_flag_p1_tracked |
339 339 dirstate_flag_possibly_dirty);
340 340 t->mode = 0;
341 341 t->size = dirstate_v1_nonnormal;
342 342 t->mtime = ambiguous_time;
343 343 } else if (mtime == ambiguous_time) {
344 344 t->flags = (dirstate_flag_wc_tracked |
345 345 dirstate_flag_p1_tracked |
346 346 dirstate_flag_possibly_dirty);
347 347 t->mode = mode;
348 348 t->size = size;
349 349 t->mtime = 0;
350 350 } else {
351 351 t->flags = (dirstate_flag_wc_tracked |
352 352 dirstate_flag_p1_tracked);
353 353 t->mode = mode;
354 354 t->size = size;
355 355 t->mtime = mtime;
356 356 }
357 357 } else if (state == ' ') {
358 358 /* XXX Rust is using this special case, it should be clean up
359 359 * later. */
360 360 t->flags = dirstate_flag_rust_special;
361 361 t->mode = mode;
362 362 t->size = size;
363 363 t->mtime = mtime;
364 364 } else {
365 365 PyErr_Format(PyExc_RuntimeError,
366 366 "unknown state: `%c` (%d, %d, %d)", state, mode,
367 367 size, mtime, NULL);
368 368 return NULL;
369 369 }
370 370
371 371 return t;
372 372 }
373 373
374 374 /* This will never change since it's bound to V1, unlike `dirstate_item_new` */
375 375 static PyObject *dirstate_item_from_v1_meth(PyTypeObject *subtype,
376 376 PyObject *args)
377 377 {
378 378 /* We do all the initialization here and not a tp_init function because
379 379 * dirstate_item is immutable. */
380 380 char state;
381 381 int size, mode, mtime;
382 382 if (!PyArg_ParseTuple(args, "ciii", &state, &mode, &size, &mtime)) {
383 383 return NULL;
384 384 }
385 385 return (PyObject *)dirstate_item_from_v1_data(state, mode, size, mtime);
386 386 };
387 387
388 388 /* constructor to help legacy API to build a new "added" item
389 389
390 390 Should eventually be removed */
391 391 static PyObject *dirstate_item_new_added(PyTypeObject *subtype)
392 392 {
393 393 dirstateItemObject *t;
394 394 t = (dirstateItemObject *)subtype->tp_alloc(subtype, 1);
395 395 if (!t) {
396 396 return NULL;
397 397 }
398 398 t->flags = dirstate_flag_wc_tracked;
399 399 t->mode = 0;
400 400 t->size = dirstate_v1_nonnormal;
401 401 t->mtime = ambiguous_time;
402 402 return (PyObject *)t;
403 403 };
404 404
405 405 /* constructor to help legacy API to build a new "merged" item
406 406
407 407 Should eventually be removed */
408 408 static PyObject *dirstate_item_new_merged(PyTypeObject *subtype)
409 409 {
410 410 dirstateItemObject *t;
411 411 t = (dirstateItemObject *)subtype->tp_alloc(subtype, 1);
412 412 if (!t) {
413 413 return NULL;
414 414 }
415 415 t->flags = (dirstate_flag_wc_tracked | dirstate_flag_p1_tracked |
416 416 dirstate_flag_p2_tracked | dirstate_flag_merged);
417 417 t->mode = 0;
418 418 t->size = dirstate_v1_from_p2;
419 419 t->mtime = ambiguous_time;
420 420 return (PyObject *)t;
421 421 };
422 422
423 423 /* constructor to help legacy API to build a new "from_p2" item
424 424
425 425 Should eventually be removed */
426 426 static PyObject *dirstate_item_new_from_p2(PyTypeObject *subtype)
427 427 {
428 428 /* We do all the initialization here and not a tp_init function because
429 429 * dirstate_item is immutable. */
430 430 dirstateItemObject *t;
431 431 t = (dirstateItemObject *)subtype->tp_alloc(subtype, 1);
432 432 if (!t) {
433 433 return NULL;
434 434 }
435 435 t->flags = (dirstate_flag_wc_tracked | dirstate_flag_p2_tracked |
436 436 dirstate_flag_clean_p2);
437 437 t->mode = 0;
438 438 t->size = dirstate_v1_from_p2;
439 439 t->mtime = ambiguous_time;
440 440 return (PyObject *)t;
441 441 };
442 442
443 443 /* constructor to help legacy API to build a new "possibly" item
444 444
445 445 Should eventually be removed */
446 446 static PyObject *dirstate_item_new_possibly_dirty(PyTypeObject *subtype)
447 447 {
448 448 /* We do all the initialization here and not a tp_init function because
449 449 * dirstate_item is immutable. */
450 450 dirstateItemObject *t;
451 451 t = (dirstateItemObject *)subtype->tp_alloc(subtype, 1);
452 452 if (!t) {
453 453 return NULL;
454 454 }
455 455 t->flags = (dirstate_flag_wc_tracked | dirstate_flag_p1_tracked |
456 456 dirstate_flag_possibly_dirty);
457 457 t->mode = 0;
458 458 t->size = dirstate_v1_nonnormal;
459 459 t->mtime = ambiguous_time;
460 460 return (PyObject *)t;
461 461 };
462 462
463 463 /* constructor to help legacy API to build a new "normal" item
464 464
465 465 Should eventually be removed */
466 466 static PyObject *dirstate_item_new_normal(PyTypeObject *subtype, PyObject *args)
467 467 {
468 468 /* We do all the initialization here and not a tp_init function because
469 469 * dirstate_item is immutable. */
470 470 dirstateItemObject *t;
471 471 int size, mode, mtime;
472 472 if (!PyArg_ParseTuple(args, "iii", &mode, &size, &mtime)) {
473 473 return NULL;
474 474 }
475 475
476 476 t = (dirstateItemObject *)subtype->tp_alloc(subtype, 1);
477 477 if (!t) {
478 478 return NULL;
479 479 }
480 480 t->flags = (dirstate_flag_wc_tracked | dirstate_flag_p1_tracked);
481 481 t->mode = mode;
482 482 t->size = size;
483 483 t->mtime = mtime;
484 484 return (PyObject *)t;
485 485 };
486 486
487 487 /* This means the next status call will have to actually check its content
488 488 to make sure it is correct. */
489 489 static PyObject *dirstate_item_set_possibly_dirty(dirstateItemObject *self)
490 490 {
491 491 self->flags |= dirstate_flag_possibly_dirty;
492 492 Py_RETURN_NONE;
493 493 }
494 494
495 495 /* See docstring of the python implementation for details */
496 496 static PyObject *dirstate_item_set_clean(dirstateItemObject *self,
497 497 PyObject *args)
498 498 {
499 499 int size, mode, mtime;
500 500 if (!PyArg_ParseTuple(args, "iii", &mode, &size, &mtime)) {
501 501 return NULL;
502 502 }
503 503 self->flags = dirstate_flag_wc_tracked | dirstate_flag_p1_tracked;
504 504 self->mode = mode;
505 505 self->size = size;
506 506 self->mtime = mtime;
507 507 Py_RETURN_NONE;
508 508 }
509 509
510 static PyObject *dirstate_item_set_tracked(dirstateItemObject *self)
511 {
512 self->flags |= dirstate_flag_wc_tracked;
513 self->flags |= dirstate_flag_possibly_dirty;
514 /* size = None on the python size turn into size = NON_NORMAL when
515 * accessed. So the next line is currently required, but a some future
516 * clean up would be welcome. */
517 self->size = dirstate_v1_nonnormal;
518 Py_RETURN_NONE;
519 }
520
510 521 static PyObject *dirstate_item_set_untracked(dirstateItemObject *self)
511 522 {
512 523 self->flags &= ~dirstate_flag_wc_tracked;
513 524 self->mode = 0;
514 525 self->mtime = 0;
515 526 self->size = 0;
516 527 Py_RETURN_NONE;
517 528 }
518 529
519 530 static PyMethodDef dirstate_item_methods[] = {
520 531 {"v1_state", (PyCFunction)dirstate_item_v1_state, METH_NOARGS,
521 532 "return a \"state\" suitable for v1 serialization"},
522 533 {"v1_mode", (PyCFunction)dirstate_item_v1_mode, METH_NOARGS,
523 534 "return a \"mode\" suitable for v1 serialization"},
524 535 {"v1_size", (PyCFunction)dirstate_item_v1_size, METH_NOARGS,
525 536 "return a \"size\" suitable for v1 serialization"},
526 537 {"v1_mtime", (PyCFunction)dirstate_item_v1_mtime, METH_NOARGS,
527 538 "return a \"mtime\" suitable for v1 serialization"},
528 539 {"need_delay", (PyCFunction)dirstate_item_need_delay, METH_O,
529 540 "True if the stored mtime would be ambiguous with the current time"},
530 541 {"from_v1_data", (PyCFunction)dirstate_item_from_v1_meth,
531 542 METH_VARARGS | METH_CLASS, "build a new DirstateItem object from V1 data"},
532 543 {"new_added", (PyCFunction)dirstate_item_new_added,
533 544 METH_NOARGS | METH_CLASS,
534 545 "constructor to help legacy API to build a new \"added\" item"},
535 546 {"new_merged", (PyCFunction)dirstate_item_new_merged,
536 547 METH_NOARGS | METH_CLASS,
537 548 "constructor to help legacy API to build a new \"merged\" item"},
538 549 {"new_from_p2", (PyCFunction)dirstate_item_new_from_p2,
539 550 METH_NOARGS | METH_CLASS,
540 551 "constructor to help legacy API to build a new \"from_p2\" item"},
541 552 {"new_possibly_dirty", (PyCFunction)dirstate_item_new_possibly_dirty,
542 553 METH_NOARGS | METH_CLASS,
543 554 "constructor to help legacy API to build a new \"possibly_dirty\" item"},
544 555 {"new_normal", (PyCFunction)dirstate_item_new_normal,
545 556 METH_VARARGS | METH_CLASS,
546 557 "constructor to help legacy API to build a new \"normal\" item"},
547 558 {"set_possibly_dirty", (PyCFunction)dirstate_item_set_possibly_dirty,
548 559 METH_NOARGS, "mark a file as \"possibly dirty\""},
549 560 {"set_clean", (PyCFunction)dirstate_item_set_clean, METH_VARARGS,
550 561 "mark a file as \"clean\""},
562 {"set_tracked", (PyCFunction)dirstate_item_set_tracked, METH_NOARGS,
563 "mark a file as \"tracked\""},
551 564 {"set_untracked", (PyCFunction)dirstate_item_set_untracked, METH_NOARGS,
552 565 "mark a file as \"untracked\""},
553 566 {NULL} /* Sentinel */
554 567 };
555 568
556 569 static PyObject *dirstate_item_get_mode(dirstateItemObject *self)
557 570 {
558 571 return PyInt_FromLong(dirstate_item_c_v1_mode(self));
559 572 };
560 573
561 574 static PyObject *dirstate_item_get_size(dirstateItemObject *self)
562 575 {
563 576 return PyInt_FromLong(dirstate_item_c_v1_size(self));
564 577 };
565 578
566 579 static PyObject *dirstate_item_get_mtime(dirstateItemObject *self)
567 580 {
568 581 return PyInt_FromLong(dirstate_item_c_v1_mtime(self));
569 582 };
570 583
571 584 static PyObject *dirstate_item_get_state(dirstateItemObject *self)
572 585 {
573 586 char state = dirstate_item_c_v1_state(self);
574 587 return PyBytes_FromStringAndSize(&state, 1);
575 588 };
576 589
577 590 static PyObject *dirstate_item_get_tracked(dirstateItemObject *self)
578 591 {
579 592 if (dirstate_item_c_tracked(self)) {
580 593 Py_RETURN_TRUE;
581 594 } else {
582 595 Py_RETURN_FALSE;
583 596 }
584 597 };
585 598
586 599 static PyObject *dirstate_item_get_added(dirstateItemObject *self)
587 600 {
588 601 if (dirstate_item_c_added(self)) {
589 602 Py_RETURN_TRUE;
590 603 } else {
591 604 Py_RETURN_FALSE;
592 605 }
593 606 };
594 607
595 608 static PyObject *dirstate_item_get_merged(dirstateItemObject *self)
596 609 {
597 610 if (dirstate_item_c_merged(self)) {
598 611 Py_RETURN_TRUE;
599 612 } else {
600 613 Py_RETURN_FALSE;
601 614 }
602 615 };
603 616
604 617 static PyObject *dirstate_item_get_merged_removed(dirstateItemObject *self)
605 618 {
606 619 if (dirstate_item_c_merged_removed(self)) {
607 620 Py_RETURN_TRUE;
608 621 } else {
609 622 Py_RETURN_FALSE;
610 623 }
611 624 };
612 625
613 626 static PyObject *dirstate_item_get_from_p2(dirstateItemObject *self)
614 627 {
615 628 if (dirstate_item_c_from_p2(self)) {
616 629 Py_RETURN_TRUE;
617 630 } else {
618 631 Py_RETURN_FALSE;
619 632 }
620 633 };
621 634
622 635 static PyObject *dirstate_item_get_from_p2_removed(dirstateItemObject *self)
623 636 {
624 637 if (dirstate_item_c_from_p2_removed(self)) {
625 638 Py_RETURN_TRUE;
626 639 } else {
627 640 Py_RETURN_FALSE;
628 641 }
629 642 };
630 643
631 644 static PyObject *dirstate_item_get_removed(dirstateItemObject *self)
632 645 {
633 646 if (dirstate_item_c_removed(self)) {
634 647 Py_RETURN_TRUE;
635 648 } else {
636 649 Py_RETURN_FALSE;
637 650 }
638 651 };
639 652
640 653 static PyObject *dm_nonnormal(dirstateItemObject *self)
641 654 {
642 655 if ((dirstate_item_c_v1_state(self) != 'n') ||
643 656 (dirstate_item_c_v1_mtime(self) == ambiguous_time)) {
644 657 Py_RETURN_TRUE;
645 658 } else {
646 659 Py_RETURN_FALSE;
647 660 }
648 661 };
649 662 static PyObject *dm_otherparent(dirstateItemObject *self)
650 663 {
651 664 if (dirstate_item_c_v1_mtime(self) == dirstate_v1_from_p2) {
652 665 Py_RETURN_TRUE;
653 666 } else {
654 667 Py_RETURN_FALSE;
655 668 }
656 669 };
657 670
658 671 static PyGetSetDef dirstate_item_getset[] = {
659 672 {"mode", (getter)dirstate_item_get_mode, NULL, "mode", NULL},
660 673 {"size", (getter)dirstate_item_get_size, NULL, "size", NULL},
661 674 {"mtime", (getter)dirstate_item_get_mtime, NULL, "mtime", NULL},
662 675 {"state", (getter)dirstate_item_get_state, NULL, "state", NULL},
663 676 {"tracked", (getter)dirstate_item_get_tracked, NULL, "tracked", NULL},
664 677 {"added", (getter)dirstate_item_get_added, NULL, "added", NULL},
665 678 {"merged_removed", (getter)dirstate_item_get_merged_removed, NULL,
666 679 "merged_removed", NULL},
667 680 {"merged", (getter)dirstate_item_get_merged, NULL, "merged", NULL},
668 681 {"from_p2_removed", (getter)dirstate_item_get_from_p2_removed, NULL,
669 682 "from_p2_removed", NULL},
670 683 {"from_p2", (getter)dirstate_item_get_from_p2, NULL, "from_p2", NULL},
671 684 {"removed", (getter)dirstate_item_get_removed, NULL, "removed", NULL},
672 685 {"dm_nonnormal", (getter)dm_nonnormal, NULL, "dm_nonnormal", NULL},
673 686 {"dm_otherparent", (getter)dm_otherparent, NULL, "dm_otherparent", NULL},
674 687 {NULL} /* Sentinel */
675 688 };
676 689
677 690 PyTypeObject dirstateItemType = {
678 691 PyVarObject_HEAD_INIT(NULL, 0) /* header */
679 692 "dirstate_tuple", /* tp_name */
680 693 sizeof(dirstateItemObject), /* tp_basicsize */
681 694 0, /* tp_itemsize */
682 695 (destructor)dirstate_item_dealloc, /* tp_dealloc */
683 696 0, /* tp_print */
684 697 0, /* tp_getattr */
685 698 0, /* tp_setattr */
686 699 0, /* tp_compare */
687 700 0, /* tp_repr */
688 701 0, /* tp_as_number */
689 702 0, /* tp_as_sequence */
690 703 0, /* tp_as_mapping */
691 704 0, /* tp_hash */
692 705 0, /* tp_call */
693 706 0, /* tp_str */
694 707 0, /* tp_getattro */
695 708 0, /* tp_setattro */
696 709 0, /* tp_as_buffer */
697 710 Py_TPFLAGS_DEFAULT, /* tp_flags */
698 711 "dirstate tuple", /* tp_doc */
699 712 0, /* tp_traverse */
700 713 0, /* tp_clear */
701 714 0, /* tp_richcompare */
702 715 0, /* tp_weaklistoffset */
703 716 0, /* tp_iter */
704 717 0, /* tp_iternext */
705 718 dirstate_item_methods, /* tp_methods */
706 719 0, /* tp_members */
707 720 dirstate_item_getset, /* tp_getset */
708 721 0, /* tp_base */
709 722 0, /* tp_dict */
710 723 0, /* tp_descr_get */
711 724 0, /* tp_descr_set */
712 725 0, /* tp_dictoffset */
713 726 0, /* tp_init */
714 727 0, /* tp_alloc */
715 728 dirstate_item_new, /* tp_new */
716 729 };
717 730
718 731 static PyObject *parse_dirstate(PyObject *self, PyObject *args)
719 732 {
720 733 PyObject *dmap, *cmap, *parents = NULL, *ret = NULL;
721 734 PyObject *fname = NULL, *cname = NULL, *entry = NULL;
722 735 char state, *cur, *str, *cpos;
723 736 int mode, size, mtime;
724 737 unsigned int flen, pos = 40;
725 738 Py_ssize_t len = 40;
726 739 Py_ssize_t readlen;
727 740
728 741 if (!PyArg_ParseTuple(
729 742 args, PY23("O!O!s#:parse_dirstate", "O!O!y#:parse_dirstate"),
730 743 &PyDict_Type, &dmap, &PyDict_Type, &cmap, &str, &readlen)) {
731 744 goto quit;
732 745 }
733 746
734 747 len = readlen;
735 748
736 749 /* read parents */
737 750 if (len < 40) {
738 751 PyErr_SetString(PyExc_ValueError,
739 752 "too little data for parents");
740 753 goto quit;
741 754 }
742 755
743 756 parents = Py_BuildValue(PY23("s#s#", "y#y#"), str, (Py_ssize_t)20,
744 757 str + 20, (Py_ssize_t)20);
745 758 if (!parents) {
746 759 goto quit;
747 760 }
748 761
749 762 /* read filenames */
750 763 while (pos >= 40 && pos < len) {
751 764 if (pos + 17 > len) {
752 765 PyErr_SetString(PyExc_ValueError,
753 766 "overflow in dirstate");
754 767 goto quit;
755 768 }
756 769 cur = str + pos;
757 770 /* unpack header */
758 771 state = *cur;
759 772 mode = getbe32(cur + 1);
760 773 size = getbe32(cur + 5);
761 774 mtime = getbe32(cur + 9);
762 775 flen = getbe32(cur + 13);
763 776 pos += 17;
764 777 cur += 17;
765 778 if (flen > len - pos) {
766 779 PyErr_SetString(PyExc_ValueError,
767 780 "overflow in dirstate");
768 781 goto quit;
769 782 }
770 783
771 784 entry = (PyObject *)dirstate_item_from_v1_data(state, mode,
772 785 size, mtime);
773 786 cpos = memchr(cur, 0, flen);
774 787 if (cpos) {
775 788 fname = PyBytes_FromStringAndSize(cur, cpos - cur);
776 789 cname = PyBytes_FromStringAndSize(
777 790 cpos + 1, flen - (cpos - cur) - 1);
778 791 if (!fname || !cname ||
779 792 PyDict_SetItem(cmap, fname, cname) == -1 ||
780 793 PyDict_SetItem(dmap, fname, entry) == -1) {
781 794 goto quit;
782 795 }
783 796 Py_DECREF(cname);
784 797 } else {
785 798 fname = PyBytes_FromStringAndSize(cur, flen);
786 799 if (!fname ||
787 800 PyDict_SetItem(dmap, fname, entry) == -1) {
788 801 goto quit;
789 802 }
790 803 }
791 804 Py_DECREF(fname);
792 805 Py_DECREF(entry);
793 806 fname = cname = entry = NULL;
794 807 pos += flen;
795 808 }
796 809
797 810 ret = parents;
798 811 Py_INCREF(ret);
799 812 quit:
800 813 Py_XDECREF(fname);
801 814 Py_XDECREF(cname);
802 815 Py_XDECREF(entry);
803 816 Py_XDECREF(parents);
804 817 return ret;
805 818 }
806 819
807 820 /*
808 821 * Build a set of non-normal and other parent entries from the dirstate dmap
809 822 */
810 823 static PyObject *nonnormalotherparententries(PyObject *self, PyObject *args)
811 824 {
812 825 PyObject *dmap, *fname, *v;
813 826 PyObject *nonnset = NULL, *otherpset = NULL, *result = NULL;
814 827 Py_ssize_t pos;
815 828
816 829 if (!PyArg_ParseTuple(args, "O!:nonnormalentries", &PyDict_Type,
817 830 &dmap)) {
818 831 goto bail;
819 832 }
820 833
821 834 nonnset = PySet_New(NULL);
822 835 if (nonnset == NULL) {
823 836 goto bail;
824 837 }
825 838
826 839 otherpset = PySet_New(NULL);
827 840 if (otherpset == NULL) {
828 841 goto bail;
829 842 }
830 843
831 844 pos = 0;
832 845 while (PyDict_Next(dmap, &pos, &fname, &v)) {
833 846 dirstateItemObject *t;
834 847 if (!dirstate_tuple_check(v)) {
835 848 PyErr_SetString(PyExc_TypeError,
836 849 "expected a dirstate tuple");
837 850 goto bail;
838 851 }
839 852 t = (dirstateItemObject *)v;
840 853
841 854 if (dirstate_item_c_from_p2(t)) {
842 855 if (PySet_Add(otherpset, fname) == -1) {
843 856 goto bail;
844 857 }
845 858 }
846 859 if (!(t->flags & dirstate_flag_wc_tracked) ||
847 860 !(t->flags &
848 861 (dirstate_flag_p1_tracked | dirstate_flag_p2_tracked)) ||
849 862 (t->flags &
850 863 (dirstate_flag_possibly_dirty | dirstate_flag_merged))) {
851 864 if (PySet_Add(nonnset, fname) == -1) {
852 865 goto bail;
853 866 }
854 867 }
855 868 }
856 869
857 870 result = Py_BuildValue("(OO)", nonnset, otherpset);
858 871 if (result == NULL) {
859 872 goto bail;
860 873 }
861 874 Py_DECREF(nonnset);
862 875 Py_DECREF(otherpset);
863 876 return result;
864 877 bail:
865 878 Py_XDECREF(nonnset);
866 879 Py_XDECREF(otherpset);
867 880 Py_XDECREF(result);
868 881 return NULL;
869 882 }
870 883
871 884 /*
872 885 * Efficiently pack a dirstate object into its on-disk format.
873 886 */
874 887 static PyObject *pack_dirstate(PyObject *self, PyObject *args)
875 888 {
876 889 PyObject *packobj = NULL;
877 890 PyObject *map, *copymap, *pl, *mtime_unset = NULL;
878 891 Py_ssize_t nbytes, pos, l;
879 892 PyObject *k, *v = NULL, *pn;
880 893 char *p, *s;
881 894 int now;
882 895
883 896 if (!PyArg_ParseTuple(args, "O!O!O!i:pack_dirstate", &PyDict_Type, &map,
884 897 &PyDict_Type, &copymap, &PyTuple_Type, &pl,
885 898 &now)) {
886 899 return NULL;
887 900 }
888 901
889 902 if (PyTuple_Size(pl) != 2) {
890 903 PyErr_SetString(PyExc_TypeError, "expected 2-element tuple");
891 904 return NULL;
892 905 }
893 906
894 907 /* Figure out how much we need to allocate. */
895 908 for (nbytes = 40, pos = 0; PyDict_Next(map, &pos, &k, &v);) {
896 909 PyObject *c;
897 910 if (!PyBytes_Check(k)) {
898 911 PyErr_SetString(PyExc_TypeError, "expected string key");
899 912 goto bail;
900 913 }
901 914 nbytes += PyBytes_GET_SIZE(k) + 17;
902 915 c = PyDict_GetItem(copymap, k);
903 916 if (c) {
904 917 if (!PyBytes_Check(c)) {
905 918 PyErr_SetString(PyExc_TypeError,
906 919 "expected string key");
907 920 goto bail;
908 921 }
909 922 nbytes += PyBytes_GET_SIZE(c) + 1;
910 923 }
911 924 }
912 925
913 926 packobj = PyBytes_FromStringAndSize(NULL, nbytes);
914 927 if (packobj == NULL) {
915 928 goto bail;
916 929 }
917 930
918 931 p = PyBytes_AS_STRING(packobj);
919 932
920 933 pn = PyTuple_GET_ITEM(pl, 0);
921 934 if (PyBytes_AsStringAndSize(pn, &s, &l) == -1 || l != 20) {
922 935 PyErr_SetString(PyExc_TypeError, "expected a 20-byte hash");
923 936 goto bail;
924 937 }
925 938 memcpy(p, s, l);
926 939 p += 20;
927 940 pn = PyTuple_GET_ITEM(pl, 1);
928 941 if (PyBytes_AsStringAndSize(pn, &s, &l) == -1 || l != 20) {
929 942 PyErr_SetString(PyExc_TypeError, "expected a 20-byte hash");
930 943 goto bail;
931 944 }
932 945 memcpy(p, s, l);
933 946 p += 20;
934 947
935 948 for (pos = 0; PyDict_Next(map, &pos, &k, &v);) {
936 949 dirstateItemObject *tuple;
937 950 char state;
938 951 int mode, size, mtime;
939 952 Py_ssize_t len, l;
940 953 PyObject *o;
941 954 char *t;
942 955
943 956 if (!dirstate_tuple_check(v)) {
944 957 PyErr_SetString(PyExc_TypeError,
945 958 "expected a dirstate tuple");
946 959 goto bail;
947 960 }
948 961 tuple = (dirstateItemObject *)v;
949 962
950 963 state = dirstate_item_c_v1_state(tuple);
951 964 mode = dirstate_item_c_v1_mode(tuple);
952 965 size = dirstate_item_c_v1_size(tuple);
953 966 mtime = dirstate_item_c_v1_mtime(tuple);
954 967 if (state == 'n' && mtime == now) {
955 968 /* See pure/parsers.py:pack_dirstate for why we do
956 969 * this. */
957 970 mtime = -1;
958 971 mtime_unset = (PyObject *)dirstate_item_from_v1_data(
959 972 state, mode, size, mtime);
960 973 if (!mtime_unset) {
961 974 goto bail;
962 975 }
963 976 if (PyDict_SetItem(map, k, mtime_unset) == -1) {
964 977 goto bail;
965 978 }
966 979 Py_DECREF(mtime_unset);
967 980 mtime_unset = NULL;
968 981 }
969 982 *p++ = state;
970 983 putbe32((uint32_t)mode, p);
971 984 putbe32((uint32_t)size, p + 4);
972 985 putbe32((uint32_t)mtime, p + 8);
973 986 t = p + 12;
974 987 p += 16;
975 988 len = PyBytes_GET_SIZE(k);
976 989 memcpy(p, PyBytes_AS_STRING(k), len);
977 990 p += len;
978 991 o = PyDict_GetItem(copymap, k);
979 992 if (o) {
980 993 *p++ = '\0';
981 994 l = PyBytes_GET_SIZE(o);
982 995 memcpy(p, PyBytes_AS_STRING(o), l);
983 996 p += l;
984 997 len += l + 1;
985 998 }
986 999 putbe32((uint32_t)len, t);
987 1000 }
988 1001
989 1002 pos = p - PyBytes_AS_STRING(packobj);
990 1003 if (pos != nbytes) {
991 1004 PyErr_Format(PyExc_SystemError, "bad dirstate size: %ld != %ld",
992 1005 (long)pos, (long)nbytes);
993 1006 goto bail;
994 1007 }
995 1008
996 1009 return packobj;
997 1010 bail:
998 1011 Py_XDECREF(mtime_unset);
999 1012 Py_XDECREF(packobj);
1000 1013 Py_XDECREF(v);
1001 1014 return NULL;
1002 1015 }
1003 1016
1004 1017 #define BUMPED_FIX 1
1005 1018 #define USING_SHA_256 2
1006 1019 #define FM1_HEADER_SIZE (4 + 8 + 2 + 2 + 1 + 1 + 1)
1007 1020
1008 1021 static PyObject *readshas(const char *source, unsigned char num,
1009 1022 Py_ssize_t hashwidth)
1010 1023 {
1011 1024 int i;
1012 1025 PyObject *list = PyTuple_New(num);
1013 1026 if (list == NULL) {
1014 1027 return NULL;
1015 1028 }
1016 1029 for (i = 0; i < num; i++) {
1017 1030 PyObject *hash = PyBytes_FromStringAndSize(source, hashwidth);
1018 1031 if (hash == NULL) {
1019 1032 Py_DECREF(list);
1020 1033 return NULL;
1021 1034 }
1022 1035 PyTuple_SET_ITEM(list, i, hash);
1023 1036 source += hashwidth;
1024 1037 }
1025 1038 return list;
1026 1039 }
1027 1040
1028 1041 static PyObject *fm1readmarker(const char *databegin, const char *dataend,
1029 1042 uint32_t *msize)
1030 1043 {
1031 1044 const char *data = databegin;
1032 1045 const char *meta;
1033 1046
1034 1047 double mtime;
1035 1048 int16_t tz;
1036 1049 uint16_t flags;
1037 1050 unsigned char nsuccs, nparents, nmetadata;
1038 1051 Py_ssize_t hashwidth = 20;
1039 1052
1040 1053 PyObject *prec = NULL, *parents = NULL, *succs = NULL;
1041 1054 PyObject *metadata = NULL, *ret = NULL;
1042 1055 int i;
1043 1056
1044 1057 if (data + FM1_HEADER_SIZE > dataend) {
1045 1058 goto overflow;
1046 1059 }
1047 1060
1048 1061 *msize = getbe32(data);
1049 1062 data += 4;
1050 1063 mtime = getbefloat64(data);
1051 1064 data += 8;
1052 1065 tz = getbeint16(data);
1053 1066 data += 2;
1054 1067 flags = getbeuint16(data);
1055 1068 data += 2;
1056 1069
1057 1070 if (flags & USING_SHA_256) {
1058 1071 hashwidth = 32;
1059 1072 }
1060 1073
1061 1074 nsuccs = (unsigned char)(*data++);
1062 1075 nparents = (unsigned char)(*data++);
1063 1076 nmetadata = (unsigned char)(*data++);
1064 1077
1065 1078 if (databegin + *msize > dataend) {
1066 1079 goto overflow;
1067 1080 }
1068 1081 dataend = databegin + *msize; /* narrow down to marker size */
1069 1082
1070 1083 if (data + hashwidth > dataend) {
1071 1084 goto overflow;
1072 1085 }
1073 1086 prec = PyBytes_FromStringAndSize(data, hashwidth);
1074 1087 data += hashwidth;
1075 1088 if (prec == NULL) {
1076 1089 goto bail;
1077 1090 }
1078 1091
1079 1092 if (data + nsuccs * hashwidth > dataend) {
1080 1093 goto overflow;
1081 1094 }
1082 1095 succs = readshas(data, nsuccs, hashwidth);
1083 1096 if (succs == NULL) {
1084 1097 goto bail;
1085 1098 }
1086 1099 data += nsuccs * hashwidth;
1087 1100
1088 1101 if (nparents == 1 || nparents == 2) {
1089 1102 if (data + nparents * hashwidth > dataend) {
1090 1103 goto overflow;
1091 1104 }
1092 1105 parents = readshas(data, nparents, hashwidth);
1093 1106 if (parents == NULL) {
1094 1107 goto bail;
1095 1108 }
1096 1109 data += nparents * hashwidth;
1097 1110 } else {
1098 1111 parents = Py_None;
1099 1112 Py_INCREF(parents);
1100 1113 }
1101 1114
1102 1115 if (data + 2 * nmetadata > dataend) {
1103 1116 goto overflow;
1104 1117 }
1105 1118 meta = data + (2 * nmetadata);
1106 1119 metadata = PyTuple_New(nmetadata);
1107 1120 if (metadata == NULL) {
1108 1121 goto bail;
1109 1122 }
1110 1123 for (i = 0; i < nmetadata; i++) {
1111 1124 PyObject *tmp, *left = NULL, *right = NULL;
1112 1125 Py_ssize_t leftsize = (unsigned char)(*data++);
1113 1126 Py_ssize_t rightsize = (unsigned char)(*data++);
1114 1127 if (meta + leftsize + rightsize > dataend) {
1115 1128 goto overflow;
1116 1129 }
1117 1130 left = PyBytes_FromStringAndSize(meta, leftsize);
1118 1131 meta += leftsize;
1119 1132 right = PyBytes_FromStringAndSize(meta, rightsize);
1120 1133 meta += rightsize;
1121 1134 tmp = PyTuple_New(2);
1122 1135 if (!left || !right || !tmp) {
1123 1136 Py_XDECREF(left);
1124 1137 Py_XDECREF(right);
1125 1138 Py_XDECREF(tmp);
1126 1139 goto bail;
1127 1140 }
1128 1141 PyTuple_SET_ITEM(tmp, 0, left);
1129 1142 PyTuple_SET_ITEM(tmp, 1, right);
1130 1143 PyTuple_SET_ITEM(metadata, i, tmp);
1131 1144 }
1132 1145 ret = Py_BuildValue("(OOHO(di)O)", prec, succs, flags, metadata, mtime,
1133 1146 (int)tz * 60, parents);
1134 1147 goto bail; /* return successfully */
1135 1148
1136 1149 overflow:
1137 1150 PyErr_SetString(PyExc_ValueError, "overflow in obsstore");
1138 1151 bail:
1139 1152 Py_XDECREF(prec);
1140 1153 Py_XDECREF(succs);
1141 1154 Py_XDECREF(metadata);
1142 1155 Py_XDECREF(parents);
1143 1156 return ret;
1144 1157 }
1145 1158
1146 1159 static PyObject *fm1readmarkers(PyObject *self, PyObject *args)
1147 1160 {
1148 1161 const char *data, *dataend;
1149 1162 Py_ssize_t datalen, offset, stop;
1150 1163 PyObject *markers = NULL;
1151 1164
1152 1165 if (!PyArg_ParseTuple(args, PY23("s#nn", "y#nn"), &data, &datalen,
1153 1166 &offset, &stop)) {
1154 1167 return NULL;
1155 1168 }
1156 1169 if (offset < 0) {
1157 1170 PyErr_SetString(PyExc_ValueError,
1158 1171 "invalid negative offset in fm1readmarkers");
1159 1172 return NULL;
1160 1173 }
1161 1174 if (stop > datalen) {
1162 1175 PyErr_SetString(
1163 1176 PyExc_ValueError,
1164 1177 "stop longer than data length in fm1readmarkers");
1165 1178 return NULL;
1166 1179 }
1167 1180 dataend = data + datalen;
1168 1181 data += offset;
1169 1182 markers = PyList_New(0);
1170 1183 if (!markers) {
1171 1184 return NULL;
1172 1185 }
1173 1186 while (offset < stop) {
1174 1187 uint32_t msize;
1175 1188 int error;
1176 1189 PyObject *record = fm1readmarker(data, dataend, &msize);
1177 1190 if (!record) {
1178 1191 goto bail;
1179 1192 }
1180 1193 error = PyList_Append(markers, record);
1181 1194 Py_DECREF(record);
1182 1195 if (error) {
1183 1196 goto bail;
1184 1197 }
1185 1198 data += msize;
1186 1199 offset += msize;
1187 1200 }
1188 1201 return markers;
1189 1202 bail:
1190 1203 Py_DECREF(markers);
1191 1204 return NULL;
1192 1205 }
1193 1206
1194 1207 static char parsers_doc[] = "Efficient content parsing.";
1195 1208
1196 1209 PyObject *encodedir(PyObject *self, PyObject *args);
1197 1210 PyObject *pathencode(PyObject *self, PyObject *args);
1198 1211 PyObject *lowerencode(PyObject *self, PyObject *args);
1199 1212 PyObject *parse_index2(PyObject *self, PyObject *args, PyObject *kwargs);
1200 1213
1201 1214 static PyMethodDef methods[] = {
1202 1215 {"pack_dirstate", pack_dirstate, METH_VARARGS, "pack a dirstate\n"},
1203 1216 {"nonnormalotherparententries", nonnormalotherparententries, METH_VARARGS,
1204 1217 "create a set containing non-normal and other parent entries of given "
1205 1218 "dirstate\n"},
1206 1219 {"parse_dirstate", parse_dirstate, METH_VARARGS, "parse a dirstate\n"},
1207 1220 {"parse_index2", (PyCFunction)parse_index2, METH_VARARGS | METH_KEYWORDS,
1208 1221 "parse a revlog index\n"},
1209 1222 {"isasciistr", isasciistr, METH_VARARGS, "check if an ASCII string\n"},
1210 1223 {"asciilower", asciilower, METH_VARARGS, "lowercase an ASCII string\n"},
1211 1224 {"asciiupper", asciiupper, METH_VARARGS, "uppercase an ASCII string\n"},
1212 1225 {"dict_new_presized", dict_new_presized, METH_VARARGS,
1213 1226 "construct a dict with an expected size\n"},
1214 1227 {"make_file_foldmap", make_file_foldmap, METH_VARARGS,
1215 1228 "make file foldmap\n"},
1216 1229 {"jsonescapeu8fast", jsonescapeu8fast, METH_VARARGS,
1217 1230 "escape a UTF-8 byte string to JSON (fast path)\n"},
1218 1231 {"encodedir", encodedir, METH_VARARGS, "encodedir a path\n"},
1219 1232 {"pathencode", pathencode, METH_VARARGS, "fncache-encode a path\n"},
1220 1233 {"lowerencode", lowerencode, METH_VARARGS, "lower-encode a path\n"},
1221 1234 {"fm1readmarkers", fm1readmarkers, METH_VARARGS,
1222 1235 "parse v1 obsolete markers\n"},
1223 1236 {NULL, NULL}};
1224 1237
1225 1238 void dirs_module_init(PyObject *mod);
1226 1239 void manifest_module_init(PyObject *mod);
1227 1240 void revlog_module_init(PyObject *mod);
1228 1241
1229 1242 static const int version = 20;
1230 1243
1231 1244 static void module_init(PyObject *mod)
1232 1245 {
1233 1246 PyObject *capsule = NULL;
1234 1247 PyModule_AddIntConstant(mod, "version", version);
1235 1248
1236 1249 /* This module constant has two purposes. First, it lets us unit test
1237 1250 * the ImportError raised without hard-coding any error text. This
1238 1251 * means we can change the text in the future without breaking tests,
1239 1252 * even across changesets without a recompile. Second, its presence
1240 1253 * can be used to determine whether the version-checking logic is
1241 1254 * present, which also helps in testing across changesets without a
1242 1255 * recompile. Note that this means the pure-Python version of parsers
1243 1256 * should not have this module constant. */
1244 1257 PyModule_AddStringConstant(mod, "versionerrortext", versionerrortext);
1245 1258
1246 1259 dirs_module_init(mod);
1247 1260 manifest_module_init(mod);
1248 1261 revlog_module_init(mod);
1249 1262
1250 1263 capsule = PyCapsule_New(
1251 1264 dirstate_item_from_v1_data,
1252 1265 "mercurial.cext.parsers.make_dirstate_item_CAPI", NULL);
1253 1266 if (capsule != NULL)
1254 1267 PyModule_AddObject(mod, "make_dirstate_item_CAPI", capsule);
1255 1268
1256 1269 if (PyType_Ready(&dirstateItemType) < 0) {
1257 1270 return;
1258 1271 }
1259 1272 Py_INCREF(&dirstateItemType);
1260 1273 PyModule_AddObject(mod, "DirstateItem", (PyObject *)&dirstateItemType);
1261 1274 }
1262 1275
1263 1276 static int check_python_version(void)
1264 1277 {
1265 1278 PyObject *sys = PyImport_ImportModule("sys"), *ver;
1266 1279 long hexversion;
1267 1280 if (!sys) {
1268 1281 return -1;
1269 1282 }
1270 1283 ver = PyObject_GetAttrString(sys, "hexversion");
1271 1284 Py_DECREF(sys);
1272 1285 if (!ver) {
1273 1286 return -1;
1274 1287 }
1275 1288 hexversion = PyInt_AsLong(ver);
1276 1289 Py_DECREF(ver);
1277 1290 /* sys.hexversion is a 32-bit number by default, so the -1 case
1278 1291 * should only occur in unusual circumstances (e.g. if sys.hexversion
1279 1292 * is manually set to an invalid value). */
1280 1293 if ((hexversion == -1) || (hexversion >> 16 != PY_VERSION_HEX >> 16)) {
1281 1294 PyErr_Format(PyExc_ImportError,
1282 1295 "%s: The Mercurial extension "
1283 1296 "modules were compiled with Python " PY_VERSION
1284 1297 ", but "
1285 1298 "Mercurial is currently using Python with "
1286 1299 "sys.hexversion=%ld: "
1287 1300 "Python %s\n at: %s",
1288 1301 versionerrortext, hexversion, Py_GetVersion(),
1289 1302 Py_GetProgramFullPath());
1290 1303 return -1;
1291 1304 }
1292 1305 return 0;
1293 1306 }
1294 1307
1295 1308 #ifdef IS_PY3K
1296 1309 static struct PyModuleDef parsers_module = {PyModuleDef_HEAD_INIT, "parsers",
1297 1310 parsers_doc, -1, methods};
1298 1311
1299 1312 PyMODINIT_FUNC PyInit_parsers(void)
1300 1313 {
1301 1314 PyObject *mod;
1302 1315
1303 1316 if (check_python_version() == -1)
1304 1317 return NULL;
1305 1318 mod = PyModule_Create(&parsers_module);
1306 1319 module_init(mod);
1307 1320 return mod;
1308 1321 }
1309 1322 #else
1310 1323 PyMODINIT_FUNC initparsers(void)
1311 1324 {
1312 1325 PyObject *mod;
1313 1326
1314 1327 if (check_python_version() == -1) {
1315 1328 return;
1316 1329 }
1317 1330 mod = Py_InitModule3("parsers", methods, parsers_doc);
1318 1331 module_init(mod);
1319 1332 }
1320 1333 #endif
@@ -1,1616 +1,1607 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .pycompat import delattr
18 18
19 19 from hgdemandimport import tracing
20 20
21 21 from . import (
22 22 dirstatemap,
23 23 encoding,
24 24 error,
25 25 match as matchmod,
26 26 pathutil,
27 27 policy,
28 28 pycompat,
29 29 scmutil,
30 30 sparse,
31 31 util,
32 32 )
33 33
34 34 from .interfaces import (
35 35 dirstate as intdirstate,
36 36 util as interfaceutil,
37 37 )
38 38
39 39 parsers = policy.importmod('parsers')
40 40 rustmod = policy.importrust('dirstate')
41 41
42 42 SUPPORTS_DIRSTATE_V2 = rustmod is not None
43 43
44 44 propertycache = util.propertycache
45 45 filecache = scmutil.filecache
46 46 _rangemask = dirstatemap.rangemask
47 47
48 48 DirstateItem = parsers.DirstateItem
49 49
50 50
51 51 class repocache(filecache):
52 52 """filecache for files in .hg/"""
53 53
54 54 def join(self, obj, fname):
55 55 return obj._opener.join(fname)
56 56
57 57
58 58 class rootcache(filecache):
59 59 """filecache for files in the repository root"""
60 60
61 61 def join(self, obj, fname):
62 62 return obj._join(fname)
63 63
64 64
65 65 def _getfsnow(vfs):
66 66 '''Get "now" timestamp on filesystem'''
67 67 tmpfd, tmpname = vfs.mkstemp()
68 68 try:
69 69 return os.fstat(tmpfd)[stat.ST_MTIME]
70 70 finally:
71 71 os.close(tmpfd)
72 72 vfs.unlink(tmpname)
73 73
74 74
75 75 def requires_parents_change(func):
76 76 def wrap(self, *args, **kwargs):
77 77 if not self.pendingparentchange():
78 78 msg = 'calling `%s` outside of a parentchange context'
79 79 msg %= func.__name__
80 80 raise error.ProgrammingError(msg)
81 81 return func(self, *args, **kwargs)
82 82
83 83 return wrap
84 84
85 85
86 86 def requires_no_parents_change(func):
87 87 def wrap(self, *args, **kwargs):
88 88 if self.pendingparentchange():
89 89 msg = 'calling `%s` inside of a parentchange context'
90 90 msg %= func.__name__
91 91 raise error.ProgrammingError(msg)
92 92 return func(self, *args, **kwargs)
93 93
94 94 return wrap
95 95
96 96
97 97 @interfaceutil.implementer(intdirstate.idirstate)
98 98 class dirstate(object):
99 99 def __init__(
100 100 self,
101 101 opener,
102 102 ui,
103 103 root,
104 104 validate,
105 105 sparsematchfn,
106 106 nodeconstants,
107 107 use_dirstate_v2,
108 108 ):
109 109 """Create a new dirstate object.
110 110
111 111 opener is an open()-like callable that can be used to open the
112 112 dirstate file; root is the root of the directory tracked by
113 113 the dirstate.
114 114 """
115 115 self._use_dirstate_v2 = use_dirstate_v2
116 116 self._nodeconstants = nodeconstants
117 117 self._opener = opener
118 118 self._validate = validate
119 119 self._root = root
120 120 self._sparsematchfn = sparsematchfn
121 121 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
122 122 # UNC path pointing to root share (issue4557)
123 123 self._rootdir = pathutil.normasprefix(root)
124 124 self._dirty = False
125 125 self._lastnormaltime = 0
126 126 self._ui = ui
127 127 self._filecache = {}
128 128 self._parentwriters = 0
129 129 self._filename = b'dirstate'
130 130 self._pendingfilename = b'%s.pending' % self._filename
131 131 self._plchangecallbacks = {}
132 132 self._origpl = None
133 133 self._updatedfiles = set()
134 134 self._mapcls = dirstatemap.dirstatemap
135 135 # Access and cache cwd early, so we don't access it for the first time
136 136 # after a working-copy update caused it to not exist (accessing it then
137 137 # raises an exception).
138 138 self._cwd
139 139
140 140 def prefetch_parents(self):
141 141 """make sure the parents are loaded
142 142
143 143 Used to avoid a race condition.
144 144 """
145 145 self._pl
146 146
147 147 @contextlib.contextmanager
148 148 def parentchange(self):
149 149 """Context manager for handling dirstate parents.
150 150
151 151 If an exception occurs in the scope of the context manager,
152 152 the incoherent dirstate won't be written when wlock is
153 153 released.
154 154 """
155 155 self._parentwriters += 1
156 156 yield
157 157 # Typically we want the "undo" step of a context manager in a
158 158 # finally block so it happens even when an exception
159 159 # occurs. In this case, however, we only want to decrement
160 160 # parentwriters if the code in the with statement exits
161 161 # normally, so we don't have a try/finally here on purpose.
162 162 self._parentwriters -= 1
163 163
164 164 def pendingparentchange(self):
165 165 """Returns true if the dirstate is in the middle of a set of changes
166 166 that modify the dirstate parent.
167 167 """
168 168 return self._parentwriters > 0
169 169
170 170 @propertycache
171 171 def _map(self):
172 172 """Return the dirstate contents (see documentation for dirstatemap)."""
173 173 self._map = self._mapcls(
174 174 self._ui,
175 175 self._opener,
176 176 self._root,
177 177 self._nodeconstants,
178 178 self._use_dirstate_v2,
179 179 )
180 180 return self._map
181 181
182 182 @property
183 183 def _sparsematcher(self):
184 184 """The matcher for the sparse checkout.
185 185
186 186 The working directory may not include every file from a manifest. The
187 187 matcher obtained by this property will match a path if it is to be
188 188 included in the working directory.
189 189 """
190 190 # TODO there is potential to cache this property. For now, the matcher
191 191 # is resolved on every access. (But the called function does use a
192 192 # cache to keep the lookup fast.)
193 193 return self._sparsematchfn()
194 194
195 195 @repocache(b'branch')
196 196 def _branch(self):
197 197 try:
198 198 return self._opener.read(b"branch").strip() or b"default"
199 199 except IOError as inst:
200 200 if inst.errno != errno.ENOENT:
201 201 raise
202 202 return b"default"
203 203
204 204 @property
205 205 def _pl(self):
206 206 return self._map.parents()
207 207
208 208 def hasdir(self, d):
209 209 return self._map.hastrackeddir(d)
210 210
211 211 @rootcache(b'.hgignore')
212 212 def _ignore(self):
213 213 files = self._ignorefiles()
214 214 if not files:
215 215 return matchmod.never()
216 216
217 217 pats = [b'include:%s' % f for f in files]
218 218 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
219 219
220 220 @propertycache
221 221 def _slash(self):
222 222 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
223 223
224 224 @propertycache
225 225 def _checklink(self):
226 226 return util.checklink(self._root)
227 227
228 228 @propertycache
229 229 def _checkexec(self):
230 230 return bool(util.checkexec(self._root))
231 231
232 232 @propertycache
233 233 def _checkcase(self):
234 234 return not util.fscasesensitive(self._join(b'.hg'))
235 235
236 236 def _join(self, f):
237 237 # much faster than os.path.join()
238 238 # it's safe because f is always a relative path
239 239 return self._rootdir + f
240 240
241 241 def flagfunc(self, buildfallback):
242 242 if self._checklink and self._checkexec:
243 243
244 244 def f(x):
245 245 try:
246 246 st = os.lstat(self._join(x))
247 247 if util.statislink(st):
248 248 return b'l'
249 249 if util.statisexec(st):
250 250 return b'x'
251 251 except OSError:
252 252 pass
253 253 return b''
254 254
255 255 return f
256 256
257 257 fallback = buildfallback()
258 258 if self._checklink:
259 259
260 260 def f(x):
261 261 if os.path.islink(self._join(x)):
262 262 return b'l'
263 263 if b'x' in fallback(x):
264 264 return b'x'
265 265 return b''
266 266
267 267 return f
268 268 if self._checkexec:
269 269
270 270 def f(x):
271 271 if b'l' in fallback(x):
272 272 return b'l'
273 273 if util.isexec(self._join(x)):
274 274 return b'x'
275 275 return b''
276 276
277 277 return f
278 278 else:
279 279 return fallback
280 280
281 281 @propertycache
282 282 def _cwd(self):
283 283 # internal config: ui.forcecwd
284 284 forcecwd = self._ui.config(b'ui', b'forcecwd')
285 285 if forcecwd:
286 286 return forcecwd
287 287 return encoding.getcwd()
288 288
289 289 def getcwd(self):
290 290 """Return the path from which a canonical path is calculated.
291 291
292 292 This path should be used to resolve file patterns or to convert
293 293 canonical paths back to file paths for display. It shouldn't be
294 294 used to get real file paths. Use vfs functions instead.
295 295 """
296 296 cwd = self._cwd
297 297 if cwd == self._root:
298 298 return b''
299 299 # self._root ends with a path separator if self._root is '/' or 'C:\'
300 300 rootsep = self._root
301 301 if not util.endswithsep(rootsep):
302 302 rootsep += pycompat.ossep
303 303 if cwd.startswith(rootsep):
304 304 return cwd[len(rootsep) :]
305 305 else:
306 306 # we're outside the repo. return an absolute path.
307 307 return cwd
308 308
309 309 def pathto(self, f, cwd=None):
310 310 if cwd is None:
311 311 cwd = self.getcwd()
312 312 path = util.pathto(self._root, cwd, f)
313 313 if self._slash:
314 314 return util.pconvert(path)
315 315 return path
316 316
317 317 def __getitem__(self, key):
318 318 """Return the current state of key (a filename) in the dirstate.
319 319
320 320 States are:
321 321 n normal
322 322 m needs merging
323 323 r marked for removal
324 324 a marked for addition
325 325 ? not tracked
326 326
327 327 XXX The "state" is a bit obscure to be in the "public" API. we should
328 328 consider migrating all user of this to going through the dirstate entry
329 329 instead.
330 330 """
331 331 entry = self._map.get(key)
332 332 if entry is not None:
333 333 return entry.state
334 334 return b'?'
335 335
336 336 def __contains__(self, key):
337 337 return key in self._map
338 338
339 339 def __iter__(self):
340 340 return iter(sorted(self._map))
341 341
342 342 def items(self):
343 343 return pycompat.iteritems(self._map)
344 344
345 345 iteritems = items
346 346
347 347 def directories(self):
348 348 return self._map.directories()
349 349
350 350 def parents(self):
351 351 return [self._validate(p) for p in self._pl]
352 352
353 353 def p1(self):
354 354 return self._validate(self._pl[0])
355 355
356 356 def p2(self):
357 357 return self._validate(self._pl[1])
358 358
359 359 @property
360 360 def in_merge(self):
361 361 """True if a merge is in progress"""
362 362 return self._pl[1] != self._nodeconstants.nullid
363 363
364 364 def branch(self):
365 365 return encoding.tolocal(self._branch)
366 366
367 367 def setparents(self, p1, p2=None):
368 368 """Set dirstate parents to p1 and p2.
369 369
370 370 When moving from two parents to one, "merged" entries a
371 371 adjusted to normal and previous copy records discarded and
372 372 returned by the call.
373 373
374 374 See localrepo.setparents()
375 375 """
376 376 if p2 is None:
377 377 p2 = self._nodeconstants.nullid
378 378 if self._parentwriters == 0:
379 379 raise ValueError(
380 380 b"cannot set dirstate parent outside of "
381 381 b"dirstate.parentchange context manager"
382 382 )
383 383
384 384 self._dirty = True
385 385 oldp2 = self._pl[1]
386 386 if self._origpl is None:
387 387 self._origpl = self._pl
388 388 self._map.setparents(p1, p2)
389 389 copies = {}
390 390 nullid = self._nodeconstants.nullid
391 391 if oldp2 != nullid and p2 == nullid:
392 392 candidatefiles = self._map.non_normal_or_other_parent_paths()
393 393
394 394 for f in candidatefiles:
395 395 s = self._map.get(f)
396 396 if s is None:
397 397 continue
398 398
399 399 # Discard "merged" markers when moving away from a merge state
400 400 if s.merged:
401 401 source = self._map.copymap.get(f)
402 402 if source:
403 403 copies[f] = source
404 404 self._normallookup(f)
405 405 # Also fix up otherparent markers
406 406 elif s.from_p2:
407 407 source = self._map.copymap.get(f)
408 408 if source:
409 409 copies[f] = source
410 410 self._check_new_tracked_filename(f)
411 411 self._updatedfiles.add(f)
412 412 self._map.reset_state(
413 413 f,
414 414 p1_tracked=False,
415 415 wc_tracked=True,
416 416 )
417 417 return copies
418 418
419 419 def setbranch(self, branch):
420 420 self.__class__._branch.set(self, encoding.fromlocal(branch))
421 421 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
422 422 try:
423 423 f.write(self._branch + b'\n')
424 424 f.close()
425 425
426 426 # make sure filecache has the correct stat info for _branch after
427 427 # replacing the underlying file
428 428 ce = self._filecache[b'_branch']
429 429 if ce:
430 430 ce.refresh()
431 431 except: # re-raises
432 432 f.discard()
433 433 raise
434 434
435 435 def invalidate(self):
436 436 """Causes the next access to reread the dirstate.
437 437
438 438 This is different from localrepo.invalidatedirstate() because it always
439 439 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
440 440 check whether the dirstate has changed before rereading it."""
441 441
442 442 for a in ("_map", "_branch", "_ignore"):
443 443 if a in self.__dict__:
444 444 delattr(self, a)
445 445 self._lastnormaltime = 0
446 446 self._dirty = False
447 447 self._updatedfiles.clear()
448 448 self._parentwriters = 0
449 449 self._origpl = None
450 450
451 451 def copy(self, source, dest):
452 452 """Mark dest as a copy of source. Unmark dest if source is None."""
453 453 if source == dest:
454 454 return
455 455 self._dirty = True
456 456 if source is not None:
457 457 self._map.copymap[dest] = source
458 458 self._updatedfiles.add(source)
459 459 self._updatedfiles.add(dest)
460 460 elif self._map.copymap.pop(dest, None):
461 461 self._updatedfiles.add(dest)
462 462
463 463 def copied(self, file):
464 464 return self._map.copymap.get(file, None)
465 465
466 466 def copies(self):
467 467 return self._map.copymap
468 468
469 469 @requires_no_parents_change
470 470 def set_tracked(self, filename):
471 471 """a "public" method for generic code to mark a file as tracked
472 472
473 473 This function is to be called outside of "update/merge" case. For
474 474 example by a command like `hg add X`.
475 475
476 476 return True the file was previously untracked, False otherwise.
477 477 """
478 478 self._dirty = True
479 479 self._updatedfiles.add(filename)
480 480 entry = self._map.get(filename)
481 if entry is None:
481 if entry is None or not entry.tracked:
482 482 self._check_new_tracked_filename(filename)
483 self._map.addfile(filename, added=True)
484 return True
485 elif not entry.tracked:
486 self._normallookup(filename)
487 return True
488 # XXX This is probably overkill for more case, but we need this to
489 # fully replace the `normallookup` call with `set_tracked` one.
490 # Consider smoothing this in the future.
491 self.set_possibly_dirty(filename)
492 return False
483 return self._map.set_tracked(filename)
493 484
494 485 @requires_no_parents_change
495 486 def set_untracked(self, filename):
496 487 """a "public" method for generic code to mark a file as untracked
497 488
498 489 This function is to be called outside of "update/merge" case. For
499 490 example by a command like `hg remove X`.
500 491
501 492 return True the file was previously tracked, False otherwise.
502 493 """
503 494 ret = self._map.set_untracked(filename)
504 495 if ret:
505 496 self._dirty = True
506 497 self._updatedfiles.add(filename)
507 498 return ret
508 499
509 500 @requires_no_parents_change
510 501 def set_clean(self, filename, parentfiledata=None):
511 502 """record that the current state of the file on disk is known to be clean"""
512 503 self._dirty = True
513 504 self._updatedfiles.add(filename)
514 505 if parentfiledata:
515 506 (mode, size, mtime) = parentfiledata
516 507 else:
517 508 (mode, size, mtime) = self._get_filedata(filename)
518 509 if not self._map[filename].tracked:
519 510 self._check_new_tracked_filename(filename)
520 511 self._map.set_clean(filename, mode, size, mtime)
521 512 if mtime > self._lastnormaltime:
522 513 # Remember the most recent modification timeslot for status(),
523 514 # to make sure we won't miss future size-preserving file content
524 515 # modifications that happen within the same timeslot.
525 516 self._lastnormaltime = mtime
526 517
527 518 @requires_no_parents_change
528 519 def set_possibly_dirty(self, filename):
529 520 """record that the current state of the file on disk is unknown"""
530 521 self._dirty = True
531 522 self._updatedfiles.add(filename)
532 523 self._map.set_possibly_dirty(filename)
533 524
534 525 @requires_parents_change
535 526 def update_file_p1(
536 527 self,
537 528 filename,
538 529 p1_tracked,
539 530 ):
540 531 """Set a file as tracked in the parent (or not)
541 532
542 533 This is to be called when adjust the dirstate to a new parent after an history
543 534 rewriting operation.
544 535
545 536 It should not be called during a merge (p2 != nullid) and only within
546 537 a `with dirstate.parentchange():` context.
547 538 """
548 539 if self.in_merge:
549 540 msg = b'update_file_reference should not be called when merging'
550 541 raise error.ProgrammingError(msg)
551 542 entry = self._map.get(filename)
552 543 if entry is None:
553 544 wc_tracked = False
554 545 else:
555 546 wc_tracked = entry.tracked
556 547 possibly_dirty = False
557 548 if p1_tracked and wc_tracked:
558 549 # the underlying reference might have changed, we will have to
559 550 # check it.
560 551 possibly_dirty = True
561 552 elif not (p1_tracked or wc_tracked):
562 553 # the file is no longer relevant to anyone
563 554 if self._map.dropfile(filename):
564 555 self._dirty = True
565 556 self._updatedfiles.add(filename)
566 557 elif (not p1_tracked) and wc_tracked:
567 558 if entry is not None and entry.added:
568 559 return # avoid dropping copy information (maybe?)
569 560 elif p1_tracked and not wc_tracked:
570 561 pass
571 562 else:
572 563 assert False, 'unreachable'
573 564
574 565 # this mean we are doing call for file we do not really care about the
575 566 # data (eg: added or removed), however this should be a minor overhead
576 567 # compared to the overall update process calling this.
577 568 parentfiledata = None
578 569 if wc_tracked:
579 570 parentfiledata = self._get_filedata(filename)
580 571
581 572 self._updatedfiles.add(filename)
582 573 self._map.reset_state(
583 574 filename,
584 575 wc_tracked,
585 576 p1_tracked,
586 577 possibly_dirty=possibly_dirty,
587 578 parentfiledata=parentfiledata,
588 579 )
589 580 if (
590 581 parentfiledata is not None
591 582 and parentfiledata[2] > self._lastnormaltime
592 583 ):
593 584 # Remember the most recent modification timeslot for status(),
594 585 # to make sure we won't miss future size-preserving file content
595 586 # modifications that happen within the same timeslot.
596 587 self._lastnormaltime = parentfiledata[2]
597 588
598 589 @requires_parents_change
599 590 def update_file(
600 591 self,
601 592 filename,
602 593 wc_tracked,
603 594 p1_tracked,
604 595 p2_tracked=False,
605 596 merged=False,
606 597 clean_p1=False,
607 598 clean_p2=False,
608 599 possibly_dirty=False,
609 600 parentfiledata=None,
610 601 ):
611 602 """update the information about a file in the dirstate
612 603
613 604 This is to be called when the direstates parent changes to keep track
614 605 of what is the file situation in regards to the working copy and its parent.
615 606
616 607 This function must be called within a `dirstate.parentchange` context.
617 608
618 609 note: the API is at an early stage and we might need to adjust it
619 610 depending of what information ends up being relevant and useful to
620 611 other processing.
621 612 """
622 613 if merged and (clean_p1 or clean_p2):
623 614 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
624 615 raise error.ProgrammingError(msg)
625 616
626 617 # note: I do not think we need to double check name clash here since we
627 618 # are in a update/merge case that should already have taken care of
628 619 # this. The test agrees
629 620
630 621 self._dirty = True
631 622 self._updatedfiles.add(filename)
632 623
633 624 need_parent_file_data = (
634 625 not (possibly_dirty or clean_p2 or merged)
635 626 and wc_tracked
636 627 and p1_tracked
637 628 )
638 629
639 630 # this mean we are doing call for file we do not really care about the
640 631 # data (eg: added or removed), however this should be a minor overhead
641 632 # compared to the overall update process calling this.
642 633 if need_parent_file_data:
643 634 if parentfiledata is None:
644 635 parentfiledata = self._get_filedata(filename)
645 636 mtime = parentfiledata[2]
646 637
647 638 if mtime > self._lastnormaltime:
648 639 # Remember the most recent modification timeslot for
649 640 # status(), to make sure we won't miss future
650 641 # size-preserving file content modifications that happen
651 642 # within the same timeslot.
652 643 self._lastnormaltime = mtime
653 644
654 645 self._map.reset_state(
655 646 filename,
656 647 wc_tracked,
657 648 p1_tracked,
658 649 p2_tracked=p2_tracked,
659 650 merged=merged,
660 651 clean_p1=clean_p1,
661 652 clean_p2=clean_p2,
662 653 possibly_dirty=possibly_dirty,
663 654 parentfiledata=parentfiledata,
664 655 )
665 656 if (
666 657 parentfiledata is not None
667 658 and parentfiledata[2] > self._lastnormaltime
668 659 ):
669 660 # Remember the most recent modification timeslot for status(),
670 661 # to make sure we won't miss future size-preserving file content
671 662 # modifications that happen within the same timeslot.
672 663 self._lastnormaltime = parentfiledata[2]
673 664
674 665 def _addpath(
675 666 self,
676 667 f,
677 668 mode=0,
678 669 size=None,
679 670 mtime=None,
680 671 added=False,
681 672 merged=False,
682 673 from_p2=False,
683 674 possibly_dirty=False,
684 675 ):
685 676 entry = self._map.get(f)
686 677 if added or entry is not None and not entry.tracked:
687 678 self._check_new_tracked_filename(f)
688 679 self._dirty = True
689 680 self._updatedfiles.add(f)
690 681 self._map.addfile(
691 682 f,
692 683 mode=mode,
693 684 size=size,
694 685 mtime=mtime,
695 686 added=added,
696 687 merged=merged,
697 688 from_p2=from_p2,
698 689 possibly_dirty=possibly_dirty,
699 690 )
700 691
701 692 def _check_new_tracked_filename(self, filename):
702 693 scmutil.checkfilename(filename)
703 694 if self._map.hastrackeddir(filename):
704 695 msg = _(b'directory %r already in dirstate')
705 696 msg %= pycompat.bytestr(filename)
706 697 raise error.Abort(msg)
707 698 # shadows
708 699 for d in pathutil.finddirs(filename):
709 700 if self._map.hastrackeddir(d):
710 701 break
711 702 entry = self._map.get(d)
712 703 if entry is not None and not entry.removed:
713 704 msg = _(b'file %r in dirstate clashes with %r')
714 705 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
715 706 raise error.Abort(msg)
716 707
717 708 def _get_filedata(self, filename):
718 709 """returns"""
719 710 s = os.lstat(self._join(filename))
720 711 mode = s.st_mode
721 712 size = s.st_size
722 713 mtime = s[stat.ST_MTIME]
723 714 return (mode, size, mtime)
724 715
725 716 def _normallookup(self, f):
726 717 '''Mark a file normal, but possibly dirty.'''
727 718 if self.in_merge:
728 719 # if there is a merge going on and the file was either
729 720 # "merged" or coming from other parent (-2) before
730 721 # being removed, restore that state.
731 722 entry = self._map.get(f)
732 723 if entry is not None:
733 724 # XXX this should probably be dealt with a a lower level
734 725 # (see `merged_removed` and `from_p2_removed`)
735 726 if entry.merged_removed or entry.from_p2_removed:
736 727 source = self._map.copymap.get(f)
737 728 if entry.merged_removed:
738 729 self._addpath(f, merged=True)
739 730 else:
740 731 self._addpath(f, from_p2=True)
741 732 self._map.copymap.pop(f, None)
742 733 if source is not None:
743 734 self.copy(source, f)
744 735 return
745 736 elif entry.merged or entry.from_p2:
746 737 return
747 738 self._addpath(f, possibly_dirty=True)
748 739 self._map.copymap.pop(f, None)
749 740
750 741 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
751 742 if exists is None:
752 743 exists = os.path.lexists(os.path.join(self._root, path))
753 744 if not exists:
754 745 # Maybe a path component exists
755 746 if not ignoremissing and b'/' in path:
756 747 d, f = path.rsplit(b'/', 1)
757 748 d = self._normalize(d, False, ignoremissing, None)
758 749 folded = d + b"/" + f
759 750 else:
760 751 # No path components, preserve original case
761 752 folded = path
762 753 else:
763 754 # recursively normalize leading directory components
764 755 # against dirstate
765 756 if b'/' in normed:
766 757 d, f = normed.rsplit(b'/', 1)
767 758 d = self._normalize(d, False, ignoremissing, True)
768 759 r = self._root + b"/" + d
769 760 folded = d + b"/" + util.fspath(f, r)
770 761 else:
771 762 folded = util.fspath(normed, self._root)
772 763 storemap[normed] = folded
773 764
774 765 return folded
775 766
776 767 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
777 768 normed = util.normcase(path)
778 769 folded = self._map.filefoldmap.get(normed, None)
779 770 if folded is None:
780 771 if isknown:
781 772 folded = path
782 773 else:
783 774 folded = self._discoverpath(
784 775 path, normed, ignoremissing, exists, self._map.filefoldmap
785 776 )
786 777 return folded
787 778
788 779 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
789 780 normed = util.normcase(path)
790 781 folded = self._map.filefoldmap.get(normed, None)
791 782 if folded is None:
792 783 folded = self._map.dirfoldmap.get(normed, None)
793 784 if folded is None:
794 785 if isknown:
795 786 folded = path
796 787 else:
797 788 # store discovered result in dirfoldmap so that future
798 789 # normalizefile calls don't start matching directories
799 790 folded = self._discoverpath(
800 791 path, normed, ignoremissing, exists, self._map.dirfoldmap
801 792 )
802 793 return folded
803 794
804 795 def normalize(self, path, isknown=False, ignoremissing=False):
805 796 """
806 797 normalize the case of a pathname when on a casefolding filesystem
807 798
808 799 isknown specifies whether the filename came from walking the
809 800 disk, to avoid extra filesystem access.
810 801
811 802 If ignoremissing is True, missing path are returned
812 803 unchanged. Otherwise, we try harder to normalize possibly
813 804 existing path components.
814 805
815 806 The normalized case is determined based on the following precedence:
816 807
817 808 - version of name already stored in the dirstate
818 809 - version of name stored on disk
819 810 - version provided via command arguments
820 811 """
821 812
822 813 if self._checkcase:
823 814 return self._normalize(path, isknown, ignoremissing)
824 815 return path
825 816
826 817 def clear(self):
827 818 self._map.clear()
828 819 self._lastnormaltime = 0
829 820 self._updatedfiles.clear()
830 821 self._dirty = True
831 822
832 823 def rebuild(self, parent, allfiles, changedfiles=None):
833 824 if changedfiles is None:
834 825 # Rebuild entire dirstate
835 826 to_lookup = allfiles
836 827 to_drop = []
837 828 lastnormaltime = self._lastnormaltime
838 829 self.clear()
839 830 self._lastnormaltime = lastnormaltime
840 831 elif len(changedfiles) < 10:
841 832 # Avoid turning allfiles into a set, which can be expensive if it's
842 833 # large.
843 834 to_lookup = []
844 835 to_drop = []
845 836 for f in changedfiles:
846 837 if f in allfiles:
847 838 to_lookup.append(f)
848 839 else:
849 840 to_drop.append(f)
850 841 else:
851 842 changedfilesset = set(changedfiles)
852 843 to_lookup = changedfilesset & set(allfiles)
853 844 to_drop = changedfilesset - to_lookup
854 845
855 846 if self._origpl is None:
856 847 self._origpl = self._pl
857 848 self._map.setparents(parent, self._nodeconstants.nullid)
858 849
859 850 for f in to_lookup:
860 851 self._normallookup(f)
861 852 for f in to_drop:
862 853 if self._map.dropfile(f):
863 854 self._updatedfiles.add(f)
864 855
865 856 self._dirty = True
866 857
867 858 def identity(self):
868 859 """Return identity of dirstate itself to detect changing in storage
869 860
870 861 If identity of previous dirstate is equal to this, writing
871 862 changes based on the former dirstate out can keep consistency.
872 863 """
873 864 return self._map.identity
874 865
875 866 def write(self, tr):
876 867 if not self._dirty:
877 868 return
878 869
879 870 filename = self._filename
880 871 if tr:
881 872 # 'dirstate.write()' is not only for writing in-memory
882 873 # changes out, but also for dropping ambiguous timestamp.
883 874 # delayed writing re-raise "ambiguous timestamp issue".
884 875 # See also the wiki page below for detail:
885 876 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
886 877
887 878 # emulate dropping timestamp in 'parsers.pack_dirstate'
888 879 now = _getfsnow(self._opener)
889 880 self._map.clearambiguoustimes(self._updatedfiles, now)
890 881
891 882 # emulate that all 'dirstate.normal' results are written out
892 883 self._lastnormaltime = 0
893 884 self._updatedfiles.clear()
894 885
895 886 # delay writing in-memory changes out
896 887 tr.addfilegenerator(
897 888 b'dirstate',
898 889 (self._filename,),
899 890 lambda f: self._writedirstate(tr, f),
900 891 location=b'plain',
901 892 )
902 893 return
903 894
904 895 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
905 896 self._writedirstate(tr, st)
906 897
907 898 def addparentchangecallback(self, category, callback):
908 899 """add a callback to be called when the wd parents are changed
909 900
910 901 Callback will be called with the following arguments:
911 902 dirstate, (oldp1, oldp2), (newp1, newp2)
912 903
913 904 Category is a unique identifier to allow overwriting an old callback
914 905 with a newer callback.
915 906 """
916 907 self._plchangecallbacks[category] = callback
917 908
918 909 def _writedirstate(self, tr, st):
919 910 # notify callbacks about parents change
920 911 if self._origpl is not None and self._origpl != self._pl:
921 912 for c, callback in sorted(
922 913 pycompat.iteritems(self._plchangecallbacks)
923 914 ):
924 915 callback(self, self._origpl, self._pl)
925 916 self._origpl = None
926 917 # use the modification time of the newly created temporary file as the
927 918 # filesystem's notion of 'now'
928 919 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
929 920
930 921 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
931 922 # timestamp of each entries in dirstate, because of 'now > mtime'
932 923 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
933 924 if delaywrite > 0:
934 925 # do we have any files to delay for?
935 926 for f, e in pycompat.iteritems(self._map):
936 927 if e.need_delay(now):
937 928 import time # to avoid useless import
938 929
939 930 # rather than sleep n seconds, sleep until the next
940 931 # multiple of n seconds
941 932 clock = time.time()
942 933 start = int(clock) - (int(clock) % delaywrite)
943 934 end = start + delaywrite
944 935 time.sleep(end - clock)
945 936 now = end # trust our estimate that the end is near now
946 937 break
947 938
948 939 self._map.write(tr, st, now)
949 940 self._lastnormaltime = 0
950 941 self._dirty = False
951 942
952 943 def _dirignore(self, f):
953 944 if self._ignore(f):
954 945 return True
955 946 for p in pathutil.finddirs(f):
956 947 if self._ignore(p):
957 948 return True
958 949 return False
959 950
960 951 def _ignorefiles(self):
961 952 files = []
962 953 if os.path.exists(self._join(b'.hgignore')):
963 954 files.append(self._join(b'.hgignore'))
964 955 for name, path in self._ui.configitems(b"ui"):
965 956 if name == b'ignore' or name.startswith(b'ignore.'):
966 957 # we need to use os.path.join here rather than self._join
967 958 # because path is arbitrary and user-specified
968 959 files.append(os.path.join(self._rootdir, util.expandpath(path)))
969 960 return files
970 961
971 962 def _ignorefileandline(self, f):
972 963 files = collections.deque(self._ignorefiles())
973 964 visited = set()
974 965 while files:
975 966 i = files.popleft()
976 967 patterns = matchmod.readpatternfile(
977 968 i, self._ui.warn, sourceinfo=True
978 969 )
979 970 for pattern, lineno, line in patterns:
980 971 kind, p = matchmod._patsplit(pattern, b'glob')
981 972 if kind == b"subinclude":
982 973 if p not in visited:
983 974 files.append(p)
984 975 continue
985 976 m = matchmod.match(
986 977 self._root, b'', [], [pattern], warn=self._ui.warn
987 978 )
988 979 if m(f):
989 980 return (i, lineno, line)
990 981 visited.add(i)
991 982 return (None, -1, b"")
992 983
993 984 def _walkexplicit(self, match, subrepos):
994 985 """Get stat data about the files explicitly specified by match.
995 986
996 987 Return a triple (results, dirsfound, dirsnotfound).
997 988 - results is a mapping from filename to stat result. It also contains
998 989 listings mapping subrepos and .hg to None.
999 990 - dirsfound is a list of files found to be directories.
1000 991 - dirsnotfound is a list of files that the dirstate thinks are
1001 992 directories and that were not found."""
1002 993
1003 994 def badtype(mode):
1004 995 kind = _(b'unknown')
1005 996 if stat.S_ISCHR(mode):
1006 997 kind = _(b'character device')
1007 998 elif stat.S_ISBLK(mode):
1008 999 kind = _(b'block device')
1009 1000 elif stat.S_ISFIFO(mode):
1010 1001 kind = _(b'fifo')
1011 1002 elif stat.S_ISSOCK(mode):
1012 1003 kind = _(b'socket')
1013 1004 elif stat.S_ISDIR(mode):
1014 1005 kind = _(b'directory')
1015 1006 return _(b'unsupported file type (type is %s)') % kind
1016 1007
1017 1008 badfn = match.bad
1018 1009 dmap = self._map
1019 1010 lstat = os.lstat
1020 1011 getkind = stat.S_IFMT
1021 1012 dirkind = stat.S_IFDIR
1022 1013 regkind = stat.S_IFREG
1023 1014 lnkkind = stat.S_IFLNK
1024 1015 join = self._join
1025 1016 dirsfound = []
1026 1017 foundadd = dirsfound.append
1027 1018 dirsnotfound = []
1028 1019 notfoundadd = dirsnotfound.append
1029 1020
1030 1021 if not match.isexact() and self._checkcase:
1031 1022 normalize = self._normalize
1032 1023 else:
1033 1024 normalize = None
1034 1025
1035 1026 files = sorted(match.files())
1036 1027 subrepos.sort()
1037 1028 i, j = 0, 0
1038 1029 while i < len(files) and j < len(subrepos):
1039 1030 subpath = subrepos[j] + b"/"
1040 1031 if files[i] < subpath:
1041 1032 i += 1
1042 1033 continue
1043 1034 while i < len(files) and files[i].startswith(subpath):
1044 1035 del files[i]
1045 1036 j += 1
1046 1037
1047 1038 if not files or b'' in files:
1048 1039 files = [b'']
1049 1040 # constructing the foldmap is expensive, so don't do it for the
1050 1041 # common case where files is ['']
1051 1042 normalize = None
1052 1043 results = dict.fromkeys(subrepos)
1053 1044 results[b'.hg'] = None
1054 1045
1055 1046 for ff in files:
1056 1047 if normalize:
1057 1048 nf = normalize(ff, False, True)
1058 1049 else:
1059 1050 nf = ff
1060 1051 if nf in results:
1061 1052 continue
1062 1053
1063 1054 try:
1064 1055 st = lstat(join(nf))
1065 1056 kind = getkind(st.st_mode)
1066 1057 if kind == dirkind:
1067 1058 if nf in dmap:
1068 1059 # file replaced by dir on disk but still in dirstate
1069 1060 results[nf] = None
1070 1061 foundadd((nf, ff))
1071 1062 elif kind == regkind or kind == lnkkind:
1072 1063 results[nf] = st
1073 1064 else:
1074 1065 badfn(ff, badtype(kind))
1075 1066 if nf in dmap:
1076 1067 results[nf] = None
1077 1068 except OSError as inst: # nf not found on disk - it is dirstate only
1078 1069 if nf in dmap: # does it exactly match a missing file?
1079 1070 results[nf] = None
1080 1071 else: # does it match a missing directory?
1081 1072 if self._map.hasdir(nf):
1082 1073 notfoundadd(nf)
1083 1074 else:
1084 1075 badfn(ff, encoding.strtolocal(inst.strerror))
1085 1076
1086 1077 # match.files() may contain explicitly-specified paths that shouldn't
1087 1078 # be taken; drop them from the list of files found. dirsfound/notfound
1088 1079 # aren't filtered here because they will be tested later.
1089 1080 if match.anypats():
1090 1081 for f in list(results):
1091 1082 if f == b'.hg' or f in subrepos:
1092 1083 # keep sentinel to disable further out-of-repo walks
1093 1084 continue
1094 1085 if not match(f):
1095 1086 del results[f]
1096 1087
1097 1088 # Case insensitive filesystems cannot rely on lstat() failing to detect
1098 1089 # a case-only rename. Prune the stat object for any file that does not
1099 1090 # match the case in the filesystem, if there are multiple files that
1100 1091 # normalize to the same path.
1101 1092 if match.isexact() and self._checkcase:
1102 1093 normed = {}
1103 1094
1104 1095 for f, st in pycompat.iteritems(results):
1105 1096 if st is None:
1106 1097 continue
1107 1098
1108 1099 nc = util.normcase(f)
1109 1100 paths = normed.get(nc)
1110 1101
1111 1102 if paths is None:
1112 1103 paths = set()
1113 1104 normed[nc] = paths
1114 1105
1115 1106 paths.add(f)
1116 1107
1117 1108 for norm, paths in pycompat.iteritems(normed):
1118 1109 if len(paths) > 1:
1119 1110 for path in paths:
1120 1111 folded = self._discoverpath(
1121 1112 path, norm, True, None, self._map.dirfoldmap
1122 1113 )
1123 1114 if path != folded:
1124 1115 results[path] = None
1125 1116
1126 1117 return results, dirsfound, dirsnotfound
1127 1118
1128 1119 def walk(self, match, subrepos, unknown, ignored, full=True):
1129 1120 """
1130 1121 Walk recursively through the directory tree, finding all files
1131 1122 matched by match.
1132 1123
1133 1124 If full is False, maybe skip some known-clean files.
1134 1125
1135 1126 Return a dict mapping filename to stat-like object (either
1136 1127 mercurial.osutil.stat instance or return value of os.stat()).
1137 1128
1138 1129 """
1139 1130 # full is a flag that extensions that hook into walk can use -- this
1140 1131 # implementation doesn't use it at all. This satisfies the contract
1141 1132 # because we only guarantee a "maybe".
1142 1133
1143 1134 if ignored:
1144 1135 ignore = util.never
1145 1136 dirignore = util.never
1146 1137 elif unknown:
1147 1138 ignore = self._ignore
1148 1139 dirignore = self._dirignore
1149 1140 else:
1150 1141 # if not unknown and not ignored, drop dir recursion and step 2
1151 1142 ignore = util.always
1152 1143 dirignore = util.always
1153 1144
1154 1145 matchfn = match.matchfn
1155 1146 matchalways = match.always()
1156 1147 matchtdir = match.traversedir
1157 1148 dmap = self._map
1158 1149 listdir = util.listdir
1159 1150 lstat = os.lstat
1160 1151 dirkind = stat.S_IFDIR
1161 1152 regkind = stat.S_IFREG
1162 1153 lnkkind = stat.S_IFLNK
1163 1154 join = self._join
1164 1155
1165 1156 exact = skipstep3 = False
1166 1157 if match.isexact(): # match.exact
1167 1158 exact = True
1168 1159 dirignore = util.always # skip step 2
1169 1160 elif match.prefix(): # match.match, no patterns
1170 1161 skipstep3 = True
1171 1162
1172 1163 if not exact and self._checkcase:
1173 1164 normalize = self._normalize
1174 1165 normalizefile = self._normalizefile
1175 1166 skipstep3 = False
1176 1167 else:
1177 1168 normalize = self._normalize
1178 1169 normalizefile = None
1179 1170
1180 1171 # step 1: find all explicit files
1181 1172 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1182 1173 if matchtdir:
1183 1174 for d in work:
1184 1175 matchtdir(d[0])
1185 1176 for d in dirsnotfound:
1186 1177 matchtdir(d)
1187 1178
1188 1179 skipstep3 = skipstep3 and not (work or dirsnotfound)
1189 1180 work = [d for d in work if not dirignore(d[0])]
1190 1181
1191 1182 # step 2: visit subdirectories
1192 1183 def traverse(work, alreadynormed):
1193 1184 wadd = work.append
1194 1185 while work:
1195 1186 tracing.counter('dirstate.walk work', len(work))
1196 1187 nd = work.pop()
1197 1188 visitentries = match.visitchildrenset(nd)
1198 1189 if not visitentries:
1199 1190 continue
1200 1191 if visitentries == b'this' or visitentries == b'all':
1201 1192 visitentries = None
1202 1193 skip = None
1203 1194 if nd != b'':
1204 1195 skip = b'.hg'
1205 1196 try:
1206 1197 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1207 1198 entries = listdir(join(nd), stat=True, skip=skip)
1208 1199 except OSError as inst:
1209 1200 if inst.errno in (errno.EACCES, errno.ENOENT):
1210 1201 match.bad(
1211 1202 self.pathto(nd), encoding.strtolocal(inst.strerror)
1212 1203 )
1213 1204 continue
1214 1205 raise
1215 1206 for f, kind, st in entries:
1216 1207 # Some matchers may return files in the visitentries set,
1217 1208 # instead of 'this', if the matcher explicitly mentions them
1218 1209 # and is not an exactmatcher. This is acceptable; we do not
1219 1210 # make any hard assumptions about file-or-directory below
1220 1211 # based on the presence of `f` in visitentries. If
1221 1212 # visitchildrenset returned a set, we can always skip the
1222 1213 # entries *not* in the set it provided regardless of whether
1223 1214 # they're actually a file or a directory.
1224 1215 if visitentries and f not in visitentries:
1225 1216 continue
1226 1217 if normalizefile:
1227 1218 # even though f might be a directory, we're only
1228 1219 # interested in comparing it to files currently in the
1229 1220 # dmap -- therefore normalizefile is enough
1230 1221 nf = normalizefile(
1231 1222 nd and (nd + b"/" + f) or f, True, True
1232 1223 )
1233 1224 else:
1234 1225 nf = nd and (nd + b"/" + f) or f
1235 1226 if nf not in results:
1236 1227 if kind == dirkind:
1237 1228 if not ignore(nf):
1238 1229 if matchtdir:
1239 1230 matchtdir(nf)
1240 1231 wadd(nf)
1241 1232 if nf in dmap and (matchalways or matchfn(nf)):
1242 1233 results[nf] = None
1243 1234 elif kind == regkind or kind == lnkkind:
1244 1235 if nf in dmap:
1245 1236 if matchalways or matchfn(nf):
1246 1237 results[nf] = st
1247 1238 elif (matchalways or matchfn(nf)) and not ignore(
1248 1239 nf
1249 1240 ):
1250 1241 # unknown file -- normalize if necessary
1251 1242 if not alreadynormed:
1252 1243 nf = normalize(nf, False, True)
1253 1244 results[nf] = st
1254 1245 elif nf in dmap and (matchalways or matchfn(nf)):
1255 1246 results[nf] = None
1256 1247
1257 1248 for nd, d in work:
1258 1249 # alreadynormed means that processwork doesn't have to do any
1259 1250 # expensive directory normalization
1260 1251 alreadynormed = not normalize or nd == d
1261 1252 traverse([d], alreadynormed)
1262 1253
1263 1254 for s in subrepos:
1264 1255 del results[s]
1265 1256 del results[b'.hg']
1266 1257
1267 1258 # step 3: visit remaining files from dmap
1268 1259 if not skipstep3 and not exact:
1269 1260 # If a dmap file is not in results yet, it was either
1270 1261 # a) not matching matchfn b) ignored, c) missing, or d) under a
1271 1262 # symlink directory.
1272 1263 if not results and matchalways:
1273 1264 visit = [f for f in dmap]
1274 1265 else:
1275 1266 visit = [f for f in dmap if f not in results and matchfn(f)]
1276 1267 visit.sort()
1277 1268
1278 1269 if unknown:
1279 1270 # unknown == True means we walked all dirs under the roots
1280 1271 # that wasn't ignored, and everything that matched was stat'ed
1281 1272 # and is already in results.
1282 1273 # The rest must thus be ignored or under a symlink.
1283 1274 audit_path = pathutil.pathauditor(self._root, cached=True)
1284 1275
1285 1276 for nf in iter(visit):
1286 1277 # If a stat for the same file was already added with a
1287 1278 # different case, don't add one for this, since that would
1288 1279 # make it appear as if the file exists under both names
1289 1280 # on disk.
1290 1281 if (
1291 1282 normalizefile
1292 1283 and normalizefile(nf, True, True) in results
1293 1284 ):
1294 1285 results[nf] = None
1295 1286 # Report ignored items in the dmap as long as they are not
1296 1287 # under a symlink directory.
1297 1288 elif audit_path.check(nf):
1298 1289 try:
1299 1290 results[nf] = lstat(join(nf))
1300 1291 # file was just ignored, no links, and exists
1301 1292 except OSError:
1302 1293 # file doesn't exist
1303 1294 results[nf] = None
1304 1295 else:
1305 1296 # It's either missing or under a symlink directory
1306 1297 # which we in this case report as missing
1307 1298 results[nf] = None
1308 1299 else:
1309 1300 # We may not have walked the full directory tree above,
1310 1301 # so stat and check everything we missed.
1311 1302 iv = iter(visit)
1312 1303 for st in util.statfiles([join(i) for i in visit]):
1313 1304 results[next(iv)] = st
1314 1305 return results
1315 1306
1316 1307 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1317 1308 # Force Rayon (Rust parallelism library) to respect the number of
1318 1309 # workers. This is a temporary workaround until Rust code knows
1319 1310 # how to read the config file.
1320 1311 numcpus = self._ui.configint(b"worker", b"numcpus")
1321 1312 if numcpus is not None:
1322 1313 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1323 1314
1324 1315 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1325 1316 if not workers_enabled:
1326 1317 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1327 1318
1328 1319 (
1329 1320 lookup,
1330 1321 modified,
1331 1322 added,
1332 1323 removed,
1333 1324 deleted,
1334 1325 clean,
1335 1326 ignored,
1336 1327 unknown,
1337 1328 warnings,
1338 1329 bad,
1339 1330 traversed,
1340 1331 dirty,
1341 1332 ) = rustmod.status(
1342 1333 self._map._rustmap,
1343 1334 matcher,
1344 1335 self._rootdir,
1345 1336 self._ignorefiles(),
1346 1337 self._checkexec,
1347 1338 self._lastnormaltime,
1348 1339 bool(list_clean),
1349 1340 bool(list_ignored),
1350 1341 bool(list_unknown),
1351 1342 bool(matcher.traversedir),
1352 1343 )
1353 1344
1354 1345 self._dirty |= dirty
1355 1346
1356 1347 if matcher.traversedir:
1357 1348 for dir in traversed:
1358 1349 matcher.traversedir(dir)
1359 1350
1360 1351 if self._ui.warn:
1361 1352 for item in warnings:
1362 1353 if isinstance(item, tuple):
1363 1354 file_path, syntax = item
1364 1355 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1365 1356 file_path,
1366 1357 syntax,
1367 1358 )
1368 1359 self._ui.warn(msg)
1369 1360 else:
1370 1361 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1371 1362 self._ui.warn(
1372 1363 msg
1373 1364 % (
1374 1365 pathutil.canonpath(
1375 1366 self._rootdir, self._rootdir, item
1376 1367 ),
1377 1368 b"No such file or directory",
1378 1369 )
1379 1370 )
1380 1371
1381 1372 for (fn, message) in bad:
1382 1373 matcher.bad(fn, encoding.strtolocal(message))
1383 1374
1384 1375 status = scmutil.status(
1385 1376 modified=modified,
1386 1377 added=added,
1387 1378 removed=removed,
1388 1379 deleted=deleted,
1389 1380 unknown=unknown,
1390 1381 ignored=ignored,
1391 1382 clean=clean,
1392 1383 )
1393 1384 return (lookup, status)
1394 1385
1395 1386 def status(self, match, subrepos, ignored, clean, unknown):
1396 1387 """Determine the status of the working copy relative to the
1397 1388 dirstate and return a pair of (unsure, status), where status is of type
1398 1389 scmutil.status and:
1399 1390
1400 1391 unsure:
1401 1392 files that might have been modified since the dirstate was
1402 1393 written, but need to be read to be sure (size is the same
1403 1394 but mtime differs)
1404 1395 status.modified:
1405 1396 files that have definitely been modified since the dirstate
1406 1397 was written (different size or mode)
1407 1398 status.clean:
1408 1399 files that have definitely not been modified since the
1409 1400 dirstate was written
1410 1401 """
1411 1402 listignored, listclean, listunknown = ignored, clean, unknown
1412 1403 lookup, modified, added, unknown, ignored = [], [], [], [], []
1413 1404 removed, deleted, clean = [], [], []
1414 1405
1415 1406 dmap = self._map
1416 1407 dmap.preload()
1417 1408
1418 1409 use_rust = True
1419 1410
1420 1411 allowed_matchers = (
1421 1412 matchmod.alwaysmatcher,
1422 1413 matchmod.exactmatcher,
1423 1414 matchmod.includematcher,
1424 1415 )
1425 1416
1426 1417 if rustmod is None:
1427 1418 use_rust = False
1428 1419 elif self._checkcase:
1429 1420 # Case-insensitive filesystems are not handled yet
1430 1421 use_rust = False
1431 1422 elif subrepos:
1432 1423 use_rust = False
1433 1424 elif sparse.enabled:
1434 1425 use_rust = False
1435 1426 elif not isinstance(match, allowed_matchers):
1436 1427 # Some matchers have yet to be implemented
1437 1428 use_rust = False
1438 1429
1439 1430 if use_rust:
1440 1431 try:
1441 1432 return self._rust_status(
1442 1433 match, listclean, listignored, listunknown
1443 1434 )
1444 1435 except rustmod.FallbackError:
1445 1436 pass
1446 1437
1447 1438 def noop(f):
1448 1439 pass
1449 1440
1450 1441 dcontains = dmap.__contains__
1451 1442 dget = dmap.__getitem__
1452 1443 ladd = lookup.append # aka "unsure"
1453 1444 madd = modified.append
1454 1445 aadd = added.append
1455 1446 uadd = unknown.append if listunknown else noop
1456 1447 iadd = ignored.append if listignored else noop
1457 1448 radd = removed.append
1458 1449 dadd = deleted.append
1459 1450 cadd = clean.append if listclean else noop
1460 1451 mexact = match.exact
1461 1452 dirignore = self._dirignore
1462 1453 checkexec = self._checkexec
1463 1454 copymap = self._map.copymap
1464 1455 lastnormaltime = self._lastnormaltime
1465 1456
1466 1457 # We need to do full walks when either
1467 1458 # - we're listing all clean files, or
1468 1459 # - match.traversedir does something, because match.traversedir should
1469 1460 # be called for every dir in the working dir
1470 1461 full = listclean or match.traversedir is not None
1471 1462 for fn, st in pycompat.iteritems(
1472 1463 self.walk(match, subrepos, listunknown, listignored, full=full)
1473 1464 ):
1474 1465 if not dcontains(fn):
1475 1466 if (listignored or mexact(fn)) and dirignore(fn):
1476 1467 if listignored:
1477 1468 iadd(fn)
1478 1469 else:
1479 1470 uadd(fn)
1480 1471 continue
1481 1472
1482 1473 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1483 1474 # written like that for performance reasons. dmap[fn] is not a
1484 1475 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1485 1476 # opcode has fast paths when the value to be unpacked is a tuple or
1486 1477 # a list, but falls back to creating a full-fledged iterator in
1487 1478 # general. That is much slower than simply accessing and storing the
1488 1479 # tuple members one by one.
1489 1480 t = dget(fn)
1490 1481 mode = t.mode
1491 1482 size = t.size
1492 1483 time = t.mtime
1493 1484
1494 1485 if not st and t.tracked:
1495 1486 dadd(fn)
1496 1487 elif t.merged:
1497 1488 madd(fn)
1498 1489 elif t.added:
1499 1490 aadd(fn)
1500 1491 elif t.removed:
1501 1492 radd(fn)
1502 1493 elif t.tracked:
1503 1494 if (
1504 1495 size >= 0
1505 1496 and (
1506 1497 (size != st.st_size and size != st.st_size & _rangemask)
1507 1498 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1508 1499 )
1509 1500 or t.from_p2
1510 1501 or fn in copymap
1511 1502 ):
1512 1503 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1513 1504 # issue6456: Size returned may be longer due to
1514 1505 # encryption on EXT-4 fscrypt, undecided.
1515 1506 ladd(fn)
1516 1507 else:
1517 1508 madd(fn)
1518 1509 elif (
1519 1510 time != st[stat.ST_MTIME]
1520 1511 and time != st[stat.ST_MTIME] & _rangemask
1521 1512 ):
1522 1513 ladd(fn)
1523 1514 elif st[stat.ST_MTIME] == lastnormaltime:
1524 1515 # fn may have just been marked as normal and it may have
1525 1516 # changed in the same second without changing its size.
1526 1517 # This can happen if we quickly do multiple commits.
1527 1518 # Force lookup, so we don't miss such a racy file change.
1528 1519 ladd(fn)
1529 1520 elif listclean:
1530 1521 cadd(fn)
1531 1522 status = scmutil.status(
1532 1523 modified, added, removed, deleted, unknown, ignored, clean
1533 1524 )
1534 1525 return (lookup, status)
1535 1526
1536 1527 def matches(self, match):
1537 1528 """
1538 1529 return files in the dirstate (in whatever state) filtered by match
1539 1530 """
1540 1531 dmap = self._map
1541 1532 if rustmod is not None:
1542 1533 dmap = self._map._rustmap
1543 1534
1544 1535 if match.always():
1545 1536 return dmap.keys()
1546 1537 files = match.files()
1547 1538 if match.isexact():
1548 1539 # fast path -- filter the other way around, since typically files is
1549 1540 # much smaller than dmap
1550 1541 return [f for f in files if f in dmap]
1551 1542 if match.prefix() and all(fn in dmap for fn in files):
1552 1543 # fast path -- all the values are known to be files, so just return
1553 1544 # that
1554 1545 return list(files)
1555 1546 return [f for f in dmap if match(f)]
1556 1547
1557 1548 def _actualfilename(self, tr):
1558 1549 if tr:
1559 1550 return self._pendingfilename
1560 1551 else:
1561 1552 return self._filename
1562 1553
1563 1554 def savebackup(self, tr, backupname):
1564 1555 '''Save current dirstate into backup file'''
1565 1556 filename = self._actualfilename(tr)
1566 1557 assert backupname != filename
1567 1558
1568 1559 # use '_writedirstate' instead of 'write' to write changes certainly,
1569 1560 # because the latter omits writing out if transaction is running.
1570 1561 # output file will be used to create backup of dirstate at this point.
1571 1562 if self._dirty or not self._opener.exists(filename):
1572 1563 self._writedirstate(
1573 1564 tr,
1574 1565 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1575 1566 )
1576 1567
1577 1568 if tr:
1578 1569 # ensure that subsequent tr.writepending returns True for
1579 1570 # changes written out above, even if dirstate is never
1580 1571 # changed after this
1581 1572 tr.addfilegenerator(
1582 1573 b'dirstate',
1583 1574 (self._filename,),
1584 1575 lambda f: self._writedirstate(tr, f),
1585 1576 location=b'plain',
1586 1577 )
1587 1578
1588 1579 # ensure that pending file written above is unlinked at
1589 1580 # failure, even if tr.writepending isn't invoked until the
1590 1581 # end of this transaction
1591 1582 tr.registertmp(filename, location=b'plain')
1592 1583
1593 1584 self._opener.tryunlink(backupname)
1594 1585 # hardlink backup is okay because _writedirstate is always called
1595 1586 # with an "atomictemp=True" file.
1596 1587 util.copyfile(
1597 1588 self._opener.join(filename),
1598 1589 self._opener.join(backupname),
1599 1590 hardlink=True,
1600 1591 )
1601 1592
1602 1593 def restorebackup(self, tr, backupname):
1603 1594 '''Restore dirstate by backup file'''
1604 1595 # this "invalidate()" prevents "wlock.release()" from writing
1605 1596 # changes of dirstate out after restoring from backup file
1606 1597 self.invalidate()
1607 1598 filename = self._actualfilename(tr)
1608 1599 o = self._opener
1609 1600 if util.samefile(o.join(backupname), o.join(filename)):
1610 1601 o.unlink(backupname)
1611 1602 else:
1612 1603 o.rename(backupname, filename, checkambig=True)
1613 1604
1614 1605 def clearbackup(self, tr, backupname):
1615 1606 '''Clear backup file'''
1616 1607 self._opener.unlink(backupname)
@@ -1,951 +1,998 b''
1 1 # dirstatemap.py
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 import errno
9 9
10 10 from .i18n import _
11 11
12 12 from . import (
13 13 error,
14 14 pathutil,
15 15 policy,
16 16 pycompat,
17 17 txnutil,
18 18 util,
19 19 )
20 20
21 21 from .dirstateutils import (
22 22 docket as docketmod,
23 23 )
24 24
25 25 parsers = policy.importmod('parsers')
26 26 rustmod = policy.importrust('dirstate')
27 27
28 28 propertycache = util.propertycache
29 29
30 30 DirstateItem = parsers.DirstateItem
31 31
32 32 rangemask = 0x7FFFFFFF
33 33
34 34
35 35 class dirstatemap(object):
36 36 """Map encapsulating the dirstate's contents.
37 37
38 38 The dirstate contains the following state:
39 39
40 40 - `identity` is the identity of the dirstate file, which can be used to
41 41 detect when changes have occurred to the dirstate file.
42 42
43 43 - `parents` is a pair containing the parents of the working copy. The
44 44 parents are updated by calling `setparents`.
45 45
46 46 - the state map maps filenames to tuples of (state, mode, size, mtime),
47 47 where state is a single character representing 'normal', 'added',
48 48 'removed', or 'merged'. It is read by treating the dirstate as a
49 49 dict. File state is updated by calling the `addfile`, `removefile` and
50 50 `dropfile` methods.
51 51
52 52 - `copymap` maps destination filenames to their source filename.
53 53
54 54 The dirstate also provides the following views onto the state:
55 55
56 56 - `nonnormalset` is a set of the filenames that have state other
57 57 than 'normal', or are normal but have an mtime of -1 ('normallookup').
58 58
59 59 - `otherparentset` is a set of the filenames that are marked as coming
60 60 from the second parent when the dirstate is currently being merged.
61 61
62 62 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
63 63 form that they appear as in the dirstate.
64 64
65 65 - `dirfoldmap` is a dict mapping normalized directory names to the
66 66 denormalized form that they appear as in the dirstate.
67 67 """
68 68
69 69 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
70 70 self._ui = ui
71 71 self._opener = opener
72 72 self._root = root
73 73 self._filename = b'dirstate'
74 74 self._nodelen = 20
75 75 self._nodeconstants = nodeconstants
76 76 assert (
77 77 not use_dirstate_v2
78 78 ), "should have detected unsupported requirement"
79 79
80 80 self._parents = None
81 81 self._dirtyparents = False
82 82
83 83 # for consistent view between _pl() and _read() invocations
84 84 self._pendingmode = None
85 85
86 86 @propertycache
87 87 def _map(self):
88 88 self._map = {}
89 89 self.read()
90 90 return self._map
91 91
92 92 @propertycache
93 93 def copymap(self):
94 94 self.copymap = {}
95 95 self._map
96 96 return self.copymap
97 97
98 98 def clear(self):
99 99 self._map.clear()
100 100 self.copymap.clear()
101 101 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
102 102 util.clearcachedproperty(self, b"_dirs")
103 103 util.clearcachedproperty(self, b"_alldirs")
104 104 util.clearcachedproperty(self, b"filefoldmap")
105 105 util.clearcachedproperty(self, b"dirfoldmap")
106 106 util.clearcachedproperty(self, b"nonnormalset")
107 107 util.clearcachedproperty(self, b"otherparentset")
108 108
109 109 def items(self):
110 110 return pycompat.iteritems(self._map)
111 111
112 112 # forward for python2,3 compat
113 113 iteritems = items
114 114
115 115 debug_iter = items
116 116
117 117 def __len__(self):
118 118 return len(self._map)
119 119
120 120 def __iter__(self):
121 121 return iter(self._map)
122 122
123 123 def get(self, key, default=None):
124 124 return self._map.get(key, default)
125 125
126 126 def __contains__(self, key):
127 127 return key in self._map
128 128
129 129 def __getitem__(self, key):
130 130 return self._map[key]
131 131
132 132 def keys(self):
133 133 return self._map.keys()
134 134
135 135 def preload(self):
136 136 """Loads the underlying data, if it's not already loaded"""
137 137 self._map
138 138
139 139 def _dirs_incr(self, filename, old_entry=None):
140 140 """incremente the dirstate counter if applicable"""
141 141 if (
142 142 old_entry is None or old_entry.removed
143 143 ) and "_dirs" in self.__dict__:
144 144 self._dirs.addpath(filename)
145 145 if old_entry is None and "_alldirs" in self.__dict__:
146 146 self._alldirs.addpath(filename)
147 147
148 148 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
149 149 """decremente the dirstate counter if applicable"""
150 150 if old_entry is not None:
151 151 if "_dirs" in self.__dict__ and not old_entry.removed:
152 152 self._dirs.delpath(filename)
153 153 if "_alldirs" in self.__dict__ and not remove_variant:
154 154 self._alldirs.delpath(filename)
155 155 elif remove_variant and "_alldirs" in self.__dict__:
156 156 self._alldirs.addpath(filename)
157 157 if "filefoldmap" in self.__dict__:
158 158 normed = util.normcase(filename)
159 159 self.filefoldmap.pop(normed, None)
160 160
161 161 def set_possibly_dirty(self, filename):
162 162 """record that the current state of the file on disk is unknown"""
163 163 self[filename].set_possibly_dirty()
164 164
165 165 def set_clean(self, filename, mode, size, mtime):
166 166 """mark a file as back to a clean state"""
167 167 entry = self[filename]
168 168 mtime = mtime & rangemask
169 169 size = size & rangemask
170 170 entry.set_clean(mode, size, mtime)
171 171 self.copymap.pop(filename, None)
172 172 self.nonnormalset.discard(filename)
173 173
174 174 def addfile(
175 175 self,
176 176 f,
177 177 mode=0,
178 178 size=None,
179 179 mtime=None,
180 180 added=False,
181 181 merged=False,
182 182 from_p2=False,
183 183 possibly_dirty=False,
184 184 ):
185 185 """Add a tracked file to the dirstate."""
186 186 if added:
187 187 assert not merged
188 188 assert not possibly_dirty
189 189 assert not from_p2
190 190 new_entry = DirstateItem.new_added()
191 191 self.copymap.pop(f, None)
192 192 elif merged:
193 193 assert not possibly_dirty
194 194 assert not from_p2
195 195 new_entry = DirstateItem.new_merged()
196 196 elif from_p2:
197 197 assert not possibly_dirty
198 198 new_entry = DirstateItem.new_from_p2()
199 199 elif possibly_dirty:
200 200 new_entry = DirstateItem.new_possibly_dirty()
201 201 else:
202 202 assert size is not None
203 203 assert mtime is not None
204 204 size = size & rangemask
205 205 mtime = mtime & rangemask
206 206 new_entry = DirstateItem.new_normal(mode, size, mtime)
207 207 old_entry = self.get(f)
208 208 self._dirs_incr(f, old_entry)
209 209 self._map[f] = new_entry
210 210 if new_entry.dm_nonnormal:
211 211 self.nonnormalset.add(f)
212 212 else:
213 213 self.nonnormalset.discard(f)
214 214 if new_entry.dm_otherparent:
215 215 self.otherparentset.add(f)
216 216 else:
217 217 self.otherparentset.discard(f)
218 218
219 219 def reset_state(
220 220 self,
221 221 filename,
222 222 wc_tracked,
223 223 p1_tracked,
224 224 p2_tracked=False,
225 225 merged=False,
226 226 clean_p1=False,
227 227 clean_p2=False,
228 228 possibly_dirty=False,
229 229 parentfiledata=None,
230 230 ):
231 231 """Set a entry to a given state, diregarding all previous state
232 232
233 233 This is to be used by the part of the dirstate API dedicated to
234 234 adjusting the dirstate after a update/merge.
235 235
236 236 note: calling this might result to no entry existing at all if the
237 237 dirstate map does not see any point at having one for this file
238 238 anymore.
239 239 """
240 240 if merged and (clean_p1 or clean_p2):
241 241 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
242 242 raise error.ProgrammingError(msg)
243 243 # copy information are now outdated
244 244 # (maybe new information should be in directly passed to this function)
245 245 self.copymap.pop(filename, None)
246 246
247 247 if not (p1_tracked or p2_tracked or wc_tracked):
248 248 self.dropfile(filename)
249 249 return
250 250 elif merged:
251 251 # XXX might be merged and removed ?
252 252 entry = self.get(filename)
253 253 if entry is None or not entry.tracked:
254 254 # XXX mostly replicate dirstate.other parent. We should get
255 255 # the higher layer to pass us more reliable data where `merged`
256 256 # actually mean merged. Dropping this clause will show failure
257 257 # in `test-graft.t`
258 258 merged = False
259 259 clean_p2 = True
260 260 elif not (p1_tracked or p2_tracked) and wc_tracked:
261 261 pass # file is added, nothing special to adjust
262 262 elif (p1_tracked or p2_tracked) and not wc_tracked:
263 263 pass
264 264 elif clean_p2 and wc_tracked:
265 265 if p1_tracked or self.get(filename) is not None:
266 266 # XXX the `self.get` call is catching some case in
267 267 # `test-merge-remove.t` where the file is tracked in p1, the
268 268 # p1_tracked argument is False.
269 269 #
270 270 # In addition, this seems to be a case where the file is marked
271 271 # as merged without actually being the result of a merge
272 272 # action. So thing are not ideal here.
273 273 merged = True
274 274 clean_p2 = False
275 275 elif not p1_tracked and p2_tracked and wc_tracked:
276 276 clean_p2 = True
277 277 elif possibly_dirty:
278 278 pass
279 279 elif wc_tracked:
280 280 # this is a "normal" file
281 281 if parentfiledata is None:
282 282 msg = b'failed to pass parentfiledata for a normal file: %s'
283 283 msg %= filename
284 284 raise error.ProgrammingError(msg)
285 285 else:
286 286 assert False, 'unreachable'
287 287
288 288 old_entry = self._map.get(filename)
289 289 self._dirs_incr(filename, old_entry)
290 290 entry = DirstateItem(
291 291 wc_tracked=wc_tracked,
292 292 p1_tracked=p1_tracked,
293 293 p2_tracked=p2_tracked,
294 294 merged=merged,
295 295 clean_p1=clean_p1,
296 296 clean_p2=clean_p2,
297 297 possibly_dirty=possibly_dirty,
298 298 parentfiledata=parentfiledata,
299 299 )
300 300 if entry.dm_nonnormal:
301 301 self.nonnormalset.add(filename)
302 302 else:
303 303 self.nonnormalset.discard(filename)
304 304 if entry.dm_otherparent:
305 305 self.otherparentset.add(filename)
306 306 else:
307 307 self.otherparentset.discard(filename)
308 308 self._map[filename] = entry
309 309
310 def set_tracked(self, filename):
311 new = False
312 entry = self.get(filename)
313 if entry is None:
314 self._dirs_incr(filename)
315 entry = DirstateItem(
316 p1_tracked=False,
317 p2_tracked=False,
318 wc_tracked=True,
319 merged=False,
320 clean_p1=False,
321 clean_p2=False,
322 possibly_dirty=False,
323 parentfiledata=None,
324 )
325 self._map[filename] = entry
326 if entry.dm_nonnormal:
327 self.nonnormalset.add(filename)
328 new = True
329 elif not entry.tracked:
330 self._dirs_incr(filename, entry)
331 entry.set_tracked()
332 new = True
333 else:
334 # XXX This is probably overkill for more case, but we need this to
335 # fully replace the `normallookup` call with `set_tracked` one.
336 # Consider smoothing this in the future.
337 self.set_possibly_dirty(filename)
338 return new
339
310 340 def set_untracked(self, f):
311 341 """Mark a file as no longer tracked in the dirstate map"""
312 342 entry = self.get(f)
313 343 if entry is None:
314 344 return False
315 345 else:
316 346 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
317 347 if not entry.merged:
318 348 self.copymap.pop(f, None)
319 349 if entry.added:
320 350 self.nonnormalset.discard(f)
321 351 self._map.pop(f, None)
322 352 else:
323 353 self.nonnormalset.add(f)
324 354 if entry.from_p2:
325 355 self.otherparentset.add(f)
326 356 entry.set_untracked()
327 357 return True
328 358
329 359 def dropfile(self, f):
330 360 """
331 361 Remove a file from the dirstate. Returns True if the file was
332 362 previously recorded.
333 363 """
334 364 old_entry = self._map.pop(f, None)
335 365 self._dirs_decr(f, old_entry=old_entry)
336 366 self.nonnormalset.discard(f)
337 367 self.copymap.pop(f, None)
338 368 return old_entry is not None
339 369
340 370 def clearambiguoustimes(self, files, now):
341 371 for f in files:
342 372 e = self.get(f)
343 373 if e is not None and e.need_delay(now):
344 374 e.set_possibly_dirty()
345 375 self.nonnormalset.add(f)
346 376
347 377 def nonnormalentries(self):
348 378 '''Compute the nonnormal dirstate entries from the dmap'''
349 379 try:
350 380 return parsers.nonnormalotherparententries(self._map)
351 381 except AttributeError:
352 382 nonnorm = set()
353 383 otherparent = set()
354 384 for fname, e in pycompat.iteritems(self._map):
355 385 if e.dm_nonnormal:
356 386 nonnorm.add(fname)
357 387 if e.from_p2:
358 388 otherparent.add(fname)
359 389 return nonnorm, otherparent
360 390
361 391 @propertycache
362 392 def filefoldmap(self):
363 393 """Returns a dictionary mapping normalized case paths to their
364 394 non-normalized versions.
365 395 """
366 396 try:
367 397 makefilefoldmap = parsers.make_file_foldmap
368 398 except AttributeError:
369 399 pass
370 400 else:
371 401 return makefilefoldmap(
372 402 self._map, util.normcasespec, util.normcasefallback
373 403 )
374 404
375 405 f = {}
376 406 normcase = util.normcase
377 407 for name, s in pycompat.iteritems(self._map):
378 408 if not s.removed:
379 409 f[normcase(name)] = name
380 410 f[b'.'] = b'.' # prevents useless util.fspath() invocation
381 411 return f
382 412
383 413 def hastrackeddir(self, d):
384 414 """
385 415 Returns True if the dirstate contains a tracked (not removed) file
386 416 in this directory.
387 417 """
388 418 return d in self._dirs
389 419
390 420 def hasdir(self, d):
391 421 """
392 422 Returns True if the dirstate contains a file (tracked or removed)
393 423 in this directory.
394 424 """
395 425 return d in self._alldirs
396 426
397 427 @propertycache
398 428 def _dirs(self):
399 429 return pathutil.dirs(self._map, only_tracked=True)
400 430
401 431 @propertycache
402 432 def _alldirs(self):
403 433 return pathutil.dirs(self._map)
404 434
405 435 def _opendirstatefile(self):
406 436 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
407 437 if self._pendingmode is not None and self._pendingmode != mode:
408 438 fp.close()
409 439 raise error.Abort(
410 440 _(b'working directory state may be changed parallelly')
411 441 )
412 442 self._pendingmode = mode
413 443 return fp
414 444
415 445 def parents(self):
416 446 if not self._parents:
417 447 try:
418 448 fp = self._opendirstatefile()
419 449 st = fp.read(2 * self._nodelen)
420 450 fp.close()
421 451 except IOError as err:
422 452 if err.errno != errno.ENOENT:
423 453 raise
424 454 # File doesn't exist, so the current state is empty
425 455 st = b''
426 456
427 457 l = len(st)
428 458 if l == self._nodelen * 2:
429 459 self._parents = (
430 460 st[: self._nodelen],
431 461 st[self._nodelen : 2 * self._nodelen],
432 462 )
433 463 elif l == 0:
434 464 self._parents = (
435 465 self._nodeconstants.nullid,
436 466 self._nodeconstants.nullid,
437 467 )
438 468 else:
439 469 raise error.Abort(
440 470 _(b'working directory state appears damaged!')
441 471 )
442 472
443 473 return self._parents
444 474
445 475 def setparents(self, p1, p2):
446 476 self._parents = (p1, p2)
447 477 self._dirtyparents = True
448 478
449 479 def read(self):
450 480 # ignore HG_PENDING because identity is used only for writing
451 481 self.identity = util.filestat.frompath(
452 482 self._opener.join(self._filename)
453 483 )
454 484
455 485 try:
456 486 fp = self._opendirstatefile()
457 487 try:
458 488 st = fp.read()
459 489 finally:
460 490 fp.close()
461 491 except IOError as err:
462 492 if err.errno != errno.ENOENT:
463 493 raise
464 494 return
465 495 if not st:
466 496 return
467 497
468 498 if util.safehasattr(parsers, b'dict_new_presized'):
469 499 # Make an estimate of the number of files in the dirstate based on
470 500 # its size. This trades wasting some memory for avoiding costly
471 501 # resizes. Each entry have a prefix of 17 bytes followed by one or
472 502 # two path names. Studies on various large-scale real-world repositories
473 503 # found 54 bytes a reasonable upper limit for the average path names.
474 504 # Copy entries are ignored for the sake of this estimate.
475 505 self._map = parsers.dict_new_presized(len(st) // 71)
476 506
477 507 # Python's garbage collector triggers a GC each time a certain number
478 508 # of container objects (the number being defined by
479 509 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
480 510 # for each file in the dirstate. The C version then immediately marks
481 511 # them as not to be tracked by the collector. However, this has no
482 512 # effect on when GCs are triggered, only on what objects the GC looks
483 513 # into. This means that O(number of files) GCs are unavoidable.
484 514 # Depending on when in the process's lifetime the dirstate is parsed,
485 515 # this can get very expensive. As a workaround, disable GC while
486 516 # parsing the dirstate.
487 517 #
488 518 # (we cannot decorate the function directly since it is in a C module)
489 519 parse_dirstate = util.nogc(parsers.parse_dirstate)
490 520 p = parse_dirstate(self._map, self.copymap, st)
491 521 if not self._dirtyparents:
492 522 self.setparents(*p)
493 523
494 524 # Avoid excess attribute lookups by fast pathing certain checks
495 525 self.__contains__ = self._map.__contains__
496 526 self.__getitem__ = self._map.__getitem__
497 527 self.get = self._map.get
498 528
499 529 def write(self, _tr, st, now):
500 530 st.write(
501 531 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
502 532 )
503 533 st.close()
504 534 self._dirtyparents = False
505 535 self.nonnormalset, self.otherparentset = self.nonnormalentries()
506 536
507 537 @propertycache
508 538 def nonnormalset(self):
509 539 nonnorm, otherparents = self.nonnormalentries()
510 540 self.otherparentset = otherparents
511 541 return nonnorm
512 542
513 543 @propertycache
514 544 def otherparentset(self):
515 545 nonnorm, otherparents = self.nonnormalentries()
516 546 self.nonnormalset = nonnorm
517 547 return otherparents
518 548
519 549 def non_normal_or_other_parent_paths(self):
520 550 return self.nonnormalset.union(self.otherparentset)
521 551
522 552 @propertycache
523 553 def identity(self):
524 554 self._map
525 555 return self.identity
526 556
527 557 @propertycache
528 558 def dirfoldmap(self):
529 559 f = {}
530 560 normcase = util.normcase
531 561 for name in self._dirs:
532 562 f[normcase(name)] = name
533 563 return f
534 564
535 565
536 566 if rustmod is not None:
537 567
538 568 class dirstatemap(object):
539 569 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
540 570 self._use_dirstate_v2 = use_dirstate_v2
541 571 self._nodeconstants = nodeconstants
542 572 self._ui = ui
543 573 self._opener = opener
544 574 self._root = root
545 575 self._filename = b'dirstate'
546 576 self._nodelen = 20 # Also update Rust code when changing this!
547 577 self._parents = None
548 578 self._dirtyparents = False
549 579 self._docket = None
550 580
551 581 # for consistent view between _pl() and _read() invocations
552 582 self._pendingmode = None
553 583
554 584 self._use_dirstate_tree = self._ui.configbool(
555 585 b"experimental",
556 586 b"dirstate-tree.in-memory",
557 587 False,
558 588 )
559 589
560 590 def addfile(
561 591 self,
562 592 f,
563 593 mode=0,
564 594 size=None,
565 595 mtime=None,
566 596 added=False,
567 597 merged=False,
568 598 from_p2=False,
569 599 possibly_dirty=False,
570 600 ):
571 601 ret = self._rustmap.addfile(
572 602 f,
573 603 mode,
574 604 size,
575 605 mtime,
576 606 added,
577 607 merged,
578 608 from_p2,
579 609 possibly_dirty,
580 610 )
581 611 if added:
582 612 self.copymap.pop(f, None)
583 613 return ret
584 614
585 615 def reset_state(
586 616 self,
587 617 filename,
588 618 wc_tracked,
589 619 p1_tracked,
590 620 p2_tracked=False,
591 621 merged=False,
592 622 clean_p1=False,
593 623 clean_p2=False,
594 624 possibly_dirty=False,
595 625 parentfiledata=None,
596 626 ):
597 627 """Set a entry to a given state, disregarding all previous state
598 628
599 629 This is to be used by the part of the dirstate API dedicated to
600 630 adjusting the dirstate after a update/merge.
601 631
602 632 note: calling this might result to no entry existing at all if the
603 633 dirstate map does not see any point at having one for this file
604 634 anymore.
605 635 """
606 636 if merged and (clean_p1 or clean_p2):
607 637 msg = (
608 638 b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
609 639 )
610 640 raise error.ProgrammingError(msg)
611 641 # copy information are now outdated
612 642 # (maybe new information should be in directly passed to this function)
613 643 self.copymap.pop(filename, None)
614 644
615 645 if not (p1_tracked or p2_tracked or wc_tracked):
616 646 self.dropfile(filename)
617 647 elif merged:
618 648 # XXX might be merged and removed ?
619 649 entry = self.get(filename)
620 650 if entry is not None and entry.tracked:
621 651 # XXX mostly replicate dirstate.other parent. We should get
622 652 # the higher layer to pass us more reliable data where `merged`
623 653 # actually mean merged. Dropping the else clause will show
624 654 # failure in `test-graft.t`
625 655 self.addfile(filename, merged=True)
626 656 else:
627 657 self.addfile(filename, from_p2=True)
628 658 elif not (p1_tracked or p2_tracked) and wc_tracked:
629 659 self.addfile(
630 660 filename, added=True, possibly_dirty=possibly_dirty
631 661 )
632 662 elif (p1_tracked or p2_tracked) and not wc_tracked:
633 663 # XXX might be merged and removed ?
634 664 self[filename] = DirstateItem.from_v1_data(b'r', 0, 0, 0)
635 665 self.nonnormalset.add(filename)
636 666 elif clean_p2 and wc_tracked:
637 667 if p1_tracked or self.get(filename) is not None:
638 668 # XXX the `self.get` call is catching some case in
639 669 # `test-merge-remove.t` where the file is tracked in p1, the
640 670 # p1_tracked argument is False.
641 671 #
642 672 # In addition, this seems to be a case where the file is marked
643 673 # as merged without actually being the result of a merge
644 674 # action. So thing are not ideal here.
645 675 self.addfile(filename, merged=True)
646 676 else:
647 677 self.addfile(filename, from_p2=True)
648 678 elif not p1_tracked and p2_tracked and wc_tracked:
649 679 self.addfile(
650 680 filename, from_p2=True, possibly_dirty=possibly_dirty
651 681 )
652 682 elif possibly_dirty:
653 683 self.addfile(filename, possibly_dirty=possibly_dirty)
654 684 elif wc_tracked:
655 685 # this is a "normal" file
656 686 if parentfiledata is None:
657 687 msg = b'failed to pass parentfiledata for a normal file: %s'
658 688 msg %= filename
659 689 raise error.ProgrammingError(msg)
660 690 mode, size, mtime = parentfiledata
661 691 self.addfile(filename, mode=mode, size=size, mtime=mtime)
662 692 self.nonnormalset.discard(filename)
663 693 else:
664 694 assert False, 'unreachable'
665 695
696 def set_tracked(self, filename):
697 new = False
698 entry = self.get(filename)
699 if entry is None:
700 self.addfile(filename, added=True)
701 new = True
702 elif not entry.tracked:
703 entry.set_tracked()
704 self._rustmap.set_v1(filename, entry)
705 new = True
706 else:
707 # XXX This is probably overkill for more case, but we need this to
708 # fully replace the `normallookup` call with `set_tracked` one.
709 # Consider smoothing this in the future.
710 self.set_possibly_dirty(filename)
711 return new
712
666 713 def set_untracked(self, f):
667 714 """Mark a file as no longer tracked in the dirstate map"""
668 715 # in merge is only trigger more logic, so it "fine" to pass it.
669 716 #
670 717 # the inner rust dirstate map code need to be adjusted once the API
671 718 # for dirstate/dirstatemap/DirstateItem is a bit more settled
672 719 entry = self.get(f)
673 720 if entry is None:
674 721 return False
675 722 else:
676 723 if entry.added:
677 724 self._rustmap.copymap().pop(f, None)
678 725 self._rustmap.dropfile(f)
679 726 else:
680 727 self._rustmap.removefile(f, in_merge=True)
681 728 return True
682 729
683 730 def removefile(self, *args, **kwargs):
684 731 return self._rustmap.removefile(*args, **kwargs)
685 732
686 733 def dropfile(self, f, *args, **kwargs):
687 734 self._rustmap.copymap().pop(f, None)
688 735 return self._rustmap.dropfile(f, *args, **kwargs)
689 736
690 737 def clearambiguoustimes(self, *args, **kwargs):
691 738 return self._rustmap.clearambiguoustimes(*args, **kwargs)
692 739
693 740 def nonnormalentries(self):
694 741 return self._rustmap.nonnormalentries()
695 742
696 743 def get(self, *args, **kwargs):
697 744 return self._rustmap.get(*args, **kwargs)
698 745
699 746 @property
700 747 def copymap(self):
701 748 return self._rustmap.copymap()
702 749
703 750 def directories(self):
704 751 return self._rustmap.directories()
705 752
706 753 def debug_iter(self):
707 754 return self._rustmap.debug_iter()
708 755
709 756 def preload(self):
710 757 self._rustmap
711 758
712 759 def clear(self):
713 760 self._rustmap.clear()
714 761 self.setparents(
715 762 self._nodeconstants.nullid, self._nodeconstants.nullid
716 763 )
717 764 util.clearcachedproperty(self, b"_dirs")
718 765 util.clearcachedproperty(self, b"_alldirs")
719 766 util.clearcachedproperty(self, b"dirfoldmap")
720 767
721 768 def items(self):
722 769 return self._rustmap.items()
723 770
724 771 def keys(self):
725 772 return iter(self._rustmap)
726 773
727 774 def __contains__(self, key):
728 775 return key in self._rustmap
729 776
730 777 def __getitem__(self, item):
731 778 return self._rustmap[item]
732 779
733 780 def __len__(self):
734 781 return len(self._rustmap)
735 782
736 783 def __iter__(self):
737 784 return iter(self._rustmap)
738 785
739 786 # forward for python2,3 compat
740 787 iteritems = items
741 788
742 789 def _opendirstatefile(self):
743 790 fp, mode = txnutil.trypending(
744 791 self._root, self._opener, self._filename
745 792 )
746 793 if self._pendingmode is not None and self._pendingmode != mode:
747 794 fp.close()
748 795 raise error.Abort(
749 796 _(b'working directory state may be changed parallelly')
750 797 )
751 798 self._pendingmode = mode
752 799 return fp
753 800
754 801 def _readdirstatefile(self, size=-1):
755 802 try:
756 803 with self._opendirstatefile() as fp:
757 804 return fp.read(size)
758 805 except IOError as err:
759 806 if err.errno != errno.ENOENT:
760 807 raise
761 808 # File doesn't exist, so the current state is empty
762 809 return b''
763 810
764 811 def setparents(self, p1, p2):
765 812 self._parents = (p1, p2)
766 813 self._dirtyparents = True
767 814
768 815 def parents(self):
769 816 if not self._parents:
770 817 if self._use_dirstate_v2:
771 818 self._parents = self.docket.parents
772 819 else:
773 820 read_len = self._nodelen * 2
774 821 st = self._readdirstatefile(read_len)
775 822 l = len(st)
776 823 if l == read_len:
777 824 self._parents = (
778 825 st[: self._nodelen],
779 826 st[self._nodelen : 2 * self._nodelen],
780 827 )
781 828 elif l == 0:
782 829 self._parents = (
783 830 self._nodeconstants.nullid,
784 831 self._nodeconstants.nullid,
785 832 )
786 833 else:
787 834 raise error.Abort(
788 835 _(b'working directory state appears damaged!')
789 836 )
790 837
791 838 return self._parents
792 839
793 840 @property
794 841 def docket(self):
795 842 if not self._docket:
796 843 if not self._use_dirstate_v2:
797 844 raise error.ProgrammingError(
798 845 b'dirstate only has a docket in v2 format'
799 846 )
800 847 self._docket = docketmod.DirstateDocket.parse(
801 848 self._readdirstatefile(), self._nodeconstants
802 849 )
803 850 return self._docket
804 851
805 852 @propertycache
806 853 def _rustmap(self):
807 854 """
808 855 Fills the Dirstatemap when called.
809 856 """
810 857 # ignore HG_PENDING because identity is used only for writing
811 858 self.identity = util.filestat.frompath(
812 859 self._opener.join(self._filename)
813 860 )
814 861
815 862 if self._use_dirstate_v2:
816 863 if self.docket.uuid:
817 864 # TODO: use mmap when possible
818 865 data = self._opener.read(self.docket.data_filename())
819 866 else:
820 867 data = b''
821 868 self._rustmap = rustmod.DirstateMap.new_v2(
822 869 data, self.docket.data_size, self.docket.tree_metadata
823 870 )
824 871 parents = self.docket.parents
825 872 else:
826 873 self._rustmap, parents = rustmod.DirstateMap.new_v1(
827 874 self._use_dirstate_tree, self._readdirstatefile()
828 875 )
829 876
830 877 if parents and not self._dirtyparents:
831 878 self.setparents(*parents)
832 879
833 880 self.__contains__ = self._rustmap.__contains__
834 881 self.__getitem__ = self._rustmap.__getitem__
835 882 self.get = self._rustmap.get
836 883 return self._rustmap
837 884
838 885 def write(self, tr, st, now):
839 886 if not self._use_dirstate_v2:
840 887 p1, p2 = self.parents()
841 888 packed = self._rustmap.write_v1(p1, p2, now)
842 889 st.write(packed)
843 890 st.close()
844 891 self._dirtyparents = False
845 892 return
846 893
847 894 # We can only append to an existing data file if there is one
848 895 can_append = self.docket.uuid is not None
849 896 packed, meta, append = self._rustmap.write_v2(now, can_append)
850 897 if append:
851 898 docket = self.docket
852 899 data_filename = docket.data_filename()
853 900 if tr:
854 901 tr.add(data_filename, docket.data_size)
855 902 with self._opener(data_filename, b'r+b') as fp:
856 903 fp.seek(docket.data_size)
857 904 assert fp.tell() == docket.data_size
858 905 written = fp.write(packed)
859 906 if written is not None: # py2 may return None
860 907 assert written == len(packed), (written, len(packed))
861 908 docket.data_size += len(packed)
862 909 docket.parents = self.parents()
863 910 docket.tree_metadata = meta
864 911 st.write(docket.serialize())
865 912 st.close()
866 913 else:
867 914 old_docket = self.docket
868 915 new_docket = docketmod.DirstateDocket.with_new_uuid(
869 916 self.parents(), len(packed), meta
870 917 )
871 918 data_filename = new_docket.data_filename()
872 919 if tr:
873 920 tr.add(data_filename, 0)
874 921 self._opener.write(data_filename, packed)
875 922 # Write the new docket after the new data file has been
876 923 # written. Because `st` was opened with `atomictemp=True`,
877 924 # the actual `.hg/dirstate` file is only affected on close.
878 925 st.write(new_docket.serialize())
879 926 st.close()
880 927 # Remove the old data file after the new docket pointing to
881 928 # the new data file was written.
882 929 if old_docket.uuid:
883 930 data_filename = old_docket.data_filename()
884 931 unlink = lambda _tr=None: self._opener.unlink(data_filename)
885 932 if tr:
886 933 category = b"dirstate-v2-clean-" + old_docket.uuid
887 934 tr.addpostclose(category, unlink)
888 935 else:
889 936 unlink()
890 937 self._docket = new_docket
891 938 # Reload from the newly-written file
892 939 util.clearcachedproperty(self, b"_rustmap")
893 940 self._dirtyparents = False
894 941
895 942 @propertycache
896 943 def filefoldmap(self):
897 944 """Returns a dictionary mapping normalized case paths to their
898 945 non-normalized versions.
899 946 """
900 947 return self._rustmap.filefoldmapasdict()
901 948
902 949 def hastrackeddir(self, d):
903 950 return self._rustmap.hastrackeddir(d)
904 951
905 952 def hasdir(self, d):
906 953 return self._rustmap.hasdir(d)
907 954
908 955 @propertycache
909 956 def identity(self):
910 957 self._rustmap
911 958 return self.identity
912 959
913 960 @property
914 961 def nonnormalset(self):
915 962 nonnorm = self._rustmap.non_normal_entries()
916 963 return nonnorm
917 964
918 965 @propertycache
919 966 def otherparentset(self):
920 967 otherparents = self._rustmap.other_parent_entries()
921 968 return otherparents
922 969
923 970 def non_normal_or_other_parent_paths(self):
924 971 return self._rustmap.non_normal_or_other_parent_paths()
925 972
926 973 @propertycache
927 974 def dirfoldmap(self):
928 975 f = {}
929 976 normcase = util.normcase
930 977 for name in self._rustmap.tracked_dirs():
931 978 f[normcase(name)] = name
932 979 return f
933 980
934 981 def set_possibly_dirty(self, filename):
935 982 """record that the current state of the file on disk is unknown"""
936 983 entry = self[filename]
937 984 entry.set_possibly_dirty()
938 985 self._rustmap.set_v1(filename, entry)
939 986
940 987 def set_clean(self, filename, mode, size, mtime):
941 988 """mark a file as back to a clean state"""
942 989 entry = self[filename]
943 990 mtime = mtime & rangemask
944 991 size = size & rangemask
945 992 entry.set_clean(mode, size, mtime)
946 993 self._rustmap.set_v1(filename, entry)
947 994 self._rustmap.copymap().pop(filename, None)
948 995
949 996 def __setitem__(self, key, value):
950 997 assert isinstance(value, DirstateItem)
951 998 self._rustmap.set_v1(key, value)
@@ -1,810 +1,822 b''
1 1 # parsers.py - Python implementation of parsers.c
2 2 #
3 3 # Copyright 2009 Olivia Mackall <olivia@selenic.com> and others
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 from __future__ import absolute_import
9 9
10 10 import struct
11 11 import zlib
12 12
13 13 from ..node import (
14 14 nullrev,
15 15 sha1nodeconstants,
16 16 )
17 17 from ..thirdparty import attr
18 18 from .. import (
19 19 error,
20 20 pycompat,
21 21 revlogutils,
22 22 util,
23 23 )
24 24
25 25 from ..revlogutils import nodemap as nodemaputil
26 26 from ..revlogutils import constants as revlog_constants
27 27
28 28 stringio = pycompat.bytesio
29 29
30 30
31 31 _pack = struct.pack
32 32 _unpack = struct.unpack
33 33 _compress = zlib.compress
34 34 _decompress = zlib.decompress
35 35
36 36
37 37 # a special value used internally for `size` if the file come from the other parent
38 38 FROM_P2 = -2
39 39
40 40 # a special value used internally for `size` if the file is modified/merged/added
41 41 NONNORMAL = -1
42 42
43 43 # a special value used internally for `time` if the time is ambigeous
44 44 AMBIGUOUS_TIME = -1
45 45
46 46
47 47 @attr.s(slots=True, init=False)
48 48 class DirstateItem(object):
49 49 """represent a dirstate entry
50 50
51 51 It contains:
52 52
53 53 - state (one of 'n', 'a', 'r', 'm')
54 54 - mode,
55 55 - size,
56 56 - mtime,
57 57 """
58 58
59 59 _wc_tracked = attr.ib()
60 60 _p1_tracked = attr.ib()
61 61 _p2_tracked = attr.ib()
62 62 # the three item above should probably be combined
63 63 #
64 64 # However it is unclear if they properly cover some of the most advanced
65 65 # merge case. So we should probably wait on this to be settled.
66 66 _merged = attr.ib()
67 67 _clean_p1 = attr.ib()
68 68 _clean_p2 = attr.ib()
69 69 _possibly_dirty = attr.ib()
70 70 _mode = attr.ib()
71 71 _size = attr.ib()
72 72 _mtime = attr.ib()
73 73
74 74 def __init__(
75 75 self,
76 76 wc_tracked=False,
77 77 p1_tracked=False,
78 78 p2_tracked=False,
79 79 merged=False,
80 80 clean_p1=False,
81 81 clean_p2=False,
82 82 possibly_dirty=False,
83 83 parentfiledata=None,
84 84 ):
85 85 if merged and (clean_p1 or clean_p2):
86 86 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
87 87 raise error.ProgrammingError(msg)
88 88
89 89 self._wc_tracked = wc_tracked
90 90 self._p1_tracked = p1_tracked
91 91 self._p2_tracked = p2_tracked
92 92 self._merged = merged
93 93 self._clean_p1 = clean_p1
94 94 self._clean_p2 = clean_p2
95 95 self._possibly_dirty = possibly_dirty
96 96 if parentfiledata is None:
97 97 self._mode = None
98 98 self._size = None
99 99 self._mtime = None
100 100 else:
101 101 self._mode = parentfiledata[0]
102 102 self._size = parentfiledata[1]
103 103 self._mtime = parentfiledata[2]
104 104
105 105 @classmethod
106 106 def new_added(cls):
107 107 """constructor to help legacy API to build a new "added" item
108 108
109 109 Should eventually be removed
110 110 """
111 111 instance = cls()
112 112 instance._wc_tracked = True
113 113 instance._p1_tracked = False
114 114 instance._p2_tracked = False
115 115 return instance
116 116
117 117 @classmethod
118 118 def new_merged(cls):
119 119 """constructor to help legacy API to build a new "merged" item
120 120
121 121 Should eventually be removed
122 122 """
123 123 instance = cls()
124 124 instance._wc_tracked = True
125 125 instance._p1_tracked = True # might not be True because of rename ?
126 126 instance._p2_tracked = True # might not be True because of rename ?
127 127 instance._merged = True
128 128 return instance
129 129
130 130 @classmethod
131 131 def new_from_p2(cls):
132 132 """constructor to help legacy API to build a new "from_p2" item
133 133
134 134 Should eventually be removed
135 135 """
136 136 instance = cls()
137 137 instance._wc_tracked = True
138 138 instance._p1_tracked = False # might actually be True
139 139 instance._p2_tracked = True
140 140 instance._clean_p2 = True
141 141 return instance
142 142
143 143 @classmethod
144 144 def new_possibly_dirty(cls):
145 145 """constructor to help legacy API to build a new "possibly_dirty" item
146 146
147 147 Should eventually be removed
148 148 """
149 149 instance = cls()
150 150 instance._wc_tracked = True
151 151 instance._p1_tracked = True
152 152 instance._possibly_dirty = True
153 153 return instance
154 154
155 155 @classmethod
156 156 def new_normal(cls, mode, size, mtime):
157 157 """constructor to help legacy API to build a new "normal" item
158 158
159 159 Should eventually be removed
160 160 """
161 161 assert size != FROM_P2
162 162 assert size != NONNORMAL
163 163 instance = cls()
164 164 instance._wc_tracked = True
165 165 instance._p1_tracked = True
166 166 instance._mode = mode
167 167 instance._size = size
168 168 instance._mtime = mtime
169 169 return instance
170 170
171 171 @classmethod
172 172 def from_v1_data(cls, state, mode, size, mtime):
173 173 """Build a new DirstateItem object from V1 data
174 174
175 175 Since the dirstate-v1 format is frozen, the signature of this function
176 176 is not expected to change, unlike the __init__ one.
177 177 """
178 178 if state == b'm':
179 179 return cls.new_merged()
180 180 elif state == b'a':
181 181 return cls.new_added()
182 182 elif state == b'r':
183 183 instance = cls()
184 184 instance._wc_tracked = False
185 185 if size == NONNORMAL:
186 186 instance._merged = True
187 187 instance._p1_tracked = (
188 188 True # might not be True because of rename ?
189 189 )
190 190 instance._p2_tracked = (
191 191 True # might not be True because of rename ?
192 192 )
193 193 elif size == FROM_P2:
194 194 instance._clean_p2 = True
195 195 instance._p1_tracked = (
196 196 False # We actually don't know (file history)
197 197 )
198 198 instance._p2_tracked = True
199 199 else:
200 200 instance._p1_tracked = True
201 201 return instance
202 202 elif state == b'n':
203 203 if size == FROM_P2:
204 204 return cls.new_from_p2()
205 205 elif size == NONNORMAL:
206 206 return cls.new_possibly_dirty()
207 207 elif mtime == AMBIGUOUS_TIME:
208 208 instance = cls.new_normal(mode, size, 42)
209 209 instance._mtime = None
210 210 instance._possibly_dirty = True
211 211 return instance
212 212 else:
213 213 return cls.new_normal(mode, size, mtime)
214 214 else:
215 215 raise RuntimeError(b'unknown state: %s' % state)
216 216
217 217 def set_possibly_dirty(self):
218 218 """Mark a file as "possibly dirty"
219 219
220 220 This means the next status call will have to actually check its content
221 221 to make sure it is correct.
222 222 """
223 223 self._possibly_dirty = True
224 224
225 225 def set_clean(self, mode, size, mtime):
226 226 """mark a file as "clean" cancelling potential "possibly dirty call"
227 227
228 228 Note: this function is a descendant of `dirstate.normal` and is
229 229 currently expected to be call on "normal" entry only. There are not
230 230 reason for this to not change in the future as long as the ccode is
231 231 updated to preserve the proper state of the non-normal files.
232 232 """
233 233 self._wc_tracked = True
234 234 self._p1_tracked = True
235 235 self._p2_tracked = False # this might be wrong
236 236 self._merged = False
237 237 self._clean_p2 = False
238 238 self._possibly_dirty = False
239 239 self._mode = mode
240 240 self._size = size
241 241 self._mtime = mtime
242 242
243 def set_tracked(self):
244 """mark a file as tracked in the working copy
245
246 This will ultimately be called by command like `hg add`.
247 """
248 self._wc_tracked = True
249 # `set_tracked` is replacing various `normallookup` call. So we set
250 # "possibly dirty" to stay on the safe side.
251 #
252 # Consider dropping this in the future in favor of something less broad.
253 self._possibly_dirty = True
254
243 255 def set_untracked(self):
244 256 """mark a file as untracked in the working copy
245 257
246 258 This will ultimately be called by command like `hg remove`.
247 259 """
248 260 # backup the previous state (useful for merge)
249 261 self._wc_tracked = False
250 262 self._mode = None
251 263 self._size = None
252 264 self._mtime = None
253 265
254 266 @property
255 267 def mode(self):
256 268 return self.v1_mode()
257 269
258 270 @property
259 271 def size(self):
260 272 return self.v1_size()
261 273
262 274 @property
263 275 def mtime(self):
264 276 return self.v1_mtime()
265 277
266 278 @property
267 279 def state(self):
268 280 """
269 281 States are:
270 282 n normal
271 283 m needs merging
272 284 r marked for removal
273 285 a marked for addition
274 286
275 287 XXX This "state" is a bit obscure and mostly a direct expression of the
276 288 dirstatev1 format. It would make sense to ultimately deprecate it in
277 289 favor of the more "semantic" attributes.
278 290 """
279 291 return self.v1_state()
280 292
281 293 @property
282 294 def tracked(self):
283 295 """True is the file is tracked in the working copy"""
284 296 return self._wc_tracked
285 297
286 298 @property
287 299 def added(self):
288 300 """True if the file has been added"""
289 301 return self._wc_tracked and not (self._p1_tracked or self._p2_tracked)
290 302
291 303 @property
292 304 def merged(self):
293 305 """True if the file has been merged
294 306
295 307 Should only be set if a merge is in progress in the dirstate
296 308 """
297 309 return self._wc_tracked and self._merged
298 310
299 311 @property
300 312 def from_p2(self):
301 313 """True if the file have been fetched from p2 during the current merge
302 314
303 315 This is only True is the file is currently tracked.
304 316
305 317 Should only be set if a merge is in progress in the dirstate
306 318 """
307 319 if not self._wc_tracked:
308 320 return False
309 321 return self._clean_p2 or (not self._p1_tracked and self._p2_tracked)
310 322
311 323 @property
312 324 def from_p2_removed(self):
313 325 """True if the file has been removed, but was "from_p2" initially
314 326
315 327 This property seems like an abstraction leakage and should probably be
316 328 dealt in this class (or maybe the dirstatemap) directly.
317 329 """
318 330 return self.removed and self._clean_p2
319 331
320 332 @property
321 333 def removed(self):
322 334 """True if the file has been removed"""
323 335 return not self._wc_tracked and (self._p1_tracked or self._p2_tracked)
324 336
325 337 @property
326 338 def merged_removed(self):
327 339 """True if the file has been removed, but was "merged" initially
328 340
329 341 This property seems like an abstraction leakage and should probably be
330 342 dealt in this class (or maybe the dirstatemap) directly.
331 343 """
332 344 return self.removed and self._merged
333 345
334 346 @property
335 347 def dm_nonnormal(self):
336 348 """True is the entry is non-normal in the dirstatemap sense
337 349
338 350 There is no reason for any code, but the dirstatemap one to use this.
339 351 """
340 352 return self.v1_state() != b'n' or self.v1_mtime() == AMBIGUOUS_TIME
341 353
342 354 @property
343 355 def dm_otherparent(self):
344 356 """True is the entry is `otherparent` in the dirstatemap sense
345 357
346 358 There is no reason for any code, but the dirstatemap one to use this.
347 359 """
348 360 return self.v1_size() == FROM_P2
349 361
350 362 def v1_state(self):
351 363 """return a "state" suitable for v1 serialization"""
352 364 if not (self._p1_tracked or self._p2_tracked or self._wc_tracked):
353 365 # the object has no state to record, this is -currently-
354 366 # unsupported
355 367 raise RuntimeError('untracked item')
356 368 elif self.removed:
357 369 return b'r'
358 370 elif self.merged:
359 371 return b'm'
360 372 elif self.added:
361 373 return b'a'
362 374 else:
363 375 return b'n'
364 376
365 377 def v1_mode(self):
366 378 """return a "mode" suitable for v1 serialization"""
367 379 return self._mode if self._mode is not None else 0
368 380
369 381 def v1_size(self):
370 382 """return a "size" suitable for v1 serialization"""
371 383 if not (self._p1_tracked or self._p2_tracked or self._wc_tracked):
372 384 # the object has no state to record, this is -currently-
373 385 # unsupported
374 386 raise RuntimeError('untracked item')
375 387 elif self.merged_removed:
376 388 return NONNORMAL
377 389 elif self.from_p2_removed:
378 390 return FROM_P2
379 391 elif self.removed:
380 392 return 0
381 393 elif self.merged:
382 394 return FROM_P2
383 395 elif self.added:
384 396 return NONNORMAL
385 397 elif self.from_p2:
386 398 return FROM_P2
387 399 elif self._possibly_dirty:
388 400 return self._size if self._size is not None else NONNORMAL
389 401 else:
390 402 return self._size
391 403
392 404 def v1_mtime(self):
393 405 """return a "mtime" suitable for v1 serialization"""
394 406 if not (self._p1_tracked or self._p2_tracked or self._wc_tracked):
395 407 # the object has no state to record, this is -currently-
396 408 # unsupported
397 409 raise RuntimeError('untracked item')
398 410 elif self.removed:
399 411 return 0
400 412 elif self._possibly_dirty:
401 413 return AMBIGUOUS_TIME
402 414 elif self.merged:
403 415 return AMBIGUOUS_TIME
404 416 elif self.added:
405 417 return AMBIGUOUS_TIME
406 418 elif self.from_p2:
407 419 return AMBIGUOUS_TIME
408 420 else:
409 421 return self._mtime if self._mtime is not None else 0
410 422
411 423 def need_delay(self, now):
412 424 """True if the stored mtime would be ambiguous with the current time"""
413 425 return self.v1_state() == b'n' and self.v1_mtime() == now
414 426
415 427
416 428 def gettype(q):
417 429 return int(q & 0xFFFF)
418 430
419 431
420 432 class BaseIndexObject(object):
421 433 # Can I be passed to an algorithme implemented in Rust ?
422 434 rust_ext_compat = 0
423 435 # Format of an index entry according to Python's `struct` language
424 436 index_format = revlog_constants.INDEX_ENTRY_V1
425 437 # Size of a C unsigned long long int, platform independent
426 438 big_int_size = struct.calcsize(b'>Q')
427 439 # Size of a C long int, platform independent
428 440 int_size = struct.calcsize(b'>i')
429 441 # An empty index entry, used as a default value to be overridden, or nullrev
430 442 null_item = (
431 443 0,
432 444 0,
433 445 0,
434 446 -1,
435 447 -1,
436 448 -1,
437 449 -1,
438 450 sha1nodeconstants.nullid,
439 451 0,
440 452 0,
441 453 revlog_constants.COMP_MODE_INLINE,
442 454 revlog_constants.COMP_MODE_INLINE,
443 455 )
444 456
445 457 @util.propertycache
446 458 def entry_size(self):
447 459 return self.index_format.size
448 460
449 461 @property
450 462 def nodemap(self):
451 463 msg = b"index.nodemap is deprecated, use index.[has_node|rev|get_rev]"
452 464 util.nouideprecwarn(msg, b'5.3', stacklevel=2)
453 465 return self._nodemap
454 466
455 467 @util.propertycache
456 468 def _nodemap(self):
457 469 nodemap = nodemaputil.NodeMap({sha1nodeconstants.nullid: nullrev})
458 470 for r in range(0, len(self)):
459 471 n = self[r][7]
460 472 nodemap[n] = r
461 473 return nodemap
462 474
463 475 def has_node(self, node):
464 476 """return True if the node exist in the index"""
465 477 return node in self._nodemap
466 478
467 479 def rev(self, node):
468 480 """return a revision for a node
469 481
470 482 If the node is unknown, raise a RevlogError"""
471 483 return self._nodemap[node]
472 484
473 485 def get_rev(self, node):
474 486 """return a revision for a node
475 487
476 488 If the node is unknown, return None"""
477 489 return self._nodemap.get(node)
478 490
479 491 def _stripnodes(self, start):
480 492 if '_nodemap' in vars(self):
481 493 for r in range(start, len(self)):
482 494 n = self[r][7]
483 495 del self._nodemap[n]
484 496
485 497 def clearcaches(self):
486 498 self.__dict__.pop('_nodemap', None)
487 499
488 500 def __len__(self):
489 501 return self._lgt + len(self._extra)
490 502
491 503 def append(self, tup):
492 504 if '_nodemap' in vars(self):
493 505 self._nodemap[tup[7]] = len(self)
494 506 data = self._pack_entry(len(self), tup)
495 507 self._extra.append(data)
496 508
497 509 def _pack_entry(self, rev, entry):
498 510 assert entry[8] == 0
499 511 assert entry[9] == 0
500 512 return self.index_format.pack(*entry[:8])
501 513
502 514 def _check_index(self, i):
503 515 if not isinstance(i, int):
504 516 raise TypeError(b"expecting int indexes")
505 517 if i < 0 or i >= len(self):
506 518 raise IndexError
507 519
508 520 def __getitem__(self, i):
509 521 if i == -1:
510 522 return self.null_item
511 523 self._check_index(i)
512 524 if i >= self._lgt:
513 525 data = self._extra[i - self._lgt]
514 526 else:
515 527 index = self._calculate_index(i)
516 528 data = self._data[index : index + self.entry_size]
517 529 r = self._unpack_entry(i, data)
518 530 if self._lgt and i == 0:
519 531 offset = revlogutils.offset_type(0, gettype(r[0]))
520 532 r = (offset,) + r[1:]
521 533 return r
522 534
523 535 def _unpack_entry(self, rev, data):
524 536 r = self.index_format.unpack(data)
525 537 r = r + (
526 538 0,
527 539 0,
528 540 revlog_constants.COMP_MODE_INLINE,
529 541 revlog_constants.COMP_MODE_INLINE,
530 542 )
531 543 return r
532 544
533 545 def pack_header(self, header):
534 546 """pack header information as binary"""
535 547 v_fmt = revlog_constants.INDEX_HEADER
536 548 return v_fmt.pack(header)
537 549
538 550 def entry_binary(self, rev):
539 551 """return the raw binary string representing a revision"""
540 552 entry = self[rev]
541 553 p = revlog_constants.INDEX_ENTRY_V1.pack(*entry[:8])
542 554 if rev == 0:
543 555 p = p[revlog_constants.INDEX_HEADER.size :]
544 556 return p
545 557
546 558
547 559 class IndexObject(BaseIndexObject):
548 560 def __init__(self, data):
549 561 assert len(data) % self.entry_size == 0, (
550 562 len(data),
551 563 self.entry_size,
552 564 len(data) % self.entry_size,
553 565 )
554 566 self._data = data
555 567 self._lgt = len(data) // self.entry_size
556 568 self._extra = []
557 569
558 570 def _calculate_index(self, i):
559 571 return i * self.entry_size
560 572
561 573 def __delitem__(self, i):
562 574 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
563 575 raise ValueError(b"deleting slices only supports a:-1 with step 1")
564 576 i = i.start
565 577 self._check_index(i)
566 578 self._stripnodes(i)
567 579 if i < self._lgt:
568 580 self._data = self._data[: i * self.entry_size]
569 581 self._lgt = i
570 582 self._extra = []
571 583 else:
572 584 self._extra = self._extra[: i - self._lgt]
573 585
574 586
575 587 class PersistentNodeMapIndexObject(IndexObject):
576 588 """a Debug oriented class to test persistent nodemap
577 589
578 590 We need a simple python object to test API and higher level behavior. See
579 591 the Rust implementation for more serious usage. This should be used only
580 592 through the dedicated `devel.persistent-nodemap` config.
581 593 """
582 594
583 595 def nodemap_data_all(self):
584 596 """Return bytes containing a full serialization of a nodemap
585 597
586 598 The nodemap should be valid for the full set of revisions in the
587 599 index."""
588 600 return nodemaputil.persistent_data(self)
589 601
590 602 def nodemap_data_incremental(self):
591 603 """Return bytes containing a incremental update to persistent nodemap
592 604
593 605 This containst the data for an append-only update of the data provided
594 606 in the last call to `update_nodemap_data`.
595 607 """
596 608 if self._nm_root is None:
597 609 return None
598 610 docket = self._nm_docket
599 611 changed, data = nodemaputil.update_persistent_data(
600 612 self, self._nm_root, self._nm_max_idx, self._nm_docket.tip_rev
601 613 )
602 614
603 615 self._nm_root = self._nm_max_idx = self._nm_docket = None
604 616 return docket, changed, data
605 617
606 618 def update_nodemap_data(self, docket, nm_data):
607 619 """provide full block of persisted binary data for a nodemap
608 620
609 621 The data are expected to come from disk. See `nodemap_data_all` for a
610 622 produceur of such data."""
611 623 if nm_data is not None:
612 624 self._nm_root, self._nm_max_idx = nodemaputil.parse_data(nm_data)
613 625 if self._nm_root:
614 626 self._nm_docket = docket
615 627 else:
616 628 self._nm_root = self._nm_max_idx = self._nm_docket = None
617 629
618 630
619 631 class InlinedIndexObject(BaseIndexObject):
620 632 def __init__(self, data, inline=0):
621 633 self._data = data
622 634 self._lgt = self._inline_scan(None)
623 635 self._inline_scan(self._lgt)
624 636 self._extra = []
625 637
626 638 def _inline_scan(self, lgt):
627 639 off = 0
628 640 if lgt is not None:
629 641 self._offsets = [0] * lgt
630 642 count = 0
631 643 while off <= len(self._data) - self.entry_size:
632 644 start = off + self.big_int_size
633 645 (s,) = struct.unpack(
634 646 b'>i',
635 647 self._data[start : start + self.int_size],
636 648 )
637 649 if lgt is not None:
638 650 self._offsets[count] = off
639 651 count += 1
640 652 off += self.entry_size + s
641 653 if off != len(self._data):
642 654 raise ValueError(b"corrupted data")
643 655 return count
644 656
645 657 def __delitem__(self, i):
646 658 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
647 659 raise ValueError(b"deleting slices only supports a:-1 with step 1")
648 660 i = i.start
649 661 self._check_index(i)
650 662 self._stripnodes(i)
651 663 if i < self._lgt:
652 664 self._offsets = self._offsets[:i]
653 665 self._lgt = i
654 666 self._extra = []
655 667 else:
656 668 self._extra = self._extra[: i - self._lgt]
657 669
658 670 def _calculate_index(self, i):
659 671 return self._offsets[i]
660 672
661 673
662 674 def parse_index2(data, inline, revlogv2=False):
663 675 if not inline:
664 676 cls = IndexObject2 if revlogv2 else IndexObject
665 677 return cls(data), None
666 678 cls = InlinedIndexObject
667 679 return cls(data, inline), (0, data)
668 680
669 681
670 682 def parse_index_cl_v2(data):
671 683 return IndexChangelogV2(data), None
672 684
673 685
674 686 class IndexObject2(IndexObject):
675 687 index_format = revlog_constants.INDEX_ENTRY_V2
676 688
677 689 def replace_sidedata_info(
678 690 self,
679 691 rev,
680 692 sidedata_offset,
681 693 sidedata_length,
682 694 offset_flags,
683 695 compression_mode,
684 696 ):
685 697 """
686 698 Replace an existing index entry's sidedata offset and length with new
687 699 ones.
688 700 This cannot be used outside of the context of sidedata rewriting,
689 701 inside the transaction that creates the revision `rev`.
690 702 """
691 703 if rev < 0:
692 704 raise KeyError
693 705 self._check_index(rev)
694 706 if rev < self._lgt:
695 707 msg = b"cannot rewrite entries outside of this transaction"
696 708 raise KeyError(msg)
697 709 else:
698 710 entry = list(self[rev])
699 711 entry[0] = offset_flags
700 712 entry[8] = sidedata_offset
701 713 entry[9] = sidedata_length
702 714 entry[11] = compression_mode
703 715 entry = tuple(entry)
704 716 new = self._pack_entry(rev, entry)
705 717 self._extra[rev - self._lgt] = new
706 718
707 719 def _unpack_entry(self, rev, data):
708 720 data = self.index_format.unpack(data)
709 721 entry = data[:10]
710 722 data_comp = data[10] & 3
711 723 sidedata_comp = (data[10] & (3 << 2)) >> 2
712 724 return entry + (data_comp, sidedata_comp)
713 725
714 726 def _pack_entry(self, rev, entry):
715 727 data = entry[:10]
716 728 data_comp = entry[10] & 3
717 729 sidedata_comp = (entry[11] & 3) << 2
718 730 data += (data_comp | sidedata_comp,)
719 731
720 732 return self.index_format.pack(*data)
721 733
722 734 def entry_binary(self, rev):
723 735 """return the raw binary string representing a revision"""
724 736 entry = self[rev]
725 737 return self._pack_entry(rev, entry)
726 738
727 739 def pack_header(self, header):
728 740 """pack header information as binary"""
729 741 msg = 'version header should go in the docket, not the index: %d'
730 742 msg %= header
731 743 raise error.ProgrammingError(msg)
732 744
733 745
734 746 class IndexChangelogV2(IndexObject2):
735 747 index_format = revlog_constants.INDEX_ENTRY_CL_V2
736 748
737 749 def _unpack_entry(self, rev, data, r=True):
738 750 items = self.index_format.unpack(data)
739 751 entry = items[:3] + (rev, rev) + items[3:8]
740 752 data_comp = items[8] & 3
741 753 sidedata_comp = (items[8] >> 2) & 3
742 754 return entry + (data_comp, sidedata_comp)
743 755
744 756 def _pack_entry(self, rev, entry):
745 757 assert entry[3] == rev, entry[3]
746 758 assert entry[4] == rev, entry[4]
747 759 data = entry[:3] + entry[5:10]
748 760 data_comp = entry[10] & 3
749 761 sidedata_comp = (entry[11] & 3) << 2
750 762 data += (data_comp | sidedata_comp,)
751 763 return self.index_format.pack(*data)
752 764
753 765
754 766 def parse_index_devel_nodemap(data, inline):
755 767 """like parse_index2, but alway return a PersistentNodeMapIndexObject"""
756 768 return PersistentNodeMapIndexObject(data), None
757 769
758 770
759 771 def parse_dirstate(dmap, copymap, st):
760 772 parents = [st[:20], st[20:40]]
761 773 # dereference fields so they will be local in loop
762 774 format = b">cllll"
763 775 e_size = struct.calcsize(format)
764 776 pos1 = 40
765 777 l = len(st)
766 778
767 779 # the inner loop
768 780 while pos1 < l:
769 781 pos2 = pos1 + e_size
770 782 e = _unpack(b">cllll", st[pos1:pos2]) # a literal here is faster
771 783 pos1 = pos2 + e[4]
772 784 f = st[pos2:pos1]
773 785 if b'\0' in f:
774 786 f, c = f.split(b'\0')
775 787 copymap[f] = c
776 788 dmap[f] = DirstateItem.from_v1_data(*e[:4])
777 789 return parents
778 790
779 791
780 792 def pack_dirstate(dmap, copymap, pl, now):
781 793 now = int(now)
782 794 cs = stringio()
783 795 write = cs.write
784 796 write(b"".join(pl))
785 797 for f, e in pycompat.iteritems(dmap):
786 798 if e.need_delay(now):
787 799 # The file was last modified "simultaneously" with the current
788 800 # write to dirstate (i.e. within the same second for file-
789 801 # systems with a granularity of 1 sec). This commonly happens
790 802 # for at least a couple of files on 'update'.
791 803 # The user could change the file without changing its size
792 804 # within the same second. Invalidate the file's mtime in
793 805 # dirstate, forcing future 'status' calls to compare the
794 806 # contents of the file if the size is the same. This prevents
795 807 # mistakenly treating such files as clean.
796 808 e.set_possibly_dirty()
797 809
798 810 if f in copymap:
799 811 f = b"%s\0%s" % (f, copymap[f])
800 812 e = _pack(
801 813 b">cllll",
802 814 e.v1_state(),
803 815 e.v1_mode(),
804 816 e.v1_size(),
805 817 e.v1_mtime(),
806 818 len(f),
807 819 )
808 820 write(e)
809 821 write(f)
810 822 return cs.getvalue()
General Comments 0
You need to be logged in to leave comments. Login now