je-editor 0.0.215__py3-none-any.whl → 0.0.216__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.
- git_client/commit_graph.py +88 -0
- git_client/git.py +181 -0
- git_client/git_cli.py +66 -0
- git_client/github.py +50 -0
- {je_editor-0.0.215.dist-info → je_editor-0.0.216.dist-info}/METADATA +2 -1
- je_editor-0.0.216.dist-info/RECORD +127 -0
- je_editor-0.0.216.dist-info/top_level.txt +4 -0
- pyside_ui/git_ui/commit_table.py +27 -0
- pyside_ui/git_ui/git_branch_tree_widget.py +119 -0
- pyside_ui/git_ui/git_client_gui.py +292 -0
- pyside_ui/git_ui/graph_view.py +174 -0
- pyside_ui/main_ui/ai_widget/ai_config.py +19 -0
- pyside_ui/main_ui/ai_widget/ask_thread.py +17 -0
- pyside_ui/main_ui/ai_widget/chat_ui.py +130 -0
- pyside_ui/main_ui/ai_widget/langchain_interface.py +45 -0
- pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +81 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/dock_menu/build_dock_menu.py +2 -2
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/tab_menu/build_tab_menu.py +2 -2
- {je_editor/utils → utils}/multi_language/english.py +1 -1
- {je_editor/utils → utils}/multi_language/traditional_chinese.py +1 -1
- je_editor/__init__.py +0 -34
- je_editor/__main__.py +0 -18
- je_editor/start_editor.py +0 -17
- je_editor-0.0.215.dist-info/RECORD +0 -117
- je_editor-0.0.215.dist-info/top_level.txt +0 -1
- {je_editor/code_scan → code_scan}/__init__.py +0 -0
- {je_editor/code_scan → code_scan}/ruff_thread.py +0 -0
- {je_editor/code_scan → code_scan}/watchdog_implement.py +0 -0
- {je_editor/code_scan → code_scan}/watchdog_thread.py +0 -0
- {je_editor-0.0.215.dist-info → je_editor-0.0.216.dist-info}/WHEEL +0 -0
- {je_editor-0.0.215.dist-info → je_editor-0.0.216.dist-info}/licenses/LICENSE +0 -0
- {je_editor/pyside_ui → pyside_ui}/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/browser/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/browser/browser_download_window.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/browser/browser_serach_lineedit.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/browser/browser_view.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/browser/browser_widget.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/auto_save/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/auto_save/auto_save_manager.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/auto_save/auto_save_thread.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/code_format/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/code_format/pep8_format.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/code_process/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/code_process/code_exec.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/plaintext_code_edit/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/plaintext_code_edit/code_edit_plaintext.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/running_process_manager.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/shell_process/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/shell_process/shell_exec.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/syntax/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/syntax/python_syntax.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/syntax/syntax_setting.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/textedit_code_result/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/textedit_code_result/code_record.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/variable_inspector/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/code/variable_inspector/inspector_gui.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/ai_dialog/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/ai_dialog/set_ai_dialog.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/file_dialog/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/file_dialog/create_file_dialog.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/file_dialog/open_file_dialog.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/file_dialog/save_file_dialog.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/search_ui/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/search_ui/search_error_box.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/dialog/search_ui/search_text_box.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/console_widget/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/console_widget/console_gui.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/console_widget/qprocess_adapter.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/dock/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/dock/destroy_dock.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/editor/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/editor/editor_widget.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/editor/editor_widget_dock.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/editor/process_input.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/ipython_widget/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/ipython_widget/rich_jupyter.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/main_editor.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/dock_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/file_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/file_menu/build_file_menu.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/help_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/help_menu/build_help_menu.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/language_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/language_menu/build_language_server.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/python_env_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/python_env_menu/build_venv_menu.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/build_run_menu.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/build_debug_menu.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/build_program_menu.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/build_shell_menu.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/utils.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/set_menu_bar.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/style_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/style_menu/build_style_menu.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/tab_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/text_menu/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/menu/text_menu/build_text_menu.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/save_settings/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/save_settings/setting_utils.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/save_settings/user_color_setting_file.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/save_settings/user_setting_file.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/system_tray/__init__.py +0 -0
- {je_editor/pyside_ui → pyside_ui}/main_ui/system_tray/extend_system_tray.py +0 -0
- {je_editor/utils → utils}/__init__.py +0 -0
- {je_editor/utils → utils}/encodings/__init__.py +0 -0
- {je_editor/utils → utils}/encodings/python_encodings.py +0 -0
- {je_editor/utils → utils}/exception/__init__.py +0 -0
- {je_editor/utils → utils}/exception/exception_tags.py +0 -0
- {je_editor/utils → utils}/exception/exceptions.py +0 -0
- {je_editor/utils → utils}/file/__init__.py +0 -0
- {je_editor/utils → utils}/file/open/__init__.py +0 -0
- {je_editor/utils → utils}/file/open/open_file.py +0 -0
- {je_editor/utils → utils}/file/save/__init__.py +0 -0
- {je_editor/utils → utils}/file/save/save_file.py +0 -0
- {je_editor/utils → utils}/json/__init__.py +0 -0
- {je_editor/utils → utils}/json/json_file.py +0 -0
- {je_editor/utils → utils}/json_format/__init__.py +0 -0
- {je_editor/utils → utils}/json_format/json_process.py +0 -0
- {je_editor/utils → utils}/logging/__init__.py +0 -0
- {je_editor/utils → utils}/logging/loggin_instance.py +0 -0
- {je_editor/utils → utils}/multi_language/__init__.py +0 -0
- {je_editor/utils → utils}/multi_language/multi_language_wrapper.py +0 -0
- {je_editor/utils → utils}/redirect_manager/__init__.py +0 -0
- {je_editor/utils → utils}/redirect_manager/redirect_manager_class.py +0 -0
- {je_editor/utils → utils}/venv_check/__init__.py +0 -0
- {je_editor/utils → utils}/venv_check/check_venv.py +0 -0
@@ -0,0 +1,119 @@
|
|
1
|
+
import logging
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from PySide6.QtCore import QTimer, QFileSystemWatcher, Qt
|
5
|
+
from PySide6.QtGui import QAction
|
6
|
+
from PySide6.QtWidgets import (
|
7
|
+
QFileDialog, QToolBar, QMessageBox, QStatusBar,
|
8
|
+
QSplitter, QWidget, QVBoxLayout
|
9
|
+
)
|
10
|
+
|
11
|
+
from je_editor.git_client.commit_graph import CommitGraph
|
12
|
+
from je_editor.git_client.git_cli import GitCLI
|
13
|
+
from je_editor.pyside_ui.git_ui.commit_table import CommitTable
|
14
|
+
from je_editor.pyside_ui.git_ui.graph_view import CommitGraphView
|
15
|
+
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
16
|
+
|
17
|
+
logging.basicConfig(level=logging.INFO)
|
18
|
+
log = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class GitTreeViewGUI(QWidget):
|
22
|
+
def __init__(self):
|
23
|
+
super().__init__()
|
24
|
+
self.language_wrapper_get = language_wrapper.language_word_dict.get
|
25
|
+
self.setWindowTitle(self.language_wrapper_get("git_graph_title"))
|
26
|
+
|
27
|
+
main_layout = QVBoxLayout(self)
|
28
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
29
|
+
main_layout.setSpacing(0)
|
30
|
+
|
31
|
+
toolbar = QToolBar()
|
32
|
+
act_open = QAction(self.language_wrapper_get("git_graph_toolbar_open"), self)
|
33
|
+
act_open.triggered.connect(self.open_repo)
|
34
|
+
toolbar.addAction(act_open)
|
35
|
+
|
36
|
+
act_refresh = QAction(self.language_wrapper_get("git_graph_toolbar_refresh"), self)
|
37
|
+
act_refresh.triggered.connect(self.refresh_graph)
|
38
|
+
toolbar.addAction(act_refresh)
|
39
|
+
|
40
|
+
main_layout.addWidget(toolbar)
|
41
|
+
|
42
|
+
splitter = QSplitter(Qt.Orientation.Horizontal)
|
43
|
+
self.graph_view = CommitGraphView()
|
44
|
+
self.commit_table = CommitTable()
|
45
|
+
splitter.addWidget(self.graph_view)
|
46
|
+
splitter.addWidget(self.commit_table)
|
47
|
+
splitter.setSizes([600, 400])
|
48
|
+
main_layout.addWidget(splitter, 1) # 1 表示可伸縮
|
49
|
+
|
50
|
+
self.status = QStatusBar()
|
51
|
+
main_layout.addWidget(self.status)
|
52
|
+
|
53
|
+
self.repo_path = None
|
54
|
+
self.git = None
|
55
|
+
|
56
|
+
self.watcher = QFileSystemWatcher()
|
57
|
+
self.watcher.directoryChanged.connect(self._on_git_changed)
|
58
|
+
self.watcher.fileChanged.connect(self._on_git_changed)
|
59
|
+
|
60
|
+
self.refresh_timer = QTimer()
|
61
|
+
self.refresh_timer.setSingleShot(True)
|
62
|
+
self.refresh_timer.timeout.connect(self.refresh_graph)
|
63
|
+
|
64
|
+
self.commit_table.selectionModel().selectionChanged.connect(self._on_table_selection)
|
65
|
+
|
66
|
+
def _on_table_selection(self, selected, _):
|
67
|
+
if not selected.indexes():
|
68
|
+
return
|
69
|
+
row = selected.indexes()[0].row()
|
70
|
+
self.graph_view.focus_row(row)
|
71
|
+
|
72
|
+
def open_repo(self):
|
73
|
+
path = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("git_graph_menu_open_repo"))
|
74
|
+
if not path:
|
75
|
+
return
|
76
|
+
repo_path = Path(path)
|
77
|
+
git = GitCLI(repo_path)
|
78
|
+
if not git.is_git_repo():
|
79
|
+
QMessageBox.warning(self, self.language_wrapper_get("git_graph_title"),
|
80
|
+
self.language_wrapper_get("git_graph_error_not_git"))
|
81
|
+
return
|
82
|
+
self.repo_path = repo_path
|
83
|
+
self.git = git
|
84
|
+
self._setup_watcher()
|
85
|
+
self.refresh_graph()
|
86
|
+
|
87
|
+
def _setup_watcher(self):
|
88
|
+
self.watcher.removePaths(self.watcher.files())
|
89
|
+
self.watcher.removePaths(self.watcher.directories())
|
90
|
+
if not self.repo_path:
|
91
|
+
return
|
92
|
+
git_dir = self.repo_path / ".git_client"
|
93
|
+
if git_dir.exists():
|
94
|
+
self.watcher.addPath(str(git_dir))
|
95
|
+
for f in ["HEAD", "packed-refs"]:
|
96
|
+
fp = git_dir / f
|
97
|
+
if fp.exists():
|
98
|
+
self.watcher.addPath(str(fp))
|
99
|
+
refs_dir = git_dir / "refs"
|
100
|
+
if refs_dir.exists():
|
101
|
+
self.watcher.addPath(str(refs_dir))
|
102
|
+
|
103
|
+
def _on_git_changed(self, path):
|
104
|
+
self.refresh_timer.start(500)
|
105
|
+
|
106
|
+
def refresh_graph(self):
|
107
|
+
if not self.git:
|
108
|
+
return
|
109
|
+
try:
|
110
|
+
refs = self.git.get_all_refs()
|
111
|
+
commits = self.git.get_commits(max_count=500)
|
112
|
+
graph = CommitGraph()
|
113
|
+
graph.build(commits, refs)
|
114
|
+
self.graph_view.set_graph(graph)
|
115
|
+
self.commit_table.set_commits(commits)
|
116
|
+
except Exception as e:
|
117
|
+
QMessageBox.critical(self, self.language_wrapper_get("git_graph_title"),
|
118
|
+
f"{self.language_wrapper_get('git_graph_error_exec_failed')}\n{e}")
|
119
|
+
|
@@ -0,0 +1,292 @@
|
|
1
|
+
import sys
|
2
|
+
import traceback
|
3
|
+
|
4
|
+
from PySide6.QtGui import QTextOption
|
5
|
+
from PySide6.QtWidgets import QWidget, QLabel, QPushButton, QComboBox, QHBoxLayout, QListWidget, QPlainTextEdit, \
|
6
|
+
QSizePolicy, QSplitter, QLineEdit, QVBoxLayout, QMessageBox, QFileDialog, QApplication, QInputDialog
|
7
|
+
|
8
|
+
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
9
|
+
from je_editor.git_client.git import Worker, GitService
|
10
|
+
from je_editor.git_client.github import GitCloneHandler
|
11
|
+
|
12
|
+
|
13
|
+
class Gitgui(QWidget):
|
14
|
+
|
15
|
+
def __init__(self):
|
16
|
+
super().__init__()
|
17
|
+
self.git = GitService()
|
18
|
+
self.clone_handler = GitCloneHandler()
|
19
|
+
self.language_wrapper_get = language_wrapper.language_word_dict.get
|
20
|
+
self._init_ui()
|
21
|
+
|
22
|
+
def _init_ui(self):
|
23
|
+
# Top controls
|
24
|
+
self.repo_label = QLabel(self.language_wrapper_get("label_repo_initial"))
|
25
|
+
self.btn_open = QPushButton(self.language_wrapper_get("btn_open_repo"))
|
26
|
+
self.branch_combo = QComboBox()
|
27
|
+
self.btn_checkout = QPushButton(self.language_wrapper_get("btn_switch_branch"))
|
28
|
+
self.btn_pull = QPushButton(self.language_wrapper_get("btn_pull"))
|
29
|
+
self.btn_push = QPushButton(self.language_wrapper_get("btn_push"))
|
30
|
+
self.remote_combo = QComboBox()
|
31
|
+
self.clone_button = QPushButton(self.language_wrapper_get("btn_clone_remote"))
|
32
|
+
self.clone_button.clicked.connect(self._on_clone_remote_repo)
|
33
|
+
|
34
|
+
top = QHBoxLayout()
|
35
|
+
top.addWidget(self.repo_label, 2)
|
36
|
+
top.addWidget(self.btn_open)
|
37
|
+
top.addWidget(QLabel(self.language_wrapper_get("label_remote")))
|
38
|
+
top.addWidget(self.remote_combo)
|
39
|
+
top.addWidget(QLabel(self.language_wrapper_get("label_branch")))
|
40
|
+
top.addWidget(self.branch_combo, 1)
|
41
|
+
top.addWidget(self.btn_checkout)
|
42
|
+
top.addWidget(self.btn_pull)
|
43
|
+
top.addWidget(self.btn_push)
|
44
|
+
top.addWidget(self.clone_button)
|
45
|
+
|
46
|
+
# Left commits list
|
47
|
+
self.commit_list = QListWidget()
|
48
|
+
self.commit_list.setMinimumWidth(380)
|
49
|
+
|
50
|
+
# Right diff viewer
|
51
|
+
self.diff_view = QPlainTextEdit()
|
52
|
+
self.diff_view.setReadOnly(True)
|
53
|
+
self.diff_view.setWordWrapMode(QTextOption.WrapMode.NoWrap)
|
54
|
+
self.diff_view.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
55
|
+
self.diff_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
56
|
+
mono = self.font()
|
57
|
+
mono.setFamily("Consolas")
|
58
|
+
mono.setPointSize(10)
|
59
|
+
self.diff_view.setFont(mono)
|
60
|
+
|
61
|
+
splitter = QSplitter()
|
62
|
+
splitter.addWidget(self.commit_list)
|
63
|
+
splitter.addWidget(self.diff_view)
|
64
|
+
splitter.setStretchFactor(0, 0)
|
65
|
+
splitter.setStretchFactor(1, 1)
|
66
|
+
|
67
|
+
# Bottom commit box
|
68
|
+
self.msg_edit = QLineEdit()
|
69
|
+
self.msg_edit.setPlaceholderText(self.language_wrapper_get("placeholder_commit_message"))
|
70
|
+
self.btn_stage_all = QPushButton(self.language_wrapper_get("btn_stage_all"))
|
71
|
+
self.btn_commit = QPushButton(self.language_wrapper_get("btn_commit"))
|
72
|
+
|
73
|
+
bottom = QHBoxLayout()
|
74
|
+
bottom.addWidget(QLabel(self.language_wrapper_get("label_message")))
|
75
|
+
bottom.addWidget(self.msg_edit, 1)
|
76
|
+
bottom.addWidget(self.btn_stage_all)
|
77
|
+
bottom.addWidget(self.btn_commit)
|
78
|
+
|
79
|
+
# Layout
|
80
|
+
center_layout = QVBoxLayout()
|
81
|
+
center_layout.addLayout(top)
|
82
|
+
center_layout.addWidget(splitter, 1)
|
83
|
+
center_layout.addLayout(bottom)
|
84
|
+
self.setLayout(center_layout)
|
85
|
+
|
86
|
+
# Events
|
87
|
+
self.btn_open.clicked.connect(self.on_open_repo)
|
88
|
+
self.branch_combo.currentTextChanged.connect(self.on_branch_changed)
|
89
|
+
self.btn_checkout.clicked.connect(self.on_checkout)
|
90
|
+
self.commit_list.itemSelectionChanged.connect(self.on_commit_selected)
|
91
|
+
self.btn_stage_all.clicked.connect(self.on_stage_all)
|
92
|
+
self.btn_commit.clicked.connect(self.on_commit)
|
93
|
+
self.btn_pull.clicked.connect(self.on_pull)
|
94
|
+
self.btn_push.clicked.connect(self.on_push)
|
95
|
+
|
96
|
+
self._update_controls(enabled=False)
|
97
|
+
|
98
|
+
# ------------- UI helpers -------------
|
99
|
+
def _update_controls(self, enabled: bool):
|
100
|
+
for w in [
|
101
|
+
self.branch_combo, self.btn_checkout, self.commit_list,
|
102
|
+
self.btn_stage_all, self.btn_commit, self.btn_pull, self.btn_push, self.remote_combo
|
103
|
+
]:
|
104
|
+
w.setEnabled(enabled)
|
105
|
+
|
106
|
+
def _error(self, title: str, err: Exception):
|
107
|
+
traceback.print_exc()
|
108
|
+
QMessageBox.critical(self, title, f"{title}\n\n{err}")
|
109
|
+
|
110
|
+
def _info(self, title: str, msg: str):
|
111
|
+
QMessageBox.information(self, title, msg)
|
112
|
+
|
113
|
+
# ------------- Event handlers -------------
|
114
|
+
def on_open_repo(self):
|
115
|
+
path = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("dialog_choose_repo"))
|
116
|
+
if not path:
|
117
|
+
return
|
118
|
+
try:
|
119
|
+
self.git.open_repo(path)
|
120
|
+
self.repo_label.setText(f"Repo: {path}")
|
121
|
+
self._refresh_branches()
|
122
|
+
self._refresh_remotes()
|
123
|
+
self._update_controls(enabled=True)
|
124
|
+
self._load_commits_async()
|
125
|
+
except Exception as e:
|
126
|
+
self._error(self.language_wrapper_get("err_open_repo"), e)
|
127
|
+
|
128
|
+
def _refresh_remotes(self):
|
129
|
+
self.remote_combo.clear()
|
130
|
+
try:
|
131
|
+
remotes = self.git.remotes()
|
132
|
+
self.remote_combo.addItems(remotes if remotes else [self.language_wrapper_get("default_remote")])
|
133
|
+
except Exception:
|
134
|
+
self.remote_combo.addItem(self.language_wrapper_get("default_remote"))
|
135
|
+
|
136
|
+
def _refresh_branches(self):
|
137
|
+
self.branch_combo.blockSignals(True)
|
138
|
+
self.branch_combo.clear()
|
139
|
+
try:
|
140
|
+
branches = self.git.list_branches()
|
141
|
+
self.branch_combo.addItems(branches)
|
142
|
+
cur = self.git.current_branch()
|
143
|
+
idx = self.branch_combo.findText(cur)
|
144
|
+
if idx >= 0:
|
145
|
+
self.branch_combo.setCurrentIndex(idx)
|
146
|
+
except Exception as e:
|
147
|
+
self._error(self.language_wrapper_get("err_load_branches"), e)
|
148
|
+
finally:
|
149
|
+
self.branch_combo.blockSignals(False)
|
150
|
+
|
151
|
+
def on_branch_changed(self, branch: str):
|
152
|
+
if not branch:
|
153
|
+
return
|
154
|
+
self._load_commits_async(branch)
|
155
|
+
|
156
|
+
def _load_commits_async(self, branch: str | None = None):
|
157
|
+
if branch is None:
|
158
|
+
try:
|
159
|
+
branch = self.git.current_branch()
|
160
|
+
except Exception:
|
161
|
+
return
|
162
|
+
self.commit_list.clear()
|
163
|
+
self.diff_view.setPlainText("")
|
164
|
+
self.worker = Worker(self.git.list_commits, branch, 200)
|
165
|
+
self.worker.done.connect(self._on_commits_loaded)
|
166
|
+
self.worker.start()
|
167
|
+
|
168
|
+
def _on_commits_loaded(self, result, error):
|
169
|
+
if error:
|
170
|
+
self._error(self.language_wrapper_get("err_load_commits"), error)
|
171
|
+
return
|
172
|
+
for c in result:
|
173
|
+
self.commit_list.addItem(f"{c['hexsha'][:8]} {c['date']} {c['author']} {c['summary']}")
|
174
|
+
self.commit_list.setProperty("commit_data", result)
|
175
|
+
|
176
|
+
def on_checkout(self):
|
177
|
+
branch = self.branch_combo.currentText()
|
178
|
+
if not branch:
|
179
|
+
return
|
180
|
+
|
181
|
+
def after(res, err):
|
182
|
+
if err:
|
183
|
+
self._error(self.language_wrapper_get("err_checkout"), err)
|
184
|
+
else:
|
185
|
+
self._refresh_branches()
|
186
|
+
self._load_commits_async(branch)
|
187
|
+
self._info(
|
188
|
+
self.language_wrapper_get("info_checkout_title"),
|
189
|
+
f"{self.language_wrapper_get("info_checkout_msg")} {branch}"
|
190
|
+
)
|
191
|
+
self.worker = Worker(self.git.checkout, branch)
|
192
|
+
self.worker.done.connect(after)
|
193
|
+
self.worker.start()
|
194
|
+
|
195
|
+
def on_commit_selected(self):
|
196
|
+
items = self.commit_list.selectedIndexes()
|
197
|
+
if not items:
|
198
|
+
return
|
199
|
+
idx = items[0].row()
|
200
|
+
data = self.commit_list.property("commit_data") or []
|
201
|
+
if idx >= len(data):
|
202
|
+
return
|
203
|
+
sha = data[idx]["hexsha"]
|
204
|
+
|
205
|
+
def after(res, err):
|
206
|
+
if err:
|
207
|
+
self._error(self.language_wrapper_get("err_read_diff"), err)
|
208
|
+
else:
|
209
|
+
self.diff_view.setPlainText(res)
|
210
|
+
|
211
|
+
self.worker = Worker(self.git.show_diff_of_commit, sha)
|
212
|
+
self.worker.done.connect(after)
|
213
|
+
self.worker.start()
|
214
|
+
|
215
|
+
def on_stage_all(self):
|
216
|
+
def after(res, err):
|
217
|
+
if err:
|
218
|
+
self._error(self.language_wrapper_get("err_stage"), err)
|
219
|
+
else:
|
220
|
+
self._info(self.language_wrapper_get("info_stage_title"), self.language_wrapper_get("info_stage_msg"))
|
221
|
+
|
222
|
+
self.worker = Worker(self.git.stage_all)
|
223
|
+
self.worker.done.connect(after)
|
224
|
+
self.worker.start()
|
225
|
+
|
226
|
+
def on_commit(self):
|
227
|
+
msg = self.msg_edit.text()
|
228
|
+
|
229
|
+
def after(res, err):
|
230
|
+
if err:
|
231
|
+
self._error(self.language_wrapper_get("err_commit"), err)
|
232
|
+
else:
|
233
|
+
self.msg_edit.clear()
|
234
|
+
self._load_commits_async()
|
235
|
+
self._info(self.language_wrapper_get("info_commit_title"), self.language_wrapper_get("info_commit_msg"))
|
236
|
+
|
237
|
+
self.worker = Worker(self.git.commit, msg)
|
238
|
+
self.worker.done.connect(after)
|
239
|
+
self.worker.start()
|
240
|
+
|
241
|
+
def on_pull(self):
|
242
|
+
remote = self.remote_combo.currentText() or self.language_wrapper_get("default_remote")
|
243
|
+
branch = self.branch_combo.currentText()
|
244
|
+
|
245
|
+
def after(res, err):
|
246
|
+
if err:
|
247
|
+
self._error(self.language_wrapper_get("err_pull"), err)
|
248
|
+
else:
|
249
|
+
self._load_commits_async()
|
250
|
+
self._info(self.language_wrapper_get("info_pull_title"), str(res))
|
251
|
+
|
252
|
+
self.worker = Worker(self.git.pull, remote, branch)
|
253
|
+
self.worker.done.connect(after)
|
254
|
+
self.worker.start()
|
255
|
+
|
256
|
+
def on_push(self):
|
257
|
+
remote = self.remote_combo.currentText() or self.language_wrapper_get("default_remote")
|
258
|
+
branch = self.branch_combo.currentText()
|
259
|
+
|
260
|
+
def after(res, err):
|
261
|
+
if err:
|
262
|
+
self._error(self.language_wrapper_get("err_push"), err)
|
263
|
+
else:
|
264
|
+
self._info(self.language_wrapper_get("info_push_title"), str(res))
|
265
|
+
|
266
|
+
self.worker = Worker(self.git.push, remote, branch)
|
267
|
+
self.worker.done.connect(after)
|
268
|
+
self.worker.start()
|
269
|
+
|
270
|
+
def _on_clone_remote_repo(self):
|
271
|
+
"""
|
272
|
+
UI handler for cloning a remote repository.
|
273
|
+
"""
|
274
|
+
url, ok = QInputDialog.getText(self,
|
275
|
+
self.language_wrapper_get("dialog_clone_title"),
|
276
|
+
self.language_wrapper_get("dialog_clone_prompt"))
|
277
|
+
if not ok or not url.strip():
|
278
|
+
return
|
279
|
+
|
280
|
+
local_dir = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("dialog_select_folder"))
|
281
|
+
if not local_dir:
|
282
|
+
return
|
283
|
+
|
284
|
+
try:
|
285
|
+
repo_path = self.clone_handler.clone_repo(url.strip(), local_dir)
|
286
|
+
self.git.open_repo(repo_path)
|
287
|
+
QMessageBox.information(self,
|
288
|
+
self.language_wrapper_get("info_clone_success_title"),
|
289
|
+
f"{self.language_wrapper_get("info_clone_success_msg")} {repo_path}")
|
290
|
+
except Exception as e:
|
291
|
+
QMessageBox.critical(self, self.language_wrapper_get("err_clone_failed_title"), str(e))
|
292
|
+
|
@@ -0,0 +1,174 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from PySide6.QtCore import Qt, QRectF, QPointF
|
6
|
+
from PySide6.QtGui import QPainter, QPainterPath, QPen, QColor, QBrush, QTransform
|
7
|
+
from PySide6.QtWidgets import (
|
8
|
+
QGraphicsView,
|
9
|
+
QGraphicsScene,
|
10
|
+
QGraphicsEllipseItem,
|
11
|
+
QGraphicsPathItem, QGraphicsSimpleTextItem,
|
12
|
+
)
|
13
|
+
|
14
|
+
from je_editor.git_client.commit_graph import CommitGraph
|
15
|
+
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
16
|
+
|
17
|
+
# Layout constants
|
18
|
+
NODE_RADIUS = 6
|
19
|
+
ROW_HEIGHT = 28
|
20
|
+
LANE_WIDTH = 22
|
21
|
+
LEFT_MARGIN = 220 # space for SHA/message text area on the left
|
22
|
+
|
23
|
+
# Colors
|
24
|
+
EDGE_COLOR = QColor("#888")
|
25
|
+
TEXT_COLOR = QColor("#222")
|
26
|
+
BG_COLOR = QColor("#ffffff")
|
27
|
+
|
28
|
+
# A small, repeatable lane palette
|
29
|
+
LANE_COLORS = [
|
30
|
+
QColor("#4C78A8"),
|
31
|
+
QColor("#F58518"),
|
32
|
+
QColor("#E45756"),
|
33
|
+
QColor("#72B7B2"),
|
34
|
+
QColor("#54A24B"),
|
35
|
+
QColor("#EECA3B"),
|
36
|
+
QColor("#B279A2"),
|
37
|
+
QColor("#FF9DA6"),
|
38
|
+
QColor("#9D755D"),
|
39
|
+
QColor("#BAB0AC"),
|
40
|
+
]
|
41
|
+
|
42
|
+
|
43
|
+
def lane_color(lane: int) -> QColor:
|
44
|
+
if lane < 0:
|
45
|
+
return QColor("#999")
|
46
|
+
return LANE_COLORS[lane % len(LANE_COLORS)]
|
47
|
+
|
48
|
+
|
49
|
+
class CommitGraphView(QGraphicsView):
|
50
|
+
def __init__(self, parent=None):
|
51
|
+
super().__init__(parent)
|
52
|
+
self.language_wrapper_get = language_wrapper.language_word_dict.get
|
53
|
+
self.setBackgroundBrush(QBrush(BG_COLOR))
|
54
|
+
self.setRenderHints(
|
55
|
+
self.renderHints()
|
56
|
+
| QPainter.RenderHint.Antialiasing
|
57
|
+
| QPainter.RenderHint.TextAntialiasing
|
58
|
+
)
|
59
|
+
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
60
|
+
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
61
|
+
|
62
|
+
self._scene = QGraphicsScene(self)
|
63
|
+
self.setScene(self._scene)
|
64
|
+
|
65
|
+
self.graph: Optional[CommitGraph] = None
|
66
|
+
self._padding = 40
|
67
|
+
self._zoom = 1.0
|
68
|
+
|
69
|
+
def set_graph(self, graph: CommitGraph):
|
70
|
+
self.graph = graph
|
71
|
+
self._redraw()
|
72
|
+
|
73
|
+
def _lane_x(self, lane: int) -> float:
|
74
|
+
return lane * LANE_WIDTH + NODE_RADIUS * 2
|
75
|
+
|
76
|
+
def _row_y(self, row: int) -> float:
|
77
|
+
return row * ROW_HEIGHT + NODE_RADIUS * 2
|
78
|
+
|
79
|
+
def _redraw(self):
|
80
|
+
self._scene.clear()
|
81
|
+
if not self.graph or not self.graph.nodes:
|
82
|
+
self._scene.setSceneRect(QRectF(0, 0, 800, 400))
|
83
|
+
return
|
84
|
+
|
85
|
+
edge_pen = QPen(EDGE_COLOR, 2)
|
86
|
+
for row, node in enumerate(self.graph.nodes):
|
87
|
+
if not node.parent_shas:
|
88
|
+
continue
|
89
|
+
x0 = self._lane_x(node.lane_index)
|
90
|
+
y0 = self._row_y(row)
|
91
|
+
for p in node.parent_shas:
|
92
|
+
if p not in self.graph.index:
|
93
|
+
continue
|
94
|
+
prow = self.graph.index[p]
|
95
|
+
parent_node = self.graph.nodes[prow]
|
96
|
+
x1 = self._lane_x(parent_node.lane_index)
|
97
|
+
y1 = self._row_y(prow)
|
98
|
+
path = QPainterPath(QPointF(x0, y0))
|
99
|
+
ctrl_y = (y0 + y1) / 2.0
|
100
|
+
path.cubicTo(QPointF(x0, ctrl_y), QPointF(x1, ctrl_y), QPointF(x1, y1))
|
101
|
+
edge_item = QGraphicsPathItem(path)
|
102
|
+
edge_item.setPen(edge_pen)
|
103
|
+
self._scene.addItem(edge_item)
|
104
|
+
|
105
|
+
for row, node in enumerate(self.graph.nodes):
|
106
|
+
cx = self._lane_x(node.lane_index)
|
107
|
+
cy = self._row_y(row)
|
108
|
+
|
109
|
+
circle = QGraphicsEllipseItem(
|
110
|
+
QRectF(cx - NODE_RADIUS, cy - NODE_RADIUS, NODE_RADIUS * 2, NODE_RADIUS * 2)
|
111
|
+
)
|
112
|
+
circle.setBrush(QBrush(lane_color(node.lane_index)))
|
113
|
+
circle.setPen(QPen(Qt.PenStyle.NoPen))
|
114
|
+
circle.setToolTip(self.language_wrapper_get(
|
115
|
+
"git_graph_tooltip_commit"
|
116
|
+
).format(short=node.commit_sha[:7],
|
117
|
+
author=node.author_name,
|
118
|
+
date=node.commit_date,
|
119
|
+
msg=node.commit_message))
|
120
|
+
self._scene.addItem(circle)
|
121
|
+
|
122
|
+
label_item = QGraphicsSimpleTextItem(str(row + 1))
|
123
|
+
label_item.setBrush(QBrush(TEXT_COLOR))
|
124
|
+
label_item.setPos(-30, cy - NODE_RADIUS * 2)
|
125
|
+
self._scene.addItem(label_item)
|
126
|
+
|
127
|
+
self._scene.setSceneRect(
|
128
|
+
QRectF(-40, 0,
|
129
|
+
self._lane_x(max(n.lane_index for n in self.graph.nodes) + 1) + self._padding,
|
130
|
+
self._row_y(len(self.graph.nodes)) + self._padding)
|
131
|
+
)
|
132
|
+
|
133
|
+
def _apply_initial_view(self):
|
134
|
+
# Start with a mild zoom to fit rows comfortably
|
135
|
+
self._zoom = 0.9
|
136
|
+
self._apply_zoom_transform()
|
137
|
+
|
138
|
+
# Zoom and interaction
|
139
|
+
def wheelEvent(self, event):
|
140
|
+
if not self._scene.items():
|
141
|
+
return
|
142
|
+
|
143
|
+
# Ctrl+Wheel to zoom, else scroll normally
|
144
|
+
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
145
|
+
angle = event.angleDelta().y()
|
146
|
+
factor = 1.0 + (0.1 if angle > 0 else -0.1)
|
147
|
+
self._zoom = max(0.1, min(3.0, self._zoom * factor))
|
148
|
+
self._apply_zoom_transform()
|
149
|
+
event.accept()
|
150
|
+
else:
|
151
|
+
super().wheelEvent(event)
|
152
|
+
|
153
|
+
def _apply_zoom_transform(self):
|
154
|
+
t = QTransform()
|
155
|
+
t.scale(self._zoom, self._zoom)
|
156
|
+
self.setTransform(t)
|
157
|
+
|
158
|
+
def resizeEvent(self, event):
|
159
|
+
super().resizeEvent(event)
|
160
|
+
# Keep scene rect; do not auto-fit. Users control zoom.
|
161
|
+
# Nothing else needed here.
|
162
|
+
|
163
|
+
# Optional helpers for external controllers
|
164
|
+
def focus_row(self, row: int):
|
165
|
+
"""
|
166
|
+
Center the view around a specific row.
|
167
|
+
"""
|
168
|
+
if not self.graph or row < 0 or row >= len(self.graph.nodes):
|
169
|
+
return
|
170
|
+
y = self._row_y(row)
|
171
|
+
rect = QRectF(0, y - ROW_HEIGHT * 2, self._scene.width(), ROW_HEIGHT * 4)
|
172
|
+
self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
|
173
|
+
# Restore user zoom preference after focusing
|
174
|
+
self._apply_zoom_transform()
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from queue import Queue
|
2
|
+
|
3
|
+
|
4
|
+
class AIConfig(object):
|
5
|
+
|
6
|
+
def __init__(self):
|
7
|
+
self.current_ai_model_system_prompt: str = ""
|
8
|
+
self.choosable_ai: dict[str, dict[str, str]] = {
|
9
|
+
"AI_model": {
|
10
|
+
"ai_base_url": "",
|
11
|
+
"ai_api_key": "",
|
12
|
+
"chat_model": "",
|
13
|
+
"prompt_template": "",
|
14
|
+
}
|
15
|
+
}
|
16
|
+
self.message_queue = Queue()
|
17
|
+
|
18
|
+
|
19
|
+
ai_config = AIConfig()
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from threading import Thread
|
2
|
+
|
3
|
+
from je_editor.pyside_ui.main_ui.ai_widget.ai_config import ai_config
|
4
|
+
from je_editor.pyside_ui.main_ui.ai_widget.langchain_interface import LangChainInterface
|
5
|
+
|
6
|
+
|
7
|
+
class AskThread(Thread):
|
8
|
+
|
9
|
+
def __init__(self, lang_chain_interface: LangChainInterface, prompt):
|
10
|
+
super().__init__()
|
11
|
+
self.lang_chain_interface = lang_chain_interface
|
12
|
+
self.prompt = prompt
|
13
|
+
|
14
|
+
|
15
|
+
def run(self):
|
16
|
+
ai_response = self.lang_chain_interface.call_ai_model(prompt=self.prompt)
|
17
|
+
ai_config.message_queue.put(ai_response)
|