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.
This commit is contained in:
Martín Lucas Golini
2026-08-29 15:11:08 -03:00
parent b3d72b5acf
commit 0fa99ab9d2
11 changed files with 395 additions and 14 deletions

View File

@@ -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

View File

@@ -358,6 +358,7 @@ ecode versucht, externes Terminal zu öffnen.</string>
<string name="git_checkout">Auschecken</string>
<string name="git_checkout_ellipsis">Auschecken...</string>
<string name="git_commit">Git-Commit</string>
<string name="git_commit_ellipsis">Commit...</string>
<string name="git_commit_message">Commit-Nachricht:</string>
<string name="git_confirm">Bestätigen</string>
<string name="git_confirm_apply_stash">Einen zuvor gespeicherten Stash anwenden?</string>
@@ -1266,6 +1267,23 @@ Für sichtbare Änderung ecode neu starten.</string>
<string name="git_working_tree_index">Arbeitsbaum / Index</string>
<string name="git_working_tree_summary">%zu geändert, %zu vorgemerkt</string>
<string name="git_uncommitted_changes">Nicht übernommene Änderungen</string>
<string name="git_copy_id">ID kopieren</string>
<string name="git_copy_message">Nachricht kopieren</string>
<string name="git_create_branch_ellipsis">Branch hinzufügen...</string>
<string name="git_add_tag">Tag hinzufügen</string>
<string name="git_add_tag_ellipsis">Tag hinzufügen...</string>
<string name="git_add_tag_to_commit">Tag zu Commit %s hinzufügen</string>
<string name="git_create_branch_at_commit">Branch bei Commit %s erstellen.</string>
<string name="git_merge_ellipsis">Zusammenführen...</string>
<string name="git_cherry_pick_ellipsis">Cherry-Pick...</string>
<string name="git_revert_ellipsis">Rückgängig machen...</string>
<string name="git_confirm_revert_commit">Möchten Sie den ausgewählten Commit rückgängig machen?&#10;Dadurch wird ein Commit erstellt, der seine Änderungen rückgängig macht.</string>
<string name="git_revert_and_commit">Rückgängig &amp; committen</string>
<string name="git_revert">Rückgängig machen</string>
<string name="git_confirm_tag_delete">Tag '%s' löschen?</string>
<string name="git_confirm_remote_branch_delete">Remote-Branch '%s' aus '%s' löschen?</string>
<string name="git_delete_tracking_remote">Auch den verfolgten Remote-Branch löschen</string>
<string name="git_delete_tag">Tag löschen</string>
<string name="git_history_no_additional_commits">Keine weiteren Commits</string>
<string name="git_show_in_history">Im Git-Verlauf anzeigen</string>
<string name="git_commit_history_title">Git-Commit-Verlauf %s</string>

View File

@@ -342,6 +342,7 @@ ecode will try to open an external terminal.</string>
<string name="git_checkout">Check Out</string>
<string name="git_checkout_ellipsis">Check Out...</string>
<string name="git_commit">Git Commit</string>
<string name="git_commit_ellipsis">Commit...</string>
<string name="git_commit_message">Commit Message:</string>
<string name="git_confirm">Confirm</string>
<string name="git_confirm_apply_stash">Apply a previously saved stash?</string>
@@ -1251,6 +1252,23 @@ Restart ecode to see the changes.</string>
<string name="git_working_tree_index">Working Tree / Index</string>
<string name="git_working_tree_summary">%zu changed, %zu staged</string>
<string name="git_uncommitted_changes">Uncommitted changes</string>
<string name="git_copy_id">Copy ID</string>
<string name="git_copy_message">Copy Message</string>
<string name="git_create_branch_ellipsis">Add Branch...</string>
<string name="git_add_tag">Add Tag</string>
<string name="git_add_tag_ellipsis">Add Tag...</string>
<string name="git_add_tag_to_commit">Add tag to commit %s</string>
<string name="git_create_branch_at_commit">Create a branch at commit %s.</string>
<string name="git_merge_ellipsis">Merge...</string>
<string name="git_cherry_pick_ellipsis">Cherry-Pick...</string>
<string name="git_revert_ellipsis">Revert...</string>
<string name="git_confirm_revert_commit">Do you want to revert the selected commit?&#10;This will create a commit that undoes its changes.</string>
<string name="git_revert_and_commit">Revert &amp; Commit</string>
<string name="git_revert">Revert</string>
<string name="git_confirm_tag_delete">Delete tag '%s'?</string>
<string name="git_confirm_remote_branch_delete">Delete remote branch '%s' from '%s'?</string>
<string name="git_delete_tracking_remote">Also delete the tracked remote branch</string>
<string name="git_delete_tag">Delete Tag</string>
<string name="git_history_no_additional_commits">No additional commits</string>
<string name="git_show_in_history">Show in Git History</string>
<string name="git_commit_history_title">Git Commit History - %s</string>

View File

