Skip to content

Commit

Permalink
Add gomod-sync
Browse files Browse the repository at this point in the history
  • Loading branch information
andrerfcsantos committed Nov 21, 2021
1 parent 3d87554 commit 1ca7f2a
Show file tree
Hide file tree
Showing 17 changed files with 1,089 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/gomod-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: go.mod check

on:
workflow_dispatch:
push:
branches:
- main
- master
pull_request:

jobs:
check:
name: go.mod check
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- uses: actions/setup-go@v2
with:
go-version: 1.17

- name: Check go.mod files
shell: bash
run: |
cd gomod-sync && go build
./gomod-sync check --goversion 1.16 --exercises ../exercises
15 changes: 15 additions & 0 deletions gomod-sync/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
65 changes: 65 additions & 0 deletions gomod-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# gomod-sync

Utility tool to check and update `go.mod` versions.

## Flags

- `-c, --config config_file`

path to the JSON configuration file. (default "config.json")

- `-e, --exercises exercises_path`

path to the exercises folder. go.mod files will be recursively searched inside this directory. (default "../exercises")
- `-v, --goversion target_version`

target go version that all go.mod files are expected to have. This will be used to check if the go.mod files are in the expected version in case of the check command, and to update all go.mod files to this version in the case of the update command.

- `-h, --help`

help for gomod-sync

## Commands

- `gomod-sync check -v target_version [-e exercises_path] [-c config_file]`

checks if all go.mod files are in the target version

- `gomod-sync completion`

generate the autocompletion script for the specified shell
- `gomod-sync help`

Help about any command
- `gomod-sync list [-e exercises_path]`

list `go.mod` files and the Go version they specify
- `gomod-sync update -v target_version [-e exercises_path] [-c config_file]`

updates `go.mod` files to the target version

## Configuration file

Besides the `-v, --goversion` flag, it is also possible to specify the expected go versions for the `go.mod` files in a JSON configuration file.
This file can be given to the program with the `-c, --config file` flag.

With a configuration file, in addition to define a default Go version all exercises' `go.mod` must have,
it's also possible to configure different versions for different exercises. This can be useful if a particular exercise
needs a superior version of Go than the default.

This an example of such configuration file:

```json
{
"default": "1.16",
"exceptions": [
{
"exercise": "strain",
"version": "1.18"
}
]
}
```

With such configuration, all `go.mod` files will be expected to have the `1.16` version of Go,
except the exercise `strain`, which must have version `1.18` in its `go.mod`.
51 changes: 51 additions & 0 deletions gomod-sync/cmd/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cmd

import (
"fmt"
"os"

"github.com/exercism/go/gomod-sync/gomod"
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(checkCmd)
}

var checkCmd = &cobra.Command{
Use: "check",
Short: "Checks if all go.mod files are in the target version",
PersistentPreRunE: loadConfig,
RunE: func(cmd *cobra.Command, args []string) error {
files, err := gomod.Infos(exercisesPathFlag)
if err != nil {
return err
}

type faultyFile struct {
gomod.Info
ExpectedVersion string
}

var faultyFiles []faultyFile
for _, file := range files {
expectedVersion := versionConfig.ExerciseExpectedVersion(file.ExerciseSlug)
if file.GoVersion != expectedVersion {
fmt.Fprintf(os.Stderr, "%v has version %s, but %s expected - FAIL\n", file.Path, file.GoVersion, expectedVersion)
faultyFiles = append(faultyFiles, faultyFile{Info: file, ExpectedVersion: expectedVersion})
} else {
fmt.Printf("%v has version %s as expected - OK\n", file.Path, file.GoVersion)
}
}

if len(faultyFiles) > 0 {
fmt.Fprintf(os.Stderr, "The following %d go.mod file(s) do not have the correct version set:\n", len(faultyFiles))
for _, file := range faultyFiles {
fmt.Fprintf(os.Stderr,"\t%v has version %s, but %s expected\n", file.Path, file.GoVersion, file.ExpectedVersion)
}
return fmt.Errorf("%d go.mod file(s) are not in the target version", len(faultyFiles))
}

return nil
},
}
30 changes: 30 additions & 0 deletions gomod-sync/cmd/config/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package config

import (
"encoding/json"
"fmt"
"os"
)

// Load loads a configuration from a path to a JSON file.
func Load(file string) (VersionConfig, error) {
var config VersionConfig

f, err := os.Open(file)
if err != nil {
return config, fmt.Errorf("failed to open config file: %v", err)
}
defer f.Close()

err = json.NewDecoder(f).Decode(&config)
if err != nil {
return config, fmt.Errorf("failed to decode config file: %v", err)
}

config.exerciseVersionMap = make(map[string]string)
for _, exception := range config.Exceptions {
config.exerciseVersionMap[exception.Exercise] = exception.Version
}

return config, nil
}
30 changes: 30 additions & 0 deletions gomod-sync/cmd/config/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package config

