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 log::info!("A new version of {pkg_name} is available: v{pkg_version} -> {new_version}",);
41 }
42 }
43}
44
45fn determine_commit_range(
50 args: &Opt,
51 config: &Config,
52 repository: &Repository,
53) -> Result<Option<String>> {
54 let tags = repository.tags(
55 &config.git.tag_pattern,
56 args.topo_order,
57 args.use_branch_tags,
58 )?;
59
60 let mut commit_range = args.range.clone();
61 if args.unreleased {
62 if let Some(last_tag) = tags.last().map(|(k, _)| k) {
63 commit_range = Some(format!("{last_tag}..HEAD"));
64 }
65 } else if args.latest || args.current {
66 if tags.len() < 2 {
67 let commits = repository.commits(None, None, None, config.git.topo_order_commits)?;
68 if let (Some(tag1), Some(tag2)) = (
69 commits.last().map(|c| c.id().to_string()),
70 tags.get_index(0).map(|(k, _)| k),
71 ) {
72 if tags.len() == 1 {
73 commit_range = Some(tag2.to_owned());
74 } else {
75 commit_range = Some(format!("{tag1}..{tag2}"));
76 }
77 }
78 } else {
79 let mut tag_index = tags.len() - 2;
80 if args.current {
81 if let Some(current_tag_index) = repository.current_tag().as_ref().and_then(|tag| {
82 tags.iter()
83 .enumerate()
84 .find(|(_, (_, v))| v.name == tag.name)
85 .map(|(i, _)| i)
86 }) {
87 match current_tag_index.checked_sub(1) {
88 Some(i) => tag_index = i,
89 None => {
90 return Err(Error::ChangelogError(String::from(
91 "No suitable tags found. Maybe run with '--topo-order'?",
92 )));
93 }
94 }
95 } else {
96 return Err(Error::ChangelogError(String::from(
97 "No tag exists for the current commit",
98 )));
99 }
100 }
101 if let (Some(tag1), Some(tag2)) = (
102 tags.get_index(tag_index).map(|(k, _)| k),
103 tags.get_index(tag_index + 1).map(|(k, _)| k),
104 ) {
105 commit_range = Some(format!("{tag1}..{tag2}"));
106 }
107 }
108 }
109
110 Ok(commit_range)
111}
112
113fn process_submodules(
115 repository: &'static Repository,
116 release: &mut Release,
117 topo_order_commits: bool,
118) -> Result<()> {
119 let first_commit = release
121 .previous
122 .as_ref()
123 .and_then(|previous_release| previous_release.commit_id.clone())
124 .and_then(|commit_id| repository.find_commit(&commit_id));
125 let last_commit = release
126 .commit_id
127 .clone()
128 .and_then(|commit_id| repository.find_commit(&commit_id));
129
130 log::debug!("Processing submodule commits in {first_commit:?}..{last_commit:?}");
131
132 if let Some(last_commit) = last_commit {
136 let submodule_ranges = repository.submodules_range(first_commit, last_commit)?;
137 let submodule_commits = submodule_ranges.iter().filter_map(|submodule_range| {
138 let SubmoduleRange {
141 repository: sub_repo,
142 range: range_str,
143 } = submodule_range;
144 let commits = sub_repo
145 .commits(Some(range_str), None, None, topo_order_commits)
146 .ok()
147 .map(|commits| commits.iter().map(Commit::from).collect());
148
149 let submodule_path = sub_repo.path().to_string_lossy().into_owned();
150 Some(submodule_path).zip(commits)
151 });
152 for (submodule_path, commits) in submodule_commits {
154 release.submodule_commits.insert(submodule_path, commits);
155 }
156 }
157 Ok(())
158}
159
160pub fn init_config(name: Option<&str>, config_path: &Path) -> Result<()> {
162 let contents = match name {
163 Some(name) => BuiltinConfig::get_config(name.to_string())?,
164 None => EmbeddedConfig::get_config()?,
165 };
166
167 let config_path = if config_path == Path::new(DEFAULT_CONFIG) {
168 PathBuf::from(DEFAULT_CONFIG)
169 } else {
170 config_path.to_path_buf()
171 };
172
173 log::info!(
174 "Saving the configuration file{} to {:?}",
175 name.map(|v| format!(" ({v})")).unwrap_or_default(),
176 config_path
177 );
178
179 fs::write(config_path, contents)?;
180
181 Ok(())
182}
183
184fn process_repository<'a>(
190 repository: &'static Repository,
191 config: &mut Config,
192 args: &Opt,
193) -> Result<Vec<Release<'a>>> {
194 let mut tags = repository.tags(
195 &config.git.tag_pattern,
196 args.topo_order,
197 args.use_branch_tags,
198 )?;
199 let skip_regex = config.git.skip_tags.as_ref();
200 let ignore_regex = config.git.ignore_tags.as_ref();
201 let count_tags = config.git.count_tags.as_ref();
202 let recurse_submodules = config.git.recurse_submodules.unwrap_or(false);
203 tags.retain(|_, tag| {
204 let name = &tag.name;
205
206 let skip = skip_regex.is_some_and(|r| r.is_match(name));
208 if skip {
209 return true;
210 }
211
212 let count = count_tags.is_none_or(|r| {
213 let count_tag = r.is_match(name);
214 if count_tag {
215 log::debug!("Counting release: {name}");
216 }
217 count_tag
218 });
219
220 let ignore = ignore_regex.is_some_and(|r| {
221 if r.as_str().trim().is_empty() {
222 return false;
223 }
224
225 let ignore_tag = r.is_match(name);
226 if ignore_tag {
227 log::debug!("Ignoring release: {name}");
228 }
229 ignore_tag
230 });
231
232 count && !ignore
233 });
234
235 if !config.remote.is_any_set() {
236 match repository.upstream_remote() {
237 Ok(remote) => {
238 if !config.remote.github.is_set() {
239 log::debug!("No GitHub remote is set, using remote: {remote}");
240 config.remote.github.owner = remote.owner;
241 config.remote.github.repo = remote.repo;
242 config.remote.github.is_custom = remote.is_custom;
243 } else if !config.remote.gitlab.is_set() {
244 log::debug!("No GitLab remote is set, using remote: {remote}");
245 config.remote.gitlab.owner = remote.owner;
246 config.remote.gitlab.repo = remote.repo;
247 config.remote.gitlab.is_custom = remote.is_custom;
248 } else if !config.remote.gitea.is_set() {
249 log::debug!("No Gitea remote is set, using remote: {remote}");
250 config.remote.gitea.owner = remote.owner;
251 config.remote.gitea.repo = remote.repo;
252 config.remote.gitea.is_custom = remote.is_custom;
253 } else if !config.remote.bitbucket.is_set() {
254 log::debug!("No Bitbucket remote is set, using remote: {remote}");
255 config.remote.bitbucket.owner = remote.owner;
256 config.remote.bitbucket.repo = remote.repo;
257 config.remote.bitbucket.is_custom = remote.is_custom;
258 }
259 }
260 Err(e) => {
261 log::debug!("Failed to get remote from repository: {e:?}");
262 }
263 }
264 }
265 if args.use_native_tls {
266 config.remote.enable_native_tls();
267 }
268
269 log::trace!("Arguments: {args:#?}");
271 log::trace!("Config: {config:#?}");
272
273 let commit_range = determine_commit_range(args, config, repository)?;
275
276 let cwd = env::current_dir()?;
290 let mut include_path = config.git.include_paths.clone();
291 if let Ok(root) = repository.root_path() {
292 if cwd.starts_with(&root) &&
293 cwd != root &&
294 args.repository.as_ref().is_none_or(Vec::is_empty) &&
295 args.workdir.is_none() &&
296 include_path.is_empty()
297 {
298 let path = cwd.join("**").join("*");
299 if let Ok(stripped) = path.strip_prefix(root) {
300 log::info!(
301 "Including changes from the current directory: {}",
302 cwd.display()
303 );
304 include_path = vec![Pattern::new(stripped.to_string_lossy().as_ref())?];
305 }
306 }
307 }
308
309 let include_path = (!include_path.is_empty()).then_some(include_path);
310 let exclude_path =
311 (!config.git.exclude_paths.is_empty()).then_some(config.git.exclude_paths.clone());
312 let mut commits = repository.commits(
313 commit_range.as_deref(),
314 include_path,
315 exclude_path,
316 config.git.topo_order_commits,
317 )?;
318 if let Some(commit_limit_value) = config.git.limit_commits {
319 commits.truncate(commit_limit_value);
320 }
321
322 let mut releases = vec![Release::default()];
324 let mut tag_timestamp = None;
325 if let Some(ref tag) = args.tag {
326 if let Some(commit_id) = commits.first().map(|c| c.id().to_string()) {
327 match tags.get(&commit_id) {
328 Some(tag) => {
329 log::warn!("There is already a tag ({}) for {}", tag.name, commit_id);
330 tag_timestamp = Some(commits[0].time().seconds());
331 }
332 None => {
333 tags.insert(commit_id, repository.resolve_tag(tag));
334 }
335 }
336 } else {
337 releases[0].version = Some(tag.clone());
338 releases[0].timestamp = Some(
339 SystemTime::now()
340 .duration_since(UNIX_EPOCH)?
341 .as_secs()
342 .try_into()?,
343 );
344 }
345 }
346
347 let mut previous_release = Release::default();
349 let mut first_processed_tag = None;
350 let repository_path = repository.root_path()?.to_string_lossy().into_owned();
351 for git_commit in commits.iter().rev() {
352 let release = releases.last_mut().unwrap();
353 let commit = Commit::from(git_commit);
354 let commit_id = commit.id.clone();
355 release.commits.push(commit);
356 release.repository = Some(repository_path.clone());
357 release.commit_id = Some(commit_id);
358 if let Some(tag) = tags.get(release.commit_id.as_ref().unwrap()) {
359 release.version = Some(tag.name.clone());
360 release.message.clone_from(&tag.message);
361 release.timestamp = if args.tag.as_deref() == Some(tag.name.as_str()) {
362 match tag_timestamp {
363 Some(timestamp) => Some(timestamp),
364 None => Some(
365 SystemTime::now()
366 .duration_since(UNIX_EPOCH)?
367 .as_secs()
368 .try_into()?,
369 ),
370 }
371 } else {
372 Some(git_commit.time().seconds())
373 };
374 if first_processed_tag.is_none() {
375 first_processed_tag = Some(tag);
376 }
377 previous_release.previous = None;
378 release.previous = Some(Box::new(previous_release));
379 previous_release = release.clone();
380 releases.push(Release::default());
381 }
382 }
383
384 debug_assert!(!releases.is_empty());
385
386 if releases.len() > 1 {
387 previous_release.previous = None;
388 releases.last_mut().unwrap().previous = Some(Box::new(previous_release));
389 }
390
391 if args.sort == Sort::Newest {
392 for release in &mut releases {
393 release.commits.reverse();
394 }
395 }
396
397 if let Some(custom_commits) = &args.with_commit {
399 releases
400 .last_mut()
401 .unwrap()
402 .commits
403 .extend(custom_commits.iter().cloned().map(Commit::from));
404 }
405
406 if releases[0]
408 .previous
409 .as_ref()
410 .and_then(|p| p.version.as_ref())
411 .is_none()
412 {
413 let first_tag = first_processed_tag
415 .map(|tag| {
416 tags.iter()
417 .enumerate()
418 .find(|(_, (_, v))| v.name == tag.name)
419 .and_then(|(i, _)| i.checked_sub(1))
420 .and_then(|i| tags.get_index(i))
421 })
422 .or_else(|| Some(tags.last()))
423 .flatten();
424
425 if let Some((commit_id, tag)) = first_tag {
427 let previous_release = Release {
428 commit_id: Some(commit_id.clone()),
429 version: Some(tag.name.clone()),
430 timestamp: Some(
431 repository
432 .find_commit(commit_id)
433 .map(|v| v.time().seconds())
434 .unwrap_or_default(),
435 ),
436 ..Default::default()
437 };
438 releases[0].previous = Some(Box::new(previous_release));
439 }
440 }
441
442 for release in &mut releases {
443 if !release.commits.is_empty() {
445 release.commit_range = Some(match args.sort {
446 Sort::Oldest => Range::new(
447 release.commits.first().unwrap(),
448 release.commits.last().unwrap(),
449 ),
450 Sort::Newest => Range::new(
451 release.commits.last().unwrap(),
452 release.commits.first().unwrap(),
453 ),
454 })
455 }
456 if recurse_submodules {
457 process_submodules(repository, release, config.git.topo_order_commits)?;
458 }
459 }
460
461 if let Some(message) = &args.with_tag_message {
463 if let Some(latest_release) = releases
464 .iter_mut()
465 .rfind(|release| !release.commits.is_empty())
466 {
467 latest_release.message = Some(message.to_owned());
468 }
469 }
470
471 Ok(releases)
472}
473
474pub fn run<'a>(args: Opt) -> Result<Changelog<'a>> {
490 run_with_changelog_modifier(args, |_| Ok(()))
491}
492
493pub fn run_with_changelog_modifier<'a>(
517 mut args: Opt,
518 changelog_modifier: impl FnOnce(&mut Changelog) -> Result<()>,
519) -> Result<Changelog<'a>> {
520 let builtin_config = BuiltinConfig::parse(args.config.to_string_lossy().to_string());
522
523 if let Some(ref workdir) = args.workdir {
525 args.config = workdir.join(args.config);
526 match args.repository.as_mut() {
527 Some(repository) => {
528 repository
529 .iter_mut()
530 .for_each(|r| *r = workdir.join(r.clone()));
531 }
532 None => args.repository = Some(vec![workdir.clone()]),
533 }
534 if let Some(changelog) = args.prepend {
535 args.prepend = Some(workdir.join(changelog));
536 }
537 args.include_path = Some(vec![Pattern::new(
540 workdir.join("").to_string_lossy().as_ref(),
541 )?]);
542 }
543
544 let mut path = args.config.clone();
546 if !path.exists() {
547 if let Some(config_path) = Config::retrieve_config_path() {
548 path = config_path;
549 }
550 }
551
552 let mut config = if let Some(url) = &args.config_url {
555 log::debug!("Using configuration file from: {url}");
556 #[cfg(feature = "remote")]
557 {
558 reqwest::blocking::get(url.clone())?
559 .error_for_status()?
560 .text()?
561 .parse()?
562 }
563 #[cfg(not(feature = "remote"))]
564 unreachable!("This option is not available without the 'remote' build-time feature");
565 } else if let Ok((config, name)) = builtin_config {
566 log::info!("Using built-in configuration file: {name}");
567 config
568 } else if path.exists() {
569 Config::load(&path)?
570 } else if let Some(contents) = Config::read_from_manifest()? {
571 contents.parse()?
572 } else if let Some(discovered_path) = env::current_dir()?.ancestors().find_map(|dir| {
573 let path = dir.join(DEFAULT_CONFIG);
574 if path.is_file() { Some(path) } else { None }
575 }) {
576 log::info!(
577 "Using configuration from parent directory: {}",
578 discovered_path.display()
579 );
580 Config::load(&discovered_path)?
581 } else {
582 if !args.context {
583 log::warn!(
584 "{:?} is not found, using the default configuration",
585 args.config
586 );
587 }
588 EmbeddedConfig::parse()?
589 };
590
591 let output = args.output.clone().or(config.changelog.output.clone());
593 match args.strip {
594 Some(Strip::Header) => {
595 config.changelog.header = None;
596 }
597 Some(Strip::Footer) => {
598 config.changelog.footer = None;
599 }
600 Some(Strip::All) => {
601 config.changelog.header = None;
602 config.changelog.footer = None;
603 }
604 None => {}
605 }
606 if args.prepend.is_some() {
607 config.changelog.footer = None;
608 if !(args.unreleased || args.latest || args.range.is_some()) {
609 return Err(Error::ArgumentError(String::from(
610 "'-u' or '-l' is not specified",
611 )));
612 }
613 }
614 if output.is_some() && args.prepend.is_some() && output.as_ref() == args.prepend.as_ref() {
615 return Err(Error::ArgumentError(String::from(
616 "'-o' and '-p' can only be used together if they point to different files",
617 )));
618 }
619 if let Some(body) = args.body.clone() {
620 config.changelog.body = body;
621 }
622 if args.sort == Sort::Oldest {
623 args.sort = Sort::from_str(&config.git.sort_commits, true)
624 .expect("Incorrect config value for 'sort_commits'");
625 }
626 if !args.topo_order {
627 args.topo_order = config.git.topo_order;
628 }
629
630 if !args.use_branch_tags {
631 args.use_branch_tags = config.git.use_branch_tags;
632 }
633
634 if args.github_token.is_some() {
635 config.remote.github.token.clone_from(&args.github_token);
636 }
637 if args.gitlab_token.is_some() {
638 config.remote.gitlab.token.clone_from(&args.gitlab_token);
639 }
640 if args.gitea_token.is_some() {
641 config.remote.gitea.token.clone_from(&args.gitea_token);
642 }
643 if args.bitbucket_token.is_some() {
644 config
645 .remote
646 .bitbucket
647 .token
648 .clone_from(&args.bitbucket_token);
649 }
650 if args.azure_devops_token.is_some() {
651 config
652 .remote
653 .azure_devops
654 .token
655 .clone_from(&args.azure_devops_token);
656 }
657 if args.offline {
658 config.remote.offline = args.offline;
659 }
660 if let Some(ref remote) = args.github_repo {
661 config.remote.github.owner = remote.0.owner.clone();
662 config.remote.github.repo = remote.0.repo.clone();
663 config.remote.github.is_custom = true;
664 }
665 if let Some(ref remote) = args.gitlab_repo {
666 config.remote.gitlab.owner = remote.0.owner.clone();
667 config.remote.gitlab.repo = remote.0.repo.clone();
668 config.remote.gitlab.is_custom = true;
669 }
670 if let Some(ref remote) = args.bitbucket_repo {
671 config.remote.bitbucket.owner = remote.0.owner.clone();
672 config.remote.bitbucket.repo = remote.0.repo.clone();
673 config.remote.bitbucket.is_custom = true;
674 }
675 if let Some(ref remote) = args.gitea_repo {
676 config.remote.gitea.owner = remote.0.owner.clone();
677 config.remote.gitea.repo = remote.0.repo.clone();
678 config.remote.gitea.is_custom = true;
679 }
680 if let Some(ref remote) = args.azure_devops_repo {
681 config.remote.azure_devops.owner = remote.0.owner.clone();
682 config.remote.azure_devops.repo = remote.0.repo.clone();
683 config.remote.azure_devops.is_custom = true;
684 }
685 if args.no_exec {
686 config
687 .git
688 .commit_preprocessors
689 .iter_mut()
690 .for_each(|v| v.replace_command = None);
691 config
692 .changelog
693 .postprocessors
694 .iter_mut()
695 .for_each(|v| v.replace_command = None);
696 }
697 if args.skip_tags.is_some() {
698 config.git.skip_tags.clone_from(&args.skip_tags);
699 }
700 config.git.skip_tags = config.git.skip_tags.filter(|r| !r.as_str().is_empty());
701 if args.tag_pattern.is_some() {
702 config.git.tag_pattern.clone_from(&args.tag_pattern);
703 }
704 if args.tag.is_some() {
705 config.bump.initial_tag.clone_from(&args.tag);
706 }
707 if args.ignore_tags.is_some() {
708 config.git.ignore_tags.clone_from(&args.ignore_tags);
709 }
710 if args.count_tags.is_some() {
711 config.git.count_tags.clone_from(&args.count_tags);
712 }
713 if let Some(include_path) = &args.include_path {
714 config
715 .git
716 .include_paths
717 .extend(include_path.iter().cloned());
718 }
719 if let Some(exclude_path) = &args.exclude_path {
720 config
721 .git
722 .exclude_paths
723 .extend(exclude_path.iter().cloned());
724 }
725
726 if let Some(BumpOption::Specific(bump_type)) = args.bump {
728 config.bump.bump_type = Some(bump_type);
729 }
730
731 let mut changelog: Changelog = if let Some(context_path) = args.from_context {
733 let mut input: Box<dyn io::Read> = if context_path == Path::new("-") {
734 Box::new(io::stdin())
735 } else {
736 Box::new(File::open(context_path)?)
737 };
738 let mut changelog = Changelog::from_context(&mut input, config)?;
739 changelog.add_remote_context()?;
740 changelog
741 } else {
742 let repositories: Vec<Repository> = if let Some(paths) = &args.repository {
744 paths
745 .iter()
746 .map(|p| {
747 let abs_path = fs::canonicalize(p)?;
748 Repository::discover(abs_path)
749 })
750 .collect::<Result<Vec<_>>>()?
751 } else {
752 let cwd = env::current_dir()?;
753 vec![Repository::discover(cwd)?]
754 };
755 let mut releases = Vec::<Release>::new();
756 let mut commit_range = None;
757 for repository in repositories {
758 let mut skip_list = Vec::new();
760 let ignore_file = repository.root_path()?.join(IGNORE_FILE);
761 if ignore_file.exists() {
762 let contents = fs::read_to_string(ignore_file)?;
763 let commits = contents
764 .lines()
765 .filter(|v| !(v.starts_with('#') || v.trim().is_empty()))
766 .map(|v| String::from(v.trim()))
767 .collect::<Vec<String>>();
768 skip_list.extend(commits);
769 }
770 if let Some(ref skip_commit) = args.skip_commit {
771 skip_list.extend(skip_commit.clone());
772 }
773 for sha1 in skip_list {
774 config.git.commit_parsers.insert(0, CommitParser {
775 sha: Some(sha1.clone()),
776 skip: Some(true),
777 ..Default::default()
778 });
779 }
780
781 commit_range = determine_commit_range(&args, &config, &repository)?;
786
787 releases.extend(process_repository(
788 Box::leak(Box::new(repository)),
789 &mut config,
790 &args,
791 )?);
792 }
793 Changelog::new(releases, config, commit_range.as_deref())?
794 };
795 changelog_modifier(&mut changelog)?;
796
797 Ok(changelog)
798}
799
800pub fn write_changelog<W: io::Write>(
802 args: &Opt,
803 mut changelog: Changelog<'_>,
804 mut out: W,
805) -> Result<()> {
806 let output = args
807 .output
808 .clone()
809 .or(changelog.config.changelog.output.clone());
810 if args.bump.is_some() || args.bumped_version {
811 let next_version = if let Some(next_version) = changelog.bump_version()? {
812 next_version
813 } else if let Some(last_version) =
814 changelog.releases.first().cloned().and_then(|v| v.version)
815 {
816 log::warn!("There is nothing to bump");
817 last_version
818 } else if changelog.releases.is_empty() {
819 changelog.config.bump.get_initial_tag()
820 } else {
821 return Ok(());
822 };
823 if let Some(tag_pattern) = &changelog.config.git.tag_pattern {
824 if !tag_pattern.is_match(&next_version) {
825 return Err(Error::ChangelogError(format!(
826 "Next version ({next_version}) does not match the tag pattern: {tag_pattern}",
827 )));
828 }
829 }
830 if args.bumped_version {
831 if changelog.config.changelog.output.is_none() {
832 writeln!(out, "{next_version}")?;
833 } else {
834 writeln!(io::stdout(), "{next_version}")?;
835 }
836 return Ok(());
837 }
838 }
839 if args.context {
840 changelog.write_context(&mut out)?;
841 return Ok(());
842 }
843 if let Some(path) = &args.prepend {
844 let changelog_before = fs::read_to_string(path)?;
845 let mut out = io::BufWriter::new(File::create(path)?);
846 changelog.prepend(changelog_before, &mut out)?;
847 }
848 if output.is_some() || args.prepend.is_none() {
849 changelog.generate(&mut out)?;
850 }
851
852 Ok(())
853}