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:
@@ -6,6 +6,8 @@ import Flora
|
||||
Item {
|
||||
id: root
|
||||
|
||||
signal historyRequested(string path)
|
||||
|
||||
property bool sideBySide: false
|
||||
property string viewMode: "edit" // edit, preview, live — per-tab, persisted
|
||||
property bool _loadingViewMode: false
|
||||
@@ -411,11 +413,13 @@ Item {
|
||||
Timer { id: liveDebounce; interval: 150; onTriggered: { if (viewMode === "live") livePreview.text = mdParser.render(textArea.text, fileStore.vault, activeFileDir()) } }
|
||||
|
||||
property string gitStatusText: ""
|
||||
property string lastModifiedStr: ""
|
||||
function refreshGitStatus() {
|
||||
if (documentManager.activeIndex < 0 || !fileStore.ready) { gitStatusText = ""; return }
|
||||
if (documentManager.activeIndex < 0 || !fileStore.ready) { gitStatusText = ""; lastModifiedStr = ""; return }
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
if (!p) { gitStatusText = ""; return }
|
||||
if (!p) { gitStatusText = ""; lastModifiedStr = ""; return }
|
||||
try { gitStatusText = gitManager.fileGitStatus(fileStore.vault, p) } catch(e) { gitStatusText = "" }
|
||||
try { lastModifiedStr = gitManager.lastModifiedTimeString(fileStore.vault, p) } catch(e) { lastModifiedStr = "" }
|
||||
}
|
||||
function loadContent() {
|
||||
const idx = documentManager.activeIndex
|
||||
@@ -458,10 +462,32 @@ Item {
|
||||
function onContentChanged(idx) { if (idx === documentManager.activeIndex && !textArea.activeFocus) root.loadContent(); refreshGitStatus() }
|
||||
function onOpenTabsChanged() { root.loadContent() }
|
||||
function onDirtyFlagChanged() { refreshGitStatus() }
|
||||
function onDocumentSaved() { refreshGitStatus() }
|
||||
function onDocumentSaved(idx) {
|
||||
refreshGitStatus()
|
||||
// Авто-коммит для простого пользователя — каждый сохранённый файл попадает в историю
|
||||
if (fileStore.ready && idx >= 0) {
|
||||
const p = documentManager.pathAt(idx)
|
||||
if (p && p.length>0) {
|
||||
// Небольшая задержка чтобы не спамить на каждый автосейв
|
||||
autoCommitDebounce.filePath = p
|
||||
autoCommitDebounce.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections { target: fileStore; function onVaultChanged() { refreshGitStatus(); loadContent() } }
|
||||
Timer { id: gitPoll; interval: 2000; running: documentManager.activeIndex >= 0; repeat: true; onTriggered: refreshGitStatus() }
|
||||
Timer { id: gitPoll; interval: 3000; running: documentManager.activeIndex >= 0; repeat: true; onTriggered: refreshGitStatus() }
|
||||
Timer {
|
||||
id: autoCommitDebounce
|
||||
property string filePath: ""
|
||||
interval: 2000
|
||||
onTriggered: {
|
||||
if (fileStore.ready && filePath.length>0) {
|
||||
gitManager.autoCommit(fileStore.vault, filePath)
|
||||
refreshGitStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: loadContent()
|
||||
|
||||
function applyInline(pre, post) {
|
||||
@@ -578,7 +604,27 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
Item { width: 8 }
|
||||
ToolButton {
|
||||
text: "🕘"
|
||||
enabled: documentManager.activeIndex >= 0
|
||||
ToolTip.text: lastModifiedStr.length>0 ? qsTr("История — последнее изменение: %1").arg(lastModifiedStr) : qsTr("История файла (Ctrl+H)")
|
||||
ToolTip.visible: hovered
|
||||
onClicked: {
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
if (p) root.historyRequested(p)
|
||||
}
|
||||
}
|
||||
Label {
|
||||
visible: lastModifiedStr.length>0 && documentManager.activeIndex>=0
|
||||
text: lastModifiedStr
|
||||
color: Theme.textMuted
|
||||
font.pixelSize: 10
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
ToolTip.text: qsTr("Последнее изменение")
|
||||
HoverHandler { id: lastModHover }
|
||||
ToolTip.visible: lastModHover.hovered
|
||||
}
|
||||
Item { width: 4 }
|
||||
ButtonGroup { id: viewGroup }
|
||||
ToolButton {
|
||||
text: "✎"
|
||||
@@ -627,6 +673,10 @@ Item {
|
||||
|
||||
Shortcut { sequence: "Ctrl+S"; onActivated: { documentManager.saveAll(); refreshGitStatus() } }
|
||||
Shortcut { sequence: "Ctrl+W"; onActivated: documentManager.closeTab(documentManager.activeIndex) }
|
||||
Shortcut { sequence: "Ctrl+H"; onActivated: {
|
||||
const p = documentManager.pathAt(documentManager.activeIndex)
|
||||
if (p) root.historyRequested(p)
|
||||
} }
|
||||
Shortcut { sequence: "Ctrl+E"; onActivated: {
|
||||
if (viewMode === "edit") {
|
||||
const rendered = mdParser.render(textArea.text, fileStore.vault, activeFileDir())
|
||||
|
||||
@@ -8,6 +8,7 @@ Item {
|
||||
|
||||
signal openVaultRequested()
|
||||
signal openFileRequested(string path)
|
||||
signal historyRequested(string path)
|
||||
|
||||
function baseName(p) {
|
||||
const idx = p.lastIndexOf("/")
|
||||
@@ -415,6 +416,12 @@ Item {
|
||||
deleteDialog.open()
|
||||
}
|
||||
}
|
||||
MenuSeparator { visible: !rowMenu.targetIsDir }
|
||||
MenuItem {
|
||||
text: qsTr("История…")
|
||||
visible: !rowMenu.targetIsDir
|
||||
onClicked: root.historyRequested(rowMenu.targetPath)
|
||||
}
|
||||
}
|
||||
|
||||
function rowIndexOf(p) { const n = fileStore.rowCount(); for (let i=0;i<n;++i) if (fileStore.itemPath(i)===p) return i; return -1 }
|
||||
|
||||
Reference in New Issue
Block a user