github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runbits/dependencies/changesummary.go (about)

     1  package dependencies
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/ActiveState/cli/internal/locale"
    10  	"github.com/ActiveState/cli/internal/logging"
    11  	"github.com/ActiveState/cli/internal/output"
    12  	"github.com/ActiveState/cli/internal/sliceutils"
    13  	"github.com/ActiveState/cli/pkg/buildplan"
    14  )
    15  
    16  // showUpdatedPackages specifies whether or not to include updated dependencies in the direct
    17  // dependencies list, and whether or not to include updated dependencies when calculating indirect
    18  // dependency numbers.
    19  const showUpdatedPackages = true
    20  
    21  // OutputChangeSummary looks over the given artifact changeset and attempts to determine if a single
    22  // package install request was made. If so, it computes and lists the additional dependencies being
    23  // installed for that package.
    24  // `artifacts` is an ArtifactMap containing artifacts in the changeset, and `filter` contains any
    25  // runtime requirements/artifacts already installed.
    26  func OutputChangeSummary(out output.Outputer, changeset *buildplan.ArtifactChangeset, alreadyInstalled buildplan.Artifacts) {
    27  	addedString := []string{}
    28  	addedLocale := []string{}
    29  	added := buildplan.Ingredients{}
    30  	dependencies := buildplan.Ingredients{}
    31  	directDependencies := buildplan.Ingredients{}
    32  	for _, a := range changeset.Added {
    33  		added = append(added, a.Ingredients...)
    34  		for _, i := range a.Ingredients {
    35  			v := fmt.Sprintf("%s@%s", i.Name, i.Version)
    36  			addedString = append(addedLocale, v)
    37  			addedLocale = append(addedLocale, fmt.Sprintf("[ACTIONABLE]%s[/RESET]", v))
    38  			dependencies = append(dependencies, i.RuntimeDependencies(true)...)
    39  			directDependencies = append(dependencies, i.RuntimeDependencies(false)...)
    40  		}
    41  	}
    42  
    43  	dependencies = sliceutils.UniqueByProperty(dependencies, func(i *buildplan.Ingredient) any { return i.IngredientID })
    44  	directDependencies = sliceutils.UniqueByProperty(directDependencies, func(i *buildplan.Ingredient) any { return i.IngredientID })
    45  	numIndirect := len(dependencies) - len(directDependencies)
    46  
    47  	sort.SliceStable(directDependencies, func(i, j int) bool {
    48  		return directDependencies[i].Name < directDependencies[j].Name
    49  	})
    50  
    51  	logging.Debug("packages %s have %d direct dependencies and %d indirect dependencies",
    52  		strings.Join(addedString, ", "), len(directDependencies), numIndirect)
    53  	if len(directDependencies) == 0 {
    54  		return
    55  	}
    56  
    57  	// Process the existing runtime requirements into something we can easily compare against.
    58  	oldRequirements := alreadyInstalled.Ingredients().ToIDMap()
    59  
    60  	localeKey := "additional_dependencies"
    61  	if numIndirect > 0 {
    62  		localeKey = "additional_total_dependencies"
    63  	}
    64  	out.Notice(locale.Tr(localeKey, strings.Join(addedLocale, ", "), strconv.Itoa(len(directDependencies)), strconv.Itoa(numIndirect)))
    65  
    66  	// A direct dependency list item is of the form:
    67  	//   ├─ name@version (X dependencies)
    68  	// or
    69  	//   └─ name@oldVersion → name@newVersion (Updated)
    70  	// depending on whether or not it has subdependencies, and whether or not showUpdatedPackages is
    71  	// `true`.
    72  	for i, ingredient := range directDependencies {
    73  		prefix := "├─"
    74  		if i == len(directDependencies)-1 {
    75  			prefix = "└─"
    76  		}
    77  
    78  		ingredientDeps := ingredient.RuntimeDependencies(true)
    79  		subdependencies := ""
    80  		if numSubs := len(ingredientDeps); numSubs > 0 {
    81  			subdependencies = fmt.Sprintf(" ([ACTIONABLE]%s[/RESET] dependencies)", // intentional leading space
    82  				strconv.Itoa(numSubs))
    83  		}
    84  
    85  		item := fmt.Sprintf("[ACTIONABLE]%s@%s[/RESET]%s", // intentional omission of space before last %s
    86  			ingredient.Name, ingredient.Version, subdependencies)
    87  		oldVersion, exists := oldRequirements[ingredient.IngredientID]
    88  		if exists && ingredient.Version != "" && oldVersion.Version != ingredient.Version {
    89  			item = fmt.Sprintf("[ACTIONABLE]%s@%s[/RESET] → %s (%s)", oldVersion.Name, oldVersion.Version, item, locale.Tl("updated", "updated"))
    90  		}
    91  
    92  		out.Notice(fmt.Sprintf("  [DISABLED]%s[/RESET] %s", prefix, item))
    93  	}
    94  
    95  	out.Notice("") // blank line
    96  }