Browse Source

Rewrite (#7)

Closes #6

This is, more or less, a fundamental rewrite of gpm.

Co-authored-by: jolheiser <john.olheiser@gmail.com>
Reviewed-on: https://gitea.com/jolheiser/gpm/pulls/7
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-committed-by: John Olheiser <john.olheiser@gmail.com>
main go-gpm/v0.2.0
John Olheiser 8 months ago
parent
commit
ef0d29afa3
  1. 8
      Earthfile
  2. 19
      LICENSE
  3. 35
      Makefile
  4. 15
      README.md
  5. 51
      cmd/add.go
  6. 100
      cmd/cmd.go
  7. 41
      cmd/config.go
  8. 30
      cmd/export.go
  9. 11
      cmd/flags/flags.go
  10. 78
      cmd/get.go
  11. 79
      cmd/import.go
  12. 18
      cmd/list.go
  13. 67
      cmd/remove.go
  14. 57
      cmd/search.go
  15. 35
      cmd/server.go
  16. 86
      cmd/update.go
  17. 146
      config/config.go
  18. 79
      database/database.go
  19. 87
      database/database_test.go
  20. 12
      docker/Dockerfile
  21. 9
      docker/docker-compose.yml
  22. 33
      docs.go
  23. 59
      go-gpm/client.go
  24. 3
      go-gpm/go.mod
  25. 207
      go-gpm/gpm_test.go
  26. 124
      go-gpm/package.go
  27. 9
      go.mod
  28. 13
      go.sum
  29. 25
      main.go
  30. 127
      router/router.go
  31. 166
      router/router_test.go

8
Earthfile

@ -1,10 +1,14 @@
# To lint, install Earthly and run `earth +lint`
# This ensures the usage of the same version of golangci-lint
FROM golangci/golangci-lint:v1.31
FROM golangci/golangci-lint:v1.37
WORKDIR /gpm
lint:
lint-cli:
COPY . .
RUN golangci-lint run
lint-lib:
COPY ./go-gpm .
RUN golangci-lint run

19
LICENSE

@ -0,0 +1,19 @@
Copyright (c) 2021 John Olheiser
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

35
Makefile

@ -3,12 +3,39 @@ VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
.PHONY: build
build:
$(GO) build -ldflags '-s -w -X "go.jolheiser.com/gpm/config.Version=$(VERSION)"'
$(GO) build -ldflags '-s -w -X "go.jolheiser.com/gpm/router.Version=$(VERSION)"'
.PHONY: lint
lint:
earth +lint-cli
earth +lint-lib
.PHONY: fmt
fmt:
$(GO) fmt ./...
fmt: fmt-cli fmt-lib
.PHONY: test
test:
test: test-cli test-lib
.PHONY: fmt-cli
fmt-cli:
$(GO) fmt ./...
.PHONY: test-cli
test-cli:
$(GO) test -race ./...
.PHONY: fmt-lib
fmt-lib:
@cd go-gpm && $(GO) fmt ./...
.PHONY: test-lib
test-lib:
@cd go-gpm && $(GO) test -race ./...
.PHONY: docker-build
docker-build:
docker build -f docker/Dockerfile -t jolheiser/gpm .
.PHONY: docker-push
docker-push: docker-build
docker push jolheiser/gpm

15
README.md

@ -17,18 +17,21 @@ Using either a GPM server or local config, I can instead `gpm get cli` which fin
* `remove` - Remove a local package
* `list` - List local packages
* `config` - Change local configuration
* `export` - Export local packages to JSON
* `import` - Import JSON to local packages. Either give a path to a `.json` file, or a URL to a GPM server export endpoint
* e.g. `https://gpm.jolheiser.com/export`
* `get` - Get a list of packages
* e.g. `gpm get beaver survey toml homedir cli` to get all the modules needed for gpm itself (assuming the map resolves to the same packages)
* e.g. `gpm get beaver survey bbolt cli chi` to get all the modules needed for gpm itself (assuming the map resolves to the same packages)
* `server` - Start a gpm server
### Server
If GPM doesn't find a package locally, it can call out to a configurable gpm server to find a package there instead.
gpm will call out to a gpm server to find a package.
This makes it much simpler to have a central library of packages rather than exporting and importing between environments.
Want to run your own server? It's very easy! This CLI comes packaged with the server inside, simply run `gpm server` to start up a GPM server.
Put it behind your favorite reverse proxy and it's ready to go!
Remember to set a `--token`!
Put it behind your favorite reverse proxy, and it's ready to go!
## License
[MIT](LICENSE)

51
cmd/add.go

@ -1,10 +1,13 @@
package cmd
import (
"context"
"regexp"
"strings"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
@ -12,26 +15,30 @@ import (
)
var Add = cli.Command{
Name: "add",
Usage: "Add a package",
Name: "add",
Aliases: []string{"a"},
Usage: "Add a package",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Overwrite existing package without prompt",
Name: "force",
Aliases: []string{"f"},
Usage: "Overwrite existing package without prompt",
Destination: &flags.Force,
},
&cli.BoolFlag{
Name: "local",
Aliases: []string{"l"},
Usage: "local mode",
Destination: &flags.Local,
},
},
Before: localOrToken,
Action: doAdd,
}
var vPattern = regexp.MustCompile(`v\d+$`)
func doAdd(ctx *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
func doAdd(_ *cli.Context) error {
goGetQuestion := &survey.Input{
Message: "Package go-get import",
}
@ -58,16 +65,26 @@ func doAdd(ctx *cli.Context) error {
return err
}
pkg := config.Package{
pkg := gpm.Package{
Name: nameAnswer,
Import: goGetAnswer,
}
cfg.AddPackages(ctx.Bool("force"), pkg)
if err := cfg.Save(); err != nil {
return err
if flags.Local {
db, err := database.Load(flags.Database)
if err != nil {
return err
}
if err := db.PutPackage(pkg); err != nil {
return err
}
} else {
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
if err := client.Add(context.Background(), pkg); err != nil {
return err
}
}
beaver.Infof("Added `%s` to local gpm.", nameAnswer)
beaver.Infof("Added %s", yellow.Format(nameAnswer))
return nil
}

100
cmd/cmd.go

@ -1,18 +1,104 @@
package cmd
import (
"go.jolheiser.com/gpm/config"
"context"
"errors"
"os"
"path/filepath"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"go.jolheiser.com/gpm/router"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver/color"
)
func NewFlags(cfg *config.Config) []cli.Flag {
return []cli.Flag{
var yellow = color.FgYellow
func New() *cli.App {
app := cli.NewApp()
app.Name = "gpm"
app.Usage = "Go Package Manager"
app.Version = router.Version
app.Commands = []*cli.Command{
&Add,
&Get,
&List,
&Remove,
&Search,
&Server,
&Update,
}
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Usage: "gpm server to use",
Value: gpm.DefaultServer,
EnvVars: []string{"GPM_SERVER"},
Destination: &flags.Server,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Usage: "gpm auth token to use",
DefaultText: "${GPM_TOKEN}",
EnvVars: []string{"GPM_TOKEN"},
Destination: &flags.Token,
},
&cli.StringFlag{
Name: "url",
Aliases: []string{"u"},
Usage: "gpm server to use",
Value: cfg.GPMURL,
Name: "database",
Aliases: []string{"d"},
Usage: "path to gpm database for server",
Value: dbPath(),
DefaultText: "`${HOME}/gpm.db` or `${BINPATH}/gpm.db`",
EnvVars: []string{"GPM_DATABASE"},
Destination: &flags.Database,
},
}
return app
}
func dbPath() string {
fn := "gpm.db"
home, err := os.UserHomeDir()
if err != nil {
bin, err := os.Executable()
if err != nil {
return fn
}
return filepath.Join(filepath.Dir(bin), fn)
}
return filepath.Join(home, fn)
}
func localOrToken(_ *cli.Context) error {
if flags.Local && flags.Token == "" {
return errors.New("server interaaction requires --token")
}
return nil
}
func listPackages() ([]gpm.Package, error) {
var pkgs []gpm.Package
if flags.Local {
db, err := database.Load(flags.Database)
if err != nil {
return pkgs, err
}
pkgs, err = db.Packages()
if err != nil {
return pkgs, err
}
} else {
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
info, err := client.Info(context.Background())
if err != nil {
return pkgs, err
}
pkgs = info.Packages
}
return pkgs, nil
}

41
cmd/config.go

@ -1,41 +0,0 @@
package cmd
import (
"go.jolheiser.com/gpm/config"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Config = cli.Command{
Name: "config",
Aliases: []string{"cfg"},
Usage: "Configure local gpm",
Action: doConfig,
}
func doConfig(_ *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
urlQuestion := &survey.Input{
Message: "gpm URL",
Default: cfg.GPMURL,
}
var urlAnswer string
if err := survey.AskOne(urlQuestion, &urlAnswer); err != nil {
return err
}
cfg.GPMURL = urlAnswer
if err := cfg.Save(); err != nil {
return err
}
beaver.Info("gpm URL saved!")
return nil
}

30
cmd/export.go

@ -1,30 +0,0 @@
package cmd
import (
"fmt"
"go.jolheiser.com/gpm/config"
"github.com/urfave/cli/v2"
)
var Export = cli.Command{
Name: "export",
Usage: "Export JSON for local packages",
Action: doExport,
}
func doExport(_ *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
export, err := cfg.Export()
if err != nil {
return err
}
fmt.Println(export)
return nil
}

11
cmd/flags/flags.go

@ -0,0 +1,11 @@
package flags
var (
Server string
Token string
Database string
Local bool
Force bool
Port int
)

78
cmd/get.go

@ -1,15 +1,13 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"context"
"os"
"os/exec"
"strings"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
@ -17,27 +15,13 @@ import (
)
var Get = cli.Command{
Name: "get",
Usage: "Get package(s)",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "ignore-local",
Usage: "Ignore local packages",
},
&cli.BoolFlag{
Name: "offline",
Usage: "Offline mode, return error instead of querying server",
},
},
Action: doGet,
Name: "get",
Aliases: []string{"g"},
Usage: "Get package(s)",
Action: doGet,
}
func doGet(ctx *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
pkgs := ctx.Args().Slice()
if len(pkgs) == 0 {
pkgsQuestion := &survey.Multiline{
@ -52,27 +36,16 @@ func doGet(ctx *cli.Context) error {
pkgs = strings.Split(pkgsAnswer, "\n")
}
local := cfg.Packages.Map()
for _, pkg := range pkgs {
var url string
if u, ok := local[pkg]; ok && !ctx.Bool("ignore-local") {
url = u.Import
} else if !ctx.Bool("offline") {
u, err := queryServer(ctx.String("url"), pkg)
if err != nil {
beaver.Error(err)
continue
}
url = u
}
if url == "" {
beaver.Errorf("no package found for `%s`", pkg)
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
for _, p := range pkgs {
pkg, err := client.Get(context.Background(), p)
if err != nil {
beaver.Error(err)
continue
}
beaver.Infof("getting `%s`...", pkg)
if err := goGet(url); err != nil {
if err := goGet(pkg.Import); err != nil {
beaver.Error(err)
}
}
@ -80,31 +53,6 @@ func doGet(ctx *cli.Context) error {
return nil
}
func queryServer(server, name string) (string, error) {
endpoint := fmt.Sprintf("%s/package/%s", server, name)
resp, err := http.Get(endpoint)
if err != nil {
return "", fmt.Errorf("could not query server at `%s`", endpoint)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("could not find server package for `%s`", name)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
var pkg config.Package
if err := json.Unmarshal(body, &pkg); err != nil {
return "", err
}
return pkg.Import, nil
}
func goGet(url string) error {
cmd := exec.Command("go", "get", url)
cmd.Stdout = os.Stdout

79
cmd/import.go

@ -1,79 +0,0 @@
package cmd
import (
"encoding/json"
"errors"
"io"
"net/http"
"os"
"strings"
"go.jolheiser.com/gpm/config"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Import = cli.Command{
Name: "import",
Usage: "Import JSON for local packages",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Overwrite any existing packages without prompt",
},
},
Action: doImport,
}
func doImport(ctx *cli.Context) error {
cfg, err := config.Load()
if err != nil {
return err
}
if ctx.NArg() == 0 {
return errors.New("must point to either a JSON file or gpm server export endpoint")
}
arg := ctx.Args().First()
isJSON := strings.HasSuffix(arg, ".json")
isHTTP := strings.HasPrefix(arg, "http")
if !isJSON && !isHTTP {
return errors.New("must point to either a JSON file or gpm server export endpoint")
}
var data []byte
if isJSON {
data, err = os.ReadFile(arg)
if err != nil {
return err
}
} else if isHTTP {
resp, err := http.Get(arg)
if err != nil {
return err
}
data, err = io.ReadAll(resp.Body)
if err != nil {
return err
}
defer resp.Body.Close()
}
var importPkgs []config.Package
if err := json.Unmarshal(data, &importPkgs); err != nil {
return err
}
cfg.AddPackages(ctx.Bool("force"), importPkgs...)
if err := cfg.Save(); err != nil {
return err
}
beaver.Info("Import complete")
return nil
}

18
cmd/list.go

@ -1,27 +1,29 @@
package cmd
import (
"go.jolheiser.com/gpm/config"
"fmt"
"os"
"text/tabwriter"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var List = cli.Command{
Name: "list",
Aliases: []string{"l"},
Aliases: []string{"ls", "l"},
Usage: "List local packages",
Action: doList,
}
func doList(_ *cli.Context) error {
cfg, err := config.Load()
pkgs, err := listPackages()
if err != nil {
return err
}
for _, pkg := range cfg.Packages {
beaver.Infof("%s -> %s", pkg.Name, pkg.Import)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
for _, pkg := range pkgs {
s := fmt.Sprintf("%s\t%s\n", pkg.Name, pkg.Import)
_, _ = w.Write([]byte(s))
}
return nil
return w.Flush()
}

67
cmd/remove.go

@ -1,10 +1,11 @@
package cmd
import (
"fmt"
"strings"
"context"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
@ -15,49 +16,53 @@ var Remove = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove package(s)",
Before: localOrToken,
Action: doRemove,
}
func doRemove(_ *cli.Context) error {
cfg, err := config.Load()
pkgs, err := listPackages()
if err != nil {
return err
}
pkgQuestion := &survey.Input{
Message: "Package name",
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]gpm.Package)
for idx, pkg := range pkgs {
pkgSlice[idx] = pkg.Name
pkgMap[pkg.Name] = pkg
}
var pkgAnswer string
if err := survey.AskOne(pkgQuestion, &pkgAnswer); err != nil {
pkgQuestion := &survey.Select{
Message: "Select package to remove",
Options: pkgSlice,
}
var pkgName string
if err := survey.AskOne(pkgQuestion, &pkgName); err != nil {
return err
}
for idx, p := range cfg.Packages {
if strings.EqualFold(p.Name, pkgAnswer) {
confirm := &survey.Confirm{
Message: fmt.Sprintf("Are you sure you want to remove %s (%s) ?", p.Name, p.Import),
Default: false,
}
var answer bool
if err := survey.AskOne(confirm, &answer); err != nil {
return err
}
if answer {
cfg.Packages = append(cfg.Packages[:idx], cfg.Packages[idx+1:]...)
if err := cfg.Save(); err != nil {
return err
}
beaver.Infof("Removed `%s` from local gpm.", p.Name)
break
}
beaver.Infof("Did not remove `%s` from local gpm.", p.Name)
break
pkg := gpm.Package{
Name: pkgName,
Import: pkgMap[pkgName].Import,
}
if flags.Local {
db, err := database.Load(flags.Database)
if err != nil {
return err
}
if err := db.RemovePackage(pkg.Name); err != nil {
return err
}
} else {
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
if err := client.Remove(context.Background(), pkg); err != nil {
return err
}
}
beaver.Infof("Removed %s", yellow.Format(pkgName))
return nil
}

57
cmd/search.go

@ -1,12 +1,7 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
@ -17,35 +12,25 @@ var Search = cli.Command{
Name: "search",
Aliases: []string{"s"},
Usage: "Search packages",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "local",
Usage: "Search locally",
},
},
Action: doSearch,
Action: doSearch,
}
func doSearch(ctx *cli.Context) error {
cfg, err := config.Load()
func doSearch(_ *cli.Context) error {
pkgs, err := listPackages()
if err != nil {
return err
}
packageMap := cfg.Packages.Map()
packageSlice := cfg.Packages.Slice()
if !ctx.Bool("local") {
export, err := queryExport(ctx.String("url"))
if err != nil {
return err
}
packageMap = export.Map()
packageSlice = export.Slice()
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]gpm.Package)
for idx, pkg := range pkgs {
pkgSlice[idx] = pkg.Name
pkgMap[pkg.Name] = pkg
}
q := &survey.MultiSelect{
Message: "Select packages",
Options: packageSlice,
Options: pkgSlice,
}
var a []string
@ -54,7 +39,7 @@ func doSearch(ctx *cli.Context) error {
}
for _, name := range a {
pkg, ok := packageMap[name]
pkg, ok := pkgMap[name]
if !ok {
beaver.Errorf("could not find package for `%s`", name)
continue
@ -67,23 +52,3 @@ func doSearch(ctx *cli.Context) error {
return nil
}
func queryExport(server string) (config.Packages, error) {
resp, err := http.Get(fmt.Sprintf("%s/export", server))
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var importPkgs config.Packages
if err := json.Unmarshal(data, &importPkgs); err != nil {
return nil, err
}
return importPkgs, nil
}

35
cmd/server.go

@ -1,10 +1,12 @@
package cmd
import (
"errors"
"fmt"
"net/http"
"go.jolheiser.com/gpm/config"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/router"
"github.com/urfave/cli/v2"
@ -12,27 +14,34 @@ import (
)
var Server = cli.Command{
Name: "server",
Usage: "Start the gpm server",
Name: "server",
Aliases: []string{"web"},
Usage: "Start the gpm server",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "Port to run the gpm server on",
Value: "3333",
&cli.IntFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "Port to run the gpm server on",
Value: 3333,
EnvVars: []string{"GPM_PORT"},
Destination: &flags.Port,
},
},
Action: doServer,
}
func doServer(ctx *cli.Context) error {
cfg, err := config.Load()
func doServer(_ *cli.Context) error {
if flags.Token == "" {
return errors.New("gpm server requires --token")
}
db, err := database.Load(flags.Database)
if err != nil {
return err
beaver.Fatalf("could not load database at %s: %v", flags.Database, err)
}
beaver.Infof("Running gpm server at http://localhost:%s", ctx.String("port"))
if err := http.ListenAndServe(fmt.Sprintf(":%s", ctx.String("port")), router.New(cfg)); err != nil {
beaver.Infof("Running gpm server at http://localhost:%d", flags.Port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", flags.Port), router.New(flags.Token, db)); err != nil {
return err
}
return nil

86
cmd/update.go

@ -0,0 +1,86 @@
package cmd
import (
"context"
"go.jolheiser.com/gpm/cmd/flags"
"go.jolheiser.com/gpm/database"
"go.jolheiser.com/gpm/go-gpm"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Update = cli.Command{
Name: "update",
Aliases: []string{"u"},
Usage: "Update a package",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "local",
Aliases: []string{"l"},
Usage: "local mode",
Destination: &flags.Local,
},
},
Before: localOrToken,
Action: doUpdate,
}
func doUpdate(_ *cli.Context) error {
pkgs, err := listPackages()
if err != nil {
return err
}
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]gpm.Package)
for idx, pkg := range pkgs {
pkgSlice[idx] = pkg.Name
pkgMap[pkg.Name] = pkg
}
pkgQuestion := &survey.Select{
Message: "Select package to update",
Options: pkgSlice,
}
var pkgName string
if err := survey.AskOne(pkgQuestion, &pkgName); err != nil {
return err
}
importQuestion := &survey.Input{
Message: "New import path",
Default: pkgMap[pkgName].Import,
}
var importPath string
if err := survey.AskOne(importQuestion, &importPath); err != nil {
return err
}
pkg := gpm.Package{
Name: pkgName,
Import: importPath,
}
if flags.Local {
db, err := database.Load(flags.Database)
if err != nil {
return err
}
if err := db.PutPackage(pkg); err != nil {
return err
}
} else {
client := gpm.New(flags.Token, gpm.WithServer(flags.Server))
if err := client.Update(context.Background(), pkg); err != nil {
return err
}
}
beaver.Infof("Updated %s", yellow.Format(pkgName))
return nil
}

146
config/config.go

@ -1,146 +0,0 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/pelletier/go-toml"
"go.jolheiser.com/beaver"
)
var Version = "develop"
type Config struct {
path string
GPMURL string `toml:"gpm-url" json:"gpm_url"`
Packages Packages `toml:"package" json:"packages"`
}
type Package struct {
Name string `toml:"name" json:"name"`
Import string `toml:"import" json:"import"`
}
type Packages []Package
func Load() (*Config, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("could not get user home dir: %v", err)
}
home = path.Join(home, ".gpm")
homeEnv := os.Getenv("GPM_HOME")
if homeEnv != "" {
home = homeEnv
}
configPath := path.Join(home, "gpm.toml")
configEnv := os.Getenv("GPM_CONFIG")
if configEnv != "" {
configPath = configEnv
}
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := os.MkdirAll(path.Dir(configPath), os.ModePerm); err != nil {
return nil, fmt.Errorf("could not create gpm home: %v", err)
}
if _, err := os.Create(configPath); err != nil {
return nil, fmt.Errorf("could not create gpm config: %v", err)
}
}
var cfg Config
tree, err := toml.LoadFile(configPath)
if err != nil {
return nil, fmt.Errorf("could not decode gpm config: %v", err)
}
if err = tree.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("could not unmarshal config: %v", err)
}
dupe := make(map[string]bool)
for _, pkg := range cfg.Packages {
name := strings.ToLower(pkg.Name)
if ok := dupe[name]; ok {
return nil, fmt.Errorf("duplicate package for %s", pkg.Name)
}
dupe[name] = true
}
cfg.path = configPath
return &cfg, nil
}
func (c *Config) Save() error {
fi, err := os.Create(c.path)
if err != nil {
return err
}
defer fi.Close()
if err := toml.NewEncoder(fi).Encode(c); err != nil {
return err
}
return nil
}
func (c *Config) Export() (string, error) {
data, err := json.Marshal(c.Packages)
return string(data), err
}
func (p Packages) Slice() []string {
pkgs := make([]string, len(p))
for idx, pkg := range p {
pkgs[idx] = fmt.Sprintf("%s (%s)", pkg.Name, pkg.Import)
}
return pkgs
}
func (p Packages) Map() map[string]Package {
pkgs := make(map[string]Package)
for _, pkg := range p {
pkgs[pkg.Name] = pkg
}
return pkgs
}
func (c *Config) AddPackages(force bool, pkgs ...Package) {
for _, pkg := range pkgs {
for idx, p := range c.Packages {
if strings.EqualFold(p.Name, pkg.Name) {
if force {
c.Packages[idx] = pkg
break
}
forceQuestion := &survey.Confirm{
Message: fmt.Sprintf("Package `%s` (%s) already exists. Overwrite with `%s`?", p.Name, p.Import, p.Import),
Default: false,
}
var forceAnswer bool
if err := survey.AskOne(forceQuestion, &forceAnswer); err != nil {
beaver.Error(err)
break
}
if !forceAnswer {
beaver.Errorf("leaving package `%s` as-is", pkg.Name)
break
}
c.Packages[idx] = pkg
break
}
}
c.Packages = append(c.Packages, pkg)
}
}

79
database/database.go

@ -0,0 +1,79 @@
package database
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"go.jolheiser.com/gpm/go-gpm"
"go.etcd.io/bbolt"
)
var packageBucket = []byte("packages")
type Database struct {
db *bbolt.DB
}
func Load(dbPath string) (*Database, error) {
if err := os.MkdirAll(filepath.Dir(dbPath), os.ModePerm); err != nil {
return nil, err
}
db, err := bbolt.Open(dbPath, os.ModePerm, nil)
if err != nil {
return nil, err
}
return &Database{
db: db,
}, db.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(packageBucket)
return err
})
}
func (d *Database) Package(name string) (gpm.Package, error) {
var pkg gpm.Package
data, err := d.PackageJSON(name)
if err != nil {
return pkg, err
}
return pkg, json.NewDecoder(bytes.NewReader(data)).Decode(&pkg)
}
func (d *Database) PackageJSON(name string) (pkg []byte, err error) {
return pkg, d.db.View(func(tx *bbolt.Tx) error {
pkg = tx.Bucket(packageBucket).Get([]byte(name))
return nil
})
}
func (d *Database) Packages() (pkgs []gpm.Package, err error) {
return pkgs, d.db.View(func(tx *bbolt.Tx) error {
return tx.Bucket(packageBucket).ForEach(func(key, val []byte) error {
var pkg gpm.Package
if err := json.NewDecoder(bytes.NewReader(val)).Decode(&pkg); err != nil {
return err
}
pkgs = append(pkgs, pkg)
return nil
})
})
}
func (d *Database) PutPackage(pkg gpm.Package) error {
return d.db.Update(func(tx *bbolt.Tx) error {
data, err := json.Marshal(pkg)
if err != nil {
return err
}
return tx.Bucket(packageBucket).Put([]byte(pkg.Name), data)
})
}
func (d *Database) RemovePackage(name string) error {
return d.db.Update(func(tx *bbolt.Tx) error {
return tx.Bucket(packageBucket).Delete([]byte(name))
})
}

87
database/database_test.go

@ -0,0 +1,87 @@
package database
import (
"os"
"path/filepath"
"testing"
"go.jolheiser.com/gpm/go-gpm"
)
var db *Database
func TestMain(m *testing.M) {
tmp, err := os.MkdirTemp(os.TempDir(), "gpm")
if err != nil {
panic(err)
}
dbPath := filepath.Join(tmp, "gpm.db")
db, err = Load(dbPath)
if err != nil {
panic(err)
}
code := m.Run()
// Cleanup
if err := os.RemoveAll(tmp); err != nil {
panic(err)
}
os.Exit(code)
}
func TestPackage(t *testing.T) {
// Does not exist
_, err := db.Package("test")
if err == nil {
t.Log("test package should not exist")
t.FailNow()
}
// Add
pkg := gpm.Package{
Name: "test",
Import: "gitea.com/test/testing",
}
err = db.PutPackage(pkg)
if err != nil {
t.Logf("could not put test package: %v\n", err)
t.FailNow()
}
// Update
pkg.Import = "gitea.com/testing/test"
err = db.PutPackage(pkg)
if err != nil {
t.Logf("could not put test package: %v\n", err)
t.FailNow()
}
// Check
p, err := db.Package("test")
if err != nil {
t.Logf("should find test package: %v\n", err)
t.FailNow()
}
if p.Import != pkg.Import {
t.Logf("test package did not match update:\n\texpected: %s\n\t got: %s\n", pkg.Import, p.Import)
t.FailNow()
}
// Remove
err = db.RemovePackage("test")
if err != nil {
t.Log("could not remove test package")
t.FailNow()
}
// Check
_, err = db.Package("test")
if err == nil {
t.Log("test package should not exist after being removed")
t.FailNow()
}
}

12
docker/Dockerfile

@ -0,0 +1,12 @@
FROM golang:1.16-alpine as builder
RUN apk --no-cache add build-base git
COPY . /app
WORKDIR /app
RUN make build
FROM alpine:latest
LABEL maintainer="john.olheiser@gmail.com"
COPY --from=builder /app/gpm gpm
EXPOSE 3333
ENV GPM_TOKEN=""
ENTRYPOINT exec gpm --token $GPM_TOKEN server

9
docker/docker-compose.yml

@ -0,0 +1,9 @@
version: "2"
services:
vanity:
image: jolheiser/gpm:latest
environment:
- GPM_TOKEN=<token>
restart: always
ports:
- "80:3333"

33
docs.go

@ -0,0 +1,33 @@
//+build docs
package main
import (
"os"
"strings"
"go.jolheiser.com/gpm/cmd"
)
func main() {
app := cmd.New()
md, err := app.ToMarkdown()
if err != nil {
panic(err)
}
// FIXME Why is this not fixed yet??
md = md[strings.Index(md, "#"):]
fi, err := os.Create("DOCS.md")
if err != nil {
panic(err)
}
if _, err := fi.WriteString(md); err != nil {
panic(err)
}
if err := fi.Close(); err != nil {
panic(err)
}
}

59
go-gpm/client.go

@ -0,0 +1,59 @@
package gpm
import (
"context"
"io"
"net/http"
"strings"
)
const (
DefaultServer = "https://gpm.jolheiser.com"
TokenHeader = "X-GPM-Token"
)
// Client is a gpm client
type Client struct {
token string
server string
http *http.Client
}
// New returns a new Client
func New(token string, opts ...ClientOption) *Client {
c := &Client{
token: token,
server: DefaultServer,
http: http.DefaultClient,
}
for _, opt := range opts {
opt(c)
}
return c
}
// ClientOption is an option for a Client
type ClientOption func(*Client)
// WithHTTP sets the http.Client for a Client
func WithHTTP(client *http.Client) ClientOption {
return func(c *Client) {
c.http = client
}
}
// WithServer sets the gpm server for a Client
func WithServer(server string) ClientOption {
return func(c *Client) {
c.server = strings.TrimSuffix(server, "/")
}
}
func (c *Client) newRequest(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
req.Header.Set(TokenHeader, c.token)
return req, nil
}

3
go-gpm/go.mod

@ -0,0 +1,3 @@
module go.jolheiser.com/gpm/go-gpm
go 1.16

207
go-gpm/gpm_test.go

@ -0,0 +1,207 @@
package gpm
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
var (
server *httptest.Server
token = "TestingLibrary"
version = "GPMTest"
packages = []Package{
{
Name: "test1",
Import: "gitea.com/test/testing",
},
}
)
func TestMain(m *testing.M) {
server = httptest.NewServer(http.HandlerFunc(testServer))
os.Exit(m.Run())
}
func TestClient(t *testing.T) {
ctx := context.Background()
client := New("", WithServer(server.URL))
// Info
checkInfo(t, client, 1)
pkg1 := Package{
Name: "test1",
Import: "gitea.com/test/testing",
}
pkg2 := Package{
Name: "test2",
Import: "gitea.com/testing/test",
}
// Add (without token)
if err := client.Add(ctx, pkg1); err == nil {
t.Log("adding without token should fail")
t.Fail()
}
// Add (with token)
client = New(token, WithServer(server.URL))
checkAdd(t, client, pkg1, pkg2)
// Info (after second package)
checkInfo(t, client, 2)
// Check package
checkGet(t, client, pkg2)
// Update package
checkUpdate(t, client, pkg1)
// Remove
checkRemove(t, client, pkg1)
// Info (final)
checkInfo(t, client, 1)
}
func checkInfo(t *testing.T, client *Client, numPackages int) {
info, err := client.Info(context.Background())
if err != nil {
t.Logf("info should not return error: %v\n", err)
t.Fail()
}
if info.Version != version || info.NumPackages != numPackages {
t.Log("info did not match expected")
t.Fail()
}
}
func checkGet(t *testing.T, client *Client, pkg Package) {
ctx := context.Background()
_, err := client.Get(ctx, "test3")
if err == nil {
t.Log("should not be able to get invalid package")
t.Fail()
}
// Check valid package
p, err := client.Get(ctx, "test2")
if err != nil {
t.Logf("should not be able to get invalid package: %v\n", err)
t.Fail()
}
if p != pkg {
t.Log("valid package should match pkg")
t.Fail()
}
}
func checkAdd(t *testing.T, client *Client, pkg1, pkg2 Package) {
ctx := context.Background()
if err := client.Add(ctx, pkg2); err != nil {
t.Logf("pkg2 should be added: %v\n", err)
t.Fail()
}
// Duplicate package
if err := client.Add(ctx, pkg1); err == nil {
t.Log("pkg1 should already exist")
t.Fail()
}
}
func checkUpdate(t *testing.T, client *Client, pkg Package) {
ctx := context.Background()
// Update invalid package
if err := client.Update(ctx, Package{Name: "test4", Import: "gitea.com/invalid"}); err == nil {
t.Log("should not be able to update invalid package")
t.Fail()
}
// Update valid package
pkg.Import = "gitea.com/tester/testing"
if err := client.Update(ctx, pkg); err != nil {
t.Logf("should be able to update valid package: %v\n", err)
t.Fail()
}
}
func checkRemove(t *testing.T, client *Client, pkg Package) {
ctx := context.Background()
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package: %v\n", err)
t.Fail()
}
// Remove (idempotent)
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package idempotently: %v\n", err)
t.Fail()
}
}
func testServer(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
switch r.Method {
case http.MethodGet:
resp := Info{
Version: version,
NumPackages: len(packages),
Packages: packages,
}
_ = json.NewEncoder(w).Encode(resp)
case http.MethodPost, http.MethodPatch, http.MethodDelete:
if r.Header.Get(TokenHeader) != token {
w.WriteHeader(http.StatusUnauthorized)
return
}
var pkg Package
if err := json.NewDecoder(r.Body).Decode(&pkg); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
switch r.Method {
case http.MethodPost:
for _, p := range packages {
if p.Name == pkg.Name {
w.WriteHeader(http.StatusConflict)
return
}
}
packages = append(packages, pkg)
w.WriteHeader(http.Status