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