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/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""codeui: Universal Code-Intelligence Library. Demo Version 18661"""
|
|
2
|
+
from codeui.core.ir import Location, Symbol, SymbolKind, Visibility, EdgeKind, Edge, Finding, Severity
|
|
3
|
+
from codeui.core.graph import Graph
|
|
4
|
+
from codeui.core.override import Override, OverrideKind, OverrideStore
|
|
5
|
+
from codeui.core.repo import get_library_repo_url
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Location",
|
|
11
|
+
"Symbol",
|
|
12
|
+
"SymbolKind",
|
|
13
|
+
"Visibility",
|
|
14
|
+
"EdgeKind",
|
|
15
|
+
"Edge",
|
|
16
|
+
"Finding",
|
|
17
|
+
"Severity",
|
|
18
|
+
"Graph",
|
|
19
|
+
"Override",
|
|
20
|
+
"OverrideKind",
|
|
21
|
+
"OverrideStore",
|
|
22
|
+
"get_library_repo_url",
|
|
23
|
+
]
|
codeui/agent/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Agent integration and dynamic context builder package for codeui."""
|
|
2
|
+
from codeui.agent.context import ContextBuilder, ContextBundle, TaskSpec
|
|
3
|
+
from codeui.agent.contribution import ContributionManager
|
|
4
|
+
from codeui.agent.edit_tools import EditProposal, EditTools
|
|
5
|
+
from codeui.agent.integrations import CodeUITools
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ContextBuilder",
|
|
9
|
+
"ContextBundle",
|
|
10
|
+
"TaskSpec",
|
|
11
|
+
"ContributionManager",
|
|
12
|
+
"EditProposal",
|
|
13
|
+
"EditTools",
|
|
14
|
+
"CodeUITools",
|
|
15
|
+
]
|
codeui/agent/context.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Dynamic context builder for AI agents based on graph structure."""
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass, field, asdict
|
|
4
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
5
|
+
from codeui.core.graph import Graph
|
|
6
|
+
from codeui.core.ir import Edge, Finding, Symbol
|
|
7
|
+
from codeui.errors import SymbolNotFoundError, FileNotFoundInGraphError
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class TaskSpec:
|
|
11
|
+
"""Specification of an AI agent coding task.
|
|
12
|
+
Example:
|
|
13
|
+
>>> spec = TaskSpec("Fix bug in main", ["sym_1"], ["app.py"])
|
|
14
|
+
>>> spec.query
|
|
15
|
+
'Fix bug in main'
|
|
16
|
+
"""
|
|
17
|
+
query: str
|
|
18
|
+
target_symbols: List[str] = field(default_factory=list)
|
|
19
|
+
target_files: List[str] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ContextBundle:
|
|
23
|
+
"""Compact context payload for LLMs built via graph structure.
|
|
24
|
+
Example:
|
|
25
|
+
>>> bundle = ContextBundle({"sym_1": "def f(): pass"}, {"sym_2": "def g():"}, [], [], {"reason": "test"})
|
|
26
|
+
>>> bundle.manifest["reason"]
|
|
27
|
+
'test'
|
|
28
|
+
"""
|
|
29
|
+
symbol_bodies: Dict[str, str]
|
|
30
|
+
cross_signatures: Dict[str, str]
|
|
31
|
+
edge_summaries: List[Dict[str, Any]]
|
|
32
|
+
findings: List[Dict[str, Any]]
|
|
33
|
+
manifest: Dict[str, Any]
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
36
|
+
"""Convert context bundle to dictionary.
|
|
37
|
+
Example:
|
|
38
|
+
>>> bundle = ContextBundle({}, {}, [], [], {})
|
|
39
|
+
>>> "symbol_bodies" in bundle.to_dict()
|
|
40
|
+
True
|
|
41
|
+
"""
|
|
42
|
+
return asdict(self)
|
|
43
|
+
|
|
44
|
+
def to_json(self) -> str:
|
|
45
|
+
"""Convert context bundle to deterministic JSON string.
|
|
46
|
+
Example:
|
|
47
|
+
>>> bundle = ContextBundle({}, {}, [], [], {})
|
|
48
|
+
>>> isinstance(bundle.to_json(), str)
|
|
49
|
+
True
|
|
50
|
+
"""
|
|
51
|
+
return json.dumps(self.to_dict(), sort_keys=True, indent=2)
|
|
52
|
+
|
|
53
|
+
class ContextBuilder:
|
|
54
|
+
"""Builds structured dynamic context bundles from graph traversal.
|
|
55
|
+
Example:
|
|
56
|
+
>>> g = Graph()
|
|
57
|
+
>>> builder = ContextBuilder(g)
|
|
58
|
+
>>> bundle = builder.for_symbol("sym_1", 1)
|
|
59
|
+
>>> bundle.manifest["status"]
|
|
60
|
+
'empty'
|
|
61
|
+
"""
|
|
62
|
+
def __init__(self, graph: Graph) -> None:
|
|
63
|
+
self.graph = graph
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def _comment_prefix(file_id: str) -> str:
|
|
67
|
+
lower = file_id.lower()
|
|
68
|
+
if lower.endswith((".py", ".pyi", ".sh", ".bash", ".zsh", ".yaml", ".yml", ".toml", ".rb")):
|
|
69
|
+
return "#"
|
|
70
|
+
if lower.endswith((".sql", ".lua")):
|
|
71
|
+
return "--"
|
|
72
|
+
return "//"
|
|
73
|
+
|
|
74
|
+
def for_symbol(self, symbol_id: str, depth: int = 1, seen_hashes: Set[str] | None = None) -> ContextBundle:
|
|
75
|
+
"""Build context bundle starting from a symbol outwards by N hops.
|
|
76
|
+
Example:
|
|
77
|
+
>>> g = Graph()
|
|
78
|
+
>>> builder = ContextBuilder(g)
|
|
79
|
+
>>> bundle = builder.for_symbol("sym_1", 1)
|
|
80
|
+
>>> "symbol_id" in bundle.manifest
|
|
81
|
+
True
|
|
82
|
+
"""
|
|
83
|
+
seen_hashes = seen_hashes or set()
|
|
84
|
+
if not self.graph.has_symbol(symbol_id):
|
|
85
|
+
return ContextBundle(
|
|
86
|
+
symbol_bodies={},
|
|
87
|
+
cross_signatures={},
|
|
88
|
+
edge_summaries=[],
|
|
89
|
+
findings=[],
|
|
90
|
+
manifest={"symbol_id": symbol_id, "status": "empty", "reason": "symbol_not_found"},
|
|
91
|
+
)
|
|
92
|
+
syms, edges = self.graph.get_subgraph(symbol_id, depth=depth)
|
|
93
|
+
target_sym = self.graph.get_symbol(symbol_id)
|
|
94
|
+
bodies: Dict[str, str] = {}
|
|
95
|
+
signatures: Dict[str, str] = {}
|
|
96
|
+
for s in syms:
|
|
97
|
+
if s.content_hash in seen_hashes:
|
|
98
|
+
continue
|
|
99
|
+
f_id = s.location.file_id if s.location else ""
|
|
100
|
+
cm = self._comment_prefix(f_id)
|
|
101
|
+
if s.id == symbol_id:
|
|
102
|
+
bodies[s.id] = f"{cm} {f_id}:{s.location.start_line if s.location else 1}\n{s.signature or s.name}"
|
|
103
|
+
else:
|
|
104
|
+
signatures[s.id] = s.signature or f"{s.kind} {s.name}"
|
|
105
|
+
|
|
106
|
+
edge_summaries = [
|
|
107
|
+
{"source": e.source_id, "target": e.target_id, "kind": str(e.kind)}
|
|
108
|
+
for e in edges
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
relevant_findings = [
|
|
112
|
+
f.to_dict() for f in self.graph.get_findings()
|
|
113
|
+
if f.symbol_id in {s.id for s in syms} or f.location.file_id == target_sym.location.file_id
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
manifest = {
|
|
117
|
+
"symbol_id": symbol_id,
|
|
118
|
+
"depth": depth,
|
|
119
|
+
"symbols_included": len(syms),
|
|
120
|
+
"edges_included": len(edges),
|
|
121
|
+
"findings_included": len(relevant_findings),
|
|
122
|
+
"status": "success",
|
|
123
|
+
}
|
|
124
|
+
return ContextBundle(
|
|
125
|
+
symbol_bodies=bodies,
|
|
126
|
+
cross_signatures=signatures,
|
|
127
|
+
edge_summaries=edge_summaries,
|
|
128
|
+
findings=relevant_findings,
|
|
129
|
+
manifest=manifest,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def for_file(self, file_id: str, depth: int = 1, seen_hashes: Set[str] | None = None) -> ContextBundle:
|
|
133
|
+
"""Build context bundle for all symbols in a file.
|
|
134
|
+
Example:
|
|
135
|
+
>>> g = Graph()
|
|
136
|
+
>>> builder = ContextBuilder(g)
|
|
137
|
+
>>> bundle = builder.for_file("app.py", 1)
|
|
138
|
+
>>> bundle.manifest["file_id"]
|
|
139
|
+
'app.py'
|
|
140
|
+
"""
|
|
141
|
+
seen_hashes = seen_hashes or set()
|
|
142
|
+
symbols = self.graph.get_symbols_by_file(file_id)
|
|
143
|
+
bodies: Dict[str, str] = {}
|
|
144
|
+
cm = self._comment_prefix(file_id)
|
|
145
|
+
for s in symbols:
|
|
146
|
+
if s.content_hash not in seen_hashes:
|
|
147
|
+
bodies[s.id] = f"{cm} {file_id}:{s.location.start_line if s.location else 1}\n{s.signature or s.name}"
|
|
148
|
+
relevant_findings = [
|
|
149
|
+
f.to_dict() for f in self.graph.get_findings()
|
|
150
|
+
if f.location.file_id == file_id
|
|
151
|
+
]
|
|
152
|
+
return ContextBundle(
|
|
153
|
+
symbol_bodies=bodies,
|
|
154
|
+
cross_signatures={},
|
|
155
|
+
edge_summaries=[],
|
|
156
|
+
findings=relevant_findings,
|
|
157
|
+
manifest={"file_id": file_id, "symbols_count": len(symbols), "status": "success"},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def for_task(self, task: TaskSpec, depth: int = 1) -> ContextBundle:
|
|
161
|
+
"""Build context bundle tailored to a task specification.
|
|
162
|
+
Example:
|
|
163
|
+
>>> g = Graph()
|
|
164
|
+
>>> builder = ContextBuilder(g)
|
|
165
|
+
>>> spec = TaskSpec("Fix issue")
|
|
166
|
+
>>> bundle = builder.for_task(spec)
|
|
167
|
+
>>> "query" in bundle.manifest
|
|
168
|
+
True
|
|
169
|
+
"""
|
|
170
|
+
combined_bodies: Dict[str, str] = {}
|
|
171
|
+
combined_sigs: Dict[str, str] = {}
|
|
172
|
+
combined_edges: List[Dict[str, Any]] = []
|
|
173
|
+
combined_findings: List[Dict[str, Any]] = []
|
|
174
|
+
for sym_id in task.target_symbols:
|
|
175
|
+
b = self.for_symbol(sym_id, depth=depth)
|
|
176
|
+
combined_bodies.update(b.symbol_bodies)
|
|
177
|
+
combined_sigs.update(b.cross_signatures)
|
|
178
|
+
combined_edges.extend(b.edge_summaries)
|
|
179
|
+
combined_findings.extend(b.findings)
|
|
180
|
+
for file_id in task.target_files:
|
|
181
|
+
b = self.for_file(file_id, depth=depth)
|
|
182
|
+
combined_bodies.update(b.symbol_bodies)
|
|
183
|
+
combined_findings.extend(b.findings)
|
|
184
|
+
return ContextBundle(
|
|
185
|
+
symbol_bodies=combined_bodies,
|
|
186
|
+
cross_signatures=combined_sigs,
|
|
187
|
+
edge_summaries=combined_edges,
|
|
188
|
+
findings=combined_findings,
|
|
189
|
+
manifest={"query": task.query, "symbols": task.target_symbols, "files": task.target_files},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def for_defect(self, finding_id: str) -> ContextBundle:
|
|
193
|
+
"""Build context bundle targeting a specific defect finding.
|
|
194
|
+
Example:
|
|
195
|
+
>>> g = Graph()
|
|
196
|
+
>>> builder = ContextBuilder(g)
|
|
197
|
+
>>> bundle = builder.for_defect("f1")
|
|
198
|
+
>>> bundle.manifest["finding_id"]
|
|
199
|
+
'f1'
|
|
200
|
+
"""
|
|
201
|
+
matching = [f for f in self.graph.get_findings() if f.id == finding_id]
|
|
202
|
+
if not matching:
|
|
203
|
+
return ContextBundle({}, {}, [], [], {"finding_id": finding_id, "status": "not_found"})
|
|
204
|
+
f = matching[0]
|
|
205
|
+
if f.symbol_id:
|
|
206
|
+
return self.for_symbol(f.symbol_id, depth=1)
|
|
207
|
+
return self.for_file(f.location.file_id, depth=1)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""AI agent feedback and repository contribution workflow for codeui."""
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
from codeui.core.repo import clone_git_repo, get_library_repo_url
|
|
10
|
+
from codeui.errors import ContributionError, RepositoryCloneError
|
|
11
|
+
|
|
12
|
+
class ContributionManager:
|
|
13
|
+
"""Manager for AI agents to clone codeui, apply patches, and push modifications.
|
|
14
|
+
Example:
|
|
15
|
+
>>> mgr = ContributionManager()
|
|
16
|
+
>>> mgr.get_repo_url()
|
|
17
|
+
'https://github.com/AAlsubari/codeUI'
|
|
18
|
+
"""
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
repo_url: Optional[str] = None,
|
|
22
|
+
token: Optional[str] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.repo_url = repo_url or get_library_repo_url()
|
|
25
|
+
self.token = token or os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
26
|
+
|
|
27
|
+
def get_repo_url(self) -> str:
|
|
28
|
+
"""Get the target GitHub repository URL.
|
|
29
|
+
Example:
|
|
30
|
+
>>> ContributionManager().get_repo_url()
|
|
31
|
+
'https://github.com/AAlsubari/codeUI'
|
|
32
|
+
"""
|
|
33
|
+
return self.repo_url
|
|
34
|
+
|
|
35
|
+
def _authenticated_url(self, token: Optional[str] = None) -> str:
|
|
36
|
+
auth_token = token or self.token
|
|
37
|
+
url = self.repo_url.strip()
|
|
38
|
+
if auth_token and url.startswith("https://"):
|
|
39
|
+
return url.replace("https://", f"https://x-access-token:{auth_token}@")
|
|
40
|
+
return url
|
|
41
|
+
|
|
42
|
+
def clone_repository(
|
|
43
|
+
self,
|
|
44
|
+
target_dir: Optional[Union[str, Path]] = None,
|
|
45
|
+
token: Optional[str] = None,
|
|
46
|
+
branch: Optional[str] = None,
|
|
47
|
+
) -> Path:
|
|
48
|
+
"""Clone the codeui library repository into target or temporary directory.
|
|
49
|
+
Example:
|
|
50
|
+
>>> mgr = ContributionManager()
|
|
51
|
+
>>> isinstance(mgr.get_repo_url(), str)
|
|
52
|
+
True
|
|
53
|
+
"""
|
|
54
|
+
auth_url = self._authenticated_url(token)
|
|
55
|
+
dest = Path(target_dir) if target_dir else None
|
|
56
|
+
cloned_path, _ = clone_git_repo(auth_url, dest_dir=dest)
|
|
57
|
+
if branch:
|
|
58
|
+
git_bin = shutil.which("git") or "git"
|
|
59
|
+
res = subprocess.run([git_bin, "checkout", branch], cwd=cloned_path, capture_output=True, text=True)
|
|
60
|
+
if res.returncode != 0:
|
|
61
|
+
subprocess.run([git_bin, "checkout", "-b", branch], cwd=cloned_path, capture_output=True, text=True)
|
|
62
|
+
return cloned_path
|
|
63
|
+
|
|
64
|
+
def create_feature_branch(self, repo_dir: Union[str, Path], branch_name: str) -> str:
|
|
65
|
+
"""Create and checkout a new branch in the cloned repository.
|
|
66
|
+
Example:
|
|
67
|
+
>>> mgr = ContributionManager()
|
|
68
|
+
>>> mgr.clean_branch_name("Fix Bug #1")
|
|
69
|
+
'agent/fix-bug-1'
|
|
70
|
+
"""
|
|
71
|
+
git_bin = shutil.which("git") or "git"
|
|
72
|
+
p = Path(repo_dir)
|
|
73
|
+
safe_branch = self.clean_branch_name(branch_name)
|
|
74
|
+
res = subprocess.run([git_bin, "checkout", "-b", safe_branch], cwd=p, capture_output=True, text=True)
|
|
75
|
+
if res.returncode != 0:
|
|
76
|
+
res_co = subprocess.run([git_bin, "checkout", safe_branch], cwd=p, capture_output=True, text=True)
|
|
77
|
+
if res_co.returncode != 0:
|
|
78
|
+
raise ContributionError(f"Failed to create branch '{safe_branch}': {res.stderr.strip()}")
|
|
79
|
+
return safe_branch
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def clean_branch_name(name: str) -> str:
|
|
83
|
+
"""Convert descriptive name into a git-safe branch identifier.
|
|
84
|
+
Example:
|
|
85
|
+
>>> ContributionManager.clean_branch_name("Fix AST Edge Leak!")
|
|
86
|
+
'agent/fix-ast-edge-leak'
|
|
87
|
+
"""
|
|
88
|
+
raw = name.strip().lower()
|
|
89
|
+
if raw.startswith("agent/"):
|
|
90
|
+
raw = raw[6:]
|
|
91
|
+
slug = re.sub(r"[^a-zA-Z0-9_-]+", "-", raw).strip("-")
|
|
92
|
+
if not slug:
|
|
93
|
+
slug = "patch"
|
|
94
|
+
return f"agent/{slug}"
|
|
95
|
+
|
|
96
|
+
def apply_changes(self, repo_dir: Union[str, Path], file_changes: Dict[str, str]) -> List[str]:
|
|
97
|
+
"""Apply modified or new file contents to the repository on disk.
|
|
98
|
+
Example:
|
|
99
|
+
>>> import tempfile
|
|
100
|
+
>>> with tempfile.TemporaryDirectory() as td:
|
|
101
|
+
... mgr = ContributionManager()
|
|
102
|
+
... mgr.apply_changes(td, {"test.txt": "hello"})
|
|
103
|
+
['test.txt']
|
|
104
|
+
"""
|
|
105
|
+
p = Path(repo_dir)
|
|
106
|
+
applied: List[str] = []
|
|
107
|
+
for rel_path, content in file_changes.items():
|
|
108
|
+
target = p / rel_path.lstrip("/\\")
|
|
109
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
target.write_text(content, encoding="utf-8")
|
|
111
|
+
applied.append(rel_path)
|
|
112
|
+
return applied
|
|
113
|
+
|
|
114
|
+
def commit_changes(
|
|
115
|
+
self,
|
|
116
|
+
repo_dir: Union[str, Path],
|
|
117
|
+
message: str,
|
|
118
|
+
author_name: str = "CodeUI Agent",
|
|
119
|
+
author_email: str = "agent@codeui.dev",
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Stage all modifications and commit with structured commit message.
|
|
122
|
+
Example:
|
|
123
|
+
>>> mgr = ContributionManager()
|
|
124
|
+
>>> mgr.get_repo_url()
|
|
125
|
+
'https://github.com/AAlsubari/codeUI'
|
|
126
|
+
"""
|
|
127
|
+
git_bin = shutil.which("git") or "git"
|
|
128
|
+
p = Path(repo_dir)
|
|
129
|
+
subprocess.run([git_bin, "config", "user.name", author_name], cwd=p, capture_output=True, text=True)
|
|
130
|
+
subprocess.run([git_bin, "config", "user.email", author_email], cwd=p, capture_output=True, text=True)
|
|
131
|
+
subprocess.run([git_bin, "add", "-A"], cwd=p, capture_output=True, text=True)
|
|
132
|
+
|
|
133
|
+
res = subprocess.run([git_bin, "commit", "-m", message], cwd=p, capture_output=True, text=True)
|
|
134
|
+
if res.returncode != 0 and "nothing to commit" not in res.stdout.lower() and "nothing to commit" not in res.stderr.lower():
|
|
135
|
+
raise ContributionError(f"Git commit failed: {res.stderr.strip() or res.stdout.strip()}")
|
|
136
|
+
return message
|
|
137
|
+
|
|
138
|
+
def push_changes(
|
|
139
|
+
self,
|
|
140
|
+
repo_dir: Union[str, Path],
|
|
141
|
+
remote: str = "origin",
|
|
142
|
+
branch: Optional[str] = None,
|
|
143
|
+
token: Optional[str] = None,
|
|
144
|
+
) -> Dict[str, Any]:
|
|
145
|
+
"""Push committed changes to remote repository.
|
|
146
|
+
Example:
|
|
147
|
+
>>> mgr = ContributionManager()
|
|
148
|
+
>>> mgr.get_repo_url()
|
|
149
|
+
'https://github.com/AAlsubari/codeUI'
|
|
150
|
+
"""
|
|
151
|
+
git_bin = shutil.which("git") or "git"
|
|
152
|
+
p = Path(repo_dir)
|
|
153
|
+
auth_url = self._authenticated_url(token)
|
|
154
|
+
|
|
155
|
+
curr_branch = branch
|
|
156
|
+
if not curr_branch:
|
|
157
|
+
res_b = subprocess.run([git_bin, "rev-parse", "--abbrev-ref", "HEAD"], cwd=p, capture_output=True, text=True)
|
|
158
|
+
curr_branch = res_b.stdout.strip() or "main"
|
|
159
|
+
|
|
160
|
+
subprocess.run([git_bin, "remote", "set-url", remote, auth_url], cwd=p, capture_output=True, text=True)
|
|
161
|
+
res_push = subprocess.run([git_bin, "push", "-u", remote, curr_branch], cwd=p, capture_output=True, text=True)
|
|
162
|
+
|
|
163
|
+
if res_push.returncode != 0:
|
|
164
|
+
return {
|
|
165
|
+
"success": False,
|
|
166
|
+
"branch": curr_branch,
|
|
167
|
+
"error": res_push.stderr.strip() or res_push.stdout.strip(),
|
|
168
|
+
"message": "Push failed, local commit retained.",
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
"success": True,
|
|
172
|
+
"branch": curr_branch,
|
|
173
|
+
"error": None,
|
|
174
|
+
"message": f"Successfully pushed branch {curr_branch} to {self.repo_url}",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def submit_contribution(
|
|
178
|
+
self,
|
|
179
|
+
title: str,
|
|
180
|
+
description: str,
|
|
181
|
+
file_changes: Dict[str, str],
|
|
182
|
+
branch_name: Optional[str] = None,
|
|
183
|
+
token: Optional[str] = None,
|
|
184
|
+
push: bool = True,
|
|
185
|
+
workspace_dir: Optional[Union[str, Path]] = None,
|
|
186
|
+
) -> Dict[str, Any]:
|
|
187
|
+
"""Execute complete contribution flow: clone, branch, edit, commit, and push.
|
|
188
|
+
Example:
|
|
189
|
+
>>> mgr = ContributionManager()
|
|
190
|
+
>>> isinstance(mgr.get_repo_url(), str)
|
|
191
|
+
True
|
|
192
|
+
"""
|
|
193
|
+
temp_dir_obj = None
|
|
194
|
+
if workspace_dir:
|
|
195
|
+
work_path = Path(workspace_dir).resolve()
|
|
196
|
+
work_path.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
else:
|
|
198
|
+
temp_dir_obj = tempfile.TemporaryDirectory(prefix="codeui_contrib_")
|
|
199
|
+
work_path = Path(temp_dir_obj.name).resolve()
|
|
200
|
+
|
|
201
|
+
push_result: Dict[str, Any] = {"success": False, "message": "Push disabled by caller"}
|
|
202
|
+
try:
|
|
203
|
+
cloned_repo = self.clone_repository(target_dir=work_path, token=token)
|
|
204
|
+
target_branch = self.create_feature_branch(cloned_repo, branch_name or title)
|
|
205
|
+
applied_files = self.apply_changes(cloned_repo, file_changes)
|
|
206
|
+
|
|
207
|
+
commit_msg = f"{title}\n\n{description}\n\nSubmitted by codeui Agent"
|
|
208
|
+
self.commit_changes(cloned_repo, commit_msg)
|
|
209
|
+
|
|
210
|
+
if push:
|
|
211
|
+
push_result = self.push_changes(cloned_repo, branch=target_branch, token=token)
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
"status": "pushed" if push_result.get("success") else "committed_locally",
|
|
215
|
+
"repo_url": self.repo_url,
|
|
216
|
+
"branch": target_branch,
|
|
217
|
+
"changed_files": applied_files,
|
|
218
|
+
"commit_message": commit_msg,
|
|
219
|
+
"push_result": push_result,
|
|
220
|
+
"cloned_dir": str(cloned_repo),
|
|
221
|
+
}
|
|
222
|
+
finally:
|
|
223
|
+
if temp_dir_obj and not push_result.get("success"):
|
|
224
|
+
pass
|