gitcc_cli/
release.rs

1//! `release` command
2
3use std::{env, fs, process::exit};
4
5use clap::Parser;
6use dialoguer::{theme::ColorfulTheme, Confirm};
7use gitcc_core::{ChangelogBuildOptions, Config, StatusShow, TEMPLATE_CHANGELOG_STD};
8
9use crate::{error, info, success, warn};
10
11/// Commit command arguments
12#[derive(Debug, Parser)]
13pub struct ReleaseArgs {
14    /// Dry run mode
15    #[arg(long)]
16    pub dry_run: bool,
17    /// Sets the next version to be v1.0.0
18    #[arg(long)]
19    pub v1: bool,
20}
21
22/// Executes the command `release`
23pub fn run(args: ReleaseArgs) -> anyhow::Result<()> {
24    // load the config
25    let cwd = env::current_dir()?;
26    let cfg_file = Config::load_from_fs(&cwd)?;
27    let cfg = if let Some(cfg) = cfg_file {
28        cfg
29    } else {
30        info!("using default config");
31        Config::default()
32    };
33
34    // make sure there is no untracked/uncommitted changes
35    let dirty_files = gitcc_core::git_status(&cwd, StatusShow::IndexAndWorkdir)?;
36    if !dirty_files.is_empty() {
37        warn!("repo is dirty");
38        match Confirm::with_theme(&ColorfulTheme::default())
39            .with_prompt("continue ?")
40            .report(true)
41            .default(false)
42            .interact()?
43        {
44            true => {}
45            false => {
46                exit(1);
47            }
48        }
49    }
50
51    // find the next version
52    let commit_history = gitcc_core::commit_history(&cwd, &cfg)?;
53    let mut next_version = commit_history.next_version_str();
54    if args.v1 {
55        warn!(format!("forcing 1st stable release"));
56        next_version = "v1.0.0".to_string();
57    }
58    info!(format!("next version: {}", next_version));
59
60    // before continuing, leave an escape hatch to set the version manually,
61    // or do other checks/tests manually.
62    info!("before committing, bump the packages manually, run tests, etc...");
63    match Confirm::with_theme(&ColorfulTheme::default())
64        .with_prompt("continue ?")
65        .report(true)
66        .default(false)
67        .interact()?
68    {
69        true => {}
70        false => {
71            exit(1);
72        }
73    }
74
75    // build the changelog
76    let changelog = gitcc_core::build_changelog(
77        &cwd,
78        &cfg,
79        &commit_history,
80        Some(ChangelogBuildOptions {
81            origin_name: None,
82            all: false,
83            next_version: Some(next_version.clone()),
84        }),
85    )?;
86    let changelog_str = match changelog.render(TEMPLATE_CHANGELOG_STD) {
87        Ok(s) => s,
88        Err(err) => {
89            error!(format!("failed to generate the changelog: {err}"));
90            exit(1);
91        }
92    };
93    if !args.dry_run {
94        let root_dir = gitcc_core::get_root_dir(&cwd).expect("not a git repo");
95        match fs::write(root_dir.join("CHANGELOG.md"), changelog_str) {
96            Ok(_ok) => {
97                success!("changelog written to file")
98            }
99            Err(err) => {
100                error!(format!("failed to write the changelog: {err}"));
101                exit(1);
102            }
103        }
104    } else {
105        info!("(dry-run) changelog not written to file")
106    }
107
108    // bump the packages versions
109    if !args.dry_run {
110        for cmd in cfg.release.bump_cmds {
111            match gitcc_core::exec_bump_command(&cmd, &next_version) {
112                Ok(_ok) => {
113                    success!(format!("executed bump command: {cmd}"));
114                }
115                Err(err) => {
116                    error!(format!("failed to bump packages: {err}"));
117                    exit(1);
118                }
119            }
120        }
121    } else {
122        info!("(dry-run) skipped executing bump commands")
123    }
124
125    // commit the changes
126    if !args.dry_run {
127        match gitcc_core::add_all_changes(&cwd) {
128            Ok(_ok) => {}
129            Err(err) => {
130                error!(format!("failed to add the changes: {err}"));
131                exit(1);
132            }
133        }
134        match gitcc_core::commit_changes(&cwd, &format!("chore(release): Release {next_version}")) {
135            Ok(_commit) => {
136                success!("commited changes");
137            }
138            Err(err) => {
139                error!(format!("failed to commit: {err}"));
140                exit(1);
141            }
142        }
143    } else {
144        info!("(dry-run) changes not committed");
145    }
146
147    // tagging the commit
148    if !args.dry_run {
149        match gitcc_core::set_annotated_tag(&cwd, &next_version, &format!("Release {next_version}"))
150        {
151            Ok(_ok) => {
152                success!(format!("tag {} added", next_version));
153            }
154            Err(err) => {
155                error!(format!("failed to add the changes: {err}"));
156                exit(1);
157            }
158        }
159    } else {
160        info!(format!("(dry-run) tag '{}' not set", next_version));
161    }
162
163    // Other steps
164    warn!("=> Push the changes with: git push --follow-tags");
165    warn!("=> Create the github release");
166    warn!("=> Publish the updated packages (crates.io, npm, brew, etc...)");
167
168    Ok(())
169}