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
4 changes: 4 additions & 0 deletions internal/devbox/devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ type Devbox struct {

// This is needed because of the --quiet flag.
stderr io.Writer

// packagesBeingUpdated tracks which packages are being updated so that
// installNixPackagesToStore only refreshes those, not all packages.
packagesBeingUpdated []*devpkg.Package
}

var legacyPackagesWarningHasBeenShown = false
Expand Down
11 changes: 10 additions & 1 deletion internal/devbox/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ func (d *Devbox) packagesToInstallInStore(ctx context.Context, mode installMode)
packagesToInstall := []*devpkg.Package{}
storePathsForPackage := map[*devpkg.Package][]string{}
for _, pkg := range packages {
if mode == update {
if mode == update && d.isBeingUpdated(pkg) {
packagesToInstall = append(packagesToInstall, pkg)
continue
}
Expand Down Expand Up @@ -666,6 +666,15 @@ func (d *Devbox) packagesToInstallInStore(ctx context.Context, mode installMode)
return lo.Uniq(packagesToInstall), nil
}

func (d *Devbox) isBeingUpdated(pkg *devpkg.Package) bool {
for _, u := range d.packagesBeingUpdated {
if u.Raw == pkg.Raw {
return true
}
}
return false
Comment on lines +669 to +675
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isBeingUpdated does a linear scan over d.packagesBeingUpdated for every package in packagesToInstallInStore. When updating all packages this becomes O(n²) and adds avoidable overhead. Consider representing the update targets as a map[string]struct{}/set keyed by pkg.Raw (or building such a set once) so membership checks are O(1).

Copilot uses AI. Check for mistakes.
}

// moveAllowInsecureFromLockfile will modernize a Devbox project by moving the allow_insecure: boolean
// setting from the devbox.lock file to the corresponding package in devbox.json.
//
Expand Down
2 changes: 2 additions & 0 deletions internal/devbox/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ func (d *Devbox) Update(ctx context.Context, opts devopt.UpdateOpts) error {
}
}

d.packagesBeingUpdated = inputs

Comment on lines +83 to +84
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change fixes a user-visible regression in devbox update <pkg> behavior, but there isn't a test covering the new “only refresh targeted packages” logic. Please add a regression test that exercises Update with a single package in a multi-package config and asserts only that package is selected for store installation/refresh (e.g., by stubbing the store-install selection or asserting on the emitted install list).

Copilot uses AI. Check for mistakes.
mode := update
if opts.NoInstall {
mode = noInstall
Expand Down
37 changes: 37 additions & 0 deletions internal/devbox/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,43 @@ func TestUpdateOtherSysInfoIsReplaced(t *testing.T) {
require.Equal(t, "store_path2", lockfile.Packages[raw].Systems[sys2].Outputs[0].Path)
}

func TestUpdateOnlyTargetedPackagesAreSelected(t *testing.T) {
devbox := devboxForTesting(t)

pkgA := devpkg.PackageFromStringWithDefaults("hello@1.2.3", nil)
pkgB := devpkg.PackageFromStringWithDefaults("curl@latest", nil)
pkgC := devpkg.PackageFromStringWithDefaults("git@latest", nil)

// Simulate updating only pkgB.
devbox.packagesBeingUpdated = []*devpkg.Package{pkgB}

require.False(t, devbox.isBeingUpdated(pkgA), "pkgA should not be marked as being updated")
require.True(t, devbox.isBeingUpdated(pkgB), "pkgB should be marked as being updated")
require.False(t, devbox.isBeingUpdated(pkgC), "pkgC should not be marked as being updated")
}

func TestUpdateAllPackagesSelectedWhenNoneTargeted(t *testing.T) {
devbox := devboxForTesting(t)

pkgA := devpkg.PackageFromStringWithDefaults("hello@1.2.3", nil)
pkgB := devpkg.PackageFromStringWithDefaults("curl@latest", nil)

// Simulate `devbox update` with no args: all packages are in the update list.
devbox.packagesBeingUpdated = []*devpkg.Package{pkgA, pkgB}

require.True(t, devbox.isBeingUpdated(pkgA))
require.True(t, devbox.isBeingUpdated(pkgB))
}

func TestUpdateEmptyUpdateListSelectsNothing(t *testing.T) {
devbox := devboxForTesting(t)

helloPkg := devpkg.PackageFromStringWithDefaults("hello@1.2.3", nil)

// packagesBeingUpdated is nil (default) — no package should be force-refreshed.
require.False(t, devbox.isBeingUpdated(helloPkg))
}

func currentSystem(*testing.T) string {
sys := nix.System() // NOTE: we could mock this too, if it helps.
return sys
Expand Down
Loading