janito 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- janito/__init__.py +5 -0
- janito/__main__.py +143 -120
- janito/callbacks.py +130 -0
- janito/cli.py +202 -0
- janito/config.py +63 -100
- janito/data/instructions.txt +6 -0
- janito/test_file.py +4 -0
- janito/token_report.py +73 -0
- janito/tools/__init__.py +10 -0
- janito/tools/decorators.py +84 -0
- janito/tools/delete_file.py +44 -0
- janito/tools/find_files.py +154 -0
- janito/tools/search_text.py +197 -0
- janito/tools/str_replace_editor/__init__.py +6 -0
- janito/tools/str_replace_editor/editor.py +43 -0
- janito/tools/str_replace_editor/handlers.py +338 -0
- janito/tools/str_replace_editor/utils.py +88 -0
- {janito-0.8.0.dist-info/licenses → janito-0.9.0.dist-info}/LICENSE +2 -2
- janito-0.9.0.dist-info/METADATA +9 -0
- janito-0.9.0.dist-info/RECORD +23 -0
- {janito-0.8.0.dist-info → janito-0.9.0.dist-info}/WHEEL +2 -1
- janito-0.9.0.dist-info/entry_points.txt +2 -0
- janito-0.9.0.dist-info/top_level.txt +1 -0
- janito/agents/__init__.py +0 -22
- janito/agents/agent.py +0 -25
- janito/agents/claudeai.py +0 -41
- janito/agents/deepseekai.py +0 -47
- janito/change/applied_blocks.py +0 -34
- janito/change/applier.py +0 -167
- janito/change/edit_blocks.py +0 -148
- janito/change/finder.py +0 -72
- janito/change/request.py +0 -144
- janito/change/validator.py +0 -87
- janito/change/view/content.py +0 -63
- janito/change/view/diff.py +0 -44
- janito/change/view/panels.py +0 -201
- janito/change/view/sections.py +0 -69
- janito/change/view/styling.py +0 -140
- janito/change/view/summary.py +0 -37
- janito/change/view/themes.py +0 -62
- janito/change/view/viewer.py +0 -59
- janito/cli/__init__.py +0 -2
- janito/cli/commands.py +0 -68
- janito/cli/functions.py +0 -66
- janito/common.py +0 -133
- janito/data/change_prompt.txt +0 -81
- janito/data/system_prompt.txt +0 -3
- janito/qa.py +0 -56
- janito/version.py +0 -23
- janito/workspace/__init__.py +0 -8
- janito/workspace/analysis.py +0 -121
- janito/workspace/models.py +0 -97
- janito/workspace/show.py +0 -115
- janito/workspace/stats.py +0 -42
- janito/workspace/workset.py +0 -135
- janito/workspace/workspace.py +0 -335
- janito-0.8.0.dist-info/METADATA +0 -106
- janito-0.8.0.dist-info/RECORD +0 -40
- janito-0.8.0.dist-info/entry_points.txt +0 -2
janito/config.py
CHANGED
@@ -1,100 +1,63 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
"""
|
53
|
-
self.
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
"""Set the workspace directory"""
|
65
|
-
self.workspace_dir = path if path is not None else Path.cwd()
|
66
|
-
|
67
|
-
def set_raw(self, enabled: bool) -> None:
|
68
|
-
"""Set raw output mode.
|
69
|
-
|
70
|
-
Args:
|
71
|
-
enabled: True to enable raw output mode, False to disable
|
72
|
-
"""
|
73
|
-
self.raw = enabled
|
74
|
-
|
75
|
-
def set_auto_apply(self, enabled: bool) -> None:
|
76
|
-
"""Set auto apply mode for changes.
|
77
|
-
|
78
|
-
Args:
|
79
|
-
enabled: True to enable auto apply mode, False to disable
|
80
|
-
"""
|
81
|
-
self.auto_apply = enabled
|
82
|
-
|
83
|
-
def set_tui(self, enabled: bool) -> None:
|
84
|
-
"""Set Text User Interface mode.
|
85
|
-
|
86
|
-
Args:
|
87
|
-
enabled: True to enable TUI mode, False to disable
|
88
|
-
"""
|
89
|
-
self.tui = enabled
|
90
|
-
|
91
|
-
def set_skip_work(self, enabled: bool) -> None:
|
92
|
-
"""Set whether to skip scanning the workspace directory.
|
93
|
-
|
94
|
-
Args:
|
95
|
-
enabled: True to skip workspace directory, False to include it
|
96
|
-
"""
|
97
|
-
self.skip_work = enabled
|
98
|
-
|
99
|
-
# Create a singleton instance
|
100
|
-
config = ConfigManager.get_instance()
|
1
|
+
"""
|
2
|
+
Configuration module for Janito.
|
3
|
+
Provides a singleton Config class to access configuration values.
|
4
|
+
"""
|
5
|
+
import os
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Optional
|
8
|
+
import typer
|
9
|
+
|
10
|
+
class Config:
|
11
|
+
"""Singleton configuration class for Janito."""
|
12
|
+
_instance = None
|
13
|
+
|
14
|
+
def __new__(cls):
|
15
|
+
if cls._instance is None:
|
16
|
+
cls._instance = super(Config, cls).__new__(cls)
|
17
|
+
cls._instance._workspace_dir = os.getcwd()
|
18
|
+
cls._instance._debug_mode = False
|
19
|
+
return cls._instance
|
20
|
+
|
21
|
+
@property
|
22
|
+
def workspace_dir(self) -> str:
|
23
|
+
"""Get the current workspace directory."""
|
24
|
+
return self._workspace_dir
|
25
|
+
|
26
|
+
@workspace_dir.setter
|
27
|
+
def workspace_dir(self, path: str) -> None:
|
28
|
+
"""Set the workspace directory."""
|
29
|
+
# Convert to absolute path if not already
|
30
|
+
if not os.path.isabs(path):
|
31
|
+
path = os.path.normpath(os.path.abspath(path))
|
32
|
+
else:
|
33
|
+
# Ensure Windows paths are properly formatted
|
34
|
+
path = os.path.normpath(path)
|
35
|
+
|
36
|
+
# Check if the directory exists
|
37
|
+
if not os.path.isdir(path):
|
38
|
+
create_dir = typer.confirm(f"Workspace directory does not exist: {path}\nDo you want to create it?")
|
39
|
+
if create_dir:
|
40
|
+
try:
|
41
|
+
os.makedirs(path, exist_ok=True)
|
42
|
+
print(f"Created workspace directory: {path}")
|
43
|
+
except Exception as e:
|
44
|
+
raise ValueError(f"Failed to create workspace directory: {str(e)}")
|
45
|
+
else:
|
46
|
+
raise ValueError(f"Workspace directory does not exist: {path}")
|
47
|
+
|
48
|
+
self._workspace_dir = path
|
49
|
+
|
50
|
+
@property
|
51
|
+
def debug_mode(self) -> bool:
|
52
|
+
"""Get the debug mode status."""
|
53
|
+
return self._debug_mode
|
54
|
+
|
55
|
+
@debug_mode.setter
|
56
|
+
def debug_mode(self, value: bool) -> None:
|
57
|
+
"""Set the debug mode status."""
|
58
|
+
self._debug_mode = value
|
59
|
+
|
60
|
+
# Convenience function to get the config instance
|
61
|
+
def get_config() -> Config:
|
62
|
+
"""Get the singleton Config instance."""
|
63
|
+
return Config()
|
@@ -0,0 +1,6 @@
|
|
1
|
+
You are a helpful AI assistant, working in a repository.
|
2
|
+
Answer the user's questions accurately and concisely.
|
3
|
+
|
4
|
+
When using str_replace_editor be aware that our files starting path is "." .
|
5
|
+
|
6
|
+
Before performing any action, always check the structure of the project for paths that might be related to the request.
|
janito/test_file.py
ADDED
janito/token_report.py
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
"""
|
2
|
+
Module for generating token usage reports.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from rich.console import Console
|
6
|
+
from claudine.token_tracking import MODEL_PRICING, DEFAULT_MODEL
|
7
|
+
|
8
|
+
def generate_token_report(agent, verbose=False):
|
9
|
+
"""
|
10
|
+
Generate a token usage report.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
agent: The Claude agent instance
|
14
|
+
verbose: Whether to show detailed token usage information
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
None - prints the report to the console
|
18
|
+
"""
|
19
|
+
console = Console()
|
20
|
+
usage = agent.get_token_usage()
|
21
|
+
text_usage = usage.text_usage
|
22
|
+
tools_usage = usage.tools_usage
|
23
|
+
|
24
|
+
if verbose:
|
25
|
+
total_usage = usage.total_usage
|
26
|
+
|
27
|
+
# Get the pricing model
|
28
|
+
pricing = MODEL_PRICING.get(DEFAULT_MODEL)
|
29
|
+
|
30
|
+
# Calculate costs manually
|
31
|
+
text_input_cost = pricing.input_tokens.calculate_cost(text_usage.input_tokens)
|
32
|
+
text_output_cost = pricing.output_tokens.calculate_cost(text_usage.output_tokens)
|
33
|
+
tools_input_cost = pricing.input_tokens.calculate_cost(tools_usage.input_tokens)
|
34
|
+
tools_output_cost = pricing.output_tokens.calculate_cost(tools_usage.output_tokens)
|
35
|
+
|
36
|
+
# Format costs
|
37
|
+
format_cost = lambda cost: f"{cost * 100:.2f}¢" if cost < 1.0 else f"${cost:.6f}"
|
38
|
+
|
39
|
+
console.print("\n[bold blue]Detailed Token Usage:[/bold blue]")
|
40
|
+
console.print(f"Text Input tokens: {text_usage.input_tokens}")
|
41
|
+
console.print(f"Text Output tokens: {text_usage.output_tokens}")
|
42
|
+
console.print(f"Text Total tokens: {text_usage.input_tokens + text_usage.output_tokens}")
|
43
|
+
console.print(f"Tool Input tokens: {tools_usage.input_tokens}")
|
44
|
+
console.print(f"Tool Output tokens: {tools_usage.output_tokens}")
|
45
|
+
console.print(f"Tool Total tokens: {tools_usage.input_tokens + tools_usage.output_tokens}")
|
46
|
+
console.print(f"Total tokens: {total_usage.input_tokens + total_usage.output_tokens}")
|
47
|
+
|
48
|
+
console.print("\n[bold blue]Pricing Information:[/bold blue]")
|
49
|
+
console.print(f"Input pricing: ${pricing.input_tokens.cost_per_million_tokens}/million tokens")
|
50
|
+
console.print(f"Output pricing: ${pricing.output_tokens.cost_per_million_tokens}/million tokens")
|
51
|
+
console.print(f"Text Input cost: {format_cost(text_input_cost)}")
|
52
|
+
console.print(f"Text Output cost: {format_cost(text_output_cost)}")
|
53
|
+
console.print(f"Text Total cost: {format_cost(text_input_cost + text_output_cost)}")
|
54
|
+
console.print(f"Tool Input cost: {format_cost(tools_input_cost)}")
|
55
|
+
console.print(f"Tool Output cost: {format_cost(tools_output_cost)}")
|
56
|
+
console.print(f"Tool Total cost: {format_cost(tools_input_cost + tools_output_cost)}")
|
57
|
+
console.print(f"Total cost: {format_cost(text_input_cost + text_output_cost + tools_input_cost + tools_output_cost)}")
|
58
|
+
|
59
|
+
# Display per-tool breakdown if available
|
60
|
+
if usage.by_tool:
|
61
|
+
console.print("\n[bold blue]Per-Tool Breakdown:[/bold blue]")
|
62
|
+
for tool_name, tool_usage in usage.by_tool.items():
|
63
|
+
tool_input_cost = pricing.input_tokens.calculate_cost(tool_usage.input_tokens)
|
64
|
+
tool_output_cost = pricing.output_tokens.calculate_cost(tool_usage.output_tokens)
|
65
|
+
console.print(f" Tool: {tool_name}")
|
66
|
+
console.print(f" Input tokens: {tool_usage.input_tokens}")
|
67
|
+
console.print(f" Output tokens: {tool_usage.output_tokens}")
|
68
|
+
console.print(f" Total tokens: {tool_usage.input_tokens + tool_usage.output_tokens}")
|
69
|
+
console.print(f" Total cost: {format_cost(tool_input_cost + tool_output_cost)}")
|
70
|
+
else:
|
71
|
+
total_tokens = text_usage.input_tokens + text_usage.output_tokens + tools_usage.input_tokens + tools_usage.output_tokens
|
72
|
+
cost_info = agent.get_cost()
|
73
|
+
console.rule(f"[bold blue]Total tokens: {total_tokens} | Cost: {cost_info.format_total_cost()}[/bold blue]")
|
janito/tools/__init__.py
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
"""
|
2
|
+
Janito tools package.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .str_replace_editor import str_replace_editor
|
6
|
+
from .find_files import find_files
|
7
|
+
from .delete_file import delete_file
|
8
|
+
from .search_text import search_text
|
9
|
+
|
10
|
+
__all__ = ["str_replace_editor", "find_files", "delete_file", "search_text"]
|
@@ -0,0 +1,84 @@
|
|
1
|
+
"""
|
2
|
+
Decorators for janito tools.
|
3
|
+
"""
|
4
|
+
import functools
|
5
|
+
import inspect
|
6
|
+
import string
|
7
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
8
|
+
|
9
|
+
|
10
|
+
class ToolMetaFormatter(string.Formatter):
|
11
|
+
"""Custom string formatter that handles conditional expressions in format strings."""
|
12
|
+
|
13
|
+
def get_value(self, key, args, kwargs):
|
14
|
+
"""Override to handle conditional expressions."""
|
15
|
+
if key in kwargs:
|
16
|
+
return kwargs[key]
|
17
|
+
|
18
|
+
# Try to evaluate the key as a Python expression
|
19
|
+
try:
|
20
|
+
# Create a safe local namespace with only the parameters
|
21
|
+
return eval(key, {"__builtins__": {}}, kwargs)
|
22
|
+
except Exception:
|
23
|
+
return f"[{key}]"
|
24
|
+
|
25
|
+
|
26
|
+
def tool_meta(label: str):
|
27
|
+
"""
|
28
|
+
Decorator to add metadata to a tool function.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
label: A format string that can reference function parameters.
|
32
|
+
Example: "Finding files {pattern}, on {root_dir}"
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Decorated function with metadata attached
|
36
|
+
"""
|
37
|
+
def decorator(func: Callable):
|
38
|
+
@functools.wraps(func)
|
39
|
+
def wrapper(*args, **kwargs):
|
40
|
+
return func(*args, **kwargs)
|
41
|
+
|
42
|
+
# Attach metadata to the function
|
43
|
+
wrapper._tool_meta = {
|
44
|
+
'label': label
|
45
|
+
}
|
46
|
+
|
47
|
+
return wrapper
|
48
|
+
|
49
|
+
return decorator
|
50
|
+
|
51
|
+
|
52
|
+
def format_tool_label(func: Callable, tool_input: Dict[str, Any]) -> Optional[str]:
|
53
|
+
"""
|
54
|
+
Format the tool label using the function's parameters.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
func: The tool function
|
58
|
+
tool_input: Input parameters for the tool
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Formatted label string or None if no label is defined
|
62
|
+
"""
|
63
|
+
if not hasattr(func, '_tool_meta') or 'label' not in func._tool_meta:
|
64
|
+
return None
|
65
|
+
|
66
|
+
# Get the label template
|
67
|
+
label_template = func._tool_meta['label']
|
68
|
+
|
69
|
+
# Special handling for str_replace_editor which uses **kwargs
|
70
|
+
if func.__name__ == 'str_replace_editor':
|
71
|
+
# Extract command and file_path from tool_input if they exist
|
72
|
+
command = tool_input.get('command', 'unknown')
|
73
|
+
file_path = tool_input.get('file_path', '')
|
74
|
+
|
75
|
+
# Simple string replacement for the common case
|
76
|
+
if '{command}' in label_template and '{file_path}' in label_template:
|
77
|
+
return label_template.replace('{command}', command).replace('{file_path}', file_path)
|
78
|
+
|
79
|
+
# Format the label with the parameters
|
80
|
+
try:
|
81
|
+
formatter = ToolMetaFormatter()
|
82
|
+
return formatter.format(label_template, **tool_input)
|
83
|
+
except Exception as e:
|
84
|
+
return f"{func.__name__}"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
"""
|
2
|
+
Tool for deleting files through the claudine agent.
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Dict, Any, Tuple
|
7
|
+
from janito.config import get_config
|
8
|
+
from janito.tools.str_replace_editor.utils import normalize_path
|
9
|
+
from janito.tools.decorators import tool_meta
|
10
|
+
|
11
|
+
|
12
|
+
@tool_meta(label="Deleting file {file_path}")
|
13
|
+
def delete_file(
|
14
|
+
file_path: str,
|
15
|
+
) -> Tuple[str, bool]:
|
16
|
+
"""
|
17
|
+
Delete an existing file.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
file_path: Path to the file to delete, relative to the workspace directory
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
A tuple containing (message, is_error)
|
24
|
+
"""
|
25
|
+
# Normalize the file path
|
26
|
+
path = normalize_path(file_path)
|
27
|
+
|
28
|
+
# Convert to Path object for better path handling
|
29
|
+
path_obj = Path(path)
|
30
|
+
|
31
|
+
# Check if the file exists
|
32
|
+
if not path_obj.exists():
|
33
|
+
return (f"File {path} does not exist.", True)
|
34
|
+
|
35
|
+
# Check if it's a directory
|
36
|
+
if path_obj.is_dir():
|
37
|
+
return (f"{path} is a directory, not a file. Use delete_directory for directories.", True)
|
38
|
+
|
39
|
+
# Delete the file
|
40
|
+
try:
|
41
|
+
path_obj.unlink()
|
42
|
+
return (f"Successfully deleted file {path}", False)
|
43
|
+
except Exception as e:
|
44
|
+
return (f"Error deleting file {path}: {str(e)}", True)
|
@@ -0,0 +1,154 @@
|
|
1
|
+
import os
|
2
|
+
import fnmatch
|
3
|
+
from typing import List, Dict, Any, Tuple
|
4
|
+
from janito.tools.decorators import tool_meta
|
5
|
+
|
6
|
+
|
7
|
+
@tool_meta(label="Finding files {pattern}, on {root_dir} ({recursive and 'recursive' or 'non-recursive'}, {respect_gitignore and 'respecting gitignore' or 'ignoring gitignore'})")
|
8
|
+
def find_files(pattern: str, root_dir: str = ".", recursive: bool = True, respect_gitignore: bool = True) -> Tuple[str, bool]:
|
9
|
+
"""
|
10
|
+
Find files whose name matches a glob pattern.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
pattern: pattern to match file names against
|
14
|
+
root_dir: root directory to start search from (default: current directory)
|
15
|
+
recursive: Whether to search recursively in subdirectories (default: True)
|
16
|
+
respect_gitignore: Whether to respect .gitignore files (default: True)
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
A tuple containing (message, is_error)
|
20
|
+
"""
|
21
|
+
try:
|
22
|
+
# Convert to absolute path if relative
|
23
|
+
abs_root = os.path.abspath(root_dir)
|
24
|
+
|
25
|
+
if not os.path.isdir(abs_root):
|
26
|
+
return f"Error: Directory '{root_dir}' does not exist", True
|
27
|
+
|
28
|
+
matching_files = []
|
29
|
+
|
30
|
+
# Get gitignore patterns if needed
|
31
|
+
ignored_patterns = []
|
32
|
+
if respect_gitignore:
|
33
|
+
ignored_patterns = _get_gitignore_patterns(abs_root)
|
34
|
+
|
35
|
+
# Use os.walk for more intuitive recursive behavior
|
36
|
+
if recursive:
|
37
|
+
for dirpath, dirnames, filenames in os.walk(abs_root):
|
38
|
+
# Skip ignored directories
|
39
|
+
if respect_gitignore:
|
40
|
+
dirnames[:] = [d for d in dirnames if not _is_ignored(os.path.join(dirpath, d), ignored_patterns, abs_root)]
|
41
|
+
|
42
|
+
for filename in fnmatch.filter(filenames, pattern):
|
43
|
+
file_path = os.path.join(dirpath, filename)
|
44
|
+
|
45
|
+
# Skip ignored files
|
46
|
+
if respect_gitignore and _is_ignored(file_path, ignored_patterns, abs_root):
|
47
|
+
continue
|
48
|
+
|
49
|
+
# Convert to relative path from root_dir
|
50
|
+
rel_path = os.path.relpath(file_path, abs_root)
|
51
|
+
matching_files.append(rel_path)
|
52
|
+
else:
|
53
|
+
# Non-recursive mode - only search in the specified directory
|
54
|
+
for filename in fnmatch.filter(os.listdir(abs_root), pattern):
|
55
|
+
file_path = os.path.join(abs_root, filename)
|
56
|
+
|
57
|
+
# Skip ignored files
|
58
|
+
if respect_gitignore and _is_ignored(file_path, ignored_patterns, abs_root):
|
59
|
+
continue
|
60
|
+
|
61
|
+
if os.path.isfile(file_path):
|
62
|
+
# Convert to relative path from root_dir
|
63
|
+
rel_path = os.path.relpath(file_path, abs_root)
|
64
|
+
matching_files.append(rel_path)
|
65
|
+
|
66
|
+
# Sort the files for consistent output
|
67
|
+
matching_files.sort()
|
68
|
+
|
69
|
+
if matching_files:
|
70
|
+
file_list = "\n- ".join(matching_files)
|
71
|
+
return f"Found {len(matching_files)} files matching pattern '{pattern}':\n- {file_list}\n{len(matching_files)}", False
|
72
|
+
else:
|
73
|
+
return f"No files found matching pattern '{pattern}' in '{root_dir}'", False
|
74
|
+
|
75
|
+
except Exception as e:
|
76
|
+
return f"Error finding files: {str(e)}", True
|
77
|
+
|
78
|
+
|
79
|
+
def _get_gitignore_patterns(root_dir: str) -> List[str]:
|
80
|
+
"""
|
81
|
+
Get patterns from .gitignore files.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
root_dir: Root directory to start from
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
List of gitignore patterns
|
88
|
+
"""
|
89
|
+
patterns = []
|
90
|
+
|
91
|
+
# Check for .gitignore in the root directory
|
92
|
+
gitignore_path = os.path.join(root_dir, '.gitignore')
|
93
|
+
if os.path.isfile(gitignore_path):
|
94
|
+
try:
|
95
|
+
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
96
|
+
for line in f:
|
97
|
+
line = line.strip()
|
98
|
+
# Skip empty lines and comments
|
99
|
+
if line and not line.startswith('#'):
|
100
|
+
patterns.append(line)
|
101
|
+
except Exception:
|
102
|
+
pass
|
103
|
+
|
104
|
+
# Add common patterns that are always ignored
|
105
|
+
common_patterns = [
|
106
|
+
'.git/', '.venv/', 'venv/', '__pycache__/', '*.pyc',
|
107
|
+
'*.pyo', '*.pyd', '.DS_Store', '*.so', '*.egg-info/'
|
108
|
+
]
|
109
|
+
patterns.extend(common_patterns)
|
110
|
+
|
111
|
+
return patterns
|
112
|
+
|
113
|
+
|
114
|
+
def _is_ignored(path: str, patterns: List[str], root_dir: str) -> bool:
|
115
|
+
"""
|
116
|
+
Check if a path should be ignored based on gitignore patterns.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
path: Path to check
|
120
|
+
patterns: List of gitignore patterns
|
121
|
+
root_dir: Root directory for relative paths
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
True if the path should be ignored, False otherwise
|
125
|
+
"""
|
126
|
+
# Get the relative path from the root directory
|
127
|
+
rel_path = os.path.relpath(path, root_dir)
|
128
|
+
|
129
|
+
# Convert to forward slashes for consistency with gitignore patterns
|
130
|
+
rel_path = rel_path.replace(os.sep, '/')
|
131
|
+
|
132
|
+
# Add trailing slash for directories
|
133
|
+
if os.path.isdir(path) and not rel_path.endswith('/'):
|
134
|
+
rel_path += '/'
|
135
|
+
|
136
|
+
for pattern in patterns:
|
137
|
+
# Handle negation patterns (those starting with !)
|
138
|
+
if pattern.startswith('!'):
|
139
|
+
continue # Skip negation patterns for simplicity
|
140
|
+
|
141
|
+
# Handle directory-specific patterns (those ending with /)
|
142
|
+
if pattern.endswith('/'):
|
143
|
+
if os.path.isdir(path) and fnmatch.fnmatch(rel_path, pattern + '*'):
|
144
|
+
return True
|
145
|
+
|
146
|
+
# Handle file patterns
|
147
|
+
if fnmatch.fnmatch(rel_path, pattern):
|
148
|
+
return True
|
149
|
+
|
150
|
+
# Handle patterns without wildcards as path prefixes
|
151
|
+
if '*' not in pattern and '?' not in pattern and rel_path.startswith(pattern):
|
152
|
+
return True
|
153
|
+
|
154
|
+
return False
|