mirror of
https://github.com/Byron/gitoxide
synced 2025-10-06 01:52:40 +02:00
fix: credential fill to allow protocol+host without URL
Co-authored-by: Byron <63622+Byron@users.noreply.github.com>
This commit is contained in:
committed by
Sebastian Thiel
parent
a80a150c75
commit
030e040b68
@@ -15,8 +15,11 @@ pub fn function(repo: gix::Repository, action: gix::credentials::program::main::
|
||||
std::io::stdin(),
|
||||
std::io::stdout(),
|
||||
|action, context| -> Result<_, Error> {
|
||||
let url = context.url.clone().or_else(|| context.to_url()).ok_or_else(|| {
|
||||
Error::Protocol(gix::credentials::protocol::Error::UrlMissing)
|
||||
})?;
|
||||
let (mut cascade, _action, prompt_options) = repo.config_snapshot().credential_helpers(gix::url::parse(
|
||||
context.url.as_ref().expect("framework assures URL is present").as_ref(),
|
||||
url.as_ref(),
|
||||
)?)?;
|
||||
cascade
|
||||
.invoke(
|
||||
|
@@ -55,7 +55,7 @@ pub enum Error {
|
||||
Context(#[from] crate::protocol::context::decode::Error),
|
||||
#[error("Credentials for {url:?} could not be obtained")]
|
||||
CredentialsMissing { url: BString },
|
||||
#[error("The url wasn't provided in input - the git credentials protocol mandates this")]
|
||||
#[error("Either 'url' field or both 'protocol' and 'host' fields must be provided")]
|
||||
UrlMissing,
|
||||
}
|
||||
|
||||
@@ -89,15 +89,17 @@ pub(crate) mod function {
|
||||
let mut buf = Vec::<u8>::with_capacity(512);
|
||||
stdin.read_to_end(&mut buf)?;
|
||||
let ctx = Context::from_bytes(&buf)?;
|
||||
if ctx.url.is_none() {
|
||||
if ctx.url.is_none() && (ctx.protocol.is_none() || ctx.host.is_none()) {
|
||||
return Err(Error::UrlMissing);
|
||||
}
|
||||
let res = credentials(action, ctx).map_err(|err| Error::Helper { source: Box::new(err) })?;
|
||||
match (action, res) {
|
||||
(Action::Get, None) => {
|
||||
return Err(Error::CredentialsMissing {
|
||||
url: Context::from_bytes(&buf)?.url.expect("present and checked above"),
|
||||
})
|
||||
let ctx_for_error = Context::from_bytes(&buf)?;
|
||||
let url = ctx_for_error.url.clone()
|
||||
.or_else(|| ctx_for_error.to_url())
|
||||
.expect("URL is available either directly or via protocol+host");
|
||||
return Err(Error::CredentialsMissing { url })
|
||||
}
|
||||
(Action::Get, Some(ctx)) => ctx.write_to(stdout)?,
|
||||
(Action::Erase | Action::Store, None) => {}
|
||||
|
@@ -92,7 +92,16 @@ mod mutate {
|
||||
/// normally this isn't the case.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn destructure_url_in_place(&mut self, use_http_path: bool) -> Result<&mut Self, protocol::Error> {
|
||||
let url = gix_url::parse(self.url.as_ref().ok_or(protocol::Error::UrlMissing)?.as_ref())?;
|
||||
if self.url.is_none() {
|
||||
// If URL is not present but protocol and host are, construct the URL
|
||||
if let Some(constructed_url) = self.to_url() {
|
||||
self.url = Some(constructed_url);
|
||||
} else {
|
||||
return Err(protocol::Error::UrlMissing);
|
||||
}
|
||||
}
|
||||
|
||||
let url = gix_url::parse(self.url.as_ref().expect("URL is present after check above").as_ref())?;
|
||||
self.protocol = Some(url.scheme.as_str().into());
|
||||
self.username = url.user().map(ToOwned::to_owned);
|
||||
self.password = url.password().map(ToOwned::to_owned);
|
||||
|
@@ -20,7 +20,7 @@ pub type Result = std::result::Result<Option<Outcome>, Error>;
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
UrlParse(#[from] gix_url::parse::Error),
|
||||
#[error("The 'url' field must be set when performing a 'get/fill' action")]
|
||||
#[error("Either 'url' field or both 'protocol' and 'host' fields must be provided")]
|
||||
UrlMissing,
|
||||
#[error(transparent)]
|
||||
ContextDecode(#[from] context::decode::Error),
|
||||
|
101
gix-credentials/tests/program/main_validation.rs
Normal file
101
gix-credentials/tests/program/main_validation.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use gix_credentials::program::main;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Test error")]
|
||||
struct TestError;
|
||||
|
||||
#[test]
|
||||
fn protocol_and_host_without_url_is_valid() {
|
||||
let input = b"protocol=https\nhost=github.com\n";
|
||||
let mut output = Vec::new();
|
||||
|
||||
let result = main(
|
||||
["get".into()],
|
||||
Cursor::new(input),
|
||||
&mut output,
|
||||
|_action, context| -> Result<Option<gix_credentials::protocol::Context>, TestError> {
|
||||
// Verify the context has the expected fields
|
||||
assert_eq!(context.protocol.as_deref(), Some("https"));
|
||||
assert_eq!(context.host.as_deref(), Some("github.com"));
|
||||
// Should return None to simulate no credentials found (which is expected in test)
|
||||
Ok(None)
|
||||
},
|
||||
);
|
||||
|
||||
// This should fail because our mock helper returned None (no credentials found)
|
||||
// but it should NOT fail because of missing URL
|
||||
match result {
|
||||
Err(gix_credentials::program::main::Error::CredentialsMissing { .. }) => {
|
||||
// This is the expected error - credentials missing, not URL missing
|
||||
}
|
||||
other => panic!("Expected CredentialsMissing error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_protocol_with_host_fails() {
|
||||
let input = b"host=github.com\n";
|
||||
let mut output = Vec::new();
|
||||
|
||||
let result = main(
|
||||
["get".into()],
|
||||
Cursor::new(input),
|
||||
&mut output,
|
||||
|_action, _context| -> Result<Option<gix_credentials::protocol::Context>, TestError> { Ok(None) },
|
||||
);
|
||||
|
||||
match result {
|
||||
Err(gix_credentials::program::main::Error::UrlMissing) => {
|
||||
// This is expected
|
||||
}
|
||||
other => panic!("Expected UrlMissing error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_host_with_protocol_fails() {
|
||||
let input = b"protocol=https\n";
|
||||
let mut output = Vec::new();
|
||||
|
||||
let result = main(
|
||||
["get".into()],
|
||||
Cursor::new(input),
|
||||
&mut output,
|
||||
|_action, _context| -> Result<Option<gix_credentials::protocol::Context>, TestError> { Ok(None) },
|
||||
);
|
||||
|
||||
match result {
|
||||
Err(gix_credentials::program::main::Error::UrlMissing) => {
|
||||
// This is expected
|
||||
}
|
||||
other => panic!("Expected UrlMissing error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_alone_is_still_valid() {
|
||||
let input = b"url=https://github.com\n";
|
||||
let mut output = Vec::new();
|
||||
|
||||
let result = main(
|
||||
["get".into()],
|
||||
Cursor::new(input),
|
||||
&mut output,
|
||||
|_action, context| -> Result<Option<gix_credentials::protocol::Context>, TestError> {
|
||||
// Verify the context has the expected fields
|
||||
assert_eq!(context.url.as_deref().map(|b| &**b), Some("https://github.com".as_bytes()));
|
||||
// Should return None to simulate no credentials found (which is expected in test)
|
||||
Ok(None)
|
||||
},
|
||||
);
|
||||
|
||||
// This should fail because our mock helper returned None (no credentials found)
|
||||
// but it should NOT fail because of missing URL
|
||||
match result {
|
||||
Err(gix_credentials::program::main::Error::CredentialsMissing { .. }) => {
|
||||
// This is the expected error - credentials missing, not URL missing
|
||||
}
|
||||
other => panic!("Expected CredentialsMissing error, got: {:?}", other),
|
||||
}
|
||||
}
|
@@ -1 +1,2 @@
|
||||
mod from_custom_definition;
|
||||
mod main_validation;
|
||||
|
@@ -72,6 +72,55 @@ mod destructure_url_in_place {
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_and_host_without_url_constructs_url_first() {
|
||||
let mut ctx = Context {
|
||||
protocol: Some("https".into()),
|
||||
host: Some("github.com".into()),
|
||||
..Default::default()
|
||||
};
|
||||
ctx.destructure_url_in_place(false).expect("should work with protocol and host");
|
||||
|
||||
// URL should be constructed from protocol and host
|
||||
assert_eq!(ctx.url.as_deref().map(|b| &**b), Some("https://github.com".as_bytes()));
|
||||
// Original fields should be preserved
|
||||
assert_eq!(ctx.protocol.as_deref(), Some("https"));
|
||||
assert_eq!(ctx.host.as_deref(), Some("github.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_and_host_with_path_without_url_constructs_full_url() {
|
||||
let mut ctx = Context {
|
||||
protocol: Some("https".into()),
|
||||
host: Some("github.com".into()),
|
||||
path: Some("org/repo".into()),
|
||||
..Default::default()
|
||||
};
|
||||
ctx.destructure_url_in_place(false).expect("should work with protocol, host and path");
|
||||
|
||||
// URL should be constructed from all provided fields
|
||||
assert_eq!(ctx.url.as_deref().map(|b| &**b), Some("https://github.com/org/repo".as_bytes()));
|
||||
// Original fields should be preserved
|
||||
assert_eq!(ctx.protocol.as_deref(), Some("https"));
|
||||
assert_eq!(ctx.host.as_deref(), Some("github.com"));
|
||||
assert_eq!(ctx.path.as_deref().map(|b| &**b), Some("org/repo".as_bytes()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_protocol_or_host_without_url_fails() {
|
||||
let mut ctx_no_protocol = Context {
|
||||
host: Some("github.com".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(ctx_no_protocol.destructure_url_in_place(false).is_err());
|
||||
|
||||
let mut ctx_no_host = Context {
|
||||
protocol: Some("https".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(ctx_no_host.destructure_url_in_place(false).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod to_prompt {
|
||||
|
Reference in New Issue
Block a user