From a3b000641113f235196d9d3cfb8fed5ccca18117 Mon Sep 17 00:00:00 2001 From: Drew Roen <102626803+drewroengoogle@users.noreply.github.com> Date: Sun, 10 Sep 2023 18:10:57 -0500 Subject: [PATCH] Add the 'PushEvent' webhook and associated PushCommit object (#386) * Add the 'PushEvent' webhook and associated PushCommit object * Add a test, handle integer times that come in from the Repository object in PushEvents only * Remove unnecessary decorator + update changelog * Add a comment about the time weirdness --- CHANGELOG.md | 23 +++-- lib/src/common/model/git.dart | 22 +++++ lib/src/common/model/git.g.dart | 18 ++++ lib/src/common/model/repos.dart | 14 +++ lib/src/common/model/repos.g.dart | 8 +- lib/src/server/hooks.dart | 37 ++++++++ lib/src/server/hooks.g.dart | 28 ++++++ test/data_object_test.dart | 139 ++++++++++++++++++++++++++++++ 8 files changed, 274 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8027a432..6193c995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 9.18.0 + +* Adds the initial `PushEvent` object in https://github.com/SpinlockLabs/github.dart/pull/386 +* Update the `Repository` values `created_at` and `pushed_at` to handle integer times + ## 9.17.0 * Add bearerToken constructor to Authentication class by @kevmoo in https://github.com/SpinlockLabs/github.dart/pull/381 @@ -307,7 +312,7 @@ Map? files; - Clean up lints https://github.com/SpinlockLabs/github.dart/pull/202 ## 6.0.5 - - Fix null errors issue https://github.com/SpinlockLabs/github.dart/issues/199 + - Fix null errors issue https://github.com/SpinlockLabs/github.dart/issues/199 ## 6.0.4 - This fixes #196 (https://github.com/SpinlockLabs/github.dart/issues/196) @@ -357,10 +362,10 @@ Map? files; Deprecations: -- The `draft` and `prerelease` properties in the CreateRelease and Release +- The `draft` and `prerelease` properties in the CreateRelease and Release - classes have been renamed to `isDraft` and `isPrerelease` for clarity. -- Release.targetCommitsh has been renamed to Release.targetCommitish. -- The `release` parameter in RepositoriesService.createRelease +- Release.targetCommitsh has been renamed to Release.targetCommitish. +- The `release` parameter in RepositoriesService.createRelease has been renamed to `createRelease`. - `RepositoriesService.getRelease` has been renamed to `RepositoriesService.getReleaseById` @@ -369,7 +374,7 @@ has been renamed to `createRelease`. - Add access to labels on Pull Requests https://github.com/SpinlockLabs/github.dart/pull/163 - Adding draft property to PR model https://github.com/SpinlockLabs/github.dart/pull/162 - updateFile request must be a PUT https://github.com/SpinlockLabs/github.dart/pull/160 - + ## v5.1.0 - `Repository`: added `updatedAt` and `license` fields. @@ -386,7 +391,7 @@ has been renamed to `createRelease`. ## v5.0.0 -- **BREAKING** `RepositoriesService.listCollaborators` now returns +- **BREAKING** `RepositoriesService.listCollaborators` now returns `Stream` instead of `Stream`. - `Collaborator` is a new type that includes collaborator-specific information. @@ -403,7 +408,7 @@ has been renamed to `createRelease`. - Removed unsupported `limit` parameter. - Removed flaky retry logic. Instead, `NotReady` is thrown, which can be used to decide to retry at the call site. - - Made associated classes `ContributorStatistics` and + - Made associated classes `ContributorStatistics` and `ContributorWeekStatistics` immutable. Since these classes are only meant as return values, we're not treating this as a breaking change. - Added `Stream github.search.code(...)` search API @@ -433,7 +438,7 @@ has been renamed to `createRelease`. ## v2.3.2 -- Automatically attempt to find GitHub user information in the process environment when running on the standalone VM. +- Automatically attempt to find GitHub user information in the process environment when running on the standalone VM. - Add `ref` parameter to `getReadme` method for the repository service. ## v2.3.1 @@ -447,7 +452,7 @@ has been renamed to `createRelease`. - Moved `CHANGELOG` content back to repo. - Added `rateLimitLimit`, `rateLimitRemaining` and `rateLimitReset` to `GitHub`. - Added `id` to `Issue` -- Added `direction`, `sort` and `since` optional arguments to +- Added `direction`, `sort` and `since` optional arguments to `IssueService.listByRepo`. ## v2.1.0 diff --git a/lib/src/common/model/git.dart b/lib/src/common/model/git.dart index 136c01c4..0e655572 100644 --- a/lib/src/common/model/git.dart +++ b/lib/src/common/model/git.dart @@ -98,6 +98,28 @@ class CreateGitCommit { Map toJson() => _$CreateGitCommitToJson(this); } +/// Model class for a pushed commit. +@JsonSerializable() +class PushGitCommit { + PushGitCommit(this.id, this.message, this.timestamp, this.url); + + /// The commit hash. + String? id; + + /// The commit message. + String? message; + + /// The timestamp of the commit. + DateTime? timestamp; + + /// The direct url to the commit. + String? url; + + factory PushGitCommit.fromJson(Map input) => + _$PushGitCommitFromJson(input); + Map toJson() => _$PushGitCommitToJson(this); +} + /// Model class for an author or committer of a commit. The [GitCommitUser] may /// not correspond to a GitHub [User]. @JsonSerializable(includeIfNull: false) diff --git a/lib/src/common/model/git.g.dart b/lib/src/common/model/git.g.dart index 61d5fc44..7b2ba940 100644 --- a/lib/src/common/model/git.g.dart +++ b/lib/src/common/model/git.g.dart @@ -88,6 +88,24 @@ Map _$CreateGitCommitToJson(CreateGitCommit instance) => 'author': instance.author, }; +PushGitCommit _$PushGitCommitFromJson(Map json) => + PushGitCommit( + json['id'] as String?, + json['message'] as String?, + json['timestamp'] == null + ? null + : DateTime.parse(json['timestamp'] as String), + json['url'] as String?, + ); + +Map _$PushGitCommitToJson(PushGitCommit instance) => + { + 'id': instance.id, + 'message': instance.message, + 'timestamp': instance.timestamp?.toIso8601String(), + 'url': instance.url, + }; + GitCommitUser _$GitCommitUserFromJson(Map json) => GitCommitUser( json['name'] as String?, diff --git a/lib/src/common/model/repos.dart b/lib/src/common/model/repos.dart index d09c1a7e..4e46566d 100644 --- a/lib/src/common/model/repos.dart +++ b/lib/src/common/model/repos.dart @@ -225,9 +225,11 @@ class Repository { int networkCount; /// The time the repository was created at + @JsonKey(fromJson: Repository.dynamicToDateTime) DateTime? createdAt; /// The last time the repository was pushed at + @JsonKey(fromJson: Repository.dynamicToDateTime) DateTime? pushedAt; DateTime? updatedAt; @@ -459,6 +461,18 @@ class Repository { @override String toString() => 'Repository: $owner/$name'; + + /// In some cases, github webhooks send time values as an integer. This method + /// is added to handle those cases, but otherwise parse like normal. + static DateTime? dynamicToDateTime(dynamic time) { + if (time == null) { + return null; + } + if (time.runtimeType == int) { + return DateTime.fromMillisecondsSinceEpoch(time * 1000); + } + return DateTime.parse(time as String); + } } /// Model class for repository permissions. diff --git a/lib/src/common/model/repos.g.dart b/lib/src/common/model/repos.g.dart index 490b18b1..871a3284 100644 --- a/lib/src/common/model/repos.g.dart +++ b/lib/src/common/model/repos.g.dart @@ -46,9 +46,7 @@ Repository _$RepositoryFromJson(Map json) => Repository( sshUrl: json['ssh_url'] as String? ?? '', svnUrl: json['svn_url'] as String? ?? '', defaultBranch: json['default_branch'] as String? ?? '', - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), + createdAt: Repository.dynamicToDateTime(json['created_at']), isPrivate: json['private'] as bool? ?? false, isFork: json['fork'] as bool? ?? false, stargazersCount: json['stargazers_count'] as int? ?? 0, @@ -68,9 +66,7 @@ Repository _$RepositoryFromJson(Map json) => Repository( updatedAt: json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), - pushedAt: json['pushed_at'] == null - ? null - : DateTime.parse(json['pushed_at'] as String), + pushedAt: Repository.dynamicToDateTime(json['pushed_at']), license: json['license'] == null ? null : LicenseKind.fromJson(json['license'] as Map), diff --git a/lib/src/server/hooks.dart b/lib/src/server/hooks.dart index ea250c48..3f8987d2 100644 --- a/lib/src/server/hooks.dart +++ b/lib/src/server/hooks.dart @@ -235,3 +235,40 @@ class CreateEvent extends HookEvent { Map toJson() => _$CreateEventToJson(this); } + +@JsonSerializable(fieldRename: FieldRename.snake) +class PushEvent extends HookEvent { + PushEvent({ + this.ref, + this.before, + this.after, + this.repository, + this.headCommit, + this.commits, + this.sender, + }); + + factory PushEvent.fromJson(Map input) => + _$PushEventFromJson(input); + String? ref; + String? before; + String? after; + @JsonKey(toJson: handleIntegerTimes) + Repository? repository; + PushGitCommit? headCommit; + List? commits; + User? sender; + + Map toJson() => _$PushEventToJson(this); + + static Map? handleIntegerTimes(Repository? repository) { + var repositoryMap = repository?.toJson(); + for (final parameter in ['created_at', 'pushed_at']) { + if (repositoryMap?[parameter] != null) { + final createdAt = DateTime.parse(repositoryMap?[parameter]); + repositoryMap?[parameter] = createdAt.millisecondsSinceEpoch ~/ 1000; + } + } + return repositoryMap; + } +} diff --git a/lib/src/server/hooks.g.dart b/lib/src/server/hooks.g.dart index 3babd5cf..0e4ca02e 100644 --- a/lib/src/server/hooks.g.dart +++ b/lib/src/server/hooks.g.dart @@ -173,3 +173,31 @@ Map _$CreateEventToJson(CreateEvent instance) => 'repository': instance.repository, 'sender': instance.sender, }; + +PushEvent _$PushEventFromJson(Map json) => PushEvent( + ref: json['ref'] as String?, + before: json['before'] as String?, + after: json['after'] as String?, + repository: json['repository'] == null + ? null + : Repository.fromJson(json['repository'] as Map), + headCommit: json['head_commit'] == null + ? null + : PushGitCommit.fromJson(json['head_commit'] as Map), + commits: (json['commits'] as List?) + ?.map((e) => PushGitCommit.fromJson(e as Map)) + .toList(), + sender: json['sender'] == null + ? null + : User.fromJson(json['sender'] as Map), + ); + +Map _$PushEventToJson(PushEvent instance) => { + 'ref': instance.ref, + 'before': instance.before, + 'after': instance.after, + 'repository': PushEvent.handleIntegerTimes(instance.repository), + 'head_commit': instance.headCommit, + 'commits': instance.commits, + 'sender': instance.sender, + }; diff --git a/test/data_object_test.dart b/test/data_object_test.dart index f1d1d5db..1654b0a6 100644 --- a/test/data_object_test.dart +++ b/test/data_object_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:github/github.dart'; +import 'package:github/hooks.dart'; import 'package:test/test.dart'; const _licenseJson = r''' { @@ -29,6 +30,134 @@ const _licenseJson = r''' { } }'''; +const _pushEventJson = r'''{ + "ref": "refs/heads/main", + "before": "def456def456", + "after": "abc123abc123", + "base_ref": null, + "repository": { + "name": "fake-repository", + "id": 680238321, + "full_name": "fakeuser/fake-repository", + "owner": { + "login": "fakeuser", + "id": 102626803, + "avatar_url": "https://avatars.githubusercontent.com/u/102626803?v=4", + "html_url": "https://github.com/fakeuser" + }, + "private": false, + "fork": false, + "html_url": "https://github.com/fakeuser/fake-repository", + "description": "", + "clone_url": "https://github.com/fakeuser/fake-repository.git", + "ssh_url": "git@github.com:fakeuser/fake-repository.git", + "svn_url": "https://github.com/fakeuser/fake-repository", + "git_url": "git://github.com/fakeuser/fake-repository.git", + "homepage": "", + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": "", + "has_issues": true, + "has_wiki": true, + "has_downloads": true, + "has_pages": false, + "forks_count": 0, + "open_issues_count": 0, + "default_branch": "main", + "subscribers_count": 0, + "network_count": 0, + "created_at": 1692379465, + "pushed_at": 1694031233, + "updated_at": "2023-08-18T17:24:25.000Z", + "archived": false, + "disabled": false, + "allow_forking": true, + "assignees_url": "https://api.github.com/repos/fakeuser/fake-repository/assignees{/user}", + "blobs_url": "https://api.github.com/repos/fakeuser/fake-repository/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/fakeuser/fake-repository/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/fakeuser/fake-repository/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/fakeuser/fake-repository/comments{/number}", + "commits_url": "https://api.github.com/repos/fakeuser/fake-repository/commits{/sha}", + "compare_url": "https://api.github.com/repos/fakeuser/fake-repository/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/fakeuser/fake-repository/contents/{+path}", + "contributors_url": "https://api.github.com/repos/fakeuser/fake-repository/contributors", + "deployments_url": "https://api.github.com/repos/fakeuser/fake-repository/deployments", + "downloads_url": "https://api.github.com/repos/fakeuser/fake-repository/downloads", + "events_url": "https://api.github.com/repos/fakeuser/fake-repository/events", + "forks": 0, + "forks_url": "https://api.github.com/repos/fakeuser/fake-repository/forks", + "git_commits_url": "https://api.github.com/repos/fakeuser/fake-repository/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/fakeuser/fake-repository/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/fakeuser/fake-repository/git/tags{/sha}", + "has_discussions": false, + "has_projects": true, + "hooks_url": "https://api.github.com/repos/fakeuser/fake-repository/hooks", + "is_template": false, + "issue_comment_url": "https://api.github.com/repos/fakeuser/fake-repository/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/fakeuser/fake-repository/issues/events{/number}", + "issues_url": "https://api.github.com/repos/fakeuser/fake-repository/issues{/number}", + "keys_url": "https://api.github.com/repos/fakeuser/fake-repository/keys{/key_id}", + "labels_url": "https://api.github.com/repos/fakeuser/fake-repository/labels{/name}", + "languages_url": "https://api.github.com/repos/fakeuser/fake-repository/languages", + "master_branch": "main", + "merges_url": "https://api.github.com/repos/fakeuser/fake-repository/merges", + "milestones_url": "https://api.github.com/repos/fakeuser/fake-repository/milestones{/number}", + "node_id": "R_kgDOKIuc8Q", + "notifications_url": "https://api.github.com/repos/fakeuser/fake-repository/notifications{?since,all,participating}", + "open_issues": 0, + "pulls_url": "https://api.github.com/repos/fakeuser/fake-repository/pulls{/number}", + "releases_url": "https://api.github.com/repos/fakeuser/fake-repository/releases{/id}", + "stargazers_url": "https://api.github.com/repos/fakeuser/fake-repository/stargazers", + "statuses_url": "https://api.github.com/repos/fakeuser/fake-repository/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/fakeuser/fake-repository/subscribers", + "subscription_url": "https://api.github.com/repos/fakeuser/fake-repository/subscription", + "tags_url": "https://api.github.com/repos/fakeuser/fake-repository/tags", + "teams_url": "https://api.github.com/repos/fakeuser/fake-repository/teams", + "topics": [], + "trees_url": "https://api.github.com/repos/fakeuser/fake-repository/git/trees{/sha}", + "url": "https://github.com/fakeuser/fake-repository", + "visibility": "public", + "watchers": 0, + "web_commit_signoff_required": false + }, + "head_commit": { + "id": "f1eab4d20a72fe5495183136e36584dfab447faf", + "message": "A message", + "timestamp": "2023-09-06T15:14:10.000Z", + "url": "fake-github-url.com" + }, + "commits": [ + { + "id": "f1eab4d20a72fe5495183136e36584dfab447faf", + "message": "A message", + "timestamp": "2023-09-06T15:14:10.000Z", + "url": "fake-github-url.com" + } + ], + "sender": { + "login": "fakeuser", + "id": 102626803, + "avatar_url": "https://avatars.githubusercontent.com/u/102626803?v=4", + "html_url": "https://github.com/fakeuser", + "site_admin": false, + "events_url": "https://api.github.com/users/fakeuser/events{/privacy}", + "followers_url": "https://api.github.com/users/fakeuser/followers", + "following_url": "https://api.github.com/users/fakeuser/following{/other_user}", + "gists_url": "https://api.github.com/users/fakeuser/gists{/gist_id}", + "gravatar_id": "", + "node_id": "U_kgDOBh318w", + "organizations_url": "https://api.github.com/users/fakeuser/orgs", + "received_events_url": "https://api.github.com/users/fakeuser/received_events", + "repos_url": "https://api.github.com/users/fakeuser/repos", + "starred_url": "https://api.github.com/users/fakeuser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/fakeuser/subscriptions", + "type": "User", + "url": "https://api.github.com/users/fakeuser" + } +} +'''; + void main() { test('License round-trip', () { final licenseJson = jsonDecode(_licenseJson) as Map; @@ -39,6 +168,16 @@ void main() { expect(_prettyEncode(toJson), _prettyEncode(licenseJson)); }); + + test('PushEvent round-trip', () { + final pushEventJson = jsonDecode(_pushEventJson) as Map; + + final instance = PushEvent.fromJson(pushEventJson); + + final toJson = instance.toJson(); + + expect(_prettyEncode(toJson), _prettyEncode(pushEventJson)); + }); } String _prettyEncode(obj) => GitHubJson.encode(obj, indent: ' ');