Skip to content

Commit

Permalink
Add initial setup.
Browse files Browse the repository at this point in the history
  • Loading branch information
danilvpetrov committed Mar 31, 2023
0 parents commit 055dbb0
Show file tree
Hide file tree
Showing 15 changed files with 988 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/artifacts/
/.makefiles/
.DS_Store
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

-include .makefiles/Makefile
-include .makefiles/pkg/go/v1/Makefile

.makefiles/%:
@curl -sfL https://makefiles.dev/v1 | bash /dev/stdin "$@"

################################################################################

.PHONY: run
run: $(GO_DEBUG_DIR)/gobump
$< $(args)
69 changes: 69 additions & 0 deletions cmd/gobump/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"flag"
"fmt"
"os"
"os/exec"
)

func run() error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("cannot determine current directory: %s", err)
}

var goTidy bool
flag.BoolVar(&goTidy, "t", false, "run 'go mod tidy' command at the end of path update")
flag.Usage = usage
flag.Parse()

modules, err := parseGoModFile(wd)
if err != nil {
return err
}

newPath := flag.Arg(0)

if err := checkModulePath(newPath, modules); err != nil {
return err
}

if err := walkDir(wd, newPath); err != nil {
return err
}

if goTidy {
if err := runTidy(); err != nil {
return err
}
}

fmt.Println("done")
return nil
}

func runTidy() error {
fmt.Println("running 'go mod tidy'...")

out, err := exec.Command("go", "mod", "tidy").CombinedOutput()
if err != nil {
os.Stderr.Write(out)
return fmt.Errorf("error running 'go mod tidy': %v", err)
}

os.Stdout.Write(out)
return nil
}

func usage() {
fmt.Fprintf(os.Stderr, "usage: gobump [flags] <new go module path>\n")
flag.PrintDefaults()
}

func main() {
if err := run(); err != nil {
fmt.Printf("error: %s\n", err)
os.Exit(1)
}
}
49 changes: 49 additions & 0 deletions cmd/gobump/transformers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

import (
"bytes"
"io"
"os"

"github.com/danilvpetrov/gobump/transformers"
)

// runTransformers runs transformers against the file. If transformers performed
// conflicting changes to the file, the last transformer always takes precedence.
func runTransformers(file string, tt ...transformers.Transformer) error {
f, err := os.OpenFile(file, os.O_RDWR, 0644)
if err != nil {
return err
}
defer f.Close()

var buf bytes.Buffer
for _, t := range tt {
ok, err := t(f, &buf)
if err != nil {
return err
}
if !ok {
continue
}

if err := f.Truncate(0); err != nil {
return err
}

if _, err := f.Seek(0, io.SeekStart); err != nil {
return err
}
if _, err := buf.WriteTo(f); err != nil {
return err
}

if _, err := f.Seek(0, io.SeekStart); err != nil {
return err
}

buf.Reset()
}

return nil
}
60 changes: 60 additions & 0 deletions cmd/gobump/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"fmt"
"os"
"path/filepath"

"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
)

// parseGoModFile parses "go.mod" file and returns the main module path and all
// direct dependency module paths declared with "require" directive.
//
// For more info refer to this resource: https://go.dev/doc/modules/gomod-ref.
func parseGoModFile(moduleDir string) ([]string, error) {
gomodPath := filepath.Join(moduleDir, "go.mod")
if fi, err := os.Stat(gomodPath); err != nil || fi.IsDir() {
return nil, fmt.Errorf("directory %q is not a valid go module", moduleDir)
}

bb, err := os.ReadFile(gomodPath)
if err != nil {
return nil, err
}

mf, err := modfile.Parse(gomodPath, bb, nil)
if err != nil {
return nil, fmt.Errorf("invalid go.mod file: %s", err)
}

modules := []string{mf.Module.Mod.Path}
for _, req := range mf.Require {
if !req.Indirect {
modules = append(modules, req.Mod.Path)
}
}

return modules, nil
}

// checkModulePath checks the module path for its validity and relation to any
// of the available module paths.
func checkModulePath(modulePath string, availableModules []string) error {
if err := module.CheckPath(modulePath); err != nil {
return fmt.Errorf("invalid module path %q: %w", modulePath, err)
}

pnew, _, _ := module.SplitPathVersion(modulePath)
for _, am := range availableModules {
if pold, _, _ := module.SplitPathVersion(am); pold == pnew {
return nil
}
}

return fmt.Errorf(
"failed to find a corresponding module for %q",
modulePath,
)
}
50 changes: 50 additions & 0 deletions cmd/gobump/walkdir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/danilvpetrov/gobump/transformers/gofile"
"github.com/danilvpetrov/gobump/transformers/gomodfile"
)

// walkDir walks a given directory and runs the relevant file transformers that
// apply the new go module path.
func walkDir(dir, newPath string) error {
return filepath.WalkDir(
dir,
func(path string, d fs.DirEntry, err error) error {
// Do not walk into "testdata" directories.
if d.IsDir() && d.Name() == "testdata" {
return filepath.SkipDir
}

if d.IsDir() {
return nil
}

// Ignore files starting with "." or "_".
if strings.HasPrefix(d.Name(), ".") || strings.HasPrefix(d.Name(), "_") {
return nil
}

if d.Type()&fs.ModeSymlink != 0 {
// Ignore symlinks pointing to directories.
if t, err := os.Stat(path); err == nil && t.IsDir() {
return nil
}
}

switch {
default:
return nil
case filepath.Ext(path) == ".go":
return runTransformers(path, gofile.UpdateImports(newPath))
case filepath.Base(path) == "go.mod":
return runTransformers(path, gomodfile.UpdateModulePath(newPath))
}
},
)
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/danilvpetrov/gobump

go 1.20

require (
golang.org/x/mod v0.9.0
golang.org/x/tools v0.7.0
)
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
58 changes: 58 additions & 0 deletions transformers/gofile/imports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package gofile

import (
"go/ast"
"go/format"
"go/parser"
"go/token"
"io"
"strconv"

"github.com/danilvpetrov/gobump/transformers"
"github.com/danilvpetrov/gobump/transformers/internal/pathx"
"golang.org/x/tools/go/ast/astutil"
)

// UpdateImports replaces the import path in a .go file with a new import path
// if the latter is applicable.
func UpdateImports(
newImportPath string,
) transformers.Transformer {
return func(in io.Reader, out io.Writer) (ok bool, err error) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", in, parser.ParseComments)
if err != nil {
return false, err
}

var rewrote bool
for _, i := range f.Imports {
p := importPath(i)
np, ok, err := pathx.UpdateImportPath(newImportPath, p)
if err != nil {
return false, err
}
if ok {
rewrote = astutil.RewriteImport(fset, f, p, np)
}
}
if !rewrote {
return false, nil
}

if err := format.Node(out, fset, f); err != nil {
return false, err
}

return true, nil
}
}

func importPath(i *ast.ImportSpec) string {
p, err := strconv.Unquote(i.Path.Value)
if err != nil {
return ""
}

return p
}
Loading

0 comments on commit 055dbb0

Please sign in to comment.