Skip to content

Commit

Permalink
♻️ refactor: unify and rephrase options descriptions (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
welpo authored Feb 8, 2024
1 parent 8eca1e3 commit 85078f0
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 91 deletions.
81 changes: 56 additions & 25 deletions src/args.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::config::{DescriptionCase, InitOption, ParsedCommitDisplayFormat};
use crate::lint::constants::config_descriptions;
use clap::{builder::ArgPredicate, Parser};
use clap_complete::Shell;

Expand All @@ -11,26 +12,27 @@ use clap_complete::Shell;
)]

pub struct Opt {
/// Commit message to lint. Alternatively, read from STDIN.
#[arg(index = 1)]
#[arg(index = 1, help = config_descriptions::COMMIT_MESSAGE)]
pub commit_message: Option<String>,

/// Initialize the default configuration ("config", default value) or commit-msg hook ("hook").
#[arg(
long,
value_name = "OPTION",
num_args = 0..=1,
required = false,
default_missing_value = "config"
default_missing_value = "config",
help = config_descriptions::INIT
)]
pub init: Option<InitOption>,

/// Generate shell completion script for the specified shell.
#[arg(long, value_enum, required = false, value_name = "SHELL")]
#[arg(long,
value_enum,
required = false,
value_name = "SHELL",
help = config_descriptions::GENERATE_SHELL_COMPLETION)]
pub generate_shell_completion: Option<Shell>,

/// Path to a TOML configuration file.
#[arg(long, env = "GIT_SUMI_CONFIG")]
#[arg(long, env = "GIT_SUMI_CONFIG", help = config_descriptions::CONFIG)]
pub config: Option<String>,

/// Suppress progress messages.
Expand All @@ -39,7 +41,8 @@ pub struct Opt {
num_args = 0,
default_missing_value = "true",
long,
env = "GIT_SUMI_QUIET"
env = "GIT_SUMI_QUIET",
help = config_descriptions::QUIET.short
)]
pub quiet: Option<bool>,

Expand All @@ -49,7 +52,8 @@ pub struct Opt {
long,
env = "GIT_SUMI_SPLIT_LINES",
num_args = 0,
default_missing_value = "true"
default_missing_value = "true",
help = config_descriptions::SPLIT_LINES.short
)]
pub split_lines: Option<bool>,

Expand All @@ -59,21 +63,25 @@ pub struct Opt {
long,
env = "GIT_SUMI_DISPLAY",
num_args = 0,
default_missing_value = "true"
default_missing_value = "true",
help = config_descriptions::DISPLAY.short
)]
pub display: Option<bool>,

/// Specify the output format for displaying the parsed commit message.
/// Options: "cli", "table", "json", "toml". Default: "cli"
#[arg(short = 'f', long, env = "GIT_SUMI_FORMAT")]
#[arg(short = 'f',
long,
env = "GIT_SUMI_FORMAT",
help = config_descriptions::FORMAT.short)]
pub format: Option<ParsedCommitDisplayFormat>,

/// Commit the message after successful linting.
#[arg(short = 'c', long)]
#[arg(short = 'c', long, help=config_descriptions::COMMIT)]
pub commit: bool,

/// Force a commit, regardless of linting errors.
#[arg(long)]
#[arg(long, help=config_descriptions::FORCE)]
pub force: bool,

/// Follow Conventional Commits format.
Expand All @@ -86,7 +94,8 @@ pub struct Opt {
("types_allowed", ArgPredicate::IsPresent, Some("true")),
("scopes_allowed", ArgPredicate::IsPresent, Some("true")),
]),
help_heading = "Rules")]
help_heading = "Rules",
help = config_descriptions::CONVENTIONAL.short)]
pub conventional: Option<bool>,

/// Use the imperative mood in the description.
Expand All @@ -95,7 +104,9 @@ pub struct Opt {
long,
env = "GIT_SUMI_IMPERATIVE",
num_args = 0,
default_missing_value = "true"
default_missing_value = "true",
help_heading = "Rules",
help = config_descriptions::IMPERATIVE.short
)]
pub imperative: Option<bool>,

Expand All @@ -105,7 +116,9 @@ pub struct Opt {
long,
env = "GIT_SUMI_GITMOJI",
num_args = 0,
default_missing_value = "true"
default_missing_value = "true",
help_heading = "Rules",
help = config_descriptions::GITMOJI.short
)]
pub gitmoji: Option<bool>,

Expand All @@ -115,7 +128,9 @@ pub struct Opt {
long,
env = "GIT_SUMI_WHITESPACE",
num_args = 0,
default_missing_value = "true"
default_missing_value = "true",
help_heading = "Rules",
help = config_descriptions::WHITESPACE.short
)]
pub whitespace: Option<bool>,

