main
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}