summaryrefslogtreecommitdiffstats
path: root/services/f3
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /services/f3
parentInitial commit. (diff)
downloadforgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.tar.xz
forgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.zip
Adding upstream version 9.0.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'services/f3')
-rw-r--r--services/f3/driver/asset.go171
-rw-r--r--services/f3/driver/assets.go42
-rw-r--r--services/f3/driver/comment.go122
-rw-r--r--services/f3/driver/comments.go49
-rw-r--r--services/f3/driver/common.go48
-rw-r--r--services/f3/driver/container.go43
-rw-r--r--services/f3/driver/forge.go64
-rw-r--r--services/f3/driver/issue.go238
-rw-r--r--services/f3/driver/issues.go40
-rw-r--r--services/f3/driver/label.go113
-rw-r--r--services/f3/driver/labels.go37
-rw-r--r--services/f3/driver/main.go17
-rw-r--r--services/f3/driver/main_test.go30
-rw-r--r--services/f3/driver/milestone.go150
-rw-r--r--services/f3/driver/milestones.go40
-rw-r--r--services/f3/driver/options.go20
-rw-r--r--services/f3/driver/options/name.go7
-rw-r--r--services/f3/driver/options/options.go31
-rw-r--r--services/f3/driver/organization.go111
-rw-r--r--services/f3/driver/organizations.go50
-rw-r--r--services/f3/driver/project.go188
-rw-r--r--services/f3/driver/projects.go55
-rw-r--r--services/f3/driver/pullrequest.go320
-rw-r--r--services/f3/driver/pullrequests.go42
-rw-r--r--services/f3/driver/reaction.go133
-rw-r--r--services/f3/driver/reactions.go59
-rw-r--r--services/f3/driver/release.go161
-rw-r--r--services/f3/driver/releases.go42
-rw-r--r--services/f3/driver/repositories.go36
-rw-r--r--services/f3/driver/repository.go101
-rw-r--r--services/f3/driver/review.go179
-rw-r--r--services/f3/driver/reviewcomment.go142
-rw-r--r--services/f3/driver/reviewcomments.go43
-rw-r--r--services/f3/driver/reviews.go49
-rw-r--r--services/f3/driver/root.go41
-rw-r--r--services/f3/driver/tests/init.go15
-rw-r--r--services/f3/driver/tests/new.go39
-rw-r--r--services/f3/driver/tests/options.go21
-rw-r--r--services/f3/driver/topic.go111
-rw-r--r--services/f3/driver/topics.go41
-rw-r--r--services/f3/driver/tree.go104
-rw-r--r--services/f3/driver/user.go128
-rw-r--r--services/f3/driver/users.go48
-rw-r--r--services/f3/util/logger.go97
-rw-r--r--services/f3/util/logger_test.go89
45 files changed, 3707 insertions, 0 deletions
diff --git a/services/f3/driver/asset.go b/services/f3/driver/asset.go
new file mode 100644
index 0000000..6759cc6
--- /dev/null
+++ b/services/f3/driver/asset.go
@@ -0,0 +1,171 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "os"
+
+ "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/storage"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/services/attachment"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+ "github.com/google/uuid"
+)
+
+var _ f3_tree.ForgeDriverInterface = &issue{}
+
+type asset struct {
+ common
+
+ forgejoAsset *repo_model.Attachment
+ sha string
+ contentType string
+ downloadFunc f3.DownloadFuncType
+}
+
+func (o *asset) SetNative(asset any) {
+ o.forgejoAsset = asset.(*repo_model.Attachment)
+}
+
+func (o *asset) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoAsset.ID)
+}
+
+func (o *asset) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *asset) ToFormat() f3.Interface {
+ if o.forgejoAsset == nil {
+ return o.NewFormat()
+ }
+
+ return &f3.ReleaseAsset{
+ Common: f3.NewCommon(o.GetNativeID()),
+ Name: o.forgejoAsset.Name,
+ ContentType: o.contentType,
+ Size: o.forgejoAsset.Size,
+ DownloadCount: o.forgejoAsset.DownloadCount,
+ Created: o.forgejoAsset.CreatedUnix.AsTime(),
+ SHA256: o.sha,
+ DownloadURL: o.forgejoAsset.DownloadURL(),
+ DownloadFunc: o.downloadFunc,
+ }
+}
+
+func (o *asset) FromFormat(content f3.Interface) {
+ asset := content.(*f3.ReleaseAsset)
+ o.forgejoAsset = &repo_model.Attachment{
+ ID: f3_util.ParseInt(asset.GetID()),
+ Name: asset.Name,
+ Size: asset.Size,
+ DownloadCount: asset.DownloadCount,
+ CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()),
+ CustomDownloadURL: asset.DownloadURL,
+ }
+ o.contentType = asset.ContentType
+ o.sha = asset.SHA256
+ o.downloadFunc = asset.DownloadFunc
+}
+
+func (o *asset) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ id := node.GetID().Int64()
+
+ asset, err := repo_model.GetAttachmentByID(ctx, id)
+ if repo_model.IsErrAttachmentNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("asset %v %w", id, err))
+ }
+
+ o.forgejoAsset = asset
+
+ path := o.forgejoAsset.RelativePath()
+
+ {
+ f, err := storage.Attachments.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ hasher := sha256.New()
+ if _, err := io.Copy(hasher, f); err != nil {
+ panic(fmt.Errorf("io.Copy to hasher: %v", err))
+ }
+ o.sha = hex.EncodeToString(hasher.Sum(nil))
+ }
+
+ o.downloadFunc = func() io.ReadCloser {
+ o.Trace("download %s from copy stored in temporary file %s", o.forgejoAsset.DownloadURL, path)
+ f, err := os.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ return f
+ }
+ return true
+}
+
+func (o *asset) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoAsset.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoAsset.ID).Cols("name").Update(o.forgejoAsset); err != nil {
+ panic(fmt.Errorf("UpdateAssetCols: %v %v", o.forgejoAsset, err))
+ }
+}
+
+func (o *asset) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ uploader, err := user_model.GetAdminUser(ctx)
+ if err != nil {
+ panic(fmt.Errorf("GetAdminUser %w", err))
+ }
+
+ o.forgejoAsset.UploaderID = uploader.ID
+ o.forgejoAsset.RepoID = f3_tree.GetProjectID(o.GetNode())
+ o.forgejoAsset.ReleaseID = f3_tree.GetReleaseID(o.GetNode())
+ o.forgejoAsset.UUID = uuid.New().String()
+
+ download := o.downloadFunc()
+ defer download.Close()
+
+ _, err = attachment.NewAttachment(ctx, o.forgejoAsset, download, o.forgejoAsset.Size)
+ if err != nil {
+ panic(err)
+ }
+
+ o.Trace("asset created %d", o.forgejoAsset.ID)
+ return generic.NewNodeID(o.forgejoAsset.ID)
+}
+
+func (o *asset) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ if err := repo_model.DeleteAttachment(ctx, o.forgejoAsset, true); err != nil {
+ panic(err)
+ }
+}
+
+func newAsset() generic.NodeDriverInterface {
+ return &asset{}
+}
diff --git a/services/f3/driver/assets.go b/services/f3/driver/assets.go
new file mode 100644
index 0000000..88a3979
--- /dev/null
+++ b/services/f3/driver/assets.go
@@ -0,0 +1,42 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type assets struct {
+ container
+}
+
+func (o *assets) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ if page > 1 {
+ return generic.NewChildrenSlice(0)
+ }
+
+ releaseID := f3_tree.GetReleaseID(o.GetNode())
+
+ release, err := repo_model.GetReleaseByID(ctx, releaseID)
+ if err != nil {
+ panic(fmt.Errorf("GetReleaseByID %v %w", releaseID, err))
+ }
+
+ if err := release.LoadAttributes(ctx); err != nil {
+ panic(fmt.Errorf("error while listing assets: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(release.Attachments...)...)
+}
+
+func newAssets() generic.NodeDriverInterface {
+ return &assets{}
+}
diff --git a/services/f3/driver/comment.go b/services/f3/driver/comment.go
new file mode 100644
index 0000000..0c10fd7
--- /dev/null
+++ b/services/f3/driver/comment.go
@@ -0,0 +1,122 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "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"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &comment{}
+
+type comment struct {
+ common
+
+ forgejoComment *issues_model.Comment
+}
+
+func (o *comment) SetNative(comment any) {
+ o.forgejoComment = comment.(*issues_model.Comment)
+}
+
+func (o *comment) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoComment.ID)
+}
+
+func (o *comment) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *comment) ToFormat() f3.Interface {
+ if o.forgejoComment == nil {
+ return o.NewFormat()
+ }
+ return &f3.Comment{
+ Common: f3.NewCommon(fmt.Sprintf("%d", o.forgejoComment.ID)),
+ PosterID: f3_tree.NewUserReference(o.forgejoComment.Poster.ID),
+ Content: o.forgejoComment.Content,
+ Created: o.forgejoComment.CreatedUnix.AsTime(),
+ Updated: o.forgejoComment.UpdatedUnix.AsTime(),
+ }
+}
+
+func (o *comment) FromFormat(content f3.Interface) {
+ comment := content.(*f3.Comment)
+
+ o.forgejoComment = &issues_model.Comment{
+ ID: f3_util.ParseInt(comment.GetID()),
+ PosterID: comment.PosterID.GetIDAsInt(),
+ Poster: &user_model.User{
+ ID: comment.PosterID.GetIDAsInt(),
+ },
+ Content: comment.Content,
+ CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()),
+ UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()),
+ }
+}
+
+func (o *comment) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ id := node.GetID().Int64()
+
+ comment, err := issues_model.GetCommentByID(ctx, id)
+ if issues_model.IsErrCommentNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("comment %v %w", id, err))
+ }
+ if err := comment.LoadPoster(ctx); err != nil {
+ panic(fmt.Errorf("LoadPoster %v %w", *comment, err))
+ }
+ o.forgejoComment = comment
+ return true
+}
+
+func (o *comment) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoComment.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoComment.ID).Cols("content").Update(o.forgejoComment); err != nil {
+ panic(fmt.Errorf("UpdateCommentCols: %v %v", o.forgejoComment, err))
+ }
+}
+
+func (o *comment) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ sess := db.GetEngine(ctx)
+
+ if _, err := sess.NoAutoTime().Insert(o.forgejoComment); err != nil {
+ panic(err)
+ }
+ o.Trace("comment created %d", o.forgejoComment.ID)
+ return generic.NewNodeID(o.forgejoComment.ID)
+}
+
+func (o *comment) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ if err := issues_model.DeleteComment(ctx, o.forgejoComment); err != nil {
+ panic(err)
+ }
+}
+
+func newComment() generic.NodeDriverInterface {
+ return &comment{}
+}
diff --git a/services/f3/driver/comments.go b/services/f3/driver/comments.go
new file mode 100644
index 0000000..eb79b74
--- /dev/null
+++ b/services/f3/driver/comments.go
@@ -0,0 +1,49 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type comments struct {
+ container
+}
+
+func (o *comments) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ project := f3_tree.GetProjectID(o.GetNode())
+ commentable := f3_tree.GetCommentableID(o.GetNode())
+
+ issue, err := issues_model.GetIssueByIndex(ctx, project, commentable)
+ if err != nil {
+ panic(fmt.Errorf("GetIssueByIndex %v %w", commentable, err))
+ }
+
+ sess := db.GetEngine(ctx).
+ Table("comment").
+ Where("`issue_id` = ? AND `type` = ?", issue.ID, issues_model.CommentTypeComment)
+ if page != 0 {
+ sess = db.SetSessionPagination(sess, &db.ListOptions{Page: page, PageSize: pageSize})
+ }
+ forgejoComments := make([]*issues_model.Comment, 0, pageSize)
+ if err := sess.Find(&forgejoComments); err != nil {
+ panic(fmt.Errorf("error while listing comments: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(forgejoComments...)...)
+}
+
+func newComments() generic.NodeDriverInterface {
+ return &comments{}
+}
diff --git a/services/f3/driver/common.go b/services/f3/driver/common.go
new file mode 100644
index 0000000..104f91c
--- /dev/null
+++ b/services/f3/driver/common.go
@@ -0,0 +1,48 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type common struct {
+ generic.NullDriver
+}
+
+func (o *common) GetHelper() any {
+ panic("not implemented")
+}
+
+func (o *common) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ return generic.NewChildrenSlice(0)
+}
+
+func (o *common) GetNativeID() string {
+ return ""
+}
+
+func (o *common) SetNative(native any) {
+}
+
+func (o *common) getTree() generic.TreeInterface {
+ return o.GetNode().GetTree()
+}
+
+func (o *common) getPageSize() int {
+ return o.getTreeDriver().GetPageSize()
+}
+
+func (o *common) getKind() generic.Kind {
+ return o.GetNode().GetKind()
+}
+
+func (o *common) getTreeDriver() *treeDriver {
+ return o.GetTreeDriver().(*treeDriver)
+}
+
+func (o *common) IsNull() bool { return false }
diff --git a/services/f3/driver/container.go b/services/f3/driver/container.go
new file mode 100644
index 0000000..1530444
--- /dev/null
+++ b/services/f3/driver/container.go
@@ -0,0 +1,43 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type container struct {
+ common
+}
+
+func (o *container) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *container) ToFormat() f3.Interface {
+ return o.NewFormat()
+}
+
+func (o *container) FromFormat(content f3.Interface) {
+}
+
+func (o *container) Get(context.Context) bool { return true }
+
+func (o *container) Put(ctx context.Context) generic.NodeID {
+ return o.upsert(ctx)
+}
+
+func (o *container) Patch(ctx context.Context) {
+ o.upsert(ctx)
+}
+
+func (o *container) upsert(context.Context) generic.NodeID {
+ return generic.NewNodeID(o.getKind())
+}
diff --git a/services/f3/driver/forge.go b/services/f3/driver/forge.go
new file mode 100644
index 0000000..a4bcf61
--- /dev/null
+++ b/services/f3/driver/forge.go
@@ -0,0 +1,64 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ user_model "code.gitea.io/gitea/models/user"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ "code.forgejo.org/f3/gof3/v3/util"
+)
+
+type forge struct {
+ generic.NullDriver
+
+ ownersKind map[string]generic.Kind
+}
+
+func newForge() generic.NodeDriverInterface {
+ return &forge{
+ ownersKind: make(map[string]generic.Kind),
+ }
+}
+
+func (o *forge) getOwnersKind(ctx context.Context, id string) generic.Kind {
+ kind, ok := o.ownersKind[id]
+ if !ok {
+ user, err := user_model.GetUserByID(ctx, util.ParseInt(id))
+ if err != nil {
+ panic(fmt.Errorf("user_repo.GetUserByID: %w", err))
+ }
+ kind = f3_tree.KindUsers
+ if user.IsOrganization() {
+ kind = f3_tree.KindOrganization
+ }
+ o.ownersKind[id] = kind
+ }
+ return kind
+}
+
+func (o *forge) getOwnersPath(ctx context.Context, id string) f3_tree.Path {
+ return f3_tree.NewPathFromString("/").SetForge().SetOwners(o.getOwnersKind(ctx, id))
+}
+
+func (o *forge) Equals(context.Context, generic.NodeInterface) bool { return true }
+func (o *forge) Get(context.Context) bool { return true }
+func (o *forge) Put(context.Context) generic.NodeID { return generic.NewNodeID("forge") }
+func (o *forge) Patch(context.Context) {}
+func (o *forge) Delete(context.Context) {}
+func (o *forge) NewFormat() f3.Interface { return &f3.Forge{} }
+func (o *forge) FromFormat(f3.Interface) {}
+
+func (o *forge) ToFormat() f3.Interface {
+ return &f3.Forge{
+ Common: f3.NewCommon("forge"),
+ URL: o.String(),
+ }
+}
diff --git a/services/f3/driver/issue.go b/services/f3/driver/issue.go
new file mode 100644
index 0000000..7f1614d
--- /dev/null
+++ b/services/f3/driver/issue.go
@@ -0,0 +1,238 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "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"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &issue{}
+
+type issue struct {
+ common
+
+ forgejoIssue *issues_model.Issue
+}
+
+func (o *issue) SetNative(issue any) {
+ o.forgejoIssue = issue.(*issues_model.Issue)
+}
+
+func (o *issue) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoIssue.Index)
+}
+
+func (o *issue) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *issue) ToFormat() f3.Interface {
+ if o.forgejoIssue == nil {
+ return o.NewFormat()
+ }
+
+ var milestone *f3.Reference
+ if o.forgejoIssue.Milestone != nil {
+ milestone = f3_tree.NewIssueMilestoneReference(o.forgejoIssue.Milestone.ID)
+ }
+
+ assignees := make([]*f3.Reference, 0, len(o.forgejoIssue.Assignees))
+ for _, assignee := range o.forgejoIssue.Assignees {
+ assignees = append(assignees, f3_tree.NewUserReference(assignee.ID))
+ }
+
+ labels := make([]*f3.Reference, 0, len(o.forgejoIssue.Labels))
+ for _, label := range o.forgejoIssue.Labels {
+ labels = append(labels, f3_tree.NewIssueLabelReference(label.ID))
+ }
+
+ return &f3.Issue{
+ Title: o.forgejoIssue.Title,
+ Common: f3.NewCommon(o.GetNativeID()),
+ PosterID: f3_tree.NewUserReference(o.forgejoIssue.Poster.ID),
+ Assignees: assignees,
+ Labels: labels,
+ Content: o.forgejoIssue.Content,
+ Milestone: milestone,
+ State: string(o.forgejoIssue.State()),
+ Created: o.forgejoIssue.CreatedUnix.AsTime(),
+ Updated: o.forgejoIssue.UpdatedUnix.AsTime(),
+ Closed: o.forgejoIssue.ClosedUnix.AsTimePtr(),
+ IsLocked: o.forgejoIssue.IsLocked,
+ }
+}
+
+func (o *issue) FromFormat(content f3.Interface) {
+ issue := content.(*f3.Issue)
+ var milestone *issues_model.Milestone
+ if issue.Milestone != nil {
+ milestone = &issues_model.Milestone{
+ ID: issue.Milestone.GetIDAsInt(),
+ }
+ }
+ o.forgejoIssue = &issues_model.Issue{
+ Title: issue.Title,
+ Index: f3_util.ParseInt(issue.GetID()),
+ PosterID: issue.PosterID.GetIDAsInt(),
+ Poster: &user_model.User{
+ ID: issue.PosterID.GetIDAsInt(),
+ },
+ Content: issue.Content,
+ Milestone: milestone,
+ IsClosed: issue.State == f3.IssueStateClosed,
+ CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()),
+ UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()),
+ IsLocked: issue.IsLocked,
+ }
+
+ assignees := make([]*user_model.User, 0, len(issue.Assignees))
+ for _, assignee := range issue.Assignees {
+ assignees = append(assignees, &user_model.User{ID: assignee.GetIDAsInt()})
+ }
+ o.forgejoIssue.Assignees = assignees
+
+ labels := make([]*issues_model.Label, 0, len(issue.Labels))
+ for _, label := range issue.Labels {
+ labels = append(labels, &issues_model.Label{ID: label.GetIDAsInt()})
+ }
+ o.forgejoIssue.Labels = labels
+
+ if issue.Closed != nil {
+ o.forgejoIssue.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix())
+ }
+}
+
+func (o *issue) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ project := f3_tree.GetProjectID(o.GetNode())
+ id := node.GetID().Int64()
+
+ issue, err := issues_model.GetIssueByIndex(ctx, project, id)
+ if issues_model.IsErrIssueNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("issue %v %w", id, err))
+ }
+ if err := issue.LoadAttributes(ctx); err != nil {
+ panic(err)
+ }
+
+ o.forgejoIssue = issue
+ return true
+}
+
+func (o *issue) Patch(ctx context.Context) {
+ node := o.GetNode()
+ project := f3_tree.GetProjectID(o.GetNode())
+ id := node.GetID().Int64()
+ o.Trace("repo_id = %d, index = %d", project, id)
+ if _, err := db.GetEngine(ctx).Where("`repo_id` = ? AND `index` = ?", project, id).Cols("name", "content", "is_closed").Update(o.forgejoIssue); err != nil {
+ panic(fmt.Errorf("%v %v", o.forgejoIssue, err))
+ }
+}
+
+func (o *issue) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ o.forgejoIssue.RepoID = f3_tree.GetProjectID(o.GetNode())
+ makeLabels := func(issueID int64) []issues_model.IssueLabel {
+ labels := make([]issues_model.IssueLabel, 0, len(o.forgejoIssue.Labels))
+ for _, label := range o.forgejoIssue.Labels {
+ o.Trace("%d with label %d", issueID, label.ID)
+ labels = append(labels, issues_model.IssueLabel{
+ IssueID: issueID,
+ LabelID: label.ID,
+ })
+ }
+ return labels
+ }
+
+ idx, err := db.GetNextResourceIndex(ctx, "issue_index", o.forgejoIssue.RepoID)
+ if err != nil {
+ panic(fmt.Errorf("generate issue index failed: %w", err))
+ }
+ o.forgejoIssue.Index = idx
+
+ sess := db.GetEngine(ctx)
+
+ if _, err = sess.NoAutoTime().Insert(o.forgejoIssue); err != nil {
+ panic(err)
+ }
+
+ labels := makeLabels(o.forgejoIssue.ID)
+ if len(labels) > 0 {
+ if _, err := sess.Insert(labels); err != nil {
+ panic(err)
+ }
+ }
+
+ makeAssignees := func(issueID int64) []issues_model.IssueAssignees {
+ assignees := make([]issues_model.IssueAssignees, 0, len(o.forgejoIssue.Assignees))
+ for _, assignee := range o.forgejoIssue.Assignees {
+ o.Trace("%d with assignee %d", issueID, assignee.ID)
+ assignees = append(assignees, issues_model.IssueAssignees{
+ IssueID: issueID,
+ AssigneeID: assignee.ID,
+ })
+ }
+ return assignees
+ }
+
+ assignees := makeAssignees(o.forgejoIssue.ID)
+ if len(assignees) > 0 {
+ if _, err := sess.Insert(assignees); err != nil {
+ panic(err)
+ }
+ }
+
+ o.Trace("issue created %d/%d", o.forgejoIssue.ID, o.forgejoIssue.Index)
+ return generic.NewNodeID(o.forgejoIssue.Index)
+}
+
+func (o *issue) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ owner := f3_tree.GetOwnerName(o.GetNode())
+ project := f3_tree.GetProjectName(o.GetNode())
+ repoPath := repo_model.RepoPath(owner, project)
+ gitRepo, err := git.OpenRepository(ctx, repoPath)
+ if err != nil {
+ panic(err)
+ }
+ defer gitRepo.Close()
+
+ doer, err := user_model.GetAdminUser(ctx)
+ if err != nil {
+ panic(fmt.Errorf("GetAdminUser %w", err))
+ }
+
+ if err := issue_service.DeleteIssue(ctx, doer, gitRepo, o.forgejoIssue); err != nil {
+ panic(err)
+ }
+}
+
+func newIssue() generic.NodeDriverInterface {
+ return &issue{}
+}
diff --git a/services/f3/driver/issues.go b/services/f3/driver/issues.go
new file mode 100644
index 0000000..3a5a64e
--- /dev/null
+++ b/services/f3/driver/issues.go
@@ -0,0 +1,40 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type issues struct {
+ container
+}
+
+func (o *issues) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ project := f3_tree.GetProjectID(o.GetNode())
+
+ forgejoIssues, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
+ Paginator: &db.ListOptions{Page: page, PageSize: pageSize},
+ RepoIDs: []int64{project},
+ })
+ if err != nil {
+ panic(fmt.Errorf("error while listing issues: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(forgejoIssues...)...)
+}
+
+func newIssues() generic.NodeDriverInterface {
+ return &issues{}
+}
diff --git a/services/f3/driver/label.go b/services/f3/driver/label.go
new file mode 100644
index 0000000..6d1fcaa
--- /dev/null
+++ b/services/f3/driver/label.go
@@ -0,0 +1,113 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &label{}
+
+type label struct {
+ common
+
+ forgejoLabel *issues_model.Label
+}
+
+func (o *label) SetNative(label any) {
+ o.forgejoLabel = label.(*issues_model.Label)
+}
+
+func (o *label) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoLabel.ID)
+}
+
+func (o *label) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *label) ToFormat() f3.Interface {
+ if o.forgejoLabel == nil {
+ return o.NewFormat()
+ }
+ return &f3.Label{
+ Common: f3.NewCommon(fmt.Sprintf("%d", o.forgejoLabel.ID)),
+ Name: o.forgejoLabel.Name,
+ Color: o.forgejoLabel.Color,
+ Description: o.forgejoLabel.Description,
+ }
+}
+
+func (o *label) FromFormat(content f3.Interface) {
+ label := content.(*f3.Label)
+ o.forgejoLabel = &issues_model.Label{
+ ID: f3_util.ParseInt(label.GetID()),
+ Name: label.Name,
+ Description: label.Description,
+ Color: label.Color,
+ }
+}
+
+func (o *label) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ project := f3_tree.GetProjectID(o.GetNode())
+ id := node.GetID().Int64()
+
+ label, err := issues_model.GetLabelInRepoByID(ctx, project, id)
+ if issues_model.IsErrRepoLabelNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("label %v %w", id, err))
+ }
+ o.forgejoLabel = label
+ return true
+}
+
+func (o *label) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoLabel.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoLabel.ID).Cols("name", "description", "color").Update(o.forgejoLabel); err != nil {
+ panic(fmt.Errorf("UpdateLabelCols: %v %v", o.forgejoLabel, err))
+ }
+}
+
+func (o *label) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ o.forgejoLabel.RepoID = f3_tree.GetProjectID(o.GetNode())
+ if err := issues_model.NewLabel(ctx, o.forgejoLabel); err != nil {
+ panic(err)
+ }
+ o.Trace("label created %d", o.forgejoLabel.ID)
+ return generic.NewNodeID(o.forgejoLabel.ID)
+}
+
+func (o *label) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ project := f3_tree.GetProjectID(o.GetNode())
+
+ if err := issues_model.DeleteLabel(ctx, project, o.forgejoLabel.ID); err != nil {
+ panic(err)
+ }
+}
+
+func newLabel() generic.NodeDriverInterface {
+ return &label{}
+}
diff --git a/services/f3/driver/labels.go b/services/f3/driver/labels.go
new file mode 100644
index 0000000..03f986b
--- /dev/null
+++ b/services/f3/driver/labels.go
@@ -0,0 +1,37 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type labels struct {
+ container
+}
+
+func (o *labels) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ project := f3_tree.GetProjectID(o.GetNode())
+
+ forgejoLabels, err := issues_model.GetLabelsByRepoID(ctx, project, "", db.ListOptions{Page: page, PageSize: pageSize})
+ if err != nil {
+ panic(fmt.Errorf("error while listing labels: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(forgejoLabels...)...)
+}
+
+func newLabels() generic.NodeDriverInterface {
+ return &labels{}
+}
diff --git a/services/f3/driver/main.go b/services/f3/driver/main.go
new file mode 100644
index 0000000..825d456
--- /dev/null
+++ b/services/f3/driver/main.go
@@ -0,0 +1,17 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ driver_options "code.gitea.io/gitea/services/f3/driver/options"
+
+ "code.forgejo.org/f3/gof3/v3/options"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+)
+
+func init() {
+ f3_tree.RegisterForgeFactory(driver_options.Name, newTreeDriver)
+ options.RegisterFactory(driver_options.Name, newOptions)
+}
diff --git a/services/f3/driver/main_test.go b/services/f3/driver/main_test.go
new file mode 100644
index 0000000..8505b69
--- /dev/null
+++ b/services/f3/driver/main_test.go
@@ -0,0 +1,30 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ driver_options "code.gitea.io/gitea/services/f3/driver/options"
+
+ _ "code.gitea.io/gitea/models"
+ _ "code.gitea.io/gitea/models/actions"
+ _ "code.gitea.io/gitea/models/activities"
+ _ "code.gitea.io/gitea/models/perm/access"
+ _ "code.gitea.io/gitea/services/f3/driver/tests"
+
+ tests_f3 "code.forgejo.org/f3/gof3/v3/tree/tests/f3"
+ "github.com/stretchr/testify/require"
+)
+
+func TestF3(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ tests_f3.ForgeCompliance(t, driver_options.Name)
+}
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m)
+}
diff --git a/services/f3/driver/milestone.go b/services/f3/driver/milestone.go
new file mode 100644
index 0000000..222407f
--- /dev/null
+++ b/services/f3/driver/milestone.go
@@ -0,0 +1,150 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &milestone{}
+
+type milestone struct {
+ common
+
+ forgejoMilestone *issues_model.Milestone
+}
+
+func (o *milestone) SetNative(milestone any) {
+ o.forgejoMilestone = milestone.(*issues_model.Milestone)
+}
+
+func (o *milestone) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoMilestone.ID)
+}
+
+func (o *milestone) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *milestone) ToFormat() f3.Interface {
+ if o.forgejoMilestone == nil {
+ return o.NewFormat()
+ }
+ return &f3.Milestone{
+ Common: f3.NewCommon(fmt.Sprintf("%d", o.forgejoMilestone.ID)),
+ Title: o.forgejoMilestone.Name,
+ Description: o.forgejoMilestone.Content,
+ Created: o.forgejoMilestone.CreatedUnix.AsTime(),
+ Updated: o.forgejoMilestone.UpdatedUnix.AsTimePtr(),
+ Deadline: o.forgejoMilestone.DeadlineUnix.AsTimePtr(),
+ State: string(o.forgejoMilestone.State()),
+ }
+}
+
+func (o *milestone) FromFormat(content f3.Interface) {
+ milestone := content.(*f3.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.forgejoMilestone = &issues_model.Milestone{
+ ID: f3_util.ParseInt(milestone.GetID()),
+ Name: milestone.Title,
+ Content: milestone.Description,
+ IsClosed: milestone.State == f3.MilestoneStateClosed,
+ CreatedUnix: timeutil.TimeStamp(milestone.Created.Unix()),
+ UpdatedUnix: timeutil.TimeStamp(milestone.Updated.Unix()),
+ ClosedDateUnix: closed,
+ DeadlineUnix: deadline,
+ }
+}
+
+func (o *milestone) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ project := f3_tree.GetProjectID(o.GetNode())
+ id := node.GetID().Int64()
+
+ milestone, err := issues_model.GetMilestoneByRepoID(ctx, project, id)
+ if issues_model.IsErrMilestoneNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("milestone %v %w", id, err))
+ }
+ o.forgejoMilestone = milestone
+ return true
+}
+
+func (o *milestone) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoMilestone.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoMilestone.ID).Cols("name", "description").Update(o.forgejoMilestone); err != nil {
+ panic(fmt.Errorf("UpdateMilestoneCols: %v %v", o.forgejoMilestone, err))
+ }
+}
+
+func (o *milestone) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ o.forgejoMilestone.RepoID = f3_tree.GetProjectID(o.GetNode())
+ if err := issues_model.NewMilestone(ctx, o.forgejoMilestone); err != nil {
+ panic(err)
+ }
+ o.Trace("milestone created %d", o.forgejoMilestone.ID)
+ return generic.NewNodeID(o.forgejoMilestone.ID)
+}
+
+func (o *milestone) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ project := f3_tree.GetProjectID(o.GetNode())
+
+ if err := issues_model.DeleteMilestoneByRepoID(ctx, project, o.forgejoMilestone.ID); err != nil {
+ panic(err)
+ }
+}
+
+func newMilestone() generic.NodeDriverInterface {
+ return &milestone{}
+}
diff --git a/services/f3/driver/milestones.go b/services/f3/driver/milestones.go
new file mode 100644
index 0000000..c816903
--- /dev/null
+++ b/services/f3/driver/milestones.go
@@ -0,0 +1,40 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type milestones struct {
+ container
+}
+
+func (o *milestones) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ project := f3_tree.GetProjectID(o.GetNode())
+
+ forgejoMilestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
+ ListOptions: db.ListOptions{Page: page, PageSize: pageSize},
+ RepoID: project,
+ })
+ if err != nil {
+ panic(fmt.Errorf("error while listing milestones: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(forgejoMilestones...)...)
+}
+
+func newMilestones() generic.NodeDriverInterface {
+ return &milestones{}
+}
diff --git a/services/f3/driver/options.go b/services/f3/driver/options.go
new file mode 100644
index 0000000..abc5015
--- /dev/null
+++ b/services/f3/driver/options.go
@@ -0,0 +1,20 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "net/http"
+
+ driver_options "code.gitea.io/gitea/services/f3/driver/options"
+
+ "code.forgejo.org/f3/gof3/v3/options"
+)
+
+func newOptions() options.Interface {
+ o := &driver_options.Options{}
+ o.SetName(driver_options.Name)
+ o.SetNewMigrationHTTPClient(func() *http.Client { return &http.Client{} })
+ return o
+}
diff --git a/services/f3/driver/options/name.go b/services/f3/driver/options/name.go
new file mode 100644
index 0000000..9922d11
--- /dev/null
+++ b/services/f3/driver/options/name.go
@@ -0,0 +1,7 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package options
+
+const Name = "internal_forgejo"
diff --git a/services/f3/driver/options/options.go b/services/f3/driver/options/options.go
new file mode 100644
index 0000000..ee9fdd6
--- /dev/null
+++ b/services/f3/driver/options/options.go
@@ -0,0 +1,31 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package options
+
+import (
+ "net/http"
+
+ "code.forgejo.org/f3/gof3/v3/options"
+ "code.forgejo.org/f3/gof3/v3/options/cli"
+ "code.forgejo.org/f3/gof3/v3/options/logger"
+)
+
+type NewMigrationHTTPClientFun func() *http.Client
+
+type Options struct {
+ options.Options
+ logger.OptionsLogger
+ cli.OptionsCLI
+
+ NewMigrationHTTPClient NewMigrationHTTPClientFun
+}
+
+func (o *Options) GetNewMigrationHTTPClient() NewMigrationHTTPClientFun {
+ return o.NewMigrationHTTPClient
+}
+
+func (o *Options) SetNewMigrationHTTPClient(fun NewMigrationHTTPClientFun) {
+ o.NewMigrationHTTPClient = fun
+}
diff --git a/services/f3/driver/organization.go b/services/f3/driver/organization.go
new file mode 100644
index 0000000..76b2400
--- /dev/null
+++ b/services/f3/driver/organization.go
@@ -0,0 +1,111 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ org_model "code.gitea.io/gitea/models/organization"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &organization{}
+
+type organization struct {
+ common
+
+ forgejoOrganization *org_model.Organization
+}
+
+func (o *organization) SetNative(organization any) {
+ o.forgejoOrganization = organization.(*org_model.Organization)
+}
+
+func (o *organization) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoOrganization.ID)
+}
+
+func (o *organization) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *organization) ToFormat() f3.Interface {
+ if o.forgejoOrganization == nil {
+ return o.NewFormat()
+ }
+ return &f3.Organization{
+ Common: f3.NewCommon(fmt.Sprintf("%d", o.forgejoOrganization.ID)),
+ Name: o.forgejoOrganization.Name,
+ FullName: o.forgejoOrganization.FullName,
+ }
+}
+
+func (o *organization) FromFormat(content f3.Interface) {
+ organization := content.(*f3.Organization)
+ o.forgejoOrganization = &org_model.Organization{
+ ID: f3_util.ParseInt(organization.GetID()),
+ Name: organization.Name,
+ FullName: organization.FullName,
+ }
+}
+
+func (o *organization) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+ id := node.GetID().Int64()
+ organization, err := org_model.GetOrgByID(ctx, id)
+ if user_model.IsErrUserNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("organization %v %w", id, err))
+ }
+ o.forgejoOrganization = organization
+ return true
+}
+
+func (o *organization) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoOrganization.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoOrganization.ID).Cols("full_name").Update(o.forgejoOrganization); err != nil {
+ panic(fmt.Errorf("UpdateOrganizationCols: %v %v", o.forgejoOrganization, err))
+ }
+}
+
+func (o *organization) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ doer, err := user_model.GetAdminUser(ctx)
+ if err != nil {
+ panic(fmt.Errorf("GetAdminUser %w", err))
+ }
+ err = org_model.CreateOrganization(ctx, o.forgejoOrganization, doer)
+ if err != nil {
+ panic(err)
+ }
+
+ return generic.NewNodeID(o.forgejoOrganization.ID)
+}
+
+func (o *organization) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ if err := org_model.DeleteOrganization(ctx, o.forgejoOrganization); err != nil {
+ panic(err)
+ }
+}
+
+func newOrganization() generic.NodeDriverInterface {
+ return &organization{}
+}
diff --git a/services/f3/driver/organizations.go b/services/f3/driver/organizations.go
new file mode 100644
index 0000000..98c4c14
--- /dev/null
+++ b/services/f3/driver/organizations.go
@@ -0,0 +1,50 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ org_model "code.gitea.io/gitea/models/organization"
+ user_model "code.gitea.io/gitea/models/user"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type organizations struct {
+ container
+}
+
+func (o *organizations) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ sess := db.GetEngine(ctx)
+ if page != 0 {
+ sess = db.SetSessionPagination(sess, &db.ListOptions{Page: page, PageSize: o.getPageSize()})
+ }
+ sess = sess.Select("`user`.*").
+ Where("`type`=?", user_model.UserTypeOrganization)
+ organizations := make([]*org_model.Organization, 0, o.getPageSize())
+
+ if err := sess.Find(&organizations); err != nil {
+ panic(fmt.Errorf("error while listing organizations: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(organizations...)...)
+}
+
+func (o *organizations) GetIDFromName(ctx context.Context, name string) generic.NodeID {
+ organization, err := org_model.GetOrgByName(ctx, name)
+ if err != nil {
+ panic(fmt.Errorf("GetOrganizationByName: %v", err))
+ }
+
+ return generic.NewNodeID(organization.ID)
+}
+
+func newOrganizations() generic.NodeDriverInterface {
+ return &organizations{}
+}
diff --git a/services/f3/driver/project.go b/services/f3/driver/project.go
new file mode 100644
index 0000000..c2a2df3
--- /dev/null
+++ b/services/f3/driver/project.go
@@ -0,0 +1,188 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ repo_service "code.gitea.io/gitea/services/repository"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &project{}
+
+type project struct {
+ common
+
+ forgejoProject *repo_model.Repository
+ forked *f3.Reference
+}
+
+func (o *project) SetNative(project any) {
+ o.forgejoProject = project.(*repo_model.Repository)
+}
+
+func (o *project) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoProject.ID)
+}
+
+func (o *project) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *project) setForkedReference(ctx context.Context) {
+ if !o.forgejoProject.IsFork {
+ return
+ }
+
+ if err := o.forgejoProject.GetBaseRepo(ctx); err != nil {
+ panic(fmt.Errorf("GetBaseRepo %v %w", o.forgejoProject, err))
+ }
+ forkParent := o.forgejoProject.BaseRepo
+ if err := forkParent.LoadOwner(ctx); err != nil {
+ panic(fmt.Errorf("LoadOwner %v %w", forkParent, err))
+ }
+ owners := "users"
+ if forkParent.Owner.IsOrganization() {
+ owners = "organizations"
+ }
+
+ o.forked = f3_tree.NewProjectReference(owners, fmt.Sprintf("%d", forkParent.Owner.ID), fmt.Sprintf("%d", forkParent.ID))
+}
+
+func (o *project) ToFormat() f3.Interface {
+ if o.forgejoProject == nil {
+ return o.NewFormat()
+ }
+ return &f3.Project{
+ Common: f3.NewCommon(fmt.Sprintf("%d", o.forgejoProject.ID)),
+ Name: o.forgejoProject.Name,
+ IsPrivate: o.forgejoProject.IsPrivate,
+ IsMirror: o.forgejoProject.IsMirror,
+ Description: o.forgejoProject.Description,
+ DefaultBranch: o.forgejoProject.DefaultBranch,
+ Forked: o.forked,
+ }
+}
+
+func (o *project) FromFormat(content f3.Interface) {
+ project := content.(*f3.Project)
+ o.forgejoProject = &repo_model.Repository{
+ ID: f3_util.ParseInt(project.GetID()),
+ Name: project.Name,
+ IsPrivate: project.IsPrivate,
+ IsMirror: project.IsMirror,
+ Description: project.Description,
+ DefaultBranch: project.DefaultBranch,
+ }
+ if project.Forked != nil {
+ o.forgejoProject.IsFork = true
+ o.forgejoProject.ForkID = project.Forked.GetIDAsInt()
+ }
+ o.forked = project.Forked
+}
+
+func (o *project) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+ id := node.GetID().Int64()
+ u, err := repo_model.GetRepositoryByID(ctx, id)
+ if repo_model.IsErrRepoNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("project %v %w", id, err))
+ }
+ o.forgejoProject = u
+ o.setForkedReference(ctx)
+ return true
+}
+
+func (o *project) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoProject.ID)
+ o.forgejoProject.LowerName = strings.ToLower(o.forgejoProject.Name)
+ if err := repo_model.UpdateRepositoryCols(ctx, o.forgejoProject,
+ "description",
+ "name",
+ "lower_name",
+ ); err != nil {
+ panic(fmt.Errorf("UpdateRepositoryCols: %v %v", o.forgejoProject, err))
+ }
+}
+
+func (o *project) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ ownerID := f3_tree.GetOwnerID(o.GetNode())
+ owner, err := user_model.GetUserByID(ctx, ownerID)
+ if err != nil {
+ panic(fmt.Errorf("GetUserByID %v %w", ownerID, err))
+ }
+ doer, err := user_model.GetAdminUser(ctx)
+ if err != nil {
+ panic(fmt.Errorf("GetAdminUser %w", err))
+ }
+
+ if o.forked == nil {
+ repo, err := repo_service.CreateRepositoryDirectly(ctx, doer, owner, repo_service.CreateRepoOptions{
+ Name: o.forgejoProject.Name,
+ Description: o.forgejoProject.Description,
+ IsPrivate: o.forgejoProject.IsPrivate,
+ DefaultBranch: o.forgejoProject.DefaultBranch,
+ })
+ if err != nil {
+ panic(err)
+ }
+ o.forgejoProject = repo
+ o.Trace("project created %d", o.forgejoProject.ID)
+ } else {
+ if err = o.forgejoProject.GetBaseRepo(ctx); err != nil {
+ panic(fmt.Errorf("GetBaseRepo %v %w", o.forgejoProject, err))
+ }
+ if err = o.forgejoProject.BaseRepo.LoadOwner(ctx); err != nil {
+ panic(fmt.Errorf("LoadOwner %v %w", o.forgejoProject.BaseRepo, err))
+ }
+
+ repo, err := repo_service.ForkRepositoryIfNotExists(ctx, doer, owner, repo_service.ForkRepoOptions{
+ BaseRepo: o.forgejoProject.BaseRepo,
+ Name: o.forgejoProject.Name,
+ Description: o.forgejoProject.Description,
+ })
+ if err != nil {
+ panic(err)
+ }
+ o.forgejoProject = repo
+ o.Trace("project created %d", o.forgejoProject.ID)
+ }
+ return generic.NewNodeID(o.forgejoProject.ID)
+}
+
+func (o *project) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ doer, err := user_model.GetAdminUser(ctx)
+ if err != nil {
+ panic(fmt.Errorf("GetAdminUser %w", err))
+ }
+
+ if err := repo_service.DeleteRepository(ctx, doer, o.forgejoProject, true); err != nil {
+ panic(err)
+ }
+}
+
+func newProject() generic.NodeDriverInterface {
+ return &project{}
+}
diff --git a/services/f3/driver/projects.go b/services/f3/driver/projects.go
new file mode 100644
index 0000000..a2dabc3
--- /dev/null
+++ b/services/f3/driver/projects.go
@@ -0,0 +1,55 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type projects struct {
+ container
+}
+
+func (o *projects) GetIDFromName(ctx context.Context, name string) generic.NodeID {
+ owner := f3_tree.GetOwnerName(o.GetNode())
+ forgejoProject, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name)
+ if repo_model.IsErrRepoNotExist(err) {
+ return generic.NilID
+ }
+
+ if err != nil {
+ panic(fmt.Errorf("error GetRepositoryByOwnerAndName(%s, %s): %v", owner, name, err))
+ }
+
+ return generic.NewNodeID(forgejoProject.ID)
+}
+
+func (o *projects) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ owner := f3_tree.GetOwner(o.GetNode())
+
+ forgejoProjects, _, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ ListOptions: db.ListOptions{Page: page, PageSize: pageSize},
+ OwnerID: owner.GetID().Int64(),
+ Private: true,
+ })
+ if err != nil {
+ panic(fmt.Errorf("error while listing projects: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(forgejoProjects...)...)
+}
+
+func newProjects() generic.NodeDriverInterface {
+ return &projects{}
+}
diff --git a/services/f3/driver/pullrequest.go b/services/f3/driver/pullrequest.go
new file mode 100644
index 0000000..466b4bd
--- /dev/null
+++ b/services/f3/driver/pullrequest.go
@@ -0,0 +1,320 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+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"
+ 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"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &pullRequest{}
+
+type pullRequest struct {
+ common
+
+ forgejoPullRequest *issues_model.Issue
+ headRepository *f3.Reference
+ baseRepository *f3.Reference
+ fetchFunc f3.PullRequestFetchFunc
+}
+
+func (o *pullRequest) SetNative(pullRequest any) {
+ o.forgejoPullRequest = pullRequest.(*issues_model.Issue)
+}
+
+func (o *pullRequest) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoPullRequest.Index)
+}
+
+func (o *pullRequest) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *pullRequest) repositoryToReference(ctx context.Context, repository *repo_model.Repository) *f3.Reference {
+ if repository == nil {
+ panic("unexpected nil repository")
+ }
+ forge := o.getTree().GetRoot().GetChild(generic.NewNodeID(f3_tree.KindForge)).GetDriver().(*forge)
+ owners := forge.getOwnersPath(ctx, fmt.Sprintf("%d", repository.OwnerID))
+ return f3_tree.NewRepositoryReference(owners.String(), repository.OwnerID, repository.ID)
+}
+
+func (o *pullRequest) referenceToRepository(reference *f3.Reference) int64 {
+ var project int64
+ if reference.Get() == "../../repository/vcs" {
+ project = f3_tree.GetProjectID(o.GetNode())
+ } else {
+ p := f3_tree.ToPath(generic.PathAbsolute(o.GetNode().GetCurrentPath().String(), reference.Get()))
+ o.Trace("%v %v", o.GetNode().GetCurrentPath().String(), p)
+ _, project = p.OwnerAndProjectID()
+ }
+ return project
+}
+
+func (o *pullRequest) ToFormat() f3.Interface {
+ if o.forgejoPullRequest == nil {
+ return o.NewFormat()
+ }
+
+ var milestone *f3.Reference
+ if o.forgejoPullRequest.Milestone != nil {
+ milestone = f3_tree.NewIssueMilestoneReference(o.forgejoPullRequest.Milestone.ID)
+ }
+
+ var mergedTime *time.Time
+ if o.forgejoPullRequest.PullRequest.HasMerged {
+ mergedTime = o.forgejoPullRequest.PullRequest.MergedUnix.AsTimePtr()
+ }
+
+ var closedTime *time.Time
+ if o.forgejoPullRequest.IsClosed {
+ closedTime = o.forgejoPullRequest.ClosedUnix.AsTimePtr()
+ }
+
+ makePullRequestBranch := func(repo *repo_model.Repository, branch string) f3.PullRequestBranch {
+ 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 f3.PullRequestBranch{
+ Ref: branch,
+ SHA: c.ID.String(),
+ }
+ }
+ if err := o.forgejoPullRequest.PullRequest.LoadHeadRepo(db.DefaultContext); err != nil {
+ panic(err)
+ }
+ head := makePullRequestBranch(o.forgejoPullRequest.PullRequest.HeadRepo, o.forgejoPullRequest.PullRequest.HeadBranch)
+ head.Repository = o.headRepository
+ if err := o.forgejoPullRequest.PullRequest.LoadBaseRepo(db.DefaultContext); err != nil {
+ panic(err)
+ }
+ base := makePullRequestBranch(o.forgejoPullRequest.PullRequest.BaseRepo, o.forgejoPullRequest.PullRequest.BaseBranch)
+ base.Repository = o.baseRepository
+
+ return &f3.PullRequest{
+ Common: f3.NewCommon(o.GetNativeID()),
+ PosterID: f3_tree.NewUserReference(o.forgejoPullRequest.Poster.ID),
+ Title: o.forgejoPullRequest.Title,
+ Content: o.forgejoPullRequest.Content,
+ Milestone: milestone,
+ State: string(o.forgejoPullRequest.State()),
+ IsLocked: o.forgejoPullRequest.IsLocked,
+ Created: o.forgejoPullRequest.CreatedUnix.AsTime(),
+ Updated: o.forgejoPullRequest.UpdatedUnix.AsTime(),
+ Closed: closedTime,
+ Merged: o.forgejoPullRequest.PullRequest.HasMerged,
+ MergedTime: mergedTime,
+ MergeCommitSHA: o.forgejoPullRequest.PullRequest.MergedCommitID,
+ Head: head,
+ Base: base,
+ FetchFunc: o.fetchFunc,
+ }
+}
+
+func (o *pullRequest) FromFormat(content f3.Interface) {
+ pullRequest := content.(*f3.PullRequest)
+ var milestone *issues_model.Milestone
+ if pullRequest.Milestone != nil {
+ milestone = &issues_model.Milestone{
+ ID: pullRequest.Milestone.GetIDAsInt(),
+ }
+ }
+
+ o.headRepository = pullRequest.Head.Repository
+ o.baseRepository = pullRequest.Base.Repository
+ pr := issues_model.PullRequest{
+ HeadBranch: pullRequest.Head.Ref,
+ HeadRepoID: o.referenceToRepository(o.headRepository),
+ BaseBranch: pullRequest.Base.Ref,
+ BaseRepoID: o.referenceToRepository(o.baseRepository),
+
+ MergeBase: pullRequest.Base.SHA,
+ Index: f3_util.ParseInt(pullRequest.GetID()),
+ HasMerged: pullRequest.Merged,
+ }
+
+ o.forgejoPullRequest = &issues_model.Issue{
+ Index: f3_util.ParseInt(pullRequest.GetID()),
+ PosterID: pullRequest.PosterID.GetIDAsInt(),
+ Poster: &user_model.User{
+ ID: pullRequest.PosterID.GetIDAsInt(),
+ },
+ Title: pullRequest.Title,
+ Content: pullRequest.Content,
+ Milestone: milestone,
+ IsClosed: pullRequest.State == f3.PullRequestStateClosed,
+ CreatedUnix: timeutil.TimeStamp(pullRequest.Created.Unix()),
+ UpdatedUnix: timeutil.TimeStamp(pullRequest.Updated.Unix()),
+ IsLocked: pullRequest.IsLocked,
+ PullRequest: &pr,
+ IsPull: true,
+ }
+
+ if pullRequest.Closed != nil {
+ o.forgejoPullRequest.ClosedUnix = timeutil.TimeStamp(pullRequest.Closed.Unix())
+ }
+}
+
+func (o *pullRequest) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ project := f3_tree.GetProjectID(o.GetNode())
+ id := node.GetID().Int64()
+
+ issue, err := issues_model.GetIssueByIndex(ctx, project, id)
+ if issues_model.IsErrIssueNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("issue %v %w", id, err))
+ }
+ if err := issue.LoadAttributes(ctx); err != nil {
+ panic(err)
+ }
+ if err := issue.PullRequest.LoadHeadRepo(ctx); err != nil {
+ panic(err)
+ }
+ o.headRepository = o.repositoryToReference(ctx, issue.PullRequest.HeadRepo)
+ if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil {
+ panic(err)
+ }
+ o.baseRepository = o.repositoryToReference(ctx, issue.PullRequest.BaseRepo)
+
+ o.forgejoPullRequest = issue
+ o.Trace("ID = %s", o.forgejoPullRequest.ID)
+ return true
+}
+
+func (o *pullRequest) Patch(ctx context.Context) {
+ node := o.GetNode()
+ project := f3_tree.GetProjectID(o.GetNode())
+ id := node.GetID().Int64()
+ o.Trace("repo_id = %d, index = %d", project, id)
+ if _, err := db.GetEngine(ctx).Where("`repo_id` = ? AND `index` = ?", project, id).Cols("name", "content").Update(o.forgejoPullRequest); err != nil {
+ panic(fmt.Errorf("%v %v", o.forgejoPullRequest, err))
+ }
+}
+
+func (o *pullRequest) GetPullRequestPushRefs() []string {
+ return []string{
+ fmt.Sprintf("refs/f3/%s/head", o.GetNativeID()),
+ fmt.Sprintf("refs/pull/%s/head", o.GetNativeID()),
+ }
+}
+
+func (o *pullRequest) GetPullRequestRef() string {
+ return fmt.Sprintf("refs/pull/%s/head", o.GetNativeID())
+}
+
+func (o *pullRequest) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ o.forgejoPullRequest.RepoID = f3_tree.GetProjectID(o.GetNode())
+
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ panic(err)
+ }
+ defer committer.Close()
+
+ idx, err := db.GetNextResourceIndex(ctx, "issue_index", o.forgejoPullRequest.RepoID)
+ if err != nil {
+ panic(fmt.Errorf("generate issue index failed: %w", err))
+ }
+ o.forgejoPullRequest.Index = idx
+
+ sess := db.GetEngine(ctx)
+
+ if _, err = sess.NoAutoTime().Insert(o.forgejoPullRequest); err != nil {
+ panic(err)
+ }
+
+ pr := o.forgejoPullRequest.PullRequest
+ pr.Index = o.forgejoPullRequest.Index
+ pr.IssueID = o.forgejoPullRequest.ID
+ pr.HeadRepoID = o.referenceToRepository(o.headRepository)
+ if pr.HeadRepoID == 0 {
+ panic(fmt.Errorf("HeadRepoID == 0 in %v", pr))
+ }
+ pr.BaseRepoID = o.referenceToRepository(o.baseRepository)
+ if pr.BaseRepoID == 0 {
+ panic(fmt.Errorf("BaseRepoID == 0 in %v", pr))
+ }
+
+ if _, err = sess.NoAutoTime().Insert(pr); err != nil {
+ panic(err)
+ }
+
+ if err = committer.Commit(); err != nil {
+ panic(fmt.Errorf("Commit: %w", err))
+ }
+
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ panic(err)
+ }
+ if err := pr.LoadHeadRepo(ctx); err != nil {
+ panic(err)
+ }
+
+ o.Trace("pullRequest created %d/%d", o.forgejoPullRequest.ID, o.forgejoPullRequest.Index)
+ return generic.NewNodeID(o.forgejoPullRequest.Index)
+}
+
+func (o *pullRequest) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ owner := f3_tree.GetOwnerName(o.GetNode())
+ project := f3_tree.GetProjectName(o.GetNode())
+ repoPath := repo_model.RepoPath(owner, project)
+ gitRepo, err := git.OpenRepository(ctx, repoPath)
+ if err != nil {
+ panic(err)
+ }
+ defer gitRepo.Close()
+
+ doer, err := user_model.GetAdminUser(ctx)
+ if err != nil {
+ panic(fmt.Errorf("GetAdminUser %w", err))
+ }
+
+ if err := issue_service.DeleteIssue(ctx, doer, gitRepo, o.forgejoPullRequest); err != nil {
+ panic(err)
+ }
+}
+
+func newPullRequest() generic.NodeDriverInterface {
+ return &pullRequest{}
+}
diff --git a/services/f3/driver/pullrequests.go b/services/f3/driver/pullrequests.go
new file mode 100644
index 0000000..e7f2910
--- /dev/null
+++ b/services/f3/driver/pullrequests.go
@@ -0,0 +1,42 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/optional"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type pullRequests struct {
+ container
+}
+
+func (o *pullRequests) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ project := f3_tree.GetProjectID(o.GetNode())
+
+ forgejoPullRequests, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
+ Paginator: &db.ListOptions{Page: page, PageSize: pageSize},
+ RepoIDs: []int64{project},
+ IsPull: optional.Some(true),
+ })
+ if err != nil {
+ panic(fmt.Errorf("error while listing pullRequests: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(forgejoPullRequests...)...)
+}
+
+func newPullRequests() generic.NodeDriverInterface {
+ return &pullRequests{}
+}
diff --git a/services/f3/driver/reaction.go b/services/f3/driver/reaction.go
new file mode 100644
index 0000000..0dc486c
--- /dev/null
+++ b/services/f3/driver/reaction.go
@@ -0,0 +1,133 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &reaction{}
+
+type reaction struct {
+ common
+
+ forgejoReaction *issues_model.Reaction
+}
+
+func (o *reaction) SetNative(reaction any) {
+ o.forgejoReaction = reaction.(*issues_model.Reaction)
+}
+
+func (o *reaction) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoReaction.ID)
+}
+
+func (o *reaction) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *reaction) ToFormat() f3.Interface {
+ if o.forgejoReaction == nil {
+ return o.NewFormat()
+ }
+ return &f3.Reaction{
+ Common: f3.NewCommon(fmt.Sprintf("%d", o.forgejoReaction.ID)),
+ UserID: f3_tree.NewUserReference(o.forgejoReaction.User.ID),
+ Content: o.forgejoReaction.Type,
+ }
+}
+
+func (o *reaction) FromFormat(content f3.Interface) {
+ reaction := content.(*f3.Reaction)
+
+ o.forgejoReaction = &issues_model.Reaction{
+ ID: f3_util.ParseInt(reaction.GetID()),
+ UserID: reaction.UserID.GetIDAsInt(),
+ User: &user_model.User{
+ ID: reaction.UserID.GetIDAsInt(),
+ },
+ Type: reaction.Content,
+ }
+}
+
+func (o *reaction) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ id := node.GetID().Int64()
+
+ if has, err := db.GetEngine(ctx).Where("ID = ?", id).Get(o.forgejoReaction); err != nil {
+ panic(fmt.Errorf("reaction %v %w", id, err))
+ } else if !has {
+ return false
+ }
+ if _, err := o.forgejoReaction.LoadUser(ctx); err != nil {
+ panic(fmt.Errorf("LoadUser %v %w", *o.forgejoReaction, err))
+ }
+ return true
+}
+
+func (o *reaction) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoReaction.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoReaction.ID).Cols("type").Update(o.forgejoReaction); err != nil {
+ panic(fmt.Errorf("UpdateReactionCols: %v %v", o.forgejoReaction, err))
+ }
+}
+
+func (o *reaction) Put(ctx context.Context) generic.NodeID {
+ o.Error("%v", o.forgejoReaction.User)
+
+ sess := db.GetEngine(ctx)
+
+ reactionable := f3_tree.GetReactionable(o.GetNode())
+ reactionableID := f3_tree.GetReactionableID(o.GetNode())
+
+ switch reactionable.GetKind() {
+ case f3_tree.KindIssue, f3_tree.KindPullRequest:
+ project := f3_tree.GetProjectID(o.GetNode())
+ issue, err := issues_model.GetIssueByIndex(ctx, project, reactionableID)
+ if err != nil {
+ panic(fmt.Errorf("GetIssueByIndex %v %w", reactionableID, err))
+ }
+ o.forgejoReaction.IssueID = issue.ID
+ case f3_tree.KindComment:
+ o.forgejoReaction.CommentID = reactionableID
+ default:
+ panic(fmt.Errorf("unexpected type %v", reactionable.GetKind()))
+ }
+
+ o.Error("%v", o.forgejoReaction)
+
+ if _, err := sess.Insert(o.forgejoReaction); err != nil {
+ panic(err)
+ }
+ o.Trace("reaction created %d", o.forgejoReaction.ID)
+ return generic.NewNodeID(o.forgejoReaction.ID)
+}
+
+func (o *reaction) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ sess := db.GetEngine(ctx)
+ if _, err := sess.Delete(o.forgejoReaction); err != nil {
+ panic(err)
+ }
+}
+
+func newReaction() generic.NodeDriverInterface {
+ return &reaction{}
+}
diff --git a/services/f3/driver/reactions.go b/services/f3/driver/reactions.go
new file mode 100644
index 0000000..b7fd5e8
--- /dev/null
+++ b/services/f3/driver/reactions.go
@@ -0,0 +1,59 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ "xorm.io/builder"
+)
+
+type reactions struct {
+ container
+}
+
+func (o *reactions) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ reactionable := f3_tree.GetReactionable(o.GetNode())
+ reactionableID := f3_tree.GetReactionableID(o.GetNode())
+
+ sess := db.GetEngine(ctx)
+ cond := builder.NewCond()
+ switch reactionable.GetKind() {
+ case f3_tree.KindIssue, f3_tree.KindPullRequest:
+ project := f3_tree.GetProjectID(o.GetNode())
+ issue, err := issues_model.GetIssueByIndex(ctx, project, reactionableID)
+ if err != nil {
+ panic(fmt.Errorf("GetIssueByIndex %v %w", reactionableID, err))
+ }
+ cond = cond.And(builder.Eq{"reaction.issue_id": issue.ID})
+ case f3_tree.KindComment:
+ cond = cond.And(builder.Eq{"reaction.comment_id": reactionableID})
+ default:
+ panic(fmt.Errorf("unexpected type %v", reactionable.GetKind()))
+ }
+
+ sess = sess.Where(cond)
+ if page > 0 {
+ sess = db.SetSessionPagination(sess, &db.ListOptions{Page: page, PageSize: pageSize})
+ }
+ reactions := make([]*issues_model.Reaction, 0, 10)
+ if err := sess.Find(&reactions); err != nil {
+ panic(fmt.Errorf("error while listing reactions: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(reactions...)...)
+}
+
+func newReactions() generic.NodeDriverInterface {
+ return &reactions{}
+}
diff --git a/services/f3/driver/release.go b/services/f3/driver/release.go
new file mode 100644
index 0000000..e937f84
--- /dev/null
+++ b/services/f3/driver/release.go
@@ -0,0 +1,161 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "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"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &release{}
+
+type release struct {
+ common
+
+ forgejoRelease *repo_model.Release
+}
+
+func (o *release) SetNative(release any) {
+ o.forgejoRelease = release.(*repo_model.Release)
+}
+
+func (o *release) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoRelease.ID)
+}
+
+func (o *release) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *release) ToFormat() f3.Interface {
+ if o.forgejoRelease == nil {
+ return o.NewFormat()
+ }
+ return &f3.Release{
+ Common: f3.NewCommon(fmt.Sprintf("%d", o.forgejoRelease.ID)),
+ TagName: o.forgejoRelease.TagName,
+ TargetCommitish: o.forgejoRelease.Target,
+ Name: o.forgejoRelease.Title,
+ Body: o.forgejoRelease.Note,
+ Draft: o.forgejoRelease.IsDraft,
+ Prerelease: o.forgejoRelease.IsPrerelease,
+ PublisherID: f3_tree.NewUserReference(o.forgejoRelease.Publisher.ID),
+ Created: o.forgejoRelease.CreatedUnix.AsTime(),
+ }
+}
+
+func (o *release) FromFormat(content f3.Interface) {
+ release := content.(*f3.Release)
+
+ o.forgejoRelease = &repo_model.Release{
+ ID: f3_util.ParseInt(release.GetID()),
+ PublisherID: release.PublisherID.GetIDAsInt(),
+ Publisher: &user_model.User{
+ ID: release.PublisherID.GetIDAsInt(),
+ },
+ 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()),
+ }
+}
+
+func (o *release) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ id := node.GetID().Int64()
+
+ release, err := repo_model.GetReleaseByID(ctx, id)
+ if repo_model.IsErrReleaseNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("release %v %w", id, err))
+ }
+
+ release.Publisher, err = user_model.GetUserByID(ctx, release.PublisherID)
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ release.Publisher = user_model.NewGhostUser()
+ } else {
+ panic(err)
+ }
+ }
+
+ o.forgejoRelease = release
+ return true
+}
+
+func (o *release) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoRelease.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoRelease.ID).Cols("title", "note").Update(o.forgejoRelease); err != nil {
+ panic(fmt.Errorf("UpdateReleaseCols: %v %v", o.forgejoRelease, err))
+ }
+}
+
+func (o *release) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ o.forgejoRelease.RepoID = f3_tree.GetProjectID(o.GetNode())
+
+ owner := f3_tree.GetOwnerName(o.GetNode())
+ project := f3_tree.GetProjectName(o.GetNode())
+ repoPath := repo_model.RepoPath(owner, project)
+ gitRepo, err := git.OpenRepository(ctx, repoPath)
+ if err != nil {
+ panic(err)
+ }
+ defer gitRepo.Close()
+ if err := release_service.CreateRelease(gitRepo, o.forgejoRelease, "", nil); err != nil {
+ panic(err)
+ }
+ o.Trace("release created %d", o.forgejoRelease.ID)
+ return generic.NewNodeID(o.forgejoRelease.ID)
+}
+
+func (o *release) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ project := f3_tree.GetProjectID(o.GetNode())
+ repo, err := repo_model.GetRepositoryByID(ctx, project)
+ if err != nil {
+ panic(err)
+ }
+
+ doer, err := user_model.GetAdminUser(ctx)
+ if err != nil {
+ panic(fmt.Errorf("GetAdminUser %w", err))
+ }
+
+ if err := release_service.DeleteReleaseByID(ctx, repo, o.forgejoRelease, doer, true); err != nil {
+ panic(err)
+ }
+}
+
+func newRelease() generic.NodeDriverInterface {
+ return &release{}
+}
diff --git a/services/f3/driver/releases.go b/services/f3/driver/releases.go
new file mode 100644
index 0000000..3b46bc7
--- /dev/null
+++ b/services/f3/driver/releases.go
@@ -0,0 +1,42 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type releases struct {
+ container
+}
+
+func (o *releases) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ project := f3_tree.GetProjectID(o.GetNode())
+
+ forgejoReleases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
+ ListOptions: db.ListOptions{Page: page, PageSize: pageSize},
+ IncludeDrafts: true,
+ IncludeTags: false,
+ RepoID: project,
+ })
+ if err != nil {
+ panic(fmt.Errorf("error while listing releases: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(forgejoReleases...)...)
+}
+
+func newReleases() generic.NodeDriverInterface {
+ return &releases{}
+}
diff --git a/services/f3/driver/repositories.go b/services/f3/driver/repositories.go
new file mode 100644
index 0000000..03daf35
--- /dev/null
+++ b/services/f3/driver/repositories.go
@@ -0,0 +1,36 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type repositories struct {
+ container
+}
+
+func (o *repositories) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ children := generic.NewChildrenSlice(0)
+ if page > 1 {
+ return children
+ }
+
+ names := []string{f3.RepositoryNameDefault}
+ project := f3_tree.GetProject(o.GetNode()).ToFormat().(*f3.Project)
+ if project.HasWiki {
+ names = append(names, f3.RepositoryNameWiki)
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(names...)...)
+}
+
+func newRepositories() generic.NodeDriverInterface {
+ return &repositories{}
+}
diff --git a/services/f3/driver/repository.go b/services/f3/driver/repository.go
new file mode 100644
index 0000000..da968b4
--- /dev/null
+++ b/services/f3/driver/repository.go
@@ -0,0 +1,101 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ helpers_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/repository"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+var _ f3_tree.ForgeDriverInterface = &repository{}
+
+type repository struct {
+ common
+
+ name string
+ h helpers_repository.Interface
+
+ f *f3.Repository
+}
+
+func (o *repository) SetNative(repository any) {
+ o.name = repository.(string)
+}
+
+func (o *repository) GetNativeID() string {
+ return o.name
+}
+
+func (o *repository) NewFormat() f3.Interface {
+ return &f3.Repository{}
+}
+
+func (o *repository) ToFormat() f3.Interface {
+ return &f3.Repository{
+ Common: f3.NewCommon(o.GetNativeID()),
+ Name: o.GetNativeID(),
+ FetchFunc: o.f.FetchFunc,
+ }
+}
+
+func (o *repository) FromFormat(content f3.Interface) {
+ f := content.Clone().(*f3.Repository)
+ o.f = f
+ o.f.SetID(f.Name)
+ o.name = f.Name
+}
+
+func (o *repository) Get(ctx context.Context) bool {
+ return o.h.Get(ctx)
+}
+
+func (o *repository) Put(ctx context.Context) generic.NodeID {
+ return o.upsert(ctx)
+}
+
+func (o *repository) Patch(ctx context.Context) {
+ o.upsert(ctx)
+}
+
+func (o *repository) upsert(ctx context.Context) generic.NodeID {
+ o.Trace("%s", o.GetNativeID())
+ o.h.Upsert(ctx, o.f)
+ return generic.NewNodeID(o.f.Name)
+}
+
+func (o *repository) SetFetchFunc(fetchFunc func(ctx context.Context, destination string)) {
+ o.f.FetchFunc = fetchFunc
+}
+
+func (o *repository) getURL() string {
+ owner := f3_tree.GetOwnerName(o.GetNode())
+ repoName := f3_tree.GetProjectName(o.GetNode())
+ if o.f.GetID() == f3.RepositoryNameWiki {
+ repoName += ".wiki"
+ }
+ return repo_model.RepoPath(owner, repoName)
+}
+
+func (o *repository) GetRepositoryURL() string {
+ return o.getURL()
+}
+
+func (o *repository) GetRepositoryPushURL() string {
+ return o.getURL()
+}
+
+func newRepository(_ context.Context) generic.NodeDriverInterface {
+ r := &repository{
+ f: &f3.Repository{},
+ }
+ r.h = helpers_repository.NewHelper(r)
+ return r
+}
diff --git a/services/f3/driver/review.go b/services/f3/driver/review.go
new file mode 100644
index 0000000..a3c074b
--- /dev/null
+++ b/services/f3/driver/review.go
@@ -0,0 +1,179 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "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"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &review{}
+
+type review struct {
+ common
+
+ forgejoReview *issues_model.Review
+}
+
+func (o *review) SetNative(review any) {
+ o.forgejoReview = review.(*issues_model.Review)
+}
+
+func (o *review) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoReview.ID)
+}
+
+func (o *review) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *review) ToFormat() f3.Interface {
+ if o.forgejoReview == nil {
+ return o.NewFormat()
+ }
+
+ review := &f3.Review{
+ Common: f3.NewCommon(o.GetNativeID()),
+ ReviewerID: f3_tree.NewUserReference(o.forgejoReview.ReviewerID),
+ Official: o.forgejoReview.Official,
+ CommitID: o.forgejoReview.CommitID,
+ Content: o.forgejoReview.Content,
+ CreatedAt: o.forgejoReview.CreatedUnix.AsTime(),
+ }
+
+ switch o.forgejoReview.Type {
+ case issues_model.ReviewTypeApprove:
+ review.State = f3.ReviewStateApproved
+ case issues_model.ReviewTypeReject:
+ review.State = f3.ReviewStateChangesRequested
+ case issues_model.ReviewTypeComment:
+ review.State = f3.ReviewStateCommented
+ case issues_model.ReviewTypePending:
+ review.State = f3.ReviewStatePending
+ case issues_model.ReviewTypeRequest:
+ review.State = f3.ReviewStateRequestReview
+ default:
+ review.State = f3.ReviewStateUnknown
+ }
+
+ if o.forgejoReview.Reviewer != nil {
+ review.ReviewerID = f3_tree.NewUserReference(o.forgejoReview.Reviewer.ID)
+ }
+
+ return review
+}
+
+func (o *review) FromFormat(content f3.Interface) {
+ review := content.(*f3.Review)
+
+ o.forgejoReview = &issues_model.Review{
+ ID: f3_util.ParseInt(review.GetID()),
+ ReviewerID: review.ReviewerID.GetIDAsInt(),
+ Reviewer: &user_model.User{
+ ID: review.ReviewerID.GetIDAsInt(),
+ },
+ Official: review.Official,
+ CommitID: review.CommitID,
+ Content: review.Content,
+ CreatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()),
+ }
+
+ switch review.State {
+ case f3.ReviewStateApproved:
+ o.forgejoReview.Type = issues_model.ReviewTypeApprove
+ case f3.ReviewStateChangesRequested:
+ o.forgejoReview.Type = issues_model.ReviewTypeReject
+ case f3.ReviewStateCommented:
+ o.forgejoReview.Type = issues_model.ReviewTypeComment
+ case f3.ReviewStatePending:
+ o.forgejoReview.Type = issues_model.ReviewTypePending
+ case f3.ReviewStateRequestReview:
+ o.forgejoReview.Type = issues_model.ReviewTypeRequest
+ default:
+ o.forgejoReview.Type = issues_model.ReviewTypeUnknown
+ }
+}
+
+func (o *review) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ id := node.GetID().Int64()
+
+ review, err := issues_model.GetReviewByID(ctx, id)
+ if issues_model.IsErrReviewNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("review %v %w", id, err))
+ }
+ if err := review.LoadReviewer(ctx); err != nil {
+ panic(fmt.Errorf("LoadReviewer %v %w", *review, err))
+ }
+ o.forgejoReview = review
+ return true
+}
+
+func (o *review) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoReview.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoReview.ID).Cols("content").Update(o.forgejoReview); err != nil {
+ panic(fmt.Errorf("UpdateReviewCols: %v %v", o.forgejoReview, err))
+ }
+}
+
+func (o *review) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ project := f3_tree.GetProjectID(o.GetNode())
+ pullRequest := f3_tree.GetPullRequestID(o.GetNode())
+
+ issue, err := issues_model.GetIssueByIndex(ctx, project, pullRequest)
+ if err != nil {
+ panic(fmt.Errorf("GetIssueByIndex %v", err))
+ }
+ o.forgejoReview.IssueID = issue.ID
+
+ sess := db.GetEngine(ctx)
+
+ if _, err := sess.NoAutoTime().Insert(o.forgejoReview); err != nil {
+ panic(err)
+ }
+ o.Trace("review created %d", o.forgejoReview.ID)
+ return generic.NewNodeID(o.forgejoReview.ID)
+}
+
+func (o *review) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ project := f3_tree.GetProjectID(o.GetNode())
+ pullRequest := f3_tree.GetPullRequestID(o.GetNode())
+
+ issue, err := issues_model.GetIssueByIndex(ctx, project, pullRequest)
+ if err != nil {
+ panic(fmt.Errorf("GetIssueByIndex %v", err))
+ }
+ o.forgejoReview.IssueID = issue.ID
+
+ if err := issues_model.DeleteReview(ctx, o.forgejoReview); err != nil {
+ panic(err)
+ }
+}
+
+func newReview() generic.NodeDriverInterface {
+ return &review{}
+}
diff --git a/services/f3/driver/reviewcomment.go b/services/f3/driver/reviewcomment.go
new file mode 100644
index 0000000..8e13d86
--- /dev/null
+++ b/services/f3/driver/reviewcomment.go
@@ -0,0 +1,142 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "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"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &reviewComment{}
+
+type reviewComment struct {
+ common
+
+ forgejoReviewComment *issues_model.Comment
+}
+
+func (o *reviewComment) SetNative(reviewComment any) {
+ o.forgejoReviewComment = reviewComment.(*issues_model.Comment)
+}
+
+func (o *reviewComment) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoReviewComment.ID)
+}
+
+func (o *reviewComment) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func patch2diff(patch string) string {
+ split := strings.Split(patch, "\n@@")
+ if len(split) == 2 {
+ return "@@" + split[1]
+ }
+ return patch
+}
+
+func (o *reviewComment) ToFormat() f3.Interface {
+ if o.forgejoReviewComment == nil {
+ return o.NewFormat()
+ }
+
+ return &f3.ReviewComment{
+ Common: f3.NewCommon(o.GetNativeID()),
+ PosterID: f3_tree.NewUserReference(o.forgejoReviewComment.Poster.ID),
+ Content: o.forgejoReviewComment.Content,
+ TreePath: o.forgejoReviewComment.TreePath,
+ DiffHunk: patch2diff(o.forgejoReviewComment.PatchQuoted),
+ Line: int(o.forgejoReviewComment.Line),
+ CommitID: o.forgejoReviewComment.CommitSHA,
+ CreatedAt: o.forgejoReviewComment.CreatedUnix.AsTime(),
+ UpdatedAt: o.forgejoReviewComment.UpdatedUnix.AsTime(),
+ }
+}
+
+func (o *reviewComment) FromFormat(content f3.Interface) {
+ reviewComment := content.(*f3.ReviewComment)
+ o.forgejoReviewComment = &issues_model.Comment{
+ ID: f3_util.ParseInt(reviewComment.GetID()),
+ PosterID: reviewComment.PosterID.GetIDAsInt(),
+ Poster: &user_model.User{
+ ID: reviewComment.PosterID.GetIDAsInt(),
+ },
+ TreePath: reviewComment.TreePath,
+ Content: reviewComment.Content,
+ // a hunk misses the patch header but it is never used so do not bother
+ // reconstructing it
+ Patch: reviewComment.DiffHunk,
+ PatchQuoted: reviewComment.DiffHunk,
+ Line: int64(reviewComment.Line),
+ CommitSHA: reviewComment.CommitID,
+ CreatedUnix: timeutil.TimeStamp(reviewComment.CreatedAt.Unix()),
+ UpdatedUnix: timeutil.TimeStamp(reviewComment.UpdatedAt.Unix()),
+ }
+}
+
+func (o *reviewComment) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ id := node.GetID().Int64()
+
+ reviewComment, err := issues_model.GetCommentByID(ctx, id)
+ if issues_model.IsErrCommentNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("reviewComment %v %w", id, err))
+ }
+ if err := reviewComment.LoadPoster(ctx); err != nil {
+ panic(fmt.Errorf("LoadPoster %v %w", *reviewComment, err))
+ }
+ o.forgejoReviewComment = reviewComment
+ return true
+}
+
+func (o *reviewComment) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoReviewComment.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoReviewComment.ID).Cols("content").Update(o.forgejoReviewComment); err != nil {
+ panic(fmt.Errorf("UpdateReviewCommentCols: %v %v", o.forgejoReviewComment, err))
+ }
+}
+
+func (o *reviewComment) Put(ctx context.Context) generic.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ sess := db.GetEngine(ctx)
+
+ if _, err := sess.NoAutoTime().Insert(o.forgejoReviewComment); err != nil {
+ panic(err)
+ }
+ o.Trace("reviewComment created %d", o.forgejoReviewComment.ID)
+ return generic.NewNodeID(o.forgejoReviewComment.ID)
+}
+
+func (o *reviewComment) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ if err := issues_model.DeleteComment(ctx, o.forgejoReviewComment); err != nil {
+ panic(err)
+ }
+}
+
+func newReviewComment() generic.NodeDriverInterface {
+ return &reviewComment{}
+}
diff --git a/services/f3/driver/reviewcomments.go b/services/f3/driver/reviewcomments.go
new file mode 100644
index 0000000..e11aaa4
--- /dev/null
+++ b/services/f3/driver/reviewcomments.go
@@ -0,0 +1,43 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type reviewComments struct {
+ container
+}
+
+func (o *reviewComments) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ id := f3_tree.GetReviewID(o.GetNode())
+
+ sess := db.GetEngine(ctx).
+ Table("comment").
+ Where("`review_id` = ? AND `type` = ?", id, issues_model.CommentTypeCode)
+ if page != 0 {
+ sess = db.SetSessionPagination(sess, &db.ListOptions{Page: page, PageSize: pageSize})
+ }
+ forgejoReviewComments := make([]*issues_model.Comment, 0, pageSize)
+ if err := sess.Find(&forgejoReviewComments); err != nil {
+ panic(fmt.Errorf("error while listing reviewComments: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(forgejoReviewComments...)...)
+}
+
+func newReviewComments() generic.NodeDriverInterface {
+ return &reviewComments{}
+}
diff --git a/services/f3/driver/reviews.go b/services/f3/driver/reviews.go
new file mode 100644
index 0000000..a20d574
--- /dev/null
+++ b/services/f3/driver/reviews.go
@@ -0,0 +1,49 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type reviews struct {
+ container
+}
+
+func (o *reviews) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ project := f3_tree.GetProjectID(o.GetNode())
+ pullRequest := f3_tree.GetPullRequestID(o.GetNode())
+
+ issue, err := issues_model.GetIssueByIndex(ctx, project, pullRequest)
+ if err != nil {
+ panic(fmt.Errorf("GetIssueByIndex %v %w", pullRequest, err))
+ }
+
+ sess := db.GetEngine(ctx).
+ Table("review").
+ Where("`issue_id` = ?", issue.ID)
+ if page != 0 {
+ sess = db.SetSessionPagination(sess, &db.ListOptions{Page: page, PageSize: pageSize})
+ }
+ forgejoReviews := make([]*issues_model.Review, 0, pageSize)
+ if err := sess.Find(&forgejoReviews); err != nil {
+ panic(fmt.Errorf("error while listing reviews: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(forgejoReviews...)...)
+}
+
+func newReviews() generic.NodeDriverInterface {
+ return &reviews{}
+}
diff --git a/services/f3/driver/root.go b/services/f3/driver/root.go
new file mode 100644
index 0000000..0e8a67f
--- /dev/null
+++ b/services/f3/driver/root.go
@@ -0,0 +1,41 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type root struct {
+ generic.NullDriver
+
+ content f3.Interface
+}
+
+func newRoot(content f3.Interface) generic.NodeDriverInterface {
+ return &root{
+ content: content,
+ }
+}
+
+func (o *root) FromFormat(content f3.Interface) {
+ o.content = content
+}
+
+func (o *root) ToFormat() f3.Interface {
+ return o.content
+}
+
+func (o *root) Get(context.Context) bool { return true }
+
+func (o *root) Put(context.Context) generic.NodeID {
+ return generic.NilID
+}
+
+func (o *root) Patch(context.Context) {
+}
diff --git a/services/f3/driver/tests/init.go b/services/f3/driver/tests/init.go
new file mode 100644
index 0000000..d7bf23a
--- /dev/null
+++ b/services/f3/driver/tests/init.go
@@ -0,0 +1,15 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package tests
+
+import (
+ driver_options "code.gitea.io/gitea/services/f3/driver/options"
+
+ tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
+)
+
+func init() {
+ tests_forge.RegisterFactory(driver_options.Name, newForgeTest)
+}
diff --git a/services/f3/driver/tests/new.go b/services/f3/driver/tests/new.go
new file mode 100644
index 0000000..2e3dfc3
--- /dev/null
+++ b/services/f3/driver/tests/new.go
@@ -0,0 +1,39 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package tests
+
+import (
+ "testing"
+
+ driver_options "code.gitea.io/gitea/services/f3/driver/options"
+
+ "code.forgejo.org/f3/gof3/v3/options"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ forge_test "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
+)
+
+type forgeTest struct {
+ forge_test.Base
+}
+
+func (o *forgeTest) NewOptions(t *testing.T) options.Interface {
+ return newTestOptions(t)
+}
+
+func (o *forgeTest) GetExceptions() []generic.Kind {
+ return []generic.Kind{}
+}
+
+func (o *forgeTest) GetNonTestUsers() []string {
+ return []string{
+ "user1",
+ }
+}
+
+func newForgeTest() forge_test.Interface {
+ t := &forgeTest{}
+ t.SetName(driver_options.Name)
+ return t
+}
diff --git a/services/f3/driver/tests/options.go b/services/f3/driver/tests/options.go
new file mode 100644
index 0000000..adaa1da
--- /dev/null
+++ b/services/f3/driver/tests/options.go
@@ -0,0 +1,21 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package tests
+
+import (
+ "testing"
+
+ forgejo_log "code.gitea.io/gitea/modules/log"
+ driver_options "code.gitea.io/gitea/services/f3/driver/options"
+ "code.gitea.io/gitea/services/f3/util"
+
+ "code.forgejo.org/f3/gof3/v3/options"
+)
+
+func newTestOptions(_ *testing.T) options.Interface {
+ o := options.GetFactory(driver_options.Name)().(*driver_options.Options)
+ o.SetLogger(util.NewF3Logger(nil, forgejo_log.GetLogger(forgejo_log.DEFAULT)))
+ return o
+}
diff --git a/services/f3/driver/topic.go b/services/f3/driver/topic.go
new file mode 100644
index 0000000..16b2eb3
--- /dev/null
+++ b/services/f3/driver/topic.go
@@ -0,0 +1,111 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &topic{}
+
+type topic struct {
+ common
+
+ forgejoTopic *repo_model.Topic
+}
+
+func (o *topic) SetNative(topic any) {
+ o.forgejoTopic = topic.(*repo_model.Topic)
+}
+
+func (o *topic) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoTopic.ID)
+}
+
+func (o *topic) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *topic) ToFormat() f3.Interface {
+ if o.forgejoTopic == nil {
+ return o.NewFormat()
+ }
+
+ return &f3.Topic{
+ Common: f3.NewCommon(o.GetNativeID()),
+ Name: o.forgejoTopic.Name,
+ }
+}
+
+func (o *topic) FromFormat(content f3.Interface) {
+ topic := content.(*f3.Topic)
+ o.forgejoTopic = &repo_model.Topic{
+ ID: f3_util.ParseInt(topic.GetID()),
+ Name: topic.Name,
+ }
+}
+
+func (o *topic) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ id := node.GetID().Int64()
+
+ if has, err := db.GetEngine(ctx).Where("ID = ?", id).Get(o.forgejoTopic); err != nil {
+ panic(fmt.Errorf("topic %v %w", id, err))
+ } else if !has {
+ return false
+ }
+
+ return true
+}
+
+func (o *topic) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoTopic.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoTopic.ID).Cols("name").Update(o.forgejoTopic); err != nil {
+ panic(fmt.Errorf("UpdateTopicCols: %v %v", o.forgejoTopic, err))
+ }
+}
+
+func (o *topic) Put(ctx context.Context) generic.NodeID {
+ sess := db.GetEngine(ctx)
+
+ if _, err := sess.Insert(o.forgejoTopic); err != nil {
+ panic(err)
+ }
+ o.Trace("topic created %d", o.forgejoTopic.ID)
+ return generic.NewNodeID(o.forgejoTopic.ID)
+}
+
+func (o *topic) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ sess := db.GetEngine(ctx)
+
+ if _, err := sess.Delete(&repo_model.RepoTopic{
+ TopicID: o.forgejoTopic.ID,
+ }); err != nil {
+ panic(fmt.Errorf("Delete RepoTopic for %v %v", o.forgejoTopic, err))
+ }
+
+ if _, err := sess.Delete(o.forgejoTopic); err != nil {
+ panic(fmt.Errorf("Delete Topic %v %v", o.forgejoTopic, err))
+ }
+}
+
+func newTopic() generic.NodeDriverInterface {
+ return &topic{}
+}
diff --git a/services/f3/driver/topics.go b/services/f3/driver/topics.go
new file mode 100644
index 0000000..2685a47
--- /dev/null
+++ b/services/f3/driver/topics.go
@@ -0,0 +1,41 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type topics struct {
+ container
+}
+
+func (o *topics) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ pageSize := o.getPageSize()
+
+ sess := db.GetEngine(ctx)
+ if page != 0 {
+ sess = db.SetSessionPagination(sess, &db.ListOptions{Page: page, PageSize: pageSize})
+ }
+ sess = sess.Select("`topic`.*")
+ topics := make([]*repo_model.Topic, 0, pageSize)
+
+ if err := sess.Find(&topics); err != nil {
+ panic(fmt.Errorf("error while listing topics: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(topics...)...)
+}
+
+func newTopics() generic.NodeDriverInterface {
+ return &topics{}
+}
diff --git a/services/f3/driver/tree.go b/services/f3/driver/tree.go
new file mode 100644
index 0000000..0302ed7
--- /dev/null
+++ b/services/f3/driver/tree.go
@@ -0,0 +1,104 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ forgejo_options "code.gitea.io/gitea/services/f3/driver/options"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type treeDriver struct {
+ generic.NullTreeDriver
+
+ options *forgejo_options.Options
+}
+
+func (o *treeDriver) Init() {
+ o.NullTreeDriver.Init()
+}
+
+func (o *treeDriver) Factory(ctx context.Context, kind generic.Kind) generic.NodeDriverInterface {
+ switch kind {
+ case f3_tree.KindForge:
+ return newForge()
+ case f3_tree.KindOrganizations:
+ return newOrganizations()
+ case f3_tree.KindOrganization:
+ return newOrganization()
+ case f3_tree.KindUsers:
+ return newUsers()
+ case f3_tree.KindUser:
+ return newUser()
+ case f3_tree.KindProjects:
+ return newProjects()
+ case f3_tree.KindProject:
+ return newProject()
+ case f3_tree.KindIssues:
+ return newIssues()
+ case f3_tree.KindIssue:
+ return newIssue()
+ case f3_tree.KindComments:
+ return newComments()
+ case f3_tree.KindComment:
+ return newComment()
+ case f3_tree.KindAssets:
+ return newAssets()
+ case f3_tree.KindAsset:
+ return newAsset()
+ case f3_tree.KindLabels:
+ return newLabels()
+ case f3_tree.KindLabel:
+ return newLabel()
+ case f3_tree.KindReactions:
+ return newReactions()
+ case f3_tree.KindReaction:
+ return newReaction()
+ case f3_tree.KindReviews:
+ return newReviews()
+ case f3_tree.KindReview:
+ return newReview()
+ case f3_tree.KindReviewComments:
+ return newReviewComments()
+ case f3_tree.KindReviewComment:
+ return newReviewComment()
+ case f3_tree.KindMilestones:
+ return newMilestones()
+ case f3_tree.KindMilestone:
+ return newMilestone()
+ case f3_tree.KindPullRequests:
+ return newPullRequests()
+ case f3_tree.KindPullRequest:
+ return newPullRequest()
+ case f3_tree.KindReleases:
+ return newReleases()
+ case f3_tree.KindRelease:
+ return newRelease()
+ case f3_tree.KindTopics:
+ return newTopics()
+ case f3_tree.KindTopic:
+ return newTopic()
+ case f3_tree.KindRepositories:
+ return newRepositories()
+ case f3_tree.KindRepository:
+ return newRepository(ctx)
+ case generic.KindRoot:
+ return newRoot(o.GetTree().(f3_tree.TreeInterface).NewFormat(kind))
+ default:
+ panic(fmt.Errorf("unexpected kind %s", kind))
+ }
+}
+
+func newTreeDriver(tree generic.TreeInterface, anyOptions any) generic.TreeDriverInterface {
+ driver := &treeDriver{
+ options: anyOptions.(*forgejo_options.Options),
+ }
+ driver.Init()
+ return driver
+}
diff --git a/services/f3/driver/user.go b/services/f3/driver/user.go
new file mode 100644
index 0000000..221b06e
--- /dev/null
+++ b/services/f3/driver/user.go
@@ -0,0 +1,128 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/optional"
+ user_service "code.gitea.io/gitea/services/user"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+)
+
+var _ f3_tree.ForgeDriverInterface = &user{}
+
+type user struct {
+ common
+
+ forgejoUser *user_model.User
+}
+
+func getSystemUserByName(name string) *user_model.User {
+ switch name {
+ case user_model.GhostUserName:
+ return user_model.NewGhostUser()
+ case user_model.ActionsUserName:
+ return user_model.NewActionsUser()
+ default:
+ return nil
+ }
+}
+
+func (o *user) SetNative(user any) {
+ o.forgejoUser = user.(*user_model.User)
+}
+
+func (o *user) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoUser.ID)
+}
+
+func (o *user) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *user) ToFormat() f3.Interface {
+ if o.forgejoUser == nil {
+ return o.NewFormat()
+ }
+ return &f3.User{
+ Common: f3.NewCommon(fmt.Sprintf("%d", o.forgejoUser.ID)),
+ UserName: o.forgejoUser.Name,
+ Name: o.forgejoUser.FullName,
+ Email: o.forgejoUser.Email,
+ IsAdmin: o.forgejoUser.IsAdmin,
+ Password: o.forgejoUser.Passwd,
+ }
+}
+
+func (o *user) FromFormat(content f3.Interface) {
+ user := content.(*f3.User)
+ o.forgejoUser = &user_model.User{
+ Type: user_model.UserTypeRemoteUser,
+ ID: f3_util.ParseInt(user.GetID()),
+ Name: user.UserName,
+ FullName: user.Name,
+ Email: user.Email,
+ IsAdmin: user.IsAdmin,
+ Passwd: user.Password,
+ }
+}
+
+func (o *user) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+ id := node.GetID().Int64()
+ u, err := user_model.GetPossibleUserByID(ctx, id)
+ if user_model.IsErrUserNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("user %v %w", id, err))
+ }
+ o.forgejoUser = u
+ return true
+}
+
+func (o *user) Patch(context.Context) {
+}
+
+func (o *user) Put(ctx context.Context) generic.NodeID {
+ if user := getSystemUserByName(o.forgejoUser.Name); user != nil {
+ return generic.NewNodeID(user.ID)
+ }
+
+ o.forgejoUser.LowerName = strings.ToLower(o.forgejoUser.Name)
+ o.Trace("%v", *o.forgejoUser)
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: optional.Some(true),
+ }
+ err := user_model.CreateUser(ctx, o.forgejoUser, overwriteDefault)
+ if err != nil {
+ panic(err)
+ }
+
+ return generic.NewNodeID(o.forgejoUser.ID)
+}
+
+func (o *user) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ if err := user_service.DeleteUser(ctx, o.forgejoUser, true); err != nil {
+ panic(err)
+ }
+}
+
+func newUser() generic.NodeDriverInterface {
+ return &user{}
+}
diff --git a/services/f3/driver/users.go b/services/f3/driver/users.go
new file mode 100644
index 0000000..92ed0bc
--- /dev/null
+++ b/services/f3/driver/users.go
@@ -0,0 +1,48 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// Copyright Loïc Dachary <loic@dachary.org>
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ user_model "code.gitea.io/gitea/models/user"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type users struct {
+ container
+}
+
+func (o *users) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ sess := db.GetEngine(ctx).In("type", user_model.UserTypeIndividual, user_model.UserTypeRemoteUser)
+ if page != 0 {
+ sess = db.SetSessionPagination(sess, &db.ListOptions{Page: page, PageSize: o.getPageSize()})
+ }
+ sess = sess.Select("`user`.*")
+ users := make([]*user_model.User, 0, o.getPageSize())
+
+ if err := sess.Find(&users); err != nil {
+ panic(fmt.Errorf("error while listing users: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(users...)...)
+}
+
+func (o *users) GetIDFromName(ctx context.Context, name string) generic.NodeID {
+ user, err := user_model.GetUserByName(ctx, name)
+ if err != nil {
+ panic(fmt.Errorf("GetUserByName: %v", err))
+ }
+
+ return generic.NewNodeID(user.ID)
+}
+
+func newUsers() generic.NodeDriverInterface {
+ return &users{}
+}
diff --git a/services/f3/util/logger.go b/services/f3/util/logger.go
new file mode 100644
index 0000000..21d8d6b
--- /dev/null
+++ b/services/f3/util/logger.go
@@ -0,0 +1,97 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// SPDX-License-Identifier: MIT
+
+package util
+
+import (
+ "fmt"
+
+ forgejo_log "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/migration"
+
+ "code.forgejo.org/f3/gof3/v3/logger"
+)
+
+type f3Logger struct {
+ m migration.Messenger
+ l forgejo_log.Logger
+}
+
+func (o *f3Logger) Message(message string, args ...any) {
+ if o.m != nil {
+ o.m(message, args...)
+ }
+}
+
+func (o *f3Logger) SetLevel(level logger.Level) {
+}
+
+func forgejoLevelToF3Level(level forgejo_log.Level) logger.Level {
+ switch level {
+ case forgejo_log.TRACE:
+ return logger.Trace
+ case forgejo_log.DEBUG:
+ return logger.Debug
+ case forgejo_log.INFO:
+ return logger.Info
+ case forgejo_log.WARN:
+ return logger.Warn
+ case forgejo_log.ERROR:
+ return logger.Error
+ case forgejo_log.FATAL:
+ return logger.Fatal
+ default:
+ panic(fmt.Errorf("unexpected level %d", level))
+ }
+}
+
+func f3LevelToForgejoLevel(level logger.Level) forgejo_log.Level {
+ switch level {
+ case logger.Trace:
+ return forgejo_log.TRACE
+ case logger.Debug:
+ return forgejo_log.DEBUG
+ case logger.Info:
+ return forgejo_log.INFO
+ case logger.Warn:
+ return forgejo_log.WARN
+ case logger.Error:
+ return forgejo_log.ERROR
+ case logger.Fatal:
+ return forgejo_log.FATAL
+ default:
+ panic(fmt.Errorf("unexpected level %d", level))
+ }
+}
+
+func (o *f3Logger) GetLevel() logger.Level {
+ return forgejoLevelToF3Level(o.l.GetLevel())
+}
+
+func (o *f3Logger) Log(skip int, level logger.Level, format string, args ...any) {
+ o.l.Log(skip+1, f3LevelToForgejoLevel(level), format, args...)
+}
+
+func (o *f3Logger) Trace(message string, args ...any) {
+ o.l.Log(1, forgejo_log.TRACE, message, args...)
+}
+
+func (o *f3Logger) Debug(message string, args ...any) {
+ o.l.Log(1, forgejo_log.DEBUG, message, args...)
+}
+func (o *f3Logger) Info(message string, args ...any) { o.l.Log(1, forgejo_log.INFO, message, args...) }
+func (o *f3Logger) Warn(message string, args ...any) { o.l.Log(1, forgejo_log.WARN, message, args...) }
+func (o *f3Logger) Error(message string, args ...any) {
+ o.l.Log(1, forgejo_log.ERROR, message, args...)
+}
+
+func (o *f3Logger) Fatal(message string, args ...any) {
+ o.l.Log(1, forgejo_log.FATAL, message, args...)
+}
+
+func NewF3Logger(messenger migration.Messenger, logger forgejo_log.Logger) logger.Interface {
+ return &f3Logger{
+ m: messenger,
+ l: logger,
+ }
+}
diff --git a/services/f3/util/logger_test.go b/services/f3/util/logger_test.go
new file mode 100644
index 0000000..db880aa
--- /dev/null
+++ b/services/f3/util/logger_test.go
@@ -0,0 +1,89 @@
+// Copyright Earl Warren <contact@earl-warren.org>
+// SPDX-License-Identifier: MIT
+
+package util
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ forgejo_log "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/test"
+
+ "code.forgejo.org/f3/gof3/v3/logger"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestF3UtilMessage(t *testing.T) {
+ expected := "EXPECTED MESSAGE"
+ var actual string
+ logger := NewF3Logger(func(message string, args ...any) {
+ actual = fmt.Sprintf(message, args...)
+ }, nil)
+ logger.Message("EXPECTED %s", "MESSAGE")
+ assert.EqualValues(t, expected, actual)
+}
+
+func TestF3UtilLogger(t *testing.T) {
+ for _, testCase := range []struct {
+ level logger.Level
+ call func(logger.MessageInterface, string, ...any)
+ }{
+ {level: logger.Trace, call: func(logger logger.MessageInterface, message string, args ...any) { logger.Trace(message, args...) }},
+ {level: logger.Debug, call: func(logger logger.MessageInterface, message string, args ...any) { logger.Debug(message, args...) }},
+ {level: logger.Info, call: func(logger logger.MessageInterface, message string, args ...any) { logger.Info(message, args...) }},
+ {level: logger.Warn, call: func(logger logger.MessageInterface, message string, args ...any) { logger.Warn(message, args...) }},
+ {level: logger.Error, call: func(logger logger.MessageInterface, message string, args ...any) { logger.Error(message, args...) }},
+ {level: logger.Fatal, call: func(logger logger.MessageInterface, message string, args ...any) { logger.Fatal(message, args...) }},
+ } {
+ t.Run(testCase.level.String(), func(t *testing.T) {
+ testLoggerCase(t, testCase.level, testCase.call)
+ })
+ }
+}
+
+func testLoggerCase(t *testing.T, level logger.Level, loggerFunc func(logger.MessageInterface, string, ...any)) {
+ lc, cleanup := test.NewLogChecker(forgejo_log.DEFAULT, f3LevelToForgejoLevel(level))
+ defer cleanup()
+ stopMark := "STOP"
+ lc.StopMark(stopMark)
+ filtered := []string{
+ "MESSAGE HERE",
+ }
+ moreVerbose := logger.MoreVerbose(level)
+ if moreVerbose != nil {
+ filtered = append(filtered, "MESSAGE MORE VERBOSE")
+ }
+ lessVerbose := logger.LessVerbose(level)
+ if lessVerbose != nil {
+ filtered = append(filtered, "MESSAGE LESS VERBOSE")
+ }
+ lc.Filter(filtered...)
+
+ logger := NewF3Logger(nil, forgejo_log.GetLogger(forgejo_log.DEFAULT))
+ loggerFunc(logger, "MESSAGE %s", "HERE")
+ if moreVerbose != nil {
+ logger.Log(1, *moreVerbose, "MESSAGE %s", "MORE VERBOSE")
+ }
+ if lessVerbose != nil {
+ logger.Log(1, *lessVerbose, "MESSAGE %s", "LESS VERBOSE")
+ }
+ logger.Fatal(stopMark)
+
+ logFiltered, logStopped := lc.Check(5 * time.Second)
+ assert.True(t, logStopped)
+ i := 0
+ assert.True(t, logFiltered[i], filtered[i])
+ if moreVerbose != nil {
+ i++
+ require.Greater(t, len(logFiltered), i)
+ assert.False(t, logFiltered[i], filtered[i])
+ }
+ if lessVerbose != nil {
+ i++
+ require.Greater(t, len(logFiltered), i)
+ assert.True(t, logFiltered[i], filtered[i])
+ }
+}