Repository: reviewdog/reviewdog (language: go)

Issue:
Add Support of Gitea as Reporter
https://gitea.io/en-us/

was also mentioned in #29 


Discussion / hints:
There is https://gitea.com/gitea/go-sdk with [CreatePullReview](https://pkg.go.dev/code.gitea.io/sdk/gitea?utm_source=godoc#Client.CreatePullReview) and some other functions which might help implementing it now.
Looking forward to support

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee4a4717e4..7d2dbdf062 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - [#1576](https://github.com/reviewdog/reviewdog/pull/1576) Support `--filter-mode=file` in `github-pr-review`
 - [#1596](https://github.com/reviewdog/reviewdog/pull/1596) Use `CI_MERGE_REQUEST_DIFF_BASE_SHA` envvar if available in `gitlab-mr-discussion`
 - [#1521](https://github.com/reviewdog/reviewdog/pull/1521) strict check of pr-review write permission
+- [#1617](https://github.com/reviewdog/reviewdog/pull/1617) Add reporter to Gitea PR review comments `gitea-pr-review`
 
 ### :bug: Fixes
 - ...
diff --git a/README.md b/README.md
index c339a0edd4..6f67bf655d 100644
--- a/README.md
+++ b/README.md
@@ -322,6 +322,7 @@ Note that not all reporters provide support for code suggestions.
 | **`gitlab-mr-commit`**       | NO [2]  |
 | **`gerrit-change-review`**   | NO [1]  |
 | **`bitbucket-code-report`**  | NO [2]  |
+| **`gitea-pr-review`**        | NO [2]  |
 
 - [1] The reporter service supports the code suggestion feature, but reviewdog does not support it yet. See [#678](https://github.com/reviewdog/reviewdog/issues/678) for the status.
 - [2] The reporter service itself doesn't support the code suggestion feature.
@@ -931,6 +932,7 @@ so reviewdog will use [Check annotation](https://docs.github.com/en/rest/checks/
 | **`gitlab-mr-commit`**       | OK      | Partially Supported [2] | Partially Supported [2] | Partially Supported [2] |
 | **`gerrit-change-review`**   | OK      | OK? [3]        | OK? [3]                 | Partially Supported? [2][3] |
 | **`bitbucket-code-report`**  | NO [4]  | NO [4]         | NO [4]                  | OK |
+| **`gitea-pr-review`**        | OK      | OK             | Partially Supported [2] | Partially Supported [2] |
 
 - [1] Report results that are outside the diff file with Check annotation as fallback if it's running in GitHub actions instead of Review API (comments). All results will be reported to console as well.
 - [2] Report results that are outside the diff file to console.
diff --git a/cmd/reviewdog/main.go b/cmd/reviewdog/main.go
index 4ba70f1220..832bf657f2 100644
--- a/cmd/reviewdog/main.go
+++ b/cmd/reviewdog/main.go
@@ -16,6 +16,7 @@ import (
 	"strings"
 	"text/tabwriter"
 
+	"code.gitea.io/sdk/gitea"
 	"golang.org/x/build/gerrit"
 	"golang.org/x/oauth2"
 
@@ -32,6 +33,7 @@ import (
 	"github.com/reviewdog/reviewdog/project"
 	bbservice "github.com/reviewdog/reviewdog/service/bitbucket"
 	gerritservice "github.com/reviewdog/reviewdog/service/gerrit"
+	giteaservice "github.com/reviewdog/reviewdog/service/gitea"
 	githubservice "github.com/reviewdog/reviewdog/service/github"
 	gitlabservice "github.com/reviewdog/reviewdog/service/gitlab"
 )
@@ -86,7 +88,7 @@ const (
 		"nofilter"
 			Do not filter any results.
 `
-	reporterDoc = `reporter of reviewdog results. (local, github-check, github-pr-check, github-pr-review, gitlab-mr-discussion, gitlab-mr-commit)
+	reporterDoc = `reporter of reviewdog results. (local, github-check, github-pr-check, github-pr-review, gitlab-mr-discussion, gitlab-mr-commit, gitea-pr-review)
 	"local" (default)
 		Report results to stdout.
 
@@ -166,7 +168,16 @@ const (
 		
 		To post results to Bitbucket Server specify BITBUCKET_SERVER_URL.
 
-	For GitHub Enterprise and self hosted GitLab, set
+	"gitea-pr-review"
+		Report results to Gitea review comments.
+
+		1. Set REVIEWDOG_GITEA_API_TOKEN environment variable.
+		Go to https://<gitea-server>/user/settings/applications and create new Access Token token with repo scope.
+		2. Set GITEA_ADDRESS environment variable to your Gitea server base address.
+		For example:
+			$ export GITEA_ADDRESS=http://localhost:3000
+
+	For GitHub Enterprise and self hosted GitLab or Gitea, set
 	REVIEWDOG_INSECURE_SKIP_VERIFY to skip verifying SSL (please use this at your own risk)
 		$ export REVIEWDOG_INSECURE_SKIP_VERIFY=true
 
@@ -353,6 +364,17 @@ func run(r io.Reader, w io.Writer, opt *option) error {
 		}
 		opt.filterMode = filter.ModeNoFilter
 		ds = &reviewdog.EmptyDiff{}
+	case "gitea-pr-review":
+		gs, isPR, err := giteaService(ctx, opt)
+		if err != nil {
+			return err
+		}
+		if !isPR {
+			fmt.Fprintln(os.Stderr, "reviewdog: this is not PullRequest build.")
+			return nil
+		}
+		cs = reviewdog.MultiCommentService(gs, cs)
+		ds = gs
 	case "local":
 		if opt.diffCmd == "" && opt.filterMode == filter.ModeNoFilter {
 			ds = &reviewdog.EmptyDiff{}
@@ -431,6 +453,111 @@ func insecureSkipVerify() bool {
 	return os.Getenv("REVIEWDOG_INSECURE_SKIP_VERIFY") == "true"
 }
 
+func giteaService(ctx context.Context, opt *option) (gs *giteaservice.PullRequest, isPR bool, err error) {
+	token, err := nonEmptyEnv("REVIEWDOG_GITEA_API_TOKEN")
+	if err != nil {
+		return nil, isPR, err
+	}
+
+	giteaAddr := os.Getenv("GITEA_ADDRESS")
+	if giteaAddr == "" {
+		return nil, isPR, errors.New("cannot get Gitea host address from environment variable. Set GITEA_ADDRESS ?")
+	}
+
+	g, isPR, err := cienv.GetBuildInfo()
+	if err != nil {
+		return nil, isPR, err
+	}
+
+	client, err := giteaClient(ctx, giteaAddr, token)
+	if err != nil {
+		return nil, isPR, err
+	}
+
+	if !isPR {
+		if !opt.guessPullRequest {
+			return nil, false, nil
+		}
+
+		if g.Branch == "" && g.SHA == "" {
+			return nil, false, nil
+		}
+
+		prID, err := getGiteaPullRequestIDByBranchOrCommit(client, g)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
+			return nil, false, nil
+		}
+		g.PullRequest = int(prID)
+	}
+
+	gs, err = giteaservice.NewGiteaPullRequest(client, g.Owner, g.Repo, int64(g.PullRequest), g.SHA)
+	if err != nil {
+		return nil, false, err
+	}
+	return gs, true, nil
+}
+
+func getGiteaPullRequestIDByBranchOrCommit(client *gitea.Client, info *cienv.BuildInfo) (int64, error) {
+	options := gitea.ListPullRequestsOptions{
+		Sort:  "updated",
+		State: gitea.StateOpen,
+		ListOptions: gitea.ListOptions{
+			Page:     1,
+			PageSize: 100,
+		},
+	}
+
+	for {
+		pullRequests, resp, err := client.ListRepoPullRequests(info.Owner, info.Repo, options)
+		if err != nil {
+			return 0, err
+		}
+
+		for _, pr := range pullRequests {
+			if info.Branch != "" && info.Branch == pr.Head.Name {
+				return pr.Index, nil
+			}
+			if info.SHA != "" && info.SHA == pr.Head.Sha {
+				return pr.Index, nil
+			}
+		}
+
+		if resp.NextPage == 0 {
+			break
+		}
+		options.Page = resp.NextPage
+	}
+
+	// Build query just for error output
+	query := []string{
+		"type:pr",
+		"state:open",
+		fmt.Sprintf("repo:%s/%s", info.Owner, info.Repo),
+	}
+	if info.Branch != "" {
+		query = append(query, fmt.Sprintf("head:%s", info.Branch))
+	}
+	if info.SHA != "" {
+		query = append(query, info.SHA)
+	}
+
+	return 0, fmt.Errorf("reviewdog: PullRequest not found, query: %s", strings.Join(query, " "))
+}
+
+func giteaClient(ctx context.Context, url, token string) (*gitea.Client, error) {
+	client, err := gitea.NewClient(url,
+		gitea.SetContext(ctx),
+		gitea.SetToken(token),
+		gitea.SetHTTPClient(newHTTPClient()),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return client, client.CheckServerVersionConstraint(">=1.17.0")
+}
+
 func githubService(ctx context.Context, opt *option) (gs *githubservice.PullRequest, isPR bool, err error) {
 	token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN")
 	if err != nil {
diff --git a/go.mod b/go.mod
index 850860fa36..4fc8bfc2ba 100644
--- a/go.mod
+++ b/go.mod
@@ -5,11 +5,9 @@ go 1.21
 toolchain go1.21.0
 
 require (
-	cloud.google.com/go v0.110.7 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3
 	cloud.google.com/go/datastore v1.15.0
-	cloud.google.com/go/monitoring v1.15.1 // indirect
-	cloud.google.com/go/trace v1.10.1 // indirect
+	code.gitea.io/sdk/gitea v0.17.0
 	contrib.go.opencensus.io/exporter/stackdriver v0.13.12
 	github.com/bradleyfalzon/ghinstallation/v2 v2.8.0
 	github.com/golang/protobuf v1.5.3
@@ -36,10 +34,15 @@ require (
 )
 
 require (
+	cloud.google.com/go v0.110.7 // indirect
 	cloud.google.com/go/compute v1.23.0 // indirect
+	cloud.google.com/go/monitoring v1.15.1 // indirect
+	cloud.google.com/go/trace v1.10.1 // indirect
 	github.com/aws/aws-sdk-go v1.43.31 // indirect
 	github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davidmz/go-pageant v1.0.2 // indirect
+	github.com/go-fed/httpsig v1.1.0 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/go-github/v56 v56.0.0 // indirect
@@ -49,6 +52,7 @@ require (
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
+	github.com/hashicorp/go-version v1.6.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/prometheus v2.5.0+incompatible // indirect
diff --git a/go.sum b/go.sum
index 40a1273ca3..30c4d0d5e5 100644
--- a/go.sum
+++ b/go.sum
@@ -56,6 +56,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
 cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A=
 cloud.google.com/go/trace v1.10.1 h1:EwGdOLCNfYOOPtgqo+D2sDLZmRCEO1AagRTJCU6ztdg=
 cloud.google.com/go/trace v1.10.1/go.mod h1:gbtL94KE5AJLH3y+WVpfWILmqgc6dXcqgNXdOPAQTYk=
+code.gitea.io/sdk/gitea v0.17.0 h1:8JPBss4+Jf7AE1YcfyiGrngTXE8dFSG3si/bypsTH34=
+code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg=
 contrib.go.opencensus.io/exporter/stackdriver v0.13.12 h1:bjBKzIf7/TAkxd7L2utGaLM78bmUWlCval5K9UeElbY=
 contrib.go.opencensus.io/exporter/stackdriver v0.13.12/go.mod h1:mmxnWlrvrFdpiOHOhxBaVi1rkc0WOqhgfknj4Yg0SeQ=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@@ -89,6 +91,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
+github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -99,6 +103,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
+github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -200,6 +206,9 @@ github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxC
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
 github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
+github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
+github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/haya14busa/go-actions-toolkit v0.0.0-20200105081403-ca0307860f01 h1:HiJF8Mek+I7PY0Bm+SuhkwaAZSZP83sw6rrTMrgZ0io=
@@ -286,8 +295,10 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
 golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -454,6 +465,8 @@ golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
 golang.org/x/sys v0.15.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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/service/gitea/gitea.go b/service/gitea/gitea.go
new file mode 100644
index 0000000000..d99514b641
--- /dev/null
+++ b/service/gitea/gitea.go
@@ -0,0 +1,238 @@
+package gitea
+
+import (
+	"context"
+	"fmt"
+	"path/filepath"
+	"sync"
+
+	"github.com/reviewdog/reviewdog"
+	"github.com/reviewdog/reviewdog/service/commentutil"
+	"github.com/reviewdog/reviewdog/service/serviceutil"
+
+	"code.gitea.io/sdk/gitea"
+)
+
+var (
+	_ reviewdog.CommentService = &PullRequest{}
+	_ reviewdog.DiffService    = &PullRequest{}
+)
+
+// PullRequest is a comment and diff service for Gitea PullRequest.
+//
+// API:
+//
+//	https://try.gitea.io/api/swagger#/issue/issueCreateComment
+//	POST /repos/:owner/:repo/issues/:number/comments
+type PullRequest struct {
+	cli   *gitea.Client
+	owner string
+	repo  string
+	pr    int64
+	sha   string
+
+	muComments   sync.Mutex
+	postComments []*reviewdog.Comment
+
+	postedcs commentutil.PostedComments
+
+	// wd is working directory relative to root of repository.
+	wd string
+}
+
+// NewGiteaPullRequest returns a new PullRequest service.
+// PullRequest service needs git command in $PATH.
+func NewGiteaPullRequest(cli *gitea.Client, owner, repo string, pr int64, sha string) (*PullRequest, error) {
+	workDir, err := serviceutil.GitRelWorkdir()
+	if err != nil {
+		return nil, fmt.Errorf("pull request needs 'git' command: %w", err)
+	}
+	return &PullRequest{
+		cli:   cli,
+		owner: owner,
+		repo:  repo,
+		pr:    pr,
+		sha:   sha,
+		wd:    workDir,
+	}, nil
+}
+
+// Diff returns a diff of PullRequest.
+func (g *PullRequest) Diff(_ context.Context) ([]byte, error) {
+	diff, _, err := g.cli.GetPullRequestDiff(g.owner, g.repo, g.pr, gitea.PullRequestDiffOptions{
+		Binary: false,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return diff, nil
+}
+
+// Strip returns 1 as a strip of git diff.
+func (g *PullRequest) Strip() int {
+	return 1
+}
+
+// Post accepts a comment and holds it. Flush method actually posts comments to
+// Gitea in parallel.
+func (g *PullRequest) Post(_ context.Context, c *reviewdog.Comment) error {
+	c.Result.Diagnostic.GetLocation().Path = filepath.ToSlash(filepath.Join(g.wd,
+		c.Result.Diagnostic.GetLocation().GetPath()))
+
+	g.muComments.Lock()
+	defer g.muComments.Unlock()
+
+	g.postComments = append(g.postComments, c)
+
+	return nil
+}
+
+// Flush posts comments which has not been posted yet.
+func (g *PullRequest) Flush(_ context.Context) error {
+	g.muComments.Lock()
+	defer g.muComments.Unlock()
+
+	if err := g.setPostedComment(); err != nil {
+		return err
+	}
+	return g.postAsReviewComment()
+}
+
+// setPostedComment get posted comments from Gitea.
+func (g *PullRequest) setPostedComment() error {
+	g.postedcs = make(commentutil.PostedComments)
+
+	cs, err := g.comment()
+	if err != nil {
+		return err
+	}
+
+	for _, c := range cs {
+		if c.LineNum == 0 || c.Path == "" || c.Body == "" {
+			continue
+		}
+		g.postedcs.AddPostedComment(c.Path, int(c.LineNum), c.Body)
+	}
+
+	return nil
+}
+
+func (g *PullRequest) comment() ([]*gitea.PullReviewComment, error) {
+	prs, err := listAllPullRequestReviews(g.cli, g.owner, g.repo, g.pr, gitea.ListPullReviewsOptions{
+		ListOptions: gitea.ListOptions{
+			Page:     1,
+			PageSize: 100,
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	comments := make([]*gitea.PullReviewComment, 0, len(prs))
+	for _, pr := range prs {
+		c, _, err := g.cli.ListPullReviewComments(g.owner, g.repo, g.pr, pr.ID)
+		if err != nil {
+			return nil, err
+		}
+
+		comments = append(comments, c...)
+	}
+
+	return comments, nil
+}
+
+func listAllPullRequestReviews(cli *gitea.Client,
+	owner, repo string, pr int64, opts gitea.ListPullReviewsOptions,
+) ([]*gitea.PullReview, error) {
+	prs, resp, err := cli.ListPullReviews(owner, repo, pr, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.NextPage == 0 {
+		return prs, nil
+	}
+
+	newOpts := gitea.ListPullReviewsOptions{
+		ListOptions: gitea.ListOptions{
+			Page:     resp.NextPage,
+			PageSize: opts.PageSize,
+		},
+	}
+
+	restPrs, err := listAllPullRequestReviews(cli, owner, repo, pr, newOpts)
+	if err != nil {
+		return nil, err
+	}
+
+	return append(prs, restPrs...), nil
+}
+
+func (g *PullRequest) postAsReviewComment() error {
+	postComments := g.postComments
+	g.postComments = nil
+	reviewComments := make([]gitea.CreatePullReviewComment, 0, len(postComments))
+
+	for _, comment := range postComments {
+		if !comment.Result.InDiffFile {
+			continue
+		}
+
+		body := commentutil.MarkdownComment(comment)
+		if g.postedcs.IsPosted(comment, giteaCommentLine(comment), body) {
+			// it's already posted. skip it.
+			continue
+		}
+
+		if !comment.Result.InDiffContext {
+			// If the result is outside of diff context, skip it.
+			continue
+		}
+
+		reviewComments = append(reviewComments, buildReviewComment(comment, body))
+	}
+
+	if len(reviewComments) > 0 {
+		// send review comments to Gitea.
+		review := gitea.CreatePullReviewOptions{
+			CommitID: g.sha,
+			State:    gitea.ReviewStateComment,
+			Comments: reviewComments,
+		}
+		_, _, err := g.cli.CreatePullReview(g.owner, g.repo, g.pr, review)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func buildReviewComment(c *reviewdog.Comment, body string) gitea.CreatePullReviewComment {
+	loc := c.Result.Diagnostic.GetLocation()
+
+	return gitea.CreatePullReviewComment{
+		Body:       body,
+		Path:       loc.GetPath(),
+		NewLineNum: int64(giteaCommentLine(c)),
+	}
+}
+
+// line represents end line if it's a multiline comment in Gitea, otherwise
+// it's start line.
+func giteaCommentLine(c *reviewdog.Comment) int {
+	if !c.Result.InDiffContext {
+		return 0
+	}
+
+	loc := c.Result.Diagnostic.GetLocation()
+	startLine := loc.GetRange().GetStart().GetLine()
+	endLine := loc.GetRange().GetEnd().GetLine()
+
+	if endLine == 0 {
+		endLine = startLine
+	}
+
+	return int(endLine)
+}


Test patch: