From f8783b86a80650203b8ec92c1d96cdea5f11c62d Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Wed, 16 Sep 2026 21:47:36 +0000 Subject: [PATCH] =?UTF-8?q?fix(files):=20=D0=BF=D1=80=D0=BE=D0=BF=D1=83?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D1=82=D1=8C=20=D0=BF=D1=83=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B9=20dedup-=D0=BA=D0=BB=D1=8E=D1=87=20(dotfiles)=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Файлы без stem (.env, .gitignore, .npmrc) давали FileDedupKey="", и findWithinFolderDuplicates/findCrossFolderDuplicates считали их одной группой дублей и удаляли лишние. Пустой ключ больше не образует группу. --- files_dedupe.go | 6 ++++++ files_dedupe_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/files_dedupe.go b/files_dedupe.go index 73e0b83..bd3a3bf 100644 --- a/files_dedupe.go +++ b/files_dedupe.go @@ -154,6 +154,9 @@ func findWithinFolderDuplicates(indexed []ProjectFolderFile) []DedupGroup { byKey[k] = append(byKey[k], it.File) } for k, group := range byKey { + if k == "" { + continue // dotfiles etc. have no stem: never treat as duplicates + } if len(group) < 2 { continue } @@ -178,6 +181,9 @@ func findCrossFolderDuplicates(indexed []ProjectFolderFile) []DedupGroup { } var out []DedupGroup for k, items := range byKey { + if k == "" { + continue // dotfiles etc. have no stem: never treat as duplicates + } if len(items) < 2 { continue } diff --git a/files_dedupe_test.go b/files_dedupe_test.go index 6d24a85..f75d2a3 100644 --- a/files_dedupe_test.go +++ b/files_dedupe_test.go @@ -84,6 +84,32 @@ func TestMergeProjectRootForDedupe(t *testing.T) { } } +func TestFindDuplicatesSkipsEmptyKey(t *testing.T) { + // Dotfiles (".env", ".gitignore", ".npmrc") normalize to an empty stem, so + // FileDedupKey is "". They are not duplicates of each other and must never + // form a dedup group that would delete one of them. + env := &FileEntry{ID: jsonNum("1"), Title: strPtr(".env")} + gitignore := &FileEntry{ID: jsonNum("2"), Title: strPtr(".gitignore")} + npmrc := &FileEntry{ID: jsonNum("3"), Title: strPtr(".npmrc")} + if FileDedupKey(env) != "" || FileDedupKey(gitignore) != "" || FileDedupKey(npmrc) != "" { + t.Fatalf("dotfiles should have empty dedup key") + } + within := []ProjectFolderFile{ + {FolderID: "500", FolderTitle: "Cfg", File: env}, + {FolderID: "500", FolderTitle: "Cfg", File: gitignore}, + } + if groups := findWithinFolderDuplicates(within); len(groups) != 0 { + t.Fatalf("within-folder empty-key groups = %d, want 0 (%+v)", len(groups), groups) + } + cross := []ProjectFolderFile{ + {FolderID: "500", FolderTitle: "Cfg", File: env}, + {FolderID: "501", FolderTitle: "Other", File: npmrc}, + } + if groups := findCrossFolderDuplicates(cross); len(groups) != 0 { + t.Fatalf("cross-folder empty-key groups = %d, want 0 (%+v)", len(groups), groups) + } +} + func TestIsTrashFolderTitle(t *testing.T) { if !IsTrashFolderTitle("_trash-md") { t.Fatal("expected trash")