1
1
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:
copilot-swe-agent[bot]
2025-09-24 08:47:24 +02:00
committed by Sebastian Thiel
parent a80a150c75
commit 030e040b68
7 changed files with 173 additions and 8 deletions

View File

@@ -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(

View File

@@ -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) => {}

View File

@@ -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);

View File

@@ -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),

View 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),
}
}

View File

@@ -1 +1,2 @@
mod from_custom_definition;
mod main_validation;

View File

@@ -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 {