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#[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 #[default]
28 Git,
29 None,
31}
32
33impl VersionControlSystem {
34 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 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";