mirror of
https://github.com/logseq/logseq.git
synced 2026-05-02 18:06:32 +00:00
569 lines
24 KiB
Clojure
569 lines
24 KiB
Clojure
(ns frontend.handler.repo
|
|
(:refer-clojure :exclude [clone])
|
|
(:require [frontend.util :as util :refer-macros [profile]]
|
|
[frontend.fs :as fs]
|
|
[promesa.core :as p]
|
|
[lambdaisland.glogi :as log]
|
|
[frontend.state :as state]
|
|
[frontend.db :as db]
|
|
[frontend.git :as git]
|
|
[cljs-bean.core :as bean]
|
|
[frontend.date :as date]
|
|
[frontend.config :as config]
|
|
[frontend.format :as format]
|
|
[frontend.handler.ui :as ui-handler]
|
|
[frontend.handler.git :as git-handler]
|
|
[frontend.handler.file :as file-handler]
|
|
[frontend.handler.notification :as notification]
|
|
[frontend.handler.route :as route-handler]
|
|
[frontend.handler.common :as common-handler]
|
|
[frontend.ui :as ui]
|
|
[cljs.reader :as reader]
|
|
[clojure.string :as string]
|
|
[frontend.dicts :as dicts]
|
|
[frontend.helper :as helper]
|
|
[frontend.spec :as spec]))
|
|
|
|
;; Project settings should be checked in two situations:
|
|
;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
|
|
;; 2. Git pulls the new change (fn: load-files)
|
|
|
|
(defn show-install-error!
|
|
[repo-url title]
|
|
(spec/validate :repos/url repo-url)
|
|
(notification/show!
|
|
[:p.content
|
|
title
|
|
[:span.mr-2
|
|
(util/format
|
|
"Please make sure that you've installed the logseq app for the repo %s on GitHub. "
|
|
repo-url)
|
|
(ui/button
|
|
"Install Logseq on GitHub"
|
|
:href (str "https://github.com/apps/" config/github-app-name "/installations/new"))]]
|
|
:error
|
|
false))
|
|
|
|
(defn create-config-file-if-not-exists
|
|
[repo-url]
|
|
(spec/validate :repos/url repo-url)
|
|
(let [repo-dir (util/get-repo-dir repo-url)
|
|
app-dir config/app-name
|
|
dir (str repo-dir "/" app-dir)]
|
|
(p/let [_ (fs/mkdir-if-not-exists dir)]
|
|
(let [default-content config/config-default-content]
|
|
(p/let [file-exists? (fs/create-if-not-exists repo-dir (str app-dir "/" config/config-file) default-content)]
|
|
(let [path (str app-dir "/" config/config-file)
|
|
old-content (when file-exists?
|
|
(db/get-file repo-url path))
|
|
content (or old-content default-content)]
|
|
(db/reset-file! repo-url path content)
|
|
(db/reset-config! repo-url content)
|
|
(when-not (= content old-content)
|
|
(git-handler/git-add repo-url path))))))))
|
|
|
|
(defn create-contents-file
|
|
[repo-url]
|
|
(spec/validate :repos/url repo-url)
|
|
(let [repo-dir (util/get-repo-dir repo-url)
|
|
format (state/get-preferred-format)
|
|
path (str (state/get-pages-directory)
|
|
"/contents."
|
|
(config/get-file-extension format))
|
|
file-path (str "/" path)
|
|
default-content (util/default-content-with-title format "contents")]
|
|
(p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" (state/get-pages-directory)))
|
|
file-exists? (fs/create-if-not-exists repo-dir file-path default-content)]
|
|
(when-not file-exists?
|
|
(db/reset-file! repo-url path default-content)
|
|
(git-handler/git-add repo-url path)))))
|
|
|
|
(defn create-custom-theme
|
|
[repo-url]
|
|
(spec/validate :repos/url repo-url)
|
|
(let [repo-dir (util/get-repo-dir repo-url)
|
|
path (str config/app-name "/" config/custom-css-file)
|
|
file-path (str "/" path)
|
|
default-content ""]
|
|
(p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/app-name))
|
|
file-exists? (fs/create-if-not-exists repo-dir file-path default-content)]
|
|
(when-not file-exists?
|
|
(db/reset-file! repo-url path default-content)
|
|
(git-handler/git-add repo-url path)))))
|
|
|
|
(defn create-dummy-notes-page
|
|
[repo-url content]
|
|
(spec/validate :repos/url repo-url)
|
|
(let [repo-dir (util/get-repo-dir repo-url)
|
|
path (str (config/get-pages-directory) "/how_to_make_dummy_notes.md")
|
|
file-path (str "/" path)]
|
|
(p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" (config/get-pages-directory)))
|
|
_file-exists? (fs/create-if-not-exists repo-dir file-path content)]
|
|
(db/reset-file! repo-url path content))))
|
|
|
|
(defn create-today-journal-if-not-exists
|
|
([repo-url]
|
|
(create-today-journal-if-not-exists repo-url nil))
|
|
([repo-url content]
|
|
(spec/validate :repos/url repo-url)
|
|
(when (config/local-db? repo-url)
|
|
(fs/check-directory-permission! repo-url))
|
|
(let [repo-dir (util/get-repo-dir repo-url)
|
|
format (state/get-preferred-format repo-url)
|
|
title (date/today)
|
|
file-name (date/journal-title->default title)
|
|
default-content (util/default-content-with-title format title false)
|
|
template (state/get-journal-template)
|
|
template (if (and template
|
|
(not (string/blank? template)))
|
|
template)
|
|
content (cond
|
|
content
|
|
content
|
|
|
|
template
|
|
(str default-content template)
|
|
|
|
:else
|
|
(util/default-content-with-title format title true))
|
|
path (str config/default-journals-directory "/" file-name "."
|
|
(config/get-file-extension format))
|
|
file-path (str "/" path)
|
|
page-exists? (db/entity repo-url [:page/name (string/lower-case title)])
|
|
empty-blocks? (empty? (db/get-page-blocks-no-cache repo-url (string/lower-case title)))]
|
|
(when (or empty-blocks?
|
|
(not page-exists?))
|
|
(p/let [_ (fs/mkdir-if-not-exists (str repo-dir "/" config/default-journals-directory))
|
|
file-exists? (fs/create-if-not-exists repo-dir file-path content)]
|
|
(when-not file-exists?
|
|
(db/reset-file! repo-url path content)
|
|
(ui-handler/re-render-root!)
|
|
(git-handler/git-add repo-url path)))))))
|
|
|
|
(defn create-default-files!
|
|
[repo-url]
|
|
(spec/validate :repos/url repo-url)
|
|
(create-config-file-if-not-exists repo-url)
|
|
(create-today-journal-if-not-exists repo-url)
|
|
(create-contents-file repo-url)
|
|
(create-custom-theme repo-url))
|
|
|
|
(defn- parse-files-and-load-to-db!
|
|
[repo-url files {:keys [first-clone? delete-files delete-blocks re-render? re-render-opts] :as opts}]
|
|
(state/set-loading-files! false)
|
|
(state/set-importing-to-db! true)
|
|
(let [file-paths (map :file/path files)
|
|
parsed-files (filter
|
|
(fn [file]
|
|
(let [format (format/get-format (:file/path file))]
|
|
(contains? config/mldoc-support-formats format)))
|
|
files)
|
|
blocks-pages (if (seq parsed-files)
|
|
(db/extract-all-blocks-pages repo-url parsed-files)
|
|
[])]
|
|
(db/reset-contents-and-blocks! repo-url files blocks-pages delete-files delete-blocks)
|
|
(let [config-file (str config/app-name "/" config/config-file)]
|
|
(if (contains? (set file-paths) config-file)
|
|
(when-let [content (some #(when (= (:file/path %) config-file)
|
|
(:file/content %)) files)]
|
|
(file-handler/restore-config! repo-url content true))))
|
|
(when first-clone? (create-default-files! repo-url))
|
|
(when re-render?
|
|
(ui-handler/re-render-root! re-render-opts))
|
|
(state/set-importing-to-db! false)))
|
|
|
|
(defn load-repo-to-db!
|
|
[repo-url {:keys [first-clone? diffs nfs-files]}]
|
|
(spec/validate :repos/url repo-url)
|
|
(let [load-contents (fn [files option]
|
|
(file-handler/load-files-contents!
|
|
repo-url
|
|
files
|
|
(fn [files-contents]
|
|
(parse-files-and-load-to-db! repo-url files-contents option))))]
|
|
(cond
|
|
(and (not (seq diffs)) (seq nfs-files))
|
|
(parse-files-and-load-to-db! repo-url nfs-files {:first-clone? true})
|
|
|
|
first-clone?
|
|
(->
|
|
(p/let [files (file-handler/load-files repo-url)]
|
|
(load-contents files {:first-clone? first-clone?}))
|
|
(p/catch (fn [error]
|
|
(println "loading files failed: ")
|
|
(js/console.dir error)
|
|
;; Empty repo
|
|
(create-default-files! repo-url)
|
|
(state/set-loading-files! false))))
|
|
|
|
:else
|
|
(when (seq diffs)
|
|
(let [filter-diffs (fn [type] (->> (filter (fn [f] (= type (:type f))) diffs)
|
|
(map :path)))
|
|
remove-files (filter-diffs "remove")
|
|
modify-files (filter-diffs "modify")
|
|
add-files (filter-diffs "add")
|
|
delete-files (if (seq remove-files)
|
|
(db/delete-files remove-files))
|
|
delete-blocks (db/delete-blocks repo-url (concat remove-files modify-files))
|
|
delete-pages (if (seq remove-files)
|
|
(db/delete-pages-by-files remove-files)
|
|
[])
|
|
add-or-modify-files (some->>
|
|
(concat modify-files add-files)
|
|
(util/remove-nils))
|
|
options {:first-clone? first-clone?
|
|
:delete-files (concat delete-files delete-pages)
|
|
:delete-blocks delete-blocks
|
|
:re-render? true}]
|
|
(if (seq nfs-files)
|
|
(parse-files-and-load-to-db! repo-url nfs-files
|
|
(assoc options :re-render-opts {:clear-all-query-state? true}))
|
|
(load-contents add-or-modify-files options)))))))
|
|
|
|
(defn load-db-and-journals!
|
|
[repo-url diffs first-clone?]
|
|
(spec/validate :repos/url repo-url)
|
|
(when (or diffs first-clone?)
|
|
(load-repo-to-db! repo-url {:first-clone? first-clone?
|
|
:diffs diffs})))
|
|
|
|
(defn transact-react-and-alter-file!
|
|
[repo tx transact-option files]
|
|
(spec/validate :repos/url repo)
|
|
(let [files (remove nil? files)
|
|
pages (->> (map db/get-file-page (map first files))
|
|
(remove nil?))]
|
|
(db/transact-react!
|
|
repo
|
|
tx
|
|
transact-option)
|
|
(when (seq pages)
|
|
(let [children-tx (mapcat #(db/rebuild-page-blocks-children repo %) pages)]
|
|
(when (seq children-tx)
|
|
(db/transact! repo children-tx))))
|
|
(when (seq files)
|
|
(file-handler/alter-files repo files))))
|
|
|
|
(declare push)
|
|
|
|
(defn get-diff-result
|
|
[repo-url]
|
|
(p/let [remote-latest-commit (common-handler/get-remote-ref repo-url)
|
|
local-latest-commit (common-handler/get-ref repo-url)]
|
|
(git/get-diffs repo-url local-latest-commit remote-latest-commit)))
|
|
|
|
(defn pull
|
|
[repo-url {:keys [force-pull? show-diff? try-times]
|
|
:or {force-pull? false
|
|
show-diff? false
|
|
try-times 2}
|
|
:as opts}]
|
|
(spec/validate :repos/url repo-url)
|
|
(when (and
|
|
(db/get-conn repo-url true)
|
|
(db/cloned? repo-url))
|
|
(p/let [remote-latest-commit (common-handler/get-remote-ref repo-url)
|
|
local-latest-commit (common-handler/get-ref repo-url)
|
|
descendent? (git/descendent? repo-url local-latest-commit remote-latest-commit)]
|
|
(when (or (= local-latest-commit remote-latest-commit)
|
|
(nil? local-latest-commit)
|
|
(not descendent?)
|
|
force-pull?)
|
|
(p/let [files (js/window.workerThread.getChangedFiles (util/get-repo-dir repo-url))]
|
|
(when (empty? files)
|
|
(let [status (db/get-key-value repo-url :git/status)]
|
|
(when (or
|
|
force-pull?
|
|
(and
|
|
(not= status :pushing)
|
|
(not (state/get-edit-input-id))
|
|
(not (state/in-draw-mode?))
|
|
;; don't pull if git conflicts not resolved yet
|
|
(or
|
|
show-diff?
|
|
(and (not show-diff?)
|
|
(empty? @state/diffs)))))
|
|
(git-handler/set-git-status! repo-url :pulling)
|
|
(->
|
|
(p/let [token (helper/get-github-token repo-url)
|
|
result (git/fetch repo-url token)]
|
|
(let [{:keys [fetchHead]} (bean/->clj result)]
|
|
(-> (git/merge repo-url)
|
|
(p/then (fn [result]
|
|
(-> (git/checkout repo-url)
|
|
(p/then (fn [result]
|
|
(git-handler/set-git-status! repo-url nil)
|
|
(git-handler/set-git-last-pulled-at! repo-url)
|
|
(when (and local-latest-commit fetchHead
|
|
(not= local-latest-commit fetchHead))
|
|
(p/let [diffs (git/get-diffs repo-url local-latest-commit fetchHead)]
|
|
(when (seq diffs)
|
|
(load-db-and-journals! repo-url diffs false))))
|
|
(common-handler/check-changed-files-status repo-url)))
|
|
(p/catch (fn [error]
|
|
(git-handler/set-git-status! repo-url :checkout-failed)
|
|
(git-handler/set-git-error! repo-url error))))))
|
|
(p/catch (fn [error]
|
|
(println "Git pull error:")
|
|
(js/console.error error)
|
|
(git-handler/set-git-status! repo-url :merge-failed)
|
|
(git-handler/set-git-error! repo-url error)
|
|
(p/let [result (get-diff-result repo-url)]
|
|
(if (seq result)
|
|
(do
|
|
(notification/show!
|
|
[:p.content
|
|
"Failed to merge, please "
|
|
[:span.font-bold
|
|
"resolve any diffs first."]]
|
|
:error)
|
|
(route-handler/redirect! {:to :diff}))
|
|
(push repo-url {:merge-push-no-diff? true
|
|
:commit-message "Merge push without diffed files"}))))))))
|
|
(p/catch
|
|
(fn [error]
|
|
(cond
|
|
(string/includes? (str error) "404")
|
|
(do (log/error :git/pull-error error)
|
|
(show-install-error! repo-url (util/format "Failed to fetch %s." repo-url)))
|
|
|
|
(string/includes? (str error) "401")
|
|
(let [remain-times (dec try-times)]
|
|
(if (> remain-times 0)
|
|
(let [new-opts (merge opts {:try-times remain-times})]
|
|
(pull repo-url new-opts))
|
|
(let [error-msg
|
|
(util/format "Failed to fetch %s. It may be caused by token expiration or missing." repo-url)]
|
|
(log/error :git/pull-error error)
|
|
(notification/show! error-msg :error false))))
|
|
|
|
:else
|
|
(log/error :git/pull-error error)))))))))))))
|
|
|
|
(defn push
|
|
[repo-url {:keys [commit-message merge-push-no-diff? custom-commit?]
|
|
:or {custom-commit false
|
|
commit-message "Logseq auto save"
|
|
merge-push-no-diff? false}}]
|
|
(spec/validate :repos/url repo-url)
|
|
(let [status (db/get-key-value repo-url :git/status)]
|
|
(if (and
|
|
(db/cloned? repo-url)
|
|
(state/input-idle? repo-url)
|
|
(or (not= status :pushing)
|
|
custom-commit?))
|
|
(-> (p/let [files (js/window.workerThread.getChangedFiles (util/get-repo-dir (state/get-current-repo)))]
|
|
(when (or (seq files) merge-push-no-diff?)
|
|
;; auto commit if there are any un-committed changes
|
|
(let [commit-message (if (string/blank? commit-message)
|
|
"Logseq auto save"
|
|
commit-message)]
|
|
(p/let [commit-oid (git/commit repo-url commit-message)
|
|
token (helper/get-github-token repo-url)
|
|
status (db/get-key-value repo-url :git/status)]
|
|
(when (and token (or (not= status :pushing)
|
|
custom-commit?))
|
|
(git-handler/set-git-status! repo-url :pushing)
|
|
(util/p-handle
|
|
(git/push repo-url token merge-push-no-diff?)
|
|
(fn [result]
|
|
(git-handler/set-git-status! repo-url nil)
|
|
(git-handler/set-git-error! repo-url nil)
|
|
(common-handler/check-changed-files-status repo-url))
|
|
(fn [error]
|
|
(log/error :git/push-error error)
|
|
(js/console.error error)
|
|
(common-handler/check-changed-files-status repo-url)
|
|
(do
|
|
(git-handler/set-git-status! repo-url :push-failed)
|
|
(git-handler/set-git-error! repo-url error)
|
|
(when (state/online?)
|
|
(pull repo-url {:force-pull? true
|
|
:show-diff? true}))))))))))
|
|
(p/catch (fn [error]
|
|
(log/error :git/get-changed-files-error error)
|
|
(git-handler/set-git-status! repo-url :push-failed)
|
|
(git-handler/set-git-error! repo-url error)
|
|
(js/console.dir error)))))))
|
|
|
|
(defn push-if-auto-enabled!
|
|
[repo]
|
|
(spec/validate :repos/url repo)
|
|
(when (state/get-git-auto-push? repo)
|
|
(push repo nil)))
|
|
|
|
(defn pull-current-repo
|
|
[]
|
|
(when-let [repo (state/get-current-repo)]
|
|
(pull repo {:force-pull? true})))
|
|
|
|
(defn- clone
|
|
[repo-url]
|
|
(spec/validate :repos/url repo-url)
|
|
(p/let [token (helper/get-github-token repo-url)]
|
|
(when token
|
|
(util/p-handle
|
|
(do
|
|
(state/set-cloning! true)
|
|
(git/clone repo-url token))
|
|
(fn [result]
|
|
(state/set-current-repo! repo-url)
|
|
(db/start-db-conn! (state/get-me) repo-url)
|
|
(db/mark-repo-as-cloned! repo-url))
|
|
(fn [e]
|
|
(println "Clone failed, error: ")
|
|
(js/console.error e)
|
|
(state/set-cloning! false)
|
|
(git-handler/set-git-status! repo-url :clone-failed)
|
|
(git-handler/set-git-error! repo-url e)
|
|
(show-install-error! repo-url (util/format "Failed to clone %s." repo-url)))))))
|
|
|
|
(defn set-config-content!
|
|
[repo path new-config]
|
|
(let [new-content (util/pp-str new-config)]
|
|
(file-handler/alter-file repo path new-content {:reset? false
|
|
:re-render-root? false})))
|
|
|
|
(defn set-config!
|
|
[k v]
|
|
(when-let [repo (state/get-current-repo)]
|
|
(let [path (str config/app-name "/" config/config-file)]
|
|
(when-let [config (db/get-file-no-sub path)]
|
|
(let [config (try
|
|
(reader/read-string config)
|
|
(catch js/Error e
|
|
(println "Parsing config file failed: ")
|
|
(js/console.dir e)
|
|
{}))
|
|
ks (if (vector? k) k [k])
|
|
new-config (assoc-in config ks v)]
|
|
(state/set-config! repo new-config)
|
|
(set-config-content! repo path new-config))))))
|
|
|
|
(defn remove-repo!
|
|
[{:keys [id url] :as repo}]
|
|
(spec/validate :repos/repo repo)
|
|
(let [delete-db-f (fn []
|
|
(db/remove-conn! url)
|
|
(db/remove-db! url)
|
|
(db/remove-files-db! url)
|
|
(fs/rmdir (util/get-repo-dir url))
|
|
(state/delete-repo! repo))]
|
|
(if (config/local-db? url)
|
|
(do
|
|
(delete-db-f)
|
|
;; clear handles
|
|
)
|
|
(util/delete (str config/api "repos/" id)
|
|
delete-db-f
|
|
(fn [error]
|
|
(prn "Delete repo failed, error: " error))))))
|
|
|
|
(defn start-repo-db-if-not-exists!
|
|
[repo option]
|
|
(state/set-current-repo! repo)
|
|
(db/start-db-conn! nil repo option))
|
|
|
|
(defn setup-local-repo-if-not-exists!
|
|
[]
|
|
(if js/window.pfs
|
|
(let [repo config/local-repo]
|
|
(p/do! (fs/mkdir-if-not-exists (str "/" repo))
|
|
(state/set-current-repo! repo)
|
|
(db/start-db-conn! nil repo)
|
|
(when-not config/publishing?
|
|
(let [dummy-notes (get-in dicts/dicts [:en :tutorial/dummy-notes])]
|
|
(create-dummy-notes-page repo dummy-notes)))
|
|
(when-not config/publishing?
|
|
(let [tutorial (get-in dicts/dicts [:en :tutorial/text])
|
|
tutorial (string/replace-first tutorial "$today" (date/today))]
|
|
(create-today-journal-if-not-exists repo tutorial)))
|
|
(create-config-file-if-not-exists repo)
|
|
(create-contents-file repo)
|
|
(create-custom-theme repo)
|
|
(state/set-db-restoring! false)))
|
|
(js/setTimeout setup-local-repo-if-not-exists! 100)))
|
|
|
|
(defn periodically-pull-current-repo
|
|
[]
|
|
(js/setInterval
|
|
(fn []
|
|
(p/let [repo-url (state/get-current-repo)
|
|
token (helper/get-github-token repo-url)]
|
|
(when token
|
|
(pull repo-url nil))))
|
|
(* (config/git-pull-secs) 1000)))
|
|
|
|
(defn periodically-push-current-repo
|
|
[]
|
|
(js/setInterval #(push-if-auto-enabled! (state/get-current-repo))
|
|
(* (config/git-push-secs) 1000)))
|
|
|
|
(defn create-repo!
|
|
[repo-url branch]
|
|
(spec/validate :repos/url repo-url)
|
|
(spec/validate :repos/branch branch)
|
|
(util/post (str config/api "repos")
|
|
{:url repo-url
|
|
:branch branch}
|
|
(fn [result]
|
|
(if (:installation_id result)
|
|
(set! (.-href js/window.location) config/website)
|
|
(set! (.-href js/window.location) (str "https://github.com/apps/" config/github-app-name "/installations/new"))))
|
|
(fn [error]
|
|
(println "Something wrong!")
|
|
(js/console.dir error))))
|
|
|
|
(defn- clone-and-load-db
|
|
[repo-url]
|
|
(spec/validate :repos/url repo-url)
|
|
(->
|
|
(p/let [_ (clone repo-url)
|
|
_ (git-handler/git-set-username-email! repo-url (state/get-me))]
|
|
(load-db-and-journals! repo-url nil true))
|
|
(p/catch (fn [error]
|
|
(js/console.error error)))))
|
|
|
|
(defn clone-and-pull-repos
|
|
[me]
|
|
(spec/validate :state/me me)
|
|
(if (and js/window.git js/window.pfs)
|
|
(do
|
|
(doseq [{:keys [id url]} (:repos me)]
|
|
(let [repo url]
|
|
(p/let [config-exists? (fs/file-exists?
|
|
(util/get-repo-dir url)
|
|
".git/config")]
|
|
(if (and config-exists?
|
|
(db/cloned? repo))
|
|
(do
|
|
(git-handler/git-set-username-email! repo me)
|
|
(pull repo nil))
|
|
(do
|
|
(clone-and-load-db repo))))))
|
|
|
|
(periodically-pull-current-repo)
|
|
(periodically-push-current-repo))
|
|
(js/setTimeout (fn []
|
|
(clone-and-pull-repos me))
|
|
500)))
|
|
|
|
(defn rebuild-index!
|
|
[{:keys [id url] :as repo}]
|
|
(spec/validate :repos/repo repo)
|
|
(db/remove-conn! url)
|
|
(db/clear-query-state!)
|
|
(-> (p/do! (db/remove-db! url)
|
|
(db/remove-files-db! url)
|
|
(fs/rmdir (util/get-repo-dir url))
|
|
(clone-and-load-db url))
|
|
(p/catch (fn [error]
|
|
(prn "Delete repo failed, error: " error)))))
|
|
|
|
(defn git-commit-and-push!
|
|
[commit-message]
|
|
(when-let [repo (state/get-current-repo)]
|
|
(push repo {:commit-message commit-message
|
|
:custom-commit? true})))
|