Skip to content

Identity

identity

Git identity resolution with schedule-based profile switching.

GitIdentity

Bases: BaseModel

A git author identity.

Source code in packages/axm-git/src/axm_git/core/identity.py
Python
class GitIdentity(BaseModel):  # type: ignore[explicit-any]  # pydantic BaseModel exposes Any in its API
    """A git author identity."""

    name: str
    email: str

GitProfileConfig

Bases: BaseModel

Full git-profiles.toml configuration.

Source code in packages/axm-git/src/axm_git/core/identity.py
Python
class GitProfileConfig(BaseModel):  # type: ignore[explicit-any]  # pydantic BaseModel exposes Any in its API
    """Full git-profiles.toml configuration."""

    default: GitIdentity
    profiles: dict[str, GitIdentity] = {}
    schedule: Schedule = Schedule()
    workspace_paths: list[Path] = []
    timezone: str = "Europe/Paris"

Schedule

Bases: BaseModel

Schedule configuration with rules.

Source code in packages/axm-git/src/axm_git/core/identity.py
Python
class Schedule(BaseModel):  # type: ignore[explicit-any]  # pydantic BaseModel exposes Any in its API
    """Schedule configuration with rules."""

    enabled: bool = True
    rules: list[ScheduleRule] = []

ScheduleRule

Bases: BaseModel

A time-based rule mapping to a profile.

days/start/end are validated at construction so a config typo ("monday" instead of "mon", "9h00" instead of "09:00") is caught by model_validate — which _load_from_file already degrades to None + WARNING — rather than raising a raw KeyError/ValueError deep inside _matches_schedule and crashing every downstream git_commit.

Source code in packages/axm-git/src/axm_git/core/identity.py
Python
class ScheduleRule(BaseModel):  # type: ignore[explicit-any]  # pydantic BaseModel exposes Any in its API
    """A time-based rule mapping to a profile.

    ``days``/``start``/``end`` are validated at construction so a config
    typo (``"monday"`` instead of ``"mon"``, ``"9h00"`` instead of
    ``"09:00"``) is caught by ``model_validate`` — which ``_load_from_file``
    already degrades to ``None`` + WARNING — rather than raising a raw
    ``KeyError``/``ValueError`` deep inside ``_matches_schedule`` and
    crashing every downstream ``git_commit``.
    """

    profile: str
    days: list[str]
    start: str
    end: str

    @field_validator("days")
    @classmethod
    def _check_days(cls, value: list[str]) -> list[str]:
        """Reject any day token not in the ``mon``…``sun`` vocabulary."""
        unknown = [d for d in value if d not in _DAY_MAP]
        if unknown:
            valid = ", ".join(_DAY_MAP)
            msg = f"Invalid schedule day(s) {unknown!r}; use one of: {valid}"
            raise ValueError(msg)
        return value

    @field_validator("start", "end")
    @classmethod
    def _check_time(cls, value: str) -> str:
        """Reject a ``start``/``end`` that ``time.fromisoformat`` can't parse."""
        try:
            time.fromisoformat(value)
        except ValueError as exc:
            msg = f"Invalid schedule time {value!r} (expected HH:MM): {exc}"
            raise ValueError(msg) from exc
        return value

author_args(identity)

Build --author arguments for a git command.

Source code in packages/axm-git/src/axm_git/core/identity.py
Python
def author_args(identity: GitIdentity | None) -> list[str]:
    """Build ``--author`` arguments for a git command."""
    if identity is None:
        return []
    return ["--author", f"{identity.name} <{identity.email}>"]

load_config(config_path=None)

Load and validate git-profiles configuration.

With an explicit config_path, parse that exact TOML file (unchanged legacy form). With config_path=None (the default), resolve from the axm_config single store [git] section, falling back to the legacy ~/axm/git-profiles.toml (with a migration WARNING) only while the store has no [git] section. Returns None when no config is resolvable anywhere.

