nikoksr/proji

View on GitHub
cmd/packageImport.go

Summary

Maintainability
A
1 hr
Test Coverage
package cmd

import (
    "fmt"
    "regexp"

    "github.com/nikoksr/proji/pkg/domain"
    packagestore "github.com/nikoksr/proji/pkg/package/store"

    "github.com/nikoksr/proji/internal/statuswriter"

    "github.com/nikoksr/proji/internal/message"
    "github.com/nikoksr/proji/pkg/remote"
    "github.com/pkg/errors"
    "github.com/spf13/cobra"
)

const (
    flagExclude            = "exclude"
    flagConfig             = "config"
    flagDirectoryStructure = "dir-structure"
    flagRepoStructure      = "remote-structure"
    flagCollection         = "collection"
    flagPackage            = "package"
)

type packageImportCommand struct {
    cmd *cobra.Command
}

func newPackageImportCommand() *packageImportCommand {
    var remoteRepos, directories, configs, packages, collections []string

    cmd := &cobra.Command{
        Use:     "import FROM [FROM...]",
        Short:   "Import one or more packages",
        Aliases: []string{"i"},
        Example: `  proji package import gh:nikoksr/proji-official-collection/configs/nikoksr/go.toml
  proji package import -r https://github.com/torvalds/linux
  proji package import -d .`,
        PreRunE: func(cmd *cobra.Command, args []string) error {
            if cmd.Flags().NFlag() == 0 {
                if len(args) < 1 {
                    return fmt.Errorf("no config path or flag given")
                }
                message.Warningf("no flag given, trying regular package import by default")
                packages = args
            } else {
                // Concat the two arrays so that '... import --config *.toml' is a valid command.
                // Without appending the args, proji would only use the first toml-file and not all of
                // them as intended with the '*'.
                configs = append(configs, args...)
            }
            return nil
        },
        RunE: func(cmd *cobra.Command, args []string) error {
            importTypes := map[string][]string{
                flagConfig:             configs,
                flagDirectoryStructure: directories,
                flagRepoStructure:      remoteRepos,
                flagPackage:            packages,
                flagCollection:         collections,
            }

            // Compile exclude flag value to regex
            regexExclude, err := regexp.Compile(session.config.Import.Exclude)
            if err != nil {
                return errors.Wrap(err, "compile regex exclude")
            }

            // Import configs
            sw := statuswriter.New()
            sw.Run()
            for importType, paths := range importTypes {
                for _, path := range paths {
                    go importPackage(sw.NewSink(), path, importType, regexExclude)
                }
            }
            sw.Wait()
            return nil
        },
    }
    cmd.Flags().StringSliceVar(&packages, flagPackage, make([]string, 0), "import a package (default) (EXPERIMENTAL)")
    cmd.Flags().StringSliceVarP(&collections, flagCollection, "c", make([]string, 0), "import a collection of packages (EXPERIMENTAL)")
    cmd.Flags().StringSliceVarP(&configs, flagConfig, "f", make([]string, 0), "import a package from a config file")
    cmd.Flags().StringSliceVarP(&remoteRepos, flagRepoStructure, "r", make([]string, 0), "create an importable config based on on the structure of a remote repository")
    cmd.Flags().StringSliceVarP(&directories, flagDirectoryStructure, "d", make([]string, 0), "create an importable config based on the structure of a local directory")
    cmd.Flags().StringP(flagExclude, "e", "", "regex pattern to exclude paths from import (only works with -c, -r, -d)")

    _ = cmd.MarkFlagDirname(flagDirectoryStructure)
    _ = cmd.MarkFlagFilename(flagConfig)

    return &packageImportCommand{cmd: cmd}
}

func importPackage(status *statuswriter.Sink, path, importType string, exclude *regexp.Regexp) {
    defer status.Close()
    var pkg *domain.Package
    var err error

    status.Write(message.Sinfof("importing %s %s\n", importType, path))

    switch importType {
    case flagConfig:
        pkg, err = importPackageFromConfig(status, path)
    case flagDirectoryStructure:
        pkg, err = importPackageFromDirectoryStructure(status, path, exclude)
    case flagRepoStructure:
        pkg, err = importPackageFromRepoStructure(status, path, exclude)
    case flagPackage:
        pkg, err = importPackageFromRemote(status, path)
    case flagCollection:
        importPackagesFromCollection(status, path, exclude)
        return
    default:
        err = fmt.Errorf("import type %s not supported", importType)
    }
    if errors.Is(err, packagestore.ErrPackageExists) && importType != flagConfig {
        handleDuplicatePackage(status, pkg)
        return
    }
    if err != nil {
        status.Write(message.Serrorf(err, "failed to import package from %s %s", importType, path))
        return
    }
    if importType != flagCollection {
        // Collections handle messages on its own
        status.Write(message.Ssuccessf("successfully imported package %s [%s]", pkg.Name, pkg.Label))
    }
}

