uv_configuration/
vcs.rs

1use std::io::Write;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4
5use serde::Deserialize;
6use uv_git::GIT;
7
8#[derive(Debug, thiserror::Error)]
9pub enum VersionControlError {
10    #[error("Attempted to initialize a Git repository, but `git` was not found in PATH")]
11    GitNotInstalled,
12    #[error("Failed to initialize Git repository at `{0}`\nstdout: {1}\nstderr: {2}")]
13    GitInit(PathBuf, String, String),
14    #[error("`git` command failed")]
15    GitCommand(#[source] std::io::Error),
16    #[error(transparent)]
17    Io(#[from] std::io::Error),
18}
19
20/// The version control system to use.
21#[derive(Clone, Copy, Debug, PartialEq, Default, Deserialize)]
22#[serde(deny_unknown_fields, rename_all = "kebab-case")]
23#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
24#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
25pub enum VersionControlSystem {
26    /// Use Git for version control.
27    #[default]
28    Git,
29    /// Do not use any version control system.
30    None,
31}
32
33impl VersionControlSystem {
34    /// Initializes the VCS system based on the provided path.
35    pub fn init(&self, path: &Path) -> Result<(), VersionControlError> {
36        match self {
37            Self::Git => {
38                let Ok(git) = GIT.as_ref() else {
39                    return Err(VersionControlError::GitNotInstalled);
40                };
41
42                let output = Command::new(git)
43                    .arg("init")
44                    .current_dir(path)
45                    .stdout(Stdio::piped())
46                    .stderr(Stdio::piped())
47                    .output()
48                    .map_err(VersionControlError::GitCommand)?;
49                if !output.status.success() {
50                    let stdout = String::from_utf8_lossy(&output.stdout);
51                    let stderr = String::from_utf8_lossy(&output.stderr);
52                    return Err(VersionControlError::GitInit(
53                        path.to_path_buf(),
54                        stdout.to_string(),
55                        stderr.to_string(),
56                    ));
57                }
58
59                // Create the `.gitignore`, if it doesn't exist.
60                match fs_err::OpenOptions::new()
61                    .write(true)
62                    .create_new(true)
63                    .open(path.join(".gitignore"))
64                {
65                    Ok(mut file) => file.write_all(GITIGNORE.as_bytes())?,
66                    Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => (),
67                    Err(err) => return Err(err.into()),
68                }
69
70                Ok(())
71            }
72            Self::None => Ok(()),
73        }
74    }
75}
76
77impl std::fmt::Display for VersionControlSystem {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            Self::Git => write!(f, "git"),
81            Self::None => write!(f, "none"),
82        }
83    }
84}
85
86const GITIGNORE: &str = "# Python-generated files
87__pycache__/
88*.py[oc]
89build/
90dist/
91wheels/
92*.egg-info
93
94# Virtual environments
95.venv
96";