changeset.rb
247 lines
| 8.2 KiB
| text/x-ruby
|
RubyLexer
|
r2004 | # Redmine - project management software | ||
|
r3352 | # Copyright (C) 2006-2010 Jean-Philippe Lang | ||
|
r374 | # | ||
# This program is free software; you can redistribute it and/or | ||||
# modify it under the terms of the GNU General Public License | ||||
# as published by the Free Software Foundation; either version 2 | ||||
# of the License, or (at your option) any later version. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU General Public License | ||||
# along with this program; if not, write to the Free Software | ||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
|
r1766 | require 'iconv' | ||
|
r374 | class Changeset < ActiveRecord::Base | ||
belongs_to :repository | ||||
|
r2004 | belongs_to :user | ||
|
r374 | has_many :changes, :dependent => :delete_all | ||
|
r470 | has_and_belongs_to_many :issues | ||
|
r663 | |||
|
r2344 | acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, | ||
:description => :long_comments, | ||||
|
r663 | :datetime => :committed_on, | ||
|
r2571 | :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}} | ||
|
r755 | |||
acts_as_searchable :columns => 'comments', | ||||
|
r1420 | :include => {:repository => :project}, | ||
|
r755 | :project_key => "#{Repository.table_name}.project_id", | ||
:date_column => 'committed_on' | ||||
|
r1692 | |||
acts_as_activity_provider :timestamp => "#{table_name}.committed_on", | ||||
|
r2064 | :author_key => :user_id, | ||
|
r2571 | :find_options => {:include => [:user, {:repository => :project}]} | ||
|
r374 | |||
|
r377 | validates_presence_of :repository_id, :revision, :committed_on, :commit_date | ||
|
r374 | validates_uniqueness_of :revision, :scope => :repository_id | ||
|
r570 | validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true | ||
|
r377 | |||
|
r3243 | named_scope :visible, lambda {|*args| { :include => {:repository => :project}, | ||
:conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } } | ||||
|
r1348 | def revision=(r) | ||
write_attribute :revision, (r.nil? ? nil : r.to_s) | ||||
end | ||||
|
r651 | def comments=(comment) | ||
|
r1767 | write_attribute(:comments, Changeset.normalize_comments(comment)) | ||
|
r651 | end | ||
|
r377 | def committed_on=(date) | ||
self.commit_date = date | ||||
super | ||||
end | ||||
|
r470 | |||
|
r3352 | def committer=(arg) | ||
write_attribute(:committer, self.class.to_utf8(arg.to_s)) | ||||
end | ||||
|
r1213 | def project | ||
repository.project | ||||
end | ||||
|
r2004 | def author | ||
user || committer.to_s.split('<').first | ||||
end | ||||
def before_create | ||||
self.user = repository.find_committer_user(committer) | ||||
end | ||||
|
r470 | def after_create | ||
scan_comment_for_issue_ids | ||||
end | ||||
|
r4356 | TIMELOG_RE = / | ||
( | ||||
(\d+([.,]\d+)?)h? | ||||
| | ||||
(\d+):(\d+) | ||||
| | ||||
((\d+)(h|hours?))?((\d+)(m|min)?)? | ||||
) | ||||
/x | ||||
|
r470 | def scan_comment_for_issue_ids | ||
|
r476 | return if comments.blank? | ||
|
r470 | # keywords used to reference issues | ||
|
r848 | ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) | ||
|
r4356 | ref_keywords_any = ref_keywords.delete('*') | ||
|
r470 | # keywords used to fix issues | ||
|
r848 | fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip) | ||
|
r470 | |||
|
r848 | kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|") | ||
|
r470 | |||
|
r848 | referenced_issues = [] | ||
|
r905 | |||
|
r4356 | comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match| | ||
action, refs = match[2], match[3] | ||||
next unless action.present? || ref_keywords_any | ||||
refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m| | ||||
issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2] | ||||
if issue | ||||
referenced_issues << issue | ||||
fix_issue(issue) if fix_keywords.include?(action.to_s.downcase) | ||||
log_time(issue, hours) if hours && Setting.commit_logtime_enabled? | ||||
|
r470 | end | ||
end | ||||
end | ||||
|
r905 | |||
|
r3359 | referenced_issues.uniq! | ||
self.issues = referenced_issues unless referenced_issues.empty? | ||||
|
r470 | end | ||
|
r1112 | |||
|
r2344 | def short_comments | ||
@short_comments || split_comments.first | ||||
end | ||||
def long_comments | ||||
@long_comments || split_comments.last | ||||
end | ||||
|
r4356 | |||
def text_tag | ||||
|
r4376 | if scmid? | ||
"commit:#{scmid}" | ||||
|
r4356 | else | ||
|
r4376 | "r#{revision}" | ||
|
r4356 | end | ||
end | ||||
|
r2344 | |||
|
r925 | # Returns the previous changeset | ||
def previous | ||||
|
r1222 | @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC') | ||
|
r925 | end | ||
# Returns the next changeset | ||||
def next | ||||
|
r1222 | @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC') | ||
|
r925 | end | ||
|
r1766 | |||
|
r1767 | # Strips and reencodes a commit log before insertion into the database | ||
def self.normalize_comments(str) | ||||
to_utf8(str.to_s.strip) | ||||
end | ||||
|
r3246 | |||
# Creates a new Change from it's common parameters | ||||
def create_change(change) | ||||
Change.create(:changeset => self, | ||||
:action => change[:action], | ||||
:path => change[:path], | ||||
:from_path => change[:from_path], | ||||
:from_revision => change[:from_revision]) | ||||
end | ||||
|
r1767 | |||
|
r1766 | private | ||
|
r2004 | |||
|
r4356 | # Finds an issue that can be referenced by the commit message | ||
# i.e. an issue that belong to the repository project, a subproject or a parent project | ||||
def find_referenced_issue_by_id(id) | ||||
return nil if id.blank? | ||||
issue = Issue.find_by_id(id.to_i, :include => :project) | ||||
if issue | ||||
unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project) | ||||
issue = nil | ||||
end | ||||
end | ||||
issue | ||||
end | ||||
def fix_issue(issue) | ||||
status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i) | ||||
if status.nil? | ||||
logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger | ||||
return issue | ||||
end | ||||
# the issue may have been updated by the closure of another one (eg. duplicate) | ||||
issue.reload | ||||
# don't change the status is the issue is closed | ||||
return if issue.status && issue.status.is_closed? | ||||
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag)) | ||||
issue.status = status | ||||
unless Setting.commit_fix_done_ratio.blank? | ||||
issue.done_ratio = Setting.commit_fix_done_ratio.to_i | ||||
end | ||||
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update, | ||||
{ :changeset => self, :issue => issue }) | ||||
unless issue.save | ||||
logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger | ||||
end | ||||
issue | ||||
end | ||||
def log_time(issue, hours) | ||||
time_entry = TimeEntry.new( | ||||
:user => user, | ||||
:hours => hours, | ||||
:issue => issue, | ||||
:spent_on => commit_date, | ||||
:comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language) | ||||
) | ||||
time_entry.activity = log_time_activity unless log_time_activity.nil? | ||||
unless time_entry.save | ||||
logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger | ||||
end | ||||
time_entry | ||||
end | ||||
def log_time_activity | ||||
if Setting.commit_logtime_activity_id.to_i > 0 | ||||
TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i) | ||||
end | ||||
|
r3243 | end | ||
|
r2344 | def split_comments | ||
comments =~ /\A(.+?)\r?\n(.*)$/m | ||||
@short_comments = $1 || comments | ||||
@long_comments = $2.to_s.strip | ||||
return @short_comments, @long_comments | ||||
end | ||||
|
r2004 | |||
|
r1767 | def self.to_utf8(str) | ||
|
r1766 | return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii | ||
encoding = Setting.commit_logs_encoding.to_s.strip | ||||
unless encoding.blank? || encoding == 'UTF-8' | ||||
begin | ||||
|
r3352 | str = Iconv.conv('UTF-8', encoding, str) | ||
|
r1766 | rescue Iconv::Failure | ||
# do nothing here | ||||
end | ||||
end | ||||
|
r3352 | # removes invalid UTF8 sequences | ||
|
r3521 | begin | ||
Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3] | ||||
rescue Iconv::InvalidEncoding | ||||
# "UTF-8//IGNORE" is not supported on some OS | ||||
str | ||||
end | ||||
|
r1766 | end | ||
|
r374 | end | ||