gui_main.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. import sys
  2. import os
  3. import threading
  4. import subprocess
  5. from PyQt5.QtWidgets import (
  6. QApplication, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
  7. QRadioButton, QPushButton, QButtonGroup, QGroupBox, QMessageBox,
  8. QLineEdit, QScrollArea, QFileDialog, QFormLayout, QCheckBox, QTextEdit,
  9. QComboBox, QDialog, QDialogButtonBox, QGridLayout, QToolButton
  10. )
  11. from PyQt5.QtCore import Qt, pyqtSignal, QObject
  12. from repo_sync import RepoSync, __version__
  13. from dotenv import load_dotenv, set_key, find_dotenv, dotenv_values
  14. import json
  15. import uuid
  16. # Explorer路径获取
  17. try:
  18. import win32com.client
  19. def get_active_explorer_path():
  20. shell = win32com.client.Dispatch("Shell.Application")
  21. for window in shell.Windows():
  22. if window.Name in ["文件资源管理器", "Windows Explorer"]:
  23. return window.Document.Folder.Self.Path
  24. return os.getcwd()
  25. except ImportError:
  26. def get_active_explorer_path():
  27. return os.getcwd()
  28. # 命令执行信号类
  29. class CommandSignals(QObject):
  30. output = pyqtSignal(str)
  31. finished = pyqtSignal(int)
  32. class MainTab(QWidget):
  33. def __init__(self, parent=None):
  34. super().__init__(parent)
  35. layout = QVBoxLayout()
  36. # 路径选择
  37. path_layout = QHBoxLayout()
  38. self.path_label = QLabel("Local Path: ")
  39. self.path_edit = QLineEdit()
  40. self.path_edit.setText(get_active_explorer_path())
  41. self.path_btn = QPushButton("Browse...")
  42. self.path_btn.clicked.connect(self.choose_path)
  43. path_layout.addWidget(self.path_label)
  44. path_layout.addWidget(self.path_edit)
  45. path_layout.addWidget(self.path_btn)
  46. layout.addLayout(path_layout)
  47. # 操作
  48. op_group = QGroupBox("Operation:")
  49. op_layout = QHBoxLayout()
  50. self.op_buttons = QButtonGroup(self)
  51. for i, op in enumerate(["create", "push", "pull", "clone", "delete"]):
  52. btn = QRadioButton(op.capitalize())
  53. if i == 0:
  54. btn.setChecked(True)
  55. self.op_buttons.addButton(btn, i)
  56. op_layout.addWidget(btn)
  57. op_group.setLayout(op_layout)
  58. layout.addWidget(op_group)
  59. # 平台
  60. pf_group = QGroupBox("Platform:")
  61. pf_layout = QHBoxLayout()
  62. self.pf_buttons = QButtonGroup(self)
  63. self.platforms = ["github", "gitlab", "gitee", "gitcode", "git.yoq.me", "coding", "aliyun", "cnb"]
  64. for i, pf in enumerate(self.platforms):
  65. btn = QRadioButton(pf.capitalize())
  66. if i == 0:
  67. btn.setChecked(True)
  68. self.pf_buttons.addButton(btn, i)
  69. pf_layout.addWidget(btn)
  70. pf_group.setLayout(pf_layout)
  71. layout.addWidget(pf_group)
  72. # 账户选择
  73. account_layout = QHBoxLayout()
  74. account_layout.addWidget(QLabel("Account:"))
  75. self.account_combo = QComboBox()
  76. self.account_combo.setMinimumWidth(200)
  77. account_layout.addWidget(self.account_combo)
  78. account_layout.addStretch()
  79. layout.addLayout(account_layout)
  80. # 按钮区域 (run和cancel)
  81. btn_layout = QHBoxLayout()
  82. self.run_btn = QPushButton("run it")
  83. self.run_btn.clicked.connect(self.run_repo_sync)
  84. self.cancel_btn = QPushButton("Cancel")
  85. self.cancel_btn.clicked.connect(self.cancel_operation)
  86. self.cancel_btn.setEnabled(False)
  87. btn_layout.addWidget(self.run_btn)
  88. btn_layout.addWidget(self.cancel_btn)
  89. layout.addLayout(btn_layout)
  90. # 命令执行结果
  91. result_group = QGroupBox("Execution Result:")
  92. result_layout = QVBoxLayout()
  93. self.result_text = QTextEdit()
  94. self.result_text.setReadOnly(True)
  95. result_layout.addWidget(self.result_text)
  96. result_group.setLayout(result_layout)
  97. layout.addWidget(result_group)
  98. self.setLayout(layout)
  99. # 命令执行相关
  100. self.process = None
  101. self.command_signals = CommandSignals()
  102. self.command_signals.output.connect(self.update_output)
  103. self.command_signals.finished.connect(self.process_finished)
  104. # 平台变更时更新账户列表
  105. self.pf_buttons.buttonClicked.connect(self.update_account_list)
  106. self.update_account_list()
  107. def choose_path(self):
  108. path = QFileDialog.getExistingDirectory(self, "Select Directory")
  109. if path:
  110. self.path_edit.setText(path)
  111. def update_account_list(self):
  112. self.account_combo.clear()
  113. pf_id = self.pf_buttons.checkedId()
  114. platform = self.platforms[pf_id]
  115. # 读取所有账户
  116. accounts = self.get_platform_accounts(platform)
  117. for account_name in accounts:
  118. self.account_combo.addItem(account_name)
  119. def get_platform_accounts(self, platform):
  120. # 读取.env文件中的所有配置
  121. env_values = dotenv_values(find_dotenv())
  122. accounts = set()
  123. # 默认账户
  124. accounts.add("default")
  125. # 查找带有账户名的配置
  126. prefix = f"{platform}_"
  127. for key in env_values.keys():
  128. if key.startswith(prefix) and "_" in key[len(prefix):]:
  129. account_name = key[len(prefix):].split("_")[0]
  130. if account_name != "default":
  131. accounts.add(account_name)
  132. return sorted(list(accounts))
  133. def run_repo_sync(self):
  134. repo_path = self.path_edit.text().strip()
  135. if not repo_path:
  136. QMessageBox.warning(self, "Warning", "Please select a local path.")
  137. return
  138. op_id = self.op_buttons.checkedId()
  139. pf_id = self.pf_buttons.checkedId()
  140. op = ["create", "push", "pull", "clone", "delete"][op_id]
  141. pf = self.platforms[pf_id]
  142. account = self.account_combo.currentText()
  143. # 清空结果区域
  144. self.result_text.clear()
  145. # 检查平台配置
  146. load_dotenv()
  147. token_key = f"{pf}_{account}_token" if account != "default" else f"{pf}_token"
  148. if not os.getenv(token_key):
  149. QMessageBox.warning(self, "Warning", f"Please configure {pf} token for account '{account}' in Settings tab first.")
  150. return
  151. # 构建命令
  152. cmd = [sys.executable, "-m", "repo_sync"]
  153. cmd.append(op)
  154. cmd.extend(["-p", pf])
  155. cmd.extend(["-repo_path", repo_path])
  156. # 执行命令
  157. self.run_btn.setEnabled(False)
  158. self.cancel_btn.setEnabled(True)
  159. self.result_text.append(f"Running: {' '.join(cmd)}\n")
  160. # 在新线程中执行命令
  161. self.process_thread = threading.Thread(
  162. target=self.run_process,
  163. args=(cmd,)
  164. )
  165. self.process_thread.daemon = True
  166. self.process_thread.start()
  167. def run_process(self, cmd):
  168. try:
  169. self.process = subprocess.Popen(
  170. cmd,
  171. stdout=subprocess.PIPE,
  172. stderr=subprocess.STDOUT,
  173. text=True,
  174. bufsize=1,
  175. universal_newlines=True
  176. )
  177. # 读取输出
  178. for line in self.process.stdout:
  179. self.command_signals.output.emit(line)
  180. self.process.wait()
  181. self.command_signals.finished.emit(self.process.returncode)
  182. except Exception as e:
  183. self.command_signals.output.emit(f"Error: {str(e)}")
  184. self.command_signals.finished.emit(1)
  185. def update_output(self, text):
  186. self.result_text.append(text)
  187. # 自动滚动到底部
  188. self.result_text.verticalScrollBar().setValue(
  189. self.result_text.verticalScrollBar().maximum()
  190. )
  191. def process_finished(self, return_code):
  192. self.process = None
  193. self.run_btn.setEnabled(True)
  194. self.cancel_btn.setEnabled(False)
  195. if return_code == 0:
  196. self.result_text.append("\nOperation completed successfully.")
  197. else:
  198. self.result_text.append(f"\nOperation failed with return code {return_code}.")
  199. def cancel_operation(self):
  200. if self.process:
  201. self.process.terminate()
  202. self.result_text.append("\nOperation cancelled by user.")
  203. # 添加账户对话框
  204. class AddAccountDialog(QDialog):
  205. def __init__(self, platform, parent=None):
  206. super().__init__(parent)
  207. self.platform = platform
  208. self.setWindowTitle(f"Add {platform.capitalize()} Account")
  209. self.resize(400, 200)
  210. layout = QVBoxLayout()
  211. form_layout = QFormLayout()
  212. self.account_name = QLineEdit()
  213. form_layout.addRow("Account Name:", self.account_name)
  214. # 根据平台添加相应字段
  215. self.fields = {}
  216. platform_fields = {
  217. "github": ["username", "token", "private"],
  218. "gitlab": ["host", "username", "token", "private"],
  219. "gitee": ["username", "token", "private"],
  220. "gitcode": ["username", "token", "private"],
  221. "git.yoq.me": ["username", "token", "private"],
  222. "coding": ["username", "token", "project", "private"],
  223. "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
  224. "cnb": ["username", "token", "private"]
  225. }
  226. for field in platform_fields.get(platform, ["username", "token"]):
  227. if field == "private":
  228. widget = QCheckBox()
  229. widget.setChecked(True)
  230. else:
  231. widget = QLineEdit()
  232. if field in ["token", "password"]:
  233. widget.setEchoMode(QLineEdit.Password)
  234. self.fields[field] = widget
  235. form_layout.addRow(f"{field.capitalize()}:", widget)
  236. layout.addLayout(form_layout)
  237. # 按钮
  238. buttons = QDialogButtonBox(
  239. QDialogButtonBox.Ok | QDialogButtonBox.Cancel
  240. )
  241. buttons.accepted.connect(self.accept)
  242. buttons.rejected.connect(self.reject)
  243. layout.addWidget(buttons)
  244. self.setLayout(layout)
  245. def get_account_data(self):
  246. data = {"name": self.account_name.text()}
  247. for field, widget in self.fields.items():
  248. if isinstance(widget, QCheckBox):
  249. data[field] = widget.isChecked()
  250. else:
  251. data[field] = widget.text()
  252. return data
  253. class SettingsTab(QWidget):
  254. def __init__(self, parent=None):
  255. super().__init__(parent)
  256. self.init_ui()
  257. self.load_settings()
  258. def init_ui(self):
  259. layout = QVBoxLayout()
  260. # 创建滚动区域
  261. scroll = QScrollArea()
  262. scroll.setWidgetResizable(True)
  263. content = QWidget()
  264. content_layout = QVBoxLayout()
  265. # 平台配置
  266. self.platform_configs = {
  267. "github": ["username", "token", "private"],
  268. "gitlab": ["host", "username", "token", "private"],
  269. "gitee": ["username", "token", "private"],
  270. "gitcode": ["username", "token", "private"],
  271. "git.yoq.me": ["username", "token", "private"],
  272. "coding": ["username", "token", "project", "private"],
  273. "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
  274. "cnb": ["username", "token", "private"]
  275. }
  276. # 为每个平台创建分组
  277. self.platform_groups = {}
  278. for platform in self.platform_configs.keys():
  279. group = QGroupBox(platform.capitalize())
  280. group_layout = QVBoxLayout()
  281. # 账户选择和管理
  282. account_header = QHBoxLayout()
  283. account_header.addWidget(QLabel("Accounts:"))
  284. account_combo = QComboBox()
  285. account_combo.setMinimumWidth(200)
  286. account_header.addWidget(account_combo)
  287. # 添加账户按钮
  288. add_btn = QToolButton()
  289. add_btn.setText("+")
  290. add_btn.clicked.connect(lambda checked, p=platform: self.add_account(p))
  291. account_header.addWidget(add_btn)
  292. # 删除账户按钮
  293. del_btn = QToolButton()
  294. del_btn.setText("-")
  295. del_btn.clicked.connect(lambda checked, p=platform, c=account_combo: self.delete_account(p, c))
  296. account_header.addWidget(del_btn)
  297. # 启用账户按钮
  298. enable_btn = QPushButton("Enable")
  299. enable_btn.clicked.connect(lambda checked, p=platform, c=account_combo: self.enable_account(p, c))
  300. account_header.addWidget(enable_btn)
  301. group_layout.addLayout(account_header)
  302. # 账户详情区域
  303. account_details = QWidget()
  304. account_form = QFormLayout()
  305. account_details.setLayout(account_form)
  306. group_layout.addWidget(account_details)
  307. group.setLayout(group_layout)
  308. content_layout.addWidget(group)
  309. # 保存引用
  310. self.platform_groups[platform] = {
  311. "group": group,
  312. "combo": account_combo,
  313. "details": account_details,
  314. "form": account_form
  315. }
  316. content.setLayout(content_layout)
  317. scroll.setWidget(content)
  318. layout.addWidget(scroll)
  319. # 保存按钮
  320. self.save_btn = QPushButton("Save Settings")
  321. self.save_btn.clicked.connect(self.save_settings)
  322. layout.addWidget(self.save_btn, alignment=Qt.AlignCenter)
  323. self.setLayout(layout)
  324. def load_settings(self):
  325. # 读取.env文件
  326. env_values = dotenv_values(find_dotenv())
  327. # 为每个平台加载账户
  328. for platform, group_data in self.platform_groups.items():
  329. combo = group_data["combo"]
  330. combo.clear()
  331. # 查找该平台的所有账户
  332. accounts = self.get_platform_accounts(platform, env_values)
  333. # 添加到下拉框
  334. for account in accounts:
  335. combo.addItem(account)
  336. # 连接选择变更事件
  337. combo.currentIndexChanged.connect(
  338. lambda idx, p=platform: self.update_account_details(p)
  339. )
  340. # 更新当前选择的账户详情
  341. if combo.count() > 0:
  342. self.update_account_details(platform)
  343. def get_platform_accounts(self, platform, env_values=None):
  344. if env_values is None:
  345. env_values = dotenv_values(find_dotenv())
  346. accounts = set()
  347. # 默认账户
  348. accounts.add("default")
  349. # 查找带有账户名的配置
  350. prefix = f"{platform}_"
  351. for key in env_values.keys():
  352. if key.startswith(prefix) and "_" in key[len(prefix):]:
  353. account_name = key[len(prefix):].split("_")[0]
  354. if account_name != "default":
  355. accounts.add(account_name)
  356. return sorted(list(accounts))
  357. def update_account_details(self, platform):
  358. group_data = self.platform_groups[platform]
  359. combo = group_data["combo"]
  360. form = group_data["form"]
  361. # 清空表单
  362. while form.rowCount() > 0:
  363. form.removeRow(0)
  364. # 获取当前选择的账户
  365. account = combo.currentText()
  366. if not account:
  367. return
  368. # 读取账户配置
  369. env_values = dotenv_values(find_dotenv())
  370. # 创建表单项
  371. self.field_widgets = {}
  372. for field in self.platform_configs[platform]:
  373. key = f"{platform}_{account}_{field}" if account != "default" else f"{platform}_{field}"
  374. value = env_values.get(key, "")
  375. if field == "private":
  376. widget = QCheckBox()
  377. widget.setChecked(value.lower() != "false")
  378. else:
  379. widget = QLineEdit()
  380. if field in ["token", "password"]:
  381. widget.setEchoMode(QLineEdit.Password)
  382. widget.setText(value)
  383. self.field_widgets[key] = widget
  384. form.addRow(f"{field.capitalize()}:", widget)
  385. def add_account(self, platform):
  386. dialog = AddAccountDialog(platform, self)
  387. if dialog.exec_() == QDialog.Accepted:
  388. account_data = dialog.get_account_data()
  389. account_name = account_data["name"]
  390. if not account_name:
  391. QMessageBox.warning(self, "Warning", "Account name cannot be empty.")
  392. return
  393. # 更新.env文件
  394. env_file = find_dotenv()
  395. if not env_file:
  396. env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
  397. for field, value in account_data.items():
  398. if field == "name":
  399. continue
  400. key = f"{platform}_{account_name}_{field}"
  401. if isinstance(value, bool):
  402. value = str(value).lower()
  403. set_key(env_file, key, value)
  404. # 重新加载设置
  405. self.load_settings()
  406. # 选择新账户
  407. combo = self.platform_groups[platform]["combo"]
  408. idx = combo.findText(account_name)
  409. if idx >= 0:
  410. combo.setCurrentIndex(idx)
  411. def delete_account(self, platform, combo):
  412. account = combo.currentText()
  413. if account == "default":
  414. QMessageBox.warning(self, "Warning", "Cannot delete the default account.")
  415. return
  416. reply = QMessageBox.question(
  417. self,
  418. "Confirm Deletion",
  419. f"Are you sure you want to delete the account '{account}' for {platform}?",
  420. QMessageBox.Yes | QMessageBox.No,
  421. QMessageBox.No
  422. )
  423. if reply == QMessageBox.Yes:
  424. # 删除.env中的相关配置
  425. env_file = find_dotenv()
  426. env_values = dotenv_values(env_file)
  427. prefix = f"{platform}_{account}_"
  428. keys_to_remove = [k for k in env_values.keys() if k.startswith(prefix)]
  429. # 重写.env文件,排除要删除的键
  430. with open(env_file, 'w') as f:
  431. for k, v in env_values.items():
  432. if k not in keys_to_remove:
  433. f.write(f"{k}={v}\n")
  434. # 重新加载设置
  435. self.load_settings()
  436. def enable_account(self, platform, combo):
  437. account = combo.currentText()
  438. if account == "default":
  439. QMessageBox.information(self, "Information", "Default account is already enabled.")
  440. return
  441. # 读取账户配置
  442. env_file = find_dotenv()
  443. env_values = dotenv_values(env_file)
  444. # 获取账户配置
  445. account_config = {}
  446. prefix = f"{platform}_{account}_"
  447. for key, value in env_values.items():
  448. if key.startswith(prefix):
  449. field = key[len(prefix):]
  450. account_config[field] = value
  451. # 更新默认配置
  452. for field, value in account_config.items():
  453. default_key = f"{platform}_{field}"
  454. set_key(env_file, default_key, value)
  455. QMessageBox.information(
  456. self,
  457. "Success",
  458. f"Account '{account}' has been enabled as the default for {platform}."
  459. )
  460. def save_settings(self):
  461. env_file = find_dotenv()
  462. if not env_file:
  463. env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
  464. # 保存当前显示的账户配置
  465. for platform, group_data in self.platform_groups.items():
  466. combo = group_data["combo"]
  467. account = combo.currentText()
  468. if account and hasattr(self, 'field_widgets'):
  469. for key, widget in self.field_widgets.items():
  470. if key.startswith(f"{platform}_{account}"):
  471. if isinstance(widget, QCheckBox):
  472. value = str(widget.isChecked()).lower()
  473. else:
  474. value = widget.text().strip()
  475. if value:
  476. set_key(env_file, key, value)
  477. QMessageBox.information(self, "Success", "Settings saved successfully!")
  478. class AboutTab(QWidget):
  479. def __init__(self, parent=None):
  480. super().__init__(parent)
  481. layout = QVBoxLayout()
  482. layout.addWidget(QLabel(f"repo_sync tools v{__version__}"))
  483. layout.addWidget(QLabel("作者: liuyuqi.gov@msn.cn"))
  484. layout.addWidget(QLabel("GitHub: https://github.com/jianboy/repo_sync"))
  485. layout.addWidget(QLabel("\n功能说明:"))
  486. layout.addWidget(QLabel("- 支持多个代码托管平台"))
  487. layout.addWidget(QLabel("- 支持创建/推送/拉取/克隆/删除操作"))
  488. layout.addWidget(QLabel("- 自动获取资源管理器当前路径"))
  489. layout.addWidget(QLabel("- 配置信息保存在.env文件中"))
  490. layout.addWidget(QLabel("- 支持每个平台配置多个账户"))
  491. layout.addWidget(QLabel("- 命令行执行结果实时显示"))
  492. self.setLayout(layout)
  493. class RepoSyncMainWindow(QTabWidget):
  494. def __init__(self):
  495. super().__init__()
  496. self.setWindowTitle('repo_sync tools v1.12')
  497. self.resize(800, 700)
  498. self.main_tab = MainTab()
  499. self.addTab(self.main_tab, '主界面')
  500. self.settings_tab = SettingsTab()
  501. self.addTab(self.settings_tab, '设置')
  502. self.about_tab = AboutTab()
  503. self.addTab(self.about_tab, '关于')
  504. def main():
  505. app = QApplication(sys.argv)
  506. window = RepoSyncMainWindow()
  507. window.show()
  508. sys.exit(app.exec_())
  509. if __name__ == '__main__':
  510. main()