mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Make uv init
resilient against broken git (#12895)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / lint (push) Waiting to run
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / lint (push) Waiting to run
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Currently, `uv init` works without a `git` executable, and with a working `git` executable, but not with a broken `git`, be it from GitHub Action's Windows CI or from the shim we insert. `uv init` calls git twice: Once `git rev-parse` to check whether a git repo already exists, and then `git init` (if there is no git repository yet and no `--vcs none`). By separately handling the cases where git failed during `git rev-parse` doesn't work vs. where the is no repository when checking for an existing repo work tree, we can avoid calling `git init` for broken git and erroring. We have to hardcode the expected git command outputs to be able to check.
This commit is contained in:
parent
211e712b83
commit
37a71fd26a
4 changed files with 162 additions and 79 deletions
|
@ -3,7 +3,6 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::{Command, Stdio};
|
||||
|
||||
use serde::Deserialize;
|
||||
use tracing::debug;
|
||||
use uv_git::GIT;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
@ -40,25 +39,21 @@ impl VersionControlSystem {
|
|||
return Err(VersionControlError::GitNotInstalled);
|
||||
};
|
||||
|
||||
if path.join(".git").try_exists()? {
|
||||
debug!("Git repository already exists at: `{}`", path.display());
|
||||
} else {
|
||||
let output = Command::new(git)
|
||||
.arg("init")
|
||||
.current_dir(path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.map_err(VersionControlError::GitCommand)?;
|
||||
if !output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(VersionControlError::GitInit(
|
||||
path.to_path_buf(),
|
||||
stdout.to_string(),
|
||||
stderr.to_string(),
|
||||
));
|
||||
}
|
||||
let output = Command::new(git)
|
||||
.arg("init")
|
||||
.current_dir(path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.map_err(VersionControlError::GitCommand)?;
|
||||
if !output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(VersionControlError::GitInit(
|
||||
path.to_path_buf(),
|
||||
stdout.to_string(),
|
||||
stderr.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Create the `.gitignore`, if it doesn't exist.
|
||||
|
@ -77,25 +72,6 @@ impl VersionControlSystem {
|
|||
Self::None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects the VCS system based on the provided path.
|
||||
pub fn detect(path: &Path) -> Option<Self> {
|
||||
// Determine whether the path is inside a Git work tree.
|
||||
let git = GIT.as_ref().ok()?;
|
||||
let exit_status = Command::new(git)
|
||||
.arg("rev-parse")
|
||||
.arg("--is-inside-work-tree")
|
||||
.current_dir(path)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.ok()?;
|
||||
if exit_status.success() {
|
||||
return Some(Self::Git);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VersionControlSystem {
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::{Command, Stdio};
|
||||
use std::str::FromStr;
|
||||
|
||||
use tracing::{debug, warn};
|
||||
use tracing::{debug, trace, warn};
|
||||
use uv_cache::Cache;
|
||||
use uv_cli::AuthorFrom;
|
||||
use uv_client::BaseClientBuilder;
|
||||
|
@ -1184,52 +1184,98 @@ fn generate_package_scripts(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize the version control system at the given path.
|
||||
#[derive(Debug, Clone)]
|
||||
enum GitDiscoveryResult {
|
||||
/// Git is initialized at the path.
|
||||
Repository,
|
||||
/// Git is not initialized at the path.
|
||||
NoRepository,
|
||||
/// There is no `git[.exe]` binary in PATH.
|
||||
NoGit,
|
||||
/// There is a `git[.exe]` binary in PATH, but it returned an unexpected output.
|
||||
BrokenGit,
|
||||
}
|
||||
|
||||
/// Checks if there is a Git work tree at the given path.
|
||||
fn detect_git_repository(path: &Path) -> GitDiscoveryResult {
|
||||
// Determine whether the path is inside a Git work tree.
|
||||
let Ok(git) = GIT.as_ref() else {
|
||||
return GitDiscoveryResult::NoGit;
|
||||
};
|
||||
let Ok(output) = Command::new(git)
|
||||
.arg("rev-parse")
|
||||
.arg("--is-inside-work-tree")
|
||||
.current_dir(path)
|
||||
.output()
|
||||
else {
|
||||
debug!(
|
||||
"`git rev-parse --is-inside-work-tree` failed to launch for `{}`",
|
||||
path.display()
|
||||
);
|
||||
return GitDiscoveryResult::BrokenGit;
|
||||
};
|
||||
if output.status.success() {
|
||||
if std::str::from_utf8(&output.stdout).map(str::trim) == Ok("true") {
|
||||
debug!("Found a Git repository for `{}`", path.display());
|
||||
GitDiscoveryResult::Repository
|
||||
} else {
|
||||
debug!(
|
||||
"`git rev-parse --is-inside-work-tree` succeeded but didn't return `true` for `{}`",
|
||||
path.display()
|
||||
);
|
||||
trace!(
|
||||
"`git rev-parse --is-inside-work-tree` stdout: {:?}",
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
);
|
||||
GitDiscoveryResult::BrokenGit
|
||||
}
|
||||
} else {
|
||||
if std::str::from_utf8(&output.stderr).is_ok_and(|err| err.contains("not a git repository"))
|
||||
{
|
||||
debug!("Not a Git repository `{}`", path.display());
|
||||
GitDiscoveryResult::NoRepository
|
||||
} else {
|
||||
debug!(
|
||||
"`git rev-parse --is-inside-work-tree` failed but didn't contain `not a git repository` in stderr for `{}`",
|
||||
path.display()
|
||||
);
|
||||
GitDiscoveryResult::BrokenGit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the version control system at the given path, if applicable.
|
||||
fn init_vcs(path: &Path, vcs: Option<VersionControlSystem>) -> Result<()> {
|
||||
// Detect any existing version control system.
|
||||
let existing = VersionControlSystem::detect(path);
|
||||
|
||||
let implicit = vcs.is_none();
|
||||
|
||||
let vcs = match (vcs, existing) {
|
||||
// If no version control system was specified, and none was detected, default to Git.
|
||||
(None, None) => VersionControlSystem::default(),
|
||||
// If no version control system was specified, but a VCS was detected, leave it as-is.
|
||||
(None, Some(existing)) => {
|
||||
debug!("Detected existing version control system: {existing}");
|
||||
VersionControlSystem::None
|
||||
}
|
||||
// If the user provides an explicit `--vcs none`,
|
||||
(Some(VersionControlSystem::None), _) => VersionControlSystem::None,
|
||||
// If a version control system was specified, use it.
|
||||
(Some(vcs), None) => vcs,
|
||||
// If a version control system was specified, but a VCS was detected...
|
||||
(Some(vcs), Some(existing)) => {
|
||||
// If they differ, raise an error.
|
||||
if vcs != existing {
|
||||
anyhow::bail!("The project is already in a version control system (`{existing}`); cannot initialize with `--vcs {vcs}`");
|
||||
}
|
||||
|
||||
// Otherwise, ignore the specified VCS, since it's already in use.
|
||||
VersionControlSystem::None
|
||||
}
|
||||
// vcs is None for an existing repository because we don't want to initialize again.
|
||||
let (vcs, implicit) = match vcs {
|
||||
None => match detect_git_repository(path) {
|
||||
GitDiscoveryResult::NoRepository => (VersionControlSystem::Git, true),
|
||||
GitDiscoveryResult::Repository
|
||||
| GitDiscoveryResult::NoGit
|
||||
| GitDiscoveryResult::BrokenGit => (VersionControlSystem::None, false),
|
||||
},
|
||||
Some(VersionControlSystem::None) => (VersionControlSystem::None, false),
|
||||
// The user requested Git explicitly, so the only reason not to invoke it is that Git is
|
||||
// already initialized. In case of an error (broken git), we will raise the real error
|
||||
// when trying to initialize, which should give us a better error message.
|
||||
Some(VersionControlSystem::Git) => match detect_git_repository(path) {
|
||||
GitDiscoveryResult::NoRepository
|
||||
| GitDiscoveryResult::BrokenGit
|
||||
| GitDiscoveryResult::NoGit => (VersionControlSystem::Git, false),
|
||||
GitDiscoveryResult::Repository => (VersionControlSystem::None, false),
|
||||
},
|
||||
};
|
||||
|
||||
// Attempt to initialize the VCS.
|
||||
match vcs.init(path) {
|
||||
Ok(()) => (),
|
||||
Ok(()) => Ok(()),
|
||||
// If the VCS isn't installed, only raise an error if a VCS was explicitly specified.
|
||||
Err(err @ VersionControlError::GitNotInstalled) => {
|
||||
if implicit {
|
||||
debug!("Failed to initialize version control: {err}");
|
||||
} else {
|
||||
return Err(err.into());
|
||||
}
|
||||
Err(err @ VersionControlError::GitNotInstalled) if implicit => {
|
||||
debug!("Failed to initialize version control: {err}");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to get the author information.
|
||||
|
|
|
@ -619,7 +619,7 @@ impl TestContext {
|
|||
command
|
||||
}
|
||||
|
||||
fn disallow_git_cli(bin_dir: &Path) -> std::io::Result<()> {
|
||||
pub fn disallow_git_cli(bin_dir: &Path) -> std::io::Result<()> {
|
||||
let contents = r"#!/bin/sh
|
||||
echo 'error: `git` operations are not allowed — are you missing a cfg for the `git` feature?' >&2
|
||||
exit 127";
|
||||
|
|
|
@ -3732,3 +3732,64 @@ fn init_python_variant() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check how `uv init` reacts to working and broken git with different `--vcs` options.
|
||||
#[test]
|
||||
fn git_states() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// First, with working git.
|
||||
|
||||
context.init().arg("working").assert().success();
|
||||
assert!(context.temp_dir.child("working/.git").is_dir());
|
||||
|
||||
context
|
||||
.init()
|
||||
.arg("working-no-git")
|
||||
.arg("--vcs")
|
||||
.arg("none")
|
||||
.assert()
|
||||
.success();
|
||||
assert!(!context.temp_dir.child("working-no-git/.git").is_dir());
|
||||
|
||||
context
|
||||
.init()
|
||||
.arg("working-git")
|
||||
.arg("--vcs")
|
||||
.arg("git")
|
||||
.assert()
|
||||
.success();
|
||||
assert!(context.temp_dir.child("working-git/.git").is_dir());
|
||||
|
||||
// The same tests again, but with broken git.
|
||||
TestContext::disallow_git_cli(&context.bin_dir)
|
||||
.expect("Failed to setup disallowed `git` command");
|
||||
|
||||
context.init().arg("broken").assert().success();
|
||||
assert!(!context.temp_dir.child("broken/.git").is_dir());
|
||||
|
||||
context
|
||||
.init()
|
||||
.arg("broken-no-git")
|
||||
.arg("--vcs")
|
||||
.arg("none")
|
||||
.assert()
|
||||
.success();
|
||||
assert!(!context.temp_dir.child("broken-no-git/.git").is_dir());
|
||||
|
||||
uv_snapshot!(context.filters(), context
|
||||
.init()
|
||||
.arg("broken-git")
|
||||
.arg("--vcs")
|
||||
.arg("git"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Failed to initialize Git repository at `[TEMP_DIR]/broken-git`
|
||||
stdout:
|
||||
stderr: error: `git` operations are not allowed — are you missing a cfg for the `git` feature?
|
||||
");
|
||||
assert!(!context.temp_dir.child("broken-git/.git").is_dir());
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue