#include "gitplugin.hpp" #include "gitbranchmodel.hpp" #include "gitstatusmodel.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace EE::UI; using namespace EE::UI::Doc; using namespace std::literals; using json = nlohmann::json; #if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN || defined( __EMSCRIPTEN_PTHREADS__ ) #define GIT_THREADED 1 #else #define GIT_THREADED 0 #endif namespace ecode { static constexpr auto DEFAULT_HIGHLIGHT_COLOR = "var(--font-highlight)"sv; std::string GitPlugin::statusTypeToString( Git::GitStatusType type ) { switch ( type ) { case Git::GitStatusType::Untracked: return i18n( "git_untracked", "Untracked" ); case Git::GitStatusType::Unmerged: return i18n( "git_unmerged", "Unmerged" ); case Git::GitStatusType::Changed: return i18n( "git_changed", "Changed" ); case Git::GitStatusType::Staged: return i18n( "git_staged", "Staged" ); case Git::GitStatusType::Ignored: return i18n( "git_ignored", "Ignored" ); } return ""; } std::vector GitPlugin::repos() { Lock l( mReposMutex ); std::vector ret; for ( const auto& repo : mRepos ) ret.push_back( repo.first ); return ret; } Plugin* GitPlugin::New( PluginManager* pluginManager ) { return eeNew( GitPlugin, ( pluginManager, false ) ); } Plugin* GitPlugin::NewSync( PluginManager* pluginManager ) { return eeNew( GitPlugin, ( pluginManager, true ) ); } GitPlugin::GitPlugin( PluginManager* pluginManager, bool sync ) : PluginBase( pluginManager ), mHighlightStyleColor( DEFAULT_HIGHLIGHT_COLOR ) { if ( sync ) { load( pluginManager ); } else { #if defined( GIT_THREADED ) && GIT_THREADED == 1 mThreadPool->run( [&, pluginManager] { load( pluginManager ); } ); #else load( pluginManager ); #endif } } GitPlugin::~GitPlugin() { mShuttingDown = true; if ( mStatusButton ) mStatusButton->close(); if ( mSidePanel && mTab ) mSidePanel->removeTab( mTab ); endModelStyler(); { Lock l( mGitBranchMutex ); } { Lock l( mGitStatusMutex ); } { Lock l( mRepoMutex ); } { Lock l( mReposMutex ); } // TODO: Add a signal for this waits while ( mRunningUpdateStatus ) Sys::sleep( 1.f ); while ( mRunningUpdateBranches ) Sys::sleep( 1.f ); } void GitPlugin::load( PluginManager* pluginManager ) { AtomicBoolScopedOp loading( mLoading, true ); pluginManager->subscribeMessages( this, [this]( const auto& notification ) -> PluginRequestHandle { return processMessage( notification ); } ); std::string path = pluginManager->getPluginsPath() + "git.json"; if ( FileSystem::fileExists( path ) || FileSystem::fileWrite( path, "{\n \"config\":{},\n \"keybindings\":{}\n}\n" ) ) { mConfigPath = path; } std::string data; if ( !FileSystem::fileGet( path, data ) ) return; mConfigHash = String::hash( data ); json j; try { j = json::parse( data, nullptr, true, true ); } catch ( const json::exception& e ) { Log::error( "GitPlugin::load - Error parsing config from path %s, error: %s, config " "file content:\n%s", path.c_str(), e.what(), data.c_str() ); // Recreate it j = json::parse( "{\n \"config\":{},\n \"keybindings\":{},\n}\n", nullptr, true, true ); } bool updateConfigFile = false; if ( j.contains( "config" ) ) { auto& config = j["config"]; if ( config.contains( "ui_refresh_frequency" ) ) mRefreshFreq = Time::fromString( config.value( "ui_refresh_frequency", "5s" ) ); else { config["ui_refresh_frequency"] = mRefreshFreq.toString(); updateConfigFile = true; } if ( config.contains( "statusbar_display_branch" ) ) mStatusBarDisplayBranch = config.value( "statusbar_display_branch", true ); else { config["statusbar_display_branch"] = mStatusBarDisplayBranch; updateConfigFile = true; } if ( config.contains( "filetree_highlight_changes" ) ) mFileTreeHighlightChanges = config.value( "filetree_highlight_changes", true ); else { config["filetree_highlight_changes"] = mFileTreeHighlightChanges; updateConfigFile = true; } if ( config.contains( "filetree_highlight_style_color" ) ) { mHighlightStyleColor = config.value( "filetree_highlight_style_color", DEFAULT_HIGHLIGHT_COLOR ); } else { config["filetree_highlight_style_color"] = mHighlightStyleColor; updateConfigFile = true; } if ( config.contains( "statusbar_display_modifications" ) ) mStatusBarDisplayModifications = config.value( "statusbar_display_modifications", true ); else { config["statusbar_display_modifications"] = mStatusBarDisplayModifications; updateConfigFile = true; } if ( config.contains( "status_recurse_submodules" ) ) mStatusRecurseSubmodules = config.value( "status_recurse_submodules", true ); else { config["status_recurse_submodules"] = mStatusRecurseSubmodules; updateConfigFile = true; } if ( config.contains( "silent" ) ) mSilence = config.value( "silent", true ); else { config["silent"] = mSilence; updateConfigFile = true; } } if ( mKeyBindings.empty() ) { mKeyBindings["git-blame"] = "alt+shift+b"; } if ( j.contains( "keybindings" ) ) { auto& kb = j["keybindings"]; auto list = { "git-blame" }; for ( const auto& key : list ) { if ( kb.contains( key ) ) { if ( !kb[key].empty() ) mKeyBindings[key] = kb[key]; } else { kb[key] = mKeyBindings[key]; updateConfigFile = true; } } } if ( updateConfigFile ) { std::string newData = j.dump( 2 ); if ( newData != data ) { FileSystem::fileWrite( path, newData ); mConfigHash = String::hash( newData ); } } mGit = std::make_unique( pluginManager->getWorkspaceFolder() ); mGit->setLogLevel( mSilence ? LogLevel::Warning : LogLevel::Info ); mGitFound = !mGit->getGitPath().empty(); mProjectPath = mRepoSelected = mGit->getProjectPath(); if ( getUISceneNode() ) { initModelStyler(); updateStatus(); updateBranches(); } subscribeFileSystemListener(); mReady = true; fireReadyCbs(); setReady(); } void GitPlugin::initModelStyler() { if ( !mFileTreeHighlightChanges ) return; auto projectView = getUISceneNode()->getRoot()->find( "project_view" ); if ( !projectView || !projectView->getModel() ) return; mModelStylerId = projectView->getModel()->subsribeModelStyler( [this]( const ModelIndex& index, const void* data ) -> Variant { static const char* STYLE_MODIFIED = "git_highlight_style"; static const char* STYLE_NONE = "git_highlight_style_clear"; auto model = static_cast( index.model() ); auto node = static_cast( data ); Lock l( mGitStatusFileCacheMutex ); auto found = mGitStatusFilesCache.find( std::string{ model->getNodeRelativePath( node ) } ); if ( found != mGitStatusFilesCache.end() ) return Variant( STYLE_MODIFIED ); return Variant( STYLE_NONE ); } ); } void GitPlugin::endModelStyler() { if ( !mFileTreeHighlightChanges || mModelStylerId == 0 || !SceneManager::existsSingleton() || SceneManager::instance()->isShuttingDown() ) return; auto projectView = getUISceneNode()->getRoot()->find( "project_view" ); if ( !projectView || !projectView->getModel() ) return; projectView->getModel()->unsubsribeModelStyler( mModelStylerId ); } void GitPlugin::updateUINow( bool force ) { if ( !mGit || !getUISceneNode() ) return; updateStatus( force ); updateBranches(); } void GitPlugin::updateUI() { if ( !mGit || !getUISceneNode() ) return; getUISceneNode()->debounce( [this] { updateUINow(); }, mRefreshFreq, String::hash( "git::status-update" ) ); } void GitPlugin::updateStatusBarSync() { buildSidePanelTab(); mGitContentView->setVisible( !mGit->getGitFolder().empty() ) ->setEnabled( !mGit->getGitFolder().empty() ); mGitNoContentView->setVisible( !mGitContentView->isVisible() ) ->setEnabled( !mGitContentView->isEnabled() ); if ( !mGit->getGitFolder().empty() ) { { Lock l( mGitStatusMutex ); mStatusTree->setModel( GitStatusModel::asModel( mGitStatus.files, this ) ); } mStatusTree->expandAll(); } else { return; } if ( !mStatusBarDisplayBranch ) return; if ( !mStatusBar ) getUISceneNode()->bind( "status_bar", mStatusBar ); if ( !mStatusBar ) return; if ( !mStatusButton ) { mStatusButton = UIPushButton::New(); mStatusButton->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::MatchParent ); mStatusButton->setParent( mStatusBar ); mStatusButton->setId( "git_status" ); mStatusButton->setClass( "status_but" ); mStatusButton->setIcon( iconDrawable( "source-control", 10 ) ); mStatusButton->reloadStyle( true, true ); mStatusButton->getTextBox()->setUsingCustomStyling( true ); auto childCount = mStatusBar->getChildCount(); if ( childCount > 2 ) mStatusButton->toPosition( mStatusBar->getChildCount() - 2 ); mStatusButton->on( Event::MouseClick, [this]( const Event* event ) { if ( nullptr == mTab ) return; mTab->setTabSelected(); if ( event->asMouseEvent()->getFlags() & EE_BUTTON_RMASK ) mPanelSwicher->getListBox()->setSelected( 0 ); else if ( mGitStatus.totalInserts || mGitStatus.totalDeletions ) mPanelSwicher->getListBox()->setSelected( 1 ); } ); } mStatusButton->setVisible( !mGit->getGitFolder().empty() ); if ( mGit->getGitFolder().empty() ) return; std::string text; { Lock l( mGitStatusMutex ); text = mStatusBarDisplayModifications && ( mGitStatus.totalInserts || mGitStatus.totalDeletions ) ? String::format( "%s (+%d / -%d)", gitBranch().c_str(), mGitStatus.totalInserts, mGitStatus.totalDeletions ) : gitBranch(); } mStatusButton->setText( text ); if ( !mStatusBarDisplayModifications ) return; if ( !mStatusCustomTokenizer.has_value() ) { std::vector patterns; auto fontColor( getVarColor( "--font" ) ); auto insertedColor( getVarColor( "--theme-success" ) ); auto deletedColor( getVarColor( "--theme-error" ) ); patterns.emplace_back( SyntaxPattern( { ".*%((%+%d+)%s/%s(%-%d+)%)" }, { "normal", "keyword", "keyword2" } ) ); SyntaxDefinition syntaxDef( "custom_build", {}, std::move( patterns ) ); SyntaxColorScheme scheme( "status_bar_git", { { "normal"_sst, { fontColor } }, { "keyword"_sst, { insertedColor } }, { "keyword2"_sst, { deletedColor } } }, {} ); mStatusCustomTokenizer = { std::move( syntaxDef ), std::move( scheme ) }; } SyntaxTokenizer::tokenizeText( mStatusCustomTokenizer->def, mStatusCustomTokenizer->scheme, *mStatusButton->getTextBox()->getTextCache() ); mStatusButton->invalidateDraw(); } void GitPlugin::updateStatus( bool force ) { if ( !mGit || !mGitFound || mRunningUpdateStatus ) return; mRunningUpdateStatus++; mThreadPool->run( [this, force] { if ( !mGit || mGit->getGitFolder().empty() ) { getUISceneNode()->runOnMainThread( [this] { updateStatusBarSync(); } ); return; } auto prevBranch = updateReposBranches(); Git::Status prevGitStatus; { Lock l( mGitStatusMutex ); prevGitStatus = mGitStatus; } Git::Status newGitStatus = mGit->status( mStatusRecurseSubmodules ); UnorderedSet cache; for ( const auto& status : newGitStatus.files ) { for ( const auto& file : status.second ) { std::string p( FileSystem::fileRemoveFileName( file.file ) ); std::string lp; while ( p != lp ) { cache.insert( p ); lp = p; p = FileSystem::removeLastFolderFromPath( p ); } cache.insert( file.file ); } } { Lock l( mGitStatusFileCacheMutex ); mGitStatusFilesCache = std::move( cache ); } { Lock l( mGitStatusMutex ); mGitStatus = std::move( newGitStatus ); if ( !force && mGitBranches == prevBranch && mGitStatus == prevGitStatus ) return; } getUISceneNode()->runOnMainThread( [this] { updateStatusBarSync(); } ); }, [this]( auto ) { mRunningUpdateStatus--; } ); } PluginRequestHandle GitPlugin::processMessage( const PluginMessage& msg ) { switch ( msg.type ) { case PluginMessageType::WorkspaceFolderChanged: { if ( mGit ) { mGit->setProjectPath( msg.asJSON()["folder"] ); { Lock l( mGitBranchMutex ); mGitBranches.clear(); } { Lock l( mRepoMutex ); mProjectPath = mRepoSelected = mGit->getProjectPath(); } { Lock l( mReposMutex ); mRepos.clear(); } updateUINow( true ); mInitialized = true; } break; } case ecode::PluginMessageType::UIReady: { if ( !mInitialized ) updateUINow(); if ( mModelStylerId == 0 ) initModelStyler(); break; } case ecode::PluginMessageType::UIThemeReloaded: { mStatusCustomTokenizer.reset(); updateUINow( true ); break; } default: break; } return PluginRequestHandle::empty(); } void GitPlugin::onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) { PluginBase::onFileSystemEvent( ev, file ); if ( mShuttingDown || isLoading() ) return; if ( String::startsWith( file.getFilepath(), mGit->getGitFolder() ) && ( file.getExtension() == "lock" || file.isDirectory() ) ) return; updateUI(); } void GitPlugin::displayTooltip( UICodeEditor* editor, const Git::Blame& blame, const Vector2f& position ) { // HACK: Gets the old font style to restore it when the tooltip is hidden UITooltip* tooltip = editor->createTooltip(); if ( tooltip == nullptr ) return; String str( blame.error.empty() ? String::format( "%s: %s (%s)\n%s: %s (%s)\n%s: %s\n\n%s", i18n( "commit", "Commit" ).toUtf8().c_str(), blame.commitHash.c_str(), blame.commitShortHash.c_str(), i18n( "author", "Author" ).toUtf8().c_str(), blame.author.c_str(), blame.authorEmail.c_str(), i18n( "date", "Date" ).toUtf8().c_str(), blame.date.c_str(), blame.commitMessage.c_str() ) : blame.error ); Text::wrapText( str, PixelDensity::dpToPx( 400 ), tooltip->getFontStyleConfig(), editor->getTabWidth() ); editor->setTooltipText( str ); mTooltipInfoShowing = true; mOldBackgroundColor = tooltip->getBackgroundColor(); if ( Color::Transparent == mOldBackgroundColor ) { tooltip->reloadStyle( true, true, true, true ); mOldBackgroundColor = tooltip->getBackgroundColor(); } mOldTextStyle = tooltip->getFontStyle(); mOldTextAlign = tooltip->getHorizontalAlign(); mOldDontAutoHideOnMouseMove = tooltip->dontAutoHideOnMouseMove(); mOldUsingCustomStyling = tooltip->getUsingCustomStyling(); tooltip->setHorizontalAlign( UI_HALIGN_LEFT ); tooltip->setPixelsPosition( tooltip->getTooltipPosition( position ) ); tooltip->setDontAutoHideOnMouseMove( true ); tooltip->setUsingCustomStyling( true ); tooltip->setData( String::hash( "git" ) ); tooltip->setBackgroundColor( editor->getColorScheme().getEditorColor( "background"_sst ) ); tooltip->getUIStyle()->setStyleSheetProperty( StyleSheetProperty( "background-color", editor->getColorScheme().getEditorColor( "background"_sst ).toHexString(), true, StyleSheetSelectorRule::SpecificityImportant ) ); if ( !mTooltipCustomSyntaxDef.has_value() ) { static std::vector patterns; patterns.emplace_back( SyntaxPattern( { "([%w:]+)%s(%x+)%s%((%x+)%)" }, { "normal", "keyword", "number", "number" } ) ); patterns.emplace_back( SyntaxPattern( { "([%w:]+)%s(.*)%(([%w%+%.-]+@[%w%.-]+%.%w+)%)" }, { "normal", "keyword", "function", "link" } ) ); patterns.emplace_back( SyntaxPattern( { "([%w:]+)%s(%d%d%d%d%-%d%d%-%d%d[%s%d%-+:]+)" }, { "normal", "keyword", "warning" } ) ); SyntaxDefinition syntaxDef( "custom_build", {}, std::move( patterns ) ); mTooltipCustomSyntaxDef = std::move( syntaxDef ); } SyntaxTokenizer::tokenizeText( *mTooltipCustomSyntaxDef, editor->getColorScheme(), *tooltip->getTextCache() ); tooltip->notifyTextChangedFromTextCache(); if ( editor->hasFocus() && !tooltip->isVisible() ) tooltip->show(); } void GitPlugin::hideTooltip( UICodeEditor* editor ) { mTooltipInfoShowing = false; UITooltip* tooltip = nullptr; if ( editor && ( tooltip = editor->getTooltip() ) && tooltip->isVisible() && tooltip->getData() == String::hash( "git" ) ) { editor->setTooltipText( "" ); tooltip->hide(); // Restore old tooltip state tooltip->setData( 0 ); tooltip->setFontStyle( mOldTextStyle ); tooltip->setHorizontalAlign( mOldTextAlign ); tooltip->setUsingCustomStyling( mOldUsingCustomStyling ); tooltip->setDontAutoHideOnMouseMove( mOldDontAutoHideOnMouseMove ); tooltip->setBackgroundColor( mOldBackgroundColor ); } } bool GitPlugin::onMouseLeave( UICodeEditor* editor, const Vector2i&, const Uint32& ) { hideTooltip( editor ); return false; } std::string GitPlugin::gitBranch() { std::string repoSel = repoSelected(); Lock l( mGitBranchMutex ); return mGitBranches[repoSel]; } void GitPlugin::onRegisterListeners( UICodeEditor* editor, std::vector& listeners ) { listeners.push_back( editor->addEventListener( Event::OnCursorPosChange, [this, editor]( const Event* ) { if ( mTooltipInfoShowing ) hideTooltip( editor ); } ) ); } void GitPlugin::onBeforeUnregister( UICodeEditor* editor ) { for ( auto& kb : mKeyBindings ) editor->getKeyBindings().removeCommandKeybind( kb.first ); } void GitPlugin::onUnregisterDocument( TextDocument* doc ) { for ( auto& kb : mKeyBindings ) doc->removeCommand( kb.first ); } Color GitPlugin::getVarColor( const std::string& var ) { return Color::fromString( getUISceneNode()->getRoot()->getUIStyle()->getVariable( var ).getValue() ); } void GitPlugin::blame( UICodeEditor* editor ) { if ( !mGitFound ) { editor->setTooltipText( i18n( "git_not_found", "Git binary not found.\nPlease check that git is accesible via PATH" ) ); return; } mThreadPool->run( [this, editor]() { auto blame = mGit->blame( editor->getDocument().getFilePath(), editor->getDocument().getSelection().start().line() + 1 ); editor->runOnMainThread( [this, editor, blame] { displayTooltip( editor, blame, editor->getScreenPosition( editor->getDocument().getSelection().start() ) .getPosition() ); } ); } ); } // Branch operations void GitPlugin::checkout( Git::Branch branch ) { if ( !mGit ) return; const auto checkOutFn = [this, branch]( bool createLocal ) { mLoader->setVisible( true ); mThreadPool->run( [this, branch, createLocal] { auto result = createLocal ? mGit->checkoutAndCreateLocalBranch( branch.name, "", repoSelected() ) : mGit->checkout( branch.name, repoSelected() ); if ( result.success() ) { { std::string repoSel = repoSelected(); Lock l( mGitBranchMutex ); mGitBranches[repoSel] = branch.name; } if ( mBranchesTree->getModel() ) { if ( createLocal ) updateBranches(); else mBranchesTree->getModel()->invalidate( Model::DontInvalidateIndexes ); } } else { showMessage( LSPMessageType::Warning, result.result ); } getUISceneNode()->runOnMainThread( [this] { mLoader->setVisible( false ); } ); } ); }; if ( branch.type == Git::RefType::Remote ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::YES_NO, i18n( "git_create_local_branch", "Create local branch?" ) ); msgBox->on( Event::OnConfirm, [checkOutFn]( const Event* ) { checkOutFn( true ); } ); msgBox->on( Event::OnCancel, [checkOutFn]( const Event* ) { checkOutFn( false ); } ); msgBox->setTitle( i18n( "git_checkout", "Check Out" ) ); msgBox->center(); msgBox->showWhenReady(); return; } checkOutFn( false ); } void GitPlugin::branchRename( Git::Branch branch ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::INPUT, String::format( i18n( "git_rename_branch_ask", "Enter the new name for the branch: '%s'" ).toUtf8(), branch.name ) ); msgBox->on( Event::OnConfirm, [this, branch, msgBox]( const Event* ) { std::string newName( msgBox->getTextInput()->getText().toUtf8() ); if ( newName.empty() || branch.name == newName ) return; msgBox->closeWindow(); runAsync( [this, branch, newName]() { return mGit->renameBranch( branch.name, newName, repoSelected() ); }, false, true ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_rename_branch", "Rename Branch" ) ); msgBox->center(); msgBox->getTextInput()->setText( branch.name ); msgBox->showWhenReady(); } void GitPlugin::branchDelete( Git::Branch branch ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, String::format( i18n( "git_confirm_branch_delete", "Are you sure you want to delete the branch \"%s\"?" ) .toUtf8(), branch.name ) ); msgBox->on( Event::OnConfirm, [this, branch]( auto ) { runAsync( [this, branch]() { return mGit->deleteBranch( branch.name, repoSelected() ); }, false, true ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_confirm", "Confirm" ) ); msgBox->center(); msgBox->showWhenReady(); } void GitPlugin::branchMerge( Git::Branch branch ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, String::format( i18n( "git_confirm_branch_merge", "Are you sure you want to merge from branch \"%s\"?" ) .toUtf8(), branch.name ) ); msgBox->on( Event::OnConfirm, [this, branch]( auto ) { runAsync( [this, branch]() { return mGit->mergeBranch( branch.name, repoSelected() ); }, true, true, true, true, true ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_confirm", "Confirm" ) ); msgBox->center(); msgBox->showWhenReady(); } void GitPlugin::pull( const std::string& repoPath ) { runAsync( [this, repoPath]() { return mGit->pull( repoPath ); }, true, true, true ); } void GitPlugin::push( const std::string& repoPath ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, i18n( "git_confirm_push_changes", "Are you sure you want to push the local changes to the remote server?" ) ); msgBox->on( Event::OnConfirm, [this, repoPath]( auto ) { runAsync( [this, repoPath]() { std::optional branch = getBranchFromRepoPath( repoPath ); bool pushNewBranch = branch && !branch->name.empty() && branch->remote.empty(); if ( pushNewBranch ) return mGit->pushNewBranch( branch->name, repoPath ); return mGit->push( repoPath ); }, true, true, true, true ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_confirm", "Confirm" ) ); msgBox->center(); msgBox->showWhenReady(); } void GitPlugin::fetch( const std::string& repoPath ) { runAsync( [this, repoPath]() { return mGit->fetch( repoPath ); }, true, true, true ); } void GitPlugin::branchCreate() { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::INPUT, i18n( "git_create_branch_ask", "Create new branch at current branch (HEAD).\nEnter the name for the branch:" ) ); msgBox->on( Event::OnConfirm, [this, msgBox]( const Event* ) { std::string newName( msgBox->getTextInput()->getText().toUtf8() ); if ( newName.empty() ) return; msgBox->closeWindow(); runAsync( [this, newName]() { return mGit->createBranch( newName, true, repoSelected() ); }, false, true ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_add_branch", "Add Branch" ) ); msgBox->center(); msgBox->showWhenReady(); } void GitPlugin::commit( const std::string& repoPath ) { if ( !mGitStatus.hasStagedChanges( mGit->repoName( repoPath, true ) ) ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK, i18n( "git_nothing_to_commit", "Nothing to Commit" ) ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_nothing_to_commit", "Nothing to Commit" ) ); msgBox->center(); msgBox->showWhenReady(); return; } UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::TEXT_EDIT, i18n( "git_commit_message", "Commit Message:" ) ); UITextEdit* txtEdit = msgBox->getTextEdit(); txtEdit->setText( mLastCommitMsg ); UICheckBox* chkAmmend = UICheckBox::New(); chkAmmend->setLayoutMargin( Rectf( 0, 8, 0, 0 ) ) ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) ->setLayoutGravity( UI_HALIGN_LEFT | UI_VALIGN_CENTER ) ->setClipType( ClipType::None ) ->setParent( msgBox->getLayoutCont()->getFirstChild() ) ->setId( "git-ammend" ); chkAmmend->setText( i18n( "git_ammend", "Ammend last commit" ) ); chkAmmend->toPosition( 2 ); chkAmmend->setTooltipText( getUISceneNode()->getKeyBindings().getShortcutString( { KEY_A, KeyMod::getDefaultModifier() }, true ) ); UICheckBox* chkBypassHook = UICheckBox::New(); chkBypassHook->setLayoutMargin( Rectf( 0, 8, 0, 0 ) ) ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) ->setLayoutGravity( UI_HALIGN_LEFT | UI_VALIGN_CENTER ) ->setClipType( ClipType::None ) ->setParent( msgBox->getLayoutCont()->getFirstChild() ) ->setId( "git-bypass-hook" ); chkBypassHook->setText( i18n( "git_bypass_hook", "Bypass commit hook" ) ); chkBypassHook->toPosition( 3 ); chkBypassHook->setTooltipText( getUISceneNode()->getKeyBindings().getShortcutString( { KEY_B, KeyMod::getDefaultModifier() }, true ) ); UICheckBox* chkPush = UICheckBox::New(); chkPush->setLayoutMargin( Rectf( 0, 8, 0, 0 ) ) ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) ->setLayoutGravity( UI_HALIGN_LEFT | UI_VALIGN_CENTER ) ->setClipType( ClipType::None ) ->setParent( msgBox->getLayoutCont()->getFirstChild() ) ->setId( "git-push-commit" ); chkPush->setText( i18n( "git_push_commit", "Push commit" ) ); chkPush->toPosition( 4 ); chkPush->setTooltipText( getUISceneNode()->getKeyBindings().getShortcutString( { KEY_P, KeyMod::getDefaultModifier() }, true ) ); txtEdit->getDocument().setCommand( "commit-ammend", [chkAmmend] { chkAmmend->setChecked( !chkAmmend->isChecked() ); } ); txtEdit->getKeyBindings().addKeybind( { KEY_L, KeyMod::getDefaultModifier() }, "commit-ammend" ); txtEdit->getDocument().setCommand( "commit-push", [chkPush] { chkPush->setChecked( !chkPush->isChecked() ); } ); txtEdit->getKeyBindings().addKeybind( { KEY_P, KeyMod::getDefaultModifier() }, "commit-push" ); txtEdit->getDocument().setCommand( "commit-bypass-hook", [chkBypassHook] { chkBypassHook->setChecked( !chkBypassHook->isChecked() ); } ); txtEdit->getKeyBindings().addKeybind( { KEY_B, KeyMod::getDefaultModifier() }, "commit-bypass-hook" ); msgBox->on( Event::OnConfirm, [this, msgBox, chkAmmend, chkBypassHook, chkPush, repoPath]( const Event* ) { std::string msg( msgBox->getTextEdit()->getText().toUtf8() ); if ( msg.empty() ) return; bool ammend = chkAmmend->isChecked(); bool bypassHook = chkBypassHook->isChecked(); bool pushCommit = chkPush->isChecked(); msgBox->closeWindow(); runAsync( [this, msg, ammend, bypassHook, pushCommit, repoPath]() { std::optional branch = getBranchFromRepoPath( repoPath ); bool pushNewBranch = branch && !branch->name.empty() && branch->remote.empty(); auto res = mGit->commit( msg, ammend, bypassHook, repoPath ); if ( res.success() ) { mLastCommitMsg.clear(); if ( pushCommit ) { if ( pushNewBranch ) return mGit->pushNewBranch( branch->name, repoPath ); return mGit->push( repoPath ); } } else mLastCommitMsg = msg; return res; }, true, true, true, true, true ); } ); msgBox->on( Event::OnCancel, [this, msgBox]( const Event* ) { mLastCommitMsg = msgBox->getTextEdit()->getText(); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_commit", "Commit" ) ); msgBox->center(); msgBox->showWhenReady(); } void GitPlugin::fastForwardMerge( Git::Branch branch ) { runAsync( [this, branch]() { if ( branch.name == gitBranch() ) return mGit->fastForwardMerge( repoSelected() ); auto remoteBranch = mGit->getAllBranchesAndTags( Git::RefType::Remote, "refs/remotes/" + branch.remote, repoSelected() ); if ( remoteBranch.empty() ) return Git::Result{ "", -1 }; return mGit->updateRef( branch.name, remoteBranch[0].lastCommit, repoSelected() ); }, false, true ); } // Branch operations // File operations static bool isPath( const std::string& file ) { bool ret = !file.empty() && file[0] == '/'; #if EE_PLATFORM == EE_PLATFORM_WIN if ( !ret ) ret = LuaPattern::matches( file, "%w:[\\/][\\/]" ); #endif return ret; } std::string GitPlugin::fixFilePath( const std::string& file ) { std::string path; if ( !isPath( file ) ) { path = ( mProjectPath + file ); } return path; } std::vector GitPlugin::fixFilePaths( const std::vector& files ) { std::vector paths; paths.reserve( files.size() ); for ( const auto& file : files ) { if ( !isPath( file ) ) { paths.push_back( mProjectPath + file ); } else { paths.push_back( file ); } } return paths; } std::optional GitPlugin::getBranchFromRepoPath( const std::string& repoPath ) { Git::Branch branch; std::string branchName; if ( repoPath.empty() ) return {}; { Lock l( mGitBranchMutex ); branchName = mGitBranches[repoPath]; } if ( branchName.empty() ) return {}; if ( repoPath != repoSelected() || !mBranchesTree->getModel() ) { auto branches = mGit->getAllBranchesAndTags( Git::RefType::Head, "refs/heads/" + branchName, repoPath ); if ( !branches.empty() ) return branches.front(); } else { auto modelShared = mBranchesTree->getModelShared(); auto model = static_cast( modelShared.get() ); return model->branch( branchName ); } return {}; } void GitPlugin::stage( const std::vector& files ) { if ( files.empty() ) return; runAsync( [this, files]() { return mGit->add( fixFilePaths( files ), mGit->repoPath( files[0] ) ); }, true, false ); } void GitPlugin::unstage( const std::vector& files ) { if ( files.empty() ) return; runAsync( [this, files]() { return mGit->reset( fixFilePaths( files ), mGit->repoPath( files[0] ) ); }, true, false ); } void GitPlugin::discard( const std::vector& files ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, i18n( "git_confirm_discard_changes", "Are you sure you want to discard all file changes?" ) .toUtf8() ); msgBox->on( Event::OnConfirm, [this, files]( auto ) { runAsync( [this, files]() { return mGit->restore( fixFilePaths( files ), mGit->repoPath( files[0] ) ); }, true, false ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_confirm", "Confirm" ) ); msgBox->center(); msgBox->showWhenReady(); } void GitPlugin::discard( const std::string& file ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, String::format( i18n( "git_confirm_discard_changes", "Are you sure you want to discard the changes in file: \"%s\"?" ) .toUtf8(), file ) ); msgBox->on( Event::OnConfirm, [this, file]( auto ) { runAsync( [this, file]() { return mGit->restore( fixFilePath( file ), mGit->repoPath( file ) ); }, true, false ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_confirm", "Confirm" ) ); msgBox->center(); msgBox->showWhenReady(); } void GitPlugin::openFile( const std::string& file ) { getUISceneNode()->runOnMainThread( [this, file] { mManager->getLoadFileFn()( mGit->getProjectPath() + file, []( auto, auto ) {} ); } ); } void GitPlugin::diff( const Git::DiffMode mode, const std::string& repoPath ) { mThreadPool->run( [this, mode, repoPath] { auto res = mGit->diff( mode, repoPath ); if ( res.fail() ) return; std::string repoName = this->repoName( repoPath ); getUISceneNode()->runOnMainThread( [this, mode, res, repoName] { auto ret = mManager->getSplitter()->createEditorInNewTab(); auto doc = ret.second->getDocumentRef(); std::string modeName; switch ( mode ) { case Git::DiffHead: { modeName = "HEAD"; break; } case Git::DiffStaged: modeName = "staged"; break; } doc->setDefaultFileName( repoName + "-" + modeName + ".diff" ); doc->setSyntaxDefinition( SyntaxDefinitionManager::instance()->getByLSPName( "diff" ) ); doc->textInput( res.result, false ); doc->moveToStartOfDoc(); doc->resetUndoRedo(); } ); } ); } void GitPlugin::diff( const std::string& file, bool isStaged ) { mThreadPool->run( [this, file, isStaged] { auto res = mGit->diff( fixFilePath( file ), isStaged, mGit->repoPath( file ) ); if ( res.fail() ) return; getUISceneNode()->runOnMainThread( [this, file, res] { auto ret = mManager->getSplitter()->createEditorInNewTab(); auto doc = ret.second->getDocumentRef(); doc->setDefaultFileName( FileSystem::fileNameFromPath( file ) + ".diff" ); doc->setSyntaxDefinition( SyntaxDefinitionManager::instance()->getByLSPName( "diff" ) ); doc->textInput( res.result, false ); doc->moveToStartOfDoc(); doc->resetUndoRedo(); } ); } ); } // File operations // Stash operations void GitPlugin::stashPush( const std::vector& files, const std::string& repoPath ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::INPUT, i18n( "git_stash_push", "Stash Local Changes\nName your stash (optional):" ) ); UIRadioButton* rKeepIndex = UIRadioButton::New(); rKeepIndex->setLayoutMargin( Rectf( 0, 8, 0, 0 ) ) ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) ->setLayoutGravity( UI_HALIGN_LEFT | UI_VALIGN_CENTER ) ->setClipType( ClipType::None ) ->setParent( msgBox->getLayoutCont()->getFirstChild() ) ->setId( "git-stash-keep-index" ); rKeepIndex->setText( i18n( "git_stash_keep_index", "Keep Index" ) ); rKeepIndex->toPosition( 2 ); UIRadioButton* rKeepWorkingTree = UIRadioButton::New(); rKeepWorkingTree->setLayoutMargin( Rectf( 0, 8, 0, 0 ) ) ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) ->setLayoutGravity( UI_HALIGN_LEFT | UI_VALIGN_CENTER ) ->setClipType( ClipType::None ) ->setParent( msgBox->getLayoutCont()->getFirstChild() ) ->setId( "git-stash-keep-working-tree" ); rKeepWorkingTree->setText( i18n( "git_stash_keep_working_tree", "Keep Working Tree" ) ); rKeepWorkingTree->toPosition( 3 ); msgBox->on( Event::OnConfirm, [this, msgBox, rKeepIndex, rKeepWorkingTree, files, repoPath]( const Event* ) { bool keepIndex = rKeepIndex->isActive(); bool keepWorkingTree = rKeepWorkingTree->isActive(); std::string message = msgBox->getTextInput()->getText().toUtf8(); String::trimInPlace( message ); String::trimInPlace( message, '\n' ); msgBox->closeWindow(); runAsync( [this, files, keepIndex, keepWorkingTree, repoPath, message]() { auto res = mGit->stashPush( fixFilePaths( files ), message, keepIndex, repoPath ); if ( res.success() && keepWorkingTree ) mGit->stashApply( "stash@{0}", true, repoPath ); return res; }, true, true ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_stash_save", "Save Stash" ) ); msgBox->center(); msgBox->showWhenReady(); } void GitPlugin::stashApply( const Git::Branch& branch ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, i18n( "git_confirm_apply_stash", "Apply a previously saved stash?" ).toUtf8() ); UICheckBox* chkIndex = UICheckBox::New(); chkIndex->setLayoutMargin( Rectf( 0, 8, 0, 0 ) ) ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) ->setLayoutGravity( UI_HALIGN_LEFT | UI_VALIGN_CENTER ) ->setClipType( ClipType::None ) ->setParent( msgBox->getLayoutCont()->getFirstChild() ) ->setId( "git-restore-index" ); chkIndex->setText( i18n( "git_restore_index", "Restore Index" ) ); chkIndex->toPosition( 2 ); chkIndex->setChecked( true ); msgBox->on( Event::OnConfirm, [this, branch, chkIndex]( auto ) { runAsync( [this, branch, chkIndex]() { return mGit->stashApply( branch.remote, chkIndex->isChecked(), repoSelected() ); }, true, true ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_apply_stash_title", "Apply Stash" ) ); msgBox->center(); msgBox->showWhenReady(); } void GitPlugin::stashDrop( const Git::Branch& branch ) { UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, i18n( "git_confirm_drop_stash", "Do you want to drop the selected stash?" ).toUtf8() ); msgBox->on( Event::OnConfirm, [this, branch]( auto ) { runAsync( [this, branch]() { return mGit->stashDrop( branch.remote, repoSelected() ); }, true, true ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_drop_stash_title", "Drop Stash" ) ); msgBox->center(); msgBox->showWhenReady(); } // Stash operations void GitPlugin::onRegister( UICodeEditor* editor ) { PluginBase::onRegister( editor ); for ( auto& kb : mKeyBindings ) { if ( !kb.second.empty() ) editor->getKeyBindings().addKeybindString( kb.second, kb.first ); } if ( !editor->hasDocument() ) return; auto& doc = editor->getDocument(); doc.setCommand( "git-blame", [this]( TextDocument::Client* client ) { blame( static_cast( client ) ); } ); doc.setCommand( "show-source-control-tab", [this]() { if ( mTab ) mTab->setTabSelected(); } ); doc.setCommand( "git-pull", [this] { pull( projectPath() ); } ); doc.setCommand( "git-push", [this] { push( projectPath() ); } ); doc.setCommand( "git-fetch", [this] { fetch( projectPath() ); } ); doc.setCommand( "git-commit", [this] { commit( projectPath() ); } ); } void GitPlugin::onUnregister( UICodeEditor* editor ) { PluginBase::onUnregister( editor ); } bool GitPlugin::onCreateContextMenu( UICodeEditor*, UIPopUpMenu* menu, const Vector2i& /*position*/, const Uint32& /*flags*/ ) { if ( !mGitFound ) return false; menu->addSeparator(); auto* subMenu = UIPopUpMenu::New(); subMenu->addClass( "gitplugin_menu" ); auto addFn = [this, subMenu]( const std::string& txtKey, const std::string& txtVal, const std::string& icon = "" ) { subMenu ->add( i18n( txtKey, txtVal ), !icon.empty() ? findIcon( icon )->getSize( PixelDensity::dpToPxI( 12 ) ) : nullptr, KeyBindings::keybindFormat( mKeyBindings[txtKey] ) ) ->setId( txtKey ); }; addFn( "git-blame", "Git Blame" ); menu->addSubMenu( i18n( "git", "Git" ), mManager->getUISceneNode() ->findIcon( "source-control" ) ->getSize( PixelDensity::dpToPxI( 12 ) ), subMenu ); return false; } bool GitPlugin::onKeyDown( UICodeEditor* editor, const KeyEvent& event ) { if ( event.getSanitizedMod() == 0 && event.getKeyCode() == KEY_ESCAPE && editor->getTooltip() && editor->getTooltip()->isVisible() ) { hideTooltip( editor ); } return false; } std::unordered_map GitPlugin::updateReposBranches() { mGit->getSubModules(); bool reposEmpty = false; { Lock l( mReposMutex ); reposEmpty = mRepos.empty(); } if ( reposEmpty ) updateRepos(); Lock l( mGitBranchMutex ); decltype( mGitBranches ) prevBranch; if ( mGitBranches.empty() || mLastBranchesUpdate.getElapsedTime() > Seconds( 1 ) ) { prevBranch = mGitBranches; mGitBranches = mGit->branches( repos() ); mLastBranchesUpdate.restart(); } else { Lock l( mGitBranchMutex ); prevBranch = mGitBranches; } return prevBranch; } void GitPlugin::updateBranches( bool force ) { if ( !mGit || !mGitFound || ( mRunningUpdateBranches && !force ) ) return; mRunningUpdateBranches++; mThreadPool->run( [this] { if ( !mGit || mGit->getGitFolder().empty() ) { getUISceneNode()->runOnMainThread( [this] { updateBranchesUI( nullptr ); } ); return; } auto prevBranch = updateReposBranches(); auto branches = mGit->getAllBranchesAndTags( Git::RefType::All, {}, repoSelected() ); auto hash = GitBranchModel::hashBranches( branches ); auto model = GitBranchModel::asModel( std::move( branches ), hash, this ); if ( mBranchesTree && mBranchesTree->getModel() && static_cast( mBranchesTree->getModel() )->getHash() == hash ) { if ( prevBranch != mGitBranches ) mBranchesTree->getModel()->invalidate( Model::DontInvalidateIndexes ); return; } getUISceneNode()->runOnMainThread( [this, model] { updateBranchesUI( model ); } ); }, [this]( auto ) { mRunningUpdateBranches--; } ); } void GitPlugin::updateRepos() { if ( !mGit ) return; auto subModules = mGit->getSubModules(); std::sort( subModules.begin(), subModules.end() ); std::vector> repos; repos.clear(); repos.emplace_back( mProjectPath, FileSystem::fileNameFromPath( mProjectPath ) ); for ( auto& subModule : subModules ) { std::string subModulePath = mProjectPath + subModule; repos.emplace_back( std::move( subModulePath ), FileSystem::fileNameFromPath( subModule ) ); } Lock l( mReposMutex ); if ( repos == mRepos ) return; mRepos = std::move( repos ); } void GitPlugin::updateBranchesUI( std::shared_ptr model ) { buildSidePanelTab(); if ( !model ) { mBranchesTree->setModel( model ); } else { mBranchesTree->setModel( model ); mBranchesTree->setColumnsVisible( { GitBranchModel::Name } ); mBranchesTree->expandAll(); } updateRepos(); std::vector items; decltype( mRepos ) repos; { Lock l( mReposMutex ); repos = mRepos; } for ( const auto& repo : repos ) items.push_back( repo.second ); if ( repos.empty() || ( repos.size() == 1 && repos.begin()->second == "" ) ) { if ( !mRepoDropDown->getListBox()->isEmpty() ) mRepoDropDown->getListBox()->clear(); return; } if ( mRepoDropDown->getListBox()->getItemsText() != items ) { mRepoDropDown->getListBox()->clear(); mRepoDropDown->getListBox()->addListBoxItems( items ); mRepoDropDown->getListBox()->setSelected( repoName( repoSelected() ) ); } } void GitPlugin::buildSidePanelTab() { if ( mTab ) return; if ( mSidePanel == nullptr ) getUISceneNode()->bind( "panel", mSidePanel ); static constexpr auto STYLE = R"html( )html"; UIIcon* icon = findIcon( "source-control" ); std::string color = !mHighlightStyleColor.empty() && Color::isColorString( mHighlightStyleColor ) ? mHighlightStyleColor : std::string{ DEFAULT_HIGHLIGHT_COLOR }; UIWidget* node = getUISceneNode()->loadLayoutFromString( String::format( STYLE, color, color ) ); mTab = mSidePanel->add( i18n( "source_control", "Source Control" ), node, icon ? icon->getSize( PixelDensity::dpToPx( 12 ) ) : nullptr ); mTab->setId( "source_control" ); mTab->setTextAsFallback( true ); node->bind( "git_panel_switcher", mPanelSwicher ); node->bind( "git_panel_stack", mStackWidget ); node->bind( "git_branches_tree", mBranchesTree ); node->bind( "git_status_tree", mStatusTree ); node->bind( "git_content", mGitContentView ); node->bind( "git_no_content", mGitNoContentView ); node->bind( "git_panel_loader", mLoader ); node->bind( "git_repo", mRepoDropDown ); node->find( "branch_pull" )->onClick( [this]( auto ) { pull( repoSelected() ); } ); node->find( "branch_push" )->onClick( [this]( auto ) { push( repoSelected() ); } ); node->find( "branch_add" )->onClick( [this]( auto ) { branchCreate(); } ); mBranchesTree->setAutoExpandOnSingleColumn( true ); mBranchesTree->setHeadersVisible( false ); mBranchesTree->setExpandersAsIcons( true ); mBranchesTree->setIndentWidth( PixelDensity::dpToPx( 16 ) ); mBranchesTree->setScrollViewType( UIScrollableWidget::Inclusive ); mBranchesTree->on( Event::OnModelEvent, [this]( const Event* event ) { const ModelEvent* modelEvent = static_cast( event ); if ( !modelEvent->getModelIndex().hasParent() ) return; const Git::Branch* branch = static_cast( modelEvent->getModelIndex().internalData() ); switch ( modelEvent->getModelEventType() ) { case ModelEventType::Open: { if ( branch->type != Git::RefType::Stash ) checkout( *branch ); else stashApply( *branch ); break; } case ModelEventType::OpenMenu: { openBranchMenu( *branch ); break; } default: break; } } ); mBranchesTree->on( Event::KeyDown, [this]( const Event* event ) { const KeyEvent* keyEvent = event->asKeyEvent(); ModelIndex modelIndex = mBranchesTree->getSelection().first(); if ( !modelIndex.isValid() || modelIndex.internalId() == -1 || !mBranchesTree->getModel() ) return; Git::Branch branch = static_cast( mBranchesTree->getModel() )->branch( modelIndex ); switch ( keyEvent->getKeyCode() ) { case KEY_F7: branchCreate(); break; case KEY_F2: branchRename( branch ); break; case KEY_DELETE: if ( branch.type == Git::RefType::Stash ) stashDrop( branch ); else if ( branch.type == Git::RefType::Head ) branchDelete( branch ); break; default: break; } } ); auto listBox = mPanelSwicher->getListBox(); listBox->addListBoxItems( { i18n( "branches", "Branches" ), i18n( "status", "Status" ) } ); mStackMap.resize( 2 ); mStackMap[0] = node->find( "git_branches" ); mStackMap[1] = node->find( "git_status" ); listBox->setSelected( 0 ); mPanelSwicher->on( Event::OnItemSelected, [this, listBox]( const Event* ) { mStackWidget->setActiveWidget( mStackMap[listBox->getItemSelectedIndex()] ); } ); mStatusTree->setAutoColumnsWidth( true ); mStatusTree->setHeadersVisible( false ); mStatusTree->setExpandersAsIcons( true ); mStatusTree->setScrollViewType( UIScrollableWidget::Inclusive ); mStatusTree->setIndentWidth( PixelDensity::dpToPx( 4 ) ); mStatusTree->on( Event::OnRowCreated, [this]( const Event* event ) { UITableRow* row = event->asRowCreatedEvent()->getRow(); row->on( Event::MouseUp, [this, row]( const Event* event ) { if ( event->asMouseEvent()->getFlags() & EE_BUTTON_RMASK ) mStatusTree->onOpenMenuModelIndex( row->getCurIndex(), event ); } ); } ); mStatusTree->on( Event::OnModelEvent, [this]( const Event* event ) { const ModelEvent* modelEvent = static_cast( event ); if ( modelEvent->getModel() == nullptr ) return; auto model = static_cast( modelEvent->getModel() ); if ( modelEvent->getModelIndex().internalId() == GitStatusModel::GitFile ) { const Git::DiffFile* file = model->file( modelEvent->getModelIndex() ); if ( file == nullptr ) return; switch ( modelEvent->getModelEventType() ) { case ModelEventType::OpenMenu: { openFileStatusMenu( *file ); break; } case ModelEventType::Open: { diff( file->file, file->report.type == Git::GitStatusType::Staged ); break; } default: break; } } else if ( modelEvent->getModelIndex().internalId() == GitStatusModel::Status ) { switch ( modelEvent->getModelEventType() ) { case ModelEventType::OpenMenu: { const auto* status = model->statusType( modelEvent->getModelIndex() ); if ( status->type == Git::GitStatusType::Staged || status->type == Git::GitStatusType::Untracked || status->type == Git::GitStatusType::Changed ) { std::string repoPath; if ( !status->files.empty() ) repoPath = mGit->repoPath( status->files.front().file ); UIPopUpMenu* menu = UIPopUpMenu::New(); menu->setId( "git_status_type_menu" ); if ( status->type == Git::GitStatusType::Staged ) { menuAdd( menu, "git-commit", i18n( "git_commit", "Commit" ), "git-commit" ); menuAdd( menu, "git-diff-staged", i18n( "git_diff_staged", "Diff Staged" ), "diff-multiple" ); menuAdd( menu, "git-unstage-all", i18n( "git_unstage_all", "Unstage All" ) ); } if ( status->type == Git::GitStatusType::Untracked || status->type == Git::GitStatusType::Changed ) menuAdd( menu, "git-stage-all", i18n( "git_stage_all", "Stage All" ) ); if ( status->type == Git::GitStatusType::Changed ) { menu->addSeparator(); menuAdd( menu, "git-discard-all", i18n( "git_discard_all", "Discard All" ) ); } menu->on( Event::OnItemClicked, [this, model, repoPath]( const Event* event ) { if ( !mGit ) return; UIMenuItem* item = event->getNode()->asType(); std::string id( item->getId() ); if ( id == "git-commit" ) { commit( repoPath ); } else if ( id == "git-stage-all" ) { stage( model->getFiles( repoFullName( repoPath ), (Uint32)Git::GitStatusType::Untracked | (Uint32)Git::GitStatusType::Changed ) ); } else if ( id == "git-unstage-all" ) { unstage( model->getFiles( repoFullName( repoPath ), (Uint32)Git::GitStatusType::Staged ) ); } else if ( id == "git-discard-all" ) { discard( model->getFiles( repoFullName( repoPath ), (Uint32)Git::GitStatusType::Changed ) ); } else if ( id == "git-diff-staged" ) { diff( Git::DiffMode::DiffStaged, repoPath ); } } ); menu->showOverMouseCursor(); } break; } default: break; } } else if ( modelEvent->getModelIndex().internalId() == GitStatusModel::Repo ) { switch ( modelEvent->getModelEventType() ) { case ModelEventType::OpenMenu: { const auto* repo = model->repo( modelEvent->getModelIndex() ); if ( repo == nullptr ) return; std::string repoName = repo->repo; std::string repoPath = this->repoPath( repo->repo ); if ( repoPath.empty() && !repo->type.empty() && !repo->type.front().files.empty() ) repoPath = mGit->repoPath( repo->type.front().files.front().file ); if ( repoPath.empty() ) return; UIPopUpMenu* menu = UIPopUpMenu::New(); menu->setId( "git_repo_type_menu" ); if ( repo->hasStatusType( Git::GitStatusType::Staged ) ) { menuAdd( menu, "git-commit", i18n( "git_commit", "Commit" ), "git-commit" ); } if ( repo->hasStatusType( Git::GitStatusType::Untracked ) ) { menuAdd( menu, "git-stage-all", i18n( "git_stage_all", "Stage All" ) ); } menuAdd( menu, "git-fetch", i18n( "git_fetch", "Fetch" ), "repo-fetch" ); menuAdd( menu, "git-pull", i18n( "git_pull", "Pull" ), "repo-pull" ); menuAdd( menu, "git-push", i18n( "git_push", "Push" ), "repo-push" ); menuAdd( menu, "git-stash", i18n( "git_stash_all", "Stash All" ), "git-stash" ); menuAdd( menu, "git-diff-head", i18n( "git_diff_head", "Diff HEAD" ), "diff-multiple" ); menu->on( Event::OnItemClicked, [this, model, repoName, repoPath]( const Event* event ) { if ( !mGit ) return; UIMenuItem* item = event->getNode()->asType(); std::string id( item->getId() ); if ( id == "git-stash" ) { stashPush( model->getFiles( repoName ), repoPath ); } else if ( id == "git-fetch" ) { fetch( repoPath ); } else if ( id == "git-pull" ) { pull( repoPath ); } else if ( id == "git-push" ) { push( repoPath ); } else if ( id == "git-commit" ) { commit( repoPath ); } else if ( id == "git-stage-all" ) { stage( model->getFiles( repoName, (Uint32)Git::GitStatusType::Untracked | (Uint32)Git::GitStatusType::Changed ) ); } else if ( id == "git-diff-head" ) { diff( Git::DiffMode::DiffHead, repoPath ); } } ); menu->showOverMouseCursor(); break; } default: break; } } } ); mRepoDropDown->on( Event::OnItemSelected, [this]( const Event* ) { const auto& txt = mRepoDropDown->getListBox()->getItemSelectedText(); for ( const auto& repo : mRepos ) { if ( txt == repo.second ) { { Lock l( mRepoMutex ); mRepoSelected = repo.first; } updateBranches( true ); break; } } } ); } void GitPlugin::openBranchMenu( const Git::Branch& branch ) { UIPopUpMenu* menu = UIPopUpMenu::New(); menu->setId( "git_branch_menu" ); if ( branch.type != Git::RefType::Stash ) { menuAdd( menu, "git-fetch", i18n( "git_fetch", "Fetch" ), "repo-fetch" ); if ( gitBranch() != branch.name ) { menuAdd( menu, "git-checkout", i18n( "git_checkout_ellipsis", "Check Out..." ), "git-fetch" ); } if ( branch.type == Git::RefType::Head ) { menuAdd( menu, "git-branch-rename", i18n( "git_rename", "Rename" ), "", { KEY_F2 } ); menuAdd( menu, "git-pull", i18n( "git_pull", "Pull" ), "repo-pull" ); if ( branch.ahead ) menuAdd( menu, "git-push", i18n( "git_push", "Push" ), "repo-push" ); if ( branch.behind ) menuAdd( menu, "git-fast-forward-merge", i18n( "git_fast_forward_merge", "Fast Forward Merge" ) ); menu->addSeparator(); menuAdd( menu, "git-branch-delete", i18n( "git_delete_branch", "Delete" ), "remove" ); } menuAdd( menu, "git-merge-branch", i18n( "git_merge_branch", "Merge Branch" ), "git-merge" ); menuAdd( menu, "git-create-branch", i18n( "git_create_branch", "Create Branch" ), "repo-forked", { KEY_F7 } ); } else { menuAdd( menu, "git-stash-apply", i18n( "git_apply_stash", "Apply Stash" ), "git-stash-apply" ); menuAdd( menu, "git-stash-drop", i18n( "git_drop_stash", "Drop Stash" ), "git-stash-pop" ); } menu->on( Event::OnItemClicked, [this, branch]( const Event* event ) { if ( !mGit ) return; UIMenuItem* item = event->getNode()->asType(); std::string id( item->getId() ); if ( id == "git-checkout" ) { checkout( branch ); } else if ( id == "git-pull" ) { pull( repoSelected() ); } else if ( id == "git-push" ) { push( repoSelected() ); } else if ( id == "git-branch-delete" ) { branchDelete( branch ); } else if ( id == "git-branch-rename" ) { branchRename( branch ); } else if ( id == "git-fetch" ) { fetch( repoSelected() ); } else if ( id == "git-fast-forward-merge" ) { fastForwardMerge( branch ); } else if ( id == "git-create-branch" ) { branchCreate(); } else if ( id == "git-stash-apply" ) { stashApply( branch ); } else if ( id == "git-stash-drop" ) { stashDrop( branch ); } else if ( id == "git-merge-branch" ) { branchMerge( branch ); } } ); menu->showOverMouseCursor(); } void GitPlugin::openFileStatusMenu( const Git::DiffFile& file ) { UIPopUpMenu* menu = UIPopUpMenu::New(); menu->setId( "git_file_status_menu" ); menuAdd( menu, "git-open-file", i18n( "git_open_file", "Open File" ), "file" ); menuAdd( menu, "git-diff", i18n( "git_open_diff", "Open Diff" ), "diff-single" ); if ( file.report.type != Git::GitStatusType::Staged ) { menuAdd( menu, "git-stage", i18n( "git_stage", "Stage" ) ); } else { menuAdd( menu, "git-unstage", i18n( "git_unstage", "Unstage" ) ); } menu->addSeparator(); if ( file.report.type != Git::GitStatusType::Staged ) menuAdd( menu, "git-discard", i18n( "git_discard", "Discard" ) ); menu->on( Event::OnItemClicked, [this, file]( const Event* event ) { if ( !mGit ) return; UIMenuItem* item = event->getNode()->asType(); std::string id( item->getId() ); if ( id == "git-stage" ) { stage( { file.file } ); } else if ( id == "git-unstage" ) { unstage( { file.file } ); } else if ( id == "git-discard" ) { discard( file.file ); } else if ( id == "git-open-file" ) { openFile( file.file ); } else if ( id == "git-diff" ) { diff( file.file, file.report.type == Git::GitStatusType::Staged ); } } ); menu->showOverMouseCursor(); } void GitPlugin::runAsync( std::function fn, bool _updateStatus, bool _updateBranches, bool displaySuccessMsg, bool updateBranchesOnError, bool updateStatusOnError ) { if ( !mGit ) return; mLoader->setVisible( true ); mThreadPool->run( [this, fn, _updateStatus, _updateBranches, displaySuccessMsg, updateBranchesOnError, updateStatusOnError] { auto res = fn(); mLoader->runOnMainThread( [this] { mLoader->setVisible( false ); } ); if ( res.fail() || displaySuccessMsg ) { showMessage( LSPMessageType::Warning, res.result ); if ( _updateBranches && updateBranchesOnError ) updateBranches(); if ( _updateStatus && updateStatusOnError ) updateStatus( true ); return; } if ( _updateBranches ) updateBranches(); if ( _updateStatus ) updateStatus( true ); } ); } void GitPlugin::menuAdd( UIMenu* menu, const std::string& cmd, const std::string& text, const std::string& icon, const KeyBindings::Shortcut& forcedKeybinding ) { menu->add( text, iconDrawable( icon, 12 ), forcedKeybinding.empty() ? KeyBindings::keybindFormat( mKeyBindings[cmd] ) : getUISceneNode()->getKeyBindings().getShortcutString( forcedKeybinding, true ) ) ->setId( cmd ); } std::string GitPlugin::repoSelected() { Lock l( mRepoMutex ); return mRepoSelected; } std::string GitPlugin::projectPath() { Lock l( mRepoMutex ); return mProjectPath; } std::string GitPlugin::repoName( const std::string& repoPath ) { Lock l( mRepoMutex ); for ( const auto& repo : mRepos ) if ( repo.first == repoPath ) return repo.second; return ""; } std::string GitPlugin::repoFullName( const std::string& repoPath ) { Lock l( mRepoMutex ); for ( const auto& repo : mRepos ) { if ( repo.first == repoPath ) { if ( repoPath != mProjectPath ) { auto fullName( repo.first ); FileSystem::filePathRemoveBasePath( mProjectPath, fullName ); return fullName; } return repo.second; } } return ""; } std::string GitPlugin::repoPath( const std::string& repoName ) { Lock l( mRepoMutex ); for ( const auto& repo : mRepos ) if ( repo.second == repoName ) return repo.first; return ""; } } // namespace ecode