@@ -1,4 +1,4 @@
-from typing import List
+from typing import List, Dict, Tuple
import llm
import os
import pathlib
@@ -7,6 +7,162 @@ import tempfile
import shutil
+def parse_fragment_string(fragment_string: str) -> Tuple[str, Dict[str, str]]:
+ """
+ Parse a fragment string into URL and arguments
+
+ Format: url:arg1:arg2=value:arg3
+ Returns: (url, {arg1: True, arg2: "value", arg3: True})
+ """
+ # Define known repomix flags to detect where arguments start
+ known_flags = {
+ "compress", "remove-comments", "remove-empty-lines",
+ "output-show-line-numbers", "no-file-summary",
+ "no-directory-structure", "no-files",
+ "include-empty-directories", "no-git-sort-by-changes",
+ "include-diffs", "no-gitignore", "no-default-patterns",
+ "no-security-check", "verbose", "quiet", "style"
+ }
+
+ # Split on colons to get all parts
+ parts = fragment_string.split(":")
+ if len(parts) < 1:
+ raise ValueError("Invalid fragment string format")
+
+ # Handle different URL formats
+ if fragment_string.startswith("https://"):
+ # Find where arguments start
+ # parts = ['https', '//github.com/user/repo', 'compress']
+ arg_start_idx = None
+ for i, part in enumerate(parts):
+ if i <= 1: # Skip protocol parts (https, //domain/path)
+ continue
+ if "=" in part or part in known_flags:
+ arg_start_idx = i
+ break
+
+ if arg_start_idx is None:
+ url = fragment_string
+ arg_parts = []
+ else:
+ # Reconstruct URL by joining the parts before arguments
+ url_parts = parts[:arg_start_idx]
+ url = ":".join(url_parts)
+ arg_parts = parts[arg_start_idx:]
+
+ elif fragment_string.startswith("ssh://"):
+ # Similar logic for SSH URLs
+ # parts = ['ssh', '//git@github.com', 'user/repo.git', 'compress']
+ arg_start_idx = None
+ for i, part in enumerate(parts):
+ if i <= 1: # Skip protocol parts (ssh, //domain)
+ continue
+ if "=" in part or part in known_flags:
+ arg_start_idx = i
+ break
+
+ if arg_start_idx is None:
+ url = fragment_string
+ arg_parts = []
+ else:
+ url_parts = parts[:arg_start_idx]
+ url = ":".join(url_parts)
+ arg_parts = parts[arg_start_idx:]
+
+ elif fragment_string.startswith("git@"):
+ # git@host:path format - need to be careful about colons
+ # Look for arguments after the repo path
+ arg_start_idx = None
+
+ # Skip the first colon (after hostname) when looking for arguments
+ for i, part in enumerate(parts):
+ if i <= 1: # Skip git@host and first path part
+ continue
+ if "=" in part or part in known_flags:
+ arg_start_idx = i
+ break
+
+ if arg_start_idx is None:
+ url = fragment_string
+ arg_parts = []
+ else:
+ url_parts = parts[:arg_start_idx]
+ url = ":".join(url_parts)
+ arg_parts = parts[arg_start_idx:]
+ else:
+ # No protocol prefix, assume simple format
+ url = parts[0]
+ arg_parts = parts[1:]
+
+ # Parse arguments
+ args = {}
+ for arg in arg_parts:
+ if arg and "=" in arg:
+ key, value = arg.split("=", 1)
+ args[key] = value
+ elif arg:
+ args[arg] = True
+
+ return url, args
+
+
+def build_repomix_command(repo_path: str, args: Dict[str, str]) -> List[str]:
+ """
+ Build repomix command with arguments
+
+ Args:
+ repo_path: Path to the repository
+ args: Dictionary of arguments
+
+ Returns:
+ List of command parts
+ """
+ cmd = ["repomix", "--stdout"]
+
+ # Map of supported arguments to their command-line flags
+ supported_args = {
+ "compress": "--compress",
+ "style": "--style",
+ "include": "--include",
+ "ignore": "--ignore",
+ "remove-comments": "--remove-comments",
+ "remove-empty-lines": "--remove-empty-lines",
+ "output-show-line-numbers": "--output-show-line-numbers",
+ "no-file-summary": "--no-file-summary",
+ "no-directory-structure": "--no-directory-structure",
+ "no-files": "--no-files",
+ "header-text": "--header-text",
+ "instruction-file-path": "--instruction-file-path",
+ "include-empty-directories": "--include-empty-directories",
+ "no-git-sort-by-changes": "--no-git-sort-by-changes",
+ "include-diffs": "--include-diffs",
+ "no-gitignore": "--no-gitignore",
+ "no-default-patterns": "--no-default-patterns",
+ "no-security-check": "--no-security-check",
+ "token-count-encoding": "--token-count-encoding",
+ "top-files-len": "--top-files-len",
+ "verbose": "--verbose",
+ "quiet": "--quiet",
+ }
+
+ # Add arguments to command
+ for arg, value in args.items():
+ if arg in supported_args:
+ flag = supported_args[arg]
+ if value is True:
+ # Boolean flag
+ cmd.append(flag)
+ elif value and value != "":
+ # Value argument
+ cmd.extend([flag, value])
+ # Skip empty string values
+
+ # Add repository path
+ cmd.append(repo_path)
+
+ return cmd
+
+
@llm.hookimpl
def register_fragment_loaders(register):
register("repomix", repomix_loader)
@@ -16,14 +172,21 @@ def repomix_loader(argument: str) -> List[llm.Fragment]:
"""
Load repository contents as fragments using Repomix
- Argument is a git repository URL (https:// or ssh://)
+ Argument can be:
+ - A git repository URL: https://git.sr.ht/~amolith/willow
+ - URL with arguments: https://git.sr.ht/~amolith/willow:compress:include=*.py
+
Examples:
repomix:https://git.sr.ht/~amolith/willow
- repomix:ssh://git.sr.ht:~amolith/willow
+ repomix:ssh://git.sr.ht:~amolith/willow:compress
+ repomix:git@github.com:user/repo.git:include=*.ts,*.js:ignore=*.md
"""
- if not argument.startswith(("https://", "ssh://", "git@")):
+ # Parse the fragment string to extract URL and arguments
+ url, args = parse_fragment_string(argument)
+
+ if not url.startswith(("https://", "ssh://", "git@")):
raise ValueError(
- f"Repository URL must start with https://, ssh://, or git@ - got: {argument}"
+ f"Repository URL must start with https://, ssh://, or git@ - got: {url}"
)
# Check if repomix is available
@@ -40,15 +203,18 @@ def repomix_loader(argument: str) -> List[llm.Fragment]:
try:
# Clone the repository
subprocess.run(
- ["git", "clone", "--depth=1", argument, str(repo_path)],
+ ["git", "clone", "--depth=1", url, str(repo_path)],
check=True,
capture_output=True,
text=True,
)
+ # Build repomix command with arguments
+ repomix_cmd = build_repomix_command(str(repo_path), args)
+
# Run repomix on the cloned repository
result = subprocess.run(
- ["repomix", "--stdout", str(repo_path)],
+ repomix_cmd,
check=True,
capture_output=True,
text=True,
@@ -68,11 +234,11 @@ def repomix_loader(argument: str) -> List[llm.Fragment]:
# Handle Git or repomix errors
if "git" in str(e.cmd):
raise ValueError(
- f"Failed to clone repository {argument}: {e.stderr}"
+ f"Failed to clone repository {url}: {e.stderr}"
)
elif "repomix" in str(e.cmd):
raise ValueError(
- f"Failed to run repomix on {argument}: {e.stderr}"
+ f"Failed to run repomix on {url}: {e.stderr}"
)
else:
raise ValueError(
@@ -0,0 +1,540 @@
+from llm_fragments_repomix import repomix_loader
+import pytest
+from unittest.mock import patch, Mock, MagicMock
+import subprocess
+import tempfile
+import pathlib
+
+
+class TestRepomixLoader:
+ """Test the repomix_loader function"""
+
+ def test_invalid_url_formats(self):
+ """Test that invalid URL formats raise ValueError"""
+ invalid_urls = [
+ "not-a-url",
+ "ftp://example.com/repo",
+ "file://local/path",
+ "http://example.com", # No path
+ "", # Empty string
+ ]
+
+ for invalid_url in invalid_urls:
+ with pytest.raises(ValueError) as ex:
+ repomix_loader(invalid_url)
+ assert "Repository URL must start with https://, ssh://, or git@" in str(ex.value)
+
+ @patch('llm_fragments_repomix.shutil.which')
+ def test_repomix_not_installed(self, mock_which):
+ """Test error when repomix is not installed"""
+ mock_which.return_value = None
+
+ with pytest.raises(ValueError) as ex:
+ repomix_loader("https://github.com/user/repo")
+ assert "repomix command not found" in str(ex.value)
+ assert "https://github.com/yamadashy/repomix" in str(ex.value)
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_git_clone_failure(self, mock_run, mock_which):
+ """Test handling of git clone failures"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ # Mock git clone failure
+ mock_run.side_effect = subprocess.CalledProcessError(
+ 1, ["git", "clone"], stderr="Repository not found"
+ )
+
+ with pytest.raises(ValueError) as ex:
+ repomix_loader("https://github.com/user/nonexistent")
+ assert "Failed to clone repository" in str(ex.value)
+ assert "Repository not found" in str(ex.value)
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_repomix_failure(self, mock_run, mock_which):
+ """Test handling of repomix execution failures"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ # Mock successful git clone, failed repomix
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ raise subprocess.CalledProcessError(
+ 1, ["repomix"], stderr="Repomix error"
+ )
+
+ mock_run.side_effect = mock_run_side_effect
+
+ with pytest.raises(ValueError) as ex:
+ repomix_loader("https://github.com/user/repo")
+ assert "Failed to run repomix" in str(ex.value)
+ assert "Repomix error" in str(ex.value)
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_successful_repomix_execution(self, mock_run, mock_which):
+ """Test successful repomix execution"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ # Mock repomix output
+ repomix_output = """
+# Repository Structure
+
+## File: main.py
+```python
+print("Hello, World!")
+```
+
+## File: README.md
+```markdown
+# Test Project
+This is a test project.
+```
+"""
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ return Mock(stdout=repomix_output, stderr="")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ fragments = repomix_loader("https://github.com/user/repo")
+
+ assert len(fragments) == 1
+ assert str(fragments[0]) == repomix_output
+ assert fragments[0].source == "repomix:https://github.com/user/repo"
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_different_url_formats(self, mock_run, mock_which):
+ """Test that different valid URL formats work"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ return Mock(stdout="test output", stderr="")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ valid_urls = [
+ "https://github.com/user/repo",
+ "https://git.sr.ht/~user/repo",
+ "ssh://git@github.com:user/repo.git",
+ "git@github.com:user/repo.git",
+ ]
+
+ for url in valid_urls:
+ fragments = repomix_loader(url)
+ assert len(fragments) == 1
+ assert fragments[0].source == f"repomix:{url}"
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_subprocess_calls(self, mock_run, mock_which):
+ """Test that subprocess calls are made correctly"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ return Mock(stdout="test output", stderr="")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ repo_url = "https://github.com/user/repo"
+ repomix_loader(repo_url)
+
+ # Check git clone call
+ git_call = mock_run.call_args_list[0]
+ assert git_call[0][0][:3] == ["git", "clone", "--depth=1"]
+ assert git_call[0][0][3] == repo_url
+ assert git_call[1]["check"] is True
+ assert git_call[1]["capture_output"] is True
+ assert git_call[1]["text"] is True
+
+ # Check repomix call
+ repomix_call = mock_run.call_args_list[1]
+ assert repomix_call[0][0][:2] == ["repomix", "--stdout"]
+ assert repomix_call[1]["check"] is True
+ assert repomix_call[1]["capture_output"] is True
+ assert repomix_call[1]["text"] is True
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_generic_exception_handling(self, mock_run, mock_which):
+ """Test handling of generic exceptions"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ raise OSError("Permission denied")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ with pytest.raises(ValueError) as ex:
+ repomix_loader("https://github.com/user/repo")
+ assert "Error processing repository" in str(ex.value)
+ assert "Permission denied" in str(ex.value)
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ @patch('llm_fragments_repomix.tempfile.TemporaryDirectory')
+ def test_temporary_directory_cleanup(self, mock_tempdir, mock_run, mock_which):
+ """Test that temporary directory is properly cleaned up"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ # Create a mock context manager for TemporaryDirectory
+ mock_context = MagicMock()
+ mock_context.__enter__.return_value = "/tmp/test_dir"
+ mock_context.__exit__.return_value = None
+ mock_tempdir.return_value = mock_context
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ return Mock(stdout="test output", stderr="")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ repomix_loader("https://github.com/user/repo")
+
+ # Verify that the temporary directory context manager was used
+ mock_tempdir.assert_called_once()
+ mock_context.__enter__.assert_called_once()
+ mock_context.__exit__.assert_called_once()
+
+
+class TestRepomixArgumentParsing:
+ """Test argument parsing for colon-separated options"""
+
+ def test_parse_fragment_string_url_only(self):
+ """Test parsing URL without arguments"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo")
+ assert url == "https://github.com/user/repo"
+ assert args == {}
+
+ def test_parse_fragment_string_with_compress(self):
+ """Test parsing URL with compress flag"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo:compress")
+ assert url == "https://github.com/user/repo"
+ assert args == {"compress": True}
+
+ def test_parse_fragment_string_with_include_patterns(self):
+ """Test parsing URL with include patterns"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo:include=*.ts,*.js")
+ assert url == "https://github.com/user/repo"
+ assert args == {"include": "*.ts,*.js"}
+
+ def test_parse_fragment_string_with_ignore_patterns(self):
+ """Test parsing URL with ignore patterns"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo:ignore=*.log,tmp/")
+ assert url == "https://github.com/user/repo"
+ assert args == {"ignore": "*.log,tmp/"}
+
+ def test_parse_fragment_string_multiple_args(self):
+ """Test parsing URL with multiple arguments"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo:compress:include=*.py:ignore=tests/")
+ assert url == "https://github.com/user/repo"
+ assert args == {
+ "compress": True,
+ "include": "*.py",
+ "ignore": "tests/"
+ }
+
+ def test_parse_fragment_string_complex_patterns(self):
+ """Test parsing with complex glob patterns"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo:include=src/**/*.ts,**/*.md:ignore=**/*.test.ts,node_modules/")
+ assert url == "https://github.com/user/repo"
+ assert args == {
+ "include": "src/**/*.ts,**/*.md",
+ "ignore": "**/*.test.ts,node_modules/"
+ }
+
+ def test_parse_fragment_string_boolean_flags(self):
+ """Test parsing various boolean flags"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo:compress:remove-comments:remove-empty-lines")
+ assert url == "https://github.com/user/repo"
+ assert args == {
+ "compress": True,
+ "remove-comments": True,
+ "remove-empty-lines": True
+ }
+
+ def test_parse_fragment_string_output_options(self):
+ """Test parsing output-related options"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo:style=markdown:output-show-line-numbers")
+ assert url == "https://github.com/user/repo"
+ assert args == {
+ "style": "markdown",
+ "output-show-line-numbers": True
+ }
+
+ def test_parse_fragment_string_ssh_url(self):
+ """Test parsing SSH URLs with arguments"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("git@github.com:user/repo.git:compress:include=*.py")
+ assert url == "git@github.com:user/repo.git"
+ assert args == {
+ "compress": True,
+ "include": "*.py"
+ }
+
+ def test_parse_fragment_string_ssh_protocol_url(self):
+ """Test parsing SSH protocol URLs with arguments"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("ssh://git@github.com:user/repo.git:compress")
+ assert url == "ssh://git@github.com:user/repo.git"
+ assert args == {"compress": True}
+
+ def test_parse_fragment_string_empty_args(self):
+ """Test parsing with empty argument values"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo:include=")
+ assert url == "https://github.com/user/repo"
+ assert args == {"include": ""}
+
+ def test_parse_fragment_string_duplicate_args(self):
+ """Test parsing with duplicate arguments (last one wins)"""
+ from llm_fragments_repomix import parse_fragment_string
+
+ url, args = parse_fragment_string("https://github.com/user/repo:include=*.ts:include=*.js")
+ assert url == "https://github.com/user/repo"
+ assert args == {"include": "*.js"}
+
+ def test_build_repomix_command_no_args(self):
+ """Test building repomix command without arguments"""
+ from llm_fragments_repomix import build_repomix_command
+
+ cmd = build_repomix_command("/tmp/repo", {})
+ assert cmd == ["repomix", "--stdout", "/tmp/repo"]
+
+ def test_build_repomix_command_with_compress(self):
+ """Test building repomix command with compress flag"""
+ from llm_fragments_repomix import build_repomix_command
+
+ cmd = build_repomix_command("/tmp/repo", {"compress": True})
+ assert cmd == ["repomix", "--stdout", "--compress", "/tmp/repo"]
+
+ def test_build_repomix_command_with_include(self):
+ """Test building repomix command with include patterns"""
+ from llm_fragments_repomix import build_repomix_command
+
+ cmd = build_repomix_command("/tmp/repo", {"include": "*.ts,*.js"})
+ assert cmd == ["repomix", "--stdout", "--include", "*.ts,*.js", "/tmp/repo"]
+
+ def test_build_repomix_command_with_ignore(self):
+ """Test building repomix command with ignore patterns"""
+ from llm_fragments_repomix import build_repomix_command
+
+ cmd = build_repomix_command("/tmp/repo", {"ignore": "*.log,tmp/"})
+ assert cmd == ["repomix", "--stdout", "--ignore", "*.log,tmp/", "/tmp/repo"]
+
+ def test_build_repomix_command_multiple_args(self):
+ """Test building repomix command with multiple arguments"""
+ from llm_fragments_repomix import build_repomix_command
+
+ args = {
+ "compress": True,
+ "include": "*.py",
+ "ignore": "tests/",
+ "remove-comments": True
+ }
+ cmd = build_repomix_command("/tmp/repo", args)
+ expected = ["repomix", "--stdout", "--compress", "--include", "*.py", "--ignore", "tests/", "--remove-comments", "/tmp/repo"]
+ assert cmd == expected
+
+ def test_build_repomix_command_with_style(self):
+ """Test building repomix command with style option"""
+ from llm_fragments_repomix import build_repomix_command
+
+ cmd = build_repomix_command("/tmp/repo", {"style": "markdown"})
+ assert cmd == ["repomix", "--stdout", "--style", "markdown", "/tmp/repo"]
+
+ def test_build_repomix_command_boolean_flags(self):
+ """Test building repomix command with various boolean flags"""
+ from llm_fragments_repomix import build_repomix_command
+
+ args = {
+ "compress": True,
+ "remove-comments": True,
+ "remove-empty-lines": True,
+ "output-show-line-numbers": True,
+ "no-file-summary": True
+ }
+ cmd = build_repomix_command("/tmp/repo", args)
+ expected = [
+ "repomix", "--stdout",
+ "--compress",
+ "--remove-comments",
+ "--remove-empty-lines",
+ "--output-show-line-numbers",
+ "--no-file-summary",
+ "/tmp/repo"
+ ]
+ assert cmd == expected
+
+ def test_build_repomix_command_unsupported_arg(self):
+ """Test that unsupported arguments are ignored"""
+ from llm_fragments_repomix import build_repomix_command
+
+ args = {
+ "compress": True,
+ "unsupported-option": "value",
+ "include": "*.py"
+ }
+ cmd = build_repomix_command("/tmp/repo", args)
+ expected = ["repomix", "--stdout", "--compress", "--include", "*.py", "/tmp/repo"]
+ assert cmd == expected
+
+ def test_build_repomix_command_empty_string_values(self):
+ """Test that empty string values are handled correctly"""
+ from llm_fragments_repomix import build_repomix_command
+
+ args = {
+ "include": "",
+ "ignore": "*.log"
+ }
+ cmd = build_repomix_command("/tmp/repo", args)
+ # Empty include should be ignored, ignore should be included
+ expected = ["repomix", "--stdout", "--ignore", "*.log", "/tmp/repo"]
+ assert cmd == expected
+
+
+class TestRepomixIntegrationWithArguments:
+ """Integration tests for repomix loader with arguments"""
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_repomix_loader_with_compress(self, mock_run, mock_which):
+ """Test repomix loader with compress argument"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ # Verify compress flag is passed
+ assert "--compress" in cmd
+ return Mock(stdout="compressed output", stderr="")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ fragments = repomix_loader("https://github.com/user/repo:compress")
+ assert len(fragments) == 1
+ assert str(fragments[0]) == "compressed output"
+ assert fragments[0].source == "repomix:https://github.com/user/repo:compress"
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_repomix_loader_with_include_patterns(self, mock_run, mock_which):
+ """Test repomix loader with include patterns"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ # Verify include patterns are passed
+ assert "--include" in cmd
+ include_idx = cmd.index("--include")
+ assert cmd[include_idx + 1] == "*.ts,*.js"
+ return Mock(stdout="filtered output", stderr="")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ fragments = repomix_loader("https://github.com/user/repo:include=*.ts,*.js")
+ assert len(fragments) == 1
+ assert str(fragments[0]) == "filtered output"
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_repomix_loader_with_multiple_args(self, mock_run, mock_which):
+ """Test repomix loader with multiple arguments"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ # Verify multiple arguments are passed
+ assert "--compress" in cmd
+ assert "--include" in cmd
+ assert "--ignore" in cmd
+ include_idx = cmd.index("--include")
+ assert cmd[include_idx + 1] == "*.py"
+ ignore_idx = cmd.index("--ignore")
+ assert cmd[ignore_idx + 1] == "tests/"
+ return Mock(stdout="multi-arg output", stderr="")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ fragments = repomix_loader("https://github.com/user/repo:compress:include=*.py:ignore=tests/")
+ assert len(fragments) == 1
+ assert str(fragments[0]) == "multi-arg output"
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_repomix_loader_with_ssh_url_and_args(self, mock_run, mock_which):
+ """Test repomix loader with SSH URL and arguments"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ assert "--compress" in cmd
+ return Mock(stdout="ssh output", stderr="")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ fragments = repomix_loader("git@github.com:user/repo.git:compress")
+ assert len(fragments) == 1
+ assert str(fragments[0]) == "ssh output"
+ assert fragments[0].source == "repomix:git@github.com:user/repo.git:compress"
+
+ @patch('llm_fragments_repomix.shutil.which')
+ @patch('llm_fragments_repomix.subprocess.run')
+ def test_repomix_loader_preserves_original_source(self, mock_run, mock_which):
+ """Test that the original fragment string is preserved in source"""
+ mock_which.return_value = "/usr/bin/repomix"
+
+ def mock_run_side_effect(cmd, **kwargs):
+ if cmd[0] == "git":
+ return Mock(stdout="", stderr="")
+ elif cmd[0] == "repomix":
+ return Mock(stdout="test output", stderr="")
+
+ mock_run.side_effect = mock_run_side_effect
+
+ original_string = "https://github.com/user/repo:compress:include=*.ts,*.js:ignore=*.md"
+ fragments = repomix_loader(original_string)
+ assert fragments[0].source == f"repomix:{original_string}"