use std::{
io,
path::PathBuf,
time::{Duration, Instant},
};
use crate::{OutputFormat, net};
use anyhow::bail;
use gix::protocol::transport::client::blocking_io::connect;
use gix::{
NestedProgress,
config::tree::Key,
objs::bstr::ByteSlice,
protocol::{self, handshake::Ref, transport},
refs::{
Target,
transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},
},
};
pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=2;
pub struct Context<W> {
pub format: OutputFormat,
pub out: W,
pub object_hash: gix::hash::Kind,
pub write_reflog: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct RefsWriteOutcome {
pub num_refs: usize,
pub elapsed: Duration,
}
pub fn refs<P, W>(
protocol: Option<net::Protocol>,
url: &str,
refs_directory: Option<PathBuf>,
mut progress: P,
ctx: Context<W>,
) -> anyhow::Result<()>
where
W: io::Write,
P: NestedProgress + 'static,
P::SubProgress: 'static,
{
if ctx.format != OutputFormat::Human {
bail!("JSON output isn't supported");
}
let mut transport = net::connect(
url,
connect::Options {
version: protocol.unwrap_or_default().into(),
..Default::default()
},
)?;
let trace_packetlines = std::env::var_os(
gix::config::tree::Gitoxide::TRACE_PACKET
.environment_override()
.expect("set"),
)
.is_some();
progress.info(format!("Connecting to {url:?}"));
let agent = protocol::agent(gix::env::agent());
let mut handshake = protocol::handshake(
&mut transport.inner,
transport::Service::UploadPack,
protocol::credentials::builtin,
vec![("agent".into(), Some(agent.clone()))],
&mut progress,
)?;
let fetch_refmap = handshake.prepare_lsrefs_or_extract_refmap(
("agent", Some(agent)),
false,
protocol::fetch::refmap::init::Context {
fetch_refspecs: Vec::new(),
extra_refspecs: Vec::new(),
},
)?;
let refmap = fetch_refmap.fetch_blocking(&mut progress, &mut transport.inner, trace_packetlines)?;
let refs = refmap.remote_refs;
let refs_write = refs_directory
.map(|directory| write_refs(&refs, directory, ctx.object_hash, ctx.write_reflog))
.transpose()?;
print_refs(ctx.out, &refs, refs_write)?;
Ok(())
}
fn print_refs(mut out: impl io::Write, refs: &[Ref], refs_write: Option<RefsWriteOutcome>) -> io::Result<()> {
crate::repository::remote::refs::print(&mut out, refs)?;
if let Some(outcome) = refs_write {
writeln!(out)?;
writeln!(out, "refs-write: {} refs in {:?}", outcome.num_refs, outcome.elapsed)?;
}
Ok(())
}
fn write_refs(
refs: &[Ref],
directory: PathBuf,
object_hash: gix::hash::Kind,
write_reflog: bool,
) -> anyhow::Result<RefsWriteOutcome> {
let _span = gix::trace::coarse!("write remote refs", refs = refs.len(), directory = ?directory);
std::fs::create_dir_all(&directory)?;
let start = Instant::now();
let precompose_unicode = gix::fs::Capabilities::probe(&directory).precompose_unicode;
let store = gix::RefStore::at(
directory,
gix::refs::store::init::Options {
write_reflog: if write_reflog {
gix::refs::store::WriteReflog::Always
} else {
gix::refs::store::WriteReflog::Disable
},
object_hash,
precompose_unicode,
prohibit_windows_device_names: cfg!(windows),
},
);
let edits = refs
.iter()
.map(ref_to_edit)
.collect::<Result<Vec<_>, gix::refs::name::Error>>()?;
store
.transaction()
.prepare(
edits,
gix::lock::acquire::Fail::Immediately,
gix::lock::acquire::Fail::Immediately,
)?
.commit(write_reflog.then_some(reflog_committer()))?;
let outcome = RefsWriteOutcome {
num_refs: refs.len(),
elapsed: start.elapsed(),
};
gix::trace::info!(
refs = outcome.num_refs,
elapsed_secs = outcome.elapsed.as_secs_f64(),
"wrote remote refs"
);
Ok(outcome)
}
fn reflog_committer() -> gix::actor::SignatureRef<'static> {
gix::actor::SignatureRef {
name: b"gitoxide".as_bstr(),
email: b"gitoxide@example.com".as_bstr(),
time: "0 +0000",
}
}
fn ref_to_edit(ref_: &Ref) -> Result<RefEdit, gix::refs::name::Error> {
let (name, target) = match ref_ {
Ref::Unborn { full_ref_name, target } => (full_ref_name, Target::Symbolic(target.as_bstr().try_into()?)),
Ref::Symbolic {
full_ref_name, target, ..
} => (full_ref_name, Target::Symbolic(target.as_bstr().try_into()?)),
Ref::Peeled { full_ref_name, tag, .. } => (full_ref_name, Target::Object(*tag)),
Ref::Direct { full_ref_name, object } => (full_ref_name, Target::Object(*object)),
};
Ok(RefEdit {
change: Change::Update {
log: LogChange {
mode: RefLog::AndReference,
force_create_reflog: false,
message: "remote refs".into(),
},
expected: PreviousValue::Any,
new: target,
},
name: name.as_bstr().try_into()?,
deref: false,
})
}