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/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 44193fb..2f5bf96 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 clap::ArgMatches; -use clap::{crate_version, Arg, Command}; +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,8 +24,8 @@ use gleisbau::settings::{ }; use itertools::enumerate; use platform_dirs::AppDirs; -use std::path::PathBuf; use std::borrow::Borrow; +use std::path::PathBuf; use std::rc::Rc; use std::str::FromStr; use std::time::Instant; @@ -45,8 +46,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 +60,32 @@ 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..), - ); - - // Return match of declared arguments with what is present on command line - app.get_matches() -} - /// 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)?; - let include_remote = !matches.get_flag("local"); + ses.commit_limit = cli.max_count.map(|max_count| max_count as usize); + + 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 +93,21 @@ 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 +161,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 +189,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 +202,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 { @@ -355,7 +214,12 @@ fn match_model_subcommand(ses: &mut Session, matches: &ArgMatches) -> Result { - 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); } }; @@ -364,250 +228,27 @@ fn match_model_subcommand(ses: &mut Session, matches: &ArgMatches) -> Result Result { - let model = get_model( - ses.repository.as_ref().unwrap(), - matches.get_one::("model").map(|s| &s[..]), - 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") { - 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 - ]) - } - }, - }; - - 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(cli: &Cli) -> bool { + if cli.no_color { false - } else if let Some(mode) = matches.get_one::("color") { - match &mode[..] { - "auto" => { + } else { + 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 - )) - } - } - } else { - atty::is(atty::Stream::Stdout) - && (!cfg!(windows) || { - yansi::enable(); - yansi::is_enabled() - }) - }; - - Ok(colored) -} - -// -// wrap flag -// - -fn add_wrap_args(app: Command) -> 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::>(); - 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(), - )) - } - } + ColorMode::Never => false, } - } else { - Some((None, Some(0), Some(8))) - }; - - Ok(wrapping) -} - -// -// commit format flags - format -// - -fn add_format_args(app: Command) -> 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)?, - }; - Ok(format) + } } //