summaryrefslogtreecommitdiff
path: root/app/models/merge_request.rb
blob: b5d1a34ec318cc2ae87c49456c67e702d4755832 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# == Schema Information
#
# Table name: merge_requests
#
#  id             :integer          not null, primary key
#  target_project_id :integer       not null
#  target_branch  :string(255)      not null
#  source_project_id :integer       not null
#  source_branch  :string(255)      not null
#  author_id      :integer
#  assignee_id    :integer
#  title          :string(255)
#  created_at     :datetime
#  updated_at     :datetime
#  st_commits     :text(2147483647)
#  st_diffs       :text(2147483647)
#  milestone_id   :integer
#  state          :string(255)
#  merge_status   :string(255)
#

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

class MergeRequest < ActiveRecord::Base

  include Issuable

  belongs_to :target_project, :foreign_key => :target_project_id, class_name: "Project"
  belongs_to :source_project, :foreign_key => :source_project_id, class_name: "Project"

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


  attr_accessor :should_remove_source_branch

  state_machine :state, initial: :opened do
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :merge do
      transition [:reopened, :opened] => :merged
    end

    event :reopen do
      transition closed: :reopened
    end

    state :opened

    state :reopened

    state :closed

    state :merged
  end

  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

    state :unchecked

    state :can_be_merged

    state :cannot_be_merged
  end

  serialize :st_commits
  serialize :st_diffs

  validates :source_project, presence: true
  validates :source_branch, presence: true
  validates :target_project, presence: true
  validates :target_branch, presence: true
  validate :validate_branches

  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) }
  scope :opened, -> { with_state(:opened) }
  scope :closed, -> { with_state(:closed) }
  scope :merged, -> { with_state(:merged) }
  scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
  scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
  scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
  scope :by_project, ->(project_id) { where("source_project_id = :project_id OR target_project_id = :project_id", project_id: project_id) }
  scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) }
  # Closed scope for merge request should return
  # both merged and closed mr's
  scope :closed, -> { with_states(:closed, :merged) }

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

    if opened? || reopened?
      similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened
      similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id

      if similar_mrs.any?
        errors.add :base, "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
      end
    end
  end

  def reload_code
    self.reloaded_commits
    self.reloaded_diffs
  end

  def check_if_can_be_merged
    if Gitlab::Satellite::MergeAction.new(self.author, self).can_be_merged?
      mark_as_mergeable
    else
      mark_as_unmergeable
    end
  end

  def diffs
    @diffs ||= (load_diffs(st_diffs) || [])
  end

  def reloaded_diffs
    if opened? && unmerged_diffs.any?
      self.st_diffs = dump_diffs(unmerged_diffs)
      self.save
    end
  end

  def broken_diffs?
    diffs == broken_diffs
  rescue
    true
  end

  def valid_diffs?
    !broken_diffs?
  end

  def unmerged_diffs
    diffs = Gitlab::Satellite::MergeAction.new(author, self).diffs_between_satellite
    diffs ||= []
    diffs
  end

  def last_commit
    commits.first
  end

  def merge_event
    self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::MERGED).last
  end

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

  def commits
    load_commits(st_commits || [])
  end

  def probably_merged?
    unmerged_commits.empty? &&
        commits.any? && opened?
  end

  def reloaded_commits
    if opened? && unmerged_commits.any?
      self.st_commits = dump_commits(unmerged_commits)
      save

    end
    commits
  end

  def unmerged_commits
    commits = Gitlab::Satellite::MergeAction.new(self.author, self).commits_between
    if commits.present?
      commits = Commit.decorate(commits).
          sort_by(&:created_at).
          reverse
    end
    commits
  end


  def merge!(user_id)
    self.author_id_of_changes = user_id
    self.merge
  end

  def automerge!(current_user)
    if Gitlab::Satellite::MergeAction.new(current_user, self).merge! && self.unmerged_commits.empty?
      self.merge!(current_user.id)
      true
    end
  rescue
    mark_as_unmergeable
    false
  end

  def mr_and_commit_notes
    commit_ids = commits.map(&:id)
    Note.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)
  end

  # Returns the raw diff for this merge request
  #
  # see "git diff"
  def to_diff(current_user)
    Gitlab::Satellite::MergeAction.new(current_user, self).diff_in_satellite
  end


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

  def last_commit_short_sha
    @last_commit_short_sha ||= last_commit.sha[0..10]
  end

  def for_fork?
    target_project != source_project
  end

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

  private

  def dump_commits(commits)
    commits.map(&:to_hash)
  end

  def load_commits(array)
    array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash)) }
  end

  def dump_diffs(diffs)
    if diffs == broken_diffs
      broken_diffs
    elsif diffs.respond_to?(:map)
      diffs.map(&:to_hash)
    end
  end

  def load_diffs(raw)
    if raw == broken_diffs
      broken_diffs
    elsif raw.respond_to?(:map)
      raw.map { |hash| Gitlab::Git::Diff.new(hash) }
    end
  end

  def broken_diffs
    [Gitlab::Git::Diff::BROKEN_DIFF]
  end
end