Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for workspaces and external modules in the same repo #199

Merged
merged 17 commits into from
Dec 11, 2023
68 changes: 68 additions & 0 deletions pkg/executors/golang/codegen/mod_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package codegen

import (
"fmt"
"io/fs"
"path"
"strings"

"golang.org/x/exp/slices"
)

type actionsModFile struct {
info fs.FileInfo
content []string
path string

writeFileFunc writeFileFunc
}

func (a *actionsModFile) containsModule(moduleName string) bool {
return slices.ContainsFunc(a.content, func(s string) bool {
return strings.Contains(s, moduleName)
})
}

func (a *actionsModFile) replaceModulePath(rootDir string, module module) {
relativeToActionsModulePath := path.Join(strings.Repeat("../", a.segmentsTo(rootDir)), module.path)

foundReplace := false
for i, line := range a.content {
lineTrim := strings.TrimSpace(line)

if strings.Contains(lineTrim, fmt.Sprintf("replace %s", module.name)) {
a.content[i] = fmt.Sprintf("replace %s => %s", module.name, relativeToActionsModulePath)
foundReplace = true
break
}

}

if !foundReplace {
a.content = append(
a.content,
fmt.Sprintf("\nreplace %s => %s", module.name, relativeToActionsModulePath),
)

}
}

func (a *actionsModFile) segmentsTo(dirPath string) int {
relativeActionsModFilePath := strings.TrimPrefix(
strings.TrimPrefix(
a.path,
dirPath,
),
"/",
)

return strings.Count(relativeActionsModFilePath, "/")
}

func (a *actionsModFile) commit() error {
return a.writeFileFunc(
a.path,
[]byte(strings.Join(a.content, "\n")),
a.info.Mode(),
)
}
247 changes: 247 additions & 0 deletions pkg/executors/golang/codegen/mod_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package codegen

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestContainsModule(t *testing.T) {
t.Parallel()

sut := actionsModFile{
content: []string{
"someModuleName",
"someOtherModuleName",
" someSpacedModule",
"package somePackaged",
"module someModModule",
"require someRequireModule",
},
}

t.Run("contains a module name", func(t *testing.T) {
actual := sut.containsModule("someModuleName")

assert.True(t, actual)
})

t.Run("does not contain module", func(t *testing.T) {
actual := sut.containsModule("someNonExistingModule")

assert.False(t, actual)
})

t.Run("spaced matches", func(t *testing.T) {
actual := sut.containsModule("someSpacedModule")

assert.True(t, actual)
})

t.Run("packaged matches", func(t *testing.T) {
actual := sut.containsModule("somePackaged")

assert.True(t, actual)
})

t.Run("modded module matches", func(t *testing.T) {
actual := sut.containsModule("someModModule")

assert.True(t, actual)
})

t.Run("required module matches", func(t *testing.T) {
actual := sut.containsModule("someRequireModule")

assert.True(t, actual)
})

t.Run("case sensitive doesn't match", func(t *testing.T) {
actual := sut.containsModule("SOMEMODULENAME")

assert.False(t, actual)
})
}

