##// END OF EJS Templates
wrapfunction: use sysstr instead of bytes as argument in the "git" extension...
marmoute -
r51672:39eb3aab default
parent child Browse files
Show More
@@ -1,352 +1,352 b''
1 """grant Mercurial the ability to operate on Git repositories. (EXPERIMENTAL)
1 """grant Mercurial the ability to operate on Git repositories. (EXPERIMENTAL)
2
2
3 This is currently super experimental. It probably will consume your
3 This is currently super experimental. It probably will consume your
4 firstborn a la Rumpelstiltskin, etc.
4 firstborn a la Rumpelstiltskin, etc.
5 """
5 """
6
6
7
7
8 import os
8 import os
9
9
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11
11
12 from mercurial import (
12 from mercurial import (
13 commands,
13 commands,
14 error,
14 error,
15 extensions,
15 extensions,
16 localrepo,
16 localrepo,
17 pycompat,
17 pycompat,
18 registrar,
18 registrar,
19 requirements as requirementsmod,
19 requirements as requirementsmod,
20 scmutil,
20 scmutil,
21 store,
21 store,
22 util,
22 util,
23 )
23 )
24
24
25 from . import (
25 from . import (
26 dirstate,
26 dirstate,
27 gitlog,
27 gitlog,
28 gitutil,
28 gitutil,
29 index,
29 index,
30 )
30 )
31
31
32 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
32 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
33 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
33 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
34 # be specifying the version(s) of Mercurial they are tested with, or
34 # be specifying the version(s) of Mercurial they are tested with, or
35 # leave the attribute unspecified.
35 # leave the attribute unspecified.
36 testedwith = b'ships-with-hg-core'
36 testedwith = b'ships-with-hg-core'
37
37
38 configtable = {}
38 configtable = {}
39 configitem = registrar.configitem(configtable)
39 configitem = registrar.configitem(configtable)
40 # git.log-index-cache-miss: internal knob for testing
40 # git.log-index-cache-miss: internal knob for testing
41 configitem(
41 configitem(
42 b"git",
42 b"git",
43 b"log-index-cache-miss",
43 b"log-index-cache-miss",
44 default=False,
44 default=False,
45 )
45 )
46
46
47 getversion = gitutil.pygit2_version
47 getversion = gitutil.pygit2_version
48
48
49
49
50 # TODO: extract an interface for this in core
50 # TODO: extract an interface for this in core
51 class gitstore: # store.basicstore):
51 class gitstore: # store.basicstore):
52 def __init__(self, path, vfstype):
52 def __init__(self, path, vfstype):
53 self.vfs = vfstype(path)
53 self.vfs = vfstype(path)
54 self.opener = self.vfs
54 self.opener = self.vfs
55 self.path = self.vfs.base
55 self.path = self.vfs.base
56 self.createmode = store._calcmode(self.vfs)
56 self.createmode = store._calcmode(self.vfs)
57 # above lines should go away in favor of:
57 # above lines should go away in favor of:
58 # super(gitstore, self).__init__(path, vfstype)
58 # super(gitstore, self).__init__(path, vfstype)
59
59
60 self.git = gitutil.get_pygit2().Repository(
60 self.git = gitutil.get_pygit2().Repository(
61 os.path.normpath(os.path.join(path, b'..', b'.git'))
61 os.path.normpath(os.path.join(path, b'..', b'.git'))
62 )
62 )
63 self._progress_factory = lambda *args, **kwargs: None
63 self._progress_factory = lambda *args, **kwargs: None
64 self._logfn = lambda x: None
64 self._logfn = lambda x: None
65
65
66 @util.propertycache
66 @util.propertycache
67 def _db(self):
67 def _db(self):
68 # We lazy-create the database because we want to thread a
68 # We lazy-create the database because we want to thread a
69 # progress callback down to the indexing process if it's
69 # progress callback down to the indexing process if it's
70 # required, and we don't have a ui handle in makestore().
70 # required, and we don't have a ui handle in makestore().
71 return index.get_index(self.git, self._logfn, self._progress_factory)
71 return index.get_index(self.git, self._logfn, self._progress_factory)
72
72
73 def join(self, f):
73 def join(self, f):
74 """Fake store.join method for git repositories.
74 """Fake store.join method for git repositories.
75
75
76 For the most part, store.join is used for @storecache
76 For the most part, store.join is used for @storecache
77 decorators to invalidate caches when various files
77 decorators to invalidate caches when various files
78 change. We'll map the ones we care about, and ignore the rest.
78 change. We'll map the ones we care about, and ignore the rest.
79 """
79 """
80 if f in (b'00changelog.i', b'00manifest.i'):
80 if f in (b'00changelog.i', b'00manifest.i'):
81 # This is close enough: in order for the changelog cache
81 # This is close enough: in order for the changelog cache
82 # to be invalidated, HEAD will have to change.
82 # to be invalidated, HEAD will have to change.
83 return os.path.join(self.path, b'HEAD')
83 return os.path.join(self.path, b'HEAD')
84 elif f == b'lock':
84 elif f == b'lock':
85 # TODO: we probably want to map this to a git lock, I
85 # TODO: we probably want to map this to a git lock, I
86 # suspect index.lock. We should figure out what the
86 # suspect index.lock. We should figure out what the
87 # most-alike file is in git-land. For now we're risking
87 # most-alike file is in git-land. For now we're risking
88 # bad concurrency errors if another git client is used.
88 # bad concurrency errors if another git client is used.
89 return os.path.join(self.path, b'hgit-bogus-lock')
89 return os.path.join(self.path, b'hgit-bogus-lock')
90 elif f in (b'obsstore', b'phaseroots', b'narrowspec', b'bookmarks'):
90 elif f in (b'obsstore', b'phaseroots', b'narrowspec', b'bookmarks'):
91 return os.path.join(self.path, b'..', b'.hg', f)
91 return os.path.join(self.path, b'..', b'.hg', f)
92 raise NotImplementedError(b'Need to pick file for %s.' % f)
92 raise NotImplementedError(b'Need to pick file for %s.' % f)
93
93
94 def changelog(self, trypending, concurrencychecker):
94 def changelog(self, trypending, concurrencychecker):
95 # TODO we don't have a plan for trypending in hg's git support yet
95 # TODO we don't have a plan for trypending in hg's git support yet
96 return gitlog.changelog(self.git, self._db)
96 return gitlog.changelog(self.git, self._db)
97
97
98 def manifestlog(self, repo, storenarrowmatch):
98 def manifestlog(self, repo, storenarrowmatch):
99 # TODO handle storenarrowmatch and figure out if we need the repo arg
99 # TODO handle storenarrowmatch and figure out if we need the repo arg
100 return gitlog.manifestlog(self.git, self._db)
100 return gitlog.manifestlog(self.git, self._db)
101
101
102 def invalidatecaches(self):
102 def invalidatecaches(self):
103 pass
103 pass
104
104
105 def write(self, tr=None):
105 def write(self, tr=None):
106 # normally this handles things like fncache writes, which we don't have
106 # normally this handles things like fncache writes, which we don't have
107 pass
107 pass
108
108
109
109
110 def _makestore(orig, requirements, storebasepath, vfstype):
110 def _makestore(orig, requirements, storebasepath, vfstype):
111 if b'git' in requirements:
111 if b'git' in requirements:
112 if not os.path.exists(os.path.join(storebasepath, b'..', b'.git')):
112 if not os.path.exists(os.path.join(storebasepath, b'..', b'.git')):
113 raise error.Abort(
113 raise error.Abort(
114 _(
114 _(
115 b'repository specified git format in '
115 b'repository specified git format in '
116 b'.hg/requires but has no .git directory'
116 b'.hg/requires but has no .git directory'
117 )
117 )
118 )
118 )
119 # Check for presence of pygit2 only here. The assumption is that we'll
119 # Check for presence of pygit2 only here. The assumption is that we'll
120 # run this code iff we'll later need pygit2.
120 # run this code iff we'll later need pygit2.
121 if gitutil.get_pygit2() is None:
121 if gitutil.get_pygit2() is None:
122 raise error.Abort(
122 raise error.Abort(
123 _(
123 _(
124 b'the git extension requires the Python '
124 b'the git extension requires the Python '
125 b'pygit2 library to be installed'
125 b'pygit2 library to be installed'
126 )
126 )
127 )
127 )
128
128
129 return gitstore(storebasepath, vfstype)
129 return gitstore(storebasepath, vfstype)
130 return orig(requirements, storebasepath, vfstype)
130 return orig(requirements, storebasepath, vfstype)
131
131
132
132
133 class gitfilestorage:
133 class gitfilestorage:
134 def file(self, path):
134 def file(self, path):
135 if path[0:1] == b'/':
135 if path[0:1] == b'/':
136 path = path[1:]
136 path = path[1:]
137 return gitlog.filelog(self.store.git, self.store._db, path)
137 return gitlog.filelog(self.store.git, self.store._db, path)
138
138
139
139
140 def _makefilestorage(orig, requirements, features, **kwargs):
140 def _makefilestorage(orig, requirements, features, **kwargs):
141 store = kwargs['store']
141 store = kwargs['store']
142 if isinstance(store, gitstore):
142 if isinstance(store, gitstore):
143 return gitfilestorage
143 return gitfilestorage
144 return orig(requirements, features, **kwargs)
144 return orig(requirements, features, **kwargs)
145
145
146
146
147 def _setupdothg(ui, path):
147 def _setupdothg(ui, path):
148 dothg = os.path.join(path, b'.hg')
148 dothg = os.path.join(path, b'.hg')
149 if os.path.exists(dothg):
149 if os.path.exists(dothg):
150 ui.warn(_(b'git repo already initialized for hg\n'))
150 ui.warn(_(b'git repo already initialized for hg\n'))
151 else:
151 else:
152 os.mkdir(os.path.join(path, b'.hg'))
152 os.mkdir(os.path.join(path, b'.hg'))
153 # TODO is it ok to extend .git/info/exclude like this?
153 # TODO is it ok to extend .git/info/exclude like this?
154 with open(
154 with open(
155 os.path.join(path, b'.git', b'info', b'exclude'), 'ab'
155 os.path.join(path, b'.git', b'info', b'exclude'), 'ab'
156 ) as exclude:
156 ) as exclude:
157 exclude.write(b'\n.hg\n')
157 exclude.write(b'\n.hg\n')
158 with open(os.path.join(dothg, b'requires'), 'wb') as f:
158 with open(os.path.join(dothg, b'requires'), 'wb') as f:
159 f.write(b'git\n')
159 f.write(b'git\n')
160
160
161
161
162 _BMS_PREFIX = 'refs/heads/'
162 _BMS_PREFIX = 'refs/heads/'
163
163
164
164
165 class gitbmstore:
165 class gitbmstore:
166 def __init__(self, gitrepo):
166 def __init__(self, gitrepo):
167 self.gitrepo = gitrepo
167 self.gitrepo = gitrepo
168 self._aclean = True
168 self._aclean = True
169 self._active = gitrepo.references['HEAD'] # git head, not mark
169 self._active = gitrepo.references['HEAD'] # git head, not mark
170
170
171 def __contains__(self, name):
171 def __contains__(self, name):
172 return (
172 return (
173 _BMS_PREFIX + pycompat.fsdecode(name)
173 _BMS_PREFIX + pycompat.fsdecode(name)
174 ) in self.gitrepo.references
174 ) in self.gitrepo.references
175
175
176 def __iter__(self):
176 def __iter__(self):
177 for r in self.gitrepo.listall_references():
177 for r in self.gitrepo.listall_references():
178 if r.startswith(_BMS_PREFIX):
178 if r.startswith(_BMS_PREFIX):
179 yield pycompat.fsencode(r[len(_BMS_PREFIX) :])
179 yield pycompat.fsencode(r[len(_BMS_PREFIX) :])
180
180
181 def __getitem__(self, k):
181 def __getitem__(self, k):
182 return (
182 return (
183 self.gitrepo.references[_BMS_PREFIX + pycompat.fsdecode(k)]
183 self.gitrepo.references[_BMS_PREFIX + pycompat.fsdecode(k)]
184 .peel()
184 .peel()
185 .id.raw
185 .id.raw
186 )
186 )
187
187
188 def get(self, k, default=None):
188 def get(self, k, default=None):
189 try:
189 try:
190 if k in self:
190 if k in self:
191 return self[k]
191 return self[k]
192 return default
192 return default
193 except gitutil.get_pygit2().InvalidSpecError:
193 except gitutil.get_pygit2().InvalidSpecError:
194 return default
194 return default
195
195
196 @property
196 @property
197 def active(self):
197 def active(self):
198 h = self.gitrepo.references['HEAD']
198 h = self.gitrepo.references['HEAD']
199 if not isinstance(h.target, str) or not h.target.startswith(
199 if not isinstance(h.target, str) or not h.target.startswith(
200 _BMS_PREFIX
200 _BMS_PREFIX
201 ):
201 ):
202 return None
202 return None
203 return pycompat.fsencode(h.target[len(_BMS_PREFIX) :])
203 return pycompat.fsencode(h.target[len(_BMS_PREFIX) :])
204
204
205 @active.setter
205 @active.setter
206 def active(self, mark):
206 def active(self, mark):
207 githead = None
207 githead = None
208 if mark is not None:
208 if mark is not None:
209 githead = _BMS_PREFIX + pycompat.fsdecode(mark)
209 githead = _BMS_PREFIX + pycompat.fsdecode(mark)
210 if githead is not None and githead not in self.gitrepo.references:
210 if githead is not None and githead not in self.gitrepo.references:
211 raise AssertionError(b'bookmark %s does not exist!' % mark)
211 raise AssertionError(b'bookmark %s does not exist!' % mark)
212
212
213 self._active = githead
213 self._active = githead
214 self._aclean = False
214 self._aclean = False
215
215
216 def _writeactive(self):
216 def _writeactive(self):
217 if self._aclean:
217 if self._aclean:
218 return
218 return
219 self.gitrepo.references.create('HEAD', self._active, True)
219 self.gitrepo.references.create('HEAD', self._active, True)
220 self._aclean = True
220 self._aclean = True
221
221
222 def names(self, node):
222 def names(self, node):
223 r = []
223 r = []
224 for ref in self.gitrepo.listall_references():
224 for ref in self.gitrepo.listall_references():
225 if not ref.startswith(_BMS_PREFIX):
225 if not ref.startswith(_BMS_PREFIX):
226 continue
226 continue
227 if self.gitrepo.references[ref].peel().id.raw != node:
227 if self.gitrepo.references[ref].peel().id.raw != node:
228 continue
228 continue
229 r.append(pycompat.fsencode(ref[len(_BMS_PREFIX) :]))
229 r.append(pycompat.fsencode(ref[len(_BMS_PREFIX) :]))
230 return r
230 return r
231
231
232 # Cleanup opportunity: this is *identical* to core's bookmarks store.
232 # Cleanup opportunity: this is *identical* to core's bookmarks store.
233 def expandname(self, bname):
233 def expandname(self, bname):
234 if bname == b'.':
234 if bname == b'.':
235 if self.active:
235 if self.active:
236 return self.active
236 return self.active
237 raise error.RepoLookupError(_(b"no active bookmark"))
237 raise error.RepoLookupError(_(b"no active bookmark"))
238 return bname
238 return bname
239
239
240 def applychanges(self, repo, tr, changes):
240 def applychanges(self, repo, tr, changes):
241 """Apply a list of changes to bookmarks"""
241 """Apply a list of changes to bookmarks"""
242 # TODO: this should respect transactions, but that's going to
242 # TODO: this should respect transactions, but that's going to
243 # require enlarging the gitbmstore to know how to do in-memory
243 # require enlarging the gitbmstore to know how to do in-memory
244 # temporary writes and read those back prior to transaction
244 # temporary writes and read those back prior to transaction
245 # finalization.
245 # finalization.
246 for name, node in changes:
246 for name, node in changes:
247 if node is None:
247 if node is None:
248 self.gitrepo.references.delete(
248 self.gitrepo.references.delete(
249 _BMS_PREFIX + pycompat.fsdecode(name)
249 _BMS_PREFIX + pycompat.fsdecode(name)
250 )
250 )
251 else:
251 else:
252 self.gitrepo.references.create(
252 self.gitrepo.references.create(
253 _BMS_PREFIX + pycompat.fsdecode(name),
253 _BMS_PREFIX + pycompat.fsdecode(name),
254 gitutil.togitnode(node),
254 gitutil.togitnode(node),
255 force=True,
255 force=True,
256 )
256 )
257
257
258 def checkconflict(self, mark, force=False, target=None):
258 def checkconflict(self, mark, force=False, target=None):
259 githead = _BMS_PREFIX + pycompat.fsdecode(mark)
259 githead = _BMS_PREFIX + pycompat.fsdecode(mark)
260 cur = self.gitrepo.references['HEAD']
260 cur = self.gitrepo.references['HEAD']
261 if githead in self.gitrepo.references and not force:
261 if githead in self.gitrepo.references and not force:
262 if target:
262 if target:
263 if self.gitrepo.references[githead] == target and target == cur:
263 if self.gitrepo.references[githead] == target and target == cur:
264 # re-activating a bookmark
264 # re-activating a bookmark
265 return []
265 return []
266 # moving a bookmark - forward?
266 # moving a bookmark - forward?
267 raise NotImplementedError
267 raise NotImplementedError
268 raise error.Abort(
268 raise error.Abort(
269 _(b"bookmark '%s' already exists (use -f to force)") % mark
269 _(b"bookmark '%s' already exists (use -f to force)") % mark
270 )
270 )
271 if len(mark) > 3 and not force:
271 if len(mark) > 3 and not force:
272 try:
272 try:
273 shadowhash = scmutil.isrevsymbol(self._repo, mark)
273 shadowhash = scmutil.isrevsymbol(self._repo, mark)
274 except error.LookupError: # ambiguous identifier
274 except error.LookupError: # ambiguous identifier
275 shadowhash = False
275 shadowhash = False
276 if shadowhash:
276 if shadowhash:
277 self._repo.ui.warn(
277 self._repo.ui.warn(
278 _(
278 _(
279 b"bookmark %s matches a changeset hash\n"
279 b"bookmark %s matches a changeset hash\n"
280 b"(did you leave a -r out of an 'hg bookmark' "
280 b"(did you leave a -r out of an 'hg bookmark' "
281 b"command?)\n"
281 b"command?)\n"
282 )
282 )
283 % mark
283 % mark
284 )
284 )
285 return []
285 return []
286
286
287
287
288 def init(orig, ui, dest=b'.', **opts):
288 def init(orig, ui, dest=b'.', **opts):
289 if opts.get('git', False):
289 if opts.get('git', False):
290 path = util.abspath(dest)
290 path = util.abspath(dest)
291 # TODO: walk up looking for the git repo
291 # TODO: walk up looking for the git repo
292 _setupdothg(ui, path)
292 _setupdothg(ui, path)
293 return 0
293 return 0
294 return orig(ui, dest=dest, **opts)
294 return orig(ui, dest=dest, **opts)
295
295
296
296
297 def reposetup(ui, repo):
297 def reposetup(ui, repo):
298 if repo.local() and isinstance(repo.store, gitstore):
298 if repo.local() and isinstance(repo.store, gitstore):
299 orig = repo.__class__
299 orig = repo.__class__
300 repo.store._progress_factory = repo.ui.makeprogress
300 repo.store._progress_factory = repo.ui.makeprogress
301 if ui.configbool(b'git', b'log-index-cache-miss'):
301 if ui.configbool(b'git', b'log-index-cache-miss'):
302 repo.store._logfn = repo.ui.warn
302 repo.store._logfn = repo.ui.warn
303
303
304 class gitlocalrepo(orig):
304 class gitlocalrepo(orig):
305 def _makedirstate(self):
305 def _makedirstate(self):
306 v2_req = requirementsmod.DIRSTATE_V2_REQUIREMENT
306 v2_req = requirementsmod.DIRSTATE_V2_REQUIREMENT
307 use_dirstate_v2 = v2_req in self.requirements
307 use_dirstate_v2 = v2_req in self.requirements
308
308
309 # TODO narrow support here
309 # TODO narrow support here
310 return dirstate.gitdirstate(
310 return dirstate.gitdirstate(
311 self.ui,
311 self.ui,
312 self.vfs,
312 self.vfs,
313 self.store.git,
313 self.store.git,
314 use_dirstate_v2,
314 use_dirstate_v2,
315 )
315 )
316
316
317 def commit(self, *args, **kwargs):
317 def commit(self, *args, **kwargs):
318 ret = orig.commit(self, *args, **kwargs)
318 ret = orig.commit(self, *args, **kwargs)
319 if ret is None:
319 if ret is None:
320 # there was nothing to commit, so we should skip
320 # there was nothing to commit, so we should skip
321 # the index fixup logic we'd otherwise do.
321 # the index fixup logic we'd otherwise do.
322 return None
322 return None
323 tid = self.store.git[gitutil.togitnode(ret)].tree.id
323 tid = self.store.git[gitutil.togitnode(ret)].tree.id
324 # DANGER! This will flush any writes staged to the
324 # DANGER! This will flush any writes staged to the
325 # index in Git, but we're sidestepping the index in a
325 # index in Git, but we're sidestepping the index in a
326 # way that confuses git when we commit. Alas.
326 # way that confuses git when we commit. Alas.
327 self.store.git.index.read_tree(tid)
327 self.store.git.index.read_tree(tid)
328 self.store.git.index.write()
328 self.store.git.index.write()
329 return ret
329 return ret
330
330
331 @property
331 @property
332 def _bookmarks(self):
332 def _bookmarks(self):
333 return gitbmstore(self.store.git)
333 return gitbmstore(self.store.git)
334
334
335 repo.__class__ = gitlocalrepo
335 repo.__class__ = gitlocalrepo
336 return repo
336 return repo
337
337
338
338
339 def _featuresetup(ui, supported):
339 def _featuresetup(ui, supported):
340 # don't die on seeing a repo with the git requirement
340 # don't die on seeing a repo with the git requirement
341 supported |= {b'git'}
341 supported |= {b'git'}
342
342
343
343
344 def extsetup(ui):
344 def extsetup(ui):
345 extensions.wrapfunction(localrepo, b'makestore', _makestore)
345 extensions.wrapfunction(localrepo, 'makestore', _makestore)
346 extensions.wrapfunction(localrepo, b'makefilestorage', _makefilestorage)
346 extensions.wrapfunction(localrepo, 'makefilestorage', _makefilestorage)
347 # Inject --git flag for `hg init`
347 # Inject --git flag for `hg init`
348 entry = extensions.wrapcommand(commands.table, b'init', init)
348 entry = extensions.wrapcommand(commands.table, b'init', init)
349 entry[1].extend(
349 entry[1].extend(
350 [(b'', b'git', None, b'setup up a git repository instead of hg')]
350 [(b'', b'git', None, b'setup up a git repository instead of hg')]
351 )
351 )
352 localrepo.featuresetupfuncs.add(_featuresetup)
352 localrepo.featuresetupfuncs.add(_featuresetup)
@@ -1,395 +1,395 b''
1 import contextlib
1 import contextlib
2 import os
2 import os
3
3
4 from mercurial.node import sha1nodeconstants
4 from mercurial.node import sha1nodeconstants
5 from mercurial import (
5 from mercurial import (
6 dirstatemap,
6 dirstatemap,
7 error,
7 error,
8 extensions,
8 extensions,
9 match as matchmod,
9 match as matchmod,
10 pycompat,
10 pycompat,
11 scmutil,
11 scmutil,
12 util,
12 util,
13 )
13 )
14 from mercurial.dirstateutils import (
14 from mercurial.dirstateutils import (
15 timestamp,
15 timestamp,
16 )
16 )
17 from mercurial.interfaces import (
17 from mercurial.interfaces import (
18 dirstate as intdirstate,
18 dirstate as intdirstate,
19 util as interfaceutil,
19 util as interfaceutil,
20 )
20 )
21
21
22 from . import gitutil
22 from . import gitutil
23
23
24
24
25 DirstateItem = dirstatemap.DirstateItem
25 DirstateItem = dirstatemap.DirstateItem
26 propertycache = util.propertycache
26 propertycache = util.propertycache
27 pygit2 = gitutil.get_pygit2()
27 pygit2 = gitutil.get_pygit2()
28
28
29
29
30 def readpatternfile(orig, filepath, warn, sourceinfo=False):
30 def readpatternfile(orig, filepath, warn, sourceinfo=False):
31 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
31 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
32 return orig(filepath, warn, sourceinfo=False)
32 return orig(filepath, warn, sourceinfo=False)
33 result = []
33 result = []
34 warnings = []
34 warnings = []
35 with open(filepath, 'rb') as fp:
35 with open(filepath, 'rb') as fp:
36 for l in fp:
36 for l in fp:
37 l = l.strip()
37 l = l.strip()
38 if not l or l.startswith(b'#'):
38 if not l or l.startswith(b'#'):
39 continue
39 continue
40 if l.startswith(b'!'):
40 if l.startswith(b'!'):
41 warnings.append(b'unsupported ignore pattern %s' % l)
41 warnings.append(b'unsupported ignore pattern %s' % l)
42 continue
42 continue
43 if l.startswith(b'/'):
43 if l.startswith(b'/'):
44 result.append(b'rootglob:' + l[1:])
44 result.append(b'rootglob:' + l[1:])
45 else:
45 else:
46 result.append(b'relglob:' + l)
46 result.append(b'relglob:' + l)
47 return result, warnings
47 return result, warnings
48
48
49
49
50 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
50 extensions.wrapfunction(matchmod, 'readpatternfile', readpatternfile)
51
51
52
52
53 _STATUS_MAP = {}
53 _STATUS_MAP = {}
54 if pygit2:
54 if pygit2:
55 _STATUS_MAP = {
55 _STATUS_MAP = {
56 pygit2.GIT_STATUS_CONFLICTED: b'm',
56 pygit2.GIT_STATUS_CONFLICTED: b'm',
57 pygit2.GIT_STATUS_CURRENT: b'n',
57 pygit2.GIT_STATUS_CURRENT: b'n',
58 pygit2.GIT_STATUS_IGNORED: b'?',
58 pygit2.GIT_STATUS_IGNORED: b'?',
59 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
59 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
60 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
60 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
61 pygit2.GIT_STATUS_INDEX_NEW: b'a',
61 pygit2.GIT_STATUS_INDEX_NEW: b'a',
62 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
62 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
63 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
63 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
64 pygit2.GIT_STATUS_WT_DELETED: b'r',
64 pygit2.GIT_STATUS_WT_DELETED: b'r',
65 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
65 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
66 pygit2.GIT_STATUS_WT_NEW: b'?',
66 pygit2.GIT_STATUS_WT_NEW: b'?',
67 pygit2.GIT_STATUS_WT_RENAMED: b'a',
67 pygit2.GIT_STATUS_WT_RENAMED: b'a',
68 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
68 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
69 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
69 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
70 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: b'm',
70 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: b'm',
71 }
71 }
72
72
73
73
74 @interfaceutil.implementer(intdirstate.idirstate)
74 @interfaceutil.implementer(intdirstate.idirstate)
75 class gitdirstate:
75 class gitdirstate:
76 def __init__(self, ui, vfs, gitrepo, use_dirstate_v2):
76 def __init__(self, ui, vfs, gitrepo, use_dirstate_v2):
77 self._ui = ui
77 self._ui = ui
78 self._root = os.path.dirname(vfs.base)
78 self._root = os.path.dirname(vfs.base)
79 self._opener = vfs
79 self._opener = vfs
80 self.git = gitrepo
80 self.git = gitrepo
81 self._plchangecallbacks = {}
81 self._plchangecallbacks = {}
82 # TODO: context.poststatusfixup is bad and uses this attribute
82 # TODO: context.poststatusfixup is bad and uses this attribute
83 self._dirty = False
83 self._dirty = False
84 self._mapcls = dirstatemap.dirstatemap
84 self._mapcls = dirstatemap.dirstatemap
85 self._use_dirstate_v2 = use_dirstate_v2
85 self._use_dirstate_v2 = use_dirstate_v2
86
86
87 @propertycache
87 @propertycache
88 def _map(self):
88 def _map(self):
89 """Return the dirstate contents (see documentation for dirstatemap)."""
89 """Return the dirstate contents (see documentation for dirstatemap)."""
90 self._map = self._mapcls(
90 self._map = self._mapcls(
91 self._ui,
91 self._ui,
92 self._opener,
92 self._opener,
93 self._root,
93 self._root,
94 sha1nodeconstants,
94 sha1nodeconstants,
95 self._use_dirstate_v2,
95 self._use_dirstate_v2,
96 )
96 )
97 return self._map
97 return self._map
98
98
99 def p1(self):
99 def p1(self):
100 try:
100 try:
101 return self.git.head.peel().id.raw
101 return self.git.head.peel().id.raw
102 except pygit2.GitError:
102 except pygit2.GitError:
103 # Typically happens when peeling HEAD fails, as in an
103 # Typically happens when peeling HEAD fails, as in an
104 # empty repository.
104 # empty repository.
105 return sha1nodeconstants.nullid
105 return sha1nodeconstants.nullid
106
106
107 def p2(self):
107 def p2(self):
108 # TODO: MERGE_HEAD? something like that, right?
108 # TODO: MERGE_HEAD? something like that, right?
109 return sha1nodeconstants.nullid
109 return sha1nodeconstants.nullid
110
110
111 def setparents(self, p1, p2=None):
111 def setparents(self, p1, p2=None):
112 if p2 is None:
112 if p2 is None:
113 p2 = sha1nodeconstants.nullid
113 p2 = sha1nodeconstants.nullid
114 assert p2 == sha1nodeconstants.nullid, b'TODO merging support'
114 assert p2 == sha1nodeconstants.nullid, b'TODO merging support'
115 self.git.head.set_target(gitutil.togitnode(p1))
115 self.git.head.set_target(gitutil.togitnode(p1))
116
116
117 @util.propertycache
117 @util.propertycache
118 def identity(self):
118 def identity(self):
119 return util.filestat.frompath(
119 return util.filestat.frompath(
120 os.path.join(self._root, b'.git', b'index')
120 os.path.join(self._root, b'.git', b'index')
121 )
121 )
122
122
123 def branch(self):
123 def branch(self):
124 return b'default'
124 return b'default'
125
125
126 def parents(self):
126 def parents(self):
127 # TODO how on earth do we find p2 if a merge is in flight?
127 # TODO how on earth do we find p2 if a merge is in flight?
128 return self.p1(), sha1nodeconstants.nullid
128 return self.p1(), sha1nodeconstants.nullid
129
129
130 def __iter__(self):
130 def __iter__(self):
131 return (pycompat.fsencode(f.path) for f in self.git.index)
131 return (pycompat.fsencode(f.path) for f in self.git.index)
132
132
133 def items(self):
133 def items(self):
134 for ie in self.git.index:
134 for ie in self.git.index:
135 yield ie.path, None # value should be a DirstateItem
135 yield ie.path, None # value should be a DirstateItem
136
136
137 # py2,3 compat forward
137 # py2,3 compat forward
138 iteritems = items
138 iteritems = items
139
139
140 def __getitem__(self, filename):
140 def __getitem__(self, filename):
141 try:
141 try:
142 gs = self.git.status_file(filename)
142 gs = self.git.status_file(filename)
143 except KeyError:
143 except KeyError:
144 return b'?'
144 return b'?'
145 return _STATUS_MAP[gs]
145 return _STATUS_MAP[gs]
146
146
147 def __contains__(self, filename):
147 def __contains__(self, filename):
148 try:
148 try:
149 gs = self.git.status_file(filename)
149 gs = self.git.status_file(filename)
150 return _STATUS_MAP[gs] != b'?'
150 return _STATUS_MAP[gs] != b'?'
151 except KeyError:
151 except KeyError:
152 return False
152 return False
153
153
154 def status(self, match, subrepos, ignored, clean, unknown):
154 def status(self, match, subrepos, ignored, clean, unknown):
155 listclean = clean
155 listclean = clean
156 # TODO handling of clean files - can we get that from git.status()?
156 # TODO handling of clean files - can we get that from git.status()?
157 modified, added, removed, deleted, unknown, ignored, clean = (
157 modified, added, removed, deleted, unknown, ignored, clean = (
158 [],
158 [],
159 [],
159 [],
160 [],
160 [],
161 [],
161 [],
162 [],
162 [],
163 [],
163 [],
164 [],
164 [],
165 )
165 )
166
166
167 try:
167 try:
168 mtime_boundary = timestamp.get_fs_now(self._opener)
168 mtime_boundary = timestamp.get_fs_now(self._opener)
169 except OSError:
169 except OSError:
170 # In largefiles or readonly context
170 # In largefiles or readonly context
171 mtime_boundary = None
171 mtime_boundary = None
172
172
173 gstatus = self.git.status()
173 gstatus = self.git.status()
174 for path, status in gstatus.items():
174 for path, status in gstatus.items():
175 path = pycompat.fsencode(path)
175 path = pycompat.fsencode(path)
176 if not match(path):
176 if not match(path):
177 continue
177 continue
178 if status == pygit2.GIT_STATUS_IGNORED:
178 if status == pygit2.GIT_STATUS_IGNORED:
179 if path.endswith(b'/'):
179 if path.endswith(b'/'):
180 continue
180 continue
181 ignored.append(path)
181 ignored.append(path)
182 elif status in (
182 elif status in (
183 pygit2.GIT_STATUS_WT_MODIFIED,
183 pygit2.GIT_STATUS_WT_MODIFIED,
184 pygit2.GIT_STATUS_INDEX_MODIFIED,
184 pygit2.GIT_STATUS_INDEX_MODIFIED,
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 ):
187 ):
188 modified.append(path)
188 modified.append(path)
189 elif status == pygit2.GIT_STATUS_INDEX_NEW:
189 elif status == pygit2.GIT_STATUS_INDEX_NEW:
190 added.append(path)
190 added.append(path)
191 elif status == pygit2.GIT_STATUS_WT_NEW:
191 elif status == pygit2.GIT_STATUS_WT_NEW:
192 unknown.append(path)
192 unknown.append(path)
193 elif status == pygit2.GIT_STATUS_WT_DELETED:
193 elif status == pygit2.GIT_STATUS_WT_DELETED:
194 deleted.append(path)
194 deleted.append(path)
195 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
195 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
196 removed.append(path)
196 removed.append(path)
197 else:
197 else:
198 raise error.Abort(
198 raise error.Abort(
199 b'unhandled case: status for %r is %r' % (path, status)
199 b'unhandled case: status for %r is %r' % (path, status)
200 )
200 )
201
201
202 if listclean:
202 if listclean:
203 observed = set(
203 observed = set(
204 modified + added + removed + deleted + unknown + ignored
204 modified + added + removed + deleted + unknown + ignored
205 )
205 )
206 index = self.git.index
206 index = self.git.index
207 index.read()
207 index.read()
208 for entry in index:
208 for entry in index:
209 path = pycompat.fsencode(entry.path)
209 path = pycompat.fsencode(entry.path)
210 if not match(path):
210 if not match(path):
211 continue
211 continue
212 if path in observed:
212 if path in observed:
213 continue # already in some other set
213 continue # already in some other set
214 if path[-1] == b'/':
214 if path[-1] == b'/':
215 continue # directory
215 continue # directory
216 clean.append(path)
216 clean.append(path)
217
217
218 # TODO are we really always sure of status here?
218 # TODO are we really always sure of status here?
219 return (
219 return (
220 False,
220 False,
221 scmutil.status(
221 scmutil.status(
222 modified, added, removed, deleted, unknown, ignored, clean
222 modified, added, removed, deleted, unknown, ignored, clean
223 ),
223 ),
224 mtime_boundary,
224 mtime_boundary,
225 )
225 )
226
226
227 def flagfunc(self, buildfallback):
227 def flagfunc(self, buildfallback):
228 # TODO we can do better
228 # TODO we can do better
229 return buildfallback()
229 return buildfallback()
230
230
231 def getcwd(self):
231 def getcwd(self):
232 # TODO is this a good way to do this?
232 # TODO is this a good way to do this?
233 return os.path.dirname(
233 return os.path.dirname(
234 os.path.dirname(pycompat.fsencode(self.git.path))
234 os.path.dirname(pycompat.fsencode(self.git.path))
235 )
235 )
236
236
237 def get_entry(self, path):
237 def get_entry(self, path):
238 """return a DirstateItem for the associated path"""
238 """return a DirstateItem for the associated path"""
239 entry = self._map.get(path)
239 entry = self._map.get(path)
240 if entry is None:
240 if entry is None:
241 return DirstateItem()
241 return DirstateItem()
242 return entry
242 return entry
243
243
244 def normalize(self, path):
244 def normalize(self, path):
245 normed = util.normcase(path)
245 normed = util.normcase(path)
246 assert normed == path, b"TODO handling of case folding: %s != %s" % (
246 assert normed == path, b"TODO handling of case folding: %s != %s" % (
247 normed,
247 normed,
248 path,
248 path,
249 )
249 )
250 return path
250 return path
251
251
252 @property
252 @property
253 def _checklink(self):
253 def _checklink(self):
254 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
254 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
255
255
256 def copies(self):
256 def copies(self):
257 # TODO support copies?
257 # TODO support copies?
258 return {}
258 return {}
259
259
260 # # TODO what the heck is this
260 # # TODO what the heck is this
261 _filecache = set()
261 _filecache = set()
262
262
263 def is_changing_parents(self):
263 def is_changing_parents(self):
264 # TODO: we need to implement the context manager bits and
264 # TODO: we need to implement the context manager bits and
265 # correctly stage/revert index edits.
265 # correctly stage/revert index edits.
266 return False
266 return False
267
267
268 def is_changing_any(self):
268 def is_changing_any(self):
269 # TODO: we need to implement the context manager bits and
269 # TODO: we need to implement the context manager bits and
270 # correctly stage/revert index edits.
270 # correctly stage/revert index edits.
271 return False
271 return False
272
272
273 def write(self, tr):
273 def write(self, tr):
274 # TODO: call parent change callbacks
274 # TODO: call parent change callbacks
275
275
276 if tr:
276 if tr:
277
277
278 def writeinner(category):
278 def writeinner(category):
279 self.git.index.write()
279 self.git.index.write()
280
280
281 tr.addpending(b'gitdirstate', writeinner)
281 tr.addpending(b'gitdirstate', writeinner)
282 else:
282 else:
283 self.git.index.write()
283 self.git.index.write()
284
284
285 def pathto(self, f, cwd=None):
285 def pathto(self, f, cwd=None):
286 if cwd is None:
286 if cwd is None:
287 cwd = self.getcwd()
287 cwd = self.getcwd()
288 # TODO core dirstate does something about slashes here
288 # TODO core dirstate does something about slashes here
289 assert isinstance(f, bytes)
289 assert isinstance(f, bytes)
290 r = util.pathto(self._root, cwd, f)
290 r = util.pathto(self._root, cwd, f)
291 return r
291 return r
292
292
293 def matches(self, match):
293 def matches(self, match):
294 for x in self.git.index:
294 for x in self.git.index:
295 p = pycompat.fsencode(x.path)
295 p = pycompat.fsencode(x.path)
296 if match(p):
296 if match(p):
297 yield p
297 yield p
298
298
299 def set_clean(self, f, parentfiledata):
299 def set_clean(self, f, parentfiledata):
300 """Mark a file normal and clean."""
300 """Mark a file normal and clean."""
301 # TODO: for now we just let libgit2 re-stat the file. We can
301 # TODO: for now we just let libgit2 re-stat the file. We can
302 # clearly do better.
302 # clearly do better.
303
303
304 def set_possibly_dirty(self, f):
304 def set_possibly_dirty(self, f):
305 """Mark a file normal, but possibly dirty."""
305 """Mark a file normal, but possibly dirty."""
306 # TODO: for now we just let libgit2 re-stat the file. We can
306 # TODO: for now we just let libgit2 re-stat the file. We can
307 # clearly do better.
307 # clearly do better.
308
308
309 def walk(self, match, subrepos, unknown, ignored, full=True):
309 def walk(self, match, subrepos, unknown, ignored, full=True):
310 # TODO: we need to use .status() and not iterate the index,
310 # TODO: we need to use .status() and not iterate the index,
311 # because the index doesn't force a re-walk and so `hg add` of
311 # because the index doesn't force a re-walk and so `hg add` of
312 # a new file without an intervening call to status will
312 # a new file without an intervening call to status will
313 # silently do nothing.
313 # silently do nothing.
314 r = {}
314 r = {}
315 cwd = self.getcwd()
315 cwd = self.getcwd()
316 for path, status in self.git.status().items():
316 for path, status in self.git.status().items():
317 if path.startswith('.hg/'):
317 if path.startswith('.hg/'):
318 continue
318 continue
319 path = pycompat.fsencode(path)
319 path = pycompat.fsencode(path)
320 if not match(path):
320 if not match(path):
321 continue
321 continue
322 # TODO construct the stat info from the status object?
322 # TODO construct the stat info from the status object?
323 try:
323 try:
324 s = os.stat(os.path.join(cwd, path))
324 s = os.stat(os.path.join(cwd, path))
325 except FileNotFoundError:
325 except FileNotFoundError:
326 continue
326 continue
327 r[path] = s
327 r[path] = s
328 return r
328 return r
329
329
330 def set_tracked(self, f, reset_copy=False):
330 def set_tracked(self, f, reset_copy=False):
331 # TODO: support copies and reset_copy=True
331 # TODO: support copies and reset_copy=True
332 uf = pycompat.fsdecode(f)
332 uf = pycompat.fsdecode(f)
333 if uf in self.git.index:
333 if uf in self.git.index:
334 return False
334 return False
335 index = self.git.index
335 index = self.git.index
336 index.read()
336 index.read()
337 index.add(uf)
337 index.add(uf)
338 index.write()
338 index.write()
339 return True
339 return True
340
340
341 def add(self, f):
341 def add(self, f):
342 index = self.git.index
342 index = self.git.index
343 index.read()
343 index.read()
344 index.add(pycompat.fsdecode(f))
344 index.add(pycompat.fsdecode(f))
345 index.write()
345 index.write()
346
346
347 def drop(self, f):
347 def drop(self, f):
348 index = self.git.index
348 index = self.git.index
349 index.read()
349 index.read()
350 fs = pycompat.fsdecode(f)
350 fs = pycompat.fsdecode(f)
351 if fs in index:
351 if fs in index:
352 index.remove(fs)
352 index.remove(fs)
353 index.write()
353 index.write()
354
354
355 def set_untracked(self, f):
355 def set_untracked(self, f):
356 index = self.git.index
356 index = self.git.index
357 index.read()
357 index.read()
358 fs = pycompat.fsdecode(f)
358 fs = pycompat.fsdecode(f)
359 if fs in index:
359 if fs in index:
360 index.remove(fs)
360 index.remove(fs)
361 index.write()
361 index.write()
362 return True
362 return True
363 return False
363 return False
364
364
365 def remove(self, f):
365 def remove(self, f):
366 index = self.git.index
366 index = self.git.index
367 index.read()
367 index.read()
368 index.remove(pycompat.fsdecode(f))
368 index.remove(pycompat.fsdecode(f))
369 index.write()
369 index.write()
370
370
371 def copied(self, path):
371 def copied(self, path):
372 # TODO: track copies?
372 # TODO: track copies?
373 return None
373 return None
374
374
375 def prefetch_parents(self):
375 def prefetch_parents(self):
376 # TODO
376 # TODO
377 pass
377 pass
378
378
379 def update_file(self, *args, **kwargs):
379 def update_file(self, *args, **kwargs):
380 # TODO
380 # TODO
381 pass
381 pass
382
382
383 @contextlib.contextmanager
383 @contextlib.contextmanager
384 def changing_parents(self, repo):
384 def changing_parents(self, repo):
385 # TODO: track this maybe?
385 # TODO: track this maybe?
386 yield
386 yield
387
387
388 def addparentchangecallback(self, category, callback):
388 def addparentchangecallback(self, category, callback):
389 # TODO: should this be added to the dirstate interface?
389 # TODO: should this be added to the dirstate interface?
390 self._plchangecallbacks[category] = callback
390 self._plchangecallbacks[category] = callback
391
391
392 def setbranch(self, branch, transaction=None):
392 def setbranch(self, branch, transaction=None):
393 raise error.Abort(
393 raise error.Abort(
394 b'git repos do not support branches. try using bookmarks'
394 b'git repos do not support branches. try using bookmarks'
395 )
395 )
General Comments 0
You need to be logged in to leave comments. Login now