<commit_before>class Subject < ApplicationRecord
  has_many :notifications, foreign_key: :subject_url, primary_key: :url
  has_many :labels, dependent: :delete_all
  has_many :users, through: :notifications
  has_many :comments, dependent: :delete_all
  belongs_to :repository, foreign_key: :repository_full_name, primary_key: :full_name, optional: true
  has_one :app_installation, through: :repository

  BOT_AUTHOR_REGEX = /\A(.*)\[bot\]\z/.freeze
  private_constant :BOT_AUTHOR_REGEX

  scope :label, ->(label_name) { joins(:labels).where(Label.arel_table[:name].matches(label_name)) }
  scope :repository, ->(full_name) { where(arel_table[:url].matches("%/repos/#{full_name}/%")) }

  validates :url, presence: true, uniqueness: true

  after_update :sync_involved_users
  after_save :push_to_channels

  def update_labels(remote_labels)
    existing_labels = labels.to_a
    remote_labels.each do |l|
      label = labels.find_by_github_id(l['id'])
      if label.nil?
        labels.create({
          github_id: l['id'],
          color: l['color'],
          name: l['name'],
        })
      else
        label.github_id = l['id'] # smoothly migrate legacy labels
        label.color = l['color']
        label.name = l['name']
        label.save if label.changed?
      end
    end

    remote_label_ids = remote_labels.map{|l| l['id'] }
    deleted_labels = existing_labels.reject{|l| remote_label_ids.include?(l.github_id) }
    deleted_labels.each(&:destroy)
    push_to_channels if existing_labels != labels.to_a
  end

  def sync_involved_users
    return unless Octobox.github_app?
    involved_user_ids.each { |user_id| SyncNotificationsWorker.perform_in(1.minute, user_id) }
  end

  def self.sync(remote_subject)
    subject = Subject.find_or_create_by(url: remote_subject['url'])

    # webhook payloads don't always have 'repository' info
    if remote_subject['repository']
      full_name = remote_subject['repository']['full_name']
    else
      full_name = extract_full_name(remote_subject['url'])
    end

    subject.update({
      repository_full_name: full_name,
      github_id: remote_subject['id'],
      state: remote_subject['merged_at'].present? ? 'merged' : remote_subject['state'],
      author: remote_subject.fetch('user', {})['login'],
      html_url: remote_subject['html_url'],
      created_at: remote_subject['created_at'] || Time.current,
      updated_at: remote_subject['updated_at'] || Time.current,
      comment_count: remote_subject['comments'] || remote_subject.fetch('commit', {})['comment_count'],
      assignees: ":#{Array(remote_subject['assignees'].try(:map) {|a| a['login'] }).join(':')}:",
      locked: remote_subject['locked'],
      sha: remote_subject.fetch('head', {})['sha'],
      body: remote_subject['body']
    })

    return unless subject.persisted?

    subject.update_labels(remote_subject['labels']) if remote_subject['labels'].present?
    subject.update_comments if Octobox.include_comments? && subject.has_comments?
    subject.update_status
    subject.sync_involved_users if (subject.saved_changes.keys & subject.notifiable_fields).any?
  end

  def self.sync_status(sha, repository_full_name)
    where(repository_full_name: repository_full_name).find_by_sha(sha)&.update_status
  end

  def has_comments?
     comment_count && comment_count > 0
  end

  def update_status
    if sha.present?
      remote_status = download_status
      if remote_status.present?
        self.status = assign_status(remote_status)
        self.save if changed?
      end
    end
  end

  def author_url_path
    if bot_author?
      "/apps/#{BOT_AUTHOR_REGEX.match(author)[1]}"
    else
      "/#{author}"
    end
  end

  def update_comments
    remote_comments = download_comments
    return unless remote_comments.present?
    remote_comments.each do |remote_comment|
      comments.find_or_create_by(github_id: remote_comment.id) do |comment|
        comment.author = remote_comment.user.login
        comment.body = remote_comment.body
        comment.author_association = remote_comment.author_association
        comment.created_at = remote_comment.created_at
        comment.save
      end
    end
  end

  def notifiable_fields
    ['state', 'assignees', 'locked', 'sha', 'comment_count']
  end

  def push_to_channels
    notifications.find_each(&:push_to_channel) if (saved_changes.keys & pushable_fields).any?
  end

  private

  def pushable_fields
    ['state', 'status', 'body']
  end

  def assign_status(remote_status)
    if remote_status.state == 'pending'
      remote_status.statuses.present? ? remote_status.state : nil
    else
      remote_status.state
    end
  end

  def download_status
    return unless github_client
    github_client.combined_status(repository_full_name, sha)
  rescue Octokit::ClientError
    nil
  end

  def download_comments
    return unless github_client
    github_client.get(url.gsub('/pulls/', '/issues/') + '/comments')
  rescue Octokit::ClientError => e
    nil
  end

  def github_client
    if app_installation.present?
      Octobox.installation_client(app_installation.github_id)
    else
      users.first&.subject_client
    end
  end

  def self.extract_full_name(url)
    url.match(/\/repos\/([\w.-]+\/[\w.-]+)\//)[1]
  end

  def involved_user_ids
    ids = users.pluck(:id)
    ids += repository.users.not_recently_synced.pluck(:id) if repository.present?
    ids.uniq
  end

  def bot_author?
    BOT_AUTHOR_REGEX.match?(author)
  end
end
<commit_msg>Involved users should always be ones that haven't been recently synced (#1282)<commit_after>class Subject < ApplicationRecord
  has_many :notifications, foreign_key: :subject_url, primary_key: :url
  has_many :labels, dependent: :delete_all
  has_many :users, through: :notifications
  has_many :comments, dependent: :delete_all
  belongs_to :repository, foreign_key: :repository_full_name, primary_key: :full_name, optional: true
  has_one :app_installation, through: :repository

  BOT_AUTHOR_REGEX = /\A(.*)\[bot\]\z/.freeze
  private_constant :BOT_AUTHOR_REGEX

  scope :label, ->(label_name) { joins(:labels).where(Label.arel_table[:name].matches(label_name)) }
  scope :repository, ->(full_name) { where(arel_table[:url].matches("%/repos/#{full_name}/%")) }

  validates :url, presence: true, uniqueness: true

  after_update :sync_involved_users
  after_save :push_to_channels

  def update_labels(remote_labels)
    existing_labels = labels.to_a
    remote_labels.each do |l|
      label = labels.find_by_github_id(l['id'])
      if label.nil?
        labels.create({
          github_id: l['id'],
          color: l['color'],
          name: l['name'],
        })
      else
        label.github_id = l['id'] # smoothly migrate legacy labels
        label.color = l['color']
        label.name = l['name']
        label.save if label.changed?
      end
    end

    remote_label_ids = remote_labels.map{|l| l['id'] }
    deleted_labels = existing_labels.reject{|l| remote_label_ids.include?(l.github_id) }
    deleted_labels.each(&:destroy)
    push_to_channels if existing_labels != labels.to_a
  end

  def sync_involved_users
    return unless Octobox.github_app?
    involved_user_ids.each { |user_id| SyncNotificationsWorker.perform_in(1.minute, user_id) }
  end

  def self.sync(remote_subject)
    subject = Subject.find_or_create_by(url: remote_subject['url'])

    # webhook payloads don't always have 'repository' info
    if remote_subject['repository']
      full_name = remote_subject['repository']['full_name']
    else
      full_name = extract_full_name(remote_subject['url'])
    end

    subject.update({
      repository_full_name: full_name,
      github_id: remote_subject['id'],
      state: remote_subject['merged_at'].present? ? 'merged' : remote_subject['state'],
      author: remote_subject.fetch('user', {})['login'],
      html_url: remote_subject['html_url'],
      created_at: remote_subject['created_at'] || Time.current,
      updated_at: remote_subject['updated_at'] || Time.current,
      comment_count: remote_subject['comments'] || remote_subject.fetch('commit', {})['comment_count'],
      assignees: ":#{Array(remote_subject['assignees'].try(:map) {|a| a['login'] }).join(':')}:",
      locked: remote_subject['locked'],
      sha: remote_subject.fetch('head', {})['sha'],
      body: remote_subject['body']
    })

    return unless subject.persisted?

    subject.update_labels(remote_subject['labels']) if remote_subject['labels'].present?
    subject.update_comments if Octobox.include_comments? && subject.has_comments?
    subject.update_status
    subject.sync_involved_users if (subject.saved_changes.keys & subject.notifiable_fields).any?
  end

  def self.sync_status(sha, repository_full_name)
    where(repository_full_name: repository_full_name).find_by_sha(sha)&.update_status
  end

  def has_comments?
     comment_count && comment_count > 0
  end

  def update_status
    if sha.present?
      remote_status = download_status
      if remote_status.present?
        self.status = assign_status(remote_status)
        self.save if changed?
      end
    end
  end

  def author_url_path
    if bot_author?
      "/apps/#{BOT_AUTHOR_REGEX.match(author)[1]}"
    else
      "/#{author}"
    end
  end

  def update_comments
    remote_comments = download_comments
    return unless remote_comments.present?
    remote_comments.each do |remote_comment|
      comments.find_or_create_by(github_id: remote_comment.id) do |comment|
        comment.author = remote_comment.user.login
        comment.body = remote_comment.body
        comment.author_association = remote_comment.author_association
        comment.created_at = remote_comment.created_at
        comment.save
      end
    end
  end

  def notifiable_fields
    ['state', 'assignees', 'locked', 'sha', 'comment_count']
  end

  def push_to_channels
    notifications.find_each(&:push_to_channel) if (saved_changes.keys & pushable_fields).any?
  end

  private

  def pushable_fields
    ['state', 'status', 'body']
  end

  def assign_status(remote_status)
    if remote_status.state == 'pending'
      remote_status.statuses.present? ? remote_status.state : nil
    else
      remote_status.state
    end
  end

  def download_status
    return unless github_client
    github_client.combined_status(repository_full_name, sha)
  rescue Octokit::ClientError
    nil
  end

  def download_comments
    return unless github_client
    github_client.get(url.gsub('/pulls/', '/issues/') + '/comments')
  rescue Octokit::ClientError => e
    nil
  end

  def github_client
    if app_installation.present?
      Octobox.installation_client(app_installation.github_id)
    else
      users.first&.subject_client
    end
  end

  def self.extract_full_name(url)
    url.match(/\/repos\/([\w.-]+\/[\w.-]+)\//)[1]
  end

  def involved_user_ids
    ids = users.not_recently_synced.pluck(:id)
    ids += repository.users.not_recently_synced.pluck(:id) if repository.present?
    ids.uniq
  end

  def bot_author?
    BOT_AUTHOR_REGEX.match?(author)
  end
end

{'fraction_non_alphanumeric': 0.11549244822284525, 'fraction_numerical': 0.001085285339603871, 'mean_word_length': 3.7357601713062096, 'pattern_counts': {'":': 2, '<': 5, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '195858', 'n_tokens_mistral': 3584, 'n_tokens_neox': 3484, 'n_words': 675}