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