Skip to content

Commit bb15d08

Browse files
feat(cli): implement config and rules (#19)
* feat(cli): impl config file reading Signed-off-by: KeisukeYamashita <[email protected]> * feat(cli): configure correct license Signed-off-by: KeisukeYamashita <[email protected]> * feat(cli): pin rust version Signed-off-by: KeisukeYamashita <[email protected]> * feat(cli): impl json and yaml config file Signed-off-by: KeisukeYamashita <[email protected]> * feat(cli): impl config Signed-off-by: KeisukeYamashita <[email protected]> --------- Signed-off-by: KeisukeYamashita <[email protected]>
1 parent 79accbb commit bb15d08

20 files changed

+982
-48
lines changed

Cargo.lock

+26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
[package]
22
name = "commitlint-rs"
33
description = "CLI tool to lint commits by Conventional Commits"
4+
authors = ["KeisukeYamashita <[email protected]>"]
45
keywords = ["conventional-commits", "lint"]
56
categories = ["command-line-utilities"]
67
version = "0.0.3"
78
readme = "README.md"
89
repository = "https://github.com/KeisukeYamashita/commitlint-rs"
9-
license-file = "LICENSE"
10+
license-file = "Apache-2.0 or MIT"
1011
edition = "2021"
1112
exclude = ["/web"]
13+
rust-version = "1.71"
1214

1315
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1416

1517
[dependencies]
1618
clap = { version = "4.3.4", features = ["derive", "env", "string"] }
1719
futures = "0.3.28"
1820
regex = "1.8.4"
19-
serde = "1.0.164"
21+
serde = { version = "1.0.164", features = ["derive"] }
22+
serde_json = "1.0.96"
2023
serde_yaml = "0.9.21"
2124
tokio = { version = "1.28.2", features = ["full"] }
2225

LICENSE LICENSE-APACHE

File renamed without changes.

LICENSE-MIT

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) 2023- KeisukeYamashita
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be
12+
included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

examples/.commitlint.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
rules:
2+
body-empty:
3+
hoge: hoho

