From 24059c489facfcfdbe29b9008c7f61e89a90a8e9 Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 5 Oct 2023 09:16:16 -0700 Subject: [PATCH 01/11] [detect-newline-style] add tests to cover no-op cases --- crates/detect-newline-style/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/detect-newline-style/src/lib.rs b/crates/detect-newline-style/src/lib.rs index 8a1923f..874fd4b 100644 --- a/crates/detect-newline-style/src/lib.rs +++ b/crates/detect-newline-style/src/lib.rs @@ -243,6 +243,20 @@ mod tests { assert_eq!(eol, LineEnding::LF); } + #[test] + fn it_uses_default_when_text_has_no_line_breaks() { + let input = "no line breaks"; + let eol = LineEnding::find(input, LineEnding::LF); + assert_eq!(eol, LineEnding::LF); + } + + #[test] + fn it_uses_default_when_text_is_empty() { + let input = ""; + let eol = LineEnding::find(input, LineEnding::LF); + assert_eq!(eol, LineEnding::LF); + } + #[test] fn it_finds_preferred_line_ending_defaulting_to_cr_endings() { let input = "\rthis\rprefers\r\nobsolete endings\n"; From f2bf7203fef567c9e6335e6a59ab2a630399e30a Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 5 Oct 2023 09:16:59 -0700 Subject: [PATCH 02/11] tune crate:list task output --- xtask/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 24bf7ba..5daa7a8 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -250,7 +250,8 @@ fn init_tasks() -> Tasks { let krates = workspace.krates()?; for krate in krates.values() { - println!("* {}: {} [{}]\n >> {}\n", krate.name, krate.description, krate.kind, krate.path.display()); + let kind = krate.kind.to_string().replace('-', ""); + println!("* {} [{}]\n ?? {}\n >> {}\n", krate.name, kind, krate.description, krate.path.display()); } println!(); From 913078ac8dca0545e7a77f4671b12b467eb3aa76 Mon Sep 17 00:00:00 2001 From: busticated Date: Sat, 7 Oct 2023 16:51:16 -0700 Subject: [PATCH 03/11] [node-js-release-info] add tests for serializing enums --- crates/node-js-release-info/src/arch.rs | 23 +++++++++++++++++++++++ crates/node-js-release-info/src/ext.rs | 19 +++++++++++++++++++ crates/node-js-release-info/src/os.rs | 15 +++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/crates/node-js-release-info/src/arch.rs b/crates/node-js-release-info/src/arch.rs index eb0ac8e..b919fef 100644 --- a/crates/node-js-release-info/src/arch.rs +++ b/crates/node-js-release-info/src/arch.rs @@ -108,6 +108,29 @@ mod tests { assert_eq!(arch, NodeJSArch::PPC64LE); } + #[test] + fn it_serializes_to_str() { + let text = format!("{}", NodeJSArch::X64); + + assert_eq!(text, "x64"); + + let text = format!("{}", NodeJSArch::X86); + + assert_eq!(text, "x86"); + + let text = format!("{}", NodeJSArch::ARM64); + + assert_eq!(text, "arm64"); + + let text = format!("{}", NodeJSArch::ARMV7L); + + assert_eq!(text, "armv7l"); + + let text = format!("{}", NodeJSArch::PPC64LE); + + assert_eq!(text, "ppc64le"); + } + #[test] fn it_initializes_using_current_environment() { NodeJSArch::from_env().unwrap(); diff --git a/crates/node-js-release-info/src/ext.rs b/crates/node-js-release-info/src/ext.rs index 9697d85..b1198ac 100644 --- a/crates/node-js-release-info/src/ext.rs +++ b/crates/node-js-release-info/src/ext.rs @@ -83,6 +83,25 @@ mod tests { assert_eq!(ext, NodeJSPkgExt::Msi); } + #[test] + fn it_serializes_to_str() { + let text = format!("{}", NodeJSPkgExt::Targz); + + assert_eq!(text, "tar.gz"); + + let text = format!("{}", NodeJSPkgExt::Tarxz); + + assert_eq!(text, "tar.xz"); + + let text = format!("{}", NodeJSPkgExt::Zip); + + assert_eq!(text, "zip"); + + let text = format!("{}", NodeJSPkgExt::Msi); + + assert_eq!(text, "msi"); + } + #[test] #[should_panic( expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedExt(\"NOPE!\")" diff --git a/crates/node-js-release-info/src/os.rs b/crates/node-js-release-info/src/os.rs index efcca36..98addc2 100644 --- a/crates/node-js-release-info/src/os.rs +++ b/crates/node-js-release-info/src/os.rs @@ -90,6 +90,21 @@ mod tests { assert_eq!(os, NodeJSOS::Windows); } + #[test] + fn it_serializes_to_str() { + let text = format!("{}", NodeJSOS::Linux); + + assert_eq!(text, "linux"); + + let text = format!("{}", NodeJSOS::Darwin); + + assert_eq!(text, "darwin"); + + let text = format!("{}", NodeJSOS::Windows); + + assert_eq!(text, "win"); + } + #[test] fn it_initializes_using_current_environment() { NodeJSOS::from_env().unwrap(); From 42a9b56bca5cf75013b4f572e712d9c961529d4d Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 12 Oct 2023 12:39:35 -0700 Subject: [PATCH 04/11] add version field to krate struct --- xtask/Cargo.toml | 1 + xtask/src/krate.rs | 62 ++++++++++++++++++++++++++++++++++++------ xtask/src/main.rs | 2 +- xtask/src/readme.rs | 2 +- xtask/src/toml.rs | 17 +++++++++++- xtask/src/workspace.rs | 5 ++-- 6 files changed, 76 insertions(+), 13 deletions(-) diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index d2d5c31..54882dc 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -9,4 +9,5 @@ edition = "2021" duct = "0.13.*" inquire = "0.6.*" regex = "1.*" +semver = "1.*" toml = "0.8.*" diff --git a/xtask/src/krate.rs b/xtask/src/krate.rs index fa8ddae..5bcdb91 100644 --- a/xtask/src/krate.rs +++ b/xtask/src/krate.rs @@ -5,6 +5,7 @@ use std::fmt::{Display, Formatter}; use std::fs; use std::path::PathBuf; use std::str::FromStr; +use semver::Version; type DynError = Box; @@ -13,11 +14,12 @@ const COVERAGE_DIRNAME: &str = "coverage"; const SRC_DIRNAME: &str = "src"; const LIB_FILENAME: &str = "lib.rs"; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Krate { + pub kind: KrateKind, + pub version: Version, pub name: String, pub description: String, - pub kind: KrateKind, pub path: PathBuf, pub readme: Readme, pub toml: Toml, @@ -29,20 +31,44 @@ impl KratePaths for Krate { } } +impl Default for Krate { + fn default() -> Self { + let kind = KrateKind::default(); + let version = Version::new(0, 1, 0); + let name = String::default(); + let description = String::default(); + let path = PathBuf::default(); + let readme = Readme::default(); + let toml = Toml::default(); + Krate { + kind, + version, + name, + description, + path, + readme, + toml, + } + } +} + impl Krate { - pub fn new, N: AsRef, D: AsRef>( + pub fn new, V: AsRef, N: AsRef, D: AsRef>( kind: K, + version: V, name: N, description: D, path: PathBuf, ) -> Self { let kind = KrateKind::new(kind.as_ref()); + let version = Version::parse(version.as_ref()).unwrap_or(Version::new(0, 1, 0)); let name = name.as_ref().to_owned(); let description = description.as_ref().to_owned(); let readme = Readme::new(path.clone()); let toml = Toml::new(path.clone()); Krate { kind, + version, name, description, path, @@ -55,16 +81,19 @@ impl Krate { let toml = Toml::from_path(path.clone())?; let readme = Readme::from_path(path.clone())?; let kind = KrateKind::from_path(path.clone())?; - let mut krate = Krate { + let name = toml.get_name()?; + let description = toml.get_description()?; + let version = toml.get_version()?; + let krate = Krate { kind, + version, + name, + description, path, - toml, readme, - ..Default::default() + toml, }; - krate.name = krate.toml.get_name()?; - krate.description = krate.toml.get_description()?; Ok(krate) } @@ -210,20 +239,35 @@ mod tests { fn it_initializes_a_krate() { let krate = Krate::new( "lib", + "1.0.0", "my-crate", "my-crate's description", PathBuf::from("fake-crate"), ); + assert_eq!(krate.kind, KrateKind::Library); + assert_eq!(krate.version, Version::new(1, 0, 0)); assert_eq!(krate.name, "my-crate"); assert_eq!(krate.description, "my-crate's description"); assert_eq!(krate.path, PathBuf::from("fake-crate")); + } + + #[test] + fn it_initializes_a_default_krate() { + let krate = Krate::default(); assert_eq!(krate.kind, KrateKind::Library); + assert_eq!(krate.version, Version::new(0, 1, 0)); + assert_eq!(krate.name, ""); + assert_eq!(krate.description, ""); + assert_eq!(krate.path, PathBuf::from("")); + assert_eq!(krate.readme.path, PathBuf::from("")); + assert_eq!(krate.toml.path, PathBuf::from("")); } #[test] fn it_gets_path_to_krate() { let krate = Krate::new( "lib", + "0.1.0", "my-crate", "my-crate's description", PathBuf::from("fake-crate"), @@ -235,6 +279,7 @@ mod tests { fn it_gets_path_to_krate_tmp_dir() { let krate = Krate::new( "lib", + "0.1.0", "my-crate", "my-crate's description", PathBuf::from("fake-crate"), @@ -246,6 +291,7 @@ mod tests { fn it_gets_path_to_krate_coverage_dir() { let krate = Krate::new( "lib", + "0.1.0", "my-crate", "my-crate's description", PathBuf::from("fake-crate"), diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 5daa7a8..77d14a9 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -229,7 +229,7 @@ fn init_tasks() -> Tasks { let question = InquireSelect::new("Crate type?", vec!["--lib", "--bin"]); let kind_flag = question.prompt()?; - workspace.add_krate(kind_flag, &name, &description)?; + workspace.add_krate(kind_flag, "0.1.0", &name, &description)?; println!(":::: Done!"); println!(); diff --git a/xtask/src/readme.rs b/xtask/src/readme.rs index 5eb8fba..f0e7542 100644 --- a/xtask/src/readme.rs +++ b/xtask/src/readme.rs @@ -11,8 +11,8 @@ const README_MD: &str = "README.md"; #[derive(Clone, Debug, Default, PartialEq)] pub struct Readme { - pub text: String, pub path: PathBuf, + text: String, } impl Readme { diff --git a/xtask/src/toml.rs b/xtask/src/toml.rs index 20a99a6..ddb6e24 100644 --- a/xtask/src/toml.rs +++ b/xtask/src/toml.rs @@ -2,6 +2,7 @@ use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; use toml::Table; +use semver::Version; type DynError = Box; @@ -9,7 +10,7 @@ const CARGO_TOML: &str = "Cargo.toml"; #[derive(Clone, Debug, Default, PartialEq)] pub struct Toml { - path: PathBuf, + pub path: PathBuf, data: Table, } @@ -66,6 +67,20 @@ impl Toml { lines.join("\n") } + pub fn get_version(&self) -> Result { + let pkg = self + .data + .get("package") + .ok_or(format_section_missing_msg("package", &self.path))?; + let version = pkg + .get("version") + .ok_or(format_field_missing_msg("version", &self.path))? + .as_str() + .ok_or(format_invalid_field_msg("version", &self.path))?; + + Ok(Version::parse(version)?) + } + pub fn get_name(&self) -> Result { let pkg = self .data diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs index d084e58..9d8b17e 100644 --- a/xtask/src/workspace.rs +++ b/xtask/src/workspace.rs @@ -70,14 +70,15 @@ impl Workspace { Ok(krates) } - pub fn add_krate, N: AsRef, D: AsRef>( + pub fn add_krate, V: AsRef, N: AsRef, D: AsRef>( &self, kind: K, + version: V, name: N, description: D, ) -> Result { let path = self.krates_path().join(name.as_ref()); - let krate = Krate::new(kind, name, description, path); + let krate = Krate::new(kind, version, name, description, path); cmd!( &self.cargo_cmd, From a63ca232570b574e44aab0a93630788e346cc144 Mon Sep 17 00:00:00 2001 From: busticated Date: Fri, 13 Oct 2023 10:24:47 -0700 Subject: [PATCH 05/11] switch to toml_edit dependency --- xtask/Cargo.toml | 3 ++- xtask/src/krate.rs | 2 +- xtask/src/toml.rs | 33 +++++++++++++++++++++++++++------ xtask/src/workspace.rs | 2 +- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 54882dc..fcaf193 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "xtask" +description = "internal-only crate used to orchestrate repo tasks" version = "0.1.0" edition = "2021" @@ -10,4 +11,4 @@ duct = "0.13.*" inquire = "0.6.*" regex = "1.*" semver = "1.*" -toml = "0.8.*" +toml_edit = "0.20.*" diff --git a/xtask/src/krate.rs b/xtask/src/krate.rs index 5bcdb91..938a34c 100644 --- a/xtask/src/krate.rs +++ b/xtask/src/krate.rs @@ -14,7 +14,7 @@ const COVERAGE_DIRNAME: &str = "coverage"; const SRC_DIRNAME: &str = "src"; const LIB_FILENAME: &str = "lib.rs"; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct Krate { pub kind: KrateKind, pub version: Version, diff --git a/xtask/src/toml.rs b/xtask/src/toml.rs index ddb6e24..0c9e715 100644 --- a/xtask/src/toml.rs +++ b/xtask/src/toml.rs @@ -1,17 +1,17 @@ use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; -use toml::Table; +use toml_edit::Document; use semver::Version; type DynError = Box; const CARGO_TOML: &str = "Cargo.toml"; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default)] pub struct Toml { pub path: PathBuf, - data: Table, + data: Document, } impl Toml { @@ -27,9 +27,9 @@ impl Toml { toml.load() } - pub fn read(&self) -> Result { - let data = fs::read_to_string(&self.path)?; - Ok(data.parse::()?) + pub fn read(&self) -> Result { + let text = fs::read_to_string(&self.path)?; + Ok(text.parse::()?) } pub fn load(&mut self) -> Result { @@ -168,4 +168,25 @@ mod tests { .join("\n") ); } + + #[test] + fn it_gets_version_field() { + let fake_crate_root = PathBuf::from(""); // points at xtask/Cargo.toml + let toml = Toml::new(fake_crate_root).load().unwrap(); + assert_eq!(toml.get_version().unwrap(), Version::new(0, 1, 0)); + } + + #[test] + fn it_gets_name_field() { + let fake_crate_root = PathBuf::from(""); + let toml = Toml::new(fake_crate_root).load().unwrap(); + assert_eq!(toml.get_name().unwrap(), "xtask"); + } + + #[test] + fn it_gets_description_field() { + let fake_crate_root = PathBuf::from(""); + let toml = Toml::new(fake_crate_root).load().unwrap(); + assert_eq!(toml.get_description().unwrap(), "internal-only crate used to orchestrate repo tasks"); + } } diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs index 9d8b17e..b83cd0f 100644 --- a/xtask/src/workspace.rs +++ b/xtask/src/workspace.rs @@ -11,7 +11,7 @@ type DynError = Box; const CRATES_DIRNAME: &str = "crates"; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default)] pub struct Workspace { pub path: PathBuf, pub cargo_cmd: String, From 27302f3b551c50a99ceb91e61b06b7047c8bd4c9 Mon Sep 17 00:00:00 2001 From: busticated Date: Fri, 13 Oct 2023 11:42:23 -0700 Subject: [PATCH 06/11] toml and readme structs always set and save instance data --- xtask/src/readme.rs | 12 +++++++----- xtask/src/toml.rs | 10 ++++++---- xtask/src/workspace.rs | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/xtask/src/readme.rs b/xtask/src/readme.rs index f0e7542..5e4b4b7 100644 --- a/xtask/src/readme.rs +++ b/xtask/src/readme.rs @@ -38,15 +38,16 @@ impl Readme { } pub fn create, D: AsRef>( - &self, + &mut self, name: N, description: D, ) -> Result<(), DynError> { - self.save(self.render(name, description)) + self.text = self.render(name, description); + self.save() } - pub fn save(&self, data: String) -> Result<(), DynError> { - Ok(fs::write(&self.path, data)?) + pub fn save(&self) -> Result<(), DynError> { + Ok(fs::write(&self.path, &self.text)?) } pub fn render, D: AsRef>(&self, name: N, description: D) -> String { @@ -95,7 +96,8 @@ impl Readme { entries.push('\n'); entries.push_str(marker_end); let updated = re.replace(&self.text, &entries); - self.save(updated.as_ref().to_owned()) + self.text = updated.as_ref().to_owned(); + self.save() } } diff --git a/xtask/src/toml.rs b/xtask/src/toml.rs index 0c9e715..14e9dbd 100644 --- a/xtask/src/toml.rs +++ b/xtask/src/toml.rs @@ -38,15 +38,17 @@ impl Toml { } pub fn create, D: AsRef>( - &self, + &mut self, name: N, description: D, ) -> Result<(), DynError> { - self.save(self.render(name, description)) + let text = self.render(name, description); + self.data = text.parse::()?; + self.save() } - pub fn save(&self, data: String) -> Result<(), DynError> { - Ok(fs::write(&self.path, data)?) + pub fn save(&self) -> Result<(), DynError> { + Ok(fs::write(&self.path, self.data.to_string())?) } pub fn render, D: AsRef>(&self, name: N, description: D) -> String { diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs index b83cd0f..de755e4 100644 --- a/xtask/src/workspace.rs +++ b/xtask/src/workspace.rs @@ -78,7 +78,7 @@ impl Workspace { description: D, ) -> Result { let path = self.krates_path().join(name.as_ref()); - let krate = Krate::new(kind, version, name, description, path); + let mut krate = Krate::new(kind, version, name, description, path); cmd!( &self.cargo_cmd, From 1a107b4b952f02ea48dd74df40ef5af6634806cf Mon Sep 17 00:00:00 2001 From: busticated Date: Fri, 13 Oct 2023 16:16:56 -0700 Subject: [PATCH 07/11] add task to mark crates for publishing --- xtask/src/git.rs | 153 ++++++++++++++++++++++++++++++++++++++++++++ xtask/src/krate.rs | 26 ++++++++ xtask/src/main.rs | 65 ++++++++++++++++++- xtask/src/semver.rs | 111 ++++++++++++++++++++++++++++++++ xtask/src/toml.rs | 7 +- 5 files changed, 359 insertions(+), 3 deletions(-) create mode 100644 xtask/src/git.rs create mode 100644 xtask/src/semver.rs diff --git a/xtask/src/git.rs b/xtask/src/git.rs new file mode 100644 index 0000000..384a51b --- /dev/null +++ b/xtask/src/git.rs @@ -0,0 +1,153 @@ +use crate::options::Options; +use duct::{cmd, Expression}; +use std::ffi::OsString; +use std::path::Path; + +#[derive(Clone, Debug, PartialEq)] +pub struct Git<'a> { + opts: &'a Options, +} + +impl<'a> Git<'a> { + pub fn new(opts: &'a Options) -> Git<'a> { + Git { opts } + } + + pub fn cmd(&self, args: Vec) -> Expression { + let mut args = args.clone(); + + if self.opts.has("dry-run"){ + args.insert(0, "skipping:".into()); + args.insert(1, "git".into()); + // TODO (mirande): windows? see: https://stackoverflow.com/a/61857874/579167 + return cmd("echo", args); + } + + cmd("git", args) + } + + fn build_args(&self, args1: U, args2: UU) -> Vec + where + U: IntoIterator, + U::Item: Into, + UU: IntoIterator, + UU::Item: Into, + { + let mut args = args1.into_iter().map(Into::::into).collect::>(); + args.extend(args2.into_iter().map(Into::::into).collect::>()); + args.retain(|a| !a.is_empty()); + args + } + + pub fn add(&self, path: P, arguments: U) -> Expression + where + P: AsRef, + U: IntoIterator, + U::Item: Into, + { + let args = self.add_raw(path, arguments); + self.cmd(args) + } + + fn add_raw(&self, path: P, arguments: U) -> Vec + where + P: AsRef, + U: IntoIterator, + U::Item: Into, + { + self.build_args( + vec![OsString::from("add"), path.as_ref().to_owned().into()], + arguments + ) + } + + pub fn commit(&self, message: M, arguments: U) -> Expression + where + M: AsRef, + U: IntoIterator, + U::Item: Into, + { + let args = self.commit_raw(message, arguments); + self.cmd(args) + } + + fn commit_raw(&self, message: M, arguments: U) -> Vec + where + M: AsRef, + U: IntoIterator, + U::Item: Into, + { + self.build_args( + vec!["commit", "--message", message.as_ref()], + arguments + ) + } + + pub fn tag(&self, tag: T, arguments: U) -> Expression + where + T: AsRef, + U: IntoIterator, + U::Item: Into, + { + let args = self.tag_raw(tag, arguments); + self.cmd(args) + } + + fn tag_raw(&self, tag: T, arguments: U) -> Vec + where + T: AsRef, + U: IntoIterator, + U::Item: Into, + { + self.build_args( + vec!["tag", tag.as_ref(), "--message", tag.as_ref()], + arguments + ) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + use crate::task_flags; + + #[test] + fn it_initializes() { + let opts = Options::new(vec![], task_flags! {}).unwrap(); + let _ = Git::new(&opts); + } + + #[test] + fn it_builds_args() { + let opts = Options::new(vec![], task_flags! {}).unwrap(); + let git = Git::new(&opts); + let args = git.build_args(["one"], vec!["two", "three"]); + assert_eq!(args, ["one", "two", "three"]); + } + + #[test] + fn it_builds_args_for_the_add_subcommand() { + let opts = Options::new(vec![], task_flags! {}).unwrap(); + let git = Git::new(&opts); + let args = git.add_raw(Path::new("path/to/file"), [""]); + assert_eq!(args, ["add", "path/to/file"]); + } + + #[test] + fn it_builds_args_for_the_commit_subcommand() { + let opts = Options::new(vec![], task_flags! {}).unwrap(); + let git = Git::new(&opts); + let args = git.commit_raw("my message", ["--one", "--two"]); + assert_eq!(args, ["commit", "--message", "my message", "--one", "--two"]); + } + + #[test] + fn it_builds_args_for_the_tag_subcommand() { + let opts = Options::new(vec![], task_flags! {}).unwrap(); + let git = Git::new(&opts); + let args = git.tag_raw("my tag", ["--one", "--two"]); + assert_eq!(args, ["tag", "my tag", "--message", "my tag", "--one", "--two"]); + } +} diff --git a/xtask/src/krate.rs b/xtask/src/krate.rs index 938a34c..1e28694 100644 --- a/xtask/src/krate.rs +++ b/xtask/src/krate.rs @@ -97,6 +97,16 @@ impl Krate { Ok(krate) } + pub fn id(&self) -> String { + format!("{}@{}", &self.name, self.version) + } + + pub fn set_version(&mut self, version: Version) -> Result<(), DynError> { + self.version = version; + self.toml.set_version(&self.version)?; + Ok(()) + } + pub fn clean(&self) -> Result<(), DynError> { Ok(fs::remove_dir_all(self.tmp_path())?) } @@ -263,6 +273,22 @@ mod tests { assert_eq!(krate.toml.path, PathBuf::from("")); } + #[test] + fn it_sets_krate_version() { + let mut krate = Krate::new( + "lib", + "0.1.0", + "my-crate", + "my-crate's description", + PathBuf::from("fake-crate"), + ); + + krate.set_version(Version::new(1, 0, 0)).unwrap(); + + assert_eq!(krate.version.to_string(), "1.0.0"); + assert_eq!(krate.toml.get_version().unwrap().to_string(), "1.0.0"); + } + #[test] fn it_gets_path_to_krate() { let krate = Krate::new( diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 77d14a9..d60dfc6 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,17 +1,22 @@ +mod git; mod krate; mod options; mod readme; +mod semver; mod tasks; mod toml; mod workspace; -use crate::krate::KratePaths; +use crate::git::Git; +use crate::krate::{Krate, KratePaths}; use crate::tasks::{Task, Tasks}; +use crate::semver::VersionChoice; use crate::workspace::Workspace; use duct::cmd; use inquire::required; +use inquire::list_option::ListOption as InquireListOption; use inquire::validator::Validation as InquireValidation; -use inquire::{Select as InquireSelect, Text as InquireText}; +use inquire::{MultiSelect as InquireMultiSelect, Select as InquireSelect, Text as InquireText}; use regex::RegexBuilder; use std::env; use std::error::Error; @@ -261,6 +266,62 @@ fn init_tasks() -> Tasks { Ok(()) }, }, + Task { + name: "crate:release".into(), + description: "prepate crates for publishing".into(), + flags: task_flags! { + "dry-run" => "run thru steps but do not save changes" + }, + run: |opts, workspace, _tasks| { + println!("::::::::::::::::::::::::::"); + println!(":::: Releasing Crates ::::"); + println!("::::::::::::::::::::::::::"); + println!(); + + let git = Git::new(&opts); + let mut krates = workspace.krates()?; + let question = InquireMultiSelect::new("Which crates should be published?", krates.keys().cloned().collect()); + let to_publish = question + .with_validator(|selections: &[InquireListOption<&String>]| { + if selections.is_empty() { + return Ok(InquireValidation::Invalid("Please select at least one crate!".into())); + } + + Ok(InquireValidation::Valid) + }) + .prompt()?; + + krates.retain(|_, v| to_publish.contains(&v.name)); + let mut krates = krates.values().cloned().collect::>(); + + for krate in krates.iter_mut() { + let version = krate.toml.get_version()?; + let options = VersionChoice::options(&version); + let message = format!("Version for `{}` [current: {}]", krate.name, version); + let question = InquireSelect::new(&message, options); + let choice = question.prompt()?; + krate.set_version(choice.get_version())?; + if opts.has("dry-run") { + println!("Skipping: Version bump for {}", krate.toml.path.display()); + } else { + krate.toml.save()?; + } + git.add(&krate.toml.path, [""]).run()?; + } + + let tags: Vec = krates.iter().map(|k| k.id()).collect(); + let message = format!("Release:\n{}", tags.join("\n")); + git.commit(message, [""]).run()?; + + for tag in tags { + git.tag(tag, [""]).run()?; + } + + println!(":::: Done!"); + println!(); + Ok(()) + }, + }, Task { name: "dist".into(), description: "create release artifacts".into(), diff --git a/xtask/src/semver.rs b/xtask/src/semver.rs new file mode 100644 index 0000000..3fd87a6 --- /dev/null +++ b/xtask/src/semver.rs @@ -0,0 +1,111 @@ +use std::fmt::{Display, Formatter}; +use semver::{BuildMetadata, Prerelease, Version}; + +#[derive(Clone, Debug, PartialEq)] +pub enum VersionChoice { + Major(Version), + Minor(Version), + Patch(Version), +} + +impl Display for VersionChoice { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + let msg = match self { + VersionChoice::Major(v) => format!("Major: {}", v), + VersionChoice::Minor(v) => format!("Minor: {}", v), + VersionChoice::Patch(v) => format!("Patch: {}", v), + }; + + write!(f, "{}", msg) + } +} + +impl VersionChoice { + pub fn options(version: &Version) -> Vec { + vec![ + VersionChoice::Major(increment_major(version)), + VersionChoice::Minor(increment_minor(version)), + VersionChoice::Patch(increment_patch(version)), + ] + } + + pub fn get_version(&self) -> Version { + match self { + VersionChoice::Major(v) => v.clone(), + VersionChoice::Minor(v) => v.clone(), + VersionChoice::Patch(v) => v.clone(), + } + } +} + +pub fn increment_major(version: &Version) -> Version { + let mut v = version.clone(); + v.major += 1; + v.minor = 0; + v.patch = 0; + v.pre = Prerelease::EMPTY; + v.build = BuildMetadata::EMPTY; + v +} + +pub fn increment_minor(version: &Version) -> Version { + let mut v = version.clone(); + v.minor += 1; + v.patch = 0; + v.pre = Prerelease::EMPTY; + v.build = BuildMetadata::EMPTY; + v +} + +pub fn increment_patch(version: &Version) -> Version { + let mut v = version.clone(); + v.patch += 1; + v.pre = Prerelease::EMPTY; + v.build = BuildMetadata::EMPTY; + v +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_initializes_version_choice_options() { + let version = Version::new(1, 0, 0); + let options = VersionChoice::options(&version); + assert_eq!(options.len(), 3); + assert_eq!(options[0], VersionChoice::Major(Version::new(2, 0, 0))); + assert_eq!(options[1], VersionChoice::Minor(Version::new(1, 1, 0))); + assert_eq!(options[2], VersionChoice::Patch(Version::new(1, 0, 1))); + } + + #[test] + fn it_gets_version() { + let choice = VersionChoice::Major(Version::new(1, 0, 0)); + assert_eq!(choice.get_version(), Version::new(1, 0, 0)); + } + + #[test] + fn it_displays_version_choice_text() { + let choice = VersionChoice::Major(Version::new(1, 0, 0)); + assert_eq!(format!("{}", choice), "Major: 1.0.0"); + } + + #[test] + fn it_increments_major_version() { + let version = Version::new(1, 0, 0); + assert_eq!(increment_major(&version), Version::new(2, 0, 0)); + } + + #[test] + fn it_increments_minor_version() { + let version = Version::new(1, 0, 0); + assert_eq!(increment_minor(&version), Version::new(1, 1, 0)); + } + + #[test] + fn it_increments_patch_version() { + let version = Version::new(1, 0, 0); + assert_eq!(increment_patch(&version), Version::new(1, 0, 1)); + } +} diff --git a/xtask/src/toml.rs b/xtask/src/toml.rs index 14e9dbd..b8a7453 100644 --- a/xtask/src/toml.rs +++ b/xtask/src/toml.rs @@ -1,7 +1,7 @@ use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; -use toml_edit::Document; +use toml_edit::{Document, value as toml_value}; use semver::Version; type DynError = Box; @@ -83,6 +83,11 @@ impl Toml { Ok(Version::parse(version)?) } + pub fn set_version(&mut self, version: &Version) -> Result<(), DynError> { + self.data["package"]["version"] = toml_value(version.to_string()); + Ok(()) + } + pub fn get_name(&self) -> Result { let pkg = self .data From a5610bc2035727026bf661643f4ffd7254e414fe Mon Sep 17 00:00:00 2001 From: busticated Date: Fri, 13 Oct 2023 17:52:59 -0700 Subject: [PATCH 08/11] add task to publish crates --- xtask/src/main.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index d60dfc6..7398238 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -266,6 +266,54 @@ fn init_tasks() -> Tasks { Ok(()) }, }, + Task { + name: "crate:publish".into(), + description: "publish released crates to crates.io".into(), + flags: task_flags! { + "dry-run" => "run thru steps but do not publish" + }, + run: |opts, workspace, _tasks| { + println!(":::::::::::::::::::::::::::"); + println!(":::: Publishing Crates ::::"); + println!(":::::::::::::::::::::::::::"); + println!(); + + let krates = workspace.krates()?; + let tag_text = cmd!("git", "tag", "--points-at", "HEAD").read()?; + let mut tags = vec![]; + + for line in tag_text.lines() { + if line.contains('@') { + tags.push(line); + } + } + + if tags.is_empty() { + println!(":::: Nothing to publish"); + println!(":::: Done!"); + println!(); + return Ok(()) + } + + for tag in tags { + let (name, _ver) = tag.split_once('@').unwrap_or_else(|| panic!("Invalid Tag: `{}`!", tag)); + let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{}`!", name)); + let message = format!("Publishing: {} at v{}", &krate.name, &krate.version); + + if opts.has("dry-run") { + println!("{} [skip]", &message); + } else { + println!("{}", &message); + cmd!(&workspace.cargo_cmd, "publish", "--package", &krate.name).run()?; + } + } + + println!(); + println!(":::: Done!"); + println!(); + Ok(()) + }, + }, Task { name: "crate:release".into(), description: "prepate crates for publishing".into(), From c2d3b0b7db003582a0bde63bc84259c689ec44f1 Mon Sep 17 00:00:00 2001 From: busticated Date: Fri, 13 Oct 2023 18:06:22 -0700 Subject: [PATCH 09/11] add publish action workflow --- .github/workflows/ci.yaml | 39 ++++++++++++++++++++++++++++++++++ .github/workflows/publish.yaml | 30 ++++++++++++++++++++++++++ bin/ci-set-commit-info.sh | 24 +++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 .github/workflows/publish.yaml create mode 100755 bin/ci-set-commit-info.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1ea707a..8625534 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,8 +9,47 @@ on: workflow_dispatch: jobs: + info: + name: Harvest Commit Info + runs-on: ubuntu-latest + outputs: + tags: ${{ steps.info.outputs.tags }} + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + with: + fetch-tags: true + - name: Set Info + id: info + run: ./bin/ci-set-commit-info.sh + + debug: + name: Debug + needs: info + runs-on: ubuntu-latest + steps: + - name: Log Info + run: | + echo ":::: GIT REF: ${{github.ref}}" + echo ":::: GIT REF_NAME: ${{github.ref_name}}" + echo ":::: TAGS: ${{needs.info.outputs.tags}}" + - name: Test Tag Check + if: contains(needs.info.outputs.tags, '@') + run: | + echo ":::: FOUND TAGS" + test: name: Run Tests + needs: [info, debug] uses: ./.github/workflows/test.yaml secrets: inherit + publish: + name: Publish Crates + needs: [info, debug, test] + if: github.ref_name == 'main' && contains(needs.info.outputs.tags, '@') + uses: ./.github/workflows/publish.yaml + secrets: inherit + with: + tags: ${{needs.info.outputs.tags}} + diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..1e57119 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,30 @@ +name: Publish + +on: + workflow_call: + inputs: + tags: + type: string + required: true + secrets: + CARGO_REGISTRY_TOKEN: + required: true + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + with: + fetch-tags: true + - name: Install Rust Toolchain (stable) + uses: dtolnay/rust-toolchain@stable + - name: Setup Project + run: cargo xtask setup + - name: Publish to Crates.io + if: contains(inputs.tags, '@') + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo xtask crate:publish + diff --git a/bin/ci-set-commit-info.sh b/bin/ci-set-commit-info.sh new file mode 100755 index 0000000..0b2059b --- /dev/null +++ b/bin/ci-set-commit-info.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +echo +echo ":::: GIT Commit:" +git log -1 + +echo +echo ":::: GIT Tags:" +git tag --points-at HEAD + +taglistStr=$(git tag --points-at HEAD) +declare -a taglist=($taglistStr) +tags="" + +for tag in "${taglist[@]}"; do + tags="${tags} ${tag}" +done + +echo +echo ":::: Set Action Output: Tags:" +echo "tags=$tags" >> $GITHUB_OUTPUT +echo $tags +echo + From fa508af278cf0e5b492b0952b083910fd40fc8cb Mon Sep 17 00:00:00 2001 From: busticated Date: Fri, 13 Oct 2023 20:02:23 -0700 Subject: [PATCH 10/11] add publishing instructions to README --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 258023e..750c00d 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,22 @@ Run `cargo xtask help` to see any other docs-related commands that are available

