/* * 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 "libgit_backend.h" #include "git_clone_handler.h" #include "git_commit_handler.h" #include "git_push_handler.h" #include "git_add_to_index_handler.h" #include "git_remove_from_index_handler.h" #include "git_status_handler.h" #include "git_config_handler.h" #include "git_init_handler.h" #include "git_branch_handler.h" #include "git_pull_handler.h" #include "git_revert_handler.h" #include "project_git_utils.h" #include "kicad_git_common.h" #include "kicad_git_memory.h" #include "trace_helpers.h" #include #include #include #include #include #include #include #include static std::string getFirstLineFromCommitMessage( const std::string& aMessage ) { if( aMessage.empty() ) return aMessage; size_t firstLineEnd = aMessage.find_first_of( '\n' ); if( firstLineEnd != std::string::npos ) return aMessage.substr( 0, firstLineEnd ); return aMessage; } static std::string getFormattedCommitDate( const git_time& aTime ) { char dateBuffer[64]; time_t time = static_cast( aTime.time ); struct tm timeInfo; #ifdef _WIN32 localtime_s( &timeInfo, &time ); #else gmtime_r( &time, &timeInfo ); #endif strftime( dateBuffer, sizeof( dateBuffer ), "%Y-%b-%d %H:%M:%S", &timeInfo ); return dateBuffer; } void LIBGIT_BACKEND::Init() { git_libgit2_init(); } void LIBGIT_BACKEND::Shutdown() { git_libgit2_shutdown(); } bool LIBGIT_BACKEND::IsLibraryAvailable() { #if ( LIBGIT2_VER_MAJOR >= 1 ) || ( LIBGIT2_VER_MINOR >= 99 ) int major = 0, minor = 0, rev = 0; return git_libgit2_version( &major, &minor, &rev ) == GIT_OK; #else // On older platforms, assume available when building with libgit2 return true; #endif } bool LIBGIT_BACKEND::Clone( GIT_CLONE_HANDLER* aHandler ) { KIGIT_COMMON* common = aHandler->GetCommon(); std::unique_lock lock( common->m_gitActionMutex, std::try_to_lock ); if( !lock.owns_lock() ) { wxLogTrace( traceGit, "GIT_CLONE_HANDLER::PerformClone() could not lock" ); return false; } wxFileName clonePath( aHandler->GetClonePath() ); if( !clonePath.DirExists() ) { if( !clonePath.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) { aHandler->AddErrorString( wxString::Format( _( "Could not create directory '%s'" ), aHandler->GetClonePath() ) ); return false; } } git_clone_options cloneOptions; git_clone_init_options( &cloneOptions, GIT_CLONE_OPTIONS_VERSION ); cloneOptions.checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; cloneOptions.checkout_opts.progress_cb = clone_progress_cb; cloneOptions.checkout_opts.progress_payload = aHandler; cloneOptions.fetch_opts.callbacks.transfer_progress = transfer_progress_cb; cloneOptions.fetch_opts.callbacks.credentials = credentials_cb; cloneOptions.fetch_opts.callbacks.payload = aHandler; aHandler->TestedTypes() = 0; aHandler->ResetNextKey(); git_repository* newRepo = nullptr; wxString remote = common->m_remote; if( git_clone( &newRepo, remote.mbc_str(), aHandler->GetClonePath().mbc_str(), &cloneOptions ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Could not clone repository '%s'" ), remote ) ); return false; } common->SetRepo( newRepo ); return true; } CommitResult LIBGIT_BACKEND::Commit( GIT_COMMIT_HANDLER* aHandler, const std::vector& aFiles, const wxString& aMessage, const wxString& aAuthorName, const wxString& aAuthorEmail ) { git_repository* repo = aHandler->GetRepo(); if( !repo ) return CommitResult::Error; git_index* index = nullptr; if( git_repository_index( &index, repo ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to get repository index: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return CommitResult::Error; } KIGIT::GitIndexPtr indexPtr( index ); for( const wxString& file : aFiles ) { if( git_index_add_bypath( index, file.mb_str() ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to add file to index: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return CommitResult::Error; } } if( git_index_write( index ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to write index: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return CommitResult::Error; } git_oid tree_id; if( git_index_write_tree( &tree_id, index ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to write tree: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return CommitResult::Error; } git_tree* tree = nullptr; if( git_tree_lookup( &tree, repo, &tree_id ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to lookup tree: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return CommitResult::Error; } KIGIT::GitTreePtr treePtr( tree ); git_commit* parent = nullptr; if( git_repository_head_unborn( repo ) == 0 ) { git_reference* headRef = nullptr; if( git_repository_head( &headRef, repo ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to get HEAD reference: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return CommitResult::Error; } KIGIT::GitReferencePtr headRefPtr( headRef ); if( git_reference_peel( (git_object**) &parent, headRef, GIT_OBJECT_COMMIT ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to get commit: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return CommitResult::Error; } } KIGIT::GitCommitPtr parentPtr( parent ); git_signature* author = nullptr; if( git_signature_now( &author, aAuthorName.mb_str(), aAuthorEmail.mb_str() ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to create author signature: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return CommitResult::Error; } KIGIT::GitSignaturePtr authorPtr( author ); git_oid oid; size_t parentsCount = parent ? 1 : 0; #if( LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \ && ( LIBGIT2_VER_REVISION < 2 || LIBGIT2_VER_REVISION == 3 ) ) git_commit* const parents[1] = { parent }; git_commit** const parentsPtr = parent ? parents : nullptr; #else const git_commit* parents[1] = { parent }; const git_commit** parentsPtr = parent ? parents : nullptr; #endif if( git_commit_create( &oid, repo, "HEAD", author, author, nullptr, aMessage.mb_str(), tree, parentsCount, parentsPtr ) != 0 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to create commit: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return CommitResult::Error; } return CommitResult::Success; } PushResult LIBGIT_BACKEND::Push( GIT_PUSH_HANDLER* aHandler ) { KIGIT_COMMON* common = aHandler->GetCommon(); std::unique_lock lock( common->m_gitActionMutex, std::try_to_lock ); if(!lock.owns_lock()) { wxLogTrace(traceGit, "GIT_PUSH_HANDLER::PerformPush: Could not lock mutex"); return PushResult::Error; } PushResult result = PushResult::Success; git_remote* remote = nullptr; if(git_remote_lookup(&remote, aHandler->GetRepo(), "origin") != 0) { aHandler->AddErrorString(_("Could not lookup remote")); return PushResult::Error; } KIGIT::GitRemotePtr remotePtr(remote); git_remote_callbacks remoteCallbacks; git_remote_init_callbacks(&remoteCallbacks, GIT_REMOTE_CALLBACKS_VERSION); remoteCallbacks.sideband_progress = progress_cb; remoteCallbacks.transfer_progress = transfer_progress_cb; remoteCallbacks.update_tips = update_cb; remoteCallbacks.push_transfer_progress = push_transfer_progress_cb; remoteCallbacks.credentials = credentials_cb; remoteCallbacks.payload = aHandler; common->SetCancelled( false ); aHandler->TestedTypes() = 0; aHandler->ResetNextKey(); if( git_remote_connect( remote, GIT_DIRECTION_PUSH, &remoteCallbacks, nullptr, nullptr ) ) { aHandler->AddErrorString( wxString::Format( _( "Could not connect to remote: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return PushResult::Error; } git_push_options pushOptions; git_push_init_options( &pushOptions, GIT_PUSH_OPTIONS_VERSION ); pushOptions.callbacks = remoteCallbacks; git_reference* head = nullptr; if( git_repository_head( &head, aHandler->GetRepo() ) != 0 ) { git_remote_disconnect( remote ); aHandler->AddErrorString( _( "Could not get repository head" ) ); return PushResult::Error; } KIGIT::GitReferencePtr headPtr( head ); const char* refs[1]; refs[0] = git_reference_name( head ); const git_strarray refspecs = { (char**) refs, 1 }; if( git_remote_push( remote, &refspecs, &pushOptions ) ) { aHandler->AddErrorString( wxString::Format( _( "Could not push to remote: %s" ), KIGIT_COMMON::GetLastGitError() ) ); git_remote_disconnect( remote ); return PushResult::Error; } git_remote_disconnect( remote ); return result; } bool LIBGIT_BACKEND::HasChangedFiles( GIT_STATUS_HANDLER* aHandler ) { git_repository* repo = aHandler->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 LIBGIT_BACKEND::GetFileStatus( GIT_STATUS_HANDLER* aHandler, const wxString& aPathspec ) { std::map fileStatusMap; git_repository* repo = aHandler->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; std::string pathspec_str; std::vector pathspec_ptrs; if( !aPathspec.IsEmpty() ) { pathspec_str = aPathspec.ToStdString(); pathspec_ptrs.push_back( pathspec_str.c_str() ); status_options.pathspec.strings = const_cast( pathspec_ptrs.data() ); status_options.pathspec.count = pathspec_ptrs.size(); } 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 = aHandler->ConvertStatus( entry->status ); fileStatusMap[absPath] = fileStatus; } return fileStatusMap; } wxString LIBGIT_BACKEND::GetCurrentBranchName( GIT_STATUS_HANDLER* aHandler ) { git_repository* repo = aHandler->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 ) { return wxEmptyString; } else { wxLogTrace( traceGit, "Failed to lookup current branch: %s", KIGIT_COMMON::GetLastGitError() ); return wxEmptyString; } } void LIBGIT_BACKEND::UpdateRemoteStatus( GIT_STATUS_HANDLER* aHandler, const std::set& aLocalChanges, const std::set& aRemoteChanges, std::map& aFileStatus ) { git_repository* repo = aHandler->GetRepo(); if( !repo ) return; wxString repoWorkDir( git_repository_workdir( repo ) ); for( auto& [absPath, fileStatus] : aFileStatus ) { wxString relativePath = absPath; if( relativePath.StartsWith( repoWorkDir ) ) { relativePath = relativePath.Mid( repoWorkDir.length() ); #ifdef _WIN32 relativePath.Replace( wxS( "\\" ), wxS( "/" ) ); #endif } std::string relativePathStd = relativePath.ToStdString(); 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 LIBGIT_BACKEND::GetWorkingDirectory( GIT_STATUS_HANDLER* aHandler ) { git_repository* repo = aHandler->GetRepo(); if( !repo ) return wxEmptyString; const char* workdir = git_repository_workdir( repo ); if( !workdir ) return wxEmptyString; return wxString( workdir ); } wxString LIBGIT_BACKEND::GetWorkingDirectory( GIT_CONFIG_HANDLER* aHandler ) { git_repository* repo = aHandler->GetRepo(); if( !repo ) return wxEmptyString; const char* workdir = git_repository_workdir( repo ); if( !workdir ) return wxEmptyString; return wxString( workdir ); } bool LIBGIT_BACKEND::GetConfigString( GIT_CONFIG_HANDLER* aHandler, const wxString& aKey, wxString& aValue ) { git_repository* repo = aHandler->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; } bool LIBGIT_BACKEND::IsRepository( GIT_INIT_HANDLER* aHandler, 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 LIBGIT_BACKEND::InitializeRepository( GIT_INIT_HANDLER* aHandler, const wxString& aPath ) { if( IsRepository( aHandler, 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 ); aHandler->AddErrorString( wxString::Format( _( "Failed to initialize Git repository: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return InitResult::Error; } aHandler->GetCommon()->SetRepo( repo ); wxLogTrace( traceGit, "Successfully initialized Git repository at %s", aPath ); return InitResult::Success; } bool LIBGIT_BACKEND::SetupRemote( GIT_INIT_HANDLER* aHandler, const RemoteConfig& aConfig ) { if( aConfig.url.IsEmpty() ) return true; git_repository* repo = aHandler->GetRepo(); if( !repo ) { aHandler->AddErrorString( _( "No repository available to set up remote" ) ); return false; } aHandler->GetCommon()->SetUsername( aConfig.username ); aHandler->GetCommon()->SetPassword( aConfig.password ); aHandler->GetCommon()->SetSSHKey( aConfig.sshKey ); git_remote* remote = nullptr; wxString fullURL; 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( "@" ) ); } 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 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to create remote: %s" ), KIGIT_COMMON::GetLastGitError() ) ); return false; } wxLogTrace( traceGit, "Successfully set up remote origin" ); return true; } static bool lookup_branch_reference( git_repository* repo, const wxString& aBranchName, git_reference** aReference ) { if( git_reference_lookup( aReference, repo, aBranchName.mb_str() ) == GIT_OK ) return true; if( git_reference_dwim( aReference, repo, aBranchName.mb_str() ) == GIT_OK ) return true; return false; } BranchResult LIBGIT_BACKEND::SwitchToBranch( GIT_BRANCH_HANDLER* aHandler, const wxString& aBranchName ) { git_repository* repo = aHandler->GetRepo(); if( !repo ) { aHandler->AddErrorString( _( "No repository available" ) ); return BranchResult::Error; } git_reference* branchRef = nullptr; if( !lookup_branch_reference( repo, aBranchName, &branchRef ) ) { aHandler->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 ) { aHandler->AddErrorString( wxString::Format( _( "Failed to find branch head for '%s': %s" ), aBranchName, KIGIT_COMMON::GetLastGitError() ) ); return BranchResult::Error; } KIGIT::GitObjectPtr branchObjPtr( branchObj ); if( git_checkout_tree( repo, branchObj, nullptr ) != GIT_OK ) { aHandler->AddErrorString( wxString::Format( _( "Failed to switch to branch '%s': %s" ), aBranchName, KIGIT_COMMON::GetLastGitError() ) ); return BranchResult::CheckoutFailed; } if( git_repository_set_head( repo, branchRefName ) != GIT_OK ) { aHandler->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; } bool LIBGIT_BACKEND::BranchExists( GIT_BRANCH_HANDLER* aHandler, const wxString& aBranchName ) { git_repository* repo = aHandler->GetRepo(); if( !repo ) return false; git_reference* branchRef = nullptr; bool exists = lookup_branch_reference( repo, aBranchName, &branchRef ); if( branchRef ) git_reference_free( branchRef ); return exists; } // Use callbacks declared/implemented in kicad_git_common.h/.cpp bool LIBGIT_BACKEND::PerformFetch( GIT_PULL_HANDLER* aHandler, bool aSkipLock ) { if( !aHandler->GetRepo() ) { wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformFetch() - No repository found" ); return false; } std::unique_lock lock( aHandler->GetCommon()->m_gitActionMutex, std::try_to_lock ); if( !aSkipLock && !lock.owns_lock() ) { wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformFetch() - Could not lock mutex" ); return false; } git_remote* remote = nullptr; if( git_remote_lookup( &remote, aHandler->GetRepo(), "origin" ) != 0 ) { wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformFetch() - Failed to lookup remote 'origin'" ); aHandler->AddErrorString( wxString::Format( _( "Could not lookup remote '%s'" ), "origin" ) ); return false; } KIGIT::GitRemotePtr remotePtr( remote ); git_remote_callbacks remoteCallbacks; git_remote_init_callbacks( &remoteCallbacks, GIT_REMOTE_CALLBACKS_VERSION ); remoteCallbacks.sideband_progress = progress_cb; remoteCallbacks.transfer_progress = transfer_progress_cb; remoteCallbacks.credentials = credentials_cb; remoteCallbacks.payload = aHandler; aHandler->GetCommon()->SetCancelled( false ); aHandler->TestedTypes() = 0; aHandler->ResetNextKey(); if( git_remote_connect( remote, GIT_DIRECTION_FETCH, &remoteCallbacks, nullptr, nullptr ) ) { wxString errorMsg = KIGIT_COMMON::GetLastGitError(); wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformFetch() - Failed to connect to remote: %s", errorMsg ); aHandler->AddErrorString( wxString::Format( _( "Could not connect to remote '%s': %s" ), "origin", errorMsg ) ); return false; } git_fetch_options fetchOptions; git_fetch_init_options( &fetchOptions, GIT_FETCH_OPTIONS_VERSION ); fetchOptions.callbacks = remoteCallbacks; if( git_remote_fetch( remote, nullptr, &fetchOptions, nullptr ) ) { wxString errorMsg = KIGIT_COMMON::GetLastGitError(); wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformFetch() - Failed to fetch from remote: %s", errorMsg ); aHandler->AddErrorString( wxString::Format( _( "Could not fetch data from remote '%s': %s" ), "origin", errorMsg ) ); return false; } wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformFetch() - Fetch completed successfully" ); return true; } PullResult LIBGIT_BACKEND::PerformPull( GIT_PULL_HANDLER* aHandler ) { PullResult result = PullResult::Success; std::unique_lock lock( aHandler->GetCommon()->m_gitActionMutex, std::try_to_lock ); if( !lock.owns_lock() ) { wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformPull() - Could not lock mutex" ); return PullResult::Error; } if( !PerformFetch( aHandler, true ) ) return PullResult::Error; git_oid pull_merge_oid = {}; if( git_repository_fetchhead_foreach( aHandler->GetRepo(), fetchhead_foreach_cb, &pull_merge_oid ) ) { aHandler->AddErrorString( _( "Could not read 'FETCH_HEAD'" ) ); return PullResult::Error; } git_annotated_commit* fetchhead_commit; if( git_annotated_commit_lookup( &fetchhead_commit, aHandler->GetRepo(), &pull_merge_oid ) ) { aHandler->AddErrorString( _( "Could not lookup commit" ) ); return PullResult::Error; } KIGIT::GitAnnotatedCommitPtr fetchheadCommitPtr( fetchhead_commit ); const git_annotated_commit* merge_commits[] = { fetchhead_commit }; git_merge_analysis_t merge_analysis; git_merge_preference_t merge_preference = GIT_MERGE_PREFERENCE_NONE; if( git_merge_analysis( &merge_analysis, &merge_preference, aHandler->GetRepo(), merge_commits, 1 ) ) { aHandler->AddErrorString( _( "Could not analyze merge" ) ); return PullResult::Error; } if( merge_analysis & GIT_MERGE_ANALYSIS_UNBORN ) { aHandler->AddErrorString( _( "Invalid HEAD. Cannot merge." ) ); return PullResult::MergeFailed; } if( merge_analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE ) { wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformPull() - Repository is up to date" ); git_repository_state_cleanup( aHandler->GetRepo() ); return PullResult::UpToDate; } if( merge_analysis & GIT_MERGE_ANALYSIS_FASTFORWARD ) { wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformPull() - Fast-forward merge" ); return handleFastForward( aHandler ); } if( merge_analysis & GIT_MERGE_ANALYSIS_NORMAL ) { wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformPull() - Normal merge" ); git_config* config = nullptr; if( git_repository_config( &config, aHandler->GetRepo() ) != GIT_OK ) { wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformPull() - Failed to get repository config" ); aHandler->AddErrorString( _( "Could not access repository configuration" ) ); return PullResult::Error; } KIGIT::GitConfigPtr configPtr( config ); int rebase_value = 0; int ret = git_config_get_bool( &rebase_value, config, "pull.rebase" ); if( ret == GIT_OK && rebase_value ) { wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformPull() - Using rebase based on config" ); return handleRebase( aHandler, merge_commits, 1 ); } wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformPull() - Using merge based on config" ); return handleMerge( aHandler, merge_commits, 1 ); } wxLogTrace( traceGit, "GIT_PULL_HANDLER::PerformPull() - Merge needs resolution" ); return result; } PullResult LIBGIT_BACKEND::handleFastForward( GIT_PULL_HANDLER* aHandler ) { git_reference* rawRef = nullptr; if( git_repository_head( &rawRef, aHandler->GetRepo() ) ) { aHandler->AddErrorString( _( "Could not get repository head" ) ); return PullResult::Error; } KIGIT::GitReferencePtr headRef( rawRef ); git_oid updatedRefOid; const char* currentBranchName = git_reference_name( rawRef ); const char* branch_shorthand = git_reference_shorthand( rawRef ); wxString remote_name = aHandler->GetRemotename(); wxString remoteBranchName = wxString::Format( "refs/remotes/%s/%s", remote_name, branch_shorthand ); if( git_reference_name_to_id( &updatedRefOid, aHandler->GetRepo(), remoteBranchName.c_str() ) != GIT_OK ) { aHandler->AddErrorString( wxString::Format( _( "Could not get reference OID for reference '%s'" ), remoteBranchName ) ); return PullResult::Error; } git_commit* targetCommit = nullptr; if( git_commit_lookup( &targetCommit, aHandler->GetRepo(), &updatedRefOid ) != GIT_OK ) { aHandler->AddErrorString( _( "Could not look up target commit" ) ); return PullResult::Error; } KIGIT::GitCommitPtr targetCommitPtr( targetCommit ); git_tree* targetTree = nullptr; if( git_commit_tree( &targetTree, targetCommit ) != GIT_OK ) { git_commit_free( targetCommit ); aHandler->AddErrorString( _( "Could not get tree from target commit" ) ); return PullResult::Error; } KIGIT::GitTreePtr targetTreePtr( targetTree ); git_checkout_options checkoutOptions; git_checkout_init_options( &checkoutOptions, GIT_CHECKOUT_OPTIONS_VERSION ); auto notify_cb = []( git_checkout_notify_t why, const char* path, const git_diff_file* baseline, const git_diff_file* target, const git_diff_file* workdir, void* payload ) -> int { switch( why ) { case GIT_CHECKOUT_NOTIFY_CONFLICT: wxLogTrace( traceGit, "Checkout conflict: %s", path ? path : "unknown" ); break; case GIT_CHECKOUT_NOTIFY_DIRTY: wxLogTrace( traceGit, "Checkout dirty: %s", path ? path : "unknown" ); break; case GIT_CHECKOUT_NOTIFY_UPDATED: wxLogTrace( traceGit, "Checkout updated: %s", path ? path : "unknown" ); break; case GIT_CHECKOUT_NOTIFY_UNTRACKED: wxLogTrace( traceGit, "Checkout untracked: %s", path ? path : "unknown" ); break; case GIT_CHECKOUT_NOTIFY_IGNORED: wxLogTrace( traceGit, "Checkout ignored: %s", path ? path : "unknown" ); break; default: break; } return 0; }; checkoutOptions.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS; checkoutOptions.notify_flags = GIT_CHECKOUT_NOTIFY_ALL; checkoutOptions.notify_cb = notify_cb; if( git_checkout_tree( aHandler->GetRepo(), reinterpret_cast( targetTree ), &checkoutOptions ) != GIT_OK ) { aHandler->AddErrorString( _( "Failed to perform checkout operation." ) ); return PullResult::Error; } git_reference* updatedRef = nullptr; if( git_reference_set_target( &updatedRef, rawRef, &updatedRefOid, nullptr ) != GIT_OK ) { aHandler->AddErrorString( wxString::Format( _( "Failed to update reference '%s' to point to '%s'" ), currentBranchName, git_oid_tostr_s( &updatedRefOid ) ) ); return PullResult::Error; } KIGIT::GitReferencePtr updatedRefPtr( updatedRef ); if( git_repository_state_cleanup( aHandler->GetRepo() ) != GIT_OK ) { aHandler->AddErrorString( _( "Failed to clean up repository state after fast-forward." ) ); return PullResult::Error; } git_revwalk* revWalker = nullptr; if( git_revwalk_new( &revWalker, aHandler->GetRepo() ) != GIT_OK ) { aHandler->AddErrorString( _( "Failed to initialize revision walker." ) ); return PullResult::Error; } KIGIT::GitRevWalkPtr revWalkerPtr( revWalker ); git_revwalk_sorting( revWalker, GIT_SORT_TIME ); if( git_revwalk_push_glob( revWalker, currentBranchName ) != GIT_OK ) { aHandler->AddErrorString( _( "Failed to push reference to revision walker." ) ); return PullResult::Error; } std::pair>& branchCommits = aHandler->m_fetchResults.emplace_back(); branchCommits.first = currentBranchName; git_oid commitOid; while( git_revwalk_next( &commitOid, revWalker ) == GIT_OK ) { git_commit* commit = nullptr; if( git_commit_lookup( &commit, aHandler->GetRepo(), &commitOid ) ) { aHandler->AddErrorString( wxString::Format( _( "Could not lookup commit '%s'" ), git_oid_tostr_s( &commitOid ) ) ); return PullResult::Error; } KIGIT::GitCommitPtr commitPtr( commit ); CommitDetails details; details.m_sha = git_oid_tostr_s( &commitOid ); details.m_firstLine = getFirstLineFromCommitMessage( git_commit_message( commit ) ); details.m_author = git_commit_author( commit )->name; details.m_date = getFormattedCommitDate( git_commit_author( commit )->when ); branchCommits.second.push_back( details ); } return PullResult::FastForward; } PullResult LIBGIT_BACKEND::handleMerge( GIT_PULL_HANDLER* aHandler, const git_annotated_commit** aMergeHeads, size_t aMergeHeadsCount ) { if( git_merge( aHandler->GetRepo(), aMergeHeads, aMergeHeadsCount, nullptr, nullptr ) ) { aHandler->AddErrorString( _( "Merge failed" ) ); return PullResult::MergeFailed; } return PullResult::Success; } PullResult LIBGIT_BACKEND::handleRebase( GIT_PULL_HANDLER* aHandler, const git_annotated_commit** aMergeHeads, size_t aMergeHeadsCount ) { git_rebase_options rebase_opts; git_rebase_init_options( &rebase_opts, GIT_REBASE_OPTIONS_VERSION ); git_rebase* rebase = nullptr; if( git_rebase_init( &rebase, aHandler->GetRepo(), nullptr, aMergeHeads[0], nullptr, &rebase_opts ) ) { aHandler->AddErrorString( _( "Rebase failed to start" ) ); return PullResult::MergeFailed; } KIGIT::GitRebasePtr rebasePtr( rebase ); while( true ) { git_rebase_operation* op = nullptr; if( git_rebase_next( &op, rebase ) != 0 ) break; if( git_rebase_commit( nullptr, rebase, nullptr, nullptr, nullptr, nullptr ) ) { aHandler->AddErrorString( _( "Rebase commit failed" ) ); return PullResult::MergeFailed; } } if( git_rebase_finish( rebase, nullptr ) ) { aHandler->AddErrorString( _( "Rebase finish failed" ) ); return PullResult::MergeFailed; } return PullResult::Success; } void LIBGIT_BACKEND::PerformRevert( GIT_REVERT_HANDLER* aHandler ) { git_object* head_commit = NULL; git_checkout_options opts; git_checkout_init_options( &opts, GIT_CHECKOUT_OPTIONS_VERSION ); if( git_revparse_single( &head_commit, aHandler->m_repository, "HEAD" ) != 0 ) { return; } opts.checkout_strategy = GIT_CHECKOUT_FORCE; char** paths = new char*[aHandler->m_filesToRevert.size()]; for( size_t ii = 0; ii < aHandler->m_filesToRevert.size(); ii++ ) { paths[ii] = wxStrdup( aHandler->m_filesToRevert[ii].ToUTF8() ); } git_strarray arr = { paths, aHandler->m_filesToRevert.size() }; opts.paths = arr; opts.progress_cb = nullptr; opts.notify_cb = nullptr; opts.notify_payload = static_cast( aHandler ); if( git_checkout_tree( aHandler->m_repository, head_commit, &opts ) != 0 ) { const git_error* e = git_error_last(); if( e ) { wxLogTrace( traceGit, wxS( "Checkout failed: %d: %s" ), e->klass, e->message ); } } for( size_t ii = 0; ii < aHandler->m_filesToRevert.size(); ii++ ) delete( paths[ii] ); delete[] paths; git_object_free( head_commit ); } git_repository* LIBGIT_BACKEND::GetRepositoryForFile( const char* aFilename ) { git_repository* repo = nullptr; git_buf repo_path = GIT_BUF_INIT; if( git_repository_discover( &repo_path, aFilename, 0, nullptr ) != GIT_OK ) { wxLogTrace( traceGit, "Can't repo discover %s: %s", aFilename, KIGIT_COMMON::GetLastGitError() ); return nullptr; } KIGIT::GitBufPtr repo_path_ptr( &repo_path ); if( git_repository_open( &repo, repo_path.ptr ) != GIT_OK ) { wxLogTrace( traceGit, "Can't open repo for %s: %s", repo_path.ptr, KIGIT_COMMON::GetLastGitError() ); return nullptr; } return repo; } int LIBGIT_BACKEND::CreateBranch( git_repository* aRepo, const wxString& aBranchName ) { git_oid head_oid; if( int error = git_reference_name_to_id( &head_oid, aRepo, "HEAD" ); error != GIT_OK ) { wxLogTrace( traceGit, "Failed to lookup HEAD reference: %s", KIGIT_COMMON::GetLastGitError() ); return error; } git_commit* commit = nullptr; if( int error = git_commit_lookup( &commit, aRepo, &head_oid ); error != GIT_OK ) { wxLogTrace( traceGit, "Failed to lookup commit: %s", KIGIT_COMMON::GetLastGitError() ); return error; } KIGIT::GitCommitPtr commitPtr( commit ); git_reference* branchRef = nullptr; if( int error = git_branch_create( &branchRef, aRepo, aBranchName.mb_str(), commit, 0 ); error != GIT_OK ) { wxLogTrace( traceGit, "Failed to create branch: %s", KIGIT_COMMON::GetLastGitError() ); return error; } git_reference_free( branchRef ); return 0; } bool LIBGIT_BACKEND::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; } bool LIBGIT_BACKEND::AddToIndex( GIT_ADD_TO_INDEX_HANDLER* aHandler, const wxString& aFilePath ) { git_repository* repo = aHandler->GetRepo(); git_index* index = nullptr; size_t at_pos = 0; if( git_repository_index( &index, repo ) != 0 ) { wxLogError( "Failed to get repository index" ); return false; } KIGIT::GitIndexPtr indexPtr( index ); if( git_index_find( &at_pos, index, aFilePath.ToUTF8().data() ) == GIT_OK ) { wxLogError( "%s already in index", aFilePath ); return false; } aHandler->m_filesToAdd.push_back( aFilePath ); return true; } bool LIBGIT_BACKEND::PerformAddToIndex( GIT_ADD_TO_INDEX_HANDLER* aHandler ) { git_repository* repo = aHandler->GetRepo(); git_index* index = nullptr; aHandler->m_filesFailedToAdd.clear(); if( git_repository_index( &index, repo ) != 0 ) { wxLogError( "Failed to get repository index" ); std::copy( aHandler->m_filesToAdd.begin(), aHandler->m_filesToAdd.end(), std::back_inserter( aHandler->m_filesFailedToAdd ) ); return false; } KIGIT::GitIndexPtr indexPtr( index ); for( auto& file : aHandler->m_filesToAdd ) { if( git_index_add_bypath( index, file.ToUTF8().data() ) != 0 ) { wxLogError( "Failed to add %s to index", file ); aHandler->m_filesFailedToAdd.push_back( file ); continue; } } if( git_index_write( index ) != 0 ) { wxLogError( "Failed to write index" ); aHandler->m_filesFailedToAdd.clear(); std::copy( aHandler->m_filesToAdd.begin(), aHandler->m_filesToAdd.end(), std::back_inserter( aHandler->m_filesFailedToAdd ) ); return false; } return true; } bool LIBGIT_BACKEND::RemoveFromIndex( GIT_REMOVE_FROM_INDEX_HANDLER* aHandler, const wxString& aFilePath ) { git_repository* repo = aHandler->GetRepo(); git_index* index = nullptr; size_t at_pos = 0; if( git_repository_index( &index, repo ) != 0 ) { wxLogError( "Failed to get repository index" ); return false; } KIGIT::GitIndexPtr indexPtr( index ); if( git_index_find( &at_pos, index, aFilePath.ToUTF8().data() ) != 0 ) { wxLogError( "Failed to find index entry for %s", aFilePath ); return false; } aHandler->m_filesToRemove.push_back( aFilePath ); return true; } void LIBGIT_BACKEND::PerformRemoveFromIndex( GIT_REMOVE_FROM_INDEX_HANDLER* aHandler ) { git_repository* repo = aHandler->GetRepo(); for( auto& file : aHandler->m_filesToRemove ) { git_index* index = nullptr; git_oid oid; if( git_repository_index( &index, repo ) != 0 ) { wxLogError( "Failed to get repository index" ); return; } KIGIT::GitIndexPtr indexPtr( index ); if( git_index_remove_bypath( index, file.ToUTF8().data() ) != 0 ) { wxLogError( "Failed to remove index entry for %s", file ); return; } if( git_index_write( index ) != 0 ) { wxLogError( "Failed to write index" ); return; } if( git_index_write_tree( &oid, index ) != 0 ) { wxLogError( "Failed to write index tree" ); return; } } }