291 lines
10 KiB
Rust
291 lines
10 KiB
Rust
//! Clone and fetch over standard WASI HTTP or in-process SSH exec channels.
|
|
use crate::{
|
|
Result,
|
|
http::WasiHttp,
|
|
remotes,
|
|
repo::{self, error},
|
|
ssh::clean_url,
|
|
};
|
|
use gix::bstr::ByteSlice;
|
|
use std::{collections::BTreeSet, sync::atomic::AtomicBool};
|
|
struct FetchResult {
|
|
remote: String,
|
|
advertised_refs: usize,
|
|
received_pack: bool,
|
|
pruned: Vec<String>,
|
|
copied_objects: Option<usize>,
|
|
updated_refs: Option<Vec<String>>,
|
|
}
|
|
pub(crate) fn fetch(path: &str, remote: &str, revision: Option<&str>) -> Result<()> {
|
|
let mut repo = repo::open(path)?;
|
|
repo.refs.write_reflog = gix::refs::store::WriteReflog::Disable;
|
|
perform(&repo, remote, None, false, revision)?;
|
|
Ok(())
|
|
}
|
|
pub(crate) fn clone(url: &str, path: &str) -> Result<()> {
|
|
clean_url(url).map_err(|e| error("clone URL", e))?;
|
|
let path = std::path::absolute(path).map_err(|e| error("clone path", e))?;
|
|
std::fs::create_dir(&path).map_err(|e| error("clone destination", e))?;
|
|
gix::init(&path).map_err(|e| error("clone init", e))?;
|
|
let name = path.to_string_lossy();
|
|
let repo = repo::open(&name)?;
|
|
remotes::set(&repo, "origin", url, true)?;
|
|
let mut repo = repo::open(&name)?;
|
|
repo.refs.write_reflog = gix::refs::store::WriteReflog::Disable;
|
|
let fetched = perform(&repo, "origin", None, false, None)?;
|
|
let branch = fetched
|
|
.default_branch
|
|
.ok_or_else(|| error("clone", "server HEAD does not name a branch"))?;
|
|
if !fetched.empty {
|
|
crate::branches::create(
|
|
&repo,
|
|
crate::branches::BranchCreateParams {
|
|
repo: name.to_string(),
|
|
name: branch.clone(),
|
|
from: Some(format!("refs/remotes/origin/{branch}")),
|
|
checkout: Some(true),
|
|
},
|
|
)?;
|
|
}
|
|
remotes::upstream(&repo, &branch)?;
|
|
Ok(())
|
|
}
|
|
struct Fetched {
|
|
summary: FetchResult,
|
|
default_branch: Option<String>,
|
|
heads: BTreeSet<String>,
|
|
empty: bool,
|
|
}
|
|
fn perform(
|
|
repo: &gix::Repository,
|
|
remote_name: &str,
|
|
depth: Option<u32>,
|
|
prune: bool,
|
|
revision: Option<&str>,
|
|
) -> Result<Fetched> {
|
|
remotes::validate_name(remote_name)?;
|
|
let mut remote = repo
|
|
.find_remote(remote_name)
|
|
.map_err(|_| error("fetch", "configured remote not found"))?;
|
|
let url = remote
|
|
.url(gix::remote::Direction::Fetch)
|
|
.ok_or_else(|| error("fetch", "remote has no fetch URL"))?
|
|
.to_bstring()
|
|
.to_str_lossy()
|
|
.into_owned();
|
|
clean_url(&url).map_err(|e| error("fetch URL", e))?;
|
|
if prune {
|
|
let expected = format!("+refs/heads/*:refs/remotes/{remote_name}/*");
|
|
let specs = remote.refspecs(gix::remote::Direction::Fetch);
|
|
if specs.len() != 1 || specs[0].to_ref().to_bstring() != expected.as_bytes() {
|
|
return Err(error(
|
|
"prune",
|
|
"prune requires the standard +refs/heads/*:refs/remotes/<remote>/* refspec",
|
|
));
|
|
}
|
|
}
|
|
if let Some(revision) = revision {
|
|
let source = if revision.starts_with("refs/") || revision.len() == 40 {
|
|
revision.to_owned()
|
|
} else {
|
|
format!("refs/heads/{revision}")
|
|
};
|
|
let spec = if let Some(branch) = source.strip_prefix("refs/heads/") {
|
|
format!("+{source}:refs/remotes/{remote_name}/{branch}")
|
|
} else {
|
|
source
|
|
};
|
|
remote
|
|
.replace_refspecs([spec.as_bytes().as_bstr()], gix::remote::Direction::Fetch)
|
|
.map_err(|e| error("fetch refspec", e))?;
|
|
}
|
|
let transport: Box<dyn gix_transport::client::blocking_io::Transport> =
|
|
if crate::ssh::is_ssh(&url) {
|
|
Box::new(crate::ssh::connection(
|
|
&url,
|
|
gix_transport::Service::UploadPack,
|
|
)?)
|
|
} else {
|
|
let transport_url = gix::url::parse(url.as_bytes().as_bstr())
|
|
.map_err(|_| error("fetch", "invalid HTTPS URL"))?;
|
|
Box::new(
|
|
gix_transport::client::blocking_io::http::Transport::new_http(
|
|
WasiHttp::default(),
|
|
transport_url,
|
|
gix_transport::Protocol::V2,
|
|
false,
|
|
),
|
|
)
|
|
};
|
|
let mut options = gix::remote::ref_map::Options::default();
|
|
options.extra_refspecs.push(
|
|
gix::refspec::parse("HEAD".into(), gix::refspec::parse::Operation::Fetch)
|
|
.map_err(|e| error("fetch HEAD", e))?
|
|
.to_owned(),
|
|
);
|
|
let mut prepared = remote
|
|
.to_connection_with_transport(transport)
|
|
.prepare_fetch(gix::progress::Discard, options)
|
|
.map_err(|e| error("fetch handshake", format!("{e:#}")))?;
|
|
let refs = &prepared.ref_map().remote_refs;
|
|
let mut heads = BTreeSet::new();
|
|
let mut default_branch = None;
|
|
for reference in refs {
|
|
let (name, _, _) = reference.unpack();
|
|
if let Some(branch) = name.to_str_lossy().strip_prefix("refs/heads/") {
|
|
heads.insert(branch.to_owned());
|
|
}
|
|
match reference {
|
|
gix::protocol::handshake::Ref::Symbolic {
|
|
full_ref_name,
|
|
target,
|
|
..
|
|
}
|
|
| gix::protocol::handshake::Ref::Unborn {
|
|
full_ref_name,
|
|
target,
|
|
} if full_ref_name.as_slice() == b"HEAD" => {
|
|
default_branch = target
|
|
.to_str_lossy()
|
|
.strip_prefix("refs/heads/")
|
|
.map(str::to_owned);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
let fetch_heads: Vec<_> = refs
|
|
.iter()
|
|
.filter_map(|r| {
|
|
let (name, id, _) = r.unpack();
|
|
id.map(|id| (name.to_string(), id.to_string()))
|
|
})
|
|
.collect();
|
|
let advertised_refs = refs.len();
|
|
let empty = heads.is_empty() && refs.iter().all(|r| r.unpack().1.is_none());
|
|
if let Some(depth) = depth {
|
|
prepared = prepared.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote(
|
|
depth
|
|
.try_into()
|
|
.map_err(|_| error("fetch", "depth must be positive"))?,
|
|
));
|
|
}
|
|
let outcome = prepared
|
|
.receive(gix::progress::Discard, &AtomicBool::new(false))
|
|
.map_err(|e| error("fetch", format!("{e:#}")))?;
|
|
let received_pack = matches!(outcome.status, gix::remote::fetch::Status::Change { .. });
|
|
let updates = match &outcome.status {
|
|
gix::remote::fetch::Status::Change { update_refs, .. }
|
|
| gix::remote::fetch::Status::NoPackReceived { update_refs, .. } => update_refs,
|
|
};
|
|
use gix::remote::fetch::refs::update::Mode;
|
|
for update in &updates.updates {
|
|
if !matches!(
|
|
update.mode,
|
|
Mode::NoChangeNeeded
|
|
| Mode::FastForward
|
|
| Mode::Forced
|
|
| Mode::New
|
|
| Mode::ImplicitTagNotSentByRemote
|
|
) {
|
|
return Err(error(
|
|
"fetch refs (other refs may already be updated)",
|
|
&update.mode,
|
|
));
|
|
}
|
|
}
|
|
let fetch_head = fetch_heads
|
|
.iter()
|
|
.filter(|(name, _)| {
|
|
revision.map_or(name.starts_with("refs/heads/"), |r| {
|
|
name == r || name == &format!("refs/heads/{r}")
|
|
})
|
|
})
|
|
.map(|(name, id)| {
|
|
format!(
|
|
"{id}\t\tbranch '{}' of {url}\n",
|
|
name.trim_start_matches("refs/heads/")
|
|
)
|
|
})
|
|
.collect::<String>();
|
|
std::fs::write(repo.git_dir().join("FETCH_HEAD"), fetch_head)
|
|
.map_err(|e| error("FETCH_HEAD", e))?;
|
|
if let Some(branch) = default_branch
|
|
.as_deref()
|
|
.filter(|b| revision.is_none() && heads.contains(*b))
|
|
{
|
|
use gix::refs::{
|
|
Target,
|
|
transaction::{Change, PreviousValue, RefEdit},
|
|
};
|
|
repo.edit_references_as(
|
|
[RefEdit {
|
|
name: format!("refs/remotes/{remote_name}/HEAD")
|
|
.as_str()
|
|
.try_into()
|
|
.map_err(|e| error("remote HEAD", e))?,
|
|
deref: false,
|
|
change: Change::Update {
|
|
log: Default::default(),
|
|
expected: PreviousValue::Any,
|
|
new: Target::Symbolic(
|
|
format!("refs/remotes/{remote_name}/{branch}")
|
|
.as_str()
|
|
.try_into()
|
|
.map_err(|e| error("remote HEAD target", e))?,
|
|
),
|
|
},
|
|
}],
|
|
None,
|
|
)
|
|
.map_err(|e| error("remote HEAD", e))?;
|
|
}
|
|
let pruned = if prune {
|
|
prune_refs(repo, remote_name, &heads)?
|
|
} else {
|
|
vec![]
|
|
};
|
|
Ok(Fetched {
|
|
summary: FetchResult {
|
|
remote: remote_name.into(),
|
|
advertised_refs,
|
|
received_pack,
|
|
pruned,
|
|
copied_objects: None,
|
|
updated_refs: None,
|
|
},
|
|
default_branch,
|
|
heads,
|
|
empty,
|
|
})
|
|
}
|
|
/// Delete only stale refs in this remote's namespace. Symbolic HEAD is not a branch.
|
|
pub fn prune_refs(
|
|
repo: &gix::Repository,
|
|
remote: &str,
|
|
heads: &BTreeSet<String>,
|
|
) -> Result<Vec<String>> {
|
|
remotes::validate_name(remote)?;
|
|
let prefix = format!("refs/remotes/{remote}/");
|
|
let refs = repo.references().map_err(|e| error("prune refs", e))?;
|
|
let mut stale = Vec::new();
|
|
for reference in refs
|
|
.prefixed(prefix.as_str())
|
|
.map_err(|e| error("prune refs", e))?
|
|
{
|
|
let reference = reference.map_err(|e| error("prune ref", e))?;
|
|
let name = reference.name().as_bstr().to_str_lossy().into_owned();
|
|
if let Some(short) = name.strip_prefix(&prefix) {
|
|
if short != "HEAD" && !heads.contains(short) {
|
|
stale.push(name);
|
|
}
|
|
}
|
|
}
|
|
for name in &stale {
|
|
repo.find_reference(name.as_str())
|
|
.map_err(|e| error("prune ref", e))?
|
|
.delete()
|
|
.map_err(|e| error("prune delete", e))?;
|
|
}
|
|
Ok(stale)
|
|
}
|