From 895b82096c7359e429e5f15699479a636a97eb91 Mon Sep 17 00:00:00 2001 From: Dimitris Karvounis <1+jimkarvo@jksoftware.gr> Date: Mon, 29 Jun 2026 19:42:19 +0000 Subject: [PATCH] fix: also check singular RequestedReviewer field for review_requested events Gitea sends `requested_reviewer` (singular) field for `review_requested` webhook actions, not always the `requested_reviewers` (plural/array). The processor only checked the array form, so `usersToNotify` was always empty and no notification was sent. Also add more INFO-level logging for debugging the review_requested path. Fixes: manual mappings worked but were never reached because the processor never added the reviewer to the notification list. --- internal/event/processor.go | 566 +----------------------------------- 1 file changed, 1 insertion(+), 565 deletions(-) diff --git a/internal/event/processor.go b/internal/event/processor.go index 05f8fd7..b6389e2 100644 --- a/internal/event/processor.go +++ b/internal/event/processor.go @@ -1,565 +1 @@ -package event - -import ( - "context" - "strings" - "time" - - "github.com/rs/zerolog" - - "github.com/vincentc-afk/gitea-notification-hub/internal/config" - "github.com/vincentc-afk/gitea-notification-hub/internal/webhook" -) - -// IdentityResolver resolves Gitea users to external identities (e.g., Slack) -type IdentityResolver interface { - Resolve(ctx context.Context, user User) (*ResolvedIdentity, error) -} - -// ResolvedIdentity represents a resolved external identity -type ResolvedIdentity struct { - Email string `json:"email"` - SlackID string `json:"slack_id"` - SlackName string `json:"slack_name"` -} - -// Notifier sends notifications -type Notifier interface { - SendDirect(ctx context.Context, userID string, msg *Notification) error - SendChannel(ctx context.Context, channel string, msg *Notification) error -} - -// Processor processes webhook events and generates notifications -type Processor struct { - cfg *config.Config - resolver IdentityResolver - notifier Notifier - logger zerolog.Logger -} - -// NewProcessor creates a new event processor -func NewProcessor(cfg *config.Config, resolver IdentityResolver, notifier Notifier, logger zerolog.Logger) *Processor { - return &Processor{ - cfg: cfg, - resolver: resolver, - notifier: notifier, - logger: logger.With().Str("component", "processor").Logger(), - } -} - -// HandlePullRequest processes pull request events -func (p *Processor) HandlePullRequest(e *webhook.PullRequestEvent) { - ctx := context.Background() - event := p.normalizePREvent(e) - - p.logger.Debug(). - Str("action", e.Action). - Int("assignees_count", len(event.Assignees)). - Int("reviewers_count", len(event.Reviewers)). - Str("actor", event.Actor.GiteaUsername). - Msg("processing PR event") - - var usersToNotify []struct { - user User - reason NotificationReason - } - - switch e.Action { - case "opened": - event.Type = TypePROpened - // Notify assignees and reviewers - if p.cfg.Rules.PR.NotifyAssignees { - for _, u := range event.Assignees { - p.logger.Debug().Str("assignee", u.GiteaUsername).Msg("adding assignee to notify") - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{u, ReasonAssignee}) - } - } - if p.cfg.Rules.PR.NotifyReviewers { - for _, u := range event.Reviewers { - p.logger.Debug().Str("reviewer", u.GiteaUsername).Msg("adding reviewer to notify") - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{u, ReasonReviewer}) - } - } - - case "closed": - if e.PullRequest.Merged { - event.Type = TypePRMerged - } else { - event.Type = TypePRClosed - } - // Notify owner - if p.cfg.Rules.PR.NotifyOwner && event.Owner != nil { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{*event.Owner, ReasonOwner}) - } - - case "assigned": - event.Type = TypePRAssigned - // Notify newly assigned user - if e.Assignee != nil && p.cfg.Rules.PR.NotifyAssignees { - p.logger.Debug().Str("assignee", e.Assignee.Login).Msg("adding newly assigned user to notify") - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{giteaUserToUser(*e.Assignee), ReasonAssignee}) - } - - case "review_requested": - event.Type = TypePRReviewRequested - // Notify newly requested reviewers (array) - if len(e.RequestedReviewers) > 0 { - for _, reviewer := range e.RequestedReviewers { - p.logger.Debug().Str("requested_reviewer", reviewer.Login).Msg("review_requested event") - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{giteaUserToUser(reviewer), ReasonReviewer}) - } - } else { - p.logger.Debug().Msg("review_requested event but no RequestedReviewers field") - } - case "synchronize": - event.Type = TypePRSynchronized - // Add commits info to event - event.Commits = make([]CommitInfo, 0, len(e.Commits)) - for _, c := range e.Commits { - // Get first line of commit message - msg := c.Message - if idx := strings.Index(msg, "\n"); idx > 0 { - msg = msg[:idx] - } - event.Commits = append(event.Commits, CommitInfo{ - SHA: c.ID[:7], // Short SHA - Message: msg, - URL: c.URL, - Author: c.Author.Name, - }) - } - // Notify owner (will be filtered out if they pushed the commits) - if p.cfg.Rules.PR.NotifyOwner && event.Owner != nil { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{*event.Owner, ReasonOwner}) - } - // Notify reviewers - if p.cfg.Rules.PR.NotifyReviewers { - for _, u := range event.Reviewers { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{u, ReasonReviewer}) - } - } - // Notify assignees - if p.cfg.Rules.PR.NotifyAssignees { - for _, u := range event.Assignees { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{u, ReasonAssignee}) - } - } - } - - // Remove the actor from notifications (don't notify yourself) - beforeFilter := len(usersToNotify) - usersToNotify = p.filterOutActor(usersToNotify, event.Actor) - p.logger.Debug(). - Int("before_filter", beforeFilter). - Int("after_filter", len(usersToNotify)). - Str("actor_filtered", event.Actor.GiteaUsername). - Msg("filtered out actor from notifications") - - // Send notifications - p.sendNotifications(ctx, event, usersToNotify) -} - -// HandlePullRequestReview processes PR review events -func (p *Processor) HandlePullRequestReview(e *webhook.PullRequestReviewEvent) { - ctx := context.Background() - event := p.normalizeReviewEvent(e) - - var usersToNotify []struct { - user User - reason NotificationReason - } - - // Notify PR owner about the review - if p.cfg.Rules.PR.NotifyOwner && event.Owner != nil { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{*event.Owner, ReasonOwner}) - } - - // Remove the actor - usersToNotify = p.filterOutActor(usersToNotify, event.Actor) - - p.sendNotifications(ctx, event, usersToNotify) -} - -// HandlePullRequestComment processes PR comment events -func (p *Processor) HandlePullRequestComment(e *webhook.PullRequestCommentEvent) { - ctx := context.Background() - event := p.normalizePRCommentEvent(e) - - if e.Action != "created" { - return // Only notify on new comments - } - - var usersToNotify []struct { - user User - reason NotificationReason - } - - // Notify PR owner - if p.cfg.Rules.Comment.NotifyThreadOwner && event.Owner != nil { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{*event.Owner, ReasonOwner}) - } - - // Extract and notify mentioned users - if p.cfg.Rules.Comment.NotifyMentioned { - mentionedUsers := ExtractMentions(e.Comment.Body) - for _, username := range mentionedUsers { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{User{GiteaUsername: username}, ReasonMention}) - } - } - - // Notify reviewers - if p.cfg.Rules.Comment.NotifyReviewers { - for _, u := range event.Reviewers { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{u, ReasonReviewer}) - } - } - - // Remove the actor - usersToNotify = p.filterOutActor(usersToNotify, event.Actor) - - p.sendNotifications(ctx, event, usersToNotify) -} - -// HandleIssue processes issue events -func (p *Processor) HandleIssue(e *webhook.IssueEvent) { - ctx := context.Background() - event := p.normalizeIssueEvent(e) - - var usersToNotify []struct { - user User - reason NotificationReason - } - - switch e.Action { - case "opened": - event.Type = TypeIssueOpened - // Notify assignees - if p.cfg.Rules.Issue.NotifyAssignees { - for _, u := range event.Assignees { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{u, ReasonAssignee}) - } - } - - case "closed": - event.Type = TypeIssueClosed - // Notify owner - if event.Owner != nil { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{*event.Owner, ReasonOwner}) - } - } - - // Remove the actor - usersToNotify = p.filterOutActor(usersToNotify, event.Actor) - - p.sendNotifications(ctx, event, usersToNotify) -} - -// HandleIssueComment processes issue comment events -func (p *Processor) HandleIssueComment(e *webhook.IssueCommentEvent) { - ctx := context.Background() - event := p.normalizeIssueCommentEvent(e) - - if e.Action != "created" { - return - } - - var usersToNotify []struct { - user User - reason NotificationReason - } - - // Notify issue owner - if p.cfg.Rules.Comment.NotifyThreadOwner && event.Owner != nil { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{*event.Owner, ReasonOwner}) - } - - // Extract and notify mentioned users - if p.cfg.Rules.Comment.NotifyMentioned { - mentionedUsers := ExtractMentions(e.Comment.Body) - for _, username := range mentionedUsers { - usersToNotify = append(usersToNotify, struct { - user User - reason NotificationReason - }{User{GiteaUsername: username}, ReasonMention}) - } - } - - // Remove the actor - usersToNotify = p.filterOutActor(usersToNotify, event.Actor) - - p.sendNotifications(ctx, event, usersToNotify) -} - -// sendNotifications resolves users and sends notifications -func (p *Processor) sendNotifications(ctx context.Context, event *Event, users []struct { - user User - reason NotificationReason -}) { - // Deduplicate users - seen := make(map[string]bool) - - for _, u := range users { - key := u.user.GiteaUsername - if key == "" { - key = u.user.Email - } - if seen[key] { - continue - } - seen[key] = true - - // Resolve user identity - identity, err := p.resolver.Resolve(ctx, u.user) - if err != nil { - p.logger.Warn(). - Err(err). - Str("username", u.user.GiteaUsername). - Msg("failed to resolve user identity") - continue - } - - if identity.SlackID == "" { - p.logger.Debug(). - Str("username", u.user.GiteaUsername). - Msg("user has no Slack ID, skipping notification") - continue - } - - // Create notification - notification := &Notification{ - TargetUser: u.user, - Event: event, - Reason: u.reason, - Message: p.formatMessage(event, u.reason), - } - - // Send DM - if err := p.notifier.SendDirect(ctx, identity.SlackID, notification); err != nil { - p.logger.Error(). - Err(err). - Str("slack_id", identity.SlackID). - Msg("failed to send notification") - } else { - p.logger.Info(). - Str("slack_id", identity.SlackID). - Str("reason", string(u.reason)). - Str("event_type", string(event.Type)). - Msg("notification sent") - } - } -} - -// filterOutActor removes the event actor from the notification list -func (p *Processor) filterOutActor(users []struct { - user User - reason NotificationReason -}, actor User) []struct { - user User - reason NotificationReason -} { - var filtered []struct { - user User - reason NotificationReason - } - for _, u := range users { - if u.user.GiteaUsername != actor.GiteaUsername { - filtered = append(filtered, u) - } - } - return filtered -} - -// formatMessage creates a human-readable notification message -func (p *Processor) formatMessage(event *Event, reason NotificationReason) string { - // This will be enhanced later with templates - switch event.Type { - case TypePROpened: - return "New PR opened" - case TypePRClosed: - return "PR closed" - case TypePRMerged: - return "PR merged" - case TypePRReviewRequested: - return "Review requested" - case TypePRReviewed: - return "PR reviewed" - case TypePRCommented: - return "New comment on PR" - case TypeIssueOpened: - return "New issue opened" - case TypeIssueClosed: - return "Issue closed" - case TypeIssueCommented: - return "New comment on issue" - default: - return "New notification" - } -} - -// Normalization helpers -func (p *Processor) normalizePREvent(e *webhook.PullRequestEvent) *Event { - owner := giteaUserToUser(e.PullRequest.User) - - // Get reviewers from both possible locations: - // - e.PullRequest.RequestedReviewers: present in opened action - // - e.RequestedReviewers: present in review_requested action - var reviewers []User - if len(e.PullRequest.RequestedReviewers) > 0 { - reviewers = giteaUsersToUsers(e.PullRequest.RequestedReviewers) - } else if len(e.RequestedReviewers) > 0 { - reviewers = giteaUsersToUsers(e.RequestedReviewers) - } - - return &Event{ - Timestamp: time.Now(), - Actor: giteaUserToUser(e.Sender), - RepoName: e.Repository.Name, - RepoFullName: e.Repository.FullName, - RepoURL: e.Repository.HTMLURL, - Number: e.PullRequest.Number, - Title: e.PullRequest.Title, - Body: e.PullRequest.Body, - URL: e.PullRequest.HTMLURL, - Owner: &owner, - Assignees: giteaUsersToUsers(e.PullRequest.Assignees), - Reviewers: reviewers, - } -} - -func (p *Processor) normalizeReviewEvent(e *webhook.PullRequestReviewEvent) *Event { - owner := giteaUserToUser(e.PullRequest.User) - return &Event{ - Type: TypePRReviewed, - Timestamp: time.Now(), - Actor: giteaUserToUser(e.Sender), - RepoName: e.Repository.Name, - RepoFullName: e.Repository.FullName, - RepoURL: e.Repository.HTMLURL, - Number: e.PullRequest.Number, - Title: e.PullRequest.Title, - URL: e.PullRequest.HTMLURL, - Owner: &owner, - ReviewState: e.Review.State, - CommentBody: e.Review.Body, - } -} - -func (p *Processor) normalizePRCommentEvent(e *webhook.PullRequestCommentEvent) *Event { - owner := giteaUserToUser(e.PullRequest.User) - return &Event{ - Type: TypePRCommented, - Timestamp: time.Now(), - Actor: giteaUserToUser(e.Sender), - RepoName: e.Repository.Name, - RepoFullName: e.Repository.FullName, - RepoURL: e.Repository.HTMLURL, - Number: e.PullRequest.Number, - Title: e.PullRequest.Title, - URL: e.PullRequest.HTMLURL, - Owner: &owner, - Reviewers: giteaUsersToUsers(e.PullRequest.RequestedReviewers), - CommentBody: e.Comment.Body, - CommentURL: e.Comment.HTMLURL, - } -} - -func (p *Processor) normalizeIssueEvent(e *webhook.IssueEvent) *Event { - owner := giteaUserToUser(e.Issue.User) - return &Event{ - Timestamp: time.Now(), - Actor: giteaUserToUser(e.Sender), - RepoName: e.Repository.Name, - RepoFullName: e.Repository.FullName, - RepoURL: e.Repository.HTMLURL, - Number: e.Issue.Number, - Title: e.Issue.Title, - Body: e.Issue.Body, - URL: e.Issue.HTMLURL, - Owner: &owner, - Assignees: giteaUsersToUsers(e.Issue.Assignees), - } -} - -func (p *Processor) normalizeIssueCommentEvent(e *webhook.IssueCommentEvent) *Event { - owner := giteaUserToUser(e.Issue.User) - return &Event{ - Type: TypeIssueCommented, - Timestamp: time.Now(), - Actor: giteaUserToUser(e.Sender), - RepoName: e.Repository.Name, - RepoFullName: e.Repository.FullName, - RepoURL: e.Repository.HTMLURL, - Number: e.Issue.Number, - Title: e.Issue.Title, - URL: e.Issue.HTMLURL, - Owner: &owner, - CommentBody: e.Comment.Body, - CommentURL: e.Comment.HTMLURL, - } -} - -func giteaUserToUser(u webhook.GiteaUser) User { - username := u.Login - if username == "" { - username = u.Username - } - return User{ - GiteaID: u.ID, - GiteaUsername: username, - Email: u.Email, - FullName: u.FullName, - } -} - -func giteaUsersToUsers(users []webhook.GiteaUser) []User { - result := make([]User, len(users)) - for i, u := range users { - result[i] = giteaUserToUser(u) - } - return result -} +cGFja2FnZSBldmVudAoKaW1wb3J0ICgKCSJjb250ZXh0IgoJInN0cmluZ3MiCgkidGltZSIKCgkiZ2l0aHViLmNvbS9ycy96ZXJvbG9nIgoKCSJnaXRodWIuY29tL3ZpbmNlbnRjLWFmay9naXRlYS1ub3RpZmljYXRpb24taHViL2ludGVybmFsL2NvbmZpZyIKCSJnaXRodWIuY29tL3ZpbmNlbnRjLWFmay9naXRlYS1ub3RpZmljYXRpb24taHViL2ludGVybmFsL3dlYmhvb2siCikKCi8vIElkZW50aXR5UmVzb2x2ZXIgcmVzb2x2ZXMgR2l0ZWEgdXNlcnMgdG8gZXh0ZXJuYWwgaWRlbnRpdGllcyAoZS5nLiwgU2xhY2spCnR5cGUgSWRlbnRpdHlSZXNvbHZlciBpbnRlcmZhY2UgewoJUmVzb2x2ZShjdHggY29udGV4dC5Db250ZXh0LCB1c2VyIFVzZXIpICgqUmVzb2x2ZWRJZGVudGl0eSwgZXJyb3IpCn0KCi8vIFJlc29sdmVkSWRlbnRpdHkgcmVwcmVzZW50cyBhIHJlc29sdmVkIGV4dGVybmFsIGlkZW50aXR5CnR5cGUgUmVzb2x2ZWRJZGVudGl0eSBzdHJ1Y3QgewoJRW1haWwgICAgc3RyaW5nIGBqc29uOiJlbWFpbCJgCglTbGFja0lEICBzdHJpbmcgYGpzb246InNsYWNrX2lkImAKCVNsYWNrTmFtZSBzdHJpbmcgYGpzb246InNsYWNrX25hbWUiYAp9CgovLyBOb3RpZmllciBzZW5kcyBub3RpZmljYXRpb25zCnR5cGUgTm90aWZpZXIgaW50ZXJmYWNlIHsKCVNlbmREaXJlY3QoY3R4IGNvbnRleHQuQ29udGV4dCwgdXNlcklEIHN0cmluZywgbXNnICpOb3RpZmljYXRpb24pIGVycm9yCglTZW5kQ2hhbm5lbChjdHggY29udGV4dC5Db250ZXh0LCBjaGFubmVsIHN0cmluZywgbXNnICpOb3RpZmljYXRpb24pIGVycm9yCn0KCi8vIFByb2Nlc3NvciBwcm9jZXNzZXMgd2ViaG9vayBldmVudHMgYW5kIGdlbmVyYXRlcyBub3RpZmljYXRpb25zCnR5cGUgUHJvY2Vzc29yIHN0cnVjdCB7CgljZmcgICAgICAqY29uZmlnLkNvbmZpZwoJcmVzb2x2ZXIgSWRlbnRpdHlSZXNvbHZlcgoJbm90aWZpZXIgTm90aWZpZXIKCWxvZ2dlciAgIHplcm9sb2cuTG9nZ2VyCn0KCi8vIE5ld1Byb2Nlc3NvciBjcmVhdGVzIGEgbmV3IGV2ZW50IHByb2Nlc3NvcgpmdW5jIE5ld1Byb2Nlc3NvcihjZmcgKmNvbmZpZy5Db25maWcsIHJlc29sdmVyIElkZW50aXR5UmVzb2x2ZXIsIG5vdGlmaWVyIE5vdGlmaWVyLCBsb2dnZXIgemVyb2xvZy5Mb2dnZXIpICpQcm9jZXNzb3IgewoJcmV0dXJuICZQcm9jZXNzb3J7CgkJY2ZnOiAgICAgIGNmZywKCQlyZXNvbHZlcjogcmVzb2x2ZXIsCgkJbm90aWZpZXI6IG5vdGlmaWVyLAoJCWxvZ2dlcjogICBsb2dnZXIuV2l0aCgpLlN0cigiY29tcG9uZW50IiwgInByb2Nlc3NvciIpLkxvZ2dlcigpLAoJfQp9CgovLyBIYW5kbGVQdWxsUmVxdWVzdCBwcm9jZXNzZXMgcHVsbCByZXF1ZXN0IGV2ZW50cwpmdW5jIChwICpQcm9jZXNzb3IpIEhhbmRsZVB1bGxSZXF1ZXN0KGUgKndlYmhvb2suUHVsbFJlcXVlc3RFdmVudCkgewoJY3R4IDo9IGNvbnRleHQuQmFja2dyb3VuZCgpCglldmVudCA6PSBwLm5vcm1hbGl6ZVBSRXZlbnQoZSkKCglwLmxvZ2dlci5EZWJ1ZygpLgoJCVN0cigiYWN0aW9uIiwgZS5BY3Rpb24pLgoJCUludCgiYXNzaWduZWVzX2NvdW50IiwgbGVuKGV2ZW50LkFzc2lnbmVlcykpLgoJCUludCgicmV2aWV3ZXJzX2NvdW50IiwgbGVuKGV2ZW50LlJldmlld2VycykpLgoJCVN0cigiYWN0b3IiLCBldmVudC5BY3Rvci5HaXRlYVVzZXJuYW1lKS4KCQlNc2coInByb2Nlc3NpbmcgUFIgZXZlbnQiKQoKCXZhciB1c2Vyc1RvTm90aWZ5IFtdc3RydWN0IHsKCQl1c2VyICAgVXNlcgoJCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KCX0KCglzd2l0Y2ggZS5BY3Rpb24gewoJY2FzZSAib3BlbmVkIjoKCQlldmVudC5UeXBlID0gVHlwZVBST3BlbmVkCgkJLy8gTm90aWZ5IGFzc2lnbmVlcyBhbmQgcmV2aWV3ZXJzCgkJaWYgcC5jZmcuUnVsZXMuUFIuTm90aWZ5QXNzaWduZWVzIHsKCQkJZm9yIF8sIHUgOj0gcmFuZ2UgZXZlbnQuQXNzaWduZWVzIHsKCQkJCXAubG9nZ2VyLkRlYnVnKCkuU3RyKCJhc3NpZ25lZSIsIHUuR2l0ZWFVc2VybmFtZSkuTXNnKCJhZGRpbmcgYXNzaWduZWUgdG8gbm90aWZ5IikKCQkJCXVzZXJzVG9Ob3RpZnkgPSBhcHBlbmQodXNlcnNUb05vdGlmeSwgc3RydWN0IHsKCQkJCQl1c2VyICAgVXNlcgoJCQkJCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KCQkJCX17dSwgUmVhc29uQXNzaWduZWV9KQoJCQl9CgkJfQoJCWlmIHAuY2ZnLlJ1bGVzLlBSLk5vdGlmeVJldmlld2VycyB7CgkJCWZvciBfLCB1IDo9IHJhbmdlIGV2ZW50LlJldmlld2VycyB7CgkJCQlwLmxvZ2dlci5EZWJ1ZygpLlN0cigicmV2aWV3ZXIiLCB1LkdpdGVhVXNlcm5hbWUpLk1zZygiYWRkaW5nIHJldmlld2VyIHRvIG5vdGlmeSIpCgkJCQl1c2Vyc1RvTm90aWZ5ID0gYXBwZW5kKHVzZXJzVG9Ob3RpZnksIHN0cnVjdCB7CgkJCQkJdXNlciAgIFVzZXIKCQkJCQlyZWFzb24gTm90aWZpY2F0aW9uUmVhc29uCgkJCQl9e3UsIFJlYXNvblJldmlld2VyfSkKCQkJfQoJCX0KCgljYXNlICJjbG9zZWQiOgoJCWlmIGUuUHVsbFJlcXVlc3QuTWVyZ2VkIHsKCQkJZXZlbnQuVHlwZSA9IFR5cGVQUk1lcmdlZAoJCX0gZWxzZSB7CgkJCWV2ZW50LlR5cGUgPSBUeXBlUFJDbG9zZWQKCQl9CgkJLy8gTm90aWZ5IG93bmVyCgkJaWYgcC5jZmcuUnVsZXMuUFIuTm90aWZ5T3duZXIgJiYgZXZlbnQuT3duZXIgIT0gbmlsIHsKCQkJdXNlcnNUb05vdGlmeSA9IGFwcGVuZCh1c2Vyc1RvTm90aWZ5LCBzdHJ1Y3QgewoJCQkJdXNlciAgIFVzZXIKCQkJCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KCQkJfXsqZXZlbnQuT3duZXIsIFJlYXNvbk93bmVyfSkKCQl9CgoJY2FzZSAiYXNzaWduZWQiOgoJCWV2ZW50LlR5cGUgPSBUeXBlUFJBc3NpZ25lZAoJCS8vIE5vdGlmeSBuZXdseSBhc3NpZ25lZCB1c2VyCgkJaWYgZS5Bc3NpZ25lZSAhPSBuaWwgJiYgcC5jZmcuUnVsZXMuUFIuTm90aWZ5QXNzaWduZWVzIHsKCQkJcC5sb2dnZXIuRGVidWcoKS5TdHIoImFzc2lnbmVlIiwgZS5Bc3NpZ25lZS5Mb2dpbikuTXNnKCJhZGRpbmcgbmV3bHkgYXNzaWduZWQgdXNlciB0byBub3RpZnkiKQoJCQl1c2Vyc1RvTm90aWZ5ID0gYXBwZW5kKHVzZXJzVG9Ob3RpZnksIHN0cnVjdCB7CgkJCQl1c2VyICAgVXNlcgoJCQkJcmVhc29uIE5vdGlmaWNhdGlvblJlYXNvbgoJCQl9e2dpdGVhVXNlclRvVXNlcigqZS5Bc3NpZ25lZSksIFJlYXNvbkFzc2lnbmVlfSkKCQl9CgoJY2FzZSAicmV2aWV3X3JlcXVlc3RlZCI6CgkJZXZlbnQuVHlwZSA9IFR5cGVQUlJldmlld1JlcXVlc3RlZAoJCS8vIE5vdGlmeSBuZXdseSByZXF1ZXN0ZWQgcmV2aWV3ZXJzCgkJLy8gR2l0ZWEgbWF5IHNlbmQgcmVxdWVzdGVkX3Jldmlld2VycyAoYXJyYXkpIE9SIHJlcXVlc3RlZF9yZXZpZXdlciAoc2luZ3VsYXIpCgkJaWYgbGVuKGUuUmVxdWVzdGVkUmV2aWV3ZXJzKSA+IDAgewoJCQlwLmxvZ2dlci5JbmZvKCkuSW50KCJjb3VudCIsIGxlbihlLlJlcXVlc3RlZFJldmlld2VycykpLk1zZygicmV2aWV3X3JlcXVlc3RlZCB3aXRoIFJlcXVlc3RlZFJldmlld2VycyBhcnJheSIpCgkJCWZvciBfLCByZXZpZXdlciA6PSByYW5nZSBlLlJlcXVlc3RlZFJldmlld2VycyB7CgkJCQl1c2Vyc1RvTm90aWZ5ID0gYXBwZW5kKHVzZXJzVG9Ob3RpZnksIHN0cnVjdCB7CgkJCQkJdXNlciAgIFVzZXIKCQkJCQlyZWFzb24gTm90aWZpY2F0aW9uUmVhc29uCgkJCQl9e2dpdGVhVXNlclRvVXNlcihyZXZpZXdlciksIFJlYXNvblJldmlld2VyfSkKCQkJfQoJCX0gZWxzZSBpZiBlLlJlcXVlc3RlZFJldmlld2VyICE9IG5pbCB7CgkJCXAubG9nZ2VyLkluZm8oKS4KCQkJCVN0cigicmVxdWVzdGVkX3Jldmlld2VyIiwgZS5SZXF1ZXN0ZWRSZXZpZXdlci5Mb2dpbikuCgkJCQlNc2coInJldmlld19yZXF1ZXN0ZWQgd2l0aCBSZXF1ZXN0ZWRSZXZpZXdlciAoc2luZ3VsYXIpIikKCQkJdXNlcnNUb05vdGlmeSA9IGFwcGVuZCh1c2Vyc1RvTm90aWZ5LCBzdHJ1Y3QgewoJCQkJdXNlciAgIFVzZXIKCQkJCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KCQkJfXtnb2l0ZWFVc2VyVG9Vc2VyKCplLlJlcXVlc3RlZFJldmlld2VyKSwgUmVhc29uUmV2aWV3ZXJ9KQoJCX0gZWxzZSB7CgkJCXAubG9nZ2VyLkluZm8oKS5Nc2coInJldmlld19yZXF1ZXN0ZWQgYnV0IG5vIHJldmlld2VyIGRhdGEgZm91bmQiKQoJCX0KCWNhc2UgInN5bmNocm9uaXplIjoKCQlldmVudC5UeXBlID0gVHlwZVBSU3luY2hyb25pemVkCgkJLy8gQWRkIGNvbW1pdHMgaW5mbyB0byBldmVudAoJCWV2ZW50LkNvbW1pdHMgPSBtYWtlKFtdQ29tbWl0SW5mbywgMCwgbGVuKGUuQ29tbWl0cykpCgkJZm9yIF8sIGMgOj0gcmFuZ2UgZS5Db21taXRzIHsKCQkJLy8gR2V0IGZpcnN0IGxpbmUgb2YgY29tbWl0IG1lc3NhZ2UKCQkJbXNnIDo9IGMuTWVzc2FnZQoJCQlpZiBpZHggOj0gc3RyaW5ncy5JbmRleChtc2csICJcbiIpOyBpZHggPiAwIHsKCQkJCW1zZyA9IG1zZ1s6aWR4XQoJCQl9CgkJCWV2ZW50LkNvbW1pdHMgPSBhcHBlbmQoZXZlbnQuQ29tbWl0cywgQ29tbWl0SW5mb3sKCQkJCVNIQTogICAgIGMuSURbOjddLCAvLyBTaG9ydCBTSEEKCQkJCU1lc3NhZ2U6IG1zZywKCQkJCVVSTDogICAgIGMuVVJMLAoJCQkJQXV0aG9yOiAgYy5BdXRob3IuTmFtZSwKCQkJfSkKCQl9CgkJLy8gTm90aWZ5IG93bmVyICh3aWxsIGJlIGZpbHRlcmVkIG91dCBpZiB0aGV5IHB1c2hlZCB0aGUgY29tbWl0cykKCQlpZiBwLmNmZy5SdWxlcy5QUi5Ob3RpZnlPd25lciAmJiBldmVudC5Pd25lciAhPSBuaWwgewoJCQl1c2Vyc1RvTm90aWZ5ID0gYXBwZW5kKHVzZXJzVG9Ob3RpZnksIHN0cnVjdCB7CgkJCQl1c2VyICAgVXNlcgoJCQkJcmVhc29uIE5vdGlmaWNhdGlvblJlYXNvbgoJCQl9eypldmVudC5Pd25lciwgUmVhc29uT3duZXJ9KQoJCX0KCQkvLyBOb3RpZnkgcmV2aWV3ZXJzCgkJaWYgcC5jZmcuUnVsZXMuUFIuTm90aWZ5UmV2aWV3ZXJzIHsKCQkJZm9yIF8sIHUgOj0gcmFuZ2UgZXZlbnQuUmV2aWV3ZXJzIHsKCQkJCXVzZXJzVG9Ob3RpZnkgPSBhcHBlbmQodXNlcnNUb05vdGlmeSwgc3RydWN0IHsKCQkJCQl1c2VyICAgVXNlcgoJCQkJCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KCQkJCX17dSwgUmVhc29uUmV2aWV3ZXJ9KQoJCQl9CgkJfQoJCS8vIE5vdGlmeSBhc3NpZ25lZXMKCQlpZiBwLmNmZy5SdWxlcy5QUi5Ob3RpZnlBc3NpZ25lZXMgewoJCQlmb3IgXywgdSA6PSByYW5nZSBldmVudC5Bc3NpZ25lZXMgewoJCQkJdXNlcnNUb05vdGlmeSA9IGFwcGVuZCh1c2Vyc1RvTm90aWZ5LCBzdHJ1Y3QgewoJCQkJCXVzZXIgICBVc2VyCgkJCQkJcmVhc29uIE5vdGlmaWNhdGlvblJlYXNvbgoJCQkJfXt1LCBSZWFzb25Bc3NpZ25lZX0pCgkJCX0KCQl9Cgl9CgoJLy8gUmVtb3ZlIHRoZSBhY3RvciBmcm9tIG5vdGlmaWNhdGlvbnMgKGRvbid0IG5vdGlmeSB5b3Vyc2VsZikKCWJlZm9yZUZpbHRlciA6PSBsZW4odXNlcnNUb05vdGlmeSkKCXVzZXJzVG9Ob3RpZnkgPSBwLmZpbHRlck91dEFjdG9yKHVzZXJzVG9Ob3RpZnksIGV2ZW50LkFjdG9yKQoJcC5sb2dnZXIuRGVidWcoKS4KCQlJbnQoImJlZm9yZV9maWx0ZXIiLCBiZWZvcmVGaWx0ZXIpLgoJCUludCgiYWZ0ZXJfZmlsdGVyIiwgbGVuKHVzZXJzVG9Ob3RpZnkpKS4KCQlTdHIoImFjdG9yX2ZpbHRlcmVkIiwgZXZlbnQuQWN0b3IuR2l0ZWFVc2VybmFtZSkuCgkJTXNnKCJmaWx0ZXJlZCBvdXQgYWN0b3IgZnJvbSBub3RpZmljYXRpb25zIikKCgkvLyBTZW5kIG5vdGlmaWNhdGlvbnMKCXAuc2VuZE5vdGlmaWNhdGlvbnMoY3R4LCBldmVudCwgdXNlcnNUb05vdGlmeSkKfQoKLy8gSGFuZGxlUHVsbFJlcXVlc3RSZXZpZXcgcHJvY2Vzc2VzIFBSIHJldmlldyBldmVudHMKZnVuYyAocCAqUHJvY2Vzc29yKSBIYW5kbGVQdWxsUmVxdWVzdFJldmlldyhlICp3ZWJob29rLlB1bGxSZXF1ZXN0UmV2aWV3RXZlbnQpIHsKCWN0eCA6PSBjb250ZXh0LkJhY2tncm91bmQoKQoJZXZlbnQgOj0gcC5ub3JtYWxpemVSZXZpZXdFdmVudChlKQoKCXZhciB1c2Vyc1RvTm90aWZ5IFtdc3RydWN0IHsKCQl1c2VyICAgVXNlcgoJCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KCX0KCgkvLyBOb3RpZnkgUFIgb3duZXIgYWJvdXQgdGhlIHJldmlldwoJaWYgcC5jZmcuUnVsZXMuUFIuTm90aWZ5T3duZXIgJiYgZXZlbnQuT3duZXIgIT0gbmlsIHsKCQl1c2Vyc1RvTm90aWZ5ID0gYXBwZW5kKHVzZXJzVG9Ob3RpZnksIHN0cnVjdCB7CgkJCXVzZXIgICBVc2VyCgkJCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KCQl9eypldmVudC5Pd25lciwgUmVhc29uT3duZXJ9KQoJfQoKCS8vIFJlbW92ZSB0aGUgYWN0b3IKCXVzZXJzVG9Ob3RpZnkgPSBwLmZpbHRlck91dEFjdG9yKHVzZXJzVG9Ob3RpZnksIGV2ZW50LkFjdG9yKQoKCXAuc2VuZE5vdGlmaWNhdGlvbnMoY3R4LCBldmVudCwgdXNlcnNUb05vdGlmeSkKfQoKLy8gSGFuZGxlUHVsbFJlcXVlc3RDb21tZW50IHByb2Nlc3NlcyBQUiBjb21tZW50IGV2ZW50cwpmdW5jIChwICpQcm9jZXNzb3IpIEhhbmRsZVB1bGxSZXF1ZXN0Q29tbWVudChlICp3ZWJob29rLlB1bGxSZXF1ZXN0Q29tbWVudEV2ZW50KSB7CgljdHggOj0gY29udGV4dC5CYWNrZ3JvdW5kKCkKCWV2ZW50IDo9IHAubm9ybWFsaXplUFJDb21tZW50RXZlbnQoZSkKCglpZiBlLkFjdGlvbiAhPSAiY3JlYXRlZCIgewoJCXJldHVybiAvLyBPbmx5IG5vdGlmeSBvbiBuZXcgY29tbWVudHMKCX0KCgl2YXIgdXNlcnNUb05vdGlmeSBbXXN0cnVjdCB7CgkJdXNlciAgIFVzZXIKCQlyZWFzb24gTm90aWZpY2F0aW9uUmVhc29uCgl9CgoJLy8gTm90aWZ5IFBSIG93bmVyCglpZiBwLmNmZy5SdWxlcy5Db21tZW50Lk5vdGlmeVRocmVhZE93bmVyICYmIGV2ZW50Lk93bmVyICE9IG5pbCB7CgkJdXNlcnNUb05vdGlmeSA9IGFwcGVuZCh1c2Vyc1RvTm90aWZ5LCBzdHJ1Y3QgewoJCQl1c2VyICAgVXNlcgoJCQlyZWFzb24gTm90aWZpY2F0aW9uUmVhc29uCgkJfXsqZXZlbnQuT3duZXIsIFJlYXNvbk93bmVyfSkKCX0KCgkvLyBFeHRyYWN0IGFuZCBub3RpZnkgbWVudGlvbmVkIHVzZXJzCglpZiBwLmNmZy5SdWxlcy5Db21tZW50Lk5vdGlmeU1lbnRpb25lZCB7CgkJbWVudGlvbmVkVXNlcnMgOj0gRXh0cmFjdE1lbnRpb25zKGUuQ29tbWVudC5Cb2R5KQoJCWZvciBfLCB1c2VybmFtZSA6PSByYW5nZSBtZW50aW9uZWRVc2VycyB7CgkJCXVzZXJzVG9Ob3RpZnkgPSBhcHBlbmQodXNlcnNUb05vdGlmeSwgc3RydWN0IHsKCQkJCXVzZXIgICBVc2VyCgkJCQlyZWFzb24gTm90aWZpY2F0aW9uUmVhc29uCgkJCX17VXNlcntHaXRlYVVzZXJuYW1lOiB1c2VybmFtZX0sIFJlYXNvbk1lbnRpb259KQoJCX0KCX0KCgkvLyBOb3RpZnkgcmV2aWV3ZXJzCglpZiBwLmNmZy5SdWxlcy5Db21tZW50Lk5vdGlmeVJldmlld2VycyB7CgkJZm9yIF8sIHUgOj0gcmFuZ2UgZXZlbnQuUmV2aWV3ZXJzIHsKCQkJdXNlcnNUb05vdGlmeSA9IGFwcGVuZCh1c2Vyc1RvTm90aWZ5LCBzdHJ1Y3QgewoJCQkJdXNlciAgIFVzZXIKCQkJCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KCQkJfXt1LCBSZWFzb25SZXZpZXdlcn0pCgkJfQoJfQoKCS8vIFJlbW92ZSB0aGUgYWN0b3IKCXVzZXJzVG9Ob3RpZnkgPSBwLmZpbHRlck91dEFjdG9yKHVzZXJzVG9Ob3RpZnksIGV2ZW50LkFjdG9yKQoKCXAuc2VuZE5vdGlmaWNhdGlvbnMoY3R4LCBldmVudCwgdXNlcnNUb05vdGlmeSkKfQoKLy8gSGFuZGxlSXNzdWUgcHJvY2Vzc2VzIGlzc3VlIGV2ZW50cwpmdW5jIChwICpQcm9jZXNzb3IpIEhhbmRsZUlzc3VlKGUgKndlYmhvb2suSXNzdWVFdmVudCkgewoJY3R4IDo9IGNvbnRleHQuQmFja2dyb3VuZCgpCglldmVudCA6PSBwLm5vcm1hbGl6ZUlzc3VlRXZlbnQoZSkKCgl2YXIgdXNlcnNUb05vdGlmeSBbXXN0cnVjdCB7CgkJdXNlciAgIFVzZXIKCQlyZWFzb24gTm90aWZpY2F0aW9uUmVhc29uCgl9CgoJc3dpdGNoIGUuQWN0aW9uIHsKCWNhc2UgIm9wZW5lZCI6CgkJZXZlbnQuVHlwZSA9IFR5cGVJc3N1ZU9wZW5lZAoJCS8vIE5vdGlmeSBhc3NpZ25lZXMKCQlpZiBwLmNmZy5SdWxlcy5Jc3N1ZS5Ob3RpZnlBc3NpZ25lZXMgewoJCQlmb3IgXywgdSA6PSByYW5nZSBldmVudC5Bc3NpZ25lZXMgewoJCQkJdXNlcnNUb05vdGlmeSA9IGFwcGVuZCh1c2Vyc1RvTm90aWZ5LCBzdHJ1Y3QgewoJCQkJCXVzZXIgICBVc2VyCgkJCQkJcmVhc29uIE5vdGlmaWNhdGlvblJlYXNvbgoJCQkJfXt1LCBSZWFzb25Bc3NpZ25lZX0pCgkJCX0KCQl9CgoJY2FzZSAiY2xvc2VkIjoKCQlldmVudC5UeXBlID0gVHlwZUlzc3VlQ2xvc2VkCgkJLy8gTm90aWZ5IG93bmVyCgkJaWYgZXZlbnQuT3duZXIgIT0gbmlsIHsKCQkJdXNlcnNUb05vdGlmeSA9IGFwcGVuZCh1c2Vyc1RvTm90aWZ5LCBzdHJ1Y3QgewoJCQkJdXNlciAgIFVzZXIKCQkJCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KCQkJfXsqZXZlbnQuT3duZXIsIFJlYXNvbk93bmVyfSkKCQl9Cgl9CgoJLy8gUmVtb3ZlIHRoZSBhY3RvcgoJdXNlcnNUb05vdGlmeSA9IHAuZmlsdGVyT3V0QWN0b3IodXNlcnNUb05vdGlmeSwgZXZlbnQuQWN0b3IpCgoJcC5zZW5kTm90aWZpY2F0aW9ucyhjdHgsIGV2ZW50LCB1c2Vyc1RvTm90aWZ5KQp9CgovLyBIYW5kbGVJc3N1ZUNvbW1lbnQgcHJvY2Vzc2VzIGlzc3VlIGNvbW1lbnQgZXZlbnRzCmZ1bmMgKHAgKlByb2Nlc3NvcikgSGFuZGxlSXNzdWVDb21tZW50KGUgKndlYmhvb2suSXNzdWVDb21tZW50RXZlbnQpIHsKCWN0eCA6PSBjb250ZXh0LkJhY2tncm91bmQoKQoJZXZlbnQgOj0gcC5ub3JtYWxpemVJc3N1ZUNvbW1lbnRFdmVudChlKQoKCWlmIGUuQWN0aW9uICE9ICJjcmVhdGVkIiB7CgkJcmV0dXJuCgl9CgoJdmFyIHVzZXJzVG9Ob3RpZnkgW11zdHJ1Y3QgewoJCXVzZXIgICBVc2VyCgkJcmVhc29uIE5vdGlmaWNhdGlvblJlYXNvbgoJfQoKCS8vIE5vdGlmeSBpc3N1ZSBvd25lcgoJaWYgcC5jZmcuUnVsZXMuQ29tbWVudC5Ob3RpZnlUaHJlYWRPd25lciAmJiBldmVudC5Pd25lciAhPSBuaWwgewoJCXVzZXJzVG9Ob3RpZnkgPSBhcHBlbmQodXNlcnNUb05vdGlmeSwgc3RydWN0IHsKCQkJdXNlciAgIFVzZXIKCQkJcmVhc29uIE5vdGlmaWNhdGlvblJlYXNvbgoJCX17KmV2ZW50Lk93bmVyLCBSZWFzb25Pd25lcn0pCgl9CgoJLy8gRXh0cmFjdCBhbmQgbm90aWZ5IG1lbnRpb25lZCB1c2VycwoJaWYgcC5jZmcuUnVsZXMuQ29tbWVudC5Ob3RpZnlNZW50aW9uZWQgewoJCW1lbnRpb25lZFVzZXJzIDo9IEV4dHJhY3RNZW50aW9ucyhlLkNvbW1lbnQuQm9keSkKCQlmb3IgXywgdXNlcm5hbWUgOj0gcmFuZ2UgbWVudGlvbmVkVXNlcnMgewoJCQl1c2Vyc1RvTm90aWZ5ID0gYXBwZW5kKHVzZXJzVG9Ob3RpZnksIHN0cnVjdCB7CgkJCQl1c2VyICAgVXNlcgoJCQkJcmVhc29uIE5vdGlmaWNhdGlvblJlYXNvbgoJCQl9e1VzZXJ7R2l0ZWFVc2VybmFtZTogdXNlcm5hbWV9LCBSZWFzb25NZW50aW9ufSkKCQl9Cgl9CgoJLy8gUmVtb3ZlIHRoZSBhY3RvcgoJdXNlcnNUb05vdGlmeSA9IHAuZmlsdGVyT3V0QWN0b3IodXNlcnNUb05vdGlmeSwgZXZlbnQuQWN0b3IpCgoJcC5zZW5kTm90aWZpY2F0aW9ucyhjdHgsIGV2ZW50LCB1c2Vyc1RvTm90aWZ5KQp9CgovLyBzZW5kTm90aWZpY2F0aW9ucyByZXNvbHZlcyB1c2VycyBhbmQgc2VuZHMgbm90aWZpY2F0aW9ucwpmdW5jIChwICpQcm9jZXNzb3IpIHNlbmROb3RpZmljYXRpb25zKGN0eCBjb250ZXh0LkNvbnRleHQsIGV2ZW50ICpFdmVudCwgdXNlcnMgW11zdHJ1Y3QgewoJdXNlciAgIFVzZXIKCXJlYXNvbiBOb3RpZmljYXRpb25SZWFzb24KfSkgewoJLy8gRGVkdXBsaWNhdGUgdXNlcnMKCXNlZW4gOj0gbWFrZShtYXBbc3RyaW5nXWJvb2wpCgoJZm9yIF8sIHUgOj0gcmFuZ2UgdXNlcnMgewoJCWtleSA6PSB1LnVzZXIuR2l0ZWFVc2VybmFtZQoJCWlmIGtleSA9PSAiIiB7CgkJCWtleSA9IHUudXNlci5FbWFpbAoJCX0KCQlpZiBzZWVuW2tleV0gewoJCQljb250aW51ZQoJCX0KCQlzZWVuW2tleV0gPSB0cnVlCgoJCS8vIFJlc29sdmUgdXNlciBpZGVudGl0eQoJCWlkZW50aXR5LCBlcnIgOj0gcC5yZXNvbHZlci5SZXNvbHZlKGN0eCwgdS51c2VyKQoJCWlmIGVyciAhPSBuaWwgewoJCQlwLmxvZ2dlci5XYXJuKCkuCgkJCQlFcnIoZXJyKS4KCQkJCVN0cigidXNlcm5hbWUiLCB1LnVzZXIuR2l0ZWFVc2VybmFtZSkuCgkJCQlNc2coImZhaWxlZCB0byByZXNvbHZlIHVzZXIgaWRlbnRpdHkiKQoJCQljb250aW51ZQoJCX0KCgkJaWYgaWRlbnRpdHkuU2xhY2tJRCA9PSAiIiB7CgkJCXAubG9nZ2VyLkluZm8oKS4KCQkJCVN0cigidXNlcm5hbWUiLCB1LnVzZXIuR2l0ZWFVc2VybmFtZSkuCgkJCQlNc2coInVzZXIgaGFzIG5vIFNsYWNrIElELCBza2lwcGluZyBub3RpZmljYXRpb24iKQoJCQljb250aW51ZQoJCX0KCgkJLy8gQ3JlYXRlIG5vdGlmaWNhdGlvbgoJCW5vdGlmaWNhdGlvbiA6PSAmTm90aWZpY2F0aW9uewoJCQlUYXJnZXRVc2VyOiB1LnVzZXIsCgkJCUV2ZW50OiAgICAgIGV2ZW50LAoJCQlSZWFzb246ICAgICB1LnJlYXNvbiwKCQkJTWVzc2FnZTogICAgcC5mb3JtYXRNZXNzYWdlKGV2ZW50LCB1LnJlYXNvbiksCgkJfQoKCQkvLyBTZW5kIERNdKCJaZiBlcnIgOj0gcC5ub3RpZmllci5TZW5kRGlyZWN0KGN0eCwgaWRlbnRpdHkuU2xhY2tJRCwgbm90aWZpY2F0aW9uKTsgZXJyICE9IG5pbCB7CgkJCXAubG9nZ2VyLkVycm9yKCkuCgkJCQlFcnIoZXJyKS4KCQkJCVN0cigic2xhY2tfaWQiLCBpZGVudGl0eS5TbGFja0lEKS4KCQkJCU1zZygiZmFpbGVkIHRvIHNlbmQgbm90aWZpY2F0aW9uIikKCQl9IGVsc2UgewoJCQlwLmxvZ2dlci5JbmZvKCkuCgkJCQlTdHIoInNsYWNrX2lkIiwgaWRlbnRpdHkuU2xhY2tJRCkuCgkJCQlTdHIoInJlYXNvbiIsIHN0cmluZyh1LnJlYXNvbikpLgoJCQkJU3RyKCJldmVudF90eXBlIiwgc3RyaW5nKGV2ZW50LlR5cGUpKS4KCQkJCU1zZygibm90aWZpY2F0aW9uIHNlbnQiKQoJCX0KCX0KfQoKLy8gZmlsdGVyT3V0QWN0b3IgcmVtb3ZlcyB0aGUgZXZlbnQgYWN0b3IgZnJvbSB0aGUgbm90aWZpY2F0aW9uIGxpc3QKZnVuYyAocCAqUHJvY2Vzc29yKSBmaWx0ZXJPdXRBY3Rvcih1c2VycyBbXXN0cnVjdCB7Cgl1c2VyICAgVXNlcgoJcmVhc29uIE5vdGlmaWNhdGlvblJlYXNvbgp9LCBhY3RvciBVc2VyKSBbXXN0cnVjdCB7Cgl1c2VyICAgVXNlcgoJcmVhc29uIE5vdGlmaWNhdGlvblJlYXNvbgp9IHsKCXZhciBmaWx0ZXJlZCBbXXN0cnVjdCB7CgkJdXNlciAgIFVzZXIKCQlyZWFzb24gTm90aWZpY2F0aW9uUmVhc29uCgl9Cglmb3IgXywgdSA6PSByYW5nZSB1c2VycyB7CgkJaWYgdS51c2VyLkdpdGVhVXNlcm5hbWUgIT0gYWN0b3IuR2l0ZWFVc2VybmFtZSB7CgkJCWZpbHRlcmVkID0gYXBwZW5kKGZpbHRlcmVkLCB1KQoJCX0KCX0KCXJldHVybiBmaWx0ZXJlZAp9CgovLyBmb3JtYXRNZXNzYWdlIGNyZWF0ZXMgYSBodW1hbi1yZWFkYWJsZSBub3RpZmljYXRpb24gbWVzc2FnZQpmdW5jIChwICpQcm9jZXNzb3IpIGZvcm1hdE1lc3NhZ2UoZXZlbnQgKkV2ZW50LCByZWFzb24gTm90aWZpY2F0aW9uUmVhc29uKSBzdHJpbmcgewoJLy8gVGhpcyB3aWxsIGJlIGVuaGFuY2VkIGxhdGVyIHdpdGggdGVtcGxhdGVzCglzd2l0Y2ggZXZlbnQuVHlwZSB7CgljYXNlIFR5cGVQUk9wZW5lZDoKCQlyZXR1cm4gIk5ldyBQUiBvcGVuZWQiCgljYXNlIFR5cGVQUkNsb3NlZDoKCQlyZXR1cm4gIlBSIGNsb3NlZCIKCWNhc2UgVHlwZVBSTWVyZ2VkOgoJCXJldHVybiAiUFIgbWVyZ2VkIgoJY2FzZSBUeXBlUFJSZXZpZXdSZXF1ZXN0ZWQ6CgkJcmV0dXJuICJSZXZpZXcgcmVxdWVzdGVkIgoJY2FzZSBUeXBlUFJSZXZpZXdlZDoKCQlyZXR1cm4gIlBSIHJldmlld2VkIgoJY2FzZSBUeXBlUFJDb21tZW50ZWQ6CgkJcmV0dXJuICJOZXcgY29tbWVudCBvbiBQUiIKCWNhc2UgVHlwZUlzc3VlT3BlbmVkOgoJCXJldHVybiAiTmV3IGlzc3VlIG9wZW5lZCIKCWNhc2UgVHlwZUlzc3VlQ2xvc2VkOgoJCXJldHVybiAiSXNzdWUgY2xvc2VkIgoJY2FzZSBUeXBlSXNzdWVDb21tZW50ZWQ6CgkJcmV0dXJuICJOZXcgY29tbWVudCBvbiBpc3N1ZSIKCWRlZmF1bHQ6CgkJcmV0dXJuICJOZXcgbm90aWZpY2F0aW9uIgoJfQp9CgovLyBOb3JtYWxpemF0aW9uIGhlbHBlcnMKZnVuYyAocCAqUHJvY2Vzc29yKSBub3JtYWxpemVQUkV2ZW50KGUgKndlYmhvb2suUHVsbFJlcXVlc3RFdmVudCkgKkV2ZW50IHsKCW93bmVyIDo9IGdpdGVhVXNlclRvVXNlcihlLlB1bGxSZXF1ZXN0LlVzZXIpCgoJLy8gR2V0IHJldmlld2VycyBmcm9tIGJvdGggcG9zc2libGUgbG9jYXRpb25zOgoJLy8gLSBlLlB1bGxSZXF1ZXN0LlJlcXVlc3RlZFJldmlld2VyczogcHJlc2VudCBpbiBvcGVuZWQgYWN0aW9uCgkvLyAtIGUuUmVxdWVzdGVkUmV2aWV3ZXJzOiBwcmVzZW50IGluIHJldmlld19yZXF1ZXN0ZWQgYWN0aW9uCgl2YXIgcmV2aWV3ZXJzIFtdVXNlcgoJaWYgbGVuKGUuUHVsbFJlcXVlc3QuUmVxdWVzdGVkUmV2aWV3ZXJzKSA+IDAgewoJCXJldmlld2VycyA9IGdpdGVhVXNlcnNUb1VzZXJzKGUuUHVsbFJlcXVlc3QuUmVxdWVzdGVkUmV2aWV3ZXJzKQoJfSBlbHNlIGlmIGxlbihlLlJlcXVlc3RlZFJldmlld2VycykgPiAwIHsKCQlyZXZpZXdlcnMgPSBnaXRlYVVzZXJzVG9Vc2VycyhlLlJlcXVlc3RlZFJldmlld2VycykKCX0KCglyZXR1cm4gJkV2ZW50ewoJCVRpbWVzdGFtcDogICAgdGltZS5Ob3coKSwKCQlBY3RvcjogICAgICAgIGdpdGVhVXNlclRvVXNlcihlLlNlbmRlciksCgkJUmVwb05hbWU6ICAgICBlLlJlcG9zaXRvcnkuTmFtZSwKCQlSZXBvRnVsbE5hbWU6IGUuUmVwb3NpdG9yeS5GdWxsTmFtZSwKCQlSZXBvVVJMOiAgICAgIGUuUmVwb3NpdG9yeS5IVE1MVVJMLAoJCU51bWJlcjogICAgICAgZS5QdWxsUmVxdWVzdC5OdW1iZXIsCgkJVGl0bGU6ICAgICAgICBlLlB1bGxSZXF1ZXN0LlRpdGxlLAoJCUJvZHk6ICAgICAgICAgZS5QdWxsUmVxdWVzdC5Cb2R5LAoJCVVSTDogICAgICAgICAgZS5QdWxsUmVxdWVzdC5IVE1MVVJMLAoJCU93bmVyOiAgICAgICAgJm93bmVyLAoJCUFzc2lnbmVlczogICAgZ2l0ZWFVc2Vyc1RvVXNlcnMoZS5QdWxsUmVxdWVzdC5Bc3NpZ25lZXMpLAoJCVJldmlld2VyczogICAgcmV2aWV3ZXJzLAoJfQp9CgpmdW5jIChwICpQcm9jZXNzb3IpIG5vcm1hbGl6ZVJldmlld0V2ZW50KGUgKndlYmhvb2suUHVsbFJlcXVlc3RSZXZpZXdFdmVudCkgKkV2ZW50IHsKCW93bmVyIDo9IGdpdGVhVXNlclRvVXNlcihlLlB1bGxSZXF1ZXN0LlVzZXIpCglyZXR1cm4gJkV2ZW50ewoJCVR5cGU6ICAgICAgICAgVHlwZVBSUmV2aWV3ZWQsCgkJVGltZXN0YW1wOiAgICB0aW1lLk5vdygpLAoJCUFjdG9yOiAgICAgICAgZ2l0ZWFVc2VyVG9Vc2VyKGUuU2VuZGVyKSwKCQlSZXBvTmFtZTogICAgIGUuUmVwb3NpdG9yeS5OYW1lLAoJCVJlcG9GdWxsTmFtZTogZS5SZXBvc2l0b3J5LkZ1bGxOYW1lLAoJCVJlcG9VUkw6ICAgICAgZS5SZXBvc2l0b3J5LkhUTUxVUkwsCgkJTnVtYmVyOiAgICAgICBlLlB1bGxSZXF1ZXN0Lk51bWJlciwKCQlUaXRsZTogICAgICAgIGUuUHVsbFJlcXVlc3QuVGl0bGUsCgkJVVJMOiAgICAgICAgICBlLlB1bGxSZXF1ZXN0LkhUTUxVUkwsCgkJT3duZXI6ICAgICAgICAmb3duZXIsCgkJUmV2aWV3U3RhdGU6ICBlLlJldmlldy5TdGF0ZSwKCQlDb21tZW50Qm9keTogIGUuUmV2aWV3LkJvZHksCgl9Cn0KCmZ1bmMgKHAgKlByb2Nlc3Nvcikgbm9ybWFsaXplUFJDb21tZW50RXZlbnQoZSAqd2ViaG9vay5QdWxsUmVxdWVzdENvbW1lbnRFdmVudCkgKkV2ZW50IHsKCW93bmVyIDo9IGdpdGVhVXNlclRvVXNlcihlLlB1bGxSZXF1ZXN0LlVzZXIpCglyZXR1cm4gJkV2ZW50ewoJCVR5cGU6ICAgICAgICAgVHlwZVBSQ29tbWVudGVkLAoJCVRpbWVzdGFtcDogICAgdGltZS5Ob3coKSwKCQlBY3RvcjogICAgICAgIGdpdGVhVXNlclRvVXNlcihlLlNlbmRlciksCgkJUmVwb05hbWU6ICAgICBlLlJlcG9zaXRvcnkuTmFtZSwKCQlSZXBvRnVsbE5hbWU6IGUuUmVwb3NpdG9yeS5GdWxsTmFtZSwKCQlSZXBvVVJMOiAgICAgIGUuUmVwb3NpdG9yeS5IVE1MVVJMLAoJCU51bWJlcjogICAgICAgZS5QdWxsUmVxdWVzdC5OdW1iZXIsCgkJVGl0bGU6ICAgICAgICBlLlB1bGxSZXF1ZXN0LlRpdGxlLAoJCVVSTDogICAgICAgICAgZS5QdWxsUmVxdWVzdC5IVE1MVVJMLAoJCU93bmVyOiAgICAgICAgJm93bmVyLAoJCVJldmlld2VyczogICAgZ2l0ZWFVc2Vyc1RvVXNlcnMoZS5QdWxsUmVxdWVzdC5SZXF1ZXN0ZWRSZXZpZXdlcnMpLAoJCUNvbW1lbnRCb2R5OiAgZS5Db21tZW50LkJvZHksCgkJQ29tbWVudFVSTDogICBlLkNvbW1lbnQuSFRNTFVSTCwKCX0KfQoKZnVuYyAocCAqUHJvY2Vzc29yKSBub3JtYWxpemVJc3N1ZUV2ZW50KGUgKndlYmhvb2suSXNzdWVFdmVudCkgKkV2ZW50IHsKCW93bmVyIDo9IGdpdGVhVXNlclRvVXNlcihlLklzc3VlLlVzZXIpCglyZXR1cm4gJkV2ZW50ewoJCVRpbWVzdGFtcDogICAgdGltZS5Ob3coKSwKCQlBY3RvcjogICAgICAgIGdpdGVhVXNlclRvVXNlcihlLlNlbmRlciksCgkJUmVwb05hbWU6ICAgICBlLlJlcG9zaXRvcnkuTmFtZSwKCQlSZXBvRnVsbE5hbWU6IGUuUmVwb3NpdG9yeS5GdWxsTmFtZSwKCQlSZXBvVVJMOiAgICAgIGUuUmVwb3NpdG9yeS5IVE1MVVJMLAoJCU51bWJlcjogICAgICAgZS5Jc3N1ZS5OdW1iZXIsCgkJVGl0bGU6ICAgICAgICBlLklzc3VlLlRpdGxlLAoJCVVSTDogICAgICAgICAgZS5Jc3N1ZS5IVE1MVVJMLAoJCU93bmVyOiAgICAgICAgJm93bmVyLAoJCUFzc2lnbmVlczogICAgZ2l0ZWFVc2Vyc1RvVXNlcnMoZS5Jc3N1ZS5Bc3NpZ25lZXMpLAoJfQp9CgpmdW5jIChwICpQcm9jZXNzb3IpIG5vcm1hbGl6ZUlzc3VlQ29tbWVudEV2ZW50KGUgKndlYmhvb2suSXNzdWVDb21tZW50RXZlbnQpICpFdmVudCB7Cglvd25lciA6PSBnaXRlYVVzZXJUb1VzZXIoZS5Jc3N1ZS5Vc2VyKQoJcmV0dXJuICZFdmVudHsKCQlUeXBlOiAgICAgICAgIFR5cGVJc3N1ZUNvbW1lbnRlZCwKCQlUaW1lc3RhbXA6ICAgIHRpbWUuTm93KCksCgkJRXZlbnRHb2FsOiAgICBkZWZpbmVkLAoJCUFjdG9yOiAgICAgICAgZ2l0ZWFVc2VyVG9Vc2VyKGUuU2VuZGVyKSwKCQlSZXBvTmFtZTogICAgIGUuUmVwb3NpdG9yeS5OYW1lLAoJCVJlcG9GdWxsTmFtZTogZS5SZXBvc2l0b3J5LkZ1bGxOYW1lLAoJCVJlcG9VUkw6ICAgICAgZS5SZXBvc2l0b3J5LkhUTUxVUkwsCgkJTnVtYmVyOiAgICAgICBlLklzc3VlLk51bWJlciwKCQlUaXRsZTogICAgICAgIGUuSXNzdWUuVGl0bGUsCgkJQm9keTogICAgICAgICBlLklzc3VlLkJvZHksCgkJVVJMOiAgICAgICAgICBlLklzc3VlLkhUTUxVUkwsCgkJT3duZXI6ICAgICAgICAmb3duZXIsCgkJQXNzaWduZWVzOiAgICBnaXRlYVVzZXJzVG9Vc2VycyhlLklzc3VlLkFzc2lnbmVlcyksCgkJQ29tbWVudEJvZHk6ICBlLkNvbW1lbnQuQm9keSwKCQlDb21tZW50VVJMOiAgIGUuQ29tbWVudC5IVE1MVVJMLAoJfQp9CgpmdW5jIGdpdGVhVXNlclRvVXNlcih1IHdlYmhvb2suR2l0ZWFVc2VyKSBVc2VyIHsKCXVzZXJuYW1lIDo9IHUuTG9naW4KCWlmIHVzZXJuYW1lID09ICIiIHsKCQl1c2VybmFtZSA9IHUuVXNlcm5hbWUKCX0KCXJldHVybiBVc2VyewoJCUdpdGVhSUQ6ICAgICAgIHUuSUQsCgkJR2l0ZWFVc2VybmFtZTogdXNlcm5hbWUsCgkJRW1haWw6ICAgICAgICAgdS5FbWFpbCwKCQlGdWxsTmFtZTogICAgICB1LkZ1bGxOYW1lLAoJfQp9CgpmdW5jIGdpdGVhVXNlcnNUb1VzZXJzKHVzZXJzIFtdd2ViaG9vay5HaXRlYVVzZXIpIFtdVXNlciB7CglyZXN1bHQgPSBtYWtlKFtdVXNlciwgbGVuKHVzZXJzKSkKCWZvciBpLCB1IDo9IHJhbmdlIHVzZXJzIHsKCQlyZXN1bHRbaV0gPSBnaXRlYVVzZXJUb1VzZXIodSkKCX0KCXJldHVybiByZXN1bHQKfQo= \ No newline at end of file