From 087ebc45116ef97d46fcc8ff6346fc62902065aa Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 25 Sep 2024 22:21:57 -0400 Subject: [PATCH] Address PR feedback --- Cargo.lock | 3 - crates/uv-cli/src/lib.rs | 12 +- crates/uv-configuration/Cargo.toml | 4 - crates/uv-configuration/src/lib.rs | 4 +- crates/uv-configuration/src/vcs.rs | 120 ++++++++++++++++++++ crates/uv-configuration/src/vcs_options.rs | 123 --------------------- crates/uv/src/commands/project/init.rs | 110 ++++++++++-------- crates/uv/src/lib.rs | 2 +- crates/uv/src/settings.rs | 6 +- crates/uv/tests/init.rs | 17 ++- docs/reference/cli.md | 6 +- 11 files changed, 206 insertions(+), 201 deletions(-) create mode 100644 crates/uv-configuration/src/vcs.rs delete mode 100644 crates/uv-configuration/src/vcs_options.rs diff --git a/Cargo.lock b/Cargo.lock index 586b49139acc3..fc5da8e685d67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4712,7 +4712,6 @@ dependencies = [ "clap", "either", "fs-err", - "indoc", "pep508_rs", "platform-tags", "pypi-types", @@ -4726,9 +4725,7 @@ dependencies = [ "uv-auth", "uv-cache", "uv-cache-info", - "uv-fs", "uv-normalize", - "uv-warnings", "which", ] diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 0b00c0c6bfeb0..68b4dba9aada7 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -14,7 +14,7 @@ use url::Url; use uv_cache::CacheArgs; use uv_configuration::{ ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, - TargetTriple, TrustedHost, TrustedPublishing, VersionControl, + TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, }; use uv_normalize::{ExtraName, PackageName}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; @@ -2373,12 +2373,12 @@ pub struct InitArgs { #[arg(long, alias="script", conflicts_with_all=["app", "lib", "package"])] pub r#script: bool, - /// Initialize a new repository for the given version control system. + /// Initialize a version control system for the project. /// - /// By default, uv will try to initialize a Git repository (`git`). - /// Use `none` to skip repository initialization. - #[arg(long, value_enum)] - pub vcs: Option, + /// By default, uv will initialize a Git repository (`git`). Use `--vcs none` to explicitly + /// avoid initializing a version control system. + #[arg(long, value_enum, conflicts_with = "script")] + pub vcs: Option, /// Do not create a `README.md` file. #[arg(long)] diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index be241af14e6cb..2d91036620c13 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -20,15 +20,11 @@ pypi-types = { workspace = true } uv-auth = { workspace = true } uv-cache = { workspace = true } uv-cache-info = { workspace = true } -uv-fs = { workspace = true } uv-normalize = { workspace = true } -uv-warnings = { workspace = true } -anyhow = { workspace = true } clap = { workspace = true, features = ["derive"], optional = true } either = { workspace = true } fs-err = { workspace = true } -indoc = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true } diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index ce39194b7fedf..fbd40a32beb84 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -17,7 +17,7 @@ pub use sources::*; pub use target_triple::*; pub use trusted_host::*; pub use trusted_publishing::*; -pub use vcs_options::*; +pub use vcs::*; mod authentication; mod build_options; @@ -38,4 +38,4 @@ mod sources; mod target_triple; mod trusted_host; mod trusted_publishing; -mod vcs_options; +mod vcs; diff --git a/crates/uv-configuration/src/vcs.rs b/crates/uv-configuration/src/vcs.rs new file mode 100644 index 0000000000000..6a56e8f36cc4c --- /dev/null +++ b/crates/uv-configuration/src/vcs.rs @@ -0,0 +1,120 @@ +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use serde::Deserialize; +use tracing::debug; + +#[derive(Debug, thiserror::Error)] +pub enum VersionControlError { + #[error("Attempted to initialize a Git repository, but `git` was not found in PATH")] + GitNotInstalled, + #[error("Failed to initialize Git repository at `{0}`\nstdout: {1}\nstderr: {2}")] + GitInit(PathBuf, String, String), + #[error("`git` command failed")] + GitCommand(#[source] std::io::Error), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +/// The version control system to use. +#[derive(Clone, Copy, Debug, PartialEq, Default, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum VersionControlSystem { + /// Use Git for version control. + #[default] + Git, + /// Do not use any version control system. + None, +} + +impl VersionControlSystem { + /// Initializes the VCS system based on the provided path. + pub fn init(&self, path: &Path) -> Result<(), VersionControlError> { + match self { + Self::Git => { + let Ok(git) = which::which("git") else { + 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(), + )); + } + } + + // Create the `.gitignore`, if it doesn't exist. + match fs_err::OpenOptions::new() + .write(true) + .create_new(true) + .open(path.join(".gitignore")) + { + Ok(mut file) => file.write_all(GITIGNORE.as_bytes())?, + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => (), + Err(err) => return Err(err.into()), + } + + Ok(()) + } + Self::None => Ok(()), + } + } + + /// Detects the VCS system based on the provided path. + pub fn detect(path: &Path) -> Option { + // Determine whether the path is inside a Git work tree. + if which::which("git").is_ok_and(|git| { + Command::new(git) + .arg("rev-parse") + .arg("--is-inside-work-tree") + .current_dir(path) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|status| status.success()) + .unwrap_or(false) + }) { + return Some(Self::Git); + } + + None + } +} + +impl std::fmt::Display for VersionControlSystem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Git => write!(f, "git"), + Self::None => write!(f, "none"), + } + } +} + +const GITIGNORE: &str = "# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +"; diff --git a/crates/uv-configuration/src/vcs_options.rs b/crates/uv-configuration/src/vcs_options.rs deleted file mode 100644 index a8b6afc798e22..0000000000000 --- a/crates/uv-configuration/src/vcs_options.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::path::Path; -use std::process::{Command, Stdio}; -use std::str::FromStr; - -use anyhow::{Context, Result}; -use serde::Deserialize; - -use uv_fs::Simplified; -use uv_warnings::warn_user; - -/// The version control system to use. -#[derive(Clone, Copy, Debug, PartialEq, Default, Deserialize)] -#[serde(deny_unknown_fields, rename_all = "kebab-case")] -#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub enum VersionControl { - /// Use Git for version control. - #[default] - Git, - - /// Do not use version control. - None, -} - -impl VersionControl { - /// Initializes the VCS system based on the provided path. - pub fn init(&self, path: &Path) -> Result<()> { - match self { - VersionControl::None => Ok(()), - VersionControl::Git => Self::init_git(path), - } - } - - fn init_git(path: &Path) -> Result<()> { - let Ok(git) = which::which("git") else { - anyhow::bail!("could not find `git` in PATH"); - }; - - if path.join(".git").try_exists()? { - warn_user!( - "Git repository already exists at `{}`", - path.simplified_display() - ); - } else { - if !Command::new(git) - .arg("init") - .current_dir(path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .context("failed to run `git init`")? - .success() - { - anyhow::bail!("`git init` failed at `{}`", path.simplified_display()); - } - } - - // Create the `.gitignore` if it does not already exist. - let gitignore = path.join(".gitignore"); - if !gitignore.try_exists()? { - fs_err::write(gitignore, gitignore_content())?; - }; - - Ok(()) - } -} - -/// The default content for a `.gitignore` file in a Python project. -fn gitignore_content() -> &'static str { - indoc::indoc! {r" - # Python generated files - __pycache__/ - *.py[oc] - build/ - dist/ - wheels/ - *.egg-info - - # venv - .venv - "} -} - -impl FromStr for VersionControl { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "git" => Ok(VersionControl::Git), - "none" => Ok(VersionControl::None), - other => Err(format!("unknown vcs specification: `{other}`")), - } - } -} - -impl std::fmt::Display for VersionControl { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VersionControl::Git => write!(f, "git"), - VersionControl::None => write!(f, "none"), - } - } -} - -/// Check if the path is inside a VCS repository. -/// -/// Currently only supports Git. -pub fn existing_vcs_repo(dir: &Path) -> bool { - is_inside_git_work_tree(dir) -} - -/// Check if the path is inside a Git work tree. -fn is_inside_git_work_tree(dir: &Path) -> bool { - Command::new("git") - .arg("rev-parse") - .arg("--is-inside-work-tree") - .current_dir(dir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map(|status| status.success()) - .unwrap_or(false) -} diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index c734c42e6c4bf..df60ce233446f 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -9,7 +9,7 @@ use pep508_rs::PackageName; use tracing::{debug, warn}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity}; -use uv_configuration::{existing_vcs_repo, VersionControl}; +use uv_configuration::{VersionControlError, VersionControlSystem}; use uv_fs::{Simplified, CWD}; use uv_python::{ EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, @@ -17,7 +17,7 @@ use uv_python::{ }; use uv_resolver::RequiresPython; use uv_scripts::{Pep723Script, ScriptTag}; -use uv_warnings::{warn_user, warn_user_once}; +use uv_warnings::warn_user_once; use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, Workspace, WorkspaceError}; @@ -34,11 +34,11 @@ pub(crate) async fn init( name: Option, package: bool, init_kind: InitKind, + vcs: Option, no_readme: bool, no_pin_python: bool, python: Option, no_workspace: bool, - version_control: Option, python_preference: PythonPreference, python_downloads: PythonDownloads, connectivity: Connectivity, @@ -108,8 +108,8 @@ pub(crate) async fn init( &name, package, project_kind, + vcs, no_readme, - version_control, no_pin_python, python, no_workspace, @@ -234,8 +234,8 @@ async fn init_project( name: &PackageName, package: bool, project_kind: InitProjectKind, + vcs: Option, no_readme: bool, - version_control: Option, no_pin_python: bool, python: Option, no_workspace: bool, @@ -459,7 +459,7 @@ async fn init_project( path, &requires_python, python_request.as_ref(), - version_control, + vcs, no_readme, package, ) @@ -548,7 +548,7 @@ impl InitProjectKind { path: &Path, requires_python: &RequiresPython, python_request: Option<&PythonRequest>, - version_control: Option, + vcs: Option, no_readme: bool, package: bool, ) -> Result<()> { @@ -559,7 +559,7 @@ impl InitProjectKind { path, requires_python, python_request, - version_control, + vcs, no_readme, package, ) @@ -571,7 +571,7 @@ impl InitProjectKind { path, requires_python, python_request, - version_control, + vcs, no_readme, package, ) @@ -580,13 +580,14 @@ impl InitProjectKind { } } + /// Initialize a Python application at the target path. async fn init_application( self, name: &PackageName, path: &Path, requires_python: &RequiresPython, python_request: Option<&PythonRequest>, - version_control: Option, + vcs: Option, no_readme: bool, package: bool, ) -> Result<()> { @@ -655,35 +656,19 @@ impl InitProjectKind { } // Initialize the version control system. - let in_existing_vcs = existing_vcs_repo(path.parent().unwrap_or(&path)); - let vcs = match (version_control, in_existing_vcs) { - (None, false) => VersionControl::default(), - (None, true) => VersionControl::None, - (Some(vcs), false) => vcs, - (Some(vcs), true) => { - warn_user!( - "The project is already in a version control system, `--vcs {vcs}` is ignored", - ); - VersionControl::None - } - }; - if let Err(err) = vcs.init(path) { - if version_control.is_some() { - return Err(err); - } - debug!("Failed to initialize version control: {:#}", err); - } + init_vcs(path, vcs)?; Ok(()) } + /// Initialize a library project at the target path. async fn init_library( self, name: &PackageName, path: &Path, requires_python: &RequiresPython, python_request: Option<&PythonRequest>, - version_control: Option, + vcs: Option, no_readme: bool, package: bool, ) -> Result<()> { @@ -736,24 +721,7 @@ impl InitProjectKind { } // Initialize the version control system. - let in_existing_vcs = existing_vcs_repo(path.parent().unwrap_or(&path)); - let vcs = match (version_control, in_existing_vcs) { - (None, false) => VersionControl::default(), - (None, true) => VersionControl::None, - (Some(vcs), false) => vcs, - (Some(vcs), true) => { - warn_user!( - "The project is already in a version control system, `--vcs {vcs}` is ignored", - ); - VersionControl::None - } - }; - if let Err(err) = vcs.init(path) { - if version_control.is_some() { - return Err(err); - } - debug!("Failed to initialize version control: {:#}", err); - } + init_vcs(path, vcs)?; Ok(()) } @@ -795,3 +763,51 @@ fn pyproject_project_scripts(package: &PackageName, executable_name: &str, targe {executable_name} = "{module_name}:{target}" "#} } + +/// Initialize the version control system at the given path. +fn init_vcs(path: &Path, vcs: Option) -> 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 + } + }; + + // Attempt to initialize the VCS. + match vcs.init(path) { + 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) => return Err(err.into()), + } + + Ok(()) +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 736d0e79362b1..983bc3c2ba3b2 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1185,11 +1185,11 @@ async fn run_project( args.name, args.package, args.kind, + args.vcs, args.no_readme, args.no_pin_python, args.python, args.no_workspace, - args.version_control, globals.python_preference, globals.python_downloads, globals.connectivity, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index ffd63ecea2957..fb924949e1617 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -26,7 +26,7 @@ use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, DevMode, EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, NoBinary, NoBuild, PreviewMode, Reinstall, SourceStrategy, TargetTriple, TrustedHost, - TrustedPublishing, Upgrade, VersionControl, + TrustedPublishing, Upgrade, VersionControlSystem, }; use uv_normalize::PackageName; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; @@ -162,7 +162,7 @@ pub(crate) struct InitSettings { pub(crate) name: Option, pub(crate) package: bool, pub(crate) kind: InitKind, - pub(crate) version_control: Option, + pub(crate) vcs: Option, pub(crate) no_readme: bool, pub(crate) no_pin_python: bool, pub(crate) no_workspace: bool, @@ -204,7 +204,7 @@ impl InitSettings { name, package, kind, - version_control: vcs, + vcs, no_readme, no_pin_python, no_workspace, diff --git a/crates/uv/tests/init.rs b/crates/uv/tests/init.rs index 6fb7d91aa1609..a0dfe4749c713 100644 --- a/crates/uv/tests/init.rs +++ b/crates/uv/tests/init.rs @@ -2156,7 +2156,7 @@ fn init_git() -> Result<()> { }, { assert_snapshot!( gitignore, @r###" - # Python generated files + # Python-generated files __pycache__/ *.py[oc] build/ @@ -2164,7 +2164,7 @@ fn init_git() -> Result<()> { wheels/ *.egg-info - # venv + # Virtual environments .venv "### ); @@ -2212,7 +2212,6 @@ fn init_inside_git_repo() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The project is already in a version control system, `--vcs git` is ignored Initialized project `foo` at `[TEMP_DIR]/foo` "###); @@ -2253,11 +2252,11 @@ fn init_git_not_installed() { let child = context.temp_dir.child("bar"); // Set `PATH` to child to make `git` command cannot be found. uv_snapshot!(context.filters(), context.init().env("PATH", &*child).arg(child.as_ref()).arg("--vcs").arg("git"), @r###" - success: false - exit_code: 2 - ----- stdout ----- + success: false + exit_code: 2 + ----- stdout ----- - ----- stderr ----- - error: could not find `git` in PATH - "###); + ----- stderr ----- + error: Attempted to initialize a Git repository, but `git` was not found in PATH + "###); } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 11f6721139afe..221f35f6599b7 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -558,16 +558,16 @@ uv init [OPTIONS] [PATH]

By default, adds a requirement on the system Python version; use --python to specify an alternative Python version requirement.

-
--vcs vcs

Initialize a new repository for the given version control system.

+
--vcs vcs

Initialize a version control system for the project.

-

By default, uv will try to initialize a Git repository (git). Use none to skip repository initialization.

+

By default, uv will initialize a Git repository (git). Use --vcs none to explicitly avoid initializing a version control system.

Possible values:

  • git: Use Git for version control
  • -
  • none: Do not use version control
  • +
  • none: Do not use any version control system
--verbose, -v

Use verbose output.