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