Skip to content
Open
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
102 changes: 94 additions & 8 deletions crates/gl/src/mirror.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,7 @@ pub async fn run(args: MirrorArgs) -> Result<()> {
let _guard = TmpGuard(tmp_root);

println!("Cloning source (this may take a while for large repos)...");
let clone_status = Command::new("git")
.args(["clone", "--mirror", &source, mirror_path.to_str().unwrap()])
.status()
.context("failed to run git clone — is git installed?")?;

if !clone_status.success() {
bail!("git clone --mirror failed\nCheck that the source URL is accessible: {source}");
}
clone_mirror(&source, &mirror_path)?;

// ── 4. Create the repo on gitlawb ─────────────────────────────────────
println!("Creating repo on gitlawb node...");
Expand Down Expand Up @@ -139,6 +132,25 @@ pub async fn run(args: MirrorArgs) -> Result<()> {
Ok(())
}

/// `git clone --mirror <source> <dest>`. The destination is passed as an
/// OS-native path so a non-UTF-8 temp dir (e.g. a TMPDIR with non-UTF-8 bytes)
/// does not panic on a `to_str()` conversion.
fn clone_mirror(source: &str, dest: &std::path::Path) -> Result<()> {
// allow-unbounded-git: gl is the client CLI; this clone runs in the user's
// terminal with no server-side permit or disconnect to survive. The
// bounded-runner rule governs node request handlers. Same spawn as before,
// only relocated into this helper.
let status = Command::new("git")
.args(["clone", "--mirror", source])
.arg(dest)
.status()
.context("failed to run git clone — is git installed?")?;
if !status.success() {
bail!("git clone --mirror failed\nCheck that the source URL is accessible: {source}");
}
Ok(())
}

/// Extract the repo name from a git URL.
/// - `https://github.com/owner/repo` → `"repo"`
/// - `https://github.com/owner/repo.git` → `"repo"`
Expand Down Expand Up @@ -221,6 +233,80 @@ mod tests {
assert_eq!(extract_repo_name("https://example.com/.git"), None);
}

// #417: a non-UTF-8 destination path must reach git as an OS-native arg, not
// panic on a `to_str()` conversion.
#[cfg(unix)]
#[test]
fn test_clone_mirror_non_utf8_dest() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;

let tmp = tempfile::TempDir::new().unwrap();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let src = tmp.path().join("src.git");
let st = Command::new("git")
.args(["init", "--bare"])
.arg(&src)
.status()
.unwrap();
assert!(st.success());

let dest = tmp.path().join(OsStr::from_bytes(b"mirror-\xffdest"));
clone_mirror(src.to_str().unwrap(), &dest).unwrap();
assert!(dest.join("HEAD").exists());
}

// #417 end-to-end: the real trigger is `std::env::temp_dir()` itself
// returning a non-UTF-8 path (a TMPDIR with non-UTF-8 bytes), because the
// mirror dest is built from it. Env is process-global, so this test
// re-execs the test binary in a child with a non-UTF-8 TMPDIR rather than
// mutating this process's env.
#[cfg(unix)]
#[test]
fn test_clone_mirror_under_non_utf8_tmpdir() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;

if std::env::var_os("GL_MIRROR_NONUTF8_TMPDIR").is_none() {
// Parent: a source repo under a normal path, a TMPDIR whose name
// is not UTF-8, and a child run of this same test under that env.
let parent = tempfile::TempDir::new().unwrap();
let src = parent.path().join("src.git");
let st = Command::new("git")
.args(["init", "--bare"])
.arg(&src)
.status()
.unwrap();
assert!(st.success());
let bad_tmp = parent.path().join(OsStr::from_bytes(b"tmp-\xff"));
std::fs::create_dir_all(&bad_tmp).unwrap();

let status = Command::new(std::env::current_exe().unwrap())
.args([
"mirror::tests::test_clone_mirror_under_non_utf8_tmpdir",
"--exact",
])
.env("GL_MIRROR_NONUTF8_TMPDIR", "1")
.env("GL_MIRROR_SRC", &src)
.env("TMPDIR", &bad_tmp)
.status()
.unwrap();
assert!(status.success(), "child failed under non-UTF-8 TMPDIR");
return;
}

// Child: the same path shape run() computes — temp_dir() joined with
// the mirror dest. It must be non-UTF-8 or the test proves nothing.
let src = std::env::var("GL_MIRROR_SRC").unwrap();
let tmp_root = std::env::temp_dir().join("gl-mirror-child");
let dest = tmp_root.join("repo");
assert!(
dest.to_str().is_none(),
"fixture broken: dest under a non-UTF-8 TMPDIR must be non-UTF-8"
);
clone_mirror(&src, &dest).unwrap();
assert!(dest.join("HEAD").exists());
}

#[tokio::test]
async fn test_create_repo_conflict_error() {
let mut server = mockito::Server::new_async().await;
Expand Down
Loading