Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions hooks/pre_gen_project.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
#!/bin/env python3
import sys
from subprocess import run

# Check if git user.name and user.email are configured --> fail early if it's not set
run(["git", "config", "user.name"], check=True)
run(["git", "config", "user.email"], check=True)

def git_config_get(key: str) -> str | None:
"""Return the value of a git config key, or an empty string if it is not set."""
result = run(["git", "config", key], capture_output=True, text=True)
return result.stdout.strip() if result.returncode == 0 else None


# use 'main' as default branch irrespective of git configuration
run(["git", "init", "--initial-branch=main", "."], check=True)

# Resolve the author identity for the initial commit.
# We do *not* mandate a global git config: some users configure git per-repository only.
# Prefer an existing git config (global or system) and fall back to the values the user
# entered in cookiecutter.
name = git_config_get("user.name") or "{{ cookiecutter.author_full_name }}".strip()
email = git_config_get("user.email") or "{{ cookiecutter.author_email }}".strip()

if not name or not email:
sys.exit(
"ERROR: could not determine an author name/email for the initial commit.\n"
"Either configure git (`git config --global user.name ...` and "
"`git config --global user.email ...`) or provide the author name/email "
"when prompted by the template."
)
Comment thread
flying-sheep marked this conversation as resolved.

# Set the identity at the repo level only when it is not already resolvable from existing
# git config, so we never clobber the user's real git identity.
if not git_config_get("user.name"):
run(["git", "config", "user.name", name], check=True)
if not git_config_get("user.email"):
run(["git", "config", "user.email", email], check=True)
32 changes: 32 additions & 0 deletions scripts/tests/test_build.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import os
import re
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -55,3 +57,33 @@ def test_build(tmp_path: Path, params: Mapping[str, Any], path: Path | str, patt
assert pattern.search(path.read_text())

assert not list(proj_dir.rglob("DELETE-ME"))


def test_build_without_global_git_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Generation must succeed without a global/system git identity (see issue #389).

Users who configure git per-repository only have no global ``user.name``/``user.email``.
In that case the hooks should fall back to the cookiecutter ``author_full_name``/
``author_email`` answers so the initial commit still succeeds.
"""
# Neutralize any global/system git config so no ambient git identity is available.
monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull)
monkeypatch.setenv("GIT_CONFIG_SYSTEM", os.devnull)

cookiecutter(
str(HERE.parent.parent),
output_dir=tmp_path,
no_input=True,
extra_context={"author_full_name": "Jane Doe", "author_email": "jane@example.com"},
)
proj_dir = tmp_path / "project-name"
assert proj_dir.is_dir()

# The initial commit must exist and be authored by the cookiecutter answers.
author = subprocess.run(
["/usr/bin/git", "-C", str(proj_dir), "log", "-1", "--format=%an <%ae>"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
assert author == "Jane Doe <jane@example.com>"
Loading