Browse Source

Add support for Migrations API

The Migrations API is the GitHub equivalent of Google TakeOut.
It allows you to create an archive of one or more repositories
that you can download and later use for import purposes.

See: https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/
for more information.

Fixes #292.

Change-Id: Id20624acb4f16a2aade9143e723c8eb85ec75402
Glenn Lewis 10 years ago
parent
commit
111e26bce5
3 changed files with 407 additions and 0 deletions
  1. +5
    -0
      github/github.go
  2. +225
    -0
      github/migrations.go
  3. +177
    -0
      github/migrations_test.go

+ 5
- 0
github/github.go View File

@ -62,6 +62,9 @@ const (
// https://developer.github.com/changes/2016-02-24-commit-reference-sha-api/
mediaTypeCommitReferenceSHAPreview = "application/vnd.github.chitauri-preview+sha"
// https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/
mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json"
)
// A Client manages communication with the GitHub API.
@ -97,6 +100,7 @@ type Client struct {
Search *SearchService
Users *UsersService
Licenses *LicensesService
Migrations *MigrationService
}
// ListOptions specifies the optional parameters to various List methods that
@ -159,6 +163,7 @@ func NewClient(httpClient *http.Client) *Client {
c.Search = &SearchService{client: c}
c.Users = &UsersService{client: c}
c.Licenses = &LicensesService{client: c}
c.Migrations = &MigrationService{client: c}
return c
}


+ 225
- 0
github/migrations.go View File

