From 9c68de43fa37e71aa8da3b29d41ba6c33adf7fcf Mon Sep 17 00:00:00 2001 From: MQ Date: Wed, 26 Feb 2025 13:26:14 +0100 Subject: [PATCH 1/6] fix ux outside repo --- src/ctxify/cli.py | 6 +-- src/ctxify/main.py | 104 +++++++++++++++++++++------------------------ uv.lock | 2 +- 3 files changed, 53 insertions(+), 59 deletions(-) diff --git a/src/ctxify/cli.py b/src/ctxify/cli.py index 3b7cda8..1c10a0f 100644 --- a/src/ctxify/cli.py +++ b/src/ctxify/cli.py @@ -1,8 +1,8 @@ import click +from typing import Optional from ctxify.main import copy_to_clipboard, print_git_contents, interactive_file_selection - @click.command() @click.argument('directory', default='.', type=click.Path(exists=True, file_okay=False)) @click.option( @@ -14,8 +14,9 @@ @click.option( '-s', '--structure', is_flag=True, help='Output only the project structure without file contents' ) -def main(directory, md, interactive, structure): +def main(directory: str, md: bool, interactive: bool, structure: bool) -> None: """A tool to print all tracked files in a git repository directory with tree structure and copy to clipboard.""" + output: str if interactive: output = interactive_file_selection(directory, include_md=md) else: @@ -23,6 +24,5 @@ def main(directory, md, interactive, structure): if copy_to_clipboard(output): click.echo('Project context copied to clipboard!') - if __name__ == '__main__': main() diff --git a/src/ctxify/main.py b/src/ctxify/main.py index 9fee5f9..6ee9256 100644 --- a/src/ctxify/main.py +++ b/src/ctxify/main.py @@ -1,6 +1,8 @@ import os import subprocess +import sys from pathlib import Path +from typing import List, Tuple, Optional, Dict, Union from prompt_toolkit import PromptSession from prompt_toolkit.completion import FuzzyWordCompleter @@ -28,12 +30,24 @@ '.lock', } +def check_git_repo(root_dir: str) -> bool: + """Check if the given directory is within a git repository.""" + try: + subprocess.check_output( + ['git', 'rev-parse', '--show-toplevel'], + text=True, + cwd=root_dir, + stderr=subprocess.STDOUT + ) + return True + except subprocess.CalledProcessError: + return False -def print_filtered_tree(files, output_lines=None): +def print_filtered_tree(files: List[str], output_lines: Optional[List[str]] = None) -> List[str]: """Builds a tree structure from a list of file paths""" if output_lines is None: output_lines = [] - tree = {} + tree: Dict[str, Union[None, Dict]] = {} for file_path in files: parts = file_path.split('/') current = tree @@ -41,7 +55,7 @@ def print_filtered_tree(files, output_lines=None): current = current.setdefault(part, {}) current[parts[-1]] = None - def render_tree(node, prefix=''): + def render_tree(node: Dict[str, Union[None, Dict]], prefix: str = '') -> None: if not isinstance(node, dict): return items = sorted(node.keys()) @@ -54,16 +68,16 @@ def render_tree(node, prefix=''): render_tree(tree) return output_lines - -def get_git_files(root_dir, include_md=False): +def get_git_files(root_dir: str, include_md: bool = False) -> Tuple[List[str], List[str], List[str]]: """Get all tracked files from a specific directory within a git repo using git ls-files""" + target_dir = Path(root_dir).resolve() try: - # Resolve the repo root and target directory + # Resolve the repo root repo_root = Path(subprocess.check_output( ['git', 'rev-parse', '--show-toplevel'], - text=True + text=True, + cwd=target_dir ).strip()) - target_dir = Path(root_dir).resolve() # Ensure the target directory is within the repo if not str(target_dir).startswith(str(repo_root)): @@ -82,16 +96,14 @@ def get_git_files(root_dir, include_md=False): dir_files = [] for f in all_files: if rel_str == '.' or f.startswith(rel_str + '/') or f == rel_str: - # Adjust path to be relative to target_dir if rel_str != '.' and f.startswith(rel_str + '/'): dir_files.append(f[len(rel_str) + 1:]) else: dir_files.append(f) - # Filter code files (exclude non-code and optionally .md files) + # Filter code files code_files = [ - f - for f in dir_files + f for f in dir_files if not ( f in NON_CODE_PATTERNS or any(f.endswith(ext) for ext in NON_CODE_PATTERNS) @@ -104,8 +116,7 @@ def get_git_files(root_dir, include_md=False): except Exception as e: return [f'Error processing directory: {e}'], [], [] - -def copy_to_clipboard(text): +def copy_to_clipboard(text: str) -> bool: """Copy text to system clipboard using xclip""" try: subprocess.run( @@ -119,44 +130,42 @@ def copy_to_clipboard(text): print("Warning: xclip not installed. Install it with 'sudo apt install xclip'") return False - -def estimate_tokens(text): +def estimate_tokens(text: str) -> int: """Estimate token count using 1 token ≈ 4 characters""" char_count = len(text) - token_count = char_count // 4 # Integer division for approximate tokens - return token_count - + return char_count // 4 -def interactive_file_selection(root_dir='.', include_md=False): +def interactive_file_selection(root_dir: str = '.', include_md: bool = False) -> str: """Interactively select files to include with fuzzy tab autocompletion""" - output_lines = [] - tree_lines = [] + if not check_git_repo(root_dir): + print(f"Error: {root_dir} is not within a git repository. This tool requires a git repository.") + sys.exit(1) + + output_lines: List[str] = [] + tree_lines: List[str] = [] - # Get all files errors, all_files, code_files = get_git_files(root_dir, include_md=include_md) if errors: tree_lines.extend(errors) print('\n'.join(tree_lines)) return '\n'.join(tree_lines) - # Set up fuzzy autocompletion with all files completer = FuzzyWordCompleter(all_files) session = PromptSession(completer=completer, complete_while_typing=True) - # Show the file tree tree_lines.append(f'\nFiles Available in Context (from {root_dir}):') print_filtered_tree(all_files, tree_lines) print('\n'.join(tree_lines)) print("\nEnter file paths to include (press Enter twice to finish):") - selected_files = [] + selected_files: List[str] = [] while True: try: file_path = session.prompt("> ") - if not file_path: # Empty input (Enter pressed) - if not selected_files: # First empty, keep prompting + if not file_path: + if not selected_files: continue - break # Second empty, finish + break if file_path in all_files: if file_path not in selected_files: selected_files.append(file_path) @@ -168,7 +177,6 @@ def interactive_file_selection(root_dir='.', include_md=False): except KeyboardInterrupt: break - # Build output with selected files output_lines.extend(tree_lines) output_lines.append('\n' + '-' * 50 + '\n') target_dir = Path(root_dir).resolve() @@ -184,45 +192,34 @@ def interactive_file_selection(root_dir='.', include_md=False): output_lines.append(f'Error reading file: {e}') output_lines.append('') - # Calculate token count and append only to stdout full_output = '\n'.join(output_lines) token_count = estimate_tokens(full_output) token_info = f'\nApproximate token count: {token_count} (based on 1 token ≈ 4 chars)' tree_lines.append(token_info) - print('\n'.join(tree_lines[len(tree_lines) - 1:])) # Print only token info + print('\n'.join(tree_lines[len(tree_lines) - 1:])) - return '\n'.join(output_lines) + return full_output - -def print_git_contents(root_dir='.', include_md=False, structure_only=False): +def print_git_contents(root_dir: str = '.', include_md: bool = False, structure_only: bool = False) -> str: """Build output for clipboard, print tree with all files and token count to stdout""" - output_lines = [] - tree_lines = [] - - # Resolve paths - repo_root = Path(subprocess.check_output( - ['git', 'rev-parse', '--show-toplevel'], - text=True - ).strip()) - target_dir = Path(root_dir).resolve() - if not str(target_dir).startswith(str(repo_root)): - tree_lines.append(f'Error: Directory {root_dir} is outside the git repository') - print('\n'.join(tree_lines)) - return '\n'.join(tree_lines) + if not check_git_repo(root_dir): + print(f"Error: {root_dir} is not within a git repository. This tool requires a git repository.") + sys.exit(1) + + output_lines: List[str] = [] + tree_lines: List[str] = [] - # Get all files and code files + target_dir = Path(root_dir).resolve() errors, all_files, code_files = get_git_files(root_dir, include_md=include_md) if errors: tree_lines.extend(errors) print('\n'.join(tree_lines)) return '\n'.join(tree_lines) - # Print tree with all files to stdout and clipboard tree_lines.append(f'\nFiles Included in Context (from {root_dir}):') print_filtered_tree(all_files, tree_lines) output_lines.extend(tree_lines) - # If not structure-only, add separator and contents of code files if not structure_only: output_lines.append('\n' + '-' * 50 + '\n') for file_path in code_files: @@ -237,13 +234,10 @@ def print_git_contents(root_dir='.', include_md=False, structure_only=False): output_lines.append(f'Error reading file: {e}') output_lines.append('') - # Calculate token count and append only to stdout full_output = '\n'.join(output_lines) token_count = estimate_tokens(full_output) token_info = f'\nApproximate token count: {token_count} (based on 1 token ≈ 4 chars)' tree_lines.append(token_info) - # Print to stdout print('\n'.join(tree_lines)) - - return '\n'.join(output_lines) + return full_output diff --git a/uv.lock b/uv.lock index 9433034..b1359c7 100644 --- a/uv.lock +++ b/uv.lock @@ -25,7 +25,7 @@ wheels = [ [[package]] name = "ctxify" -version = "0.1.2" +version = "0.1.4" source = { editable = "." } dependencies = [ { name = "click" }, From 1d18e6005775f8bc2303ea98d5a8d6eee78b8b6a Mon Sep 17 00:00:00 2001 From: MQ Date: Wed, 26 Feb 2025 13:31:43 +0100 Subject: [PATCH 2/6] format lint fix --- src/ctxify/cli.py | 24 +++++++++--- src/ctxify/main.py | 93 ++++++++++++++++++++++++++++++---------------- 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/src/ctxify/cli.py b/src/ctxify/cli.py index 1c10a0f..2cd9476 100644 --- a/src/ctxify/cli.py +++ b/src/ctxify/cli.py @@ -1,7 +1,12 @@ + import click -from typing import Optional -from ctxify.main import copy_to_clipboard, print_git_contents, interactive_file_selection +from ctxify.main import ( + copy_to_clipboard, + interactive_file_selection, + print_git_contents, +) + @click.command() @click.argument('directory', default='.', type=click.Path(exists=True, file_okay=False)) @@ -9,10 +14,16 @@ '--md', '-md', is_flag=True, help='Include README and other .md files in output' ) @click.option( - '-i', '--interactive', is_flag=True, help='Interactively select files to include with tab autocompletion' + '-i', + '--interactive', + is_flag=True, + help='Interactively select files to include with tab autocompletion', ) @click.option( - '-s', '--structure', is_flag=True, help='Output only the project structure without file contents' + '-s', + '--structure', + is_flag=True, + help='Output only the project structure without file contents', ) def main(directory: str, md: bool, interactive: bool, structure: bool) -> None: """A tool to print all tracked files in a git repository directory with tree structure and copy to clipboard.""" @@ -20,9 +31,12 @@ def main(directory: str, md: bool, interactive: bool, structure: bool) -> None: if interactive: output = interactive_file_selection(directory, include_md=md) else: - output = print_git_contents(root_dir=directory, include_md=md, structure_only=structure) + output = print_git_contents( + root_dir=directory, include_md=md, structure_only=structure + ) if copy_to_clipboard(output): click.echo('Project context copied to clipboard!') + if __name__ == '__main__': main() diff --git a/src/ctxify/main.py b/src/ctxify/main.py index 6ee9256..e56cb4f 100644 --- a/src/ctxify/main.py +++ b/src/ctxify/main.py @@ -1,8 +1,8 @@ -import os import subprocess import sys from pathlib import Path -from typing import List, Tuple, Optional, Dict, Union +from typing import Dict, List, Optional, Tuple, Union + from prompt_toolkit import PromptSession from prompt_toolkit.completion import FuzzyWordCompleter @@ -30,6 +30,7 @@ '.lock', } + def check_git_repo(root_dir: str) -> bool: """Check if the given directory is within a git repository.""" try: @@ -37,13 +38,16 @@ def check_git_repo(root_dir: str) -> bool: ['git', 'rev-parse', '--show-toplevel'], text=True, cwd=root_dir, - stderr=subprocess.STDOUT + stderr=subprocess.STDOUT, ) return True except subprocess.CalledProcessError: return False -def print_filtered_tree(files: List[str], output_lines: Optional[List[str]] = None) -> List[str]: + +def print_filtered_tree( + files: List[str], output_lines: Optional[List[str]] = None +) -> List[str]: """Builds a tree structure from a list of file paths""" if output_lines is None: output_lines = [] @@ -52,8 +56,10 @@ def print_filtered_tree(files: List[str], output_lines: Optional[List[str]] = No parts = file_path.split('/') current = tree for part in parts[:-1]: - current = current.setdefault(part, {}) - current[parts[-1]] = None + if current is not None: + current = current.setdefault(part, {}) + if current is not None: + current[parts[-1]] = None def render_tree(node: Dict[str, Union[None, Dict]], prefix: str = '') -> None: if not isinstance(node, dict): @@ -62,48 +68,57 @@ def render_tree(node: Dict[str, Union[None, Dict]], prefix: str = '') -> None: for i, item in enumerate(items): is_last = i == len(items) - 1 output_lines.append(f'{prefix}{"└── " if is_last else "├── "}{item}') - if isinstance(node[item], dict): - render_tree(node[item], prefix + (' ' if is_last else '│ ')) + next_node = node[item] + if isinstance(next_node, dict): + render_tree(next_node, prefix + (' ' if is_last else '│ ')) render_tree(tree) return output_lines -def get_git_files(root_dir: str, include_md: bool = False) -> Tuple[List[str], List[str], List[str]]: + +def get_git_files( + root_dir: str, include_md: bool = False +) -> Tuple[List[str], List[str], List[str]]: """Get all tracked files from a specific directory within a git repo using git ls-files""" target_dir = Path(root_dir).resolve() try: # Resolve the repo root - repo_root = Path(subprocess.check_output( - ['git', 'rev-parse', '--show-toplevel'], - text=True, - cwd=target_dir - ).strip()) + repo_root = Path( + subprocess.check_output( + ['git', 'rev-parse', '--show-toplevel'], text=True, cwd=target_dir + ).strip() + ) # Ensure the target directory is within the repo if not str(target_dir).startswith(str(repo_root)): - return [f'Error: Directory {root_dir} is outside the git repository'], [], [] + return ( + [f'Error: Directory {root_dir} is outside the git repository'], + [], + [], + ) # Get all tracked files using git ls-files all_files = subprocess.check_output( - ['git', 'ls-files'], - cwd=repo_root, - text=True + ['git', 'ls-files'], cwd=repo_root, text=True ).splitlines() # Filter files to those under the target directory - rel_path = target_dir.relative_to(repo_root) if target_dir != repo_root else Path('.') + rel_path = ( + target_dir.relative_to(repo_root) if target_dir != repo_root else Path('.') + ) rel_str = str(rel_path) dir_files = [] for f in all_files: if rel_str == '.' or f.startswith(rel_str + '/') or f == rel_str: if rel_str != '.' and f.startswith(rel_str + '/'): - dir_files.append(f[len(rel_str) + 1:]) + dir_files.append(f[len(rel_str) + 1 :]) else: dir_files.append(f) # Filter code files code_files = [ - f for f in dir_files + f + for f in dir_files if not ( f in NON_CODE_PATTERNS or any(f.endswith(ext) for ext in NON_CODE_PATTERNS) @@ -116,6 +131,7 @@ def get_git_files(root_dir: str, include_md: bool = False) -> Tuple[List[str], L except Exception as e: return [f'Error processing directory: {e}'], [], [] + def copy_to_clipboard(text: str) -> bool: """Copy text to system clipboard using xclip""" try: @@ -130,15 +146,19 @@ def copy_to_clipboard(text: str) -> bool: print("Warning: xclip not installed. Install it with 'sudo apt install xclip'") return False + def estimate_tokens(text: str) -> int: """Estimate token count using 1 token ≈ 4 characters""" char_count = len(text) return char_count // 4 + def interactive_file_selection(root_dir: str = '.', include_md: bool = False) -> str: """Interactively select files to include with fuzzy tab autocompletion""" if not check_git_repo(root_dir): - print(f"Error: {root_dir} is not within a git repository. This tool requires a git repository.") + print( + f'Error: {root_dir} is not within a git repository. This tool requires a git repository.' + ) sys.exit(1) output_lines: List[str] = [] @@ -156,12 +176,12 @@ def interactive_file_selection(root_dir: str = '.', include_md: bool = False) -> tree_lines.append(f'\nFiles Available in Context (from {root_dir}):') print_filtered_tree(all_files, tree_lines) print('\n'.join(tree_lines)) - print("\nEnter file paths to include (press Enter twice to finish):") + print('\nEnter file paths to include (press Enter twice to finish):') selected_files: List[str] = [] while True: try: - file_path = session.prompt("> ") + file_path = session.prompt('> ') if not file_path: if not selected_files: continue @@ -169,11 +189,11 @@ def interactive_file_selection(root_dir: str = '.', include_md: bool = False) -> if file_path in all_files: if file_path not in selected_files: selected_files.append(file_path) - print(f"Added: {file_path}") + print(f'Added: {file_path}') else: - print(f"Already added: {file_path}") + print(f'Already added: {file_path}') else: - print(f"File not found: {file_path}") + print(f'File not found: {file_path}') except KeyboardInterrupt: break @@ -194,16 +214,23 @@ def interactive_file_selection(root_dir: str = '.', include_md: bool = False) -> full_output = '\n'.join(output_lines) token_count = estimate_tokens(full_output) - token_info = f'\nApproximate token count: {token_count} (based on 1 token ≈ 4 chars)' + token_info = ( + f'\nApproximate token count: {token_count} (based on 1 token ≈ 4 chars)' + ) tree_lines.append(token_info) - print('\n'.join(tree_lines[len(tree_lines) - 1:])) + print('\n'.join(tree_lines[len(tree_lines) - 1 :])) return full_output -def print_git_contents(root_dir: str = '.', include_md: bool = False, structure_only: bool = False) -> str: + +def print_git_contents( + root_dir: str = '.', include_md: bool = False, structure_only: bool = False +) -> str: """Build output for clipboard, print tree with all files and token count to stdout""" if not check_git_repo(root_dir): - print(f"Error: {root_dir} is not within a git repository. This tool requires a git repository.") + print( + f'Error: {root_dir} is not within a git repository. This tool requires a git repository.' + ) sys.exit(1) output_lines: List[str] = [] @@ -236,7 +263,9 @@ def print_git_contents(root_dir: str = '.', include_md: bool = False, structure_ full_output = '\n'.join(output_lines) token_count = estimate_tokens(full_output) - token_info = f'\nApproximate token count: {token_count} (based on 1 token ≈ 4 chars)' + token_info = ( + f'\nApproximate token count: {token_count} (based on 1 token ≈ 4 chars)' + ) tree_lines.append(token_info) print('\n'.join(tree_lines)) From 445fd35dac8777be25650812c8d7070ff4781da4 Mon Sep 17 00:00:00 2001 From: MQ Date: Wed, 26 Feb 2025 16:18:23 +0100 Subject: [PATCH 3/6] add test and ci --- .github/workflows/code_checks.yml | 45 ++++++++++++ Makefile | 21 ++++-- pyproject.toml | 2 + src/ctxify/cli.py | 1 - tests/test_cli.py | 48 +++++++++++++ tests/test_main.py | 89 ++++++++++++++++++++++++ uv.lock | 112 +++++++++++++++++++++++++++++- 7 files changed, 310 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/code_checks.yml create mode 100644 tests/test_cli.py create mode 100644 tests/test_main.py diff --git a/.github/workflows/code_checks.yml b/.github/workflows/code_checks.yml new file mode 100644 index 0000000..370f777 --- /dev/null +++ b/.github/workflows/code_checks.yml @@ -0,0 +1,45 @@ +name: Code checks + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout code + uses: actions/checkout@v4 + + # Set up Python 3.13 (matches .python-version) + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + # Install uv for dependency management + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + # Install project dependencies with uv + - name: Install dependencies + run: | + uv sync --frozen + + # Run linting (using Makefile's lint target) + - name: Lint code + run: | + make lint + + # Run tests (using Makefile's test target) + - name: Run tests + run: | + make test diff --git a/Makefile b/Makefile index ea5b87c..a6be5f0 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,20 @@ -.PHONY: format lint bump +.PHONY: format lint bump test + +CODE_DIRS := src/ tests/ # Format code with ruff format: - ruff format src/ # Apply formatting - ruff format src/ --check # Check formatting first + uv run ruff format $(CODE_DIRS) # Apply formatting + uv run ruff format $(CODE_DIRS) --check # Check formatting first # Lint code with ruff lint: - ruff check src/ --fix # Check and auto-fix where possible - ruff check src/ # Final check after fixes + uv run ruff check $(CODE_DIRS) --fix # Check and auto-fix where possible + uv run ruff check $(CODE_DIRS) # Final check after fixes # Bump version (patch, minor, major) bump: - @python bump_version.py $(TYPE) + uv run python bump_version.py $(TYPE) # Default to patch if no TYPE is specified TYPE ?= patch @@ -26,3 +28,10 @@ bump-minor: bump bump-major: TYPE = major bump-major: bump + +# Run tests with pytest +test: + uv run pytest tests/ -v + +# Combined check for CI +check: format lint test diff --git a/pyproject.toml b/pyproject.toml index 36343a5..e1800be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,5 +30,7 @@ quote-style = "single" [dependency-groups] dev = [ + "pytest>=8.3.4", + "pytest-mock>=3.14.0", "ruff>=0.9.7", ] diff --git a/src/ctxify/cli.py b/src/ctxify/cli.py index 2cd9476..5049a7c 100644 --- a/src/ctxify/cli.py +++ b/src/ctxify/cli.py @@ -1,4 +1,3 @@ - import click from ctxify.main import ( diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..7a8c58f --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,48 @@ +from unittest.mock import patch + +from click.testing import CliRunner + +from ctxify.cli import main + + +def test_cli_default(): + runner = CliRunner() + with patch('ctxify.cli.copy_to_clipboard', return_value=True) as mock_copy: + with patch('ctxify.cli.print_git_contents', return_value='mock output'): + result = runner.invoke(main, ['.']) + assert result.exit_code == 0 + assert 'Project context copied to clipboard!' in result.output + mock_copy.assert_called_once_with('mock output') + + +def test_cli_with_md_flag(): + runner = CliRunner() + with patch('ctxify.cli.copy_to_clipboard', return_value=True): + with patch('ctxify.cli.print_git_contents') as mock_print: + result = runner.invoke(main, ['.', '--md']) + assert result.exit_code == 0 + mock_print.assert_called_once_with( + root_dir='.', include_md=True, structure_only=False + ) + + +def test_cli_interactive(): + runner = CliRunner() + with patch('ctxify.cli.copy_to_clipboard', return_value=True): + with patch( + 'ctxify.cli.interactive_file_selection', return_value='interactive output' + ) as mock_interactive: + result = runner.invoke(main, ['.', '-i']) + assert result.exit_code == 0 + mock_interactive.assert_called_once_with('.', include_md=False) + + +def test_cli_structure_only(): + runner = CliRunner() + with patch('ctxify.cli.copy_to_clipboard', return_value=True): + with patch('ctxify.cli.print_git_contents') as mock_print: + result = runner.invoke(main, ['.', '-s']) + assert result.exit_code == 0 + mock_print.assert_called_once_with( + root_dir='.', include_md=False, structure_only=True + ) diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..2846328 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,89 @@ +import subprocess +import sys +from unittest.mock import MagicMock, patch + +from ctxify.main import ( + check_git_repo, + copy_to_clipboard, + estimate_tokens, + get_git_files, + print_filtered_tree, + print_git_contents, +) + + +def test_check_git_repo_success(tmp_path): + with patch('subprocess.check_output', return_value='/path/to/repo\n'): + assert check_git_repo(str(tmp_path)) is True + + +def test_check_git_repo_failure(tmp_path): + with patch( + 'subprocess.check_output', side_effect=subprocess.CalledProcessError(1, 'git') + ): + assert check_git_repo(str(tmp_path)) is False + + +def test_print_filtered_tree(): + files = ['src/main.py', 'src/utils/helper.py', 'README.md'] + output = print_filtered_tree(files) + expected = [ + '├── README.md', + '└── src', + ' ├── main.py', + ' └── utils', + ' └── helper.py', + ] + assert output == expected + + +def test_get_git_files_success(tmp_path): + with patch('subprocess.check_output') as mock_output: + mock_output.side_effect = [ + f'{tmp_path}\n', # git rev-parse --show-toplevel + 'file1.py\nfile2.txt\nREADME.md\n', # git ls-files + ] + errors, all_files, code_files = get_git_files(str(tmp_path)) + assert errors == [] + assert all_files == ['README.md', 'file1.py', 'file2.txt'] + assert code_files == ['file1.py'] + + +def test_get_git_files_not_in_repo(tmp_path): + outside_path = tmp_path / 'outside' + outside_path.mkdir() + repo_root = '/some/other/repo' # A path outside tmp_path + with patch('subprocess.check_output', return_value=f'{repo_root}\n'): + errors, all_files, code_files = get_git_files(str(outside_path)) + assert len(errors) == 1 + assert 'outside the git repository' in errors[0] + assert all_files == [] + assert code_files == [] + + +def test_copy_to_clipboard_success(): + with patch('subprocess.run', return_value=MagicMock(returncode=0)) as mock_run: + assert copy_to_clipboard('test text') is True + mock_run.assert_called_once() + + +def test_copy_to_clipboard_xclip_missing(): + with patch('subprocess.run', side_effect=FileNotFoundError): + with patch('sys.stdout', new_callable=lambda: sys.stdout): # Suppress print + assert copy_to_clipboard('test text') is False + + +def test_estimate_tokens(): + assert estimate_tokens('Hello world') == 2 # 11 chars / 4 = 2 tokens + assert estimate_tokens('') == 0 + + +def test_print_git_contents_structure_only(tmp_path, mocker): + mocker.patch('ctxify.main.check_git_repo', return_value=True) + mocker.patch( + 'ctxify.main.get_git_files', return_value=([], ['file1.py'], ['file1.py']) + ) + mocker.patch('sys.stdout', new_callable=lambda: sys.stdout) # Suppress print + output = print_git_contents(str(tmp_path), structure_only=True) + assert 'file1.py' in output + assert '└── file1.py' in output # Check tree structure diff --git a/uv.lock b/uv.lock index b1359c7..6a05ebf 100644 --- a/uv.lock +++ b/uv.lock @@ -35,6 +35,8 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "pytest" }, + { name = "pytest-mock" }, { name = "ruff" }, ] @@ -46,7 +48,20 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.9.7" }] +dev = [ + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-mock", specifier = ">=3.14.0" }, + { name = "ruff", specifier = ">=0.9.7" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] [[package]] name = "gitdb" @@ -72,6 +87,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + [[package]] name = "prompt-toolkit" version = "3.0.50" @@ -84,6 +126,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, ] +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + [[package]] name = "ruff" version = "0.9.7" @@ -118,6 +189,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + [[package]] name = "wcwidth" version = "0.2.13" From 6ca9c7f6f41580ba116dbbb086e8bb4b5386197c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kopeck=C3=BD?= Date: Wed, 26 Feb 2025 16:21:03 +0100 Subject: [PATCH 4/6] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index df5f0ac..594524d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ **Turn Your Git Repo into a Clipboard-Ready Context Machine** ![GitHub release (latest by date)](https://img.shields.io/github/v/release/MQ37/ctxify?color=brightgreen) +![Code Checks](https://github.com/mq37/ctxify/actions/workflows/code_checks.yml/badge.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) **`ctxify`** is a sleek CLI tool that grabs all tracked files in your Git repository, builds a neat tree structure, and copies everything—code and all—to your clipboard with a single command. Perfect for sharing project context, debugging, or feeding your code straight into AI assistants. It even gives you an approximate token count for fun! 🚀 From ee467c88d092d5cc0b5b4c1d8fc67dbdea463b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kopeck=C3=BD?= Date: Wed, 26 Feb 2025 16:23:31 +0100 Subject: [PATCH 5/6] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 594524d..68b2ad6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # ctxify 🎉 **Turn Your Git Repo into a Clipboard-Ready Context Machine** +*Built mostly with the help of xAI's Grok model—AI-powered coding at its finest!* + ![GitHub release (latest by date)](https://img.shields.io/github/v/release/MQ37/ctxify?color=brightgreen) ![Code Checks](https://github.com/mq37/ctxify/actions/workflows/code_checks.yml/badge.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) From 64cc8ef1a98727282f7869c9409c0197a6b2f596 Mon Sep 17 00:00:00 2001 From: MQ Date: Wed, 26 Feb 2025 16:23:48 +0100 Subject: [PATCH 6/6] up ver --- pyproject.toml | 2 +- src/ctxify/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e1800be..36f58d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ctxify" -version = "0.1.4" +version = "0.1.5" description = "A tool to print git repository files with tree structure" readme = "README.md" requires-python = ">=3.8" diff --git a/src/ctxify/__init__.py b/src/ctxify/__init__.py index 7525d19..66a87bb 100644 --- a/src/ctxify/__init__.py +++ b/src/ctxify/__init__.py @@ -1 +1 @@ -__version__ = '0.1.4' +__version__ = '0.1.5'