src/args.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use crate::message::Message;
1717
#[command(author, about = "CLI to lint with conventional commits", long_about = None, version)]
1818
pub struct Args {
1919
/// Path to the config file
20-
#[arg(short = 'g', long, default_value = ".")]
21-
pub config: PathBuf,
20+
#[arg(short = 'g', long)]
21+
pub config: Option<PathBuf>,
2222

2323
/// Directory to execute in
2424
#[arg(short = 'd', long, default_value = ".")]

src/config.rs

+85-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
use std::{fmt::Error, path::PathBuf};
1+
use serde::Deserialize;
2+
use std::{fs, path::PathBuf};
3+
4+
use crate::rule::{Rules};
5+
6+
/// Default Root config file path to search for.
7+
const DEFAULT_CONFIG_ROOT: &str = ".";
28

39
/// Default commitlintrc configuration files
410
/// If the user didn't specify a configuration file with -g or --config argument,
@@ -11,23 +17,27 @@ const DEFAULT_CONFIG_FILE: [&str; 4] = [
1117
];
1218

1319
/// Config represents the configuration of commitlint.
14-
#[derive(Debug)]
15-
pub struct Config {}
16-
17-
/// Default configuration if no configuration file is found.
18-
pub fn default_config() -> Config {
19-
Config {}
20+
#[derive(Clone, Debug, Default, Deserialize)]
21+
pub struct Config {
22+
/// Rules represents the rules of commitlint.
23+
pub rules: Rules,
2024
}
2125

2226
/// Load configuration from the specified path.
23-
pub async fn load(path: PathBuf) -> Result<Config, Error> {
24-
let config_file = find_config_file(path);
27+
pub async fn load(path: Option<PathBuf>) -> Result<Config, String> {
28+
let config_file = match &path {
29+
Some(p) => Some(p.clone()),
30+
None => find_config_file(PathBuf::from(DEFAULT_CONFIG_ROOT)),
31+
};
2532

26-
if config_file.is_none() {
27-
return Ok(default_config());
33+
match (config_file, path) {
34+
// If the file was specified and found, load it.
35+
(Some(p), _) => load_config_file(p).await,
36+
// If the file was not specified and not found, return default config.
37+
(None, None) => Ok(Config::default()),
38+
// If the was explicitly specified but not found, return an error.
39+
(None, Some(p)) => Err(format!("Configuration file not found in {}", p.display())),
2840
}
29-
30-
Ok(Config {})
3141
}
3242

3343
/// Find configuration file in the specified path.
@@ -43,3 +53,65 @@ pub fn find_config_file(path: PathBuf) -> Option<PathBuf> {
4353

4454
None
4555
}
56+
57+
/// Load config file from the specified path.
58+
pub async fn load_config_file(path: PathBuf) -> Result<Config, String> {
59+
if !path.exists() {
60+
return Err(format!(
61+
"Configuration file not found in {}",
62+
path.display()
63+
));
64+
}
65+
66+
match path.extension() {
67+
Some(ext) => match ext.to_str() {
68+
Some("json") => load_json_config_file(path).await,
69+
Some("yaml") | Some("yml") => load_yaml_config_file(path).await,
70+
_ => load_unknown_config_file(path).await,
71+
},
72+
None => Err(format!(
73+
"Unsupported configuration file format: {}",
74+
path.display()
75+
)),
76+
}
77+
}
78+
79+
/// Load JSON config file from the specified path.
80+
async fn load_json_config_file(path: PathBuf) -> Result<Config, String> {
81+
let text = fs::read_to_string(path).unwrap();
82+
83+
match serde_json::from_str::<Config>(&text) {
84+
Ok(config) => Ok(config),
85+
Err(err) => Err(format!("Failed to parse configuration file: {}", err)),
86+
}
87+
}
88+
89+
/// Load YAML config file from the specified path.
90+
async fn load_yaml_config_file(path: PathBuf) -> Result<Config, String> {
91+
let text = fs::read_to_string(path).unwrap();
92+
93+
match serde_yaml::from_str::<Config>(&text) {
94+
Ok(config) => Ok(config),
95+
Err(err) => Err(format!("Failed to parse configuration file: {}", err)),
96+
}
97+
}
98+
99+
/// Try to load configuration file from the specified path.
100+
/// First try to load it as JSON, then as YAML.
101+
/// If both fail, return an error.
102+
async fn load_unknown_config_file(path: PathBuf) -> Result<Config, String> {
103+
let text = fs::read_to_string(path.clone()).unwrap();
104+
105+
if let Ok(config) = serde_json::from_str::<Config>(&text) {
106+
return Ok(config);
107+
}
108+
109+
if let Ok(config) = serde_yaml::from_str::<Config>(&text) {
110+
return Ok(config);
111+
}
112+
113+
Err(format!(
114+
"Failed to parse configuration file: {}",
115+
path.display()
116+
))
117+
}

src/git.rs

+8-6
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ pub fn parse_commit_message(
7575

7676
for line in lines {
7777
if in_footer {
78-
let parts: Vec<&str> = line.splitn(2, ":").collect();
78+
let parts: Vec<&str> = line.splitn(2, ':').collect();
7979
if parts.len() == 2 {
8080
let key = parts[0].trim().to_string();
8181
let value = parts[1].trim().to_string();
@@ -86,7 +86,9 @@ pub fn parse_commit_message(
8686
in_footer = true;
8787
} else {
8888
body.get_or_insert_with(String::new).push_str(line);
89-
body.as_mut().map(|b| b.push('\n'));
89+
if let Some(b) = body.as_mut() {
90+
b.push('\n')
91+
}
9092
}
9193
}
9294

@@ -117,7 +119,7 @@ mod tests {
117119
use super::*;
118120

119121
#[test]
120-
fn text_parse_subject_with_scope() {
122+
fn test_parse_subject_with_scope() {
121123
let input = "feat(cli): add dummy option";
122124
assert_eq!(
123125
parse_subject(input),
@@ -130,7 +132,7 @@ mod tests {
130132
}
131133

132134
#[test]
133-
fn text_parse_subject_with_emphasized_type_with_scope() {
135+
fn test_parse_subject_with_emphasized_type_with_scope() {
134136
let input = "feat(cli)!: add dummy option";
135137
assert_eq!(
136138
parse_subject(input),
@@ -143,7 +145,7 @@ mod tests {
143145
}
144146

145147
#[test]
146-
fn text_parse_subject_without_scope() {
148+
fn test_parse_subject_without_scope() {
147149
let input = "feat: add dummy option";
148150
assert_eq!(
149151
parse_subject(input),
@@ -152,7 +154,7 @@ mod tests {
152154
}
153155

154156
#[test]
155-
fn text_parse_subject_with_emphasized_type_without_scope() {
157+
fn test_parse_subject_with_emphasized_type_without_scope() {
156158
let input = "feat!: add dummy option";
157159
assert_eq!(
158160
parse_subject(input),

src/main.rs

+13-9
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ mod config;
33
mod git;
44
mod message;
55
mod result;
6+
mod rule;
67

78
use args::Args;
89
use clap::Parser;
10+
use message::validate;
911

1012
use std::process::exit;
1113

@@ -14,7 +16,7 @@ async fn main() {
1416
let args = Args::parse();
1517

1618
let config = match config::load(args.config.clone()).await {
17-
Ok(config) => config,
19+
Ok(c) => c,
1820
Err(err) => {
1921
eprintln!("Failed to load config: {}", err);
2022
exit(1)
@@ -35,7 +37,11 @@ async fn main() {
3537

3638
let threads = messages
3739
.into_iter()
38-
.map(|message| tokio::spawn(async move { message.lint().await }))
40+
.map(|message| {
41+
let config = config.clone();
42+
43+
tokio::spawn(async move { validate(&message, &config).await })
44+
})
3945
.collect::<Vec<_>>();
4046

4147
let results = futures::future::join_all(threads).await;
@@ -46,13 +52,11 @@ async fn main() {
4652
eprintln!("{}", err);
4753
}
4854

49-
if let Ok(r) = result {
50-
if let Ok(h) = r {
51-
if let Some(violations) = &h.violations {
52-
for violation in violations {
53-
eprintln!("{}", violation);
54-
invalid = true;
55-
}
55+
if let Ok(Ok(h)) = result {
56+
if !h.violations.is_empty() {
57+
for violation in &h.violations {
58+
eprintln!("{}", violation.message);
59+
invalid = true;
5660
}
5761
}
5862
}

src/message.rs

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{
2+
config::Config,
23
git::{parse_commit_message, parse_subject},
34
result::Result as LintResult,
45
};
@@ -17,9 +18,15 @@ use std::{collections::HashMap, fmt::Error};
1718
///
1819
#[derive(Clone, Debug)]
1920
pub struct Message {
21+
/// Body part of the commit message.
22+
pub body: Option<String>,
23+
2024
/// Description part of the commit message.
2125
pub description: Option<String>,
2226

27+
/// Footers part of the commit message.
28+
pub footers: Option<HashMap<String, String>>,
29+
2330
/// Raw commit message (or any input from stdin) including the body and footers.
2431
pub raw: String,
2532

@@ -28,12 +35,6 @@ pub struct Message {
2835

2936
/// Scope part of the commit message.
3037
pub scope: Option<String>,
31-
32-
/// Body part of the commit message.
33-
pub body: Option<String>,
34-
35-
/// Footers part of the commit message.
36-
pub footers: Option<HashMap<String, String>>,
3738
}
3839

3940
/// Message represents a commit message.
@@ -60,12 +61,11 @@ impl Message {
6061
},
6162
}
6263
}
64+
}
6365

64-
/// Lint the raw commit message.
65-
pub async fn lint(&self) -> Result<LintResult, Error> {
66-
// TODO: Implement linting.
67-
Ok(LintResult {
68-
violations: Some(vec!["Hello".to_string()]),
69-
})
70-
}
66+
/// validate the raw commit message.
67+
pub async fn validate(msg: &Message, config: &Config) -> Result<LintResult, Error> {
68+
let violations = config.rules.validate(msg);
69+
70+
Ok(LintResult { violations })
7171
}

0 commit comments

Comments
 (0)