From 3f50107062d55a15decc47e93fd4e9f473de86e8 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 5 Jan 2026 14:54:29 +0000 Subject: sync all repos when authorised state data push received --- src/git/handlers.rs | 38 ++++- src/git/mod.rs | 1 + src/git/sync.rs | 453 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/http/mod.rs | 1 + src/purgatory/mod.rs | 344 +------------------------------------- 5 files changed, 497 insertions(+), 340 deletions(-) create mode 100644 src/git/sync.rs (limited to 'src') diff --git a/src/git/handlers.rs b/src/git/handlers.rs index 2930852..e86d2a3 100644 --- a/src/git/handlers.rs +++ b/src/git/handlers.rs @@ -15,7 +15,8 @@ use super::protocol::{GitService, PktLine}; use super::subprocess::GitSubprocess; use super::try_set_head_if_available; -use crate::git::authorization::authorize_push; +use crate::git::authorization::{authorize_push, fetch_repository_data}; +use crate::git::sync::sync_to_owner_repos; use crate::nostr::builder::SharedDatabase; use crate::nostr::events::{KIND_PR, KIND_PR_UPDATE, KIND_REPOSITORY_STATE}; use crate::purgatory::Purgatory; @@ -180,6 +181,7 @@ pub async fn handle_upload_pack( /// * `database` - Database reference for authorization queries /// * `identifier` - The repository identifier (d tag) for authorization lookup /// * `owner_pubkey` - The owner's public key (hex) from the URL path, scoping authorization +/// * `git_data_path` - Base path for git repositories (for syncing to other owner repos) pub async fn handle_receive_pack( repo_path: PathBuf, request_body: Bytes, @@ -188,6 +190,7 @@ pub async fn handle_receive_pack( identifier: &str, owner_pubkey: &str, purgatory: Arc, + git_data_path: &str, ) -> Result>, GitError> { debug!("Handling receive-pack for {:?}", repo_path); @@ -347,7 +350,38 @@ pub async fn handle_receive_pack( } // TODO figure out what atomic pushes look like in GRASP (we cant accepted differnte state events changing different branches at the same time) - // TODO sync git data to other repos that these events authorise. + + // Sync git data to other owner repositories that authorize the same state event + // This ensures all owners who share maintainers get the same git data + if let Some(ref state) = auth_result.state { + // Fetch repository data for sync + match fetch_repository_data(&database, identifier).await { + Ok(db_repo_data) => { + let git_data_path_buf = std::path::PathBuf::from(git_data_path); + let sync_result = + sync_to_owner_repos(&repo_path, state, &db_repo_data, &git_data_path_buf); + + if sync_result.repos_synced > 0 { + info!( + "Synced git data to {} other owner repositories for {}", + sync_result.repos_synced, identifier + ); + } + + if !sync_result.errors.is_empty() { + for (repo, error) in &sync_result.errors { + warn!("Error syncing to {}: {}", repo, error); + } + } + } + Err(e) => { + warn!( + "Failed to fetch repository data for sync after push to {}: {}", + identifier, e + ); + } + } + } Ok(Response::builder() .status(StatusCode::OK) diff --git a/src/git/mod.rs b/src/git/mod.rs index d34f98b..fb17c53 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -21,6 +21,7 @@ pub mod authorization; pub mod handlers; pub mod protocol; pub mod subprocess; +pub mod sync; use std::path::{Path, PathBuf}; use std::process::Command; diff --git a/src/git/sync.rs b/src/git/sync.rs new file mode 100644 index 0000000..c99eb43 --- /dev/null +++ b/src/git/sync.rs @@ -0,0 +1,453 @@ +//! Git Data Synchronization Across Owner Repositories +//! +//! This module provides functions to sync git data across multiple owner repositories +//! that are authorized by the same state event. This is used when: +//! +//! 1. A push is received that satisfies a state event - the git data needs to be +//! copied to other owner repos that authorize the same state +//! 2. Purgatory sync fetches git data from remote - needs to distribute to all +//! authorized owner repos +//! +//! ## Architecture +//! +//! The key insight is that multiple owners can have announcements for the same +//! repository identifier, and they may share maintainers. When a state event +//! authorizes a push, that push should be reflected in ALL owner repositories +//! that would authorize the same state. + +use std::collections::HashMap; +use std::path::Path; +use std::process::Command; +use tracing::{debug, info, warn}; + +use crate::git::{self, oid_exists}; +use crate::git::authorization::{collect_authorized_maintainers, RepositoryData}; +use crate::nostr::events::RepositoryState; + +/// Result of syncing git data to owner repositories +#[derive(Debug, Default)] +pub struct SyncResult { + /// Number of repositories synced + pub repos_synced: usize, + /// Number of refs created across all repos + pub refs_created: usize, + /// Number of refs updated across all repos + pub refs_updated: usize, + /// Number of refs deleted across all repos + pub refs_deleted: usize, + /// Number of repositories where HEAD was set + pub heads_set: usize, + /// Errors encountered (repo path -> error message) + pub errors: Vec<(String, String)>, +} + +/// Result of aligning a single repository with state +#[derive(Debug, Default)] +pub struct AlignmentResult { + /// Number of refs created + pub refs_created: usize, + /// Number of refs updated + pub refs_updated: usize, + /// Number of refs deleted + pub refs_deleted: usize, + /// Whether HEAD was set + pub head_set: bool, +} + +/// Sync git data from a source repository to all other owner repositories +/// that authorize the given state event. +/// +/// This function: +/// 1. Collects all authorized maintainers per owner from announcements +/// 2. For each owner whose maintainer set authorizes the state author: +/// - Skips if a newer state already exists for that owner +/// - Copies missing OIDs from source repo to target repo +/// - Aligns refs with the state +/// +/// # Arguments +/// * `source_repo_path` - Path to the repository that has the git data +/// * `state` - The repository state event that authorized the push +/// * `db_repo_data` - Repository data from database (announcements + states) +/// * `git_data_path` - Base path for git repositories +/// +/// # Returns +/// A `SyncResult` with statistics about what was synced +pub fn sync_to_owner_repos( + source_repo_path: &Path, + state: &RepositoryState, + db_repo_data: &RepositoryData, + git_data_path: &Path, +) -> SyncResult { + let mut result = SyncResult::default(); + + // Collect authorized maintainers per owner + let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); + let state_author = state.event.pubkey.to_hex(); + + debug!( + identifier = %state.identifier, + owners = by_owner.len(), + "Syncing git data to owner repositories" + ); + + for (owner, maintainers) in &by_owner { + // Check if this state's author is authorized for this owner + if !maintainers.contains(&state_author) { + debug!( + identifier = %state.identifier, + owner = %owner, + "Skipping owner - state author not in maintainer set" + ); + continue; + } + + // Find the previous latest state for this owner's maintainer set + let previous_state = db_repo_data + .states + .iter() + .filter(|s| maintainers.contains(&s.event.pubkey.to_hex())) + .max_by_key(|s| s.event.created_at); + + // Only update if this state is newer than any existing state + // TODO: in event of a tie, the event with the biggest event id wins + if let Some(prev) = previous_state { + if state.event.created_at <= prev.event.created_at { + debug!( + identifier = %state.identifier, + owner = %owner, + "Skipping owner - existing state is newer or equal" + ); + continue; + } + } + + // Find the announcement for this owner + let announcement = db_repo_data + .announcements + .iter() + .find(|a| a.event.pubkey.to_hex() == *owner); + + let Some(announcement) = announcement else { + continue; + }; + + let target_repo_path = git_data_path.join(announcement.repo_path()); + + if !target_repo_path.exists() { + // Repository doesn't exist (e.g., announcement doesn't list this service) + debug!( + identifier = %state.identifier, + owner = %owner, + repo_path = %target_repo_path.display(), + "Skipping owner - repository doesn't exist" + ); + continue; + } + + // Copy missing OIDs from source repo to target repo if different + if target_repo_path != source_repo_path { + if let Err(e) = copy_missing_oids_between_repos(source_repo_path, &target_repo_path, state) + { + warn!( + identifier = %state.identifier, + source = %source_repo_path.display(), + target = %target_repo_path.display(), + error = %e, + "Failed to copy OIDs between repos" + ); + result.errors.push((target_repo_path.display().to_string(), e)); + // Continue anyway - we'll try to align what we can + } + } + + // Align refs with state + let align_result = align_repository_with_state(&target_repo_path, state); + result.repos_synced += 1; + result.refs_created += align_result.refs_created; + result.refs_updated += align_result.refs_updated; + result.refs_deleted += align_result.refs_deleted; + if align_result.head_set { + result.heads_set += 1; + } + + info!( + identifier = %state.identifier, + owner = %owner, + repo_path = %target_repo_path.display(), + refs_created = align_result.refs_created, + refs_updated = align_result.refs_updated, + refs_deleted = align_result.refs_deleted, + head_set = align_result.head_set, + "Aligned repository with state" + ); + } + + info!( + identifier = %state.identifier, + repos_synced = result.repos_synced, + refs_created = result.refs_created, + refs_updated = result.refs_updated, + refs_deleted = result.refs_deleted, + heads_set = result.heads_set, + "Completed git data sync to owner repositories" + ); + + result +} + +/// Copy missing OIDs from a source repository to a target repository. +/// +/// Identifies commits referenced in the state that are missing from the target +/// repository and copies them from the source repository using git fetch. +pub fn copy_missing_oids_between_repos( + source_repo: &Path, + target_repo: &Path, + state: &RepositoryState, +) -> Result<(), String> { + // Collect all commits referenced in the state + let mut commits_to_check = Vec::new(); + + for branch in &state.branches { + if !branch.commit.starts_with("ref: ") { + commits_to_check.push(&branch.commit); + } + } + + for tag in &state.tags { + if !tag.commit.starts_with("ref: ") { + commits_to_check.push(&tag.commit); + } + } + + // Identify missing commits + let mut missing_commits = Vec::new(); + for commit in commits_to_check { + if !oid_exists(target_repo, commit) { + missing_commits.push(commit); + } + } + + if missing_commits.is_empty() { + debug!( + "No missing commits to copy from {} to {}", + source_repo.display(), + target_repo.display() + ); + return Ok(()); + } + + info!( + "Copying {} missing commits from {} to {}", + missing_commits.len(), + source_repo.display(), + target_repo.display() + ); + + // Fetch each missing commit from source to target + for commit in &missing_commits { + let output = Command::new("git") + .args([ + "fetch", + source_repo.to_str().ok_or("Invalid source path")?, + commit, + ]) + .current_dir(target_repo) + .output() + .map_err(|e| format!("Failed to execute git fetch: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!( + "git fetch failed for commit {}: {}", + commit, stderr + )); + } + + debug!("Copied commit {} to {}", commit, target_repo.display()); + } + + Ok(()) +} + +/// Align a repository's refs with the authorized state. +/// +/// This function: +/// 1. Deletes refs that are in the repo but not in the state (for refs/heads/ and refs/tags/) +/// 2. Updates refs that exist in state if we have the commit +/// 3. Sets HEAD if the HEAD branch's commit is available +pub fn align_repository_with_state(repo_path: &Path, state: &RepositoryState) -> AlignmentResult { + let mut result = AlignmentResult::default(); + + // Check if repository exists + if !repo_path.exists() { + debug!( + "Repository not found at {}, cannot align with state", + repo_path.display() + ); + return result; + } + + // Get current refs from the repository + let current_refs = match git::list_refs(repo_path) { + Ok(refs) => refs, + Err(e) => { + warn!("Failed to list refs in {}: {}", repo_path.display(), e); + return result; + } + }; + + // Build expected refs from state + let mut expected_refs: HashMap = HashMap::new(); + + for branch in &state.branches { + let ref_name = format!("refs/heads/{}", branch.name); + expected_refs.insert(ref_name, branch.commit.clone()); + } + + for tag in &state.tags { + let ref_name = format!("refs/tags/{}", tag.name); + expected_refs.insert(ref_name, tag.commit.clone()); + } + + // Delete refs that exist in repo but not in state (only for refs/heads/ and refs/tags/) + for (ref_name, _current_commit) in ¤t_refs { + if (ref_name.starts_with("refs/heads/") || ref_name.starts_with("refs/tags/")) + && !expected_refs.contains_key(ref_name) + { + match git::delete_ref(repo_path, ref_name) { + Ok(()) => { + info!( + "Deleted {} from {} (not in state)", + ref_name, + repo_path.display() + ); + result.refs_deleted += 1; + } + Err(e) => { + warn!( + "Failed to delete {} from {}: {}", + ref_name, + repo_path.display(), + e + ); + } + } + } + } + + // Update refs that exist in state (if we have the commit) + for (ref_name, expected_commit) in &expected_refs { + // Skip symbolic refs + if expected_commit.starts_with("ref: ") { + continue; + } + + // Check if we have the commit + if !git::oid_exists(repo_path, expected_commit) { + debug!( + "Commit {} not available for {} in {}", + expected_commit, + ref_name, + repo_path.display() + ); + continue; + } + + // Check current value + let current_commit = current_refs + .iter() + .find(|(r, _)| r == ref_name) + .map(|(_, c)| c.as_str()); + + if current_commit == Some(expected_commit.as_str()) { + // Already correct + continue; + } + + // Update or create the ref + match git::update_ref(repo_path, ref_name, expected_commit) { + Ok(()) => { + if current_commit.is_some() { + info!( + "Updated {} to {} in {}", + ref_name, + expected_commit, + repo_path.display() + ); + result.refs_updated += 1; + } else { + info!( + "Created {} at {} in {}", + ref_name, + expected_commit, + repo_path.display() + ); + result.refs_created += 1; + } + } + Err(e) => { + warn!( + "Failed to update {} in {}: {}", + ref_name, + repo_path.display(), + e + ); + } + } + } + + // Set HEAD if specified in state + if let Some(head_ref) = &state.head { + if let Some(branch_name) = state.get_head_branch() { + if let Some(head_commit) = state.get_branch_commit(branch_name) { + match git::try_set_head_if_available(repo_path, head_ref, head_commit) { + Ok(true) => { + info!( + "Set HEAD to {} in {}", + head_ref, + repo_path.display() + ); + result.head_set = true; + } + Ok(false) => { + debug!( + "HEAD commit {} not available yet in {}", + head_commit, + repo_path.display() + ); + } + Err(e) => { + warn!("Failed to set HEAD in {}: {}", repo_path.display(), e); + } + } + } + } + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sync_result_default() { + let result = SyncResult::default(); + assert_eq!(result.repos_synced, 0); + assert_eq!(result.refs_created, 0); + assert_eq!(result.refs_updated, 0); + assert_eq!(result.refs_deleted, 0); + assert_eq!(result.heads_set, 0); + assert!(result.errors.is_empty()); + } + + #[test] + fn test_alignment_result_default() { + let result = AlignmentResult::default(); + assert_eq!(result.refs_created, 0); + assert_eq!(result.refs_updated, 0); + assert_eq!(result.refs_deleted, 0); + assert!(!result.head_set); + } +} diff --git a/src/http/mod.rs b/src/http/mod.rs index 10563da..e2caf5d 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -237,6 +237,7 @@ impl Service> for HttpService { &identifier, &owner_pubkey_hex, purgatory.clone(), + &git_data_path, ) .await; diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs index f15d6bd..88377fb 100644 --- a/src/purgatory/mod.rs +++ b/src/purgatory/mod.rs @@ -26,11 +26,9 @@ use std::process::Command; use std::sync::Arc; use std::time::{Duration, Instant}; -use crate::git::authorization::{ - collect_authorized_maintainers, fetch_repository_data, pubkey_authorised_for_repo_owners, - RepositoryData, -}; +use crate::git::authorization::{fetch_repository_data, pubkey_authorised_for_repo_owners, RepositoryData}; use crate::git::oid_exists; +use crate::git::sync::sync_to_owner_repos; use crate::nostr::builder::SharedDatabase; use crate::nostr::events::RepositoryState; @@ -596,96 +594,14 @@ async fn sync_state_git_data( } // Now that we have all OIDs, sync to other owner repositories and align refs - let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); - let mut repo_count = 0; - - for (owner, maintainers) in &by_owner { - // Check if this state's author is authorized for this owner - if !maintainers.contains(&state.event.pubkey.to_hex()) { - continue; - } - - // Find the previous latest state for this owner's maintainer set - let previous_state = db_repo_data - .states - .iter() - .filter(|s| maintainers.contains(&s.event.pubkey.to_hex())) - .max_by_key(|s| s.event.created_at); - - // Only update if this state is newer than any existing state - // TODO: in event of a tie, the event with the biggest event id wins - if let Some(prev) = previous_state { - if state.event.created_at <= prev.event.created_at { - tracing::debug!( - identifier = %state.identifier, - owner = %owner, - "Skipping owner - existing state is newer or equal" - ); - continue; - } - } - - // Find the announcement for this owner - let announcement = db_repo_data - .announcements - .iter() - .find(|a| a.event.pubkey.to_hex() == *owner); - - let Some(announcement) = announcement else { - continue; - }; - - let target_repo_path = git_data_path.join(announcement.repo_path()); - - if !target_repo_path.exists() { - // Repository doesn't exist (e.g., announcement doesn't list this service) - tracing::debug!( - identifier = %state.identifier, - owner = %owner, - repo_path = %target_repo_path.display(), - "Skipping owner - repository doesn't exist" - ); - continue; - } - - // Copy missing OIDs from source repo to target repo if different - if target_repo_path != source_repo_path { - if let Err(e) = - copy_missing_oids_between_repos(&source_repo_path, &target_repo_path, &state) - { - tracing::warn!( - identifier = %state.identifier, - source = %source_repo_path.display(), - target = %target_repo_path.display(), - error = %e, - "Failed to copy OIDs between repos" - ); - // Continue anyway - we'll try to align what we can - } - } - - // Align refs with state - let result = align_repository_with_state(&target_repo_path, &state); - repo_count += 1; - - tracing::info!( - identifier = %state.identifier, - owner = %owner, - repo_path = %target_repo_path.display(), - refs_created = result.refs_created, - refs_updated = result.refs_updated, - refs_deleted = result.refs_deleted, - head_set = result.head_set, - "Aligned repository with state from purgatory sync" - ); - } + let sync_result = sync_to_owner_repos(&source_repo_path, &state, &db_repo_data, git_data_path); tracing::info!( identifier = %state.identifier, event_id = %state.event.id, - repo_count = repo_count, - "Synced git data and aligned {} repositories", - repo_count + repos_synced = sync_result.repos_synced, + "Synced git data and aligned {} repositories from purgatory", + sync_result.repos_synced ); // Save state event to database @@ -737,254 +653,6 @@ async fn sync_state_git_data( Ok(()) } -/// Copy missing OIDs from a source repository to a target repository. -/// -/// Identifies commits referenced in the state that are missing from the target -/// repository and copies them from the source repository using git fetch. -fn copy_missing_oids_between_repos( - source_repo: &Path, - target_repo: &Path, - state: &RepositoryState, -) -> Result<(), String> { - // Collect all commits referenced in the state - let mut commits_to_check = Vec::new(); - - for branch in &state.branches { - if !branch.commit.starts_with("ref: ") { - commits_to_check.push(&branch.commit); - } - } - - for tag in &state.tags { - if !tag.commit.starts_with("ref: ") { - commits_to_check.push(&tag.commit); - } - } - - // Identify missing commits - let mut missing_commits = Vec::new(); - for commit in commits_to_check { - if !oid_exists(target_repo, commit) { - missing_commits.push(commit); - } - } - - if missing_commits.is_empty() { - tracing::debug!( - "No missing commits to copy from {} to {}", - source_repo.display(), - target_repo.display() - ); - return Ok(()); - } - - tracing::info!( - "Copying {} missing commits from {} to {}", - missing_commits.len(), - source_repo.display(), - target_repo.display() - ); - - // Fetch each missing commit from source to target - for commit in &missing_commits { - let output = Command::new("git") - .args([ - "fetch", - source_repo.to_str().ok_or("Invalid source path")?, - commit, - ]) - .current_dir(target_repo) - .output() - .map_err(|e| format!("Failed to execute git fetch: {}", e))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!( - "git fetch failed for commit {}: {}", - commit, stderr - )); - } - - tracing::debug!("Copied commit {} to {}", commit, target_repo.display()); - } - - Ok(()) -} - -/// Result of aligning a repository with authorized state -#[derive(Debug, Default)] -struct SyncAlignmentResult { - /// Number of refs created - refs_created: usize, - /// Number of refs updated - refs_updated: usize, - /// Number of refs deleted - refs_deleted: usize, - /// Whether HEAD was set - head_set: bool, -} - -/// Align a repository's refs with the authorized state. -/// -/// This function: -/// 1. Deletes refs that are in the repo but not in the state (for refs/heads/ and refs/tags/) -/// 2. Updates refs that exist in state if we have the commit -/// 3. Sets HEAD if the HEAD branch's commit is available -fn align_repository_with_state(repo_path: &Path, state: &RepositoryState) -> SyncAlignmentResult { - use crate::git; - - let mut result = SyncAlignmentResult::default(); - - // Check if repository exists - if !repo_path.exists() { - tracing::debug!( - "Repository not found at {}, cannot align with state", - repo_path.display() - ); - return result; - } - - // Get current refs from the repository - let current_refs = match git::list_refs(repo_path) { - Ok(refs) => refs, - Err(e) => { - tracing::warn!("Failed to list refs in {}: {}", repo_path.display(), e); - return result; - } - }; - - // Build expected refs from state - let mut expected_refs: std::collections::HashMap = - std::collections::HashMap::new(); - - for branch in &state.branches { - let ref_name = format!("refs/heads/{}", branch.name); - expected_refs.insert(ref_name, branch.commit.clone()); - } - - for tag in &state.tags { - let ref_name = format!("refs/tags/{}", tag.name); - expected_refs.insert(ref_name, tag.commit.clone()); - } - - // Delete refs that exist in repo but not in state (only for refs/heads/ and refs/tags/) - for (ref_name, _current_commit) in ¤t_refs { - if (ref_name.starts_with("refs/heads/") || ref_name.starts_with("refs/tags/")) - && !expected_refs.contains_key(ref_name) - { - match git::delete_ref(repo_path, ref_name) { - Ok(()) => { - tracing::info!( - "Deleted {} from {} (not in state)", - ref_name, - repo_path.display() - ); - result.refs_deleted += 1; - } - Err(e) => { - tracing::warn!( - "Failed to delete {} from {}: {}", - ref_name, - repo_path.display(), - e - ); - } - } - } - } - - // Update refs that exist in state (if we have the commit) - for (ref_name, expected_commit) in &expected_refs { - // Skip symbolic refs - if expected_commit.starts_with("ref: ") { - continue; - } - - // Check if we have the commit - if !git::oid_exists(repo_path, expected_commit) { - tracing::debug!( - "Commit {} not available for {} in {}", - expected_commit, - ref_name, - repo_path.display() - ); - continue; - } - - // Check current value - let current_commit = current_refs - .iter() - .find(|(r, _)| r == ref_name) - .map(|(_, c)| c.as_str()); - - if current_commit == Some(expected_commit.as_str()) { - // Already correct - continue; - } - - // Update or create the ref - match git::update_ref(repo_path, ref_name, expected_commit) { - Ok(()) => { - if current_commit.is_some() { - tracing::info!( - "Updated {} to {} in {}", - ref_name, - expected_commit, - repo_path.display() - ); - result.refs_updated += 1; - } else { - tracing::info!( - "Created {} at {} in {}", - ref_name, - expected_commit, - repo_path.display() - ); - result.refs_created += 1; - } - } - Err(e) => { - tracing::warn!( - "Failed to update {} in {}: {}", - ref_name, - repo_path.display(), - e - ); - } - } - } - - // Set HEAD if specified in state - if let Some(head_ref) = &state.head { - if let Some(branch_name) = state.get_head_branch() { - if let Some(head_commit) = state.get_branch_commit(branch_name) { - match git::try_set_head_if_available(repo_path, head_ref, head_commit) { - Ok(true) => { - tracing::info!( - "Set HEAD to {} in {} (from purgatory sync)", - head_ref, - repo_path.display() - ); - result.head_set = true; - } - Ok(false) => { - tracing::debug!( - "HEAD commit {} not available yet in {}", - head_commit, - repo_path.display() - ); - } - Err(e) => { - tracing::warn!("Failed to set HEAD in {}: {}", repo_path.display(), e); - } - } - } - } - } - - result -} - /// Fetch missing OIDs from a remote git server. /// /// Uses `git fetch` to retrieve specific commits from the server. -- cgit v1.2.3