#[cfg(feature = "client")]
use std::collections::HashSet;
#[cfg(feature = "client")]
use anyhow::Context;
use anyhow::Result;
#[cfg(feature = "client")]
use objects::object::ChangeId;
use objects::object::{MarkerName, ThreadName};
#[cfg(feature = "client")]
use objects::store::ObjectStore;
use repo::{Repository, RepositoryCapability};
use serde::Serialize;
use super::{
advice::RecoveryAdvice, git_overlay_health::build_repository_verification_state,
remote::resolved_default_remote_name,
};
#[cfg(feature = "client")]
use crate::client::{HostedAuthMode, HostedGrpcClient};
#[cfg(feature = "client")]
use crate::config::UserConfig;
use crate::{
bridge::GitBridge,
cli::{Cli, should_output_json, style},
client::LocalSync,
remote::{RemoteTarget, resolve_remote_with_key},
};
#[derive(Serialize)]
struct FetchOutput {
output_kind: &'static str,
remote: String,
#[serde(skip_serializing_if = "Option::is_none")]
ref_scope: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
tags_included: Option<bool>,
refs_fetched: usize,
objects_fetched: usize,
#[allow(dead_code)]
#[serde(skip_serializing)]
#[serde(rename = "verification")]
trust: super::git_overlay_health::RepositoryVerificationState,
}
pub async fn cmd_fetch(cli: &Cli, remote: Option<String>, all: bool) -> Result<()> {
let repo = cli.open_repo()?;
if repo.capability() == RepositoryCapability::GitOverlay && !repo.hosted_enabled() {
let remotes = if all {
let configured = repo.refs().list_remotes()?;
if configured.is_empty() {
vec!["origin".to_string()]
} else {
configured
}
} else {
let selected = if let Some(remote) = remote.as_ref() {
remote.clone()
} else if let Some(default) = resolved_default_remote_name(&repo)? {
default
} else {
return Err(RecoveryAdvice::remote_name_required_for_fetch().into());
};
vec![selected]
};
for remote_name in &remotes {
let mut bridge = GitBridge::new(&repo);
bridge.fetch(remote_name)?;
}
if should_output_json(cli, Some(repo.config())) {
println!(
"{}",
serde_json::to_string(&FetchOutput {
output_kind: "fetch",
remote: if all {
"all".to_string()
} else {
remotes
.first()
.cloned()
.unwrap_or_else(|| "origin".to_string())
},
ref_scope: Some("branches_and_heddle_notes"),
tags_included: Some(false),
refs_fetched: remotes.len(),
objects_fetched: 0,
trust: build_repository_verification_state(&repo),
})?
);
} else {
println!(
"{} fetched branches + refs/notes/heddle from {} (tags skipped)",
style::ok_marker(),
if all {
style::bold("all remotes")
} else {
style::bold(&remotes.join(", "))
}
);
}
return Ok(());
}
let remotes = if all {
repo.refs().list_remotes()?
} else if let Some(remote) = remote.as_ref() {
vec![remote.clone()]
} else if let Some(default) = resolved_default_remote_name(&repo)? {
vec![default]
} else {
return Err(RecoveryAdvice::remote_name_required_for_fetch().into());
};
let mut total_refs = 0;
let mut total_objects = 0;
#[cfg(feature = "client")]
let user_config = UserConfig::load_default()?;
for remote_name in &remotes {
#[cfg(feature = "client")]
let (target, server_key) = resolve_remote_with_key(&repo, Some(remote_name.as_str()))
.map_err(anyhow::Error::msg)?;
#[cfg(not(feature = "client"))]
let (target, _server_key) = resolve_remote_with_key(&repo, Some(remote_name.as_str()))
.map_err(anyhow::Error::msg)?;
match target {
RemoteTarget::Local(path) => {
let (refs, objects) = fetch_local(&repo, &path, remote_name, cli).await?;
total_refs += refs;
total_objects += objects;
}
RemoteTarget::Network { addr, repo_path } => {
#[cfg(feature = "client")]
{
let (refs, objects) = match fetch_network(
&repo,
FetchNetworkOptions {
addr,
repo_path: repo_path.as_deref(),
user_config: &user_config,
server_key,
remote_name,
cli,
},
)
.await
{
Ok(result) => result,
Err(err) => {
return Err(augment_missing_blob_error(&repo, err));
}
};
total_refs += refs;
total_objects += objects;
}
#[cfg(not(feature = "client"))]
{
let _ = (addr, repo_path);
anyhow::bail!(RecoveryAdvice::network_feature_unavailable("fetch"));
}
}
}
}
if should_output_json(cli, Some(repo.config())) {
println!(
"{}",
serde_json::to_string(&FetchOutput {
output_kind: "fetch",
remote: if all {
"all".to_string()
} else {
remotes
.first()
.cloned()
.unwrap_or_else(|| "origin".to_string())
},
ref_scope: None,
tags_included: None,
refs_fetched: total_refs,
objects_fetched: total_objects,
trust: build_repository_verification_state(&repo),
})?
);
} else {
println!(
"{} fetched {} and {}",
style::ok_marker(),
style::count(total_refs, "ref"),
style::count(total_objects, "object")
);
}
Ok(())
}
async fn fetch_local(
repo: &Repository,
source_path: &std::path::Path,
remote_name: &str,
cli: &Cli,
) -> Result<(usize, usize)> {
if !should_output_json(cli, Some(repo.config())) {
println!(
"{} fetching from {} {}",
style::working_marker(),
style::bold(remote_name),
style::dim(&format!("file://{}", source_path.display()))
);
}
let source = LocalSync::open(source_path)?;
let mut refs_fetched = 0;
let mut objects_fetched = 0;
for (track_name, change_id) in source.list_threads()? {
let count = source.fetch_state(repo, &change_id)?;
objects_fetched += count;
repo.refs()
.set_remote_thread(remote_name, &ThreadName::new(&track_name), &change_id)?;
refs_fetched += 1;
}
for (marker_name, change_id) in source.list_markers()? {
let count = source.fetch_state(repo, &change_id)?;
objects_fetched += count;
let mn = MarkerName::new(&marker_name);
if repo.refs().get_marker(&mn)?.is_none() {
repo.refs().create_marker(&mn, &change_id)?;
}
}
Ok((refs_fetched, objects_fetched))
}
#[cfg(feature = "client")]
struct FetchNetworkOptions<'a> {
addr: std::net::SocketAddr,
repo_path: Option<&'a str>,
user_config: &'a UserConfig,
server_key: Option<String>,
remote_name: &'a str,
cli: &'a Cli,
}
#[cfg(feature = "client")]
async fn fetch_network(
repo: &Repository,
options: FetchNetworkOptions<'_>,
) -> Result<(usize, usize)> {
let repo_path = options
.repo_path
.context("network remotes must include a hosted repository path")?;
let mut client = HostedGrpcClient::open_session(
options.addr,
options.user_config,
options.server_key,
HostedAuthMode::ConfigToken,
)
.await?;
if !should_output_json(options.cli, Some(repo.config())) {
println!(
"{} connected to {} {}",
style::ok_marker(),
style::bold(options.remote_name),
style::dim(&options.addr.to_string())
);
}
let remote_refs = client.list_refs(repo_path).await?;
let mut refs_to_update = Vec::new();
let mut markers_to_create = Vec::new();
let mut fetched_states = HashSet::new();
let mut objects_fetched = 0;
for ref_entry in &remote_refs {
if fetched_states.insert(ref_entry.change_id)
&& !repo.store().has_state(&ref_entry.change_id)?
{
objects_fetched += fetch_remote_state(
&mut client,
repo,
repo_path,
&ref_entry.name,
ref_entry.change_id,
)
.await?;
}
if ref_entry.is_thread {
refs_to_update.push((ref_entry.name.clone(), ref_entry.change_id));
} else {
markers_to_create.push((ref_entry.name.clone(), ref_entry.change_id));
}
}
let refs_fetched = refs_to_update.len();
for (track_name, change_id) in refs_to_update {
repo.refs().set_remote_thread(
options.remote_name,
&ThreadName::new(&track_name),
&change_id,
)?;
}
for (marker_name, change_id) in markers_to_create {
let mn = MarkerName::new(&marker_name);
if repo.refs().get_marker(&mn)?.is_none() {
repo.refs().create_marker(&mn, &change_id)?;
}
}
Ok((refs_fetched, objects_fetched))
}
#[cfg(feature = "client")]
async fn fetch_remote_state(
client: &mut HostedGrpcClient,
repo: &Repository,
repo_path: &str,
remote_name: &str,
state_id: ChangeId,
) -> Result<usize> {
client
.fetch_state(repo, repo_path, remote_name, state_id)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))
}
#[cfg(feature = "client")]
fn augment_missing_blob_error(repo: &Repository, err: anyhow::Error) -> anyhow::Error {
let Ok(missing) = repo.missing_blobs() else {
return err;
};
if missing.is_empty() {
return err;
}
let missing = missing
.into_iter()
.map(|hash| hash.short())
.collect::<Vec<_>>()
.join(", ");
anyhow::anyhow!("{err}; missing blobs: {missing}")
}