├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── release.yml │ └── testing.yml ├── .gitignore ├── .goreleaser.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── access.go ├── access_test.go ├── accounts.go ├── authentication.go ├── changes.go ├── changes_attention.go ├── changes_edit.go ├── changes_hashtags.go ├── changes_hashtags_test.go ├── changes_reviewer.go ├── changes_reviewer_test.go ├── changes_revision.go ├── changes_revision_test.go ├── changes_test.go ├── config.go ├── config_test.go ├── doc.go ├── events.go ├── events_test.go ├── examples ├── create_new_project │ └── main.go ├── get_changes │ └── main.go ├── get_gerrit_version │ └── main.go └── get_public_projects │ └── main.go ├── gerrit.go ├── gerrit_test.go ├── go.mod ├── go.sum ├── groups.go ├── groups_include.go ├── groups_member.go ├── img └── logo.png ├── plugins.go ├── projects.go ├── projects_access.go ├── projects_access_test.go ├── projects_branch.go ├── projects_childproject.go ├── projects_commit.go ├── projects_commit_test.go ├── projects_dashboard.go ├── projects_tag.go ├── projects_tag_test.go ├── projects_test.go ├── types.go └── types_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: andygrunwald 2 | custom: "https://paypal.me/andygrunwald" -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "gomod" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "monthly" 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: "1.23" 24 | 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v6 27 | with: 28 | distribution: goreleaser 29 | version: "~> v2" 30 | args: release --clean 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | schedule: 10 | - cron: "5 1 * * *" 11 | 12 | jobs: 13 | gofmt: 14 | name: go fmt (Go ${{ matrix.go }}) 15 | runs-on: ubuntu-24.04 16 | strategy: 17 | matrix: 18 | go: ["1.23", "1.22"] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go }} 25 | 26 | - name: Run go fmt 27 | if: runner.os != 'Windows' 28 | run: diff -u <(echo -n) <(gofmt -d -s .) 29 | 30 | govet: 31 | name: go vet (Go ${{ matrix.go }}) 32 | runs-on: ubuntu-24.04 33 | strategy: 34 | matrix: 35 | go: ["1.23", "1.22"] 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: actions/setup-go@v5 40 | with: 41 | go-version: ${{ matrix.go }} 42 | 43 | - name: Run go vet 44 | run: make vet 45 | 46 | staticcheck: 47 | name: staticcheck (Go ${{ matrix.go }}) 48 | runs-on: ubuntu-24.04 49 | strategy: 50 | matrix: 51 | go: ["1.23", "1.22"] 52 | 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: actions/setup-go@v5 56 | with: 57 | go-version: ${{ matrix.go }} 58 | 59 | - name: Run staticcheck 60 | uses: dominikh/staticcheck-action@v1.3.1 61 | with: 62 | version: "2024.1.1" 63 | install-go: false 64 | cache-key: ${{ matrix.go }} 65 | 66 | unittesting: 67 | name: unit testing (Go ${{ matrix.go }}) 68 | runs-on: ubuntu-24.04 69 | strategy: 70 | matrix: 71 | go: ["1.23", "1.22"] 72 | 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: actions/setup-go@v5 76 | with: 77 | go-version: ${{ matrix.go }} 78 | 79 | - name: Run Unit tests. 80 | run: make test 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage.txt 2 | dist/ 3 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 3 | version: 2 4 | 5 | before: 6 | hooks: 7 | - go mod tidy 8 | 9 | builds: 10 | - skip: true 11 | 12 | changelog: 13 | use: github 14 | sort: asc 15 | filters: 16 | exclude: 17 | - "^docs:" 18 | - "^test:" 19 | 20 | release: 21 | name_template: "v{{ .Version }}" 22 | footer: | 23 | **Full Changelog**: https://github.com/andygrunwald/go-gerrit/compare/{{ .PreviousTag }}...{{ if .IsNightly }}nightly{{ else }}{{ .Tag }}{{ end }} 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This is a high level log of changes, bugfixes, enhancements, etc 4 | that have taken place between releases. Later versions are shown 5 | first. For more complete details see 6 | [the releases on GitHub.](https://github.com/andygrunwald/go-gerrit/releases) 7 | 8 | ## Versions 9 | 10 | ### 1.0.0 (2024-10-20) 11 | 12 | This is the first release in 7 years (since 2017-11-04). 13 | The project itself was alive and received new features and bug fixes during this time. 14 | Only the creation of an "official" release was absent. 15 | 16 | If you were/are using the latest head branch, it is safe to switch to v1.0.0. 17 | No new breaking changes have been introduced. 18 | 19 | If you are upgrading from v0.5.2 (2017-11-04), please check out the breaking changes. 20 | 21 | **WARNING**: This release includes breaking changes. These changes are prefixed by `[BREAKING CHANGE]`. 22 | 23 | #### Features 24 | 25 | 18c7753 Add support for submit_requirement fields in ChangeInfo. (#169) 26 | 0983e87 Update CherryPickInput to support new fields (#165) 27 | bb1cabe Update SubmitInput entity to support new fields (#163) 28 | 444a268 Add missing fields `Error` and `ChangeInfo` to ReviewResult (#160) 29 | 235fa61 Implement parents_data in RevisionInfo (#157) 30 | 40e4f30 [BREAKING CHANGE] Add golang context support (#153) 31 | f01a53b Add `Strategy`, `AllowConflicts`, `OnBehalfOfUploader`, `CommitterEmail` and `ValidationOptions` fields to RebaseInput (#155) 32 | 4f1401b AccountExternalIdInfo/Accounts.GetAccountExternalIDs: Adjusted docs according the official documentation 33 | 8a06083 Add AccountService.QueryAccounts 34 | 1d229fd Add AccountService.GetAccountExternalIDs 35 | 999473b Add fields `SecondaryEmails`, `Status`, `Inactive` and `Tags` in AccountInfo 36 | c44fe2f Add create project's tag and delete project's tag and tags (#146) 37 | 2dcb9fb Add support for project deletion (with plugin) (#147) 38 | 423d372 add Comment{Input,Info} fields (#145) 39 | 8cd0d63 Add GetHashtags/SetHashtags (#141) 40 | 9ea5350 Add revision kind to RevisionInfo (#135) 41 | 5de64ee changes: Add RemoveAttention (#124) 42 | 2e881e2 Add fields `Reviewers`, `Ready`, `WorkInProgress`, `AddToAttentionSet`, `RemoveFromAttentionSet` and `IgnoreAutomaticAttentionSetRules` to ReviewInput (#123) 43 | f645b08 add: project commits included in (#122) 44 | d3e91fb Add support for Change API's "Set Ready-For-Review" endpoint (#110) 45 | ff14d06 Groups: Add _more_groups attribute to GroupInfo 46 | 67c9345 [BREAKING CHANGE] Fix #92: Add pagination support for SuggestAccount 47 | 67e5874 changes: fill in the rest of Changes.ChangeInput (#97) 48 | 101051e Fix #92: Add _more_accounts attribute and start parameter to paginate through SuggestAccount results 49 | dcedff1 Add more supported fields to the ChangeInfo struct (#90) 50 | 4e82ec7 Add Changes.MoveChange (#81) 51 | 590067b Added go modules (#83) 52 | ed2419a Add support for Change API's "Set Commit Message" endpoint (#80) 53 | 5c7c90c feat: add access method for project 54 | b4b4fdb Add fields `Groups` and `ConfigWebLinks` to `ProjectAccessInfo` 55 | 5959a9b add ReviewerUpdates field to ChangeInfo 56 | 759bda1 feat: add submittable field for change info 57 | 64931d2 add Hashtags field to ChangeInfo 58 | 19ef3e9 add tests for project.GetBranch 59 | 197fe0d Add types for robot comments 60 | 5ea6031 Add Reviewers field to ChangeInfo 61 | 5632c7f Expose the client's BaseURL. 62 | 90fea2d Improve consistency of baseURL trailing slash handling. 63 | 70bbb05 [BREAKING CHANGE] Use Timestamp type for all time fields. 64 | 5416038 Add Avatars field to AccountInfo. 65 | 2e8da2e Add FilesOptions to ListFiles, ListFilesReviewed. 66 | 5ff0cbc Add missing fields to ChangeMessageInfo, FileInfo. 67 | 3418ea4 Add PatchOptions.Path field. 68 | 0655566 Added comment to DiffIntralineInfo from gerrit rest api docs (as suggested by @shurcooL). 69 | 70 | #### Bugfixes 71 | 72 | 7c5be02 Fix MoveInput struct definition (keep_all_labels -> keep_all_votes) (#161) 73 | b85f9f5 [BREAKING CHANGE] Change ReviewInput to use map[string]int for labels (#159) 74 | b24a961 Escape % character in fm.Fprint (#106) 75 | 918f939 fix: remove a extra characters in url path (#103) 76 | 668ecf2 [BREAKING CHANGE] changes: rename Changes.DeleteDraftChange to DeleteChange (#95) 77 | 34f3353 fix: omit optional empty fields in ProjectInput (#94) 78 | cc4e14e fix: no need to send content-type header if no header sent (#91) 79 | eab0848 omitempty for AbandonInput.Notify and AbandonInput.NotifyDetails fields (#89) 80 | 3f5e365 fix: fix url error in `GroupsService.AddGroupMembers`, `GroupsService.DeleteGroupMember`, `ProjectsService.GetConfig`, `ProjectsService.SetProjectDescription`, `ProjectsService.DeleteProjectDescription`, `ProjectsService.BanCommit`, `ProjectsService.SetConfig`, `ProjectsService.SetHEAD`, `ProjectsService.SetProjectParent` and `ProjectsService.RunGC` 81 | adf825f Fix JSON tags of `CheckAccessOptions` and added unit test `TestProjectsService_ListAccessRights` 82 | 6761407 Renamed projects_test.go to projects_access_test.go 83 | ea89ae5 fix: Rename `ExampleChangesService_QueryChanges_WithSubmittable` to `ExampleChangesService_QueryChanges_withSubmittable` 84 | 7b0d1f8 escape branch names in the query as well 85 | 43cfd7a Fix GetCommitContent to actually get commit content 86 | 30ce279 Make code more readable and consistent regards return values 87 | 5f93656 Fixed handling of unhandled error 88 | aab0406 [BREAKING CHANGE] Rename struct fields according go rules 89 | 3c4bc0f Fixed #44. DiffContent.A/B/AB should be []string instead of string. DiffIntralineInfo should be [][2]int. 90 | 91 | #### Removal and deprecations 92 | 93 | 294d14b [BREAKING CHANGE] Remove CreateChangeDeprecated 94 | 7b2c737 api: deprecate CreateChange using ChangeInfo (use ChangeInput instead) 95 | 96 | #### Chore and automation 97 | 98 | d21ca62 Introduce goreleaser to make the release of a new version easier 99 | 08ff0c0 Github Actions: Upgrade supported Go versions from 1.21, 1.22 to 1.22, 1.23 (#172) 100 | d093e01 Github Actions: Fix Actions trigger on branch name (#171) 101 | b410a34 Github Actions: Upgrade `runs-on` image from ubuntu-22.04 to ubtuntu-24.04 (#170) 102 | c725a0f Update Go version (add v1.22, remove v1.20) (#164) 103 | e8bb8e4 Bump dominikh/staticcheck-action from 1.3.0 to 1.3.1 (#162) 104 | 6478d30 Bump actions/setup-go from 4 to 5 (#156) 105 | 89fb5cf Bump actions/checkout from 3 to 4 (#154) 106 | d2361a1 Update Go version for testing: Remove 1.19, add 1.21 (#151) 107 | e9d8f54 Update dependabot.yml: Set interval to monthly 108 | da63a5c GitHub Actions: Upgrade Ubuntu + Go version and remove Caching 109 | 34adb1f Bump actions/setup-go from 3 to 4 (#144) 110 | cf782c5 Bump actions/cache from 3.3.0 to 3.3.1 (#143) 111 | 3ee7c89 Bump actions/cache from 3.2.6 to 3.3.0 (#142) 112 | d865180 Bump actions/cache from 3.2.5 to 3.2.6 (#140) 113 | 04e01d7 Upgrade static check to v2023.1 (#139) 114 | 7e1bcf3 go mod tidy (#137) 115 | 7e847d2 Bump actions/cache from 3.2.4 to 3.2.5 (#136) 116 | 15d9b44 Bump dominikh/staticcheck-action from 1.2.0 to 1.3.0 (#132) 117 | 14e2304 Bump actions/cache from 3.0.11 to 3.2.4 (#134) 118 | 952a13d Upgrade Go versions: Removing v1.18, adding v1.20 119 | f726227 Bump actions/cache from 3.0.8 to 3.0.11 (#128) 120 | 41749a6 GitHub Actions: Raise Go versions to v1.18 and v1.19 (#125) 121 | 4fc9999 Bump actions/cache from 3.0.7 to 3.0.8 (#121) 122 | b678d1c Bump actions/cache from 3.0.5 to 3.0.7 (#120) 123 | 4861c8c Bump actions/cache from 3.0.4 to 3.0.5 (#118) 124 | 28cf26f Bump actions/cache from 3.0.3 to 3.0.4 (#114) 125 | 2d5a93b Bump actions/cache from 3.0.2 to 3.0.3 (#113) 126 | 4bec308 Bump actions/cache from 3.0.1 to 3.0.2 (#108) 127 | 5f47610 Bump actions/setup-go from 2 to 3 (#109) 128 | 525eecd Bump dominikh/staticcheck-action from 1.1.0 to 1.2.0 (#107) 129 | 79adba8 Bump actions/checkout from 2 to 3 (#104) 130 | 6928c4f Bump actions/cache from 2 to 3.0.1 (#105) 131 | 90f99d2 Fix staticcheck in GitHub CI 132 | 5105eaf Switch CI to only support v1.17 und v1.18 133 | 916b31a Configured dependabot 134 | 58f949a go mod tidy 135 | d813ef1 Run testing on a new pull request 136 | 1154c96 Testing: Use go version '1.17', '1.16', '1.15' 137 | 9d38b0b Bump github.com/google/go-querystring from 1.0.0 to 1.1.0 (#87) 138 | 58fd2fe Upgrade to GitHub-native Dependabot (#88) 139 | e56a0c4 Rework CI: Removed travis + gometalinter, added GitHub Actions + staticcheck (#82) 140 | 9181c5d Fix order of package imports 141 | 78ea334 drop support for Go 1.9, add Go 1.12 142 | 174420e Remove "unused" from gometalinter 143 | f48c3d1 Drop support for go v1.8 144 | eefac78 gometalinter: Renamed linter gas to gosec 145 | 9e4f624 TravisCI: Added Go 1.11 to TravisCI build matrix 146 | 201f25d gometalinter: Rename gas security checker to gosec 147 | c3ce3c2 Fix vet issue. 148 | a9c12d7 Require Go 1.8 or newer. 149 | 150 | #### Documentation 151 | 152 | 1fe64e5 Changes: Add documentation link to ChangeInput 153 | 027139b Improve documentation consistency. 154 | 155 | #### README and examples 156 | 157 | 5a2c9c2 New example: Creating a project 158 | 024fc51 Add section about "Development" into README 159 | 3fc80f9 Smaller README rework and example directory 160 | 46815e4 README: Follow the Go Release Policy (#85) 161 | 8fc5ccb Rework README: Changed godoc links, Renamed Go(lang) to Go, added make commands (#84) 162 | 163 | #### Other 164 | 165 | c9ff84f Shorten paypal link for sponsoring 166 | 5dc4910 Add sponsoring information 167 | 168 | ### 0.5.2 (2017-11-04) 169 | 170 | * Fix panic in checkAuth() if Gerrit is down #42 171 | * Implement ListVotes(), DeleteVotes() and add missing tests 172 | 173 | ### 0.5.1 (2017-09-14) 174 | 175 | * Added the `AbandonChange`, `RebaseChange`, `RestoreChange` and 176 | `RevertChange` functions. 177 | 178 | ### 0.5.0 (2017-09-11) 179 | 180 | **WARNING**: This release includes breaking changes. 181 | 182 | * [BREAKING CHANGE] The SetReview function was returning the wrong 183 | entity type. (#40) 184 | 185 | ### 0.4.0 (2017-09-05) 186 | 187 | **WARNING**: This release includes breaking changes. 188 | 189 | * [BREAKING CHANGE] - Added gometalinter to the build and fixed problems 190 | discovered by the linters. 191 | * Comment and error string fixes. 192 | * Numerous lint and styling fixes. 193 | * Ensured error values are being properly checked where appropriate. 194 | * Addition of missing documentation 195 | * Removed filePath parameter from DeleteChangeEdit which was unused and 196 | unnecessary for the request. 197 | * Fixed CherryPickRevision and IncludeGroups functions which didn't pass 198 | along the provided input structs into the request. 199 | * Go 1.5 has been removed from testing on Travis. The linters introduced in 200 | 0.4.0 do not support this version, Go 1.5 is lacking security updates and 201 | most Linux distros have moved beyond Go 1.5 now. 202 | * Add Go 1.9 to the Travis matrix. 203 | * Fixed an issue where urls containing certain characters in the credentials 204 | could cause NewClient() to use an invalid url. Something like `/`, which 205 | Gerrit could use for generated passwords, for example would break url.Parse's 206 | expectations. 207 | 208 | ### 0.3.0 (2017-06-05) 209 | 210 | **WARNING**: This release includes breaking changes. 211 | 212 | * [BREAKING CHANGE] Fix Changes.PublishDraftChange to accept a notify parameter. 213 | * [BREAKING CHANGE] Fix PublishChangeEdit to accept a notify parameter. 214 | * [BREAKING CHANGE] Fix ChangeFileContentInChangeEdit to allow the file content 215 | to be included in the request. 216 | * Fix the url being used by CreateChange 217 | * Fix type serialization of EventInfo.PatchSet.Number so it's consistent. 218 | * Fix Changes.AddReviewer so it passes along the reviewer to the request. 219 | * Simplify and optimize RemoveMagicPrefixLine 220 | 221 | ### 0.2.0 (2016-11-15) 222 | 223 | **WARNING**: This release includes breaking changes. 224 | 225 | * [BREAKING CHANGE] Several bugfixes to GetEvents: 226 | * Update EventInfo to handle the changeKey field and apply 227 | the proper type for the Project field 228 | * Provide a means to ignore marshaling errors 229 | * Update GetEvents() to return the failed lines and remove 230 | the pointer to the return value because it's unnecessary. 231 | * [BREAKING CHANGE] In ec28f77 `ChangeInfo.Labels` has been changed to map 232 | to fix #21. 233 | 234 | ### 0.1.1 (2016-11-05) 235 | 236 | * Minor fix to SubmitChange to use the `http.StatusConflict` constant 237 | instead of a hard coded value when comparing response codes. 238 | * Updated AccountInfo.AccountID to be omitted of empty (such as when 239 | used in ApprovalInfo). 240 | * + and : in url parameters for queries are no longer escaped. This was 241 | causing `400 Bad Request` to be returned when the + symbol was 242 | included as part of the query. To match behavior with Gerrit's search 243 | handling, the : symbol was also excluded. 244 | * Fixed documentation for NewClient and moved fmt.Errorf call from 245 | inside the function to a `ErrNoInstanceGiven` variable so it's 246 | easier to compare against. 247 | * Updated internal function digestAuthHeader to return exported errors 248 | (ErrWWWAuthenticateHeader*) rather than calling fmt.Errorf. This makes 249 | it easier to test against externally and also fixes a lint issue too. 250 | * Updated NewClient function to handle credentials in the url. 251 | * Added the missing `Submitted` field to `ChangeInfo`. 252 | * Added the missing `URL` field to `ChangeInfo` which is usually included 253 | as part of an event from the events-log plugin. 254 | 255 | ### 0.1.0 (2016-10-08) 256 | 257 | * The first official release 258 | * Implemented digest auth and several fixes for it. 259 | * Ensured Content-Type is included in all requests 260 | * Fixed several internal bugs as well as a few documentation issues 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andy Grunwald 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | .PHONY: help 4 | help: ## Outputs the help 5 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 6 | 7 | .PHONY: test 8 | test: ## Runs all unit tests 9 | go test -v -race ./... 10 | 11 | .PHONY: vet 12 | vet: ## Runs go vet 13 | go vet ./... 14 | 15 | .PHONY: staticcheck 16 | staticcheck: ## Runs static code analyzer staticcheck 17 | go get -u honnef.co/go/tools/cmd/staticcheck 18 | staticcheck ./... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-gerrit 2 | 3 | [![GoDoc](https://pkg.go.dev/badge/github.com/andygrunwald/go-gerrit?utm_source=godoc)](https://pkg.go.dev/github.com/andygrunwald/go-gerrit) 4 | 5 | go-gerrit is a [Go](https://golang.org/) client library for the [Gerrit Code Review](https://www.gerritcodereview.com/) system. 6 | 7 | ![go-gerrit - Go client/library for Gerrit Code Review](./img/logo.png "go-gerrit - Go client/library for Gerrit Code Review") 8 | 9 | ## Features 10 | 11 | * [Authentication](https://pkg.go.dev/github.com/andygrunwald/go-gerrit#AuthenticationService) (HTTP Basic, HTTP Digest, HTTP Cookie) 12 | * Every API Endpoint like Gerrit 13 | * [/access/](https://pkg.go.dev/github.com/andygrunwald/go-gerrit#AccessService) 14 | * [/accounts/](https://pkg.go.dev/github.com/andygrunwald/go-gerrit#AccountsService) 15 | * [/changes/](https://pkg.go.dev/github.com/andygrunwald/go-gerrit#ChangesService) 16 | * [/config/](https://pkg.go.dev/github.com/andygrunwald/go-gerrit#ConfigService) 17 | * [/groups/](https://pkg.go.dev/github.com/andygrunwald/go-gerrit#GroupsService) 18 | * [/plugins/](https://pkg.go.dev/github.com/andygrunwald/go-gerrit#PluginsService) 19 | * [/projects/](https://pkg.go.dev/github.com/andygrunwald/go-gerrit#ProjectsService) 20 | * Supports optional plugin APIs such as 21 | * events-log - [About](https://gerrit.googlesource.com/plugins/events-log/+/master/src/main/resources/Documentation/about.md), [REST API](https://gerrit.googlesource.com/plugins/events-log/+/master/src/main/resources/Documentation/rest-api-events.md) 22 | 23 | ## Installation 24 | 25 | _go-gerrit_ follows the [Go Release Policy](https://golang.org/doc/devel/release.html#policy). 26 | This means we support the current + 2 previous Go versions. 27 | 28 | It is go gettable ... 29 | 30 | ```sh 31 | $ go get github.com/andygrunwald/go-gerrit 32 | ``` 33 | 34 | ## API / Usage 35 | 36 | Have a look at the [GoDoc documentation](https://pkg.go.dev/github.com/andygrunwald/go-gerrit) for a detailed API description. 37 | 38 | The [Gerrit Code Review - REST API](https://gerrit-review.googlesource.com/Documentation/rest-api.html) was the foundation document. 39 | 40 | ### Authentication 41 | 42 | Gerrit supports multiple ways for [authentication](https://gerrit-review.googlesource.com/Documentation/rest-api.html#authentication). 43 | 44 | #### HTTP Basic 45 | 46 | Some Gerrit instances (like [TYPO3](https://review.typo3.org/)) has [auth.gitBasicAuth](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#auth.gitBasicAuth) activated. 47 | With this, you can authenticate with HTTP Basic like this: 48 | 49 | ```go 50 | instance := "https://review.typo3.org/" 51 | client, _ := gerrit.NewClient(instance, nil) 52 | client.Authentication.SetBasicAuth("andy.grunwald", "my secrect password") 53 | 54 | self, _, _ := client.Accounts.GetAccount("self") 55 | 56 | fmt.Printf("Username: %s", self.Name) 57 | 58 | // Username: Andy Grunwald 59 | ``` 60 | 61 | If you get a `401 Unauthorized`, check your Account Settings and have a look at the `HTTP Password` configuration. 62 | 63 | #### HTTP Digest 64 | 65 | Some Gerrit instances (like [Wikimedia](https://gerrit.wikimedia.org/)) has [Digest access authentication](https://en.wikipedia.org/wiki/Digest_access_authentication) activated. 66 | 67 | ```go 68 | instance := "https://gerrit.wikimedia.org/r/" 69 | client, _ := gerrit.NewClient(instance, nil) 70 | client.Authentication.SetDigestAuth("andy.grunwald", "my secrect http password") 71 | 72 | self, resp, err := client.Accounts.GetAccount("self") 73 | 74 | fmt.Printf("Username: %s", self.Name) 75 | 76 | // Username: Andy Grunwald 77 | ``` 78 | 79 | If the chosen Gerrit instance does not support digest auth, an error like `WWW-Authenticate header type is not Digest` is thrown. 80 | 81 | If you get a `401 Unauthorized`, check your Account Settings and have a look at the `HTTP Password` configuration. 82 | 83 | #### HTTP Cookie 84 | 85 | Some Gerrit instances hosted like the one hosted googlesource.com (e.g. [Go](https://go-review.googlesource.com/), [Android](https://android-review.googlesource.com/) or [Gerrit](https://gerrit-review.googlesource.com/)) support HTTP Cookie authentication. 86 | 87 | You need the cookie name and the cookie value. 88 | You can get them by click on "Settings > HTTP Password > Obtain Password" in your Gerrit instance. 89 | 90 | There you can receive your values. 91 | The cookie name will be (mostly) `o` (if hosted on googlesource.com). 92 | Your cookie secret will be something like `git-your@email.com=SomeHash...`. 93 | 94 | ```go 95 | instance := "https://gerrit-review.googlesource.com/" 96 | client, _ := gerrit.NewClient(instance, nil) 97 | client.Authentication.SetCookieAuth("o", "my-cookie-secret") 98 | 99 | self, _, _ := client.Accounts.GetAccount("self") 100 | 101 | fmt.Printf("Username: %s", self.Name) 102 | 103 | // Username: Andy G. 104 | ``` 105 | 106 | ## Examples 107 | 108 | More examples are available 109 | 110 | * in the [GoDoc examples section](https://pkg.go.dev/github.com/andygrunwald/go-gerrit#pkg-examples). 111 | * in the [examples folder](./examples) 112 | 113 | ### Get version of Gerrit instance 114 | 115 | Receive the version of the [Gerrit instance used by the Gerrit team](https://gerrit-review.googlesource.com/) for development: 116 | 117 | ```go 118 | package main 119 | 120 | import ( 121 | "fmt" 122 | 123 | "github.com/andygrunwald/go-gerrit" 124 | ) 125 | 126 | func main() { 127 | instance := "https://gerrit-review.googlesource.com/" 128 | client, err := gerrit.NewClient(instance, nil) 129 | if err != nil { 130 | panic(err) 131 | } 132 | 133 | v, _, err := client.Config.GetVersion() 134 | if err != nil { 135 | panic(err) 136 | } 137 | 138 | fmt.Printf("Version: %s", v) 139 | 140 | // Version: 3.4.1-2066-g8db5605430 141 | } 142 | ``` 143 | 144 | ### Get all public projects 145 | 146 | List all projects from [Chromium](https://chromium-review.googlesource.com/): 147 | 148 | ```go 149 | package main 150 | 151 | import ( 152 | "fmt" 153 | 154 | "github.com/andygrunwald/go-gerrit" 155 | ) 156 | 157 | func main() { 158 | instance := "https://chromium-review.googlesource.com/" 159 | client, err := gerrit.NewClient(instance, nil) 160 | if err != nil { 161 | panic(err) 162 | } 163 | 164 | opt := &gerrit.ProjectOptions{ 165 | Description: true, 166 | } 167 | projects, _, err := client.Projects.ListProjects(opt) 168 | if err != nil { 169 | panic(err) 170 | } 171 | 172 | for name, p := range *projects { 173 | fmt.Printf("%s - State: %s\n", name, p.State) 174 | } 175 | 176 | // chromiumos/third_party/bluez - State: ACTIVE 177 | // external/github.com/Polymer/ShadowDOM - State: ACTIVE 178 | // external/github.com/domokit/mojo_sdk - State: ACTIVE 179 | // ... 180 | } 181 | ``` 182 | 183 | ### Query changes 184 | 185 | Get some changes of the [kernel/common project](https://android-review.googlesource.com/#/q/project:kernel/common) from the [Android](http://source.android.com/)[Gerrit Review System](https://android-review.googlesource.com/). 186 | 187 | ```go 188 | package main 189 | 190 | import ( 191 | "fmt" 192 | 193 | "github.com/andygrunwald/go-gerrit" 194 | ) 195 | 196 | func main() { 197 | instance := "https://android-review.googlesource.com/" 198 | client, err := gerrit.NewClient(instance, nil) 199 | if err != nil { 200 | panic(err) 201 | } 202 | 203 | opt := &gerrit.QueryChangeOptions{} 204 | opt.Query = []string{"project:kernel/common"} 205 | opt.AdditionalFields = []string{"LABELS"} 206 | changes, _, err := client.Changes.QueryChanges(opt) 207 | if err != nil { 208 | panic(err) 209 | } 210 | 211 | for _, change := range *changes { 212 | fmt.Printf("Project: %s -> %s -> %s%d\n", change.Project, change.Subject, instance, change.Number) 213 | } 214 | 215 | // Project: kernel/common -> ANDROID: GKI: Update symbols to symbol list -> https://android-review.googlesource.com/1830553 216 | // Project: kernel/common -> ANDROID: db845c_gki.fragment: Remove CONFIG_USB_NET_AX8817X from fragment -> https://android-review.googlesource.com/1830439 217 | // Project: kernel/common -> ANDROID: Update the ABI representation -> https://android-review.googlesource.com/1830469 218 | // ... 219 | } 220 | ``` 221 | 222 | ## Development 223 | 224 | ### Running tests and linters 225 | 226 | Tests only: 227 | 228 | ```sh 229 | $ make test 230 | ``` 231 | 232 | Checks, tests and linters 233 | 234 | ```sh 235 | $ make vet staticcheck test 236 | ``` 237 | 238 | ### Local Gerrit setup 239 | 240 | For local development, we suggest the usage of the [official Gerrit Code Review docker image](https://hub.docker.com/r/gerritcodereview/gerrit): 241 | 242 | ``` 243 | $ docker run -ti -p 8080:8080 -p 29418:29418 gerritcodereview/gerrit:3.4.1 244 | ``` 245 | 246 | Wait a few minutes until the ```Gerrit Code Review NNN ready``` message appears, 247 | where NNN is your current Gerrit version, then open your browser to http://localhost:8080 248 | and you will be in Gerrit Code Review. 249 | 250 | #### Authentication 251 | 252 | For local development setups, go to http://localhost:8080/settings/#HTTPCredentials and click `GENERATE NEW PASSWORD`. 253 | Now you can use (only for development purposes): 254 | 255 | ```go 256 | client.Authentication.SetBasicAuth("admin", "secret") 257 | ``` 258 | 259 | Replace `secret` with your new value. 260 | 261 | ## Frequently Asked Questions (FAQ) 262 | 263 | ### How is the source code organized? 264 | 265 | The source code organization is inspired by [go-github by Google](https://github.com/google/go-github). 266 | 267 | Every REST API Endpoint (e.g. [`/access/`](https://gerrit-review.googlesource.com/Documentation/rest-api-access.html), [`/changes/`](https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html)) is coupled in a service (e.g. [`AccessService` in access.go](./access.go), [`ChangesService` in changes.go](./changes.go)). 268 | Every service is part of [`gerrit.Client`](./gerrit.go) as a member variable. 269 | 270 | `gerrit.Client` can provide essential helper functions to avoid unnecessary code duplications, such as building a new request or parse responses. 271 | 272 | Based on this structure, implementing a new API functionality is straight forward. 273 | Here is an example of `*ChangeService.DeleteTopic*` / [DELETE /changes/{change-id}/topic](https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-topic): 274 | 275 | ```go 276 | func (s *ChangesService) DeleteTopic(changeID string) (*Response, error) { 277 | u := fmt.Sprintf("changes/%s/topic", changeID) 278 | return s.client.DeleteRequest(u, nil) 279 | } 280 | ``` 281 | 282 | ### What about the version compatibility with Gerrit? 283 | 284 | The library was implemented based on the REST API of Gerrit version 2.11.3-1230-gb8336f1 and tested against this version. 285 | 286 | This library might be working with older versions as well. 287 | If you notice an incompatibility [open a new issue](https://github.com/andygrunwald/go-gerrit/issues/new). 288 | We also appreciate your Pull Requests to improve this library. 289 | We welcome contributions! 290 | 291 | ### What about adding code to support the REST API of an (optional) plugin? 292 | 293 | It will depend on the plugin, and you are welcome to [open a new issue](https://github.com/andygrunwald/go-gerrit/issues/new) first to propose the idea and use-case. 294 | As an example, the addition of support for `events-log` plugin was supported because the plugin itself is fairly 295 | popular. 296 | The structures that the REST API uses could also be used by `gerrit stream-events`. 297 | 298 | ## License 299 | 300 | This project is released under the terms of the [MIT license](https://choosealicense.com/licenses/mit/). 301 | -------------------------------------------------------------------------------- /access.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import "context" 4 | 5 | // AccessService contains Access Right related REST endpoints 6 | // 7 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-access.html 8 | type AccessService struct { 9 | client *Client 10 | } 11 | 12 | // AccessSectionInfo describes the access rights that are assigned on a ref. 13 | // 14 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-access.html#access-section-info 15 | type AccessSectionInfo struct { 16 | Permissions map[string]PermissionInfo `json:"permissions"` 17 | } 18 | 19 | // PermissionInfo entity contains information about an assigned permission. 20 | // 21 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-access.html#permission-info 22 | type PermissionInfo struct { 23 | Label string `json:"label,omitempty"` 24 | Exclusive bool `json:"exclusive"` 25 | Rules map[string]PermissionRuleInfo `json:"rules"` 26 | } 27 | 28 | // PermissionRuleInfo entity contains information about a permission rule that is assigned to group. 29 | // 30 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-access.html#permission-rule-info 31 | type PermissionRuleInfo struct { 32 | // TODO Possible values for action: ALLOW, DENY or BLOCK, INTERACTIVE and BATCH 33 | Action string `json:"action"` 34 | Force bool `json:"force"` 35 | Min int `json:"min"` 36 | Max int `json:"max"` 37 | } 38 | 39 | // ProjectAccessInfo entity contains information about the access rights for a project. 40 | // 41 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-access.html#project-access-info 42 | type ProjectAccessInfo struct { 43 | Revision string `json:"revision"` 44 | InheritsFrom ProjectInfo `json:"inherits_from"` 45 | Local map[string]AccessSectionInfo `json:"local"` 46 | IsOwner bool `json:"is_owner"` 47 | OwnerOf []string `json:"owner_of"` 48 | CanUpload bool `json:"can_upload"` 49 | CanAdd bool `json:"can_add"` 50 | CanAddTags bool `json:"can_add_tags"` 51 | ConfigVisible bool `json:"config_visible"` 52 | Groups map[string]GroupInfo `json:"groups"` 53 | ConfigWebLinks []string `json:"configWebLinks"` 54 | } 55 | 56 | // ListAccessRightsOptions specifies the parameters to the AccessService.ListAccessRights. 57 | type ListAccessRightsOptions struct { 58 | // The projects for which the access rights should be returned must be specified as project options. 59 | // The project can be specified multiple times. 60 | Project []string `url:"project,omitempty"` 61 | } 62 | 63 | // ListAccessRights lists the access rights for projects. 64 | // As result a map is returned that maps the project name to ProjectAccessInfo entities. 65 | // The entries in the map are sorted by project name. 66 | // 67 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-access.html#list-access 68 | func (s *AccessService) ListAccessRights(ctx context.Context, opt *ListAccessRightsOptions) (*map[string]ProjectAccessInfo, *Response, error) { 69 | u := "access/" 70 | 71 | u, err := addOptions(u, opt) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | 76 | v := new(map[string]ProjectAccessInfo) 77 | resp, err := s.client.Call(ctx, "GET", u, nil, v) 78 | return v, resp, err 79 | } 80 | -------------------------------------------------------------------------------- /access_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/andygrunwald/go-gerrit" 11 | ) 12 | 13 | func TestAccessService_ListAccessRights(t *testing.T) { 14 | setup() 15 | defer teardown() 16 | 17 | testMux.HandleFunc("/access/", func(w http.ResponseWriter, r *http.Request) { 18 | testMethod(t, r, "GET") 19 | testFormValues(t, r, testValues{ 20 | "project": "go", 21 | }) 22 | 23 | fmt.Fprint(w, `)]}'`+"\n"+`{"go":{"revision":"08f45ba74baef9699b650f42022df6467389c1f0","inherits_from":{"id":"All-Projects","name":"All-Projects","description":"Access inherited by all other projects.","state":"ACTIVE"},"local":{},"owner_of":[],"config_visible":false}}`) 24 | }) 25 | 26 | opt := &gerrit.ListAccessRightsOptions{ 27 | Project: []string{"go"}, 28 | } 29 | access, _, err := testClient.Access.ListAccessRights(context.Background(), opt) 30 | if err != nil { 31 | t.Errorf("Access.ListAccessRights returned error: %v", err) 32 | } 33 | 34 | want := &map[string]gerrit.ProjectAccessInfo{ 35 | "go": { 36 | Revision: "08f45ba74baef9699b650f42022df6467389c1f0", 37 | InheritsFrom: gerrit.ProjectInfo{ 38 | ID: "All-Projects", 39 | Name: "All-Projects", 40 | Parent: "", 41 | Description: "Access inherited by all other projects.", 42 | State: "ACTIVE", 43 | }, 44 | Local: map[string]gerrit.AccessSectionInfo{}, 45 | OwnerOf: []string{}, 46 | ConfigVisible: false, 47 | }, 48 | } 49 | if !reflect.DeepEqual(access, want) { 50 | t.Errorf("Access.ListAccessRights returned %+v, want %+v", access, want) 51 | } 52 | } 53 | 54 | func TestAccessService_ListAccessRights_WithoutOpts(t *testing.T) { 55 | setup() 56 | defer teardown() 57 | 58 | testMux.HandleFunc("/access/", func(w http.ResponseWriter, r *http.Request) { 59 | testMethod(t, r, "GET") 60 | 61 | fmt.Fprint(w, `)]}'`+"\n"+`{}`) 62 | }) 63 | 64 | access, _, err := testClient.Access.ListAccessRights(context.Background(), nil) 65 | if err != nil { 66 | t.Errorf("Access.ListAccessRights returned error: %v", err) 67 | } 68 | 69 | want := &map[string]gerrit.ProjectAccessInfo{} 70 | if !reflect.DeepEqual(access, want) { 71 | t.Errorf("Access.ListAccessRights returned %+v, want %+v", access, want) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /authentication.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "crypto/md5" // nolint: gosec 5 | "crypto/rand" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | // ErrWWWAuthenticateHeaderMissing is returned by digestAuthHeader when the WWW-Authenticate header is missing 16 | ErrWWWAuthenticateHeaderMissing = errors.New("WWW-Authenticate header is missing") 17 | 18 | // ErrWWWAuthenticateHeaderInvalid is returned by digestAuthHeader when the WWW-Authenticate invalid 19 | ErrWWWAuthenticateHeaderInvalid = errors.New("WWW-Authenticate header is invalid") 20 | 21 | // ErrWWWAuthenticateHeaderNotDigest is returned by digestAuthHeader when the WWW-Authenticate header is not 'Digest' 22 | ErrWWWAuthenticateHeaderNotDigest = errors.New("WWW-Authenticate header type is not Digest") 23 | ) 24 | 25 | const ( 26 | // HTTP Basic Authentication 27 | authTypeBasic = 1 28 | // HTTP Digest Authentication 29 | authTypeDigest = 2 30 | // HTTP Cookie Authentication 31 | authTypeCookie = 3 32 | ) 33 | 34 | // AuthenticationService contains Authentication related functions. 35 | // 36 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#authentication 37 | type AuthenticationService struct { 38 | client *Client 39 | 40 | // Storage for authentication 41 | // Username or name of cookie 42 | name string 43 | // Password or value of cookie 44 | secret string 45 | authType int 46 | } 47 | 48 | // SetBasicAuth sets basic parameters for HTTP Basic auth 49 | func (s *AuthenticationService) SetBasicAuth(username, password string) { 50 | s.name = username 51 | s.secret = password 52 | s.authType = authTypeBasic 53 | } 54 | 55 | // SetDigestAuth sets digest parameters for HTTP Digest auth. 56 | func (s *AuthenticationService) SetDigestAuth(username, password string) { 57 | s.name = username 58 | s.secret = password 59 | s.authType = authTypeDigest 60 | } 61 | 62 | // digestAuthHeader is called by gerrit.Client.Do in the event the server 63 | // returns 401 Unauthorized and authType was set to authTypeDigest. The 64 | // resulting string is used to set the Authorization header before retrying 65 | // the request. 66 | func (s *AuthenticationService) digestAuthHeader(response *http.Response) (string, error) { 67 | authenticateHeader := response.Header.Get("WWW-Authenticate") 68 | if authenticateHeader == "" { 69 | return "", ErrWWWAuthenticateHeaderMissing 70 | } 71 | 72 | split := strings.SplitN(authenticateHeader, " ", 2) 73 | if len(split) != 2 { 74 | return "", ErrWWWAuthenticateHeaderInvalid 75 | } 76 | 77 | if split[0] != "Digest" { 78 | return "", ErrWWWAuthenticateHeaderNotDigest 79 | } 80 | 81 | // Iterate over all the fields from the WWW-Authenticate header 82 | // and create a map of keys and values. 83 | authenticate := map[string]string{} 84 | for _, value := range strings.Split(split[1], ",") { 85 | kv := strings.SplitN(value, "=", 2) 86 | if len(kv) != 2 { 87 | continue 88 | } 89 | 90 | key := strings.Trim(strings.Trim(kv[0], " "), "\"") 91 | value := strings.Trim(strings.Trim(kv[1], " "), "\"") 92 | authenticate[key] = value 93 | } 94 | 95 | // Gerrit usually responds without providing the algorithm. According 96 | // to RFC2617 if no algorithm is provided then the default is to use 97 | // MD5. At the time this code was implemented Gerrit did not appear 98 | // to support other algorithms or provide a means of changing the 99 | // algorithm. 100 | if value, ok := authenticate["algorithm"]; ok { 101 | if value != "MD5" { 102 | return "", fmt.Errorf( 103 | "algorithm not implemented: %s", value) 104 | } 105 | } 106 | 107 | realmHeader := authenticate["realm"] 108 | qopHeader := authenticate["qop"] 109 | nonceHeader := authenticate["nonce"] 110 | 111 | // If the server does not inform us what the uri is supposed 112 | // to be then use the last requests's uri instead. 113 | if _, ok := authenticate["uri"]; !ok { 114 | authenticate["uri"] = response.Request.URL.Path 115 | } 116 | 117 | uriHeader := authenticate["uri"] 118 | 119 | // A1 120 | h := md5.New() // nolint: gosec 121 | A1 := fmt.Sprintf("%s:%s:%s", s.name, realmHeader, s.secret) 122 | if _, err := io.WriteString(h, A1); err != nil { 123 | return "", err 124 | } 125 | HA1 := fmt.Sprintf("%x", h.Sum(nil)) 126 | 127 | // A2 128 | h = md5.New() // nolint: gosec 129 | A2 := fmt.Sprintf("%s:%s", response.Request.Method, uriHeader) 130 | if _, err := io.WriteString(h, A2); err != nil { 131 | return "", err 132 | } 133 | HA2 := fmt.Sprintf("%x", h.Sum(nil)) 134 | 135 | k := make([]byte, 12) 136 | for bytes := 0; bytes < len(k); { 137 | n, err := rand.Read(k[bytes:]) 138 | if err != nil { 139 | return "", fmt.Errorf("cnonce generation failed: %s", err) 140 | } 141 | bytes += n 142 | } 143 | cnonce := base64.StdEncoding.EncodeToString(k) 144 | digest := md5.New() // nolint: gosec 145 | if _, err := digest.Write([]byte(strings.Join([]string{HA1, nonceHeader, "00000001", cnonce, qopHeader, HA2}, ":"))); err != nil { 146 | return "", err 147 | } 148 | responseField := fmt.Sprintf("%x", digest.Sum(nil)) 149 | 150 | return fmt.Sprintf( 151 | `Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc=00000001, qop=%s, response="%s"`, 152 | s.name, realmHeader, nonceHeader, uriHeader, cnonce, qopHeader, responseField), nil 153 | } 154 | 155 | // SetCookieAuth sets basic parameters for HTTP Cookie 156 | func (s *AuthenticationService) SetCookieAuth(name, value string) { 157 | s.name = name 158 | s.secret = value 159 | s.authType = authTypeCookie 160 | } 161 | 162 | // HasBasicAuth checks if the auth type is HTTP Basic auth 163 | func (s *AuthenticationService) HasBasicAuth() bool { 164 | return s.authType == authTypeBasic 165 | } 166 | 167 | // HasDigestAuth checks if the auth type is HTTP Digest based 168 | func (s *AuthenticationService) HasDigestAuth() bool { 169 | return s.authType == authTypeDigest 170 | } 171 | 172 | // HasCookieAuth checks if the auth type is HTTP Cookie based 173 | func (s *AuthenticationService) HasCookieAuth() bool { 174 | return s.authType == authTypeCookie 175 | } 176 | 177 | // HasAuth checks if an auth type is used 178 | func (s *AuthenticationService) HasAuth() bool { 179 | return s.authType > 0 180 | } 181 | 182 | // ResetAuth resets all former authentification settings 183 | func (s *AuthenticationService) ResetAuth() { 184 | s.name = "" 185 | s.secret = "" 186 | s.authType = 0 187 | } 188 | -------------------------------------------------------------------------------- /changes_attention.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // AttentionSetInfo entity contains details of users that are in the attention set. 9 | // 10 | // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#attention-set-info 11 | type AttentionSetInfo struct { 12 | // AccountInfo entity. 13 | Account AccountInfo `json:"account"` 14 | // The timestamp of the last update. 15 | LastUpdate Timestamp `json:"last_update"` 16 | // The reason of for adding or removing the user. 17 | Reason string `json:"reason"` 18 | } 19 | 20 | // Doc: https://gerrit-review.googlesource.com/Documentation/user-notify.html#recipient-types 21 | type RecipientType string 22 | 23 | // AttentionSetInput entity contains details for adding users to the attention 24 | // set and removing them from it. 25 | // 26 | // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#attention-set-input 27 | type AttentionSetInput struct { 28 | User string `json:"user,omitempty"` 29 | Reason string `json:"reason"` 30 | Notify string `json:"notify,omitempty"` 31 | NotifyDetails map[RecipientType]NotifyInfo `json:"notify_details,omitempty"` 32 | } 33 | 34 | // RemoveAttention deletes a single user from the attention set of a change. 35 | // AttentionSetInput.Input must be provided 36 | // 37 | // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#remove-from-attention-set 38 | func (s *ChangesService) RemoveAttention(ctx context.Context, changeID, accountID string, input *AttentionSetInput) (*Response, error) { 39 | u := fmt.Sprintf("changes/%s/attention/%s", changeID, accountID) 40 | 41 | return s.client.DeleteRequest(ctx, u, input) 42 | } 43 | -------------------------------------------------------------------------------- /changes_edit.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // EditInfo entity contains information about a change edit. 10 | type EditInfo struct { 11 | Commit CommitInfo `json:"commit"` 12 | BaseRevision string `json:"baseRevision"` 13 | Fetch map[string]FetchInfo `json:"fetch"` 14 | Files map[string]FileInfo `json:"files,omitempty"` 15 | } 16 | 17 | // EditFileInfo entity contains additional information of a file within a change edit. 18 | type EditFileInfo struct { 19 | WebLinks []WebLinkInfo `json:"web_links,omitempty"` 20 | } 21 | 22 | // ChangeEditDetailOptions specifies the parameters to the ChangesService.GetChangeEditDetails. 23 | // 24 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-edit-detail 25 | type ChangeEditDetailOptions struct { 26 | // When request parameter list is provided the response also includes the file list. 27 | List bool `url:"list,omitempty"` 28 | // When base request parameter is provided the file list is computed against this base revision. 29 | Base bool `url:"base,omitempty"` 30 | // When request parameter download-commands is provided fetch info map is also included. 31 | DownloadCommands bool `url:"download-commands,omitempty"` 32 | } 33 | 34 | // GetChangeEditDetails retrieves a change edit details. 35 | // As response an EditInfo entity is returned that describes the change edit, or “204 No Content” when change edit doesn’t exist for this change. 36 | // Change edits are stored on special branches and there can be max one edit per user per change. 37 | // Edits aren’t tracked in the database. 38 | // 39 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-edit-detail 40 | func (s *ChangesService) GetChangeEditDetails(ctx context.Context, changeID string, opt *ChangeEditDetailOptions) (*EditInfo, *Response, error) { 41 | u := fmt.Sprintf("changes/%s/edit", changeID) 42 | 43 | u, err := addOptions(u, opt) 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | 48 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | 53 | v := new(EditInfo) 54 | resp, err := s.client.Do(req, v) 55 | if err != nil { 56 | return nil, resp, err 57 | } 58 | 59 | return v, resp, err 60 | } 61 | 62 | // RetrieveMetaDataOfAFileFromChangeEdit retrieves meta data of a file from a change edit. 63 | // Currently only web links are returned. 64 | // 65 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-edit-meta-data 66 | func (s *ChangesService) RetrieveMetaDataOfAFileFromChangeEdit(ctx context.Context, changeID, filePath string) (*EditFileInfo, *Response, error) { 67 | u := fmt.Sprintf("changes/%s/edit/%s/meta", changeID, filePath) 68 | 69 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 70 | if err != nil { 71 | return nil, nil, err 72 | } 73 | 74 | v := new(EditFileInfo) 75 | resp, err := s.client.Do(req, v) 76 | if err != nil { 77 | return nil, resp, err 78 | } 79 | 80 | return v, resp, err 81 | } 82 | 83 | // RetrieveCommitMessageFromChangeEdit retrieves commit message from change edit. 84 | // The commit message is returned as base64 encoded string. 85 | // 86 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-edit-message 87 | func (s *ChangesService) RetrieveCommitMessageFromChangeEdit(ctx context.Context, changeID string) (string, *Response, error) { 88 | u := fmt.Sprintf("changes/%s/edit:message", changeID) 89 | return getStringResponseWithoutOptions(ctx, s.client, u) 90 | } 91 | 92 | // ChangeFileContentInChangeEdit put content of a file to a change edit. 93 | // 94 | // When change edit doesn’t exist for this change yet it is created. 95 | // When file content isn’t provided, it is wiped out for that file. 96 | // As response “204 No Content” is returned. 97 | // 98 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#put-edit-file 99 | func (s *ChangesService) ChangeFileContentInChangeEdit(ctx context.Context, changeID, filePath, content string) (*Response, error) { 100 | u := fmt.Sprintf("changes/%s/edit/%s", changeID, url.QueryEscape(filePath)) 101 | 102 | req, err := s.client.NewRawPutRequest(ctx, u, content) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | return s.client.Do(req, nil) 108 | } 109 | 110 | // ChangeCommitMessageInChangeEdit modify commit message. 111 | // The request body needs to include a ChangeEditMessageInput entity. 112 | // 113 | // If a change edit doesn’t exist for this change yet, it is created. 114 | // As response “204 No Content” is returned. 115 | // 116 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#put-change-edit-message 117 | func (s *ChangesService) ChangeCommitMessageInChangeEdit(ctx context.Context, changeID string, input *ChangeEditMessageInput) (*Response, error) { 118 | u := fmt.Sprintf("changes/%s/edit:message", changeID) 119 | 120 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | return s.client.Do(req, nil) 126 | } 127 | 128 | // DeleteFileInChangeEdit deletes a file from a change edit. 129 | // This deletes the file from the repository completely. 130 | // This is not the same as reverting or restoring a file to its previous contents. 131 | // 132 | // When change edit doesn’t exist for this change yet it is created. 133 | // 134 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-edit-file 135 | func (s *ChangesService) DeleteFileInChangeEdit(ctx context.Context, changeID, filePath string) (*Response, error) { 136 | u := fmt.Sprintf("changes/%s/edit/%s", changeID, filePath) 137 | return s.client.DeleteRequest(ctx, u, nil) 138 | } 139 | 140 | // DeleteChangeEdit deletes change edit. 141 | // 142 | // As response “204 No Content” is returned. 143 | // 144 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-edit 145 | func (s *ChangesService) DeleteChangeEdit(ctx context.Context, changeID string) (*Response, error) { 146 | u := fmt.Sprintf("changes/%s/edit", changeID) 147 | return s.client.DeleteRequest(ctx, u, nil) 148 | } 149 | 150 | // PublishChangeEdit promotes change edit to a regular patch set. 151 | // 152 | // As response “204 No Content” is returned. 153 | // 154 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#publish-edit 155 | func (s *ChangesService) PublishChangeEdit(ctx context.Context, changeID, notify string) (*Response, error) { 156 | u := fmt.Sprintf("changes/%s/edit:publish", changeID) 157 | 158 | req, err := s.client.NewRequest(ctx, "POST", u, map[string]string{ 159 | "notify": notify, 160 | }) 161 | if err != nil { 162 | return nil, err 163 | } 164 | return s.client.Do(req, nil) 165 | } 166 | 167 | // RebaseChangeEdit rebases change edit on top of latest patch set. 168 | // 169 | // When change was rebased on top of latest patch set, response “204 No Content” is returned. 170 | // When change edit is already based on top of the latest patch set, the response “409 Conflict” is returned. 171 | // 172 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#rebase-edit 173 | func (s *ChangesService) RebaseChangeEdit(ctx context.Context, changeID string) (*Response, error) { 174 | u := fmt.Sprintf("changes/%s/edit:rebase", changeID) 175 | 176 | req, err := s.client.NewRequest(ctx, "POST", u, nil) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | return s.client.Do(req, nil) 182 | } 183 | 184 | // RetrieveFileContentFromChangeEdit retrieves content of a file from a change edit. 185 | // 186 | // The content of the file is returned as text encoded inside base64. 187 | // The Content-Type header will always be text/plain reflecting the outer base64 encoding. 188 | // A Gerrit-specific X-FYI-Content-Type header can be examined to find the server detected content type of the file. 189 | // 190 | // When the specified file was deleted in the change edit “204 No Content” is returned. 191 | // If only the content type is required, callers should use HEAD to avoid downloading the encoded file contents. 192 | // 193 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-edit-file 194 | func (s *ChangesService) RetrieveFileContentFromChangeEdit(ctx context.Context, changeID, filePath string) (*string, *Response, error) { 195 | u := fmt.Sprintf("changes/%s/edit/%s", changeID, filePath) 196 | 197 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 198 | if err != nil { 199 | return nil, nil, err 200 | } 201 | 202 | v := new(string) 203 | resp, err := s.client.Do(req, v) 204 | if err != nil { 205 | return nil, resp, err 206 | } 207 | 208 | return v, resp, err 209 | } 210 | 211 | // RetrieveFileContentTypeFromChangeEdit retrieves content type of a file from a change edit. 212 | // This is nearly the same as RetrieveFileContentFromChangeEdit. 213 | // But if only the content type is required, callers should use HEAD to avoid downloading the encoded file contents. 214 | // 215 | // For further documentation please have a look at RetrieveFileContentFromChangeEdit. 216 | // 217 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-edit-file 218 | func (s *ChangesService) RetrieveFileContentTypeFromChangeEdit(ctx context.Context, changeID, filePath string) (*Response, error) { 219 | u := fmt.Sprintf("changes/%s/edit/%s", changeID, filePath) 220 | 221 | req, err := s.client.NewRequest(ctx, "HEAD", u, nil) 222 | if err != nil { 223 | return nil, err 224 | } 225 | 226 | return s.client.Do(req, nil) 227 | } 228 | 229 | /* 230 | Missing Change Edit Endpoints 231 | Restore file content or rename files in Change Edit 232 | */ 233 | -------------------------------------------------------------------------------- /changes_hashtags.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // GetHashtags gets the hashtags associated with a change. 9 | // 10 | // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-hashtags 11 | func (c *ChangesService) GetHashtags(ctx context.Context, changeID string) ([]string, *Response, error) { 12 | u := fmt.Sprintf("changes/%s/hashtags", changeID) 13 | 14 | req, err := c.client.NewRequest(ctx, "GET", u, nil) 15 | if err != nil { 16 | return nil, nil, err 17 | } 18 | 19 | var hashtags []string 20 | resp, err := c.client.Do(req, &hashtags) 21 | 22 | return hashtags, resp, err 23 | } 24 | 25 | // HashtagsInput entity contains information about hashtags to add to, and/or remove from, a change. 26 | // 27 | // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#hashtags-input 28 | type HashtagsInput struct { 29 | // The list of hashtags to be added to the change. 30 | Add []string `json:"add,omitempty"` 31 | 32 | // The list of hashtags to be removed from the change. 33 | Remove []string `json:"remove,omitempty"` 34 | } 35 | 36 | // SetHashtags adds and/or removes hashtags from a change. 37 | // 38 | // As response the change’s hashtags are returned as a list of strings. 39 | // 40 | // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#set-hashtags 41 | func (c *ChangesService) SetHashtags(ctx context.Context, changeID string, input *HashtagsInput) ([]string, *Response, error) { 42 | u := fmt.Sprintf("changes/%s/hashtags", changeID) 43 | 44 | req, err := c.client.NewRequest(ctx, "POST", u, input) 45 | if err != nil { 46 | return nil, nil, err 47 | } 48 | 49 | var hashtags []string 50 | resp, err := c.client.Do(req, &hashtags) 51 | 52 | return hashtags, resp, err 53 | } 54 | -------------------------------------------------------------------------------- /changes_hashtags_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/andygrunwald/go-gerrit" 11 | ) 12 | 13 | func TestChangesService_GetHashtags(t *testing.T) { 14 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | const ( 16 | method = "GET" 17 | path = "/changes/123/hashtags" 18 | ) 19 | if r.Method != method { 20 | t.Errorf("Method %q != %q", r.Method, method) 21 | } 22 | if r.URL.Path != path { 23 | t.Errorf("Path %q != %q", r.URL.Path, path) 24 | } 25 | fmt.Fprintf(w, `)]}' 26 | [ 27 | "hashtag1", 28 | "hashtag2" 29 | ] 30 | `) 31 | })) 32 | defer ts.Close() 33 | 34 | ctx := context.Background() 35 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | hashtags, _, err := client.Changes.GetHashtags(ctx, "123") 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | if len(hashtags) != 2 || hashtags[0] != "hashtag1" || hashtags[1] != "hashtag2" { 46 | t.Errorf("Unexpected hashtags %+v", hashtags) 47 | } 48 | } 49 | 50 | func TestChangesService_SetHashtags(t *testing.T) { 51 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 | const ( 53 | method = "POST" 54 | path = "/changes/123/hashtags" 55 | ) 56 | if r.Method != method { 57 | t.Errorf("Method %q != %q", r.Method, method) 58 | } 59 | if r.URL.Path != path { 60 | t.Errorf("Path %q != %q", r.URL.Path, path) 61 | } 62 | fmt.Fprintf(w, `)]}' 63 | [ 64 | "hashtag1", 65 | "hashtag3" 66 | ] 67 | `) 68 | })) 69 | defer ts.Close() 70 | 71 | ctx := context.Background() 72 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | hashtags, _, err := client.Changes.SetHashtags(ctx, "123", &gerrit.HashtagsInput{ 78 | Add: []string{"hashtag3"}, 79 | Remove: []string{"hashtag2"}, 80 | }) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | if len(hashtags) != 2 || hashtags[0] != "hashtag1" || hashtags[1] != "hashtag3" { 86 | t.Errorf("Unexpected hashtags %+v", hashtags) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /changes_reviewer.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // ReviewerInfo entity contains information about a reviewer and its votes on a change. 9 | type ReviewerInfo struct { 10 | AccountInfo 11 | Approvals map[string]string `json:"approvals"` 12 | } 13 | 14 | // SuggestedReviewerInfo entity contains information about a reviewer that can be added to a change (an account or a group). 15 | type SuggestedReviewerInfo struct { 16 | Account AccountInfo `json:"account,omitempty"` 17 | Group GroupBaseInfo `json:"group,omitempty"` 18 | } 19 | 20 | // AddReviewerResult entity describes the result of adding a reviewer to a change. 21 | type AddReviewerResult struct { 22 | Input string `json:"input,omitempty"` 23 | Reviewers []ReviewerInfo `json:"reviewers,omitempty"` 24 | CCS []ReviewerInfo `json:"ccs,omitempty"` 25 | Error string `json:"error,omitempty"` 26 | Confirm bool `json:"confirm,omitempty"` 27 | } 28 | 29 | // DeleteVoteInput entity contains options for the deletion of a vote. 30 | // 31 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-vote-input 32 | type DeleteVoteInput struct { 33 | Label string `json:"label,omitempty"` 34 | Notify string `json:"notify,omitempty"` 35 | NotifyDetails map[string]NotifyInfo `json:"notify_details"` 36 | } 37 | 38 | // ListReviewers lists the reviewers of a change. 39 | // 40 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-reviewers 41 | func (s *ChangesService) ListReviewers(ctx context.Context, changeID string) (*[]ReviewerInfo, *Response, error) { 42 | u := fmt.Sprintf("changes/%s/reviewers/", changeID) 43 | 44 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 45 | if err != nil { 46 | return nil, nil, err 47 | } 48 | 49 | v := new([]ReviewerInfo) 50 | resp, err := s.client.Do(req, v) 51 | if err != nil { 52 | return nil, resp, err 53 | } 54 | 55 | return v, resp, err 56 | } 57 | 58 | // SuggestReviewers suggest the reviewers for a given query q and result limit n. 59 | // If result limit is not passed, then the default 10 is used. 60 | // 61 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#suggest-reviewers 62 | func (s *ChangesService) SuggestReviewers(ctx context.Context, changeID string, opt *QueryOptions) (*[]SuggestedReviewerInfo, *Response, error) { 63 | u := fmt.Sprintf("changes/%s/suggest_reviewers", changeID) 64 | 65 | u, err := addOptions(u, opt) 66 | if err != nil { 67 | return nil, nil, err 68 | } 69 | 70 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 71 | if err != nil { 72 | return nil, nil, err 73 | } 74 | 75 | v := new([]SuggestedReviewerInfo) 76 | resp, err := s.client.Do(req, v) 77 | if err != nil { 78 | return nil, resp, err 79 | } 80 | 81 | return v, resp, err 82 | } 83 | 84 | // GetReviewer retrieves a reviewer of a change. 85 | // 86 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-reviewer 87 | func (s *ChangesService) GetReviewer(ctx context.Context, changeID, accountID string) (*ReviewerInfo, *Response, error) { 88 | u := fmt.Sprintf("changes/%s/reviewers/%s", changeID, accountID) 89 | 90 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | 95 | v := new(ReviewerInfo) 96 | resp, err := s.client.Do(req, v) 97 | if err != nil { 98 | return nil, resp, err 99 | } 100 | 101 | return v, resp, err 102 | } 103 | 104 | // AddReviewer adds one user or all members of one group as reviewer to the change. 105 | // The reviewer to be added to the change must be provided in the request body as a ReviewerInput entity. 106 | // 107 | // As response an AddReviewerResult entity is returned that describes the newly added reviewers. 108 | // If a group is specified, adding the group members as reviewers is an atomic operation. 109 | // This means if an error is returned, none of the members are added as reviewer. 110 | // If a group with many members is added as reviewer a confirmation may be required. 111 | // 112 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#add-reviewer 113 | func (s *ChangesService) AddReviewer(ctx context.Context, changeID string, input *ReviewerInput) (*AddReviewerResult, *Response, error) { 114 | u := fmt.Sprintf("changes/%s/reviewers", changeID) 115 | 116 | req, err := s.client.NewRequest(ctx, "POST", u, input) 117 | if err != nil { 118 | return nil, nil, err 119 | } 120 | 121 | v := new(AddReviewerResult) 122 | resp, err := s.client.Do(req, v) 123 | if err != nil { 124 | return nil, resp, err 125 | } 126 | 127 | return v, resp, err 128 | } 129 | 130 | // DeleteReviewer deletes a reviewer from a change. 131 | // 132 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-reviewer 133 | func (s *ChangesService) DeleteReviewer(ctx context.Context, changeID, accountID string) (*Response, error) { 134 | u := fmt.Sprintf("changes/%s/reviewers/%s", changeID, accountID) 135 | return s.client.DeleteRequest(ctx, u, nil) 136 | } 137 | 138 | // ListVotes lists the votes for a specific reviewer of the change. 139 | // 140 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-votes 141 | func (s *ChangesService) ListVotes(ctx context.Context, changeID string, accountID string) (map[string]int, *Response, error) { 142 | u := fmt.Sprintf("changes/%s/reviewers/%s/votes/", changeID, accountID) 143 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 144 | if err != nil { 145 | return nil, nil, err 146 | } 147 | 148 | var v map[string]int 149 | resp, err := s.client.Do(req, &v) 150 | if err != nil { 151 | return nil, resp, err 152 | } 153 | return v, resp, err 154 | } 155 | 156 | // DeleteVote deletes a single vote from a change. Note, that even when the 157 | // last vote of a reviewer is removed the reviewer itself is still listed on 158 | // the change. 159 | // 160 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-vote 161 | func (s *ChangesService) DeleteVote(ctx context.Context, changeID string, accountID string, label string, input *DeleteVoteInput) (*Response, error) { 162 | u := fmt.Sprintf("changes/%s/reviewers/%s/votes/%s", changeID, accountID, label) 163 | return s.client.DeleteRequest(ctx, u, input) 164 | } 165 | -------------------------------------------------------------------------------- /changes_reviewer_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/andygrunwald/go-gerrit" 11 | ) 12 | 13 | func newClient(ctx context.Context, t *testing.T, server *httptest.Server) *gerrit.Client { 14 | client, err := gerrit.NewClient(ctx, server.URL, nil) 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | return client 19 | } 20 | 21 | func TestChangesService_ListReviewers(t *testing.T) { 22 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 | expected := "/changes/123/reviewers/" 24 | if r.URL.Path != expected { 25 | t.Errorf("%s != %s", r.URL.Path, expected) 26 | } 27 | 28 | fmt.Fprint(w, `[{"_account_id": 1}]`) 29 | })) 30 | defer ts.Close() 31 | 32 | ctx := context.Background() 33 | client := newClient(ctx, t, ts) 34 | data, _, err := client.Changes.ListReviewers(ctx, "123") 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | if len(*data) != 1 { 40 | t.Error("Length of data !=1 ") 41 | } 42 | 43 | if (*data)[0].AccountID != 1 { 44 | t.Error("AccountID != 1") 45 | } 46 | } 47 | 48 | func TestChangesService_SuggestReviewers(t *testing.T) { 49 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | expected := "/changes/123/suggest_reviewers" 51 | if r.URL.Path != expected { 52 | t.Errorf("%s != %s", r.URL.Path, expected) 53 | } 54 | 55 | fmt.Fprint(w, `[{"account": {"_account_id": 1}}]`) 56 | })) 57 | defer ts.Close() 58 | 59 | ctx := context.Background() 60 | client := newClient(ctx, t, ts) 61 | data, _, err := client.Changes.SuggestReviewers(ctx, "123", nil) 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | 66 | if len(*data) != 1 { 67 | t.Error("Length of data !=1 ") 68 | } 69 | 70 | if (*data)[0].Account.AccountID != 1 { 71 | t.Error("AccountID != 1") 72 | } 73 | } 74 | 75 | func TestChangesService_GetReviewer(t *testing.T) { 76 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 77 | expected := "/changes/123/reviewers/1" 78 | if r.URL.Path != expected { 79 | t.Errorf("%s != %s", r.URL.Path, expected) 80 | } 81 | 82 | fmt.Fprint(w, `{"_account_id": 1}`) 83 | })) 84 | defer ts.Close() 85 | 86 | ctx := context.Background() 87 | client := newClient(ctx, t, ts) 88 | data, _, err := client.Changes.GetReviewer(ctx, "123", "1") 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | if data.AccountID != 1 { 93 | t.Error("AccountID != 1") 94 | } 95 | } 96 | 97 | func TestChangesService_AddReviewer(t *testing.T) { 98 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 99 | expected := "/changes/123/reviewers" 100 | if r.URL.Path != expected { 101 | t.Errorf("%s != %s", r.URL.Path, expected) 102 | } 103 | if r.Method != "POST" { 104 | t.Error("Method != POST") 105 | } 106 | 107 | fmt.Fprint(w, `{"confirm": true}`) 108 | })) 109 | defer ts.Close() 110 | 111 | ctx := context.Background() 112 | client := newClient(ctx, t, ts) 113 | data, _, err := client.Changes.AddReviewer(ctx, "123", &gerrit.ReviewerInput{}) 114 | if err != nil { 115 | t.Error(err) 116 | } 117 | if !data.Confirm { 118 | t.Error("Confirm != true") 119 | } 120 | } 121 | 122 | func TestChangesService_DeleteReviewer(t *testing.T) { 123 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 124 | expected := "/changes/123/reviewers/1" 125 | if r.URL.Path != expected { 126 | t.Errorf("%s != %s", r.URL.Path, expected) 127 | } 128 | if r.Method != "DELETE" { 129 | t.Error("Method != DELETE") 130 | } 131 | w.WriteHeader(http.StatusOK) 132 | })) 133 | defer ts.Close() 134 | 135 | ctx := context.Background() 136 | client := newClient(ctx, t, ts) 137 | _, err := client.Changes.DeleteReviewer(ctx, "123", "1") 138 | if err != nil { 139 | t.Error(err) 140 | } 141 | } 142 | 143 | func TestChangesService_ListVotes(t *testing.T) { 144 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 | expected := "/changes/123/reviewers/1/votes/" 146 | if r.URL.Path != expected { 147 | t.Errorf("%s != %s", r.URL.Path, expected) 148 | } 149 | fmt.Fprint(w, `{"Code-Review": 2, "Verified": 1}`) 150 | })) 151 | defer ts.Close() 152 | 153 | ctx := context.Background() 154 | client := newClient(ctx, t, ts) 155 | votes, _, err := client.Changes.ListVotes(ctx, "123", "1") 156 | if err != nil { 157 | t.Error(err) 158 | } 159 | if votes["Code-Review"] != 2 { 160 | t.Error("Code-Review != 2") 161 | } 162 | if votes["Verified"] != 1 { 163 | t.Error("Verified != 1") 164 | } 165 | } 166 | 167 | func TestChangesService_DeleteVote(t *testing.T) { 168 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 169 | expected := "/changes/123/reviewers/1/votes/Code-Review" 170 | if r.URL.Path != expected { 171 | t.Errorf("%s != %s", r.URL.Path, expected) 172 | } 173 | 174 | if r.Method != "DELETE" { 175 | t.Error("Method != DELETE") 176 | } 177 | w.WriteHeader(http.StatusOK) 178 | })) 179 | defer ts.Close() 180 | 181 | ctx := context.Background() 182 | client := newClient(ctx, t, ts) 183 | _, err := client.Changes.DeleteVote(ctx, "123", "1", "Code-Review", nil) 184 | if err != nil { 185 | t.Error(err) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /changes_revision_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/andygrunwald/go-gerrit" 12 | ) 13 | 14 | func TestChangesService_ListFiles(t *testing.T) { 15 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | if got, want := r.URL.String(), "/changes/123/revisions/456/files/?base=7"; got != want { 17 | t.Errorf("request URL:\ngot: %q\nwant: %q", got, want) 18 | } 19 | fmt.Fprint(w, `{ 20 | "/COMMIT_MSG": { 21 | "status": "A", 22 | "lines_inserted": 7, 23 | "size_delta": 551, 24 | "size": 551 25 | }, 26 | "gerrit-server/RefControl.java": { 27 | "lines_inserted": 5, 28 | "lines_deleted": 3, 29 | "size_delta": 98, 30 | "size": 23348 31 | } 32 | }`) 33 | })) 34 | defer ts.Close() 35 | 36 | ctx := context.Background() 37 | client := newClient(ctx, t, ts) 38 | got, _, err := client.Changes.ListFiles(ctx, "123", "456", &gerrit.FilesOptions{ 39 | Base: "7", 40 | }) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | want := map[string]gerrit.FileInfo{ 45 | "/COMMIT_MSG": { 46 | Status: "A", 47 | LinesInserted: 7, 48 | SizeDelta: 551, 49 | Size: 551, 50 | }, 51 | "gerrit-server/RefControl.java": { 52 | LinesInserted: 5, 53 | LinesDeleted: 3, 54 | SizeDelta: 98, 55 | Size: 23348, 56 | }, 57 | } 58 | if !reflect.DeepEqual(got, want) { 59 | t.Errorf("client.Changes.ListFiles:\ngot: %+v\nwant: %+v", got, want) 60 | } 61 | } 62 | 63 | func TestChangesService_ListFilesReviewed(t *testing.T) { 64 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 | if got, want := r.URL.String(), "/changes/123/revisions/456/files/?q=abc&reviewed=true"; got != want { 66 | t.Errorf("request URL:\ngot: %q\nwant: %q", got, want) 67 | } 68 | fmt.Fprint(w, `["/COMMIT_MSG","gerrit-server/RefControl.java"]`) 69 | })) 70 | defer ts.Close() 71 | 72 | ctx := context.Background() 73 | client := newClient(ctx, t, ts) 74 | got, _, err := client.Changes.ListFilesReviewed(ctx, "123", "456", &gerrit.FilesOptions{ 75 | Q: "abc", 76 | }) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | want := []string{"/COMMIT_MSG", "gerrit-server/RefControl.java"} 81 | if !reflect.DeepEqual(got, want) { 82 | t.Errorf("client.Changes.ListFilesReviewed:\ngot: %q\nwant: %q", got, want) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /changes_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/andygrunwald/go-gerrit" 12 | ) 13 | 14 | func ExampleChangesService_QueryChanges() { 15 | ctx := context.Background() 16 | instance := "https://android-review.googlesource.com/" 17 | client, err := gerrit.NewClient(ctx, instance, nil) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | opt := &gerrit.QueryChangeOptions{} 23 | opt.Query = []string{ 24 | "change:249244", 25 | } 26 | opt.Limit = 2 27 | opt.AdditionalFields = []string{"LABELS"} 28 | changes, _, err := client.Changes.QueryChanges(ctx, opt) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | for _, change := range *changes { 34 | fmt.Printf("Project: %s -> %s -> %s%d\n", change.Project, change.Subject, instance, change.Number) 35 | } 36 | 37 | // Output: 38 | // Project: platform/art -> ART: Change return types of field access entrypoints -> https://android-review.googlesource.com/249244 39 | } 40 | 41 | func ExampleChangesService_QueryChanges_withSubmittable() { 42 | instance := "https://android-review.googlesource.com/" 43 | ctx := context.Background() 44 | client, err := gerrit.NewClient(ctx, instance, nil) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | opt := &gerrit.QueryChangeOptions{} 50 | opt.Query = []string{ 51 | "change:249244", 52 | } 53 | opt.AdditionalFields = []string{"SUBMITTABLE"} 54 | 55 | changes, _, err := client.Changes.QueryChanges(ctx, opt) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | for _, change := range *changes { 61 | fmt.Printf("Project: %s -> %s -> %s%d, Ready to submit: %t\n", change.Project, change.Subject, instance, change.Number, change.Submittable) 62 | } 63 | 64 | // Output: 65 | // Project: platform/art -> ART: Change return types of field access entrypoints -> https://android-review.googlesource.com/249244, Ready to submit: false 66 | } 67 | 68 | // Prior to fixing #18 this test would fail. 69 | func ExampleChangesService_QueryChanges_withSymbols() { 70 | instance := "https://android-review.googlesource.com/" 71 | ctx := context.Background() 72 | client, err := gerrit.NewClient(ctx, instance, nil) 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | opt := &gerrit.QueryChangeOptions{} 78 | opt.Query = []string{ 79 | "change:249244+status:merged", 80 | } 81 | opt.Limit = 2 82 | opt.AdditionalFields = []string{"LABELS"} 83 | changes, _, err := client.Changes.QueryChanges(ctx, opt) 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | for _, change := range *changes { 89 | fmt.Printf("Project: %s -> %s -> %s%d\n", change.Project, change.Subject, instance, change.Number) 90 | } 91 | 92 | // Output: 93 | // Project: platform/art -> ART: Change return types of field access entrypoints -> https://android-review.googlesource.com/249244 94 | } 95 | 96 | func ExampleChangesService_PublishChangeEdit() { 97 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 98 | fmt.Fprintf(w, "ok") 99 | })) 100 | defer ts.Close() 101 | 102 | ctx := context.Background() 103 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | _, err = client.Changes.PublishChangeEdit(ctx, "123", "NONE") 109 | if err != nil { 110 | panic(err) 111 | } 112 | } 113 | 114 | func disallowEmptyFields(t *testing.T, payload map[string]interface{}, path string) { 115 | for field, generic := range payload { 116 | curPath := field 117 | if len(path) > 0 { 118 | curPath = path + "." + field 119 | } 120 | switch value := generic.(type) { 121 | case string: 122 | if len(value) == 0 { 123 | t.Errorf("Empty value for field %q", curPath) 124 | } 125 | case map[string]interface{}: 126 | if len(value) == 0 { 127 | t.Errorf("Empty value for field %q", curPath) 128 | } 129 | disallowEmptyFields(t, value, curPath) 130 | } 131 | } 132 | } 133 | 134 | func TestChangesService_CreateChange(t *testing.T) { 135 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 136 | decoder := json.NewDecoder(r.Body) 137 | var payload map[string]interface{} 138 | if err := decoder.Decode(&payload); err != nil { 139 | t.Error(err) 140 | } 141 | 142 | jsonStr, err := json.MarshalIndent(payload, "", " ") 143 | if err != nil { 144 | t.Error(err) 145 | } 146 | if len(jsonStr) == 0 { 147 | t.Error("Empty request payload") 148 | } 149 | 150 | required := func(field string) string { 151 | value, ok := payload[field] 152 | strVal := value.(string) 153 | if !ok { 154 | t.Errorf("Missing required field %q", field) 155 | } 156 | return strVal 157 | } 158 | project := required("project") 159 | branch := required("branch") 160 | subject := required("subject") 161 | if merge, ok := payload["merge"]; ok { 162 | if _, ok := merge.(map[string]interface{})["source"]; !ok { 163 | t.Error(`Missing required field "merge.source"`) 164 | } 165 | } 166 | 167 | disallowEmptyFields(t, payload, "") 168 | 169 | if r.URL.Path != "/changes/" { 170 | t.Errorf("%s != /changes/", r.URL.Path) 171 | } 172 | if r.Method != "POST" { 173 | t.Errorf("%s != POST", r.Method) 174 | } 175 | fmt.Fprintf(w, `{ "id": "abc1234", "project": "%s", "branch": "%s", "subject": "%s"}`, project, branch, subject) 176 | })) 177 | defer ts.Close() 178 | 179 | ctx := context.Background() 180 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 181 | if err != nil { 182 | t.Error(err) 183 | } 184 | 185 | cases := map[string]gerrit.ChangeInput{ 186 | "RequiredOnly": { 187 | Project: "myProject", 188 | Branch: "main", 189 | Subject: "test change", 190 | }, 191 | "WithMerge": { 192 | Project: "myProject", 193 | Branch: "main", 194 | Subject: "test change", 195 | Merge: &gerrit.MergeInput{ 196 | Source: "45/3/1", 197 | }, 198 | }, 199 | "WithAppend": { 200 | Project: "myProject", 201 | Branch: "main", 202 | Subject: "test change", 203 | Author: &gerrit.AccountInput{ 204 | Username: "roboto", 205 | Name: "Rob Oto", 206 | }, 207 | }, 208 | } 209 | for name, input := range cases { 210 | t.Run(name, func(t *testing.T) { 211 | info, _, err := client.Changes.CreateChange(ctx, &input) 212 | if err != nil { 213 | t.Error(err) 214 | } 215 | 216 | if info.ID != "abc1234" { 217 | t.Error("Invalid id") 218 | } 219 | }) 220 | } 221 | } 222 | 223 | func TestChangesService_SubmitChange(t *testing.T) { 224 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 225 | if r.URL.Path != "/changes/123/submit" { 226 | t.Errorf("%s != /changes/123/submit", r.URL.Path) 227 | } 228 | fmt.Fprint(w, `{"id": "123"}`) 229 | })) 230 | defer ts.Close() 231 | 232 | ctx := context.Background() 233 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 234 | if err != nil { 235 | t.Error(err) 236 | } 237 | info, _, err := client.Changes.SubmitChange(ctx, "123", nil) 238 | if err != nil { 239 | t.Error(err) 240 | } 241 | if info.ID != "123" { 242 | t.Error("Invalid id") 243 | } 244 | } 245 | 246 | func TestChangesService_SubmitChange_Conflict(t *testing.T) { 247 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 248 | w.WriteHeader(http.StatusConflict) 249 | })) 250 | defer ts.Close() 251 | 252 | ctx := context.Background() 253 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 254 | if err != nil { 255 | t.Error(err) 256 | } 257 | _, response, _ := client.Changes.SubmitChange(ctx, "123", nil) 258 | if response.StatusCode != http.StatusConflict { 259 | t.Error("Expected 409 code") 260 | } 261 | } 262 | 263 | func TestChangesService_AbandonChange(t *testing.T) { 264 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 265 | if r.URL.Path != "/changes/123/abandon" { 266 | t.Errorf("%s != /changes/123/abandon", r.URL.Path) 267 | } 268 | fmt.Fprint(w, `{"id": "123"}`) 269 | })) 270 | defer ts.Close() 271 | 272 | ctx := context.Background() 273 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 274 | if err != nil { 275 | t.Error(err) 276 | } 277 | info, _, err := client.Changes.AbandonChange(ctx, "123", nil) 278 | if err != nil { 279 | t.Error(err) 280 | } 281 | if info.ID != "123" { 282 | t.Error("Invalid id") 283 | } 284 | } 285 | 286 | func TestChangesService_AbandonChange_Conflict(t *testing.T) { 287 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 288 | w.WriteHeader(http.StatusConflict) 289 | })) 290 | defer ts.Close() 291 | 292 | ctx := context.Background() 293 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 294 | if err != nil { 295 | t.Error(err) 296 | } 297 | _, response, _ := client.Changes.AbandonChange(ctx, "123", nil) 298 | if response.StatusCode != http.StatusConflict { 299 | t.Error("Expected 409 code") 300 | } 301 | } 302 | 303 | func TestChangesService_RebaseChange(t *testing.T) { 304 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 305 | if r.URL.Path != "/changes/123/rebase" { 306 | t.Errorf("%s != /changes/123/rebase", r.URL.Path) 307 | } 308 | fmt.Fprint(w, `{"id": "123"}`) 309 | })) 310 | defer ts.Close() 311 | 312 | ctx := context.Background() 313 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 314 | if err != nil { 315 | t.Error(err) 316 | } 317 | info, _, err := client.Changes.RebaseChange(ctx, "123", nil) 318 | if err != nil { 319 | t.Error(err) 320 | } 321 | if info.ID != "123" { 322 | t.Error("Invalid id") 323 | } 324 | } 325 | 326 | func TestChangesService_RebaseChange_Conflict(t *testing.T) { 327 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 328 | w.WriteHeader(http.StatusConflict) 329 | })) 330 | defer ts.Close() 331 | 332 | ctx := context.Background() 333 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 334 | if err != nil { 335 | t.Error(err) 336 | } 337 | _, response, _ := client.Changes.RebaseChange(ctx, "123", nil) 338 | if response.StatusCode != http.StatusConflict { 339 | t.Error("Expected 409 code") 340 | } 341 | } 342 | 343 | func TestChangesService_RestoreChange(t *testing.T) { 344 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 345 | if r.URL.Path != "/changes/123/restore" { 346 | t.Errorf("%s != /changes/123/restore", r.URL.Path) 347 | } 348 | fmt.Fprint(w, `{"id": "123"}`) 349 | })) 350 | defer ts.Close() 351 | 352 | ctx := context.Background() 353 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 354 | if err != nil { 355 | t.Error(err) 356 | } 357 | info, _, err := client.Changes.RestoreChange(ctx, "123", nil) 358 | if err != nil { 359 | t.Error(err) 360 | } 361 | if info.ID != "123" { 362 | t.Error("Invalid id") 363 | } 364 | } 365 | 366 | func TestChangesService_RestoreChange_Conflict(t *testing.T) { 367 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 368 | w.WriteHeader(http.StatusConflict) 369 | })) 370 | defer ts.Close() 371 | 372 | ctx := context.Background() 373 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 374 | if err != nil { 375 | t.Error(err) 376 | } 377 | _, response, _ := client.Changes.RestoreChange(ctx, "123", nil) 378 | if response.StatusCode != http.StatusConflict { 379 | t.Error("Expected 409 code") 380 | } 381 | } 382 | 383 | func TestChangesService_RevertChange(t *testing.T) { 384 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 385 | if r.URL.Path != "/changes/123/revert" { 386 | t.Errorf("%s != /changes/123/revert", r.URL.Path) 387 | } 388 | fmt.Fprint(w, `{"id": "123"}`) 389 | })) 390 | defer ts.Close() 391 | 392 | ctx := context.Background() 393 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 394 | if err != nil { 395 | t.Error(err) 396 | } 397 | info, _, err := client.Changes.RevertChange(ctx, "123", nil) 398 | if err != nil { 399 | t.Error(err) 400 | } 401 | if info.ID != "123" { 402 | t.Error("Invalid id") 403 | } 404 | } 405 | 406 | func TestChangesService_RevertChange_Conflict(t *testing.T) { 407 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 408 | w.WriteHeader(http.StatusConflict) 409 | })) 410 | defer ts.Close() 411 | 412 | ctx := context.Background() 413 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 414 | if err != nil { 415 | t.Error(err) 416 | } 417 | _, response, _ := client.Changes.RevertChange(ctx, "123", nil) 418 | if response.StatusCode != http.StatusConflict { 419 | t.Error("Expected 409 code") 420 | } 421 | } 422 | 423 | func TestChangesService_SetCommitMessage(t *testing.T) { 424 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 425 | if r.URL.Path != "/changes/123/message" { 426 | t.Errorf("%s != /changes/123/message", r.URL.Path) 427 | } 428 | if r.Method != "PUT" { 429 | t.Error("Method != PUT") 430 | } 431 | w.WriteHeader(http.StatusOK) 432 | })) 433 | defer ts.Close() 434 | 435 | ctx := context.Background() 436 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 437 | if err != nil { 438 | t.Error(err) 439 | } 440 | cm := &gerrit.CommitMessageInput{Message: "New commit message"} 441 | _, err = client.Changes.SetCommitMessage(ctx, "123", cm) 442 | if err != nil { 443 | t.Error(err) 444 | } 445 | } 446 | 447 | func TestChangesService_SetCommitMessage_NotFound(t *testing.T) { 448 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 449 | if r.URL.Path != "/changes/123/message" { 450 | t.Errorf("%s != /changes/123/message", r.URL.Path) 451 | } 452 | if r.Method != "PUT" { 453 | t.Error("Method != PUT") 454 | } 455 | w.WriteHeader(http.StatusNotFound) 456 | })) 457 | defer ts.Close() 458 | 459 | ctx := context.Background() 460 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 461 | if err != nil { 462 | t.Error(err) 463 | } 464 | cm := &gerrit.CommitMessageInput{Message: "New commit message"} 465 | resp, err := client.Changes.SetCommitMessage(ctx, "123", cm) 466 | if err == nil { 467 | t.Error("Expected error, instead nil") 468 | } 469 | if resp.StatusCode != http.StatusNotFound { 470 | t.Error("Expected 404 code") 471 | } 472 | } 473 | 474 | func TestChangesService_SetReadyForReview(t *testing.T) { 475 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 476 | if r.URL.Path != "/changes/123/ready" { 477 | t.Errorf("%s != /changes/123/ready", r.URL.Path) 478 | } 479 | if r.Method != "POST" { 480 | t.Error("Method != POST") 481 | } 482 | w.WriteHeader(http.StatusOK) 483 | })) 484 | defer ts.Close() 485 | 486 | ctx := context.Background() 487 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 488 | if err != nil { 489 | t.Error(err) 490 | } 491 | cm := &gerrit.ReadyForReviewInput{Message: "Now ready for review"} 492 | _, err = client.Changes.SetReadyForReview(ctx, "123", cm) 493 | if err != nil { 494 | t.Error(err) 495 | } 496 | } 497 | 498 | func TestChangesService_SetReadyForReview_NotFound(t *testing.T) { 499 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 500 | if r.URL.Path != "/changes/123/ready" { 501 | t.Errorf("%s != /changes/123/ready", r.URL.Path) 502 | } 503 | if r.Method != "POST" { 504 | t.Error("Method != POST") 505 | } 506 | w.WriteHeader(http.StatusNotFound) 507 | })) 508 | defer ts.Close() 509 | 510 | ctx := context.Background() 511 | client, err := gerrit.NewClient(ctx, ts.URL, nil) 512 | if err != nil { 513 | t.Error(err) 514 | } 515 | cm := &gerrit.ReadyForReviewInput{Message: "Now ready for review"} 516 | resp, err := client.Changes.SetReadyForReview(ctx, "123", cm) 517 | if err == nil { 518 | t.Error("Expected error, instead nil") 519 | } 520 | if resp.StatusCode != http.StatusNotFound { 521 | t.Error("Expected 404 code") 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/andygrunwald/go-gerrit" 8 | ) 9 | 10 | func ExampleConfigService_GetVersion() { 11 | instance := "https://gerrit-review.googlesource.com/" 12 | ctx := context.Background() 13 | client, err := gerrit.NewClient(ctx, instance, nil) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | v, _, err := client.Config.GetVersion(ctx) 19 | if err != nil { 20 | panic(err) 21 | } 22 | // We can`t output the direct version here, because 23 | // the test would fail if gerrit-review.googlesource.com 24 | // will upgrade their instance. 25 | // To access the version just print variable v 26 | if len(v) > 0 { 27 | fmt.Println("Got version from Gerrit") 28 | } 29 | 30 | // Output: Got version from Gerrit 31 | } 32 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gerrit provides a client for using the Gerrit API. 3 | 4 | Construct a new Gerrit client, then use the various services on the client to 5 | access different parts of the Gerrit API. For example: 6 | 7 | instance := "https://go-review.googlesource.com/" 8 | client, _ := gerrit.NewClient(instance, nil) 9 | 10 | // Get all public projects 11 | projects, _, err := client.Projects.ListProjects(nil) 12 | 13 | Set optional parameters for an API method by passing an Options object. 14 | 15 | // Get all projects with descriptions 16 | opt := &gerrit.ProjectOptions{ 17 | Description: true, 18 | } 19 | projects, _, err := client.Projects.ListProjects(opt) 20 | 21 | The services of a client divide the API into logical chunks and correspond to 22 | the structure of the Gerrit API documentation at 23 | https://gerrit-review.googlesource.com/Documentation/rest-api.html#_endpoints. 24 | 25 | # Authentication 26 | 27 | The go-gerrit library supports various methods to support the authentication. 28 | This methods are combined in the AuthenticationService that is available at client.Authentication. 29 | 30 | One way is an authentication via HTTP cookie. 31 | Some Gerrit instances hosted like the one hosted googlesource.com (e.g. https://go-review.googlesource.com/, 32 | https://android-review.googlesource.com/ or https://gerrit-review.googlesource.com/) support HTTP Cookie authentication. 33 | 34 | You need the cookie name and the cookie value. 35 | You can get them by click on "Settings > HTTP Password > Obtain Password" in your Gerrit instance. 36 | There you can receive your values. 37 | The cookie name will be (mostly) "o" (if hosted on googlesource.com). 38 | Your cookie secret will be something like "git-your@email.com=SomeHash...". 39 | 40 | instance := "https://gerrit-review.googlesource.com/" 41 | client, _ := gerrit.NewClient(instance, nil) 42 | client.Authentication.SetCookieAuth("o", "my-cookie-secret") 43 | 44 | self, _, _ := client.Accounts.GetAccount("self") 45 | 46 | fmt.Printf("Username: %s", self.Name) 47 | 48 | // Username: Andy G. 49 | 50 | Some other Gerrit instances (like https://review.typo3.org/) has auth.gitBasicAuth activated. 51 | With this you can authenticate with HTTP Basic like this: 52 | 53 | instance := "https://review.typo3.org/" 54 | client, _ := gerrit.NewClient(instance, nil) 55 | client.Authentication.SetBasicAuth("andy.grunwald", "my secrect password") 56 | 57 | self, _, _ := client.Accounts.GetAccount("self") 58 | 59 | fmt.Printf("Username: %s", self.Name) 60 | 61 | // Username: Andy Grunwald 62 | 63 | Additionally when creating a new client, pass an http.Client that supports further actions for you. 64 | For more information regarding authentication have a look at the Gerrit documentation: 65 | https://gerrit-review.googlesource.com/Documentation/rest-api.html#authentication 66 | */ 67 | package gerrit 68 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io" 8 | "net/url" 9 | "time" 10 | ) 11 | 12 | // PatchSet contains detailed information about a specific patch set. 13 | // 14 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/json.html#patchSet 15 | type PatchSet struct { 16 | Number Number `json:"number"` 17 | Revision string `json:"revision"` 18 | Parents []string `json:"parents"` 19 | Ref string `json:"ref"` 20 | Uploader AccountInfo `json:"uploader"` 21 | Author AccountInfo `json:"author"` 22 | CreatedOn int `json:"createdOn"` 23 | IsDraft bool `json:"isDraft"` 24 | Kind string `json:"kind"` 25 | } 26 | 27 | // RefUpdate contains data about a reference update. 28 | // 29 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/json.html#refUpdate 30 | type RefUpdate struct { 31 | OldRev string `json:"oldRev"` 32 | NewRev string `json:"newRev"` 33 | RefName string `json:"refName"` 34 | Project string `json:"project"` 35 | } 36 | 37 | // EventInfo contains information about an event emitted by Gerrit. This 38 | // structure can be used either when parsing streamed events or when reading 39 | // the output of the events-log plugin. 40 | // 41 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/cmd-stream-events.html#events 42 | type EventInfo struct { 43 | Type string `json:"type"` 44 | Change ChangeInfo `json:"change,omitempty"` 45 | ChangeKey ChangeInfo `json:"changeKey,omitempty"` 46 | PatchSet PatchSet `json:"patchSet,omitempty"` 47 | EventCreatedOn int `json:"eventCreatedOn,omitempty"` 48 | Reason string `json:"reason,omitempty"` 49 | Abandoner AccountInfo `json:"abandoner,omitempty"` 50 | Restorer AccountInfo `json:"restorer,omitempty"` 51 | Submitter AccountInfo `json:"submitter,omitempty"` 52 | Author AccountInfo `json:"author,omitempty"` 53 | Uploader AccountInfo `json:"uploader,omitempty"` 54 | Approvals []AccountInfo `json:"approvals,omitempty"` 55 | Comment string `json:"comment,omitempty"` 56 | Editor AccountInfo `json:"editor,omitempty"` 57 | Added []string `json:"added,omitempty"` 58 | Removed []string `json:"removed,omitempty"` 59 | Hashtags []string `json:"hashtags,omitempty"` 60 | RefUpdate RefUpdate `json:"refUpdate,omitempty"` 61 | Project ProjectInfo `json:"project,omitempty"` 62 | Reviewer AccountInfo `json:"reviewer,omitempty"` 63 | OldTopic string `json:"oldTopic,omitempty"` 64 | Changer AccountInfo `json:"changer,omitempty"` 65 | } 66 | 67 | // EventsLogService contains functions for querying the API provided 68 | // by the optional events-log plugin. 69 | type EventsLogService struct { 70 | client *Client 71 | } 72 | 73 | // EventsLogOptions contains options for querying events from the events-logs 74 | // plugin. 75 | type EventsLogOptions struct { 76 | From time.Time 77 | To time.Time 78 | 79 | // IgnoreUnmarshalErrors will cause GetEvents to ignore any errors 80 | // that come up when calling json.Unmarshal. This can be useful in 81 | // cases where the events-log plugin was not kept up to date with 82 | // the Gerrit version for some reason. In these cases the events-log 83 | // plugin will return data structs that don't match the EventInfo 84 | // struct which in turn causes issues for json.Unmarshal. 85 | IgnoreUnmarshalErrors bool 86 | } 87 | 88 | // getURL returns the url that should be used in the request. This will vary 89 | // depending on the options provided to GetEvents. 90 | func (events *EventsLogService) getURL(options *EventsLogOptions) (string, error) { 91 | parsed, err := url.Parse("/plugins/events-log/events/") 92 | if err != nil { 93 | return "", err 94 | } 95 | 96 | query := parsed.Query() 97 | 98 | if !options.From.IsZero() { 99 | query.Set("t1", options.From.Format("2006-01-02 15:04:05")) 100 | } 101 | 102 | if !options.To.IsZero() { 103 | query.Set("t2", options.To.Format("2006-01-02 15:04:05")) 104 | } 105 | 106 | encoded := query.Encode() 107 | if len(encoded) > 0 { 108 | parsed.RawQuery = encoded 109 | } 110 | 111 | return parsed.String(), nil 112 | } 113 | 114 | // GetEvents returns a list of events for the given input options. Use of this 115 | // function requires an authenticated user and for the events-log plugin to be 116 | // installed. This function returns the unmarshalled EventInfo structs, response, 117 | // failed lines and errors. Marshaling errors will cause this function to return 118 | // before processing is complete unless you set EventsLogOptions.IgnoreUnmarshalErrors 119 | // to true. This can be useful in cases where the events-log plugin got out of sync 120 | // with the Gerrit version which in turn produced events which can't be transformed 121 | // unmarshalled into EventInfo. 122 | // 123 | // Gerrit API docs: https:///plugins/events-log/Documentation/rest-api-events.html 124 | func (events *EventsLogService) GetEvents(ctx context.Context, options *EventsLogOptions) ([]EventInfo, *Response, [][]byte, error) { 125 | info := []EventInfo{} 126 | failures := [][]byte{} 127 | requestURL, err := events.getURL(options) 128 | 129 | if err != nil { 130 | return info, nil, failures, err 131 | } 132 | 133 | request, err := events.client.NewRequest(ctx, "GET", requestURL, nil) 134 | if err != nil { 135 | return info, nil, failures, err 136 | } 137 | 138 | // Perform the request but do not pass in a structure to unpack 139 | // the response into. The format of the response is one EventInfo 140 | // object per line so we need to manually handle the response here. 141 | response, err := events.client.Do(request, nil) 142 | if err != nil { 143 | return info, response, failures, err 144 | } 145 | 146 | body, err := io.ReadAll(response.Body) 147 | defer response.Body.Close() // nolint: errcheck 148 | if err != nil { 149 | return info, response, failures, err 150 | } 151 | 152 | for _, line := range bytes.Split(body, []byte("\n")) { 153 | if len(line) > 0 { 154 | event := EventInfo{} 155 | if err := json.Unmarshal(line, &event); err != nil { // nolint: vetshadow 156 | failures = append(failures, line) 157 | 158 | if !options.IgnoreUnmarshalErrors { 159 | return info, response, failures, err 160 | } 161 | continue 162 | } 163 | info = append(info, event) 164 | } 165 | } 166 | return info, response, failures, err 167 | } 168 | -------------------------------------------------------------------------------- /events_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/andygrunwald/go-gerrit" 10 | ) 11 | 12 | var ( 13 | fakeEvents = []byte(` 14 | {"submitter":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"newRev":"0000000000000000000000000000000000000000","patchSet":{"number":"1","revision":"0000000000000000000000000000000000000000","parents":["0000000000000000000000000000000000000000"],"ref":"refs/changes/1/1/1","uploader":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"createdOn":1470000000,"author":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"isDraft":false,"kind":"TRIVIAL_REBASE","sizeInsertions":10,"sizeDeletions":0},"change":{"project":"test","branch":"master","id":"Iffffffffffffffffffffffffffffffffffffffff","number":"1","subject":"subject","owner":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"url":"https://localhost/1","commitMessage":"commitMessage\n\nline2\n\nChange-Id: Iffffffffffffffffffffffffffffffffffffffff\n","status":"MERGED"},"type":"change-merged","eventCreatedOn":1470000000} 15 | {"author":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"comment":"Patch Set 1:\n\n(2 comments)\n\nSome comment","patchSet":{"number":"1","revision":"0000000000000000000000000000000000000000","parents":["0000000000000000000000000000000000000000"],"ref":"refs/changes/1/1/1","uploader":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"createdOn":1470000000,"author":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"isDraft":false,"kind":"REWORK","sizeInsertions":4,"sizeDeletions":-2},"change":{"project":"test","branch":"master","id":"Iffffffffffffffffffffffffffffffffffffffff","number":"1","subject":"subject","owner":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"url":"https://localhost/1","commitMessage":"commitMessage\n\nChange-Id: Iffffffffffffffffffffffffffffffffffffffff\n","status":"NEW"},"type":"comment-added","eventCreatedOn":1470000000}`) 16 | 17 | fakeEventsWithError = []byte(` 18 | {"submitter":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"newRev":"0000000000000000000000000000000000000000","patchSet":{"number":"1","revision":"0000000000000000000000000000000000000000","parents":["0000000000000000000000000000000000000000"],"ref":"refs/changes/1/1/1","uploader":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"createdOn":1470000000,"author":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"isDraft":false,"kind":"TRIVIAL_REBASE","sizeInsertions":10,"sizeDeletions":0},"change":{"project":"test","branch":"master","id":"Iffffffffffffffffffffffffffffffffffffffff","number":"1","subject":"subject","owner":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"url":"https://localhost/1","commitMessage":"commitMessage\n\nline2\n\nChange-Id: Iffffffffffffffffffffffffffffffffffffffff\n","status":"MERGED"},"type":"change-merged","eventCreatedOn":1470000000} 19 | {"author":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"comment":"Patch Set 1:\n\n(2 comments)\n\nSome comment","patchSet":{"number":"1","revision":"0000000000000000000000000000000000000000","parents":["0000000000000000000000000000000000000000"],"ref":"refs/changes/1/1/1","uploader":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"createdOn":1470000000,"author":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"isDraft":false,"kind":"REWORK","sizeInsertions":4,"sizeDeletions":-2},"change":{"project":"test","branch":"master","id":"Iffffffffffffffffffffffffffffffffffffffff","number":"1","subject":"subject","owner":{"name":"Foo Bar","email":"fbar@example.com","username":"fbar"},"url":"https://localhost/1","commitMessage":"commitMessage\n\nChange-Id: Iffffffffffffffffffffffffffffffffffffffff\n","status":"NEW"},"type":"comment-added","eventCreatedOn":1470000000} 20 | {"author":1}`) 21 | ) 22 | 23 | func TestEventsLogService_GetEvents_NoDateRange(t *testing.T) { 24 | setup() 25 | defer teardown() 26 | 27 | testMux.HandleFunc("/plugins/events-log/events/", func(writer http.ResponseWriter, request *http.Request) { 28 | if _, err := writer.Write(fakeEvents); err != nil { 29 | t.Error(err) 30 | } 31 | }) 32 | 33 | options := &gerrit.EventsLogOptions{} 34 | events, _, _, err := testClient.EventsLog.GetEvents(context.Background(), options) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | if len(events) != 2 { 40 | t.Error("Expected 2 events") 41 | } 42 | 43 | // Basic test 44 | for i, event := range events { 45 | switch i { 46 | case 0: 47 | if event.Type != "change-merged" { 48 | t.Error("Expected event type to be `change-merged`") 49 | } 50 | case 1: 51 | if event.Type != "comment-added" { 52 | t.Error("Expected event type to be `comment-added`") 53 | } 54 | } 55 | } 56 | } 57 | 58 | func TestEventsLogService_GetEvents_DateRangeFromAndTo(t *testing.T) { 59 | setup() 60 | defer teardown() 61 | 62 | to := time.Now() 63 | from := to.AddDate(0, 0, -7) 64 | 65 | testMux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { 66 | query := request.URL.Query() 67 | 68 | fromFormat := from.Format("2006-01-02 15:04:05") 69 | if query.Get("t1") != fromFormat { 70 | t.Errorf("%s != %s", query.Get("t1"), fromFormat) 71 | } 72 | 73 | toFormat := to.Format("2006-01-02 15:04:05") 74 | if query.Get("t2") != toFormat { 75 | t.Errorf("%s != %s", query.Get("t2"), toFormat) 76 | } 77 | 78 | if _, err := writer.Write(fakeEvents); err != nil { 79 | t.Error(err) 80 | } 81 | }) 82 | 83 | options := &gerrit.EventsLogOptions{From: from, To: to} 84 | _, _, _, err := testClient.EventsLog.GetEvents(context.Background(), options) 85 | if err != nil { 86 | t.Error(err) 87 | } 88 | } 89 | 90 | func TestEventsLogService_GetEvents_DateRangeFromOnly(t *testing.T) { 91 | setup() 92 | defer teardown() 93 | 94 | to := time.Now() 95 | from := to.AddDate(0, 0, -7) 96 | 97 | testMux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { 98 | query := request.URL.Query() 99 | 100 | fromFormat := from.Format("2006-01-02 15:04:05") 101 | if query.Get("t1") != fromFormat { 102 | t.Errorf("%s != %s", query.Get("t1"), fromFormat) 103 | } 104 | 105 | if query.Get("t2") != "" { 106 | t.Error("Did not expect t2 to be set") 107 | } 108 | 109 | if _, err := writer.Write(fakeEvents); err != nil { 110 | t.Error(err) 111 | } 112 | }) 113 | 114 | options := &gerrit.EventsLogOptions{From: from} 115 | _, _, _, err := testClient.EventsLog.GetEvents(context.Background(), options) 116 | if err != nil { 117 | t.Error(err) 118 | } 119 | } 120 | 121 | func TestEventsLogService_GetEvents_DateRangeToOnly(t *testing.T) { 122 | setup() 123 | defer teardown() 124 | 125 | to := time.Now() 126 | 127 | testMux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { 128 | query := request.URL.Query() 129 | 130 | toFormat := to.Format("2006-01-02 15:04:05") 131 | if query.Get("t2") != toFormat { 132 | t.Errorf("%s != %s", query.Get("t2"), toFormat) 133 | } 134 | 135 | if query.Get("t1") != "" { 136 | t.Error("Did not expect t1 to be set") 137 | } 138 | 139 | if _, err := writer.Write(fakeEvents); err != nil { 140 | t.Error(err) 141 | } 142 | }) 143 | 144 | options := &gerrit.EventsLogOptions{To: to} 145 | _, _, _, err := testClient.EventsLog.GetEvents(context.Background(), options) 146 | if err != nil { 147 | t.Error(err) 148 | } 149 | } 150 | 151 | func TestEventsLogService_GetEvents_UnmarshalError(t *testing.T) { 152 | setup() 153 | defer teardown() 154 | 155 | testMux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { 156 | if _, err := writer.Write(fakeEventsWithError); err != nil { 157 | t.Error(err) 158 | } 159 | }) 160 | 161 | options := &gerrit.EventsLogOptions{IgnoreUnmarshalErrors: true} 162 | events, _, failures, err := testClient.EventsLog.GetEvents(context.Background(), options) 163 | if err != nil { 164 | t.Error(err) 165 | } 166 | if len(failures) != 1 { 167 | t.Error("Expected 1 failures") 168 | } 169 | if len(events) != 2 { 170 | t.Error(len(events)) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /examples/create_new_project/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/andygrunwald/go-gerrit" 8 | ) 9 | 10 | func main() { 11 | instance := fmt.Sprintf("http://%s:8080", "localhost") 12 | ctx := context.Background() 13 | client, err := gerrit.NewClient(ctx, instance, nil) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | // Get your credentials 19 | // For local development setups, go to 20 | // http://localhost:8080/settings/#HTTPCredentials 21 | // and click `GENERATE NEW PASSWORD`. 22 | // Replace `secret` with your new value. 23 | client.Authentication.SetBasicAuth("admin", "secret") 24 | 25 | gerritProject := "Example project to the moon" 26 | gerritBranch := "main" 27 | 28 | data := &gerrit.ProjectInput{ 29 | Name: gerritProject, 30 | Branches: []string{gerritBranch}, 31 | CreateEmptyCommit: true, 32 | } 33 | projectInfo, _, err := client.Projects.CreateProject(ctx, gerritProject, data) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | fmt.Printf("Project '%s' created with ID '%+v'", projectInfo.Name, projectInfo.ID) 39 | 40 | // Project 'Example project to the moon' created with ID 'Example+project+to+the+moon' 41 | } 42 | -------------------------------------------------------------------------------- /examples/get_changes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/andygrunwald/go-gerrit" 8 | ) 9 | 10 | func main() { 11 | instance := "https://android-review.googlesource.com/" 12 | ctx := context.Background() 13 | client, err := gerrit.NewClient(ctx, instance, nil) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | opt := &gerrit.QueryChangeOptions{} 19 | opt.Query = []string{"project:kernel/common"} 20 | opt.AdditionalFields = []string{"LABELS"} 21 | changes, _, err := client.Changes.QueryChanges(ctx, opt) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | for _, change := range *changes { 27 | fmt.Printf("Project: %s -> %s -> %s%d\n", change.Project, change.Subject, instance, change.Number) 28 | } 29 | 30 | // Project: kernel/common -> ANDROID: GKI: Update symbols to symbol list -> https://android-review.googlesource.com/1830553 31 | // Project: kernel/common -> ANDROID: db845c_gki.fragment: Remove CONFIG_USB_NET_AX8817X from fragment -> https://android-review.googlesource.com/1830439 32 | // Project: kernel/common -> ANDROID: Update the ABI representation -> https://android-review.googlesource.com/1830469 33 | // ... 34 | } 35 | -------------------------------------------------------------------------------- /examples/get_gerrit_version/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/andygrunwald/go-gerrit" 8 | ) 9 | 10 | func main() { 11 | instance := "https://gerrit-review.googlesource.com/" 12 | ctx := context.Background() 13 | client, err := gerrit.NewClient(ctx, instance, nil) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | v, _, err := client.Config.GetVersion(ctx) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | fmt.Printf("Version: %s", v) 24 | 25 | // Version: 3.4.1-2066-g8db5605430 26 | } 27 | -------------------------------------------------------------------------------- /examples/get_public_projects/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/andygrunwald/go-gerrit" 8 | ) 9 | 10 | func main() { 11 | instance := "https://chromium-review.googlesource.com/" 12 | ctx := context.Background() 13 | client, err := gerrit.NewClient(ctx, instance, nil) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | opt := &gerrit.ProjectOptions{ 19 | Description: true, 20 | } 21 | projects, _, err := client.Projects.ListProjects(ctx, opt) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | for name, p := range *projects { 27 | fmt.Printf("%s - State: %s\n", name, p.State) 28 | } 29 | 30 | // chromiumos/third_party/bluez - State: ACTIVE 31 | // external/github.com/Polymer/ShadowDOM - State: ACTIVE 32 | // external/github.com/domokit/mojo_sdk - State: ACTIVE 33 | // ... 34 | } 35 | -------------------------------------------------------------------------------- /gerrit_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "reflect" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/andygrunwald/go-gerrit" 17 | ) 18 | 19 | const ( 20 | // testGerritInstanceURL is a test instance url that won`t be called 21 | testGerritInstanceURL = "https://go-review.googlesource.com/" 22 | ) 23 | 24 | var ( 25 | // testMux is the HTTP request multiplexer used with the test server. 26 | testMux *http.ServeMux 27 | 28 | // testClient is the gerrit client being tested. 29 | testClient *gerrit.Client 30 | 31 | // testServer is a test HTTP server used to provide mock API responses. 32 | testServer *httptest.Server 33 | ) 34 | 35 | type testValues map[string]string 36 | 37 | // setup sets up a test HTTP server along with a gerrit.Client that is configured to talk to that test server. 38 | // Tests should register handlers on mux which provide mock responses for the API method being tested. 39 | func setup() { 40 | // Test server 41 | testMux = http.NewServeMux() 42 | testServer = httptest.NewServer(testMux) 43 | 44 | // gerrit client configured to use test server 45 | testClient, _ = gerrit.NewClient(context.Background(), testServer.URL, nil) 46 | } 47 | 48 | // teardown closes the test HTTP server. 49 | func teardown() { 50 | testServer.Close() 51 | } 52 | 53 | // makedigestheader takes the incoming request and produces a string 54 | // which can be used for the WWW-Authenticate header. 55 | func makedigestheader(request *http.Request) string { 56 | return fmt.Sprintf( 57 | `Digest realm="Gerrit Code Review", domain="http://%s/", qop="auth", nonce="fakevaluefortesting"`, 58 | request.Host) 59 | } 60 | 61 | // writeresponse writes the requested value to the provided response writer and sets 62 | // the http code 63 | func writeresponse(t *testing.T, writer http.ResponseWriter, value interface{}, code int) { 64 | writer.WriteHeader(code) 65 | 66 | unmarshalled, err := json.Marshal(value) 67 | if err != nil { 68 | t.Error(err.Error()) 69 | return 70 | } 71 | 72 | data := []byte(`)]}'` + "\n" + string(unmarshalled)) 73 | if _, err := writer.Write(data); err != nil { 74 | t.Error(err.Error()) 75 | return 76 | } 77 | } 78 | 79 | func testMethod(t *testing.T, r *http.Request, want string) { 80 | if got := r.Method; got != want { 81 | t.Errorf("Request method: %v, want %v", got, want) 82 | } 83 | } 84 | 85 | func testRequestURL(t *testing.T, r *http.Request, want string) { // nolint: unparam 86 | if got := r.URL.String(); got != want { 87 | t.Errorf("Request URL: %v, want %v", got, want) 88 | } 89 | } 90 | 91 | func testFormValues(t *testing.T, r *http.Request, values testValues) { 92 | want := url.Values{} 93 | for k, v := range values { 94 | want.Add(k, v) 95 | } 96 | 97 | if err := r.ParseForm(); err != nil { 98 | t.Error(err) 99 | } 100 | if got := r.Form; !reflect.DeepEqual(got, want) { 101 | t.Errorf("Request parameters: %v, want %v", got, want) 102 | } 103 | } 104 | 105 | func testQueryValues(t *testing.T, r *http.Request, values testValues) { 106 | want := url.Values{} 107 | for k, v := range values { 108 | want.Add(k, v) 109 | } 110 | 111 | if got := r.URL.Query(); !reflect.DeepEqual(got, want) { 112 | t.Errorf("Request parameters: %v, want %v", got, want) 113 | } 114 | } 115 | 116 | func TestNewClient_NoGerritInstance(t *testing.T) { 117 | mockData := []string{"", "://not-existing"} 118 | for _, data := range mockData { 119 | c, err := gerrit.NewClient(context.Background(), data, nil) 120 | if c != nil { 121 | t.Errorf("NewClient return is not nil. Expected no client. Go %+v", c) 122 | } 123 | if err == nil { 124 | t.Error("No error occured by empty Gerrit Instance. Expected one.") 125 | } 126 | } 127 | } 128 | 129 | func TestNewClient_Services(t *testing.T) { 130 | c, err := gerrit.NewClient(context.Background(), "https://gerrit-review.googlesource.com/", nil) 131 | if err != nil { 132 | t.Errorf("An error occured. Expected nil. Got %+v.", err) 133 | } 134 | 135 | if c.Authentication == nil { 136 | t.Error("No AuthenticationService found.") 137 | } 138 | if c.Access == nil { 139 | t.Error("No AccessService found.") 140 | } 141 | if c.Accounts == nil { 142 | t.Error("No AccountsService found.") 143 | } 144 | if c.Changes == nil { 145 | t.Error("No ChangesService found.") 146 | } 147 | if c.Config == nil { 148 | t.Error("No ConfigService found.") 149 | } 150 | if c.Groups == nil { 151 | t.Error("No GroupsService found.") 152 | } 153 | if c.Plugins == nil { 154 | t.Error("No PluginsService found.") 155 | } 156 | if c.Projects == nil { 157 | t.Error("No ProjectsService found.") 158 | } 159 | } 160 | 161 | func TestNewClient_TestErrNoInstanceGiven(t *testing.T) { 162 | _, err := gerrit.NewClient(context.Background(), "", nil) 163 | if err != gerrit.ErrNoInstanceGiven { 164 | t.Error("Expected `ErrNoInstanceGiven`") 165 | } 166 | } 167 | 168 | func TestNewClient_NoCredentials(t *testing.T) { 169 | client, err := gerrit.NewClient(context.Background(), "http://localhost/", nil) 170 | if err != nil { 171 | t.Errorf("Unexpected error: %s", err.Error()) 172 | } 173 | if client.Authentication.HasAuth() { 174 | t.Error("Expected HasAuth() to return false") 175 | } 176 | } 177 | 178 | func TestNewClient_UsernameWithoutPassword(t *testing.T) { 179 | _, err := gerrit.NewClient(context.Background(), "http://foo@localhost/", nil) 180 | if err != gerrit.ErrUserProvidedWithoutPassword { 181 | t.Error("Expected ErrUserProvidedWithoutPassword") 182 | } 183 | } 184 | 185 | func TestNewClient_AuthenticationFailed(t *testing.T) { 186 | setup() 187 | defer teardown() 188 | 189 | testMux.HandleFunc("/a/accounts/self", func(w http.ResponseWriter, r *http.Request) { 190 | writeresponse(t, w, nil, http.StatusUnauthorized) 191 | }) 192 | 193 | ctx := context.Background() 194 | serverURL := fmt.Sprintf("http://admin:secret@%s/", testServer.Listener.Addr().String()) 195 | client, err := gerrit.NewClient(ctx, serverURL, nil) 196 | if err != gerrit.ErrAuthenticationFailed { 197 | t.Error(err) 198 | } 199 | if client.Authentication.HasAuth() { 200 | t.Error("Expected HasAuth() == false") 201 | } 202 | } 203 | 204 | func TestNewClient_DigestAuth(t *testing.T) { 205 | setup() 206 | defer teardown() 207 | 208 | account := gerrit.AccountInfo{ 209 | AccountID: 100000, 210 | Name: "test", 211 | Email: "test@localhost", 212 | Username: "test"} 213 | hits := 0 214 | 215 | testMux.HandleFunc("/a/accounts/self", func(w http.ResponseWriter, r *http.Request) { 216 | hits++ 217 | switch hits { 218 | case 1: 219 | w.Header().Set("WWW-Authenticate", makedigestheader(r)) 220 | writeresponse(t, w, nil, http.StatusUnauthorized) 221 | case 2: 222 | // go-gerrit should set Authorization in response to a `WWW-Authenticate` header 223 | if !strings.Contains(r.Header.Get("Authorization"), `username="admin"`) { 224 | t.Error(`Missing username="admin"`) 225 | } 226 | writeresponse(t, w, account, http.StatusOK) 227 | case 3: 228 | t.Error("Did not expect another request") 229 | } 230 | }) 231 | 232 | ctx := context.Background() 233 | serverURL := fmt.Sprintf("http://admin:secret@%s/", testServer.Listener.Addr().String()) 234 | client, err := gerrit.NewClient(ctx, serverURL, nil) 235 | if err != nil { 236 | t.Error(err) 237 | } 238 | if !client.Authentication.HasDigestAuth() { 239 | t.Error("Expected HasDigestAuth() == true") 240 | } 241 | } 242 | 243 | func TestNewClient_BasicAuth(t *testing.T) { 244 | setup() 245 | defer teardown() 246 | 247 | account := gerrit.AccountInfo{ 248 | AccountID: 100000, 249 | Name: "test", 250 | Email: "test@localhost", 251 | Username: "test"} 252 | hits := 0 253 | 254 | testMux.HandleFunc("/a/accounts/self", func(w http.ResponseWriter, r *http.Request) { 255 | hits++ 256 | switch hits { 257 | case 1: 258 | writeresponse(t, w, nil, http.StatusUnauthorized) 259 | case 2: 260 | // The second request should be a basic auth request if the first request, which is for 261 | // digest based auth, fails. 262 | if !strings.HasPrefix(r.Header.Get("Authorization"), "Basic ") { 263 | t.Error("Missing 'Basic ' prefix") 264 | } 265 | writeresponse(t, w, account, http.StatusOK) 266 | case 3: 267 | t.Error("Did not expect another request") 268 | } 269 | }) 270 | 271 | serverURL := fmt.Sprintf("http://admin:secret@%s/", testServer.Listener.Addr().String()) 272 | client, err := gerrit.NewClient(context.Background(), serverURL, nil) 273 | if err != nil { 274 | t.Error(err) 275 | } 276 | if !client.Authentication.HasBasicAuth() { 277 | t.Error("Expected HasBasicAuth() == true") 278 | } 279 | } 280 | 281 | func TestNewClient_ReParseURL(t *testing.T) { 282 | urls := map[string][]string{ 283 | "http://admin:ZOSOKjgV/kgEkN0bzPJp+oGeJLqpXykqWFJpon/Ckg@127.0.0.1:5000/": { 284 | "http://127.0.0.1:5000/", "admin", "ZOSOKjgV/kgEkN0bzPJp+oGeJLqpXykqWFJpon/Ckg", 285 | }, 286 | "http://admin:ZOSOKjgV/kgEkN0bzPJp+oGeJLqpXykqWFJpon/Ckg@127.0.0.1:5000/foo": { 287 | "http://127.0.0.1:5000/foo", "admin", "ZOSOKjgV/kgEkN0bzPJp+oGeJLqpXykqWFJpon/Ckg", 288 | }, 289 | "http://admin:ZOSOKjgV/kgEkN0bzPJp+oGeJLqpXykqWFJpon/Ckg@127.0.0.1:5000": { 290 | "http://127.0.0.1:5000", "admin", "ZOSOKjgV/kgEkN0bzPJp+oGeJLqpXykqWFJpon/Ckg", 291 | }, 292 | "https://admin:foo/bar@localhost:5": { 293 | "https://localhost:5", "admin", "foo/bar", 294 | }, 295 | } 296 | for input, expectations := range urls { 297 | submatches := gerrit.ReParseURL.FindAllStringSubmatch(input, -1) 298 | submatch := submatches[0] 299 | username := submatch[2] 300 | password := submatch[3] 301 | endpoint := fmt.Sprintf( 302 | "%s://%s:%s%s", submatch[1], submatch[4], submatch[5], submatch[6]) 303 | if endpoint != expectations[0] { 304 | t.Errorf("%s != %s", expectations[0], endpoint) 305 | } 306 | if username != expectations[1] { 307 | t.Errorf("%s != %s", expectations[1], username) 308 | } 309 | if password != expectations[2] { 310 | t.Errorf("%s != %s", expectations[2], password) 311 | } 312 | 313 | } 314 | } 315 | 316 | func TestNewClient_BasicAuth_PasswordWithSlashes(t *testing.T) { 317 | setup() 318 | defer teardown() 319 | 320 | account := gerrit.AccountInfo{ 321 | AccountID: 100000, 322 | Name: "test", 323 | Email: "test@localhost", 324 | Username: "test"} 325 | hits := 0 326 | 327 | testMux.HandleFunc("/a/accounts/self", func(w http.ResponseWriter, r *http.Request) { 328 | hits++ 329 | switch hits { 330 | case 1: 331 | writeresponse(t, w, nil, http.StatusUnauthorized) 332 | case 2: 333 | // The second request should be a basic auth request if the first request, which is for 334 | // digest based auth, fails. 335 | if !strings.HasPrefix(r.Header.Get("Authorization"), "Basic ") { 336 | t.Error("Missing 'Basic ' prefix") 337 | } 338 | writeresponse(t, w, account, http.StatusOK) 339 | case 3: 340 | t.Error("Did not expect another request") 341 | } 342 | }) 343 | 344 | serverURL := fmt.Sprintf( 345 | "http://admin:ZOSOKjgV/kgEkN0bzPJp+oGeJLqpXykqWFJpon/Ckg@%s", 346 | testServer.Listener.Addr().String()) 347 | client, err := gerrit.NewClient(context.Background(), serverURL, nil) 348 | if err != nil { 349 | t.Error(err) 350 | } 351 | if !client.Authentication.HasAuth() { 352 | t.Error("Expected HasAuth() == true") 353 | } 354 | } 355 | 356 | func TestNewClient_CookieAuth(t *testing.T) { 357 | setup() 358 | defer teardown() 359 | 360 | account := gerrit.AccountInfo{ 361 | AccountID: 100000, 362 | Name: "test", 363 | Email: "test@localhost", 364 | Username: "test"} 365 | hits := 0 366 | 367 | testMux.HandleFunc("/a/accounts/self", func(w http.ResponseWriter, r *http.Request) { 368 | hits++ 369 | switch hits { 370 | case 1: 371 | writeresponse(t, w, nil, http.StatusUnauthorized) 372 | case 2: 373 | writeresponse(t, w, nil, http.StatusUnauthorized) 374 | case 3: 375 | if r.Header.Get("Cookie") != "admin=secret" { 376 | t.Error("Expected cookie to equal 'admin=secret") 377 | } 378 | 379 | writeresponse(t, w, account, http.StatusOK) 380 | case 4: 381 | t.Error("Did not expect another request") 382 | } 383 | }) 384 | 385 | serverURL := fmt.Sprintf("http://admin:secret@%s/", testServer.Listener.Addr().String()) 386 | client, err := gerrit.NewClient(context.Background(), serverURL, nil) 387 | if err != nil { 388 | t.Error(err) 389 | } 390 | if !client.Authentication.HasCookieAuth() { 391 | t.Error("Expected HasCookieAuth() == true") 392 | } 393 | } 394 | 395 | func TestNewRequest(t *testing.T) { 396 | ctx := context.Background() 397 | c, err := gerrit.NewClient(ctx, testGerritInstanceURL, nil) 398 | if err != nil { 399 | t.Errorf("An error occured. Expected nil. Got %+v.", err) 400 | } 401 | 402 | inURL, outURL := "/foo", testGerritInstanceURL+"foo" 403 | inBody, outBody := &gerrit.PermissionRuleInfo{Action: "ALLOW", Force: true, Min: 0, Max: 0}, `{"action":"ALLOW","force":true,"min":0,"max":0}`+"\n" 404 | req, _ := c.NewRequest(ctx, "GET", inURL, inBody) 405 | 406 | // Test that relative URL was expanded 407 | if got, want := req.URL.String(), outURL; got != want { 408 | t.Errorf("NewRequest(%q) URL is %v, want %v", inURL, got, want) 409 | } 410 | 411 | // Test that body was JSON encoded 412 | body, _ := io.ReadAll(req.Body) 413 | if got, want := string(body), outBody; got != want { 414 | t.Errorf("NewRequest Body is %v, want %v", got, want) 415 | } 416 | } 417 | 418 | func TestNewRawPutRequest(t *testing.T) { 419 | ctx := context.Background() 420 | c, err := gerrit.NewClient(ctx, testGerritInstanceURL, nil) 421 | if err != nil { 422 | t.Errorf("An error occured. Expected nil. Got %+v.", err) 423 | } 424 | 425 | inURL, outURL := "/foo", testGerritInstanceURL+"foo" 426 | req, _ := c.NewRawPutRequest(ctx, inURL, "test raw PUT contents") 427 | 428 | // Test that relative URL was expanded 429 | if got, want := req.URL.String(), outURL; got != want { 430 | t.Errorf("NewRequest(%q) URL is %v, want %v", inURL, got, want) 431 | } 432 | 433 | // Test that body was JSON encoded 434 | body, _ := io.ReadAll(req.Body) 435 | if got, want := string(body), "test raw PUT contents"; got != want { 436 | t.Errorf("NewRequest Body is %v, want %v", got, want) 437 | } 438 | } 439 | 440 | func testURLParseError(t *testing.T, err error) { 441 | if err == nil { 442 | t.Errorf("Expected error to be returned") 443 | } 444 | if err, ok := err.(*url.Error); !ok || err.Op != "parse" { 445 | t.Errorf("Expected URL parse error, got %+v", err) 446 | } 447 | } 448 | 449 | func TestNewRequest_BadURL(t *testing.T) { 450 | ctx := context.Background() 451 | c, err := gerrit.NewClient(ctx, testGerritInstanceURL, nil) 452 | if err != nil { 453 | t.Errorf("An error occured. Expected nil. Got %+v.", err) 454 | } 455 | _, err = c.NewRequest(ctx, "GET", ":", nil) 456 | testURLParseError(t, err) 457 | } 458 | 459 | // If a nil body is passed to gerrit.NewRequest, make sure that nil is also passed to http.NewRequest. 460 | // In most cases, passing an io.Reader that returns no content is fine, 461 | // since there is no difference between an HTTP request body that is an empty string versus one that is not set at all. 462 | // However in certain cases, intermediate systems may treat these differently resulting in subtle errors. 463 | func TestNewRequest_EmptyBody(t *testing.T) { 464 | ctx := context.Background() 465 | c, err := gerrit.NewClient(ctx, testGerritInstanceURL, nil) 466 | if err != nil { 467 | t.Errorf("An error occured. Expected nil. Got %+v.", err) 468 | } 469 | req, err := c.NewRequest(ctx, "GET", "/", nil) 470 | if err != nil { 471 | t.Fatalf("NewRequest returned unexpected error: %v", err) 472 | } 473 | if req.Body != nil { 474 | t.Fatalf("constructed request contains a non-nil Body") 475 | } 476 | } 477 | 478 | func TestDo(t *testing.T) { 479 | setup() 480 | defer teardown() 481 | 482 | type foo struct { 483 | A string 484 | } 485 | 486 | testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 487 | if m := "GET"; m != r.Method { 488 | t.Errorf("Request method = %v, want %v", r.Method, m) 489 | } 490 | fmt.Fprint(w, `)]}'`+"\n"+`{"A":"a"}`) 491 | }) 492 | 493 | req, err := testClient.NewRequest(context.Background(), "GET", "/", nil) 494 | if err != nil { 495 | t.Error(err) 496 | } 497 | body := new(foo) 498 | if _, err := testClient.Do(req, body); err != nil { 499 | t.Error(err) 500 | } 501 | 502 | want := &foo{"a"} 503 | if !reflect.DeepEqual(body, want) { 504 | t.Errorf("Response body = %v, want %v", body, want) 505 | } 506 | } 507 | 508 | func TestDo_ioWriter(t *testing.T) { 509 | setup() 510 | defer teardown() 511 | content := `)]}'` + "\n" + `{"A":"a"}` 512 | 513 | testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 514 | if m := "GET"; m != r.Method { 515 | t.Errorf("Request method = %v, want %v", r.Method, m) 516 | } 517 | fmt.Fprint(w, content) 518 | }) 519 | 520 | req, err := testClient.NewRequest(context.Background(), "GET", "/", nil) 521 | if err != nil { 522 | t.Error(err) 523 | } 524 | var buf []byte 525 | actual := bytes.NewBuffer(buf) 526 | if _, err := testClient.Do(req, actual); err != nil { 527 | t.Error(err) 528 | } 529 | 530 | expected := []byte(content) 531 | if !reflect.DeepEqual(actual.Bytes(), expected) { 532 | t.Errorf("Response body = %v, want %v", actual, string(expected)) 533 | } 534 | } 535 | 536 | func TestDo_HTTPError(t *testing.T) { 537 | setup() 538 | defer teardown() 539 | 540 | testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 541 | http.Error(w, "Bad Request", 400) 542 | }) 543 | 544 | req, _ := testClient.NewRequest(context.Background(), "GET", "/", nil) 545 | _, err := testClient.Do(req, nil) 546 | 547 | if err == nil { 548 | t.Error("Expected HTTP 400 error.") 549 | } 550 | } 551 | 552 | // Test handling of an error caused by the internal http client's Do() function. 553 | // A redirect loop is pretty unlikely to occur within the Gerrit API, but does allow us to exercise the right code path. 554 | func TestDo_RedirectLoop(t *testing.T) { 555 | setup() 556 | defer teardown() 557 | 558 | testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 559 | http.Redirect(w, r, "/", http.StatusFound) 560 | }) 561 | 562 | req, _ := testClient.NewRequest(context.Background(), "GET", "/", nil) 563 | _, err := testClient.Do(req, nil) 564 | 565 | if err == nil { 566 | t.Error("Expected error to be returned.") 567 | } 568 | if err, ok := err.(*url.Error); !ok { 569 | t.Errorf("Expected a URL error; got %#v.", err) 570 | } 571 | } 572 | 573 | func TestRemoveMagicPrefixLine(t *testing.T) { 574 | mockData := []struct { 575 | Current, Expected []byte 576 | }{ 577 | {[]byte(`{"A":"a"}`), []byte(`{"A":"a"}`)}, 578 | {[]byte(`)]}'` + "\n" + `{"A":"a"}`), []byte(`{"A":"a"}`)}, 579 | } 580 | for _, mock := range mockData { 581 | body := gerrit.RemoveMagicPrefixLine(mock.Current) 582 | if !reflect.DeepEqual(body, mock.Expected) { 583 | t.Errorf("Response body = %v, want %v", body, mock.Expected) 584 | } 585 | } 586 | } 587 | 588 | func TestRemoveMagicPrefixLineDoesNothingWithoutPrefix(t *testing.T) { 589 | mockData := []struct { 590 | Current, Expected []byte 591 | }{ 592 | {[]byte(`{"A":"a"}`), []byte(`{"A":"a"}`)}, 593 | {[]byte(`{"A":"a"}`), []byte(`{"A":"a"}`)}, 594 | } 595 | for _, mock := range mockData { 596 | body := gerrit.RemoveMagicPrefixLine(mock.Current) 597 | if !reflect.DeepEqual(body, mock.Expected) { 598 | t.Errorf("Response body = %v, want %v", body, mock.Expected) 599 | } 600 | } 601 | } 602 | 603 | func TestNewClientFailsOnDeadConnection(t *testing.T) { 604 | setup() 605 | serverURL := fmt.Sprintf("http://admin:secret@%s/", testServer.Listener.Addr().String()) 606 | teardown() // Closes the server 607 | _, err := gerrit.NewClient(context.Background(), serverURL, nil) 608 | if err == nil { 609 | t.Fatal("Expected err to not be nil") 610 | } 611 | if !strings.Contains(err.Error(), "connection refused") { 612 | t.Fatalf("Unexpected error. 'connected refused' not found in %s", err.Error()) 613 | } 614 | } 615 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/andygrunwald/go-gerrit 2 | 3 | go 1.16 4 | 5 | require github.com/google/go-querystring v1.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 2 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 4 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 5 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 6 | -------------------------------------------------------------------------------- /groups.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // GroupsService contains Group related REST endpoints 9 | // 10 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html 11 | type GroupsService struct { 12 | client *Client 13 | } 14 | 15 | // GroupAuditEventInfo entity contains information about an audit event of a group. 16 | type GroupAuditEventInfo struct { 17 | // TODO Member AccountInfo OR GroupInfo `json:"member"` 18 | Type string `json:"type"` 19 | User AccountInfo `json:"user"` 20 | Date Timestamp `json:"date"` 21 | } 22 | 23 | // GroupInfo entity contains information about a group. 24 | // This can be a Gerrit internal group, or an external group that is known to Gerrit. 25 | type GroupInfo struct { 26 | ID string `json:"id"` 27 | Name string `json:"name,omitempty"` 28 | URL string `json:"url,omitempty"` 29 | Options GroupOptionsInfo `json:"options"` 30 | Description string `json:"description,omitempty"` 31 | GroupID int `json:"group_id,omitempty"` 32 | Owner string `json:"owner,omitempty"` 33 | OwnerID string `json:"owner_id,omitempty"` 34 | CreatedOn *Timestamp `json:"created_on,omitempty"` 35 | MoreGroups bool `json:"_more_groups,omitempty"` 36 | Members []AccountInfo `json:"members,omitempty"` 37 | Includes []GroupInfo `json:"includes,omitempty"` 38 | } 39 | 40 | // GroupInput entity contains information for the creation of a new internal group. 41 | type GroupInput struct { 42 | Name string `json:"name,omitempty"` 43 | Description string `json:"description,omitempty"` 44 | VisibleToAll bool `json:"visible_to_all,omitempty"` 45 | OwnerID string `json:"owner_id,omitempty"` 46 | } 47 | 48 | // GroupOptionsInfo entity contains options of the group. 49 | type GroupOptionsInfo struct { 50 | VisibleToAll bool `json:"visible_to_all,omitempty"` 51 | } 52 | 53 | // GroupOptionsInput entity contains new options for a group. 54 | type GroupOptionsInput struct { 55 | VisibleToAll bool `json:"visible_to_all,omitempty"` 56 | } 57 | 58 | // GroupsInput entity contains information about groups that should be included into a group or that should be deleted from a group. 59 | type GroupsInput struct { 60 | OneGroup string `json:"_one_group,omitempty"` 61 | Groups []string `json:"groups,omitempty"` 62 | } 63 | 64 | // ListGroupsOptions specifies the different options for the ListGroups call. 65 | // 66 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#list-groups 67 | type ListGroupsOptions struct { 68 | // Group Options 69 | // Options fields can be obtained by adding o parameters, each option requires more lookups and slows down the query response time to the client so they are generally disabled by default. 70 | // Optional fields are: 71 | // INCLUDES: include list of directly included groups. 72 | // MEMBERS: include list of direct group members. 73 | Options []string `url:"o,omitempty"` 74 | 75 | // Check if a group is owned by the calling user 76 | // By setting the option owned and specifying a group to inspect with the option q, it is possible to find out, if this group is owned by the calling user. 77 | // If the group is owned by the calling user, the returned map contains this group. If the calling user doesn’t own this group an empty map is returned. 78 | Owned string `url:"owned,omitempty"` 79 | Group string `url:"q,omitempty"` 80 | 81 | // Group Limit 82 | // The /groups/ URL also accepts a limit integer in the n parameter. This limits the results to show n groups. 83 | Limit int `url:"n,omitempty"` 84 | // The /groups/ URL also accepts a start integer in the S parameter. The results will skip S groups from group list. 85 | Skip int `url:"S,omitempty"` 86 | } 87 | 88 | // ListGroups lists the groups accessible by the caller. 89 | // This is the same as using the ls-groups command over SSH, and accepts the same options as query parameters. 90 | // The entries in the map are sorted by group name. 91 | // 92 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#list-groups 93 | func (s *GroupsService) ListGroups(ctx context.Context, opt *ListGroupsOptions) (*map[string]GroupInfo, *Response, error) { 94 | u := "groups/" 95 | 96 | u, err := addOptions(u, opt) 97 | if err != nil { 98 | return nil, nil, err 99 | } 100 | 101 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 102 | if err != nil { 103 | return nil, nil, err 104 | } 105 | 106 | v := new(map[string]GroupInfo) 107 | resp, err := s.client.Do(req, v) 108 | if err != nil { 109 | return nil, resp, err 110 | } 111 | 112 | return v, resp, err 113 | } 114 | 115 | // GetGroup retrieves a group. 116 | // 117 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#get-group 118 | func (s *GroupsService) GetGroup(ctx context.Context, groupID string) (*GroupInfo, *Response, error) { 119 | u := fmt.Sprintf("groups/%s", groupID) 120 | return s.getGroupInfoResponse(ctx, u) 121 | } 122 | 123 | // GetGroupDetail retrieves a group with the direct members and the directly included groups. 124 | // 125 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#get-group-detail 126 | func (s *GroupsService) GetGroupDetail(ctx context.Context, groupID string) (*GroupInfo, *Response, error) { 127 | u := fmt.Sprintf("groups/%s/detail", groupID) 128 | return s.getGroupInfoResponse(ctx, u) 129 | } 130 | 131 | // getGroupInfoResponse retrieved a single GroupInfo Response for a GET request 132 | func (s *GroupsService) getGroupInfoResponse(ctx context.Context, u string) (*GroupInfo, *Response, error) { 133 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 134 | if err != nil { 135 | return nil, nil, err 136 | } 137 | 138 | v := new(GroupInfo) 139 | resp, err := s.client.Do(req, v) 140 | if err != nil { 141 | return nil, resp, err 142 | } 143 | 144 | return v, resp, err 145 | } 146 | 147 | // GetGroupName retrieves the name of a group. 148 | // 149 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#get-group-name 150 | func (s *GroupsService) GetGroupName(ctx context.Context, groupID string) (string, *Response, error) { 151 | u := fmt.Sprintf("groups/%s/name", groupID) 152 | return getStringResponseWithoutOptions(ctx, s.client, u) 153 | } 154 | 155 | // GetGroupDescription retrieves the description of a group. 156 | // 157 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#get-group-description 158 | func (s *GroupsService) GetGroupDescription(ctx context.Context, groupID string) (string, *Response, error) { 159 | u := fmt.Sprintf("groups/%s/description", groupID) 160 | return getStringResponseWithoutOptions(ctx, s.client, u) 161 | } 162 | 163 | // GetGroupOptions retrieves the options of a group. 164 | // 165 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#get-group-options 166 | func (s *GroupsService) GetGroupOptions(ctx context.Context, groupID string) (*GroupOptionsInfo, *Response, error) { 167 | u := fmt.Sprintf("groups/%s/options", groupID) 168 | 169 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 170 | if err != nil { 171 | return nil, nil, err 172 | } 173 | 174 | v := new(GroupOptionsInfo) 175 | resp, err := s.client.Do(req, v) 176 | if err != nil { 177 | return nil, resp, err 178 | } 179 | 180 | return v, resp, err 181 | } 182 | 183 | // GetGroupOwner retrieves the owner group of a Gerrit internal group. 184 | // 185 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#get-group-owner 186 | func (s *GroupsService) GetGroupOwner(ctx context.Context, groupID string) (*GroupInfo, *Response, error) { 187 | u := fmt.Sprintf("groups/%s/owner", groupID) 188 | 189 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 190 | if err != nil { 191 | return nil, nil, err 192 | } 193 | 194 | v := new(GroupInfo) 195 | resp, err := s.client.Do(req, v) 196 | if err != nil { 197 | return nil, resp, err 198 | } 199 | 200 | return v, resp, err 201 | } 202 | 203 | // GetAuditLog gets the audit log of a Gerrit internal group. 204 | // The returned audit events are sorted by date in reverse order so that the newest audit event comes first. 205 | // 206 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#get-audit-log 207 | func (s *GroupsService) GetAuditLog(ctx context.Context, groupID string) (*[]GroupAuditEventInfo, *Response, error) { 208 | u := fmt.Sprintf("groups/%s/log.audit", groupID) 209 | 210 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 211 | if err != nil { 212 | return nil, nil, err 213 | } 214 | 215 | v := new([]GroupAuditEventInfo) 216 | resp, err := s.client.Do(req, v) 217 | if err != nil { 218 | return nil, resp, err 219 | } 220 | 221 | return v, resp, err 222 | } 223 | 224 | // CreateGroup creates a new Gerrit internal group. 225 | // In the request body additional data for the group can be provided as GroupInput. 226 | // 227 | // As response the GroupInfo entity is returned that describes the created group. 228 | // If the group creation fails because the name is already in use the response is “409 Conflict”. 229 | // 230 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#create-group 231 | func (s *GroupsService) CreateGroup(ctx context.Context, groupID string, input *GroupInput) (*GroupInfo, *Response, error) { 232 | u := fmt.Sprintf("groups/%s", groupID) 233 | 234 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 235 | if err != nil { 236 | return nil, nil, err 237 | } 238 | 239 | v := new(GroupInfo) 240 | resp, err := s.client.Do(req, v) 241 | if err != nil { 242 | return nil, resp, err 243 | } 244 | 245 | return v, resp, err 246 | } 247 | 248 | // RenameGroup renames a Gerrit internal group. 249 | // The new group name must be provided in the request body. 250 | // 251 | // As response the new group name is returned. 252 | // If renaming the group fails because the new name is already in use the response is “409 Conflict”. 253 | // 254 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#rename-group 255 | func (s *GroupsService) RenameGroup(ctx context.Context, groupID, name string) (*string, *Response, error) { 256 | u := fmt.Sprintf("groups/%s/name", groupID) 257 | input := struct { 258 | Name string `json:"name"` 259 | }{ 260 | Name: name, 261 | } 262 | 263 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 264 | if err != nil { 265 | return nil, nil, err 266 | } 267 | 268 | v := new(string) 269 | resp, err := s.client.Do(req, v) 270 | if err != nil { 271 | return nil, resp, err 272 | } 273 | 274 | return v, resp, err 275 | } 276 | 277 | // SetGroupDescription sets the description of a Gerrit internal group. 278 | // The new group description must be provided in the request body. 279 | // 280 | // As response the new group description is returned. 281 | // If the description was deleted the response is “204 No Content”. 282 | // 283 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#set-group-description 284 | func (s *GroupsService) SetGroupDescription(ctx context.Context, groupID, description string) (*string, *Response, error) { 285 | u := fmt.Sprintf("groups/%s/description", groupID) 286 | input := struct { 287 | Description string `json:"description"` 288 | }{ 289 | Description: description, 290 | } 291 | 292 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 293 | if err != nil { 294 | return nil, nil, err 295 | } 296 | 297 | v := new(string) 298 | resp, err := s.client.Do(req, v) 299 | if err != nil { 300 | return nil, resp, err 301 | } 302 | 303 | return v, resp, err 304 | } 305 | 306 | // DeleteGroupDescription deletes the description of a Gerrit internal group. 307 | // 308 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#delete-group-description 309 | func (s *GroupsService) DeleteGroupDescription(ctx context.Context, groupID string) (*Response, error) { 310 | u := fmt.Sprintf("groups/%s/description", groupID) 311 | return s.client.DeleteRequest(ctx, u, nil) 312 | } 313 | 314 | // SetGroupOptions sets the options of a Gerrit internal group. 315 | // The new group options must be provided in the request body as a GroupOptionsInput entity. 316 | // 317 | // As response the new group options are returned as a GroupOptionsInfo entity. 318 | // 319 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#set-group-options 320 | func (s *GroupsService) SetGroupOptions(ctx context.Context, groupID string, input *GroupOptionsInput) (*GroupOptionsInfo, *Response, error) { 321 | u := fmt.Sprintf("groups/%s/options", groupID) 322 | 323 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 324 | if err != nil { 325 | return nil, nil, err 326 | } 327 | 328 | v := new(GroupOptionsInfo) 329 | resp, err := s.client.Do(req, v) 330 | if err != nil { 331 | return nil, resp, err 332 | } 333 | 334 | return v, resp, err 335 | } 336 | 337 | // SetGroupOwner sets the owner group of a Gerrit internal group. 338 | // The new owner group must be provided in the request body. 339 | // The new owner can be specified by name, by group UUID or by the legacy numeric group ID. 340 | // 341 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#set-group-owner 342 | func (s *GroupsService) SetGroupOwner(ctx context.Context, groupID, owner string) (*GroupInfo, *Response, error) { 343 | u := fmt.Sprintf("groups/%s/owner", groupID) 344 | input := struct { 345 | Owner string `json:"owner"` 346 | }{ 347 | Owner: owner, 348 | } 349 | 350 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 351 | if err != nil { 352 | return nil, nil, err 353 | } 354 | 355 | v := new(GroupInfo) 356 | resp, err := s.client.Do(req, v) 357 | if err != nil { 358 | return nil, resp, err 359 | } 360 | 361 | return v, resp, err 362 | } 363 | -------------------------------------------------------------------------------- /groups_include.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // ListIncludedGroups lists the directly included groups of a group. 9 | // The entries in the list are sorted by group name and UUID. 10 | // 11 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#included-groups 12 | func (s *GroupsService) ListIncludedGroups(ctx context.Context, groupID string) (*[]GroupInfo, *Response, error) { 13 | u := fmt.Sprintf("groups/%s/groups/", groupID) 14 | 15 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 16 | if err != nil { 17 | return nil, nil, err 18 | } 19 | 20 | v := new([]GroupInfo) 21 | resp, err := s.client.Do(req, v) 22 | if err != nil { 23 | return nil, resp, err 24 | } 25 | 26 | return v, resp, err 27 | } 28 | 29 | // GetIncludedGroup retrieves an included group. 30 | // 31 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#get-included-group 32 | func (s *GroupsService) GetIncludedGroup(ctx context.Context, groupID, includeGroupID string) (*GroupInfo, *Response, error) { 33 | u := fmt.Sprintf("groups/%s/groups/%s", groupID, includeGroupID) 34 | 35 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 36 | if err != nil { 37 | return nil, nil, err 38 | } 39 | 40 | v := new(GroupInfo) 41 | resp, err := s.client.Do(req, v) 42 | if err != nil { 43 | return nil, resp, err 44 | } 45 | 46 | return v, resp, err 47 | } 48 | 49 | // IncludeGroup includes an internal or external group into a Gerrit internal group. 50 | // External groups must be specified using the UUID. 51 | // 52 | // As response a GroupInfo entity is returned that describes the included group. 53 | // The request also succeeds if the group is already included in this group, but then the HTTP response code is 200 OK. 54 | // 55 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#include-group 56 | func (s *GroupsService) IncludeGroup(ctx context.Context, groupID, includeGroupID string) (*GroupInfo, *Response, error) { 57 | u := fmt.Sprintf("groups/%s/groups/%s", groupID, includeGroupID) 58 | 59 | req, err := s.client.NewRequest(ctx, "PUT", u, nil) 60 | if err != nil { 61 | return nil, nil, err 62 | } 63 | 64 | v := new(GroupInfo) 65 | resp, err := s.client.Do(req, v) 66 | if err != nil { 67 | return nil, resp, err 68 | } 69 | 70 | return v, resp, err 71 | } 72 | 73 | // IncludeGroups includes one or several groups into a Gerrit internal group. 74 | // The groups to be included into the group must be provided in the request body as a GroupsInput entity. 75 | // 76 | // As response a list of GroupInfo entities is returned that describes the groups that were specified in the GroupsInput. 77 | // A GroupInfo entity is returned for each group specified in the input, independently of whether the group was newly included into the group or whether the group was already included in the group. 78 | // 79 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#include-groups 80 | func (s *GroupsService) IncludeGroups(ctx context.Context, groupID string, input *GroupsInput) (*[]GroupInfo, *Response, error) { 81 | u := fmt.Sprintf("groups/%s/groups", groupID) 82 | 83 | req, err := s.client.NewRequest(ctx, "POST", u, input) 84 | if err != nil { 85 | return nil, nil, err 86 | } 87 | 88 | v := new([]GroupInfo) 89 | resp, err := s.client.Do(req, v) 90 | if err != nil { 91 | return nil, resp, err 92 | } 93 | 94 | return v, resp, err 95 | } 96 | 97 | // DeleteIncludedGroup deletes an included group from a Gerrit internal group. 98 | // 99 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#include-group 100 | func (s *GroupsService) DeleteIncludedGroup(ctx context.Context, groupID, includeGroupID string) (*Response, error) { 101 | u := fmt.Sprintf("groups/%s/groups/%s", groupID, includeGroupID) 102 | return s.client.DeleteRequest(ctx, u, nil) 103 | } 104 | 105 | // DeleteIncludedGroups delete one or several included groups from a Gerrit internal group. 106 | // The groups to be deleted from the group must be provided in the request body as a GroupsInput entity. 107 | // 108 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#delete-included-groups 109 | func (s *GroupsService) DeleteIncludedGroups(ctx context.Context, groupID string, input *GroupsInput) (*Response, error) { 110 | u := fmt.Sprintf("groups/%s/groups.delete", groupID) 111 | 112 | req, err := s.client.NewRequest(ctx, "POST", u, input) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | return s.client.Do(req, nil) 118 | } 119 | -------------------------------------------------------------------------------- /groups_member.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // ListGroupMembersOptions specifies the different options for the ListGroupMembers call. 9 | // 10 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#group-members 11 | type ListGroupMembersOptions struct { 12 | // To resolve the included groups of a group recursively and to list all members the parameter recursive can be set. 13 | // Members from included external groups and from included groups which are not visible to the calling user are ignored. 14 | Recursive bool `url:"recursive,omitempty"` 15 | } 16 | 17 | // MembersInput entity contains information about accounts that should be added as members to a group or that should be deleted from the group 18 | type MembersInput struct { 19 | OneMember string `json:"_one_member,omitempty"` 20 | Members []string `json:"members,omitempty"` 21 | } 22 | 23 | // ListGroupMembers lists the direct members of a Gerrit internal group. 24 | // The entries in the list are sorted by full name, preferred email and id. 25 | // 26 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#group-members 27 | func (s *GroupsService) ListGroupMembers(ctx context.Context, groupID string, opt *ListGroupMembersOptions) (*[]AccountInfo, *Response, error) { 28 | u := fmt.Sprintf("groups/%s/members/", groupID) 29 | 30 | u, err := addOptions(u, opt) 31 | if err != nil { 32 | return nil, nil, err 33 | } 34 | 35 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 36 | if err != nil { 37 | return nil, nil, err 38 | } 39 | 40 | v := new([]AccountInfo) 41 | resp, err := s.client.Do(req, v) 42 | if err != nil { 43 | return nil, resp, err 44 | } 45 | 46 | return v, resp, err 47 | } 48 | 49 | // GetGroupMember retrieves a group member. 50 | // 51 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#get-group-member 52 | func (s *GroupsService) GetGroupMember(ctx context.Context, groupID, accountID string) (*AccountInfo, *Response, error) { 53 | u := fmt.Sprintf("groups/%s/members/%s", groupID, accountID) 54 | 55 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 56 | if err != nil { 57 | return nil, nil, err 58 | } 59 | 60 | v := new(AccountInfo) 61 | resp, err := s.client.Do(req, v) 62 | if err != nil { 63 | return nil, resp, err 64 | } 65 | 66 | return v, resp, err 67 | } 68 | 69 | // AddGroupMember adds a user as member to a Gerrit internal group. 70 | // 71 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#add-group-member 72 | func (s *GroupsService) AddGroupMember(ctx context.Context, groupID, accountID string) (*AccountInfo, *Response, error) { 73 | u := fmt.Sprintf("groups/%s/members/%s", groupID, accountID) 74 | 75 | req, err := s.client.NewRequest(ctx, "PUT", u, nil) 76 | if err != nil { 77 | return nil, nil, err 78 | } 79 | 80 | v := new(AccountInfo) 81 | resp, err := s.client.Do(req, v) 82 | if err != nil { 83 | return nil, resp, err 84 | } 85 | 86 | return v, resp, err 87 | } 88 | 89 | // AddGroupMembers adds one or several users to a Gerrit internal group. 90 | // The users to be added to the group must be provided in the request body as a MembersInput entity. 91 | // 92 | // As response a list of detailed AccountInfo entities is returned that describes the group members that were specified in the MembersInput. 93 | // An AccountInfo entity is returned for each user specified in the input, independently of whether the user was newly added to the group or whether the user was already a member of the group. 94 | // 95 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#_add_group_members 96 | func (s *GroupsService) AddGroupMembers(ctx context.Context, groupID string, input *MembersInput) (*[]AccountInfo, *Response, error) { 97 | u := fmt.Sprintf("groups/%s/members", groupID) 98 | 99 | req, err := s.client.NewRequest(ctx, "POST", u, input) 100 | if err != nil { 101 | return nil, nil, err 102 | } 103 | 104 | v := new([]AccountInfo) 105 | resp, err := s.client.Do(req, v) 106 | if err != nil { 107 | return nil, resp, err 108 | } 109 | 110 | return v, resp, err 111 | } 112 | 113 | // DeleteGroupMember deletes a user from a Gerrit internal group. 114 | // 115 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#delete-group-member 116 | func (s *GroupsService) DeleteGroupMember(ctx context.Context, groupID, accountID string) (*Response, error) { 117 | u := fmt.Sprintf("groups/%s/members/%s", groupID, accountID) 118 | return s.client.DeleteRequest(ctx, u, nil) 119 | } 120 | 121 | // DeleteGroupMembers delete one or several users from a Gerrit internal group. 122 | // The users to be deleted from the group must be provided in the request body as a MembersInput entity. 123 | // 124 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#delete-group-members 125 | func (s *GroupsService) DeleteGroupMembers(ctx context.Context, groupID string, input *MembersInput) (*Response, error) { 126 | u := fmt.Sprintf("groups/%s/members.delete", groupID) 127 | 128 | req, err := s.client.NewRequest(ctx, "POST", u, input) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | return s.client.Do(req, nil) 134 | } 135 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andygrunwald/go-gerrit/650ad12c8718fc7b18463001cb54ec8593ea5045/img/logo.png -------------------------------------------------------------------------------- /plugins.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // PluginsService contains Plugin related REST endpoints 9 | // 10 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-plugins.html 11 | type PluginsService struct { 12 | client *Client 13 | } 14 | 15 | // PluginInfo entity describes a plugin. 16 | type PluginInfo struct { 17 | ID string `json:"id"` 18 | Version string `json:"version"` 19 | IndexURL string `json:"index_url,omitempty"` 20 | Disabled bool `json:"disabled,omitempty"` 21 | } 22 | 23 | // PluginInput entity describes a plugin that should be installed. 24 | type PluginInput struct { 25 | URL string `json:"url"` 26 | } 27 | 28 | // PluginOptions specifies the different options for the ListPlugins call. 29 | // 30 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-plugins.html#list-plugins 31 | type PluginOptions struct { 32 | // All enabled that all plugins are returned (enabled and disabled). 33 | All bool `url:"all,omitempty"` 34 | } 35 | 36 | // ListPlugins lists the plugins installed on the Gerrit server. 37 | // Only the enabled plugins are returned unless the all option is specified. 38 | // 39 | // To be allowed to see the installed plugins, a user must be a member of a group that is granted the 'View Plugins' capability or the 'Administrate Server' capability. 40 | // The entries in the map are sorted by plugin ID. 41 | // 42 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-plugins.html#list-plugins 43 | func (s *PluginsService) ListPlugins(ctx context.Context, opt *PluginOptions) (*map[string]PluginInfo, *Response, error) { 44 | u := "plugins/" 45 | 46 | u, err := addOptions(u, opt) 47 | if err != nil { 48 | return nil, nil, err 49 | } 50 | 51 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 52 | if err != nil { 53 | return nil, nil, err 54 | } 55 | 56 | v := new(map[string]PluginInfo) 57 | resp, err := s.client.Do(req, v) 58 | if err != nil { 59 | return nil, resp, err 60 | } 61 | 62 | return v, resp, err 63 | } 64 | 65 | // GetPluginStatus retrieves the status of a plugin on the Gerrit server. 66 | // 67 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-plugins.html#get-plugin-status 68 | func (s *PluginsService) GetPluginStatus(ctx context.Context, pluginID string) (*PluginInfo, *Response, error) { 69 | u := fmt.Sprintf("plugins/%s/gerrit~status", pluginID) 70 | return s.requestWithPluginInfoResponse(ctx, "GET", u, nil) 71 | } 72 | 73 | // InstallPlugin installs a new plugin on the Gerrit server. 74 | // If a plugin with the specified name already exists it is overwritten. 75 | // 76 | // Note: if the plugin provides its own name in the MANIFEST file, then the plugin name from the MANIFEST file has precedence over the {plugin-id} above. 77 | // 78 | // The plugin jar can either be sent as binary data in the request body or a URL to the plugin jar must be provided in the request body inside a PluginInput entity. 79 | // 80 | // As response a PluginInfo entity is returned that describes the plugin. 81 | // If an existing plugin was overwritten the response is “200 OK”. 82 | // 83 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#set-dashboard 84 | func (s *PluginsService) InstallPlugin(ctx context.Context, pluginID string, input *PluginInput) (*PluginInfo, *Response, error) { 85 | u := fmt.Sprintf("plugins/%s", pluginID) 86 | return s.requestWithPluginInfoResponse(ctx, "PUT", u, input) 87 | } 88 | 89 | // EnablePlugin enables a plugin on the Gerrit server. 90 | // 91 | // As response a PluginInfo entity is returned that describes the plugin. 92 | // 93 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-plugins.html#enable-plugin 94 | func (s *PluginsService) EnablePlugin(ctx context.Context, pluginID string) (*PluginInfo, *Response, error) { 95 | u := fmt.Sprintf("plugins/%s/gerrit~enable", pluginID) 96 | return s.requestWithPluginInfoResponse(ctx, "POST", u, nil) 97 | } 98 | 99 | // DisablePlugin disables a plugin on the Gerrit server. 100 | // 101 | // As response a PluginInfo entity is returned that describes the plugin. 102 | // 103 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-plugins.html#disable-plugin 104 | func (s *PluginsService) DisablePlugin(ctx context.Context, pluginID string) (*PluginInfo, *Response, error) { 105 | u := fmt.Sprintf("plugins/%s/gerrit~disable", pluginID) 106 | return s.requestWithPluginInfoResponse(ctx, "POST", u, nil) 107 | } 108 | 109 | // ReloadPlugin reloads a plugin on the Gerrit server. 110 | // 111 | // As response a PluginInfo entity is returned that describes the plugin. 112 | // 113 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-plugins.html#disable-plugin 114 | func (s *PluginsService) ReloadPlugin(ctx context.Context, pluginID string) (*PluginInfo, *Response, error) { 115 | u := fmt.Sprintf("plugins/%s/gerrit~reload", pluginID) 116 | return s.requestWithPluginInfoResponse(ctx, "POST", u, nil) 117 | } 118 | 119 | func (s *PluginsService) requestWithPluginInfoResponse(ctx context.Context, method, u string, input interface{}) (*PluginInfo, *Response, error) { 120 | req, err := s.client.NewRequest(ctx, method, u, input) 121 | if err != nil { 122 | return nil, nil, err 123 | } 124 | 125 | v := new(PluginInfo) 126 | resp, err := s.client.Do(req, v) 127 | if err != nil { 128 | return nil, resp, err 129 | } 130 | 131 | return v, resp, err 132 | } 133 | -------------------------------------------------------------------------------- /projects_access.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // ProjectAccessInput describes changes that should be applied to a project access config 10 | // 11 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-access-input 12 | type ProjectAccessInput struct { 13 | // A list of deductions to be applied to the project access as ProjectAccessInfo entities. 14 | Remove map[string]AccessSectionInfo `json:"remove"` 15 | 16 | // A list of additions to be applied to the project access as ProjectAccessInfo entities. 17 | Add map[string]AccessSectionInfo `json:"add"` 18 | 19 | // A commit message for this change. 20 | Message string `json:"message"` 21 | 22 | // A new parent for the project to inherit from. Changing the parent project requires administrative privileges. 23 | Parent string `json:"parent"` 24 | } 25 | 26 | // AccessCheckInfo entity is the result of an access check. 27 | // 28 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#access-check-info 29 | type AccessCheckInfo struct { 30 | // The HTTP status code for the access. 200 means success and 403 means denied. 31 | Status int `json:"status"` 32 | 33 | // A clarifying message if status is not 200. 34 | Message string `json:"message"` 35 | } 36 | 37 | // CheckAccessOptions is options for check access 38 | type CheckAccessOptions struct { 39 | // The account for which to check access. Mandatory. 40 | Account string `url:"account,omitempty"` 41 | 42 | // The ref permission for which to check access. If not specified, read access to at least branch is checked. 43 | Permission string `url:"perm,omitempty"` 44 | 45 | // The branch for which to check access. This must be given if perm is specified. 46 | Ref string `url:"ref,omitempty"` 47 | } 48 | 49 | // ListAccessRights lists the access rights for a single project 50 | // 51 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-access 52 | func (s *ProjectsService) ListAccessRights(ctx context.Context, projectName string) (*ProjectAccessInfo, *Response, error) { 53 | u := fmt.Sprintf("projects/%s/access", url.QueryEscape(projectName)) 54 | 55 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 56 | if err != nil { 57 | return nil, nil, err 58 | } 59 | 60 | v := new(ProjectAccessInfo) 61 | resp, err := s.client.Do(req, v) 62 | if err != nil { 63 | return nil, resp, err 64 | } 65 | 66 | return v, resp, err 67 | } 68 | 69 | // AddUpdateDeleteAccessRights add, update and delete access rights for project 70 | // 71 | // Sets access rights for the project using the diff schema provided by ProjectAccessInput. 72 | // Deductions are used to remove access sections, permissions or permission rules. 73 | // The backend will remove the entity with the finest granularity in the request, 74 | // meaning that if an access section without permissions is posted, the access section will be removed; 75 | // if an access section with a permission but no permission rules is posted, the permission will be removed; 76 | // if an access section with a permission and a permission rule is posted, the permission rule will be removed. 77 | // 78 | // Additionally, access sections and permissions will be cleaned up after applying the deductions by 79 | // removing items that have no child elements. 80 | // 81 | // After removals have been applied, additions will be applied. 82 | // 83 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#set-access 84 | func (s *ProjectsService) AddUpdateDeleteAccessRights(ctx context.Context, projectName string, input *ProjectAccessInput) (*ProjectAccessInfo, *Response, error) { 85 | u := fmt.Sprintf("projects/%s/access", url.QueryEscape(projectName)) 86 | 87 | req, err := s.client.NewRequest(ctx, "POST", u, input) 88 | if err != nil { 89 | return nil, nil, err 90 | } 91 | 92 | v := new(ProjectAccessInfo) 93 | resp, err := s.client.Do(req, v) 94 | if err != nil { 95 | return nil, resp, err 96 | } 97 | 98 | return v, resp, err 99 | } 100 | 101 | // CreateAccessRightChange sets access rights for the project using the diff schema provided by ProjectAccessInput 102 | // 103 | // This takes the same input as Update Access Rights, but creates a pending change for review. 104 | // Like Create Change, it returns a ChangeInfo entity describing the resulting change. 105 | // 106 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-access-change 107 | func (s *ProjectsService) CreateAccessRightChange(ctx context.Context, projectName string, input *ProjectAccessInput) (*ChangeInfo, *Response, error) { 108 | u := fmt.Sprintf("projects/%s/access:review", url.QueryEscape(projectName)) 109 | 110 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 111 | if err != nil { 112 | return nil, nil, err 113 | } 114 | 115 | v := new(ChangeInfo) 116 | resp, err := s.client.Do(req, v) 117 | if err != nil { 118 | return nil, resp, err 119 | } 120 | 121 | return v, resp, err 122 | } 123 | 124 | // CheckAccess runs access checks for other users. This requires the View Access global capability. 125 | // 126 | // The result is a AccessCheckInfo entity detailing the access of the given user for the given project, project-ref, or project-permission-ref combination. 127 | // 128 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#check-access 129 | func (s *ProjectsService) CheckAccess(ctx context.Context, projectName string, opt *CheckAccessOptions) (*AccessCheckInfo, *Response, error) { 130 | u := fmt.Sprintf("projects/%s/check.access", url.QueryEscape(projectName)) 131 | 132 | u, err := addOptions(u, opt) 133 | if err != nil { 134 | return nil, nil, err 135 | } 136 | 137 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 138 | if err != nil { 139 | return nil, nil, err 140 | } 141 | 142 | v := new(AccessCheckInfo) 143 | resp, err := s.client.Do(req, v) 144 | if err != nil { 145 | return nil, resp, err 146 | } 147 | 148 | return v, resp, err 149 | } 150 | -------------------------------------------------------------------------------- /projects_access_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/andygrunwald/go-gerrit" 11 | ) 12 | 13 | func TestProjectsService_ListAccessRights(t *testing.T) { 14 | setup() 15 | defer teardown() 16 | 17 | testMux.HandleFunc("/projects/MyProject/access", func(w http.ResponseWriter, r *http.Request) { 18 | testMethod(t, r, "GET") 19 | 20 | // from: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-access 21 | resp := `{"can_add":true,"can_add_tags":true,"can_upload":true,"config_visible":true,"groups":{"c2ce4749a32ceb82cd6adcce65b8216e12afb41c":{"created_on":"2009-06-08 23:31:00.000000000","description":"Users who perform batch actions on Gerrit","group_id":2,"name":"Non-Interactive Users","options":{},"owner":"Administrators","owner_id":"d5b7124af4de52924ed397913e2c3b37bf186948","url":"#/admin/groups/uuid-c2ce4749a32ceb82cd6adcce65b8216e12afb41c"},"global:Anonymous-Users":{"name":"Anonymous Users","options":{}}},"inherits_from":{"description":"Access inherited by all other projects.","id":"All-Projects","name":"All-Projects"},"is_owner":true,"local":{"refs/*":{"permissions":{"read":{"rules":{"c2ce4749a32ceb82cd6adcce65b8216e12afb41c":{"action":"ALLOW","force":false},"global:Anonymous-Users":{"action":"ALLOW","force":false}}}}}},"owner_of":["refs/*"],"revision":"61157ed63e14d261b6dca40650472a9b0bd88474"}` 22 | _, _ = fmt.Fprint(w, `)]}'`+"\n"+resp) 23 | }) 24 | 25 | projectAccessRight, _, err := testClient.Projects.ListAccessRights(context.Background(), "MyProject") 26 | if err != nil { 27 | t.Errorf("project: list access rights error: %s", err) 28 | } 29 | 30 | // Doing one deep check to verify the mapping 31 | if projectAccessRight.InheritsFrom.Name != "All-Projects" { 32 | t.Errorf("projectAccessRight.InheritsFrom.Name not matching. Expected '%s', got '%s'", "All-Projects", projectAccessRight.InheritsFrom.Name) 33 | } 34 | } 35 | 36 | func TestProjectsService_AddUpdateDeleteAccessRights(t *testing.T) { 37 | setup() 38 | defer teardown() 39 | 40 | testMux.HandleFunc("/projects/MyProject/access", func(w http.ResponseWriter, r *http.Request) { 41 | testMethod(t, r, "POST") 42 | 43 | // from: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#set-access 44 | resp := `{"revision":"61157ed63e14d261b6dca40650472a9b0bd88474","inherits_from":{"id":"All-Projects","name":"All-Projects","description":"Accessinheritedbyallotherprojects."},"local":{"refs/*":{"permissions":{"read":{"rules":{"global:Anonymous-Users":{"action":"ALLOW","force":false}}}}}},"is_owner":true,"owner_of":["refs/*"],"can_upload":true,"can_add":true,"can_add_tags":true,"config_visible":true,"groups":{"global:Anonymous-Users":{"options":{},"name":"AnonymousUsers"}}}` 45 | _, _ = fmt.Fprint(w, `)]}'`+"\n"+resp) 46 | }) 47 | 48 | var data = `{"remove":{"refs/*":{"permissions":{"read":{"rules":{"c2ce4749a32ceb82cd6adcce65b8216e12afb41c":{"action":"ALLOW"}}}}}}}` 49 | var req = new(gerrit.ProjectAccessInput) 50 | if err := json.Unmarshal([]byte(data), &req); err != nil { 51 | t.Errorf("project: add/update/delete access right request params error: %s", err) 52 | } 53 | 54 | projectAccessRight, _, err := testClient.Projects.AddUpdateDeleteAccessRights(context.Background(), "MyProject", req) 55 | if err != nil { 56 | t.Errorf("project: add/update/delete access right error: %s", err) 57 | } 58 | 59 | // Doing one deep check to verify the mapping 60 | if projectAccessRight.InheritsFrom.Name != "All-Projects" { 61 | t.Errorf("projectAccessRight.InheritsFrom.Name not matching. Expected '%s', got '%s'", "All-Projects", projectAccessRight.InheritsFrom.Name) 62 | } 63 | } 64 | 65 | func TestProjectsService_AccessCheck(t *testing.T) { 66 | setup() 67 | defer teardown() 68 | 69 | testMux.HandleFunc("/projects/MyProject/check.access", func(w http.ResponseWriter, r *http.Request) { 70 | testMethod(t, r, "GET") 71 | 72 | testQueryValues(t, r, testValues{ 73 | "account": "1000098", 74 | "ref": "refs/heads/secret/bla", 75 | }) 76 | 77 | // from: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#set-access 78 | resp := `{"message": "user Kristen Burns \u003cKristen.Burns@gerritcodereview.com\u003e (1000098) cannot see ref refs/heads/secret/bla in project MyProject","status":403}` 79 | _, _ = fmt.Fprint(w, `)]}'`+"\n"+resp) 80 | }) 81 | 82 | var data = `{"account":"1000098","ref":"refs/heads/secret/bla"}` 83 | var req = new(gerrit.CheckAccessOptions) 84 | if err := json.Unmarshal([]byte(data), &req); err != nil { 85 | t.Errorf("project: access check request params error: %s", err) 86 | } 87 | 88 | accessCheckInfo, _, err := testClient.Projects.CheckAccess(context.Background(), "MyProject", req) 89 | if err != nil { 90 | t.Errorf("project: access check error: %s", err) 91 | } 92 | 93 | // Doing one deep check to verify the mapping 94 | if accessCheckInfo.Status != 403 { 95 | t.Errorf("accessCheckInfo.Status not matching. Expected '%d', got '%d'", 403, accessCheckInfo.Status) 96 | } 97 | } 98 | 99 | func TestProjectsService_CreateAccessChange(t *testing.T) { 100 | setup() 101 | defer teardown() 102 | 103 | testMux.HandleFunc("/projects/MyProject/access:review", func(w http.ResponseWriter, r *http.Request) { 104 | testMethod(t, r, "PUT") 105 | 106 | // from: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-access-change 107 | resp := `{"id":"testproj~refs%2Fmeta%2Fconfig~Ieaf185bf90a1fc3b58461e399385e158a20b31a2","project":"testproj","branch":"refs/meta/config","hashtags":[],"change_id":"Ieaf185bf90a1fc3b58461e399385e158a20b31a2","subject":"Reviewaccesschange","status":"NEW","created":"2017-09-07 14:31:11.852000000","updated":"2017-09-07 14:31:11.852000000","submit_type":"CHERRY_PICK","mergeable":true,"insertions":2,"deletions":0,"unresolved_comment_count":0,"has_review_started":true,"_number":7,"owner":{"_account_id":1000000}}` 108 | _, _ = fmt.Fprint(w, `)]}'`+"\n"+resp) 109 | }) 110 | 111 | var data = `{"add":{"refs/heads/*":{"permissions":{"read":{"rules":{"global:Anonymous-Users":{"action":"DENY","force":false}}}}}}}` 112 | var req = new(gerrit.ProjectAccessInput) 113 | if err := json.Unmarshal([]byte(data), &req); err != nil { 114 | t.Errorf("project: create access change request params error: %s", err) 115 | } 116 | 117 | changeInfo, _, err := testClient.Projects.CreateAccessRightChange(context.Background(), "MyProject", req) 118 | if err != nil { 119 | t.Errorf("project: create access change error: %s", err) 120 | } 121 | 122 | // Doing one deep check to verify the mapping 123 | if changeInfo.ChangeID != "Ieaf185bf90a1fc3b58461e399385e158a20b31a2" { 124 | t.Errorf("changeInfo.ChangeID not matching. Expected '%s', got '%s'", "Ieaf185bf90a1fc3b58461e399385e158a20b31a2", changeInfo.ChangeID) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /projects_branch.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // BranchInfo entity contains information about a branch. 10 | type BranchInfo struct { 11 | Ref string `json:"ref"` 12 | Revision string `json:"revision"` 13 | CanDelete bool `json:"can_delete"` 14 | WebLinks []WebLinkInfo `json:"web_links,omitempty"` 15 | } 16 | 17 | // BranchInput entity contains information for the creation of a new branch. 18 | type BranchInput struct { 19 | Ref string `json:"ref,omitempty"` 20 | Revision string `json:"revision,omitempty"` 21 | } 22 | 23 | // DeleteBranchesInput entity contains information about branches that should be deleted. 24 | type DeleteBranchesInput struct { 25 | Branches []string `json:"DeleteBranchesInput"` 26 | } 27 | 28 | // BranchOptions specifies the parameters to the branch API endpoints. 29 | // 30 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#branch-options 31 | type BranchOptions struct { 32 | // Limit the number of branches to be included in the results. 33 | Limit int `url:"n,omitempty"` 34 | 35 | // Skip the given number of branches from the beginning of the list. 36 | Skip string `url:"s,omitempty"` 37 | 38 | // Substring limits the results to those projects that match the specified substring. 39 | Substring string `url:"m,omitempty"` 40 | 41 | // Limit the results to those branches that match the specified regex. 42 | // Boundary matchers '^' and '$' are implicit. 43 | // For example: the regex 't*' will match any branches that start with 'test' and regex '*t' will match any branches that end with 'test'. 44 | Regex string `url:"r,omitempty"` 45 | } 46 | 47 | // ListBranches list the branches of a project. 48 | // 49 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-branches 50 | func (s *ProjectsService) ListBranches(ctx context.Context, projectName string, opt *BranchOptions) (*[]BranchInfo, *Response, error) { 51 | u := fmt.Sprintf("projects/%s/branches/", url.QueryEscape(projectName)) 52 | 53 | u, err := addOptions(u, opt) 54 | if err != nil { 55 | return nil, nil, err 56 | } 57 | 58 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 59 | if err != nil { 60 | return nil, nil, err 61 | } 62 | 63 | v := new([]BranchInfo) 64 | resp, err := s.client.Do(req, v) 65 | if err != nil { 66 | return nil, resp, err 67 | } 68 | 69 | return v, resp, err 70 | } 71 | 72 | // GetBranch retrieves a branch of a project. 73 | // 74 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-branch 75 | func (s *ProjectsService) GetBranch(ctx context.Context, projectName, branchID string) (*BranchInfo, *Response, error) { 76 | u := fmt.Sprintf("projects/%s/branches/%s", url.QueryEscape(projectName), url.QueryEscape(branchID)) 77 | 78 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 79 | if err != nil { 80 | return nil, nil, err 81 | } 82 | 83 | v := new(BranchInfo) 84 | resp, err := s.client.Do(req, v) 85 | if err != nil { 86 | return nil, resp, err 87 | } 88 | 89 | return v, resp, err 90 | } 91 | 92 | // GetReflog gets the reflog of a certain branch. 93 | // The caller must be project owner. 94 | // 95 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-reflog 96 | func (s *ProjectsService) GetReflog(ctx context.Context, projectName, branchID string) (*[]ReflogEntryInfo, *Response, error) { 97 | u := fmt.Sprintf("projects/%s/branches/%s/reflog", url.QueryEscape(projectName), url.QueryEscape(branchID)) 98 | 99 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 100 | if err != nil { 101 | return nil, nil, err 102 | } 103 | 104 | v := new([]ReflogEntryInfo) 105 | resp, err := s.client.Do(req, v) 106 | if err != nil { 107 | return nil, resp, err 108 | } 109 | 110 | return v, resp, err 111 | } 112 | 113 | // CreateBranch creates a new branch. 114 | // In the request body additional data for the branch can be provided as BranchInput. 115 | // 116 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-branch 117 | func (s *ProjectsService) CreateBranch(ctx context.Context, projectName, branchID string, input *BranchInput) (*BranchInfo, *Response, error) { 118 | u := fmt.Sprintf("projects/%s/branches/%s", url.QueryEscape(projectName), url.QueryEscape(branchID)) 119 | 120 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 121 | if err != nil { 122 | return nil, nil, err 123 | } 124 | 125 | v := new(BranchInfo) 126 | resp, err := s.client.Do(req, v) 127 | if err != nil { 128 | return nil, resp, err 129 | } 130 | 131 | return v, resp, err 132 | } 133 | 134 | // DeleteBranch deletes a branch. 135 | // 136 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#delete-branch 137 | func (s *ProjectsService) DeleteBranch(ctx context.Context, projectName, branchID string) (*Response, error) { 138 | u := fmt.Sprintf("projects/%s/branches/%s", url.QueryEscape(projectName), url.QueryEscape(branchID)) 139 | return s.client.DeleteRequest(ctx, u, nil) 140 | } 141 | 142 | // DeleteBranches delete one or more branches. 143 | // If some branches could not be deleted, the response is “409 Conflict” and the error message is contained in the response body. 144 | // 145 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#delete-branches 146 | func (s *ProjectsService) DeleteBranches(ctx context.Context, projectName string, input *DeleteBranchesInput) (*Response, error) { 147 | u := fmt.Sprintf("projects/%s/branches:delete", url.QueryEscape(projectName)) 148 | return s.client.DeleteRequest(ctx, u, input) 149 | } 150 | 151 | // GetBranchContent gets the content of a file from the HEAD revision of a certain branch. 152 | // The content is returned as base64 encoded string. 153 | // 154 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-content 155 | func (s *ProjectsService) GetBranchContent(ctx context.Context, projectName, branchID, fileID string) (string, *Response, error) { 156 | u := fmt.Sprintf("projects/%s/branches/%s/files/%s/content", url.QueryEscape(projectName), url.QueryEscape(branchID), fileID) 157 | return getStringResponseWithoutOptions(ctx, s.client, u) 158 | } 159 | -------------------------------------------------------------------------------- /projects_childproject.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // ChildProjectOptions specifies the parameters to the Child Project API endpoints. 10 | // 11 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-child-projects 12 | type ChildProjectOptions struct { 13 | // Recursive resolve the child projects of a project recursively. 14 | // Child projects that are not visible to the calling user are ignored and are not resolved further. 15 | Recursive int `url:"recursive,omitempty"` 16 | } 17 | 18 | // ListChildProjects lists the direct child projects of a project. 19 | // 20 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-child-projects 21 | func (s *ProjectsService) ListChildProjects(ctx context.Context, projectName string, opt *ChildProjectOptions) (*[]ProjectInfo, *Response, error) { 22 | u := fmt.Sprintf("projects/%s/children/", url.QueryEscape(projectName)) 23 | 24 | u, err := addOptions(u, opt) 25 | if err != nil { 26 | return nil, nil, err 27 | } 28 | 29 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 30 | if err != nil { 31 | return nil, nil, err 32 | } 33 | 34 | v := new([]ProjectInfo) 35 | resp, err := s.client.Do(req, v) 36 | if err != nil { 37 | return nil, resp, err 38 | } 39 | 40 | return v, resp, err 41 | } 42 | 43 | // GetChildProject retrieves a child project. 44 | // If a non-direct child project should be retrieved the parameter recursive must be set. 45 | // 46 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-child-project 47 | func (s *ProjectsService) GetChildProject(ctx context.Context, projectName, childProjectName string, opt *ChildProjectOptions) (*ProjectInfo, *Response, error) { 48 | u := fmt.Sprintf("projects/%s/children/%s", url.QueryEscape(projectName), url.QueryEscape(childProjectName)) 49 | 50 | u, err := addOptions(u, opt) 51 | if err != nil { 52 | return nil, nil, err 53 | } 54 | 55 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 56 | if err != nil { 57 | return nil, nil, err 58 | } 59 | 60 | v := new(ProjectInfo) 61 | resp, err := s.client.Do(req, v) 62 | if err != nil { 63 | return nil, resp, err 64 | } 65 | 66 | return v, resp, err 67 | } 68 | -------------------------------------------------------------------------------- /projects_commit.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // GetCommit retrieves a commit of a project. 10 | // The commit must be visible to the caller. 11 | // 12 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-commit 13 | func (s *ProjectsService) GetCommit(ctx context.Context, projectName, commitID string) (*CommitInfo, *Response, error) { 14 | u := fmt.Sprintf("projects/%s/commits/%s", url.QueryEscape(projectName), commitID) 15 | 16 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 17 | if err != nil { 18 | return nil, nil, err 19 | } 20 | 21 | v := new(CommitInfo) 22 | resp, err := s.client.Do(req, v) 23 | if err != nil { 24 | return nil, resp, err 25 | } 26 | 27 | return v, resp, err 28 | } 29 | 30 | // GetIncludeIn Retrieves the branches and tags in which a change is included. 31 | // Branches that are not visible to the calling user according to the project’s read permissions are filtered out from the result. 32 | // 33 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-included-in 34 | func (s *ProjectsService) GetIncludeIn(ctx context.Context, projectName, commitID string) (*IncludedInInfo, *Response, error) { 35 | u := fmt.Sprintf("projects/%s/commits/%s/in", url.QueryEscape((projectName)), commitID) 36 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | v := new(IncludedInInfo) 41 | resp, err := s.client.Do(req, v) 42 | if err != nil { 43 | return nil, resp, err 44 | } 45 | 46 | return v, resp, err 47 | } 48 | 49 | // GetCommitContent gets the content of a file from a certain commit. 50 | // The content is returned as base64 encoded string. 51 | // 52 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html##get-content-from-commit 53 | func (s *ProjectsService) GetCommitContent(ctx context.Context, projectName, commitID, fileID string) (string, *Response, error) { 54 | u := fmt.Sprintf("projects/%s/commits/%s/files/%s/content", url.QueryEscape(projectName), commitID, fileID) 55 | return getStringResponseWithoutOptions(ctx, s.client, u) 56 | } 57 | -------------------------------------------------------------------------------- /projects_commit_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/andygrunwald/go-gerrit" 11 | ) 12 | 13 | // +func (s *ProjectsService) GetIncludeIn(projectName, commitID string) (*IncludedInInfo, *Response, error){ 14 | func TestProjectsService_GetIncludeIn(t *testing.T) { 15 | setup() 16 | defer teardown() 17 | 18 | testMux.HandleFunc("/projects/swift/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96/in", func(w http.ResponseWriter, r *http.Request) { 19 | testMethod(t, r, "GET") 20 | fmt.Fprint(w, `)]}'`+"\n"+`{"branches": ["master"],"tags": ["1.1.0"]}`) 21 | }) 22 | 23 | includedInInfo, _, err := testClient.Projects.GetIncludeIn(context.Background(), "swift", "a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96") 24 | if err != nil { 25 | t.Errorf("Projects.GetIncludeIn returned error: %v", err) 26 | } 27 | 28 | want := &gerrit.IncludedInInfo{ 29 | Branches: []string{ 30 | "master", 31 | }, 32 | Tags: []string{ 33 | "1.1.0", 34 | }, 35 | } 36 | 37 | if !reflect.DeepEqual(includedInInfo, want) { 38 | t.Errorf("Projects.GetIncludeIn returned %+v, want %+v", includedInInfo, want) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects_dashboard.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // DashboardSectionInfo entity contains information about a section in a dashboard. 10 | type DashboardSectionInfo struct { 11 | Name string `json:"name"` 12 | Query string `json:"query"` 13 | } 14 | 15 | // DashboardInput entity contains information to create/update a project dashboard. 16 | type DashboardInput struct { 17 | ID string `json:"id,omitempty"` 18 | CommitMessage string `json:"commit_message,omitempty"` 19 | } 20 | 21 | // DashboardInfo entity contains information about a project dashboard. 22 | type DashboardInfo struct { 23 | ID string `json:"id"` 24 | Project string `json:"project"` 25 | DefiningProject string `json:"defining_project"` 26 | Ref string `json:"ref"` 27 | Path string `json:"path"` 28 | Description string `json:"description,omitempty"` 29 | Foreach string `json:"foreach,omitempty"` 30 | URL string `json:"url"` 31 | Default bool `json:"default"` 32 | Title string `json:"title,omitempty"` 33 | Sections []DashboardSectionInfo `json:"sections"` 34 | } 35 | 36 | // ListDashboards list custom dashboards for a project. 37 | // 38 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-dashboards 39 | func (s *ProjectsService) ListDashboards(ctx context.Context, projectName string) (*[]DashboardInfo, *Response, error) { 40 | u := fmt.Sprintf("projects/%s/dashboards/", url.QueryEscape(projectName)) 41 | 42 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | 47 | v := new([]DashboardInfo) 48 | resp, err := s.client.Do(req, v) 49 | if err != nil { 50 | return nil, resp, err 51 | } 52 | 53 | return v, resp, err 54 | } 55 | 56 | // GetDashboard list custom dashboards for a project. 57 | // 58 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard 59 | func (s *ProjectsService) GetDashboard(ctx context.Context, projectName, dashboardName string) (*DashboardInfo, *Response, error) { 60 | u := fmt.Sprintf("projects/%s/dashboards/%s", url.QueryEscape(projectName), url.QueryEscape(dashboardName)) 61 | 62 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 63 | if err != nil { 64 | return nil, nil, err 65 | } 66 | 67 | v := new(DashboardInfo) 68 | resp, err := s.client.Do(req, v) 69 | if err != nil { 70 | return nil, resp, err 71 | } 72 | 73 | return v, resp, err 74 | } 75 | 76 | // SetDashboard updates/Creates a project dashboard. 77 | // Currently only supported for the default dashboard. 78 | // 79 | // The creation/update information for the dashboard must be provided in the request body as a DashboardInput entity. 80 | // 81 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#set-dashboard 82 | func (s *ProjectsService) SetDashboard(ctx context.Context, projectName, dashboardID string, input *DashboardInput) (*DashboardInfo, *Response, error) { 83 | u := fmt.Sprintf("projects/%s/dashboards/%s", url.QueryEscape(projectName), url.QueryEscape(dashboardID)) 84 | 85 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 86 | if err != nil { 87 | return nil, nil, err 88 | } 89 | 90 | v := new(DashboardInfo) 91 | resp, err := s.client.Do(req, v) 92 | if err != nil { 93 | return nil, resp, err 94 | } 95 | 96 | return v, resp, err 97 | } 98 | 99 | // DeleteDashboard deletes a project dashboard. 100 | // Currently only supported for the default dashboard. 101 | // 102 | // The request body does not need to include a DashboardInput entity if no commit message is specified. 103 | // Please note that some proxies prohibit request bodies for DELETE requests. 104 | // 105 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#delete-dashboard 106 | func (s *ProjectsService) DeleteDashboard(ctx context.Context, projectName, dashboardID string, input *DashboardInput) (*Response, error) { 107 | u := fmt.Sprintf("projects/%s/dashboards/%s", url.QueryEscape(projectName), url.QueryEscape(dashboardID)) 108 | return s.client.DeleteRequest(ctx, u, input) 109 | } 110 | -------------------------------------------------------------------------------- /projects_tag.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // TagInfo entity contains information about a tag. 10 | type TagInfo struct { 11 | Ref string `json:"ref"` 12 | Revision string `json:"revision"` 13 | Object string `json:"object"` 14 | Message string `json:"message"` 15 | Tagger GitPersonInfo `json:"tagger"` 16 | Created *Timestamp `json:"created,omitempty"` 17 | } 18 | 19 | // TagInput entity for create a tag. 20 | type TagInput struct { 21 | Ref string `json:"ref"` 22 | Revision string `json:"revision,omitempty"` 23 | Message string `json:"message,omitempty"` 24 | } 25 | 26 | // DeleteTagsInput entity for delete tags. 27 | type DeleteTagsInput struct { 28 | Tags []string `json:"tags"` 29 | } 30 | 31 | // ListTags list the tags of a project. 32 | // 33 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#list-tags 34 | func (s *ProjectsService) ListTags(ctx context.Context, projectName string, opt *ProjectBaseOptions) (*[]TagInfo, *Response, error) { 35 | u := fmt.Sprintf("projects/%s/tags/", url.QueryEscape(projectName)) 36 | u, err := addOptions(u, opt) 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | 41 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 42 | if err != nil { 43 | return nil, nil, err 44 | } 45 | 46 | v := new([]TagInfo) 47 | resp, err := s.client.Do(req, v) 48 | if err != nil { 49 | return nil, resp, err 50 | } 51 | 52 | return v, resp, err 53 | } 54 | 55 | // GetTag retrieves a tag of a project. 56 | // 57 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-tag 58 | func (s *ProjectsService) GetTag(ctx context.Context, projectName, tagName string) (*TagInfo, *Response, error) { 59 | u := fmt.Sprintf("projects/%s/tags/%s", url.QueryEscape(projectName), url.QueryEscape(tagName)) 60 | 61 | req, err := s.client.NewRequest(ctx, "GET", u, nil) 62 | if err != nil { 63 | return nil, nil, err 64 | } 65 | 66 | v := new(TagInfo) 67 | resp, err := s.client.Do(req, v) 68 | if err != nil { 69 | return nil, resp, err 70 | } 71 | 72 | return v, resp, err 73 | } 74 | 75 | // CreateTag create a tag of a project 76 | // 77 | // Gerrit API docs:https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-tag 78 | func (s *ProjectsService) CreateTag(ctx context.Context, projectName, tagName string, input *TagInput) (*TagInfo, *Response, error) { 79 | u := fmt.Sprintf("projects/%s/tags/%s", url.QueryEscape(projectName), url.QueryEscape(tagName)) 80 | 81 | req, err := s.client.NewRequest(ctx, "PUT", u, input) 82 | if err != nil { 83 | return nil, nil, err 84 | } 85 | 86 | v := new(TagInfo) 87 | resp, err := s.client.Do(req, v) 88 | if err != nil { 89 | return nil, resp, err 90 | } 91 | 92 | return v, resp, err 93 | } 94 | 95 | // DeleteTag delete a tag of a project 96 | // 97 | // Gerrit API docs:https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#delete-tag 98 | func (s *ProjectsService) DeleteTag(ctx context.Context, projectName, tagName string) (*Response, error) { 99 | u := fmt.Sprintf("projects/%s/tags/%s", url.QueryEscape(projectName), url.QueryEscape(tagName)) 100 | 101 | req, err := s.client.NewRequest(ctx, "DELETE", u, nil) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | resp, err := s.client.Do(req, nil) 107 | 108 | return resp, err 109 | } 110 | 111 | // DeleteTags delete tags of a project 112 | // 113 | // Gerrit API docs:https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#delete-tags 114 | func (s *ProjectsService) DeleteTags(ctx context.Context, projectName string, input *DeleteTagsInput) (*Response, error) { 115 | u := fmt.Sprintf("projects/%s/tags:delete", url.QueryEscape(projectName)) 116 | 117 | req, err := s.client.NewRequest(ctx, "POST", u, input) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | resp, err := s.client.Do(req, nil) 123 | 124 | return resp, err 125 | } 126 | -------------------------------------------------------------------------------- /projects_tag_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/andygrunwald/go-gerrit" 12 | ) 13 | 14 | // +func (s *ProjectsService) CreateTag(projectName, tagName string, input *TagInput) (*TagInfo, *Response, error) 15 | func TestProjectsService_CreateTag(t *testing.T) { 16 | setup() 17 | defer teardown() 18 | 19 | input := &gerrit.TagInput{ 20 | Ref: "v1.0.0", 21 | Revision: "master", 22 | Message: "v1.0.0 release", 23 | } 24 | 25 | testMux.HandleFunc("/projects/go/tags/v1.0.0", func(w http.ResponseWriter, r *http.Request) { 26 | testMethod(t, r, "PUT") 27 | 28 | v := new(gerrit.TagInput) 29 | if err := json.NewDecoder(r.Body).Decode(v); err != nil { 30 | t.Error(err) 31 | } 32 | 33 | if !reflect.DeepEqual(v, input) { 34 | t.Errorf("Request body = %+v, want %+v", v, input) 35 | } 36 | 37 | fmt.Fprint(w, `)]}'`+"\n"+`{"ref":"v1.0.0","revision":"master","message":"v1.0.0 release"}`) 38 | }) 39 | 40 | tag, _, err := testClient.Projects.CreateTag(context.Background(), "go", "v1.0.0", input) 41 | if err != nil { 42 | t.Errorf("Projects.CreateTag returned error: %v", err) 43 | } 44 | 45 | want := &gerrit.TagInfo{ 46 | Ref: "v1.0.0", 47 | Revision: "master", 48 | Message: "v1.0.0 release", 49 | } 50 | 51 | if !reflect.DeepEqual(tag, want) { 52 | t.Errorf("Projects.CreateTag returned %+v, want %+v", tag, want) 53 | } 54 | } 55 | 56 | func TestProjectsService_DeleteTag(t *testing.T) { 57 | setup() 58 | defer teardown() 59 | 60 | testMux.HandleFunc("/projects/go/tags/v1.0.0", func(w http.ResponseWriter, r *http.Request) { 61 | testMethod(t, r, "DELETE") 62 | w.WriteHeader(http.StatusNoContent) 63 | }) 64 | 65 | _, err := testClient.Projects.DeleteTag(context.Background(), "go", "v1.0.0") 66 | if err != nil { 67 | t.Errorf("Projects.DeleteTag returned error: %v", err) 68 | } 69 | } 70 | 71 | func TestProjectsService_DeleteTags(t *testing.T) { 72 | setup() 73 | defer teardown() 74 | input := &gerrit.DeleteTagsInput{ 75 | Tags: []string{"v1.0.0", "v1.1.0"}, 76 | } 77 | testMux.HandleFunc("/projects/go/tags:delete", func(w http.ResponseWriter, r *http.Request) { 78 | testMethod(t, r, "POST") 79 | v := new(gerrit.DeleteTagsInput) 80 | if err := json.NewDecoder(r.Body).Decode(v); err != nil { 81 | t.Error(err) 82 | } 83 | 84 | if !reflect.DeepEqual(v, input) { 85 | t.Errorf("Request body = %+v, want %+v", v, input) 86 | } 87 | 88 | w.WriteHeader(http.StatusNoContent) 89 | }) 90 | 91 | _, err := testClient.Projects.DeleteTags(context.Background(), "go", input) 92 | if err != nil { 93 | t.Errorf("Projects.DeleteTags returned error: %v", err) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /projects_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/andygrunwald/go-gerrit" 12 | ) 13 | 14 | func TestProjectsService_ListProjects(t *testing.T) { 15 | setup() 16 | defer teardown() 17 | 18 | testMux.HandleFunc("/projects/", func(w http.ResponseWriter, r *http.Request) { 19 | testMethod(t, r, "GET") 20 | testFormValues(t, r, testValues{ 21 | "r": "(arch|benchmarks)", 22 | "n": "2", 23 | }) 24 | 25 | fmt.Fprint(w, `)]}'`+"\n"+`{"arch":{"id":"arch","state":"ACTIVE"},"benchmarks":{"id":"benchmarks","state":"ACTIVE"}}`) 26 | }) 27 | 28 | opt := &gerrit.ProjectOptions{ 29 | Regex: "(arch|benchmarks)", 30 | } 31 | opt.Limit = 2 32 | project, _, err := testClient.Projects.ListProjects(context.Background(), opt) 33 | if err != nil { 34 | t.Errorf("Projects.ListProjects returned error: %v", err) 35 | } 36 | 37 | want := &map[string]gerrit.ProjectInfo{ 38 | "arch": { 39 | ID: "arch", 40 | State: "ACTIVE", 41 | }, 42 | "benchmarks": { 43 | ID: "benchmarks", 44 | State: "ACTIVE", 45 | }, 46 | } 47 | 48 | if !reflect.DeepEqual(project, want) { 49 | t.Errorf("Projects.ListProjects returned %+v, want %+v", project, want) 50 | } 51 | } 52 | 53 | func TestProjectsService_GetProject(t *testing.T) { 54 | setup() 55 | defer teardown() 56 | 57 | testMux.HandleFunc("/projects/go/", func(w http.ResponseWriter, r *http.Request) { 58 | testMethod(t, r, "GET") 59 | 60 | fmt.Fprint(w, `)]}'`+"\n"+`{"id":"go","name":"go","parent":"All-Projects","description":"The Go Programming Language","state":"ACTIVE"}`) 61 | }) 62 | 63 | project, _, err := testClient.Projects.GetProject(context.Background(), "go") 64 | if err != nil { 65 | t.Errorf("Projects.GetProject returned error: %v", err) 66 | } 67 | 68 | want := &gerrit.ProjectInfo{ 69 | ID: "go", 70 | Name: "go", 71 | Parent: "All-Projects", 72 | Description: "The Go Programming Language", 73 | State: "ACTIVE", 74 | } 75 | 76 | if !reflect.DeepEqual(project, want) { 77 | t.Errorf("Projects.GetProject returned %+v, want %+v", project, want) 78 | } 79 | } 80 | 81 | func TestProjectsService_GetProject_WithSlash(t *testing.T) { 82 | setup() 83 | defer teardown() 84 | 85 | testMux.HandleFunc("/projects/plugins/delete-project", func(w http.ResponseWriter, r *http.Request) { 86 | testMethod(t, r, "GET") 87 | testRequestURL(t, r, "/projects/plugins%2Fdelete-project") 88 | 89 | c := `)]}'` + "\n" + `{"id":"plugins%2Fdelete-project","name":"plugins/delete-project","parent":"Public-Plugins","description":"A plugin which allows projects to be deleted from Gerrit via an SSH command","state":"ACTIVE"}` 90 | fmt.Fprint(w, c) 91 | }) 92 | project, _, err := testClient.Projects.GetProject(context.Background(), "plugins/delete-project") 93 | if err != nil { 94 | t.Errorf("Projects.GetProject returned error: %v", err) 95 | } 96 | 97 | want := &gerrit.ProjectInfo{ 98 | ID: "plugins%2Fdelete-project", 99 | Name: "plugins/delete-project", 100 | Parent: "Public-Plugins", 101 | Description: "A plugin which allows projects to be deleted from Gerrit via an SSH command", 102 | State: "ACTIVE", 103 | } 104 | 105 | if !reflect.DeepEqual(project, want) { 106 | t.Errorf("Projects.GetProject returned %+v, want %+v", project, want) 107 | } 108 | } 109 | 110 | // +func (s *ProjectsService) CreateProject(name string, input *ProjectInput) (*ProjectInfo, *Response, error) { 111 | func TestProjectsService_CreateProject(t *testing.T) { 112 | setup() 113 | defer teardown() 114 | 115 | input := &gerrit.ProjectInput{ 116 | Description: "The Go Programming Language", 117 | } 118 | 119 | testMux.HandleFunc("/projects/go/", func(w http.ResponseWriter, r *http.Request) { 120 | testMethod(t, r, "PUT") 121 | 122 | v := new(gerrit.ProjectInput) 123 | if err := json.NewDecoder(r.Body).Decode(v); err != nil { 124 | t.Error(err) 125 | } 126 | 127 | if !reflect.DeepEqual(v, input) { 128 | t.Errorf("Request body = %+v, want %+v", v, input) 129 | } 130 | 131 | fmt.Fprint(w, `)]}'`+"\n"+`{"id":"go","name":"go","parent":"All-Projects","description":"The Go Programming Language"}`) 132 | }) 133 | 134 | project, _, err := testClient.Projects.CreateProject(context.Background(), "go", input) 135 | if err != nil { 136 | t.Errorf("Projects.CreateProject returned error: %v", err) 137 | } 138 | 139 | want := &gerrit.ProjectInfo{ 140 | ID: "go", 141 | Name: "go", 142 | Parent: "All-Projects", 143 | Description: "The Go Programming Language", 144 | } 145 | 146 | if !reflect.DeepEqual(project, want) { 147 | t.Errorf("Projects.CreateProject returned %+v, want %+v", project, want) 148 | } 149 | } 150 | 151 | // +func (s *ProjectsService) GetProjectDescription(name string) (*string, *Response, error) { 152 | func TestProjectsService_GetProjectDescription(t *testing.T) { 153 | setup() 154 | defer teardown() 155 | 156 | testMux.HandleFunc("/projects/go/description/", func(w http.ResponseWriter, r *http.Request) { 157 | testMethod(t, r, "GET") 158 | 159 | fmt.Fprint(w, `)]}'`+"\n"+`"The Go Programming Language"`) 160 | }) 161 | 162 | description, _, err := testClient.Projects.GetProjectDescription(context.Background(), "go") 163 | if err != nil { 164 | t.Errorf("Projects.GetProjectDescription returned error: %v", err) 165 | } 166 | 167 | want := "The Go Programming Language" 168 | 169 | if !reflect.DeepEqual(description, want) { 170 | t.Errorf("Projects.GetProjectDescription returned %+v, want %+v", description, want) 171 | } 172 | } 173 | 174 | func ExampleProjectsService_ListProjects() { 175 | instance := "https://chromium-review.googlesource.com/" 176 | ctx := context.Background() 177 | client, err := gerrit.NewClient(ctx, instance, nil) 178 | if err != nil { 179 | panic(err) 180 | } 181 | 182 | opt := &gerrit.ProjectOptions{ 183 | Description: true, 184 | Prefix: "infra/infra/infra_l", 185 | } 186 | projects, _, err := client.Projects.ListProjects(ctx, opt) 187 | if err != nil { 188 | panic(err) 189 | } 190 | 191 | for name, p := range *projects { 192 | fmt.Printf("%s - State: %s\n", name, p.State) 193 | } 194 | 195 | // Output: 196 | // infra/infra/infra_libs - State: ACTIVE 197 | } 198 | 199 | func TestProjectsService_GetBranch(t *testing.T) { 200 | setup() 201 | defer teardown() 202 | 203 | existBranches := map[string]*gerrit.BranchInfo{ 204 | "branch": { 205 | Ref: "123", 206 | Revision: "abcd1234", 207 | CanDelete: true, 208 | }, 209 | "branch/foo": { 210 | Ref: "456", 211 | Revision: "deadbeef", 212 | CanDelete: false, 213 | }, 214 | } 215 | 216 | testMux.HandleFunc("/projects/go/branches/", func(w http.ResponseWriter, r *http.Request) { 217 | testMethod(t, r, "GET") 218 | 219 | branchName := r.URL.Path[len("/projects/go/branches/"):] 220 | 221 | branchInfo, ok := existBranches[branchName] 222 | if !ok { 223 | http.Error(w, branchName, http.StatusBadRequest) 224 | } 225 | 226 | branchInfoRaw, err := json.Marshal(&branchInfo) 227 | if err != nil { 228 | http.Error(w, branchName, http.StatusBadRequest) 229 | } 230 | 231 | fmt.Fprint(w, `)]}'`+"\n"+string(branchInfoRaw)) 232 | }) 233 | 234 | var tests = []struct { 235 | name string 236 | branch string 237 | expected *gerrit.BranchInfo 238 | }{ 239 | { 240 | name: "branch without slash", 241 | branch: "branch", 242 | expected: &gerrit.BranchInfo{ 243 | Ref: "123", 244 | Revision: "abcd1234", 245 | CanDelete: true, 246 | }, 247 | }, 248 | { 249 | name: "branch with slash", 250 | branch: "branch/foo", 251 | expected: &gerrit.BranchInfo{ 252 | Ref: "456", 253 | Revision: "deadbeef", 254 | CanDelete: false, 255 | }, 256 | }, 257 | } 258 | 259 | for _, tc := range tests { 260 | branchInfo, _, err := testClient.Projects.GetBranch(context.Background(), "go", tc.branch) 261 | if err != nil { 262 | t.Errorf("tc %s: Projects.GetProject returned error: %v", tc.name, err) 263 | } 264 | 265 | if !reflect.DeepEqual(branchInfo, tc.expected) { 266 | t.Errorf("tc %s: Projects.GetBranch returned %+v, want %+v", tc.name, branchInfo, tc.expected) 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package gerrit 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | // Timestamp represents an instant in time with nanosecond precision, in UTC time zone. 11 | // It encodes to and from JSON in Gerrit's timestamp format. 12 | // All exported methods of time.Time can be called on Timestamp. 13 | // 14 | // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#timestamp 15 | type Timestamp struct { 16 | // Time is an instant in time. Its time zone must be UTC. 17 | time.Time 18 | } 19 | 20 | // MarshalJSON implements the json.Marshaler interface. 21 | // The time is a quoted string in Gerrit's timestamp format. 22 | // An error is returned if t.Time time zone is not UTC. 23 | func (t Timestamp) MarshalJSON() ([]byte, error) { 24 | if t.Location() != time.UTC { 25 | return nil, errors.New("Timestamp.MarshalJSON: time zone must be UTC") 26 | } 27 | if y := t.Year(); y < 0 || 9999 < y { 28 | // RFC 3339 is clear that years are 4 digits exactly. 29 | // See golang.org/issue/4556#issuecomment-66073163 for more discussion. 30 | return nil, errors.New("Timestamp.MarshalJSON: year outside of range [0,9999]") 31 | } 32 | b := make([]byte, 0, len(timeLayout)+2) 33 | b = append(b, '"') 34 | b = t.AppendFormat(b, timeLayout) 35 | b = append(b, '"') 36 | return b, nil 37 | } 38 | 39 | // UnmarshalJSON implements the json.Unmarshaler interface. 40 | // The time is expected to be a quoted string in Gerrit's timestamp format. 41 | func (t *Timestamp) UnmarshalJSON(b []byte) error { 42 | // Ignore null, like in the main JSON package. 43 | if string(b) == "null" { 44 | return nil 45 | } 46 | var err error 47 | t.Time, err = time.Parse(`"`+timeLayout+`"`, string(b)) 48 | return err 49 | } 50 | 51 | // Gerrit's timestamp layout is like time.RFC3339Nano, but with a space instead 52 | // of the "T", without a timezone (it's always in UTC), and always includes nanoseconds. 53 | // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#timestamp. 54 | const timeLayout = "2006-01-02 15:04:05.000000000" 55 | 56 | // Number is a string representing a number. This type is only used in cases 57 | // where the API being queried may return an inconsistent result. 58 | type Number string 59 | 60 | // String returns the string representing the current number. 61 | func (n *Number) String() string { 62 | return string(*n) 63 | } 64 | 65 | // Int returns the current number as an integer 66 | func (n *Number) Int() (int, error) { 67 | return strconv.Atoi(n.String()) 68 | } 69 | 70 | // UnmarshalJSON will marshal the provided data into the current *Number struct. 71 | func (n *Number) UnmarshalJSON(data []byte) error { 72 | // `data` is a number represented as a string (ex. "5"). 73 | var stringNumber string 74 | if err := json.Unmarshal(data, &stringNumber); err == nil { 75 | *n = Number(stringNumber) 76 | return nil 77 | } 78 | 79 | // `data` is a number represented as an integer (ex. 5). Here 80 | // we're using json.Unmarshal to convert bytes -> number which 81 | // we then convert to our own Number type. 82 | var number int 83 | if err := json.Unmarshal(data, &number); err == nil { 84 | *n = Number(strconv.Itoa(number)) 85 | return nil 86 | } 87 | return errors.New("cannot convert data to number") 88 | } 89 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package gerrit_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/andygrunwald/go-gerrit" 11 | ) 12 | 13 | func TestTimestamp(t *testing.T) { 14 | const jsonData = `{ 15 | "subject": "net/http: write status code in Redirect when Content-Type header set", 16 | "created": "2018-05-04 17:24:39.000000000", 17 | "updated": "0001-01-01 00:00:00.000000000", 18 | "submitted": "2018-05-04 18:01:10.000000000", 19 | "_number": 111517 20 | } 21 | ` 22 | type ChangeInfo struct { 23 | Subject string `json:"subject"` 24 | Created gerrit.Timestamp `json:"created"` 25 | Updated gerrit.Timestamp `json:"updated"` 26 | Submitted *gerrit.Timestamp `json:"submitted,omitempty"` 27 | Omitted *gerrit.Timestamp `json:"omitted,omitempty"` 28 | Number int `json:"_number"` 29 | } 30 | ci := ChangeInfo{ 31 | Subject: "net/http: write status code in Redirect when Content-Type header set", 32 | Created: gerrit.Timestamp{Time: time.Date(2018, 5, 4, 17, 24, 39, 0, time.UTC)}, 33 | Updated: gerrit.Timestamp{}, 34 | Submitted: &gerrit.Timestamp{Time: time.Date(2018, 5, 4, 18, 1, 10, 0, time.UTC)}, 35 | Omitted: nil, 36 | Number: 111517, 37 | } 38 | 39 | // Try decoding JSON data into a ChangeInfo struct. 40 | var v ChangeInfo 41 | err := json.Unmarshal([]byte(jsonData), &v) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if got, want := v, ci; !reflect.DeepEqual(got, want) { 46 | t.Errorf("decoding JSON data into a ChangeInfo struct:\ngot:\n%v\nwant:\n%v", got, want) 47 | } 48 | 49 | // Try encoding a ChangeInfo struct into JSON data. 50 | var buf bytes.Buffer 51 | e := json.NewEncoder(&buf) 52 | e.SetIndent("", "\t") 53 | err = e.Encode(ci) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | if got, want := buf.String(), jsonData; got != want { 58 | t.Errorf("encoding a ChangeInfo struct into JSON data:\ngot:\n%v\nwant:\n%v", got, want) 59 | } 60 | } 61 | 62 | func TestTypesNumber_String(t *testing.T) { 63 | number := gerrit.Number("7") 64 | if number.String() != "7" { 65 | t.Fatalf("%s != 7", number.String()) 66 | } 67 | } 68 | 69 | func TestTypesNumber_Int(t *testing.T) { 70 | number := gerrit.Number("7") 71 | integer, err := number.Int() 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | if integer != 7 { 76 | t.Fatalf("%d != 7", integer) 77 | } 78 | } 79 | 80 | func TestTypesNumber_UnmarshalJSON_String(t *testing.T) { 81 | var number gerrit.Number 82 | if err := json.Unmarshal([]byte(`"7"`), &number); err != nil { 83 | t.Fatal(err) 84 | } 85 | if number.String() != "7" { 86 | t.Fatalf("%s != 7", number.String()) 87 | } 88 | } 89 | 90 | func TestTypesNumber_UnmarshalJSON_Int(t *testing.T) { 91 | var number gerrit.Number 92 | if err := json.Unmarshal([]byte("7"), &number); err != nil { 93 | t.Fatal(err) 94 | } 95 | integer, err := number.Int() 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | if integer != 7 { 100 | t.Fatalf("%d != 7", integer) 101 | } 102 | } 103 | --------------------------------------------------------------------------------