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__)
22 22
23 23
24REPOSITORY_LOG_QUERY = """ 24REPOSITORY_LOG_QUERY = """
25query RepositoryLog($username: String!, $repoName: String!, $cursor: Cursor) { 25query RepositoryLog($username: String!, $repoName: String!, $cursor: Cursor, $from: String) {
26 user(username: $username) { 26 user(username: $username) {
27 repository(name: $repoName) { 27 repository(name: $repoName) {
28 name 28 name
29 owner { 29 owner {
30 canonicalName 30 canonicalName
31 } 31 }
32 log(cursor: $cursor) { 32 HEAD {
33 name
34 target
35 }
36 log(cursor: $cursor, from: $from) {
33 results { 37 results {
34 id 38 id
35 shortId 39 shortId
@@ -235,7 +239,12 @@ class GitIngestionService:
235 owner, repo_name = self._split_repository(actor, repository_name) 239 owner, repo_name = self._split_repository(actor, repository_name)
236 data = self.client.execute( 240 data = self.client.execute(
237 REPOSITORY_LOG_QUERY, 241 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 },
239 ) 248 )
240 user = data.get("user") or {} 249 user = data.get("user") or {}
241 repository = user.get("repository") or {} 250 repository = user.get("repository") or {}
@@ -353,7 +362,7 @@ class GitIngestionService:
353 for _ in range(50): 362 for _ in range(50):
354 data = self.client.execute( 363 data = self.client.execute(
355 REPOSITORY_LOG_QUERY, 364 REPOSITORY_LOG_QUERY,
356 {"username": owner, "repoName": repo_name, "cursor": cursor}, 365 {"username": owner, "repoName": repo_name, "cursor": cursor, "from": "HEAD"},
357 ) 366 )
358 user = data.get("user") or {} 367 user = data.get("user") or {}
359 repository = user.get("repository") or {} 368 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:
96 poller = PollerService(todo_service=todo_service, git_service=git_service, settings=settings) 96 poller = PollerService(todo_service=todo_service, git_service=git_service, settings=settings)
97 97
98 first_inserted = poller.poll_all(db_session, "~ccleberg") 98 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]]
99 second_inserted = poller.poll_all(db_session, "~ccleberg") 100 second_inserted = poller.poll_all(db_session, "~ccleberg")
100 101
101 user_repository_calls = [call for call in client.calls if "query UserRepositories" in call[0]] 102 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:
103 104
104 assert first_inserted == 1 105 assert first_inserted == 1
105 assert second_inserted == 0 106 assert second_inserted == 0
106 assert len(user_repository_calls) == 1 107 assert len(user_repository_calls) == len(first_poll_user_repository_calls)
107 assert cached_names == ["~ccleberg/Hutch"] 108 assert cached_names == ["~ccleberg/Hutch"]
tests/test_ingestion.py +52 −3
@@ -210,7 +210,7 @@ def test_todo_ingestion_is_idempotent(db_session) -> None:
210 "results": [ 210 "results": [
211 { 211 {
212 "id": "1001", 212 "id": "1001",
213 "created": "2026-03-29T10:00:00Z", 213 "created": "2026-05-01T10:00:00Z",
214 "ticket": { 214 "ticket": {
215 "id": "123", 215 "id": "123",
216 "ref": "~ccleberg/todo/123", 216 "ref": "~ccleberg/todo/123",
@@ -229,7 +229,7 @@ def test_todo_ingestion_is_idempotent(db_session) -> None:
229 }, 229 },
230 { 230 {
231 "id": "1002", 231 "id": "1002",
232 "created": "2026-03-30T09:00:00Z", 232 "created": "2026-05-02T09:00:00Z",
233 "ticket": { 233 "ticket": {
234 "id": "123", 234 "id": "123",
235 "ref": "~ccleberg/todo/123", 235 "ref": "~ccleberg/todo/123",
@@ -248,7 +248,7 @@ def test_todo_ingestion_is_idempotent(db_session) -> None:
248 }, 248 },
249 { 249 {
250 "id": "1003", 250 "id": "1003",
251 "created": "2026-03-30T10:00:00Z", 251 "created": "2026-05-02T10:00:00Z",
252 "ticket": { 252 "ticket": {
253 "id": "123", 253 "id": "123",
254 "ref": "~ccleberg/todo/123", 254 "ref": "~ccleberg/todo/123",
@@ -507,6 +507,55 @@ def test_git_ingestion_auto_discovers_owned_repositories(db_session) -> None:
507 assert any("query UserRepositories" in call[0] for call in client.calls) 507 assert any("query UserRepositories" in call[0] for call in client.calls)
508 508
509 509
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
510def test_sync_overlap_reuses_cursor_window_and_suppresses_duplicates(db_session) -> None: 559def test_sync_overlap_reuses_cursor_window_and_suppresses_duplicates(db_session) -> None:
511 settings = make_settings() 560 settings = make_settings()
512 event = NormalizedEvent( 561 event = NormalizedEvent(