Merge pull request #27 from SWM-Codection/dev

main 최신화
This commit is contained in:
cloudchamb3r 2024-08-02 11:15:18 +09:00 committed by GitHub
commit cdc9fda92a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

AI 샘플 코드 생성 중입니다

Loading...
23 changed files with 850 additions and 14 deletions

15
client/base.go Normal file
View File

@ -0,0 +1,15 @@
package client
import (
"code.gitea.io/gitea/modules/setting"
"github.com/go-resty/resty/v2"
)
var client *resty.Client = nil
func Request() *resty.Request {
if client == nil {
client = resty.New().SetBaseURL(setting.DiscussionServer.Url)
}
return client.R()
}

View File

@ -0,0 +1,89 @@
package discussion
import (
"database/sql"
"fmt"
"code.gitea.io/gitea/client"
"github.com/go-resty/resty/v2"
)
type DiscussionCode struct {
Id sql.NullInt64 `json:"id"`
FilePath string `json:"filePath"`
StartLine int `json:"startLine"`
EndLine int `json:"endLine"`
}
type PostDiscussionRequest struct {
RepoId int64 `json:"repoId"`
PosterId int64 `json:"posterId"`
Name string `json:"name"`
Content string `json:"content"`
Codes []DiscussionCode `json:"codes"`
}
type DiscussionAvailableRequest struct {
RepoId int64 `json:"repoId"`
Available bool `json:"available"`
}
type CommentScopeEnum int
const (
CommentScopeGlobal CommentScopeEnum = iota
CommentScopeLocal
)
type PostCommentRequest struct {
DiscussionId int64 `json:"discussionId"`
CodeId int64 `json:"codeId"`
PosterId int64 `json:"posterId"`
Scope CommentScopeEnum `json:"scope"`
StartLine sql.NullInt32 `json:"startLine"`
EndLine sql.NullInt32 `json:"endLine"`
Content string `json:"content"`
}
type ModifyDiscussionRequest struct {
RepoId int64 `json:"repoId"`
DiscussionId int64 `json:"discussionId"`
PosterId int64 `json:"posterId"`
Name string `json:"name"`
Content string `json:"content"`
Codes []DiscussionCode `json:"codes"`
}
func PostDiscussion(request PostDiscussionRequest) (*resty.Response, error) {
return client.Request().SetBody(request).Post("/discussion")
}
func GetDiscussionCount(repoId int64, isClosed bool) (*resty.Response, error) {
var isClosedAsInt = map[bool]int{false: 0, true: 1}[isClosed]
return client.Request().
SetQueryParam("isClosed", string(isClosedAsInt)).
Get(fmt.Sprintf("/discussion/%d/count", repoId))
}
func GetDiscussionList(repoId int64, isClosed bool) (*resty.Response, error) {
var isClosedAsInt = map[bool]int{false: 0, true: 1}[isClosed]
return client.Request().
SetQueryParam("isClosed", string(isClosedAsInt)).
Get(fmt.Sprintf("/discussion/%d/list", repoId))
}
func HandleDiscussionAvailable() (*resty.Response, error) {
return client.Request().Post("/discussion/available")
}
func GetDiscussionContents(discussionId int64) (*resty.Response, error) {
return client.Request().Get(fmt.Sprintf("/discussion/%d/codes", discussionId))
}
func PostComment(request PostCommentRequest) (*resty.Response, error) {
return client.Request().SetBody(request).Post("/discussion/comment")
}
func ModifyDiscussion(request ModifyDiscussionRequest) (*resty.Response, error) {
return client.Request().SetBody(request).Put("/discussion")
}

10
go.mod
View File

