Implement SSH upload-pack fetches
This commit is contained in:
1 parent
5dbda4bbd2
commit
051e14235f
9 files changed
+1367
-53
No files matched your search
+138
-30
@@ -489,6 +489,11 @@ pub enum GitError {
|
||||
path: PathBuf,
|
||||
},
|
||||
SshProtocolFailed,
|
||||
SshRemoteServiceFailed {
|
||||
diagnostic: String,
|
||||
},
|
||||
GitProtocolFailed,
|
||||
MalformedGitPack,
|
||||
CredentialsUnavailable,
|
||||
CredentialAccessDenied,
|
||||
CredentialCancelled,
|
||||
@@ -607,6 +612,14 @@ impl fmt::Display for GitError {
|
||||
path.display()
|
||||
),
|
||||
Self::SshProtocolFailed => formatter.write_str("the SSH protocol failed"),
|
||||
Self::SshRemoteServiceFailed { diagnostic } if diagnostic.is_empty() => {
|
||||
formatter.write_str("the remote SSH Git service failed")
|
||||
}
|
||||
Self::SshRemoteServiceFailed { diagnostic } => {
|
||||
write!(formatter, "the remote SSH Git service failed: {diagnostic}")
|
||||
}
|
||||
Self::GitProtocolFailed => formatter.write_str("the Git wire protocol failed"),
|
||||
Self::MalformedGitPack => formatter.write_str("the remote sent a malformed Git pack"),
|
||||
Self::CredentialsUnavailable => {
|
||||
formatter.write_str("HTTPS Git credentials are unavailable")
|
||||
}
|
||||
@@ -617,7 +630,7 @@ impl fmt::Display for GitError {
|
||||
formatter.write_str("HTTPS Git credential authentication was cancelled")
|
||||
}
|
||||
Self::AuthenticationFailed => formatter.write_str("HTTPS Git authentication failed"),
|
||||
Self::NetworkUnavailable => formatter.write_str("the HTTPS Git remote is unavailable"),
|
||||
Self::NetworkUnavailable => formatter.write_str("the Git remote is unavailable"),
|
||||
Self::TlsFailed => formatter.write_str("TLS validation for the Git remote failed"),
|
||||
Self::Cancelled => formatter.write_str("the Git operation was cancelled"),
|
||||
Self::NonFastForward => formatter.write_str("the remote update is not a fast-forward"),
|
||||
@@ -787,6 +800,10 @@ pub trait SshPassphraseProvider {
|
||||
fn ssh_key_passphrase(&self, fingerprint: &SshFingerprint) -> Result<SecretBytes, GitError>;
|
||||
}
|
||||
|
||||
pub trait GitRemoteCredentialProvider: GitCredentialProvider + SshPassphraseProvider {}
|
||||
|
||||
impl<T: GitCredentialProvider + SshPassphraseProvider> GitRemoteCredentialProvider for T {}
|
||||
|
||||
pub trait GitSmartHttpTransport {
|
||||
fn advertise_receive_pack(
|
||||
&self,
|
||||
@@ -831,18 +848,18 @@ pub trait GitFetchTransport {
|
||||
&self,
|
||||
repository: &GitRepository,
|
||||
configured: &GitRemote,
|
||||
credential: &GitCredential,
|
||||
credentials: &dyn GitRemoteCredentialProvider,
|
||||
) -> Result<bool, GitError>;
|
||||
|
||||
fn fetch_controlled(
|
||||
&self,
|
||||
repository: &GitRepository,
|
||||
configured: &GitRemote,
|
||||
credential: &GitCredential,
|
||||
credentials: &dyn GitRemoteCredentialProvider,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<bool, GitError> {
|
||||
control.checkpoint(GitProgressPhase::Receiving)?;
|
||||
let result = self.fetch(repository, configured, credential)?;
|
||||
let result = self.fetch(repository, configured, credentials)?;
|
||||
control.checkpoint(GitProgressPhase::Receiving)?;
|
||||
Ok(result)
|
||||
}
|
||||
@@ -856,19 +873,19 @@ impl GitFetchTransport for EmbeddedFetchTransport {
|
||||
&self,
|
||||
repository: &GitRepository,
|
||||
configured: &GitRemote,
|
||||
credential: &GitCredential,
|
||||
credentials: &dyn GitRemoteCredentialProvider,
|
||||
) -> Result<bool, GitError> {
|
||||
repository.fetch_embedded(configured, credential)
|
||||
repository.fetch_embedded(configured, credentials)
|
||||
}
|
||||
|
||||
fn fetch_controlled(
|
||||
&self,
|
||||
repository: &GitRepository,
|
||||
configured: &GitRemote,
|
||||
credential: &GitCredential,
|
||||
credentials: &dyn GitRemoteCredentialProvider,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<bool, GitError> {
|
||||
repository.fetch_embedded_controlled(configured, credential, control)
|
||||
repository.fetch_embedded_controlled(configured, credentials, control)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -968,6 +985,29 @@ fn map_reqwest_error(error: reqwest::Error) -> GitError {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssh")]
|
||||
fn map_ssh_fetch_error(
|
||||
error: gix::remote::fetch::Error,
|
||||
control: &GitOperationControl,
|
||||
) -> GitError {
|
||||
if control.is_cancelled() {
|
||||
return GitError::Cancelled;
|
||||
}
|
||||
match error {
|
||||
gix::remote::fetch::Error::Fetch(gix::protocol::fetch::Error::ConsumePack(_)) => {
|
||||
GitError::MalformedGitPack
|
||||
}
|
||||
gix::remote::fetch::Error::Fetch(gix::protocol::fetch::Error::ReadRemainingBytes(_))
|
||||
| gix::remote::fetch::Error::Client(_) => GitError::SshProtocolFailed,
|
||||
gix::remote::fetch::Error::Fetch(
|
||||
gix::protocol::fetch::Error::FetchResponse(_)
|
||||
| gix::protocol::fetch::Error::Negotiate(_)
|
||||
| gix::protocol::fetch::Error::MissingServerFeature { .. },
|
||||
) => GitError::GitProtocolFailed,
|
||||
error => invalid(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FetchOutcome {
|
||||
remote: String,
|
||||
@@ -1018,7 +1058,7 @@ impl GitRepository {
|
||||
parent: &Path,
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Vec<String>, GitError> {
|
||||
Self::discover_remote_branches_with_transport(
|
||||
@@ -1035,12 +1075,12 @@ impl GitRepository {
|
||||
parent: &Path,
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Vec<String>, GitError> {
|
||||
control.report(GitProgressPhase::Validating)?;
|
||||
require_https_remote(configured)?;
|
||||
ensure_remote_transport_available(configured.endpoint())?;
|
||||
ensure_clone_parent(parent)?;
|
||||
let temporary = private_temporary_directory(parent, "probe")?;
|
||||
let result = (|| {
|
||||
@@ -1070,7 +1110,7 @@ impl GitRepository {
|
||||
destination: &Path,
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
) -> Result<Self, GitError> {
|
||||
Self::clone_into_with_transport_controlled(
|
||||
destination,
|
||||
@@ -1088,7 +1128,7 @@ impl GitRepository {
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
branch: &str,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Self, GitError> {
|
||||
Self::clone_into_with_transport_controlled(
|
||||
@@ -1106,7 +1146,7 @@ impl GitRepository {
|
||||
destination: &Path,
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
) -> Result<Self, GitError> {
|
||||
Self::clone_into_with_transport_controlled(
|
||||
@@ -1125,12 +1165,12 @@ impl GitRepository {
|
||||
identity: GitIdentity,
|
||||
configured: &GitRemote,
|
||||
branch: Option<&str>,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Self, GitError> {
|
||||
control.report(GitProgressPhase::Validating)?;
|
||||
require_https_remote(configured)?;
|
||||
ensure_remote_transport_available(configured.endpoint())?;
|
||||
if let Some(branch) = branch {
|
||||
validate_remote_name(branch)?;
|
||||
}
|
||||
@@ -1449,7 +1489,7 @@ impl GitRepository {
|
||||
pub fn fetch(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
) -> Result<FetchOutcome, GitError> {
|
||||
self.fetch_with_transport(configured, credentials, &EmbeddedFetchTransport)
|
||||
}
|
||||
@@ -1457,7 +1497,7 @@ impl GitRepository {
|
||||
pub fn fetch_with_transport(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
) -> Result<FetchOutcome, GitError> {
|
||||
self.fetch_with_transport_controlled(
|
||||
@@ -1471,7 +1511,7 @@ impl GitRepository {
|
||||
pub fn fetch_with_transport_controlled(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<FetchOutcome, GitError> {
|
||||
@@ -1481,10 +1521,8 @@ impl GitRepository {
|
||||
if !same_remote_endpoint(&actual_url, configured.url())? {
|
||||
return Err(GitError::ForbiddenRemoteUrl);
|
||||
}
|
||||
let (_, server_id, application_id) = require_https_remote(configured)?;
|
||||
control.checkpoint(GitProgressPhase::Authenticating)?;
|
||||
let credential = credentials.credential(server_id, application_id)?;
|
||||
let received_pack = transport.fetch_controlled(self, configured, &credential, control)?;
|
||||
let received_pack = transport.fetch_controlled(self, configured, credentials, control)?;
|
||||
Ok(FetchOutcome {
|
||||
remote: name.to_owned(),
|
||||
received_pack,
|
||||
@@ -1498,9 +1536,9 @@ impl GitRepository {
|
||||
fn fetch_embedded(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credential: &GitCredential,
|
||||
credentials: &dyn GitRemoteCredentialProvider,
|
||||
) -> Result<bool, GitError> {
|
||||
self.fetch_embedded_controlled(configured, credential, &GitOperationControl::default())
|
||||
self.fetch_embedded_controlled(configured, credentials, &GitOperationControl::default())
|
||||
}
|
||||
|
||||
#[allow(
|
||||
@@ -1508,6 +1546,37 @@ impl GitRepository {
|
||||
reason = "the gix credential callback fixes its protocol error type"
|
||||
)]
|
||||
fn fetch_embedded_controlled(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credentials: &dyn GitRemoteCredentialProvider,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<bool, GitError> {
|
||||
match configured.endpoint() {
|
||||
RemoteEndpoint::Https(_) => {
|
||||
let (_, server_id, application_id) = require_https_remote(configured)?;
|
||||
let credential = credentials.credential(server_id, application_id)?;
|
||||
self.fetch_embedded_https(configured, &credential, control)
|
||||
}
|
||||
RemoteEndpoint::Ssh(_) => {
|
||||
#[cfg(feature = "ssh")]
|
||||
{
|
||||
self.fetch_embedded_ssh(configured, credentials, control)
|
||||
}
|
||||
#[cfg(not(feature = "ssh"))]
|
||||
{
|
||||
Err(GitError::UnsupportedRemoteTransport {
|
||||
transport: RemoteTransport::Ssh,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::result_large_err,
|
||||
reason = "the gix credential callback fixes its protocol error type"
|
||||
)]
|
||||
fn fetch_embedded_https(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credential: &GitCredential,
|
||||
@@ -1583,11 +1652,47 @@ impl GitRepository {
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssh")]
|
||||
fn fetch_embedded_ssh(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credentials: &dyn GitRemoteCredentialProvider,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<bool, GitError> {
|
||||
let session = crate::ssh::SshSession::connect(configured, credentials, control)?;
|
||||
let crate::ssh::SshGitCommand {
|
||||
transport,
|
||||
completion,
|
||||
} = session.open_upload_pack(configured, control)?;
|
||||
let fetch = (|| {
|
||||
let remote = self
|
||||
.repository
|
||||
.find_fetch_remote(Some(configured.name().as_str().into()))
|
||||
.map_err(invalid)?;
|
||||
let connection = remote.to_connection_with_transport(transport);
|
||||
let prepared = connection
|
||||
.prepare_fetch(gix::progress::Discard, Default::default())
|
||||
.map_err(|_| GitError::GitProtocolFailed)?;
|
||||
prepared
|
||||
.receive(gix::progress::Discard, control.cancelled.as_ref())
|
||||
.map_err(|error| map_ssh_fetch_error(error, control))
|
||||
})();
|
||||
let command = session.finish_command(completion, control);
|
||||
let close = session.close();
|
||||
command?;
|
||||
let outcome = fetch?;
|
||||
close?;
|
||||
Ok(matches!(
|
||||
outcome.status,
|
||||
gix::remote::fetch::Status::Change { .. }
|
||||
))
|
||||
}
|
||||
|
||||
pub fn pull(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
branch: Option<&str>,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
) -> Result<PullOutcome, GitError> {
|
||||
self.pull_with_transport(configured, branch, credentials, &EmbeddedFetchTransport)
|
||||
}
|
||||
@@ -1596,7 +1701,7 @@ impl GitRepository {
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
branch: Option<&str>,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
) -> Result<PullOutcome, GitError> {
|
||||
self.pull_with_transport_controlled(
|
||||
@@ -1612,7 +1717,7 @@ impl GitRepository {
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
branch: Option<&str>,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
transport: &impl GitFetchTransport,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<PullOutcome, GitError> {
|
||||
@@ -1833,7 +1938,7 @@ impl GitRepository {
|
||||
pub fn sync(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
) -> Result<(PullOutcome, PushOutcome), GitError> {
|
||||
let pull = self.pull(configured, None, credentials)?;
|
||||
let push = self.push(configured, None, credentials)?;
|
||||
@@ -1843,7 +1948,7 @@ impl GitRepository {
|
||||
pub fn sync_controlled(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
control: &GitOperationControl,
|
||||
) -> Result<(PullOutcome, PushOutcome), GitError> {
|
||||
self.sync_with_transports_controlled(
|
||||
@@ -1858,7 +1963,7 @@ impl GitRepository {
|
||||
pub fn sync_with_transports_controlled(
|
||||
&self,
|
||||
configured: &GitRemote,
|
||||
credentials: &impl GitCredentialProvider,
|
||||
credentials: &impl GitRemoteCredentialProvider,
|
||||
fetch: &impl GitFetchTransport,
|
||||
push: &impl GitSmartHttpTransport,
|
||||
control: &GitOperationControl,
|
||||
@@ -3784,6 +3889,9 @@ fn io(operation: &'static str, path: &Path) -> GitError {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "ssh"))]
|
||||
mod ssh_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{GitIdentity, GitRepository};
|
||||
|
||||
Reference in new issue
Block a user