mirror of https://github.com/gogits/gogs.git
Adam Strzelecki
9 years ago
9 changed files with 505 additions and 1 deletions
@ -0,0 +1,65 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"log" |
||||
"net/url" |
||||
|
||||
"github.com/codegangsta/cli" |
||||
|
||||
"github.com/gogits/gogs/models" |
||||
"github.com/gogits/gogs/modules/importer" |
||||
"github.com/gogits/gogs/modules/setting" |
||||
) |
||||
|
||||
var CmdImport = cli.Command{ |
||||
Name: "import", |
||||
Usage: "Import foreign project tracking service", |
||||
Description: `Import connects to foreign project tracking service API and imports all common entities such as users, projects, issues into Gogs. |
||||
This can be used to migrate from other service to Gogs.`, |
||||
Subcommands: []cli.Command{ |
||||
{ |
||||
Name: "gitlab", |
||||
Usage: "import from GitLab", |
||||
Action: importGitLab, |
||||
Flags: []cli.Flag{ |
||||
stringFlag("url", "", "GitLab service base URL"), |
||||
stringFlag("token", "", "private authentication token"), |
||||
}, |
||||
}, |
||||
}, |
||||
Flags: []cli.Flag{ |
||||
stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"), |
||||
boolFlag("verbose, v", "show process details"), |
||||
}, |
||||
} |
||||
|
||||
func importGitLab(ctx *cli.Context) { |
||||
baseUrl, err := url.Parse(ctx.String("url")) |
||||
if err != nil { |
||||
log.Fatal("Required --url parameter is missing or not valid URL") |
||||
} |
||||
if len(baseUrl.Path) > 0 { |
||||
log.Fatal("Provided --url parameter must not contain path") |
||||
} |
||||
token := ctx.String("token") |
||||
if len(token) == 0 { |
||||
log.Fatal("Missing required --token parameter") |
||||
} |
||||
|
||||
if ctx.IsSet("config") { |
||||
setting.CustomConf = ctx.String("config") |
||||
} |
||||
setting.NewContext() |
||||
models.LoadConfigs() |
||||
models.SetEngine() |
||||
|
||||
if importer.ImportGitLab(baseUrl, token) == nil { |
||||
log.Println("Finished importing!") |
||||
} else { |
||||
log.Println("Import failed!") |
||||
} |
||||
} |
@ -0,0 +1,303 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package importer |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"log" |
||||
"net/url" |
||||
"sort" |
||||
"time" |
||||
|
||||
"github.com/gogits/gogs/models" |
||||
) |
||||
|
||||
func ImportGitLab(baseUrl *url.URL, token string) error { |
||||
if err := importGitLabUsers(baseUrl, token); err != nil { |
||||
return err |
||||
} |
||||
if err := importGitLabOrgs(baseUrl, token); err != nil { |
||||
return err |
||||
} |
||||
if err := importGitLabRepos(baseUrl, token); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api
|
||||
// https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/api/users.md
|
||||
|
||||
type GitLabUser struct { |
||||
Id int64 `json:"id"` |
||||
Name string `json:"username"` |
||||
FullName string `json:"name"` |
||||
Email string `json:"email"` |
||||
Passwd string `json:"password_hash"` // this requires API patch (returns bcrypt hash)
|
||||
State string `json:"state"` // "active", "inactive"
|
||||
IsAdmin bool `json:"is_admin"` |
||||
Website string `json:"website_url"` |
||||
Created time.Time `json:"created_at"` |
||||
} |
||||
type GitLabUsers []GitLabUser |
||||
|
||||
func (slice GitLabUsers) Len() int { return len(slice) } |
||||
func (slice GitLabUsers) Less(i, j int) bool { return slice[i].Id < slice[j].Id } |
||||
func (slice GitLabUsers) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] } |
||||
|
||||
type GitLabKey struct { |
||||
Id int64 `json:"id"` |
||||
Name string `json:"title"` |
||||
Key string `json:"key"` |
||||
Created time.Time `json:"created_at"` |
||||
} |
||||
type GitLabKeys []GitLabKey |
||||
|
||||
func (slice GitLabKeys) Len() int { return len(slice) } |
||||
func (slice GitLabKeys) Less(i, j int) bool { return slice[i].Id < slice[j].Id } |
||||
func (slice GitLabKeys) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] } |
||||
|
||||
func importGitLabUsers(baseUrl *url.URL, token string) error { |
||||
var remoteUsers GitLabUsers |
||||
if err := fetchObjects("/api/v3/users", baseUrl, token, &remoteUsers); err != nil { |
||||
return err |
||||
} |
||||
sort.Sort(remoteUsers) |
||||
|
||||
for _, remoteUser := range remoteUsers { |
||||
if user, _ := models.GetUserByName(remoteUser.Name); user == nil { |
||||
log.Printf("User: %s (%s)", remoteUser.Name, remoteUser.FullName) |
||||
user := &models.User{ |
||||
Name: remoteUser.Name, |
||||
FullName: remoteUser.FullName, |
||||
Email: remoteUser.Email, |
||||
IsActive: remoteUser.State == "active", |
||||
IsAdmin: remoteUser.IsAdmin, |
||||
Website: remoteUser.Website, |
||||
Created: remoteUser.Created, |
||||
LoginType: models.PLAIN, |
||||
} |
||||
err := models.CreateUser(user) |
||||
if err != nil { |
||||
log.Fatalf("Cannot create user: %s", err) |
||||
return err |
||||
} |
||||
// extra step needed to put bcrypt hash directly
|
||||
user.Passwd = remoteUser.Passwd |
||||
models.UpdateUser(user) |
||||
var remoteKeys GitLabKeys |
||||
if err := fetchObjects(fmt.Sprintf("/api/v3/users/%d/keys", remoteUser.Id), baseUrl, token, &remoteKeys); err != nil { |
||||
return err |
||||
} else { |
||||
sort.Sort(remoteKeys) |
||||
for _, remoteKey := range remoteKeys { |
||||
if err := models.AddPublicKeyCreated(user.Id, remoteKey.Name, remoteKey.Key, remoteKey.Created); err != nil { |
||||
if models.IsErrKeyNameAlreadyUsed(err) { |
||||
log.Printf("Duplicate key: %s", remoteKey.Name, remoteUser.Name, err) |
||||
} else { |
||||
log.Fatalf("Cannot add public key %s to user %s: %s", remoteKey.Name, remoteUser.Name, err) |
||||
return err |
||||
} |
||||
} else { |
||||
log.Printf(" Keys <- %s", remoteKey.Name) |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
log.Printf("User %s already exists!", remoteUser.Name) |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/api/groups.md
|
||||
|
||||
type GitLabOrg struct { |
||||
Id int64 `json:"id"` |
||||
Name string `json:"path"` |
||||
FullName string `json:"name"` |
||||
Email string `json:"email"` |
||||
Description string `json:"description"` |
||||
} |
||||
type GitLabOrgs []GitLabOrg |
||||
|
||||
func (slice GitLabOrgs) Len() int { return len(slice) } |
||||
func (slice GitLabOrgs) Less(i, j int) bool { return slice[i].Id < slice[j].Id } |
||||
func (slice GitLabOrgs) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] } |
||||
|
||||
type GitLabAccessLevel int |
||||
|
||||
const ( |
||||
GitLabNoAccess GitLabAccessLevel = iota |
||||
GitLabGuest = 10 |
||||
GitLabReporter = 20 |
||||
GitLabDeveloper = 30 |
||||
GitLabMaster = 40 |
||||
GitLabOwner = 50 |
||||
) |
||||
|
||||
type GitLabMember struct { |
||||
Id int64 `json:"id"` |
||||
Name string `json:"username"` |
||||
FullName string `json:"name"` |
||||
Email string `json:"email"` |
||||
AccessLevel GitLabAccessLevel `json:"access_level"` |
||||
} |
||||
type GitLabMembers []GitLabMember |
||||
|
||||
func (slice GitLabMembers) Len() int { return len(slice) } |
||||
func (slice GitLabMembers) Less(i, j int) bool { return slice[i].AccessLevel > slice[j].AccessLevel } |
||||
func (slice GitLabMembers) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] } |
||||
|
||||
func importGitLabOrgs(baseUrl *url.URL, token string) error { |
||||
var remoteOrgs GitLabOrgs |
||||
if err := fetchObjects("/api/v3/groups", baseUrl, token, &remoteOrgs); err != nil { |
||||
return err |
||||
} |
||||
sort.Sort(remoteOrgs) |
||||
|
||||
for _, remoteOrg := range remoteOrgs { |
||||
if org, _ := models.GetOrgByName(remoteOrg.Name); org == nil { |
||||
var remoteMembers GitLabMembers |
||||
if err := fetchObjects(fmt.Sprintf("/api/v3/groups/%d/members", remoteOrg.Id), baseUrl, token, &remoteMembers); err != nil { |
||||
return err |
||||
} |
||||
if len(remoteMembers) == 0 { |
||||
log.Fatalf("No members in org: %s! Skipping", remoteOrg.Name) |
||||
continue |
||||
} |
||||
if owner, _ := models.GetUserByName(remoteMembers[0].Name); owner == nil { |
||||
log.Fatalf("Inexistent owner %s (%s) for org: %s!", remoteMembers[0].FullName, remoteMembers[0].Name, remoteOrg.Name) |
||||
} else { |
||||
log.Printf("Org: %s (%s)", remoteOrg.Name, remoteOrg.FullName) |
||||
log.Printf(" %s (%s) -> Owners", owner.FullName, owner.Name) |
||||
org := &models.User{ |
||||
Name: remoteOrg.Name, |
||||
FullName: remoteOrg.FullName, |
||||
Created: owner.Created, // GitLab does not expose creation time, use owner's
|
||||
IsActive: true, |
||||
Type: models.ORGANIZATION, |
||||
} |
||||
err := models.CreateOrganization(org, owner) |
||||
if err != nil { |
||||
log.Fatalf("Cannot create org: %s", err) |
||||
return err |
||||
} |
||||
ownersTeam, err := org.GetOwnerTeam() |
||||
if err != nil { |
||||
log.Fatalf("Cannot get org owners team: %s", err) |
||||
return err |
||||
} |
||||
var adminsTeam, writersTeam, readersTeam *models.Team |
||||
for index, remoteMember := range remoteMembers { |
||||
if index == 0 { |
||||
continue |
||||
} |
||||
if member, _ := models.GetUserByName(remoteMember.Name); owner == nil { |
||||
log.Fatalf("Inexistent member %s (%s) for org: %s!", remoteMember.FullName, remoteMember.Name, remoteOrg.Name) |
||||
} else { |
||||
var team *models.Team |
||||
switch { |
||||
case remoteMember.AccessLevel >= GitLabOwner: |
||||
team = ownersTeam |
||||
err = nil |
||||
case remoteMember.AccessLevel >= GitLabMaster: |
||||
team, err = getOrgTeam(org, &adminsTeam, "Admins", models.ACCESS_MODE_ADMIN) |
||||
case remoteMember.AccessLevel >= GitLabDeveloper: |
||||
team, err = getOrgTeam(org, &writersTeam, "Writers", models.ACCESS_MODE_WRITE) |
||||
case remoteMember.AccessLevel >= GitLabGuest: |
||||
team, err = getOrgTeam(org, &readersTeam, "Readers", models.ACCESS_MODE_READ) |
||||
} |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if err := team.AddMember(member.Id); err != nil { |
||||
log.Fatalf("Cannot add member %s (%s) to org: %s, %s", member.FullName, member.Name, remoteOrg.Name, err) |
||||
} |
||||
log.Printf(" %s (%s) -> %s", member.FullName, member.Name, team.Name) |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
log.Printf("Org %s already exists!", remoteOrg.Name) |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/api/projects.md
|
||||
|
||||
type GitLabNamespace struct { |
||||
Id int64 `json:"id"` |
||||
Name string `json:"path"` |
||||
FullName string `json:"name"` |
||||
Description string `json:"description"` |
||||
} |
||||
type GitLabRepo struct { |
||||
Id int64 `json:"id"` |
||||
Name string `json:"path"` |
||||
Description string `json:"description"` |
||||
Created time.Time `json:"created_at"` |
||||
IsPublic bool `json:"public"` |
||||
VisibilityLevel int `json:"visibility_level"` // (0) private, (10) internal, (20) public
|
||||
Owner GitLabUser `json:"owner"` |
||||
Namespace GitLabNamespace `json:"namespace"` |
||||
DefaultBranch string |
||||
} |
||||
type GitLabRepos []GitLabRepo |
||||
|
||||
func importGitLabRepos(baseUrl *url.URL, token string) error { |
||||
var remoteRepos GitLabRepos |
||||
if err := fetchObjects("/api/v3/projects/all?order_by=id&sort=asc", baseUrl, token, &remoteRepos); err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, remoteRepo := range remoteRepos { |
||||
if owner, _ := models.GetUserByName(remoteRepo.Namespace.Name); owner == nil { |
||||
log.Fatalf("Cannot find owner: %s for repo: %s", remoteRepo.Namespace.Name, remoteRepo.Name) |
||||
return errors.New("Cannot find repo owner") |
||||
} else { |
||||
if repo, _ := models.GetRepositoryByName(owner.Id, remoteRepo.Name); repo == nil { |
||||
log.Printf("Repo: %s (%s)", remoteRepo.Name, remoteRepo.Namespace.Name) |
||||
repo, err := models.CreateRepository(owner, models.CreateRepoOptions{ |
||||
Name: remoteRepo.Name, |
||||
Description: remoteRepo.Description, |
||||
IsPrivate: remoteRepo.VisibilityLevel == 0, |
||||
Created: remoteRepo.Created, |
||||
IsMirror: false, |
||||
AutoInit: true, |
||||
Readme: "Default", |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("Cannot create repo: %s", err) |
||||
return err |
||||
} |
||||
// NOTE: Gogs only supports plain list of collaborators in project, while GitLab has fine grained access control
|
||||
var remoteMembers GitLabMembers |
||||
if err := fetchObjects(fmt.Sprintf("/api/v3/projects/%d/members", remoteRepo.Id), baseUrl, token, &remoteMembers); err != nil { |
||||
return err |
||||
} |
||||
for _, remoteMember := range remoteMembers { |
||||
if member, _ := models.GetUserByName(remoteMember.Name); owner == nil { |
||||
log.Fatalf("Inexistent member %s (%s) for repo: %s!", remoteMember.FullName, remoteMember.Name, remoteRepo.Name) |
||||
} else { |
||||
if member.Id == owner.Id { |
||||
continue // skip if we got owner here
|
||||
} |
||||
if err := repo.AddCollaborator(member); err != nil { |
||||
log.Fatalf("Cannot add collaborator %s (%s) to repo: %s, %s", member.FullName, member.Name, remoteRepo.Name, err) |
||||
return err |
||||
} |
||||
log.Printf(" %s (%s) -> Collaborators", member.FullName, member.Name) |
||||
} |
||||
} |
||||
} else { |
||||
log.Printf("Repo %s already exists!", remoteRepo.Name) |
||||
} |
||||
} |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,110 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package importer |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"log" |
||||
"net/url" |
||||
"reflect" |
||||
"strings" |
||||
|
||||
"github.com/gogits/gogs/models" |
||||
"github.com/gogits/gogs/modules/httplib" |
||||
) |
||||
|
||||
func fetchURL(api string, baseUrl *url.URL, token string) ([]byte, error) { |
||||
baseUrl.Path = api |
||||
log.Printf("Fetching: %s", baseUrl) |
||||
req := httplib.Get(baseUrl.String()).Header("PRIVATE-TOKEN", token) |
||||
resp, err := req.Response() |
||||
if err != nil { |
||||
log.Fatalf("Cannot connect: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
if resp.StatusCode != 200 { |
||||
log.Fatalf("Unexpected server reponse: %s", resp.Status) |
||||
return nil, errors.New(resp.Status) |
||||
} |
||||
|
||||
defer resp.Body.Close() |
||||
body, err := ioutil.ReadAll(resp.Body) |
||||
if err != nil { |
||||
log.Fatalf("Cannot read: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
return body, nil |
||||
} |
||||
|
||||
func fetchObject(api string, baseUrl *url.URL, token string, out interface{}) error { |
||||
body, err := fetchURL(api, baseUrl, token) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
err = json.Unmarshal(body, out) |
||||
if err != nil { |
||||
log.Fatalf("Cannot decode: %s", err) |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func fetchObjects(api string, baseUrl *url.URL, token string, out interface{}) error { |
||||
outSliceValue := reflect.ValueOf(out).Elem() |
||||
slicePtrValue := reflect.New(outSliceValue.Type()) |
||||
|
||||
if strings.Contains(api, "?") { |
||||
api += "&" |
||||
} else { |
||||
api += "?" |
||||
} |
||||
|
||||
for page := 1; ; page++ { |
||||
body, err := fetchURL(fmt.Sprintf("%sper_page=100&page=%d", api, page), baseUrl, token) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
err = json.Unmarshal(body, slicePtrValue.Interface()) |
||||
if err != nil { |
||||
log.Fatalf("Cannot decode: %s", err) |
||||
return err |
||||
} |
||||
outSliceValue.Set(reflect.AppendSlice(outSliceValue, slicePtrValue.Elem())) |
||||
if slicePtrValue.Elem().Len() < 100 { |
||||
break |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func getOrgTeam(org *models.User, cached **models.Team, name string, access models.AccessMode) (*models.Team, error) { |
||||
if *cached != nil { |
||||
return *cached, nil |
||||
} |
||||
if team, err := org.GetTeam(name); err != nil && err != models.ErrTeamNotExist { |
||||
log.Fatalf("Cannot get %s team for org: %s, %s", name, org.Name, err) |
||||
return nil, err |
||||
} else if team != nil { |
||||
*cached = team |
||||
} else { |
||||
team := &models.Team{ |
||||
OrgID: org.Id, |
||||
Name: name, |
||||
Authorize: access, |
||||
} |
||||
if err := models.NewTeam(team); err != nil { |
||||
log.Fatalf("Cannot create %s team for org: %s, %s", name, org.Name, err) |
||||
return nil, err |
||||
} |
||||
*cached = team |
||||
} |
||||
return *cached, nil |
||||
} |
Loading…
Reference in new issue