Source code in packages/axm-git/src/axm_git/core/identity.py
Python
def load_config(config_path: Path | None = None) -> GitProfileConfig | None:
    """Load and validate git-profiles configuration.

    With an explicit *config_path*, parse that exact TOML file (unchanged
    legacy form). With ``config_path=None`` (the default), resolve from the
    ``axm_config`` single store ``[git]`` section, falling back to the legacy
    ``~/axm/git-profiles.toml`` (with a migration ``WARNING``) only while the
    store has no ``[git]`` section. Returns ``None`` when no config is
    resolvable anywhere.
    """
    if config_path is not None:
        return _load_from_file(config_path)
    from_store = _load_from_store()
    if from_store is not None:
        return from_store
    legacy_path = _legacy_config_path()
    legacy = _load_from_file(legacy_path)
    if legacy is not None:
        logger.warning(
            "Loaded git-profiles from legacy %s — migrate to the axm_config "
            "[git] section (see `axm-config`); the legacy file is transitional",
            legacy_path,
        )
    return legacy

resolve_by_override(config, profile_override)

Resolve identity from an explicit profile override.

Returns the matching identity, or None when profile_override is None or names an unknown profile.

Source code in packages/axm-git/src/axm_git/core/identity.py
Python
def resolve_by_override(
    config: GitProfileConfig,
    profile_override: str | None,
) -> GitIdentity | None:
    """Resolve identity from an explicit profile override.

    Returns the matching identity, or ``None`` when *profile_override*
    is ``None`` or names an unknown profile.
    """
    if profile_override is None:
        return None
    if profile_override == "default":
        return config.default
    identity = config.profiles.get(profile_override)
    if identity is None:
        _warn_unknown_profile(config, profile_override)
    return identity

resolve_by_schedule(config, workspace_path, now)

Resolve identity from schedule rules for AXM workspaces.

Returns None when the schedule is disabled, the path is outside AXM workspaces, or no schedule rule matches.

Source code in packages/axm-git/src/axm_git/core/identity.py
Python
def resolve_by_schedule(
    config: GitProfileConfig,
    workspace_path: Path,
    now: datetime,
) -> GitIdentity | None:
    """Resolve identity from schedule rules for AXM workspaces.

    Returns ``None`` when the schedule is disabled, the path is outside
    AXM workspaces, or no schedule rule matches.
    """
    if not config.schedule.enabled:
        return None
    if not _is_axm_workspace(workspace_path, config.workspace_paths):
        return None
    for rule in config.schedule.rules:
        if _matches_schedule(rule, now) and rule.profile in config.profiles:
            return config.profiles[rule.profile]
    return None

resolve_identity(workspace_path, *, now=None, profile_override=None, config_path=None)

Resolve the git identity for the given workspace.

Returns None when no config is available or an unknown profile is requested via profile_override. An unknown profile_override (a typo, or a request against an empty profile set) emits a WARNING naming the requested profile and the available ones before falling back to None — observability, not a hard failure.

Source code in packages/axm-git/src/axm_git/core/identity.py
Python
def resolve_identity(
    workspace_path: Path,
    *,
    now: datetime | None = None,
    profile_override: str | None = None,
    config_path: Path | None = None,
) -> GitIdentity | None:
    """Resolve the git identity for the given workspace.

    Returns ``None`` when no config is available or an unknown profile
    is requested via *profile_override*. An unknown *profile_override*
    (a typo, or a request against an empty profile set) emits a
    ``WARNING`` naming the requested profile and the available ones
    before falling back to ``None`` — observability, not a hard failure.
    """
    config = load_config(config_path)
    if config is None:
        return None

    override = resolve_by_override(config, profile_override)
    if profile_override is not None:
        return override

    tz = ZoneInfo(config.timezone)
    if now is None:
        effective_now = datetime.now(tz=tz)
    elif now.tzinfo is None:
        effective_now = now
    else:
        effective_now = now.astimezone(tz)
    return resolve_by_schedule(config, workspace_path, effective_now) or config.default