Skip to content
Draft
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
28 changes: 20 additions & 8 deletions src/scm/providers/github/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1645,17 +1645,29 @@ def map_file_content(raw: dict[str, Any]) -> FileContent:
)


def map_commit_author(raw_author: dict[str, Any] | None) -> CommitAuthor | None:
if raw_author is None:
def _github_commit_scm_author(raw: dict[str, Any]) -> Author | None:
# Commit responses carry git metadata under commit.author and the linked
# GitHub user at the top level as author (id + login).
return map_author(raw.get("author"))


def map_commit_author(
git_author: dict[str, Any] | None,
*,
scm_author: Author | None = None,
) -> CommitAuthor | None:
"""Map commit.author (git name/email/date) and optional linked SCM user."""
if git_author is None:
return None

raw_date = raw_author.get("date")
raw_date = git_author.get("date")
date = datetime.fromisoformat(raw_date) if raw_date else None

return CommitAuthor(
name=raw_author.get("name", ""),
email=raw_author.get("email", ""),
name=git_author.get("name", ""),
email=git_author.get("email", ""),
date=date,
scm_author=scm_author,
)


Expand Down Expand Up @@ -1698,7 +1710,7 @@ def map_commit(raw: dict[str, Any]) -> Commit:
return Commit(
id=raw["sha"],
message=commit.get("message", ""),
author=map_commit_author(commit.get("author")),
author=map_commit_author(commit.get("author"), scm_author=_github_commit_scm_author(raw)),
additions=stats.get("additions"),
deletions=stats.get("deletions"),
)
Expand All @@ -1710,7 +1722,7 @@ def map_commit_with_changes(raw: dict[str, Any]) -> CommitWithChanges:
return CommitWithChanges(
id=raw["sha"],
message=commit.get("message", ""),
author=map_commit_author(commit.get("author")),
author=map_commit_author(commit.get("author"), scm_author=_github_commit_scm_author(raw)),
files=[map_commit_file(f) for f in raw.get("files", [])],
additions=stats.get("additions"),
deletions=stats.get("deletions"),
Expand Down Expand Up @@ -1781,7 +1793,7 @@ def map_pull_request_commit(raw: dict[str, Any]) -> PullRequestCommit:
return PullRequestCommit(
sha=raw["sha"],
message=raw.get("commit", {}).get("message", ""),
author=map_commit_author(raw_author),
author=map_commit_author(raw_author, scm_author=_github_commit_scm_author(raw)),
)


Expand Down
40 changes: 25 additions & 15 deletions src/scm/providers/gitlab/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1587,16 +1587,34 @@ def map_commit_comparison(raw: dict[str, Any]) -> CommitComparison:
)


def _gitlab_commit_scm_author(raw: dict[str, Any]) -> Author | None:
# Most commit endpoints only populate author_name/author_email; nested
# author (id + username) appears on some responses (e.g. MR commits).
author = raw.get("author")
if not isinstance(author, dict):
return None
author_id = author.get("id")
username = author.get("username")
if author_id is None or username is None:
return None
return map_author(author)


def map_gitlab_commit_author(raw: dict[str, Any], *, date_key: str) -> CommitAuthor:
return CommitAuthor(
name=raw["author_name"],
email=raw["author_email"],
date=datetime.datetime.fromisoformat(raw[date_key]),
scm_author=_gitlab_commit_scm_author(raw),
)