func TestReplaceModulePath(t *testing.T) {
t.Parallel()

createSut := func() actionsModFile {

modFileContent := `module actions

require (
root_workspace v0.0.0
subpackage v0.0.0
othersubpackage v0.0.0
)

go 1.21.4

replace othersubpackage => ../../../../othersubpackage`

return actionsModFile{
path: "/some-path/some-other-path",
content: strings.Split(modFileContent, "\n"),
}

}

t.Run("module matches, not replaced already", func(t *testing.T) {
sut := createSut()

expected := `module actions

require (
root_workspace v0.0.0
subpackage v0.0.0
othersubpackage v0.0.0
)

go 1.21.4

replace othersubpackage => ../../../../othersubpackage

replace subpackage => ../subpackage`

sut.replaceModulePath("some-other-path/newpath", module{
name: "subpackage",
path: "subpackage",
})

assert.Equal(t, expected, strings.Join(sut.content, "\n"))
})

t.Run("module matches, not replaced already, deeper nesting", func(t *testing.T) {
sut := createSut()

expected := `module actions

require (
root_workspace v0.0.0
subpackage v0.0.0
othersubpackage v0.0.0
)

go 1.21.4

replace othersubpackage => ../../../../othersubpackage

replace subpackage => subpackage`

sut.replaceModulePath("/some-path", module{
name: "subpackage",
path: "subpackage",
})

assert.Equal(t, expected, strings.Join(sut.content, "\n"))
})

t.Run("module matches, already replaced already", func(t *testing.T) {
sut := createSut()

expected := `module actions

require (
root_workspace v0.0.0
subpackage v0.0.0
othersubpackage v0.0.0
)

go 1.21.4

replace othersubpackage => ../othersubpackage`

sut.replaceModulePath("some-other-path/newpath", module{
name: "othersubpackage",
path: "othersubpackage",
})

assert.Equal(t, expected, strings.Join(sut.content, "\n"))
})

t.Run("module matches, already replaced already deeper nesting", func(t *testing.T) {
sut := createSut()

expected := `module actions

require (
root_workspace v0.0.0
subpackage v0.0.0
othersubpackage v0.0.0
)

go 1.21.4

replace othersubpackage => othersubpackage`

sut.replaceModulePath("/some-path", module{
name: "othersubpackage",
path: "othersubpackage",
})

assert.Equal(t, expected, strings.Join(sut.content, "\n"))
})
}

func TestSegmentsTo(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
path string
rootDir string
expected int
}{
{
name: "empty",
path: "",
rootDir: "",
expected: 0,
},
{
name: "current dir",
path: "/some-dir/",
rootDir: "/some-dir/",
expected: 0,
},
{
name: "one level",
path: "/some-dir/some-other-dir/",
rootDir: "/some-dir",
expected: 1,
},
{
name: "2 level",
path: "/some-dir/some-other-dir/some-third-dir/",
rootDir: "/some-dir",
expected: 2,
},
{
name: "1 level",
path: "/some-dir/some-other-dir/some-third-dir/",
rootDir: "/some-dir/some-other-dir",
expected: 1,
},
{
name: "without trailing",
path: "/some-dir/some-third-dir",
rootDir: "/some-dir",
expected: 0,
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
sut := actionsModFile{
path: testCase.path,
}

actual := sut.segmentsTo(testCase.rootDir)

assert.Equal(t, testCase.expected, actual)
})
}

}
27 changes: 27 additions & 0 deletions pkg/executors/golang/codegen/module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package codegen

import (
"strings"

"golang.org/x/exp/slices"
)

type module struct {
name string
path string
}

func modulesFromMap(packages map[string]string) []module {
modules := make([]module, 0, len(packages))
for moduleName, modulePath := range packages {
modules = append(modules, module{
name: moduleName,
path: modulePath,
})
}
slices.SortFunc(modules, func(a, b module) int {
return strings.Compare(a.name, b.name)
})

return modules
}
38 changes: 38 additions & 0 deletions pkg/executors/golang/codegen/patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package codegen

import (
"context"
"io/fs"
"os"
)

type writeFileFunc = func(name string, contents []byte, permissions fs.FileMode) error

type Patcher struct {
patchFinder *chainedPackageFinder
patcher *goModPatcher
}

func NewPatcher() *Patcher {
return &Patcher{
patchFinder: newChainedPatchFinder(
newWorkspaceFinder(),
newGoModuleFinder(),
newDefaultFinder(),
),
patcher: newGoModPatcher(os.WriteFile),
}
}

func (p *Patcher) Patch(ctx context.Context, rootDir string, shuttleLocalDir string) error {
packages, err := p.patchFinder.findPackages(ctx, rootDir)
if err != nil {
return err
}

if err := p.patcher.patch(rootDir, shuttleLocalDir, packages); err != nil {
return err
}

return nil
}
14 changes: 14 additions & 0 deletions pkg/executors/golang/codegen/patch_default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package codegen

import "context"

type defaultFinder struct{}

func newDefaultFinder() *defaultFinder {
return &defaultFinder{}
}

func (s *defaultFinder) Find(ctx context.Context, _ string) (packages map[string]string, ok bool, err error) {
// We return true, as this should be placed last in the chain
return make(map[string]string, 0), true, nil
}
Loading
Loading