Skip to content

Commit c54245b

Browse files
author
Lukas Geiger
committed
fix: improve git editor status handling
1 parent 9de183b commit c54245b

7 files changed

Lines changed: 148 additions & 22 deletions

File tree

.gitattributes

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
* text=auto
2+
3+
*.py text eol=lf
4+
*.json text eol=lf
5+
*.md text eol=lf
6+
*.yml text eol=lf
7+
*.yaml text eol=lf
8+
*.bat text eol=crlf
9+
10+
*.png binary
11+
*.ico binary

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/).
2222
- Doppelte `run_script`-Definition in `PythonArchitect` beseitigt, damit F5 wieder konsistent über das Debug-Output-Panel läuft.
2323
- Entfernte Qt6-APIs `fontMetrics().width()` und `setTabStopWidth()` durch aktuelle Alternativen ersetzt.
2424
- Externe Python-Skripte starten jetzt mit `sys.executable` statt einem hardcodierten `python`/`python3`.
25-
2625
- Die Minimap-Option im Einstellungsdialog nutzt jetzt denselben `show_minimap`-Key wie das Ansicht-Menü und wird auch über den Apply-Button direkt auf die Hauptansicht angewendet.
26+
- Kombinierte Git-Porcelain-Statuscodes wie `AM` werden in der Statusleiste lesbar zusammengefasst.
27+
- Git-Diff-Markierungen behandeln ersetzte Zeilen als geändert statt als reine Hinzufügung.
28+
- `Speichern unter` stellt den bisherigen Dateipfad wieder her, wenn der Dialog abgebrochen wird.
29+
- Deutsche Übersetzungshinweise und Docstrings nutzen echte Umlaute.
2730

2831
## [1.0.0] - YYYY-MM-DD
2932

PythonBox_v8.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,38 @@ class GitIntegration:
11761176
def __init__(self):
11771177
self.has_git = shutil.which("git") is not None
11781178

1179+
@staticmethod
1180+
def _describe_status_code(code: str) -> str:
1181+
"""Formatiert einen Git-Porcelain-Status in eine lesbare Beschreibung."""
1182+
if not code:
1183+
return ""
1184+
if code == "??":
1185+
return "? Untracked"
1186+
if code == "!!":
1187+
return "! Ignored"
1188+
1189+
status_labels = []
1190+
label_map = {
1191+
"M": "● Modified",
1192+
"A": "+ Added",
1193+
"D": "− Deleted",
1194+
"R": "→ Renamed",
1195+
"C": "⊕ Copied",
1196+
"U": "⚡ Conflict",
1197+
}
1198+
1199+
for char in code:
1200+
if char == " ":
1201+
continue
1202+
label = label_map.get(char)
1203+
if label and label not in status_labels:
1204+
status_labels.append(label)
1205+
1206+
if status_labels:
1207+
return " / ".join(status_labels)
1208+
1209+
return code.strip() or code
1210+
11791211
def get_repo_root(self, file_path: str) -> Optional[str]:
11801212
"""Findet das Git-Repository Root"""
11811213
if not self.has_git:
@@ -1208,17 +1240,8 @@ def get_file_status(self, file_path: str) -> str:
12081240
)
12091241
status = result.stdout.strip()
12101242
if status:
1211-
code = status[:2].strip()
1212-
status_map = {
1213-
'M': '● Modified',
1214-
'A': '+ Added',
1215-
'D': '− Deleted',
1216-
'??': '? Untracked',
1217-
'R': '→ Renamed',
1218-
'C': '⊕ Copied',
1219-
'U': '⚡ Conflict'
1220-
}
1221-
return status_map.get(code, code)
1243+
code = status[:2]
1244+
return self._describe_status_code(code)
12221245
return "✓ Unchanged"
12231246
except Exception:
12241247
return ""
@@ -1252,19 +1275,27 @@ def get_modified_lines(self, file_path: str) -> Tuple[set, set, set]:
12521275
return added, modified, deleted
12531276

12541277
current_line = 0
1278+
pending_deletion = False
12551279
for line in diff.split('\n'):
12561280
if line.startswith('@@'):
12571281
# Parse @@ -start,count +start,count @@
12581282
match = re.search(r'\+(\d+)', line)
12591283
if match:
12601284
current_line = int(match.group(1)) - 1
1285+
pending_deletion = False
12611286
elif line.startswith('+') and not line.startswith('+++'):
12621287
current_line += 1
1263-
added.add(current_line)
1288+
if pending_deletion:
1289+
modified.add(current_line)
1290+
else:
1291+
added.add(current_line)
1292+
pending_deletion = False
12641293
elif line.startswith('-') and not line.startswith('---'):
12651294
deleted.add(current_line)
1295+
pending_deletion = True
12661296
else:
12671297
current_line += 1
1298+
pending_deletion = False
12681299

