Add support for the Friendly Forge Format (F3) #2

Closed
earl-warren wants to merge 1 commits from gitea-feature-f3 into main
27 changed files with 2782 additions and 6 deletions

View File

@ -98,6 +98,9 @@ issues:
- gosec
- unparam
- staticcheck
- path: services/f3/driver/driver.go
linters:
- typecheck
- path: models/migrations/v
linters:
- gocyclo

108
cmd/f3.go Normal file
View File

@ -0,0 +1,108 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"context"
"fmt"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/f3/util"
"lab.forgefriends.org/friendlyforgeformat/gof3"
f3_format "lab.forgefriends.org/friendlyforgeformat/gof3/format"
"github.com/urfave/cli"
)
var CmdF3 = cli.Command{
Name: "f3",
Usage: "Friendly Forge Format (F3) format export/import.",
Description: "Import or export a repository from or to the Friendly Forge Format (F3) format.",
Action: runF3,
Flags: []cli.Flag{
cli.StringFlag{
Name: "directory",
Value: "./f3",
Usage: "Path of the directory where the F3 dump is stored",
},
cli.StringFlag{
Name: "user",
Value: "",
Usage: "The name of the user who owns the repository",
},
cli.StringFlag{
Name: "repository",
Value: "",
Usage: "The name of the repository",
},
cli.BoolFlag{
Name: "no-pull-request",
Usage: "Do not dump pull requests",
},
cli.BoolFlag{
Name: "import",
Usage: "Import from the directory",
},
cli.BoolFlag{
Name: "export",
Usage: "Export to the directory",
},
},
}
func runF3(ctx *cli.Context) error {
stdCtx, cancel := installSignals()
defer cancel()
if err := initDB(stdCtx); err != nil {
return err
}
if err := git.InitSimple(stdCtx); err != nil {
return err
}
return RunF3(stdCtx, ctx)
}
func RunF3(stdCtx context.Context, ctx *cli.Context) error {
doer, err := user_model.GetAdminUser()
if err != nil {
return err
}
features := gof3.AllFeatures
if ctx.Bool("no-pull-request") {
features.PullRequests = false
}
gitea := util.GiteaForgeRoot(stdCtx, features, doer)
f3 := util.F3ForgeRoot(stdCtx, features, ctx.String("directory"))
if ctx.Bool("export") {
gitea.Forge.Users.List()
user := gitea.Forge.Users.GetFromFormat(&f3_format.User{UserName: ctx.String("user")})
if user.IsNil() {
return fmt.Errorf("%s is not a known user", ctx.String("user"))
}
user.Projects.List()
project := user.Projects.GetFromFormat(&f3_format.Project{Name: ctx.String("repository")})
if project.IsNil() {
return fmt.Errorf("%s/%s is not a known repository", ctx.String("user"), ctx.String("repository"))
}
f3.Forge.Mirror(gitea.Forge, user, project)
fmt.Println("exported")
} else if ctx.Bool("import") {
gitea.Forge.Mirror(f3.Forge)
fmt.Println("imported")
} else {
return fmt.Errorf("either --import or --export must be specified")
}
return nil
}

View File

@ -2294,6 +2294,15 @@ ROUTER = console
;; If a domain is allowed by ALLOWED_DOMAINS, this option will be ignored.
;ALLOW_LOCALNETWORKS = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[F3]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Enable/Disable Friendly Forge Format (F3)
;ENABLED = true
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[federation]

6
go.mod
View File