@ -0,0 +1,225 @@
// Copyright 2016 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package github
import (
"errors"
"fmt"
"net/http"
"strings"
)
// MigrationService provides access to the migration related functions
// in the GitHub API.
//
// GitHub API docs: https://developer.github.com/v3/migration/
type MigrationService struct {
client *Client
}
// Migration represents a GitHub migration (archival).
type Migration struct {
ID *int `json:"id,omitempty"`
GUID *string `json:"guid,omitempty"`
// State is the current state of a migration.
// Possible values are:
// "pending" which means the migration hasn't started yet,
// "exporting" which means the migration is in progress,
// "exported" which means the migration finished successfully, or
// "failed" which means the migration failed.
State *string `json:"state,omitempty"`
// LockRepositories indicates whether repositories are locked (to prevent
// manipulation) while migrating data.
LockRepositories *bool `json:"lock_repositories,omitempty"`
// ExcludeAttachments indicates whether attachments should be excluded from
// the migration (to reduce migration archive file size).
ExcludeAttachments *bool `json:"exclude_attachments,omitempty"`
URL *string `json:"url,omitempty"`
CreatedAt *string `json:"created_at,omitempty"`
UpdatedAt *string `json:"updated_at,omitempty"`
Repositories []*Repository `json:"repositories,omitempty"`
}
func (m Migration) String() string {
return Stringify(m)
}
// MigrationOptions specifies the optional parameters to Migration methods.
type MigrationOptions struct {
// LockRepositories indicates whether repositories should be locked (to prevent
// manipulation) while migrating data.
LockRepositories bool
// ExcludeAttachments indicates whether attachments should be excluded from
// the migration (to reduce migration archive file size).
ExcludeAttachments bool
}
// startMigration represents the body of a StartMigration request.
type startMigration struct {
// Repositories is a slice of repository names to migrate.
Repositories []string `json:"repositories,omitempty"`
// LockRepositories indicates whether repositories should be locked (to prevent
// manipulation) while migrating data.
LockRepositories *bool `json:"lock_repositories,omitempty"`
// ExcludeAttachments indicates whether attachments should be excluded from
// the migration (to reduce migration archive file size).
ExcludeAttachments *bool `json:"exclude_attachments,omitempty"`
}
// StartMigration starts the generation of a migration archive.
// repos is a slice of repository names to migrate.
//
// GitHub API docs: https://developer.github.com/v3/migration/migrations/#start-a-migration
func (s *MigrationService) StartMigration(org string, repos []string, opt *MigrationOptions) (*Migration, *Response, error) {
u := fmt.Sprintf("orgs/%v/migrations", org)
body := &startMigration{Repositories: repos}
if opt != nil {
body.LockRepositories = Bool(opt.LockRepositories)
body.ExcludeAttachments = Bool(opt.ExcludeAttachments)
}
req, err := s.client.NewRequest("POST", u, body)
if err != nil {
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches.
req.Header.Set("Accept", mediaTypeMigrationsPreview)
m := &Migration{}
resp, err := s.client.Do(req, m)
if err != nil {
return nil, resp, err
}
return m, resp, nil
}
// ListMigrations lists the most recent migrations.
//
// GitHub API docs: https://developer.github.com/v3/migration/migrations/#get-a-list-of-migrations
func (s *MigrationService) ListMigrations(org string) ([]*Migration, *Response, error) {
u := fmt.Sprintf("orgs/%v/migrations", org)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches.
req.Header.Set("Accept", mediaTypeMigrationsPreview)
var m []*Migration
resp, err := s.client.Do(req, &m)
if err != nil {
return nil, resp, err
}
return m, resp, nil
}
// MigrationStatus gets the status of a specific migration archive.
// id is the migration ID.
//
// GitHub API docs: https://developer.github.com/v3/migration/migrations/#get-the-status-of-a-migration
func (s *MigrationService) MigrationStatus(org string, id int) (*Migration, *Response, error) {
u := fmt.Sprintf("orgs/%v/migrations/%v", org, id)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches.
req.Header.Set("Accept", mediaTypeMigrationsPreview)
m := &Migration{}
resp, err := s.client.Do(req, m)
if err != nil {
return nil, resp, err
}
return m, resp, nil
}
// MigrationArchiveURL fetches a migration archive URL.
// id is the migration ID.
//
// GitHub API docs: https://developer.github.com/v3/migration/migrations/#download-a-migration-archive
func (s *MigrationService) MigrationArchiveURL(org string, id int) (url string, err error) {
u := fmt.Sprintf("orgs/%v/migrations/%v/archive", org, id)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return "", err
}
// TODO: remove custom Accept header when this API fully launches.
req.Header.Set("Accept", mediaTypeMigrationsPreview)
s.client.clientMu.Lock()
defer s.client.clientMu.Unlock()
// Disable the redirect mechanism because AWS fails if the GitHub auth token is provided.
var loc string
saveRedirect := s.client.client.CheckRedirect
s.client.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
loc = req.URL.String()
return errors.New("disable redirect")
}
defer func() { s.client.client.CheckRedirect = saveRedirect }()
_, err = s.client.Do(req, nil) // expect error from disable redirect
if err == nil {
return "", errors.New("expected redirect, none provided")
}
if !strings.Contains(err.Error(), "disable redirect") {
return "", err
}
return loc, nil
}
// DeleteMigration deletes a previous migration archive.
// id is the migration ID.
//
// GitHub API docs: https://developer.github.com/v3/migration/migrations/#delete-a-migration-archive
func (s *MigrationService) DeleteMigration(org string, id int) (*Response, error) {
u := fmt.Sprintf("orgs/%v/migrations/%v/archive", org, id)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return nil, err
}
// TODO: remove custom Accept header when this API fully launches.
req.Header.Set("Accept", mediaTypeMigrationsPreview)
return s.client.Do(req, nil)
}
// UnlockRepo unlocks a repository that was locked for migration.
// id is the migration ID.
// You should unlock each migrated repository and delete them when the migration
// is complete and you no longer need the source data.
//
// GitHub API docs: https://developer.github.com/v3/migration/migrations/#unlock-a-repository
func (s *MigrationService) UnlockRepo(org string, id int, repo string) (*Response, error) {
u := fmt.Sprintf("orgs/%v/migrations/%v/repos/%v/lock", org, id, repo)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return nil, err
}
// TODO: remove custom Accept header when this API fully launches.
req.Header.Set("Accept", mediaTypeMigrationsPreview)
return s.client.Do(req, nil)
}

+ 177
- 0
github/migrations_test.go View File

