Some checks failed
Release / build-windows (push) Has been cancelled
✨ 新功能: - GitLab 平台完整支持(16+7 个 API 方法、前端图标/选择器) - Release 同步支持手动输入 Token 🐛 Bug 修复: - 自建平台凭证匹配(BaseURL 优先精确匹配) - Gitea 迁移 API service 字段类型修复(HTTP 422) - 构建时不再覆盖已有 config.yaml(数据丢失问题) - 错误信息显示实际 URL 🔨 改进: - 源平台开启代理时自动跳过 Gitea 原生迁移,改用本地代理中转 - Push 超时从 30s 提升到 10 分钟 - 设置 http.postBuffer=500MB,push --all 失败自动逐分支推送 - 新增 RunWithProxyTimeout 方法
1245 lines
34 KiB
Go
1245 lines
34 KiB
Go
package app
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"strings"
|
||
)
|
||
|
||
// --- 迁移相关扩展类型 ---
|
||
|
||
// LabelInfo 标签
|
||
type LabelInfo struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Color string `json:"color"` // 不含 # 前缀,如 "fc2929"
|
||
Desc string `json:"description,omitempty"`
|
||
}
|
||
|
||
// MilestoneInfo 里程碑
|
||
type MilestoneInfo struct {
|
||
ID int64 `json:"id"`
|
||
Title string `json:"title"`
|
||
Description string `json:"description"`
|
||
State string `json:"state"` // open / closed
|
||
DueOn string `json:"dueOn,omitempty"`
|
||
}
|
||
|
||
// IssueInfo 工单
|
||
type IssueInfo struct {
|
||
ID int64 `json:"id"`
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
Body string `json:"body"`
|
||
State string `json:"state"` // open / closed
|
||
Labels []string `json:"labels,omitempty"`
|
||
MilestoneTitle string `json:"milestoneTitle,omitempty"`
|
||
MilestoneID int64 `json:"milestoneId,omitempty"`
|
||
Comments []CommentInfo `json:"comments,omitempty"`
|
||
CreatedAt string `json:"createdAt,omitempty"`
|
||
}
|
||
|
||
// CommentInfo 评论
|
||
type CommentInfo struct {
|
||
Body string `json:"body"`
|
||
User string `json:"user,omitempty"`
|
||
CreatedAt string `json:"createdAt,omitempty"`
|
||
}
|
||
|
||
// PullRequestInfo 合并请求(简化信息,用于记录式迁移)
|
||
type PullRequestInfo struct {
|
||
ID int64 `json:"id"`
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
Body string `json:"body"`
|
||
State string `json:"state"` // open / closed / merged
|
||
Head string `json:"head"`
|
||
Base string `json:"base"`
|
||
User string `json:"user,omitempty"`
|
||
CreatedAt string `json:"createdAt,omitempty"`
|
||
}
|
||
|
||
// ================ GitHub ================
|
||
|
||
func (g *GitHubAPI) ListLabels(owner, repo string) ([]LabelInfo, error) {
|
||
var all []LabelInfo
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/labels?per_page=100&page=%d", owner, repo, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Color string `json:"color"`
|
||
Desc string `json:"description"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
all = append(all, LabelInfo{ID: it.ID, Name: it.Name, Color: it.Color, Desc: it.Desc})
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GitHubAPI) CreateLabel(owner, repo string, label LabelInfo) error {
|
||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/labels", owner, repo)
|
||
payload := map[string]interface{}{
|
||
"name": label.Name,
|
||
"color": strings.TrimPrefix(label.Color, "#"),
|
||
"description": label.Desc,
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (g *GitHubAPI) ListMilestones(owner, repo string) ([]MilestoneInfo, error) {
|
||
var all []MilestoneInfo
|
||
for _, state := range []string{"open", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/milestones?state=%s&per_page=100&page=%d", owner, repo, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
Description string `json:"description"`
|
||
State string `json:"state"`
|
||
DueOn string `json:"due_on"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
all = append(all, MilestoneInfo{
|
||
ID: it.ID, Title: it.Title, Description: it.Description, State: it.State, DueOn: it.DueOn,
|
||
})
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GitHubAPI) CreateMilestone(owner, repo string, ms MilestoneInfo) (*MilestoneInfo, error) {
|
||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/milestones", owner, repo)
|
||
payload := map[string]interface{}{
|
||
"title": ms.Title,
|
||
"description": ms.Description,
|
||
"state": ms.State,
|
||
}
|
||
if ms.DueOn != "" {
|
||
payload["due_on"] = ms.DueOn
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
var result struct {
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&result)
|
||
return &MilestoneInfo{ID: int64(result.Number), Title: result.Title, State: ms.State}, nil
|
||
}
|
||
|
||
func (g *GitHubAPI) ListIssues(owner, repo string) ([]IssueInfo, error) {
|
||
var all []IssueInfo
|
||
for _, state := range []string{"open", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues?state=%s&per_page=100&page=%d&direction=asc", owner, repo, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
Body string `json:"body"`
|
||
State string `json:"state"`
|
||
Labels []struct {
|
||
Name string `json:"name"`
|
||
} `json:"labels"`
|
||
Milestone *struct {
|
||
Title string `json:"title"`
|
||
} `json:"milestone"`
|
||
PullRequest *struct {
|
||
URL string `json:"url"`
|
||
} `json:"pull_request"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
// GitHub Issues API 也返回 PR,通过 pull_request 字段过滤
|
||
if it.PullRequest != nil {
|
||
continue
|
||
}
|
||
issue := IssueInfo{
|
||
ID: it.ID, Number: it.Number, Title: it.Title, Body: it.Body,
|
||
State: it.State, CreatedAt: it.CreatedAt,
|
||
}
|
||
for _, l := range it.Labels {
|
||
issue.Labels = append(issue.Labels, l.Name)
|
||
}
|
||
if it.Milestone != nil {
|
||
issue.MilestoneTitle = it.Milestone.Title
|
||
}
|
||
all = append(all, issue)
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GitHubAPI) CreateIssue(owner, repo string, issue IssueInfo, milestoneMap map[string]int64) error {
|
||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues", owner, repo)
|
||
payload := map[string]interface{}{
|
||
"title": issue.Title,
|
||
"body": issue.Body,
|
||
"labels": issue.Labels,
|
||
}
|
||
if issue.MilestoneTitle != "" {
|
||
if msID, ok := milestoneMap[issue.MilestoneTitle]; ok {
|
||
payload["milestone"] = msID
|
||
}
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
// 如果需要关闭 issue
|
||
if issue.State == "closed" {
|
||
var created struct {
|
||
Number int `json:"number"`
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&created)
|
||
closeURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues/%d", owner, repo, created.Number)
|
||
closeResp, err := g.doRequest("PATCH", closeURL, map[string]string{"state": "closed"})
|
||
if err == nil {
|
||
closeResp.Body.Close()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (g *GitHubAPI) ListPullRequests(owner, repo string) ([]PullRequestInfo, error) {
|
||
var all []PullRequestInfo
|
||
for _, state := range []string{"open", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls?state=%s&per_page=100&page=%d&direction=asc", owner, repo, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
Body string `json:"body"`
|
||
State string `json:"state"`
|
||
Merged bool `json:"merged"`
|
||
Head struct {
|
||
Ref string `json:"ref"`
|
||
} `json:"head"`
|
||
Base struct {
|
||
Ref string `json:"ref"`
|
||
} `json:"base"`
|
||
User struct {
|
||
Login string `json:"login"`
|
||
} `json:"user"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
st := it.State
|
||
if it.Merged {
|
||
st = "merged"
|
||
}
|
||
all = append(all, PullRequestInfo{
|
||
ID: it.ID, Number: it.Number, Title: it.Title, Body: it.Body,
|
||
State: st, Head: it.Head.Ref, Base: it.Base.Ref,
|
||
User: it.User.Login, CreatedAt: it.CreatedAt,
|
||
})
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
// ================ Gitea ================
|
||
|
||
func (g *GiteaAPI) ListLabels(owner, repo string) ([]LabelInfo, error) {
|
||
var all []LabelInfo
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels?page=%d&limit=50", g.BaseURL, owner, repo, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Color string `json:"color"`
|
||
Desc string `json:"description"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
all = append(all, LabelInfo{ID: it.ID, Name: it.Name, Color: strings.TrimPrefix(it.Color, "#"), Desc: it.Desc})
|
||
}
|
||
if len(items) < 50 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GiteaAPI) CreateLabel(owner, repo string, label LabelInfo) error {
|
||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels", g.BaseURL, owner, repo)
|
||
color := label.Color
|
||
if !strings.HasPrefix(color, "#") {
|
||
color = "#" + color
|
||
}
|
||
payload := map[string]interface{}{
|
||
"name": label.Name,
|
||
"color": color,
|
||
"description": label.Desc,
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (g *GiteaAPI) ListMilestones(owner, repo string) ([]MilestoneInfo, error) {
|
||
var all []MilestoneInfo
|
||
for _, state := range []string{"open", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/milestones?state=%s&page=%d&limit=50", g.BaseURL, owner, repo, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Title string `json:"title"`
|
||
Description string `json:"description"`
|
||
State string `json:"state"`
|
||
DueOn string `json:"due_on"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
all = append(all, MilestoneInfo{
|
||
ID: it.ID, Title: it.Title, Description: it.Description, State: it.State, DueOn: it.DueOn,
|
||
})
|
||
}
|
||
if len(items) < 50 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GiteaAPI) CreateMilestone(owner, repo string, ms MilestoneInfo) (*MilestoneInfo, error) {
|
||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/milestones", g.BaseURL, owner, repo)
|
||
payload := map[string]interface{}{
|
||
"title": ms.Title,
|
||
"description": ms.Description,
|
||
"state": ms.State,
|
||
}
|
||
if ms.DueOn != "" {
|
||
payload["due_on"] = ms.DueOn
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
var result struct {
|
||
ID int64 `json:"id"`
|
||
Title string `json:"title"`
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&result)
|
||
return &MilestoneInfo{ID: result.ID, Title: result.Title, State: ms.State}, nil
|
||
}
|
||
|
||
func (g *GiteaAPI) ListIssues(owner, repo string) ([]IssueInfo, error) {
|
||
var all []IssueInfo
|
||
for _, state := range []string{"open", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues?state=%s&type=issues&page=%d&limit=50", g.BaseURL, owner, repo, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
Body string `json:"body"`
|
||
State string `json:"state"`
|
||
Labels []struct {
|
||
Name string `json:"name"`
|
||
} `json:"labels"`
|
||
Milestone *struct {
|
||
Title string `json:"title"`
|
||
} `json:"milestone"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
issue := IssueInfo{
|
||
ID: it.ID, Number: it.Number, Title: it.Title, Body: it.Body,
|
||
State: it.State, CreatedAt: it.CreatedAt,
|
||
}
|
||
for _, l := range it.Labels {
|
||
issue.Labels = append(issue.Labels, l.Name)
|
||
}
|
||
if it.Milestone != nil {
|
||
issue.MilestoneTitle = it.Milestone.Title
|
||
}
|
||
all = append(all, issue)
|
||
}
|
||
if len(items) < 50 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GiteaAPI) CreateIssue(owner, repo string, issue IssueInfo, milestoneMap map[string]int64) error {
|
||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues", g.BaseURL, owner, repo)
|
||
payload := map[string]interface{}{
|
||
"title": issue.Title,
|
||
"body": issue.Body,
|
||
"labels": []int64{}, // Gitea 需要 label ID,后面处理
|
||
}
|
||
if issue.MilestoneTitle != "" {
|
||
if msID, ok := milestoneMap[issue.MilestoneTitle]; ok {
|
||
payload["milestone"] = msID
|
||
}
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
// 关闭 issue
|
||
if issue.State == "closed" {
|
||
var created struct {
|
||
Number int `json:"number"`
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&created)
|
||
closeURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", g.BaseURL, owner, repo, created.Number)
|
||
closeResp, err := g.doRequest("PATCH", closeURL, map[string]string{"state": "closed"})
|
||
if err == nil {
|
||
closeResp.Body.Close()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (g *GiteaAPI) ListPullRequests(owner, repo string) ([]PullRequestInfo, error) {
|
||
var all []PullRequestInfo
|
||
for _, state := range []string{"open", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls?state=%s&page=%d&limit=50", g.BaseURL, owner, repo, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
Body string `json:"body"`
|
||
State string `json:"state"`
|
||
Merged bool `json:"merged"`
|
||
Head struct {
|
||
Ref string `json:"ref"`
|
||
} `json:"head"`
|
||
Base struct {
|
||
Ref string `json:"ref"`
|
||
} `json:"base"`
|
||
User struct {
|
||
Username string `json:"username"`
|
||
} `json:"user"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
st := it.State
|
||
if it.Merged {
|
||
st = "merged"
|
||
}
|
||
all = append(all, PullRequestInfo{
|
||
ID: it.ID, Number: it.Number, Title: it.Title, Body: it.Body,
|
||
State: st, Head: it.Head.Ref, Base: it.Base.Ref,
|
||
User: it.User.Username, CreatedAt: it.CreatedAt,
|
||
})
|
||
}
|
||
if len(items) < 50 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
// Gitea 迁移 API — 当目标平台是 Gitea 时可直接使用
|
||
func (g *GiteaAPI) MigrateRepo(cloneAddr, repoName, token, srcPlatform string, opts MigrateOptions) (string, error) {
|
||
url := g.BaseURL + "/api/v1/repos/migrate"
|
||
|
||
serviceType := "git"
|
||
switch strings.ToLower(srcPlatform) {
|
||
case "github":
|
||
serviceType = "github"
|
||
case "gitea":
|
||
serviceType = "gitea"
|
||
case "gitlab":
|
||
serviceType = "gitlab"
|
||
case "gitee":
|
||
serviceType = "gitee"
|
||
}
|
||
|
||
payload := map[string]interface{}{
|
||
"clone_addr": cloneAddr,
|
||
"auth_token": token,
|
||
"repo_name": repoName,
|
||
"repo_owner": g.Username,
|
||
"service": serviceType,
|
||
"mirror": false,
|
||
"issues": opts.Issues,
|
||
"labels": opts.Labels,
|
||
"milestones": opts.Milestones,
|
||
"releases": opts.Releases,
|
||
"pull_requests": opts.PullRequests,
|
||
"wiki": false,
|
||
}
|
||
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return "", fmt.Errorf("请求失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return "", fmt.Errorf("迁移失败 (HTTP %d): %s", resp.StatusCode, string(body))
|
||
}
|
||
|
||
var result struct {
|
||
CloneURL string `json:"clone_url"`
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&result)
|
||
return result.CloneURL, nil
|
||
}
|
||
|
||
// ================ GitLab ================
|
||
|
||
func (g *GitLabAPI) ListLabels(owner, repo string) ([]LabelInfo, error) {
|
||
var all []LabelInfo
|
||
pp := g.projectPath(owner, repo)
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("%s/api/v4/projects/%s/labels?per_page=100&page=%d", g.BaseURL, pp, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Color string `json:"color"`
|
||
Desc string `json:"description"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
all = append(all, LabelInfo{ID: it.ID, Name: it.Name, Color: strings.TrimPrefix(it.Color, "#"), Desc: it.Desc})
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GitLabAPI) CreateLabel(owner, repo string, label LabelInfo) error {
|
||
pp := g.projectPath(owner, repo)
|
||
url := fmt.Sprintf("%s/api/v4/projects/%s/labels", g.BaseURL, pp)
|
||
color := label.Color
|
||
if !strings.HasPrefix(color, "#") {
|
||
color = "#" + color
|
||
}
|
||
payload := map[string]interface{}{
|
||
"name": label.Name,
|
||
"color": color,
|
||
"description": label.Desc,
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (g *GitLabAPI) ListMilestones(owner, repo string) ([]MilestoneInfo, error) {
|
||
var all []MilestoneInfo
|
||
pp := g.projectPath(owner, repo)
|
||
for _, state := range []string{"active", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("%s/api/v4/projects/%s/milestones?state=%s&per_page=100&page=%d", g.BaseURL, pp, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Title string `json:"title"`
|
||
Description string `json:"description"`
|
||
State string `json:"state"`
|
||
DueDate string `json:"due_date"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
msState := it.State
|
||
if msState == "active" {
|
||
msState = "open"
|
||
}
|
||
all = append(all, MilestoneInfo{
|
||
ID: it.ID, Title: it.Title, Description: it.Description, State: msState, DueOn: it.DueDate,
|
||
})
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GitLabAPI) CreateMilestone(owner, repo string, ms MilestoneInfo) (*MilestoneInfo, error) {
|
||
pp := g.projectPath(owner, repo)
|
||
url := fmt.Sprintf("%s/api/v4/projects/%s/milestones", g.BaseURL, pp)
|
||
state := ms.State
|
||
if state == "open" {
|
||
state = "activate"
|
||
} else if state == "closed" {
|
||
state = "close"
|
||
}
|
||
payload := map[string]interface{}{
|
||
"title": ms.Title,
|
||
"description": ms.Description,
|
||
"state_event": state,
|
||
}
|
||
if ms.DueOn != "" {
|
||
payload["due_date"] = ms.DueOn
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
var result struct {
|
||
ID int64 `json:"id"`
|
||
Title string `json:"title"`
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&result)
|
||
return &MilestoneInfo{ID: result.ID, Title: result.Title, State: ms.State}, nil
|
||
}
|
||
|
||
func (g *GitLabAPI) ListIssues(owner, repo string) ([]IssueInfo, error) {
|
||
var all []IssueInfo
|
||
pp := g.projectPath(owner, repo)
|
||
for _, state := range []string{"opened", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("%s/api/v4/projects/%s/issues?state=%s&per_page=100&page=%d&order_by=created_at&sort=asc", g.BaseURL, pp, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
IID int `json:"iid"`
|
||
Title string `json:"title"`
|
||
Desc string `json:"description"`
|
||
State string `json:"state"`
|
||
Labels []string `json:"labels"`
|
||
Milestone *struct {
|
||
Title string `json:"title"`
|
||
} `json:"milestone"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
issueState := it.State
|
||
if issueState == "opened" {
|
||
issueState = "open"
|
||
}
|
||
issue := IssueInfo{
|
||
ID: it.ID, Number: it.IID, Title: it.Title, Body: it.Desc,
|
||
State: issueState, Labels: it.Labels, CreatedAt: it.CreatedAt,
|
||
}
|
||
if it.Milestone != nil {
|
||
issue.MilestoneTitle = it.Milestone.Title
|
||
}
|
||
all = append(all, issue)
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GitLabAPI) CreateIssue(owner, repo string, issue IssueInfo, milestoneMap map[string]int64) error {
|
||
pp := g.projectPath(owner, repo)
|
||
url := fmt.Sprintf("%s/api/v4/projects/%s/issues", g.BaseURL, pp)
|
||
payload := map[string]interface{}{
|
||
"title": issue.Title,
|
||
"description": issue.Body,
|
||
"labels": strings.Join(issue.Labels, ","),
|
||
}
|
||
if issue.MilestoneTitle != "" {
|
||
if msID, ok := milestoneMap[issue.MilestoneTitle]; ok {
|
||
payload["milestone_id"] = msID
|
||
}
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
// 如果需要关闭 issue
|
||
if issue.State == "closed" {
|
||
var created struct {
|
||
IID int `json:"iid"`
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&created)
|
||
closeURL := fmt.Sprintf("%s/api/v4/projects/%s/issues/%d", g.BaseURL, pp, created.IID)
|
||
closeResp, err := g.doRequest("PUT", closeURL, map[string]string{"state_event": "close"})
|
||
if err == nil {
|
||
closeResp.Body.Close()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (g *GitLabAPI) ListPullRequests(owner, repo string) ([]PullRequestInfo, error) {
|
||
var all []PullRequestInfo
|
||
pp := g.projectPath(owner, repo)
|
||
for _, state := range []string{"opened", "closed", "merged"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("%s/api/v4/projects/%s/merge_requests?state=%s&per_page=100&page=%d&order_by=created_at&sort=asc", g.BaseURL, pp, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
IID int `json:"iid"`
|
||
Title string `json:"title"`
|
||
Description string `json:"description"`
|
||
State string `json:"state"`
|
||
SourceBranch string `json:"source_branch"`
|
||
TargetBranch string `json:"target_branch"`
|
||
Author struct {
|
||
Username string `json:"username"`
|
||
} `json:"author"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
st := it.State
|
||
if st == "opened" {
|
||
st = "open"
|
||
}
|
||
all = append(all, PullRequestInfo{
|
||
ID: it.ID, Number: it.IID, Title: it.Title, Body: it.Description,
|
||
State: st, Head: it.SourceBranch, Base: it.TargetBranch,
|
||
User: it.Author.Username, CreatedAt: it.CreatedAt,
|
||
})
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
// ================ Gitee ================
|
||
|
||
func (g *GiteeAPI) ListLabels(owner, repo string) ([]LabelInfo, error) {
|
||
var all []LabelInfo
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/%s/labels?access_token=%s&per_page=100&page=%d", owner, repo, g.Token, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Color string `json:"color"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
all = append(all, LabelInfo{ID: it.ID, Name: it.Name, Color: strings.TrimPrefix(it.Color, "#")})
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GiteeAPI) CreateLabel(owner, repo string, label LabelInfo) error {
|
||
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/%s/labels", owner, repo)
|
||
color := label.Color
|
||
if !strings.HasPrefix(color, "#") {
|
||
color = "#" + color
|
||
}
|
||
payload := map[string]interface{}{
|
||
"access_token": g.Token,
|
||
"name": label.Name,
|
||
"color": color,
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (g *GiteeAPI) ListMilestones(owner, repo string) ([]MilestoneInfo, error) {
|
||
var all []MilestoneInfo
|
||
for _, state := range []string{"open", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/%s/milestones?access_token=%s&state=%s&per_page=100&page=%d", owner, repo, g.Token, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
Description string `json:"description"`
|
||
State string `json:"state"`
|
||
DueOn string `json:"due_on"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
all = append(all, MilestoneInfo{
|
||
ID: it.ID, Title: it.Title, Description: it.Description, State: it.State, DueOn: it.DueOn,
|
||
})
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GiteeAPI) CreateMilestone(owner, repo string, ms MilestoneInfo) (*MilestoneInfo, error) {
|
||
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/%s/milestones", owner, repo)
|
||
payload := map[string]interface{}{
|
||
"access_token": g.Token,
|
||
"title": ms.Title,
|
||
"description": ms.Description,
|
||
"state": ms.State,
|
||
}
|
||
if ms.DueOn != "" {
|
||
payload["due_on"] = ms.DueOn
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
var result struct {
|
||
ID int64 `json:"id"`
|
||
Title string `json:"title"`
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&result)
|
||
return &MilestoneInfo{ID: result.ID, Title: result.Title, State: ms.State}, nil
|
||
}
|
||
|
||
func (g *GiteeAPI) ListIssues(owner, repo string) ([]IssueInfo, error) {
|
||
var all []IssueInfo
|
||
for _, state := range []string{"open", "closed"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/%s/issues?access_token=%s&state=%s&per_page=100&page=%d&direction=asc", owner, repo, g.Token, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Number string `json:"number"` // Gitee uses string
|
||
Title string `json:"title"`
|
||
Body string `json:"body"`
|
||
State string `json:"state"`
|
||
Labels []struct {
|
||
Name string `json:"name"`
|
||
} `json:"labels"`
|
||
Milestone *struct {
|
||
Title string `json:"title"`
|
||
} `json:"milestone"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
issue := IssueInfo{
|
||
ID: it.ID, Title: it.Title, Body: it.Body,
|
||
State: it.State, CreatedAt: it.CreatedAt,
|
||
}
|
||
for _, l := range it.Labels {
|
||
issue.Labels = append(issue.Labels, l.Name)
|
||
}
|
||
if it.Milestone != nil {
|
||
issue.MilestoneTitle = it.Milestone.Title
|
||
}
|
||
all = append(all, issue)
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|
||
|
||
func (g *GiteeAPI) CreateIssue(owner, repo string, issue IssueInfo, milestoneMap map[string]int64) error {
|
||
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/issues", owner)
|
||
payload := map[string]interface{}{
|
||
"access_token": g.Token,
|
||
"repo": repo,
|
||
"title": issue.Title,
|
||
"body": issue.Body,
|
||
}
|
||
if len(issue.Labels) > 0 {
|
||
payload["labels"] = strings.Join(issue.Labels, ",")
|
||
}
|
||
if issue.MilestoneTitle != "" {
|
||
if msID, ok := milestoneMap[issue.MilestoneTitle]; ok {
|
||
payload["milestone"] = msID
|
||
}
|
||
}
|
||
resp, err := g.doRequest("POST", url, payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != 201 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
// Gitee 关闭 issue 需要 PATCH
|
||
if issue.State == "closed" {
|
||
var created struct {
|
||
Number string `json:"number"`
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&created)
|
||
closeURL := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/%s/issues/%s", owner, repo, created.Number)
|
||
closeResp, err := g.doRequest("PATCH", closeURL, map[string]interface{}{
|
||
"access_token": g.Token,
|
||
"state": "closed",
|
||
})
|
||
if err == nil {
|
||
closeResp.Body.Close()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (g *GiteeAPI) ListPullRequests(owner, repo string) ([]PullRequestInfo, error) {
|
||
var all []PullRequestInfo
|
||
for _, state := range []string{"open", "closed", "merged"} {
|
||
page := 1
|
||
for {
|
||
url := fmt.Sprintf("https://gitee.com/api/v5/repos/%s/%s/pulls?access_token=%s&state=%s&per_page=100&page=%d", owner, repo, g.Token, state, page)
|
||
resp, err := g.doRequest("GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var items []struct {
|
||
ID int64 `json:"id"`
|
||
Number int `json:"number"`
|
||
Title string `json:"title"`
|
||
Body string `json:"body"`
|
||
State string `json:"state"`
|
||
Head struct {
|
||
Ref string `json:"ref"`
|
||
} `json:"head"`
|
||
Base struct {
|
||
Ref string `json:"ref"`
|
||
} `json:"base"`
|
||
User struct {
|
||
Login string `json:"login"`
|
||
} `json:"user"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
if resp.StatusCode != 200 {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
|
||
}
|
||
json.NewDecoder(resp.Body).Decode(&items)
|
||
resp.Body.Close()
|
||
if len(items) == 0 {
|
||
break
|
||
}
|
||
for _, it := range items {
|
||
all = append(all, PullRequestInfo{
|
||
ID: it.ID, Number: it.Number, Title: it.Title, Body: it.Body,
|
||
State: it.State, Head: it.Head.Ref, Base: it.Base.Ref,
|
||
User: it.User.Login, CreatedAt: it.CreatedAt,
|
||
})
|
||
}
|
||
if len(items) < 100 {
|
||
break
|
||
}
|
||
page++
|
||
}
|
||
}
|
||
return all, nil
|
||
}
|