##// END OF EJS Templates
pathutil: replace the `skip` argument of `dirs` with a boolean...
marmoute -
r48756:e02f9af7 default
parent child Browse files
Show More
@@ -1,327 +1,329 b''
1 /*
1 /*
2 dirs.c - dynamic directory diddling for dirstates
2 dirs.c - dynamic directory diddling for dirstates
3
3
4 Copyright 2013 Facebook
4 Copyright 2013 Facebook
5
5
6 This software may be used and distributed according to the terms of
6 This software may be used and distributed according to the terms of
7 the GNU General Public License, incorporated herein by reference.
7 the GNU General Public License, incorporated herein by reference.
8 */
8 */
9
9
10 #define PY_SSIZE_T_CLEAN
10 #define PY_SSIZE_T_CLEAN
11 #include <Python.h>
11 #include <Python.h>
12 #include <string.h>
12 #include <string.h>
13
13
14 #include "util.h"
14 #include "util.h"
15
15
16 #ifdef IS_PY3K
16 #ifdef IS_PY3K
17 #define PYLONG_VALUE(o) ((PyLongObject *)o)->ob_digit[0]
17 #define PYLONG_VALUE(o) ((PyLongObject *)o)->ob_digit[0]
18 #else
18 #else
19 #define PYLONG_VALUE(o) PyInt_AS_LONG(o)
19 #define PYLONG_VALUE(o) PyInt_AS_LONG(o)
20 #endif
20 #endif
21
21
22 /*
22 /*
23 * This is a multiset of directory names, built from the files that
23 * This is a multiset of directory names, built from the files that
24 * appear in a dirstate or manifest.
24 * appear in a dirstate or manifest.
25 *
25 *
26 * A few implementation notes:
26 * A few implementation notes:
27 *
27 *
28 * We modify Python integers for refcounting, but those integers are
28 * We modify Python integers for refcounting, but those integers are
29 * never visible to Python code.
29 * never visible to Python code.
30 */
30 */
31 /* clang-format off */
31 /* clang-format off */
32 typedef struct {
32 typedef struct {
33 PyObject_HEAD
33 PyObject_HEAD
34 PyObject *dict;
34 PyObject *dict;
35 } dirsObject;
35 } dirsObject;
36 /* clang-format on */
36 /* clang-format on */
37
37
38 static inline Py_ssize_t _finddir(const char *path, Py_ssize_t pos)
38 static inline Py_ssize_t _finddir(const char *path, Py_ssize_t pos)
39 {
39 {
40 while (pos != -1) {
40 while (pos != -1) {
41 if (path[pos] == '/')
41 if (path[pos] == '/')
42 break;
42 break;
43 pos -= 1;
43 pos -= 1;
44 }
44 }
45 if (pos == -1) {
45 if (pos == -1) {
46 return 0;
46 return 0;
47 }
47 }
48
48
49 return pos;
49 return pos;
50 }
50 }
51
51
52 /* Mercurial will fail to run on directory hierarchies deeper than
52 /* Mercurial will fail to run on directory hierarchies deeper than
53 * this constant, so we should try and keep this constant as big as
53 * this constant, so we should try and keep this constant as big as
54 * possible.
54 * possible.
55 */
55 */
56 #define MAX_DIRS_DEPTH 2048
56 #define MAX_DIRS_DEPTH 2048
57
57
58 static int _addpath(PyObject *dirs, PyObject *path)
58 static int _addpath(PyObject *dirs, PyObject *path)
59 {
59 {
60 const char *cpath = PyBytes_AS_STRING(path);
60 const char *cpath = PyBytes_AS_STRING(path);
61 Py_ssize_t pos = PyBytes_GET_SIZE(path);
61 Py_ssize_t pos = PyBytes_GET_SIZE(path);
62 PyObject *key = NULL;
62 PyObject *key = NULL;
63 int ret = -1;
63 int ret = -1;
64 size_t num_slashes = 0;
64 size_t num_slashes = 0;
65
65
66 /* This loop is super critical for performance. That's why we inline
66 /* This loop is super critical for performance. That's why we inline
67 * access to Python structs instead of going through a supported API.
67 * access to Python structs instead of going through a supported API.
68 * The implementation, therefore, is heavily dependent on CPython
68 * The implementation, therefore, is heavily dependent on CPython
69 * implementation details. We also commit violations of the Python
69 * implementation details. We also commit violations of the Python
70 * "protocol" such as mutating immutable objects. But since we only
70 * "protocol" such as mutating immutable objects. But since we only
71 * mutate objects created in this function or in other well-defined
71 * mutate objects created in this function or in other well-defined
72 * locations, the references are known so these violations should go
72 * locations, the references are known so these violations should go
73 * unnoticed. */
73 * unnoticed. */
74 while ((pos = _finddir(cpath, pos - 1)) != -1) {
74 while ((pos = _finddir(cpath, pos - 1)) != -1) {
75 PyObject *val;
75 PyObject *val;
76 ++num_slashes;
76 ++num_slashes;
77 if (num_slashes > MAX_DIRS_DEPTH) {
77 if (num_slashes > MAX_DIRS_DEPTH) {
78 PyErr_SetString(PyExc_ValueError,
78 PyErr_SetString(PyExc_ValueError,
79 "Directory hierarchy too deep.");
79 "Directory hierarchy too deep.");
80 goto bail;
80 goto bail;
81 }
81 }
82
82
83 /* Sniff for trailing slashes, a marker of an invalid input. */
83 /* Sniff for trailing slashes, a marker of an invalid input. */
84 if (pos > 0 && cpath[pos - 1] == '/') {
84 if (pos > 0 && cpath[pos - 1] == '/') {
85 PyErr_SetString(
85 PyErr_SetString(
86 PyExc_ValueError,
86 PyExc_ValueError,
87 "found invalid consecutive slashes in path");
87 "found invalid consecutive slashes in path");
88 goto bail;
88 goto bail;
89 }
89 }
90
90
91 key = PyBytes_FromStringAndSize(cpath, pos);
91 key = PyBytes_FromStringAndSize(cpath, pos);
92 if (key == NULL)
92 if (key == NULL)
93 goto bail;
93 goto bail;
94
94
95 val = PyDict_GetItem(dirs, key);
95 val = PyDict_GetItem(dirs, key);
96 if (val != NULL) {
96 if (val != NULL) {
97 PYLONG_VALUE(val) += 1;
97 PYLONG_VALUE(val) += 1;
98 Py_CLEAR(key);
98 Py_CLEAR(key);
99 break;
99 break;
100 }
100 }
101
101
102 /* Force Python to not reuse a small shared int. */
102 /* Force Python to not reuse a small shared int. */
103 #ifdef IS_PY3K
103 #ifdef IS_PY3K
104 val = PyLong_FromLong(0x1eadbeef);
104 val = PyLong_FromLong(0x1eadbeef);
105 #else
105 #else
106 val = PyInt_FromLong(0x1eadbeef);
106 val = PyInt_FromLong(0x1eadbeef);
107 #endif
107 #endif
108
108
109 if (val == NULL)
109 if (val == NULL)
110 goto bail;
110 goto bail;
111
111
112 PYLONG_VALUE(val) = 1;
112 PYLONG_VALUE(val) = 1;
113 ret = PyDict_SetItem(dirs, key, val);
113 ret = PyDict_SetItem(dirs, key, val);
114 Py_DECREF(val);
114 Py_DECREF(val);
115 if (ret == -1)
115 if (ret == -1)
116 goto bail;
116 goto bail;
117 Py_CLEAR(key);
117 Py_CLEAR(key);
118 }
118 }
119 ret = 0;
119 ret = 0;
120
120
121 bail:
121 bail:
122 Py_XDECREF(key);
122 Py_XDECREF(key);
123
123
124 return ret;
124 return ret;
125 }
125 }
126
126
127 static int _delpath(PyObject *dirs, PyObject *path)
127 static int _delpath(PyObject *dirs, PyObject *path)
128 {
128 {
129 char *cpath = PyBytes_AS_STRING(path);
129 char *cpath = PyBytes_AS_STRING(path);
130 Py_ssize_t pos = PyBytes_GET_SIZE(path);
130 Py_ssize_t pos = PyBytes_GET_SIZE(path);
131 PyObject *key = NULL;
131 PyObject *key = NULL;
132 int ret = -1;
132 int ret = -1;
133
133
134 while ((pos = _finddir(cpath, pos - 1)) != -1) {
134 while ((pos = _finddir(cpath, pos - 1)) != -1) {
135 PyObject *val;
135 PyObject *val;
136
136
137 key = PyBytes_FromStringAndSize(cpath, pos);
137 key = PyBytes_FromStringAndSize(cpath, pos);
138
138
139 if (key == NULL)
139 if (key == NULL)
140 goto bail;
140 goto bail;
141
141
142 val = PyDict_GetItem(dirs, key);
142 val = PyDict_GetItem(dirs, key);
143 if (val == NULL) {
143 if (val == NULL) {
144 PyErr_SetString(PyExc_ValueError,
144 PyErr_SetString(PyExc_ValueError,
145 "expected a value, found none");
145 "expected a value, found none");
146 goto bail;
146 goto bail;
147 }
147 }
148
148
149 if (--PYLONG_VALUE(val) <= 0) {
149 if (--PYLONG_VALUE(val) <= 0) {
150 if (PyDict_DelItem(dirs, key) == -1)
150 if (PyDict_DelItem(dirs, key) == -1)
151 goto bail;
151 goto bail;
152 } else
152 } else
153 break;
153 break;
154 Py_CLEAR(key);
154 Py_CLEAR(key);
155 }
155 }
156 ret = 0;
156 ret = 0;
157
157
158 bail:
158 bail:
159 Py_XDECREF(key);
159 Py_XDECREF(key);
160
160
161 return ret;
161 return ret;
162 }
162 }
163
163
164 static int dirs_fromdict(PyObject *dirs, PyObject *source, char skipchar)
164 static int dirs_fromdict(PyObject *dirs, PyObject *source, bool only_tracked)
165 {
165 {
166 PyObject *key, *value;
166 PyObject *key, *value;
167 Py_ssize_t pos = 0;
167 Py_ssize_t pos = 0;
168
168
169 while (PyDict_Next(source, &pos, &key, &value)) {
169 while (PyDict_Next(source, &pos, &key, &value)) {
170 if (!PyBytes_Check(key)) {
170 if (!PyBytes_Check(key)) {
171 PyErr_SetString(PyExc_TypeError, "expected string key");
171 PyErr_SetString(PyExc_TypeError, "expected string key");
172 return -1;
172 return -1;
173 }
173 }
174 if (skipchar) {
174 if (only_tracked) {
175 if (!dirstate_tuple_check(value)) {
175 if (!dirstate_tuple_check(value)) {
176 PyErr_SetString(PyExc_TypeError,
176 PyErr_SetString(PyExc_TypeError,
177 "expected a dirstate tuple");
177 "expected a dirstate tuple");
178 return -1;
178 return -1;
179 }
179 }
180 if (((dirstateItemObject *)value)->state == skipchar)
180 if (((dirstateItemObject *)value)->state == 'r')
181 continue;
181 continue;
182 }
182 }
183
183
184 if (_addpath(dirs, key) == -1)
184 if (_addpath(dirs, key) == -1)
185 return -1;
185 return -1;
186 }
186 }
187
187
188 return 0;
188 return 0;
189 }
189 }
190
190
191 static int dirs_fromiter(PyObject *dirs, PyObject *source)
191 static int dirs_fromiter(PyObject *dirs, PyObject *source)
192 {
192 {
193 PyObject *iter, *item = NULL;
193 PyObject *iter, *item = NULL;
194 int ret;
194 int ret;
195
195
196 iter = PyObject_GetIter(source);
196 iter = PyObject_GetIter(source);
197 if (iter == NULL)
197 if (iter == NULL)
198 return -1;
198 return -1;
199
199
200 while ((item = PyIter_Next(iter)) != NULL) {
200 while ((item = PyIter_Next(iter)) != NULL) {
201 if (!PyBytes_Check(item)) {
201 if (!PyBytes_Check(item)) {
202 PyErr_SetString(PyExc_TypeError, "expected string");
202 PyErr_SetString(PyExc_TypeError, "expected string");
203 break;
203 break;
204 }
204 }
205
205
206 if (_addpath(dirs, item) == -1)
206 if (_addpath(dirs, item) == -1)
207 break;
207 break;
208 Py_CLEAR(item);
208 Py_CLEAR(item);
209 }
209 }
210
210
211 ret = PyErr_Occurred() ? -1 : 0;
211 ret = PyErr_Occurred() ? -1 : 0;
212 Py_DECREF(iter);
212 Py_DECREF(iter);
213 Py_XDECREF(item);
213 Py_XDECREF(item);
214 return ret;
214 return ret;
215 }
215 }
216
216
217 /*
217 /*
218 * Calculate a refcounted set of directory names for the files in a
218 * Calculate a refcounted set of directory names for the files in a
219 * dirstate.
219 * dirstate.
220 */
220 */
221 static int dirs_init(dirsObject *self, PyObject *args)
221 static int dirs_init(dirsObject *self, PyObject *args, PyObject *kwargs)
222 {
222 {
223 PyObject *dirs = NULL, *source = NULL;
223 PyObject *dirs = NULL, *source = NULL;
224 char skipchar = 0;
224 int only_tracked = 0;
225 int ret = -1;
225 int ret = -1;
226 static char *keywords_name[] = {"map", "only_tracked", NULL};
226
227
227 self->dict = NULL;
228 self->dict = NULL;
228
229
229 if (!PyArg_ParseTuple(args, "|Oc:__init__", &source, &skipchar))
230 if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Oi:__init__",
231 keywords_name, &source, &only_tracked))
230 return -1;
232 return -1;
231
233
232 dirs = PyDict_New();
234 dirs = PyDict_New();
233
235
234 if (dirs == NULL)
236 if (dirs == NULL)
235 return -1;
237 return -1;
236
238
237 if (source == NULL)
239 if (source == NULL)
238 ret = 0;
240 ret = 0;
239 else if (PyDict_Check(source))
241 else if (PyDict_Check(source))
240 ret = dirs_fromdict(dirs, source, skipchar);
242 ret = dirs_fromdict(dirs, source, (bool)only_tracked);
241 else if (skipchar)
243 else if (only_tracked)
242 PyErr_SetString(PyExc_ValueError,
244 PyErr_SetString(PyExc_ValueError,
243 "skip character is only supported "
245 "`only_tracked` is only supported "
244 "with a dict source");
246 "with a dict source");
245 else
247 else
246 ret = dirs_fromiter(dirs, source);
248 ret = dirs_fromiter(dirs, source);
247
249
248 if (ret == -1)
250 if (ret == -1)
249 Py_XDECREF(dirs);
251 Py_XDECREF(dirs);
250 else
252 else
251 self->dict = dirs;
253 self->dict = dirs;
252
254
253 return ret;
255 return ret;
254 }
256 }
255
257
256 PyObject *dirs_addpath(dirsObject *self, PyObject *args)
258 PyObject *dirs_addpath(dirsObject *self, PyObject *args)
257 {
259 {
258 PyObject *path;
260 PyObject *path;
259
261
260 if (!PyArg_ParseTuple(args, "O!:addpath", &PyBytes_Type, &path))
262 if (!PyArg_ParseTuple(args, "O!:addpath", &PyBytes_Type, &path))
261 return NULL;
263 return NULL;
262
264
263 if (_addpath(self->dict, path) == -1)
265 if (_addpath(self->dict, path) == -1)
264 return NULL;
266 return NULL;
265
267
266 Py_RETURN_NONE;
268 Py_RETURN_NONE;
267 }
269 }
268
270
269 static PyObject *dirs_delpath(dirsObject *self, PyObject *args)
271 static PyObject *dirs_delpath(dirsObject *self, PyObject *args)
270 {
272 {
271 PyObject *path;
273 PyObject *path;
272
274
273 if (!PyArg_ParseTuple(args, "O!:delpath", &PyBytes_Type, &path))
275 if (!PyArg_ParseTuple(args, "O!:delpath", &PyBytes_Type, &path))
274 return NULL;
276 return NULL;
275
277
276 if (_delpath(self->dict, path) == -1)
278 if (_delpath(self->dict, path) == -1)
277 return NULL;
279 return NULL;
278
280
279 Py_RETURN_NONE;
281 Py_RETURN_NONE;
280 }
282 }
281
283
282 static int dirs_contains(dirsObject *self, PyObject *value)
284 static int dirs_contains(dirsObject *self, PyObject *value)
283 {
285 {
284 return PyBytes_Check(value) ? PyDict_Contains(self->dict, value) : 0;
286 return PyBytes_Check(value) ? PyDict_Contains(self->dict, value) : 0;
285 }
287 }
286
288
287 static void dirs_dealloc(dirsObject *self)
289 static void dirs_dealloc(dirsObject *self)
288 {
290 {
289 Py_XDECREF(self->dict);
291 Py_XDECREF(self->dict);
290 PyObject_Del(self);
292 PyObject_Del(self);
291 }
293 }
292
294
293 static PyObject *dirs_iter(dirsObject *self)
295 static PyObject *dirs_iter(dirsObject *self)
294 {
296 {
295 return PyObject_GetIter(self->dict);
297 return PyObject_GetIter(self->dict);
296 }
298 }
297
299
298 static PySequenceMethods dirs_sequence_methods;
300 static PySequenceMethods dirs_sequence_methods;
299
301
300 static PyMethodDef dirs_methods[] = {
302 static PyMethodDef dirs_methods[] = {
301 {"addpath", (PyCFunction)dirs_addpath, METH_VARARGS, "add a path"},
303 {"addpath", (PyCFunction)dirs_addpath, METH_VARARGS, "add a path"},
302 {"delpath", (PyCFunction)dirs_delpath, METH_VARARGS, "remove a path"},
304 {"delpath", (PyCFunction)dirs_delpath, METH_VARARGS, "remove a path"},
303 {NULL} /* Sentinel */
305 {NULL} /* Sentinel */
304 };
306 };
305
307
306 static PyTypeObject dirsType = {PyVarObject_HEAD_INIT(NULL, 0)};
308 static PyTypeObject dirsType = {PyVarObject_HEAD_INIT(NULL, 0)};
307
309
308 void dirs_module_init(PyObject *mod)
310 void dirs_module_init(PyObject *mod)
309 {
311 {
310 dirs_sequence_methods.sq_contains = (objobjproc)dirs_contains;
312 dirs_sequence_methods.sq_contains = (objobjproc)dirs_contains;
311 dirsType.tp_name = "parsers.dirs";
313 dirsType.tp_name = "parsers.dirs";
312 dirsType.tp_new = PyType_GenericNew;
314 dirsType.tp_new = PyType_GenericNew;
313 dirsType.tp_basicsize = sizeof(dirsObject);
315 dirsType.tp_basicsize = sizeof(dirsObject);
314 dirsType.tp_dealloc = (destructor)dirs_dealloc;
316 dirsType.tp_dealloc = (destructor)dirs_dealloc;
315 dirsType.tp_as_sequence = &dirs_sequence_methods;
317 dirsType.tp_as_sequence = &dirs_sequence_methods;
316 dirsType.tp_flags = Py_TPFLAGS_DEFAULT;
318 dirsType.tp_flags = Py_TPFLAGS_DEFAULT;
317 dirsType.tp_doc = "dirs";
319 dirsType.tp_doc = "dirs";
318 dirsType.tp_iter = (getiterfunc)dirs_iter;
320 dirsType.tp_iter = (getiterfunc)dirs_iter;
319 dirsType.tp_methods = dirs_methods;
321 dirsType.tp_methods = dirs_methods;
320 dirsType.tp_init = (initproc)dirs_init;
322 dirsType.tp_init = (initproc)dirs_init;
321
323
322 if (PyType_Ready(&dirsType) < 0)
324 if (PyType_Ready(&dirsType) < 0)
323 return;
325 return;
324 Py_INCREF(&dirsType);
326 Py_INCREF(&dirsType);
325
327
326 PyModule_AddObject(mod, "dirs", (PyObject *)&dirsType);
328 PyModule_AddObject(mod, "dirs", (PyObject *)&dirsType);
327 }
329 }
@@ -1,910 +1,910 b''
1 # dirstatemap.py
1 # dirstatemap.py
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 from __future__ import absolute_import
6 from __future__ import absolute_import
7
7
8 import errno
8 import errno
9
9
10 from .i18n import _
10 from .i18n import _
11
11
12 from . import (
12 from . import (
13 error,
13 error,
14 pathutil,
14 pathutil,
15 policy,
15 policy,
16 pycompat,
16 pycompat,
17 txnutil,
17 txnutil,
18 util,
18 util,
19 )
19 )
20
20
21 from .dirstateutils import (
21 from .dirstateutils import (
22 docket as docketmod,
22 docket as docketmod,
23 )
23 )
24
24
25 parsers = policy.importmod('parsers')
25 parsers = policy.importmod('parsers')
26 rustmod = policy.importrust('dirstate')
26 rustmod = policy.importrust('dirstate')
27
27
28 propertycache = util.propertycache
28 propertycache = util.propertycache
29
29
30 DirstateItem = parsers.DirstateItem
30 DirstateItem = parsers.DirstateItem
31
31
32 rangemask = 0x7FFFFFFF
32 rangemask = 0x7FFFFFFF
33
33
34
34
35 class dirstatemap(object):
35 class dirstatemap(object):
36 """Map encapsulating the dirstate's contents.
36 """Map encapsulating the dirstate's contents.
37
37
38 The dirstate contains the following state:
38 The dirstate contains the following state:
39
39
40 - `identity` is the identity of the dirstate file, which can be used to
40 - `identity` is the identity of the dirstate file, which can be used to
41 detect when changes have occurred to the dirstate file.
41 detect when changes have occurred to the dirstate file.
42
42
43 - `parents` is a pair containing the parents of the working copy. The
43 - `parents` is a pair containing the parents of the working copy. The
44 parents are updated by calling `setparents`.
44 parents are updated by calling `setparents`.
45
45
46 - the state map maps filenames to tuples of (state, mode, size, mtime),
46 - the state map maps filenames to tuples of (state, mode, size, mtime),
47 where state is a single character representing 'normal', 'added',
47 where state is a single character representing 'normal', 'added',
48 'removed', or 'merged'. It is read by treating the dirstate as a
48 'removed', or 'merged'. It is read by treating the dirstate as a
49 dict. File state is updated by calling the `addfile`, `removefile` and
49 dict. File state is updated by calling the `addfile`, `removefile` and
50 `dropfile` methods.
50 `dropfile` methods.
51
51
52 - `copymap` maps destination filenames to their source filename.
52 - `copymap` maps destination filenames to their source filename.
53
53
54 The dirstate also provides the following views onto the state:
54 The dirstate also provides the following views onto the state:
55
55
56 - `nonnormalset` is a set of the filenames that have state other
56 - `nonnormalset` is a set of the filenames that have state other
57 than 'normal', or are normal but have an mtime of -1 ('normallookup').
57 than 'normal', or are normal but have an mtime of -1 ('normallookup').
58
58
59 - `otherparentset` is a set of the filenames that are marked as coming
59 - `otherparentset` is a set of the filenames that are marked as coming
60 from the second parent when the dirstate is currently being merged.
60 from the second parent when the dirstate is currently being merged.
61
61
62 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
62 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
63 form that they appear as in the dirstate.
63 form that they appear as in the dirstate.
64
64
65 - `dirfoldmap` is a dict mapping normalized directory names to the
65 - `dirfoldmap` is a dict mapping normalized directory names to the
66 denormalized form that they appear as in the dirstate.
66 denormalized form that they appear as in the dirstate.
67 """
67 """
68
68
69 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
69 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
70 self._ui = ui
70 self._ui = ui
71 self._opener = opener
71 self._opener = opener
72 self._root = root
72 self._root = root
73 self._filename = b'dirstate'
73 self._filename = b'dirstate'
74 self._nodelen = 20
74 self._nodelen = 20
75 self._nodeconstants = nodeconstants
75 self._nodeconstants = nodeconstants
76 assert (
76 assert (
77 not use_dirstate_v2
77 not use_dirstate_v2
78 ), "should have detected unsupported requirement"
78 ), "should have detected unsupported requirement"
79
79
80 self._parents = None
80 self._parents = None
81 self._dirtyparents = False
81 self._dirtyparents = False
82
82
83 # for consistent view between _pl() and _read() invocations
83 # for consistent view between _pl() and _read() invocations
84 self._pendingmode = None
84 self._pendingmode = None
85
85
86 @propertycache
86 @propertycache
87 def _map(self):
87 def _map(self):
88 self._map = {}
88 self._map = {}
89 self.read()
89 self.read()
90 return self._map
90 return self._map
91
91
92 @propertycache
92 @propertycache
93 def copymap(self):
93 def copymap(self):
94 self.copymap = {}
94 self.copymap = {}
95 self._map
95 self._map
96 return self.copymap
96 return self.copymap
97
97
98 def clear(self):
98 def clear(self):
99 self._map.clear()
99 self._map.clear()
100 self.copymap.clear()
100 self.copymap.clear()
101 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
101 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
102 util.clearcachedproperty(self, b"_dirs")
102 util.clearcachedproperty(self, b"_dirs")
103 util.clearcachedproperty(self, b"_alldirs")
103 util.clearcachedproperty(self, b"_alldirs")
104 util.clearcachedproperty(self, b"filefoldmap")
104 util.clearcachedproperty(self, b"filefoldmap")
105 util.clearcachedproperty(self, b"dirfoldmap")
105 util.clearcachedproperty(self, b"dirfoldmap")
106 util.clearcachedproperty(self, b"nonnormalset")
106 util.clearcachedproperty(self, b"nonnormalset")
107 util.clearcachedproperty(self, b"otherparentset")
107 util.clearcachedproperty(self, b"otherparentset")
108
108
109 def items(self):
109 def items(self):
110 return pycompat.iteritems(self._map)
110 return pycompat.iteritems(self._map)
111
111
112 # forward for python2,3 compat
112 # forward for python2,3 compat
113 iteritems = items
113 iteritems = items
114
114
115 debug_iter = items
115 debug_iter = items
116
116
117 def __len__(self):
117 def __len__(self):
118 return len(self._map)
118 return len(self._map)
119
119
120 def __iter__(self):
120 def __iter__(self):
121 return iter(self._map)
121 return iter(self._map)
122
122
123 def get(self, key, default=None):
123 def get(self, key, default=None):
124 return self._map.get(key, default)
124 return self._map.get(key, default)
125
125
126 def __contains__(self, key):
126 def __contains__(self, key):
127 return key in self._map
127 return key in self._map
128
128
129 def __getitem__(self, key):
129 def __getitem__(self, key):
130 return self._map[key]
130 return self._map[key]
131
131
132 def keys(self):
132 def keys(self):
133 return self._map.keys()
133 return self._map.keys()
134
134
135 def preload(self):
135 def preload(self):
136 """Loads the underlying data, if it's not already loaded"""
136 """Loads the underlying data, if it's not already loaded"""
137 self._map
137 self._map
138
138
139 def _dirs_incr(self, filename, old_entry=None):
139 def _dirs_incr(self, filename, old_entry=None):
140 """incremente the dirstate counter if applicable"""
140 """incremente the dirstate counter if applicable"""
141 if (
141 if (
142 old_entry is None or old_entry.removed
142 old_entry is None or old_entry.removed
143 ) and "_dirs" in self.__dict__:
143 ) and "_dirs" in self.__dict__:
144 self._dirs.addpath(filename)
144 self._dirs.addpath(filename)
145 if old_entry is None and "_alldirs" in self.__dict__:
145 if old_entry is None and "_alldirs" in self.__dict__:
146 self._alldirs.addpath(filename)
146 self._alldirs.addpath(filename)
147
147
148 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
148 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
149 """decremente the dirstate counter if applicable"""
149 """decremente the dirstate counter if applicable"""
150 if old_entry is not None:
150 if old_entry is not None:
151 if "_dirs" in self.__dict__ and not old_entry.removed:
151 if "_dirs" in self.__dict__ and not old_entry.removed:
152 self._dirs.delpath(filename)
152 self._dirs.delpath(filename)
153 if "_alldirs" in self.__dict__ and not remove_variant:
153 if "_alldirs" in self.__dict__ and not remove_variant:
154 self._alldirs.delpath(filename)
154 self._alldirs.delpath(filename)
155 elif remove_variant and "_alldirs" in self.__dict__:
155 elif remove_variant and "_alldirs" in self.__dict__:
156 self._alldirs.addpath(filename)
156 self._alldirs.addpath(filename)
157 if "filefoldmap" in self.__dict__:
157 if "filefoldmap" in self.__dict__:
158 normed = util.normcase(filename)
158 normed = util.normcase(filename)
159 self.filefoldmap.pop(normed, None)
159 self.filefoldmap.pop(normed, None)
160
160
161 def set_possibly_dirty(self, filename):
161 def set_possibly_dirty(self, filename):
162 """record that the current state of the file on disk is unknown"""
162 """record that the current state of the file on disk is unknown"""
163 self[filename].set_possibly_dirty()
163 self[filename].set_possibly_dirty()
164
164
165 def addfile(
165 def addfile(
166 self,
166 self,
167 f,
167 f,
168 mode=0,
168 mode=0,
169 size=None,
169 size=None,
170 mtime=None,
170 mtime=None,
171 added=False,
171 added=False,
172 merged=False,
172 merged=False,
173 from_p2=False,
173 from_p2=False,
174 possibly_dirty=False,
174 possibly_dirty=False,
175 ):
175 ):
176 """Add a tracked file to the dirstate."""
176 """Add a tracked file to the dirstate."""
177 if added:
177 if added:
178 assert not merged
178 assert not merged
179 assert not possibly_dirty
179 assert not possibly_dirty
180 assert not from_p2
180 assert not from_p2
181 new_entry = DirstateItem.new_added()
181 new_entry = DirstateItem.new_added()
182 elif merged:
182 elif merged:
183 assert not possibly_dirty
183 assert not possibly_dirty
184 assert not from_p2
184 assert not from_p2
185 new_entry = DirstateItem.new_merged()
185 new_entry = DirstateItem.new_merged()
186 elif from_p2:
186 elif from_p2:
187 assert not possibly_dirty
187 assert not possibly_dirty
188 new_entry = DirstateItem.new_from_p2()
188 new_entry = DirstateItem.new_from_p2()
189 elif possibly_dirty:
189 elif possibly_dirty:
190 new_entry = DirstateItem.new_possibly_dirty()
190 new_entry = DirstateItem.new_possibly_dirty()
191 else:
191 else:
192 assert size is not None
192 assert size is not None
193 assert mtime is not None
193 assert mtime is not None
194 size = size & rangemask
194 size = size & rangemask
195 mtime = mtime & rangemask
195 mtime = mtime & rangemask
196 new_entry = DirstateItem.new_normal(mode, size, mtime)
196 new_entry = DirstateItem.new_normal(mode, size, mtime)
197 old_entry = self.get(f)
197 old_entry = self.get(f)
198 self._dirs_incr(f, old_entry)
198 self._dirs_incr(f, old_entry)
199 self._map[f] = new_entry
199 self._map[f] = new_entry
200 if new_entry.dm_nonnormal:
200 if new_entry.dm_nonnormal:
201 self.nonnormalset.add(f)
201 self.nonnormalset.add(f)
202 else:
202 else:
203 self.nonnormalset.discard(f)
203 self.nonnormalset.discard(f)
204 if new_entry.dm_otherparent:
204 if new_entry.dm_otherparent:
205 self.otherparentset.add(f)
205 self.otherparentset.add(f)
206 else:
206 else:
207 self.otherparentset.discard(f)
207 self.otherparentset.discard(f)
208
208
209 def reset_state(
209 def reset_state(
210 self,
210 self,
211 filename,
211 filename,
212 wc_tracked,
212 wc_tracked,
213 p1_tracked,
213 p1_tracked,
214 p2_tracked=False,
214 p2_tracked=False,
215 merged=False,
215 merged=False,
216 clean_p1=False,
216 clean_p1=False,
217 clean_p2=False,
217 clean_p2=False,
218 possibly_dirty=False,
218 possibly_dirty=False,
219 parentfiledata=None,
219 parentfiledata=None,
220 ):
220 ):
221 """Set a entry to a given state, diregarding all previous state
221 """Set a entry to a given state, diregarding all previous state
222
222
223 This is to be used by the part of the dirstate API dedicated to
223 This is to be used by the part of the dirstate API dedicated to
224 adjusting the dirstate after a update/merge.
224 adjusting the dirstate after a update/merge.
225
225
226 note: calling this might result to no entry existing at all if the
226 note: calling this might result to no entry existing at all if the
227 dirstate map does not see any point at having one for this file
227 dirstate map does not see any point at having one for this file
228 anymore.
228 anymore.
229 """
229 """
230 if merged and (clean_p1 or clean_p2):
230 if merged and (clean_p1 or clean_p2):
231 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
231 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
232 raise error.ProgrammingError(msg)
232 raise error.ProgrammingError(msg)
233 # copy information are now outdated
233 # copy information are now outdated
234 # (maybe new information should be in directly passed to this function)
234 # (maybe new information should be in directly passed to this function)
235 self.copymap.pop(filename, None)
235 self.copymap.pop(filename, None)
236
236
237 if not (p1_tracked or p2_tracked or wc_tracked):
237 if not (p1_tracked or p2_tracked or wc_tracked):
238 self.dropfile(filename)
238 self.dropfile(filename)
239 return
239 return
240 elif merged:
240 elif merged:
241 # XXX might be merged and removed ?
241 # XXX might be merged and removed ?
242 entry = self.get(filename)
242 entry = self.get(filename)
243 if entry is None or not entry.tracked:
243 if entry is None or not entry.tracked:
244 # XXX mostly replicate dirstate.other parent. We should get
244 # XXX mostly replicate dirstate.other parent. We should get
245 # the higher layer to pass us more reliable data where `merged`
245 # the higher layer to pass us more reliable data where `merged`
246 # actually mean merged. Dropping this clause will show failure
246 # actually mean merged. Dropping this clause will show failure
247 # in `test-graft.t`
247 # in `test-graft.t`
248 merged = False
248 merged = False
249 clean_p2 = True
249 clean_p2 = True
250 elif not (p1_tracked or p2_tracked) and wc_tracked:
250 elif not (p1_tracked or p2_tracked) and wc_tracked:
251 pass # file is added, nothing special to adjust
251 pass # file is added, nothing special to adjust
252 elif (p1_tracked or p2_tracked) and not wc_tracked:
252 elif (p1_tracked or p2_tracked) and not wc_tracked:
253 pass
253 pass
254 elif clean_p2 and wc_tracked:
254 elif clean_p2 and wc_tracked:
255 if p1_tracked or self.get(filename) is not None:
255 if p1_tracked or self.get(filename) is not None:
256 # XXX the `self.get` call is catching some case in
256 # XXX the `self.get` call is catching some case in
257 # `test-merge-remove.t` where the file is tracked in p1, the
257 # `test-merge-remove.t` where the file is tracked in p1, the
258 # p1_tracked argument is False.
258 # p1_tracked argument is False.
259 #
259 #
260 # In addition, this seems to be a case where the file is marked
260 # In addition, this seems to be a case where the file is marked
261 # as merged without actually being the result of a merge
261 # as merged without actually being the result of a merge
262 # action. So thing are not ideal here.
262 # action. So thing are not ideal here.
263 merged = True
263 merged = True
264 clean_p2 = False
264 clean_p2 = False
265 elif not p1_tracked and p2_tracked and wc_tracked:
265 elif not p1_tracked and p2_tracked and wc_tracked:
266 clean_p2 = True
266 clean_p2 = True
267 elif possibly_dirty:
267 elif possibly_dirty:
268 pass
268 pass
269 elif wc_tracked:
269 elif wc_tracked:
270 # this is a "normal" file
270 # this is a "normal" file
271 if parentfiledata is None:
271 if parentfiledata is None:
272 msg = b'failed to pass parentfiledata for a normal file: %s'
272 msg = b'failed to pass parentfiledata for a normal file: %s'
273 msg %= filename
273 msg %= filename
274 raise error.ProgrammingError(msg)
274 raise error.ProgrammingError(msg)
275 else:
275 else:
276 assert False, 'unreachable'
276 assert False, 'unreachable'
277
277
278 old_entry = self._map.get(filename)
278 old_entry = self._map.get(filename)
279 self._dirs_incr(filename, old_entry)
279 self._dirs_incr(filename, old_entry)
280 entry = DirstateItem(
280 entry = DirstateItem(
281 wc_tracked=wc_tracked,
281 wc_tracked=wc_tracked,
282 p1_tracked=p1_tracked,
282 p1_tracked=p1_tracked,
283 p2_tracked=p2_tracked,
283 p2_tracked=p2_tracked,
284 merged=merged,
284 merged=merged,
285 clean_p1=clean_p1,
285 clean_p1=clean_p1,
286 clean_p2=clean_p2,
286 clean_p2=clean_p2,
287 possibly_dirty=possibly_dirty,
287 possibly_dirty=possibly_dirty,
288 parentfiledata=parentfiledata,
288 parentfiledata=parentfiledata,
289 )
289 )
290 if entry.dm_nonnormal:
290 if entry.dm_nonnormal:
291 self.nonnormalset.add(filename)
291 self.nonnormalset.add(filename)
292 else:
292 else:
293 self.nonnormalset.discard(filename)
293 self.nonnormalset.discard(filename)
294 if entry.dm_otherparent:
294 if entry.dm_otherparent:
295 self.otherparentset.add(filename)
295 self.otherparentset.add(filename)
296 else:
296 else:
297 self.otherparentset.discard(filename)
297 self.otherparentset.discard(filename)
298 self._map[filename] = entry
298 self._map[filename] = entry
299
299
300 def set_untracked(self, f):
300 def set_untracked(self, f):
301 """Mark a file as no longer tracked in the dirstate map"""
301 """Mark a file as no longer tracked in the dirstate map"""
302 entry = self[f]
302 entry = self[f]
303 self._dirs_decr(f, old_entry=entry, remove_variant=True)
303 self._dirs_decr(f, old_entry=entry, remove_variant=True)
304 if entry.from_p2:
304 if entry.from_p2:
305 self.otherparentset.add(f)
305 self.otherparentset.add(f)
306 elif not entry.merged:
306 elif not entry.merged:
307 self.copymap.pop(f, None)
307 self.copymap.pop(f, None)
308 entry.set_untracked()
308 entry.set_untracked()
309 self.nonnormalset.add(f)
309 self.nonnormalset.add(f)
310
310
311 def dropfile(self, f):
311 def dropfile(self, f):
312 """
312 """
313 Remove a file from the dirstate. Returns True if the file was
313 Remove a file from the dirstate. Returns True if the file was
314 previously recorded.
314 previously recorded.
315 """
315 """
316 old_entry = self._map.pop(f, None)
316 old_entry = self._map.pop(f, None)
317 self._dirs_decr(f, old_entry=old_entry)
317 self._dirs_decr(f, old_entry=old_entry)
318 self.nonnormalset.discard(f)
318 self.nonnormalset.discard(f)
319 return old_entry is not None
319 return old_entry is not None
320
320
321 def clearambiguoustimes(self, files, now):
321 def clearambiguoustimes(self, files, now):
322 for f in files:
322 for f in files:
323 e = self.get(f)
323 e = self.get(f)
324 if e is not None and e.need_delay(now):
324 if e is not None and e.need_delay(now):
325 e.set_possibly_dirty()
325 e.set_possibly_dirty()
326 self.nonnormalset.add(f)
326 self.nonnormalset.add(f)
327
327
328 def nonnormalentries(self):
328 def nonnormalentries(self):
329 '''Compute the nonnormal dirstate entries from the dmap'''
329 '''Compute the nonnormal dirstate entries from the dmap'''
330 try:
330 try:
331 return parsers.nonnormalotherparententries(self._map)
331 return parsers.nonnormalotherparententries(self._map)
332 except AttributeError:
332 except AttributeError:
333 nonnorm = set()
333 nonnorm = set()
334 otherparent = set()
334 otherparent = set()
335 for fname, e in pycompat.iteritems(self._map):
335 for fname, e in pycompat.iteritems(self._map):
336 if e.dm_nonnormal:
336 if e.dm_nonnormal:
337 nonnorm.add(fname)
337 nonnorm.add(fname)
338 if e.from_p2:
338 if e.from_p2:
339 otherparent.add(fname)
339 otherparent.add(fname)
340 return nonnorm, otherparent
340 return nonnorm, otherparent
341
341
342 @propertycache
342 @propertycache
343 def filefoldmap(self):
343 def filefoldmap(self):
344 """Returns a dictionary mapping normalized case paths to their
344 """Returns a dictionary mapping normalized case paths to their
345 non-normalized versions.
345 non-normalized versions.
346 """
346 """
347 try:
347 try:
348 makefilefoldmap = parsers.make_file_foldmap
348 makefilefoldmap = parsers.make_file_foldmap
349 except AttributeError:
349 except AttributeError:
350 pass
350 pass
351 else:
351 else:
352 return makefilefoldmap(
352 return makefilefoldmap(
353 self._map, util.normcasespec, util.normcasefallback
353 self._map, util.normcasespec, util.normcasefallback
354 )
354 )
355
355
356 f = {}
356 f = {}
357 normcase = util.normcase
357 normcase = util.normcase
358 for name, s in pycompat.iteritems(self._map):
358 for name, s in pycompat.iteritems(self._map):
359 if not s.removed:
359 if not s.removed:
360 f[normcase(name)] = name
360 f[normcase(name)] = name
361 f[b'.'] = b'.' # prevents useless util.fspath() invocation
361 f[b'.'] = b'.' # prevents useless util.fspath() invocation
362 return f
362 return f
363
363
364 def hastrackeddir(self, d):
364 def hastrackeddir(self, d):
365 """
365 """
366 Returns True if the dirstate contains a tracked (not removed) file
366 Returns True if the dirstate contains a tracked (not removed) file
367 in this directory.
367 in this directory.
368 """
368 """
369 return d in self._dirs
369 return d in self._dirs
370
370
371 def hasdir(self, d):
371 def hasdir(self, d):
372 """
372 """
373 Returns True if the dirstate contains a file (tracked or removed)
373 Returns True if the dirstate contains a file (tracked or removed)
374 in this directory.
374 in this directory.
375 """
375 """
376 return d in self._alldirs
376 return d in self._alldirs
377
377
378 @propertycache
378 @propertycache
379 def _dirs(self):
379 def _dirs(self):
380 return pathutil.dirs(self._map, b'r')
380 return pathutil.dirs(self._map, only_tracked=True)
381
381
382 @propertycache
382 @propertycache
383 def _alldirs(self):
383 def _alldirs(self):
384 return pathutil.dirs(self._map)
384 return pathutil.dirs(self._map)
385
385
386 def _opendirstatefile(self):
386 def _opendirstatefile(self):
387 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
387 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
388 if self._pendingmode is not None and self._pendingmode != mode:
388 if self._pendingmode is not None and self._pendingmode != mode:
389 fp.close()
389 fp.close()
390 raise error.Abort(
390 raise error.Abort(
391 _(b'working directory state may be changed parallelly')
391 _(b'working directory state may be changed parallelly')
392 )
392 )
393 self._pendingmode = mode
393 self._pendingmode = mode
394 return fp
394 return fp
395
395
396 def parents(self):
396 def parents(self):
397 if not self._parents:
397 if not self._parents:
398 try:
398 try:
399 fp = self._opendirstatefile()
399 fp = self._opendirstatefile()
400 st = fp.read(2 * self._nodelen)
400 st = fp.read(2 * self._nodelen)
401 fp.close()
401 fp.close()
402 except IOError as err:
402 except IOError as err:
403 if err.errno != errno.ENOENT:
403 if err.errno != errno.ENOENT:
404 raise
404 raise
405 # File doesn't exist, so the current state is empty
405 # File doesn't exist, so the current state is empty
406 st = b''
406 st = b''
407
407
408 l = len(st)
408 l = len(st)
409 if l == self._nodelen * 2:
409 if l == self._nodelen * 2:
410 self._parents = (
410 self._parents = (
411 st[: self._nodelen],
411 st[: self._nodelen],
412 st[self._nodelen : 2 * self._nodelen],
412 st[self._nodelen : 2 * self._nodelen],
413 )
413 )
414 elif l == 0:
414 elif l == 0:
415 self._parents = (
415 self._parents = (
416 self._nodeconstants.nullid,
416 self._nodeconstants.nullid,
417 self._nodeconstants.nullid,
417 self._nodeconstants.nullid,
418 )
418 )
419 else:
419 else:
420 raise error.Abort(
420 raise error.Abort(
421 _(b'working directory state appears damaged!')
421 _(b'working directory state appears damaged!')
422 )
422 )
423
423
424 return self._parents
424 return self._parents
425
425
426 def setparents(self, p1, p2):
426 def setparents(self, p1, p2):
427 self._parents = (p1, p2)
427 self._parents = (p1, p2)
428 self._dirtyparents = True
428 self._dirtyparents = True
429
429
430 def read(self):
430 def read(self):
431 # ignore HG_PENDING because identity is used only for writing
431 # ignore HG_PENDING because identity is used only for writing
432 self.identity = util.filestat.frompath(
432 self.identity = util.filestat.frompath(
433 self._opener.join(self._filename)
433 self._opener.join(self._filename)
434 )
434 )
435
435
436 try:
436 try:
437 fp = self._opendirstatefile()
437 fp = self._opendirstatefile()
438 try:
438 try:
439 st = fp.read()
439 st = fp.read()
440 finally:
440 finally:
441 fp.close()
441 fp.close()
442 except IOError as err:
442 except IOError as err:
443 if err.errno != errno.ENOENT:
443 if err.errno != errno.ENOENT:
444 raise
444 raise
445 return
445 return
446 if not st:
446 if not st:
447 return
447 return
448
448
449 if util.safehasattr(parsers, b'dict_new_presized'):
449 if util.safehasattr(parsers, b'dict_new_presized'):
450 # Make an estimate of the number of files in the dirstate based on
450 # Make an estimate of the number of files in the dirstate based on
451 # its size. This trades wasting some memory for avoiding costly
451 # its size. This trades wasting some memory for avoiding costly
452 # resizes. Each entry have a prefix of 17 bytes followed by one or
452 # resizes. Each entry have a prefix of 17 bytes followed by one or
453 # two path names. Studies on various large-scale real-world repositories
453 # two path names. Studies on various large-scale real-world repositories
454 # found 54 bytes a reasonable upper limit for the average path names.
454 # found 54 bytes a reasonable upper limit for the average path names.
455 # Copy entries are ignored for the sake of this estimate.
455 # Copy entries are ignored for the sake of this estimate.
456 self._map = parsers.dict_new_presized(len(st) // 71)
456 self._map = parsers.dict_new_presized(len(st) // 71)
457
457
458 # Python's garbage collector triggers a GC each time a certain number
458 # Python's garbage collector triggers a GC each time a certain number
459 # of container objects (the number being defined by
459 # of container objects (the number being defined by
460 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
460 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
461 # for each file in the dirstate. The C version then immediately marks
461 # for each file in the dirstate. The C version then immediately marks
462 # them as not to be tracked by the collector. However, this has no
462 # them as not to be tracked by the collector. However, this has no
463 # effect on when GCs are triggered, only on what objects the GC looks
463 # effect on when GCs are triggered, only on what objects the GC looks
464 # into. This means that O(number of files) GCs are unavoidable.
464 # into. This means that O(number of files) GCs are unavoidable.
465 # Depending on when in the process's lifetime the dirstate is parsed,
465 # Depending on when in the process's lifetime the dirstate is parsed,
466 # this can get very expensive. As a workaround, disable GC while
466 # this can get very expensive. As a workaround, disable GC while
467 # parsing the dirstate.
467 # parsing the dirstate.
468 #
468 #
469 # (we cannot decorate the function directly since it is in a C module)
469 # (we cannot decorate the function directly since it is in a C module)
470 parse_dirstate = util.nogc(parsers.parse_dirstate)
470 parse_dirstate = util.nogc(parsers.parse_dirstate)
471 p = parse_dirstate(self._map, self.copymap, st)
471 p = parse_dirstate(self._map, self.copymap, st)
472 if not self._dirtyparents:
472 if not self._dirtyparents:
473 self.setparents(*p)
473 self.setparents(*p)
474
474
475 # Avoid excess attribute lookups by fast pathing certain checks
475 # Avoid excess attribute lookups by fast pathing certain checks
476 self.__contains__ = self._map.__contains__
476 self.__contains__ = self._map.__contains__
477 self.__getitem__ = self._map.__getitem__
477 self.__getitem__ = self._map.__getitem__
478 self.get = self._map.get
478 self.get = self._map.get
479
479
480 def write(self, _tr, st, now):
480 def write(self, _tr, st, now):
481 st.write(
481 st.write(
482 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
482 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
483 )
483 )
484 st.close()
484 st.close()
485 self._dirtyparents = False
485 self._dirtyparents = False
486 self.nonnormalset, self.otherparentset = self.nonnormalentries()
486 self.nonnormalset, self.otherparentset = self.nonnormalentries()
487
487
488 @propertycache
488 @propertycache
489 def nonnormalset(self):
489 def nonnormalset(self):
490 nonnorm, otherparents = self.nonnormalentries()
490 nonnorm, otherparents = self.nonnormalentries()
491 self.otherparentset = otherparents
491 self.otherparentset = otherparents
492 return nonnorm
492 return nonnorm
493
493
494 @propertycache
494 @propertycache
495 def otherparentset(self):
495 def otherparentset(self):
496 nonnorm, otherparents = self.nonnormalentries()
496 nonnorm, otherparents = self.nonnormalentries()
497 self.nonnormalset = nonnorm
497 self.nonnormalset = nonnorm
498 return otherparents
498 return otherparents
499
499
500 def non_normal_or_other_parent_paths(self):
500 def non_normal_or_other_parent_paths(self):
501 return self.nonnormalset.union(self.otherparentset)
501 return self.nonnormalset.union(self.otherparentset)
502
502
503 @propertycache
503 @propertycache
504 def identity(self):
504 def identity(self):
505 self._map
505 self._map
506 return self.identity
506 return self.identity
507
507
508 @propertycache
508 @propertycache
509 def dirfoldmap(self):
509 def dirfoldmap(self):
510 f = {}
510 f = {}
511 normcase = util.normcase
511 normcase = util.normcase
512 for name in self._dirs:
512 for name in self._dirs:
513 f[normcase(name)] = name
513 f[normcase(name)] = name
514 return f
514 return f
515
515
516
516
517 if rustmod is not None:
517 if rustmod is not None:
518
518
519 class dirstatemap(object):
519 class dirstatemap(object):
520 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
520 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
521 self._use_dirstate_v2 = use_dirstate_v2
521 self._use_dirstate_v2 = use_dirstate_v2
522 self._nodeconstants = nodeconstants
522 self._nodeconstants = nodeconstants
523 self._ui = ui
523 self._ui = ui
524 self._opener = opener
524 self._opener = opener
525 self._root = root
525 self._root = root
526 self._filename = b'dirstate'
526 self._filename = b'dirstate'
527 self._nodelen = 20 # Also update Rust code when changing this!
527 self._nodelen = 20 # Also update Rust code when changing this!
528 self._parents = None
528 self._parents = None
529 self._dirtyparents = False
529 self._dirtyparents = False
530 self._docket = None
530 self._docket = None
531
531
532 # for consistent view between _pl() and _read() invocations
532 # for consistent view between _pl() and _read() invocations
533 self._pendingmode = None
533 self._pendingmode = None
534
534
535 self._use_dirstate_tree = self._ui.configbool(
535 self._use_dirstate_tree = self._ui.configbool(
536 b"experimental",
536 b"experimental",
537 b"dirstate-tree.in-memory",
537 b"dirstate-tree.in-memory",
538 False,
538 False,
539 )
539 )
540
540
541 def addfile(
541 def addfile(
542 self,
542 self,
543 f,
543 f,
544 mode=0,
544 mode=0,
545 size=None,
545 size=None,
546 mtime=None,
546 mtime=None,
547 added=False,
547 added=False,
548 merged=False,
548 merged=False,
549 from_p2=False,
549 from_p2=False,
550 possibly_dirty=False,
550 possibly_dirty=False,
551 ):
551 ):
552 return self._rustmap.addfile(
552 return self._rustmap.addfile(
553 f,
553 f,
554 mode,
554 mode,
555 size,
555 size,
556 mtime,
556 mtime,
557 added,
557 added,
558 merged,
558 merged,
559 from_p2,
559 from_p2,
560 possibly_dirty,
560 possibly_dirty,
561 )
561 )
562
562
563 def reset_state(
563 def reset_state(
564 self,
564 self,
565 filename,
565 filename,
566 wc_tracked,
566 wc_tracked,
567 p1_tracked,
567 p1_tracked,
568 p2_tracked=False,
568 p2_tracked=False,
569 merged=False,
569 merged=False,
570 clean_p1=False,
570 clean_p1=False,
571 clean_p2=False,
571 clean_p2=False,
572 possibly_dirty=False,
572 possibly_dirty=False,
573 parentfiledata=None,
573 parentfiledata=None,
574 ):
574 ):
575 """Set a entry to a given state, disregarding all previous state
575 """Set a entry to a given state, disregarding all previous state
576
576
577 This is to be used by the part of the dirstate API dedicated to
577 This is to be used by the part of the dirstate API dedicated to
578 adjusting the dirstate after a update/merge.
578 adjusting the dirstate after a update/merge.
579
579
580 note: calling this might result to no entry existing at all if the
580 note: calling this might result to no entry existing at all if the
581 dirstate map does not see any point at having one for this file
581 dirstate map does not see any point at having one for this file
582 anymore.
582 anymore.
583 """
583 """
584 if merged and (clean_p1 or clean_p2):
584 if merged and (clean_p1 or clean_p2):
585 msg = (
585 msg = (
586 b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
586 b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
587 )
587 )
588 raise error.ProgrammingError(msg)
588 raise error.ProgrammingError(msg)
589 # copy information are now outdated
589 # copy information are now outdated
590 # (maybe new information should be in directly passed to this function)
590 # (maybe new information should be in directly passed to this function)
591 self.copymap.pop(filename, None)
591 self.copymap.pop(filename, None)
592
592
593 if not (p1_tracked or p2_tracked or wc_tracked):
593 if not (p1_tracked or p2_tracked or wc_tracked):
594 self.dropfile(filename)
594 self.dropfile(filename)
595 elif merged:
595 elif merged:
596 # XXX might be merged and removed ?
596 # XXX might be merged and removed ?
597 entry = self.get(filename)
597 entry = self.get(filename)
598 if entry is not None and entry.tracked:
598 if entry is not None and entry.tracked:
599 # XXX mostly replicate dirstate.other parent. We should get
599 # XXX mostly replicate dirstate.other parent. We should get
600 # the higher layer to pass us more reliable data where `merged`
600 # the higher layer to pass us more reliable data where `merged`
601 # actually mean merged. Dropping the else clause will show
601 # actually mean merged. Dropping the else clause will show
602 # failure in `test-graft.t`
602 # failure in `test-graft.t`
603 self.addfile(filename, merged=True)
603 self.addfile(filename, merged=True)
604 else:
604 else:
605 self.addfile(filename, from_p2=True)
605 self.addfile(filename, from_p2=True)
606 elif not (p1_tracked or p2_tracked) and wc_tracked:
606 elif not (p1_tracked or p2_tracked) and wc_tracked:
607 self.addfile(
607 self.addfile(
608 filename, added=True, possibly_dirty=possibly_dirty
608 filename, added=True, possibly_dirty=possibly_dirty
609 )
609 )
610 elif (p1_tracked or p2_tracked) and not wc_tracked:
610 elif (p1_tracked or p2_tracked) and not wc_tracked:
611 # XXX might be merged and removed ?
611 # XXX might be merged and removed ?
612 self[filename] = DirstateItem.from_v1_data(b'r', 0, 0, 0)
612 self[filename] = DirstateItem.from_v1_data(b'r', 0, 0, 0)
613 self.nonnormalset.add(filename)
613 self.nonnormalset.add(filename)
614 elif clean_p2 and wc_tracked:
614 elif clean_p2 and wc_tracked:
615 if p1_tracked or self.get(filename) is not None:
615 if p1_tracked or self.get(filename) is not None:
616 # XXX the `self.get` call is catching some case in
616 # XXX the `self.get` call is catching some case in
617 # `test-merge-remove.t` where the file is tracked in p1, the
617 # `test-merge-remove.t` where the file is tracked in p1, the
618 # p1_tracked argument is False.
618 # p1_tracked argument is False.
619 #
619 #
620 # In addition, this seems to be a case where the file is marked
620 # In addition, this seems to be a case where the file is marked
621 # as merged without actually being the result of a merge
621 # as merged without actually being the result of a merge
622 # action. So thing are not ideal here.
622 # action. So thing are not ideal here.
623 self.addfile(filename, merged=True)
623 self.addfile(filename, merged=True)
624 else:
624 else:
625 self.addfile(filename, from_p2=True)
625 self.addfile(filename, from_p2=True)
626 elif not p1_tracked and p2_tracked and wc_tracked:
626 elif not p1_tracked and p2_tracked and wc_tracked:
627 self.addfile(
627 self.addfile(
628 filename, from_p2=True, possibly_dirty=possibly_dirty
628 filename, from_p2=True, possibly_dirty=possibly_dirty
629 )
629 )
630 elif possibly_dirty:
630 elif possibly_dirty:
631 self.addfile(filename, possibly_dirty=possibly_dirty)
631 self.addfile(filename, possibly_dirty=possibly_dirty)
632 elif wc_tracked:
632 elif wc_tracked:
633 # this is a "normal" file
633 # this is a "normal" file
634 if parentfiledata is None:
634 if parentfiledata is None:
635 msg = b'failed to pass parentfiledata for a normal file: %s'
635 msg = b'failed to pass parentfiledata for a normal file: %s'
636 msg %= filename
636 msg %= filename
637 raise error.ProgrammingError(msg)
637 raise error.ProgrammingError(msg)
638 mode, size, mtime = parentfiledata
638 mode, size, mtime = parentfiledata
639 self.addfile(filename, mode=mode, size=size, mtime=mtime)
639 self.addfile(filename, mode=mode, size=size, mtime=mtime)
640 self.nonnormalset.discard(filename)
640 self.nonnormalset.discard(filename)
641 else:
641 else:
642 assert False, 'unreachable'
642 assert False, 'unreachable'
643
643
644 def set_untracked(self, f):
644 def set_untracked(self, f):
645 """Mark a file as no longer tracked in the dirstate map"""
645 """Mark a file as no longer tracked in the dirstate map"""
646 # in merge is only trigger more logic, so it "fine" to pass it.
646 # in merge is only trigger more logic, so it "fine" to pass it.
647 #
647 #
648 # the inner rust dirstate map code need to be adjusted once the API
648 # the inner rust dirstate map code need to be adjusted once the API
649 # for dirstate/dirstatemap/DirstateItem is a bit more settled
649 # for dirstate/dirstatemap/DirstateItem is a bit more settled
650 self._rustmap.removefile(f, in_merge=True)
650 self._rustmap.removefile(f, in_merge=True)
651
651
652 def removefile(self, *args, **kwargs):
652 def removefile(self, *args, **kwargs):
653 return self._rustmap.removefile(*args, **kwargs)
653 return self._rustmap.removefile(*args, **kwargs)
654
654
655 def dropfile(self, *args, **kwargs):
655 def dropfile(self, *args, **kwargs):
656 return self._rustmap.dropfile(*args, **kwargs)
656 return self._rustmap.dropfile(*args, **kwargs)
657
657
658 def clearambiguoustimes(self, *args, **kwargs):
658 def clearambiguoustimes(self, *args, **kwargs):
659 return self._rustmap.clearambiguoustimes(*args, **kwargs)
659 return self._rustmap.clearambiguoustimes(*args, **kwargs)
660
660
661 def nonnormalentries(self):
661 def nonnormalentries(self):
662 return self._rustmap.nonnormalentries()
662 return self._rustmap.nonnormalentries()
663
663
664 def get(self, *args, **kwargs):
664 def get(self, *args, **kwargs):
665 return self._rustmap.get(*args, **kwargs)
665 return self._rustmap.get(*args, **kwargs)
666
666
667 @property
667 @property
668 def copymap(self):
668 def copymap(self):
669 return self._rustmap.copymap()
669 return self._rustmap.copymap()
670
670
671 def directories(self):
671 def directories(self):
672 return self._rustmap.directories()
672 return self._rustmap.directories()
673
673
674 def debug_iter(self):
674 def debug_iter(self):
675 return self._rustmap.debug_iter()
675 return self._rustmap.debug_iter()
676
676
677 def preload(self):
677 def preload(self):
678 self._rustmap
678 self._rustmap
679
679
680 def clear(self):
680 def clear(self):
681 self._rustmap.clear()
681 self._rustmap.clear()
682 self.setparents(
682 self.setparents(
683 self._nodeconstants.nullid, self._nodeconstants.nullid
683 self._nodeconstants.nullid, self._nodeconstants.nullid
684 )
684 )
685 util.clearcachedproperty(self, b"_dirs")
685 util.clearcachedproperty(self, b"_dirs")
686 util.clearcachedproperty(self, b"_alldirs")
686 util.clearcachedproperty(self, b"_alldirs")
687 util.clearcachedproperty(self, b"dirfoldmap")
687 util.clearcachedproperty(self, b"dirfoldmap")
688
688
689 def items(self):
689 def items(self):
690 return self._rustmap.items()
690 return self._rustmap.items()
691
691
692 def keys(self):
692 def keys(self):
693 return iter(self._rustmap)
693 return iter(self._rustmap)
694
694
695 def __contains__(self, key):
695 def __contains__(self, key):
696 return key in self._rustmap
696 return key in self._rustmap
697
697
698 def __getitem__(self, item):
698 def __getitem__(self, item):
699 return self._rustmap[item]
699 return self._rustmap[item]
700
700
701 def __len__(self):
701 def __len__(self):
702 return len(self._rustmap)
702 return len(self._rustmap)
703
703
704 def __iter__(self):
704 def __iter__(self):
705 return iter(self._rustmap)
705 return iter(self._rustmap)
706
706
707 # forward for python2,3 compat
707 # forward for python2,3 compat
708 iteritems = items
708 iteritems = items
709
709
710 def _opendirstatefile(self):
710 def _opendirstatefile(self):
711 fp, mode = txnutil.trypending(
711 fp, mode = txnutil.trypending(
712 self._root, self._opener, self._filename
712 self._root, self._opener, self._filename
713 )
713 )
714 if self._pendingmode is not None and self._pendingmode != mode:
714 if self._pendingmode is not None and self._pendingmode != mode:
715 fp.close()
715 fp.close()
716 raise error.Abort(
716 raise error.Abort(
717 _(b'working directory state may be changed parallelly')
717 _(b'working directory state may be changed parallelly')
718 )
718 )
719 self._pendingmode = mode
719 self._pendingmode = mode
720 return fp
720 return fp
721
721
722 def _readdirstatefile(self, size=-1):
722 def _readdirstatefile(self, size=-1):
723 try:
723 try:
724 with self._opendirstatefile() as fp:
724 with self._opendirstatefile() as fp:
725 return fp.read(size)
725 return fp.read(size)
726 except IOError as err:
726 except IOError as err:
727 if err.errno != errno.ENOENT:
727 if err.errno != errno.ENOENT:
728 raise
728 raise
729 # File doesn't exist, so the current state is empty
729 # File doesn't exist, so the current state is empty
730 return b''
730 return b''
731
731
732 def setparents(self, p1, p2):
732 def setparents(self, p1, p2):
733 self._parents = (p1, p2)
733 self._parents = (p1, p2)
734 self._dirtyparents = True
734 self._dirtyparents = True
735
735
736 def parents(self):
736 def parents(self):
737 if not self._parents:
737 if not self._parents:
738 if self._use_dirstate_v2:
738 if self._use_dirstate_v2:
739 self._parents = self.docket.parents
739 self._parents = self.docket.parents
740 else:
740 else:
741 read_len = self._nodelen * 2
741 read_len = self._nodelen * 2
742 st = self._readdirstatefile(read_len)
742 st = self._readdirstatefile(read_len)
743 l = len(st)
743 l = len(st)
744 if l == read_len:
744 if l == read_len:
745 self._parents = (
745 self._parents = (
746 st[: self._nodelen],
746 st[: self._nodelen],
747 st[self._nodelen : 2 * self._nodelen],
747 st[self._nodelen : 2 * self._nodelen],
748 )
748 )
749 elif l == 0:
749 elif l == 0:
750 self._parents = (
750 self._parents = (
751 self._nodeconstants.nullid,
751 self._nodeconstants.nullid,
752 self._nodeconstants.nullid,
752 self._nodeconstants.nullid,
753 )
753 )
754 else:
754 else:
755 raise error.Abort(
755 raise error.Abort(
756 _(b'working directory state appears damaged!')
756 _(b'working directory state appears damaged!')
757 )
757 )
758
758
759 return self._parents
759 return self._parents
760
760
761 @property
761 @property
762 def docket(self):
762 def docket(self):
763 if not self._docket:
763 if not self._docket:
764 if not self._use_dirstate_v2:
764 if not self._use_dirstate_v2:
765 raise error.ProgrammingError(
765 raise error.ProgrammingError(
766 b'dirstate only has a docket in v2 format'
766 b'dirstate only has a docket in v2 format'
767 )
767 )
768 self._docket = docketmod.DirstateDocket.parse(
768 self._docket = docketmod.DirstateDocket.parse(
769 self._readdirstatefile(), self._nodeconstants
769 self._readdirstatefile(), self._nodeconstants
770 )
770 )
771 return self._docket
771 return self._docket
772
772
773 @propertycache
773 @propertycache
774 def _rustmap(self):
774 def _rustmap(self):
775 """
775 """
776 Fills the Dirstatemap when called.
776 Fills the Dirstatemap when called.
777 """
777 """
778 # ignore HG_PENDING because identity is used only for writing
778 # ignore HG_PENDING because identity is used only for writing
779 self.identity = util.filestat.frompath(
779 self.identity = util.filestat.frompath(
780 self._opener.join(self._filename)
780 self._opener.join(self._filename)
781 )
781 )
782
782
783 if self._use_dirstate_v2:
783 if self._use_dirstate_v2:
784 if self.docket.uuid:
784 if self.docket.uuid:
785 # TODO: use mmap when possible
785 # TODO: use mmap when possible
786 data = self._opener.read(self.docket.data_filename())
786 data = self._opener.read(self.docket.data_filename())
787 else:
787 else:
788 data = b''
788 data = b''
789 self._rustmap = rustmod.DirstateMap.new_v2(
789 self._rustmap = rustmod.DirstateMap.new_v2(
790 data, self.docket.data_size, self.docket.tree_metadata
790 data, self.docket.data_size, self.docket.tree_metadata
791 )
791 )
792 parents = self.docket.parents
792 parents = self.docket.parents
793 else:
793 else:
794 self._rustmap, parents = rustmod.DirstateMap.new_v1(
794 self._rustmap, parents = rustmod.DirstateMap.new_v1(
795 self._use_dirstate_tree, self._readdirstatefile()
795 self._use_dirstate_tree, self._readdirstatefile()
796 )
796 )
797
797
798 if parents and not self._dirtyparents:
798 if parents and not self._dirtyparents:
799 self.setparents(*parents)
799 self.setparents(*parents)
800
800
801 self.__contains__ = self._rustmap.__contains__
801 self.__contains__ = self._rustmap.__contains__
802 self.__getitem__ = self._rustmap.__getitem__
802 self.__getitem__ = self._rustmap.__getitem__
803 self.get = self._rustmap.get
803 self.get = self._rustmap.get
804 return self._rustmap
804 return self._rustmap
805
805
806 def write(self, tr, st, now):
806 def write(self, tr, st, now):
807 if not self._use_dirstate_v2:
807 if not self._use_dirstate_v2:
808 p1, p2 = self.parents()
808 p1, p2 = self.parents()
809 packed = self._rustmap.write_v1(p1, p2, now)
809 packed = self._rustmap.write_v1(p1, p2, now)
810 st.write(packed)
810 st.write(packed)
811 st.close()
811 st.close()
812 self._dirtyparents = False
812 self._dirtyparents = False
813 return
813 return
814
814
815 # We can only append to an existing data file if there is one
815 # We can only append to an existing data file if there is one
816 can_append = self.docket.uuid is not None
816 can_append = self.docket.uuid is not None
817 packed, meta, append = self._rustmap.write_v2(now, can_append)
817 packed, meta, append = self._rustmap.write_v2(now, can_append)
818 if append:
818 if append:
819 docket = self.docket
819 docket = self.docket
820 data_filename = docket.data_filename()
820 data_filename = docket.data_filename()
821 if tr:
821 if tr:
822 tr.add(data_filename, docket.data_size)
822 tr.add(data_filename, docket.data_size)
823 with self._opener(data_filename, b'r+b') as fp:
823 with self._opener(data_filename, b'r+b') as fp:
824 fp.seek(docket.data_size)
824 fp.seek(docket.data_size)
825 assert fp.tell() == docket.data_size
825 assert fp.tell() == docket.data_size
826 written = fp.write(packed)
826 written = fp.write(packed)
827 if written is not None: # py2 may return None
827 if written is not None: # py2 may return None
828 assert written == len(packed), (written, len(packed))
828 assert written == len(packed), (written, len(packed))
829 docket.data_size += len(packed)
829 docket.data_size += len(packed)
830 docket.parents = self.parents()
830 docket.parents = self.parents()
831 docket.tree_metadata = meta
831 docket.tree_metadata = meta
832 st.write(docket.serialize())
832 st.write(docket.serialize())
833 st.close()
833 st.close()
834 else:
834 else:
835 old_docket = self.docket
835 old_docket = self.docket
836 new_docket = docketmod.DirstateDocket.with_new_uuid(
836 new_docket = docketmod.DirstateDocket.with_new_uuid(
837 self.parents(), len(packed), meta
837 self.parents(), len(packed), meta
838 )
838 )
839 data_filename = new_docket.data_filename()
839 data_filename = new_docket.data_filename()
840 if tr:
840 if tr:
841 tr.add(data_filename, 0)
841 tr.add(data_filename, 0)
842 self._opener.write(data_filename, packed)
842 self._opener.write(data_filename, packed)
843 # Write the new docket after the new data file has been
843 # Write the new docket after the new data file has been
844 # written. Because `st` was opened with `atomictemp=True`,
844 # written. Because `st` was opened with `atomictemp=True`,
845 # the actual `.hg/dirstate` file is only affected on close.
845 # the actual `.hg/dirstate` file is only affected on close.
846 st.write(new_docket.serialize())
846 st.write(new_docket.serialize())
847 st.close()
847 st.close()
848 # Remove the old data file after the new docket pointing to
848 # Remove the old data file after the new docket pointing to
849 # the new data file was written.
849 # the new data file was written.
850 if old_docket.uuid:
850 if old_docket.uuid:
851 data_filename = old_docket.data_filename()
851 data_filename = old_docket.data_filename()
852 unlink = lambda _tr=None: self._opener.unlink(data_filename)
852 unlink = lambda _tr=None: self._opener.unlink(data_filename)
853 if tr:
853 if tr:
854 category = b"dirstate-v2-clean-" + old_docket.uuid
854 category = b"dirstate-v2-clean-" + old_docket.uuid
855 tr.addpostclose(category, unlink)
855 tr.addpostclose(category, unlink)
856 else:
856 else:
857 unlink()
857 unlink()
858 self._docket = new_docket
858 self._docket = new_docket
859 # Reload from the newly-written file
859 # Reload from the newly-written file
860 util.clearcachedproperty(self, b"_rustmap")
860 util.clearcachedproperty(self, b"_rustmap")
861 self._dirtyparents = False
861 self._dirtyparents = False
862
862
863 @propertycache
863 @propertycache
864 def filefoldmap(self):
864 def filefoldmap(self):
865 """Returns a dictionary mapping normalized case paths to their
865 """Returns a dictionary mapping normalized case paths to their
866 non-normalized versions.
866 non-normalized versions.
867 """
867 """
868 return self._rustmap.filefoldmapasdict()
868 return self._rustmap.filefoldmapasdict()
869
869
870 def hastrackeddir(self, d):
870 def hastrackeddir(self, d):
871 return self._rustmap.hastrackeddir(d)
871 return self._rustmap.hastrackeddir(d)
872
872
873 def hasdir(self, d):
873 def hasdir(self, d):
874 return self._rustmap.hasdir(d)
874 return self._rustmap.hasdir(d)
875
875
876 @propertycache
876 @propertycache
877 def identity(self):
877 def identity(self):
878 self._rustmap
878 self._rustmap
879 return self.identity
879 return self.identity
880
880
881 @property
881 @property
882 def nonnormalset(self):
882 def nonnormalset(self):
883 nonnorm = self._rustmap.non_normal_entries()
883 nonnorm = self._rustmap.non_normal_entries()
884 return nonnorm
884 return nonnorm
885
885
886 @propertycache
886 @propertycache
887 def otherparentset(self):
887 def otherparentset(self):
888 otherparents = self._rustmap.other_parent_entries()
888 otherparents = self._rustmap.other_parent_entries()
889 return otherparents
889 return otherparents
890
890
891 def non_normal_or_other_parent_paths(self):
891 def non_normal_or_other_parent_paths(self):
892 return self._rustmap.non_normal_or_other_parent_paths()
892 return self._rustmap.non_normal_or_other_parent_paths()
893
893
894 @propertycache
894 @propertycache
895 def dirfoldmap(self):
895 def dirfoldmap(self):
896 f = {}
896 f = {}
897 normcase = util.normcase
897 normcase = util.normcase
898 for name in self._rustmap.tracked_dirs():
898 for name in self._rustmap.tracked_dirs():
899 f[normcase(name)] = name
899 f[normcase(name)] = name
900 return f
900 return f
901
901
902 def set_possibly_dirty(self, filename):
902 def set_possibly_dirty(self, filename):
903 """record that the current state of the file on disk is unknown"""
903 """record that the current state of the file on disk is unknown"""
904 entry = self[filename]
904 entry = self[filename]
905 entry.set_possibly_dirty()
905 entry.set_possibly_dirty()
906 self._rustmap.set_v1(filename, entry)
906 self._rustmap.set_v1(filename, entry)
907
907
908 def __setitem__(self, key, value):
908 def __setitem__(self, key, value):
909 assert isinstance(value, DirstateItem)
909 assert isinstance(value, DirstateItem)
910 self._rustmap.set_v1(key, value)
910 self._rustmap.set_v1(key, value)
@@ -1,377 +1,376 b''
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2
2
3 import contextlib
3 import contextlib
4 import errno
4 import errno
5 import os
5 import os
6 import posixpath
6 import posixpath
7 import stat
7 import stat
8
8
9 from .i18n import _
9 from .i18n import _
10 from . import (
10 from . import (
11 encoding,
11 encoding,
12 error,
12 error,
13 policy,
13 policy,
14 pycompat,
14 pycompat,
15 util,
15 util,
16 )
16 )
17
17
18 if pycompat.TYPE_CHECKING:
18 if pycompat.TYPE_CHECKING:
19 from typing import (
19 from typing import (
20 Any,
20 Any,
21 Callable,
21 Callable,
22 Iterator,
22 Iterator,
23 Optional,
23 Optional,
24 )
24 )
25
25
26
26
27 rustdirs = policy.importrust('dirstate', 'Dirs')
27 rustdirs = policy.importrust('dirstate', 'Dirs')
28 parsers = policy.importmod('parsers')
28 parsers = policy.importmod('parsers')
29
29
30
30
31 def _lowerclean(s):
31 def _lowerclean(s):
32 # type: (bytes) -> bytes
32 # type: (bytes) -> bytes
33 return encoding.hfsignoreclean(s.lower())
33 return encoding.hfsignoreclean(s.lower())
34
34
35
35
36 class pathauditor(object):
36 class pathauditor(object):
37 """ensure that a filesystem path contains no banned components.
37 """ensure that a filesystem path contains no banned components.
38 the following properties of a path are checked:
38 the following properties of a path are checked:
39
39
40 - ends with a directory separator
40 - ends with a directory separator
41 - under top-level .hg
41 - under top-level .hg
42 - starts at the root of a windows drive
42 - starts at the root of a windows drive
43 - contains ".."
43 - contains ".."
44
44
45 More check are also done about the file system states:
45 More check are also done about the file system states:
46 - traverses a symlink (e.g. a/symlink_here/b)
46 - traverses a symlink (e.g. a/symlink_here/b)
47 - inside a nested repository (a callback can be used to approve
47 - inside a nested repository (a callback can be used to approve
48 some nested repositories, e.g., subrepositories)
48 some nested repositories, e.g., subrepositories)
49
49
50 The file system checks are only done when 'realfs' is set to True (the
50 The file system checks are only done when 'realfs' is set to True (the
51 default). They should be disable then we are auditing path for operation on
51 default). They should be disable then we are auditing path for operation on
52 stored history.
52 stored history.
53
53
54 If 'cached' is set to True, audited paths and sub-directories are cached.
54 If 'cached' is set to True, audited paths and sub-directories are cached.
55 Be careful to not keep the cache of unmanaged directories for long because
55 Be careful to not keep the cache of unmanaged directories for long because
56 audited paths may be replaced with symlinks.
56 audited paths may be replaced with symlinks.
57 """
57 """
58
58
59 def __init__(self, root, callback=None, realfs=True, cached=False):
59 def __init__(self, root, callback=None, realfs=True, cached=False):
60 self.audited = set()
60 self.audited = set()
61 self.auditeddir = set()
61 self.auditeddir = set()
62 self.root = root
62 self.root = root
63 self._realfs = realfs
63 self._realfs = realfs
64 self._cached = cached
64 self._cached = cached
65 self.callback = callback
65 self.callback = callback
66 if os.path.lexists(root) and not util.fscasesensitive(root):
66 if os.path.lexists(root) and not util.fscasesensitive(root):
67 self.normcase = util.normcase
67 self.normcase = util.normcase
68 else:
68 else:
69 self.normcase = lambda x: x
69 self.normcase = lambda x: x
70
70
71 def __call__(self, path, mode=None):
71 def __call__(self, path, mode=None):
72 # type: (bytes, Optional[Any]) -> None
72 # type: (bytes, Optional[Any]) -> None
73 """Check the relative path.
73 """Check the relative path.
74 path may contain a pattern (e.g. foodir/**.txt)"""
74 path may contain a pattern (e.g. foodir/**.txt)"""
75
75
76 path = util.localpath(path)
76 path = util.localpath(path)
77 normpath = self.normcase(path)
77 normpath = self.normcase(path)
78 if normpath in self.audited:
78 if normpath in self.audited:
79 return
79 return
80 # AIX ignores "/" at end of path, others raise EISDIR.
80 # AIX ignores "/" at end of path, others raise EISDIR.
81 if util.endswithsep(path):
81 if util.endswithsep(path):
82 raise error.Abort(_(b"path ends in directory separator: %s") % path)
82 raise error.Abort(_(b"path ends in directory separator: %s") % path)
83 parts = util.splitpath(path)
83 parts = util.splitpath(path)
84 if (
84 if (
85 os.path.splitdrive(path)[0]
85 os.path.splitdrive(path)[0]
86 or _lowerclean(parts[0]) in (b'.hg', b'.hg.', b'')
86 or _lowerclean(parts[0]) in (b'.hg', b'.hg.', b'')
87 or pycompat.ospardir in parts
87 or pycompat.ospardir in parts
88 ):
88 ):
89 raise error.Abort(_(b"path contains illegal component: %s") % path)
89 raise error.Abort(_(b"path contains illegal component: %s") % path)
90 # Windows shortname aliases
90 # Windows shortname aliases
91 for p in parts:
91 for p in parts:
92 if b"~" in p:
92 if b"~" in p:
93 first, last = p.split(b"~", 1)
93 first, last = p.split(b"~", 1)
94 if last.isdigit() and first.upper() in [b"HG", b"HG8B6C"]:
94 if last.isdigit() and first.upper() in [b"HG", b"HG8B6C"]:
95 raise error.Abort(
95 raise error.Abort(
96 _(b"path contains illegal component: %s") % path
96 _(b"path contains illegal component: %s") % path
97 )
97 )
98 if b'.hg' in _lowerclean(path):
98 if b'.hg' in _lowerclean(path):
99 lparts = [_lowerclean(p) for p in parts]
99 lparts = [_lowerclean(p) for p in parts]
100 for p in b'.hg', b'.hg.':
100 for p in b'.hg', b'.hg.':
101 if p in lparts[1:]:
101 if p in lparts[1:]:
102 pos = lparts.index(p)
102 pos = lparts.index(p)
103 base = os.path.join(*parts[:pos])
103 base = os.path.join(*parts[:pos])
104 raise error.Abort(
104 raise error.Abort(
105 _(b"path '%s' is inside nested repo %r")
105 _(b"path '%s' is inside nested repo %r")
106 % (path, pycompat.bytestr(base))
106 % (path, pycompat.bytestr(base))
107 )
107 )
108
108
109 normparts = util.splitpath(normpath)
109 normparts = util.splitpath(normpath)
110 assert len(parts) == len(normparts)
110 assert len(parts) == len(normparts)
111
111
112 parts.pop()
112 parts.pop()
113 normparts.pop()
113 normparts.pop()
114 # It's important that we check the path parts starting from the root.
114 # It's important that we check the path parts starting from the root.
115 # We don't want to add "foo/bar/baz" to auditeddir before checking if
115 # We don't want to add "foo/bar/baz" to auditeddir before checking if
116 # there's a "foo/.hg" directory. This also means we won't accidentally
116 # there's a "foo/.hg" directory. This also means we won't accidentally
117 # traverse a symlink into some other filesystem (which is potentially
117 # traverse a symlink into some other filesystem (which is potentially
118 # expensive to access).
118 # expensive to access).
119 for i in range(len(parts)):
119 for i in range(len(parts)):
120 prefix = pycompat.ossep.join(parts[: i + 1])
120 prefix = pycompat.ossep.join(parts[: i + 1])
121 normprefix = pycompat.ossep.join(normparts[: i + 1])
121 normprefix = pycompat.ossep.join(normparts[: i + 1])
122 if normprefix in self.auditeddir:
122 if normprefix in self.auditeddir:
123 continue
123 continue
124 if self._realfs:
124 if self._realfs:
125 self._checkfs(prefix, path)
125 self._checkfs(prefix, path)
126 if self._cached:
126 if self._cached:
127 self.auditeddir.add(normprefix)
127 self.auditeddir.add(normprefix)
128
128
129 if self._cached:
129 if self._cached:
130 self.audited.add(normpath)
130 self.audited.add(normpath)
131
131
132 def _checkfs(self, prefix, path):
132 def _checkfs(self, prefix, path):
133 # type: (bytes, bytes) -> None
133 # type: (bytes, bytes) -> None
134 """raise exception if a file system backed check fails"""
134 """raise exception if a file system backed check fails"""
135 curpath = os.path.join(self.root, prefix)
135 curpath = os.path.join(self.root, prefix)
136 try:
136 try:
137 st = os.lstat(curpath)
137 st = os.lstat(curpath)
138 except OSError as err:
138 except OSError as err:
139 # EINVAL can be raised as invalid path syntax under win32.
139 # EINVAL can be raised as invalid path syntax under win32.
140 # They must be ignored for patterns can be checked too.
140 # They must be ignored for patterns can be checked too.
141 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
141 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
142 raise
142 raise
143 else:
143 else:
144 if stat.S_ISLNK(st.st_mode):
144 if stat.S_ISLNK(st.st_mode):
145 msg = _(b'path %r traverses symbolic link %r') % (
145 msg = _(b'path %r traverses symbolic link %r') % (
146 pycompat.bytestr(path),
146 pycompat.bytestr(path),
147 pycompat.bytestr(prefix),
147 pycompat.bytestr(prefix),
148 )
148 )
149 raise error.Abort(msg)
149 raise error.Abort(msg)
150 elif stat.S_ISDIR(st.st_mode) and os.path.isdir(
150 elif stat.S_ISDIR(st.st_mode) and os.path.isdir(
151 os.path.join(curpath, b'.hg')
151 os.path.join(curpath, b'.hg')
152 ):
152 ):
153 if not self.callback or not self.callback(curpath):
153 if not self.callback or not self.callback(curpath):
154 msg = _(b"path '%s' is inside nested repo %r")
154 msg = _(b"path '%s' is inside nested repo %r")
155 raise error.Abort(msg % (path, pycompat.bytestr(prefix)))
155 raise error.Abort(msg % (path, pycompat.bytestr(prefix)))
156
156
157 def check(self, path):
157 def check(self, path):
158 # type: (bytes) -> bool
158 # type: (bytes) -> bool
159 try:
159 try:
160 self(path)
160 self(path)
161 return True
161 return True
162 except (OSError, error.Abort):
162 except (OSError, error.Abort):
163 return False
163 return False
164
164
165 @contextlib.contextmanager
165 @contextlib.contextmanager
166 def cached(self):
166 def cached(self):
167 if self._cached:
167 if self._cached:
168 yield
168 yield
169 else:
169 else:
170 try:
170 try:
171 self._cached = True
171 self._cached = True
172 yield
172 yield
173 finally:
173 finally:
174 self.audited.clear()
174 self.audited.clear()
175 self.auditeddir.clear()
175 self.auditeddir.clear()
176 self._cached = False
176 self._cached = False
177
177
178
178
179 def canonpath(root, cwd, myname, auditor=None):
179 def canonpath(root, cwd, myname, auditor=None):
180 # type: (bytes, bytes, bytes, Optional[pathauditor]) -> bytes
180 # type: (bytes, bytes, bytes, Optional[pathauditor]) -> bytes
181 """return the canonical path of myname, given cwd and root
181 """return the canonical path of myname, given cwd and root
182
182
183 >>> def check(root, cwd, myname):
183 >>> def check(root, cwd, myname):
184 ... a = pathauditor(root, realfs=False)
184 ... a = pathauditor(root, realfs=False)
185 ... try:
185 ... try:
186 ... return canonpath(root, cwd, myname, a)
186 ... return canonpath(root, cwd, myname, a)
187 ... except error.Abort:
187 ... except error.Abort:
188 ... return 'aborted'
188 ... return 'aborted'
189 >>> def unixonly(root, cwd, myname, expected='aborted'):
189 >>> def unixonly(root, cwd, myname, expected='aborted'):
190 ... if pycompat.iswindows:
190 ... if pycompat.iswindows:
191 ... return expected
191 ... return expected
192 ... return check(root, cwd, myname)
192 ... return check(root, cwd, myname)
193 >>> def winonly(root, cwd, myname, expected='aborted'):
193 >>> def winonly(root, cwd, myname, expected='aborted'):
194 ... if not pycompat.iswindows:
194 ... if not pycompat.iswindows:
195 ... return expected
195 ... return expected
196 ... return check(root, cwd, myname)
196 ... return check(root, cwd, myname)
197 >>> winonly(b'd:\\\\repo', b'c:\\\\dir', b'filename')
197 >>> winonly(b'd:\\\\repo', b'c:\\\\dir', b'filename')
198 'aborted'
198 'aborted'
199 >>> winonly(b'c:\\\\repo', b'c:\\\\dir', b'filename')
199 >>> winonly(b'c:\\\\repo', b'c:\\\\dir', b'filename')
200 'aborted'
200 'aborted'
201 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'filename')
201 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'filename')
202 'aborted'
202 'aborted'
203 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'repo\\\\filename',
203 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'repo\\\\filename',
204 ... b'filename')
204 ... b'filename')
205 'filename'
205 'filename'
206 >>> winonly(b'c:\\\\repo', b'c:\\\\repo', b'filename', b'filename')
206 >>> winonly(b'c:\\\\repo', b'c:\\\\repo', b'filename', b'filename')
207 'filename'
207 'filename'
208 >>> winonly(b'c:\\\\repo', b'c:\\\\repo\\\\subdir', b'filename',
208 >>> winonly(b'c:\\\\repo', b'c:\\\\repo\\\\subdir', b'filename',
209 ... b'subdir/filename')
209 ... b'subdir/filename')
210 'subdir/filename'
210 'subdir/filename'
211 >>> unixonly(b'/repo', b'/dir', b'filename')
211 >>> unixonly(b'/repo', b'/dir', b'filename')
212 'aborted'
212 'aborted'
213 >>> unixonly(b'/repo', b'/', b'filename')
213 >>> unixonly(b'/repo', b'/', b'filename')
214 'aborted'
214 'aborted'
215 >>> unixonly(b'/repo', b'/', b'repo/filename', b'filename')
215 >>> unixonly(b'/repo', b'/', b'repo/filename', b'filename')
216 'filename'
216 'filename'
217 >>> unixonly(b'/repo', b'/repo', b'filename', b'filename')
217 >>> unixonly(b'/repo', b'/repo', b'filename', b'filename')
218 'filename'
218 'filename'
219 >>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename')
219 >>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename')
220 'subdir/filename'
220 'subdir/filename'
221 """
221 """
222 if util.endswithsep(root):
222 if util.endswithsep(root):
223 rootsep = root
223 rootsep = root
224 else:
224 else:
225 rootsep = root + pycompat.ossep
225 rootsep = root + pycompat.ossep
226 name = myname
226 name = myname
227 if not os.path.isabs(name):
227 if not os.path.isabs(name):
228 name = os.path.join(root, cwd, name)
228 name = os.path.join(root, cwd, name)
229 name = os.path.normpath(name)
229 name = os.path.normpath(name)
230 if auditor is None:
230 if auditor is None:
231 auditor = pathauditor(root)
231 auditor = pathauditor(root)
232 if name != rootsep and name.startswith(rootsep):
232 if name != rootsep and name.startswith(rootsep):
233 name = name[len(rootsep) :]
233 name = name[len(rootsep) :]
234 auditor(name)
234 auditor(name)
235 return util.pconvert(name)
235 return util.pconvert(name)
236 elif name == root:
236 elif name == root:
237 return b''
237 return b''
238 else:
238 else:
239 # Determine whether `name' is in the hierarchy at or beneath `root',
239 # Determine whether `name' is in the hierarchy at or beneath `root',
240 # by iterating name=dirname(name) until that causes no change (can't
240 # by iterating name=dirname(name) until that causes no change (can't
241 # check name == '/', because that doesn't work on windows). The list
241 # check name == '/', because that doesn't work on windows). The list
242 # `rel' holds the reversed list of components making up the relative
242 # `rel' holds the reversed list of components making up the relative
243 # file name we want.
243 # file name we want.
244 rel = []
244 rel = []
245 while True:
245 while True:
246 try:
246 try:
247 s = util.samefile(name, root)
247 s = util.samefile(name, root)
248 except OSError:
248 except OSError:
249 s = False
249 s = False
250 if s:
250 if s:
251 if not rel:
251 if not rel:
252 # name was actually the same as root (maybe a symlink)
252 # name was actually the same as root (maybe a symlink)
253 return b''
253 return b''
254 rel.reverse()
254 rel.reverse()
255 name = os.path.join(*rel)
255 name = os.path.join(*rel)
256 auditor(name)
256 auditor(name)
257 return util.pconvert(name)
257 return util.pconvert(name)
258 dirname, basename = util.split(name)
258 dirname, basename = util.split(name)
259 rel.append(basename)
259 rel.append(basename)
260 if dirname == name:
260 if dirname == name:
261 break
261 break
262 name = dirname
262 name = dirname
263
263
264 # A common mistake is to use -R, but specify a file relative to the repo
264 # A common mistake is to use -R, but specify a file relative to the repo
265 # instead of cwd. Detect that case, and provide a hint to the user.
265 # instead of cwd. Detect that case, and provide a hint to the user.
266 hint = None
266 hint = None
267 try:
267 try:
268 if cwd != root:
268 if cwd != root:
269 canonpath(root, root, myname, auditor)
269 canonpath(root, root, myname, auditor)
270 relpath = util.pathto(root, cwd, b'')
270 relpath = util.pathto(root, cwd, b'')
271 if relpath.endswith(pycompat.ossep):
271 if relpath.endswith(pycompat.ossep):
272 relpath = relpath[:-1]
272 relpath = relpath[:-1]
273 hint = _(b"consider using '--cwd %s'") % relpath
273 hint = _(b"consider using '--cwd %s'") % relpath
274 except error.Abort:
274 except error.Abort:
275 pass
275 pass
276
276
277 raise error.Abort(
277 raise error.Abort(
278 _(b"%s not under root '%s'") % (myname, root), hint=hint
278 _(b"%s not under root '%s'") % (myname, root), hint=hint
279 )
279 )
280
280
281
281
282 def normasprefix(path):
282 def normasprefix(path):
283 # type: (bytes) -> bytes
283 # type: (bytes) -> bytes
284 """normalize the specified path as path prefix
284 """normalize the specified path as path prefix
285
285
286 Returned value can be used safely for "p.startswith(prefix)",
286 Returned value can be used safely for "p.startswith(prefix)",
287 "p[len(prefix):]", and so on.
287 "p[len(prefix):]", and so on.
288
288
289 For efficiency, this expects "path" argument to be already
289 For efficiency, this expects "path" argument to be already
290 normalized by "os.path.normpath", "os.path.realpath", and so on.
290 normalized by "os.path.normpath", "os.path.realpath", and so on.
291
291
292 See also issue3033 for detail about need of this function.
292 See also issue3033 for detail about need of this function.
293
293
294 >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/')
294 >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/')
295 '/foo/bar/'
295 '/foo/bar/'
296 >>> normasprefix(b'/').replace(pycompat.ossep, b'/')
296 >>> normasprefix(b'/').replace(pycompat.ossep, b'/')
297 '/'
297 '/'
298 """
298 """
299 d, p = os.path.splitdrive(path)
299 d, p = os.path.splitdrive(path)
300 if len(p) != len(pycompat.ossep):
300 if len(p) != len(pycompat.ossep):
301 return path + pycompat.ossep
301 return path + pycompat.ossep
302 else:
302 else:
303 return path
303 return path
304
304
305
305
306 def finddirs(path):
306 def finddirs(path):
307 # type: (bytes) -> Iterator[bytes]
307 # type: (bytes) -> Iterator[bytes]
308 pos = path.rfind(b'/')
308 pos = path.rfind(b'/')
309 while pos != -1:
309 while pos != -1:
310 yield path[:pos]
310 yield path[:pos]
311 pos = path.rfind(b'/', 0, pos)
311 pos = path.rfind(b'/', 0, pos)
312 yield b''
312 yield b''
313
313
314
314
315 class dirs(object):
315 class dirs(object):
316 '''a multiset of directory names from a set of file paths'''
316 '''a multiset of directory names from a set of file paths'''
317
317
318 def __init__(self, map, skip=None):
318 def __init__(self, map, only_tracked=False):
319 """
319 """
320 a dict map indicates a dirstate while a list indicates a manifest
320 a dict map indicates a dirstate while a list indicates a manifest
321 """
321 """
322 self._dirs = {}
322 self._dirs = {}
323 addpath = self.addpath
323 addpath = self.addpath
324 if isinstance(map, dict) and skip is not None:
324 if isinstance(map, dict) and only_tracked:
325 for f, s in pycompat.iteritems(map):
325 for f, s in pycompat.iteritems(map):
326 if s.state != skip:
326 if s.state != b'r':
327 addpath(f)
327 addpath(f)
328 elif skip is not None:
328 elif only_tracked:
329 raise error.ProgrammingError(
329 msg = b"`only_tracked` is only supported with a dict source"
330 b"skip character is only supported with a dict source"
330 raise error.ProgrammingError(msg)
331 )
332 else:
331 else:
333 for f in map:
332 for f in map:
334 addpath(f)
333 addpath(f)
335
334
336 def addpath(self, path):
335 def addpath(self, path):
337 # type: (bytes) -> None
336 # type: (bytes) -> None
338 dirs = self._dirs
337 dirs = self._dirs
339 for base in finddirs(path):
338 for base in finddirs(path):
340 if base.endswith(b'/'):
339 if base.endswith(b'/'):
341 raise ValueError(
340 raise ValueError(
342 "found invalid consecutive slashes in path: %r" % base
341 "found invalid consecutive slashes in path: %r" % base
343 )
342 )
344 if base in dirs:
343 if base in dirs:
345 dirs[base] += 1
344 dirs[base] += 1
346 return
345 return
347 dirs[base] = 1
346 dirs[base] = 1
348
347
349 def delpath(self, path):
348 def delpath(self, path):
350 # type: (bytes) -> None
349 # type: (bytes) -> None
351 dirs = self._dirs
350 dirs = self._dirs
352 for base in finddirs(path):
351 for base in finddirs(path):
353 if dirs[base] > 1:
352 if dirs[base] > 1:
354 dirs[base] -= 1
353 dirs[base] -= 1
355 return
354 return
356 del dirs[base]
355 del dirs[base]
357
356
358 def __iter__(self):
357 def __iter__(self):
359 return iter(self._dirs)
358 return iter(self._dirs)
360
359
361 def __contains__(self, d):
360 def __contains__(self, d):
362 # type: (bytes) -> bool
361 # type: (bytes) -> bool
363 return d in self._dirs
362 return d in self._dirs
364
363
365
364
366 if util.safehasattr(parsers, 'dirs'):
365 if util.safehasattr(parsers, 'dirs'):
367 dirs = parsers.dirs
366 dirs = parsers.dirs
368
367
369 if rustdirs is not None:
368 if rustdirs is not None:
370 dirs = rustdirs
369 dirs = rustdirs
371
370
372
371
373 # forward two methods from posixpath that do what we need, but we'd
372 # forward two methods from posixpath that do what we need, but we'd
374 # rather not let our internals know that we're thinking in posix terms
373 # rather not let our internals know that we're thinking in posix terms
375 # - instead we'll let them be oblivious.
374 # - instead we'll let them be oblivious.
376 join = posixpath.join
375 join = posixpath.join
377 dirname = posixpath.dirname # type: Callable[[bytes], bytes]
376 dirname = posixpath.dirname # type: Callable[[bytes], bytes]
@@ -1,430 +1,428 b''
1 // dirs_multiset.rs
1 // dirs_multiset.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! A multiset of directory names.
8 //! A multiset of directory names.
9 //!
9 //!
10 //! Used to counts the references to directories in a manifest or dirstate.
10 //! Used to counts the references to directories in a manifest or dirstate.
11 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
11 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
12 use crate::{
12 use crate::{
13 dirstate::EntryState,
13 dirstate::EntryState,
14 utils::{
14 utils::{
15 files,
15 files,
16 hg_path::{HgPath, HgPathBuf, HgPathError},
16 hg_path::{HgPath, HgPathBuf, HgPathError},
17 },
17 },
18 DirstateEntry, DirstateError, DirstateMapError, FastHashMap,
18 DirstateEntry, DirstateError, DirstateMapError, FastHashMap,
19 };
19 };
20 use std::collections::{hash_map, hash_map::Entry, HashMap, HashSet};
20 use std::collections::{hash_map, hash_map::Entry, HashMap, HashSet};
21
21
22 // could be encapsulated if we care API stability more seriously
22 // could be encapsulated if we care API stability more seriously
23 pub type DirsMultisetIter<'a> = hash_map::Keys<'a, HgPathBuf, u32>;
23 pub type DirsMultisetIter<'a> = hash_map::Keys<'a, HgPathBuf, u32>;
24
24
25 #[derive(PartialEq, Debug)]
25 #[derive(PartialEq, Debug)]
26 pub struct DirsMultiset {
26 pub struct DirsMultiset {
27 inner: FastHashMap<HgPathBuf, u32>,
27 inner: FastHashMap<HgPathBuf, u32>,
28 }
28 }
29
29
30 impl DirsMultiset {
30 impl DirsMultiset {
31 /// Initializes the multiset from a dirstate.
31 /// Initializes the multiset from a dirstate.
32 ///
32 ///
33 /// If `skip_state` is provided, skips dirstate entries with equal state.
33 /// If `skip_state` is provided, skips dirstate entries with equal state.
34 pub fn from_dirstate<I, P>(
34 pub fn from_dirstate<I, P>(
35 dirstate: I,
35 dirstate: I,
36 skip_state: Option<EntryState>,
36 only_tracked: bool,
37 ) -> Result<Self, DirstateError>
37 ) -> Result<Self, DirstateError>
38 where
38 where
39 I: IntoIterator<
39 I: IntoIterator<
40 Item = Result<(P, DirstateEntry), DirstateV2ParseError>,
40 Item = Result<(P, DirstateEntry), DirstateV2ParseError>,
41 >,
41 >,
42 P: AsRef<HgPath>,
42 P: AsRef<HgPath>,
43 {
43 {
44 let mut multiset = DirsMultiset {
44 let mut multiset = DirsMultiset {
45 inner: FastHashMap::default(),
45 inner: FastHashMap::default(),
46 };
46 };
47 for item in dirstate {
47 for item in dirstate {
48 let (filename, entry) = item?;
48 let (filename, entry) = item?;
49 let filename = filename.as_ref();
49 let filename = filename.as_ref();
50 // This `if` is optimized out of the loop
50 // This `if` is optimized out of the loop
51 if let Some(skip) = skip_state {
51 if only_tracked {
52 if skip != entry.state {
52 if entry.state != EntryState::Removed {
53 multiset.add_path(filename)?;
53 multiset.add_path(filename)?;
54 }
54 }
55 } else {
55 } else {
56 multiset.add_path(filename)?;
56 multiset.add_path(filename)?;
57 }
57 }
58 }
58 }
59
59
60 Ok(multiset)
60 Ok(multiset)
61 }
61 }
62
62
63 /// Initializes the multiset from a manifest.
63 /// Initializes the multiset from a manifest.
64 pub fn from_manifest(
64 pub fn from_manifest(
65 manifest: &[impl AsRef<HgPath>],
65 manifest: &[impl AsRef<HgPath>],
66 ) -> Result<Self, DirstateMapError> {
66 ) -> Result<Self, DirstateMapError> {
67 let mut multiset = DirsMultiset {
67 let mut multiset = DirsMultiset {
68 inner: FastHashMap::default(),
68 inner: FastHashMap::default(),
69 };
69 };
70
70
71 for filename in manifest {
71 for filename in manifest {
72 multiset.add_path(filename.as_ref())?;
72 multiset.add_path(filename.as_ref())?;
73 }
73 }
74
74
75 Ok(multiset)
75 Ok(multiset)
76 }
76 }
77
77
78 /// Increases the count of deepest directory contained in the path.
78 /// Increases the count of deepest directory contained in the path.
79 ///
79 ///
80 /// If the directory is not yet in the map, adds its parents.
80 /// If the directory is not yet in the map, adds its parents.
81 pub fn add_path(
81 pub fn add_path(
82 &mut self,
82 &mut self,
83 path: impl AsRef<HgPath>,
83 path: impl AsRef<HgPath>,
84 ) -> Result<(), DirstateMapError> {
84 ) -> Result<(), DirstateMapError> {
85 for subpath in files::find_dirs(path.as_ref()) {
85 for subpath in files::find_dirs(path.as_ref()) {
86 if subpath.as_bytes().last() == Some(&b'/') {
86 if subpath.as_bytes().last() == Some(&b'/') {
87 // TODO Remove this once PathAuditor is certified
87 // TODO Remove this once PathAuditor is certified
88 // as the only entrypoint for path data
88 // as the only entrypoint for path data
89 let second_slash_index = subpath.len() - 1;
89 let second_slash_index = subpath.len() - 1;
90
90
91 return Err(DirstateMapError::InvalidPath(
91 return Err(DirstateMapError::InvalidPath(
92 HgPathError::ConsecutiveSlashes {
92 HgPathError::ConsecutiveSlashes {
93 bytes: path.as_ref().as_bytes().to_owned(),
93 bytes: path.as_ref().as_bytes().to_owned(),
94 second_slash_index,
94 second_slash_index,
95 },
95 },
96 ));
96 ));
97 }
97 }
98 if let Some(val) = self.inner.get_mut(subpath) {
98 if let Some(val) = self.inner.get_mut(subpath) {
99 *val += 1;
99 *val += 1;
100 break;
100 break;
101 }
101 }
102 self.inner.insert(subpath.to_owned(), 1);
102 self.inner.insert(subpath.to_owned(), 1);
103 }
103 }
104 Ok(())
104 Ok(())
105 }
105 }
106
106
107 /// Decreases the count of deepest directory contained in the path.
107 /// Decreases the count of deepest directory contained in the path.
108 ///
108 ///
109 /// If it is the only reference, decreases all parents until one is
109 /// If it is the only reference, decreases all parents until one is
110 /// removed.
110 /// removed.
111 /// If the directory is not in the map, something horrible has happened.
111 /// If the directory is not in the map, something horrible has happened.
112 pub fn delete_path(
112 pub fn delete_path(
113 &mut self,
113 &mut self,
114 path: impl AsRef<HgPath>,
114 path: impl AsRef<HgPath>,
115 ) -> Result<(), DirstateMapError> {
115 ) -> Result<(), DirstateMapError> {
116 for subpath in files::find_dirs(path.as_ref()) {
116 for subpath in files::find_dirs(path.as_ref()) {
117 match self.inner.entry(subpath.to_owned()) {
117 match self.inner.entry(subpath.to_owned()) {
118 Entry::Occupied(mut entry) => {
118 Entry::Occupied(mut entry) => {
119 let val = *entry.get();
119 let val = *entry.get();
120 if val > 1 {
120 if val > 1 {
121 entry.insert(val - 1);
121 entry.insert(val - 1);
122 break;
122 break;
123 }
123 }
124 entry.remove();
124 entry.remove();
125 }
125 }
126 Entry::Vacant(_) => {
126 Entry::Vacant(_) => {
127 return Err(DirstateMapError::PathNotFound(
127 return Err(DirstateMapError::PathNotFound(
128 path.as_ref().to_owned(),
128 path.as_ref().to_owned(),
129 ))
129 ))
130 }
130 }
131 };
131 };
132 }
132 }
133
133
134 Ok(())
134 Ok(())
135 }
135 }
136
136
137 pub fn contains(&self, key: impl AsRef<HgPath>) -> bool {
137 pub fn contains(&self, key: impl AsRef<HgPath>) -> bool {
138 self.inner.contains_key(key.as_ref())
138 self.inner.contains_key(key.as_ref())
139 }
139 }
140
140
141 pub fn iter(&self) -> DirsMultisetIter {
141 pub fn iter(&self) -> DirsMultisetIter {
142 self.inner.keys()
142 self.inner.keys()
143 }
143 }
144
144
145 pub fn len(&self) -> usize {
145 pub fn len(&self) -> usize {
146 self.inner.len()
146 self.inner.len()
147 }
147 }
148
148
149 pub fn is_empty(&self) -> bool {
149 pub fn is_empty(&self) -> bool {
150 self.len() == 0
150 self.len() == 0
151 }
151 }
152 }
152 }
153
153
154 /// This is basically a reimplementation of `DirsMultiset` that stores the
154 /// This is basically a reimplementation of `DirsMultiset` that stores the
155 /// children instead of just a count of them, plus a small optional
155 /// children instead of just a count of them, plus a small optional
156 /// optimization to avoid some directories we don't need.
156 /// optimization to avoid some directories we don't need.
157 #[derive(PartialEq, Debug)]
157 #[derive(PartialEq, Debug)]
158 pub struct DirsChildrenMultiset<'a> {
158 pub struct DirsChildrenMultiset<'a> {
159 inner: FastHashMap<&'a HgPath, HashSet<&'a HgPath>>,
159 inner: FastHashMap<&'a HgPath, HashSet<&'a HgPath>>,
160 only_include: Option<HashSet<&'a HgPath>>,
160 only_include: Option<HashSet<&'a HgPath>>,
161 }
161 }
162
162
163 impl<'a> DirsChildrenMultiset<'a> {
163 impl<'a> DirsChildrenMultiset<'a> {
164 pub fn new(
164 pub fn new(
165 paths: impl Iterator<Item = &'a HgPathBuf>,
165 paths: impl Iterator<Item = &'a HgPathBuf>,
166 only_include: Option<&'a HashSet<impl AsRef<HgPath> + 'a>>,
166 only_include: Option<&'a HashSet<impl AsRef<HgPath> + 'a>>,
167 ) -> Self {
167 ) -> Self {
168 let mut new = Self {
168 let mut new = Self {
169 inner: HashMap::default(),
169 inner: HashMap::default(),
170 only_include: only_include
170 only_include: only_include
171 .map(|s| s.iter().map(AsRef::as_ref).collect()),
171 .map(|s| s.iter().map(AsRef::as_ref).collect()),
172 };
172 };
173
173
174 for path in paths {
174 for path in paths {
175 new.add_path(path)
175 new.add_path(path)
176 }
176 }
177
177
178 new
178 new
179 }
179 }
180 fn add_path(&mut self, path: &'a (impl AsRef<HgPath> + 'a)) {
180 fn add_path(&mut self, path: &'a (impl AsRef<HgPath> + 'a)) {
181 if path.as_ref().is_empty() {
181 if path.as_ref().is_empty() {
182 return;
182 return;
183 }
183 }
184 for (directory, basename) in files::find_dirs_with_base(path.as_ref())
184 for (directory, basename) in files::find_dirs_with_base(path.as_ref())
185 {
185 {
186 if !self.is_dir_included(directory) {
186 if !self.is_dir_included(directory) {
187 continue;
187 continue;
188 }
188 }
189 self.inner
189 self.inner
190 .entry(directory)
190 .entry(directory)
191 .and_modify(|e| {
191 .and_modify(|e| {
192 e.insert(basename);
192 e.insert(basename);
193 })
193 })
194 .or_insert_with(|| {
194 .or_insert_with(|| {
195 let mut set = HashSet::new();
195 let mut set = HashSet::new();
196 set.insert(basename);
196 set.insert(basename);
197 set
197 set
198 });
198 });
199 }
199 }
200 }
200 }
201 fn is_dir_included(&self, dir: impl AsRef<HgPath>) -> bool {
201 fn is_dir_included(&self, dir: impl AsRef<HgPath>) -> bool {
202 match &self.only_include {
202 match &self.only_include {
203 None => false,
203 None => false,
204 Some(i) => i.contains(dir.as_ref()),
204 Some(i) => i.contains(dir.as_ref()),
205 }
205 }
206 }
206 }
207
207
208 pub fn get(
208 pub fn get(
209 &self,
209 &self,
210 path: impl AsRef<HgPath>,
210 path: impl AsRef<HgPath>,
211 ) -> Option<&HashSet<&'a HgPath>> {
211 ) -> Option<&HashSet<&'a HgPath>> {
212 self.inner.get(path.as_ref())
212 self.inner.get(path.as_ref())
213 }
213 }
214 }
214 }
215
215
216 #[cfg(test)]
216 #[cfg(test)]
217 mod tests {
217 mod tests {
218 use super::*;
218 use super::*;
219 use crate::StateMap;
219 use crate::StateMap;
220
220
221 #[test]
221 #[test]
222 fn test_delete_path_path_not_found() {
222 fn test_delete_path_path_not_found() {
223 let manifest: Vec<HgPathBuf> = vec![];
223 let manifest: Vec<HgPathBuf> = vec![];
224 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
224 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
225 let path = HgPathBuf::from_bytes(b"doesnotexist/");
225 let path = HgPathBuf::from_bytes(b"doesnotexist/");
226 assert_eq!(
226 assert_eq!(
227 Err(DirstateMapError::PathNotFound(path.to_owned())),
227 Err(DirstateMapError::PathNotFound(path.to_owned())),
228 map.delete_path(&path)
228 map.delete_path(&path)
229 );
229 );
230 }
230 }
231
231
232 #[test]
232 #[test]
233 fn test_delete_path_empty_path() {
233 fn test_delete_path_empty_path() {
234 let mut map =
234 let mut map =
235 DirsMultiset::from_manifest(&vec![HgPathBuf::new()]).unwrap();
235 DirsMultiset::from_manifest(&vec![HgPathBuf::new()]).unwrap();
236 let path = HgPath::new(b"");
236 let path = HgPath::new(b"");
237 assert_eq!(Ok(()), map.delete_path(path));
237 assert_eq!(Ok(()), map.delete_path(path));
238 assert_eq!(
238 assert_eq!(
239 Err(DirstateMapError::PathNotFound(path.to_owned())),
239 Err(DirstateMapError::PathNotFound(path.to_owned())),
240 map.delete_path(path)
240 map.delete_path(path)
241 );
241 );
242 }
242 }
243
243
244 #[test]
244 #[test]
245 fn test_delete_path_successful() {
245 fn test_delete_path_successful() {
246 let mut map = DirsMultiset {
246 let mut map = DirsMultiset {
247 inner: [("", 5), ("a", 3), ("a/b", 2), ("a/c", 1)]
247 inner: [("", 5), ("a", 3), ("a/b", 2), ("a/c", 1)]
248 .iter()
248 .iter()
249 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
249 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
250 .collect(),
250 .collect(),
251 };
251 };
252
252
253 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
253 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
254 eprintln!("{:?}", map);
254 eprintln!("{:?}", map);
255 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
255 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
256 eprintln!("{:?}", map);
256 eprintln!("{:?}", map);
257 assert_eq!(
257 assert_eq!(
258 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
258 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
259 b"a/b/"
259 b"a/b/"
260 ))),
260 ))),
261 map.delete_path(HgPath::new(b"a/b/"))
261 map.delete_path(HgPath::new(b"a/b/"))
262 );
262 );
263
263
264 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
264 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
265 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
265 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
266 eprintln!("{:?}", map);
266 eprintln!("{:?}", map);
267 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/")));
267 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/")));
268 eprintln!("{:?}", map);
268 eprintln!("{:?}", map);
269
269
270 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/c/")));
270 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/c/")));
271 assert_eq!(
271 assert_eq!(
272 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
272 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
273 b"a/c/"
273 b"a/c/"
274 ))),
274 ))),
275 map.delete_path(HgPath::new(b"a/c/"))
275 map.delete_path(HgPath::new(b"a/c/"))
276 );
276 );
277 }
277 }
278
278
279 #[test]
279 #[test]
280 fn test_add_path_empty_path() {
280 fn test_add_path_empty_path() {
281 let manifest: Vec<HgPathBuf> = vec![];
281 let manifest: Vec<HgPathBuf> = vec![];
282 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
282 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
283 let path = HgPath::new(b"");
283 let path = HgPath::new(b"");
284 map.add_path(path).unwrap();
284 map.add_path(path).unwrap();
285
285
286 assert_eq!(1, map.len());
286 assert_eq!(1, map.len());
287 }
287 }
288
288
289 #[test]
289 #[test]
290 fn test_add_path_successful() {
290 fn test_add_path_successful() {
291 let manifest: Vec<HgPathBuf> = vec![];
291 let manifest: Vec<HgPathBuf> = vec![];
292 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
292 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
293
293
294 map.add_path(HgPath::new(b"a/")).unwrap();
294 map.add_path(HgPath::new(b"a/")).unwrap();
295 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
295 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
296 assert_eq!(1, *map.inner.get(HgPath::new(b"")).unwrap());
296 assert_eq!(1, *map.inner.get(HgPath::new(b"")).unwrap());
297 assert_eq!(2, map.len());
297 assert_eq!(2, map.len());
298
298
299 // Non directory should be ignored
299 // Non directory should be ignored
300 map.add_path(HgPath::new(b"a")).unwrap();
300 map.add_path(HgPath::new(b"a")).unwrap();
301 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
301 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
302 assert_eq!(2, map.len());
302 assert_eq!(2, map.len());
303
303
304 // Non directory will still add its base
304 // Non directory will still add its base
305 map.add_path(HgPath::new(b"a/b")).unwrap();
305 map.add_path(HgPath::new(b"a/b")).unwrap();
306 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
306 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
307 assert_eq!(2, map.len());
307 assert_eq!(2, map.len());
308
308
309 // Duplicate path works
309 // Duplicate path works
310 map.add_path(HgPath::new(b"a/")).unwrap();
310 map.add_path(HgPath::new(b"a/")).unwrap();
311 assert_eq!(3, *map.inner.get(HgPath::new(b"a")).unwrap());
311 assert_eq!(3, *map.inner.get(HgPath::new(b"a")).unwrap());
312
312
313 // Nested dir adds to its base
313 // Nested dir adds to its base
314 map.add_path(HgPath::new(b"a/b/")).unwrap();
314 map.add_path(HgPath::new(b"a/b/")).unwrap();
315 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
315 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
316 assert_eq!(1, *map.inner.get(HgPath::new(b"a/b")).unwrap());
316 assert_eq!(1, *map.inner.get(HgPath::new(b"a/b")).unwrap());
317
317
318 // but not its base's base, because it already existed
318 // but not its base's base, because it already existed
319 map.add_path(HgPath::new(b"a/b/c/")).unwrap();
319 map.add_path(HgPath::new(b"a/b/c/")).unwrap();
320 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
320 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
321 assert_eq!(2, *map.inner.get(HgPath::new(b"a/b")).unwrap());
321 assert_eq!(2, *map.inner.get(HgPath::new(b"a/b")).unwrap());
322
322
323 map.add_path(HgPath::new(b"a/c/")).unwrap();
323 map.add_path(HgPath::new(b"a/c/")).unwrap();
324 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
324 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
325
325
326 let expected = DirsMultiset {
326 let expected = DirsMultiset {
327 inner: [("", 2), ("a", 5), ("a/b", 2), ("a/b/c", 1), ("a/c", 1)]
327 inner: [("", 2), ("a", 5), ("a/b", 2), ("a/b/c", 1), ("a/c", 1)]
328 .iter()
328 .iter()
329 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
329 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
330 .collect(),
330 .collect(),
331 };
331 };
332 assert_eq!(map, expected);
332 assert_eq!(map, expected);
333 }
333 }
334
334
335 #[test]
335 #[test]
336 fn test_dirsmultiset_new_empty() {
336 fn test_dirsmultiset_new_empty() {
337 let manifest: Vec<HgPathBuf> = vec![];
337 let manifest: Vec<HgPathBuf> = vec![];
338 let new = DirsMultiset::from_manifest(&manifest).unwrap();
338 let new = DirsMultiset::from_manifest(&manifest).unwrap();
339 let expected = DirsMultiset {
339 let expected = DirsMultiset {
340 inner: FastHashMap::default(),
340 inner: FastHashMap::default(),
341 };
341 };
342 assert_eq!(expected, new);
342 assert_eq!(expected, new);
343
343
344 let new = DirsMultiset::from_dirstate(
344 let new = DirsMultiset::from_dirstate(
345 StateMap::default().into_iter().map(Ok),
345 StateMap::default().into_iter().map(Ok),
346 None,
346 false,
347 )
347 )
348 .unwrap();
348 .unwrap();
349 let expected = DirsMultiset {
349 let expected = DirsMultiset {
350 inner: FastHashMap::default(),
350 inner: FastHashMap::default(),
351 };
351 };
352 assert_eq!(expected, new);
352 assert_eq!(expected, new);
353 }
353 }
354
354
355 #[test]
355 #[test]
356 fn test_dirsmultiset_new_no_skip() {
356 fn test_dirsmultiset_new_no_skip() {
357 let input_vec: Vec<HgPathBuf> = ["a/", "b/", "a/c", "a/d/"]
357 let input_vec: Vec<HgPathBuf> = ["a/", "b/", "a/c", "a/d/"]
358 .iter()
358 .iter()
359 .map(|e| HgPathBuf::from_bytes(e.as_bytes()))
359 .map(|e| HgPathBuf::from_bytes(e.as_bytes()))
360 .collect();
360 .collect();
361 let expected_inner = [("", 2), ("a", 3), ("b", 1), ("a/d", 1)]
361 let expected_inner = [("", 2), ("a", 3), ("b", 1), ("a/d", 1)]
362 .iter()
362 .iter()
363 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
363 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
364 .collect();
364 .collect();
365
365
366 let new = DirsMultiset::from_manifest(&input_vec).unwrap();
366 let new = DirsMultiset::from_manifest(&input_vec).unwrap();
367 let expected = DirsMultiset {
367 let expected = DirsMultiset {
368 inner: expected_inner,
368 inner: expected_inner,
369 };
369 };
370 assert_eq!(expected, new);
370 assert_eq!(expected, new);
371
371
372 let input_map = ["b/x", "a/c", "a/d/x"].iter().map(|f| {
372 let input_map = ["b/x", "a/c", "a/d/x"].iter().map(|f| {
373 Ok((
373 Ok((
374 HgPathBuf::from_bytes(f.as_bytes()),
374 HgPathBuf::from_bytes(f.as_bytes()),
375 DirstateEntry {
375 DirstateEntry {
376 state: EntryState::Normal,
376 state: EntryState::Normal,
377 mode: 0,
377 mode: 0,
378 mtime: 0,
378 mtime: 0,
379 size: 0,
379 size: 0,
380 },
380 },
381 ))
381 ))
382 });
382 });
383 let expected_inner = [("", 2), ("a", 2), ("b", 1), ("a/d", 1)]
383 let expected_inner = [("", 2), ("a", 2), ("b", 1), ("a/d", 1)]
384 .iter()
384 .iter()
385 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
385 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
386 .collect();
386 .collect();
387
387
388 let new = DirsMultiset::from_dirstate(input_map, None).unwrap();
388 let new = DirsMultiset::from_dirstate(input_map, false).unwrap();
389 let expected = DirsMultiset {
389 let expected = DirsMultiset {
390 inner: expected_inner,
390 inner: expected_inner,
391 };
391 };
392 assert_eq!(expected, new);
392 assert_eq!(expected, new);
393 }
393 }
394
394
395 #[test]
395 #[test]
396 fn test_dirsmultiset_new_skip() {
396 fn test_dirsmultiset_new_skip() {
397 let input_map = [
397 let input_map = [
398 ("a/", EntryState::Normal),
398 ("a/", EntryState::Normal),
399 ("a/b", EntryState::Normal),
399 ("a/b", EntryState::Normal),
400 ("a/c", EntryState::Removed),
400 ("a/c", EntryState::Removed),
401 ("a/d", EntryState::Merged),
401 ("a/d", EntryState::Merged),
402 ]
402 ]
403 .iter()
403 .iter()
404 .map(|(f, state)| {
404 .map(|(f, state)| {
405 Ok((
405 Ok((
406 HgPathBuf::from_bytes(f.as_bytes()),
406 HgPathBuf::from_bytes(f.as_bytes()),
407 DirstateEntry {
407 DirstateEntry {
408 state: *state,
408 state: *state,
409 mode: 0,
409 mode: 0,
410 mtime: 0,
410 mtime: 0,
411 size: 0,
411 size: 0,
412 },
412 },
413 ))
413 ))
414 });
414 });
415
415
416 // "a" incremented with "a/c" and "a/d/"
416 // "a" incremented with "a/c" and "a/d/"
417 let expected_inner = [("", 1), ("a", 2)]
417 let expected_inner = [("", 1), ("a", 3)]
418 .iter()
418 .iter()
419 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
419 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
420 .collect();
420 .collect();
421
421
422 let new =
422 let new = DirsMultiset::from_dirstate(input_map, true).unwrap();
423 DirsMultiset::from_dirstate(input_map, Some(EntryState::Normal))
424 .unwrap();
425 let expected = DirsMultiset {
423 let expected = DirsMultiset {
426 inner: expected_inner,
424 inner: expected_inner,
427 };
425 };
428 assert_eq!(expected, new);
426 assert_eq!(expected, new);
429 }
427 }
430 }
428 }
@@ -1,494 +1,494 b''
1 // dirstate_map.rs
1 // dirstate_map.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 use crate::dirstate::parsers::Timestamp;
8 use crate::dirstate::parsers::Timestamp;
9 use crate::{
9 use crate::{
10 dirstate::EntryState,
10 dirstate::EntryState,
11 dirstate::MTIME_UNSET,
11 dirstate::MTIME_UNSET,
12 dirstate::SIZE_FROM_OTHER_PARENT,
12 dirstate::SIZE_FROM_OTHER_PARENT,
13 dirstate::SIZE_NON_NORMAL,
13 dirstate::SIZE_NON_NORMAL,
14 dirstate::V1_RANGEMASK,
14 dirstate::V1_RANGEMASK,
15 pack_dirstate, parse_dirstate,
15 pack_dirstate, parse_dirstate,
16 utils::hg_path::{HgPath, HgPathBuf},
16 utils::hg_path::{HgPath, HgPathBuf},
17 CopyMap, DirsMultiset, DirstateEntry, DirstateError, DirstateParents,
17 CopyMap, DirsMultiset, DirstateEntry, DirstateError, DirstateParents,
18 StateMap,
18 StateMap,
19 };
19 };
20 use micro_timer::timed;
20 use micro_timer::timed;
21 use std::collections::HashSet;
21 use std::collections::HashSet;
22 use std::iter::FromIterator;
22 use std::iter::FromIterator;
23 use std::ops::Deref;
23 use std::ops::Deref;
24
24
25 #[derive(Default)]
25 #[derive(Default)]
26 pub struct DirstateMap {
26 pub struct DirstateMap {
27 state_map: StateMap,
27 state_map: StateMap,
28 pub copy_map: CopyMap,
28 pub copy_map: CopyMap,
29 pub dirs: Option<DirsMultiset>,
29 pub dirs: Option<DirsMultiset>,
30 pub all_dirs: Option<DirsMultiset>,
30 pub all_dirs: Option<DirsMultiset>,
31 non_normal_set: Option<HashSet<HgPathBuf>>,
31 non_normal_set: Option<HashSet<HgPathBuf>>,
32 other_parent_set: Option<HashSet<HgPathBuf>>,
32 other_parent_set: Option<HashSet<HgPathBuf>>,
33 }
33 }
34
34
35 /// Should only really be used in python interface code, for clarity
35 /// Should only really be used in python interface code, for clarity
36 impl Deref for DirstateMap {
36 impl Deref for DirstateMap {
37 type Target = StateMap;
37 type Target = StateMap;
38
38
39 fn deref(&self) -> &Self::Target {
39 fn deref(&self) -> &Self::Target {
40 &self.state_map
40 &self.state_map
41 }
41 }
42 }
42 }
43
43
44 impl FromIterator<(HgPathBuf, DirstateEntry)> for DirstateMap {
44 impl FromIterator<(HgPathBuf, DirstateEntry)> for DirstateMap {
45 fn from_iter<I: IntoIterator<Item = (HgPathBuf, DirstateEntry)>>(
45 fn from_iter<I: IntoIterator<Item = (HgPathBuf, DirstateEntry)>>(
46 iter: I,
46 iter: I,
47 ) -> Self {
47 ) -> Self {
48 Self {
48 Self {
49 state_map: iter.into_iter().collect(),
49 state_map: iter.into_iter().collect(),
50 ..Self::default()
50 ..Self::default()
51 }
51 }
52 }
52 }
53 }
53 }
54
54
55 impl DirstateMap {
55 impl DirstateMap {
56 pub fn new() -> Self {
56 pub fn new() -> Self {
57 Self::default()
57 Self::default()
58 }
58 }
59
59
60 pub fn clear(&mut self) {
60 pub fn clear(&mut self) {
61 self.state_map = StateMap::default();
61 self.state_map = StateMap::default();
62 self.copy_map.clear();
62 self.copy_map.clear();
63 self.non_normal_set = None;
63 self.non_normal_set = None;
64 self.other_parent_set = None;
64 self.other_parent_set = None;
65 }
65 }
66
66
67 pub fn set_v1_inner(&mut self, filename: &HgPath, entry: DirstateEntry) {
67 pub fn set_v1_inner(&mut self, filename: &HgPath, entry: DirstateEntry) {
68 self.state_map.insert(filename.to_owned(), entry);
68 self.state_map.insert(filename.to_owned(), entry);
69 }
69 }
70
70
71 /// Add a tracked file to the dirstate
71 /// Add a tracked file to the dirstate
72 pub fn add_file(
72 pub fn add_file(
73 &mut self,
73 &mut self,
74 filename: &HgPath,
74 filename: &HgPath,
75 entry: DirstateEntry,
75 entry: DirstateEntry,
76 // XXX once the dust settle this should probably become an enum
76 // XXX once the dust settle this should probably become an enum
77 added: bool,
77 added: bool,
78 merged: bool,
78 merged: bool,
79 from_p2: bool,
79 from_p2: bool,
80 possibly_dirty: bool,
80 possibly_dirty: bool,
81 ) -> Result<(), DirstateError> {
81 ) -> Result<(), DirstateError> {
82 let mut entry = entry;
82 let mut entry = entry;
83 if added {
83 if added {
84 assert!(!merged);
84 assert!(!merged);
85 assert!(!possibly_dirty);
85 assert!(!possibly_dirty);
86 assert!(!from_p2);
86 assert!(!from_p2);
87 entry.state = EntryState::Added;
87 entry.state = EntryState::Added;
88 entry.size = SIZE_NON_NORMAL;
88 entry.size = SIZE_NON_NORMAL;
89 entry.mtime = MTIME_UNSET;
89 entry.mtime = MTIME_UNSET;
90 } else if merged {
90 } else if merged {
91 assert!(!possibly_dirty);
91 assert!(!possibly_dirty);
92 assert!(!from_p2);
92 assert!(!from_p2);
93 entry.state = EntryState::Merged;
93 entry.state = EntryState::Merged;
94 entry.size = SIZE_FROM_OTHER_PARENT;
94 entry.size = SIZE_FROM_OTHER_PARENT;
95 entry.mtime = MTIME_UNSET;
95 entry.mtime = MTIME_UNSET;
96 } else if from_p2 {
96 } else if from_p2 {
97 assert!(!possibly_dirty);
97 assert!(!possibly_dirty);
98 entry.state = EntryState::Normal;
98 entry.state = EntryState::Normal;
99 entry.size = SIZE_FROM_OTHER_PARENT;
99 entry.size = SIZE_FROM_OTHER_PARENT;
100 entry.mtime = MTIME_UNSET;
100 entry.mtime = MTIME_UNSET;
101 } else if possibly_dirty {
101 } else if possibly_dirty {
102 entry.state = EntryState::Normal;
102 entry.state = EntryState::Normal;
103 entry.size = SIZE_NON_NORMAL;
103 entry.size = SIZE_NON_NORMAL;
104 entry.mtime = MTIME_UNSET;
104 entry.mtime = MTIME_UNSET;
105 } else {
105 } else {
106 entry.state = EntryState::Normal;
106 entry.state = EntryState::Normal;
107 entry.size = entry.size & V1_RANGEMASK;
107 entry.size = entry.size & V1_RANGEMASK;
108 entry.mtime = entry.mtime & V1_RANGEMASK;
108 entry.mtime = entry.mtime & V1_RANGEMASK;
109 }
109 }
110 let old_state = match self.get(filename) {
110 let old_state = match self.get(filename) {
111 Some(e) => e.state,
111 Some(e) => e.state,
112 None => EntryState::Unknown,
112 None => EntryState::Unknown,
113 };
113 };
114 if old_state == EntryState::Unknown || old_state == EntryState::Removed
114 if old_state == EntryState::Unknown || old_state == EntryState::Removed
115 {
115 {
116 if let Some(ref mut dirs) = self.dirs {
116 if let Some(ref mut dirs) = self.dirs {
117 dirs.add_path(filename)?;
117 dirs.add_path(filename)?;
118 }
118 }
119 }
119 }
120 if old_state == EntryState::Unknown {
120 if old_state == EntryState::Unknown {
121 if let Some(ref mut all_dirs) = self.all_dirs {
121 if let Some(ref mut all_dirs) = self.all_dirs {
122 all_dirs.add_path(filename)?;
122 all_dirs.add_path(filename)?;
123 }
123 }
124 }
124 }
125 self.state_map.insert(filename.to_owned(), entry.to_owned());
125 self.state_map.insert(filename.to_owned(), entry.to_owned());
126
126
127 if entry.is_non_normal() {
127 if entry.is_non_normal() {
128 self.get_non_normal_other_parent_entries()
128 self.get_non_normal_other_parent_entries()
129 .0
129 .0
130 .insert(filename.to_owned());
130 .insert(filename.to_owned());
131 }
131 }
132
132
133 if entry.is_from_other_parent() {
133 if entry.is_from_other_parent() {
134 self.get_non_normal_other_parent_entries()
134 self.get_non_normal_other_parent_entries()
135 .1
135 .1
136 .insert(filename.to_owned());
136 .insert(filename.to_owned());
137 }
137 }
138 Ok(())
138 Ok(())
139 }
139 }
140
140
141 /// Mark a file as removed in the dirstate.
141 /// Mark a file as removed in the dirstate.
142 ///
142 ///
143 /// The `size` parameter is used to store sentinel values that indicate
143 /// The `size` parameter is used to store sentinel values that indicate
144 /// the file's previous state. In the future, we should refactor this
144 /// the file's previous state. In the future, we should refactor this
145 /// to be more explicit about what that state is.
145 /// to be more explicit about what that state is.
146 pub fn remove_file(
146 pub fn remove_file(
147 &mut self,
147 &mut self,
148 filename: &HgPath,
148 filename: &HgPath,
149 in_merge: bool,
149 in_merge: bool,
150 ) -> Result<(), DirstateError> {
150 ) -> Result<(), DirstateError> {
151 let old_entry_opt = self.get(filename);
151 let old_entry_opt = self.get(filename);
152 let old_state = match old_entry_opt {
152 let old_state = match old_entry_opt {
153 Some(e) => e.state,
153 Some(e) => e.state,
154 None => EntryState::Unknown,
154 None => EntryState::Unknown,
155 };
155 };
156 let mut size = 0;
156 let mut size = 0;
157 if in_merge {
157 if in_merge {
158 // XXX we should not be able to have 'm' state and 'FROM_P2' if not
158 // XXX we should not be able to have 'm' state and 'FROM_P2' if not
159 // during a merge. So I (marmoute) am not sure we need the
159 // during a merge. So I (marmoute) am not sure we need the
160 // conditionnal at all. Adding double checking this with assert
160 // conditionnal at all. Adding double checking this with assert
161 // would be nice.
161 // would be nice.
162 if let Some(old_entry) = old_entry_opt {
162 if let Some(old_entry) = old_entry_opt {
163 // backup the previous state
163 // backup the previous state
164 if old_entry.state == EntryState::Merged {
164 if old_entry.state == EntryState::Merged {
165 size = SIZE_NON_NORMAL;
165 size = SIZE_NON_NORMAL;
166 } else if old_entry.state == EntryState::Normal
166 } else if old_entry.state == EntryState::Normal
167 && old_entry.size == SIZE_FROM_OTHER_PARENT
167 && old_entry.size == SIZE_FROM_OTHER_PARENT
168 {
168 {
169 // other parent
169 // other parent
170 size = SIZE_FROM_OTHER_PARENT;
170 size = SIZE_FROM_OTHER_PARENT;
171 self.get_non_normal_other_parent_entries()
171 self.get_non_normal_other_parent_entries()
172 .1
172 .1
173 .insert(filename.to_owned());
173 .insert(filename.to_owned());
174 }
174 }
175 }
175 }
176 }
176 }
177 if old_state != EntryState::Unknown && old_state != EntryState::Removed
177 if old_state != EntryState::Unknown && old_state != EntryState::Removed
178 {
178 {
179 if let Some(ref mut dirs) = self.dirs {
179 if let Some(ref mut dirs) = self.dirs {
180 dirs.delete_path(filename)?;
180 dirs.delete_path(filename)?;
181 }
181 }
182 }
182 }
183 if old_state == EntryState::Unknown {
183 if old_state == EntryState::Unknown {
184 if let Some(ref mut all_dirs) = self.all_dirs {
184 if let Some(ref mut all_dirs) = self.all_dirs {
185 all_dirs.add_path(filename)?;
185 all_dirs.add_path(filename)?;
186 }
186 }
187 }
187 }
188 if size == 0 {
188 if size == 0 {
189 self.copy_map.remove(filename);
189 self.copy_map.remove(filename);
190 }
190 }
191
191
192 self.state_map.insert(
192 self.state_map.insert(
193 filename.to_owned(),
193 filename.to_owned(),
194 DirstateEntry {
194 DirstateEntry {
195 state: EntryState::Removed,
195 state: EntryState::Removed,
196 mode: 0,
196 mode: 0,
197 size,
197 size,
198 mtime: 0,
198 mtime: 0,
199 },
199 },
200 );
200 );
201 self.get_non_normal_other_parent_entries()
201 self.get_non_normal_other_parent_entries()
202 .0
202 .0
203 .insert(filename.to_owned());
203 .insert(filename.to_owned());
204 Ok(())
204 Ok(())
205 }
205 }
206
206
207 /// Remove a file from the dirstate.
207 /// Remove a file from the dirstate.
208 /// Returns `true` if the file was previously recorded.
208 /// Returns `true` if the file was previously recorded.
209 pub fn drop_file(
209 pub fn drop_file(
210 &mut self,
210 &mut self,
211 filename: &HgPath,
211 filename: &HgPath,
212 ) -> Result<bool, DirstateError> {
212 ) -> Result<bool, DirstateError> {
213 let old_state = match self.get(filename) {
213 let old_state = match self.get(filename) {
214 Some(e) => e.state,
214 Some(e) => e.state,
215 None => EntryState::Unknown,
215 None => EntryState::Unknown,
216 };
216 };
217 let exists = self.state_map.remove(filename).is_some();
217 let exists = self.state_map.remove(filename).is_some();
218
218
219 if exists {
219 if exists {
220 if old_state != EntryState::Removed {
220 if old_state != EntryState::Removed {
221 if let Some(ref mut dirs) = self.dirs {
221 if let Some(ref mut dirs) = self.dirs {
222 dirs.delete_path(filename)?;
222 dirs.delete_path(filename)?;
223 }
223 }
224 }
224 }
225 if let Some(ref mut all_dirs) = self.all_dirs {
225 if let Some(ref mut all_dirs) = self.all_dirs {
226 all_dirs.delete_path(filename)?;
226 all_dirs.delete_path(filename)?;
227 }
227 }
228 }
228 }
229 self.get_non_normal_other_parent_entries()
229 self.get_non_normal_other_parent_entries()
230 .0
230 .0
231 .remove(filename);
231 .remove(filename);
232
232
233 Ok(exists)
233 Ok(exists)
234 }
234 }
235
235
236 pub fn clear_ambiguous_times(
236 pub fn clear_ambiguous_times(
237 &mut self,
237 &mut self,
238 filenames: Vec<HgPathBuf>,
238 filenames: Vec<HgPathBuf>,
239 now: i32,
239 now: i32,
240 ) {
240 ) {
241 for filename in filenames {
241 for filename in filenames {
242 if let Some(entry) = self.state_map.get_mut(&filename) {
242 if let Some(entry) = self.state_map.get_mut(&filename) {
243 if entry.clear_ambiguous_mtime(now) {
243 if entry.clear_ambiguous_mtime(now) {
244 self.get_non_normal_other_parent_entries()
244 self.get_non_normal_other_parent_entries()
245 .0
245 .0
246 .insert(filename.to_owned());
246 .insert(filename.to_owned());
247 }
247 }
248 }
248 }
249 }
249 }
250 }
250 }
251
251
252 pub fn non_normal_entries_remove(
252 pub fn non_normal_entries_remove(
253 &mut self,
253 &mut self,
254 key: impl AsRef<HgPath>,
254 key: impl AsRef<HgPath>,
255 ) -> bool {
255 ) -> bool {
256 self.get_non_normal_other_parent_entries()
256 self.get_non_normal_other_parent_entries()
257 .0
257 .0
258 .remove(key.as_ref())
258 .remove(key.as_ref())
259 }
259 }
260
260
261 pub fn non_normal_entries_add(&mut self, key: impl AsRef<HgPath>) {
261 pub fn non_normal_entries_add(&mut self, key: impl AsRef<HgPath>) {
262 self.get_non_normal_other_parent_entries()
262 self.get_non_normal_other_parent_entries()
263 .0
263 .0
264 .insert(key.as_ref().into());
264 .insert(key.as_ref().into());
265 }
265 }
266
266
267 pub fn non_normal_entries_union(
267 pub fn non_normal_entries_union(
268 &mut self,
268 &mut self,
269 other: HashSet<HgPathBuf>,
269 other: HashSet<HgPathBuf>,
270 ) -> Vec<HgPathBuf> {
270 ) -> Vec<HgPathBuf> {
271 self.get_non_normal_other_parent_entries()
271 self.get_non_normal_other_parent_entries()
272 .0
272 .0
273 .union(&other)
273 .union(&other)
274 .map(ToOwned::to_owned)
274 .map(ToOwned::to_owned)
275 .collect()
275 .collect()
276 }
276 }
277
277
278 pub fn get_non_normal_other_parent_entries(
278 pub fn get_non_normal_other_parent_entries(
279 &mut self,
279 &mut self,
280 ) -> (&mut HashSet<HgPathBuf>, &mut HashSet<HgPathBuf>) {
280 ) -> (&mut HashSet<HgPathBuf>, &mut HashSet<HgPathBuf>) {
281 self.set_non_normal_other_parent_entries(false);
281 self.set_non_normal_other_parent_entries(false);
282 (
282 (
283 self.non_normal_set.as_mut().unwrap(),
283 self.non_normal_set.as_mut().unwrap(),
284 self.other_parent_set.as_mut().unwrap(),
284 self.other_parent_set.as_mut().unwrap(),
285 )
285 )
286 }
286 }
287
287
288 /// Useful to get immutable references to those sets in contexts where
288 /// Useful to get immutable references to those sets in contexts where
289 /// you only have an immutable reference to the `DirstateMap`, like when
289 /// you only have an immutable reference to the `DirstateMap`, like when
290 /// sharing references with Python.
290 /// sharing references with Python.
291 ///
291 ///
292 /// TODO, get rid of this along with the other "setter/getter" stuff when
292 /// TODO, get rid of this along with the other "setter/getter" stuff when
293 /// a nice typestate plan is defined.
293 /// a nice typestate plan is defined.
294 ///
294 ///
295 /// # Panics
295 /// # Panics
296 ///
296 ///
297 /// Will panic if either set is `None`.
297 /// Will panic if either set is `None`.
298 pub fn get_non_normal_other_parent_entries_panic(
298 pub fn get_non_normal_other_parent_entries_panic(
299 &self,
299 &self,
300 ) -> (&HashSet<HgPathBuf>, &HashSet<HgPathBuf>) {
300 ) -> (&HashSet<HgPathBuf>, &HashSet<HgPathBuf>) {
301 (
301 (
302 self.non_normal_set.as_ref().unwrap(),
302 self.non_normal_set.as_ref().unwrap(),
303 self.other_parent_set.as_ref().unwrap(),
303 self.other_parent_set.as_ref().unwrap(),
304 )
304 )
305 }
305 }
306
306
307 pub fn set_non_normal_other_parent_entries(&mut self, force: bool) {
307 pub fn set_non_normal_other_parent_entries(&mut self, force: bool) {
308 if !force
308 if !force
309 && self.non_normal_set.is_some()
309 && self.non_normal_set.is_some()
310 && self.other_parent_set.is_some()
310 && self.other_parent_set.is_some()
311 {
311 {
312 return;
312 return;
313 }
313 }
314 let mut non_normal = HashSet::new();
314 let mut non_normal = HashSet::new();
315 let mut other_parent = HashSet::new();
315 let mut other_parent = HashSet::new();
316
316
317 for (filename, entry) in self.state_map.iter() {
317 for (filename, entry) in self.state_map.iter() {
318 if entry.is_non_normal() {
318 if entry.is_non_normal() {
319 non_normal.insert(filename.to_owned());
319 non_normal.insert(filename.to_owned());
320 }
320 }
321 if entry.is_from_other_parent() {
321 if entry.is_from_other_parent() {
322 other_parent.insert(filename.to_owned());
322 other_parent.insert(filename.to_owned());
323 }
323 }
324 }
324 }
325 self.non_normal_set = Some(non_normal);
325 self.non_normal_set = Some(non_normal);
326 self.other_parent_set = Some(other_parent);
326 self.other_parent_set = Some(other_parent);
327 }
327 }
328
328
329 /// Both of these setters and their uses appear to be the simplest way to
329 /// Both of these setters and their uses appear to be the simplest way to
330 /// emulate a Python lazy property, but it is ugly and unidiomatic.
330 /// emulate a Python lazy property, but it is ugly and unidiomatic.
331 /// TODO One day, rewriting this struct using the typestate might be a
331 /// TODO One day, rewriting this struct using the typestate might be a
332 /// good idea.
332 /// good idea.
333 pub fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
333 pub fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
334 if self.all_dirs.is_none() {
334 if self.all_dirs.is_none() {
335 self.all_dirs = Some(DirsMultiset::from_dirstate(
335 self.all_dirs = Some(DirsMultiset::from_dirstate(
336 self.state_map.iter().map(|(k, v)| Ok((k, *v))),
336 self.state_map.iter().map(|(k, v)| Ok((k, *v))),
337 None,
337 false,
338 )?);
338 )?);
339 }
339 }
340 Ok(())
340 Ok(())
341 }
341 }
342
342
343 pub fn set_dirs(&mut self) -> Result<(), DirstateError> {
343 pub fn set_dirs(&mut self) -> Result<(), DirstateError> {
344 if self.dirs.is_none() {
344 if self.dirs.is_none() {
345 self.dirs = Some(DirsMultiset::from_dirstate(
345 self.dirs = Some(DirsMultiset::from_dirstate(
346 self.state_map.iter().map(|(k, v)| Ok((k, *v))),
346 self.state_map.iter().map(|(k, v)| Ok((k, *v))),
347 Some(EntryState::Removed),
347 true,
348 )?);
348 )?);
349 }
349 }
350 Ok(())
350 Ok(())
351 }
351 }
352
352
353 pub fn has_tracked_dir(
353 pub fn has_tracked_dir(
354 &mut self,
354 &mut self,
355 directory: &HgPath,
355 directory: &HgPath,
356 ) -> Result<bool, DirstateError> {
356 ) -> Result<bool, DirstateError> {
357 self.set_dirs()?;
357 self.set_dirs()?;
358 Ok(self.dirs.as_ref().unwrap().contains(directory))
358 Ok(self.dirs.as_ref().unwrap().contains(directory))
359 }
359 }
360
360
361 pub fn has_dir(
361 pub fn has_dir(
362 &mut self,
362 &mut self,
363 directory: &HgPath,
363 directory: &HgPath,
364 ) -> Result<bool, DirstateError> {
364 ) -> Result<bool, DirstateError> {
365 self.set_all_dirs()?;
365 self.set_all_dirs()?;
366 Ok(self.all_dirs.as_ref().unwrap().contains(directory))
366 Ok(self.all_dirs.as_ref().unwrap().contains(directory))
367 }
367 }
368
368
369 #[timed]
369 #[timed]
370 pub fn read(
370 pub fn read(
371 &mut self,
371 &mut self,
372 file_contents: &[u8],
372 file_contents: &[u8],
373 ) -> Result<Option<DirstateParents>, DirstateError> {
373 ) -> Result<Option<DirstateParents>, DirstateError> {
374 if file_contents.is_empty() {
374 if file_contents.is_empty() {
375 return Ok(None);
375 return Ok(None);
376 }
376 }
377
377
378 let (parents, entries, copies) = parse_dirstate(file_contents)?;
378 let (parents, entries, copies) = parse_dirstate(file_contents)?;
379 self.state_map.extend(
379 self.state_map.extend(
380 entries
380 entries
381 .into_iter()
381 .into_iter()
382 .map(|(path, entry)| (path.to_owned(), entry)),
382 .map(|(path, entry)| (path.to_owned(), entry)),
383 );
383 );
384 self.copy_map.extend(
384 self.copy_map.extend(
385 copies
385 copies
386 .into_iter()
386 .into_iter()
387 .map(|(path, copy)| (path.to_owned(), copy.to_owned())),
387 .map(|(path, copy)| (path.to_owned(), copy.to_owned())),
388 );
388 );
389 Ok(Some(parents.clone()))
389 Ok(Some(parents.clone()))
390 }
390 }
391
391
392 pub fn pack(
392 pub fn pack(
393 &mut self,
393 &mut self,
394 parents: DirstateParents,
394 parents: DirstateParents,
395 now: Timestamp,
395 now: Timestamp,
396 ) -> Result<Vec<u8>, DirstateError> {
396 ) -> Result<Vec<u8>, DirstateError> {
397 let packed =
397 let packed =
398 pack_dirstate(&mut self.state_map, &self.copy_map, parents, now)?;
398 pack_dirstate(&mut self.state_map, &self.copy_map, parents, now)?;
399
399
400 self.set_non_normal_other_parent_entries(true);
400 self.set_non_normal_other_parent_entries(true);
401 Ok(packed)
401 Ok(packed)
402 }
402 }
403 }
403 }
404
404
405 #[cfg(test)]
405 #[cfg(test)]
406 mod tests {
406 mod tests {
407 use super::*;
407 use super::*;
408
408
409 #[test]
409 #[test]
410 fn test_dirs_multiset() {
410 fn test_dirs_multiset() {
411 let mut map = DirstateMap::new();
411 let mut map = DirstateMap::new();
412 assert!(map.dirs.is_none());
412 assert!(map.dirs.is_none());
413 assert!(map.all_dirs.is_none());
413 assert!(map.all_dirs.is_none());
414
414
415 assert_eq!(map.has_dir(HgPath::new(b"nope")).unwrap(), false);
415 assert_eq!(map.has_dir(HgPath::new(b"nope")).unwrap(), false);
416 assert!(map.all_dirs.is_some());
416 assert!(map.all_dirs.is_some());
417 assert!(map.dirs.is_none());
417 assert!(map.dirs.is_none());
418
418
419 assert_eq!(map.has_tracked_dir(HgPath::new(b"nope")).unwrap(), false);
419 assert_eq!(map.has_tracked_dir(HgPath::new(b"nope")).unwrap(), false);
420 assert!(map.dirs.is_some());
420 assert!(map.dirs.is_some());
421 }
421 }
422
422
423 #[test]
423 #[test]
424 fn test_add_file() {
424 fn test_add_file() {
425 let mut map = DirstateMap::new();
425 let mut map = DirstateMap::new();
426
426
427 assert_eq!(0, map.len());
427 assert_eq!(0, map.len());
428
428
429 map.add_file(
429 map.add_file(
430 HgPath::new(b"meh"),
430 HgPath::new(b"meh"),
431 DirstateEntry {
431 DirstateEntry {
432 state: EntryState::Normal,
432 state: EntryState::Normal,
433 mode: 1337,
433 mode: 1337,
434 mtime: 1337,
434 mtime: 1337,
435 size: 1337,
435 size: 1337,
436 },
436 },
437 false,
437 false,
438 false,
438 false,
439 false,
439 false,
440 false,
440 false,
441 )
441 )
442 .unwrap();
442 .unwrap();
443
443
444 assert_eq!(1, map.len());
444 assert_eq!(1, map.len());
445 assert_eq!(0, map.get_non_normal_other_parent_entries().0.len());
445 assert_eq!(0, map.get_non_normal_other_parent_entries().0.len());
446 assert_eq!(0, map.get_non_normal_other_parent_entries().1.len());
446 assert_eq!(0, map.get_non_normal_other_parent_entries().1.len());
447 }
447 }
448
448
449 #[test]
449 #[test]
450 fn test_non_normal_other_parent_entries() {
450 fn test_non_normal_other_parent_entries() {
451 let mut map: DirstateMap = [
451 let mut map: DirstateMap = [
452 (b"f1", (EntryState::Removed, 1337, 1337, 1337)),
452 (b"f1", (EntryState::Removed, 1337, 1337, 1337)),
453 (b"f2", (EntryState::Normal, 1337, 1337, -1)),
453 (b"f2", (EntryState::Normal, 1337, 1337, -1)),
454 (b"f3", (EntryState::Normal, 1337, 1337, 1337)),
454 (b"f3", (EntryState::Normal, 1337, 1337, 1337)),
455 (b"f4", (EntryState::Normal, 1337, -2, 1337)),
455 (b"f4", (EntryState::Normal, 1337, -2, 1337)),
456 (b"f5", (EntryState::Added, 1337, 1337, 1337)),
456 (b"f5", (EntryState::Added, 1337, 1337, 1337)),
457 (b"f6", (EntryState::Added, 1337, 1337, -1)),
457 (b"f6", (EntryState::Added, 1337, 1337, -1)),
458 (b"f7", (EntryState::Merged, 1337, 1337, -1)),
458 (b"f7", (EntryState::Merged, 1337, 1337, -1)),
459 (b"f8", (EntryState::Merged, 1337, 1337, 1337)),
459 (b"f8", (EntryState::Merged, 1337, 1337, 1337)),
460 (b"f9", (EntryState::Merged, 1337, -2, 1337)),
460 (b"f9", (EntryState::Merged, 1337, -2, 1337)),
461 (b"fa", (EntryState::Added, 1337, -2, 1337)),
461 (b"fa", (EntryState::Added, 1337, -2, 1337)),
462 (b"fb", (EntryState::Removed, 1337, -2, 1337)),
462 (b"fb", (EntryState::Removed, 1337, -2, 1337)),
463 ]
463 ]
464 .iter()
464 .iter()
465 .map(|(fname, (state, mode, size, mtime))| {
465 .map(|(fname, (state, mode, size, mtime))| {
466 (
466 (
467 HgPathBuf::from_bytes(fname.as_ref()),
467 HgPathBuf::from_bytes(fname.as_ref()),
468 DirstateEntry {
468 DirstateEntry {
469 state: *state,
469 state: *state,
470 mode: *mode,
470 mode: *mode,
471 size: *size,
471 size: *size,
472 mtime: *mtime,
472 mtime: *mtime,
473 },
473 },
474 )
474 )
475 })
475 })
476 .collect();
476 .collect();
477
477
478 let mut non_normal = [
478 let mut non_normal = [
479 b"f1", b"f2", b"f5", b"f6", b"f7", b"f8", b"f9", b"fa", b"fb",
479 b"f1", b"f2", b"f5", b"f6", b"f7", b"f8", b"f9", b"fa", b"fb",
480 ]
480 ]
481 .iter()
481 .iter()
482 .map(|x| HgPathBuf::from_bytes(x.as_ref()))
482 .map(|x| HgPathBuf::from_bytes(x.as_ref()))
483 .collect();
483 .collect();
484
484
485 let mut other_parent = HashSet::new();
485 let mut other_parent = HashSet::new();
486 other_parent.insert(HgPathBuf::from_bytes(b"f4"));
486 other_parent.insert(HgPathBuf::from_bytes(b"f4"));
487 let entries = map.get_non_normal_other_parent_entries();
487 let entries = map.get_non_normal_other_parent_entries();
488
488
489 assert_eq!(
489 assert_eq!(
490 (&mut non_normal, &mut other_parent),
490 (&mut non_normal, &mut other_parent),
491 (entries.0, entries.1)
491 (entries.0, entries.1)
492 );
492 );
493 }
493 }
494 }
494 }
@@ -1,142 +1,134 b''
1 // dirs_multiset.rs
1 // dirs_multiset.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Bindings for the `hg::dirstate::dirs_multiset` file provided by the
8 //! Bindings for the `hg::dirstate::dirs_multiset` file provided by the
9 //! `hg-core` package.
9 //! `hg-core` package.
10
10
11 use std::cell::RefCell;
11 use std::cell::RefCell;
12 use std::convert::TryInto;
13
12
14 use cpython::{
13 use cpython::{
15 exc, ObjectProtocol, PyBytes, PyClone, PyDict, PyErr, PyObject, PyResult,
14 exc, ObjectProtocol, PyBool, PyBytes, PyClone, PyDict, PyErr, PyObject,
16 Python, UnsafePyLeaked,
15 PyResult, Python, UnsafePyLeaked,
17 };
16 };
18
17
19 use crate::dirstate::extract_dirstate;
18 use crate::dirstate::extract_dirstate;
20 use hg::{
19 use hg::{
21 errors::HgError,
22 utils::hg_path::{HgPath, HgPathBuf},
20 utils::hg_path::{HgPath, HgPathBuf},
23 DirsMultiset, DirsMultisetIter, DirstateError, DirstateMapError,
21 DirsMultiset, DirsMultisetIter, DirstateError, DirstateMapError,
24 EntryState,
25 };
22 };
26
23
27 py_class!(pub class Dirs |py| {
24 py_class!(pub class Dirs |py| {
28 @shared data inner: DirsMultiset;
25 @shared data inner: DirsMultiset;
29
26
30 // `map` is either a `dict` or a flat iterator (usually a `set`, sometimes
27 // `map` is either a `dict` or a flat iterator (usually a `set`, sometimes
31 // a `list`)
28 // a `list`)
32 def __new__(
29 def __new__(
33 _cls,
30 _cls,
34 map: PyObject,
31 map: PyObject,
35 skip: Option<PyObject> = None
32 only_tracked: Option<PyObject> = None
36 ) -> PyResult<Self> {
33 ) -> PyResult<Self> {
37 let mut skip_state: Option<EntryState> = None;
34 let only_tracked_b = if let Some(only_tracked) = only_tracked {
38 if let Some(skip) = skip {
35 only_tracked.extract::<PyBool>(py)?.is_true()
39 skip_state = Some(
36 } else {
40 skip.extract::<PyBytes>(py)?.data(py)[0]
37 false
41 .try_into()
38 };
42 .map_err(|e: HgError| {
43 PyErr::new::<exc::ValueError, _>(py, e.to_string())
44 })?,
45 );
46 }
47 let inner = if let Ok(map) = map.cast_as::<PyDict>(py) {
39 let inner = if let Ok(map) = map.cast_as::<PyDict>(py) {
48 let dirstate = extract_dirstate(py, &map)?;
40 let dirstate = extract_dirstate(py, &map)?;
49 let dirstate = dirstate.iter().map(|(k, v)| Ok((k, *v)));
41 let dirstate = dirstate.iter().map(|(k, v)| Ok((k, *v)));
50 DirsMultiset::from_dirstate(dirstate, skip_state)
42 DirsMultiset::from_dirstate(dirstate, only_tracked_b)
51 .map_err(|e: DirstateError| {
43 .map_err(|e: DirstateError| {
52 PyErr::new::<exc::ValueError, _>(py, e.to_string())
44 PyErr::new::<exc::ValueError, _>(py, e.to_string())
53 })?
45 })?
54 } else {
46 } else {
55 let map: Result<Vec<HgPathBuf>, PyErr> = map
47 let map: Result<Vec<HgPathBuf>, PyErr> = map
56 .iter(py)?
48 .iter(py)?
57 .map(|o| {
49 .map(|o| {
58 Ok(HgPathBuf::from_bytes(
50 Ok(HgPathBuf::from_bytes(
59 o?.extract::<PyBytes>(py)?.data(py),
51 o?.extract::<PyBytes>(py)?.data(py),
60 ))
52 ))
61 })
53 })
62 .collect();
54 .collect();
63 DirsMultiset::from_manifest(&map?)
55 DirsMultiset::from_manifest(&map?)
64 .map_err(|e| {
56 .map_err(|e| {
65 PyErr::new::<exc::ValueError, _>(py, e.to_string())
57 PyErr::new::<exc::ValueError, _>(py, e.to_string())
66 })?
58 })?
67 };
59 };
68
60
69 Self::create_instance(py, inner)
61 Self::create_instance(py, inner)
70 }
62 }
71
63
72 def addpath(&self, path: PyObject) -> PyResult<PyObject> {
64 def addpath(&self, path: PyObject) -> PyResult<PyObject> {
73 self.inner(py).borrow_mut().add_path(
65 self.inner(py).borrow_mut().add_path(
74 HgPath::new(path.extract::<PyBytes>(py)?.data(py)),
66 HgPath::new(path.extract::<PyBytes>(py)?.data(py)),
75 ).and(Ok(py.None())).or_else(|e| {
67 ).and(Ok(py.None())).or_else(|e| {
76 match e {
68 match e {
77 DirstateMapError::EmptyPath => {
69 DirstateMapError::EmptyPath => {
78 Ok(py.None())
70 Ok(py.None())
79 },
71 },
80 e => {
72 e => {
81 Err(PyErr::new::<exc::ValueError, _>(
73 Err(PyErr::new::<exc::ValueError, _>(
82 py,
74 py,
83 e.to_string(),
75 e.to_string(),
84 ))
76 ))
85 }
77 }
86 }
78 }
87 })
79 })
88 }
80 }
89
81
90 def delpath(&self, path: PyObject) -> PyResult<PyObject> {
82 def delpath(&self, path: PyObject) -> PyResult<PyObject> {
91 self.inner(py).borrow_mut().delete_path(
83 self.inner(py).borrow_mut().delete_path(
92 HgPath::new(path.extract::<PyBytes>(py)?.data(py)),
84 HgPath::new(path.extract::<PyBytes>(py)?.data(py)),
93 )
85 )
94 .and(Ok(py.None()))
86 .and(Ok(py.None()))
95 .or_else(|e| {
87 .or_else(|e| {
96 match e {
88 match e {
97 DirstateMapError::EmptyPath => {
89 DirstateMapError::EmptyPath => {
98 Ok(py.None())
90 Ok(py.None())
99 },
91 },
100 e => {
92 e => {
101 Err(PyErr::new::<exc::ValueError, _>(
93 Err(PyErr::new::<exc::ValueError, _>(
102 py,
94 py,
103 e.to_string(),
95 e.to_string(),
104 ))
96 ))
105 }
97 }
106 }
98 }
107 })
99 })
108 }
100 }
109 def __iter__(&self) -> PyResult<DirsMultisetKeysIterator> {
101 def __iter__(&self) -> PyResult<DirsMultisetKeysIterator> {
110 let leaked_ref = self.inner(py).leak_immutable();
102 let leaked_ref = self.inner(py).leak_immutable();
111 DirsMultisetKeysIterator::from_inner(
103 DirsMultisetKeysIterator::from_inner(
112 py,
104 py,
113 unsafe { leaked_ref.map(py, |o| o.iter()) },
105 unsafe { leaked_ref.map(py, |o| o.iter()) },
114 )
106 )
115 }
107 }
116
108
117 def __contains__(&self, item: PyObject) -> PyResult<bool> {
109 def __contains__(&self, item: PyObject) -> PyResult<bool> {
118 Ok(self.inner(py).borrow().contains(HgPath::new(
110 Ok(self.inner(py).borrow().contains(HgPath::new(
119 item.extract::<PyBytes>(py)?.data(py).as_ref(),
111 item.extract::<PyBytes>(py)?.data(py).as_ref(),
120 )))
112 )))
121 }
113 }
122 });
114 });
123
115
124 impl Dirs {
116 impl Dirs {
125 pub fn from_inner(py: Python, d: DirsMultiset) -> PyResult<Self> {
117 pub fn from_inner(py: Python, d: DirsMultiset) -> PyResult<Self> {
126 Self::create_instance(py, d)
118 Self::create_instance(py, d)
127 }
119 }
128
120
129 fn translate_key(
121 fn translate_key(
130 py: Python,
122 py: Python,
131 res: &HgPathBuf,
123 res: &HgPathBuf,
132 ) -> PyResult<Option<PyBytes>> {
124 ) -> PyResult<Option<PyBytes>> {
133 Ok(Some(PyBytes::new(py, res.as_bytes())))
125 Ok(Some(PyBytes::new(py, res.as_bytes())))
134 }
126 }
135 }
127 }
136
128
137 py_shared_iterator!(
129 py_shared_iterator!(
138 DirsMultisetKeysIterator,
130 DirsMultisetKeysIterator,
139 UnsafePyLeaked<DirsMultisetIter<'static>>,
131 UnsafePyLeaked<DirsMultisetIter<'static>>,
140 Dirs::translate_key,
132 Dirs::translate_key,
141 Option<PyBytes>
133 Option<PyBytes>
142 );
134 );
General Comments 0
You need to be logged in to leave comments. Login now