##// END OF EJS Templates
windows: use abspath in the git extension...
marmoute -
r48431:16bae8ab default
parent child Browse files
Show More
@@ -1,343 +1,343 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 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import os
9 import os
10
10
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 from mercurial import (
13 from mercurial import (
14 commands,
14 commands,
15 error,
15 error,
16 extensions,
16 extensions,
17 localrepo,
17 localrepo,
18 pycompat,
18 pycompat,
19 registrar,
19 registrar,
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(object): # store.basicstore):
51 class gitstore(object): # 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.path = self.vfs.base
54 self.path = self.vfs.base
55 self.createmode = store._calcmode(self.vfs)
55 self.createmode = store._calcmode(self.vfs)
56 # above lines should go away in favor of:
56 # above lines should go away in favor of:
57 # super(gitstore, self).__init__(path, vfstype)
57 # super(gitstore, self).__init__(path, vfstype)
58
58
59 self.git = gitutil.get_pygit2().Repository(
59 self.git = gitutil.get_pygit2().Repository(
60 os.path.normpath(os.path.join(path, b'..', b'.git'))
60 os.path.normpath(os.path.join(path, b'..', b'.git'))
61 )
61 )
62 self._progress_factory = lambda *args, **kwargs: None
62 self._progress_factory = lambda *args, **kwargs: None
63 self._logfn = lambda x: None
63 self._logfn = lambda x: None
64
64
65 @util.propertycache
65 @util.propertycache
66 def _db(self):
66 def _db(self):
67 # We lazy-create the database because we want to thread a
67 # We lazy-create the database because we want to thread a
68 # progress callback down to the indexing process if it's
68 # progress callback down to the indexing process if it's
69 # required, and we don't have a ui handle in makestore().
69 # required, and we don't have a ui handle in makestore().
70 return index.get_index(self.git, self._logfn, self._progress_factory)
70 return index.get_index(self.git, self._logfn, self._progress_factory)
71
71
72 def join(self, f):
72 def join(self, f):
73 """Fake store.join method for git repositories.
73 """Fake store.join method for git repositories.
74
74
75 For the most part, store.join is used for @storecache
75 For the most part, store.join is used for @storecache
76 decorators to invalidate caches when various files
76 decorators to invalidate caches when various files
77 change. We'll map the ones we care about, and ignore the rest.
77 change. We'll map the ones we care about, and ignore the rest.
78 """
78 """
79 if f in (b'00changelog.i', b'00manifest.i'):
79 if f in (b'00changelog.i', b'00manifest.i'):
80 # This is close enough: in order for the changelog cache
80 # This is close enough: in order for the changelog cache
81 # to be invalidated, HEAD will have to change.
81 # to be invalidated, HEAD will have to change.
82 return os.path.join(self.path, b'HEAD')
82 return os.path.join(self.path, b'HEAD')
83 elif f == b'lock':
83 elif f == b'lock':
84 # TODO: we probably want to map this to a git lock, I
84 # TODO: we probably want to map this to a git lock, I
85 # suspect index.lock. We should figure out what the
85 # suspect index.lock. We should figure out what the
86 # most-alike file is in git-land. For now we're risking
86 # most-alike file is in git-land. For now we're risking
87 # bad concurrency errors if another git client is used.
87 # bad concurrency errors if another git client is used.
88 return os.path.join(self.path, b'hgit-bogus-lock')
88 return os.path.join(self.path, b'hgit-bogus-lock')
89 elif f in (b'obsstore', b'phaseroots', b'narrowspec', b'bookmarks'):
89 elif f in (b'obsstore', b'phaseroots', b'narrowspec', b'bookmarks'):
90 return os.path.join(self.path, b'..', b'.hg', f)
90 return os.path.join(self.path, b'..', b'.hg', f)
91 raise NotImplementedError(b'Need to pick file for %s.' % f)
91 raise NotImplementedError(b'Need to pick file for %s.' % f)
92
92
93 def changelog(self, trypending, concurrencychecker):
93 def changelog(self, trypending, concurrencychecker):
94 # TODO we don't have a plan for trypending in hg's git support yet
94 # TODO we don't have a plan for trypending in hg's git support yet
95 return gitlog.changelog(self.git, self._db)
95 return gitlog.changelog(self.git, self._db)
96
96
97 def manifestlog(self, repo, storenarrowmatch):
97 def manifestlog(self, repo, storenarrowmatch):
98 # TODO handle storenarrowmatch and figure out if we need the repo arg
98 # TODO handle storenarrowmatch and figure out if we need the repo arg
99 return gitlog.manifestlog(self.git, self._db)
99 return gitlog.manifestlog(self.git, self._db)
100
100
101 def invalidatecaches(self):
101 def invalidatecaches(self):
102 pass
102 pass
103
103
104 def write(self, tr=None):
104 def write(self, tr=None):
105 # normally this handles things like fncache writes, which we don't have
105 # normally this handles things like fncache writes, which we don't have
106 pass
106 pass
107
107
108
108
109 def _makestore(orig, requirements, storebasepath, vfstype):
109 def _makestore(orig, requirements, storebasepath, vfstype):
110 if b'git' in requirements:
110 if b'git' in requirements:
111 if not os.path.exists(os.path.join(storebasepath, b'..', b'.git')):
111 if not os.path.exists(os.path.join(storebasepath, b'..', b'.git')):
112 raise error.Abort(
112 raise error.Abort(
113 _(
113 _(
114 b'repository specified git format in '
114 b'repository specified git format in '
115 b'.hg/requires but has no .git directory'
115 b'.hg/requires but has no .git directory'
116 )
116 )
117 )
117 )
118 # Check for presence of pygit2 only here. The assumption is that we'll
118 # Check for presence of pygit2 only here. The assumption is that we'll
119 # run this code iff we'll later need pygit2.
119 # run this code iff we'll later need pygit2.
120 if gitutil.get_pygit2() is None:
120 if gitutil.get_pygit2() is None:
121 raise error.Abort(
121 raise error.Abort(
122 _(
122 _(
123 b'the git extension requires the Python '
123 b'the git extension requires the Python '
124 b'pygit2 library to be installed'
124 b'pygit2 library to be installed'
125 )
125 )
126 )
126 )
127
127
128 return gitstore(storebasepath, vfstype)
128 return gitstore(storebasepath, vfstype)
129 return orig(requirements, storebasepath, vfstype)
129 return orig(requirements, storebasepath, vfstype)
130
130
131
131
132 class gitfilestorage(object):
132 class gitfilestorage(object):
133 def file(self, path):
133 def file(self, path):
134 if path[0:1] == b'/':
134 if path[0:1] == b'/':
135 path = path[1:]
135 path = path[1:]
136 return gitlog.filelog(self.store.git, self.store._db, path)
136 return gitlog.filelog(self.store.git, self.store._db, path)
137
137
138
138
139 def _makefilestorage(orig, requirements, features, **kwargs):
139 def _makefilestorage(orig, requirements, features, **kwargs):
140 store = kwargs['store']
140 store = kwargs['store']
141 if isinstance(store, gitstore):
141 if isinstance(store, gitstore):
142 return gitfilestorage
142 return gitfilestorage
143 return orig(requirements, features, **kwargs)
143 return orig(requirements, features, **kwargs)
144
144
145
145
146 def _setupdothg(ui, path):
146 def _setupdothg(ui, path):
147 dothg = os.path.join(path, b'.hg')
147 dothg = os.path.join(path, b'.hg')
148 if os.path.exists(dothg):
148 if os.path.exists(dothg):
149 ui.warn(_(b'git repo already initialized for hg\n'))
149 ui.warn(_(b'git repo already initialized for hg\n'))
150 else:
150 else:
151 os.mkdir(os.path.join(path, b'.hg'))
151 os.mkdir(os.path.join(path, b'.hg'))
152 # TODO is it ok to extend .git/info/exclude like this?
152 # TODO is it ok to extend .git/info/exclude like this?
153 with open(
153 with open(
154 os.path.join(path, b'.git', b'info', b'exclude'), 'ab'
154 os.path.join(path, b'.git', b'info', b'exclude'), 'ab'
155 ) as exclude:
155 ) as exclude:
156 exclude.write(b'\n.hg\n')
156 exclude.write(b'\n.hg\n')
157 with open(os.path.join(dothg, b'requires'), 'wb') as f:
157 with open(os.path.join(dothg, b'requires'), 'wb') as f:
158 f.write(b'git\n')
158 f.write(b'git\n')
159
159
160
160
161 _BMS_PREFIX = 'refs/heads/'
161 _BMS_PREFIX = 'refs/heads/'
162
162
163
163
164 class gitbmstore(object):
164 class gitbmstore(object):
165 def __init__(self, gitrepo):
165 def __init__(self, gitrepo):
166 self.gitrepo = gitrepo
166 self.gitrepo = gitrepo
167 self._aclean = True
167 self._aclean = True
168 self._active = gitrepo.references['HEAD'] # git head, not mark
168 self._active = gitrepo.references['HEAD'] # git head, not mark
169
169
170 def __contains__(self, name):
170 def __contains__(self, name):
171 return (
171 return (
172 _BMS_PREFIX + pycompat.fsdecode(name)
172 _BMS_PREFIX + pycompat.fsdecode(name)
173 ) in self.gitrepo.references
173 ) in self.gitrepo.references
174
174
175 def __iter__(self):
175 def __iter__(self):
176 for r in self.gitrepo.listall_references():
176 for r in self.gitrepo.listall_references():
177 if r.startswith(_BMS_PREFIX):
177 if r.startswith(_BMS_PREFIX):
178 yield pycompat.fsencode(r[len(_BMS_PREFIX) :])
178 yield pycompat.fsencode(r[len(_BMS_PREFIX) :])
179
179
180 def __getitem__(self, k):
180 def __getitem__(self, k):
181 return (
181 return (
182 self.gitrepo.references[_BMS_PREFIX + pycompat.fsdecode(k)]
182 self.gitrepo.references[_BMS_PREFIX + pycompat.fsdecode(k)]
183 .peel()
183 .peel()
184 .id.raw
184 .id.raw
185 )
185 )
186
186
187 def get(self, k, default=None):
187 def get(self, k, default=None):
188 try:
188 try:
189 if k in self:
189 if k in self:
190 return self[k]
190 return self[k]
191 return default
191 return default
192 except gitutil.get_pygit2().InvalidSpecError:
192 except gitutil.get_pygit2().InvalidSpecError:
193 return default
193 return default
194
194
195 @property
195 @property
196 def active(self):
196 def active(self):
197 h = self.gitrepo.references['HEAD']
197 h = self.gitrepo.references['HEAD']
198 if not isinstance(h.target, str) or not h.target.startswith(
198 if not isinstance(h.target, str) or not h.target.startswith(
199 _BMS_PREFIX
199 _BMS_PREFIX
200 ):
200 ):
201 return None
201 return None
202 return pycompat.fsencode(h.target[len(_BMS_PREFIX) :])
202 return pycompat.fsencode(h.target[len(_BMS_PREFIX) :])
203
203
204 @active.setter
204 @active.setter
205 def active(self, mark):
205 def active(self, mark):
206 githead = mark is not None and (_BMS_PREFIX + mark) or None
206 githead = mark is not None and (_BMS_PREFIX + mark) or None
207 if githead is not None and githead not in self.gitrepo.references:
207 if githead is not None and githead not in self.gitrepo.references:
208 raise AssertionError(b'bookmark %s does not exist!' % mark)
208 raise AssertionError(b'bookmark %s does not exist!' % mark)
209
209
210 self._active = githead
210 self._active = githead
211 self._aclean = False
211 self._aclean = False
212
212
213 def _writeactive(self):
213 def _writeactive(self):
214 if self._aclean:
214 if self._aclean:
215 return
215 return
216 self.gitrepo.references.create('HEAD', self._active, True)
216 self.gitrepo.references.create('HEAD', self._active, True)
217 self._aclean = True
217 self._aclean = True
218
218
219 def names(self, node):
219 def names(self, node):
220 r = []
220 r = []
221 for ref in self.gitrepo.listall_references():
221 for ref in self.gitrepo.listall_references():
222 if not ref.startswith(_BMS_PREFIX):
222 if not ref.startswith(_BMS_PREFIX):
223 continue
223 continue
224 if self.gitrepo.references[ref].peel().id.raw != node:
224 if self.gitrepo.references[ref].peel().id.raw != node:
225 continue
225 continue
226 r.append(pycompat.fsencode(ref[len(_BMS_PREFIX) :]))
226 r.append(pycompat.fsencode(ref[len(_BMS_PREFIX) :]))
227 return r
227 return r
228
228
229 # Cleanup opportunity: this is *identical* to core's bookmarks store.
229 # Cleanup opportunity: this is *identical* to core's bookmarks store.
230 def expandname(self, bname):
230 def expandname(self, bname):
231 if bname == b'.':
231 if bname == b'.':
232 if self.active:
232 if self.active:
233 return self.active
233 return self.active
234 raise error.RepoLookupError(_(b"no active bookmark"))
234 raise error.RepoLookupError(_(b"no active bookmark"))
235 return bname
235 return bname
236
236
237 def applychanges(self, repo, tr, changes):
237 def applychanges(self, repo, tr, changes):
238 """Apply a list of changes to bookmarks"""
238 """Apply a list of changes to bookmarks"""
239 # TODO: this should respect transactions, but that's going to
239 # TODO: this should respect transactions, but that's going to
240 # require enlarging the gitbmstore to know how to do in-memory
240 # require enlarging the gitbmstore to know how to do in-memory
241 # temporary writes and read those back prior to transaction
241 # temporary writes and read those back prior to transaction
242 # finalization.
242 # finalization.
243 for name, node in changes:
243 for name, node in changes:
244 if node is None:
244 if node is None:
245 self.gitrepo.references.delete(
245 self.gitrepo.references.delete(
246 _BMS_PREFIX + pycompat.fsdecode(name)
246 _BMS_PREFIX + pycompat.fsdecode(name)
247 )
247 )
248 else:
248 else:
249 self.gitrepo.references.create(
249 self.gitrepo.references.create(
250 _BMS_PREFIX + pycompat.fsdecode(name),
250 _BMS_PREFIX + pycompat.fsdecode(name),
251 gitutil.togitnode(node),
251 gitutil.togitnode(node),
252 force=True,
252 force=True,
253 )
253 )
254
254
255 def checkconflict(self, mark, force=False, target=None):
255 def checkconflict(self, mark, force=False, target=None):
256 githead = _BMS_PREFIX + mark
256 githead = _BMS_PREFIX + mark
257 cur = self.gitrepo.references['HEAD']
257 cur = self.gitrepo.references['HEAD']
258 if githead in self.gitrepo.references and not force:
258 if githead in self.gitrepo.references and not force:
259 if target:
259 if target:
260 if self.gitrepo.references[githead] == target and target == cur:
260 if self.gitrepo.references[githead] == target and target == cur:
261 # re-activating a bookmark
261 # re-activating a bookmark
262 return []
262 return []
263 # moving a bookmark - forward?
263 # moving a bookmark - forward?
264 raise NotImplementedError
264 raise NotImplementedError
265 raise error.Abort(
265 raise error.Abort(
266 _(b"bookmark '%s' already exists (use -f to force)") % mark
266 _(b"bookmark '%s' already exists (use -f to force)") % mark
267 )
267 )
268 if len(mark) > 3 and not force:
268 if len(mark) > 3 and not force:
269 try:
269 try:
270 shadowhash = scmutil.isrevsymbol(self._repo, mark)
270 shadowhash = scmutil.isrevsymbol(self._repo, mark)
271 except error.LookupError: # ambiguous identifier
271 except error.LookupError: # ambiguous identifier
272 shadowhash = False
272 shadowhash = False
273 if shadowhash:
273 if shadowhash:
274 self._repo.ui.warn(
274 self._repo.ui.warn(
275 _(
275 _(
276 b"bookmark %s matches a changeset hash\n"
276 b"bookmark %s matches a changeset hash\n"
277 b"(did you leave a -r out of an 'hg bookmark' "
277 b"(did you leave a -r out of an 'hg bookmark' "
278 b"command?)\n"
278 b"command?)\n"
279 )
279 )
280 % mark
280 % mark
281 )
281 )
282 return []
282 return []
283
283
284
284
285 def init(orig, ui, dest=b'.', **opts):
285 def init(orig, ui, dest=b'.', **opts):
286 if opts.get('git', False):
286 if opts.get('git', False):
287 path = os.path.abspath(dest)
287 path = util.abspath(dest)
288 # TODO: walk up looking for the git repo
288 # TODO: walk up looking for the git repo
289 _setupdothg(ui, path)
289 _setupdothg(ui, path)
290 return 0
290 return 0
291 return orig(ui, dest=dest, **opts)
291 return orig(ui, dest=dest, **opts)
292
292
293
293
294 def reposetup(ui, repo):
294 def reposetup(ui, repo):
295 if repo.local() and isinstance(repo.store, gitstore):
295 if repo.local() and isinstance(repo.store, gitstore):
296 orig = repo.__class__
296 orig = repo.__class__
297 repo.store._progress_factory = repo.ui.makeprogress
297 repo.store._progress_factory = repo.ui.makeprogress
298 if ui.configbool(b'git', b'log-index-cache-miss'):
298 if ui.configbool(b'git', b'log-index-cache-miss'):
299 repo.store._logfn = repo.ui.warn
299 repo.store._logfn = repo.ui.warn
300
300
301 class gitlocalrepo(orig):
301 class gitlocalrepo(orig):
302 def _makedirstate(self):
302 def _makedirstate(self):
303 # TODO narrow support here
303 # TODO narrow support here
304 return dirstate.gitdirstate(
304 return dirstate.gitdirstate(
305 self.ui, self.vfs.base, self.store.git
305 self.ui, self.vfs.base, self.store.git
306 )
306 )
307
307
308 def commit(self, *args, **kwargs):
308 def commit(self, *args, **kwargs):
309 ret = orig.commit(self, *args, **kwargs)
309 ret = orig.commit(self, *args, **kwargs)
310 if ret is None:
310 if ret is None:
311 # there was nothing to commit, so we should skip
311 # there was nothing to commit, so we should skip
312 # the index fixup logic we'd otherwise do.
312 # the index fixup logic we'd otherwise do.
313 return None
313 return None
314 tid = self.store.git[gitutil.togitnode(ret)].tree.id
314 tid = self.store.git[gitutil.togitnode(ret)].tree.id
315 # DANGER! This will flush any writes staged to the
315 # DANGER! This will flush any writes staged to the
316 # index in Git, but we're sidestepping the index in a
316 # index in Git, but we're sidestepping the index in a
317 # way that confuses git when we commit. Alas.
317 # way that confuses git when we commit. Alas.
318 self.store.git.index.read_tree(tid)
318 self.store.git.index.read_tree(tid)
319 self.store.git.index.write()
319 self.store.git.index.write()
320 return ret
320 return ret
321
321
322 @property
322 @property
323 def _bookmarks(self):
323 def _bookmarks(self):
324 return gitbmstore(self.store.git)
324 return gitbmstore(self.store.git)
325
325
326 repo.__class__ = gitlocalrepo
326 repo.__class__ = gitlocalrepo
327 return repo
327 return repo
328
328
329
329
330 def _featuresetup(ui, supported):
330 def _featuresetup(ui, supported):
331 # don't die on seeing a repo with the git requirement
331 # don't die on seeing a repo with the git requirement
332 supported |= {b'git'}
332 supported |= {b'git'}
333
333
334
334
335 def extsetup(ui):
335 def extsetup(ui):
336 extensions.wrapfunction(localrepo, b'makestore', _makestore)
336 extensions.wrapfunction(localrepo, b'makestore', _makestore)
337 extensions.wrapfunction(localrepo, b'makefilestorage', _makefilestorage)
337 extensions.wrapfunction(localrepo, b'makefilestorage', _makefilestorage)
338 # Inject --git flag for `hg init`
338 # Inject --git flag for `hg init`
339 entry = extensions.wrapcommand(commands.table, b'init', init)
339 entry = extensions.wrapcommand(commands.table, b'init', init)
340 entry[1].extend(
340 entry[1].extend(
341 [(b'', b'git', None, b'setup up a git repository instead of hg')]
341 [(b'', b'git', None, b'setup up a git repository instead of hg')]
342 )
342 )
343 localrepo.featuresetupfuncs.add(_featuresetup)
343 localrepo.featuresetupfuncs.add(_featuresetup)
General Comments 0
You need to be logged in to leave comments. Login now