🐛 Merge token activation through develop's tokens status

develop moved token activation out of the tokens lib into the document's
:tokens-status (penpot/penpot#10293): ctob/get-active-theme-paths is
gone, :set-active-token-themes is no longer a change type, and a set is
activated by id in the status rather than through the hidden theme. The
branch merge still read active theme paths off the lib, which on the JVM
is a compile error in app.common.files.branch-merge and therefore stops
every backend namespace that requires it from loading.

The merge now reads both halves of activation off :tokens-status, as ids,
and keeps the two conflict kinds and resolution keys the UI already
offers (:active-themes, :active-sets). compute-changes settles each half
by the rule it used before and emits one :set-tokens-status, because the
change replaces the whole status. The hidden theme stays out of the
theme diff, as before.

The two activation tests now build the status with develop's own
cfo/activate-set and cfo/activate-theme, and a third pins that a
conflicting activation keeps main's status until a resolution takes the
branch's.

common-tests.files-branch-merge-test: 84 tests, 331 assertions, 0
failures.

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Álvaro Tejero Cantero 2026-10-04 00:55:19 +02:00
parent b0e01ef2ef
commit 6605968633
No known key found for this signature in database
2 changed files with 104 additions and 74 deletions

View File

@ -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))})))

View File

@ -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 ---