func importPackageFromConfig(status *statuswriter.Sink, path string) (*domain.Package, error) {
    // Import the package
    pkg, err := session.packageService.ImportPackageFromConfig(path)
    if err != nil {
        return nil, err
    }

    // Save the package
    status.Write(message.Sinfof("storing package %s [%s]", pkg.Name, pkg.Label))
    err = session.packageService.StorePackage(pkg)
    return pkg, err
}

func importPackageFromDirectoryStructure(status *statuswriter.Sink, path string, exclude *regexp.Regexp) (*domain.Package, error) {
    // Import the package
    pkg, err := session.packageService.ImportPackageFromDirectoryStructure(path, exclude)
    if err != nil {
        return nil, err
    }

    // Save the package
    status.Write(message.Sinfof("storing package %s [%s]", pkg.Name, pkg.Label))
    err = session.packageService.StorePackage(pkg)
    if err != nil {
        return nil, err
    }
    return pkg, err
}

func importPackageFromRepoStructure(status *statuswriter.Sink, url string, exclude *regexp.Regexp) (*domain.Package, error) {
    // Parse url string to object
    status.Write(message.Sinfof("parsing url"))
    parsedURL, err := remote.ParseURL(url)
    if err != nil {
        return nil, err
    }

    // Import the package
    status.Write(message.Sinfof("creating package from repository structure of %s", parsedURL.String()))
    pkg, err := session.packageService.ImportPackageFromRepositoryStructure(parsedURL, exclude)
    if err != nil {
        return nil, err
    }

    // Save the package
    status.Write(message.Sinfof("storing package %s [%s]", pkg.Name, pkg.Label))
    err = session.packageService.StorePackage(pkg)
    return pkg, err
}

func importPackageFromRemote(status *statuswriter.Sink, url string) (*domain.Package, error) {
    // Parse url string to object
    status.Write(message.Sinfof("parsing url"))
    parsedURL, err := remote.ParseURL(url)
    if err != nil {
        return nil, err
    }

    // Import the package
    status.Write(message.Sinfof("importing package from %s", parsedURL.String()))
    pkg, err := session.packageService.ImportPackageFromRemote(parsedURL)
    if err != nil {
        return nil, err
    }

    // Save the package
    status.Write(message.Sinfof("storing package %s [%s]", pkg.Name, pkg.Label))
    err = session.packageService.StorePackage(pkg)
    if err != nil {
        return nil, err
    }
    message.Successf("successfully imported package %s", pkg.Name)
    return pkg, err
}

func importPackagesFromCollection(status *statuswriter.Sink, url string, exclude *regexp.Regexp) {
    // Parse url string to object
    status.Write(message.Sinfof("parsing url"))
    parsedURL, err := remote.ParseURL(url)
    if err != nil {
        status.Write(message.Serrorf(err, "failed to parse collection url %s", url))
        return
    }

    // Import the packages
    status.Write(message.Sinfof("importing packages from collection %s", parsedURL.String()))
    pkgs, err := session.packageService.ImportPackagesFromCollection(parsedURL, exclude)
    if err != nil {
        status.Write(message.Serrorf(err, "failed to import packages from collection %s", parsedURL.String()))
        return
    }

    // Save the packages
    var successfulImports int
    for _, pkg := range pkgs {
        status.Write(message.Sinfof("storing package %s [%s]", pkg.Name, pkg.Label))
        err = session.packageService.StorePackage(pkg)
        if errors.Is(err, packagestore.ErrPackageExists) {
            handleDuplicatePackage(status, pkg)
            continue
        }
        if err != nil {
            status.Write(message.Serrorf(err, "failed to store package %s [%s]", pkg.Name, pkg.Label))
        } else {
            status.Write(message.Ssuccessf("successfully imported package %s [%s]", pkg.Name, pkg.Label))
            successfulImports++
        }
    }
    status.Write(message.Ssuccessf("successfully imported %d of %d package from collection %s", successfulImports, len(pkgs), parsedURL.String()))
}

func handleDuplicatePackage(status *statuswriter.Sink, pkg *domain.Package) {
    // Announce config export
    status.Write(message.Swarningf(
        "%v (label %s): exporting package config",
        packagestore.ErrPackageExists,
        pkg.Label),
    )

    // Try to export package config for editing
    exportedTo, err := session.packageService.ExportPackageToConfig(*pkg, ".")
    if err != nil {
        status.Write(message.Serrorf(
            err,
            "%v (label %s): failed to export package config",
            packagestore.ErrPackageExists,
            pkg.Label),
        )
        return
    }
    status.Write(message.Swarningf(
        "%v (label %s): exported package config to %s",
        packagestore.ErrPackageExists,
        pkg.Label,
        exportedTo),
    )
}