Compare commits

...

263 Commits

Author SHA1 Message Date
Liam Newman
349ef7a54c [maven-release-plugin] prepare release github-api-1.116 2020-08-12 18:47:42 -07:00
Liam Newman
94df5fc389 Merge pull request #923 from bitwiseman/feature/is-template
Added GHRepository.isTemplate() method
2020-08-12 18:43:07 -07:00
Liam Newman
906238a297 Add GHRepository.isTemplate() 2020-08-12 18:28:18 -07:00
Liam Newman
7963fa82b5 Ensure withPreview can be called multiple times 2020-08-12 18:27:48 -07:00
Liam Newman
1aba6012fb Centralize GHRepository read 2020-08-12 18:21:44 -07:00
Liam Newman
ff4324ac67 Merge pull request #897 from bonnie-young/add-create-repo-with-template-support
add create repo with template support
2020-08-12 08:43:17 -07:00
Yang Ting
11bc669e1d update code for create repository from template
Signed-off-by: Yang Ting <bonnie.young@maxwit.com>
2020-08-12 22:08:31 +08:00
Liam Newman
dcf26d58e4 Merge pull request #919 from bitwiseman/task/one-more-date
Fixed and streamlined date parsing
2020-08-07 16:42:49 -07:00
Liam Newman
4d46872c35 Fixed and streamlined date parsing
Fixes #917
2020-08-07 16:36:23 -07:00
Liam Newman
4f0d62f421 Merge pull request #920 from ewiegs4/deployment-payload
Support for object deployment payloads
2020-08-07 16:35:54 -07:00
Eddie Wiegers
f7ad1f517b formatting 2020-08-07 17:46:33 -05:00
Eddie Wiegers
345d6197f3 Support for object deployment payloads. 2020-08-07 17:36:28 -05:00
Liam Newman
bb4d44138a Merge branch 'master' into add-create-repo-with-template-support 2020-08-04 16:05:02 -07:00
Liam Newman
a8ef0cde53 Merge pull request #913 from hub4j/dependabot/maven/spotbugs.version-4.1.1
Chore(deps): Bump spotbugs.version from 4.0.6 to 4.1.1
2020-08-03 10:58:51 -07:00
dependabot[bot]
77dc009c95 Chore(deps): Bump spotbugs.version from 4.0.6 to 4.1.1
Bumps `spotbugs.version` from 4.0.6 to 4.1.1.

