up2GitX/platform/gitee.go

534 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), &paramsJson)
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), &paramsJson)
if result, _, err := share.PostFormByte(orgUrl, paramsJson); err == nil {
orgRes, _ := simplejson.NewJson([]byte(result))
login, _ := orgRes.Get("login").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(orgRes)
}
} 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), &paramsJson)
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
}