Repository: DavidVujic/python-polylith

Issue:
poly diff is not working when structure is not at the root
**Describe the bug**
We have a mono repository that contains multiple languages.
The structure look like that

```
|- Python
       |- bases
       |- components
       |- projects
       |- ...
| Javascript
| IaC
| gitlab
| just
```
All Python code is in a folder called "python," which contains the structure of the poly project.

**To Reproduce**
Created a unit test on my branch that shows the problem
https://github.com/davzucky/python-polylith/blob/b2d05174dfc8a6eefa2dd202e7d5a8b89e395552/test/components/polylith/diff/test_collect.py#L53

**Error Log**
Return all the time empty list from the diff

**Expected behavior**
Return the diff since the commit hash change

**Desktop (please complete the following information):**
 - OS: Linux and Mac
 - Python version:  3.12
 - uv version: 0.5.2

**Additional context**
Add any other context about the problem here.

Hints from issue comments:
@DavidVujic: Thank you for reporting and adding the unit test @davzucky! ⭐  

I'll have a look, I think I have an idea of why it is not working as expected.@davzucky: The problem appears in ```collect.py``` in the function ```_get_changed```.

in my case the regex is ```bases/qdev``` however the file name change is ```python/bases/qdev/hawk/common/snapshot_validator.py'```.  I try to add the root in ```_parse_path_pattern```; however, this doesn't solve the problem as this is the full path.
We need the relative path from the git root to the workspace. 

Happy to test anything you have with my workspace.@DavidVujic: @davzucky I have published new versions of the Poetry plugin and the CLI. It would be great if you can test and verify things work as expected in your workspace.

Also: thank you for the sponsoring, much appreciated! ⭐ 🙏 @davzucky: Thank you, @DavidVujic. I tested the new version, 1.21, and confirmed it solved the problem. Thank you for your quick turnaround in solving the problem. 

Solution patch:
diff --git a/components/polylith/commands/diff.py b/components/polylith/commands/diff.py
index abac0ed..2672d17 100644
--- a/components/polylith/commands/diff.py
+++ b/components/polylith/commands/diff.py
@@ -66,7 +66,7 @@ def print_views(root: Path, tag: str, options: dict) -> None:
     changed_bases = diff.collect.get_changed_bases(root, files, ns)
     changed_components = diff.collect.get_changed_components(root, files, ns)
     changed_bricks = set(changed_bases + changed_components)
-    changed_projects = diff.collect.get_changed_projects(files)
+    changed_projects = diff.collect.get_changed_projects(root, files)
 
     projects_data = get_projects_data(root, ns)
 
diff --git a/components/polylith/diff/collect.py b/components/polylith/diff/collect.py
index e6571aa..2323fe7 100644
--- a/components/polylith/diff/collect.py
+++ b/components/polylith/diff/collect.py
@@ -15,11 +15,31 @@ def _parse_folder_parts(pattern: str, changed_file: Path) -> str:
     return next(p for p in file_path.parts if p != file_path.root)
 
 
-def _get_changed(pattern: str, changed_files: List[Path]) -> set:
+def _is_in_workspace(root: Path, top_dir: str, changed_file: Path) -> bool:
+    file_path = changed_file.as_posix()
+
+    if file_path.startswith(top_dir):
+        return True
+
+    return f"{root.name}/{top_dir}" in file_path
+
+
+def _is_match(root: Path, top_dir: str, pattern: str, file_path: Path) -> bool:
+    if re.match(pattern, file_path.as_posix()):
+        return True
+
+    found = re.search(pattern, file_path.as_posix())
+
+    return found is not None and _is_in_workspace(root, top_dir, file_path)
+
+
+def _get_changed(
+    root: Path, top_dir: str, pattern: str, changed_files: List[Path]
+) -> set:
     return {
         _parse_folder_parts(pattern, f)
         for f in changed_files
-        if re.match(pattern, f.as_posix())
+        if _is_match(root, top_dir, pattern, f)
     }
 
 
@@ -37,7 +57,7 @@ def _get_changed_bricks(
 ) -> list:
     pattern = _parse_path_pattern(root, top_dir, namespace)
 
