From e442996de30898a977e5114528445ef42831a2a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 14:17:24 +0000 Subject: [PATCH] fix(config): keep git flake subdir refs as distinct inputs (#2704) Two git flake references pointing at the same repository but different subdirectories (via the `dir` query parameter) are distinct inputs, yet devbox deduplicated them and only installed one. The root cause was `parseVersionedName`: it split package strings on `@` to separate name from version. Git flake URLs legitimately contain `@` in their `git@host` authority, so e.g. git+ssh://git@gitlab.com/org/repo.git?dir=betteralign&ref=...&rev=... git+ssh://git@gitlab.com/org/repo.git?dir=recovergoroutine&ref=...&rev=... both parsed to the name "git+ssh://git", with the rest dumped into the "version". Config.Packages() then deduplicates by name (lo.UniqBy), so one of the two inputs was silently dropped. Fix: treat flake references as unversioned in parseVersionedName, so the whole reference is used as the name. This keeps refs that differ only by query parameters distinct while leaving versioned devbox packages (and flake refs without an `@`) unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_013x2JTaq6GPt4kX5bubQC2D --- internal/devconfig/config_test.go | 29 +++++++++++++++++++ internal/devconfig/configfile/packages.go | 12 ++++++++ .../devconfig/configfile/packages_test.go | 16 ++++++++++ 3 files changed, 57 insertions(+) diff --git a/internal/devconfig/config_test.go b/internal/devconfig/config_test.go index 9f747796156..f7152eec384 100644 --- a/internal/devconfig/config_test.go +++ b/internal/devconfig/config_test.go @@ -1,6 +1,7 @@ package devconfig import ( + "fmt" "io/fs" "os" "path/filepath" @@ -430,6 +431,34 @@ func TestDefault(t *testing.T) { } } +// TestPackagesDoesNotDedupDistinctGitFlakes is a regression test for issue +// #2704. Two git flake references that point at the same repository but +// different subdirectories (via the "dir" query parameter) are distinct +// inputs and must both be preserved by Config.Packages(). Previously they +// were deduplicated because the "@" in the "git@host" portion of the URL was +// mistaken for a version delimiter, giving both packages the same parsed name. +func TestPackagesDoesNotDedupDistinctGitFlakes(t *testing.T) { + pkgA := "git+ssh://git@gitlab.com/org/repo.git?dir=betteralign&ref=master&rev=17d2bedca4884176e0d08078aa42311053e531c2" + pkgB := "git+ssh://git@gitlab.com/org/repo.git?dir=recovergoroutine&ref=master&rev=593d44945851d912f0c2158c46cc76da445adc43" + + jsonConfig := fmt.Sprintf(`{"packages":[%q,%q]}`, pkgA, pkgB) + cfg, err := loadBytes([]byte(jsonConfig)) + if err != nil { + t.Fatalf("load error: %v", err) + } + + packages := cfg.Packages(false /*includeRemovedTriggerPackages*/) + got := make([]string, len(packages)) + for i, p := range packages { + got[i] = p.VersionedName() + } + + want := []string{pkgA, pkgB} + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Config.Packages() dropped a distinct git flake input (-want +got):\n%s", diff) + } +} + func TestOSExpandIfPossible(t *testing.T) { tests := []struct { name string diff --git a/internal/devconfig/configfile/packages.go b/internal/devconfig/configfile/packages.go index 2c445d33d1d..3589af0e66b 100644 --- a/internal/devconfig/configfile/packages.go +++ b/internal/devconfig/configfile/packages.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" orderedmap "github.com/wk8/go-ordered-map/v2" "go.jetify.com/devbox/internal/boxcli/usererr" + "go.jetify.com/devbox/internal/devpkg/pkgtype" "go.jetify.com/devbox/internal/nix" "go.jetify.com/devbox/internal/searcher" "go.jetify.com/devbox/internal/ux" @@ -361,6 +362,17 @@ func (p *Package) UnmarshalJSON(data []byte) error { // parseVersionedName parses the name and version from package@version representation func parseVersionedName(versionedName string) (name, version string) { + // Flake references are not versioned Devbox packages, and they may + // legitimately contain an "@" in their URL (for example + // "git+ssh://git@gitlab.com/org/repo.git?dir=foo"). Splitting such a + // reference on "@" produces a name that is not unique across flakes that + // differ only by their query parameters (such as "dir"), which causes + // distinct inputs to be incorrectly deduplicated. Treat the whole + // reference as the name in that case. See issue #2704. + if pkgtype.IsFlake(versionedName) { + return versionedName, "" /*version*/ + } + var found bool name, version, found = searcher.ParseVersionedPackage(versionedName) if !found { diff --git a/internal/devconfig/configfile/packages_test.go b/internal/devconfig/configfile/packages_test.go index 38aefa46cd0..0f2d3c20cda 100644 --- a/internal/devconfig/configfile/packages_test.go +++ b/internal/devconfig/configfile/packages_test.go @@ -288,6 +288,22 @@ func TestParseVersionedName(t *testing.T) { expectedName: "github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello", expectedVersion: "", }, + { + // Git flake refs contain "@" in their URL (git@host) but are + // not versioned packages. The whole ref must be the name so + // that refs differing only by query params (e.g. "dir") stay + // distinct. See issue #2704. + name: "git-ssh-flake-with-dir", + input: "git+ssh://git@gitlab.com/org/repo.git?dir=betteralign&ref=master&rev=17d2bedca4884176e0d08078aa42311053e531c2", + expectedName: "git+ssh://git@gitlab.com/org/repo.git?dir=betteralign&ref=master&rev=17d2bedca4884176e0d08078aa42311053e531c2", + expectedVersion: "", + }, + { + name: "git-ssh-flake-without-query", + input: "git+ssh://git@github.com/org/repo.git", + expectedName: "git+ssh://git@github.com/org/repo.git", + expectedVersion: "", + }, } for _, testCase := range testCases {