214 lines
6.2 KiB
Ruby
214 lines
6.2 KiB
Ruby
class PullRequest < ActiveRecord::Base
|
|
STATUSES = %w(ready already blocked merged closed)
|
|
belongs_to :issue, :autosave => true, :dependent => :destroy, :touch => true, :validate => true
|
|
belongs_to :base_project, :class_name => 'Project', :foreign_key => 'base_project_id'
|
|
belongs_to :head_project, :class_name => 'Project', :foreign_key => 'head_project_id'
|
|
delegate :user, :title, :body, :serial_id, :assignee, :status, :to_param,
|
|
:created_at, :updated_at, :comments, :to => :issue, :allow_nil => true
|
|
|
|
validate :uniq_merge
|
|
validates_each :head_ref, :base_ref do |record, attr, value|
|
|
project = attr == :head_ref ? record.head_project : record.base_project
|
|
if !((project.branches + project.tags).map(&:name).include?(value) || project.git_repository.commits.map(&:id).include?(value))
|
|
record.errors.add attr, I18n.t('projects.pull_requests.wrong_ref')
|
|
end
|
|
end
|
|
|
|
before_create :clean_dir
|
|
after_destroy :clean_dir
|
|
|
|
accepts_nested_attributes_for :issue
|
|
attr_accessible :issue_attributes, :base_ref, :head_ref
|
|
|
|
scope :needed_checking, includes(:issue).where(:issues => {:status => ['open', 'blocked', 'ready']})
|
|
|
|
state_machine :status, :initial => :open do
|
|
#after_transition [:ready, :blocked] => [:merged, :closed] do |pull, transition|
|
|
# FileUtils.rm_rf(pull.path) # What about diff?
|
|
#end
|
|
|
|
event :ready do
|
|
transition [:ready, :open, :blocked] => :ready
|
|
end
|
|
|
|
event :block do
|
|
transition [:blocked, :open, :ready] => :blocked
|
|
end
|
|
|
|
event :merging do
|
|
transition :ready => :merged
|
|
end
|
|
|
|
event :close do
|
|
transition [:open, :ready, :blocked] => :closed
|
|
end
|
|
|
|
event :reopen do
|
|
transition :closed => :open
|
|
end
|
|
end
|
|
|
|
def status=(value)
|
|
issue.status = value
|
|
end
|
|
|
|
def can_merge?
|
|
status == 'ready'
|
|
end
|
|
|
|
def check(do_transaction = true)
|
|
new_status = case merge
|
|
when /Already up-to-date/
|
|
'already'
|
|
when /Merge made by the 'recursive' strategy/
|
|
system("cd #{path} && git reset --hard HEAD^") # remove merge commit
|
|
'ready'
|
|
when /Automatic merge failed/
|
|
system("cd #{path} && git reset --hard HEAD") # clean git index
|
|
'block'
|
|
else
|
|
raise ret
|
|
end
|
|
|
|
if do_transaction
|
|
if new_status == 'already'
|
|
ready; merging
|
|
else
|
|
send(new_status)
|
|
end
|
|
else
|
|
self.status = new_status == 'block' ? 'blocked' : new_status
|
|
end
|
|
end
|
|
|
|
def merge!(who)
|
|
return false unless can_merge?
|
|
Dir.chdir(path) do
|
|
system "git config user.name \"#{who.uname}\" && git config user.email \"#{who.email}\""
|
|
if merge
|
|
system("git push origin HEAD")
|
|
system("git reset --hard HEAD^") # for diff maybe FIXME
|
|
set_user_and_time who
|
|
merging
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.default_base_project(project)
|
|
project.is_root? ? project : project.root
|
|
end
|
|
|
|
def path
|
|
filename = [id, base_ref, head_project.owner.uname, head_project.name, head_ref].compact.join('-')
|
|
File.join(APP_CONFIG['root_path'], 'pull_requests', base_project.owner.uname, base_project.name, filename)
|
|
end
|
|
|
|
def head_branch
|
|
if base_project != head_project
|
|
"head_#{head_ref}"
|
|
else
|
|
head_ref
|
|
end
|
|
end
|
|
|
|
def common_ancestor
|
|
return @common_ancestor if @common_ancestor
|
|
repo = Grit::Repo.new(path)
|
|
base_commit = repo.commits(base_ref).first
|
|
head_commit = repo.commits(head_branch).first
|
|
@common_ancestor = repo.commit(repo.git.merge_base({}, base_commit, head_commit))
|
|
end
|
|
|
|
def diff_stats(repo, a,b)
|
|
stats = []
|
|
Dir.chdir(path) do
|
|
lines = repo.git.native(:diff, {:numstat => true, :M => true}, "#{a.id}...#{b.id}").split("\n")
|
|
while !lines.empty?
|
|
files = []
|
|
while lines.first =~ /^([-\d]+)\s+([-\d]+)\s+(.+)/
|
|
additions, deletions, filename = lines.shift.gsub(' => ', '=>').split
|
|
additions, deletions = additions.to_i, deletions.to_i
|
|
stat = Grit::DiffStat.new filename, additions, deletions
|
|
stats << stat
|
|
end
|
|
end
|
|
stats
|
|
end
|
|
end
|
|
|
|
# FIXME копипизд from grit (maybe move to warpc/gri?)
|
|
def diff(repo, a, b)
|
|
diff = repo.git.native('diff', {:M => true}, "#{a}...#{b}")
|
|
|
|
if diff =~ /diff --git a/
|
|
diff = diff.sub(/.*?(diff --git a)/m, '\1')
|
|
else
|
|
diff = ''
|
|
end
|
|
Grit::Diff.list_from_string(repo, diff)
|
|
end
|
|
|
|
protected
|
|
|
|
def merge
|
|
clone
|
|
%x(cd #{path} && git checkout #{base_ref} && git merge --no-ff #{head_branch}) #FIXME need sanitize branch name!
|
|
end
|
|
|
|
def clone
|
|
git = Grit::Git.new(path)
|
|
|
|
unless git.exist?
|
|
FileUtils.mkdir_p(path)
|
|
system("git clone --local --no-hardlinks #{base_project.path} #{path}")
|
|
if base_project != head_project
|
|
Dir.chdir(path) do
|
|
system 'git', 'remote', 'add', 'head', head_project.path
|
|
end
|
|
end
|
|
end
|
|
|
|
clean
|
|
Dir.chdir(path) do
|
|
system 'git', 'checkout', base_ref
|
|
system 'git', 'pull', 'origin', base_ref
|
|
if base_project == head_project
|
|
system 'git', 'checkout', head_ref
|
|
system 'git', 'pull', 'origin', head_ref
|
|
else
|
|
system 'git', 'fetch', 'head', "+#{head_ref}:#{head_branch}"
|
|
end
|
|
end
|
|
# TODO catch errors
|
|
end
|
|
|
|
def clean
|
|
Dir.chdir(path) do
|
|
base_project.branches.each {|branch| system 'git', 'checkout', branch.name}
|
|
system 'git', 'checkout', base_ref
|
|
|
|
base_project.branches.each do |branch|
|
|
system 'git', 'branch', '-D', branch.name unless [base_ref, head_branch].include? branch.name
|
|
end
|
|
base_project.tags.each do |tag|
|
|
system 'git', 'tag', '-d', tag.name unless [base_ref, head_branch].include? tag.name
|
|
end
|
|
end
|
|
end
|
|
|
|
def uniq_merge
|
|
if base_project.pull_requests.needed_checking.where(:head_project_id => head_project, :base_ref => base_ref, :head_ref => head_ref).where('pull_requests.id <> :id or :id is null', :id => id).count > 0
|
|
errors.add(:base_branch, I18n.t('projects.pull_requests.duplicate', :head_ref => head_ref))
|
|
end
|
|
end
|
|
|
|
def clean_dir
|
|
FileUtils.rm_rf path
|
|
end
|
|
|
|
def set_user_and_time user
|
|
issue.closed_at = Time.now.utc
|
|
issue.closer = user
|
|
end
|
|
end
|