@@ -473,6 +473,43 @@ public function createPullRequest(string $owner, string $repositoryName, string
473473 return $ responseBody ;
474474 }
475475
476+ /**
477+ * Create a webhook on a repository
478+ *
479+ * @param string $owner Owner of the repository
480+ * @param string $repositoryName Name of the repository
481+ * @param string $url Webhook URL to send events to
482+ * @param string $secret Webhook secret for signature validation
483+ * @param array<string> $events Events to trigger the webhook
484+ * @return int Webhook ID
485+ */
486+ public function createWebhook (string $ owner , string $ repositoryName , string $ url , string $ secret , array $ events = ['push ' , 'pull_request ' ]): int
487+ {
488+ $ response = $ this ->call (
489+ self ::METHOD_POST ,
490+ "/repos/ {$ owner }/ {$ repositoryName }/hooks " ,
491+ ['Authorization ' => "token $ this ->accessToken " ],
492+ [
493+ 'type ' => 'gitea ' ,
494+ 'active ' => true ,
495+ 'events ' => $ events ,
496+ 'config ' => [
497+ 'url ' => $ url ,
498+ 'content_type ' => 'json ' ,
499+ 'secret ' => $ secret ,
500+ ],
501+ ]
502+ );
503+
504+ $ responseHeaders = $ response ['headers ' ] ?? [];
505+ $ responseHeadersStatusCode = $ responseHeaders ['status-code ' ] ?? 0 ;
506+ if ($ responseHeadersStatusCode >= 400 ) {
507+ throw new Exception ("Failed to create webhook: HTTP {$ responseHeadersStatusCode }" );
508+ }
509+
510+ return (int ) ($ response ['body ' ]['id ' ] ?? 0 );
511+ }
512+
476513 public function createComment (string $ owner , string $ repositoryName , int $ pullRequestNumber , string $ comment ): string
477514 {
478515 $ url = "/repos/ {$ owner }/ {$ repositoryName }/issues/ {$ pullRequestNumber }/comments " ;
@@ -739,13 +776,142 @@ public function generateCloneCommand(string $owner, string $repositoryName, stri
739776 throw new Exception ("Not implemented yet " );
740777 }
741778
779+ /**
780+ * Parses webhook event payload
781+ *
782+ * @param string $event Type of event: push, pull_request, etc
783+ * @param string $payload The webhook payload received from Gitea
784+ * @return array<mixed> Parsed payload as an array
785+ */
742786 public function getEvent (string $ event , string $ payload ): array
743787 {
744- throw new Exception ("Not implemented yet " );
788+ $ payload = json_decode ($ payload , true );
789+
790+ if ($ payload === null || !is_array ($ payload )) {
791+ throw new Exception ("Invalid payload. " );
792+ }
793+
794+ switch ($ event ) {
795+ case 'push ' :
796+ $ payloadRepository = $ payload ['repository ' ] ?? [];
797+ $ payloadRepositoryOwner = $ payloadRepository ['owner ' ] ?? [];
798+ $ payloadSender = $ payload ['sender ' ] ?? [];
799+ $ payloadHeadCommit = $ payload ['head_commit ' ] ?? [];
800+ $ payloadHeadCommitAuthor = $ payloadHeadCommit ['author ' ] ?? [];
801+
802+ $ branchCreated = $ payload ['created ' ] ?? false ;
803+ $ branchDeleted = $ payload ['deleted ' ] ?? false ;
804+ $ repositoryId = strval ($ payloadRepository ['id ' ] ?? '' );
805+ $ repositoryName = $ payloadRepository ['name ' ] ?? '' ;
806+ $ branch = str_replace ('refs/heads/ ' , '' , $ payload ['ref ' ] ?? '' );
807+ $ repositoryUrl = $ payloadRepository ['html_url ' ] ?? '' ;
808+ $ branchUrl = !empty ($ repositoryUrl ) && !empty ($ branch ) ? $ repositoryUrl . "/src/branch/ " . $ branch : '' ;
809+ $ commitHash = $ payload ['after ' ] ?? '' ;
810+ $ owner = $ payloadRepositoryOwner ['login ' ] ?? '' ;
811+ $ authorUrl = $ payloadSender ['html_url ' ] ?? '' ;
812+ $ authorAvatarUrl = $ payloadSender ['avatar_url ' ] ?? '' ;
813+ $ headCommitAuthorName = $ payloadHeadCommitAuthor ['name ' ] ?? '' ;
814+ $ headCommitAuthorEmail = $ payloadHeadCommitAuthor ['email ' ] ?? '' ;
815+ $ headCommitMessage = $ payloadHeadCommit ['message ' ] ?? '' ;
816+ $ headCommitUrl = $ payloadHeadCommit ['url ' ] ?? '' ;
817+
818+ $ affectedFiles = [];
819+ foreach (($ payload ['commits ' ] ?? []) as $ commit ) {
820+ foreach (($ commit ['added ' ] ?? []) as $ added ) {
821+ $ affectedFiles [$ added ] = true ;
822+ }
823+
824+ foreach (($ commit ['removed ' ] ?? []) as $ removed ) {
825+ $ affectedFiles [$ removed ] = true ;
826+ }
827+
828+ foreach (($ commit ['modified ' ] ?? []) as $ modified ) {
829+ $ affectedFiles [$ modified ] = true ;
830+ }
831+ }
832+
833+ return [
834+ 'branchCreated ' => $ branchCreated ,
835+ 'branchDeleted ' => $ branchDeleted ,
836+ 'branch ' => $ branch ,
837+ 'branchUrl ' => $ branchUrl ,
838+ 'repositoryId ' => $ repositoryId ,
839+ 'repositoryName ' => $ repositoryName ,
840+ 'repositoryUrl ' => $ repositoryUrl ,
841+ 'installationId ' => '' , // Gitea doesn't have installations
842+ 'commitHash ' => $ commitHash ,
843+ 'owner ' => $ owner ,
844+ 'authorUrl ' => $ authorUrl ,
845+ 'authorAvatarUrl ' => $ authorAvatarUrl ,
846+ 'headCommitAuthorName ' => $ headCommitAuthorName ,
847+ 'headCommitAuthorEmail ' => $ headCommitAuthorEmail ,
848+ 'headCommitMessage ' => $ headCommitMessage ,
849+ 'headCommitUrl ' => $ headCommitUrl ,
850+ 'external ' => false ,
851+ 'pullRequestNumber ' => '' ,
852+ 'action ' => '' ,
853+ 'affectedFiles ' => \array_keys ($ affectedFiles ),
854+ ];
855+
856+ case 'pull_request ' :
857+ $ payloadRepository = $ payload ['repository ' ] ?? [];
858+ $ payloadRepositoryOwner = $ payloadRepository ['owner ' ] ?? [];
859+ $ payloadSender = $ payload ['sender ' ] ?? [];
860+ $ payloadPullRequest = $ payload ['pull_request ' ] ?? [];
861+ $ payloadPullRequestHead = $ payloadPullRequest ['head ' ] ?? [];
862+ $ payloadPullRequestHeadRepo = $ payloadPullRequestHead ['repo ' ] ?? [];
863+ $ payloadPullRequestUser = $ payloadPullRequest ['user ' ] ?? [];
864+ $ payloadPullRequestBase = $ payloadPullRequest ['base ' ] ?? [];
865+
866+ $ repositoryId = strval ($ payloadRepository ['id ' ] ?? '' );
867+ $ branch = $ payloadPullRequestHead ['ref ' ] ?? '' ;
868+ $ repositoryName = $ payloadRepository ['name ' ] ?? '' ;
869+ $ repositoryUrl = $ payloadRepository ['html_url ' ] ?? '' ;
870+ $ branchUrl = !empty ($ repositoryUrl ) && !empty ($ branch ) ? $ repositoryUrl . "/src/branch/ " . $ branch : '' ;
871+ $ pullRequestNumber = $ payload ['number ' ] ?? '' ;
872+ $ action = $ payload ['action ' ] ?? '' ;
873+ $ owner = $ payloadRepositoryOwner ['login ' ] ?? '' ;
874+ $ authorUrl = $ payloadSender ['html_url ' ] ?? '' ;
875+ $ authorAvatarUrl = $ payloadPullRequestUser ['avatar_url ' ] ?? '' ;
876+ $ commitHash = $ payloadPullRequestHead ['sha ' ] ?? '' ;
877+ $ headCommitUrl = $ repositoryUrl ? $ repositoryUrl . "/commit/ " . $ commitHash : '' ;
878+
879+ // Check if PR is from a fork (external)
880+ $ headRepoFullName = $ payloadPullRequestHeadRepo ['full_name ' ] ?? '' ;
881+ $ baseRepoFullName = $ payloadRepository ['full_name ' ] ?? '' ;
882+ $ external = !empty ($ headRepoFullName ) && !empty ($ baseRepoFullName ) && $ headRepoFullName !== $ baseRepoFullName ;
883+
884+ return [
885+ 'branch ' => $ branch ,
886+ 'branchUrl ' => $ branchUrl ,
887+ 'repositoryId ' => $ repositoryId ,
888+ 'repositoryName ' => $ repositoryName ,
889+ 'repositoryUrl ' => $ repositoryUrl ,
890+ 'installationId ' => '' , // Gitea doesn't have installations
891+ 'commitHash ' => $ commitHash ,
892+ 'owner ' => $ owner ,
893+ 'authorUrl ' => $ authorUrl ,
894+ 'authorAvatarUrl ' => $ authorAvatarUrl ,
895+ 'headCommitUrl ' => $ headCommitUrl ,
896+ 'external ' => $ external ,
897+ 'pullRequestNumber ' => $ pullRequestNumber ,
898+ 'action ' => $ action ,
899+ ];
900+ }
901+
902+ return [];
745903 }
746904
905+ /**
906+ * Validate webhook event
907+ *
908+ * @param string $payload Raw body of HTTP request
909+ * @param string $signature Signature provided by Gitea in X-Gitea-Signature header
910+ * @param string $signatureKey Webhook secret configured on Gitea
911+ * @return bool
912+ */
747913 public function validateWebhookEvent (string $ payload , string $ signature , string $ signatureKey ): bool
748914 {
749- throw new Exception ( " Not implemented yet " );
915+ return hash_equals ( $ signature , hash_hmac ( ' sha256 ' , $ payload , $ signatureKey ) );
750916 }
751917}
0 commit comments