diff --git a/README.md b/README.md index c91d6be..b7328e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # QuickGit - 萌芽一键Git管理工具 -一个简单易用的Git命令行管理工具,让Git操作更加便捷高效。 +一个简单易用的模块化Git命令行管理工具,让Git操作更加便捷高效。 ## 功能特性 @@ -10,12 +10,33 @@ - **远程仓库管理** - 便捷地添加、删除、查看远程仓库 - **状态查看** - 快速查看仓库状态和提交历史 - **彩色界面** - 友好的彩色控制台输出 +- **模块化设计** - 易于维护和扩展 + +## 项目结构 + +``` +QuickGit/ +├── quickgit/ # 核心模块 +│ ├── __init__.py # 包初始化 +│ ├── config.py # 配置模块 +│ ├── utils.py # 工具类(命令执行、输出格式化、输入验证) +│ ├── git_operations.py # Git操作模块 +│ ├── remote_manager.py # 远程仓库管理模块 +│ └── ui.py # UI交互模块 +├── quickgit.py # 主程序入口 +├── mengya_git_manager.py # 旧版单文件脚本(兼容保留) +└── README.md # 项目文档 +``` ## 快速开始 ### 运行脚本 ```bash +# 使用新版模块化脚本(推荐) +python quickgit.py + +# 或使用旧版单文件脚本 python mengya_git_manager.py ``` @@ -103,12 +124,40 @@ A: 使用 `5. 管理远程仓库` 功能添加或删除远程仓库 **Q: 支持哪些Git操作?** A: 目前支持init、add、commit、push、pull等常用操作 +## 模块说明 + +### config.py - 配置模块 +存储所有配置信息,包括Gitea服务器地址、GitHub用户名、.gitignore模板等。 + +### utils.py - 工具类模块 +- `Colors`: 控制台颜色定义 +- `CommandExecutor`: 命令执行器 +- `OutputFormatter`: 输出格式化器 +- `InputValidator`: 输入验证器 + +### git_operations.py - Git操作模块 +提供Git基本操作功能: +- 初始化仓库 +- 检查状态 +- 添加/提交更改 +- 推送/拉取代码 + +### remote_manager.py - 远程仓库管理模块 +管理GitHub和Gitea远程仓库: +- 添加/删除远程仓库 +- 查看远程仓库列表 +- 选择推送/拉取目标 + +### ui.py - UI交互模块 +处理用户界面和交互逻辑,整合所有功能模块。 + ## 开发计划 +- [x] 模块化架构重构 - [ ] 支持分支管理 - [ ] 支持标签管理 - [ ] 支持冲突解决辅助 -- [ ] 支持配置文件 +- [ ] 支持自定义配置文件 - [ ] 支持批量操作多个仓库 ## 许可证 diff --git a/mengya_git_manager.py b/mengya_git_manager.py deleted file mode 100644 index 50dbf77..0000000 --- a/mengya_git_manager.py +++ /dev/null @@ -1,561 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -萌芽一键Git管理工具 -支持快速初始化Git仓库、提交更改、推送到GitHub和Gitea -""" - -import os -import sys -import subprocess -from typing import Optional, List - - -class Colors: - """控制台颜色""" - HEADER = '\033[95m' - BLUE = '\033[94m' - CYAN = '\033[96m' - GREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[91m' - ENDC = '\033[0m' - BOLD = '\033[1m' - - -class GitManager: - """Git管理器""" - - def __init__(self): - self.gitea_host = "repo.shumengya.top" - self.gitea_port = "8022" - self.github_user = "shumengya" - self.current_dir = os.getcwd() - - def print_header(self, text: str): - """打印标题""" - print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*50}") - print(f" {text}") - print(f"{'='*50}{Colors.ENDC}\n") - - def print_success(self, text: str): - """打印成功信息""" - print(f"{Colors.GREEN}✓ {text}{Colors.ENDC}") - - def print_error(self, text: str): - """打印错误信息""" - print(f"{Colors.FAIL}✗ {text}{Colors.ENDC}") - - def print_info(self, text: str): - """打印提示信息""" - print(f"{Colors.CYAN}ℹ {text}{Colors.ENDC}") - - def print_warning(self, text: str): - """打印警告信息""" - print(f"{Colors.WARNING}⚠ {text}{Colors.ENDC}") - - def run_command(self, command: str, show_output: bool = True) -> tuple[bool, str]: - """ - 执行命令 - 返回: (是否成功, 输出内容) - """ - try: - result = subprocess.run( - command, - shell=True, - capture_output=True, - text=True, - encoding='utf-8', - errors='ignore' - ) - - output = result.stdout + result.stderr - - if show_output and output.strip(): - print(output) - - return result.returncode == 0, output - except Exception as e: - self.print_error(f"命令执行失败: {str(e)}") - return False, str(e) - - def is_git_repo(self) -> bool: - """检查当前目录是否是Git仓库""" - return os.path.isdir('.git') - - def get_gitignore_template(self) -> str: - """获取.gitignore模板""" - return """# Node/React -node_modules/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* -build/ -dist/ -coverage/ -.env.local -.env.development.local -.env.test.local -.env.production.local - -# Go -*.exe -*.exe~ -*.test -*.out -*.dll -*.so -*.dylib -vendor/ - -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -env/ -venv/ -ENV/ -*.egg-info/ -dist/ -build/ - -# 数据文件 -data/data.json -*.db -*.sqlite - -# 日志文件 -*.log -logs/ - -# 操作系统 -.DS_Store -Thumbs.db -desktop.ini - -# IDE -.idea/ -.vscode/ -*.swp -*.swo -*~ -.project -.classpath -.settings/ - -# 其他 -*.bak -*.tmp -*.temp -""" - - def init_git_repo(self): - """初始化Git仓库""" - self.print_header("初始化Git仓库") - - if self.is_git_repo(): - self.print_warning("当前目录已经是Git仓库") - return True - - # 1. 初始化Git仓库 - self.print_info("正在初始化Git仓库...") - success, _ = self.run_command("git init", show_output=False) - if not success: - self.print_error("Git初始化失败") - return False - self.print_success("Git仓库初始化成功") - - # 2. 创建main分支 - self.print_info("正在创建main分支...") - success, _ = self.run_command("git checkout -b main", show_output=False) - if success: - self.print_success("main分支创建成功") - else: - self.print_warning("main分支创建失败,将使用默认分支") - - # 3. 创建.gitignore文件 - self.print_info("正在创建.gitignore文件...") - try: - with open('.gitignore', 'w', encoding='utf-8') as f: - f.write(self.get_gitignore_template()) - self.print_success(".gitignore文件创建成功") - except Exception as e: - self.print_error(f".gitignore文件创建失败: {str(e)}") - - # 4. 首次提交 - self.print_info("正在进行首次提交...") - self.run_command("git add .", show_output=False) - success, _ = self.run_command('git commit -m "first commit"', show_output=False) - if success: - self.print_success("首次提交完成") - else: - self.print_warning("首次提交失败(可能没有文件可提交)") - - # 5. 配置远程仓库 - self.configure_remotes() - - return True - - def configure_remotes(self): - """配置远程仓库""" - self.print_info("\n配置远程仓库...") - - print("\n请选择要配置的远程仓库:") - print("1. GitHub") - print("2. Gitea") - print("3. 两者都配置") - print("4. 跳过") - - choice = input("\n请选择 [1-4]: ").strip() - - if choice == '1': - self.add_github_remote() - elif choice == '2': - self.add_gitea_remote() - elif choice == '3': - self.add_github_remote() - self.add_gitea_remote() - else: - self.print_info("跳过远程仓库配置") - - def add_github_remote(self): - """添加GitHub远程仓库""" - repo_name = input(f"\n请输入GitHub仓库名: ").strip() - if not repo_name: - self.print_warning("仓库名不能为空,跳过GitHub配置") - return - - remote_url = f"git@github.com:{self.github_user}/{repo_name}.git" - - # 检查remote是否已存在 - success, output = self.run_command("git remote", show_output=False) - if "github" in output: - self.run_command("git remote remove github", show_output=False) - - success, _ = self.run_command(f'git remote add github {remote_url}', show_output=False) - if success: - self.print_success(f"GitHub远程仓库已添加: {remote_url}") - else: - self.print_error("GitHub远程仓库添加失败") - - def add_gitea_remote(self): - """添加Gitea远程仓库""" - user = input(f"\n请输入Gitea用户名: ").strip() - if not user: - self.print_warning("用户名不能为空,跳过Gitea配置") - return - - repo_name = input(f"请输入Gitea仓库名: ").strip() - if not repo_name: - self.print_warning("仓库名不能为空,跳过Gitea配置") - return - - remote_url = f"ssh://git@{self.gitea_host}:{self.gitea_port}/{user}/{repo_name}.git" - - # 检查remote是否已存在 - success, output = self.run_command("git remote", show_output=False) - if "gitea" in output: - self.run_command("git remote remove gitea", show_output=False) - - success, _ = self.run_command(f'git remote add gitea {remote_url}', show_output=False) - if success: - self.print_success(f"Gitea远程仓库已添加: {remote_url}") - else: - self.print_error("Gitea远程仓库添加失败") - - def commit_and_push(self): - """提交并推送更改""" - self.print_header("提交并推送更改") - - if not self.is_git_repo(): - self.print_error("当前目录不是Git仓库,请先初始化") - return False - - # 1. 检查是否有更改 - self.print_info("检查文件更改...") - success, output = self.run_command("git status --short", show_output=False) - - if not output.strip(): - self.print_warning("没有文件更改,无需提交") - return True - - print("\n当前更改的文件:") - print(output) - - # 2. 输入提交信息 - commit_msg = input("\n请输入提交信息 (直接回车使用默认信息): ").strip() - if not commit_msg: - from datetime import datetime - commit_msg = f"update: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - - # 3. 添加所有更改 - self.print_info("正在添加文件...") - success, _ = self.run_command("git add .", show_output=False) - if not success: - self.print_error("添加文件失败") - return False - self.print_success("文件添加成功") - - # 4. 提交更改 - self.print_info("正在提交更改...") - success, _ = self.run_command(f'git commit -m "{commit_msg}"', show_output=True) - if not success: - self.print_error("提交失败") - return False - self.print_success("提交成功") - - # 5. 推送到远程仓库 - self.push_to_remote() - - return True - - def push_to_remote(self): - """推送到远程仓库""" - # 获取当前分支 - success, branch = self.run_command("git branch --show-current", show_output=False) - if not success or not branch.strip(): - branch = "main" - else: - branch = branch.strip() - - # 获取所有远程仓库 - success, output = self.run_command("git remote", show_output=False) - if not success or not output.strip(): - self.print_warning("没有配置远程仓库") - return - - remotes = [r.strip() for r in output.strip().split('\n') if r.strip()] - - if not remotes: - self.print_warning("没有配置远程仓库") - return - - print("\n可用的远程仓库:") - for idx, remote in enumerate(remotes, 1): - print(f"{idx}. {remote}") - print(f"{len(remotes) + 1}. 全部推送") - - choice = input(f"\n请选择要推送的远程仓库 [1-{len(remotes) + 1}]: ").strip() - - try: - choice_idx = int(choice) - if choice_idx == len(remotes) + 1: - # 推送到所有远程仓库 - for remote in remotes: - self.push_to_specific_remote(remote, branch) - elif 1 <= choice_idx <= len(remotes): - # 推送到指定远程仓库 - remote = remotes[choice_idx - 1] - self.push_to_specific_remote(remote, branch) - else: - self.print_error("无效的选择") - except ValueError: - self.print_error("无效的输入") - - def push_to_specific_remote(self, remote: str, branch: str): - """推送到指定远程仓库""" - self.print_info(f"正在推送到 {remote}...") - - # 检查是否需要设置上游分支 - success, _ = self.run_command(f"git push {remote} {branch}", show_output=True) - - if not success: - # 尝试设置上游分支 - self.print_info(f"尝试设置上游分支...") - success, _ = self.run_command(f"git push -u {remote} {branch}", show_output=True) - - if success: - self.print_success(f"成功推送到 {remote}") - else: - self.print_error(f"推送到 {remote} 失败") - - def pull_from_remote(self): - """从远程仓库拉取""" - self.print_header("从远程仓库拉取") - - if not self.is_git_repo(): - self.print_error("当前目录不是Git仓库") - return False - - # 获取所有远程仓库 - success, output = self.run_command("git remote", show_output=False) - if not success or not output.strip(): - self.print_warning("没有配置远程仓库") - return False - - remotes = [r.strip() for r in output.strip().split('\n') if r.strip()] - - if not remotes: - self.print_warning("没有配置远程仓库") - return False - - print("\n可用的远程仓库:") - for idx, remote in enumerate(remotes, 1): - print(f"{idx}. {remote}") - - choice = input(f"\n请选择要拉取的远程仓库 [1-{len(remotes)}]: ").strip() - - try: - choice_idx = int(choice) - if 1 <= choice_idx <= len(remotes): - remote = remotes[choice_idx - 1] - - # 获取当前分支 - success, branch = self.run_command("git branch --show-current", show_output=False) - if not success or not branch.strip(): - branch = "main" - else: - branch = branch.strip() - - self.print_info(f"正在从 {remote} 拉取 {branch} 分支...") - success, _ = self.run_command(f"git pull {remote} {branch}", show_output=True) - - if success: - self.print_success(f"成功从 {remote} 拉取更新") - else: - self.print_error(f"从 {remote} 拉取失败") - else: - self.print_error("无效的选择") - except ValueError: - self.print_error("无效的输入") - - def show_status(self): - """显示仓库状态""" - self.print_header("仓库状态") - - if not self.is_git_repo(): - self.print_error("当前目录不是Git仓库") - return - - print(f"{Colors.CYAN}当前目录:{Colors.ENDC} {self.current_dir}\n") - - # Git状态 - self.print_info("Git状态:") - self.run_command("git status", show_output=True) - - # 远程仓库 - print(f"\n{Colors.CYAN}远程仓库:{Colors.ENDC}") - self.run_command("git remote -v", show_output=True) - - # 最近提交 - print(f"\n{Colors.CYAN}最近3次提交:{Colors.ENDC}") - self.run_command("git log --oneline -3", show_output=True) - - def manage_remotes(self): - """管理远程仓库""" - self.print_header("管理远程仓库") - - if not self.is_git_repo(): - self.print_error("当前目录不是Git仓库") - return - - while True: - print("\n远程仓库管理:") - print("1. 查看远程仓库") - print("2. 添加GitHub远程仓库") - print("3. 添加Gitea远程仓库") - print("4. 删除远程仓库") - print("5. 返回主菜单") - - choice = input("\n请选择 [1-5]: ").strip() - - if choice == '1': - print(f"\n{Colors.CYAN}当前远程仓库:{Colors.ENDC}") - self.run_command("git remote -v", show_output=True) - elif choice == '2': - self.add_github_remote() - elif choice == '3': - self.add_gitea_remote() - elif choice == '4': - self.remove_remote() - elif choice == '5': - break - else: - self.print_error("无效的选择") - - def remove_remote(self): - """删除远程仓库""" - success, output = self.run_command("git remote", show_output=False) - if not success or not output.strip(): - self.print_warning("没有配置远程仓库") - return - - remotes = [r.strip() for r in output.strip().split('\n') if r.strip()] - - if not remotes: - self.print_warning("没有配置远程仓库") - return - - print("\n当前远程仓库:") - for idx, remote in enumerate(remotes, 1): - print(f"{idx}. {remote}") - - choice = input(f"\n请选择要删除的远程仓库 [1-{len(remotes)}]: ").strip() - - try: - choice_idx = int(choice) - if 1 <= choice_idx <= len(remotes): - remote = remotes[choice_idx - 1] - confirm = input(f"确认删除远程仓库 '{remote}'? [y/N]: ").strip().lower() - if confirm == 'y': - success, _ = self.run_command(f"git remote remove {remote}", show_output=False) - if success: - self.print_success(f"远程仓库 '{remote}' 已删除") - else: - self.print_error("删除失败") - else: - self.print_error("无效的选择") - except ValueError: - self.print_error("无效的输入") - - def run(self): - """运行主程序""" - self.print_header("萌芽一键Git管理工具") - print(f"{Colors.CYAN}当前目录:{Colors.ENDC} {self.current_dir}") - print(f"{Colors.CYAN}Git仓库:{Colors.ENDC} {'是' if self.is_git_repo() else '否'}") - - while True: - print(f"\n{Colors.BOLD}请选择操作:{Colors.ENDC}") - print("1. 初始化Git仓库") - print("2. 提交并推送更改") - print("3. 从远程仓库拉取") - print("4. 查看仓库状态") - print("5. 管理远程仓库") - print("6. 退出") - - choice = input(f"\n{Colors.BOLD}请输入选项 [1-6]: {Colors.ENDC}").strip() - - if choice == '1': - self.init_git_repo() - elif choice == '2': - self.commit_and_push() - elif choice == '3': - self.pull_from_remote() - elif choice == '4': - self.show_status() - elif choice == '5': - self.manage_remotes() - elif choice == '6': - self.print_success("感谢使用萌芽Git管理工具!") - break - else: - self.print_error("无效的选择,请重新输入") - - -def main(): - """主函数""" - try: - manager = GitManager() - manager.run() - except KeyboardInterrupt: - print(f"\n\n{Colors.WARNING}程序被用户中断{Colors.ENDC}") - sys.exit(0) - except Exception as e: - print(f"\n{Colors.FAIL}发生错误: {str(e)}{Colors.ENDC}") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/quickgit.py b/quickgit.py new file mode 100644 index 0000000..3107b33 --- /dev/null +++ b/quickgit.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +QuickGit - 萌芽一键Git管理工具 +主程序入口 +""" + +import sys +from quickgit.ui import GitManagerUI + + +def main(): + """主函数""" + try: + ui = GitManagerUI() + ui.run() + except KeyboardInterrupt: + from quickgit.utils import Colors + print(f"\n\n{Colors.WARNING}程序被用户中断{Colors.ENDC}") + sys.exit(0) + except Exception as e: + from quickgit.utils import Colors + print(f"\n{Colors.FAIL}发生错误: {str(e)}{Colors.ENDC}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/quickgit/__init__.py b/quickgit/__init__.py new file mode 100644 index 0000000..05c26f8 --- /dev/null +++ b/quickgit/__init__.py @@ -0,0 +1,6 @@ +""" +QuickGit - 萌芽一键Git管理工具 +""" + +__version__ = "1.0.0" +__author__ = "shumengya" diff --git a/quickgit/config.py b/quickgit/config.py new file mode 100644 index 0000000..a9e5605 --- /dev/null +++ b/quickgit/config.py @@ -0,0 +1,84 @@ +""" +配置模块 - 存储项目配置信息 +""" + + +class Config: + """配置类""" + + # Gitea服务器配置 + GITEA_HOST = "repo.shumengya.top" + GITEA_PORT = "8022" + + # GitHub配置 + GITHUB_USER = "shumengya" + + # Git配置 + DEFAULT_BRANCH = "main" + + # .gitignore模板 + GITIGNORE_TEMPLATE = """# Node/React +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +build/ +dist/ +coverage/ +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Go +*.exe +*.exe~ +*.test +*.out +*.dll +*.so +*.dylib +vendor/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +*.egg-info/ +dist/ +build/ + +# 数据文件 +data/data.json +*.db +*.sqlite + +# 日志文件 +*.log +logs/ + +# 操作系统 +.DS_Store +Thumbs.db +desktop.ini + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.classpath +.settings/ + +# 其他 +*.bak +*.tmp +*.temp +""" diff --git a/quickgit/git_operations.py b/quickgit/git_operations.py new file mode 100644 index 0000000..77b58ad --- /dev/null +++ b/quickgit/git_operations.py @@ -0,0 +1,207 @@ +""" +Git操作模块 - 提供Git基本操作功能 +""" + +import os +from datetime import datetime +from .utils import CommandExecutor, OutputFormatter +from .config import Config + + +class GitOperations: + """Git操作类""" + + def __init__(self): + self.executor = CommandExecutor() + self.current_dir = os.getcwd() + + def is_git_repo(self) -> bool: + """检查当前目录是否是Git仓库""" + return os.path.isdir('.git') + + def init_repo(self) -> bool: + """ + 初始化Git仓库 + + Returns: + 是否成功 + """ + if self.is_git_repo(): + OutputFormatter.warning("当前目录已经是Git仓库") + return True + + # 初始化Git仓库 + OutputFormatter.info("正在初始化Git仓库...") + success, _ = self.executor.run("git init", show_output=False) + if not success: + OutputFormatter.error("Git初始化失败") + return False + OutputFormatter.success("Git仓库初始化成功") + + # 创建main分支 + OutputFormatter.info("正在创建main分支...") + success, _ = self.executor.run(f"git checkout -b {Config.DEFAULT_BRANCH}", show_output=False) + if success: + OutputFormatter.success("main分支创建成功") + else: + OutputFormatter.warning("main分支创建失败,将使用默认分支") + + # 创建.gitignore文件 + self._create_gitignore() + + # 首次提交 + OutputFormatter.info("正在进行首次提交...") + self.executor.run("git add .", show_output=False) + success, _ = self.executor.run('git commit -m "first commit"', show_output=False) + if success: + OutputFormatter.success("首次提交完成") + else: + OutputFormatter.warning("首次提交失败(可能没有文件可提交)") + + return True + + def _create_gitignore(self): + """创建.gitignore文件""" + OutputFormatter.info("正在创建.gitignore文件...") + try: + with open('.gitignore', 'w', encoding='utf-8') as f: + f.write(Config.GITIGNORE_TEMPLATE) + OutputFormatter.success(".gitignore文件创建成功") + except Exception as e: + OutputFormatter.error(f".gitignore文件创建失败: {str(e)}") + + def get_status(self) -> tuple[bool, str]: + """ + 获取Git状态 + + Returns: + (是否成功, 状态输出) + """ + return self.executor.run("git status --short", show_output=False) + + def has_changes(self) -> bool: + """ + 检查是否有更改 + + Returns: + 是否有更改 + """ + success, output = self.get_status() + return success and bool(output.strip()) + + def add_all(self) -> bool: + """ + 添加所有更改 + + Returns: + 是否成功 + """ + OutputFormatter.info("正在添加文件...") + success, _ = self.executor.run("git add .", show_output=False) + if success: + OutputFormatter.success("文件添加成功") + else: + OutputFormatter.error("添加文件失败") + return success + + def commit(self, message: str = None) -> bool: + """ + 提交更改 + + Args: + message: 提交信息 + + Returns: + 是否成功 + """ + if not message: + message = f"update: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + OutputFormatter.info("正在提交更改...") + success, _ = self.executor.run(f'git commit -m "{message}"', show_output=True) + if success: + OutputFormatter.success("提交成功") + else: + OutputFormatter.error("提交失败") + return success + + def get_current_branch(self) -> str: + """ + 获取当前分支 + + Returns: + 分支名 + """ + success, branch = self.executor.run("git branch --show-current", show_output=False) + if success and branch.strip(): + return branch.strip() + return Config.DEFAULT_BRANCH + + def push(self, remote: str, branch: str = None) -> bool: + """ + 推送到远程仓库 + + Args: + remote: 远程仓库名 + branch: 分支名(默认为当前分支) + + Returns: + 是否成功 + """ + if not branch: + branch = self.get_current_branch() + + OutputFormatter.info(f"正在推送到 {remote}...") + + # 先尝试直接推送 + success, _ = self.executor.run(f"git push {remote} {branch}", show_output=True) + + if not success: + # 如果失败,尝试设置上游分支 + OutputFormatter.info("尝试设置上游分支...") + success, _ = self.executor.run(f"git push -u {remote} {branch}", show_output=True) + + if success: + OutputFormatter.success(f"成功推送到 {remote}") + else: + OutputFormatter.error(f"推送到 {remote} 失败") + + return success + + def pull(self, remote: str, branch: str = None) -> bool: + """ + 从远程仓库拉取 + + Args: + remote: 远程仓库名 + branch: 分支名(默认为当前分支) + + Returns: + 是否成功 + """ + if not branch: + branch = self.get_current_branch() + + OutputFormatter.info(f"正在从 {remote} 拉取 {branch} 分支...") + success, _ = self.executor.run(f"git pull {remote} {branch}", show_output=True) + + if success: + OutputFormatter.success(f"成功从 {remote} 拉取更新") + else: + OutputFormatter.error(f"从 {remote} 拉取失败") + + return success + + def show_status(self): + """显示仓库状态""" + from .utils import Colors + + print(f"{Colors.CYAN}当前目录:{Colors.ENDC} {self.current_dir}\n") + + # Git状态 + OutputFormatter.info("Git状态:") + self.executor.run("git status", show_output=True) + + # 最近提交 + print(f"\n{Colors.CYAN}最近3次提交:{Colors.ENDC}") + self.executor.run("git log --oneline -3", show_output=True) diff --git a/quickgit/remote_manager.py b/quickgit/remote_manager.py new file mode 100644 index 0000000..326b841 --- /dev/null +++ b/quickgit/remote_manager.py @@ -0,0 +1,210 @@ +""" +远程仓库管理模块 - 处理GitHub和Gitea远程仓库的管理 +""" + +from .utils import CommandExecutor, OutputFormatter, InputValidator +from .config import Config + + +class RemoteManager: + """远程仓库管理器""" + + def __init__(self): + self.executor = CommandExecutor() + + def get_remotes(self) -> list[str]: + """ + 获取所有远程仓库 + + Returns: + 远程仓库列表 + """ + success, output = self.executor.run("git remote", show_output=False) + if success and output.strip(): + return [r.strip() for r in output.strip().split('\n') if r.strip()] + return [] + + def show_remotes(self): + """显示所有远程仓库""" + from .utils import Colors + print(f"\n{Colors.CYAN}远程仓库:{Colors.ENDC}") + self.executor.run("git remote -v", show_output=True) + + def add_github_remote(self, repo_name: str = None) -> bool: + """ + 添加GitHub远程仓库 + + Args: + repo_name: 仓库名(如果为None则提示用户输入) + + Returns: + 是否成功 + """ + if not repo_name: + repo_name = InputValidator.get_input("\n请输入GitHub仓库名: ") + + remote_url = f"git@github.com:{Config.GITHUB_USER}/{repo_name}.git" + + return self._add_remote("github", remote_url) + + def add_gitea_remote(self, user: str = None, repo_name: str = None) -> bool: + """ + 添加Gitea远程仓库 + + Args: + user: Gitea用户名(如果为None则提示用户输入) + repo_name: 仓库名(如果为None则提示用户输入) + + Returns: + 是否成功 + """ + if not user: + user = InputValidator.get_input("\n请输入Gitea用户名: ") + + if not repo_name: + repo_name = InputValidator.get_input("请输入Gitea仓库名: ") + + remote_url = f"ssh://git@{Config.GITEA_HOST}:{Config.GITEA_PORT}/{user}/{repo_name}.git" + + return self._add_remote("gitea", remote_url) + + def _add_remote(self, name: str, url: str) -> bool: + """ + 添加远程仓库 + + Args: + name: 远程仓库名称 + url: 远程仓库URL + + Returns: + 是否成功 + """ + # 检查remote是否已存在 + remotes = self.get_remotes() + if name in remotes: + if InputValidator.confirm(f"远程仓库 '{name}' 已存在,是否覆盖?", default=False): + self.executor.run(f"git remote remove {name}", show_output=False) + else: + return False + + success, _ = self.executor.run(f'git remote add {name} {url}', show_output=False) + if success: + OutputFormatter.success(f"{name.capitalize()}远程仓库已添加: {url}") + else: + OutputFormatter.error(f"{name.capitalize()}远程仓库添加失败") + + return success + + def remove_remote(self, name: str = None) -> bool: + """ + 删除远程仓库 + + Args: + name: 远程仓库名(如果为None则提示用户选择) + + Returns: + 是否成功 + """ + remotes = self.get_remotes() + + if not remotes: + OutputFormatter.warning("没有配置远程仓库") + return False + + if not name: + # 让用户选择要删除的远程仓库 + print("\n当前远程仓库:") + for idx, remote in enumerate(remotes, 1): + print(f"{idx}. {remote}") + + choice = InputValidator.get_choice( + f"\n请选择要删除的远程仓库 [1-{len(remotes)}]: ", + range(1, len(remotes) + 1) + ) + + name = remotes[choice - 1] + + if not InputValidator.confirm(f"确认删除远程仓库 '{name}'?", default=False): + return False + + success, _ = self.executor.run(f"git remote remove {name}", show_output=False) + if success: + OutputFormatter.success(f"远程仓库 '{name}' 已删除") + else: + OutputFormatter.error("删除失败") + + return success + + def configure_remotes_interactive(self): + """交互式配置远程仓库""" + OutputFormatter.info("\n配置远程仓库...") + + print("\n请选择要配置的远程仓库:") + print("1. GitHub") + print("2. Gitea") + print("3. 两者都配置") + print("4. 跳过") + + choice = InputValidator.get_choice("\n请选择 [1-4]: ", range(1, 5)) + + if choice == 1: + self.add_github_remote() + elif choice == 2: + self.add_gitea_remote() + elif choice == 3: + self.add_github_remote() + self.add_gitea_remote() + else: + OutputFormatter.info("跳过远程仓库配置") + + def select_remote_for_push(self) -> list[str]: + """ + 选择要推送的远程仓库 + + Returns: + 选中的远程仓库列表 + """ + remotes = self.get_remotes() + + if not remotes: + OutputFormatter.warning("没有配置远程仓库") + return [] + + print("\n可用的远程仓库:") + for idx, remote in enumerate(remotes, 1): + print(f"{idx}. {remote}") + print(f"{len(remotes) + 1}. 全部推送") + + choice = InputValidator.get_choice( + f"\n请选择要推送的远程仓库 [1-{len(remotes) + 1}]: ", + range(1, len(remotes) + 2) + ) + + if choice == len(remotes) + 1: + return remotes + else: + return [remotes[choice - 1]] + + def select_remote_for_pull(self) -> str: + """ + 选择要拉取的远程仓库 + + Returns: + 选中的远程仓库名 + """ + remotes = self.get_remotes() + + if not remotes: + OutputFormatter.warning("没有配置远程仓库") + return None + + print("\n可用的远程仓库:") + for idx, remote in enumerate(remotes, 1): + print(f"{idx}. {remote}") + + choice = InputValidator.get_choice( + f"\n请选择要拉取的远程仓库 [1-{len(remotes)}]: ", + range(1, len(remotes) + 1) + ) + + return remotes[choice - 1] diff --git a/quickgit/ui.py b/quickgit/ui.py new file mode 100644 index 0000000..8d10328 --- /dev/null +++ b/quickgit/ui.py @@ -0,0 +1,158 @@ +""" +UI交互模块 - 处理用户界面和交互逻辑 +""" + +from .git_operations import GitOperations +from .remote_manager import RemoteManager +from .utils import OutputFormatter, InputValidator, Colors + + +class GitManagerUI: + """Git管理器UI""" + + def __init__(self): + self.git_ops = GitOperations() + self.remote_mgr = RemoteManager() + + def show_welcome(self): + """显示欢迎信息""" + OutputFormatter.header("萌芽一键Git管理工具 - QuickGit") + print(f"{Colors.CYAN}当前目录:{Colors.ENDC} {self.git_ops.current_dir}") + print(f"{Colors.CYAN}Git仓库:{Colors.ENDC} {'是' if self.git_ops.is_git_repo() else '否'}") + + def show_main_menu(self): + """显示主菜单""" + print(f"\n{Colors.BOLD}请选择操作:{Colors.ENDC}") + print("1. 初始化Git仓库") + print("2. 提交并推送更改") + print("3. 从远程仓库拉取") + print("4. 查看仓库状态") + print("5. 管理远程仓库") + print("6. 退出") + + def handle_init_repo(self): + """处理初始化仓库""" + OutputFormatter.header("初始化Git仓库") + + if self.git_ops.init_repo(): + # 配置远程仓库 + self.remote_mgr.configure_remotes_interactive() + + def handle_commit_and_push(self): + """处理提交并推送""" + OutputFormatter.header("提交并推送更改") + + if not self.git_ops.is_git_repo(): + OutputFormatter.error("当前目录不是Git仓库,请先初始化") + return + + # 检查是否有更改 + OutputFormatter.info("检查文件更改...") + + if not self.git_ops.has_changes(): + OutputFormatter.warning("没有文件更改,无需提交") + return + + # 显示更改 + print("\n当前更改的文件:") + _, status = self.git_ops.get_status() + print(status) + + # 输入提交信息 + commit_msg = InputValidator.get_input( + "\n请输入提交信息 (直接回车使用默认信息): ", + allow_empty=True + ) + + # 添加并提交 + if not self.git_ops.add_all(): + return + + if not self.git_ops.commit(commit_msg if commit_msg else None): + return + + # 推送到远程仓库 + selected_remotes = self.remote_mgr.select_remote_for_push() + + for remote in selected_remotes: + self.git_ops.push(remote) + + def handle_pull(self): + """处理拉取""" + OutputFormatter.header("从远程仓库拉取") + + if not self.git_ops.is_git_repo(): + OutputFormatter.error("当前目录不是Git仓库") + return + + remote = self.remote_mgr.select_remote_for_pull() + if remote: + self.git_ops.pull(remote) + + def handle_show_status(self): + """处理显示状态""" + OutputFormatter.header("仓库状态") + + if not self.git_ops.is_git_repo(): + OutputFormatter.error("当前目录不是Git仓库") + return + + self.git_ops.show_status() + + # 显示远程仓库 + self.remote_mgr.show_remotes() + + def handle_manage_remotes(self): + """处理远程仓库管理""" + OutputFormatter.header("管理远程仓库") + + if not self.git_ops.is_git_repo(): + OutputFormatter.error("当前目录不是Git仓库") + return + + while True: + print("\n远程仓库管理:") + print("1. 查看远程仓库") + print("2. 添加GitHub远程仓库") + print("3. 添加Gitea远程仓库") + print("4. 删除远程仓库") + print("5. 返回主菜单") + + choice = InputValidator.get_choice("\n请选择 [1-5]: ", range(1, 6)) + + if choice == 1: + self.remote_mgr.show_remotes() + elif choice == 2: + self.remote_mgr.add_github_remote() + elif choice == 3: + self.remote_mgr.add_gitea_remote() + elif choice == 4: + self.remote_mgr.remove_remote() + elif choice == 5: + break + + def run(self): + """运行主程序""" + self.show_welcome() + + while True: + self.show_main_menu() + + choice = InputValidator.get_choice( + f"\n{Colors.BOLD}请输入选项 [1-6]: {Colors.ENDC}", + range(1, 7) + ) + + if choice == 1: + self.handle_init_repo() + elif choice == 2: + self.handle_commit_and_push() + elif choice == 3: + self.handle_pull() + elif choice == 4: + self.handle_show_status() + elif choice == 5: + self.handle_manage_remotes() + elif choice == 6: + OutputFormatter.success("感谢使用萌芽Git管理工具!") + break diff --git a/quickgit/utils.py b/quickgit/utils.py new file mode 100644 index 0000000..77ff090 --- /dev/null +++ b/quickgit/utils.py @@ -0,0 +1,150 @@ +""" +工具类模块 - 提供命令执行、输出格式化等工具函数 +""" + +import subprocess + + +class Colors: + """控制台颜色""" + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + + +class CommandExecutor: + """命令执行器""" + + @staticmethod + def run(command: str, show_output: bool = True) -> tuple[bool, str]: + """ + 执行命令 + + Args: + command: 要执行的命令 + show_output: 是否显示输出 + + Returns: + (是否成功, 输出内容) + """ + try: + result = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + encoding='utf-8', + errors='ignore' + ) + + output = result.stdout + result.stderr + + if show_output and output.strip(): + print(output) + + return result.returncode == 0, output + except Exception as e: + OutputFormatter.error(f"命令执行失败: {str(e)}") + return False, str(e) + + +class OutputFormatter: + """输出格式化器""" + + @staticmethod + def header(text: str): + """打印标题""" + print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*50}") + print(f" {text}") + print(f"{'='*50}{Colors.ENDC}\n") + + @staticmethod + def success(text: str): + """打印成功信息""" + print(f"{Colors.GREEN}✓ {text}{Colors.ENDC}") + + @staticmethod + def error(text: str): + """打印错误信息""" + print(f"{Colors.FAIL}✗ {text}{Colors.ENDC}") + + @staticmethod + def info(text: str): + """打印提示信息""" + print(f"{Colors.CYAN}ℹ {text}{Colors.ENDC}") + + @staticmethod + def warning(text: str): + """打印警告信息""" + print(f"{Colors.WARNING}⚠ {text}{Colors.ENDC}") + + +class InputValidator: + """输入验证器""" + + @staticmethod + def get_choice(prompt: str, valid_range: range) -> int: + """ + 获取用户选择(数字) + + Args: + prompt: 提示信息 + valid_range: 有效范围 + + Returns: + 用户选择的数字 + """ + while True: + try: + choice = input(prompt).strip() + choice_num = int(choice) + if choice_num in valid_range: + return choice_num + else: + OutputFormatter.error(f"请输入 {valid_range.start}-{valid_range.stop-1} 之间的数字") + except ValueError: + OutputFormatter.error("请输入有效的数字") + + @staticmethod + def get_input(prompt: str, allow_empty: bool = False) -> str: + """ + 获取用户输入 + + Args: + prompt: 提示信息 + allow_empty: 是否允许空输入 + + Returns: + 用户输入的字符串 + """ + while True: + user_input = input(prompt).strip() + if user_input or allow_empty: + return user_input + else: + OutputFormatter.error("输入不能为空") + + @staticmethod + def confirm(prompt: str, default: bool = False) -> bool: + """ + 获取用户确认 + + Args: + prompt: 提示信息 + default: 默认值 + + Returns: + 用户确认结果 + """ + suffix = " [Y/n]: " if default else " [y/N]: " + user_input = input(prompt + suffix).strip().lower() + + if not user_input: + return default + + return user_input in ['y', 'yes']