Skip to content

Commit

Permalink
cli: git: extract absolute_git_source() as utility function
Browse files Browse the repository at this point in the history
  • Loading branch information
yuja committed Jan 12, 2025
1 parent 7b47368 commit 20b3d02
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 73 deletions.
75 changes: 2 additions & 73 deletions cli/src/commands/git/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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()
Expand Down Expand Up @@ -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, "[email protected]:foo/bar.git").unwrap(),
"[email protected]: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:[email protected]/").unwrap(),
"https://user:[email protected]/"
);
}
}
69 changes: 69 additions & 0 deletions cli/src/git_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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, "[email protected]:foo/bar.git").unwrap(),
"[email protected]: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:[email protected]/").unwrap(),
"https://user:[email protected]/"
);
}

#[test]
fn test_bar() {
let mut buf = String::new();
Expand Down

0 comments on commit 20b3d02

Please sign in to comment.