Mercurial > hg-git-serve
view src/hgext3rd/hggit_serve/_export.py @ 17:a70f387ab3cd default tip
Fix formatting in documentation.
| author | Paul Fisher <paul@pfish.zone> |
|---|---|
| date | Fri, 20 Feb 2026 21:12:40 -0500 |
| parents | 78ea1ec94be5 |
| children |
line wrap: on
line source
from __future__ import annotations import binascii import typing as t import dulwich.refs import mercurial.error as hgerr from hggit import git_handler if t.TYPE_CHECKING: import mercurial.interfaces.repository as hgrepo import mercurial.ui as hgui class GittyRepo(hgrepo.IRepo, t.Protocol): githandler: git_handler.GitHandler def is_gitty(repo: hgrepo.IRepo) -> t.TypeGuard[GittyRepo]: """Ensures that we have hg-git installed and active.""" return hasattr(repo, 'githandler') PULL = b'pull' PUSH = b'push' SERVICE_PERMISSIONS = { b'git-upload-pack': PULL, b'git-receive-pack': PUSH, } """The Mercurial permission corresponding to each Git action. These seem backwards because the direction of up/download is relative to the server, so when the client pulls, the server is *uploading*, and when the client pushes, the server is *downloading*. """ # # Stuff so that we don't try to export revisions while we're importing. # _ILEVEL_ATTR = '@hggit_import_level' """An attribute that tracks how many "levels deep" we are into importing. We set this on the repository object when we're importing and remove it when we're done. It's not just a bool in case somebody sets up some crazy recursive hook situation where we start importing inside another import. """ def _importing_enter(repo: hgrepo.IRepo) -> None: """Call this before you start importing from Git.""" level = getattr(repo, _ILEVEL_ATTR, 0) + 1 setattr(repo, _ILEVEL_ATTR, level) def _is_importing(repo: hgrepo.IRepo) -> bool: """Call this to check if you're currently importing.""" return hasattr(repo, _ILEVEL_ATTR) def _importing_exit(repo: hgrepo.IRepo) -> None: """Call this after you finish importing from Git.""" level = getattr(repo, _ILEVEL_ATTR) - 1 if level: setattr(repo, _ILEVEL_ATTR, level) else: delattr(repo, _ILEVEL_ATTR) def import_all(repo: GittyRepo, command: bytes = b'(unknown)') -> None: _importing_enter(repo) try: gh = repo.githandler gh.import_git_objects( command, remote_names=(), refs=gh.git.refs.as_dict() ) finally: _importing_exit(repo) # # Export handling. # def _clean_all_refs(refs: dulwich.refs.RefsContainer) -> None: """Removes all refs from the Git repository.""" # dump to allkeys so we explicitly are iterating over a snapshot # and not over something while we mutate for ref in refs.allkeys(): refs.remove_if_equals(ref, None) def _set_head(ui: hgui.ui, repo: GittyRepo, at_name: bytes) -> None: """Creates a HEAD reference in Git referring to the current HEAD.""" # By default, we use '@', since that's what will be auto checked out. current = b'@' if current not in repo._bookmarks: current = repo._bookmarks.active or current # We'll be moving this (possibly fake) bookmark into Git. git_current = current if current == b'@': # @ is a special keyword in Git, so we can't use it as a bookmark. git_current = at_name git_branch = dulwich.refs.LOCAL_BRANCH_PREFIX + git_current if not dulwich.refs.check_ref_format(git_branch): # We can't export this ref to Git. Give up. ui.warn(f'{git_branch!r} is not a valid branch name for Git.'.encode()) return try: # Maybe this is a real bookmark? hgnode = repo._bookmarks[current] except KeyError: # Not a real bookmark. Assume we want the tip of the current branch. branch = repo.dirstate.branch() try: hgnode = repo.branchtip(branch) except hgerr.RepoLookupError: # This branch somehow doesn't exist??? ui.warn(f"{branch!r} doesn't seem to exist?".encode()) return hgsha = binascii.hexlify(hgnode) gitsha = repo.githandler.map_git_get(hgsha) if not gitsha: # No Git SHA to match this Hg sha. Give up. ui.warn(f'revision {hgsha!r} was not exported to Git'.encode()) return refs = repo.githandler.git.refs refs.add_packed_refs({git_branch: gitsha}) refs.set_symbolic_ref(b'HEAD', git_branch) def _fix_refs(ui: hgui.ui, repo: GittyRepo) -> None: """After a git export, fix up the refs. This ensures that there are no leftover refs from older, removed bookmarks and that there is a proper HEAD set so that cloning works. """ _clean_all_refs(repo.githandler.git.refs) repo.githandler.export_hg_tags() repo.githandler.update_references() default_branch_name = ui.config( b'hggit-serve', b'default-branch', b'default' ) _set_head(ui, repo, default_branch_name) # # Hooks # _AX_ALWAYS = b'always' _AX_DEFAULT = b'default' _AX_NEVER = b'never' def _export_hook(ui: hgui.ui, repo: hgrepo.IRepo, **__: object) -> None: """Maybe exports the repository to get after we get new revs.""" if not is_gitty(repo): return auto_export = ui.config(b'hggit-serve', b'auto-export') if auto_export == _AX_NEVER: return if auto_export == _AX_ALWAYS or git_handler.has_gitrepo(repo): if auto_export not in (None, _AX_ALWAYS, _AX_DEFAULT): ui.warn( b'unrecognized auto-export setting "' + auto_export + b'"; using "' + _AX_DEFAULT + b'"\n' ) if _is_importing(repo): ui.note(b'currently importing revs from git; not exporting\n') return repo.githandler.export_commits() _fix_refs(ui, repo) def _fix_refs_hook(ui: hgui.ui, repo: hgrepo.IRepo, **__: object) -> None: """Exports to Git and sets up for serving. See ``_fix_refs``.""" if not is_gitty(repo): return _fix_refs(ui, repo) # # Interfacing with Mercurial # def uipopulate(ui: hgui.ui) -> None: # Fix up our tags after a Git export. ui.setconfig( b'hooks', b'post-git-export.__gitserve_add_tag__', _fix_refs_hook, source=b'hggit_serve', ) # Whenever we get new revisions, export them to the Git repository. ui.setconfig( b'hooks', b'txnclose.__gitserve_export__', _export_hook, source=b'hggit_serve', ) # Don't step on ourselves when importing data from Git. ui.setconfig( b'hooks', b'pre-git-import.__gitserve_suppress_export__', lambda _, repo, **__: _importing_enter(repo), source=b'hggit_serve', ) ui.setconfig( b'hooks', b'post-git-import.__gitserve_suppress_export__', lambda _, repo, **__: _importing_exit(repo), source=b'hggit-serve', )
