Summary
parse_repo_name in gittensor/utils/utils.py dereferences repo_data["owner"]["login"] without a null check. When a PR's repository has owner: null — which GitHub's GraphQL API returns when the fork owner's account was deleted (ghost-user scenario) — this raises TypeError: 'NoneType' object is not subscriptable. The crash occurs at the top of PullRequest.from_graphql_response, before any per-PR try/except, so the entire PR is silently dropped from scoring.
Location
https://github.com/entrius/gittensor/blob/test/gittensor/utils/utils.py#L9-L11
python def parse_repo_name(repo_data: Dict): """Normalizes and converts repository name from dict""" return f'{repo_data["owner"]["login"]}/{repo_data["name"]}'.lower()
Impact
Honest-miner harm: any miner whose PR history includes a fork whose owner later deleted their GitHub account has those PRs (and potentially the entire scoring pass for that miner, depending on call site) silently dropped.
Six call sites (all unguarded):
| File |
Line |
Risk |
gittensor/classes.py |
239 |
Primary crash path — top of from_graphql_response, drops PR from scoring |
gittensor/classes.py |
422, 430, 436 |
Log-formatting — shielded in practice by earlier :239 failure |
gittensor/utils/github_api_tools.py |
887 |
head_repo and parse_repo_name(head_repo) — the head_repo truthy check only guards head_repo = None, NOT head_repo['owner'] = None, so still crashes |
gittensor/utils/github_api_tools.py |
983 |
Inside a try: — caught but noisy |
Reproduction
A PR whose headRepository.owner (or repository.owner) is null in GitHub's GraphQL response. This happens when the fork owner's GitHub account has been deleted; GitHub keeps the PR visible but returns null for the repository's owner field.
Why this isn't covered by #537
#537's null-author fix guarded pr_raw['author'] only. The nested repository.owner / headRepository.owner paths were not touched. Same ghost-user pattern, different field.
Suggested fix (~3 lines)
```python
from typing import Optional
def parse_repo_name(repo_data: Dict) -> Optional[str]:
"""Normalizes and converts repository name from dict.
Returns None if owner is missing (e.g. deleted fork-owner account)."""
owner = (repo_data.get('owner') or {}).get('login')
name = repo_data.get('name')
return f'{owner}/{name}'.lower() if owner and name else None
```
Call sites need a None-guard on the result — skip the PR / log and continue rather than propagating None into downstream string comparisons. Happy to open a PR if useful.
Summary
parse_repo_nameingittensor/utils/utils.pydereferencesrepo_data["owner"]["login"]without a null check. When a PR's repository hasowner: null— which GitHub's GraphQL API returns when the fork owner's account was deleted (ghost-user scenario) — this raisesTypeError: 'NoneType' object is not subscriptable. The crash occurs at the top ofPullRequest.from_graphql_response, before any per-PR try/except, so the entire PR is silently dropped from scoring.Location
https://github.com/entrius/gittensor/blob/test/gittensor/utils/utils.py#L9-L11
python def parse_repo_name(repo_data: Dict): """Normalizes and converts repository name from dict""" return f'{repo_data["owner"]["login"]}/{repo_data["name"]}'.lower() Impact
Honest-miner harm: any miner whose PR history includes a fork whose owner later deleted their GitHub account has those PRs (and potentially the entire scoring pass for that miner, depending on call site) silently dropped.
Six call sites (all unguarded):
gittensor/classes.pyfrom_graphql_response, drops PR from scoringgittensor/classes.pygittensor/utils/github_api_tools.pyhead_repo and parse_repo_name(head_repo)— thehead_repotruthy check only guardshead_repo = None, NOThead_repo['owner'] = None, so still crashesgittensor/utils/github_api_tools.pytry:— caught but noisyReproduction
A PR whose
headRepository.owner(orrepository.owner) isnullin GitHub's GraphQL response. This happens when the fork owner's GitHub account has been deleted; GitHub keeps the PR visible but returnsnullfor the repository'sownerfield.Why this isn't covered by #537
#537's null-author fix guarded
pr_raw['author']only. The nestedrepository.owner/headRepository.ownerpaths were not touched. Same ghost-user pattern, different field.Suggested fix (~3 lines)
```python
from typing import Optional
def parse_repo_name(repo_data: Dict) -> Optional[str]:
"""Normalizes and converts repository name from dict.
Returns None if owner is missing (e.g. deleted fork-owner account)."""
owner = (repo_data.get('owner') or {}).get('login')
name = repo_data.get('name')
return f'{owner}/{name}'.lower() if owner and name else None
```
Call sites need a
None-guard on the result — skip the PR / log and continue rather than propagatingNoneinto downstream string comparisons. Happy to open a PR if useful.