whatcanGOwrong
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.github_test_secrets
|
||||
@@ -0,0 +1,16 @@
|
||||
# github
|
||||
|
||||
This driver is catered for those that want to source migrations from [github.com](https://github.com). The URL scheme doesn't require a hostname, as it just simply defaults to `github.com`.
|
||||
|
||||
Authenticated client: `github://user:personal-access-token@owner/repo/path#ref`
|
||||
|
||||
Unauthenticated client: `github://owner/repo/path#ref`
|
||||
|
||||
| URL Query | WithInstance Config | Description |
|
||||
|------------|---------------------|-------------|
|
||||
| user | | (optional) The username of the user connecting |
|
||||
| personal-access-token | | (optional) An access token from GitHub (https://github.com/settings/tokens) |
|
||||
| owner | | the repo owner |
|
||||
| repo | | the name of the repository |
|
||||
| path | | path in repo to migrations |
|
||||
| ref | | (optional) can be a SHA, branch, or tag |
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE users (
|
||||
user_id integer unique,
|
||||
name varchar(40),
|
||||
email varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS city;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users ADD COLUMN city varchar(100);
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS users_email_index;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email);
|
||||
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS books;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE books (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
author varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS movies;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE movies (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
director varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
+1
@@ -0,0 +1 @@
|
||||
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
|
||||
@@ -0,0 +1,215 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
nurl "net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"github.com/google/go-github/v39/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
source.Register("github", &Github{})
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoUserInfo = fmt.Errorf("no username:token provided")
|
||||
ErrNoAccessToken = fmt.Errorf("no access token")
|
||||
ErrInvalidRepo = fmt.Errorf("invalid repo")
|
||||
ErrInvalidGithubClient = fmt.Errorf("expected *github.Client")
|
||||
ErrNoDir = fmt.Errorf("no directory")
|
||||
)
|
||||
|
||||
type Github struct {
|
||||
config *Config
|
||||
client *github.Client
|
||||
options *github.RepositoryContentGetOptions
|
||||
migrations *source.Migrations
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Owner string
|
||||
Repo string
|
||||
Path string
|
||||
Ref string
|
||||
}
|
||||
|
||||
func (g *Github) Open(url string) (source.Driver, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// client defaults to http.DefaultClient
|
||||
var client *http.Client
|
||||
if u.User != nil {
|
||||
password, ok := u.User.Password()
|
||||
if !ok {
|
||||
return nil, ErrNoUserInfo
|
||||
}
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: password},
|
||||
)
|
||||
client = oauth2.NewClient(context.Background(), ts)
|
||||
|
||||
}
|
||||
|
||||
gn := &Github{
|
||||
client: github.NewClient(client),
|
||||
migrations: source.NewMigrations(),
|
||||
options: &github.RepositoryContentGetOptions{Ref: u.Fragment},
|
||||
}
|
||||
|
||||
gn.ensureFields()
|
||||
|
||||
// set owner, repo and path in repo
|
||||
gn.config.Owner = u.Host
|
||||
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
if len(pe) < 1 {
|
||||
return nil, ErrInvalidRepo
|
||||
}
|
||||
gn.config.Repo = pe[0]
|
||||
if len(pe) > 1 {
|
||||
gn.config.Path = strings.Join(pe[1:], "/")
|
||||
}
|
||||
|
||||
if err := gn.readDirectory(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gn, nil
|
||||
}
|
||||
|
||||
func WithInstance(client *github.Client, config *Config) (source.Driver, error) {
|
||||
gn := &Github{
|
||||
client: client,
|
||||
config: config,
|
||||
migrations: source.NewMigrations(),
|
||||
options: &github.RepositoryContentGetOptions{Ref: config.Ref},
|
||||
}
|
||||
|
||||
if err := gn.readDirectory(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gn, nil
|
||||
}
|
||||
|
||||
func (g *Github) readDirectory() error {
|
||||
g.ensureFields()
|
||||
|
||||
fileContent, dirContents, _, err := g.client.Repositories.GetContents(
|
||||
context.Background(),
|
||||
g.config.Owner,
|
||||
g.config.Repo,
|
||||
g.config.Path,
|
||||
g.options,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fileContent != nil {
|
||||
return ErrNoDir
|
||||
}
|
||||
|
||||
for _, fi := range dirContents {
|
||||
m, err := source.DefaultParse(*fi.Name)
|
||||
if err != nil {
|
||||
continue // ignore files that we can't parse
|
||||
}
|
||||
if !g.migrations.Append(m) {
|
||||
return fmt.Errorf("unable to parse file %v", *fi.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Github) ensureFields() {
|
||||
if g.config == nil {
|
||||
g.config = &Config{}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Github) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Github) First() (version uint, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if v, ok := g.migrations.First(); !ok {
|
||||
return 0, &os.PathError{Op: "first", Path: g.config.Path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Github) Prev(version uint) (prevVersion uint, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if v, ok := g.migrations.Prev(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Github) Next(version uint) (nextVersion uint, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if v, ok := g.migrations.Next(version); !ok {
|
||||
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Github) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if m, ok := g.migrations.Up(version); ok {
|
||||
r, _, err := g.client.Repositories.DownloadContents(
|
||||
context.Background(),
|
||||
g.config.Owner,
|
||||
g.config.Repo,
|
||||
path.Join(g.config.Path, m.Raw),
|
||||
g.options,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return r, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
|
||||
func (g *Github) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||
g.ensureFields()
|
||||
|
||||
if m, ok := g.migrations.Down(version); ok {
|
||||
r, _, err := g.client.Repositories.DownloadContents(
|
||||
context.Background(),
|
||||
g.config.Owner,
|
||||
g.config.Repo,
|
||||
path.Join(g.config.Path, m.Raw),
|
||||
g.options,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return r, m.Identifier, nil
|
||||
}
|
||||
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
st "github.com/golang-migrate/migrate/v4/source/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var GithubTestSecret = "" // username:token
|
||||
|
||||
func init() {
|
||||
secrets, err := os.ReadFile(".github_test_secrets")
|
||||
if err == nil {
|
||||
GithubTestSecret = string(bytes.TrimSpace(secrets)[:])
|
||||
}
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
if len(GithubTestSecret) == 0 {
|
||||
t.Skip("test requires .github_test_secrets")
|
||||
}
|
||||
|
||||
g := &Github{}
|
||||
d, err := g.Open("github://" + GithubTestSecret + "@mattes/migrate_test_tmp/test#452b8003e7")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
st.Test(t, d)
|
||||
}
|
||||
|
||||
func TestDefaultClient(t *testing.T) {
|
||||
g := &Github{}
|
||||
owner := "golang-migrate"
|
||||
repo := "migrate"
|
||||
path := "source/github/examples/migrations"
|
||||
|
||||
url := fmt.Sprintf("github://%s/%s/%s", owner, repo, path)
|
||||
d, err := g.Open(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ver, err := d.First()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, uint(1085649617), ver)
|
||||
|
||||
ver, err = d.Next(ver)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, uint(1185749658), ver)
|
||||
}
|
||||
Reference in New Issue
Block a user