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:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user