Updates `spotbugs` from 4.0.6 to 4.1.1
- [Release notes](https://github.com/spotbugs/spotbugs/releases)
- [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spotbugs/spotbugs/compare/4.0.6...4.1.1)

Updates `spotbugs-annotations` from 4.0.6 to 4.1.1
- [Release notes](https://github.com/spotbugs/spotbugs/releases)
- [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spotbugs/spotbugs/compare/4.0.6...4.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-03 02:00:46 +00:00
Liam Newman
aa298c93cc Merge pull request #911 from bitwiseman/task/workflow-run
Add GHEvent.WORKFLOW_RUN
2020-08-02 08:27:02 -07:00
Liam Newman
dfb0a5240e Merge pull request #909 from JKalash/jkalash/master
GHCheckRun.getPullRequests public
2020-07-31 16:06:17 -07:00
Liam Newman
9cfc3c22b5 Add GHEvent.WORKFLOW_RUN 2020-07-31 16:03:34 -07:00
Liam Newman
b177d98e29 Populate Check pull requests
Turns out there were quite a few bugs in this area (it was exposed publicly before. Some
tweaking was needed and updates to the tests to show this working as expected.
2020-07-31 15:57:17 -07:00
Joe
5405fb0370 return unmodifiable list 2020-07-30 21:48:42 +01:00
Joe
72a1c24b3b format 2020-07-30 21:43:37 +01:00
Joe
f146ae94ec return iterable 2020-07-30 21:41:16 +01:00
Joe
a0bbba748a check suite exposes pull requests 2020-07-30 20:10:44 +01:00
Joe
81bf818573 pullRequests returns iterable 2020-07-30 20:01:44 +01:00
Joe
d5913dc292 fix array exposure 2020-07-30 15:25:35 +01:00
Joe Kalash
e1e901b794 Merge branch 'master' into jkalash/master 2020-07-30 14:01:01 +01:00
Joe
2f2f26767e make getPullRequests public 2020-07-30 13:59:36 +01:00
Liam Newman
bffa78c1b8 Merge pull request #907 from hub4j/dependabot/maven/org.mockito-mockito-core-3.4.6
Chore(deps-dev): Bump mockito-core from 3.4.4 to 3.4.6
2020-07-29 20:11:55 -07:00
dependabot[bot]
c55719c67a Chore(deps-dev): Bump mockito-core from 3.4.4 to 3.4.6
Bumps [mockito-core](https://github.com/mockito/mockito) from 3.4.4 to 3.4.6.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.4.4...v3.4.6)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-30 02:00:49 +00:00
Liam Newman
cb3b4a6642 Merge pull request #906 from JKalash/jkalash/master
Merge arbitrary branches
2020-07-28 16:37:40 -07:00
Liam Newman
92c141cee6 Add support for string head
Also support all changes already merged.
2020-07-28 12:57:31 -07:00
Liam Newman
fd1a1a1c23 Simplified tests 2020-07-28 11:22:07 -07:00
Joe
b835884b2e fix test 2020-07-28 14:37:50 +01:00
Joe
660763908d test refactor 2020-07-28 14:24:35 +01:00
Joe
fe8bdb755a Merge branches method 2020-07-28 12:41:31 +01:00
Liam Newman
67dc6d2d23 Merge pull request #904 from Javaru/add-missing-org-permissions
Added MAINTAIN and TRIAGE to GHOrganization.Permission enum
2020-07-27 21:27:27 -07:00
Mark Vedder
9c8d73cbe2 Merge branch 'master' into add-missing-org-permissions 2020-07-27 18:54:32 -04:00
Liam Newman
5db97d92dd Merge pull request #899 from gastaldi/set_milestone
PullRequest.setMilestone should use the Issue API
2020-07-27 14:18:05 -07:00
Liam Newman
ac470dddb5 Merge branch 'master' into add-create-repo-with-template-support 2020-07-27 14:14:16 -07:00
Liam Newman
43063fe8ce Minor tweaks 2020-07-27 14:12:15 -07:00
George Gastaldi
59e0046c1e Validate argument in GitHub.getRepository
This avoids the ArrayIndexOutOfBoundsException when the argument format is invalid
2020-07-27 17:47:56 -03:00
George Gastaldi
36ab05c265 PullRequest.setMilestone should use the Issue API 2020-07-27 17:47:56 -03:00
Liam Newman
2b2be05dae Merge pull request #900 from gastaldi/repo_fix
Fixes modifyCollaborators for multiple users
2020-07-27 13:23:06 -07:00
Liam Newman
fb1adbd1ef Make withUrlPath() overwrite instead of append
I had some ideas about having multiple calls to  apend to build up paths,
but it turns out that idea is pretty bad.  `with*()` methods should
overwrite when called for the same field.

If we want to create and , we can do that later.
2020-07-27 13:12:46 -07:00
Javaru
ab68a59b25 Added MAINTAIN and TRIAGE to GHOrganization.Permission enum 2020-07-27 15:50:32 -04:00
George Gastaldi
9c7de767e9 Fixes modifyCollaborators for multiple users
Fixes #868
2020-07-27 12:37:34 -07:00
George Gastaldi
8ba5cf7c2e Formatting tweaks and fixes 2020-07-27 12:31:09 -07:00
Bonnie Young
b194a19b98 Merge branch 'master' into add-create-repo-with-template-support 2020-07-21 00:32:52 +08:00
Liam Newman
1d344b016f Merge pull request #898 from hub4j/dependabot/maven/org.mockito-mockito-core-3.4.4
Chore(deps-dev): Bump mockito-core from 3.4.2 to 3.4.4
2020-07-20 00:35:58 -07:00
dependabot[bot]
474f3ef4ca Chore(deps-dev): Bump mockito-core from 3.4.2 to 3.4.4
Bumps [mockito-core](https://github.com/mockito/mockito) from 3.4.2 to 3.4.4.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.4.2...v3.4.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 02:00:54 +00:00
Yang Ting
9830927020 add create repo with template support
Signed-off-by: Yang Ting <bonnie.young@maxwit.com>
2020-07-19 23:07:19 +08:00
Liam Newman
727932a442 Merge pull request #895 from jeetchoudhary/protection
GHBranch getProtection method to return correct number of reviewers #890
2020-07-17 12:27:05 -07:00
jeetchoudhary
cd92b51845 Merge branch 'master' into protection 2020-07-17 23:59:51 +05:30
Liam Newman
fe26d16411 Update test and data to verify getProtection() code path
The create and get code paths are different.  Both need testing.
2020-07-17 10:50:54 -07:00
Liam Newman
d68c66ce2b Merge pull request #893 from hub4j/dependabot/maven/org.mockito-mockito-core-3.4.2
Chore(deps-dev): Bump mockito-core from 3.4.0 to 3.4.2
2020-07-17 09:59:31 -07:00
Jitender kumar
e7bfbfb48f reformat class 2020-07-17 17:28:15 +05:30
jeetchoudhary
f2a88ae61c fixing getRequiredReviewers to return count #890 2020-07-17 16:55:51 +05:30
jeetchoudhary
e2113f6ee5 Update rest request for test case 2020-07-17 16:52:17 +05:30
dependabot[bot]
3867224024 Chore(deps-dev): Bump mockito-core from 3.4.0 to 3.4.2
Bumps [mockito-core](https://github.com/mockito/mockito) from 3.4.0 to 3.4.2.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.4.0...v3.4.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-17 02:01:30 +00:00
Liam Newman
9ee0bf43bc [maven-release-plugin] prepare for next development iteration 2020-07-16 15:16:56 -07:00
Liam Newman
2844542efa [maven-release-plugin] prepare release github-api-1.115 2020-07-16 15:16:47 -07:00
Liam Newman
e3fcae9392 Revert "Remove Java 15-ea"
This reverts commit c6ccfa91f3.
2020-07-15 15:32:46 -07:00
Liam Newman
c6ccfa91f3 Remove Java 15-ea
It is unclear exactly why this started failing but we're not interested figuring this out.
2020-07-15 15:19:06 -07:00
Liam Newman
b6fcee1cb9 Rollback to surefire 2.22.2 2020-07-15 15:14:16 -07:00
Liam Newman
9071befb04 Run slow or flaky tests in test phase 2020-07-15 15:05:26 -07:00
Liam Newman
bdd5fe98f3 Use file to specify slow-or-flaky-tests list 2020-07-15 15:00:03 -07:00
Liam Newman
a3d3e83a49 Move slow or flaky tests to separate surefire execution
This lets us have most tests run immediately and with no retries and then have slower tests that may be flaky run later with retries.
2020-07-15 14:45:32 -07:00
Liam Newman
08bde72028 Merge pull request #849 from alexanderkjall/add-support-for-child-teams
Added support for fetching what teams are part of this team.
2020-07-15 14:05:47 -07:00
Liam Newman
108a136368 Remove getChildTeams and add test for no children 2020-07-15 13:46:11 -07:00
Liam Newman
57d87ad6b1 Merge branch 'master' into add-support-for-child-teams 2020-07-15 13:28:32 -07:00
Liam Newman
0c22815ff7 Merge pull request #872 from MarcosCela/doc/org-level-resources
Add documentation for organization-level resources
2020-07-15 13:27:05 -07:00
Liam Newman
0ca792ecfd Merge pull request #830 from bitwiseman/task/rate-limit/full
Handle header and endpoint rate limit responses consistently
2020-07-15 12:42:11 -07:00
Liam Newman
987c34c69e Merge branch 'master' into task/rate-limit/full 2020-07-15 12:13:16 -07:00
Liam Newman
c1c02bc8ab Merge pull request #887 from hub4j/dependabot/maven/org.mockito-mockito-core-3.4.0
Chore(deps-dev): Bump mockito-core from 3.3.3 to 3.4.0
2020-07-14 12:02:54 -07:00
dependabot[bot]
4ee369f27c Chore(deps-dev): Bump mockito-core from 3.3.3 to 3.4.0
Bumps [mockito-core](https://github.com/mockito/mockito) from 3.3.3 to 3.4.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.3.3...v3.4.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-13 15:52:33 +00:00
Liam Newman
c9012efdcb Merge pull request #886 from hub4j/dependabot/maven/net.revelc.code.formatter-formatter-maven-plugin-2.12.1
Chore(deps): Bump formatter-maven-plugin from 2.12.0 to 2.12.1
2020-07-13 08:51:51 -07:00
dependabot[bot]
41524fc67d Chore(deps): Bump formatter-maven-plugin from 2.12.0 to 2.12.1
Bumps [formatter-maven-plugin](https://github.com/revelc/formatter-maven-plugin) from 2.12.0 to 2.12.1.
- [Release notes](https://github.com/revelc/formatter-maven-plugin/releases)
- [Changelog](https://github.com/revelc/formatter-maven-plugin/blob/formatter-maven-plugin-2.12.1/CHANGELOG.md)
- [Commits](https://github.com/revelc/formatter-maven-plugin/compare/formatter-maven-plugin-2.12.0...formatter-maven-plugin-2.12.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-13 02:00:47 +00:00
Liam Newman
04ff61e981 Merge pull request #885 from XiongKezhi/add-missing-conclusion
Add the missing SKIPPED check run conclusion
2020-07-12 14:11:56 -07:00
Liam Newman
532468dc67 Update src/main/java/org/kohsuke/github/GHCheckRun.java 2020-07-12 13:41:57 -07:00
Kezhi Xiong
9c9a2dae47 Add JavaDoc for check run conclusion 2020-07-12 15:58:34 +08:00
Kezhi Xiong
c8a868b57f Add the missing SKIPPED check run conclusion 2020-07-12 15:54:09 +08:00
Liam Newman
4b3f81ee34 Clean up comments and javadoc 2020-07-10 10:51:20 -07:00
Liam Newman
afa170ba7c Add more tests for rate limit record selection 2020-07-10 09:01:29 -07:00
Liam Newman
46e3b2272e Clean up and reorganize changes
Changed GitHubRateLimitSpecifier to RateLimitTarget
Made RateLimitTarget public so it can be passed to GitHubBuilder
2020-07-10 09:01:29 -07:00
Liam Newman
52472e90ec Simplified rate limit record selection
Here we have another example of trying to do something clever when simplicity is the better choice.
Rather than trying to guess the rate limit record for a request based on the url path,
I added an enumeration which can be set on the request to say which rate limit record to applies.

This is simpler, safer, and faster than trying to guess the rate limit from the url path.
2020-07-10 09:01:29 -07:00
Liam Newman
4ef0d00846 Integrate full rate limit checking 2020-07-10 09:01:29 -07:00
Liam Newman
580f2537f2 Move GHRef readers to GHRef class 2020-07-09 15:00:40 -07:00
Liam Newman
3d9fd96026 Merge pull request #884 from bitwiseman/task/gitbucket
Workaround for GitBucket refs issue
2020-07-09 13:38:55 -07:00
Liam Newman
f449b92721 Workaround for GitBucket refs issue
The bug that caused this has been fixed by GitBucket but may not be released for some time.
It was a change to this library that broke them, so to be nice we're fully working around it here.

That said, the GET /refs endpoint is deprecated and will be going away at some point.
We will be making a breaking change in this area again in the next few months. We'll need to raise
it to GitBucket to make sure they handle it proproperly.
2020-07-09 09:50:29 -07:00
Liam Newman
3b0216b023 Merge pull request #877 from hub4j/dependabot/maven/com.github.tomakehurst-wiremock-jre8-standalone-2.27.1
Chore(deps-dev): Bump wiremock-jre8-standalone from 2.26.3 to 2.27.1
2020-07-07 12:40:23 -07:00
Liam Newman
98cf839737 Add new methods 2020-07-07 12:29:59 -07:00
dependabot[bot]
0bb0846505 Chore(deps-dev): Bump wiremock-jre8-standalone from 2.26.3 to 2.27.1
Bumps [wiremock-jre8-standalone](https://github.com/tomakehurst/wiremock) from 2.26.3 to 2.27.1.
- [Release notes](https://github.com/tomakehurst/wiremock/releases)
- [Commits](https://github.com/tomakehurst/wiremock/compare/2.26.3...2.27.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-07 19:23:07 +00:00
Liam Newman
70969400a3 Merge pull request #876 from jtnord/symlink-support
Add minimal symlink support via GHContent.getTarget()
2020-07-07 12:21:39 -07:00
Liam Newman
147e8d5d12 Merge branch 'master' into symlink-support 2020-07-07 11:38:14 -07:00
Liam Newman
cacc3e6edd Update formatter cache directory 2020-07-07 10:15:00 -07:00
Liam Newman
a284eca147 Shorten file paths for windows 2020-07-07 09:48:31 -07:00
James Nord
0d3ba9d7f0 formatting fixes 2020-07-07 13:29:33 +01:00
Liam Newman
be8064d642 Merge pull request #879 from hub4j/dependabot/maven/org.codehaus.mojo-animal-sniffer-maven-plugin-1.19
Chore(deps): Bump animal-sniffer-maven-plugin from 1.18 to 1.19
2020-07-06 10:15:33 -07:00
dependabot[bot]
e30dba742d Chore(deps): Bump animal-sniffer-maven-plugin from 1.18 to 1.19
Bumps [animal-sniffer-maven-plugin](https://github.com/mojohaus/animal-sniffer) from 1.18 to 1.19.
- [Release notes](https://github.com/mojohaus/animal-sniffer/releases)
- [Commits](https://github.com/mojohaus/animal-sniffer/compare/animal-sniffer-parent-1.18...animal-sniffer-parent-1.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-06 02:01:00 +00:00
James Nord
44b72ed647 Add initial symlink support.
this adds rudimentary symlink support (just enough to get a symlink).

still todo, howto handle GHRepository.getFileContent where the path
contains a symlink within the repository
2020-07-02 17:18:20 +01:00
Liam Newman
666bd77dac Merge pull request #874 from hub4j/bitwiseman-patch-1
Add "workflow_dispatch" to GHEvent
2020-07-02 08:52:31 -07:00
Liam Newman
0a6613e60d Add "workflow_dispatch" to GHEvent
This event is undocumented at this time, but this change will stop the deserialization errors.

Fixes #854
2020-07-01 15:57:33 -07:00
Liam Newman
62e186c123 Merge pull request #871 from hub4j/dependabot/maven/net.revelc.code.formatter-formatter-maven-plugin-2.12.0
Chore(deps): Bump formatter-maven-plugin from 2.11.0 to 2.12.0
2020-06-30 11:02:37 -07:00
Marcos.Cela
50dd8f5bcc add a new doc section "Working with organizations" with a simple example
Closes #812
2020-06-30 09:44:57 +02:00
Marcos Cela
d5fcac9c45 Merge pull request #1 from hub4j/master
Update base
2020-06-30 08:44:42 +02:00
dependabot[bot]
c2bed85190 Chore(deps): Bump formatter-maven-plugin from 2.11.0 to 2.12.0
Bumps [formatter-maven-plugin](https://github.com/revelc/formatter-maven-plugin) from 2.11.0 to 2.12.0.
- [Release notes](https://github.com/revelc/formatter-maven-plugin/releases)
- [Changelog](https://github.com/revelc/formatter-maven-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/revelc/formatter-maven-plugin/compare/formatter-maven-plugin-2.11.0...formatter-maven-plugin-2.12.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-30 02:00:52 +00:00
Liam Newman
183b463ef2 Merge branch 'master' into add-support-for-child-teams 2020-06-29 09:58:14 -07:00
Liam Newman
92fdac44a0 Merge pull request #869 from hub4j/dependabot/maven/org.apache.maven.plugins-maven-site-plugin-3.9.1
Chore(deps): Bump maven-site-plugin from 3.9.0 to 3.9.1
2020-06-29 09:57:00 -07:00
dependabot[bot]
12829ecc73 Chore(deps): Bump maven-site-plugin from 3.9.0 to 3.9.1
Bumps [maven-site-plugin](https://github.com/apache/maven-site-plugin) from 3.9.0 to 3.9.1.
- [Release notes](https://github.com/apache/maven-site-plugin/releases)
- [Commits](https://github.com/apache/maven-site-plugin/compare/maven-site-plugin-3.9.0...maven-site-plugin-3.9.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-25 02:00:46 +00:00
Alexander Kjäll
51319c3b26 use this.organization instead of providing the organization as a parameter 2020-06-24 07:30:59 +02:00
Liam Newman
8fd827040b Merge pull request #860 from hub4j/dependabot/github_actions/actions/cache-v2
Chore(deps): Bump actions/cache from v1 to v2
2020-06-23 09:24:06 -07:00
dependabot[bot]
5ec46eae0d Chore(deps): Bump actions/cache from v1 to v2
Bumps [actions/cache](https://github.com/actions/cache) from v1 to v2.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v1...b8204782bbb5f872091ecc5eb9cb7d004e35b1fa)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-23 14:59:54 +00:00
Liam Newman
32c03301be Merge pull request #853 from sullis/add-dependabot
add Dependabot
2020-06-23 07:59:26 -07:00
Liam Newman
df7f29b2ab Merge pull request #858 from hub4j/dependabot/maven/spotbugs.version-4.0.6
Chore(deps): Bump spotbugs.version from 4.0.4 to 4.0.6
2020-06-23 07:55:48 -07:00
dependabot-preview[bot]
e863113c36 Chore(deps): Bump spotbugs.version from 4.0.4 to 4.0.6
Bumps `spotbugs.version` from 4.0.4 to 4.0.6.

Updates `spotbugs` from 4.0.4 to 4.0.6
- [Release notes](https://github.com/spotbugs/spotbugs/releases)
- [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spotbugs/spotbugs/compare/4.0.4...4.0.6)

Updates `spotbugs-annotations` from 4.0.4 to 4.0.6
- [Release notes](https://github.com/spotbugs/spotbugs/releases)
- [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spotbugs/spotbugs/compare/4.0.4...4.0.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-23 06:40:11 +00:00
Liam Newman
8e2c1d7382 Merge pull request #851 from MarcosCela/fix/ghteambuilder-accept-long
GHTeamBuilder#parentTeamId now accepts a long instead of an int
2020-06-22 06:28:20 -07:00
Liam Newman
ab7b9cccba Merge branch 'master' into fix/ghteambuilder-accept-long 2020-06-22 06:18:54 -07:00
Liam Newman
81bf61a161 Merge pull request #857 from hub4j/dependabot/maven/com.github.spotbugs-spotbugs-maven-plugin-4.0.4
Chore(deps): Bump spotbugs-maven-plugin from 4.0.0 to 4.0.4
2020-06-22 06:14:39 -07:00
dependabot-preview[bot]
b40f008647 Chore(deps): Bump spotbugs-maven-plugin from 4.0.0 to 4.0.4
Bumps [spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) from 4.0.0 to 4.0.4.
- [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases)
- [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.0.0...spotbugs-maven-plugin-4.0.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-22 06:57:13 +00:00
Sean C. Sullivan
734e41702b add Dependabot
https://github.blog/2020-06-01-keep-all-your-packages-up-to-date-with-dependabot/
2020-06-20 07:10:28 -07:00
Alexander Kjäll
038dd20a91 added a unit test for fetching a child team 2020-06-19 08:27:25 +02:00
Marcos.Cela
1dd62b8550 add a simple test for GHTeamBuilder: create a team with a parent/child relation
Additionally, ensure that when creating the team and setting the parentTeamId
on the GHTeamBuilder, we receive it directly from a previously retrieved
GHTeam. This ensures that the return type of GHTeam#getId() is compatible
with GHTeamBuilder#parentTeamId()
2020-06-18 10:33:04 +02:00
Marcos.Cela
715deebe05 GHTeamBuilder#parentTeamId now accepts a long instead of an int
Closes #850
2020-06-17 12:15:29 +02:00
Alexander Kjäll
b3fe3d8590 Added support for fetching what teams are part of this team.
The call is to this endpoint https://developer.github.com/v3/teams/#list-child-teams-legacy
2020-06-17 09:38:17 +02:00
Liam Newman
f74c3ed3ea Merge pull request #848 from hub4j/dependabot/maven/org.kohsuke.stapler-stapler-1.260
Chore(deps-dev): Bump stapler from 1.259 to 1.260
2020-06-16 08:39:53 -07:00
Liam Newman
2c9aebeeed Merge pull request #847 from hub4j/timja-patch-1
Fix tag template in release drafter
2020-06-16 08:34:54 -07:00
dependabot-preview[bot]
7474f1e11f Chore(deps-dev): Bump stapler from 1.259 to 1.260
Bumps [stapler](https://github.com/stapler/stapler) from 1.259 to 1.260.
- [Release notes](https://github.com/stapler/stapler/releases)
- [Changelog](https://github.com/stapler/stapler/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stapler/stapler/compare/stapler-parent-1.259...stapler-parent-1.260)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-16 06:28:21 +00:00
Tim Jacomb
dba9c55b64 Fix tag template in release drafter 2020-06-16 07:16:52 +01:00
Liam Newman
b432364397 [maven-release-plugin] prepare for next development iteration 2020-06-10 20:02:26 -07:00
Liam Newman
696967bdd1 [maven-release-plugin] prepare release github-api-1.114 2020-06-10 20:02:18 -07:00
Liam Newman
b76889efc3 Merge pull request #845 from bitwiseman/task/redo-822
Modify getRef() changes to be compatible with older GHE versions
2020-06-10 20:00:44 -07:00
Liam Newman
e6a7b64ebe Merge branch 'master' into task/redo-822 2020-06-10 19:53:42 -07:00
Liam Newman
9daa0df311 Modify getRef() changes to be compatible with older GHE versions
Fixes #844
    Fixes #794
2020-06-10 19:47:23 -07:00
Liam Newman
612800bda5 Merge pull request #843 from bitwiseman/task/body-close
[JENKINS-62655] Ensure connection response stream is always closed
2020-06-10 17:30:12 -07:00
Liam Newman
a6bbb1dec9 Ensure connection response stream is always closed 2020-06-10 17:22:25 -07:00
Liam Newman
873c93ab64 Merge pull request #841 from hub4j/dependabot/maven/org.apache.bcel-bcel-6.5.0
Chore(deps): Bump bcel from 6.4.1 to 6.5.0
2020-06-10 08:59:00 -07:00
dependabot-preview[bot]
d15242e2d2 Chore(deps): Bump bcel from 6.4.1 to 6.5.0
Bumps bcel from 6.4.1 to 6.5.0.

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-10 03:50:37 +00:00
Liam Newman
992d2b937c Merge pull request #840 from hub4j/dependabot/maven/spotbugs.version-4.0.4
Chore(deps): Bump spotbugs.version from 4.0.3 to 4.0.4
2020-06-09 20:49:24 -07:00
dependabot-preview[bot]
1e05ddad4b Chore(deps): Bump spotbugs.version from 4.0.3 to 4.0.4
Bumps `spotbugs.version` from 4.0.3 to 4.0.4.

Updates `spotbugs` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/spotbugs/spotbugs/releases)
- [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spotbugs/spotbugs/compare/4.0.3...4.0.4)

Updates `spotbugs-annotations` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/spotbugs/spotbugs/releases)
- [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spotbugs/spotbugs/compare/4.0.3...4.0.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-09 06:33:50 +00:00
Liam Newman
4f8a64610b [maven-release-plugin] prepare for next development iteration 2020-06-08 14:37:20 -07:00
Liam Newman
b82366218c [maven-release-plugin] prepare release github-api-1.113 2020-06-08 14:37:11 -07:00
Liam Newman
acbe1f4cb3 Update PULL_REQUEST_TEMPLATE.md 2020-06-08 13:25:56 -07:00
Liam Newman
4c5e018583 Merge pull request #838 from Chew/feature/new-profile-data
Add bio, hireable, and twitter_username fields to Person
2020-06-08 12:21:16 -07:00
Liam Newman
6c0380e85c Merge pull request #839 from bitwiseman/issue-828
Add GitHub Team Discussions as GHDiscussion
2020-06-08 12:18:46 -07:00
Liam Newman
fde48e604f Test coverage and javadoc fixes 2020-06-08 12:06:39 -07:00
Liam Newman
e83a4de5fb Merge branch 'master' into feature/new-profile-data 2020-06-08 10:33:29 -07:00
Liam Newman
927d2799dc Move url construction to single method 2020-06-08 10:12:15 -07:00
Liam Newman
1ad701fe5d Add convenience override of getId() 2020-06-08 09:59:43 -07:00
Liam Newman
086425d2da Tweaks for batch update 2020-06-08 09:59:43 -07:00
Charles Moulliard
beca54416a Merge branch 'master' into issue-828 2020-06-08 18:43:15 +02:00
Chew
c92f5c5713 Update test value and add new test for Twitter Username 2020-06-07 15:05:02 -05:00
Chew
dee4e6caff Add twitter_username to Person and bio and hireable to User 2020-06-07 00:55:44 -05:00
Liam Newman
dd5a39e72e Improve wiremock stub accuracy 2020-06-06 16:29:26 -07:00
Charles Moulliard
e5ed52165c Fix: Add missing @param for the delete() method 2020-06-04 18:26:14 +02:00
Charles Moulliard
9484f8e0f5 Chore: Add more methods to test CRUD operations on discusions 2020-06-04 18:19:41 +02:00
Charles Moulliard
947caffe0a Chore: Add method to get a discussion using a number/id 2020-06-04 17:27:15 +02:00
Charles Moulliard
870090e8df Chore: Remove javadoc Throwing the exception for the GHDiscussionbuilder - update method 2020-06-04 13:51:04 +02:00
Charles Moulliard
73f07f13c5 Chore: Remove javadoc Throwing the exception 2020-06-04 13:47:41 +02:00
Charles Moulliard
d1952bf591 Chore: Reformat method 2020-06-04 13:37:08 +02:00
Charles Moulliard
5a612e1332 Chore: Add try/catch block if we cannot find the discussion to be updated 2020-06-04 13:25:57 +02:00
Charles Moulliard
b00a9faea6 Fix: Add missing parameter 2020-06-04 12:58:07 +02:00
Charles Moulliard
74db42a703 Chore: Add method to update a discussion 2020-06-04 12:52:58 +02:00
Charles Moulliard
ddf625ca04 Chore: Add method to delete a discussion using its number. Add field number 2020-06-04 12:20:36 +02:00
Charles Moulliard
eca2f017d8 Fix: Add missing import statement for the Jackson Annotation. Use the correct htmlUrl field 2020-06-04 11:32:33 +02:00
Charles Moulliard
3190bde343 Fix: Add mising try/catch block to report the exeption when no discussions are found 2020-06-04 11:31:50 +02:00
Charles Moulliard
c6ebf42a47 Update src/main/java/org/kohsuke/github/GHTeam.java
Dont wrapUp using the team object

Co-authored-by: Liam Newman <bitwiseman@gmail.com>
2020-06-04 11:14:42 +02:00
Charles Moulliard
c116b60d12 Update src/main/java/org/kohsuke/github/GHDiscussion.java
Add doc link to github team discussion API

Co-authored-by: Liam Newman <bitwiseman@gmail.com>
2020-06-04 11:13:19 +02:00
Charles Moulliard
5d09e6d9ab Update src/main/java/org/kohsuke/github/GHDiscussion.java
Remove `this.root` as it is already set with the org

Co-authored-by: Liam Newman <bitwiseman@gmail.com>
2020-06-04 11:12:38 +02:00
Charles Moulliard
2613ce0ac9 Update src/main/java/org/kohsuke/github/GHDiscussion.java
Remove to set the field `root`

Co-authored-by: Liam Newman <bitwiseman@gmail.com>
2020-06-04 11:10:21 +02:00
Charles Moulliard
a88e9b28ea Update src/main/java/org/kohsuke/github/GHDiscussion.java
Change the visibility of the fields from protected to private. Add @JacksonInject annotation. Rename html_url to htmlUrl as needed by Jackson

Co-authored-by: Liam Newman <bitwiseman@gmail.com>
2020-06-04 11:09:24 +02:00
Charles Moulliard
f0a3c26ee6 Fix: Add the missing correct file to check the discussion created using wiremock 2020-06-04 10:44:26 +02:00
Charles Moulliard
84c87ecb32 Chore: Fixed the null org within the generated json file but we still get an error 404 2020-06-04 08:49:16 +02:00
Charles Moulliard
6573f44d41 Fix: As the name of the organization could be empty/null, then use getLogin to get the org name 2020-06-04 07:46:51 +02:00
Charles Moulliard
3cacbc552c Fix: Set the organisation name to avoid to populate a url request having /orgs/null 2020-06-04 07:30:44 +02:00
Charles Moulliard
343d623e02 chore: Push new resource files generated 2020-06-04 07:22:53 +02:00
Charles Moulliard
6b80bb2b11 chore: Remove deleted resources files 2020-06-04 07:00:14 +02:00
Charles Moulliard
56fe7452eb chore. Review test case. Add new wrapUp methods 2020-06-04 06:59:41 +02:00
Charles Moulliard
d3a66f6605 chore: Regenerate new testing files 2020-05-29 17:37:49 +02:00
Charles Moulliard
dd7b4712f1 fix: Add missing @throws IOException 2020-05-29 17:24:49 +02:00
Charles Moulliard
9df5871f6b chore: wrapUp Github instance 2020-05-29 17:20:54 +02:00
Charles Moulliard
29aab9e9f4 chore: Add missing classes and test case 2020-05-29 16:34:07 +02:00
Charles Moulliard
af67eb7f0b feat: add new APi for Discussion 2020-05-29 16:28:47 +02:00
Liam Newman
10482c0141 [maven-release-plugin] prepare for next development iteration 2020-05-28 07:48:53 -07:00
Liam Newman
a7a792251a [maven-release-plugin] prepare release github-api-1.112 2020-05-28 07:48:45 -07:00
Liam Newman
aec2308144 Merge pull request #831 from hub4j/dependabot/maven/org.apache.maven.plugins-maven-project-info-reports-plugin-3.1.0
Bump maven-project-info-reports-plugin from 3.0.0 to 3.1.0
2020-05-28 07:34:56 -07:00
dependabot-preview[bot]
0741b8aa6a Bump maven-project-info-reports-plugin from 3.0.0 to 3.1.0
Bumps [maven-project-info-reports-plugin](https://github.com/apache/maven-project-info-reports-plugin) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/apache/maven-project-info-reports-plugin/releases)
- [Commits](https://github.com/apache/maven-project-info-reports-plugin/compare/maven-project-info-reports-plugin-3.0.0...maven-project-info-reports-plugin-3.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-28 06:40:32 +00:00
Liam Newman
3082622394 Merge pull request #822 from bitwiseman/issue/794
Fixed getRef() to use git/ref endpoint instead of git/refs
2020-05-21 11:02:43 -07:00
Liam Newman
965c9cb0af Fixed getRef() to use git/ref endpoint instead of git/refs
Fixes #794
2020-05-21 10:41:36 -07:00
Liam Newman
495a46e2d8 Merge pull request #823 from bitwiseman/task/cleanup-more
Code cleanup for GHObject and GHRepositoryStatistics
2020-05-21 07:51:51 -07:00
Liam Newman
05bda1192e Merge branch 'master' into task/cleanup-more 2020-05-20 21:00:31 -07:00
Liam Newman
6058af0ca1 Merge pull request #821 from bitwiseman/issue/805
Populate Source and Parent if needed
2020-05-20 20:59:21 -07:00
Liam Newman
1eb8bf9719 Reduce round trips and cleanup test data 2020-05-20 19:13:37 -07:00
Liam Newman
afc02faeda Clean up GHObject field access 2020-05-20 19:11:48 -07:00
Liam Newman
66f22de90f Populate Source and Parent if needed
NOTE: this also addresses a bug in push events where the repository url is incorrect
2020-05-20 13:22:09 -07:00
Liam Newman
2949a2e0ff Clean up GHRepositoryStatistics constructors 2020-05-19 17:25:58 -07:00
Liam Newman
ba12efea9d Merge pull request #820 from bitwiseman/issue/800
Prevent NPE when accessing description for GHLicense
2020-05-19 17:25:14 -07:00
Liam Newman
e1180a12fb Merge pull request #817 from bitwiseman/issue/802
Consider header rate limit information for getRateLimit()
2020-05-19 17:08:25 -07:00
Liam Newman
1393706f13 Prevent NPE when accessing description for license
When license can be identified, the description will be null.  So will the url.
Tht means that url cannot be converted to string.

Fixes #800 .
2020-05-19 17:06:59 -07:00
Liam Newman
6f994f31f7 Update src/main/java/org/kohsuke/github/GitHubClient.java 2020-05-19 14:25:16 -07:00
Liam Newman
38aa99a063 Consider header rate limit information for getRateLimit()
Fixes #802
2020-05-18 13:56:10 -07:00
Liam Newman
85c44b3529 Merge pull request #809 from pzygielo/imports
Remove unused imports
2020-05-16 13:50:23 -07:00
Liam Newman
e1a2768de5 Merge pull request #810 from pzygielo/equals
Use .equals to compare Strings
2020-05-16 13:49:54 -07:00
Liam Newman
e1c9b27203 Merge pull request #811 from pzygielo/close
Be nice and close Closeable
2020-05-16 13:49:14 -07:00
Piotrek Żygieło
969f6ef826 Use .equals to compare Strings 2020-05-16 11:55:21 +02:00
Piotrek Żygieło
7abc4d4e76 Be nice and close Closeable 2020-05-16 11:49:02 +02:00
Piotrek Żygieło
ac97147c1f Remove unused imports 2020-05-16 09:58:20 +02:00
Liam Newman
dbd20fe396 Merge pull request #804 from springernature/master
fix to #803
2020-05-13 09:12:20 -07:00
Liam Newman
44e57c9c4b Merge pull request #808 from hub4j/dependabot/maven/spotbugs.version-4.0.3
Bump spotbugs.version from 4.0.2 to 4.0.3
2020-05-13 09:07:37 -07:00
Stefan Reisner
488e5e531f updated GHContentIntegrationTest.java
dummy commit
2020-05-13 12:39:10 +02:00
dependabot-preview[bot]
42a6a8d770 Bump spotbugs.version from 4.0.2 to 4.0.3
Bumps `spotbugs.version` from 4.0.2 to 4.0.3.

Updates `spotbugs` from 4.0.2 to 4.0.3
- [Release notes](https://github.com/spotbugs/spotbugs/releases)
- [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spotbugs/spotbugs/compare/4.0.2...4.0.3)

Updates `spotbugs-annotations` from 4.0.2 to 4.0.3
- [Release notes](https://github.com/spotbugs/spotbugs/releases)
- [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spotbugs/spotbugs/compare/4.0.2...4.0.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-13 06:52:27 +00:00
Liam Newman
f8e877ea05 Merge pull request #806 from bitwiseman/task/rename
Rename organization to hub4j
2020-05-12 08:52:06 -07:00
Stefan Reisner
65d6fc7272 updated GHContentIntegrationTest.java
dummy commit
2020-05-12 12:06:44 +02:00
Dr. Stefan Reisner
63ce8e461b Update src/test/java/org/kohsuke/github/GHContentIntegrationTest.java
Co-authored-by: Liam Newman <bitwiseman@gmail.com>
2020-05-12 09:59:53 +02:00
Stefan Reisner
fbf4c48461 updated GHContentIntegrationTest.java, src/test/resources/org/kohsuke/github/GHContentIntegrationTest/wiremock/**
adapting to new org hub4-test-org (was github-api-test-org)
2020-05-11 16:33:29 +02:00
Stefan Reisner
81a55db644 updated src/test/resources/org/kohsuke/github/GHContentIntegrationTest/wiremock/testGetFileWithNoneAsciiPath/** 2020-05-11 15:29:02 +02:00
Stefan Reisner
4d4edfa181 updated GHContentIntegrationTest.java
adapting testGetFileContentWithNonAsciiPath to shortened file path
2020-05-11 08:01:32 +02:00
Liam Newman
6f9182f1f6 Rename organization to hub4j
Fixes #801
2020-05-08 15:17:14 -07:00
Stefan Reisner
fa600c03e2 updated GHContentIntegrationTest.java
sorting imports
2020-05-06 17:33:11 +02:00
Dr. Stefan Reisner
4a53301e9f Merge branch 'master' into master 2020-05-06 17:26:06 +02:00
Stefan Reisner
676984b3d5 added src/test/resources/org/kohsuke/github/GHContentIntegrationTest/wiremock/testGetFileContentWithNonAsciiPath/** 2020-05-06 16:58:42 +02:00
Stefan Reisner
e6d7f7248b updated GitHubRequest.java
using URI.toASCIIString() instead of URI.toString() in urlPathEncode()
2020-05-06 16:54:21 +02:00
Stefan Reisner
50903b5c4a updated GHContentIntegrationTest.java 2020-05-06 16:53:23 +02:00
Liam Newman
01e399fb91 Add Java 15 early access to CI
Switch canary compile to Java 13.
2020-05-01 10:55:42 -07:00
Liam Newman
911aeb7af0 Merge pull request #796 from github-api/dependabot/maven/net.revelc.code-impsort-maven-plugin-1.4.1
Bump impsort-maven-plugin from 1.3.2 to 1.4.1
2020-04-27 14:21:34 -07:00
Liam Newman
7e5cd9abbc Merge branch 'master' into dependabot/maven/net.revelc.code-impsort-maven-plugin-1.4.1 2020-04-27 14:12:08 -07:00
Liam Newman
115527a21a Merge pull request #792 from bitwiseman/issue/682
Fix Gist getId() and deleteFile(), add getGistId()
2020-04-27 13:37:34 -07:00
Liam Newman
eff4f4f601 Merge branch 'master' into issue/682 2020-04-27 13:20:35 -07:00
Liam Newman
16e0099a0d Add deleteFile() to GHGist
Related to #466 and #484
2020-04-27 10:44:25 -07:00
Liam Newman
2c8c678275 Merge pull request #770 from sladyn98/change_url_methods
Change withURLPath to setRawURLPath
2020-04-27 09:54:14 -07:00
Liam Newman
3b51e87fbf Merge branch 'master' into change_url_methods 2020-04-27 09:48:23 -07:00
Liam Newman
6c6eef5e2b Merge pull request #795 from bitwiseman/task/check-raw-url
Check that raw url starts with 'http'
2020-04-27 09:47:24 -07:00
Liam Newman
6e5910f44c Check that raw url starts with 'http' 2020-04-27 09:41:43 -07:00
dependabot-preview[bot]
a967189bc6 Bump impsort-maven-plugin from 1.3.2 to 1.4.1
Bumps [impsort-maven-plugin](https://github.com/revelc/impsort-maven-plugin) from 1.3.2 to 1.4.1.
- [Release notes](https://github.com/revelc/impsort-maven-plugin/releases)
- [Commits](https://github.com/revelc/impsort-maven-plugin/compare/impsort-maven-plugin-1.3.2...impsort-maven-plugin-1.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-04-27 06:59:29 +00:00
Sladyn
7069176cf6 Apply suggestions from code review
Co-Authored-By: Liam Newman <bitwiseman@gmail.com>
2020-04-27 10:49:57 +05:30
Liam Newman
44dcbe773d Fix Gist getId() and add getGistId()
Fixes #682
2020-04-25 20:16:58 -07:00
Liam Newman
ca76975461 Record Disable test 2020-04-23 11:13:51 -07:00
Liam Newman
83122ac99e Take new snapshot of testGetProtection() 2020-04-23 11:13:51 -07:00
Sladyn Nunes
c3e9458555 Additional modifications to test 2020-04-23 11:13:51 -07:00
Sladyn Nunes
057ba38873 Added getProtectionTest 2020-04-23 11:13:50 -07:00
Sladyn Nunes
81d7d6236b Add resource files 2020-04-23 11:13:50 -07:00
Sladyn Nunes
191dd49653 added test and modified url 2020-04-23 11:13:50 -07:00
Sladyn Nunes
21e9dd6f51 Change withURLPath to setRawURLPath 2020-04-23 11:13:50 -07:00
Liam Newman
cc2d14acc6 Merge pull request #791 from ingwarsw/global_node_id
Add global node_id to GHObject + GHTeam extends GHObject
2020-04-22 13:15:13 -07:00
Liam Newman
87f37e9f1c Formatting and improved bridge method tests 2020-04-22 09:40:19 -07:00
Karol Lassak
d536a9f874 Merge branch 'master' into global_node_id 2020-04-19 11:41:17 +02:00
Karol Lassak
b45f353fa9 Fix tests + add deprecation to one of methods 2020-04-19 11:38:36 +02:00
Karol Lassak
a3073ec14e Fix formatting 2020-04-19 11:19:48 +02:00
Karol Lassak
f77eb33029 Add deprecated method 2020-04-19 11:18:29 +02:00
Liam Newman
c1c919097a Merge pull request #790 from chids/reintroduce-proxy-test-confirmation
Reintroduce the confirmation message for the Github proxy setup
2020-04-18 15:07:41 -07:00
Karol Lassak
e05348463c Fix javadoc 2020-04-18 14:32:51 +02:00
Karol Lassak
fdcf74eaf2 Add global node_id to GHObject + GHTeam extends GHObject 2020-04-18 14:23:06 +02:00
Mårten Gustafson
6d57a3e3b9 Reintroduce the confirmation message for the Github proxy setup
which is referenced from the contribution guidlines.
2020-04-17 15:17:19 +02:00
Liam Newman
1f449c866e Merge pull request #781 from bitwiseman/task/app-set-dep
Deprecate set methods that should not be public
2020-04-16 11:49:09 -07:00
Liam Newman
e12deccd24 Merge pull request #782 from chids/support-issue-event-attributes
Support issue event attributes
2020-04-16 11:36:42 -07:00
Mårten Gustafson
3184ebb5ee Link to Github docs for issue events. 2020-04-16 09:28:38 +02:00
Liam Newman
4a35ed2b35 Merge branch 'master' into support-issue-event-attributes 2020-04-16 00:02:42 -07:00
Liam Newman
5c9474d1c8 [maven-release-plugin] prepare for next development iteration 2020-04-15 23:48:24 -07:00
Mårten Gustafson
2724211535 Merge branch 'master' into support-issue-event-attributes 2020-04-16 08:30:51 +02:00
Mårten Gustafson
81068de0f1 Revert "WireMockStatusReporterTest doesn't print to stdout as of"
This reverts commit d76718e8b2.
2020-04-16 08:26:03 +02:00
Mårten Gustafson
df576e2738 Expose and test per issue event attributes for milestone, label and
assignment events.
2020-04-14 11:47:44 +02:00
Mårten Gustafson
bb1356b25d Add Wiremock recordings for issue events. 2020-04-14 11:24:44 +02:00
Liam Newman
1b67960da4 Deprecate set methods that should not be public 2020-04-13 13:41:28 -07:00
Mårten Gustafson
d76718e8b2 WireMockStatusReporterTest doesn't print to stdout as of
061e8bb662.
2020-04-13 10:30:08 +02:00
3622 changed files with 210810 additions and 184482 deletions

View File

@@ -10,3 +10,7 @@ We love getting PRs, but we hate asking people for the same basic changes every
- [ ] Run `mvn clean compile` locally. This may reformat your code, commit those changes.
- [ ] Run `mvn -D enable-ci clean install site` locally. If this command doesn't succeed, your change will not pass CI.
# When creating a PR:
- [ ] Fill in the "Description" above.
- [ ] Enable "Allow edits from maintainers".

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "daily"
time: "02:00"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
time: "02:00"

View File

@@ -1,5 +1,5 @@
name-template: 'v$NEXT_PATCH_VERSION 🌈'
tag-template: 'v$NEXT_PATCH_VERSION'
tag-template: 'github-api-$NEXT_MINOR_VERSION'
categories:
- title: '🚀 Features'
labels:

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ 11 ]
java: [ 13 ]
steps:
- uses: actions/checkout@v2
- name: Set up JDK
@@ -17,7 +17,7 @@ jobs:
with:
java-version: ${{ matrix.java }}
- name: Cached .m2
uses: actions/cache@v1
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -37,7 +37,7 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.java }}
- uses: actions/cache@v1
- uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -51,14 +51,14 @@ jobs:
strategy:
matrix:
os: [ ubuntu, windows ]
java: [ 8, 11, 13 ]
java: [ 8, 11, 13, 15-ea ]
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.java }}
- uses: actions/cache@v1
- uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
# Java API for GitHub
[![Sonatype Nexus (Releases)](https://img.shields.io/nexus/r/org.kohsuke/github-api?server=https%3A%2F%2Foss.sonatype.org)](https://mvnrepository.com/artifact/org.kohsuke/github-api)
[![Join the chat at https://gitter.im/github-api/github-api](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/github-api/github-api?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
![CI](https://github.com/github-api/github-api/workflows/CI/badge.svg?branch=master)
[![Join the chat at https://gitter.im/hub4j/github-api](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hub4j/github-api?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
![CI](https://github.com/hub4j/github-api/workflows/CI/badge.svg?branch=master)

68
pom.xml
View File

@@ -2,16 +2,16 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.kohsuke</groupId>
<artifactId>github-api</artifactId>
<version>1.111</version>
<version>1.116</version>
<name>GitHub API for Java</name>
<url>https://github-api.kohsuke.org/</url>
<description>GitHub API for Java</description>
<scm>
<connection>scm:git:git@github.com/github-api/${project.artifactId}.git</connection>
<developerConnection>scm:git:ssh://git@github.com/github-api/${project.artifactId}.git</developerConnection>
<url>https://github.com/github-api/github-api/</url>
<tag>github-api-1.111</tag>
<connection>scm:git:git@github.com/hub4j/${project.artifactId}.git</connection>
<developerConnection>scm:git:ssh://git@github.com/hub4j/${project.artifactId}.git</developerConnection>
<url>https://github.com/hub4j/github-api/</url>
<tag>github-api-1.116</tag>
</scm>
<distributionManagement>
@@ -27,14 +27,14 @@
</repository>
<site>
<id>github-pages</id>
<url>gitsite:git@github.com/github-api/${project.artifactId}.git</url>
<url>gitsite:git@github.com/hub4j/${project.artifactId}.git</url>
</site>
</distributionManagement>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spotbugs-maven-plugin.version>4.0.0</spotbugs-maven-plugin.version>
<spotbugs.version>4.0.2</spotbugs.version>
<spotbugs-maven-plugin.version>4.0.4</spotbugs-maven-plugin.version>
<spotbugs.version>4.1.1</spotbugs.version>
<spotbugs-maven-plugin.failOnError>true</spotbugs-maven-plugin.failOnError>
<hamcrest.version>2.2</hamcrest.version>
<okhttp3.version>4.4.1</okhttp3.version>
@@ -79,6 +79,14 @@
</testResources>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<!-- SUREFIRE-1226 workaround -->
<trimStackTrace>false</trimStackTrace>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
@@ -233,7 +241,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.9.0</version>
<version>3.9.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -253,12 +261,12 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
<dependencies>
<dependency>
<groupId>org.apache.bcel</groupId>
<artifactId>bcel</artifactId>
<version>6.4.1</version>
<version>6.5.0</version>
</dependency>
</dependencies>
</plugin>
@@ -280,16 +288,31 @@
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<!-- SUREFIRE-1226 workaround -->
<trimStackTrace>false</trimStackTrace>
</configuration>
<executions>
<execution>
<id>default-test</id>
<configuration>
<excludesFile>src/test/resources/slow-or-flaky-tests.txt</excludesFile>
</configuration>
</execution>
<execution>
<id>slow-or-flaky-test</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<rerunFailingTestsCount>2</rerunFailingTestsCount>
<!-- There are some tests that take longer or are a little flaky. Run them here. -->
<includesFile>src/test/resources/slow-or-flaky-tests.txt</includesFile>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<version>1.18</version>
<version>1.19</version>
<configuration>
<signature>
<groupId>org.codehaus.mojo.signature</groupId>
@@ -322,7 +345,7 @@
<plugin>
<groupId>net.revelc.code.formatter</groupId>
<artifactId>formatter-maven-plugin</artifactId>
<version>2.11.0</version>
<version>2.12.1</version>
<executions>
<execution>
<goals>
@@ -330,6 +353,7 @@
</goals>
<configuration>
<configFile>src/main/resources/eclipse/formatter.xml</configFile>
<cachedir>${project.build.directory}/.cache</cachedir>
</configuration>
</execution>
</executions>
@@ -337,7 +361,7 @@
<plugin>
<groupId>net.revelc.code</groupId>
<artifactId>impsort-maven-plugin</artifactId>
<version>1.3.2</version>
<version>1.4.1</version>
<configuration>
<groups>*,java.,javax.</groups>
<removeUnused>true</removeUnused>
@@ -445,7 +469,7 @@
<dependency>
<groupId>org.kohsuke.stapler</groupId>
<artifactId>stapler</artifactId>
<version>1.259</version>
<version>1.260</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -495,7 +519,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<version>3.4.6</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -507,7 +531,7 @@
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8-standalone</artifactId>
<version>2.26.3</version>
<version>2.27.1</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -39,7 +39,9 @@ public class GHApp extends GHObject {
*
* @param owner
* the owner
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setOwner(GHUser owner) {
this.owner = owner;
}
@@ -58,7 +60,9 @@ public class GHApp extends GHObject {
*
* @param name
* the name
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setName(String name) {
this.name = name;
}
@@ -77,7 +81,9 @@ public class GHApp extends GHObject {
*
* @param description
* the description
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setDescription(String description) {
this.description = description;
}
@@ -96,7 +102,9 @@ public class GHApp extends GHObject {
*
* @param externalUrl
* the external url
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setExternalUrl(String externalUrl) {
this.externalUrl = externalUrl;
}
@@ -115,7 +123,9 @@ public class GHApp extends GHObject {
*
* @param events
* the events
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setEvents(List<GHEvent> events) {
this.events = events;
}
@@ -134,7 +144,9 @@ public class GHApp extends GHObject {
*
* @param installationsCount
* the installations count
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setInstallationsCount(long installationsCount) {
this.installationsCount = installationsCount;
}
@@ -157,7 +169,9 @@ public class GHApp extends GHObject {
*
* @param permissions
* the permissions
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setPermissions(Map<String, String> permissions) {
this.permissions = permissions;
}

View File

@@ -59,7 +59,9 @@ public class GHAppInstallation extends GHObject {
*
* @param root
* the root
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setRoot(GitHub root) {
this.root = root;
}
@@ -78,7 +80,9 @@ public class GHAppInstallation extends GHObject {
*
* @param account
* the account
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setAccount(GHUser account) {
this.account = account;
}
@@ -97,7 +101,9 @@ public class GHAppInstallation extends GHObject {
*
* @param accessTokenUrl
* the access token url
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setAccessTokenUrl(String accessTokenUrl) {
this.accessTokenUrl = accessTokenUrl;
}
@@ -116,7 +122,9 @@ public class GHAppInstallation extends GHObject {
*
* @param repositoriesUrl
* the repositories url
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setRepositoriesUrl(String repositoriesUrl) {
this.repositoriesUrl = repositoriesUrl;
}
@@ -135,7 +143,9 @@ public class GHAppInstallation extends GHObject {
*
* @param appId
* the app id
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setAppId(long appId) {
this.appId = appId;
}
@@ -154,7 +164,9 @@ public class GHAppInstallation extends GHObject {
*
* @param targetId
* the target id
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setTargetId(long targetId) {
this.targetId = targetId;
}
@@ -173,7 +185,9 @@ public class GHAppInstallation extends GHObject {
*
* @param targetType
* the target type
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setTargetType(GHTargetType targetType) {
this.targetType = targetType;
}
@@ -192,7 +206,9 @@ public class GHAppInstallation extends GHObject {
*
* @param permissions
* the permissions
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setPermissions(Map<String, GHPermissionType> permissions) {
this.permissions = permissions;
}
@@ -211,7 +227,9 @@ public class GHAppInstallation extends GHObject {
*
* @param events
* the events
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setEvents(List<GHEvent> events) {
this.events = events;
}
@@ -230,7 +248,9 @@ public class GHAppInstallation extends GHObject {
*
* @param singleFileName
* the single file name
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setSingleFileName(String singleFileName) {
this.singleFileName = singleFileName;
}
@@ -249,7 +269,9 @@ public class GHAppInstallation extends GHObject {
*
* @param repositorySelection
* the repository selection
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setRepositorySelection(GHRepositorySelection repositorySelection) {
this.repositorySelection = repositorySelection;
}
@@ -274,7 +296,7 @@ public class GHAppInstallation extends GHObject {
root.createRequest()
.method("DELETE")
.withPreview(GAMBIT)
.withUrlPath(String.format("/app/installations/%d", id))
.withUrlPath(String.format("/app/installations/%d", getId()))
.send();
}
@@ -293,7 +315,9 @@ public class GHAppInstallation extends GHObject {
@Preview
@Deprecated
public GHAppCreateTokenBuilder createToken(Map<String, GHPermissionType> permissions) {
return new GHAppCreateTokenBuilder(root, String.format("/app/installations/%d/access_tokens", id), permissions);
return new GHAppCreateTokenBuilder(root,
String.format("/app/installations/%d/access_tokens", getId()),
permissions);
}
/**
@@ -308,6 +332,6 @@ public class GHAppInstallation extends GHObject {
@Preview
@Deprecated
public GHAppCreateTokenBuilder createToken() {
return new GHAppCreateTokenBuilder(root, String.format("/app/installations/%d/access_tokens", id));
return new GHAppCreateTokenBuilder(root, String.format("/app/installations/%d/access_tokens", getId()));
}
}

View File

@@ -37,7 +37,9 @@ public class GHAppInstallationToken {
*
* @param root
* the root
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setRoot(GitHub root) {
this.root = root;
}
@@ -56,7 +58,9 @@ public class GHAppInstallationToken {
*
* @param permissions
* the permissions
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setPermissions(Map<String, String> permissions) {
this.permissions = permissions;
}
@@ -75,7 +79,9 @@ public class GHAppInstallationToken {
*
* @param token
* the token
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setToken(String token) {
this.token = token;
}
@@ -94,7 +100,9 @@ public class GHAppInstallationToken {
*
* @param repositories
* the repositories
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setRepositories(List<GHRepository> repositories) {
this.repositories = repositories;
}
@@ -113,7 +121,9 @@ public class GHAppInstallationToken {
*
* @param repositorySelection
* the repository selection
* @deprecated Do not use this method. It was added due to incomplete understanding of Jackson binding.
*/
@Deprecated
public void setRepositorySelection(GHRepositorySelection repositorySelection) {
this.repositorySelection = repositorySelection;
}

View File

@@ -149,7 +149,7 @@ public class GHAsset extends GHObject {
}
private String getApiRoute() {
return "/repos/" + owner.getOwnerName() + "/" + owner.getName() + "/releases/assets/" + id;
return "/repos/" + owner.getOwnerName() + "/" + owner.getName() + "/releases/assets/" + getId();
}
GHAsset wrap(GHRelease release) {

View File

@@ -112,10 +112,12 @@ public class GHAuthorization extends GHObject {
* Gets api url.
*
* @return the api url
* @deprecated use {@link #getUrl()}
*/
@Deprecated
@SuppressFBWarnings(value = "NM_CONFUSING", justification = "It's a part of the library API, cannot be changed")
public URL getApiURL() {
return GitHubClient.parseURL(url);
return getUrl();
}
/**

View File

@@ -9,6 +9,8 @@ import java.net.URL;
import java.util.Collection;
import java.util.Objects;
import javax.annotation.CheckForNull;
/**
* A branch in a repository.
*
@@ -101,7 +103,11 @@ public class GHBranch {
* the io exception
*/
public GHBranchProtection getProtection() throws IOException {
return root.createRequest().withUrlPath(protection_url).fetch(GHBranchProtection.class).wrap(this);
return root.createRequest()
.withPreview(Previews.LUKE_CAGE)
.setRawUrlPath(protection_url)
.fetch(GHBranchProtection.class)
.wrap(this);
}
/**
@@ -120,7 +126,7 @@ public class GHBranch {
* if disabling protection fails
*/
public void disableProtection() throws IOException {
root.createRequest().method("DELETE").withUrlPath(protection_url).send();
root.createRequest().method("DELETE").setRawUrlPath(protection_url).send();
}
/**
@@ -161,6 +167,59 @@ public class GHBranch {
}
}
/**
* Merge a branch into this branch.
*
* @param headBranch
* the branch whose head will be merged
*
* @param commitMessage
* the commit message
*
* @return the merge {@link GHCommit} created, or {@code null} if the base already contains the head (nothing to
* merge).
*
* @throws IOException
* if merging fails
*/
@CheckForNull
public GHCommit merge(GHBranch headBranch, String commitMessage) throws IOException {
return merge(headBranch.getName(), commitMessage);
}
/**
* Merge a ref into this branch.
*
* @param head
* the ref name that will be merged into this branch. Follows the usual ref naming rules, could be a
* branch name, tag, or commit sha.
*
* @param commitMessage
* the commit message
*
* @return the merge {@link GHCommit} created, or {@code null} if the base already contains the head (nothing to
* merge).
*
* @throws IOException
* if merging fails
*/
@CheckForNull
public GHCommit merge(String head, String commitMessage) throws IOException {
GHCommit result = root.createRequest()
.withUrlPath(owner.getApiTailUrl("merges"))
.method("POST")
.with("commit_message", commitMessage)
.with("base", this.name)
.with("head", head)
.fetch(GHCommit.class);
if (result != null) {
result.wrapUp(owner);
}
return result;
}
String getApiRoute() {
return owner.getApiTailUrl("/branches/" + name);
}

View File

@@ -10,6 +10,8 @@ import static org.kohsuke.github.Previews.ZZZAX;
/**
* The type GHBranchProtection.
*
* @see <a href="https://docs.github.com/en/rest/reference/repos#get-branch-protection">GitHub Branch Protection</a>
*/
@SuppressFBWarnings(
value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD",

View File

@@ -1,10 +1,14 @@
package org.kohsuke.github;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* Represents a check run.
@@ -14,6 +18,8 @@ import java.util.Date;
@SuppressFBWarnings(value = { "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", "URF_UNREAD_FIELD" },
justification = "JSON API")
public class GHCheckRun extends GHObject {
@JsonProperty("repository")
GHRepository owner;
GitHub root;
@@ -34,7 +40,7 @@ public class GHCheckRun extends GHObject {
GHCheckRun wrap(GHRepository owner) {
this.owner = owner;
this.root = owner.root;
wrap(owner.root);
return this;
}
@@ -42,7 +48,24 @@ public class GHCheckRun extends GHObject {
this.root = root;
if (owner != null) {
owner.wrap(root);
if (pullRequests != null && pullRequests.length != 0) {
for (GHPullRequest singlePull : pullRequests) {
singlePull.wrap(owner);
}
}
}
if (checkSuite != null) {
if (owner != null) {
checkSuite.wrap(owner);
} else {
checkSuite.wrap(root);
}
}
if (app != null) {
app.wrapUp(root);
}
return this;
}
@@ -74,8 +97,14 @@ public class GHCheckRun extends GHObject {
return conclusion;
}
/**
* Final conclusion of the check.
*
* From <a href="https://docs.github.com/en/rest/reference/checks#create-a-check-run--parameters">Check Run
* Parameters - <code>conclusion</code></a>.
*/
public static enum Conclusion {
SUCCESS, FAILURE, NEUTRAL, CANCELLED, TIMED_OUT, ACTION_REQUIRED
SUCCESS, FAILURE, NEUTRAL, CANCELLED, TIMED_OUT, ACTION_REQUIRED, SKIPPED
}
/**
@@ -99,15 +128,22 @@ public class GHCheckRun extends GHObject {
/**
* Gets the pull requests participated in this check run.
*
* @return Pull requests of this check run
* Note this field is only populated for events. When getting a {@link GHCheckRun} outside of an event, this is
* always empty.
*
* @return the list of {@link GHPullRequest}s for this check run. Only populated for events.
* @throws IOException
* the io exception
*/
GHPullRequest[] getPullRequests() throws IOException {
public List<GHPullRequest> getPullRequests() throws IOException {
if (pullRequests != null && pullRequests.length != 0) {
for (GHPullRequest singlePull : pullRequests) {
singlePull.refresh();
// Only refresh if we haven't do so before
singlePull.refresh(singlePull.getTitle());
}
return Collections.unmodifiableList(Arrays.asList(pullRequests));
}
return pullRequests;
return Collections.emptyList();
}
/**

View File

@@ -144,7 +144,7 @@ public final class GHCheckRunBuilder {
.withPreview(Previews.ANTIOPE)
.method("PATCH")
.with("output", output2)
.withUrlPath(repo.getApiTailUrl("check-runs/" + run.id))
.withUrlPath(repo.getApiTailUrl("check-runs/" + run.getId()))
.fetch(GHCheckRun.class)
.wrap(repo);
}

View File

@@ -1,10 +1,14 @@
package org.kohsuke.github;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* Represents a check suite.
@@ -14,6 +18,8 @@ import java.util.Date;
@SuppressFBWarnings(value = { "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", "URF_UNREAD_FIELD" },
justification = "JSON API")
public class GHCheckSuite extends GHObject {
@JsonProperty("repository")
GHRepository owner;
GitHub root;
@@ -32,7 +38,7 @@ public class GHCheckSuite extends GHObject {
GHCheckSuite wrap(GHRepository owner) {
this.owner = owner;
this.root = owner.root;
this.wrap(owner.root);
return this;
}
@@ -40,6 +46,14 @@ public class GHCheckSuite extends GHObject {
this.root = root;
if (owner != null) {
owner.wrap(root);
if (pullRequests != null && pullRequests.length != 0) {
for (GHPullRequest singlePull : pullRequests) {
singlePull.wrap(owner);
}
}
}
if (app != null) {
app.wrapUp(root);
}
return this;
}
@@ -153,15 +167,22 @@ public class GHCheckSuite extends GHObject {
/**
* Gets the pull requests participated in this check suite.
*
* @return Pull requests
* Note this field is only populated for events. When getting a {@link GHCheckSuite} outside of an event, this is
* always empty.
*
* @return the list of {@link GHPullRequest}s for this check suite. Only populated for events.
* @throws IOException
* the io exception
*/
GHPullRequest[] getPullRequests() throws IOException {
public List<GHPullRequest> getPullRequests() throws IOException {
if (pullRequests != null && pullRequests.length != 0) {
for (GHPullRequest singlePull : pullRequests) {
singlePull.refresh();
// Only refresh if we haven't do so before
singlePull.refresh(singlePull.getTitle());
}
return Collections.unmodifiableList(Arrays.asList(pullRequests));
}
return pullRequests;
return Collections.emptyList();
}
/**

View File

@@ -153,7 +153,7 @@ public class GHCommitComment extends GHObject implements Reactable {
}
private String getApiTail() {
return String.format("/repos/%s/%s/comments/%s", owner.getOwnerName(), owner.getName(), id);
return String.format("/repos/%s/%s/comments/%s", owner.getOwnerName(), owner.getName(), getId());
}
GHCommitComment wrap(GHRepository owner) {

View File

@@ -30,6 +30,7 @@ public class GHContent implements Refreshable {
private String sha;
private String name;
private String path;
private String target;
private String content;
private String url; // this is the API url
private String git_url; // this is the Blob url
@@ -99,6 +100,15 @@ public class GHContent implements Refreshable {
return path;
}
/**
* Gets target of a symlink. This will only be set if {@code "symlink".equals(getType())}
*
* @return the target
*/
public String getTarget() {
return target;
}
/**
* Retrieve the decoded content that is stored at this location.
*

View File

@@ -3,6 +3,8 @@ package org.kohsuke.github;
import java.io.IOException;
import java.net.URL;
import static org.kohsuke.github.Previews.BAPTISE;
/**
* Creates a repository
*
@@ -11,7 +13,7 @@ import java.net.URL;
public class GHCreateRepositoryBuilder {
private final GitHub root;
protected final Requester builder;
private final String apiUrlTail;
private String apiUrlTail;
GHCreateRepositoryBuilder(GitHub root, String apiUrlTail, String name) {
this.root = root;
@@ -200,6 +202,51 @@ public class GHCreateRepositoryBuilder {
return this;
}
/**
* Specifies whether the repository is a template.
*
* @param enabled
* true if enabled
* @return a builder to continue with building
*/
@Preview
@Deprecated
public GHCreateRepositoryBuilder templateRepository(boolean enabled) {
this.builder.withPreview(BAPTISE);
this.builder.with("is_template", enabled);
return this;
}
/**
* Specifies the ownership of the repository.
*
* @param owner
* organization or personage
* @return a builder to continue with building
*/
public GHCreateRepositoryBuilder owner(String owner) {
this.builder.with("owner", owner);
return this;
}
/**
* Create repository from template repository.
*
* @param templateOwner
* template repository owner
* @param templateRepo
* template repository
* @return a builder to continue with building
* @see <a href="https://developer.github.com/v3/previews/">GitHub API Previews</a>
*/
@Preview
@Deprecated
public GHCreateRepositoryBuilder fromTemplateRepository(String templateOwner, String templateRepo) {
this.builder.withPreview(BAPTISE);
this.apiUrlTail = "/repos/" + templateOwner + "/" + templateRepo + "/generate";
return this;
}
/**
* Creates a repository with all the parameters.
*

View File

@@ -2,6 +2,7 @@ package org.kohsuke.github;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
/**
* Represents a deployment
@@ -60,7 +61,8 @@ public class GHDeployment extends GHObject {
}
/**
* Gets payload.
* Gets payload. <b>NOTE:</b> only use this method if you can guarantee the payload will be a simple string,
* otherwise use {@link #getPayloadObject()}.
*
* @return the payload
*/
@@ -68,6 +70,25 @@ public class GHDeployment extends GHObject {
return (String) payload;
}
/**
* Gets payload. <b>NOTE:</b> only use this method if you can guarantee the payload will be a JSON object (Map),
* otherwise use {@link #getPayloadObject()}.
*
* @return the payload
*/
public Map<String, Object> getPayloadMap() {
return (Map<String, Object>) payload;
}
/**
* Gets payload without assuming its type. It could be a String or a Map.
*
* @return the payload
*/
public Object getPayloadObject() {
return payload;
}
/**
* Gets environment.
*
@@ -122,7 +143,7 @@ public class GHDeployment extends GHObject {
* @return the gh deployment status builder
*/
public GHDeploymentStatusBuilder createStatus(GHDeploymentState state) {
return new GHDeploymentStatusBuilder(owner, id, state);
return new GHDeploymentStatusBuilder(owner, getId(), state);
}
/**

View File

@@ -0,0 +1,232 @@
package org.kohsuke.github;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.IOException;
import java.net.URL;
import java.util.Objects;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
/**
* A discussion in GitHub Team.
*
* @author Charles Moulliard
* @see <a href="https://developer.github.com/v3/teams/discussions">GitHub Team Discussions</a>
*/
public class GHDiscussion extends GHObject {
@JacksonInject
private GitHub root;
private GHTeam team;
private long number;
private String body, title, htmlUrl;
@JsonProperty(value = "private")
private boolean isPrivate;
@Override
public URL getHtmlUrl() throws IOException {
return GitHubClient.parseURL(htmlUrl);
}
GHDiscussion wrapUp(GHTeam team) {
this.team = team;
return this;
}
/**
* Get the team to which this discussion belongs.
*
* @return the team for this discussion
*/
@Nonnull
public GHTeam getTeam() {
return team;
}
/**
* Get the title of the discussion.
*
* @return the title
*/
public String getTitle() {
return title;
}
/**
* The description of this discussion.
*
* @return the body
*/
public String getBody() {
return body;
}
/**
* The number of this discussion.
*
* @return the number
*/
public long getNumber() {
return number;
}
/**
* The id number of this discussion. GitHub discussions have "number" instead of "id". This is provided for
* convenience.
*
* @return the id number for this discussion
* @see #getNumber()
*/
@Override
public long getId() {
return getNumber();
}
/**
* Whether the discussion is private to the team.
*
* @return {@code true} if discussion is private.
*/
public boolean isPrivate() {
return isPrivate;
}
/**
* Begins the creation of a new instance.
*
* Consumer must call {@link GHDiscussion.Creator#done()} to commit changes.
*
* @param team
* the team in which the discussion will be created.
* @return a {@link GHLabel.Creator}
* @throws IOException
* the io exception
*/
static GHDiscussion.Creator create(GHTeam team) throws IOException {
return new GHDiscussion.Creator(team);
}
static GHDiscussion read(GHTeam team, long discussionNumber) throws IOException {
return team.root.createRequest()
.setRawUrlPath(getRawUrlPath(team, discussionNumber))
.fetch(GHDiscussion.class)
.wrapUp(team);
}
static PagedIterable<GHDiscussion> readAll(GHTeam team) throws IOException {
return team.root.createRequest()
.setRawUrlPath(getRawUrlPath(team, null))
.toIterable(GHDiscussion[].class, item -> item.wrapUp(team));
}
/**
* Begins a batch update
*
* Consumer must call {@link GHDiscussion.Updater#done()} to commit changes.
*
* @return a {@link GHDiscussion.Updater}
*/
@Preview
@Deprecated
public GHDiscussion.Updater update() {
return new GHDiscussion.Updater(this);
}
/**
* Begins a single property update.
*
* @return a {@link GHDiscussion.Setter}
*/
@Preview
@Deprecated
public GHDiscussion.Setter set() {
return new GHDiscussion.Setter(this);
}
/**
* Delete the discussion
*
* @throws IOException
* the io exception
*/
public void delete() throws IOException {
team.root.createRequest().method("DELETE").setRawUrlPath(getRawUrlPath(team, number)).send();
}
private static String getRawUrlPath(@Nonnull GHTeam team, @CheckForNull Long discussionNumber) {
return team.getUrl().toString() + "/discussions" + (discussionNumber == null ? "" : "/" + discussionNumber);
}
/**
* A {@link GHLabelBuilder} that updates a single property per request
*
* {@link #done()} is called automatically after the property is set.
*/
public static class Setter extends GHDiscussionBuilder<GHDiscussion> {
private Setter(@Nonnull GHDiscussion base) {
super(GHDiscussion.class, base.team, base);
requester.method("PATCH").setRawUrlPath(base.getUrl().toString());
}
}
/**
* A {@link GHLabelBuilder} that allows multiple properties to be updated per request.
*
* Consumer must call {@link #done()} to commit changes.
*/
public static class Updater extends GHDiscussionBuilder<Updater> {
private Updater(@Nonnull GHDiscussion base) {
super(GHDiscussion.Updater.class, base.team, base);
requester.method("PATCH").setRawUrlPath(base.getUrl().toString());
}
}
/**
* A {@link GHLabelBuilder} that creates a new {@link GHLabel}
*
* Consumer must call {@link #done()} to create the new instance.
*/
public static class Creator extends GHDiscussionBuilder<Creator> {
private Creator(@Nonnull GHTeam team) {
super(GHDiscussion.Creator.class, team, null);
requester.method("POST").setRawUrlPath(getRawUrlPath(team, null));
}
/**
* Sets whether this discussion is private to this team.
*
* @param value
* privacy of this discussion
* @return either a continuing builder or an updated {@link GHDiscussion}
* @throws IOException
* if there is an I/O Exception
*/
@Nonnull
public Creator private_(boolean value) throws IOException {
return with("private", value);
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GHDiscussion that = (GHDiscussion) o;
return number == that.number && Objects.equals(getUrl(), that.getUrl()) && Objects.equals(team, that.team)
&& Objects.equals(body, that.body) && Objects.equals(title, that.title);
}
@Override
public int hashCode() {
return Objects.hash(team, number, body, title);
}
}

View File

@@ -0,0 +1,80 @@
package org.kohsuke.github;
import java.io.IOException;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
/**
* Base class for creating or updating a discussion.
*
* @param <S>
* Intermediate return type for this builder returned by calls to {@link #with(String, Object)}. If {@link S}
* the same as {@link GHLabel}, this builder will commit changes after each call to
* {@link #with(String, Object)}.
*/
class GHDiscussionBuilder<S> extends AbstractBuilder<GHDiscussion, S> {
private final GHTeam team;
/**
*
* @param intermediateReturnType
* Intermediate return type for this builder returned by calls to {@link #with(String, Object)}. If
* {@link S} the same as {@link GHDiscussion}, this builder will commit changes after each call to
* {@link #with(String, Object)}.
* @param team
* the GitHub team. Updates will be sent to the root of this team.
* @param baseInstance
* instance on which to base this builder. If {@code null} a new instance will be created.
*/
protected GHDiscussionBuilder(@Nonnull Class<S> intermediateReturnType,
@Nonnull GHTeam team,
@CheckForNull GHDiscussion baseInstance) {
super(GHDiscussion.class, intermediateReturnType, team.root, baseInstance);
this.team = team;
if (baseInstance != null) {
requester.with("title", baseInstance.getTitle());
requester.with("body", baseInstance.getBody());
}
}
/**
* Title for this discussion.
*
* @param value
* title of discussion
* @return either a continuing builder or an updated {@link GHDiscussion}
* @throws IOException
* if there is an I/O Exception
*/
@Nonnull
public S title(String value) throws IOException {
return with("title", value);
}
/**
* Body content for this discussion.
*
* @param value
* body of discussion*
* @return either a continuing builder or an updated {@link GHDiscussion}
* @throws IOException
* if there is an I/O Exception
*/
@Nonnull
public S body(String value) throws IOException {
return with("body", value);
}
/**
* {@inheritDoc}
*/
@Nonnull
@Override
public GHDiscussion done() throws IOException {
return super.done().wrapUp(team);
}
}

View File

@@ -62,6 +62,8 @@ public enum GHEvent {
TEAM,
TEAM_ADD,
WATCH,
WORKFLOW_DISPATCH,
WORKFLOW_RUN,
/**
* Special event type that means "every possible event"

View File

@@ -337,7 +337,7 @@ public abstract class GHEventPayload {
installation.wrapUp(root);
List<GHRepository> repositories;
if (action == "added")
if ("added".equals(action))
repositories = repositoriesAdded;
else // action == "removed"
repositories = repositoriesRemoved;

View File

@@ -50,6 +50,30 @@ public class GHGist extends GHObject {
this.owner = root.getUser(owner);
}
/**
* Unlike most other GitHub objects, the id for Gists can be non-numeric, such as "aa5a315d61ae9438b18d". If the id
* is numeric, this method will get it. If id is not numeric, this will throw a runtime
* {@link NumberFormatException}.
*
* @return id of the Gist.
* @deprecated Use {@link #getGistId()} instead.
*/
@Deprecated
@Override
public long getId() {
return Long.parseLong(getGistId());
}
/**
* Gets the id for this Gist. Unlike most other GitHub objects, the id for Gists can be non-numeric, such as
* "aa5a315d61ae9438b18d". This should be used instead of {@link #getId()}.
*
* @return id of this Gist
*/
public String getGistId() {
return this.id;
}
/**
* Gets owner.
*
@@ -97,6 +121,11 @@ public class GHGist extends GHObject {
return git_push_url;
}
/**
* Get the html url.
*
* @return the github html url
*/
public URL getHtmlUrl() {
return GitHubClient.parseURL(html_url);
}

View File

@@ -4,6 +4,8 @@ import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import javax.annotation.Nonnull;
/**
* Builder pattern for creating a new Gist.
*
@@ -59,7 +61,7 @@ public class GHGistBuilder {
* the content
* @return Adds a new file.
*/
public GHGistBuilder file(String fileName, String content) {
public GHGistBuilder file(@Nonnull String fileName, @Nonnull String content) {
files.put(fileName, Collections.singletonMap("content", content));
return this;
}

View File

@@ -1,8 +1,11 @@
package org.kohsuke.github;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.annotation.Nonnull;
/**
* Builder pattern for updating a Gist.
@@ -12,7 +15,7 @@ import java.util.LinkedHashMap;
public class GHGistUpdater {
private final GHGist base;
private final Requester builder;
LinkedHashMap<String, Object> files;
LinkedHashMap<String, Map<String, String>> files;
GHGistUpdater(GHGist base) {
this.base = base;
@@ -32,16 +35,15 @@ public class GHGistUpdater {
* @throws IOException
* the io exception
*/
public GHGistUpdater addFile(String fileName, String content) throws IOException {
public GHGistUpdater addFile(@Nonnull String fileName, @Nonnull String content) throws IOException {
updateFile(fileName, content);
return this;
}
// // This method does not work.
// public GHGistUpdater deleteFile(String fileName) throws IOException {
// files.put(fileName, Collections.singletonMap("filename", null));
// return this;
// }
public GHGistUpdater deleteFile(@Nonnull String fileName) throws IOException {
files.put(fileName, null);
return this;
}
/**
* Rename file gh gist updater.
@@ -54,8 +56,9 @@ public class GHGistUpdater {
* @throws IOException
* the io exception
*/
public GHGistUpdater renameFile(String fileName, String newFileName) throws IOException {
files.put(fileName, Collections.singletonMap("filename", newFileName));
public GHGistUpdater renameFile(@Nonnull String fileName, @Nonnull String newFileName) throws IOException {
Map<String, String> file = files.computeIfAbsent(fileName, d -> new HashMap<>());
file.put("filename", newFileName);
return this;
}
@@ -70,8 +73,31 @@ public class GHGistUpdater {
* @throws IOException
* the io exception
*/
public GHGistUpdater updateFile(String fileName, String content) throws IOException {
files.put(fileName, Collections.singletonMap("content", content));
public GHGistUpdater updateFile(@Nonnull String fileName, @Nonnull String content) throws IOException {
Map<String, String> file = files.computeIfAbsent(fileName, d -> new HashMap<>());
file.put("content", content);
return this;
}
/**
* Update file name and content
*
* @param fileName
* the file name
* @param newFileName
* the new file name
* @param content
* the content
* @return the gh gist updater
* @throws IOException
* the io exception
*/
public GHGistUpdater updateFile(@Nonnull String fileName, @Nonnull String newFileName, @Nonnull String content)
throws IOException {
Map<String, String> file = files.computeIfAbsent(fileName, d -> new HashMap<>());
file.put("content", content);
file.put("filename", newFileName);
files.put(fileName, file);
return this;
}

View File

@@ -37,6 +37,7 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import static org.kohsuke.github.Previews.SQUIRREL_GIRL;
@@ -179,10 +180,12 @@ public class GHIssue extends GHObject implements Reactable {
/**
* Gets api url.
*
* @return the api url
* @return API URL of this object.
* @deprecated use {@link #getUrl()}
*/
@Deprecated
public URL getApiURL() {
return GitHubClient.parseURL(url);
return getUrl();
}
/**
@@ -236,7 +239,7 @@ public class GHIssue extends GHObject implements Reactable {
}
private void editIssue(String key, Object value) throws IOException {
root.createRequest().with(key, value).method("PATCH").withUrlPath(getIssuesApiRoute()).send();
root.createRequest().withNullable(key, value).method("PATCH").withUrlPath(getIssuesApiRoute()).send();
}
/**
@@ -293,9 +296,9 @@ public class GHIssue extends GHObject implements Reactable {
*/
public void setMilestone(GHMilestone milestone) throws IOException {
if (milestone == null) {
editNullable("milestone", null);
editIssue("milestone", null);
} else {
edit("milestone", milestone.getNumber());
editIssue("milestone", milestone.getNumber());
}
}
@@ -570,7 +573,8 @@ public class GHIssue extends GHObject implements Reactable {
protected String getIssuesApiRoute() {
if (owner == null) {
// Issues returned from search to do not have an owner. Attempt to use url.
return StringUtils.prependIfMissing(getUrl().toString().replace(root.getApiUrl(), ""), "/");
final URL url = Objects.requireNonNull(getUrl(), "Missing instance URL!");
return StringUtils.prependIfMissing(url.toString().replace(root.getApiUrl(), ""), "/");
}
return "/repos/" + owner.getOwnerName() + "/" + owner.getName() + "/issues/" + number;
}

View File

@@ -149,6 +149,6 @@ public class GHIssueComment extends GHObject implements Reactable {
private String getApiRoute() {
return "/repos/" + owner.getRepository().getOwnerName() + "/" + owner.getRepository().getName()
+ "/issues/comments/" + id;
+ "/issues/comments/" + getId();
}
}

View File

@@ -5,6 +5,8 @@ import java.util.Date;
/**
* The type GHIssueEvent.
*
* @see <a href="https://developer.github.com/v3/issues/events/">Github documentation for issue events</a>
*
* @author Martin van Zijl
*/
public class GHIssueEvent {
@@ -18,6 +20,9 @@ public class GHIssueEvent {
private String commit_id;
private String commit_url;
private String created_at;
private GHMilestone milestone;
private GHLabel label;
private GHUser assignee;
private GHIssue issue;
@@ -111,6 +116,36 @@ public class GHIssueEvent {
return issue;
}
/**
* Get the {@link GHMilestone} that this issue was added to or removed from. Only present for events "milestoned"
* and "demilestoned", <code>null</code> otherwise.
*
* @return the milestone
*/
public GHMilestone getMilestone() {
return milestone;
}
/**
* Get the {@link GHLabel} that was added to or removed from the issue. Only present for events "labeled" and
* "unlabeled", <code>null</code> otherwise.
*
* @return the label
*/
public GHLabel getLabel() {
return label;
}
/**
* Get the {@link GHUser} that was assigned or unassigned from the issue. Only present for events "assigned" and
* "unassigned", <code>null</code> otherwise.
*
* @return the user
*/
public GHUser getAssignee() {
return assignee;
}
GHIssueEvent wrapUp(GitHub root) {
this.root = root;
return this;

View File

@@ -24,13 +24,13 @@
package org.kohsuke.github;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* The GitHub Preview API's license information
@@ -78,14 +78,6 @@ public class GHLicense extends GHObject {
return name;
}
/**
* @return API URL of this object.
*/
@WithBridgeMethods(value = String.class, adapterMethod = "urlToString")
public URL getUrl() {
return GitHubClient.parseURL(url);
}
/**
* Featured licenses are bold in the new repository drop-down
*
@@ -199,7 +191,14 @@ public class GHLicense extends GHObject {
if (description != null)
return; // already populated
root.createRequest().withUrlPath(url).fetchInto(this);
if (root == null || root.isOffline()) {
return; // cannot populate, will have to live with what we have
}
URL url = getUrl();
if (url != null) {
root.createRequest().setRawUrlPath(url.toString()).fetchInto(this);
}
}
@Override
@@ -210,12 +209,12 @@ public class GHLicense extends GHObject {
return false;
GHLicense that = (GHLicense) o;
return this.url.equals(that.url);
return Objects.equals(getUrl(), that.getUrl());
}
@Override
public int hashCode() {
return url.hashCode();
return Objects.hashCode(getUrl());
}
GHLicense wrap(GitHub root) {

View File

@@ -26,10 +26,12 @@ public abstract class GHObject {
*/
protected transient Map<String, List<String>> responseHeaderFields;
protected String url;
protected long id;
protected String created_at;
protected String updated_at;
private String url;
private long id;
private String nodeId;
private String createdAt;
private String updatedAt;
GHObject() {
}
@@ -74,12 +76,12 @@ public abstract class GHObject {
*/
@WithBridgeMethods(value = String.class, adapterMethod = "createdAtStr")
public Date getCreatedAt() throws IOException {
return GitHubClient.parseDate(created_at);
return GitHubClient.parseDate(createdAt);
}
@SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification = "Bridge method of getCreatedAt")
private Object createdAtStr(Date id, Class type) {
return created_at;
return createdAt;
}
/**
@@ -110,7 +112,18 @@ public abstract class GHObject {
* on error
*/
public Date getUpdatedAt() throws IOException {
return GitHubClient.parseDate(updated_at);
return GitHubClient.parseDate(updatedAt);
}
/**
* Get Global node_id from Github object.
*
* @see <a href="https://developer.github.com/v4/guides/using-global-node-ids/">Using Global Node IDs</a>
*
* @return Global Node ID.
*/
public String getNodeId() {
return nodeId;
}
/**

View File

@@ -22,6 +22,6 @@ class GHOrgHook extends GHHook {
@Override
String getApiRoute() {
return String.format("/orgs/%s/hooks/%d", organization.getLogin(), id);
return String.format("/orgs/%s/hooks/%d", organization.getLogin(), getId());
}
}

View File

@@ -128,6 +128,22 @@ public class GHOrganization extends GHPerson {
.toIterable(GHTeam[].class, item -> item.wrapUp(this));
}
/**
* Gets a single team by ID.
*
* @param teamId
* id of the team that we want to query for
* @return the team
* @throws IOException
* the io exception
*
* @deprecated Use {@link GHOrganization#getTeam(long)}
*/
@Deprecated
public GHTeam getTeam(int teamId) throws IOException {
return getTeam((long) teamId);
}
/**
* Gets a single team by ID.
*
@@ -139,9 +155,9 @@ public class GHOrganization extends GHPerson {
*
* @see <a href= "https://developer.github.com/v3/teams/#get-team-by-name">documentation</a>
*/
public GHTeam getTeam(int teamId) throws IOException {
public GHTeam getTeam(long teamId) throws IOException {
return root.createRequest()
.withUrlPath(String.format("/organizations/%d/team/%d", id, teamId))
.withUrlPath(String.format("/organizations/%d/team/%d", getId(), teamId))
.fetch(GHTeam.class)
.wrapUp(this);
}
@@ -405,7 +421,7 @@ public class GHOrganization extends GHPerson {
* The enum Permission.
*/
public enum Permission {
ADMIN, PUSH, PULL
ADMIN, MAINTAIN, PUSH, TRIAGE, PULL
}
/**

View File

@@ -24,10 +24,10 @@ public abstract class GHPerson extends GHObject {
protected String login, avatar_url;
// other fields (that only show up in full data)
protected String location, blog, email, name, company, type;
protected String location, blog, email, bio, name, company, type, twitter_username;
protected String html_url;
protected int followers, following, public_repos, public_gists;
protected boolean site_admin;
protected boolean site_admin, hireable;
// other fields (that only show up in full data) that require privileged scope
protected Integer total_private_repos;
@@ -46,13 +46,16 @@ public abstract class GHPerson extends GHObject {
* the io exception
*/
protected synchronized void populate() throws IOException {
if (created_at != null) {
if (super.getCreatedAt() != null) {
return; // already populated
}
if (root == null || root.isOffline()) {
return; // cannot populate, will have to live with what we have
}
root.createRequest().withUrlPath(url).fetchInto(this);
URL url = getUrl();
if (url != null) {
root.createRequest().setRawUrlPath(url.toString()).fetchInto(this);
}
}
/**
@@ -151,10 +154,7 @@ public abstract class GHPerson extends GHObject {
*/
public GHRepository getRepository(String name) throws IOException {
try {
return root.createRequest()
.withUrlPath("/repos/" + login + '/' + name)
.fetch(GHRepository.class)
.wrap(root);
return GHRepository.read(root, login, name);
} catch (FileNotFoundException e) {
return null;
}
@@ -234,6 +234,18 @@ public abstract class GHPerson extends GHObject {
return location;
}
/**
* Gets the Twitter Username of this user, like "GitHub"
*
* @return the Twitter username
* @throws IOException
* the io exception
*/
public String getTwitterUsername() throws IOException {
populate();
return twitter_username;
}
public Date getCreatedAt() throws IOException {
populate();
return super.getCreatedAt();

View File

@@ -42,7 +42,6 @@ public class GHProject extends GHObject {
private String owner_url;
private String html_url;
private String node_id;
private String name;
private String body;
private int number;
@@ -81,10 +80,8 @@ public class GHProject extends GHObject {
} else if (owner_url.contains("/users/")) {
owner = root.createRequest().withUrlPath(getOwnerUrl().getPath()).fetch(GHUser.class).wrapUp(root);
} else if (owner_url.contains("/repos/")) {
owner = root.createRequest()
.withUrlPath(getOwnerUrl().getPath())
.fetch(GHRepository.class)
.wrap(root);
String[] pathElements = getOwnerUrl().getPath().split("/");
owner = GHRepository.read(root, pathElements[1], pathElements[2]);
}
} catch (FileNotFoundException e) {
return null;
@@ -105,10 +102,12 @@ public class GHProject extends GHObject {
/**
* Gets node id.
*
* @deprecated Use {@link GHObject#getNodeId()}
* @return the node id
*/
@Deprecated
public String getNode_id() {
return node_id;
return getNodeId();
}
/**
@@ -191,7 +190,7 @@ public class GHProject extends GHObject {
* @return the api route
*/
protected String getApiRoute() {
return "/projects/" + id;
return "/projects/" + getId();
}
/**
@@ -290,7 +289,7 @@ public class GHProject extends GHObject {
final GHProject project = this;
return root.createRequest()
.withPreview(INERTIA)
.withUrlPath(String.format("/projects/%d/columns", id))
.withUrlPath(String.format("/projects/%d/columns", getId()))
.toIterable(GHProjectColumn[].class, item -> item.wrap(project));
}
@@ -308,7 +307,7 @@ public class GHProject extends GHObject {
.method("POST")
.withPreview(INERTIA)
.with("name", name)
.withUrlPath(String.format("/projects/%d/columns", id))
.withUrlPath(String.format("/projects/%d/columns", getId()))
.fetch(GHProjectColumn.class)
.wrap(this);
}

View File

@@ -213,7 +213,7 @@ public class GHProjectCard extends GHObject {
* @return the api route
*/
protected String getApiRoute() {
return String.format("/projects/columns/cards/%d", id);
return String.format("/projects/columns/cards/%d", getId());
}
/**

View File

@@ -115,7 +115,7 @@ public class GHProjectColumn extends GHObject {
* @return the api route
*/
protected String getApiRoute() {
return String.format("/projects/columns/%d", id);
return String.format("/projects/columns/%d", getId());
}
/**
@@ -139,7 +139,7 @@ public class GHProjectColumn extends GHObject {
final GHProjectColumn column = this;
return root.createRequest()
.withPreview(INERTIA)
.withUrlPath(String.format("/projects/columns/%d/cards", id))
.withUrlPath(String.format("/projects/columns/%d/cards", getId()))
.toIterable(GHProjectCard[].class, item -> item.wrap(column));
}
@@ -157,7 +157,7 @@ public class GHProjectColumn extends GHObject {
.method("POST")
.withPreview(INERTIA)
.with("note", note)
.withUrlPath(String.format("/projects/columns/%d/cards", id))
.withUrlPath(String.format("/projects/columns/%d/cards", getId()))
.fetch(GHProjectCard.class)
.wrap(this);
}
@@ -177,7 +177,7 @@ public class GHProjectColumn extends GHObject {
.withPreview(INERTIA)
.with("content_type", issue instanceof GHPullRequest ? "PullRequest" : "Issue")
.with("content_id", issue.getId())
.withUrlPath(String.format("/projects/columns/%d/cards", id))
.withUrlPath(String.format("/projects/columns/%d/cards", getId()))
.fetch(GHProjectCard.class)
.wrap(this);
}

View File

@@ -33,6 +33,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import javax.annotation.CheckForNull;
@@ -103,7 +104,9 @@ public class GHPullRequest extends GHIssue implements Refreshable {
protected String getApiRoute() {
if (owner == null) {
// Issues returned from search to do not have an owner. Attempt to use url.
return StringUtils.prependIfMissing(getUrl().toString().replace(root.getApiUrl(), ""), "/");
final URL url = Objects.requireNonNull(getUrl(), "Missing instance URL!");
return StringUtils.prependIfMissing(url.toString().replace(root.getApiUrl(), ""), "/");
}
return "/repos/" + owner.getOwnerName() + "/" + owner.getName() + "/pulls/" + number;
}
@@ -387,10 +390,14 @@ public class GHPullRequest extends GHIssue implements Refreshable {
* Repopulates this object.
*/
public void refresh() throws IOException {
if (root.isOffline()) {
if (root == null || root.isOffline()) {
return; // cannot populate, will have to live with what we have
}
root.createRequest().withPreview(SHADOW_CAT).withUrlPath(url).fetchInto(this).wrapUp(owner);
URL url = getUrl();
if (url != null) {
root.createRequest().withPreview(SHADOW_CAT).setRawUrlPath(url.toString()).fetchInto(this).wrapUp(owner);
}
}
/**

View File

@@ -111,7 +111,7 @@ public class GHPullRequestReview extends GHObject {
* @return the api route
*/
protected String getApiRoute() {
return owner.getApiRoute() + "/reviews/" + id;
return owner.getApiRoute() + "/reviews/" + getId();
}
/**

View File

@@ -153,7 +153,7 @@ public class GHPullRequestReviewComment extends GHObject implements Reactable {
* @return the api route
*/
protected String getApiRoute() {
return "/repos/" + owner.getRepository().getFullName() + "/pulls/comments/" + id;
return "/repos/" + owner.getRepository().getFullName() + "/pulls/comments/" + getId();
}
/**

View File

@@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.lang3.StringUtils;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
@@ -29,7 +30,7 @@ public class GHRateLimit {
/**
* Remaining calls that can be made.
*
* @deprecated This value should never have been made public. Use {@link #getRemaining()}
* @deprecated This field should never have been made public. Use {@link #getRemaining()}
*/
@Deprecated
public int remaining;
@@ -37,7 +38,7 @@ public class GHRateLimit {
/**
* Allotted API call per hour.
*
* @deprecated This value should never have been made public. Use {@link #getLimit()}
* @deprecated This field should never have been made public. Use {@link #getLimit()}
*/
@Deprecated
public int limit;
@@ -48,7 +49,7 @@ public class GHRateLimit {
* date. To use this field in any meaningful way, it must be converted to a long using {@link Date#getTime()}
* multiplied by 1000.
*
* @deprecated This value should never have been made public. Use {@link #getResetDate()}
* @deprecated This field should never have been made public. Use {@link #getResetDate()}
*/
@Deprecated
public Date reset;
@@ -65,17 +66,58 @@ public class GHRateLimit {
@Nonnull
private final Record integrationManifest;
/**
* The default GHRateLimit provided to new {@link GitHubClient}s.
*
* Contains all expired records that will cause {@link GitHubClient#rateLimit(RateLimitTarget)} to refresh with new
* data when called.
*
* Private, but made internal for testing.
*/
@Nonnull
static GHRateLimit Unknown() {
return new GHRateLimit(new UnknownLimitRecord(),
new UnknownLimitRecord(),
new UnknownLimitRecord(),
new UnknownLimitRecord());
}
static final GHRateLimit DEFAULT = new GHRateLimit(UnknownLimitRecord.DEFAULT,
UnknownLimitRecord.DEFAULT,
UnknownLimitRecord.DEFAULT,
UnknownLimitRecord.DEFAULT);
/**
* Creates a new {@link GHRateLimit} from a single record for the specified endpoint with place holders for other
* records.
*
* This is used to create {@link GHRateLimit} instances that can merged with other instances.
*
* @param record
* the rate limit record. Can be a regular {@link Record} constructed from header information or an
* {@link UnknownLimitRecord} placeholder.
* @param rateLimitTarget
* which rate limit record to fill
* @return a new {@link GHRateLimit} instance containing the supplied record
*/
@Nonnull
static GHRateLimit fromHeaderRecord(Record header) {
return new GHRateLimit(header, new UnknownLimitRecord(), new UnknownLimitRecord(), new UnknownLimitRecord());
static GHRateLimit fromRecord(@Nonnull Record record, @Nonnull RateLimitTarget rateLimitTarget) {
if (rateLimitTarget == RateLimitTarget.CORE || rateLimitTarget == RateLimitTarget.NONE) {
return new GHRateLimit(record,
UnknownLimitRecord.DEFAULT,
UnknownLimitRecord.DEFAULT,
UnknownLimitRecord.DEFAULT);
} else if (rateLimitTarget == RateLimitTarget.SEARCH) {
return new GHRateLimit(UnknownLimitRecord.DEFAULT,
record,
UnknownLimitRecord.DEFAULT,
UnknownLimitRecord.DEFAULT);
} else if (rateLimitTarget == RateLimitTarget.GRAPHQL) {
return new GHRateLimit(UnknownLimitRecord.DEFAULT,
UnknownLimitRecord.DEFAULT,
record,
UnknownLimitRecord.DEFAULT);
} else if (rateLimitTarget == RateLimitTarget.INTEGRATION_MANIFEST) {
return new GHRateLimit(UnknownLimitRecord.DEFAULT,
UnknownLimitRecord.DEFAULT,
UnknownLimitRecord.DEFAULT,
record);
} else {
throw new IllegalArgumentException("Unknown rate limit target: " + rateLimitTarget.toString());
}
}
@JsonCreator
@@ -142,7 +184,7 @@ public class GHRateLimit {
}
/**
* Whether the rate limit reset date for this instance has passed.
* Whether the reset date for the Core API rate limit has passed.
*
* @return true if the rate limit reset date has passed. Otherwise false.
* @since 1.100
@@ -152,7 +194,7 @@ public class GHRateLimit {
}
/**
* The core object provides your rate limit status for all non-search-related resources in the REST API.
* The core object provides the rate limit status for all non-search-related resources in the REST API.
*
* @return a rate limit record
* @since 1.100
@@ -163,42 +205,43 @@ public class GHRateLimit {
}
/**
* The search object provides your rate limit status for the Search API. TODO: integrate with header limit updating.
* Issue #605.
* The search record provides the rate limit status for the Search API.
*
* @return a rate limit record
* @since 1.115
*/
@Nonnull
Record getSearch() {
public Record getSearch() {
return search;
}
/**
* The graphql object provides your rate limit status for the GraphQL API. TODO: integrate with header limit
* updating. Issue #605.
* The graphql record provides the rate limit status for the GraphQL API.
*
* @return a rate limit record
* @since 1.115
*/
@Nonnull
Record getGraphQL() {
public Record getGraphQL() {
return graphql;
}
/**
* The integration_manifest object provides your rate limit status for the GitHub App Manifest code conversion
* endpoint. TODO: integrate with header limit updating. Issue #605.
* The integration manifest record provides the rate limit status for the GitHub App Manifest code conversion
* endpoint.
*
* @return a rate limit record
* @since 1.115
*/
@Nonnull
Record getIntegrationManifest() {
public Record getIntegrationManifest() {
return integrationManifest;
}
@Override
public String toString() {
return "GHRateLimit {" + "core " + getCore().toString() + "search " + getSearch().toString() + "graphql "
+ getGraphQL().toString() + "integrationManifest " + getIntegrationManifest().toString() + '}';
return "GHRateLimit {" + "core " + getCore().toString() + ", search " + getSearch().toString() + ", graphql "
+ getGraphQL().toString() + ", integrationManifest " + getIntegrationManifest().toString() + "}";
}
@Override
@@ -221,44 +264,111 @@ public class GHRateLimit {
}
/**
* Gets the appropriate {@link Record} for a particular url path.
*
* @param urlPath
* the url path of the request
* @return the {@link Record} for a url path.
* Merge a {@link GHRateLimit} with another one to create a new {@link GHRateLimit} keeping the latest
* {@link Record}s from each.
*
* @param newLimit
* {@link GHRateLimit} with potentially updated {@link Record}s.
* @return a merged {@link GHRateLimit} with the latest {@link Record}s from these two instances. If the merged
* instance is equal to the current instance, the current instance is returned.
*/
@Nonnull
Record getRecordForUrlPath(@Nonnull String urlPath) {
if (urlPath.equals("/rate_limit")) {
return new UnknownLimitRecord();
} else if (urlPath.startsWith("/search")) {
return getSearch();
} else if (urlPath.startsWith("/graphql")) {
return getGraphQL();
} else if (urlPath.startsWith("/app-manifests")) {
return getIntegrationManifest();
} else {
GHRateLimit getMergedRateLimit(@Nonnull GHRateLimit newLimit) {
GHRateLimit merged = new GHRateLimit(getCore().currentOrUpdated(newLimit.getCore()),
getSearch().currentOrUpdated(newLimit.getSearch()),
getGraphQL().currentOrUpdated(newLimit.getGraphQL()),
getIntegrationManifest().currentOrUpdated(newLimit.getIntegrationManifest()));
if (merged.equals(this)) {
merged = this;
}
return merged;
}
/**
* Gets the specified {@link Record}.
*
* {@link RateLimitTarget#NONE} will return {@link UnknownLimitRecord#DEFAULT} to prevent any clients from
* accidentally waiting on that record to reset before continuing.
*
* @param rateLimitTarget
* the target rate limit record
* @return the target {@link Record} from this instance.
*/
@Nonnull
Record getRecord(@Nonnull RateLimitTarget rateLimitTarget) {
if (rateLimitTarget == RateLimitTarget.CORE) {
return getCore();
} else if (rateLimitTarget == RateLimitTarget.SEARCH) {
return getSearch();
} else if (rateLimitTarget == RateLimitTarget.GRAPHQL) {
return getGraphQL();
} else if (rateLimitTarget == RateLimitTarget.INTEGRATION_MANIFEST) {
return getIntegrationManifest();
} else if (rateLimitTarget == RateLimitTarget.NONE) {
return UnknownLimitRecord.DEFAULT;
} else {
throw new IllegalArgumentException("Unknown rate limit target: " + rateLimitTarget.toString());
}
}
/**
* A limit record used as a placeholder when the the actual limit is not known.
* <p>
* Has a large limit and long duration so that it will doesn't expire too often.
*
* @since 1.100
*/
public static class UnknownLimitRecord extends Record {
// One hour
private static final long unknownLimitResetSeconds = 60L * 60L;
private static final long defaultUnknownLimitResetSeconds = Duration.ofSeconds(30).getSeconds();
/**
* The number of seconds until a {@link UnknownLimitRecord} will expire.
*
* This is set to a somewhat short duration, rather than a long one. This avoids
* {@link {@link GitHubClient#rateLimit(RateLimitTarget)}} requesting rate limit updates continuously, but also
* avoids holding on to stale unknown records indefinitely.
*
* When merging {@link GHRateLimit} instances, {@link UnknownLimitRecord}s will be superseded by incoming
* regular {@link Record}s.
*
* @see GHRateLimit#getMergedRateLimit(GHRateLimit)
*/
static long unknownLimitResetSeconds = defaultUnknownLimitResetSeconds;
static final int unknownLimit = 1000000;
static final int unknownRemaining = 999999;
private UnknownLimitRecord() {
super(unknownLimit, unknownRemaining, System.currentTimeMillis() / 1000L + unknownLimitResetSeconds);
// The default UnknownLimitRecord is an expired record.
private static final UnknownLimitRecord DEFAULT = new UnknownLimitRecord(Long.MIN_VALUE);
// The starting current UnknownLimitRecord is an expired record.
private static UnknownLimitRecord current = DEFAULT;
/**
* Create a new unknown record that resets at the specified time.
*
* @param resetEpochSeconds
* the epoch second time when this record will expire.
*/
private UnknownLimitRecord(long resetEpochSeconds) {
super(unknownLimit, unknownRemaining, resetEpochSeconds);
}
static synchronized Record current() {
if (current.isExpired()) {
current = new UnknownLimitRecord(System.currentTimeMillis() / 1000L + unknownLimitResetSeconds);
}
return current;
}
/**
* Reset the current UnknownLimitRecord. For use during testing only.
*/
static synchronized void reset() {
current = DEFAULT;
unknownLimitResetSeconds = defaultUnknownLimitResetSeconds;
}
}
@@ -274,14 +384,12 @@ public class GHRateLimit {
private final int remaining;
/**
* Allotted API call per hour.
* Allotted API call per time period.
*/
private final int limit;
/**
* The time at which the current rate limit window resets in UTC epoch seconds.
*
* This is the raw value returned by the server.
*/
private final long resetEpochSeconds;
@@ -291,9 +399,11 @@ public class GHRateLimit {
private final long createdAtEpochSeconds = System.currentTimeMillis() / 1000;
/**
* The time at which the rate limit will reset. This value is calculated based on
* {@link #getResetEpochSeconds()} by calling {@link #calculateResetDate}. If the clock on the local machine not
* synchronized with the server clock, this time value will be adjusted to match the local machine's clock.
* The date at which the rate limit will reset, adjusted to local machine time if the local machine's clock not
* synchronized with to the same clock as the GitHub server.
*
* @see #calculateResetDate(String)
* @see #getResetDate()
*/
@Nonnull
private final Date resetDate;
@@ -341,12 +451,58 @@ public class GHRateLimit {
this.resetDate = calculateResetDate(updatedAt);
}
/**
* Determine if the current {@link Record} is outdated compared to another. Rate Limit dates are only accurate
* to the second, so we look at other information in the record as well.
*
* {@link Record}s with earlier {@link #getResetEpochSeconds()} are replaced by those with later.
* {@link Record}s with the same {@link #getResetEpochSeconds()} are replaced by those with less remaining
* count.
*
* {@link UnknownLimitRecord}s compare with each other like regular {@link Record}s.
*
* {@link Record}s are replaced by {@link UnknownLimitRecord}s only when the current {@link Record} is expired
* and the {@link UnknownLimitRecord} is not. Otherwise Regular {@link Record}s are not replaced by
* {@link UnknownLimitRecord}s.
*
* Expiration is only considered after other checks, meaning expired records may sometimes be replaced by other
* expired records.
*
* @param other
* the other {@link Record}
* @return the {@link Record} that is most current
*/
Record currentOrUpdated(@Nonnull Record other) {
// This set of checks avoids most calls to isExpired()
// Depends on UnknownLimitRecord.current() to prevent continuous updating of GHRateLimit rateLimit()
if (getResetEpochSeconds() > other.getResetEpochSeconds()
|| (getResetEpochSeconds() == other.getResetEpochSeconds()
&& getRemaining() <= other.getRemaining())) {
// If the current record has a later reset
// or the current record has the same reset and fewer or same requests remaining
// Then it is most recent
return this;
} else if (!(other instanceof UnknownLimitRecord)) {
// If the above is not the case that means other has a later reset
// or the same resent and fewer requests remaining.
// If the other record is not an unknown record, the the other is more recent
return other;
} else if (this.isExpired() && !other.isExpired()) {
// The other is an unknown record.
// If the current record has expired and the other hasn't, return the other.
return other;
}
// If none of the above, the current record is most valid.
return this;
}
/**
* Recalculates the {@link #resetDate} relative to the local machine clock.
* <p>
* {@link RateLimitChecker}s and {@link RateLimitHandler}s use {@link #getResetDate()} to make decisions about
* how long to wait for until for the rate limit to reset. That means that {@link #getResetDate()} needs to be
* accurate to the local machine.
* calculated based on the local machine clock.
* </p>
* <p>
* When we say that the clock on two machines is "synchronized", we mean that the UTC time returned from
@@ -415,7 +571,7 @@ public class GHRateLimit {
* {@link #getResetDate()} or implement a {@link RateLimitChecker} instead.
*
* @return a long representing the time in epoch seconds when the rate limit will reset
* @see #getResetDate() #getResetDate()
* @see #getResetDate()
*/
public long getResetEpochSeconds() {
return resetEpochSeconds;
@@ -424,6 +580,8 @@ public class GHRateLimit {
/**
* Whether the rate limit reset date indicated by this instance is expired
*
* If attempting to wait for the rate limit to reset, consider implementing a {@link RateLimitChecker} instead.
*
* @return true if the rate limit reset date has passed. Otherwise false.
*/
public boolean isExpired() {
@@ -431,8 +589,8 @@ public class GHRateLimit {
}
/**
* Returns the date at which the rate limit will reset, adjusted to local machine time if the local machine's
* clock not synchronized with to the same clock as the GitHub server.
* The date at which the rate limit will reset, adjusted to local machine time if the local machine's clock not
* synchronized with to the same clock as the GitHub server.
*
* If attempting to wait for the rate limit to reset, consider implementing a {@link RateLimitChecker} instead.
*

View File

@@ -58,6 +58,6 @@ public class GHReaction extends GHObject {
* the io exception
*/
public void delete() throws IOException {
root.createRequest().method("DELETE").withPreview(SQUIRREL_GIRL).withUrlPath("/reactions/" + id).send();
root.createRequest().method("DELETE").withPreview(SQUIRREL_GIRL).withUrlPath("/reactions/" + getId()).send();
}
}

View File

@@ -1,5 +1,6 @@
package org.kohsuke.github;
import com.fasterxml.jackson.databind.JsonMappingException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
@@ -90,6 +91,78 @@ public class GHRef {
return this;
}
/**
* Retrive a ref of the given type for the current GitHub repository.
*
* @param repository
* the repository to read from
* @param refName
* eg: heads/branch
* @return refs matching the request type
* @throws IOException
* on failure communicating with GitHub, potentially due to an invalid ref type being requested
*/
static GHRef read(GHRepository repository, String refName) throws IOException {
// Also accept e.g. "refs/heads/branch" for consistency with createRef().
if (refName.startsWith("refs/")) {
refName = refName.replaceFirst("refs/", "");
}
// We would expect this to use `git/ref/%s` but some versions of GHE seem to not support it
// Instead use `git/refs/%s` and check the result actually matches the ref
GHRef result = null;
try {
result = repository.root.createRequest()
.withUrlPath(repository.getApiTailUrl(String.format("git/refs/%s", refName)))
.fetch(GHRef.class)
.wrap(repository.root);
} catch (IOException e) {
// If the parse exception is due to the above returning an array instead of a single ref
// that means the individual ref did not exist. Handled by result check below.
// Otherwise, rethrow.
if (!(e.getCause() instanceof JsonMappingException)) {
throw e;
}
}
// Verify that the ref returned is the one requested
// Used .endsWith(refName) instead of .equals("refs/" + refName) to workaround a GitBucket
// issue where the "ref" field omits the "refs/" prefix. "endsWith()" is functionally
// the same for this scenario - the server refs matching is prefix-based, so
// a ref that ends with the correct string will always be the correct one.
if (result == null || !result.getRef().endsWith(refName)) {
throw new GHFileNotFoundException(String.format("git/refs/%s", refName)
+ " {\"message\":\"Not Found\",\"documentation_url\":\"https://developer.github.com/v3/git/refs/#get-a-reference\"}");
}
return result;
}
/**
* Retrieves all refs of the given type for the current GitHub repository.
*
* @param repository
* the repository to read from
* @param refType
* the type of reg to search for e.g. <code>tags</code> or <code>commits</code>
* @return paged iterable of all refs of the specified type
* @throws IOException
* on failure communicating with GitHub, potentially due to an invalid ref type being requested
*/
static PagedIterable<GHRef> readMatching(GHRepository repository, String refType) throws IOException {
if (refType.startsWith("refs/")) {
refType = refType.replaceFirst("refs/", "");
}
String url = repository.getApiTailUrl(String.format("git/refs/%s", refType));
// if no types, do not end with slash just to be safe.
if (refType.equals("")) {
url = url.substring(0, url.length() - 1);
}
return repository.root.createRequest()
.withUrlPath(url)
.toIterable(GHRef[].class, item -> item.wrap(repository.root));
}
/**
* The type GHObject.
*/

View File

@@ -270,7 +270,7 @@ public class GHRelease extends GHObject {
* the io exception
*/
public void delete() throws IOException {
root.createRequest().method("DELETE").withUrlPath(owner.getApiTailUrl("releases/" + id)).send();
root.createRequest().method("DELETE").withUrlPath(owner.getApiTailUrl("releases/" + getId())).send();
}
/**
@@ -283,6 +283,6 @@ public class GHRelease extends GHObject {
}
private String getApiTailUrl(String end) {
return owner.getApiTailUrl(format("releases/%s/%s", id, end));
return owner.getApiTailUrl(format("releases/%s/%s", getId(), end));
}
}

View File

@@ -100,7 +100,7 @@ public class GHReleaseUpdater {
*/
public GHRelease update() throws IOException {
return builder.method("PATCH")
.withUrlPath(base.owner.getApiTailUrl("releases/" + base.id))
.withUrlPath(base.owner.getApiTailUrl("releases/" + base.getId()))
.fetch(GHRelease.class)
.wrap(base.owner);
}

View File

@@ -18,6 +18,6 @@ class GHRepoHook extends GHHook {
@Override
String getApiRoute() {
return String.format("/repos/%s/%s/hooks/%d", repository.getOwnerName(), repository.getName(), id);
return String.format("/repos/%s/%s/hooks/%d", repository.getOwnerName(), repository.getName(), getId());
}
}

View File

@@ -24,6 +24,7 @@
package org.kohsuke.github;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParseException;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
@@ -45,8 +46,10 @@ import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.WeakHashMap;
@@ -66,7 +69,9 @@ public class GHRepository extends GHObject {
/* package almost final */ transient GitHub root;
private String nodeId, description, homepage, name, full_name;
private String html_url; // this is the UI
/*
* The license information makes use of the preview API.
*
@@ -75,22 +80,30 @@ public class GHRepository extends GHObject {
private GHLicense license;
private String git_url, ssh_url, clone_url, svn_url, mirror_url;
private GHUser owner; // not fully populated. beware.
private boolean has_issues, has_wiki, fork, has_downloads, has_pages, archived, has_projects;
private boolean allow_squash_merge;
private boolean allow_merge_commit;
private boolean allow_rebase_merge;
private boolean delete_branch_on_merge;
@JsonProperty("private")
private boolean _private;
private int forks_count, stargazers_count, watchers_count, size, open_issues_count, subscribers_count;
private String pushed_at;
private Map<Integer, GHMilestone> milestones = new WeakHashMap<Integer, GHMilestone>();
private String default_branch, language;
private Map<String, GHCommit> commits = new WeakHashMap<String, GHCommit>();
@SkipFromToString
@@ -98,6 +111,12 @@ public class GHRepository extends GHObject {
private GHRepository source, parent;
private Boolean isTemplate;
static GHRepository read(GitHub root, String owner, String name) throws IOException {
return root.createRequest().withUrlPath("/repos/" + owner + '/' + name).fetch(GHRepository.class).wrap(root);
}
/**
* Create deployment gh deployment builder.
*
@@ -679,6 +698,28 @@ public class GHRepository extends GHObject {
return _private;
}
/**
* Is template boolean.
*
* @return the boolean
*/
@Deprecated
@Preview
public boolean isTemplate() {
// isTemplate is still in preview, we do not want to retrieve it unless needed.
if (isTemplate == null) {
try {
populate();
} catch (IOException e) {
// Convert this to a runtime exception to avoid messy method signature
throw new GHException("Could not populate the template setting of the repository", e);
}
// if this somehow is not populated, set it to false;
isTemplate = Boolean.TRUE.equals(isTemplate);
}
return isTemplate;
}
/**
* Has downloads boolean.
*
@@ -968,12 +1009,12 @@ public class GHRepository extends GHObject {
@NonNull String method,
@CheckForNull GHOrganization.Permission permission) throws IOException {
Requester requester = root.createRequest().method(method);
if (permission != null) {
requester = requester.with("permission", permission).inBody();
}
for (GHUser user : users) {
// Make sure that the users collection doesn't have any duplicates
for (GHUser user : new LinkedHashSet<GHUser>(users)) {
requester.withUrlPath(getApiTailUrl("collaborators/" + user.getLogin())).send();
}
}
@@ -1000,8 +1041,9 @@ public class GHRepository extends GHObject {
private void edit(String key, String value) throws IOException {
Requester requester = root.createRequest();
if (!key.equals("name"))
if (!key.equals("name")) {
requester.with("name", name); // even when we don't change the name, we need to send it in
}
requester.with(key, value).method("PATCH").withUrlPath(getApiTailUrl("")).send();
}
@@ -1246,8 +1288,9 @@ public class GHRepository extends GHObject {
// this API is asynchronous. we need to wait for a bit
for (int i = 0; i < 10; i++) {
GHRepository r = root.getMyself().getRepository(name);
if (r != null)
if (r != null) {
return r;
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
@@ -1276,8 +1319,9 @@ public class GHRepository extends GHObject {
// this API is asynchronous. we need to wait for a bit
for (int i = 0; i < 10; i++) {
GHRepository r = org.getRepository(name);
if (r != null)
if (r != null) {
return r;
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
@@ -1538,8 +1582,7 @@ public class GHRepository extends GHObject {
* on failure communicating with GitHub, potentially due to an invalid ref type being requested
*/
public PagedIterable<GHRef> listRefs() throws IOException {
final String url = String.format("/repos/%s/%s/git/refs", getOwnerName(), name);
return root.createRequest().withUrlPath(url).toIterable(GHRef[].class, item -> item.wrap(root));
return listRefs("");
}
/**
@@ -1565,8 +1608,7 @@ public class GHRepository extends GHObject {
* on failure communicating with GitHub, potentially due to an invalid ref type being requested
*/
public PagedIterable<GHRef> listRefs(String refType) throws IOException {
final String url = String.format("/repos/%s/%s/git/refs/%s", getOwnerName(), name, refType);
return root.createRequest().withUrlPath(url).toIterable(GHRef[].class, item -> item.wrap(root));
return GHRef.readMatching(this, refType);
}
/**
@@ -1579,15 +1621,7 @@ public class GHRepository extends GHObject {
* on failure communicating with GitHub, potentially due to an invalid ref type being requested
*/
public GHRef getRef(String refName) throws IOException {
// Also accept e.g. "refs/heads/branch" for consistency with createRef().
if (refName.startsWith("refs/")) {
refName = refName.replaceFirst("refs/", "");
}
return root.createRequest()
.withUrlPath(getApiTailUrl(String.format("git/refs/%s", refName)))
.fetch(GHRef.class)
.wrap(root);
return GHRef.read(this, refName);
}
/**
@@ -2158,6 +2192,12 @@ public class GHRepository extends GHObject {
if (root.isOffline() && owner != null) {
owner.wrapUp(root);
}
if (source != null) {
source.wrap(root);
}
if (parent != null) {
parent.wrap(root);
}
return this;
}
@@ -2477,10 +2517,13 @@ public class GHRepository extends GHObject {
* @see #getParent() #getParent()
*/
public GHRepository getSource() throws IOException {
if (source == null)
if (fork && source == null) {
populate();
}
if (source == null) {
return null;
if (source.root == null)
source = root.getRepository(source.getFullName());
}
return source;
}
@@ -2495,10 +2538,13 @@ public class GHRepository extends GHObject {
* @see #getSource() #getSource()
*/
public GHRepository getParent() throws IOException {
if (parent == null)
if (fork && parent == null) {
populate();
}
if (parent == null) {
return null;
if (parent.root == null)
parent = root.getRepository(parent.getFullName());
}
return parent;
}
@@ -2717,8 +2763,9 @@ public class GHRepository extends GHObject {
}
String getApiTailUrl(String tail) {
if (tail.length() > 0 && !tail.startsWith("/"))
if (tail.length() > 0 && !tail.startsWith("/")) {
tail = '/' + tail;
}
return "/repos/" + getOwnerName() + "/" + name + tail;
}
@@ -2825,9 +2872,24 @@ public class GHRepository extends GHObject {
* The IO exception
*/
void populate() throws IOException {
if (root.isOffline())
if (root.isOffline()) {
return; // can't populate if the root is offline
}
root.createRequest().withApiUrl(root.getApiUrl() + full_name).fetchInto(this).wrap(root);
final URL url = Objects.requireNonNull(getUrl(), "Missing instance URL!");
try {
// IMPORTANT: the url for repository records is does not reliably point to the API url.
// There is bug in Push event payloads that returns the wrong url.
// All other occurrences of "url" take the form "https://api.github.com/...".
// For Push event repository records, they take the form "https://github.com/{fullName}".
root.createRequest().withPreview(BAPTISE).setRawUrlPath(url.toString()).fetchInto(this).wrap(root);
} catch (HttpException e) {
if (e.getCause() instanceof JsonParseException) {
root.createRequest().withPreview(BAPTISE).withUrlPath("/repos/" + full_name).fetchInto(this).wrap(root);
} else {
throw e;
}
}
}
}

View File

@@ -1,12 +1,13 @@
package org.kohsuke.github;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
@@ -315,21 +316,12 @@ public class GHRepositoryStatistics {
* the io exception
*/
public List<CodeFrequency> getCodeFrequency() throws IOException {
// Map to arrays first, since there are no field names in the
// returned JSON.
try {
Integer[][] list = root.createRequest()
CodeFrequency[] list = root.createRequest()
.withUrlPath(getApiTailUrl("code_frequency"))
.fetch(Integer[][].class);
.fetch(CodeFrequency[].class);
// Convert to proper objects.
List<CodeFrequency> returnList = new ArrayList<>();
for (Integer[] item : list) {
CodeFrequency cf = new CodeFrequency(Arrays.asList(item));
returnList.add(cf);
}
return returnList;
return Arrays.asList(list);
} catch (MismatchedInputException e) {
// This sometimes happens when retrieving code frequency statistics
// for a repository for the first time. It is probably still being
@@ -342,10 +334,12 @@ public class GHRepositoryStatistics {
* The type CodeFrequency.
*/
public static class CodeFrequency {
private int week;
private int additions;
private int deletions;
private final int week;
private final int additions;
private final int deletions;
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
private CodeFrequency(List<Integer> item) {
week = item.get(0);
additions = item.get(1);
@@ -428,7 +422,7 @@ public class GHRepositoryStatistics {
* @return The list of commit counts for everyone combined, for the last 52 weeks.
*/
public List<Integer> getAllCommits() {
return all;
return Collections.unmodifiableList(all);
}
/**
@@ -437,7 +431,7 @@ public class GHRepositoryStatistics {
* @return The list of commit counts for the owner, for the last 52 weeks.
*/
public List<Integer> getOwnerCommits() {
return owner;
return Collections.unmodifiableList(owner);
}
Participation wrapUp(GitHub root) {
@@ -455,28 +449,22 @@ public class GHRepositoryStatistics {
* the io exception
*/
public List<PunchCardItem> getPunchCard() throws IOException {
// Map to ArrayLists first, since there are no field names in the
// returned JSON.
Integer[][] list = root.createRequest().withUrlPath(getApiTailUrl("punch_card")).fetch(Integer[][].class);
// Convert to proper objects.
ArrayList<PunchCardItem> returnList = new ArrayList<>();
for (Integer[] item : list) {
PunchCardItem pci = new PunchCardItem(Arrays.asList(item));
returnList.add(pci);
}
return returnList;
PunchCardItem[] list = root.createRequest()
.withUrlPath(getApiTailUrl("punch_card"))
.fetch(PunchCardItem[].class);
return Arrays.asList(list);
}
/**
* The type PunchCardItem.
*/
public static class PunchCardItem {
private int dayOfWeek;
private int hourOfDay;
private int numberOfCommits;
private final int dayOfWeek;
private final int hourOfDay;
private final int numberOfCommits;
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
private PunchCardItem(List<Integer> item) {
dayOfWeek = item.get(0);
hourOfDay = item.get(1);

View File

@@ -25,6 +25,7 @@ public abstract class GHSearchBuilder<T> extends GHQueryBuilder<T> {
super(root);
this.receiverType = receiverType;
req.withUrlPath(getApiUrl());
req.rateLimit(RateLimitTarget.SEARCH);
}
/**

View File

@@ -1,23 +1,27 @@
package org.kohsuke.github;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import javax.annotation.Nonnull;
/**
* A team in GitHub organization.
*
* @author Kohsuke Kawaguchi
*/
public class GHTeam implements Refreshable {
public class GHTeam extends GHObject implements Refreshable {
private String html_url;
private String name;
private String permission;
private String slug;
private String description;
private Privacy privacy;
private int id;
private GHOrganization organization; // populated by GET /user/teams where Teams+Orgs are returned together
protected /* final */ GitHub root;
@@ -130,12 +134,33 @@ public class GHTeam implements Refreshable {
}
/**
* Gets id.
* Retrieves the discussions.
*
* @return the id
* @return the paged iterable
* @throws IOException
* the io exception
*/
public int getId() {
return id;
@Nonnull
public PagedIterable<GHDiscussion> listDiscussions() throws IOException {
return GHDiscussion.readAll(this);
}
/**
* Gets a single discussion by ID.
*
* @param discussionNumber
* id of the discussion that we want to query for
* @return the discussion
* @throws java.io.FileNotFoundException
* if the discussion does not exist
* @throws IOException
* the io exception
*
* @see <a href= "https://developer.github.com/v3/teams/discussions/#get-a-discussion">documentation</a>
*/
@Nonnull
public GHDiscussion getDiscussion(long discussionNumber) throws IOException {
return GHDiscussion.read(this, discussionNumber);
}
/**
@@ -149,6 +174,19 @@ public class GHTeam implements Refreshable {
return root.createRequest().withUrlPath(api("/members")).toIterable(GHUser[].class, item -> item.wrapUp(root));
}
/**
* Retrieves the teams that are children of this team.
*
* @return the paged iterable
* @throws IOException
* the io exception
*/
public PagedIterable<GHTeam> listChildTeams() throws IOException {
return root.createRequest()
.withUrlPath(api("/teams"))
.toIterable(GHTeam[].class, item -> item.wrapUp(this.organization));
}
/**
* Gets members.
*
@@ -169,7 +207,7 @@ public class GHTeam implements Refreshable {
*/
public boolean hasMember(GHUser user) {
try {
root.createRequest().withUrlPath("/teams/" + id + "/members/" + user.getLogin()).send();
root.createRequest().withUrlPath("/teams/" + getId() + "/members/" + user.getLogin()).send();
return true;
} catch (IOException ignore) {
return false;
@@ -302,7 +340,22 @@ public class GHTeam implements Refreshable {
}
private String api(String tail) {
return "/teams/" + id + tail;
return "/teams/" + getId() + tail;
}
/**
* Begins the creation of a new instance.
*
* Consumer must call {@link GHDiscussion.Creator#done()} to commit changes.
*
* @param title
* title of the discussion to be created
* @return a {@link GHDiscussion.Creator}
* @throws IOException
* the io exception
*/
public GHDiscussion.Creator createDiscussion(String title) throws IOException {
return GHDiscussion.create(this).title(title);
}
/**
@@ -321,4 +374,28 @@ public class GHTeam implements Refreshable {
public void refresh() throws IOException {
root.createRequest().withUrlPath(api("")).fetchInto(this).wrapUp(root);
}
@Override
public URL getHtmlUrl() {
return GitHubClient.parseURL(html_url);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GHTeam ghTeam = (GHTeam) o;
return Objects.equals(name, ghTeam.name) && Objects.equals(getUrl(), ghTeam.getUrl())
&& Objects.equals(permission, ghTeam.permission) && Objects.equals(slug, ghTeam.slug)
&& Objects.equals(description, ghTeam.description) && privacy == ghTeam.privacy;
}
@Override
public int hashCode() {
return Objects.hash(name, getUrl(), permission, slug, description, privacy);
}
}

View File

@@ -75,7 +75,7 @@ public class GHTeamBuilder {
* parentTeamId of team
* @return a builder to continue with building
*/
public GHTeamBuilder parentTeamId(int parentTeamId) {
public GHTeamBuilder parentTeamId(long parentTeamId) {
this.builder.with("parent_team_id", parentTeamId);
return this;
}

View File

@@ -173,6 +173,19 @@ public class GHUser extends GHPerson {
return org.hasPublicMember(this);
}
/**
* Returns true if this user is marked as hireable, false otherwise
*
* @return if the user is marked as hireable
*/
public boolean isHireable() {
return hireable;
}
public String getBio() {
return bio;
}
static GHUser[] wrap(GHUser[] users, GitHub root) {
for (GHUser f : users)
f.root = root;

View File

@@ -373,12 +373,20 @@ public class GitHub {
}
/**
* Gets the current rate limit.
* Gets the current full rate limit information from the server.
*
* For some versions of GitHub Enterprise, the {@code /rate_limit} endpoint returns a {@code 404 Not Found}. In that
* case, the most recent {@link GHRateLimit} information will be returned, including rate limit information returned
* in the response header for this request in if was present.
*
* For most use cases it would be better to implement a {@link RateLimitChecker} and add it via
* {@link GitHubBuilder#withRateLimitChecker(RateLimitChecker)}.
*
* @return the rate limit
* @throws IOException
* the io exception
*/
@Nonnull
public GHRateLimit getRateLimit() throws IOException {
return client.getRateLimit();
}
@@ -388,8 +396,11 @@ public class GitHub {
* GitHub Enterprise) or if no requests have been made.
*
* @return the most recently observed rate limit data or {@code null}.
* @deprecated implement a {@link RateLimitChecker} and add it via
* {@link GitHubBuilder#withRateLimitChecker(RateLimitChecker)}.
*/
@CheckForNull
@Nonnull
@Deprecated
public GHRateLimit lastRateLimit() {
return client.lastRateLimit();
}
@@ -400,10 +411,13 @@ public class GitHub {
* @return the current rate limit data.
* @throws IOException
* if we couldn't get the current rate limit data.
* @deprecated implement a {@link RateLimitChecker} and add it via
* {@link GitHubBuilder#withRateLimitChecker(RateLimitChecker)}.
*/
@Nonnull
@Deprecated
public GHRateLimit rateLimit() throws IOException {
return client.rateLimit();
return client.rateLimit(RateLimitTarget.CORE);
}
/**
@@ -413,7 +427,7 @@ public class GitHub {
* @throws IOException
* the io exception
*/
@WithBridgeMethods(GHUser.class)
@WithBridgeMethods(value = GHUser.class)
public GHMyself getMyself() throws IOException {
client.requireCredential();
synchronized (this) {
@@ -518,7 +532,7 @@ public class GitHub {
}
/**
* Gets the repository object from 'user/reponame' string that GitHub calls as "repository name"
* Gets the repository object from 'owner/repo' string that GitHub calls as "repository name"
*
* @param name
* the name
@@ -529,9 +543,10 @@ public class GitHub {
*/
public GHRepository getRepository(String name) throws IOException {
String[] tokens = name.split("/");
return createRequest().withUrlPath("/repos/" + tokens[0] + '/' + tokens[1])
.fetch(GHRepository.class)
.wrap(this);
if (tokens.length < 2) {
throw new IllegalArgumentException("Repository name must be in format owner/repo");
}
return GHRepository.read(this, tokens[0], tokens[1]);
}
/**
@@ -727,7 +742,7 @@ public class GitHub {
* @throws IOException
* the io exception
*
* @deprecated Use {@link GHOrganization#getTeam(int)}
* @deprecated Use {@link GHOrganization#getTeam(long)}
* @see <a href= "https://developer.github.com/v3/teams/#get-team-legacy">deprecation notice</a>
*/
@Deprecated

View File

@@ -9,7 +9,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Properties;
@@ -359,6 +358,18 @@ public class GitHubBuilder implements Cloneable {
return this;
}
/**
* Adds a {@link RateLimitChecker} for the Core API for this {@link GitHubBuilder}.
*
* @param coreRateLimitChecker
* the {@link RateLimitChecker} for core GitHub API requests
* @return the git hub builder
* @see #withRateLimitChecker(RateLimitChecker, RateLimitTarget)
*/
public GitHubBuilder withRateLimitChecker(@Nonnull RateLimitChecker coreRateLimitChecker) {
return withRateLimitChecker(coreRateLimitChecker, RateLimitTarget.CORE);
}
/**
* Adds a {@link RateLimitChecker} to this {@link GitHubBuilder}.
* <p>
@@ -377,15 +388,15 @@ public class GitHubBuilder implements Cloneable {
* request.
* </p>
*
* @param coreRateLimitChecker
* the {@link RateLimitChecker} for core GitHub API requests
* @param rateLimitChecker
* the {@link RateLimitChecker} for requests
* @param rateLimitTarget
* the {@link RateLimitTarget} specifying which rate limit record to check
* @return the git hub builder
*/
public GitHubBuilder withRateLimitChecker(@Nonnull RateLimitChecker coreRateLimitChecker) {
this.rateLimitChecker = new GitHubRateLimitChecker(coreRateLimitChecker,
RateLimitChecker.NONE,
RateLimitChecker.NONE,
RateLimitChecker.NONE);
public GitHubBuilder withRateLimitChecker(@Nonnull RateLimitChecker rateLimitChecker,
@Nonnull RateLimitTarget rateLimitTarget) {
this.rateLimitChecker = this.rateLimitChecker.with(rateLimitChecker, rateLimitTarget);
return this;
}

View File

@@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.io.IOUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -19,15 +19,15 @@ import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TimeZone;
import java.util.function.Consumer;
import java.util.logging.Logger;
@@ -44,7 +44,7 @@ import static java.util.logging.Level.*;
* A GitHub API Client
* <p>
* A GitHubClient can be used to send requests and retrieve their responses. GitHubClient is thread-safe and can be used
* to send multiple requests. GitHubClient also track some GitHub API information such as {@link #rateLimit()}.
* to send multiple requests. GitHubClient also track some GitHub API information such as {@link GHRateLimit}.
* </p>
*/
abstract class GitHubClient {
@@ -70,18 +70,18 @@ abstract class GitHubClient {
private HttpConnector connector;
private final Object headerRateLimitLock = new Object();
private GHRateLimit headerRateLimit = null;
private volatile GHRateLimit rateLimit = null;
private final Object rateLimitLock = new Object();
@Nonnull
private GHRateLimit rateLimit = GHRateLimit.DEFAULT;
private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());
private static final ObjectMapper MAPPER = new ObjectMapper();
static final String GITHUB_URL = "https://api.github.com";
private static final String[] TIME_FORMATS = { "yyyy/MM/dd HH:mm:ss ZZZZ", "yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd'T'HH:mm:ss.S'Z'" // GitHub App endpoints return a different date format
};
private static final DateTimeFormatter DATE_TIME_PARSER_SLASHES = DateTimeFormatter
.ofPattern("yyyy/MM/dd HH:mm:ss Z");
static {
MAPPER.setVisibility(new VisibilityChecker.Std(NONE, NONE, NONE, NONE, ANY));
@@ -141,10 +141,8 @@ abstract class GitHubClient {
}
private <T> T fetch(Class<T> type, String urlPath) throws IOException {
return this
.sendRequest(GitHubRequest.newBuilder().withApiUrl(getApiUrl()).withUrlPath(urlPath).build(),
(responseInfo) -> GitHubResponse.parseBody(responseInfo, type))
.body();
GitHubRequest request = GitHubRequest.newBuilder().withApiUrl(getApiUrl()).withUrlPath(urlPath).build();
return this.sendRequest(request, (responseInfo) -> GitHubResponse.parseBody(responseInfo, type)).body();
}
/**
@@ -208,57 +206,110 @@ abstract class GitHubClient {
}
/**
* Gets the current rate limit from the server.
* Gets the current full rate limit information from the server.
*
* For some versions of GitHub Enterprise, the {@code /rate_limit} endpoint returns a {@code 404 Not Found}. In that
* case, the most recent {@link GHRateLimit} information will be returned, including rate limit information returned
* in the response header for this request in if was present.
*
* For most use cases it would be better to implement a {@link RateLimitChecker} and add it via
* {@link GitHubBuilder#withRateLimitChecker(RateLimitChecker)}.
*
* @return the rate limit
* @throws IOException
* the io exception
*/
@Nonnull
public GHRateLimit getRateLimit() throws IOException {
GHRateLimit rateLimit;
return getRateLimit(RateLimitTarget.NONE);
}
@Nonnull
GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
GHRateLimit result;
try {
rateLimit = fetch(JsonRateLimit.class, "/rate_limit").resources;
GitHubRequest request = GitHubRequest.newBuilder()
.rateLimit(RateLimitTarget.NONE)
.withApiUrl(getApiUrl())
.withUrlPath("/rate_limit")
.build();
result = this
.sendRequest(request, (responseInfo) -> GitHubResponse.parseBody(responseInfo, JsonRateLimit.class))
.body().resources;
} catch (FileNotFoundException e) {
// GitHub Enterprise doesn't have the rate limit
// return a default rate limit that
rateLimit = GHRateLimit.Unknown();
}
// For some versions of GitHub Enterprise, the rate_limit endpoint returns a 404.
LOGGER.log(FINE, "/rate_limit returned 404 Not Found.");
return this.rateLimit = rateLimit;
// However some newer versions of GHE include rate limit header information
// If the header info is missing and the endpoint returns 404, fill the rate limit
// with unknown
result = GHRateLimit.fromRecord(GHRateLimit.UnknownLimitRecord.current(), rateLimitTarget);
}
return updateRateLimit(result);
}
/**
* Returns the most recently observed rate limit data or {@code null} if either there is no rate limit (for example
* GitHub Enterprise) or if no requests have been made.
* Returns the most recently observed rate limit data.
*
* @return the most recently observed rate limit data or {@code null}.
*/
@CheckForNull
public GHRateLimit lastRateLimit() {
synchronized (headerRateLimitLock) {
return headerRateLimit;
}
}
/**
* Gets the current rate limit while trying not to actually make any remote requests unless absolutely necessary.
* Generally, instead of calling this you should implement a {@link RateLimitChecker} or call
*
* @return the current rate limit data.
* @throws IOException
* if we couldn't get the current rate limit data.
* @return the most recently observed rate limit data. This may include expired or
* {@link GHRateLimit.UnknownLimitRecord} entries.
* @deprecated implement a {@link RateLimitChecker} and add it via
* {@link GitHubBuilder#withRateLimitChecker(RateLimitChecker)}.
*/
@Nonnull
public GHRateLimit rateLimit() throws IOException {
synchronized (headerRateLimitLock) {
if (headerRateLimit != null && !headerRateLimit.isExpired()) {
return headerRateLimit;
@Deprecated
GHRateLimit lastRateLimit() {
synchronized (rateLimitLock) {
return rateLimit;
}
}
/**
* Gets the current rate limit for an endpoint while trying not to actually make any remote requests unless
* absolutely necessary.
*
* If the {@link GHRateLimit.Record} for {@code urlPath} is not expired, it is returned. If the
* {@link GHRateLimit.Record} for {@code urlPath} is expired, {@link #getRateLimit()} will be called to get the
* current rate limit.
*
* @param rateLimitTarget
* the endpoint to get the rate limit for.
*
* @return the current rate limit data. {@link GHRateLimit.Record}s in this instance may be expired when returned.
* @throws IOException
* if there was an error getting current rate limit data.
*/
@Nonnull
GHRateLimit rateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
synchronized (rateLimitLock) {
if (rateLimit.getRecord(rateLimitTarget).isExpired()) {
getRateLimit(rateLimitTarget);
}
return rateLimit;
}
GHRateLimit rateLimit = this.rateLimit;
if (rateLimit == null || rateLimit.isExpired()) {
rateLimit = getRateLimit();
}
/**
* Update the Rate Limit with the latest info from response header.
*
* Due to multi-threading, requests might complete out of order. This method calls
* {@link GHRateLimit#getMergedRateLimit(GHRateLimit)} to ensure the most current records are used.
*
* @param observed
* {@link GHRateLimit.Record} constructed from the response header information
*/
private GHRateLimit updateRateLimit(@Nonnull GHRateLimit observed) {
synchronized (rateLimitLock) {
observed = rateLimit.getMergedRateLimit(observed);
if (rateLimit != observed) {
rateLimit = observed;
LOGGER.log(FINE, "Rate limit now: {0}", rateLimit);
}
return rateLimit;
}
return rateLimit;
}
/**
@@ -337,39 +388,43 @@ abstract class GitHubClient {
GitHubResponse.ResponseInfo responseInfo = null;
try {
if (LOGGER.isLoggable(FINE)) {
LOGGER.log(FINE,
"GitHub API request [" + (login == null ? "anonymous" : login) + "]: " + request.method()
+ " " + request.url().toString());
try {
if (LOGGER.isLoggable(FINE)) {
LOGGER.log(FINE,
"GitHub API request [" + (login == null ? "anonymous" : login) + "]: "
+ request.method() + " " + request.url().toString());
}
rateLimitChecker.checkRateLimit(this, request);
responseInfo = getResponseInfo(request);
noteRateLimit(responseInfo);
detectOTPRequired(responseInfo);
if (isInvalidCached404Response(responseInfo)) {
// Setting "Cache-Control" to "no-cache" stops the cache from supplying
// "If-Modified-Since" or "If-None-Match" values.
// This makes GitHub give us current data (not incorrectly cached data)
request = request.toBuilder().setHeader("Cache-Control", "no-cache").build();
continue;
}
if (!(isRateLimitResponse(responseInfo) || isAbuseLimitResponse(responseInfo))) {
return createResponse(responseInfo, handler);
}
} catch (IOException e) {
// For transient errors, retry
if (retryConnectionError(e, request.url(), retries)) {
continue;
}
throw interpretApiError(e, request, responseInfo);
}
rateLimitChecker.checkRateLimit(this, request);
responseInfo = getResponseInfo(request);
noteRateLimit(responseInfo);
detectOTPRequired(responseInfo);
if (isInvalidCached404Response(responseInfo)) {
// Setting "Cache-Control" to "no-cache" stops the cache from supplying
// "If-Modified-Since" or "If-None-Match" values.
// This makes GitHub give us current data (not incorrectly cached data)
request = request.toBuilder().withHeader("Cache-Control", "no-cache").build();
continue;
}
if (!(isRateLimitResponse(responseInfo) || isAbuseLimitResponse(responseInfo))) {
return createResponse(responseInfo, handler);
}
} catch (IOException e) {
// For transient errors, retry
if (retryConnectionError(e, request.url(), retries)) {
continue;
}
throw interpretApiError(e, request, responseInfo);
handleLimitingErrors(responseInfo);
} finally {
IOUtils.closeQuietly(responseInfo);
}
handleLimitingErrors(responseInfo);
} while (--retries >= 0);
throw new GHIOException("Ran out of retries for URL: " + request.url().toString());
@@ -498,58 +553,25 @@ abstract class GitHubClient {
}
private void noteRateLimit(@Nonnull GitHubResponse.ResponseInfo responseInfo) {
if (responseInfo.request().urlPath().startsWith("/search")) {
// the search API uses a different rate limit
return;
}
String limitString = responseInfo.headerField("X-RateLimit-Limit");
if (StringUtils.isBlank(limitString)) {
// if we are missing a header, return fast
return;
}
String remainingString = responseInfo.headerField("X-RateLimit-Remaining");
if (StringUtils.isBlank(remainingString)) {
// if we are missing a header, return fast
return;
}
String resetString = responseInfo.headerField("X-RateLimit-Reset");
if (StringUtils.isBlank(resetString)) {
// if we are missing a header, return fast
return;
}
int limit, remaining;
long reset;
try {
String limitString = Objects.requireNonNull(responseInfo.headerField("X-RateLimit-Limit"),
"Missing X-RateLimit-Limit");
String remainingString = Objects.requireNonNull(responseInfo.headerField("X-RateLimit-Remaining"),
"Missing X-RateLimit-Remaining");
String resetString = Objects.requireNonNull(responseInfo.headerField("X-RateLimit-Reset"),
"Missing X-RateLimit-Reset");
int limit, remaining;
long reset;
limit = Integer.parseInt(limitString);
} catch (NumberFormatException e) {
if (LOGGER.isLoggable(FINEST)) {
LOGGER.log(FINEST, "Malformed X-RateLimit-Limit header value " + limitString, e);
}
return;
}
try {
remaining = Integer.parseInt(remainingString);
} catch (NumberFormatException e) {
if (LOGGER.isLoggable(FINEST)) {
LOGGER.log(FINEST, "Malformed X-RateLimit-Remaining header value " + remainingString, e);
}
return;
}
try {
reset = Long.parseLong(resetString);
} catch (NumberFormatException e) {
GHRateLimit.Record observed = new GHRateLimit.Record(limit, remaining, reset, responseInfo);
updateRateLimit(GHRateLimit.fromRecord(observed, responseInfo.request().rateLimitTarget()));
} catch (NumberFormatException | NullPointerException e) {
if (LOGGER.isLoggable(FINEST)) {
LOGGER.log(FINEST, "Malformed X-RateLimit-Reset header value " + resetString, e);
LOGGER.log(FINEST, "Missing or malformed X-RateLimit header: ", e);
}
return;
}
GHRateLimit.Record observed = new GHRateLimit.Record(limit, remaining, reset, responseInfo);
updateCoreRateLimit(observed);
}
private static void detectOTPRequired(@Nonnull GitHubResponse.ResponseInfo responseInfo) throws GHIOException {
@@ -569,23 +591,6 @@ abstract class GitHubClient {
"This operation requires a credential but none is given to the GitHub constructor");
}
/**
* Update the Rate Limit with the latest info from response header. Due to multi-threading requests might complete
* out of order, we want to pick the one with the most recent info from the server. Calls
* {@link #shouldReplace(GHRateLimit.Record, GHRateLimit.Record)}
*
* @param observed
* {@link GHRateLimit.Record} constructed from the response header information
*/
private void updateCoreRateLimit(@Nonnull GHRateLimit.Record observed) {
synchronized (headerRateLimitLock) {
if (headerRateLimit == null || shouldReplace(observed, headerRateLimit.getCore())) {
headerRateLimit = GHRateLimit.fromHeaderRecord(observed);
LOGGER.log(FINE, "Rate limit now: {0}", headerRateLimit);
}
}
}
private static class GHApiInfo {
private String rate_limit_url;
@@ -635,37 +640,6 @@ abstract class GitHubClient {
}
}
/**
* Determine if one {@link GHRateLimit.Record} should replace another. Header date is only accurate to the second,
* so we look at the information in the record itself.
*
* {@link GHRateLimit.UnknownLimitRecord}s are always replaced by regular {@link GHRateLimit.Record}s. Regular
* {@link GHRateLimit.Record}s are never replaced by {@link GHRateLimit.UnknownLimitRecord}s. Candidates with
* resetEpochSeconds later than current record are more recent. Candidates with the same reset and a lower remaining
* count are more recent. Candidates with an earlier reset are older.
*
* @param candidate
* {@link GHRateLimit.Record} constructed from the response header information
* @param current
* the current {@link GHRateLimit.Record} record
*/
static boolean shouldReplace(@Nonnull GHRateLimit.Record candidate, @Nonnull GHRateLimit.Record current) {
if (candidate instanceof GHRateLimit.UnknownLimitRecord
&& !(current instanceof GHRateLimit.UnknownLimitRecord)) {
// Unknown candidate never replaces a regular record
return false;
} else if (current instanceof GHRateLimit.UnknownLimitRecord
&& !(candidate instanceof GHRateLimit.UnknownLimitRecord)) {
// Any real record should replace an unknown Record.
return true;
} else {
// records of the same type compare to each other as normal.
return current.getResetEpochSeconds() < candidate.getResetEpochSeconds()
|| (current.getResetEpochSeconds() == candidate.getResetEpochSeconds()
&& current.getRemaining() > candidate.getRemaining());
}
}
static URL parseURL(String s) {
try {
return s == null ? null : new URL(s);
@@ -677,22 +651,24 @@ abstract class GitHubClient {
static Date parseDate(String timestamp) {
if (timestamp == null)
return null;
for (String f : TIME_FORMATS) {
try {
SimpleDateFormat df = new SimpleDateFormat(f);
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df.parse(timestamp);
} catch (ParseException e) {
// try next
}
return Date.from(parseInstant(timestamp));
}
static Instant parseInstant(String timestamp) {
if (timestamp == null)
return null;
if (timestamp.charAt(4) == '/') {
// Unsure where this is used, but retained for compatibility.
return Instant.from(DATE_TIME_PARSER_SLASHES.parse(timestamp));
} else {
return Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(timestamp));
}
throw new IllegalStateException("Unable to parse the timestamp: " + timestamp);
}
static String printDate(Date dt) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df.format(dt);
return DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(dt.getTime()).truncatedTo(ChronoUnit.SECONDS));
}
/**

View File

@@ -24,7 +24,7 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
* A GitHub API Client for HttpUrlConnection
* <p>
* A GitHubClient can be used to send requests and retrieve their responses. GitHubClient is thread-safe and can be used
* to send multiple requests. GitHubClient also track some GitHub API information such as {@link #rateLimit()}.
* to send multiple requests. GitHubClient also track some GitHub API information such as {@link GHRateLimit}.
* </p>
* <p>
* GitHubHttpUrlConnectionClient gets a new {@link HttpURLConnection} for each call to send.
@@ -235,6 +235,10 @@ class GitHubHttpUrlConnectionClient extends GitHubClient {
private static final Logger LOGGER = Logger.getLogger(GitHubClient.class.getName());
@Override
public void close() throws IOException {
IOUtils.closeQuietly(connection.getInputStream());
}
}
}

View File

@@ -3,26 +3,28 @@ package org.kohsuke.github;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Objects;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
/**
* A GitHub API Rate Limit Checker called before each request. This class provides the basic infrastructure for calling
* the appropriate {@link RateLimitChecker} for a request and retrying as many times as needed. This class supports more
* complex throttling strategies and polling, but leaves the specifics to the {@link RateLimitChecker} implementations.
* A GitHub API Rate Limit Checker called before each request.
*
* <p>
* GitHub allots a certain number of requests to each user or application per period of time (usually per hour). The
* number of requests remaining is returned in the response header and can also be requested using
* {@link GitHub#getRateLimit()}. This requests per interval is referred to as the "rate limit".
* GitHub allots a certain number of requests to each user or application per period of time. The number of requests
* remaining and the time when the number will be reset is returned in the response header and can also be requested
* using {@link GitHub#getRateLimit()}. The "requests per interval" is referred to as the "rate limit".
* </p>
* <p>
* GitHub prefers that clients stop before exceeding their rate limit rather than stopping after they exceed it. The
* {@link RateLimitChecker} is called before each request to check the rate limit and wait if the checker criteria are
* met.
* Different parts of the GitHub API have separate rate limits, but most of REST API uses {@link RateLimitTarget#CORE}.
* Checking your rate limit using {@link GitHub#getRateLimit()} does not effect your rate limit. GitHub prefers that
* clients stop before exceeding their rate limit rather than stopping after they exceed it.
* </p>
* <p>
* Checking your rate limit using {@link GitHub#getRateLimit()} does not effect your rate limit, but each {@link GitHub}
* instance will attempt to cache and reuse the last see rate limit rather than making a new request.
* This class provides the infrastructure for calling the appropriate {@link RateLimitChecker} before each request and
* retrying than call many times as needed. Each {@link RateLimitChecker} decides whether to wait and for how long. This
* allows for a wide range of {@link RateLimitChecker} implementations, including complex throttling strategies and
* polling.
* </p>
*/
class GitHubRateLimitChecker {
@@ -39,6 +41,8 @@ class GitHubRateLimitChecker {
@Nonnull
private final RateLimitChecker integrationManifest;
private static final Logger LOGGER = Logger.getLogger(GitHubRateLimitChecker.class.getName());
GitHubRateLimitChecker() {
this(RateLimitChecker.NONE, RateLimitChecker.NONE, RateLimitChecker.NONE, RateLimitChecker.NONE);
}
@@ -48,40 +52,57 @@ class GitHubRateLimitChecker {
@Nonnull RateLimitChecker graphql,
@Nonnull RateLimitChecker integrationManifest) {
this.core = Objects.requireNonNull(core);
// for now only support rate limiting on core
// remove these asserts when that changes
assert search == RateLimitChecker.NONE;
assert graphql == RateLimitChecker.NONE;
assert integrationManifest == RateLimitChecker.NONE;
this.search = Objects.requireNonNull(search);
this.graphql = Objects.requireNonNull(graphql);
this.integrationManifest = Objects.requireNonNull(integrationManifest);
}
/**
* Constructs a new {@link GitHubRateLimitChecker} with a new checker for a particular target.
*
* Only one {@link RateLimitChecker} is allowed per target.
*
* @param checker
* the {@link RateLimitChecker} to apply.
* @param rateLimitTarget
* the {@link RateLimitTarget} for this checker. If {@link RateLimitTarget#NONE}, checker will be ignored
* and no change will be made.
* @return a new {@link GitHubRateLimitChecker}
*/
GitHubRateLimitChecker with(@Nonnull RateLimitChecker checker, @Nonnull RateLimitTarget rateLimitTarget) {
return new GitHubRateLimitChecker(rateLimitTarget == RateLimitTarget.CORE ? checker : core,
rateLimitTarget == RateLimitTarget.SEARCH ? checker : search,
rateLimitTarget == RateLimitTarget.GRAPHQL ? checker : graphql,
rateLimitTarget == RateLimitTarget.INTEGRATION_MANIFEST ? checker : integrationManifest);
}
/**
* Checks whether there is sufficient requests remaining within this client's rate limit quota to make the current
* request.
* <p>
* This method does not do the actual check. Instead it select the appropriate {@link RateLimitChecker} and
* {@link GHRateLimit.Record} for the current request's urlPath. If the {@link RateLimitChecker} for this the
* current request's urlPath is {@link RateLimitChecker#NONE} the rate limit is not checked. If not, it calls
* {@link RateLimitChecker#checkRateLimit(GHRateLimit.Record, long)}. which decides if the rate limit has been
* exceeded and then sleeps for as long is it choose.
* This method does not do the actual check. Instead it selects the appropriate {@link RateLimitChecker} and
* {@link GHRateLimit.Record} for the current request's {@link RateLimitTarget}. It then calls
* {@link RateLimitChecker#checkRateLimit(GHRateLimit.Record, long)}.
* </p>
* <p>
* It is up to the {@link RateLimitChecker#checkRateLimit(GHRateLimit.Record, long)} which decide if the rate limit
* has been exceeded. If it has, that method will sleep for as long is it chooses and then return {@code true}. If
* not, that method will return {@code false}.
* It is up to {@link RateLimitChecker#checkRateLimit(GHRateLimit.Record, long)} to which decide if the rate limit
* has been exceeded. If it has, {@link RateLimitChecker#checkRateLimit(GHRateLimit.Record, long)} will sleep for as
* long is it chooses and then return {@code true}. If not, that method will return {@code false}.
* </p>
* <p>
* As long as {@link RateLimitChecker#checkRateLimit(GHRateLimit.Record, long)} returns {@code true}, this method
* will request updated rate limit information and call
* {@link RateLimitChecker#checkRateLimit(GHRateLimit.Record, long)} again. This looping allows implementers of
* {@link RateLimitChecker#checkRateLimit(GHRateLimit.Record, long)} to apply any number of strategies to
* controlling the speed at which requests are made. When it returns {@code false} this method will return and the
* request will be sent.
* {@link RateLimitChecker#checkRateLimit(GHRateLimit.Record, long)} again. This looping allows different
* {@link RateLimitChecker} implementations to apply any number of strategies to controlling the speed at which
* requests are made.
* </p>
* <p>
* When the {@link RateLimitChecker} returns {@code false} this method will return and the request processing will
* continue.
* </p>
* <p>
* If the {@link RateLimitChecker} for this the current request's urlPath is {@link RateLimitChecker#NONE} the rate
* limit is not checked.
* </p>
*
* @param client
@@ -92,14 +113,14 @@ class GitHubRateLimitChecker {
* if there is an I/O error
*/
void checkRateLimit(GitHubClient client, GitHubRequest request) throws IOException {
RateLimitChecker guard = selectChecker(request.urlPath());
RateLimitChecker guard = selectChecker(request.rateLimitTarget());
if (guard == RateLimitChecker.NONE) {
return;
}
// For the first rate limit, accept the current limit if a valid one is already present.
GHRateLimit rateLimit = client.rateLimit();
GHRateLimit.Record rateLimitRecord = rateLimit.getRecordForUrlPath(request.urlPath());
GHRateLimit rateLimit = client.rateLimit(request.rateLimitTarget());
GHRateLimit.Record rateLimitRecord = rateLimit.getRecord(request.rateLimitTarget());
long waitCount = 0;
try {
while (guard.checkRateLimit(rateLimitRecord, waitCount)) {
@@ -112,8 +133,8 @@ class GitHubRateLimitChecker {
Thread.sleep(1000);
// After the first wait, always request a new rate limit from the server.
rateLimit = client.getRateLimit();
rateLimitRecord = rateLimit.getRecordForUrlPath(request.urlPath());
rateLimit = client.getRateLimit(request.rateLimitTarget());
rateLimitRecord = rateLimit.getRecord(request.rateLimitTarget());
}
} catch (InterruptedException e) {
throw (IOException) new InterruptedIOException(e.getMessage()).initCause(e);
@@ -121,25 +142,28 @@ class GitHubRateLimitChecker {
}
/**
* Gets the appropriate {@link RateLimitChecker} for a particular url path. Similar to
* {@link GHRateLimit#getRecordForUrlPath(String)}.
* Gets the appropriate {@link RateLimitChecker} for a particular target.
*
* @param urlPath
* the url path of the request
* @return the {@link RateLimitChecker} for a url path.
* Analogous with {@link GHRateLimit#getRecord(RateLimitTarget)}.
*
* @param rateLimitTarget
* the rate limit to check
* @return the {@link RateLimitChecker} for a particular target
*/
@Nonnull
private RateLimitChecker selectChecker(@Nonnull String urlPath) {
if (urlPath.equals("/rate_limit")) {
private RateLimitChecker selectChecker(@Nonnull RateLimitTarget rateLimitTarget) {
if (rateLimitTarget == RateLimitTarget.NONE) {
return RateLimitChecker.NONE;
} else if (urlPath.startsWith("/search")) {
} else if (rateLimitTarget == RateLimitTarget.CORE) {
return core;
} else if (rateLimitTarget == RateLimitTarget.SEARCH) {
return search;
} else if (urlPath.startsWith("/graphql")) {
} else if (rateLimitTarget == RateLimitTarget.GRAPHQL) {
return graphql;
} else if (urlPath.startsWith("/app-manifests")) {
} else if (rateLimitTarget == RateLimitTarget.INTEGRATION_MANIFEST) {
return integrationManifest;
} else {
return core;
throw new IllegalArgumentException("Unknown rate limit target: " + rateLimitTarget.toString());
}
}
}

View File

@@ -45,6 +45,7 @@ class GitHubRequest {
private final String apiUrl;
private final String urlPath;
private final String method;
private final RateLimitTarget rateLimitTarget;
private final InputStream body;
private final boolean forceBody;
@@ -56,6 +57,7 @@ class GitHubRequest {
@Nonnull String apiUrl,
@Nonnull String urlPath,
@Nonnull String method,
@Nonnull RateLimitTarget rateLimitTarget,
@CheckForNull InputStream body,
boolean forceBody) throws MalformedURLException {
this.args = Collections.unmodifiableList(new ArrayList<>(args));
@@ -64,6 +66,7 @@ class GitHubRequest {
this.apiUrl = apiUrl;
this.urlPath = urlPath;
this.method = method;
this.rateLimitTarget = rateLimitTarget;
this.body = body;
this.forceBody = forceBody;
String tailApiUrl = buildTailApiUrl();
@@ -119,6 +122,16 @@ class GitHubRequest {
return method;
}
/**
* The rate limit target for this request.
*
* @return the rate limit to use for this request.
*/
@Nonnull
public RateLimitTarget rateLimitTarget() {
return rateLimitTarget;
}
/**
* The arguments for this request. Depending on the {@link #method()} and {@code #inBody()} these maybe added to the
* url or to the request body.
@@ -217,7 +230,15 @@ class GitHubRequest {
* @return a {@link Builder} based on this request.
*/
public Builder<?> toBuilder() {
return new Builder<>(args, headers, injectedMappingValues, apiUrl, urlPath, method, body, forceBody);
return new Builder<>(args,
headers,
injectedMappingValues,
apiUrl,
urlPath,
method,
rateLimitTarget,
body,
forceBody);
}
private String buildTailApiUrl() {
@@ -281,6 +302,10 @@ class GitHubRequest {
*/
@Nonnull
private String method;
@Nonnull
private RateLimitTarget rateLimitTarget;
private InputStream body;
private boolean forceBody;
@@ -294,6 +319,7 @@ class GitHubRequest {
GitHubClient.GITHUB_URL,
"/",
"GET",
RateLimitTarget.CORE,
null,
false);
}
@@ -304,6 +330,7 @@ class GitHubRequest {
@Nonnull String apiUrl,
@Nonnull String urlPath,
@Nonnull String method,
@Nonnull RateLimitTarget rateLimitTarget,
@CheckForNull @WillClose InputStream body,
boolean forceBody) {
this.args = new ArrayList<>(args);
@@ -312,6 +339,7 @@ class GitHubRequest {
this.apiUrl = apiUrl;
this.urlPath = urlPath;
this.method = method;
this.rateLimitTarget = rateLimitTarget;
this.body = body;
this.forceBody = forceBody;
}
@@ -324,7 +352,15 @@ class GitHubRequest {
* if the GitHub API URL cannot be constructed
*/
public GitHubRequest build() throws MalformedURLException {
return new GitHubRequest(args, headers, injectedMappingValues, apiUrl, urlPath, method, body, forceBody);
return new GitHubRequest(args,
headers,
injectedMappingValues,
apiUrl,
urlPath,
method,
rateLimitTarget,
body,
forceBody);
}
/**
@@ -348,9 +384,11 @@ class GitHubRequest {
* the name
* @param value
* the value
* @return the request builder
*/
public void setHeader(String name, String value) {
public B setHeader(String name, String value) {
headers.put(name, value);
return (B) this;
}
/**
@@ -363,8 +401,11 @@ class GitHubRequest {
* @return the request builder
*/
public B withHeader(String name, String value) {
setHeader(name, value);
return (B) this;
String oldValue = headers.get(name);
if (!StringUtils.isBlank(oldValue)) {
value = oldValue + ", " + value;
}
return setHeader(name, value);
}
/**
@@ -562,6 +603,18 @@ class GitHubRequest {
return (B) this;
}
/**
* Method requester.
*
* @param rateLimitTarget
* the rate limit target for this request. Default is {@link RateLimitTarget#CORE}.
* @return the request builder
*/
public B rateLimit(@Nonnull RateLimitTarget rateLimitTarget) {
this.rateLimitTarget = rateLimitTarget;
return (B) this;
}
/**
* Content type requester.
*
@@ -583,13 +636,17 @@ class GitHubRequest {
* when needing to set query parameters on requests methods that don't usually have them, such as
* {@link GHRelease#uploadAsset(String, InputStream, String)}.
*
* @param urlOrPath
* @param rawUrlPath
* the content type
* @return the request builder
*/
B setRawUrlPath(String urlOrPath) {
Objects.requireNonNull(urlOrPath);
this.urlPath = urlOrPath;
B setRawUrlPath(@Nonnull String rawUrlPath) {
Objects.requireNonNull(rawUrlPath);
// This method should only work for full urls, which must start with "http"
if (!rawUrlPath.startsWith("http")) {
throw new GHException("Raw URL must start with 'http'");
}
this.urlPath = rawUrlPath;
return (B) this;
}
@@ -603,10 +660,10 @@ class GitHubRequest {
* the content type
* @return the request builder
*/
public B withUrlPath(String... urlPathItems) {
public B withUrlPath(@Nonnull String urlPath, @Nonnull String... urlPathItems) {
// full url may be set and reset as needed
if (urlPathItems.length == 1 && !urlPathItems[0].startsWith("/")) {
return setRawUrlPath(urlPathItems[0]);
if (urlPathItems.length == 0 && !urlPath.startsWith("/")) {
return setRawUrlPath(urlPath);
}
// Once full url is set, do not allow path setting
@@ -614,15 +671,14 @@ class GitHubRequest {
throw new GHException("Cannot append to url path after setting a full url");
}
String tailUrlPath = String.join("/", urlPathItems);
if (this.urlPath.endsWith("/")) {
tailUrlPath = StringUtils.stripStart(tailUrlPath, "/");
} else {
tailUrlPath = StringUtils.prependIfMissing(tailUrlPath, "/");
String tailUrlPath = urlPath;
if (urlPathItems.length != 0) {
tailUrlPath += "/" + String.join("/", urlPathItems);
}
this.urlPath += urlPathEncode(tailUrlPath);
tailUrlPath = StringUtils.prependIfMissing(tailUrlPath, "/");
this.urlPath = urlPathEncode(tailUrlPath);
return (B) this;
}
@@ -658,7 +714,7 @@ class GitHubRequest {
*/
private static String urlPathEncode(String value) {
try {
return new URI(null, null, value, null, null).toString();
return new URI(null, null, value, null, null).toASCIIString();
} catch (URISyntaxException ex) {
throw new AssertionError(ex);
}

View File

@@ -1,9 +1,11 @@
package org.kohsuke.github;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.apache.commons.io.IOUtils;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -16,6 +18,8 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
@@ -31,6 +35,8 @@ import javax.annotation.Nonnull;
*/
class GitHubResponse<T> {
private static final Logger LOGGER = Logger.getLogger(GitHubResponse.class.getName());
private final int statusCode;
@Nonnull
@@ -72,9 +78,14 @@ class GitHubResponse<T> {
@CheckForNull
static <T> T parseBody(ResponseInfo responseInfo, Class<T> type) throws IOException {
if (responseInfo.statusCode() == HttpURLConnection.HTTP_NO_CONTENT && type != null && type.isArray()) {
// no content
return type.cast(Array.newInstance(type.getComponentType(), 0));
if (responseInfo.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) {
if (type != null && type.isArray()) {
// no content for array should be empty array
return type.cast(Array.newInstance(type.getComponentType(), 0));
} else {
// no content for object should be null
return null;
}
}
String data = responseInfo.getBodyAsString();
@@ -83,9 +94,10 @@ class GitHubResponse<T> {
inject.addValue(ResponseInfo.class, responseInfo);
return GitHubClient.getMappingObjectReader(responseInfo).forType(type).readValue(data);
} catch (JsonMappingException e) {
String message = "Failed to deserialize " + data;
throw new IOException(message, e);
} catch (JsonMappingException | JsonParseException e) {
String message = "Failed to deserialize: " + data;
LOGGER.log(Level.FINE, message);
throw e;
}
}
@@ -108,9 +120,10 @@ class GitHubResponse<T> {
String data = responseInfo.getBodyAsString();
try {
return GitHubClient.getMappingObjectReader(responseInfo).withValueToUpdate(instance).readValue(data);
} catch (JsonMappingException e) {
String message = "Failed to deserialize " + data;
throw new IOException(message, e);
} catch (JsonMappingException | JsonParseException e) {
String message = "Failed to deserialize: " + data;
LOGGER.log(Level.FINE, message);
throw e;
}
}
@@ -205,7 +218,7 @@ class GitHubResponse<T> {
* Initial response information supplied to a {@link BodyHandler} when a response is initially received and before
* the body is processed.
*/
static abstract class ResponseInfo {
static abstract class ResponseInfo implements Closeable {
private static final Comparator<String> nullableCaseInsensitiveComparator = Comparator
.nullsFirst(String.CASE_INSENSITIVE_ORDER);
@@ -307,14 +320,11 @@ class GitHubResponse<T> {
* @throws IOException
* if an I/O Exception occurs.
*/
@Nonnull
String getBodyAsString() throws IOException {
InputStreamReader r = null;
try {
r = new InputStreamReader(this.bodyStream(), StandardCharsets.UTF_8);
return IOUtils.toString(r);
} finally {
IOUtils.closeQuietly(r);
}
r = new InputStreamReader(this.bodyStream(), StandardCharsets.UTF_8);
return IOUtils.toString(r);
}
}

View File

@@ -5,7 +5,6 @@ import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

View File

@@ -15,6 +15,14 @@ class Previews {
*/
static final String ANTIOPE = "application/vnd.github.antiope-preview+json";
/**
* Create repository from template repository
*
* @see <a href="https://developer.github.com/v3/previews/#create-and-use-repository-templates">GitHub API
* Previews</a>
*/
static final String BAPTISE = "application/vnd.github.baptiste-preview+json";
/**
* Commit Search
*
@@ -58,6 +66,14 @@ class Previews {
*/
static final String MERCY = "application/vnd.github.mercy-preview+json";
/**
* New visibility parameter for the Repositories API
*
* @see <a href="https://developer.github.com/v3/previews/#new-visibility-parameter-for-the-repositories-api">GitHub
* API Previews</a>
*/
static final String NEBULA = "application/vnd.github.nebula-preview+json";
/**
* Draft pull requests
*
@@ -78,4 +94,5 @@ class Previews {
* @see <a href="https://developer.github.com/v3/previews/#require-signed-commits">GitHub API Previews</a>
*/
static final String ZZZAX = "application/vnd.github.zzzax-preview+json";
}

View File

@@ -5,20 +5,17 @@ import java.util.logging.Logger;
/**
* A GitHub API Rate Limit Checker called before each request
*
* <p>
* GitHub allots a certain number of requests to each user or application per period of time (usually per hour). The
* number of requests remaining is returned in the response header and can also be requested using
* {@link GitHub#getRateLimit()}. This requests per interval is referred to as the "rate limit".
* GitHub allots a certain number of requests to each user or application per period of time. The number of requests
* remaining and the time when the number will be reset is returned in the response header and can also be requested
* using {@link GitHub#getRateLimit()}. The "requests per interval" is referred to as the "rate limit".
* </p>
* <p>
* GitHub prefers that clients stop before exceeding their rate limit rather than stopping after they exceed it. The
* {@link RateLimitChecker} is called before each request to check the rate limit and wait if the checker criteria are
* met.
* </p>
* <p>
* Checking your rate limit using {@link GitHub#getRateLimit()} does not effect your rate limit, but each {@link GitHub}
* instance will attempt to cache and reuse the last see rate limit rather than making a new request.
* </p>
*/
public abstract class RateLimitChecker {
@@ -33,28 +30,21 @@ public abstract class RateLimitChecker {
* free to choose whatever strategy they prefer for what is considered to exceed the budget and how long to sleep.
*
* <p>
* The caller of this method figures out which {@link GHRateLimit.Record} applies for the current request add
* The caller of this method figures out which {@link GHRateLimit.Record} applies for the current request and
* provides it to this method.
* </p>
* <p>
* It is important to remember that rate limit reset times are only accurate to the second. Trying to sleep to
* exactly the reset time would be likely to produce worse behavior rather than better. For this reason
* {@link GitHubRateLimitChecker} may choose to add more sleep times when a checker indicates the rate limit was
* exceeded.
* As long as this method returns {@code true} it is guaranteed that {@link GitHubRateLimitChecker} will retrieve
* updated rate limit information and call this method again with {@code count} incremented by one. When this
* checker returns {@code false}, the calling {@link GitHubRateLimitChecker} will let the request continue.
* </p>
* <p>
* As long as this method returns {@code true} it is guaranteed that {@link GitHubRateLimitChecker} will get updated
* rate limit information and call this method again with {@code count} incremented by one. After this method
* returns {@code true} at least once, the calling {@link GitHubRateLimitChecker} may choose to wait some additional
* period of time between calls to this checker.
* Rate limit reset times are only accurate to the second. Trying to sleep to exactly the reset time could result in
* requests being sent before the new rate limit was available. For this reason, if this method returned
* {@code true} at least once for a particular request, {@link GitHubRateLimitChecker} may choose to sleep for some
* small additional between calls and before letting the request continue.
* </p>
* <p>
* After this checker returns {@code false}, the calling {@link GitHubRateLimitChecker} will let the request
* continue. If this method returned {@code true} at least once for a particular request, the calling
* {@link GitHubRateLimitChecker} may choose to wait some additional period of time before letting the request be
* sent.
* </p>
*
*
* @param rateLimitRecord
* the current {@link GHRateLimit.Record} to check against.
* @param count

View File

@@ -0,0 +1,36 @@
package org.kohsuke.github;
/**
* Specifies the rate limit record of an operation.
*
* @see GitHubBuilder#withRateLimitChecker(RateLimitChecker, RateLimitTarget)
*/
public enum RateLimitTarget {
/**
* Selects or updates the {@link GHRateLimit#getCore()} record.
*/
CORE,
/**
* Selects or updates the {@link GHRateLimit#getSearch()} record.
*/
SEARCH,
/**
* Selects or updates the {@link GHRateLimit#getGraphQL()} record.
*/
GRAPHQL,
/**
* Selects or updates the {@link GHRateLimit#getIntegrationManifest()} record.
*/
INTEGRATION_MANIFEST,
/**
* Selects no rate limit.
*
* This request uses no rate limit. If the response header includes rate limit information, it will apply to
* {@link #CORE}.
*/
NONE
}

View File

@@ -1,7 +1,6 @@
package org.kohsuke.github;
import java.io.IOException;
import java.net.URL;
/**
* The interface Refreshable.

View File

@@ -23,6 +23,9 @@
*/
package org.kohsuke.github;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
@@ -108,7 +111,10 @@ class Requester extends GitHubRequest.Builder<Requester> {
* the io exception
*/
public InputStream fetchStream() throws IOException {
return client.sendRequest(this, (responseInfo) -> responseInfo.bodyStream()).body();
return client
.sendRequest(this,
(responseInfo) -> new ByteArrayInputStream(IOUtils.toByteArray(responseInfo.bodyStream())))
.body();
}
/**

View File

@@ -280,14 +280,15 @@ public final class ObsoleteUrlFactory implements URLStreamHandlerFactory, Clonea
if (c > '\u001f' && c < '\u007f')
continue;
Buffer buffer = new Buffer();
buffer.writeUtf8(s, 0, i);
buffer.writeUtf8CodePoint('?');
for (int j = i + Character.charCount(c); j < length; j += Character.charCount(c)) {
c = s.codePointAt(j);
buffer.writeUtf8CodePoint(c > '\u001f' && c < '\u007f' ? c : '?');
try (Buffer buffer = new Buffer()) {
buffer.writeUtf8(s, 0, i);
buffer.writeUtf8CodePoint('?');
for (int j = i + Character.charCount(c); j < length; j += Character.charCount(c)) {
c = s.codePointAt(j);
buffer.writeUtf8CodePoint(c > '\u001f' && c < '\u007f' ? c : '?');
}
return buffer.readUtf8();
}
return buffer.readUtf8();
}
return s;
}

View File

@@ -0,0 +1,31 @@
Creating resources at the organization level
In order to create resources in GitHub for a given organization, you must first create an object of type <<<GHOrganization>>>.
As an example:
+-----+
GHOrganization organizationClient(GitHub gitHub, String organizationName) throws IOException {
return gitHub.getOrganization(organizationName);
}
+-----+
Now you can easily work with the GHOrganization, and all methods that will create resources will create them
in the given organization.
One of the most common use cases is to create a repository in a given organization:
+-----+
void createRepository(GHOrganization organization) {
organization.createRepository("repository-name")
.private_(true)
.wiki(false)
.projects(false)
.description("Description")
.allowMergeCommit(true)
.allowSquashMerge(false)
.allowRebaseMerge(false)
.create()
}
+-----+

View File

@@ -14,7 +14,7 @@
<menu name="Git Hub API for Java">
<item name="Introduction" href="/index.html"/>
<item name="Download" href="https://mvnrepository.com/artifact/${project.groupId}/${project.artifactId}"/>
<item name="Source code" href="https://github.com/github-api/${project.artifactId}"/>
<item name="Source code" href="https://github.com/hub4j/${project.artifactId}"/>
<item name="Mailing List" href="https://groups.google.com/forum/#!forum/github-api"/>
</menu>
@@ -23,6 +23,7 @@
<item name="JWT Authentication" href="/githubappjwtauth.html"/>
<item name="App Installation Token " href="/githubappappinsttokenauth.html"/>
</item>
<item name="Working with organizations" href="/createorglevelresources.html"/>
</menu>
<menu name="References">

View File

@@ -31,7 +31,7 @@ public abstract class AbstractGitHubWireMockTest extends Assert {
private final GitHubBuilder githubBuilder = createGitHubBuilder();
final static String GITHUB_API_TEST_ORG = "github-api-test-org";
final static String GITHUB_API_TEST_ORG = "hub4j-test-org";
final static String STUBBED_USER_LOGIN = "placeholder-user";
final static String STUBBED_USER_PASSWORD = "placeholder-password";

View File

@@ -20,6 +20,7 @@ import java.util.Map.Entry;
import java.util.regex.Pattern;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.oneOf;
@@ -108,7 +109,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
gitHub = getGitHubBuilder().withOAuthToken("bogus", "user")
.withEndpoint(mockGitHub.apiServer().baseUrl())
.build();
assertThat(gitHub.lastRateLimit(), nullValue());
assertThat(gitHub.lastRateLimit(), sameInstance(GHRateLimit.DEFAULT));
assertFalse(gitHub.isCredentialValid());
// For invalid credentials, we get a 401 but it includes anonymous rate limit headers
assertThat(gitHub.lastRateLimit().getCore(), not(instanceOf(GHRateLimit.UnknownLimitRecord.class)));
@@ -118,18 +119,22 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Test
public void testCredentialValidEnterprise() throws IOException {
// Simulated GHE: getRateLimit returns 404
assertThat(gitHub.lastRateLimit(), nullValue());
assertThat(gitHub.lastRateLimit(), sameInstance(GHRateLimit.DEFAULT));
assertThat(gitHub.lastRateLimit().getCore().isExpired(), is(true));
assertTrue(gitHub.isCredentialValid());
// lastRateLimit stays null when 404 is encountered
assertThat(gitHub.lastRateLimit(), nullValue());
// lastRateLimitUpdates because 404 still includes header rate limit info
assertThat(gitHub.lastRateLimit(), notNullValue());
assertThat(gitHub.lastRateLimit(), not(equalTo(GHRateLimit.DEFAULT)));
assertThat(gitHub.lastRateLimit().getCore().isExpired(), is(false));
gitHub = getGitHubBuilder().withOAuthToken("bogus", "user")
.withEndpoint(mockGitHub.apiServer().baseUrl())
.build();
assertThat(gitHub.lastRateLimit(), nullValue());
assertThat(gitHub.lastRateLimit(), sameInstance(GHRateLimit.DEFAULT));
assertFalse(gitHub.isCredentialValid());
// Simulated GHE: For invalid credentials, we get a 401 that does not include ratelimit info
assertThat(gitHub.lastRateLimit(), nullValue());
assertThat(gitHub.lastRateLimit(), sameInstance(GHRateLimit.DEFAULT));
}
@Test
@@ -198,7 +203,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Test
public void testGetIssues() throws Exception {
List<GHIssue> closedIssues = gitHub.getOrganization("github-api")
List<GHIssue> closedIssues = gitHub.getOrganization("hub4j")
.getRepository("github-api")
.getIssues(GHIssueState.CLOSED);
// prior to using PagedIterable GHRepository.getIssues(GHIssueState) would only retrieve 30 issues
@@ -211,7 +216,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Test
public void testListIssues() throws IOException {
Iterable<GHIssue> closedIssues = gitHub.getOrganization("github-api")
Iterable<GHIssue> closedIssues = gitHub.getOrganization("hub4j")
.getRepository("github-api")
.listIssues(GHIssueState.CLOSED);
@@ -291,7 +296,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
GHOrganization organization = gitHub.getOrganization(GITHUB_API_TEST_ORG);
GHTeam teamByName = organization.getTeams().get("Core Developers");
GHTeam teamById = gitHub.getTeam(teamByName.getId());
GHTeam teamById = gitHub.getTeam((int) teamByName.getId());
assertNotNull(teamById);
assertEquals(teamByName.getId(), teamById.getId());
@@ -308,6 +313,13 @@ public class AppTest extends AbstractGitHubWireMockTest {
assertEquals(teamByName.getId(), teamById.getId());
assertEquals(teamByName.getDescription(), teamById.getDescription());
GHTeam teamById2 = organization.getTeam((int) teamByName.getId());
assertNotNull(teamById2);
assertEquals(teamByName.getId(), teamById2.getId());
assertEquals(teamByName.getDescription(), teamById2.getDescription());
}
@Ignore("Needs mocking check")
@@ -322,7 +334,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Ignore("Needs mocking check")
@Test
public void testFetchPullRequestAsList() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
assertEquals("master", r.getMasterBranch());
PagedIterable<GHPullRequest> i = r.listPullRequests(GHIssueState.CLOSED);
List<GHPullRequest> prs = i.toList();
@@ -628,7 +640,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Test
public void testCommitStatus() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommitStatus state;
@@ -644,7 +656,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Test
public void testCommitShortInfo() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f23");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Kohsuke Kawaguchi");
assertEquals(commit.getCommitShortInfo().getMessage(), "doc");
@@ -807,7 +819,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Test // issue #99
public void testReadme() throws IOException {
GHContent readme = gitHub.getRepository("github-api-test-org/test-readme").getReadme();
GHContent readme = gitHub.getRepository("hub4j-test-org/test-readme").getReadme();
assertEquals(readme.getName(), "README.md");
assertEquals(readme.getContent(), "This is a markdown readme.\n");
}
@@ -815,7 +827,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Ignore("Needs mocking check")
@Test
public void testTrees() throws IOException {
GHTree masterTree = gitHub.getRepository("github-api/github-api").getTree("master");
GHTree masterTree = gitHub.getRepository("hub4j/github-api").getTree("master");
boolean foundReadme = false;
for (GHTreeEntry e : masterTree.getTree()) {
if ("readme".equalsIgnoreCase(e.getPath().replaceAll("\\.md", ""))) {
@@ -828,7 +840,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Test
public void testTreesRecursive() throws IOException {
GHTree masterTree = gitHub.getRepository("github-api/github-api").getTreeRecursive("master", 1);
GHTree masterTree = gitHub.getRepository("hub4j/github-api").getTreeRecursive("master", 1);
boolean foundThisFile = false;
for (GHTreeEntry e : masterTree.getTree()) {
if (e.getPath().endsWith(AppTest.class.getSimpleName() + ".java")) {
@@ -844,7 +856,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
cleanupLabel("test");
cleanupLabel("test2");
GHRepository r = gitHub.getRepository("github-api-test-org/test-labels");
GHRepository r = gitHub.getRepository("hub4j-test-org/test-labels");
List<GHLabel> lst = r.listLabels().toList();
for (GHLabel l : lst) {
assertThat(l.getUrl(), containsString(l.getName().replace(" ", "%20")));
@@ -943,7 +955,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
void cleanupLabel(String name) {
if (mockGitHub.isUseProxy()) {
try {
GHLabel t = getGitHubBeforeAfter().getRepository("github-api-test-org/test-labels").getLabel(name);
GHLabel t = getGitHubBeforeAfter().getRepository("hub4j-test-org/test-labels").getLabel(name);
t.delete();
} catch (IOException e) {
@@ -1012,7 +1024,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
@Test
public void reactions() throws Exception {
GHIssue i = gitHub.getRepository("github-api/github-api").getIssue(311);
GHIssue i = gitHub.getRepository("hub4j/github-api").getIssue(311);
List<GHReaction> l;
// retrieval
@@ -1084,7 +1096,7 @@ public class AppTest extends AbstractGitHubWireMockTest {
public void blob() throws Exception {
Assume.assumeFalse(SystemUtils.IS_OS_WINDOWS);
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
String sha1 = "a12243f2fc5b8c2ba47dd677d0b0c7583539584d";
assertBlobContent(r.readBlob(sha1));

View File

@@ -5,12 +5,15 @@ import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.equalTo;
import javax.annotation.Nonnull;
import static org.hamcrest.Matchers.*;
/**
* @author Kohsuke Kawaguchi
@@ -18,26 +21,50 @@ import static org.hamcrest.Matchers.equalTo;
public class BridgeMethodTest extends Assert {
@Test
public void lastStatus() throws IOException {
GHObject obj = new GHIssue();
public void testBridgeMethods() throws IOException {
List<Method> createdAtMethods = new ArrayList<>();
for (Method method : obj.getClass().getMethods()) {
if (method.getName().equalsIgnoreCase("getCreatedAt")) {
if (method.getReturnType() == Date.class) {
createdAtMethods.add(0, method);
} else {
createdAtMethods.add(method);
}
// Some would say this is redundant, given that bridge methods are so thin anyway
// In the interest of maintaining binary compatibility, we'll do this anyway for a sampling of methods
// Something odd here
// verifyBridgeMethods(new GHCommit(), "getAuthor", GHCommit.GHAuthor.class, GitUser.class);
// verifyBridgeMethods(new GHCommit(), "getCommitter", GHCommit.GHAuthor.class, GitUser.class);
verifyBridgeMethods(GHIssue.class, "getCreatedAt", Date.class, String.class);
verifyBridgeMethods(GHIssue.class, "getId", int.class, long.class, String.class);
verifyBridgeMethods(GHIssue.class, "getUrl", String.class, URL.class);
verifyBridgeMethods(GHOrganization.class, "getHtmlUrl", String.class, URL.class);
verifyBridgeMethods(GHOrganization.class, "getId", int.class, long.class, String.class);
verifyBridgeMethods(GHOrganization.class, "getUrl", String.class, URL.class);
verifyBridgeMethods(GHRepository.class, "getCollaborators", GHPersonSet.class, Set.class);
verifyBridgeMethods(GHRepository.class, "getHtmlUrl", String.class, URL.class);
verifyBridgeMethods(GHRepository.class, "getId", int.class, long.class, String.class);
verifyBridgeMethods(GHRepository.class, "getUrl", String.class, URL.class);
verifyBridgeMethods(GHUser.class, "getFollows", GHPersonSet.class, Set.class);
verifyBridgeMethods(GHUser.class, "getFollowers", GHPersonSet.class, Set.class);
verifyBridgeMethods(GHUser.class, "getOrganizations", GHPersonSet.class, Set.class);
verifyBridgeMethods(GHUser.class, "getId", int.class, long.class, String.class);
verifyBridgeMethods(GHTeam.class, "getId", int.class, long.class, String.class);
// verifyBridgeMethods(GitHub.class, "getMyself", GHMyself.class, GHUser.class);
}
void verifyBridgeMethods(@Nonnull Class<?> targetClass, @Nonnull String methodName, Class<?>... returnTypes) {
List<Class<?>> foundMethods = new ArrayList<>();
Method[] methods = targetClass.getMethods();
for (Method method : methods) {
if (method.getName().equalsIgnoreCase(methodName)) {
// Bridge methods are only
assertThat(method.getParameterCount(), equalTo(0));
foundMethods.add(method.getReturnType());
}
}
assertThat(createdAtMethods.size(), equalTo(2));
assertThat(createdAtMethods.get(0).getParameterCount(), equalTo(0));
assertThat(createdAtMethods.get(1).getParameterCount(), equalTo(0));
assertThat(createdAtMethods.get(0).getReturnType(), is(Date.class));
assertThat(createdAtMethods.get(1).getReturnType(), is(String.class));
assertThat(foundMethods, containsInAnyOrder(returnTypes));
}
}

View File

@@ -28,8 +28,8 @@ public class GHAppTest extends AbstractGitHubWireMockTest {
@Test
public void getGitHubApp() throws IOException {
GHApp app = gitHub.getApp();
assertThat(app.id, is((long) 11111));
assertThat(app.getOwner().id, is((long) 111111111));
assertThat(app.getId(), is((long) 11111));
assertThat(app.getOwner().getId(), is((long) 111111111));
assertThat(app.getOwner().login, is("bogus"));
assertThat(app.getName(), is("Bogus-Development"));
assertThat(app.getDescription(), is(""));
@@ -132,8 +132,8 @@ public class GHAppTest extends AbstractGitHubWireMockTest {
Map<String, GHPermissionType> appPermissions = appInstallation.getPermissions();
GHUser appAccount = appInstallation.getAccount();
assertThat(appInstallation.id, is((long) 11111111));
assertThat(appAccount.id, is((long) 111111111));
assertThat(appInstallation.getId(), is((long) 11111111));
assertThat(appAccount.getId(), is((long) 111111111));
assertThat(appAccount.login, is("bogus"));
assertThat(appInstallation.getRepositorySelection(), is(GHRepositorySelection.SELECTED));
assertThat(appInstallation.getAccessTokenUrl(), endsWith("/app/installations/11111111/access_tokens"));

View File

@@ -6,6 +6,8 @@ import org.kohsuke.github.GHBranchProtection.EnforceAdmins;
import org.kohsuke.github.GHBranchProtection.RequiredReviews;
import org.kohsuke.github.GHBranchProtection.RequiredStatusChecks;
import static org.hamcrest.CoreMatchers.*;
public class GHBranchProtectionTest extends AbstractGitHubWireMockTest {
private static final String BRANCH = "master";
private static final String BRANCH_REF = "heads/" + BRANCH;
@@ -32,6 +34,14 @@ public class GHBranchProtectionTest extends AbstractGitHubWireMockTest {
.includeAdmins()
.enable();
verifyBranchProtection(protection);
// Get goes through a different code path. Make sure it also gets the correct data.
protection = branch.getProtection();
verifyBranchProtection(protection);
}
private void verifyBranchProtection(GHBranchProtection protection) {
RequiredStatusChecks statusChecks = protection.getRequiredStatusChecks();
assertNotNull(statusChecks);
assertTrue(statusChecks.isRequiresBranchUpToDate());
@@ -54,11 +64,32 @@ public class GHBranchProtectionTest extends AbstractGitHubWireMockTest {
assertTrue(repo.getBranch(BRANCH).isProtected());
}
@Test
public void testDisableProtectionOnly() throws Exception {
GHBranchProtection protection = branch.enableProtection().enable();
assertTrue(repo.getBranch(BRANCH).isProtected());
branch.disableProtection();
assertFalse(repo.getBranch(BRANCH).isProtected());
}
@Test
public void testEnableRequireReviewsOnly() throws Exception {
GHBranchProtection protection = branch.enableProtection().requireReviews().enable();
RequiredReviews requiredReviews = protection.getRequiredReviews();
assertNotNull(protection.getRequiredReviews());
assertFalse(requiredReviews.isDismissStaleReviews());
assertFalse(requiredReviews.isRequireCodeOwnerReviews());
assertThat(protection.getRequiredReviews().getRequiredReviewers(), equalTo(1));
// Get goes through a different code path. Make sure it also gets the correct data.
protection = branch.getProtection();
requiredReviews = protection.getRequiredReviews();
assertNotNull(protection.getRequiredReviews());
assertFalse(requiredReviews.isDismissStaleReviews());
assertFalse(requiredReviews.isRequireCodeOwnerReviews());
assertThat(protection.getRequiredReviews().getRequiredReviewers(), equalTo(1));
}
@Test
@@ -73,4 +104,12 @@ public class GHBranchProtectionTest extends AbstractGitHubWireMockTest {
protection.disableSignedCommits();
assertFalse(protection.getRequiredSignatures());
}
@Test
public void testGetProtection() throws Exception {
GHBranchProtection protection = branch.enableProtection().enable();
GHBranchProtection protectionTest = repo.getBranch(BRANCH).getProtection();
assertTrue(protectionTest instanceof GHBranchProtection);
assertTrue(repo.getBranch(BRANCH).isProtected());
}
}

View File

@@ -0,0 +1,47 @@
package org.kohsuke.github;
import org.junit.Test;
import java.io.IOException;
import static org.hamcrest.Matchers.*;
public class GHBranchTest extends AbstractGitHubWireMockTest {
private static final String BRANCH_1 = "testBranch1";
private static final String BRANCH_2 = "testBranch2";
private GHRepository repository;
@Test
public void testMergeBranch() throws Exception {
repository = getTempRepository();
String masterHead = repository.getRef("heads/master").getObject().getSha();
createRefAndPostContent(BRANCH_1, masterHead);
createRefAndPostContent(BRANCH_2, masterHead);
GHBranch otherBranch = repository.getBranch(BRANCH_2);
String commitMessage = "merging " + BRANCH_2;
GHCommit mergeCommit = repository.getBranch(BRANCH_1).merge(otherBranch, commitMessage);
assertThat(mergeCommit, notNullValue());
assertThat(mergeCommit.getCommitShortInfo().getMessage(), equalTo(commitMessage));
// Merging commit sha should work
commitMessage = "merging from " + mergeCommit.getSHA1();
GHBranch master = repository.getBranch("master");
mergeCommit = master.merge(mergeCommit.getSHA1(), commitMessage);
assertThat(mergeCommit, notNullValue());
assertThat(mergeCommit.getCommitShortInfo().getMessage(), equalTo(commitMessage));
mergeCommit = master.merge(mergeCommit.getSHA1(), commitMessage);
// Should be null since all changes already merged
assertThat(mergeCommit, nullValue());
}
private void createRefAndPostContent(String branchName, String sha) throws IOException {
String refName = "refs/heads/" + branchName;
repository.createRef(refName, sha);
repository.createContent().content(branchName).message(branchName).path(branchName).branch(branchName).commit();
}
}

View File

@@ -55,7 +55,7 @@ public class GHCheckRunBuilderTest extends AbstractGitHubWireMockTest {
.create();
assertEquals("completed", checkRun.getStatus());
assertEquals(1, checkRun.getOutput().getAnnotationsCount());
assertEquals(546384586, checkRun.id);
assertEquals(546384586, checkRun.getId());
}
@Test
@@ -74,7 +74,7 @@ public class GHCheckRunBuilderTest extends AbstractGitHubWireMockTest {
assertEquals("Big Run", checkRun.getOutput().getTitle());
assertEquals("Lots of stuff here »", checkRun.getOutput().getSummary());
assertEquals(101, checkRun.getOutput().getAnnotationsCount());
assertEquals(546384622, checkRun.id);
assertEquals(546384622, checkRun.getId());
}
@Test
@@ -86,7 +86,7 @@ public class GHCheckRunBuilderTest extends AbstractGitHubWireMockTest {
.create();
assertEquals("completed", checkRun.getStatus());
assertEquals(0, checkRun.getOutput().getAnnotationsCount());
assertEquals(546384705, checkRun.id);
assertEquals(546384705, checkRun.getId());
}
@Test
@@ -97,7 +97,7 @@ public class GHCheckRunBuilderTest extends AbstractGitHubWireMockTest {
.create();
assertEquals("in_progress", checkRun.getStatus());
assertNull(checkRun.getConclusion());
assertEquals(546469053, checkRun.id);
assertEquals(546469053, checkRun.getId());
}
@Test

View File

@@ -1,5 +1,6 @@
package org.kohsuke.github;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -7,9 +8,11 @@ import org.junit.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.hasProperty;
/**
* Integration test for {@link GHContent}.
@@ -26,7 +29,7 @@ public class GHContentIntegrationTest extends AbstractGitHubWireMockTest {
@After
public void cleanup() throws Exception {
if (mockGitHub.isUseProxy()) {
repo = getGitHubBeforeAfter().getRepository("github-api-test-org/GHContentIntegrationTest");
repo = getGitHubBeforeAfter().getRepository("hub4j-test-org/GHContentIntegrationTest");
try {
GHContent content = repo.getFileContent(createdFilename);
if (content != null) {
@@ -39,12 +42,12 @@ public class GHContentIntegrationTest extends AbstractGitHubWireMockTest {
@Before
public void setUp() throws Exception {
repo = gitHub.getRepository("github-api-test-org/GHContentIntegrationTest");
repo = gitHub.getRepository("hub4j-test-org/GHContentIntegrationTest");
}
@Test
public void testGetFileContent() throws Exception {
repo = gitHub.getRepository("github-api-test-org/GHContentIntegrationTest");
repo = gitHub.getRepository("hub4j-test-org/GHContentIntegrationTest");
GHContent content = repo.getFileContent("ghcontent-ro/a-file-with-content");
assertTrue(content.isFile());
@@ -123,7 +126,7 @@ public class GHContentIntegrationTest extends AbstractGitHubWireMockTest {
} catch (GHFileNotFoundException e) {
assertThat(e.getMessage(),
endsWith(
"/repos/github-api-test-org/GHContentIntegrationTest/contents/test+directory%20%2350/test%20file-to+create-%231.txt {\"message\":\"Not Found\",\"documentation_url\":\"https://developer.github.com/v3/repos/contents/#get-contents\"}"));
"/repos/hub4j-test-org/GHContentIntegrationTest/contents/test+directory%20%2350/test%20file-to+create-%231.txt {\"message\":\"Not Found\",\"documentation_url\":\"https://developer.github.com/v3/repos/contents/#get-contents\"}"));
}
}
@@ -158,4 +161,34 @@ public class GHContentIntegrationTest extends AbstractGitHubWireMockTest {
+ "123456789012345678901234567890123456789012345678901234567890");
ghContentBuilder.commit();
}
@Test
public void testGetFileContentWithNonAsciiPath() throws Exception {
final GHRepository repo = gitHub.getRepository("hub4j-test-org/GHContentIntegrationTest");
final GHContent fileContent = repo.getFileContent("ghcontent-ro/a-file-with-\u00F6");
assertThat(IOUtils.readLines(fileContent.read(), StandardCharsets.UTF_8), hasItems("test"));
final GHContent fileContent2 = repo.getFileContent(fileContent.getPath());
assertThat(IOUtils.readLines(fileContent2.read(), StandardCharsets.UTF_8), hasItems("test"));
}
@Test
public void testGetFileContentWithSymlink() throws Exception {
final GHRepository repo = gitHub.getRepository("hub4j-test-org/GHContentIntegrationTest");
final GHContent fileContent = repo.getFileContent("ghcontent-ro/a-symlink-to-a-file");
// for whatever reason GH says this is a file :-o
assertThat(IOUtils.toString(fileContent.read(), StandardCharsets.UTF_8), is("thanks for reading me\n"));
final GHContent dirContent = repo.getFileContent("ghcontent-ro/a-symlink-to-a-dir");
// but symlinks to directories are symlinks!
assertThat(dirContent,
allOf(hasProperty("target", is("a-dir-with-3-entries")), hasProperty("type", is("symlink"))));
// future somehow...
// final GHContent fileContent2 = repo.getFileContent("ghcontent-ro/a-symlink-to-a-dir/entry-one");
// this needs special handling and will 404 from GitHub
// assertThat(IOUtils.toString(fileContent.read(), StandardCharsets.UTF_8), is(""));
}
}

View File

@@ -3,6 +3,8 @@ package org.kohsuke.github;
import org.junit.Test;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
/**
* @author Martin van Zijl
@@ -10,17 +12,42 @@ import java.io.IOException;
public class GHDeploymentTest extends AbstractGitHubWireMockTest {
@Test
public void testGetDeploymentById() throws IOException {
GHRepository repo = getRepository();
GHDeployment deployment = repo.getDeployment(178653229);
public void testGetDeploymentByIdStringPayload() throws IOException {
final GHRepository repo = getRepository();
final GHDeployment deployment = repo.getDeployment(178653229);
assertNotNull(deployment);
assertEquals(178653229, deployment.getId());
assertEquals("production", deployment.getEnvironment());
assertEquals("custom", deployment.getPayload());
assertEquals("custom", deployment.getPayloadObject());
assertEquals("master", deployment.getRef());
assertEquals("3a09d2de4a9a1322a0ba2c3e2f54a919ca8fe353", deployment.getSha());
assertEquals("deploy", deployment.getTask());
}
@Test
public void testGetDeploymentByIdObjectPayload() throws IOException {
final GHRepository repo = getRepository();
final GHDeployment deployment = repo.getDeployment(178653229);
assertNotNull(deployment);
assertEquals(178653229, deployment.getId());
assertEquals("production", deployment.getEnvironment());
assertEquals("master", deployment.getRef());
assertEquals("3a09d2de4a9a1322a0ba2c3e2f54a919ca8fe353", deployment.getSha());
assertEquals("deploy", deployment.getTask());
final Map<String, Object> payload = deployment.getPayloadMap();
assertEquals(4, payload.size());
assertEquals(1, payload.get("custom1"));
assertEquals("two", payload.get("custom2"));
assertEquals(Arrays.asList("3", 3, "three"), payload.get("custom3"));
assertNull(payload.get("custom4"));
}
protected GHRepository getRepository() throws IOException {
return getRepository(gitHub);
}
private GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("github-api");
private GHRepository getRepository(final GitHub gitHub) throws IOException {
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
}
}

View File

@@ -0,0 +1,145 @@
package org.kohsuke.github;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Set;
import static org.hamcrest.Matchers.*;
/**
* @author Charles Moulliard
*/
public class GHDiscussionTest extends AbstractGitHubWireMockTest {
private final String TEAM_SLUG = "dummy-team";
private GHTeam team;
@Before
public void setUp() throws Exception {
team = gitHub.getOrganization(GITHUB_API_TEST_ORG).getTeamBySlug(TEAM_SLUG);
}
@After
public void cleanupDiscussions() throws Exception {
// only need to clean up if we're pointing to the live site
if (mockGitHub.isUseProxy()) {
for (GHDiscussion discussion : getGitHubBeforeAfter().getOrganization(GITHUB_API_TEST_ORG)
.getTeamBySlug(TEAM_SLUG)
.listDiscussions()) {
discussion.delete();
}
}
}
@Test
public void testCreatedDiscussion() throws IOException {
GHDiscussion discussion = team.createDiscussion("Some Discussion").body("This is a public discussion").done();
assertThat(discussion, notNullValue());
assertThat(discussion.getTeam(), equalTo(team));
assertThat(discussion.getTitle(), equalTo("Some Discussion"));
assertThat(discussion.getBody(), equalTo("This is a public discussion"));
assertThat(discussion.isPrivate(), is(false));
discussion = team.createDiscussion("Some Discussion")
.body("This is another public discussion")
.private_(false)
.done();
assertThat(discussion, notNullValue());
assertThat(discussion.getTeam(), equalTo(team));
assertThat(discussion.getTitle(), equalTo("Some Discussion"));
assertThat(discussion.getBody(), equalTo("This is another public discussion"));
assertThat(discussion.isPrivate(), is(false));
discussion = team.createDiscussion("Some Discussion")
.body("This is a private (secret) discussion")
.private_(true)
.done();
assertThat(discussion, notNullValue());
assertThat(discussion.getTeam(), equalTo(team));
assertThat(discussion.getTitle(), equalTo("Some Discussion"));
assertThat(discussion.getBody(), equalTo("This is a private (secret) discussion"));
assertThat(discussion.isPrivate(), is(true));
try {
team.createDiscussion("Some Discussion").done();
fail("Body is required.");
} catch (HttpException e) {
assertThat(e, instanceOf(HttpException.class));
assertThat(e.getMessage(),
containsString("https://developer.github.com/v3/teams/discussions/#create-a-discussion"));
}
}
@Test
public void testGetAndEditDiscussion() throws IOException {
GHDiscussion created = team.createDiscussion("Some Discussion").body("This is a test discussion").done();
GHDiscussion discussion = team.getDiscussion(created.getNumber());
// Test convenience getId() override
assertThat(discussion.getNumber(), equalTo(created.getId()));
assertThat(discussion.getTeam(), equalTo(team));
assertThat(discussion.getTitle(), equalTo("Some Discussion"));
assertThat(discussion.getBody(), equalTo("This is a test discussion"));
assertThat(discussion.isPrivate(), is(false));
// Test equality
assertThat(discussion, equalTo(created));
discussion = discussion.set().body("This is a test discussion changed");
assertThat(discussion.getTeam(), notNullValue());
assertThat(discussion.getTitle(), equalTo("Some Discussion"));
assertThat(discussion.getBody(), equalTo("This is a test discussion changed"));
discussion = discussion.set().title("Title changed");
assertThat(discussion.getTitle(), equalTo("Title changed"));
assertThat(discussion.getBody(), equalTo("This is a test discussion changed"));
GHDiscussion discussion2 = gitHub.getOrganization(GITHUB_API_TEST_ORG)
.getTeamBySlug(TEAM_SLUG)
.getDiscussion(discussion.getNumber());
assertThat(discussion2, equalTo(discussion));
assertThat(discussion2.getTitle(), equalTo("Title changed"));
assertThat(discussion2.getBody(), equalTo("This is a test discussion changed"));
discussion = discussion.update().body("This is a test discussion updated").title("Title updated").done();
assertThat(discussion.getTeam(), notNullValue());
assertThat(discussion.getTitle(), equalTo("Title updated"));
assertThat(discussion.getBody(), equalTo("This is a test discussion updated"));
}
@Test
public void testListDiscussion() throws IOException {
team.createDiscussion("Some Discussion A").body("This is a test discussion").done();
team.createDiscussion("Some Discussion B").body("This is a test discussion").done();
team.createDiscussion("Some Discussion C").body("This is a test discussion").done();
Set<GHDiscussion> all = team.listDiscussions().toSet();
assertThat(all.size(), equalTo(3));
}
@Test
public void testToDeleteDiscussion() throws IOException {
GHDiscussion discussion = team.createDiscussion("Some Discussion").body("This is a test discussion").done();
assertThat(discussion.getTitle(), equalTo("Some Discussion"));
discussion.delete();
try {
gitHub.getOrganization(GITHUB_API_TEST_ORG).getTeamBySlug(TEAM_SLUG).getDiscussion(discussion.getNumber());
fail();
} catch (FileNotFoundException e) {
assertThat(e.getMessage(),
containsString("https://developer.github.com/v3/teams/discussions/#get-a-single-discussion"));
}
}
}

View File

@@ -3,19 +3,22 @@ package org.kohsuke.github;
import org.junit.Rule;
import org.junit.Test;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.TimeZone;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
public class GHEventPayloadTest {
public class GHEventPayloadTest extends AbstractGitHubWireMockTest {
@Rule
public final PayloadRule payload = new PayloadRule(".json");
public GHEventPayloadTest() {
useDefaultGitHub = false;
}
@Test
public void commit_comment() throws Exception {
GHEventPayload.CommitComment event = GitHub.offline()
@@ -283,6 +286,70 @@ public class GHEventPayloadTest {
assertThat(event.getSender().getLogin(), is("baxterthehacker"));
}
@Test
@Payload("push.fork")
public void pushToFork() throws Exception {
gitHub = getGitHubBuilder().withEndpoint(mockGitHub.apiServer().baseUrl()).build();
GHEventPayload.Push event = GitHub.offline().parseEventPayload(payload.asReader(), GHEventPayload.Push.class);
assertThat(event.getRef(), is("refs/heads/changes"));
assertThat(event.getBefore(), is("85c44b352958bf6d81b74ab8b21920f1d313a287"));
assertThat(event.getHead(), is("1393706f1364742defbc28ba459082630ca979af"));
assertThat(event.isCreated(), is(false));
assertThat(event.isDeleted(), is(false));
assertThat(event.isForced(), is(false));
assertThat(event.getCommits().size(), is(1));
assertThat(event.getCommits().get(0).getSha(), is("1393706f1364742defbc28ba459082630ca979af"));
assertThat(event.getCommits().get(0).getAuthor().getEmail(), is("bitwiseman@gmail.com"));
assertThat(event.getCommits().get(0).getCommitter().getEmail(), is("bitwiseman@gmail.com"));
assertThat(event.getCommits().get(0).getAdded().size(), is(6));
assertThat(event.getCommits().get(0).getRemoved().size(), is(0));
assertThat(event.getCommits().get(0).getModified().size(), is(2));
assertThat(event.getCommits().get(0).getModified().get(0),
is("src/main/java/org/kohsuke/github/GHLicense.java"));
assertThat(event.getRepository().getName(), is("github-api"));
assertThat(event.getRepository().getOwnerName(), is("hub4j-test-org"));
assertThat(event.getRepository().getUrl().toExternalForm(), is("https://github.com/hub4j-test-org/github-api"));
assertThat(event.getPusher().getName(), is("bitwiseman"));
assertThat(event.getPusher().getEmail(), is("bitwiseman@gmail.com"));
assertThat(event.getSender().getLogin(), is("bitwiseman"));
assertThat(event.getRepository().isFork(), is(true));
// in offliine mode, we should not populate missing fields
assertThat(event.getRepository().getSource(), is(nullValue()));
assertThat(event.getRepository().getParent(), is(nullValue()));
assertThat(event.getRepository().getUrl().toString(), is("https://github.com/hub4j-test-org/github-api"));
assertThat(event.getRepository().getHttpTransportUrl().toString(),
is("https://github.com/hub4j-test-org/github-api.git"));
// Test repository populate
event = gitHub.parseEventPayload(payload.asReader(mockGitHub::mapToMockGitHub), GHEventPayload.Push.class);
assertThat(event.getRepository().getUrl().toString(), is("https://github.com/hub4j-test-org/github-api"));
assertThat(event.getRepository().getHttpTransportUrl(), is("https://github.com/hub4j-test-org/github-api.git"));
event.getRepository().populate();
// After populate the url is fixed to point to the correct API endpoint
assertThat(event.getRepository().getUrl().toString(),
is(mockGitHub.apiServer().baseUrl() + "/repos/hub4j-test-org/github-api"));
assertThat(event.getRepository().getHttpTransportUrl(), is("https://github.com/hub4j-test-org/github-api.git"));
// ensure that root has been bound after populate
event.getRepository().getSource().getRef("heads/master");
event.getRepository().getParent().getRef("heads/master");
// Source
event = gitHub.parseEventPayload(payload.asReader(mockGitHub::mapToMockGitHub), GHEventPayload.Push.class);
assertThat(event.getRepository().getSource().getFullName(), is("hub4j/github-api"));
// Parent
event = gitHub.parseEventPayload(payload.asReader(mockGitHub::mapToMockGitHub), GHEventPayload.Push.class);
assertThat(event.getRepository().getParent().getFullName(), is("hub4j/github-api"));
}
// TODO implement support classes and write test
// @Test
// public void release() throws Exception {}
@@ -321,7 +388,25 @@ public class GHEventPayloadTest {
@Payload("check-run")
public void checkRunEvent() throws Exception {
GHEventPayload.CheckRun event = GitHub.offline()
.parseEventPayload(payload.asReader(), GHEventPayload.CheckRun.class);
.parseEventPayload(payload.asReader(mockGitHub::mapToMockGitHub), GHEventPayload.CheckRun.class);
GHCheckRun checkRun = verifyBasicCheckRunEvent(event);
assertThat("pull body not populated offline", checkRun.getPullRequests().get(0).getBody(), nullValue());
assertThat("using offline github", mockGitHub.getRequestCount(), equalTo(0));
gitHub = getGitHubBuilder().withEndpoint(mockGitHub.apiServer().baseUrl()).build();
event = gitHub.parseEventPayload(payload.asReader(mockGitHub::mapToMockGitHub), GHEventPayload.CheckRun.class);
checkRun = verifyBasicCheckRunEvent(event);
int expectedRequestCount = mockGitHub.isUseProxy() ? 3 : 2;
assertThat("pull body should be populated",
checkRun.getPullRequests().get(0).getBody(),
equalTo("This is a pretty simple change that we need to pull into master."));
assertThat("multiple getPullRequests() calls are made, the pull is populated only once",
mockGitHub.getRequestCount(),
equalTo(expectedRequestCount));
}
private GHCheckRun verifyBasicCheckRunEvent(GHEventPayload.CheckRun event) throws IOException {
assertThat(event.getRepository().getName(), is("Hello-World"));
assertThat(event.getRepository().getOwner().getLogin(), is("Codertocat"));
assertThat(event.getAction(), is("created"));
@@ -340,9 +425,9 @@ public class GHEventPayloadTest {
assertThat(formatter.format(checkRun.getCompletedAt()), is("2019-05-15T20:22:22Z"));
assertThat(checkRun.getConclusion(), is("success"));
assertThat(checkRun.getUrl().toString(),
is("https://api.github.com/repos/Codertocat/Hello-World/check-runs/128620228"));
assertThat(checkRun.getHtmlUrl().toString(), is("https://github.com/Codertocat/Hello-World/runs/128620228"));
assertThat(checkRun.getUrl().toString(), endsWith("/repos/Codertocat/Hello-World/check-runs/128620228"));
assertThat(checkRun.getHtmlUrl().toString(),
endsWith("https://github.com/Codertocat/Hello-World/runs/128620228"));
assertThat(checkRun.getDetailsUrl().toString(), is("https://octocoders.io"));
assertThat(checkRun.getApp().getId(), is(29310L));
assertThat(checkRun.getCheckSuite().getId(), is(118578147L));
@@ -351,18 +436,41 @@ public class GHEventPayloadTest {
assertThat(checkRun.getOutput().getText(), nullValue());
assertThat(checkRun.getOutput().getAnnotationsCount(), is(0));
assertThat(checkRun.getOutput().getAnnotationsUrl().toString(),
is("https://api.github.com/repos/Codertocat/Hello-World/check-runs/128620228/annotations"));
endsWith("/repos/Codertocat/Hello-World/check-runs/128620228/annotations"));
// Checks the deserialization of sender
assertThat(event.getSender().getId(), is(21031067L));
assertThat(checkRun.getPullRequests(), notNullValue());
assertThat(checkRun.getPullRequests().size(), equalTo(1));
assertThat(checkRun.getPullRequests().get(0).getNumber(), equalTo(2));
return checkRun;
}
@Test
@Payload("check-suite")
public void checkSuiteEvent() throws Exception {
GHEventPayload.CheckSuite event = GitHub.offline()
.parseEventPayload(payload.asReader(), GHEventPayload.CheckSuite.class);
.parseEventPayload(payload.asReader(mockGitHub::mapToMockGitHub), GHEventPayload.CheckSuite.class);
GHCheckSuite checkSuite = verifyBasicCheckSuiteEvent(event);
assertThat("pull body not populated offline", checkSuite.getPullRequests().get(0).getBody(), nullValue());
assertThat("using offline github", mockGitHub.getRequestCount(), equalTo(0));
gitHub = getGitHubBuilder().withEndpoint(mockGitHub.apiServer().baseUrl()).build();
event = gitHub.parseEventPayload(payload.asReader(mockGitHub::mapToMockGitHub),
GHEventPayload.CheckSuite.class);
checkSuite = verifyBasicCheckSuiteEvent(event);
int expectedRequestCount = mockGitHub.isUseProxy() ? 3 : 2;
assertThat("pull body should be populated",
checkSuite.getPullRequests().get(0).getBody(),
equalTo("This is a pretty simple change that we need to pull into master."));
assertThat("multiple getPullRequests() calls are made, the pull is populated only once",
mockGitHub.getRequestCount(),
lessThanOrEqualTo(expectedRequestCount));
}
private GHCheckSuite verifyBasicCheckSuiteEvent(GHEventPayload.CheckSuite event) throws IOException {
assertThat(event.getRepository().getName(), is("Hello-World"));
assertThat(event.getRepository().getOwner().getLogin(), is("Codertocat"));
assertThat(event.getAction(), is("completed"));
@@ -379,7 +487,7 @@ public class GHEventPayloadTest {
assertThat(checkSuite.getAfter(), is("ec26c3e57ca3a959ca5aad62de7213c562f8c821"));
assertThat(checkSuite.getLatestCheckRunsCount(), is(1));
assertThat(checkSuite.getCheckRunsUrl().toString(),
is("https://api.github.com/repos/Codertocat/Hello-World/check-suites/118578147/check-runs"));
endsWith("/repos/Codertocat/Hello-World/check-suites/118578147/check-runs"));
assertThat(checkSuite.getHeadCommit().getMessage(), is("Update README.md"));
assertThat(checkSuite.getHeadCommit().getId(), is("ec26c3e57ca3a959ca5aad62de7213c562f8c821"));
assertThat(checkSuite.getHeadCommit().getTreeId(), is("31b122c26a97cf9af023e9ddab94a82c6e77b0ea"));
@@ -391,6 +499,11 @@ public class GHEventPayloadTest {
assertThat(formatter.format(checkSuite.getHeadCommit().getTimestamp()), is("2019-05-15T15:20:30Z"));
assertThat(checkSuite.getApp().getId(), is(29310L));
assertThat(checkSuite.getPullRequests(), notNullValue());
assertThat(checkSuite.getPullRequests().size(), equalTo(1));
assertThat(checkSuite.getPullRequests().get(0).getNumber(), equalTo(2));
return checkSuite;
}
@Test

View File

@@ -2,7 +2,9 @@ package org.kohsuke.github;
import org.junit.Test;
import static org.hamcrest.Matchers.notNullValue;
import java.io.FileNotFoundException;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.Is.is;
/**
@@ -17,9 +19,12 @@ public class GHGistTest extends AbstractGitHubWireMockTest {
.description("Test Gist")
.file("abc.txt", "abc")
.file("def.txt", "def")
.file("ghi.txt", "ghi")
.create();
assertThat(gist.getCreatedAt(), is(notNullValue()));
assertThat(gist.getDescription(), equalTo("Test Gist"));
assertThat(gist.getFiles().size(), equalTo(3));
assertNotNull(gist.getUpdatedAt());
assertNotNull(gist.getCommentsUrl());
@@ -28,7 +33,66 @@ public class GHGistTest extends AbstractGitHubWireMockTest {
assertNotNull(gist.getGitPushUrl());
assertNotNull(gist.getHtmlUrl());
String id = gist.getGistId();
GHGist gistUpdate = gitHub.getGist(id);
assertThat(gistUpdate.getGistId(), equalTo(gist.getGistId()));
assertThat(gistUpdate.getDescription(), equalTo(gist.getDescription()));
assertThat(gistUpdate.getFiles().size(), equalTo(3));
gistUpdate = gistUpdate.update().description("Gist Test").addFile("jkl.txt", "jkl").update();
assertThat(gistUpdate.getGistId(), equalTo(gist.getGistId()));
assertThat(gistUpdate.getDescription(), equalTo("Gist Test"));
assertThat(gistUpdate.getFiles().size(), equalTo(4));
gistUpdate = gistUpdate.update()
.renameFile("abc.txt", "ab.txt")
.deleteFile("def.txt")
.updateFile("ghi.txt", "gh")
.updateFile("jkl.txt", "klm.txt", "nop")
.update();
assertThat(gistUpdate.getGistId(), equalTo(gist.getGistId()));
assertThat(gistUpdate.getDescription(), equalTo("Gist Test"));
assertThat(gistUpdate.getFiles().size(), equalTo(3));
// verify delete works
assertThat(gistUpdate.getFile("def.txt"), nullValue());
// verify rename
assertThat(gistUpdate.getFile("ab.txt").getContent(), equalTo("abc"));
// verify updates
assertThat(gistUpdate.getFile("ghi.txt").getContent(), equalTo("gh"));
assertThat(gistUpdate.getFile("klm.txt").getContent(), equalTo("nop"));
// rename and update on the same file in one update shoudl work.
gistUpdate = gistUpdate.update().renameFile("ab.txt", "a.txt").updateFile("ab.txt", "abcd").update();
assertThat(gistUpdate.getGistId(), equalTo(gist.getGistId()));
assertThat(gistUpdate.getFiles().size(), equalTo(3));
// verify rename and update
assertThat(gistUpdate.getFile("a.txt").getContent(), equalTo("abcd"));
try {
gist.getId();
fail("Newly created gists do not have numeric ids.");
} catch (NumberFormatException e) {
assertThat(e, notNullValue());
}
assertThat(gist.getGistId(), notNullValue());
gist.delete();
try {
gitHub.getGist(id);
fail("Gist should be deleted.");
} catch (FileNotFoundException e) {
assertThat(e, notNullValue());
}
}
@Test
@@ -63,6 +127,8 @@ public class GHGistTest extends AbstractGitHubWireMockTest {
GHGist gist = gitHub.getGist("9903708");
assertTrue(gist.isPublic());
assertThat(gist.getId(), equalTo(9903708L));
assertThat(gist.getGistId(), equalTo("9903708"));
assertEquals(1, gist.getFiles().size());
GHGistFile f = gist.getFile("keybase.md");

View File

@@ -0,0 +1,59 @@
package org.kohsuke.github;
import org.junit.Test;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.StreamSupport;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.Matchers.hasSize;
public class GHIssueEventAttributeTest extends AbstractGitHubWireMockTest {
private enum Type implements Predicate<GHIssueEvent>, Consumer<GHIssueEvent> {
milestone(e -> assertNotNull(e.getMilestone()), "milestoned", "demilestoned"),
label(e -> assertNotNull(e.getLabel()), "labeled", "unlabeled"),
assignment(e -> assertNotNull(e.getAssignee()), "assigned", "unassigned");
private final Consumer<GHIssueEvent> assertion;
private final Set<String> subtypes;
Type(final Consumer<GHIssueEvent> assertion, final String... subtypes) {
this.assertion = assertion;
this.subtypes = new HashSet<>(asList(subtypes));
}
@Override
public boolean test(final GHIssueEvent event) {
return this.subtypes.contains(event.getEvent());
}
@Override
public void accept(final GHIssueEvent event) {
this.assertion.accept(event);
}
}
private List<GHIssueEvent> listEvents(final Type type) throws IOException {
return StreamSupport
.stream(gitHub.getRepository("chids/project-milestone-test").getIssue(1).listEvents().spliterator(),
false)
.filter(type)
.collect(toList());
}
@Test
public void testEventSpecificAttributes() throws IOException {
for (Type type : Type.values()) {
final List<GHIssueEvent> events = listEvents(type);
assertThat(events, hasSize(2));
events.forEach(type);
}
}
}

View File

@@ -56,6 +56,6 @@ public class GHIssueEventTest extends AbstractGitHubWireMockTest {
}
private GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("github-api");
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
}
}

View File

@@ -30,6 +30,8 @@ import org.junit.Test;
import java.io.IOException;
import java.net.URL;
import static org.hamcrest.Matchers.*;
/**
* @author Duncan Dickinson
*/
@@ -91,7 +93,7 @@ public class GHLicenseTest extends AbstractGitHubWireMockTest {
*/
@Test
public void checkRepositoryLicense() throws IOException {
GHRepository repo = gitHub.getRepository("github-api/github-api");
GHRepository repo = gitHub.getRepository("hub4j/github-api");
GHLicense license = repo.getLicense();
assertNotNull("The license is populated", license);
assertTrue("The key is correct", license.getKey().equals("mit"));
@@ -157,7 +159,7 @@ public class GHLicenseTest extends AbstractGitHubWireMockTest {
*/
@Test
public void checkRepositoryFullLicense() throws IOException {
GHRepository repo = gitHub.getRepository("github-api/github-api");
GHRepository repo = gitHub.getRepository("hub4j/github-api");
GHLicense license = repo.getLicense();
assertNotNull("The license is populated", license);
assertTrue("The key is correct", license.getKey().equals("mit"));
@@ -190,4 +192,21 @@ public class GHLicenseTest extends AbstractGitHubWireMockTest {
fail("Expected the license to be Base64 encoded but instead it was " + content.getEncoding());
}
}
/**
* Accesses the 'bndtools/bnd' repo using {@link GitHub#getRepository(String)} and then calls
* {@link GHRepository#getLicense()}. The description is null due to multiple licences
*
* @throws IOException
* if test fails
*/
@Test
public void checkRepositoryLicenseForIndeterminate() throws IOException {
GHRepository repo = gitHub.getRepository("bndtools/bnd");
GHLicense license = repo.getLicense();
assertNotNull("The license is populated", license);
assertThat(license.getKey(), equalTo("other"));
assertThat(license.getDescription(), is(nullValue()));
assertThat(license.getUrl(), is(nullValue()));
}
}

View File

@@ -70,11 +70,31 @@ public class GHMilestoneTest extends AbstractGitHubWireMockTest {
assertEquals(null, issue.getMilestone());
}
@Test
public void testUnsetMilestoneFromPullRequest() throws IOException {
GHRepository repo = getRepository();
GHMilestone milestone = repo.createMilestone("Unset Test Milestone", "For testUnsetMilestone");
GHPullRequest p = repo.createPullRequest("testUnsetMilestoneFromPullRequest",
"test/stable",
"master",
"## test pull request");
// set the milestone
p.setMilestone(milestone);
p = repo.getPullRequest(p.getNumber()); // force reload
assertEquals(milestone.getNumber(), p.getMilestone().getNumber());
// remove the milestone
p.setMilestone(null);
p = repo.getPullRequest(p.getNumber()); // force reload
assertNull(p.getMilestone());
}
protected GHRepository getRepository() throws IOException {
return getRepository(gitHub);
}
private GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("github-api");
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
}
}

View File

@@ -11,7 +11,7 @@ public class GHObjectTest extends org.kohsuke.github.AbstractGitHubWireMockTest
GHOrganization org = gitHub.getOrganization(GITHUB_API_TEST_ORG);
assertThat(org.toString(),
containsString(
"login=github-api-test-org,location=<null>,blog=<null>,email=<null>,name=<null>,company=<null>,type=Organization,followers=0,following=0"));
"login=hub4j-test-org,location=<null>,blog=<null>,email=<null>,bio=<null>,name=<null>,company=<null>,type=Organization,followers=0,following=0,hireable=false"));
// getResponseHeaderFields is deprecated but we should not break it.
assertThat(org.getResponseHeaderFields(), notNullValue());

View File

@@ -9,9 +9,12 @@ import org.kohsuke.github.GHOrganization.Permission;
import java.io.IOException;
import java.util.List;
import static org.hamcrest.Matchers.*;
public class GHOrganizationTest extends AbstractGitHubWireMockTest {
public static final String GITHUB_API_TEST = "github-api-test";
public static final String GITHUB_API_TEMPLATE_TEST = "github-api-template-test";
public static final String TEAM_NAME_CREATE = "create-team-test";
@Before
@@ -56,6 +59,58 @@ public class GHOrganizationTest extends AbstractGitHubWireMockTest {
Assert.assertNotNull(repository.getReadme());
}
@Test
public void testCreateRepositoryWithParameterIsTemplate() throws IOException {
cleanupRepository(GITHUB_API_TEST_ORG + '/' + GITHUB_API_TEMPLATE_TEST);
GHOrganization org = gitHub.getOrganization(GITHUB_API_TEST_ORG);
GHTeam team = org.getTeamByName("Core Developers");
int requestCount = mockGitHub.getRequestCount();
GHRepository repository = org.createRepository(GITHUB_API_TEMPLATE_TEST)
.description("a test template repository used to test kohsuke's github-api")
.homepage("http://github-api.kohsuke.org/")
.team(team)
.autoInit(true)
.templateRepository(true)
.create();
Assert.assertNotNull(repository);
assertThat(mockGitHub.getRequestCount(), equalTo(requestCount + 1));
Assert.assertNotNull(repository.getReadme());
assertThat(mockGitHub.getRequestCount(), equalTo(requestCount + 2));
// isTemplate() does not call populate() from create
assertThat(repository.isTemplate(), equalTo(true));
assertThat(mockGitHub.getRequestCount(), equalTo(requestCount + 2));
repository = org.getRepository(GITHUB_API_TEMPLATE_TEST);
// first isTemplate() calls populate()
assertThat(repository.isTemplate(), equalTo(true));
assertThat(mockGitHub.getRequestCount(), equalTo(requestCount + 4));
// second isTemplate() does not call populate()
assertThat(repository.isTemplate(), equalTo(true));
assertThat(mockGitHub.getRequestCount(), equalTo(requestCount + 4));
}
@Test
public void testCreateRepositoryWithTemplate() throws IOException {
cleanupRepository(GITHUB_API_TEST_ORG + '/' + GITHUB_API_TEST);
GHOrganization org = gitHub.getOrganization(GITHUB_API_TEST_ORG);
GHRepository repository = org.createRepository(GITHUB_API_TEST)
.fromTemplateRepository(GITHUB_API_TEST_ORG, GITHUB_API_TEMPLATE_TEST)
.owner(GITHUB_API_TEST_ORG)
.create();
Assert.assertNotNull(repository);
Assert.assertNotNull(repository.getReadme());
}
@Test
public void testInviteUser() throws IOException {
GHOrganization org = gitHub.getOrganization(GITHUB_API_TEST_ORG);

View File

@@ -28,6 +28,6 @@ public class GHPersonTest extends AbstractGitHubWireMockTest {
}
private GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("github-api");
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
}
}

View File

@@ -122,7 +122,7 @@ public class GHPullRequestTest extends AbstractGitHubWireMockTest {
// Assert htmlUrl is not null
assertNotNull(comment.getHtmlUrl());
assertEquals(new URL("https://github.com/github-api-test-org/github-api/pull/266#discussion_r321995146"),
assertEquals(new URL("https://github.com/hub4j-test-org/github-api/pull/266#discussion_r321995146"),
comment.getHtmlUrl());
comment.update("Updated review comment");
@@ -156,7 +156,7 @@ public class GHPullRequestTest extends AbstractGitHubWireMockTest {
// System.out.println(p.getUrl());
assertTrue(p.getRequestedReviewers().isEmpty());
GHOrganization testOrg = gitHub.getOrganization("github-api-test-org");
GHOrganization testOrg = gitHub.getOrganization("hub4j-test-org");
GHTeam testTeam = testOrg.getTeamBySlug("dummy-team");
p.requestTeamReviewers(Collections.singletonList(testTeam));
@@ -258,7 +258,7 @@ public class GHPullRequestTest extends AbstractGitHubWireMockTest {
// Query by one of the heads and make sure we only get that branch's PR back.
List<GHPullRequest> prs = repo.queryPullRequests()
.state(GHIssueState.OPEN)
.head("github-api-test-org:test/stable")
.head("hub4j-test-org:test/stable")
.base("master")
.list()
.toList();
@@ -329,6 +329,6 @@ public class GHPullRequestTest extends AbstractGitHubWireMockTest {
}
private GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("github-api");
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
}
}

View File

@@ -3,13 +3,15 @@ package org.kohsuke.github;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
import java.io.IOException;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
@@ -51,13 +53,16 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
public void testGitHubRateLimit() throws Exception {
// Customized response that templates the date to keep things working
snapshotNotAllowed();
GHRateLimit.UnknownLimitRecord.reset();
assertThat(mockGitHub.getRequestCount(), equalTo(0));
// 4897 is just the what the limit was when the snapshot was taken
previousLimit = GHRateLimit.fromHeaderRecord(new GHRateLimit.Record(5000,
4897,
(templating.testStartDate.getTime() + Duration.ofHours(1).toMillis()) / 1000L));
previousLimit = GHRateLimit.fromRecord(
new GHRateLimit.Record(5000,
4897,
(templating.testStartDate.getTime() + Duration.ofHours(1).toMillis()) / 1000L),
RateLimitTarget.CORE);
// -------------------------------------------------------------
// /user gets response with rate limit information
@@ -76,8 +81,8 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
// Give this a moment
Thread.sleep(1500);
// ratelimit() uses headerRateLimit if available and headerRateLimit is not expired
assertThat(gitHub.rateLimit(), equalTo(headerRateLimit));
// ratelimit() uses cached rate limit if available and not expired
assertThat(gitHub.rateLimit(), sameInstance(headerRateLimit));
assertThat(mockGitHub.getRequestCount(), equalTo(1));
@@ -88,8 +93,13 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
rateLimit = gitHub.getRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(2));
// Because remaining and reset date are unchanged, the header should be unchanged as well
assertThat(gitHub.lastRateLimit(), sameInstance(headerRateLimit));
// Because remaining and reset date are unchanged in core, the header should be unchanged as well
// But the overall instance has changed because of filling in of unknown data.
assertThat(gitHub.lastRateLimit(), not(sameInstance(headerRateLimit)));
// Identical Records should be preserved even when GHRateLimit is merged
assertThat(gitHub.lastRateLimit().getCore(), sameInstance(headerRateLimit.getCore()));
assertThat(gitHub.lastRateLimit().getSearch(), not(sameInstance(headerRateLimit.getSearch())));
headerRateLimit = gitHub.lastRateLimit();
// rate limit request is free, remaining is unchanged
verifyRateLimitValues(previousLimit, previousLimit.getRemaining());
@@ -141,9 +151,8 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
verifyRateLimitValues(previousLimit, previousLimit.getRemaining(), true);
previousLimit = rateLimit;
// When getRateLimit() succeeds, headerRateLimit updates as usual as well (if needed)
// These are separate instances, but should be equal
assertThat(gitHub.rateLimit(), not(sameInstance(rateLimit)));
// When getRateLimit() succeeds, cached rate limit updates as usual as well (if needed)
assertThat(gitHub.rateLimit(), sameInstance(rateLimit));
// Verify different record instances can be compared
assertThat(gitHub.rateLimit().getCore(), equalTo(rateLimit.getCore()));
@@ -154,8 +163,43 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
assertThat(gitHub.rateLimit(), not(sameInstance(headerRateLimit)));
assertThat(gitHub.rateLimit(), sameInstance(gitHub.lastRateLimit()));
headerRateLimit = gitHub.lastRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(5));
// Verify the requesting a search url updates the search rate limit
assertThat(gitHub.lastRateLimit().getSearch().getRemaining(), equalTo(30));
HashMap<String, Object> searchResult = (HashMap<String, Object>) gitHub.createRequest()
.rateLimit(RateLimitTarget.SEARCH)
.setRawUrlPath(mockGitHub.apiServer().baseUrl()
+ "/search/repositories?q=tetris+language%3Aassembly&sort=stars&order=desc")
.fetch(HashMap.class);
assertThat(searchResult.get("total_count"), equalTo(1918));
assertThat(mockGitHub.getRequestCount(), equalTo(6));
assertThat(gitHub.lastRateLimit(), not(sameInstance(headerRateLimit)));
assertThat(gitHub.lastRateLimit().getCore(), sameInstance(headerRateLimit.getCore()));
assertThat(gitHub.lastRateLimit().getSearch(), not(sameInstance(headerRateLimit.getSearch())));
assertThat(gitHub.lastRateLimit().getSearch().getRemaining(), equalTo(29));
PagedSearchIterable<GHRepository> searchResult2 = gitHub.searchRepositories()
.q("tetris")
.language("assembly")
.sort(GHRepositorySearchBuilder.Sort.STARS)
.order(GHDirection.DESC)
.list();
assertThat(searchResult2.getTotalCount(), equalTo(1918));
assertThat(mockGitHub.getRequestCount(), equalTo(7));
assertThat(gitHub.lastRateLimit(), not(sameInstance(headerRateLimit)));
assertThat(gitHub.lastRateLimit().getCore(), sameInstance(headerRateLimit.getCore()));
assertThat(gitHub.lastRateLimit().getSearch(), not(sameInstance(headerRateLimit.getSearch())));
assertThat(gitHub.lastRateLimit().getSearch().getRemaining(), equalTo(28));
}
private void verifyRateLimitValues(GHRateLimit previousLimit, int remaining) {
@@ -194,6 +238,8 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
public void testGitHubEnterpriseDoesNotHaveRateLimit() throws Exception {
// Customized response that results in file not found the same as GitHub Enterprise
snapshotNotAllowed();
GHRateLimit.UnknownLimitRecord.reset();
assertThat(mockGitHub.getRequestCount(), equalTo(0));
GHRateLimit rateLimit = null;
@@ -203,13 +249,14 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
Thread.sleep(1500);
// -------------------------------------------------------------
// Before any queries, rate limit starts as null but may be requested
// Before any queries, rate limit starts as default but may be requested
gitHub = GitHub.connectToEnterprise(mockGitHub.apiServer().baseUrl(), "bogus", "bogus");
assertThat(mockGitHub.getRequestCount(), equalTo(0));
assertThat(gitHub.lastRateLimit(), CoreMatchers.nullValue());
assertThat(gitHub.lastRateLimit(), sameInstance(GHRateLimit.DEFAULT));
rateLimit = gitHub.rateLimit();
assertThat(gitHub.lastRateLimit(), not(equalTo(GHRateLimit.DEFAULT)));
assertThat(rateLimit.getCore(), instanceOf(GHRateLimit.UnknownLimitRecord.class));
assertThat(rateLimit.getLimit(), equalTo(GHRateLimit.UnknownLimitRecord.unknownLimit));
assertThat(rateLimit.getRemaining(), equalTo(GHRateLimit.UnknownLimitRecord.unknownRemaining));
@@ -218,8 +265,8 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
assertThat(mockGitHub.getRequestCount(), equalTo(1));
// last is still null, because it actually means lastHeaderRateLimit
assertThat(gitHub.lastRateLimit(), CoreMatchers.nullValue());
// lastRateLimit the same as rateLimit
assertThat(gitHub.lastRateLimit(), sameInstance(rateLimit));
assertThat(mockGitHub.getRequestCount(), equalTo(1));
@@ -227,18 +274,20 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
Thread.sleep(1500);
// -------------------------------------------------------------
// First call to /user gets response without rate limit information
// Some versions of GHE include header rate limit information, some do not
// This response mocks the behavior without header rate limit information
gitHub = GitHub.connectToEnterprise(mockGitHub.apiServer().baseUrl(), "bogus", "bogus");
gitHub.getMyself();
assertThat(mockGitHub.getRequestCount(), equalTo(2));
assertThat(gitHub.lastRateLimit(), CoreMatchers.nullValue());
assertThat(gitHub.lastRateLimit(), sameInstance(GHRateLimit.DEFAULT));
rateLimit = gitHub.rateLimit();
assertThat(rateLimit.getCore(), instanceOf(GHRateLimit.UnknownLimitRecord.class));
assertThat(rateLimit.getLimit(), equalTo(GHRateLimit.UnknownLimitRecord.unknownLimit));
assertThat(rateLimit.getRemaining(), equalTo(GHRateLimit.UnknownLimitRecord.unknownRemaining));
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(1));
// Same unknown instance is reused for a while
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(0));
lastReset = rateLimit.getResetDate();
assertThat(mockGitHub.getRequestCount(), equalTo(3));
@@ -246,6 +295,10 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
// Give this a moment
Thread.sleep(1500);
// -------------------------------------------------------------
// GHE returns 404 from /rate_limit
// Some versions of GHE include header rate limit information, some do not
// This response mocks the behavior without header rate limit information
// Always requests new info
rateLimit = gitHub.getRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(4));
@@ -253,33 +306,38 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
assertThat(rateLimit.getCore(), instanceOf(GHRateLimit.UnknownLimitRecord.class));
assertThat(rateLimit.getLimit(), equalTo(GHRateLimit.UnknownLimitRecord.unknownLimit));
assertThat(rateLimit.getRemaining(), equalTo(GHRateLimit.UnknownLimitRecord.unknownRemaining));
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(1));
// When not expired, unknowns do not replace each other so last reset remains unchanged
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(0));
// Give this a moment
Thread.sleep(1500);
// last is still null, because it actually means lastHeaderRateLimit
assertThat(gitHub.lastRateLimit(), CoreMatchers.nullValue());
// lastRateLimit the same as rateLimit
assertThat(gitHub.lastRateLimit(), sameInstance(rateLimit));
// ratelimit() tries not to make additional requests, uses queried rate limit since header not available
Thread.sleep(1500);
assertThat(gitHub.rateLimit(), sameInstance(rateLimit));
assertThat(mockGitHub.getRequestCount(), equalTo(4));
// -------------------------------------------------------------
// Second call to /user gets response with rate limit information
// Some versions of GHE include header rate limit information, some do not
// This response mocks the behavior with header rate limit information
gitHub = GitHub.connectToEnterprise(mockGitHub.apiServer().baseUrl(), "bogus", "bogus");
gitHub.getMyself();
assertThat(mockGitHub.getRequestCount(), equalTo(5));
// Since we already had rate limit info these don't request again
// Since just got rate limit from header, these don't request again
rateLimit = gitHub.lastRateLimit();
assertThat(rateLimit, notNullValue());
assertThat(rateLimit.getLimit(), equalTo(5000));
assertThat(rateLimit.getRemaining(), equalTo(4978));
// The previous record was an "Unknown", so even though this records resets sooner we take it
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(-1));
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(1));
lastReset = rateLimit.getResetDate();
// When getting only header updates, the unknowns are also expired
assertThat(rateLimit.getSearch().isExpired(), is(true));
GHRateLimit headerRateLimit = rateLimit;
// Give this a moment
@@ -293,22 +351,70 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
// Give this a moment
Thread.sleep(1500);
// -------------------------------------------------------------
// GHE returns 404 from /rate_limit
// Some versions of GHE include header rate limit information, some do not
// This response mocks the behavior with header rate limit information
// Mocks that other requests come in since previous call
// Always requests new info
rateLimit = gitHub.getRateLimit();
assertThat(mockGitHub.getRequestCount(), equalTo(6));
assertThat(rateLimit.getCore(), instanceOf(GHRateLimit.UnknownLimitRecord.class));
assertThat(rateLimit.getLimit(), equalTo(GHRateLimit.UnknownLimitRecord.unknownLimit));
assertThat(rateLimit.getRemaining(), equalTo(GHRateLimit.UnknownLimitRecord.unknownRemaining));
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(1));
// getRateLimit() uses headerRateLimit if /rate_limit returns a 404
// and headerRateLimit is available and not expired
assertThat(rateLimit, notNullValue());
assertThat(rateLimit.getLimit(), equalTo(5000));
// 11 requests since previous api call
// This verifies that header rate limit info is recorded even for /rate_limit endpoint and 404 response
assertThat(rateLimit.getRemaining(), equalTo(4967));
assertThat(rateLimit.getResetDate().compareTo(lastReset), equalTo(0));
// getRateLimit() uses headerRateLimit if /rate_limit returns a 404
// and headerRateLimit is available and not expired
assertThat(rateLimit, sameInstance(gitHub.lastRateLimit()));
headerRateLimit = rateLimit;
// ratelimit() should prefer headerRateLimit when getRateLimit fails and headerRateLimit is not expired
assertThat(gitHub.rateLimit(), equalTo(headerRateLimit));
assertThat(gitHub.rateLimit(), sameInstance(rateLimit));
assertThat(mockGitHub.getRequestCount(), equalTo(6));
// Wait for the header
Thread.sleep(1500);
// Verify the requesting a search url updates the search rate limit
// Core rate limit record should not change while search is updated.
assertThat(gitHub.lastRateLimit().getSearch(), instanceOf(GHRateLimit.UnknownLimitRecord.class));
assertThat(gitHub.lastRateLimit().getSearch().isExpired(), equalTo(true));
HashMap<String, Object> searchResult = (HashMap<String, Object>) gitHub.createRequest()
.rateLimit(RateLimitTarget.SEARCH)
.setRawUrlPath(mockGitHub.apiServer().baseUrl()
+ "/search/repositories?q=tetris+language%3Aassembly&sort=stars&order=desc")
.fetch(Object.class);
assertThat(searchResult.get("total_count"), equalTo(1918));
assertThat(mockGitHub.getRequestCount(), equalTo(7));
assertThat(gitHub.lastRateLimit(), not(sameInstance(headerRateLimit)));
assertThat(gitHub.lastRateLimit().getCore(), sameInstance(headerRateLimit.getCore()));
assertThat(gitHub.lastRateLimit().getSearch(), not(sameInstance(headerRateLimit.getSearch())));
assertThat(gitHub.lastRateLimit().getSearch().getRemaining(), equalTo(29));
PagedSearchIterable<GHRepository> searchResult2 = gitHub.searchRepositories()
.q("tetris")
.language("assembly")
.sort(GHRepositorySearchBuilder.Sort.STARS)
.order(GHDirection.DESC)
.list();
assertThat(searchResult2.getTotalCount(), equalTo(1918));
assertThat(mockGitHub.getRequestCount(), equalTo(8));
assertThat(gitHub.lastRateLimit(), not(sameInstance(headerRateLimit)));
assertThat(gitHub.lastRateLimit().getCore(), sameInstance(headerRateLimit.getCore()));
assertThat(gitHub.lastRateLimit().getSearch(), not(sameInstance(headerRateLimit.getSearch())));
assertThat(gitHub.lastRateLimit().getSearch().getRemaining(), equalTo(28));
}
@Test
@@ -321,9 +427,8 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
fail("Invalid rate limit missing some records should throw");
} catch (Exception e) {
assertThat(e, instanceOf(HttpException.class));
assertThat(e.getCause(), instanceOf(IOException.class));
assertThat(e.getCause().getCause(), instanceOf(ValueInstantiationException.class));
assertThat(e.getCause().getCause().getMessage(),
assertThat(e.getCause(), instanceOf(ValueInstantiationException.class));
assertThat(e.getCause().getMessage(),
containsString(
"Cannot construct instance of `org.kohsuke.github.GHRateLimit`, problem: `java.lang.NullPointerException`"));
}
@@ -333,9 +438,8 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
fail("Invalid rate limit record missing a value should throw");
} catch (Exception e) {
assertThat(e, instanceOf(HttpException.class));
assertThat(e.getCause(), instanceOf(IOException.class));
assertThat(e.getCause().getCause(), instanceOf(MismatchedInputException.class));
assertThat(e.getCause().getCause().getMessage(),
assertThat(e.getCause(), instanceOf(MismatchedInputException.class));
assertThat(e.getCause().getMessage(),
containsString("Missing required creator property 'reset' (index 2)"));
}
@@ -355,6 +459,7 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
private void executeExpirationTest() throws Exception {
// Customized response that templates the date to keep things working
snapshotNotAllowed();
GHRateLimit.UnknownLimitRecord.reset();
assertThat(mockGitHub.getRequestCount(), equalTo(0));
GHRateLimit rateLimit = null;
@@ -396,15 +501,15 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
assertThat("Header instance has expired", gitHub.lastRateLimit().isExpired(), is(true));
assertThat("rateLimit() will ask server when header instance expires and it has not called getRateLimit() yet",
assertThat("rateLimit() will ask server when cached instance has expired",
gitHub.rateLimit(),
not(sameInstance(rateLimit)));
assertThat("lastRateLimit() (header instance) is populated as part of internal call to getRateLimit()",
assertThat("lastRateLimit() is populated as part of internal call to getRateLimit()",
gitHub.lastRateLimit(),
not(sameInstance(rateLimit)));
assertThat("After request, rateLimit() selects header instance since it has been refreshed",
assertThat("After request, rateLimit() selects cached since it has been refreshed",
gitHub.rateLimit(),
sameInstance(gitHub.lastRateLimit()));
@@ -412,27 +517,27 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
assertThat(mockGitHub.getRequestCount(), equalTo(2));
// This time, rateLimit() should find an expired header record, but a valid returned record
// During the previous call we returned expired header info but valid returned record
// Merging means this has already been merged into a valid cached instance
Thread.sleep(4000);
rateLimit = gitHub.rateLimit();
// Using custom data to have a header instance that expires before the queried instance
assertThat(
"if header instance expires but queried instance is valid, ratelimit() uses it without asking server",
assertThat("if valid ratelimit() uses it without asking server",
gitHub.rateLimit(),
not(sameInstance(gitHub.lastRateLimit())));
sameInstance(gitHub.lastRateLimit()));
assertThat("ratelimit() should almost never return a return a GHRateLimit that is already expired",
gitHub.rateLimit().isExpired(),
is(false));
assertThat("Header instance hasn't been reloaded", gitHub.lastRateLimit(), sameInstance(headerRateLimit));
assertThat("Header instance has expired", gitHub.lastRateLimit().isExpired(), is(true));
assertThat("Cached instance hasn't been reloaded", gitHub.lastRateLimit(), sameInstance(headerRateLimit));
assertThat("Cached instance has not expired", gitHub.lastRateLimit().isExpired(), is(false));
assertThat(mockGitHub.getRequestCount(), equalTo(2));
// Finally they both expire and rateLimit() should find both expired and get a new record
// Finally the cached instance expires and rateLimit() should get a new record
Thread.sleep(2000);
headerRateLimit = gitHub.rateLimit();
@@ -452,7 +557,7 @@ public class GHRateLimitTest extends AbstractGitHubWireMockTest {
}
private static GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("github-api");
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
}
}

View File

@@ -1,5 +1,6 @@
package org.kohsuke.github;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
@@ -23,7 +24,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
}
private GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("github-api");
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
}
@Test
@@ -43,12 +44,12 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
assertThat(r.isAllowRebaseMerge(), is(true));
assertThat(r.isAllowSquashMerge(), is(true));
String httpTransport = "https://github.com/github-api-test-org/temp-testGetters.git";
String httpTransport = "https://github.com/hub4j-test-org/temp-testGetters.git";
assertThat(r.getHttpTransportUrl(), equalTo(httpTransport));
assertThat(r.gitHttpTransportUrl(), equalTo(httpTransport));
assertThat(r.getName(), equalTo("temp-testGetters"));
assertThat(r.getFullName(), equalTo("github-api-test-org/temp-testGetters"));
assertThat(r.getFullName(), equalTo("hub4j-test-org/temp-testGetters"));
}
@Test
@@ -91,7 +92,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
assertThat(e.getMessage(),
equalTo("Server returned HTTP response code: 200, message: '404 Not Found' for URL: "
+ mockGitHub.apiServer().baseUrl()
+ "/repos/github-api-test-org/github-api/branches/test/NonExistent"));
+ "/repos/hub4j-test-org/github-api/branches/test/NonExistent"));
}
}
@@ -127,7 +128,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
@Test
public void listContributors() throws IOException {
GHRepository r = gitHub.getOrganization("github-api").getRepository("github-api");
GHRepository r = gitHub.getOrganization("hub4j").getRepository("github-api");
int i = 0;
boolean kohsuke = false;
@@ -147,7 +148,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
@Test
public void getPermission() throws Exception {
kohsuke();
GHRepository r = gitHub.getRepository("github-api-test-org/test-permission");
GHRepository r = gitHub.getRepository("hub4j-test-org/test-permission");
assertEquals(GHPermissionType.ADMIN, r.getPermission("kohsuke"));
assertEquals(GHPermissionType.READ, r.getPermission("dude"));
r = gitHub.getOrganization("apache").getRepository("groovy");
@@ -190,6 +191,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
List<GHUser> users = new ArrayList<GHUser>();
users.add(user);
users.add(gitHub.getUser("jimmysombrero2"));
repo.addCollaborators(users, GHOrganization.Permission.PUSH);
GHPersonSet<GHUser> collabs = repo.getCollaborators();
@@ -244,7 +246,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
@Test
public void listLanguages() throws IOException {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
String mainLanguage = r.getLanguage();
assertTrue(r.listLanguages().containsKey(mainLanguage));
}
@@ -273,7 +275,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
@Test // issue #162
public void testIssue162() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
List<GHContent> contents = r.getDirectoryContent("", "gh-pages");
for (GHContent content : contents) {
if (content.isFile()) {
@@ -289,11 +291,11 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
public void markDown() throws Exception {
assertEquals("<p><strong>Test日本語</strong></p>", IOUtils.toString(gitHub.renderMarkdown("**Test日本語**")).trim());
String actual = IOUtils.toString(gitHub.getRepository("github-api/github-api")
.renderMarkdown("@kohsuke to fix issue #1", MarkdownMode.GFM));
String actual = IOUtils.toString(
gitHub.getRepository("hub4j/github-api").renderMarkdown("@kohsuke to fix issue #1", MarkdownMode.GFM));
// System.out.println(actual);
assertTrue(actual.contains("href=\"https://github.com/kohsuke\""));
assertTrue(actual.contains("href=\"https://github.com/github-api/github-api/pull/1\""));
assertTrue(actual.contains("href=\"https://github.com/hub4j/github-api/pull/1\""));
assertTrue(actual.contains("class=\"user-mention\""));
assertTrue(actual.contains("class=\"issue-link "));
assertTrue(actual.contains("to fix issue"));
@@ -301,7 +303,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
@Test
public void setMergeOptions() throws IOException {
// String repoName = "github-api-test-org/test-mergeoptions";
// String repoName = "hub4j-test-org/test-mergeoptions";
GHRepository r = getTempRepository();
// at least one merge option must be selected
@@ -428,25 +430,93 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
assertThat(e.getMessage(),
containsString(
"{\"message\":\"Not Found\",\"documentation_url\":\"https://developer.github.com/v3/git/refs/#get-a-reference\"}"));
assertThat(e.getCause(), instanceOf(FileNotFoundException.class));
}
}
@Test
public void listRefs() throws Exception {
GHRepository repo = getTempRepository();
List<GHRef> refs = repo.listRefs().toList();
assertThat(refs, notNullValue());
assertThat(refs.size(), equalTo(1));
assertThat(refs.get(0).getRef(), equalTo("refs/heads/master"));
GHRepository repo = getRepository();
List<GHRef> ghRefs;
// handle refs/*
ghRefs = repo.listRefs("heads").toList();
List<GHRef> ghRefsWithPrefix = repo.listRefs("refs/heads").toList();
assertThat(ghRefs, notNullValue());
assertThat(ghRefs.size(), greaterThan(3));
assertThat(ghRefs.get(0).getRef(), equalTo("refs/heads/changes"));
assertThat(ghRefsWithPrefix.size(), equalTo(ghRefs.size()));
assertThat(ghRefsWithPrefix.get(0).getRef(), equalTo(ghRefs.get(0).getRef()));
// git/refs/heads/gh-pages
// passing a specific ref to listRefs will fail to parse due to returning a single item not an array
try {
ghRefs = repo.listRefs("heads/gh-pages").toList();
fail();
} catch (Exception e) {
assertThat(e, instanceOf(HttpException.class));
assertThat(e.getCause(), instanceOf(JsonMappingException.class));
}
// git/refs/heads/gh
ghRefs = repo.listRefs("heads/gh").toList();
assertThat(ghRefs, notNullValue());
assertThat(ghRefs.size(), equalTo(1));
assertThat(ghRefs.get(0).getRef(), equalTo("refs/heads/gh-pages"));
// git/refs/headz
try {
ghRefs = repo.listRefs("headz").toList();
fail();
} catch (Exception e) {
assertThat(e, instanceOf(GHFileNotFoundException.class));
assertThat(e.getMessage(),
containsString(
"{\"message\":\"Not Found\",\"documentation_url\":\"https://developer.github.com/v3/git/refs/#get-a-reference\"}"));
}
}
@Test
public void getRefWithPrefix() throws Exception {
GHRepository repo = getTempRepository();
GHRef refWithoutPrefix = repo.getRef("heads/master");
GHRef refWithPrefix = repo.getRef("refs/heads/master");
assertThat(refWithoutPrefix.getRef(), equalTo(refWithPrefix.getRef()));
public void getRef() throws Exception {
GHRepository repo = getRepository();
GHRef ghRef;
// handle refs/*
ghRef = repo.getRef("heads/gh-pages");
GHRef ghRefWithPrefix = repo.getRef("refs/heads/gh-pages");
assertThat(ghRef, notNullValue());
assertThat(ghRef.getRef(), equalTo("refs/heads/gh-pages"));
assertThat(ghRefWithPrefix.getRef(), equalTo(ghRef.getRef()));
// git/refs/heads/gh-pages
ghRef = repo.getRef("heads/gh-pages");
assertThat(ghRef, notNullValue());
assertThat(ghRef.getRef(), equalTo("refs/heads/gh-pages"));
// git/refs/heads/gh
try {
ghRef = repo.getRef("heads/gh");
fail();
} catch (Exception e) {
assertThat(e, instanceOf(GHFileNotFoundException.class));
assertThat(e.getMessage(),
containsString(
"{\"message\":\"Not Found\",\"documentation_url\":\"https://developer.github.com/v3/git/refs/#get-a-reference\"}"));
}
// git/refs/headz
try {
ghRef = repo.getRef("headz");
fail();
} catch (Exception e) {
assertThat(e, instanceOf(GHFileNotFoundException.class));
assertThat(e.getMessage(),
containsString(
"{\"message\":\"Not Found\",\"documentation_url\":\"https://developer.github.com/v3/git/refs/#get-a-reference\"}"));
}
}
@Test
@@ -466,8 +536,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
fail();
} catch (Exception e) {
assertThat(e, instanceOf(GHFileNotFoundException.class));
assertThat(e.getMessage(),
containsString("/repos/github-api-test-org/temp-listRefsEmptyTags/git/refs/tags"));
assertThat(e.getMessage(), containsString("/repos/hub4j-test-org/temp-listRefsEmptyTags/git/refs/tags"));
}
try {
@@ -477,8 +546,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
} catch (Exception e) {
assertThat(e, instanceOf(GHException.class));
assertThat(e.getMessage(), containsString("Failed to retrieve "));
assertThat(e.getMessage(),
containsString("/repos/github-api-test-org/temp-listRefsEmptyTags/git/refs/tags"));
assertThat(e.getMessage(), containsString("/repos/hub4j-test-org/temp-listRefsEmptyTags/git/refs/tags"));
assertThat(e.getCause(), instanceOf(GHFileNotFoundException.class));
}
}
@@ -526,7 +594,7 @@ public class GHRepositoryTest extends AbstractGitHubWireMockTest {
public void getCheckRuns() throws Exception {
final int expectedCount = 8;
// Use github-api repository as it has checks set up
PagedIterable<GHCheckRun> checkRuns = gitHub.getOrganization("github-api")
PagedIterable<GHCheckRun> checkRuns = gitHub.getOrganization("hub4j")
.getRepository("github-api")
.getCheckRuns("78b9ff49d47daaa158eb373c4e2e040f739df8b9");
// Check if the paging works correctly

View File

@@ -60,6 +60,6 @@ public class GHTagTest extends AbstractGitHubWireMockTest {
}
private GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("github-api");
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
}
}

View File

@@ -0,0 +1,32 @@
package org.kohsuke.github;
import org.junit.Test;
import java.io.IOException;
public class GHTeamBuilderTest extends AbstractGitHubWireMockTest {
@Test
public void testCreateChildTeam() throws IOException {
String parentTeamSlug = "dummy-team";
String childTeamSlug = "dummy-team-child";
String description = "description";
// Get the parent team
GHTeam parentTeam = gitHub.getOrganization(GITHUB_API_TEST_ORG).getTeamBySlug(parentTeamSlug);
// Create a child team, using the parent team identifier
GHTeam childTeam = gitHub.getOrganization(GITHUB_API_TEST_ORG)
.createTeam(childTeamSlug)
.description(description)
.privacy(GHTeam.Privacy.CLOSED)
.parentTeamId(parentTeam.getId())
.create();
assertEquals(description, childTeam.getDescription());
assertEquals(childTeamSlug, childTeam.getName());
assertEquals(childTeamSlug, childTeam.getSlug());
assertEquals(GHTeam.Privacy.CLOSED, childTeam.getPrivacy());
}
}

View File

@@ -4,6 +4,7 @@ import org.junit.Test;
import org.kohsuke.github.GHTeam.Privacy;
import java.io.IOException;
import java.util.Set;
import static org.junit.Assert.assertEquals;
@@ -56,4 +57,27 @@ public class GHTeamTest extends AbstractGitHubWireMockTest {
assertEquals(privacy, team.getPrivacy());
}
@Test
public void testFetchChildTeams() throws IOException {
String teamSlug = "dummy-team";
GHOrganization org = gitHub.getOrganization(GITHUB_API_TEST_ORG);
GHTeam team = org.getTeamBySlug(teamSlug);
Set<GHTeam> result = team.listChildTeams().toSet();
assertEquals(1, result.size());
assertEquals("child-team-for-dummy", result.toArray(new GHTeam[]{})[0].getName());
}
@Test
public void testFetchEmptyChildTeams() throws IOException {
String teamSlug = "simple-team";
GHOrganization org = gitHub.getOrganization(GITHUB_API_TEST_ORG);
GHTeam team = org.getTeamBySlug(teamSlug);
Set<GHTeam> result = team.listChildTeams().toSet();
assertEquals(0, result.size());
}
}

View File

@@ -11,7 +11,7 @@ import java.util.Arrays;
import static org.junit.Assert.assertEquals;
public class GHTreeBuilderTest extends AbstractGitHubWireMockTest {
private static String REPO_NAME = "github-api-test-org/GHTreeBuilderTest";
private static String REPO_NAME = "hub4j-test-org/GHTreeBuilderTest";
private static String PATH_SCRIPT = "app/run.sh";
private static String CONTENT_SCRIPT = "#!/bin/bash\necho Hello\n";

View File

@@ -102,4 +102,12 @@ public class GHUserTest extends AbstractGitHubWireMockTest {
repository.delete();
}
}
@Test
public void verifyBioAndHireable() throws IOException {
GHUser u = gitHub.getUser("Chew");
assertThat(u.getBio(), equalTo("I like to program things and I hope to program something cool one day :D"));
assertTrue(u.isHireable());
assertNotNull(u.getTwitterUsername());
}
}

View File

@@ -9,7 +9,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
// Issue 737
@Test
public void testExpiredKey() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f01");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -18,7 +18,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testNotSigningKey() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f02");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -27,7 +27,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testGpgverifyError() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f03");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -36,7 +36,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testGpgverifyUnavailable() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f04");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -46,7 +46,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testUnsigned() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f05");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -55,7 +55,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testUnknownSignatureType() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f06");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -65,7 +65,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testNoUser() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f07");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -74,7 +74,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testUnverifiedEmail() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f08");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -83,7 +83,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testBadEmail() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f09");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -92,7 +92,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testUnknownKey() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f10");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -101,7 +101,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testMalformedSignature() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f11");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -111,7 +111,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testInvalid() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f12");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertFalse(commit.getCommitShortInfo().getVerification().isVerified());
@@ -120,7 +120,7 @@ public class GHVerificationReasonTest extends AbstractGitHubWireMockTest {
@Test
public void testValid() throws Exception {
GHRepository r = gitHub.getRepository("github-api/github-api");
GHRepository r = gitHub.getRepository("hub4j/github-api");
GHCommit commit = r.getCommit("86a2e245aa6d71d54923655066049d9e21a15f13");
assertEquals(commit.getCommitShortInfo().getAuthor().getName(), "Sourabh Parkala");
assertTrue(commit.getCommitShortInfo().getVerification().isVerified());

View File

@@ -4,12 +4,13 @@ import org.junit.Test;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.TimeZone;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.fail;
/**
* Unit test for {@link GitHub} static helpers.
@@ -20,25 +21,38 @@ public class GitHubStaticTest extends AbstractGitHubWireMockTest {
@Test
public void timeRoundTrip() throws Exception {
Instant instantNow = Instant.now();
final long stableInstantEpochMilli = 1533721222255L;
Instant instantNow = Instant.ofEpochMilli(stableInstantEpochMilli);
Date instantSeconds = Date.from(instantNow.truncatedTo(ChronoUnit.SECONDS));
Date instantMillis = Date.from(instantNow.truncatedTo(ChronoUnit.MILLIS));
// if we happen to land exactly on zero milliseconds, add 1 milli
if (instantSeconds.equals(instantMillis)) {
instantMillis = Date.from(instantNow.plusMillis(1).truncatedTo(ChronoUnit.MILLIS));
}
String instantFormatSlash = formatZonedDate(instantMillis, "yyyy/MM/dd HH:mm:ss ZZZZ", "PST");
assertThat(instantFormatSlash, equalTo("2018/08/08 02:40:22 -0700"));
// TODO: other formats
String instantFormatSlash = formatDate(instantMillis, "yyyy/MM/dd HH:mm:ss ZZZZ");
String instantFormatDash = formatDate(instantMillis, "yyyy-MM-dd'T'HH:mm:ss'Z'");
assertThat(instantFormatDash, equalTo("2018-08-08T09:40:22Z"));
String instantFormatMillis = formatDate(instantMillis, "yyyy-MM-dd'T'HH:mm:ss.S'Z'");
assertThat(instantFormatMillis, equalTo("2018-08-08T09:40:22.255Z"));
String instantFormatMillisZoned = formatZonedDate(instantMillis, "yyyy-MM-dd'T'HH:mm:ss.SXXX", "PST");
assertThat(instantFormatMillisZoned, equalTo("2018-08-08T02:40:22.255-07:00"));
String instantSecondsFormatMillis = formatDate(instantSeconds, "yyyy-MM-dd'T'HH:mm:ss.S'Z'");
assertThat(instantSecondsFormatMillis, equalTo("2018-08-08T09:40:22.0Z"));
String instantSecondsFormatMillisZoned = formatZonedDate(instantSeconds, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "PST");
assertThat(instantSecondsFormatMillisZoned, equalTo("2018-08-08T02:40:22.000-07:00"));
String instantBadFormat = formatDate(instantMillis, "yy-MM-dd'T'HH:mm'Z'");
assertThat(instantBadFormat, equalTo("18-08-08T09:40Z"));
assertThat(GitHubClient.parseDate(GitHubClient.printDate(instantSeconds)),
equalTo(GitHubClient.parseDate(GitHubClient.printDate(instantMillis))));
assertThat(GitHubClient.printDate(instantSeconds), equalTo("2018-08-08T09:40:22Z"));
assertThat(GitHubClient.printDate(GitHubClient.parseDate(instantFormatMillisZoned)),
equalTo("2018-08-08T09:40:22Z"));
assertThat(instantSeconds, equalTo(GitHubClient.parseDate(GitHubClient.printDate(instantSeconds))));
@@ -51,82 +65,153 @@ public class GitHubStaticTest extends AbstractGitHubWireMockTest {
// This parser does not truncate to the nearest second, so it will be equal
assertThat(instantMillis, equalTo(GitHubClient.parseDate(instantFormatMillis)));
assertThat(instantMillis, equalTo(GitHubClient.parseDate(instantFormatMillisZoned)));
assertThat(instantSeconds, equalTo(GitHubClient.parseDate(instantSecondsFormatMillis)));
assertThat(instantSeconds, equalTo(GitHubClient.parseDate(instantSecondsFormatMillisZoned)));
try {
GitHubClient.parseDate(instantBadFormat);
fail("Bad time format should throw.");
} catch (IllegalStateException e) {
assertThat(e.getMessage(), equalTo("Unable to parse the timestamp: " + instantBadFormat));
} catch (DateTimeParseException e) {
assertThat(e.getMessage(), equalTo("Text '" + instantBadFormat + "' could not be parsed at index 0"));
}
}
@Test
public void testGitHubRateLimitShouldReplaceRateLimit() throws Exception {
GHRateLimit.Record unknown0 = GHRateLimit.Unknown().getCore();
GHRateLimit.Record unknown1 = GHRateLimit.Unknown().getCore();
GHRateLimit.UnknownLimitRecord.reset();
GHRateLimit.UnknownLimitRecord.unknownLimitResetSeconds = 5;
GHRateLimit.Record record0 = new GHRateLimit.Record(10, 10, 10L);
GHRateLimit.Record record1 = new GHRateLimit.Record(10, 9, 10L);
GHRateLimit.Record record2 = new GHRateLimit.Record(10, 2, 10L);
GHRateLimit.Record record3 = new GHRateLimit.Record(10, 10, 20L);
GHRateLimit.Record record4 = new GHRateLimit.Record(10, 5, 20L);
GHRateLimit.Record unknown0 = GHRateLimit.UnknownLimitRecord.current();
Thread.sleep(2000);
Thread.sleep(1500);
GHRateLimit.UnknownLimitRecord.reset();
GHRateLimit.UnknownLimitRecord.unknownLimitResetSeconds = 5;
// For testing, we create an new unknown.
GHRateLimit.Record unknown1 = GHRateLimit.UnknownLimitRecord.current();
assertThat("Valid unknown should not replace an existing one, regardless of created or reset time",
unknown1.currentOrUpdated(unknown0),
sameInstance(unknown1));
assertThat("Valid unknown should not replace an existing one, regardless of created or reset time",
unknown0.currentOrUpdated(unknown1),
sameInstance(unknown0));
// Sleep to make different created time
Thread.sleep(1500);
// To reduce object creation: There is only one valid Unknown record at a time.
assertThat("Unknown current should should limit the creation of new unknown records",
unknown1,
sameInstance(GHRateLimit.UnknownLimitRecord.current()));
long epochSeconds = Instant.now().getEpochSecond();
GHRateLimit.Record record0 = new GHRateLimit.Record(10, 10, epochSeconds + 10L);
GHRateLimit.Record record1 = new GHRateLimit.Record(10, 9, epochSeconds + 10L);
GHRateLimit.Record record2 = new GHRateLimit.Record(10, 2, epochSeconds + 10L);
GHRateLimit.Record record3 = new GHRateLimit.Record(10, 10, epochSeconds + 20L);
GHRateLimit.Record record4 = new GHRateLimit.Record(10, 5, epochSeconds + 20L);
GHRateLimit.Record recordExpired0 = new GHRateLimit.Record(10, 10, epochSeconds - 1L);
GHRateLimit.Record recordExpired1 = new GHRateLimit.Record(10, 10, epochSeconds + 2L);
// Sleep to make expired and different created time
Thread.sleep(4000);
GHRateLimit.Record recordWorst = new GHRateLimit.Record(Integer.MAX_VALUE, Integer.MAX_VALUE, Long.MIN_VALUE);
GHRateLimit.Record record00 = new GHRateLimit.Record(10, 10, 10L);
GHRateLimit.Record unknown2 = GHRateLimit.Unknown().getCore();
GHRateLimit.Record record00 = new GHRateLimit.Record(10, 10, epochSeconds + 10L);
GHRateLimit.Record unknownExpired0 = unknown0;
GHRateLimit.Record unknownExpired1 = unknown1;
unknown0 = GHRateLimit.UnknownLimitRecord.current();
// Rate-limit records maybe created and returned in different orders.
// We should update to the regular records over unknowns.
// We should update to the unexpired regular records over unknowns.
// After that, we should update to the candidate if its limit is lower or its reset is later.
assertThat("Equivalent unknown should not replace", GitHubClient.shouldReplace(unknown0, unknown1), is(false));
assertThat("Equivalent unknown should not replace", GitHubClient.shouldReplace(unknown1, unknown0), is(false));
assertThat("Expired unknowns should not replace another expired one, regardless of created or reset time",
unknownExpired0.currentOrUpdated(unknownExpired1),
sameInstance(unknownExpired0));
assertThat("Expired unknowns should not replace another expired one, regardless of created or reset time",
unknownExpired1.currentOrUpdated(unknownExpired0),
sameInstance(unknownExpired1));
assertThat("Later unknown should replace earlier", GitHubClient.shouldReplace(unknown2, unknown0), is(true));
assertThat("Earlier unknown should not replace later",
GitHubClient.shouldReplace(unknown0, unknown2),
is(false));
assertThat("Expired unknown should not be replaced by expired earlier normal record",
unknownExpired0.currentOrUpdated(recordExpired0),
sameInstance(unknownExpired0));
assertThat("Expired normal record should not be replaced an expired earlier unknown record",
recordExpired0.currentOrUpdated(unknownExpired0),
sameInstance(recordExpired0));
assertThat("Worst record should replace later unknown",
GitHubClient.shouldReplace(recordWorst, unknown1),
is(true));
assertThat("Unknown should not replace worst record",
GitHubClient.shouldReplace(unknown1, recordWorst),
is(false));
assertThat("Expired unknown should be replaced by expired later normal record",
unknownExpired0.currentOrUpdated(recordExpired1),
sameInstance(recordExpired1));
assertThat(
"Expired later normal record should not be replaced an expired unknown record, regardless of created or reset time",
recordExpired1.currentOrUpdated(unknownExpired0),
sameInstance(recordExpired1));
assertThat("Earlier record should replace later worst",
GitHubClient.shouldReplace(record0, recordWorst),
is(true));
assertThat("Valid unknown should not be replaced by an expired unknown",
unknown0.currentOrUpdated(unknownExpired0),
sameInstance(unknown0));
assertThat("Expired unknown should be replaced by valid unknown",
unknownExpired0.currentOrUpdated(unknown0),
sameInstance(unknown0));
assertThat("Valid unknown should replace an expired normal record",
recordExpired1.currentOrUpdated(unknown0),
sameInstance(unknown0));
assertThat("Valid unknown record should not be replaced by expired normal record",
unknown0.currentOrUpdated(recordExpired1),
sameInstance(unknown0));
// In normal comparision, expiration doesn't matter
assertThat("Expired normal should not be replaced by an earlier expired one",
recordExpired1.currentOrUpdated(recordExpired0),
sameInstance(recordExpired1));
assertThat("Expired normal should be replaced by a later expired one",
recordExpired0.currentOrUpdated(recordExpired1),
sameInstance(recordExpired1));
assertThat("Later worst record should be replaced by earlier record",
recordWorst.currentOrUpdated(record0),
sameInstance(record0));
assertThat("Later worst record should not replace earlier",
GitHubClient.shouldReplace(recordWorst, record0),
is(false));
record0.currentOrUpdated(recordWorst),
sameInstance(record0));
assertThat("Equivalent record should not replace", GitHubClient.shouldReplace(record0, record00), is(false));
assertThat("Equivalent record should not replace", GitHubClient.shouldReplace(record00, record0), is(false));
assertThat("Equivalent record should not replace other",
record00.currentOrUpdated(record0),
sameInstance(record00));
assertThat("Equivalent record should not replace other",
record0.currentOrUpdated(record00),
sameInstance(record0));
assertThat("Lower limit record should replace higher", GitHubClient.shouldReplace(record1, record0), is(true));
assertThat("Lower limit record should replace higher", GitHubClient.shouldReplace(record2, record1), is(true));
assertThat("Higher limit record should be replaced by lower",
record0.currentOrUpdated(record1),
sameInstance(record1));
assertThat("Higher limit record should be replaced by lower",
record1.currentOrUpdated(record2),
sameInstance(record2));
assertThat("Higher limit record should not replace lower",
GitHubClient.shouldReplace(record1, record2),
is(false));
assertThat("Lower limit record should not be replaced higher",
record2.currentOrUpdated(record1),
sameInstance(record2));
assertThat("Higher limit record with later reset should replace lower",
GitHubClient.shouldReplace(record3, record2),
is(true));
assertThat("Lower limit record should be replaced by higher limit record with later reset",
record2.currentOrUpdated(record3),
sameInstance(record3));
assertThat("Lower limit record with later reset should replace higher",
GitHubClient.shouldReplace(record4, record1),
is(true));
assertThat("Higher limit record should be replaced by lower limit record with later reset",
record1.currentOrUpdated(record4),
sameInstance(record4));
assertThat("Lower limit record with earlier reset should not replace higher",
GitHubClient.shouldReplace(record2, record4),
is(false));
assertThat("Higher limit record should not be replaced by lower limit record with earlier reset",
record4.currentOrUpdated(record2),
sameInstance(record4));
}
@@ -157,8 +242,12 @@ public class GitHubStaticTest extends AbstractGitHubWireMockTest {
}
static String formatDate(Date dt, String format) {
return formatZonedDate(dt, format, "GMT");
}
static String formatZonedDate(Date dt, String format, String timeZone) {
SimpleDateFormat df = new SimpleDateFormat(format);
df.setTimeZone(TimeZone.getTimeZone("GMT"));
df.setTimeZone(TimeZone.getTimeZone(timeZone));
return df.format(dt);
}

View File

@@ -10,7 +10,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.function.Function;
import javax.annotation.Nonnull;
/**
* @author Stephen Connolly
@@ -79,6 +83,11 @@ public class PayloadRule implements TestRule {
return new InputStreamReader(asInputStream(), Charset.defaultCharset());
}
public Reader asReader(@Nonnull Function<String, String> transformer) throws IOException {
String payloadString = asString();
return new StringReader(transformer.apply(payloadString));
}
public Reader asReader(String encoding) throws IOException {
return new InputStreamReader(asInputStream(), encoding);
}

Some files were not shown because too many files have changed in this diff Show More