Git история: откат файлов в любой момент (фаза 9) — UI для простого пользователя
- GitManager: fileHistory (список версий с hash/time/message/author), fileContentAt, fileDiffHtml/Text (git_patch_from_buffers, цветной HTML), restoreFile (запись + автокоммит), lastModifiedTime/String, autoCommit, ensureGitignore (.flora/.stfolder)
- HistoryDialog.qml: список версий (время «только что/5 мин назад» + дата dd.MM.yyyy, сообщение, автор, shortHash), diff с легендой (зелёный +/красный -/синий заголовок), превью содержимого, кнопка «Восстановить эту версию» с подтверждением (объясняет что создастся новый коммит и можно вернуться вперёд), «Обновить/Закрыть»
- MainWindow: меню Правка → История файла (Ctrl+H) и Вернуться к предыдущей версии, FileTree ПКМ → История, передача vaultPath/filePath
- EditorPanel: индикатор «🕘» + lastModifiedStr (gitManager.lastModifiedTimeString), debounce autoCommit (2с) при documentSaved, shortcut Ctrl+H
- FileTreePanel: сигнал historyRequested
- CMake: добавлен HistoryDialog.qml в qml модуль
- Тесты: git_history_test — создание v1/v2, история 1→2, diff, восстановление назад к v1 (история 3), вперёд к v2 (4), v3, diff v1→v3, bad hash, other.md не влияет на историю note.md, autoCommit, lastModified, .gitignore; 21/21 зелёных
- Проверено вперёд/назад: v1→v2→восстановление v1→восстановление v2→v3→autoCommit, каждый шаг с проверкой содержимого и истории
This commit is contained in:
+10
-1
@@ -151,4 +151,13 @@ target_link_libraries(flora_integration_test PRIVATE Qt6::Core Qt6::Gui cmark::c
|
||||
if(TARGET KF6SyntaxHighlighting)
|
||||
target_link_libraries(flora_integration_test PRIVATE KF6SyntaxHighlighting)
|
||||
endif()
|
||||
add_test(NAME flora_integration_test COMMAND flora_integration_test)
|
||||
add_test(NAME flora_integration_test COMMAND flora_integration_test)
|
||||
|
||||
# Тест истории Git (вперёд/назад, diff, восстановление, время)
|
||||
add_executable(flora_git_history_test git_history_test.cpp
|
||||
../src/git/GitManager.cpp
|
||||
../src/git/GitManager.h
|
||||
)
|
||||
target_include_directories(flora_git_history_test PRIVATE ../src)
|
||||
target_link_libraries(flora_git_history_test PRIVATE Qt6::Core Flora_libgit2_static)
|
||||
add_test(NAME flora_git_history_test COMMAND flora_git_history_test)
|
||||
@@ -0,0 +1,179 @@
|
||||
// Тест 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);
|
||||
// diff с текущим (одинаково) должен быть "Нет изменений" или пусто?
|
||||
QString diffSame = git.fileDiffText(vault, file, hash1);
|
||||
if(diffSame != "Нет изменений" && !diffSame.isEmpty()) {
|
||||
// если файл не менялся, diff должен быть "Нет изменений"
|
||||
// Но мы ещё не меняли, так что должно быть нет изменений
|
||||
if(!diffSame.contains("Нет изменений"))
|
||||
return fail("diff same should be нет изменений", diffSame);
|
||||
}
|
||||
|
||||
// Изменяем файл → 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);
|
||||
// diff hash1 vs current (v2): должен показать + строка B и -Версия1 +Версия2
|
||||
QString diff = git.fileDiffText(vault, file, hash1);
|
||||
if(!diff.contains("+") || !diff.contains("-")) return fail("diff should have + -", diff);
|
||||
if(!diff.contains("Версия 1") || !diff.contains("Версия 2")) return fail("diff contains versions", diff);
|
||||
QString diffHtml = git.fileDiffHtml(vault, file, hash1);
|
||||
if(!diffHtml.contains("background:#e6ffec") || !diffHtml.contains("background:#ffebe9")) return fail("diffHtml colors", diffHtml.left(500));
|
||||
|
||||
// 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");
|
||||
|
||||
// Проверка что diff между v1 и текущим (v3) показывает изменения
|
||||
QString diffV1Now = git.fileDiffText(vault, file, hash1);
|
||||
if(!diffV1Now.contains("Версия 1") || !diffV1Now.contains("Версия 3")) return fail("diff v1->v3", diffV1Now);
|
||||
|
||||
// Проверка несуществующего коммита
|
||||
QString badContent = git.fileContentAt(vault, file, "0000000000000000000000000000000000000000");
|
||||
if(!badContent.isEmpty()) return fail("bad hash should return empty");
|
||||
|
||||
QString badDiff = git.fileDiffText(vault, file, "0000000000000000000000000000000000000000");
|
||||
// должен либо вернуть ошибку diff, либо содержать old 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");
|
||||
|
||||
std::printf("GIT HISTORY TEST OK (forward/backward, diff, restore, time)\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user