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