def map_commit(raw: dict[str, Any]) -> Commit:
stats = raw.get("stats") or {}
return Commit(
id=str(raw["id"]),
message=raw["message"],
author=CommitAuthor(
name=raw["author_name"],
email=raw["author_email"],
date=datetime.datetime.fromisoformat(raw["created_at"]),
),
author=map_gitlab_commit_author(raw, date_key="created_at"),
additions=stats.get("additions"),
deletions=stats.get("deletions"),
)
Expand All @@ -1607,11 +1625,7 @@ def map_commit_with_changes(raw: dict[str, Any]) -> CommitWithChanges:
return CommitWithChanges(
id=str(raw["id"]),
message=raw["message"],
author=CommitAuthor(
name=raw["author_name"],
email=raw["author_email"],
date=datetime.datetime.fromisoformat(raw["created_at"]),
),
author=map_gitlab_commit_author(raw, date_key="created_at"),
files=None,
additions=stats.get("additions"),
deletions=stats.get("deletions"),
Expand Down Expand Up @@ -1694,11 +1708,7 @@ def map_pull_request_commit(raw: dict[str, Any]) -> PullRequestCommit:
return PullRequestCommit(
sha=raw["id"],
message=raw["message"],
author=CommitAuthor(
name=raw["author_name"],
email=raw["author_email"],
date=datetime.datetime.fromisoformat(raw["authored_date"]),
),
author=map_gitlab_commit_author(raw, date_key="authored_date"),
)


Expand Down
6 changes: 5 additions & 1 deletion src/scm/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ def make_github_pull_request_commit(
author_email: str = "test@example.com",
author_date: str = "2026-02-04T10:00:00Z",
author_login: str | None = "testuser",
author_id: int = 1,
) -> dict[str, Any]:
"""Factory for GitHub pull request commit API responses."""
result: dict[str, Any] = {
Expand All @@ -380,7 +381,7 @@ def make_github_pull_request_commit(
},
}
if author_login is not None:
result["author"] = {"login": author_login}
result["author"] = {"id": author_id, "login": author_login}
else:
result["author"] = None
return result
Expand Down Expand Up @@ -1114,6 +1115,7 @@ def get_commit(
name="Test User",
email="test@example.com",
date=datetime.fromisoformat("2026-02-04T10:00:00Z"),
scm_author=None,
),
files=[
CommitFile(
Expand Down Expand Up @@ -1211,6 +1213,7 @@ def create_commit(
name="Test User",
email="test@example.com",
date=datetime.fromisoformat("2026-02-04T10:00:00Z"),
scm_author=None,
),
additions=None,
deletions=None,
Expand Down Expand Up @@ -1327,6 +1330,7 @@ def get_pull_request_commits(
name="Test User",
email="test@example.com",
date=datetime.fromisoformat("2026-02-04T10:00:00Z"),
scm_author=Author(id="1", username="testuser"),
),
),
],
Expand Down
7 changes: 7 additions & 0 deletions src/scm/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,16 @@ class FileContent(TypedDict):


class CommitAuthor(TypedDict):
"""Git commit author metadata, optionally linked to an SCM user.

``name``, ``email``, and ``date`` come from the git commit object.
``scm_author`` is the provider user tied to the commit when the API exposes one.
"""

name: str
email: str
date: datetime | None
scm_author: Author | None


class CommitFile(TypedDict):
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/provider/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,17 @@ def expected_file_content(raw: dict[str, Any]) -> dict[str, Any]:
}


def expected_commit_scm_author(raw: dict[str, Any]) -> dict[str, str] | None:
github_user = raw.get("author")
if not isinstance(github_user, dict):
return None
user_id = github_user.get("id")
login = github_user.get("login")
if user_id is None or login is None:
return None
return {"id": str(user_id), "username": login}


def expected_commit(raw: dict[str, Any]) -> dict[str, Any]:
author = raw["commit"]["author"]
stats = raw.get("stats") or {}
Expand All @@ -300,6 +311,7 @@ def expected_commit(raw: dict[str, Any]) -> dict[str, Any]:
"name": author["name"],
"email": author["email"],
"date": datetime.fromisoformat(author["date"]),
"scm_author": expected_commit_scm_author(raw),
},
"additions": stats.get("additions"),
"deletions": stats.get("deletions"),
Expand Down Expand Up @@ -368,6 +380,7 @@ def expected_pull_request_commit(raw: dict[str, Any]) -> dict[str, Any]:
"name": author["name"],
"email": author["email"],
"date": datetime.fromisoformat(author["date"]),
"scm_author": expected_commit_scm_author(raw),
},
}

Expand Down
7 changes: 7 additions & 0 deletions tests/unit/provider/test_gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,7 @@ def _make_mock_response(json_data):
"author": {
"name": "Vincent Jacques",
"email": "vincent@vincent-jacques.net",
"scm_author": None,
"date": datetime.datetime(
2026,
2,
Expand Down Expand Up @@ -1691,6 +1692,7 @@ def _make_mock_response(json_data):
"author": {
"name": "Vincent Jacques",
"email": "vincent@vincent-jacques.net",
"scm_author": None,
"date": datetime.datetime(
2026,
2,
Expand Down Expand Up @@ -10914,6 +10916,7 @@ def _make_mock_response(json_data):
"author": {
"name": "Vincent Jacques",
"email": "vincent@vincent-jacques.net",
"scm_author": None,
"date": datetime.datetime(
2026,
2,
Expand Down Expand Up @@ -11078,6 +11081,7 @@ def _make_mock_response(json_data):
"author": {
"name": "Vincent Jacques",
"email": "vincent@vincent-jacques.net",
"scm_author": None,
"date": datetime.datetime(
2026,
3,
Expand Down Expand Up @@ -11154,6 +11158,7 @@ def _make_mock_response(json_data):
"author": {
"name": "Vincent Jacques",
"email": "vincent@vincent-jacques.net",
"scm_author": None,
"date": datetime.datetime(
2026,
2,
Expand Down Expand Up @@ -11363,6 +11368,7 @@ def _make_mock_response(json_data):
"author": {
"name": "Vincent Jacques",
"email": "vincent@vincent-jacques.net",
"scm_author": None,
"date": datetime.datetime(
2026,
2,
Expand All @@ -11380,6 +11386,7 @@ def _make_mock_response(json_data):
"author": {
"name": "Vincent Jacques",
"email": "vincent@vincent-jacques.net",
"scm_author": None,
"date": datetime.datetime(
2026,
3,
Expand Down
Loading