deps: diff-merge

dev: graph parser IoC hook

test: use test db for diff-merge tests

fix: ci lint

dev: refactoring post block-parsing process

feat: diff-merge 2 way merge integration

fix: key namespace of uuid in fix-duplicated-id

fix: duplicated uuid ci
This commit is contained in:
Junyi Du
2023-03-07 18:45:11 +08:00
committed by Gabriel Horner
parent b547ad8fbc
commit 5aba871ead
13 changed files with 596 additions and 41 deletions

View File

@@ -225,6 +225,7 @@
db-utils/seq-flatten)))
(defn set-file-last-modified-at!
"Refresh file timestamps to DB"
[repo path last-modified-at]
(when (and repo path last-modified-at)
(when-let [conn (conn/get-db repo false)]
@@ -459,6 +460,34 @@ independent of format as format specific heading characters are stripped"
blocks-map (zipmap (map :db/id blocks) blocks)]
(keep blocks-map sorted-ids)))
;; Diverged of get-sorted-page-block-ids
(defn get-sorted-page-block-ids-and-levels
"page-name: the page name, original name
return: a list with elements in:
:id - a list of block ids, sorted by :block/left
:level - the level of the block, 1 for root, 2 for children of root, etc."
[page-name]
{:pre [(string? page-name)]}
(let [sanitized-page (gp-util/page-name-sanity-lc page-name)
page-id (:db/id (db-utils/entity [:block/name sanitized-page]))
root (db-utils/entity page-id)]
(loop [result []
children (sort-by-left (:block/_parent root) root)
;; BFS log of walking depth
levels (repeat (count children) 1)]
(if (seq children)
(let [child (first children)
cur-level (first levels)
next-children (sort-by-left (:block/_parent child) child)]
(recur (conj result {:id (:db/id child) :level cur-level})
(concat
next-children
(rest children))
(concat
(repeat (count next-children) (inc cur-level))
(rest levels))))
result))))
(defn has-children?
([block-id]
(has-children? (conn/get-db) block-id))

View File

@@ -0,0 +1,93 @@
(ns frontend.fs.diff-merge
;; Disable clj linters since we don't support clj
#?(:clj {:clj-kondo/config {:linters {:unresolved-namespace {:level :off}
:unresolved-symbol {:level :off}}}})
(:require #?(:org.babashka/nbb ["@logseq/diff-merge$default" :refer [Merger Differ visualizeAsHTML attach_uuids]]
:default ["@logseq/diff-merge" :refer [Differ Merger visualizeAsHTML attach_uuids]])
[logseq.graph-parser.block :as gp-block]
[logseq.graph-parser.property :as gp-property]
[logseq.graph-parser.utf8 :as utf8]
[cljs-bean.core :as bean]
[frontend.db.utils :as db-utils]
[frontend.db.model :as db-model]))
;; (defn diff-merge
;; "N-ways diff & merge
;; Accept: blocks
;; https://github.com/logseq/diff-merge/blob/44546f2427f20bd417b898c8ba7b7d10a9254774/lib/mldoc.ts#L17-L22
;; https://github.com/logseq/diff-merge/blob/85ca7e9bf7740d3880ed97d535a4f782a963395d/lib/merge.ts#L40"
;; [base & branches]
;; ()
;; (let [merger (Merger.)]
;; (.mergeBlocks merger (bean/->js base) (bean/->js branches))))
(defn diff
"2-ways diff
Accept: blocks
https://github.com/logseq/diff-merge/blob/44546f2427f20bd417b898c8ba7b7d10a9254774/lib/mldoc.ts#L17-L22"
[base income]
(let [differ (Differ.)]
(.diff_logseqMode differ (bean/->js base) (bean/->js income))))
;; (defonce getHTML visualizeAsHTML)
(defonce attachUUID attach_uuids)
(defn db->diff-blocks
"db: datascript db
page-name: string"
[page-name]
{:pre (string? page-name)}
(let [walked (db-model/get-sorted-page-block-ids-and-levels page-name)
blocks (db-utils/pull-many [:block/uuid :block/content :block/level] (map :id walked))
levels (map :level walked)
blocks (map (fn [block level]
{:uuid (str (:block/uuid block)) ;; Force to be string
:body (:block/content block)
:level level})
blocks levels)]
blocks))
;; TODO Junyi: merge back to gp-block/extract-blocks
;; From back to first to ensure end_pos is correct
(defn ast->diff-blocks
"Prepare the blocks for diff-merge
blocks: ast of blocks
content: corresponding raw content"
[blocks content format {:keys [user-config block-pattern]}]
{:pre [(string? content) (contains? #{:markdown :org} format)]}
(let [encoded-content (utf8/encode content)]
(loop [headings []
blocks (reverse blocks)
properties {}
end-pos (.-length encoded-content)]
(if (seq blocks)
(let [[block pos-meta] (first blocks)
;; fix start_pos
pos-meta (assoc pos-meta :end_pos end-pos)]
(cond
(gp-block/heading-block? block)
(let [content (gp-block/get-block-content encoded-content block format pos-meta block-pattern)]
(recur (conj headings {:body content
:level (:level (second block))
:uuid (:id properties)})
(rest blocks) {} (:start_pos pos-meta))) ;; The current block's start pos is the next block's end pos
(gp-property/properties-ast? block)
(let [new-props (:properties (gp-block/extract-properties (second block) (assoc user-config :format format)))]
;; sending the current end pos to next, as it's not finished yet
;; supports multiple properties sub-block possible in future
(recur headings (rest blocks) (merge properties new-props) (:end_pos pos-meta)))
:else
(recur headings (rest blocks) properties (:end_pos pos-meta))))
(if (empty? properties)
(reverse headings)
(let [[block _] (first blocks)
pos-meta {:start_pos 0 :end_pos end-pos}
content (gp-block/get-block-content encoded-content block format pos-meta block-pattern)
uuid (:id properties)]
(cons {:body content
:level 1
:uuid uuid}
(reverse headings))))))))

View File

@@ -23,6 +23,7 @@
;; all IPC paths must be normalized! (via gp-util/path-normalize)
(defn- set-missing-block-ids!
"For every referred block in the content, fix their block ids in files if missing."
[content]
(when (string? content)
(doseq [block-id (block-ref/get-all-block-ref-ids content)]
@@ -43,7 +44,8 @@
(p/catch #(js/console.error "❌ Bak Error: " path %))))
_ (file-handler/alter-file repo path content {:re-render-root? true
:from-disk? true})]
:from-disk? true
:fs/event :fs/local-file-change})]
(set-missing-block-ids! content)
(db/set-file-last-modified-at! repo path mtime)))

View File

@@ -6,10 +6,12 @@
[logseq.graph-parser :as graph-parser]
[logseq.graph-parser.util :as gp-util]
[logseq.graph-parser.config :as gp-config]
[frontend.fs.diff-merge :as diff-merge]
[frontend.fs :as fs]
[frontend.context.i18n :refer [t]]
[promesa.core :as p]
[clojure.string :as string]
[promesa.core :as p]))
[cljs-bean.core :as bean]))
(defn- page-exists-in-another-file
"Conflict of files towards same page"
@@ -20,12 +22,18 @@
current-file))))
(defn- validate-existing-file
"Handle the case when the file is already exists in db
Likely caused by renaming between caps and non-caps, then cause file system
bugs on some OS
e.g. on macOS, it doesn't fire the file change event when renaming between
caps and non-caps"
[repo-url file-page file-path]
(when-let [current-file (page-exists-in-another-file repo-url file-page file-path)]
(when (not= file-path current-file)
(cond
(= (string/lower-case current-file)
(string/lower-case file-path))
;; TODO: handle case sensitive file system
(= (gp-util/path-normalize (string/lower-case current-file))
(gp-util/path-normalize (string/lower-case file-path)))
;; case renamed
(when-let [file (db/pull [:file/path current-file])]
(p/let [disk-content (fs/read-file "" current-file)]
@@ -41,17 +49,53 @@
:clear? false}]))))))
(defn- validate-and-get-blocks-to-delete
"An implementation for the delete-blocks-fn in graph-parser/parse-file"
[repo-url db file-page file-path retain-uuid-blocks]
(validate-existing-file repo-url file-page file-path)
(graph-parser/get-blocks-to-delete db file-page file-path retain-uuid-blocks))
(defn- diff-merge-uuids
"Infer new uuids from existing DB data and diff with the new AST
Return a list of uuids for the new blocks"
[format ast content {:keys [page-name] :as options}]
(let [base-diffblocks (diff-merge/db->diff-blocks page-name)
income-diffblocks (diff-merge/ast->diff-blocks ast content format options)
diff-ops (diff-merge/diff base-diffblocks income-diffblocks)
new-uuids (diff-merge/attachUUID diff-ops (map :uuid base-diffblocks))]
(bean/->clj new-uuids)))
(defn- reset-file!-impl
"Parse file considering diff-merge with local or remote file
Decide how to treat the parsed file based on the file's triggering event
options -
:fs/reset-event - the event that triggered the file update
:fs/local-file-change - file changed on local disk
:fs/remote-file-change - file changed on remote"
[repo-url file content {:fs/keys [event] :as options}]
(let [db-conn (db/get-db repo-url false)]
(case event
;; the file is already in db, so we can use the existing file's blocks
;; to do the diff-merge
:fs/local-file-change
(graph-parser/parse-file db-conn file content (assoc-in options [:extract-options :resolve-uuid-fn] diff-merge-uuids))
;; TODO Junyi: 3 ways to handle remote file change
;; The file is on remote, so we should have
;; 1. a "common ancestor" file locally
;; the worst case is that the file is not in db, so we should use the
;; empty file as the common ancestor
;; 2. a "remote version" just fetched from remote
;; default to parse the file
(graph-parser/parse-file db-conn file content options))))
(defn reset-file!
"Main fn for updating a db with the results of a parsed file"
([repo-url file-path content]
(reset-file! repo-url file-path content {}))
([repo-url file-path content {:keys [verbose] :as options}]
([repo-url file-path content {:keys [verbose extracted-block-ids] :as options}]
(let [new? (nil? (db/entity [:file/path file-path]))
options (merge (dissoc options :verbose)
options (merge (dissoc options :verbose :extracted-block-ids)
{:new? new?
:delete-blocks-fn (partial validate-and-get-blocks-to-delete repo-url)
;; Options here should also be present in gp-cli/parse-graph
@@ -60,7 +104,8 @@
:date-formatter (state/get-date-formatter)
:block-pattern (config/get-block-pattern (gp-util/get-format file-path))
:supported-formats (gp-config/supported-formats)
:filename-format (state/get-filename-format repo-url)
:extracted-block-ids (:extracted-block-ids options)}
:filename-format (state/get-filename-format repo-url)}
;; To avoid skipping the `:or` bounds for keyword destructuring
(when (some? extracted-block-ids) {:extracted-block-ids extracted-block-ids})
(when (some? verbose) {:verbose verbose}))})]
(:tx (graph-parser/parse-file (db/get-db repo-url false) file-path content options)))))
(:tx (reset-file!-impl repo-url file-path content options)))))

