app/models/merge_request.rb
require 'tmpdir'
require 'fileutils'
require 'net/http'
require 'openssl'
require 'tempfile'
class MergeRequest < ApplicationRecord
belongs_to :author, class_name: 'User'
belongs_to :reviewer, class_name: 'User'
belongs_to :project
has_many :patches, -> { order(:created_at) }, dependent: :destroy
has_many :history_events, -> { order(:when) }, dependent: :destroy
has_many :likes, dependent: :destroy
enum status: %i(open integrating needs_rebase accepted abandoned)
# Any status >= this is considered a closed MR
CLOSE_LIMIT = 3
scope :pending, -> { where("status < #{CLOSE_LIMIT}") }
scope :closed, -> { where("status >= #{CLOSE_LIMIT}") }
validates :target_branch, presence: true
validates :subject, presence: true, length: { maximum: 255 }
validates :author, presence: true
validate :author_cant_be_reviewer
validates :target_branch, format: /\A[\w\d,\.-]+[^.](?<!\.lock)\z/
before_save :write_history
after_create :send_webpush_creation_notification
def can_update?
!%w(accepted integrating).include?(status)
end
def closed?
MergeRequest.statuses[status] >= CLOSE_LIMIT
end
def general_comments?
Comment.joins(:patch).where(patches: { merge_request_id: id }, comments: { location: 0 }).any?
end
def solve_issues_by_id(reviewer, issue_ids)
raise 'Merge request author cannot mark an issue as solved' if author == reviewer
comments.blocker.where(id: issue_ids).update_all(status: Comment.statuses[:solved], reviewer_id: reviewer.id)
add_history_event(reviewer, "tagged #{'issue'.pluralize(issue_ids.count)} #{issue_ids.to_sentence} as solved.")
end
def solve_issues_by_location(reviewer, patch, locations)
raise 'Merge request author cannot mark an issue as solved' if author == reviewer
issue_ids = patch.comments.blocker.where(location: locations).pluck(:id)
patch.comments.blocker.where(id: issue_ids).update_all(status: Comment.statuses[:solved], reviewer_id: reviewer.id)
add_history_event(reviewer, "tagged #{'issue'.pluralize(issue_ids.count)} #{issue_ids.to_sentence} as solved.")
end
def add_patch(diff:, linter_ok:, ci_enabled:, description: '')
patch = Patch.new
patch.subject = diff.subject
patch.commit_message = diff.commit_message
patch.description = description
patch.diff = diff.raw
patch.linter_ok = linter_ok
patch.gitlab_ci_status = :canceled unless ci_enabled
patches << patch
add_history_event(author, 'updated the merge request') if persisted?
end
def add_comments(author, patch, comments, blockers)
return if comments.nil?
blockers ||= {}
count = 0
transaction do
comments.each do |location, text|
next if text.strip.empty?
comment = Comment.new
comment.user = author
comment.patch = patch
comment.content = text
comment.location = location
comment.status = :blocker if !closed? && blockers[location]
comment.save!
count += 1
end
end
return if count.zero?
add_history_event(author, count == 1 ? 'added a comment.' : "added #{count} comments.")
send_webpush_comment_notification(author, count)
end
def abandon!(reviewer)
raise "Can't abandon an accepted merge request." if accepted?
add_history_event reviewer, 'abandoned the merge request'
self.reviewer = reviewer
self.status = :abandoned
save!
RemoveCIBranchesWorker.perform_async(id) if project.gitlab_ci?
end
def integrate!(reviewer, patch_id = :not_specified)
raise 'You tried to accept an outdated version of this merge request.' if patch.id != patch_id &&
patch_id != :not_specified
raise 'This merge request is already closed.' if closed?
raise 'This merge request is being integrated by another request, please wait' if integrating?
raise "The target branch was locked by #{branch_lock.who.name}" if target_branch_locked?
add_history_event reviewer, 'accepted the merge request'
self.reviewer = reviewer
self.status = :integrating
save!
RemoveCIBranchesWorker.perform_async(id) if project.gitlab_ci?
GitPushWorker.perform_async(patch.id, :integration)
end
def patch
@patch ||= patches.last
end
def patch_diff(from = 0, to = nil)
to ||= patches.count
raise ActiveRecord::RecordNotFound, 'Patch diff not found' if from >= to || to > patches.count
# convert to zero based index.
from -= 1
to -= 1
return Diff.new(patches[to].diff) if from.negative?
Diff.new(interdiff(patches[from].diff, patches[to].diff), source: :interdiff)
end
def deprecated_patches
patches.where.not(id: patch.id)
end
# FIXME: Remove this and implement a watchers list.
def people_involved(notification)
people = User.joins(:comments)
.joins('INNER JOIN patches ON patches.id = comments.patch_id')
.joins('INNER JOIN merge_requests ON merge_requests.id = patches.merge_request_id')
.where(notification => true)
.where('merge_requests.id = ?', id).uniq
people << reviewer if reviewer
(people << author).uniq
end
def comments
Comment.joins(:patch).where('patches.merge_request_id = ?', id)
end
class << self
def waiting_others(mrs, user)
mrs.select do |mr|
last_comment = mr.comments.last
# No coments
if last_comment.nil? || last_comment.patch_id != mr.patch.id
mr.author_id == user.id
# A comment
else
last_comment.user_id == user.id
end
end
end
end
def my_path
@my_path ||= Rails.application.routes.url_helpers.project_merge_request_path(project, self)
end
def send_webpush_creation_notification
users = project.users.webpush_enabled.where(notify_mr_creation_by_webpush: true).to_a - [author]
User.send_webpush(users, "MR created on #{project.name}", subject, my_path)
end
def send_webpush_accept_notification
return unless author.notify_mr_status_by_webpush?
author.send_webpush_assync('Your MR got accepted!', subject, my_path)
end
def send_webpush_comment_notification(who, n_of_comments)
return if who == author
return unless author.notify_mr_update_by_webpush?
author.send_webpush_assync("#{n_of_comments} new comments", subject, my_path)
end
def send_webpush_needs_rebase_notification
return unless author.notify_mr_status_by_webpush?
author.send_webpush_assync('Rebase needed', subject, my_path)
end
def send_webpush_integration_failed
return unless author.notify_mr_status_by_webpush?
author.send_webpush_assync('Integration failed', subject, my_path)
end
def target_branch_locked?
branch_lock.present?
end
def branch_lock
@branch_lock ||= project.locked_branches.find_by(branch: target_branch)
end
def notify_jira
uri = URI("#{project.jira_api_url}/issue/#{jira_ticket}/comment")
request = Net::HTTP::Post.new(uri.to_s)
request.basic_auth(project.jira_username, project.jira_password)
request['Content-Type'] = 'application/json'
request.body = { 'body' => "Merge request created at https://#{ReviewitConfig.mail.domain}/mr/#{id}" }.to_json
http = Net::HTTP.new(uri.hostname, uri.port)
http.use_ssl = true if uri.scheme == 'https'
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?
http.start { |h| h.request(request) }
end
private
def interdiff(diff1, diff2)
prune_git_headers!(diff1)
prune_git_headers!(diff2)
file1 = Tempfile.open('diff1') do |f|
f.puts(diff1)
f
end
file2 = Tempfile.open('diff2') do |f|
f.puts(diff2)
f
end
`interdiff #{file1.path} #{file2.path} < /dev/null`.tap do
file1.unlink
file2.unlink
end
end
GIT_HEADERS = [/^old mode .+\n/,
/^new mode .+\n/,
/^deleted file mode .+\n/,
/^new file mode .+\n/,
/^copy from .+\n/,
/^copy to .+\n/,
/^rename from .+\n/,
/^rename to .+\n/,
/^similarity index .+\n/,
/^dissimilarity index .+\n/,
/^index .+\n/]
# interdiff has a bug with some git headers in the patch, the bug was already fixed
# but most distro doesn't have this fix yet.
# https://github.com/twaugh/patchutils/commit/14261ad5461e6c4b3ffc2f87131601ff79e2a0fc
def prune_git_headers!(diff)
GIT_HEADERS.each do |header|
diff.gsub!(header, '')
end
diff
end
def write_history
return if !target_branch_changed? || target_branch_was.nil?
add_history_event(author, "changed the target branch from #{target_branch_was} to #{target_branch}")
end
def add_history_event(who, what)
history_events << HistoryEvent.new(who: who, what: what)
end
def indent_comment(comment)
comment.each_line.map { |line| " #{line}" }.join
end
def author_cant_be_reviewer
errors.add(:reviewer, 'can\'t be the author.') if !abandoned? && author == reviewer
end
end