From db33c4d7f0e11a143ce82630f6df9c177b816c1c Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Tue, 13 Oct 2026 08:26:24 +0200 Subject: [PATCH 1/5] Rewrite CLI parsing using clap with "derive" feature Signed-off-by: Matthias Beyer --- Cargo.lock | 26 +++ Cargo.toml | 3 +- src/main.rs | 531 +++++++++++++++++++++------------------------------- 3 files changed, 239 insertions(+), 321 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 647a026..8b08942 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,6 +99,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" + [[package]] name = "cc" version = "1.4.0" @@ -137,6 +143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -151,6 +158,18 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "clap_lex" version = "1.1.0" @@ -336,6 +355,7 @@ name = "git-graph" version = "0.8.0" dependencies = [ "atty", + "camino", "chrono", "clap", "crossterm", @@ -392,6 +412,12 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" diff --git a/Cargo.toml b/Cargo.toml index c4b8c87..5ef2d03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,10 +26,11 @@ serde_derive = {version = "1.0", default-features = false, optional = false} toml = "0.9" itertools = "0.14" svg = "0.18" -clap = {version = "4.0", optional = false, features = ["cargo"]} +clap = {version = "4.0", optional = false, features = ["derive"]} lazy_static = "1.4" yansi = "1.0" atty = "0.2" +camino = "1.2.6" platform-dirs = "0.3" crossterm = {version = "0.29", optional = false} chrono = {version = "0.4", optional = false} diff --git a/src/main.rs b/src/main.rs index 44193fb..0966e07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,8 @@ mod print; use crate::print::svg::print_svg; -use clap::ArgMatches; -use clap::{crate_version, Arg, Command}; +use camino::Utf8PathBuf; +use clap::{ArgAction, Args, Parser, Subcommand}; use git2::Repository; use gleisbau::config::{create_config, get_available_models, get_model, get_model_name, set_model}; use gleisbau::get_repo; @@ -45,8 +45,8 @@ fn from_args() -> Result<(), String> { let mut ses = Session::new(); store_default_models(&mut ses)?; - let matches = match_args(); - if !configure_session(&mut ses, &matches)? { + let cli = Cli::parse(); + if !configure_session(&mut ses, &cli)? { return Ok(()); // If the configuration decided session should not start }; @@ -59,122 +59,203 @@ fn from_args() -> Result<(), String> { ) } -fn match_args() -> ArgMatches { - // Declare command line argument interface for clap - let app = Command::new("git-graph").version(crate_version!()).about( - "Structured Git graphs for your branching model.\n \ - https://github.com/mlange-42/git-graph\n\ - \n\ - EXAMPES:\n \ - git-graph -> Show graph\n \ - git-graph --style round -> Show graph in a different style\n \ - git-graph --model -> Show graph using a certain \n \ - git-graph model --list -> List available branching models\n \ - git-graph model -> Show repo's current branching models\n \ - git-graph model -> Permanently set model for this repo", - ); - let app = add_repo_args(app); - let app = add_model_args(app); - let app = add_commit_limit_args(app); - let app = add_color_args(app); - let app = add_wrap_args(app); - let app = add_format_args(app); - - let app = app - .arg( - Arg::new("reverse") - .long("reverse") - .short('r') - .help("Reverse the order of commits.") - .required(false) - .num_args(0), - ) - .arg( - Arg::new("local") - .long("local") - .short('l') - .help("Show only local branches, no remotes.") - .required(false) - .num_args(0), - ) - .arg( - Arg::new("svg") - .long("svg") - .help("Render graph as SVG instead of text-based.") - .required(false) - .num_args(0), - ) - .arg( - Arg::new("debug") - .long("debug") - .short('d') - .help("Additional debug output and graphics.") - .required(false) - .num_args(0), - ) - .arg( - Arg::new("sparse") - .long("sparse") - .short('S') - .help( - "Print a less compact graph: merge lines point to target lines\n\ - rather than merge commits.", - ) - .required(false) - .num_args(0), - ) - .arg( - Arg::new("style") - .long("style") - .short('s') - .help( - "Output style. One of [normal/thin|round|bold|double|ascii].\n \ - (First character can be used as abbreviation, e.g. '-s r')", - ) - .required(false) - .num_args(1), - ) - .arg( - Arg::new("refspecs") - .help( - "Branch names or refspecs to show.\n \ - Only the subgraph between the merge-base and the tips is displayed.", - ) - .num_args(1..), - ); +/// Structured Git graphs for your branching model +/// +/// https://github.com/mlange-42/git-graph +/// +/// Examples: +/// git-graph Show graph +/// git-graph --style round Show graph in a different style +/// git-graph --model Show graph using a certain +/// git-graph model --list List available branching models +/// git-graph model Show repo's current branching model +/// git-graph model Permanently set model for this repo +#[derive(Parser)] +#[command(version, verbatim_doc_comment)] +struct Cli { + /// Open repository from this path or above + #[arg(long, short, value_name = "path", default_value = ".")] + path: Utf8PathBuf, + + /// Skip owner validation for the repository (insecure, see --help) + /// + /// This turns off libgit2's owner validation, which may increase security risks. + /// Please do not disable this validation for repositories you do not trust. + #[arg(long)] + skip_repo_owner_validation: bool, + + /// Branching model: git-flow, simple or none + /// + /// Defaults to the model set for the repository, or git-flow if none is set. + /// To permanently set the model for a repository, use 'git-graph model '. + #[arg(long, short, value_name = "model")] + model: Option, + + /// Maximum number of commits to show + #[arg(long, short = 'n', value_name = "n")] + max_count: Option, + + /// When to use colors: auto, always or never + #[arg(long, value_name = "color", default_value = "auto")] + color: String, + + /// Print without colors, overrides --color + /// + /// Missing color support should be detected automatically, + /// e.g. when piping to a file. + #[arg(long)] + no_color: bool, + + /// Line wrapping for formatted commit text + /// + /// Argument format: [|auto|none [ []]] + /// 'auto' uses the terminal's width if on a terminal. + /// + /// Examples: + /// git-graph --wrap auto + /// git-graph --wrap auto 0 8 + /// git-graph --wrap none + /// git-graph --wrap 80 + /// git-graph --wrap 80 0 8 + #[arg( + long, + short, + value_name = "wrap", + action = ArgAction::Set, + num_args = 0..=3, + default_values = ["auto", "0", "8"], + verbatim_doc_comment + )] + wrap: Option>, + + /// Commit format: oneline, short, medium, full or "" + /// + /// The first character can be used as abbreviation, e.g. '-f m'. + /// + /// Formatting placeholders for "": + /// %n newline + /// %H commit hash + /// %h abbreviated commit hash + /// %P parent commit hashes + /// %p abbreviated parent commit hashes + /// %d refs (branches, tags) + /// %s commit summary + /// %b commit message body + /// %B raw body (subject and body) + /// %an author name + /// %ae author email + /// %ad author date + /// %as author date in short format 'YYYY-MM-DD' + /// %cn committer name + /// %ce committer email + /// %cd committer date + /// %cs committer date in short format 'YYYY-MM-DD' + /// + /// Modifiers after the % of a placeholder: + /// + (plus sign) a line-feed is inserted immediately before the + /// expansion if and only if the placeholder expands to a + /// non-empty string + /// - (minus sign) all consecutive line-feeds immediately preceding + /// the expansion are deleted if and only if the placeholder + /// expands to an empty string + /// ' ' (space) a space is inserted immediately before the expansion + /// if and only if the placeholder expands to a non-empty string + /// + /// See also the respective git help: https://git-scm.com/docs/pretty-formats + #[arg( + long, + short, + value_name = "format", + default_value = "oneline", + verbatim_doc_comment + )] + format: String, + + // + // other flags + // + /// Reverse the order of commits + #[arg(long, short)] + reverse: bool, + + /// Show only local branches, no remotes + #[arg(long, short)] + local: bool, + + /// Render graph as SVG instead of text-based + #[arg(long)] + svg: bool, + + /// Additional debug output and graphics + #[arg(long, short)] + debug: bool, + + /// Print a less compact graph + /// + /// Merge lines point to target lines rather than merge commits. + #[arg(long, short = 'S')] + sparse: bool, + + /// Output style: thin/normal, round, bold, double or ascii + /// + /// The first character can be used as abbreviation, e.g. '-s r'. + #[arg(long, short, value_name = "style", default_value = "thin")] + style: String, + + /// Branch names or refspecs to show + /// + /// Only the subgraph between the merge-base and the tips is displayed. + #[arg(value_name = "refspecs", num_args = 1..)] + refspecs: Vec, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum CliCommand { + /// Print or permanently set the branching model for a repository + Model(ModelArgs), +} - // Return match of declared arguments with what is present on command line - app.get_matches() +#[derive(Args)] +struct ModelArgs { + /// Branching model to set: git-flow, simple or none + /// + /// When not given, prints the currently set model. + #[arg(value_name = "model")] + model: Option, + + /// List all available branching models + #[arg(long, short)] + list: bool, } /// Return true if session should continue, false if it should exit now -fn configure_session(ses: &mut Session, matches: &ArgMatches) -> Result { +fn configure_session(ses: &mut Session, cli: &Cli) -> Result { // return values let exit_now = false; let run_application = true; - if match_model_list(ses, matches)? { + if match_model_list(ses, cli)? { return Ok(exit_now); // Exit after showing model list } - match_repo_args(ses, matches)?; + match_repo_args(ses, cli)?; - if match_model_subcommand(ses, matches)? { + if match_model_subcommand(ses, cli)? { return Ok(exit_now); // Exit after model subcommand } - match_commit_limit_args(ses, matches)?; + match_commit_limit_args(ses, cli)?; - let include_remote = !matches.get_flag("local"); + let include_remote = !cli.local; - let reverse_commit_order = matches.get_flag("reverse"); + let reverse_commit_order = cli.reverse; - ses.svg = matches.get_flag("svg"); - let compact = !matches.get_flag("sparse"); - let debug = matches.get_flag("debug"); - let style = matches - .get_one::("style") - .map(|s| Characters::from_str(s)) - .unwrap_or_else(|| Ok(Characters::thin()))?; + ses.svg = cli.svg; + let compact = !cli.sparse; + let debug = cli.debug; + let style = Characters::from_str(&cli.style)?; let style = if reverse_commit_order { style.reverse() @@ -182,13 +263,13 @@ fn configure_session(ses: &mut Session, matches: &ArgMatches) -> Result Result("refspecs") - .map(|vals| vals.cloned().collect()) - .unwrap_or_default(); + ses.refspecs = cli.refspecs.clone(); Ok(run_application) } @@ -245,36 +323,12 @@ impl Session { } } -fn add_repo_args(app: Command) -> Command { - app.arg( - Arg::new("path") - .long("path") - .short('p') - .help("Open repository from this path or above. Default '.'") - .required(false) - .num_args(1), - ) - .arg( - Arg::new("skip-repo-owner-validation") - .long("skip-repo-owner-validation") - .help( - "Skip owner validation for the repository.\n\ - This will turn off libgit2's owner validation, which may increase security risks.\n\ - Please do not disable this validation for repositories you do not trust.", - ) - .required(false) - .num_args(0), - ) -} - -fn match_repo_args(ses: &mut Session, matches: &ArgMatches) -> Result<(), String> { - let skip_repo_owner_validation = matches.get_flag("skip-repo-owner-validation"); +fn match_repo_args(ses: &mut Session, cli: &Cli) -> Result<(), String> { + let skip_repo_owner_validation = cli.skip_repo_owner_validation; if skip_repo_owner_validation { println!("Warning: skip-repo-owner-validation is set! "); } - let default_path = ".".to_string(); - let path = matches.get_one::("path").unwrap_or(&default_path); - let repository = get_repo(path, skip_repo_owner_validation) + let repository = get_repo(&cli.path, skip_repo_owner_validation) .map_err(|err| format!("ERROR: {}\n Navigate into a repository before running git-graph, or use option --path", err.message()))?; ses.repository = Some(repository); @@ -297,42 +351,9 @@ fn store_default_models(ses: &mut Session) -> Result<(), String> { Ok(()) } -fn add_model_args(app: Command) -> Command { - app - .arg( - Arg::new("model") - .long("model") - .short('m') - .help("Branching model. Available presets are [simple|git-flow|none].\n\ - Default: git-flow. \n\ - Permanently set the model for a repository with\n\ - > git-graph model ") - .required(false) - .num_args(1), - ) - .subcommand(Command::new("model") - .about("Prints or permanently sets the branching model for a repository.") - .arg( - Arg::new("model") - .help("The branching model to be used. Available presets are [simple|git-flow|none].\n\ - When not given, prints the currently set model.") - .value_name("model") - .num_args(1) - .required(false) - .index(1)) - .arg( - Arg::new("list") - .long("list") - .short('l') - .help("List all available branching models.") - .required(false) - .num_args(0), - )) -} - -fn match_model_list(ses: &mut Session, matches: &ArgMatches) -> Result { - if let Some(matches) = matches.subcommand_matches("model") { - if matches.get_flag("list") { +fn match_model_list(ses: &mut Session, cli: &Cli) -> Result { + if let Some(CliCommand::Model(model_args)) = &cli.command { + if model_args.list { println!( "{}", itertools::join(get_available_models(&ses.models_dir)?, "\n") @@ -343,10 +364,10 @@ fn match_model_list(ses: &mut Session, matches: &ArgMatches) -> Result Result { - if let Some(matches) = matches.subcommand_matches("model") { +fn match_model_subcommand(ses: &mut Session, cli: &Cli) -> Result { + if let Some(CliCommand::Model(model_args)) = &cli.command { let repository = ses.repository.as_ref().unwrap(); - match matches.get_one::("model") { + match &model_args.model { None => { let curr_model = get_model_name(repository, REPO_CONFIG_FILE)?; match curr_model { @@ -364,34 +385,18 @@ fn match_model_subcommand(ses: &mut Session, matches: &ArgMatches) -> Result Result { +fn match_model_opt(ses: &mut Session, cli: &Cli) -> Result { let model = get_model( ses.repository.as_ref().unwrap(), - matches.get_one::("model").map(|s| &s[..]), + cli.model.as_deref(), REPO_CONFIG_FILE, &ses.models_dir, )?; Ok(model) } -// -// commit_limit flag -// - -fn add_commit_limit_args(app: Command) -> Command { - app.arg( - Arg::new("max-count") - .long("max-count") - .short('n') - .help("Maximum number of commits") - .required(false) - .num_args(1) - .value_name("n"), - ) -} - -fn match_commit_limit_args(ses: &mut Session, matches: &ArgMatches) -> Result<(), String> { - ses.commit_limit = match matches.get_one::("max-count") { +fn match_commit_limit_args(ses: &mut Session, cli: &Cli) -> Result<(), String> { + ses.commit_limit = match &cli.max_count { None => None, Some(str) => match str.parse::() { Ok(val) => Some(val), @@ -407,39 +412,11 @@ fn match_commit_limit_args(ses: &mut Session, matches: &ArgMatches) -> Result<() Ok(()) } -// -// color flag -// - -fn add_color_args(app: Command) -> Command { - app.arg( - Arg::new("color") - .long("color") - .help( - "Specify when colors should be used. One of [auto|always|never].\n\ - Default: auto.", - ) - .required(false) - .num_args(1), - ) - .arg( - Arg::new("no-color") - .long("no-color") - .help( - "Print without colors. Missing color support should be detected\n\ - automatically (e.g. when piping to a file).\n\ - Overrides option '--color'", - ) - .required(false) - .num_args(0), - ) -} - -fn match_color_args(_ses: &mut Session, matches: &ArgMatches) -> Result { - let colored = if matches.get_flag("no-color") { +fn match_color_args(_ses: &mut Session, cli: &Cli) -> Result { + let colored = if cli.no_color { false - } else if let Some(mode) = matches.get_one::("color") { - match &mode[..] { + } else { + match &cli.color[..] { "auto" => { atty::is(atty::Stream::Stdout) && (!cfg!(windows) || { @@ -461,12 +438,6 @@ fn match_color_args(_ses: &mut Session, matches: &ArgMatches) -> Result Result Command { - app.arg( - Arg::new("wrap") - .long("wrap") - .short('w') - .help( - "Line wrapping for formatted commit text. Default: 'auto 0 8'\n\ - Argument format: [|auto|none[ [ ]]]\n\ - For examples, consult 'git-graph --help'", - ) - .long_help( - "Line wrapping for formatted commit text. Default: 'auto 0 8'\n\ - Argument format: [|auto|none[ [ ]]]\n\ - Examples:\n \ - git-graph --wrap auto\n \ - git-graph --wrap auto 0 8\n \ - git-graph --wrap none\n \ - git-graph --wrap 80\n \ - git-graph --wrap 80 0 8\n\ - 'auto' uses the terminal's width if on a terminal.", - ) - .required(false) - .num_args(0..=3), - ) -} - type WrapType = Option<(Option, Option, Option)>; -fn match_wrap_args(_ses: &mut Session, matches: &ArgMatches) -> Result { - let wrapping = if let Some(wrap_values) = matches.get_many::("wrap") { - let strings = wrap_values.map(|s| s.as_str()).collect::>(); +fn match_wrap_args(_ses: &mut Session, cli: &Cli) -> Result { + let wrapping = if let Some(wrap_values) = &cli.wrap { + let strings = wrap_values.iter().map(|s| s.as_str()).collect::>(); if strings.is_empty() { Some((None, Some(0), Some(8))) } else { @@ -551,62 +496,8 @@ fn match_wrap_args(_ses: &mut Session, matches: &ArgMatches) -> Result Command { - app - .arg( - Arg::new("format") - .long("format") - .short('f') - .help("Commit format. One of [oneline|short|medium|full|\"\"].\n \ - (First character can be used as abbreviation, e.g. '-f m')\n\ - Default: oneline.\n\ - For placeholders supported in \"\", consult 'git-graph --help'") - .long_help("Commit format. One of [oneline|short|medium|full|\"\"].\n \ - (First character can be used as abbreviation, e.g. '-f m')\n\ - Formatting placeholders for \"\":\n \ - %n newline\n \ - %H commit hash\n \ - %h abbreviated commit hash\n \ - %P parent commit hashes\n \ - %p abbreviated parent commit hashes\n \ - %d refs (branches, tags)\n \ - %s commit summary\n \ - %b commit message body\n \ - %B raw body (subject and body)\n \ - %an author name\n \ - %ae author email\n \ - %ad author date\n \ - %as author date in short format 'YYYY-MM-DD'\n \ - %cn committer name\n \ - %ce committer email\n \ - %cd committer date\n \ - %cs committer date in short format 'YYYY-MM-DD'\n \ - \n \ - If you add a + (plus sign) after % of a placeholder,\n \ - a line-feed is inserted immediately before the expansion if\n \ - and only if the placeholder expands to a non-empty string.\n \ - If you add a - (minus sign) after % of a placeholder, all\n \ - consecutive line-feeds immediately preceding the expansion are\n \ - deleted if and only if the placeholder expands to an empty string.\n \ - If you add a ' ' (space) after % of a placeholder, a space is\n \ - inserted immediately before the expansion if and only if\n \ - the placeholder expands to a non-empty string.\n\ - \n \ - See also the respective git help: https://git-scm.com/docs/pretty-formats\n") - .required(false) - .num_args(1), - ) -} - -fn match_format_args(_ses: &mut Session, matches: &ArgMatches) -> Result { - let format = match matches.get_one::("format") { - None => CommitFormat::OneLine, - Some(str) => CommitFormat::from_str(str)?, - }; +fn match_format_args(_ses: &mut Session, cli: &Cli) -> Result { + let format = CommitFormat::from_str(&cli.format)?; Ok(format) } From 2b66b3b97d10f18a65229b25e02f0890822b476b Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Tue, 13 Oct 2026 08:37:44 +0200 Subject: [PATCH 2/5] Make arg parsing more robust with dedicated types Signed-off-by: Matthias Beyer --- src/main.rs | 252 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 176 insertions(+), 76 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0966e07..bdfdf6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ mod print; use crate::print::svg::print_svg; use camino::Utf8PathBuf; -use clap::{ArgAction, Args, Parser, Subcommand}; +use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum}; use git2::Repository; use gleisbau::config::{create_config, get_available_models, get_model, get_model_name, set_model}; use gleisbau::get_repo; @@ -25,6 +25,8 @@ use itertools::enumerate; use platform_dirs::AppDirs; use std::path::PathBuf; use std::borrow::Borrow; +use std::convert::Infallible; +use std::fmt; use std::rc::Rc; use std::str::FromStr; use std::time::Instant; @@ -89,15 +91,15 @@ struct Cli { /// Defaults to the model set for the repository, or git-flow if none is set. /// To permanently set the model for a repository, use 'git-graph model '. #[arg(long, short, value_name = "model")] - model: Option, + model: Option, /// Maximum number of commits to show #[arg(long, short = 'n', value_name = "n")] - max_count: Option, + max_count: Option, - /// When to use colors: auto, always or never + /// When to use colors #[arg(long, value_name = "color", default_value = "auto")] - color: String, + color: ColorMode, /// Print without colors, overrides --color /// @@ -126,7 +128,7 @@ struct Cli { default_values = ["auto", "0", "8"], verbatim_doc_comment )] - wrap: Option>, + wrap: Vec, /// Commit format: oneline, short, medium, full or "" /// @@ -224,13 +226,171 @@ struct ModelArgs { /// /// When not given, prints the currently set model. #[arg(value_name = "model")] - model: Option, + model: Option, /// List all available branching models #[arg(long, short)] list: bool, } +/// Branching model, either one of the presets or a custom model +#[derive(Clone, Debug, PartialEq, Eq)] +enum Model { + GitFlow, + Simple, + None, + /// Name of a custom model file in the models directory, without `.toml` + Custom(String), +} + +impl Model { + fn as_str(&self) -> &str { + match self { + Model::GitFlow => "git-flow", + Model::Simple => "simple", + Model::None => "none", + Model::Custom(name) => name, + } + } +} + +impl FromStr for Model { + type Err = Infallible; + + fn from_str(str: &str) -> Result { + Ok(match str { + "git-flow" => Model::GitFlow, + "simple" => Model::Simple, + "none" => Model::None, + other => Model::Custom(other.to_string()), + }) + } +} + +impl fmt::Display for Model { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// When to use colors +#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] +enum ColorMode { + /// Use colors if printing to a terminal + Auto, + /// Always use colors + Always, + /// Never use colors + Never, +} + +/// A single value passed to `--wrap` +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum WrapToken { + None, + Auto, + Number(usize), +} + +impl FromStr for WrapToken { + type Err = String; + + fn from_str(str: &str) -> Result { + match str { + "none" => Ok(WrapToken::None), + "auto" => Ok(WrapToken::Auto), + _ => str + .parse() + .map(WrapToken::Number) + .map_err(|_| "expected 'auto', 'none' or a positive number".to_string()), + } + } +} + +impl fmt::Display for WrapToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WrapToken::None => f.write_str("none"), + WrapToken::Auto => f.write_str("auto"), + WrapToken::Number(number) => write!(f, "{}", number), + } + } +} + +/// Line wrapping for formatted commit text, built from the values passed to `--wrap` +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Wrap { + /// No wrapping + None, + /// Wrap at the terminal's width if on a terminal + Auto { + indent1: Option, + indent2: Option, + }, + /// Wrap at a fixed width + Width { + width: usize, + indent1: Option, + indent2: Option, + }, +} + +impl Wrap { + fn into_settings(self) -> WrapType { + match self { + Wrap::None => None, + Wrap::Auto { indent1, indent2 } => Some((None, indent1, indent2)), + Wrap::Width { + width, + indent1, + indent2, + } => Some((Some(width), indent1, indent2)), + } + } +} + +impl TryFrom<&[WrapToken]> for Wrap { + type Error = String; + + fn try_from(tokens: &[WrapToken]) -> Result { + // Indents following the first value must be numbers + let indents = |rest: &[WrapToken]| { + rest.iter() + .map(|token| match token { + WrapToken::Number(number) => Ok(*number), + _ => Err(format!( + "ERROR: Can't parse option --wrap '{}' to integers.", + itertools::join(tokens, " ") + )), + }) + .collect::, _>>() + }; + + match tokens { + [] => Ok(Wrap::Auto { + indent1: Some(0), + indent2: Some(8), + }), + [WrapToken::None, ..] => Ok(Wrap::None), + [WrapToken::Auto, rest @ ..] => { + let indents = indents(rest)?; + Ok(Wrap::Auto { + indent1: indents.first().copied(), + indent2: indents.get(1).copied(), + }) + } + [WrapToken::Number(width), rest @ ..] => { + let indents = indents(rest)?; + Ok(Wrap::Width { + width: *width, + indent1: indents.first().copied(), + indent2: indents.get(1).copied(), + }) + } + } + } +} + /// Return true if session should continue, false if it should exit now fn configure_session(ses: &mut Session, cli: &Cli) -> Result { // return values @@ -376,7 +536,7 @@ fn match_model_subcommand(ses: &mut Session, cli: &Cli) -> Result } } Some(model) => { - set_model(repository, model, REPO_CONFIG_FILE, &ses.models_dir)?; + set_model(repository, model.as_str(), REPO_CONFIG_FILE, &ses.models_dir)?; eprint!("Branching model set to '{}'", model); } }; @@ -388,7 +548,7 @@ fn match_model_subcommand(ses: &mut Session, cli: &Cli) -> Result fn match_model_opt(ses: &mut Session, cli: &Cli) -> Result { let model = get_model( ses.repository.as_ref().unwrap(), - cli.model.as_deref(), + cli.model.as_ref().map(Model::as_str), REPO_CONFIG_FILE, &ses.models_dir, )?; @@ -396,18 +556,7 @@ fn match_model_opt(ses: &mut Session, cli: &Cli) -> Result Result<(), String> { - ses.commit_limit = match &cli.max_count { - None => None, - Some(str) => match str.parse::() { - Ok(val) => Some(val), - Err(_) => { - return Err(format![ - "Option max-count must be a positive number, but got '{}'", - str - ]) - } - }, - }; + ses.commit_limit = cli.max_count.map(|max_count| max_count as usize); Ok(()) } @@ -416,27 +565,21 @@ fn match_color_args(_ses: &mut Session, cli: &Cli) -> Result { let colored = if cli.no_color { false } else { - match &cli.color[..] { - "auto" => { + match cli.color { + ColorMode::Auto => { atty::is(atty::Stream::Stdout) && (!cfg!(windows) || { yansi::enable(); yansi::is_enabled() }) } - "always" => { + ColorMode::Always => { if cfg!(windows) { yansi::enable(); } true } - "never" => false, - other => { - return Err(format!( - "Unknown color mode '{}'. Supports [auto|always|never].", - other - )) - } + ColorMode::Never => false, } }; @@ -449,51 +592,8 @@ fn match_color_args(_ses: &mut Session, cli: &Cli) -> Result { type WrapType = Option<(Option, Option, Option)>; fn match_wrap_args(_ses: &mut Session, cli: &Cli) -> Result { - let wrapping = if let Some(wrap_values) = &cli.wrap { - let strings = wrap_values.iter().map(|s| s.as_str()).collect::>(); - if strings.is_empty() { - Some((None, Some(0), Some(8))) - } else { - match strings[0] { - "none" => None, - "auto" => { - let wrap = strings - .iter() - .skip(1) - .map(|str| str.parse::()) - .collect::, _>>() - .map_err(|_| { - format!( - "ERROR: Can't parse option --wrap '{}' to integers.", - strings.join(" ") - ) - })?; - Some((None, wrap.first().cloned(), wrap.get(1).cloned())) - } - _ => { - let wrap = strings - .iter() - .map(|str| str.parse::()) - .collect::, _>>() - .map_err(|_| { - format!( - "ERROR: Can't parse option --wrap '{}' to integers.", - strings.join(" ") - ) - })?; - Some(( - wrap.first().cloned(), - wrap.get(1).cloned(), - wrap.get(2).cloned(), - )) - } - } - } - } else { - Some((None, Some(0), Some(8))) - }; - - Ok(wrapping) + let wrap = Wrap::try_from(&cli.wrap[..])?; + Ok(wrap.into_settings()) } fn match_format_args(_ses: &mut Session, cli: &Cli) -> Result { From dfb23c1f90d01b560cf6d96552ea72ec4aea9214 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Tue, 13 Oct 2026 08:42:58 +0200 Subject: [PATCH 3/5] Outsource CLI parsing into "cli" module Signed-off-by: Matthias Beyer --- src/cli.rs | 338 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 368 +++------------------------------------------------- 2 files changed, 353 insertions(+), 353 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..361c2cf --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,338 @@ +use std::convert::Infallible; +use std::fmt; + +use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum}; +use camino::Utf8PathBuf; + +pub type WrapType = Option<(Option, Option, Option)>; + +/// Structured Git graphs for your branching model +/// +/// https://github.com/mlange-42/git-graph +/// +/// Examples: +/// git-graph Show graph +/// git-graph --style round Show graph in a different style +/// git-graph --model Show graph using a certain +/// git-graph model --list List available branching models +/// git-graph model Show repo's current branching model +/// git-graph model Permanently set model for this repo +#[derive(Parser)] +#[command(version, verbatim_doc_comment)] +pub struct Cli { + /// Open repository from this path or above + #[arg(long, short, value_name = "path", default_value = ".")] + pub path: Utf8PathBuf, + + /// Skip owner validation for the repository (insecure, see --help) + /// + /// This turns off libgit2's owner validation, which may increase security risks. + /// Please do not disable this validation for repositories you do not trust. + #[arg(long)] + pub skip_repo_owner_validation: bool, + + /// Branching model: git-flow, simple or none + /// + /// Defaults to the model set for the repository, or git-flow if none is set. + /// To permanently set the model for a repository, use 'git-graph model '. + #[arg(long, short, value_name = "model")] + pub model: Option, + + /// Maximum number of commits to show + #[arg(long, short = 'n', value_name = "n")] + pub max_count: Option, + + /// When to use colors + #[arg(long, value_name = "color", default_value = "auto")] + pub color: ColorMode, + + /// Print without colors, overrides --color + /// + /// Missing color support should be detected automatically, + /// e.g. when piping to a file. + #[arg(long)] + pub no_color: bool, + + /// Line wrapping for formatted commit text + /// + /// Argument format: [|auto|none [ []]] + /// 'auto' uses the terminal's width if on a terminal. + /// + /// Examples: + /// git-graph --wrap auto + /// git-graph --wrap auto 0 8 + /// git-graph --wrap none + /// git-graph --wrap 80 + /// git-graph --wrap 80 0 8 + #[arg( + long, + short, + value_name = "wrap", + action = ArgAction::Set, + num_args = 0..=3, + default_values = ["auto", "0", "8"], + verbatim_doc_comment + )] + pub wrap: Vec, + + /// Commit format: oneline, short, medium, full or "" + /// + /// The first character can be used as abbreviation, e.g. '-f m'. + /// + /// Formatting placeholders for "": + /// %n newline + /// %H commit hash + /// %h abbreviated commit hash + /// %P parent commit hashes + /// %p abbreviated parent commit hashes + /// %d refs (branches, tags) + /// %s commit summary + /// %b commit message body + /// %B raw body (subject and body) + /// %an author name + /// %ae author email + /// %ad author date + /// %as author date in short format 'YYYY-MM-DD' + /// %cn committer name + /// %ce committer email + /// %cd committer date + /// %cs committer date in short format 'YYYY-MM-DD' + /// + /// Modifiers after the % of a placeholder: + /// + (plus sign) a line-feed is inserted immediately before the + /// expansion if and only if the placeholder expands to a + /// non-empty string + /// - (minus sign) all consecutive line-feeds immediately preceding + /// the expansion are deleted if and only if the placeholder + /// expands to an empty string + /// ' ' (space) a space is inserted immediately before the expansion + /// if and only if the placeholder expands to a non-empty string + /// + /// See also the respective git help: https://git-scm.com/docs/pretty-formats + #[arg( + long, + short, + value_name = "format", + default_value = "oneline", + verbatim_doc_comment + )] + pub format: String, + + // + // other flags + // + /// Reverse the order of commits + #[arg(long, short)] + pub reverse: bool, + + /// Show only local branches, no remotes + #[arg(long, short)] + pub local: bool, + + /// Render graph as SVG instead of text-based + #[arg(long)] + pub svg: bool, + + /// Additional debug output and graphics + #[arg(long, short)] + pub debug: bool, + + /// Print a less compact graph + /// + /// Merge lines point to target lines rather than merge commits. + #[arg(long, short = 'S')] + pub sparse: bool, + + /// Output style: thin/normal, round, bold, double or ascii + /// + /// The first character can be used as abbreviation, e.g. '-s r'. + #[arg(long, short, value_name = "style", default_value = "thin")] + pub style: String, + + /// Branch names or refspecs to show + /// + /// Only the subgraph between the merge-base and the tips is displayed. + #[arg(value_name = "refspecs", num_args = 1..)] + pub refspecs: Vec, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand)] +pub enum CliCommand { + /// Print or permanently set the branching model for a repository + Model(ModelArgs), +} + +#[derive(Args)] +pub struct ModelArgs { + /// Branching model to set: git-flow, simple or none + /// + /// When not given, prints the currently set model. + #[arg(value_name = "model")] + pub model: Option, + + /// List all available branching models + #[arg(long, short)] + pub list: bool, +} + +/// Branching model, either one of the presets or a custom model +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Model { + GitFlow, + Simple, + None, + /// Name of a custom model file in the models directory, without `.toml` + Custom(String), +} + +impl Model { + pub fn as_str(&self) -> &str { + match self { + Model::GitFlow => "git-flow", + Model::Simple => "simple", + Model::None => "none", + Model::Custom(name) => name, + } + } +} + +impl std::str::FromStr for Model { + type Err = Infallible; + + fn from_str(str: &str) -> Result { + Ok(match str { + "git-flow" => Model::GitFlow, + "simple" => Model::Simple, + "none" => Model::None, + other => Model::Custom(other.to_string()), + }) + } +} + +impl fmt::Display for Model { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// When to use colors +#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] +pub enum ColorMode { + /// Use colors if printing to a terminal + Auto, + /// Always use colors + Always, + /// Never use colors + Never, +} + +/// A single value passed to `--wrap` +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WrapToken { + None, + Auto, + Number(usize), +} + +impl std::str::FromStr for WrapToken { + type Err = String; + + fn from_str(str: &str) -> Result { + match str { + "none" => Ok(WrapToken::None), + "auto" => Ok(WrapToken::Auto), + _ => str + .parse() + .map(WrapToken::Number) + .map_err(|_| "expected 'auto', 'none' or a positive number".to_string()), + } + } +} + +impl fmt::Display for WrapToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WrapToken::None => f.write_str("none"), + WrapToken::Auto => f.write_str("auto"), + WrapToken::Number(number) => write!(f, "{}", number), + } + } +} + +/// Line wrapping for formatted commit text, built from the values passed to `--wrap` +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Wrap { + /// No wrapping + None, + /// Wrap at the terminal's width if on a terminal + Auto { + indent1: Option, + indent2: Option, + }, + /// Wrap at a fixed width + Width { + width: usize, + indent1: Option, + indent2: Option, + }, +} + +impl Wrap { + pub fn into_settings(self) -> WrapType { + match self { + Wrap::None => None, + Wrap::Auto { indent1, indent2 } => Some((None, indent1, indent2)), + Wrap::Width { + width, + indent1, + indent2, + } => Some((Some(width), indent1, indent2)), + } + } +} + +impl TryFrom<&[WrapToken]> for Wrap { + type Error = String; + + fn try_from(tokens: &[WrapToken]) -> Result { + // Indents following the first value must be numbers + let indents = |rest: &[WrapToken]| { + rest.iter() + .map(|token| match token { + WrapToken::Number(number) => Ok(*number), + _ => Err(format!( + "ERROR: Can't parse option --wrap '{}' to integers.", + itertools::join(tokens, " ") + )), + }) + .collect::, _>>() + }; + + match tokens { + [] => Ok(Wrap::Auto { + indent1: Some(0), + indent2: Some(8), + }), + [WrapToken::None, ..] => Ok(Wrap::None), + [WrapToken::Auto, rest @ ..] => { + let indents = indents(rest)?; + Ok(Wrap::Auto { + indent1: indents.first().copied(), + indent2: indents.get(1).copied(), + }) + } + [WrapToken::Number(width), rest @ ..] => { + let indents = indents(rest)?; + Ok(Wrap::Width { + width: *width, + indent1: indents.first().copied(), + indent2: indents.get(1).copied(), + }) + } + } + } +} + diff --git a/src/main.rs b/src/main.rs index bdfdf6d..8bc37cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,14 +4,15 @@ #![warn(clippy::cognitive_complexity)] #![warn(clippy::too_many_lines)] +mod cli; // TODO remove code once gleisbau API has stabilized // Some of these features might return to the CLI tool (this application) //mod config; mod print; +use crate::cli::{Cli, CliCommand, ColorMode, Model}; use crate::print::svg::print_svg; -use camino::Utf8PathBuf; -use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum}; +use clap::Parser; use git2::Repository; use gleisbau::config::{create_config, get_available_models, get_model, get_model_name, set_model}; use gleisbau::get_repo; @@ -23,10 +24,8 @@ use gleisbau::settings::{ }; use itertools::enumerate; use platform_dirs::AppDirs; -use std::path::PathBuf; use std::borrow::Borrow; -use std::convert::Infallible; -use std::fmt; +use std::path::PathBuf; use std::rc::Rc; use std::str::FromStr; use std::time::Instant; @@ -61,336 +60,6 @@ fn from_args() -> Result<(), String> { ) } -/// Structured Git graphs for your branching model -/// -/// https://github.com/mlange-42/git-graph -/// -/// Examples: -/// git-graph Show graph -/// git-graph --style round Show graph in a different style -/// git-graph --model Show graph using a certain -/// git-graph model --list List available branching models -/// git-graph model Show repo's current branching model -/// git-graph model Permanently set model for this repo -#[derive(Parser)] -#[command(version, verbatim_doc_comment)] -struct Cli { - /// Open repository from this path or above - #[arg(long, short, value_name = "path", default_value = ".")] - path: Utf8PathBuf, - - /// Skip owner validation for the repository (insecure, see --help) - /// - /// This turns off libgit2's owner validation, which may increase security risks. - /// Please do not disable this validation for repositories you do not trust. - #[arg(long)] - skip_repo_owner_validation: bool, - - /// Branching model: git-flow, simple or none - /// - /// Defaults to the model set for the repository, or git-flow if none is set. - /// To permanently set the model for a repository, use 'git-graph model '. - #[arg(long, short, value_name = "model")] - model: Option, - - /// Maximum number of commits to show - #[arg(long, short = 'n', value_name = "n")] - max_count: Option, - - /// When to use colors - #[arg(long, value_name = "color", default_value = "auto")] - color: ColorMode, - - /// Print without colors, overrides --color - /// - /// Missing color support should be detected automatically, - /// e.g. when piping to a file. - #[arg(long)] - no_color: bool, - - /// Line wrapping for formatted commit text - /// - /// Argument format: [|auto|none [ []]] - /// 'auto' uses the terminal's width if on a terminal. - /// - /// Examples: - /// git-graph --wrap auto - /// git-graph --wrap auto 0 8 - /// git-graph --wrap none - /// git-graph --wrap 80 - /// git-graph --wrap 80 0 8 - #[arg( - long, - short, - value_name = "wrap", - action = ArgAction::Set, - num_args = 0..=3, - default_values = ["auto", "0", "8"], - verbatim_doc_comment - )] - wrap: Vec, - - /// Commit format: oneline, short, medium, full or "" - /// - /// The first character can be used as abbreviation, e.g. '-f m'. - /// - /// Formatting placeholders for "": - /// %n newline - /// %H commit hash - /// %h abbreviated commit hash - /// %P parent commit hashes - /// %p abbreviated parent commit hashes - /// %d refs (branches, tags) - /// %s commit summary - /// %b commit message body - /// %B raw body (subject and body) - /// %an author name - /// %ae author email - /// %ad author date - /// %as author date in short format 'YYYY-MM-DD' - /// %cn committer name - /// %ce committer email - /// %cd committer date - /// %cs committer date in short format 'YYYY-MM-DD' - /// - /// Modifiers after the % of a placeholder: - /// + (plus sign) a line-feed is inserted immediately before the - /// expansion if and only if the placeholder expands to a - /// non-empty string - /// - (minus sign) all consecutive line-feeds immediately preceding - /// the expansion are deleted if and only if the placeholder - /// expands to an empty string - /// ' ' (space) a space is inserted immediately before the expansion - /// if and only if the placeholder expands to a non-empty string - /// - /// See also the respective git help: https://git-scm.com/docs/pretty-formats - #[arg( - long, - short, - value_name = "format", - default_value = "oneline", - verbatim_doc_comment - )] - format: String, - - // - // other flags - // - /// Reverse the order of commits - #[arg(long, short)] - reverse: bool, - - /// Show only local branches, no remotes - #[arg(long, short)] - local: bool, - - /// Render graph as SVG instead of text-based - #[arg(long)] - svg: bool, - - /// Additional debug output and graphics - #[arg(long, short)] - debug: bool, - - /// Print a less compact graph - /// - /// Merge lines point to target lines rather than merge commits. - #[arg(long, short = 'S')] - sparse: bool, - - /// Output style: thin/normal, round, bold, double or ascii - /// - /// The first character can be used as abbreviation, e.g. '-s r'. - #[arg(long, short, value_name = "style", default_value = "thin")] - style: String, - - /// Branch names or refspecs to show - /// - /// Only the subgraph between the merge-base and the tips is displayed. - #[arg(value_name = "refspecs", num_args = 1..)] - refspecs: Vec, - - #[command(subcommand)] - command: Option, -} - -#[derive(Subcommand)] -enum CliCommand { - /// Print or permanently set the branching model for a repository - Model(ModelArgs), -} - -#[derive(Args)] -struct ModelArgs { - /// Branching model to set: git-flow, simple or none - /// - /// When not given, prints the currently set model. - #[arg(value_name = "model")] - model: Option, - - /// List all available branching models - #[arg(long, short)] - list: bool, -} - -/// Branching model, either one of the presets or a custom model -#[derive(Clone, Debug, PartialEq, Eq)] -enum Model { - GitFlow, - Simple, - None, - /// Name of a custom model file in the models directory, without `.toml` - Custom(String), -} - -impl Model { - fn as_str(&self) -> &str { - match self { - Model::GitFlow => "git-flow", - Model::Simple => "simple", - Model::None => "none", - Model::Custom(name) => name, - } - } -} - -impl FromStr for Model { - type Err = Infallible; - - fn from_str(str: &str) -> Result { - Ok(match str { - "git-flow" => Model::GitFlow, - "simple" => Model::Simple, - "none" => Model::None, - other => Model::Custom(other.to_string()), - }) - } -} - -impl fmt::Display for Model { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -/// When to use colors -#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] -enum ColorMode { - /// Use colors if printing to a terminal - Auto, - /// Always use colors - Always, - /// Never use colors - Never, -} - -/// A single value passed to `--wrap` -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum WrapToken { - None, - Auto, - Number(usize), -} - -impl FromStr for WrapToken { - type Err = String; - - fn from_str(str: &str) -> Result { - match str { - "none" => Ok(WrapToken::None), - "auto" => Ok(WrapToken::Auto), - _ => str - .parse() - .map(WrapToken::Number) - .map_err(|_| "expected 'auto', 'none' or a positive number".to_string()), - } - } -} - -impl fmt::Display for WrapToken { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - WrapToken::None => f.write_str("none"), - WrapToken::Auto => f.write_str("auto"), - WrapToken::Number(number) => write!(f, "{}", number), - } - } -} - -/// Line wrapping for formatted commit text, built from the values passed to `--wrap` -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Wrap { - /// No wrapping - None, - /// Wrap at the terminal's width if on a terminal - Auto { - indent1: Option, - indent2: Option, - }, - /// Wrap at a fixed width - Width { - width: usize, - indent1: Option, - indent2: Option, - }, -} - -impl Wrap { - fn into_settings(self) -> WrapType { - match self { - Wrap::None => None, - Wrap::Auto { indent1, indent2 } => Some((None, indent1, indent2)), - Wrap::Width { - width, - indent1, - indent2, - } => Some((Some(width), indent1, indent2)), - } - } -} - -impl TryFrom<&[WrapToken]> for Wrap { - type Error = String; - - fn try_from(tokens: &[WrapToken]) -> Result { - // Indents following the first value must be numbers - let indents = |rest: &[WrapToken]| { - rest.iter() - .map(|token| match token { - WrapToken::Number(number) => Ok(*number), - _ => Err(format!( - "ERROR: Can't parse option --wrap '{}' to integers.", - itertools::join(tokens, " ") - )), - }) - .collect::, _>>() - }; - - match tokens { - [] => Ok(Wrap::Auto { - indent1: Some(0), - indent2: Some(8), - }), - [WrapToken::None, ..] => Ok(Wrap::None), - [WrapToken::Auto, rest @ ..] => { - let indents = indents(rest)?; - Ok(Wrap::Auto { - indent1: indents.first().copied(), - indent2: indents.get(1).copied(), - }) - } - [WrapToken::Number(width), rest @ ..] => { - let indents = indents(rest)?; - Ok(Wrap::Width { - width: *width, - indent1: indents.first().copied(), - indent2: indents.get(1).copied(), - }) - } - } - } -} - /// Return true if session should continue, false if it should exit now fn configure_session(ses: &mut Session, cli: &Cli) -> Result { // return values @@ -425,11 +94,14 @@ fn configure_session(ses: &mut Session, cli: &Cli) -> Result { let model = match_model_opt(ses, cli)?; - let format = match_format_args(ses, cli)?; + let format = { + let _ses: &mut Session = ses; + CommitFormat::from_str(&cli.format) + }?; let colored = match_color_args(ses, cli)?; - let wrapping = match_wrap_args(ses, cli)?; + let wrapping = crate::cli::Wrap::try_from(&cli.wrap[..]).map(|w| w.into_settings())?; let settings = Settings { reverse_commit_order, @@ -536,7 +208,12 @@ fn match_model_subcommand(ses: &mut Session, cli: &Cli) -> Result } } Some(model) => { - set_model(repository, model.as_str(), REPO_CONFIG_FILE, &ses.models_dir)?; + set_model( + repository, + model.as_str(), + REPO_CONFIG_FILE, + &ses.models_dir, + )?; eprint!("Branching model set to '{}'", model); } }; @@ -586,21 +263,6 @@ fn match_color_args(_ses: &mut Session, cli: &Cli) -> Result { Ok(colored) } -// -// wrap flag -// - -type WrapType = Option<(Option, Option, Option)>; -fn match_wrap_args(_ses: &mut Session, cli: &Cli) -> Result { - let wrap = Wrap::try_from(&cli.wrap[..])?; - Ok(wrap.into_settings()) -} - -fn match_format_args(_ses: &mut Session, cli: &Cli) -> Result { - let format = CommitFormat::from_str(&cli.format)?; - Ok(format) -} - // // Run application // From 5297e447e246b48fa05be2fcaf0fdb3e49f6e10f Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Tue, 13 Oct 2026 08:48:15 +0200 Subject: [PATCH 4/5] Inline trivial helpers Signed-off-by: Matthias Beyer --- src/main.rs | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8bc37cb..af96648 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,7 +75,8 @@ fn configure_session(ses: &mut Session, cli: &Cli) -> Result { if match_model_subcommand(ses, cli)? { return Ok(exit_now); // Exit after model subcommand } - match_commit_limit_args(ses, cli)?; + + ses.commit_limit = cli.max_count.map(|max_count| max_count as usize); let include_remote = !cli.local; @@ -92,7 +93,12 @@ fn configure_session(ses: &mut Session, cli: &Cli) -> Result { style }; - let model = match_model_opt(ses, cli)?; + let model = get_model( + ses.repository.as_ref().unwrap(), + cli.model.as_ref().map(Model::as_str), + REPO_CONFIG_FILE, + &ses.models_dir, + )?; let format = { let _ses: &mut Session = ses; @@ -222,22 +228,6 @@ fn match_model_subcommand(ses: &mut Session, cli: &Cli) -> Result Ok(false) } -fn match_model_opt(ses: &mut Session, cli: &Cli) -> Result { - let model = get_model( - ses.repository.as_ref().unwrap(), - cli.model.as_ref().map(Model::as_str), - REPO_CONFIG_FILE, - &ses.models_dir, - )?; - Ok(model) -} - -fn match_commit_limit_args(ses: &mut Session, cli: &Cli) -> Result<(), String> { - ses.commit_limit = cli.max_count.map(|max_count| max_count as usize); - - Ok(()) -} - fn match_color_args(_ses: &mut Session, cli: &Cli) -> Result { let colored = if cli.no_color { false From 3424f717b31ec221150169a92548cdcc7eb6e7f0 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Tue, 13 Oct 2026 08:48:29 +0200 Subject: [PATCH 5/5] match_color_args() cannot fail, fix return type Signed-off-by: Matthias Beyer --- src/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index af96648..2f5bf96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -105,7 +105,7 @@ fn configure_session(ses: &mut Session, cli: &Cli) -> Result { CommitFormat::from_str(&cli.format) }?; - let colored = match_color_args(ses, cli)?; + let colored = match_color_args(cli); let wrapping = crate::cli::Wrap::try_from(&cli.wrap[..]).map(|w| w.into_settings())?; @@ -228,8 +228,8 @@ fn match_model_subcommand(ses: &mut Session, cli: &Cli) -> Result Ok(false) } -fn match_color_args(_ses: &mut Session, cli: &Cli) -> Result { - let colored = if cli.no_color { +fn match_color_args(cli: &Cli) -> bool { + if cli.no_color { false } else { match cli.color { @@ -248,9 +248,7 @@ fn match_color_args(_ses: &mut Session, cli: &Cli) -> Result { } ColorMode::Never => false, } - }; - - Ok(colored) + } } //