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,