relay_knowledge/application/code_repository/repository_set/status/
mod.rs1use crate::{
4 api::{ApiError, ApiMetadata, CodeRepositorySetStatusResponse, RequestContext},
5 application::service::RelayKnowledgeService,
6 domain::{CodeRepositorySelector, CodeRepositorySetMember, CodeRepositorySetStatus},
7};
8
9use super::{
10 super::scope::resolve_code_ref_for_selector, errors::storage_api_error,
11 member_freshness::refresh_fact_version_member_freshness,
12};
13
14impl RelayKnowledgeService {
15 pub async fn code_repository_set_status(
17 &self,
18 set_alias: String,
19 context: RequestContext,
20 ) -> Result<CodeRepositorySetStatusResponse, ApiError> {
21 let store = self.store().await.map_err(storage_api_error)?;
22 let status = required_set_status(&store, &set_alias).await?;
23 let graph_version = store
24 .current_graph_version()
25 .await
26 .map_err(storage_api_error)?;
27
28 Ok(CodeRepositorySetStatusResponse {
29 metadata: ApiMetadata::graph_only(&context, graph_version),
30 status,
31 })
32 }
33}
34
35pub(super) async fn required_set_status(
36 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
37 set_alias: &str,
38) -> Result<CodeRepositorySetStatus, ApiError> {
39 refreshed_required_set_status(store, set_alias)
40 .await
41 .map(|(status, _)| status)
42}
43
44pub(super) async fn refreshed_required_set_status(
45 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
46 set_alias: &str,
47) -> Result<(CodeRepositorySetStatus, Vec<CodeRepositorySetMember>), ApiError> {
48 let mut status = store
49 .code_repository_set_status(set_alias.to_owned())
50 .await
51 .map_err(storage_api_error)?
52 .ok_or_else(|| {
53 ApiError::invalid_argument(format!(
54 "code repository set '{set_alias}' is not registered"
55 ))
56 })?;
57 let fact_version_replacements =
58 refresh_fact_version_member_freshness(store, &mut status).await?;
59 refresh_moving_member_freshness(store, &mut status).await?;
60 refresh_repository_set_freshness(&mut status);
61
62 Ok((status, fact_version_replacements))
63}
64
65async fn refresh_moving_member_freshness(
66 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
67 status: &mut CodeRepositorySetStatus,
68) -> Result<(), ApiError> {
69 for index in 0..status.members.len() {
70 let member = status.members[index].member.clone();
71 let Some(reason) = moving_member_stale_reason(store, &member).await? else {
72 continue;
73 };
74 status.members[index].stale = true;
75 status.members[index].freshness_state = "stale".to_owned();
76 status.members[index].degraded_reason = Some(reason);
77 }
78
79 Ok(())
80}
81
82async fn moving_member_stale_reason(
83 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
84 member: &crate::domain::CodeRepositorySetMember,
85) -> Result<Option<String>, ApiError> {
86 if !member_ref_tracks_repository(&member.ref_selector, &member.resolved_commit_sha) {
87 return Ok(None);
88 }
89 let repository = store
90 .code_repository_status(member.repository_id.clone())
91 .await
92 .map_err(storage_api_error)?
93 .ok_or_else(|| {
94 ApiError::invalid_argument(format!(
95 "code repository '{}' is not registered",
96 member.repository_alias
97 ))
98 })?;
99 let ref_selector = member.ref_selector.clone();
100 let selector = CodeRepositorySelector {
101 repository: member.repository_alias.clone(),
102 ref_selector: ref_selector.clone(),
103 path_filters: member.path_filters.clone(),
104 language_filters: member.language_filters.clone(),
105 };
106 let resolved = resolve_code_ref_for_selector(&repository, &selector, ref_selector).await;
107
108 match resolved {
109 Ok(current_commit) if current_commit == member.resolved_commit_sha => Ok(None),
110 Ok(current_commit) => Ok(Some(format!(
111 "repository set member '{}' ref '{}' now resolves to {}, not stored snapshot {}",
112 member.repository_alias,
113 member.ref_selector,
114 current_commit,
115 member.resolved_commit_sha
116 ))),
117 Err(error) => Ok(Some(format!(
118 "repository set member '{}' ref '{}' could not be resolved: {error}",
119 member.repository_alias,
120 member.ref_selector,
121 error = error.message
122 ))),
123 }
124}
125
126fn member_ref_tracks_repository(ref_selector: &str, resolved_commit_sha: &str) -> bool {
127 let ref_selector = ref_selector.trim();
128 !(ref_selector == resolved_commit_sha
129 || (is_git_oid_prefix(ref_selector) && resolved_commit_sha.starts_with(ref_selector)))
130}
131
132fn is_git_oid_prefix(value: &str) -> bool {
133 (7..=64).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
134}
135
136fn refresh_repository_set_freshness(status: &mut CodeRepositorySetStatus) {
137 let member_stale = status.members.iter().any(|member| member.stale);
138 if member_stale && !status.overlay.stale {
139 status.overlay.stale = true;
140 status.overlay.state = "overlay_stale".to_owned();
141 }
142 status.freshness_state = if status.members.is_empty() {
143 "incomplete"
144 } else if member_stale {
145 "stale"
146 } else if status.overlay.stale {
147 "overlay_stale"
148 } else {
149 "fresh"
150 }
151 .to_owned();
152 status.degraded_reason = status
153 .members
154 .iter()
155 .find_map(|member| member.degraded_reason.clone())
156 .or_else(|| status.overlay.degraded_reason.clone());
157}
158
159#[cfg(test)]
160#[path = "mod_tests.rs"]
161mod tests;