Files
GitPilot/internal/app/platform_extra.go
zyj 3ebcd10a23
Some checks failed
Release / build-windows (push) Has been cancelled
release: v0.4.1 - GitLab支持、迁移优化、Bug修复
 新功能:
- 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 方法
2026-03-12 15:07:41 +08:00

1245 lines
34 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}