From 18c26eed1965cf6ea9b1313667a39a49532d47ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Thu, 23 Nov 2023 12:42:08 -0800 Subject: [PATCH] Major refactor of all internals (#30) * Moved viper loaded config to config package * Created internal interfaces to: * GitManager * ExecManager * RepositoryManager * RepositoriesManager * CopyManager * Implemented gomock * Moved packages to repo patern * Removed go integration tests, and instead use afero for filesystem testing * Switched output to slog * Removed/moved unnecessary packages * Added current configuration debug log * Corrected exec to return Stderr All tests currently passing. --- LICENSE | 2 +- README.md | 10 +- Taskfile.yml | 14 +- cmd/overlay.go | 67 ++-- cmd/root.go | 61 ++- go.mod | 5 +- go.sum | 5 +- internal/config/types.go | 58 +++ internal/git.go | 28 ++ internal/{testing/helper.go => git/exec.go} | 55 +-- internal/git/exec_mock.go | 53 +++ .../exec_public_test.go} | 68 ++-- internal/git/git.go | 128 ++---- internal/git/git_mock.go | 76 ++++ internal/git/git_public_test.go | 161 ++++---- internal/git/git_test.go | 143 ------- internal/git/mocked.go | 73 ---- internal/git/types.go | 24 +- internal/ioutil/copy_test.go | 141 ------- internal/repositories.go | 26 ++ internal/repositories/repositories.go | 116 +++++- .../repositories/repositories_public_test.go | 211 ++++++---- internal/repositories/repositories_test.go | 128 +++++- internal/repositories/types.go | 21 +- internal/repository.go | 32 ++ internal/{ioutil => repository}/copy.go | 106 +++-- internal/repository/copy_mock.go | 62 +++ internal/repository/copy_public_test.go | 140 +++++++ internal/repository/helper_test.go | 50 +++ internal/repository/repository.go | 165 ++++++-- .../repository/repository_integration_test.go | 222 ----------- internal/repository/repository_mock.go | 77 ++++ internal/repository/repository_public_test.go | 367 +++++++++++++++++- internal/repository/types.go | 44 +-- test/integration/test_cli.bats | 5 +- 35 files changed, 1796 insertions(+), 1148 deletions(-) create mode 100644 internal/config/types.go create mode 100644 internal/git.go rename internal/{testing/helper.go => git/exec.go} (62%) create mode 100644 internal/git/exec_mock.go rename internal/{repositories/repositories_integration_test.go => git/exec_public_test.go} (57%) create mode 100644 internal/git/git_mock.go delete mode 100644 internal/git/git_test.go delete mode 100644 internal/git/mocked.go delete mode 100644 internal/ioutil/copy_test.go create mode 100644 internal/repositories.go create mode 100644 internal/repository.go rename internal/{ioutil => repository}/copy.go (71%) create mode 100644 internal/repository/copy_mock.go create mode 100644 internal/repository/copy_public_test.go create mode 100644 internal/repository/helper_test.go delete mode 100644 internal/repository/repository_integration_test.go create mode 100644 internal/repository/repository_mock.go diff --git a/LICENSE b/LICENSE index fd8b509..e123e05 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2018 John Dewey +Copyright (c) 2018-2023 John Dewey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 2e095ae..0e43cdc 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,13 @@ often shared, but Ansible's [Galaxy][] has no mechanism to handle this. ## Port -This project is a port of [Gilt][], it is -not 100% compatible with the python version, and aims to correct some poor decisions -made in the python version of Gilt. +This project is a port of [Gilt][], it is not 100% compatible with the python +version, and aims to correct poor decisions made in the python version of +Gilt. -This version of Gilt does not provide built in locking, unlike our python friend. +This version of Gilt does not provide built in locking, branches, tags, and +post commands unlike our python friend. However, these features will be added +in the future. ## Installation diff --git a/Taskfile.yml b/Taskfile.yml index 76a594f..c065234 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -63,7 +63,6 @@ tasks: unit:int: desc: Integration test packages cmds: - - go test -tags=integration -parallel 5 -race -v ./... - task: unit:bats unit:bats: @@ -85,10 +84,11 @@ tasks: cov: desc: Generate coverage cmds: - - go test -race -coverprofile cover.out -v 2>&1 ./... | go-junit-report --set-exit-code > result.xml || (cat result.xml && echo "fail" && exit 1) + - go test -race -coverprofile=cover.out -v $(go list ./... | egrep -v '(/test|/test/mock)$') 2>&1 | go-junit-report --set-exit-code > result.xml || (cat result.xml && echo "fail" && exit 1) - $(go env GOPATH)/bin/gocover-cobertura < cover.out > cobertura.xml + - go tool cover -func=cover.out - covmap: + cov:map: desc: Generate coverage and show heatmap cmds: - task: cov @@ -111,3 +111,11 @@ tasks: desc: Build ARCH compatible binary. cmds: - goreleaser release --snapshot --clean + + mockgen: + desc: Generate mock for interface + cmds: + - mockgen -source=internal/git.go -destination=internal/git/git_mock.go -package=git + - mockgen -source=internal/repository.go -destination=internal/repository/repository_mock.go -package=repository + - mockgen -source=internal/git/types.go -destination=internal/git/exec_mock.go -package=git + - mockgen -source=internal/repository/types.go -destination=internal/repository/copy_mock.go -package=repository diff --git a/cmd/overlay.go b/cmd/overlay.go index fad7ea4..1c2f830 100644 --- a/cmd/overlay.go +++ b/cmd/overlay.go @@ -22,42 +22,34 @@ package cmd import ( "log/slog" - "os" - "os/user" - "path/filepath" + "strconv" "github.com/spf13/cobra" ) -func expandUser(path string) (string, error) { - if len(path) == 0 || path[0] != '~' { - return path, nil - } - - usr, err := user.Current() - if err != nil { - return "", err - } - - return filepath.Join(usr.HomeDir, path[1:]), nil -} - -// getGiltDir create the GiltDir if it doesn't exist. -func getGiltDir() (string, error) { - expandedGiltDir, err := expandUser(r.GiltDir) - if err != nil { - return "", err - } +func logRepositoriesGroup() []any { + logGroups := make([]any, 0, len(appConfig.Repositories)) - cacheGiltDir := filepath.Join(expandedGiltDir, "cache") - - if _, err := os.Stat(cacheGiltDir); os.IsNotExist(err) { - if err := os.Mkdir(cacheGiltDir, 0o755); err != nil { - return "", err + for i, repo := range appConfig.Repositories { + var sourceGroups []any + for i, s := range repo.Sources { + group := slog.Group(strconv.Itoa(i), + slog.String("Src", s.Src), + slog.String("DstFile", s.DstFile), + slog.String("DstDir", s.DstDir), + ) + sourceGroups = append(sourceGroups, group) } + group := slog.Group(strconv.Itoa(i), + slog.String("Git", repo.Git), + slog.String("Version", repo.Version), + slog.String("DstDir", repo.DstDir), + slog.Group("Sources", sourceGroups...), + ) + logGroups = append(logGroups, group) } - return cacheGiltDir, nil + return logGroups } // overlayCmd represents the overlay command @@ -72,18 +64,15 @@ var overlayCmd = &cobra.Command{ // We are logging errors, no need for cobra to re-log the error cmd.SilenceErrors = true - cacheDir, err := getGiltDir() - if err != nil { - logger.Error( - "error expanding dir", - slog.String("giltDir", r.GiltDir), - slog.String("err", err.Error()), - ) - return err - } + logger.Debug( + "current configuration", + slog.String("GiltDir", appConfig.GiltDir), + slog.String("GiltFile", appConfig.GiltFile), + slog.Bool("Debug", appConfig.Debug), + slog.Group("Repository", logRepositoriesGroup()...), + ) - r.GiltDir = cacheDir - if err := r.Overlay(); err != nil { + if err := repos.Overlay(); err != nil { logger.Error( "error overlaying repositories", slog.String("err", err.Error()), diff --git a/cmd/root.go b/cmd/root.go index 25d0614..21d1efb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,15 +27,21 @@ import ( "time" "github.com/lmittmann/tint" + "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/retr0h/go-gilt/internal" + "github.com/retr0h/go-gilt/internal/config" + "github.com/retr0h/go-gilt/internal/git" "github.com/retr0h/go-gilt/internal/repositories" + "github.com/retr0h/go-gilt/internal/repository" ) var ( - r repositories.Repositories - logger *slog.Logger + repos internal.RepositoriesManager + logger *slog.Logger + appConfig config.Repositories ) // rootCmd represents the base command when called without any subcommands @@ -43,17 +49,13 @@ var rootCmd = &cobra.Command{ Use: "go-gilt", Short: "A GIT layering command line tool", Long: ` - - ,o888888o. 8 8888 8 8888 8888888 8888888888 - 8888 '88. 8 8888 8 8888 8 8888 - ,8 8888 '8. 8 8888 8 8888 8 8888 - 88 8888 8 8888 8 8888 8 8888 - 88 8888 8 8888 8 8888 8 8888 - 88 8888 8 8888 8 8888 8 8888 - 88 8888 8888888 8 8888 8 8888 8 8888 - '8 8888 .8' 8 8888 8 8888 8 8888 - 8888 ,88' 8 8888 8 8888 8 8888 - '8888888P' 8 8888 8 888888888888 8 8888 + o o + o | | + o--o | -o- + | | | | | + o--O | o o + | + o--o A GIT layering command line tool. @@ -117,7 +119,7 @@ func initConfig() { os.Exit(1) } - if err := viper.Unmarshal(&r); err != nil { + if err := viper.Unmarshal(&appConfig); err != nil { logger.Error( "failed to unmarshal config", slog.String("Giltfile", viper.ConfigFileUsed()), @@ -125,4 +127,35 @@ func initConfig() { ) os.Exit(1) } + + appFs := afero.NewOsFs() + + repoManager := repository.NewCopy( + appFs, + logger, + ) + + execManager := git.NewExecManagerCmd( + appConfig.Debug, + logger, + ) + + g := git.New( + appFs, + appConfig.Debug, + execManager, + logger, + ) + + repos = repositories.New( + appFs, + appConfig, + repository.New( + appFs, + repoManager, + g, + logger, + ), + logger, + ) } diff --git a/go.mod b/go.mod index 9242246..890e92a 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,13 @@ module github.com/retr0h/go-gilt go 1.21.0 require ( - github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d + github.com/golang/mock v1.4.4 github.com/lmittmann/tint v1.0.3 - github.com/logrusorgru/aurora/v4 v4.0.0 github.com/spf13/afero v1.10.0 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.17.0 github.com/stretchr/testify v1.8.4 go.hein.dev/go-version v0.1.0 - sigs.k8s.io/yaml v1.4.0 ) require ( @@ -37,4 +35,5 @@ require ( gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index a3ae2c4..e7f9582 100644 --- a/go.sum +++ b/go.sum @@ -99,6 +99,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -162,8 +163,6 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d h1:cVtBfNW5XTHiKQe7jDaDBSh/EVM4XLPutLAGboIXuM0= -github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -179,8 +178,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lmittmann/tint v1.0.3 h1:W5PHeA2D8bBJVvabNfQD/XW9HPLZK1XoPZH0cq8NouQ= github.com/lmittmann/tint v1.0.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= -github.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA= -github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= diff --git a/internal/config/types.go b/internal/config/types.go new file mode 100644 index 0000000..b300ac6 --- /dev/null +++ b/internal/config/types.go @@ -0,0 +1,58 @@ +// Copyright (c) 2023 John Dewey + +// 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. + +package config + +// Repositories perform repository operations. +type Repositories struct { + // Debug enable or disable debug option set from CLI. + Debug bool `mapstruture:"debug"` + // GiltFile path to Gilt's config file option set from CLI. + GiltFile string `mapstructure:"giltFile"` + // GiltDir path to Gilt's clone dir option set from CLI. + GiltDir string `mapstructure:"giltDir"` + // Repositories a slice of repository configurations to overlay. + Repositories []Repository `mapstruture:"repositories"` +} + +// Sources mapping of files and/or directories needing copied. +type Sources struct { + // Src source file or directory to copy. + Src string `mapstructure:"src"` + // DstFile destination of file copy. + DstFile string `mapstructure:"dstFile"` + // DstDir destination of directory copy. + DstDir string `mapstructure:"dstDir"` +} + +// Repository contains the repository's details for cloning. +type Repository struct { + // Git url of Git repository to clone. + Git string `mapstructure:"git"` + // Version of Git repository to use. + Version string `mapstructure:"version"` + // DstDir destination directory to copy clone to. + DstDir string `mapstructure:"dstDir"` + // Sources containing files and/or directories to copy. + Sources []Sources `mapstructure:"sources"` + + // Directory to clone into. + CloneDir string +} diff --git a/internal/git.go b/internal/git.go new file mode 100644 index 0000000..7c012d7 --- /dev/null +++ b/internal/git.go @@ -0,0 +1,28 @@ +// Copyright (c) 2023 John Dewey + +// 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. + +package internal + +// GitManager manager responsible for Git operations. +type GitManager interface { + Clone(gitURL string, cloneDir string) error + Reset(cloneDir string, gitVersion string) error + CheckoutIndex(dstDir string, cloneDir string) error +} diff --git a/internal/testing/helper.go b/internal/git/exec.go similarity index 62% rename from internal/testing/helper.go rename to internal/git/exec.go index 79b02d4..cf01ad2 100644 --- a/internal/testing/helper.go +++ b/internal/git/exec.go @@ -18,40 +18,45 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -package testing +package git import ( - "encoding/json" - "fmt" - "os" - - "sigs.k8s.io/yaml" + "log/slog" + "os/exec" + "strings" ) -// CreateTempDirectory generate and create a temp directory for tests. -func CreateTempDirectory() string { - dir, err := os.MkdirTemp("", "git-test") - if err != nil { - panic(err) +// NewExecManagerCmd factory to create a new exec manager instance. +func NewExecManagerCmd( + debug bool, + logger *slog.Logger, +) *ExecManagerCmd { + return &ExecManagerCmd{ + debug: debug, + logger: logger, } - - fmt.Printf("Created temporary directory: '%s'.\n", dir) - - return dir } -// RemoveTempDirectory removes the temp directory created for tests. -func RemoveTempDirectory(dir string) { - fmt.Printf("Removed temporary directory: '%s'.\n", dir) - - _ = os.RemoveAll(dir) -} +// RunCmd execute the provided command with args. +// Yeah, yeah, yeah, I know I cheated by using Exec in this package. +func (e *ExecManagerCmd) RunCmd( + name string, + args ...string, +) error { + cmd := exec.Command(name, args...) + + if e.debug { + commands := strings.Join(cmd.Args, " ") + e.logger.Debug( + "exec", + slog.String("command", commands), + ) + } -// UnmarshalYAML decodes YAML document into dest type. -func UnmarshalYAML(data []byte, dest interface{}) error { - jsonData, err := yaml.YAMLToJSON([]byte(data)) + _, err := cmd.CombinedOutput() if err != nil { return err } - return json.Unmarshal(jsonData, &dest) + + return nil } diff --git a/internal/git/exec_mock.go b/internal/git/exec_mock.go new file mode 100644 index 0000000..eb28e6f --- /dev/null +++ b/internal/git/exec_mock.go @@ -0,0 +1,53 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/git/types.go + +// Package git is a generated GoMock package. +package git + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockExecManager is a mock of ExecManager interface. +type MockExecManager struct { + ctrl *gomock.Controller + recorder *MockExecManagerMockRecorder +} + +// MockExecManagerMockRecorder is the mock recorder for MockExecManager. +type MockExecManagerMockRecorder struct { + mock *MockExecManager +} + +// NewMockExecManager creates a new mock instance. +func NewMockExecManager(ctrl *gomock.Controller) *MockExecManager { + mock := &MockExecManager{ctrl: ctrl} + mock.recorder = &MockExecManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExecManager) EXPECT() *MockExecManagerMockRecorder { + return m.recorder +} + +// RunCmd mocks base method. +func (m *MockExecManager) RunCmd(name string, args ...string) error { + m.ctrl.T.Helper() + varargs := []interface{}{name} + for _, a := range args { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RunCmd", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// RunCmd indicates an expected call of RunCmd. +func (mr *MockExecManagerMockRecorder) RunCmd(name interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{name}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunCmd", reflect.TypeOf((*MockExecManager)(nil).RunCmd), varargs...) +} diff --git a/internal/repositories/repositories_integration_test.go b/internal/git/exec_public_test.go similarity index 57% rename from internal/repositories/repositories_integration_test.go rename to internal/git/exec_public_test.go index f03f124..bf27854 100644 --- a/internal/repositories/repositories_integration_test.go +++ b/internal/git/exec_public_test.go @@ -1,7 +1,4 @@ -//go:build integration -// +build integration - -// Copyright (c) 2018 John Dewey +// Copyright (c) 2023 John Dewey // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to @@ -21,53 +18,60 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -package repositories_test +package git_test import ( - "fmt" + "log/slog" + "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - helper "github.com/retr0h/go-gilt/internal/testing" + "github.com/retr0h/go-gilt/internal/git" ) -func (suite *RepositoriesTestSuite) TestOverlayRemovesSrcDirPriorToCheckoutIndex() { - tempDir := helper.CreateTempDirectory() - data := fmt.Sprintf(` ---- -- git: https://github.com/retr0h/ansible-etcd.git - version: 77a95b7 - dstDir: %s/retr0h.ansible-etcd -`, tempDir) - err := suite.unmarshalYAML([]byte(data)) - assert.NoError(suite.T(), err) +type ExecManagerPublicTestSuite struct { + suite.Suite +} + +func (suite *ExecManagerPublicTestSuite) NewTestExecManager( + debug bool, +) git.ExecManager { + return git.NewExecManagerCmd( + debug, + slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })), + ) +} - suite.r.Overlay() +func (suite *ExecManagerPublicTestSuite) TestRunCmdOk() { + em := suite.NewTestExecManager(false) - err = suite.r.Overlay() + err := em.RunCmd("ls") assert.NoError(suite.T(), err) } -func (suite *RepositoriesTestSuite) TestOverlayFailsCopySourcesReturnsError() { - data := ` ---- -- git: https://github.com/lorin/openstack-ansible-modules.git - version: 2677cc3 - sources: - - src: "*_manage" - dstDir: /super/invalid/path/to/write/to -` - err := suite.unmarshalYAML([]byte(data)) +func (suite *ExecManagerPublicTestSuite) TestRunCmdWithDebug() { + suite.T().Skip("cannot seem to capture Stdout when logging in em") + + em := suite.NewTestExecManager(true) + + err := em.RunCmd("echo", "-n", "foo") assert.NoError(suite.T(), err) +} + +func (suite *ExecManagerPublicTestSuite) TestRunCmdReturnsError() { + em := suite.NewTestExecManager(false) - err = suite.r.Overlay() + err := em.RunCmd("invalid", "foo") assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "not found") } // In order for `go test` to run this suite, we need to create // a normal test function and pass our suite to suite.Run. -func TestRepositoryTestSuite(t *testing.T) { - suite.Run(t, new(RepositoriesTestSuite)) +func TestExecPublicTestSuite(t *testing.T) { + suite.Run(t, new(ExecManagerPublicTestSuite)) } diff --git a/internal/git/git.go b/internal/git/git.go index 7961149..0d74df1 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -24,89 +24,59 @@ package git import ( - "bytes" - "errors" - "fmt" + "log/slog" "os" - "os/exec" "path/filepath" - "strings" - "github.com/logrusorgru/aurora/v4" - - "github.com/retr0h/go-gilt/internal/repository" -) - -var ( - // RunCommand is mocked for tests. - RunCommand = runCmd - // FilePathAbs is mocked for tests. - FilePathAbs = filepath.Abs + "github.com/spf13/afero" ) -// NewGit factory to create a new Git instance. -func NewGit(debug bool) *Git { +// New factory to create a new Git instance. +func New( + appFs afero.Fs, + debug bool, + execManager ExecManager, + logger *slog.Logger, +) *Git { return &Git{ - debug: debug, + appFs: appFs, + debug: debug, + execManager: execManager, + logger: logger, } } -// Clone clone Repository.Git to Repository.getCloneDir, and hard checkout -// to Repository.Version. -func (g *Git) Clone(repository repository.Repository) error { - cloneDir := repository.GetCloneDir() - - msg := fmt.Sprintf( - "[%s@%s]:", - aurora.Magenta(repository.Git), - aurora.Magenta(repository.Version), - ) - fmt.Println(msg) - - msg = fmt.Sprintf("%-2s - Cloning to '%s'", "", aurora.Cyan(cloneDir)) - fmt.Println(msg) - - if _, err := os.Stat(cloneDir); os.IsNotExist(err) { - if err := g.clone(repository); err != nil { - return err - } - - if err := g.reset(repository); err != nil { - return err - } - } else { - bang := aurora.Bold(aurora.Red("!")) - msg := fmt.Sprintf("%-2s %s %s", "", bang, aurora.Yellow("Clone already exists")) - fmt.Println(msg) - } - - return nil +// Clone as exec manager to clone repo. +func (g *Git) Clone( + gitURL string, + cloneDir string, +) error { + // return g.execManager.Clone(gitURL, cloneDir) + return g.execManager.RunCmd("git", "clone", gitURL, cloneDir) } -func (g *Git) clone(repository repository.Repository) error { - cloneDir := repository.GetCloneDir() - err := RunCommand(g.debug, "git", "clone", repository.Git, cloneDir) - - return err -} - -func (g *Git) reset(repository repository.Repository) error { - cloneDir := repository.GetCloneDir() - err := RunCommand(g.debug, "git", "-C", cloneDir, "reset", "--hard", repository.Version) - - return err +// Reset to the given git version. +func (g *Git) Reset( + cloneDir string, + gitVersion string, +) error { + return g.execManager.RunCmd("git", "-C", cloneDir, "reset", "--hard", gitVersion) } // CheckoutIndex checkout Repository.Git to Repository.DstDir. -func (g *Git) CheckoutIndex(repository repository.Repository) error { - cloneDir := repository.GetCloneDir() - dstDir, err := FilePathAbs(repository.DstDir) +func (g *Git) CheckoutIndex( + dstDir string, + cloneDir string, +) error { + dst, err := filepath.Abs(dstDir) if err != nil { return err } - msg := fmt.Sprintf("%-2s - Extracting to '%s'", "", aurora.Cyan(dstDir)) - fmt.Println(msg) + g.logger.Info( + "extracting", + slog.String("to", dst), + ) cmdArgs := []string{ "-C", @@ -116,32 +86,8 @@ func (g *Git) CheckoutIndex(repository repository.Repository) error { "--all", "--prefix", // Trailing separator needed by git checkout-index. - dstDir + string(os.PathSeparator), - } - - return RunCommand(g.debug, "git", cmdArgs...) -} - -// runCmd execute the provided command with args. -// Yeah, yeah, yeah, I know I cheated by using Exec in this package. -func runCmd(debug bool, name string, args ...string) error { - var stderr bytes.Buffer - cmd := exec.Command(name, args...) - - if debug { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - commands := strings.Join(cmd.Args, " ") - msg := fmt.Sprintf("COMMAND: %s", aurora.Colorize(commands, aurora.BlackFg|aurora.RedBg)) - fmt.Println(msg) - } else { - cmd.Stderr = &stderr - } - - if err := cmd.Run(); err != nil { - return errors.New(stderr.String()) + dst + string(os.PathSeparator), } - return nil + return g.execManager.RunCmd("git", cmdArgs...) } diff --git a/internal/git/git_mock.go b/internal/git/git_mock.go new file mode 100644 index 0000000..f401c80 --- /dev/null +++ b/internal/git/git_mock.go @@ -0,0 +1,76 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/git.go + +// Package git is a generated GoMock package. +package git + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockGitManager is a mock of GitManager interface. +type MockGitManager struct { + ctrl *gomock.Controller + recorder *MockGitManagerMockRecorder +} + +// MockGitManagerMockRecorder is the mock recorder for MockGitManager. +type MockGitManagerMockRecorder struct { + mock *MockGitManager +} + +// NewMockGitManager creates a new mock instance. +func NewMockGitManager(ctrl *gomock.Controller) *MockGitManager { + mock := &MockGitManager{ctrl: ctrl} + mock.recorder = &MockGitManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGitManager) EXPECT() *MockGitManagerMockRecorder { + return m.recorder +} + +// CheckoutIndex mocks base method. +func (m *MockGitManager) CheckoutIndex(dstDir, cloneDir string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckoutIndex", dstDir, cloneDir) + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckoutIndex indicates an expected call of CheckoutIndex. +func (mr *MockGitManagerMockRecorder) CheckoutIndex(dstDir, cloneDir interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckoutIndex", reflect.TypeOf((*MockGitManager)(nil).CheckoutIndex), dstDir, cloneDir) +} + +// Clone mocks base method. +func (m *MockGitManager) Clone(gitURL, cloneDir string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Clone", gitURL, cloneDir) + ret0, _ := ret[0].(error) + return ret0 +} + +// Clone indicates an expected call of Clone. +func (mr *MockGitManagerMockRecorder) Clone(gitURL, cloneDir interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clone", reflect.TypeOf((*MockGitManager)(nil).Clone), gitURL, cloneDir) +} + +// Reset mocks base method. +func (m *MockGitManager) Reset(cloneDir, gitVersion string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Reset", cloneDir, gitVersion) + ret0, _ := ret[0].(error) + return ret0 +} + +// Reset indicates an expected call of Reset. +func (mr *MockGitManagerMockRecorder) Reset(cloneDir, gitVersion interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockGitManager)(nil).Reset), cloneDir, gitVersion) +} diff --git a/internal/git/git_public_test.go b/internal/git/git_public_test.go index 38828ef..b192a4e 100644 --- a/internal/git/git_public_test.go +++ b/internal/git/git_public_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018 John Dewey +// Copyright (c) 2023 John Dewey // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to @@ -22,138 +22,111 @@ package git_test import ( "errors" - "fmt" + "log/slog" "os" - "path/filepath" "testing" + "github.com/golang/mock/gomock" + "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/retr0h/go-gilt/internal" "github.com/retr0h/go-gilt/internal/git" - "github.com/retr0h/go-gilt/internal/repository" - helper "github.com/retr0h/go-gilt/internal/testing" ) -type GitTestSuite struct { +type GitManagerPublicTestSuite struct { suite.Suite - g *git.Git - r repository.Repository -} - -func (suite *GitTestSuite) SetupTest() { - suite.g = git.NewGit(false) - suite.r = repository.Repository{ - Git: "https://example.com/user/repo.git", - Version: "abc1234", - DstDir: "path/user.repo", - GiltDir: helper.CreateTempDirectory(), - } -} -func (suite *GitTestSuite) TearDownTest() { - helper.RemoveTempDirectory(suite.r.GiltDir) -} + ctrl *gomock.Controller + mockExec *git.MockExecManager -func (suite *GitTestSuite) TestCloneAlreadyExists() { - cloneDir := filepath.Join(suite.r.GiltDir, "https---example.com-user-repo.git-abc1234") - if _, err := os.Stat(cloneDir); os.IsNotExist(err) { - _ = os.Mkdir(cloneDir, 0o755) - } + gitURL string + gitVersion string + cloneDir string + dstDir string - _ = suite.g.Clone(suite.r) + gm internal.GitManager +} - defer func() { _ = os.RemoveAll(cloneDir) }() +func (suite *GitManagerPublicTestSuite) NewTestGitManager() internal.GitManager { + return git.New( + afero.NewMemMapFs(), + false, + suite.mockExec, + slog.New(slog.NewTextHandler(os.Stdout, nil)), + ) } -func (suite *GitTestSuite) TestCloneErrorsOnCloneReturnsError() { - anon := func() error { - err := suite.g.Clone(suite.r) - assert.Error(suite.T(), err) +func (suite *GitManagerPublicTestSuite) SetupTest() { + suite.ctrl = gomock.NewController(suite.T()) + suite.mockExec = git.NewMockExecManager(suite.ctrl) + defer suite.ctrl.Finish() - return err - } + suite.gitURL = "https://example.com/user/repo.git" + suite.gitVersion = "abc123" + suite.cloneDir = "/cloneDir" + suite.dstDir = "/dstDir" - git.MockRunCommandErrorsOn("clone", anon) + suite.gm = suite.NewTestGitManager() } -func (suite *GitTestSuite) TestCloneErrorsOnResetReturnsError() { - anon := func() error { - err := suite.g.Clone(suite.r) - assert.Error(suite.T(), err) +func (suite *GitManagerPublicTestSuite) TestCloneOk() { + suite.mockExec.EXPECT().RunCmd("git", "clone", suite.gitURL, suite.cloneDir).Return(nil) - return err - } - - git.MockRunCommandErrorsOn("reset", anon) + err := suite.gm.Clone(suite.gitURL, suite.cloneDir) + assert.NoError(suite.T(), err) } -func (suite *GitTestSuite) TestClone() { - anon := func() error { - err := suite.g.Clone(suite.r) - assert.NoError(suite.T(), err) +func (suite *GitManagerPublicTestSuite) TestCloneReturnsError() { + errors := errors.New("tests error") + suite.mockExec.EXPECT().RunCmd(gomock.Any(), gomock.Any()).Return(errors) - return err - } + err := suite.gm.Clone(suite.gitURL, suite.cloneDir) + assert.Error(suite.T(), err) +} - got := git.MockRunCommand(anon) - want := []string{ - fmt.Sprintf( - "git clone https://example.com/user/repo.git %s/https---example.com-user-repo.git-abc1234", - suite.r.GiltDir, - ), - fmt.Sprintf("git -C %s/https---example.com-user-repo.git-abc1234 reset --hard abc1234", - suite.r.GiltDir), - } +func (suite *GitManagerPublicTestSuite) TestResetOk() { + suite.mockExec.EXPECT().RunCmd("git", "-C", suite.cloneDir, "reset", "--hard", suite.gitVersion) - assert.Equal(suite.T(), want, got) + err := suite.gm.Reset(suite.cloneDir, suite.gitVersion) + assert.NoError(suite.T(), err) } -func (suite *GitTestSuite) TestCheckoutIndexFailsFilepathAbsReturnsError() { - originalFilepathAbs := git.FilePathAbs - git.FilePathAbs = func(string) (string, error) { - return "", errors.New("Failed filepath.Abs") - } - defer func() { git.FilePathAbs = originalFilepathAbs }() +func (suite *GitManagerPublicTestSuite) TestResetReturnsError() { + errors := errors.New("tests error") + suite.mockExec.EXPECT().RunCmd(gomock.Any(), gomock.Any()).Return(errors) - err := suite.g.CheckoutIndex(suite.r) + err := suite.gm.Reset(suite.cloneDir, suite.gitVersion) assert.Error(suite.T(), err) } -func (suite *GitTestSuite) TestCheckoutIndexFailsCheckoutIndexReturnsError() { - anon := func() error { - err := suite.g.CheckoutIndex(suite.r) - assert.Error(suite.T(), err) - - return err +func (suite *GitManagerPublicTestSuite) TestCheckoutIndexOk() { + cmdArgs := []string{ + "-C", + suite.cloneDir, + "checkout-index", + "--force", + "--all", + "--prefix", + suite.dstDir + string(os.PathSeparator), } + suite.mockExec.EXPECT().RunCmd("git", cmdArgs).Return(nil) - git.MockRunCommandErrorsOn("git", anon) + err := suite.gm.CheckoutIndex(suite.dstDir, suite.cloneDir) + assert.NoError(suite.T(), err) } -func (suite *GitTestSuite) TestCheckoutIndex() { - anon := func() error { - err := suite.g.CheckoutIndex(suite.r) - assert.NoError(suite.T(), err) +func (suite *GitManagerPublicTestSuite) TestCheckoutIndexReturnsError() { + errors := errors.New("tests error") + suite.mockExec.EXPECT().RunCmd(gomock.Any(), gomock.Any()).Return(errors) - return err - } - - dstDir, _ := git.FilePathAbs(suite.r.DstDir) - got := git.MockRunCommand(anon) - want := []string{ - fmt.Sprintf( - "git -C %s/https---example.com-user-repo.git-abc1234 checkout-index --force --all --prefix %s", - suite.r.GiltDir, - (dstDir + string(os.PathSeparator)), - ), - } - - assert.Equal(suite.T(), want, got) + err := suite.gm.CheckoutIndex(suite.dstDir, suite.cloneDir) + assert.Error(suite.T(), err) } // In order for `go test` to run this suite, we need to create // a normal test function and pass our suite to suite.Run. -func TestGitTestSuite(t *testing.T) { - suite.Run(t, new(GitTestSuite)) +func TestGitManagerPublicTestSuite(t *testing.T) { + suite.Run(t, new(GitManagerPublicTestSuite)) } diff --git a/internal/git/git_test.go b/internal/git/git_test.go deleted file mode 100644 index 186dfd0..0000000 --- a/internal/git/git_test.go +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2018 John Dewey - -// 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. - -package git - -import ( - "fmt" - "testing" - - capturer "github.com/kami-zh/go-capturer" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - - "github.com/retr0h/go-gilt/internal/repository" - helper "github.com/retr0h/go-gilt/internal/testing" -) - -type GitTestSuite struct { - suite.Suite - g *Git - r repository.Repository -} - -func (suite *GitTestSuite) SetupTest() { - suite.g = NewGit(false) - suite.r = repository.Repository{ - Git: "https://example.com/user/repo.git", - Version: "abc1234", - DstDir: "path/user.repo", - GiltDir: helper.CreateTempDirectory(), - } -} - -func (suite *GitTestSuite) TearDownTest() { - helper.RemoveTempDirectory(suite.r.GiltDir) -} - -func (suite *GitTestSuite) TestCloneReturnsError() { - anon := func() error { - err := suite.g.clone(suite.r) - assert.Error(suite.T(), err) - - return err - } - - MockRunCommandErrorsOn("git", anon) -} - -func (suite *GitTestSuite) TestClone() { - anon := func() error { - err := suite.g.clone(suite.r) - assert.NoError(suite.T(), err) - - return err - } - - got := MockRunCommand(anon) - want := []string{ - fmt.Sprintf( - "git clone https://example.com/user/repo.git %s/https---example.com-user-repo.git-abc1234", - suite.r.GiltDir, - ), - } - - assert.Equal(suite.T(), want, got) -} - -func (suite *GitTestSuite) TestResetReturnsError() { - anon := func() error { - err := suite.g.reset(suite.r) - assert.Error(suite.T(), err) - - return err - } - - MockRunCommandErrorsOn("git", anon) -} - -func (suite *GitTestSuite) TestReset() { - anon := func() error { - err := suite.g.reset(suite.r) - assert.NoError(suite.T(), err) - - return err - } - - got := MockRunCommand(anon) - want := []string{ - fmt.Sprintf("git -C %s/https---example.com-user-repo.git-abc1234 reset --hard abc1234", - suite.r.GiltDir), - } - - assert.Equal(suite.T(), want, got) -} - -func TestRunCommandReturnsError(t *testing.T) { - err := runCmd(false, "false") - - assert.Error(t, err) -} - -func TestRunCommandPrintsStreamingStdout(t *testing.T) { - got := capturer.CaptureStdout(func() { - err := runCmd(true, "echo", "-n", "foo") - assert.NoError(t, err) - }) - want := "COMMAND: \x1b[30;41mecho -n foo\x1b[0m\nfoo" - - assert.Equal(t, want, got) -} - -func TestRunCommandPrintsStreamingStderr(t *testing.T) { - got := capturer.CaptureStderr(func() { - err := runCmd(true, "cat", "foo") - assert.Error(t, err) - }) - want := "cat: foo: No such file or directory\n" - - assert.Equal(t, want, got) -} - -// In order for `go test` to run this suite, we need to create -// a normal test function and pass our suite to suite.Run. -func TestGitTestSuite(t *testing.T) { - suite.Run(t, new(GitTestSuite)) -} diff --git a/internal/git/mocked.go b/internal/git/mocked.go deleted file mode 100644 index 44635e2..0000000 --- a/internal/git/mocked.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2018 John Dewey - -// 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. - -package git - -import ( - "errors" - "os/exec" - "strings" -) - -// MockRunCommandImpl records and return the arguments passed to RunCommand as -// a string. An errString may be passed to force the command to fail with -// an error, if the command executed contains the errString. Useful, when -// mocking functions with multiple calls to RunCommand. -// This seems super wrong to have a test function living in the `git` package. -func MockRunCommandImpl(errString string, f func() error) []string { - var got []string - - originalRunCommand := RunCommand - RunCommand = func(debug bool, name string, args ...string) error { - cmd := exec.Command(name, args...) - cmdString := strings.Join(cmd.Args, " ") - - if errString == "" { - got = append(got, cmdString) - return nil - } - - if strings.Contains(cmdString, errString) { - return errors.New("RunCommand had an error") - } - - // NOTE(retr0h): Never hit this path since this is only used by unit tests - // and we keep an eye on our returns. However, the lack of this path causes - // our code coverage to drop. - return nil - } - defer func() { RunCommand = originalRunCommand }() - - _ = f() - - return got -} - -// MockRunCommand is sugar around MockRunCommandImpl, and returns -// a string with the arguments passed to RunCommand. -func MockRunCommand(f func() error) []string { - return MockRunCommandImpl("", f) -} - -// MockRunCommandErrorsOn is sugar around MockedRunCommandImpl and -// returns an error when invoked. -func MockRunCommandErrorsOn(errCmd string, f func() error) { - MockRunCommandImpl(errCmd, f) -} diff --git a/internal/git/types.go b/internal/git/types.go index 9906ce6..2b76267 100644 --- a/internal/git/types.go +++ b/internal/git/types.go @@ -20,7 +20,27 @@ package git -// Git perform Git operations. +import ( + "log/slog" + + "github.com/spf13/afero" +) + +// Git implementation responsible for Git operations. type Git struct { - debug bool + appFs afero.Fs + debug bool + execManager ExecManager + logger *slog.Logger +} + +// ExecManager interface to managing exec calls. +type ExecManager interface { + RunCmd(name string, args ...string) error +} + +// ExecManagerCmd disk implementation. +type ExecManagerCmd struct { + debug bool + logger *slog.Logger } diff --git a/internal/ioutil/copy_test.go b/internal/ioutil/copy_test.go deleted file mode 100644 index eb55fc7..0000000 --- a/internal/ioutil/copy_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) 2018 John Dewey - -// 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. - -package ioutil - -import ( - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type IOUtilTestSuite struct { - suite.Suite -} - -func (suite *IOUtilTestSuite) TestCopyFile() { - type spec struct { - appFs afero.Fs - baseDir string - srcFile string - dstFile string - } - - specs := []spec{ - { - appFs: afero.NewMemMapFs(), - baseDir: "/base", - srcFile: "/base/srcFile", - dstFile: "/base/dstFile", - }, - } - - for _, s := range specs { - _ = s.appFs.MkdirAll(s.baseDir, 0o755) - _, err := s.appFs.Create(s.srcFile) - assert.NoError(suite.T(), err) - - err = CopyFile(s.appFs, s.srcFile, s.dstFile) - assert.NoError(suite.T(), err) - - got, _ := afero.Exists(s.appFs, s.dstFile) - assert.True(suite.T(), got) - } -} - -func (suite *IOUtilTestSuite) TestCopyFileReturnsError() { - type spec struct { - appFs afero.Fs - srcFile string - dstFile string - } - - specs := []spec{ - {appFs: afero.NewMemMapFs(), srcFile: "srcFile", dstFile: "/invalid/path"}, - } - - for _, s := range specs { - err := CopyFile(s.appFs, s.srcFile, s.dstFile) - assert.Error(suite.T(), err) - - got, _ := afero.Exists(s.appFs, s.dstFile) - assert.False(suite.T(), got) - } -} - -func (suite *IOUtilTestSuite) TestCopyDir() { - type spec struct { - appFs afero.Fs - srcDir string - srcFile string - dstDir string - dstFile string - } - - specs := []spec{ - { - appFs: afero.NewMemMapFs(), - srcDir: "/src", - srcFile: "/src/srcFile", - dstDir: "/dst", - dstFile: "/dst/dstFile", - }, - } - - for _, s := range specs { - _ = s.appFs.MkdirAll(s.srcDir, 0o755) - _, err := s.appFs.Create(s.srcFile) - assert.NoError(suite.T(), err) - - err = CopyFile(s.appFs, s.srcDir, s.dstDir) - assert.NoError(suite.T(), err) - - got, _ := afero.Exists(s.appFs, s.dstFile) - assert.False(suite.T(), got) - } -} - -func (suite *IOUtilTestSuite) TestCopyDirReturnsError() { - type spec struct { - appFs afero.Fs - srcDir string - dstDir string - } - - specs := []spec{ - {appFs: afero.NewMemMapFs(), srcDir: "/src", dstDir: "/dst"}, - } - - for _, s := range specs { - err := CopyDir(s.appFs, s.srcDir, s.dstDir) - assert.Error(suite.T(), err) - - got, _ := afero.DirExists(s.appFs, s.dstDir) - assert.False(suite.T(), got) - } -} - -// In order for `go test` to run this suite, we need to create -// a normal test function and pass our suite to suite.Run. -func TestIOUtilTestSuite(t *testing.T) { - suite.Run(t, new(IOUtilTestSuite)) -} diff --git a/internal/repositories.go b/internal/repositories.go new file mode 100644 index 0000000..316ee80 --- /dev/null +++ b/internal/repositories.go @@ -0,0 +1,26 @@ +// Copyright (c) 2023 John Dewey + +// 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. + +package internal + +// RepositoriesManager manager responsible for Repositories operations. +type RepositoriesManager interface { + Overlay() error +} diff --git a/internal/repositories/repositories.go b/internal/repositories/repositories.go index 7e03df1..5dfa85b 100644 --- a/internal/repositories/repositories.go +++ b/internal/repositories/repositories.go @@ -21,41 +21,125 @@ package repositories import ( + "fmt" + "log/slog" "os" + "os/user" + "path/filepath" + "strings" - "github.com/retr0h/go-gilt/internal/git" "github.com/spf13/afero" + + "github.com/retr0h/go-gilt/internal" + "github.com/retr0h/go-gilt/internal/config" ) +var currentUser = user.Current + +// New factory to create a new Repository instance. +func New( + appFs afero.Fs, + c config.Repositories, + repoManager internal.RepositoryManager, + logger *slog.Logger, +) *Repositories { + return &Repositories{ + appFs: appFs, + config: c, + repoManager: repoManager, + logger: logger, + } +} + +func expandUser( + path string, +) (string, error) { + if len(path) == 0 || path[0] != '~' { + return path, nil + } + + usr, err := currentUser() + if err != nil { + return "", err + } + + return filepath.Join(usr.HomeDir, path[1:]), nil +} + +// getCloneDir returns the path to the Repository's clone directory. +func (r *Repositories) getCloneDir( + giltDir string, + c config.Repository, +) string { + return filepath.Join(giltDir, r.getCloneHash(c)) +} + +func (r *Repositories) getCloneHash( + c config.Repository, +) string { + replacer := strings.NewReplacer( + "/", "-", + ":", "-", + ) + replacedGitURL := replacer.Replace(c.Git) + + return fmt.Sprintf("%s-%s", replacedGitURL, c.Version) +} + +// getGiltDir create the GiltDir if it doesn't exist. +func (r *Repositories) getGiltDir() (string, error) { + expandedGiltDir, err := expandUser(r.config.GiltDir) + if err != nil { + return "", err + } + + cacheGiltDir := filepath.Join(expandedGiltDir, "cache") + if _, err := r.appFs.Stat(cacheGiltDir); os.IsNotExist(err) { + if err := r.appFs.Mkdir(cacheGiltDir, 0o755); err != nil { + return "", err + } + } + + return cacheGiltDir, nil +} + // Overlay clone and extract the Repository items. func (r *Repositories) Overlay() error { - g := git.NewGit(r.Debug) + cacheDir, err := r.getGiltDir() + if err != nil { + r.logger.Error( + "error expanding dir", + slog.String("giltDir", r.config.GiltDir), + slog.String("cacheDir", cacheDir), + slog.String("err", err.Error()), + ) + return err + } - for _, repository := range r.Repositories { - repository.GiltDir = r.GiltDir - repository.AppFs = afero.NewOsFs() - err := g.Clone(repository) + for _, c := range r.config.Repositories { + cloneDir := r.getCloneDir(cacheDir, c) + err = r.repoManager.Clone(c, cloneDir) if err != nil { return err } - // Checkout into repository.DstDir. - if repository.DstDir != "" { - // Delete dstDir since Checkout-Index does not clean old files that may - // no longer exist in repository. - if info, err := os.Stat(repository.DstDir); err == nil && info.Mode().IsDir() { - if err := os.RemoveAll(repository.DstDir); err != nil { + // checkout into c.DstDir + if c.DstDir != "" { + // delete dstDir since Checkout-Index does not clean old files that may + // no longer exist in config + if info, err := r.appFs.Stat(c.DstDir); err == nil && info.Mode().IsDir() { + if err := os.RemoveAll(c.DstDir); err != nil { return err } } - if err := g.CheckoutIndex(repository); err != nil { + if err := r.repoManager.CheckoutIndex(c, cloneDir); err != nil { return err } } - // Copy sources from Repository.Src to Repository.DstDir or Repository.DstFile. - if len(repository.Sources) > 0 { - if err := repository.CopySources(); err != nil { + // copy sources from Repository.Src to Repository.DstDir or Repository.DstFile + if len(c.Sources) > 0 { + if err := r.repoManager.CopySources(c, cloneDir); err != nil { return err } } diff --git a/internal/repositories/repositories_public_test.go b/internal/repositories/repositories_public_test.go index c952602..65b9d4b 100644 --- a/internal/repositories/repositories_public_test.go +++ b/internal/repositories/repositories_public_test.go @@ -21,125 +21,166 @@ package repositories_test import ( - "fmt" + "errors" + "log/slog" "os" + "path/filepath" "testing" + "github.com/golang/mock/gomock" + "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/retr0h/go-gilt/internal/git" + "github.com/retr0h/go-gilt/internal" + "github.com/retr0h/go-gilt/internal/config" "github.com/retr0h/go-gilt/internal/repositories" - helper "github.com/retr0h/go-gilt/internal/testing" + "github.com/retr0h/go-gilt/internal/repository" ) -type RepositoriesTestSuite struct { +type RepositoriesPublicTestSuite struct { suite.Suite - r repositories.Repositories -} -func (suite *RepositoriesTestSuite) unmarshalYAML(data []byte) error { - return helper.UnmarshalYAML([]byte(data), &suite.r.Repositories) + ctrl *gomock.Controller + mockRepo *repository.MockRepositoryManager + + appFs afero.Fs + dstDir string + giltDir string + gitURL string + gitVersion string + repoConfigDstDir []config.Repository + repoConfigSources []config.Repository + logger *slog.Logger } -func (suite *RepositoriesTestSuite) SetupTest() { - suite.r = repositories.Repositories{} - suite.r.GiltDir = helper.CreateTempDirectory() +func (suite *RepositoriesPublicTestSuite) NewTestRepositoriesManager( + repoConfig []config.Repository, +) internal.RepositoriesManager { + reposConfig := config.Repositories{ + Debug: false, + GiltFile: "Giltfile.yaml", + GiltDir: suite.giltDir, + Repositories: repoConfig, + } + + return repositories.New( + suite.appFs, + reposConfig, + suite.mockRepo, + suite.logger, + ) } -func (suite *RepositoriesTestSuite) TearDownTest() { - helper.RemoveTempDirectory(suite.r.GiltDir) +func (suite *RepositoriesPublicTestSuite) SetupTest() { + suite.ctrl = gomock.NewController(suite.T()) + suite.mockRepo = repository.NewMockRepositoryManager(suite.ctrl) + defer suite.ctrl.Finish() + + suite.appFs = afero.NewMemMapFs() + suite.dstDir = "/dstDir" + suite.giltDir = "/giltDir" + suite.gitURL = "https://example.com/user/repo.git" + suite.gitVersion = "abc1234" + suite.repoConfigDstDir = []config.Repository{ + { + Git: suite.gitURL, + Version: suite.gitVersion, + DstDir: suite.dstDir, + }, + } + suite.repoConfigSources = []config.Repository{ + { + Git: suite.gitURL, + Version: suite.gitVersion, + Sources: []config.Sources{ + { + Src: "srcDir", + DstDir: suite.dstDir, + }, + }, + }, + } + + suite.logger = slog.New(slog.NewTextHandler(os.Stdout, nil)) } -func (suite *RepositoriesTestSuite) TestOverlayFailsCloneReturnsError() { - data := ` ---- -- git: invalid. - version: abc1234 - dstDir: path/user.repo -` - err := suite.unmarshalYAML([]byte(data)) +func (suite *RepositoriesPublicTestSuite) TestOverlayOkWhenDstDir() { + repos := suite.NewTestRepositoriesManager(suite.repoConfigDstDir) + + suite.mockRepo.EXPECT(). + Clone(suite.repoConfigDstDir[0], filepath.Join(suite.giltDir, "cache/https---example.com-user-repo.git-abc1234")). + Return(nil) + suite.mockRepo.EXPECT(). + CheckoutIndex(suite.repoConfigDstDir[0], filepath.Join(suite.giltDir, "cache/https---example.com-user-repo.git-abc1234")). + Return(nil) + + err := repos.Overlay() assert.NoError(suite.T(), err) +} - anon := func() error { - err := suite.r.Overlay() - assert.Error(suite.T(), err) +func (suite *RepositoriesPublicTestSuite) TestOverlayDstDirExists() { + repos := suite.NewTestRepositoriesManager(suite.repoConfigDstDir) - return err - } + suite.mockRepo.EXPECT().Clone(gomock.Any(), gomock.Any()).Return(nil) + suite.mockRepo.EXPECT().CheckoutIndex(gomock.Any(), gomock.Any()).Return(nil) - git.MockRunCommandErrorsOn("git", anon) + _ = suite.appFs.MkdirAll(suite.dstDir, 0o755) + err := repos.Overlay() + assert.NoError(suite.T(), err) } -func (suite *RepositoriesTestSuite) TestOverlayFailsCheckoutIndexReturnsError() { - data := ` ---- -- git: https://example.com/user/repo.git - version: abc1234 - dstDir: /invalid/directory -` - err := suite.unmarshalYAML([]byte(data)) - assert.NoError(suite.T(), err) +func (suite *RepositoriesPublicTestSuite) TestOverlayReturnsErrorWhenDstDirDeleteFails() { + suite.T().Skip("implement") +} - anon := func() error { - err := suite.r.Overlay() - assert.Error(suite.T(), err) +func (suite *RepositoriesPublicTestSuite) TestOverlayReturnsErrorWhenCloneErrors() { + repos := suite.NewTestRepositoriesManager(suite.repoConfigDstDir) - return err - } + errors := errors.New("tests error") + suite.mockRepo.EXPECT().Clone(gomock.Any(), gomock.Any()).Return(errors) + suite.mockRepo.EXPECT().CheckoutIndex(gomock.Any(), gomock.Any()).Return(nil) - git.MockRunCommandErrorsOn("checkout-index", anon) + err := repos.Overlay() + assert.Error(suite.T(), err) } -func (suite *RepositoriesTestSuite) TestOverlay() { - data := ` ---- -- git: https://example.com/user/repo1.git - version: abc1234 - dstDir: path/user.repo -- git: https://example.com/user/repo2.git - version: abc1234 - sources: - - src: foo - dstFile: bar -` - err := suite.unmarshalYAML([]byte(data)) - assert.NoError(suite.T(), err) +func (suite *RepositoriesPublicTestSuite) TestOverlayReturnsErrorWhenCheckoutIndexErrors() { + repos := suite.NewTestRepositoriesManager(suite.repoConfigDstDir) - anon := func() error { - err := suite.r.Overlay() - assert.NoError(suite.T(), err) + suite.mockRepo.EXPECT().Clone(gomock.Any(), gomock.Any()).Return(nil) + errors := errors.New("tests error") + suite.mockRepo.EXPECT().CheckoutIndex(gomock.Any(), gomock.Any()).Return(errors) - return err - } + err := repos.Overlay() + assert.Error(suite.T(), err) +} - dstDir, _ := git.FilePathAbs(suite.r.Repositories[0].DstDir) - got := git.MockRunCommand(anon) - want := []string{ - fmt.Sprintf( - "git clone https://example.com/user/repo1.git %s/https---example.com-user-repo1.git-abc1234", - suite.r.GiltDir, - ), - fmt.Sprintf("git -C %s/https---example.com-user-repo1.git-abc1234 reset --hard abc1234", - suite.r.GiltDir), - fmt.Sprintf( - "git -C %s/https---example.com-user-repo1.git-abc1234 checkout-index --force --all --prefix %s", - suite.r.GiltDir, - (dstDir + string(os.PathSeparator)), - ), - fmt.Sprintf( - "git clone https://example.com/user/repo2.git %s/https---example.com-user-repo2.git-abc1234", - suite.r.GiltDir, - ), - fmt.Sprintf("git -C %s/https---example.com-user-repo2.git-abc1234 reset --hard abc1234", - suite.r.GiltDir), - } +func (suite *RepositoriesPublicTestSuite) TestOverlayOkWhenSources() { + repos := suite.NewTestRepositoriesManager(suite.repoConfigSources) + + suite.mockRepo.EXPECT().Clone(gomock.Any(), gomock.Any()).Return(nil) + suite.mockRepo.EXPECT(). + CopySources(suite.repoConfigSources[0], filepath.Join(suite.giltDir, "cache/https---example.com-user-repo.git-abc1234")). + Return(nil) + + err := repos.Overlay() + assert.NoError(suite.T(), err) +} + +func (suite *RepositoriesPublicTestSuite) TestOverlayReturnsErrorWhenCopySourcesErrors() { + repos := suite.NewTestRepositoriesManager(suite.repoConfigSources) + + suite.mockRepo.EXPECT().Clone(gomock.Any(), gomock.Any()).Return(nil) + errors := errors.New("tests error") + suite.mockRepo.EXPECT().CopySources(gomock.Any(), gomock.Any()).Return(errors) - assert.Equal(suite.T(), want, got) + err := repos.Overlay() + assert.Error(suite.T(), err) } // In order for `go test` to run this suite, we need to create // a normal test function and pass our suite to suite.Run. -func TestRepositoriesTestSuite(t *testing.T) { - suite.Run(t, new(RepositoriesTestSuite)) +func TestRepositoriesPublicTestSuite(t *testing.T) { + suite.Run(t, new(RepositoriesPublicTestSuite)) } diff --git a/internal/repositories/repositories_test.go b/internal/repositories/repositories_test.go index 1c39bbb..733147c 100644 --- a/internal/repositories/repositories_test.go +++ b/internal/repositories/repositories_test.go @@ -21,19 +21,145 @@ package repositories import ( + "fmt" + "log/slog" + "os" + "os/user" "testing" + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + + "github.com/retr0h/go-gilt/internal/config" + "github.com/retr0h/go-gilt/internal/repository" ) type RepositoriesTestSuite struct { suite.Suite + + ctrl *gomock.Controller + mockRepo *repository.MockRepositoryManager + + appFs afero.Fs + giltDir string + logger *slog.Logger +} + +func (suite *RepositoriesTestSuite) NewTestRepositories( + giltDir string, +) *Repositories { + reposConfig := config.Repositories{ + Debug: false, + GiltFile: "Giltfile.yaml", + GiltDir: giltDir, + Repositories: []config.Repository{}, + } + + return New( + suite.appFs, + reposConfig, + suite.mockRepo, + suite.logger, + ) } func (suite *RepositoriesTestSuite) SetupTest() { + suite.ctrl = gomock.NewController(suite.T()) + suite.mockRepo = repository.NewMockRepositoryManager(suite.ctrl) + defer suite.ctrl.Finish() + + suite.appFs = afero.NewMemMapFs() + suite.giltDir = "/giltDir" + + suite.logger = slog.New(slog.NewTextHandler(os.Stdout, nil)) +} + +func (suite *RepositoriesTestSuite) TestexpandUserOk() { + originalCurrentUser := currentUser + currentUser = func() (*user.User, error) { + return &user.User{ + HomeDir: "/testUser", + }, nil + } + defer func() { currentUser = originalCurrentUser }() + + got, err := expandUser("~/foo/bar") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), got, "/testUser/foo/bar") +} + +func (suite *RepositoriesTestSuite) TestexpandUserReturnsError() { + originalCurrentUser := currentUser + currentUser = func() (*user.User, error) { + return nil, fmt.Errorf("failed to get current user") + } + defer func() { currentUser = originalCurrentUser }() + + _, err := expandUser("~/foo/bar") + assert.Error(suite.T(), err) } -func (suite *RepositoriesTestSuite) TearDownTest() { +func (suite *RepositoriesTestSuite) TestgetCloneDirOk() { + repos := suite.NewTestRepositories(suite.giltDir) + + got := repos.getCloneDir( + "/giltDir", + config.Repository{ + Version: "abc123", + }, + ) + assert.Equal(suite.T(), got, "/giltDir/-abc123") +} + +func (suite *RepositoriesTestSuite) TestgetCloneHash() { + repos := suite.NewTestRepositories(suite.giltDir) + + got := repos.getCloneDir( + "https://example.com/user/repo2.git", + config.Repository{ + Version: "abc123", + }, + ) + assert.Equal(suite.T(), got, "https:/example.com/user/repo2.git/-abc123") +} + +func (suite *RepositoriesTestSuite) TestgetGiltDir() { + repos := suite.NewTestRepositories(suite.giltDir) + + expectedDir := "/giltDir/cache" + got, err := repos.getGiltDir() + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), got, expectedDir) + + exists, err := afero.Exists(suite.appFs, expectedDir) + assert.NoError(suite.T(), err) + assert.True(suite.T(), exists) +} + +func (suite *RepositoriesTestSuite) TestgetGiltDirReturnsErrorWhenexpandUserErrors() { + repos := suite.NewTestRepositories("~/foo/bar") + + originalCurrentUser := currentUser + currentUser = func() (*user.User, error) { + return nil, fmt.Errorf("failed to get current user") + } + defer func() { currentUser = originalCurrentUser }() + + // _, err := expandUser("~/foo/bar") + // assert.Error(suite.T(), err) + _, err := repos.getGiltDir() + assert.Error(suite.T(), err) + + // expectedDir := "/giltDir/cache" + // got, err := repos.getGiltDir() + // assert.NoError(suite.T(), err) + // assert.Equal(suite.T(), got, expectedDir) + + // exists, err := afero.Exists(suite.appFs, expectedDir) + // assert.NoError(suite.T(), err) + // assert.True(suite.T(), exists) } // In order for `go test` to run this suite, we need to create diff --git a/internal/repositories/types.go b/internal/repositories/types.go index bf79fe1..2244009 100644 --- a/internal/repositories/types.go +++ b/internal/repositories/types.go @@ -18,21 +18,22 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -// Package repositories TODO change to lower cases members. package repositories import ( - "github.com/retr0h/go-gilt/internal/repository" + "log/slog" + + "github.com/spf13/afero" + + "github.com/retr0h/go-gilt/internal" + "github.com/retr0h/go-gilt/internal/config" ) // Repositories perform repository operations. type Repositories struct { - // Debug enable or disable debug option set from CLI. - Debug bool `mapstruture:"debug"` - // GiltFile path to Gilt's config file option set from CLI. - GiltFile string `mapstructure:"giltFile"` - // GiltDir path to Gilt's clone dir option set from CLI. - GiltDir string `mapstructure:"giltDir"` - // Repositories a slice of repository configurations to overlay. - Repositories []repository.Repository `mapstruture:"repositories"` + appFs afero.Fs + config config.Repositories + logger *slog.Logger + + repoManager internal.RepositoryManager } diff --git a/internal/repository.go b/internal/repository.go new file mode 100644 index 0000000..c8792b8 --- /dev/null +++ b/internal/repository.go @@ -0,0 +1,32 @@ +// Copyright (c) 2023 John Dewey + +// 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. + +package internal + +import ( + "github.com/retr0h/go-gilt/internal/config" +) + +// RepositoryManager manager responsible for Repository operations. +type RepositoryManager interface { + Clone(config config.Repository, cloneDir string) error + CheckoutIndex(config config.Repository, cloneDir string) error + CopySources(config config.Repository, cloneDir string) error +} diff --git a/internal/ioutil/copy.go b/internal/repository/copy.go similarity index 71% rename from internal/ioutil/copy.go rename to internal/repository/copy.go index c93777a..09a49de 100644 --- a/internal/ioutil/copy.go +++ b/internal/repository/copy.go @@ -18,35 +18,53 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -package ioutil +package repository import ( "fmt" "io" + "log/slog" "os" "path/filepath" - "github.com/logrusorgru/aurora/v4" "github.com/spf13/afero" ) -// copyFile copies the contents of the file named src to the file named +// NewCopy factory to create a new copy instance. +func NewCopy( + appFs afero.Fs, + logger *slog.Logger, +) *Copy { + return &Copy{ + appFs: appFs, + logger: logger, + } +} + +// CopyFile copies the contents of the file named src to the file named // by dst. The file will be created if it does not already exist. If the // destination file exists, all it's contents will be replaced by the contents // of the source file. The file mode will be copied from the source and // the copied data is synced/flushed to stable storage. -func copyFile( - appFs afero.Fs, +func (r *Copy) CopyFile( src string, dst string, ) (err error) { - in, err := appFs.Open(src) + baseSrc := filepath.Base(src) + + r.logger.Info( + "copying file", + slog.String("srcFile", baseSrc), + slog.String("dstFile", dst), + ) + + in, err := r.appFs.Open(src) if err != nil { return err } defer func() { _ = in.Close() }() - out, err := appFs.Create(dst) + out, err := r.appFs.Create(dst) if err != nil { return err } @@ -66,12 +84,12 @@ func copyFile( return err } - si, err := appFs.Stat(src) + si, err := r.appFs.Stat(src) if err != nil { return err } - err = appFs.Chmod(dst, si.Mode()) + err = r.appFs.Chmod(dst, si.Mode()) if err != nil { return err } @@ -79,36 +97,24 @@ func copyFile( return nil } -// CopyFile copies src file to dst. -func CopyFile( - appFs afero.Fs, - src string, - dst string, -) error { - baseSrc := filepath.Base(src) - msg := fmt.Sprintf( - "%-4s - Copying file '%s' to '%s'", - "", - aurora.Cyan(baseSrc), - aurora.Cyan(dst), - ) - fmt.Println(msg) - - return copyFile(appFs, src, dst) -} - -// copyDir recursively copies a directory tree, attempting to preserve permissions. +// CopyDir recursively copies a directory tree, attempting to preserve permissions. // Source directory must exist, destination directory must *not* exist. // Symlinks are ignored and skipped. -func copyDir( - appFs afero.Fs, +func (r *Copy) CopyDir( src string, dst string, ) (err error) { src = filepath.Clean(src) dst = filepath.Clean(dst) + baseSrc := filepath.Base(src) + + r.logger.Info( + "copying dir", + slog.String("srcDir", baseSrc), + slog.String("dstDir", dst), + ) - si, err := appFs.Stat(src) + si, err := r.appFs.Stat(src) if err != nil { return err } @@ -116,22 +122,22 @@ func copyDir( return fmt.Errorf("source is not a directory") } - _, err = appFs.Stat(dst) + _, err = r.appFs.Stat(dst) if err != nil && !os.IsNotExist(err) { - return + return err } if err == nil { return fmt.Errorf("destination already exists") } - err = appFs.MkdirAll(dst, si.Mode()) + err = r.appFs.MkdirAll(dst, si.Mode()) if err != nil { - return + return err } - entries, err := afero.ReadDir(appFs, src) + entries, err := afero.ReadDir(r.appFs, src) if err != nil { - return + return err } for _, entry := range entries { @@ -145,42 +151,24 @@ func copyDir( return err } - entry, err = appFs.Stat(target) + entry, err = r.appFs.Stat(target) if err != nil { return err } } if entry.IsDir() { - err = CopyDir(appFs, srcPath, dstPath) + err = r.CopyDir(srcPath, dstPath) if err != nil { return err } } else { - err = CopyFile(appFs, srcPath, dstPath) + err = r.CopyFile(srcPath, dstPath) if err != nil { return err } } } - return -} - -// CopyDir copies src directory to dst. -func CopyDir( - appFs afero.Fs, - src string, - dst string, -) error { - baseSrc := filepath.Base(src) - msg := fmt.Sprintf( - "%-4s - Copying dir '%s' to '%s'", - "", - aurora.Cyan(baseSrc), - aurora.Cyan(dst), - ) - fmt.Println(msg) - - return copyDir(appFs, src, dst) + return nil } diff --git a/internal/repository/copy_mock.go b/internal/repository/copy_mock.go new file mode 100644 index 0000000..5a418f5 --- /dev/null +++ b/internal/repository/copy_mock.go @@ -0,0 +1,62 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/repository/types.go + +// Package repository is a generated GoMock package. +package repository + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockCopyManager is a mock of CopyManager interface. +type MockCopyManager struct { + ctrl *gomock.Controller + recorder *MockCopyManagerMockRecorder +} + +// MockCopyManagerMockRecorder is the mock recorder for MockCopyManager. +type MockCopyManagerMockRecorder struct { + mock *MockCopyManager +} + +// NewMockCopyManager creates a new mock instance. +func NewMockCopyManager(ctrl *gomock.Controller) *MockCopyManager { + mock := &MockCopyManager{ctrl: ctrl} + mock.recorder = &MockCopyManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCopyManager) EXPECT() *MockCopyManagerMockRecorder { + return m.recorder +} + +// CopyDir mocks base method. +func (m *MockCopyManager) CopyDir(src, dst string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CopyDir", src, dst) + ret0, _ := ret[0].(error) + return ret0 +} + +// CopyDir indicates an expected call of CopyDir. +func (mr *MockCopyManagerMockRecorder) CopyDir(src, dst interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyDir", reflect.TypeOf((*MockCopyManager)(nil).CopyDir), src, dst) +} + +// CopyFile mocks base method. +func (m *MockCopyManager) CopyFile(src, dst string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CopyFile", src, dst) + ret0, _ := ret[0].(error) + return ret0 +} + +// CopyFile indicates an expected call of CopyFile. +func (mr *MockCopyManagerMockRecorder) CopyFile(src, dst interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyFile", reflect.TypeOf((*MockCopyManager)(nil).CopyFile), src, dst) +} diff --git a/internal/repository/copy_public_test.go b/internal/repository/copy_public_test.go new file mode 100644 index 0000000..fa2ff9b --- /dev/null +++ b/internal/repository/copy_public_test.go @@ -0,0 +1,140 @@ +// Copyright (c) 2018 John Dewey + +// 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. + +package repository_test + +import ( + "log/slog" + "os" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/retr0h/go-gilt/internal/repository" +) + +type CopyPublicTestSuite struct { + suite.Suite + + appFs afero.Fs + cloneDir string + dstDir string +} + +func (suite *CopyPublicTestSuite) NewTestCopyManager() repository.CopyManager { + return repository.NewCopy( + suite.appFs, + slog.New(slog.NewTextHandler(os.Stdout, nil)), + ) +} + +func (suite *CopyPublicTestSuite) SetupTest() { + suite.appFs = afero.NewMemMapFs() + suite.cloneDir = "/cloneDir" + suite.dstDir = "/dstDir" +} + +func (suite *CopyPublicTestSuite) TestCopyFileOk() { + cm := suite.NewTestCopyManager() + + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir, "srcDir"), + srcFile: filepath.Join(suite.cloneDir, "srcDir", "1.txt"), + }, + } + createFileSpecs(specs) + + assertFile := filepath.Join(suite.dstDir, "1.txt") + err := cm.CopyFile(specs[0].srcFile, assertFile) + assert.NoError(suite.T(), err) + + got, _ := afero.Exists(suite.appFs, assertFile) + assert.True(suite.T(), got) +} + +func (suite *CopyPublicTestSuite) TestCopyFileReturnsError() { + cm := suite.NewTestCopyManager() + + assertFile := "/invalidSrc" + err := cm.CopyFile("/invalidSrc", assertFile) + assert.Error(suite.T(), err) + + got, _ := afero.Exists(suite.appFs, assertFile) + assert.False(suite.T(), got) +} + +func (suite *CopyPublicTestSuite) TestCopyDirOk() { + cm := suite.NewTestCopyManager() + + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir, "srcDir"), + srcFile: filepath.Join(suite.cloneDir, "srcDir", "1.txt"), + }, + } + createFileSpecs(specs) + + assertFile := filepath.Join(suite.dstDir, "1.txt") + err := cm.CopyDir(specs[0].srcDir, suite.dstDir) + assert.NoError(suite.T(), err) + + got, _ := afero.Exists(suite.appFs, assertFile) + assert.True(suite.T(), got) +} + +func (suite *CopyPublicTestSuite) TestCopyDirReturnsError() { + cm := suite.NewTestCopyManager() + + assertDir := "/invalidDir" + err := cm.CopyDir("/invalidSrc", assertDir) + assert.Error(suite.T(), err) + + got, _ := afero.Exists(suite.appFs, assertDir) + assert.False(suite.T(), got) +} + +func (suite *CopyPublicTestSuite) TestCopyDirReturnsErrorWhenSrcIsNotDir() { + cm := suite.NewTestCopyManager() + + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir, "srcDir"), + srcFile: filepath.Join(suite.cloneDir, "srcDir", "1.txt"), + }, + } + createFileSpecs(specs) + + assertFile := specs[0].srcFile + err := cm.CopyDir(assertFile, suite.dstDir) + assert.Error(suite.T(), err) +} + +// In order for `go test` to run this suite, we need to create +// a normal test function and pass our suite to suite.Run. +func TestCopyPublicTestSuite(t *testing.T) { + suite.Run(t, new(CopyPublicTestSuite)) +} diff --git a/internal/repository/helper_test.go b/internal/repository/helper_test.go new file mode 100644 index 0000000..f62b53a --- /dev/null +++ b/internal/repository/helper_test.go @@ -0,0 +1,50 @@ +// Copyright (c) 2018 John Dewey + +// 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. + +package repository_test + +import ( + "github.com/spf13/afero" +) + +type FileSpec struct { + appFs afero.Fs + srcDir string + srcFile string + srcFiles []string +} + +func createFileSpecs(specs []FileSpec) { + for _, s := range specs { + if s.srcDir != "" { + _ = s.appFs.MkdirAll(s.srcDir, 0o755) + } + + if s.srcFile != "" { + _, _ = s.appFs.Create(s.srcFile) + } + + if len(s.srcFiles) > 0 { + for _, f := range s.srcFiles { + _, _ = s.appFs.Create(f) + } + } + } +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index a3402a3..1eeb998 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -21,75 +21,156 @@ package repository import ( - "errors" "fmt" + "log/slog" "os" "path/filepath" "strings" - "github.com/logrusorgru/aurora/v4" + "github.com/spf13/afero" - "github.com/retr0h/go-gilt/internal/ioutil" + "github.com/retr0h/go-gilt/internal" + "github.com/retr0h/go-gilt/internal/config" ) -// GetCloneDir returns the path to the Repository's clone directory. -func (r *Repository) GetCloneDir() string { - return filepath.Join(r.GiltDir, r.getCloneHash()) +// New factory to create a new Repository instance. +func New( + appFs afero.Fs, + copyManager CopyManager, + gitManager internal.GitManager, + logger *slog.Logger, +) *Repository { + return &Repository{ + appFs: appFs, + copyManager: copyManager, + gitManager: gitManager, + logger: logger, + } } -func (r *Repository) getCloneHash() string { - replacer := strings.NewReplacer( - "/", "-", - ":", "-", - ) - replacedGitURL := replacer.Replace(r.Git) +// glob searches for files matching pattern in the directory dir +// and appends them to matches, returning the updated slice. +// If the directory cannot be opened, glob returns the existing matches. +// New matches are added in lexicographical order. +// Inspired by io/fs/glob since afero doesn't support filepath.Glob. +func glob( + fs afero.Fs, + dir string, + pattern string, +) ([]string, error) { + m := []string{} + infos, err := afero.ReadDir(fs, dir) + if err != nil { + return nil, err + } - return fmt.Sprintf("%s-%s", replacedGitURL, r.Version) + for _, info := range infos { + n := info.Name() + matched, err := filepath.Match(pattern, n) + if err != nil { + return m, err + } + + if matched { + m = append(m, filepath.Join(dir, n)) + } + } + + return m, nil } -// CopySources copy Repository.Src to Repository.DstFile or Repository.DstDir. -func (r *Repository) CopySources() error { - cloneDir := r.GetCloneDir() +// Clone clone Repository.Git to Repository.getCloneDir, and hard checkout +// to Repository.Version. +func (r *Repository) Clone( + c config.Repository, + cloneDir string, +) error { + r.logger.Info( + "cloning", + slog.String("repository", c.Git), + slog.String("version", c.Version), + slog.String("dstDir", cloneDir), + ) - msg := fmt.Sprintf("%-2s [%s]:", "", aurora.Magenta(cloneDir)) - fmt.Println(msg) + if _, err := r.appFs.Stat(cloneDir); os.IsNotExist(err) { + if err := r.gitManager.Clone(c.Git, cloneDir); err != nil { + return err + } - for _, rSource := range r.Sources { - srcFullPath := filepath.Join(cloneDir, rSource.Src) - globbedSrc, err := filepath.Glob(srcFullPath) + if err := r.gitManager.Reset(cloneDir, c.Version); err != nil { + return err + } + } else { + r.logger.Warn( + "clone already exists", + slog.String("dstDir", cloneDir), + ) + } + + return nil +} + +// CheckoutIndex checkout Repository.Git to Repository.DstDir. +func (r *Repository) CheckoutIndex( + c config.Repository, + cloneDir string, +) error { + return r.gitManager.CheckoutIndex(c.DstDir, cloneDir) +} + +// CopySources copy Repository.Src to Repository.DstFile or Repository.DstDir. +func (r *Repository) CopySources( + c config.Repository, + cloneDir string, +) error { + for _, source := range c.Sources { + parts := strings.Split( + source.Src, + string(os.PathSeparator), + ) // break up source.Src path + head := parts[0 : len(parts)-1] // take all path parts but last + tail := parts[len(parts)-1] // take the last path part + cloneDirWithSrcPath := filepath.Join( + cloneDir, + strings.Join(head, string(os.PathSeparator)), + ) // join clone dir with head + globbedSrc, err := glob( + r.appFs, + cloneDirWithSrcPath, + tail, + ) // tail is used by glob for path matching if err != nil { return err } for _, src := range globbedSrc { - // The source is a file. - if info, err := os.Stat(src); err == nil && info.Mode().IsRegular() { - // ... and the destination is declared a directory. - if rSource.DstFile != "" { - if err := ioutil.CopyFile(r.AppFs, src, rSource.DstFile); err != nil { + // The source is a file + if info, err := r.appFs.Stat(src); err == nil && info.Mode().IsRegular() { + // ... and the dst is declared a directory + if source.DstFile != "" { + if err := r.copyManager.CopyFile(src, source.DstFile); err != nil { return err } - } else if rSource.DstDir != "" { - // ... and the destination directory exists. - if info, err := os.Stat(rSource.DstDir); err == nil && info.Mode().IsDir() { - srcBaseFile := filepath.Base(src) - newDst := filepath.Join(rSource.DstDir, srcBaseFile) - if err := ioutil.CopyFile(r.AppFs, src, newDst); err != nil { - return err - } - } else { - msg := fmt.Sprintf("DstDir '%s' does not exist", rSource.DstDir) - return errors.New(msg) + } else if source.DstDir != "" { + // ... and create te dst directory + if err := r.appFs.MkdirAll(source.DstDir, 0o755); err != nil { + return fmt.Errorf("unable to create dest dir: %s", err) + } + srcBaseFile := filepath.Base(src) + newDst := filepath.Join(source.DstDir, srcBaseFile) + if err := r.copyManager.CopyFile(src, newDst); err != nil { + return err } } - // The source is a directory. - } else if info, err := os.Stat(src); err == nil && info.Mode().IsDir() { - if info, err := os.Stat(rSource.DstDir); err == nil && info.Mode().IsDir() { - if err := os.RemoveAll(rSource.DstDir); err != nil { + // The source is a directory + } else if info, err := r.appFs.Stat(src); err == nil && info.Mode().IsDir() { + // ... and dst dir exists + if info, err := r.appFs.Stat(source.DstDir); err == nil && info.Mode().IsDir() { + if err := r.appFs.RemoveAll(source.DstDir); err != nil { return err } } - if err := ioutil.CopyDir(r.AppFs, src, rSource.DstDir); err != nil { + if err := r.copyManager.CopyDir(src, source.DstDir); err != nil { return err } } diff --git a/internal/repository/repository_integration_test.go b/internal/repository/repository_integration_test.go deleted file mode 100644 index f11db5e..0000000 --- a/internal/repository/repository_integration_test.go +++ /dev/null @@ -1,222 +0,0 @@ -//go:build integration -// +build integration - -// Copyright (c) 2018 John Dewey - -// 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. - -package repository_test - -import ( - // "errors" - "fmt" - // "os" - "path/filepath" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - - "github.com/retr0h/go-gilt/internal/git" - "github.com/retr0h/go-gilt/internal/repositories" - "github.com/retr0h/go-gilt/internal/repository" - helper "github.com/retr0h/go-gilt/internal/testing" -) - -type RepositoryIntegrationTestSuite struct { - suite.Suite - r repository.Repository - rr repositories.Repositories - g *git.Git - fakeFs afero.Fs - realFs afero.Fs -} - -func (suite *RepositoryIntegrationTestSuite) unmarshalYAML(data []byte) error { - return helper.UnmarshalYAML([]byte(data), &suite.rr.Repositories) -} - -func (suite *RepositoryIntegrationTestSuite) SetupTest() { - suite.rr = repositories.Repositories{} - suite.g = git.NewGit(suite.rr.Debug) - suite.fakeFs = afero.NewMemMapFs() - suite.realFs = afero.NewOsFs() -} - -func (suite *RepositoryIntegrationTestSuite) TearDownTest() { - helper.RemoveTempDirectory(suite.r.GiltDir) -} - -func (suite *RepositoryIntegrationTestSuite) TestCopySourcesHasErrorWhenDstDirDoesNotExist() { - data := ` -- git: https://github.com/lorin/openstack-ansible-modules.git - version: 2677cc3 - sources: - - src: "*_manage" - dstDir: invalid/path -` - err := suite.unmarshalYAML([]byte(data)) - assert.NoError(suite.T(), err) - - r := suite.rr.Repositories[0] - r.GiltDir = helper.CreateTempDirectory() - r.AppFs = suite.fakeFs - - err = suite.g.Clone(r) - assert.NoError(suite.T(), err) - - err = r.CopySources() - assert.Error(suite.T(), err) -} - -func (suite *RepositoryIntegrationTestSuite) TestCopySourcesHasErrorWhenFileCopyFails() { - tempDir := helper.CreateTempDirectory() - dstDir := filepath.Join(tempDir, "library") - data := fmt.Sprintf(` -- git: https://github.com/lorin/openstack-ansible-modules.git - version: 2677cc3 - sources: - - src: cinder_manage - dstDir: %s -`, dstDir) - err := suite.unmarshalYAML([]byte(data)) - assert.NoError(suite.T(), err) - - r := suite.rr.Repositories[0] - r.GiltDir = tempDir - r.AppFs = suite.fakeFs - - err = suite.g.Clone(r) - assert.NoError(suite.T(), err) - - suite.fakeFs.MkdirAll(dstDir, 0o755) - - err = r.CopySources() - assert.Error(suite.T(), err) -} - -func (suite *RepositoryIntegrationTestSuite) TestCopySourcesHasErrorWhenDstFileDoesNotExist() { - data := ` -- git: https://github.com/lorin/openstack-ansible-modules.git - version: 2677cc3 - sources: - - src: cinder_manage - dstFile: invalid/path -` - err := suite.unmarshalYAML([]byte(data)) - assert.NoError(suite.T(), err) - - r := suite.rr.Repositories[0] - r.GiltDir = helper.CreateTempDirectory() - r.AppFs = suite.fakeFs - - err = suite.g.Clone(r) - assert.NoError(suite.T(), err) - - err = r.CopySources() - assert.Error(suite.T(), err) -} - -func (suite *RepositoryIntegrationTestSuite) TestCopySourcesCopiesFile() { - tempDir := helper.CreateTempDirectory() - dstFile := filepath.Join(tempDir, "cinder_manage") - dstDir := filepath.Join(tempDir, "library") - dstDirFile := filepath.Join(tempDir, "library", "glance_manage") - data := fmt.Sprintf(` -- git: https://github.com/lorin/openstack-ansible-modules.git - version: 2677cc3 - sources: - - src: cinder_manage - dstFile: %s - - src: glance_manage - dstDir: %s -`, dstFile, dstDir) - err := suite.unmarshalYAML([]byte(data)) - assert.NoError(suite.T(), err) - - r := suite.rr.Repositories[0] - r.GiltDir = tempDir - r.AppFs = suite.realFs - suite.realFs.MkdirAll(dstDir, 0o755) - - err = suite.g.Clone(r) - assert.NoError(suite.T(), err) - - err = r.CopySources() - assert.NoError(suite.T(), err) - assert.FileExistsf(suite.T(), dstFile, "File does not exist") - assert.FileExistsf(suite.T(), dstDirFile, "File does not exist") -} - -func (suite *RepositoryIntegrationTestSuite) TestCopySourcesHasErrorWhenDirExistsAndDirCopyFails() { - tempDir := helper.CreateTempDirectory() - dstDir := filepath.Join(tempDir, "tests") - data := fmt.Sprintf(` -- git: https://github.com/lorin/openstack-ansible-modules.git - version: 2677cc3 - sources: - - src: tests - dstDir: %s -`, dstDir) - err := suite.unmarshalYAML([]byte(data)) - assert.NoError(suite.T(), err) - - r := suite.rr.Repositories[0] - r.GiltDir = tempDir - r.AppFs = suite.fakeFs - - err = suite.g.Clone(r) - assert.NoError(suite.T(), err) - - err = r.CopySources() - assert.Error(suite.T(), err) -} - -func (suite *RepositoryIntegrationTestSuite) TestCopySourcesCopiesDir() { - tempDir := helper.CreateTempDirectory() - dstDir := filepath.Join(tempDir, "tests") - data := fmt.Sprintf(` -- git: https://github.com/lorin/openstack-ansible-modules.git - version: 2677cc3 - sources: - - src: tests - dstDir: %s -`, dstDir) - err := suite.unmarshalYAML([]byte(data)) - assert.NoError(suite.T(), err) - - r := suite.rr.Repositories[0] - r.GiltDir = tempDir - r.AppFs = suite.realFs - suite.realFs.MkdirAll(dstDir, 0o755) - - err = suite.g.Clone(r) - assert.NoError(suite.T(), err) - - err = r.CopySources() - assert.NoError(suite.T(), err) - assert.DirExistsf(suite.T(), dstDir, "Dir does not exist") -} - -// In order for `go test` to run this suite, we need to create -// a normal test function and pass our suite to suite.Run. -func TestRepositoryIntegrationTestSuite(t *testing.T) { - suite.Run(t, new(RepositoryIntegrationTestSuite)) -} diff --git a/internal/repository/repository_mock.go b/internal/repository/repository_mock.go new file mode 100644 index 0000000..702349b --- /dev/null +++ b/internal/repository/repository_mock.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/repository.go + +// Package repository is a generated GoMock package. +package repository + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + config "github.com/retr0h/go-gilt/internal/config" +) + +// MockRepositoryManager is a mock of RepositoryManager interface. +type MockRepositoryManager struct { + ctrl *gomock.Controller + recorder *MockRepositoryManagerMockRecorder +} + +// MockRepositoryManagerMockRecorder is the mock recorder for MockRepositoryManager. +type MockRepositoryManagerMockRecorder struct { + mock *MockRepositoryManager +} + +// NewMockRepositoryManager creates a new mock instance. +func NewMockRepositoryManager(ctrl *gomock.Controller) *MockRepositoryManager { + mock := &MockRepositoryManager{ctrl: ctrl} + mock.recorder = &MockRepositoryManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepositoryManager) EXPECT() *MockRepositoryManagerMockRecorder { + return m.recorder +} + +// CheckoutIndex mocks base method. +func (m *MockRepositoryManager) CheckoutIndex(config config.Repository, cloneDir string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckoutIndex", config, cloneDir) + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckoutIndex indicates an expected call of CheckoutIndex. +func (mr *MockRepositoryManagerMockRecorder) CheckoutIndex(config, cloneDir interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckoutIndex", reflect.TypeOf((*MockRepositoryManager)(nil).CheckoutIndex), config, cloneDir) +} + +// Clone mocks base method. +func (m *MockRepositoryManager) Clone(config config.Repository, cloneDir string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Clone", config, cloneDir) + ret0, _ := ret[0].(error) + return ret0 +} + +// Clone indicates an expected call of Clone. +func (mr *MockRepositoryManagerMockRecorder) Clone(config, cloneDir interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clone", reflect.TypeOf((*MockRepositoryManager)(nil).Clone), config, cloneDir) +} + +// CopySources mocks base method. +func (m *MockRepositoryManager) CopySources(config config.Repository, cloneDir string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CopySources", config, cloneDir) + ret0, _ := ret[0].(error) + return ret0 +} + +// CopySources indicates an expected call of CopySources. +func (mr *MockRepositoryManagerMockRecorder) CopySources(config, cloneDir interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopySources", reflect.TypeOf((*MockRepositoryManager)(nil).CopySources), config, cloneDir) +} diff --git a/internal/repository/repository_public_test.go b/internal/repository/repository_public_test.go index fd21b01..91adb1f 100644 --- a/internal/repository/repository_public_test.go +++ b/internal/repository/repository_public_test.go @@ -21,43 +21,376 @@ package repository_test import ( + "errors" + "log/slog" + "os" "path/filepath" "testing" + "github.com/golang/mock/gomock" + "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/retr0h/go-gilt/internal" + "github.com/retr0h/go-gilt/internal/config" + "github.com/retr0h/go-gilt/internal/git" "github.com/retr0h/go-gilt/internal/repository" - helper "github.com/retr0h/go-gilt/internal/testing" ) -type RepositoryTestSuite struct { +type RepositoryPublicTestSuite struct { suite.Suite - r repository.Repository + + ctrl *gomock.Controller + mockGit *git.MockGitManager + mockCopyManager *repository.MockCopyManager + + appFs afero.Fs + cloneDir string + dstDir string + gitURL string + gitVersion string + logger *slog.Logger +} + +func (suite *RepositoryPublicTestSuite) NewRepositoryManager() internal.RepositoryManager { + return repository.New( + suite.appFs, + suite.mockCopyManager, + suite.mockGit, + suite.logger, + ) +} + +func (suite *RepositoryPublicTestSuite) SetupTest() { + suite.ctrl = gomock.NewController(suite.T()) + suite.mockGit = git.NewMockGitManager(suite.ctrl) + suite.mockCopyManager = repository.NewMockCopyManager(suite.ctrl) + defer suite.ctrl.Finish() + + suite.appFs = afero.NewMemMapFs() + suite.cloneDir = "/cloneDir" + suite.dstDir = "/dstDir" + suite.gitURL = "https://example.com/user/repo.git" + suite.gitVersion = "abc123" + suite.logger = slog.New(slog.NewTextHandler(os.Stdout, nil)) } -func (suite *RepositoryTestSuite) SetupTest() { - suite.r = repository.Repository{ - Git: "https://example.com/user/repo.git", - Version: "abc1234", - DstDir: "path/user.repo", +func (suite *RepositoryPublicTestSuite) TestCloneOk() { + repo := suite.NewRepositoryManager() + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, } - suite.r.GiltDir = helper.CreateTempDirectory() + + gomock.InOrder( + suite.mockGit.EXPECT().Clone(suite.gitURL, suite.cloneDir).Return(nil), + suite.mockGit.EXPECT().Reset(suite.cloneDir, suite.gitVersion).Return(nil), + ) + + err := repo.Clone(c, suite.cloneDir) + assert.NoError(suite.T(), err) } -func (suite *RepositoryTestSuite) TearDownTest() { - helper.RemoveTempDirectory(suite.r.GiltDir) +func (suite *RepositoryPublicTestSuite) TestCloneReturnsErrorWhenCloneErrors() { + repo := suite.NewRepositoryManager() + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + } + + errors := errors.New("tests error") + gomock.InOrder( + suite.mockGit.EXPECT().Clone(suite.gitURL, suite.cloneDir).Return(errors), + suite.mockGit.EXPECT().Reset(suite.cloneDir, suite.gitVersion).Return(nil), + ) + + err := repo.Clone(c, suite.cloneDir) + assert.Error(suite.T(), err) } -func (suite *RepositoryTestSuite) TestGetCloneDir() { - got := suite.r.GetCloneDir() - want := filepath.Join(suite.r.GiltDir, "https---example.com-user-repo.git-abc1234") +func (suite *RepositoryPublicTestSuite) TestCloneReturnsErrorWhenResetErrors() { + repo := suite.NewRepositoryManager() + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + } + + errors := errors.New("tests error") + gomock.InOrder( + suite.mockGit.EXPECT().Clone(suite.gitURL, suite.cloneDir).Return(nil), + suite.mockGit.EXPECT().Reset(suite.cloneDir, suite.gitVersion).Return(errors), + ) + + err := repo.Clone(c, suite.cloneDir) + assert.Error(suite.T(), err) +} + +func (suite *RepositoryPublicTestSuite) TestCloneDoesNotCloneWhenCloneDirExists() { + repo := suite.NewRepositoryManager() + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + } + + _ = suite.appFs.MkdirAll(suite.cloneDir, 0o755) + + err := repo.Clone(c, suite.cloneDir) + assert.NoError(suite.T(), err) +} + +func (suite *RepositoryPublicTestSuite) TestCopySourcesOkWhenSourceIsDirAndDstDirDoesNotExist() { + repo := suite.NewRepositoryManager() + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir, "subDir"), + srcFile: filepath.Join(suite.cloneDir, "subDir", "1.txt"), + }, + } + createFileSpecs(specs) + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + Sources: []config.Sources{ + { + Src: filepath.Base(specs[0].srcDir), + DstDir: suite.dstDir, + }, + }, + } + + suite.mockCopyManager.EXPECT(). + CopyDir(filepath.Join(suite.cloneDir, c.Sources[0].Src), c.Sources[0].DstDir). + Return(nil) + + err := repo.CopySources(c, suite.cloneDir) + assert.NoError(suite.T(), err) +} + +func (suite *RepositoryPublicTestSuite) TestCopySourcesReturnsErrorWhenSourceIsDirAndDstDirDoesNotExistAndCopyDirErrors() { + repo := suite.NewRepositoryManager() + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir, "subDir"), + srcFile: filepath.Join(suite.cloneDir, "subDir", "1.txt"), + }, + } + createFileSpecs(specs) + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + Sources: []config.Sources{ + { + Src: filepath.Base(specs[0].srcDir), + DstDir: suite.dstDir, + }, + }, + } + + errors := errors.New("tests error") + suite.mockCopyManager.EXPECT().CopyDir(gomock.Any(), gomock.Any()).Return(errors) + + err := repo.CopySources(c, suite.cloneDir) + assert.Error(suite.T(), err) +} + +func (suite *RepositoryPublicTestSuite) TestCopySourcesOkWhenSourceIsDirAndDstDirExists() { + repo := suite.NewRepositoryManager() + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir, "subDir"), + srcFile: filepath.Join(suite.cloneDir, "subDir", "1.txt"), + }, + } + createFileSpecs(specs) + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + Sources: []config.Sources{ + { + Src: filepath.Base(specs[0].srcDir), + DstDir: suite.dstDir, + }, + }, + } + + suite.mockCopyManager.EXPECT(). + CopyDir(filepath.Join(suite.cloneDir, c.Sources[0].Src), c.Sources[0].DstDir). + Return(nil) + + // create dstDir + _ = suite.appFs.MkdirAll(suite.dstDir, 0o755) + err := repo.CopySources(c, suite.cloneDir) + assert.NoError(suite.T(), err) +} + +func (suite *RepositoryPublicTestSuite) TestCopySourcesOkWhenSourceIsFilesAndDstDir() { + repo := suite.NewRepositoryManager() + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir, "subDir"), + srcFiles: []string{ + filepath.Join(suite.cloneDir, "subDir", "1.txt"), + filepath.Join(suite.cloneDir, "subDir", "cinder_manage"), + filepath.Join(suite.cloneDir, "subDir", "nova_manage"), + filepath.Join(suite.cloneDir, "subDir", "glance_manage"), + }, + }, + { + appFs: suite.appFs, + srcDir: suite.cloneDir, + srcFiles: []string{ + filepath.Join(suite.cloneDir, "1.txt"), + filepath.Join(suite.cloneDir, "cinder_manage"), + filepath.Join(suite.cloneDir, "nova_manage"), + filepath.Join(suite.cloneDir, "glance_manage"), + }, + }, + } + createFileSpecs(specs) + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + Sources: []config.Sources{ + { + Src: "subDir/*_manage", + DstDir: suite.dstDir, + }, + { + Src: "*_manage", + DstDir: suite.dstDir, + }, + }, + } + + suite.mockCopyManager.EXPECT(). + CopyFile(filepath.Join(suite.cloneDir, "subDir", "cinder_manage"), filepath.Join(suite.dstDir, "cinder_manage")). + Return(nil) + suite.mockCopyManager.EXPECT(). + CopyFile(filepath.Join(suite.cloneDir, "subDir", "glance_manage"), filepath.Join(suite.dstDir, "glance_manage")). + Return(nil) + suite.mockCopyManager.EXPECT(). + CopyFile(filepath.Join(suite.cloneDir, "subDir", "nova_manage"), filepath.Join(suite.dstDir, "nova_manage")). + Return(nil) + + suite.mockCopyManager.EXPECT(). + CopyFile(filepath.Join(suite.cloneDir, "cinder_manage"), filepath.Join(suite.dstDir, "cinder_manage")). + Return(nil) + suite.mockCopyManager.EXPECT(). + CopyFile(filepath.Join(suite.cloneDir, "glance_manage"), filepath.Join(suite.dstDir, "glance_manage")). + Return(nil) + suite.mockCopyManager.EXPECT(). + CopyFile(filepath.Join(suite.cloneDir, "nova_manage"), filepath.Join(suite.dstDir, "nova_manage")). + Return(nil) + + err := repo.CopySources(c, suite.cloneDir) + assert.NoError(suite.T(), err) +} + +func (suite *RepositoryPublicTestSuite) TestCopySourcesReturnsErrorWhenSourceIsFilesAndDstDirAndCopyFileErrors() { + repo := suite.NewRepositoryManager() + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir, "subDir"), + srcFiles: []string{ + filepath.Join(suite.cloneDir, "subDir", "1.txt"), + }, + }, + } + createFileSpecs(specs) + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + Sources: []config.Sources{ + { + Src: "subDir/*.txt", + DstDir: suite.dstDir, + }, + }, + } + + errors := errors.New("tests error") + suite.mockCopyManager.EXPECT().CopyFile(gomock.Any(), gomock.Any()).Return(errors) + + err := repo.CopySources(c, suite.cloneDir) + assert.Error(suite.T(), err) +} + +func (suite *RepositoryPublicTestSuite) TestCopySourcesOkWhenSourceIsFileAndDstFile() { + repo := suite.NewRepositoryManager() + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir), + srcFile: filepath.Join(suite.cloneDir, "1.txt"), + }, + } + createFileSpecs(specs) + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + Sources: []config.Sources{ + { + Src: "1.txt", + DstFile: filepath.Join(suite.dstDir, "1.txt"), + }, + }, + } + + suite.mockCopyManager.EXPECT(). + CopyFile(filepath.Join(suite.cloneDir, "1.txt"), filepath.Join(suite.dstDir, "1.txt")). + Return(nil) + + err := repo.CopySources(c, suite.cloneDir) + assert.NoError(suite.T(), err) +} + +func (suite *RepositoryPublicTestSuite) TestCopySourcesReturnsErrorWhenSourceIsFileAndDstFileAndCopyFileErrors() { + repo := suite.NewRepositoryManager() + specs := []FileSpec{ + { + appFs: suite.appFs, + srcDir: filepath.Join(suite.cloneDir), + srcFile: filepath.Join(suite.cloneDir, "1.txt"), + }, + } + createFileSpecs(specs) + + c := config.Repository{ + Git: suite.gitURL, + Version: suite.gitVersion, + Sources: []config.Sources{ + { + Src: "1.txt", + DstFile: filepath.Join(suite.dstDir, "1.txt"), + }, + }, + } + + errors := errors.New("tests error") + suite.mockCopyManager.EXPECT().CopyFile(gomock.Any(), gomock.Any()).Return(errors) - assert.Equal(suite.T(), want, got) + err := repo.CopySources(c, suite.cloneDir) + assert.Error(suite.T(), err) } // In order for `go test` to run this suite, we need to create // a normal test function and pass our suite to suite.Run. -func TestRepositoryTestSuite(t *testing.T) { - suite.Run(t, new(RepositoryTestSuite)) +func TestRepositoryPublicTestSuite(t *testing.T) { + suite.Run(t, new(RepositoryPublicTestSuite)) } diff --git a/internal/repository/types.go b/internal/repository/types.go index 6f7f50e..1d19e05 100644 --- a/internal/repository/types.go +++ b/internal/repository/types.go @@ -18,36 +18,32 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -// Package repository TODO change to lower cases members. package repository import ( + "log/slog" + "github.com/spf13/afero" -) -// Sources mapping of files and/or directories needing copied. -type Sources struct { - // Src source file or directory to copy. - Src string `mapstructure:"src"` - // DstFile destination of file copy. - DstFile string `mapstructure:"dstFile"` - // DstDir destination of directory copy. - DstDir string `mapstructure:"dstDir"` -} + "github.com/retr0h/go-gilt/internal" +) // Repository contains the repository's details for cloning. type Repository struct { - // Git url of Git repository to clone. - Git string `mapstructure:"git"` - // Version of Git repository to use. - Version string `mapstructure:"version"` - // DstDir destination directory to copy clone to. - DstDir string `mapstructure:"dstDir"` - // Sources containing files and/or directories to copy. - Sources []Sources `mapstructure:"sources"` - // GiltDir path to Gilt's clone dir option set from CLI. - GiltDir string - // AppFs file system mocking. - // TODO(retr0h): Needs passed in as repo pattern. - AppFs afero.Fs + appFs afero.Fs + copyManager CopyManager + gitManager internal.GitManager + logger *slog.Logger +} + +// CopyManager manager responsible for Copy operations. +type CopyManager interface { + CopyDir(src string, dst string) error + CopyFile(src string, dst string) error +} + +// Copy copy implementation. +type Copy struct { + appFs afero.Fs + logger *slog.Logger } diff --git a/test/integration/test_cli.bats b/test/integration/test_cli.bats index 168fc74..863fc62 100644 --- a/test/integration/test_cli.bats +++ b/test/integration/test_cli.bats @@ -31,7 +31,6 @@ setup() { GILT_CLONED_REPO_2=${GILT_DIR}/cache/https---github.com-lorin-openstack-ansible-modules.git-2677cc3 GILT_CLONED_REPO_1_DST_DIR=${GILT_TEST_BASE_DIR}/retr0h.ansible-etcd - # TODO(retr0h): go-gilt should create this dir mkdir -p ${GILT_DIR} mkdir -p ${GILT_LIBRARY_DIR} @@ -107,14 +106,14 @@ teardown() { [ "$status" -eq 0 ] echo "${output}" | grep "[https://github.com/retr0h/ansible-etcd.git@77a95b7]" - echo "${output}" | grep -E ".*Cloning to.*https---github.com-retr0h-ansible-etcd.git-77a95b7" + echo "${output}" | grep -E ".*cloning.*https---github.com-retr0h-ansible-etcd.git-77a95b7" } @test "invoke gilt overlay when already cloned" { run bash -c "cd ${GILT_TEST_BASE_DIR}; go run ${GILT_PROGRAM} overlay" run bash -c "cd ${GILT_TEST_BASE_DIR}; go run ${GILT_PROGRAM} overlay" - echo "${output}" | grep "Clone already exists" + echo "${output}" | grep "clone already exists" } @test "invoke gilt overlay and clone" {