cli: git: extract absolute_git_source() as utility function

This commit is contained in:
Yuya Nishihara 2025-01-11 15:28:58 +09:00
parent 7b47368c24
commit 20b3d02ff2
2 changed files with 71 additions and 73 deletions

View file

@ -15,7 +15,6 @@
use std::fs;
use std::io;
use std::io::Write;
use std::mem;
use std::num::NonZeroU32;
use std::path::Path;
@ -34,6 +33,7 @@ use crate::command_error::user_error;
use crate::command_error::user_error_with_message;
use crate::command_error::CommandError;
use crate::commands::git::maybe_add_gitignore;
use crate::git_util::absolute_git_url;
use crate::git_util::get_git_repo;
use crate::git_util::map_git_error;
use crate::git_util::print_git_import_stats;
@ -65,22 +65,6 @@ pub struct GitCloneArgs {
depth: Option<NonZeroU32>,
}
fn absolute_git_source(cwd: &Path, source: &str) -> Result<String, CommandError> {
// Git appears to turn URL-like source to absolute path if local git directory
// exits, and fails because '$PWD/https' is unsupported protocol. Since it would
// be tedious to copy the exact git (or libgit2) behavior, we simply let gix
// parse the input as URL, rcp-like, or local path.
let mut url = gix::url::parse(source.as_ref()).map_err(cli_error)?;
url.canonicalize(cwd).map_err(user_error)?;
// As of gix 0.68.0, the canonicalized path uses platform-native directory
// separator, which isn't compatible with libgit2 on Windows.
if url.scheme == gix::url::Scheme::File {
url.path = gix::path::to_unix_separators_on_windows(mem::take(&mut url.path)).into_owned();
}
// It's less likely that cwd isn't utf-8, so just fall back to original source.
Ok(String::from_utf8(url.to_bstring().into()).unwrap_or_else(|_| source.to_owned()))
}
fn clone_destination_for_source(source: &str) -> Option<&str> {
let destination = source.strip_suffix(".git").unwrap_or(source);
let destination = destination.strip_suffix('/').unwrap_or(destination);
@ -106,7 +90,7 @@ pub fn cmd_git_clone(
if command.global_args().at_operation.is_some() {
return Err(cli_error("--at-op is not respected"));
}
let source = absolute_git_source(command.cwd(), &args.source)?;
let source = absolute_git_url(command.cwd(), &args.source)?;
let wc_path_str = args
.destination
.as_deref()
@ -240,58 +224,3 @@ fn do_git_clone(
fetch_tx.finish(ui, "fetch from git remote into empty repo")?;
Ok((workspace_command, stats))
}
#[cfg(test)]
mod tests {
use std::path::MAIN_SEPARATOR;
use super::*;
#[test]
fn test_absolute_git_source() {
// gix::Url::canonicalize() works even if the path doesn't exist.
// However, we need to ensure that no symlinks exist at the test paths.
let temp_dir = testutils::new_temp_dir();
let cwd = dunce::canonicalize(temp_dir.path()).unwrap();
let cwd_slash = cwd.to_str().unwrap().replace(MAIN_SEPARATOR, "/");
// Local path
assert_eq!(
absolute_git_source(&cwd, "foo").unwrap(),
format!("{cwd_slash}/foo")
);
assert_eq!(
absolute_git_source(&cwd, r"foo\bar").unwrap(),
if cfg!(windows) {
format!("{cwd_slash}/foo/bar")
} else {
format!(r"{cwd_slash}/foo\bar")
}
);
assert_eq!(
absolute_git_source(&cwd.join("bar"), &format!("{cwd_slash}/foo")).unwrap(),
format!("{cwd_slash}/foo")
);
// rcp-like
assert_eq!(
absolute_git_source(&cwd, "git@example.org:foo/bar.git").unwrap(),
"git@example.org:foo/bar.git"
);
// URL
assert_eq!(
absolute_git_source(&cwd, "https://example.org/foo.git").unwrap(),
"https://example.org/foo.git"
);
// Custom scheme isn't an error
assert_eq!(
absolute_git_source(&cwd, "custom://example.org/foo.git").unwrap(),
"custom://example.org/foo.git"
);
// Password shouldn't be redacted (gix::Url::to_string() would do)
assert_eq!(
absolute_git_source(&cwd, "https://user:pass@example.org/").unwrap(),
"https://user:pass@example.org/"
);
}
}

View file

@ -19,6 +19,7 @@ use std::io;
use std::io::Read;
use std::io::Write;
use std::iter;
use std::mem;
use std::path::Path;
use std::path::PathBuf;
use std::process::Stdio;
@ -47,6 +48,7 @@ use unicode_width::UnicodeWidthStr;
use crate::cleanup_guard::CleanupGuard;
use crate::cli_util::WorkspaceCommandTransaction;
use crate::command_error::cli_error;
use crate::command_error::user_error;
use crate::command_error::user_error_with_hint;
use crate::command_error::CommandError;
@ -97,6 +99,23 @@ pub fn is_colocated_git_workspace(workspace: &Workspace, repo: &ReadonlyRepo) ->
dunce::canonicalize(git_workdir).ok().as_deref() == dot_git_path.parent()
}
/// Parses user-specified remote URL or path to absolute form.
pub fn absolute_git_url(cwd: &Path, source: &str) -> Result<String, CommandError> {
// Git appears to turn URL-like source to absolute path if local git directory
// exits, and fails because '$PWD/https' is unsupported protocol. Since it would
// be tedious to copy the exact git (or libgit2) behavior, we simply let gix
// parse the input as URL, rcp-like, or local path.
let mut url = gix::url::parse(source.as_ref()).map_err(cli_error)?;
url.canonicalize(cwd).map_err(user_error)?;
// As of gix 0.68.0, the canonicalized path uses platform-native directory
// separator, which isn't compatible with libgit2 on Windows.
if url.scheme == gix::url::Scheme::File {
url.path = gix::path::to_unix_separators_on_windows(mem::take(&mut url.path)).into_owned();
}
// It's less likely that cwd isn't utf-8, so just fall back to original source.
Ok(String::from_utf8(url.to_bstring().into()).unwrap_or_else(|_| source.to_owned()))
}
fn terminal_get_username(ui: &Ui, url: &str) -> Option<String> {
ui.prompt(&format!("Username for {url}")).ok()
}
@ -688,10 +707,60 @@ fn warn_if_branches_not_found(
#[cfg(test)]
mod tests {
use std::path::MAIN_SEPARATOR;
use insta::assert_snapshot;
use super::*;
#[test]
fn test_absolute_git_url() {
// gix::Url::canonicalize() works even if the path doesn't exist.
// However, we need to ensure that no symlinks exist at the test paths.
let temp_dir = testutils::new_temp_dir();
let cwd = dunce::canonicalize(temp_dir.path()).unwrap();
let cwd_slash = cwd.to_str().unwrap().replace(MAIN_SEPARATOR, "/");
// Local path
assert_eq!(
absolute_git_url(&cwd, "foo").unwrap(),
format!("{cwd_slash}/foo")
);
assert_eq!(
absolute_git_url(&cwd, r"foo\bar").unwrap(),
if cfg!(windows) {
format!("{cwd_slash}/foo/bar")
} else {
format!(r"{cwd_slash}/foo\bar")
}
);
assert_eq!(
absolute_git_url(&cwd.join("bar"), &format!("{cwd_slash}/foo")).unwrap(),
format!("{cwd_slash}/foo")
);
// rcp-like
assert_eq!(
absolute_git_url(&cwd, "git@example.org:foo/bar.git").unwrap(),
"git@example.org:foo/bar.git"
);
// URL
assert_eq!(
absolute_git_url(&cwd, "https://example.org/foo.git").unwrap(),
"https://example.org/foo.git"
);
// Custom scheme isn't an error
assert_eq!(
absolute_git_url(&cwd, "custom://example.org/foo.git").unwrap(),
"custom://example.org/foo.git"
);
// Password shouldn't be redacted (gix::Url::to_string() would do)
assert_eq!(
absolute_git_url(&cwd, "https://user:pass@example.org/").unwrap(),
"https://user:pass@example.org/"
);
}
#[test]
fn test_bar() {
let mut buf = String::new();