Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use url::Url;
use uv_cache::CacheArgs;
use uv_configuration::{
ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier,
TargetTriple, TrustedHost, TrustedPublishing,
TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem,
};
use uv_normalize::{ExtraName, PackageName};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
Expand Down Expand Up @@ -2373,6 +2373,13 @@ pub struct InitArgs {
#[arg(long, alias="script", conflicts_with_all=["app", "lib", "package"])]
pub r#script: bool,

/// Initialize a version control system for the project.
///
/// 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<VersionControlSystem>,

/// Do not create a `README.md` file.
#[arg(long)]
pub no_readme: bool,
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-configuration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ uv-normalize = { workspace = true }

clap = { workspace = true, features = ["derive"], optional = true }
either = { workspace = true }
fs-err = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
which = { workspace = true }

[dev-dependencies]
anyhow = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub use sources::*;
pub use target_triple::*;
pub use trusted_host::*;
pub use trusted_publishing::*;
pub use vcs::*;

mod authentication;
mod build_options;
Expand All @@ -37,3 +38,4 @@ mod sources;
mod target_triple;
mod trusted_host;
mod trusted_publishing;
mod vcs;
120 changes: 120 additions & 0 deletions crates/uv-configuration/src/vcs.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
// 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
";
67 changes: 67 additions & 0 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Result};
use owo_colors::OwoColorize;

use pep440_rs::Version;
use pep508_rs::PackageName;
use tracing::{debug, warn};
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{VersionControlError, VersionControlSystem};
use uv_fs::{Simplified, CWD};
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
Expand All @@ -32,6 +34,7 @@ pub(crate) async fn init(
name: Option<PackageName>,
package: bool,
init_kind: InitKind,
vcs: Option<VersionControlSystem>,
no_readme: bool,
no_pin_python: bool,
python: Option<String>,
Expand Down Expand Up @@ -106,6 +109,7 @@ pub(crate) async fn init(
&name,
package,
project_kind,
vcs,
no_readme,
no_pin_python,
python,
Expand Down Expand Up @@ -231,6 +235,7 @@ async fn init_project(
name: &PackageName,
package: bool,
project_kind: InitProjectKind,
vcs: Option<VersionControlSystem>,
no_readme: bool,
no_pin_python: bool,
python: Option<String>,
Expand Down Expand Up @@ -455,6 +460,7 @@ async fn init_project(
path,
&requires_python,
python_request.as_ref(),
vcs,
no_readme,
package,
)
Expand Down Expand Up @@ -543,6 +549,7 @@ impl InitProjectKind {
path: &Path,
requires_python: &RequiresPython,
python_request: Option<&PythonRequest>,
vcs: Option<VersionControlSystem>,
no_readme: bool,
package: bool,
) -> Result<()> {
Expand All @@ -553,6 +560,7 @@ impl InitProjectKind {
path,
requires_python,
python_request,
vcs,
no_readme,
package,
)
Expand All @@ -564,6 +572,7 @@ impl InitProjectKind {
path,
requires_python,
python_request,
vcs,
no_readme,
package,
)
Expand All @@ -572,12 +581,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>,
vcs: Option<VersionControlSystem>,
no_readme: bool,
package: bool,
) -> Result<()> {
Expand Down Expand Up @@ -645,15 +656,20 @@ impl InitProjectKind {
}
}

// Initialize the version control system.
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>,
vcs: Option<VersionControlSystem>,
no_readme: bool,
package: bool,
) -> Result<()> {
Expand Down Expand Up @@ -705,6 +721,9 @@ impl InitProjectKind {
}
}

// Initialize the version control system.
init_vcs(path, vcs)?;

Ok(())
}
}
Expand Down Expand Up @@ -745,3 +764,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<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
}
};

// 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(())
}
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,7 @@ async fn run_project(
args.name,
args.package,
args.kind,
args.vcs,
args.no_readme,
args.no_pin_python,
args.python,
Expand Down
5 changes: 4 additions & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
TrustedPublishing, Upgrade, VersionControlSystem,
};
use uv_normalize::PackageName;
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
Expand Down Expand Up @@ -162,6 +162,7 @@ pub(crate) struct InitSettings {
pub(crate) name: Option<PackageName>,
pub(crate) package: bool,
pub(crate) kind: InitKind,
pub(crate) vcs: Option<VersionControlSystem>,
pub(crate) no_readme: bool,
pub(crate) no_pin_python: bool,
pub(crate) no_workspace: bool,
Expand All @@ -181,6 +182,7 @@ impl InitSettings {
app,
lib,
script,
vcs,
no_readme,
no_pin_python,
no_workspace,
Expand All @@ -202,6 +204,7 @@ impl InitSettings {
name,
package,
kind,
vcs,
no_readme,
no_pin_python,
no_workspace,
Expand Down
Loading