Skip to content

Commit

Permalink
uv init initializes git repository
Browse files Browse the repository at this point in the history
Add tests

`uv init` initializes git repository

Add tests

tmp
  • Loading branch information
j178 committed Sep 8, 2024
1 parent a178051 commit de43690
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 2 deletions.
5 changes: 5 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 @@ -13,7 +13,7 @@ use pypi_types::VerbatimParsedUrl;
use uv_cache::CacheArgs;
use uv_configuration::{
ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier,
TargetTriple, TrustedHost,
TargetTriple, TrustedHost, VersionControl,
};
use uv_normalize::{ExtraName, PackageName};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
Expand Down Expand Up @@ -2310,6 +2310,13 @@ pub struct InitArgs {
#[arg(long, alias = "library", conflicts_with = "app")]
pub r#lib: bool,

/// Initialize a new repository for the given version control system.
///
/// By default, uv will try to initialize a Git repository (`git`).
/// Use `none` to skip repository initialization.
#[arg(long, value_enum)]
pub vcs: Option<VersionControl>,

/// Do not create a `README.md` file.
#[arg(long)]
pub no_readme: bool,
Expand Down
6 changes: 6 additions & 0 deletions crates/uv-configuration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,23 @@ platform-tags = { workspace = true }
pypi-types = { workspace = true }
uv-auth = { workspace = true }
uv-cache = { 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 }
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 @@ -14,6 +14,7 @@ pub use preview::*;
pub use sources::*;
pub use target_triple::*;
pub use trusted_host::*;
pub use vcs_options::*;

mod authentication;
mod build_options;
Expand All @@ -31,3 +32,4 @@ mod preview;
mod sources;
mod target_triple;
mod trusted_host;
mod vcs_options;
123 changes: 123 additions & 0 deletions crates/uv-configuration/src/vcs_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use std::path::{Path, PathBuf};
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: &PathBuf) -> Result<()> {
match self {
VersionControl::None => Ok(()),
VersionControl::Git => Self::init_git(path),
}
}

fn init_git(path: &PathBuf) -> 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<Self, Self::Err> {
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)
}
24 changes: 24 additions & 0 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ use std::path::Path;

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::{existing_vcs_repo, VersionControl};
use uv_fs::{Simplified, CWD};
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
PythonVersionFile, VersionRequest,
};
use uv_resolver::RequiresPython;
use uv_warnings::warn_user;
use uv_workspace::pyproject_mut::{DependencyTarget, PyProjectTomlMut};
use uv_workspace::{DiscoveryOptions, MemberDiscovery, Workspace, WorkspaceError};

Expand All @@ -33,6 +36,7 @@ pub(crate) async fn init(
no_pin_python: bool,
python: Option<String>,
no_workspace: bool,
version_control: Option<VersionControl>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
connectivity: Connectivity,
Expand Down Expand Up @@ -94,6 +98,26 @@ pub(crate) async fn init(
}
}

// 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);
}

match explicit_path {
// Initialized a project in the current directory.
None => {
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,7 @@ async fn run_project(
args.no_pin_python,
args.python,
args.no_workspace,
args.version_control,
globals.python_preference,
globals.python_downloads,
globals.connectivity,
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 @@ -24,7 +24,7 @@ use uv_client::Connectivity;
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, ExportFormat, ExtrasSpecification, HashCheckingMode,
IndexStrategy, InstallOptions, KeyringProviderType, NoBinary, NoBuild, PreviewMode, Reinstall,
SourceStrategy, TargetTriple, TrustedHost, Upgrade,
SourceStrategy, TargetTriple, TrustedHost, Upgrade, VersionControl,
};
use uv_normalize::PackageName;
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
Expand Down Expand Up @@ -156,6 +156,7 @@ pub(crate) struct InitSettings {
pub(crate) name: Option<PackageName>,
pub(crate) package: bool,
pub(crate) kind: InitProjectKind,
pub(crate) version_control: Option<VersionControl>,
pub(crate) no_readme: bool,
pub(crate) no_pin_python: bool,
pub(crate) no_workspace: bool,
Expand All @@ -174,6 +175,7 @@ impl InitSettings {
no_package,
app,
lib,
vcs,
no_readme,
no_pin_python,
no_workspace,
Expand All @@ -194,6 +196,7 @@ impl InitSettings {
name,
package,
kind,
version_control: vcs,
no_readme,
no_pin_python,
no_workspace,
Expand Down
Loading

0 comments on commit de43690

Please sign in to comment.