annotate app/models/.svn/text-base/changeset.rb.svn-base @ 45:65d9e2cabaa3 luisf

Added tipoftheday to the config/settings in order to correct previous issues. Tip of the day is now working correctly. Added the heading strings to the locales files.
author luisf
date Tue, 23 Nov 2010 11:50:01 +0000
parents 1d32c0a0efbf
children af80e5618e9b 8661b858af72
rev   line source
Chris@0 1 # Redmine - project management software
Chris@0 2 # Copyright (C) 2006-2010 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@0 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@0 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 require 'iconv'
Chris@0 19
Chris@0 20 class Changeset < ActiveRecord::Base
Chris@0 21 belongs_to :repository
Chris@0 22 belongs_to :user
Chris@0 23 has_many :changes, :dependent => :delete_all
Chris@0 24 has_and_belongs_to_many :issues
Chris@0 25
Chris@0 26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
Chris@0 27 :description => :long_comments,
Chris@0 28 :datetime => :committed_on,
Chris@0 29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
Chris@0 30
Chris@0 31 acts_as_searchable :columns => 'comments',
Chris@0 32 :include => {:repository => :project},
Chris@0 33 :project_key => "#{Repository.table_name}.project_id",
Chris@0 34 :date_column => 'committed_on'
Chris@0 35
Chris@0 36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
Chris@0 37 :author_key => :user_id,
Chris@0 38 :find_options => {:include => [:user, {:repository => :project}]}
Chris@0 39
Chris@0 40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
Chris@0 41 validates_uniqueness_of :revision, :scope => :repository_id
Chris@0 42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
Chris@0 43
Chris@0 44 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
Chris@0 45 :conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
Chris@0 46
Chris@0 47 def revision=(r)
Chris@0 48 write_attribute :revision, (r.nil? ? nil : r.to_s)
Chris@0 49 end
Chris@0 50
Chris@0 51 def comments=(comment)
Chris@0 52 write_attribute(:comments, Changeset.normalize_comments(comment))
Chris@0 53 end
Chris@0 54
Chris@0 55 def committed_on=(date)
Chris@0 56 self.commit_date = date
Chris@0 57 super
Chris@0 58 end
Chris@0 59
Chris@0 60 def committer=(arg)
Chris@0 61 write_attribute(:committer, self.class.to_utf8(arg.to_s))
Chris@0 62 end
Chris@0 63
Chris@0 64 def project
Chris@0 65 repository.project
Chris@0 66 end
Chris@0 67
Chris@0 68 def author
Chris@0 69 user || committer.to_s.split('<').first
Chris@0 70 end
Chris@0 71
Chris@0 72 def before_create
Chris@0 73 self.user = repository.find_committer_user(committer)
Chris@0 74 end
Chris@0 75
Chris@0 76 def after_create
Chris@0 77 scan_comment_for_issue_ids
Chris@0 78 end
Chris@0 79
Chris@0 80 def scan_comment_for_issue_ids
Chris@0 81 return if comments.blank?
Chris@0 82 # keywords used to reference issues
Chris@0 83 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
Chris@0 84 # keywords used to fix issues
Chris@0 85 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
Chris@0 86
Chris@0 87 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
Chris@0 88 return if kw_regexp.blank?
Chris@0 89
Chris@0 90 referenced_issues = []
Chris@0 91
Chris@0 92 if ref_keywords.delete('*')
Chris@0 93 # find any issue ID in the comments
Chris@0 94 target_issue_ids = []
Chris@0 95 comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
Chris@0 96 referenced_issues += find_referenced_issues_by_id(target_issue_ids)
Chris@0 97 end
Chris@0 98
Chris@0 99 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
Chris@0 100 action = match[0]
Chris@0 101 target_issue_ids = match[1].scan(/\d+/)
Chris@0 102 target_issues = find_referenced_issues_by_id(target_issue_ids)
Chris@0 103 if fix_keywords.include?(action.downcase) && fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
Chris@0 104 # update status of issues
Chris@0 105 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
Chris@0 106 target_issues.each do |issue|
Chris@0 107 # the issue may have been updated by the closure of another one (eg. duplicate)
Chris@0 108 issue.reload
Chris@0 109 # don't change the status is the issue is closed
Chris@0 110 next if issue.status.is_closed?
Chris@0 111 csettext = "r#{self.revision}"
Chris@0 112 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
Chris@0 113 csettext = "commit:\"#{self.scmid}\""
Chris@0 114 end
Chris@0 115 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
Chris@0 116 issue.status = fix_status
Chris@0 117 unless Setting.commit_fix_done_ratio.blank?
Chris@0 118 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
Chris@0 119 end
Chris@0 120 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
Chris@0 121 { :changeset => self, :issue => issue })
Chris@0 122 issue.save
Chris@0 123 end
Chris@0 124 end
Chris@0 125 referenced_issues += target_issues
Chris@0 126 end
Chris@0 127
Chris@0 128 referenced_issues.uniq!
Chris@0 129 self.issues = referenced_issues unless referenced_issues.empty?
Chris@0 130 end
Chris@0 131
Chris@0 132 def short_comments
Chris@0 133 @short_comments || split_comments.first
Chris@0 134 end
Chris@0 135
Chris@0 136 def long_comments
Chris@0 137 @long_comments || split_comments.last
Chris@0 138 end
Chris@0 139
Chris@0 140 # Returns the previous changeset
Chris@0 141 def previous
Chris@0 142 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
Chris@0 143 end
Chris@0 144
Chris@0 145 # Returns the next changeset
Chris@0 146 def next
Chris@0 147 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
Chris@0 148 end
Chris@0 149
Chris@0 150 # Strips and reencodes a commit log before insertion into the database
Chris@0 151 def self.normalize_comments(str)
Chris@0 152 to_utf8(str.to_s.strip)
Chris@0 153 end
Chris@0 154
Chris@0 155 # Creates a new Change from it's common parameters
Chris@0 156 def create_change(change)
Chris@0 157 Change.create(:changeset => self,
Chris@0 158 :action => change[:action],
Chris@0 159 :path => change[:path],
Chris@0 160 :from_path => change[:from_path],
Chris@0 161 :from_revision => change[:from_revision])
Chris@0 162 end
Chris@0 163
Chris@0 164 private
Chris@0 165
Chris@0 166 # Finds issues that can be referenced by the commit message
Chris@0 167 # i.e. issues that belong to the repository project, a subproject or a parent project
Chris@0 168 def find_referenced_issues_by_id(ids)
Chris@0 169 return [] if ids.compact.empty?
Chris@0 170 Issue.find_all_by_id(ids, :include => :project).select {|issue|
Chris@0 171 project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
Chris@0 172 }
Chris@0 173 end
Chris@0 174
Chris@0 175 def split_comments
Chris@0 176 comments =~ /\A(.+?)\r?\n(.*)$/m
Chris@0 177 @short_comments = $1 || comments
Chris@0 178 @long_comments = $2.to_s.strip
Chris@0 179 return @short_comments, @long_comments
Chris@0 180 end
Chris@0 181
Chris@0 182 def self.to_utf8(str)
Chris@0 183 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
Chris@0 184 encoding = Setting.commit_logs_encoding.to_s.strip
Chris@0 185 unless encoding.blank? || encoding == 'UTF-8'
Chris@0 186 begin
Chris@0 187 str = Iconv.conv('UTF-8', encoding, str)
Chris@0 188 rescue Iconv::Failure
Chris@0 189 # do nothing here
Chris@0 190 end
Chris@0 191 end
Chris@0 192 # removes invalid UTF8 sequences
Chris@0 193 begin
Chris@0 194 Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
Chris@0 195 rescue Iconv::InvalidEncoding
Chris@0 196 # "UTF-8//IGNORE" is not supported on some OS
Chris@0 197 str
Chris@0 198 end
Chris@0 199 end
Chris@0 200 end