123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614 |
- import sys
- import os
- import threading
- import subprocess
- from PyQt5.QtWidgets import (
- QApplication, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QRadioButton, QPushButton, QButtonGroup, QGroupBox, QMessageBox,
- QLineEdit, QScrollArea, QFileDialog, QFormLayout, QCheckBox, QTextEdit,
- QComboBox, QDialog, QDialogButtonBox, QGridLayout, QToolButton
- )
- from PyQt5.QtCore import Qt, pyqtSignal, QObject
- from repo_sync import RepoSync, __version__
- from dotenv import load_dotenv, set_key, find_dotenv, dotenv_values
- import json
- import uuid
- # Explorer路径获取
- try:
- import win32com.client
- def get_active_explorer_path():
- shell = win32com.client.Dispatch("Shell.Application")
- for window in shell.Windows():
- if window.Name in ["文件资源管理器", "Windows Explorer"]:
- return window.Document.Folder.Self.Path
- return os.getcwd()
- except ImportError:
- def get_active_explorer_path():
- return os.getcwd()
- # 命令执行信号类
- class CommandSignals(QObject):
- output = pyqtSignal(str)
- finished = pyqtSignal(int)
- class MainTab(QWidget):
- def __init__(self, parent=None):
- super().__init__(parent)
- layout = QVBoxLayout()
-
- # 路径选择
- path_layout = QHBoxLayout()
- self.path_label = QLabel("Local Path: ")
- self.path_edit = QLineEdit()
- self.path_edit.setText(get_active_explorer_path())
- self.path_btn = QPushButton("Browse...")
- self.path_btn.clicked.connect(self.choose_path)
- path_layout.addWidget(self.path_label)
- path_layout.addWidget(self.path_edit)
- path_layout.addWidget(self.path_btn)
- layout.addLayout(path_layout)
-
- # 操作
- op_group = QGroupBox("Operation:")
- op_layout = QHBoxLayout()
- self.op_buttons = QButtonGroup(self)
- for i, op in enumerate(["create", "push", "pull", "clone", "delete"]):
- btn = QRadioButton(op.capitalize())
- if i == 0:
- btn.setChecked(True)
- self.op_buttons.addButton(btn, i)
- op_layout.addWidget(btn)
- op_group.setLayout(op_layout)
- layout.addWidget(op_group)
-
- # 平台
- pf_group = QGroupBox("Platform:")
- pf_layout = QHBoxLayout()
- self.pf_buttons = QButtonGroup(self)
- self.platforms = ["github", "gitlab", "gitee", "gitcode", "git.yoq.me", "coding", "aliyun", "cnb"]
- for i, pf in enumerate(self.platforms):
- btn = QRadioButton(pf.capitalize())
- if i == 0:
- btn.setChecked(True)
- self.pf_buttons.addButton(btn, i)
- pf_layout.addWidget(btn)
- pf_group.setLayout(pf_layout)
- layout.addWidget(pf_group)
-
- # 账户选择
- account_layout = QHBoxLayout()
- account_layout.addWidget(QLabel("Account:"))
- self.account_combo = QComboBox()
- self.account_combo.setMinimumWidth(200)
- account_layout.addWidget(self.account_combo)
- account_layout.addStretch()
- layout.addLayout(account_layout)
-
- # 按钮区域 (run和cancel)
- btn_layout = QHBoxLayout()
- self.run_btn = QPushButton("run it")
- self.run_btn.clicked.connect(self.run_repo_sync)
- self.cancel_btn = QPushButton("Cancel")
- self.cancel_btn.clicked.connect(self.cancel_operation)
- self.cancel_btn.setEnabled(False)
- btn_layout.addWidget(self.run_btn)
- btn_layout.addWidget(self.cancel_btn)
- layout.addLayout(btn_layout)
-
- # 命令执行结果
- result_group = QGroupBox("Execution Result:")
- result_layout = QVBoxLayout()
- self.result_text = QTextEdit()
- self.result_text.setReadOnly(True)
- result_layout.addWidget(self.result_text)
- result_group.setLayout(result_layout)
- layout.addWidget(result_group)
-
- self.setLayout(layout)
-
- # 命令执行相关
- self.process = None
- self.command_signals = CommandSignals()
- self.command_signals.output.connect(self.update_output)
- self.command_signals.finished.connect(self.process_finished)
-
- # 平台变更时更新账户列表
- self.pf_buttons.buttonClicked.connect(self.update_account_list)
- self.update_account_list()
- def choose_path(self):
- path = QFileDialog.getExistingDirectory(self, "Select Directory")
- if path:
- self.path_edit.setText(path)
- def update_account_list(self):
- self.account_combo.clear()
- pf_id = self.pf_buttons.checkedId()
- platform = self.platforms[pf_id]
-
- # 读取所有账户
- accounts = self.get_platform_accounts(platform)
- for account_name in accounts:
- self.account_combo.addItem(account_name)
- def get_platform_accounts(self, platform):
- # 读取.env文件中的所有配置
- env_values = dotenv_values(find_dotenv())
- accounts = set()
-
- # 默认账户
- accounts.add("default")
-
- # 查找带有账户名的配置
- prefix = f"{platform}_"
- for key in env_values.keys():
- if key.startswith(prefix) and "_" in key[len(prefix):]:
- account_name = key[len(prefix):].split("_")[0]
- if account_name != "default":
- accounts.add(account_name)
-
- return sorted(list(accounts))
- def run_repo_sync(self):
- repo_path = self.path_edit.text().strip()
- if not repo_path:
- QMessageBox.warning(self, "Warning", "Please select a local path.")
- return
-
- op_id = self.op_buttons.checkedId()
- pf_id = self.pf_buttons.checkedId()
- op = ["create", "push", "pull", "clone", "delete"][op_id]
- pf = self.platforms[pf_id]
- account = self.account_combo.currentText()
-
- # 清空结果区域
- self.result_text.clear()
-
- # 检查平台配置
- load_dotenv()
- token_key = f"{pf}_{account}_token" if account != "default" else f"{pf}_token"
- if not os.getenv(token_key):
- QMessageBox.warning(self, "Warning", f"Please configure {pf} token for account '{account}' in Settings tab first.")
- return
-
- # 构建命令
- cmd = [sys.executable, "-m", "repo_sync"]
- cmd.append(op)
- cmd.extend(["-p", pf])
- cmd.extend(["-repo_path", repo_path])
-
- # 执行命令
- self.run_btn.setEnabled(False)
- self.cancel_btn.setEnabled(True)
- self.result_text.append(f"Running: {' '.join(cmd)}\n")
-
- # 在新线程中执行命令
- self.process_thread = threading.Thread(
- target=self.run_process,
- args=(cmd,)
- )
- self.process_thread.daemon = True
- self.process_thread.start()
- def run_process(self, cmd):
- try:
- self.process = subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- bufsize=1,
- universal_newlines=True
- )
-
- # 读取输出
- for line in self.process.stdout:
- self.command_signals.output.emit(line)
-
- self.process.wait()
- self.command_signals.finished.emit(self.process.returncode)
- except Exception as e:
- self.command_signals.output.emit(f"Error: {str(e)}")
- self.command_signals.finished.emit(1)
- def update_output(self, text):
- self.result_text.append(text)
- # 自动滚动到底部
- self.result_text.verticalScrollBar().setValue(
- self.result_text.verticalScrollBar().maximum()
- )
- def process_finished(self, return_code):
- self.process = None
- self.run_btn.setEnabled(True)
- self.cancel_btn.setEnabled(False)
-
- if return_code == 0:
- self.result_text.append("\nOperation completed successfully.")
- else:
- self.result_text.append(f"\nOperation failed with return code {return_code}.")
- def cancel_operation(self):
- if self.process:
- self.process.terminate()
- self.result_text.append("\nOperation cancelled by user.")
- # 添加账户对话框
- class AddAccountDialog(QDialog):
- def __init__(self, platform, parent=None):
- super().__init__(parent)
- self.platform = platform
- self.setWindowTitle(f"Add {platform.capitalize()} Account")
- self.resize(400, 200)
-
- layout = QVBoxLayout()
-
- form_layout = QFormLayout()
- self.account_name = QLineEdit()
- form_layout.addRow("Account Name:", self.account_name)
-
- # 根据平台添加相应字段
- self.fields = {}
- platform_fields = {
- "github": ["username", "token", "private"],
- "gitlab": ["host", "username", "token", "private"],
- "gitee": ["username", "token", "private"],
- "gitcode": ["username", "token", "private"],
- "git.yoq.me": ["username", "token", "private"],
- "coding": ["username", "token", "project", "private"],
- "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
- "cnb": ["username", "token", "private"]
- }
-
- for field in platform_fields.get(platform, ["username", "token"]):
- if field == "private":
- widget = QCheckBox()
- widget.setChecked(True)
- else:
- widget = QLineEdit()
- if field in ["token", "password"]:
- widget.setEchoMode(QLineEdit.Password)
-
- self.fields[field] = widget
- form_layout.addRow(f"{field.capitalize()}:", widget)
-
- layout.addLayout(form_layout)
-
- # 按钮
- buttons = QDialogButtonBox(
- QDialogButtonBox.Ok | QDialogButtonBox.Cancel
- )
- buttons.accepted.connect(self.accept)
- buttons.rejected.connect(self.reject)
- layout.addWidget(buttons)
-
- self.setLayout(layout)
-
- def get_account_data(self):
- data = {"name": self.account_name.text()}
- for field, widget in self.fields.items():
- if isinstance(widget, QCheckBox):
- data[field] = widget.isChecked()
- else:
- data[field] = widget.text()
- return data
- class SettingsTab(QWidget):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.init_ui()
- self.load_settings()
- def init_ui(self):
- layout = QVBoxLayout()
-
- # 创建滚动区域
- scroll = QScrollArea()
- scroll.setWidgetResizable(True)
- content = QWidget()
- content_layout = QVBoxLayout()
-
- # 平台配置
- self.platform_configs = {
- "github": ["username", "token", "private"],
- "gitlab": ["host", "username", "token", "private"],
- "gitee": ["username", "token", "private"],
- "gitcode": ["username", "token", "private"],
- "git.yoq.me": ["username", "token", "private"],
- "coding": ["username", "token", "project", "private"],
- "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
- "cnb": ["username", "token", "private"]
- }
-
- # 为每个平台创建分组
- self.platform_groups = {}
- for platform in self.platform_configs.keys():
- group = QGroupBox(platform.capitalize())
- group_layout = QVBoxLayout()
-
- # 账户选择和管理
- account_header = QHBoxLayout()
- account_header.addWidget(QLabel("Accounts:"))
- account_combo = QComboBox()
- account_combo.setMinimumWidth(200)
- account_header.addWidget(account_combo)
-
- # 添加账户按钮
- add_btn = QToolButton()
- add_btn.setText("+")
- add_btn.clicked.connect(lambda checked, p=platform: self.add_account(p))
- account_header.addWidget(add_btn)
-
- # 删除账户按钮
- del_btn = QToolButton()
- del_btn.setText("-")
- del_btn.clicked.connect(lambda checked, p=platform, c=account_combo: self.delete_account(p, c))
- account_header.addWidget(del_btn)
-
- # 启用账户按钮
- enable_btn = QPushButton("Enable")
- enable_btn.clicked.connect(lambda checked, p=platform, c=account_combo: self.enable_account(p, c))
- account_header.addWidget(enable_btn)
-
- group_layout.addLayout(account_header)
-
- # 账户详情区域
- account_details = QWidget()
- account_form = QFormLayout()
- account_details.setLayout(account_form)
- group_layout.addWidget(account_details)
-
- group.setLayout(group_layout)
- content_layout.addWidget(group)
-
- # 保存引用
- self.platform_groups[platform] = {
- "group": group,
- "combo": account_combo,
- "details": account_details,
- "form": account_form
- }
-
- content.setLayout(content_layout)
- scroll.setWidget(content)
- layout.addWidget(scroll)
-
- # 保存按钮
- self.save_btn = QPushButton("Save Settings")
- self.save_btn.clicked.connect(self.save_settings)
- layout.addWidget(self.save_btn, alignment=Qt.AlignCenter)
-
- self.setLayout(layout)
- def load_settings(self):
- # 读取.env文件
- env_values = dotenv_values(find_dotenv())
-
- # 为每个平台加载账户
- for platform, group_data in self.platform_groups.items():
- combo = group_data["combo"]
- combo.clear()
-
- # 查找该平台的所有账户
- accounts = self.get_platform_accounts(platform, env_values)
-
- # 添加到下拉框
- for account in accounts:
- combo.addItem(account)
-
- # 连接选择变更事件
- combo.currentIndexChanged.connect(
- lambda idx, p=platform: self.update_account_details(p)
- )
-
- # 更新当前选择的账户详情
- if combo.count() > 0:
- self.update_account_details(platform)
- def get_platform_accounts(self, platform, env_values=None):
- if env_values is None:
- env_values = dotenv_values(find_dotenv())
-
- accounts = set()
- # 默认账户
- accounts.add("default")
-
- # 查找带有账户名的配置
- prefix = f"{platform}_"
- for key in env_values.keys():
- if key.startswith(prefix) and "_" in key[len(prefix):]:
- account_name = key[len(prefix):].split("_")[0]
- if account_name != "default":
- accounts.add(account_name)
-
- return sorted(list(accounts))
- def update_account_details(self, platform):
- group_data = self.platform_groups[platform]
- combo = group_data["combo"]
- form = group_data["form"]
-
- # 清空表单
- while form.rowCount() > 0:
- form.removeRow(0)
-
- # 获取当前选择的账户
- account = combo.currentText()
- if not account:
- return
-
- # 读取账户配置
- env_values = dotenv_values(find_dotenv())
-
- # 创建表单项
- self.field_widgets = {}
- for field in self.platform_configs[platform]:
- key = f"{platform}_{account}_{field}" if account != "default" else f"{platform}_{field}"
- value = env_values.get(key, "")
-
- if field == "private":
- widget = QCheckBox()
- widget.setChecked(value.lower() != "false")
- else:
- widget = QLineEdit()
- if field in ["token", "password"]:
- widget.setEchoMode(QLineEdit.Password)
- widget.setText(value)
-
- self.field_widgets[key] = widget
- form.addRow(f"{field.capitalize()}:", widget)
- def add_account(self, platform):
- dialog = AddAccountDialog(platform, self)
- if dialog.exec_() == QDialog.Accepted:
- account_data = dialog.get_account_data()
- account_name = account_data["name"]
-
- if not account_name:
- QMessageBox.warning(self, "Warning", "Account name cannot be empty.")
- return
-
- # 更新.env文件
- env_file = find_dotenv()
- if not env_file:
- env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
-
- for field, value in account_data.items():
- if field == "name":
- continue
-
- key = f"{platform}_{account_name}_{field}"
- if isinstance(value, bool):
- value = str(value).lower()
-
- set_key(env_file, key, value)
-
- # 重新加载设置
- self.load_settings()
-
- # 选择新账户
- combo = self.platform_groups[platform]["combo"]
- idx = combo.findText(account_name)
- if idx >= 0:
- combo.setCurrentIndex(idx)
- def delete_account(self, platform, combo):
- account = combo.currentText()
- if account == "default":
- QMessageBox.warning(self, "Warning", "Cannot delete the default account.")
- return
-
- reply = QMessageBox.question(
- self,
- "Confirm Deletion",
- f"Are you sure you want to delete the account '{account}' for {platform}?",
- QMessageBox.Yes | QMessageBox.No,
- QMessageBox.No
- )
-
- if reply == QMessageBox.Yes:
- # 删除.env中的相关配置
- env_file = find_dotenv()
- env_values = dotenv_values(env_file)
-
- prefix = f"{platform}_{account}_"
- keys_to_remove = [k for k in env_values.keys() if k.startswith(prefix)]
-
- # 重写.env文件,排除要删除的键
- with open(env_file, 'w') as f:
- for k, v in env_values.items():
- if k not in keys_to_remove:
- f.write(f"{k}={v}\n")
-
- # 重新加载设置
- self.load_settings()
- def enable_account(self, platform, combo):
- account = combo.currentText()
- if account == "default":
- QMessageBox.information(self, "Information", "Default account is already enabled.")
- return
-
- # 读取账户配置
- env_file = find_dotenv()
- env_values = dotenv_values(env_file)
-
- # 获取账户配置
- account_config = {}
- prefix = f"{platform}_{account}_"
- for key, value in env_values.items():
- if key.startswith(prefix):
- field = key[len(prefix):]
- account_config[field] = value
-
- # 更新默认配置
- for field, value in account_config.items():
- default_key = f"{platform}_{field}"
- set_key(env_file, default_key, value)
-
- QMessageBox.information(
- self,
- "Success",
- f"Account '{account}' has been enabled as the default for {platform}."
- )
- def save_settings(self):
- env_file = find_dotenv()
- if not env_file:
- env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
-
- # 保存当前显示的账户配置
- for platform, group_data in self.platform_groups.items():
- combo = group_data["combo"]
- account = combo.currentText()
-
- if account and hasattr(self, 'field_widgets'):
- for key, widget in self.field_widgets.items():
- if key.startswith(f"{platform}_{account}"):
- if isinstance(widget, QCheckBox):
- value = str(widget.isChecked()).lower()
- else:
- value = widget.text().strip()
-
- if value:
- set_key(env_file, key, value)
-
- QMessageBox.information(self, "Success", "Settings saved successfully!")
- class AboutTab(QWidget):
- def __init__(self, parent=None):
- super().__init__(parent)
- layout = QVBoxLayout()
- layout.addWidget(QLabel(f"repo_sync tools v{__version__}"))
- layout.addWidget(QLabel("作者: liuyuqi.gov@msn.cn"))
- layout.addWidget(QLabel("GitHub: https://github.com/jianboy/repo_sync"))
- layout.addWidget(QLabel("\n功能说明:"))
- layout.addWidget(QLabel("- 支持多个代码托管平台"))
- layout.addWidget(QLabel("- 支持创建/推送/拉取/克隆/删除操作"))
- layout.addWidget(QLabel("- 自动获取资源管理器当前路径"))
- layout.addWidget(QLabel("- 配置信息保存在.env文件中"))
- layout.addWidget(QLabel("- 支持每个平台配置多个账户"))
- layout.addWidget(QLabel("- 命令行执行结果实时显示"))
- self.setLayout(layout)
- class RepoSyncMainWindow(QTabWidget):
- def __init__(self):
- super().__init__()
- self.setWindowTitle('repo_sync tools v1.12')
- self.resize(800, 700)
- self.main_tab = MainTab()
- self.addTab(self.main_tab, '主界面')
- self.settings_tab = SettingsTab()
- self.addTab(self.settings_tab, '设置')
- self.about_tab = AboutTab()
- self.addTab(self.about_tab, '关于')
- def main():
- app = QApplication(sys.argv)
- window = RepoSyncMainWindow()
- window.show()
- sys.exit(app.exec_())
- if __name__ == '__main__':
- main()
|