diff --git a/README.md b/README.md index a1513dcd1e..55aa00c950 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ git-cliff [FLAGS] [OPTIONS] [--] [RANGE] ``` -c, --config Sets the configuration file [env: GIT_CLIFF_CONFIG=] [default: cliff.toml] -w, --workdir Sets the working directory [env: GIT_CLIFF_WORKDIR=] --r, --repository Sets the git repository [env: GIT_CLIFF_REPOSITORY=] +-r, --repository ... Sets the git repository [env: GIT_CLIFF_REPOSITORY=] --include-path ... Sets the path to include related commits [env: GIT_CLIFF_INCLUDE_PATH=] --exclude-path ... Sets the path to exclude related commits [env: GIT_CLIFF_EXCLUDE_PATH=] --with-commit ... Sets custom commit messages to include in the changelog [env: GIT_CLIFF_WITH_COMMIT=] @@ -245,6 +245,13 @@ git cliff --include-path "**/*.toml" --include-path "*.md" git cliff --exclude-path ".github/*" ``` +Generate a changelog for multiple git repositories: + +```sh +# merges the commit history +git cliff --repository path1 path2 +``` + Generate a changelog that includes yet unexisting commit messages: ```sh diff --git a/git-cliff/src/args.rs b/git-cliff/src/args.rs index afea919894..f4823633fe 100644 --- a/git-cliff/src/args.rs +++ b/git-cliff/src/args.rs @@ -44,8 +44,14 @@ pub struct Opt { #[clap(short, long, env = "GIT_CLIFF_WORKDIR", value_name = "PATH")] pub workdir: Option, /// Sets the git repository. - #[clap(short, long, env = "GIT_CLIFF_REPOSITORY", value_name = "PATH")] - pub repository: Option, + #[clap( + short, + long, + env = "GIT_CLIFF_REPOSITORY", + value_name = "PATH", + multiple_values = true + )] + pub repository: Option>, /// Sets the path to include related commits. #[clap( long, diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs index f23d3e310c..3bf96c6326 100644 --- a/git-cliff/src/lib.rs +++ b/git-cliff/src/lib.rs @@ -53,103 +53,17 @@ fn check_new_version() { } } -/// Runs `git-cliff`. -pub fn run(mut args: Opt) -> Result<()> { - // Check if there is a new version available. - #[cfg(feature = "update-informer")] - check_new_version(); - - // Create the configuration file if init flag is given. - if args.init { - info!("Saving the configuration file to {:?}", DEFAULT_CONFIG); - fs::write(DEFAULT_CONFIG, EmbeddedConfig::get_config()?)?; - return Ok(()); - } - - // Set the working directory. - if let Some(ref workdir) = args.workdir { - args.config = workdir.join(args.config); - args.repository = match args.repository { - Some(repository) => Some(workdir.join(repository)), - None => Some(workdir.clone()), - }; - if let Some(changelog) = args.prepend { - args.prepend = Some(workdir.join(changelog)); - } - } - - // Parse the configuration file. - let mut path = args.config.clone(); - if !path.exists() { - if let Some(config_path) = dirs_next::config_dir() - .map(|dir| dir.join(env!("CARGO_PKG_NAME")).join(DEFAULT_CONFIG)) - { - path = config_path; - } - } - - // Load the default configuration if necessary. - let mut config = if path.exists() { - Config::parse(&path)? - } else { - if !args.context { - warn!( - "{:?} is not found, using the default configuration.", - args.config - ); - } - EmbeddedConfig::parse()? - }; - if config.changelog.body.is_none() && !args.context { - warn!("Changelog body is not specified, using the default template."); - config.changelog.body = EmbeddedConfig::parse()?.changelog.body; - } - - // Update the configuration based on command line arguments and vice versa. - match args.strip { - Some(Strip::Header) => { - config.changelog.header = None; - } - Some(Strip::Footer) => { - config.changelog.footer = None; - } - Some(Strip::All) => { - config.changelog.header = None; - config.changelog.footer = None; - } - None => {} - } - if args.prepend.is_some() { - config.changelog.footer = None; - if !(args.unreleased || args.latest || args.range.is_some()) { - return Err(Error::ArgumentError(String::from( - "'-u' or '-l' is not specified", - ))); - } - } - if args.body.is_some() { - config.changelog.body = args.body.clone(); - } - if args.sort == Sort::Oldest { - if let Some(ref sort_commits) = config.git.sort_commits { - args.sort = Sort::from_str(sort_commits, true) - .expect("Incorrect config value for 'sort_commits'"); - } - } - if !args.topo_order { - if let Some(topo_order) = config.git.topo_order { - args.topo_order = topo_order; - } - } - - // Initialize the git repository. - let repository = - Repository::init(args.repository.clone().unwrap_or(env::current_dir()?))?; - - // Parse tags. +/// Processes the tags and commits for creating release entries for the +/// changelog. +/// +/// This function uses the configuration and arguments to process the given +/// repository individually. +fn process_repository<'a>( + repository: &'static Repository, + mut config: Config, + args: &Opt, +) -> Result>> { let mut tags = repository.tags(&config.git.tag_pattern, args.topo_order)?; - - // Skip tags. config.git.skip_tags = config.git.skip_tags.filter(|r| !r.as_str().is_empty()); let skip_regex = config.git.skip_tags.as_ref(); let ignore_regex = config.git.ignore_tags.as_ref(); @@ -182,7 +96,7 @@ pub fn run(mut args: Opt) -> Result<()> { log::trace!("{:#?}", config); // Parse commits. - let mut commit_range = args.range; + let mut commit_range = args.range.clone(); if args.unreleased { if let Some(last_tag) = tags.last().map(|(k, _)| k) { commit_range = Some(format!("{last_tag}..HEAD")); @@ -229,8 +143,11 @@ pub fn run(mut args: Opt) -> Result<()> { } } } - let mut commits = - repository.commits(commit_range, args.include_path, args.exclude_path)?; + let mut commits = repository.commits( + commit_range, + args.include_path.clone(), + args.exclude_path.clone(), + )?; if let Some(commit_limit_value) = config.git.limit_commits { commits = commits.drain(..commit_limit_value).collect(); } @@ -281,10 +198,12 @@ pub fn run(mut args: Opt) -> Result<()> { } // Add custom commit messages to the latest release. - if let Some(custom_commits) = args.with_commit { + if let Some(custom_commits) = &args.with_commit { if let Some(latest_release) = releases.iter_mut().last() { - custom_commits.into_iter().for_each(|message| { - latest_release.commits.push(Commit::from(message)) + custom_commits.iter().for_each(|message| { + latest_release + .commits + .push(Commit::from(message.to_string())) }); } } @@ -303,6 +222,114 @@ pub fn run(mut args: Opt) -> Result<()> { } } + Ok(releases) +} + +/// Runs `git-cliff`. +pub fn run(mut args: Opt) -> Result<()> { + // Check if there is a new version available. + #[cfg(feature = "update-informer")] + check_new_version(); + + // Create the configuration file if init flag is given. + if args.init { + info!("Saving the configuration file to {:?}", DEFAULT_CONFIG); + fs::write(DEFAULT_CONFIG, EmbeddedConfig::get_config()?)?; + return Ok(()); + } + + // Set the working directory. + if let Some(ref workdir) = args.workdir { + args.config = workdir.join(args.config); + match args.repository.as_mut() { + Some(repository) => { + repository + .iter_mut() + .for_each(|r| *r = workdir.join(r.clone())); + } + None => args.repository = Some(vec![workdir.clone()]), + } + if let Some(changelog) = args.prepend { + args.prepend = Some(workdir.join(changelog)); + } + } + + // Parse the configuration file. + let mut path = args.config.clone(); + if !path.exists() { + if let Some(config_path) = dirs_next::config_dir() + .map(|dir| dir.join(env!("CARGO_PKG_NAME")).join(DEFAULT_CONFIG)) + { + path = config_path; + } + } + + // Load the default configuration if necessary. + let mut config = if path.exists() { + Config::parse(&path)? + } else { + if !args.context { + warn!( + "{:?} is not found, using the default configuration.", + args.config + ); + } + EmbeddedConfig::parse()? + }; + if config.changelog.body.is_none() && !args.context { + warn!("Changelog body is not specified, using the default template."); + config.changelog.body = EmbeddedConfig::parse()?.changelog.body; + } + + // Update the configuration based on command line arguments and vice versa. + match args.strip { + Some(Strip::Header) => { + config.changelog.header = None; + } + Some(Strip::Footer) => { + config.changelog.footer = None; + } + Some(Strip::All) => { + config.changelog.header = None; + config.changelog.footer = None; + } + None => {} + } + if args.prepend.is_some() { + config.changelog.footer = None; + if !(args.unreleased || args.latest || args.range.is_some()) { + return Err(Error::ArgumentError(String::from( + "'-u' or '-l' is not specified", + ))); + } + } + if args.body.is_some() { + config.changelog.body = args.body.clone(); + } + if args.sort == Sort::Oldest { + if let Some(ref sort_commits) = config.git.sort_commits { + args.sort = Sort::from_str(sort_commits, true) + .expect("Incorrect config value for 'sort_commits'"); + } + } + if !args.topo_order { + if let Some(topo_order) = config.git.topo_order { + args.topo_order = topo_order; + } + } + + // Process the repository. + let repositories = args.repository.clone().unwrap_or(vec![env::current_dir()?]); + let mut releases = Vec::::new(); + for repository in repositories { + let repository = Repository::init(repository)?; + releases.extend(process_repository( + Box::leak(Box::new(repository)), + config.clone(), + &args, + )?); + } + // Generate output. let changelog = Changelog::new(releases, &config)?; if args.context {