package main import ( "crypto/hmac" "crypto/sha1" "encoding/hex" "encoding/json" "github.com/gorilla/mux" "io/ioutil" "net/http" "os" "os/exec" "path/filepath" "time" ) const ( // the directory to clone the repos in gitTree = "" secret = "" // please do not hardcode this, use .env files or something similar ) type payload interface{} type PushPayload struct { payload Ref string `json:"ref"` Before string `json:"before"` After string `json:"after"` Created bool `json:"created"` Deleted bool `json:"deleted"` Forced bool `json:"forced"` BaseRef *string `json:"base_ref"` Compare string `json:"compare"` Commits []struct { Sha string `json:"sha"` ID string `json:"id"` NodeID string `json:"node_id"` TreeID string `json:"tree_id"` Distinct bool `json:"distinct"` Message string `json:"message"` Timestamp string `json:"timestamp"` URL string `json:"url"` Author struct { Name string `json:"name"` Email string `json:"email"` Username string `json:"username"` } `json:"author"` Committer struct { Name string `json:"name"` Email string `json:"email"` Username string `json:"username"` } `json:"committer"` Added []string `json:"added"` Removed []string `json:"removed"` Modified []string `json:"modified"` } `json:"commits"` HeadCommit struct { ID string `json:"id"` NodeID string `json:"node_id"` TreeID string `json:"tree_id"` Distinct bool `json:"distinct"` Message string `json:"message"` Timestamp string `json:"timestamp"` URL string `json:"url"` Author struct { Name string `json:"name"` Email string `json:"email"` Username string `json:"username"` } `json:"author"` Committer struct { Name string `json:"name"` Email string `json:"email"` Username string `json:"username"` } `json:"committer"` Added []string `json:"added"` Removed []string `json:"removed"` Modified []string `json:"modified"` } `json:"head_commit"` Repository struct { ID int64 `json:"id"` NodeID string `json:"node_id"` Name string `json:"name"` FullName string `json:"full_name"` Owner struct { Login string `json:"login"` ID int64 `json:"id"` NodeID string `json:"node_id"` AvatarURL string `json:"avatar_url"` GravatarID string `json:"gravatar_id"` URL string `json:"url"` HTMLURL string `json:"html_url"` FollowersURL string `json:"followers_url"` FollowingURL string `json:"following_url"` GistsURL string `json:"gists_url"` StarredURL string `json:"starred_url"` SubscriptionsURL string `json:"subscriptions_url"` OrganizationsURL string `json:"organizations_url"` ReposURL string `json:"repos_url"` EventsURL string `json:"events_url"` ReceivedEventsURL string `json:"received_events_url"` Type string `json:"type"` SiteAdmin bool `json:"site_admin"` } `json:"owner"` Private bool `json:"private"` HTMLURL string `json:"html_url"` Description string `json:"description"` Fork bool `json:"fork"` URL string `json:"url"` ForksURL string `json:"forks_url"` KeysURL string `json:"keys_url"` CollaboratorsURL string `json:"collaborators_url"` TeamsURL string `json:"teams_url"` HooksURL string `json:"hooks_url"` IssueEventsURL string `json:"issue_events_url"` EventsURL string `json:"events_url"` AssigneesURL string `json:"assignees_url"` BranchesURL string `json:"branches_url"` TagsURL string `json:"tags_url"` BlobsURL string `json:"blobs_url"` GitTagsURL string `json:"git_tags_url"` GitRefsURL string `json:"git_refs_url"` TreesURL string `json:"trees_url"` StatusesURL string `json:"statuses_url"` LanguagesURL string `json:"languages_url"` StargazersURL string `json:"stargazers_url"` ContributorsURL string `json:"contributors_url"` SubscribersURL string `json:"subscribers_url"` SubscriptionURL string `json:"subscription_url"` CommitsURL string `json:"commits_url"` GitCommitsURL string `json:"git_commits_url"` CommentsURL string `json:"comments_url"` IssueCommentURL string `json:"issue_comment_url"` ContentsURL string `json:"contents_url"` CompareURL string `json:"compare_url"` MergesURL string `json:"merges_url"` ArchiveURL string `json:"archive_url"` DownloadsURL string `json:"downloads_url"` IssuesURL string `json:"issues_url"` PullsURL string `json:"pulls_url"` MilestonesURL string `json:"milestones_url"` NotificationsURL string `json:"notifications_url"` LabelsURL string `json:"labels_url"` ReleasesURL string `json:"releases_url"` CreatedAt int64 `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` PushedAt int64 `json:"pushed_at"` GitURL string `json:"git_url"` SSHURL string `json:"ssh_url"` CloneURL string `json:"clone_url"` SvnURL string `json:"svn_url"` Homepage *string `json:"homepage"` Size int64 `json:"size"` StargazersCount int64 `json:"stargazers_count"` WatchersCount int64 `json:"watchers_count"` Language *string `json:"language"` HasIssues bool `json:"has_issues"` HasDownloads bool `json:"has_downloads"` HasWiki bool `json:"has_wiki"` HasPages bool `json:"has_pages"` ForksCount int64 `json:"forks_count"` MirrorURL *string `json:"mirror_url"` OpenIssuesCount int64 `json:"open_issues_count"` Forks int64 `json:"forks"` OpenIssues int64 `json:"open_issues"` Watchers int64 `json:"watchers"` DefaultBranch string `json:"default_branch"` Stargazers int64 `json:"stargazers"` MasterBranch string `json:"master_branch"` } `json:"repository"` Pusher struct { Name string `json:"name"` Email string `json:"email"` } `json:"pusher"` Sender struct { Login string `json:"login"` ID int64 `json:"id"` NodeID string `json:"node_id"` AvatarURL string `json:"avatar_url"` GravatarID string `json:"gravatar_id"` URL string `json:"url"` HTMLURL string `json:"html_url"` FollowersURL string `json:"followers_url"` FollowingURL string `json:"following_url"` GistsURL string `json:"gists_url"` StarredURL string `json:"starred_url"` SubscriptionsURL string `json:"subscriptions_url"` OrganizationsURL string `json:"organizations_url"` ReposURL string `json:"repos_url"` EventsURL string `json:"events_url"` ReceivedEventsURL string `json:"received_events_url"` Type string `json:"type"` SiteAdmin bool `json:"site_admin"` } `json:"sender"` Installation struct { ID int `json:"id"` } `json:"installation"` } type RepositoryPayload struct { Action string `json:"action"` Repository struct { ID int64 `json:"id"` NodeID string `json:"node_id"` Name string `json:"name"` FullName string `json:"full_name"` Owner struct { Login string `json:"login"` ID int64 `json:"id"` NodeID string `json:"node_id"` AvatarURL string `json:"avatar_url"` GravatarID string `json:"gravatar_id"` URL string `json:"url"` HTMLURL string `json:"html_url"` FollowersURL string `json:"followers_url"` FollowingURL string `json:"following_url"` GistsURL string `json:"gists_url"` StarredURL string `json:"starred_url"` SubscriptionsURL string `json:"subscriptions_url"` OrganizationsURL string `json:"organizations_url"` ReposURL string `json:"repos_url"` EventsURL string `json:"events_url"` ReceivedEventsURL string `json:"received_events_url"` Type string `json:"type"` SiteAdmin bool `json:"site_admin"` } `json:"owner"` Private bool `json:"private"` HTMLURL string `json:"html_url"` Description string `json:"description"` Fork bool `json:"fork"` URL string `json:"url"` ForksURL string `json:"forks_url"` KeysURL string `json:"keys_url"` CollaboratorsURL string `json:"collaborators_url"` TeamsURL string `json:"teams_url"` HooksURL string `json:"hooks_url"` IssueEventsURL string `json:"issue_events_url"` EventsURL string `json:"events_url"` AssigneesURL string `json:"assignees_url"` BranchesURL string `json:"branches_url"` TagsURL string `json:"tags_url"` BlobsURL string `json:"blobs_url"` GitTagsURL string `json:"git_tags_url"` GitRefsURL string `json:"git_refs_url"` TreesURL string `json:"trees_url"` StatusesURL string `json:"statuses_url"` LanguagesURL string `json:"languages_url"` StargazersURL string `json:"stargazers_url"` ContributorsURL string `json:"contributors_url"` SubscribersURL string `json:"subscribers_url"` SubscriptionURL string `json:"subscription_url"` CommitsURL string `json:"commits_url"` GitCommitsURL string `json:"git_commits_url"` CommentsURL string `json:"comments_url"` IssueCommentURL string `json:"issue_comment_url"` ContentsURL string `json:"contents_url"` CompareURL string `json:"compare_url"` MergesURL string `json:"merges_url"` ArchiveURL string `json:"archive_url"` DownloadsURL string `json:"downloads_url"` IssuesURL string `json:"issues_url"` PullsURL string `json:"pulls_url"` MilestonesURL string `json:"milestones_url"` NotificationsURL string `json:"notifications_url"` LabelsURL string `json:"labels_url"` ReleasesURL string `json:"releases_url"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` PushedAt time.Time `json:"pushed_at"` GitURL string `json:"git_url"` SSHURL string `json:"ssh_url"` CloneURL string `json:"clone_url"` SvnURL string `json:"svn_url"` Homepage *string `json:"homepage"` Size int64 `json:"size"` StargazersCount int64 `json:"stargazers_count"` WatchersCount int64 `json:"watchers_count"` Language *string `json:"language"` HasIssues bool `json:"has_issues"` HasDownloads bool `json:"has_downloads"` HasWiki bool `json:"has_wiki"` HasPages bool `json:"has_pages"` ForksCount int64 `json:"forks_count"` MirrorURL *string `json:"mirror_url"` OpenIssuesCount int64 `json:"open_issues_count"` Forks int64 `json:"forks"` OpenIssues int64 `json:"open_issues"` Watchers int64 `json:"watchers"` DefaultBranch string `json:"default_branch"` } `json:"repository"` Organization struct { Login string `json:"login"` ID int64 `json:"id"` NodeID string `json:"node_id"` URL string `json:"url"` ReposURL string `json:"repos_url"` EventsURL string `json:"events_url"` MembersURL string `json:"members_url"` PublicMembersURL string `json:"public_members_url"` AvatarURL string `json:"avatar_url"` } `json:"organization"` Sender struct { Login string `json:"login"` ID int64 `json:"id"` NodeID string `json:"node_id"` AvatarURL string `json:"avatar_url"` GravatarID string `json:"gravatar_id"` URL string `json:"url"` HTMLURL string `json:"html_url"` FollowersURL string `json:"followers_url"` FollowingURL string `json:"following_url"` GistsURL string `json:"gists_url"` StarredURL string `json:"starred_url"` SubscriptionsURL string `json:"subscriptions_url"` OrganizationsURL string `json:"organizations_url"` ReposURL string `json:"repos_url"` EventsURL string `json:"events_url"` ReceivedEventsURL string `json:"received_events_url"` Type string `json:"type"` SiteAdmin bool `json:"site_admin"` } `json:"sender"` } type OnPayload func(payload payload, gw *GithubWebhook) error type GithubWebhook struct { pushEvents map[int64][]OnPayload repositoryEvents map[int64][]OnPayload repoNames map[int64]string } func (gw *GithubWebhook) AddPushEvent(repoID int64, function OnPayload) { gw.pushEvents[repoID] = append(gw.pushEvents[repoID], function) } func (gw *GithubWebhook) AddRepositoryEvent(repoID int64, function OnPayload) { gw.repositoryEvents[repoID] = append(gw.repositoryEvents[repoID], function) } func (gw *GithubWebhook) OnWebhook(w http.ResponseWriter, r *http.Request) { event := r.Header.Get("X-GitHub-Event") if event == "" { return } payload, err := ioutil.ReadAll(r.Body) if err != nil || len(payload) == 0 { return } // when a secret is given, this checks if the webhook and the local secret are even if secret != "" { signature := r.Header.Get("X-Hub-Signature") if len(signature) == 0 { return } mac := hmac.New(sha1.New, []byte(secret)) _, _ = mac.Write(payload) expectedMAC := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(signature[5:]), []byte(expectedMAC)) { return } } switch event { case "push": var pushPayload PushPayload if err = json.Unmarshal(payload, &pushPayload); err != nil { return } if events, ok := gw.pushEvents[pushPayload.Repository.ID]; ok { for _, event := range events { if err := event(pushPayload, gw); err != nil { panic(err) } } } case "repository": var repositoryPayload RepositoryPayload if err = json.Unmarshal(payload, &repositoryPayload); err != nil { return } if events, ok := gw.repositoryEvents[repositoryPayload.Repository.ID]; ok { for _, event := range events { if err := event(repositoryPayload, gw); err != nil { panic(err) } } } default: return } } func main() { r := mux.NewRouter() gw := &GithubWebhook{pushEvents: make(map[int64][]OnPayload), repositoryEvents: make(map[int64][]OnPayload), repoNames: make(map[int64]string)} gw.AddPushEvent(364735060, updateLocal) gw.AddRepositoryEvent(364735060, renameLocal) // NOTE: To find out the id of a repository, go to the repo page, open the browser / developer console and execute `document.head.querySelector("meta[name=octolytics-dimension-repository_id]").content` r.HandleFunc("/github_webhook", gw.OnWebhook).Methods("POST") http.ListenAndServe(":8080", r) } func checkIfLocalPresent(gw *GithubWebhook, id int64, name string, cloneUrl string) error { if _, ok := gw.repoNames[id]; !ok { files, err := ioutil.ReadDir(gitTree) if err != nil { return err } for _, file := range files { if file.IsDir() && file.Name() == name { gw.repoNames[id] = name return nil } } cmd := exec.Command("git", "clone", cloneUrl, filepath.Join(gitTree, name)) if err = cmd.Run(); err != nil { return err } gw.repoNames[id] = name } return nil } func renameLocal(payload2 payload, gw *GithubWebhook) error { repoPayload := payload2.(RepositoryPayload) if repoPayload.Action == "rename" { if err := checkIfLocalPresent(gw, repoPayload.Repository.ID, repoPayload.Repository.Name, repoPayload.Repository.CloneURL); err != nil { return err } os.Rename(filepath.Join(gitTree, gw.repoNames[repoPayload.Repository.ID]), filepath.Join(gitTree, repoPayload.Repository.Name)) gw.repoNames[repoPayload.Repository.ID] = repoPayload.Repository.Name } return nil } func updateLocal(payload2 payload, gw *GithubWebhook) error { pushPayload := payload2.(PushPayload) if err := checkIfLocalPresent(gw, pushPayload.Repository.ID, pushPayload.Repository.Name, pushPayload.Repository.CloneURL); err != nil { return err } return exec.Command("git", "-C", filepath.Join(gitTree, pushPayload.Repository.Name), "pull", "origin").Run() }