##// END OF EJS Templates
git: adapt to some recent dirstate API changes...
Matt Harbison -
r49968:20d151e4 default
parent child Browse files
Show More
@@ -1,343 +1,350 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
7
8 import os
8 import os
9
9
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11
11
12 from mercurial import (
12 from mercurial import (
13 commands,
13 commands,
14 error,
14 error,
15 extensions,
15 extensions,
16 localrepo,
16 localrepo,
17 pycompat,
17 pycompat,
18 registrar,
18 registrar,
19 requirements as requirementsmod,
19 scmutil,
20 scmutil,
20 store,
21 store,
21 util,
22 util,
22 )
23 )
23
24
24 from . import (
25 from . import (
25 dirstate,
26 dirstate,
26 gitlog,
27 gitlog,
27 gitutil,
28 gitutil,
28 index,
29 index,
29 )
30 )
30
31
31 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
32 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
32 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
33 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
33 # be specifying the version(s) of Mercurial they are tested with, or
34 # be specifying the version(s) of Mercurial they are tested with, or
34 # leave the attribute unspecified.
35 # leave the attribute unspecified.
35 testedwith = b'ships-with-hg-core'
36 testedwith = b'ships-with-hg-core'
36
37
37 configtable = {}
38 configtable = {}
38 configitem = registrar.configitem(configtable)
39 configitem = registrar.configitem(configtable)
39 # git.log-index-cache-miss: internal knob for testing
40 # git.log-index-cache-miss: internal knob for testing
40 configitem(
41 configitem(
41 b"git",
42 b"git",
42 b"log-index-cache-miss",
43 b"log-index-cache-miss",
43 default=False,
44 default=False,
44 )
45 )
45
46
46 getversion = gitutil.pygit2_version
47 getversion = gitutil.pygit2_version
47
48
48
49
49 # TODO: extract an interface for this in core
50 # TODO: extract an interface for this in core
50 class gitstore: # store.basicstore):
51 class gitstore: # store.basicstore):
51 def __init__(self, path, vfstype):
52 def __init__(self, path, vfstype):
52 self.vfs = vfstype(path)
53 self.vfs = vfstype(path)
53 self.opener = self.vfs
54 self.opener = self.vfs
54 self.path = self.vfs.base
55 self.path = self.vfs.base
55 self.createmode = store._calcmode(self.vfs)
56 self.createmode = store._calcmode(self.vfs)
56 # above lines should go away in favor of:
57 # above lines should go away in favor of:
57 # super(gitstore, self).__init__(path, vfstype)
58 # super(gitstore, self).__init__(path, vfstype)
58
59
59 self.git = gitutil.get_pygit2().Repository(
60 self.git = gitutil.get_pygit2().Repository(
60 os.path.normpath(os.path.join(path, b'..', b'.git'))
61 os.path.normpath(os.path.join(path, b'..', b'.git'))
61 )
62 )
62 self._progress_factory = lambda *args, **kwargs: None
63 self._progress_factory = lambda *args, **kwargs: None
63 self._logfn = lambda x: None
64 self._logfn = lambda x: None
64
65
65 @util.propertycache
66 @util.propertycache
66 def _db(self):
67 def _db(self):
67 # We lazy-create the database because we want to thread a
68 # We lazy-create the database because we want to thread a
68 # progress callback down to the indexing process if it's
69 # progress callback down to the indexing process if it's
69 # required, and we don't have a ui handle in makestore().
70 # required, and we don't have a ui handle in makestore().
70 return index.get_index(self.git, self._logfn, self._progress_factory)
71 return index.get_index(self.git, self._logfn, self._progress_factory)
71
72
72 def join(self, f):
73 def join(self, f):
73 """Fake store.join method for git repositories.
74 """Fake store.join method for git repositories.
74
75
75 For the most part, store.join is used for @storecache
76 For the most part, store.join is used for @storecache
76 decorators to invalidate caches when various files
77 decorators to invalidate caches when various files
77 change. We'll map the ones we care about, and ignore the rest.
78 change. We'll map the ones we care about, and ignore the rest.
78 """
79 """
79 if f in (b'00changelog.i', b'00manifest.i'):
80 if f in (b'00changelog.i', b'00manifest.i'):
80 # This is close enough: in order for the changelog cache
81 # This is close enough: in order for the changelog cache
81 # to be invalidated, HEAD will have to change.
82 # to be invalidated, HEAD will have to change.
82 return os.path.join(self.path, b'HEAD')
83 return os.path.join(self.path, b'HEAD')
83 elif f == b'lock':
84 elif f == b'lock':
84 # TODO: we probably want to map this to a git lock, I
85 # TODO: we probably want to map this to a git lock, I
85 # suspect index.lock. We should figure out what the
86 # suspect index.lock. We should figure out what the
86 # most-alike file is in git-land. For now we're risking
87 # most-alike file is in git-land. For now we're risking
87 # bad concurrency errors if another git client is used.
88 # bad concurrency errors if another git client is used.
88 return os.path.join(self.path, b'hgit-bogus-lock')
89 return os.path.join(self.path, b'hgit-bogus-lock')
89 elif f in (b'obsstore', b'phaseroots', b'narrowspec', b'bookmarks'):
90 elif f in (b'obsstore', b'phaseroots', b'narrowspec', b'bookmarks'):
90 return os.path.join(self.path, b'..', b'.hg', f)
91 return os.path.join(self.path, b'..', b'.hg', f)
91 raise NotImplementedError(b'Need to pick file for %s.' % f)
92 raise NotImplementedError(b'Need to pick file for %s.' % f)
92
93
93 def changelog(self, trypending, concurrencychecker):
94 def changelog(self, trypending, concurrencychecker):
94 # TODO we don't have a plan for trypending in hg's git support yet
95 # TODO we don't have a plan for trypending in hg's git support yet
95 return gitlog.changelog(self.git, self._db)
96 return gitlog.changelog(self.git, self._db)
96
97
97 def manifestlog(self, repo, storenarrowmatch):
98 def manifestlog(self, repo, storenarrowmatch):
98 # TODO handle storenarrowmatch and figure out if we need the repo arg
99 # TODO handle storenarrowmatch and figure out if we need the repo arg
99 return gitlog.manifestlog(self.git, self._db)
100 return gitlog.manifestlog(self.git, self._db)
100
101
101 def invalidatecaches(self):
102 def invalidatecaches(self):
102 pass
103 pass
103
104
104 def write(self, tr=None):
105 def write(self, tr=None):
105 # normally this handles things like fncache writes, which we don't have
106 # normally this handles things like fncache writes, which we don't have
106 pass
107 pass
107
108
108
109
109 def _makestore(orig, requirements, storebasepath, vfstype):
110 def _makestore(orig, requirements, storebasepath, vfstype):
110 if b'git' in requirements:
111 if b'git' in requirements:
111 if not os.path.exists(os.path.join(storebasepath, b'..', b'.git')):
112 if not os.path.exists(os.path.join(storebasepath, b'..', b'.git')):
112 raise error.Abort(
113 raise error.Abort(
113 _(
114 _(
114 b'repository specified git format in '
115 b'repository specified git format in '
115 b'.hg/requires but has no .git directory'
116 b'.hg/requires but has no .git directory'
116 )
117 )
117 )
118 )
118 # Check for presence of pygit2 only here. The assumption is that we'll
119 # Check for presence of pygit2 only here. The assumption is that we'll
119 # run this code iff we'll later need pygit2.
120 # run this code iff we'll later need pygit2.
120 if gitutil.get_pygit2() is None:
121 if gitutil.get_pygit2() is None:
121 raise error.Abort(
122 raise error.Abort(
122 _(
123 _(
123 b'the git extension requires the Python '
124 b'the git extension requires the Python '
124 b'pygit2 library to be installed'
125 b'pygit2 library to be installed'
125 )
126 )
126 )
127 )
127
128
128 return gitstore(storebasepath, vfstype)
129 return gitstore(storebasepath, vfstype)
129 return orig(requirements, storebasepath, vfstype)
130 return orig(requirements, storebasepath, vfstype)
130
131
131
132
132 class gitfilestorage:
133 class gitfilestorage:
133 def file(self, path):
134 def file(self, path):
134 if path[0:1] == b'/':
135 if path[0:1] == b'/':
135 path = path[1:]
136 path = path[1:]
136 return gitlog.filelog(self.store.git, self.store._db, path)
137 return gitlog.filelog(self.store.git, self.store._db, path)
137
138
138
139
139 def _makefilestorage(orig, requirements, features, **kwargs):
140 def _makefilestorage(orig, requirements, features, **kwargs):
140 store = kwargs['store']
141 store = kwargs['store']
141 if isinstance(store, gitstore):
142 if isinstance(store, gitstore):
142 return gitfilestorage
143 return gitfilestorage
143 return orig(requirements, features, **kwargs)
144 return orig(requirements, features, **kwargs)
144
145
145
146
146 def _setupdothg(ui, path):
147 def _setupdothg(ui, path):
147 dothg = os.path.join(path, b'.hg')
148 dothg = os.path.join(path, b'.hg')
148 if os.path.exists(dothg):
149 if os.path.exists(dothg):
149 ui.warn(_(b'git repo already initialized for hg\n'))
150 ui.warn(_(b'git repo already initialized for hg\n'))
150 else:
151 else:
151 os.mkdir(os.path.join(path, b'.hg'))
152 os.mkdir(os.path.join(path, b'.hg'))
152 # TODO is it ok to extend .git/info/exclude like this?
153 # TODO is it ok to extend .git/info/exclude like this?
153 with open(
154 with open(
154 os.path.join(path, b'.git', b'info', b'exclude'), 'ab'
155 os.path.join(path, b'.git', b'info', b'exclude'), 'ab'
155 ) as exclude:
156 ) as exclude:
156 exclude.write(b'\n.hg\n')
157 exclude.write(b'\n.hg\n')
157 with open(os.path.join(dothg, b'requires'), 'wb') as f:
158 with open(os.path.join(dothg, b'requires'), 'wb') as f:
158 f.write(b'git\n')
159 f.write(b'git\n')
159
160
160
161
161 _BMS_PREFIX = 'refs/heads/'
162 _BMS_PREFIX = 'refs/heads/'
162
163
163
164
164 class gitbmstore:
165 class gitbmstore:
165 def __init__(self, gitrepo):
166 def __init__(self, gitrepo):
166 self.gitrepo = gitrepo
167 self.gitrepo = gitrepo
167 self._aclean = True
168 self._aclean = True
168 self._active = gitrepo.references['HEAD'] # git head, not mark
169 self._active = gitrepo.references['HEAD'] # git head, not mark
169
170
170 def __contains__(self, name):
171 def __contains__(self, name):
171 return (
172 return (
172 _BMS_PREFIX + pycompat.fsdecode(name)
173 _BMS_PREFIX + pycompat.fsdecode(name)
173 ) in self.gitrepo.references
174 ) in self.gitrepo.references
174
175
175 def __iter__(self):
176 def __iter__(self):
176 for r in self.gitrepo.listall_references():
177 for r in self.gitrepo.listall_references():
177 if r.startswith(_BMS_PREFIX):
178 if r.startswith(_BMS_PREFIX):
178 yield pycompat.fsencode(r[len(_BMS_PREFIX) :])
179 yield pycompat.fsencode(r[len(_BMS_PREFIX) :])
179
180
180 def __getitem__(self, k):
181 def __getitem__(self, k):
181 return (
182 return (
182 self.gitrepo.references[_BMS_PREFIX + pycompat.fsdecode(k)]
183 self.gitrepo.references[_BMS_PREFIX + pycompat.fsdecode(k)]
183 .peel()
184 .peel()
184 .id.raw
185 .id.raw
185 )
186 )
186
187
187 def get(self, k, default=None):
188 def get(self, k, default=None):
188 try:
189 try:
189 if k in self:
190 if k in self:
190 return self[k]
191 return self[k]
191 return default
192 return default
192 except gitutil.get_pygit2().InvalidSpecError:
193 except gitutil.get_pygit2().InvalidSpecError:
193 return default
194 return default
194
195
195 @property
196 @property
196 def active(self):
197 def active(self):
197 h = self.gitrepo.references['HEAD']
198 h = self.gitrepo.references['HEAD']
198 if not isinstance(h.target, str) or not h.target.startswith(
199 if not isinstance(h.target, str) or not h.target.startswith(
199 _BMS_PREFIX
200 _BMS_PREFIX
200 ):
201 ):
201 return None
202 return None
202 return pycompat.fsencode(h.target[len(_BMS_PREFIX) :])
203 return pycompat.fsencode(h.target[len(_BMS_PREFIX) :])
203
204
204 @active.setter
205 @active.setter
205 def active(self, mark):
206 def active(self, mark):
206 githead = mark is not None and (_BMS_PREFIX + mark) or None
207 githead = mark is not None and (_BMS_PREFIX + mark) or None
207 if githead is not None and githead not in self.gitrepo.references:
208 if githead is not None and githead not in self.gitrepo.references:
208 raise AssertionError(b'bookmark %s does not exist!' % mark)
209 raise AssertionError(b'bookmark %s does not exist!' % mark)
209
210
210 self._active = githead
211 self._active = githead
211 self._aclean = False
212 self._aclean = False
212
213
213 def _writeactive(self):
214 def _writeactive(self):
214 if self._aclean:
215 if self._aclean:
215 return
216 return
216 self.gitrepo.references.create('HEAD', self._active, True)
217 self.gitrepo.references.create('HEAD', self._active, True)
217 self._aclean = True
218 self._aclean = True
218
219
219 def names(self, node):
220 def names(self, node):
220 r = []
221 r = []
221 for ref in self.gitrepo.listall_references():
222 for ref in self.gitrepo.listall_references():
222 if not ref.startswith(_BMS_PREFIX):
223 if not ref.startswith(_BMS_PREFIX):
223 continue
224 continue
224 if self.gitrepo.references[ref].peel().id.raw != node:
225 if self.gitrepo.references[ref].peel().id.raw != node:
225 continue
226 continue
226 r.append(pycompat.fsencode(ref[len(_BMS_PREFIX) :]))
227 r.append(pycompat.fsencode(ref[len(_BMS_PREFIX) :]))
227 return r
228 return r
228
229
229 # Cleanup opportunity: this is *identical* to core's bookmarks store.
230 # Cleanup opportunity: this is *identical* to core's bookmarks store.
230 def expandname(self, bname):
231 def expandname(self, bname):
231 if bname == b'.':
232 if bname == b'.':
232 if self.active:
233 if self.active:
233 return self.active
234 return self.active
234 raise error.RepoLookupError(_(b"no active bookmark"))
235 raise error.RepoLookupError(_(b"no active bookmark"))
235 return bname
236 return bname
236
237
237 def applychanges(self, repo, tr, changes):
238 def applychanges(self, repo, tr, changes):
238 """Apply a list of changes to bookmarks"""
239 """Apply a list of changes to bookmarks"""
239 # TODO: this should respect transactions, but that's going to
240 # TODO: this should respect transactions, but that's going to
240 # require enlarging the gitbmstore to know how to do in-memory
241 # require enlarging the gitbmstore to know how to do in-memory
241 # temporary writes and read those back prior to transaction
242 # temporary writes and read those back prior to transaction
242 # finalization.
243 # finalization.
243 for name, node in changes:
244 for name, node in changes:
244 if node is None:
245 if node is None:
245 self.gitrepo.references.delete(
246 self.gitrepo.references.delete(
246 _BMS_PREFIX + pycompat.fsdecode(name)
247 _BMS_PREFIX + pycompat.fsdecode(name)
247 )
248 )
248 else:
249 else:
249 self.gitrepo.references.create(
250 self.gitrepo.references.create(
250 _BMS_PREFIX + pycompat.fsdecode(name),
251 _BMS_PREFIX + pycompat.fsdecode(name),
251 gitutil.togitnode(node),
252 gitutil.togitnode(node),
252 force=True,
253 force=True,
253 )
254 )
254
255
255 def checkconflict(self, mark, force=False, target=None):
256 def checkconflict(self, mark, force=False, target=None):
256 githead = _BMS_PREFIX + mark
257 githead = _BMS_PREFIX + mark
257 cur = self.gitrepo.references['HEAD']
258 cur = self.gitrepo.references['HEAD']
258 if githead in self.gitrepo.references and not force:
259 if githead in self.gitrepo.references and not force:
259 if target:
260 if target:
260 if self.gitrepo.references[githead] == target and target == cur:
261 if self.gitrepo.references[githead] == target and target == cur:
261 # re-activating a bookmark
262 # re-activating a bookmark
262 return []
263 return []
263 # moving a bookmark - forward?
264 # moving a bookmark - forward?
264 raise NotImplementedError
265 raise NotImplementedError
265 raise error.Abort(
266 raise error.Abort(
266 _(b"bookmark '%s' already exists (use -f to force)") % mark
267 _(b"bookmark '%s' already exists (use -f to force)") % mark
267 )
268 )
268 if len(mark) > 3 and not force:
269 if len(mark) > 3 and not force:
269 try:
270 try:
270 shadowhash = scmutil.isrevsymbol(self._repo, mark)
271 shadowhash = scmutil.isrevsymbol(self._repo, mark)
271 except error.LookupError: # ambiguous identifier
272 except error.LookupError: # ambiguous identifier
272 shadowhash = False
273 shadowhash = False
273 if shadowhash:
274 if shadowhash:
274 self._repo.ui.warn(
275 self._repo.ui.warn(
275 _(
276 _(
276 b"bookmark %s matches a changeset hash\n"
277 b"bookmark %s matches a changeset hash\n"
277 b"(did you leave a -r out of an 'hg bookmark' "
278 b"(did you leave a -r out of an 'hg bookmark' "
278 b"command?)\n"
279 b"command?)\n"
279 )
280 )
280 % mark
281 % mark
281 )
282 )
282 return []
283 return []
283
284
284
285
285 def init(orig, ui, dest=b'.', **opts):
286 def init(orig, ui, dest=b'.', **opts):
286 if opts.get('git', False):
287 if opts.get('git', False):
287 path = util.abspath(dest)
288 path = util.abspath(dest)
288 # TODO: walk up looking for the git repo
289 # TODO: walk up looking for the git repo
289 _setupdothg(ui, path)
290 _setupdothg(ui, path)
290 return 0
291 return 0
291 return orig(ui, dest=dest, **opts)
292 return orig(ui, dest=dest, **opts)
292
293
293
294
294 def reposetup(ui, repo):
295 def reposetup(ui, repo):
295 if repo.local() and isinstance(repo.store, gitstore):
296 if repo.local() and isinstance(repo.store, gitstore):
296 orig = repo.__class__
297 orig = repo.__class__
297 repo.store._progress_factory = repo.ui.makeprogress
298 repo.store._progress_factory = repo.ui.makeprogress
298 if ui.configbool(b'git', b'log-index-cache-miss'):
299 if ui.configbool(b'git', b'log-index-cache-miss'):
299 repo.store._logfn = repo.ui.warn
300 repo.store._logfn = repo.ui.warn
300
301
301 class gitlocalrepo(orig):
302 class gitlocalrepo(orig):
302 def _makedirstate(self):
303 def _makedirstate(self):
304 v2_req = requirementsmod.DIRSTATE_V2_REQUIREMENT
305 use_dirstate_v2 = v2_req in self.requirements
306
303 # TODO narrow support here
307 # TODO narrow support here
304 return dirstate.gitdirstate(
308 return dirstate.gitdirstate(
305 self.ui, self.vfs.base, self.store.git
309 self.ui,
310 self.vfs,
311 self.store.git,
312 use_dirstate_v2,
306 )
313 )
307
314
308 def commit(self, *args, **kwargs):
315 def commit(self, *args, **kwargs):
309 ret = orig.commit(self, *args, **kwargs)
316 ret = orig.commit(self, *args, **kwargs)
310 if ret is None:
317 if ret is None:
311 # there was nothing to commit, so we should skip
318 # there was nothing to commit, so we should skip
312 # the index fixup logic we'd otherwise do.
319 # the index fixup logic we'd otherwise do.
313 return None
320 return None
314 tid = self.store.git[gitutil.togitnode(ret)].tree.id
321 tid = self.store.git[gitutil.togitnode(ret)].tree.id
315 # DANGER! This will flush any writes staged to the
322 # DANGER! This will flush any writes staged to the
316 # index in Git, but we're sidestepping the index in a
323 # index in Git, but we're sidestepping the index in a
317 # way that confuses git when we commit. Alas.
324 # way that confuses git when we commit. Alas.
318 self.store.git.index.read_tree(tid)
325 self.store.git.index.read_tree(tid)
319 self.store.git.index.write()
326 self.store.git.index.write()
320 return ret
327 return ret
321
328
322 @property
329 @property
323 def _bookmarks(self):
330 def _bookmarks(self):
324 return gitbmstore(self.store.git)
331 return gitbmstore(self.store.git)
325
332
326 repo.__class__ = gitlocalrepo
333 repo.__class__ = gitlocalrepo
327 return repo
334 return repo
328
335
329
336
330 def _featuresetup(ui, supported):
337 def _featuresetup(ui, supported):
331 # don't die on seeing a repo with the git requirement
338 # don't die on seeing a repo with the git requirement
332 supported |= {b'git'}
339 supported |= {b'git'}
333
340
334
341
335 def extsetup(ui):
342 def extsetup(ui):
336 extensions.wrapfunction(localrepo, b'makestore', _makestore)
343 extensions.wrapfunction(localrepo, b'makestore', _makestore)
337 extensions.wrapfunction(localrepo, b'makefilestorage', _makefilestorage)
344 extensions.wrapfunction(localrepo, b'makefilestorage', _makefilestorage)
338 # Inject --git flag for `hg init`
345 # Inject --git flag for `hg init`
339 entry = extensions.wrapcommand(commands.table, b'init', init)
346 entry = extensions.wrapcommand(commands.table, b'init', init)
340 entry[1].extend(
347 entry[1].extend(
341 [(b'', b'git', None, b'setup up a git repository instead of hg')]
348 [(b'', b'git', None, b'setup up a git repository instead of hg')]
342 )
349 )
343 localrepo.featuresetupfuncs.add(_featuresetup)
350 localrepo.featuresetupfuncs.add(_featuresetup)
@@ -1,367 +1,404 b''
1 import contextlib
1 import contextlib
2 import errno
2 import errno
3 import os
3 import os
4
4
5 from mercurial.node import sha1nodeconstants
5 from mercurial.node import sha1nodeconstants
6 from mercurial import (
6 from mercurial import (
7 dirstatemap,
7 error,
8 error,
8 extensions,
9 extensions,
9 match as matchmod,
10 match as matchmod,
10 pycompat,
11 pycompat,
11 scmutil,
12 scmutil,
12 util,
13 util,
13 )
14 )
15 from mercurial.dirstateutils import (
16 timestamp,
17 )
14 from mercurial.interfaces import (
18 from mercurial.interfaces import (
15 dirstate as intdirstate,
19 dirstate as intdirstate,
16 util as interfaceutil,
20 util as interfaceutil,
17 )
21 )
18
22
19 from . import gitutil
23 from . import gitutil
20
24
25
26 DirstateItem = dirstatemap.DirstateItem
27 propertycache = util.propertycache
21 pygit2 = gitutil.get_pygit2()
28 pygit2 = gitutil.get_pygit2()
22
29
23
30
24 def readpatternfile(orig, filepath, warn, sourceinfo=False):
31 def readpatternfile(orig, filepath, warn, sourceinfo=False):
25 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
32 if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
26 return orig(filepath, warn, sourceinfo=False)
33 return orig(filepath, warn, sourceinfo=False)
27 result = []
34 result = []
28 warnings = []
35 warnings = []
29 with open(filepath, b'rb') as fp:
36 with open(filepath, b'rb') as fp:
30 for l in fp:
37 for l in fp:
31 l = l.strip()
38 l = l.strip()
32 if not l or l.startswith(b'#'):
39 if not l or l.startswith(b'#'):
33 continue
40 continue
34 if l.startswith(b'!'):
41 if l.startswith(b'!'):
35 warnings.append(b'unsupported ignore pattern %s' % l)
42 warnings.append(b'unsupported ignore pattern %s' % l)
36 continue
43 continue
37 if l.startswith(b'/'):
44 if l.startswith(b'/'):
38 result.append(b'rootglob:' + l[1:])
45 result.append(b'rootglob:' + l[1:])
39 else:
46 else:
40 result.append(b'relglob:' + l)
47 result.append(b'relglob:' + l)
41 return result, warnings
48 return result, warnings
42
49
43
50
44 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
51 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
45
52
46
53
47 _STATUS_MAP = {}
54 _STATUS_MAP = {}
48 if pygit2:
55 if pygit2:
49 _STATUS_MAP = {
56 _STATUS_MAP = {
50 pygit2.GIT_STATUS_CONFLICTED: b'm',
57 pygit2.GIT_STATUS_CONFLICTED: b'm',
51 pygit2.GIT_STATUS_CURRENT: b'n',
58 pygit2.GIT_STATUS_CURRENT: b'n',
52 pygit2.GIT_STATUS_IGNORED: b'?',
59 pygit2.GIT_STATUS_IGNORED: b'?',
53 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
60 pygit2.GIT_STATUS_INDEX_DELETED: b'r',
54 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
61 pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
55 pygit2.GIT_STATUS_INDEX_NEW: b'a',
62 pygit2.GIT_STATUS_INDEX_NEW: b'a',
56 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
63 pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
57 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
64 pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
58 pygit2.GIT_STATUS_WT_DELETED: b'r',
65 pygit2.GIT_STATUS_WT_DELETED: b'r',
59 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
66 pygit2.GIT_STATUS_WT_MODIFIED: b'n',
60 pygit2.GIT_STATUS_WT_NEW: b'?',
67 pygit2.GIT_STATUS_WT_NEW: b'?',
61 pygit2.GIT_STATUS_WT_RENAMED: b'a',
68 pygit2.GIT_STATUS_WT_RENAMED: b'a',
62 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
69 pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
63 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
70 pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
64 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: b'm',
71 pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: b'm',
65 }
72 }
66
73
67
74
68 @interfaceutil.implementer(intdirstate.idirstate)
75 @interfaceutil.implementer(intdirstate.idirstate)
69 class gitdirstate:
76 class gitdirstate:
70 def __init__(self, ui, root, gitrepo):
77 def __init__(self, ui, vfs, gitrepo, use_dirstate_v2):
71 self._ui = ui
78 self._ui = ui
72 self._root = os.path.dirname(root)
79 self._root = os.path.dirname(vfs.base)
80 self._opener = vfs
73 self.git = gitrepo
81 self.git = gitrepo
74 self._plchangecallbacks = {}
82 self._plchangecallbacks = {}
75 # TODO: context.poststatusfixup is bad and uses this attribute
83 # TODO: context.poststatusfixup is bad and uses this attribute
76 self._dirty = False
84 self._dirty = False
85 self._mapcls = dirstatemap.dirstatemap
86 self._use_dirstate_v2 = use_dirstate_v2
87
88 @propertycache
89 def _map(self):
90 """Return the dirstate contents (see documentation for dirstatemap)."""
91 self._map = self._mapcls(
92 self._ui,
93 self._opener,
94 self._root,
95 sha1nodeconstants,
96 self._use_dirstate_v2,
97 )
98 return self._map
77
99
78 def p1(self):
100 def p1(self):
79 try:
101 try:
80 return self.git.head.peel().id.raw
102 return self.git.head.peel().id.raw
81 except pygit2.GitError:
103 except pygit2.GitError:
82 # Typically happens when peeling HEAD fails, as in an
104 # Typically happens when peeling HEAD fails, as in an
83 # empty repository.
105 # empty repository.
84 return sha1nodeconstants.nullid
106 return sha1nodeconstants.nullid
85
107
86 def p2(self):
108 def p2(self):
87 # TODO: MERGE_HEAD? something like that, right?
109 # TODO: MERGE_HEAD? something like that, right?
88 return sha1nodeconstants.nullid
110 return sha1nodeconstants.nullid
89
111
90 def setparents(self, p1, p2=None):
112 def setparents(self, p1, p2=None):
91 if p2 is None:
113 if p2 is None:
92 p2 = sha1nodeconstants.nullid
114 p2 = sha1nodeconstants.nullid
93 assert p2 == sha1nodeconstants.nullid, b'TODO merging support'
115 assert p2 == sha1nodeconstants.nullid, b'TODO merging support'
94 self.git.head.set_target(gitutil.togitnode(p1))
116 self.git.head.set_target(gitutil.togitnode(p1))
95
117
96 @util.propertycache
118 @util.propertycache
97 def identity(self):
119 def identity(self):
98 return util.filestat.frompath(
120 return util.filestat.frompath(
99 os.path.join(self._root, b'.git', b'index')
121 os.path.join(self._root, b'.git', b'index')
100 )
122 )
101
123
102 def branch(self):
124 def branch(self):
103 return b'default'
125 return b'default'
104
126
105 def parents(self):
127 def parents(self):
106 # TODO how on earth do we find p2 if a merge is in flight?
128 # TODO how on earth do we find p2 if a merge is in flight?
107 return self.p1(), sha1nodeconstants.nullid
129 return self.p1(), sha1nodeconstants.nullid
108
130
109 def __iter__(self):
131 def __iter__(self):
110 return (pycompat.fsencode(f.path) for f in self.git.index)
132 return (pycompat.fsencode(f.path) for f in self.git.index)
111
133
112 def items(self):
134 def items(self):
113 for ie in self.git.index:
135 for ie in self.git.index:
114 yield ie.path, None # value should be a DirstateItem
136 yield ie.path, None # value should be a DirstateItem
115
137
116 # py2,3 compat forward
138 # py2,3 compat forward
117 iteritems = items
139 iteritems = items
118
140
119 def __getitem__(self, filename):
141 def __getitem__(self, filename):
120 try:
142 try:
121 gs = self.git.status_file(filename)
143 gs = self.git.status_file(filename)
122 except KeyError:
144 except KeyError:
123 return b'?'
145 return b'?'
124 return _STATUS_MAP[gs]
146 return _STATUS_MAP[gs]
125
147
126 def __contains__(self, filename):
148 def __contains__(self, filename):
127 try:
149 try:
128 gs = self.git.status_file(filename)
150 gs = self.git.status_file(filename)
129 return _STATUS_MAP[gs] != b'?'
151 return _STATUS_MAP[gs] != b'?'
130 except KeyError:
152 except KeyError:
131 return False
153 return False
132
154
133 def status(self, match, subrepos, ignored, clean, unknown):
155 def status(self, match, subrepos, ignored, clean, unknown):
134 listclean = clean
156 listclean = clean
135 # TODO handling of clean files - can we get that from git.status()?
157 # TODO handling of clean files - can we get that from git.status()?
136 modified, added, removed, deleted, unknown, ignored, clean = (
158 modified, added, removed, deleted, unknown, ignored, clean = (
137 [],
159 [],
138 [],
160 [],
139 [],
161 [],
140 [],
162 [],
141 [],
163 [],
142 [],
164 [],
143 [],
165 [],
144 )
166 )
167
168 try:
169 mtime_boundary = timestamp.get_fs_now(self._opener)
170 except OSError:
171 # In largefiles or readonly context
172 mtime_boundary = None
173
145 gstatus = self.git.status()
174 gstatus = self.git.status()
146 for path, status in gstatus.items():
175 for path, status in gstatus.items():
147 path = pycompat.fsencode(path)
176 path = pycompat.fsencode(path)
148 if not match(path):
177 if not match(path):
149 continue
178 continue
150 if status == pygit2.GIT_STATUS_IGNORED:
179 if status == pygit2.GIT_STATUS_IGNORED:
151 if path.endswith(b'/'):
180 if path.endswith(b'/'):
152 continue
181 continue
153 ignored.append(path)
182 ignored.append(path)
154 elif status in (
183 elif status in (
155 pygit2.GIT_STATUS_WT_MODIFIED,
184 pygit2.GIT_STATUS_WT_MODIFIED,
156 pygit2.GIT_STATUS_INDEX_MODIFIED,
185 pygit2.GIT_STATUS_INDEX_MODIFIED,
157 pygit2.GIT_STATUS_WT_MODIFIED
186 pygit2.GIT_STATUS_WT_MODIFIED
158 | pygit2.GIT_STATUS_INDEX_MODIFIED,
187 | pygit2.GIT_STATUS_INDEX_MODIFIED,
159 ):
188 ):
160 modified.append(path)
189 modified.append(path)
161 elif status == pygit2.GIT_STATUS_INDEX_NEW:
190 elif status == pygit2.GIT_STATUS_INDEX_NEW:
162 added.append(path)
191 added.append(path)
163 elif status == pygit2.GIT_STATUS_WT_NEW:
192 elif status == pygit2.GIT_STATUS_WT_NEW:
164 unknown.append(path)
193 unknown.append(path)
165 elif status == pygit2.GIT_STATUS_WT_DELETED:
194 elif status == pygit2.GIT_STATUS_WT_DELETED:
166 deleted.append(path)
195 deleted.append(path)
167 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
196 elif status == pygit2.GIT_STATUS_INDEX_DELETED:
168 removed.append(path)
197 removed.append(path)
169 else:
198 else:
170 raise error.Abort(
199 raise error.Abort(
171 b'unhandled case: status for %r is %r' % (path, status)
200 b'unhandled case: status for %r is %r' % (path, status)
172 )
201 )
173
202
174 if listclean:
203 if listclean:
175 observed = set(
204 observed = set(
176 modified + added + removed + deleted + unknown + ignored
205 modified + added + removed + deleted + unknown + ignored
177 )
206 )
178 index = self.git.index
207 index = self.git.index
179 index.read()
208 index.read()
180 for entry in index:
209 for entry in index:
181 path = pycompat.fsencode(entry.path)
210 path = pycompat.fsencode(entry.path)
182 if not match(path):
211 if not match(path):
183 continue
212 continue
184 if path in observed:
213 if path in observed:
185 continue # already in some other set
214 continue # already in some other set
186 if path[-1] == b'/':
215 if path[-1] == b'/':
187 continue # directory
216 continue # directory
188 clean.append(path)
217 clean.append(path)
189
218
190 # TODO are we really always sure of status here?
219 # TODO are we really always sure of status here?
191 return (
220 return (
192 False,
221 False,
193 scmutil.status(
222 scmutil.status(
194 modified, added, removed, deleted, unknown, ignored, clean
223 modified, added, removed, deleted, unknown, ignored, clean
195 ),
224 ),
225 mtime_boundary,
196 )
226 )
197
227
198 def flagfunc(self, buildfallback):
228 def flagfunc(self, buildfallback):
199 # TODO we can do better
229 # TODO we can do better
200 return buildfallback()
230 return buildfallback()
201
231
202 def getcwd(self):
232 def getcwd(self):
203 # TODO is this a good way to do this?
233 # TODO is this a good way to do this?
204 return os.path.dirname(
234 return os.path.dirname(
205 os.path.dirname(pycompat.fsencode(self.git.path))
235 os.path.dirname(pycompat.fsencode(self.git.path))
206 )
236 )
207
237
238 def get_entry(self, path):
239 """return a DirstateItem for the associated path"""
240 entry = self._map.get(path)
241 if entry is None:
242 return DirstateItem()
243 return entry
244
208 def normalize(self, path):
245 def normalize(self, path):
209 normed = util.normcase(path)
246 normed = util.normcase(path)
210 assert normed == path, b"TODO handling of case folding: %s != %s" % (
247 assert normed == path, b"TODO handling of case folding: %s != %s" % (
211 normed,
248 normed,
212 path,
249 path,
213 )
250 )
214 return path
251 return path
215
252
216 @property
253 @property
217 def _checklink(self):
254 def _checklink(self):
218 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
255 return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
219
256
220 def copies(self):
257 def copies(self):
221 # TODO support copies?
258 # TODO support copies?
222 return {}
259 return {}
223
260
224 # # TODO what the heck is this
261 # # TODO what the heck is this
225 _filecache = set()
262 _filecache = set()
226
263
227 def pendingparentchange(self):
264 def pendingparentchange(self):
228 # TODO: we need to implement the context manager bits and
265 # TODO: we need to implement the context manager bits and
229 # correctly stage/revert index edits.
266 # correctly stage/revert index edits.
230 return False
267 return False
231
268
232 def write(self, tr):
269 def write(self, tr):
233 # TODO: call parent change callbacks
270 # TODO: call parent change callbacks
234
271
235 if tr:
272 if tr:
236
273
237 def writeinner(category):
274 def writeinner(category):
238 self.git.index.write()
275 self.git.index.write()
239
276
240 tr.addpending(b'gitdirstate', writeinner)
277 tr.addpending(b'gitdirstate', writeinner)
241 else:
278 else:
242 self.git.index.write()
279 self.git.index.write()
243
280
244 def pathto(self, f, cwd=None):
281 def pathto(self, f, cwd=None):
245 if cwd is None:
282 if cwd is None:
246 cwd = self.getcwd()
283 cwd = self.getcwd()
247 # TODO core dirstate does something about slashes here
284 # TODO core dirstate does something about slashes here
248 assert isinstance(f, bytes)
285 assert isinstance(f, bytes)
249 r = util.pathto(self._root, cwd, f)
286 r = util.pathto(self._root, cwd, f)
250 return r
287 return r
251
288
252 def matches(self, match):
289 def matches(self, match):
253 for x in self.git.index:
290 for x in self.git.index:
254 p = pycompat.fsencode(x.path)
291 p = pycompat.fsencode(x.path)
255 if match(p):
292 if match(p):
256 yield p
293 yield p
257
294
258 def set_clean(self, f, parentfiledata):
295 def set_clean(self, f, parentfiledata):
259 """Mark a file normal and clean."""
296 """Mark a file normal and clean."""
260 # TODO: for now we just let libgit2 re-stat the file. We can
297 # TODO: for now we just let libgit2 re-stat the file. We can
261 # clearly do better.
298 # clearly do better.
262
299
263 def set_possibly_dirty(self, f):
300 def set_possibly_dirty(self, f):
264 """Mark a file normal, but possibly dirty."""
301 """Mark a file normal, but possibly dirty."""
265 # TODO: for now we just let libgit2 re-stat the file. We can
302 # TODO: for now we just let libgit2 re-stat the file. We can
266 # clearly do better.
303 # clearly do better.
267
304
268 def walk(self, match, subrepos, unknown, ignored, full=True):
305 def walk(self, match, subrepos, unknown, ignored, full=True):
269 # TODO: we need to use .status() and not iterate the index,
306 # TODO: we need to use .status() and not iterate the index,
270 # because the index doesn't force a re-walk and so `hg add` of
307 # because the index doesn't force a re-walk and so `hg add` of
271 # a new file without an intervening call to status will
308 # a new file without an intervening call to status will
272 # silently do nothing.
309 # silently do nothing.
273 r = {}
310 r = {}
274 cwd = self.getcwd()
311 cwd = self.getcwd()
275 for path, status in self.git.status().items():
312 for path, status in self.git.status().items():
276 if path.startswith('.hg/'):
313 if path.startswith('.hg/'):
277 continue
314 continue
278 path = pycompat.fsencode(path)
315 path = pycompat.fsencode(path)
279 if not match(path):
316 if not match(path):
280 continue
317 continue
281 # TODO construct the stat info from the status object?
318 # TODO construct the stat info from the status object?
282 try:
319 try:
283 s = os.stat(os.path.join(cwd, path))
320 s = os.stat(os.path.join(cwd, path))
284 except OSError as e:
321 except OSError as e:
285 if e.errno != errno.ENOENT:
322 if e.errno != errno.ENOENT:
286 raise
323 raise
287 continue
324 continue
288 r[path] = s
325 r[path] = s
289 return r
326 return r
290
327
291 def savebackup(self, tr, backupname):
328 def savebackup(self, tr, backupname):
292 # TODO: figure out a strategy for saving index backups.
329 # TODO: figure out a strategy for saving index backups.
293 pass
330 pass
294
331
295 def restorebackup(self, tr, backupname):
332 def restorebackup(self, tr, backupname):
296 # TODO: figure out a strategy for saving index backups.
333 # TODO: figure out a strategy for saving index backups.
297 pass
334 pass
298
335
299 def set_tracked(self, f):
336 def set_tracked(self, f):
300 uf = pycompat.fsdecode(f)
337 uf = pycompat.fsdecode(f)
301 if uf in self.git.index:
338 if uf in self.git.index:
302 return False
339 return False
303 index = self.git.index
340 index = self.git.index
304 index.read()
341 index.read()
305 index.add(uf)
342 index.add(uf)
306 index.write()
343 index.write()
307 return True
344 return True
308
345
309 def add(self, f):
346 def add(self, f):
310 index = self.git.index
347 index = self.git.index
311 index.read()
348 index.read()
312 index.add(pycompat.fsdecode(f))
349 index.add(pycompat.fsdecode(f))
313 index.write()
350 index.write()
314
351
315 def drop(self, f):
352 def drop(self, f):
316 index = self.git.index
353 index = self.git.index
317 index.read()
354 index.read()
318 fs = pycompat.fsdecode(f)
355 fs = pycompat.fsdecode(f)
319 if fs in index:
356 if fs in index:
320 index.remove(fs)
357 index.remove(fs)
321 index.write()
358 index.write()
322
359
323 def set_untracked(self, f):
360 def set_untracked(self, f):
324 index = self.git.index
361 index = self.git.index
325 index.read()
362 index.read()
326 fs = pycompat.fsdecode(f)
363 fs = pycompat.fsdecode(f)
327 if fs in index:
364 if fs in index:
328 index.remove(fs)
365 index.remove(fs)
329 index.write()
366 index.write()
330 return True
367 return True
331 return False
368 return False
332
369
333 def remove(self, f):
370 def remove(self, f):
334 index = self.git.index
371 index = self.git.index
335 index.read()
372 index.read()
336 index.remove(pycompat.fsdecode(f))
373 index.remove(pycompat.fsdecode(f))
337 index.write()
374 index.write()
338
375
339 def copied(self, path):
376 def copied(self, path):
340 # TODO: track copies?
377 # TODO: track copies?
341 return None
378 return None
342
379
343 def prefetch_parents(self):
380 def prefetch_parents(self):
344 # TODO
381 # TODO
345 pass
382 pass
346
383
347 def update_file(self, *args, **kwargs):
384 def update_file(self, *args, **kwargs):
348 # TODO
385 # TODO
349 pass
386 pass
350
387
351 @contextlib.contextmanager
388 @contextlib.contextmanager
352 def parentchange(self):
389 def parentchange(self):
353 # TODO: track this maybe?
390 # TODO: track this maybe?
354 yield
391 yield
355
392
356 def addparentchangecallback(self, category, callback):
393 def addparentchangecallback(self, category, callback):
357 # TODO: should this be added to the dirstate interface?
394 # TODO: should this be added to the dirstate interface?
358 self._plchangecallbacks[category] = callback
395 self._plchangecallbacks[category] = callback
359
396
360 def clearbackup(self, tr, backupname):
397 def clearbackup(self, tr, backupname):
361 # TODO
398 # TODO
362 pass
399 pass
363
400
364 def setbranch(self, branch):
401 def setbranch(self, branch):
365 raise error.Abort(
402 raise error.Abort(
366 b'git repos do not support branches. try using bookmarks'
403 b'git repos do not support branches. try using bookmarks'
367 )
404 )
General Comments 0
You need to be logged in to leave comments. Login now