diff --git a/api/tangled/repodefs.go b/api/tangled/repodefs.go index f95814521..84e74b610 100644 --- a/api/tangled/repodefs.go +++ b/api/tangled/repodefs.go @@ -8,15 +8,17 @@ const () // RepoDefs_RepoViewBasic is a "repoViewBasic" in the sh.tangled.repo.defs schema. type RepoDefs_RepoViewBasic struct { - CreatedAt string `json:"createdAt" cborgen:"createdAt"` - Description *string `json:"description,omitempty" cborgen:"description,omitempty"` - Did string `json:"did" cborgen:"did"` - ForkCount *int64 `json:"forkCount,omitempty" cborgen:"forkCount,omitempty"` - IsStarred *bool `json:"isStarred,omitempty" cborgen:"isStarred,omitempty"` - Owner *ActorDefs_ProfileViewBasic `json:"owner" cborgen:"owner"` - Slug string `json:"slug" cborgen:"slug"` - StarCount *int64 `json:"starCount,omitempty" cborgen:"starCount,omitempty"` - Viewer *RepoDefs_ViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` + Did string `json:"did" cborgen:"did"` + ForkCount *int64 `json:"forkCount,omitempty" cborgen:"forkCount,omitempty"` + IsStarred *bool `json:"isStarred,omitempty" cborgen:"isStarred,omitempty"` + OpenIssueCount *int64 `json:"openIssueCount,omitempty" cborgen:"openIssueCount,omitempty"` + OpenPullCount *int64 `json:"openPullCount,omitempty" cborgen:"openPullCount,omitempty"` + Owner *ActorDefs_ProfileViewBasic `json:"owner" cborgen:"owner"` + Slug string `json:"slug" cborgen:"slug"` + StarCount *int64 `json:"starCount,omitempty" cborgen:"starCount,omitempty"` + Viewer *RepoDefs_ViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` } // RepoDefs_ViewerState is a "viewerState" in the sh.tangled.repo.defs schema. diff --git a/bobbin/crates/xrpc/src/view.rs b/bobbin/crates/xrpc/src/view.rs index 04b23219b..642b35200 100644 --- a/bobbin/crates/xrpc/src/view.rs +++ b/bobbin/crates/xrpc/src/view.rs @@ -1,11 +1,12 @@ -use bobbin_edge_index::EdgeStore; +use bobbin_edge_index::{EdgeStore, IssueStateKind, PullStatusKind}; use bobbin_types::{ + edges::REPO_SOURCE_EDGE_KIND, ids::{EdgeKey, SubjectRef, nsid_static, owner_did_from_aturi}, sh_tangled::{ actor, feed::star::StarRecord, graph::follow::FollowRecord, - repo::{self, Repo, RepoRecord}, + repo::{self, Repo, RepoRecord, issue::IssueRecord, pull::PullRecord}, }, }; use jacquard_common::{ @@ -117,11 +118,31 @@ pub(crate) async fn build_repo_view_basic_from_record( .map(|r| DefaultStr::from(r.as_ref())) .unwrap_or_default() }); - let star_key = EdgeKey::new( - nsid_static(StarRecord::NSID), - SubjectRef::Did(repo_did.clone()), - ); + let subject = SubjectRef::Did(repo_did.clone()); + let star_key = EdgeKey::new(nsid_static(StarRecord::NSID), subject.clone()); let star_count = state.edges.count(&star_key) as i64; + let fork_count = state.edges.count(&EdgeKey::new( + nsid_static(REPO_SOURCE_EDGE_KIND), + subject.clone(), + )) as i64; + let open_issue_count = state + .edges + .count_issue_state( + &EdgeKey::new(nsid_static(IssueRecord::NSID), subject.clone()), + IssueStateKind::Open, + None, + ) + .count + .get() as i64; + let open_pull_count = state + .edges + .count_pull_status( + &EdgeKey::new(nsid_static(PullRecord::NSID), subject), + PullStatusKind::Open, + None, + ) + .count + .get() as i64; let viewer = viewer.map(|v| { let mut viewer = repo::ViewerState::default(); viewer.star = state.edges.viewer_source(&star_key, v.as_str()); @@ -134,6 +155,9 @@ pub(crate) async fn build_repo_view_basic_from_record( .created_at(repo_record.created_at.clone()) .description(repo_record.description.clone()) .star_count(star_count) + .fork_count(fork_count) + .open_issue_count(open_issue_count) + .open_pull_count(open_pull_count) .viewer(viewer) .build()) } diff --git a/bobbin/crates/xrpc/tests/trending.rs b/bobbin/crates/xrpc/tests/trending.rs index e511b8ccb..6d9014bb5 100644 --- a/bobbin/crates/xrpc/tests/trending.rs +++ b/bobbin/crates/xrpc/tests/trending.rs @@ -239,3 +239,80 @@ async fn get_trending_returns_fixed_viewer_neutral_candidates() { assert_eq!(body["repos"][0]["starCount"], 3); assert!(body["repos"][0]["viewer"].is_null()); } + +#[tokio::test] +async fn trending_repos_count_open_issues_open_pulls_and_forks() { + let harness = Harness::new().await; + let reef = did("did:plc:reef"); + let now = now_micros(); + harness.add_edge( + "sh.tangled.feed.star", + reef.clone(), + "at://did:plc:a/sh.tangled.feed.star/s1", + now, + ); + + let open_issue = at("at://did:plc:a/sh.tangled.repo.issue/i1"); + let closed_issue = at("at://did:plc:b/sh.tangled.repo.issue/i2"); + for issue in [&open_issue, &closed_issue] { + harness.add_edge("sh.tangled.repo.issue", reef.clone(), issue.as_ref(), now); + harness + .edges + .refresh_issue_counts(&harness.state.issue_states, issue); + } + harness.state.issue_states.upsert( + at("at://did:plc:b/sh.tangled.repo.issue.state/c1"), + closed_issue.clone(), + now, + IssueStateKind::Closed, + ); + harness + .edges + .refresh_issue_counts(&harness.state.issue_states, &closed_issue); + + let open_pull = at("at://did:plc:a/sh.tangled.repo.pull/p1"); + let merged_pull = at("at://did:plc:b/sh.tangled.repo.pull/p2"); + for pull in [&open_pull, &merged_pull] { + harness.add_edge("sh.tangled.repo.pull", reef.clone(), pull.as_ref(), now); + harness + .edges + .refresh_pull_counts(&harness.state.pull_statuses, pull); + } + harness.state.pull_statuses.upsert( + at("at://did:plc:b/sh.tangled.repo.pull.status/m1"), + merged_pull.clone(), + now, + PullStatusKind::Merged, + ); + harness + .edges + .refresh_pull_counts(&harness.state.pull_statuses, &merged_pull); + + harness.add_edge( + "sh.tangled.repo.source", + reef.clone(), + "at://did:plc:c/sh.tangled.repo/fork", + now, + ); + harness + .mount_repo(did("did:plc:owner"), rkey("reef"), reef.clone(), "reef") + .await; + + let response = router(harness.state.clone()) + .oneshot( + Request::builder() + .uri("/xrpc/sh.tangled.actor.getTrending") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body: Value = + serde_json::from_slice(&to_bytes(response.into_body(), 1 << 20).await.unwrap()).unwrap(); + let repo = &body["repos"][0]; + assert_eq!(repo["did"], reef.as_ref()); + assert_eq!(repo["openIssueCount"], 1, "{repo}"); + assert_eq!(repo["openPullCount"], 1, "{repo}"); + assert_eq!(repo["forkCount"], 1, "{repo}"); +} diff --git a/lexicons/repo/defs.json b/lexicons/repo/defs.json index fbf628f27..584c2a3e3 100644 --- a/lexicons/repo/defs.json +++ b/lexicons/repo/defs.json @@ -13,6 +13,8 @@ "createdAt": { "type": "string", "format": "datetime" }, "forkCount": { "type": "integer" }, "starCount": { "type": "integer" }, + "openIssueCount": { "type": "integer" }, + "openPullCount": { "type": "integer" }, "viewer": { "type": "ref", "ref": "#viewerState" }, "isStarred": { "type": "boolean" } } diff --git a/web/src/lib/api/lexicons/types/sh/tangled/repo/defs.ts b/web/src/lib/api/lexicons/types/sh/tangled/repo/defs.ts index 321dcc73c..8fd40700f 100644 --- a/web/src/lib/api/lexicons/types/sh/tangled/repo/defs.ts +++ b/web/src/lib/api/lexicons/types/sh/tangled/repo/defs.ts @@ -11,6 +11,8 @@ const _repoViewBasicSchema = /*#__PURE__*/ v.object({ did: /*#__PURE__*/ v.didString(), forkCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), isStarred: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), + openIssueCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), + openPullCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()), get owner() { return ShTangledActorDefs.profileViewBasicSchema; },