Fix conflict discard and merge view copy commands

- add Discard actions for conflicted files and conflict groups
  - restore discarded conflicts from HEAD in both index and working tree
  - restrict regular discard actions to changed tracked files
  - preserve default editor commands when replacing an editor document
  - enable copying from the read-only merge-view side panels
  - cover conflict restoration and merge-side copying
  - document build parallelism and function-spacing conventions
This commit is contained in:
Martín Lucas Golini
2026-08-31 11:03:19 -03:00
parent 7c92b59e00
commit 36200430aa
10 changed files with 104 additions and 20 deletions
+2
View File
@@ -46,6 +46,8 @@ Run this **after** all edits and **before** attempting to compile.
## Step 2: Compile the Project ## Step 2: Compile the Project
To compile the project in debug mode, execute the `make` command, ensuring you point to the correct directory for your current Operating System. To compile the project in debug mode, execute the `make` command, ensuring you point to the correct directory for your current Operating System.
### Build Parallelism
Always use all processors reported by the platform when selecting the parallel job count. On Linux Always use all processors reported by the platform when selecting the parallel job count. On Linux
and other systems with `nproc`, use `-j$(nproc)` exactly; do not substitute an arbitrary fixed value and other systems with `nproc`, use `-j$(nproc)` exactly; do not substitute an arbitrary fixed value
such as `-j4`. Use the platform-equivalent processor-count command where `nproc` is unavailable. such as `-j4`. Use the platform-equivalent processor-count command where `nproc` is unavailable.
+6
View File
@@ -43,6 +43,12 @@ declarations and unqualified eepp type names, such as `UISplitter`, over repeate
qualified names such as `EE::UI::UISplitter`. Keep explicit qualification only where it is required qualified names such as `EE::UI::UISplitter`. Keep explicit qualification only where it is required
to resolve ambiguity or avoid importing an unusually broad namespace into an unsuitable scope. to resolve ambiguity or avoid importing an unusually broad namespace into an unsuitable scope.
## Function Spacing
Always leave one blank line between consecutive function declarations in headers and between
consecutive function definitions in implementation files. This is a project-wide stylistic
constraint, including short inline functions.
## Control-Statement Braces ## Control-Statement Braces
Use braces around the body of an `if`, `else`, `for`, `while`, or similar control statement whenever Use braces around the body of an `if`, `else`, `for`, `while`, or similar control statement whenever
+2 -2
View File
@@ -407,8 +407,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
void addKeyBindsString( const std::map<std::string, std::string>& binds, void addKeyBindsString( const std::map<std::string, std::string>& binds,
const bool& allowLocked = false ); const bool& allowLocked = false );
void addKeyBinds( const KeyBindings::ShortcutMap& binds, void addKeyBinds( const KeyBindings::ShortcutMap& binds, const bool& allowLocked = false );
const bool& allowLocked = false );
const bool& getHighlightCurrentLine() const; const bool& getHighlightCurrentLine() const;
@@ -907,6 +906,7 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client {
std::shared_ptr<Doc::TextDocument> mDoc; std::shared_ptr<Doc::TextDocument> mDoc;
MainThreadLifetime<UICodeEditor> mAsyncLifetime; MainThreadLifetime<UICodeEditor> mAsyncLifetime;
bool mDirtyEditor{ false }; bool mDirtyEditor{ false };
bool mAutoRegisterBaseCommands{ true };
bool mDirtyScroll{ false }; bool mDirtyScroll{ false };
bool mCursorVisible{ false }; bool mCursorVisible{ false };
bool mMouseDown{ false }; bool mMouseDown{ false };
+5 -3
View File
@@ -167,6 +167,7 @@ UICodeEditor::UICodeEditor( const std::string& elementTag, const bool& autoRegis
mFont( getUISceneNode()->getResourceScope()->findFont( "monospace" ).get() ), mFont( getUISceneNode()->getResourceScope()->findFont( "monospace" ).get() ),
mDoc( std::make_shared<TextDocument>() ), mDoc( std::make_shared<TextDocument>() ),
mAsyncLifetime( this, this ), mAsyncLifetime( this, this ),
mAutoRegisterBaseCommands( autoRegisterBaseCommands ),
mDocView( mDoc, mFontStyleConfig, mDocView( mDoc, mFontStyleConfig,
{ .textHints = TextHints::NoKerning, .tabStops = mTabStops } ), { .textHints = TextHints::NoKerning, .tabStops = mTabStops } ),
mBlinkTime( Seconds( 0.5f ) ), mBlinkTime( Seconds( 0.5f ) ),
@@ -214,7 +215,7 @@ UICodeEditor::UICodeEditor( const std::string& elementTag, const bool& autoRegis
mDoc->registerClient( this ); mDoc->registerClient( this );
subscribeScheduledUpdate(); subscribeScheduledUpdate();
if ( autoRegisterBaseCommands ) if ( mAutoRegisterBaseCommands )
registerCommands(); registerCommands();
if ( autoRegisterBaseKeybindings ) if ( autoRegisterBaseKeybindings )
registerKeybindings(); registerKeybindings();
@@ -1066,6 +1067,8 @@ void UICodeEditor::setDocument( std::shared_ptr<TextDocument> doc ) {
if ( clientsOfTypeCount == 1 || useCount == 1 ) if ( clientsOfTypeCount == 1 || useCount == 1 )
onDocumentClosed( mDoc.get() ); onDocumentClosed( mDoc.get() );
mDoc = doc; mDoc = doc;
if ( mAutoRegisterBaseCommands )
mDoc->setSharedRefCommands( getDefaultEditorCommands() );
mDoc->registerClient( this ); mDoc->registerClient( this );
mDocView.setDocument( doc ); mDocView.setDocument( doc );
onDocumentChanged( oldDocURI ); onDocumentChanged( oldDocURI );
@@ -2909,8 +2912,7 @@ void UICodeEditor::addKeyBindsString( const std::map<std::string, std::string>&
} }
} }
void UICodeEditor::addKeyBinds( const KeyBindings::ShortcutMap& binds, void UICodeEditor::addKeyBinds( const KeyBindings::ShortcutMap& binds, const bool& allowLocked ) {
const bool& allowLocked ) {
mKeyBindings.addKeybinds( binds ); mKeyBindings.addKeybinds( binds );
for ( const auto& bind : binds ) { for ( const auto& bind : binds ) {
if ( allowLocked ) { if ( allowLocked ) {
@@ -101,6 +101,17 @@ UTEST( GitConflict, KeepsMergeOperationAfterAllConflictsAreStaged ) {
EXPECT_TRUE( state.error.empty() ); EXPECT_TRUE( state.error.empty() );
EXPECT_FALSE( state.hasConflicts() ); EXPECT_FALSE( state.hasConflicts() );
EXPECT_EQ( Git::GitOperation::Merge, state.operation ); EXPECT_EQ( Git::GitOperation::Merge, state.operation );
ASSERT_EQ( EXIT_SUCCESS, run( { "merge", "--abort" } ) );
EXPECT_NE( EXIT_SUCCESS, run( { "merge", "incoming" } ) );
state = git.conflictState( temp.path.string(), false );
EXPECT_TRUE( state.hasConflicts() );
ASSERT_EQ( EXIT_SUCCESS, git.restoreHead( { "conflict.txt" }, temp.path.string() ).returnCode );
state = git.conflictState( temp.path.string(), false );
EXPECT_FALSE( state.hasConflicts() );
std::string restored;
ASSERT_TRUE( FileSystem::fileGet( file, restored ) );
EXPECT_TRUE( restored == "current\n" );
} }
UTEST( GitHistory, PaginatesFirstParentWithoutDuplicates ) { UTEST( GitHistory, PaginatesFirstParentWithoutDuplicates ) {
@@ -2,6 +2,7 @@
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp> #include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
#include <eepp/ui/tools/uimergeview.hpp> #include <eepp/ui/tools/uimergeview.hpp>
#include <eepp/ui/uiapplication.hpp> #include <eepp/ui/uiapplication.hpp>
#include <eepp/window/clipboard.hpp>
using namespace EE; using namespace EE;
using namespace EE::UI; using namespace EE::UI;
@@ -80,6 +81,10 @@ UTEST( UIMergeView, UsesSharedResultDocumentAndAcceptIsUndoable ) {
EXPECT_TRUE( view->getRightEditor()->getVerticalScrollBarEnabled() ); EXPECT_TRUE( view->getRightEditor()->getVerticalScrollBarEnabled() );
EXPECT_TRUE( view->isToolbarVisible() ); EXPECT_TRUE( view->isToolbarVisible() );
EXPECT_TRUE( view->hasCommand( "merge-accept-left" ) ); EXPECT_TRUE( view->hasCommand( "merge-accept-left" ) );
auto* leftEditor = view->getLeftEditor();
leftEditor->getDocument().setSelection( { { 0, 0 }, { 0, 4 } } );
leftEditor->getDocument().execute( "copy", leftEditor );
EXPECT_STREQ( "left", app.getWindow()->getClipboard()->getText().c_str() );
view->setToolbarVisible( false ); view->setToolbarVisible( false );
EXPECT_FALSE( view->isToolbarVisible() ); EXPECT_FALSE( view->isToolbarVisible() );
view->setToolbarVisible( true ); view->setToolbarVisible( true );
+9
View File
@@ -862,6 +862,15 @@ Git::Result Git::restore( const std::string& file, const std::string& projectDir
return gitSimple( String::format( "restore \"%s\"", file ), projectDir ); return gitSimple( String::format( "restore \"%s\"", file ), projectDir );
} }
Git::Result Git::restoreHead( const std::vector<std::string>& files,
const std::string& projectDir ) {
Result result;
std::vector<std::string> args{ "restore", "--source=HEAD", "--staged", "--worktree", "--" };
args.insert( args.end(), files.begin(), files.end() );
result.returnCode = git( args, projectDir, result.result );
return result;
}
Git::Result Git::reset( std::vector<std::string> files, const std::string& projectDir ) { Git::Result Git::reset( std::vector<std::string> files, const std::string& projectDir ) {
return gitSimple( String::format( "reset -q HEAD -- %s", asList( files ) ), projectDir ); return gitSimple( String::format( "reset -q HEAD -- %s", asList( files ) ), projectDir );
} }
+1
View File
@@ -359,6 +359,7 @@ class Git {
Result restore( std::vector<std::string> files, const std::string& projectDir = "" ); Result restore( std::vector<std::string> files, const std::string& projectDir = "" );
Result restore( const std::string& file, const std::string& projectDir = "" ); Result restore( const std::string& file, const std::string& projectDir = "" );
Result restoreHead( const std::vector<std::string>& files, const std::string& projectDir = "" );
Result reset( std::vector<std::string> files, const std::string& projectDir = "" ); Result reset( std::vector<std::string> files, const std::string& projectDir = "" );
+61 -14
View File
@@ -1488,6 +1488,9 @@ void GitPlugin::runFileOperation( std::vector<std::string> files, FileOperation
case FileOperation::Discard: case FileOperation::Discard:
result = mGit->restore( paths, repoPath ); result = mGit->restore( paths, repoPath );
break; break;
case FileOperation::RestoreHead:
result = mGit->restoreHead( paths, repoPath );
break;
} }
if ( result.fail() ) if ( result.fail() )
return result; return result;
@@ -1513,6 +1516,27 @@ void GitPlugin::discard( const std::vector<std::string>& files ) {
msgBox->showWhenReady(); msgBox->showWhenReady();
} }
void GitPlugin::discardConflicts( const std::vector<std::string>& files ) {
if ( files.empty() )
return;
String message =
files.size() == 1
? String::fromUtf8( String::format(
i18n( "git_confirm_discard_changes",
"Are you sure you want to discard the changes in file: \"%s\"?" )
.toUtf8(),
files.front() ) )
: i18n( "git_confirm_discard_changes",
"Are you sure you want to discard all file changes?" );
UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, message );
msgBox->on( Event::OnConfirm,
[this, files]( auto ) { runFileOperation( files, FileOperation::RestoreHead ); } );
msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } );
msgBox->setTitle( i18n( "git_confirm", "Confirm" ) );
msgBox->center();
msgBox->showWhenReady();
}
void GitPlugin::discard( const std::string& file ) { void GitPlugin::discard( const std::string& file ) {
UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox* msgBox = UIMessageBox::New(
UIMessageBox::OK_CANCEL, UIMessageBox::OK_CANCEL,
@@ -3751,7 +3775,8 @@ void GitPlugin::buildSidePanelTab() {
auto type = status->type; auto type = status->type;
if ( type == Git::GitStatusType::Staged || if ( type == Git::GitStatusType::Staged ||
type == Git::GitStatusType::Untracked || type == Git::GitStatusType::Untracked ||
type == Git::GitStatusType::Changed ) { type == Git::GitStatusType::Changed ||
type == Git::GitStatusType::Unmerged ) {
std::string repoPath; std::string repoPath;
if ( !status->files.empty() ) if ( !status->files.empty() )
repoPath = mGit->repoPath( status->files.front().file ); repoPath = mGit->repoPath( status->files.front().file );
@@ -3780,6 +3805,9 @@ void GitPlugin::buildSidePanelTab() {
menuAdd( menu, "git-discard-all", menuAdd( menu, "git-discard-all",
i18n( "git_discard_all", "Discard All" ) ); i18n( "git_discard_all", "Discard All" ) );
} }
if ( type == Git::GitStatusType::Unmerged )
menuAdd( menu, "git-discard-all",
i18n( "git_discard_all", "Discard All" ) );
menu->on( Event::OnItemClicked, [this, modelShared, repoPath, menu->on( Event::OnItemClicked, [this, modelShared, repoPath,
type]( const Event* event ) { type]( const Event* event ) {
@@ -3797,8 +3825,12 @@ void GitPlugin::buildSidePanelTab() {
unstage( model->getFiles( repoFullName( repoPath ), unstage( model->getFiles( repoFullName( repoPath ),
(Uint32)Git::GitStatusType::Staged ) ); (Uint32)Git::GitStatusType::Staged ) );
} else if ( id == "git-discard-all" ) { } else if ( id == "git-discard-all" ) {
discard( model->getFiles( repoFullName( repoPath ), auto discardFiles = model->getFiles( repoFullName( repoPath ),
(Uint32)Git::GitStatusType::Changed ) ); static_cast<Uint32>( type ) );
if ( type == Git::GitStatusType::Unmerged )
discardConflicts( discardFiles );
else
discard( discardFiles );
} else if ( id == "git-diff-staged" ) { } else if ( id == "git-diff-staged" ) {
diff( Git::DiffMode::DiffStaged, repoPath ); diff( Git::DiffMode::DiffStaged, repoPath );
} else if ( id == "git-diff-changed" ) { } else if ( id == "git-diff-changed" ) {
@@ -4001,19 +4033,31 @@ void GitPlugin::openFileStatusMenu( std::vector<Git::DiffFile> files ) {
hasUnstaged |= file.report.type != Git::GitStatusType::Staged; hasUnstaged |= file.report.type != Git::GitStatusType::Staged;
hasUnmerged |= file.report.type == Git::GitStatusType::Unmerged; hasUnmerged |= file.report.type == Git::GitStatusType::Unmerged;
} }
if ( hasUnmerged && !multiple ) { if ( hasUnmerged ) {
menuAdd( menu, "git-resolve-conflict", i18n( "git_resolve_conflict", "Resolve Conflict" ), if ( !multiple ) {
"diff-modified" ); menuAdd( menu, "git-resolve-conflict",
menuAdd( menu, "git-accept-ours", i18n( "git_accept_ours", "Accept Ours" ) ); i18n( "git_resolve_conflict", "Resolve Conflict" ), "diff-modified" );
menuAdd( menu, "git-accept-theirs", i18n( "git_accept_theirs", "Accept Theirs" ) ); menuAdd( menu, "git-accept-ours", i18n( "git_accept_ours", "Accept Ours" ) );
menu->on( Event::OnItemClicked, [this, file = files.front()]( const Event* event ) { menuAdd( menu, "git-accept-theirs", i18n( "git_accept_theirs", "Accept Theirs" ) );
}
menu->addSeparator();
menuAdd( menu, "git-discard", i18n( "git_discard", "Discard" ) );
menu->on( Event::OnItemClicked, [this, files = std::move( files )]( const Event* event ) {
const std::string id = event->getNode()->asType<UIMenuItem>()->getId(); const std::string id = event->getNode()->asType<UIMenuItem>()->getId();
if ( id == "git-resolve-conflict" ) if ( id == "git-resolve-conflict" )
openConflictResolver( file.file ); openConflictResolver( files.front().file );
else if ( id == "git-accept-ours" ) else if ( id == "git-accept-ours" )
acceptConflictSide( file.file, true ); acceptConflictSide( files.front().file, true );
else if ( id == "git-accept-theirs" ) else if ( id == "git-accept-theirs" )
acceptConflictSide( file.file, false ); acceptConflictSide( files.front().file, false );
else if ( id == "git-discard" ) {
std::vector<std::string> paths;
paths.reserve( files.size() );
for ( const auto& file : files )
if ( file.report.type == Git::GitStatusType::Unmerged )
paths.emplace_back( file.file );
discardConflicts( paths );
}
} ); } );
menu->showOverMouseCursor(); menu->showOverMouseCursor();
return; return;
@@ -4035,7 +4079,10 @@ void GitPlugin::openFileStatusMenu( std::vector<Git::DiffFile> files ) {
menu->addSeparator(); menu->addSeparator();
if ( hasUnstaged ) const bool hasDiscardable = std::any_of( files.begin(), files.end(), []( const auto& file ) {
return file.report.type == Git::GitStatusType::Changed;
} );
if ( hasDiscardable )
menuAdd( menu, "git-discard", i18n( "git_discard", "Discard" ) ); menuAdd( menu, "git-discard", i18n( "git_discard", "Discard" ) );
menu->on( Event::OnItemClicked, menu->on( Event::OnItemClicked,
@@ -4058,7 +4105,7 @@ void GitPlugin::openFileStatusMenu( std::vector<Git::DiffFile> files ) {
unstage( paths ); unstage( paths );
} else if ( id == "git-discard" ) { } else if ( id == "git-discard" ) {
for ( const auto& file : files ) for ( const auto& file : files )
if ( file.report.type != Git::GitStatusType::Staged ) if ( file.report.type == Git::GitStatusType::Changed )
paths.emplace_back( file.file ); paths.emplace_back( file.file );
if ( paths.size() == 1 ) if ( paths.size() == 1 )
discard( paths.front() ); discard( paths.front() );
+2 -1
View File
@@ -323,11 +323,12 @@ class GitPlugin : public PluginBase {
void unstage( const std::vector<std::string>& files ); void unstage( const std::vector<std::string>& files );
enum class FileOperation { Stage, Unstage, Discard }; enum class FileOperation { Stage, Unstage, Discard, RestoreHead };
void runFileOperation( std::vector<std::string> files, FileOperation operation ); void runFileOperation( std::vector<std::string> files, FileOperation operation );
void discard( const std::vector<std::string>& files ); void discard( const std::vector<std::string>& files );
void discardConflicts( const std::vector<std::string>& files );
void discard( const std::string& file ); void discard( const std::string& file );