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