From f2b6ac5d188158a174bddcdeb54c2478f5f55305 Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Thu, 31 Jul 2025 08:31:44 -0700 Subject: [PATCH] Refactor git calls into their own namespace --- common/CMakeLists.txt | 5 + common/git/git_branch_handler.cpp | 126 ++++++++ common/git/git_branch_handler.h | 71 +++++ common/git/git_config_handler.cpp | 109 +++++++ common/git/git_config_handler.h | 69 ++++ common/git/git_init_handler.cpp | 151 +++++++++ common/git/git_init_handler.h | 76 +++++ common/git/git_status_handler.cpp | 232 ++++++++++++++ common/git/git_status_handler.h | 91 ++++++ common/git/project_git_utils.cpp | 35 ++- common/git/project_git_utils.h | 15 +- kicad/project_tree_pane.cpp | 503 ++++++------------------------ 12 files changed, 1078 insertions(+), 405 deletions(-) create mode 100644 common/git/git_branch_handler.cpp create mode 100644 common/git/git_branch_handler.h create mode 100644 common/git/git_config_handler.cpp create mode 100644 common/git/git_config_handler.h create mode 100644 common/git/git_init_handler.cpp create mode 100644 common/git/git_init_handler.h create mode 100644 common/git/git_status_handler.cpp create mode 100644 common/git/git_status_handler.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 153afcffc1..f06414488d 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -601,17 +601,22 @@ set( COMMON_IMPORT_GFX_SRCS set( COMMON_GIT_SRCS git/git_add_to_index_handler.cpp + git/git_branch_handler.cpp git/git_clone_handler.cpp git/git_commit_handler.cpp + git/git_config_handler.cpp + git/git_init_handler.cpp git/git_pull_handler.cpp git/git_push_handler.cpp git/git_remove_from_index_handler.cpp git/git_resolve_conflict_handler.cpp git/git_revert_handler.cpp + git/git_status_handler.cpp git/git_sync_handler.cpp git/project_git_utils.cpp git/kicad_git_common.cpp git/kicad_git_errors.cpp + git/project_git_utils.cpp ) set( COMMON_SRCS diff --git a/common/git/git_branch_handler.cpp b/common/git/git_branch_handler.cpp new file mode 100644 index 0000000000..9cb0ab66be --- /dev/null +++ b/common/git/git_branch_handler.cpp @@ -0,0 +1,126 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_branch_handler.h" +#include +#include +#include +#include + +GIT_BRANCH_HANDLER::GIT_BRANCH_HANDLER( KIGIT_COMMON* aCommon ) : KIGIT_REPO_MIXIN( aCommon ) +{} + +GIT_BRANCH_HANDLER::~GIT_BRANCH_HANDLER() +{} + +bool GIT_BRANCH_HANDLER::BranchExists( const wxString& aBranchName ) +{ + git_repository* repo = GetRepo(); + + if( !repo ) + return false; + + git_reference* branchRef = nullptr; + bool exists = LookupBranchReference( aBranchName, &branchRef ); + + if( branchRef ) + git_reference_free( branchRef ); + + return exists; +} + +bool GIT_BRANCH_HANDLER::LookupBranchReference( const wxString& aBranchName, git_reference** aReference ) +{ + git_repository* repo = GetRepo(); + + if( !repo ) + return false; + + // Try direct lookup first + if( git_reference_lookup( aReference, repo, aBranchName.mb_str() ) == GIT_OK ) + return true; + + // Try dwim (Do What I Mean) lookup for short branch names + if( git_reference_dwim( aReference, repo, aBranchName.mb_str() ) == GIT_OK ) + return true; + + return false; +} + +BranchResult GIT_BRANCH_HANDLER::SwitchToBranch( const wxString& aBranchName ) +{ + git_repository* repo = GetRepo(); + + if( !repo ) + { + AddErrorString( _( "No repository available" ) ); + return BranchResult::Error; + } + + // Look up the branch reference + git_reference* branchRef = nullptr; + + if( !LookupBranchReference( aBranchName, &branchRef ) ) + { + AddErrorString( wxString::Format( _( "Failed to lookup branch '%s': %s" ), + aBranchName, KIGIT_COMMON::GetLastGitError() ) ); + return BranchResult::BranchNotFound; + } + + KIGIT::GitReferencePtr branchRefPtr( branchRef ); + const char* branchRefName = git_reference_name( branchRef ); + git_object* branchObj = nullptr; + + if( git_revparse_single( &branchObj, repo, aBranchName.mb_str() ) != GIT_OK ) + { + AddErrorString( wxString::Format( _( "Failed to find branch head for '%s': %s" ), + aBranchName, KIGIT_COMMON::GetLastGitError() ) ); + return BranchResult::Error; + } + + KIGIT::GitObjectPtr branchObjPtr( branchObj ); + + // Switch to the branch + if( git_checkout_tree( repo, branchObj, nullptr ) != GIT_OK ) + { + AddErrorString( wxString::Format( _( "Failed to switch to branch '%s': %s" ), + aBranchName, KIGIT_COMMON::GetLastGitError() ) ); + return BranchResult::CheckoutFailed; + } + + // Update the HEAD reference + if( git_repository_set_head( repo, branchRefName ) != GIT_OK ) + { + AddErrorString( wxString::Format( _( "Failed to update HEAD reference for branch '%s': %s" ), + aBranchName, KIGIT_COMMON::GetLastGitError() ) ); + return BranchResult::Error; + } + + wxLogTrace( traceGit, "Successfully switched to branch '%s'", aBranchName ); + return BranchResult::Success; +} + +void GIT_BRANCH_HANDLER::UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) +{ + ReportProgress( aCurrent, aTotal, aMessage ); +} \ No newline at end of file diff --git a/common/git/git_branch_handler.h b/common/git/git_branch_handler.h new file mode 100644 index 0000000000..7555fd28dd --- /dev/null +++ b/common/git/git_branch_handler.h @@ -0,0 +1,71 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_BRANCH_HANDLER_H +#define GIT_BRANCH_HANDLER_H + +#include +#include +#include + +enum class BranchResult +{ + Success, + BranchNotFound, + CheckoutFailed, + Error +}; + +class GIT_BRANCH_HANDLER : public KIGIT_REPO_MIXIN +{ +public: + GIT_BRANCH_HANDLER( KIGIT_COMMON* aCommon ); + virtual ~GIT_BRANCH_HANDLER(); + + /** + * Switch to the specified branch + * @param aBranchName Name of the branch to switch to + * @return BranchResult indicating the outcome of the operation + */ + BranchResult SwitchToBranch( const wxString& aBranchName ); + + /** + * Check if a branch exists + * @param aBranchName Name of the branch to check + * @return True if branch exists, false otherwise + */ + bool BranchExists( const wxString& aBranchName ); + + void UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) override; + +private: + /** + * Look up a branch reference by name + * @param aBranchName Name of the branch + * @param aReference Output parameter for the reference + * @return True if successful, false otherwise + */ + bool LookupBranchReference( const wxString& aBranchName, git_reference** aReference ); +}; + +#endif // GIT_BRANCH_HANDLER_H \ No newline at end of file diff --git a/common/git/git_config_handler.cpp b/common/git/git_config_handler.cpp new file mode 100644 index 0000000000..86284aaab4 --- /dev/null +++ b/common/git/git_config_handler.cpp @@ -0,0 +1,109 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_config_handler.h" +#include +#include +#include +#include +#include +#include + +GIT_CONFIG_HANDLER::GIT_CONFIG_HANDLER( KIGIT_COMMON* aCommon ) : KIGIT_REPO_MIXIN( aCommon ) +{} + +GIT_CONFIG_HANDLER::~GIT_CONFIG_HANDLER() +{} + +GitUserConfig GIT_CONFIG_HANDLER::GetUserConfig() +{ + GitUserConfig userConfig; + + // Try to get from git config first + userConfig.hasName = GetConfigString( "user.name", userConfig.authorName ); + userConfig.hasEmail = GetConfigString( "user.email", userConfig.authorEmail ); + + // Fall back to common settings if not found in git config + if( !userConfig.hasName ) + { + userConfig.authorName = Pgm().GetCommonSettings()->m_Git.authorName; + } + + if( !userConfig.hasEmail ) + { + userConfig.authorEmail = Pgm().GetCommonSettings()->m_Git.authorEmail; + } + + return userConfig; +} + +wxString GIT_CONFIG_HANDLER::GetWorkingDirectory() +{ + git_repository* repo = GetRepo(); + + if( !repo ) + return wxEmptyString; + + const char* workdir = git_repository_workdir( repo ); + + if( !workdir ) + return wxEmptyString; + + return wxString( workdir ); +} + +bool GIT_CONFIG_HANDLER::GetConfigString( const wxString& aKey, wxString& aValue ) +{ + git_repository* repo = GetRepo(); + + if( !repo ) + return false; + + git_config* config = nullptr; + + if( git_repository_config( &config, repo ) != GIT_OK ) + { + wxLogTrace( traceGit, "Failed to get repository config: %s", KIGIT_COMMON::GetLastGitError() ); + return false; + } + + KIGIT::GitConfigPtr configPtr( config ); + + git_config_entry* entry = nullptr; + int result = git_config_get_entry( &entry, config, aKey.mb_str() ); + KIGIT::GitConfigEntryPtr entryPtr( entry ); + + if( result != GIT_OK || entry == nullptr ) + { + wxLogTrace( traceGit, "Config key '%s' not found", aKey ); + return false; + } + + aValue = wxString( entry->value ); + return true; +} + +void GIT_CONFIG_HANDLER::UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) +{ + ReportProgress( aCurrent, aTotal, aMessage ); +} \ No newline at end of file diff --git a/common/git/git_config_handler.h b/common/git/git_config_handler.h new file mode 100644 index 0000000000..d23857c7bf --- /dev/null +++ b/common/git/git_config_handler.h @@ -0,0 +1,69 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_CONFIG_HANDLER_H +#define GIT_CONFIG_HANDLER_H + +#include +#include + +struct GitUserConfig +{ + wxString authorName; + wxString authorEmail; + bool hasName = false; + bool hasEmail = false; +}; + +class GIT_CONFIG_HANDLER : public KIGIT_REPO_MIXIN +{ +public: + GIT_CONFIG_HANDLER( KIGIT_COMMON* aCommon ); + virtual ~GIT_CONFIG_HANDLER(); + + /** + * Get user configuration (name and email) from git config + * Falls back to common settings if not found in git config + * @return GitUserConfig structure with author information + */ + GitUserConfig GetUserConfig(); + + /** + * Get the repository working directory path + * @return Working directory path, or empty string if not available + */ + wxString GetWorkingDirectory(); + + void UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) override; + +private: + /** + * Get a string value from git config + * @param aKey Configuration key to retrieve + * @param aValue Output parameter for the value + * @return True if value was found, false otherwise + */ + bool GetConfigString( const wxString& aKey, wxString& aValue ); +}; + +#endif // GIT_CONFIG_HANDLER_H \ No newline at end of file diff --git a/common/git/git_init_handler.cpp b/common/git/git_init_handler.cpp new file mode 100644 index 0000000000..0cbe6e4089 --- /dev/null +++ b/common/git/git_init_handler.cpp @@ -0,0 +1,151 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_init_handler.h" +#include +#include +#include +#include + +GIT_INIT_HANDLER::GIT_INIT_HANDLER( KIGIT_COMMON* aCommon ) : KIGIT_REPO_MIXIN( aCommon ) +{} + +GIT_INIT_HANDLER::~GIT_INIT_HANDLER() +{} + +bool GIT_INIT_HANDLER::IsRepository( const wxString& aPath ) +{ + git_repository* repo = nullptr; + int error = git_repository_open( &repo, aPath.mb_str() ); + + if( error == 0 ) + { + git_repository_free( repo ); + return true; + } + + return false; +} + +InitResult GIT_INIT_HANDLER::InitializeRepository( const wxString& aPath ) +{ + // Check if directory is already a git repository + if( IsRepository( aPath ) ) + { + return InitResult::AlreadyExists; + } + + git_repository* repo = nullptr; + + if( git_repository_init( &repo, aPath.mb_str(), 0 ) != GIT_OK ) + { + if( repo ) + git_repository_free( repo ); + + AddErrorString( wxString::Format( _( "Failed to initialize Git repository: %s" ), + KIGIT_COMMON::GetLastGitError() ) ); + return InitResult::Error; + } + + // Update the common repository pointer + GetCommon()->SetRepo( repo ); + + wxLogTrace( traceGit, "Successfully initialized Git repository at %s", aPath ); + return InitResult::Success; +} + +bool GIT_INIT_HANDLER::SetupRemote( const RemoteConfig& aConfig ) +{ + git_repository* repo = GetRepo(); + + if( !repo ) + { + AddErrorString( _( "No repository available to set up remote" ) ); + return false; + } + + // Update connection settings in common + GetCommon()->SetUsername( aConfig.username ); + GetCommon()->SetPassword( aConfig.password ); + GetCommon()->SetSSHKey( aConfig.sshKey ); + + git_remote* remote = nullptr; + wxString fullURL; + + // Build the full URL based on connection type + if( aConfig.connType == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) + { + fullURL = aConfig.username + "@" + aConfig.url; + } + else if( aConfig.connType == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) + { + fullURL = aConfig.url.StartsWith( "https" ) ? "https://" : "http://"; + + if( !aConfig.username.empty() ) + { + fullURL.append( aConfig.username ); + + if( !aConfig.password.empty() ) + { + fullURL.append( wxS( ":" ) ); + fullURL.append( aConfig.password ); + } + + fullURL.append( wxS( "@" ) ); + } + + // Extract the bare URL (without protocol prefix) + wxString bareURL = aConfig.url; + if( bareURL.StartsWith( "https://" ) ) + bareURL = bareURL.Mid( 8 ); + else if( bareURL.StartsWith( "http://" ) ) + bareURL = bareURL.Mid( 7 ); + + fullURL.append( bareURL ); + } + else + { + fullURL = aConfig.url; + } + + int error = git_remote_create_with_fetchspec( &remote, repo, "origin", + fullURL.ToStdString().c_str(), + "+refs/heads/*:refs/remotes/origin/*" ); + + KIGIT::GitRemotePtr remotePtr( remote ); + + if( error != GIT_OK ) + { + AddErrorString( wxString::Format( _( "Failed to create remote: %s" ), + KIGIT_COMMON::GetLastGitError() ) ); + return false; + } + + wxLogTrace( traceGit, "Successfully set up remote origin" ); + return true; +} + +void GIT_INIT_HANDLER::UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) +{ + ReportProgress( aCurrent, aTotal, aMessage ); +} \ No newline at end of file diff --git a/common/git/git_init_handler.h b/common/git/git_init_handler.h new file mode 100644 index 0000000000..14fb0e6909 --- /dev/null +++ b/common/git/git_init_handler.h @@ -0,0 +1,76 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_INIT_HANDLER_H +#define GIT_INIT_HANDLER_H + +#include +#include + +enum class InitResult +{ + Success, + AlreadyExists, + Error +}; + +struct RemoteConfig +{ + wxString url; + wxString username; + wxString password; + wxString sshKey; + KIGIT_COMMON::GIT_CONN_TYPE connType; +}; + +class GIT_INIT_HANDLER : public KIGIT_REPO_MIXIN +{ +public: + GIT_INIT_HANDLER( KIGIT_COMMON* aCommon ); + virtual ~GIT_INIT_HANDLER(); + + /** + * Check if a directory is already a git repository + * @param aPath Directory path to check + * @return True if directory contains a git repository + */ + bool IsRepository( const wxString& aPath ); + + /** + * Initialize a new git repository in the specified directory + * @param aPath Directory path where to initialize the repository + * @return InitResult indicating success, already exists, or error + */ + InitResult InitializeRepository( const wxString& aPath ); + + /** + * Set up a remote for the repository + * @param aConfig Remote configuration parameters + * @return True on success, false on error + */ + bool SetupRemote( const RemoteConfig& aConfig ); + + void UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) override; +}; + +#endif // GIT_INIT_HANDLER_H \ No newline at end of file diff --git a/common/git/git_status_handler.cpp b/common/git/git_status_handler.cpp new file mode 100644 index 0000000000..e33cf8dc59 --- /dev/null +++ b/common/git/git_status_handler.cpp @@ -0,0 +1,232 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "git_status_handler.h" +#include +#include +#include +#include + +GIT_STATUS_HANDLER::GIT_STATUS_HANDLER( KIGIT_COMMON* aCommon ) : KIGIT_REPO_MIXIN( aCommon ) +{} + +GIT_STATUS_HANDLER::~GIT_STATUS_HANDLER() +{} + +bool GIT_STATUS_HANDLER::HasChangedFiles() +{ + git_repository* repo = GetRepo(); + + if( !repo ) + return false; + + git_status_options opts; + git_status_init_options( &opts, GIT_STATUS_OPTIONS_VERSION ); + + opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX + | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY; + + git_status_list* status_list = nullptr; + + if( git_status_list_new( &status_list, repo, &opts ) != GIT_OK ) + { + wxLogTrace( traceGit, "Failed to get status list: %s", KIGIT_COMMON::GetLastGitError() ); + return false; + } + + KIGIT::GitStatusListPtr status_list_ptr( status_list ); + bool hasChanges = ( git_status_list_entrycount( status_list ) > 0 ); + + return hasChanges; +} + +std::map GIT_STATUS_HANDLER::GetFileStatus( const wxString& aPathspec ) +{ + std::map fileStatusMap; + git_repository* repo = GetRepo(); + + if( !repo ) + return fileStatusMap; + + git_status_options status_options; + git_status_init_options( &status_options, GIT_STATUS_OPTIONS_VERSION ); + status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; + status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_INCLUDE_UNMODIFIED; + + // Set up pathspec if provided + const char* pathspec_cstr = nullptr; + if( !aPathspec.IsEmpty() ) + { + pathspec_cstr = aPathspec.c_str().AsChar(); + const char* pathspec[] = { pathspec_cstr }; + status_options.pathspec = { (char**) pathspec, 1 }; + } + + git_status_list* status_list = nullptr; + + if( git_status_list_new( &status_list, repo, &status_options ) != GIT_OK ) + { + wxLogTrace( traceGit, "Failed to get git status list: %s", KIGIT_COMMON::GetLastGitError() ); + return fileStatusMap; + } + + KIGIT::GitStatusListPtr statusListPtr( status_list ); + + size_t count = git_status_list_entrycount( status_list ); + wxString repoWorkDir( git_repository_workdir( repo ) ); + + for( size_t ii = 0; ii < count; ++ii ) + { + const git_status_entry* entry = git_status_byindex( status_list, ii ); + std::string path( entry->head_to_index ? entry->head_to_index->old_file.path + : entry->index_to_workdir->old_file.path ); + + wxString absPath = repoWorkDir + path; + + FileStatus fileStatus; + fileStatus.filePath = absPath; + fileStatus.gitStatus = entry->status; + fileStatus.status = ConvertStatus( entry->status ); + + fileStatusMap[absPath] = fileStatus; + } + + return fileStatusMap; +} + +wxString GIT_STATUS_HANDLER::GetCurrentBranchName() +{ + git_repository* repo = GetRepo(); + + if( !repo ) + return wxEmptyString; + + git_reference* currentBranchReference = nullptr; + int rc = git_repository_head( ¤tBranchReference, repo ); + KIGIT::GitReferencePtr currentBranchReferencePtr( currentBranchReference ); + + if( currentBranchReference ) + { + return git_reference_shorthand( currentBranchReference ); + } + else if( rc == GIT_EUNBORNBRANCH ) + { + // Unborn branch - could return empty or a default name + return wxEmptyString; + } + else + { + wxLogTrace( traceGit, "Failed to lookup current branch: %s", KIGIT_COMMON::GetLastGitError() ); + return wxEmptyString; + } +} + +void GIT_STATUS_HANDLER::UpdateRemoteStatus( const std::set& aLocalChanges, + const std::set& aRemoteChanges, + std::map& aFileStatus ) +{ + git_repository* repo = GetRepo(); + + if( !repo ) + return; + + wxString repoWorkDir( git_repository_workdir( repo ) ); + + // Update status based on local/remote changes + for( auto& [absPath, fileStatus] : aFileStatus ) + { + // Convert absolute path to relative path for comparison + wxString relativePath = absPath; + if( relativePath.StartsWith( repoWorkDir ) ) + { + relativePath = relativePath.Mid( repoWorkDir.length() ); +#ifdef _WIN32 + relativePath.Replace( wxS( "\\" ), wxS( "/" ) ); +#endif + } + + std::string relativePathStd = relativePath.ToStdString(); + + // Only update if the file is not already modified/added/deleted + if( fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT ) + { + if( aLocalChanges.count( relativePathStd ) ) + { + fileStatus.status = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD; + } + else if( aRemoteChanges.count( relativePathStd ) ) + { + fileStatus.status = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND; + } + } + } +} + +wxString GIT_STATUS_HANDLER::GetWorkingDirectory() +{ + git_repository* repo = GetRepo(); + + if( !repo ) + return wxEmptyString; + + const char* workdir = git_repository_workdir( repo ); + + if( !workdir ) + return wxEmptyString; + + return wxString( workdir ); +} + +KIGIT_COMMON::GIT_STATUS GIT_STATUS_HANDLER::ConvertStatus( unsigned int aGitStatus ) +{ + if( aGitStatus & GIT_STATUS_IGNORED ) + { + return KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED; + } + else if( aGitStatus & ( GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_MODIFIED ) ) + { + return KIGIT_COMMON::GIT_STATUS::GIT_STATUS_MODIFIED; + } + else if( aGitStatus & ( GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_NEW ) ) + { + return KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED; + } + else if( aGitStatus & ( GIT_STATUS_INDEX_DELETED | GIT_STATUS_WT_DELETED ) ) + { + return KIGIT_COMMON::GIT_STATUS::GIT_STATUS_DELETED; + } + else if( aGitStatus & ( GIT_STATUS_CONFLICTED ) ) + { + return KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CONFLICTED; + } + else + { + return KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT; + } +} + +void GIT_STATUS_HANDLER::UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) +{ + ReportProgress( aCurrent, aTotal, aMessage ); +} \ No newline at end of file diff --git a/common/git/git_status_handler.h b/common/git/git_status_handler.h new file mode 100644 index 0000000000..4c8833bf8a --- /dev/null +++ b/common/git/git_status_handler.h @@ -0,0 +1,91 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.TXT for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef GIT_STATUS_HANDLER_H +#define GIT_STATUS_HANDLER_H + +#include +#include +#include +#include + +struct FileStatus +{ + wxString filePath; + KIGIT_COMMON::GIT_STATUS status; + unsigned int gitStatus; // Raw git status flags +}; + +class GIT_STATUS_HANDLER : public KIGIT_REPO_MIXIN +{ +public: + GIT_STATUS_HANDLER( KIGIT_COMMON* aCommon ); + virtual ~GIT_STATUS_HANDLER(); + + /** + * Check if the repository has any changed files + * @return True if there are uncommitted changes, false otherwise + */ + bool HasChangedFiles(); + + /** + * Get detailed file status for all files in the specified path + * @param aPathspec Path specification to filter files (empty for all files) + * @return Map of absolute file paths to their status information + */ + std::map GetFileStatus( const wxString& aPathspec = wxEmptyString ); + + /** + * Get the current branch name + * @return Current branch name, or empty string if not available + */ + wxString GetCurrentBranchName(); + + /** + * Get status for modified files based on local/remote changes + * @param aLocalChanges Set of files with local changes + * @param aRemoteChanges Set of files with remote changes + * @param aFileStatus Map to update with ahead/behind status + */ + void UpdateRemoteStatus( const std::set& aLocalChanges, + const std::set& aRemoteChanges, + std::map& aFileStatus ); + + /** + * Get the repository working directory path + * @return Working directory path, or empty string if not available + */ + wxString GetWorkingDirectory(); + + void UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage ) override; + +private: + /** + * Convert git status flags to KIGIT_COMMON::GIT_STATUS + * @param aGitStatus Raw git status flags + * @return Converted status + */ + KIGIT_COMMON::GIT_STATUS ConvertStatus( unsigned int aGitStatus ); +}; + +#endif // GIT_STATUS_HANDLER_H \ No newline at end of file diff --git a/common/git/project_git_utils.cpp b/common/git/project_git_utils.cpp index b835dced92..9ff5819ba3 100644 --- a/common/git/project_git_utils.cpp +++ b/common/git/project_git_utils.cpp @@ -26,6 +26,8 @@ #include "kicad_git_memory.h" #include #include +#include +#include namespace KIGIT { @@ -88,5 +90,36 @@ int PROJECT_GIT_UTILS::CreateBranch( git_repository* aRepo, const wxString& aBra return 0; } -} // namespace KIGIT +bool PROJECT_GIT_UTILS::RemoveVCS( git_repository*& aRepo, const wxString& aProjectPath, + bool aRemoveGitDir, wxString* aErrors ) +{ + if( aRepo ) + { + git_repository_free( aRepo ); + aRepo = nullptr; + } + if( aRemoveGitDir ) + { + wxFileName gitDir( aProjectPath, wxEmptyString ); + gitDir.AppendDir( ".git" ); + + if( gitDir.DirExists() ) + { + wxString errors; + if( !RmDirRecursive( gitDir.GetPath(), &errors ) ) + { + if( aErrors ) + *aErrors = errors; + + wxLogTrace( traceGit, "Failed to remove .git directory: %s", errors ); + return false; + } + } + } + + wxLogTrace( traceGit, "Successfully removed VCS from project" ); + return true; +} + +} // namespace KIGIT \ No newline at end of file diff --git a/common/git/project_git_utils.h b/common/git/project_git_utils.h index bdf81816c4..82d523043c 100644 --- a/common/git/project_git_utils.h +++ b/common/git/project_git_utils.h @@ -50,8 +50,21 @@ public: * @return 0 on success, libgit2 error code on failure. */ static int CreateBranch( git_repository* aRepo, const wxString& aBranchName ); + + /** + * Remove version control from a directory by freeing the repository and + * optionally removing the .git directory. + * + * @param aRepo Repository to free (will be set to nullptr) + * @param aProjectPath Path to the project directory + * @param aRemoveGitDir If true, also remove the .git directory from disk + * @param aErrors Output parameter for any error messages + * @return True on success, false on failure + */ +static bool RemoveVCS( git_repository*& aRepo, const wxString& aProjectPath = wxEmptyString, + bool aRemoveGitDir = false, wxString* aErrors = nullptr ); }; } // namespace KIGIT -#endif // PROJECT_GIT_UTILS_H +#endif // PROJECT_GIT_UTILS_H \ No newline at end of file diff --git a/kicad/project_tree_pane.cpp b/kicad/project_tree_pane.cpp index e0b57cc209..2528caf35c 100644 --- a/kicad/project_tree_pane.cpp +++ b/kicad/project_tree_pane.cpp @@ -57,6 +57,7 @@ #include #include +#include #include #include #include @@ -65,6 +66,9 @@ #include #include #include +#include +#include +#include #include #include @@ -650,7 +654,8 @@ void PROJECT_TREE_PANE::ReCreateTreePrj() if( m_TreeProject->GetGitRepo() ) { - git_repository_free( m_TreeProject->GetGitRepo() ); + git_repository* repo = m_TreeProject->GetGitRepo(); + KIGIT::PROJECT_GIT_UTILS::RemoveVCS( repo ); m_TreeProject->SetGitRepo( nullptr ); m_gitIconsInitialized = false; } @@ -753,28 +758,11 @@ void PROJECT_TREE_PANE::ReCreateTreePrj() bool PROJECT_TREE_PANE::hasChangedFiles() { - git_repository* repo = m_TreeProject->GetGitRepo(); - - if( !repo ) + if( !m_TreeProject->GetGitRepo() ) return false; - git_status_options opts; - git_status_init_options( &opts, GIT_STATUS_OPTIONS_VERSION ); - - opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; - opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX - | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY; - - git_status_list* status_list = nullptr; - - if( git_status_list_new( &status_list, repo, &opts ) != GIT_OK ) - { - wxLogError( _( "Failed to get status list: %s" ), KIGIT_COMMON::GetLastGitError() ); - return false; - } - - KIGIT::GitStatusListPtr status_list_ptr( status_list ); - return ( git_status_list_entrycount( status_list ) > 0 ); + GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() ); + return statusHandler.HasChangedFiles(); } @@ -1626,7 +1614,8 @@ void PROJECT_TREE_PANE::EmptyTreePrj() } } - git_repository_free( m_TreeProject->GetGitRepo() ); + git_repository* repo = m_TreeProject->GetGitRepo(); + KIGIT::PROJECT_GIT_UTILS::RemoveVCS( repo ); m_TreeProject->SetGitRepo( nullptr ); } } @@ -1673,110 +1662,51 @@ void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent ) return; } - // Check if the directory is already a git repository - git_repository* repo = nullptr; - int error = git_repository_open( &repo, dir.mb_str() ); + GIT_INIT_HANDLER initHandler( m_TreeProject->GitCommon() ); - if( error == 0 ) + if( initHandler.IsRepository( dir ) ) { - // Directory is already a git repository wxWindow* topLevelParent = wxGetTopLevelParent( this ); - DisplayInfoMessage( topLevelParent, _( "The selected directory is already a Git project." ) ); - git_repository_free( repo ); return; } DIALOG_GIT_REPOSITORY dlg( wxGetTopLevelParent( this ), nullptr ); - dlg.SetTitle( _( "Set default remote" ) ); if( dlg.ShowModal() != wxID_OK ) return; - // Directory is not a git repository - if( git_repository_init( &repo, dir.mb_str(), 0 ) != GIT_OK ) + InitResult result = initHandler.InitializeRepository( dir ); + if( result != InitResult::Success ) { - if( repo ) - git_repository_free( repo ); - - if( m_gitLastError != git_error_last()->klass ) - { - m_gitLastError = git_error_last()->klass; - DisplayErrorMessage( m_parent, _( "Failed to initialize Git project." ), - KIGIT_COMMON::GetLastGitError() ); - } - + DisplayErrorMessage( m_parent, _( "Failed to initialize Git project." ), + initHandler.GetErrorString() ); return; } - else + + m_gitLastError = GIT_ERROR_NONE; + + // Set up the remote + RemoteConfig remoteConfig; + remoteConfig.url = dlg.GetRepoURL(); + remoteConfig.username = dlg.GetUsername(); + remoteConfig.password = dlg.GetPassword(); + remoteConfig.sshKey = dlg.GetRepoSSHPath(); + remoteConfig.connType = dlg.GetRepoType(); + + if( !initHandler.SetupRemote( remoteConfig ) ) { - m_TreeProject->SetGitRepo( repo ); - m_gitLastError = GIT_ERROR_NONE; - } - - //Set up the git remote - m_TreeProject->GitCommon()->SetPassword( dlg.GetPassword() ); - m_TreeProject->GitCommon()->SetUsername( dlg.GetUsername() ); - m_TreeProject->GitCommon()->SetSSHKey( dlg.GetRepoSSHPath() ); - - git_remote* remote = nullptr; - wxString fullURL; - - if( dlg.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) - { - fullURL = dlg.GetUsername() + "@" + dlg.GetRepoURL(); - } - else if( dlg.GetRepoType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) - { - fullURL = dlg.GetRepoURL().StartsWith( "https" ) ? "https://" : "http://"; - - if( !dlg.GetUsername().empty() ) - { - fullURL.append( dlg.GetUsername() ); - - if( !dlg.GetPassword().empty() ) - { - fullURL.append( wxS( ":" ) ); - fullURL.append( dlg.GetPassword() ); - } - - fullURL.append( wxS( "@" ) ); - } - - fullURL.append( dlg.GetBareRepoURL() ); - } - else - { - fullURL = dlg.GetRepoURL(); - } - - - error = git_remote_create_with_fetchspec( &remote, repo, "origin", - fullURL.ToStdString().c_str(), - "+refs/heads/*:refs/remotes/origin/*" ); - - KIGIT::GitRemotePtr remotePtr( remote ); - - if( error != GIT_OK ) - { - if( m_gitLastError != git_error_last()->klass ) - { - m_gitLastError = git_error_last()->klass; - DisplayErrorMessage( m_parent, _( "Failed to set default remote." ), - KIGIT_COMMON::GetLastGitError() ); - } - + DisplayErrorMessage( m_parent, _( "Failed to set default remote." ), + initHandler.GetErrorString() ); return; } m_gitLastError = GIT_ERROR_NONE; GIT_PULL_HANDLER handler( m_TreeProject->GitCommon() ); - handler.SetProgressReporter( std::make_unique( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) ); - handler.PerformFetch(); KIPLATFORM::SECRETS::StoreSecret( dlg.GetRepoURL(), dlg.GetUsername(), dlg.GetPassword() ); @@ -1844,27 +1774,23 @@ void PROJECT_TREE_PANE::onGitPushProject( wxCommandEvent& aEvent ) } - - - void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent ) { - git_repository* repo = m_TreeProject->GetGitRepo(); - - if( !repo ) + if( !m_TreeProject->GetGitRepo() ) return; + GIT_BRANCH_HANDLER branchHandler( m_TreeProject->GitCommon() ); wxString branchName; if( aEvent.GetId() == ID_GIT_SWITCH_BRANCH ) { - DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( this ), repo ); + DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( this ), m_TreeProject->GetGitRepo() ); int retval = dlg.ShowModal(); branchName = dlg.GetBranchName(); if( retval == wxID_ADD ) - KIGIT::PROJECT_GIT_UTILS::CreateBranch( repo, branchName ); + KIGIT::PROJECT_GIT_UTILS::CreateBranch( m_TreeProject->GetGitRepo(), branchName ); else if( retval != wxID_OK ) return; } @@ -1879,48 +1805,10 @@ void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent ) branchName = branches[branchIndex]; } - // Retrieve the reference to the existing branch using libgit2 - git_reference* branchRef = nullptr; - - if( git_reference_lookup( &branchRef, repo, branchName.mb_str() ) != GIT_OK && - git_reference_dwim( &branchRef, repo, branchName.mb_str() ) != GIT_OK ) + wxLogTrace( traceGit, wxS( "onGitSwitchBranch: Switching to branch '%s'" ), branchName ); + if( branchHandler.SwitchToBranch( branchName ) != BranchResult::Success ) { - wxString errorMessage = wxString::Format( _( "Failed to lookup branch '%s': %s" ), - branchName, KIGIT_COMMON::GetLastGitError() ); - DisplayError( m_parent, errorMessage ); - return; - } - - KIGIT::GitReferencePtr branchRefPtr( branchRef ); - const char* branchRefName = git_reference_name( branchRef ); - git_object* branchObj = nullptr; - - if( git_revparse_single( &branchObj, repo, branchName.mb_str() ) != 0 ) - { - wxString errorMessage = - wxString::Format( _( "Failed to find branch head for '%s'" ), branchName ); - DisplayError( m_parent, errorMessage ); - return; - } - - KIGIT::GitObjectPtr branchObjPtr( branchObj ); - - // Switch to the branch - if( git_checkout_tree( repo, branchObj, nullptr ) != 0 ) - { - wxString errorMessage = - wxString::Format( _( "Failed to switch to branch '%s'" ), branchName ); - DisplayError( m_parent, errorMessage ); - return; - } - - // Update the HEAD reference - if( git_repository_set_head( repo, branchRefName ) != 0 ) - { - wxString errorMessage = wxString::Format( - _( "Failed to update HEAD reference for branch '%s'" ), branchName ); - DisplayError( m_parent, errorMessage ); - return; + DisplayError( m_parent, branchHandler.GetErrorString() ); } } @@ -1936,21 +1824,18 @@ void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent ) return; } - // Remove the VCS (git) from the project directory - git_repository_free( repo ); - m_TreeProject->SetGitRepo( nullptr ); - - // Remove the .git directory - wxFileName fn( m_Parent->GetProjectFileName() ); - fn.AppendDir( ".git" ); - + // Fully delete the git repository on disk + wxLogTrace( traceGit, wxS( "onGitRemoveVCS: Removing VCS from project" ) ); wxString errors; - if( !RmDirRecursive( fn.GetPath(), &errors ) ) + if( !KIGIT::PROJECT_GIT_UTILS::RemoveVCS( repo, Prj().GetProjectPath(), true, &errors ) ) { - DisplayErrorMessage( m_parent, _( "Failed to remove Git directory" ), errors ); + DisplayErrorMessage( m_parent, _( "Failed to remove VCS" ), errors ); + return; } + m_TreeProject->SetGitRepo( nullptr ); + // Clear all item states std::stack items; items.push( m_TreeProject->GetRootItem() ); @@ -2118,17 +2003,17 @@ void PROJECT_TREE_PANE::updateGitStatusIconMap() return; } - git_repository* repo = m_TreeProject->GetGitRepo(); - - if( !repo ) + if( !m_TreeProject->GetGitRepo() ) { wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: No git repository found" ) ); return; } - // Get Current Branch + GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() ); + + // Set up pathspec for project files wxFileName rootFilename( Prj().GetProjectFullName() ); - wxString repoWorkDir( git_repository_workdir( repo ) ); + wxString repoWorkDir = statusHandler.GetWorkingDirectory(); wxFileName relative = rootFilename; relative.MakeRelativeTo( repoWorkDir ); @@ -2138,164 +2023,35 @@ void PROJECT_TREE_PANE::updateGitStatusIconMap() pathspecStr.Replace( wxS( "\\" ), wxS( "/" ) ); #endif - const char* pathspec[] = { pathspecStr.c_str().AsChar() }; - - git_status_options status_options; - git_status_init_options( &status_options, GIT_STATUS_OPTIONS_VERSION ); - status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; - status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_INCLUDE_UNMODIFIED; - status_options.pathspec = { (char**) pathspec, 1 }; - - git_index* index = nullptr; - - if( git_repository_index( &index, repo ) != GIT_OK ) - { - m_gitLastError = giterr_last()->klass; - wxLogTrace( traceGit, wxS( "Failed to get git index: %s" ), KIGIT_COMMON::GetLastGitError() ); - return; - } - - KIGIT::GitIndexPtr indexPtr( index ); - git_status_list* status_list = nullptr; - - if( git_status_list_new( &status_list, repo, &status_options ) != GIT_OK ) - { - wxLogTrace( traceGit, wxS( "Failed to get git status list: %s" ), KIGIT_COMMON::GetLastGitError() ); - return; - } - - KIGIT::GitStatusListPtr statusListPtr( status_list ); + // Get file status + auto fileStatusMap = statusHandler.GetFileStatus( pathspecStr ); auto [localChanges, remoteChanges] = m_TreeProject->GitCommon()->GetDifferentFiles(); + statusHandler.UpdateRemoteStatus( localChanges, remoteChanges, fileStatusMap ); - size_t count = git_status_list_entrycount( status_list ); - bool updated = false; + bool updated = false; - for( size_t ii = 0; ii < count; ++ii ) + // Update status icons based on file status + for( const auto& [absPath, fileStatus] : fileStatusMap ) { - const git_status_entry* entry = git_status_byindex( status_list, ii ); - std::string path( entry->head_to_index? entry->head_to_index->old_file.path - : entry->index_to_workdir->old_file.path ); - - wxString absPath = repoWorkDir; - absPath << path; - auto iter = m_gitTreeCache.find( absPath ); - if( iter == m_gitTreeCache.end() ) { wxLogTrace( traceGit, wxS( "File '%s' not found in tree cache" ), absPath ); continue; } - // If we are current, don't continue because we still need to check to see if the - // current commit is ahead/behind the remote. If the file is modified/added/deleted, - // that is the main status we want to show. - if( entry->status & GIT_STATUS_IGNORED ) - { - wxLogTrace( traceGit, wxS( "File '%s' is ignored" ), absPath ); - auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, - KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED ); - - if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED ) - updated = true; - - it->second = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED; - } - else if( entry->status & ( GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_MODIFIED ) ) - { - wxLogTrace( traceGit, wxS( "File '%s' is modified in %s" ), - absPath, ( entry->status & GIT_STATUS_INDEX_MODIFIED )? "index" : "working tree" ); - auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, - KIGIT_COMMON::GIT_STATUS::GIT_STATUS_MODIFIED ); - - if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_MODIFIED ) - updated = true; - - it->second = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_MODIFIED; - } - else if( entry->status & ( GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_NEW ) ) - { - wxLogTrace( traceGit, wxS( "File '%s' is new in %s" ), - absPath, ( entry->status & GIT_STATUS_INDEX_NEW )? "index" : "working tree" ); - auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, - KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED ); - - if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED ) - updated = true; - - it->second = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED; - } - else if( entry->status & ( GIT_STATUS_INDEX_DELETED | GIT_STATUS_WT_DELETED ) ) - { - wxLogTrace( traceGit, wxS( "File '%s' is deleted in %s" ), - absPath, ( entry->status & GIT_STATUS_INDEX_DELETED )? "index" : "working tree" ); - auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, - KIGIT_COMMON::GIT_STATUS::GIT_STATUS_DELETED ); - - if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_DELETED ) - updated = true; - - it->second = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_DELETED; - } - else if( localChanges.count( path ) ) - { - wxLogTrace( traceGit, wxS( "File '%s' is ahead of remote" ), absPath ); - auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, - KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD ); - - if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD ) - updated = true; - - it->second = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD; - } - else if( remoteChanges.count( path ) ) - { - wxLogTrace( traceGit, wxS( "File '%s' is behind remote" ), absPath ); - auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, - KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND ); - - if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND ) - updated = true; - - it->second = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND; - } - else - { - // If we are here, the file is unmodified and not ignored - auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, - KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT ); - - if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT ) - updated = true; - - it->second = KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT; - } + auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, fileStatus.status ); + if( inserted || it->second != fileStatus.status ) + updated = true; + it->second = fileStatus.status; } - git_reference* currentBranchReference = nullptr; - int rc = git_repository_head( ¤tBranchReference, repo ); - KIGIT::GitReferencePtr currentBranchReferencePtr( currentBranchReference ); - // Get the current branch name - if( currentBranchReference ) - { - m_gitCurrentBranchName = git_reference_shorthand( currentBranchReference ); - } - else if( rc == GIT_EUNBORNBRANCH ) - { - // TODO: couldn't immediately figure out if libgit2 can return the name of an unborn branch - // For now, just do nothing - } - else - { - if( giterr_last()->klass != m_gitLastError ) - wxLogTrace( "git", "Failed to lookup current branch: %s", KIGIT_COMMON::GetLastGitError() ); - - m_gitLastError = giterr_last()->klass; - } + m_gitCurrentBranchName = statusHandler.GetCurrentBranchName(); wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updated git status icons" ) ); - // If the icons are not changed, queue an event to update in the main thread + + // Update UI if icons changed if( updated || !m_gitIconsInitialized ) { CallAfter( @@ -2319,55 +2075,15 @@ void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent ) return; } - git_config* config = nullptr; - git_repository_config( &config, repo ); - KIGIT::GitConfigPtr configPtr( config ); - - // Read relevant data from the git config - wxString authorName; - wxString authorEmail; - - // Read author name - git_config_entry* name_c = nullptr; - git_config_entry* email_c = nullptr; - int authorNameError = git_config_get_entry( &name_c, config, "user.name" ); - KIGIT::GitConfigEntryPtr namePtr( name_c ); - - if( authorNameError != 0 || name_c == nullptr ) - { - authorName = Pgm().GetCommonSettings()->m_Git.authorName; - } - else - { - authorName = name_c->value; - } - - // Read author email - int authorEmailError = git_config_get_entry( &email_c, config, "user.email" ); - - if( authorEmailError != 0 || email_c == nullptr ) - { - authorEmail = Pgm().GetCommonSettings()->m_Git.authorEmail; - } - else - { - authorEmail = email_c->value; - } + // Get git configuration + GIT_CONFIG_HANDLER configHandler( m_TreeProject->GitCommon() ); + GitUserConfig userConfig = configHandler.GetUserConfig(); // Collect modified files in the repository - git_status_options status_options; - git_status_init_options( &status_options, GIT_STATUS_OPTIONS_VERSION ); - status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; - status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED; - - git_status_list* status_list = nullptr; - git_status_list_new( &status_list, repo, &status_options ); - KIGIT::GitStatusListPtr statusListPtr( status_list ); + GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() ); + auto fileStatusMap = statusHandler.GetFileStatus(); std::map modifiedFiles; - - size_t count = git_status_list_entrycount( status_list ); - std::set selected_files; for( PROJECT_TREE_ITEM* item : tree_data ) @@ -2376,45 +2092,33 @@ void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent ) selected_files.emplace( item->GetFileName() ); } - for( size_t i = 0; i < count; ++i ) - { - const git_status_entry* entry = git_status_byindex( status_list, i ); + wxString repoWorkDir = statusHandler.GetWorkingDirectory(); - // Check if the file is modified (index or workdir changes) - if( entry->status == GIT_STATUS_CURRENT - || ( entry->status & ( GIT_STATUS_CONFLICTED | GIT_STATUS_IGNORED ) ) ) + for( const auto& [absPath, fileStatus] : fileStatusMap ) + { + // Skip current, conflicted, or ignored files + if( fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT + || fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CONFLICTED + || fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED ) { continue; } - wxFileName fn; - wxString filePath; + wxFileName fn( absPath ); - // TODO: we are kind of erasing the difference between workdir and index here, - // because the Commit dialog doesn't show that difference. - // Entry may only have a head_to_index if it was previously staged - if( entry->index_to_workdir ) + // Convert to relative path for the modifiedFiles map + wxString relativePath = absPath; + if( relativePath.StartsWith( repoWorkDir ) ) { - fn.Assign( entry->index_to_workdir->old_file.path ); - fn.MakeAbsolute( git_repository_workdir( repo ) ); - filePath = wxString( entry->index_to_workdir->old_file.path, wxConvUTF8 ); - } - else if( entry->head_to_index ) - { - fn.Assign( entry->head_to_index->old_file.path ); - fn.MakeAbsolute( git_repository_workdir( repo ) ); - filePath = wxString( entry->head_to_index->old_file.path, wxConvUTF8 ); - } - else - { - wxCHECK2_MSG( false, continue, "File status with neither git_status_entry set!" ); + relativePath = relativePath.Mid( repoWorkDir.length() ); +#ifdef _WIN32 + relativePath.Replace( wxS( "\\" ), wxS( "/" ) ); +#endif } // Do not commit files outside the project directory wxString projectPath = Prj().GetProjectPath(); - wxString fileName = fn.GetFullPath(); - - if( !fileName.StartsWith( projectPath ) ) + if( !absPath.StartsWith( projectPath ) ) continue; // Skip lock files @@ -2435,23 +2139,22 @@ void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent ) if( aEvent.GetId() == ID_GIT_COMMIT_PROJECT ) { - modifiedFiles.emplace( filePath, entry->status ); + modifiedFiles.emplace( relativePath, fileStatus.gitStatus ); } - else if( selected_files.count( fn.GetFullPath() ) ) + else if( selected_files.count( absPath ) ) { - modifiedFiles.emplace( filePath, entry->status ); + modifiedFiles.emplace( relativePath, fileStatus.gitStatus ); } } // Create a commit dialog - DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, authorName, authorEmail, + DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, userConfig.authorName, userConfig.authorEmail, modifiedFiles ); auto ret = dlg.ShowModal(); if( ret != wxID_OK ) return; - std::vector files = dlg.GetSelectedFiles(); if( dlg.GetCommitMessage().IsEmpty() ) @@ -2490,29 +2193,23 @@ void PROJECT_TREE_PANE::onGitAddToIndex( wxCommandEvent& aEvent ) bool PROJECT_TREE_PANE::canFileBeAddedToVCS( const wxString& aFile ) { - git_index *index; - size_t entry_pos; - - git_repository* repo = m_TreeProject->GetGitRepo(); - - if( !repo ) + if( !m_TreeProject->GetGitRepo() ) return false; - if( git_repository_index( &index, repo ) != 0 ) + GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() ); + auto fileStatusMap = statusHandler.GetFileStatus(); + + // Check if file is already tracked or staged + for( const auto& [filePath, fileStatus] : fileStatusMap ) { - wxLogTrace( traceGit, "Failed to get git index: %s", KIGIT_COMMON::GetLastGitError() ); - return false; - } - - KIGIT::GitIndexPtr indexPtr( index ); - - // If we successfully find the file in the index, we may not add it to the VCS - if( git_index_find( &entry_pos, index, aFile.mb_str() ) == 0 ) - { - wxLogTrace( traceGit, "File already in index: %s", aFile ); - return false; + if( filePath.EndsWith( aFile ) || filePath == aFile ) + { + // File can be added if it's untracked + return fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_UNTRACKED; + } } + // If file not found in status, it might be addable return true; }