diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml
index 03431ac23..73a0d9918 100644
--- a/bin/assets/i18n/de.xml
+++ b/bin/assets/i18n/de.xml
@@ -366,6 +366,9 @@ ecode versucht, externes Terminal zu öffnen.
Sind Sie sicher, dass Sie Zweig "%s" löschen möchten?
Sind Sie sicher, dass Sie aus Zweig "%s" vereinigen möchten?
Sind Sie sicher, dass Sie alle Änderungen verwerfen möchten in: "%s"?
+ Möchten Sie das folgende nicht versionierte Element wirklich dauerhaft löschen:
+%s?
+ Möchten Sie wirklich %d nicht versionierte Elemente dauerhaft löschen?
Sind Sie sicher, dass Sie den ausgewählten Stash fallen lassen möchten?
Sind Sie sicher, dass Sie lokale Änderungen gen Remoteserver pushen wollen?
Zweig erstellen
@@ -373,6 +376,7 @@ ecode versucht, externes Terminal zu öffnen.
Zweignamen eingeben:
Lokalen Zweig erstellen?
Löschen
+ Die folgenden nicht versionierten Elemente konnten nicht gelöscht werden:
HEAD-Diff
Änderungs-Diff
Stage-Diff
diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml
index 5ef9d1e30..86f9c50bc 100644
--- a/bin/assets/i18n/en.xml
+++ b/bin/assets/i18n/en.xml
@@ -350,6 +350,9 @@ ecode will try to open an external terminal.
Are you sure you want to delete the branch "%s"?
Are you sure you want to merge from branch "%s"?
Are you sure you want to discard the changes in file: "%s"?
+ Are you sure you want to permanently delete the untracked item:
+%s?
+ Are you sure you want to permanently delete %d untracked items?
Do you want to drop the selected stash?
Are you sure you want to push the local changes to the remote server?
Create Branch
@@ -357,6 +360,7 @@ ecode will try to open an external terminal.
Enter the name for the branch:
Create local branch?
Delete
+ Could not delete the following untracked items:
Diff HEAD
Diff Changed
Diff Staged
diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml
index f6e850348..771ec4d5f 100644
--- a/bin/assets/i18n/fr.xml
+++ b/bin/assets/i18n/fr.xml
@@ -349,6 +349,9 @@ ecode tentera d'ouvrir un terminal externe.
Êtes-vous sûr de vouloir supprimer la branche "%s" ?
Êtes-vous sûr de vouloir fusionner à partir de la branche "%s" ?
Êtes-vous sûr de vouloir annuler les modifications du fichier: "%s" ?
+ Êtes-vous sûr de vouloir supprimer définitivement l’élément non suivi :
+%s ?
+ Êtes-vous sûr de vouloir supprimer définitivement %d éléments non suivis ?
Voulez-vous supprimer le stash sélectionné ?
Êtes-vous sûr de vouloir envoyer les modifications locales vers le serveur distant ?
Créer une branche
@@ -356,6 +359,7 @@ ecode tentera d'ouvrir un terminal externe.
Entrez le nom de la branche :
Créer une branche locale ?
Supprimer
+ Impossible de supprimer les éléments non suivis suivants :
Diff HEAD
Diff des modifications
Diff Staged
diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml
index d91d42f44..03873377e 100644
--- a/bin/assets/i18n/zh.xml
+++ b/bin/assets/i18n/zh.xml
@@ -271,6 +271,9 @@
Are you sure you want to delete the branch "%s"?
Are you sure you want to merge from branch "%s"?
Are you sure you want to discard the changes in file: "%s"?
+ 确定要永久删除以下未跟踪项吗?
+%s
+ 确定要永久删除 %d 个未跟踪项吗?
Do you want to drop the selected stash?
Are you sure you want to push the local changes to the remote server?
创建分支
@@ -278,6 +281,7 @@
Enter the name for the branch:
新建本地分支?
删除
+ 无法删除以下未跟踪项:
更改差异
Discard
Drop Stash
diff --git a/include/eepp/core/string.hpp b/include/eepp/core/string.hpp
index ec5f26bb5..0f1700bcb 100644
--- a/include/eepp/core/string.hpp
+++ b/include/eepp/core/string.hpp
@@ -112,9 +112,27 @@ class EE_API String {
/** Escape string sequence */
static String escape( const String& str );
+ /** Escape byte string sequence */
+ static std::string escape( std::string_view str );
+
+ static std::string escape( const std::string& str ) {
+ return escape( std::string_view{ str } );
+ }
+
+ static std::string escape( const char* str ) { return escape( std::string_view{ str } ); }
+
/** Unescape string sequence */
static String unescape( const String& str );
+ /** Unescape byte string sequence */
+ static std::string unescape( std::string_view str );
+
+ static std::string unescape( const std::string& str ) {
+ return unescape( std::string_view{ str } );
+ }
+
+ static std::string unescape( const char* str ) { return unescape( std::string_view{ str } ); }
+
/** @return string hash */
static String::HashType hash( const std::string& str );
diff --git a/src/eepp/core/string.cpp b/src/eepp/core/string.cpp
index d29519981..8fc2855d6 100644
--- a/src/eepp/core/string.cpp
+++ b/src/eepp/core/string.cpp
@@ -614,9 +614,10 @@ Int64 String::BMH::find( std::string_view haystack, std::string_view needle,
return find( haystack, needle, haystackOffset, occ, caseInsensitive );
}
-String String::escape( const String& str ) {
- String output;
- for ( size_t i = 0; i < str.size(); i++ ) {
+template static Output escapeStringSequence( const Input& str ) {
+ Output output;
+ output.reserve( str.size() );
+ for ( size_t i = 0; i < str.size(); ++i ) {
switch ( str[i] ) {
case '\r':
output += "\\r";
@@ -648,6 +649,14 @@ String String::escape( const String& str ) {
return output;
}
+String String::escape( const String& str ) {
+ return escapeStringSequence( str );
+}
+
+std::string String::escape( std::string_view str ) {
+ return escapeStringSequence( str );
+}
+
String String::unescape( const String& str ) {
String output;
bool lastWasEscape = false;
@@ -770,6 +779,63 @@ String String::unescape( const String& str ) {
return output;
}
+std::string String::unescape( std::string_view str ) {
+ std::string output;
+ output.reserve( str.size() );
+ for ( size_t i = 0; i < str.size(); ++i ) {
+ if ( str[i] != '\\' || i + 1 >= str.size() ) {
+ output += str[i];
+ continue;
+ }
+
+ const char escaped = str[++i];
+ switch ( escaped ) {
+ case '\\':
+ case '\'':
+ case '"':
+ case '?':
+ output += escaped;
+ break;
+ case 'r':
+ output += '\r';
+ break;
+ case 't':
+ output += '\t';
+ break;
+ case 'n':
+ output += '\n';
+ break;
+ case 'a':
+ output += '\a';
+ break;
+ case 'b':
+ output += '\b';
+ break;
+ case 'f':
+ output += '\f';
+ break;
+ case 'v':
+ output += '\v';
+ break;
+ default:
+ if ( escaped < '0' || escaped > '7' ) {
+ output += '\\';
+ output += escaped;
+ break;
+ }
+ unsigned char value = static_cast( escaped - '0' );
+ for ( int digit = 1;
+ digit < 3 && i + 1 < str.size() && str[i + 1] >= '0' && str[i + 1] <= '7';
+ ++digit ) {
+ value = static_cast( value * 8 + str[++i] - '0' );
+ }
+ output += static_cast( value );
+ break;
+ }
+ }
+ return output;
+}
+
String::HashType String::hash( const std::string& str ) {
return String::hash( str.c_str() );
}
diff --git a/src/eepp/ui/doc/languages/markdown.cpp b/src/eepp/ui/doc/languages/markdown.cpp
index 4461d89df..79198f34a 100644
--- a/src/eepp/ui/doc/languages/markdown.cpp
+++ b/src/eepp/ui/doc/languages/markdown.cpp
@@ -15,6 +15,7 @@ void addMarkdown() {
{ "Markdown",
{ "%.md$", "%.markdown$" },
{
+ { { "```text", "```" }, "string" },
{ { "```[%w \t%+%-#]+", "```" }, "function", dynSyntax },
{ { "include", "#comments" }, "normal" },
{ { "include", "#strings" }, "normal" },
diff --git a/src/tests/unit_tests/gitconflict_tests.cpp b/src/tests/unit_tests/gitconflict_tests.cpp
index 4ec037a0f..c017ea485 100644
--- a/src/tests/unit_tests/gitconflict_tests.cpp
+++ b/src/tests/unit_tests/gitconflict_tests.cpp
@@ -1,4 +1,4 @@
-#include "utest.h"
+#include "utest.hpp"
#include "../../tools/ecode/plugins/git/git.hpp"
#include
@@ -25,6 +25,26 @@ struct GitTempDirectory {
} // namespace
+UTEST( GitStatus, DecodesQuotedUntrackedPaths ) {
+ const std::string gitPath = Sys::which( "git" );
+ if ( gitPath.empty() )
+ UTEST_SKIP( "Git is not installed" );
+
+ GitTempDirectory temp;
+ Git git( temp.path.string(), gitPath );
+ std::string output;
+ ASSERT_EQ( EXIT_SUCCESS,
+ git.git( std::vector{ "init" }, temp.path.string(), output ) );
+ const std::string relativePath = "Untitled 1--conversation doc.yjs";
+ ASSERT_TRUE( FileSystem::fileWrite( ( temp.path / relativePath ).string(), "test" ) );
+
+ auto status = git.status( false, temp.path.string() );
+ ASSERT_EQ( 1u, status.files.size() );
+ ASSERT_EQ( 1u, status.files.begin()->second.size() );
+ EXPECT_STDSTREQ( relativePath, status.files.begin()->second.front().file );
+ EXPECT_EQ( Git::GitStatusType::Untracked, status.files.begin()->second.front().report.type );
+}
+
UTEST( GitConflict, ParsesNulDelimitedStageRecordsAndUnusualPaths ) {
if ( Sys::which( "git" ).empty() )
UTEST_SKIP( "Git is not installed" );
diff --git a/src/tests/unit_tests/stringsoperations_tests.cpp b/src/tests/unit_tests/stringsoperations_tests.cpp
index 1c0f10770..49a4fa3f6 100644
--- a/src/tests/unit_tests/stringsoperations_tests.cpp
+++ b/src/tests/unit_tests/stringsoperations_tests.cpp
@@ -1,4 +1,4 @@
-#include "utest.h"
+#include "utest.hpp"
#include
#include
#include
@@ -109,6 +109,19 @@ UTEST( String, reusableFormattingAndUtf8Assignment ) {
EXPECT_EQ( utf8Storage, reusableUtf8.data() );
}
+UTEST( String, byteStringEscapeAndUnescape ) {
+ const std::string raw = "line one\r\nline two\t\a\b\f\v";
+ const std::string escaped = String::escape( raw );
+ EXPECT_STDSTREQ( "line one\\r\\nline two\\t\\a\\b\\f\\v", escaped );
+ const std::string unescaped = String::unescape( escaped );
+ EXPECT_STDSTREQ( raw, unescaped );
+ const std::string quoted = String::unescape( R"(quoted\" slash\\ unknown\q)" );
+ EXPECT_STDSTREQ( "quoted\" slash\\ unknown\\q", quoted );
+
+ const std::string utf8Bytes = String::unescape( std::string_view{ R"(\303\261)" } );
+ EXPECT_STDSTREQ( "ñ", utf8Bytes );
+}
+
UTEST( FileSystem, fileCountLines ) {
std::string path = Sys::getTempPath() + "eepp_test_count_lines.txt";
FileSystem::fileWrite( path, "A\nB\nC" );
diff --git a/src/tools/ecode/plugins/git/git.cpp b/src/tools/ecode/plugins/git/git.cpp
index 1c75c5105..d844143fa 100644
--- a/src/tools/ecode/plugins/git/git.cpp
+++ b/src/tools/ecode/plugins/git/git.cpp
@@ -1300,6 +1300,19 @@ std::string Git::repoPath( const std::string& file ) {
return mProjectPath;
}
+static void appendDecodedGitPath( std::string& decoded, std::string_view path ) {
+ if ( path.size() < 2 || path.front() != '"' || path.back() != '"' ) {
+ decoded.append( path );
+ return;
+ }
+
+ auto unescaped = String::unescape( path.substr( 1, path.size() - 2 ) );
+ if ( decoded.empty() )
+ decoded = std::move( unescaped );
+ else
+ decoded += unescaped;
+}
+
Git::Result Git::gitSimple( const std::string& cmd, const std::string& projectDir ) {
std::string buf;
int retCode = git( cmd, projectDir, buf );
@@ -1381,7 +1394,8 @@ Git::Status Git::status( bool recurseSubmodules, const std::string& projectDir )
file = file.substr( rranges[1].start, rranges[1].end - rranges[1].start );
}
- std::string filePath = subModulePath + file;
+ std::string filePath{ subModulePath };
+ appendDecodedGitPath( filePath, file );
auto repo = repoName( filePath, false, projectDir );
auto repoIt = s.files.find( repo );
bool found = false;
@@ -1449,7 +1463,8 @@ Git::Status Git::status( bool recurseSubmodules, const std::string& projectDir )
file.substr( matches[3].start, matches[3].end - matches[3].start );
}
- auto filePath = subModulePath + file;
+ std::string filePath{ subModulePath };
+ appendDecodedGitPath( filePath, file );
auto repo = repoName( filePath, false, projectDir );
auto repoIt = s.files.find( repo );
GitStatusReport status = { GitStatus::NotSet, GitStatusType::Untracked,
diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp
index 8e72b9b52..5656c2bff 100644
--- a/src/tools/ecode/plugins/git/gitplugin.cpp
+++ b/src/tools/ecode/plugins/git/gitplugin.cpp
@@ -1442,6 +1442,56 @@ void GitPlugin::unstage( const std::vector& files ) {
runFileOperation( files, FileOperation::Unstage );
}
+void GitPlugin::deleteUntrackedFiles( std::vector files ) {
+ std::string projectPath = this->projectPath();
+ if ( files.empty() || projectPath.empty() )
+ return;
+
+ String message =
+ files.size() == 1
+ ? String::fromUtf8( String::format(
+ i18n( "git_confirm_delete_untracked_item",
+ "Are you sure you want to permanently delete the untracked item:\n%s?" )
+ .toUtf8(),
+ files.front() ) )
+ : String::fromUtf8( String::format(
+ i18n( "git_confirm_delete_untracked_items",
+ "Are you sure you want to permanently delete %d untracked items?" )
+ .toUtf8(),
+ static_cast( files.size() ) ) );
+ std::string failureMessage =
+ i18n( "git_delete_untracked_failed", "Could not delete the following untracked items:" )
+ .toUtf8();
+ UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, message );
+ msgBox->on(
+ Event::OnConfirm, [this, files = std::move( files ), projectPath = std::move( projectPath ),
+ failureMessage = std::move( failureMessage )]( const Event* ) mutable {
+ runAsync(
+ [files = std::move( files ), projectPath = std::move( projectPath ),
+ failureMessage = std::move( failureMessage )]() {
+ Git::Result result;
+ for ( const auto& file : files ) {
+ const std::string path = isPath( file ) ? file : projectPath + file;
+ FileInfo info( path, true );
+ const bool removed = info.isDirectory() ? FileSystem::dirRemoveAll( path )
+ : FileSystem::fileRemove( path );
+ if ( !removed ) {
+ result.returnCode = EXIT_FAILURE;
+ if ( result.result.empty() )
+ result.result = failureMessage;
+ result.result += "\n" + file;
+ }
+ }
+ return result;
+ },
+ true, false, false, false, true );
+ } );
+ msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } );
+ msgBox->setTitle( i18n( "delete", "Delete" ) );
+ msgBox->center();
+ msgBox->showWhenReady();
+}
+
void GitPlugin::runFileOperation( std::vector files, FileOperation operation ) {
if ( files.empty() )
return;
@@ -3723,6 +3773,8 @@ void GitPlugin::buildSidePanelTab() {
type == Git::GitStatusType::Changed )
menuAdd( menu, "git-stage-all", i18n( "git_stage_all", "Stage All" ),
"diff-added" );
+ if ( type == Git::GitStatusType::Untracked )
+ menuAdd( menu, "git-delete-untracked", i18n( "delete", "Delete" ) );
if ( type == Git::GitStatusType::Changed ) {
menuAdd( menu, "git-diff-changed",
@@ -3750,6 +3802,10 @@ void GitPlugin::buildSidePanelTab() {
} else if ( id == "git-unstage-all" ) {
unstage( model->getFiles( repoFullName( repoPath ),
(Uint32)Git::GitStatusType::Staged ) );
+ } else if ( id == "git-delete-untracked" ) {
+ deleteUntrackedFiles( model->getFiles(
+ repoFullName( repoPath ),
+ static_cast( Git::GitStatusType::Untracked ) ) );
} else if ( id == "git-discard-all" ) {
auto discardFiles = model->getFiles( repoFullName( repoPath ),
static_cast( type ) );
@@ -3954,10 +4010,12 @@ void GitPlugin::openFileStatusMenu( std::vector files ) {
bool hasStaged = false;
bool hasUnstaged = false;
bool hasUnmerged = false;
+ bool allUntracked = true;
for ( const auto& file : files ) {
hasStaged |= file.report.type == Git::GitStatusType::Staged;
hasUnstaged |= file.report.type != Git::GitStatusType::Staged;
hasUnmerged |= file.report.type == Git::GitStatusType::Unmerged;
+ allUntracked &= file.report.type == Git::GitStatusType::Untracked;
}
if ( hasUnmerged ) {
if ( !multiple ) {
@@ -4003,14 +4061,19 @@ void GitPlugin::openFileStatusMenu( std::vector files ) {
if ( hasStaged )
menuAdd( menu, "git-unstage", i18n( "git_unstage", "Unstage" ), "diff-removed" );
- menu->addSeparator();
-
const bool hasDiscardable = std::any_of( files.begin(), files.end(), []( const auto& file ) {
return file.report.type == Git::GitStatusType::Changed;
} );
+
+ if ( hasDiscardable || allUntracked )
+ menu->addSeparator();
+
if ( hasDiscardable )
menuAdd( menu, "git-discard", i18n( "git_discard", "Discard" ) );
+ if ( allUntracked )
+ menuAdd( menu, "git-delete-untracked", i18n( "delete", "Delete" ) );
+
menu->on( Event::OnItemClicked,
[this, files = std::move( files )]( const Event* event ) mutable {
if ( !mGit )
@@ -4037,6 +4100,10 @@ void GitPlugin::openFileStatusMenu( std::vector files ) {
discard( paths.front() );
else
discard( paths );
+ } else if ( id == "git-delete-untracked" ) {
+ for ( const auto& file : files )
+ paths.emplace_back( file.file );
+ deleteUntrackedFiles( std::move( paths ) );
} else if ( id == "git-open-file" ) {
for ( const auto& file : files )
openFile( file.file );
diff --git a/src/tools/ecode/plugins/git/gitplugin.hpp b/src/tools/ecode/plugins/git/gitplugin.hpp
index 69a8c22cb..be7fcb14c 100644
--- a/src/tools/ecode/plugins/git/gitplugin.hpp
+++ b/src/tools/ecode/plugins/git/gitplugin.hpp
@@ -314,6 +314,8 @@ class GitPlugin : public PluginBase {
void unstage( const std::vector& files );
+ void deleteUntrackedFiles( std::vector files );
+
enum class FileOperation { Stage, Unstage, Discard, RestoreHead };
void runFileOperation( std::vector files, FileOperation operation );