@ -4,7 +4,7 @@ go 1.18
require (
code.gitea.io/gitea-vet v0.2.2-0.20220122151748-48ebc902541b
code.gitea.io/sdk/gitea v0.15.1
code.gitea.io/sdk/gitea v0.15.1-0.20220915214501-aef4e5e2bd47
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb
gitea.com/go-chi/cache v0.2.0
@ -104,6 +104,7 @@ require (
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
lab.forgefriends.org/friendlyforgeformat/gof3 v0.0.0-20220928084330-fe2f884e84a0
mvdan.cc/xurls/v2 v2.4.0
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
xorm.io/builder v0.3.11
@ -159,6 +160,7 @@ require (
github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
@ -194,6 +196,7 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
@ -284,6 +287,7 @@ require (
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/exp v0.0.0-20220516143420-24438e51023a // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
google.golang.org/appengine v1.6.7 // indirect

14
go.sum
View File

@ -64,12 +64,11 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
code.gitea.io/gitea-vet v0.2.2-0.20220122151748-48ebc902541b h1:uv9a8eGSdQ8Dr4HyUcuHFfDsk/QuwO+wf+Y99RYdxY0=
code.gitea.io/gitea-vet v0.2.2-0.20220122151748-48ebc902541b/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
code.gitea.io/sdk/gitea v0.11.3/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY=
code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M=
code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA=
code.gitea.io/sdk/gitea v0.15.1-0.20220915214501-aef4e5e2bd47 h1:e9J9QdBk4NbdskZyGmJcaUGC/agG0UHMGyMrjgPb+6Q=
code.gitea.io/sdk/gitea v0.15.1-0.20220915214501-aef4e5e2bd47/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE=
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY=
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA=
@ -377,6 +376,8 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
@ -489,6 +490,7 @@ github.com/go-enry/go-enry/v2 v2.8.2 h1:uiGmC+3K8sVd/6DOe2AOJEOihJdqda83nPyJNtMR
github.com/go-enry/go-enry/v2 v2.8.2/go.mod h1:GVzIiAytiS5uT/QiuakK7TF1u4xDab87Y8V5EJRpsIQ=
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8=
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
@ -1600,12 +1602,14 @@ golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
@ -1621,6 +1625,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/exp v0.0.0-20220516143420-24438e51023a h1:tiLLxEjKNE6Hrah/Dp/cyHvsyjDLcMFSocOHO5XDmOM=
golang.org/x/exp v0.0.0-20220516143420-24438e51023a/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -2254,6 +2260,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
lab.forgefriends.org/friendlyforgeformat/gof3 v0.0.0-20220928084330-fe2f884e84a0 h1:5Vns5nMBUjMAIkVrTKsZkJ9AH6UCeBj5An8yeMAxVa4=
lab.forgefriends.org/friendlyforgeformat/gof3 v0.0.0-20220928084330-fe2f884e84a0/go.mod h1:v2t/aa0w224NzBcx1mdzuxJSRWPcaaanQRhV43v7+yw=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=

View File

@ -75,6 +75,7 @@ arguments - which can alternatively be run by running the subcommand web.`
cmd.CmdDocs,
cmd.CmdDumpRepository,
cmd.CmdRestoreRepository,
cmd.CmdF3,
}
// Now adjust these commands to add our global configuration options

View File

@ -223,6 +223,21 @@ func GetRepoTopicByName(ctx context.Context, repoID int64, topicName string) (*T
return nil, err
}
// GetRepoTopicByID retrieves topic from ID for a repo if it exist
func GetRepoTopicByID(ctx context.Context, repoID, topicID int64) (*Topic, error) {
cond := builder.NewCond()
var topic Topic
cond = cond.And(builder.Eq{"repo_topic.repo_id": repoID}).And(builder.Eq{"topic.id": topicID})
sess := db.GetEngine(ctx).Table("topic").Where(cond)
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
if has, err := sess.Select("topic.*").Get(&topic); err != nil {
return nil, err
} else if !has {
return nil, ErrTopicNotExist{""}
}
return &topic, nil
}
// AddTopic adds a topic name to a repository (if it does not already have it)
func AddTopic(repoID int64, topicName string) (*Topic, error) {
ctx, committer, err := db.TxContext()

View File

@ -101,7 +101,7 @@ func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, d
Path: comment.TreePath,
CommitID: comment.CommitSHA,
OrigCommitID: comment.OldRef,
DiffHunk: patch2diff(comment.Patch),
DiffHunk: Patch2diff(comment.Patch),
HTMLURL: comment.HTMLURL(),
HTMLPullURL: review.Issue.HTMLURL(),
}
@ -118,7 +118,7 @@ func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, d
return apiComments, nil
}
func patch2diff(patch string) string {
func Patch2diff(patch string) string {
split := strings.Split(patch, "\n@@")
if len(split) == 2 {
return "@@" + split[1]

24
modules/setting/f3.go Normal file
View File

@ -0,0 +1,24 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package setting
import (
"code.gitea.io/gitea/modules/log"
)
// Friendly Forge Format (F3) settings
var (
F3 = struct {
Enabled bool
}{
Enabled: true,
}
)
func newF3Service() {
if err := Cfg.Section("F3").MapTo(&F3); err != nil {
log.Fatal("Failed to map F3 settings: %v", err)
}
}

View File

@ -1299,6 +1299,7 @@ func NewServices() {
newProject()
newMimeTypeMap()
newFederationService()
newF3Service()
}
// NewServicesForInstall initializes the services for install

158
services/f3/driver/asset.go Normal file
View File

@ -0,0 +1,158 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"io"
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/services/attachment"
"github.com/google/uuid"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Asset struct {
repo_model.Attachment
DownloadFunc func() io.ReadCloser
}
func AssetConverter(f *repo_model.Attachment) *Asset {
return &Asset{
Attachment: *f,
}
}
func (o Asset) GetID() int64 {
return o.ID
}
func (o *Asset) SetID(id int64) {
o.ID = id
}
func (o *Asset) IsNil() bool {
return o.ID == 0
}
func (o *Asset) Equals(other *Asset) bool {
return o.Name == other.Name
}
func (o *Asset) ToFormat() *format.ReleaseAsset {
return &format.ReleaseAsset{
Common: format.Common{Index: o.ID},
Name: o.Name,
Size: int(o.Size),
DownloadCount: int(o.DownloadCount),
Created: o.CreatedUnix.AsTime(),
DownloadURL: o.DownloadURL(),
DownloadFunc: o.DownloadFunc,
}
}
func (o *Asset) FromFormat(asset *format.ReleaseAsset) {
*o = Asset{
Attachment: repo_model.Attachment{
ID: asset.GetID(),
Name: asset.Name,
Size: int64(asset.Size),
DownloadCount: int64(asset.DownloadCount),
CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()),
},
DownloadFunc: asset.DownloadFunc,
}
}
type AssetProvider struct {
g *Gitea
}
func (o *AssetProvider) ToFormat(asset *Asset) *format.ReleaseAsset {
httpClient := o.g.GetNewMigrationHTTPClient()()
a := asset.ToFormat()
a.DownloadFunc = func() io.ReadCloser {
o.g.GetLogger().Debug("download from %s", asset.DownloadURL())
req, err := http.NewRequest("GET", asset.DownloadURL(), nil)
if err != nil {
panic(err)
}
resp, err := httpClient.Do(req)
if err != nil {
panic(fmt.Errorf("while downloading %s %w", asset.DownloadURL(), err))
}
// resp.Body is closed by the consumer
return resp.Body
}
return a
}
func (o *AssetProvider) FromFormat(p *format.ReleaseAsset) *Asset {
var asset Asset
asset.FromFormat(p)
return &asset
}
func (o *AssetProvider) ProcessObject(user *User, project *Project, release *Release, asset *Asset) {
}
func (o *AssetProvider) GetObjects(user *User, project *Project, release *Release, page int) []*Asset {
if page > 1 {
return []*Asset{}
}
r, err := repo_model.GetReleaseByID(o.g.ctx, release.GetID())
if err != nil {
panic(err)
}
if err := r.LoadAttributes(); err != nil {
panic(fmt.Errorf("error while listing assets: %v", err))
}
return util.ConvertMap[*repo_model.Attachment, *Asset](r.Attachments, AssetConverter)
}
func (o *AssetProvider) Get(user *User, project *Project, release *Release, exemplar *Asset) *Asset {
id := exemplar.GetID()
asset, err := repo_model.GetAttachmentByID(o.g.ctx, id)
if repo_model.IsErrAttachmentNotExist(err) {
return &Asset{}
}
if err != nil {
panic(err)
}
return AssetConverter(asset)
}
func (o *AssetProvider) Put(user *User, project *Project, release *Release, asset *Asset) *Asset {
asset.ID = 0
asset.UploaderID = user.GetID()
asset.RepoID = project.GetID()
asset.ReleaseID = release.GetID()
asset.UUID = uuid.New().String()
download := asset.DownloadFunc()
defer download.Close()
a, err := attachment.NewAttachment(&asset.Attachment, download)
if err != nil {
panic(err)
}
return o.Get(user, project, release, AssetConverter(a))
}
func (o *AssetProvider) Delete(user *User, project *Project, release *Release, asset *Asset) *Asset {
a := o.Get(user, project, release, asset)
if !a.IsNil() {
err := repo_model.DeleteAttachment(&a.Attachment, true)
if err != nil {
panic(err)
}
}
return a
}

View File

@ -0,0 +1,155 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
comment_service "code.gitea.io/gitea/services/comments"
"lab.forgefriends.org/friendlyforgeformat/gof3/forges/common"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Comment struct {
issues_model.Comment
}
func CommentConverter(f *issues_model.Comment) *Comment {
return &Comment{
Comment: *f,
}
}
func (o Comment) GetID() int64 {
return o.Comment.ID
}
func (o *Comment) SetID(id int64) {
o.Comment.ID = id
}
func (o *Comment) IsNil() bool {
return o.ID == 0
}
func (o *Comment) Equals(other *Comment) bool {
return o.Comment.ID == other.Comment.ID
}
func (o *Comment) ToFormat() *format.Comment {
return &format.Comment{
Common: format.Common{Index: o.Comment.ID},
IssueIndex: o.Comment.IssueID,
PosterID: o.Comment.PosterID,
PosterName: o.Comment.Poster.Name,
PosterEmail: o.Comment.Poster.Email,
Content: o.Comment.Content,
Created: o.Comment.CreatedUnix.AsTime(),
Updated: o.Comment.UpdatedUnix.AsTime(),
}
}
func (o *Comment) FromFormat(comment *format.Comment) {
*o = Comment{
Comment: issues_model.Comment{
ID: comment.Index,
PosterID: comment.PosterID,
Poster: &user_model.User{
ID: comment.PosterID,
Name: comment.PosterName,
Email: comment.PosterEmail,
},
Content: comment.Content,
CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()),
UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()),
},
}
}
type CommentProvider struct {
g *Gitea
}
func (o *CommentProvider) ToFormat(comment *Comment) *format.Comment {
return comment.ToFormat()
}
func (o *CommentProvider) FromFormat(f *format.Comment) *Comment {
var comment Comment
comment.FromFormat(f)
return &comment
}
func (o *CommentProvider) GetObjects(user *User, project *Project, commentable common.ContainerObjectInterface, page int) []*Comment {
comments, err := issues_model.FindComments(o.g.ctx, &issues_model.FindCommentsOptions{
ListOptions: db.ListOptions{Page: page, PageSize: o.g.perPage},
RepoID: project.GetID(),
IssueID: commentable.GetID(),
Type: issues_model.CommentTypeComment,
})
if err != nil {
panic(fmt.Errorf("error while listing comment: %v", err))
}
return util.ConvertMap[*issues_model.Comment, *Comment](comments, CommentConverter)
}
func (o *CommentProvider) ProcessObject(user *User, project *Project, commentable common.ContainerObjectInterface, comment *Comment) {
if err := comment.LoadIssue(); err != nil {
panic(err)
}
if err := comment.LoadPoster(); err != nil {
panic(err)
}
}
func (o *CommentProvider) Get(user *User, project *Project, commentable common.ContainerObjectInterface, comment *Comment) *Comment {
id := comment.GetID()
c, err := issues_model.GetCommentByID(o.g.ctx, id)
if issues_model.IsErrCommentNotExist(err) {
return &Comment{}
}
if err != nil {
panic(err)
}
co := CommentConverter(c)
o.ProcessObject(user, project, commentable, co)
return co
}
func (o *CommentProvider) Put(user *User, project *Project, commentable common.ContainerObjectInterface, comment *Comment) *Comment {
var issue *issues_model.Issue
switch c := commentable.(type) {
case *PullRequest:
issue = c.PullRequest.Issue
case *Issue:
issue = &c.Issue
default:
panic(fmt.Errorf("unexpected type %T", commentable))
}
c, err := comment_service.CreateIssueComment(o.g.GetDoer(), &project.Repository, issue, comment.Content, nil)
if err != nil {
panic(err)
}
return o.Get(user, project, commentable, CommentConverter(c))
}
func (o *CommentProvider) Delete(user *User, project *Project, commentable common.ContainerObjectInterface, comment *Comment) *Comment {
c := o.Get(user, project, commentable, comment)
if !c.IsNil() {
err := issues_model.DeleteComment(o.g.ctx, &c.Comment)
if err != nil {
panic(err)
}
}
return c
}

View File

@ -0,0 +1,118 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"context"
"fmt"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/migrations"
"lab.forgefriends.org/friendlyforgeformat/gof3"
"lab.forgefriends.org/friendlyforgeformat/gof3/forges/common"
"lab.forgefriends.org/friendlyforgeformat/gof3/forges/driver"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
)
type Options struct {
gof3.Options
Doer *user_model.User
}
type Gitea struct {
perPage int
ctx context.Context
options *Options
}
func (o *Gitea) GetPerPage() int {
return o.perPage
}
func (o *Gitea) GetOptions() gof3.OptionsInterface {
return o.options
}
func (o *Gitea) SetOptions(options gof3.OptionsInterface) {
var ok bool
o.options, ok = options.(*Options)
if !ok {
panic(fmt.Errorf("unexpected type %T", options))
}
}
func (o *Gitea) GetLogger() *gof3.Logger {
return o.GetOptions().GetLogger()
}
func (o *Gitea) Init(options gof3.OptionsInterface) {
o.SetOptions(options)
o.perPage = setting.ItemsPerPage
}
func (o *Gitea) GetDirectory() string {
return o.options.GetDirectory()
}
func (o *Gitea) GetDoer() *user_model.User {
return o.options.Doer
}
func (o *Gitea) GetNewMigrationHTTPClient() gof3.NewMigrationHTTPClientFun {
return migrations.NewMigrationHTTPClient
}
func (o *Gitea) SupportGetRepoComments() bool {
return false
}
func (o *Gitea) SetContext(ctx context.Context) {
o.ctx = ctx
}
func (o *Gitea) GetProvider(name string, parent common.ProviderInterface) common.ProviderInterface {
var parentImpl any
if parent != nil {
parentImpl = parent.GetImplementation()
}
switch name {
case driver.ProviderUser:
return &driver.Provider[UserProvider, *UserProvider, User, *User, format.User, *format.User]{Impl: &UserProvider{g: o}}
case driver.ProviderProject:
return &driver.ProviderWithParentOne[ProjectProvider, *ProjectProvider, Project, *Project, format.Project, *format.Project, User, *User]{Impl: &ProjectProvider{g: o}}
case driver.ProviderMilestone:
return &driver.ProviderWithParentOneTwo[MilestoneProvider, *MilestoneProvider, Milestone, *Milestone, format.Milestone, *format.Milestone, User, *User, Project, *Project]{Impl: &MilestoneProvider{g: o, project: parentImpl.(*ProjectProvider)}}
case driver.ProviderIssue:
return &driver.ProviderWithParentOneTwo[IssueProvider, *IssueProvider, Issue, *Issue, format.Issue, *format.Issue, User, *User, Project, *Project]{Impl: &IssueProvider{g: o, project: parentImpl.(*ProjectProvider)}}
case driver.ProviderPullRequest:
return &driver.ProviderWithParentOneTwo[PullRequestProvider, *PullRequestProvider, PullRequest, *PullRequest, format.PullRequest, *format.PullRequest, User, *User, Project, *Project]{Impl: &PullRequestProvider{g: o, project: parentImpl.(*ProjectProvider)}}
case driver.ProviderReview:
return &driver.ProviderWithParentOneTwoThree[ReviewProvider, *ReviewProvider, Review, *Review, format.Review, *format.Review, User, *User, Project, *Project, PullRequest, *PullRequest]{Impl: &ReviewProvider{g: o}}
case driver.ProviderRepository:
return &driver.ProviderWithParentOneTwo[RepositoryProvider, *RepositoryProvider, Repository, *Repository, format.Repository, *format.Repository, User, *User, Project, *Project]{Impl: &RepositoryProvider{g: o}}
case driver.ProviderTopic:
return &driver.ProviderWithParentOneTwo[TopicProvider, *TopicProvider, Topic, *Topic, format.Topic, *format.Topic, User, *User, Project, *Project]{Impl: &TopicProvider{g: o}}
case driver.ProviderLabel:
return &driver.ProviderWithParentOneTwo[LabelProvider, *LabelProvider, Label, *Label, format.Label, *format.Label, User, *User, Project, *Project]{Impl: &LabelProvider{g: o, project: parentImpl.(*ProjectProvider)}}
case driver.ProviderRelease:
return &driver.ProviderWithParentOneTwo[ReleaseProvider, *ReleaseProvider, Release, *Release, format.Release, *format.Release, User, *User, Project, *Project]{Impl: &ReleaseProvider{g: o}}
case driver.ProviderAsset:
return &driver.ProviderWithParentOneTwoThree[AssetProvider, *AssetProvider, Asset, *Asset, format.ReleaseAsset, *format.ReleaseAsset, User, *User, Project, *Project, Release, *Release]{Impl: &AssetProvider{g: o}}
case driver.ProviderComment:
return &driver.ProviderWithParentOneTwoThreeInterface[CommentProvider, *CommentProvider, Comment, *Comment, format.Comment, *format.Comment, User, *User, Project, *Project]{Impl: &CommentProvider{g: o}}
case driver.ProviderCommentReaction:
return &driver.ProviderWithParentOneTwoRest[ReactionProvider, *ReactionProvider, Reaction, *Reaction, format.Reaction, *format.Reaction, User, *User, Project, *Project]{Impl: &ReactionProvider{g: o}}
case driver.ProviderIssueReaction:
return &driver.ProviderWithParentOneTwoRest[ReactionProvider, *ReactionProvider, Reaction, *Reaction, format.Reaction, *format.Reaction, User, *User, Project, *Project]{Impl: &ReactionProvider{g: o}}
default:
panic(fmt.Sprintf("unknown provider name %s", name))
}
}
func (o Gitea) Finish() {
}

200
services/f3/driver/issue.go Normal file
View File

@ -0,0 +1,200 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/timeutil"
issue_service "code.gitea.io/gitea/services/issue"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Issue struct {
issues_model.Issue
}
func IssueConverter(f *issues_model.Issue) *Issue {
return &Issue{
Issue: *f,
}
}
func (o Issue) GetID() int64 {
return o.Index
}
func (o *Issue) SetID(id int64) {
o.Index = id
}
func (o *Issue) IsNil() bool {
return o.Index == 0
}
func (o *Issue) Equals(other *Issue) bool {
return o.Index == other.Index
}
func (o *Issue) ToFormat() *format.Issue {
var milestone string
if o.Milestone != nil {
milestone = o.Milestone.Name
}
labels := make([]string, 0, len(o.Labels))
for _, label := range o.Labels {
labels = append(labels, label.Name)
}
var assignees []string
for i := range o.Assignees {
assignees = append(assignees, o.Assignees[i].Name)
}
return &format.Issue{
Common: format.Common{Index: o.Index},
Title: o.Title,
PosterID: o.Poster.ID,
PosterName: o.Poster.Name,
PosterEmail: o.Poster.Email,
Content: o.Content,
Milestone: milestone,
State: string(o.State()),
Created: o.CreatedUnix.AsTime(),
Updated: o.UpdatedUnix.AsTime(),
Closed: o.ClosedUnix.AsTimePtr(),
IsLocked: o.IsLocked,
Labels: labels,
Assignees: assignees,
}
}
func (o *Issue) FromFormat(issue *format.Issue) {
labels := make([]*issues_model.Label, 0, len(issue.Labels))
for _, label := range issue.Labels {
labels = append(labels, &issues_model.Label{Name: label})
}
assignees := make([]*user_model.User, 0, len(issue.Assignees))
for _, a := range issue.Assignees {
assignees = append(assignees, &user_model.User{
Name: a,
})
}
*o = Issue{
Issue: issues_model.Issue{
Title: issue.Title,
Index: issue.Index,
Poster: &user_model.User{
ID: issue.PosterID,
Name: issue.PosterName,
Email: issue.PosterEmail,
},
Content: issue.Content,
Milestone: &issues_model.Milestone{
Name: issue.Milestone,
},
IsClosed: issue.State == "closed",
CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()),
UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()),
ClosedUnix: timeutil.TimeStamp(issue.Closed.Unix()),
IsLocked: issue.IsLocked,
Labels: labels,
Assignees: assignees,
},
}
}
type IssueProvider struct {
g *Gitea
project *ProjectProvider
}
func (o *IssueProvider) ToFormat(issue *Issue) *format.Issue {
return issue.ToFormat()
}
func (o *IssueProvider) FromFormat(i *format.Issue) *Issue {
var issue Issue
issue.FromFormat(i)
if i.Milestone != "" {
issue.Milestone.ID = o.project.milestones.GetID(issue.Milestone.Name)
}
for _, label := range issue.Labels {
label.ID = o.project.labels.GetID(label.Name)
}
return &issue
}
func (o *IssueProvider) GetObjects(user *User, project *Project, page int) []*Issue {
issues, err := issues_model.Issues(&issues_model.IssuesOptions{
ListOptions: db.ListOptions{Page: page, PageSize: o.g.perPage},
RepoID: project.GetID(),
})
if err != nil {
panic(fmt.Errorf("error while listing issues: %v", err))
}
return util.ConvertMap[*issues_model.Issue, *Issue](issues, IssueConverter)
}
func (o *IssueProvider) ProcessObject(user *User, project *Project, issue *Issue) {
if err := (&issue.Issue).LoadAttributes(o.g.ctx); err != nil {
panic(true)
}
}
func (o *IssueProvider) Get(user *User, project *Project, exemplar *Issue) *Issue {
id := exemplar.GetID()
issue, err := issues_model.GetIssueByIndex(project.GetID(), id)
if issues_model.IsErrIssueNotExist(err) {
return &Issue{}
}
if err != nil {
panic(err)
}
i := IssueConverter(issue)
o.ProcessObject(user, project, i)
return i
}
func (o *IssueProvider) Put(user *User, project *Project, issue *Issue) *Issue {
i := issue.Issue
i.RepoID = project.GetID()
labels := make([]int64, 0, len(i.Labels))
for _, label := range i.Labels {
labels = append(labels, label.ID)
}
if err := issues_model.NewIssue(&project.Repository, &i, labels, []string{}); err != nil {
panic(err)
}
return o.Get(user, project, IssueConverter(&i))
}
func (o *IssueProvider) Delete(user *User, project *Project, issue *Issue) *Issue {
m := o.Get(user, project, issue)
if !m.IsNil() {
repoPath := repo_model.RepoPath(user.Name, project.Name)
gitRepo, err := git.OpenRepository(o.g.ctx, repoPath)
if err != nil {
panic(err)
}
defer gitRepo.Close()
if err := issue_service.DeleteIssue(o.g.GetDoer(), gitRepo, &issue.Issue); err != nil {
panic(err)
}
}
return m
}

127
services/f3/driver/label.go Normal file
View File

@ -0,0 +1,127 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Label struct {
issues_model.Label
}
func LabelConverter(f *issues_model.Label) *Label {
return &Label{
Label: *f,
}
}
func (o Label) GetID() int64 {
return o.ID
}
func (o Label) GetName() string {
return o.Name
}
func (o *Label) SetID(id int64) {
o.ID = id
}
func (o *Label) IsNil() bool {
return o.ID == 0
}
func (o *Label) Equals(other *Label) bool {
return o.Name == other.Name
}
func (o *Label) ToFormat() *format.Label {
return &format.Label{
Common: format.Common{Index: o.ID},
Name: o.Name,
Color: o.Color,
Description: o.Description,
}
}
func (o *Label) FromFormat(label *format.Label) {
*o = Label{
Label: issues_model.Label{
ID: label.Index,
Name: label.Name,
Description: label.Description,
Color: label.Color,
},
}
}
type LabelProvider struct {
g *Gitea
project *ProjectProvider
}
func (o *LabelProvider) ToFormat(label *Label) *format.Label {
return label.ToFormat()
}
func (o *LabelProvider) FromFormat(m *format.Label) *Label {
var label Label
label.FromFormat(m)
return &label
}
func (o *LabelProvider) GetObjects(user *User, project *Project, page int) []*Label {
labels, err := issues_model.GetLabelsByRepoID(o.g.ctx, project.GetID(), "", db.ListOptions{Page: page, PageSize: o.g.perPage})
if err != nil {
panic(fmt.Errorf("error while listing labels: %v", err))
}
r := util.ConvertMap[*issues_model.Label, *Label](labels, LabelConverter)
if o.project != nil {
o.project.labels = util.NewNameIDMap[*Label](r)
}
return r
}
func (o *LabelProvider) ProcessObject(user *User, project *Project, label *Label) {
}
func (o *LabelProvider) Get(user *User, project *Project, exemplar *Label) *Label {
id := exemplar.GetID()
label, err := issues_model.GetLabelInRepoByID(o.g.ctx, project.GetID(), id)
if issues_model.IsErrRepoLabelNotExist(err) {
return &Label{}
}
if err != nil {
panic(err)
}
return LabelConverter(label)
}
func (o *LabelProvider) Put(user *User, project *Project, label *Label) *Label {
l := label.Label
l.RepoID = project.GetID()
if err := issues_model.NewLabel(o.g.ctx, &l); err != nil {
panic(err)
}
return o.Get(user, project, LabelConverter(&l))
}
func (o *LabelProvider) Delete(user *User, project *Project, label *Label) *Label {
l := o.Get(user, project, label)
if !l.IsNil() {
if err := issues_model.DeleteLabel(project.GetID(), l.GetID()); err != nil {
panic(err)
}
}
return l
}

View File

@ -0,0 +1,174 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Milestone struct {
issues_model.Milestone
}
func MilestoneConverter(f *issues_model.Milestone) *Milestone {
return &Milestone{
Milestone: *f,
}
}
func (o Milestone) GetID() int64 {
return o.ID
}
func (o Milestone) GetName() string {
return o.Name
}
func (o *Milestone) SetID(id int64) {
o.ID = id
}
func (o *Milestone) IsNil() bool {
return o.ID == 0
}
func (o *Milestone) Equals(other *Milestone) bool {
return o.Name == other.Name
}
func (o *Milestone) ToFormat() *format.Milestone {
milestone := &format.Milestone{
Common: format.Common{Index: o.ID},
Title: o.Name,
Description: o.Content,
Created: o.CreatedUnix.AsTime(),
Updated: o.UpdatedUnix.AsTimePtr(),
State: string(o.State()),
}
if o.IsClosed {
milestone.Closed = o.ClosedDateUnix.AsTimePtr()
}
if o.DeadlineUnix.Year() < 9999 {
milestone.Deadline = o.DeadlineUnix.AsTimePtr()
}
return milestone
}
func (o *Milestone) FromFormat(milestone *format.Milestone) {
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 closed timeutil.TimeStamp
if milestone.Closed != nil {
closed = timeutil.TimeStamp(milestone.Closed.Unix())
}
if milestone.Created.IsZero() {
if milestone.Updated != nil {
milestone.Created = *milestone.Updated
} else if milestone.Deadline != nil {
milestone.Created = *milestone.Deadline
} else {
milestone.Created = time.Now()
}
}
if milestone.Updated == nil || milestone.Updated.IsZero() {
milestone.Updated = &milestone.Created
}
*o = Milestone{
issues_model.Milestone{
ID: milestone.Index,
Name: milestone.Title,
Content: milestone.Description,
IsClosed: milestone.State == "closed",
CreatedUnix: timeutil.TimeStamp(milestone.Created.Unix()),
UpdatedUnix: timeutil.TimeStamp(milestone.Updated.Unix()),
ClosedDateUnix: closed,
DeadlineUnix: deadline,
},
}
}
type MilestoneProvider struct {
g *Gitea
project *ProjectProvider
}
func (o *MilestoneProvider) ToFormat(milestone *Milestone) *format.Milestone {
return milestone.ToFormat()
}
func (o *MilestoneProvider) FromFormat(m *format.Milestone) *Milestone {
var milestone Milestone
milestone.FromFormat(m)
return &milestone
}
func (o *MilestoneProvider) GetObjects(user *User, project *Project, page int) []*Milestone {
milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
ListOptions: db.ListOptions{Page: page, PageSize: o.g.perPage},
RepoID: project.GetID(),
State: api.StateAll,
})
if err != nil {
panic(fmt.Errorf("error while listing milestones: %v", err))
}
r := util.ConvertMap[*issues_model.Milestone, *Milestone](([]*issues_model.Milestone)(milestones), MilestoneConverter)
if o.project != nil {
o.project.milestones = util.NewNameIDMap[*Milestone](r)
}
return r
}
func (o *MilestoneProvider) ProcessObject(user *User, project *Project, milestone *Milestone) {
}
func (o *MilestoneProvider) Get(user *User, project *Project, exemplar *Milestone) *Milestone {
id := exemplar.GetID()
milestone, err := issues_model.GetMilestoneByRepoID(o.g.ctx, project.GetID(), id)
if issues_model.IsErrMilestoneNotExist(err) {
return &Milestone{}
}
if err != nil {
panic(err)
}
return MilestoneConverter(milestone)
}
func (o *MilestoneProvider) Put(user *User, project *Project, milestone *Milestone) *Milestone {
m := milestone.Milestone
m.RepoID = project.GetID()
if err := issues_model.NewMilestone(&m); err != nil {
panic(err)
}
return o.Get(user, project, MilestoneConverter(&m))
}
func (o *MilestoneProvider) Delete(user *User, project *Project, milestone *Milestone) *Milestone {
m := o.Get(user, project, milestone)
if !m.IsNil() {
if err := issues_model.DeleteMilestoneByRepoID(project.GetID(), m.GetID()); err != nil {
panic(err)
}
}
return m
}

View File

@ -0,0 +1,157 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
repo_module "code.gitea.io/gitea/modules/repository"
repo_service "code.gitea.io/gitea/services/repository"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
f3_util "lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Project struct {
repo_model.Repository
}
func ProjectConverter(f *repo_model.Repository) *Project {
return &Project{
Repository: *f,
}
}
func (o Project) GetID() int64 {
return o.ID
}
func (o *Project) SetID(id int64) {
o.ID = id
}
func (o *Project) IsNil() bool {
return o.ID == 0
}
func (o *Project) Equals(other *Project) bool {
return (o.Name == other.Name)
}
func (o *Project) ToFormat() *format.Project {
return &format.Project{
Common: format.Common{Index: o.ID},
Name: o.Name,
Owner: o.Owner.Name,
IsPrivate: o.IsPrivate,
Description: o.Description,
CloneURL: repo_model.ComposeHTTPSCloneURL(o.Owner.Name, o.Name),
OriginalURL: o.OriginalURL,
DefaultBranch: o.DefaultBranch,
}
}
func (o *Project) FromFormat(project *format.Project) {
*o = Project{
Repository: repo_model.Repository{
ID: project.Index,
Name: project.Name,
Owner: &user_model.User{
Name: project.Owner,
},
IsPrivate: project.IsPrivate,
Description: project.Description,
OriginalURL: project.OriginalURL,
DefaultBranch: project.DefaultBranch,
},
}
}
type ProjectProvider struct {
g *Gitea
milestones f3_util.NameIDMap
labels f3_util.NameIDMap
}
func (o *ProjectProvider) ToFormat(project *Project) *format.Project {
return project.ToFormat()
}
func (o *ProjectProvider) FromFormat(p *format.Project) *Project {
var project Project
project.FromFormat(p)
return &project
}
func (o *ProjectProvider) GetObjects(user *User, page int) []*Project {
repoList, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{Page: page, PageSize: o.g.perPage},
Actor: &user.User,
Private: true,
})
if err != nil {
panic(fmt.Errorf("error while listing projects: %T %v", err, err))
}
if err := repoList.LoadAttributes(); err != nil {
panic(nil)
}
return f3_util.ConvertMap[*repo_model.Repository, *Project](([]*repo_model.Repository)(repoList), ProjectConverter)
}
func (o *ProjectProvider) ProcessObject(user *User, project *Project) {
}
func (o *ProjectProvider) Get(user *User, exemplar *Project) *Project {
var project *repo_model.Repository
var err error
if exemplar.GetID() > 0 {
project, err = repo_model.GetRepositoryByIDCtx(o.g.ctx, exemplar.GetID())
} else if exemplar.Name != "" {
project, err = repo_model.GetRepositoryByName(user.GetID(), exemplar.Name)
} else {
panic("GetID() == 0 and ProjectName == \"\"")
}
if repo_model.IsErrRepoNotExist(err) {
return &Project{}
}
if err != nil {
panic(fmt.Errorf("project %v %w", exemplar, err))
}
if err := project.GetOwner(o.g.ctx); err != nil {
panic(err)
}
return ProjectConverter(project)
}
func (o *ProjectProvider) Put(user *User, project *Project) *Project {
repo, err := repo_module.CreateRepository(o.g.GetDoer(), &user.User, repo_module.CreateRepoOptions{
Name: project.Name,
Description: project.Description,
OriginalURL: project.OriginalURL,
IsPrivate: project.IsPrivate,
})
if err != nil {
panic(err)
}
return o.Get(user, ProjectConverter(repo))
}
func (o *ProjectProvider) Delete(user *User, project *Project) *Project {
if project.IsNil() {
return project
}
if project.ID > 0 {
project = o.Get(user, project)
}
if !project.IsNil() {
err := repo_service.DeleteRepository(o.g.ctx, o.g.GetDoer(), &project.Repository, true)
if err != nil {
panic(err)
}
}
return project
}

View File

@ -0,0 +1,310 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"context"
"fmt"
"time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
issue_service "code.gitea.io/gitea/services/issue"
f3_gitea "lab.forgefriends.org/friendlyforgeformat/gof3/forges/gitea"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type PullRequest struct {
issues_model.PullRequest
FetchFunc func(repository string) string
}
func PullRequestConverter(f *issues_model.PullRequest) *PullRequest {
return &PullRequest{
PullRequest: *f,
}
}
func (o PullRequest) GetID() int64 {
return o.Index
}
func (o *PullRequest) SetID(id int64) {
o.Index = id
}
func (o *PullRequest) IsNil() bool {
return o.Index == 0
}
func (o *PullRequest) Equals(other *PullRequest) bool {
return o.Issue.Title == other.Issue.Title
}
func (o PullRequest) IsForkPullRequest() bool {
return o.HeadRepoID != o.BaseRepoID
}
func (o *PullRequest) ToFormat() *format.PullRequest {
var milestone string
if o.Issue.Milestone != nil {
milestone = o.Issue.Milestone.Name
}
labels := make([]string, 0, len(o.Issue.Labels))
for _, label := range o.Issue.Labels {
labels = append(labels, label.Name)
}
var mergedTime *time.Time
if o.HasMerged {
mergedTime = o.MergedUnix.AsTimePtr()
}
getSHA := func(repo *repo_model.Repository, branch string) string {
r, err := git.OpenRepository(context.Background(), repo.RepoPath())
if err != nil {
panic(err)
}
defer r.Close()
b, err := r.GetBranch(branch)
if err != nil {
panic(err)
}
c, err := b.GetCommit()
if err != nil {
panic(err)
}
return c.ID.String()
}
head := format.PullRequestBranch{
CloneURL: o.HeadRepo.CloneLink().HTTPS,
Ref: o.HeadBranch,
SHA: getSHA(o.HeadRepo, o.HeadBranch),
RepoName: o.HeadRepo.Name,
OwnerName: o.HeadRepo.OwnerName,
}
base := format.PullRequestBranch{
CloneURL: o.BaseRepo.CloneLink().HTTPS,
Ref: o.BaseBranch,
SHA: getSHA(o.BaseRepo, o.BaseBranch),
RepoName: o.BaseRepo.Name,
OwnerName: o.BaseRepo.OwnerName,
}
return &format.PullRequest{
Common: format.Common{Index: o.Index},
PosterID: o.Issue.Poster.ID,
PosterName: o.Issue.Poster.Name,
PosterEmail: o.Issue.Poster.Email,
Title: o.Issue.Title,
Content: o.Issue.Content,
Milestone: milestone,
State: string(o.Issue.State()),
IsLocked: o.Issue.IsLocked,
Created: o.Issue.CreatedUnix.AsTime(),
Updated: o.Issue.UpdatedUnix.AsTime(),
Closed: o.Issue.ClosedUnix.AsTimePtr(),
Labels: labels,
PatchURL: o.Issue.PatchURL(),
Merged: o.HasMerged,
MergedTime: mergedTime,
MergeCommitSHA: o.MergedCommitID,
Head: head,
Base: base,
}
}
func (o *PullRequest) FromFormat(pullRequest *format.PullRequest) {
labels := make([]*issues_model.Label, 0, len(pullRequest.Labels))
for _, label := range pullRequest.Labels {
labels = append(labels, &issues_model.Label{Name: label})
}
if pullRequest.Created.IsZero() {
if pullRequest.Closed != nil {
pullRequest.Created = *pullRequest.Closed
} else if pullRequest.MergedTime != nil {
pullRequest.Created = *pullRequest.MergedTime
} else {
pullRequest.Created = time.Now()
}
}
if pullRequest.Updated.IsZero() {
pullRequest.Updated = pullRequest.Created
}
base, err := repo_model.GetRepositoryByOwnerAndName(pullRequest.Base.OwnerName, pullRequest.Base.RepoName)
if err != nil {
panic(err)
}
var head *repo_model.Repository
if pullRequest.Head.RepoName == "" {
head = base
} else {
head, err = repo_model.GetRepositoryByOwnerAndName(pullRequest.Head.OwnerName, pullRequest.Head.RepoName)
if err != nil {
panic(err)
}
}
issue := issues_model.Issue{
RepoID: base.ID,
Repo: base,
Title: pullRequest.Title,
Index: pullRequest.Index,
Content: pullRequest.Content,
IsPull: true,
IsClosed: pullRequest.State == "closed",
IsLocked: pullRequest.IsLocked,
Labels: labels,
CreatedUnix: timeutil.TimeStamp(pullRequest.Created.Unix()),
UpdatedUnix: timeutil.TimeStamp(pullRequest.Updated.Unix()),
}
pr := issues_model.PullRequest{
HeadRepoID: head.ID,
HeadBranch: pullRequest.Head.Ref,
BaseRepoID: base.ID,
BaseBranch: pullRequest.Base.Ref,
MergeBase: pullRequest.Base.SHA,
Index: pullRequest.Index,
HasMerged: pullRequest.Merged,
Issue: &issue,
}
if pr.Issue.IsClosed && pullRequest.Closed != nil {
pr.Issue.ClosedUnix = timeutil.TimeStamp(pullRequest.Closed.Unix())
}
if pr.HasMerged && pullRequest.MergedTime != nil {
pr.MergedUnix = timeutil.TimeStamp(pullRequest.MergedTime.Unix())
pr.MergedCommitID = pullRequest.MergeCommitSHA
}
*o = PullRequest{
PullRequest: pr,
}
}
type PullRequestProvider struct {
g *Gitea
project *ProjectProvider
prHeadCache f3_gitea.PrHeadCache
}
func (o *PullRequestProvider) ToFormat(pullRequest *PullRequest) *format.PullRequest {
return pullRequest.ToFormat()
}
func (o *PullRequestProvider) FromFormat(pr *format.PullRequest) *PullRequest {
var pullRequest PullRequest
pullRequest.FromFormat(pr)
return &pullRequest
}
func (o *PullRequestProvider) Init() *PullRequestProvider {
o.prHeadCache = make(f3_gitea.PrHeadCache)
return o
}
func (o *PullRequestProvider) cleanupRemotes(repository string) {
for remote := range o.prHeadCache {
util.Command(o.g.ctx, "git", "-C", repository, "remote", "rm", remote)
}
o.prHeadCache = make(f3_gitea.PrHeadCache)
}
func (o *PullRequestProvider) GetObjects(user *User, project *Project, page int) []*PullRequest {
pullRequests, _, err := issues_model.PullRequests(project.GetID(), &issues_model.PullRequestsOptions{
ListOptions: db.ListOptions{Page: page, PageSize: o.g.perPage},
State: string(api.StateAll),
})
if err != nil {
panic(fmt.Errorf("error while listing pullRequests: %v", err))
}
return util.ConvertMap[*issues_model.PullRequest, *PullRequest](pullRequests, PullRequestConverter)
}
func (o *PullRequestProvider) ProcessObject(user *User, project *Project, pr *PullRequest) {
if err := pr.LoadIssue(); err != nil {
panic(err)
}
if err := pr.Issue.LoadRepo(o.g.ctx); err != nil {
panic(err)
}
if err := pr.LoadAttributes(); err != nil {
panic(err)
}
if err := pr.LoadBaseRepoCtx(o.g.ctx); err != nil {
panic(err)
}
if err := pr.LoadHeadRepoCtx(o.g.ctx); err != nil {
panic(err)
}
pr.FetchFunc = func(repository string) string {
head, messages := f3_gitea.UpdateGitForPullRequest(o.g.ctx, &o.prHeadCache, pr.ToFormat(), repository)
for _, message := range messages {
o.g.GetLogger().Warn(message)
}
o.cleanupRemotes(repository)
return head
}
}
func (o *PullRequestProvider) Get(user *User, project *Project, pullRequest *PullRequest) *PullRequest {
id := pullRequest.GetID()
pr, err := issues_model.GetPullRequestByIndex(o.g.ctx, project.GetID(), id)
if issues_model.IsErrPullRequestNotExist(err) {
return &PullRequest{}
}
if err != nil {
panic(err)
}
p := PullRequestConverter(pr)
o.ProcessObject(user, project, p)
return p
}
func (o *PullRequestProvider) Put(user *User, project *Project, pullRequest *PullRequest) *PullRequest {
i := pullRequest.PullRequest.Issue
i.RepoID = project.GetID()
labels := make([]int64, 0, len(i.Labels))
for _, label := range i.Labels {
labels = append(labels, label.ID)
}
if err := issues_model.NewPullRequest(o.g.ctx, &project.Repository, i, labels, []string{}, &pullRequest.PullRequest); err != nil {
panic(err)
}
return o.Get(user, project, pullRequest)
}
func (o *PullRequestProvider) Delete(user *User, project *Project, pullRequest *PullRequest) *PullRequest {
p := o.Get(user, project, pullRequest)
if !p.IsNil() {
repoPath := repo_model.RepoPath(user.Name, project.Name)
gitRepo, err := git.OpenRepository(o.g.ctx, repoPath)
if err != nil {
panic(err)
}
defer gitRepo.Close()
if err := issue_service.DeleteIssue(o.g.GetDoer(), gitRepo, p.PullRequest.Issue); err != nil {
panic(err)
}
}
return p
}

View File

@ -0,0 +1,179 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"lab.forgefriends.org/friendlyforgeformat/gof3/forges/common"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
"xorm.io/builder"
)
type Reaction struct {
issues_model.Reaction
}
func ReactionConverter(f *issues_model.Reaction) *Reaction {
return &Reaction{
Reaction: *f,
}
}
func (o Reaction) GetID() int64 {
return o.ID
}
func (o *Reaction) SetID(id int64) {
o.ID = id
}
func (o *Reaction) IsNil() bool {
return o.ID == 0
}
func (o *Reaction) Equals(other *Reaction) bool {
return o.UserID == other.UserID && o.Type == other.Type
}
func (o *Reaction) ToFormat() *format.Reaction {
return &format.Reaction{
Common: format.Common{Index: o.ID},
UserID: o.User.ID,
UserName: o.User.Name,
Content: o.Type,
}
}
func (o *Reaction) FromFormat(reaction *format.Reaction) {
*o = Reaction{
Reaction: issues_model.Reaction{
ID: reaction.GetID(),
UserID: reaction.UserID,
User: &user_model.User{
ID: reaction.UserID,
Name: reaction.UserName,
},
Type: reaction.Content,
},
}
}
type ReactionProvider struct {
g *Gitea
}
func (o *ReactionProvider) ToFormat(reaction *Reaction) *format.Reaction {
return reaction.ToFormat()
}
func (o *ReactionProvider) FromFormat(m *format.Reaction) *Reaction {
var reaction Reaction
reaction.FromFormat(m)
return &reaction
}
//
// Although it would be possible to use a higher level logic instead of the database,
// as of September 2022 (1.18 dev)
// (i) models/issues/reaction.go imposes a significant overhead
// (ii) is fragile and bugous https://github.com/go-gitea/gitea/issues/20860
//
func (o *ReactionProvider) GetObjects(user *User, project *Project, parents []common.ContainerObjectInterface, page int) []*Reaction {
cond := builder.NewCond()
switch l := parents[len(parents)-1].(type) {
case *Issue:
cond = cond.And(builder.Eq{"reaction.issue_id": l.ID})
cond = cond.And(builder.Eq{"reaction.comment_id": 0})
case *Comment:
cond = cond.And(builder.Eq{"reaction.comment_id": l.ID})
default:
panic(fmt.Errorf("unexpected type %T", parents[len(parents)-1]))
}
sess := db.GetEngine(o.g.ctx).Where(cond)
if page > 0 {
sess = db.SetSessionPagination(sess, &db.ListOptions{Page: page, PageSize: o.g.perPage})
}
reactions := make([]*issues_model.Reaction, 0, 10)
if err := sess.Find(&reactions); err != nil {
panic(err)
}
_, err := (issues_model.ReactionList)(reactions).LoadUsers(o.g.ctx, nil)
if err != nil {
panic(err)
}
return util.ConvertMap[*issues_model.Reaction, *Reaction](reactions, ReactionConverter)
}
func (o *ReactionProvider) ProcessObject(user *User, project *Project, parents []common.ContainerObjectInterface, reaction *Reaction) {
}
func (o *ReactionProvider) Get(user *User, project *Project, parents []common.ContainerObjectInterface, exemplar *Reaction) *Reaction {
reaction := &Reaction{}
has, err := db.GetEngine(o.g.ctx).ID(exemplar.GetID()).Get(&reaction.Reaction)
if err != nil {
panic(err)
} else if !has {
return &Reaction{}
}
if _, err := (issues_model.ReactionList{&reaction.Reaction}).LoadUsers(o.g.ctx, nil); err != nil {
panic(err)
}
return reaction
}
func (o *ReactionProvider) Put(user *User, project *Project, parents []common.ContainerObjectInterface, reaction *Reaction) *Reaction {
r := &issues_model.Reaction{
Type: reaction.Type,
UserID: o.g.GetDoer().ID,
}
switch l := parents[len(parents)-1].(type) {
case *Issue:
r.IssueID = l.ID
r.CommentID = 0
case *Comment:
i, ok := parents[len(parents)-2].(*Issue)
if !ok {
panic(fmt.Errorf("unexpected type %T", parents[len(parents)-2]))
}
r.IssueID = i.ID
r.CommentID = l.ID
default:
panic(fmt.Errorf("unexpected type %T", parents[len(parents)-1]))
}
ctx, committer, err := db.TxContext()
if err != nil {
panic(err)
}
defer committer.Close()
if _, err := db.GetEngine(ctx).Insert(r); err != nil {
panic(err)
}
if err := committer.Commit(); err != nil {
panic(err)
}
return ReactionConverter(r)
}
func (o *ReactionProvider) Delete(user *User, project *Project, parents []common.ContainerObjectInterface, reaction *Reaction) *Reaction {
r := o.Get(user, project, parents, reaction)
if !r.IsNil() {
if _, err := db.GetEngine(o.g.ctx).Delete(&reaction.Reaction); err != nil {
panic(err)
}
return reaction
}
return r
}

View File

@ -0,0 +1,167 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"strings"
"time"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/timeutil"
release_service "code.gitea.io/gitea/services/release"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Release struct {
repo_model.Release
}
func ReleaseConverter(f *repo_model.Release) *Release {
return &Release{
Release: *f,
}
}
func (o Release) GetID() int64 {
return o.ID
}
func (o *Release) SetID(id int64) {
o.ID = id
}
func (o *Release) IsNil() bool {
return o.ID == 0
}
func (o *Release) Equals(other *Release) bool {
return o.ID == other.ID
}
func (o *Release) ToFormat() *format.Release {
return &format.Release{
Common: format.Common{Index: o.ID},
TagName: o.TagName,
TargetCommitish: o.Target,
Name: o.Title,
Body: o.Note,
Draft: o.IsDraft,
Prerelease: o.IsPrerelease,
Created: o.CreatedUnix.AsTime(),
PublisherID: o.Publisher.ID,
PublisherName: o.Publisher.Name,
PublisherEmail: o.Publisher.Email,
}
}
func (o *Release) FromFormat(release *format.Release) {
if release.Created.IsZero() {
if !release.Published.IsZero() {
release.Created = release.Published
} else {
release.Created = time.Now()
}
}
*o = Release{
repo_model.Release{
PublisherID: release.PublisherID,
Publisher: &user_model.User{
ID: release.PublisherID,
Name: release.PublisherName,
Email: release.PublisherEmail,
},
TagName: release.TagName,
LowerTagName: strings.ToLower(release.TagName),
Target: release.TargetCommitish,
Title: release.Name,
Note: release.Body,
IsDraft: release.Draft,
IsPrerelease: release.Prerelease,
IsTag: false,
CreatedUnix: timeutil.TimeStamp(release.Created.Unix()),
},
}
}
type ReleaseProvider struct {
g *Gitea
}
func (o *ReleaseProvider) ToFormat(release *Release) *format.Release {
return release.ToFormat()
}
func (o *ReleaseProvider) FromFormat(i *format.Release) *Release {
var release Release
release.FromFormat(i)
return &release
}
func (o *ReleaseProvider) GetObjects(user *User, project *Project, page int) []*Release {
releases, err := repo_model.GetReleasesByRepoID(project.GetID(), repo_model.FindReleasesOptions{
ListOptions: db.ListOptions{Page: page, PageSize: o.g.perPage},
IncludeDrafts: true,
IncludeTags: false,
})
if err != nil {
panic(fmt.Errorf("error while listing releases: %v", err))
}
return util.ConvertMap[*repo_model.Release, *Release](releases, ReleaseConverter)
}
func (o *ReleaseProvider) ProcessObject(user *User, project *Project, release *Release) {
if err := (&release.Release).LoadAttributes(); err != nil {
panic(err)
}
}
func (o *ReleaseProvider) Get(user *User, project *Project, exemplar *Release) *Release {
id := exemplar.GetID()
release, err := repo_model.GetReleaseByID(o.g.ctx, id)
if repo_model.IsErrReleaseNotExist(err) {
return &Release{}
}
if err != nil {
panic(err)
}
r := ReleaseConverter(release)
o.ProcessObject(user, project, r)
return r
}
func (o *ReleaseProvider) Put(user *User, project *Project, release *Release) *Release {
r := release.Release
r.RepoID = project.GetID()
repoPath := repo_model.RepoPath(user.Name, project.Name)
gitRepo, err := git.OpenRepository(o.g.ctx, repoPath)
if err != nil {
panic(err)
}
defer gitRepo.Close()
if err := release_service.CreateRelease(gitRepo, &r, nil, ""); err != nil {
panic(err)
}
return o.Get(user, project, ReleaseConverter(&r))
}
func (o *ReleaseProvider) Delete(user *User, project *Project, release *Release) *Release {
m := o.Get(user, project, release)
if !m.IsNil() {
if err := release_service.DeleteReleaseByID(o.g.ctx, release.GetID(), o.g.GetDoer(), false); err != nil {
panic(err)
}
}
return m
}

View File

@ -0,0 +1,102 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
repo_model "code.gitea.io/gitea/models/repo"
base "code.gitea.io/gitea/modules/migration"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/services/migrations"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Repository struct {
format.Repository
}
func (o *Repository) Equals(other *Repository) bool {
return false // it is costly to figure that out, mirroring is as fast
}
func (o *Repository) ToFormat() *format.Repository {
return &o.Repository
}
func (o *Repository) FromFormat(repository *format.Repository) {
o.Repository = *repository
}
type RepositoryProvider struct {
g *Gitea
}
func (o *RepositoryProvider) ToFormat(repository *Repository) *format.Repository {
return repository.ToFormat()
}
func (o *RepositoryProvider) FromFormat(p *format.Repository) *Repository {
var repository Repository
repository.FromFormat(p)
return &repository
}
func (o *RepositoryProvider) GetObjects(user *User, project *Project, page int) []*Repository {
if page > 0 {
return make([]*Repository, 0)
}
repositories := make([]*Repository, len(format.RepositoryNames))
for _, name := range format.RepositoryNames {
repositories = append(repositories, o.Get(user, project, &Repository{
Repository: format.Repository{
Name: name,
},
}))
}
return repositories
}
func (o *RepositoryProvider) ProcessObject(user *User, project *Project, repository *Repository) {
}
func (o *RepositoryProvider) Get(user *User, project *Project, exemplar *Repository) *Repository {
repoPath := repo_model.RepoPath(user.Name, project.Name) + exemplar.Name
return &Repository{
Repository: format.Repository{
Name: exemplar.Name,
FetchFunc: func(destination string) {
util.Command(o.g.ctx, "git", "clone", "--bare", repoPath, destination)
},
},
}
}
func (o *RepositoryProvider) Put(user *User, project *Project, repository *Repository) *Repository {
if repository.FetchFunc != nil {
directory, delete := format.RepositoryDefaultDirectory()
defer delete()
repository.FetchFunc(directory)
_, err := repo_module.MigrateRepositoryGitData(o.g.ctx, &user.User, &project.Repository, base.MigrateOptions{
RepoName: project.Name,
Mirror: false,
MirrorInterval: "",
LFS: false,
LFSEndpoint: "",
CloneAddr: directory,
Wiki: o.g.GetOptions().GetFeatures().Wiki,
Releases: o.g.GetOptions().GetFeatures().Releases,
}, migrations.NewMigrationHTTPTransport())
if err != nil {
panic(err)
}
}
return o.Get(user, project, repository)
}
func (o *RepositoryProvider) Delete(user *User, project *Project, repository *Repository) *Repository {
panic("It is not possible to delete a repository")
}

View File

@ -0,0 +1,216 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/timeutil"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Review struct {
issues_model.Review
}
func ReviewConverter(f *issues_model.Review) *Review {
return &Review{
Review: *f,
}
}
func (o Review) GetID() int64 {
return o.ID
}
func (o *Review) SetID(id int64) {
o.ID = id
}
func (o *Review) IsNil() bool {
return o.ID == 0
}
func (o *Review) Equals(other *Review) bool {
return o.Content == other.Content
}
func (o *Review) ToFormat() *format.Review {
comments := make([]*format.ReviewComment, 0, len(o.Comments))
for _, comment := range o.Comments {
comments = append(comments, &format.ReviewComment{
Common: format.Common{Index: comment.ID},
// InReplyTo
Content: comment.Content,
TreePath: comment.TreePath,
DiffHunk: convert.Patch2diff(comment.Patch),
Patch: comment.Patch,
Line: int(comment.Line),
CommitID: comment.CommitSHA,
PosterID: comment.PosterID,
CreatedAt: comment.CreatedUnix.AsTime(),
UpdatedAt: comment.UpdatedUnix.AsTime(),
})
}
review := format.Review{
Common: format.Common{Index: o.Review.ID},
IssueIndex: o.IssueID,
Official: o.Review.Official,
CommitID: o.Review.CommitID,
Content: o.Review.Content,
CreatedAt: o.Review.CreatedUnix.AsTime(),
State: format.ReviewStateUnknown,
Comments: comments,
}
if o.Review.Reviewer != nil {
review.ReviewerID = o.Review.Reviewer.ID
review.ReviewerName = o.Review.Reviewer.Name
}
switch o.Type {
case issues_model.ReviewTypeApprove:
review.State = format.ReviewStateApproved
case issues_model.ReviewTypeReject:
review.State = format.ReviewStateChangesRequested
case issues_model.ReviewTypeComment:
review.State = format.ReviewStateCommented
case issues_model.ReviewTypePending:
review.State = format.ReviewStatePending
case issues_model.ReviewTypeRequest:
review.State = format.ReviewStateRequestReview
}
return &review
}
func (o *Review) FromFormat(review *format.Review) {
comments := make([]*issues_model.Comment, 0, len(review.Comments))
for _, comment := range review.Comments {
comments = append(comments, &issues_model.Comment{
ID: comment.GetID(),
Type: issues_model.CommentTypeReview,
// InReplyTo
CommitSHA: comment.CommitID,
Line: int64(comment.Line),
TreePath: comment.TreePath,
Content: comment.Content,
Patch: comment.Patch,
PosterID: comment.PosterID,
CreatedUnix: timeutil.TimeStamp(comment.CreatedAt.Unix()),
UpdatedUnix: timeutil.TimeStamp(comment.UpdatedAt.Unix()),
})
}
*o = Review{
Review: issues_model.Review{
ID: review.GetID(),
Reviewer: &user_model.User{
ID: review.ReviewerID,
Name: review.ReviewerName,
},
IssueID: review.IssueIndex,
Official: review.Official,
CommitID: review.CommitID,
Content: review.Content,
CreatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()),
Comments: comments,
},
}
switch review.State {
case format.ReviewStateApproved:
o.Type = issues_model.ReviewTypeApprove
case format.ReviewStateChangesRequested:
o.Type = issues_model.ReviewTypeReject
case format.ReviewStateCommented:
o.Type = issues_model.ReviewTypeComment
case format.ReviewStatePending:
o.Type = issues_model.ReviewTypePending
case format.ReviewStateRequestReview:
o.Type = issues_model.ReviewTypeRequest
}
}
type ReviewProvider struct {
g *Gitea
}
func (o *ReviewProvider) ToFormat(review *Review) *format.Review {
return review.ToFormat()
}
func (o *ReviewProvider) FromFormat(i *format.Review) *Review {
var review Review
review.FromFormat(i)
return &review
}
func (o *ReviewProvider) GetObjects(user *User, project *Project, pullRequest *PullRequest, page int) []*Review {
reviews, err := issues_model.FindReviews(o.g.ctx, issues_model.FindReviewOptions{
ListOptions: db.ListOptions{Page: page, PageSize: o.g.perPage},
IssueID: pullRequest.IssueID,
})
if err != nil {
panic(fmt.Errorf("error while listing reviews: %v", err))
}
return util.ConvertMap[*issues_model.Review, *Review](reviews, ReviewConverter)
}
func (o *ReviewProvider) ProcessObject(user *User, project *Project, pullRequest *PullRequest, review *Review) {
if err := (&review.Review).LoadAttributes(o.g.ctx); err != nil {
panic(err)
}
}
func (o *ReviewProvider) Get(user *User, project *Project, pullRequest *PullRequest, exemplar *Review) *Review {
id := exemplar.GetID()
review, err := issues_model.GetReviewByID(o.g.ctx, id)
if issues_model.IsErrReviewNotExist(err) {
return &Review{}
}
if err != nil {
panic(err)
}
if err := review.LoadAttributes(o.g.ctx); err != nil {
panic(err)
}
return ReviewConverter(review)
}
func (o *ReviewProvider) Put(user *User, project *Project, pullRequest *PullRequest, review *Review) *Review {
r := &review.Review
r.ID = 0
for _, comment := range r.Comments {
comment.ID = 0
}
r.IssueID = pullRequest.IssueID
u, err := user_model.GetUserByName(o.g.ctx, r.Reviewer.Name)
if err != nil {
panic(err)
}
r.ReviewerID = u.ID
if err := issues_model.InsertReviews([]*issues_model.Review{r}); err != nil {
panic(err)
}
return o.Get(user, project, pullRequest, ReviewConverter(r))
}
func (o *ReviewProvider) Delete(user *User, project *Project, pullRequest *PullRequest, review *Review) *Review {
r := o.Get(user, project, pullRequest, review)
if !r.IsNil() {
if err := issues_model.DeleteReview(&r.Review); err != nil {
panic(err)
}
}
return r
}

116
services/f3/driver/topic.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
"lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type Topic struct {
repo_model.Topic
}
func TopicConverter(f *repo_model.Topic) *Topic {
return &Topic{
Topic: *f,
}
}
func (o Topic) GetID() int64 {
return o.ID
}
func (o *Topic) SetID(id int64) {
o.ID = id
}
func (o *Topic) IsNil() bool {
return o.ID == 0
}
func (o *Topic) Equals(other *Topic) bool {
return o.Name == other.Name
}
func (o *Topic) ToFormat() *format.Topic {
return &format.Topic{
Common: format.Common{Index: o.ID},
Name: o.Name,
}
}
func (o *Topic) FromFormat(topic *format.Topic) {
*o = Topic{
Topic: repo_model.Topic{
ID: topic.Index,
Name: topic.Name,
},
}
}
type TopicProvider struct {
g *Gitea
}
func (o *TopicProvider) ToFormat(topic *Topic) *format.Topic {
return topic.ToFormat()
}
func (o *TopicProvider) FromFormat(m *format.Topic) *Topic {
var topic Topic
topic.FromFormat(m)
return &topic
}
func (o *TopicProvider) GetObjects(user *User, project *Project, page int) []*Topic {
topics, _, err := repo_model.FindTopics(&repo_model.FindTopicOptions{
ListOptions: db.ListOptions{Page: page, PageSize: o.g.perPage},
RepoID: project.GetID(),
})
if err != nil {
panic(err)
}
return util.ConvertMap[*repo_model.Topic, *Topic](topics, TopicConverter)
}
func (o *TopicProvider) ProcessObject(user *User, project *Project, topic *Topic) {
}
func (o *TopicProvider) Get(user *User, project *Project, exemplar *Topic) *Topic {
id := exemplar.GetID()
topic, err := repo_model.GetRepoTopicByID(o.g.ctx, project.GetID(), id)
if repo_model.IsErrTopicNotExist(err) {
return &Topic{}
}
if err != nil {
panic(err)
}
return TopicConverter(topic)
}
func (o *TopicProvider) Put(user *User, project *Project, topic *Topic) *Topic {
t, err := repo_model.AddTopic(project.GetID(), topic.Name)
if err != nil {
panic(err)
}
return o.Get(user, project, TopicConverter(t))
}
func (o *TopicProvider) Delete(user *User, project *Project, topic *Topic) *Topic {
t := o.Get(user, project, topic)
if !t.IsNil() {
t, err := repo_model.DeleteTopic(project.GetID(), t.Name)
if err != nil {
panic(err)
}
return TopicConverter(t)
}
return t
}

135
services/f3/driver/user.go Normal file
View File

@ -0,0 +1,135 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package driver
import (
"fmt"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
user_service "code.gitea.io/gitea/services/user"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
f3_util "lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
type User struct {
user_model.User
}
func UserConverter(f *user_model.User) *User {
return &User{
User: *f,
}
}
func (o User) GetID() int64 {
return o.ID
}
func (o *User) SetID(id int64) {
o.ID = id
}
func (o *User) IsNil() bool {
return o.ID == 0
}
func (o *User) Equals(other *User) bool {
return (o.Name == other.Name)
}
func (o *User) ToFormat() *format.User {
return &format.User{
Common: format.Common{Index: o.ID},
UserName: o.Name,
Name: o.FullName,
Email: o.Email,
Password: o.Passwd,
}
}
func (o *User) FromFormat(user *format.User) {
*o = User{
User: user_model.User{
ID: user.Index,
Name: user.UserName,
FullName: user.Name,
Email: user.Email,
Passwd: user.Password,
},
}
}
type UserProvider struct {
g *Gitea
}
func (o *UserProvider) ToFormat(user *User) *format.User {
return user.ToFormat()
}
func (o *UserProvider) FromFormat(p *format.User) *User {
var user User
user.FromFormat(p)
return &user
}
func (o *UserProvider) GetObjects(page int) []*User {
users, _, err := user_model.SearchUsers(&user_model.SearchUserOptions{
Actor: o.g.GetDoer(),
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{Page: page, PageSize: o.g.perPage},
})
if err != nil {
panic(fmt.Errorf("error while listing users: %v", err))
}
return f3_util.ConvertMap[*user_model.User, *User](users, UserConverter)
}
func (o *UserProvider) ProcessObject(user *User) {
}
func (o *UserProvider) Get(exemplar *User) *User {
var user *user_model.User
var err error
if exemplar.GetID() > 0 {
user, err = user_model.GetUserByIDCtx(o.g.ctx, exemplar.GetID())
} else if exemplar.Name != "" {
user, err = user_model.GetUserByName(o.g.ctx, exemplar.Name)
} else {
panic("GetID() == 0 and UserName == \"\"")
}
if user_model.IsErrUserNotExist(err) {
return &User{}
}
if err != nil {
panic(fmt.Errorf("user %v %w", exemplar, err))
}
return UserConverter(user)
}
func (o *UserProvider) Put(user *User) *User {
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsActive: util.OptionalBoolTrue,
}
u := user.User
err := user_model.CreateUser(&u, overwriteDefault)
if err != nil {
panic(err)
}
return o.Get(UserConverter(&u))
}
func (o *UserProvider) Delete(user *User) *User {
u := o.Get(user)
if !u.IsNil() {
if err := user_service.DeleteUser(o.g.ctx, &user.User, true); err != nil {
panic(err)
}
}
return u
}

60
services/f3/util/util.go Normal file
View File

@ -0,0 +1,60 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package util
import (
"context"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/services/f3/driver"
"lab.forgefriends.org/friendlyforgeformat/gof3"
f3_forges "lab.forgefriends.org/friendlyforgeformat/gof3/forges"
"lab.forgefriends.org/friendlyforgeformat/gof3/forges/f3"
)
func ToF3Logger(messenger base.Messenger) gof3.Logger {
if messenger == nil {
messenger = func(string, ...interface{}) {}
}
return gof3.Logger{
Message: messenger,
Trace: log.Trace,
Debug: log.Debug,
Info: log.Info,
Warn: log.Warn,
Error: log.Error,
Critical: log.Critical,
Fatal: log.Fatal,
}
}
func GiteaForgeRoot(ctx context.Context, features gof3.Features, doer *user_model.User) *f3_forges.ForgeRoot {
forgeRoot := f3_forges.NewForgeRootFromDriver(&driver.Gitea{}, &driver.Options{
Options: gof3.Options{
Features: features,
Logger: ToF3Logger(nil),
},
Doer: doer,
})
forgeRoot.SetContext(ctx)
return forgeRoot
}
func F3ForgeRoot(ctx context.Context, features gof3.Features, directory string) *f3_forges.ForgeRoot {
forgeRoot := f3_forges.NewForgeRoot(&f3.Options{
Options: gof3.Options{
Configuration: gof3.Configuration{
Directory: directory,
},
Features: features,
Logger: ToF3Logger(nil),
},
Remap: true,
})
forgeRoot.SetContext(ctx)
return forgeRoot
}

View File

@ -0,0 +1,111 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integration
import (
"bytes"
"context"
"flag"
"io"
"net/url"
"os"
"testing"
"code.gitea.io/gitea/cmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/migrations"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli"
f3_forges "lab.forgefriends.org/friendlyforgeformat/gof3/forges"
f3_util "lab.forgefriends.org/friendlyforgeformat/gof3/util"
)
func Test_CmdF3(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
AllowLocalNetworks := setting.Migrations.AllowLocalNetworks
setting.Migrations.AllowLocalNetworks = true
// without migrations.Init() AllowLocalNetworks = true is not effective and
// a http call fails with "...migration can only call allowed HTTP servers..."
migrations.Init()
AppVer := setting.AppVer
// Gitea SDK (go-sdk) need to parse the AppVer from server response, so we must set it to a valid version string.
setting.AppVer = "1.16.0"
defer func() {
setting.Migrations.AllowLocalNetworks = AllowLocalNetworks
setting.AppVer = AppVer
}()
//
// Step 1: create a fixture
//
fixture := f3_forges.NewFixture(t, f3_forges.FixtureF3Factory)
fixture.NewUser()
fixture.NewMilestone()
fixture.NewLabel()
fixture.NewIssue()
fixture.NewTopic()
fixture.NewRepository()
fixture.NewRelease()
fixture.NewAsset()
fixture.NewIssueComment()
fixture.NewIssueReaction()
//
// Step 2: import the fixture into Gitea
//
cmd.CmdF3.Action = func(ctx *cli.Context) { cmd.RunF3(context.Background(), ctx) }
{
realStdout := os.Stdout // Backup Stdout
r, w, _ := os.Pipe()
os.Stdout = w
set := flag.NewFlagSet("f3", 0)
_ = set.Parse([]string{"f3", "--import", "--directory", fixture.ForgeRoot.GetDirectory()})
cliContext := cli.NewContext(&cli.App{Writer: os.Stdout}, set, nil)
assert.NoError(t, cmd.CmdF3.Run(cliContext))
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
commandOutput := buf.String()
assert.EqualValues(t, "imported\n", commandOutput)
os.Stdout = realStdout
}
//
// Step 3: export Gitea into F3
//
directory := t.TempDir()
{
realStdout := os.Stdout // Backup Stdout
r, w, _ := os.Pipe()
os.Stdout = w
set := flag.NewFlagSet("f3", 0)
_ = set.Parse([]string{"f3", "--export", "--no-pull-request", "--user", fixture.UserFormat.UserName, "--repository", fixture.ProjectFormat.Name, "--directory", directory})
cliContext := cli.NewContext(&cli.App{Writer: os.Stdout}, set, nil)
assert.NoError(t, cmd.CmdF3.Run(cliContext))
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
commandOutput := buf.String()
assert.EqualValues(t, "exported\n", commandOutput)
os.Stdout = realStdout
}
//
// Step 4: verify the export and import are equivalent
//
files := f3_util.Command(context.Background(), "find", directory)
assert.Contains(t, files, "/label/")
assert.Contains(t, files, "/issue/")
assert.Contains(t, files, "/milestone/")
assert.Contains(t, files, "/topic/")
assert.Contains(t, files, "/release/")
assert.Contains(t, files, "/asset/")
assert.Contains(t, files, "/issue_reaction/")
})
}

View File

@ -0,0 +1,118 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integration
import (
"context"
"net/url"
"testing"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/f3/util"
"lab.forgefriends.org/friendlyforgeformat/gof3"
f3_forges "lab.forgefriends.org/friendlyforgeformat/gof3/forges"
f3_f3 "lab.forgefriends.org/friendlyforgeformat/gof3/forges/f3"
f3_gitea "lab.forgefriends.org/friendlyforgeformat/gof3/forges/gitea"
"lab.forgefriends.org/friendlyforgeformat/gof3/format"
f3_util "lab.forgefriends.org/friendlyforgeformat/gof3/util"
"github.com/stretchr/testify/assert"
)
func TestF3(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
AllowLocalNetworks := setting.Migrations.AllowLocalNetworks
setting.Migrations.AllowLocalNetworks = true
AppVer := setting.AppVer
// Gitea SDK (go-sdk) need to parse the AppVer from server response, so we must set it to a valid version string.
setting.AppVer = "1.16.0"
defer func() {
setting.Migrations.AllowLocalNetworks = AllowLocalNetworks
setting.AppVer = AppVer
}()
//
// Step 1: create a fixture
//
fixtureNewF3Forge := func(t *testing.T, user *format.User) *f3_forges.ForgeRoot {
root := f3_forges.NewForgeRoot(&f3_f3.Options{
Options: gof3.Options{
Configuration: gof3.Configuration{
Directory: t.TempDir(),
},
Features: gof3.AllFeatures,
},
Remap: true,
})
root.SetContext(context.Background())
return root
}
fixture := f3_forges.NewFixture(t, f3_forges.FixtureForgeFactory{Fun: fixtureNewF3Forge, RootRequired: false})
fixture.NewUser()
fixture.NewMilestone()
fixture.NewLabel()
fixture.NewIssue()
fixture.NewTopic()
fixture.NewRepository()
fixture.NewPullRequest()
fixture.NewRelease()
fixture.NewAsset()
fixture.NewIssueComment()
fixture.NewPullRequestComment()
fixture.NewReview()
fixture.NewIssueReaction()
fixture.NewCommentReaction()
//
// Step 2: mirror the fixture into Gitea
//
doer, err := user_model.GetAdminUser()
assert.NoError(t, err)
giteaLocal := util.GiteaForgeRoot(context.Background(), gof3.AllFeatures, doer)
giteaLocal.Forge.Mirror(fixture.Forge)
//
// Step 3: mirror Gitea into F3
//
adminUsername := "user1"
giteaAPI := f3_forges.NewForgeRootFromDriver(&f3_gitea.Gitea{}, &f3_gitea.Options{
Options: gof3.Options{
Configuration: gof3.Configuration{
URL: setting.AppURL,
Directory: t.TempDir(),
},
Features: gof3.AllFeatures,
},
AuthToken: getUserToken(t, adminUsername),
})
giteaAPI.SetContext(context.Background())
f3 := f3_forges.FixtureNewF3Forge(t, nil)
apiForge := giteaAPI.Forge
apiUser := apiForge.Users.GetFromFormat(&format.User{UserName: fixture.UserFormat.UserName})
apiProject := apiUser.Projects.GetFromFormat(&format.Project{Name: fixture.ProjectFormat.Name})
f3.Forge.Mirror(apiForge, apiUser, apiProject)
//
// Step 4: verify the fixture and F3 are equivalent
//
files := f3_util.Command(context.Background(), "find", f3.GetDirectory())
assert.Contains(t, files, "/repository/git/hooks")
assert.Contains(t, files, "/label/")
assert.Contains(t, files, "/issue/")
assert.Contains(t, files, "/milestone/")
assert.Contains(t, files, "/topic/")
assert.Contains(t, files, "/pull_request/")
assert.Contains(t, files, "/release/")
assert.Contains(t, files, "/asset/")
assert.Contains(t, files, "/comment/")
assert.Contains(t, files, "/review/")
assert.Contains(t, files, "/issue_reaction/")
assert.Contains(t, files, "/comment_reaction/")
// f3_util.Command(context.Background(), "cp", "-a", f3.GetDirectory(), "abc")
})
}