535 lines
14 KiB
Go
535 lines
14 KiB
Go
package platform
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"up2GitX/share"
|
|
|
|
"github.com/gookit/gcli/v2"
|
|
"github.com/gookit/color"
|
|
"github.com/gookit/gcli/v2/interact"
|
|
"github.com/gookit/gcli/v2/progress"
|
|
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
|
|
"github.com/bitly/go-simplejson"
|
|
)
|
|
|
|
type RepoResult struct {
|
|
source string
|
|
uri string
|
|
status int
|
|
error string
|
|
}
|
|
|
|
const (
|
|
SUCCESS int = 0
|
|
EXIST int = 1
|
|
ERROR int = 2
|
|
SKIP string = "1"
|
|
SPL string = "\n"
|
|
WORKER = 5
|
|
)
|
|
|
|
func GiteeCommand() *gcli.Command {
|
|
gitee := &gcli.Command{
|
|
Func: syncGitee,
|
|
Name: "gitee",
|
|
UseFor: "This command is used for sync local repo to Gitee",
|
|
Examples: `
|
|
<yellow>Using dir: </> <cyan>{$binName} {$cmd} /Zoker/repos/</>
|
|
Dir example
|
|
<gray>$ ls -l /Zoker/repos/</>
|
|
drwxr-xr-x 4 zoker 128B Jun 1 19:05 git-work-repo1
|
|
drwxr-xr-x 4 zoker 128B Jun 1 19:02 taskover
|
|
drwxr-xr-x 4 zoker 128B Jun 1 19:03 blogine
|
|
drwxr-xr-x 3 zoker 96B Jun 1 12:15 git-bare-repo3
|
|
...
|
|
|
|
<yellow>Using file: </> <cyan>{$binName} {$cmd} /Zoker/repos.list</>
|
|
File example
|
|
<gray>$ cat /Zoker/repos.list</>
|
|
/tmp/repos/git-work-repo1
|
|
/Zoker/workspace/git-work-repo2
|
|
/other/path/to/git-bare-repo3
|
|
...
|
|
|
|
<yellow>Using Source: </> <cyan>{$binName} {$cmd} github:zoker </><gray>YOUR_TOKEN_HERE(OPTIONAL)</>
|
|
Support import from Github source, replace {zoker} with your expected Github path
|
|
<blue>It better to provide your own access_token to avoid api rate limit, eg on Github: https://github.com/settings/tokens</>
|
|
Alert: Only Github source and public project supported, other platform like Gitlab, Bitbucket will be added later
|
|
|
|
`}
|
|
|
|
// bind args with names
|
|
gitee.AddArg("repoSource", "Tell me which repo dir or list your want to sync, is required", false)
|
|
gitee.AddArg("token", "Provide platform token for skip api rate limit", false)
|
|
|
|
return gitee
|
|
}
|
|
|
|
func syncGitee(c *gcli.Command, args []string) error {
|
|
// check message
|
|
if len(args) == 0 {
|
|
share.InvalidAlert(c.Name)
|
|
return nil
|
|
}
|
|
|
|
// check repodir and print projects to ensure
|
|
repos, source, orgPath := share.ReadyToAuth(args, c.Name)
|
|
if repos == nil {
|
|
return nil
|
|
}
|
|
|
|
// enter userinfo to get access token
|
|
askResult, success, auth := askForAccount()
|
|
if !success {
|
|
color.Red.Println(askResult)
|
|
return nil
|
|
}
|
|
accessToken := askResult
|
|
|
|
// get userinfo via access token
|
|
userInfo, success := getUserInfo(accessToken)
|
|
if !success {
|
|
color.Red.Println(userInfo["error"])
|
|
return nil
|
|
}
|
|
color.Green.Printf("\nHello, %s! \n\n", userInfo["name"])
|
|
|
|
// get available namespace todo: enterprise and group
|
|
allNamespace := getNamespace(userInfo, accessToken)
|
|
namespace := make([]string, len(allNamespace))
|
|
for i, n := range allNamespace {
|
|
namespace[i] = n[1]
|
|
}
|
|
selectedNumber := askNamespace(namespace)
|
|
numberD, _ := strconv.Atoi(selectedNumber)
|
|
|
|
var selectedNp []string
|
|
if numberD == 0 {
|
|
// create a new group
|
|
selectedNp = createGroup(accessToken)
|
|
if selectedNp == nil {
|
|
return nil
|
|
}
|
|
} else {
|
|
// select namespace and ask for ensure
|
|
selectedNp = allNamespace[numberD]
|
|
}
|
|
color.Notice.Printf("\nSelected %s(https://gitee.com/%s) as namespace, Type: %s \n" +
|
|
"The following projects will be generated on Gitee: \n\n", selectedNp[0], selectedNp[1], selectedNp[2])
|
|
|
|
// show projects list and ensure
|
|
share.ShowProjectLists("gitee.com", repos, selectedNp[1])
|
|
|
|
// ask for public or not
|
|
public := share.AskPublic(selectedNp[2])
|
|
|
|
// create projects
|
|
fmt.Println("\n", "Creating Projects, Please Wait...")
|
|
repoRes := generateProjects(repos, public, accessToken, selectedNp)
|
|
|
|
// show results
|
|
_, exiNum, errNum := showRepoRes(repoRes)
|
|
|
|
if errNum == len(repoRes) {
|
|
color.Red.Println("No repositories are available to be uploaded!")
|
|
return nil
|
|
}
|
|
if errNum > 0 {
|
|
asErr := share.AskError()
|
|
if asErr == "0" {
|
|
return nil
|
|
}
|
|
}
|
|
var asExi string
|
|
if exiNum > 0 {
|
|
asExi = share.AskExist()
|
|
if asExi == "0" {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// available check
|
|
avaiRepo := availableRepo(repoRes, asExi)
|
|
if len(avaiRepo) == 0 {
|
|
color.Red.Println("No repositories are available to be uploaded!")
|
|
return nil
|
|
}
|
|
|
|
// sync code
|
|
var tmpDir string
|
|
if source == "local" {
|
|
fmt.Println("\n", "Syncing Projects to Gitee, Please Wait...")
|
|
} else {
|
|
currentDir, _ := os.Getwd()
|
|
tmpDir = fmt.Sprintf("%s/up2GitX-%s-%s", currentDir, source, orgPath)
|
|
if err := os.MkdirAll(tmpDir, 0755); err !=nil {
|
|
color.Red.Println(err.Error())
|
|
return nil
|
|
}
|
|
fmt.Printf("\nA tmp repo dir `%s` created for tmp repositories, you can remove it after sync successed\n\n", tmpDir)
|
|
fmt.Println("Cloning and Uploading Projects to Gitee, Please Wait...")
|
|
}
|
|
syncRes := multiSync(avaiRepo, auth, asExi, tmpDir)
|
|
showSyncRes(syncRes)
|
|
return nil
|
|
}
|
|
|
|
func askForAccount() (string, bool, *http.BasicAuth) {
|
|
email, _ := interact.ReadLine("\nPlease enter your Gitee email: ")
|
|
password := interact.ReadPassword("Please enter your Gitee password: ")
|
|
if len(email) == 0 || len(password) == 0 {
|
|
return "Email or Password must be provided!", false, nil
|
|
} else {
|
|
// replace your client_id and client_secret from Gitee
|
|
params := fmt.Sprintf(`{
|
|
"grant_type": "password",
|
|
"username": "%s",
|
|
"password": "%s",
|
|
"client_id": "xxxx",
|
|
"client_secret": "xxxx",
|
|
"scope": "user_info projects groups enterprises"
|
|
}`, email, password)
|
|
|
|
var paramsJson map[string]interface{}
|
|
json.Unmarshal([]byte(params), ¶msJson)
|
|
result, err := share.PostForm("https://gitee.com/oauth/token", paramsJson)
|
|
|
|
if err != nil {
|
|
return err.Error(), false, nil
|
|
}
|
|
|
|
filterVal, ok := filterResult(result, "access_token")
|
|
auth := &http.BasicAuth{email, password}
|
|
return filterVal, ok, auth
|
|
}
|
|
}
|
|
|
|
func getUserInfo(token string) (map [string]string, bool) {
|
|
info := make(map [string]string)
|
|
uri := fmt.Sprintf("https://gitee.com/api/v5/user?access_token=%s", token)
|
|
result, err := share.Get(uri)
|
|
if err != nil {
|
|
info["error"] = err.Error()
|
|
return info, false
|
|
}
|
|
name, ok := filterResult(result, "name")
|
|
if !ok {
|
|
info["error"] = name
|
|
return info, ok
|
|
}
|
|
info["name"] = name
|
|
username, ok := filterResult(result, "login")
|
|
info["username"] = username
|
|
return info, ok
|
|
}
|
|
|
|
func filterResult(result map[string]interface{}, key string) (string, bool) {
|
|
val, atok := result[key].(string)
|
|
_, errok := result["error"].(string)
|
|
if atok {
|
|
return val, true
|
|
} else if errok {
|
|
return result["error_description"].(string), false
|
|
}
|
|
return "Unexpectedly exit", false
|
|
}
|
|
|
|
// todo enable select group and enterprise
|
|
func getNamespace(userInfo map [string]string, token string) [][]string {
|
|
var namespace [][]string
|
|
newOrg := []string{"Create a new Group", "Create a new Group", ""}
|
|
owner := []string{userInfo["name"], userInfo["username"], "Personal"}
|
|
namespace = append(namespace, newOrg, owner)
|
|
|
|
orgUrl := fmt.Sprintf("https://gitee.com/api/v5/user/orgs?access_token=%s&per_page=100", token)
|
|
if result, _, err := share.GetByte(orgUrl); err == nil {
|
|
orgRes, _ := simplejson.NewJson([]byte(result))
|
|
serOrg, _ := orgRes.Array()
|
|
for _, org := range serOrg {
|
|
orgA := org.(map[string]interface{})
|
|
orgName := orgA["login"].(string)
|
|
namespace = append(namespace, []string{orgName, orgName, "Group"})
|
|
}
|
|
}
|
|
|
|
return namespace
|
|
}
|
|
|
|
func createGroup(token string) []string {
|
|
orgPath, _ := interact.ReadLine("\nPlease enter your new Group path(eg: zoker, zoker123, osc123): ")
|
|
if len(orgPath) == 0 {
|
|
color.Red.Println("Group path must be provided!")
|
|
} else {
|
|
orgUrl := "https://gitee.com/api/v5/users/organization"
|
|
params := fmt.Sprintf(`{
|
|
"access_token": "%s",
|
|
"name": "%s",
|
|
"org": "%s"
|
|
}`, token, orgPath, orgPath)
|
|
var paramsJson map[string]interface{}
|
|
json.Unmarshal([]byte(params), ¶msJson)
|
|
|
|
if result, _, err := share.PostFormByte(orgUrl, paramsJson); err == nil {
|
|
orgRes, _ := simplejson.NewJson([]byte(result))
|
|
login, _ := orgRes.Get("login").String()
|
|
errorMsg, _ := orgRes.Get("message").String()
|
|
if login == orgPath {
|
|
color.Green.Printf("\nGroup https://gitee.com/%s has been successfully Created!\n", orgPath)
|
|
return []string{orgPath, orgPath, "Group"}
|
|
} else {
|
|
color.Red.Println(errorMsg)
|
|
}
|
|
} else {
|
|
color.Red.Println(err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func askNamespace(namespace []string) string {
|
|
np := interact.SelectOne(
|
|
"Please select which namespace you want to put this repositories: ",
|
|
namespace,
|
|
"",
|
|
)
|
|
return np
|
|
}
|
|
|
|
func generateProjects(repos []string, public string, token string, np []string) (repoRes []RepoResult) {
|
|
step := progress.Bar(len(repos))
|
|
var wg sync.WaitGroup
|
|
var mutex = &sync.Mutex{}
|
|
paths := make(chan string)
|
|
|
|
step.Start()
|
|
wg.Add(len(repos))
|
|
|
|
for w := 1; w <= WORKER; w++ {
|
|
go createProjectWorker(paths, public, token, np, &wg, &repoRes, mutex, step)
|
|
}
|
|
|
|
for _, p := range repos {
|
|
paths <- p
|
|
}
|
|
close(paths)
|
|
|
|
wg.Wait()
|
|
step.Finish()
|
|
|
|
fmt.Printf(SPL)
|
|
return repoRes
|
|
}
|
|
|
|
func createProjectWorker(paths chan string, public string, token string, np []string, wg *sync.WaitGroup, repoRes *[]RepoResult, mutex *sync.Mutex, step *progress.Progress) {
|
|
for path := range paths {
|
|
createProject(path, public, token, np, repoRes, wg, mutex)
|
|
step.Advance()
|
|
wg.Done()
|
|
}
|
|
}
|
|
|
|
func createProject(path string, public string, token string, np []string, repoRes *[]RepoResult, wg *sync.WaitGroup, mutex *sync.Mutex) {
|
|
repoUrl := getRepoUrl(np)
|
|
phs := strings.Split(path, "/")
|
|
repoPath := phs[len(phs) - 1]
|
|
params := fmt.Sprintf(`{
|
|
"access_token": "%s",
|
|
"name": "%s",
|
|
"path": "%s",
|
|
"private": "%s"
|
|
}`, token, repoPath, repoPath, public)
|
|
var paramsJson map[string]interface{}
|
|
json.Unmarshal([]byte(params), ¶msJson)
|
|
result, err := share.PostForm(repoUrl, paramsJson)
|
|
if err != nil {
|
|
return
|
|
}
|
|
uri, eType := filterProjectResult(result, "html_url")
|
|
errMsg := uri
|
|
if eType == EXIST {
|
|
uri = fmt.Sprintf("https://gitee.com/%s/%s.git", np[1], repoPath)
|
|
}
|
|
mutex.Lock()
|
|
*repoRes = append(*repoRes, RepoResult{source: path, uri: uri, status: eType, error: errMsg})
|
|
mutex.Unlock()
|
|
}
|
|
|
|
func getRepoUrl(np []string) string {
|
|
var uri string
|
|
switch np[2] {
|
|
case "Personal":
|
|
uri = "https://gitee.com/api/v5/user/repos"
|
|
case "Group":
|
|
uri = fmt.Sprintf("https://gitee.com/api/v5/orgs/%s/repos", np[1])
|
|
case "Enterprise":
|
|
uri = fmt.Sprintf("https://gitee.com/api/v5/enterprises/%s/repos", np[1])
|
|
}
|
|
return uri
|
|
}
|
|
|
|
func filterProjectResult(result map[string]interface{}, key string) (string, int) {
|
|
var err string
|
|
var eType int
|
|
if result["error"] != nil {
|
|
for k, v := range result["error"].(map[string]interface{}) {
|
|
err = fmt.Sprint(v) // skip Type Assertion
|
|
if k == "base" {
|
|
eType = EXIST
|
|
} else {
|
|
eType = ERROR
|
|
}
|
|
}
|
|
return err, eType
|
|
}
|
|
val, atok := result[key].(string)
|
|
if atok {
|
|
return val, SUCCESS
|
|
}
|
|
return "Unexpectedly exit", ERROR
|
|
}
|
|
|
|
func showRepoRes(repoRes []RepoResult) (int, int, int) {
|
|
success := printRepo(repoRes, SUCCESS)
|
|
exist := printRepo(repoRes, EXIST)
|
|
errNum := printRepo(repoRes, ERROR)
|
|
return success, exist, errNum
|
|
}
|
|
|
|
func printRepo(repoRes []RepoResult, status int) int {
|
|
var p, result string
|
|
num := 0
|
|
repoStatus := repoStatus(status)
|
|
for _, item := range repoRes {
|
|
if item.status == status {
|
|
num = num + 1
|
|
if status == ERROR {
|
|
result = item.error
|
|
} else {
|
|
result = item.uri
|
|
}
|
|
p = fmt.Sprintf("Source: (%s)\n Status: %s\n Result: ", item.source, repoStatus)
|
|
colorRepo(status, p)
|
|
colorResult(status, result)
|
|
fmt.Printf(SPL)
|
|
}
|
|
}
|
|
return num
|
|
}
|
|
|
|
func showSyncRes(syncRes []RepoResult) {
|
|
printSync(syncRes, SUCCESS)
|
|
printSync(syncRes, ERROR)
|
|
}
|
|
|
|
func printSync(syncRes []RepoResult, status int) {
|
|
var p, result string
|
|
for _, item := range syncRes {
|
|
if item.status == status {
|
|
if status == SUCCESS {
|
|
result = "Sync to Gitee SUCCESS!"
|
|
} else {
|
|
result = item.error
|
|
}
|
|
p = fmt.Sprintf("Source: (%s)\n Gitee: %s\n Result: ", item.source, item.uri)
|
|
colorRepo(EXIST, p)
|
|
colorResult(item.status, result)
|
|
fmt.Printf(SPL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func repoStatus(status int) string {
|
|
str := ""
|
|
switch status {
|
|
case SUCCESS:
|
|
str = "Created"
|
|
case EXIST:
|
|
str = "Exists"
|
|
case ERROR:
|
|
str = "Error"
|
|
default:
|
|
str = "Unknown Error"
|
|
}
|
|
return str
|
|
}
|
|
|
|
func colorRepo(status int, p string) {
|
|
switch status {
|
|
case SUCCESS:
|
|
color.Green.Printf(p)
|
|
case EXIST:
|
|
color.Yellow.Printf(p)
|
|
case ERROR:
|
|
color.Red.Printf(p)
|
|
default:
|
|
color.Red.Printf(p)
|
|
}
|
|
}
|
|
|
|
func colorResult(status int, p string) {
|
|
switch status {
|
|
case SUCCESS:
|
|
color.Green.Println(p)
|
|
case EXIST:
|
|
color.Yellow.Println(p)
|
|
case ERROR:
|
|
color.Red.Println(p)
|
|
default:
|
|
color.Red.Println(p)
|
|
}
|
|
}
|
|
|
|
func multiSync(avaiRepo []RepoResult, auth *http.BasicAuth, force string, tmpDir string) (syncRes []RepoResult) {
|
|
step := progress.Bar(len(avaiRepo))
|
|
var wg sync.WaitGroup
|
|
var mutex = &sync.Mutex{}
|
|
avais := make(chan RepoResult)
|
|
|
|
step.Start()
|
|
wg.Add(len(avaiRepo))
|
|
|
|
for w := 1; w <= WORKER; w++ {
|
|
go multiSyncWorker(avais, auth, force, &syncRes, &wg, mutex, step, tmpDir)
|
|
}
|
|
|
|
for _, p := range avaiRepo {
|
|
avais <- p
|
|
}
|
|
close(avais)
|
|
|
|
wg.Wait()
|
|
step.Finish()
|
|
|
|
fmt.Printf(SPL)
|
|
return syncRes
|
|
}
|
|
|
|
func multiSyncWorker(avais chan RepoResult, auth *http.BasicAuth, force string, syncRes *[]RepoResult, wg *sync.WaitGroup, mutex *sync.Mutex, step *progress.Progress, tmpDir string) {
|
|
for item := range avais {
|
|
err := share.SyncRepo(auth, item.source, item.uri, force, tmpDir)
|
|
if err != nil {
|
|
item.status = ERROR
|
|
item.error = err.Error()
|
|
} else {
|
|
item.status = SUCCESS
|
|
}
|
|
*syncRes = append(*syncRes, item)
|
|
step.Advance()
|
|
wg.Done()
|
|
}
|
|
}
|
|
|
|
func availableRepo(repoRes []RepoResult, force string) []RepoResult {
|
|
var avaiRepo []RepoResult
|
|
for _, item := range repoRes {
|
|
if item.status == SUCCESS || (item.status == EXIST && force != SKIP) {
|
|
avaiRepo = append(avaiRepo, item)
|
|
}
|
|
}
|
|
return avaiRepo
|
|
} |