View File

@@ -143,6 +143,7 @@
"Write any in-DB file, e.g. repo config, page, whiteboard, etc."
[repo path content {:keys [reset? re-render-root? from-disk? skip-compare? new-graph? verbose
skip-db-transact? extracted-block-ids]
:fs/keys [event]
:or {reset? true
re-render-root? false
from-disk? false
@@ -156,7 +157,7 @@
(let [opts {:new-graph? new-graph?
:from-disk? from-disk?
:skip-db-transact? skip-db-transact?
:extracted-block-ids extracted-block-ids}
:fs/event event}
result (if reset?
(do
(when-not skip-db-transact?
@@ -167,6 +168,8 @@
opts)))
(file-common-handler/reset-file!
repo path content (merge opts
;; To avoid skipping the `:or` bounds for keyword destructuring
(when (some? extracted-block-ids) {:extracted-block-ids extracted-block-ids})
(when (some? verbose) {:verbose verbose}))))
(db/set-file-content! repo path content opts))]
(-> (p/let [_ (when-not from-disk?

View File

@@ -141,8 +141,9 @@
(merge {:new-graph? new-graph?
:re-render-root? false
:from-disk? true
:skip-db-transact? skip-db-transact?
:extracted-block-ids extracted-block-ids}
:skip-db-transact? skip-db-transact?}
;; To avoid skipping the `:or` bounds for keyword destructuring
(when (some? extracted-block-ids) {:extracted-block-ids extracted-block-ids})
(when (some? verbose) {:verbose verbose}))))
(state/set-parsing-state! (fn [m]
(update m :finished inc)))

View File

@@ -0,0 +1,339 @@
(ns frontend.fs.diff-merge-test
(:require [datascript.core :as d]
[cljs.test :refer [deftest are is]]
[logseq.db :as ldb]
[logseq.graph-parser :as graph-parser]
[frontend.fs.diff-merge :as fs-diff]
[frontend.handler.common.file :as file-common-handler]
[frontend.db.conn :as conn]
[logseq.graph-parser.mldoc :as gp-mldoc]
[cljs-bean.core :as bean]))
(defn test-db->diff-blocks
"A hijacked version of db->diff-blocks for testing.
It overwrites the internal db getter with the test db connection."
[conn & args]
(with-redefs [conn/get-db (constantly @conn)]
(apply fs-diff/db->diff-blocks args)))
(defn org-text->diffblocks
[text]
(-> (gp-mldoc/->edn text (gp-mldoc/default-config :org))
(fs-diff/ast->diff-blocks text :org {:block-pattern "-"})))
(deftest org->ast->diff-blocks-test
(are [text diff-blocks]
(= (org-text->diffblocks text)
diff-blocks)
":PROPERTIES:
:ID: 72289d9a-eb2f-427b-ad97-b605a4b8c59b
:END:
#+tItLe: Well parsed!"
[{:body ":PROPERTIES:\n:ID: 72289d9a-eb2f-427b-ad97-b605a4b8c59b\n:END:\n#+tItLe: Well parsed!"
:uuid "72289d9a-eb2f-427b-ad97-b605a4b8c59b"
:level 1}]
"#+title: Howdy"
[{:body "#+title: Howdy" :uuid nil :level 1}]
":PROPERTIES:
:fiction: [[aldsjfklsda]]
:END:\n#+title: Howdy"
[{:body ":PROPERTIES:\n:fiction: [[aldsjfklsda]]\n:END:\n#+title: Howdy"
:uuid nil
:level 1}]))
(deftest db<->ast-diff-blocks-test
(let [conn (ldb/start-conn)
text ":PROPERTIES:
:ID: 72289d9a-eb2f-427b-ad97-b605a4b8c59b
:END:
#+tItLe: Well parsed!"]
(graph-parser/parse-file conn "foo.org" text {})
(is (= (test-db->diff-blocks conn "Well parsed!")
(org-text->diffblocks text)))))
(defn text->diffblocks
[text]
(-> (gp-mldoc/->edn text (gp-mldoc/default-config :markdown))
(fs-diff/ast->diff-blocks text :markdown {:block-pattern "-"})))
(deftest md->ast->diff-blocks-test
(are [text diff-blocks]
(= (text->diffblocks text)
diff-blocks)
"- a
\t- b
\t\t- c"
[{:body "a" :uuid nil :level 1}
{:body "b" :uuid nil :level 2}
{:body "c" :uuid nil :level 3}]
"## hello
\t- world
\t\t- nice
\t\t\t- nice
\t\t\t- bingo
\t\t\t- world"
[{:body "## hello" :uuid nil :level 2}
{:body "world" :uuid nil :level 2}
{:body "nice" :uuid nil :level 3}
{:body "nice" :uuid nil :level 4}
{:body "bingo" :uuid nil :level 4}
{:body "world" :uuid nil :level 4}]
"# a
## b
### c
#### d
### e
- f
\t- g
\t\t- h
\t- i
- j"
[{:body "# a" :uuid nil :level 1}
{:body "## b" :uuid nil :level 2}
{:body "### c" :uuid nil :level 3}
{:body "#### d" :uuid nil :level 4}
{:body "### e" :uuid nil :level 3}
{:body "f" :uuid nil :level 1}
{:body "g" :uuid nil :level 2}
{:body "h" :uuid nil :level 3}
{:body "i" :uuid nil :level 2}
{:body "j" :uuid nil :level 1}]
"- a\n id:: 63e25526-3612-4fb1-8cf9-f66db1254a58
\t- b
\t\t- c"
[{:body "a\n id:: 63e25526-3612-4fb1-8cf9-f66db1254a58"
:uuid "63e25526-3612-4fb1-8cf9-f66db1254a58" :level 1}
{:body "b" :uuid nil :level 2}
{:body "c" :uuid nil :level 3}]))
(deftest diff-test
(are [text1 text2 diffs]
(= (bean/->clj (fs-diff/diff (text->diffblocks text1)
(text->diffblocks text2)))
diffs)
"## hello
\t- world
\t\t- nice
\t\t\t- nice
\t\t\t- bingo
\t\t\t- world"
"## Halooooo
\t- world
\t\t- nice
\t\t\t- nice
\t\t\t- bingo
\t\t\t- world"
[[[-1 {:body "## hello"
:level 2
:uuid nil}]
[1 {:body "## Halooooo"
:level 2
:uuid nil}]]
[[0 {:body "world"
:level 2
:uuid nil}]]
[[0 {:body "nice"
:level 3
:uuid nil}]]
[[0 {:body "nice"
:level 4
:uuid nil}]]
[[0 {:body "bingo"
:level 4
:uuid nil}]]
[[0 {:body "world"
:level 4
:uuid nil}]]]
"## hello
\t- world
\t id:: 63e25526-3612-4fb1-8cf9-abcd12354abc
\t\t- nice
\t\t\t- nice
\t\t\t- bingo
\t\t\t- world"
"## Halooooo
\t- world
\t\t- nice
\t\t\t- nice
\t\t\t- bingo
\t\t\t- world"
[[[-1 {:body "## hello"
:level 2
:uuid nil}]
[1 {:body "## Halooooo"
:level 2
:uuid nil}]
[1 {:body "world"
:level 2
:uuid nil}]]
[[-1 {:body "world\n id:: 63e25526-3612-4fb1-8cf9-abcd12354abc"
:level 2
:uuid "63e25526-3612-4fb1-8cf9-abcd12354abc"}]]
[[0 {:body "nice"
:level 3
:uuid nil}]]
[[0 {:body "nice"
:level 4
:uuid nil}]]
[[0 {:body "bingo"
:level 4
:uuid nil}]]
[[0 {:body "world"
:level 4
:uuid nil}]]]
""
"- abc def"
[[[1 {:body "abc def"
:level 1
:uuid nil}]]]))
(deftest db->diffblocks
(let [conn (ldb/start-conn)]
(graph-parser/parse-file conn
"foo.md"
(str "- abc
id:: 11451400-0000-0000-0000-000000000000\n"
"- def
id:: 63246324-6324-6324-6324-632463246324\n")
{})
(graph-parser/parse-file conn
"bar.md"
(str "- ghi
id:: 11451411-1111-1111-1111-111111111111\n"
"\t- jkl
\t id:: 63241234-1234-1234-1234-123412341234\n")
{})
(are [page-name diff-blocks] (= (test-db->diff-blocks conn page-name)
diff-blocks)
"foo"
[{:body "abc\nid:: 11451400-0000-0000-0000-000000000000" :uuid "11451400-0000-0000-0000-000000000000" :level 1}
{:body "def\nid:: 63246324-6324-6324-6324-632463246324" :uuid "63246324-6324-6324-6324-632463246324" :level 1}]
"bar"
[{:body "ghi\nid:: 11451411-1111-1111-1111-111111111111" :uuid "11451411-1111-1111-1111-111111111111" :level 1}
{:body "jkl\nid:: 63241234-1234-1234-1234-123412341234" :uuid "63241234-1234-1234-1234-123412341234" :level 2}])
(are [page-name text new-uuids] (= (let [old-blks (test-db->diff-blocks conn page-name)
new-blks (text->diffblocks text)
diff-ops (fs-diff/diff old-blks new-blks)]
(bean/->clj (fs-diff/attachUUID diff-ops (bean/->js (map :uuid old-blks)) "NEW_ID")))
new-uuids)
"foo"
"- abc
- def"
["11451400-0000-0000-0000-000000000000"
"NEW_ID"]
"bar"
"- ghi
\t- jkl"
["11451411-1111-1111-1111-111111111111"
"NEW_ID"]
"non exist page"
"- k\n\t- l"
["NEW_ID" "NEW_ID"]
"another non exist page"
":PROPERTIES:
:ID: 72289d9a-eb2f-427b-ad97-b605a4b8c59b
:END:
#+tItLe: Well parsed!"
["72289d9a-eb2f-427b-ad97-b605a4b8c59b"])))
(deftest ast->diff-blocks-test
(are [ast text diff-blocks]
(= (fs-diff/ast->diff-blocks ast text :org {:block-pattern "-"})
diff-blocks)
[[["Properties" [["TiTlE" "Howdy" []]]] nil]]
"#+title: Howdy"
[{:body "#+title: Howdy", :level 1, :uuid nil}])
(are [ast text diff-blocks]
(= (fs-diff/ast->diff-blocks ast text :org {:block-pattern "-" :user-config {:property-pages/enabled? true}})
diff-blocks)
[[["Property_Drawer" [["foo" "#bar" [["Tag" [["Plain" "bar"]]]]] ["baz" "#bing" [["Tag" [["Plain" "bing"]]]]]]] {:start_pos 0, :end_pos 22}]]
"foo:: #bar\nbaz:: #bing"
[{:body "foo:: #bar\nbaz:: #bing", :level 1, :uuid nil}]))
(deftest ast-empty-diff-test
(are [ast text diff-ops]
(= (bean/->clj (->> (fs-diff/ast->diff-blocks ast text :org {:block-pattern "-" :user-config {:property-pages/enabled? true}})
(fs-diff/diff [])))
diff-ops)
[[["Property_Drawer" [["foo" "#bar" [["Tag" [["Plain" "bar"]]]]] ["baz" "#bing" [["Tag" [["Plain" "bing"]]]]]]] {:start_pos 0, :end_pos 22}]]
"foo:: #bar\nbaz:: #bing"
[[[1 {:body "foo:: #bar\nbaz:: #bing", :level 1, :uuid nil}]]]))
;; Ensure diff-merge-uuids follows the id:: in the content
(deftest diff-merge-uuid-extract-test
(let [conn (ldb/start-conn)
foo-content (str "- abc
id:: 11451400-0000-0000-0000-000000000000\n"
"- def
id:: 63246324-6324-6324-6324-632463246324\n")
bar-content (str "- ghi
id:: 11451411-1111-1111-1111-111111111111\n"
"\t- jkl
\t id:: 63241234-1234-1234-1234-123412341234\n") ]
(graph-parser/parse-file conn "foo.md" foo-content {})
(graph-parser/parse-file conn "bar.md" bar-content {})
(are [ast content page-name uuids]
(= (with-redefs [conn/get-db (constantly @conn)]
(#'file-common-handler/diff-merge-uuids :markdown ast content {:page-name page-name
:block-pattern "-"}))
uuids)
(gp-mldoc/->edn (str foo-content "- newline\n") (gp-mldoc/default-config :markdown))
(str foo-content "- newline\n")
"foo"
["11451400-0000-0000-0000-000000000000"
"63246324-6324-6324-6324-632463246324"
nil]
(gp-mldoc/->edn (str bar-content "- newline\n") (gp-mldoc/default-config :markdown))
(str bar-content "- newline\n")
"bar"
["11451411-1111-1111-1111-111111111111"
"63241234-1234-1234-1234-123412341234"
nil])))
;; Ensure diff-merge-uuids keeps the block uuids unchanged at best effort
(deftest diff-merge-uuid-persist-test
(let [conn (ldb/start-conn)
foo-content (str "- abc\n"
"- def\n")
bar-content (str "- ghi\n"
"\t- jkl\n")]
(graph-parser/parse-file conn "foo.md" foo-content {})
(graph-parser/parse-file conn "bar.md" bar-content {})
(are [ast content page-name uuids]
(= (with-redefs [conn/get-db (constantly @conn)]
(#'file-common-handler/diff-merge-uuids :markdown ast content {:page-name page-name
:block-pattern "-"}))
;; Get all uuids under the page
(conj (->> page-name
(test-db->diff-blocks conn)
(map :uuid)
(vec)) nil))
(gp-mldoc/->edn (str foo-content "- newline\n") (gp-mldoc/default-config :markdown))
(str foo-content "- newline\n")
"foo"
["11451400-0000-0000-0000-000000000000"
"63246324-6324-6324-6324-632463246324"
nil]
(gp-mldoc/->edn (str bar-content "- newline\n") (gp-mldoc/default-config :markdown))
(str bar-content "- newline\n")
"bar"
["11451411-1111-1111-1111-111111111111"
"63241234-1234-1234-1234-123412341234"
nil])))