From 135e7c70f4fedd13f233afc46a9edaef6deaf50c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 06:24:07 +0000 Subject: [PATCH] Add IsHeld method to Holder interface and implement in apt/dnf backends Co-authored-by: taigrr <8261498+taigrr@users.noreply.github.com> --- apt/apt.go | 5 +++++ apt/apt_integration_test.go | 10 ++++++++++ apt/capabilities_linux.go | 9 +++++++++ apt/capabilities_other.go | 4 ++++ dnf/capabilities.go | 5 +++++ dnf/capabilities_linux.go | 20 ++++++++++++++++++++ dnf/capabilities_other.go | 4 ++++ dnf/dnf_integration_test.go | 10 ++++++++++ snack.go | 3 +++ 9 files changed, 70 insertions(+) diff --git a/apt/apt.go b/apt/apt.go index 18fe7ad..60c4fdf 100644 --- a/apt/apt.go +++ b/apt/apt.go @@ -124,6 +124,11 @@ func (a *Apt) ListHeld(ctx context.Context) ([]snack.Package, error) { return listHeld(ctx) } +// IsHeld reports whether a specific package is currently held. +func (a *Apt) IsHeld(ctx context.Context, pkg string) (bool, error) { + return isHeld(ctx, pkg) +} + // Autoremove removes packages no longer required as dependencies. func (a *Apt) Autoremove(ctx context.Context, opts ...snack.Option) error { a.Lock() diff --git a/apt/apt_integration_test.go b/apt/apt_integration_test.go index b90048b..d9e4593 100644 --- a/apt/apt_integration_test.go +++ b/apt/apt_integration_test.go @@ -230,6 +230,16 @@ func TestIntegration_Apt(t *testing.T) { require.NoError(t, err) }) + t.Run("IsHeld", func(t *testing.T) { + held, err := h.IsHeld(ctx, "tree") + require.NoError(t, err) + assert.True(t, held, "tree should be held") + + notHeld, err := h.IsHeld(ctx, "curl") + require.NoError(t, err) + assert.False(t, notHeld, "curl should not be held") + }) + t.Run("ListHeld", func(t *testing.T) { held, err := h.ListHeld(ctx) require.NoError(t, err) diff --git a/apt/capabilities_linux.go b/apt/capabilities_linux.go index 77a6267..8b6adf4 100644 --- a/apt/capabilities_linux.go +++ b/apt/capabilities_linux.go @@ -159,6 +159,15 @@ func listHeld(ctx context.Context) ([]snack.Package, error) { return pkgs, nil } +func isHeld(ctx context.Context, pkg string) (bool, error) { + cmd := exec.CommandContext(ctx, "apt-mark", "showhold", pkg) + out, err := cmd.Output() + if err != nil { + return false, fmt.Errorf("apt-mark showhold %s: %w", pkg, err) + } + return strings.TrimSpace(string(out)) == pkg, nil +} + // --- Cleaner --- func autoremove(ctx context.Context, opts ...snack.Option) error { diff --git a/apt/capabilities_other.go b/apt/capabilities_other.go index 6ac0224..b7499ca 100644 --- a/apt/capabilities_other.go +++ b/apt/capabilities_other.go @@ -36,6 +36,10 @@ func listHeld(_ context.Context) ([]snack.Package, error) { return nil, snack.ErrUnsupportedPlatform } +func isHeld(_ context.Context, _ string) (bool, error) { + return false, snack.ErrUnsupportedPlatform +} + func autoremove(_ context.Context, _ ...snack.Option) error { return snack.ErrUnsupportedPlatform } diff --git a/dnf/capabilities.go b/dnf/capabilities.go index 2693bf8..1ee90c4 100644 --- a/dnf/capabilities.go +++ b/dnf/capabilities.go @@ -57,6 +57,11 @@ func (d *DNF) ListHeld(ctx context.Context) ([]snack.Package, error) { return listHeld(ctx, d.v5) } +// IsHeld reports whether a specific package is currently held. +func (d *DNF) IsHeld(ctx context.Context, pkg string) (bool, error) { + return isHeld(ctx, pkg, d.v5) +} + // Autoremove removes orphaned packages. func (d *DNF) Autoremove(ctx context.Context, opts ...snack.Option) error { d.Lock() diff --git a/dnf/capabilities_linux.go b/dnf/capabilities_linux.go index 6d2b6ec..5186fd0 100644 --- a/dnf/capabilities_linux.go +++ b/dnf/capabilities_linux.go @@ -113,6 +113,26 @@ func listHeld(ctx context.Context, v5 bool) ([]snack.Package, error) { return parseVersionLock(out), nil } +func isHeld(ctx context.Context, pkg string, v5 bool) (bool, error) { + out, err := run(ctx, []string{"versionlock", "list", pkg}, snack.Options{}) + if err != nil { + // versionlock list exits non-zero when no match is found on some versions + return false, nil + } + var pkgs []snack.Package + if v5 { + pkgs = parseVersionLockDNF5(out) + } else { + pkgs = parseVersionLock(out) + } + for _, p := range pkgs { + if p.Name == pkg { + return true, nil + } + } + return false, nil +} + func autoremove(ctx context.Context, opts ...snack.Option) error { o := snack.ApplyOptions(opts...) _, err := run(ctx, []string{"autoremove", "-y"}, o) diff --git a/dnf/capabilities_other.go b/dnf/capabilities_other.go index 0e76482..a958b46 100644 --- a/dnf/capabilities_other.go +++ b/dnf/capabilities_other.go @@ -36,6 +36,10 @@ func listHeld(_ context.Context, _ bool) ([]snack.Package, error) { return nil, snack.ErrUnsupportedPlatform } +func isHeld(_ context.Context, _ string, _ bool) (bool, error) { + return false, snack.ErrUnsupportedPlatform +} + func autoremove(_ context.Context, _ ...snack.Option) error { return snack.ErrUnsupportedPlatform } diff --git a/dnf/dnf_integration_test.go b/dnf/dnf_integration_test.go index 2f20fc4..0e0133b 100644 --- a/dnf/dnf_integration_test.go +++ b/dnf/dnf_integration_test.go @@ -225,6 +225,16 @@ func TestIntegration_DNF(t *testing.T) { t.Skipf("versionlock plugin not available: %v", err) } + t.Run("IsHeld", func(t *testing.T) { + held, err := h.IsHeld(ctx, "tree") + require.NoError(t, err) + assert.True(t, held, "tree should be held") + + notHeld, err := h.IsHeld(ctx, "curl") + require.NoError(t, err) + assert.False(t, notHeld, "curl should not be held") + }) + t.Run("ListHeld", func(t *testing.T) { held, err := h.ListHeld(ctx) require.NoError(t, err) diff --git a/snack.go b/snack.go index 9dae405..b700929 100644 --- a/snack.go +++ b/snack.go @@ -125,6 +125,9 @@ type Holder interface { // ListHeld returns all currently held/pinned packages. ListHeld(ctx context.Context) ([]Package, error) + + // IsHeld reports whether a specific package is currently held/pinned. + IsHeld(ctx context.Context, pkg string) (bool, error) } // Cleaner provides orphan/cache cleanup operations.