1use std::io::Write;
2use std::path::{Path, PathBuf};
3use std::process::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 = git
43 .build_command()
44 .arg("init")
45 .current_dir(path)
46 .stdout(Stdio::piped())
47 .stderr(Stdio::piped())
48 .output()
49 .map_err(VersionControlError::GitCommand)?;
50 if !output.status.success() {
51 let stdout = String::from_utf8_lossy(&output.stdout);
52 let stderr = String::from_utf8_lossy(&output.stderr);
53 return Err(VersionControlError::GitInit(
54 path.to_path_buf(),
55 stdout.to_string(),
56 stderr.to_string(),
57 ));
58 }
59
60 match fs_err::OpenOptions::new()
62 .write(true)
63 .create_new(true)
64 .open(path.join(".gitignore"))
65 {
66 Ok(mut file) => file.write_all(GITIGNORE.as_bytes())?,
67 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => (),
68 Err(err) => return Err(err.into()),
69 }
70
71 Ok(())
72 }
73 Self::None => Ok(()),
74 }
75 }
76}
77
78impl std::fmt::Display for VersionControlSystem {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 Self::Git => write!(f, "git"),
82 Self::None => write!(f, "none"),
83 }
84 }
85}
86
87const GITIGNORE: &str = "# Python-generated files
88__pycache__/
89*.py[oc]
90build/
91dist/
92wheels/
93*.egg-info
94
95# Virtual environments
96.venv
97";
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub enum GitLfsSetting {
102 #[default]
104 Disabled,
105 Enabled { from_env: bool },
107}
108
109impl GitLfsSetting {
110 pub fn new(from_arg: Option<bool>, from_env: Option<bool>) -> Self {
111 match (from_arg, from_env) {
112 (Some(true), _) => Self::Enabled { from_env: false },
113 (_, Some(true)) => Self::Enabled { from_env: true },
114 _ => Self::Disabled,
115 }
116 }
117}
118
119impl From<GitLfsSetting> for Option<bool> {
120 fn from(setting: GitLfsSetting) -> Self {
121 match setting {
122 GitLfsSetting::Enabled { .. } => Some(true),
123 GitLfsSetting::Disabled => None,
124 }
125 }
126}