12691300
return added, modified, deleted
12701301

@@ -3330,9 +3361,11 @@ def save_file(self):
33303361
def save_file_as(self):
33313362
tab = self.tab_editor.current_tab()
33323363
if tab:
3364+
original_path = tab.file_path
33333365
tab.file_path = None # Force "Save As" dialog
33343366
index = self.tab_editor.tab_widget.currentIndex()
3335-
self.tab_editor.save_tab(index)
3367+
if not self.tab_editor.save_tab(index):
3368+
tab.file_path = original_path
33363369

33373370
# --- LIBRARY TREE ---
33383371

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ PythonBox is a lightweight Python IDE with a dark theme, integrated debugging, c
2424
- Debug-Toolbar mit Step In, Step Over und Step Out
2525
- Linter-Integration für Pylint und Flake8
2626
- Git-Status, Diff und Modified-Markierung
27+
- Kombinierte Git-Statuscodes werden lesbar angezeigt; ersetzte Diff-Zeilen werden als geändert statt nur als hinzugefügt markiert
2728
- Qt6-kompatible Editor-Metriken und F5-Ausführung über das Debug-Output-Panel
29+
- `Speichern unter` behält bei abgebrochenem Dialog den bisherigen Dateipfad
2830
- Die Minimap-Einstellung bleibt zwischen Ansicht-Menü und Einstellungsdialog synchron, inklusive Fallback für ältere Konfigurationen
2931

3032
### Windows-Paketierung
@@ -65,7 +67,7 @@ Das Build-Ergebnis liegt anschließend in `dist/`. Build-Artefakte und lokale Re
6567

6668
## Tests
6769

68-
Die Regressionstests prüfen die Qt6-API-Kompatibilität, die F5-Ausführung über `debug_output.run_normal`, die externe Terminal-Ausführung mit dem aktuellen Python-Interpreter, die Minimap-Einstellungssynchronisation und einen Offscreen-Smoke-Test für das Hauptfenster.
70+
Die Regressionstests prüfen die Qt6-API-Kompatibilität, die F5-Ausführung über `debug_output.run_normal`, die externe Terminal-Ausführung mit dem aktuellen Python-Interpreter, die Minimap-Einstellungssynchronisation, Git-Status-/Diff-Erkennung, `Speichern unter`-Abbruchverhalten und einen Offscreen-Smoke-Test für das Hauptfenster.
6971

