1use std::{
2 io,
3 path::PathBuf,
4 time::{Duration, Instant},
5};
6
7use crate::{OutputFormat, net};
8use anyhow::bail;
9use gix::protocol::transport::client::blocking_io::connect;
10use gix::{
11 NestedProgress,
12 config::tree::Key,
13 objs::bstr::ByteSlice,
14 protocol::{self, handshake::Ref, transport},
15 refs::{
16 Target,
17 transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},
18 },
19};
20
21pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=2;
22
23pub struct Context<W> {
24 pub format: OutputFormat,
25 pub out: W,
26 pub object_hash: gix::hash::Kind,
27 pub write_reflog: bool,
28}
29
30#[derive(Clone, Copy, Debug)]
31pub struct RefsWriteOutcome {
32 pub num_refs: usize,
33 pub elapsed: Duration,
34}
35
36pub fn refs<P, W>(
37 protocol: Option<net::Protocol>,
38 url: &str,
39 refs_directory: Option<PathBuf>,
40 mut progress: P,
41 ctx: Context<W>,
42) -> anyhow::Result<()>
43where
44 W: io::Write,
45 P: NestedProgress + 'static,
46 P::SubProgress: 'static,
47{
48 if ctx.format != OutputFormat::Human {
49 bail!("JSON output isn't supported");
50 }
51
52 let mut transport = net::connect(
53 url,
54 connect::Options {
55 version: protocol.unwrap_or_default().into(),
56 ..Default::default()
57 },
58 )?;
59 let trace_packetlines = std::env::var_os(
60 gix::config::tree::Gitoxide::TRACE_PACKET
61 .environment_override()
62 .expect("set"),
63 )
64 .is_some();
65
66 progress.info(format!("Connecting to {url:?}"));
67 let agent = protocol::agent(gix::env::agent());
68 let mut handshake = protocol::handshake(
69 &mut transport.inner,
70 transport::Service::UploadPack,
71 protocol::credentials::builtin,
72 vec![("agent".into(), Some(agent.clone()))],
73 &mut progress,
74 )?;
75 let fetch_refmap = handshake.prepare_lsrefs_or_extract_refmap(
76 ("agent", Some(agent)),
77 false,
78 protocol::fetch::refmap::init::Context {
79 fetch_refspecs: Vec::new(),
80 extra_refspecs: Vec::new(),
81 },
82 )?;
83
84 let refmap = fetch_refmap.fetch_blocking(&mut progress, &mut transport.inner, trace_packetlines)?;
85 let refs = refmap.remote_refs;
86
87 let refs_write = refs_directory
88 .map(|directory| write_refs(&refs, directory, ctx.object_hash, ctx.write_reflog))
89 .transpose()?;
90 print_refs(ctx.out, &refs, refs_write)?;
91
92 Ok(())
93}
94
95fn print_refs(mut out: impl io::Write, refs: &[Ref], refs_write: Option<RefsWriteOutcome>) -> io::Result<()> {
96 crate::repository::remote::refs::print(&mut out, refs)?;
97 if let Some(outcome) = refs_write {
98 writeln!(out)?;
99 writeln!(out, "refs-write: {} refs in {:?}", outcome.num_refs, outcome.elapsed)?;
100 }
101 Ok(())
102}
103
104fn write_refs(
105 refs: &[Ref],
106 directory: PathBuf,
107 object_hash: gix::hash::Kind,
108 write_reflog: bool,
109) -> anyhow::Result<RefsWriteOutcome> {
110 let _span = gix::trace::coarse!("write remote refs", refs = refs.len(), directory = ?directory);
111 std::fs::create_dir_all(&directory)?;
112
113 let start = Instant::now();
114 let precompose_unicode = gix::fs::Capabilities::probe(&directory).precompose_unicode;
115 let store = gix::RefStore::at(
116 directory,
117 gix::refs::store::init::Options {
118 write_reflog: if write_reflog {
119 gix::refs::store::WriteReflog::Always
120 } else {
121 gix::refs::store::WriteReflog::Disable
122 },
123 object_hash,
124 precompose_unicode,
125 prohibit_windows_device_names: cfg!(windows),
126 },
127 );
128 let edits = refs
129 .iter()
130 .map(ref_to_edit)
131 .collect::<Result<Vec<_>, gix::refs::name::Error>>()?;
132
133 store
134 .transaction()
135 .prepare(
136 edits,
137 gix::lock::acquire::Fail::Immediately,
138 gix::lock::acquire::Fail::Immediately,
139 )?
140 .commit(write_reflog.then_some(reflog_committer()))?;
141 let outcome = RefsWriteOutcome {
142 num_refs: refs.len(),
143 elapsed: start.elapsed(),
144 };
145 gix::trace::info!(
146 refs = outcome.num_refs,
147 elapsed_secs = outcome.elapsed.as_secs_f64(),
148 "wrote remote refs"
149 );
150 Ok(outcome)
151}
152
153fn reflog_committer() -> gix::actor::SignatureRef<'static> {
154 gix::actor::SignatureRef {
155 name: b"gitoxide".as_bstr(),
156 email: b"gitoxide@example.com".as_bstr(),
157 time: "0 +0000",
158 }
159}
160
161fn ref_to_edit(ref_: &Ref) -> Result<RefEdit, gix::refs::name::Error> {
162 let (name, target) = match ref_ {
163 Ref::Unborn { full_ref_name, target } => (full_ref_name, Target::Symbolic(target.as_bstr().try_into()?)),
164 Ref::Symbolic {
165 full_ref_name, target, ..
166 } => (full_ref_name, Target::Symbolic(target.as_bstr().try_into()?)),
167 Ref::Peeled { full_ref_name, tag, .. } => (full_ref_name, Target::Object(*tag)),
168 Ref::Direct { full_ref_name, object } => (full_ref_name, Target::Object(*object)),
169 };
170 Ok(RefEdit {
171 change: Change::Update {
172 log: LogChange {
173 mode: RefLog::AndReference,
174 force_create_reflog: false,
175 message: "remote refs".into(),
176 },
177 expected: PreviousValue::Any,
178 new: target,
179 },
180 name: name.as_bstr().try_into()?,
181 deref: false,
182 })
183}