diff.cljs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. (ns frontend.components.diff
  2. (:require [rum.core :as rum]
  3. [frontend.util :as util]
  4. [frontend.config :as config]
  5. [frontend.handler.git :as git-handler]
  6. [frontend.handler.file :as file]
  7. [frontend.handler.notification :as notification]
  8. [frontend.handler.common :as common-handler]
  9. [frontend.state :as state]
  10. [clojure.string :as string]
  11. [frontend.db :as db]
  12. [frontend.components.svg :as svg]
  13. [frontend.ui :as ui]
  14. [frontend.git :as git]
  15. [goog.object :as gobj]
  16. [promesa.core :as p]
  17. [frontend.github :as github]
  18. [frontend.diff :as diff]
  19. [medley.core :as medley]
  20. [frontend.encrypt :as encrypt]))
  21. (defonce remote-hash-id (atom nil))
  22. (defonce diff-state (atom {}))
  23. (defonce commit-message (atom ""))
  24. ;; TODO: use db :git/status
  25. (defonce *pushing? (atom nil))
  26. (defonce *edit? (atom false))
  27. (defonce *edit-content (atom ""))
  28. (defn- toggle-collapse?
  29. [path]
  30. (swap! diff-state update-in [path :collapse?] not))
  31. (defn- mark-as-resolved
  32. [path]
  33. (swap! diff-state assoc-in [path :resolved?] true)
  34. (swap! diff-state assoc-in [path :collapse?] true))
  35. (rum/defc diff-cp
  36. [diff]
  37. [:div
  38. (for [[idx {:keys [added removed value]}] diff]
  39. (let [bg-color (cond
  40. added "#057a55"
  41. removed "#d61f69"
  42. :else
  43. "initial")]
  44. [:span.diff {:key idx
  45. :style {:background-color bg-color}}
  46. value]))])
  47. (rum/defcs file < rum/reactive
  48. {:will-mount (fn [state]
  49. (let [*local-content (atom "")
  50. [repo _ path & _others] (:rum/args state)]
  51. (p/let [content (file/load-file repo path )]
  52. (reset! *local-content content))
  53. (assoc state ::local-content *local-content)))}
  54. [state repo type path contents remote-oid]
  55. (let [local-content (rum/react (get state ::local-content))
  56. {:keys [collapse? resolved?]} (util/react (rum/cursor diff-state path))
  57. edit? (util/react *edit?)
  58. delete? (= type "remove")]
  59. [:div.cp__diff-file
  60. [:div.cp__diff-file-header
  61. [:a.mr-2 {:on-click (fn [] (toggle-collapse? path))}
  62. (if collapse?
  63. (svg/arrow-right-2)
  64. (svg/arrow-down))]
  65. [:span.cp__diff-file-header-content {:style {:word-break "break-word"}}
  66. path]
  67. (when resolved?
  68. [:span.text-green-600
  69. {:dangerouslySetInnerHTML
  70. {:__html "&#10003;"}}])]
  71. (let [content (get contents path)]
  72. (if (or (and delete? (nil? content))
  73. content)
  74. (if (not= content local-content)
  75. (let [local-content (or local-content "")
  76. content (or content "")
  77. diff (medley/indexed (diff/diff local-content content))
  78. diff? (some (fn [[_idx {:keys [added removed]}]]
  79. (or added removed))
  80. diff)]
  81. [:div.pre-line-white-space.p-2 {:class (if collapse? "hidden")
  82. :style {:overflow "auto"}}
  83. (if edit?
  84. [:div.grid.grid-cols-2.gap-1
  85. (diff-cp diff)
  86. (ui/textarea
  87. {:default-value local-content
  88. :on-change (fn [e]
  89. (reset! *edit-content (util/evalue e)))})]
  90. (diff-cp diff))
  91. (cond
  92. edit?
  93. [:div.mt-2
  94. (ui/button "Save"
  95. :on-click
  96. (fn []
  97. (reset! *edit? false)
  98. (let [new-content @*edit-content]
  99. (file/alter-file repo path new-content
  100. {:commit? false
  101. :re-render-root? true})
  102. (swap! state/state
  103. assoc-in [:github/contents repo remote-oid path] new-content)
  104. (mark-as-resolved path))))]
  105. diff?
  106. [:div.mt-2
  107. (ui/button "Use remote"
  108. :on-click
  109. (fn []
  110. ;; overwrite the file
  111. (if delete?
  112. (file/remove-file! repo path)
  113. (file/alter-file repo path content
  114. {:commit? false
  115. :re-render-root? true}))
  116. (mark-as-resolved path))
  117. :background "green")
  118. [:span.pl-2.pr-2 "or"]
  119. (ui/button "Keep local"
  120. :on-click
  121. (fn []
  122. ;; overwrite the file
  123. (swap! state/state
  124. assoc-in [:github/contents repo remote-oid path] local-content)
  125. (mark-as-resolved path))
  126. :background "pink")
  127. [:span.pl-2.pr-2 "or"]
  128. (ui/button "Edit"
  129. :on-click
  130. (fn []
  131. (reset! *edit? true)))]
  132. :else
  133. nil)]))
  134. [:div "loading..."]))]))
  135. ;; TODO: `n` shortcut for next diff, `p` for previous diff
  136. (rum/defc diff <
  137. rum/reactive
  138. {:will-mount
  139. (fn [state]
  140. (when-let [repo (state/get-current-repo)]
  141. (p/let [remote-latest-commit (common-handler/get-remote-ref repo)
  142. local-latest-commit (common-handler/get-ref repo)
  143. result (git/get-diffs repo local-latest-commit remote-latest-commit)
  144. token (common-handler/get-github-token repo)]
  145. (reset! state/diffs result)
  146. (reset! remote-hash-id remote-latest-commit)
  147. (doseq [{:keys [type path]} result]
  148. (when (contains? #{"add" "modify"}
  149. type)
  150. (github/get-content
  151. token
  152. repo
  153. path
  154. remote-latest-commit
  155. (fn [{:keys [repo-url path ref content]}]
  156. (p/let [content (encrypt/decrypt content)]
  157. (swap! state/state
  158. assoc-in [:github/contents repo-url remote-latest-commit path] content)))
  159. (fn [response]
  160. (when (= (gobj/get response "status") 401)
  161. (notification/show!
  162. [:span.mr-2
  163. (util/format
  164. "Please make sure that you've installed the logseq app for the repo %s on GitHub. "
  165. repo)
  166. (ui/button
  167. "Install Logseq on GitHub"
  168. :href (str "https://github.com/apps/" config/github-app-name "/installations/new"))]
  169. :error
  170. false))))))))
  171. state)
  172. :will-unmount
  173. (fn [state]
  174. (reset! state/diffs nil)
  175. (reset! remote-hash-id nil)
  176. (reset! diff-state {})
  177. (reset! commit-message "")
  178. (reset! *pushing? nil)
  179. (reset! *edit? false)
  180. (reset! *edit-content "")
  181. state)}
  182. []
  183. (let [diffs (util/react state/diffs)
  184. remote-oid (util/react remote-hash-id)
  185. repo (state/get-current-repo)
  186. contents (if remote-oid (state/sub [:github/contents repo remote-oid]))
  187. pushing? (util/react *pushing?)]
  188. [:div#diffs {:style {:margin-bottom 200}}
  189. [:h1.title "Diff"]
  190. (cond
  191. (false? pushing?)
  192. [:div "No diffs"]
  193. (seq diffs)
  194. [:div#diffs-body
  195. (for [{:keys [type path]} diffs]
  196. (rum/with-key (file repo type path contents remote-oid)
  197. path))
  198. [:div
  199. (ui/textarea
  200. {:placeholder "Commit message (optional)"
  201. :on-change (fn [e]
  202. (reset! commit-message (util/evalue e)))})
  203. (if pushing?
  204. [:span (ui/loading "Pushing")]
  205. (ui/button "Commit and push"
  206. :on-click
  207. (fn []
  208. (let [commit-message (if (string/blank? @commit-message)
  209. "Merge"
  210. @commit-message)]
  211. (reset! *pushing? true)
  212. (git-handler/commit-and-force-push! commit-message *pushing?)))))]]
  213. :else
  214. [:div "No diffs"])]))
  215. (rum/defcs local-file < rum/reactive
  216. [state repo path disk-content local-content]
  217. (let [content disk-content
  218. edit? (util/react *edit?)]
  219. [:div.cp__diff-file
  220. [:div.cp__diff-file-header
  221. [:span.cp__diff-file-header-content {:style {:word-break "break-word"}}
  222. path]]
  223. (when (not= content local-content)
  224. (let [local-content (or local-content "")
  225. content (or content "")
  226. diff (medley/indexed (diff/diff local-content content))
  227. diff? (some (fn [[_idx {:keys [added removed]}]]
  228. (or added removed))
  229. diff)
  230. diff-cp [:div.overflow-y-scroll
  231. [:div {:style {:max-height "65vh"}}
  232. (diff-cp diff)]]]
  233. [:div.pre-line-white-space.p-2.overflow-y-hidden
  234. (if edit?
  235. [:div.grid.grid-cols-2.gap-1
  236. diff-cp
  237. (ui/textarea
  238. {:default-value local-content
  239. :auto-focus true
  240. :style {:border "1px solid"}
  241. :on-change (fn [e]
  242. (reset! *edit-content (util/evalue e)))})]
  243. diff-cp)
  244. (cond
  245. edit?
  246. [:div.mt-2
  247. (ui/button "Save"
  248. :on-click
  249. (fn []
  250. (reset! *edit? false)
  251. (let [new-content @*edit-content]
  252. (file/alter-file repo path new-content
  253. {:re-render-root? true
  254. :skip-compare? true})
  255. (state/close-modal!))))]
  256. diff?
  257. [:div.mt-2
  258. (ui/button "Use latest changes from the disk"
  259. :on-click
  260. (fn []
  261. (file/alter-file repo path content
  262. {:re-render-root? true
  263. :skip-compare? true})
  264. (state/close-modal!))
  265. :background "green")
  266. [:span.pl-2.pr-2 "or"]
  267. (ui/button "Keep local changes in Logseq"
  268. :on-click
  269. (fn []
  270. (file/alter-file repo path local-content
  271. {:re-render-root? true
  272. :skip-compare? true})
  273. (state/close-modal!))
  274. :background "pink")
  275. [:span.pl-2.pr-2 "or"]
  276. (ui/button "Edit"
  277. :on-click
  278. (fn []
  279. (reset! *edit? true)))]
  280. :else
  281. nil)]))]))