--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/git/__init__.py Tue Feb 11 00:44:59 2020 -0500
@@ -0,0 +1,259 @@
+"""grant Mercurial the ability to operate on Git repositories. (EXPERIMENTAL)
+
+This is currently super experimental. It probably will consume your
+firstborn a la Rumpelstiltskin, etc.
+"""
+
+from __future__ import absolute_import
+
+import os
+
+import pygit2
+
+from mercurial.i18n import _
+
+from mercurial import (
+ commands,
+ error,
+ extensions,
+ localrepo,
+ pycompat,
+ store,
+ util,
+)
+
+from . import (
+ dirstate,
+ gitlog,
+ gitutil,
+ index,
+)
+
+
+# TODO: extract an interface for this in core
+class gitstore(object): # store.basicstore):
+ def __init__(self, path, vfstype):
+ self.vfs = vfstype(path)
+ self.path = self.vfs.base
+ self.createmode = store._calcmode(self.vfs)
+ # above lines should go away in favor of:
+ # super(gitstore, self).__init__(path, vfstype)
+
+ self.git = pygit2.Repository(
+ os.path.normpath(os.path.join(path, b'..', b'.git'))
+ )
+ self._progress_factory = lambda *args, **kwargs: None
+
+ @util.propertycache
+ def _db(self):
+ # We lazy-create the database because we want to thread a
+ # progress callback down to the indexing process if it's
+ # required, and we don't have a ui handle in makestore().
+ return index.get_index(self.git, self._progress_factory)
+
+ def join(self, f):
+ """Fake store.join method for git repositories.
+
+ For the most part, store.join is used for @storecache
+ decorators to invalidate caches when various files
+ change. We'll map the ones we care about, and ignore the rest.
+ """
+ if f in (b'00changelog.i', b'00manifest.i'):
+ # This is close enough: in order for the changelog cache
+ # to be invalidated, HEAD will have to change.
+ return os.path.join(self.path, b'HEAD')
+ elif f == b'lock':
+ # TODO: we probably want to map this to a git lock, I
+ # suspect index.lock. We should figure out what the
+ # most-alike file is in git-land. For now we're risking
+ # bad concurrency errors if another git client is used.
+ return os.path.join(self.path, b'hgit-bogus-lock')
+ elif f in (b'obsstore', b'phaseroots', b'narrowspec', b'bookmarks'):
+ return os.path.join(self.path, b'..', b'.hg', f)
+ raise NotImplementedError(b'Need to pick file for %s.' % f)
+
+ def changelog(self, trypending):
+ # TODO we don't have a plan for trypending in hg's git support yet
+ return gitlog.changelog(self.git, self._db)
+
+ def manifestlog(self, repo, storenarrowmatch):
+ # TODO handle storenarrowmatch and figure out if we need the repo arg
+ return gitlog.manifestlog(self.git, self._db)
+
+ def invalidatecaches(self):
+ pass
+
+ def write(self, tr=None):
+ # normally this handles things like fncache writes, which we don't have
+ pass
+
+
+def _makestore(orig, requirements, storebasepath, vfstype):
+ if os.path.exists(
+ os.path.join(storebasepath, b'this-is-git')
+ ) and os.path.exists(os.path.join(storebasepath, b'..', b'.git')):
+ return gitstore(storebasepath, vfstype)
+ return orig(requirements, storebasepath, vfstype)
+
+
+class gitfilestorage(object):
+ def file(self, path):
+ if path[0:1] == b'/':
+ path = path[1:]
+ return gitlog.filelog(self.store.git, self.store._db, path)
+
+
+def _makefilestorage(orig, requirements, features, **kwargs):
+ store = kwargs['store']
+ if isinstance(store, gitstore):
+ return gitfilestorage
+ return orig(requirements, features, **kwargs)
+
+
+def _setupdothg(ui, path):
+ dothg = os.path.join(path, b'.hg')
+ if os.path.exists(dothg):
+ ui.warn(_(b'git repo already initialized for hg\n'))
+ else:
+ os.mkdir(os.path.join(path, b'.hg'))
+ # TODO is it ok to extend .git/info/exclude like this?
+ with open(
+ os.path.join(path, b'.git', b'info', b'exclude'), 'ab'
+ ) as exclude:
+ exclude.write(b'\n.hg\n')
+ with open(os.path.join(dothg, b'this-is-git'), 'wb') as f:
+ pass
+ with open(os.path.join(dothg, b'requirements'), 'wb') as f:
+ f.write(b'git\n')
+
+
+_BMS_PREFIX = 'refs/heads/'
+
+
+class gitbmstore(object):
+ def __init__(self, gitrepo):
+ self.gitrepo = gitrepo
+
+ def __contains__(self, name):
+ return (
+ _BMS_PREFIX + pycompat.fsdecode(name)
+ ) in self.gitrepo.references
+
+ def __iter__(self):
+ for r in self.gitrepo.listall_references():
+ if r.startswith(_BMS_PREFIX):
+ yield pycompat.fsencode(r[len(_BMS_PREFIX) :])
+
+ def __getitem__(self, k):
+ return (
+ self.gitrepo.references[_BMS_PREFIX + pycompat.fsdecode(k)]
+ .peel()
+ .id.raw
+ )
+
+ def get(self, k, default=None):
+ try:
+ if k in self:
+ return self[k]
+ return default
+ except pygit2.InvalidSpecError:
+ return default
+
+ @property
+ def active(self):
+ h = self.gitrepo.references['HEAD']
+ if not isinstance(h.target, str) or not h.target.startswith(
+ _BMS_PREFIX
+ ):
+ return None
+ return pycompat.fsencode(h.target[len(_BMS_PREFIX) :])
+
+ @active.setter
+ def active(self, mark):
+ raise NotImplementedError
+
+ def names(self, node):
+ r = []
+ for ref in self.gitrepo.listall_references():
+ if not ref.startswith(_BMS_PREFIX):
+ continue
+ if self.gitrepo.references[ref].peel().id.raw != node:
+ continue
+ r.append(pycompat.fsencode(ref[len(_BMS_PREFIX) :]))
+ return r
+
+ # Cleanup opportunity: this is *identical* to core's bookmarks store.
+ def expandname(self, bname):
+ if bname == b'.':
+ if self.active:
+ return self.active
+ raise error.RepoLookupError(_(b"no active bookmark"))
+ return bname
+
+ def applychanges(self, repo, tr, changes):
+ """Apply a list of changes to bookmarks
+ """
+ # TODO: this should respect transactions, but that's going to
+ # require enlarging the gitbmstore to know how to do in-memory
+ # temporary writes and read those back prior to transaction
+ # finalization.
+ for name, node in changes:
+ if node is None:
+ self.gitrepo.references.delete(
+ _BMS_PREFIX + pycompat.fsdecode(name)
+ )
+ else:
+ self.gitrepo.references.create(
+ _BMS_PREFIX + pycompat.fsdecode(name),
+ gitutil.togitnode(node),
+ force=True,
+ )
+
+
+def init(orig, ui, dest=b'.', **opts):
+ if opts.get('git', False):
+ path = os.path.abspath(dest)
+ # TODO: walk up looking for the git repo
+ _setupdothg(ui, path)
+ return 0
+ return orig(ui, dest=dest, **opts)
+
+
+def reposetup(ui, repo):
+ if isinstance(repo.store, gitstore):
+ orig = repo.__class__
+ repo.store._progress_factory = repo.ui.makeprogress
+
+ class gitlocalrepo(orig):
+ def _makedirstate(self):
+ # TODO narrow support here
+ return dirstate.gitdirstate(
+ self.ui, self.vfs.base, self.store.git
+ )
+
+ def commit(self, *args, **kwargs):
+ ret = orig.commit(self, *args, **kwargs)
+ tid = self.store.git[gitutil.togitnode(ret)].tree.id
+ # DANGER! This will flush any writes staged to the
+ # index in Git, but we're sidestepping the index in a
+ # way that confuses git when we commit. Alas.
+ self.store.git.index.read_tree(tid)
+ self.store.git.index.write()
+ return ret
+
+ @property
+ def _bookmarks(self):
+ return gitbmstore(self.store.git)
+
+ repo.__class__ = gitlocalrepo
+ return repo
+
+
+def extsetup(ui):
+ extensions.wrapfunction(localrepo, b'makestore', _makestore)
+ extensions.wrapfunction(localrepo, b'makefilestorage', _makefilestorage)
+ # Inject --git flag for `hg init`
+ entry = extensions.wrapcommand(commands.table, b'init', init)
+ entry[1].extend(
+ [(b'', b'git', None, b'setup up a git repository instead of hg')]
+ )