✨ Tell a library branch that its consumers read main (#12570)

Files that use a shared library read its main, never a branch, so a
branch of a library is not isolated in the sense the create dialog
promised. On a shared library the create dialog now says that files
using the library can adopt changes from main only, the chip of a
library branch says the same, and the note beside main reads
"Default branch (read by library consumers)". Other files keep their
copy.

get-file-branch-info returns source-is-shared so a branch knows its
main is a library.

AI-assisted-by: claude-opus-5-5

Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
This commit is contained in:
Álvaro Tejero-Cantero 2026-10-22 20:04:23 +02:00 committed by GitHub
parent d613972474
commit d4f999e6dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 214 additions and 6 deletions

View File

@ -1733,7 +1733,8 @@
fb.base_snapshot_id,
bf.revn AS branch_revn,
sf.revn AS source_revn,
sf.name AS source_name
sf.name AS source_name,
sf.is_shared AS source_is_shared
FROM file_branch AS fb
JOIN file AS bf ON (bf.id = fb.branch_file_id)
JOIN file AS sf ON (sf.id = fb.source_file_id)

View File

@ -0,0 +1,58 @@
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "New File 1",
"~:revn": 11,
"~:modified-at": "~m1713873823633",
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:is-shared": true,
"~:version": 46,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1713536343369",
"~:data": {
"~:pages": [
"~u66697432-c33d-8055-8006-2c62cc084cad"
],
"~:pages-index": {
"~u66697432-c33d-8055-8006-2c62cc084cad": {
"~#penpot/pointer": [
"~ude58c8f6-c5c2-8196-8004-3df9e2e52d88",
{
"~:created-at": "~m1713873823636"
}
]
}
},
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:options": {
"~:components-v2": true
},
"~:recent-colors": [
{
"~:color": "#0000ff",
"~:opacity": 1,
"~:id": null,
"~:file-id": null,
"~:image": null
}
]
}
}

View File

@ -0,0 +1,16 @@
{
"~:id": "~uc7ce0794-0992-8105-8004-38f280443998",
"~:branch-file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:source-file-id": "~uc7ce0794-0992-8105-8004-38f280443997",
"~:name": "checkout",
"~:description": null,
"~:status": "open",
"~:created-by": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:created-at": "~m1713536343369",
"~:updated-at": "~m1713873823633",
"~:source-name": "New File 1",
"~:ahead": 1,
"~:behind": 0,
"~:conflicts": 0,
"~:source-is-shared": true
}

View File

@ -0,0 +1,17 @@
[
{
"~:id": "~uc7ce0794-0992-8105-8004-38f280443998",
"~:branch-file-id": "~uc7ce0794-0992-8105-8004-38f280443850",
"~:source-file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:name": "checkout",
"~:description": null,
"~:status": "open",
"~:created-by": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:created-at": "~m1713536343369",
"~:updated-at": "~m1713873823633",
"~:source-name": "New File 1",
"~:ahead": 1,
"~:behind": 0,
"~:conflicts": 0
}
]

View File

@ -0,0 +1,80 @@
import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../pages/WorkspacePage";
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
await WorkspacePage.mockConfigFlags(page, ["enable-branching"]);
});
const setupBranching = async (workspacePage, rpcs) => {
await workspacePage.setupEmptyFile();
await workspacePage.mockRPCs({
"get-file-branch-info?file-id=*":
"pull-requests/get-file-branch-info-none.json",
"get-file-branches?file-id=*": "pull-requests/get-file-branches-empty.json",
"get-file-pull-requests?file-id=*":
"pull-requests/get-file-branches-empty.json",
...rpcs,
});
};
test("on a branch of a library, the header says consumers read main", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await setupBranching(workspacePage, {
"get-file-branch-info?file-id=*":
"pull-requests/get-file-branch-info-library-branch.json",
});
await workspacePage.goToWorkspace();
const chip = page.getByTitle(
"On branch checkout from main. Library consumers can adopt changes from main only.",
);
await expect(chip).toBeVisible();
await chip.click();
await expect(
page.getByText("Default branch (read by library consumers)"),
).toBeVisible();
});
test("on a library, the create dialog says consumers read main", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await setupBranching(workspacePage, {
"get-file-branches?file-id=*": "pull-requests/get-file-branches-one.json",
});
await workspacePage.mockGetFile("pull-requests/get-file-blank-shared.json");
await workspacePage.goToWorkspace();
await expect(
page.getByText("Default branch (read by library consumers)"),
).toBeVisible();
await page.getByTitle("Switch branch").click();
await page.getByText("New branch").click();
await expect(
page.getByText(
"files that use this library can adopt changes from main only",
),
).toBeVisible();
});
test("on an ordinary file, the create dialog keeps its note", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await setupBranching(workspacePage, {
"get-file-branches?file-id=*": "pull-requests/get-file-branches-one.json",
});
await workspacePage.goToWorkspace();
await expect(page.getByText("Default branch", { exact: true })).toBeVisible();
await page.getByTitle("Switch branch").click();
await page.getByText("New branch").click();
await expect(page.getByText("isolated copy")).toBeVisible();
await expect(page.getByText("files that use this library")).toBeHidden();
});

