##// END OF EJS Templates
Merged r3469, r3472 and r3473 from trunk....
Jean-Philippe Lang -
r3391:d80fb751fd01
parent child
Show More
@@ -1,186 +1,187
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'iconv'
19 19
20 20 class Changeset < ActiveRecord::Base
21 21 belongs_to :repository
22 22 belongs_to :user
23 23 has_many :changes, :dependent => :delete_all
24 24 has_and_belongs_to_many :issues
25 25
26 26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 27 :description => :long_comments,
28 28 :datetime => :committed_on,
29 29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
30 30
31 31 acts_as_searchable :columns => 'comments',
32 32 :include => {:repository => :project},
33 33 :project_key => "#{Repository.table_name}.project_id",
34 34 :date_column => 'committed_on'
35 35
36 36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 37 :author_key => :user_id,
38 38 :find_options => {:include => [:user, {:repository => :project}]}
39 39
40 40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 41 validates_uniqueness_of :revision, :scope => :repository_id
42 42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43 43
44 44 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
45 45 :conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
46 46
47 47 def revision=(r)
48 48 write_attribute :revision, (r.nil? ? nil : r.to_s)
49 49 end
50 50
51 51 def comments=(comment)
52 52 write_attribute(:comments, Changeset.normalize_comments(comment))
53 53 end
54 54
55 55 def committed_on=(date)
56 56 self.commit_date = date
57 57 super
58 58 end
59 59
60 60 def committer=(arg)
61 61 write_attribute(:committer, self.class.to_utf8(arg.to_s))
62 62 end
63 63
64 64 def project
65 65 repository.project
66 66 end
67 67
68 68 def author
69 69 user || committer.to_s.split('<').first
70 70 end
71 71
72 72 def before_create
73 73 self.user = repository.find_committer_user(committer)
74 74 end
75 75
76 76 def after_create
77 77 scan_comment_for_issue_ids
78 78 end
79 79 require 'pp'
80 80
81 81 def scan_comment_for_issue_ids
82 82 return if comments.blank?
83 83 # keywords used to reference issues
84 84 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
85 85 # keywords used to fix issues
86 86 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
87 # status and optional done ratio applied
88 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
89 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
90 87
91 88 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
92 89 return if kw_regexp.blank?
93 90
94 91 referenced_issues = []
95 92
96 93 if ref_keywords.delete('*')
97 94 # find any issue ID in the comments
98 95 target_issue_ids = []
99 96 comments.scan(%r{([\s\(\[,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
100 97 referenced_issues += find_referenced_issues_by_id(target_issue_ids)
101 98 end
102 99
103 100 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
104 101 action = match[0]
105 102 target_issue_ids = match[1].scan(/\d+/)
106 103 target_issues = find_referenced_issues_by_id(target_issue_ids)
107 if fix_status && fix_keywords.include?(action.downcase)
104 if fix_keywords.include?(action.downcase) && fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
108 105 # update status of issues
109 106 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
110 107 target_issues.each do |issue|
111 108 # the issue may have been updated by the closure of another one (eg. duplicate)
112 109 issue.reload
113 110 # don't change the status is the issue is closed
114 111 next if issue.status.is_closed?
115 112 csettext = "r#{self.revision}"
116 113 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
117 114 csettext = "commit:\"#{self.scmid}\""
118 115 end
119 116 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
120 117 issue.status = fix_status
121 issue.done_ratio = done_ratio if done_ratio
118 unless Setting.commit_fix_done_ratio.blank?
119 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
120 end
122 121 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
123 122 { :changeset => self, :issue => issue })
124 123 issue.save
125 124 end
126 125 end
127 126 referenced_issues += target_issues
128 127 end
129 128
130 self.issues = referenced_issues.uniq
129 referenced_issues.uniq!
130 self.issues = referenced_issues unless referenced_issues.empty?
131 131 end
132 132
133 133 def short_comments
134 134 @short_comments || split_comments.first
135 135 end
136 136
137 137 def long_comments
138 138 @long_comments || split_comments.last
139 139 end
140 140
141 141 # Returns the previous changeset
142 142 def previous
143 143 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
144 144 end
145 145
146 146 # Returns the next changeset
147 147 def next
148 148 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
149 149 end
150 150
151 151 # Strips and reencodes a commit log before insertion into the database
152 152 def self.normalize_comments(str)
153 153 to_utf8(str.to_s.strip)
154 154 end
155 155
156 156 private
157 157
158 158 # Finds issues that can be referenced by the commit message
159 159 # i.e. issues that belong to the repository project, a subproject or a parent project
160 160 def find_referenced_issues_by_id(ids)
161 return [] if ids.compact.empty?
161 162 Issue.find_all_by_id(ids, :include => :project).select {|issue|
162 163 project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
163 164 }
164 165 end
165 166
166 167 def split_comments
167 168 comments =~ /\A(.+?)\r?\n(.*)$/m
168 169 @short_comments = $1 || comments
169 170 @long_comments = $2.to_s.strip
170 171 return @short_comments, @long_comments
171 172 end
172 173
173 174 def self.to_utf8(str)
174 175 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
175 176 encoding = Setting.commit_logs_encoding.to_s.strip
176 177 unless encoding.blank? || encoding == 'UTF-8'
177 178 begin
178 179 str = Iconv.conv('UTF-8', encoding, str)
179 180 rescue Iconv::Failure
180 181 # do nothing here
181 182 end
182 183 end
183 184 # removes invalid UTF8 sequences
184 185 Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
185 186 end
186 187 end
@@ -1,208 +1,215
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Repository < ActiveRecord::Base
19 19 belongs_to :project
20 20 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
21 21 has_many :changes, :through => :changesets
22 22
23 23 # Raw SQL to delete changesets and changes in the database
24 24 # has_many :changesets, :dependent => :destroy is too slow for big repositories
25 25 before_destroy :clear_changesets
26 26
27 27 # Checks if the SCM is enabled when creating a repository
28 28 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
29 29
30 30 # Removes leading and trailing whitespace
31 31 def url=(arg)
32 32 write_attribute(:url, arg ? arg.to_s.strip : nil)
33 33 end
34 34
35 35 # Removes leading and trailing whitespace
36 36 def root_url=(arg)
37 37 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
38 38 end
39 39
40 40 def scm
41 41 @scm ||= self.scm_adapter.new url, root_url, login, password
42 42 update_attribute(:root_url, @scm.root_url) if root_url.blank?
43 43 @scm
44 44 end
45 45
46 46 def scm_name
47 47 self.class.scm_name
48 48 end
49 49
50 50 def supports_cat?
51 51 scm.supports_cat?
52 52 end
53 53
54 54 def supports_annotate?
55 55 scm.supports_annotate?
56 56 end
57 57
58 58 def entry(path=nil, identifier=nil)
59 59 scm.entry(path, identifier)
60 60 end
61 61
62 62 def entries(path=nil, identifier=nil)
63 63 scm.entries(path, identifier)
64 64 end
65 65
66 66 def branches
67 67 scm.branches
68 68 end
69 69
70 70 def tags
71 71 scm.tags
72 72 end
73 73
74 74 def default_branch
75 75 scm.default_branch
76 76 end
77 77
78 78 def properties(path, identifier=nil)
79 79 scm.properties(path, identifier)
80 80 end
81 81
82 82 def cat(path, identifier=nil)
83 83 scm.cat(path, identifier)
84 84 end
85 85
86 86 def diff(path, rev, rev_to)
87 87 scm.diff(path, rev, rev_to)
88 88 end
89 89
90 90 # Returns a path relative to the url of the repository
91 91 def relative_path(path)
92 92 path
93 93 end
94 94
95 95 # Finds and returns a revision with a number or the beginning of a hash
96 96 def find_changeset_by_name(name)
97 97 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
98 98 end
99 99
100 100 def latest_changeset
101 101 @latest_changeset ||= changesets.find(:first)
102 102 end
103 103
104 104 # Returns the latest changesets for +path+
105 105 # Default behaviour is to search in cached changesets
106 106 def latest_changesets(path, rev, limit=10)
107 107 if path.blank?
108 108 changesets.find(:all, :include => :user,
109 109 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
110 110 :limit => limit)
111 111 else
112 112 changes.find(:all, :include => {:changeset => :user},
113 113 :conditions => ["path = ?", path.with_leading_slash],
114 114 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
115 115 :limit => limit).collect(&:changeset)
116 116 end
117 117 end
118 118
119 119 def scan_changesets_for_issue_ids
120 120 self.changesets.each(&:scan_comment_for_issue_ids)
121 121 end
122 122
123 123 # Returns an array of committers usernames and associated user_id
124 124 def committers
125 125 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
126 126 end
127 127
128 128 # Maps committers username to a user ids
129 129 def committer_ids=(h)
130 130 if h.is_a?(Hash)
131 131 committers.each do |committer, user_id|
132 132 new_user_id = h[committer]
133 133 if new_user_id && (new_user_id.to_i != user_id.to_i)
134 134 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
135 135 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
136 136 end
137 137 end
138 138 @committers = nil
139 @found_committer_users = nil
139 140 true
140 141 else
141 142 false
142 143 end
143 144 end
144 145
145 146 # Returns the Redmine User corresponding to the given +committer+
146 147 # It will return nil if the committer is not yet mapped and if no User
147 148 # with the same username or email was found
148 149 def find_committer_user(committer)
149 if committer
150 unless committer.blank?
151 @found_committer_users ||= {}
152 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
153
154 user = nil
150 155 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
151 156 if c && c.user
152 c.user
157 user = c.user
153 158 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
154 159 username, email = $1.strip, $3
155 160 u = User.find_by_login(username)
156 161 u ||= User.find_by_mail(email) unless email.blank?
157 u
162 user = u
158 163 end
164 @found_committer_users[committer] = user
165 user
159 166 end
160 167 end
161 168
162 169 # Fetches new changesets for all repositories of active projects
163 170 # Can be called periodically by an external script
164 171 # eg. ruby script/runner "Repository.fetch_changesets"
165 172 def self.fetch_changesets
166 173 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
167 174 if project.repository
168 175 project.repository.fetch_changesets
169 176 end
170 177 end
171 178 end
172 179
173 180 # scan changeset comments to find related and fixed issues for all repositories
174 181 def self.scan_changesets_for_issue_ids
175 182 find(:all).each(&:scan_changesets_for_issue_ids)
176 183 end
177 184
178 185 def self.scm_name
179 186 'Abstract'
180 187 end
181 188
182 189 def self.available_scm
183 190 subclasses.collect {|klass| [klass.scm_name, klass.name]}
184 191 end
185 192
186 193 def self.factory(klass_name, *args)
187 194 klass = "Repository::#{klass_name}".constantize
188 195 klass.new(*args)
189 196 rescue
190 197 nil
191 198 end
192 199
193 200 private
194 201
195 202 def before_save
196 203 # Strips url and root_url
197 204 url.strip!
198 205 root_url.strip!
199 206 true
200 207 end
201 208
202 209 def clear_changesets
203 210 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
204 211 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
205 212 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
206 213 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
207 214 end
208 215 end
@@ -1,331 +1,333
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'cgi'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class CommandFailed < StandardError #:nodoc:
24 24 end
25 25
26 26 class AbstractAdapter #:nodoc:
27 27 class << self
28 28 # Returns the version of the scm client
29 29 # Eg: [1, 5, 0] or [] if unknown
30 30 def client_version
31 31 []
32 32 end
33 33
34 34 # Returns the version string of the scm client
35 35 # Eg: '1.5.0' or 'Unknown version' if unknown
36 36 def client_version_string
37 37 v = client_version || 'Unknown version'
38 38 v.is_a?(Array) ? v.join('.') : v.to_s
39 39 end
40 40
41 41 # Returns true if the current client version is above
42 42 # or equals the given one
43 43 # If option is :unknown is set to true, it will return
44 44 # true if the client version is unknown
45 45 def client_version_above?(v, options={})
46 46 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
47 47 end
48 48 end
49 49
50 50 def initialize(url, root_url=nil, login=nil, password=nil)
51 51 @url = url
52 52 @login = login if login && !login.empty?
53 53 @password = (password || "") if @login
54 54 @root_url = root_url.blank? ? retrieve_root_url : root_url
55 55 end
56 56
57 57 def adapter_name
58 58 'Abstract'
59 59 end
60 60
61 61 def supports_cat?
62 62 true
63 63 end
64 64
65 65 def supports_annotate?
66 66 respond_to?('annotate')
67 67 end
68 68
69 69 def root_url
70 70 @root_url
71 71 end
72 72
73 73 def url
74 74 @url
75 75 end
76 76
77 77 # get info about the svn repository
78 78 def info
79 79 return nil
80 80 end
81 81
82 82 # Returns the entry identified by path and revision identifier
83 83 # or nil if entry doesn't exist in the repository
84 84 def entry(path=nil, identifier=nil)
85 85 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
86 86 search_path = parts[0..-2].join('/')
87 87 search_name = parts[-1]
88 88 if search_path.blank? && search_name.blank?
89 89 # Root entry
90 90 Entry.new(:path => '', :kind => 'dir')
91 91 else
92 92 # Search for the entry in the parent directory
93 93 es = entries(search_path, identifier)
94 94 es ? es.detect {|e| e.name == search_name} : nil
95 95 end
96 96 end
97 97
98 98 # Returns an Entries collection
99 99 # or nil if the given path doesn't exist in the repository
100 100 def entries(path=nil, identifier=nil)
101 101 return nil
102 102 end
103 103
104 104 def branches
105 105 return nil
106 106 end
107 107
108 108 def tags
109 109 return nil
110 110 end
111 111
112 112 def default_branch
113 113 return nil
114 114 end
115 115
116 116 def properties(path, identifier=nil)
117 117 return nil
118 118 end
119 119
120 120 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
121 121 return nil
122 122 end
123 123
124 124 def diff(path, identifier_from, identifier_to=nil)
125 125 return nil
126 126 end
127 127
128 128 def cat(path, identifier=nil)
129 129 return nil
130 130 end
131 131
132 132 def with_leading_slash(path)
133 133 path ||= ''
134 134 (path[0,1]!="/") ? "/#{path}" : path
135 135 end
136 136
137 137 def with_trailling_slash(path)
138 138 path ||= ''
139 139 (path[-1,1] == "/") ? path : "#{path}/"
140 140 end
141 141
142 142 def without_leading_slash(path)
143 143 path ||= ''
144 144 path.gsub(%r{^/+}, '')
145 145 end
146 146
147 147 def without_trailling_slash(path)
148 148 path ||= ''
149 149 (path[-1,1] == "/") ? path[0..-2] : path
150 150 end
151 151
152 152 def shell_quote(str)
153 153 if Redmine::Platform.mswin?
154 154 '"' + str.gsub(/"/, '\\"') + '"'
155 155 else
156 156 "'" + str.gsub(/'/, "'\"'\"'") + "'"
157 157 end
158 158 end
159 159
160 160 private
161 161 def retrieve_root_url
162 162 info = self.info
163 163 info ? info.root_url : nil
164 164 end
165 165
166 166 def target(path)
167 167 path ||= ''
168 168 base = path.match(/^\//) ? root_url : url
169 169 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
170 170 end
171 171
172 172 def logger
173 173 self.class.logger
174 174 end
175 175
176 176 def shellout(cmd, &block)
177 177 self.class.shellout(cmd, &block)
178 178 end
179 179
180 180 def self.logger
181 181 RAILS_DEFAULT_LOGGER
182 182 end
183 183
184 184 def self.shellout(cmd, &block)
185 185 logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug?
186 186 if Rails.env == 'development'
187 187 # Capture stderr when running in dev environment
188 188 cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
189 189 end
190 190 begin
191 191 IO.popen(cmd, "r+") do |io|
192 192 io.close_write
193 193 block.call(io) if block_given?
194 194 end
195 195 rescue Errno::ENOENT => e
196 196 msg = strip_credential(e.message)
197 197 # The command failed, log it and re-raise
198 198 logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
199 199 raise CommandFailed.new(msg)
200 200 end
201 201 end
202 202
203 203 # Hides username/password in a given command
204 204 def self.strip_credential(cmd)
205 205 q = (Redmine::Platform.mswin? ? '"' : "'")
206 206 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
207 207 end
208 208
209 209 def strip_credential(cmd)
210 210 self.class.strip_credential(cmd)
211 211 end
212 212 end
213 213
214 214 class Entries < Array
215 215 def sort_by_name
216 216 sort {|x,y|
217 217 if x.kind == y.kind
218 218 x.name.to_s <=> y.name.to_s
219 219 else
220 220 x.kind <=> y.kind
221 221 end
222 222 }
223 223 end
224 224
225 225 def revisions
226 226 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
227 227 end
228 228 end
229 229
230 230 class Info
231 231 attr_accessor :root_url, :lastrev
232 232 def initialize(attributes={})
233 233 self.root_url = attributes[:root_url] if attributes[:root_url]
234 234 self.lastrev = attributes[:lastrev]
235 235 end
236 236 end
237 237
238 238 class Entry
239 239 attr_accessor :name, :path, :kind, :size, :lastrev
240 240 def initialize(attributes={})
241 241 self.name = attributes[:name] if attributes[:name]
242 242 self.path = attributes[:path] if attributes[:path]
243 243 self.kind = attributes[:kind] if attributes[:kind]
244 244 self.size = attributes[:size].to_i if attributes[:size]
245 245 self.lastrev = attributes[:lastrev]
246 246 end
247 247
248 248 def is_file?
249 249 'file' == self.kind
250 250 end
251 251
252 252 def is_dir?
253 253 'dir' == self.kind
254 254 end
255 255
256 256 def is_text?
257 257 Redmine::MimeType.is_type?('text', name)
258 258 end
259 259 end
260 260
261 261 class Revisions < Array
262 262 def latest
263 263 sort {|x,y|
264 264 unless x.time.nil? or y.time.nil?
265 265 x.time <=> y.time
266 266 else
267 267 0
268 268 end
269 269 }.last
270 270 end
271 271 end
272 272
273 273 class Revision
274 274 attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
275 275
276 276 def initialize(attributes={})
277 277 self.identifier = attributes[:identifier]
278 278 self.scmid = attributes[:scmid]
279 279 self.name = attributes[:name] || self.identifier
280 280 self.author = attributes[:author]
281 281 self.time = attributes[:time]
282 282 self.message = attributes[:message] || ""
283 283 self.paths = attributes[:paths]
284 284 self.revision = attributes[:revision]
285 285 self.branch = attributes[:branch]
286 286 end
287 287
288 288 def save(repo)
289 if repo.changesets.find_by_scmid(scmid.to_s).nil?
290 changeset = Changeset.create!(
289 Changeset.transaction do
290 changeset = Changeset.new(
291 291 :repository => repo,
292 292 :revision => identifier,
293 293 :scmid => scmid,
294 294 :committer => author,
295 295 :committed_on => time,
296 296 :comments => message)
297
298 paths.each do |file|
299 Change.create!(
300 :changeset => changeset,
301 :action => file[:action],
302 :path => file[:path])
303 end
297
298 if changeset.save
299 paths.each do |file|
300 Change.create(
301 :changeset => changeset,
302 :action => file[:action],
303 :path => file[:path])
304 end
305 end
304 306 end
305 307 end
306 308 end
307 309
308 310 class Annotate
309 311 attr_reader :lines, :revisions
310 312
311 313 def initialize
312 314 @lines = []
313 315 @revisions = []
314 316 end
315 317
316 318 def add_line(line, revision)
317 319 @lines << line
318 320 @revisions << revision
319 321 end
320 322
321 323 def content
322 324 content = lines.join("\n")
323 325 end
324 326
325 327 def empty?
326 328 lines.empty?
327 329 end
328 330 end
329 331 end
330 332 end
331 333 end
General Comments 0
You need to be logged in to leave comments. Login now