1#![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
7pub mod args;
9
10pub mod logger;
12
13use std::env;
14use std::fs::{self, File};
15use std::io::{self, Write};
16use std::path::{Path, PathBuf};
17use std::time::{SystemTime, UNIX_EPOCH};
18
19use args::{BumpOption, Opt, Sort, Strip};
20use clap::ValueEnum;
21use git_cliff_core::changelog::Changelog;
22use git_cliff_core::commit::{Commit, Range};
23use git_cliff_core::config::{CommitParser, Config};
24use git_cliff_core::embed::{BuiltinConfig, EmbeddedConfig};
25use git_cliff_core::error::{Error, Result};
26use git_cliff_core::release::Release;
27use git_cliff_core::repo::{Repository, SubmoduleRange};
28use git_cliff_core::{DEFAULT_CONFIG, IGNORE_FILE};
29use glob::Pattern;
30
31#[cfg(feature = "update-informer")]
33pub fn check_new_version() {
34 use update_informer::Check;
35 let pkg_name = env!("CARGO_PKG_NAME");
36 let pkg_version = env!("CARGO_PKG_VERSION");
37 let informer = update_informer::new(update_informer::registry::Crates, pkg_name, pkg_version);
38 if let Some(new_version) = informer.check_version().ok().flatten() {
39 if new_version.semver().pre.is_empty() {
40 tracing::info!(
41 "A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",
42 );
43 }
44 }
45}
46
47fn 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
115fn process_submodules(
117 repository: &'static Repository,
118 release: &mut Release,
119 topo_order_commits: bool,
120) -> Result<()> {
121 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 tracing::debug!("Processing submodule commits in {first_commit:?}..{last_commit:?}");
133
134 if let Some(last_commit) = last_commit {
138 let submodule_ranges = repository.submodules_range(first_commit.as_ref(), &last_commit)?;
139 let submodule_commits = submodule_ranges.iter().filter_map(|submodule_range| {
140 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 for (submodule_path, commits) in submodule_commits {
156 release.submodule_commits.insert(submodule_path, commits);
157 }
158 }
159 Ok(())
160}
161
162pub fn init_config(name: Option<&str>, config_path: &Path) -> Result<()> {
164 let contents = match name {
165 Some(name) => BuiltinConfig::get_config(name.to_string())?,
166 None => EmbeddedConfig::get_config()?,
167 };
168
169 let config_path = if config_path == Path::new(DEFAULT_CONFIG) {
170 PathBuf::from(DEFAULT_CONFIG)
171 } else {
172 config_path.to_path_buf()
173 };
174
175 tracing::info!(
176 "Saving the configuration file{} to {}",
177 name.map(|v| format!(" ({v})")).unwrap_or_default(),
178 config_path.display(),
179 );
180
181 fs::write(config_path, contents)?;
182
183 Ok(())
184}
185
186fn process_repository<'a>(
192 repository: &'static Repository,
193 config: &mut Config,
194 args: &Opt,
195) -> Result<Vec<Release<'a>>> {
196 let mut tags = repository.tags(
197 &config.git.tag_pattern,
198 args.topo_order,
199 args.use_branch_tags,
200 )?;
201 let skip_regex = config.git.skip_tags.as_ref();
202 let ignore_regex = config.git.ignore_tags.as_ref();
203 let count_tags = config.git.count_tags.as_ref();
204 let recurse_submodules = config.git.recurse_submodules.unwrap_or(false);
205 tags.retain(|_, tag| {
206 let name = &tag.name;
207
208 let skip = skip_regex.is_some_and(|r| r.is_match(name));
210 if skip {
211 return true;
212 }
213
214 let count = count_tags.is_none_or(|r| {
215 let count_tag = r.is_match(name);
216 if count_tag {
217 tracing::debug!("Counting release: {name}");
218 }
219 count_tag
220 });
221
222 let ignore = ignore_regex.is_some_and(|r| {
223 if r.as_str().trim().is_empty() {
224 return false;
225 }
226
227 let ignore_tag = r.is_match(name);
228 if ignore_tag {
229 tracing::debug!("Ignoring release: {name}");
230 }
231 ignore_tag
232 });
233
234 count && !ignore
235 });
236
237 if !config.remote.is_any_set() {
238 match repository.upstream_remote() {
239 Ok(remote) => {
240 if !config.remote.github.is_set() {
241 tracing::debug!("No GitHub remote is set, using remote: {remote}");
242 config.remote.github.owner = remote.owner;
243 config.remote.github.repo = remote.repo;
244 config.remote.github.is_custom = remote.is_custom;
245 } else if !config.remote.gitlab.is_set() {
246 tracing::debug!("No GitLab remote is set, using remote: {remote}");
247 config.remote.gitlab.owner = remote.owner;
248 config.remote.gitlab.repo = remote.repo;
249 config.remote.gitlab.is_custom = remote.is_custom;
250 } else if !config.remote.gitea.is_set() {
251 tracing::debug!("No Gitea remote is set, using remote: {remote}");
252 config.remote.gitea.owner = remote.owner;
253 config.remote.gitea.repo = remote.repo;
254 config.remote.gitea.is_custom = remote.is_custom;
255 } else if !config.remote.bitbucket.is_set() {
256 tracing::debug!("No Bitbucket remote is set, using remote: {remote}");
257 config.remote.bitbucket.owner = remote.owner;
258 config.remote.bitbucket.repo = remote.repo;
259 config.remote.bitbucket.is_custom = remote.is_custom;
260 }
261 }
262 Err(e) => {
263 tracing::debug!("Failed to get remote from repository: {e:?}");
264 }
265 }
266 }
267 if args.use_native_tls {
268 config.remote.enable_native_tls();
269 }
270
271 tracing::trace!("Arguments: {args:#?}");
273 tracing::trace!("Config: {config:#?}");
274
275 let commit_range = determine_commit_range(args, config, repository)?;
277
278 let cwd = env::current_dir()?;
292 let mut include_path = config.git.include_paths.clone();
293 if let Ok(root) = repository.root_path() {
294 if cwd.starts_with(&root) &&
295 cwd != root &&
296 args.repository.as_ref().is_none_or(Vec::is_empty) &&
297 args.workdir.is_none() &&
298 include_path.is_empty()
299 {
300 let path = cwd.join("**").join("*");
301 if let Ok(stripped) = path.strip_prefix(root) {
302 tracing::info!(
303 "Including changes from the current directory: {}",
304 cwd.display()
305 );
306 include_path = vec![Pattern::new(stripped.to_string_lossy().as_ref())?];
307 }
308 }
309 }
310
311 let include_path = (!include_path.is_empty()).then_some(include_path);
312 let exclude_path =
313 (!config.git.exclude_paths.is_empty()).then_some(config.git.exclude_paths.clone());
314 let mut commits = repository.commits(
315 commit_range.as_deref(),
316 include_path,
317 exclude_path,
318 config.git.topo_order_commits,
319 )?;
320 if let Some(commit_limit_value) = config.git.limit_commits {
321 commits.truncate(commit_limit_value);
322 }
323
324 let mut releases = vec![Release::default()];
326 let mut tag_timestamp = None;
327 if let Some(ref tag) = args.tag {
328 if let Some(commit_id) = commits.first().map(|c| c.id().to_string()) {
329 match tags.get(&commit_id) {
330 Some(tag) => {
331 tracing::warn!("There is already a tag ({}) for {}", tag.name, commit_id);
332 tag_timestamp = Some(commits[0].time().seconds());
333 }
334 None => {
335 tags.insert(commit_id, repository.resolve_tag(tag));
336 }
337 }
338 } else {
339 releases[0].version = Some(tag.clone());
340 releases[0].timestamp = Some(
341 SystemTime::now()
342 .duration_since(UNIX_EPOCH)?
343 .as_secs()
344 .try_into()?,
345 );
346 }
347 }
348
349 let mut previous_release = Release::default();
351 let mut first_processed_tag = None;
352 let repository_path = repository.root_path()?.to_string_lossy().into_owned();
353 for git_commit in commits.iter().rev() {
354 let release = releases.last_mut().unwrap();
355 let mut commit = Commit::from(git_commit);
356 commit.statistics = repository.commit_statistics(git_commit)?;
357 let commit_id = commit.id.clone();
358 release.commits.push(commit);
359 release.repository = Some(repository_path.clone());
360 release.commit_id = Some(commit_id);
361 if let Some(tag) = tags.get(release.commit_id.as_ref().unwrap()) {
362 release.version = Some(tag.name.clone());
363 release.message.clone_from(&tag.message);
364 release.timestamp = if args.tag.as_deref() == Some(tag.name.as_str()) {
365 match tag_timestamp {
366 Some(timestamp) => Some(timestamp),
367 None => Some(
368 SystemTime::now()
369 .duration_since(UNIX_EPOCH)?
370 .as_secs()
371 .try_into()?,
372 ),
373 }
374 } else {
375 Some(git_commit.time().seconds())
376 };
377 if first_processed_tag.is_none() {
378 first_processed_tag = Some(tag);
379 }
380 previous_release.previous = None;
381 release.previous = Some(Box::new(previous_release));
382 previous_release = release.clone();
383 releases.push(Release::default());
384 }
385 }
386
387 debug_assert!(!releases.is_empty());
388
389 if releases.len() > 1 {
390 previous_release.previous = None;
391 releases.last_mut().unwrap().previous = Some(Box::new(previous_release));
392 }
393
394 if args.sort == Sort::Newest {
395 for release in &mut releases {
396 release.commits.reverse();
397 }
398 }
399
400 if let Some(custom_commits) = &args.with_commit {
402 releases
403 .last_mut()
404 .unwrap()
405 .commits
406 .extend(custom_commits.iter().cloned().map(Commit::from));
407 }
408
409 if releases[0]
411 .previous
412 .as_ref()
413 .and_then(|p| p.version.as_ref())
414 .is_none()
415 {
416 let first_tag = first_processed_tag
418 .map(|tag| {
419 tags.iter()
420 .enumerate()
421 .find(|(_, (_, v))| v.name == tag.name)
422 .and_then(|(i, _)| i.checked_sub(1))
423 .and_then(|i| tags.get_index(i))
424 })
425 .or_else(|| Some(tags.last()))
426 .flatten();
427
428 if let Some((commit_id, tag)) = first_tag {
430 let previous_release = Release {
431 commit_id: Some(commit_id.clone()),
432 version: Some(tag.name.clone()),
433 timestamp: Some(
434 repository
435 .find_commit(commit_id)
436 .map(|v| v.time().seconds())
437 .unwrap_or_default(),
438 ),
439 ..Default::default()
440 };
441 releases[0].previous = Some(Box::new(previous_release));
442 }
443 }
444
445 for release in &mut releases {
446 if !release.commits.is_empty() {
448 release.commit_range = Some(match args.sort {
449 Sort::Oldest => Range::new(
450 release.commits.first().unwrap(),
451 release.commits.last().unwrap(),
452 ),
453 Sort::Newest => Range::new(
454 release.commits.last().unwrap(),
455 release.commits.first().unwrap(),
456 ),
457 });
458 }
459 if recurse_submodules {
460 process_submodules(repository, release, config.git.topo_order_commits)?;
461 }
462 }
463
464 if let Some(message) = &args.with_tag_message {
466 if let Some(latest_release) = releases
467 .iter_mut()
468 .rfind(|release| !release.commits.is_empty())
469 {
470 latest_release.message = Some(message.to_owned());
471 }
472 }
473
474 Ok(releases)
475}
476
477pub fn run<'a>(args: Opt) -> Result<Changelog<'a>> {
493 run_with_changelog_modifier(args, |_| Ok(()))
494}
495
496pub fn run_with_changelog_modifier<'a>(
520 mut args: Opt,
521 changelog_modifier: impl FnOnce(&mut Changelog) -> Result<()>,
522) -> Result<Changelog<'a>> {
523 let builtin_config = BuiltinConfig::parse(args.config.to_string_lossy().to_string());
525
526 if let Some(ref workdir) = args.workdir {
528 args.config = workdir.join(args.config);
529 match args.repository.as_mut() {
530 Some(repository) => {
531 repository
532 .iter_mut()
533 .for_each(|r| *r = workdir.join(r.clone()));
534 }
535 None => args.repository = Some(vec![workdir.clone()]),
536 }
537 if let Some(changelog) = args.prepend {
538 args.prepend = Some(workdir.join(changelog));
539 }
540 args.include_path = Some(vec![Pattern::new(
543 workdir.join("").to_string_lossy().as_ref(),
544 )?]);
545 }
546
547 let mut path = args.config.clone();
549 if !path.exists() {
550 if let Some(config_path) = Config::retrieve_user_config_path() {
551 path = config_path;
552 }
553 }
554
555 let mut config = if let Some(url) = &args.config_url {
558 tracing::debug!("Using configuration file from: {url}");
559 #[cfg(feature = "remote")]
560 {
561 reqwest::blocking::get(url.clone())?
562 .error_for_status()?
563 .text()?
564 .parse()?
565 }
566 #[cfg(not(feature = "remote"))]
567 unreachable!("This option is not available without the 'remote' build-time feature");
568 } else if let Ok((config, name)) = builtin_config {
569 tracing::info!("Using built-in configuration file: {name}");
570 config
571 } else if path.exists() {
572 Config::load(&path)?
573 } else if let Some(contents) = Config::read_from_manifest()? {
574 contents.parse()?
575 } else if let Some(discovered_path) = env::current_dir()?
576 .ancestors()
577 .find_map(Config::retrieve_project_config_path)
578 {
579 tracing::info!(
580 "Using configuration from parent directory: {}",
581 discovered_path.display()
582 );
583 Config::load(&discovered_path)?
584 } else {
585 #[allow(clippy::unnecessary_debug_formatting)]
586 if !args.context {
587 tracing::warn!(
588 "{:?} is not found, using the default configuration",
589 args.config
590 );
591 }
592 EmbeddedConfig::parse()?
593 };
594
595 let output = args.output.clone().or(config.changelog.output.clone());
597 match args.strip {
598 Some(Strip::Header) => {
599 config.changelog.header = None;
600 }
601 Some(Strip::Footer) => {
602 config.changelog.footer = None;
603 }
604 Some(Strip::All) => {
605 config.changelog.header = None;
606 config.changelog.footer = None;
607 }
608 None => {}
609 }
610 if args.prepend.is_some() {
611 config.changelog.footer = None;
612 if !(args.unreleased || args.latest || args.range.is_some()) {
613 return Err(Error::ArgumentError(String::from(
614 "'-u' or '-l' is not specified",
615 )));
616 }
617 }
618 if output.is_some() && args.prepend.is_some() && output.as_ref() == args.prepend.as_ref() {
619 return Err(Error::ArgumentError(String::from(
620 "'-o' and '-p' can only be used together if they point to different files",
621 )));
622 }
623 if let Some(body) = args.body.clone() {
624 config.changelog.body = body;
625 }
626 if args.sort == Sort::Oldest {
627 args.sort = Sort::from_str(&config.git.sort_commits, true)
628 .expect("Incorrect config value for 'sort_commits'");
629 }
630 if !args.topo_order {
631 args.topo_order = config.git.topo_order;
632 }
633
634 if !args.use_branch_tags {
635 args.use_branch_tags = config.git.use_branch_tags;
636 }
637
638 if args.github_token.is_some() {
639 config.remote.github.token.clone_from(&args.github_token);
640 }
641 if args.gitlab_token.is_some() {
642 config.remote.gitlab.token.clone_from(&args.gitlab_token);
643 }
644 if args.gitea_token.is_some() {
645 config.remote.gitea.token.clone_from(&args.gitea_token);
646 }
647 if args.bitbucket_token.is_some() {
648 config
649 .remote
650 .bitbucket
651 .token
652 .clone_from(&args.bitbucket_token);
653 }
654 if args.azure_devops_token.is_some() {
655 config
656 .remote
657 .azure_devops
658 .token
659 .clone_from(&args.azure_devops_token);
660 }
661 if args.offline {
662 config.remote.offline = args.offline;
663 }
664 if let Some(ref remote) = args.github_repo {
665 config.remote.github.owner.clone_from(&remote.0.owner);
666 config.remote.github.repo.clone_from(&remote.0.repo);
667 config.remote.github.is_custom = true;
668 }
669 if let Some(ref remote) = args.gitlab_repo {
670 config.remote.gitlab.owner.clone_from(&remote.0.owner);
671 config.remote.gitlab.repo.clone_from(&remote.0.repo);
672 config.remote.gitlab.is_custom = true;
673 }
674 if let Some(ref remote) = args.bitbucket_repo {
675 config.remote.bitbucket.owner.clone_from(&remote.0.owner);
676 config.remote.bitbucket.repo.clone_from(&remote.0.repo);
677 config.remote.bitbucket.is_custom = true;
678 }
679 if let Some(ref remote) = args.gitea_repo {
680 config.remote.gitea.owner.clone_from(&remote.0.owner);
681 config.remote.gitea.repo.clone_from(&remote.0.repo);
682 config.remote.gitea.is_custom = true;
683 }
684 if let Some(ref remote) = args.azure_devops_repo {
685 config.remote.azure_devops.owner.clone_from(&remote.0.owner);
686 config.remote.azure_devops.repo.clone_from(&remote.0.repo);
687 config.remote.azure_devops.is_custom = true;
688 }
689 if args.no_exec {
690 config
691 .git
692 .commit_preprocessors
693 .iter_mut()
694 .for_each(|v| v.replace_command = None);
695 config
696 .changelog
697 .postprocessors
698 .iter_mut()
699 .for_each(|v| v.replace_command = None);
700 }
701 if args.skip_tags.is_some() {
702 config.git.skip_tags.clone_from(&args.skip_tags);
703 }
704 config.git.skip_tags = config.git.skip_tags.filter(|r| !r.as_str().is_empty());
705 if args.tag_pattern.is_some() {
706 config.git.tag_pattern.clone_from(&args.tag_pattern);
707 }
708 if args.tag.is_some() {
709 config.bump.initial_tag.clone_from(&args.tag);
710 }
711 if args.ignore_tags.is_some() {
712 config.git.ignore_tags.clone_from(&args.ignore_tags);
713 }
714 if args.count_tags.is_some() {
715 config.git.count_tags.clone_from(&args.count_tags);
716 }
717 if let Some(include_path) = &args.include_path {
718 config
719 .git
720 .include_paths
721 .extend(include_path.iter().cloned());
722 }
723 if let Some(exclude_path) = &args.exclude_path {
724 config
725 .git
726 .exclude_paths
727 .extend(exclude_path.iter().cloned());
728 }
729
730 if let Some(BumpOption::Specific(bump_type)) = args.bump {
732 config.bump.bump_type = Some(bump_type);
733 }
734
735 let mut changelog: Changelog = if let Some(context_path) = args.from_context {
737 let mut input: Box<dyn io::Read> = if context_path == Path::new("-") {
738 Box::new(io::stdin())
739 } else {
740 Box::new(File::open(context_path)?)
741 };
742 let mut changelog = Changelog::from_context(&mut input, config)?;
743 changelog.add_remote_context()?;
744 changelog
745 } else {
746 let repositories: Vec<Repository> = if let Some(paths) = &args.repository {
748 paths
749 .iter()
750 .map(|p| {
751 let abs_path = fs::canonicalize(p)?;
752 Repository::discover(abs_path)
753 })
754 .collect::<Result<Vec<_>>>()?
755 } else {
756 let cwd = env::current_dir()?;
757 vec![Repository::discover(cwd)?]
758 };
759 let mut releases = Vec::<Release>::new();
760 let mut commit_range = None;
761 for repository in repositories {
762 let mut skip_list = Vec::new();
764 let ignore_file = repository.root_path()?.join(IGNORE_FILE);
765 if ignore_file.exists() {
766 let contents = fs::read_to_string(ignore_file)?;
767 let commits = contents
768 .lines()
769 .filter(|v| !(v.starts_with('#') || v.trim().is_empty()))
770 .map(|v| String::from(v.trim()))
771 .collect::<Vec<String>>();
772 skip_list.extend(commits);
773 }
774 if let Some(ref skip_commit) = args.skip_commit {
775 skip_list.extend(skip_commit.clone());
776 }
777 for sha1 in skip_list {
778 config.git.commit_parsers.insert(0, CommitParser {
779 sha: Some(sha1.clone()),
780 skip: Some(true),
781 ..Default::default()
782 });
783 }
784
785 commit_range = determine_commit_range(&args, &config, &repository)?;
790
791 releases.extend(process_repository(
792 Box::leak(Box::new(repository)),
793 &mut config,
794 &args,
795 )?);
796 }
797 Changelog::new(releases, config, commit_range.as_deref())?
798 };
799 changelog_modifier(&mut changelog)?;
800
801 Ok(changelog)
802}
803
804pub fn write_changelog<W: io::Write>(
806 args: &Opt,
807 mut changelog: Changelog<'_>,
808 mut out: W,
809) -> Result<()> {
810 let output = args
811 .output
812 .clone()
813 .or(changelog.config.changelog.output.clone());
814 if args.bump.is_some() || args.bumped_version {
815 let current_version = changelog.releases.first().and_then(|release| {
816 release.version.clone().or_else(|| {
817 release
818 .previous
819 .as_ref()
820 .and_then(|previous| previous.version.clone())
821 })
822 });
823 let next_version = if let Some(next_version) = changelog.bump_version()? {
824 if current_version.as_ref() == Some(&next_version) {
825 tracing::warn!(
826 "The next version is the same as the current version, there is nothing to bump"
827 );
828 }
829 next_version
830 } else if let Some(last_version) =
831 changelog.releases.first().cloned().and_then(|v| v.version)
832 {
833 tracing::warn!("There is nothing to bump");
834 last_version
835 } else if changelog.releases.is_empty() {
836 changelog.config.bump.get_initial_tag()
837 } else {
838 return Ok(());
839 };
840 if let Some(tag_pattern) = &changelog.config.git.tag_pattern {
841 if !tag_pattern.is_match(&next_version) {
842 return Err(Error::ChangelogError(format!(
843 "Next version ({next_version}) does not match the tag pattern: {tag_pattern}",
844 )));
845 }
846 }
847 if args.bumped_version {
848 if changelog.config.changelog.output.is_none() {
849 writeln!(out, "{next_version}")?;
850 } else {
851 writeln!(io::stdout(), "{next_version}")?;
852 }
853 return Ok(());
854 }
855 }
856 if args.context {
857 changelog.write_context(&mut out)?;
858 return Ok(());
859 }
860 if let Some(path) = &args.prepend {
861 let changelog_before = fs::read_to_string(path)?;
862 let mut out = io::BufWriter::new(File::create(path)?);
863 changelog.prepend(changelog_before, &mut out)?;
864 }
865 if output.is_some() || args.prepend.is_none() {
866 changelog.generate(&mut out)?;
867 }
868
869 Ok(())
870}