Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ refactor: unify and rephrase options descriptions #12

Merged
merged 1 commit into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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