1mod summary;
  2
  3use crate::summary::Summary;
  4use anyhow::{bail, Context, Result};
  5use regex::Regex;
  6use std::io::Write;
  7use std::{env, fs, io, path::PathBuf};
  8
  9// TODO:
 10// - changelog management
 11// - weblate integration
 12// - git commits / PR-ing
 13// - release tagging / github publishing
 14
 15fn main() -> Result<()>{
 16    let mut summary = Summary::default();
 17
 18    summary.root = git_dir()
 19        .context("Couldn't find repository root")?;
 20    println!("Using repository at {:?}", summary.root);
 21
 22    let version = get_version(&summary.root)
 23        .context("Couldn't read current app version")?;
 24    println!("Current app version is {} ({})", &version.0, &version.1);
 25
 26    let bump_version = prompt_bool(format!("Bump app version ({}->{})?", version.1, version.1 + 1).as_str(), Some(true))?;
 27    if bump_version {
 28        print!("Version name (e.g. {}): ", version.0);
 29        io::stdout().flush()?;
 30        let mut buffer = String::new();
 31        io::stdin().read_line(&mut buffer)?;
 32        buffer = buffer.trim().to_string();
 33        let regex = Regex::new(r"(\d+).(\d+).(\d+)")?;
 34        if regex.is_match(&buffer) {
 35            summary.new_version_line = Some(format!("version: {buffer}+{}", version.1 + 1));
 36        } else {
 37            bail!("New version is malformed");
 38        }
 39    }
 40
 41    summary.update_flutter = prompt_bool("Update flutter?", Some(true))?;
 42    summary.update_dependencies = prompt_bool("Update dependencies?", Some(true))?;
 43    summary.run_tests = prompt_bool("Run tests?", Some(false))?;
 44    summary.build = prompt_bool("Build app?", Some(true))?;
 45
 46    summary.print();
 47    summary.apply();
 48
 49    Ok(())
 50}
 51
 52pub fn prompt_bool(prompt: &str, default: Option<bool>) -> Result<bool> {
 53    let y = if default.is_some_and(|d| d) { "Y" } else { "y" };
 54    let n = if default.is_some_and(|d| !d) { "N" } else { "n" };
 55    print!("{} [{}/{}] ", prompt, y, n);
 56    io::stdout().flush()?;
 57
 58    let mut buffer = String::new();
 59    io::stdin().read_line(&mut buffer)?;
 60    buffer = buffer.trim().to_string();
 61
 62    if buffer.eq_ignore_ascii_case("y") {
 63        Ok(true)
 64    } else if buffer.eq_ignore_ascii_case("n") {
 65        Ok(false)
 66    } else if let Some(default) = default {
 67        Ok(default)
 68    } else {
 69        bail!("Invalid input '{buffer}', please provide either 'y' or 'n'");
 70    }
 71}
 72
 73
 74/// Get the closest ancestor dir that contains a .git folder in order to find the repository root.
 75pub fn git_dir() -> Result<PathBuf> {
 76    let mut dir = env::current_dir()
 77        .context("no CWD")?;
 78
 79    loop {
 80        // find a child dir with matching name
 81        let child = dir.read_dir()?
 82            .find(|e| e.is_ok() && e.as_ref().unwrap().file_name()
 83                    .eq_ignore_ascii_case(".git"));
 84        if let Some(Ok(_)) = child {
 85            return Ok(dir);
 86        }
 87        if let Some(parent) = dir.parent() {
 88            dir = parent.to_path_buf();
 89        } else {
 90            bail!("Reached fs root")
 91        }
 92    }
 93}
 94
 95/// Read current app verison name and number from pubspec in `$root/app/pubspec.yaml`.
 96/// 
 97/// Example: ("1.8.4", 49)
 98pub fn get_version(root: &PathBuf) -> Result<(String, usize)> {
 99    let pubspec = root.join("app").join("pubspec.yaml");
100    let pubspec = fs::read_to_string(pubspec).context("Couldn't find pubspec.yaml")?;
101
102    // Matches the `version: ...+..` line of the file, capturing the name in 1 and the number in 2.
103    let regex =  Regex::new(r"version:\s*([0-9.]*)\+([0-9]*)")?;
104    let pubspec = regex.captures(&pubspec)
105        .context("Can't find app version declaration in pubspec.yaml")?;
106    
107    let version_name = pubspec.get(1).expect("implied by regex");
108    let version_num = pubspec.get(2).expect("implied by regex");
109    let parsed_version_num = version_num.as_str().parse::<usize>()
110        .context(format!("Extracted version string is: '{}'", version_num.as_str()))?;
111    Ok((version_name.as_str().to_string(), parsed_version_num))
112}