diff --git a/CHANGELOG.md b/CHANGELOG.md index a2f75f63..92c2f064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the fast magnitude function in agb_fixnum. This is also used in fast_normalise. Previously only worked for positive (x, y). - Fixed formatting of fixed point numbers in the range (-1, 0), which previously appeared positive. -## Changed +### Changed - `testing` is now a default feature, so you no longer need to add a separate `dev-dependencies` line for `agb` in order to enable unit tests for your project. ## [0.11.1] - 2022/08/02 diff --git a/justfile b/justfile index 23837d70..201ced58 100644 --- a/justfile +++ b/justfile @@ -68,8 +68,11 @@ update-linker-scripts: publish: (_run-tool "publish") +release +args: (_run-tool "release" args) + _run-tool +tool: - cargo run --manifest-path "{{justfile_directory() + "/tools/Cargo.toml"}}" -- {{tool}} + (cd tools && cargo build) + "$CARGO_TARGET_DIR/debug/tools" {{tool}} _build-rom folder name: #!/usr/bin/env bash diff --git a/release.sh b/release.sh deleted file mode 100755 index a454e429..00000000 --- a/release.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# Fail if any command fails -set -e -set -x - -VERSION=$1 -NO_COMMIT=$2 - -# Sanity check that we actually have a version -if [ "$VERSION" = "" ]; then - echo "Usage $0 [--no-commit]" - exit 1 -fi - -# Check the format of version -if echo "$VERSION" | grep -q -Ev "^[0-9]+\.[0-9]+\.[0-9]+$"; then - echo "Version must be of the form x.y.z, got $VERSION" - exit 1 -fi - -# Check if no commit option is valid -if [ ! "$NO_COMMIT" = "" ] && [ ! "$NO_COMMIT" = "--no-commit" ]; then - echo "Must pass either no last argument or --no-commit" - exit 1 -fi - -function maybe_git() { - if [ "$NO_COMMIT" = "--no-commit" ]; then - echo "Would run: git $*" - else - git "$@" - fi -} - -# Check that no out-standing changes in git -if [ "$NO_COMMIT" = "" ] && [ -n "$(git status --porcelain)" ]; then - echo "Uncommitted changes, please commit first" - exit 1 -fi - -# Check that we are in the master branch, but only if actually committing -if [ ! "$NO_COMMIT" = "--no-commit" ] && [ "$(git symbolic-ref --short HEAD)" != "master" ]; then - echo "You must be in the master branch before releasing" - exit 1 -fi - -TAGNAME="v$VERSION" - -for PROJECT_TOML_FILE in agb/Cargo.toml agb-*/Cargo.toml; do - DIRECTORY=$(dirname "$PROJECT_TOML_FILE") - - # Update the version in Cargo.toml - sed -i -e "s/^version = \".*\"/version = \"$VERSION\"/" "$DIRECTORY/Cargo.toml" - - # Also update the lock file - (cd "$DIRECTORY" && cargo update) - - if [ "$DIRECTORY" = "agb" ]; then - # also update the agb version in the template and the examples - sed -i -e "s/^agb = \".*\"/agb = \"$VERSION\"/" template/Cargo.toml - - for EXAMPLE_TOML_FILE in examples/*/Cargo.toml book/games/*/Cargo.toml template/Cargo.toml; do - EXAMPLE_DIR=$(dirname "$EXAMPLE_TOML_FILE") - sed -E -i -e "/agb =/ s/version = \"[^\"]+\"/version = \"$VERSION\"/" "$EXAMPLE_DIR/Cargo.toml" - done - for EXAMPLE_TOML_FILE in examples/*/Cargo.toml book/games/*/Cargo.toml; do - EXAMPLE_DIR=$(dirname "$EXAMPLE_TOML_FILE") - (cd "$EXAMPLE_DIR" && cargo update) - done - else - PROJECT_NAME_WITH_UNDERSCORES=$(echo -n "$DIRECTORY" | tr - _) - - for CARGO_TOML_FILE in agb-*/Cargo.toml agb/Cargo.toml examples/*/Cargo.toml book/games/*/Cargo.toml; do - sed -i -E -e "s/($PROJECT_NAME_WITH_UNDERSCORES = .*version = \")[^\"]+(\".*)/\1$VERSION\2/" "$CARGO_TOML_FILE" - (cd "$(dirname "$CARGO_TOML_FILE")" && cargo generate-lockfile) - done - fi -done - -# Sanity check to make sure the build works -just ci - -for EXAMPLE_TOML_FILE in examples/*/Cargo.toml book/games/*/Cargo.toml; do - EXAMPLE_DIR=$(dirname "$EXAMPLE_TOML_FILE") - (cd "$EXAMPLE_DIR" && cargo check --release) -done - -# Commit the Cargo.toml changes -maybe_git commit -am "Release v$VERSION" - -# Tag the version -maybe_git tag -a "$TAGNAME" -m "v$VERSION" - -echo "Done! Push with" -echo "git push --atomic origin master $TAGNAME" diff --git a/tools/Cargo.lock b/tools/Cargo.lock index 9c1065ce..871dc847 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "atty" version = "0.2.14" @@ -25,12 +34,39 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + [[package]] name = "bytes" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + [[package]] name = "clap" version = "4.0.7" @@ -63,12 +99,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "hashbrown" version = "0.12.3" @@ -84,6 +132,19 @@ dependencies = [ "libc", ] +[[package]] +name = "iana-time-zone" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + [[package]] name = "indexmap" version = "1.9.1" @@ -103,30 +164,102 @@ dependencies = [ "either", ] +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" + [[package]] name = "os_str_bytes" version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "proc-macro2" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -136,6 +269,17 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "toml_edit" version = "0.14.4" @@ -151,10 +295,78 @@ dependencies = [ name = "tools" version = "0.1.0" dependencies = [ + "chrono", "clap", + "glob", "toml_edit", ] +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + [[package]] name = "winapi" version = "0.3.9" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 7c53cbf7..b0777d6c 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -8,3 +8,5 @@ edition = "2021" [dependencies] clap = "4" toml_edit = "0.14" +glob = "0.3" +chrono = "0.4" diff --git a/tools/src/main.rs b/tools/src/main.rs index 676c7388..79118229 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -2,19 +2,35 @@ use clap::Command; mod publish; +mod release; +mod utils; + +#[derive(Debug)] +pub enum Error { + PublishError(publish::Error), + ReleaseError(release::Error), +} fn cli() -> Command { Command::new("Agb tools") .subcommand_required(true) .arg_required_else_help(true) .subcommand(publish::command()) + .subcommand(release::command()) } fn main() { let matches = cli().get_matches(); let result = match matches.subcommand() { - Some(("publish", arg_matches)) => publish::publish(arg_matches), + Some(("publish", arg_matches)) => { + publish::publish(arg_matches).map_err(Error::PublishError) + } + + Some(("release", arg_matches)) => { + release::release(arg_matches).map_err(Error::ReleaseError) + } + _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), }; diff --git a/tools/src/publish.rs b/tools/src/publish.rs index 73a34d57..da637e1e 100644 --- a/tools/src/publish.rs +++ b/tools/src/publish.rs @@ -1,12 +1,14 @@ use clap::{Arg, ArgAction, ArgMatches}; use std::collections::{HashMap, HashSet}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; +use std::thread; use std::time::Duration; -use std::{env, thread}; use toml_edit::Document; +use crate::utils::*; + #[derive(Debug)] pub enum Error { FindRootDirectory, @@ -31,7 +33,7 @@ pub fn command() -> clap::Command { pub fn publish(matches: &ArgMatches) -> Result<(), Error> { let dry_run = matches.get_one::("Dry run").expect("defined by clap"); - let root_directory = find_agb_root_directory()?; + let root_directory = find_agb_root_directory().map_err(|_| Error::FindRootDirectory)?; let mut fully_published_crates: HashSet = HashSet::new(); let mut published_crates: HashSet = HashSet::new(); @@ -60,11 +62,12 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> { if *dry_run { println!("Would execute cargo publish for {publishable_crate}"); } else { - Command::new("cargo") + assert!(Command::new("cargo") .arg("publish") .current_dir(&root_directory.join(publishable_crate)) - .spawn() - .map_err(|_| Error::PublishCrate)?; + .status() + .map_err(|_| Error::PublishCrate)? + .success()); } published_crates.insert(publishable_crate.to_string()); @@ -86,19 +89,6 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> { Ok(()) } -fn find_agb_root_directory() -> Result { - let mut current_path = env::current_dir().map_err(|_| Error::FindRootDirectory)?; - - while !current_path.clone().join("justfile").exists() { - current_path = current_path - .parent() - .ok_or(Error::FindRootDirectory)? - .to_owned(); - } - - Ok(current_path) -} - fn check_if_released(crate_to_publish: &str, expected_version: &str) -> Result { let url_to_poll = &get_url_to_poll(crate_to_publish); @@ -220,16 +210,9 @@ mod test { } } - #[test] - fn should_find_root_directory() -> Result<(), Error> { - assert_ne!(find_agb_root_directory()?.to_string_lossy(), ""); - - Ok(()) - } - #[test] fn should_read_version() -> Result<(), Error> { - let root_directory = find_agb_root_directory()?; + let root_directory = crate::utils::find_agb_root_directory().unwrap(); let my_version = read_cargo_toml_version(&root_directory.join("tools"))?; assert_eq!(my_version, "0.1.0"); @@ -238,7 +221,7 @@ mod test { #[test] fn should_detect_dependencies() -> Result<(), Error> { - let root_directory = find_agb_root_directory()?; + let root_directory = crate::utils::find_agb_root_directory().unwrap(); let deps = get_agb_dependencies(&root_directory.join("agb"))?; assert_eq!( diff --git a/tools/src/release.rs b/tools/src/release.rs new file mode 100644 index 00000000..30ae3d2e --- /dev/null +++ b/tools/src/release.rs @@ -0,0 +1,305 @@ +use std::{path::Path, process::Command}; + +use crate::utils::find_agb_root_directory; + +pub fn command() -> clap::Command { + clap::Command::new("release") + .about("Prepares and commits the changes required to release agb") + .arg( + clap::Arg::new("version") + .required(true) + .help("New version to release") + .value_parser(version_parser), + ) + .arg( + clap::Arg::new("Dry run") + .long("dry-run") + .help("Don't do anything with git (but does everything else)") + .action(clap::ArgAction::SetTrue), + ) +} + +pub fn release(matches: &clap::ArgMatches) -> Result<(), Error> { + let dry_run = matches.get_one::("Dry run").expect("defined by clap"); + let version = matches + .get_one::("version") + .expect("defined by clap"); + + let root_directory = find_agb_root_directory().map_err(|_| Error::FindRootDirectory)?; + + // if not dry run, check that there are no out-standing changes in git + if !dry_run && !execute_git_command(&root_directory, &["status", "--porcelain"])?.is_empty() { + println!("Uncommitted changes, please commit first"); + return Ok(()); + } + + // Check that we are in the master branch + if !dry_run + && execute_git_command(&root_directory, &["symbolic-ref", "--short", "HEAD"])? != "master" + { + println!("You must be on the master branch before releasing"); + return Ok(()); + } + + let project_toml_files = glob_many(&root_directory, &["agb-*/Cargo.toml"])?; + let agb_cargo_toml = root_directory.join("agb/Cargo.toml"); + + update_to_version(&root_directory, &agb_cargo_toml, version)?; + + for toml_file in &project_toml_files { + update_to_version(&root_directory, toml_file, version)?; + } + + assert!(Command::new("just") + .arg("ci") + .current_dir(&root_directory) + .status() + .map_err(|_| Error::JustCiFailed)? + .success()); + + let changelog_text = update_changelog(&root_directory, version)?; + + println!("Content of changelog:\n{changelog_text}"); + + if !dry_run { + execute_git_command( + &root_directory, + &["commit", "-am", &format!("Release v{version}")], + )?; + execute_git_command( + &root_directory, + &[ + "tag", + "-a", + &version.to_string(), + "-m", + &format!("#v{version}\n{changelog_text}"), + ], + )?; + } + + println!("Done! Push with"); + println!("git push --atomic origin master v{version}"); + + Ok(()) +} + +fn update_to_version( + root_directory: &Path, + toml_file: &Path, + new_version: &Version, +) -> Result<(), Error> { + let directory_name = toml_file.parent().unwrap().file_name().unwrap(); + let project_name = directory_name.to_string_lossy().replace('-', "_"); + + let toml_file_content = std::fs::read_to_string(toml_file).map_err(|_| Error::ReadTomlFile)?; + let mut cargo_toml = toml_file_content + .parse::() + .map_err(|_| Error::InvalidToml(toml_file.to_string_lossy().into_owned()))?; + + let new_version = format!("{new_version}"); + cargo_toml["package"]["version"] = toml_edit::value(&new_version); + + std::fs::write(toml_file, cargo_toml.to_string()).map_err(|_| Error::WriteTomlFile)?; + + for cargo_toml_file in glob_many( + root_directory, + &[ + "agb-*/Cargo.toml", + "agb/Cargo.toml", + "examples/*/Cargo.toml", + "book/games/*/Cargo.toml", + "template/Cargo.toml", + ], + )? { + let toml_file_content = + std::fs::read_to_string(&cargo_toml_file).map_err(|_| Error::ReadTomlFile)?; + let mut cargo_toml = toml_file_content + .parse::() + .map_err(|_| Error::InvalidToml(cargo_toml_file.to_string_lossy().into_owned()))?; + + if let Some(this_dep) = cargo_toml["dependencies"].get_mut(&project_name) { + match this_dep { + toml_edit::Item::Value(s @ toml_edit::Value::String(_)) => { + *s = new_version.clone().into() + } + toml_edit::Item::Value(toml_edit::Value::InlineTable(t)) => { + t["version"] = new_version.clone().into() + } + toml_edit::Item::None => continue, + _ => { + return Err(Error::InvalidToml(format!( + "{:?} while seaching dependencies in {}", + this_dep, + cargo_toml_file.to_string_lossy() + ))) + } + } + } + + std::fs::write(cargo_toml_file, cargo_toml.to_string()) + .map_err(|_| Error::WriteTomlFile)?; + } + + Ok(()) +} + +fn update_changelog(root_directory: &Path, new_version: &Version) -> Result { + use chrono::Datelike; + + let changelog_file = root_directory.join("CHANGELOG.md"); + let changelog_content = + std::fs::read_to_string(&changelog_file).map_err(|_| Error::FailedToReadChangelog)?; + + let today = chrono::Local::today(); + let formatted_date = format!( + "{:04}/{:02}/{:02}", + today.year(), + today.month(), + today.day() + ); + + const UNRELEASED_HEADER: &str = "## [Unreleased]"; + + let unreleased_bit_start = changelog_content + .find(UNRELEASED_HEADER) + .ok_or(Error::FailedToParseChangelog)? + + UNRELEASED_HEADER.len(); + let unreleased_bit_end = changelog_content[unreleased_bit_start..] + .find("\n## [") // the start of the next entry + .ok_or(Error::FailedToParseChangelog)? + + unreleased_bit_start; + + let change_content = changelog_content[unreleased_bit_start..unreleased_bit_end].to_owned(); + + let changelog_content = changelog_content.replacen( + UNRELEASED_HEADER, + &format!("{UNRELEASED_HEADER}\n\n## [{new_version}] - {formatted_date}"), + 1, + ); + + std::fs::write(&changelog_file, &changelog_content) + .map_err(|_| Error::FailedToWriteChangelog)?; + + Ok(change_content) +} + +fn execute_git_command(root_directory: &Path, args: &[&str]) -> Result { + let git_cmd = Command::new("git") + .args(args) + .current_dir(root_directory) + .output() + .map_err(|_| Error::Git("Failed to run command"))?; + + assert!(git_cmd.status.success()); + + String::from_utf8(git_cmd.stdout).map_err(|_| Error::Git("Output not utf-8")) +} + +fn glob_many(root_directory: &Path, globs: &[&str]) -> Result, Error> { + let mut result = vec![]; + + for g in globs.iter() { + for path in glob::glob(&root_directory.join(g).to_string_lossy()).expect("Invalid glob") { + result.push(path.map_err(|_| Error::Glob)?); + } + } + + Ok(result) +} + +#[derive(Debug)] +pub enum Error { + FindRootDirectory, + Git(&'static str), + Glob, + ReadTomlFile, + InvalidToml(String), + WriteTomlFile, + JustCiFailed, + CargoUpdateFailed, + FailedToReadChangelog, + FailedToWriteChangelog, + FailedToParseChangelog, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Version { + major: u32, + minor: u32, + patch: u32, +} + +impl Version { + #[cfg(test)] + pub fn new(major: u32, minor: u32, patch: u32) -> Self { + Self { + major, + minor, + patch, + } + } +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +#[derive(Debug, PartialEq, Eq)] +struct ParseVersionError; + +impl std::str::FromStr for Version { + type Err = ParseVersionError; + + fn from_str(s: &str) -> Result { + let version_array: Vec<_> = s + .split('.') + .map(|v| v.parse()) + .collect::, _>>() + .map_err(|_| ParseVersionError)?; + + if version_array.len() > 3 || version_array.is_empty() { + return Err(ParseVersionError); + } + + Ok(Version { + major: version_array[0], + minor: *version_array.get(1).unwrap_or(&0), + patch: *version_array.get(2).unwrap_or(&0), + }) + } +} + +fn version_parser(maybe_version: &str) -> Result { + maybe_version + .parse() + .map_err(|_| "Failed to parse version, must be of the format x.y.z") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn verify_cli() { + command().debug_assert(); + } + + #[test] + fn can_parse_versions() { + assert_eq!(Version::from_str("0.1.2").unwrap(), Version::new(0, 1, 2)); + assert_eq!(Version::from_str("0.1").unwrap(), Version::new(0, 1, 0)); + assert_eq!( + Version::from_str("33.23.4000").unwrap(), + Version::new(33, 23, 4000) + ); + + assert_eq!(Version::from_str("abc").unwrap_err(), ParseVersionError); + assert_eq!(Version::from_str("").unwrap_err(), ParseVersionError); + assert_eq!(Version::from_str("0.2.4.5").unwrap_err(), ParseVersionError); + assert_eq!(Version::from_str("0.2.4a").unwrap_err(), ParseVersionError); + } +} diff --git a/tools/src/utils.rs b/tools/src/utils.rs new file mode 100644 index 00000000..a9759165 --- /dev/null +++ b/tools/src/utils.rs @@ -0,0 +1,27 @@ +use std::{env, path::PathBuf}; + +#[derive(Debug)] +pub struct FindRootDirectoryError; + +pub fn find_agb_root_directory() -> Result { + let mut current_path = env::current_dir().map_err(|_| FindRootDirectoryError)?; + + while !current_path.clone().join("justfile").exists() { + current_path = current_path + .parent() + .ok_or(FindRootDirectoryError)? + .to_owned(); + } + + Ok(current_path) +} + +#[cfg(test)] +mod tests { + use super::find_agb_root_directory; + + #[test] + fn find_agb_root_directory_works() { + find_agb_root_directory().unwrap(); + } +}