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