Expand All @@ -126,7 +141,8 @@ pub struct Opt {
long,
env = "GIT_SUMI_DESCRIPTION_CASE",
value_name = "CASE",
help_heading = "Rules"
help_heading = "Rules",
help = config_descriptions::DESCRIPTION_CASE.short
)]
pub description_case: Option<DescriptionCase>,

Expand All @@ -136,16 +152,28 @@ pub struct Opt {
long,
env = "GIT_SUMI_NO_PERIOD",
num_args = 0,
default_missing_value = "true"
default_missing_value = "true",
help_heading = "Rules",
help = config_descriptions::NO_PERIOD.short
)]
pub no_period: Option<bool>,

/// Limit the header to the specified length.
#[arg(short = 'H', long, env = "GIT_SUMI_MAX_HEADER_LENGTH", value_parser = clap::value_parser!(usize), help_heading = "Rules")]
#[arg(short = 'H',
long,
env = "GIT_SUMI_MAX_HEADER_LENGTH",
value_parser = clap::value_parser!(usize),
help_heading = "Rules",
help = config_descriptions::MAX_HEADER_LENGTH.short)]
pub max_header_length: Option<usize>,

/// Wrap the body at the specified length.
#[arg(short = 'B', long, env = "GIT_SUMI_MAX_BODY_LENGTH", value_parser = clap::value_parser!(usize), help_heading = "Rules")]
#[arg(short = 'B',
long,
env = "GIT_SUMI_MAX_BODY_LENGTH",
value_parser = clap::value_parser!(usize),
help_heading = "Rules",
help = config_descriptions::MAX_BODY_LENGTH.short)]
pub max_body_length: Option<usize>,

/// Only allow the specified, comma-separated commit scopes.
Expand All @@ -154,7 +182,8 @@ pub struct Opt {
long,
env = "GIT_SUMI_SCOPES_ALLOWED",
value_name = "SCOPES",
help_heading = "Rules"
help_heading = "Rules",
help = config_descriptions::SCOPES_ALLOWED.short
)]
pub scopes_allowed: Vec<String>,

Expand All @@ -164,7 +193,8 @@ pub struct Opt {
long,
env = "GIT_SUMI_TYPES_ALLOWED",
value_name = "TYPES",
help_heading = "Rules"
help_heading = "Rules",
help = config_descriptions::TYPES_ALLOWED.short
)]
pub types_allowed: Vec<String>,

