Initial version of migrate-gitlab-gogs issue migration
commit
418b37f518
@ -0,0 +1 @@
|
|||||||
|
migrate-gitlab-gogs
|
@ -0,0 +1,58 @@
|
|||||||
|
# Gitlab to Gogs Issue Migrator
|
||||||
|
|
||||||
|
This is a small app written in go that migrates issues from Gitlab to Gogs. It
|
||||||
|
uses the Gitlab and Gogs APIs, which results in some limitations. Specifically,
|
||||||
|
the Gogs API does not permit modification of timestamps.
|
||||||
|
|
||||||
|
# What it does
|
||||||
|
|
||||||
|
- Migrate issues from Gitlab to Gogs using the APIs
|
||||||
|
- Migrate issue comments from Gitlab to Gogs
|
||||||
|
- Migrate Milestones from Gitlab to Gogs
|
||||||
|
- Create issue labels as necessary
|
||||||
|
- Use a predefined user map to map Gitlab usernames to Gogs usernames
|
||||||
|
|
||||||
|
# What it does not do
|
||||||
|
|
||||||
|
- *Preserve timestamps*
|
||||||
|
- Create or migrate projects
|
||||||
|
- Create or migrate users
|
||||||
|
- Migrate the wiki
|
||||||
|
- Migrate git repositories
|
||||||
|
- Migrate attachments
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
- Install go
|
||||||
|
- Set GOPATH
|
||||||
|
- *Backup your Gogs data!* Your first migration may not go as planned
|
||||||
|
|
||||||
|
# Building and running
|
||||||
|
|
||||||
|
Text in single quotations are commands intended to be run on the command line.
|
||||||
|
Do not include the quotes when you enter them on the command line.
|
||||||
|
|
||||||
|
1. Clone this repository
|
||||||
|
2. Change directory into this repository
|
||||||
|
3. Run 'go get github.com/plouc/go-gitlab-client'
|
||||||
|
4. Run 'go get github.com/gogits/go-gogs-client'
|
||||||
|
5. Run 'go build'
|
||||||
|
6. Edit config.json
|
||||||
|
- Modify the Gitlab API URL to point to your server
|
||||||
|
- Change GITLABAPIKEY to your [Gitlab API key](https://www.safaribooksonline.com/library/view/gitlab-cookbook/9781783986842/ch06s05.html)
|
||||||
|
- Modify the Gogs API URL to point to your server
|
||||||
|
- Change GOGSAPIKEY to your Gogs API key
|
||||||
|
7. Run ./migrate-gitlab-gogs
|
||||||
|
8. Enter the number of the Gitlab project that you want to migrate and press
|
||||||
|
<enter>
|
||||||
|
9. Enter the number of the Gogs project that you want to migrate and press
|
||||||
|
<enter>
|
||||||
|
10. Review the simulation information (The script does not attempt to modify the
|
||||||
|
Gogs repository during a dry run. Therefore the actual migration may be
|
||||||
|
slightly different.)
|
||||||
|
11. If you are happy with the simulation, then press <Enter> to perform the actual
|
||||||
|
migration
|
||||||
|
|
||||||
|
After the migration, verify the results in your Gogs repository. If you are not
|
||||||
|
happy with the migration, then restore your backup and modify this script to
|
||||||
|
meet your needs.
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"GitlabURL": "http://gitlab.avocado.lan/api/v3",
|
||||||
|
"GitlabAPIKey": "Jb_zeFc99w8TDwC_e75-",
|
||||||
|
"GogsURL": "http://gogs.avocado.lan",
|
||||||
|
"GogsAPIKey": "a4f87d9ef73a557d6dba387ccda2344f9ae69385",
|
||||||
|
"UserMap": [{"From": "matthew", "To": "mruschmann"},
|
||||||
|
{"From": "cjohnson", "To": "cochocinco"},
|
||||||
|
{"From": "cochocinco", "To": "cjohnson"}]
|
||||||
|
}
|
@ -0,0 +1,345 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/xanzy/go-gitlab"
|
||||||
|
"github.com/gogits/go-gogs-client"
|
||||||
|
"fmt"
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/** The configuration that will be loaded from a JSON file */
|
||||||
|
type Configuration struct {
|
||||||
|
GitlabURL string; ///< URL to the Gitlab API interface
|
||||||
|
GitlabAPIKey string; ///< API Key for Gitlab
|
||||||
|
GogsURL string; ///< URL to the Gogs API interface
|
||||||
|
GogsAPIKey string; ///< API Key for Gogs
|
||||||
|
UserMap []UsersMap; ///< Map of Gitlab usernames to Gogs usernames
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** An instance of the Configuration to store the loaded data */
|
||||||
|
var config Configuration
|
||||||
|
|
||||||
|
|
||||||
|
/** The main routine for this program, which migrates a Gitlab project to Gogs
|
||||||
|
*
|
||||||
|
* 1. Reads the configuration from config.json.
|
||||||
|
* 2. Polls the Gitlab server for projects
|
||||||
|
* 3. Prompts the user for the Gitlab project to migrate
|
||||||
|
* 4. Pools the Gogs server for projects
|
||||||
|
* 5. Prompts the user for the Gogs project to migrate
|
||||||
|
* 6. Simulates the migration without writing to the Gogs API
|
||||||
|
* 7. Prompts the user to press <Enter> to perform the migration
|
||||||
|
* 8. Performs the migration of Gitlab project to Gogs Project
|
||||||
|
*/
|
||||||
|
func main() {
|
||||||
|
var projPtr []*gogs.Repository
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
found := false
|
||||||
|
num := 0
|
||||||
|
var gogsPrj *gogs.Repository
|
||||||
|
var gitPrj *gitlab.Project
|
||||||
|
|
||||||
|
// Load configuration from config.json
|
||||||
|
file, err9 := ioutil.ReadFile("./config.json")
|
||||||
|
CheckError(err9)
|
||||||
|
err9 = json.Unmarshal(file, &config)
|
||||||
|
fmt.Println("GitlabURL:", config.GitlabURL)
|
||||||
|
fmt.Println("GitlabAPIKey:", config.GitlabAPIKey)
|
||||||
|
fmt.Println("GogsURL:", config.GogsURL)
|
||||||
|
fmt.Println("GogsAPIKey:", config.GogsAPIKey)
|
||||||
|
fmt.Println("UserMap: [")
|
||||||
|
for i := range config.UserMap {
|
||||||
|
fmt.Println("\t", config.UserMap[i].From, "to", config.UserMap[i].To)
|
||||||
|
}
|
||||||
|
fmt.Println("]")
|
||||||
|
CheckError(err9)
|
||||||
|
|
||||||
|
// Have user select a source project from gitlab
|
||||||
|
git := gitlab.NewClient(nil, config.GitlabAPIKey)
|
||||||
|
git.SetBaseURL(config.GitlabURL)
|
||||||
|
opt := &gitlab.ListProjectsOptions{}
|
||||||
|
gitlabProjects, _, err := git.Projects.ListProjects(opt)
|
||||||
|
CheckError(err)
|
||||||
|
|
||||||
|
fmt.Println("")
|
||||||
|
for i := range gitlabProjects {
|
||||||
|
fmt.Println(gitlabProjects[i].ID, ":", gitlabProjects[i].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Select source gitlab project: ")
|
||||||
|
text, _ := reader.ReadString('\n')
|
||||||
|
text = strings.Trim(text, "\n")
|
||||||
|
|
||||||
|
for i := range gitlabProjects {
|
||||||
|
num, _ = strconv.Atoi(text)
|
||||||
|
if num == gitlabProjects[i].ID {
|
||||||
|
found = true
|
||||||
|
gitPrj = gitlabProjects[i]
|
||||||
|
} // else purposefully omitted
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
fmt.Println(text, "not found")
|
||||||
|
os.Exit(1)
|
||||||
|
} // else purposefully omitted
|
||||||
|
|
||||||
|
// Have user select a destination project in gogs
|
||||||
|
gg := gogs.NewClient(config.GogsURL, config.GogsAPIKey)
|
||||||
|
projPtr, err = gg.ListMyRepos()
|
||||||
|
CheckError(err)
|
||||||
|
|
||||||
|
fmt.Println("")
|
||||||
|
for i := range projPtr {
|
||||||
|
fmt.Println(projPtr[i].ID, ":", projPtr[i].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Select destination gogs project: ")
|
||||||
|
text, _ = reader.ReadString('\n')
|
||||||
|
text = strings.Trim(text, "\n")
|
||||||
|
|
||||||
|
for i := range projPtr {
|
||||||
|
num, _ = strconv.Atoi(text)
|
||||||
|
if int64(num) == projPtr[i].ID {
|
||||||
|
found = true
|
||||||
|
gogsPrj = projPtr[i]
|
||||||
|
} // else purposefully omitted
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
fmt.Println(text, "not found")
|
||||||
|
os.Exit(1)
|
||||||
|
} // else purposefully omitted
|
||||||
|
|
||||||
|
// Perform pre merge
|
||||||
|
fmt.Println("\nSimulated migration of", gitPrj.Name, "to", gogsPrj.Name)
|
||||||
|
DoMigration(true, git, gg, gitPrj.ID, gogsPrj.Name, gogsPrj.Owner.UserName)
|
||||||
|
|
||||||
|
// Perform actual migration
|
||||||
|
fmt.Println("\nCompleted simulation. Press <Enter> to perform migration...")
|
||||||
|
text, _ = reader.ReadString('\n')
|
||||||
|
DoMigration(false, git, gg, gitPrj.ID, gogsPrj.Name, gogsPrj.Owner.UserName)
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** A map of a milestone from its Gitlab ID to its new Gogs ID */
|
||||||
|
type MilestoneMap struct {
|
||||||
|
from int ///< ID in Gitlab
|
||||||
|
to int64 ///< New ID in Gogs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Performs a migration
|
||||||
|
* \param dryrun Does not write to the Gogs API if true
|
||||||
|
* \param git A gitlab client for making API calls
|
||||||
|
* \param gg A gogs client for making API calls
|
||||||
|
* \param gitPrj ID of the Gitlab project to migrate from
|
||||||
|
* \param gogsPrj The name of the Gitlab project to migrate into
|
||||||
|
* \param owner The owner of gogsPrj, which is required to make API calls
|
||||||
|
*
|
||||||
|
* This function migrates the Milestones first. It creates a map from the old
|
||||||
|
* Gitlab milestone IDs to the new Gogs milestone IDs. It uses these IDs to
|
||||||
|
* migrate the issues. For each issue, it migrates all of the comments.
|
||||||
|
*/
|
||||||
|
func DoMigration(dryrun bool, git *gitlab.Client, gg *gogs.Client, gitPrj int, gogsPrj string, owner string) {
|
||||||
|
var mmap []MilestoneMap
|
||||||
|
var listMiles gitlab.ListMilestonesOptions
|
||||||
|
var listIssues gitlab.ListProjectIssuesOptions
|
||||||
|
var listNotes gitlab.ListIssueNotesOptions
|
||||||
|
var err error
|
||||||
|
var milestone *gogs.Milestone
|
||||||
|
var issueIndex int64
|
||||||
|
issueNum := 0
|
||||||
|
sort := "asc"
|
||||||
|
|
||||||
|
listIssues.PerPage = 1000
|
||||||
|
listIssues.Sort = &sort
|
||||||
|
|
||||||
|
// Migrate all of the milestones
|
||||||
|
milestones, _, err0 := git.Milestones.ListMilestones(gitPrj, &listMiles)
|
||||||
|
CheckError(err0)
|
||||||
|
for i := range milestones {
|
||||||
|
fmt.Println("Create Milestone:", milestones[i].Title)
|
||||||
|
|
||||||
|
var opt gogs.CreateMilestoneOption
|
||||||
|
opt.Title = milestones[i].Title
|
||||||
|
opt.Description = milestones[i].Description
|
||||||
|
if !dryrun {
|
||||||
|
// Never write to the API during a dryrun
|
||||||
|
milestone, err = gg.CreateMilestone(owner, gogsPrj, opt)
|
||||||
|
CheckError(err)
|
||||||
|
mmap = append(mmap, MilestoneMap{milestones[i].ID, milestone.ID})
|
||||||
|
} // else purposefully omitted
|
||||||
|
|
||||||
|
if milestones[i].State == "closed" {
|
||||||
|
fmt.Println("Marking as closed")
|
||||||
|
var opt2 gogs.EditMilestoneOption
|
||||||
|
opt2.Title = opt.Title
|
||||||
|
opt2.Description = &opt.Description
|
||||||
|
opt2.State = &milestones[i].State
|
||||||
|
if !dryrun {
|
||||||
|
// Never write to the API during a dryrun
|
||||||
|
milestone, err = gg.EditMilestone(owner, gogsPrj, milestone.ID, opt2)
|
||||||
|
CheckError(err)
|
||||||
|
} // else purposefully omitted
|
||||||
|
} // else purposefully omitted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate all of the issues
|
||||||
|
issues, _, err1 := git.Issues.ListProjectIssues(gitPrj, &listIssues)
|
||||||
|
CheckError(err1)
|
||||||
|
for i := range issues {
|
||||||
|
issueNum++
|
||||||
|
if issueNum == issues[i].IID {
|
||||||
|
fmt.Println("Create Issue", issues[i].IID, ":", issues[i].Title)
|
||||||
|
|
||||||
|
var opt gogs.CreateIssueOption
|
||||||
|
opt.Title = issues[i].Title
|
||||||
|
opt.Body = issues[i].Description
|
||||||
|
opt.Assignee = MapUser(issues[i].Author.Username) // Gitlab user to Gogs user map
|
||||||
|
opt.Milestone = MapMilestone(mmap, issues[i].Milestone.ID)
|
||||||
|
opt.Closed = issues[i].State == "closed"
|
||||||
|
if !dryrun {
|
||||||
|
// Never write to the API during a dryrun
|
||||||
|
for k := range issues[i].Labels {
|
||||||
|
opt.Labels = append(opt.Labels, GetIssueLabel(git, gg, gitPrj, gogsPrj, owner, issues[i].Labels[k]));
|
||||||
|
}
|
||||||
|
issue, err6 := gg.CreateIssue(owner, gogsPrj, opt)
|
||||||
|
issueIndex = issue.Index
|
||||||
|
CheckError(err6)
|
||||||
|
} // else purposefully omitted
|
||||||
|
|
||||||
|
// Migrate all of the issue notes
|
||||||
|
notes, _, err2 := git.Notes.ListIssueNotes(gitPrj, issues[i].ID, &listNotes)
|
||||||
|
CheckError(err2)
|
||||||
|
for j := range notes {
|
||||||
|
fmt.Println("Adding note", notes[j].ID)
|
||||||
|
|
||||||
|
var opt2 gogs.CreateIssueCommentOption
|
||||||
|
//var opt3 gogs.EditIssueCommentOption
|
||||||
|
opt2.Body = notes[j].Body
|
||||||
|
//opt3.Body = notes[j].Body
|
||||||
|
if !dryrun {
|
||||||
|
// Never write to the API during a dryrun
|
||||||
|
_, err := gg.CreateIssueComment(owner, gogsPrj, issueIndex, opt2)
|
||||||
|
//_, err = gg.EditIssueComment(owner, gogsPrj, issueIndex, comment.ID, opt3)
|
||||||
|
CheckError(err)
|
||||||
|
} // else purposefully omitted
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO Create a temp issue and delete it later (MCR 9/29/16)
|
||||||
|
fmt.Println("Issues not in order!!")
|
||||||
|
fmt.Println("Preservation of skipped issues IDs is not implemented")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Find the ID of an label from its name or create a new label
|
||||||
|
* \param git A gitlab client for making API calls
|
||||||
|
* \param gg A gogs client for making API calls
|
||||||
|
* \param gitPrj ID of the Gitlab project to migrate from
|
||||||
|
* \param gogsPrj The name of the Gitlab project to migrate into
|
||||||
|
* \param owner The owner of gogsPrj, which is required to make API calls
|
||||||
|
* \param label The name of the label to find or create
|
||||||
|
* \return The ID of the tag in Gogs
|
||||||
|
*/
|
||||||
|
func GetIssueLabel(git *gitlab.Client, gg *gogs.Client, gitPrj int, gogsPrj string, owner string, label string) (int64) {
|
||||||
|
ID := int64(-1)
|
||||||
|
found := false
|
||||||
|
|
||||||
|
labels, err := gg.ListRepoLabels(owner, gogsPrj)
|
||||||
|
CheckError(err)
|
||||||
|
for i := range labels {
|
||||||
|
if labels[i].Name == label {
|
||||||
|
fmt.Println("Found label", label)
|
||||||
|
ID = labels[i].ID
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
tags, _, err1 := git.Labels.ListLabels(gitPrj)
|
||||||
|
CheckError(err1)
|
||||||
|
for i:= range tags {
|
||||||
|
if tags[i].Name == label {
|
||||||
|
fmt.Println("Create label", label, "color", tags[i].Color)
|
||||||
|
var opt gogs.CreateLabelOption
|
||||||
|
opt.Name = label
|
||||||
|
opt.Color = tags[i].Color
|
||||||
|
tag, err2 := gg.CreateLabel(owner, gogsPrj, opt)
|
||||||
|
CheckError(err2)
|
||||||
|
found = true
|
||||||
|
ID = tag.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // else purposefully omitted
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
fmt.Println("Unable to find label", label, "in gitlab!!")
|
||||||
|
os.Exit(5)
|
||||||
|
} // else purposefully omitted
|
||||||
|
|
||||||
|
return ID
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** An entry in the user map from Gitlab to Gogs */
|
||||||
|
type UsersMap struct {
|
||||||
|
From string ///< The user name to map from
|
||||||
|
To string ///< The user name to map to
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Maps a Gitlab user name to the desired Gogs user name
|
||||||
|
* @param user The Gitlab user name to map
|
||||||
|
* @return The Gogs user name
|
||||||
|
*/
|
||||||
|
func MapUser(user string) (string) {
|
||||||
|
u := user
|
||||||
|
|
||||||
|
for i := range config.UserMap {
|
||||||
|
if user == config.UserMap[i].From {
|
||||||
|
u = config.UserMap[i].To
|
||||||
|
} // else purposefully omitted
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Maps a Gitlab milestone to the desired Gogs milestone
|
||||||
|
* @param mmap An array of ID maps from Gitlab to Gogs
|
||||||
|
* @param user The Gitlab milestone to map
|
||||||
|
* @return The Gogs milstone
|
||||||
|
*/
|
||||||
|
func MapMilestone(mmap []MilestoneMap, ID int) (int64) {
|
||||||
|
var toID int64
|
||||||
|
toID = int64(ID)
|
||||||
|
|
||||||
|
for i := range mmap {
|
||||||
|
if (mmap[i].from == ID) {
|
||||||
|
toID = mmap[i].to
|
||||||
|
} // else purposefully omitted
|
||||||
|
}
|
||||||
|
|
||||||
|
return toID
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Checks an error code and exists if not nil
|
||||||
|
* @param err The error code to check
|
||||||
|
*/
|
||||||
|
func CheckError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
} // else purposefully omitted
|
||||||
|
}
|
Loading…
Reference in New Issue