annotate app/models/changeset.rb @ 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 9c6c72729d91
children de76cd3e8c8e b859cc0c4fa1
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@3 26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{RepositoriesHelper.format_revision(o)}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
Chris@0 27 :description => :long_comments,
Chris@0 28 :datetime => :committed_on,
Chris@3 29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
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@3 50
Chris@3 51 # Returns the identifier of this changeset.
Chris@3 52 # e.g. revision number for centralized system; hash id for DVCS
Chris@3 53 def identifier
Chris@3 54 scmid || revision
Chris@3 55 end
Chris@3 56
Chris@3 57 # Returns the wiki identifier, "rN" or "commit:ABCDEF"
Chris@3 58 def wiki_identifier
Chris@3 59 if scmid # hash-like
Chris@3 60 "commit:#{scmid}"
Chris@3 61 else # numeric
Chris@3 62 "r#{revision}"
Chris@3 63 end
Chris@3 64 end
Chris@3 65 private :wiki_identifier
Chris@0 66
Chris@0 67 def comments=(comment)
Chris@0 68 write_attribute(:comments, Changeset.normalize_comments(comment))
Chris@0 69 end
Chris@0 70
Chris@0 71 def committed_on=(date)
Chris@0 72 self.commit_date = date
Chris@0 73 super
Chris@0 74 end
Chris@0 75
Chris@0 76 def committer=(arg)
Chris@0 77 write_attribute(:committer, self.class.to_utf8(arg.to_s))
Chris@0 78 end
Chris@0 79
Chris@0 80 def project
Chris@0 81 repository.project
Chris@0 82 end
Chris@0 83
Chris@0 84 def author
Chris@0 85 user || committer.to_s.split('<').first
Chris@0 86 end
Chris@0 87
Chris@0 88 def before_create
Chris@0 89 self.user = repository.find_committer_user(committer)
Chris@0 90 end
Chris@0 91
Chris@0 92 def after_create
Chris@0 93 scan_comment_for_issue_ids
Chris@0 94 end
Chris@0 95
Chris@0 96 def scan_comment_for_issue_ids
Chris@0 97 return if comments.blank?
Chris@0 98 # keywords used to reference issues
Chris@0 99 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
Chris@0 100 # keywords used to fix issues
Chris@0 101 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
Chris@0 102
Chris@0 103 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
Chris@0 104 return if kw_regexp.blank?
Chris@0 105
Chris@0 106 referenced_issues = []
Chris@0 107
Chris@0 108 if ref_keywords.delete('*')
Chris@0 109 # find any issue ID in the comments
Chris@0 110 target_issue_ids = []
Chris@0 111 comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
Chris@0 112 referenced_issues += find_referenced_issues_by_id(target_issue_ids)
Chris@0 113 end
Chris@0 114
Chris@0 115 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
Chris@0 116 action = match[0]
Chris@0 117 target_issue_ids = match[1].scan(/\d+/)
Chris@0 118 target_issues = find_referenced_issues_by_id(target_issue_ids)
Chris@0 119 if fix_keywords.include?(action.downcase) && fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
Chris@0 120 # update status of issues
Chris@0 121 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
Chris@0 122 target_issues.each do |issue|
Chris@0 123 # the issue may have been updated by the closure of another one (eg. duplicate)
Chris@0 124 issue.reload
Chris@0 125 # don't change the status is the issue is closed
Chris@0 126 next if issue.status.is_closed?
Chris@3 127 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, wiki_identifier))
Chris@0 128 issue.status = fix_status
Chris@0 129 unless Setting.commit_fix_done_ratio.blank?
Chris@0 130 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
Chris@0 131 end
Chris@0 132 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
Chris@0 133 { :changeset => self, :issue => issue })
Chris@0 134 issue.save
Chris@0 135 end
Chris@0 136 end
Chris@0 137 referenced_issues += target_issues
Chris@0 138 end
Chris@0 139
Chris@0 140 referenced_issues.uniq!
Chris@0 141 self.issues = referenced_issues unless referenced_issues.empty?
Chris@0 142 end
Chris@0 143
Chris@0 144 def short_comments
Chris@0 145 @short_comments || split_comments.first
Chris@0 146 end
Chris@0 147
Chris@0 148 def long_comments
Chris@0 149 @long_comments || split_comments.last
Chris@0 150 end
Chris@0 151
Chris@0 152 # Returns the previous changeset
Chris@0 153 def previous
Chris@0 154 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
Chris@0 155 end
Chris@0 156
Chris@0 157 # Returns the next changeset
Chris@0 158 def next
Chris@0 159 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
Chris@0 160 end
Chris@0 161
Chris@0 162 # Strips and reencodes a commit log before insertion into the database
Chris@0 163 def self.normalize_comments(str)
Chris@0 164 to_utf8(str.to_s.strip)
Chris@0 165 end
Chris@0 166
Chris@0 167 # Creates a new Change from it's common parameters
Chris@0 168 def create_change(change)
Chris@0 169 Change.create(:changeset => self,
Chris@0 170 :action => change[:action],
Chris@0 171 :path => change[:path],
Chris@0 172 :from_path => change[:from_path],
Chris@0 173 :from_revision => change[:from_revision])
Chris@0 174 end
Chris@0 175
Chris@0 176 private
Chris@0 177
Chris@0 178 # Finds issues that can be referenced by the commit message
Chris@0 179 # i.e. issues that belong to the repository project, a subproject or a parent project
Chris@0 180 def find_referenced_issues_by_id(ids)
Chris@0 181 return [] if ids.compact.empty?
Chris@0 182 Issue.find_all_by_id(ids, :include => :project).select {|issue|
Chris@0 183 project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
Chris@0 184 }
Chris@0 185 end
Chris@0 186
Chris@0 187 def split_comments
Chris@0 188 comments =~ /\A(.+?)\r?\n(.*)$/m
Chris@0 189 @short_comments = $1 || comments
Chris@0 190 @long_comments = $2.to_s.strip
Chris@0 191 return @short_comments, @long_comments
Chris@0 192 end
Chris@0 193
Chris@0 194 def self.to_utf8(str)
Chris@0 195 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
Chris@0 196 encoding = Setting.commit_logs_encoding.to_s.strip
Chris@0 197 unless encoding.blank? || encoding == 'UTF-8'
Chris@0 198 begin
Chris@0 199 str = Iconv.conv('UTF-8', encoding, str)
Chris@0 200 rescue Iconv::Failure
Chris@0 201 # do nothing here
Chris@0 202 end
Chris@0 203 end
Chris@0 204 # removes invalid UTF8 sequences
Chris@0 205 begin
Chris@0 206 Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
Chris@0 207 rescue Iconv::InvalidEncoding
Chris@0 208 # "UTF-8//IGNORE" is not supported on some OS
Chris@0 209 str
Chris@0 210 end
Chris@0 211 end
Chris@0 212 end