To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / app / models / changeset.rb @ 441:cbce1fd3b1b7
History | View | Annotate | Download (9.32 KB)
| 1 | 0:513646585e45 | Chris | # Redmine - project management software
|
|---|---|---|---|
| 2 | 441:cbce1fd3b1b7 | Chris | # Copyright (C) 2006-2011 Jean-Philippe Lang
|
| 3 | 0:513646585e45 | Chris | #
|
| 4 | # This program is free software; you can redistribute it and/or
|
||
| 5 | # modify it under the terms of the GNU General Public License
|
||
| 6 | # as published by the Free Software Foundation; either version 2
|
||
| 7 | # of the License, or (at your option) any later version.
|
||
| 8 | 441:cbce1fd3b1b7 | Chris | #
|
| 9 | 0:513646585e45 | Chris | # This program is distributed in the hope that it will be useful,
|
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
| 12 | # GNU General Public License for more details.
|
||
| 13 | 441:cbce1fd3b1b7 | Chris | #
|
| 14 | 0:513646585e45 | Chris | # You should have received a copy of the GNU General Public License
|
| 15 | # along with this program; if not, write to the Free Software
|
||
| 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
| 17 | |||
| 18 | require 'iconv'
|
||
| 19 | |||
| 20 | class Changeset < ActiveRecord::Base |
||
| 21 | belongs_to :repository
|
||
| 22 | belongs_to :user
|
||
| 23 | has_many :changes, :dependent => :delete_all |
||
| 24 | has_and_belongs_to_many :issues
|
||
| 25 | |||
| 26 | 119:8661b858af72 | Chris | acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, |
| 27 | 0:513646585e45 | Chris | :description => :long_comments, |
| 28 | :datetime => :committed_on, |
||
| 29 | 119:8661b858af72 | Chris | :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}} |
| 30 | 441:cbce1fd3b1b7 | Chris | |
| 31 | 0:513646585e45 | Chris | acts_as_searchable :columns => 'comments', |
| 32 | :include => {:repository => :project}, |
||
| 33 | :project_key => "#{Repository.table_name}.project_id", |
||
| 34 | :date_column => 'committed_on' |
||
| 35 | 441:cbce1fd3b1b7 | Chris | |
| 36 | 0:513646585e45 | Chris | acts_as_activity_provider :timestamp => "#{table_name}.committed_on", |
| 37 | :author_key => :user_id, |
||
| 38 | :find_options => {:include => [:user, {:repository => :project}]} |
||
| 39 | 441:cbce1fd3b1b7 | Chris | |
| 40 | 0:513646585e45 | Chris | validates_presence_of :repository_id, :revision, :committed_on, :commit_date |
| 41 | validates_uniqueness_of :revision, :scope => :repository_id |
||
| 42 | validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true |
||
| 43 | 441:cbce1fd3b1b7 | Chris | |
| 44 | 0:513646585e45 | Chris | named_scope :visible, lambda {|*args| { :include => {:repository => :project}, |
| 45 | 441:cbce1fd3b1b7 | Chris | :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } } |
| 46 | |||
| 47 | 0:513646585e45 | Chris | def revision=(r) |
| 48 | write_attribute :revision, (r.nil? ? nil : r.to_s) |
||
| 49 | end
|
||
| 50 | 119:8661b858af72 | Chris | |
| 51 | # Returns the identifier of this changeset; depending on repository backends
|
||
| 52 | def identifier |
||
| 53 | if repository.class.respond_to? :changeset_identifier |
||
| 54 | repository.class.changeset_identifier self
|
||
| 55 | else
|
||
| 56 | revision.to_s |
||
| 57 | end
|
||
| 58 | end
|
||
| 59 | 0:513646585e45 | Chris | |
| 60 | def committed_on=(date) |
||
| 61 | self.commit_date = date
|
||
| 62 | super
|
||
| 63 | end
|
||
| 64 | 119:8661b858af72 | Chris | |
| 65 | # Returns the readable identifier
|
||
| 66 | def format_identifier |
||
| 67 | if repository.class.respond_to? :format_changeset_identifier |
||
| 68 | repository.class.format_changeset_identifier self
|
||
| 69 | else
|
||
| 70 | identifier |
||
| 71 | end
|
||
| 72 | end
|
||
| 73 | 441:cbce1fd3b1b7 | Chris | |
| 74 | 0:513646585e45 | Chris | def project |
| 75 | repository.project |
||
| 76 | end
|
||
| 77 | 441:cbce1fd3b1b7 | Chris | |
| 78 | 0:513646585e45 | Chris | def author |
| 79 | user || committer.to_s.split('<').first
|
||
| 80 | end
|
||
| 81 | 441:cbce1fd3b1b7 | Chris | |
| 82 | 0:513646585e45 | Chris | def before_create |
| 83 | 245:051f544170fe | Chris | self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding) |
| 84 | 441:cbce1fd3b1b7 | Chris | self.comments = self.class.normalize_comments( |
| 85 | self.comments, repository.repo_log_encoding)
|
||
| 86 | 245:051f544170fe | Chris | self.user = repository.find_committer_user(self.committer) |
| 87 | 0:513646585e45 | Chris | end
|
| 88 | 245:051f544170fe | Chris | |
| 89 | 0:513646585e45 | Chris | def after_create |
| 90 | scan_comment_for_issue_ids |
||
| 91 | end
|
||
| 92 | 441:cbce1fd3b1b7 | Chris | |
| 93 | 119:8661b858af72 | Chris | TIMELOG_RE = / |
| 94 | (
|
||
| 95 | 245:051f544170fe | Chris | ((\d+)(h|hours?))((\d+)(m|min)?)?
|
| 96 | |
|
||
| 97 | ((\d+)(h|hours?|m|min))
|
||
| 98 | 119:8661b858af72 | Chris | |
|
| 99 | (\d+):(\d+)
|
||
| 100 | |
|
||
| 101 | 245:051f544170fe | Chris | (\d+([\.,]\d+)?)h?
|
| 102 | 119:8661b858af72 | Chris | )
|
| 103 | /x
|
||
| 104 | 441:cbce1fd3b1b7 | Chris | |
| 105 | 0:513646585e45 | Chris | def scan_comment_for_issue_ids |
| 106 | return if comments.blank? |
||
| 107 | # keywords used to reference issues
|
||
| 108 | ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) |
||
| 109 | 119:8661b858af72 | Chris | ref_keywords_any = ref_keywords.delete('*')
|
| 110 | 0:513646585e45 | Chris | # keywords used to fix issues
|
| 111 | fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip) |
||
| 112 | 441:cbce1fd3b1b7 | Chris | |
| 113 | 0:513646585e45 | Chris | kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
|
| 114 | 441:cbce1fd3b1b7 | Chris | |
| 115 | 0:513646585e45 | Chris | referenced_issues = [] |
| 116 | 441:cbce1fd3b1b7 | Chris | |
| 117 | 119:8661b858af72 | Chris | comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match| |
| 118 | action, refs = match[2], match[3] |
||
| 119 | next unless action.present? || ref_keywords_any |
||
| 120 | 441:cbce1fd3b1b7 | Chris | |
| 121 | 119:8661b858af72 | Chris | refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m| |
| 122 | issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2] |
||
| 123 | if issue
|
||
| 124 | referenced_issues << issue |
||
| 125 | fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
|
||
| 126 | log_time(issue, hours) if hours && Setting.commit_logtime_enabled? |
||
| 127 | 0:513646585e45 | Chris | end
|
| 128 | end
|
||
| 129 | end
|
||
| 130 | 441:cbce1fd3b1b7 | Chris | |
| 131 | 0:513646585e45 | Chris | referenced_issues.uniq! |
| 132 | self.issues = referenced_issues unless referenced_issues.empty? |
||
| 133 | end
|
||
| 134 | 441:cbce1fd3b1b7 | Chris | |
| 135 | 0:513646585e45 | Chris | def short_comments |
| 136 | @short_comments || split_comments.first
|
||
| 137 | end
|
||
| 138 | 441:cbce1fd3b1b7 | Chris | |
| 139 | 0:513646585e45 | Chris | def long_comments |
| 140 | @long_comments || split_comments.last
|
||
| 141 | end
|
||
| 142 | 119:8661b858af72 | Chris | |
| 143 | def text_tag |
||
| 144 | if scmid?
|
||
| 145 | "commit:#{scmid}"
|
||
| 146 | else
|
||
| 147 | "r#{revision}"
|
||
| 148 | end
|
||
| 149 | end
|
||
| 150 | 441:cbce1fd3b1b7 | Chris | |
| 151 | 0:513646585e45 | Chris | # Returns the previous changeset
|
| 152 | def previous |
||
| 153 | 441:cbce1fd3b1b7 | Chris | @previous ||= Changeset.find(:first, |
| 154 | :conditions => ['id < ? AND repository_id = ?', |
||
| 155 | self.id, self.repository_id], |
||
| 156 | :order => 'id DESC') |
||
| 157 | 0:513646585e45 | Chris | end
|
| 158 | |||
| 159 | # Returns the next changeset
|
||
| 160 | def next |
||
| 161 | 441:cbce1fd3b1b7 | Chris | @next ||= Changeset.find(:first, |
| 162 | :conditions => ['id > ? AND repository_id = ?', |
||
| 163 | self.id, self.repository_id], |
||
| 164 | :order => 'id ASC') |
||
| 165 | 0:513646585e45 | Chris | end
|
| 166 | 441:cbce1fd3b1b7 | Chris | |
| 167 | 0:513646585e45 | Chris | # Creates a new Change from it's common parameters
|
| 168 | def create_change(change) |
||
| 169 | 441:cbce1fd3b1b7 | Chris | Change.create(:changeset => self, |
| 170 | :action => change[:action], |
||
| 171 | :path => change[:path], |
||
| 172 | :from_path => change[:from_path], |
||
| 173 | 0:513646585e45 | Chris | :from_revision => change[:from_revision]) |
| 174 | end
|
||
| 175 | 245:051f544170fe | Chris | |
| 176 | 0:513646585e45 | Chris | private |
| 177 | |||
| 178 | 119:8661b858af72 | Chris | # Finds an issue that can be referenced by the commit message
|
| 179 | # i.e. an issue that belong to the repository project, a subproject or a parent project
|
||
| 180 | def find_referenced_issue_by_id(id) |
||
| 181 | return nil if id.blank? |
||
| 182 | issue = Issue.find_by_id(id.to_i, :include => :project) |
||
| 183 | if issue
|
||
| 184 | 441:cbce1fd3b1b7 | Chris | unless issue.project &&
|
| 185 | (project == issue.project || project.is_ancestor_of?(issue.project) || |
||
| 186 | project.is_descendant_of?(issue.project)) |
||
| 187 | 119:8661b858af72 | Chris | issue = nil
|
| 188 | end
|
||
| 189 | end
|
||
| 190 | issue |
||
| 191 | end
|
||
| 192 | 441:cbce1fd3b1b7 | Chris | |
| 193 | 119:8661b858af72 | Chris | def fix_issue(issue) |
| 194 | status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i) |
||
| 195 | if status.nil?
|
||
| 196 | logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger |
||
| 197 | return issue
|
||
| 198 | end
|
||
| 199 | 441:cbce1fd3b1b7 | Chris | |
| 200 | 119:8661b858af72 | Chris | # the issue may have been updated by the closure of another one (eg. duplicate)
|
| 201 | issue.reload |
||
| 202 | # don't change the status is the issue is closed
|
||
| 203 | return if issue.status && issue.status.is_closed? |
||
| 204 | 441:cbce1fd3b1b7 | Chris | |
| 205 | 119:8661b858af72 | Chris | journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag)) |
| 206 | issue.status = status |
||
| 207 | unless Setting.commit_fix_done_ratio.blank? |
||
| 208 | issue.done_ratio = Setting.commit_fix_done_ratio.to_i
|
||
| 209 | end
|
||
| 210 | Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update, |
||
| 211 | { :changeset => self, :issue => issue })
|
||
| 212 | unless issue.save
|
||
| 213 | logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger |
||
| 214 | end
|
||
| 215 | issue |
||
| 216 | end
|
||
| 217 | 441:cbce1fd3b1b7 | Chris | |
| 218 | 119:8661b858af72 | Chris | def log_time(issue, hours) |
| 219 | time_entry = TimeEntry.new(
|
||
| 220 | :user => user,
|
||
| 221 | :hours => hours,
|
||
| 222 | :issue => issue,
|
||
| 223 | :spent_on => commit_date,
|
||
| 224 | 441:cbce1fd3b1b7 | Chris | :comments => l(:text_time_logged_by_changeset, :value => text_tag, |
| 225 | :locale => Setting.default_language) |
||
| 226 | 119:8661b858af72 | Chris | ) |
| 227 | time_entry.activity = log_time_activity unless log_time_activity.nil?
|
||
| 228 | 441:cbce1fd3b1b7 | Chris | |
| 229 | 119:8661b858af72 | Chris | unless time_entry.save
|
| 230 | logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger |
||
| 231 | end
|
||
| 232 | time_entry |
||
| 233 | end
|
||
| 234 | 441:cbce1fd3b1b7 | Chris | |
| 235 | 119:8661b858af72 | Chris | def log_time_activity |
| 236 | if Setting.commit_logtime_activity_id.to_i > 0 |
||
| 237 | TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i) |
||
| 238 | end
|
||
| 239 | 0:513646585e45 | Chris | end
|
| 240 | 441:cbce1fd3b1b7 | Chris | |
| 241 | 0:513646585e45 | Chris | def split_comments |
| 242 | comments =~ /\A(.+?)\r?\n(.*)$/m
|
||
| 243 | @short_comments = $1 || comments |
||
| 244 | @long_comments = $2.to_s.strip |
||
| 245 | return @short_comments, @long_comments |
||
| 246 | end
|
||
| 247 | |||
| 248 | 245:051f544170fe | Chris | public |
| 249 | 119:8661b858af72 | Chris | |
| 250 | 245:051f544170fe | Chris | # Strips and reencodes a commit log before insertion into the database
|
| 251 | def self.normalize_comments(str, encoding) |
||
| 252 | Changeset.to_utf8(str.to_s.strip, encoding)
|
||
| 253 | end
|
||
| 254 | |||
| 255 | def self.to_utf8(str, encoding) |
||
| 256 | 441:cbce1fd3b1b7 | Chris | return str if str.nil? |
| 257 | str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding) |
||
| 258 | if str.empty?
|
||
| 259 | str.force_encoding("UTF-8") if str.respond_to?(:force_encoding) |
||
| 260 | return str
|
||
| 261 | 0:513646585e45 | Chris | end
|
| 262 | 441:cbce1fd3b1b7 | Chris | enc = encoding.blank? ? "UTF-8" : encoding
|
| 263 | 245:051f544170fe | Chris | if str.respond_to?(:force_encoding) |
| 264 | 441:cbce1fd3b1b7 | Chris | if enc.upcase != "UTF-8" |
| 265 | str.force_encoding(enc) |
||
| 266 | str = str.encode("UTF-8", :invalid => :replace, |
||
| 267 | :undef => :replace, :replace => '?') |
||
| 268 | else
|
||
| 269 | str.force_encoding("UTF-8")
|
||
| 270 | if ! str.valid_encoding?
|
||
| 271 | str = str.encode("US-ASCII", :invalid => :replace, |
||
| 272 | :undef => :replace, :replace => '?').encode("UTF-8") |
||
| 273 | end
|
||
| 274 | 245:051f544170fe | Chris | end
|
| 275 | else
|
||
| 276 | 441:cbce1fd3b1b7 | Chris | ic = Iconv.new('UTF-8', enc) |
| 277 | txtar = ""
|
||
| 278 | 245:051f544170fe | Chris | begin
|
| 279 | 441:cbce1fd3b1b7 | Chris | txtar += ic.iconv(str) |
| 280 | rescue Iconv::IllegalSequence |
||
| 281 | txtar += $!.success
|
||
| 282 | str = '?' + $!.failed[1,$!.failed.length] |
||
| 283 | retry
|
||
| 284 | rescue
|
||
| 285 | txtar += $!.success
|
||
| 286 | 245:051f544170fe | Chris | end
|
| 287 | 441:cbce1fd3b1b7 | Chris | str = txtar |
| 288 | 0:513646585e45 | Chris | end
|
| 289 | 245:051f544170fe | Chris | str |
| 290 | 0:513646585e45 | Chris | end
|
| 291 | end |