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
This commit is contained in:
2026-09-06 21:17:25 +03:00
parent f6074df0fc
commit 0846605c2b
12 changed files with 474 additions and 138 deletions
+50 -22
View File
@@ -32,6 +32,9 @@ Dialog {
property string currentContent: ""
property string emptyMessage: qsTr("Нет версии для просмотра")
property bool busy: false
// Пагинация: подгружаем порциями, чтобы revwalk не бежал по всей истории сразу
property int historyLimit: 60
property bool hasMore: false
function fileDirOf(p) {
const i = p.lastIndexOf("/")
@@ -66,13 +69,23 @@ Dialog {
return
}
busy = true
// берём историю
const hist = gitManager.fileHistory(vaultPath, filePath)
historyModel = hist
listView.model = hist
if (hist.length>0) {
selectedIndex = 0
selectedHash = hist[0].hash
// берём историю порцией limit+1: лишний элемент — признак «есть ещё»
const keepHash = selectedHash
const hist = gitManager.fileHistory(vaultPath, filePath, historyLimit + 1)
hasMore = hist.length > historyLimit
const shown = hasMore ? hist.slice(0, historyLimit) : hist
historyModel = shown
listView.model = shown
if (shown.length>0) {
// сохраняем выбор при «Показать ещё»/обновлении
let idx = 0
if (keepHash.length > 0) {
for (let i = 0; i < shown.length; ++i) {
if (shown[i].hash === keepHash) { idx = i; break }
}
}
selectedIndex = idx
selectedHash = shown[idx].hash
updateDiff()
} else {
selectedIndex = -1
@@ -93,13 +106,15 @@ Dialog {
if (selectedHash.length===0) {
versionContent = ""
currentContent = ""
busy = false
return
}
// свежее содержимое с диска — diff всегда актуален, даже если файл правили при открытом диалоге
currentContent = gitManager.workdirContent(filePath)
// содержимое версии — сырой markdown, отрисуется через MarkdownView как в «Просмотре»,
// с подсветкой отличий от текущего файла прямо там
// с подсветкой отличий от текущего файла прямо там (считается в рабочем потоке)
versionContent = gitManager.fileContentAt(vaultPath, filePath, selectedHash)
busy = currentContent.length > 0 && versionContent.length > 0
}
onOpened: reload()
@@ -280,6 +295,12 @@ Dialog {
font.family: "Menlo, monospace"
}
}
Button {
visible: root.hasMore
Layout.fillWidth: true
text: qsTr("Показать ещё")
onClicked: { root.historyLimit += 60; root.reload() }
}
}
}
@@ -302,16 +323,17 @@ Dialog {
ScrollBar.vertical: ScrollBar {}
Column {
width: versionScroll.availableWidth
MarkdownView {
id: versionPreview
width: parent.width
markdown: root.versionContent
compareMarkdown: root.currentContent
vaultRoot: root.vaultPath
fileDir: root.fileDirOf(root.filePath)
emptyText: root.emptyMessage
onLinkActivated: (href) => root.handleHistoryLink(href)
}
MarkdownView {
id: versionPreview
width: parent.width
markdown: root.versionContent
compareMarkdown: root.currentContent
vaultRoot: root.vaultPath
fileDir: root.fileDirOf(root.filePath)
emptyText: root.emptyMessage
onLinkActivated: (href) => root.handleHistoryLink(href)
onDiffApplied: root.busy = false
}
}
}
}
@@ -355,7 +377,10 @@ Dialog {
text: qsTr("Восстановить эту версию")
enabled: root.selectedHash.length>0
highlighted: true
onClicked: confirmRestore.open()
onClicked: {
confirmRestore.willDelete = !gitManager.fileExistedAt(root.vaultPath, root.filePath, root.selectedHash)
confirmRestore.open()
}
}
}
}
@@ -363,6 +388,7 @@ Dialog {
// Подтверждение восстановления
Dialog {
id: confirmRestore
property bool willDelete: false
title: qsTr("Восстановить версию?")
modal: true
anchors.centerIn: parent
@@ -373,10 +399,12 @@ Dialog {
anchors.fill: parent
spacing: 12
Label {
text: qsTr("Файл будет заменён на выбранную версию и сохранён как новая версия. Вы сможете снова вернуться к текущей версии через историю.")
text: confirmRestore.willDelete
? qsTr("ВНИМАНИЕ: в выбранной версии этого файла не было — файл будет УДАЛЁН (это тоже сохранится как новая версия, отменить можно через историю).")
: qsTr("Файл будет заменён на выбранную версию и сохранён как новая версия. Вы сможете снова вернуться к текущей версии через историю.")
wrapMode: Text.Wrap
Layout.fillWidth: true
color: Theme.text
color: confirmRestore.willDelete ? Theme.errorText : Theme.text
}
Label {
text: qsTr("Версия: %1 (%2)").arg(root.selectedHash.substring(0,7)).arg(root.selectedIndex>=0 && historyModel.length>root.selectedIndex ? historyModel[root.selectedIndex].timeStr : "")
@@ -389,7 +417,7 @@ Dialog {
Item { Layout.fillWidth: true }
Button { text: qsTr("Отмена"); onClicked: confirmRestore.close() }
Button {
text: qsTr("Восстановить")
text: confirmRestore.willDelete ? qsTr("Удалить файл") : qsTr("Восстановить")
highlighted: true
onClicked: {
confirmRestore.close()
+22 -4
View File
@@ -10,19 +10,24 @@ Item {
property string fileDir: ""
property string emptyText: qsTr("Пустой файл — начните писать в режиме редактирования")
// Если задан — markdown считается старой версией и подсвечиваются отличия от него
// (блоки del — только в markdown, add — только в compareMarkdown)
// (блоки del — только в markdown, add — только в compareMarkdown).
// Сравнение считается в рабочем потоке, чтобы UI не висел на больших файлах.
property string compareMarkdown: ""
property int _diffReq: -1
implicitHeight: column.implicitHeight + 24
signal linkActivated(string href)
signal diffApplied()
// Для обновления
function update() {
if (compareMarkdown.length > 0) {
_diffReq = mdParser.parseBlocksWithDiffAsync(markdown, compareMarkdown, vaultRoot, fileDir)
return
}
// Триггерим пересборку блоков
blocksModel.clear()
const blocks = compareMarkdown.length > 0
? mdParser.parseBlocksWithDiff(markdown, compareMarkdown, vaultRoot, fileDir)
: mdParser.parseBlocks(markdown, vaultRoot, fileDir)
const blocks = mdParser.parseBlocks(markdown, vaultRoot, fileDir)
for (let i = 0; i < blocks.length; i++) {
blocksModel.append(blocks[i])
}
@@ -30,6 +35,19 @@ Item {
// contentItem.contentY = 0
}
Connections {
target: mdParser
function onBlocksReady(reqId, blocks) {
if (reqId !== root._diffReq)
return // устарел — уже запрошен более свежий
blocksModel.clear()
for (let i = 0; i < blocks.length; i++) {
blocksModel.append(blocks[i])
}
root.diffApplied()
}
}
onMarkdownChanged: update()
onCompareMarkdownChanged: update()
onVaultRootChanged: update()