Add Github Webhook data

Change-Id: I69417685de1264b10f60fc4c74e290890cdb0a67
diff --git a/server/git/webhook.go b/server/git/webhook.go
new file mode 100644
index 0000000..df12492
--- /dev/null
+++ b/server/git/webhook.go
@@ -0,0 +1,362 @@
+package git
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+)
+
+// GitHubWebhook represents the GitHub pull request webhook payload
+type GitHubWebhook struct {
+	Action      string             `json:"action"`
+	Number      int                `json:"number"`
+	PullRequest *GitHubPullRequest `json:"pull_request"`
+	Repository  *Repository        `json:"repository"`
+	Sender      *User              `json:"sender"`
+	Changes     *Changes           `json:"changes,omitempty"`
+}
+
+// GitHubPullRequest contains the pull request data from GitHub webhook
+type GitHubPullRequest struct {
+	ID                 int                `json:"id"`
+	Number             int                `json:"number"`
+	State              string             `json:"state"`
+	Locked             bool               `json:"locked"`
+	Title              string             `json:"title"`
+	Body               string             `json:"body"`
+	CreatedAt          time.Time          `json:"created_at"`
+	UpdatedAt          time.Time          `json:"updated_at"`
+	ClosedAt           *time.Time         `json:"closed_at"`
+	MergedAt           *time.Time         `json:"merged_at"`
+	MergeCommitSHA     *string            `json:"merge_commit_sha"`
+	Assignee           *User              `json:"assignee"`
+	Assignees          []*User            `json:"assignees"`
+	RequestedReviewers []*User            `json:"requested_reviewers"`
+	Labels             []*Label           `json:"labels"`
+	Milestone          *Milestone         `json:"milestone"`
+	Draft              bool               `json:"draft"`
+	Merged             bool               `json:"merged"`
+	Mergeable          *bool              `json:"mergeable"`
+	MergeableState     string             `json:"mergeable_state"`
+	MergedBy           *User              `json:"merged_by"`
+	Comments           int                `json:"comments"`
+	ReviewComments     int                `json:"review_comments"`
+	Commits            int                `json:"commits"`
+	Additions          int                `json:"additions"`
+	Deletions          int                `json:"deletions"`
+	ChangedFiles       int                `json:"changed_files"`
+	Head               *PullRequestBranch `json:"head"`
+	Base               *PullRequestBranch `json:"base"`
+	User               *User              `json:"user"`
+}
+
+// PullRequestBranch contains branch information
+type PullRequestBranch struct {
+	Label string      `json:"label"`
+	Ref   string      `json:"ref"`
+	SHA   string      `json:"sha"`
+	User  *User       `json:"user"`
+	Repo  *Repository `json:"repo"`
+}
+
+// Repository contains repository information
+type Repository struct {
+	ID                        int                  `json:"id"`
+	NodeID                    string               `json:"node_id"`
+	Name                      string               `json:"name"`
+	FullName                  string               `json:"full_name"`
+	Private                   bool                 `json:"private"`
+	Owner                     *User                `json:"owner"`
+	Description               *string              `json:"description"`
+	Fork                      bool                 `json:"fork"`
+	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                      int                  `json:"size"`
+	StargazersCount           int                  `json:"stargazers_count"`
+	WatchersCount             int                  `json:"watchers_count"`
+	Language                  *string              `json:"language"`
+	HasIssues                 bool                 `json:"has_issues"`
+	HasProjects               bool                 `json:"has_projects"`
+	HasDownloads              bool                 `json:"has_downloads"`
+	HasWiki                   bool                 `json:"has_wiki"`
+	HasPages                  bool                 `json:"has_pages"`
+	HasDiscussions            bool                 `json:"has_discussions"`
+	ForksCount                int                  `json:"forks_count"`
+	Archived                  bool                 `json:"archived"`
+	Disabled                  bool                 `json:"disabled"`
+	License                   *License             `json:"license"`
+	AllowForking              bool                 `json:"allow_forking"`
+	IsTemplate                bool                 `json:"is_template"`
+	WebCommitSignoffRequired  bool                 `json:"web_commit_signoff_required"`
+	Topics                    []string             `json:"topics"`
+	Visibility                string               `json:"visibility"`
+	DefaultBranch             string               `json:"default_branch"`
+	AllowSquashMerge          bool                 `json:"allow_squash_merge"`
+	AllowMergeCommit          bool                 `json:"allow_merge_commit"`
+	AllowRebaseMerge          bool                 `json:"allow_rebase_merge"`
+	AllowAutoMerge            bool                 `json:"allow_auto_merge"`
+	DeleteBranchOnMerge       bool                 `json:"delete_branch_on_merge"`
+	AllowUpdateBranch         bool                 `json:"allow_update_branch"`
+	UseSquashPrTitleAsDefault bool                 `json:"use_squash_pr_title_as_default"`
+	SquashMergeCommitMessage  string               `json:"squash_merge_commit_message"`
+	SquashMergeCommitTitle    string               `json:"squash_merge_commit_title"`
+	MergeCommitMessage        string               `json:"merge_commit_message"`
+	MergeCommitTitle          string               `json:"merge_commit_title"`
+	SecurityAndAnalysis       *SecurityAndAnalysis `json:"security_and_analysis"`
+}
+
+// User contains user information
+type User struct {
+	Login                   string     `json:"login"`
+	ID                      int        `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"`
+	Name                    *string    `json:"name"`
+	Company                 *string    `json:"company"`
+	Blog                    *string    `json:"blog"`
+	Location                *string    `json:"location"`
+	Email                   *string    `json:"email"`
+	Hireable                *bool      `json:"hireable"`
+	Bio                     *string    `json:"bio"`
+	TwitterUsername         *string    `json:"twitter_username"`
+	PublicRepos             int        `json:"public_repos"`
+	PublicGists             int        `json:"public_gists"`
+	Followers               int        `json:"followers"`
+	Following               int        `json:"following"`
+	CreatedAt               time.Time  `json:"created_at"`
+	UpdatedAt               time.Time  `json:"updated_at"`
+	PrivateGists            int        `json:"private_gists"`
+	TotalPrivateRepos       int        `json:"total_private_repos"`
+	OwnedPrivateRepos       int        `json:"owned_private_repos"`
+	DiskUsage               int        `json:"disk_usage"`
+	Collaborators           int        `json:"collaborators"`
+	TwoFactorAuthentication bool       `json:"two_factor_authentication"`
+	Plan                    *Plan      `json:"plan"`
+	SuspendedAt             *time.Time `json:"suspended_at"`
+	BusinessPlus            bool       `json:"business_plus"`
+	LdapDn                  *string    `json:"ldap_dn"`
+}
+
+// Label contains label information
+type Label struct {
+	ID          int     `json:"id"`
+	NodeID      string  `json:"node_id"`
+	URL         string  `json:"url"`
+	Name        string  `json:"name"`
+	Description *string `json:"description"`
+	Color       string  `json:"color"`
+	Default     bool    `json:"default"`
+}
+
+// Milestone contains milestone information
+type Milestone struct {
+	URL          string     `json:"url"`
+	HTMLURL      string     `json:"html_url"`
+	LabelsURL    string     `json:"labels_url"`
+	ID           int        `json:"id"`
+	NodeID       string     `json:"node_id"`
+	Number       int        `json:"number"`
+	State        string     `json:"state"`
+	Title        string     `json:"title"`
+	Description  *string    `json:"description"`
+	Creator      *User      `json:"creator"`
+	OpenIssues   int        `json:"open_issues"`
+	ClosedIssues int        `json:"closed_issues"`
+	CreatedAt    time.Time  `json:"created_at"`
+	UpdatedAt    time.Time  `json:"updated_at"`
+	DueOn        *time.Time `json:"due_on"`
+	ClosedAt     *time.Time `json:"closed_at"`
+}
+
+// License contains license information
+type License struct {
+	Key     string `json:"key"`
+	Name    string `json:"name"`
+	URL     string `json:"url"`
+	SpdxID  string `json:"spdx_id"`
+	NodeID  string `json:"node_id"`
+	HTMLURL string `json:"html_url"`
+}
+
+// SecurityAndAnalysis contains security and analysis information
+type SecurityAndAnalysis struct {
+	AdvancedSecurity             *SecurityFeature `json:"advanced_security"`
+	SecretScanning               *SecurityFeature `json:"secret_scanning"`
+	SecretScanningPushProtection *SecurityFeature `json:"secret_scanning_push_protection"`
+	DependabotSecurityUpdates    *SecurityFeature `json:"dependabot_security_updates"`
+	DependencyGraph              *SecurityFeature `json:"dependency_graph"`
+}
+
+// SecurityFeature contains security feature information
+type SecurityFeature struct {
+	Status string `json:"status"`
+}
+
+// Plan contains plan information
+type Plan struct {
+	Name          string `json:"name"`
+	Space         int    `json:"space"`
+	Collaborators int    `json:"collaborators"`
+	PrivateRepos  int    `json:"private_repos"`
+}
+
+// Changes contains information about what changed in the webhook
+type Changes struct {
+	Title *Change `json:"title,omitempty"`
+	Body  *Change `json:"body,omitempty"`
+	Base  *Change `json:"base,omitempty"`
+}
+
+// Change contains information about a specific change
+type Change struct {
+	From string `json:"from"`
+}
+
+// ValidateSignature validates GitHub webhook signature using SHA-256 HMAC
+func ValidateSignature(payload []byte, signature, secret string) error {
+	if secret == "" {
+		return errors.New("webhook secret not configured")
+	}
+
+	if !strings.HasPrefix(signature, "sha256=") {
+		return errors.New("signature must use sha256")
+	}
+
+	expectedMAC, err := hex.DecodeString(signature[7:])
+	if err != nil {
+		return fmt.Errorf("invalid signature format: %w", err)
+	}
+
+	mac := hmac.New(sha256.New, []byte(secret))
+	mac.Write(payload)
+	computedMAC := mac.Sum(nil)
+
+	if !hmac.Equal(expectedMAC, computedMAC) {
+		return errors.New("signature verification failed")
+	}
+
+	return nil
+}
+
+// ParseWebhook parses GitHub webhook payload
+func ParseWebhook(payload []byte) (*GitHubWebhook, error) {
+	var webhook GitHubWebhook
+	if err := json.Unmarshal(payload, &webhook); err != nil {
+		return nil, fmt.Errorf("failed to parse webhook: %w", err)
+	}
+	return &webhook, nil
+}
+
+// IsValidMergeEvent checks if webhook represents a merged PR
+func IsValidMergeEvent(webhook *GitHubWebhook) bool {
+	return webhook.Action == "closed" &&
+		webhook.PullRequest != nil &&
+		webhook.PullRequest.Merged &&
+		webhook.PullRequest.Head != nil
+}
+
+// ExtractTaskID extracts task ID from branch name like "solution/{task-id}" or "subtasks/{task-id}"
+func ExtractTaskID(branchName string) (string, error) {
+	// Match patterns like "solution/task-123", "subtasks/task-456", etc.
+	re := regexp.MustCompile(`^(?:solution|subtasks)/(.+)$`)
+	matches := re.FindStringSubmatch(branchName)
+
+	if len(matches) != 2 {
+		return "", fmt.Errorf("branch name '%s' does not match expected pattern (solution/{task-id} or subtasks/{task-id})", branchName)
+	}
+
+	return matches[1], nil
+}
+
+// ProcessMergeWebhook processes a GitHub PR merge webhook and returns the task ID
+// This function handles the complete GitHub webhook payload structure
+func ProcessMergeWebhook(payload []byte, signature, secret string) (string, error) {
+	// Validate signature
+	if err := ValidateSignature(payload, signature, secret); err != nil {
+		return "", fmt.Errorf("signature validation failed: %w", err)
+	}
+
+	// Parse webhook
+	webhook, err := ParseWebhook(payload)
+	if err != nil {
+		return "", fmt.Errorf("webhook parsing failed: %w", err)
+	}
+
+	// Check if it's a merge event
+	if !IsValidMergeEvent(webhook) {
+		return "", errors.New("not a valid merge event")
+	}
+
+	// Extract task ID from branch name
+	taskID, err := ExtractTaskID(webhook.PullRequest.Head.Ref)
+	if err != nil {
+		return "", fmt.Errorf("task ID extraction failed: %w", err)
+	}
+
+	return taskID, nil
+}
+
+// GetWebhookInfo returns comprehensive information about the webhook for debugging/logging
+func GetWebhookInfo(webhook *GitHubWebhook) map[string]interface{} {
+	info := map[string]interface{}{
+		"action": webhook.Action,
+		"number": webhook.Number,
+	}
+
+	if webhook.PullRequest != nil {
+		info["pr_id"] = webhook.PullRequest.ID
+		info["pr_title"] = webhook.PullRequest.Title
+		info["pr_state"] = webhook.PullRequest.State
+		info["pr_merged"] = webhook.PullRequest.Merged
+		info["pr_merged_at"] = webhook.PullRequest.MergedAt
+		info["pr_merged_by"] = webhook.PullRequest.MergedBy
+
+		if webhook.PullRequest.Head != nil {
+			info["head_ref"] = webhook.PullRequest.Head.Ref
+			info["head_sha"] = webhook.PullRequest.Head.SHA
+		}
+
+		if webhook.PullRequest.Base != nil {
+			info["base_ref"] = webhook.PullRequest.Base.Ref
+			info["base_sha"] = webhook.PullRequest.Base.SHA
+		}
+	}
+
+	if webhook.Repository != nil {
+		info["repo_name"] = webhook.Repository.Name
+		info["repo_full_name"] = webhook.Repository.FullName
+	}
+
+	if webhook.Sender != nil {
+		info["sender_login"] = webhook.Sender.Login
+		info["sender_id"] = webhook.Sender.ID
+	}
+
+	return info
+}