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