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 )