From 0fa99ab9d26eeecedc6b20047e172e30d458e9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 29 Aug 2026 15:11:08 -0300 Subject: [PATCH] Expand Git History actions and branch management - Add SmartGit-inspired context actions to Git History commits, including checkout, merge, fast-forward merge, cherry-pick, revert, branch creation, tag creation, and copying commit IDs or complete messages. - Order commit actions into consistent operation, ref-creation, and clipboard groups. Hide the redundant Show in Git History action inside detached history tabs while retaining it in the source-control sidebar. - Add a Working Tree / Index context menu with status-aware Commit and Stage actions. Track staged and unstaged availability in the history model and keep it synchronized as repository status changes. - Extend the Git command wrapper with operations for creating branches at commits, creating and deleting tags, deleting remote branches, cherry-picking commits, and reverting commits. - Support deleting tags and remote branches from the Branches view. When deleting a local branch with an upstream, optionally delete its tracked remote branch as part of the operation. - Add confirmation and input dialogs for branch, tag, remote-deletion, and revert workflows. Disable history-changing commit actions while another Git operation is active. --- .agent/rules/project-introduction.md | 4 + bin/assets/i18n/de.xml | 18 ++ bin/assets/i18n/en.xml | 18 ++ bin/assets/i18n/fr.xml | 18 ++ bin/assets/i18n/zh.xml | 18 ++ src/tools/ecode/plugins/git/git.cpp | 50 ++++ src/tools/ecode/plugins/git/git.hpp | 15 ++ .../ecode/plugins/git/githistorymodel.cpp | 4 + .../ecode/plugins/git/githistorymodel.hpp | 2 + src/tools/ecode/plugins/git/gitplugin.cpp | 249 +++++++++++++++++- src/tools/ecode/plugins/git/gitplugin.hpp | 13 +- 11 files changed, 395 insertions(+), 14 deletions(-) diff --git a/.agent/rules/project-introduction.md b/.agent/rules/project-introduction.md index 7bf928d05..a3ce994f0 100644 --- a/.agent/rules/project-introduction.md +++ b/.agent/rules/project-introduction.md @@ -3,14 +3,17 @@ This repository contains two primary components: a core framework (`eepp`) and an application built on top of it (`ecode`). ## 1. eepp (Core Framework) + [eepp](https://github.com/SpartanJ/eepp/) is an open-source, cross-platform game and application development framework. It is heavily focused on providing robust technology for rich, hardware-accelerated Graphical User Interfaces (GUIs). ## 2. ecode (Application) + [ecode](https://github.com/SpartanJ/ecode/) is a lightweight, multi-platform code editor designed for responsiveness and performance. * **Relationship:** `ecode` is built *using* the `eepp` GUI framework. It acts as the primary real-world consumer of `eepp`. * **Goal:** Development on `ecode` is often used to test, improve, and drive new features in the underlying `eepp` library. ## Documentation & Code References + When working on this project, rely on the following resources to understand existing implementations: * **C++ Headers (Primary Reference):** Rely heavily on Doxygen documentation found directly inside the class headers located at `include/eepp/`. * **Basic Documentation:** Found in `docs/articles/`. @@ -26,6 +29,7 @@ in the source. Keep all locale files structurally valid and verify that every su contains the new or renamed key. ## C++ Virtual Method Style + Follow the convention already used by the class being edited. In particular, when a class declares virtual methods without the `override` specifier, do not introduce `override` on new methods in that class. Mixing the styles can enable Clang's inconsistent-missing-override warnings for the existing diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index 3d22fe538..23afaa892 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -358,6 +358,7 @@ ecode versucht, externes Terminal zu öffnen. Auschecken Auschecken... Git-Commit + Commit... Commit-Nachricht: Bestätigen Einen zuvor gespeicherten Stash anwenden? @@ -1266,6 +1267,23 @@ Für sichtbare Änderung ecode neu starten. Arbeitsbaum / Index %zu geändert, %zu vorgemerkt Nicht übernommene Änderungen + ID kopieren + Nachricht kopieren + Branch hinzufügen... + Tag hinzufügen + Tag hinzufügen... + Tag zu Commit %s hinzufügen + Branch bei Commit %s erstellen. + Zusammenführen... + Cherry-Pick... + Rückgängig machen... + Möchten Sie den ausgewählten Commit rückgängig machen? Dadurch wird ein Commit erstellt, der seine Änderungen rückgängig macht. + Rückgängig & committen + Rückgängig machen + Tag '%s' löschen? + Remote-Branch '%s' aus '%s' löschen? + Auch den verfolgten Remote-Branch löschen + Tag löschen Keine weiteren Commits Im Git-Verlauf anzeigen Git-Commit-Verlauf – %s diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index 27065ba12..7a809bcd2 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -342,6 +342,7 @@ ecode will try to open an external terminal. Check Out Check Out... Git Commit + Commit... Commit Message: Confirm Apply a previously saved stash? @@ -1251,6 +1252,23 @@ Restart ecode to see the changes. Working Tree / Index %zu changed, %zu staged Uncommitted changes + Copy ID + Copy Message + Add Branch... + Add Tag + Add Tag... + Add tag to commit %s + Create a branch at commit %s. + Merge... + Cherry-Pick... + Revert... + Do you want to revert the selected commit? This will create a commit that undoes its changes. + Revert & Commit + Revert + Delete tag '%s'? + Delete remote branch '%s' from '%s'? + Also delete the tracked remote branch + Delete Tag No additional commits Show in Git History Git Commit History - %s diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index 9be5a2b4b..c53b4e7b3 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -341,6 +341,7 @@ ecode tentera d'ouvrir un terminal externe. Check Out Check Out... Git Commit + Valider... Message de commit : Confirmer Appliquer un stash précédemment sauvegardé ? @@ -1250,6 +1251,23 @@ Redémarrer ecode pour voir les changements. Arbre de travail / Index %zu modifié(s), %zu indexé(s) Modifications non validées + Copier l’identifiant + Copier le message + Ajouter une branche... + Ajouter une étiquette + Ajouter une étiquette... + Ajouter une étiquette au commit %s + Créer une branche au commit %s. + Fusionner... + Cherry-pick... + Annuler... + Voulez-vous annuler le commit sélectionné ? Cela créera un commit annulant ses modifications. + Annuler & valider + Annuler + Supprimer l’étiquette '%s' ? + Supprimer la branche distante '%s' de '%s' ? + Supprimer également la branche distante suivie + Supprimer l’étiquette Aucun commit supplémentaire Afficher dans l’historique Git Historique des commits Git – %s diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml index 7e91ee8c9..38f0be4c5 100644 --- a/bin/assets/i18n/zh.xml +++ b/bin/assets/i18n/zh.xml @@ -263,6 +263,7 @@ 检查 检查... 提交Git + 提交... 提交信息: 确认 Apply a previously saved stash? @@ -1055,6 +1056,23 @@ file in the directory tree. 工作树 / 索引 %zu 个更改,%zu 个已暂存 未提交的更改 + 复制 ID + 复制提交消息 + 添加分支... + 添加标签 + 添加标签... + 为提交 %s 添加标签 + 在提交 %s 创建分支。 + 合并... + 拣选提交... + 还原... + 要还原所选提交吗? 这将创建一个撤销其更改的提交。 + 还原并提交 + 还原 + 删除标签“%s”吗? + 删除远程分支“%s”(远程仓库“%s”)吗? + 同时删除跟踪的远程分支 + 删除标签 没有其他提交 在 Git 历史记录中显示 Git 提交历史记录 - %s diff --git a/src/tools/ecode/plugins/git/git.cpp b/src/tools/ecode/plugins/git/git.cpp index 3d7c4e56b..2349878a5 100644 --- a/src/tools/ecode/plugins/git/git.cpp +++ b/src/tools/ecode/plugins/git/git.cpp @@ -929,6 +929,56 @@ Git::Result Git::createBranch( const std::string& branchName, bool _checkout, return res; } +Git::Result Git::createBranchAt( const std::string& branchName, const std::string& revision, + const std::string& projectDir ) { + return gitSimple( String::format( "branch --no-track %s %s", branchName, revision ), + projectDir ); +} + +Git::Result Git::createTag( const std::string& name, const std::string& revision, + const std::string& message, const std::string& projectDir ) { + std::vector args{ "tag" }; + if ( !message.empty() ) { + args.emplace_back( "-a" ); + args.emplace_back( "-m" ); + args.emplace_back( message ); + } + args.emplace_back( name ); + args.emplace_back( revision ); + Result result; + result.returnCode = git( args, projectDir, result.result ); + return result; +} + +Git::Result Git::deleteTag( const std::string& name, const std::string& projectDir ) { + Result result; + result.returnCode = git( { "tag", "-d", name }, projectDir, result.result ); + return result; +} + +Git::Result Git::deleteRemoteBranch( const std::string& remote, const std::string& branch, + const std::string& projectDir ) { + Result result; + result.returnCode = git( { "push", remote, "--delete", branch }, projectDir, result.result ); + return result; +} + +Git::Result Git::cherryPick( const std::string& revision, const std::string& projectDir ) { + Result result; + result.returnCode = git( { "cherry-pick", revision }, projectDir, result.result ); + return result; +} + +Git::Result Git::revert( const std::string& revision, bool commit, const std::string& projectDir ) { + Result result; + std::vector args{ "revert" }; + if ( !commit ) + args.emplace_back( "--no-commit" ); + args.emplace_back( revision ); + result.returnCode = git( args, projectDir, result.result ); + return result; +} + Git::Result Git::renameBranch( const std::string& branch, const std::string& newName, const std::string& projectDir ) { return gitSimple( String::format( "branch -M %s %s", branch, newName ), projectDir ); diff --git a/src/tools/ecode/plugins/git/git.hpp b/src/tools/ecode/plugins/git/git.hpp index 9ea2eda65..96a6bbab6 100644 --- a/src/tools/ecode/plugins/git/git.hpp +++ b/src/tools/ecode/plugins/git/git.hpp @@ -374,6 +374,21 @@ class Git { Result createBranch( const std::string& branchName, bool checkout = false, const std::string& projectDir = "" ); + Result createBranchAt( const std::string& branchName, const std::string& revision, + const std::string& projectDir = "" ); + + Result createTag( const std::string& name, const std::string& revision, + const std::string& message = "", const std::string& projectDir = "" ); + + Result deleteTag( const std::string& name, const std::string& projectDir = "" ); + + Result deleteRemoteBranch( const std::string& remote, const std::string& branch, + const std::string& projectDir = "" ); + + Result cherryPick( const std::string& revision, const std::string& projectDir = "" ); + + Result revert( const std::string& revision, bool commit, const std::string& projectDir = "" ); + Result renameBranch( const std::string& branch, const std::string& newName, const std::string& projectDir = "" ); diff --git a/src/tools/ecode/plugins/git/githistorymodel.cpp b/src/tools/ecode/plugins/git/githistorymodel.cpp index 5654b576a..89d3f14bc 100644 --- a/src/tools/ecode/plugins/git/githistorymodel.cpp +++ b/src/tools/ecode/plugins/git/githistorymodel.cpp @@ -155,6 +155,8 @@ GitHistoryModel::workingTreeNode( const Git::Status& status, const std::string& } auto item = std::make_unique(); item->type = NodeType::WorkingTree; + item->hasWorkingTreeChanges = changed > 0; + item->hasStagedChanges = staged > 0; item->subject = mPlugin->i18n( "git_working_tree_index", "Working Tree / Index" ); item->message = String::format( mPlugin->i18n( "git_working_tree_summary", "%zu changed, %zu staged" ).toUtf8(), changed, @@ -234,6 +236,8 @@ void GitHistoryModel::setWorkingTreeStatus( const Git::Status& status, } else if ( hasItem && item ) { mRoots.front()->message = std::move( item->message ); mRoots.front()->tooltip = std::move( item->tooltip ); + mRoots.front()->hasWorkingTreeChanges = item->hasWorkingTreeChanges; + mRoots.front()->hasStagedChanges = item->hasStagedChanges; } invalidate( Model::DontInvalidateIndexes ); } diff --git a/src/tools/ecode/plugins/git/githistorymodel.hpp b/src/tools/ecode/plugins/git/githistorymodel.hpp index e4bb5c5ac..ab125e3e7 100644 --- a/src/tools/ecode/plugins/git/githistorymodel.hpp +++ b/src/tools/ecode/plugins/git/githistorymodel.hpp @@ -31,6 +31,8 @@ class GitHistoryModel : public Model { bool childrenLoaded{ false }; bool childrenLoading{ false }; bool retryAppend{ false }; + bool hasWorkingTreeChanges{ false }; + bool hasStagedChanges{ false }; }; static std::shared_ptr asModel( GitPlugin* plugin ) { diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index 04725ebbf..290e7d3f4 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -1070,6 +1070,46 @@ void GitPlugin::branchRename( Git::Branch branch ) { } void GitPlugin::branchDelete( Git::Branch branch ) { + if ( branch.type == Git::RefType::Tag ) { + UIMessageBox* msgBox = UIMessageBox::New( + UIMessageBox::OK_CANCEL, + String::format( i18n( "git_confirm_tag_delete", "Delete tag '%s'?" ).toUtf8(), + branch.name ) ); + msgBox->on( Event::OnConfirm, [this, branch]( auto ) { + runAsync( [this, branch] { return mGit->deleteTag( branch.name, repoSelected() ); }, + false, true ); + } ); + msgBox->setTitle( i18n( "git_delete", "Delete" ) ); + msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } )->center(); + msgBox->showWhenReady(); + return; + } + + auto remoteParts = []( const std::string& remoteBranch ) { + const size_t separator = remoteBranch.find( '/' ); + return std::pair{ + separator == std::string::npos ? std::string{} : remoteBranch.substr( 0, separator ), + separator == std::string::npos ? remoteBranch : remoteBranch.substr( separator + 1 ) }; + }; + if ( branch.type == Git::RefType::Remote ) { + auto [remote, name] = remoteParts( branch.name ); + UIMessageBox* msgBox = UIMessageBox::New( + UIMessageBox::OK_CANCEL, String::format( i18n( "git_confirm_remote_branch_delete", + "Delete remote branch '%s' from '%s'?" ) + .toUtf8(), + name, remote ) ); + msgBox->on( Event::OnConfirm, [this, remote = std::move( remote ), + name = std::move( name )]( auto ) { + runAsync( [this, remote, + name] { return mGit->deleteRemoteBranch( remote, name, repoSelected() ); }, + true, true ); + } ); + msgBox->setTitle( i18n( "git_delete", "Delete" ) ); + msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } )->center(); + msgBox->showWhenReady(); + return; + } + UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, String::format( i18n( "git_confirm_branch_delete", @@ -1077,9 +1117,28 @@ void GitPlugin::branchDelete( Git::Branch branch ) { .toUtf8(), branch.name ) ); - msgBox->on( Event::OnConfirm, [this, branch]( auto ) { - runAsync( [this, branch]() { return mGit->deleteBranch( branch.name, repoSelected() ); }, - false, true ); + UICheckBox* deleteRemote = nullptr; + if ( !branch.remote.empty() ) { + deleteRemote = UICheckBox::New(); + deleteRemote + ->setText( + i18n( "git_delete_tracking_remote", "Also delete the tracked remote branch" ) ) + ->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::WrapContent ) + ->setLayoutMarginTop( 8 ) + ->setParent( msgBox->getTextBox()->getParent() ); + deleteRemote->toPosition( 1 ); + } + msgBox->on( Event::OnConfirm, [this, branch, deleteRemote, remoteParts]( auto ) { + const bool removeRemote = deleteRemote && deleteRemote->isChecked(); + runAsync( + [this, branch, removeRemote, remoteParts]() { + auto result = mGit->deleteBranch( branch.name, repoSelected() ); + if ( result.fail() || !removeRemote ) + return result; + auto [remote, name] = remoteParts( branch.remote ); + return mGit->deleteRemoteBranch( remote, name, repoSelected() ); + }, + false, true ); } ); msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } ); msgBox->setTitle( i18n( "git_confirm", "Confirm" ) ); @@ -2539,23 +2598,180 @@ void GitPlugin::activateHistoryIndex( const ModelIndex& index, bool expand ) { } } -void GitPlugin::openHistoryMenu( const ModelIndex& index ) { +void GitPlugin::openHistoryMenu( const ModelIndex& index, bool showHistoryAction ) { if ( !mHistoryModel ) return; const auto* node = mHistoryModel->node( index ); - if ( !node || node->type != GitHistoryModel::NodeType::Commit ) + if ( !node ) + return; + if ( node->type == GitHistoryModel::NodeType::WorkingTree ) { + if ( !canStartGitOperation() ) + return; + UIPopUpMenu* menu = UIPopUpMenu::New(); + if ( node->hasStagedChanges ) { + menuAdd( menu, "git-commit", i18n( "git_commit_ellipsis", "Commit..." ), "git-commit" ); + } + if ( node->hasWorkingTreeChanges ) + menuAdd( menu, "git-stage", i18n( "git_stage", "Stage" ), "diff-added" ); + const std::string repo = repoSelected(); + menu->on( Event::OnItemClicked, [this, repo]( const Event* event ) { + const std::string id = event->getNode()->asType()->getId(); + if ( id == "git-commit" ) { + commit( repo ); + } else if ( id == "git-stage" ) { + std::vector files; + { + Lock l( mGitStatusMutex ); + const auto found = mGitStatus.files.find( mGit->repoName( repo, true, repo ) ); + if ( found != mGitStatus.files.end() ) { + for ( const auto& file : found->second ) { + if ( file.report.type != Git::GitStatusType::Staged ) + files.emplace_back( file.file ); + } + } + } + stage( files ); + } + } ); + menu->showOverMouseCursor(); + return; + } + if ( node->type != GitHistoryModel::NodeType::Commit ) return; UIPopUpMenu* menu = UIPopUpMenu::New(); - menuAdd( menu, "git-show-history", i18n( "git_show_in_history", "Show in Git History" ), - "history" ); + if ( showHistoryAction ) { + menuAdd( menu, "git-show-history", i18n( "git_show_in_history", "Show in Git History" ), + "history" ); + menu->addSeparator(); + } + if ( canStartGitOperation() ) { + menuAdd( menu, "git-checkout", i18n( "git_checkout_ellipsis", "Check Out..." ), + "git-fetch" ); + menuAdd( menu, "git-merge-commit", i18n( "git_merge_ellipsis", "Merge..." ), "git-merge" ); + menuAdd( menu, "git-fast-forward-commit", + i18n( "git_fast_forward_merge", "Fast Forward Merge" ), "git-merge" ); + menuAdd( menu, "git-cherry-pick", i18n( "git_cherry_pick_ellipsis", "Cherry-Pick..." ), + "git-cherry-pick" ); + menuAdd( menu, "git-revert-commit", i18n( "git_revert_ellipsis", "Revert..." ), "discard" ); + menu->addSeparator(); + } + menuAdd( menu, "git-create-branch", i18n( "git_create_branch_ellipsis", "Add Branch..." ), + "repo-forked" ); + menuAdd( menu, "git-add-tag", i18n( "git_add_tag_ellipsis", "Add Tag..." ), "tag" ); + menu->addSeparator(); + menuAdd( menu, "git-copy-message", i18n( "git_copy_message", "Copy Message" ), "copy" ); + menuAdd( menu, "git-copy-id", i18n( "git_copy_id", "Copy ID" ), "copy" ); const Git::Commit commit = node->commit; menu->on( Event::OnItemClicked, [this, commit]( const Event* event ) { - if ( event->getNode()->asType()->getId() == "git-show-history" ) + const std::string id = event->getNode()->asType()->getId(); + if ( id == "git-show-history" ) showGitHistory( &commit ); + else if ( id == "git-copy-id" ) + getUISceneNode()->getWindow()->getClipboard()->setText( commit.hash ); + else if ( id == "git-copy-message" ) { + getUISceneNode()->getWindow()->getClipboard()->setText( + commit.subject + ( commit.message.empty() ? "" : "\n\n" + commit.message ) ); + } else if ( id == "git-create-branch" ) + createBranchAtCommit( commit ); + else if ( id == "git-add-tag" ) + addTag( commit ); + else if ( id == "git-checkout" ) + runAsync( [this, commit] { return mGit->checkout( commit.hash, repoSelected() ); }, + true, true, false, false, false, true ); + else if ( id == "git-merge-commit" ) { + std::string repo = repoSelected(); + runMergeLikeAsync( + [commit, repo = std::move( repo )]( Git& git ) { + return git.mergeBranch( commit.hash, false, repo ); + }, + repoSelected() ); + } else if ( id == "git-fast-forward-commit" ) { + std::string repo = repoSelected(); + runMergeLikeAsync( + [commit, repo = std::move( repo )]( Git& git ) { + return git.mergeBranch( commit.hash, true, repo ); + }, + repoSelected() ); + } else if ( id == "git-cherry-pick" ) { + std::string repo = repoSelected(); + runMergeLikeAsync( [commit, repo = std::move( repo )]( + Git& git ) { return git.cherryPick( commit.hash, repo ); }, + repoSelected() ); + } else if ( id == "git-revert-commit" ) + revertCommit( commit ); } ); menu->showOverMouseCursor(); } +bool GitPlugin::canStartGitOperation() { + return mGit && mGit->operation( repoSelected() ) == Git::GitOperation::None; +} + +void GitPlugin::createBranchAtCommit( const Git::Commit& commit ) { + UIMessageBox* box = UIMessageBox::New( + UIMessageBox::INPUT, + String::format( + i18n( "git_create_branch_at_commit", "Create a branch at commit %s." ).toUtf8(), + commit.shortHash ) ); + box->on( Event::OnConfirm, [this, box, commit]( const Event* ) { + const std::string name = box->getTextInput()->getText().toUtf8(); + if ( name.empty() ) + return; + box->closeWindow(); + runAsync( [this, name, + commit] { return mGit->createBranchAt( name, commit.hash, repoSelected() ); }, + false, true ); + } ); + box->setTitle( i18n( "git_add_branch", "Add Branch" ) ); + box->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } )->center(); + box->showWhenReady(); +} + +void GitPlugin::addTag( const Git::Commit& commit ) { + UIMessageBox* box = UIMessageBox::New( + UIMessageBox::TEXT_EDIT, + String::format( i18n( "git_add_tag_to_commit", "Add tag to commit %s" ).toUtf8(), + commit.shortHash ) ); + auto* name = UITextInput::New(); + name->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent ) + ->setLayoutMargin( Rectf( 0, 4, 0, 4 ) ) + ->setParent( box->getTextEdit()->getParent() ); + name->toPosition( 1 ); + box->getTextEdit()->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::Fixed ); + box->getButtonOK()->setText( i18n( "git_add_tag", "Add Tag" ) ); + box->on( Event::OnConfirm, [this, box, name, commit]( const Event* ) { + const std::string tag = name->getText().toUtf8(); + if ( tag.empty() ) + return; + const std::string message = box->getTextEdit()->getText().toUtf8(); + box->closeWindow(); + runAsync( [this, tag, message, + commit] { return mGit->createTag( tag, commit.hash, message, repoSelected() ); }, + false, true ); + } ); + box->setTitle( i18n( "git_add_tag", "Add Tag" ) ); + box->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } )->center(); + box->showWhenReady(); + name->setFocus(); +} + +void GitPlugin::revertCommit( const Git::Commit& commit ) { + UIMessageBox* box = UIMessageBox::New( + UIMessageBox::YES_NO, + i18n( "git_confirm_revert_commit", "Do you want to revert the selected commit?\nThis will " + "create a commit that undoes its changes." ) ); + box->getButtonOK()->setText( i18n( "git_revert_and_commit", "Revert & Commit" ) ); + box->on( Event::OnConfirm, [this, commit]( const Event* ) { + std::string repo = repoSelected(); + runMergeLikeAsync( [commit, repo = std::move( repo )]( + Git& git ) { return git.revert( commit.hash, true, repo ); }, + repoSelected() ); + } ); + box->setTitle( i18n( "git_revert", "Revert" ) ); + box->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } )->center(); + box->showWhenReady(); +} + std::string GitPlugin::detachedHistoryTitle() { String state = String::fromUtf8( mHistoryRevision ); if ( mHistoryRefDropDown && mHistoryRefModel ) { @@ -2637,7 +2853,7 @@ void GitPlugin::showGitHistory( const Git::Commit* commit ) { else if ( modelEvent->getModelEventType() == ModelEventType::Open ) activateHistoryIndex( modelEvent->getModelIndex(), false ); else if ( modelEvent->getModelEventType() == ModelEventType::OpenMenu ) - openHistoryMenu( modelEvent->getModelIndex() ); + openHistoryMenu( modelEvent->getModelIndex(), false ); } ); mDetachedHistory.tree->setOnSelection( [this]( const ModelIndex& index ) { if ( const auto* node = mHistoryModel ? mHistoryModel->node( index ) : nullptr; node ) { @@ -3396,7 +3612,7 @@ void GitPlugin::buildSidePanelTab() { openCommitDetails( node->commit ); } } else if ( modelEvent->getModelEventType() == ModelEventType::OpenMenu ) - openHistoryMenu( modelEvent->getModelIndex() ); + openHistoryMenu( modelEvent->getModelIndex(), true ); } ); mHistoryTree->setOnSelection( [this]( const ModelIndex& index ) { if ( !mHistoryModel ) @@ -3640,12 +3856,19 @@ void GitPlugin::openBranchMenu( const Git::Branch& branch ) { if ( branch.behind ) menuAdd( menu, "git-fast-forward-merge", i18n( "git_fast_forward_merge", "Fast Forward Merge" ) ); + } + if ( branch.type == Git::RefType::Head || branch.type == Git::RefType::Remote || + branch.type == Git::RefType::Tag ) { menu->addSeparator(); - menuAdd( menu, "git-branch-delete", i18n( "git_delete_branch", "Delete" ), "remove" ); + menuAdd( menu, "git-branch-delete", + branch.type == Git::RefType::Tag ? i18n( "git_delete_tag", "Delete Tag" ) + : i18n( "git_delete_branch", "Delete" ), + "remove" ); } - menuAdd( menu, "git-merge-branch", i18n( "git_merge_branch", "Merge Branch" ), - "git-merge" ); + if ( branch.type != Git::RefType::Tag ) + 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 { diff --git a/src/tools/ecode/plugins/git/gitplugin.hpp b/src/tools/ecode/plugins/git/gitplugin.hpp index 8f298fca3..2168ed86f 100644 --- a/src/tools/ecode/plugins/git/gitplugin.hpp +++ b/src/tools/ecode/plugins/git/gitplugin.hpp @@ -377,15 +377,25 @@ class GitPlugin : public PluginBase { void activateHistoryIndex( const ModelIndex& index, bool expand ); void openCommitDetails( const Git::Commit& commit ); + void openWorkingTreeDetails( bool detached ); void showGitHistory( const Git::Commit* commit = nullptr ); void focusDetachedHistory(); - void openHistoryMenu( const ModelIndex& index ); + void openHistoryMenu( const ModelIndex& index, bool showHistoryAction ); + + void addTag( const Git::Commit& commit ); + + void createBranchAtCommit( const Git::Commit& commit ); + + void revertCommit( const Git::Commit& commit ); + + bool canStartGitOperation(); void openDetachedCommitDetails( const Git::Commit& commit ); + void ensureDetachedCommitDetailsHost(); std::string detachedHistoryTitle(); @@ -414,6 +424,7 @@ class GitPlugin : public PluginBase { GitConflictSession* conflictSession( const std::string& repoPath ); GitConflictSession* activeConflictSession(); + bool updateConflictSessions( UnorderedMap& conflictStates ); void menuAdd( UIMenu* menu, const std::string& cmd, const std::string& text,