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