722a7c902d
In investigating #7947 it has become clear that the storage component of go-git repositories needs closing. This PR adds this Close function and adds the Close functions as necessary. In TransferOwnership the ctx.Repo.GitRepo is closed if it is open to help prevent the risk of multiple open files. Fixes #7947
630 lines
16 KiB
Go
630 lines
16 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// Copyright 2018 Jonas Franz. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package migrations
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/models"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/migrations/base"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
|
|
gouuid "github.com/satori/go.uuid"
|
|
)
|
|
|
|
var (
|
|
_ base.Uploader = &GiteaLocalUploader{}
|
|
)
|
|
|
|
// GiteaLocalUploader implements an Uploader to gitea sites
|
|
type GiteaLocalUploader struct {
|
|
doer *models.User
|
|
repoOwner string
|
|
repoName string
|
|
repo *models.Repository
|
|
labels sync.Map
|
|
milestones sync.Map
|
|
issues sync.Map
|
|
gitRepo *git.Repository
|
|
prHeadCache map[string]struct{}
|
|
userMap map[int64]int64 // external user id mapping to user id
|
|
gitServiceType structs.GitServiceType
|
|
}
|
|
|
|
// NewGiteaLocalUploader creates an gitea Uploader via gitea API v1
|
|
func NewGiteaLocalUploader(doer *models.User, repoOwner, repoName string) *GiteaLocalUploader {
|
|
return &GiteaLocalUploader{
|
|
doer: doer,
|
|
repoOwner: repoOwner,
|
|
repoName: repoName,
|
|
prHeadCache: make(map[string]struct{}),
|
|
userMap: make(map[int64]int64),
|
|
}
|
|
}
|
|
|
|
// MaxBatchInsertSize returns the table's max batch insert size
|
|
func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int {
|
|
switch tp {
|
|
case "issue":
|
|
return models.MaxBatchInsertSize(new(models.Issue))
|
|
case "comment":
|
|
return models.MaxBatchInsertSize(new(models.Comment))
|
|
case "milestone":
|
|
return models.MaxBatchInsertSize(new(models.Milestone))
|
|
case "label":
|
|
return models.MaxBatchInsertSize(new(models.Label))
|
|
case "release":
|
|
return models.MaxBatchInsertSize(new(models.Release))
|
|
case "pullrequest":
|
|
return models.MaxBatchInsertSize(new(models.PullRequest))
|
|
}
|
|
return 10
|
|
}
|
|
|
|
// CreateRepo creates a repository
|
|
func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error {
|
|
owner, err := models.GetUserByName(g.repoOwner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var remoteAddr = repo.CloneURL
|
|
if len(opts.AuthUsername) > 0 {
|
|
u, err := url.Parse(repo.CloneURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword)
|
|
remoteAddr = u.String()
|
|
}
|
|
|
|
var r *models.Repository
|
|
if opts.MigrateToRepoID <= 0 {
|
|
r, err = models.CreateRepository(g.doer, owner, models.CreateRepoOptions{
|
|
Name: g.repoName,
|
|
Description: repo.Description,
|
|
OriginalURL: repo.OriginalURL,
|
|
IsPrivate: opts.Private,
|
|
IsMirror: opts.Mirror,
|
|
Status: models.RepositoryBeingMigrated,
|
|
})
|
|
} else {
|
|
r, err = models.GetRepositoryByID(opts.MigrateToRepoID)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, err = models.MigrateRepositoryGitData(g.doer, owner, r, structs.MigrateRepoOption{
|
|
RepoName: g.repoName,
|
|
Description: repo.Description,
|
|
OriginalURL: repo.OriginalURL,
|
|
GitServiceType: opts.GitServiceType,
|
|
Mirror: repo.IsMirror,
|
|
CloneAddr: remoteAddr,
|
|
Private: repo.IsPrivate,
|
|
Wiki: opts.Wiki,
|
|
Releases: opts.Releases, // if didn't get releases, then sync them from tags
|
|
})
|
|
|
|
g.repo = r
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g.gitRepo, err = git.OpenRepository(r.RepoPath())
|
|
return err
|
|
}
|
|
|
|
// Close closes this uploader
|
|
func (g *GiteaLocalUploader) Close() {
|
|
if g.gitRepo != nil {
|
|
g.gitRepo.Close()
|
|
}
|
|
}
|
|
|
|
// CreateTopics creates topics
|
|
func (g *GiteaLocalUploader) CreateTopics(topics ...string) error {
|
|
return models.SaveTopics(g.repo.ID, topics...)
|
|
}
|
|
|
|
// CreateMilestones creates milestones
|
|
func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) error {
|
|
var mss = make([]*models.Milestone, 0, len(milestones))
|
|
for _, milestone := range milestones {
|
|
var deadline timeutil.TimeStamp
|
|
if milestone.Deadline != nil {
|
|
deadline = timeutil.TimeStamp(milestone.Deadline.Unix())
|
|
}
|
|
if deadline == 0 {
|
|
deadline = timeutil.TimeStamp(time.Date(9999, 1, 1, 0, 0, 0, 0, setting.DefaultUILocation).Unix())
|
|
}
|
|
var ms = models.Milestone{
|
|
RepoID: g.repo.ID,
|
|
Name: milestone.Title,
|
|
Content: milestone.Description,
|
|
IsClosed: milestone.State == "closed",
|
|
DeadlineUnix: deadline,
|
|
}
|
|
if ms.IsClosed && milestone.Closed != nil {
|
|
ms.ClosedDateUnix = timeutil.TimeStamp(milestone.Closed.Unix())
|
|
}
|
|
mss = append(mss, &ms)
|
|
}
|
|
|
|
err := models.InsertMilestones(mss...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, ms := range mss {
|
|
g.milestones.Store(ms.Name, ms.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateLabels creates labels
|
|
func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
|
|
var lbs = make([]*models.Label, 0, len(labels))
|
|
for _, label := range labels {
|
|
lbs = append(lbs, &models.Label{
|
|
RepoID: g.repo.ID,
|
|
Name: label.Name,
|
|
Description: label.Description,
|
|
Color: fmt.Sprintf("#%s", label.Color),
|
|
})
|
|
}
|
|
|
|
err := models.NewLabels(lbs...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, lb := range lbs {
|
|
g.labels.Store(lb.Name, lb)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateReleases creates releases
|
|
func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
|
|
var rels = make([]*models.Release, 0, len(releases))
|
|
for _, release := range releases {
|
|
var rel = models.Release{
|
|
RepoID: g.repo.ID,
|
|
TagName: release.TagName,
|
|
LowerTagName: strings.ToLower(release.TagName),
|
|
Target: release.TargetCommitish,
|
|
Title: release.Name,
|
|
Sha1: release.TargetCommitish,
|
|
Note: release.Body,
|
|
IsDraft: release.Draft,
|
|
IsPrerelease: release.Prerelease,
|
|
IsTag: false,
|
|
CreatedUnix: timeutil.TimeStamp(release.Created.Unix()),
|
|
}
|
|
|
|
userid, ok := g.userMap[release.PublisherID]
|
|
tp := g.gitServiceType.Name()
|
|
if !ok && tp != "" {
|
|
var err error
|
|
userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", release.PublisherID))
|
|
if err != nil {
|
|
log.Error("GetUserIDByExternalUserID: %v", err)
|
|
}
|
|
if userid > 0 {
|
|
g.userMap[release.PublisherID] = userid
|
|
}
|
|
}
|
|
|
|
if userid > 0 {
|
|
rel.PublisherID = userid
|
|
} else {
|
|
rel.PublisherID = g.doer.ID
|
|
rel.OriginalAuthor = release.PublisherName
|
|
rel.OriginalAuthorID = release.PublisherID
|
|
}
|
|
|
|
// calc NumCommits
|
|
commit, err := g.gitRepo.GetCommit(rel.TagName)
|
|
if err != nil {
|
|
return fmt.Errorf("GetCommit: %v", err)
|
|
}
|
|
rel.NumCommits, err = commit.CommitsCount()
|
|
if err != nil {
|
|
return fmt.Errorf("CommitsCount: %v", err)
|
|
}
|
|
|
|
for _, asset := range release.Assets {
|
|
var attach = models.Attachment{
|
|
UUID: gouuid.NewV4().String(),
|
|
Name: asset.Name,
|
|
DownloadCount: int64(*asset.DownloadCount),
|
|
Size: int64(*asset.Size),
|
|
CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()),
|
|
}
|
|
|
|
// download attachment
|
|
err = func() error {
|
|
resp, err := http.Get(asset.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
localPath := attach.LocalPath()
|
|
if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil {
|
|
return fmt.Errorf("MkdirAll: %v", err)
|
|
}
|
|
|
|
fw, err := os.Create(localPath)
|
|
if err != nil {
|
|
return fmt.Errorf("Create: %v", err)
|
|
}
|
|
defer fw.Close()
|
|
|
|
_, err = io.Copy(fw, resp.Body)
|
|
return err
|
|
}()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel.Attachments = append(rel.Attachments, &attach)
|
|
}
|
|
|
|
rels = append(rels, &rel)
|
|
}
|
|
if err := models.InsertReleases(rels...); err != nil {
|
|
return err
|
|
}
|
|
|
|
// sync tags to releases in database
|
|
return models.SyncReleasesWithTags(g.repo, g.gitRepo)
|
|
}
|
|
|
|
// CreateIssues creates issues
|
|
func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
|
|
var iss = make([]*models.Issue, 0, len(issues))
|
|
for _, issue := range issues {
|
|
var labels []*models.Label
|
|
for _, label := range issue.Labels {
|
|
lb, ok := g.labels.Load(label.Name)
|
|
if ok {
|
|
labels = append(labels, lb.(*models.Label))
|
|
}
|
|
}
|
|
|
|
var milestoneID int64
|
|
if issue.Milestone != "" {
|
|
milestone, ok := g.milestones.Load(issue.Milestone)
|
|
if ok {
|
|
milestoneID = milestone.(int64)
|
|
}
|
|
}
|
|
|
|
var is = models.Issue{
|
|
RepoID: g.repo.ID,
|
|
Repo: g.repo,
|
|
Index: issue.Number,
|
|
Title: issue.Title,
|
|
Content: issue.Content,
|
|
IsClosed: issue.State == "closed",
|
|
IsLocked: issue.IsLocked,
|
|
MilestoneID: milestoneID,
|
|
Labels: labels,
|
|
CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()),
|
|
}
|
|
|
|
userid, ok := g.userMap[issue.PosterID]
|
|
tp := g.gitServiceType.Name()
|
|
if !ok && tp != "" {
|
|
var err error
|
|
userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", issue.PosterID))
|
|
if err != nil {
|
|
log.Error("GetUserIDByExternalUserID: %v", err)
|
|
}
|
|
if userid > 0 {
|
|
g.userMap[issue.PosterID] = userid
|
|
}
|
|
}
|
|
|
|
if userid > 0 {
|
|
is.PosterID = userid
|
|
} else {
|
|
is.PosterID = g.doer.ID
|
|
is.OriginalAuthor = issue.PosterName
|
|
is.OriginalAuthorID = issue.PosterID
|
|
}
|
|
|
|
if issue.Closed != nil {
|
|
is.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix())
|
|
}
|
|
// TODO: add reactions
|
|
iss = append(iss, &is)
|
|
}
|
|
|
|
err := models.InsertIssues(iss...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, is := range iss {
|
|
g.issues.Store(is.Index, is.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateComments creates comments of issues
|
|
func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
|
|
var cms = make([]*models.Comment, 0, len(comments))
|
|
for _, comment := range comments {
|
|
var issueID int64
|
|
if issueIDStr, ok := g.issues.Load(comment.IssueIndex); !ok {
|
|
issue, err := models.GetIssueByIndex(g.repo.ID, comment.IssueIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
issueID = issue.ID
|
|
g.issues.Store(comment.IssueIndex, issueID)
|
|
} else {
|
|
issueID = issueIDStr.(int64)
|
|
}
|
|
|
|
userid, ok := g.userMap[comment.PosterID]
|
|
tp := g.gitServiceType.Name()
|
|
if !ok && tp != "" {
|
|
var err error
|
|
userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", comment.PosterID))
|
|
if err != nil {
|
|
log.Error("GetUserIDByExternalUserID: %v", err)
|
|
}
|
|
if userid > 0 {
|
|
g.userMap[comment.PosterID] = userid
|
|
}
|
|
}
|
|
|
|
cm := models.Comment{
|
|
IssueID: issueID,
|
|
Type: models.CommentTypeComment,
|
|
Content: comment.Content,
|
|
CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()),
|
|
}
|
|
|
|
if userid > 0 {
|
|
cm.PosterID = userid
|
|
} else {
|
|
cm.PosterID = g.doer.ID
|
|
cm.OriginalAuthor = comment.PosterName
|
|
cm.OriginalAuthorID = comment.PosterID
|
|
}
|
|
|
|
cms = append(cms, &cm)
|
|
|
|
// TODO: Reactions
|
|
}
|
|
|
|
return models.InsertIssueComments(cms)
|
|
}
|
|
|
|
// CreatePullRequests creates pull requests
|
|
func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error {
|
|
var gprs = make([]*models.PullRequest, 0, len(prs))
|
|
for _, pr := range prs {
|
|
gpr, err := g.newPullRequest(pr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userid, ok := g.userMap[pr.PosterID]
|
|
tp := g.gitServiceType.Name()
|
|
if !ok && tp != "" {
|
|
var err error
|
|
userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", pr.PosterID))
|
|
if err != nil {
|
|
log.Error("GetUserIDByExternalUserID: %v", err)
|
|
}
|
|
if userid > 0 {
|
|
g.userMap[pr.PosterID] = userid
|
|
}
|
|
}
|
|
|
|
if userid > 0 {
|
|
gpr.Issue.PosterID = userid
|
|
} else {
|
|
gpr.Issue.PosterID = g.doer.ID
|
|
gpr.Issue.OriginalAuthor = pr.PosterName
|
|
gpr.Issue.OriginalAuthorID = pr.PosterID
|
|
}
|
|
|
|
gprs = append(gprs, gpr)
|
|
}
|
|
if err := models.InsertPullRequests(gprs...); err != nil {
|
|
return err
|
|
}
|
|
for _, pr := range gprs {
|
|
g.issues.Store(pr.Issue.Index, pr.Issue.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullRequest, error) {
|
|
var labels []*models.Label
|
|
for _, label := range pr.Labels {
|
|
lb, ok := g.labels.Load(label.Name)
|
|
if ok {
|
|
labels = append(labels, lb.(*models.Label))
|
|
}
|
|
}
|
|
|
|
var milestoneID int64
|
|
if pr.Milestone != "" {
|
|
milestone, ok := g.milestones.Load(pr.Milestone)
|
|
if ok {
|
|
milestoneID = milestone.(int64)
|
|
}
|
|
}
|
|
|
|
// download patch file
|
|
err := func() error {
|
|
resp, err := http.Get(pr.PatchURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
pullDir := filepath.Join(g.repo.RepoPath(), "pulls")
|
|
if err = os.MkdirAll(pullDir, os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
f, err := os.Create(filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(f, resp.Body)
|
|
return err
|
|
}()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// set head information
|
|
pullHead := filepath.Join(g.repo.RepoPath(), "refs", "pull", fmt.Sprintf("%d", pr.Number))
|
|
if err := os.MkdirAll(pullHead, os.ModePerm); err != nil {
|
|
return nil, err
|
|
}
|
|
p, err := os.Create(filepath.Join(pullHead, "head"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = p.WriteString(pr.Head.SHA)
|
|
p.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var head = "unknown repository"
|
|
if pr.IsForkPullRequest() && pr.State != "closed" {
|
|
if pr.Head.OwnerName != "" {
|
|
remote := pr.Head.OwnerName
|
|
_, ok := g.prHeadCache[remote]
|
|
if !ok {
|
|
// git remote add
|
|
err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
|
|
if err != nil {
|
|
log.Error("AddRemote failed: %s", err)
|
|
} else {
|
|
g.prHeadCache[remote] = struct{}{}
|
|
ok = true
|
|
}
|
|
}
|
|
|
|
if ok {
|
|
_, err = git.NewCommand("fetch", remote, pr.Head.Ref).RunInDir(g.repo.RepoPath())
|
|
if err != nil {
|
|
log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
|
|
} else {
|
|
headBranch := filepath.Join(g.repo.RepoPath(), "refs", "heads", pr.Head.OwnerName, pr.Head.Ref)
|
|
if err := os.MkdirAll(filepath.Dir(headBranch), os.ModePerm); err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := os.Create(headBranch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = b.WriteString(pr.Head.SHA)
|
|
b.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
head = pr.Head.OwnerName + "/" + pr.Head.Ref
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
head = pr.Head.Ref
|
|
}
|
|
|
|
var issue = models.Issue{
|
|
RepoID: g.repo.ID,
|
|
Repo: g.repo,
|
|
Title: pr.Title,
|
|
Index: pr.Number,
|
|
Content: pr.Content,
|
|
MilestoneID: milestoneID,
|
|
IsPull: true,
|
|
IsClosed: pr.State == "closed",
|
|
IsLocked: pr.IsLocked,
|
|
Labels: labels,
|
|
CreatedUnix: timeutil.TimeStamp(pr.Created.Unix()),
|
|
}
|
|
|
|
userid, ok := g.userMap[pr.PosterID]
|
|
if !ok {
|
|
var err error
|
|
userid, err = models.GetUserIDByExternalUserID("github", fmt.Sprintf("%v", pr.PosterID))
|
|
if err != nil {
|
|
log.Error("GetUserIDByExternalUserID: %v", err)
|
|
}
|
|
if userid > 0 {
|
|
g.userMap[pr.PosterID] = userid
|
|
}
|
|
}
|
|
|
|
if userid > 0 {
|
|
issue.PosterID = userid
|
|
} else {
|
|
issue.PosterID = g.doer.ID
|
|
issue.OriginalAuthor = pr.PosterName
|
|
issue.OriginalAuthorID = pr.PosterID
|
|
}
|
|
|
|
var pullRequest = models.PullRequest{
|
|
HeadRepoID: g.repo.ID,
|
|
HeadBranch: head,
|
|
BaseRepoID: g.repo.ID,
|
|
BaseBranch: pr.Base.Ref,
|
|
MergeBase: pr.Base.SHA,
|
|
Index: pr.Number,
|
|
HasMerged: pr.Merged,
|
|
|
|
Issue: &issue,
|
|
}
|
|
|
|
if pullRequest.Issue.IsClosed && pr.Closed != nil {
|
|
pullRequest.Issue.ClosedUnix = timeutil.TimeStamp(pr.Closed.Unix())
|
|
}
|
|
if pullRequest.HasMerged && pr.MergedTime != nil {
|
|
pullRequest.MergedUnix = timeutil.TimeStamp(pr.MergedTime.Unix())
|
|
pullRequest.MergedCommitID = pr.MergeCommitSHA
|
|
pullRequest.MergerID = g.doer.ID
|
|
}
|
|
|
|
// TODO: reactions
|
|
// TODO: assignees
|
|
|
|
return &pullRequest, nil
|
|
}
|
|
|
|
// Rollback when migrating failed, this will rollback all the changes.
|
|
func (g *GiteaLocalUploader) Rollback() error {
|
|
if g.repo != nil && g.repo.ID > 0 {
|
|
if err := models.DeleteRepository(g.doer, g.repo.OwnerID, g.repo.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|