krz/hutch-stats

Server-side utility for calculating contributions for sourcehut users. contributions server sourcehut stats

Commit fdfb7831fe

fdfb7831fe7dba6d17a1697c7cbb731fd008ba7b

parent: 4390684995

Verified · cmc

cmc <hello@cleberg.net> · 2026-05-04 00:39 UTC

fix: ensure git crawling accounts for all branches

Layout: unified · split

src/srht_contrib/services/git.py +13 −4
@@ -22,14 +22,18 @@ logger = logging.getLogger(__name__)
2222
2323
2424REPOSITORY_LOG_QUERY = """
25query RepositoryLog($username: String!, $repoName: String!, $cursor: Cursor) {
25query RepositoryLog($username: String!, $repoName: String!, $cursor: Cursor, $from: String) {
2626 user(username: $username) {
2727 repository(name: $repoName) {
2828 name
2929 owner {
3030 canonicalName
3131 }
32 log(cursor: $cursor) {
32 HEAD {
33 name
34 target
35 }
36 log(cursor: $cursor, from: $from) {
3337 results {
3438 id
3539 shortId
@@ -235,7 +239,12 @@ class GitIngestionService:
235239 owner, repo_name = self._split_repository(actor, repository_name)
236240 data = self.client.execute(
237241 REPOSITORY_LOG_QUERY,
238 {"username": owner, "repoName": repo_name, "cursor": state["current_repository"]["cursor"]},
242 {
243 "username": owner,
244 "repoName": repo_name,
245 "cursor": state["current_repository"]["cursor"],
246 "from": "HEAD",
247 },
239248 )
240249 user = data.get("user") or {}
241250 repository = user.get("repository") or {}
@@ -353,7 +362,7 @@ class GitIngestionService:
353362 for _ in range(50):
354363 data = self.client.execute(
355364 REPOSITORY_LOG_QUERY,
356 {"username": owner, "repoName": repo_name, "cursor": cursor},
365 {"username": owner, "repoName": repo_name, "cursor": cursor, "from": "HEAD"},
357366 )
358367 user = data.get("user") or {}
359368 repository = user.get("repository") or {}
tests/test_git_repository_cache.py +2 −1
@@ -96,6 +96,7 @@ def test_git_poll_reuses_cached_discovered_repositories(db_session) -> None:
9696 poller = PollerService(todo_service=todo_service, git_service=git_service, settings=settings)
9797
9898 first_inserted = poller.poll_all(db_session, "~ccleberg")
99 first_poll_user_repository_calls = [call for call in client.calls if "query UserRepositories" in call[0]]
99100 second_inserted = poller.poll_all(db_session, "~ccleberg")
100101
101102 user_repository_calls = [call for call in client.calls if "query UserRepositories" in call[0]]
@@ -103,5 +104,5 @@ def test_git_poll_reuses_cached_discovered_repositories(db_session) -> None:
103104
104105 assert first_inserted == 1
105106 assert second_inserted == 0
106 assert len(user_repository_calls) == 1
107 assert len(user_repository_calls) == len(first_poll_user_repository_calls)
107108 assert cached_names == ["~ccleberg/Hutch"]
tests/test_ingestion.py +52 −3
@@ -210,7 +210,7 @@ def test_todo_ingestion_is_idempotent(db_session) -> None:
210210 "results": [
211211 {
212212 "id": "1001",
213 "created": "2026-03-29T10:00:00Z",
213 "created": "2026-05-01T10:00:00Z",
214214 "ticket": {
215215 "id": "123",
216216 "ref": "~ccleberg/todo/123",
@@ -229,7 +229,7 @@ def test_todo_ingestion_is_idempotent(db_session) -> None:
229229 },
230230 {
231231 "id": "1002",
232 "created": "2026-03-30T09:00:00Z",
232 "created": "2026-05-02T09:00:00Z",
233233 "ticket": {
234234 "id": "123",
235235 "ref": "~ccleberg/todo/123",
@@ -248,7 +248,7 @@ def test_todo_ingestion_is_idempotent(db_session) -> None:
248248 },
249249 {
250250 "id": "1003",
251 "created": "2026-03-30T10:00:00Z",
251 "created": "2026-05-02T10:00:00Z",
252252 "ticket": {
253253 "id": "123",
254254 "ref": "~ccleberg/todo/123",
@@ -507,6 +507,55 @@ def test_git_ingestion_auto_discovers_owned_repositories(db_session) -> None:
507507 assert any("query UserRepositories" in call[0] for call in client.calls)
508508
509509
510def test_git_ingestion_reads_repository_log_from_default_head_branch(db_session) -> None:
511 settings = make_settings(
512 ACTOR_ALIASES_JSON={"~ccleberg": ["cmc@example.com", "Chris Cleberg"]},
513 GIT_TRACKED_REPOSITORIES=["Hutch"],
514 )
515 git_payload = {
516 "user": {
517 "repository": {
518 "name": "Hutch",
519 "owner": {"canonicalName": "~ccleberg"},
520 "HEAD": {"name": "refs/heads/trunk", "target": "abc123"},
521 "log": {
522 "results": [
523 {
524 "id": "abc123",
525 "shortId": "abc123",
526 "author": {
527 "name": "Chris Cleberg",
528 "email": "cmc@example.com",
529 "time": "2026-03-30T12:00:00Z",
530 },
531 "committer": {
532 "name": "Chris Cleberg",
533 "email": "cmc@example.com",
534 "time": "2026-03-30T12:00:00Z",
535 },
536 "message": "Commit on non-standard default branch",
537 }
538 ],
539 "cursor": None,
540 },
541 }
542 }
543 }
544 client = StubClient(payloads_by_query={"query RepositoryLog": git_payload})
545 git_service = GitIngestionService(client, settings)
546
547 result = git_service.fetch_recent_events("~ccleberg", since=datetime(2026, 3, 1, tzinfo=UTC))
548
549 repository_log_calls = [call for call in client.calls if "query RepositoryLog" in call[0]]
550 assert len(result.events) == 1
551 assert repository_log_calls[0][1] == {
552 "username": "ccleberg",
553 "repoName": "Hutch",
554 "cursor": None,
555 "from": "HEAD",
556 }
557
558
510559def test_sync_overlap_reuses_cursor_window_and_suppresses_duplicates(db_session) -> None:
511560 settings = make_settings()
512561 event = NormalizedEvent(