Files
Flora/tests/git_history_test.cpp
T
grendervill 0846605c2b Git: кэш+пагинация истории, diff в потоке, точечный коммит, честное удаление
- История: кэш по vault|rel|headOid, fileHistory(vault,file,limit), в диалоге Показать ещё по 60 с сохранением выбора
- Diff считается в QtConcurrent-пуле (parseBlocksWithDiffAsync+blocksReady, stale-guard), спиннер busy; MarkdownView.diffApplied
- commitFile точечно вместо всего хранилища (общий commitFromIndex), autoCommit честный; fileExistedAt + willDelete в подтверждении
- DocumentManager.readTextFile общий (GitManager.workdirContent через него); fileGitStatus через relativePath
- Тесты: пагинация/кэш, existed, commitFile не трогает B, async==sync
2026-09-06 21:17:25 +03:00

216 lines
13 KiB
C++

// Тест GitManager истории: вперёд/назад, diff, восстановление, время
#include <QCoreApplication>
#include <QTemporaryDir>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QDateTime>
#include <QThread>
#include <cstdio>
#include "git/GitManager.h"
static int fail(const char *msg, const QString &detail=QString()){
std::fprintf(stderr,"FAIL: %s | %s\n", msg, qPrintable(detail));
return 1;
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QTemporaryDir dir;
if(!dir.isValid()) return fail("tmp dir");
QString vault = dir.path();
GitManager git;
if(!git.initRepo(vault)) return fail("initRepo", git.lastError());
// должен создать .gitignore
if(!QFile::exists(vault+"/.gitignore")) return fail(".gitignore not created");
QFile ig(vault+"/.gitignore");
ig.open(QIODevice::ReadOnly);
QString igContent = QString::fromUtf8(ig.readAll());
if(!igContent.contains(".flora/")) return fail(".gitignore missing .flora", igContent);
// .flora должен игнорироваться
QDir().mkpath(vault+"/.flora");
QFile hidden(vault+"/.flora/test"); hidden.open(QIODevice::WriteOnly); hidden.write("x"); hidden.close();
// статус hidden должен быть игнорируется
QString st = git.fileGitStatus(vault, vault+"/.flora/test");
if(st != "игнорируется" && st != "—") {
// допустимо, но должен быть игнорируется после коммита .gitignore?
// Сначала закоммитим .gitignore
}
// Создаём файл v1
QString file = vault+"/note.md";
QFile f(file); f.open(QIODevice::WriteOnly); f.write("Версия 1\nСтрока A\n"); f.close();
if(!git.commitAll(vault, "v1")) return fail("commit v1", git.lastError());
if(!git.hasCommits(vault)) return fail("hasCommits");
// История после v1: 1 элемент
QVariantList hist = git.fileHistory(vault, file);
if(hist.size()!=1) return fail("hist size 1", QString::number(hist.size()));
QString hash1 = hist.first().toMap().value("hash").toString();
QString short1 = hist.first().toMap().value("shortHash").toString();
if(hash1.isEmpty() || short1.isEmpty()) return fail("hash empty");
if(hist.first().toMap().value("message").toString()!="v1") return fail("message v1");
QDateTime t1 = hist.first().toMap().value("time").toDateTime();
if(!t1.isValid()) return fail("time invalid");
if(hist.first().toMap().value("timeAgo").toString().isEmpty()) return fail("timeAgo empty");
// lastModified
QDateTime lm = git.lastModifiedTime(vault, file);
if(!lm.isValid()) return fail("lastModified invalid");
QString lmStr = git.lastModifiedTimeString(vault, file);
if(lmStr.isEmpty() || lmStr=="—") return fail("lmStr empty", lmStr);
// content at
QString c1 = git.fileContentAt(vault, file, hash1);
if(!c1.contains("Версия 1")) return fail("content v1", c1);
// workdirContent читает текущее с диска
if(git.workdirContent(file) != "Версия 1\nСтрока A\n") return fail("workdirContent", git.workdirContent(file));
// Изменяем файл → v2
QThread::msleep(1100); // чтобы время коммита отличалось
QFile f2(file); f2.open(QIODevice::WriteOnly | QIODevice::Truncate); f2.write("Версия 2\nСтрока A\nСтрока B\n"); f2.close();
if(!git.commitAll(vault, "v2 - добавили строку B")) return fail("commit v2", git.lastError());
QVariantList hist2 = git.fileHistory(vault, file);
if(hist2.size()!=2) return fail("hist size 2", QString::number(hist2.size()));
// Новый сверху
QString hash2 = hist2.first().toMap().value("hash").toString();
if(hash2==hash1) return fail("hash2 same as hash1");
if(hist2.first().toMap().value("message").toString().left(2)!="v2") return fail("hist2 top message", hist2.first().toMap().value("message").toString());
if(hist2.last().toMap().value("hash").toString()!=hash1) return fail("hist2 last not hash1");
// время второго должно быть позже
QDateTime t2 = hist2.first().toMap().value("time").toDateTime();
if(t2 <= t1) return fail("t2 not later", t2.toString() + " vs " + t1.toString());
// content at hash2
QString c2 = git.fileContentAt(vault, file, hash2);
if(!c2.contains("Версия 2") || !c2.contains("Строка B")) return fail("c2 content", c2);
// content at hash1 всё ещё v1
QString c1again = git.fileContentAt(vault, file, hash1);
if(!c1again.contains("Версия 1") || c1again.contains("Строка B")) return fail("c1again", c1again);
// forward/backward: добавляем ещё файл other.md, коммитим, история note.md не должна вырасти
QString other = vault+"/other.md";
QFile of(other); of.open(QIODevice::WriteOnly); of.write("other\n"); of.close();
if(!git.commitAll(vault, "add other")) return fail("commit other");
QVariantList histAfterOther = git.fileHistory(vault, file);
if(histAfterOther.size()!=2) return fail("hist after other should still 2", QString::number(histAfterOther.size()));
// Восстановление назад к v1
QString err = git.restoreFile(vault, file, hash1);
if(!err.isEmpty()) return fail("restore to v1", err);
QFile check(file); check.open(QIODevice::ReadOnly); QString cur = QString::fromUtf8(check.readAll());
if(!cur.contains("Версия 1") || cur.contains("Строка B")) return fail("after restore v1 content", cur);
// История после восстановления должна иметь 3 элемента (v1, v2, восстановление)
QVariantList hist3 = git.fileHistory(vault, file);
if(hist3.size()!=3) return fail("hist after restore size 3", QString::number(hist3.size()));
if(!hist3.first().toMap().value("message").toString().contains("Восстановление")) return fail("restore commit message", hist3.first().toMap().value("message").toString());
// lastModified должно быть сейчас (только что)
QString lmAfter = git.lastModifiedTimeString(vault, file);
if(!lmAfter.contains("только что") && !lmAfter.contains("мин назад") && !lmAfter.contains("ч назад")) {
// допустимо, но должно быть свежим
}
// Восстановление вперёд к v2 (используем hash2)
err = git.restoreFile(vault, file, hash2);
if(!err.isEmpty()) return fail("restore forward to v2", err);
QFile check2(file); check2.open(QIODevice::ReadOnly); QString cur2 = QString::fromUtf8(check2.readAll());
if(!cur2.contains("Версия 2")) return fail("after restore forward v2", cur2);
QVariantList hist4 = git.fileHistory(vault, file);
if(hist4.size()!=4) return fail("hist after forward restore 4", QString::number(hist4.size()));
// Ещё одно изменение вручную вперёд
QThread::msleep(1100);
QFile f3(file); f3.open(QIODevice::WriteOnly | QIODevice::Truncate); f3.write("Версия 3\nНовая строка\n"); f3.close();
if(!git.commitAll(vault, "v3")) return fail("commit v3");
QVariantList hist5 = git.fileHistory(vault, file);
if(hist5.size()!=5) return fail("hist 5", QString::number(hist5.size()));
if(hist5.first().toMap().value("message").toString()!="v3") return fail("top v3");
// Проверка несуществующего коммита
QString badContent = git.fileContentAt(vault, file, "0000000000000000000000000000000000000000");
if(!badContent.isEmpty()) return fail("bad hash should return empty");
QString badRestore = git.restoreFile(vault, file, "0000000000000000000000000000000000000000");
if(badRestore.isEmpty()) return fail("bad restore should fail");
// Проверка lastModified для несуществующего файла
QDateTime lmBad = git.lastModifiedTime(vault, vault+"/nonexistent.md");
if(lmBad.isValid()) {
// если файл не существует, должен быть невалидным или mtime? Сейчас возвращает невалидный
// допустимо
}
// Авто-коммит
QFile f4(file); f4.open(QIODevice::WriteOnly | QIODevice::Append); f4.write("\nauto\n"); f4.close();
bool autoOk = git.autoCommit(vault, file);
if(!autoOk) return fail("autoCommit", git.lastError());
QVariantList histAuto = git.fileHistory(vault, file);
if(histAuto.size()!=6) return fail("hist after auto 6", QString::number(histAuto.size()));
if(!histAuto.first().toMap().value("message").toString().contains("Автосохранение")) return fail("auto message", histAuto.first().toMap().value("message").toString());
// Повторный autoCommit без изменений должен быть true (нет ошибки)
bool autoAgain = git.autoCommit(vault, file);
if(!autoAgain) return fail("auto again should be true even if no changes");
// Пагинация: limit режет сверху, порядок новые сверху, кэш консистентен
{
QVariantList full = git.fileHistory(vault, file);
QVariantList part = git.fileHistory(vault, file, 2);
if(part.size()!=2) return fail("paged size", QString::number(part.size()));
if(part.first().toMap().value("hash") != full.first().toMap().value("hash"))
return fail("paged order");
// повторный запрос с тем же лимитом — из кэша, тот же результат
QVariantList part2 = git.fileHistory(vault, file, 2);
if(part2.size()!=2 || part2.first().toMap().value("hash") != part.first().toMap().value("hash"))
return fail("paged cache");
// limit<=0 — как раньше (всё)
if(git.fileHistory(vault, file, 0).size() != full.size()) return fail("limit 0");
}
// fileExistedAt: отличает «пустой файл» от «файла не было»
// (коммит удаления в историю файла не попадает — файла нет в дереве,
// поэтому false проверяем коммитом, предшествующим созданию файла)
{
if(!git.fileExistedAt(vault, file, hash1)) return fail("existed v1");
const QString late = vault+"/late.md";
QFile lf(late);
if(!lf.open(QIODevice::WriteOnly)) return fail("late create");
lf.write("late\n");
lf.close();
if(!git.commitAll(vault, "late file")) return fail("commit late", git.lastError());
QString lateHash = git.fileHistory(vault, late).first().toMap().value("hash").toString();
if(!git.fileExistedAt(vault, late, lateHash)) return fail("late existed");
if(git.fileExistedAt(vault, late, hash1)) return fail("late should not exist at v1");
if(git.fileExistedAt(vault, late, "0000000000000000000000000000000000000000"))
return fail("bad hash existed");
}
// commitFile: точечный коммит одного файла (второй файл не трогаем)
{
QFile re(file);
if(!re.open(QIODevice::WriteOnly)) return fail("recreate");
re.write("v-final\n");
re.close();
if(!git.commitFile(vault, file, "final A")) return fail("commitFile A", git.lastError());
const int histA = git.fileHistory(vault, file).size();
// правим оба файла
QFile fa(file);
fa.open(QIODevice::WriteOnly | QIODevice::Append); fa.write("more A\n"); fa.close();
QFile fb(vault+"/other.md");
fb.open(QIODevice::WriteOnly | QIODevice::Append); fb.write("more B\n"); fb.close();
const int histBBefore = git.fileHistory(vault, vault+"/other.md").size();
if(!git.commitFile(vault, file, "final B")) return fail("commitFile B", git.lastError());
if(git.fileHistory(vault, file).size() != histA + 1) return fail("commitFile history A");
if(git.fileHistory(vault, vault+"/other.md").size() != histBBefore) return fail("commitFile затронул B");
if(!git.fileHistory(vault, file).first().toMap().value("message").toString().contains("final B"))
return fail("commitFile message");
// commitFile без изменений — false с понятной ошибкой, как commitAll
if(git.commitFile(vault, file, "noop")) return fail("commitFile noop should fail");
}
std::printf("GIT HISTORY TEST OK (forward/backward, diff, restore, time)\n");
return 0;
}