From 0a1235e27944f152ca195c32e7eef8985d475989 Mon Sep 17 00:00:00 2001
From: David Knaack <davidkna@users.noreply.github.com>
Date: Wed, 20 Jul 2022 11:17:27 +0200
Subject: [PATCH] feat(package): support cargo workspace versions (#4161)

---
 src/modules/package.rs | 125 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 122 insertions(+), 3 deletions(-)

diff --git a/src/modules/package.rs b/src/modules/package.rs
index 7d707a66..2906d992 100644
--- a/src/modules/package.rs
+++ b/src/modules/package.rs
@@ -7,6 +7,8 @@ use quick_xml::events::Event as QXEvent;
 use quick_xml::Reader as QXReader;
 use regex::Regex;
 use serde_json as json;
+use std::fs;
+use std::io::Read;
 
 /// Creates a module with the current package version
 pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
@@ -213,10 +215,45 @@ fn get_sbt_version(context: &Context, config: &PackageConfig) -> Option<String>
 }
 
 fn get_cargo_version(context: &Context, config: &PackageConfig) -> Option<String> {
-    let file_contents = context.read_file_from_pwd("Cargo.toml")?;
+    let mut file_contents = context.read_file_from_pwd("Cargo.toml")?;
 
-    let cargo_toml: toml::Value = toml::from_str(&file_contents).ok()?;
-    let raw_version = cargo_toml.get("package")?.get("version")?.as_str()?;
+    let mut cargo_toml: toml::Value = toml::from_str(&file_contents).ok()?;
+    let cargo_version = cargo_toml.get("package").and_then(|p| p.get("version"));
+    let raw_version = if let Some(v) = cargo_version.and_then(|v| v.as_str()) {
+        // regular version string
+        v
+    } else if cargo_version
+        .and_then(|v| v.get("workspace"))
+        .and_then(|w| w.as_bool())
+        .unwrap_or_default()
+    {
+        // workspace version string (`package.version.worspace = true`)
+        // need to read the Cargo.toml file from the workspace root
+        let mut version = None;
+        // disover the workspace root
+        for path in context.current_dir.ancestors().skip(1) {
+            // Assume the workspace root is the first ancestor that contains a Cargo.toml file
+            if let Ok(mut file) = fs::File::open(path.join("Cargo.toml")) {
+                file.read_to_string(&mut file_contents).ok()?;
+                cargo_toml = toml::from_str(&file_contents).ok()?;
+                // Read workspace.package.version
+                version = cargo_toml
+                    .get("workspace")?
+                    .get("package")?
+                    .get("version")?
+                    .as_str();
+                break;
+            }
+        }
+        version?
+    } else {
+        // This might be a workspace file
+        cargo_toml
+            .get("workspace")?
+            .get("package")?
+            .get("version")?
+            .as_str()?
+    };
 
     format_version(raw_version, config.version_format)
 }
@@ -356,6 +393,88 @@ mod tests {
         project_dir.close()
     }
 
+    #[test]
+    fn test_extract_cargo_version_ws() -> io::Result<()> {
+        let ws_config_name = "Cargo.toml";
+        let ws_config_content = toml::toml! {
+            [workspace.package]
+            version = "0.1.0"
+        }
+        .to_string();
+
+        let config_name = "member/Cargo.toml";
+        let config_content = toml::toml! {
+            [package]
+            version.workspace = true
+        }
+        .to_string();
+
+        let project_dir = create_project_dir()?;
+        fs::create_dir(project_dir.path().join("member"))?;
+
+        fill_config(&project_dir, ws_config_name, Some(&ws_config_content))?;
+        fill_config(&project_dir, config_name, Some(&config_content))?;
+
+        // Version can be read both from the workspace and the member.
+        expect_output(&project_dir, Some("v0.1.0"), None);
+        let actual = ModuleRenderer::new("package")
+            .path(project_dir.path().join("member"))
+            .collect();
+        let expected = Some(format!(
+            "is {} ",
+            Color::Fixed(208).bold().paint("📦 v0.1.0")
+        ));
+        assert_eq!(actual, expected);
+        project_dir.close()
+    }
+
+    #[test]
+    fn test_extract_cargo_version_ws_false() -> io::Result<()> {
+        let ws_config_name = "Cargo.toml";
+        let ws_config_content = toml::toml! {
+            [workspace.package]
+            version = "0.1.0"
+        }
+        .to_string();
+
+        let config_name = "member/Cargo.toml";
+        let config_content = toml::toml! {
+            [package]
+            version.workspace = false
+        }
+        .to_string();
+
+        let project_dir = create_project_dir()?;
+        fs::create_dir(project_dir.path().join("member"))?;
+
+        fill_config(&project_dir, ws_config_name, Some(&ws_config_content))?;
+        fill_config(&project_dir, config_name, Some(&config_content))?;
+
+        // Version can be read both from the workspace and the member.
+        let actual = ModuleRenderer::new("package")
+            .path(project_dir.path().join("member"))
+            .collect();
+        let expected = None;
+        assert_eq!(actual, expected);
+        project_dir.close()
+    }
+
+    #[test]
+    fn test_extract_cargo_version_ws_missing_parent() -> io::Result<()> {
+        let config_name = "Cargo.toml";
+        let config_content = toml::toml! {
+            [package]
+            name = "starship"
+            version.workspace = true
+        }
+        .to_string();
+
+        let project_dir = create_project_dir()?;
+        fill_config(&project_dir, config_name, Some(&config_content))?;
+        expect_output(&project_dir, None, None);
+        project_dir.close()
+    }
+
     #[test]
     fn test_extract_nimble_package_version() -> io::Result<()> {
         let config_name = "test_project.nimble";