comparison hgext/git/dirstate.py @ 51929:93d872a06132

typing: add type annotations to the dirstate classes The basic procedure here was to use `merge-pyi` to merge the `git/dirstate.pyi` file in (after renaming the interface class to match), cleaning up the import statement mess, and then repeating the procedure for `mercurial/dirstate.pyi`. Surprisingly, git's dirstate had more hints inferred in its *.pyi file. After that, it was a manual examination of each method in the interface, and how they were implemented in the core and git classes to verify what was inferred by pytype, and fill in the missing gaps. Since this involved jumping around between three different files, I applied the same type info to all three at the same time. Complex types I rolled up into type aliases in the interface module, and used that as needed. That way if it changes, there's one place to edit. There are some hints still missing, and some documentation that doesn't match the signatures. They should all be marked with TODOs. There are also a bunch of methods on the core class that aren't on the Protocol class that seem like maybe they should be (like `set_tracked()`). There are even more methods missing from the git class. But that's a project for another time.
author Matt Harbison <matt_harbison@yahoo.com>
date Fri, 27 Sep 2024 12:30:37 -0400
parents e99c007030da
children
comparison
equal deleted inserted replaced
51928:3688a984134b 51929:93d872a06132
1 from __future__ import annotations 1 from __future__ import annotations
2 2
3 import contextlib 3 import contextlib
4 import os 4 import os
5
6 from typing import (
7 Any,
8 Dict,
9 Iterable,
10 Iterator,
11 List,
12 Optional,
13 Tuple,
14 )
5 15
6 from mercurial.node import sha1nodeconstants 16 from mercurial.node import sha1nodeconstants
7 from mercurial import ( 17 from mercurial import (
8 dirstatemap, 18 dirstatemap,
9 error, 19 error,
94 sha1nodeconstants, 104 sha1nodeconstants,
95 self._use_dirstate_v2, 105 self._use_dirstate_v2,
96 ) 106 )
97 return self._map 107 return self._map
98 108
99 def p1(self): 109 def p1(self) -> bytes:
100 try: 110 try:
101 return self.git.head.peel().id.raw 111 return self.git.head.peel().id.raw
102 except pygit2.GitError: 112 except pygit2.GitError:
103 # Typically happens when peeling HEAD fails, as in an 113 # Typically happens when peeling HEAD fails, as in an
104 # empty repository. 114 # empty repository.
105 return sha1nodeconstants.nullid 115 return sha1nodeconstants.nullid
106 116
107 def p2(self): 117 def p2(self) -> bytes:
108 # TODO: MERGE_HEAD? something like that, right? 118 # TODO: MERGE_HEAD? something like that, right?
109 return sha1nodeconstants.nullid 119 return sha1nodeconstants.nullid
110 120
111 def setparents(self, p1, p2=None): 121 def setparents(self, p1: bytes, p2: Optional[bytes] = None):
112 if p2 is None: 122 if p2 is None:
113 p2 = sha1nodeconstants.nullid 123 p2 = sha1nodeconstants.nullid
114 assert p2 == sha1nodeconstants.nullid, b'TODO merging support' 124 assert p2 == sha1nodeconstants.nullid, b'TODO merging support'
115 self.git.head.set_target(gitutil.togitnode(p1)) 125 self.git.head.set_target(gitutil.togitnode(p1))
116 126
118 def identity(self): 128 def identity(self):
119 return util.filestat.frompath( 129 return util.filestat.frompath(
120 os.path.join(self._root, b'.git', b'index') 130 os.path.join(self._root, b'.git', b'index')
121 ) 131 )
122 132
123 def branch(self): 133 def branch(self) -> bytes:
124 return b'default' 134 return b'default'
125 135
126 def parents(self): 136 def parents(self) -> List[bytes]:
127 # TODO how on earth do we find p2 if a merge is in flight? 137 # TODO how on earth do we find p2 if a merge is in flight?
128 return [self.p1(), sha1nodeconstants.nullid] 138 return [self.p1(), sha1nodeconstants.nullid]
129 139
130 def __iter__(self): 140 def __iter__(self) -> Iterator[bytes]:
131 return (pycompat.fsencode(f.path) for f in self.git.index) 141 return (pycompat.fsencode(f.path) for f in self.git.index)
132 142
133 def items(self): 143 def items(self) -> Iterator[Tuple[bytes, intdirstate.DirstateItemT]]:
134 for ie in self.git.index: 144 for ie in self.git.index:
135 yield ie.path, None # value should be a DirstateItem 145 yield ie.path, None # value should be a DirstateItem
136 146
137 # py2,3 compat forward 147 # py2,3 compat forward
138 iteritems = items 148 iteritems = items
142 gs = self.git.status_file(filename) 152 gs = self.git.status_file(filename)
143 except KeyError: 153 except KeyError:
144 return b'?' 154 return b'?'
145 return _STATUS_MAP[gs] 155 return _STATUS_MAP[gs]
146 156
147 def __contains__(self, filename): 157 def __contains__(self, filename: Any) -> bool:
148 try: 158 try:
149 gs = self.git.status_file(filename) 159 gs = self.git.status_file(filename)
150 return _STATUS_MAP[gs] != b'?' 160 return _STATUS_MAP[gs] != b'?'
151 except KeyError: 161 except KeyError:
152 return False 162 return False
153 163
154 def status(self, match, subrepos, ignored, clean, unknown): 164 def status(
165 self,
166 match: matchmod.basematcher,
167 subrepos: bool,
168 ignored: bool,
169 clean: bool,
170 unknown: bool,
171 ) -> intdirstate.StatusReturnT:
155 listclean = clean 172 listclean = clean
156 # TODO handling of clean files - can we get that from git.status()? 173 # TODO handling of clean files - can we get that from git.status()?
157 modified, added, removed, deleted, unknown, ignored, clean = ( 174 modified, added, removed, deleted, unknown, ignored, clean = (
158 [], 175 [],
159 [], 176 [],
222 modified, added, removed, deleted, unknown, ignored, clean 239 modified, added, removed, deleted, unknown, ignored, clean
223 ), 240 ),
224 mtime_boundary, 241 mtime_boundary,
225 ) 242 )
226 243
227 def flagfunc(self, buildfallback): 244 def flagfunc(
245 self, buildfallback: intdirstate.FlagFuncFallbackT
246 ) -> intdirstate.FlagFuncReturnT:
228 # TODO we can do better 247 # TODO we can do better
229 return buildfallback() 248 return buildfallback()
230 249
231 def getcwd(self): 250 def getcwd(self) -> bytes:
232 # TODO is this a good way to do this? 251 # TODO is this a good way to do this?
233 return os.path.dirname( 252 return os.path.dirname(
234 os.path.dirname(pycompat.fsencode(self.git.path)) 253 os.path.dirname(pycompat.fsencode(self.git.path))
235 ) 254 )
236 255
237 def get_entry(self, path): 256 def get_entry(self, path: bytes) -> intdirstate.DirstateItemT:
238 """return a DirstateItem for the associated path""" 257 """return a DirstateItem for the associated path"""
239 entry = self._map.get(path) 258 entry = self._map.get(path)
240 if entry is None: 259 if entry is None:
241 return DirstateItem() 260 return DirstateItem()
242 return entry 261 return entry
243 262
244 def normalize(self, path, isknown=False, ignoremissing=False): 263 def normalize(
264 self, path: bytes, isknown: bool = False, ignoremissing: bool = False
265 ) -> bytes:
245 normed = util.normcase(path) 266 normed = util.normcase(path)
246 assert normed == path, b"TODO handling of case folding: %s != %s" % ( 267 assert normed == path, b"TODO handling of case folding: %s != %s" % (
247 normed, 268 normed,
248 path, 269 path,
249 ) 270 )
250 return path 271 return path
251 272
252 @property 273 @property
253 def _checklink(self): 274 def _checklink(self) -> bool:
254 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path))) 275 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
255 276
256 def copies(self): 277 def copies(self) -> Dict[bytes, bytes]:
257 # TODO support copies? 278 # TODO support copies?
258 return {} 279 return {}
259 280
260 # # TODO what the heck is this 281 # # TODO what the heck is this
261 _filecache = set() 282 _filecache = set()
262 283
263 @property 284 @property
264 def is_changing_parents(self): 285 def is_changing_parents(self) -> bool:
265 # TODO: we need to implement the context manager bits and 286 # TODO: we need to implement the context manager bits and
266 # correctly stage/revert index edits. 287 # correctly stage/revert index edits.
267 return False 288 return False
268 289
269 @property 290 @property
270 def is_changing_any(self): 291 def is_changing_any(self) -> bool:
271 # TODO: we need to implement the context manager bits and 292 # TODO: we need to implement the context manager bits and
272 # correctly stage/revert index edits. 293 # correctly stage/revert index edits.
273 return False 294 return False
274 295
275 def write(self, tr): 296 def write(self, tr: Optional[intdirstate.TransactionT]) -> None:
276 # TODO: call parent change callbacks 297 # TODO: call parent change callbacks
277 298
278 if tr: 299 if tr:
279 300
280 def writeinner(category): 301 def writeinner(category):
282 303
283 tr.addpending(b'gitdirstate', writeinner) 304 tr.addpending(b'gitdirstate', writeinner)
284 else: 305 else:
285 self.git.index.write() 306 self.git.index.write()
286 307
287 def pathto(self, f, cwd=None): 308 def pathto(self, f: bytes, cwd: Optional[bytes] = None) -> bytes:
288 if cwd is None: 309 if cwd is None:
289 cwd = self.getcwd() 310 cwd = self.getcwd()
290 # TODO core dirstate does something about slashes here 311 # TODO core dirstate does something about slashes here
291 assert isinstance(f, bytes) 312 assert isinstance(f, bytes)
292 r = util.pathto(self._root, cwd, f) 313 r = util.pathto(self._root, cwd, f)
293 return r 314 return r
294 315
295 def matches(self, match): 316 def matches(self, match: matchmod.basematcher) -> Iterable[bytes]:
296 for x in self.git.index: 317 for x in self.git.index:
297 p = pycompat.fsencode(x.path) 318 p = pycompat.fsencode(x.path)
298 if match(p): 319 if match(p):
299 yield p 320 yield p # TODO: return list instead of yielding?
300 321
301 def set_clean(self, f, parentfiledata): 322 def set_clean(self, f, parentfiledata):
302 """Mark a file normal and clean.""" 323 """Mark a file normal and clean."""
303 # TODO: for now we just let libgit2 re-stat the file. We can 324 # TODO: for now we just let libgit2 re-stat the file. We can
304 # clearly do better. 325 # clearly do better.
306 def set_possibly_dirty(self, f): 327 def set_possibly_dirty(self, f):
307 """Mark a file normal, but possibly dirty.""" 328 """Mark a file normal, but possibly dirty."""
308 # TODO: for now we just let libgit2 re-stat the file. We can 329 # TODO: for now we just let libgit2 re-stat the file. We can
309 # clearly do better. 330 # clearly do better.
310 331
311 def walk(self, match, subrepos, unknown, ignored, full=True): 332 def walk(
333 self,
334 match: matchmod.basematcher,
335 subrepos: Any,
336 unknown: bool,
337 ignored: bool,
338 full: bool = True,
339 ) -> intdirstate.WalkReturnT:
312 # TODO: we need to use .status() and not iterate the index, 340 # TODO: we need to use .status() and not iterate the index,
313 # because the index doesn't force a re-walk and so `hg add` of 341 # because the index doesn't force a re-walk and so `hg add` of
314 # a new file without an intervening call to status will 342 # a new file without an intervening call to status will
315 # silently do nothing. 343 # silently do nothing.
316 r = {} 344 r = {}
368 index = self.git.index 396 index = self.git.index
369 index.read() 397 index.read()
370 index.remove(pycompat.fsdecode(f)) 398 index.remove(pycompat.fsdecode(f))
371 index.write() 399 index.write()
372 400
373 def copied(self, path): 401 def copied(self, file: bytes) -> Optional[bytes]:
374 # TODO: track copies? 402 # TODO: track copies?
375 return None 403 return None
376 404
377 def prefetch_parents(self): 405 def prefetch_parents(self):
378 # TODO 406 # TODO
385 @contextlib.contextmanager 413 @contextlib.contextmanager
386 def changing_parents(self, repo): 414 def changing_parents(self, repo):
387 # TODO: track this maybe? 415 # TODO: track this maybe?
388 yield 416 yield
389 417
390 def addparentchangecallback(self, category, callback): 418 def addparentchangecallback(
419 self, category: bytes, callback: intdirstate.AddParentChangeCallbackT
420 ) -> None:
391 # TODO: should this be added to the dirstate interface? 421 # TODO: should this be added to the dirstate interface?
392 self._plchangecallbacks[category] = callback 422 self._plchangecallbacks[category] = callback
393 423
394 def setbranch(self, branch, transaction): 424 def setbranch(
425 self, branch: bytes, transaction: Optional[intdirstate.TransactionT]
426 ) -> None:
395 raise error.Abort( 427 raise error.Abort(
396 b'git repos do not support branches. try using bookmarks' 428 b'git repos do not support branches. try using bookmarks'
397 ) 429 )