Skip to main content

gitoxide_core/
remote.rs

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}