git_cliff/
lib.rs

1//! A highly customizable changelog generator ⛰️
2#![doc(
3    html_logo_url = "https://raw.githubusercontent.com/orhun/git-cliff/main/website/static/img/git-cliff.png",
4    html_favicon_url = "https://raw.githubusercontent.com/orhun/git-cliff/main/website/static/favicon/favicon.ico"
5)]
6
7/// Command-line argument parser.
8pub mod args;
9
10/// Custom logger implementation.
11pub mod logger;
12
13#[macro_use]
14extern crate log;
15
16use std::fs::{self, File};
17use std::path::{Path, PathBuf};
18use std::time::{SystemTime, UNIX_EPOCH};
19use std::{env, io};
20
21use args::{BumpOption, Opt, Sort, Strip};
22use clap::ValueEnum;
23use git_cliff_core::changelog::Changelog;
24use git_cliff_core::commit::{Commit, Range};
25use git_cliff_core::config::{CommitParser, Config};
26use git_cliff_core::embed::{BuiltinConfig, EmbeddedConfig};
27use git_cliff_core::error::{Error, Result};
28use git_cliff_core::release::Release;
29use git_cliff_core::repo::{Repository, SubmoduleRange};
30use git_cliff_core::{DEFAULT_CONFIG, IGNORE_FILE};
31use glob::Pattern;
32
33/// Checks for a new version on crates.io
34#[cfg(feature = "update-informer")]
35fn check_new_version() {
36    use update_informer::Check;
37    let pkg_name = env!("CARGO_PKG_NAME");
38    let pkg_version = env!("CARGO_PKG_VERSION");
39    let informer = update_informer::new(update_informer::registry::Crates, pkg_name, pkg_version);
40    if let Some(new_version) = informer.check_version().ok().flatten() {
41        if new_version.semver().pre.is_empty() {
42            log::info!("A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",);
43        }
44    }
45}
46
47/// Produces a commit range on the format `BASE..HEAD`, derived from the
48/// command line arguments and repository tags.
49///
50/// If no commit range could be determined, `None` is returned.
51fn determine_commit_range(
52    args: &Opt,
53    config: &Config,
54    repository: &Repository,
55) -> Result<Option<String>> {
56    let tags = repository.tags(
57        &config.git.tag_pattern,
58        args.topo_order,
59        args.use_branch_tags,
60    )?;
61
62    let mut commit_range = args.range.clone();
63    if args.unreleased {
64        if let Some(last_tag) = tags.last().map(|(k, _)| k) {
65            commit_range = Some(format!("{last_tag}..HEAD"));
66        }
67    } else if args.latest || args.current {
68        if tags.len() < 2 {
69            let commits = repository.commits(None, None, None, config.git.topo_order_commits)?;
70            if let (Some(tag1), Some(tag2)) = (
71                commits.last().map(|c| c.id().to_string()),
72                tags.get_index(0).map(|(k, _)| k),
73            ) {
74                if tags.len() == 1 {
75                    commit_range = Some(tag2.to_owned());
76                } else {
77                    commit_range = Some(format!("{tag1}..{tag2}"));
78                }
79            }
80        } else {
81            let mut tag_index = tags.len() - 2;
82            if args.current {
83                if let Some(current_tag_index) = repository.current_tag().as_ref().and_then(|tag| {
84                    tags.iter()
85                        .enumerate()
86                        .find(|(_, (_, v))| v.name == tag.name)
87                        .map(|(i, _)| i)
88                }) {
89                    match current_tag_index.checked_sub(1) {
90                        Some(i) => tag_index = i,
91                        None => {
92                            return Err(Error::ChangelogError(String::from(
93                                "No suitable tags found. Maybe run with '--topo-order'?",
94                            )));
95                        }
96                    }
97                } else {
98                    return Err(Error::ChangelogError(String::from(
99                        "No tag exists for the current commit",
100                    )));
101                }
102            }
103            if let (Some(tag1), Some(tag2)) = (
104                tags.get_index(tag_index).map(|(k, _)| k),
105                tags.get_index(tag_index + 1).map(|(k, _)| k),
106            ) {
107                commit_range = Some(format!("{tag1}..{tag2}"));
108            }
109        }
110    }
111
112    Ok(commit_range)
113}
114
115/// Process submodules and add commits to release.
116fn process_submodules(
117    repository: &'static Repository,
118    release: &mut Release,
119    topo_order_commits: bool,
120) -> Result<()> {
121    // Retrieve first and last commit of a release to create a commit range.
122    let first_commit = release
123        .previous
124        .as_ref()
125        .and_then(|previous_release| previous_release.commit_id.clone())
126        .and_then(|commit_id| repository.find_commit(&commit_id));
127    let last_commit = release
128        .commit_id
129        .clone()
130        .and_then(|commit_id| repository.find_commit(&commit_id));
131
132    trace!("Processing submodule commits in {first_commit:?}..{last_commit:?}");
133
134    // Query repository for submodule changes. For each submodule a
135    // SubmoduleRange is created, describing the range of commits in the context
136    // of that submodule.
137    if let Some(last_commit) = last_commit {
138        let submodule_ranges = repository.submodules_range(first_commit, last_commit)?;
139        let submodule_commits = submodule_ranges.iter().filter_map(|submodule_range| {
140            // For each submodule, the commit range is exploded into a list of
141            // commits.
142            let SubmoduleRange {
143                repository: sub_repo,
144                range: range_str,
145            } = submodule_range;
146            let commits = sub_repo
147                .commits(Some(range_str), None, None, topo_order_commits)
148                .ok()
149                .map(|commits| commits.iter().map(Commit::from).collect());
150
151            let submodule_path = sub_repo.path().to_string_lossy().into_owned();
152            Some(submodule_path).zip(commits)
153        });
154        // Insert submodule commits into map.
155        for (submodule_path, commits) in submodule_commits {
156            release.submodule_commits.insert(submodule_path, commits);
157        }
158    }
159    Ok(())
160}
161
162/// Processes the tags and commits for creating release entries for the
163/// changelog.
164///
165/// This function uses the configuration and arguments to process the given
166/// repository individually.
167fn process_repository<'a>(
168    repository: &'static Repository,
169    config: &mut Config,
170    args: &Opt,
171) -> Result<Vec<Release<'a>>> {
172    let mut tags = repository.tags(
173        &config.git.tag_pattern,
174        args.topo_order,
175        args.use_branch_tags,
176    )?;
177    let skip_regex = config.git.skip_tags.as_ref();
178    let ignore_regex = config.git.ignore_tags.as_ref();
179    let count_tags = config.git.count_tags.as_ref();
180    let recurse_submodules = config.git.recurse_submodules.unwrap_or(false);
181    tags.retain(|_, tag| {
182        let name = &tag.name;
183
184        // Keep skip tags to drop commits in the later stage.
185        let skip = skip_regex.is_some_and(|r| r.is_match(name));
186        if skip {
187            return true;
188        }
189
190        let count = count_tags.is_none_or(|r| {
191            let count_tag = r.is_match(name);
192            if count_tag {
193                trace!("Counting release: {}", name);
194            }
195            count_tag
196        });
197
198        let ignore = ignore_regex.is_some_and(|r| {
199            if r.as_str().trim().is_empty() {
200                return false;
201            }
202
203            let ignore_tag = r.is_match(name);
204            if ignore_tag {
205                trace!("Ignoring release: {}", name);
206            }
207            ignore_tag
208        });
209
210        count && !ignore
211    });
212
213    if !config.remote.is_any_set() {
214        match repository.upstream_remote() {
215            Ok(remote) => {
216                if !config.remote.github.is_set() {
217                    debug!("No GitHub remote is set, using remote: {}", remote);
218                    config.remote.github.owner = remote.owner;
219                    config.remote.github.repo = remote.repo;
220                    config.remote.github.is_custom = remote.is_custom;
221                } else if !config.remote.gitlab.is_set() {
222                    debug!("No GitLab remote is set, using remote: {}", remote);
223                    config.remote.gitlab.owner = remote.owner;
224                    config.remote.gitlab.repo = remote.repo;
225                    config.remote.gitlab.is_custom = remote.is_custom;
226                } else if !config.remote.gitea.is_set() {
227                    debug!("No Gitea remote is set, using remote: {}", remote);
228                    config.remote.gitea.owner = remote.owner;
229                    config.remote.gitea.repo = remote.repo;
230                    config.remote.gitea.is_custom = remote.is_custom;
231                } else if !config.remote.bitbucket.is_set() {
232                    debug!("No Bitbucket remote is set, using remote: {}", remote);
233                    config.remote.bitbucket.owner = remote.owner;
234                    config.remote.bitbucket.repo = remote.repo;
235                    config.remote.bitbucket.is_custom = remote.is_custom;
236                }
237            }
238            Err(e) => {
239                debug!("Failed to get remote from repository: {:?}", e);
240            }
241        }
242    }
243    if args.use_native_tls {
244        config.remote.enable_native_tls();
245    }
246
247    // Print debug information about configuration and arguments.
248    log::trace!("Arguments: {:#?}", args);
249    log::trace!("Config: {:#?}", config);
250
251    // Parse commits.
252    let commit_range = determine_commit_range(args, config, repository)?;
253
254    // Include only the current directory if not running from the root repository
255    let mut include_path = config.git.include_paths.clone();
256    if let Some(mut path_diff) = pathdiff::diff_paths(repository.root_path()?, env::current_dir()?)
257    {
258        if args.workdir.is_none() && include_path.is_empty() && path_diff != Path::new("") {
259            info!(
260                "Including changes from the current directory: {:?}",
261                path_diff.display()
262            );
263            path_diff.extend(["**", "*"]);
264            include_path = vec![Pattern::new(path_diff.to_string_lossy().as_ref())?];
265        }
266    }
267
268    let include_path = (!include_path.is_empty()).then_some(include_path);
269    let exclude_path =
270        (!config.git.exclude_paths.is_empty()).then_some(config.git.exclude_paths.clone());
271    let mut commits = repository.commits(
272        commit_range.as_deref(),
273        include_path,
274        exclude_path,
275        config.git.topo_order_commits,
276    )?;
277    if let Some(commit_limit_value) = config.git.limit_commits {
278        commits.truncate(commit_limit_value);
279    }
280
281    // Update tags.
282    let mut releases = vec![Release::default()];
283    let mut tag_timestamp = None;
284    if let Some(ref tag) = args.tag {
285        if let Some(commit_id) = commits.first().map(|c| c.id().to_string()) {
286            match tags.get(&commit_id) {
287                Some(tag) => {
288                    warn!("There is already a tag ({}) for {}", tag.name, commit_id);
289                    tag_timestamp = Some(commits[0].time().seconds());
290                }
291                None => {
292                    tags.insert(commit_id, repository.resolve_tag(tag));
293                }
294            }
295        } else {
296            releases[0].version = Some(tag.to_string());
297            releases[0].timestamp = Some(
298                SystemTime::now()
299                    .duration_since(UNIX_EPOCH)?
300                    .as_secs()
301                    .try_into()?,
302            );
303        }
304    }
305
306    // Process releases.
307    let mut previous_release = Release::default();
308    let mut first_processed_tag = None;
309    let repository_path = repository.root_path()?.to_string_lossy().into_owned();
310    for git_commit in commits.iter().rev() {
311        let release = releases.last_mut().unwrap();
312        let commit = Commit::from(git_commit);
313        let commit_id = commit.id.to_string();
314        release.commits.push(commit);
315        release.repository = Some(repository_path.clone());
316        release.commit_id = Some(commit_id);
317        if let Some(tag) = tags.get(release.commit_id.as_ref().unwrap()) {
318            release.version = Some(tag.name.to_string());
319            release.message.clone_from(&tag.message);
320            release.timestamp = if args.tag.as_deref() == Some(tag.name.as_str()) {
321                match tag_timestamp {
322                    Some(timestamp) => Some(timestamp),
323                    None => Some(
324                        SystemTime::now()
325                            .duration_since(UNIX_EPOCH)?
326                            .as_secs()
327                            .try_into()?,
328                    ),
329                }
330            } else {
331                Some(git_commit.time().seconds())
332            };
333            if first_processed_tag.is_none() {
334                first_processed_tag = Some(tag);
335            }
336            previous_release.previous = None;
337            release.previous = Some(Box::new(previous_release));
338            previous_release = release.clone();
339            releases.push(Release::default());
340        }
341    }
342
343    debug_assert!(!releases.is_empty());
344
345    if releases.len() > 1 {
346        previous_release.previous = None;
347        releases.last_mut().unwrap().previous = Some(Box::new(previous_release));
348    }
349
350    if args.sort == Sort::Newest {
351        for release in &mut releases {
352            release.commits.reverse();
353        }
354    }
355
356    // Add custom commit messages to the latest release.
357    if let Some(custom_commits) = &args.with_commit {
358        releases
359            .last_mut()
360            .unwrap()
361            .commits
362            .extend(custom_commits.iter().cloned().map(Commit::from));
363    }
364
365    // Set the previous release if the first release does not have one set.
366    if releases[0]
367        .previous
368        .as_ref()
369        .and_then(|p| p.version.as_ref())
370        .is_none()
371    {
372        // Get the previous tag of the first processed tag in the release loop.
373        let first_tag = first_processed_tag
374            .map(|tag| {
375                tags.iter()
376                    .enumerate()
377                    .find(|(_, (_, v))| v.name == tag.name)
378                    .and_then(|(i, _)| i.checked_sub(1))
379                    .and_then(|i| tags.get_index(i))
380            })
381            .or_else(|| Some(tags.last()))
382            .flatten();
383
384        // Set the previous release if the first tag is found.
385        if let Some((commit_id, tag)) = first_tag {
386            let previous_release = Release {
387                commit_id: Some(commit_id.to_string()),
388                version: Some(tag.name.clone()),
389                timestamp: Some(
390                    repository
391                        .find_commit(commit_id)
392                        .map(|v| v.time().seconds())
393                        .unwrap_or_default(),
394                ),
395                ..Default::default()
396            };
397            releases[0].previous = Some(Box::new(previous_release));
398        }
399    }
400
401    for release in &mut releases {
402        // Set the commit ranges for all releases
403        if !release.commits.is_empty() {
404            release.commit_range = Some(match args.sort {
405                Sort::Oldest => Range::new(
406                    release.commits.first().unwrap(),
407                    release.commits.last().unwrap(),
408                ),
409                Sort::Newest => Range::new(
410                    release.commits.last().unwrap(),
411                    release.commits.first().unwrap(),
412                ),
413            })
414        }
415        if recurse_submodules {
416            process_submodules(repository, release, config.git.topo_order_commits)?;
417        }
418    }
419
420    // Set custom message for the latest release.
421    if let Some(message) = &args.with_tag_message {
422        if let Some(latest_release) = releases
423            .iter_mut()
424            .filter(|release| !release.commits.is_empty())
425            .next_back()
426        {
427            latest_release.message = Some(message.to_owned());
428        }
429    }
430
431    Ok(releases)
432}
433
434/// Runs `git-cliff`.
435///
436/// # Example
437///
438/// ```no_run
439/// use clap::Parser;
440/// use git_cliff::args::Opt;
441/// use git_cliff_core::error::Result;
442///
443/// fn main() -> Result<()> {
444///     let args = Opt::parse();
445///     git_cliff::run(args)?;
446///     Ok(())
447/// }
448/// ```
449pub fn run(args: Opt) -> Result<()> {
450    run_with_changelog_modifier(args, |_| Ok(()))
451}
452
453/// Runs `git-cliff` with a changelog modifier.
454///
455/// This is useful if you want to modify the [`Changelog`] before
456/// it's written or the context is printed (depending how git-cliff is started).
457///
458/// # Example
459///
460/// ```no_run
461/// use clap::Parser;
462/// use git_cliff::args::Opt;
463/// use git_cliff_core::error::Result;
464///
465/// fn main() -> Result<()> {
466///     let args = Opt::parse();
467///
468///     git_cliff::run_with_changelog_modifier(args, |changelog| {
469///         println!("Releases: {:?}", changelog.releases);
470///         Ok(())
471///     })?;
472///
473///     Ok(())
474/// }
475/// ```
476pub fn run_with_changelog_modifier(
477    mut args: Opt,
478    changelog_modifier: impl FnOnce(&mut Changelog) -> Result<()>,
479) -> Result<()> {
480    // Check if there is a new version available.
481    #[cfg(feature = "update-informer")]
482    check_new_version();
483
484    // Create the configuration file if init flag is given.
485    if let Some(init_config) = args.init {
486        let contents = match init_config {
487            Some(ref name) => BuiltinConfig::get_config(name.to_string())?,
488            None => EmbeddedConfig::get_config()?,
489        };
490
491        let config_path = if args.config == PathBuf::from(DEFAULT_CONFIG) {
492            PathBuf::from(DEFAULT_CONFIG)
493        } else {
494            args.config.clone()
495        };
496
497        info!(
498            "Saving the configuration file{} to {:?}",
499            init_config.map(|v| format!(" ({v})")).unwrap_or_default(),
500            config_path
501        );
502        fs::write(config_path, contents)?;
503        return Ok(());
504    }
505
506    // Retrieve the built-in configuration.
507    let builtin_config = BuiltinConfig::parse(args.config.to_string_lossy().to_string());
508
509    // Set the working directory.
510    if let Some(ref workdir) = args.workdir {
511        args.config = workdir.join(args.config);
512        match args.repository.as_mut() {
513            Some(repository) => {
514                repository
515                    .iter_mut()
516                    .for_each(|r| *r = workdir.join(r.clone()));
517            }
518            None => args.repository = Some(vec![workdir.clone()]),
519        }
520        if let Some(changelog) = args.prepend {
521            args.prepend = Some(workdir.join(changelog));
522        }
523    }
524
525    // Set path for the configuration file.
526    let mut path = args.config.clone();
527    if !path.exists() {
528        if let Some(config_path) =
529            dirs::config_dir().map(|dir| dir.join(env!("CARGO_PKG_NAME")).join(DEFAULT_CONFIG))
530        {
531            path = config_path;
532        }
533    }
534
535    // Parse the configuration file.
536    // Load the default configuration if necessary.
537    let mut config = if let Some(url) = &args.config_url {
538        debug!("Using configuration file from: {url}");
539        #[cfg(feature = "remote")]
540        {
541            reqwest::blocking::get(url.clone())?
542                .error_for_status()?
543                .text()?
544                .parse()?
545        }
546        #[cfg(not(feature = "remote"))]
547        unreachable!("This option is not available without the 'remote' build-time feature");
548    } else if let Ok((config, name)) = builtin_config {
549        info!("Using built-in configuration file: {name}");
550        config
551    } else if path.exists() {
552        Config::load(&path)?
553    } else if let Some(contents) = Config::read_from_manifest()? {
554        contents.parse()?
555    } else if let Some(discovered_path) = env::current_dir()?.ancestors().find_map(|dir| {
556        let path = dir.join(DEFAULT_CONFIG);
557        if path.is_file() { Some(path) } else { None }
558    }) {
559        info!(
560            "Using configuration from parent directory: {}",
561            discovered_path.display()
562        );
563        Config::load(&discovered_path)?
564    } else {
565        if !args.context {
566            warn!(
567                "{:?} is not found, using the default configuration.",
568                args.config
569            );
570        }
571        EmbeddedConfig::parse()?
572    };
573
574    // Update the configuration based on command line arguments and vice versa.
575    let output = args.output.clone().or(config.changelog.output.clone());
576    match args.strip {
577        Some(Strip::Header) => {
578            config.changelog.header = None;
579        }
580        Some(Strip::Footer) => {
581            config.changelog.footer = None;
582        }
583        Some(Strip::All) => {
584            config.changelog.header = None;
585            config.changelog.footer = None;
586        }
587        None => {}
588    }
589    if args.prepend.is_some() {
590        config.changelog.footer = None;
591        if !(args.unreleased || args.latest || args.range.is_some()) {
592            return Err(Error::ArgumentError(String::from(
593                "'-u' or '-l' is not specified",
594            )));
595        }
596    }
597    if output.is_some() && args.prepend.is_some() && output.as_ref() == args.prepend.as_ref() {
598        return Err(Error::ArgumentError(String::from(
599            "'-o' and '-p' can only be used together if they point to different files",
600        )));
601    }
602    if let Some(body) = args.body.clone() {
603        config.changelog.body = body;
604    }
605    if args.sort == Sort::Oldest {
606        args.sort = Sort::from_str(&config.git.sort_commits, true)
607            .expect("Incorrect config value for 'sort_commits'");
608    }
609    if !args.topo_order {
610        args.topo_order = config.git.topo_order;
611    }
612
613    if !args.use_branch_tags {
614        args.use_branch_tags = config.git.use_branch_tags;
615    }
616
617    if args.github_token.is_some() {
618        config.remote.github.token.clone_from(&args.github_token);
619    }
620    if args.gitlab_token.is_some() {
621        config.remote.gitlab.token.clone_from(&args.gitlab_token);
622    }
623    if args.gitea_token.is_some() {
624        config.remote.gitea.token.clone_from(&args.gitea_token);
625    }
626    if args.bitbucket_token.is_some() {
627        config
628            .remote
629            .bitbucket
630            .token
631            .clone_from(&args.bitbucket_token);
632    }
633    if let Some(ref remote) = args.github_repo {
634        config.remote.github.owner = remote.0.owner.to_string();
635        config.remote.github.repo = remote.0.repo.to_string();
636        config.remote.github.is_custom = true;
637    }
638    if let Some(ref remote) = args.gitlab_repo {
639        config.remote.gitlab.owner = remote.0.owner.to_string();
640        config.remote.gitlab.repo = remote.0.repo.to_string();
641        config.remote.gitlab.is_custom = true;
642    }
643    if let Some(ref remote) = args.bitbucket_repo {
644        config.remote.bitbucket.owner = remote.0.owner.to_string();
645        config.remote.bitbucket.repo = remote.0.repo.to_string();
646        config.remote.bitbucket.is_custom = true;
647    }
648    if let Some(ref remote) = args.gitea_repo {
649        config.remote.gitea.owner = remote.0.owner.to_string();
650        config.remote.gitea.repo = remote.0.repo.to_string();
651        config.remote.gitea.is_custom = true;
652    }
653    if args.no_exec {
654        config
655            .git
656            .commit_preprocessors
657            .iter_mut()
658            .for_each(|v| v.replace_command = None);
659        config
660            .changelog
661            .postprocessors
662            .iter_mut()
663            .for_each(|v| v.replace_command = None);
664    }
665    config.git.skip_tags = config.git.skip_tags.filter(|r| !r.as_str().is_empty());
666    if args.tag_pattern.is_some() {
667        config.git.tag_pattern.clone_from(&args.tag_pattern);
668    }
669    if args.tag.is_some() {
670        config.bump.initial_tag.clone_from(&args.tag);
671    }
672    if args.ignore_tags.is_some() {
673        config.git.ignore_tags.clone_from(&args.ignore_tags);
674    }
675    if args.count_tags.is_some() {
676        config.git.count_tags.clone_from(&args.count_tags);
677    }
678    if let Some(include_path) = &args.include_path {
679        config
680            .git
681            .include_paths
682            .extend(include_path.iter().cloned());
683    }
684    if let Some(exclude_path) = &args.exclude_path {
685        config
686            .git
687            .exclude_paths
688            .extend(exclude_path.iter().cloned());
689    }
690
691    // Process commits and releases for the changelog.
692    if let Some(BumpOption::Specific(bump_type)) = args.bump {
693        config.bump.bump_type = Some(bump_type);
694    }
695
696    // Generate changelog from context.
697    let mut changelog: Changelog = if let Some(context_path) = args.from_context {
698        let mut input: Box<dyn io::Read> = if context_path == Path::new("-") {
699            Box::new(io::stdin())
700        } else {
701            Box::new(File::open(context_path)?)
702        };
703        let mut changelog = Changelog::from_context(&mut input, &config)?;
704        changelog.add_remote_context()?;
705        changelog
706    } else {
707        // Process the repositories.
708        let repositories = args.repository.clone().unwrap_or(vec![env::current_dir()?]);
709        let mut releases = Vec::<Release>::new();
710        let mut commit_range = None;
711        for repository in repositories {
712            // Skip commits
713            let mut skip_list = Vec::new();
714            let ignore_file = repository.join(IGNORE_FILE);
715            if ignore_file.exists() {
716                let contents = fs::read_to_string(ignore_file)?;
717                let commits = contents
718                    .lines()
719                    .filter(|v| !(v.starts_with('#') || v.trim().is_empty()))
720                    .map(|v| String::from(v.trim()))
721                    .collect::<Vec<String>>();
722                skip_list.extend(commits);
723            }
724            if let Some(ref skip_commit) = args.skip_commit {
725                skip_list.extend(skip_commit.clone());
726            }
727            for sha1 in skip_list {
728                config.git.commit_parsers.insert(0, CommitParser {
729                    sha: Some(sha1.to_string()),
730                    skip: Some(true),
731                    ..Default::default()
732                });
733            }
734
735            // Process the repository.
736            let repository = Repository::init(repository)?;
737
738            // The commit range, used for determining the remote commits to include
739            // in the changelog, doesn't make sense if multiple repositories are
740            // specified. As such, pick the commit range from the last given
741            // repository.
742            commit_range = determine_commit_range(&args, &config, &repository)?;
743
744            releases.extend(process_repository(
745                Box::leak(Box::new(repository)),
746                &mut config,
747                &args,
748            )?);
749        }
750        Changelog::new(releases, &config, commit_range.as_deref())?
751    };
752    changelog_modifier(&mut changelog)?;
753
754    // Print the result.
755    let mut out: Box<dyn io::Write> = if let Some(path) = &output {
756        if path == Path::new("-") {
757            Box::new(io::stdout())
758        } else {
759            Box::new(io::BufWriter::new(File::create(path)?))
760        }
761    } else {
762        Box::new(io::stdout())
763    };
764    if args.bump.is_some() || args.bumped_version {
765        let next_version = if let Some(next_version) = changelog.bump_version()? {
766            next_version
767        } else if let Some(last_version) =
768            changelog.releases.first().cloned().and_then(|v| v.version)
769        {
770            warn!("There is nothing to bump.");
771            last_version
772        } else if changelog.releases.is_empty() {
773            config.bump.get_initial_tag()
774        } else {
775            return Ok(());
776        };
777        if let Some(tag_pattern) = &config.git.tag_pattern {
778            if !tag_pattern.is_match(&next_version) {
779                return Err(Error::ChangelogError(format!(
780                    "Next version ({}) does not match the tag pattern: {}",
781                    next_version, tag_pattern
782                )));
783            }
784        }
785        if args.bumped_version {
786            writeln!(out, "{next_version}")?;
787            return Ok(());
788        }
789    }
790    if args.context {
791        changelog.write_context(&mut out)?;
792        return Ok(());
793    }
794    if let Some(path) = &args.prepend {
795        let changelog_before = fs::read_to_string(path)?;
796        let mut out = io::BufWriter::new(File::create(path)?);
797        changelog.prepend(changelog_before, &mut out)?;
798    }
799    if output.is_some() || args.prepend.is_none() {
800        changelog.generate(&mut out)?;
801    }
802
803    Ok(())
804}