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