codeui-python 0.1.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.
- codeui/__init__.py +23 -0
- codeui/agent/__init__.py +15 -0
- codeui/agent/context.py +207 -0
- codeui/agent/contribution.py +224 -0
- codeui/agent/edit_tools.py +280 -0
- codeui/agent/integrations.py +243 -0
- codeui/agent/mcp_server.py +94 -0
- codeui/analysis/__init__.py +1 -0
- codeui/analysis/api_drift.py +79 -0
- codeui/analysis/base.py +42 -0
- codeui/analysis/circular.py +116 -0
- codeui/analysis/dead_code.py +63 -0
- codeui/analysis/duplicates.py +69 -0
- codeui/analysis/runner.py +57 -0
- codeui/analysis/shadowing.py +49 -0
- codeui/analysis/undefined.py +97 -0
- codeui/analysis/unreachable.py +28 -0
- codeui/analysis/unresolved_import.py +153 -0
- codeui/cli/__init__.py +1 -0
- codeui/cli/main.py +437 -0
- codeui/core/__init__.py +1 -0
- codeui/core/cache.py +135 -0
- codeui/core/graph.py +435 -0
- codeui/core/ir.py +218 -0
- codeui/core/override.py +230 -0
- codeui/core/repo.py +85 -0
- codeui/core/resolver.py +281 -0
- codeui/errors.py +98 -0
- codeui/lang/__init__.py +1 -0
- codeui/lang/base.py +117 -0
- codeui/lang/generic.py +90 -0
- codeui/lang/go.py +167 -0
- codeui/lang/python.py +294 -0
- codeui/lang/registry.py +83 -0
- codeui/lang/rust.py +185 -0
- codeui/lang/ts.py +396 -0
- codeui/plugins/__init__.py +1 -0
- codeui/plugins/registry.py +31 -0
- codeui/report/__init__.py +1 -0
- codeui/report/json_emitter.py +24 -0
- codeui/report/markdown_emitter.py +33 -0
- codeui/report/sarif_emitter.py +56 -0
- codeui/server/__init__.py +1 -0
- codeui/server/server.py +705 -0
- codeui/tracer/__init__.py +1 -0
- codeui/tracer/tracer.py +147 -0
- codeui_python-0.1.0.dist-info/METADATA +191 -0
- codeui_python-0.1.0.dist-info/RECORD +52 -0
- codeui_python-0.1.0.dist-info/WHEEL +5 -0
- codeui_python-0.1.0.dist-info/entry_points.txt +2 -0
- codeui_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- codeui_python-0.1.0.dist-info/top_level.txt +1 -0
codeui/core/ir.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Intermediate Representation (IR) data structures for codeui."""
|
|
2
|
+
from dataclasses import dataclass, asdict
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
class StrEnum(str, Enum):
|
|
7
|
+
"""String enum for Python 3.10+ compatibility.
|
|
8
|
+
Example:
|
|
9
|
+
>>> class Color(StrEnum):
|
|
10
|
+
... RED = "red"
|
|
11
|
+
>>> str(Color.RED)
|
|
12
|
+
'red'
|
|
13
|
+
"""
|
|
14
|
+
def __str__(self) -> str:
|
|
15
|
+
return str(self.value)
|
|
16
|
+
|
|
17
|
+
class Visibility(StrEnum):
|
|
18
|
+
"""Symbol visibility scope."""
|
|
19
|
+
PUBLIC = "public"
|
|
20
|
+
PROTECTED = "protected"
|
|
21
|
+
PRIVATE = "private"
|
|
22
|
+
INTERNAL = "internal"
|
|
23
|
+
|
|
24
|
+
class LayerKind(StrEnum):
|
|
25
|
+
"""Architectural classification layer for files and modules.
|
|
26
|
+
Example:
|
|
27
|
+
>>> str(LayerKind.FRONTEND)
|
|
28
|
+
'frontend'
|
|
29
|
+
"""
|
|
30
|
+
ENTRY = "entry"
|
|
31
|
+
FRONTEND = "frontend"
|
|
32
|
+
BACKEND = "backend"
|
|
33
|
+
HOOKS = "hooks"
|
|
34
|
+
SHARED = "shared"
|
|
35
|
+
TEST = "test"
|
|
36
|
+
CONFIG = "config"
|
|
37
|
+
OTHER = "other"
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class FileClassification:
|
|
41
|
+
"""Architectural classification for a source file.
|
|
42
|
+
Example:
|
|
43
|
+
>>> c = FileClassification(LayerKind.FRONTEND, "auth", "components")
|
|
44
|
+
>>> str(c.layer)
|
|
45
|
+
'frontend'
|
|
46
|
+
"""
|
|
47
|
+
layer: LayerKind
|
|
48
|
+
feature: str
|
|
49
|
+
module: str
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
52
|
+
"""Convert classification to dictionary.
|
|
53
|
+
Example:
|
|
54
|
+
>>> c = FileClassification(LayerKind.BACKEND, "api", "server")
|
|
55
|
+
>>> c.to_dict()["layer"]
|
|
56
|
+
'backend'
|
|
57
|
+
"""
|
|
58
|
+
return {
|
|
59
|
+
"layer": str(self.layer),
|
|
60
|
+
"feature": self.feature,
|
|
61
|
+
"module": self.module,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class SymbolKind(StrEnum):
|
|
65
|
+
"""Kind of programming symbol."""
|
|
66
|
+
FILE = "file"
|
|
67
|
+
MODULE = "module"
|
|
68
|
+
NAMESPACE = "namespace"
|
|
69
|
+
CLASS = "class"
|
|
70
|
+
INTERFACE = "interface"
|
|
71
|
+
STRUCT = "struct"
|
|
72
|
+
ENUM = "enum"
|
|
73
|
+
FUNCTION = "function"
|
|
74
|
+
METHOD = "method"
|
|
75
|
+
PROPERTY = "property"
|
|
76
|
+
VARIABLE = "variable"
|
|
77
|
+
CONSTANT = "constant"
|
|
78
|
+
TYPE_ALIAS = "type_alias"
|
|
79
|
+
IMPORT = "import"
|
|
80
|
+
EXPORT = "export"
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, slots=True)
|
|
83
|
+
class Location:
|
|
84
|
+
"""Source code range location.
|
|
85
|
+
Example:
|
|
86
|
+
>>> loc = Location("app.py", 1, 0, 10, 5)
|
|
87
|
+
>>> loc.file_id
|
|
88
|
+
'app.py'
|
|
89
|
+
"""
|
|
90
|
+
file_id: str
|
|
91
|
+
start_line: int
|
|
92
|
+
start_col: int
|
|
93
|
+
end_line: int
|
|
94
|
+
end_col: int
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
97
|
+
"""Convert Location to dictionary.
|
|
98
|
+
Example:
|
|
99
|
+
>>> loc = Location("a.py", 1, 0, 1, 10)
|
|
100
|
+
>>> loc.to_dict()["file_id"]
|
|
101
|
+
'a.py'
|
|
102
|
+
"""
|
|
103
|
+
return asdict(self)
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True, slots=True)
|
|
106
|
+
class Symbol:
|
|
107
|
+
"""Code symbol node in graph.
|
|
108
|
+
Example:
|
|
109
|
+
>>> loc = Location("a.py", 1, 0, 2, 0)
|
|
110
|
+
>>> sym = Symbol("s1", "foo", "a.foo", SymbolKind.FUNCTION, "python", loc, None, "def foo():", Visibility.PUBLIC, (), "hash1")
|
|
111
|
+
>>> sym.name
|
|
112
|
+
'foo'
|
|
113
|
+
"""
|
|
114
|
+
id: str
|
|
115
|
+
name: str
|
|
116
|
+
qualified_name: str
|
|
117
|
+
kind: SymbolKind
|
|
118
|
+
language: str
|
|
119
|
+
location: Location
|
|
120
|
+
parent_id: str | None
|
|
121
|
+
signature: str | None
|
|
122
|
+
visibility: Visibility
|
|
123
|
+
modifiers: tuple[str, ...]
|
|
124
|
+
content_hash: str
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
127
|
+
"""Convert Symbol to dictionary representation.
|
|
128
|
+
Example:
|
|
129
|
+
>>> loc = Location("a.py", 1, 0, 2, 0)
|
|
130
|
+
>>> sym = Symbol("s1", "foo", "a.foo", SymbolKind.FUNCTION, "python", loc, None, "def foo():", Visibility.PUBLIC, (), "hash1")
|
|
131
|
+
>>> sym.to_dict()["name"]
|
|
132
|
+
'foo'
|
|
133
|
+
"""
|
|
134
|
+
d = asdict(self)
|
|
135
|
+
d["kind"] = str(self.kind)
|
|
136
|
+
d["visibility"] = str(self.visibility)
|
|
137
|
+
return d
|
|
138
|
+
|
|
139
|
+
class EdgeKind(StrEnum):
|
|
140
|
+
"""Kinds of directed relationship edges."""
|
|
141
|
+
CONTAINS = "contains"
|
|
142
|
+
IMPORTS = "imports"
|
|
143
|
+
EXPORTS = "exports"
|
|
144
|
+
CALLS = "calls"
|
|
145
|
+
REFERENCES = "references"
|
|
146
|
+
INHERITS = "inherits"
|
|
147
|
+
IMPLEMENTS = "implements"
|
|
148
|
+
OVERRIDES = "overrides"
|
|
149
|
+
READS = "reads"
|
|
150
|
+
WRITES = "writes"
|
|
151
|
+
RETURNS = "returns"
|
|
152
|
+
INSTANTIATES = "instantiates"
|
|
153
|
+
DEPENDS_ON = "depends_on"
|
|
154
|
+
TESTS = "tests"
|
|
155
|
+
DOCUMENTS = "documents"
|
|
156
|
+
DUPLICATES = "duplicates"
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True, slots=True)
|
|
159
|
+
class Edge:
|
|
160
|
+
"""Directed edge between two symbols or nodes.
|
|
161
|
+
Example:
|
|
162
|
+
>>> edge = Edge("s1", "s2", EdgeKind.CALLS, 1.0, 1.0, None)
|
|
163
|
+
>>> edge.kind
|
|
164
|
+
<EdgeKind.CALLS: 'calls'>
|
|
165
|
+
"""
|
|
166
|
+
source_id: str
|
|
167
|
+
target_id: str
|
|
168
|
+
kind: EdgeKind
|
|
169
|
+
weight: float
|
|
170
|
+
confidence: float
|
|
171
|
+
location: Location | None
|
|
172
|
+
|
|
173
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
174
|
+
"""Convert Edge to dictionary representation.
|
|
175
|
+
Example:
|
|
176
|
+
>>> edge = Edge("s1", "s2", EdgeKind.CALLS, 1.0, 1.0, None)
|
|
177
|
+
>>> edge.to_dict()["kind"]
|
|
178
|
+
'calls'
|
|
179
|
+
"""
|
|
180
|
+
d = asdict(self)
|
|
181
|
+
d["kind"] = str(self.kind)
|
|
182
|
+
return d
|
|
183
|
+
|
|
184
|
+
class Severity(StrEnum):
|
|
185
|
+
"""Finding severity level."""
|
|
186
|
+
ERROR = "error"
|
|
187
|
+
WARNING = "warning"
|
|
188
|
+
INFO = "info"
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True, slots=True)
|
|
191
|
+
class Finding:
|
|
192
|
+
"""Defect finding report.
|
|
193
|
+
Example:
|
|
194
|
+
>>> loc = Location("a.py", 1, 0, 1, 10)
|
|
195
|
+
>>> f = Finding("f1", "undefined_symbol", "Symbol x undefined", Severity.ERROR, loc, "s1", "Import x or define it", 1.0)
|
|
196
|
+
>>> f.rule_id
|
|
197
|
+
'undefined_symbol'
|
|
198
|
+
"""
|
|
199
|
+
id: str
|
|
200
|
+
rule_id: str
|
|
201
|
+
message: str
|
|
202
|
+
severity: Severity
|
|
203
|
+
location: Location
|
|
204
|
+
symbol_id: str | None
|
|
205
|
+
fix_hint: str | None
|
|
206
|
+
confidence: float
|
|
207
|
+
|
|
208
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
209
|
+
"""Convert Finding to dictionary.
|
|
210
|
+
Example:
|
|
211
|
+
>>> loc = Location("a.py", 1, 0, 1, 10)
|
|
212
|
+
>>> f = Finding("f1", "undefined_symbol", "Symbol x undefined", Severity.ERROR, loc, "s1", "Import x", 1.0)
|
|
213
|
+
>>> f.to_dict()["severity"]
|
|
214
|
+
'error'
|
|
215
|
+
"""
|
|
216
|
+
d = asdict(self)
|
|
217
|
+
d["severity"] = str(self.severity)
|
|
218
|
+
return d
|
codeui/core/override.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""In-memory OverrideStore for non-destructive code editing."""
|
|
2
|
+
import difflib
|
|
3
|
+
import hashlib
|
|
4
|
+
from dataclasses import dataclass, field, asdict
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
7
|
+
from codeui.core.ir import StrEnum
|
|
8
|
+
from codeui.errors import OverrideConflictError
|
|
9
|
+
|
|
10
|
+
class OverrideKind(StrEnum):
|
|
11
|
+
"""Kind of override modification."""
|
|
12
|
+
RENAME_SYMBOL = "rename_symbol"
|
|
13
|
+
RETYPE_SYMBOL = "retype_symbol"
|
|
14
|
+
REPLACE_SYMBOL_BODY = "replace_symbol_body"
|
|
15
|
+
REPLACE_FILE = "replace_file"
|
|
16
|
+
TOGGLE_EDGE = "toggle_edge"
|
|
17
|
+
TRIAGE_FINDING = "triage_finding"
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Override:
|
|
21
|
+
"""Represents an active override on a target symbol, file, or edge.
|
|
22
|
+
Example:
|
|
23
|
+
>>> o = Override("sym_1", OverrideKind.RENAME_SYMBOL, "hash123", "new_name", "user", "2026-01-01T00:00:00Z")
|
|
24
|
+
>>> o.target_id
|
|
25
|
+
'sym_1'
|
|
26
|
+
"""
|
|
27
|
+
target_id: str
|
|
28
|
+
kind: OverrideKind
|
|
29
|
+
original_hash: str
|
|
30
|
+
new_value: Any
|
|
31
|
+
author: str
|
|
32
|
+
created_at: str
|
|
33
|
+
note: str | None = None
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
36
|
+
"""Convert override to serializable dict.
|
|
37
|
+
Example:
|
|
38
|
+
>>> o = Override("sym_1", OverrideKind.RENAME_SYMBOL, "hash123", "new_name", "user", "2026-01-01T00:00:00Z")
|
|
39
|
+
>>> o.to_dict()["kind"]
|
|
40
|
+
'rename_symbol'
|
|
41
|
+
"""
|
|
42
|
+
d = asdict(self)
|
|
43
|
+
d["kind"] = str(self.kind)
|
|
44
|
+
return d
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class Conflict:
|
|
48
|
+
"""Override conflict descriptor.
|
|
49
|
+
Example:
|
|
50
|
+
>>> c = Conflict("src/main.py", "hash1", "hash2", "File on disk changed")
|
|
51
|
+
>>> c.file_path
|
|
52
|
+
'src/main.py'
|
|
53
|
+
"""
|
|
54
|
+
file_path: str
|
|
55
|
+
expected_hash: str
|
|
56
|
+
actual_hash: str
|
|
57
|
+
reason: str
|
|
58
|
+
|
|
59
|
+
class OverrideStore:
|
|
60
|
+
"""Manages active overrides without writing to disk.
|
|
61
|
+
Example:
|
|
62
|
+
>>> store = OverrideStore()
|
|
63
|
+
>>> o = Override("file.py", OverrideKind.REPLACE_FILE, "h1", "print('hello')", "agent", "2026-01-01")
|
|
64
|
+
>>> store.set(o)
|
|
65
|
+
>>> store.get("file.py").new_value
|
|
66
|
+
"print('hello')"
|
|
67
|
+
"""
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
self._overrides: Dict[str, Override] = {}
|
|
70
|
+
self._file_contents: Dict[str, str] = {}
|
|
71
|
+
self._original_file_contents: Dict[str, str] = {}
|
|
72
|
+
|
|
73
|
+
def get(self, target_id: str) -> Override | None:
|
|
74
|
+
"""Retrieve active override for target ID.
|
|
75
|
+
Example:
|
|
76
|
+
>>> store = OverrideStore()
|
|
77
|
+
>>> store.get("nonexistent") is None
|
|
78
|
+
True
|
|
79
|
+
"""
|
|
80
|
+
return self._overrides.get(target_id)
|
|
81
|
+
|
|
82
|
+
def has_override(self, target_id: str) -> bool:
|
|
83
|
+
"""Check if target has an active override.
|
|
84
|
+
Example:
|
|
85
|
+
>>> store = OverrideStore()
|
|
86
|
+
>>> store.has_override("test.py")
|
|
87
|
+
False
|
|
88
|
+
"""
|
|
89
|
+
return target_id in self._overrides
|
|
90
|
+
|
|
91
|
+
def set(self, override: Override) -> None:
|
|
92
|
+
"""Set or update an override.
|
|
93
|
+
Example:
|
|
94
|
+
>>> store = OverrideStore()
|
|
95
|
+
>>> o = Override("t1", OverrideKind.RENAME_SYMBOL, "h1", "new", "user", "2026")
|
|
96
|
+
>>> store.set(o)
|
|
97
|
+
>>> store.get("t1").new_value
|
|
98
|
+
'new'
|
|
99
|
+
"""
|
|
100
|
+
self._overrides[override.target_id] = override
|
|
101
|
+
|
|
102
|
+
def set_file_override(self, file_path: str, original_content: str, new_content: str, author: str = "user", note: str | None = None) -> None:
|
|
103
|
+
"""Register full file replacement override.
|
|
104
|
+
Example:
|
|
105
|
+
>>> store = OverrideStore()
|
|
106
|
+
>>> store.set_file_override("a.py", "x = 1", "x = 2")
|
|
107
|
+
>>> store.get_file_content("a.py")
|
|
108
|
+
'x = 2'
|
|
109
|
+
"""
|
|
110
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
111
|
+
self._original_file_contents[file_path] = original_content
|
|
112
|
+
self._file_contents[file_path] = new_content
|
|
113
|
+
override = Override(
|
|
114
|
+
target_id=file_path,
|
|
115
|
+
kind=OverrideKind.REPLACE_FILE,
|
|
116
|
+
original_hash=hashlib.sha256(original_content.encode("utf-8")).hexdigest(),
|
|
117
|
+
new_value=new_content,
|
|
118
|
+
author=author,
|
|
119
|
+
created_at=now,
|
|
120
|
+
note=note,
|
|
121
|
+
)
|
|
122
|
+
self._overrides[file_path] = override
|
|
123
|
+
|
|
124
|
+
def get_file_content(self, file_path: str) -> str | None:
|
|
125
|
+
"""Return overridden file content if present.
|
|
126
|
+
Example:
|
|
127
|
+
>>> store = OverrideStore()
|
|
128
|
+
>>> store.set_file_override("a.py", "old", "new")
|
|
129
|
+
>>> store.get_file_content("a.py")
|
|
130
|
+
'new'
|
|
131
|
+
"""
|
|
132
|
+
return self._file_contents.get(file_path)
|
|
133
|
+
|
|
134
|
+
def revert(self, target_id: str) -> None:
|
|
135
|
+
"""Revert override for specific target ID or file path across all key variations.
|
|
136
|
+
Example:
|
|
137
|
+
>>> store = OverrideStore()
|
|
138
|
+
>>> store.set_file_override("a/b.py", "old", "new")
|
|
139
|
+
>>> store.revert("b.py")
|
|
140
|
+
>>> store.get("a/b.py") is None
|
|
141
|
+
True
|
|
142
|
+
"""
|
|
143
|
+
if not target_id:
|
|
144
|
+
return
|
|
145
|
+
clean_target = target_id.split("::")[0].replace("module::", "").replace("\\", "/").strip().lstrip("./")
|
|
146
|
+
keys_to_remove = set()
|
|
147
|
+
for k in list(self._overrides.keys()) + list(self._file_contents.keys()):
|
|
148
|
+
k_clean = k.split("::")[0].replace("module::", "").replace("\\", "/").strip().lstrip("./")
|
|
149
|
+
if k == target_id or k_clean == clean_target or k_clean.endswith("/" + clean_target) or clean_target.endswith("/" + k_clean):
|
|
150
|
+
keys_to_remove.add(k)
|
|
151
|
+
for k in keys_to_remove:
|
|
152
|
+
self._overrides.pop(k, None)
|
|
153
|
+
self._file_contents.pop(k, None)
|
|
154
|
+
self._original_file_contents.pop(k, None)
|
|
155
|
+
|
|
156
|
+
def revert_all(self) -> None:
|
|
157
|
+
"""Revert all overrides in the store.
|
|
158
|
+
Example:
|
|
159
|
+
>>> store = OverrideStore()
|
|
160
|
+
>>> store.set_file_override("a.py", "old", "new")
|
|
161
|
+
>>> store.revert_all()
|
|
162
|
+
>>> len(list(store.list()))
|
|
163
|
+
0
|
|
164
|
+
"""
|
|
165
|
+
self._overrides.clear()
|
|
166
|
+
self._file_contents.clear()
|
|
167
|
+
|
|
168
|
+
def list(self) -> Iterable[Override]:
|
|
169
|
+
"""List all current overrides in deterministic order.
|
|
170
|
+
Example:
|
|
171
|
+
>>> store = OverrideStore()
|
|
172
|
+
>>> list(store.list())
|
|
173
|
+
[]
|
|
174
|
+
"""
|
|
175
|
+
return sorted(self._overrides.values(), key=lambda o: o.target_id)
|
|
176
|
+
|
|
177
|
+
def to_patch_files(self) -> Dict[str, str]:
|
|
178
|
+
"""Export map of path to modified file content.
|
|
179
|
+
Example:
|
|
180
|
+
>>> store = OverrideStore()
|
|
181
|
+
>>> store.set_file_override("a.py", "old", "new")
|
|
182
|
+
>>> store.to_patch_files()["a.py"]
|
|
183
|
+
'new'
|
|
184
|
+
"""
|
|
185
|
+
return dict(self._file_contents)
|
|
186
|
+
|
|
187
|
+
def to_diff(self) -> str:
|
|
188
|
+
"""Generate unified diff string across all overridden files.
|
|
189
|
+
Example:
|
|
190
|
+
>>> store = OverrideStore()
|
|
191
|
+
>>> store.set_file_override("a.py", "old", "new")
|
|
192
|
+
>>> '--- a/a.py' in store.to_diff()
|
|
193
|
+
True
|
|
194
|
+
"""
|
|
195
|
+
diff_chunks: List[str] = []
|
|
196
|
+
for file_path in sorted(self._file_contents.keys()):
|
|
197
|
+
orig = self._original_file_contents.get(file_path, "").splitlines(keepends=True)
|
|
198
|
+
mod = self._file_contents[file_path].splitlines(keepends=True)
|
|
199
|
+
lines = list(difflib.unified_diff(
|
|
200
|
+
orig,
|
|
201
|
+
mod,
|
|
202
|
+
fromfile=f"a/{file_path}",
|
|
203
|
+
tofile=f"b/{file_path}",
|
|
204
|
+
))
|
|
205
|
+
if lines:
|
|
206
|
+
diff_chunks.extend(lines)
|
|
207
|
+
return "".join(diff_chunks)
|
|
208
|
+
|
|
209
|
+
def check_conflicts(self, disk_state: Dict[str, str]) -> List[Conflict]:
|
|
210
|
+
"""Check for conflicts between overrides and current disk files.
|
|
211
|
+
Example:
|
|
212
|
+
>>> store = OverrideStore()
|
|
213
|
+
>>> store.set_file_override("a.py", "v1", "v2")
|
|
214
|
+
>>> conflicts = store.check_conflicts({"a.py": "v1_changed"})
|
|
215
|
+
>>> len(conflicts)
|
|
216
|
+
1
|
|
217
|
+
"""
|
|
218
|
+
conflicts: List[Conflict] = []
|
|
219
|
+
for file_path, override in self._overrides.items():
|
|
220
|
+
if override.kind == OverrideKind.REPLACE_FILE and file_path in disk_state:
|
|
221
|
+
current_disk_content = disk_state[file_path]
|
|
222
|
+
expected_original = self._original_file_contents.get(file_path, "")
|
|
223
|
+
if current_disk_content != expected_original:
|
|
224
|
+
conflicts.append(Conflict(
|
|
225
|
+
file_path=file_path,
|
|
226
|
+
expected_hash=override.original_hash,
|
|
227
|
+
actual_hash=hashlib.sha256(current_disk_content.encode("utf-8")).hexdigest(),
|
|
228
|
+
reason="File content on disk changed since override creation",
|
|
229
|
+
))
|
|
230
|
+
return conflicts
|
codeui/core/repo.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Git repository cloning and remote URL resolution helper."""
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
from codeui.errors import RepositoryCloneError
|
|
9
|
+
|
|
10
|
+
DEFAULT_REPOSITORY_URL = "https://github.com/AAlsubari/codeUI"
|
|
11
|
+
|
|
12
|
+
def get_library_repo_url() -> str:
|
|
13
|
+
"""Retrieve the official GitHub repository URL for the codeui library.
|
|
14
|
+
Example:
|
|
15
|
+
>>> get_library_repo_url()
|
|
16
|
+
'https://github.com/AAlsubari/codeUI'
|
|
17
|
+
"""
|
|
18
|
+
return os.environ.get("CODEUI_GITHUB_REPO_URL", DEFAULT_REPOSITORY_URL)
|
|
19
|
+
|
|
20
|
+
def is_git_url(target: str) -> bool:
|
|
21
|
+
"""Check if target string represents a remote Git repository URL.
|
|
22
|
+
Example:
|
|
23
|
+
>>> is_git_url("https://github.com/psf/black")
|
|
24
|
+
True
|
|
25
|
+
>>> is_git_url("git@github.com:psf/black.git")
|
|
26
|
+
True
|
|
27
|
+
>>> is_git_url("./local/path")
|
|
28
|
+
False
|
|
29
|
+
"""
|
|
30
|
+
if not isinstance(target, str):
|
|
31
|
+
return False
|
|
32
|
+
t = target.strip()
|
|
33
|
+
if t.startswith(("http://", "https://", "git@", "git://", "ssh://")):
|
|
34
|
+
return True
|
|
35
|
+
if t.startswith("github.com/") or t.startswith("gitlab.com/") or t.startswith("bitbucket.org/"):
|
|
36
|
+
return True
|
|
37
|
+
if t.endswith(".git") and (":" in t or "/" in t):
|
|
38
|
+
return True
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def clone_git_repo(repo_url: str, dest_dir: Optional[Path] = None) -> Tuple[Path, Optional[tempfile.TemporaryDirectory]]:
|
|
42
|
+
"""Clone a remote Git repository with depth 1 into destination or temporary directory.
|
|
43
|
+
Example:
|
|
44
|
+
>>> is_git_url("https://github.com/example/repo")
|
|
45
|
+
True
|
|
46
|
+
"""
|
|
47
|
+
git_bin = shutil.which("git")
|
|
48
|
+
if not git_bin:
|
|
49
|
+
raise RepositoryCloneError(repo_url, "Git executable not found in system PATH")
|
|
50
|
+
|
|
51
|
+
url = repo_url.strip()
|
|
52
|
+
if url.startswith("github.com/"):
|
|
53
|
+
url = f"https://{url}"
|
|
54
|
+
elif url.startswith("gitlab.com/"):
|
|
55
|
+
url = f"https://{url}"
|
|
56
|
+
elif url.startswith("bitbucket.org/"):
|
|
57
|
+
url = f"https://{url}"
|
|
58
|
+
|
|
59
|
+
temp_obj: Optional[tempfile.TemporaryDirectory] = None
|
|
60
|
+
if dest_dir is None:
|
|
61
|
+
temp_obj = tempfile.TemporaryDirectory(prefix="codeui_repo_")
|
|
62
|
+
target_path = Path(temp_obj.name).resolve()
|
|
63
|
+
else:
|
|
64
|
+
target_path = dest_dir.resolve()
|
|
65
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
cmd = [git_bin, "clone", "--depth", "1", "--quiet", url, str(target_path)]
|
|
68
|
+
try:
|
|
69
|
+
res = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
70
|
+
if res.returncode != 0:
|
|
71
|
+
if temp_obj:
|
|
72
|
+
temp_obj.cleanup()
|
|
73
|
+
raise RepositoryCloneError(repo_url, res.stderr.strip() or f"Git clone failed with code {res.returncode}")
|
|
74
|
+
except subprocess.TimeoutExpired:
|
|
75
|
+
if temp_obj:
|
|
76
|
+
temp_obj.cleanup()
|
|
77
|
+
raise RepositoryCloneError(repo_url, "Git clone timed out after 120s")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
if temp_obj:
|
|
80
|
+
temp_obj.cleanup()
|
|
81
|
+
if isinstance(e, RepositoryCloneError):
|
|
82
|
+
raise
|
|
83
|
+
raise RepositoryCloneError(repo_url, str(e))
|
|
84
|
+
|
|
85
|
+
return target_path, temp_obj
|