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 @ 912:5e80956cc792
History | View | Annotate | Download (9.01 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 | 909:cbb26bc654de | Chris | has_and_belongs_to_many :parents,
|
| 26 | :class_name => "Changeset", |
||
| 27 | :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}", |
||
| 28 | :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id' |
||
| 29 | has_and_belongs_to_many :children,
|
||
| 30 | :class_name => "Changeset", |
||
| 31 | :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}", |
||
| 32 | :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id' |
||
| 33 | 0:513646585e45 | Chris | |
| 34 | 119:8661b858af72 | Chris | acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, |
| 35 | 0:513646585e45 | Chris | :description => :long_comments, |
| 36 | :datetime => :committed_on, |
||
| 37 | 119:8661b858af72 | Chris | :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}} |
| 38 | 441:cbce1fd3b1b7 | Chris | |
| 39 | 0:513646585e45 | Chris | acts_as_searchable :columns => 'comments', |
| 40 | :include => {:repository => :project}, |
||
| 41 | :project_key => "#{Repository.table_name}.project_id", |
||
| 42 | :date_column => 'committed_on' |
||
| 43 | 441:cbce1fd3b1b7 | Chris | |
| 44 | 0:513646585e45 | Chris | acts_as_activity_provider :timestamp => "#{table_name}.committed_on", |
| 45 | :author_key => :user_id, |
||
| 46 | :find_options => {:include => [:user, {:repository => :project}]} |
||
| 47 | 441:cbce1fd3b1b7 | Chris | |
| 48 | 0:513646585e45 | Chris | validates_presence_of :repository_id, :revision, :committed_on, :commit_date |
| 49 | validates_uniqueness_of :revision, :scope => :repository_id |
||
| 50 | validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true |
||
| 51 | 441:cbce1fd3b1b7 | Chris | |
| 52 | 0:513646585e45 | Chris | named_scope :visible, lambda {|*args| { :include => {:repository => :project}, |
| 53 | 441:cbce1fd3b1b7 | Chris | :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } } |
| 54 | |||
| 55 | 909:cbb26bc654de | Chris | after_create :scan_for_issues
|
| 56 | before_create :before_create_cs
|
||
| 57 | |||
| 58 | 0:513646585e45 | Chris | def revision=(r) |
| 59 | write_attribute :revision, (r.nil? ? nil : r.to_s) |
||
| 60 | end
|
||
| 61 | 119:8661b858af72 | Chris | |
| 62 | # Returns the identifier of this changeset; depending on repository backends
|
||
| 63 | def identifier |
||
| 64 | if repository.class.respond_to? :changeset_identifier |
||
| 65 | repository.class.changeset_identifier self
|
||
| 66 | else
|
||
| 67 | revision.to_s |
||
| 68 | end
|
||
| 69 | end
|
||
| 70 | 0:513646585e45 | Chris | |
| 71 | def committed_on=(date) |
||
| 72 | self.commit_date = date
|
||
| 73 | super
|
||
| 74 | end
|
||
| 75 | 119:8661b858af72 | Chris | |
| 76 | # Returns the readable identifier
|
||
| 77 | def format_identifier |
||
| 78 | if repository.class.respond_to? :format_changeset_identifier |
||
| 79 | repository.class.format_changeset_identifier self
|
||
| 80 | else
|
||
| 81 | identifier |
||
| 82 | end
|
||
| 83 | end
|
||
| 84 | 441:cbce1fd3b1b7 | Chris | |
| 85 | 0:513646585e45 | Chris | def project |
| 86 | repository.project |
||
| 87 | end
|
||
| 88 | 441:cbce1fd3b1b7 | Chris | |
| 89 | 0:513646585e45 | Chris | def author |
| 90 | user || committer.to_s.split('<').first
|
||
| 91 | end
|
||
| 92 | 441:cbce1fd3b1b7 | Chris | |
| 93 | 909:cbb26bc654de | Chris | def before_create_cs |
| 94 | 245:051f544170fe | Chris | self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding) |
| 95 | 441:cbce1fd3b1b7 | Chris | self.comments = self.class.normalize_comments( |
| 96 | self.comments, repository.repo_log_encoding)
|
||
| 97 | 245:051f544170fe | Chris | self.user = repository.find_committer_user(self.committer) |
| 98 | 0:513646585e45 | Chris | end
|
| 99 | 245:051f544170fe | Chris | |
| 100 | 909:cbb26bc654de | Chris | def scan_for_issues |
| 101 | 0:513646585e45 | Chris | scan_comment_for_issue_ids |
| 102 | end
|
||
| 103 | 441:cbce1fd3b1b7 | Chris | |
| 104 | 119:8661b858af72 | Chris | TIMELOG_RE = / |
| 105 | (
|
||
| 106 | 245:051f544170fe | Chris | ((\d+)(h|hours?))((\d+)(m|min)?)?
|
| 107 | |
|
||
| 108 | ((\d+)(h|hours?|m|min))
|
||
| 109 | 119:8661b858af72 | Chris | |
|
| 110 | (\d+):(\d+)
|
||
| 111 | |
|
||
| 112 | 245:051f544170fe | Chris | (\d+([\.,]\d+)?)h?
|
| 113 | 119:8661b858af72 | Chris | )
|
| 114 | /x
|
||
| 115 | 441:cbce1fd3b1b7 | Chris | |
| 116 | 0:513646585e45 | Chris | def scan_comment_for_issue_ids |
| 117 | return if comments.blank? |
||
| 118 | # keywords used to reference issues
|
||
| 119 | ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) |
||
| 120 | 119:8661b858af72 | Chris | ref_keywords_any = ref_keywords.delete('*')
|
| 121 | 0:513646585e45 | Chris | # keywords used to fix issues
|
| 122 | fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip) |
||
| 123 | 441:cbce1fd3b1b7 | Chris | |
| 124 | 0:513646585e45 | Chris | kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
|
| 125 | 441:cbce1fd3b1b7 | Chris | |
| 126 | 0:513646585e45 | Chris | referenced_issues = [] |
| 127 | 441:cbce1fd3b1b7 | Chris | |
| 128 | 119:8661b858af72 | Chris | comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match| |
| 129 | action, refs = match[2], match[3] |
||
| 130 | next unless action.present? || ref_keywords_any |
||
| 131 | 441:cbce1fd3b1b7 | Chris | |
| 132 | 119:8661b858af72 | Chris | refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m| |
| 133 | issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2] |
||
| 134 | if issue
|
||
| 135 | referenced_issues << issue |
||
| 136 | fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
|
||
| 137 | log_time(issue, hours) if hours && Setting.commit_logtime_enabled? |
||
| 138 | 0:513646585e45 | Chris | end
|
| 139 | end
|
||
| 140 | end
|
||
| 141 | 441:cbce1fd3b1b7 | Chris | |
| 142 | 0:513646585e45 | Chris | referenced_issues.uniq! |
| 143 | self.issues = referenced_issues unless referenced_issues.empty? |
||
| 144 | end
|
||
| 145 | 441:cbce1fd3b1b7 | Chris | |
| 146 | 0:513646585e45 | Chris | def short_comments |
| 147 | @short_comments || split_comments.first
|
||
| 148 | end
|
||
| 149 | 441:cbce1fd3b1b7 | Chris | |
| 150 | 0:513646585e45 | Chris | def long_comments |
| 151 | @long_comments || split_comments.last
|
||
| 152 | end
|
||
| 153 | 119:8661b858af72 | Chris | |
| 154 | def text_tag |
||
| 155 | if scmid?
|
||
| 156 | "commit:#{scmid}"
|
||
| 157 | else
|
||
| 158 | "r#{revision}"
|
||
| 159 | end
|
||
| 160 | end
|
||
| 161 | 441:cbce1fd3b1b7 | Chris | |
| 162 | 0:513646585e45 | Chris | # Returns the previous changeset
|
| 163 | def previous |
||
| 164 | 441:cbce1fd3b1b7 | Chris | @previous ||= Changeset.find(:first, |
| 165 | :conditions => ['id < ? AND repository_id = ?', |
||
| 166 | self.id, self.repository_id], |
||
| 167 | :order => 'id DESC') |
||
| 168 | 0:513646585e45 | Chris | end
|
| 169 | |||
| 170 | # Returns the next changeset
|
||
| 171 | def next |
||
| 172 | 441:cbce1fd3b1b7 | Chris | @next ||= Changeset.find(:first, |
| 173 | :conditions => ['id > ? AND repository_id = ?', |
||
| 174 | self.id, self.repository_id], |
||
| 175 | :order => 'id ASC') |
||
| 176 | 0:513646585e45 | Chris | end
|
| 177 | 441:cbce1fd3b1b7 | Chris | |
| 178 | 0:513646585e45 | Chris | # Creates a new Change from it's common parameters
|
| 179 | def create_change(change) |
||
| 180 | 441:cbce1fd3b1b7 | Chris | Change.create(:changeset => self, |
| 181 | :action => change[:action], |
||
| 182 | :path => change[:path], |
||
| 183 | :from_path => change[:from_path], |
||
| 184 | 0:513646585e45 | Chris | :from_revision => change[:from_revision]) |
| 185 | end
|
||
| 186 | 245:051f544170fe | Chris | |
| 187 | 0:513646585e45 | Chris | private |
| 188 | |||
| 189 | 119:8661b858af72 | Chris | # Finds an issue that can be referenced by the commit message
|
| 190 | # i.e. an issue that belong to the repository project, a subproject or a parent project
|
||
| 191 | def find_referenced_issue_by_id(id) |
||
| 192 | return nil if id.blank? |
||
| 193 | issue = Issue.find_by_id(id.to_i, :include => :project) |
||
| 194 | if issue
|
||
| 195 | 441:cbce1fd3b1b7 | Chris | unless issue.project &&
|
| 196 | (project == issue.project || project.is_ancestor_of?(issue.project) || |
||
| 197 | project.is_descendant_of?(issue.project)) |
||
| 198 | 119:8661b858af72 | Chris | issue = nil
|
| 199 | end
|
||
| 200 | end
|
||
| 201 | issue |
||
| 202 | end
|
||
| 203 | 441:cbce1fd3b1b7 | Chris | |
| 204 | 119:8661b858af72 | Chris | def fix_issue(issue) |
| 205 | status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i) |
||
| 206 | if status.nil?
|
||
| 207 | 909:cbb26bc654de | Chris | logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger |
| 208 | 119:8661b858af72 | Chris | return issue
|
| 209 | end
|
||
| 210 | 441:cbce1fd3b1b7 | Chris | |
| 211 | 119:8661b858af72 | Chris | # the issue may have been updated by the closure of another one (eg. duplicate)
|
| 212 | issue.reload |
||
| 213 | # don't change the status is the issue is closed
|
||
| 214 | return if issue.status && issue.status.is_closed? |
||
| 215 | 441:cbce1fd3b1b7 | Chris | |
| 216 | 119:8661b858af72 | Chris | journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag)) |
| 217 | issue.status = status |
||
| 218 | unless Setting.commit_fix_done_ratio.blank? |
||
| 219 | issue.done_ratio = Setting.commit_fix_done_ratio.to_i
|
||
| 220 | end
|
||
| 221 | Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update, |
||
| 222 | { :changeset => self, :issue => issue })
|
||
| 223 | unless issue.save
|
||
| 224 | logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger |
||
| 225 | end
|
||
| 226 | issue |
||
| 227 | end
|
||
| 228 | 441:cbce1fd3b1b7 | Chris | |
| 229 | 119:8661b858af72 | Chris | def log_time(issue, hours) |
| 230 | time_entry = TimeEntry.new(
|
||
| 231 | :user => user,
|
||
| 232 | :hours => hours,
|
||
| 233 | :issue => issue,
|
||
| 234 | :spent_on => commit_date,
|
||
| 235 | 441:cbce1fd3b1b7 | Chris | :comments => l(:text_time_logged_by_changeset, :value => text_tag, |
| 236 | :locale => Setting.default_language) |
||
| 237 | 119:8661b858af72 | Chris | ) |
| 238 | time_entry.activity = log_time_activity unless log_time_activity.nil?
|
||
| 239 | 441:cbce1fd3b1b7 | Chris | |
| 240 | 119:8661b858af72 | Chris | unless time_entry.save
|
| 241 | logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger |
||
| 242 | end
|
||
| 243 | time_entry |
||
| 244 | end
|
||
| 245 | 441:cbce1fd3b1b7 | Chris | |
| 246 | 119:8661b858af72 | Chris | def log_time_activity |
| 247 | if Setting.commit_logtime_activity_id.to_i > 0 |
||
| 248 | TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i) |
||
| 249 | end
|
||
| 250 | 0:513646585e45 | Chris | end
|
| 251 | 441:cbce1fd3b1b7 | Chris | |
| 252 | 0:513646585e45 | Chris | def split_comments |
| 253 | comments =~ /\A(.+?)\r?\n(.*)$/m
|
||
| 254 | @short_comments = $1 || comments |
||
| 255 | @long_comments = $2.to_s.strip |
||
| 256 | return @short_comments, @long_comments |
||
| 257 | end
|
||
| 258 | |||
| 259 | 245:051f544170fe | Chris | public |
| 260 | 119:8661b858af72 | Chris | |
| 261 | 245:051f544170fe | Chris | # Strips and reencodes a commit log before insertion into the database
|
| 262 | def self.normalize_comments(str, encoding) |
||
| 263 | Changeset.to_utf8(str.to_s.strip, encoding)
|
||
| 264 | end
|
||
| 265 | |||
| 266 | def self.to_utf8(str, encoding) |
||
| 267 | 909:cbb26bc654de | Chris | Redmine::CodesetUtil.to_utf8(str, encoding) |
| 268 | 0:513646585e45 | Chris | end
|
| 269 | end |