Skip to content

Commit 3fa9187

Browse files
authored
Add prek util yaml-to-toml to convert .pre-commit-config.yaml to prek.toml (#1584)
1 parent 2314871 commit 3fa9187

6 files changed

Lines changed: 524 additions & 2 deletions

File tree

crates/prek/src/cli/mod.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod sample_config;
2727
mod self_update;
2828
mod try_repo;
2929
mod validate;
30+
mod yaml_to_toml;
3031

3132
pub(crate) use auto_update::auto_update;
3233
pub(crate) use cache_clean::cache_clean;
@@ -43,8 +44,9 @@ pub(crate) use sample_config::sample_config;
4344
pub(crate) use self_update::self_update;
4445
pub(crate) use try_repo::try_repo;
4546
pub(crate) use validate::{validate_configs, validate_manifest};
47+
pub(crate) use yaml_to_toml::yaml_to_toml;
4648

47-
#[derive(Copy, Clone, PartialEq, Eq)]
49+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
4850
pub(crate) enum ExitStatus {
4951
/// The command succeeded.
5052
Success,
@@ -723,11 +725,29 @@ pub(crate) enum UtilCommand {
723725
/// Install hook script in a directory intended for use with `git config init.templateDir`.
724726
#[command(alias = "init-templatedir")]
725727
InitTemplateDir(InitTemplateDirArgs),
728+
/// Convert a YAML configuration file to prek.toml.
729+
YamlToToml(YamlToTomlArgs),
726730
/// Generate shell completion scripts.
727731
#[command(hide = true)]
728732
GenerateShellCompletion(GenerateShellCompletionArgs),
729733
}
730734

735+
#[derive(Debug, Args)]
736+
pub(crate) struct YamlToTomlArgs {
737+
/// The YAML configuration file to convert.
738+
#[arg(value_name = "CONFIG", value_hint = ValueHint::FilePath)]
739+
pub(crate) input: PathBuf,
740+
741+
/// Path to write the generated prek.toml file.
742+
/// Defaults to `prek.toml` in the same directory as the input file.
743+
#[arg(short, long, value_name = "OUTPUT", value_hint = ValueHint::FilePath)]
744+
pub(crate) output: Option<PathBuf>,
745+
746+
/// Overwrite the output file if it already exists.
747+
#[arg(long)]
748+
pub(crate) force: bool,
749+
}
750+
731751
#[derive(Debug, Subcommand)]
732752
pub(crate) enum CacheCommand {
733753
/// Show the location of the prek cache.
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
use std::fmt::Write as _;
2+
use std::io::Write;
3+
use std::path::{Path, PathBuf};
4+
5+
use anyhow::{Context, Result};
6+
use owo_colors::OwoColorize;
7+
use prek_consts::PREK_TOML;
8+
use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Table, Value};
9+
10+
use crate::cli::ExitStatus;
11+
use crate::config;
12+
use crate::fs::Simplified;
13+
use crate::printer::Printer;
14+
15+
pub(crate) fn yaml_to_toml(
16+
input: &Path,
17+
output: Option<PathBuf>,
18+
force: bool,
19+
printer: Printer,
20+
) -> Result<ExitStatus> {
21+
// Validate the input file first.
22+
let _ = config::load_config(input)?;
23+
24+
let content = fs_err::read_to_string(input)?;
25+
let value: serde_json::Value = serde_saphyr::from_str(&content)?;
26+
27+
let output = output.unwrap_or_else(|| input.parent().unwrap_or(Path::new(".")).join(PREK_TOML));
28+
29+
if output == input {
30+
anyhow::bail!(
31+
"Output path `{}` matches input; choose a different output path",
32+
output.simplified_display().cyan()
33+
);
34+
}
35+
36+
let mut rendered = json_to_toml(&value)?;
37+
if !rendered.ends_with('\n') {
38+
rendered.push('\n');
39+
}
40+
41+
if let Some(parent) = output.parent() {
42+
fs_err::create_dir_all(parent)?;
43+
}
44+
45+
let mut options = fs_err::OpenOptions::new();
46+
options.write(true);
47+
if force {
48+
options.create(true).truncate(true);
49+
} else {
50+
options.create_new(true);
51+
}
52+
53+
let mut file = match options.open(&output) {
54+
Ok(file) => file,
55+
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
56+
anyhow::bail!(
57+
"File `{}` already exists (use `--force` to overwrite)",
58+
output.simplified_display().cyan()
59+
);
60+
}
61+
Err(err) => return Err(err.into()),
62+
};
63+
64+
file.write_all(rendered.as_bytes())?;
65+
66+
writeln!(
67+
printer.stdout(),
68+
"Written to `{}`",
69+
output.simplified_display().cyan()
70+
)?;
71+
72+
Ok(ExitStatus::Success)
73+
}
74+
75+
fn json_to_toml(value: &serde_json::Value) -> Result<String> {
76+
let map = value
77+
.as_object()
78+
.context("Expected a top-level mapping in the config file")?;
79+
80+
let mut doc = DocumentMut::new();
81+
for (key, value) in map {
82+
if key == "repos" {
83+
let repos = value.as_array().context("`repos` must be an array")?;
84+
doc["repos"] = repos_to_array_of_tables(repos)?.into();
85+
continue;
86+
}
87+
doc[key] = json_to_toml_value(value).into();
88+
}
89+
90+
Ok(doc.to_string())
91+
}
92+
93+
fn json_to_toml_value(value: &serde_json::Value) -> Value {
94+
match value {
95+
serde_json::Value::Null => Value::from(""),
96+
serde_json::Value::Bool(value) => Value::from(*value),
97+
serde_json::Value::Number(value) => {
98+
if let Some(value) = value.as_i64() {
99+
Value::from(value)
100+
} else if let Some(value) = value.as_f64() {
101+
Value::from(value)
102+
} else {
103+
Value::from(0.0)
104+
}
105+
}
106+
serde_json::Value::String(value) => Value::from(value.as_str()),
107+
serde_json::Value::Array(values) => {
108+
json_array_to_value_with_indent(values, " ", " ", false)
109+
}
110+
serde_json::Value::Object(values) => Value::InlineTable(json_object_to_inline(values)),
111+
}
112+
}
113+
114+
fn json_array_to_value_with_indent(
115+
values: &[serde_json::Value],
116+
item_indent: &str,
117+
closing_indent: &str,
118+
force_multiline: bool,
119+
) -> Value {
120+
let mut array = Array::new();
121+
if values.len() == 1 && !force_multiline {
122+
let value = match &values[0] {
123+
serde_json::Value::Object(map) => Value::InlineTable(json_object_to_inline(map)),
124+
_ => json_to_toml_value(&values[0]),
125+
};
126+
array.push(value);
127+
array.set_trailing("");
128+
return Value::Array(array);
129+
}
130+
131+
for value in values {
132+
let mut value = match value {
133+
serde_json::Value::Object(map) => Value::InlineTable(json_object_to_inline(map)),
134+
_ => json_to_toml_value(value),
135+
};
136+
value.decor_mut().set_prefix(format!("\n{item_indent}"));
137+
array.push(value);
138+
}
139+
array.set_trailing(format!("\n{closing_indent}"));
140+
Value::Array(array)
141+
}
142+
143+
fn json_object_to_inline(values: &serde_json::Map<String, serde_json::Value>) -> InlineTable {
144+
let mut table = InlineTable::new();
145+
for (key, value) in values {
146+
let value = match value {
147+
serde_json::Value::Array(values) => {
148+
json_array_to_value_with_indent(values, " ", " ", false)
149+
}
150+
_ => json_to_toml_value(value),
151+
};
152+
table.insert(key.as_str(), value);
153+
}
154+
format_inline_table_multiline(&mut table, " ", " ");
155+
table
156+
}
157+
158+
fn format_inline_table_multiline(table: &mut InlineTable, base_indent: &str, closing_indent: &str) {
159+
let len = table.len();
160+
if len <= 1 {
161+
return;
162+
}
163+
for (idx, (mut key, value)) in table.iter_mut().enumerate() {
164+
key.leaf_decor_mut().set_prefix(format!("\n{base_indent}"));
165+
key.leaf_decor_mut().set_suffix(" ");
166+
167+
let suffix = if idx + 1 == len {
168+
format!("\n{closing_indent}")
169+
} else {
170+
String::new()
171+
};
172+
value.decor_mut().set_prefix(" ");
173+
value.decor_mut().set_suffix(suffix);
174+
175+
if let Value::InlineTable(inner) = value {
176+
let nested_base = format!("{base_indent} ");
177+
let nested_closing = format!("{closing_indent} ");
178+
format_inline_table_multiline(inner, &nested_base, &nested_closing);
179+
}
180+
}
181+
}
182+
183+
fn repos_to_array_of_tables(values: &[serde_json::Value]) -> Result<ArrayOfTables> {
184+
let mut array = ArrayOfTables::new();
185+
for value in values {
186+
let map = value
187+
.as_object()
188+
.context("Each repo entry must be a mapping")?;
189+
let mut table = Table::new();
190+
for (key, value) in map {
191+
if key == "hooks" {
192+
let hooks = value.as_array().context("`hooks` must be an array")?;
193+
table[key] = json_array_to_value_with_indent(hooks, " ", "", true).into();
194+
continue;
195+
}
196+
table[key] = json_to_toml_value(value).into();
197+
}
198+
array.push(table);
199+
}
200+
Ok(array)
201+
}

crates/prek/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,11 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
398398
)
399399
.await
400400
}
401+
UtilCommand::YamlToToml(args) => {
402+
show_settings!(args);
403+
404+
cli::yaml_to_toml(&args.input, args.output, args.force, printer)
405+
}
401406
UtilCommand::GenerateShellCompletion(args) => {
402407
show_settings!(args);
403408

0 commit comments

Comments
 (0)