From 04b397c6cf6e9be0b26c98c49827be24a2763405 Mon Sep 17 00:00:00 2001 From: Patrick Dahlke Date: Wed, 10 Dec 2025 09:55:23 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=94=A7=20=20Explicit=20Git=20root=20c?= =?UTF-8?q?onfiguration=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/components/configuration.rst | 25 +++++++++ src/sphinx_codelinks/analyse/analyse.py | 6 ++- src/sphinx_codelinks/config.py | 16 ++++-- tests/test_analyse.py | 69 ++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/docs/source/components/configuration.rst b/docs/source/components/configuration.rst index 99fe8fa..fff32db 100644 --- a/docs/source/components/configuration.rst +++ b/docs/source/components/configuration.rst @@ -361,6 +361,8 @@ Configures how **Sphinx-CodeLinks** analyse source files to extract markers from get_need_id_refs = true get_oneline_needs = true get_rst = true + # Optional: Explicit Git root for Bazel or deeply nested configs + # git_root = "/path/to/repo" [codelinks.projects.my_project.analyse.oneline_comment_style] start_sequence = "@" @@ -420,6 +422,29 @@ Enables the extraction of marked RST text from source code comments. When enable [codelinks.projects.my_project.analyse] get_rst = false +.. _`git_root`: + +git_root +^^^^^^^^ + +Specifies an explicit path to the Git repository root directory. This option is particularly useful in environments where the standard Git root auto-detection fails, such as: + +- **Bazel builds**: Where the execution path differs from the standard repository layout +- **Deeply nested configurations**: Where ``conf.py`` is located in a deep subdirectory far from the repository root +- **Custom build systems**: Where the working directory is different from the source repository + +When not set, **Sphinx-CodeLinks** will automatically traverse parent directories to locate the ``.git`` folder. + +**Type:** ``str`` (path) +**Default:** Not set (auto-detection) + +.. code-block:: toml + + [codelinks.projects.my_project.analyse] + git_root = "/absolute/path/to/repo" + +.. note:: When ``git_root`` is explicitly set, **Sphinx-CodeLinks** will use this path directly without attempting auto-detection. Ensure the path points to a valid Git repository containing a ``.git`` directory. + .. _`oneline_comment_style`: analyse.oneline_comment_style diff --git a/src/sphinx_codelinks/analyse/analyse.py b/src/sphinx_codelinks/analyse/analyse.py index a8e9751..6d0637d 100644 --- a/src/sphinx_codelinks/analyse/analyse.py +++ b/src/sphinx_codelinks/analyse/analyse.py @@ -65,7 +65,11 @@ def __init__( self.oneline_needs: list[OneLineNeed] = [] self.marked_rst: list[MarkedRst] = [] self.all_marked_content: list[NeedIdRefs | OneLineNeed | MarkedRst] = [] - self.git_root: Path | None = utils.locate_git_root(self.analyse_config.src_dir) + # Use explicitly configured git_root if provided, otherwise auto-detect + if self.analyse_config.git_root is not None: + self.git_root: Path | None = self.analyse_config.git_root.resolve() + else: + self.git_root = utils.locate_git_root(self.analyse_config.src_dir) self.git_remote_url: str | None = ( utils.get_remote_url(self.git_root) if self.git_root else None ) diff --git a/src/sphinx_codelinks/config.py b/src/sphinx_codelinks/config.py index 3aac458..c9f0b94 100644 --- a/src/sphinx_codelinks/config.py +++ b/src/sphinx_codelinks/config.py @@ -305,6 +305,7 @@ class AnalyseSectionConfigType(TypedDict, total=False): get_oneline_needs: bool get_rst: bool outdir: str + git_root: str need_id_refs: NeedIdRefsConfigType marked_rst: MarkedRstConfigType oneline_comment_style: OneLineCommentStyleType @@ -319,6 +320,7 @@ class SourceAnalyseConfigType(TypedDict, total=False): get_need_id_refs: bool get_oneline_needs: bool get_rst: bool + git_root: Path | None need_id_refs_config: NeedIdRefsConfig marked_rst_config: MarkedRstConfig oneline_comment_style: OneLineCommentStyle @@ -361,6 +363,12 @@ def field_names(cls) -> set[str]: get_rst: bool = field(default=False, metadata={"schema": {"type": "boolean"}}) """Whether to extract rst texts from comments""" + git_root: Path | None = field( + default=None, metadata={"schema": {"type": ["string", "null"]}} + ) + """Explicit path to the Git repository root. If not set, it will be auto-detected + by traversing parent directories. Useful for Bazel builds or deeply nested configs.""" + need_id_refs_config: NeedIdRefsConfig = field(default_factory=NeedIdRefsConfig) """Configuration for extracting need id references from comments.""" @@ -765,9 +773,11 @@ def convert_analyse_config( if config_dict: for k, v in config_dict.items(): if k not in {"online_comment_style", "need_id_refs", "marked_rst"}: - analyse_config_dict[k] = ( # type: ignore[literal-required] # dynamical assignment - Path(v) if k == "src_dic" and isinstance(v, str) else v - ) + # Convert string paths to Path objects + if k in {"src_dir", "git_root"} and isinstance(v, str): + analyse_config_dict[k] = Path(v) # type: ignore[literal-required] + else: + analyse_config_dict[k] = v # type: ignore[literal-required] # dynamical assignment # Get oneline_comment_style configuration oneline_comment_style_dict: OneLineCommentStyleType | None = config_dict.get( diff --git a/tests/test_analyse.py b/tests/test_analyse.py index 9f74ca7..b952c36 100644 --- a/tests/test_analyse.py +++ b/tests/test_analyse.py @@ -141,3 +141,72 @@ def test_analyse_oneline_needs( for src_file in src_analyse.src_files: cnt_comments += len(src_file.src_comments) assert cnt_comments == result["num_comments"] + + +def test_explicit_git_root_configuration(tmp_path): + """Test that explicit git_root configuration is used instead of auto-detection.""" + # Create a fake git repo structure in tmp_path + fake_git_root = tmp_path / "fake_repo" + fake_git_root.mkdir() + (fake_git_root / ".git").mkdir() + + # Create a minimal .git/config with remote URL + git_config = fake_git_root / ".git" / "config" + git_config.write_text( + '[remote "origin"]\n url = https://github.com/test/repo.git\n' + ) + + # Create HEAD file pointing to a branch ref + git_head = fake_git_root / ".git" / "HEAD" + git_head.write_text("ref: refs/heads/main\n") + + # Create the refs/heads/main file with the commit hash + refs_dir = fake_git_root / ".git" / "refs" / "heads" + refs_dir.mkdir(parents=True) + (refs_dir / "main").write_text("abc123def456\n") + + # Create source file in a deeply nested location + src_dir = tmp_path / "deeply" / "nested" / "src" + src_dir.mkdir(parents=True) + src_file = src_dir / "test.c" + src_file.write_text("// @Test, TEST_1\nvoid test() {}\n") + + # Configure with explicit git_root + src_analyse_config = SourceAnalyseConfig( + src_files=[src_file], + src_dir=src_dir, + get_need_id_refs=False, + get_oneline_needs=True, + get_rst=False, + git_root=fake_git_root, + ) + + src_analyse = SourceAnalyse(src_analyse_config) + + # Verify the explicit git_root was used + assert src_analyse.git_root == fake_git_root.resolve() + assert src_analyse.git_remote_url == "https://github.com/test/repo.git" + assert src_analyse.git_commit_rev == "abc123def456" + + +def test_git_root_auto_detection_when_not_configured(tmp_path): + """Test that git_root is auto-detected when not explicitly configured.""" + src_dir = TEST_DIR / "data" / "dcdc" + src_paths = [src_dir / "charge" / "demo_1.cpp"] + + # Don't set git_root - it should auto-detect + src_analyse_config = SourceAnalyseConfig( + src_files=src_paths, + src_dir=src_dir, + get_need_id_refs=False, + get_oneline_needs=True, + get_rst=False, + # git_root is not set, so auto-detection should be used + ) + + src_analyse = SourceAnalyse(src_analyse_config) + + # The test is running inside a git repo, so git_root should be detected + # We just verify it's not None (since this test runs in the sphinx-codelinks repo) + assert src_analyse.git_root is not None + assert (src_analyse.git_root / ".git").exists() From 2f103ea6d7c54760e93004a245f22caec91dedcc Mon Sep 17 00:00:00 2001 From: Patrick Dahlke Date: Wed, 10 Dec 2025 11:41:26 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=94=A7=20=20Resolve=20git=5Froot=20pa?= =?UTF-8?q?ths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relative to config file location in analyse command --- src/sphinx_codelinks/cmd.py | 8 +- .../sphinx_extension/directives/src_trace.py | 7 ++ tests/test_cmd.py | 107 ++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/src/sphinx_codelinks/cmd.py b/src/sphinx_codelinks/cmd.py index a702414..2c59c0b 100644 --- a/src/sphinx_codelinks/cmd.py +++ b/src/sphinx_codelinks/cmd.py @@ -56,7 +56,7 @@ @app.command(no_args_is_help=True) -def analyse( +def analyse( # noqa: PLR0912 # for CLI, so it needs the branches config: Annotated[ Path, typer.Argument( @@ -141,6 +141,12 @@ def analyse( analyse_config.src_files = src_discover.source_paths analyse_config.src_dir = Path(src_discover.src_discover_config.src_dir) + # git_root shall be relative to the config file's location (like src_dir) + if analyse_config.git_root is not None: + analyse_config.git_root = ( + config.parent / analyse_config.git_root + ).resolve() + analyse_errors = analyse_config.check_fields_configuration() errors.extend(analyse_errors) if errors: diff --git a/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py b/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py index 3e46e51..d7965e5 100644 --- a/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +++ b/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py @@ -116,6 +116,13 @@ def run(self) -> list[nodes.Node]: analyse_config = src_trace_conf["analyse_config"] analyse_config.src_dir = src_dir analyse_config.src_files = source_files + # git_root shall be relative to the config file's location (if provided) + if analyse_config.git_root: + conf_dir = Path(self.env.app.confdir) + if src_trace_sphinx_config.config_from_toml: + src_trace_toml_path = Path(src_trace_sphinx_config.config_from_toml) + conf_dir = conf_dir / src_trace_toml_path.parent + analyse_config.git_root = (conf_dir / analyse_config.git_root).resolve() src_analyse = SourceAnalyse(analyse_config) src_analyse.run() diff --git a/tests/test_cmd.py b/tests/test_cmd.py index 4222dca..8992cf5 100644 --- a/tests/test_cmd.py +++ b/tests/test_cmd.py @@ -283,3 +283,110 @@ def test_write_rst_negative(json_objs: list[dict], output_lines, tmp_path) -> No assert result.exit_code != 0 for line in output_lines: assert line in result.stdout + + +def test_analyse_with_relative_git_root(tmp_path: Path) -> None: + """Test that relative git_root is resolved relative to the config file location.""" + # Create a fake git repo structure + fake_git_root = tmp_path / "fake_repo" + fake_git_root.mkdir() + (fake_git_root / ".git").mkdir() + git_config = fake_git_root / ".git" / "config" + git_config.write_text( + '[remote "origin"]\n url = https://github.com/test/repo.git\n' + ) + git_head = fake_git_root / ".git" / "HEAD" + git_head.write_text("ref: refs/heads/main\n") + refs_dir = fake_git_root / ".git" / "refs" / "heads" + refs_dir.mkdir(parents=True) + (refs_dir / "main").write_text("abc123def456\n") + + # Create source file + src_dir = fake_git_root / "src" + src_dir.mkdir() + src_file = src_dir / "test.c" + src_file.write_text("// @Test, TEST_1, test\nvoid test() {}\n") + + # Create config in a subdirectory using a RELATIVE git_root path + config_dir = tmp_path / "config" + config_dir.mkdir() + config_file = config_dir / "codelinks.toml" + config_content = """ +[codelinks.projects.test_project.source_discover] +src_dir = "../fake_repo/src" +gitignore = false + +[codelinks.projects.test_project.analyse] +get_oneline_needs = true +git_root = "../fake_repo" +""" + config_file.write_text(config_content) + + outdir = tmp_path / "output" + outdir.mkdir() + + options = ["analyse", str(config_file), "--outdir", str(outdir)] + result = runner.invoke(app, options) + + assert result.exit_code == 0, f"CLI failed: {result.stdout}" + output_path = outdir / "marked_content.json" + assert output_path.exists() + + with output_path.open("r") as f: + marked_content = json.load(f) + # Verify the content was analysed using the correct git_root + assert len(marked_content["test_project"]) > 0 + + +def test_analyse_with_absolute_git_root(tmp_path: Path) -> None: + """Test that absolute git_root is used as-is.""" + # Create a fake git repo structure + fake_git_root = tmp_path / "fake_repo" + fake_git_root.mkdir() + (fake_git_root / ".git").mkdir() + git_config = fake_git_root / ".git" / "config" + git_config.write_text( + '[remote "origin"]\n url = https://github.com/test/repo.git\n' + ) + git_head = fake_git_root / ".git" / "HEAD" + git_head.write_text("ref: refs/heads/main\n") + refs_dir = fake_git_root / ".git" / "refs" / "heads" + refs_dir.mkdir(parents=True) + (refs_dir / "main").write_text("abc123def456\n") + + # Create source file + src_dir = fake_git_root / "src" + src_dir.mkdir() + src_file = src_dir / "test.c" + src_file.write_text("// @Test, TEST_2, test\nvoid test() {}\n") + + # Create config in a different location using an ABSOLUTE git_root path + config_dir = tmp_path / "config" + config_dir.mkdir() + config_file = config_dir / "codelinks.toml" + # Use absolute path for both src_dir and git_root + config_content = f""" +[codelinks.projects.test_project.source_discover] +src_dir = "{src_dir.as_posix()}" +gitignore = false + +[codelinks.projects.test_project.analyse] +get_oneline_needs = true +git_root = "{fake_git_root.as_posix()}" +""" + config_file.write_text(config_content) + + outdir = tmp_path / "output" + outdir.mkdir() + + options = ["analyse", str(config_file), "--outdir", str(outdir)] + result = runner.invoke(app, options) + + assert result.exit_code == 0, f"CLI failed: {result.stdout}" + output_path = outdir / "marked_content.json" + assert output_path.exists() + + with output_path.open("r") as f: + marked_content = json.load(f) + # Verify the content was analysed using the correct git_root + assert len(marked_content["test_project"]) > 0 From 063d46e19e90c89f9364f5b410f6c753d0a22b9b Mon Sep 17 00:00:00 2001 From: Patrick Dahlke Date: Thu, 11 Dec 2025 16:14:08 +0100 Subject: [PATCH 3/3] Fix merge issue --- tests/test_analyse.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_analyse.py b/tests/test_analyse.py index 6572547..6e6c2a7 100644 --- a/tests/test_analyse.py +++ b/tests/test_analyse.py @@ -210,13 +210,20 @@ def test_git_root_auto_detection_when_not_configured(tmp_path): # We just verify it's not None (since this test runs in the sphinx-codelinks repo) assert src_analyse.git_root is not None assert (src_analyse.git_root / ".git").exists() - oneline_comment_style=ONELINE_COMMENT_STYLE_DEFAULT, - ) + def test_oneline_parser_warnings_are_collected(tmp_path): """Test that oneline parser warnings are collected for later output.""" src_dir = TEST_DIR / "data" / "oneline_comment_default" src_paths = [src_dir / "default_oneliners.c"] + src_analyse_config = SourceAnalyseConfig( + src_files=src_paths, + src_dir=src_dir, + get_need_id_refs=False, + get_oneline_needs=True, + get_rst=False, + oneline_comment_style=ONELINE_COMMENT_STYLE_DEFAULT, + ) src_analyse = SourceAnalyse(src_analyse_config) src_analyse.run()