mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
Merge remote-tracking branch 'origin/copilot/add-group-is-installed-check' into cd/integration-copilot-prs
This commit is contained in:
@@ -15,6 +15,7 @@ var (
|
||||
_ snack.RepoManager = (*DNF)(nil)
|
||||
_ snack.KeyManager = (*DNF)(nil)
|
||||
_ snack.Grouper = (*DNF)(nil)
|
||||
_ snack.GroupQuerier = (*DNF)(nil)
|
||||
_ snack.NameNormalizer = (*DNF)(nil)
|
||||
)
|
||||
|
||||
@@ -136,6 +137,11 @@ func (d *DNF) GroupInstall(ctx context.Context, group string, opts ...snack.Opti
|
||||
return groupInstall(ctx, group, opts...)
|
||||
}
|
||||
|
||||
// GroupIsInstalled reports whether all packages in the group are installed.
|
||||
func (d *DNF) GroupIsInstalled(ctx context.Context, group string) (bool, error) {
|
||||
return groupIsInstalled(ctx, group, d.v5)
|
||||
}
|
||||
|
||||
// NormalizeName returns the canonical form of a package name.
|
||||
func (d *DNF) NormalizeName(name string) string {
|
||||
return normalizeName(name)
|
||||
|
||||
@@ -264,3 +264,14 @@ func groupInstall(ctx context.Context, group string, opts ...snack.Option) error
|
||||
_, err := run(ctx, []string{"group", "install", "-y", group}, o)
|
||||
return err
|
||||
}
|
||||
|
||||
func groupIsInstalled(ctx context.Context, group string, v5 bool) (bool, error) {
|
||||
out, err := run(ctx, []string{"group", "list"}, snack.Options{})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("dnf groupIsInstalled: %w", err)
|
||||
}
|
||||
if v5 {
|
||||
return parseGroupIsInstalledDNF5(out, group), nil
|
||||
}
|
||||
return parseGroupIsInstalled(out, group), nil
|
||||
}
|
||||
|
||||
@@ -87,3 +87,7 @@ func groupInfo(_ context.Context, _ string, _ bool) ([]snack.Package, error) {
|
||||
func groupInstall(_ context.Context, _ string, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func groupIsInstalled(_ context.Context, _ string, _ bool) (bool, error) {
|
||||
return false, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
20
dnf/parse.go
20
dnf/parse.go
@@ -265,6 +265,26 @@ func parseGroupList(output string) []string {
|
||||
return groups
|
||||
}
|
||||
|
||||
// parseGroupIsInstalled checks whether a named group appears under "Installed Groups:"
|
||||
// in `dnf group list` output.
|
||||
func parseGroupIsInstalled(output, group string) bool {
|
||||
inInstalled := false
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasSuffix(trimmed, "Groups:") || strings.HasSuffix(trimmed, "groups:") {
|
||||
inInstalled = strings.HasPrefix(strings.ToLower(trimmed), "installed")
|
||||
continue
|
||||
}
|
||||
if !inInstalled || trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(trimmed, group) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseGroupInfo parses `dnf group info <group>` output.
|
||||
// Packages are listed under section headers like "Mandatory Packages:", "Default Packages:", "Optional Packages:".
|
||||
func parseGroupInfo(output string) []snack.Package {
|
||||
|
||||
@@ -188,6 +188,39 @@ func parseGroupListDNF5(output string) []string {
|
||||
return groups
|
||||
}
|
||||
|
||||
// parseGroupIsInstalledDNF5 checks whether a named group is installed from
|
||||
// `dnf5 group list` tabular output. A group is installed when its status field is "yes".
|
||||
func parseGroupIsInstalledDNF5(output, group string) bool {
|
||||
output = stripPreamble(output)
|
||||
inBody := false
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if !inBody {
|
||||
if strings.HasPrefix(trimmed, "ID") && strings.Contains(trimmed, "Name") {
|
||||
inBody = true
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
status := parts[len(parts)-1]
|
||||
if status != "yes" && status != "no" {
|
||||
continue
|
||||
}
|
||||
name := strings.Join(parts[1:len(parts)-1], " ")
|
||||
if strings.EqualFold(name, group) {
|
||||
return status == "yes"
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseVersionLockDNF5 parses `dnf5 versionlock list` output.
|
||||
// Format:
|
||||
//
|
||||
|
||||
@@ -186,6 +186,43 @@ func TestParseGroupInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupIsInstalled(t *testing.T) {
|
||||
input := `Available Groups:
|
||||
Container Management
|
||||
Development Tools
|
||||
Headless Management
|
||||
Installed Groups:
|
||||
Minimal Install
|
||||
Server
|
||||
`
|
||||
tests := []struct {
|
||||
group string
|
||||
want bool
|
||||
}{
|
||||
{"Minimal Install", true},
|
||||
{"Server", true},
|
||||
{"Development Tools", false},
|
||||
{"Container Management", false},
|
||||
{"Nonexistent Group", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := parseGroupIsInstalled(input, tt.group)
|
||||
if got != tt.want {
|
||||
t.Errorf("parseGroupIsInstalled(%q) = %v, want %v", tt.group, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that empty lines within the Installed section don't stop parsing.
|
||||
inputWithBlankLines := `Installed Groups:
|
||||
First Group
|
||||
|
||||
Second Group
|
||||
`
|
||||
if !parseGroupIsInstalled(inputWithBlankLines, "Second Group") {
|
||||
t.Error("parseGroupIsInstalled: should find group after blank line in installed section")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeName(t *testing.T) {
|
||||
tests := []struct {
|
||||
input, want string
|
||||
@@ -338,6 +375,27 @@ kde-desktop KDE n
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupIsInstalledDNF5(t *testing.T) {
|
||||
input := `ID Name Installed
|
||||
neuron-modelling-simulators Neuron Modelling Simulators no
|
||||
kde-desktop KDE yes
|
||||
`
|
||||
tests := []struct {
|
||||
group string
|
||||
want bool
|
||||
}{
|
||||
{"KDE", true},
|
||||
{"Neuron Modelling Simulators", false},
|
||||
{"Nonexistent Group", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := parseGroupIsInstalledDNF5(input, tt.group)
|
||||
if got != tt.want {
|
||||
t.Errorf("parseGroupIsInstalledDNF5(%q) = %v, want %v", tt.group, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGroupInfoDNF5(t *testing.T) {
|
||||
input := `Id : kde-desktop
|
||||
Name : KDE
|
||||
@@ -410,5 +468,6 @@ var (
|
||||
_ snack.RepoManager = (*DNF)(nil)
|
||||
_ snack.KeyManager = (*DNF)(nil)
|
||||
_ snack.Grouper = (*DNF)(nil)
|
||||
_ snack.GroupQuerier = (*DNF)(nil)
|
||||
_ snack.NameNormalizer = (*DNF)(nil)
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ var (
|
||||
_ snack.Cleaner = (*Pacman)(nil)
|
||||
_ snack.FileOwner = (*Pacman)(nil)
|
||||
_ snack.Grouper = (*Pacman)(nil)
|
||||
_ snack.GroupQuerier = (*Pacman)(nil)
|
||||
)
|
||||
|
||||
// NOTE: snack.Holder is not implemented for pacman. While pacman supports
|
||||
@@ -78,3 +79,8 @@ func (p *Pacman) GroupInstall(ctx context.Context, group string, opts ...snack.O
|
||||
defer p.Unlock()
|
||||
return groupInstall(ctx, group, opts...)
|
||||
}
|
||||
|
||||
// GroupIsInstalled reports whether all packages in the group are installed.
|
||||
func (p *Pacman) GroupIsInstalled(ctx context.Context, group string) (bool, error) {
|
||||
return groupIsInstalled(ctx, group)
|
||||
}
|
||||
|
||||
@@ -219,3 +219,50 @@ func groupInstall(ctx context.Context, group string, opts ...snack.Option) error
|
||||
_, err := run(ctx, []string{"-S", "--noconfirm", group}, o)
|
||||
return err
|
||||
}
|
||||
|
||||
// parseGroupPkgSet parses "group pkg" lines and returns the set of package names.
|
||||
func parseGroupPkgSet(output string) map[string]struct{} {
|
||||
set := make(map[string]struct{})
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
set[parts[1]] = struct{}{}
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func groupIsInstalled(ctx context.Context, group string) (bool, error) {
|
||||
// Get all packages in the group from the sync database.
|
||||
availOut, err := run(ctx, []string{"-Sg", group}, snack.Options{})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "exit status 1") {
|
||||
return false, fmt.Errorf("pacman groupIsInstalled %s: %w", group, snack.ErrNotFound)
|
||||
}
|
||||
return false, fmt.Errorf("pacman groupIsInstalled: %w", err)
|
||||
}
|
||||
avail := parseGroupPkgSet(availOut)
|
||||
if len(avail) == 0 {
|
||||
return false, fmt.Errorf("pacman groupIsInstalled %s: %w", group, snack.ErrNotFound)
|
||||
}
|
||||
// Get packages from the group that are installed (local database).
|
||||
instOut, err := run(ctx, []string{"-Qg", group}, snack.Options{})
|
||||
if err != nil {
|
||||
// exit status 1 means nothing from this group is installed.
|
||||
if strings.Contains(err.Error(), "exit status 1") {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("pacman groupIsInstalled: %w", err)
|
||||
}
|
||||
inst := parseGroupPkgSet(instOut)
|
||||
for pkg := range avail {
|
||||
if _, ok := inst[pkg]; !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -51,3 +51,7 @@ func groupInfo(_ context.Context, _ string) ([]snack.Package, error) {
|
||||
func groupInstall(_ context.Context, _ string, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func groupIsInstalled(_ context.Context, _ string) (bool, error) {
|
||||
return false, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
8
snack.go
8
snack.go
@@ -188,6 +188,14 @@ type Grouper interface {
|
||||
GroupInstall(ctx context.Context, group string, opts ...Option) error
|
||||
}
|
||||
|
||||
// GroupQuerier provides an efficient check for whether a package group is
|
||||
// fully installed. This is an optional extension of [Grouper].
|
||||
// Supported by: pacman, dnf/yum.
|
||||
type GroupQuerier interface {
|
||||
// GroupIsInstalled reports whether all packages in the group are installed.
|
||||
GroupIsInstalled(ctx context.Context, group string) (bool, error)
|
||||
}
|
||||
|
||||
// NormalizeName provides package name normalization.
|
||||
// Supported by: apt (strips :arch suffixes), rpm.
|
||||
type NameNormalizer interface {
|
||||
|
||||
Reference in New Issue
Block a user