Expand All @@ -174,7 +204,8 @@ pub struct Opt {
long,
env = "GIT_SUMI_HEADER_PATTERN",
value_name = "PATTERN",
help_heading = "Rules"
help_heading = "Rules",
help = config_descriptions::HEADER_PATTERN.short
)]
pub header_pattern: Option<String>,
}
56 changes: 37 additions & 19 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::lint::constants::config_descriptions::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
Expand Down Expand Up @@ -298,32 +299,49 @@ impl Config {
let toml = toml::to_string(&default_config)?;

let config_comments = HashMap::from([
("quiet", "Suppress progress messages."),
("display", "Shows the parsed commit message post-linting. See 'format' for options."),
("format", "Output format for the parsed commit message. Options: \"cli\", \"json\", \"table\", \"toml\"."),
("split_lines", "Process each non-empty line in the commit message as an individual commit."),
("gitmoji", "Rule: include one valid Gitmoji: https://gitmoji.dev/"),
("conventional", "Rule: follow Conventional Commits format: https://www.conventionalcommits.org/"),
("imperative", "Rule: use the imperative mood in the description (e.g. \"Fix bug\" instead of \"Fixed bug\")."),
("whitespace", "Rule: disallow leading/trailing whitespace and consecutive spaces."),
("description_case", "Rule: commit description must start with the specified case. Options: \"any\", \"lower\", \"upper\"."),
("no_period", "Rule: do not end commit header with a period."),
("max_header_length", "Rule: limit the header to the specified length. A value of 0 disables this rule."),
("max_body_length", "Rule: wrap the body at the specified length. A value of 0 disables this rule."),
("scopes_allowed", "Rule: only allow the specified commit scopes. Example: [\"docs\", \"cli\"]. An empty list allows any scope."),
("types_allowed", "Rule: only allow the specified commit types. Example: [\"feat\", \"fix\"]. An empty list allows any type."),
("header_pattern", "Rule: commit header must match the specified (regex) pattern. Example: '^JIRA-\\d+:'"),
("quiet", format_description(&QUIET, false)),
("display", format_description(&DISPLAY, false)),
("format", format_description(&FORMAT, false)),
("split_lines", format_description(&SPLIT_LINES, false)),
("gitmoji", format_description(&GITMOJI, true)),
(
"description_case",
format_description(&DESCRIPTION_CASE, true),
),
("imperative", format_description(&IMPERATIVE, true)),
("no_period", format_description(&NO_PERIOD, true)),
("whitespace", format_description(&WHITESPACE, true)),
(
"max_header_length",
format_description(&MAX_HEADER_LENGTH, true),
),
(
"max_body_length",
format_description(&MAX_BODY_LENGTH, true),
),
("conventional", format_description(&CONVENTIONAL, true)),
("scopes_allowed", format_description(&SCOPES_ALLOWED, true)),
("types_allowed", format_description(&TYPES_ALLOWED, true)),
("header_pattern", format_description(&HEADER_PATTERN, true)),
]);

fn format_description(description: &RuleDescription, is_rule: bool) -> String {
let prefix = if is_rule { "Rule: " } else { "" };
let mut formatted_description = format!("\n# {}{}.\n", prefix, description.short);
if let Some(extra) = description.extra {
formatted_description += &format!("# {}.\n", extra);
}
formatted_description
}

let mut toml_with_comments = String::new();
for line in toml.lines() {
if let Some((key, _)) = line.split_once('=') {
if let Some((key, value)) = line.split_once('=') {
if let Some(comment) = config_comments.get(key.trim()) {
toml_with_comments.push_str(&format!("\n# {}\n", comment));
toml_with_comments.push_str(comment);
}
toml_with_comments.push_str(&format!("{}={}\n", key, value));
}
toml_with_comments.push_str(line);
toml_with_comments.push('\n');
}

let toml_with_comments = format!(
Expand Down
1 change: 1 addition & 0 deletions src/lint/constants.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod config_descriptions;
pub mod gitmoji;
pub mod non_imperative_verbs;
91 changes: 91 additions & 0 deletions src/lint/constants/config_descriptions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// CLI-exclusive --help descriptions.
pub const COMMIT_MESSAGE: &str = "Commit message to lint. Alternatively, read from STDIN";
pub const INIT: &str =
"Initialize the default configuration ('config') or commit-msg hook ('hook'). Default: 'config'";
pub const GENERATE_SHELL_COMPLETION: &str =
"Generate shell completion script for the specified shell";
pub const CONFIG: &str = "Path to a TOML configuration file";
pub const COMMIT: &str = "Commit the message after successful linting";
pub const FORCE: &str = "Force the commit even if linting fails";

// Structure for rule descriptions.
// The 'short' version is used in --help.
// The 'extra' version is used to create the default config.
pub struct RuleDescription {
pub short: &'static str,
pub extra: Option<&'static str>,
}

// General configuration.
pub const QUIET: RuleDescription = RuleDescription {
short: "Suppresses progress messages",
extra: None,
};
pub const DISPLAY: RuleDescription = RuleDescription {
short: "Displays parsed commit message",
extra: None,
};
pub const FORMAT: RuleDescription = RuleDescription {
short: "Sets display format: cli, json, table, toml",
extra: None,
};
pub const SPLIT_LINES: RuleDescription = RuleDescription {
short: "Processes each non-empty line as an individual commit",
extra: None,
};

// Rules.
pub const GITMOJI: RuleDescription = RuleDescription {
short: "Include one valid Gitmoji",
extra: Some("See https://gitmoji.dev/"),
};

pub const CONVENTIONAL: RuleDescription = RuleDescription {
short: "Follow Conventional Commits format",
extra: Some("See https://www.conventionalcommits.org/"),
};

pub const IMPERATIVE: RuleDescription = RuleDescription {
short: "Use the imperative mood in the description",
extra: Some("Example: 'Fix bug' instead of 'Fixed bug'"),
};

pub const WHITESPACE: RuleDescription = RuleDescription {
short: "No leading, trailing, or consecutive spaces",
extra: None,
};

pub const DESCRIPTION_CASE: RuleDescription = RuleDescription {
short: "Description must start with the specified case",
extra: Some("Options: 'any', 'lower', 'upper'"),
};

pub const NO_PERIOD: RuleDescription = RuleDescription {
short: "Do not end commit header with a period",
extra: None,
};

pub const MAX_HEADER_LENGTH: RuleDescription = RuleDescription {
short: "Header length limit",
extra: Some("A value of 0 disables the rule"),
};

pub const MAX_BODY_LENGTH: RuleDescription = RuleDescription {
short: "Body line length limit",
extra: Some("A value of 0 disables the rule"),
};

pub const SCOPES_ALLOWED: RuleDescription = RuleDescription {
short: "List of allowed commit scopes",
extra: Some("An empty list allows all scopes. Example: [\"docs\", \"cli\"]"),
};

pub const TYPES_ALLOWED: RuleDescription = RuleDescription {
short: "List of allowed commit types",
extra: Some("An empty list allows all types. Example: [\"feat\", \"fix\", \"docs\"]"),
};

pub const HEADER_PATTERN: RuleDescription = RuleDescription {
short: "Header must match regex pattern",
extra: Some("Example: '^JIRA-\\d+:'"),
};
Loading

0 comments on commit 85078f0

Please sign in to comment.