mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-09-30 19:22:08 +00:00
601 lines
17 KiB
Go
601 lines
17 KiB
Go
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||
|
// SPDX-License-Identifier: MIT
|
||
|
|
||
|
package migrations
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"forgejo.org/modules/json"
|
||
|
"forgejo.org/modules/log"
|
||
|
base "forgejo.org/modules/migration"
|
||
|
"forgejo.org/modules/proxy"
|
||
|
"forgejo.org/modules/structs"
|
||
|
"forgejo.org/modules/util"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
_ base.Downloader = &PagureDownloader{}
|
||
|
_ base.DownloaderFactory = &PagureDownloaderFactory{}
|
||
|
)
|
||
|
|
||
|
func init() {
|
||
|
RegisterDownloaderFactory(&PagureDownloaderFactory{})
|
||
|
}
|
||
|
|
||
|
// PagureDownloaderFactory defines a downloader factory
|
||
|
type PagureDownloaderFactory struct{}
|
||
|
|
||
|
// PagureUser defines a user on Pagure to be migrated over to Forgejo
|
||
|
type PagureUser struct {
|
||
|
FullURL string `json:"full_url"`
|
||
|
Fullname string `json:"fullname"`
|
||
|
Name string `json:"name"`
|
||
|
URLPath string `json:"url_path"`
|
||
|
}
|
||
|
|
||
|
// PagureRepoInfo describes the repository with preliminary information
|
||
|
type PagureRepoInfo struct {
|
||
|
ID int64 `json:"id"`
|
||
|
Name string `json:"name"`
|
||
|
FullName string `json:"fullname"`
|
||
|
Description string `json:"description"`
|
||
|
Topics []string `json:"tags"`
|
||
|
CloseStatuses []string `json:"close_status"`
|
||
|
Priorities map[string]string `json:"priorities"`
|
||
|
Milestones map[string]struct {
|
||
|
Active bool `json:"active"`
|
||
|
Date *string `json:"date"`
|
||
|
} `json:"milestones"`
|
||
|
}
|
||
|
|
||
|
// PagureDownloader implements a Downloader interface to get repository information from Pagure
|
||
|
type PagureDownloader struct {
|
||
|
base.NullDownloader
|
||
|
ctx context.Context
|
||
|
client *http.Client
|
||
|
baseURL *url.URL
|
||
|
meta PagureRepoInfo
|
||
|
repoName string
|
||
|
token string
|
||
|
privateIssuesOnlyRepo bool
|
||
|
repoID int64
|
||
|
maxIssueIndex int64
|
||
|
userMap map[string]*PagureUser
|
||
|
milestoneMap map[int64]string
|
||
|
priorities map[string]string
|
||
|
}
|
||
|
|
||
|
// PagureLabelsList defines a list of labels under an issue tracker
|
||
|
type PagureLabelsList struct {
|
||
|
Labels []string `json:"tags"`
|
||
|
}
|
||
|
|
||
|
// PagureLabel defines a label under the issue tracker labels list
|
||
|
type PagureLabel struct {
|
||
|
Label string `json:"tag"`
|
||
|
LabelColor string `json:"tag_color"`
|
||
|
LabelDescription string `json:"tag_description"`
|
||
|
}
|
||
|
|
||
|
// PagureIssueContext confirms if a said unit is an issue ticket or a pull request
|
||
|
type PagureIssueContext struct {
|
||
|
IsPullRequest bool
|
||
|
}
|
||
|
|
||
|
// PagureIssuesResponse describes a list of issue tickets under an issue tracker
|
||
|
type PagureIssuesResponse struct {
|
||
|
Issues []struct {
|
||
|
Assignee any `json:"assignee"`
|
||
|
Blocks []string `json:"blocks"`
|
||
|
CloseStatus string `json:"close_status"`
|
||
|
ClosedAt string `json:"closed_at"`
|
||
|
ClosedBy any `json:"closed_by"`
|
||
|
Comments []any `json:"comments"`
|
||
|
Content string `json:"content"`
|
||
|
CustomFields []any `json:"custom_fields"`
|
||
|
DateCreated string `json:"date_created"`
|
||
|
Depends []any `json:"depends"`
|
||
|
ID int64 `json:"id"`
|
||
|
LastUpdated string `json:"last_updated"`
|
||
|
Milestone string `json:"milestone"`
|
||
|
Priority int64 `json:"priority"`
|
||
|
Private bool `json:"private"`
|
||
|
Status string `json:"status"`
|
||
|
Tags []string `json:"tags"`
|
||
|
Title string `json:"title"`
|
||
|
User PagureUser `json:"user"`
|
||
|
} `json:"issues"`
|
||
|
Pagination struct {
|
||
|
First string `json:"first"`
|
||
|
Last string `json:"last"`
|
||
|
Next *string `json:"next"`
|
||
|
Page int `json:"page"`
|
||
|
Pages int `json:"pages"`
|
||
|
PerPage int `json:"per_page"`
|
||
|
Prev string `json:"prev"`
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// PagureIssueDetail describes a list of issue comments under an issue ticket
|
||
|
type PagureIssueDetail struct {
|
||
|
Comment string `json:"comment"`
|
||
|
DateCreated string `json:"date_created"`
|
||
|
ID int64 `json:"id"`
|
||
|
Notification bool `json:"notification"`
|
||
|
User PagureUser `json:"user"`
|
||
|
}
|
||
|
|
||
|
// PagureCommitInfo describes a commit
|
||
|
type PagureCommitInfo struct {
|
||
|
Author string `json:"author"`
|
||
|
CommitTime int64 `json:"commit_time"`
|
||
|
CommitTimeOffset int `json:"commit_time_offset"`
|
||
|
Committer string `json:"committer"`
|
||
|
Hash string `json:"hash"`
|
||
|
Message string `json:"message"`
|
||
|
ParentIDs []string `json:"parent_ids"`
|
||
|
TreeID string `json:"tree_id"`
|
||
|
}
|
||
|
|
||
|
// PagurePRRresponse describes a list of pull requests under an issue tracker
|
||
|
type PagurePRResponse struct {
|
||
|
Pagination struct {
|
||
|
Next *string `json:"next"`
|
||
|
} `json:"pagination"`
|
||
|
Requests []struct {
|
||
|
Branch string `json:"branch"`
|
||
|
BranchFrom string `json:"branch_from"`
|
||
|
CommitStop string `json:"commit_stop"`
|
||
|
DateCreated string `json:"date_created"`
|
||
|
FullURL string `json:"full_url"`
|
||
|
ID int `json:"id"`
|
||
|
InitialComment string `json:"initial_comment"`
|
||
|
LastUpdated string `json:"last_updated"`
|
||
|
Status string `json:"status"`
|
||
|
Tags []string `json:"tags"`
|
||
|
Title string `json:"title"`
|
||
|
User PagureUser `json:"user"`
|
||
|
ClosedAt string `json:"closed_at"`
|
||
|
ClosedBy PagureUser `json:"closed_by"`
|
||
|
RepoFrom struct {
|
||
|
FullURL string `json:"full_url"`
|
||
|
} `json:"repo_from"`
|
||
|
} `json:"requests"`
|
||
|
}
|
||
|
|
||
|
// processDate converts epoch time string to Go formatted time
|
||
|
func processDate(dateStr *string) time.Time {
|
||
|
date := time.Time{}
|
||
|
if dateStr == nil || *dateStr == "" {
|
||
|
return date
|
||
|
}
|
||
|
|
||
|
unix, err := strconv.Atoi(*dateStr)
|
||
|
if err != nil {
|
||
|
log.Error("Error:", err)
|
||
|
return date
|
||
|
}
|
||
|
|
||
|
date = time.Unix(int64(unix), 0)
|
||
|
return date
|
||
|
}
|
||
|
|
||
|
// New returns a downloader related to this factory according MigrateOptions
|
||
|
func (f *PagureDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
|
||
|
u, err := url.Parse(opts.CloneAddr)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
var repoName string
|
||
|
|
||
|
fields := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||
|
if len(fields) == 2 {
|
||
|
repoName = fields[0] + "/" + strings.TrimSuffix(fields[1], ".git")
|
||
|
} else if len(fields) == 1 {
|
||
|
repoName = strings.TrimSuffix(fields[0], ".git")
|
||
|
} else {
|
||
|
return nil, fmt.Errorf("invalid path: %s", u.Path)
|
||
|
}
|
||
|
|
||
|
u.Path, u.Fragment = "", ""
|
||
|
log.Info("Create Pagure downloader. BaseURL: %v RepoName: %s", u, repoName)
|
||
|
|
||
|
return NewPagureDownloader(ctx, u, opts.AuthToken, repoName), nil
|
||
|
}
|
||
|
|
||
|
// GitServiceType returns the type of Git service
|
||
|
func (f *PagureDownloaderFactory) GitServiceType() structs.GitServiceType {
|
||
|
return structs.PagureService
|
||
|
}
|
||
|
|
||
|
// SetContext sets context
|
||
|
func (d *PagureDownloader) SetContext(ctx context.Context) {
|
||
|
d.ctx = ctx
|
||
|
}
|
||
|
|
||
|
// NewPagureDownloader creates a new downloader object
|
||
|
func NewPagureDownloader(ctx context.Context, baseURL *url.URL, token, repoName string) *PagureDownloader {
|
||
|
var privateIssuesOnlyRepo bool
|
||
|
if token != "" {
|
||
|
privateIssuesOnlyRepo = true
|
||
|
}
|
||
|
|
||
|
downloader := &PagureDownloader{
|
||
|
ctx: ctx,
|
||
|
baseURL: baseURL,
|
||
|
repoName: repoName,
|
||
|
client: &http.Client{
|
||
|
Transport: &http.Transport{
|
||
|
Proxy: proxy.Proxy(),
|
||
|
},
|
||
|
},
|
||
|
token: token,
|
||
|
privateIssuesOnlyRepo: privateIssuesOnlyRepo,
|
||
|
userMap: make(map[string]*PagureUser),
|
||
|
milestoneMap: make(map[int64]string),
|
||
|
priorities: make(map[string]string),
|
||
|
}
|
||
|
|
||
|
return downloader
|
||
|
}
|
||
|
|
||
|
// String sets the default text for information purposes
|
||
|
func (d *PagureDownloader) String() string {
|
||
|
return fmt.Sprintf("migration from Pagure server %s [%d]/%s", d.baseURL, d.repoID, d.repoName)
|
||
|
}
|
||
|
|
||
|
// LogString sets the default text for logging purposes
|
||
|
func (d *PagureDownloader) LogString() string {
|
||
|
if d == nil {
|
||
|
return "<PagureDownloader nil>"
|
||
|
}
|
||
|
return fmt.Sprintf("<PagureDownloader %s [%d]/%s>", d.baseURL, d.repoID, d.repoName)
|
||
|
}
|
||
|
|
||
|
// callAPI handles all the requests made against Pagure
|
||
|
func (d *PagureDownloader) callAPI(endpoint string, parameter map[string]string, result any) error {
|
||
|
u, err := d.baseURL.Parse(endpoint)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if parameter != nil {
|
||
|
query := u.Query()
|
||
|
for k, v := range parameter {
|
||
|
query.Set(k, v)
|
||
|
}
|
||
|
u.RawQuery = query.Encode()
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if d.privateIssuesOnlyRepo {
|
||
|
req.Header.Set("Authorization", "token "+d.token)
|
||
|
}
|
||
|
|
||
|
resp, err := d.client.Do(req)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
decoder := json.NewDecoder(resp.Body)
|
||
|
return decoder.Decode(&result)
|
||
|
}
|
||
|
|
||
|
// GetRepoInfo returns repository information from Pagure
|
||
|
func (d *PagureDownloader) GetRepoInfo() (*base.Repository, error) {
|
||
|
err := d.callAPI("/api/0/"+d.repoName, nil, &d.meta)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
d.repoID, d.priorities = d.meta.ID, d.meta.Priorities
|
||
|
|
||
|
cloneURL, err := d.baseURL.Parse(d.meta.FullName)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
originalURL, err := d.baseURL.Parse(d.meta.FullName)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &base.Repository{
|
||
|
Name: d.meta.Name,
|
||
|
Description: d.meta.Description,
|
||
|
CloneURL: cloneURL.String() + ".git",
|
||
|
OriginalURL: originalURL.String(),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// GetMilestones returns milestones information from Pagure
|
||
|
func (d *PagureDownloader) GetMilestones() ([]*base.Milestone, error) {
|
||
|
milestones := make([]*base.Milestone, 0, len(d.meta.Milestones))
|
||
|
for name, details := range d.meta.Milestones {
|
||
|
state := "closed"
|
||
|
if details.Active {
|
||
|
state = "open"
|
||
|
}
|
||
|
|
||
|
deadline := processDate(details.Date)
|
||
|
milestones = append(milestones, &base.Milestone{
|
||
|
Title: name,
|
||
|
Description: "",
|
||
|
Deadline: &deadline,
|
||
|
State: state,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
return milestones, nil
|
||
|
}
|
||
|
|
||
|
// GetLabels returns labels information from Pagure
|
||
|
func (d *PagureDownloader) GetLabels() ([]*base.Label, error) {
|
||
|
rawLabels := PagureLabelsList{}
|
||
|
|
||
|
err := d.callAPI("/api/0/"+d.repoName+"/tags", nil, &rawLabels)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
labels := make([]*base.Label, 0, len(rawLabels.Labels)+len(d.meta.CloseStatuses))
|
||
|
|
||
|
for _, label := range rawLabels.Labels {
|
||
|
rawLabel := PagureLabel{}
|
||
|
err = d.callAPI("/api/0/"+d.repoName+"/tag/"+label, nil, &rawLabel)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
labels = append(labels, &base.Label{
|
||
|
Name: label,
|
||
|
Description: rawLabel.LabelDescription,
|
||
|
Color: strings.TrimPrefix(rawLabel.LabelColor, "#"),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
for _, closeStatus := range d.meta.CloseStatuses {
|
||
|
labels = append(labels, &base.Label{
|
||
|
Name: "Closed As/" + closeStatus,
|
||
|
Description: "Closed with the reason of " + closeStatus,
|
||
|
Color: "FF0000",
|
||
|
Exclusive: true,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
for _, value := range d.priorities {
|
||
|
if value != "" {
|
||
|
labels = append(labels, &base.Label{
|
||
|
Name: "Priority/" + value,
|
||
|
Description: "Priority of " + value,
|
||
|
Color: "FF00FF",
|
||
|
Exclusive: true,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return labels, nil
|
||
|
}
|
||
|
|
||
|
// GetIssues returns issue tickets from Pagure
|
||
|
func (d *PagureDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
|
||
|
rawIssues := PagureIssuesResponse{}
|
||
|
|
||
|
err := d.callAPI(
|
||
|
"/api/0/"+d.repoName+"/issues",
|
||
|
map[string]string{
|
||
|
"page": strconv.Itoa(page),
|
||
|
"per_page": strconv.Itoa(perPage),
|
||
|
"status": "all",
|
||
|
},
|
||
|
&rawIssues,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return nil, false, err
|
||
|
}
|
||
|
|
||
|
issues := make([]*base.Issue, 0, len(rawIssues.Issues))
|
||
|
for _, issue := range rawIssues.Issues {
|
||
|
log.Debug("Processing issue %d", issue.ID)
|
||
|
if d.privateIssuesOnlyRepo && !issue.Private {
|
||
|
log.Info("Skipping issue %d because it is not private and we are only downloading private issues", issue.ID)
|
||
|
continue
|
||
|
}
|
||
|
labels := []*base.Label{}
|
||
|
for _, tag := range issue.Tags {
|
||
|
labels = append(labels, &base.Label{Name: tag})
|
||
|
}
|
||
|
|
||
|
if issue.CloseStatus != "" {
|
||
|
labels = append(labels, &base.Label{Name: "Closed As/" + issue.CloseStatus})
|
||
|
}
|
||
|
|
||
|
priorityStr := ""
|
||
|
if issue.Priority != 0 {
|
||
|
priorityStr = strconv.FormatInt(issue.Priority, 10)
|
||
|
}
|
||
|
|
||
|
if priorityStr != "" {
|
||
|
priorityValue, ok := d.priorities[priorityStr]
|
||
|
if ok {
|
||
|
labels = append(labels, &base.Label{Name: "Priority/" + priorityValue})
|
||
|
}
|
||
|
}
|
||
|
log.Trace("Adding issue: %d", issue.ID)
|
||
|
|
||
|
closedat := processDate(&issue.ClosedAt)
|
||
|
|
||
|
issues = append(issues, &base.Issue{
|
||
|
Title: issue.Title,
|
||
|
Number: issue.ID,
|
||
|
PosterName: issue.User.Name,
|
||
|
PosterID: -1,
|
||
|
Content: issue.Content,
|
||
|
Milestone: issue.Milestone,
|
||
|
State: strings.ToLower(issue.Status),
|
||
|
Created: processDate(&issue.DateCreated),
|
||
|
Updated: processDate(&issue.LastUpdated),
|
||
|
Closed: &closedat,
|
||
|
Labels: labels,
|
||
|
ForeignIndex: issue.ID,
|
||
|
Context: PagureIssueContext{IsPullRequest: false},
|
||
|
})
|
||
|
|
||
|
if d.maxIssueIndex < issue.ID {
|
||
|
d.maxIssueIndex = issue.ID
|
||
|
}
|
||
|
}
|
||
|
hasNext := rawIssues.Pagination.Next == nil
|
||
|
|
||
|
return issues, hasNext, nil
|
||
|
}
|
||
|
|
||
|
// GetComments returns issue comments from Pagure
|
||
|
func (d *PagureDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
|
||
|
context, ok := commentable.GetContext().(PagureIssueContext)
|
||
|
if !ok {
|
||
|
return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
|
||
|
}
|
||
|
|
||
|
list := struct {
|
||
|
Comments []PagureIssueDetail `json:"comments"`
|
||
|
}{}
|
||
|
var endpoint string
|
||
|
|
||
|
if context.IsPullRequest {
|
||
|
endpoint = fmt.Sprintf("/api/0/%s/pull-request/%d", d.repoName, commentable.GetForeignIndex())
|
||
|
} else {
|
||
|
endpoint = fmt.Sprintf("/api/0/%s/issue/%d", d.repoName, commentable.GetForeignIndex())
|
||
|
}
|
||
|
|
||
|
err := d.callAPI(endpoint, nil, &list)
|
||
|
if err != nil {
|
||
|
log.Error("Error calling API: %v", err)
|
||
|
return nil, false, err
|
||
|
}
|
||
|
|
||
|
comments := make([]*base.Comment, 0, len(list.Comments))
|
||
|
for _, unit := range list.Comments {
|
||
|
if len(unit.Comment) == 0 {
|
||
|
log.Error("Empty comment")
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
log.Trace("Adding comment: %d", unit.ID)
|
||
|
c := &base.Comment{
|
||
|
IssueIndex: commentable.GetLocalIndex(),
|
||
|
Index: unit.ID,
|
||
|
PosterName: unit.User.Name,
|
||
|
PosterID: -1,
|
||
|
Content: unit.Comment,
|
||
|
Created: processDate(&unit.DateCreated),
|
||
|
}
|
||
|
comments = append(comments, c)
|
||
|
}
|
||
|
|
||
|
return comments, true, nil
|
||
|
}
|
||
|
|
||
|
// GetPullRequests returns pull requests from Pagure
|
||
|
func (d *PagureDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
|
||
|
// Could not figure out how to disable this in opts, so if a private issues only repo,
|
||
|
// We just return an empty list
|
||
|
if d.privateIssuesOnlyRepo {
|
||
|
pullRequests := make([]*base.PullRequest, 0)
|
||
|
return pullRequests, true, nil
|
||
|
}
|
||
|
|
||
|
rawPullRequests := PagurePRResponse{}
|
||
|
commit := PagureCommitInfo{}
|
||
|
|
||
|
err := d.callAPI(
|
||
|
"/api/0/"+d.repoName+"/pull-requests",
|
||
|
map[string]string{
|
||
|
"page": strconv.Itoa(page),
|
||
|
"per_page": strconv.Itoa(perPage),
|
||
|
"status": "all",
|
||
|
},
|
||
|
&rawPullRequests,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return nil, false, err
|
||
|
}
|
||
|
|
||
|
pullRequests := make([]*base.PullRequest, 0, len(rawPullRequests.Requests))
|
||
|
|
||
|
for _, pr := range rawPullRequests.Requests {
|
||
|
var state, baseSHA string
|
||
|
var merged bool
|
||
|
labels := []*base.Label{}
|
||
|
|
||
|
for _, tag := range pr.Tags {
|
||
|
labels = append(labels, &base.Label{Name: tag})
|
||
|
}
|
||
|
mergedtime := processDate(&pr.ClosedAt)
|
||
|
|
||
|
err = d.callAPI("/api/0/"+d.repoName+"/c/"+pr.CommitStop+"/info", nil, &commit)
|
||
|
if err != nil {
|
||
|
return nil, false, err
|
||
|
}
|
||
|
|
||
|
if util.ASCIIEqualFold(pr.Status, "merged") {
|
||
|
state, merged, baseSHA = "closed", true, commit.ParentIDs[0]
|
||
|
} else if util.ASCIIEqualFold(pr.Status, "open") {
|
||
|
state, merged, baseSHA = "open", false, commit.ParentIDs[0]
|
||
|
} else {
|
||
|
state, merged, baseSHA = "closed", false, commit.ParentIDs[0]
|
||
|
}
|
||
|
|
||
|
pullRequests = append(pullRequests, &base.PullRequest{
|
||
|
Title: pr.Title,
|
||
|
Number: int64(pr.ID),
|
||
|
PosterName: pr.User.Name,
|
||
|
PosterID: -1,
|
||
|
Content: pr.InitialComment,
|
||
|
State: state,
|
||
|
Created: processDate(&pr.DateCreated),
|
||
|
Updated: processDate(&pr.LastUpdated),
|
||
|
MergedTime: &mergedtime,
|
||
|
Closed: &mergedtime,
|
||
|
Merged: merged,
|
||
|
Labels: labels,
|
||
|
Head: base.PullRequestBranch{
|
||
|
Ref: pr.BranchFrom,
|
||
|
SHA: pr.CommitStop,
|
||
|
RepoName: d.repoName,
|
||
|
CloneURL: pr.RepoFrom.FullURL + ".git",
|
||
|
},
|
||
|
Base: base.PullRequestBranch{
|
||
|
Ref: pr.Branch,
|
||
|
SHA: baseSHA,
|
||
|
RepoName: d.repoName,
|
||
|
},
|
||
|
ForeignIndex: int64(pr.ID),
|
||
|
PatchURL: pr.FullURL + ".patch",
|
||
|
Context: PagureIssueContext{IsPullRequest: true},
|
||
|
})
|
||
|
|
||
|
// SECURITY: Ensure that the PR is safe
|
||
|
_ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], d.baseURL.String(), d)
|
||
|
}
|
||
|
|
||
|
hasNext := rawPullRequests.Pagination.Next == nil
|
||
|
|
||
|
return pullRequests, hasNext, nil
|
||
|
}
|
||
|
|
||
|
// GetTopics return repository topics from Pagure
|
||
|
func (d *PagureDownloader) GetTopics() ([]string, error) {
|
||
|
return d.meta.Topics, nil
|
||
|
}
|