Skip to content

Commit 2fb244d

Browse files
copy from source repo (#31)
Support `--from path-to-local-repository`
1 parent 612aa6a commit 2fb244d

File tree

5 files changed

+284
-12
lines changed

5 files changed

+284
-12
lines changed

pkg/avancement/pull_request.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import (
1111
// TODO: For the Head, should this try and determine whether or not this is a
1212
// fork ("user" of both repoURLs) and if so, simplify the Head?
1313
func makePullRequestInput(fromURL, toURL, branchName string) (*scm.PullRequestInput, error) {
14-
fromUser, fromRepo, err := util.ExtractUserAndRepo(fromURL)
14+
_, fromRepo, err := util.ExtractUserAndRepo(fromURL)
1515
if err != nil {
1616
return nil, err
1717
}
18-
_, toRepo, err := util.ExtractUserAndRepo(toURL)
18+
fromUser, toRepo, err := util.ExtractUserAndRepo(toURL)
1919
if err != nil {
2020
return nil, err
2121
}

pkg/avancement/service_manager.go

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/jenkins-x/go-scm/scm"
1111

1212
"github.com/rhd-gitops-example/services/pkg/git"
13+
"github.com/rhd-gitops-example/services/pkg/local"
1314
"github.com/rhd-gitops-example/services/pkg/util"
1415
)
1516

@@ -18,6 +19,7 @@ type ServiceManager struct {
1819
author *git.Author
1920
clientFactory scmClientFactory
2021
repoFactory repoFactory
22+
localFactory localFactory
2123
debug bool
2224
}
2325

@@ -27,6 +29,7 @@ const (
2729

2830
type scmClientFactory func(token string) *scm.Client
2931
type repoFactory func(url, localPath string, debug bool) (git.Repo, error)
32+
type localFactory func(localPath string, debug bool) (git.Source, error)
3033
type serviceOpt func(*ServiceManager)
3134

3235
// New creates and returns a new ServiceManager.
@@ -42,6 +45,10 @@ func New(cacheDir string, author *git.Author, opts ...serviceOpt) *ServiceManage
4245
r, err := git.NewRepository(url, localPath, debug)
4346
return git.Repo(r), err
4447
},
48+
localFactory: func(localPath string, debug bool)(git.Source, error){
49+
l := &local.Local{LocalPath: localPath, Debug: debug, Logger: log.Printf}
50+
return git.Source(l), nil
51+
},
4552
}
4653
for _, o := range opts {
4754
o(sm)
@@ -80,22 +87,30 @@ func (s *ServiceManager) Promote(serviceName, fromURL, toURL, newBranchName stri
8087
}
8188
}(keepCache, &reposToDelete)
8289

83-
source, err := s.checkoutSourceRepo(fromURL, fromBranch)
84-
if err != nil {
85-
return err
86-
}
87-
reposToDelete = append(reposToDelete, source)
88-
89-
destination, err = s.checkoutDestinationRepo(toURL, newBranchName)
90+
destination, err := s.checkoutDestinationRepo(toURL, newBranchName)
9091
if err != nil {
9192
return fmt.Errorf("failed to checkout repo: %w", err)
9293
}
9394
reposToDelete = append(reposToDelete, destination)
9495

95-
copied, err := git.CopyService(serviceName, source, destination)
96+
copied := []string{}
97+
if fromLocalRepo(fromURL) {
98+
localSource, err := s.localFactory(fromURL, s.debug)
99+
copied, err = local.CopyConfig(serviceName, localSource, destination)
100+
if err != nil {
101+
return fmt.Errorf("failed to setup local repo: %w", err)
102+
}
103+
} else {
104+
source, err = s.checkoutSourceRepo(fromURL, fromBranch)
105+
if err != nil {
106+
return fmt.Errorf("failed to checkout repo: %w", err)
107+
}
108+
reposToDelete = append(reposToDelete, source)
96109

97-
if err != nil {
98-
return fmt.Errorf("failed to copy service: %w", err)
110+
copied, err = git.CopyService(serviceName, source, destination)
111+
if err != nil {
112+
return fmt.Errorf("failed to copy service: %w", err)
113+
}
99114
}
100115
if err := destination.StageFiles(copied...); err != nil {
101116
return fmt.Errorf("failed to stage files: %w", err)
@@ -190,3 +205,10 @@ func addCredentialsIfNecessary(s string, a *git.Author) (string, error) {
190205
parsed.User = url.UserPassword("promotion", a.Token)
191206
return parsed.String(), nil
192207
}
208+
func fromLocalRepo(s string) bool {
209+
parsed, err := url.Parse(s)
210+
if err != nil || parsed.Scheme == "" {
211+
return true
212+
}
213+
return false
214+
}

pkg/avancement/service_manager_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package avancement
22

33
import (
44
"testing"
5+
"path/filepath"
6+
"strings"
7+
"path"
58

69
"github.com/jenkins-x/go-scm/scm"
710
fakescm "github.com/jenkins-x/go-scm/scm/driver/fake"
@@ -11,6 +14,7 @@ import (
1114

1215
const (
1316
dev = "https://example.com/testing/dev-env"
17+
ldev = "/root/repo"
1418
staging = "https://example.com/testing/staging-env"
1519
)
1620

@@ -60,6 +64,52 @@ func promoteWithSuccess(t *testing.T, keepCache bool) {
6064
}
6165
}
6266

67+
func TestPromoteLocalWithSuccessKeepCacheFalse(t *testing.T) {
68+
promoteLocalWithSuccess(t, false)
69+
}
70+
71+
func TestPromoteLocalWithSuccessKeepCacheTrue(t *testing.T) {
72+
promoteLocalWithSuccess(t, true)
73+
}
74+
75+
func promoteLocalWithSuccess(t *testing.T, keepCache bool) {
76+
dstBranch := "test-branch"
77+
author := &git.Author{Name: "Testing User", Email: "[email protected]", Token: "test-token"}
78+
client, _ := fakescm.NewDefault()
79+
fakeClientFactory := func(s string) *scm.Client {
80+
return client
81+
}
82+
stagingRepo := mock.New("/staging", "master")
83+
devRepo := NewLocal("/dev")
84+
85+
sm := New("tmp", author)
86+
sm.clientFactory = fakeClientFactory
87+
sm.repoFactory = func(url, _ string, _ bool) (git.Repo, error) {
88+
return git.Repo(stagingRepo), nil
89+
}
90+
sm.localFactory = func(path string, _ bool) (git.Source, error) {
91+
return git.Source(devRepo), nil
92+
}
93+
sm.debug = true
94+
devRepo.AddFiles("/config/myfile.yaml")
95+
96+
err := sm.Promote("my-service", ldev, staging, dstBranch, keepCache)
97+
if err != nil {
98+
t.Fatal(err)
99+
}
100+
101+
stagingRepo.AssertBranchCreated(t, "master", dstBranch)
102+
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "/dev/config/myfile.yaml", "/staging/services/my-service/base/config/myfile.yaml")
103+
stagingRepo.AssertCommit(t, dstBranch, defaultCommitMsg, author)
104+
stagingRepo.AssertPush(t, dstBranch)
105+
106+
if keepCache {
107+
stagingRepo.AssertNotDeletedFromCache(t)
108+
} else {
109+
stagingRepo.AssertDeletedFromCache(t)
110+
}
111+
}
112+
63113
func TestAddCredentials(t *testing.T) {
64114
testUser := &git.Author{Name: "Test User", Email: "[email protected]", Token: "test-token"}
65115
tests := []struct {
@@ -124,3 +174,47 @@ func TestPromoteWithCacheDeletionFailure(t *testing.T) {
124174
stagingRepo.AssertNotDeletedFromCache(t)
125175
devRepo.AssertDeletedFromCache(t)
126176
}
177+
178+
179+
type mockSource struct {
180+
files []string
181+
localPath string
182+
}
183+
184+
func NewLocal(localPath string) *mockSource {
185+
return &mockSource{localPath: localPath}
186+
}
187+
188+
// Walk: a mock function to emulate what happens in Repository.Walk()
189+
// The Mock version is different: it iterates over mockSource.files[] and then drives
190+
// the visitor callback in CopyService() as usual.
191+
//
192+
// To preserve the same behaviour, we see that Repository Walk receives /full/path/to/repo/services/service-name
193+
// and then calls filePath.Walk() on /full/path/to/repo/services/ .
194+
// When CopyService() drives Walk(), 'base' is typically services/service-name
195+
// Thus we take each /full/path/to/file/in/mockSource.files[] and split it at 'services/' as happens in the Walk() method we're mocking.
196+
func (s *mockSource) Walk(base string, cb func(string, string) error) error {
197+
if s.files == nil {
198+
return nil
199+
}
200+
base = filepath.Join(s.localPath, "config")
201+
202+
for _, f := range s.files {
203+
splitString := filepath.Dir(base) + "/"
204+
splitPoint := strings.Index(f, splitString) + len(splitString)
205+
prefix := f[:splitPoint]
206+
name := f[splitPoint:]
207+
err := cb(prefix, name)
208+
if err != nil {
209+
return err
210+
}
211+
}
212+
return nil
213+
}
214+
215+
func (s *mockSource) AddFiles(name string) {
216+
if s.files == nil {
217+
s.files = []string{}
218+
}
219+
s.files = append(s.files, path.Join(s.localPath, name))
220+
}

pkg/local/local.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package local
2+
3+
import (
4+
"os"
5+
"path"
6+
"path/filepath"
7+
"strings"
8+
"github.com/rhd-gitops-example/services/pkg/git"
9+
)
10+
11+
12+
type Local struct {
13+
LocalPath string
14+
Debug bool
15+
Logger func(fmt string, v ...interface{})
16+
}
17+
18+
19+
// CopyConfig takes the name of a service and a Source local service root path to be copied to a Destination.
20+
//
21+
// Only files under /path/to/local/repo/config/* are copied to the destination /services/[serviceName]/base/config/*
22+
//
23+
// Returns the list of files that were copied, and possibly an error.
24+
func CopyConfig(serviceName string, source git.Source, dest git.Destination) ([]string, error) {
25+
26+
copied := []string{}
27+
err := source.Walk("", func(prefix, name string) error {
28+
sourcePath := path.Join(prefix, name)
29+
destPath := pathForDestServiceConfig(serviceName, name)
30+
err := dest.CopyFile(sourcePath, destPath)
31+
if err == nil {
32+
copied = append(copied, destPath)
33+
}
34+
return err
35+
})
36+
return copied, err
37+
}
38+
39+
// pathForDestServiceConfig defines where in a 'gitops' repository the config
40+
// for a given service should live.
41+
func pathForDestServiceConfig(serviceName, name string) string {
42+
return filepath.Join("services/", serviceName, "base", name)
43+
}
44+
45+
func (l *Local) Walk(base string, cb func(prefix, name string) error) error {
46+
base = filepath.Join(l.LocalPath, "config")
47+
prefix := filepath.Dir(base) + "/"
48+
return filepath.Walk(base, func(path string, info os.FileInfo, err error) error {
49+
if err != nil {
50+
return err
51+
}
52+
53+
if info.IsDir() {
54+
return nil
55+
}
56+
return cb(prefix, strings.TrimPrefix(path, prefix))
57+
})
58+
}

pkg/local/local_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package local
2+
3+
import (
4+
"errors"
5+
"io"
6+
"path"
7+
"path/filepath"
8+
"reflect"
9+
"strings"
10+
"testing"
11+
12+
"github.com/google/go-cmp/cmp"
13+
)
14+
15+
func TestCopyConfig(t *testing.T) {
16+
s := &mockSource{localPath: "/tmp/testing"}
17+
files := []string{"config/my-file.yaml", "config/this-file.yaml"}
18+
copiedfiles := []string{"services/service-a/base/config/my-file.yaml", "services/service-a/base/config/this-file.yaml"}
19+
for _, f := range files {
20+
s.addFile(f)
21+
}
22+
d := &mockDestination{}
23+
24+
copied, err := CopyConfig("service-a", s, d)
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
d.assertFilesWritten(t, copiedfiles)
29+
if !reflect.DeepEqual(copiedfiles, copied) {
30+
t.Fatalf("failed to copy the files, got %#v, want %#v", copied, files)
31+
}
32+
}
33+
34+
35+
type mockSource struct {
36+
files []string
37+
localPath string
38+
}
39+
40+
// Walk: a mock function to emulate what happens in Repository.Walk()
41+
// The Mock version is different: it iterates over mockSource.files[] and then drives
42+
// the visitor callback in CopyService() as usual.
43+
//
44+
// To preserve the same behaviour, we see that Repository Walk receives /full/path/to/repo/services/service-name
45+
// and then calls filePath.Walk() on /full/path/to/repo/services/ .
46+
// When CopyService() drives Walk(), 'base' is typically services/service-name
47+
// Thus we take each /full/path/to/file/in/mockSource.files[] and split it at 'services/' as happens in the Walk() method we're mocking.
48+
func (s *mockSource) Walk(base string, cb func(string, string) error) error {
49+
if s.files == nil {
50+
return nil
51+
}
52+
base = filepath.Join(s.localPath, "config")
53+
54+
for _, f := range s.files {
55+
splitString := filepath.Dir(base) + "/"
56+
splitPoint := strings.Index(f, splitString) + len(splitString)
57+
prefix := f[:splitPoint]
58+
name := f[splitPoint:]
59+
err := cb(prefix, name)
60+
if err != nil {
61+
return err
62+
}
63+
}
64+
return nil
65+
}
66+
67+
func (s *mockSource) addFile(name string) {
68+
if s.files == nil {
69+
s.files = []string{}
70+
}
71+
s.files = append(s.files, path.Join(s.localPath, name))
72+
}
73+
74+
type mockDestination struct {
75+
written []string
76+
copyError error
77+
}
78+
79+
func (d *mockDestination) CopyFile(src, dst string) error {
80+
if d.written == nil {
81+
d.written = []string{}
82+
}
83+
if d.copyError != nil {
84+
return d.copyError
85+
}
86+
d.written = append(d.written, dst)
87+
return nil
88+
}
89+
90+
func (d *mockDestination) WriteFile(src io.Reader, dst string) error {
91+
return errors.New("not implemented just now")
92+
}
93+
94+
func (d *mockDestination) assertFilesWritten(t *testing.T, want []string) {
95+
if diff := cmp.Diff(want, d.written); diff != "" {
96+
t.Fatalf("written files do not match: %s", diff)
97+
}
98+
}

0 commit comments

Comments
 (0)