diff --git a/common/src/app/common/files/branch_merge.cljc b/common/src/app/common/files/branch_merge.cljc index cadfc8ec3b..f5aa633ef0 100644 --- a/common/src/app/common/files/branch_merge.cljc +++ b/common/src/app/common/files/branch_merge.cljc @@ -25,6 +25,7 @@ [app.common.types.component :as ctk] [app.common.types.pages-list :as ctpl] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [clojure.set :as set] [clojure.string :as str])) @@ -845,8 +846,10 @@ ;; existing set) carry kind :token -> :set-token. Structural changes ;; carry their own kinds, each translated by `compute-changes`: set ;; add/delete :token-set, rename/description :token-set-rename, set order -;; :token-set-order, themes :token-theme, active themes -;; :token-active-themes and active-set toggles :token-active-sets. +;; :token-set-order, themes :token-theme, and activation, which the +;; document keeps in `:tokens-status` rather than in the lib: active themes +;; :token-active-themes and active sets :token-active-sets, both written +;; back as one :set-tokens-status. (defn- lib-set-ids [lib] @@ -947,20 +950,19 @@ (ctob/get-themes lib)) {})) -(defn- lib-active-paths - "Active theme paths (mergeable via :set-active-token-themes)." - [lib] - (if lib (set (ctob/get-active-theme-paths lib)) #{})) +(defn- active-theme-ids + "The active theme ids of `data`'s `:tokens-status`." + [data] + (if-let [status (:tokens-status data)] + (ctos/get-active-theme-ids status) + #{})) -(defn- lib-hidden-sets - "The hidden theme's active sets (active-set toggles)." - [lib] - (some-> lib (ctob/get-theme ctob/hidden-theme-id) :sets set)) - -(defn- hidden-theme-map - "The hidden theme as a plain map (for :set-token-theme)." - [lib] - (some->> (when lib (ctob/get-theme lib ctob/hidden-theme-id)) (into {}))) +(defn- active-set-ids + "The active set ids of `data`'s `:tokens-status`." + [data] + (if-let [status (:tokens-status data)] + (ctos/get-active-set-ids status) + #{})) (defn- set-order-by-id "Set ids in their stored order, filtered to `ids`." @@ -999,15 +1001,15 @@ ;; themes, excluding hidden (mergeable: :token-theme) themes (three-way-entities (lib-themes bl) (lib-themes tl) (lib-themes ol) {:kind :token-theme}) - ;; active theme paths (mergeable: :token-active-themes) - active-paths (three-way-entities {:active-themes (lib-active-paths bl)} - {:active-themes (lib-active-paths tl)} - {:active-themes (lib-active-paths ol)} - {:kind :token-active-themes}) - ;; active-set toggles (hidden theme) (mergeable: :token-active-sets) - active-sets (three-way-entities {:active-sets (lib-hidden-sets bl)} - {:active-sets (lib-hidden-sets tl)} - {:active-sets (lib-hidden-sets ol)} + ;; activation, read off each document's tokens status (mergeable: + ;; :token-active-themes and :token-active-sets) + active-themes (three-way-entities {:active-themes (active-theme-ids base)} + {:active-themes (active-theme-ids theirs)} + {:active-themes (active-theme-ids ours)} + {:kind :token-active-themes}) + active-sets (three-way-entities {:active-sets (active-set-ids base)} + {:active-sets (active-set-ids theirs)} + {:active-sets (active-set-ids ours)} {:kind :token-active-sets}) ;; per-token values (mergeable: :token). Sets deleted on either side ;; relative to the base are decided wholesale at set level (delete or @@ -1024,7 +1026,7 @@ (lib-tokens-by-id ol sid) {:kind :token :set-id sid})) (remove deleted-on-a-side? set-ids))] - (merge-results (concat [presence rename order themes active-paths active-sets] tokens)))) + (merge-results (concat [presence rename order themes active-themes active-sets] tokens)))) (defn remap-refs "Rewrite cross-file references in `data` through `id-map` @@ -1951,28 +1953,24 @@ (map (fn [sid] {:type :set-token-set :id sid :attrs (set-rename-attrs ml ol sid)}))) common-sets) - ;; active theme paths + ;; activation: each half settles the way `diff-tokens` classified + ;; it, and one :set-tokens-status carries both, because the change + ;; replaces the whole status + settle (fn [b m o k] + (cond + (= o b) m + (= m b) o + (= o m) m + (= (get resolutions k) :branch) o + :else m)) + main-theme-ids (active-theme-ids main) + main-set-ids (active-set-ids main) + theme-ids (settle (active-theme-ids base) main-theme-ids (active-theme-ids branch) :active-themes) + active-ids (settle (active-set-ids base) main-set-ids (active-set-ids branch) :active-sets) active-changes - (let [bp (lib-active-paths bl) mp (lib-active-paths ml) op (lib-active-paths ol)] - (cond - (= op bp) [] - (= mp bp) [{:type :set-active-token-themes :theme-paths op}] - (= op mp) [] - (= (get resolutions :active-themes) :branch) [{:type :set-active-token-themes :theme-paths op}] - :else [])) - - ;; active sets (hidden theme): bring branch's hidden theme - active-sets-changes - (let [bs (lib-hidden-sets bl) ms (lib-hidden-sets ml) os (lib-hidden-sets ol) - take! (fn [] (if-let [h (hidden-theme-map ol)] - [{:type :set-token-theme :id ctob/hidden-theme-id :attrs h}] - []))] - (cond - (= os bs) [] - (= ms bs) (take!) - (= os ms) [] - (= (get resolutions :active-sets) :branch) (take!) - :else [])) + (if (and (= theme-ids main-theme-ids) (= active-ids main-set-ids)) + [] + [{:type :set-tokens-status :theme-ids theme-ids :set-ids active-ids}]) ;; set order: reorder main's common sets to branch's order using ;; "move nᵢ before nᵢ₊₁" right-to-left. Emitted after renames so set @@ -2031,4 +2029,4 @@ colors typos media set-presence set-rename-changes set-order-changes token-vals themes - active-changes active-sets-changes))}))) + active-changes))}))) diff --git a/common/test/common_tests/files_branch_merge_test.cljc b/common/test/common_tests/files_branch_merge_test.cljc index 8949139fe8..6c113579c8 100644 --- a/common/test/common_tests/files_branch_merge_test.cljc +++ b/common/test/common_tests/files_branch_merge_test.cljc @@ -8,10 +8,12 @@ (:require [app.common.files.branch-merge :as bm] [app.common.files.changes :as cfc] + [app.common.files.tokens :as cfo] [app.common.types.page :as ctp] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [clojure.test :as t])) @@ -472,6 +474,11 @@ [lib] (assoc (mkdata {}) :tokens-lib lib)) +(defn- with-status + "`with-tokens`, carrying the tokens status `f` builds from an empty one." + [lib f] + (assoc (with-tokens lib) :tokens-status (f (ctos/make-tokens-status)))) + (t/deftest compute-changes-token-value-roundtrip (let [sid (uuid/next) tid (uuid/next) @@ -751,37 +758,62 @@ (t/is (= ["c" "a" "b"] order))))) (t/deftest compute-changes-token-active-sets - (let [sid (uuid/next) - tid (uuid/next) - base-lib (token-lib sid tid "#ff0000") - ;; branch toggles the "core" set active in the hidden theme - branch-lib (ctob/toggle-set-in-theme base-lib ctob/hidden-theme-id "core") - {:keys [changes unsupported]} (bm/compute-changes (with-tokens base-lib) - (with-tokens base-lib) - (with-tokens branch-lib)) - ch (first (filter #(= :set-token-theme (:type %)) changes)) - hidden-sets (fn [lib] (set (:sets (ctob/get-theme lib ctob/hidden-theme-id))))] + (let [sid (uuid/next) + tid (uuid/next) + lib (token-lib sid tid "#ff0000") + base (with-status lib identity) + ;; branch activates the "core" set directly + branch (with-status lib #(cfo/activate-set % lib sid)) + {:keys [changes unsupported]} (bm/compute-changes base base branch) + ch (first (filter #(= :set-tokens-status (:type %)) changes))] (t/is (empty? unsupported)) - (t/is (= ctob/hidden-theme-id (:id ch))) - ;; round-trip: main's hidden theme active sets match the branch - (let [data' (cfc/process-changes {:tokens-lib base-lib} changes)] - (t/is (= (hidden-sets branch-lib) (hidden-sets (:tokens-lib data'))))))) + (t/is (= #{sid} (:set-ids ch))) + ;; round-trip: main's active sets match the branch + (let [data' (cfc/process-changes base changes)] + (t/is (= #{sid} (ctos/get-active-set-ids (:tokens-status data'))))))) (t/deftest compute-changes-token-active-themes - (let [thid (uuid/next) - base-lib (-> (ctob/make-tokens-lib) - (ctob/add-theme (ctob/make-token-theme {:id thid :name "Dark" :group ""}))) - branch-lib (ctob/activate-theme base-lib thid) - {:keys [changes unsupported]} (bm/compute-changes (with-tokens base-lib) - (with-tokens base-lib) - (with-tokens branch-lib)) - ch (first (filter #(= :set-active-token-themes (:type %)) changes))] + (let [sid (uuid/next) + tid (uuid/next) + thid (uuid/next) + lib (-> (token-lib sid tid "#ff0000") + (ctob/add-theme (ctob/make-token-theme {:id thid :name "Dark" :group "" :sets #{"core"}}))) + base (with-status lib identity) + branch (with-status lib #(cfo/activate-theme % lib thid)) + {:keys [changes unsupported]} (bm/compute-changes base base branch) + ch (first (filter #(= :set-tokens-status (:type %)) changes))] (t/is (empty? unsupported)) - (t/is (some? ch)) - ;; round-trip: active theme paths match the branch - (let [data' (cfc/process-changes {:tokens-lib base-lib} changes)] - (t/is (= (set (ctob/get-active-theme-paths branch-lib)) - (set (ctob/get-active-theme-paths (:tokens-lib data')))))))) + (t/is (= #{thid} (:theme-ids ch))) + (t/is (= #{sid} (:set-ids ch)) "activating a theme activates its sets") + ;; round-trip: main's tokens status matches the branch + (let [status (:tokens-status (cfc/process-changes base changes))] + (t/is (= #{thid} (ctos/get-active-theme-ids status))) + (t/is (= #{sid} (ctos/get-active-set-ids status)))))) + +(t/deftest token-activation-conflict-settles-by-resolution + ;; Both sides activate a different theme of one group, so both halves of + ;; the status diverge. Main's status stands until a resolution takes the + ;; branch's. + (let [sid-a (uuid/next) + sid-b (uuid/next) + th-a (uuid/next) + th-b (uuid/next) + lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set {:id sid-a :name "a"})) + (ctob/add-set (ctob/make-token-set {:id sid-b :name "b"})) + (ctob/add-theme (ctob/make-token-theme {:id th-a :name "A" :group "" :sets #{"a"}})) + (ctob/add-theme (ctob/make-token-theme {:id th-b :name "B" :group "" :sets #{"b"}}))) + base (with-status lib identity) + main (with-status lib #(cfo/activate-theme % lib th-a)) + branch (with-status lib #(cfo/activate-theme % lib th-b)) + status-changes (fn [resolutions] + (filterv #(= :set-tokens-status (:type %)) + (:changes (bm/compute-changes base main branch resolutions))))] + (t/is (= #{:token-active-themes :token-active-sets} + (set (map :kind (:conflicts (bm/compute-merge base main branch :branch->main)))))) + (t/is (= [] (status-changes {})) "an unresolved conflict keeps main's status") + (t/is (= [{:type :set-tokens-status :theme-ids #{th-b} :set-ids #{sid-b}}] + (status-changes {:active-themes :branch :active-sets :branch}))))) ;;; --- Regression tests: classification on stripped shapes ---