Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions src/daemon/git_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,9 +627,16 @@ fn takes_value(arg: &str) -> bool {
}

fn default_clone_target_from_source(source: &str) -> Option<PathBuf> {
let source = source.trim_end_matches('/');
let source = source.trim_end_matches(&['/', '\\'] as &[char]);
let source = source.strip_suffix(".git").unwrap_or(source);
let name = source.rsplit('/').next()?.rsplit(':').next()?.to_string();
// Split on both / and \ to handle Windows paths
let after_last_sep = source.rsplit(&['/', '\\'] as &[char]).next()?;
// Handle SCP-like syntax (user@host:path), but skip Windows drive letters (C:)
let name = if after_last_sep.contains(':') && after_last_sep.len() > 2 {
after_last_sep.rsplit(':').next()?
} else {
after_last_sep
};
if name.is_empty() {
return None;
}
Expand Down Expand Up @@ -711,7 +718,7 @@ fn parse_alias_tokens(value: &str) -> Option<Vec<String>> {

#[cfg(test)]
mod tests {
use super::{GitBackend, SystemGitBackend};
use super::{GitBackend, SystemGitBackend, default_clone_target_from_source};
use std::path::PathBuf;

#[test]
Expand All @@ -727,6 +734,38 @@ mod tests {
assert_eq!(resolved.as_deref(), Some("commit"));
}

#[test]
fn default_clone_target_from_url() {
assert_eq!(
default_clone_target_from_source("https://github.com/user/repo.git"),
Some(PathBuf::from("repo"))
);
assert_eq!(
default_clone_target_from_source("git@github.com:user/repo.git"),
Some(PathBuf::from("repo"))
);
assert_eq!(
default_clone_target_from_source("/local/path/repo"),
Some(PathBuf::from("repo"))
);
}

#[test]
fn default_clone_target_from_windows_path() {
assert_eq!(
default_clone_target_from_source(r"C:\Users\runner\Temp\repo"),
Some(PathBuf::from("repo"))
);
assert_eq!(
default_clone_target_from_source(r"C:\Users\runner\Temp\repo.git"),
Some(PathBuf::from("repo"))
);
assert_eq!(
default_clone_target_from_source(r"\\?\C:\Temp\bare-repo"),
Some(PathBuf::from("bare-repo"))
);
}

#[test]
fn unknown_primary_command_still_requires_repository_lookup() {
let backend = SystemGitBackend::new();
Expand Down
22 changes: 18 additions & 4 deletions src/git/cli_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,11 +685,12 @@ pub fn extract_clone_target_directory(args: &[String]) -> Option<String> {
/// Derive the target directory name from a repository URL.
/// Mimics git's behavior of using the last path component, stripping .git suffix.
fn derive_directory_from_url(url: &str) -> Option<String> {
// Remove trailing slashes
let url = url.trim_end_matches('/');
// Remove trailing slashes and backslashes (Windows)
let url = url.trim_end_matches(&['/', '\\'] as &[char]);

// Extract the last path component
let last_component = if let Some(pos) = url.rfind('/') {
// Extract the last path component (consider both / and \ for Windows paths)
let last_sep = url.rfind(&['/', '\\'] as &[char]);
let last_component = if let Some(pos) = last_sep {
&url[pos + 1..]
} else if let Some(pos) = url.rfind(':') {
// Handle SCP-like syntax: user@host:path
Expand Down Expand Up @@ -808,6 +809,19 @@ mod tests {
derive_directory_from_url("/local/path/repo.git"),
Some("repo".to_string())
);
// Windows backslash paths
assert_eq!(
derive_directory_from_url(r"C:\Users\runner\AppData\Local\Temp\repo"),
Some("repo".to_string())
);
assert_eq!(
derive_directory_from_url(r"C:\Users\runner\AppData\Local\Temp\repo.git"),
Some("repo".to_string())
);
assert_eq!(
derive_directory_from_url(r"\\?\C:\Temp\bare-repo"),
Some("bare-repo".to_string())
);
}

#[test]
Expand Down
Loading