Skip to content

Commit 272a1ea

Browse files
authored
Determine development version hash without running git (#21212)
We've called git in a subprocess so that we can include git version hash in the version (like `2.0.0+dev.32f307a7cca81c13db6bf6a7984493cefebd7b1e`). This happens with development versions run in the git mypy repo. Running git as a subprocess can dominate mypy startup time in certain environments, so this PR adds logic to figure out current HEAD hash without running git. I've seen the git invocations add 200ms+ to mypy runtimes. Unfortunately, we can't easily figure out if .dirty should be added without git, so I'm just removing it altogether from the version. Since startup overhead becomes a worse issue when using parallel type checking, not having .dirty support seems like a smaller issue than slow startup. This was written using Claude Code.
1 parent f315c8a commit 272a1ea

File tree

2 files changed

+41
-4
lines changed

2 files changed

+41
-4
lines changed

mypy/git.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,41 @@ def git_revision(dir: str) -> bytes:
2828
return subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=dir).strip()
2929

3030

31+
def git_revision_no_subprocess(dir: str) -> bytes | None:
32+
"""Get the SHA-1 of HEAD by reading git files directly, without a subprocess.
33+
34+
Returns None if the revision cannot be determined this way (e.g. unusual
35+
git state), in which case the caller should fall back to git_revision().
36+
"""
37+
try:
38+
head_path = os.path.join(dir, ".git", "HEAD")
39+
with open(head_path, "rb") as f:
40+
head = f.read().strip()
41+
if head.startswith(b"ref: "):
42+
ref = head[5:] # e.g. b"refs/heads/main"
43+
ref_path = os.path.join(dir, ".git", os.fsdecode(ref))
44+
if os.path.exists(ref_path):
45+
with open(ref_path, "rb") as f:
46+
return f.read().strip()
47+
# The ref may be in packed-refs instead of a loose file.
48+
packed_refs_path = os.path.join(dir, ".git", "packed-refs")
49+
if os.path.exists(packed_refs_path):
50+
with open(packed_refs_path, "rb") as f:
51+
for line in f:
52+
if line.startswith(b"#"):
53+
continue
54+
parts = line.strip().split()
55+
if len(parts) >= 2 and parts[1] == ref:
56+
return parts[0]
57+
return None
58+
# Detached HEAD: content is the SHA itself.
59+
if len(head) == 40 and all(chr(c) in "0123456789abcdef" for c in head):
60+
return head
61+
return None
62+
except OSError:
63+
return None
64+
65+
3166
def is_dirty(dir: str) -> bool:
3267
"""Check whether a git repository has uncommitted changes."""
3368
output = subprocess.check_output(["git", "status", "-uno", "--porcelain"], cwd=dir)

mypy/version.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
base_version = __version__
1313

1414
mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
15-
if __version__.endswith("+dev") and git.is_git_repo(mypy_dir) and git.have_git():
16-
__version__ += "." + git.git_revision(mypy_dir).decode("utf-8")
17-
if git.is_dirty(mypy_dir):
18-
__version__ += ".dirty"
15+
if __version__.endswith("+dev") and git.is_git_repo(mypy_dir):
16+
revision = git.git_revision_no_subprocess(mypy_dir)
17+
if revision is not None:
18+
__version__ += "." + revision.decode("ascii")
19+
elif git.have_git():
20+
__version__ += "." + git.git_revision(mypy_dir).decode("utf-8")
1921
del mypy_dir

0 commit comments

Comments
 (0)