From 6eb14af6e16b5b440bbe9dfd9910eaf0e7af94aa Mon Sep 17 00:00:00 2001 From: daiqingshuang Date: Thu, 10 Apr 2025 18:29:49 +0800 Subject: [PATCH] init --- .gitignore | 17 + CMakeLists.txt | 46 +++ cmake/project_cpp_standard.cmake | 54 +++ cmake/retrieve_files.cmake | 317 ++++++++++++++++++ src/branch_selector.cpp | 99 ++++++ src/branch_selector.h | 26 ++ src/colored_log_ctrl.cpp | 39 +++ src/colored_log_ctrl.h | 13 + src/config_manager.cpp | 68 ++++ src/config_manager.h | 34 ++ src/git_repository.cpp | 552 +++++++++++++++++++++++++++++++ src/git_repository.h | 81 +++++ src/main.cpp | 73 ++++ src/main_frame.cpp | 158 +++++++++ src/main_frame.h | 52 +++ third_party/wxWidgets | 1 + 16 files changed, 1630 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 cmake/project_cpp_standard.cmake create mode 100644 cmake/retrieve_files.cmake create mode 100644 src/branch_selector.cpp create mode 100644 src/branch_selector.h create mode 100644 src/colored_log_ctrl.cpp create mode 100644 src/colored_log_ctrl.h create mode 100644 src/config_manager.cpp create mode 100644 src/config_manager.h create mode 100644 src/git_repository.cpp create mode 100644 src/git_repository.h create mode 100644 src/main.cpp create mode 100644 src/main_frame.cpp create mode 100644 src/main_frame.h create mode 160000 third_party/wxWidgets diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5abd834 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/cmake-build-debug +/cmake-build-release +/.idea +/scripts/shader_paths.txt +/cache/shader_loader.h +#>fips +# this area is managed by fips, do not edit +.fips-* +fips-files/build/ +fips-files/deploy/ +*.pyc +.vscode/ +.idea/ +CMakeUserPresets.json +# 配合一个已知的可执行文件名,但这会使函数复杂化。 + # 最好的做法是要求用户设置 CMAKE_RUNTIME_OUTPUT_DIRECTORY_ + # 或者我们直接报错,强制用户设置 CMAKE_RUNTIME_OUTPUT_DIRECTORY + message(FATAL_ERROR "**add_resource_file**: **无法确定可执行文件输出目录**。请在您的项目中设置 **CMAKE_RUNTIME_OUTPUT_DIRECTORY** 变量 (例如 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY \"\${CMAKE_BINARY_DIR}/bin\"))。对于多配置生成器,可能需要设置 CMAKE_RUNTIME_OUTPUT_DIRECTORY_ 变量。") + else() + # 对于单配置生成器(如 Makefiles, Ninja),可以默认到 CMAKE_BINARY_DIR + set(DESTINATION_BASE "${CMAKE_BINARY_DIR}") + message(WARNING "**add_resource_file**: **未设置 CMAKE_RUNTIME_OUTPUT_DIRECTORY**。默认将资源复制到 CMAKE_BINARY_DIR ('${CMAKE_BINARY_DIR}')。强烈建议设置 CMAKE_RUNTIME_OUTPUT_DIRECTORY 以获得可预测的行为。") + endif() + # message(FATAL_ERROR "**add_resource_file**: **无法确定可执行文件输出目录**。请在您的项目中设置 **CMAKE_RUNTIME_OUTPUT_DIRECTORY** 变量 (例如 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY \"\${CMAKE_BINARY_DIR}/bin\"))。") + endif() + + # 处理子目录 + set(DESTINATION_DIR "${DESTINATION_BASE}") # 默认目标目录 + if(ARG_OUTPUT_SUBDIR) + # 清理子目录路径字符串 + string(STRIP "${ARG_OUTPUT_SUBDIR}" _subdir) + if(IS_ABSOLUTE "${_subdir}") + message(FATAL_ERROR "**add_resource_file**: **OUTPUT_SUBDIR** ('${ARG_OUTPUT_SUBDIR}') **必须是相对路径**。") + else() + # 移除可能存在的前导/后导斜杠,以便干净地拼接路径 + string(REGEX REPLACE "^[/\\\\]+" "" _subdir "${_subdir}") + string(REGEX REPLACE "[/\\\\]+$" "" _subdir "${_subdir}") + if(_subdir) # 仅当子目录清理后非空时才追加 + set(DESTINATION_DIR "${DESTINATION_BASE}/${_subdir}") + endif() + endif() + endif() + + # --- 准备源文件路径 --- + set(ABS_RESOURCE_FILES "") + foreach(RESOURCE_FILE ${ARG_RESOURCE_FILES}) + get_filename_component(RESOURCE_FILE_REALPATH "${RESOURCE_FILE}" REALPATH BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + # if(IS_ABSOLUTE "${RESOURCE_FILE}") + # # 如果已经是绝对路径,直接使用 + # list(APPEND ABS_RESOURCE_FILES "${RESOURCE_FILE}") + # else() + # # 如果是相对路径,相对于当前源目录进行解析 + # list(APPEND ABS_RESOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_FILE}") + # endif() + list(APPEND ABS_RESOURCE_FILES "${RESOURCE_FILE_REALPATH}") + + # 检查文件是否存在 (在配置时发出警告,有助于早期发现错误) + # list(GET ABS_RESOURCE_FILES -1 _current_abs_file) # 获取刚才添加的绝对路径 + if(NOT EXISTS "${RESOURCE_FILE_REALPATH}") + message(WARNING "**add_resource_file**: **资源文件** '${RESOURCE_FILE}' (解析为 '${RESOURCE_FILE_REALPATH}') **在配置时不存在**。") + endif() + endforeach() + + # --- 添加自定义命令 --- + # 使用 add_custom_command 在目标构建完成后执行复制操作 + if(ABS_RESOURCE_FILES) # 确保有文件需要复制 + # 注意:DESTINATION_DIR 可能包含特定于配置的路径(例如,如果 CMAKE_RUNTIME_OUTPUT_DIRECTORY + # 设置为 ${CMAKE_BINARY_DIR}/${CMAKE_CFG_INTDIR}/bin)。 + # add_custom_command 的 COMMAND 参数在构建时执行,此时这些变量/生成器表达式已解析。 + add_custom_command( + TARGET ${ARG_TARGET_NAME} + POST_BUILD # 指定在目标构建之后执行 + # 步骤 1: 确保目标目录存在 (copy_if_different 不会创建目录) + COMMAND ${CMAKE_COMMAND} -E make_directory "${DESTINATION_DIR}" + # 步骤 2: 复制文件 + COMMAND ${CMAKE_COMMAND} -E copy_if_different # 使用CMake内置命令复制(仅当文件不同时) + ${ABS_RESOURCE_FILES} # 要复制的源文件列表(绝对路径) + "${DESTINATION_DIR}" # 最终可执行文件所在的目标目录 (带引号以处理空格) + COMMENT "为 ${ARG_TARGET_NAME} 将资源复制到可执行文件目录: ${DESTINATION_DIR}..." # 构建时显示的注释 + VERBATIM # 确保参数(尤其是路径和生成器表达式)被正确处理 + ) + else() + message(WARNING "**add_resource_file**: 没有有效的资源文件提供给目标 '${ARG_TARGET_NAME}'。") + endif() + + # --- 可选: 将资源文件添加到 IDE 项目结构中 --- + if(ABS_RESOURCE_FILES) + set(_source_group_name "Resource Files") # 基础组名 + if(ARG_OUTPUT_SUBDIR) + # 使用与目标目录结构匹配的组名 + string(STRIP "${ARG_OUTPUT_SUBDIR}" _clean_subdir) + string(REPLACE "\\" "/" _clean_subdir "${_clean_subdir}") # 统一使用正斜杠 + string(REGEX REPLACE "^[/]+" "" _clean_subdir "${_clean_subdir}") + string(REGEX REPLACE "[/]+$" "" _clean_subdir "${_clean_subdir}") + if(_clean_subdir) + set(_source_group_name "Resource Files/${_clean_subdir}") + endif() + endif() + # 使用 source_group 将文件添加到 IDE 的指定组下 + source_group(${_source_group_name} FILES ${ABS_RESOURCE_FILES}) + endif() + +endfunction() + diff --git a/src/branch_selector.cpp b/src/branch_selector.cpp new file mode 100644 index 0000000..af00e3e --- /dev/null +++ b/src/branch_selector.cpp @@ -0,0 +1,99 @@ +#include "branch_selector.h" +#include "config_manager.h" + +wxBEGIN_EVENT_TABLE(BranchSelector, wxPanel) + EVT_COMBOBOX(wxID_ANY, BranchSelector::OnBranchSelected) +wxEND_EVENT_TABLE() + +BranchSelector::BranchSelector(wxWindow* parent, std::shared_ptr repo, + const wxString& label) + : wxPanel(parent, wxID_ANY), repo_(repo) { + + wxBoxSizer* hbox = new wxBoxSizer(wxHORIZONTAL); + + // 如果没有提供标签,使用存储库名称 + wxString display_label = label.IsEmpty() ? wxString(repo->GetName()) : label; + wxStaticText* label_ctrl = new wxStaticText(this, wxID_ANY, display_label); + + // 创建下拉列表 + branch_combo_ = new wxComboBox(this, wxID_ANY); + + // 填充分支列表 + const auto& branches = repo->GetAllBranches(); + for (const auto& branch : branches) { + branch_combo_->Append(branch); + } + + // 选择当前分支 + branch_combo_->SetStringSelection(repo->GetCurrentBranchName()); + + // 布局 + hbox->Add(label_ctrl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 5); + hbox->Add(branch_combo_, 1); + + SetSizerAndFit(hbox); +} + +bool BranchSelector::SelectBranch(const wxString& branch_name) { + return branch_combo_->SetStringSelection(branch_name); +} + +bool BranchSelector::SelectGroupBranch(const wxString& group_name) { + current_group_ = group_name; + + // 尝试从配置中获取分支 + bool found = false; + std::string branch_name = ConfigManager::Instance().GetBranchFromGroup( + group_name.ToStdString(), + repo_->GetName(), + "Develop", + &found + ); + + // 如果找到了缓存,使用缓存,否则使用组名 + if (found) { + if (!branch_combo_->SetStringSelection(branch_name)) { + // 如果设置失败,可能是因为分支不存在,尝试使用组名 + return branch_combo_->SetStringSelection(group_name); + } + return true; + } else { + // 尝试使用组名作为分支名 + if (!branch_combo_->SetStringSelection(group_name)) { + // 如果组名设置失败,使用默认分支 + return branch_combo_->SetStringSelection(branch_name); + } + return true; + } +} + +std::string BranchSelector::GetSelectedBranchName() const { + return branch_combo_->GetStringSelection().ToStdString(); +} + +bool BranchSelector::ApplyBranchChange(bool hard_reset) { + std::string selected_branch = GetSelectedBranchName(); + if (selected_branch.empty()) { + return false; + } + + // 切换分支 + bool success = repo_->SwitchBranch(selected_branch, hard_reset); + if (!success) { + return false; + } + + // 拉取更新 + return repo_->Pull(); +} + +void BranchSelector::OnBranchSelected(wxCommandEvent& event) { + // 如果选择的分支不是组名,保存到配置 + if (!current_group_.IsEmpty() && event.GetString() != current_group_) { + ConfigManager::Instance().SaveBranchToGroup( + current_group_.ToStdString(), + repo_->GetName(), + event.GetString().ToStdString() + ); + } +} diff --git a/src/branch_selector.h b/src/branch_selector.h new file mode 100644 index 0000000..738c042 --- /dev/null +++ b/src/branch_selector.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include "git_repository.h" + +class BranchSelector : public wxPanel { +public: + BranchSelector(wxWindow* parent, std::shared_ptr repo, + const wxString& label = wxEmptyString); + + bool SelectBranch(const wxString& branch_name); + bool SelectGroupBranch(const wxString& group_name); + std::string GetSelectedBranchName() const; + bool ApplyBranchChange(bool hard_reset); + + std::shared_ptr GetRepo() const { return repo_; } + +private: + void OnBranchSelected(wxCommandEvent& event); + + wxComboBox* branch_combo_ = nullptr; + std::shared_ptr repo_; + wxString current_group_; + + wxDECLARE_EVENT_TABLE(); +}; diff --git a/src/colored_log_ctrl.cpp b/src/colored_log_ctrl.cpp new file mode 100644 index 0000000..78273a3 --- /dev/null +++ b/src/colored_log_ctrl.cpp @@ -0,0 +1,39 @@ +#include "colored_log_ctrl.h" + +ColoredLogCtrl::ColoredLogCtrl(wxWindow* parent) + : wxTextCtrl(parent, wxID_ANY, wxEmptyString, + wxDefaultPosition, wxDefaultSize, + wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH2) { + SetFont(wxFont(10, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL)); +} + +void ColoredLogCtrl::LogMessage(const wxString& message, const wxColour& color) { + // 获取当前文本长度 + long start = GetLastPosition(); + + // 添加时间戳和消息 + wxString timeStamp = wxDateTime::Now().Format("%Y-%m-%d %H:%M:%S: "); + AppendText(timeStamp + message + "\n"); + + // 设置颜色 + SetStyle(start, GetLastPosition(), wxTextAttr(color)); + + // 滚动到底部 + ShowPosition(GetLastPosition()); +} + +void ColoredLogCtrl::LogError(const wxString& message) { + LogMessage(message, *wxRED); +} + +void ColoredLogCtrl::LogWarning(const wxString& message) { + LogMessage(message, wxColour(255, 140, 0)); // 橙色 +} + +void ColoredLogCtrl::LogInfo(const wxString& message) { + LogMessage(message, *wxBLUE); +} + +void ColoredLogCtrl::LogSuccess(const wxString& message) { + LogMessage(message, wxColour(0, 128, 0)); // 绿色 +} diff --git a/src/colored_log_ctrl.h b/src/colored_log_ctrl.h new file mode 100644 index 0000000..1688323 --- /dev/null +++ b/src/colored_log_ctrl.h @@ -0,0 +1,13 @@ +#pragma once +#include + +class ColoredLogCtrl : public wxTextCtrl { +public: + ColoredLogCtrl(wxWindow* parent); + + void LogMessage(const wxString& message, const wxColour& color = *wxBLACK); + void LogError(const wxString& message); + void LogWarning(const wxString& message); + void LogInfo(const wxString& message); + void LogSuccess(const wxString& message); +}; diff --git a/src/config_manager.cpp b/src/config_manager.cpp new file mode 100644 index 0000000..a1e53df --- /dev/null +++ b/src/config_manager.cpp @@ -0,0 +1,68 @@ +#include "config_manager.h" +#include +#include + +ConfigManager& ConfigManager::Instance() { + static ConfigManager instance; + return instance; +} + +bool ConfigManager::Load(const std::string& file_path) { + // 检查文件是否存在 + std::ifstream file(file_path); + if (!file.good()) { + // 文件不存在或无法访问 + InitializeDefaultConfig(); + return false; + } + file.close(); + + // 加载配置文件 + SI_Error result = ini_.LoadFile(file_path.c_str()); + if (result < 0) { + // 加载失败 + InitializeDefaultConfig(); + return false; + } + + return true; +} + +bool ConfigManager::Save(const std::string& file_path) { + SI_Error result = ini_.SaveFile(file_path.c_str()); + return result >= 0; +} + +void ConfigManager::InitializeDefaultConfig() { + // 设置默认值 + ini_.SetValue("settings", "repo_path", ""); + ini_.SetBoolValue("settings", "hard_reset", false); +} + +std::string ConfigManager::GetRepositoryPath() const { + return ini_.GetValue("settings", "repo_path", ""); +} + +void ConfigManager::SetRepositoryPath(const std::string& path) { + ini_.SetValue("settings", "repo_path", path.c_str()); +} + +bool ConfigManager::GetHardReset() const { + return ini_.GetBoolValue("settings", "hard_reset", false); +} + +void ConfigManager::SetHardReset(bool value) { + ini_.SetBoolValue("settings", "hard_reset", value); +} + +void ConfigManager::SaveBranchToGroup(const std::string& group_name, const std::string& repo_name, const std::string& branch_name) { + ini_.SetValue(group_name.c_str(), repo_name.c_str(), branch_name.c_str()); +} + +std::string ConfigManager::GetBranchFromGroup(const std::string& group_name, const std::string& repo_name, const std::string& default_branch, bool* found) { + const char* value = ini_.GetValue(group_name.c_str(), repo_name.c_str(), default_branch.c_str()); + if (found) { + *found = (value != default_branch.c_str()); // 检查是否使用了默认值 + } + return value; +} diff --git a/src/config_manager.h b/src/config_manager.h new file mode 100644 index 0000000..2eb76ed --- /dev/null +++ b/src/config_manager.h @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include "SimpleIni.h" + +class ConfigManager { +public: + static ConfigManager& Instance(); + + bool Load(const std::string& file_path = "repo.ini"); + bool Save(const std::string& file_path = "repo.ini"); + + std::string GetRepositoryPath() const; + void SetRepositoryPath(const std::string& path); + + bool GetHardReset() const; + void SetHardReset(bool value); + + // 分支组操作 + void SaveBranchToGroup(const std::string& group_name, const std::string& repo_name, const std::string& branch_name); + std::string GetBranchFromGroup(const std::string& group_name, const std::string& repo_name, const std::string& default_branch = "Develop", bool* found = nullptr); + +private: + ConfigManager() = default; + ~ConfigManager() = default; + + void InitializeDefaultConfig(); + + CSimpleIniA ini_; + + // 禁止复制 + ConfigManager(const ConfigManager&) = delete; + ConfigManager& operator=(const ConfigManager&) = delete; +}; diff --git a/src/git_repository.cpp b/src/git_repository.cpp new file mode 100644 index 0000000..8301b7d --- /dev/null +++ b/src/git_repository.cpp @@ -0,0 +1,552 @@ +#include "git_repository.h" +#include +#include + +/** + * @brief 检查 libgit2 函数调用的错误代码。 + * + * 如果错误代码指示失败(通常小于 0),则获取最后的 libgit2 错误信息, + * 构造一个包含操作描述和错误详情的字符串,并抛出 std::runtime_error 异常。 + * + * @param error_code libgit2 函数返回的整数错误代码。 + * @param action_description 描述执行失败的操作的字符串(例如,“打开仓库”、“拉取更新”)。 + * @throws std::runtime_error 如果 error_code 表示失败。 + */ +void CheckGitError(int error_code, const std::string& action_description) { + // libgit2 通常在出错时返回负值 + if (error_code < 0) { + // 获取 libgit2 线程本地存储中的最后一个错误信息 + const git_error* last_error = git_error_last(); + + // 构建详细的错误消息 + std::string error_message = action_description + " 失败: "; + + // 检查 last_error 是否有效以及是否有错误消息文本 + if (last_error && last_error->message) { + error_message += last_error->message; + } else { + // 如果 libgit2 没有提供具体的错误信息 + error_message += "未知错误 (libgit2 未提供详细信息)"; + } + + // 可以选择性地附加错误代码以供调试 + error_message += " (错误码: " + std::to_string(error_code) + ")"; + + // 抛出标准运行时异常 + throw std::runtime_error(error_message); + } + // 如果 error_code >= 0,则表示操作成功,函数不执行任何操作 +} + +namespace git { + // --- 假设 RAII 封装类已存在 --- + // GitRemote 示例: + class GitRemote : public GitResource { + public: + explicit GitRemote(git_remote* remote) : remote_(remote) {} + ~GitRemote() override { if (remote_) git_remote_free(remote_); } + operator git_remote*() const { return remote_; } + private: + git_remote* remote_ = nullptr; + }; + + // GitAnnotatedCommit 示例: + class GitAnnotatedCommit : public GitResource { + public: + explicit GitAnnotatedCommit(git_annotated_commit* commit) : commit_(commit) {} + ~GitAnnotatedCommit() override { if (commit_) git_annotated_commit_free(commit_); } + operator git_annotated_commit*() const { return commit_; } + const git_oid* Id() const { return commit_ ? git_annotated_commit_id(commit_) : nullptr; } + private: + git_annotated_commit* commit_ = nullptr; + }; + + // GitIndex 示例 + class GitIndex : public GitResource { + public: + explicit GitIndex(git_index *index) : index_(index) {} + ~GitIndex() override { if (index_) git_index_free(index_); } + operator git_index*() const { return index_; } + bool HasConflicts() const { return index_ && git_index_has_conflicts(index_); } + int Write() { return index_ ? git_index_write(index_) : -1; } + int ReadTree(const git_tree* tree) { return index_ ? git_index_read_tree(index_, tree) : -1; } + int WriteTree(git_oid* out_oid) { return index_ ? git_index_write_tree(out_oid, index_) : -1; } + private: + git_index* index_ = nullptr; + }; + + // GitCommit 示例 + class GitCommit : public GitResource { + public: + explicit GitCommit(git_commit *commit) : commit_(commit) {} + ~GitCommit() override { if (commit_) git_commit_free(commit_); } + operator git_commit*() const { return commit_; } + const git_oid* Id() const { return commit_ ? git_commit_id(commit_) : nullptr; } + const git_signature* Committer() const { return commit_ ? git_commit_committer(commit_) : nullptr; } + const git_signature* Author() const { return commit_ ? git_commit_author(commit_) : nullptr; } + const char* Message() const { return commit_ ? git_commit_message(commit_) : ""; } + git_tree* Tree() const { + git_tree* tree = nullptr; + if (commit_ && git_commit_tree(&tree, commit_) == 0) { + return tree; // 调用者必须管理此 tree 的生命周期或将其包装 + } + return nullptr; + } + private: + git_commit* commit_ = nullptr; + }; + + // GitTree 示例 + class GitTree : public GitResource { + public: + explicit GitTree(git_tree *tree) : tree_(tree) {} + ~GitTree() override { if (tree_) git_tree_free(tree_); } + operator git_tree*() const { return tree_; } + const git_oid* Id() const { return tree_ ? git_tree_id(tree_) : nullptr; } + private: + git_tree* tree_ = nullptr; + }; + + // GitSignature 示例 + class GitSignature : public GitResource { + public: + explicit GitSignature(git_signature *sig) : sig_(sig) {} + ~GitSignature() override { if (sig_) git_signature_free(sig_); } + operator git_signature*() const { return sig_; } + private: + git_signature* sig_ = nullptr; + }; + // --- RAII 封装类结束 --- + + // GitReference 实现 + std::string GitReference::GetShortName() const { + if (!ref_) return ""; + return git_reference_shorthand(ref_); + } + + // Repository 实现 + Repository::Repository(git_repository* repo) : repo_(repo) { + if (repo_) { + path_ = git_repository_path(repo_); + + // 默认仓库名为目录名 + size_t last_slash = path_.find_last_of("/\\"); + if (last_slash != std::string::npos) { + name_ = path_.substr(last_slash + 1); + // 如果路径以 .git 结尾,去掉它 + size_t git_suffix = name_.find(".git"); + if (git_suffix != std::string::npos && git_suffix == name_.size() - 4) { + name_ = name_.substr(0, git_suffix); + } + } else { + name_ = path_; + } + } + } + + Repository::~Repository() { + if (repo_) { + git_repository_free(repo_); + repo_ = nullptr; + } + } + + std::string Repository::GetCurrentBranchName() const { + if (!repo_) return ""; + + git_reference* head = nullptr; + int error = git_repository_head(&head, repo_); + if (error != 0) { + const git_error* e = git_error_last(); + std::cerr << "Error getting HEAD: " << (e ? e->message : "unknown error") << std::endl; + return ""; + } + + GitReference ref(head); + return ref.GetShortName(); + } + + void ForEachBranch(git_repository* repo, git_branch_t flags, + const std::function& callback) { + git_branch_iterator* iter = nullptr; + if (git_branch_iterator_new(&iter, repo, flags) != 0) { + return; + } + + git_reference* ref = nullptr; + git_branch_t type; + while (git_branch_next(&ref, &type, iter) == 0) { + GitReference git_ref(ref); + callback(git_ref); + } + + git_branch_iterator_free(iter); + } + + std::vector Repository::GetAllBranches() const { + std::vector branches; + if (!repo_) return branches; + + ForEachBranch(repo_, GIT_BRANCH_ALL, [&branches](const GitReference& ref) { + const char* name; + if (git_branch_name(&name, ref) == 0) { + branches.push_back(name); + } + }); + + return branches; + } + + bool Repository::SwitchBranch(const std::string& branch_name, bool hard_reset) { + if (!repo_) return false; + + try { + // 尝试查找本地分支 + git_reference* target_ref = nullptr; + int error = git_branch_lookup(&target_ref, repo_, branch_name.c_str(), GIT_BRANCH_LOCAL); + if (error != 0) { + // 尝试查找远程分支 + error = git_branch_lookup(&target_ref, repo_, branch_name.c_str(), GIT_BRANCH_REMOTE); + if (error != 0) { + std::cerr << "Branch not found: " << branch_name << std::endl; + return false; + } + } + + GitReference ref(target_ref); + + // 获取目标commit + git_object* target_commit = nullptr; + error = git_reference_peel(&target_commit, ref, GIT_OBJECT_COMMIT); + if (error != 0) { + std::cerr << "Failed to peel reference to commit" << std::endl; + return false; + } + + // 创建检出选项 + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; + + // 检出分支 + error = git_checkout_tree(repo_, target_commit, &checkout_opts); + if (error != 0) { + git_object_free(target_commit); + std::cerr << "Failed to checkout tree" << std::endl; + return false; + } + + git_object_free(target_commit); + + // 更新HEAD引用 + error = git_repository_set_head(repo_, git_reference_name(ref)); + if (error != 0) { + std::cerr << "Failed to update HEAD reference" << std::endl; + return false; + } + + // 如果需要硬重置 + if (hard_reset) { + git_reference* remote_ref = nullptr; + std::string remote_branch_name = "origin/" + branch_name; + error = git_branch_lookup(&remote_ref, repo_, remote_branch_name.c_str(), GIT_BRANCH_REMOTE); + if (error == 0) { + GitReference remote(remote_ref); + + git_object* remote_commit = nullptr; + error = git_reference_peel(&remote_commit, remote, GIT_OBJECT_COMMIT); + if (error == 0) { + // 重置到远程分支 + error = git_reset(repo_, remote_commit, GIT_RESET_HARD, nullptr); + git_object_free(remote_commit); + + if (error != 0) { + std::cerr << "Failed to hard reset" << std::endl; + return false; + } + } + } + } + + return true; + } + catch (const std::exception& e) { + std::cerr << "Exception during branch switch: " << e.what() << std::endl; + return false; + } + } + + bool Repository::Pull() { + if (!repo_) { + std::cerr << "Pull 错误:仓库未打开。" << std::endl; + return false; + } + + // 初始化原始指针,稍后由 RAII 接管 + git_reference* head_ref_ptr = nullptr; + git_annotated_commit* remote_head_annotated_ptr = nullptr; + git_remote* remote_ptr = nullptr; + + try { + // 1. 获取当前的 HEAD 引用 + CheckGitError(git_repository_head(&head_ref_ptr, repo_), "获取 HEAD 引用"); + GitReference head_ref(head_ref_ptr); // RAII 封装 + + if (!git_reference_is_branch(head_ref)) { + std::cerr << "Pull 警告:HEAD 处于分离状态。无法执行 pull。" << std::endl; + // 这不完全是错误,但 pull 在此无意义 + return true; // 或根据期望行为返回 false + } + std::cout << "当前分支: " << git_reference_shorthand(head_ref) << std::endl; + + // 2. 获取当前 HEAD 的上游分支引用 + git_reference* upstream_ref_ptr = nullptr; + int upstream_error = git_branch_upstream(&upstream_ref_ptr, head_ref); + if (upstream_error == GIT_ENOTFOUND) { + std::cerr << "Pull 错误:分支 '" << git_reference_shorthand(head_ref) + << "' 未配置上游分支。" << std::endl; + return false; + } + CheckGitError(upstream_error, "获取上游分支"); + GitReference upstream_ref(upstream_ref_ptr); // RAII 封装 + + const char* upstream_name = git_reference_name(upstream_ref); // e.g., "refs/remotes/origin/main" + if (!upstream_name) { + throw std::runtime_error("无法获取上游引用名称。"); + } + std::cout << "上游分支引用: " << upstream_name << std::endl; + + + // 3. 从上游引用中获取远程仓库名称和远程分支名称 + char remote_name_buffer[256]; // 假设远程名称不会太长 + git_buf remote_name_buf = { remote_name_buffer, 0, sizeof(remote_name_buffer), }; + CheckGitError(git_branch_remote_name(&remote_name_buf, repo_, upstream_name), "获取远程仓库名称"); + std::string remote_name_str(remote_name_buffer); // 例如 "origin" + + if (remote_name_str.empty()) { + throw std::runtime_error("无法从上游引用确定远程仓库名称。"); + } + std::cout << "远程仓库名称: " << remote_name_str << std::endl; + + // 提取远程分支名称 (例如从 "refs/remotes/origin/main" 提取 "main") + std::string remote_branch_name_str; + const char* shorthand = git_reference_shorthand(upstream_ref); // 例如 "origin/main" + if (shorthand) { + std::string shorthand_str(shorthand); + size_t slash_pos = shorthand_str.find('/'); + if (slash_pos != std::string::npos) { + remote_branch_name_str = shorthand_str.substr(slash_pos + 1); + } + } + if (remote_branch_name_str.empty()) { + throw std::runtime_error("无法从上游引用确定远程分支名称。"); + } + std::cout << "远程分支名称: " << remote_branch_name_str << std::endl; + + + // 4. 查找远程仓库 + CheckGitError(git_remote_lookup(&remote_ptr, repo_, remote_name_str.c_str()), "查找远程仓库 '" + remote_name_str + "'"); + GitRemote remote(remote_ptr); // RAII 封装 + + // 5. 从远程仓库 Fetch + std::cout << "正在从 " << remote_name_str << " 拉取..." << std::endl; + git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT; + // 您可能需要配置 fetch_opts,例如设置凭据回调 + // fetch_opts.callbacks.credentials = ...; + CheckGitError(git_remote_fetch(remote, nullptr, &fetch_opts, "fetch: Pull 过程中自动 fetch"), "从远程仓库 Fetch"); + std::cout << "Fetch 完成。" << std::endl; + + // 6. 获取 Fetch 后的远程 HEAD 的 Annotated Commit + // upstream_ref 现在可能指向 *新* Fetch 下来的 commit ID + CheckGitError(git_annotated_commit_from_ref(&remote_head_annotated_ptr, repo_, upstream_ref), "获取远程 HEAD 的 Annotated Commit"); + GitAnnotatedCommit remote_head_annotated(remote_head_annotated_ptr); // RAII 封装 + + // 7. 执行合并分析 (Merge Analysis) + git_merge_analysis_t analysis_out; + git_merge_preference_t preference_out; + const git_annotated_commit* merge_heads[] = {remote_head_annotated}; + + CheckGitError(git_merge_analysis(&analysis_out, &preference_out, repo_, merge_heads, 1), "执行合并分析"); + + // 8. 处理不同的合并场景 + if (analysis_out & GIT_MERGE_ANALYSIS_UP_TO_DATE) { + std::cout << "分支 '" << git_reference_shorthand(head_ref) + << "' 已经是最新的。" << std::endl; + return true; + } else { + if (analysis_out & GIT_MERGE_ANALYSIS_FASTFORWARD) { + std::cout << "执行快进 (Fast-forward) 合并..." << std::endl; + + // 从 annotated commit 获取目标 commit OID + const git_oid* target_oid = remote_head_annotated.Id(); + if (!target_oid) throw std::runtime_error("无法从远程 annotated commit 获取 OID。"); + + // 获取本地分支 (HEAD) 的引用对象 + git_reference* local_branch_ref_ptr = nullptr; + CheckGitError(git_reference_lookup(&local_branch_ref_ptr, repo_, git_reference_name(head_ref)), "查找本地分支引用以进行快进合并"); + GitReference local_branch_ref(local_branch_ref_ptr); // RAII + + // 更新本地分支引用,使其直接指向远程 commit + git_reference* new_direct_ref_ptr = nullptr; + CheckGitError(git_reference_set_target(&new_direct_ref_ptr, local_branch_ref, target_oid, "pull: 快进更新。"), "快进合并期间设置本地分支目标"); + // 如果需要管理 new_direct_ref_ptr 的生命周期,也用 RAII 包装 + if (new_direct_ref_ptr) git_reference_free(new_direct_ref_ptr); + + + // Checkout HEAD 以更新工作目录和索引 + git_checkout_options ff_checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + ff_checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE; // 使用 FORCE 确保工作目录匹配 + CheckGitError(git_checkout_head(repo_, &ff_checkout_opts), "快进合并后检出 HEAD"); + + std::cout << "快进合并成功完成。" << std::endl; + return true; + + } + if (analysis_out & GIT_MERGE_ANALYSIS_NORMAL) { + std::cout << "执行普通合并..." << std::endl; + + git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT; + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + // 倾向于创建包含冲突的文件,而不是立即中止 + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_RECREATE_MISSING | GIT_CHECKOUT_ALLOW_CONFLICTS; + + CheckGitError(git_merge(repo_, merge_heads, 1, &merge_opts, &checkout_opts), + "执行合并操作"); + + // 检查是否存在冲突 + git_index* index_ptr = nullptr; + CheckGitError(git_repository_index(&index_ptr, repo_), "合并后获取仓库索引"); + GitIndex index(index_ptr); // RAII 封装 + + if (index.HasConflicts()) { + std::cerr << "Pull 错误:检测到合并冲突。请手动解决冲突并提交。" << std::endl; + // 将仓库留在合并状态供用户解决 + // 如果需要,可以在此处保存冲突细节 + CheckGitError(index.Write(), "写入包含冲突的索引"); // 保存包含冲突标记的索引状态 + return false; // 因冲突而指示失败 + } else { + std::cout << "合并完成,无冲突。正在创建合并提交..." << std::endl; + + // 创建合并提交 (Merge Commit) + git_oid tree_oid; + CheckGitError(index.WriteTree(&tree_oid), "写入合并后的树"); // 将索引更改写入树对象 + + git_tree* tree_ptr = nullptr; + CheckGitError(git_tree_lookup(&tree_ptr, repo_, &tree_oid), "查找合并后的树"); + GitTree merged_tree(tree_ptr); // RAII 封装 + + // 创建签名 (Signature) + git_signature* signature_ptr = nullptr; + // !! 重要:替换为实际的用户名和邮箱,最好从 Git 配置读取 + CheckGitError(git_signature_default(&signature_ptr, repo_), "获取默认签名"); + // 如果 git_signature_default 失败或未配置,需要手动创建或提供默认值 + if(!signature_ptr) { + CheckGitError(git_signature_now(&signature_ptr, "默认用户名", "user@example.com"), "创建签名"); + } + GitSignature signature(signature_ptr); // RAII 封装 + + // 获取本地 HEAD commit 作为父提交 + git_commit* local_head_commit_ptr = nullptr; + git_oid local_head_oid; + CheckGitError(git_reference_name_to_id(&local_head_oid, repo_, git_reference_name(head_ref)), + "获取本地 HEAD OID 用于合并提交"); + CheckGitError(git_commit_lookup(&local_head_commit_ptr, repo_, &local_head_oid), + "查找本地 HEAD commit 用于合并提交"); + GitCommit local_head_commit(local_head_commit_ptr); // RAII + + // 获取远程 commit 作为另一个父提交 + git_commit* remote_commit_ptr = nullptr; + CheckGitError(git_commit_lookup(&remote_commit_ptr, repo_, remote_head_annotated.Id()), + "查找远程 commit 用于合并提交"); + GitCommit remote_commit(remote_commit_ptr); // RAII + + + git_commit* parents[] = {local_head_commit, remote_commit}; + // 生成合并提交消息 + std::string commit_message = "Merge remote-tracking branch '" + remote_name_str + "/" + remote_branch_name_str + "'"; + + // 创建合并提交 + git_oid new_commit_oid; + CheckGitError(git_commit_create(&new_commit_oid, repo_, + git_reference_name(head_ref), // 更新哪个引用 (HEAD) + signature, signature, // 作者和提交者签名 + nullptr, // 编码,nullptr 表示 UTF-8 + commit_message.c_str(), // 提交信息 + merged_tree, // 合并后的树 + 2, parents), // 父提交数量和数组 + "创建合并提交"); + + // 清理仓库状态(例如,移除 MERGE_HEAD 文件) + CheckGitError(git_repository_state_cleanup(repo_), "合并后清理仓库状态"); + + std::cout << "合并提交成功创建。" << std::endl; + return true; + } + } else if (analysis_out & GIT_MERGE_ANALYSIS_UNBORN) { + std::cerr << "Pull 错误:本地分支 '" << git_reference_shorthand(head_ref) + << "' 尚未出生 (unborn)。无法合并。" << std::endl; + return false; + } else { + // 其他未知的分析结果 + std::cerr << "Pull 错误:未知的合并分析结果 (" << analysis_out << ")。" << std::endl; + return false; + } + } + } catch (const std::exception& e) { + std::cerr << "Pull 操作失败: " << e.what() << std::endl; + // 清理在部分失败情况下可能未被 RAII 覆盖的资源 + // (尽管如果 RAII 实现正确,大多数情况应已处理) + return false; + } + // 当 RAII 包装器离开作用域时,它们会自动释放资源(如 head_ref, upstream_ref, remote 等) + // 即使发生异常也是如此。 + } + + int OpenSubmodule(git_submodule* submodule, const char* name, void* payload) { + auto submodules = static_cast>*>(payload); + + git_repository* subrepo = nullptr; + if (git_submodule_open(&subrepo, submodule) == 0) { + auto repo = std::make_shared(subrepo); + repo->SetName(name); + submodules->push_back(repo); + } + + return 0; + } + + std::vector> Repository::GetSubmodules() const { + std::vector> submodules; + if (!repo_) return submodules; + + git_submodule_foreach(repo_, OpenSubmodule, &submodules); + return submodules; + } + + bool InitLibGit() { + return git_libgit2_init() >= 0; + } + + void CleanupLibGit() { + git_libgit2_shutdown(); + } + + bool IsValidRepository(const std::string& path) { + git_repository* repo = nullptr; + int error = git_repository_open(&repo, path.c_str()); + if (error == 0) { + git_repository_free(repo); + return true; + } + return false; + } + + std::shared_ptr OpenRepository(const std::string& path) { + git_repository* repo = nullptr; + int error = git_repository_open(&repo, path.c_str()); + if (error != 0) { + return nullptr; + } + + return std::make_shared(repo); + } +} // namespace git diff --git a/src/git_repository.h b/src/git_repository.h new file mode 100644 index 0000000..b0a7f64 --- /dev/null +++ b/src/git_repository.h @@ -0,0 +1,81 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace git { + + // 为libgit2对象提供RAII包装的类 + class GitResource { + public: + virtual ~GitResource() = default; + }; + + // Git引用的RAII包装 + class GitReference : public GitResource { + public: + explicit GitReference(git_reference* ref) : ref_(ref) {} + ~GitReference() override { if (ref_) git_reference_free(ref_); } + + operator git_reference*() const { return ref_; } + std::string GetShortName() const; + + private: + git_reference* ref_ = nullptr; + }; + + // Git分支操作类 + class Branch { + public: + explicit Branch(const std::string& name) : name_(name) {} + + const std::string& GetName() const { return name_; } + + private: + std::string name_; + }; + + // Git仓库操作类 + class Repository { + public: + explicit Repository(git_repository* repo); + ~Repository(); + + Repository(const Repository&) = delete; + Repository& operator=(const Repository&) = delete; + + // 基本属性 + const std::string& GetName() const { return name_; } + const std::string& GetPath() const { return path_; } + void SetName(const std::string& name) { name_ = name; } + + // 分支操作 + std::string GetCurrentBranchName() const; + std::vector GetAllBranches() const; + bool SwitchBranch(const std::string& branch_name, bool hard_reset = false); + bool Pull(); + + // 子模块操作 + std::vector> GetSubmodules() const; + + // 转换操作符 + operator git_repository*() const { return repo_; } + operator bool() const { return repo_ != nullptr; } + + private: + git_repository* repo_ = nullptr; + std::string name_; + std::string path_; + }; + + // 初始化和清理 + bool InitLibGit(); + void CleanupLibGit(); + + // 辅助函数 + bool IsValidRepository(const std::string& path); + std::shared_ptr OpenRepository(const std::string& path); + +} // namespace git diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..f83072c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,73 @@ +#include +#include "git_repository.h" +#include "config_manager.h" +#include "main_frame.h" + +class GitBranchApp : public wxApp { +public: + bool OnInit() override { + // 初始化Git库 + if (!git::InitLibGit()) { + wxMessageBox("Failed to initialize libgit2", "Error", wxICON_ERROR); + return false; + } + + // 加载配置 + auto& config = ConfigManager::Instance(); + if (!config.Load()) { + // 配置不存在或无效,初始化新配置 + if (!InitializeConfig()) { + return false; + } + } + + // 打开主仓库 + std::string repo_path = config.GetRepositoryPath(); + auto repo = git::OpenRepository(repo_path); + if (!repo) { + wxMessageBox("Failed to open repository: " + repo_path, "Error", wxICON_ERROR); + return false; + } + + // 创建并显示主窗口 + auto* frame = new MainFrame("Git Branch Manager"); + frame->Show(true); + + return true; + } + + int OnExit() override { + // 保存配置 + ConfigManager::Instance().Save(); + + // 清理Git库 + git::CleanupLibGit(); + + return 0; + } + +private: + bool InitializeConfig() { + // 显示目录选择对话框 + wxDirDialog dialog(nullptr, "Select Git Repository", "", + wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); + + if (dialog.ShowModal() != wxID_OK) { + return false; + } + + std::string path = dialog.GetPath().ToStdString(); + if (!git::IsValidRepository(path)) { + wxMessageBox("Please select a valid Git repository", "Error", wxICON_ERROR); + return false; + } + + auto& config = ConfigManager::Instance(); + config.SetRepositoryPath(path); + config.Save(); + + return true; + } +}; + +wxIMPLEMENT_APP(GitBranchApp); diff --git a/src/main_frame.cpp b/src/main_frame.cpp new file mode 100644 index 0000000..0f9c569 --- /dev/null +++ b/src/main_frame.cpp @@ -0,0 +1,158 @@ +#include "main_frame.h" +#include "config_manager.h" + +wxBEGIN_EVENT_TABLE(MainFrame, wxFrame) + EVT_BUTTON(wxID_APPLY, MainFrame::OnApply) + EVT_COMBOBOX(wxID_ANY, MainFrame::OnGroupSelect) + EVT_CHECKBOX(wxID_ANY, MainFrame::OnHardResetChanged) +wxEND_EVENT_TABLE() + +MainFrame::MainFrame(const wxString& title) + : wxFrame(nullptr, wxID_ANY, title, wxDefaultPosition, wxDefaultSize) { + + // 加载主仓库 + std::string repo_path = ConfigManager::Instance().GetRepositoryPath(); + main_repo_ = git::OpenRepository(repo_path); + if (!main_repo_) { + wxMessageBox("Failed to open repository: " + repo_path, "Error", wxICON_ERROR); + Close(true); + return; + } + + // 获取硬重置配置 + is_hard_reset_ = ConfigManager::Instance().GetHardReset(); + + // 初始化UI + InitializeUI(); + + // 默认选择Develop分支组 + if (group_combo_->GetCount() > 0) { + group_combo_->SetStringSelection("Develop"); + wxCommandEvent event(wxEVT_COMBOBOX); + OnGroupSelect(event); + } +} + +void MainFrame::InitializeUI() { + // 创建主布局 + wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); + + // 创建分支组选择器 + PopulateBranchGroups(); + + // 创建主仓库分支选择器 + branch_selectors_.push_back(new BranchSelector(this, main_repo_, "Main Repository")); + + // 创建子模块分支选择器 + auto submodules = main_repo_->GetSubmodules(); + for (const auto& submodule : submodules) { + branch_selectors_.push_back(new BranchSelector(this, submodule)); + } + + // 创建硬重置选项 + hard_reset_checkbox_ = new wxCheckBox(this, wxID_ANY, "Hard reset"); + hard_reset_checkbox_->SetValue(is_hard_reset_); + + // 创建应用按钮 + wxButton* apply_button = new wxButton(this, wxID_APPLY, "Apply"); + + // 创建日志控件 + log_ctrl_ = new ColoredLogCtrl(this); + + // 添加到主布局 + main_sizer->Add(group_combo_, 0, wxEXPAND | wxALL, 5); + + for (auto* selector : branch_selectors_) { + main_sizer->Add(selector, 0, wxEXPAND | wxALL, 5); + } + + main_sizer->Add(hard_reset_checkbox_, 0, wxALL, 5); + main_sizer->Add(apply_button, 0, wxALL, 5); + main_sizer->Add(log_ctrl_, 1, wxEXPAND | wxALL, 5); + + SetSizerAndFit(main_sizer); + SetMinSize(wxSize(600, 400)); +} + +void MainFrame::PopulateBranchGroups() { + // 获取所有分支组 + std::vector branch_groups; + + // 获取主仓库的所有分支 + for (const auto& branch : main_repo_->GetAllBranches()) { + branch_groups.push_back(branch); + } + + // 获取子模块的所有分支 + auto submodules = main_repo_->GetSubmodules(); + for (const auto& submodule : submodules) { + for (const auto& branch : submodule->GetAllBranches()) { + branch_groups.push_back(branch); + } + } + + // 去重 + std::sort(branch_groups.begin(), branch_groups.end()); + branch_groups.erase(std::unique(branch_groups.begin(), branch_groups.end()), branch_groups.end()); + + // 创建分支组选择器 + group_combo_ = new wxComboBox(this, wxID_ANY); + + // 填充分支组 + for (const auto& group : branch_groups) { + group_combo_->Append(group); + } +} + +void MainFrame::OnApply(wxCommandEvent& event) { + log_ctrl_->LogInfo("Applying branch changes..."); + + bool any_failures = false; + + for (auto* selector : branch_selectors_) { + std::string repo_name = selector->GetRepo()->GetName(); + std::string branch_name = selector->GetSelectedBranchName(); + + log_ctrl_->LogInfo(wxString::Format("Switching %s to branch: %s", repo_name, branch_name)); + + if (selector->ApplyBranchChange(is_hard_reset_)) { + log_ctrl_->LogSuccess(wxString::Format("Successfully updated %s to %s", repo_name, branch_name)); + } else { + log_ctrl_->LogError(wxString::Format("Failed to update %s to %s", repo_name, branch_name)); + any_failures = true; + } + } + + if (any_failures) { + log_ctrl_->LogWarning("Some operations failed. See errors above."); + } else { + log_ctrl_->LogSuccess("All operations completed successfully!"); + } + + log_ctrl_->LogInfo("==============完成=============="); +} + +void MainFrame::OnGroupSelect(wxCommandEvent& event) { + wxString selected_group = group_combo_->GetStringSelection(); + if (selected_group.IsEmpty()) return; + + log_ctrl_->LogInfo(wxString::Format("Selecting branch group: %s", selected_group)); + + for (auto* selector : branch_selectors_) { + selector->SelectGroupBranch(selected_group); + } + + // 保存配置 + ConfigManager::Instance().Save(); +} + +void MainFrame::OnHardResetChanged(wxCommandEvent& event) { + is_hard_reset_ = event.IsChecked(); + ConfigManager::Instance().SetHardReset(is_hard_reset_); + + if (is_hard_reset_) { + log_ctrl_->LogWarning("Hard reset mode enabled. This will discard local changes!"); + } else { + log_ctrl_->LogInfo("Hard reset mode disabled."); + } +} diff --git a/src/main_frame.h b/src/main_frame.h new file mode 100644 index 0000000..352da40 --- /dev/null +++ b/src/main_frame.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include +#include +#include "git_repository.h" +#include "colored_log_ctrl.h" + +// 分支选择器组件 +class BranchSelector : public wxPanel { +public: + BranchSelector(wxWindow* parent, std::shared_ptr repo, const wxString& label = wxEmptyString); + + bool SelectBranch(const wxString& branch_name); + bool SelectGroupBranch(const wxString& group_name); + std::string GetSelectedBranchName() const; + bool ApplyBranchChange(bool hard_reset); + + auto GetRepo() const { return repo_; } +private: + void OnBranchSelected(wxCommandEvent& event); + + wxComboBox* branch_combo_ = nullptr; + std::shared_ptr repo_; + wxString current_group_; + + wxDECLARE_EVENT_TABLE(); +}; + +// 主窗口 +class MainFrame : public wxFrame { +public: + explicit MainFrame(const wxString& title); + ~MainFrame() override = default; + +private: + void OnApply(wxCommandEvent& event); + void OnGroupSelect(wxCommandEvent& event); + void OnHardResetChanged(wxCommandEvent& event); + + void InitializeUI(); + void PopulateBranchGroups(); + + std::vector branch_selectors_; + wxComboBox* group_combo_ = nullptr; + wxCheckBox* hard_reset_checkbox_ = nullptr; + ColoredLogCtrl* log_ctrl_ = nullptr; + + std::shared_ptr main_repo_; + bool is_hard_reset_ = false; + + wxDECLARE_EVENT_TABLE(); +}; diff --git a/third_party/wxWidgets b/third_party/wxWidgets new file mode 160000 index 0000000..0aa14d3 --- /dev/null +++ b/third_party/wxWidgets @@ -0,0 +1 @@ +Subproject commit 0aa14d33b5e78e60e8280444a0afe8b7d9355317