Repository: reviewdog/reviewdog
Pull request #487 (2020-02-29 16:31:09)

Add Gerrit change review support
An attempt to add Gerrit comment support: https://github.com/reviewdog/reviewdog/issues/69

staticmukesh: Hello @haya14busa, kindly review the pull request. 😄 

Patch:
diff --git a/README.md b/README.md
index 3dc63034..cef83901 100644
--- a/README.md
+++ b/README.md
@@ -383,6 +383,22 @@ $ export REVIEWDOG_GITLAB_API_TOKEN="<token>"
 $ reviewdog -reporter=gitlab-mr-commit
 ```
 
+### Reporter: Gerrit Change review (-reporter=gerrit-change-review)
+
+gerrit-change-review reporter reports result to Gerrit Change using Gerrit Rest APIs.
+
+The reporter supports Basic Authentication and Git-cookie based authentication for reporting results.
+
+Set `GERRIT_USERNAME` and `GERRIT_PASSWORD` environment variables for basic authentication, and put `GIT_GITCOOKIE_PATH` for git cookie based authentication.
+
+```shell
+$ export GERRIT_CHANGE_ID=changeID
+$ export GERRIT_REVISION_ID=revisionID
+$ export GERRIT_BRANCH=master
+$ export GERRIT_ADDRESS=http://<gerrit-host>:<gerrit-port>
+$ reviewdog -reporter=gerrit-change-review
+```
+
 ## Supported CI services
 
 ### [GitHub Actions](https://github.com/features/actions)
diff --git a/cienv/cienv.go b/cienv/cienv.go
index 5d9fb25f..1f9f4d0a 100644
--- a/cienv/cienv.go
+++ b/cienv/cienv.go
@@ -20,6 +20,10 @@ type BuildInfo struct {
 
 	// Optional.
 	Branch string
+
+	// Gerrit related params
+	GerritChangeID   string
+	GerritRevisionID string
 }
 
 // GetBuildInfo returns BuildInfo from environment variables.
@@ -93,6 +97,30 @@ func GetBuildInfo() (prInfo *BuildInfo, isPR bool, err error) {
 	}, pr != 0, nil
 }
 
+// GetGerritBuildInfo returns Gerrit specific build info
+func GetGerritBuildInfo() (*BuildInfo, error) {
+	changeID := os.Getenv("GERRIT_CHANGE_ID")
+	if changeID == "" {
+		return nil, errors.New("cannot get change id from environment variable. Set GERRIT_CHANGE_ID ?")
+	}
+
+	revisionID := os.Getenv("GERRIT_REVISION_ID")
+	if revisionID == "" {
+		return nil, errors.New("cannot get revision id from environment variable. Set GERRIT_REVISION_ID ?")
+	}
+
+	branch := os.Getenv("GERRIT_BRANCH")
+	if branch == "" {
+		return nil, errors.New("cannot get branch from environment variable. Set GERRIT_BRANCH ?")
+	}
+
+	return &BuildInfo{
+		GerritChangeID:   changeID,
+		GerritRevisionID: revisionID,
+		Branch:           branch,
+	}, nil
+}
+
 func getPullRequestNum() int {
 	envs := []string{
 		// Common.
diff --git a/cmd/reviewdog/main.go b/cmd/reviewdog/main.go
index 7aaeec62..8953adaf 100644
--- a/cmd/reviewdog/main.go
+++ b/cmd/reviewdog/main.go
@@ -15,8 +15,8 @@ import (
 	"strings"
 	"text/tabwriter"
 
+	"golang.org/x/build/gerrit"
 	"golang.org/x/net/context" // "context"
-
 	"golang.org/x/oauth2"
 
 	"github.com/google/go-github/v31/github"
@@ -28,6 +28,7 @@ import (
 	"github.com/reviewdog/reviewdog/cienv"
 	"github.com/reviewdog/reviewdog/commands"
 	"github.com/reviewdog/reviewdog/project"
+	gerritservice "github.com/reviewdog/reviewdog/service/gerrit"
 	githubservice "github.com/reviewdog/reviewdog/service/github"
 	"github.com/reviewdog/reviewdog/service/github/githubutils"
 	gitlabservice "github.com/reviewdog/reviewdog/service/gitlab"
@@ -131,6 +132,19 @@ const (
 		Same as gitlab-mr-discussion, but report results to GitLab comments for
 		each commits in Merge Requests.
 
+	"gerrit-change-review"
+		Report results to Gerrit Change comments.
+
+		1. Set GERRIT_USERNAME and GERRIT_PASSWORD for basic authentication or
+		GIT_GITCOOKIE_PATH for git cookie based authentication.
+		2. Set GERRIT_CHANGE_ID, GERRIT_REVISION_ID GERRIT_BRANCH abd GERRIT_ADDRESS
+
+		For example:
+			$ export GERRIT_CHANGE_ID=myproject~master~I1293efab014de2
+			$ export GERRIT_REVISION_ID=ed318bf9a3c
+			$ export GERRIT_BRANCH=master
+			$ export GERRIT_ADDRESS=http://localhost:8080
+
 	For GitHub Enterprise and self hosted GitLab, set
 	REVIEWDOG_INSECURE_SKIP_VERIFY to skip verifying SSL (please use this at your own risk)
 		$ export REVIEWDOG_INSECURE_SKIP_VERIFY=true
@@ -298,6 +312,22 @@ github-pr-check reporter as a fallback.
 		if err != nil {
 			return err
 		}
+	case "gerrit-change-review":
+		b, cli, err := gerritBuildWithClient()
+		if err != nil {
+			return err
+		}
+		gc, err := gerritservice.NewChangeReviewCommenter(cli, b.GerritChangeID, b.GerritRevisionID)
+		if err != nil {
+			return err
+		}
+		cs = gc
+
+		d, err := gerritservice.NewChangeDiff(cli, b.Branch, b.GerritChangeID)
+		if err != nil {
+			return err
+		}
+		ds = d
 	case "local":
 		d, err := diffService(opt.diffCmd, opt.diffStrip)
 		if err != nil {
@@ -497,6 +527,33 @@ func gitlabBuildWithClient() (*cienv.BuildInfo, *gitlab.Client, error) {
 	return g, client, err
 }
 
+func gerritBuildWithClient() (*cienv.BuildInfo, *gerrit.Client, error) {
+	buildInfo, err := cienv.GetGerritBuildInfo()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	gerritAddr := os.Getenv("GERRIT_ADDRESS")
+	if gerritAddr == "" {
+		return nil, nil, errors.New("cannot get gerrit host address from environment variable. Set GERRIT_ADDRESS ?")
+	}
+
+	username := os.Getenv("GERRIT_USERNAME")
+	password := os.Getenv("GERRIT_PASSWORD")
+	if username != "" && password != "" {
+		client := gerrit.NewClient(gerritAddr, gerrit.BasicAuth(username, password))
+		return buildInfo, client, nil
+	}
+
+	if useGitCookiePath := os.Getenv("GERRIT_GIT_COOKIE_PATH"); useGitCookiePath != "" {
+		client := gerrit.NewClient(gerritAddr, gerrit.GitCookieFileAuth(useGitCookiePath))
+		return buildInfo, client, nil
+	}
+
+	client := gerrit.NewClient(gerritAddr, gerrit.NoAuth)
+	return buildInfo, client, nil
+}
+
 func fetchMergeRequestIDFromCommit(cli *gitlab.Client, projectID, sha string) (id int, err error) {
 	// https://docs.gitlab.com/ce/api/merge_requests.html#list-project-merge-requests
 	opt := &gitlab.ListProjectMergeRequestsOptions{
diff --git a/go.mod b/go.mod
index b49d4f47..4891cc71 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
 	github.com/rakyll/statik v0.1.7
 	github.com/reviewdog/errorformat v0.0.0-20200502163639-f5b5a3cb8aed
 	github.com/xanzy/go-gitlab v0.31.0
+	golang.org/x/build v0.0.0-20200226193612-7ece5dab5e4e
 	golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
 	golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
diff --git a/go.sum b/go.sum
index f4df5279..fe3e458c 100644
--- a/go.sum
+++ b/go.sum
@@ -36,6 +36,9 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
 github.com/bradleyfalzon/ghinstallation v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I=
 github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -44,6 +47,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 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=
@@ -54,6 +58,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
 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=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 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=
@@ -85,6 +91,8 @@ github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts=
 github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
 github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
@@ -103,6 +111,7 @@ github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
@@ -118,6 +127,7 @@ github.com/haya14busa/go-checkstyle v0.0.0-20170303121022-5e9d09f51fa1/go.mod h1
 github.com/haya14busa/secretbox v0.0.0-20180525171038-07c7ecf409f5 h1:ylgozezbuxA/i4uFtWCG/qGKYOZydsS8VUNNwfugn2Q=
 github.com/haya14busa/secretbox v0.0.0-20180525171038-07c7ecf409f5/go.mod h1:FGO/dXIFZnan7KvvUSFk1hYMnoVNzB6NTMPrmke8SSI=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
@@ -128,6 +138,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@@ -139,15 +150,17 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
 github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
-github.com/reviewdog/errorformat v0.0.0-20200501104359-d3e6dc22482a h1:EIdWiU1oKAgebSrULT0H/4FV4t/oLzsXso/W+qf6kVo=
-github.com/reviewdog/errorformat v0.0.0-20200501104359-d3e6dc22482a/go.mod h1:giYAXnpegRDPsXUO7TRpDKXJo1lFGYxyWRfEt5iQ+OA=
 github.com/reviewdog/errorformat v0.0.0-20200502163639-f5b5a3cb8aed h1:loFcu5lrYzlfJzrp5oZGeK0X8LrwNDQXle248X89a8s=
 github.com/reviewdog/errorformat v0.0.0-20200502163639-f5b5a3cb8aed/go.mod h1:giYAXnpegRDPsXUO7TRpDKXJo1lFGYxyWRfEt5iQ+OA=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
 github.com/xanzy/go-gitlab v0.31.0 h1:+nHztQuCXGSMluKe5Q9IRaPdz6tO8O0gMkQ0vqGpiBk=
 github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -158,6 +171,9 @@ go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+golang.org/x/build v0.0.0-20200226193612-7ece5dab5e4e h1:OME89lwjo2IsHFQBc0gAAxgJr6Y7T0WiqNMUtUkEqfg=
+golang.org/x/build v0.0.0-20200226193612-7ece5dab5e4e/go.mod h1:AZUA4Q+VyW6vr2O+wetMY3SpcTqdCUrAcdQp3ueAxLs=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
@@ -236,6 +252,7 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0H
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -358,6 +375,7 @@ google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba h1:pRj9OXZbwNtbtZt
 google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200207204624-4f3edf09f4f6/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce h1:1mbrb1tUU+Zmt5C94IGKADBTJZjZXAd+BubWi7r9EiI=
 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
@@ -380,10 +398,12 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/service/gerrit/change_diff.go b/service/gerrit/change_diff.go
new file mode 100644
index 00000000..8d4e30e6
--- /dev/null
+++ b/service/gerrit/change_diff.go
@@ -0,0 +1,78 @@
+package gerrit
+
+import (
+	"context"
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"golang.org/x/build/gerrit"
+
+	"github.com/reviewdog/reviewdog"
+	"github.com/reviewdog/reviewdog/service/serviceutil"
+)
+
+const (
+	stripDiffResult = 1
+)
+
+var _ reviewdog.DiffService = &ChangeDiff{}
+
+// ChangeDiff is a diff service for Gerrit changes.
+type ChangeDiff struct {
+	cli      *gerrit.Client
+	changeID string
+	branch   string
+
+	// wd is working directory relative to root of repository.
+	wd string
+}
+
+// NewChangeDiff returns a new ChangeDiff service,
+// it needs git command in $PATH.
+func NewChangeDiff(cli *gerrit.Client, branch, changeID string) (*ChangeDiff, error) {
+	workDir, err := serviceutil.GitRelWorkdir()
+	if err != nil {
+		return nil, fmt.Errorf("ChangeDiff needs 'git' command: %v", err)
+	}
+	return &ChangeDiff{
+		cli:      cli,
+		branch:   branch,
+		changeID: changeID,
+		wd:       workDir,
+	}, nil
+}
+
+// Diff returns a diff of MergeRequest. It runs `git diff` locally instead of
+// diff_url of GitLab Merge Request because diff of diff_url is not suited for
+// comment API in a sense that diff of diff_url is equivalent to
+// `git diff --no-renames`, we want diff which is equivalent to
+// `git diff --find-renames`.
+func (g *ChangeDiff) Diff(ctx context.Context) ([]byte, error) {
+	change, err := g.cli.GetChangeDetail(ctx, g.changeID, gerrit.QueryChangesOpt{
+		Fields: []string{"CURRENT_REVISION"},
+	})
+	if err != nil {
+		return nil, err
+	}
+	return g.gitDiff(ctx, change.CurrentRevision, g.branch)
+}
+
+func (g *ChangeDiff) gitDiff(_ context.Context, baseSha, targetSha string) ([]byte, error) {
+	b, err := exec.Command("git", "merge-base", targetSha, baseSha).Output() // #nosec
+	if err != nil {
+		return nil, fmt.Errorf("failed to get merge-base commit: %v", err)
+	}
+	mergeBase := strings.Trim(string(b), "\n")
+	relArg := fmt.Sprintf("--relative=%s", g.wd)
+	bytes, err := exec.Command("git", "diff", relArg, "--find-renames", mergeBase, baseSha).Output()
+	if err != nil {
+		return nil, fmt.Errorf("failed to run git diff: %v", err)
+	}
+	return bytes, nil
+}
+
+// Strip returns 1 as a strip of git diff.
+func (g *ChangeDiff) Strip() int {
+	return stripDiffResult
+}
diff --git a/service/gerrit/change_review.go b/service/gerrit/change_review.go
new file mode 100644
index 00000000..191ba7da
--- /dev/null
+++ b/service/gerrit/change_review.go
@@ -0,0 +1,78 @@
+package gerrit
+
+import (
+	"context"
+	"fmt"
+	"path/filepath"
+	"sync"
+
+	"github.com/reviewdog/reviewdog"
+	"github.com/reviewdog/reviewdog/service/serviceutil"
+	"golang.org/x/build/gerrit"
+)
+
+var _ reviewdog.CommentService = &ChangeReviewCommenter{}
+
+// ChangeReviewCommenter is a comment service for Gerrit Change Review
+// API:
+// 	https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-review
+// 	POST /changes/{change-id}/revisions/{revision-id}/review
+type ChangeReviewCommenter struct {
+	cli        *gerrit.Client
+	changeID   string
+	revisionID string
+
+	muComments   sync.Mutex
+	postComments []*reviewdog.Comment
+
+	// wd is working directory relative to root of repository.
+	wd string
+}
+
+// NewChangeReviewCommenter returns a new NewChangeReviewCommenter service.
+// ChangeReviewCommenter service needs git command in $PATH.
+func NewChangeReviewCommenter(cli *gerrit.Client, changeID, revisionID string) (*ChangeReviewCommenter, error) {
+	workDir, err := serviceutil.GitRelWorkdir()
+	if err != nil {
+		return nil, fmt.Errorf("ChangeReviewCommenter needs 'git' command: %v", err)
+	}
+
+	return &ChangeReviewCommenter{
+		cli:          cli,
+		changeID:     changeID,
+		revisionID:   revisionID,
+		postComments: []*reviewdog.Comment{},
+		wd:           workDir,
+	}, nil
+}
+
+// Post accepts a comment and holds it. Flush method actually posts comments to Gerrit
+func (g *ChangeReviewCommenter) Post(_ context.Context, c *reviewdog.Comment) error {
+	c.Path = filepath.Join(g.wd, c.Path)
+	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 *ChangeReviewCommenter) Flush(ctx context.Context) error {
+	g.muComments.Lock()
+	defer g.muComments.Unlock()
+
+	return g.postAllComments(ctx)
+}
+
+func (g *ChangeReviewCommenter) postAllComments(ctx context.Context) error {
+	review := gerrit.ReviewInput{
+		Comments: map[string][]gerrit.CommentInput{},
+	}
+	for _, c := range g.postComments {
+		review.Comments[c.Path] = append(review.Comments[c.Path], gerrit.CommentInput{
+			Line:    c.Lnum,
+			Message: c.Body,
+		})
+	}
+
+	return g.cli.SetReview(ctx, g.changeID, g.revisionID, review)
+}


Test patch:
diff --git a/cienv/cienv_test.go b/cienv/cienv_test.go
index 54f90b12..9d37ba3d 100644
--- a/cienv/cienv_test.go
+++ b/cienv/cienv_test.go
@@ -34,6 +34,9 @@ func setupEnvs() (cleanup func()) {
 		"TRAVIS_PULL_REQUEST_SHA",
 		"TRAVIS_REPO_SLUG",
 		"GITHUB_ACTION",
+		"GERRIT_CHANGE_ID",
+		"GERRIT_REVISION_ID",
+		"GERRIT_BRANCH",
 	}
 	saveEnvs := make(map[string]string)
 	for _, key := range cleanEnvs {
@@ -266,3 +269,34 @@ func TestGetBuildInfo_common(t *testing.T) {
 		t.Errorf("got: %#v, want: %#v", g, want)
 	}
 }
+
+func TestGetGerritBuildInfo(t *testing.T) {
+	cleanup := setupEnvs()
+	defer cleanup()
+
+	// without any environment variables
+	if _, err := GetGerritBuildInfo(); err == nil {
+		t.Error("error expected but got nil")
+	} else {
+		t.Log(err)
+	}
+
+	os.Setenv("GERRIT_CHANGE_ID", "changedID1")
+	if _, err := GetGerritBuildInfo(); err == nil {
+		t.Error("error expected but got nil")
+	} else {
+		t.Log(err)
+	}
+
+	os.Setenv("GERRIT_REVISION_ID", "revisionID1")
+	if _, err := GetGerritBuildInfo(); err == nil {
+		t.Error("error expected but got nil")
+	} else {
+		t.Log(err)
+	}
+
+	os.Setenv("GERRIT_BRANCH", "master")
+	if _, err := GetGerritBuildInfo(); err != nil {
+		t.Error("nil expected but got err")
+	}
+}
diff --git a/service/gerrit/change_diff_test.go b/service/gerrit/change_diff_test.go
new file mode 100644
index 00000000..2dbf137a
--- /dev/null
+++ b/service/gerrit/change_diff_test.go
@@ -0,0 +1,41 @@
+package gerrit
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"golang.org/x/build/gerrit"
+)
+
+func TestChangeDiff_Diff(t *testing.T) {
+	getChangeDetailAPICall := 0
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/changes/changeID/detail", func(w http.ResponseWriter, r *http.Request) {
+		getChangeDetailAPICall++
+		if r.Method != http.MethodGet {
+			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
+		}
+
+		fmt.Fprintf(w, ")]}\n{\"current_revision\": \"HEAD\"}")
+	})
+
+	ts := httptest.NewServer(mux)
+	defer ts.Close()
+
+	cli := gerrit.NewClient(ts.URL, gerrit.NoAuth)
+
+	g, err := NewChangeDiff(cli, "HEAD^", "changeID")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if _, err := g.Diff(context.Background()); err != nil {
+		t.Fatal(err)
+	}
+	if getChangeDetailAPICall != 1 {
+		t.Errorf("Get GitLab MergeRequest API called %v times, want once", getChangeDetailAPICall)
+	}
+}
diff --git a/service/gerrit/change_review_test.go b/service/gerrit/change_review_test.go
new file mode 100644
index 00000000..bc6facd6
--- /dev/null
+++ b/service/gerrit/change_review_test.go
@@ -0,0 +1,99 @@
+package gerrit
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/reviewdog/reviewdog"
+	"golang.org/x/build/gerrit"
+)
+
+func TestChangeReviewCommenter_Post_Flush(t *testing.T) {
+	cwd, _ := os.Getwd()
+	defer func(dir string) {
+		if err := os.Chdir(dir); err != nil {
+			t.Error(err)
+		}
+	}(cwd)
+	if err := os.Chdir("../.."); err != nil {
+		t.Error(err)
+	}
+
+	ctx := context.Background()
+	newLnum1 := 14
+	newComment1 := &reviewdog.Comment{
+		CheckResult: &reviewdog.CheckResult{
+			Path: "file.go",
+			Lnum: newLnum1,
+		},
+		Body: "new comment",
+	}
+	newLnum2 := 15
+	newComment2 := &reviewdog.Comment{
+		CheckResult: &reviewdog.CheckResult{
+			Path: "file2.go",
+			Lnum: newLnum2,
+		},
+		Body: "new comment 2",
+	}
+
+	comments := []*reviewdog.Comment{
+		newComment1,
+		newComment2,
+	}
+
+	mux := http.NewServeMux()
+	mux.HandleFunc(`/changes/testChangeID/revisions/testRevisionID/review`, func(w http.ResponseWriter, r *http.Request) {
+		switch r.Method {
+		case http.MethodPost:
+			got := new(gerrit.ReviewInput)
+			if err := json.NewDecoder(r.Body).Decode(got); err != nil {
+				t.Error(err)
+			}
+
+			if len(got.Comments) != len(comments) {
+				t.Error("expected two comments")
+			}
+
+			want := []gerrit.CommentInput{{Line: newComment1.Lnum, Message: newComment1.Body}}
+			if diff := cmp.Diff(got.Comments["file.go"], want); diff != "" {
+				t.Error(diff)
+			}
+
+			want = []gerrit.CommentInput{{Line: newComment2.Lnum, Message: newComment2.Body}}
+			if diff := cmp.Diff(got.Comments["file2.go"], want); diff != "" {
+				t.Error(diff)
+			}
+
+			fmt.Fprintf(w, ")]}\n{}")
+		default:
+			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
+		}
+	})
+
+	ts := httptest.NewServer(mux)
+	defer ts.Close()
+
+	cli := gerrit.NewClient(ts.URL, gerrit.NoAuth)
+
+	g, err := NewChangeReviewCommenter(cli, "testChangeID", "testRevisionID")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for _, c := range comments {
+		if err := g.Post(ctx, c); err != nil {
+			t.Error(err)
+		}
+	}
+
+	if err := g.Flush(ctx); err != nil {
+		t.Errorf("%v", err)
+	}
+}