cognify-code 0.2.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.
- ai_code_assistant/__init__.py +14 -0
- ai_code_assistant/agent/__init__.py +63 -0
- ai_code_assistant/agent/code_agent.py +461 -0
- ai_code_assistant/agent/code_generator.py +388 -0
- ai_code_assistant/agent/code_reviewer.py +365 -0
- ai_code_assistant/agent/diff_engine.py +308 -0
- ai_code_assistant/agent/file_manager.py +300 -0
- ai_code_assistant/agent/intent_classifier.py +284 -0
- ai_code_assistant/chat/__init__.py +11 -0
- ai_code_assistant/chat/agent_session.py +156 -0
- ai_code_assistant/chat/session.py +165 -0
- ai_code_assistant/cli.py +1571 -0
- ai_code_assistant/config.py +149 -0
- ai_code_assistant/editor/__init__.py +8 -0
- ai_code_assistant/editor/diff_handler.py +270 -0
- ai_code_assistant/editor/file_editor.py +350 -0
- ai_code_assistant/editor/prompts.py +146 -0
- ai_code_assistant/generator/__init__.py +7 -0
- ai_code_assistant/generator/code_gen.py +265 -0
- ai_code_assistant/generator/prompts.py +114 -0
- ai_code_assistant/git/__init__.py +6 -0
- ai_code_assistant/git/commit_generator.py +130 -0
- ai_code_assistant/git/manager.py +203 -0
- ai_code_assistant/llm.py +111 -0
- ai_code_assistant/providers/__init__.py +23 -0
- ai_code_assistant/providers/base.py +124 -0
- ai_code_assistant/providers/cerebras.py +97 -0
- ai_code_assistant/providers/factory.py +148 -0
- ai_code_assistant/providers/google.py +103 -0
- ai_code_assistant/providers/groq.py +111 -0
- ai_code_assistant/providers/ollama.py +86 -0
- ai_code_assistant/providers/openai.py +114 -0
- ai_code_assistant/providers/openrouter.py +130 -0
- ai_code_assistant/py.typed +0 -0
- ai_code_assistant/refactor/__init__.py +20 -0
- ai_code_assistant/refactor/analyzer.py +189 -0
- ai_code_assistant/refactor/change_plan.py +172 -0
- ai_code_assistant/refactor/multi_file_editor.py +346 -0
- ai_code_assistant/refactor/prompts.py +175 -0
- ai_code_assistant/retrieval/__init__.py +19 -0
- ai_code_assistant/retrieval/chunker.py +215 -0
- ai_code_assistant/retrieval/indexer.py +236 -0
- ai_code_assistant/retrieval/search.py +239 -0
- ai_code_assistant/reviewer/__init__.py +7 -0
- ai_code_assistant/reviewer/analyzer.py +278 -0
- ai_code_assistant/reviewer/prompts.py +113 -0
- ai_code_assistant/utils/__init__.py +18 -0
- ai_code_assistant/utils/file_handler.py +155 -0
- ai_code_assistant/utils/formatters.py +259 -0
- cognify_code-0.2.0.dist-info/METADATA +383 -0
- cognify_code-0.2.0.dist-info/RECORD +55 -0
- cognify_code-0.2.0.dist-info/WHEEL +5 -0
- cognify_code-0.2.0.dist-info/entry_points.txt +3 -0
- cognify_code-0.2.0.dist-info/licenses/LICENSE +22 -0
- cognify_code-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Configuration management for AI Code Assistant."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from pydantic_settings import BaseSettings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LLMConfig(BaseModel):
|
|
12
|
+
"""LLM configuration settings."""
|
|
13
|
+
provider: str = "ollama" # ollama, google, groq, cerebras, openrouter, openai
|
|
14
|
+
model: str = "deepseek-coder:6.7b"
|
|
15
|
+
api_key: Optional[str] = None # Can also use environment variables
|
|
16
|
+
base_url: Optional[str] = None # Optional custom base URL
|
|
17
|
+
temperature: float = 0.1
|
|
18
|
+
max_tokens: int = 4096
|
|
19
|
+
timeout: int = 120
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ReviewConfig(BaseModel):
|
|
23
|
+
"""Code review configuration settings."""
|
|
24
|
+
severity_levels: List[str] = Field(default=["critical", "warning", "suggestion"])
|
|
25
|
+
categories: List[str] = Field(
|
|
26
|
+
default=["bugs", "security", "performance", "style", "best_practices"]
|
|
27
|
+
)
|
|
28
|
+
max_file_size_kb: int = 500
|
|
29
|
+
include_line_numbers: bool = True
|
|
30
|
+
include_confidence: bool = True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GenerationConfig(BaseModel):
|
|
34
|
+
"""Code generation configuration settings."""
|
|
35
|
+
include_type_hints: bool = True
|
|
36
|
+
include_docstrings: bool = True
|
|
37
|
+
default_mode: str = "function"
|
|
38
|
+
max_lines: int = 500
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OutputConfig(BaseModel):
|
|
42
|
+
"""Output configuration settings."""
|
|
43
|
+
default_format: str = "console"
|
|
44
|
+
use_colors: bool = True
|
|
45
|
+
output_dir: str = "./output"
|
|
46
|
+
verbose: bool = False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RetrievalConfig(BaseModel):
|
|
50
|
+
"""Codebase retrieval configuration settings."""
|
|
51
|
+
embedding_model: str = "all-MiniLM-L6-v2"
|
|
52
|
+
persist_directory: str = ".ai-assistant-index"
|
|
53
|
+
collection_name: str = "codebase"
|
|
54
|
+
chunk_size: int = 50
|
|
55
|
+
chunk_overlap: int = 10
|
|
56
|
+
max_file_size_kb: int = 1024
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class EditorConfig(BaseModel):
|
|
60
|
+
"""File editing configuration settings."""
|
|
61
|
+
create_backup: bool = True
|
|
62
|
+
show_diff: bool = True
|
|
63
|
+
max_file_size_kb: int = 500
|
|
64
|
+
auto_format: bool = False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RefactorConfig(BaseModel):
|
|
68
|
+
"""Multi-file refactoring configuration settings."""
|
|
69
|
+
max_files: int = 20
|
|
70
|
+
max_file_size_kb: int = 500
|
|
71
|
+
create_backup: bool = True
|
|
72
|
+
require_confirmation: bool = True
|
|
73
|
+
show_plan: bool = True
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LanguageConfig(BaseModel):
|
|
77
|
+
"""Language-specific configuration."""
|
|
78
|
+
extensions: List[str]
|
|
79
|
+
comment_style: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Config(BaseSettings):
|
|
83
|
+
"""Main configuration class."""
|
|
84
|
+
llm: LLMConfig = Field(default_factory=LLMConfig)
|
|
85
|
+
review: ReviewConfig = Field(default_factory=ReviewConfig)
|
|
86
|
+
generation: GenerationConfig = Field(default_factory=GenerationConfig)
|
|
87
|
+
output: OutputConfig = Field(default_factory=OutputConfig)
|
|
88
|
+
retrieval: RetrievalConfig = Field(default_factory=RetrievalConfig)
|
|
89
|
+
editor: EditorConfig = Field(default_factory=EditorConfig)
|
|
90
|
+
refactor: RefactorConfig = Field(default_factory=RefactorConfig)
|
|
91
|
+
languages: Dict[str, LanguageConfig] = Field(default_factory=dict)
|
|
92
|
+
|
|
93
|
+
class Config:
|
|
94
|
+
env_prefix = "AI_ASSIST_"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def find_config_file() -> Optional[Path]:
|
|
98
|
+
"""Find configuration file in standard locations."""
|
|
99
|
+
locations = [
|
|
100
|
+
Path.cwd() / "config.yaml",
|
|
101
|
+
Path.cwd() / ".ai-code-assistant.yaml",
|
|
102
|
+
Path.home() / ".ai-code-assistant" / "config.yaml",
|
|
103
|
+
Path.home() / ".config" / "ai-code-assistant" / "config.yaml",
|
|
104
|
+
]
|
|
105
|
+
for path in locations:
|
|
106
|
+
if path.exists():
|
|
107
|
+
return path
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def load_config(config_path: Optional[Path] = None) -> Config:
|
|
112
|
+
"""Load configuration from file or use defaults."""
|
|
113
|
+
if config_path is None:
|
|
114
|
+
config_path = find_config_file()
|
|
115
|
+
|
|
116
|
+
if config_path and config_path.exists():
|
|
117
|
+
with open(config_path) as f:
|
|
118
|
+
data = yaml.safe_load(f)
|
|
119
|
+
return _parse_config(data)
|
|
120
|
+
|
|
121
|
+
return Config()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _parse_config(data: Dict[str, Any]) -> Config:
|
|
125
|
+
"""Parse configuration dictionary into Config object."""
|
|
126
|
+
languages = {}
|
|
127
|
+
if "languages" in data:
|
|
128
|
+
for lang_name, lang_data in data["languages"].items():
|
|
129
|
+
languages[lang_name] = LanguageConfig(**lang_data)
|
|
130
|
+
data["languages"] = languages
|
|
131
|
+
|
|
132
|
+
return Config(**data)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_language_by_extension(config: Config, file_path: Path) -> Optional[str]:
|
|
136
|
+
"""Detect language from file extension."""
|
|
137
|
+
ext = file_path.suffix.lower()
|
|
138
|
+
for lang_name, lang_config in config.languages.items():
|
|
139
|
+
if ext in lang_config.extensions:
|
|
140
|
+
return lang_name
|
|
141
|
+
|
|
142
|
+
# Fallback detection
|
|
143
|
+
extension_map = {
|
|
144
|
+
".py": "python", ".pyw": "python",
|
|
145
|
+
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript",
|
|
146
|
+
".ts": "typescript", ".tsx": "typescript",
|
|
147
|
+
".java": "java", ".go": "go", ".rs": "rust",
|
|
148
|
+
}
|
|
149
|
+
return extension_map.get(ext)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""File editing module for AI Code Assistant."""
|
|
2
|
+
|
|
3
|
+
from ai_code_assistant.editor.file_editor import FileEditor, EditResult
|
|
4
|
+
from ai_code_assistant.editor.diff_handler import DiffHandler, DiffResult
|
|
5
|
+
from ai_code_assistant.editor.prompts import EDIT_PROMPTS
|
|
6
|
+
|
|
7
|
+
__all__ = ["FileEditor", "EditResult", "DiffHandler", "DiffResult", "EDIT_PROMPTS"]
|
|
8
|
+
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Diff generation and application for code editing."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class DiffLine:
|
|
10
|
+
"""Represents a single line in a diff."""
|
|
11
|
+
line_number: int
|
|
12
|
+
content: str
|
|
13
|
+
change_type: str # 'add', 'remove', 'context', 'header'
|
|
14
|
+
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
prefix = {
|
|
17
|
+
'add': '+',
|
|
18
|
+
'remove': '-',
|
|
19
|
+
'context': ' ',
|
|
20
|
+
'header': '@',
|
|
21
|
+
}.get(self.change_type, ' ')
|
|
22
|
+
return f"{prefix} {self.content}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class DiffHunk:
|
|
27
|
+
"""Represents a hunk (section) of changes in a diff."""
|
|
28
|
+
old_start: int
|
|
29
|
+
old_count: int
|
|
30
|
+
new_start: int
|
|
31
|
+
new_count: int
|
|
32
|
+
lines: List[DiffLine] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def header(self) -> str:
|
|
36
|
+
return f"@@ -{self.old_start},{self.old_count} +{self.new_start},{self.new_count} @@"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class DiffResult:
|
|
41
|
+
"""Result of diff generation."""
|
|
42
|
+
original_file: str
|
|
43
|
+
modified_file: str
|
|
44
|
+
hunks: List[DiffHunk] = field(default_factory=list)
|
|
45
|
+
unified_diff: str = ""
|
|
46
|
+
additions: int = 0
|
|
47
|
+
deletions: int = 0
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def has_changes(self) -> bool:
|
|
51
|
+
return self.additions > 0 or self.deletions > 0
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def summary(self) -> str:
|
|
55
|
+
return f"+{self.additions} -{self.deletions} lines"
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return {
|
|
59
|
+
"original_file": self.original_file,
|
|
60
|
+
"modified_file": self.modified_file,
|
|
61
|
+
"additions": self.additions,
|
|
62
|
+
"deletions": self.deletions,
|
|
63
|
+
"unified_diff": self.unified_diff,
|
|
64
|
+
"has_changes": self.has_changes,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DiffHandler:
|
|
69
|
+
"""Handles diff generation and application."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, context_lines: int = 3):
|
|
72
|
+
"""Initialize diff handler.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
context_lines: Number of context lines around changes
|
|
76
|
+
"""
|
|
77
|
+
self.context_lines = context_lines
|
|
78
|
+
|
|
79
|
+
def generate_diff(
|
|
80
|
+
self,
|
|
81
|
+
original: str,
|
|
82
|
+
modified: str,
|
|
83
|
+
filename: str = "file",
|
|
84
|
+
) -> DiffResult:
|
|
85
|
+
"""Generate a unified diff between original and modified content.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
original: Original file content
|
|
89
|
+
modified: Modified file content
|
|
90
|
+
filename: Name of the file for diff headers
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
DiffResult with diff information
|
|
94
|
+
"""
|
|
95
|
+
original_lines = original.splitlines(keepends=True)
|
|
96
|
+
modified_lines = modified.splitlines(keepends=True)
|
|
97
|
+
|
|
98
|
+
# Ensure files end with newline for proper diff
|
|
99
|
+
if original_lines and not original_lines[-1].endswith('\n'):
|
|
100
|
+
original_lines[-1] += '\n'
|
|
101
|
+
if modified_lines and not modified_lines[-1].endswith('\n'):
|
|
102
|
+
modified_lines[-1] += '\n'
|
|
103
|
+
|
|
104
|
+
# Generate unified diff
|
|
105
|
+
diff_lines = list(difflib.unified_diff(
|
|
106
|
+
original_lines,
|
|
107
|
+
modified_lines,
|
|
108
|
+
fromfile=f"a/{filename}",
|
|
109
|
+
tofile=f"b/{filename}",
|
|
110
|
+
n=self.context_lines,
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
unified_diff = ''.join(diff_lines)
|
|
114
|
+
|
|
115
|
+
# Count additions and deletions
|
|
116
|
+
additions = sum(1 for line in diff_lines if line.startswith('+') and not line.startswith('+++'))
|
|
117
|
+
deletions = sum(1 for line in diff_lines if line.startswith('-') and not line.startswith('---'))
|
|
118
|
+
|
|
119
|
+
# Parse hunks
|
|
120
|
+
hunks = self._parse_hunks(diff_lines)
|
|
121
|
+
|
|
122
|
+
return DiffResult(
|
|
123
|
+
original_file=filename,
|
|
124
|
+
modified_file=filename,
|
|
125
|
+
hunks=hunks,
|
|
126
|
+
unified_diff=unified_diff,
|
|
127
|
+
additions=additions,
|
|
128
|
+
deletions=deletions,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _parse_hunks(self, diff_lines: List[str]) -> List[DiffHunk]:
|
|
132
|
+
"""Parse diff lines into hunks."""
|
|
133
|
+
hunks = []
|
|
134
|
+
current_hunk = None
|
|
135
|
+
|
|
136
|
+
for line in diff_lines:
|
|
137
|
+
if line.startswith('@@'):
|
|
138
|
+
# Parse hunk header: @@ -start,count +start,count @@
|
|
139
|
+
if current_hunk:
|
|
140
|
+
hunks.append(current_hunk)
|
|
141
|
+
|
|
142
|
+
# Extract numbers from header
|
|
143
|
+
parts = line.split('@@')[1].strip().split()
|
|
144
|
+
old_part = parts[0][1:].split(',') # Remove '-'
|
|
145
|
+
new_part = parts[1][1:].split(',') # Remove '+'
|
|
146
|
+
|
|
147
|
+
old_start = int(old_part[0])
|
|
148
|
+
old_count = int(old_part[1]) if len(old_part) > 1 else 1
|
|
149
|
+
new_start = int(new_part[0])
|
|
150
|
+
new_count = int(new_part[1]) if len(new_part) > 1 else 1
|
|
151
|
+
|
|
152
|
+
current_hunk = DiffHunk(
|
|
153
|
+
old_start=old_start,
|
|
154
|
+
old_count=old_count,
|
|
155
|
+
new_start=new_start,
|
|
156
|
+
new_count=new_count,
|
|
157
|
+
)
|
|
158
|
+
elif current_hunk is not None:
|
|
159
|
+
if line.startswith('+') and not line.startswith('+++'):
|
|
160
|
+
change_type = 'add'
|
|
161
|
+
elif line.startswith('-') and not line.startswith('---'):
|
|
162
|
+
change_type = 'remove'
|
|
163
|
+
elif line.startswith(' '):
|
|
164
|
+
change_type = 'context'
|
|
165
|
+
else:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
current_hunk.lines.append(DiffLine(
|
|
169
|
+
line_number=0, # Will be calculated if needed
|
|
170
|
+
content=line[1:].rstrip('\n'),
|
|
171
|
+
change_type=change_type,
|
|
172
|
+
))
|
|
173
|
+
|
|
174
|
+
if current_hunk:
|
|
175
|
+
hunks.append(current_hunk)
|
|
176
|
+
|
|
177
|
+
return hunks
|
|
178
|
+
|
|
179
|
+
def apply_diff(self, original: str, diff_result: DiffResult) -> str:
|
|
180
|
+
"""Apply a diff to original content.
|
|
181
|
+
|
|
182
|
+
Note: This is a simplified implementation. For complex diffs,
|
|
183
|
+
consider using the patch utility or similar tools.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
original: Original file content
|
|
187
|
+
diff_result: DiffResult to apply
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Modified content after applying diff
|
|
191
|
+
"""
|
|
192
|
+
# For simplicity, we return the modified content that was used
|
|
193
|
+
# to generate the diff. In a real implementation, you might
|
|
194
|
+
# want to parse and apply the diff hunks.
|
|
195
|
+
# This is mainly useful for verification.
|
|
196
|
+
return diff_result.modified_file if hasattr(diff_result, '_modified_content') else original
|
|
197
|
+
|
|
198
|
+
def format_for_display(
|
|
199
|
+
self,
|
|
200
|
+
diff_result: DiffResult,
|
|
201
|
+
use_colors: bool = True,
|
|
202
|
+
) -> str:
|
|
203
|
+
"""Format diff for terminal display.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
diff_result: DiffResult to format
|
|
207
|
+
use_colors: Whether to use ANSI colors
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Formatted diff string
|
|
211
|
+
"""
|
|
212
|
+
if not diff_result.has_changes:
|
|
213
|
+
return "No changes detected."
|
|
214
|
+
|
|
215
|
+
lines = []
|
|
216
|
+
|
|
217
|
+
# Header
|
|
218
|
+
lines.append(f"--- a/{diff_result.original_file}")
|
|
219
|
+
lines.append(f"+++ b/{diff_result.modified_file}")
|
|
220
|
+
|
|
221
|
+
for hunk in diff_result.hunks:
|
|
222
|
+
lines.append(hunk.header)
|
|
223
|
+
|
|
224
|
+
for diff_line in hunk.lines:
|
|
225
|
+
if use_colors:
|
|
226
|
+
if diff_line.change_type == 'add':
|
|
227
|
+
lines.append(f"\033[32m+{diff_line.content}\033[0m")
|
|
228
|
+
elif diff_line.change_type == 'remove':
|
|
229
|
+
lines.append(f"\033[31m-{diff_line.content}\033[0m")
|
|
230
|
+
else:
|
|
231
|
+
lines.append(f" {diff_line.content}")
|
|
232
|
+
else:
|
|
233
|
+
prefix = {
|
|
234
|
+
'add': '+',
|
|
235
|
+
'remove': '-',
|
|
236
|
+
'context': ' ',
|
|
237
|
+
}.get(diff_line.change_type, ' ')
|
|
238
|
+
lines.append(f"{prefix}{diff_line.content}")
|
|
239
|
+
|
|
240
|
+
return '\n'.join(lines)
|
|
241
|
+
|
|
242
|
+
def get_changed_lines(self, diff_result: DiffResult) -> Tuple[List[int], List[int]]:
|
|
243
|
+
"""Get lists of added and removed line numbers.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
diff_result: DiffResult to analyze
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Tuple of (added_lines, removed_lines)
|
|
250
|
+
"""
|
|
251
|
+
added = []
|
|
252
|
+
removed = []
|
|
253
|
+
|
|
254
|
+
for hunk in diff_result.hunks:
|
|
255
|
+
old_line = hunk.old_start
|
|
256
|
+
new_line = hunk.new_start
|
|
257
|
+
|
|
258
|
+
for diff_line in hunk.lines:
|
|
259
|
+
if diff_line.change_type == 'add':
|
|
260
|
+
added.append(new_line)
|
|
261
|
+
new_line += 1
|
|
262
|
+
elif diff_line.change_type == 'remove':
|
|
263
|
+
removed.append(old_line)
|
|
264
|
+
old_line += 1
|
|
265
|
+
else: # context
|
|
266
|
+
old_line += 1
|
|
267
|
+
new_line += 1
|
|
268
|
+
|
|
269
|
+
return added, removed
|
|
270
|
+
|