View File

@ -81,6 +81,14 @@
(zero? (or (:behind row) 0))
(zero? (or (:conflicts row) 0))))
(defn- main-subtitle
"The note beside main. Files that use a shared library read its main,
so on a library it says so."
[library?]
(if library?
(tr "workspace.branches.main-subtitle-library")
(tr "workspace.branches.main-subtitle")))
;; --- Popover
(mf/defc switcher-row*
@ -92,7 +100,7 @@
:data-branch (dm/str (:branch-file-id row))
:on-click on-open}
(if (:is-main row)
(tr "workspace.branches.main-subtitle")
(main-subtitle (:is-library row))
[:> row-status* {:row row :pr pr}])]])
(mf/defc switcher-popover*
@ -115,6 +123,7 @@
(when-not (:is-main current)
{:name (tr "workspace.branches.main")
:is-main true
:is-library (boolean (:source-is-shared current))
:branch-file-id (:source-file-id current)})
rows
@ -234,6 +243,7 @@
(or ctx
{:name (tr "workspace.branches.main")
:is-main true
:is-library (boolean (:is-shared file))
:created-by (:id profile)
:branch-file-id file-id})
@ -275,8 +285,16 @@
[:div {:class (stl/css :branch-bar) :ref bar-ref}
[:> branch-chip* {:label (:name current)
:is-open open?
:title (if (:is-main current)
:title (cond
(:is-main current)
(tr "workspace.branches.switcher.tooltip")
;; files that use the library read main,
;; never this branch
(:source-is-shared current)
(tr "workspace.branches.header-badge-tooltip-library" (:name current))
:else
(tr "workspace.branches.header-badge-tooltip" (:name current)))
:on-click on-toggle}]
@ -288,7 +306,7 @@
[:> text* {:as "span"
:typography t/body-small
:class (stl/css :status-note)}
(tr "workspace.branches.main-subtitle")]
(main-subtitle (:is-shared file))]
^boolean merged?
nil

View File

@ -224,7 +224,9 @@
{::mf/register modal/components
::mf/register-as :create-branch}
[{:keys [file-name file-id]}]
(let [branch-name* (mf/use-state "")
(let [;; a branch only grows from main, so the open file is main
library? (boolean (:is-shared (mf/deref refs/file)))
branch-name* (mf/use-state "")
description* (mf/use-state "")
branch-name (deref branch-name*)
description (deref description*)
@ -279,7 +281,9 @@
(tr "workspace.branches.create.derive-from-latest")]]]
[:> context-notification* {:level :info :type :context}
(tr "workspace.branches.create.info")]]
(if library?
(tr "workspace.branches.create.info-library")
(tr "workspace.branches.create.info"))]]
[:> modal-footer* {}
[:> button* {:variant "secondary" :on-click on-close}

View File

@ -6808,6 +6808,12 @@ msgstr ""
"Your branch is a complete, isolated copy. Nothing you do affects main until "
"you merge it."
#: src/app/main/ui/workspace/sidebar/branches.cljs:283
msgid "workspace.branches.create.info-library"
msgstr ""
"Your branch is a complete copy. Nothing you do affects main until you merge "
"it, and files that use this library can adopt changes from main only."
#: src/app/main/ui/workspace/sidebar/branches.cljs:283
msgid "workspace.branches.create.name-label"
msgstr "Branch name"
@ -6849,6 +6855,10 @@ msgstr "No branches yet"
msgid "workspace.branches.header-badge-tooltip"
msgstr "On branch %s from main"
#: src/app/main/ui/workspace/branch_switcher.cljs:295
msgid "workspace.branches.header-badge-tooltip-library"
msgstr "On branch %s from main. Library consumers can adopt changes from main only."
#: src/app/main/ui/workspace/sidebar/branches.cljs:485
msgid "workspace.branches.info.author"
msgstr "Created by"
@ -7070,6 +7080,10 @@ msgstr "Main has new changes. You can update this branch"
msgid "workspace.branches.main-subtitle"
msgstr "Default branch"
#: src/app/main/ui/workspace/branch_switcher.cljs:88
msgid "workspace.branches.main-subtitle-library"
msgstr "Default branch (read by library consumers)"
#: src/app/main/ui/workspace/sidebar/branches.cljs:779
msgid "workspace.branches.materialize.accept"
msgstr "Turn it into a file"