Verified Commit 64461229 authored by Loïc Dachary's avatar Loïc Dachary
Browse files

gitea: implement reviews

parent 0c2824fd
Pipeline #1080 passed with stage
in 2 minutes and 52 seconds
......@@ -15,14 +15,16 @@
package gofff
const (
ForeignReferenceUnknown = "Unknown"
ForeignReferenceProject = "Project"
ForeignReferenceMilestone = "Milestone"
ForeignReferenceLabel = "Label"
ForeignReferenceIssue = "Issue"
ForeignReferenceComment = "Comment"
ForeignReferenceRelease = "Release"
ForeignReferenceAttachment = "Attachment"
ForeignReferenceUnknown = "Unknown"
ForeignReferenceProject = "Project"
ForeignReferenceMilestone = "Milestone"
ForeignReferenceLabel = "Label"
ForeignReferenceIssue = "Issue"
ForeignReferenceComment = "Comment"
ForeignReferenceRelease = "Release"
ForeignReferenceAttachment = "Attachment"
ForeignReferenceReview = "Review"
ForeignReferenceReviewComment = "ReviewComment"
)
type ForeignReference struct {
......
......@@ -69,7 +69,10 @@ func migrate(ctx context.Context, downloader, uploader gofff.ForgeInterface, log
uploader.CreateComments(downloader.GetComments(pr)...)
}
}
// reviews go here
util.MaybeTerminate(ctx)
for _, pr := range prs {
uploader.CreateReviews(downloader.GetReviews(pr)...)
}
}
if features.Releases {
......
......@@ -52,11 +52,13 @@ type File struct {
Options *Options
project *format.Project
commentsPath string
reviewsPath string
}
func (f *File) Init(options *Options) {
f.Options = options
f.commentsPath = "comments"
f.commentsPath = "reviews"
}
func (f *File) GetDirectory() string {
......@@ -187,6 +189,25 @@ func (f *File) SupportGetRepoComments() bool {
return false
}
func (f *File) GetReviews(reviewable format.Reviewable) []*format.Review {
return MaybeGetBeans[format.Review](f, filepath.Join(f.reviewsPath, fmt.Sprintf("%d.json", reviewable.GetID())))
}
func (f *File) CreateReviews(reviews ...*format.Review) {
reviewsMap := make(map[int64][]*format.Review, len(reviews))
for _, review := range reviews {
reviewsMap[review.IssueIndex] = append(reviewsMap[review.IssueIndex], review)
}
if err := os.MkdirAll(filepath.Join(f.GetDirectory(), f.reviewsPath), 0o770); err != nil {
panic(err)
}
for issueIndex, issueReviews := range reviewsMap {
CreateBeans[format.Review](f, filepath.Join(f.reviewsPath, fmt.Sprintf("%d.json", issueIndex)), issueReviews...)
}
}
func (f *File) GetPullRequests() []*format.PullRequest {
return GetBeans[format.PullRequest](f, "pull_request.json")
}
......
......@@ -75,5 +75,10 @@ func TestFile(t *testing.T) {
copy.CreateComments(original.GetComments(issues[0])...)
assert.EqualValues(t, original.GetComments(issues[0]), copy.GetComments(issues[0]))
prs := original.GetPullRequests()
assert.NotEmpty(t, original.GetReviews(prs[0]))
copy.CreateReviews(original.GetReviews(prs[0])...)
assert.EqualValues(t, original.GetReviews(prs[0]), copy.GetReviews(prs[0]))
fixture.AssertEquals(original, copy)
}
......@@ -293,6 +293,48 @@ func (f *Fixture) CreateComment(project *format.Project, commentable format.Comm
}
}
func (f *Fixture) CreateReview(project *format.Project, reviewable format.Reviewable, repositoryDirectory string, user User) (review format.Review) {
now := time.Now()
tick := time.Duration(1)
tick++
reviewCreated := now.Add(tick)
tick++
commentCreated := now.Add(tick)
tick++
commentUpdated := now.Add(tick)
featureSha := f.GetRepositorySha(repositoryDirectory, "feature")
comment := &format.ReviewComment{
ID: 8435,
// InReplyTo
Content: "comment content",
TreePath: "README.md",
DiffHunk: "hunk",
// Position
Line: 0,
CommitID: featureSha,
PosterID: user.ID,
// Reactions
CreatedAt: commentCreated,
UpdatedAt: commentUpdated,
}
return format.Review{
ID: 4593,
IssueIndex: reviewable.GetID(),
ReviewerID: user.ID,
ReviewerName: user.Name,
Official: true,
CommitID: featureSha,
Content: "the review content",
CreatedAt: reviewCreated,
State: format.ReviewStateCommented,
Comments: []*format.ReviewComment{comment},
}
}
func (f *Fixture) CreateIssue(project *format.Project, labels []*format.Label, user User) (issue format.Issue) {
now := time.Now()
tick := time.Duration(1)
......@@ -429,4 +471,7 @@ func (f *Fixture) CreateProject() {
comment := f.CreateComment(project, &issue, user1)
f.f.CreateComments(&comment)
review := f.CreateReview(project, &pr, repositoryDirectory, user1)
f.f.CreateReviews(&review)
}
This diff is collapsed.
......@@ -102,6 +102,7 @@ type Gitea struct {
milestoneProvider *MilestoneProvider
labelProvider *LabelProvider
issueProvider *IssueProvider
reviewProvider *ReviewProvider
commentProvider *CommentProvider
pullRequestProvider *PullRequestProvider
releaseProvider *ReleaseProvider
......@@ -126,6 +127,7 @@ func (g *Gitea) Init(options *Options) {
g.labelProvider = (&LabelProvider{}).Init(g)
g.issueProvider = (&IssueProvider{}).Init(g)
g.commentProvider = (&CommentProvider{}).Init(g)
g.reviewProvider = (&ReviewProvider{}).Init(g)
g.pullRequestProvider = (&PullRequestProvider{}).Init(g)
g.releaseProvider = (&ReleaseProvider{}).Init(g)
g.topicProvider = (&TopicProvider{}).Init(g)
......@@ -191,6 +193,11 @@ func (g Gitea) GetCommentProvider(project *Project) *CommentProvider {
return g.commentProvider
}
func (g Gitea) GetReviewProvider(project *Project) *ReviewProvider {
g.reviewProvider.project = project
return g.reviewProvider
}
func (g Gitea) GetPullRequestProvider(project *Project) *PullRequestProvider {
g.pullRequestProvider.project = project
return g.pullRequestProvider
......
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package gitea
import (
"lab.forgefriends.org/friendlyforgeformat/gofff"
"lab.forgefriends.org/friendlyforgeformat/gofff/format"
"lab.forgefriends.org/friendlyforgeformat/gofff/util"
gitea_sdk "code.gitea.io/sdk/gitea"
)
type Review struct {
gitea_sdk.PullReview
Comments []*gitea_sdk.PullReviewComment
ReviewableIndex int64
}
func (o Review) GetID() format.ReviewIndexType {
return format.ReviewIndexType{
PullRequestID: o.ReviewableIndex,
ReviewID: o.PullReview.ID,
}
}
func (o *Review) SetID(id format.ReviewIndexType) {
o.PullReview.ID = id.ReviewID
o.ReviewableIndex = id.PullRequestID
}
func (o Review) Equals(other Review) bool {
return o.PullReview.Body == other.PullReview.Body
}
func (o *Review) ToFormat() *format.Review {
comments := make([]*format.ReviewComment, 0, len(o.Comments))
for _, comment := range o.Comments {
line := int(comment.LineNum)
if comment.OldLineNum > 0 {
line = int(comment.OldLineNum) * -1
}
comments = append(comments, &format.ReviewComment{
ID: comment.ID,
// InReplyTo
Content: comment.Body,
TreePath: comment.Path,
DiffHunk: comment.DiffHunk,
// Position
Line: line,
CommitID: comment.CommitID,
PosterID: comment.Reviewer.ID,
// Reactions
CreatedAt: comment.Created,
UpdatedAt: comment.Updated,
})
}
return &format.Review{
ID: o.PullReview.ID,
IssueIndex: o.ReviewableIndex,
ReviewerID: o.PullReview.Reviewer.ID,
ReviewerName: o.PullReview.Reviewer.UserName,
Official: o.PullReview.Official,
CommitID: o.PullReview.CommitID,
Content: o.PullReview.Body,
CreatedAt: o.PullReview.Submitted,
State: string(o.State),
Comments: comments,
}
}
func (o *Review) FromFormat(review format.Review) {
comments := make([]*gitea_sdk.PullReviewComment, 0, len(review.Comments))
for _, comment := range review.Comments {
comments = append(comments, &gitea_sdk.PullReviewComment{
ID: comment.ID,
// InReplyTo
Body: comment.Content,
Path: comment.TreePath,
DiffHunk: comment.DiffHunk,
// Position
LineNum: uint64(comment.Line),
CommitID: comment.CommitID,
Reviewer: &gitea_sdk.User{
ID: comment.PosterID,
},
// Reactions
Created: comment.CreatedAt,
Updated: comment.UpdatedAt,
})
}
*o = Review{
PullReview: gitea_sdk.PullReview{
ID: review.ID,
Reviewer: &gitea_sdk.User{
ID: review.ReviewerID,
UserName: review.ReviewerName,
},
Official: review.Official,
CommitID: review.CommitID,
Body: review.Content,
Submitted: review.CreatedAt,
State: gitea_sdk.ReviewStateType(review.State),
},
Comments: comments,
ReviewableIndex: review.IssueIndex,
}
}
type ReviewProvider struct {
g *Gitea
project *Project
}
type ForeignReview gitea_sdk.PullReview
func (o *ForeignReview) GetID() int64 {
return o.ID
}
func (o *ForeignReview) SetID(id int64) {
o.ID = id
}
func (o *ReviewProvider) ToFormat(review *Review) *format.Review {
f := review.ToFormat()
o.g.GetDb().LocalToForeign(o.project.ID, gofff.ForeignReferenceReview, (*ForeignReview)(&review.PullReview), f)
return f
}
func (o *ReviewProvider) FromFormat(review *Review, f *format.Review) {
review.FromFormat(*f)
o.g.GetDb().ForeignToLocal(o.project.ID, gofff.ForeignReferenceReview, f, (*ForeignReview)(&review.PullReview))
}
func (o *ReviewProvider) StoreForeignReference(review *Review, f *format.Review) {
o.g.GetDb().Store(o.project.ID, gofff.ForeignReferenceReview, (*ForeignReview)(&review.PullReview), f)
}
func (o *ReviewProvider) Init(gitea *Gitea) *ReviewProvider {
o.g = gitea
return o
}
func (o *ReviewProvider) GetReviews(reviewable format.Reviewable) []Review {
allReviews := make([]Review, 0, o.g.perPage)
for page := 1; ; page++ {
util.MaybeTerminate(o.g.ctx)
reviews, _, err := o.g.client.ListPullReviews(o.project.Owner.UserName, o.project.Name, reviewable.GetID(), gitea_sdk.ListPullReviewsOptions{
ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: o.g.perPage},
})
if err != nil {
panic(err)
}
for _, review := range reviews {
comments, _, err := o.g.client.ListPullReviewComments(o.project.Owner.UserName, o.project.Name, reviewable.GetID(), review.ID)
if err != nil {
panic(err)
}
allReviews = append(allReviews, Review{
PullReview: *review,
Comments: comments,
ReviewableIndex: reviewable.GetID(),
})
}
if len(reviews) < o.g.perPage {
break
}
}
return allReviews
}
func (o *ReviewProvider) Get(id format.ReviewIndexType) *Review {
review, _, err := o.g.client.GetPullReview(o.project.Owner.UserName, o.project.Name, id.PullRequestID, id.ReviewID)
if err != nil {
panic(err)
}
comments, _, err := o.g.client.ListPullReviewComments(o.project.Owner.UserName, o.project.Name, id.PullRequestID, id.ReviewID)
if err != nil {
panic(err)
}
return &Review{
PullReview: *review,
Comments: comments,
ReviewableIndex: id.PullRequestID,
}
}
func (o *ReviewProvider) Put(review *Review) *Review {
r, _, err := o.g.client.CreatePullReview(o.project.Owner.UserName, o.project.Name, review.ReviewableIndex, gitea_sdk.CreatePullReviewOptions{
Body: review.Body,
})
if err != nil {
panic(err)
}
return o.Get(format.ReviewIndexType{
PullRequestID: review.ReviewableIndex,
ReviewID: r.ID,
})
}
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package gitea
import (
"testing"
"time"
"lab.forgefriends.org/friendlyforgeformat/gofff/format"
"lab.forgefriends.org/friendlyforgeformat/gofff/util"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/stretchr/testify/assert"
)
func TestReview(t *testing.T) {
f := newPrFixture(t)
defer f.delete()
pr := &PullRequest{
PullRequest: gitea_sdk.PullRequest{
Title: "title",
Body: "body",
Base: &gitea_sdk.PRBranchInfo{
Ref: f.baseRef,
Repository: (*gitea_sdk.Repository)(f.fromProject),
},
Head: &gitea_sdk.PRBranchInfo{
Ref: f.headRef,
Repository: (*gitea_sdk.Repository)(f.fromProject),
},
},
}
prProvider := f.rootGitea.g.GetPullRequestProvider(f.fromProject)
var prInserted *PullRequest
util.Retry(func() { prInserted = prProvider.Put(pr) }, 10)
assert.EqualValues(t, prInserted.Title, pr.Title)
reviewProvider := f.rootGitea.g.GetReviewProvider(f.fromProject)
reviewBody := "review"
review := reviewProvider.Put(&Review{
ReviewableIndex: prInserted.Index,
PullReview: gitea_sdk.PullReview{
Body: reviewBody,
},
})
assert.EqualValues(t, review.Body, reviewBody)
}
func TestReviewFormat(t *testing.T) {
now := time.Now()
commentCreated := now.Add(1)
commentUpdated := now.Add(2)
submitted := now.Add(3)
review := Review{
ReviewableIndex: 456,
PullReview: gitea_sdk.PullReview{
ID: 123,
Reviewer: &gitea_sdk.User{
ID: 11111,
UserName: "username",
},
Official: true,
CommitID: "1324",
Body: "content",
Submitted: submitted,
State: gitea_sdk.ReviewStateComment,
},
Comments: []*gitea_sdk.PullReviewComment{
{
ID: 43244,
// InReplyTo
Body: "comment body",
Path: "path",
DiffHunk: "hunk",
// Position
LineNum: uint64(1),
CommitID: "4353",
Reviewer: &gitea_sdk.User{
ID: 45,
},
// Reactions
Created: commentCreated,
Updated: commentUpdated,
},
},
}
toFromFormat[Review, format.Review, *Review, *format.Review](t, &review)
}
......@@ -8,8 +8,7 @@ import "time"
// Reviewable can be reviewed
type Reviewable interface {
GetLocalIndex() int64
GetForeignIndex() int64
GetID() int64
}
// enumerate all review states
......@@ -20,6 +19,11 @@ const (
ReviewStateCommented = "COMMENTED"
)
type ReviewIndexType struct {
PullRequestID int64
ReviewID int64
}
// Review is a standard review information
type Review struct {
ID int64 `json:"id"`
......@@ -34,6 +38,9 @@ type Review struct {
Comments []*ReviewComment `json:"comments"`
}
func (r *Review) GetID() int64 { return r.ID }
func (r *Review) SetID(id int64) { r.ID = id }
// GetExternalName ExternalUserMigrated interface
func (r *Review) GetExternalName() string { return r.ReviewerName }
......@@ -55,3 +62,6 @@ type ReviewComment struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (r *ReviewComment) GetID() int64 { return r.ID }
func (r *ReviewComment) SetID(id int64) { r.ID = id }
......@@ -16,53 +16,61 @@ package list
type CacheableConstraint[
Cacheable any,
Index comparable,
] interface {
*Cacheable
GetID() int64
SetID(int64)
GetID() Index
SetID(Index)
Equals(Cacheable) bool
}
type Cache[
Cacheable any,
CacheablePtr CacheableConstraint[Cacheable],
CacheablePtr CacheableConstraint[Cacheable, Index],
Index comparable,
] struct {
m map[int64]CacheablePtr
m map[Index]CacheablePtr
}
func (c *Cache[Cacheable, CacheablePtr]) Init() {
c.m = make(map[int64]CacheablePtr)
func (c *Cache[Cacheable, CacheablePtr, Index]) Init() {
c.m = make(map[Index]CacheablePtr)
}
func (c *Cache[Cacheable, CacheablePtr]) Store(cacheable CacheablePtr) {
func (c *Cache[Cacheable, CacheablePtr, Index]) Store(cacheable CacheablePtr) {
c.m[cacheable.GetID()] = cacheable
}
func (c *Cache[Cacheable, CacheablePtr]) Load(id int64) CacheablePtr {
func (c *Cache[Cacheable, CacheablePtr, Index]) Load(id Index) CacheablePtr {
return c.m[id]
}
type ProviderConstraint[
Bean any,
BeanPtr CacheableConstraint[Bean],
BeanPtr CacheableConstraint[Bean, Index],
Provider any,
Index comparable,
] interface {
*Provider
Get(int64) BeanPtr
Get(Index) BeanPtr
Put(*Bean) BeanPtr
}
func zero[Index comparable]() (zero Index) {
return
}
func UpsertBean[
Bean any,
Provider any,
Index comparable,
BeanPtr CacheableConstraint[Bean],
ProviderPtr ProviderConstraint[Bean, BeanPtr, Provider],
](cache *Cache[Bean, BeanPtr], provider ProviderPtr, bean BeanPtr,
BeanPtr CacheableConstraint[Bean, Index],
ProviderPtr ProviderConstraint[Bean, BeanPtr, Provider, Index],
](cache *Cache[Bean, BeanPtr, Index], provider ProviderPtr, bean BeanPtr,
) (before, after BeanPtr) {
id := bean.GetID()
var existing *Bean
if id != 0 {
if id == zero[Index]() {
existing = (*Bean)(cache.Load(id))