-    return sorted(_get_changed(pattern, changed_files))
+    return sorted(_get_changed(root, top_dir, pattern, changed_files))
 
 
 def get_changed_components(
@@ -50,8 +70,8 @@ def get_changed_bases(root: Path, changed_files: List[Path], namespace: str) ->
     return _get_changed_bricks(root, repo.bases_dir, changed_files, namespace)
 
 
-def get_changed_projects(changed_files: List[Path]) -> list:
-    res = _get_changed(repo.projects_dir, changed_files)
+def get_changed_projects(root: Path, changed_files: List[Path]) -> list:
+    res = _get_changed(root, repo.projects_dir, repo.projects_dir, changed_files)
     filtered = {p for p in res if p != repo.projects_dir}
     return sorted(filtered)
 
diff --git a/development/david.py b/development/david.py
index 4f0d47e..3b66823 100644
--- a/development/david.py
+++ b/development/david.py
@@ -42,7 +42,7 @@
 changed_files = diff.collect.get_files(tag)
 changed_components = diff.collect.get_changed_components(root, changed_files, ns)
 changed_bases = diff.collect.get_changed_bases(root, changed_files, ns)
-changed_projects = diff.collect.get_changed_projects(changed_files)
+changed_projects = diff.collect.get_changed_projects(root, changed_files)
 
 projects_data = info.get_bricks_in_projects(root, changed_components, changed_bases, ns)
 
diff --git a/projects/poetry_polylith_plugin/pyproject.toml b/projects/poetry_polylith_plugin/pyproject.toml
index 97b5a81..d9f1d19 100644
--- a/projects/poetry_polylith_plugin/pyproject.toml
+++ b/projects/poetry_polylith_plugin/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "poetry-polylith-plugin"
-version = "1.31.0"
+version = "1.32.0"
 description = "A Poetry plugin that adds tooling support for the Polylith Architecture"
 authors = ["David Vujic"]
 homepage = "https://davidvujic.github.io/python-polylith-docs/"
diff --git a/projects/polylith_cli/pyproject.toml b/projects/polylith_cli/pyproject.toml
index 7e69eb1..db163d8 100644
--- a/projects/polylith_cli/pyproject.toml
+++ b/projects/polylith_cli/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "polylith-cli"
-version = "1.20.2"
+version = "1.21.0"
 description = "Python tooling support for the Polylith Architecture"
 authors = ['David Vujic']
 homepage = "https://davidvujic.github.io/python-polylith-docs/"


Test patch:
diff --git a/test/components/polylith/diff/test_collect.py b/test/components/polylith/diff/test_collect.py
index 0af00eb..bbed936 100644
--- a/test/components/polylith/diff/test_collect.py
+++ b/test/components/polylith/diff/test_collect.py
@@ -1,10 +1,13 @@
-from polylith.diff import collect
 from pathlib import Path
+
 import pytest
+from polylith.diff import collect
 
 root = Path.cwd()
 ns = "my_namespace"
 
+subfolder = "python"
+workspace_root_in_subfolder = Path(subfolder)
 
 changed_files_loose = [
     Path(f"components/{ns}/a/core.py"),
@@ -12,6 +15,8 @@
     Path(f"bases/{ns}/b/core.py"),
     Path(f"components/{ns}/b/core.py"),
     Path(f"components/{ns}/c/nested/subfolder/core.py"),
+    Path(f"test/components/{ns}/x/core.py"),
+    Path("projects/z/pyproject.toml"),
 ]
 
 changed_files_tdd = [
@@ -21,6 +26,8 @@
     Path(f"components/b/src/{ns}/b/core.py"),
     Path(f"components/{ns}/x/core.py"),
     Path(f"components/c/src/{ns}/c/nested/subfolder/core.py"),
+    Path(f"components/x/test/{ns}/x/core.py"),
+    Path("projects/z/pyproject.toml"),
 ]
 
 
@@ -64,3 +71,37 @@ def test_get_changed_bases_with_tdd_theme(setup):
     res = collect.get_changed_bases(root, changed_files_tdd, ns)
 
     assert res == ["b"]
+
+
+def test_get_changed_components_with_workspace_in_sub_folder(setup):
+    setup(theme="loose")
+
+    changes = [Path(f"{subfolder}/{p.as_posix()}") for p in changed_files_loose]
+
+    res = collect.get_changed_components(workspace_root_in_subfolder, changes, ns)
+
+    assert res == ["a", "b", "c"]
+
+
+def test_get_changed_components_with_workspace_in_sub_folder_tdd_theme(setup):
+    setup(theme="tdd")
+
+    changes = [Path(f"{subfolder}/{p.as_posix()}") for p in changed_files_tdd]
+
+    res = collect.get_changed_components(workspace_root_in_subfolder, changes, ns)
+
+    assert res == ["a", "b", "c"]
+
+
+def test_get_changed_projects():
+    res = collect.get_changed_projects(root, changed_files_loose)
+
+    assert res == ["z"]
+
+
+def test_get_changed_projects_in_subfolder():
+    changes = [Path(f"{subfolder}/{p.as_posix()}") for p in changed_files_loose]
+
+    res = collect.get_changed_projects(workspace_root_in_subfolder, changes)
+
+    assert res == ["z"]