Add WASI SSH CLI and in-process Git SSH transport
This commit is contained in:
@@ -4,6 +4,7 @@ mod branches;
|
||||
mod changes;
|
||||
mod checkout;
|
||||
mod http;
|
||||
mod ssh;
|
||||
mod merge;
|
||||
mod network;
|
||||
mod output;
|
||||
|
||||
+21
-14
@@ -1,9 +1,10 @@
|
||||
//! Anonymous HTTP and HTTPS clone/fetch through standard wasi:http.
|
||||
//! Clone and fetch over standard WASI HTTP or in-process SSH exec channels.
|
||||
use crate::{
|
||||
Result,
|
||||
http::{WasiHttp, clean_url},
|
||||
http::WasiHttp,
|
||||
remotes,
|
||||
repo::{self, error},
|
||||
ssh::clean_url,
|
||||
};
|
||||
use gix::bstr::ByteSlice;
|
||||
use std::{collections::BTreeSet, sync::atomic::AtomicBool};
|
||||
@@ -82,18 +83,24 @@ fn perform(
|
||||
));
|
||||
}
|
||||
}
|
||||
let transport: Box<dyn gix_transport::client::blocking_io::Transport> = {
|
||||
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 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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Single-ref receive-pack client over standard wasi:http.
|
||||
//! Single-ref receive-pack over standard WASI HTTP or in-process SSH exec channels.
|
||||
|
||||
use crate::Result;
|
||||
use crate::{
|
||||
@@ -587,6 +587,9 @@ pub fn push(p: PushParams) -> Result<PushResult> {
|
||||
.to_bstring()
|
||||
.to_str_lossy()
|
||||
.into_owned();
|
||||
if crate::ssh::is_ssh(&url) {
|
||||
return ssh_push(&repo, &url, remote, &plan, p.set_upstream.unwrap_or(false));
|
||||
}
|
||||
let base = clean_url(&url)
|
||||
.map_err(|e| error("push URL", e))?
|
||||
.to_string()
|
||||
@@ -670,3 +673,57 @@ pub fn push(p: PushParams) -> Result<PushResult> {
|
||||
})();
|
||||
result
|
||||
}
|
||||
fn ssh_push(
|
||||
repo: &gix::Repository,
|
||||
url: &str,
|
||||
remote: &str,
|
||||
plan: &Plan,
|
||||
upstream: bool,
|
||||
) -> Result<PushResult> {
|
||||
let (mut read, mut write) =
|
||||
crate::ssh::connection(url, gix_transport::Service::ReceivePack)?.into_inner();
|
||||
let advertised = advertise_refs(&mut read)?;
|
||||
let old = advertised.refs.get(&plan.destination).copied();
|
||||
if let (Some(old), Some(new)) = (old, plan.new) {
|
||||
if old != new && !plan.force && !is_ancestor(repo, old, new)? {
|
||||
return Err(error("push", "non-fast-forward; fetch first or use force"));
|
||||
}
|
||||
}
|
||||
let up_to_date = old == plan.new;
|
||||
let mut objects = 0;
|
||||
if !up_to_date {
|
||||
if plan.new.is_none() && !advertised.caps.contains("delete-refs") {
|
||||
return Err(error("push", "remote does not allow deleting refs"));
|
||||
}
|
||||
let ids = match plan.new {
|
||||
Some(new) => objects_to_send(repo, new, advertised.refs.values().copied())?,
|
||||
None => BTreeSet::new(),
|
||||
};
|
||||
objects = ids.len();
|
||||
let sideband = advertised.caps.contains("side-band-64k");
|
||||
let mut caps = vec!["report-status"];
|
||||
if sideband {
|
||||
caps.push("side-band-64k");
|
||||
}
|
||||
commands(old, plan, &caps, &mut write)?;
|
||||
if plan.new.is_some() {
|
||||
pack(repo, &ids, &mut write)?;
|
||||
}
|
||||
write.flush().map_err(|e| error("push SSH flush", e))?;
|
||||
report(&mut read, &plan.destination, sideband)?;
|
||||
} else {
|
||||
write
|
||||
.write_all(b"0000")
|
||||
.and_then(|_| write.flush())
|
||||
.map_err(|e| error("push SSH finish", e))?;
|
||||
}
|
||||
update_local(repo, remote, plan, upstream)?;
|
||||
Ok(PushResult {
|
||||
remote: remote.into(),
|
||||
reference: plan.destination.clone(),
|
||||
old: old.map(|id| id.to_string()),
|
||||
new: plan.new.map(|id| id.to_string()),
|
||||
up_to_date,
|
||||
objects,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Non-destructive local Git config edits. No plugin-owned persistent format.
|
||||
use crate::Result;
|
||||
use crate::{http::clean_url, repo::error};
|
||||
use crate::{repo::error, ssh::clean_url};
|
||||
use gix::bstr::ByteSlice;
|
||||
|
||||
/// Remote identity.
|
||||
@@ -81,7 +81,7 @@ pub fn list(repo: &gix::Repository) -> Result<Vec<RemoteInfo>> {
|
||||
// Do not return legacy embedded credentials from repositories imported outside this plugin.
|
||||
let url = url
|
||||
.map(|s| {
|
||||
clean_url(&s).map(|u| u.to_string()).map_err(|_| {
|
||||
clean_url(&s).map_err(|_| {
|
||||
error(
|
||||
"remote list",
|
||||
"remote URL must be HTTPS without credentials or SSH without password",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//! Git exec channels over the shared SSH library, with host-mediated signing.
|
||||
use crate::{Result, repo::error};
|
||||
use gix::bstr::ByteSlice;
|
||||
use std::io;
|
||||
fn parse(input: &str) -> io::Result<gix::Url> {
|
||||
let url = gix::url::parse(input.as_bytes().as_bstr())
|
||||
.map_err(|_| io::Error::other("invalid SSH URL"))?;
|
||||
if url.scheme != gix::url::Scheme::Ssh
|
||||
|| url.host.as_deref().is_none_or(str::is_empty)
|
||||
|| url.password.is_some()
|
||||
|| url.path.is_empty()
|
||||
|| url.path.iter().any(|b| matches!(b, 0 | b'\n' | b'\r'))
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"use ssh://[user@]host[:port]/path or [user@]host:path without a password",
|
||||
));
|
||||
}
|
||||
Ok(url)
|
||||
}
|
||||
pub fn clean_url(input: &str) -> io::Result<String> {
|
||||
if input.starts_with("http://") || input.starts_with("https://") {
|
||||
crate::http::clean_url(input).map(|url| url.to_string())
|
||||
} else {
|
||||
parse(input)?;
|
||||
Ok(input.into())
|
||||
}
|
||||
}
|
||||
pub fn is_ssh(input: &str) -> bool {
|
||||
!input.starts_with("http://") && !input.starts_with("https://")
|
||||
}
|
||||
pub type Connection =
|
||||
gix_transport::client::git::blocking_io::Connection<sausage_ssh::Reader, sausage_ssh::Writer>;
|
||||
pub fn connection(input: &str, service: gix_transport::Service) -> Result<Connection> {
|
||||
let url = parse(input).map_err(|e| error("SSH URL", e))?;
|
||||
let host = url.host.as_deref().unwrap();
|
||||
let mut session =
|
||||
sausage_ssh::identity::connect(host, url.port.unwrap_or(22), url.user.as_deref(), false)
|
||||
.map_err(|e| error("SSH", e))?;
|
||||
let mut command = service.as_str().as_bytes().to_vec();
|
||||
command.extend_from_slice(b" '");
|
||||
for byte in url.path.iter() {
|
||||
if *byte == b'\'' {
|
||||
command.extend_from_slice(b"'\\''");
|
||||
} else {
|
||||
command.push(*byte);
|
||||
}
|
||||
}
|
||||
command.push(b'\'');
|
||||
session
|
||||
.open_command(&command)
|
||||
.map_err(|e| error("SSH exec", e))?;
|
||||
let (read, write) = session.split();
|
||||
Ok(Connection::new(
|
||||
read,
|
||||
write,
|
||||
gix_transport::Protocol::V1,
|
||||
url.path,
|
||||
None::<(&str, Option<u16>)>,
|
||||
gix_transport::client::git::ConnectMode::Process,
|
||||
false,
|
||||
)
|
||||
.custom_url(Some(input.into())))
|
||||
}
|
||||
Reference in New Issue
Block a user