je-editor 0.0.222__py3-none-any.whl → 0.0.224__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.
Potentially problematic release.
This version of je-editor might be problematic. Click here for more details.
- je_editor/__init__.py +2 -2
- je_editor/git_client/commit_graph.py +7 -7
- je_editor/git_client/git_action.py +0 -7
- je_editor/pyside_ui/browser/browser_view.py +16 -4
- je_editor/pyside_ui/browser/browser_widget.py +43 -29
- je_editor/pyside_ui/browser/main_browser_widget.py +85 -0
- je_editor/pyside_ui/code/auto_save/auto_save_manager.py +34 -2
- je_editor/pyside_ui/code/auto_save/auto_save_thread.py +19 -6
- je_editor/pyside_ui/code/code_format/pep8_format.py +53 -9
- je_editor/pyside_ui/code/code_process/code_exec.py +88 -52
- je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py +116 -55
- je_editor/pyside_ui/code/running_process_manager.py +19 -1
- je_editor/pyside_ui/code/shell_process/shell_exec.py +71 -48
- je_editor/pyside_ui/code/syntax/python_syntax.py +45 -10
- je_editor/pyside_ui/code/syntax/syntax_setting.py +40 -12
- je_editor/pyside_ui/code/textedit_code_result/code_record.py +34 -12
- je_editor/pyside_ui/code/variable_inspector/inspector_gui.py +53 -6
- je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py +30 -3
- je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py +35 -2
- je_editor/pyside_ui/dialog/file_dialog/open_file_dialog.py +33 -5
- je_editor/pyside_ui/dialog/file_dialog/save_file_dialog.py +25 -3
- je_editor/pyside_ui/dialog/search_ui/search_error_box.py +26 -1
- je_editor/pyside_ui/dialog/search_ui/search_text_box.py +26 -1
- je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py +11 -11
- je_editor/pyside_ui/git_ui/git_client/commit_table.py +46 -8
- je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py +49 -15
- je_editor/pyside_ui/git_ui/git_client/git_client_gui.py +81 -16
- je_editor/pyside_ui/git_ui/git_client/graph_view.py +64 -20
- je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +20 -5
- je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +20 -1
- je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +56 -41
- je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +45 -6
- je_editor/pyside_ui/main_ui/console_widget/console_gui.py +44 -12
- je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py +34 -13
- je_editor/pyside_ui/main_ui/dock/destroy_dock.py +33 -2
- je_editor/pyside_ui/main_ui/editor/editor_widget.py +104 -20
- je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py +34 -7
- je_editor/pyside_ui/main_ui/editor/process_input.py +38 -11
- je_editor/pyside_ui/main_ui/ipython_widget/rich_jupyter.py +46 -11
- je_editor/pyside_ui/main_ui/main_editor.py +180 -42
- je_editor/pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +51 -28
- je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +83 -36
- je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py +70 -17
- je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py +35 -4
- je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py +41 -1
- je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py +100 -42
- je_editor/pyside_ui/main_ui/menu/run_menu/build_run_menu.py +57 -7
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_debug_menu.py +50 -4
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_program_menu.py +52 -6
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_shell_menu.py +44 -4
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py +23 -1
- je_editor/pyside_ui/main_ui/menu/set_menu_bar.py +37 -12
- je_editor/pyside_ui/main_ui/menu/style_menu/build_style_menu.py +44 -7
- je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +127 -44
- je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py +65 -1
- je_editor/pyside_ui/main_ui/save_settings/setting_utils.py +18 -1
- je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py +33 -3
- je_editor/pyside_ui/main_ui/save_settings/user_setting_file.py +38 -11
- je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py +39 -2
- je_editor/start_editor.py +26 -1
- je_editor/utils/encodings/python_encodings.py +101 -98
- je_editor/utils/file/open/open_file.py +36 -19
- je_editor/utils/file/save/save_file.py +35 -14
- je_editor/utils/json/json_file.py +29 -14
- je_editor/utils/json_format/json_process.py +33 -2
- je_editor/utils/logging/loggin_instance.py +38 -8
- je_editor/utils/multi_language/multi_language_wrapper.py +29 -4
- je_editor/utils/redirect_manager/redirect_manager_class.py +49 -11
- je_editor/utils/venv_check/check_venv.py +45 -15
- {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/METADATA +1 -1
- {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/RECORD +74 -74
- je_editor/git_client/github.py +0 -81
- {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/WHEEL +0 -0
- {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/licenses/LICENSE +0 -0
- {je_editor-0.0.222.dist-info → je_editor-0.0.224.dist-info}/top_level.txt +0 -0
|
@@ -3,25 +3,63 @@ from PySide6.QtWidgets import QTableView
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class CommitTable(QTableView):
|
|
6
|
+
"""
|
|
7
|
+
CommitTable 類別:用來顯示 Git Commit 紀錄的表格
|
|
8
|
+
CommitTable class: A table view to display Git commit history
|
|
9
|
+
"""
|
|
10
|
+
|
|
6
11
|
def __init__(self, parent=None):
|
|
7
12
|
super().__init__(parent)
|
|
8
|
-
|
|
13
|
+
|
|
14
|
+
# 建立一個 QStandardItemModel,初始為 0 行 5 欄
|
|
15
|
+
# Create a QStandardItemModel with 0 rows and 5 columns
|
|
16
|
+
self.model_data = QStandardItemModel(0, 5, self) # 多一欄 / 5 columns
|
|
17
|
+
|
|
18
|
+
# 設定表頭標籤
|
|
19
|
+
# Set horizontal header labels
|
|
9
20
|
self.model_data.setHorizontalHeaderLabels(["#", "SHA", "Message", "Author", "Date"])
|
|
21
|
+
|
|
22
|
+
# 將模型綁定到 QTableView
|
|
23
|
+
# Set the model for QTableView
|
|
10
24
|
self.setModel(self.model_data)
|
|
25
|
+
|
|
26
|
+
# 設定選擇行為為整列選取
|
|
27
|
+
# Set selection behavior to select entire rows
|
|
11
28
|
self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
|
29
|
+
|
|
30
|
+
# 禁止編輯表格內容
|
|
31
|
+
# Disable editing of table cells
|
|
12
32
|
self.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
|
|
33
|
+
|
|
34
|
+
# 讓最後一欄自動延展填滿
|
|
35
|
+
# Stretch the last column to fill available space
|
|
13
36
|
self.horizontalHeader().setStretchLastSection(True)
|
|
14
37
|
|
|
15
38
|
def set_commits(self, commits):
|
|
39
|
+
"""
|
|
40
|
+
將 commit 資料填入表格
|
|
41
|
+
Populate the table with commit data
|
|
42
|
+
:param commits: commit 資料清單 (list of dicts with sha, message, author, date)
|
|
43
|
+
"""
|
|
44
|
+
# 清空現有資料
|
|
45
|
+
# Clear existing rows
|
|
16
46
|
self.model_data.setRowCount(0)
|
|
17
|
-
|
|
47
|
+
|
|
48
|
+
# 遍歷 commit 清單,逐行加入表格
|
|
49
|
+
# Iterate over commits and append rows
|
|
50
|
+
for index, commit in enumerate(commits, start=1):
|
|
18
51
|
row = [
|
|
19
|
-
QStandardItem(str(
|
|
20
|
-
QStandardItem(
|
|
21
|
-
QStandardItem(
|
|
22
|
-
QStandardItem(
|
|
23
|
-
QStandardItem(
|
|
52
|
+
QStandardItem(str(index)), # 行號 / row number
|
|
53
|
+
QStandardItem(commit["sha"][:7]), # SHA 短碼 / short SHA
|
|
54
|
+
QStandardItem(commit["message"]), # 提交訊息 / commit message
|
|
55
|
+
QStandardItem(commit["author"]), # 作者 / author
|
|
56
|
+
QStandardItem(commit["date"]), # 日期 / date
|
|
24
57
|
]
|
|
58
|
+
# 設定每個欄位不可編輯
|
|
59
|
+
# Make each item non-editable
|
|
25
60
|
for item in row:
|
|
26
61
|
item.setEditable(False)
|
|
27
|
-
|
|
62
|
+
|
|
63
|
+
# 將這一列加入模型
|
|
64
|
+
# Append row to the model
|
|
65
|
+
self.model_data.appendRow(row)
|
|
@@ -19,64 +19,89 @@ log = logging.getLogger(__name__)
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class GitTreeViewGUI(QWidget):
|
|
22
|
+
"""
|
|
23
|
+
GitTreeViewGUI: Git 提交歷史圖形化檢視器
|
|
24
|
+
GitTreeViewGUI: A graphical viewer for Git commit history
|
|
25
|
+
"""
|
|
26
|
+
|
|
22
27
|
def __init__(self):
|
|
23
28
|
super().__init__()
|
|
24
29
|
self.language_wrapper_get = language_wrapper.language_word_dict.get
|
|
25
30
|
self.setWindowTitle(self.language_wrapper_get("git_graph_title"))
|
|
26
31
|
|
|
32
|
+
# === 主版面配置 / Main layout ===
|
|
27
33
|
git_treeview_main_layout = QVBoxLayout(self)
|
|
28
34
|
git_treeview_main_layout.setContentsMargins(0, 0, 0, 0)
|
|
29
35
|
git_treeview_main_layout.setSpacing(0)
|
|
30
36
|
|
|
37
|
+
# === 工具列 / Toolbar ===
|
|
31
38
|
git_treeview_toolbar = QToolBar()
|
|
39
|
+
|
|
40
|
+
# 開啟 Git 專案動作 / Action to open Git repo
|
|
32
41
|
open_repo_action = QAction(self.language_wrapper_get("git_graph_toolbar_open"), self)
|
|
33
42
|
open_repo_action.triggered.connect(self.open_repo)
|
|
34
43
|
git_treeview_toolbar.addAction(open_repo_action)
|
|
35
44
|
|
|
45
|
+
# 刷新圖表動作 / Action to refresh graph
|
|
36
46
|
act_refresh = QAction(self.language_wrapper_get("git_graph_toolbar_refresh"), self)
|
|
37
47
|
act_refresh.triggered.connect(self.refresh_graph)
|
|
38
48
|
git_treeview_toolbar.addAction(act_refresh)
|
|
39
49
|
|
|
40
50
|
git_treeview_main_layout.addWidget(git_treeview_toolbar)
|
|
41
51
|
|
|
52
|
+
# === 分割器 (左:圖形檢視 / 右:提交表格) ===
|
|
42
53
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
43
|
-
self.graph_view = CommitGraphView()
|
|
44
|
-
self.commit_table = CommitTable()
|
|
54
|
+
self.graph_view = CommitGraphView() # 提交圖形檢視器 / commit graph view
|
|
55
|
+
self.commit_table = CommitTable() # 提交表格 / commit table
|
|
45
56
|
splitter.addWidget(self.graph_view)
|
|
46
57
|
splitter.addWidget(self.commit_table)
|
|
47
|
-
splitter.setSizes([600, 400])
|
|
48
|
-
git_treeview_main_layout.addWidget(splitter, 1) # 1 表示可伸縮
|
|
58
|
+
splitter.setSizes([600, 400]) # 左右比例 / left-right ratio
|
|
59
|
+
git_treeview_main_layout.addWidget(splitter, 1) # stretch=1 表示可伸縮
|
|
49
60
|
|
|
61
|
+
# === 狀態列 / Status bar ===
|
|
50
62
|
self.status = QStatusBar()
|
|
51
63
|
git_treeview_main_layout.addWidget(self.status)
|
|
52
64
|
|
|
65
|
+
# === Git 相關屬性 ===
|
|
53
66
|
self.repo_path = None
|
|
54
67
|
self.git = None
|
|
55
68
|
|
|
69
|
+
# === 檔案監控器 / File system watcher ===
|
|
56
70
|
self.watcher = QFileSystemWatcher()
|
|
57
71
|
self.watcher.directoryChanged.connect(self._on_git_changed)
|
|
58
72
|
self.watcher.fileChanged.connect(self._on_git_changed)
|
|
59
73
|
|
|
74
|
+
# === 定時器 (延遲刷新) / Timer for delayed refresh ===
|
|
60
75
|
self.refresh_timer = QTimer()
|
|
61
76
|
self.refresh_timer.setSingleShot(True)
|
|
62
77
|
self.refresh_timer.timeout.connect(self.refresh_graph)
|
|
63
78
|
|
|
79
|
+
# === 表格選擇事件 / Table selection event ===
|
|
64
80
|
self.commit_table.selectionModel().selectionChanged.connect(self._on_table_selection)
|
|
65
81
|
|
|
66
82
|
def _on_table_selection(self, selected, _):
|
|
83
|
+
"""
|
|
84
|
+
當使用者在提交表格中選擇某一列時,讓圖形檢視器聚焦到該行
|
|
85
|
+
When user selects a row in commit table, focus graph view on that row
|
|
86
|
+
"""
|
|
67
87
|
if not selected.indexes():
|
|
68
88
|
return
|
|
69
89
|
row = selected.indexes()[0].row()
|
|
70
90
|
self.graph_view.focus_row(row)
|
|
71
91
|
|
|
72
92
|
def open_repo(self):
|
|
73
|
-
|
|
93
|
+
"""
|
|
94
|
+
開啟 Git 專案並初始化圖形檢視
|
|
95
|
+
Open a Git repository and set up the graph view
|
|
96
|
+
"""
|
|
74
97
|
path = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("git_graph_menu_open_repo"))
|
|
75
98
|
if not path:
|
|
76
99
|
return
|
|
77
100
|
repo_path = Path(path)
|
|
78
101
|
git = GitCLI(repo_path)
|
|
79
102
|
if not git.is_git_repo():
|
|
103
|
+
# 若不是 Git 專案,顯示警告
|
|
104
|
+
# Show warning if not a Git repo
|
|
80
105
|
QMessageBox.warning(self, self.language_wrapper_get("git_graph_title"),
|
|
81
106
|
self.language_wrapper_get("git_graph_error_not_git"))
|
|
82
107
|
return
|
|
@@ -86,11 +111,15 @@ class GitTreeViewGUI(QWidget):
|
|
|
86
111
|
self.refresh_graph()
|
|
87
112
|
|
|
88
113
|
def _setup_watcher(self):
|
|
114
|
+
"""
|
|
115
|
+
設定檔案監控,監控 .git 目錄與相關檔案
|
|
116
|
+
Setup file watcher to monitor .git directory and related files
|
|
117
|
+
"""
|
|
89
118
|
self.watcher.removePaths(self.watcher.files())
|
|
90
119
|
self.watcher.removePaths(self.watcher.directories())
|
|
91
120
|
if not self.repo_path:
|
|
92
121
|
return
|
|
93
|
-
git_dir = self.repo_path / ".git_client"
|
|
122
|
+
git_dir = self.repo_path / ".git_client" # ⚠️ 這裡應該是 ".git",可能是自訂路徑
|
|
94
123
|
if git_dir.exists():
|
|
95
124
|
self.watcher.addPath(str(git_dir))
|
|
96
125
|
for f in ["HEAD", "packed-refs"]:
|
|
@@ -102,21 +131,26 @@ class GitTreeViewGUI(QWidget):
|
|
|
102
131
|
self.watcher.addPath(str(refs_dir))
|
|
103
132
|
|
|
104
133
|
def _on_git_changed(self):
|
|
105
|
-
|
|
134
|
+
"""
|
|
135
|
+
當 Git 目錄變更時,啟動延遲刷新
|
|
136
|
+
When Git directory changes, start delayed refresh
|
|
137
|
+
"""
|
|
106
138
|
self.refresh_timer.start(500)
|
|
107
139
|
|
|
108
140
|
def refresh_graph(self):
|
|
109
|
-
|
|
141
|
+
"""
|
|
142
|
+
刷新 Git 提交圖與提交表格
|
|
143
|
+
Refresh Git commit graph and commit table
|
|
144
|
+
"""
|
|
110
145
|
if not self.git:
|
|
111
146
|
return
|
|
112
147
|
try:
|
|
113
|
-
refs = self.git.get_all_refs()
|
|
114
|
-
commits = self.git.get_commits(max_count=500)
|
|
148
|
+
refs = self.git.get_all_refs() # 取得所有 refs
|
|
149
|
+
commits = self.git.get_commits(max_count=500) # 取得最近 500 筆提交
|
|
115
150
|
graph = CommitGraph()
|
|
116
|
-
graph.build(commits, refs)
|
|
117
|
-
self.graph_view.set_graph(graph)
|
|
118
|
-
self.commit_table.set_commits(commits)
|
|
151
|
+
graph.build(commits, refs) # 建立提交圖
|
|
152
|
+
self.graph_view.set_graph(graph) # 更新圖形檢視
|
|
153
|
+
self.commit_table.set_commits(commits) # 更新提交表格
|
|
119
154
|
except Exception as e:
|
|
120
155
|
QMessageBox.critical(self, self.language_wrapper_get("git_graph_title"),
|
|
121
|
-
f"{self.language_wrapper_get('git_graph_error_exec_failed')}\n{e}")
|
|
122
|
-
|
|
156
|
+
f"{self.language_wrapper_get('git_graph_error_exec_failed')}\n{e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from PySide6.QtCore import Qt
|
|
3
|
+
from PySide6.QtCore import Qt, QTimer
|
|
4
4
|
from PySide6.QtGui import QTextOption, QTextCharFormat, QColor, QFont, QSyntaxHighlighter, QAction
|
|
5
5
|
from PySide6.QtWidgets import (
|
|
6
6
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
@@ -15,9 +15,9 @@ class GitChangeItem:
|
|
|
15
15
|
簡單的資料結構,用來存放檔案變更資訊。
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
def __init__(self,
|
|
19
|
-
self.
|
|
20
|
-
self.
|
|
18
|
+
def __init__(self, path: str, status: str):
|
|
19
|
+
self.path = path # repo 相對路徑 / repo-relative path
|
|
20
|
+
self.status = status # 狀態,例如 'untracked', 'modified', 'deleted', 'renamed', 'staged'
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class GitGui(QWidget):
|
|
@@ -36,6 +36,7 @@ class GitGui(QWidget):
|
|
|
36
36
|
self.checkout_button = QPushButton("Checkout")
|
|
37
37
|
self.clone_repo_button = QPushButton("Clone Repo")
|
|
38
38
|
self.repo_status_label = QLabel("Status: -")
|
|
39
|
+
self.commit_status_label = QLabel("Unpushed commits: ...")
|
|
39
40
|
|
|
40
41
|
top = QHBoxLayout()
|
|
41
42
|
top.addWidget(self.repo_path_label, 1)
|
|
@@ -48,7 +49,6 @@ class GitGui(QWidget):
|
|
|
48
49
|
# === Left: changes list / 左側:變更清單 ===
|
|
49
50
|
self.changes_list_widget = QListWidget()
|
|
50
51
|
self.changes_list_widget.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
|
|
51
|
-
self.changes_list_widget.setMinimumWidth(420)
|
|
52
52
|
|
|
53
53
|
# === Right: diff / info viewer / 右側:差異或資訊檢視器 ===
|
|
54
54
|
self.diff_viewer = QPlainTextEdit()
|
|
@@ -84,6 +84,7 @@ class GitGui(QWidget):
|
|
|
84
84
|
self.commit_button = QPushButton("Commit")
|
|
85
85
|
self.unstage_all_button = QPushButton("Unstage All")
|
|
86
86
|
self.track_all_untracked_button = QPushButton("Track All Untracked")
|
|
87
|
+
self.git_push_button = QPushButton("Push")
|
|
87
88
|
|
|
88
89
|
bottom = QHBoxLayout()
|
|
89
90
|
bottom.addWidget(QLabel("Message:"))
|
|
@@ -94,11 +95,13 @@ class GitGui(QWidget):
|
|
|
94
95
|
bottom.addWidget(self.stage_all_button)
|
|
95
96
|
bottom.addWidget(self.commit_button)
|
|
96
97
|
bottom.addWidget(self.track_all_untracked_button)
|
|
98
|
+
bottom.addWidget(self.git_push_button)
|
|
97
99
|
|
|
98
100
|
# === Main layout / 主版面配置 ===
|
|
99
101
|
center_layout = QVBoxLayout()
|
|
100
102
|
center_layout.addLayout(top)
|
|
101
103
|
center_layout.addWidget(self.repo_status_label)
|
|
104
|
+
center_layout.addWidget(self.commit_status_label)
|
|
102
105
|
center_layout.addWidget(splitter, 1)
|
|
103
106
|
center_layout.addLayout(bottom)
|
|
104
107
|
self.setLayout(center_layout)
|
|
@@ -115,6 +118,7 @@ class GitGui(QWidget):
|
|
|
115
118
|
self.commit_button.clicked.connect(self.on_commit_staged_changes)
|
|
116
119
|
self.unstage_all_button.clicked.connect(self.on_unstage_all_changes)
|
|
117
120
|
self.track_all_untracked_button.clicked.connect(self.on_track_all_untracked_files)
|
|
121
|
+
self.git_push_button.clicked.connect(self.on_push_to_github)
|
|
118
122
|
|
|
119
123
|
self._update_ui_controls(enabled=False)
|
|
120
124
|
|
|
@@ -133,6 +137,12 @@ class GitGui(QWidget):
|
|
|
133
137
|
|
|
134
138
|
center_layout.setMenuBar(menubar)
|
|
135
139
|
|
|
140
|
+
# === Timer ===
|
|
141
|
+
self.update_commit_status_timer = QTimer()
|
|
142
|
+
self.update_commit_status_timer.setInterval(1000)
|
|
143
|
+
self.update_commit_status_timer.timeout.connect(self.update_commit_status)
|
|
144
|
+
self.update_commit_status_timer.start()
|
|
145
|
+
|
|
136
146
|
# ---------- Repo operations ----------
|
|
137
147
|
# ---------- 儲存庫操作 ----------
|
|
138
148
|
|
|
@@ -258,16 +268,16 @@ class GitGui(QWidget):
|
|
|
258
268
|
|
|
259
269
|
# Normalize paths (for rename "a -> b" 取目的與來源)
|
|
260
270
|
src, dst = None, None
|
|
261
|
-
if "->" in change.
|
|
262
|
-
parts = [p.strip() for p in change.
|
|
271
|
+
if "->" in change.path and change.status in ("renamed", "staged"):
|
|
272
|
+
parts = [p.strip() for p in change.path.split("->")]
|
|
263
273
|
if len(parts) == 2:
|
|
264
274
|
src, dst = parts
|
|
265
|
-
rel = dst or change.
|
|
275
|
+
rel = dst or change.path
|
|
266
276
|
abs_path = Path(current_repo.working_tree_dir) / Path(rel)
|
|
267
277
|
|
|
268
278
|
try:
|
|
269
279
|
# Untracked: 顯示新增檔案的內容(模擬 unified diff)
|
|
270
|
-
if change.
|
|
280
|
+
if change.status == "untracked":
|
|
271
281
|
if not abs_path.exists():
|
|
272
282
|
self._safe_set_diff_text(f"(untracked file missing: {rel})")
|
|
273
283
|
return
|
|
@@ -282,7 +292,7 @@ class GitGui(QWidget):
|
|
|
282
292
|
return
|
|
283
293
|
|
|
284
294
|
# Deleted: 顯示與 HEAD/Index 的差異(若工作樹檔案不存在也能呈現)
|
|
285
|
-
if change.
|
|
295
|
+
if change.status == "deleted":
|
|
286
296
|
# working tree vs index(未暫存刪除)
|
|
287
297
|
if rel and not current_repo.index.diff(None, paths=[rel]):
|
|
288
298
|
# 若 diff(None) 空,試試 index vs HEAD
|
|
@@ -296,7 +306,7 @@ class GitGui(QWidget):
|
|
|
296
306
|
return
|
|
297
307
|
|
|
298
308
|
# Renamed: 顯示 rename 變更;若僅 rename 無內容改動,diff 可能為空
|
|
299
|
-
if change.
|
|
309
|
+
if change.status == "renamed":
|
|
300
310
|
# 優先顯示 staged rename
|
|
301
311
|
diff_text = ""
|
|
302
312
|
try:
|
|
@@ -319,7 +329,7 @@ class GitGui(QWidget):
|
|
|
319
329
|
return
|
|
320
330
|
|
|
321
331
|
# Staged vs HEAD
|
|
322
|
-
if change.
|
|
332
|
+
if change.status == "staged":
|
|
323
333
|
diff_text = current_repo.git.diff("--cached", rel)
|
|
324
334
|
if not diff_text.strip():
|
|
325
335
|
self._safe_set_diff_text("(no staged changes vs HEAD)")
|
|
@@ -328,7 +338,7 @@ class GitGui(QWidget):
|
|
|
328
338
|
return
|
|
329
339
|
|
|
330
340
|
# Modified / general unstaged
|
|
331
|
-
if change.
|
|
341
|
+
if change.status in ("modified",):
|
|
332
342
|
# working tree vs index
|
|
333
343
|
diff_text = current_repo.git.diff(rel)
|
|
334
344
|
if not diff_text.strip():
|
|
@@ -344,7 +354,7 @@ class GitGui(QWidget):
|
|
|
344
354
|
return
|
|
345
355
|
|
|
346
356
|
# Fallback
|
|
347
|
-
self._safe_set_diff_text(f"(no diff handler for status: {change.
|
|
357
|
+
self._safe_set_diff_text(f"(no diff handler for status: {change.status})")
|
|
348
358
|
|
|
349
359
|
except GitCommandError as e:
|
|
350
360
|
self._safe_set_diff_text(f"Git error while generating diff:\n{e}")
|
|
@@ -433,7 +443,7 @@ class GitGui(QWidget):
|
|
|
433
443
|
Add a file change entry to the list.
|
|
434
444
|
將檔案變更項目加入清單。
|
|
435
445
|
"""
|
|
436
|
-
item_text = f"[{change.
|
|
446
|
+
item_text = f"[{change.status}] {change.path}"
|
|
437
447
|
list_item = QListWidgetItem(item_text)
|
|
438
448
|
list_item.setData(Qt.ItemDataRole.UserRole, change) # 存放 GitChangeItem 物件
|
|
439
449
|
list_item.setCheckState(Qt.CheckState.Unchecked) # 預設未勾選
|
|
@@ -513,6 +523,7 @@ class GitGui(QWidget):
|
|
|
513
523
|
try:
|
|
514
524
|
file_paths = []
|
|
515
525
|
for change_entry in selected_changes:
|
|
526
|
+
print(change_entry.path)
|
|
516
527
|
if "->" in change_entry.path and change_entry.status in ("renamed", "staged"):
|
|
517
528
|
parts = change_entry.path.split("->")
|
|
518
529
|
source_path = parts[0].strip()
|
|
@@ -582,7 +593,7 @@ class GitGui(QWidget):
|
|
|
582
593
|
self.branch_selector, self.checkout_button, self.changes_list_widget,
|
|
583
594
|
self.diff_viewer, self.commit_message_input, self.stage_selected_button,
|
|
584
595
|
self.unstage_selected_button, self.stage_all_button, self.commit_button,
|
|
585
|
-
self.unstage_all_button, self.track_all_untracked_button
|
|
596
|
+
self.unstage_all_button, self.track_all_untracked_button, self.git_push_button
|
|
586
597
|
):
|
|
587
598
|
widget.setEnabled(enabled)
|
|
588
599
|
|
|
@@ -655,6 +666,60 @@ class GitGui(QWidget):
|
|
|
655
666
|
except GitCommandError as e:
|
|
656
667
|
QMessageBox.critical(self, "Clone Error", str(e))
|
|
657
668
|
|
|
669
|
+
# ===== GitHub =====
|
|
670
|
+
|
|
671
|
+
def on_push_to_github(self):
|
|
672
|
+
if not self.current_repo:
|
|
673
|
+
QMessageBox.warning(self, "Warning", "No repository opened.")
|
|
674
|
+
return
|
|
675
|
+
try:
|
|
676
|
+
origin = self.current_repo.remote(name="origin")
|
|
677
|
+
result = origin.push()
|
|
678
|
+
msg = "\n".join(str(r) for r in result)
|
|
679
|
+
QMessageBox.information(self, "Push Result", f"Pushed to origin:\n{msg}")
|
|
680
|
+
except Exception as e:
|
|
681
|
+
QMessageBox.critical(self, "Track Untracked Error", str(e))
|
|
682
|
+
|
|
683
|
+
def get_unpushed_commit_count(self, remote_name: str = "origin") -> dict:
|
|
684
|
+
try:
|
|
685
|
+
repo = self.current_repo
|
|
686
|
+
if repo is None:
|
|
687
|
+
return {"ahead": 0, "behind": 0, "error": "No repo loaded"}
|
|
688
|
+
if repo.bare:
|
|
689
|
+
return {"ahead": 0, "behind": 0, "error": "Repository is bare"}
|
|
690
|
+
if repo.head.is_detached:
|
|
691
|
+
return {"ahead": 0, "behind": 0, "error": "HEAD is detached"}
|
|
692
|
+
|
|
693
|
+
branch = repo.active_branch
|
|
694
|
+
remote = repo.remote(remote_name)
|
|
695
|
+
remote.fetch()
|
|
696
|
+
|
|
697
|
+
upstream_ref = f"{remote_name}/{branch.name}"
|
|
698
|
+
if upstream_ref not in repo.refs:
|
|
699
|
+
return {"ahead": 0, "behind": 0, "error": f"No upstream branch for {branch.name}"}
|
|
700
|
+
|
|
701
|
+
ahead_commits = list(repo.iter_commits(f"{upstream_ref}..{branch.name}"))
|
|
702
|
+
behind_commits = list(repo.iter_commits(f"{branch.name}..{upstream_ref}"))
|
|
703
|
+
|
|
704
|
+
return {"ahead": len(ahead_commits), "behind": len(behind_commits), "error": None}
|
|
705
|
+
|
|
706
|
+
except GitCommandError as e:
|
|
707
|
+
return {"ahead": 0, "behind": 0, "error": f"Git error: {e}"}
|
|
708
|
+
except Exception as e:
|
|
709
|
+
return {"ahead": 0, "behind": 0, "error": str(e)}
|
|
710
|
+
|
|
711
|
+
def update_commit_status(self):
|
|
712
|
+
result = self.get_unpushed_commit_count()
|
|
713
|
+
if result["error"]:
|
|
714
|
+
self.commit_status_label.setText(f"Error: {result['error']}")
|
|
715
|
+
else:
|
|
716
|
+
self.commit_status_label.setText(
|
|
717
|
+
f"Ahead (push): {result['ahead']} | Behind (pull): {result['behind']}"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# ===== Theme =====
|
|
722
|
+
|
|
658
723
|
def apply_light_theme(self):
|
|
659
724
|
"""
|
|
660
725
|
Switch to light theme highlighting.
|
|
@@ -14,18 +14,18 @@ from PySide6.QtWidgets import (
|
|
|
14
14
|
from je_editor.git_client.commit_graph import CommitGraph
|
|
15
15
|
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
|
16
16
|
|
|
17
|
-
# Layout constants
|
|
18
|
-
NODE_RADIUS = 6
|
|
19
|
-
ROW_HEIGHT = 28
|
|
20
|
-
LANE_WIDTH = 22
|
|
21
|
-
LEFT_MARGIN = 220
|
|
22
|
-
|
|
23
|
-
# Colors
|
|
24
|
-
EDGE_COLOR = QColor("#888")
|
|
25
|
-
TEXT_COLOR = QColor("#222")
|
|
26
|
-
BG_COLOR = QColor("#ffffff")
|
|
27
|
-
|
|
28
|
-
#
|
|
17
|
+
# === 版面配置常數 / Layout constants ===
|
|
18
|
+
NODE_RADIUS = 6 # 節點半徑 / node radius
|
|
19
|
+
ROW_HEIGHT = 28 # 每一列高度 / row height
|
|
20
|
+
LANE_WIDTH = 22 # 每一條 lane 的寬度 / lane width
|
|
21
|
+
LEFT_MARGIN = 220 # 左側 SHA/訊息文字區域保留空間 / left margin for SHA/message text
|
|
22
|
+
|
|
23
|
+
# === 顏色設定 / Colors ===
|
|
24
|
+
EDGE_COLOR = QColor("#888") # 邊線顏色 / edge color
|
|
25
|
+
TEXT_COLOR = QColor("#222") # 文字顏色 / text color
|
|
26
|
+
BG_COLOR = QColor("#ffffff") # 背景顏色 / background color
|
|
27
|
+
|
|
28
|
+
# Lane 顏色調色盤 / Lane color palette
|
|
29
29
|
LANE_COLORS = [
|
|
30
30
|
QColor("#4C78A8"),
|
|
31
31
|
QColor("#F58518"),
|
|
@@ -41,24 +41,38 @@ LANE_COLORS = [
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def lane_color(lane: int) -> QColor:
|
|
44
|
+
"""
|
|
45
|
+
根據 lane index 回傳顏色
|
|
46
|
+
Return color based on lane index
|
|
47
|
+
"""
|
|
44
48
|
if lane < 0:
|
|
45
49
|
return QColor("#999")
|
|
46
50
|
return LANE_COLORS[lane % len(LANE_COLORS)]
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
class CommitGraphView(QGraphicsView):
|
|
54
|
+
"""
|
|
55
|
+
Git Commit Graph 視覺化檢視器
|
|
56
|
+
Git commit graph visualization view
|
|
57
|
+
"""
|
|
58
|
+
|
|
50
59
|
def __init__(self, parent=None):
|
|
51
60
|
super().__init__(parent)
|
|
52
61
|
self.language_wrapper_get = language_wrapper.language_word_dict.get
|
|
62
|
+
|
|
63
|
+
# 設定背景與抗鋸齒 / set background and antialiasing
|
|
53
64
|
self.setBackgroundBrush(QBrush(BG_COLOR))
|
|
54
65
|
self.setRenderHints(
|
|
55
66
|
self.renderHints()
|
|
56
67
|
| QPainter.RenderHint.Antialiasing
|
|
57
68
|
| QPainter.RenderHint.TextAntialiasing
|
|
58
69
|
)
|
|
70
|
+
|
|
71
|
+
# 啟用拖曳與縮放模式 / enable drag and zoom mode
|
|
59
72
|
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
|
60
73
|
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
|
61
74
|
|
|
75
|
+
# 建立場景 / create scene
|
|
62
76
|
self._scene = QGraphicsScene(self)
|
|
63
77
|
self.setScene(self._scene)
|
|
64
78
|
|
|
@@ -67,21 +81,32 @@ class CommitGraphView(QGraphicsView):
|
|
|
67
81
|
self._zoom = 1.0
|
|
68
82
|
|
|
69
83
|
def set_graph(self, graph: CommitGraph):
|
|
84
|
+
"""
|
|
85
|
+
設定 commit graph 並重新繪製
|
|
86
|
+
Set commit graph and redraw
|
|
87
|
+
"""
|
|
70
88
|
self.graph = graph
|
|
71
89
|
self._redraw()
|
|
72
90
|
|
|
73
91
|
def _lane_x(self, lane: int) -> float:
|
|
92
|
+
# 計算 lane 的 X 座標 / calculate X position for lane
|
|
74
93
|
return lane * LANE_WIDTH + NODE_RADIUS * 2
|
|
75
94
|
|
|
76
95
|
def _row_y(self, row: int) -> float:
|
|
96
|
+
# 計算 row 的 Y 座標 / calculate Y position for row
|
|
77
97
|
return row * ROW_HEIGHT + NODE_RADIUS * 2
|
|
78
98
|
|
|
79
99
|
def _redraw(self):
|
|
100
|
+
"""
|
|
101
|
+
重新繪製整個 commit graph
|
|
102
|
+
Redraw the entire commit graph
|
|
103
|
+
"""
|
|
80
104
|
self._scene.clear()
|
|
81
105
|
if not self.graph or not self.graph.nodes:
|
|
82
106
|
self._scene.setSceneRect(QRectF(0, 0, 800, 400))
|
|
83
107
|
return
|
|
84
108
|
|
|
109
|
+
# === 繪製邊線 (父子關係) / Draw edges (parent-child relationships) ===
|
|
85
110
|
edge_pen = QPen(EDGE_COLOR, 2)
|
|
86
111
|
for row, node in enumerate(self.graph.nodes):
|
|
87
112
|
if not node.parent_shas:
|
|
@@ -95,6 +120,8 @@ class CommitGraphView(QGraphicsView):
|
|
|
95
120
|
parent_node = self.graph.nodes[prow]
|
|
96
121
|
x1 = self._lane_x(parent_node.lane_index)
|
|
97
122
|
y1 = self._row_y(prow)
|
|
123
|
+
|
|
124
|
+
# 使用三次貝茲曲線繪製邊線 / cubic Bezier curve for edge
|
|
98
125
|
path = QPainterPath(QPointF(x0, y0))
|
|
99
126
|
ctrl_y = (y0 + y1) / 2.0
|
|
100
127
|
path.cubicTo(QPointF(x0, ctrl_y), QPointF(x1, ctrl_y), QPointF(x1, y1))
|
|
@@ -102,6 +129,7 @@ class CommitGraphView(QGraphicsView):
|
|
|
102
129
|
edge_item.setPen(edge_pen)
|
|
103
130
|
self._scene.addItem(edge_item)
|
|
104
131
|
|
|
132
|
+
# === 繪製節點 (commit 圓點) / Draw nodes (commit circles) ===
|
|
105
133
|
for row, node in enumerate(self.graph.nodes):
|
|
106
134
|
cx = self._lane_x(node.lane_index)
|
|
107
135
|
cy = self._row_y(row)
|
|
@@ -111,6 +139,7 @@ class CommitGraphView(QGraphicsView):
|
|
|
111
139
|
)
|
|
112
140
|
circle.setBrush(QBrush(lane_color(node.lane_index)))
|
|
113
141
|
circle.setPen(QPen(Qt.PenStyle.NoPen))
|
|
142
|
+
# 設定 tooltip 顯示 commit 資訊 / tooltip with commit info
|
|
114
143
|
circle.setToolTip(self.language_wrapper_get(
|
|
115
144
|
"git_graph_tooltip_commit"
|
|
116
145
|
).format(short=node.commit_sha[:7],
|
|
@@ -119,11 +148,13 @@ class CommitGraphView(QGraphicsView):
|
|
|
119
148
|
msg=node.commit_message))
|
|
120
149
|
self._scene.addItem(circle)
|
|
121
150
|
|
|
151
|
+
# 在左側顯示行號 / show row number on the left
|
|
122
152
|
label_item = QGraphicsSimpleTextItem(str(row + 1))
|
|
123
153
|
label_item.setBrush(QBrush(TEXT_COLOR))
|
|
124
154
|
label_item.setPos(-30, cy - NODE_RADIUS * 2)
|
|
125
155
|
self._scene.addItem(label_item)
|
|
126
156
|
|
|
157
|
+
# 設定場景大小 / set scene rect
|
|
127
158
|
self._scene.setSceneRect(
|
|
128
159
|
QRectF(-40, 0,
|
|
129
160
|
self._lane_x(max(n.lane_index for n in self.graph.nodes) + 1) + self._padding,
|
|
@@ -131,15 +162,19 @@ class CommitGraphView(QGraphicsView):
|
|
|
131
162
|
)
|
|
132
163
|
|
|
133
164
|
def _apply_initial_view(self):
|
|
134
|
-
|
|
165
|
+
"""
|
|
166
|
+
初始縮放設定
|
|
167
|
+
Apply initial zoom setting
|
|
168
|
+
"""
|
|
135
169
|
self._zoom = 0.9
|
|
136
170
|
self._apply_zoom_transform()
|
|
137
171
|
|
|
138
|
-
#
|
|
172
|
+
# === 滑鼠滾輪縮放 / Mouse wheel zoom ===
|
|
139
173
|
def wheelEvent(self, event):
|
|
140
174
|
if not self._scene.items():
|
|
141
175
|
return
|
|
142
176
|
|
|
177
|
+
# Ctrl+滾輪縮放,否則正常滾動
|
|
143
178
|
# Ctrl+Wheel to zoom, else scroll normally
|
|
144
179
|
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
145
180
|
angle = event.angleDelta().y()
|
|
@@ -151,24 +186,33 @@ class CommitGraphView(QGraphicsView):
|
|
|
151
186
|
super().wheelEvent(event)
|
|
152
187
|
|
|
153
188
|
def _apply_zoom_transform(self):
|
|
189
|
+
"""
|
|
190
|
+
套用縮放轉換
|
|
191
|
+
Apply zoom transform
|
|
192
|
+
"""
|
|
154
193
|
t = QTransform()
|
|
155
194
|
t.scale(self._zoom, self._zoom)
|
|
156
195
|
self.setTransform(t)
|
|
157
196
|
|
|
158
197
|
def resizeEvent(self, event):
|
|
198
|
+
"""
|
|
199
|
+
視窗大小改變時的事件
|
|
200
|
+
Handle resize event (keep scene rect, no auto-fit)
|
|
201
|
+
"""
|
|
159
202
|
super().resizeEvent(event)
|
|
160
|
-
#
|
|
161
|
-
#
|
|
203
|
+
# 保持場景大小,不自動縮放
|
|
204
|
+
# Keep scene rect; do not auto-fit
|
|
162
205
|
|
|
163
|
-
#
|
|
206
|
+
# === 外部控制輔助方法 / Helper for external controllers ===
|
|
164
207
|
def focus_row(self, row: int):
|
|
165
208
|
"""
|
|
166
|
-
|
|
209
|
+
將檢視器聚焦到指定的 row
|
|
210
|
+
Center the view around a specific row
|
|
167
211
|
"""
|
|
168
212
|
if not self.graph or row < 0 or row >= len(self.graph.nodes):
|
|
169
213
|
return
|
|
170
214
|
y = self._row_y(row)
|
|
171
215
|
rect = QRectF(0, y - ROW_HEIGHT * 2, self._scene.width(), ROW_HEIGHT * 4)
|
|
172
216
|
self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
|
|
173
|
-
#
|
|
174
|
-
self._apply_zoom_transform()
|
|
217
|
+
# 聚焦後恢復使用者縮放比例 / restore user zoom after focusing
|
|
218
|
+
self._apply_zoom_transform()
|