##// END OF EJS Templates
git: correct some signature mismatches between dirstate and the Protocol class...
Matt Harbison -
r52817:51be8bf8 default
parent child Browse files
Show More
@@ -1,397 +1,399
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
5
6 from mercurial.node import sha1nodeconstants
6 from mercurial.node import sha1nodeconstants
7 from mercurial import (
7 from mercurial import (
8 dirstatemap,
8 dirstatemap,
9 error,
9 error,
10 extensions,
10 extensions,
11 match as matchmod,
11 match as matchmod,
12 pycompat,
12 pycompat,
13 scmutil,
13 scmutil,
14 util,
14 util,
15 )
15 )
16 from mercurial.dirstateutils import (
16 from mercurial.dirstateutils import (
17 timestamp,
17 timestamp,
18 )
18 )
19 from mercurial.interfaces import (
19 from mercurial.interfaces import (
20 dirstate as intdirstate,
20 dirstate as intdirstate,
21 util as interfaceutil,
21 util as interfaceutil,
22 )
22 )
23
23
24 from . import gitutil
24 from . import gitutil
25
25
26
26
27 DirstateItem = dirstatemap.DirstateItem
27 DirstateItem = dirstatemap.DirstateItem
28 propertycache = util.propertycache
28 propertycache = util.propertycache
29 pygit2 = gitutil.get_pygit2()
29 pygit2 = gitutil.get_pygit2()
30
30
31
31
32 def readpatternfile(orig, filepath, warn, sourceinfo=False):
32 def readpatternfile(orig, filepath, warn, sourceinfo=False):
33 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
33 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
34 return orig(filepath, warn, sourceinfo=False)
34 return orig(filepath, warn, sourceinfo=False)
35 result = []
35 result = []
36 warnings = []
36 warnings = []
37 with open(filepath, 'rb') as fp:
37 with open(filepath, 'rb') as fp:
38 for l in fp:
38 for l in fp:
39 l = l.strip()
39 l = l.strip()
40 if not l or l.startswith(b'#'):
40 if not l or l.startswith(b'#'):
41 continue
41 continue
42 if l.startswith(b'!'):
42 if l.startswith(b'!'):
43 warnings.append(b'unsupported ignore pattern %s' % l)
43 warnings.append(b'unsupported ignore pattern %s' % l)
44 continue
44 continue
45 if l.startswith(b'/'):
45 if l.startswith(b'/'):
46 result.append(b'rootglob:' + l[1:])
46 result.append(b'rootglob:' + l[1:])
47 else:
47 else:
48 result.append(b'relglob:' + l)
48 result.append(b'relglob:' + l)
49 return result, warnings
49 return result, warnings
50
50
51
51
52 extensions.wrapfunction(matchmod, 'readpatternfile', readpatternfile)
52 extensions.wrapfunction(matchmod, 'readpatternfile', readpatternfile)
53
53
54
54
55 _STATUS_MAP = {}
55 _STATUS_MAP = {}
56 if pygit2:
56 if pygit2:
57 _STATUS_MAP = {
57 _STATUS_MAP = {
58 pygit2.GIT_STATUS_CONFLICTED: b'm',
58 pygit2.GIT_STATUS_CONFLICTED: b'm',
59 pygit2.GIT_STATUS_CURRENT: b'n',
59 pygit2.GIT_STATUS_CURRENT: b'n',
60 pygit2.GIT_STATUS_IGNORED: b'?',
60 pygit2.GIT_STATUS_IGNORED: b'?',
61 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
61 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
62 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
62 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
63 pygit2.GIT_STATUS_INDEX_NEW: b'a',
63 pygit2.GIT_STATUS_INDEX_NEW: b'a',
64 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
64 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
65 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
65 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
66 pygit2.GIT_STATUS_WT_DELETED: b'r',
66 pygit2.GIT_STATUS_WT_DELETED: b'r',
67 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
67 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
68 pygit2.GIT_STATUS_WT_NEW: b'?',
68 pygit2.GIT_STATUS_WT_NEW: b'?',
69 pygit2.GIT_STATUS_WT_RENAMED: b'a',
69 pygit2.GIT_STATUS_WT_RENAMED: b'a',
70 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
70 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
71 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
71 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
72 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: b'm',
72 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: b'm',
73 }
73 }
74
74
75
75
76 @interfaceutil.implementer(intdirstate.idirstate)
76 @interfaceutil.implementer(intdirstate.idirstate)
77 class gitdirstate:
77 class gitdirstate:
78 def __init__(self, ui, vfs, gitrepo, use_dirstate_v2):
78 def __init__(self, ui, vfs, gitrepo, use_dirstate_v2):
79 self._ui = ui
79 self._ui = ui
80 self._root = os.path.dirname(vfs.base)
80 self._root = os.path.dirname(vfs.base)
81 self._opener = vfs
81 self._opener = vfs
82 self.git = gitrepo
82 self.git = gitrepo
83 self._plchangecallbacks = {}
83 self._plchangecallbacks = {}
84 # TODO: context.poststatusfixup is bad and uses this attribute
84 # TODO: context.poststatusfixup is bad and uses this attribute
85 self._dirty = False
85 self._dirty = False
86 self._mapcls = dirstatemap.dirstatemap
86 self._mapcls = dirstatemap.dirstatemap
87 self._use_dirstate_v2 = use_dirstate_v2
87 self._use_dirstate_v2 = use_dirstate_v2
88
88
89 @propertycache
89 @propertycache
90 def _map(self):
90 def _map(self):
91 """Return the dirstate contents (see documentation for dirstatemap)."""
91 """Return the dirstate contents (see documentation for dirstatemap)."""
92 self._map = self._mapcls(
92 self._map = self._mapcls(
93 self._ui,
93 self._ui,
94 self._opener,
94 self._opener,
95 self._root,
95 self._root,
96 sha1nodeconstants,
96 sha1nodeconstants,
97 self._use_dirstate_v2,
97 self._use_dirstate_v2,
98 )
98 )
99 return self._map
99 return self._map
100
100
101 def p1(self):
101 def p1(self):
102 try:
102 try:
103 return self.git.head.peel().id.raw
103 return self.git.head.peel().id.raw
104 except pygit2.GitError:
104 except pygit2.GitError:
105 # Typically happens when peeling HEAD fails, as in an
105 # Typically happens when peeling HEAD fails, as in an
106 # empty repository.
106 # empty repository.
107 return sha1nodeconstants.nullid
107 return sha1nodeconstants.nullid
108
108
109 def p2(self):
109 def p2(self):
110 # TODO: MERGE_HEAD? something like that, right?
110 # TODO: MERGE_HEAD? something like that, right?
111 return sha1nodeconstants.nullid
111 return sha1nodeconstants.nullid
112
112
113 def setparents(self, p1, p2=None):
113 def setparents(self, p1, p2=None):
114 if p2 is None:
114 if p2 is None:
115 p2 = sha1nodeconstants.nullid
115 p2 = sha1nodeconstants.nullid
116 assert p2 == sha1nodeconstants.nullid, b'TODO merging support'
116 assert p2 == sha1nodeconstants.nullid, b'TODO merging support'
117 self.git.head.set_target(gitutil.togitnode(p1))
117 self.git.head.set_target(gitutil.togitnode(p1))
118
118
119 @util.propertycache
119 @util.propertycache
120 def identity(self):
120 def identity(self):
121 return util.filestat.frompath(
121 return util.filestat.frompath(
122 os.path.join(self._root, b'.git', b'index')
122 os.path.join(self._root, b'.git', b'index')
123 )
123 )
124
124
125 def branch(self):
125 def branch(self):
126 return b'default'
126 return b'default'
127
127
128 def parents(self):
128 def parents(self):
129 # TODO how on earth do we find p2 if a merge is in flight?
129 # TODO how on earth do we find p2 if a merge is in flight?
130 return self.p1(), sha1nodeconstants.nullid
130 return self.p1(), sha1nodeconstants.nullid
131
131
132 def __iter__(self):
132 def __iter__(self):
133 return (pycompat.fsencode(f.path) for f in self.git.index)
133 return (pycompat.fsencode(f.path) for f in self.git.index)
134
134
135 def items(self):
135 def items(self):
136 for ie in self.git.index:
136 for ie in self.git.index:
137 yield ie.path, None # value should be a DirstateItem
137 yield ie.path, None # value should be a DirstateItem
138
138
139 # py2,3 compat forward
139 # py2,3 compat forward
140 iteritems = items
140 iteritems = items
141
141
142 def __getitem__(self, filename):
142 def __getitem__(self, filename):
143 try:
143 try:
144 gs = self.git.status_file(filename)
144 gs = self.git.status_file(filename)
145 except KeyError:
145 except KeyError:
146 return b'?'
146 return b'?'
147 return _STATUS_MAP[gs]
147 return _STATUS_MAP[gs]
148
148
149 def __contains__(self, filename):
149 def __contains__(self, filename):
150 try:
150 try:
151 gs = self.git.status_file(filename)
151 gs = self.git.status_file(filename)
152 return _STATUS_MAP[gs] != b'?'
152 return _STATUS_MAP[gs] != b'?'
153 except KeyError:
153 except KeyError:
154 return False
154 return False
155
155
156 def status(self, match, subrepos, ignored, clean, unknown):
156 def status(self, match, subrepos, ignored, clean, unknown):
157 listclean = clean
157 listclean = clean
158 # TODO handling of clean files - can we get that from git.status()?
158 # TODO handling of clean files - can we get that from git.status()?
159 modified, added, removed, deleted, unknown, ignored, clean = (
159 modified, added, removed, deleted, unknown, ignored, clean = (
160 [],
160 [],
161 [],
161 [],
162 [],
162 [],
163 [],
163 [],
164 [],
164 [],
165 [],
165 [],
166 [],
166 [],
167 )
167 )
168
168
169 try:
169 try:
170 mtime_boundary = timestamp.get_fs_now(self._opener)
170 mtime_boundary = timestamp.get_fs_now(self._opener)
171 except OSError:
171 except OSError:
172 # In largefiles or readonly context
172 # In largefiles or readonly context
173 mtime_boundary = None
173 mtime_boundary = None
174
174
175 gstatus = self.git.status()
175 gstatus = self.git.status()
176 for path, status in gstatus.items():
176 for path, status in gstatus.items():
177 path = pycompat.fsencode(path)
177 path = pycompat.fsencode(path)
178 if not match(path):
178 if not match(path):
179 continue
179 continue
180 if status == pygit2.GIT_STATUS_IGNORED:
180 if status == pygit2.GIT_STATUS_IGNORED:
181 if path.endswith(b'/'):
181 if path.endswith(b'/'):
182 continue
182 continue
183 ignored.append(path)
183 ignored.append(path)
184 elif status in (
184 elif status in (
185 pygit2.GIT_STATUS_WT_MODIFIED,
185 pygit2.GIT_STATUS_WT_MODIFIED,
186 pygit2.GIT_STATUS_INDEX_MODIFIED,
186 pygit2.GIT_STATUS_INDEX_MODIFIED,
187 pygit2.GIT_STATUS_WT_MODIFIED
187 pygit2.GIT_STATUS_WT_MODIFIED
188 | pygit2.GIT_STATUS_INDEX_MODIFIED,
188 | pygit2.GIT_STATUS_INDEX_MODIFIED,
189 ):
189 ):
190 modified.append(path)
190 modified.append(path)
191 elif status == pygit2.GIT_STATUS_INDEX_NEW:
191 elif status == pygit2.GIT_STATUS_INDEX_NEW:
192 added.append(path)
192 added.append(path)
193 elif status == pygit2.GIT_STATUS_WT_NEW:
193 elif status == pygit2.GIT_STATUS_WT_NEW:
194 unknown.append(path)
194 unknown.append(path)
195 elif status == pygit2.GIT_STATUS_WT_DELETED:
195 elif status == pygit2.GIT_STATUS_WT_DELETED:
196 deleted.append(path)
196 deleted.append(path)
197 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
197 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
198 removed.append(path)
198 removed.append(path)
199 else:
199 else:
200 raise error.Abort(
200 raise error.Abort(
201 b'unhandled case: status for %r is %r' % (path, status)
201 b'unhandled case: status for %r is %r' % (path, status)
202 )
202 )
203
203
204 if listclean:
204 if listclean:
205 observed = set(
205 observed = set(
206 modified + added + removed + deleted + unknown + ignored
206 modified + added + removed + deleted + unknown + ignored
207 )
207 )
208 index = self.git.index
208 index = self.git.index
209 index.read()
209 index.read()
210 for entry in index:
210 for entry in index:
211 path = pycompat.fsencode(entry.path)
211 path = pycompat.fsencode(entry.path)
212 if not match(path):
212 if not match(path):
213 continue
213 continue
214 if path in observed:
214 if path in observed:
215 continue # already in some other set
215 continue # already in some other set
216 if path[-1] == b'/':
216 if path[-1] == b'/':
217 continue # directory
217 continue # directory
218 clean.append(path)
218 clean.append(path)
219
219
220 # TODO are we really always sure of status here?
220 # TODO are we really always sure of status here?
221 return (
221 return (
222 False,
222 False,
223 scmutil.status(
223 scmutil.status(
224 modified, added, removed, deleted, unknown, ignored, clean
224 modified, added, removed, deleted, unknown, ignored, clean
225 ),
225 ),
226 mtime_boundary,
226 mtime_boundary,
227 )
227 )
228
228
229 def flagfunc(self, buildfallback):
229 def flagfunc(self, buildfallback):
230 # TODO we can do better
230 # TODO we can do better
231 return buildfallback()
231 return buildfallback()
232
232
233 def getcwd(self):
233 def getcwd(self):
234 # TODO is this a good way to do this?
234 # TODO is this a good way to do this?
235 return os.path.dirname(
235 return os.path.dirname(
236 os.path.dirname(pycompat.fsencode(self.git.path))
236 os.path.dirname(pycompat.fsencode(self.git.path))
237 )
237 )
238
238
239 def get_entry(self, path):
239 def get_entry(self, path):
240 """return a DirstateItem for the associated path"""
240 """return a DirstateItem for the associated path"""
241 entry = self._map.get(path)
241 entry = self._map.get(path)
242 if entry is None:
242 if entry is None:
243 return DirstateItem()
243 return DirstateItem()
244 return entry
244 return entry
245
245
246 def normalize(self, path):
246 def normalize(self, path, isknown=False, ignoremissing=False):
247 normed = util.normcase(path)
247 normed = util.normcase(path)
248 assert normed == path, b"TODO handling of case folding: %s != %s" % (
248 assert normed == path, b"TODO handling of case folding: %s != %s" % (
249 normed,
249 normed,
250 path,
250 path,
251 )
251 )
252 return path
252 return path
253
253
254 @property
254 @property
255 def _checklink(self):
255 def _checklink(self):
256 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
256 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
257
257
258 def copies(self):
258 def copies(self):
259 # TODO support copies?
259 # TODO support copies?
260 return {}
260 return {}
261
261
262 # # TODO what the heck is this
262 # # TODO what the heck is this
263 _filecache = set()
263 _filecache = set()
264
264
265 @property
265 def is_changing_parents(self):
266 def is_changing_parents(self):
266 # TODO: we need to implement the context manager bits and
267 # TODO: we need to implement the context manager bits and
267 # correctly stage/revert index edits.
268 # correctly stage/revert index edits.
268 return False
269 return False
269
270
271 @property
270 def is_changing_any(self):
272 def is_changing_any(self):
271 # TODO: we need to implement the context manager bits and
273 # TODO: we need to implement the context manager bits and
272 # correctly stage/revert index edits.
274 # correctly stage/revert index edits.
273 return False
275 return False
274
276
275 def write(self, tr):
277 def write(self, tr):
276 # TODO: call parent change callbacks
278 # TODO: call parent change callbacks
277
279
278 if tr:
280 if tr:
279
281
280 def writeinner(category):
282 def writeinner(category):
281 self.git.index.write()
283 self.git.index.write()
282
284
283 tr.addpending(b'gitdirstate', writeinner)
285 tr.addpending(b'gitdirstate', writeinner)
284 else:
286 else:
285 self.git.index.write()
287 self.git.index.write()
286
288
287 def pathto(self, f, cwd=None):
289 def pathto(self, f, cwd=None):
288 if cwd is None:
290 if cwd is None:
289 cwd = self.getcwd()
291 cwd = self.getcwd()
290 # TODO core dirstate does something about slashes here
292 # TODO core dirstate does something about slashes here
291 assert isinstance(f, bytes)
293 assert isinstance(f, bytes)
292 r = util.pathto(self._root, cwd, f)
294 r = util.pathto(self._root, cwd, f)
293 return r
295 return r
294
296
295 def matches(self, match):
297 def matches(self, match):
296 for x in self.git.index:
298 for x in self.git.index:
297 p = pycompat.fsencode(x.path)
299 p = pycompat.fsencode(x.path)
298 if match(p):
300 if match(p):
299 yield p
301 yield p
300
302
301 def set_clean(self, f, parentfiledata):
303 def set_clean(self, f, parentfiledata):
302 """Mark a file normal and clean."""
304 """Mark a file normal and clean."""
303 # TODO: for now we just let libgit2 re-stat the file. We can
305 # TODO: for now we just let libgit2 re-stat the file. We can
304 # clearly do better.
306 # clearly do better.
305
307
306 def set_possibly_dirty(self, f):
308 def set_possibly_dirty(self, f):
307 """Mark a file normal, but possibly dirty."""
309 """Mark a file normal, but possibly dirty."""
308 # TODO: for now we just let libgit2 re-stat the file. We can
310 # TODO: for now we just let libgit2 re-stat the file. We can
309 # clearly do better.
311 # clearly do better.
310
312
311 def walk(self, match, subrepos, unknown, ignored, full=True):
313 def walk(self, match, subrepos, unknown, ignored, full=True):
312 # TODO: we need to use .status() and not iterate the index,
314 # 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
315 # 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
316 # a new file without an intervening call to status will
315 # silently do nothing.
317 # silently do nothing.
316 r = {}
318 r = {}
317 cwd = self.getcwd()
319 cwd = self.getcwd()
318 for path, status in self.git.status().items():
320 for path, status in self.git.status().items():
319 if path.startswith('.hg/'):
321 if path.startswith('.hg/'):
320 continue
322 continue
321 path = pycompat.fsencode(path)
323 path = pycompat.fsencode(path)
322 if not match(path):
324 if not match(path):
323 continue
325 continue
324 # TODO construct the stat info from the status object?
326 # TODO construct the stat info from the status object?
325 try:
327 try:
326 s = os.stat(os.path.join(cwd, path))
328 s = os.stat(os.path.join(cwd, path))
327 except FileNotFoundError:
329 except FileNotFoundError:
328 continue
330 continue
329 r[path] = s
331 r[path] = s
330 return r
332 return r
331
333
332 def set_tracked(self, f, reset_copy=False):
334 def set_tracked(self, f, reset_copy=False):
333 # TODO: support copies and reset_copy=True
335 # TODO: support copies and reset_copy=True
334 uf = pycompat.fsdecode(f)
336 uf = pycompat.fsdecode(f)
335 if uf in self.git.index:
337 if uf in self.git.index:
336 return False
338 return False
337 index = self.git.index
339 index = self.git.index
338 index.read()
340 index.read()
339 index.add(uf)
341 index.add(uf)
340 index.write()
342 index.write()
341 return True
343 return True
342
344
343 def add(self, f):
345 def add(self, f):
344 index = self.git.index
346 index = self.git.index
345 index.read()
347 index.read()
346 index.add(pycompat.fsdecode(f))
348 index.add(pycompat.fsdecode(f))
347 index.write()
349 index.write()
348
350
349 def drop(self, f):
351 def drop(self, f):
350 index = self.git.index
352 index = self.git.index
351 index.read()
353 index.read()
352 fs = pycompat.fsdecode(f)
354 fs = pycompat.fsdecode(f)
353 if fs in index:
355 if fs in index:
354 index.remove(fs)
356 index.remove(fs)
355 index.write()
357 index.write()
356
358
357 def set_untracked(self, f):
359 def set_untracked(self, f):
358 index = self.git.index
360 index = self.git.index
359 index.read()
361 index.read()
360 fs = pycompat.fsdecode(f)
362 fs = pycompat.fsdecode(f)
361 if fs in index:
363 if fs in index:
362 index.remove(fs)
364 index.remove(fs)
363 index.write()
365 index.write()
364 return True
366 return True
365 return False
367 return False
366
368
367 def remove(self, f):
369 def remove(self, f):
368 index = self.git.index
370 index = self.git.index
369 index.read()
371 index.read()
370 index.remove(pycompat.fsdecode(f))
372 index.remove(pycompat.fsdecode(f))
371 index.write()
373 index.write()
372
374
373 def copied(self, path):
375 def copied(self, path):
374 # TODO: track copies?
376 # TODO: track copies?
375 return None
377 return None
376
378
377 def prefetch_parents(self):
379 def prefetch_parents(self):
378 # TODO
380 # TODO
379 pass
381 pass
380
382
381 def update_file(self, *args, **kwargs):
383 def update_file(self, *args, **kwargs):
382 # TODO
384 # TODO
383 pass
385 pass
384
386
385 @contextlib.contextmanager
387 @contextlib.contextmanager
386 def changing_parents(self, repo):
388 def changing_parents(self, repo):
387 # TODO: track this maybe?
389 # TODO: track this maybe?
388 yield
390 yield
389
391
390 def addparentchangecallback(self, category, callback):
392 def addparentchangecallback(self, category, callback):
391 # TODO: should this be added to the dirstate interface?
393 # TODO: should this be added to the dirstate interface?
392 self._plchangecallbacks[category] = callback
394 self._plchangecallbacks[category] = callback
393
395
394 def setbranch(self, branch, transaction):
396 def setbranch(self, branch, transaction):
395 raise error.Abort(
397 raise error.Abort(
396 b'git repos do not support branches. try using bookmarks'
398 b'git repos do not support branches. try using bookmarks'
397 )
399 )
General Comments 0
You need to be logged in to leave comments. Login now