@ -0,0 +1,177 @@
// Copyright 2016 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package github
import (
"fmt"
"net/http"
"reflect"
"strings"
"testing"
)
func TestMigrationService_StartMigration(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/orgs/o/migrations", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testHeader(t, r, "Accept", mediaTypeMigrationsPreview)
w.WriteHeader(http.StatusCreated)
w.Write(migrationJSON)
})
opt := &MigrationOptions{
LockRepositories: true,
ExcludeAttachments: false,
}
got, _, err := client.Migrations.StartMigration("o", []string{"r"}, opt)
if err != nil {
t.Errorf("StartMigration returned error: %v", err)
}
if want := wantMigration; !reflect.DeepEqual(got, want) {
t.Errorf("StartMigration = %+v, want %+v", got, want)
}
}
func TestMigrationService_ListMigrations(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/orgs/o/migrations", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testHeader(t, r, "Accept", mediaTypeMigrationsPreview)
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("[%s]", migrationJSON)))
})
got, _, err := client.Migrations.ListMigrations("o")
if err != nil {
t.Errorf("ListMigrations returned error: %v", err)
}
if want := []*Migration{wantMigration}; !reflect.DeepEqual(got, want) {
t.Errorf("ListMigrations = %+v, want %+v", got, want)
}
}
func TestMigrationService_MigrationStatus(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/orgs/o/migrations/1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testHeader(t, r, "Accept", mediaTypeMigrationsPreview)
w.WriteHeader(http.StatusOK)
w.Write(migrationJSON)
})
got, _, err := client.Migrations.MigrationStatus("o", 1)
if err != nil {
t.Errorf("MigrationStatus returned error: %v", err)
}
if want := wantMigration; !reflect.DeepEqual(got, want) {
t.Errorf("MigrationStatus = %+v, want %+v", got, want)
}
}
func TestMigrationService_MigrationArchiveURL(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/orgs/o/migrations/1/archive", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testHeader(t, r, "Accept", mediaTypeMigrationsPreview)
http.Redirect(w, r, "/yo", http.StatusFound)
})
mux.HandleFunc("/yo", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
w.WriteHeader(http.StatusOK)
w.Write([]byte("0123456789abcdef"))
})
got, err := client.Migrations.MigrationArchiveURL("o", 1)
if err != nil {
t.Errorf("MigrationStatus returned error: %v", err)
}
if want := "/yo"; !strings.HasSuffix(got, want) {
t.Errorf("MigrationArchiveURL = %+v, want %+v", got, want)
}
}
func TestMigrationService_DeleteMigration(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/orgs/o/migrations/1/archive", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
testHeader(t, r, "Accept", mediaTypeMigrationsPreview)
w.WriteHeader(http.StatusNoContent)
})
if _, err := client.Migrations.DeleteMigration("o", 1); err != nil {
t.Errorf("DeleteMigration returned error: %v", err)
}
}
func TestMigrationService_UnlockRepo(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/orgs/o/migrations/1/repos/r/lock", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
testHeader(t, r, "Accept", mediaTypeMigrationsPreview)
w.WriteHeader(http.StatusNoContent)
})
if _, err := client.Migrations.UnlockRepo("o", 1, "r"); err != nil {
t.Errorf("UnlockRepo returned error: %v", err)
}
}
var migrationJSON = []byte(`{
"id": 79,
"guid": "0b989ba4-242f-11e5-81e1-c7b6966d2516",
"state": "pending",
"lock_repositories": true,
"exclude_attachments": false,
"url": "https://api.github.com/orgs/octo-org/migrations/79",
"created_at": "2015-07-06T15:33:38-07:00",
"updated_at": "2015-07-06T15:33:38-07:00",
"repositories": [
{
"id": 1296269,
"name": "Hello-World",
"full_name": "octocat/Hello-World",
"description": "This your first repo!"
}
]
}`)
var wantMigration = &Migration{
ID: Int(79),
GUID: String("0b989ba4-242f-11e5-81e1-c7b6966d2516"),
State: String("pending"),
LockRepositories: Bool(true),
ExcludeAttachments: Bool(false),
URL: String("https://api.github.com/orgs/octo-org/migrations/79"),
CreatedAt: String("2015-07-06T15:33:38-07:00"),
UpdatedAt: String("2015-07-06T15:33:38-07:00"),
Repositories: []*Repository{
{
ID: Int(1296269),
Name: String("Hello-World"),
FullName: String("octocat/Hello-World"),
Description: String("This your first repo!"),
},
},
}

Loading…
Cancel
Save