Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions internal/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,22 @@ func processImport(app core.App, site *core.Record, zipData []byte, previewOnly
// one at random.
seenPaths := make(map[string]string)

// Parent/slug maps for every page already in the DB, so the diff pass can
// reject a slug/name fallback match that actually lives in a different
// folder. Without this, a same-name page under another parent (e.g.
// /product/docs vs /support/docs) is reported as "modified" when it's
// really an unrelated page — importPage then creates the correct page, and
// the preview diff disagrees with what the import does. Built once here;
// the DB doesn't change during this read-only diff pass.
existingParentMap := make(map[string]string)
existingSlugMap := make(map[string]string)
if existingPages, err := app.FindRecordsByFilter("pages", "site = {:site}", "", 0, 0, dbx.Params{"site": siteId}); err == nil {
for _, p := range existingPages {
existingParentMap[p.Id] = p.GetString("parent")
existingSlugMap[p.Id] = p.GetString("slug")
}
}

// Iterate in sorted key order so duplicate-route errors (and any other
// per-file behavior) are deterministic across runs.
sortedFilePaths := make([]string, 0, len(files))
Expand Down Expand Up @@ -977,6 +993,23 @@ func processImport(app core.App, site *core.Record, zipData []byte, previewOnly
existing, _ = app.FindFirstRecordByFilter("pages", "site = {:site} && name = {:name}", dbx.Params{"site": siteId, "name": pageData.Name})
}

// A slug/name fallback match may point at a page in a different folder
// that happens to share a leaf slug or display name. importPage rejects
// that as a cross-folder false positive and creates a fresh record;
// mirror that here so the diff reports this file as Added, not Modified.
//
// Only do this when the file asserts a specific identity via a valid
// exported _id that the matched record doesn't have — that's the signal
// these are two DISTINCT pages. A file WITHOUT an _id matched by name at
// a different path is a legitimate move/rename of one page (there's no
// competing identity), and must still be reported as Modified.
if existing != nil && pbRecordIdPattern.MatchString(pageData.ID) && existing.Id != pageData.ID {
existingPath := buildFullPagePath(existing.Id, existingParentMap, existingSlugMap)
if existingPath != pagePath {
existing = nil
}
}

// Slug/parent are derived from the file path, not from raw_source, so a
// moved or renamed page can have identical bytes. Mirror importPage's
// skip condition here so the diff label stays truthful: same bytes AND
Expand Down Expand Up @@ -1923,6 +1956,37 @@ func importPage(app core.App, site *core.Record, pageData ExportedPage, raw []by
slug = pagePath[lastSlash+1:]
}

// `existing` was matched in an earlier pass by id, then slug, then name —
// and the slug/name filters are NOT scoped by parent. So a page in a
// different folder that happens to share a leaf slug or a display name
// (e.g. /product/docs vs /support/docs, both named "Docs") can be matched
// as the "existing" record for THIS file. Reusing it would re-parent and
// overwrite the wrong page, silently merging two distinct pages into one.
//
// Drop the match only when this file asserts a specific identity via a valid
// exported _id that the matched record doesn't have, AND the record lives
// under a different parent — that's the signal these are two DISTINCT pages.
// A file WITHOUT an _id matched by name under a different parent is a
// legitimate move/rename of one page (no competing identity), so it must
// keep matching and re-parent rather than fork into a duplicate.
if existing != nil && pbRecordIdPattern.MatchString(pageData.ID) &&
existing.Id != pageData.ID && existing.GetString("parent") != parentId {
existing = nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// A genuine match (same parent) whose record id has drifted from a valid
// exported _id still orphans every cross-file `page:` link that references
// that _id — the link points at an id no page has. The importer already
// rebuilds a page's sections/entries from the file on every import, so the
// record carries no state worth preserving: delete it and let the create
// branch recreate it under the exported _id. Children rebuild as usual.
if existing != nil && pbRecordIdPattern.MatchString(pageData.ID) && existing.Id != pageData.ID {
if err := app.Delete(existing); err != nil {
return "", nil, fmt.Errorf("re-key page %q to exported _id %s: %w", pagePath, pageData.ID, err)
}
existing = nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// No-op guard: if the incoming page bytes are byte-identical to what we
// stored on the last import AND the page's file-derived location is
// unchanged (same parent + slug), the page is untouched since the dev's
Expand Down
Loading