+
+How to publish crates +

+ +To publish a crate to the [crates.io](https://crates.io) registry, follow these steps: + +1. Checkout the `main` branch: `git checkout main` +2. Run `cargo xtask crate:release` and follow the prompts +3. Verify all checks pass: `cargo xtask ci` +4. Push to remote: `git push origin main --follow-tags` + +Each crate you select for publishing will be assigned its new version and all changes will be committed and tagged in `git`. The assigned tag will be formatted like `name@version` (e.g. `detect-newline-style@1.0.0`). After pushing to the remote, CI will execute the publishing steps and if all goes well, your crate will be available on [crates.io](https://crates.io). + +

+
+
How to view and add TODO source code comments

From 4fbbe98fb4f1764917c88631f4ea43ea0245f208 Mon Sep 17 00:00:00 2001 From: busticated Date: Sat, 14 Oct 2023 17:35:02 -0700 Subject: [PATCH 11/11] rustfmt fixes --- xtask/src/git.rs | 36 ++++++++++++++++++++++++------------ xtask/src/main.rs | 10 ++++++---- xtask/src/options.rs | 1 - xtask/src/toml.rs | 5 ++++- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/xtask/src/git.rs b/xtask/src/git.rs index 384a51b..0854c3d 100644 --- a/xtask/src/git.rs +++ b/xtask/src/git.rs @@ -16,7 +16,7 @@ impl<'a> Git<'a> { pub fn cmd(&self, args: Vec) -> Expression { let mut args = args.clone(); - if self.opts.has("dry-run"){ + if self.opts.has("dry-run") { args.insert(0, "skipping:".into()); args.insert(1, "git".into()); // TODO (mirande): windows? see: https://stackoverflow.com/a/61857874/579167 @@ -33,8 +33,18 @@ impl<'a> Git<'a> { UU: IntoIterator, UU::Item: Into, { - let mut args = args1.into_iter().map(Into::::into).collect::>(); - args.extend(args2.into_iter().map(Into::::into).collect::>()); + let mut args = args1 + .into_iter() + .map(Into::::into) + .collect::>(); + + args.extend( + args2 + .into_iter() + .map(Into::::into) + .collect::>(), + ); + args.retain(|a| !a.is_empty()); args } @@ -57,7 +67,7 @@ impl<'a> Git<'a> { { self.build_args( vec![OsString::from("add"), path.as_ref().to_owned().into()], - arguments + arguments, ) } @@ -77,10 +87,7 @@ impl<'a> Git<'a> { U: IntoIterator, U::Item: Into, { - self.build_args( - vec!["commit", "--message", message.as_ref()], - arguments - ) + self.build_args(vec!["commit", "--message", message.as_ref()], arguments) } pub fn tag(&self, tag: T, arguments: U) -> Expression @@ -101,12 +108,11 @@ impl<'a> Git<'a> { { self.build_args( vec!["tag", tag.as_ref(), "--message", tag.as_ref()], - arguments + arguments, ) } } - #[cfg(test)] mod tests { use super::*; @@ -140,7 +146,10 @@ mod tests { let opts = Options::new(vec![], task_flags! {}).unwrap(); let git = Git::new(&opts); let args = git.commit_raw("my message", ["--one", "--two"]); - assert_eq!(args, ["commit", "--message", "my message", "--one", "--two"]); + assert_eq!( + args, + ["commit", "--message", "my message", "--one", "--two"] + ); } #[test] @@ -148,6 +157,9 @@ mod tests { let opts = Options::new(vec![], task_flags! {}).unwrap(); let git = Git::new(&opts); let args = git.tag_raw("my tag", ["--one", "--two"]); - assert_eq!(args, ["tag", "my tag", "--message", "my tag", "--one", "--two"]); + assert_eq!( + args, + ["tag", "my tag", "--message", "my tag", "--one", "--two"] + ); } } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 7398238..41df5c5 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -32,9 +32,6 @@ fn main() { } fn try_main() -> Result<(), DynError> { - let cargo_cmd = get_cargo_cmd(); - let root_path = get_root_path(&cargo_cmd)?; - let mut workspace = Workspace::from_path(cargo_cmd, root_path)?; let mut args: Vec = env::args().collect(); args.remove(0); // drop executable path @@ -57,8 +54,13 @@ fn try_main() -> Result<(), DynError> { let tasks = init_tasks(); match tasks.get(cmd.clone()) { - Some(task) => task.exec(args, &mut workspace, &tasks), None => print_help(cmd, args, tasks), + Some(task) => { + let cargo_cmd = get_cargo_cmd(); + let root_path = get_root_path(&cargo_cmd)?; + let mut workspace = Workspace::from_path(cargo_cmd, root_path)?; + task.exec(args, &mut workspace, &tasks) + } } } diff --git a/xtask/src/options.rs b/xtask/src/options.rs index 5aba52a..231c951 100644 --- a/xtask/src/options.rs +++ b/xtask/src/options.rs @@ -11,7 +11,6 @@ pub struct Options { pub flags: TaskFlags, } -#[allow(dead_code)] impl Options { pub fn new(args: Vec, flags: TaskFlags) -> Result { let re = Regex::new(r"^-*")?; diff --git a/xtask/src/toml.rs b/xtask/src/toml.rs index b8a7453..1f1dede 100644 --- a/xtask/src/toml.rs +++ b/xtask/src/toml.rs @@ -194,6 +194,9 @@ mod tests { fn it_gets_description_field() { let fake_crate_root = PathBuf::from(""); let toml = Toml::new(fake_crate_root).load().unwrap(); - assert_eq!(toml.get_description().unwrap(), "internal-only crate used to orchestrate repo tasks"); + assert_eq!( + toml.get_description().unwrap(), + "internal-only crate used to orchestrate repo tasks" + ); } }