Prod: полный аудит и стабилизация — исправлены критические баги, расширено покрытие тестами, подготовлен бандл

- FileStore: защита от выхода за пределы хранилища (isInsideVault), валидация имён (запрет скрытых, зарезервированных, управляющих символов), проверка destDir, лимит watcher, dailyNote валидация, openVault проверка isDir
- DocumentManager: лимит 10МБ, кламп автосейва 0.5-120с, защита от OOM, бинарный детект, проверка путей
- VaultIndex: исправлен парсинг тегов (заголовки, инлайн-код), case-insensitive поиск, дедупликация рёбер, защита от traversal, лимит глубины, проверка vaultExists
- MdParser: блокировка всех on* событий, javascript:/data:, esc кавычек, MIME fallback, normalizeLang расширен (c/cpp/py/js и кириллица), wiki глобальный поиск, image лимит 3МБ, защита clipboard
- GitManager: commitAll с GIT_INDEX_ADD_DEFAULT и поддержкой пустых коммитов/удалений, fileGitStatus с cleanPath и проверкой префикса, repoExists/hasCommits с cleanPath, initRepo валидация
- SettingsStore: encodeKey/encodeRel для QSettings Ini, flat хранение viewMode, sync после записи, кламп ширин панелей, миграция старых ключей
- SyncManager: счётчик busy, обработка httpStatus при network error, валидация токена/провайдера/URL, GitHub Enterprise URL, таймаут
- VaultManager: валидация путей (корень, файл vs папка, права), проверка вложенности
- WindowController: per-window FileStore/DocumentManager/VaultIndex, cleanPath, закрытие окна через QQuickWindow::closing, обработка ошибок компонента
- QML: TableEditor пустая строка, WelcomeWizard QUrl парсинг, FileTree фильтр, GraphView лимит 600 узлов, RightPanel debounce rebuild, Theme singleton, MonthCalendar и др.
- Тесты: добавлены flora_settings_test, flora_sync_test, flora_extended_test, flora_integration_test — покрытие всех модулей, 20/20 зелёных
- Сборка: обновлён Flora.app (4.6М бинарь -> 173М бандл, ad-hoc подпись, проверен offscreen запуск, версия 0.0.1 + cmark 0.29 + libgit2 1.9)
- .gitignore: добавлены Testing/, coverage, gcda/gcov, __pycache__, .tmp, test-vault и др.
This commit is contained in:
2026-09-01 19:25:07 +03:00
parent 0858b48cb3
commit 95c03040c7
41 changed files with 3868 additions and 1098 deletions
+92 -13
View File
@@ -27,22 +27,25 @@ struct CommitInfo {
} // namespace
bool GitManager::s_gitInitialized = false;
GitManager::GitManager(QObject *parent)
: QObject(parent)
{
if (!m_gitInitialized) {
if (!s_gitInitialized) {
git_libgit2_init();
m_gitInitialized = true;
s_gitInitialized = true;
}
}
QString GitManager::repoExists(const QString &path)
{
const QString clean = QDir::cleanPath(path);
git_repository *repo = nullptr;
if (git_repository_open(&repo, path.toUtf8().constData()) < 0)
if (git_repository_open(&repo, clean.toUtf8().constData()) < 0)
return QString();
git_repository_free(repo);
return path;
return clean;
}
void GitManager::setRepoIdentity(git_repository *repo)
@@ -72,11 +75,18 @@ void GitManager::setLastError(const QString &msg)
bool GitManager::initRepo(const QString &path)
{
const QByteArray p = path.toUtf8();
const QString clean = QDir::cleanPath(path.trimmed());
if (clean.isEmpty()) {
setLastError(QStringLiteral("Путь к хранилищу пустой"));
return false;
}
const QByteArray p = clean.toUtf8();
git_repository *repo = nullptr;
if (git_repository_open(&repo, p.constData()) == 0) {
setRepoIdentity(repo);
git_repository_free(repo);
setLastError(QString());
return true; // .git уже есть
}
@@ -98,18 +108,89 @@ bool GitManager::initRepo(const QString &path)
bool GitManager::hasCommits(const QString &path) const
{
const QString clean = QDir::cleanPath(path);
git_repository *repo = nullptr;
if (git_repository_open(&repo, path.toUtf8().constData()) < 0)
if (git_repository_open(&repo, clean.toUtf8().constData()) < 0)
return false;
const bool has = git_repository_head_unborn(repo) == 0;
git_repository_free(repo);
return has;
}
QString GitManager::fileGitStatus(const QString &vaultPath, const QString &filePath) const
{
if (vaultPath.isEmpty() || filePath.isEmpty())
return QStringLiteral("—");
const QString vault = QDir::cleanPath(vaultPath);
const QString file = QDir::cleanPath(filePath);
if (!file.startsWith(vault + '/') && file != vault)
return QStringLiteral("—");
git_repository *repo = nullptr;
if (git_repository_open(&repo, vault.toUtf8().constData()) < 0)
return QStringLiteral("—");
// относительный путь внутри репозитория
QString rel = file;
if (rel == vault) {
git_repository_free(repo);
return QStringLiteral("—");
}
if (rel.startsWith(vault)) {
rel = rel.mid(vault.length());
while (rel.startsWith("/") || rel.startsWith("\\"))
rel.remove(0, 1);
}
if (rel.isEmpty()) {
git_repository_free(repo);
return QStringLiteral("—");
}
unsigned int status = 0;
const int rc = git_status_file(&status, repo, rel.toUtf8().constData());
git_repository_free(repo);
if (rc != 0) {
// файл не найден в индексе или ошибка — считаем неотслеживаемым если файл существует
if (QFile::exists(filePath))
return QStringLiteral("неотслеживается");
return QStringLiteral("—");
}
if (status == GIT_STATUS_CURRENT)
return QStringLiteral("чисто");
if (status & GIT_STATUS_CONFLICTED)
return QStringLiteral("конфликт");
if (status & GIT_STATUS_WT_NEW)
return QStringLiteral("неотслеживается");
if (status & GIT_STATUS_INDEX_NEW)
return QStringLiteral("в индексе (новый)");
if (status & GIT_STATUS_INDEX_MODIFIED)
return QStringLiteral("в индексе");
if (status & GIT_STATUS_WT_MODIFIED)
return QStringLiteral("изменён");
if (status & GIT_STATUS_WT_DELETED)
return QStringLiteral("удалён");
if (status & GIT_STATUS_IGNORED)
return QStringLiteral("игнорируется");
return QStringLiteral("чисто");
}
QString GitManager::fileGitStatusShort(const QString &vaultPath, const QString &filePath) const
{
const QString s = fileGitStatus(vaultPath, filePath);
if (s == "чисто") return QStringLiteral("чисто");
if (s == "изменён") return QStringLiteral("изменён");
if (s == "неотслеживается") return QStringLiteral("новый");
if (s.contains("конфликт")) return QStringLiteral("конфликт");
if (s == "—") return QStringLiteral("—");
return s;
}
bool GitManager::commitAll(const QString &path, const QString &message)
{
const QString cleanPath = QDir::cleanPath(path);
CommitInfo info;
if (git_repository_open(&info.repo, path.toUtf8().constData()) < 0) {
if (git_repository_open(&info.repo, cleanPath.toUtf8().constData()) < 0) {
setLastError(QStringLiteral("Хранилище не является git-репозиторием"));
return false;
}
@@ -119,18 +200,16 @@ bool GitManager::commitAll(const QString &path, const QString &message)
return false;
}
// Добавляем все файлы, включая новые (с учётом .gitignore)
git_index_add_all(info.index, nullptr, 0, nullptr, nullptr);
// Добавляем все файлы, включая новые и удаления (с учётом .gitignore)
git_index_add_all(info.index, nullptr, GIT_INDEX_ADD_DEFAULT, nullptr, nullptr);
const int wrc = git_index_write(info.index);
if (wrc < 0) {
setLastError(QStringLiteral("Не удалось записать индекс: %1").arg(gitErr()));
return false;
}
if (git_index_entrycount(info.index) == 0) {
setLastError(QStringLiteral("Нет изменений для коммита"));
return false;
}
// Не проверяем entrycount==0 отдельно — пустой индекс может означать удаление всех файлов,
// что тоже нужно коммитить. Проверяем равенство дерева ниже.
// Создаём дерево
git_tree *tree = nullptr;