From 61302ebbbddc2c381b637fd87dceed9fc2d497be Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Tue, 6 Sep 2022 14:35:43 +1000 Subject: [PATCH] F3: Gitea driver and CLI user, topic, project, label, milestone, repository, pull_request, release, asset, comment, reaction, review providers Signed-off-by: Earl Warren --- .golangci.yml | 3 + cmd/f3.go | 108 ++++++++++ custom/conf/app.example.ini | 9 + go.mod | 6 +- go.sum | 14 +- main.go | 1 + models/repo/topic.go | 15 ++ modules/convert/pull_review.go | 4 +- modules/setting/f3.go | 24 +++ modules/setting/setting.go | 1 + services/f3/driver/asset.go | 158 +++++++++++++++ services/f3/driver/comment.go | 155 +++++++++++++++ services/f3/driver/driver.go | 118 +++++++++++ services/f3/driver/issue.go | 200 +++++++++++++++++++ services/f3/driver/label.go | 127 ++++++++++++ services/f3/driver/milestone.go | 174 ++++++++++++++++ services/f3/driver/project.go | 157 +++++++++++++++ services/f3/driver/pull_request.go | 310 +++++++++++++++++++++++++++++ services/f3/driver/reaction.go | 179 +++++++++++++++++ services/f3/driver/release.go | 167 ++++++++++++++++ services/f3/driver/repository.go | 102 ++++++++++ services/f3/driver/review.go | 216 ++++++++++++++++++++ services/f3/driver/topic.go | 116 +++++++++++ services/f3/driver/user.go | 135 +++++++++++++ services/f3/util/util.go | 60 ++++++ tests/integration/cmd_f3_test.go | 111 +++++++++++ tests/integration/f3_test.go | 118 +++++++++++ 27 files changed, 2782 insertions(+), 6 deletions(-) create mode 100644 cmd/f3.go create mode 100644 modules/setting/f3.go create mode 100644 services/f3/driver/asset.go create mode 100644 services/f3/driver/comment.go create mode 100644 services/f3/driver/driver.go create mode 100644 services/f3/driver/issue.go create mode 100644 services/f3/driver/label.go create mode 100644 services/f3/driver/milestone.go create mode 100644 services/f3/driver/project.go create mode 100644 services/f3/driver/pull_request.go create mode 100644 services/f3/driver/reaction.go create mode 100644 services/f3/driver/release.go create mode 100644 services/f3/driver/repository.go create mode 100644 services/f3/driver/review.go create mode 100644 services/f3/driver/topic.go create mode 100644 services/f3/driver/user.go create mode 100644 services/f3/util/util.go create mode 100644 tests/integration/cmd_f3_test.go create mode 100644 tests/integration/f3_test.go diff --git a/.golangci.yml b/.golangci.yml index 0e796a2016b0a..896831a6cfe8c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -98,6 +98,9 @@ issues: - gosec - unparam - staticcheck + - path: services/f3/driver/driver.go + linters: + - typecheck - path: models/migrations/v linters: - gocyclo diff --git a/cmd/f3.go b/cmd/f3.go new file mode 100644 index 0000000000000..2d10e467b3b48 --- /dev/null +++ b/cmd/f3.go @@ -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 +} diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 3759428ed5cef..4aea134cbd64a 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -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] diff --git a/go.mod b/go.mod index 4501944a22064..66c32006251fd 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b6d76dfe6f462..a503c5d2641af 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 0e550f05ebca2..6db3b35916c4e 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/models/repo/topic.go b/models/repo/topic.go index 2a16467215d3a..5664344289ddf 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -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() diff --git a/modules/convert/pull_review.go b/modules/convert/pull_review.go index 93ce208224f8f..ff36136018f07 100644 --- a/modules/convert/pull_review.go +++ b/modules/convert/pull_review.go @@ -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] diff --git a/modules/setting/f3.go b/modules/setting/f3.go new file mode 100644 index 0000000000000..17e7b297dfde8 --- /dev/null +++ b/modules/setting/f3.go @@ -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) + } +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 6233437bf5aac..c5273c3e52cf4 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1299,6 +1299,7 @@ func NewServices() { newProject() newMimeTypeMap() newFederationService() + newF3Service() } // NewServicesForInstall initializes the services for install diff --git a/services/f3/driver/asset.go b/services/f3/driver/asset.go new file mode 100644 index 0000000000000..d16bec11bce0d --- /dev/null +++ b/services/f3/driver/asset.go @@ -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 +} diff --git a/services/f3/driver/comment.go b/services/f3/driver/comment.go new file mode 100644 index 0000000000000..59e9ab2658131 --- /dev/null +++ b/services/f3/driver/comment.go @@ -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 +} diff --git a/services/f3/driver/driver.go b/services/f3/driver/driver.go new file mode 100644 index 0000000000000..abe5147396987 --- /dev/null +++ b/services/f3/driver/driver.go @@ -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() { +} diff --git a/services/f3/driver/issue.go b/services/f3/driver/issue.go new file mode 100644 index 0000000000000..ec8d71529e910 --- /dev/null +++ b/services/f3/driver/issue.go @@ -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 +} diff --git a/services/f3/driver/label.go b/services/f3/driver/label.go new file mode 100644 index 0000000000000..f69dbcdf3bc26 --- /dev/null +++ b/services/f3/driver/label.go @@ -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 +} diff --git a/services/f3/driver/milestone.go b/services/f3/driver/milestone.go new file mode 100644 index 0000000000000..1ef1a0f4655c9 --- /dev/null +++ b/services/f3/driver/milestone.go @@ -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 +} diff --git a/services/f3/driver/project.go b/services/f3/driver/project.go new file mode 100644 index 0000000000000..84161c5be045c --- /dev/null +++ b/services/f3/driver/project.go @@ -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 +} diff --git a/services/f3/driver/pull_request.go b/services/f3/driver/pull_request.go new file mode 100644 index 0000000000000..026ecba6dfffc --- /dev/null +++ b/services/f3/driver/pull_request.go @@ -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 +} diff --git a/services/f3/driver/reaction.go b/services/f3/driver/reaction.go new file mode 100644 index 0000000000000..d0b08f60ec482 --- /dev/null +++ b/services/f3/driver/reaction.go @@ -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 +} diff --git a/services/f3/driver/release.go b/services/f3/driver/release.go new file mode 100644 index 0000000000000..e5e90956c208d --- /dev/null +++ b/services/f3/driver/release.go @@ -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 +} diff --git a/services/f3/driver/repository.go b/services/f3/driver/repository.go new file mode 100644 index 0000000000000..483c76065ce94 --- /dev/null +++ b/services/f3/driver/repository.go @@ -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") +} diff --git a/services/f3/driver/review.go b/services/f3/driver/review.go new file mode 100644 index 0000000000000..5e59bf10d1f07 --- /dev/null +++ b/services/f3/driver/review.go @@ -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 +} diff --git a/services/f3/driver/topic.go b/services/f3/driver/topic.go new file mode 100644 index 0000000000000..ce81d9e6af819 --- /dev/null +++ b/services/f3/driver/topic.go @@ -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 +} diff --git a/services/f3/driver/user.go b/services/f3/driver/user.go new file mode 100644 index 0000000000000..46c93f3a721b3 --- /dev/null +++ b/services/f3/driver/user.go @@ -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 +} diff --git a/services/f3/util/util.go b/services/f3/util/util.go new file mode 100644 index 0000000000000..127cb45a0fe5f --- /dev/null +++ b/services/f3/util/util.go @@ -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 +} diff --git a/tests/integration/cmd_f3_test.go b/tests/integration/cmd_f3_test.go new file mode 100644 index 0000000000000..55c3466aca188 --- /dev/null +++ b/tests/integration/cmd_f3_test.go @@ -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/") + }) +} diff --git a/tests/integration/f3_test.go b/tests/integration/f3_test.go new file mode 100644 index 0000000000000..2ba0dd3d46520 --- /dev/null +++ b/tests/integration/f3_test.go @@ -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") + }) +}