7072
```bash
7173
python -m unittest discover -s tests -v

locales/translations.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
"de": "⬇️ Einfügen",
1616
"en": "⬇️ Insert"
1717
}
18-
}
18+
}

tests/test_pythonbox_regressions.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import ast
22
import importlib.util
33
import os
4+
import shutil
5+
import subprocess
46
import tempfile
57
import unittest
68
from pathlib import Path
@@ -105,6 +107,57 @@ def test_non_windows_external_commands_use_current_interpreter(self):
105107
self.assertEqual(["/opt/current-python/bin/python", "tool.py"], cmd)
106108

107109

110+
@unittest.skipUnless(shutil.which("git") is not None, "git required")
111+
class GitIntegrationRegressionTests(unittest.TestCase):
112+
def _create_repo_with_tracked_file(self, initial_text: str):
113+
tmp_dir = Path(tempfile.mkdtemp(prefix="pythonbox-git-"))
114+
subprocess.run(["git", "init"], cwd=tmp_dir, check=True, capture_output=True)
115+
116+
file_path = tmp_dir / "demo.py"
117+
file_path.write_text(initial_text, encoding="utf-8")
118+
subprocess.run(["git", "-C", str(tmp_dir), "add", "demo.py"], check=True, capture_output=True)
119+
120+
self.addCleanup(shutil.rmtree, tmp_dir, True)
121+
return tmp_dir, file_path
122+
123+
def test_git_status_formats_combined_codes_readably(self):
124+
module = load_pythonbox_module()
125+
126+
_, file_path = self._create_repo_with_tracked_file("a\nb\nc\n")
127+
file_path.write_text("a\nX\nc\n", encoding="utf-8")
128+
129+
git = module.GitIntegration()
130+
status = git.get_file_status(str(file_path))
131+
132+
self.assertEqual("+ Added / ● Modified", status)
133+
134+
def test_git_modified_lines_classify_replacements_as_modified(self):
135+
module = load_pythonbox_module()
136+
137+
_, file_path = self._create_repo_with_tracked_file("a\nb\nc\n")
138+
file_path.write_text("a\nX\nc\n", encoding="utf-8")
139+
140+
git = module.GitIntegration()
141+
added, modified, deleted = git.get_modified_lines(str(file_path))
142+
143+
self.assertEqual([], sorted(added))
144+
self.assertEqual([2], sorted(modified))
145+
self.assertEqual([1], sorted(deleted))
146+
147+
def test_git_modified_lines_classify_insertions_as_added(self):
148+
module = load_pythonbox_module()
149+
150+
_, file_path = self._create_repo_with_tracked_file("a\nc\n")
151+
file_path.write_text("a\nb\nc\n", encoding="utf-8")
152+
153+
git = module.GitIntegration()
154+
added, modified, deleted = git.get_modified_lines(str(file_path))
155+
156+
self.assertEqual([2], sorted(added))
157+
self.assertEqual([], sorted(modified))
158+
self.assertEqual([], sorted(deleted))
159+
160+
108161
class SettingsRegressionTests(unittest.TestCase):
109162
def _temp_settings(self, module, folder: str):
110163
settings_path = Path(folder) / "settings.ini"
@@ -158,6 +211,30 @@ def test_main_window_reacts_to_minimap_setting_changes(self):
158211
window.deleteLater()
159212
app.processEvents()
160213

214+
def test_save_file_as_cancel_keeps_original_path(self):
215+
module = load_pythonbox_module()
216+
app = module.QApplication.instance() or module.QApplication([])
217+
218+
with tempfile.TemporaryDirectory() as temp_dir:
219+
original_path = Path(temp_dir) / "demo.py"
220+
original_path.write_text("print('hi')\n", encoding="utf-8")
221+
222+
window = module.PythonArchitect()
223+
try:
224+
tab = window.tab_editor.current_tab()
225+
tab.file_path = str(original_path)
226+
227+
with mock.patch.object(
228+
module.QFileDialog, "getSaveFileName", return_value=("", "")
229+
):
230+
window.save_file_as()
231+
232+
self.assertEqual(str(original_path), tab.file_path)
233+
finally:
234+
window.close()
235+
window.deleteLater()
236+
app.processEvents()
237+
161238

162239
if __name__ == "__main__":
163240
unittest.main()

translator.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
TranslationSystem - Multi-Language Support fuer Anwendungen
2+
TranslationSystem - Multi-Language Support für Anwendungen
33
============================================================
44
Version: 1.0.0 (isoliert aus _LANG)
55
Quelle: ARC_EntwicklungsschleifeAdvanced/TranslationSystem.py v2.4
@@ -9,7 +9,7 @@
99
from translator import TranslationSystem
1010
1111
translator = TranslationSystem('de')
12-
label.setText(translator.t('Datei oeffnen'))
12+
label.setText(translator.t('Datei öffnen'))
1313
translator.set_language('en')
1414
"""
1515

@@ -49,8 +49,8 @@ def __init__(self, default_lang: str = 'de', app_dir: Path = None):
4949
]
5050

5151
self.german_hints = [
52-
"datei", "bearbeiten", "ansicht", "hilfe", "oeffnen", "speichern",
53-
"schliessen", "einstellungen", "abbrechen", "ok", "ja", "nein",
52+
"datei", "bearbeiten", "ansicht", "hilfe", "oeffnen", "öffnen", "speichern",
53+
"schliessen", "schließen", "einstellungen", "abbrechen", "ok", "ja", "nein",
5454
"start", "stop", "pause", "fortsetzen", "laden", "aktualisieren",
5555
"filter", "fehler", "export", "import", "optionen", "anzeigen",
5656
]
@@ -75,13 +75,13 @@ def _save_translations(self):
7575

7676
def t(self, key: str) -> str:
7777
"""
78-
Uebersetzt einen Key in die aktuelle Sprache.
78+
Übersetzt einen Key in die aktuelle Sprache.
7979
8080
Args:
8181
key: Translation-Key (oft der deutsche Originaltext)
8282
8383
Returns:
84-
Uebersetzter Text oder Key als Fallback
84+
Übersetzter Text oder Key als Fallback
8585
"""
8686
if key in self.translations:
8787
return self.translations[key].get(self.current_lang, key)

0 commit comments

Comments
 (0)