diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index b92cfa08e43ca..55a678f0310d9 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -21,7 +21,6 @@ #include "common/utility.h" #include "common/filesystembase.h" -#include "../version.h" #include #include @@ -220,7 +219,6 @@ using namespace OCC; ExcludedFiles::ExcludedFiles(const QString &localPath) : _localPath(localPath) - , _clientVersion(MIRALL_VERSION_MAJOR, MIRALL_VERSION_MINOR, MIRALL_VERSION_PATCH) { Q_ASSERT(_localPath.endsWith(QStringLiteral("/"))); // Windows used to use PathMatchSpec which allows *foo to match abc/deffoo. @@ -283,22 +281,14 @@ void ExcludedFiles::setWildcardsMatchSlash(bool onoff) prepare(); } -void ExcludedFiles::setClientVersion(ExcludedFiles::Version version) -{ - _clientVersion = version; -} - void ExcludedFiles::loadExcludeFilePatterns(const QString &basePath, QFile &file) { QStringList patterns; while (!file.atEnd()) { QByteArray line = file.readLine().trimmed(); - if (line.startsWith("#!version")) { - if (!versionDirectiveKeepNextLine(line)) - file.readLine(); - } - if (line.isEmpty() || line.startsWith('#')) + if (line.isEmpty() || line.startsWith('#')) { continue; + } const auto patternStr = QString::fromUtf8(line); if (QStringView{patternStr}.trimmed() == QLatin1StringView("*")) { continue; @@ -309,7 +299,7 @@ void ExcludedFiles::loadExcludeFilePatterns(const QString &basePath, QFile &file _allExcludes[basePath].append(patterns); // nothing to prepare if the user decided to not exclude anything - if (!_allExcludes.value(basePath).isEmpty()){ + if (!_allExcludes.value(basePath).isEmpty()) { prepare(basePath); } } @@ -360,32 +350,6 @@ bool ExcludedFiles::reloadExcludeFiles() return success; } -bool ExcludedFiles::versionDirectiveKeepNextLine(const QByteArray &directive) const -{ - if (!directive.startsWith("#!version")) - return true; - QByteArrayList args = directive.split(' '); - if (args.size() != 3) - return true; - QByteArray op = args[1]; - QByteArrayList argVersions = args[2].split('.'); - if (argVersions.size() != 3) - return true; - - auto argVersion = std::make_tuple(argVersions[0].toInt(), argVersions[1].toInt(), argVersions[2].toInt()); - if (op == "<=") - return _clientVersion <= argVersion; - if (op == "<") - return _clientVersion < argVersion; - if (op == ">") - return _clientVersion > argVersion; - if (op == ">=") - return _clientVersion >= argVersion; - if (op == "==") - return _clientVersion == argVersion; - return true; -} - bool ExcludedFiles::isExcluded( const QString &filePath, const QString &basePath, @@ -470,8 +434,9 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const QString &path, Ite continue; } - if (!m.hasMatch()) - return CSYNC_NOT_EXCLUDED; + if (!m.hasMatch()) { + continue; + } if (m.capturedStart(QStringLiteral("exclude")) != -1) { return CSYNC_FILE_EXCLUDE_LIST; } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { @@ -494,12 +459,13 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const QString &path, Ite continue; } - if (m.hasMatch()) { - if (m.capturedStart(QStringLiteral("exclude")) != -1) { - return CSYNC_FILE_EXCLUDE_LIST; - } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { - return CSYNC_FILE_EXCLUDE_AND_REMOVE; - } + if (!m.hasMatch()) { + continue; + } + if (m.capturedStart(QStringLiteral("exclude")) != -1) { + return CSYNC_FILE_EXCLUDE_LIST; + } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { + return CSYNC_FILE_EXCLUDE_AND_REMOVE; } } return CSYNC_NOT_EXCLUDED; @@ -533,12 +499,13 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::fullPatternMatch(const QString &p, ItemType fi continue; } - if (m.hasMatch()) { - if (m.capturedStart(QStringLiteral("exclude")) != -1) { - return CSYNC_FILE_EXCLUDE_LIST; - } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { - return CSYNC_FILE_EXCLUDE_AND_REMOVE; - } + if (!m.hasMatch()) { + continue; + } + if (m.capturedStart(QStringLiteral("exclude")) != -1) { + return CSYNC_FILE_EXCLUDE_LIST; + } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { + return CSYNC_FILE_EXCLUDE_AND_REMOVE; } } diff --git a/src/csync/csync_exclude.h b/src/csync/csync_exclude.h index 5e4c9358d28a4..9011be72b876d 100644 --- a/src/csync/csync_exclude.h +++ b/src/csync/csync_exclude.h @@ -59,8 +59,6 @@ class OCSYNC_EXPORT ExcludedFiles : public QObject { Q_OBJECT public: - using Version = std::tuple; - explicit ExcludedFiles(const QString &localPath = QStringLiteral("/")); ~ExcludedFiles() override; @@ -110,11 +108,6 @@ class OCSYNC_EXPORT ExcludedFiles : public QObject */ void setWildcardsMatchSlash(bool onoff); - /** - * Sets the client version, only used for testing. - */ - void setClientVersion(Version version); - /** * @brief Check if the given path should be excluded in a traversal situation. * @@ -148,23 +141,6 @@ public slots: void loadExcludeFilePatterns(const QString &basePath, QFile &file); private: - /** - * Returns true if the version directive indicates the next line - * should be skipped. - * - * A version directive has the form "#!version " - * where can be <, <=, ==, >, >= and can be any version - * like 2.5.0. - * - * Example: - * - * #!version < 2.5.0 - * myexclude - * - * Would enable the "myexclude" pattern only for versions before 2.5.0. - */ - [[nodiscard]] bool versionDirectiveKeepNextLine(const QByteArray &directive) const; - /** * @brief Match the exclude pattern against the full path. * @@ -257,12 +233,6 @@ public slots: */ bool _wildcardsMatchSlash = false; - /** - * The client version. Used to evaluate version-dependent excludes, - * see versionDirectiveKeepNextLine(). - */ - Version _clientVersion; - friend class TestExcludedFiles; }; diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 0c0c62dbc9f90..92940994cca91 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -32,7 +32,7 @@ #include "filesystem.h" #include "encryptfolderjob.h" #include "syncresult.h" -#include "ignorelisttablewidget.h" +#include "ignorelisteditor.h" #include "wizard/owncloudwizard.h" #include "networksettings.h" #include "ui_mnemonicdialog.h" @@ -551,26 +551,10 @@ void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) Q_ASSERT(QFileInfo(absFolderPath).isAbsolute()); const QString ignoreFile{absFolderPath + ".sync-exclude.lst"}; - const auto layout = new QVBoxLayout(); - const auto ignoreListWidget = new IgnoreListTableWidget(this); - ignoreListWidget->readIgnoreFile(ignoreFile); - layout->addWidget(ignoreListWidget); - const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - layout->addWidget(buttonBox); - - const auto dialog = new QDialog(); - dialog->setLayout(layout); - - connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton * button) { - if (buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { - ignoreListWidget->slotWriteIgnoreFile(ignoreFile); - } - dialog->close(); - }); - connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::close); - - dialog->open(); + auto ignoreListEditor = new IgnoreListEditor(ignoreFile, this); + ignoreListEditor->setAttribute(Qt::WA_DeleteOnClose); + ignoreListEditor->open(); } void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index, const QPoint& pos) diff --git a/src/gui/ignorelisteditor.cpp b/src/gui/ignorelisteditor.cpp index f17252168419b..0834da9ca4f81 100644 --- a/src/gui/ignorelisteditor.cpp +++ b/src/gui/ignorelisteditor.cpp @@ -9,49 +9,37 @@ #include "folderman.h" #include "generalsettings.h" #include "ignorelisteditor.h" +#include "ignorelisttablewidget.h" #include "ui_ignorelisteditor.h" -#include #include +#include +#include #include #include #include -#include +#include namespace OCC { IgnoreListEditor::IgnoreListEditor(QWidget *parent) - : QDialog(parent) - , ui(new Ui::IgnoreListEditor) + : QDialog{parent} + , ui{new Ui::IgnoreListEditor} + , _ignoreListType{IgnoreListType::Global} { - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - ui->setupUi(this); - ConfigFile cfgFile; - //FIXME This is not true. The entries are hardcoded below in setupTableReadOnlyItems - readOnlyTooltip = tr("This entry is provided by the system at \"%1\" " - "and cannot be modified in this view.") - .arg(QDir::toNativeSeparators(cfgFile.excludeFile(ConfigFile::SystemScope))); - - setupTableReadOnlyItems(); - const auto userConfig = cfgFile.excludeFile(ConfigFile::Scope::UserScope); - ui->ignoreTableWidget->readIgnoreFile(userConfig); - - connect(this, &QDialog::accepted, [=, this]() { - ui->ignoreTableWidget->slotWriteIgnoreFile(userConfig); - /* handle the hidden file checkbox */ - - /* the ignoreHiddenFiles flag is a folder specific setting, but for now, it is - * handled globally. Save it to every folder that is defined. - * TODO this can now be fixed, simply attach this IgnoreListEditor to top-level account - * settings - */ - FolderMan::instance()->setIgnoreHiddenFiles(ignoreHiddenFiles()); - }); - connect(ui->buttonBox, &QDialogButtonBox::clicked, - this, &IgnoreListEditor::slotRestoreDefaults); + _ignoreFile = cfgFile.excludeFile(ConfigFile::Scope::UserScope); + setupUi(); +} - ui->syncHiddenFilesCheckBox->setChecked(!FolderMan::instance()->ignoreHiddenFiles()); +IgnoreListEditor::IgnoreListEditor(const QString &ignoreFile, QWidget *parent) + : QDialog{parent} + , ui{new Ui::IgnoreListEditor} + , _ignoreFile{ignoreFile} + , _ignoreListType{IgnoreListType::Folder} +{ + setupUi(); + ui->groupboxGlobalIgnoreSettings->hide(); } IgnoreListEditor::~IgnoreListEditor() @@ -59,28 +47,73 @@ IgnoreListEditor::~IgnoreListEditor() delete ui; } -void IgnoreListEditor::setupTableReadOnlyItems() +bool IgnoreListEditor::ignoreHiddenFiles() const { - ui->ignoreTableWidget->addPattern(".csync_journal.db*", /*deletable=*/false, /*readonly=*/true); - ui->ignoreTableWidget->addPattern("._sync_*.db*", /*deletable=*/false, /*readonly=*/true); - ui->ignoreTableWidget->addPattern(".sync_*.db*", /*deletable=*/false, /*readonly=*/true); + return !ui->syncHiddenFilesCheckBox->isChecked(); } -bool IgnoreListEditor::ignoreHiddenFiles() +void IgnoreListEditor::slotSaveIgnoreList() { - return !ui->syncHiddenFilesCheckBox->isChecked(); + // TODO: this will tell the file provider extension a different set of files to globally ignore + // when called from the local editor -- not good! + ui->ignoreTableWidget->slotWriteIgnoreFile(_ignoreFile); + + if (_ignoreListType != Global) { + return; + } + + /* handle the hidden file checkbox for the global ignore list editor */ + + /* the ignoreHiddenFiles flag is a folder specific setting, but for now, it is + * handled globally. Save it to every folder that is defined. + * TODO this can now be fixed, simply attach this IgnoreListEditor to top-level account + * settings + */ + FolderMan::instance()->setIgnoreHiddenFiles(ignoreHiddenFiles()); } void IgnoreListEditor::slotRestoreDefaults(QAbstractButton *button) { - if(ui->buttonBox->buttonRole(button) != QDialogButtonBox::ResetRole) + if(ui->buttonBox->buttonRole(button) != QDialogButtonBox::ResetRole) { return; + } ui->ignoreTableWidget->slotRemoveAllItems(); - ConfigFile cfgFile; setupTableReadOnlyItems(); - ui->ignoreTableWidget->readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), false); + + if (_ignoreListType == Global) { + ConfigFile cfgFile; + ui->ignoreTableWidget->readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), false); + return; + } + + ui->ignoreTableWidget->readIgnoreFile(_ignoreFile); +} + +void IgnoreListEditor::setupUi() +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + ui->setupUi(this); + + setupTableReadOnlyItems(); + ui->ignoreTableWidget->readIgnoreFile(_ignoreFile); + + connect(this, &QDialog::accepted, this, &IgnoreListEditor::slotSaveIgnoreList); + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &IgnoreListEditor::slotRestoreDefaults); + + ui->syncHiddenFilesCheckBox->setChecked(!FolderMan::instance()->ignoreHiddenFiles()); +} + +void IgnoreListEditor::setupTableReadOnlyItems() +{ + if (_ignoreListType != Global) { + return; + } + + ui->ignoreTableWidget->addPattern(".csync_journal.db*", /*deletable=*/false, /*readonly=*/true); + ui->ignoreTableWidget->addPattern("._sync_*.db*", /*deletable=*/false, /*readonly=*/true); + ui->ignoreTableWidget->addPattern(".sync_*.db*", /*deletable=*/false, /*readonly=*/true); } } // namespace OCC diff --git a/src/gui/ignorelisteditor.h b/src/gui/ignorelisteditor.h index dbda14e28a3aa..2301eafbad050 100644 --- a/src/gui/ignorelisteditor.h +++ b/src/gui/ignorelisteditor.h @@ -27,18 +27,30 @@ class IgnoreListEditor : public QDialog Q_OBJECT public: + enum IgnoreListType { + Global, + Folder, + }; + IgnoreListEditor(QWidget *parent = nullptr); + IgnoreListEditor(const QString &ignoreFile, QWidget *parent = nullptr); + ~IgnoreListEditor() override; - bool ignoreHiddenFiles(); + [[nodiscard]] bool ignoreHiddenFiles() const; private slots: + void slotSaveIgnoreList(); void slotRestoreDefaults(QAbstractButton *button); private: - void setupTableReadOnlyItems(); - QString readOnlyTooltip; Ui::IgnoreListEditor *ui; + QString _ignoreFile; + + IgnoreListType _ignoreListType; + + void setupUi(); + void setupTableReadOnlyItems(); }; } // namespace OCC diff --git a/src/gui/ignorelisteditor.ui b/src/gui/ignorelisteditor.ui index 3ebdf524938c0..518332201b225 100644 --- a/src/gui/ignorelisteditor.ui +++ b/src/gui/ignorelisteditor.ui @@ -15,7 +15,7 @@ - + Global Ignore Settings @@ -31,7 +31,7 @@ - + Files Ignored by Patterns @@ -45,7 +45,7 @@ - QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::RestoreDefaults diff --git a/src/gui/ignorelisttablewidget.cpp b/src/gui/ignorelisttablewidget.cpp index 047d8251f6e12..8fa34ee6e7085 100644 --- a/src/gui/ignorelisttablewidget.cpp +++ b/src/gui/ignorelisttablewidget.cpp @@ -24,6 +24,8 @@ static constexpr int patternCol = 0; static constexpr int deletableCol = 1; static constexpr int readOnlyRows = 3; +Q_LOGGING_CATEGORY(lcIgnoreListTableWidget, "nextcloud.gui.ignorelisttablewidget", QtInfoMsg) + IgnoreListTableWidget::IgnoreListTableWidget(QWidget *parent) : QWidget(parent) , ui(new Ui::IgnoreListTableWidget) @@ -84,30 +86,38 @@ void IgnoreListTableWidget::slotWriteIgnoreFile(const QString &file) { QFile ignores(file); - if (ignores.open(QIODevice::WriteOnly)) { - // rewrites the whole file since now the user can also remove system patterns - QFile::resize(file, 0); - - for (auto row = 0; row < ui->tableWidget->rowCount(); ++row) { - const auto patternItem = ui->tableWidget->item(row, patternCol); - const auto deletableItem = ui->tableWidget->item(row, deletableCol); - - if (patternItem->flags() & Qt::ItemIsEnabled) { - QByteArray prepend; - if (deletableItem->checkState() == Qt::Checked) { - prepend = "]"; - } else if (patternItem->text().startsWith('#')) { - prepend = "\\"; - } - ignores.write(prepend + patternItem->text().toUtf8() + '\n'); - } - } - } else { + if (!ignores.open(QIODevice::WriteOnly)) { + qCWarning(lcIgnoreListTableWidget).nospace() << "failed to write ignore list" + << " file=" << file + << " errorString=" << ignores.errorString(); QMessageBox::warning(this, tr("Could not open file"), tr("Cannot write changes to \"%1\".").arg(file)); + + ignores.close(); + return; } - ignores.close(); //close the file before reloading stuff. + + // rewrite the whole file since the user can also remove system patterns + ignores.resize(0); + + for (auto row = 0; row < ui->tableWidget->rowCount(); ++row) { + const auto patternItem = ui->tableWidget->item(row, patternCol); + if (!(patternItem->flags() & Qt::ItemIsEnabled)) { + // skip read-only patterns + continue; + } + const auto deletableItem = ui->tableWidget->item(row, deletableCol); + + QByteArray prefix; + if (deletableItem && deletableItem->checkState() == Qt::Checked) { + prefix = "]"; + } else if (patternItem->text().startsWith('#')) { + prefix = "\\"; + } + ignores.write(prefix + patternItem->text().toUtf8() + '\n'); + } + ignores.close(); // close the file before reloading stuff. const auto folderMan = FolderMan::instance(); @@ -134,8 +144,9 @@ void IgnoreListTableWidget::slotAddPattern() {}, &okClicked); - if (!okClicked || pattern.isEmpty()) + if (!okClicked || pattern.isEmpty()) { return; + } addPattern(pattern, false, false); ui->tableWidget->scrollToBottom(); @@ -181,7 +192,6 @@ int IgnoreListTableWidget::addPattern(const QString &pattern, const bool deletab if (readOnly) { patternItem->setFlags(patternItem->flags() ^ Qt::ItemIsEnabled); - patternItem->setToolTip(readOnlyTooltip); deletableItem->setFlags(deletableItem->flags() ^ Qt::ItemIsEnabled); } diff --git a/src/gui/ignorelisttablewidget.h b/src/gui/ignorelisttablewidget.h index e0b1e0314ead1..f446955d69082 100644 --- a/src/gui/ignorelisttablewidget.h +++ b/src/gui/ignorelisttablewidget.h @@ -37,7 +37,6 @@ private slots: private: void setupTableReadOnlyItems(); - QString readOnlyTooltip; Ui::IgnoreListTableWidget *ui; }; } // namespace OCC diff --git a/test/testexcludedfiles.cpp b/test/testexcludedfiles.cpp index 988b4f8f548ab..e69d809b5fd34 100644 --- a/test/testexcludedfiles.cpp +++ b/test/testexcludedfiles.cpp @@ -11,12 +11,16 @@ #include #include +#include "common/utility.h" +#include "csync.h" #include "csync_exclude.h" #include "logger.h" using namespace OCC; -#define EXCLUDE_LIST_FILE SOURCEDIR "/../../sync-exclude.lst" +using namespace Qt::StringLiterals; + +constexpr auto EXCLUDE_LIST_FILE = SOURCEDIR "/../../sync-exclude.lst"; // The tests were converted from the old CMocka framework, that's why there is a global static QScopedPointer excludedFiles; @@ -397,8 +401,8 @@ private slots: QCOMPARE(check_file_traversal("/excludepath/withsubdir"), CSYNC_FILE_EXCLUDE_LIST); QCOMPARE(check_dir_traversal("/excludepath/withsubdir2"), CSYNC_NOT_EXCLUDED); - // because leading dirs aren't checked! - QCOMPARE(check_dir_traversal("/excludepath/withsubdir/foo"), CSYNC_NOT_EXCLUDED); + // Parent directories are considered too + QCOMPARE(check_dir_traversal("/excludepath/withsubdir/foo"), CSYNC_FILE_EXCLUDE_LIST); /* Check ending of pattern */ QCOMPARE(check_file_traversal("/exclude"), CSYNC_FILE_EXCLUDE_LIST); @@ -689,32 +693,6 @@ private slots: QVERIFY(0 == strcmp(line.constData(), "\\")); } - void check_version_directive() - { - ExcludedFiles excludes; - excludes.setClientVersion(ExcludedFiles::Version(2, 5, 0)); - - std::vector> tests = { - { "#!version == 2.5.0", true }, - { "#!version == 2.6.0", false }, - { "#!version < 2.6.0", true }, - { "#!version <= 2.6.0", true }, - { "#!version > 2.6.0", false }, - { "#!version >= 2.6.0", false }, - { "#!version < 2.4.0", false }, - { "#!version <= 2.4.0", false }, - { "#!version > 2.4.0", true }, - { "#!version >= 2.4.0", true }, - { "#!version < 2.5.0", false }, - { "#!version <= 2.5.0", true }, - { "#!version > 2.5.0", false }, - { "#!version >= 2.5.0", true }, - }; - for (auto test : tests) { - QVERIFY(excludes.versionDirectiveKeepNextLine(test.first) == test.second); - } - } - void testAddExcludeFilePath_addSameFilePath_listSizeDoesNotIncrease() { excludedFiles.reset(new ExcludedFiles()); @@ -785,6 +763,71 @@ private slots: QCOMPARE(excludedFiles->reloadExcludeFiles(), true); QCOMPARE(excludedFiles->_allExcludes.size(), 1); } + + void testFolderExcludeListInheritsGlobalExcludes() + { + QTemporaryDir tempDir; + const auto localPath = Utility::trailingSlashPath(tempDir.path()); + excludedFiles.reset(new ExcludedFiles(localPath)); + + // create .sync-exclude.lst files inside `a/b` and `a/b/c` subdirectories + QDir localDir(localPath); + QVERIFY(localDir.mkpath("a/b/c/d")); + const auto writeExcludeList = [&localDir](const QString& path, const QStringList& contents) -> bool { + QFile f(localDir.filePath("%1/.sync-exclude.lst"_L1.arg(path))); + if (!f.open(QIODevice::WriteOnly)) { + return false; + } + f.write(contents.join("\n").toLocal8Bit()); + f.close(); + return true; + }; + QVERIFY(writeExcludeList("a/b", {"B_ignore*"})); + QVERIFY(writeExcludeList("a/b/c", {"C_ignore*"})); + + // add default/global exclude list from the client + excludedFiles->addExcludeFilePath(EXCLUDE_LIST_FILE); + QVERIFY(excludedFiles->reloadExcludeFiles()); + + QCOMPARE(excludedFiles->traversalPatternMatch("~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + + // according to `ExcludedFiles::traversalPatternMatch`, directories are + // guaranteed to be visited before their files, so match those first. + // + // The function has the side effect of reading additional excludes from + // a ".sync-exclude.lst" file in the passed directory + QCOMPARE(excludedFiles->_allExcludes.size(), 1); + QCOMPARE(excludedFiles->traversalPatternMatch("a", ItemTypeDirectory), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->_allExcludes.size(), 1); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b", ItemTypeDirectory), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->_allExcludes.size(), 2); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c", ItemTypeDirectory), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->_allExcludes.size(), 3); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/d", ItemTypeDirectory), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->_allExcludes.size(), 3); + + // validate custom ignores from the directory-specific .sync-exclude.lst + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/B_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/C_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + + // excludes from subfolders are not propagated to their parent(s) + QCOMPARE(excludedFiles->traversalPatternMatch("B_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->traversalPatternMatch("a/B_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->traversalPatternMatch("C_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->traversalPatternMatch("a/C_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/C_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + + // global exclude list should still match in subdirs + QCOMPARE(excludedFiles->traversalPatternMatch("a/~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/d/~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + + // excludes from subfolders inherit the parent excludes + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/B_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/d/B_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/d/C_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + } }; QTEST_APPLESS_MAIN(TestExcludedFiles)