Show relevant ignore pattern for items in file browser
This commit is contained in:
parent
fc47cea9e5
commit
3ffe62b289
|
@ -19,6 +19,7 @@ set(HEADER_FILES
|
|||
syncthingconnectionsettings.h
|
||||
syncthingnotifier.h
|
||||
syncthingconfig.h
|
||||
syncthingignorepattern.h
|
||||
syncthingprocess.h
|
||||
syncthingservice.h
|
||||
qstringhash.h
|
||||
|
@ -31,6 +32,7 @@ set(SRC_FILES
|
|||
syncthingconnectionsettings.cpp
|
||||
syncthingnotifier.cpp
|
||||
syncthingconfig.cpp
|
||||
syncthingignorepattern.cpp
|
||||
syncthingprocess.cpp
|
||||
syncthingservice.cpp
|
||||
utils.cpp)
|
||||
|
|
|
@ -59,6 +59,11 @@ enum class SyncthingItemType {
|
|||
};
|
||||
|
||||
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
|
||||
/// \brief The matching ignore pattern was not initialized for this item.
|
||||
static constexpr auto ignorePatternNotInitialized = std::numeric_limits<std::size_t>::max();
|
||||
/// \brief The item did not match any of the current ignore patterns.
|
||||
static constexpr auto ignorePatternNoMatch = ignorePatternNotInitialized - 1;
|
||||
|
||||
/// \brief The name of the filesystem item or error/loading message in case of those item types.
|
||||
QString name;
|
||||
/// \brief The modification time. Only populated with a meaningful value for files and directories.
|
||||
|
@ -75,6 +80,9 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem {
|
|||
QString path;
|
||||
/// \brief The index of the item within its parent.
|
||||
std::size_t index = std::size_t();
|
||||
/// \brief The index of the ignore pattern (in the current list of ignore patterns) this item matches.
|
||||
/// \remarks Not populated by default.
|
||||
std::size_t ignorePattern = ignorePatternNotInitialized;
|
||||
/// \brief The level of nesting, does *not* include levels of the prefix.
|
||||
int level = 0;
|
||||
/// \brief Whether children are populated (depends on the requested level).
|
||||
|
|
|
@ -0,0 +1,408 @@
|
|||
#include "./syncthingignorepattern.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
/// \cond
|
||||
namespace SyncthingIgnorePatternState {
|
||||
enum State {
|
||||
Escaping, // passed the esacping character "\"
|
||||
AppendRangeLower, // passed a "[" marking the start of a character range
|
||||
AppendRangeUpper, // passed a "-" within a range marking the start of an upper-bound range character
|
||||
AppendAlternative, // passed a "{" marking the start of an alternative set
|
||||
MatchVerbatimly, // initial/default state
|
||||
MatchRange, // passed a "]" marking the end of a character range
|
||||
MatchAlternatives, // passed a "}" marking the end of an alternative set
|
||||
MatchAny, // passed the "?" character that allows matching any character but the path separator
|
||||
MatchManyAny, // passed the "*" character that allows matching many arbitrary characters except the path separator
|
||||
MatchManyAnyIncludingDirSep, // passed a sequence of two "*" characters allowing to match also the path separator
|
||||
};
|
||||
|
||||
struct Asterisk {
|
||||
QString::const_iterator pos;
|
||||
SyncthingIgnorePatternState::State state;
|
||||
bool visited = false;
|
||||
};
|
||||
|
||||
struct CharacterRange {
|
||||
QChar lowerBound, upperBound;
|
||||
};
|
||||
|
||||
struct AlternativeRange {
|
||||
explicit AlternativeRange(QString::const_iterator beg)
|
||||
: beg(beg)
|
||||
, end(beg)
|
||||
{
|
||||
}
|
||||
QString::const_iterator beg, end;
|
||||
};
|
||||
} // namespace SyncthingIgnorePatternState
|
||||
/// \endcond
|
||||
|
||||
/*!
|
||||
* \brief Parses the specified \a pattern populating the struct's fields.
|
||||
*/
|
||||
SyncthingIgnorePattern::SyncthingIgnorePattern(QString &&pattern)
|
||||
: pattern(std::move(pattern))
|
||||
{
|
||||
if (glob.startsWith(QLatin1String("//"))) {
|
||||
comment = true;
|
||||
ignore = false;
|
||||
return;
|
||||
}
|
||||
glob = this->pattern;
|
||||
for (;;) {
|
||||
if (glob.startsWith(QLatin1String("!"))) {
|
||||
ignore = !ignore;
|
||||
glob.remove(0, 1);
|
||||
} else if (glob.startsWith(QLatin1String("(?i)"))) {
|
||||
caseInsensitive = true;
|
||||
glob.remove(0, 4);
|
||||
} else if (glob.startsWith(QLatin1String("(?d)"))) {
|
||||
allowRemovalOnParentDirRemoval = true;
|
||||
glob.remove(0, 4);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Moves the ignore pattern.
|
||||
*/
|
||||
SyncthingIgnorePattern::SyncthingIgnorePattern(SyncthingIgnorePattern &&) = default;
|
||||
|
||||
/*!
|
||||
* \brief Destroys the ignore pattern.
|
||||
*/
|
||||
SyncthingIgnorePattern::~SyncthingIgnorePattern()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Matches the assigned glob against the specified \a path.
|
||||
* \remarks
|
||||
* - Returns always false if the pattern is flagged as comment or the glob is empty.
|
||||
* - This function tries to follow rules outlined on https://docs.syncthing.net/users/ignoring.html.
|
||||
* - The specified \a path is *not* supposed to start with a "/". It must always be a path relative to the root of the
|
||||
* Syncthing folder it is contained by. A pattern that is only supposed to match from the root of the Syncthing folder
|
||||
* is supposed to start with a "/", though.
|
||||
* - This function probably doesn't work if the pattern or \a path contain a surrogate pair.
|
||||
*/
|
||||
bool SyncthingIgnorePattern::matches(const QString &path) const
|
||||
{
|
||||
if (comment || glob.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get iterators
|
||||
auto globIter = glob.begin(), globEnd = glob.end();
|
||||
auto pathIter = path.begin(), pathEnd = path.end();
|
||||
|
||||
// handle pattners starting with "/" indicating the pattern must match from the root (see last remark in docstring)
|
||||
static constexpr auto pathSep = QChar('/');
|
||||
const auto matchFromRoot = *globIter == pathSep;
|
||||
if (matchFromRoot) {
|
||||
++globIter;
|
||||
}
|
||||
|
||||
// define variables to track the state when processing the glob pattern
|
||||
using namespace SyncthingIgnorePatternState;
|
||||
auto state = MatchVerbatimly;
|
||||
auto escapedState = MatchVerbatimly;
|
||||
auto inAsterisk = false;
|
||||
asterisks.clear();
|
||||
|
||||
// define behavior to handle the current character in the glob pattern not matching the current pattern in the path
|
||||
const auto handleMismatch = [&, this] {
|
||||
// fail the match immediately if the glob pattern started with a "/" indicating it is supposed to match only from the root
|
||||
if (matchFromRoot) {
|
||||
return false;
|
||||
}
|
||||
// deal with the mismatch by trying to match previous asterisks more greedily
|
||||
while (!asterisks.empty() && asterisks.back().visited) {
|
||||
// do not consider asterisks we have already visited, though (as it would lead to an endless loop)
|
||||
asterisks.pop_back();
|
||||
}
|
||||
if (!asterisks.empty()) {
|
||||
// rewind back to when we have passed the last non-visited asterisk
|
||||
auto &asterisk = asterisks.back();
|
||||
globIter = asterisk.pos;
|
||||
inAsterisk = asterisk.visited = true;
|
||||
state = asterisk.state;
|
||||
return true;
|
||||
}
|
||||
// deal with the mismatch by checking the path as of the next path element
|
||||
for (; pathIter != pathEnd; ++pathIter) {
|
||||
// forward to the next path separator
|
||||
if (*pathIter != pathSep) {
|
||||
continue;
|
||||
}
|
||||
// skip the path separator itself and give up when the end of the path is reached
|
||||
if (++pathIter == pathEnd) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// give up when the end of the path is reached
|
||||
if (pathIter == pathEnd) {
|
||||
return false;
|
||||
}
|
||||
// start matching the glob pattern from the beginning
|
||||
globIter = glob.begin();
|
||||
asterisks.clear();
|
||||
inAsterisk = false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// define function to handle single match
|
||||
const auto handleSingleMatch = [&, this] {
|
||||
// proceed with the next characters on a match
|
||||
++globIter;
|
||||
inAsterisk = false;
|
||||
if (!asterisks.empty()) {
|
||||
asterisks.back().visited = false;
|
||||
}
|
||||
};
|
||||
|
||||
// define function to match single character against the current character in the path
|
||||
const auto matchSingleChar
|
||||
= [&, this](QChar singleChar) { return caseInsensitive ? pathIter->toCaseFolded() == singleChar.toCaseFolded() : *pathIter == singleChar; };
|
||||
|
||||
// try to match each character of the glob against a character in the path
|
||||
match:
|
||||
while (globIter != globEnd) {
|
||||
// decide what to do next depending on the current glob pattern character and state transitioning the state accordingly
|
||||
switch (state) {
|
||||
case Escaping:
|
||||
// treat every character as-is in "escaping" state
|
||||
state = escapedState;
|
||||
break;
|
||||
default:
|
||||
// transition state according to special meaning of the current glob pattern character
|
||||
switch (globIter->unicode()) {
|
||||
case '\\':
|
||||
// transition into "escaping" state
|
||||
escapedState = state;
|
||||
state = Escaping;
|
||||
break;
|
||||
case '[':
|
||||
state = AppendRangeLower;
|
||||
characterRange.clear();
|
||||
++globIter;
|
||||
continue;
|
||||
case ']':
|
||||
switch (state) {
|
||||
case AppendRangeLower:
|
||||
case AppendRangeUpper:
|
||||
state = MatchRange;
|
||||
break;
|
||||
default:
|
||||
state = MatchVerbatimly;
|
||||
}
|
||||
break;
|
||||
case '-':
|
||||
switch (state) {
|
||||
case AppendRangeLower:
|
||||
state = AppendRangeUpper;
|
||||
++globIter;
|
||||
continue;
|
||||
default:
|
||||
state = MatchVerbatimly;
|
||||
}
|
||||
break;
|
||||
case '{':
|
||||
switch (state) {
|
||||
case AppendAlternative:
|
||||
continue;
|
||||
default:
|
||||
state = AppendAlternative;
|
||||
alternatives.clear();
|
||||
alternatives.emplace_back(++globIter);
|
||||
}
|
||||
continue;
|
||||
case '}':
|
||||
switch (state) {
|
||||
case AppendAlternative:
|
||||
alternatives.back().end = globIter;
|
||||
state = MatchAlternatives;
|
||||
break;
|
||||
default:
|
||||
state = MatchVerbatimly;
|
||||
}
|
||||
break;
|
||||
case ',':
|
||||
switch (state) {
|
||||
case AppendAlternative:
|
||||
alternatives.back().end = globIter;
|
||||
alternatives.emplace_back(++globIter);
|
||||
continue;
|
||||
default:
|
||||
state = MatchVerbatimly;
|
||||
}
|
||||
break;
|
||||
case '?':
|
||||
// transition into "match any" state
|
||||
state = MatchAny;
|
||||
break;
|
||||
case '*':
|
||||
// transition into one of the "match many any" state (depending on current state)
|
||||
switch (state) {
|
||||
case MatchManyAny:
|
||||
state = MatchManyAnyIncludingDirSep;
|
||||
break;
|
||||
default:
|
||||
state = MatchManyAny;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// try to match/append all other non-special characters as-is
|
||||
switch (state) {
|
||||
case AppendRangeLower:
|
||||
case AppendRangeUpper:
|
||||
case AppendAlternative:
|
||||
break;
|
||||
default:
|
||||
state = MatchVerbatimly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// proceed according to state
|
||||
switch (state) {
|
||||
case Escaping:
|
||||
// proceed with the next character in the glob pattern which will be matched as-is (even if it is special)
|
||||
[[fallthrough]];
|
||||
case AppendAlternative:
|
||||
// just move on to the next character (alternatives are populated completely in the previous switch-case)
|
||||
++globIter;
|
||||
break;
|
||||
case AppendRangeLower:
|
||||
// add the current character in the glob pattern as start of a new range
|
||||
characterRange.emplace_back().lowerBound = *globIter++;
|
||||
break;
|
||||
case AppendRangeUpper:
|
||||
// add the current character in the glob pattern as end of a new or the current range
|
||||
(characterRange.empty() ? characterRange.emplace_back() : characterRange.back()).upperBound = *globIter++;
|
||||
state = AppendRangeLower;
|
||||
break;
|
||||
case MatchVerbatimly:
|
||||
// match the current character in the glob pattern verbatimly against the current character in the path
|
||||
if (pathIter != pathEnd && matchSingleChar(*globIter)) {
|
||||
++pathIter;
|
||||
handleSingleMatch();
|
||||
} else if (inAsterisk && (asterisks.back().state == MatchManyAnyIncludingDirSep || (pathIter == pathEnd || *pathIter != pathSep))) {
|
||||
// consider the path character dealt with despite no match if we have just passed an asterisk in the glob pattern
|
||||
if (pathIter != pathEnd) {
|
||||
++pathIter;
|
||||
} else {
|
||||
inAsterisk = false;
|
||||
}
|
||||
} else if (!handleMismatch()) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case MatchRange:
|
||||
// match the concluded character range in the glob pattern against the current character in the path
|
||||
if (pathIter != pathEnd) {
|
||||
auto inRange = false;
|
||||
for (const auto &bounds : characterRange) {
|
||||
if ((!bounds.upperBound.isNull() && *pathIter >= bounds.lowerBound && *pathIter <= bounds.upperBound)
|
||||
|| (bounds.upperBound.isNull() && matchSingleChar(bounds.lowerBound))) {
|
||||
inRange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (inRange) {
|
||||
characterRange.clear();
|
||||
state = MatchVerbatimly;
|
||||
++pathIter;
|
||||
handleSingleMatch();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!handleMismatch()) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case MatchAlternatives:
|
||||
// match the current alternatives as of the current character in the path
|
||||
if (pathIter != pathEnd) {
|
||||
const auto pathStart = pathIter;
|
||||
for (auto &alternative : alternatives) {
|
||||
// match characters in the alternative against the path
|
||||
// note: Special characters like "*" are matched verbatimly. Is that the correct behavior?
|
||||
pathIter = pathStart;
|
||||
for (; alternative.beg != alternative.end && pathIter != pathEnd; ++alternative.beg) {
|
||||
if (*alternative.beg == QChar('\\')) {
|
||||
continue;
|
||||
}
|
||||
if (!matchSingleChar(*alternative.beg)) {
|
||||
break;
|
||||
}
|
||||
++pathIter;
|
||||
}
|
||||
// go with the first alternative that fully matched
|
||||
// note: What is the correct behavior? Should this be most/least greedy (matching the longest/shortest possible alternative) instead?
|
||||
if (alternative.beg == alternative.end) {
|
||||
alternatives.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alternatives.empty()) {
|
||||
state = MatchVerbatimly;
|
||||
handleSingleMatch();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!handleMismatch()) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case MatchAny:
|
||||
// allow the current character in the path to be anything but a path separator; otherwise consider it as mismatch as in the case for an exact match
|
||||
if (pathIter == pathEnd || *pathIter != pathSep) {
|
||||
++globIter, ++pathIter;
|
||||
} else if (!handleMismatch()) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case MatchManyAny: {
|
||||
// take record of the asterisks
|
||||
auto &glob = asterisks.emplace_back();
|
||||
glob.pos = ++globIter;
|
||||
glob.state = MatchManyAny;
|
||||
inAsterisk = true;
|
||||
break;
|
||||
}
|
||||
case MatchManyAnyIncludingDirSep: {
|
||||
// take record of the second asterisks
|
||||
auto &glob = asterisks.back();
|
||||
glob.pos = ++globIter;
|
||||
glob.state = MatchManyAnyIncludingDirSep;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check whether all characters of the glob have been matched against all characters of the path
|
||||
if (globIter == globEnd) {
|
||||
if (pathIter == pathEnd) {
|
||||
return true;
|
||||
}
|
||||
// try again as of the next path segment if the glob fully matched but there are still characters in the path to be matched
|
||||
// note: This allows "foo" to match against "foo/foo" even tough the glob characters have already consumed after matching the first path segment.
|
||||
if (!matchFromRoot && *pathIter == pathSep) {
|
||||
state = MatchVerbatimly;
|
||||
++pathIter;
|
||||
globIter = glob.begin();
|
||||
asterisks.clear();
|
||||
inAsterisk = false;
|
||||
goto match;
|
||||
}
|
||||
}
|
||||
|
||||
// consider the match a success if there are still characters in the path to be matched but the glob ended with a "*"
|
||||
return state == MatchManyAny || state == MatchManyAnyIncludingDirSep;
|
||||
}
|
||||
|
||||
} // namespace Data
|
|
@ -0,0 +1,50 @@
|
|||
#ifndef DATA_SYNCTHINGIGNOREPATTERN_H
|
||||
#define DATA_SYNCTHINGIGNOREPATTERN_H
|
||||
|
||||
#include "./global.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace Data {
|
||||
|
||||
namespace SyncthingIgnorePatternState {
|
||||
struct Asterisk;
|
||||
struct CharacterRange;
|
||||
struct AlternativeRange;
|
||||
} // namespace SyncthingIgnorePatternState
|
||||
|
||||
/*!
|
||||
* \brief The SyncthingIgnorePattern struct allows matching a Syncthing ignore pattern against a path.
|
||||
* \sa
|
||||
* - https://docs.syncthing.net/users/ignoring.html
|
||||
* - https://docs.syncthing.net/rest/db-ignores-get.html
|
||||
*/
|
||||
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingIgnorePattern {
|
||||
explicit SyncthingIgnorePattern(QString &&pattern);
|
||||
SyncthingIgnorePattern(const SyncthingIgnorePattern &) = delete;
|
||||
SyncthingIgnorePattern(SyncthingIgnorePattern &&);
|
||||
~SyncthingIgnorePattern();
|
||||
bool matches(const QString &path) const;
|
||||
|
||||
/// \brief The full ignore pattern as passed to the c'tor (unless modified).
|
||||
QString pattern;
|
||||
/// \brief The part of the pattern that will actually be used for globbing by the matches() function.
|
||||
QString glob;
|
||||
/// \brief Whether the pattern is a comment.
|
||||
bool comment = false;
|
||||
/// \brief Whether the pattern will lead to ignoring matching items (the default).
|
||||
bool ignore = true;
|
||||
/// \brief Whether the pattern will match case-insensetively.
|
||||
bool caseInsensitive = false;
|
||||
/// \brief Whether matching items may be removed when the otherwise empty parent directory would be removed.
|
||||
bool allowRemovalOnParentDirRemoval = false;
|
||||
|
||||
private:
|
||||
mutable std::vector<SyncthingIgnorePatternState::Asterisk> asterisks;
|
||||
mutable std::vector<SyncthingIgnorePatternState::CharacterRange> characterRange;
|
||||
mutable std::vector<SyncthingIgnorePatternState::AlternativeRange> alternatives;
|
||||
};
|
||||
|
||||
} // namespace Data
|
||||
|
||||
#endif // DATA_SYNCTHINGIGNOREPATTERN_H
|
|
@ -1,6 +1,7 @@
|
|||
#include "../syncthingconfig.h"
|
||||
#include "../syncthingconnection.h"
|
||||
#include "../syncthingconnectionsettings.h"
|
||||
#include "../syncthingignorepattern.h"
|
||||
#include "../syncthingprocess.h"
|
||||
#include "../syncthingservice.h"
|
||||
#include "../utils.h"
|
||||
|
@ -39,6 +40,7 @@ class MiscTests : public TestFixture {
|
|||
#endif
|
||||
CPPUNIT_TEST(testConnectionSettingsAndLoadingSelfSignedCert);
|
||||
CPPUNIT_TEST(testSyncthingDir);
|
||||
CPPUNIT_TEST(testIgnorePatternMatching);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
|
@ -52,6 +54,7 @@ public:
|
|||
#endif
|
||||
void testConnectionSettingsAndLoadingSelfSignedCert();
|
||||
void testSyncthingDir();
|
||||
void testIgnorePatternMatching();
|
||||
|
||||
void setUp() override;
|
||||
void tearDown() override;
|
||||
|
@ -279,3 +282,122 @@ void MiscTests::testSyncthingDir()
|
|||
CPPUNIT_ASSERT_MESSAGE("same status again not considered an update",
|
||||
!dir.assignStatus(QStringLiteral("idle"), updateEvent += 1, updateTime += TimeSpan::fromMinutes(1.5)));
|
||||
}
|
||||
|
||||
void MiscTests::testIgnorePatternMatching()
|
||||
{
|
||||
auto p1 = SyncthingIgnorePattern(QStringLiteral("foo"));
|
||||
CPPUNIT_ASSERT(!p1.comment);
|
||||
CPPUNIT_ASSERT(!p1.caseInsensitive);
|
||||
CPPUNIT_ASSERT(p1.ignore);
|
||||
CPPUNIT_ASSERT(!p1.allowRemovalOnParentDirRemoval);
|
||||
CPPUNIT_ASSERT_EQUAL(QStringLiteral("foo"), p1.glob);
|
||||
CPPUNIT_ASSERT(p1.matches(QStringLiteral("foo")));
|
||||
CPPUNIT_ASSERT(!p1.matches(QStringLiteral("foofoo")));
|
||||
CPPUNIT_ASSERT(p1.matches(QStringLiteral("foo/foo")));
|
||||
|
||||
auto p2 = SyncthingIgnorePattern(QStringLiteral("foo*"));
|
||||
CPPUNIT_ASSERT(p2.matches(QStringLiteral("foo")));
|
||||
CPPUNIT_ASSERT(p2.matches(QStringLiteral("foobar")));
|
||||
CPPUNIT_ASSERT(!p2.matches(QStringLiteral("barfoo")));
|
||||
CPPUNIT_ASSERT(p2.matches(QStringLiteral("bar/foo")));
|
||||
|
||||
auto p3 = SyncthingIgnorePattern(QStringLiteral("fo*ar"));
|
||||
CPPUNIT_ASSERT(!p3.matches(QStringLiteral("foo")));
|
||||
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foar")));
|
||||
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foobar")));
|
||||
CPPUNIT_ASSERT(!p3.matches(QStringLiteral("foobaR")));
|
||||
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foobaRar")));
|
||||
CPPUNIT_ASSERT(p3.matches(QStringLiteral("foo*bar")));
|
||||
|
||||
auto p4 = SyncthingIgnorePattern(QStringLiteral("fo\\*ar"));
|
||||
CPPUNIT_ASSERT(!p4.matches(QStringLiteral("foar")));
|
||||
CPPUNIT_ASSERT(!p4.matches(QStringLiteral("foobar")));
|
||||
CPPUNIT_ASSERT(!p4.matches(QStringLiteral("foo*bar")));
|
||||
CPPUNIT_ASSERT(p4.matches(QStringLiteral("fo*ar")));
|
||||
|
||||
auto p5 = SyncthingIgnorePattern(QStringLiteral("te*ne"));
|
||||
CPPUNIT_ASSERT(p5.matches(QStringLiteral("telephone")));
|
||||
CPPUNIT_ASSERT(p5.matches(QStringLiteral("subdir/telephone")));
|
||||
CPPUNIT_ASSERT(!p5.matches(QStringLiteral("tele/phone")));
|
||||
|
||||
auto p6 = SyncthingIgnorePattern(QStringLiteral("te**ne"));
|
||||
CPPUNIT_ASSERT(p6.matches(QStringLiteral("telephone")));
|
||||
CPPUNIT_ASSERT(p6.matches(QStringLiteral("subdir/telephone")));
|
||||
CPPUNIT_ASSERT(p6.matches(QStringLiteral("tele/phone")));
|
||||
|
||||
auto p7 = SyncthingIgnorePattern(QStringLiteral("te??st"));
|
||||
CPPUNIT_ASSERT(p7.matches(QStringLiteral("tebest")));
|
||||
CPPUNIT_ASSERT(!p7.matches(QStringLiteral("teb/st")));
|
||||
CPPUNIT_ASSERT(!p7.matches(QStringLiteral("test")));
|
||||
|
||||
auto p8 = SyncthingIgnorePattern(QStringLiteral("fo*ar*y"));
|
||||
CPPUNIT_ASSERT(!p8.matches(QStringLiteral("fooy")));
|
||||
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foary")));
|
||||
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foobary")));
|
||||
CPPUNIT_ASSERT(!p8.matches(QStringLiteral("foobaRy")));
|
||||
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foobaRary")));
|
||||
CPPUNIT_ASSERT(!p8.matches(QStringLiteral("foobaRaRy")));
|
||||
CPPUNIT_ASSERT(p8.matches(QStringLiteral("foobaRaRarxy")));
|
||||
CPPUNIT_ASSERT(p8.matches(QStringLiteral("bar/foobaRaRarxy")));
|
||||
|
||||
auto p9 = SyncthingIgnorePattern(QStringLiteral("/foo*"));
|
||||
CPPUNIT_ASSERT(p9.matches(QStringLiteral("foo")));
|
||||
CPPUNIT_ASSERT(p9.matches(QStringLiteral("foobar")));
|
||||
CPPUNIT_ASSERT(!p9.matches(QStringLiteral("barfoo")));
|
||||
CPPUNIT_ASSERT(!p9.matches(QStringLiteral("bar/foo")));
|
||||
|
||||
auto p10 = SyncthingIgnorePattern(QStringLiteral("/fo?"));
|
||||
CPPUNIT_ASSERT(p10.matches(QStringLiteral("foO")));
|
||||
CPPUNIT_ASSERT(!p10.matches(QStringLiteral("foO/")));
|
||||
CPPUNIT_ASSERT(!p10.matches(QStringLiteral("fo/")));
|
||||
CPPUNIT_ASSERT(!p10.matches(QStringLiteral("bar/foO")));
|
||||
|
||||
auto p11 = SyncthingIgnorePattern(QStringLiteral("/fo[o0.]/bar"));
|
||||
CPPUNIT_ASSERT(p11.matches(QStringLiteral("foo/bar")));
|
||||
CPPUNIT_ASSERT(p11.matches(QStringLiteral("fo0/bar")));
|
||||
CPPUNIT_ASSERT(p11.matches(QStringLiteral("fo./bar")));
|
||||
CPPUNIT_ASSERT(!p11.matches(QStringLiteral("foO/bar")));
|
||||
CPPUNIT_ASSERT(!p11.matches(QStringLiteral("fo?/bar")));
|
||||
|
||||
p11.caseInsensitive = true;
|
||||
CPPUNIT_ASSERT(p11.matches(QStringLiteral("foO/bar")));
|
||||
|
||||
auto p12 = SyncthingIgnorePattern(QStringLiteral("/f[oA-C0-3.]o/bar"));
|
||||
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f0o/bar")));
|
||||
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f1o/bar")));
|
||||
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f2o/bar")));
|
||||
CPPUNIT_ASSERT(p12.matches(QStringLiteral("f3o/bar")));
|
||||
CPPUNIT_ASSERT(!p12.matches(QStringLiteral("f4o/bar")));
|
||||
CPPUNIT_ASSERT(p12.matches(QStringLiteral("foo/bar")));
|
||||
CPPUNIT_ASSERT(!p12.matches(QStringLiteral("foO/bar")));
|
||||
CPPUNIT_ASSERT(p12.matches(QStringLiteral("fAo/bar")));
|
||||
CPPUNIT_ASSERT(p12.matches(QStringLiteral("fBo/bar")));
|
||||
CPPUNIT_ASSERT(p12.matches(QStringLiteral("fCo/bar")));
|
||||
CPPUNIT_ASSERT(!p12.matches(QStringLiteral("fDo/bar")));
|
||||
|
||||
auto p13 = SyncthingIgnorePattern(QStringLiteral("/f{o,0o0,l}o/{bar,biz}"));
|
||||
CPPUNIT_ASSERT(p13.matches(QStringLiteral("foo/bar")));
|
||||
CPPUNIT_ASSERT(p13.matches(QStringLiteral("foo/biz")));
|
||||
CPPUNIT_ASSERT(!p13.matches(QStringLiteral("f/biz")));
|
||||
CPPUNIT_ASSERT(!p13.matches(QStringLiteral("f0o0/bar")));
|
||||
CPPUNIT_ASSERT(p13.matches(QStringLiteral("f0o0o/bar")));
|
||||
CPPUNIT_ASSERT(!p13.matches(QStringLiteral("f{o,0o0,}o/{bar,biz}")));
|
||||
|
||||
auto p14 = SyncthingIgnorePattern(QStringLiteral("/rust/**/target"));
|
||||
CPPUNIT_ASSERT(p14.matches(QStringLiteral("rust/formatter/target")));
|
||||
CPPUNIT_ASSERT(!p14.matches(QStringLiteral("rust/formatter/target/CACHEDIR.TAG")));
|
||||
|
||||
auto p14a = SyncthingIgnorePattern(QStringLiteral("/rust/**/target/"));
|
||||
CPPUNIT_ASSERT(!p14a.matches(QStringLiteral("rust/formatter/target")));
|
||||
CPPUNIT_ASSERT(!p14a.matches(QStringLiteral("rust/formatter/target/CACHEDIR.TAG")));
|
||||
|
||||
auto p15 = SyncthingIgnorePattern(QStringLiteral("/fo[\\]o]/bar"));
|
||||
CPPUNIT_ASSERT(p15.matches(QStringLiteral("foo/bar")));
|
||||
CPPUNIT_ASSERT(p15.matches(QStringLiteral("fo]/bar")));
|
||||
CPPUNIT_ASSERT(!p15.matches(QStringLiteral("fo\\/bar")));
|
||||
|
||||
auto p16 = SyncthingIgnorePattern(QStringLiteral("/fo{o\\},o}/bar"));
|
||||
CPPUNIT_ASSERT(p16.matches(QStringLiteral("foo/bar")));
|
||||
CPPUNIT_ASSERT(p16.matches(QStringLiteral("foo}/bar")));
|
||||
CPPUNIT_ASSERT(!p16.matches(QStringLiteral("/foo\\,o}/bar")));
|
||||
}
|
||||
|
|
|
@ -74,13 +74,16 @@ SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const Sy
|
|||
m_root->type = SyncthingItemType::Directory;
|
||||
m_root->path = QStringLiteral(""); // assign an empty QString that is not null
|
||||
m_fetchQueue.append(m_root->path);
|
||||
queryIgnores();
|
||||
processFetchQueue();
|
||||
}
|
||||
|
||||
SyncthingFileModel::~SyncthingFileModel()
|
||||
{
|
||||
QObject::disconnect(m_pendingRequest.connection);
|
||||
QObject::disconnect(m_ignorePatternsRequest.connection);
|
||||
delete m_pendingRequest.reply;
|
||||
delete m_ignorePatternsRequest.reply;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> SyncthingFileModel::roleNames() const
|
||||
|
@ -99,7 +102,7 @@ QHash<int, QByteArray> SyncthingFileModel::roleNames() const
|
|||
|
||||
QModelIndex SyncthingFileModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (row < 0 || column < 0 || column > 2 || parent.column() > 0) {
|
||||
if (row < 0 || column < 0 || column > 3 || parent.column() > 0) {
|
||||
return QModelIndex();
|
||||
}
|
||||
if (!parent.isValid()) {
|
||||
|
@ -177,6 +180,8 @@ QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation
|
|||
return tr("Size");
|
||||
case 2:
|
||||
return tr("Last modified");
|
||||
case 3:
|
||||
return tr("Ignore pattern");
|
||||
}
|
||||
break;
|
||||
default:;
|
||||
|
@ -236,6 +241,13 @@ QVariant SyncthingFileModel::data(const QModelIndex &index, int role) const
|
|||
default:
|
||||
return QString();
|
||||
}
|
||||
case 3:
|
||||
if (item->ignorePattern == SyncthingItem::ignorePatternNotInitialized) {
|
||||
matchItemAgainstIgnorePatterns(*item);
|
||||
}
|
||||
if (item->ignorePattern < m_presentIgnorePatterns.size()) {
|
||||
return m_presentIgnorePatterns[item->ignorePattern].pattern;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Qt::CheckStateRole:
|
||||
|
@ -411,7 +423,7 @@ int SyncthingFileModel::rowCount(const QModelIndex &parent) const
|
|||
int SyncthingFileModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
bool SyncthingFileModel::canFetchMore(const QModelIndex &parent) const
|
||||
|
@ -606,6 +618,44 @@ void SyncthingFileModel::processFetchQueue(const QString &lastItemPath)
|
|||
}
|
||||
}
|
||||
|
||||
void SyncthingFileModel::queryIgnores()
|
||||
{
|
||||
m_connection.ignores(m_dirId, [this](SyncthingIgnores &&ignores, QString &&errorMessage) {
|
||||
m_ignorePatternsRequest.reply = nullptr;
|
||||
m_hasIgnorePatterns = errorMessage.isEmpty();
|
||||
m_presentIgnorePatterns.clear();
|
||||
if (!m_hasIgnorePatterns) {
|
||||
return;
|
||||
}
|
||||
m_presentIgnorePatterns.reserve(static_cast<std::size_t>(ignores.ignore.size()));
|
||||
for (auto &ignorePattern : ignores.ignore) {
|
||||
m_presentIgnorePatterns.emplace_back(std::move(ignorePattern));
|
||||
}
|
||||
invalidateAllIndicies(QVector<int>{ Qt::DisplayRole }, 3, QModelIndex());
|
||||
});
|
||||
}
|
||||
|
||||
void SyncthingFileModel::matchItemAgainstIgnorePatterns(SyncthingItem &item) const
|
||||
{
|
||||
if (!m_hasIgnorePatterns) {
|
||||
item.ignorePattern = SyncthingItem::ignorePatternNotInitialized;
|
||||
return;
|
||||
}
|
||||
item.ignorePattern = SyncthingItem::ignorePatternNoMatch;
|
||||
if (!item.isFilesystemItem()) {
|
||||
return;
|
||||
}
|
||||
auto index = std::size_t();
|
||||
for (const auto &ignorePattern : m_presentIgnorePatterns) {
|
||||
if (ignorePattern.matches(item.path)) {
|
||||
item.ignorePattern = index;
|
||||
break;
|
||||
} else {
|
||||
++index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SyncthingFileModel::handleLocalLookupFinished()
|
||||
{
|
||||
// get refreshed index/item
|
||||
|
|
|
@ -4,12 +4,14 @@
|
|||
#include "./syncthingmodel.h"
|
||||
|
||||
#include <syncthingconnector/syncthingconnection.h>
|
||||
#include <syncthingconnector/syncthingignorepattern.h>
|
||||
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
QT_FORWARD_DECLARE_CLASS(QAction)
|
||||
|
||||
|
@ -67,6 +69,8 @@ private Q_SLOTS:
|
|||
private:
|
||||
void setCheckState(const QModelIndex &index, Qt::CheckState checkState);
|
||||
void processFetchQueue(const QString &lastItemPath = QString());
|
||||
void queryIgnores();
|
||||
void matchItemAgainstIgnorePatterns(SyncthingItem &item) const;
|
||||
|
||||
private:
|
||||
using SyncthingItems = std::vector<std::unique_ptr<SyncthingItem>>;
|
||||
|
@ -81,11 +85,14 @@ private:
|
|||
SyncthingConnection &m_connection;
|
||||
QString m_dirId;
|
||||
QString m_localPath;
|
||||
std::vector<SyncthingIgnorePattern> m_presentIgnorePatterns;
|
||||
QStringList m_fetchQueue;
|
||||
SyncthingConnection::QueryResult m_ignorePatternsRequest;
|
||||
QueryResult m_pendingRequest;
|
||||
QFutureWatcher<LocalLookupRes> m_localItemLookup;
|
||||
std::unique_ptr<SyncthingItem> m_root;
|
||||
bool m_selectionMode;
|
||||
bool m_hasIgnorePatterns;
|
||||
};
|
||||
|
||||
inline bool SyncthingFileModel::isSelectionModeEnabled() const
|
||||
|
|
|
@ -57,6 +57,23 @@ void SyncthingModel::invalidateAllIndicies(const QVector<int> &affectedRoles, co
|
|||
}
|
||||
}
|
||||
|
||||
void SyncthingModel::invalidateAllIndicies(const QVector<int> &affectedRoles, int column, const QModelIndex &parentIndex)
|
||||
{
|
||||
const auto rows = rowCount(parentIndex);
|
||||
const auto columns = columnCount(parentIndex);
|
||||
if (rows <= 0 || column >= columns) {
|
||||
return;
|
||||
}
|
||||
const auto topLeftIndex = index(0, column, parentIndex);
|
||||
const auto bottomRightIndex = index(rows - 1, column, parentIndex);
|
||||
emit dataChanged(topLeftIndex, bottomRightIndex, affectedRoles);
|
||||
for (auto row = 0; row != rows; ++row) {
|
||||
if (const auto idx = index(row, 0, parentIndex); idx.isValid()) {
|
||||
invalidateAllIndicies(affectedRoles, column, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SyncthingModel::setBrightColors(bool brightColors)
|
||||
{
|
||||
if (m_brightColors == brightColors) {
|
||||
|
|
|
@ -35,6 +35,7 @@ protected:
|
|||
void invalidateTopLevelIndicies(const QVector<int> &affectedRoles);
|
||||
void invalidateNestedIndicies(const QVector<int> &affectedRoles);
|
||||
void invalidateAllIndicies(const QVector<int> &affectedRoles, const QModelIndex &parentIndex = QModelIndex());
|
||||
void invalidateAllIndicies(const QVector<int> &affectedRoles, int column, const QModelIndex &parentIndex = QModelIndex());
|
||||
|
||||
private Q_SLOTS:
|
||||
virtual void handleConfigInvalidated();
|
||||
|
|
Loading…
Reference in New Issue