##// END OF EJS Templates
git: add missing `repository.imanifestdict` methods to `gittreemanifest`...
Matt Harbison -
r53391:db6efd74 default
parent child Browse files
Show More
@@ -1,389 +1,401
1 1 from __future__ import annotations
2 2
3 3 import typing
4 4
5 5 from typing import (
6 6 Any,
7 Iterable,
7 8 Iterator,
8 9 Set,
9 10 )
10 11
11 12 from mercurial import (
12 13 match as matchmod,
13 14 pathutil,
14 15 pycompat,
15 16 util,
16 17 )
17 18 from mercurial.interfaces import (
18 19 repository,
19 20 util as interfaceutil,
20 21 )
21 22 from . import gitutil
22 23
23 24 if typing.TYPE_CHECKING:
24 25 from typing import (
25 26 ByteString, # TODO: change to Buffer for 3.14
26 27 )
27 28
28 29 pygit2 = gitutil.get_pygit2()
29 30
30 31
31 32 @interfaceutil.implementer(repository.imanifestdict)
32 33 class gittreemanifest:
33 34 """Expose git trees (and optionally a builder's overlay) as a manifestdict.
34 35
35 36 Very similar to mercurial.manifest.treemanifest.
36 37 """
37 38
38 39 def __init__(self, git_repo, root_tree, pending_changes):
39 40 """Initializer.
40 41
41 42 Args:
42 43 git_repo: The git_repo we're walking (required to look up child
43 44 trees).
44 45 root_tree: The root Git tree object for this manifest.
45 46 pending_changes: A dict in which pending changes will be
46 47 tracked. The enclosing memgittreemanifestctx will use this to
47 48 construct any required Tree objects in Git during it's
48 49 `write()` method.
49 50 """
50 51 self._git_repo = git_repo
51 52 self._tree = root_tree
52 53 if pending_changes is None:
53 54 pending_changes = {}
54 55 # dict of path: Optional[Tuple(node, flags)]
55 56 self._pending_changes = pending_changes
56 57
57 58 def _resolve_entry(self, path) -> tuple[bytes, bytes]:
58 59 """Given a path, load its node and flags, or raise KeyError if missing.
59 60
60 61 This takes into account any pending writes in the builder.
61 62 """
62 63 upath = pycompat.fsdecode(path)
63 64 ent = None
64 65 if path in self._pending_changes:
65 66 val = self._pending_changes[path]
66 67 if val is None:
67 68 raise KeyError
68 69 return val
69 70 t = self._tree
70 71 comps = upath.split('/')
71 72 te = self._tree
72 73 for comp in comps[:-1]:
73 74 te = te[comp]
74 75 t = self._git_repo[te.id]
75 76 ent = t[comps[-1]]
76 77 if ent.filemode == pygit2.GIT_FILEMODE_BLOB:
77 78 flags = b''
78 79 elif ent.filemode == pygit2.GIT_FILEMODE_BLOB_EXECUTABLE:
79 80 flags = b'x'
80 81 elif ent.filemode == pygit2.GIT_FILEMODE_LINK:
81 82 flags = b'l'
82 83 else:
83 84 raise ValueError('unsupported mode %s' % oct(ent.filemode))
84 85 return ent.id.raw, flags
85 86
86 87 def __getitem__(self, path: bytes) -> bytes:
87 88 return self._resolve_entry(path)[0]
88 89
89 90 def find(self, path: bytes) -> tuple[bytes, bytes]:
90 91 return self._resolve_entry(path)
91 92
92 93 def __len__(self) -> int:
93 94 return len(list(self.walk(matchmod.always())))
94 95
95 96 def __nonzero__(self) -> bool:
96 97 try:
97 98 next(iter(self))
98 99 return True
99 100 except StopIteration:
100 101 return False
101 102
102 103 __bool__ = __nonzero__
103 104
104 105 def __contains__(self, path: bytes) -> bool:
105 106 try:
106 107 self._resolve_entry(path)
107 108 return True
108 109 except KeyError:
109 110 return False
110 111
111 112 def iterkeys(self) -> Iterator[bytes]:
112 113 return self.walk(matchmod.always())
113 114
114 115 def keys(self) -> list[bytes]:
115 116 return list(self.iterkeys())
116 117
117 118 def __iter__(self) -> Iterator[bytes]:
118 119 return self.iterkeys()
119 120
121 def set(self, path: bytes, node: bytes, flags: bytes) -> None:
122 raise NotImplementedError # TODO: implement this
123
120 124 def __setitem__(self, path: bytes, node: bytes) -> None:
121 125 self._pending_changes[path] = node, self.flags(path)
122 126
123 127 def __delitem__(self, path: bytes) -> None:
124 128 # TODO: should probably KeyError for already-deleted files?
125 129 self._pending_changes[path] = None
126 130
127 131 def filesnotin(self, other, match=None) -> Set[bytes]:
128 132 if match is not None:
129 133 match = matchmod.badmatch(match, lambda path, msg: None)
130 134 sm2 = set(other.walk(match))
131 135 return {f for f in self.walk(match) if f not in sm2}
132 136 return {f for f in self if f not in other}
133 137
134 138 @util.propertycache
135 139 def _dirs(self):
136 140 return pathutil.dirs(self)
137 141
142 def dirs(self) -> pathutil.dirs:
143 return self._dirs # TODO: why is there a prpoertycache?
144
138 145 def hasdir(self, dir: bytes) -> bool:
139 146 return dir in self._dirs
140 147
141 148 def diff(
142 149 self,
143 150 other: Any, # TODO: 'manifestdict' or (better) equivalent interface
144 151 match: Any = lambda x: True, # TODO: Optional[matchmod.basematcher] = None,
145 152 clean: bool = False,
146 153 ) -> dict[
147 154 bytes,
148 155 tuple[tuple[bytes | None, bytes], tuple[bytes | None, bytes]] | None,
149 156 ]:
150 157 """Finds changes between the current manifest and m2.
151 158
152 159 The result is returned as a dict with filename as key and
153 160 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
154 161 nodeid in the current/other manifest and fl1/fl2 is the flag
155 162 in the current/other manifest. Where the file does not exist,
156 163 the nodeid will be None and the flags will be the empty
157 164 string.
158 165 """
159 166 result = {}
160 167
161 168 def _iterativediff(t1, t2, subdir):
162 169 """compares two trees and appends new tree nodes to examine to
163 170 the stack"""
164 171 if t1 is None:
165 172 t1 = {}
166 173 if t2 is None:
167 174 t2 = {}
168 175
169 176 for e1 in t1:
170 177 realname = subdir + pycompat.fsencode(e1.name)
171 178
172 179 if e1.type == pygit2.GIT_OBJ_TREE:
173 180 try:
174 181 e2 = t2[e1.name]
175 182 if e2.type != pygit2.GIT_OBJ_TREE:
176 183 e2 = None
177 184 except KeyError:
178 185 e2 = None
179 186
180 187 stack.append((realname + b'/', e1, e2))
181 188 else:
182 189 n1, fl1 = self.find(realname)
183 190
184 191 try:
185 192 e2 = t2[e1.name]
186 193 n2, fl2 = other.find(realname)
187 194 except KeyError:
188 195 e2 = None
189 196 n2, fl2 = (None, b'')
190 197
191 198 if e2 is not None and e2.type == pygit2.GIT_OBJ_TREE:
192 199 stack.append((realname + b'/', None, e2))
193 200
194 201 if not match(realname):
195 202 continue
196 203
197 204 if n1 != n2 or fl1 != fl2:
198 205 result[realname] = ((n1, fl1), (n2, fl2))
199 206 elif clean:
200 207 result[realname] = None
201 208
202 209 for e2 in t2:
203 210 if e2.name in t1:
204 211 continue
205 212
206 213 realname = subdir + pycompat.fsencode(e2.name)
207 214
208 215 if e2.type == pygit2.GIT_OBJ_TREE:
209 216 stack.append((realname + b'/', None, e2))
210 217 elif match(realname):
211 218 n2, fl2 = other.find(realname)
212 219 result[realname] = ((None, b''), (n2, fl2))
213 220
214 221 stack = []
215 222 _iterativediff(self._tree, other._tree, b'')
216 223 while stack:
217 224 subdir, t1, t2 = stack.pop()
218 225 # stack is populated in the function call
219 226 _iterativediff(t1, t2, subdir)
220 227
221 228 return result
222 229
223 230 def setflag(self, path: bytes, flag: bytes) -> None:
224 231 node, unused_flag = self._resolve_entry(path)
225 232 self._pending_changes[path] = node, flag
226 233
227 234 def get(self, path: bytes, default=None) -> bytes | None:
228 235 try:
229 236 return self._resolve_entry(path)[0]
230 237 except KeyError:
231 238 return default
232 239
233 240 def flags(self, path: bytes) -> bytes:
234 241 try:
235 242 return self._resolve_entry(path)[1]
236 243 except KeyError:
237 244 return b''
238 245
239 246 def copy(self) -> 'gittreemanifest':
240 247 return gittreemanifest(
241 248 self._git_repo, self._tree, dict(self._pending_changes)
242 249 )
243 250
244 251 def items(self) -> Iterator[tuple[bytes, bytes]]:
245 252 for f in self:
246 253 # TODO: build a proper iterator version of this
247 254 yield f, self[f]
248 255
249 256 def iteritems(self) -> Iterator[tuple[bytes, bytes]]:
250 257 return self.items()
251 258
252 259 def iterentries(self) -> Iterator[tuple[bytes, bytes, bytes]]:
253 260 for f in self:
254 261 # TODO: build a proper iterator version of this
255 262 yield f, *self._resolve_entry(f)
256 263
257 264 def text(self) -> ByteString:
258 265 # TODO can this method move out of the manifest iface?
259 266 raise NotImplementedError
260 267
268 def fastdelta(
269 self, base: ByteString, changes: Iterable[tuple[bytes, bool]]
270 ) -> tuple[ByteString, ByteString]:
271 raise NotImplementedError # TODO: implement this
272
261 273 def _walkonetree(self, tree, match, subdir):
262 274 for te in tree:
263 275 # TODO: can we prune dir walks with the matcher?
264 276 realname = subdir + pycompat.fsencode(te.name)
265 277 if te.type == pygit2.GIT_OBJ_TREE:
266 278 for inner in self._walkonetree(
267 279 self._git_repo[te.id], match, realname + b'/'
268 280 ):
269 281 yield inner
270 282 elif match(realname):
271 283 yield pycompat.fsencode(realname)
272 284
273 285 def walk(self, match: matchmod.basematcher) -> Iterator[bytes]:
274 286 # TODO: this is a very lazy way to merge in the pending
275 287 # changes. There is absolutely room for optimization here by
276 288 # being clever about walking over the sets...
277 289 baseline = set(self._walkonetree(self._tree, match, b''))
278 290 deleted = {p for p, v in self._pending_changes.items() if v is None}
279 291 pend = {p for p in self._pending_changes if match(p)}
280 292 return iter(sorted((baseline | pend) - deleted))
281 293
282 294
283 295 class gittreemanifestctx(repository.imanifestrevisionstored):
284 296 def __init__(self, repo, gittree):
285 297 self._repo = repo
286 298 self._tree = gittree
287 299
288 300 def read(self):
289 301 return gittreemanifest(self._repo, self._tree, None)
290 302
291 303 def readfast(self, shallow: bool = False):
292 304 return self.read()
293 305
294 306 def copy(self):
295 307 # NB: it's important that we return a memgittreemanifestctx
296 308 # because the caller expects a mutable manifest.
297 309 return memgittreemanifestctx(self._repo, self._tree)
298 310
299 311 def find(self, path: bytes) -> tuple[bytes, bytes]:
300 312 return self.read().find(path)
301 313
302 314
303 315 class memgittreemanifestctx(repository.imanifestrevisionwritable):
304 316 def __init__(self, repo, tree):
305 317 self._repo = repo
306 318 self._tree = tree
307 319 # dict of path: Optional[Tuple(node, flags)]
308 320 self._pending_changes = {}
309 321
310 322 def read(self):
311 323 return gittreemanifest(self._repo, self._tree, self._pending_changes)
312 324
313 325 def copy(self):
314 326 # TODO: if we have a builder in play, what should happen here?
315 327 # Maybe we can shuffle copy() into the immutable interface.
316 328 return memgittreemanifestctx(self._repo, self._tree)
317 329
318 330 def write(self, transaction, link, p1, p2, added, removed, match=None):
319 331 # We're not (for now, anyway) going to audit filenames, so we
320 332 # can ignore added and removed.
321 333
322 334 # TODO what does this match argument get used for? hopefully
323 335 # just narrow?
324 336 assert not match or isinstance(match, matchmod.alwaysmatcher)
325 337
326 338 touched_dirs = pathutil.dirs(list(self._pending_changes))
327 339 trees = {
328 340 b'': self._tree,
329 341 }
330 342 # path: treebuilder
331 343 builders = {
332 344 b'': self._repo.TreeBuilder(self._tree),
333 345 }
334 346 # get a TreeBuilder for every tree in the touched_dirs set
335 347 for d in sorted(touched_dirs, key=lambda x: (len(x), x)):
336 348 if d == b'':
337 349 # loaded root tree above
338 350 continue
339 351 comps = d.split(b'/')
340 352 full = b''
341 353 for part in comps:
342 354 parent = trees[full]
343 355 try:
344 356 parent_tree_id = parent[pycompat.fsdecode(part)].id
345 357 new = self._repo[parent_tree_id]
346 358 except KeyError:
347 359 # new directory
348 360 new = None
349 361 full += b'/' + part
350 362 if new is not None:
351 363 # existing directory
352 364 trees[full] = new
353 365 builders[full] = self._repo.TreeBuilder(new)
354 366 else:
355 367 # new directory, use an empty dict to easily
356 368 # generate KeyError as any nested new dirs get
357 369 # created.
358 370 trees[full] = {}
359 371 builders[full] = self._repo.TreeBuilder()
360 372 for f, info in self._pending_changes.items():
361 373 if b'/' not in f:
362 374 dirname = b''
363 375 basename = f
364 376 else:
365 377 dirname, basename = f.rsplit(b'/', 1)
366 378 dirname = b'/' + dirname
367 379 if info is None:
368 380 builders[dirname].remove(pycompat.fsdecode(basename))
369 381 else:
370 382 n, fl = info
371 383 mode = {
372 384 b'': pygit2.GIT_FILEMODE_BLOB,
373 385 b'x': pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
374 386 b'l': pygit2.GIT_FILEMODE_LINK,
375 387 }[fl]
376 388 builders[dirname].insert(
377 389 pycompat.fsdecode(basename), gitutil.togitnode(n), mode
378 390 )
379 391 # This visits the buffered TreeBuilders in deepest-first
380 392 # order, bubbling up the edits.
381 393 for b in sorted(builders, key=len, reverse=True):
382 394 if b == b'':
383 395 break
384 396 cb = builders[b]
385 397 dn, bn = b.rsplit(b'/', 1)
386 398 builders[dn].insert(
387 399 pycompat.fsdecode(bn), cb.write(), pygit2.GIT_FILEMODE_TREE
388 400 )
389 401 return builders[b''].write().raw
General Comments 0
You need to be logged in to leave comments. Login now