瀏覽代碼

feat: git integration

Tienson Qin 4 年之前
父節點
當前提交
4c3bdb0bf3

+ 3 - 0
externs.js

@@ -115,6 +115,9 @@ dummy.getPageView = function() {};
 dummy.convertToPdfPoint = function() {};
 dummy.scrollPageIntoView = function() {};
 dummy.convertToViewportRectangle = function() {};
+dummy.init = function() {};
+dummy.commit = function() {};
+dummy.raw = function() {};
 
 /**
  * @typedef {{

+ 2 - 1
resources/package.json

@@ -27,7 +27,8 @@
     "node-fetch": "^2.6.1",
     "open": "^7.3.1",
     "semver": "^7.3.5",
-    "update-electron-app": "^2.0.1"
+    "update-electron-app": "^2.0.1",
+    "simple-git": "2.44.0"
   },
   "devDependencies": {
     "@electron-forge/cli": "^6.0.0-beta.57",

+ 4 - 1
src/electron/electron/core.cljs

@@ -11,7 +11,8 @@
             ["electron" :refer [BrowserWindow app protocol ipcMain dialog Menu MenuItem] :as electron]
             ["electron-window-state" :as windowStateKeeper]
             [clojure.core.async :as async]
-            [electron.state :as state]))
+            [electron.state :as state]
+            [electron.git :as git]))
 
 (def MAIN_WINDOW_ENTRY (if dev?
                          "http://localhost:3001"
@@ -224,6 +225,8 @@
 
                (search/open-dbs!)
 
+               (git/auto-commit-current-graph!)
+
                (vreset! *setup-fn
                         (fn []
                           (let [t0 (setup-updater! win)

+ 113 - 0
src/electron/electron/git.cljs

@@ -0,0 +1,113 @@
+(ns electron.git
+  (:require ["child_process" :as child-process]
+            ["simple-git" :as simple-git]
+            [goog.object :as gobj]
+            [electron.state :as state]
+            [electron.utils :as utils]
+            [promesa.core :as p]
+            [clojure.string :as string]))
+
+(def spawn-sync (gobj/get child-process "spawnSync"))
+
+(defonce gits
+  (atom {}))
+
+(defn installed?
+  []
+  (let [command (spawn-sync "git"
+                            #js ["--version"]
+                            #js {:stdio "ignore"})]
+    (if-let [error (gobj/get command "error")]
+      (do
+        (js/console.error error)
+        false)
+      true)))
+
+(defn get-git
+  []
+  (when (installed?)
+    (when-let [path (:graph/current @state/state)]
+      (if-let [result (get @gits path)]
+        result
+        (let [result (simple-git path)]
+          (swap! gits assoc path result)
+          result)))))
+
+(defn init!
+  []
+  (when-let [git ^js (get-git)]
+    (.init git false)))
+
+(defn add-all!
+  ([]
+   (add-all! (get-git)))
+  ([^js git]
+   (when git
+     (.add git "./*" (fn [error] (js/console.error error))))))
+
+(defn add-all-and-commit!
+  ([]
+   (add-all-and-commit! "Auto saved by Logseq"))
+  ([message]
+   (when-let [git ^js (get-git)]
+     (p/let [_ (add-all! git)]
+       (.commit git message)))))
+
+(defonce quotes-regex #"\"[^\"]+\"")
+(defn wrapped-by-quotes?
+  [v]
+  (and (string? v) (>= (count v) 2) (= "\"" (first v) (last v))))
+
+(defn unquote-string
+  [v]
+  (string/trim (subs v 1 (dec (count v)))))
+
+(defn- split-args
+  [s]
+  (let [quotes (re-seq quotes-regex s)
+        non-quotes (string/split s quotes-regex)
+        col (if (seq quotes)
+              (concat (interleave non-quotes quotes)
+                      (drop (count quotes) non-quotes))
+              non-quotes)]
+    (->> col
+         (map (fn [s]
+                (if (wrapped-by-quotes? s)
+                  [(unquote-string s)]
+                  (string/split s #"\s"))))
+         (flatten)
+         (remove string/blank?))))
+
+(defn raw!
+  [args & {:keys [ok-handler error-handler]}]
+  (when-let [git ^js (get-git)]
+    (let [args (if (string? args)
+                 (split-args args)
+                 args)
+          ok-handler (if ok-handler
+                       ok-handler
+                       (fn [result]
+                         (utils/send-to-renderer "notification" {:type "success"
+                                                                 :payload result})))
+          error-handler (if error-handler
+                          error-handler
+                          (fn [error]
+                            (js/console.dir error)
+                            (utils/send-to-renderer "notification" {:type "error"
+                                                                    :payload (.toString error)})))]
+      (p/let [_ (when (= (first args) "commit")
+                  (add-all!))]
+        (->
+         (p/let [result (.raw git (clj->js args))]
+           (when ok-handler
+             (ok-handler result)))
+         (p/catch error-handler))))))
+
+(defn auto-commit-current-graph!
+  []
+  (when (installed?)
+    (state/clear-git-commit-interval!)
+    (p/let [_ (add-all-and-commit!)]
+      (let [seconds (state/get-git-commit-seconds)
+            interval (js/setInterval add-all-and-commit! (* seconds 1000))]
+        (state/set-git-commit-interval! interval)))))

+ 10 - 1
src/electron/electron/handler.cljs

@@ -13,7 +13,8 @@
             [electron.utils :as utils]
             [electron.state :as state]
             [clojure.core.async :as async]
-            [electron.search :as search]))
+            [electron.search :as search]
+            [electron.git :as git]))
 
 (defmulti handle (fn [_window args] (keyword (first args))))
 
@@ -198,6 +199,14 @@
 (defmethod handle :getDirname [_]
   js/__dirname)
 
+(defmethod handle :setCurrentGraph [_ [_ path]]
+  (let [path (when path (string/replace path "logseq_local_" ""))]
+    (swap! state/state assoc :graph/current path)))
+
+(defmethod handle :runGit [_ [_ args]]
+  (when (seq args)
+    (git/raw! args)))
+
 (defmethod handle :default [args]
   (println "Error: no ipc handler for: " (bean/->js args)))
 

+ 31 - 0
src/electron/electron/state.cljs

@@ -2,3 +2,34 @@
   (:require [clojure.core.async :as async]))
 
 (defonce persistent-dbs-chan (async/chan 1))
+
+(defonce state
+  (atom {:graph/current nil
+         :git/auto-commit-seconds 60
+         :git/auto-commit-interval nil}))
+
+(defn set-state!
+  [path value]
+  (if (vector? path)
+    (swap! state assoc-in path value)
+    (swap! state assoc path value)))
+
+(defn set-git-commit-interval!
+  [v]
+  (set-state! :git/auto-commit-interval v))
+
+(defn clear-git-commit-interval!
+  []
+  (when-let [interval (get @state :git/auto-commit-interval)]
+    (js/clearInterval interval)))
+
+(defn set-git-commit-seconds!
+  [v]
+  (let [v (if (and (integer? v) (< 0 v (inc (* 60 10)))) ; max 10 minutes
+            v
+            60)]
+    (set-state! :git/auto-commit-seconds v)))
+
+(defn get-git-commit-seconds
+  []
+  (or (get @state :git/auto-commit-seconds) 60))

+ 13 - 1
src/electron/electron/utils.cljs

@@ -2,7 +2,9 @@
   (:require [clojure.string :as string]
             ["fs" :as fs]
             ["path" :as path]
-            [clojure.string :as string]))
+            [clojure.string :as string]
+            [cljs-bean.core :as bean]
+            ["electron" :refer [BrowserWindow]]))
 
 (defonce mac? (= (.-platform js/process) "darwin"))
 (defonce win32? (= (.-platform js/process) "win32"))
@@ -45,3 +47,13 @@
   [path]
   (when (fs/existsSync path)
     (.toString (fs/readFileSync path))))
+
+(defn get-focused-window
+  []
+  (.getFocusedWindow BrowserWindow))
+
+(defn send-to-renderer
+  [kind payload]
+  (when-let [window (get-focused-window)]
+    (.. ^js window -webContents
+       (send kind (bean/->js payload)))))

+ 22 - 18
src/main/electron/listener.cljs

@@ -11,22 +11,6 @@
             [frontend.handler.metadata :as metadata-handler]
             [frontend.ui :as ui]))
 
-(defn listen-to-open-dir!
-  []
-  (js/window.apis.on "open-dir-confirmed"
-                     (fn []
-                       (state/set-loading-files! true)
-                       (when-not (state/home?)
-                         (route-handler/redirect-to-home!)))))
-
-(defn run-dirs-watcher!
-  []
-  ;; TODO: move "file-watcher" to electron.ipc.channels
-  (js/window.apis.on "file-watcher"
-                     (fn [data]
-                       (let [{:keys [type payload]} (bean/->clj data)]
-                         (watcher-handler/handle-changed! type payload)))))
-
 (defn listen-persistent-dbs!
   []
   ;; TODO: move "file-watcher" to electron.ipc.channels
@@ -54,8 +38,28 @@
             100))
          (ipc/ipc "persistent-dbs-saved"))))))
 
+(defn listen-to-electron!
+  []
+  (js/window.apis.on "open-dir-confirmed"
+                     (fn []
+                       (state/set-loading-files! true)
+                       (when-not (state/home?)
+                         (route-handler/redirect-to-home!))))
+
+  ;; TODO: move "file-watcher" to electron.ipc.channels
+  (js/window.apis.on "file-watcher"
+                     (fn [data]
+                       (let [{:keys [type payload]} (bean/->clj data)]
+                         (watcher-handler/handle-changed! type payload))))
+
+  (js/window.apis.on "notification"
+                     (fn [data]
+                       (let [{:keys [type payload]} (bean/->clj data)
+                             type (keyword type)
+                             comp [:div (str payload)]]
+                         (notification/show! comp type false)))))
+
 (defn listen!
   []
-  (listen-to-open-dir!)
-  (run-dirs-watcher!)
+  (listen-to-electron!)
   (listen-persistent-dbs!))

+ 34 - 0
src/main/frontend/components/shell.cljs

@@ -0,0 +1,34 @@
+(ns frontend.components.shell
+  (:require [rum.core :as rum]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [frontend.handler.shell :as shell-handler]
+            [clojure.string :as string]
+            [frontend.mixins :as mixins]))
+
+(defn- run-command
+  [command]
+  (when-not (string/blank? @command)
+    (shell-handler/run-command! @command)))
+
+(defonce command (atom ""))
+(rum/defcs shell < rum/reactive
+  (mixins/event-mixin
+   (fn [state]
+     (mixins/on-enter state
+                      :on-enter (fn [state]
+                                  (run-command command)))))
+  [state]
+  [:div.flex.flex-col
+   [:div.w-full.mx-auto.sm:max-w-lg.sm:w-96
+    [:div
+     [:div
+      [:h1.title
+       "Input command"]
+      [:div.mt-4.mb-4.relative.rounded-md.shadow-sm.max-w-xs
+       [:input#run-command.form-input.block.w-full.sm:text-sm.sm:leading-5
+        {:autoFocus true
+         :placeholder "git ..."
+         :on-change (fn [e]
+                      (reset! command (util/evalue e)))}]]]]
+    (ui/button "Run" :on-click #(run-command command))]])

+ 8 - 7
src/main/frontend/format/block.cljs

@@ -269,16 +269,17 @@
 (defn convert-page-if-journal
   "Convert journal file name to user' custom date format"
   [original-page-name]
-  (let [page-name (string/lower-case original-page-name)
-        day (date/journal-title->int page-name)]
-    (if day
-      (let [original-page-name (date/int->journal-title day)]
-        [original-page-name (string/lower-case original-page-name) day])
-      [original-page-name page-name day])))
+  (when original-page-name
+    (let [page-name (string/lower-case original-page-name)
+         day (date/journal-title->int page-name)]
+     (if day
+       (let [original-page-name (date/int->journal-title day)]
+         [original-page-name (string/lower-case original-page-name) day])
+       [original-page-name page-name day]))))
 
 (defn page-name->map
   [original-page-name with-id?]
-  (when original-page-name
+  (when (and original-page-name (string? original-page-name))
     (let [original-page-name (util/remove-boundary-slashes original-page-name)
           [original-page-name page-name journal-day] (convert-page-if-journal original-page-name)
           namespace? (and (string/includes? original-page-name "/")

+ 6 - 5
src/main/frontend/format/mldoc.cljs

@@ -152,7 +152,7 @@
                       properties-ast
                       (map (fn [[k v]]
                              (let [k (keyword (string/lower-case k))
-                                   v (if (contains? #{:title :description :filters :roam_tags} k)
+                                   v (if (contains? #{:title :description :filters :roam_tags :macro} k)
                                        v
                                        (text/split-page-refs-without-brackets v))]
                                [k v]))))
@@ -162,10 +162,11 @@
                    (->>
                     (map
                      (fn [[_ v]]
-                       (let [[k v] (util/split-first " " v)]
-                         (mapv
-                          string/trim
-                          [k v])))
+                       (do
+                         (let [[k v] (util/split-first " " v)]
+                          (mapv
+                           string/trim
+                           [k v]))))
                      macro-properties)
                     (into {}))
                    {})

+ 5 - 0
src/main/frontend/handler/events.cljs

@@ -11,6 +11,7 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.page :as page-handler]
             [frontend.components.encryption :as encryption]
+            [frontend.components.shell :as shell]
             [frontend.fs.nfs :as nfs]
             [frontend.db.conn :as conn]
             [frontend.extensions.srs :as srs]
@@ -155,6 +156,10 @@
                false))))
         repos))
 
+(defmethod handle :command/run [_]
+  (when (util/electron?)
+    (state/set-modal! shell/shell)))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 30 - 0
src/main/frontend/handler/shell.cljs

@@ -0,0 +1,30 @@
+(ns frontend.handler.shell
+  (:require [electron.ipc :as ipc]
+            [clojure.string :as string]
+            [frontend.util :as util]
+            [frontend.handler.notification :as notification]))
+
+(defn run-git-command!
+  [command]
+  (ipc/ipc "runGit" command))
+
+(defn run-pandoc-command!
+  [command]
+  (ipc/ipc "runPandoc" command))
+
+(defn run-command!
+  [command]
+  (let [[command args] (util/split-first " " command)
+        command (and command (string/lower-case command))]
+    (when (and (not (string/blank? command)) (not (string/blank? args)))
+      (let [args (string/trim args)]
+        (case (keyword command)
+         :git
+         (run-git-command! args)
+
+         :pandoc
+         (run-pandoc-command! args)
+
+         (notification/show!
+          [:div (str command " is not supported yet!")]
+          :error))))))

+ 5 - 1
src/main/frontend/modules/shortcut/config.cljs

@@ -322,7 +322,11 @@
 
    :shortcut.handler/global-non-editing-only
    ^{:before m/enable-when-not-editing-mode!}
-   {:ui/toggle-document-mode
+   {:command/run
+    {:desc    "Run git/pandoc/others commands"
+     :binding "r"
+     :fn      #(state/pub-event! [:command/run])}
+    :ui/toggle-document-mode
     {:desc    "Toggle document mode"
      :binding "t d"
      :fn      state/toggle-document-mode!}

+ 8 - 4
src/main/frontend/state.cljs

@@ -17,8 +17,11 @@
             [cljs-time.format :as tf]))
 
 (defonce ^:private state
-  (atom
-   (let [document-mode? (or (storage/get :document/mode?) false)]
+  (let [document-mode? (or (storage/get :document/mode?) false)
+        current-graph (let [graph (storage/get :git/current-repo)]
+                        (when graph (ipc/ipc "setCurrentGraph" graph))
+                        graph)]
+    (atom
      {:route-match nil
       :today nil
       :system/events (async/chan 100)
@@ -38,7 +41,7 @@
       :network/online? true
       :indexeddb/support? true
       :me nil
-      :git/current-repo (storage/get :git/current-repo)
+      :git/current-repo current-graph
       :git/status {}
       :format/loading {}
       :draw? false
@@ -391,7 +394,8 @@
   (swap! state assoc :git/current-repo repo)
   (if repo
     (storage/set :git/current-repo repo)
-    (storage/remove :git/current-repo)))
+    (storage/remove :git/current-repo))
+  (ipc/ipc "setCurrentGraph" repo))
 
 (defn set-preferred-format!
   [format]

+ 4 - 0
templates/config.edn

@@ -169,4 +169,8 @@
  ;; hide specific properties for blocks
  ;; E.g. #{:created-at :updated-at}
  ;; :block-hidden-properties #{}
+
+ ;; only for the desktop app
+ :git/auto-commit-seconds 60
+ :git/disable-auto-commit? false
  }