@ -104,12 +104,12 @@ require (
github.com/yuin/goldmark v1.7.0
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
github.com/yuin/goldmark-meta v1.1.0
golang.org/x/crypto v0.22.0
golang.org/x/crypto v0.23.0
golang.org/x/image v0.15.0
golang.org/x/net v0.24.0
golang.org/x/net v0.25.0
golang.org/x/oauth2 v0.18.0
golang.org/x/sys v0.19.0
golang.org/x/text v0.14.0
golang.org/x/sys v0.20.0
golang.org/x/text v0.15.0
golang.org/x/tools v0.19.0
google.golang.org/grpc v1.62.1
google.golang.org/protobuf v1.33.0
@ -194,6 +194,7 @@ require (
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/go-webauthn/x v0.1.9 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
@ -267,6 +268,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.18.2 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/toqueteos/webbrowser v1.2.0 // indirect
github.com/unknwon/com v1.0.1 // indirect

14
go.sum
View File

@ -329,6 +329,8 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@ -736,6 +738,8 @@ github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -854,8 +858,10 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
@ -888,8 +894,11 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -939,8 +948,10 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@ -950,8 +961,10 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -965,6 +978,7 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -0,0 +1,6 @@
-
id: 2 # create 전용
poster_id: 1
pull_id: 1

140
models/issues/ai_comment.go Normal file
View File

@ -0,0 +1,140 @@
package issues
import (
"context"
"fmt"
"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/timeutil"
)
// init 메소드가 있으면 자동적으로 xorm에서 이 메소드를 실행하는듯 하다.
func init() {
db.RegisterModel(new(AiPullComment))
}
// TODOC AI 코멘트 테이블 만들기
// TODOC outdated가 어떤 식으로 나타나는 것인지 알아보기
// TODOC 먼저 영속성 계층부터-도메인 계층 순서로 만들어가기
type AiPullComment struct {
ID int64 `xorm:"pk autoincr"`
PosterID int64 `xorm:"INDEX"`
Poster *user_model.User `xorm:"-"`
PullID int64
Pull *Issue `xorm:"-"`
TreePath string
Content string `xorm:"LONGTEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
Status string `xorm:"status"` //
DeletedUnix timeutil.TimeStamp `xorm:"deleted"`
CommitSHA string `xorm:"VARCHAR(64)"`
// CommitID int64
}
type CreateAiPullCommentOption struct {
Doer *user_model.User
Repo *repo_model.Repository
Pull *Issue
TreePath string
Content string
CommitSHA string
// CommitID string
}
type ErrAiPullCommentNotExist struct {
ID int64
RepoID int64
}
func IsErrAiPullCommentNotExist(err error) bool {
_, ok := err.(ErrIssueWasClosed)
return ok
}
func (err ErrAiPullCommentNotExist) Error() string {
return fmt.Sprintf("AiPullComment does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID)
}
func CreateAiPullComment(ctx context.Context, opts *CreateAiPullCommentOption) (*AiPullComment, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, err
}
defer committer.Close()
aiPullComment := &AiPullComment{
PosterID: opts.Doer.ID,
PullID: opts.Pull.ID,
TreePath: opts.TreePath,
Content: opts.Content,
CommitSHA: opts.CommitSHA,
}
e := db.GetEngine(ctx)
_, err = e.Insert(aiPullComment)
if err != nil {
fmt.Errorf("new Comment insert is invalid")
return nil, err
}
if err = committer.Commit(); err != nil {
return nil, err
}
return aiPullComment, nil
}
func GetAIPullCommentByID(ctx context.Context, id int64) (*AiPullComment, error) {
comment := new(AiPullComment)
has, err := db.GetEngine(ctx).ID(id).Get(comment)
if err != nil {
return nil, ErrAiPullCommentNotExist{id, 0}
} else if !has {
return nil, err
}
return comment, nil
}
func DeleteAiPullCommentByID(ctx context.Context, id int64) error {
_, err := GetAIPullCommentByID(ctx, id)
if err != nil {
if IsErrAiPullCommentNotExist(err) {
return nil
}
return err
}
dbCtx, commiter, err := db.TxContext(ctx)
defer commiter.Close()
if err != nil {
return err
}
_, err = db.DeleteByID[AiPullComment](dbCtx, id)
if err != nil {
return err
}
return commiter.Commit()
}
// TODOC repo가 삭제되면 Ai Comment도 삭제하는 로직
// TODOC

View File

@ -0,0 +1,52 @@
package issues_test
import (
"testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
ai_comment "code.gitea.io/gitea/models/issues"
)
func TestCreateAiPullComment(t *testing.T) {
assert := assert.New(t)
assert.NoError(unittest.PrepareTestDatabase())
pull := unittest.AssertExistsAndLoadBean(t, &ai_comment.Issue{ID: 2})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pull.RepoID})
content := "this code is sh**"
treePath := "/src/ddd"
newAiComment, err := ai_comment.CreateAiPullComment(db.DefaultContext, &ai_comment.CreateAiPullCommentOption{
Doer: doer,
Pull: pull,
Repo: repo,
Content: content,
TreePath: treePath,
})
assert.NoError(err)
assert.EqualValues(newAiComment.Content, content)
assert.EqualValues(newAiComment.TreePath, treePath)
assert.EqualValues(newAiComment.PosterID, doer.ID)
assert.EqualValues(newAiComment.PullID, pull.ID)
}
func TestDeleteAiPullRequest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
comment := unittest.AssertExistsAndLoadBean(t, &ai_comment.AiPullComment{ID: 2})
assert.NoError(t, ai_comment.DeleteAiPullCommentByID(db.DefaultContext, comment.ID))
unittest.AssertNotExistsBean(t, &ai_comment.AiPullComment{ID: comment.ID})
assert.NoError(t, ai_comment.DeleteAiPullCommentByID(db.DefaultContext, unittest.NonexistentID))
unittest.CheckConsistencyFor(t, &ai_comment.AiPullComment{})
}

View File

@ -453,7 +453,7 @@ func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeuti
return committer.Commit()
}
// 커멘트에서 멘션된 유저를 찾고 데이터베이스에 저장하기
// FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.
func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) {
rawMentions := references.FindAllMentionsMarkdown(content)

View File

@ -0,0 +1,20 @@
package setting
import "fmt"
var AiServer = struct {
Host string
Port int
Url string
}{
Host: "localhost",
Port: 8000,
Url: "http://localhost:8000",
}
func loadAiServerFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("ai_server")
AiServer.Host = sec.Key("host").MustString("localhost")
AiServer.Port = sec.Key("PORT").MustInt(8000)
AiServer.Url = fmt.Sprintf("http://%s:%d", AiServer.Host, AiServer.Port)
}

View File

@ -0,0 +1,20 @@
package setting
import "fmt"
var DiscussionServer = struct {
Host string
Port int
Url string
}{
Host: "localhost",
Port: 8081,
Url: "http://localhost:8081",
}
func loadDiscussionServerFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("discussion_server")
DiscussionServer.Host = sec.Key("HOST").MustString("localhost")
DiscussionServer.Port = sec.Key("PORT").MustInt(8081)
DiscussionServer.Url = fmt.Sprintf("http://%s:%d", DiscussionServer.Host, DiscussionServer.Port)
}

View File

@ -118,6 +118,8 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
loadLogGlobalFrom(cfg)
loadServerFrom(cfg)
loadSSHFrom(cfg)
loadDiscussionServerFrom(cfg)
loadAiServerFrom(cfg)
mustCurrentRunUserMatch(cfg) // it depends on the SSH config, only non-builtin SSH server requires this check

13
modules/structs/ai.go Normal file
View File

@ -0,0 +1,13 @@
package structs
type CreateAiPullCommentForm struct {
Branch string `json:"branch"`
FileContents *[]PathContentMap `json:"file_contents"`
RepoID string `json:"repo_id"`
PullID string `json:"pull_id"`
}
type PathContentMap struct {
TreePath string `json:"file_path"`
Content string `json:"code"`
}

View File

@ -920,6 +920,8 @@ func Routes() *web.Route {
Patch(reqToken(), notify.ReadThread)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
// Users (requires user scope)
m.Group("/users", func() {
m.Get("/search", reqExploreSignIn(), user.Search)

View File

@ -0,0 +1,56 @@
package repo
import (
"net/http"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
ai_service "code.gitea.io/gitea/services/ai"
"code.gitea.io/gitea/services/context"
)
// TODOC 특정한 PR에 대한 AI comment 리퀘스트가 이미 이루어졌을 경우를 체크해서 중복 요청 차단.
// CreateAiPullComment creates an attachment and saves the given file
func CreateAiPullComment(ctx *context.Context) {
// swagger:operation POST /ai/pull/review repository repoCreateAiPullComment
// ---
// summary: Create ai pull comment
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateAiPullCommentForm"
// responses:
// "201":
// "$ref": "#/responses/Attachment"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// Check if attachments are enabled
form := web.GetForm(ctx).(*api.CreateAiPullCommentForm)
// TODOC 싱글톤으로 바꾼 뒤에
// TODOC 설정을 통해 의존성 주입하는 방식으로 바꾸기
aiService := new(ai_service.AiServiceImpl)
aiRequester := new(ai_service.AiRequesterImpl)
adapter := new(ai_service.DbAdapterImpl)
err := ai_service.AiService.CreateAiPullComment(aiService, ctx, form, aiRequester, adapter)
if err != nil {
ctx.JSON(http.StatusBadRequest, map[string]any{
"message": err.Error()})
return
}
ctx.JSON(http.StatusAccepted, map[string]any{
"message": "request has accepted",
})
}

View File

@ -399,6 +399,12 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("server").Key("ROOT_URL").SetValue(form.AppURL)
cfg.Section("server").Key("APP_DATA_PATH").SetValue(setting.AppDataPath)
// TODO: hardcoded for now, make it configurable later
cfg.Section("discussion_server").Key("HOST").SetValue("localhost")
cfg.Section("discussion_server").Key("PORT").SetValue("8081")
cfg.Section("ai_server").Key("HOST").SetValue("localhost")
cfg.Section("ai_server").Key("PORT").SetValue("8000")
if form.SSHPort == 0 {
cfg.Section("server").Key("DISABLE_SSH").SetValue("true")
} else {

View File

@ -0,0 +1,60 @@
package repo
// TODO 추후에 api.go로 옮기기
// import (
// "fmt"
// issues_model "code.gitea.io/gitea/models/issues"
// ai_service "code.gitea.io/gitea/services/ai"
// api "code.gitea.io/gitea/modules/structs"
// "code.gitea.io/gitea/modules/web"
// "code.gitea.io/gitea/services/context"
// )
// type AiController interface {
// CreateAiReviewComment(ctx *context.Context, service *ai_service.AiService, aiRequest ai_service.AiRequester, issueService ai_service.DbAdapter)
// }
// type AiControllerImpl struct{}
// func GetActionPull(ctx *context.Context) *issues_model.Issue {
// return GetActionIssue(ctx)
// }
// var _ AiController = &AiControllerImpl{}
// func (aiController *AiControllerImpl) CreateAiReviewComment(ctx *context.Context, service *ai_service.AiService, aiRequest ai_service.AiRequester, issueService ai_service.DbAdapter) {
// pull := GetActionPull(ctx)
// form := web.GetForm(ctx).(*api.CreateAiPullCommentForm)
// // TODO 결과를 받아서 AiPullComment로 저장
// if ctx.Written() {
// return
// }
// if !pull.IsPull {
// return
// }
// if ctx.HasError() {
// ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
// ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, pull.Index))
// return
// }
// ai_service.AiService.CreateAiPullComment(*service, ctx, form, aiRequest, issueService)
// // ai_service.AiService.CreateAiPullComment(*service,
// // ctx,
// // &ai_service.AiReviewCommentResult{
// // PullID: form.PullID,
// // RepoID: form.RepoID,
// // Content: result.Content,
// // TreePath: result.TreePath,
// // })
// }
// // TODOC AI 리뷰 삭제

View File

@ -58,6 +58,7 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplNewComment)
}
// 체크 코드 코멘트를 만드는 곳
// CreateCodeComment will create a code comment including an pending review if required
func CreateCodeComment(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CodeCommentForm)

View File

@ -5,6 +5,7 @@ package web
import (
gocontext "context"
"fmt"
"net/http"
"strings"
@ -23,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/modules/web/routing"
api_repo_router "code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/admin"
"code.gitea.io/gitea/routers/web/auth"
@ -47,7 +49,7 @@ import (
"code.gitea.io/gitea/services/lfs"
_ "code.gitea.io/gitea/modules/session" // to registers all internal adapters
"gitea.com/go-chi/binding"
"gitea.com/go-chi/captcha"
chi_middleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
@ -290,6 +292,19 @@ func Routes() *web.Route {
return routes
}
// bind binding an obj to a func(ctx *context.APIContext)
func bind[T any](_ T) any {
return func(ctx *context.Context) {
theObj := new(T) // create a new form obj for every request but not use obj directly
errs := binding.Bind(ctx.Req, theObj)
if len(errs) > 0 {
ctx.Error(http.StatusUnprocessableEntity, "validationError", fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error()))
return
}
web.SetForm(ctx, theObj)
}
}
var ignSignInAndCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
// registerRoutes register routes
@ -841,6 +856,10 @@ func registerRoutes(m *web.Route) {
}
}
m.Group("/ai/pull/review", func() {
m.Post("", bind(structs.CreateAiPullCommentForm{}), api_repo_router.CreateAiPullComment) // 라우팅
})
m.Group("/org", func() {
m.Group("/{org}", func() {
m.Get("/members", org.Members)
@ -1499,6 +1518,7 @@ func registerRoutes(m *web.Route) {
m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
m.Group("/reviews", func() {
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
// 체크 = CreateCodeComment를 호출
m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment)
m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview)
}, context.RepoMustNotBeArchived())

165
services/ai/review.go Normal file
View File

@ -0,0 +1,165 @@
package ai
import (
"bytes"
"fmt"
"io"
"net/http"
"strconv"
"sync"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
)
type AiService interface {
CreateAiPullComment(ctx *context.Context, form *api.CreateAiPullCommentForm, aiRequester AiRequester, adapter DbAdapter) error
DeleteAiPullComment(ctx *context.Context, id int64, adapter DbAdapter) error
}
type AiReviewRequest struct {
Branch string `json:"branch"`
TreePath string `json:"file_path"`
Content string `json:"code"`
}
type AiReviewResponse struct {
Branch string `json:"branch"`
TreePath string `json:"file_path"`
Content string `json:"code"`
}
type AiServiceImpl struct{}
type AiRequesterImpl struct{}
type AiRequester interface {
RequestReviewToAI(ctx *context.Context, request *AiReviewRequest) (*AiReviewResponse, error)
}
var _ AiService = &AiServiceImpl{}
var _ AiRequester = &AiRequesterImpl{}
var apiURL = setting.AiServer.Url
type AiReviewCommentResult struct {
PullID string
RepoID string
TreePath string
Content string
}
func (aiRequest *AiRequesterImpl) RequestReviewToAI(ctx *context.Context, request *AiReviewRequest) (*AiReviewResponse, error) {
requestBytes, _ := json.Marshal(request)
buffer := bytes.NewBuffer(requestBytes)
response, err := http.Post(apiURL, "application/json", buffer)
if err != nil {
return nil, err
}
defer response.Body.Close()
bodyJson, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
result := &AiReviewResponse{}
err = json.Unmarshal(bodyJson, result)
if err != nil {
return nil, err
}
return result, nil
}
// TODOC 잘못된 형식의 json이 돌아올 때 예외 반환하기(json 형식 표시하도록)
func (aiController *AiServiceImpl) CreateAiPullComment(ctx *context.Context, form *api.CreateAiPullCommentForm, aiRequester AiRequester, adapter DbAdapter) error {
branch := form.Branch
var wg *sync.WaitGroup = new(sync.WaitGroup)
requestCnt := len(*form.FileContents)
wg.Add(requestCnt)
resultQueue := make(chan *AiReviewResponse, requestCnt)
for _, fileContent := range *form.FileContents {
go func(fileContent *api.PathContentMap) {
defer wg.Done()
result, err := aiRequester.RequestReviewToAI(ctx, &AiReviewRequest{
Branch: branch,
TreePath: fileContent.TreePath,
Content: fileContent.Content,
})
if err != nil {
fmt.Errorf("request to ai server fail %s", result.TreePath)
resultQueue <- nil
return
}
resultQueue <- result
}(&fileContent)
}
wg.Wait()
close(resultQueue)
pullID, err := strconv.ParseInt(form.PullID, 10, 64)
if err != nil {
return fmt.Errorf("pullID is invalid")
}
return saveResults(ctx, resultQueue, pullID, adapter)
}
func (aiService *AiServiceImpl) DeleteAiPullComment(ctx *context.Context, id int64, adapter DbAdapter) error {
return adapter.DeleteAiPullCommentByID(ctx, id)
}
func saveResults(ctx *context.Context, reviewResults chan *AiReviewResponse, pullID int64, adapter DbAdapter) error {
pull, err := adapter.GetIssueByID(ctx, pullID)
if err != nil {
return fmt.Errorf("pr not found by id")
}
for result := range reviewResults {
_, err := adapter.CreateAiPullComment(ctx, &issues_model.CreateAiPullCommentOption{
Doer: ctx.Doer,
Repo: pull.Repo,
Pull: pull,
TreePath: result.TreePath,
Content: result.Content,
})
if err != nil {
return err
}
}
return nil
}
type DbAdapter interface {
GetIssueByID(ctx *context.Context, id int64) (*issues_model.Issue, error)
CreateAiPullComment(ctx *context.Context, opts *issues_model.CreateAiPullCommentOption) (*issues_model.AiPullComment, error)
DeleteAiPullCommentByID(ctx *context.Context, id int64) error
}
type DbAdapterImpl struct{}
func (is *DbAdapterImpl) GetIssueByID(ctx *context.Context, id int64) (*issues_model.Issue, error) {
return issues_model.GetIssueByID(ctx, id)
}
func (is *DbAdapterImpl) CreateAiPullComment(ctx *context.Context, opts *issues_model.CreateAiPullCommentOption) (*issues_model.AiPullComment, error) {
return issues_model.CreateAiPullComment(ctx, opts)
}
func (is *DbAdapterImpl) DeleteAiPullCommentByID(ctx *context.Context, id int64) error {
return issues_model.DeleteAiPullCommentByID(ctx, id)
}

111
services/ai/review_test.go Normal file
View File

@ -0,0 +1,111 @@
package ai
import (
"fmt"
"testing"
"time"
"code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockAiRequester is a mock implementation of AiRequester
type MockAiRequester struct {
mock.Mock
}
func (m *MockAiRequester) RequestReviewToAI(ctx *context.Context, request *AiReviewRequest) (*AiReviewResponse, error) {
time.Sleep(1000 * time.Millisecond)
args := m.Called(ctx, request)
return args.Get(0).(*AiReviewResponse), args.Error(1)
}
// MockDbAdapter is a mock implementation of DbAdapter
type MockDbAdapter struct {
mock.Mock
}
func (m *MockDbAdapter) GetIssueByID(ctx *context.Context, id int64) (*issues.Issue, error) {
args := m.Called(ctx, id)
return args.Get(0).(*issues.Issue), args.Error(1)
}
func (m *MockDbAdapter) CreateAiPullComment(ctx *context.Context, opts *issues.CreateAiPullCommentOption) (*issues.AiPullComment, error) {
args := m.Called(ctx, opts)
if args.Get(0) == nil {
return nil, args.Error(1)
}
commentID := args.Get(0).(*issues.AiPullComment)
return commentID, args.Error(1)
}
func (m *MockDbAdapter) DeleteAiPullCommentByID(ctx *context.Context, id int64) error {
args := m.Called(ctx, 1)
return args.Error(0)
}
func TestCreateAiPullComment(t *testing.T) {
// Set up the mock AiRequester
mockRequester := new(MockAiRequester)
aiService := &AiServiceImpl{}
// Set up the mock DbAdapter
mockDbAdapter := new(MockDbAdapter)
// Mock context and form
ctx := &context.Context{}
var fileContent *[]structs.PathContentMap = new([]structs.PathContentMap)
for i := 0; i < 100; i++ {
*fileContent = append(*fileContent, structs.PathContentMap{
TreePath: fmt.Sprintf("file%d.go", i),
Content: fmt.Sprintf("code content %d", i),
})
mockRequester.On("RequestReviewToAI", ctx, &AiReviewRequest{
Branch: "main",
TreePath: fmt.Sprintf("file%d.go", i),
Content: fmt.Sprintf("code content %d", i),
}).Return(&AiReviewResponse{
Branch: "main",
TreePath: fmt.Sprintf("file%d.go", i+100),
Content: fmt.Sprintf("code content %d", i+100),
}, nil)
}
form := &structs.CreateAiPullCommentForm{
PullID: "123",
Branch: "main",
FileContents: fileContent,
}
// Mock response from AI
// Mock GetIssueByID
issue := &issues.Issue{}
mockDbAdapter.On("GetIssueByID", ctx, int64(123)).Return(issue, nil)
// Mock CreateAiPullComment
comment := issues.AiPullComment{ID: 10}
mockDbAdapter.On("CreateAiPullComment", ctx, mock.Anything).Return(&comment, nil)
// Call the method under test
err := aiService.CreateAiPullComment(ctx, form, mockRequester, mockDbAdapter)
// Assert the expectations
assert.NoError(t, err)
mockRequester.AssertExpectations(t)
mockDbAdapter.AssertExpectations(t)
}
// TODOC delete 테스트

View File

@ -621,12 +621,12 @@ func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors)
// CodeCommentForm form for adding code comments for PRs
type CodeCommentForm struct {
Origin string `binding:"Required;In(timeline,diff)"`
Content string `binding:"Required"`
Side string `binding:"Required;In(previous,proposed)"`
Line int64
TreePath string `form:"path" binding:"Required"`
SingleReview bool `form:"single_review"`
Origin string `binding:"Required;In(timeline,diff)"` // diff인지 timeline인지
Content string `binding:"Required"` // 리뷰 내용 들어갈
Side string `binding:"Required;In(previous,proposed)"` //
Line int64 // 줄 수
TreePath string `form:"path" binding:"Required"` // 해당 파일의 경로 같은 거
SingleReview bool `form:"single_review"` //
Reply int64 `form:"reply"`
LatestCommitID string
Files []string
@ -902,3 +902,10 @@ func (f *DeadlineForm) Validate(req *http.Request, errs binding.Errors) binding.
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
type AiReviewRequestForm struct {
TreePath []string `form:"path" binding:"Required"` // 해당 파일의 경로 같은 거
LatestCommitID string
Files []string
}

View File

@ -89,7 +89,7 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
}
return nil
}
// 체크 코드 코멘트에 필요한 것들이 모여있음.
// CreateCodeComment creates a comment on the code line
func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (*issues_model.Comment, error) {
var (
@ -182,7 +182,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
return comment, nil
}
// 체크 코드에 대한 코멘트가 작성되는 곳
// createCodeComment creates a plain code comment at the specified line / path
func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64, attachments []string) (*issues_model.Comment, error) {
var commitID, patch string

View File

@ -1009,6 +1009,41 @@
}
}
},
"/ai/pull/review": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Create ai pull comment",
"operationId": "repoCreateAiPullComment",
"parameters": [
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/CreateAiPullCommentForm"
}
}
],
"responses": {
"201": {
"$ref": "#/responses/Attachment"
},
"400": {
"$ref": "#/responses/error"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/gitignore/templates": {
"get": {
"produces": [