// VersionConfig represents the version configuration
type VersionConfig struct {
// Default version for the go.mod files
Default string `json:"default"`
// List of exercises and their versions that must not have
// the default version.
Exceptions []ExerciseVersion `json:"exceptions"`
// exerciseVersionMap must hold the same information as Exceptions,
// but in map form. This is a mapping of exercise slugs to their versions.
exerciseVersionMap map[string]string
}

// ExerciseExpectedVersion gives the expected version the go.mod file for
// an exercise should be at. The argument should be the slug of an exercise.
func (vc *VersionConfig) ExerciseExpectedVersion(exercise string) string {
if version, ok := vc.exerciseVersionMap[exercise]; ok {
return version
}

return vc.Default
}

// ExerciseVersion represents the version one particular exercise should
// have in its go.mod file
type ExerciseVersion struct {
Exercise string `json:"exercise"`
Version string `json:"version"`
}
29 changes: 29 additions & 0 deletions gomod-sync/cmd/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cmd

import (
"fmt"

"github.com/exercism/go/gomod-sync/gomod"
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(listCmd)
}

var listCmd = &cobra.Command{
Use: "list",
Short: "List go.mod files and the Go version they specify",
RunE: func(cmd *cobra.Command, args []string) error {
files, err := gomod.Infos(exercisesPathFlag)
if err != nil {
return fmt.Errorf("could not get go.mod information: %w", err)
}

for _, file := range files {
fmt.Printf("%s => %s\n", file.Path, file.GoVersion)
}

return nil
},
}
66 changes: 66 additions & 0 deletions gomod-sync/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cmd

import (
"fmt"
"os"

"github.com/exercism/go/gomod-sync/cmd/config"
"github.com/spf13/cobra"
)

var exercisesPathFlag string
var targetVersionFlag string
var configFileFlag string
var versionConfig config.VersionConfig

func init() {
rootCmd.PersistentFlags().StringVarP(&exercisesPathFlag,
"exercises", "e", "../exercises",
"path to the exercises folder. go.mod files will "+
"be recursively searched inside this directory.")
rootCmd.PersistentFlags().StringVarP(&targetVersionFlag,
"goversion", "v", "",
"target go version that all go.mod files are expected to have. "+
"This will be used to check if the go.mod files are in the expected "+
"version in case of the check command, and to update all go.mod files to this version "+
"in the case of the update command.")
rootCmd.PersistentFlags().StringVarP(&configFileFlag,
"config", "c", "config.json",
"path to the JSON configuration file. ")
}

var rootCmd = &cobra.Command{
Use: "gomod-sync",
Short: "gomod-sync checks and updates the go version for all go.mod files in a path.",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}

func Execute() {
rootCmd.SilenceUsage = true
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func loadConfig(cmd *cobra.Command, args []string) error {
var err error

// Load version config
versionConfig, err = config.Load(configFileFlag)

versionWasGiven := cmd.PersistentFlags().Changed("goversion")

if err != nil && !versionWasGiven {
return fmt.Errorf("failed to load config file and flag --goversion not present: %v", err)
}

// Override config default if version passed via flag
if versionWasGiven {
versionConfig.Default = targetVersionFlag
}

return nil
}
36 changes: 36 additions & 0 deletions gomod-sync/cmd/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cmd

import (
"fmt"

"github.com/exercism/go/gomod-sync/gomod"
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(updateCmd)
}

var updateCmd = &cobra.Command{
Use: "update",
Short: "Updates go.mod files to the target version",
PersistentPreRunE: loadConfig,
RunE: func(cmd *cobra.Command, args []string) error {
files, err := gomod.Infos(exercisesPathFlag)
if err != nil {
return fmt.Errorf("could not get go.mod information: %w", err)
}

for _, file := range files {
expectedVersion := versionConfig.ExerciseExpectedVersion(file.ExerciseSlug)
if file.GoVersion != expectedVersion {
if err := gomod.Update(file.Path, expectedVersion); err != nil {
return fmt.Errorf("failed to update %q: %w", file.Path, err)
}
fmt.Printf("Updated %s: %s => %s\n", file.Path, file.GoVersion, expectedVersion)
}
}

return nil
},
}
4 changes: 4 additions & 0 deletions gomod-sync/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"default": "1.16",
"exceptions": []
}
9 changes: 9 additions & 0 deletions gomod-sync/config.sample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"default": "1.16",
"exceptions": [
{
"exercise": "beer-song",
"version": "1.17"
}
]
}
9 changes: 9 additions & 0 deletions gomod-sync/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/exercism/go/gomod-sync

go 1.17

require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/spf13/cobra v1.2.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
Loading

0 comments on commit 1ca7f2a

Please sign in to comment.