@@ -341,6 +341,7 @@ ecode tentera d'ouvrir un terminal externe.</string>
<string name="git_checkout">Check Out</string>
<string name="git_checkout_ellipsis">Check Out...</string>
<string name="git_commit">Git Commit</string>
<string name="git_commit_ellipsis">Valider...</string>
<string name="git_commit_message">Message de commit :</string>
<string name="git_confirm">Confirmer</string>
<string name="git_confirm_apply_stash">Appliquer un stash précédemment sauvegardé ?</string>
@@ -1250,6 +1251,23 @@ Redémarrer ecode pour voir les changements.</string>
<string name="git_working_tree_index">Arbre de travail / Index</string>
<string name="git_working_tree_summary">%zu modifié(s), %zu indexé(s)</string>
<string name="git_uncommitted_changes">Modifications non validées</string>
<string name="git_copy_id">Copier lidentifiant</string>
<string name="git_copy_message">Copier le message</string>
<string name="git_create_branch_ellipsis">Ajouter une branche...</string>
<string name="git_add_tag">Ajouter une étiquette</string>
<string name="git_add_tag_ellipsis">Ajouter une étiquette...</string>
<string name="git_add_tag_to_commit">Ajouter une étiquette au commit %s</string>
<string name="git_create_branch_at_commit">Créer une branche au commit %s.</string>
<string name="git_merge_ellipsis">Fusionner...</string>
<string name="git_cherry_pick_ellipsis">Cherry-pick...</string>
<string name="git_revert_ellipsis">Annuler...</string>
<string name="git_confirm_revert_commit">Voulez-vous annuler le commit sélectionné ?&#10;Cela créera un commit annulant ses modifications.</string>
<string name="git_revert_and_commit">Annuler &amp; valider</string>
<string name="git_revert">Annuler</string>
<string name="git_confirm_tag_delete">Supprimer létiquette '%s' ?</string>
<string name="git_confirm_remote_branch_delete">Supprimer la branche distante '%s' de '%s' ?</string>
<string name="git_delete_tracking_remote">Supprimer également la branche distante suivie</string>
<string name="git_delete_tag">Supprimer létiquette</string>
<string name="git_history_no_additional_commits">Aucun commit supplémentaire</string>
<string name="git_show_in_history">Afficher dans lhistorique Git</string>
<string name="git_commit_history_title">Historique des commits Git %s</string>

View File

@@ -263,6 +263,7 @@
<string name="git_checkout">检查</string>
<string name="git_checkout_ellipsis">检查...</string>
<string name="git_commit">提交Git</string>
<string name="git_commit_ellipsis">提交...</string>
<string name="git_commit_message">提交信息:</string>
<string name="git_confirm">确认</string>
<string name="git_confirm_apply_stash">Apply a previously saved stash?</string>
@@ -1055,6 +1056,23 @@ file in the directory tree.</string>
<string name="git_working_tree_index">工作树 / 索引</string>
<string name="git_working_tree_summary">%zu 个更改,%zu 个已暂存</string>
<string name="git_uncommitted_changes">未提交的更改</string>
<string name="git_copy_id">复制 ID</string>
<string name="git_copy_message">复制提交消息</string>
<string name="git_create_branch_ellipsis">添加分支...</string>
<string name="git_add_tag">添加标签</string>
<string name="git_add_tag_ellipsis">添加标签...</string>
<string name="git_add_tag_to_commit">为提交 %s 添加标签</string>
<string name="git_create_branch_at_commit">在提交 %s 创建分支。</string>
<string name="git_merge_ellipsis">合并...</string>
<string name="git_cherry_pick_ellipsis">拣选提交...</string>
<string name="git_revert_ellipsis">还原...</string>
<string name="git_confirm_revert_commit">要还原所选提交吗?&#10;这将创建一个撤销其更改的提交。</string>
<string name="git_revert_and_commit">还原并提交</string>
<string name="git_revert">还原</string>
<string name="git_confirm_tag_delete">删除标签“%s”吗</string>
<string name="git_confirm_remote_branch_delete">删除远程分支“%s”远程仓库“%s”</string>
<string name="git_delete_tracking_remote">同时删除跟踪的远程分支</string>
<string name="git_delete_tag">删除标签</string>
<string name="git_history_no_additional_commits">没有其他提交</string>
<string name="git_show_in_history">在 Git 历史记录中显示</string>
<string name="git_commit_history_title">Git 提交历史记录 - %s</string>

View File

@@ -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<std::string> 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<std::string> 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 );

View File

@@ -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 = "" );

View File

@@ -155,6 +155,8 @@ GitHistoryModel::workingTreeNode( const Git::Status& status, const std::string&
}
auto item = std::make_unique<Node>();
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 );
}

View File

@@ -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<GitHistoryModel> asModel( GitPlugin* plugin ) {

View File

@@ -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<UIMenuItem>()->getId();
if ( id == "git-commit" ) {
commit( repo );
} else if ( id == "git-stage" ) {
std::vector<std::string> 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<UIMenuItem>()->getId() == "git-show-history" )
const std::string id = event->getNode()->asType<UIMenuItem>()->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 {

View File

@@ -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<std::string, Git::ConflictState>& conflictStates );
void menuAdd( UIMenu* menu, const std::string& cmd, const std::string& text,