Mercurial > hg-git-serve
comparison src/hgext3rd/hggit_serve/_export.py @ 12:f630d9904ea7
Reorganize project into multiple files.
| author | Paul Fisher <paul@pfish.zone> |
|---|---|
| date | Wed, 18 Feb 2026 16:17:05 -0500 |
| parents | src/hgext3rd/hggit_serve.py@ce204bcc4e04 |
| children | 00bdfac5416c |
comparison
equal
deleted
inserted
replaced
| 11:ce204bcc4e04 | 12:f630d9904ea7 |
|---|---|
| 1 from __future__ import annotations | |
| 2 | |
| 3 import binascii | |
| 4 import typing as t | |
| 5 | |
| 6 import dulwich.refs | |
| 7 import mercurial.error as hgerr | |
| 8 from hggit import git_handler | |
| 9 | |
| 10 if t.TYPE_CHECKING: | |
| 11 import mercurial.interfaces.repository as hgrepo | |
| 12 import mercurial.ui as hgui | |
| 13 | |
| 14 class GittyRepo(hgrepo.IRepo, t.Protocol): | |
| 15 githandler: git_handler.GitHandler | |
| 16 | |
| 17 | |
| 18 def is_gitty(repo: hgrepo.IRepo) -> t.TypeGuard[GittyRepo]: | |
| 19 """Ensures that we have hg-git installed and active.""" | |
| 20 return hasattr(repo, 'githandler') | |
| 21 | |
| 22 | |
| 23 PULL = b'pull' | |
| 24 PUSH = b'push' | |
| 25 | |
| 26 SERVICE_PERMISSIONS = { | |
| 27 b'git-upload-pack': PULL, | |
| 28 b'git-receive-pack': PUSH, | |
| 29 } | |
| 30 """The Mercurial permission corresponding to each Git action. | |
| 31 | |
| 32 These seem backwards because the direction of up/download is relative to | |
| 33 the server, so when the client pulls, the server is *uploading*, | |
| 34 and when the client pushes, the server is *downloading*. | |
| 35 """ | |
| 36 | |
| 37 | |
| 38 # | |
| 39 # Stuff so that we don't try to export revisions while we're importing. | |
| 40 # | |
| 41 | |
| 42 _ILEVEL_ATTR = '@hggit_import_level' | |
| 43 """An attribute that tracks how many "levels deep" we are into importing. | |
| 44 | |
| 45 We set this on the repository object when we're importing and remove it | |
| 46 when we're done. It's not just a bool in case somebody sets up some crazy | |
| 47 recursive hook situation where we start importing inside another import. | |
| 48 """ | |
| 49 | |
| 50 | |
| 51 def importing_enter(repo: hgrepo.IRepo) -> None: | |
| 52 """Call this before you start importing from Git.""" | |
| 53 level = getattr(repo, _ILEVEL_ATTR, 0) + 1 | |
| 54 setattr(repo, _ILEVEL_ATTR, level) | |
| 55 | |
| 56 | |
| 57 def is_importing(repo: hgrepo.IRepo) -> bool: | |
| 58 """Call this to check if you're currently importing.""" | |
| 59 return hasattr(repo, _ILEVEL_ATTR) | |
| 60 | |
| 61 | |
| 62 def importing_exit(repo: hgrepo.IRepo) -> None: | |
| 63 """Call this after you finish importing from Git.""" | |
| 64 level = getattr(repo, _ILEVEL_ATTR) - 1 | |
| 65 if level: | |
| 66 setattr(repo, _ILEVEL_ATTR, level) | |
| 67 else: | |
| 68 delattr(repo, _ILEVEL_ATTR) | |
| 69 | |
| 70 | |
| 71 # | |
| 72 # Export handling. | |
| 73 # | |
| 74 | |
| 75 | |
| 76 def clean_all_refs(refs: dulwich.refs.RefsContainer) -> None: | |
| 77 """Removes all refs from the Git repository.""" | |
| 78 | |
| 79 | |
| 80 def set_head(ui: hgui.ui, repo: GittyRepo, at_name: bytes) -> None: | |
| 81 """Creates a HEAD reference in Git referring to the current HEAD.""" | |
| 82 # By default, we use '@', since that's what will be auto checked out. | |
| 83 current = b'@' | |
| 84 if current not in repo._bookmarks: | |
| 85 current = repo._bookmarks.active or current | |
| 86 | |
| 87 # We'll be moving this (possibly fake) bookmark into Git. | |
| 88 git_current = current | |
| 89 if current == b'@': | |
| 90 # @ is a special keyword in Git, so we can't use it as a bookmark. | |
| 91 git_current = at_name | |
| 92 git_branch = dulwich.refs.LOCAL_BRANCH_PREFIX + git_current | |
| 93 if not dulwich.refs.check_ref_format(git_branch): | |
| 94 # We can't export this ref to Git. Give up. | |
| 95 ui.warn(f'{git_branch!r} is not a valid branch name for Git.'.encode()) | |
| 96 return | |
| 97 try: | |
| 98 # Maybe this is a real bookmark? | |
| 99 hgnode = repo._bookmarks[current] | |
| 100 except KeyError: | |
| 101 # Not a real bookmark. Assume we want the tip of the current branch. | |
| 102 branch = repo.dirstate.branch() | |
| 103 try: | |
| 104 hgnode = repo.branchtip(branch) | |
| 105 except hgerr.RepoLookupError: | |
| 106 # This branch somehow doesn't exist??? | |
| 107 ui.warn(f"{branch!r} doesn't seem to exist?".encode()) | |
| 108 return | |
| 109 hgsha = binascii.hexlify(hgnode) | |
| 110 gitsha = repo.githandler.map_git_get(hgsha) | |
| 111 if not gitsha: | |
| 112 # No Git SHA to match this Hg sha. Give up. | |
| 113 ui.warn(f'revision {hgsha!r} was not exported to Git'.encode()) | |
| 114 return | |
| 115 refs = repo.githandler.git.refs | |
| 116 refs.add_packed_refs({git_branch: gitsha}) | |
| 117 refs.set_symbolic_ref(b'HEAD', git_branch) | |
| 118 | |
| 119 | |
| 120 def fix_refs(ui: hgui.ui, repo: GittyRepo) -> None: | |
| 121 """After a git export, fix up the refs. | |
| 122 | |
| 123 This ensures that there are no leftover refs from older, removed bookmarks | |
| 124 and that there is a proper HEAD set so that cloning works. | |
| 125 """ | |
| 126 refs = repo.githandler.git.refs | |
| 127 # dump to allkeys so we explicitly are iterating over a snapshot | |
| 128 # and not over something while we mutate | |
| 129 for ref in refs.allkeys(): | |
| 130 refs.remove_if_equals(ref, None) | |
| 131 repo.githandler.export_hg_tags() | |
| 132 repo.githandler.update_references() | |
| 133 default_branch_name = ui.config( | |
| 134 b'hggit-serve', b'default-branch', b'default' | |
| 135 ) | |
| 136 set_head(ui, repo, default_branch_name) | |
| 137 | |
| 138 | |
| 139 # | |
| 140 # Hooks | |
| 141 # | |
| 142 | |
| 143 | |
| 144 def _export_hook(ui: hgui.ui, repo: hgrepo.IRepo, **__: object) -> None: | |
| 145 """Maybe exports the repository to get after we get new revs.""" | |
| 146 if not is_gitty(repo): | |
| 147 return | |
| 148 auto_export = ui.config(b'hggit-serve', b'auto-export') | |
| 149 if auto_export == b'never': | |
| 150 return | |
| 151 if auto_export == b'always' or git_handler.has_gitrepo(repo): | |
| 152 if is_importing(repo): | |
| 153 ui.note(b'currently importing revs from git; not exporting\n') | |
| 154 return | |
| 155 repo.githandler.export_commits() | |
| 156 fix_refs(ui, repo) | |
| 157 | |
| 158 | |
| 159 def _fix_refs_hook(ui: hgui.ui, repo: hgrepo.IRepo, **__: object) -> None: | |
| 160 """Exports to Git and sets up for serving. See ``_fix_refs``.""" | |
| 161 if not is_gitty(repo): | |
| 162 return | |
| 163 fix_refs(ui, repo) | |
| 164 | |
| 165 | |
| 166 # | |
| 167 # Interfacing with Mercurial | |
| 168 # | |
| 169 | |
| 170 | |
| 171 def uipopulate(ui: hgui.ui) -> None: | |
| 172 # Fix up our tags after a Git export. | |
| 173 ui.setconfig( | |
| 174 b'hooks', b'post-git-export.__gitserve_add_tag__', _fix_refs_hook | |
| 175 ) | |
| 176 # Whenever we get new revisions, export them to the Git repository. | |
| 177 ui.setconfig(b'hooks', b'txnclose.__gitserve_export__', _export_hook) | |
| 178 # Don't step on ourselves when importing data from Git. | |
| 179 ui.setconfig( | |
| 180 b'hooks', | |
| 181 b'pre-git-import.__gitserve_suppress_export__', | |
| 182 lambda _, repo, **__: importing_enter(repo), | |
| 183 ) | |
| 184 ui.setconfig( | |
| 185 b'hooks', | |
| 186 b'post-git-import.__gitserve_suppress_export__', | |
| 187 lambda _, repo, **__: importing_exit(repo), | |
| 188 ) |
