This instance was upgraded to Heptapod 0.31.0 (final) today.

merge_request.rb 7.76 KB
Newer Older
1
2
3
4
# == Schema Information
#
# Table name: merge_requests
#
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#  id                :integer          not null, primary key
#  target_branch     :string(255)      not null
#  source_branch     :string(255)      not null
#  source_project_id :integer          not null
#  author_id         :integer
#  assignee_id       :integer
#  title             :string(255)
#  created_at        :datetime         not null
#  updated_at        :datetime         not null
#  st_commits        :text(2147483647)
#  st_diffs          :text(2147483647)
#  milestone_id      :integer
#  state             :string(255)
#  merge_status      :string(255)
#  target_project_id :integer          not null
#  iid               :integer
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
21
#  description       :text
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
22
#
23

24
require Rails.root.join("app/models/commit")
25
require Rails.root.join("lib/static_model")
26

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
27
class MergeRequest < ActiveRecord::Base
28
  include Issuable
29
  include InternalId
30

31
32
  belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
  belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
33

34
  has_one :merge_request_diff, dependent: :destroy
35
  after_create :create_merge_request_diff
36
37

  delegate :commits, :diffs, :last_commit, :last_commit_short_sha, to: :merge_request_diff, prefix: nil
38

39
  attr_accessible :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, :author_id_of_changes, :state_event, :description
40

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
41
42
  attr_accessor :should_remove_source_branch

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
43
44
45
46
  # When this attribute is true some MR validation is ignored
  # It allows us to close or modify broken merge requests
  attr_accessor :allow_broken

Andrew8xx8's avatar
Andrew8xx8 committed
47
  state_machine :state, initial: :opened do
48
49
50
51
52
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :merge do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
53
      transition [:reopened, :opened, :locked] => :merged
54
55
56
    end

    event :reopen do
Andrew8xx8's avatar
Andrew8xx8 committed
57
      transition closed: :reopened
58
59
    end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
60
61
62
63
64
65
66
67
    event :lock do
      transition [:reopened, :opened] => :locked
    end

    event :unlock do
      transition locked: :reopened
    end

68
69
70
71
    state :opened
    state :reopened
    state :closed
    state :merged
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
72
    state :locked
73
74
  end

75
76
77
78
79
80
81
82
83
84
85
86
87
  state_machine :merge_status, initial: :unchecked do
    event :mark_as_unchecked do
      transition [:can_be_merged, :cannot_be_merged] => :unchecked
    end

    event :mark_as_mergeable do
      transition unchecked: :can_be_merged
    end

    event :mark_as_unmergeable do
      transition unchecked: :cannot_be_merged
    end

88
    state :unchecked
89
90
91
    state :can_be_merged
    state :cannot_be_merged
  end
92

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
93
  validates :source_project, presence: true, unless: :allow_broken
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
94
  validates :source_branch, presence: true
95
  validates :target_project, presence: true
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
96
  validates :target_branch, presence: true
97
  validate :validate_branches
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
98

99
100
  scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) }
  scope :of_user_team, ->(team) { where("(source_project_id in (:team_project_ids) OR target_project_id in (:team_project_ids) AND assignee_id in (:team_member_ids))", team_project_ids: team.project_ids, team_member_ids: team.member_ids) }
101
102
  scope :opened, -> { with_state(:opened) }
  scope :closed, -> { with_state(:closed) }
103
  scope :merged, -> { with_state(:merged) }
104
  scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
105
  scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
106
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
107
  scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
108
  scope :of_projects, ->(ids) { where(target_project_id: ids) }
109
110
111
112
  # Closed scope for merge request should return
  # both merged and closed mr's
  scope :closed, -> { with_states(:closed, :merged) }

113
  def validate_branches
114
    if target_project == source_project && target_branch == source_branch
115
      errors.add :branch_conflict, "You can not use same project/branch for source and target"
116
    end
117

118
    if opened? || reopened?
Izaak Alpert's avatar
Izaak Alpert committed
119
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
120
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
121

122
      if similar_mrs.any?
Izaak Alpert's avatar
Izaak Alpert committed
123
        errors.add :base, "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
124
      end
125
    end
126
127
  end

128
  def reload_code
129
    merge_request_diff.reload_content if opened?
130
131
  end

132
  def check_if_can_be_merged
133
134
135
136
137
    if Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged?
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
138
139
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
140
  def merge_event
141
    self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
142
143
  end

144
  def closed_event
145
    self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
146
147
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
148
  def automerge!(current_user, commit_message = nil)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
149
    MergeRequests::AutoMergeService.new.execute(self, current_user, commit_message)
150
  end
randx's avatar
randx committed
151

152
  def mr_and_commit_notes
153
154
155
156
    # Fetch comments only from last 100 commits
    commits_for_notes_limit = 100
    commit_ids = commits.last(commits_for_notes_limit).map(&:id)

157
158
159
160
161
    project.notes.where(
      "(noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR (noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
      mr_id: id,
      commit_ids: commit_ids
    )
162
  end
163

164
165
166
  # Returns the raw diff for this merge request
  #
  # see "git diff"
167
168
  def to_diff(current_user)
    Gitlab::Satellite::MergeAction.new(current_user, self).diff_in_satellite
169
170
171
172
173
  end

  # Returns the commit as a series of email patches.
  #
  # see "git format-patch"
174
175
  def to_patch(current_user)
    Gitlab::Satellite::MergeAction.new(current_user, self).format_patch
176
  end
177

178
179
180
181
182
183
184
185
  def for_fork?
    target_project != source_project
  end

  def disallow_source_branch_removal?
    (source_project.root_ref? source_branch) || for_fork?
  end

186
187
188
189
  def project
    target_project
  end

190
191
192
  # Return the set of issues that will be closed if this merge request is accepted.
  def closes_issues
    if target_branch == project.default_branch
193
      commits.map { |c| c.closes_issues(project) }.flatten.uniq.sort_by(&:id)
194
195
196
197
198
199
200
201
202
203
    else
      []
    end
  end

  # Mentionable override.
  def gfm_reference
    "merge request !#{iid}"
  end

204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
  def target_project_path
    if target_project
      target_project.path_with_namespace
    else
      "(removed)"
    end
  end

  def source_project_path
    if source_project
      source_project.path_with_namespace
    else
      "(removed)"
    end
  end

  def source_branch_exists?
    return false unless self.source_project

    self.source_project.repository.branch_names.include?(self.source_branch)
  end

  def target_branch_exists?
    return false unless self.target_project

    self.target_project.repository.branch_names.include?(self.target_branch)
  end

Drew Blessing's avatar
Drew Blessing committed
232
233
234
235
236
237
238
239
240
241
242
243
244
245
  # Reset merge request events cache
  #
  # Since we do cache @event we need to reset cache in special cases:
  # * when a merge request is updated
  # Events cache stored like  events/23-20130109142513.
  # The cache key includes updated_at timestamp.
  # Thus it will automatically generate a new fragment
  # when the event is updated because the key changes.
  def reset_events_cache
    Event.where(target_id: self.id, target_type: 'MergeRequest').
        order('id DESC').limit(100).
        update_all(updated_at: Time.now)
  end

246
247
248
  def merge_commit_message
    message = "Merge branch '#{source_branch}' into '#{target_branch}'"
    message << "\n\n"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
249
    message << title.to_s
250
    message << "\n\n"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
251
252
    message << description.to_s
    message
253
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
254
end