##// END OF EJS Templates
git: make dirstate status() respect matcher...
Augie Fackler -
r45990:0c6b2cc9 default
parent child Browse files
Show More
@@ -1,320 +1,322 b''
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2
2
3 import contextlib
3 import contextlib
4 import errno
4 import errno
5 import os
5 import os
6
6
7 from mercurial import (
7 from mercurial import (
8 error,
8 error,
9 extensions,
9 extensions,
10 match as matchmod,
10 match as matchmod,
11 node as nodemod,
11 node as nodemod,
12 pycompat,
12 pycompat,
13 scmutil,
13 scmutil,
14 util,
14 util,
15 )
15 )
16 from mercurial.interfaces import (
16 from mercurial.interfaces import (
17 dirstate as intdirstate,
17 dirstate as intdirstate,
18 util as interfaceutil,
18 util as interfaceutil,
19 )
19 )
20
20
21 from . import gitutil
21 from . import gitutil
22
22
23 pygit2 = gitutil.get_pygit2()
23 pygit2 = gitutil.get_pygit2()
24
24
25
25
26 def readpatternfile(orig, filepath, warn, sourceinfo=False):
26 def readpatternfile(orig, filepath, warn, sourceinfo=False):
27 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
27 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
28 return orig(filepath, warn, sourceinfo=False)
28 return orig(filepath, warn, sourceinfo=False)
29 result = []
29 result = []
30 warnings = []
30 warnings = []
31 with open(filepath, b'rb') as fp:
31 with open(filepath, b'rb') as fp:
32 for l in fp:
32 for l in fp:
33 l = l.strip()
33 l = l.strip()
34 if not l or l.startswith(b'#'):
34 if not l or l.startswith(b'#'):
35 continue
35 continue
36 if l.startswith(b'!'):
36 if l.startswith(b'!'):
37 warnings.append(b'unsupported ignore pattern %s' % l)
37 warnings.append(b'unsupported ignore pattern %s' % l)
38 continue
38 continue
39 if l.startswith(b'/'):
39 if l.startswith(b'/'):
40 result.append(b'rootglob:' + l[1:])
40 result.append(b'rootglob:' + l[1:])
41 else:
41 else:
42 result.append(b'relglob:' + l)
42 result.append(b'relglob:' + l)
43 return result, warnings
43 return result, warnings
44
44
45
45
46 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
46 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
47
47
48
48
49 _STATUS_MAP = {}
49 _STATUS_MAP = {}
50 if pygit2:
50 if pygit2:
51 _STATUS_MAP = {
51 _STATUS_MAP = {
52 pygit2.GIT_STATUS_CONFLICTED: b'm',
52 pygit2.GIT_STATUS_CONFLICTED: b'm',
53 pygit2.GIT_STATUS_CURRENT: b'n',
53 pygit2.GIT_STATUS_CURRENT: b'n',
54 pygit2.GIT_STATUS_IGNORED: b'?',
54 pygit2.GIT_STATUS_IGNORED: b'?',
55 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
55 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
56 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
56 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
57 pygit2.GIT_STATUS_INDEX_NEW: b'a',
57 pygit2.GIT_STATUS_INDEX_NEW: b'a',
58 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
58 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
59 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
59 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
60 pygit2.GIT_STATUS_WT_DELETED: b'r',
60 pygit2.GIT_STATUS_WT_DELETED: b'r',
61 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
61 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
62 pygit2.GIT_STATUS_WT_NEW: b'?',
62 pygit2.GIT_STATUS_WT_NEW: b'?',
63 pygit2.GIT_STATUS_WT_RENAMED: b'a',
63 pygit2.GIT_STATUS_WT_RENAMED: b'a',
64 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
64 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
65 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
65 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
66 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: 'm',
66 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: 'm',
67 }
67 }
68
68
69
69
70 @interfaceutil.implementer(intdirstate.idirstate)
70 @interfaceutil.implementer(intdirstate.idirstate)
71 class gitdirstate(object):
71 class gitdirstate(object):
72 def __init__(self, ui, root, gitrepo):
72 def __init__(self, ui, root, gitrepo):
73 self._ui = ui
73 self._ui = ui
74 self._root = os.path.dirname(root)
74 self._root = os.path.dirname(root)
75 self.git = gitrepo
75 self.git = gitrepo
76 self._plchangecallbacks = {}
76 self._plchangecallbacks = {}
77
77
78 def p1(self):
78 def p1(self):
79 try:
79 try:
80 return self.git.head.peel().id.raw
80 return self.git.head.peel().id.raw
81 except pygit2.GitError:
81 except pygit2.GitError:
82 # Typically happens when peeling HEAD fails, as in an
82 # Typically happens when peeling HEAD fails, as in an
83 # empty repository.
83 # empty repository.
84 return nodemod.nullid
84 return nodemod.nullid
85
85
86 def p2(self):
86 def p2(self):
87 # TODO: MERGE_HEAD? something like that, right?
87 # TODO: MERGE_HEAD? something like that, right?
88 return nodemod.nullid
88 return nodemod.nullid
89
89
90 def setparents(self, p1, p2=nodemod.nullid):
90 def setparents(self, p1, p2=nodemod.nullid):
91 assert p2 == nodemod.nullid, b'TODO merging support'
91 assert p2 == nodemod.nullid, b'TODO merging support'
92 self.git.head.set_target(gitutil.togitnode(p1))
92 self.git.head.set_target(gitutil.togitnode(p1))
93
93
94 @util.propertycache
94 @util.propertycache
95 def identity(self):
95 def identity(self):
96 return util.filestat.frompath(
96 return util.filestat.frompath(
97 os.path.join(self._root, b'.git', b'index')
97 os.path.join(self._root, b'.git', b'index')
98 )
98 )
99
99
100 def branch(self):
100 def branch(self):
101 return b'default'
101 return b'default'
102
102
103 def parents(self):
103 def parents(self):
104 # TODO how on earth do we find p2 if a merge is in flight?
104 # TODO how on earth do we find p2 if a merge is in flight?
105 return self.p1(), nodemod.nullid
105 return self.p1(), nodemod.nullid
106
106
107 def __iter__(self):
107 def __iter__(self):
108 return (pycompat.fsencode(f.path) for f in self.git.index)
108 return (pycompat.fsencode(f.path) for f in self.git.index)
109
109
110 def items(self):
110 def items(self):
111 for ie in self.git.index:
111 for ie in self.git.index:
112 yield ie.path, None # value should be a dirstatetuple
112 yield ie.path, None # value should be a dirstatetuple
113
113
114 # py2,3 compat forward
114 # py2,3 compat forward
115 iteritems = items
115 iteritems = items
116
116
117 def __getitem__(self, filename):
117 def __getitem__(self, filename):
118 try:
118 try:
119 gs = self.git.status_file(filename)
119 gs = self.git.status_file(filename)
120 except KeyError:
120 except KeyError:
121 return b'?'
121 return b'?'
122 return _STATUS_MAP[gs]
122 return _STATUS_MAP[gs]
123
123
124 def __contains__(self, filename):
124 def __contains__(self, filename):
125 try:
125 try:
126 gs = self.git.status_file(filename)
126 gs = self.git.status_file(filename)
127 return _STATUS_MAP[gs] != b'?'
127 return _STATUS_MAP[gs] != b'?'
128 except KeyError:
128 except KeyError:
129 return False
129 return False
130
130
131 def status(self, match, subrepos, ignored, clean, unknown):
131 def status(self, match, subrepos, ignored, clean, unknown):
132 # TODO handling of clean files - can we get that from git.status()?
132 # TODO handling of clean files - can we get that from git.status()?
133 modified, added, removed, deleted, unknown, ignored, clean = (
133 modified, added, removed, deleted, unknown, ignored, clean = (
134 [],
134 [],
135 [],
135 [],
136 [],
136 [],
137 [],
137 [],
138 [],
138 [],
139 [],
139 [],
140 [],
140 [],
141 )
141 )
142 gstatus = self.git.status()
142 gstatus = self.git.status()
143 for path, status in gstatus.items():
143 for path, status in gstatus.items():
144 path = pycompat.fsencode(path)
144 path = pycompat.fsencode(path)
145 if not match(path):
146 continue
145 if status == pygit2.GIT_STATUS_IGNORED:
147 if status == pygit2.GIT_STATUS_IGNORED:
146 if path.endswith(b'/'):
148 if path.endswith(b'/'):
147 continue
149 continue
148 ignored.append(path)
150 ignored.append(path)
149 elif status in (
151 elif status in (
150 pygit2.GIT_STATUS_WT_MODIFIED,
152 pygit2.GIT_STATUS_WT_MODIFIED,
151 pygit2.GIT_STATUS_INDEX_MODIFIED,
153 pygit2.GIT_STATUS_INDEX_MODIFIED,
152 pygit2.GIT_STATUS_WT_MODIFIED
154 pygit2.GIT_STATUS_WT_MODIFIED
153 | pygit2.GIT_STATUS_INDEX_MODIFIED,
155 | pygit2.GIT_STATUS_INDEX_MODIFIED,
154 ):
156 ):
155 modified.append(path)
157 modified.append(path)
156 elif status == pygit2.GIT_STATUS_INDEX_NEW:
158 elif status == pygit2.GIT_STATUS_INDEX_NEW:
157 added.append(path)
159 added.append(path)
158 elif status == pygit2.GIT_STATUS_WT_NEW:
160 elif status == pygit2.GIT_STATUS_WT_NEW:
159 unknown.append(path)
161 unknown.append(path)
160 elif status == pygit2.GIT_STATUS_WT_DELETED:
162 elif status == pygit2.GIT_STATUS_WT_DELETED:
161 deleted.append(path)
163 deleted.append(path)
162 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
164 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
163 removed.append(path)
165 removed.append(path)
164 else:
166 else:
165 raise error.Abort(
167 raise error.Abort(
166 b'unhandled case: status for %r is %r' % (path, status)
168 b'unhandled case: status for %r is %r' % (path, status)
167 )
169 )
168
170
169 # TODO are we really always sure of status here?
171 # TODO are we really always sure of status here?
170 return (
172 return (
171 False,
173 False,
172 scmutil.status(
174 scmutil.status(
173 modified, added, removed, deleted, unknown, ignored, clean
175 modified, added, removed, deleted, unknown, ignored, clean
174 ),
176 ),
175 )
177 )
176
178
177 def flagfunc(self, buildfallback):
179 def flagfunc(self, buildfallback):
178 # TODO we can do better
180 # TODO we can do better
179 return buildfallback()
181 return buildfallback()
180
182
181 def getcwd(self):
183 def getcwd(self):
182 # TODO is this a good way to do this?
184 # TODO is this a good way to do this?
183 return os.path.dirname(
185 return os.path.dirname(
184 os.path.dirname(pycompat.fsencode(self.git.path))
186 os.path.dirname(pycompat.fsencode(self.git.path))
185 )
187 )
186
188
187 def normalize(self, path):
189 def normalize(self, path):
188 normed = util.normcase(path)
190 normed = util.normcase(path)
189 assert normed == path, b"TODO handling of case folding: %s != %s" % (
191 assert normed == path, b"TODO handling of case folding: %s != %s" % (
190 normed,
192 normed,
191 path,
193 path,
192 )
194 )
193 return path
195 return path
194
196
195 @property
197 @property
196 def _checklink(self):
198 def _checklink(self):
197 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
199 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
198
200
199 def copies(self):
201 def copies(self):
200 # TODO support copies?
202 # TODO support copies?
201 return {}
203 return {}
202
204
203 # # TODO what the heck is this
205 # # TODO what the heck is this
204 _filecache = set()
206 _filecache = set()
205
207
206 def pendingparentchange(self):
208 def pendingparentchange(self):
207 # TODO: we need to implement the context manager bits and
209 # TODO: we need to implement the context manager bits and
208 # correctly stage/revert index edits.
210 # correctly stage/revert index edits.
209 return False
211 return False
210
212
211 def write(self, tr):
213 def write(self, tr):
212 # TODO: call parent change callbacks
214 # TODO: call parent change callbacks
213
215
214 if tr:
216 if tr:
215
217
216 def writeinner(category):
218 def writeinner(category):
217 self.git.index.write()
219 self.git.index.write()
218
220
219 tr.addpending(b'gitdirstate', writeinner)
221 tr.addpending(b'gitdirstate', writeinner)
220 else:
222 else:
221 self.git.index.write()
223 self.git.index.write()
222
224
223 def pathto(self, f, cwd=None):
225 def pathto(self, f, cwd=None):
224 if cwd is None:
226 if cwd is None:
225 cwd = self.getcwd()
227 cwd = self.getcwd()
226 # TODO core dirstate does something about slashes here
228 # TODO core dirstate does something about slashes here
227 assert isinstance(f, bytes)
229 assert isinstance(f, bytes)
228 r = util.pathto(self._root, cwd, f)
230 r = util.pathto(self._root, cwd, f)
229 return r
231 return r
230
232
231 def matches(self, match):
233 def matches(self, match):
232 for x in self.git.index:
234 for x in self.git.index:
233 p = pycompat.fsencode(x.path)
235 p = pycompat.fsencode(x.path)
234 if match(p):
236 if match(p):
235 yield p
237 yield p
236
238
237 def normal(self, f, parentfiledata=None):
239 def normal(self, f, parentfiledata=None):
238 """Mark a file normal and clean."""
240 """Mark a file normal and clean."""
239 # TODO: for now we just let libgit2 re-stat the file. We can
241 # TODO: for now we just let libgit2 re-stat the file. We can
240 # clearly do better.
242 # clearly do better.
241
243
242 def normallookup(self, f):
244 def normallookup(self, f):
243 """Mark a file normal, but possibly dirty."""
245 """Mark a file normal, but possibly dirty."""
244 # TODO: for now we just let libgit2 re-stat the file. We can
246 # TODO: for now we just let libgit2 re-stat the file. We can
245 # clearly do better.
247 # clearly do better.
246
248
247 def walk(self, match, subrepos, unknown, ignored, full=True):
249 def walk(self, match, subrepos, unknown, ignored, full=True):
248 # TODO: we need to use .status() and not iterate the index,
250 # TODO: we need to use .status() and not iterate the index,
249 # because the index doesn't force a re-walk and so `hg add` of
251 # because the index doesn't force a re-walk and so `hg add` of
250 # a new file without an intervening call to status will
252 # a new file without an intervening call to status will
251 # silently do nothing.
253 # silently do nothing.
252 r = {}
254 r = {}
253 cwd = self.getcwd()
255 cwd = self.getcwd()
254 for path, status in self.git.status().items():
256 for path, status in self.git.status().items():
255 if path.startswith('.hg/'):
257 if path.startswith('.hg/'):
256 continue
258 continue
257 path = pycompat.fsencode(path)
259 path = pycompat.fsencode(path)
258 if not match(path):
260 if not match(path):
259 continue
261 continue
260 # TODO construct the stat info from the status object?
262 # TODO construct the stat info from the status object?
261 try:
263 try:
262 s = os.stat(os.path.join(cwd, path))
264 s = os.stat(os.path.join(cwd, path))
263 except OSError as e:
265 except OSError as e:
264 if e.errno != errno.ENOENT:
266 if e.errno != errno.ENOENT:
265 raise
267 raise
266 continue
268 continue
267 r[path] = s
269 r[path] = s
268 return r
270 return r
269
271
270 def savebackup(self, tr, backupname):
272 def savebackup(self, tr, backupname):
271 # TODO: figure out a strategy for saving index backups.
273 # TODO: figure out a strategy for saving index backups.
272 pass
274 pass
273
275
274 def restorebackup(self, tr, backupname):
276 def restorebackup(self, tr, backupname):
275 # TODO: figure out a strategy for saving index backups.
277 # TODO: figure out a strategy for saving index backups.
276 pass
278 pass
277
279
278 def add(self, f):
280 def add(self, f):
279 index = self.git.index
281 index = self.git.index
280 index.read()
282 index.read()
281 index.add(pycompat.fsdecode(f))
283 index.add(pycompat.fsdecode(f))
282 index.write()
284 index.write()
283
285
284 def drop(self, f):
286 def drop(self, f):
285 index = self.git.index
287 index = self.git.index
286 index.read()
288 index.read()
287 index.remove(pycompat.fsdecode(f))
289 index.remove(pycompat.fsdecode(f))
288 index.write()
290 index.write()
289
291
290 def remove(self, f):
292 def remove(self, f):
291 index = self.git.index
293 index = self.git.index
292 index.read()
294 index.read()
293 index.remove(pycompat.fsdecode(f))
295 index.remove(pycompat.fsdecode(f))
294 index.write()
296 index.write()
295
297
296 def copied(self, path):
298 def copied(self, path):
297 # TODO: track copies?
299 # TODO: track copies?
298 return None
300 return None
299
301
300 def prefetch_parents(self):
302 def prefetch_parents(self):
301 # TODO
303 # TODO
302 pass
304 pass
303
305
304 @contextlib.contextmanager
306 @contextlib.contextmanager
305 def parentchange(self):
307 def parentchange(self):
306 # TODO: track this maybe?
308 # TODO: track this maybe?
307 yield
309 yield
308
310
309 def addparentchangecallback(self, category, callback):
311 def addparentchangecallback(self, category, callback):
310 # TODO: should this be added to the dirstate interface?
312 # TODO: should this be added to the dirstate interface?
311 self._plchangecallbacks[category] = callback
313 self._plchangecallbacks[category] = callback
312
314
313 def clearbackup(self, tr, backupname):
315 def clearbackup(self, tr, backupname):
314 # TODO
316 # TODO
315 pass
317 pass
316
318
317 def setbranch(self, branch):
319 def setbranch(self, branch):
318 raise error.Abort(
320 raise error.Abort(
319 b'git repos do not support branches. try using bookmarks'
321 b'git repos do not support branches. try using bookmarks'
320 )
322 )
General Comments 0
You need to be logged in to leave comments. Login now