codedna 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.
- codedna/__init__.py +4 -0
- codedna/ai_fingerprint.py +223 -0
- codedna/analyzer.py +245 -0
- codedna/api.py +1505 -0
- codedna/auth.py +372 -0
- codedna/bus_factor.py +259 -0
- codedna/cli.py +1965 -0
- codedna/db.py +336 -0
- codedna/git_hook.py +212 -0
- codedna/integrations/__init__.py +1 -0
- codedna/integrations/github_bot.py +259 -0
- codedna/integrations/jira.py +166 -0
- codedna/integrations/lemonsqueezy.py +236 -0
- codedna/interview.py +298 -0
- codedna/onboarding.py +195 -0
- codedna/plan.py +184 -0
- codedna/protection.py +211 -0
- codedna/rate_limit.py +83 -0
- codedna/scorer.py +221 -0
- codedna/sprint_health.py +187 -0
- codedna/survey.py +104 -0
- codedna/tech_debt.py +232 -0
- codedna-0.2.0.dist-info/METADATA +93 -0
- codedna-0.2.0.dist-info/RECORD +26 -0
- codedna-0.2.0.dist-info/WHEEL +4 -0
- codedna-0.2.0.dist-info/entry_points.txt +2 -0
codedna/db.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""SQLite veritabanı CRUD işlemleri."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Generator, Optional
|
|
8
|
+
|
|
9
|
+
# Varsayılan veritabanı yolu
|
|
10
|
+
DB_PATH = Path.home() / ".codedna" / "codedna.db"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_db_path(repo_path: Optional[Path] = None) -> Path:
|
|
14
|
+
"""Repo'ya özgü veritabanı yolunu döndür."""
|
|
15
|
+
if repo_path:
|
|
16
|
+
return repo_path / ".codedna.db"
|
|
17
|
+
return DB_PATH
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextmanager
|
|
21
|
+
def get_connection(db_path: Optional[Path] = None) -> Generator[sqlite3.Connection, None, None]:
|
|
22
|
+
"""SQLite bağlantısını context manager ile yönet."""
|
|
23
|
+
path = db_path or DB_PATH
|
|
24
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
conn = sqlite3.connect(str(path))
|
|
26
|
+
conn.row_factory = sqlite3.Row
|
|
27
|
+
try:
|
|
28
|
+
yield conn
|
|
29
|
+
conn.commit()
|
|
30
|
+
except Exception:
|
|
31
|
+
conn.rollback()
|
|
32
|
+
raise
|
|
33
|
+
finally:
|
|
34
|
+
conn.close()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def init_db(db_path: Optional[Path] = None) -> None:
|
|
38
|
+
"""Veritabanı şemasını oluştur (yoksa)."""
|
|
39
|
+
with get_connection(db_path) as conn:
|
|
40
|
+
conn.executescript("""
|
|
41
|
+
CREATE TABLE IF NOT EXISTS commits (
|
|
42
|
+
id INTEGER PRIMARY KEY,
|
|
43
|
+
commit_hash TEXT UNIQUE,
|
|
44
|
+
author TEXT,
|
|
45
|
+
timestamp INTEGER,
|
|
46
|
+
files_changed INTEGER,
|
|
47
|
+
understanding_score REAL,
|
|
48
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS file_scores (
|
|
52
|
+
id INTEGER PRIMARY KEY,
|
|
53
|
+
commit_hash TEXT,
|
|
54
|
+
file_path TEXT,
|
|
55
|
+
ai_probability REAL,
|
|
56
|
+
complexity_score REAL,
|
|
57
|
+
comment_ratio REAL,
|
|
58
|
+
understanding_score REAL,
|
|
59
|
+
ai_tool_guess TEXT,
|
|
60
|
+
FOREIGN KEY (commit_hash) REFERENCES commits(commit_hash)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS file_ownership (
|
|
64
|
+
id INTEGER PRIMARY KEY,
|
|
65
|
+
file_path TEXT,
|
|
66
|
+
author TEXT,
|
|
67
|
+
lines_owned INTEGER,
|
|
68
|
+
last_touched INTEGER,
|
|
69
|
+
avg_understanding REAL,
|
|
70
|
+
UNIQUE(file_path, author)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS sprints (
|
|
74
|
+
id INTEGER PRIMARY KEY,
|
|
75
|
+
sprint_name TEXT,
|
|
76
|
+
start_date INTEGER,
|
|
77
|
+
end_date INTEGER,
|
|
78
|
+
total_lines_ai INTEGER,
|
|
79
|
+
total_lines_human INTEGER,
|
|
80
|
+
avg_understanding REAL,
|
|
81
|
+
debt_delta_hours REAL,
|
|
82
|
+
health_score REAL,
|
|
83
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE IF NOT EXISTS protected_modules (
|
|
87
|
+
id INTEGER PRIMARY KEY,
|
|
88
|
+
file_path TEXT UNIQUE,
|
|
89
|
+
min_understanding_threshold REAL DEFAULT 3.5,
|
|
90
|
+
label TEXT,
|
|
91
|
+
added_by TEXT,
|
|
92
|
+
added_at INTEGER,
|
|
93
|
+
last_alert_at INTEGER,
|
|
94
|
+
is_active INTEGER DEFAULT 1
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
CREATE TABLE IF NOT EXISTS interview_sessions (
|
|
98
|
+
id INTEGER PRIMARY KEY,
|
|
99
|
+
candidate_name TEXT,
|
|
100
|
+
file_path TEXT,
|
|
101
|
+
started_at INTEGER,
|
|
102
|
+
completed_at INTEGER,
|
|
103
|
+
questions_json TEXT,
|
|
104
|
+
comprehension_score REAL,
|
|
105
|
+
time_taken_seconds INTEGER,
|
|
106
|
+
evaluator_notes TEXT,
|
|
107
|
+
created_by TEXT
|
|
108
|
+
);
|
|
109
|
+
""")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def save_commit(
|
|
113
|
+
commit_hash: str,
|
|
114
|
+
author: str,
|
|
115
|
+
timestamp: int,
|
|
116
|
+
files_changed: int,
|
|
117
|
+
understanding_score: Optional[float],
|
|
118
|
+
db_path: Optional[Path] = None,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Commit bilgisini kaydet veya güncelle."""
|
|
121
|
+
with get_connection(db_path) as conn:
|
|
122
|
+
conn.execute(
|
|
123
|
+
"""
|
|
124
|
+
INSERT INTO commits (commit_hash, author, timestamp, files_changed, understanding_score)
|
|
125
|
+
VALUES (?, ?, ?, ?, ?)
|
|
126
|
+
ON CONFLICT(commit_hash) DO UPDATE SET
|
|
127
|
+
understanding_score = excluded.understanding_score
|
|
128
|
+
""",
|
|
129
|
+
(commit_hash, author, timestamp, files_changed, understanding_score),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def save_file_score(
|
|
134
|
+
commit_hash: str,
|
|
135
|
+
file_path: str,
|
|
136
|
+
ai_probability: float,
|
|
137
|
+
complexity_score: float,
|
|
138
|
+
comment_ratio: float,
|
|
139
|
+
understanding_score: Optional[float] = None,
|
|
140
|
+
db_path: Optional[Path] = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Dosya analiz skorunu kaydet."""
|
|
143
|
+
with get_connection(db_path) as conn:
|
|
144
|
+
conn.execute(
|
|
145
|
+
"""
|
|
146
|
+
INSERT INTO file_scores
|
|
147
|
+
(commit_hash, file_path, ai_probability, complexity_score, comment_ratio, understanding_score)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
149
|
+
""",
|
|
150
|
+
(commit_hash, file_path, ai_probability, complexity_score, comment_ratio, understanding_score),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_commit_history(limit: int = 20, db_path: Optional[Path] = None) -> list[sqlite3.Row]:
|
|
155
|
+
"""Son N commit'i tarihe göre sıralı getir."""
|
|
156
|
+
with get_connection(db_path) as conn:
|
|
157
|
+
rows = conn.execute(
|
|
158
|
+
"""
|
|
159
|
+
SELECT * FROM commits
|
|
160
|
+
ORDER BY timestamp DESC
|
|
161
|
+
LIMIT ?
|
|
162
|
+
""",
|
|
163
|
+
(limit,),
|
|
164
|
+
).fetchall()
|
|
165
|
+
return rows
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_file_scores_for_commit(commit_hash: str, db_path: Optional[Path] = None) -> list[sqlite3.Row]:
|
|
169
|
+
"""Belirli bir commit'e ait dosya skorlarını getir."""
|
|
170
|
+
with get_connection(db_path) as conn:
|
|
171
|
+
rows = conn.execute(
|
|
172
|
+
"SELECT * FROM file_scores WHERE commit_hash = ?",
|
|
173
|
+
(commit_hash,),
|
|
174
|
+
).fetchall()
|
|
175
|
+
return rows
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_latest_commit(db_path: Optional[Path] = None) -> Optional[sqlite3.Row]:
|
|
179
|
+
"""En son kaydedilen commit'i getir."""
|
|
180
|
+
with get_connection(db_path) as conn:
|
|
181
|
+
row = conn.execute(
|
|
182
|
+
"SELECT * FROM commits ORDER BY timestamp DESC LIMIT 1"
|
|
183
|
+
).fetchone()
|
|
184
|
+
return row
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_latest_understanding_for_file(
|
|
188
|
+
file_path: str, db_path: Optional[Path] = None
|
|
189
|
+
) -> Optional[float]:
|
|
190
|
+
"""Belirli bir dosyanın en son anlama skorunu getir."""
|
|
191
|
+
with get_connection(db_path) as conn:
|
|
192
|
+
row = conn.execute(
|
|
193
|
+
"""
|
|
194
|
+
SELECT fs.understanding_score
|
|
195
|
+
FROM file_scores fs
|
|
196
|
+
JOIN commits c ON fs.commit_hash = c.commit_hash
|
|
197
|
+
WHERE fs.file_path = ? AND fs.understanding_score IS NOT NULL
|
|
198
|
+
ORDER BY c.timestamp DESC
|
|
199
|
+
LIMIT 1
|
|
200
|
+
""",
|
|
201
|
+
(file_path,),
|
|
202
|
+
).fetchone()
|
|
203
|
+
return float(row["understanding_score"]) if row else None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_all_file_understanding_scores(
|
|
207
|
+
db_path: Optional[Path] = None,
|
|
208
|
+
) -> dict[str, float]:
|
|
209
|
+
"""
|
|
210
|
+
Tüm dosyaların en son anlama skorlarını dict olarak getir.
|
|
211
|
+
{file_path: understanding_score} formatında döner.
|
|
212
|
+
"""
|
|
213
|
+
with get_connection(db_path) as conn:
|
|
214
|
+
rows = conn.execute(
|
|
215
|
+
"""
|
|
216
|
+
SELECT fs.file_path, fs.understanding_score
|
|
217
|
+
FROM file_scores fs
|
|
218
|
+
JOIN commits c ON fs.commit_hash = c.commit_hash
|
|
219
|
+
WHERE fs.understanding_score IS NOT NULL
|
|
220
|
+
GROUP BY fs.file_path
|
|
221
|
+
HAVING c.timestamp = MAX(c.timestamp)
|
|
222
|
+
""",
|
|
223
|
+
).fetchall()
|
|
224
|
+
return {r["file_path"]: float(r["understanding_score"]) for r in rows}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def update_understanding_score(
|
|
228
|
+
commit_hash: str,
|
|
229
|
+
understanding_score: float,
|
|
230
|
+
db_path: Optional[Path] = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Commit'in ve ilgili dosyaların anlama skorunu güncelle."""
|
|
233
|
+
with get_connection(db_path) as conn:
|
|
234
|
+
conn.execute(
|
|
235
|
+
"UPDATE commits SET understanding_score = ? WHERE commit_hash = ?",
|
|
236
|
+
(understanding_score, commit_hash),
|
|
237
|
+
)
|
|
238
|
+
# Aynı commit'e ait tüm dosya skorlarını da güncelle
|
|
239
|
+
conn.execute(
|
|
240
|
+
"UPDATE file_scores SET understanding_score = ? WHERE commit_hash = ?",
|
|
241
|
+
(understanding_score, commit_hash),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def upsert_file_ownership(
|
|
246
|
+
file_path: str,
|
|
247
|
+
author: str,
|
|
248
|
+
lines_owned: int,
|
|
249
|
+
last_touched: int,
|
|
250
|
+
avg_understanding: Optional[float] = None,
|
|
251
|
+
db_path: Optional[Path] = None,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Dosya sahiplik kaydını ekle veya güncelle."""
|
|
254
|
+
with get_connection(db_path) as conn:
|
|
255
|
+
conn.execute(
|
|
256
|
+
"""
|
|
257
|
+
INSERT INTO file_ownership
|
|
258
|
+
(file_path, author, lines_owned, last_touched, avg_understanding)
|
|
259
|
+
VALUES (?, ?, ?, ?, ?)
|
|
260
|
+
ON CONFLICT(file_path, author) DO UPDATE SET
|
|
261
|
+
lines_owned = excluded.lines_owned,
|
|
262
|
+
last_touched = excluded.last_touched,
|
|
263
|
+
avg_understanding = COALESCE(excluded.avg_understanding, avg_understanding)
|
|
264
|
+
""",
|
|
265
|
+
(file_path, author, lines_owned, last_touched, avg_understanding),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_file_ownership(
|
|
270
|
+
file_path: Optional[str] = None,
|
|
271
|
+
db_path: Optional[Path] = None,
|
|
272
|
+
) -> list[sqlite3.Row]:
|
|
273
|
+
"""
|
|
274
|
+
Dosya sahiplik kayıtlarını getir.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
file_path: Belirli bir dosya filtrele (None ise tüm dosyalar)
|
|
278
|
+
"""
|
|
279
|
+
with get_connection(db_path) as conn:
|
|
280
|
+
if file_path:
|
|
281
|
+
rows = conn.execute(
|
|
282
|
+
"SELECT * FROM file_ownership WHERE file_path = ? ORDER BY lines_owned DESC",
|
|
283
|
+
(file_path,),
|
|
284
|
+
).fetchall()
|
|
285
|
+
else:
|
|
286
|
+
rows = conn.execute(
|
|
287
|
+
"SELECT * FROM file_ownership ORDER BY file_path, lines_owned DESC"
|
|
288
|
+
).fetchall()
|
|
289
|
+
return rows
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def save_sprint(
|
|
293
|
+
sprint_name: str,
|
|
294
|
+
start_date: int,
|
|
295
|
+
end_date: int,
|
|
296
|
+
total_lines_ai: int,
|
|
297
|
+
total_lines_human: int,
|
|
298
|
+
avg_understanding: Optional[float],
|
|
299
|
+
debt_delta_hours: Optional[float],
|
|
300
|
+
health_score: Optional[float],
|
|
301
|
+
db_path: Optional[Path] = None,
|
|
302
|
+
) -> int:
|
|
303
|
+
"""Sprint kaydı oluştur, yeni kaydın id'sini döndür."""
|
|
304
|
+
with get_connection(db_path) as conn:
|
|
305
|
+
cur = conn.execute(
|
|
306
|
+
"""
|
|
307
|
+
INSERT INTO sprints
|
|
308
|
+
(sprint_name, start_date, end_date, total_lines_ai, total_lines_human,
|
|
309
|
+
avg_understanding, debt_delta_hours, health_score)
|
|
310
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
311
|
+
""",
|
|
312
|
+
(sprint_name, start_date, end_date, total_lines_ai, total_lines_human,
|
|
313
|
+
avg_understanding, debt_delta_hours, health_score),
|
|
314
|
+
)
|
|
315
|
+
return cur.lastrowid or 0
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_sprint_history(
|
|
319
|
+
limit: int = 10,
|
|
320
|
+
db_path: Optional[Path] = None,
|
|
321
|
+
) -> list[sqlite3.Row]:
|
|
322
|
+
"""Geçmiş sprint'leri tarihe göre sıralı getir."""
|
|
323
|
+
with get_connection(db_path) as conn:
|
|
324
|
+
rows = conn.execute(
|
|
325
|
+
"SELECT * FROM sprints ORDER BY start_date DESC LIMIT ?",
|
|
326
|
+
(limit,),
|
|
327
|
+
).fetchall()
|
|
328
|
+
return rows
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def get_latest_sprint(db_path: Optional[Path] = None) -> Optional[sqlite3.Row]:
|
|
332
|
+
"""En son sprint kaydını getir."""
|
|
333
|
+
with get_connection(db_path) as conn:
|
|
334
|
+
return conn.execute(
|
|
335
|
+
"SELECT * FROM sprints ORDER BY start_date DESC LIMIT 1"
|
|
336
|
+
).fetchone()
|
codedna/git_hook.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Git hook kurulum ve yönetim modülü."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# Post-commit hook içeriği
|
|
13
|
+
HOOK_TEMPLATE = """#!/bin/bash
|
|
14
|
+
# CodeDNA post-commit hook
|
|
15
|
+
# Otomatik olarak codedna tarafından oluşturuldu.
|
|
16
|
+
#
|
|
17
|
+
# Anketin çalışması için stdin'i doğrudan terminale (/dev/tty) bağla.
|
|
18
|
+
# Git hook'ları bazı durumlarda stdin'i kapalı/boş bir akışa yönlendirir;
|
|
19
|
+
# bu satır olmadan IntPrompt anında EOFError alıp anketi sessizce atlıyor.
|
|
20
|
+
#
|
|
21
|
+
# Koşullar:
|
|
22
|
+
# [ -t 1 ] → çıkış bir terminale bağlı (CI/pipe'da false → anket sorulmaz, bu doğru)
|
|
23
|
+
# [ -r /dev/tty ] → /dev/tty gerçekten okunabilir
|
|
24
|
+
if [ -t 1 ] && [ -r /dev/tty ]; then
|
|
25
|
+
exec < /dev/tty
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# codedna'nın PATH'te olup olmadığını kontrol et
|
|
29
|
+
if command -v codedna &> /dev/null; then
|
|
30
|
+
codedna status --hook
|
|
31
|
+
else
|
|
32
|
+
# uv ile çalıştırmayı dene (geliştirme ortamı için)
|
|
33
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
34
|
+
if [ -f "$REPO_ROOT/.venv/bin/codedna" ]; then
|
|
35
|
+
"$REPO_ROOT/.venv/bin/codedna" status --hook
|
|
36
|
+
elif [ -f "$HOME/.local/bin/codedna" ]; then
|
|
37
|
+
"$HOME/.local/bin/codedna" status --hook
|
|
38
|
+
else
|
|
39
|
+
echo "[CodeDNA] 'codedna' komutu bulunamadı. 'pip install codedna' deneyin."
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def find_git_root(start_path: Optional[Path] = None) -> Optional[Path]:
|
|
46
|
+
"""
|
|
47
|
+
Verilen yoldan yukarı doğru .git dizinini ara.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
start_path: Arama başlangıç noktası (None ise mevcut dizin)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
.git dizinini içeren repo kökü veya None
|
|
54
|
+
"""
|
|
55
|
+
yol = Path(start_path or Path.cwd()).resolve()
|
|
56
|
+
for ebeveyn in [yol, *yol.parents]:
|
|
57
|
+
if (ebeveyn / ".git").exists():
|
|
58
|
+
return ebeveyn
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def install_hook(repo_path: Optional[Path] = None) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Post-commit hook'u kur.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
repo_path: Git repo kök dizini (None ise otomatik bul)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Başarıyla kurulduysa True
|
|
71
|
+
"""
|
|
72
|
+
kok = repo_path or find_git_root()
|
|
73
|
+
if not kok:
|
|
74
|
+
console.print("[bold red]Hata:[/bold red] Git repo bulunamadı. 'git init' ile başlatın.")
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
hooks_dir = kok / ".git" / "hooks"
|
|
78
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
hook_dosyasi = hooks_dir / "post-commit"
|
|
80
|
+
|
|
81
|
+
# Mevcut hook varsa yedekle
|
|
82
|
+
if hook_dosyasi.exists():
|
|
83
|
+
yedek = hooks_dir / "post-commit.backup"
|
|
84
|
+
hook_dosyasi.rename(yedek)
|
|
85
|
+
console.print(f"[yellow]Mevcut hook yedeklendi:[/yellow] {yedek}")
|
|
86
|
+
|
|
87
|
+
# Yeni hook'u yaz
|
|
88
|
+
hook_dosyasi.write_text(HOOK_TEMPLATE, encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
# Çalıştırılabilir yap (chmod +x)
|
|
91
|
+
mevcut_mod = hook_dosyasi.stat().st_mode
|
|
92
|
+
hook_dosyasi.chmod(mevcut_mod | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
93
|
+
|
|
94
|
+
console.print(f"[green]✓[/green] Hook kuruldu: {hook_dosyasi}")
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def uninstall_hook(repo_path: Optional[Path] = None) -> bool:
|
|
99
|
+
"""
|
|
100
|
+
Post-commit hook'u kaldır.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
repo_path: Git repo kök dizini (None ise otomatik bul)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Başarıyla kaldırıldıysa True
|
|
107
|
+
"""
|
|
108
|
+
kok = repo_path or find_git_root()
|
|
109
|
+
if not kok:
|
|
110
|
+
console.print("[bold red]Hata:[/bold red] Git repo bulunamadı.")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
hook_dosyasi = kok / ".git" / "hooks" / "post-commit"
|
|
114
|
+
|
|
115
|
+
if not hook_dosyasi.exists():
|
|
116
|
+
console.print("[yellow]Post-commit hook bulunamadı, zaten kaldırılmış.[/yellow]")
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
# Yedekten geri yükle
|
|
120
|
+
yedek = kok / ".git" / "hooks" / "post-commit.backup"
|
|
121
|
+
if yedek.exists():
|
|
122
|
+
hook_dosyasi.unlink()
|
|
123
|
+
yedek.rename(hook_dosyasi)
|
|
124
|
+
console.print("[green]✓[/green] Önceki hook geri yüklendi.")
|
|
125
|
+
else:
|
|
126
|
+
hook_dosyasi.unlink()
|
|
127
|
+
console.print("[green]✓[/green] CodeDNA hook kaldırıldı.")
|
|
128
|
+
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def install_ci_workflow(repo_path: Optional[Path] = None) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
GitHub Actions CI şablonunu repo'ya yaz.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
repo_path: Git repo kök dizini (None ise otomatik bul)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Başarıyla yazıldıysa True
|
|
141
|
+
"""
|
|
142
|
+
import importlib.resources
|
|
143
|
+
|
|
144
|
+
kok = repo_path or find_git_root()
|
|
145
|
+
if not kok:
|
|
146
|
+
console.print("[bold red]Hata:[/bold red] Git repo bulunamadı.")
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
workflows_dir = kok / ".github" / "workflows"
|
|
150
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
hedef = workflows_dir / "codedna.yml"
|
|
152
|
+
|
|
153
|
+
if hedef.exists():
|
|
154
|
+
console.print("[yellow]⚠[/yellow] .github/workflows/codedna.yml zaten mevcut, üzerine yazılmıyor.")
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
# Paketle birlikte gelen şablonu kopyala
|
|
158
|
+
try:
|
|
159
|
+
sablon_yolu = Path(__file__).parent.parent / ".github" / "workflows" / "codedna.yml"
|
|
160
|
+
if sablon_yolu.exists():
|
|
161
|
+
hedef.write_text(sablon_yolu.read_text(encoding="utf-8"), encoding="utf-8")
|
|
162
|
+
else:
|
|
163
|
+
# Fallback: inline şablon
|
|
164
|
+
hedef.write_text(_CI_SABLON, encoding="utf-8")
|
|
165
|
+
console.print(f"[green]✓[/green] CI şablonu oluşturuldu: [dim]{hedef}[/dim]")
|
|
166
|
+
return True
|
|
167
|
+
except Exception as e:
|
|
168
|
+
console.print(f"[red]✗[/red] CI şablonu yazılamadı: {e}")
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# GitHub Actions şablon içeriği (fallback)
|
|
173
|
+
_CI_SABLON = """\
|
|
174
|
+
name: CodeDNA Analysis
|
|
175
|
+
|
|
176
|
+
on:
|
|
177
|
+
push:
|
|
178
|
+
branches: ["**"]
|
|
179
|
+
pull_request:
|
|
180
|
+
branches: ["**"]
|
|
181
|
+
|
|
182
|
+
jobs:
|
|
183
|
+
analyze:
|
|
184
|
+
name: AI Kod Analizi
|
|
185
|
+
runs-on: ubuntu-latest
|
|
186
|
+
steps:
|
|
187
|
+
- uses: actions/checkout@v4
|
|
188
|
+
with:
|
|
189
|
+
fetch-depth: 0
|
|
190
|
+
- uses: actions/setup-python@v5
|
|
191
|
+
with:
|
|
192
|
+
python-version: "3.10"
|
|
193
|
+
- run: pip install codedna
|
|
194
|
+
- run: codedna scan
|
|
195
|
+
continue-on-error: true
|
|
196
|
+
- name: Yüksek risk kontrolü
|
|
197
|
+
run: |
|
|
198
|
+
codedna scan --min-risk 0.7 && echo "✅ Yüksek riskli dosya yok." || echo "⚠️ Yüksek riskli dosyalar tespit edildi."
|
|
199
|
+
continue-on-error: true
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def is_hook_installed(repo_path: Optional[Path] = None) -> bool:
|
|
204
|
+
"""CodeDNA hook'unun kurulu olup olmadığını kontrol et."""
|
|
205
|
+
kok = repo_path or find_git_root()
|
|
206
|
+
if not kok:
|
|
207
|
+
return False
|
|
208
|
+
hook_dosyasi = kok / ".git" / "hooks" / "post-commit"
|
|
209
|
+
if not hook_dosyasi.exists():
|
|
210
|
+
return False
|
|
211
|
+
icerik = hook_dosyasi.read_text(encoding="utf-8", errors="replace")
|
|
212
|
+
return "CodeDNA" in icerik
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CodeDNA dış servis entegrasyonları."""
|