Compare commits
2 Commits
c2fb4ca08e
...
5e4c03d0cd
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e4c03d0cd | |||
| 6b5d6586dc |
@@ -30,7 +30,9 @@ tools:
|
||||
|
||||
1. **`docs/decision-methodology.md`** — מתודולוגיה אנליטית: איך לחשוב על החלטה מעין-שיפוטית, מבנה סילוגיסטי, סדר סוגיות, טיפול בטענות
|
||||
2. **`docs/block-schema.md`** — ארכיטקטורת 12 בלוקים
|
||||
3. **`docs/legal-decision-lessons.md`** — לקחים מהחלטות קודמות
|
||||
3. **`docs/daphna-block-zayin-claims.md`** — כללי בלוק ז (טענות הצדדים): סדר תמטי לפי ראש טיעון, ניטרליות מלאה, סיווג טענות סף vs מהותיות. **הניתוח שלך הוא הקלט לבלוק ז של ה-writer — אם תסווג שגוי או תפספס טענה, זה ייכשל גם בבלוק ז וגם בבלוק י.**
|
||||
4. **`docs/daphna-precedent-network.md`** — לכל סוגיה משפטית, איזה תקדם מועדף של דפנה. שימושי כשעורר/משיב מסתמך על תקדם — לדעת אם זה תקדם בקאנון.
|
||||
5. **`docs/legal-decision-lessons.md`** — לקחים מהחלטות קודמות
|
||||
|
||||
## שפה
|
||||
|
||||
|
||||
@@ -48,10 +48,24 @@ tools:
|
||||
|
||||
| מסמך | תוכן | מתי לקרוא |
|
||||
|------|-------|-----------|
|
||||
| `docs/daphna-decision-tree.md` | **כלי הפעולה היומיומי** — עץ החלטה: מהי הראיה הניצחת? איזו תבנית? איזה אורך? | **לפני כל החלטה** |
|
||||
| `docs/decision-methodology.md` | מתודולוגיה אנליטית — סילוגיזמים, סדר סוגיות, איזון | **לפני כל החלטה** |
|
||||
| `docs/block-schema.md` | הגדרת 12 בלוקים — content model, constraints | **לפני כל החלטה** |
|
||||
| `docs/legal-decision-lessons.md` | לקחים מ-3 החלטות — מה עבד, מה השתנה | **לפני כל החלטה** |
|
||||
|
||||
### מסמכי הקול של דפנה (להפנייה לסוכנים)
|
||||
|
||||
הסוכנים שלך (writer, qa, researcher, analyst) קוראים את מסמכי הקול בעצמם. **התפקיד שלך**: לוודא שהם **קוראים** אותם, ולנתב את הסוכן הנכון לפי סוג התיק.
|
||||
|
||||
| מסמך | תפקיד | סוכן רלוונטי |
|
||||
|------|--------|---------------|
|
||||
| `docs/daphna-voice-fingerprint.md` | קבועי הקול | writer + qa |
|
||||
| `docs/daphna-precedent-network.md` | קאנון תקדמים | researcher + writer + qa |
|
||||
| `docs/daphna-architecture-by-outcome.md` | מבנה בלוק י לפי תוצאה | writer + qa |
|
||||
| `docs/daphna-acceptance-architecture.md` | 5 תבניות קבלה | writer + qa (אם תוצאה = קבלה) |
|
||||
| `docs/daphna-block-zayin-claims.md` | כללי בלוק ז | analyst + writer + qa |
|
||||
| `docs/voice-1130-25.md` | דוגמה עמוקה | writer (אם תיק 1xxx מורכב) |
|
||||
|
||||
## הסוכנים שלך
|
||||
|
||||
| סוכן | Agent ID | תפקיד |
|
||||
|
||||
@@ -32,7 +32,18 @@ tools:
|
||||
|
||||
אם issue מכוון לתיק שלא בטווח שלך — סרב ודווח ב-comment.
|
||||
|
||||
## 6 בדיקות
|
||||
## לפני שאתה מתחיל — קרא את מסמכי הקול
|
||||
|
||||
בלי קריאת מסמכי הקול, אינך יכול לבדוק שה-writer עקב אחר הסגנון של דפנה.
|
||||
|
||||
1. **`docs/daphna-decision-tree.md`** — תקציר תפעולי. ממנו תגיע למסמכים הספציפיים לפי שאלה.
|
||||
2. **`docs/daphna-voice-fingerprint.md`** — קבועי הקול (פעלי "אנחנו", אנטי-דפוסים, ביטויי קישור)
|
||||
3. **`docs/daphna-architecture-by-outcome.md`** — מבנה בלוק י לפי תוצאה
|
||||
4. **`docs/daphna-acceptance-architecture.md`** — חמש תבניות קבלה. **חובה אם התיק קבלה (לא חלקית)**
|
||||
5. **`docs/daphna-block-zayin-claims.md`** — כללי בלוק ז (טענות הצדדים)
|
||||
6. **`docs/daphna-precedent-network.md`** — לכל סוגיה משפטית, איזה תקדם דפנה מצטטת
|
||||
|
||||
## 7 בדיקות
|
||||
|
||||
### 1. שלמות מבנית (structural_integrity)
|
||||
- כל בלוקי חובה קיימים (ה עד יא)
|
||||
@@ -74,6 +85,44 @@ tools:
|
||||
- אין "נוסחאות ריקות" (משפטים שמחיקתם לא משנה כלום)?
|
||||
- ציטוטים עטופים בסנדוויץ' (הקדמה → ציטוט → ניתוח)?
|
||||
|
||||
### 8. עמידה בקול דפנה (voice_compliance)
|
||||
מבוסס על 6 מסמכי הקול. בדוק:
|
||||
|
||||
#### בלוק ז (מ-`daphna-block-zayin-claims.md`)
|
||||
- כותרת **"תמצית טענות הצדדים"** (לא "טענות הצדדים")?
|
||||
- כל צד מקבל כותרת משנה (טענות העוררים / תגובת הוועדה / תגובת מבקשי ההיתר)?
|
||||
- אין רשימה ממוספרת `(1)... (2)...` בתוך פסקה?
|
||||
- אין מילות הערכה ("בצדק", "בטעות", "משכנעת")?
|
||||
- אין גילוי מסקנה עתידית ("טענה זו תידחה בהמשך")?
|
||||
- אין ציטוטי פסיקה ארוכים — רק שם + הפניה?
|
||||
- קול פעיל ("העורר טוען") ולא פסיביזציה ("טענות העורר היו")?
|
||||
|
||||
#### בלוק י (מ-`daphna-voice-fingerprint.md` + `daphna-architecture-by-outcome.md`)
|
||||
- כותרת בלוק י = **"דיון והכרעה"** (קבוע)?
|
||||
- קול "אנחנו" פעיל — אין "הוועדה מוצאת" אלא "מצאנו"?
|
||||
- כל פועל "אנחנו" נושא תפקיד — אין "נחדד" כפתיחת פסקה אקראית?
|
||||
- דפוס "אכן... אולם" לטענות שנדחות (לא דחייה במשפט אחד)?
|
||||
- אין רשימה ממוספרת באנליזה?
|
||||
- אין מספור פסקאות סדרתי (1., 2., 3.) — מגמה ישנה שנטושה ב-2025+?
|
||||
- כותרות משנה רק אם 3+ סוגיות מובחנות (לא בתיק עם סוגיה אחת)?
|
||||
- ציטוטי פסיקה במלואם (4-15 שורות), לא תמציות?
|
||||
- אם תיק 1xxx מורכב — מסגור פילוסופי בפתיחה?
|
||||
- אם תיק 8xxx עם הכרעה שמאית — ציטוט בר"מ 3644/13 קיים?
|
||||
- "למעלה מן הצורך" לטיעונים מרכזיים?
|
||||
- אין רטוריקה דרמטית של הצדדים בקול ההכרעה?
|
||||
- אין תוצאה הכל-או-לא-כלום בתיק עם טענות מהותיות משני הצדדים?
|
||||
|
||||
#### תקדמים (מ-`daphna-precedent-network.md`)
|
||||
- לכל סוגיה משפטית — האם נבחר התקדים המועדף של דפנה?
|
||||
- האם יש תקדים אישי שלה רלוונטי? אם כן — האם הופנה אליו (חיסכון / דחייה / הבחנה)?
|
||||
|
||||
#### תבנית קבלה (מ-`daphna-acceptance-architecture.md` — אם תוצאה = קבלה)
|
||||
- האם הסיבה לקבלה ברורה: פגם פנימי / החזרה / תיקונים / 8xxx מהותית / שומה?
|
||||
- האם התבנית הנבחרת (A/B/C/D/E) מתאימה לסיבה?
|
||||
- האם פורמט הסיום נכון לתבנית? (תבנית A: "מתבטלת"; B: "תיקבע לדיון" + הוראת הבהרה; C: "בכפוף לתיקונים"; D: "דרישת התשלום בטלה"; E: "השומה תושב לתיקון")
|
||||
- בתבנית A: יש "הודאת צד נגדי" ו"השמטה רחבה"?
|
||||
- בתבנית C: יש פסקת הכרה בוועדה ("פעלה נכון בקיום הדיון")?
|
||||
|
||||
## חומרה
|
||||
|
||||
| בדיקה | חומרה | משמעות |
|
||||
@@ -85,6 +134,7 @@ tools:
|
||||
| כפילות | warning | מדווח, לא חוסם |
|
||||
| מספור | warning | מדווח, לא חוסם |
|
||||
| מתודולוגיה | critical | חוסם ייצוא |
|
||||
| **קול דפנה** | **critical** | **חוסם ייצוא** |
|
||||
|
||||
## תהליך עבודה
|
||||
|
||||
|
||||
@@ -37,8 +37,10 @@ tools:
|
||||
|
||||
## לפני שאתה מתחיל — קרא!
|
||||
|
||||
1. **מתודולוגיה אנליטית**: `docs/decision-methodology.md` — במיוחד סעיפים ד.2 (התחל מלשון הטקסט), ד.3 (שלושה מקורות להנחה עליונה), ז (ציטוטים ואזכורי פסיקה)
|
||||
2. לקחים מהחלטות קודמות: `docs/legal-decision-lessons.md`
|
||||
1. **רשת תקדמים של דפנה**: `docs/daphna-precedent-network.md` — **קריאת חובה**. לכל סוגיה משפטית, יש לדפנה תקדם **מועדף** שהיא מצטטת באופן עקבי (אייזן/רוזן/שפר/הרמלין/חוף השרון/בר"מ 3644/13 גלר וכו'). אל תחפש תקדמים אקראיים — בדוק את הקאנון שלה תחילה.
|
||||
2. **מתודולוגיה אנליטית**: `docs/decision-methodology.md` — במיוחד סעיפים ד.2 (התחל מלשון הטקסט), ד.3 (שלושה מקורות להנחה עליונה), ז (ציטוטים ואזכורי פסיקה)
|
||||
3. **תקדמים אישיים של דפנה**: השתמש ב-`search_decisions` לפני שמציעים תקדם חיצוני. אם דפנה כבר הכריעה בסוגיה זהה — התקדם שלה הוא חלק מהקאנון.
|
||||
4. לקחים מהחלטות קודמות: `docs/legal-decision-lessons.md`
|
||||
|
||||
## סוגי מסמכים שאתה מטפל בהם
|
||||
|
||||
@@ -69,8 +71,19 @@ tools:
|
||||
- **רמת התקדים**: עליון / מנהלי / ועדת ערר ארצית / ועדת ערר מחוזית
|
||||
- **הלכה מחייבת או אמרת אגב**
|
||||
- **כיצד ישרת את מבנה ההנמקה**: כ"כלל" (הנחה עליונה), כ"הרחבה" (Explanation ב-CREAC), או כאנלוגיה
|
||||
- **האם זה תקדם מהקאנון של דפנה?** (בדוק `docs/daphna-precedent-network.md` — אם כן, ציין שזה התקדם המועדף שלה לסוגיה)
|
||||
4. הפק הפניות (`extract_references`)
|
||||
|
||||
### שלב 2ב: בדיקה מצטלבת מול הקאנון של דפנה
|
||||
אחרי שאספת את הפסיקה הרלוונטית בתיק:
|
||||
1. **לכל סוגיה משפטית** בתיק — בדוק ב-`daphna-precedent-network.md`:
|
||||
- האם יש תקדם מועדף של דפנה לסוגיה?
|
||||
- האם הוא הוצג בכתבי הטענות? אם לא — סמן כתקדם שיש להוסיף
|
||||
2. **תקדמים אישיים**: `search_decisions` בקטגוריה זהה לתיק. אם דפנה כבר הכריעה בסוגיה דומה:
|
||||
- אם תוצאה דומה: תקדם לחיסכון דוקטרינרי ("כפי שקבענו ב-X")
|
||||
- אם תוצאה הפוכה: ציין כי **חובה** הבחנה (distinguishing)
|
||||
3. **דווח** איזה תקדמים מהקאנון רלוונטיים, ואיזה תקדמים אישיים נמצאו
|
||||
|
||||
### שלב 3: מיפוי תכנית
|
||||
1. קרא הוראות התכנית **במלואן** — לא רק את הסעיף הנטען
|
||||
2. זהה סעיפים רלוונטיים למחלוקת
|
||||
|
||||
92
mcp-server/src/legal_mcp/services/git_sync.py
Normal file
92
mcp-server/src/legal_mcp/services/git_sync.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Git sync helpers for case repos.
|
||||
|
||||
Each case lives in its own git repo with a Gitea remote. The remote URL
|
||||
embeds an auth token (https://chaim:TOKEN@host/...). When the token is
|
||||
rotated in Infisical, repos created with the old token will fail to
|
||||
push silently — only logged at WARNING level. ``commit_and_push``
|
||||
re-injects the *current* token into the existing origin URL on every
|
||||
call, so push survives token rotation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _gitea_token() -> str:
|
||||
return os.environ.get("GITEA_ACCESS_TOKEN") or os.environ.get("GITEA_TOKEN", "")
|
||||
|
||||
|
||||
def _git_env() -> dict:
|
||||
return {
|
||||
"GIT_AUTHOR_NAME": "Ezer Mishpati",
|
||||
"GIT_AUTHOR_EMAIL": "legal@local",
|
||||
"GIT_COMMITTER_NAME": "Ezer Mishpati",
|
||||
"GIT_COMMITTER_EMAIL": "legal@local",
|
||||
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
|
||||
"GIT_TERMINAL_PROMPT": "0",
|
||||
}
|
||||
|
||||
|
||||
def _refresh_remote_url(case_dir: Path, env: dict) -> bool:
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
cwd=case_dir, capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return False
|
||||
current_url = result.stdout.strip()
|
||||
if "@" in current_url and current_url.startswith("https://"):
|
||||
bare_url = "https://" + current_url.split("@", 1)[1]
|
||||
else:
|
||||
bare_url = current_url
|
||||
token = _gitea_token()
|
||||
if not token:
|
||||
return True # Push without auth — will fail, but caller decides what to do
|
||||
auth_url = bare_url.replace("https://", f"https://chaim:{token}@")
|
||||
if auth_url != current_url:
|
||||
subprocess.run(
|
||||
["git", "remote", "set-url", "origin", auth_url],
|
||||
cwd=case_dir, capture_output=True, env=env,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def commit_and_push(case_dir: str | Path, message: str) -> bool:
|
||||
"""Stage, commit, refresh origin URL with current token, and push.
|
||||
|
||||
Best-effort: on failure logs at WARNING and returns False, but never
|
||||
raises. Continues to push even if the commit was a no-op (in case
|
||||
earlier commits are unpushed).
|
||||
"""
|
||||
case_dir = Path(case_dir)
|
||||
if not (case_dir / ".git").exists():
|
||||
return False
|
||||
|
||||
env = _git_env()
|
||||
|
||||
subprocess.run(["git", "add", "."], cwd=case_dir, capture_output=True, env=env)
|
||||
commit = subprocess.run(
|
||||
["git", "commit", "-m", message],
|
||||
cwd=case_dir, capture_output=True, text=True, env=env,
|
||||
)
|
||||
if commit.returncode != 0 and "nothing to commit" not in commit.stdout:
|
||||
logger.warning("Git commit failed in %s: %s", case_dir, commit.stderr or commit.stdout)
|
||||
|
||||
if not _refresh_remote_url(case_dir, env):
|
||||
logger.warning("No origin remote configured in %s — skipping push", case_dir)
|
||||
return False
|
||||
|
||||
push = subprocess.run(
|
||||
["git", "push"],
|
||||
cwd=case_dir, capture_output=True, text=True, env=env,
|
||||
)
|
||||
if push.returncode != 0:
|
||||
logger.warning("Git push failed in %s: %s", case_dir, push.stderr)
|
||||
return False
|
||||
return True
|
||||
@@ -13,7 +13,7 @@ from uuid import UUID
|
||||
import httpx
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import audit, db, practice_area as pa
|
||||
from legal_mcp.services import audit, db, git_sync, practice_area as pa
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -315,21 +315,13 @@ async def case_update(
|
||||
|
||||
updated = await db.update_case(UUID(case["id"]), **fields)
|
||||
|
||||
# Git commit the update (best-effort)
|
||||
# Git commit + push the update (best-effort)
|
||||
try:
|
||||
case_dir = config.find_case_dir(case_number)
|
||||
if case_dir.exists():
|
||||
case_json = case_dir / "case.json"
|
||||
case_json.write_text(json.dumps(updated, default=str, ensure_ascii=False, indent=2))
|
||||
subprocess.run(["git", "add", "case.json"], cwd=case_dir, capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", f"עדכון תיק: {', '.join(fields.keys())}"],
|
||||
cwd=case_dir,
|
||||
capture_output=True,
|
||||
env={"GIT_AUTHOR_NAME": "Ezer Mishpati", "GIT_AUTHOR_EMAIL": "legal@local",
|
||||
"GIT_COMMITTER_NAME": "Ezer Mishpati", "GIT_COMMITTER_EMAIL": "legal@local",
|
||||
"PATH": "/usr/bin:/bin"},
|
||||
)
|
||||
git_sync.commit_and_push(case_dir, f"עדכון תיק: {', '.join(fields.keys())}")
|
||||
except Exception:
|
||||
pass # git not available — non-critical
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import db, processor
|
||||
from legal_mcp.services import db, git_sync, processor
|
||||
|
||||
|
||||
async def document_upload(
|
||||
@@ -67,11 +66,10 @@ async def document_upload(
|
||||
await db.update_document(UUID(doc["id"]), doc_type=classified_type)
|
||||
doc["doc_type"] = classified_type
|
||||
|
||||
# Git commit (best-effort — don't fail upload on git errors)
|
||||
# Git commit + push (best-effort — don't fail upload on git errors)
|
||||
try:
|
||||
repo_dir = config.find_case_dir(case_number)
|
||||
if repo_dir.exists():
|
||||
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
|
||||
doc_type_hebrew = {
|
||||
"appeal": "כתב ערר",
|
||||
"response": "תשובה",
|
||||
@@ -85,14 +83,7 @@ async def document_upload(
|
||||
"exhibit": "נספח",
|
||||
"reference": "מסמך עזר",
|
||||
}.get(actual_doc_type, actual_doc_type)
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", f"הוספת {doc_type_hebrew}: {title}"],
|
||||
cwd=repo_dir,
|
||||
capture_output=True,
|
||||
env={"GIT_AUTHOR_NAME": "Ezer Mishpati", "GIT_AUTHOR_EMAIL": "legal@local",
|
||||
"GIT_COMMITTER_NAME": "Ezer Mishpati", "GIT_COMMITTER_EMAIL": "legal@local",
|
||||
"PATH": "/usr/bin:/bin"},
|
||||
)
|
||||
git_sync.commit_and_push(repo_dir, f"הוספת {doc_type_hebrew}: {title}")
|
||||
except Exception:
|
||||
pass # git not available in container — non-critical
|
||||
|
||||
|
||||
30
web/app.py
30
web/app.py
@@ -34,7 +34,7 @@ from legal_mcp.tools import cases as cases_tools, search as search_tools, workfl
|
||||
# Import integration clients (same directory)
|
||||
_web_dir = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_web_dir.parent))
|
||||
from web.gitea_client import create_repo, setup_remote_and_push
|
||||
from web.gitea_client import commit_and_push, create_repo, setup_remote_and_push
|
||||
from web.paperclip_client import (
|
||||
archive_project as pc_archive_project,
|
||||
create_project as pc_create_project,
|
||||
@@ -3005,26 +3005,11 @@ async def _process_tagged_document(task_id: str, dest: Path, case_number: str, c
|
||||
_progress[task_id] = {"status": "processing", "filename": display_name, "step": "extracting"}
|
||||
result = await processor.process_document(doc_id, case_id)
|
||||
|
||||
# Git commit + push (best-effort — don't fail upload on git errors)
|
||||
try:
|
||||
repo_dir = config.find_case_dir(case_number)
|
||||
if repo_dir.exists():
|
||||
env = {
|
||||
"GIT_AUTHOR_NAME": "Ezer Mishpati", "GIT_AUTHOR_EMAIL": "legal@local",
|
||||
"GIT_COMMITTER_NAME": "Ezer Mishpati", "GIT_COMMITTER_EMAIL": "legal@local",
|
||||
"PATH": "/usr/bin:/bin",
|
||||
}
|
||||
doc_type_hebrew = DOC_TYPE_NAMES.get(doc_type, doc_type)
|
||||
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", f"הוספת {doc_type_hebrew}: {display_name}"],
|
||||
cwd=repo_dir, capture_output=True, env=env,
|
||||
)
|
||||
# Try to push to Gitea (non-blocking)
|
||||
subprocess.run(["git", "push"], cwd=repo_dir, capture_output=True, env={
|
||||
**env,
|
||||
"GIT_TERMINAL_PROMPT": "0",
|
||||
})
|
||||
commit_and_push(repo_dir, f"הוספת {doc_type_hebrew}: {display_name}")
|
||||
except Exception:
|
||||
logger.warning("Git commit/push failed for %s (non-critical)", display_name)
|
||||
|
||||
@@ -3360,20 +3345,13 @@ async def _process_case_document(task_id: str, source: Path, req: ClassifyReques
|
||||
try:
|
||||
repo_dir = config.find_case_dir(req.case_number)
|
||||
if repo_dir.exists():
|
||||
subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
|
||||
doc_type_hebrew = {
|
||||
"appeal": "כתב ערר", "response": "תשובה", "decision": "החלטה",
|
||||
"reference": "מסמך עזר", "exhibit": "נספח",
|
||||
}.get(req.doc_type, req.doc_type)
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", f"הוספת {doc_type_hebrew}: {title}"],
|
||||
cwd=repo_dir, capture_output=True,
|
||||
env={"GIT_AUTHOR_NAME": "Ezer Mishpati", "GIT_AUTHOR_EMAIL": "legal@local",
|
||||
"GIT_COMMITTER_NAME": "Ezer Mishpati", "GIT_COMMITTER_EMAIL": "legal@local",
|
||||
"PATH": "/usr/bin:/bin"},
|
||||
)
|
||||
commit_and_push(repo_dir, f"הוספת {doc_type_hebrew}: {title}")
|
||||
except Exception:
|
||||
logger.warning("Git commit failed for %s (non-critical)", req.filename)
|
||||
logger.warning("Git commit/push failed for %s (non-critical)", req.filename)
|
||||
|
||||
# Remove from uploads
|
||||
source.unlink(missing_ok=True)
|
||||
|
||||
@@ -48,24 +48,96 @@ async def create_repo(case_number: str, title: str, description: str = "") -> di
|
||||
return resp.json()
|
||||
|
||||
|
||||
def setup_remote_and_push(case_dir: str | Path, repo_clone_url: str) -> bool:
|
||||
"""Add Gitea as remote 'origin' (or update it) and push."""
|
||||
case_dir = Path(case_dir)
|
||||
if not (case_dir / ".git").exists():
|
||||
return False
|
||||
|
||||
env = {
|
||||
def _git_env() -> dict:
|
||||
return {
|
||||
"GIT_AUTHOR_NAME": "Ezer Mishpati",
|
||||
"GIT_AUTHOR_EMAIL": "legal@local",
|
||||
"GIT_COMMITTER_NAME": "Ezer Mishpati",
|
||||
"GIT_COMMITTER_EMAIL": "legal@local",
|
||||
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
|
||||
"GIT_TERMINAL_PROMPT": "0",
|
||||
}
|
||||
|
||||
# Inject token into clone URL for auth
|
||||
|
||||
def _refresh_remote_url(case_dir: Path, env: dict) -> bool:
|
||||
"""Re-inject current token into existing origin URL.
|
||||
|
||||
Handles the case where the token in Infisical was rotated but the
|
||||
case repo's origin URL still has the old token embedded.
|
||||
Returns True if origin exists (and was refreshed), False otherwise.
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
cwd=case_dir, capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return False
|
||||
current_url = result.stdout.strip()
|
||||
# Strip any embedded credentials (https://user:token@host -> https://host)
|
||||
if "@" in current_url and current_url.startswith("https://"):
|
||||
bare_url = "https://" + current_url.split("@", 1)[1]
|
||||
else:
|
||||
bare_url = current_url
|
||||
auth_url = bare_url.replace("https://", f"https://chaim:{_token()}@")
|
||||
if auth_url != current_url:
|
||||
subprocess.run(
|
||||
["git", "remote", "set-url", "origin", auth_url],
|
||||
cwd=case_dir, capture_output=True, env=env,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def commit_and_push(case_dir: str | Path, message: str) -> bool:
|
||||
"""Stage, commit, refresh remote URL with current token, and push.
|
||||
|
||||
Best-effort: returns False on any failure, but logs the reason at
|
||||
WARNING level so issues are visible (the previous code swallowed
|
||||
push failures silently).
|
||||
"""
|
||||
case_dir = Path(case_dir)
|
||||
if not (case_dir / ".git").exists():
|
||||
return False
|
||||
|
||||
env = _git_env()
|
||||
|
||||
subprocess.run(["git", "add", "."], cwd=case_dir, capture_output=True, env=env)
|
||||
commit = subprocess.run(
|
||||
["git", "commit", "-m", message],
|
||||
cwd=case_dir, capture_output=True, text=True, env=env,
|
||||
)
|
||||
if commit.returncode != 0 and "nothing to commit" not in commit.stdout:
|
||||
logger.warning("Git commit failed in %s: %s", case_dir, commit.stderr or commit.stdout)
|
||||
# Continue anyway — maybe there were no new changes but we still
|
||||
# want to push earlier unpushed commits.
|
||||
|
||||
if not _refresh_remote_url(case_dir, env):
|
||||
logger.warning("No origin remote configured in %s — skipping push", case_dir)
|
||||
return False
|
||||
|
||||
push = subprocess.run(
|
||||
["git", "push"],
|
||||
cwd=case_dir, capture_output=True, text=True, env=env,
|
||||
)
|
||||
if push.returncode != 0:
|
||||
logger.warning("Git push failed in %s: %s", case_dir, push.stderr)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def setup_remote_and_push(case_dir: str | Path, repo_clone_url: str) -> bool:
|
||||
"""Add Gitea as remote 'origin' (or update it) and push.
|
||||
|
||||
Used on initial case creation. For routine commits + push on
|
||||
existing repos, prefer ``commit_and_push``.
|
||||
"""
|
||||
case_dir = Path(case_dir)
|
||||
if not (case_dir / ".git").exists():
|
||||
return False
|
||||
|
||||
env = _git_env()
|
||||
|
||||
auth_url = repo_clone_url.replace("https://", f"https://chaim:{_token()}@")
|
||||
|
||||
# Check if remote exists
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
cwd=case_dir, capture_output=True, text=True,
|
||||
@@ -81,13 +153,11 @@ def setup_remote_and_push(case_dir: str | Path, repo_clone_url: str) -> bool:
|
||||
cwd=case_dir, capture_output=True, env=env,
|
||||
)
|
||||
|
||||
# Push
|
||||
push_result = subprocess.run(
|
||||
["git", "push", "-u", "origin", "main"],
|
||||
cwd=case_dir, capture_output=True, text=True, env=env,
|
||||
)
|
||||
if push_result.returncode != 0:
|
||||
# Try master branch
|
||||
push_result = subprocess.run(
|
||||
["git", "push", "-u", "origin", "master"],
|
||||
cwd=case_dir, capture_output=True, text=True, env=env,
|
||||
|
||||
Reference in New Issue
Block a user