cortexcode 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.
cortexcode/git_diff.py ADDED
@@ -0,0 +1,157 @@
1
+ """Git diff-aware context — show only changed symbols."""
2
+
3
+ import json
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def get_changed_files(root: Path, ref: str = "HEAD") -> list[str]:
10
+ """Get list of files changed since ref (default: uncommitted changes)."""
11
+ try:
12
+ # Unstaged + staged changes
13
+ result = subprocess.run(
14
+ ["git", "diff", "--name-only", ref],
15
+ capture_output=True, text=True, cwd=str(root)
16
+ )
17
+ files = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set()
18
+
19
+ # Also include staged changes
20
+ result2 = subprocess.run(
21
+ ["git", "diff", "--cached", "--name-only"],
22
+ capture_output=True, text=True, cwd=str(root)
23
+ )
24
+ if result2.stdout.strip():
25
+ files.update(result2.stdout.strip().split("\n"))
26
+
27
+ # Also include untracked files
28
+ result3 = subprocess.run(
29
+ ["git", "ls-files", "--others", "--exclude-standard"],
30
+ capture_output=True, text=True, cwd=str(root)
31
+ )
32
+ if result3.stdout.strip():
33
+ files.update(result3.stdout.strip().split("\n"))
34
+
35
+ return [f for f in files if f]
36
+ except (subprocess.SubprocessError, FileNotFoundError):
37
+ return []
38
+
39
+
40
+ def get_changed_lines(root: Path, file_path: str, ref: str = "HEAD") -> list[tuple[int, int]]:
41
+ """Get ranges of changed lines in a file."""
42
+ try:
43
+ result = subprocess.run(
44
+ ["git", "diff", "-U0", ref, "--", file_path],
45
+ capture_output=True, text=True, cwd=str(root)
46
+ )
47
+
48
+ ranges = []
49
+ for line in result.stdout.split("\n"):
50
+ if line.startswith("@@"):
51
+ # Parse @@ -old,count +new,count @@
52
+ parts = line.split("+")
53
+ if len(parts) >= 2:
54
+ new_part = parts[1].split(" ")[0].split(",")
55
+ start = int(new_part[0])
56
+ count = int(new_part[1]) if len(new_part) > 1 else 1
57
+ ranges.append((start, start + count))
58
+
59
+ return ranges
60
+ except (subprocess.SubprocessError, FileNotFoundError, ValueError):
61
+ return []
62
+
63
+
64
+ def get_diff_context(index_path: Path, ref: str = "HEAD") -> dict[str, Any]:
65
+ """Get context for only the changed symbols since ref.
66
+
67
+ Returns symbols that are in files that have been modified,
68
+ with indicators of which ones are in changed line ranges.
69
+ """
70
+ index = json.loads(index_path.read_text(encoding="utf-8"))
71
+ files = index.get("files", {})
72
+ call_graph = index.get("call_graph", {})
73
+ root = Path(index.get("project_root", "."))
74
+
75
+ changed_files = get_changed_files(root, ref)
76
+ if not changed_files:
77
+ return {
78
+ "ref": ref,
79
+ "changed_files": 0,
80
+ "changed_symbols": [],
81
+ "affected_symbols": [],
82
+ }
83
+
84
+ changed_symbols = []
85
+ affected_symbol_names = set()
86
+
87
+ for changed_file in changed_files:
88
+ # Normalize path separators
89
+ norm_file = changed_file.replace("\\", "/")
90
+
91
+ # Find matching file in index
92
+ file_data = None
93
+ matched_path = None
94
+ for rel_path, data in files.items():
95
+ if rel_path.replace("\\", "/") == norm_file:
96
+ file_data = data
97
+ matched_path = rel_path
98
+ break
99
+
100
+ if not file_data or not isinstance(file_data, dict):
101
+ continue
102
+
103
+ symbols = file_data.get("symbols", [])
104
+ changed_ranges = get_changed_lines(root, changed_file, ref)
105
+
106
+ for sym in symbols:
107
+ sym_line = sym.get("line", 0)
108
+ in_changed_range = any(start <= sym_line <= end for start, end in changed_ranges) if changed_ranges else True
109
+
110
+ entry = {
111
+ "name": sym.get("name"),
112
+ "type": sym.get("type"),
113
+ "file": matched_path,
114
+ "line": sym_line,
115
+ "changed": in_changed_range,
116
+ "params": sym.get("params", []),
117
+ "calls": sym.get("calls", []),
118
+ }
119
+
120
+ if sym.get("doc"):
121
+ entry["doc"] = sym["doc"]
122
+
123
+ changed_symbols.append(entry)
124
+
125
+ if in_changed_range:
126
+ affected_symbol_names.add(sym.get("name"))
127
+
128
+ # Find symbols affected by the changes (callers of changed symbols)
129
+ affected_symbols = []
130
+ for name, calls in call_graph.items():
131
+ if any(c in affected_symbol_names for c in calls):
132
+ if name not in affected_symbol_names:
133
+ # Find the file for this symbol
134
+ for rel_path, data in files.items():
135
+ if not isinstance(data, dict):
136
+ continue
137
+ for sym in data.get("symbols", []):
138
+ if sym.get("name") == name:
139
+ affected_symbols.append({
140
+ "name": name,
141
+ "type": sym.get("type"),
142
+ "file": rel_path,
143
+ "line": sym.get("line"),
144
+ "reason": f"calls changed symbol",
145
+ "calls_changed": [c for c in calls if c in affected_symbol_names],
146
+ })
147
+ break
148
+ else:
149
+ continue
150
+ break
151
+
152
+ return {
153
+ "ref": ref,
154
+ "changed_files": len(changed_files),
155
+ "changed_symbols": changed_symbols,
156
+ "affected_symbols": affected_symbols[:20],
157
+ }