##// END OF EJS Templates
Move unified diff parser out of the scm abstract adapter so it can be reused for viewing attached diffs (#1403)....
Jean-Philippe Lang -
r1499:ec0525d4975b
parent child
Show More
@@ -0,0 +1,178
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
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 #
9 # 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 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 # Class used to parse unified diffs
20 class UnifiedDiff < Array
21 def initialize(diff, type="inline")
22 diff_table = DiffTable.new type
23 diff.each do |line|
24 if line =~ /^(---|\+\+\+) (.*)$/
25 self << diff_table if diff_table.length > 1
26 diff_table = DiffTable.new type
27 end
28 a = diff_table.add_line line
29 end
30 self << diff_table unless diff_table.empty?
31 self
32 end
33 end
34
35 # Class that represents a file diff
36 class DiffTable < Hash
37 attr_reader :file_name, :line_num_l, :line_num_r
38
39 # Initialize with a Diff file and the type of Diff View
40 # The type view must be inline or sbs (side_by_side)
41 def initialize(type="inline")
42 @parsing = false
43 @nb_line = 1
44 @start = false
45 @before = 'same'
46 @second = true
47 @type = type
48 end
49
50 # Function for add a line of this Diff
51 def add_line(line)
52 unless @parsing
53 if line =~ /^(---|\+\+\+) (.*)$/
54 @file_name = $2
55 return false
56 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
57 @line_num_l = $5.to_i
58 @line_num_r = $2.to_i
59 @parsing = true
60 end
61 else
62 if line =~ /^[^\+\-\s@\\]/
63 @parsing = false
64 return false
65 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
66 @line_num_l = $5.to_i
67 @line_num_r = $2.to_i
68 else
69 @nb_line += 1 if parse_line(line, @type)
70 end
71 end
72 return true
73 end
74
75 def inspect
76 puts '### DIFF TABLE ###'
77 puts "file : #{file_name}"
78 self.each do |d|
79 d.inspect
80 end
81 end
82
83 private
84 # Test if is a Side By Side type
85 def sbs?(type, func)
86 if @start and type == "sbs"
87 if @before == func and @second
88 tmp_nb_line = @nb_line
89 self[tmp_nb_line] = Diff.new
90 else
91 @second = false
92 tmp_nb_line = @start
93 @start += 1
94 @nb_line -= 1
95 end
96 else
97 tmp_nb_line = @nb_line
98 @start = @nb_line
99 self[tmp_nb_line] = Diff.new
100 @second = true
101 end
102 unless self[tmp_nb_line]
103 @nb_line += 1
104 self[tmp_nb_line] = Diff.new
105 else
106 self[tmp_nb_line]
107 end
108 end
109
110 # Escape the HTML for the diff
111 def escapeHTML(line)
112 CGI.escapeHTML(line)
113 end
114
115 def parse_line(line, type="inline")
116 if line[0, 1] == "+"
117 diff = sbs? type, 'add'
118 @before = 'add'
119 diff.line_left = escapeHTML line[1..-1]
120 diff.nb_line_left = @line_num_l
121 diff.type_diff_left = 'diff_in'
122 @line_num_l += 1
123 true
124 elsif line[0, 1] == "-"
125 diff = sbs? type, 'remove'
126 @before = 'remove'
127 diff.line_right = escapeHTML line[1..-1]
128 diff.nb_line_right = @line_num_r
129 diff.type_diff_right = 'diff_out'
130 @line_num_r += 1
131 true
132 elsif line[0, 1] =~ /\s/
133 @before = 'same'
134 @start = false
135 diff = Diff.new
136 diff.line_right = escapeHTML line[1..-1]
137 diff.nb_line_right = @line_num_r
138 diff.line_left = escapeHTML line[1..-1]
139 diff.nb_line_left = @line_num_l
140 self[@nb_line] = diff
141 @line_num_l += 1
142 @line_num_r += 1
143 true
144 elsif line[0, 1] = "\\"
145 true
146 else
147 false
148 end
149 end
150 end
151
152 # A line of diff
153 class Diff
154 attr_accessor :nb_line_left
155 attr_accessor :line_left
156 attr_accessor :nb_line_right
157 attr_accessor :line_right
158 attr_accessor :type_diff_right
159 attr_accessor :type_diff_left
160
161 def initialize()
162 self.nb_line_left = ''
163 self.nb_line_right = ''
164 self.line_left = ''
165 self.line_right = ''
166 self.type_diff_right = ''
167 self.type_diff_left = ''
168 end
169
170 def inspect
171 puts '### Start Line Diff ###'
172 puts self.nb_line_left
173 puts self.line_left
174 puts self.nb_line_right
175 puts self.line_right
176 end
177 end
178 end
@@ -1,314 +1,314
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 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 21
22 22 class ChangesetNotFound < Exception; end
23 23 class InvalidRevisionParam < Exception; end
24 24
25 25 class RepositoriesController < ApplicationController
26 26 layout 'base'
27 27 menu_item :repository
28 28 before_filter :find_repository, :except => :edit
29 29 before_filter :find_project, :only => :edit
30 30 before_filter :authorize
31 31 accept_key_auth :revisions
32 32
33 33 def edit
34 34 @repository = @project.repository
35 35 if !@repository
36 36 @repository = Repository.factory(params[:repository_scm])
37 37 @repository.project = @project if @repository
38 38 end
39 39 if request.post? && @repository
40 40 @repository.attributes = params[:repository]
41 41 @repository.save
42 42 end
43 43 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
44 44 end
45 45
46 46 def destroy
47 47 @repository.destroy
48 48 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
49 49 end
50 50
51 51 def show
52 52 # check if new revisions have been committed in the repository
53 53 @repository.fetch_changesets if Setting.autofetch_changesets?
54 54 # root entries
55 55 @entries = @repository.entries('', @rev)
56 56 # latest changesets
57 57 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
58 58 show_error_not_found unless @entries || @changesets.any?
59 59 rescue Redmine::Scm::Adapters::CommandFailed => e
60 60 show_error_command_failed(e.message)
61 61 end
62 62
63 63 def browse
64 64 @entries = @repository.entries(@path, @rev)
65 65 if request.xhr?
66 66 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
67 67 else
68 68 show_error_not_found and return unless @entries
69 69 render :action => 'browse'
70 70 end
71 71 rescue Redmine::Scm::Adapters::CommandFailed => e
72 72 show_error_command_failed(e.message)
73 73 end
74 74
75 75 def changes
76 76 @entry = @repository.scm.entry(@path, @rev)
77 77 show_error_not_found and return unless @entry
78 78 @changesets = @repository.changesets_for_path(@path)
79 79 rescue Redmine::Scm::Adapters::CommandFailed => e
80 80 show_error_command_failed(e.message)
81 81 end
82 82
83 83 def revisions
84 84 @changeset_count = @repository.changesets.count
85 85 @changeset_pages = Paginator.new self, @changeset_count,
86 86 per_page_option,
87 87 params['page']
88 88 @changesets = @repository.changesets.find(:all,
89 89 :limit => @changeset_pages.items_per_page,
90 90 :offset => @changeset_pages.current.offset)
91 91
92 92 respond_to do |format|
93 93 format.html { render :layout => false if request.xhr? }
94 94 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
95 95 end
96 96 end
97 97
98 98 def entry
99 99 @entry = @repository.scm.entry(@path, @rev)
100 100 show_error_not_found and return unless @entry
101 101
102 102 # If the entry is a dir, show the browser
103 103 browse and return if @entry.is_dir?
104 104
105 105 @content = @repository.scm.cat(@path, @rev)
106 106 show_error_not_found and return unless @content
107 107 if 'raw' == params[:format] || @content.is_binary_data?
108 108 # Force the download if it's a binary file
109 109 send_data @content, :filename => @path.split('/').last
110 110 else
111 111 # Prevent empty lines when displaying a file with Windows style eol
112 112 @content.gsub!("\r\n", "\n")
113 113 end
114 114 rescue Redmine::Scm::Adapters::CommandFailed => e
115 115 show_error_command_failed(e.message)
116 116 end
117 117
118 118 def annotate
119 119 @annotate = @repository.scm.annotate(@path, @rev)
120 120 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
121 121 rescue Redmine::Scm::Adapters::CommandFailed => e
122 122 show_error_command_failed(e.message)
123 123 end
124 124
125 125 def revision
126 126 @changeset = @repository.changesets.find_by_revision(@rev)
127 127 raise ChangesetNotFound unless @changeset
128 128 @changes_count = @changeset.changes.size
129 129 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
130 130 @changes = @changeset.changes.find(:all,
131 131 :limit => @changes_pages.items_per_page,
132 132 :offset => @changes_pages.current.offset)
133 133
134 134 respond_to do |format|
135 135 format.html
136 136 format.js {render :layout => false}
137 137 end
138 138 rescue ChangesetNotFound
139 139 show_error_not_found
140 140 rescue Redmine::Scm::Adapters::CommandFailed => e
141 141 show_error_command_failed(e.message)
142 142 end
143 143
144 144 def diff
145 145 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
146 146 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
147 147
148 148 # Save diff type as user preference
149 149 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
150 150 User.current.pref[:diff_type] = @diff_type
151 151 User.current.preference.save
152 152 end
153 153
154 154 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
155 155 unless read_fragment(@cache_key)
156 @diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
156 @diff = @repository.diff(@path, @rev, @rev_to)
157 157 show_error_not_found unless @diff
158 158 end
159 159 rescue Redmine::Scm::Adapters::CommandFailed => e
160 160 show_error_command_failed(e.message)
161 161 end
162 162
163 163 def stats
164 164 end
165 165
166 166 def graph
167 167 data = nil
168 168 case params[:graph]
169 169 when "commits_per_month"
170 170 data = graph_commits_per_month(@repository)
171 171 when "commits_per_author"
172 172 data = graph_commits_per_author(@repository)
173 173 end
174 174 if data
175 175 headers["Content-Type"] = "image/svg+xml"
176 176 send_data(data, :type => "image/svg+xml", :disposition => "inline")
177 177 else
178 178 render_404
179 179 end
180 180 end
181 181
182 182 private
183 183 def find_project
184 184 @project = Project.find(params[:id])
185 185 rescue ActiveRecord::RecordNotFound
186 186 render_404
187 187 end
188 188
189 189 REV_PARAM_RE = %r{^[a-f0-9]*$}
190 190
191 191 def find_repository
192 192 @project = Project.find(params[:id])
193 193 @repository = @project.repository
194 194 render_404 and return false unless @repository
195 195 @path = params[:path].join('/') unless params[:path].nil?
196 196 @path ||= ''
197 197 @rev = params[:rev]
198 198 @rev_to = params[:rev_to]
199 199 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
200 200 rescue ActiveRecord::RecordNotFound
201 201 render_404
202 202 rescue InvalidRevisionParam
203 203 show_error_not_found
204 204 end
205 205
206 206 def show_error_not_found
207 207 render_error l(:error_scm_not_found)
208 208 end
209 209
210 210 def show_error_command_failed(msg)
211 211 render_error l(:error_scm_command_failed, msg)
212 212 end
213 213
214 214 def graph_commits_per_month(repository)
215 215 @date_to = Date.today
216 216 @date_from = @date_to << 11
217 217 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
218 218 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
219 219 commits_by_month = [0] * 12
220 220 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
221 221
222 222 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
223 223 changes_by_month = [0] * 12
224 224 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
225 225
226 226 fields = []
227 227 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
228 228 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
229 229
230 230 graph = SVG::Graph::Bar.new(
231 231 :height => 300,
232 232 :width => 500,
233 233 :fields => fields.reverse,
234 234 :stack => :side,
235 235 :scale_integers => true,
236 236 :step_x_labels => 2,
237 237 :show_data_values => false,
238 238 :graph_title => l(:label_commits_per_month),
239 239 :show_graph_title => true
240 240 )
241 241
242 242 graph.add_data(
243 243 :data => commits_by_month[0..11].reverse,
244 244 :title => l(:label_revision_plural)
245 245 )
246 246
247 247 graph.add_data(
248 248 :data => changes_by_month[0..11].reverse,
249 249 :title => l(:label_change_plural)
250 250 )
251 251
252 252 graph.burn
253 253 end
254 254
255 255 def graph_commits_per_author(repository)
256 256 commits_by_author = repository.changesets.count(:all, :group => :committer)
257 257 commits_by_author.sort! {|x, y| x.last <=> y.last}
258 258
259 259 changes_by_author = repository.changes.count(:all, :group => :committer)
260 260 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
261 261
262 262 fields = commits_by_author.collect {|r| r.first}
263 263 commits_data = commits_by_author.collect {|r| r.last}
264 264 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
265 265
266 266 fields = fields + [""]*(10 - fields.length) if fields.length<10
267 267 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
268 268 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
269 269
270 270 # Remove email adress in usernames
271 271 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
272 272
273 273 graph = SVG::Graph::BarHorizontal.new(
274 274 :height => 300,
275 275 :width => 500,
276 276 :fields => fields,
277 277 :stack => :side,
278 278 :scale_integers => true,
279 279 :show_data_values => false,
280 280 :rotate_y_labels => false,
281 281 :graph_title => l(:label_commits_per_author),
282 282 :show_graph_title => true
283 283 )
284 284
285 285 graph.add_data(
286 286 :data => commits_data,
287 287 :title => l(:label_revision_plural)
288 288 )
289 289
290 290 graph.add_data(
291 291 :data => changes_data,
292 292 :title => l(:label_change_plural)
293 293 )
294 294
295 295 graph.burn
296 296 end
297 297
298 298 end
299 299
300 300 class Date
301 301 def months_ago(date = Date.today)
302 302 (date.year - self.year)*12 + (date.month - self.month)
303 303 end
304 304
305 305 def weeks_ago(date = Date.today)
306 306 (date.year - self.year)*52 + (date.cweek - self.cweek)
307 307 end
308 308 end
309 309
310 310 class String
311 311 def with_leading_slash
312 312 starts_with?('/') ? self : "/#{self}"
313 313 end
314 314 end
@@ -1,118 +1,118
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, :dependent => :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
21 21 has_many :changes, :through => :changesets
22 22
23 23 # Checks if the SCM is enabled when creating a repository
24 24 validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
25 25
26 26 # Removes leading and trailing whitespace
27 27 def url=(arg)
28 28 write_attribute(:url, arg ? arg.to_s.strip : nil)
29 29 end
30 30
31 31 # Removes leading and trailing whitespace
32 32 def root_url=(arg)
33 33 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
34 34 end
35 35
36 36 def scm
37 37 @scm ||= self.scm_adapter.new url, root_url, login, password
38 38 update_attribute(:root_url, @scm.root_url) if root_url.blank?
39 39 @scm
40 40 end
41 41
42 42 def scm_name
43 43 self.class.scm_name
44 44 end
45 45
46 46 def supports_cat?
47 47 scm.supports_cat?
48 48 end
49 49
50 50 def supports_annotate?
51 51 scm.supports_annotate?
52 52 end
53 53
54 54 def entries(path=nil, identifier=nil)
55 55 scm.entries(path, identifier)
56 56 end
57 57
58 def diff(path, rev, rev_to, type)
59 scm.diff(path, rev, rev_to, type)
58 def diff(path, rev, rev_to)
59 scm.diff(path, rev, rev_to)
60 60 end
61 61
62 62 # Default behaviour: we search in cached changesets
63 63 def changesets_for_path(path)
64 64 path = "/#{path}" unless path.starts_with?('/')
65 65 Change.find(:all, :include => :changeset,
66 66 :conditions => ["repository_id = ? AND path = ?", id, path],
67 67 :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
68 68 end
69 69
70 70 # Returns a path relative to the url of the repository
71 71 def relative_path(path)
72 72 path
73 73 end
74 74
75 75 def latest_changeset
76 76 @latest_changeset ||= changesets.find(:first)
77 77 end
78 78
79 79 def scan_changesets_for_issue_ids
80 80 self.changesets.each(&:scan_comment_for_issue_ids)
81 81 end
82 82
83 83 # fetch new changesets for all repositories
84 84 # can be called periodically by an external script
85 85 # eg. ruby script/runner "Repository.fetch_changesets"
86 86 def self.fetch_changesets
87 87 find(:all).each(&:fetch_changesets)
88 88 end
89 89
90 90 # scan changeset comments to find related and fixed issues for all repositories
91 91 def self.scan_changesets_for_issue_ids
92 92 find(:all).each(&:scan_changesets_for_issue_ids)
93 93 end
94 94
95 95 def self.scm_name
96 96 'Abstract'
97 97 end
98 98
99 99 def self.available_scm
100 100 subclasses.collect {|klass| [klass.scm_name, klass.name]}
101 101 end
102 102
103 103 def self.factory(klass_name, *args)
104 104 klass = "Repository::#{klass_name}".constantize
105 105 klass.new(*args)
106 106 rescue
107 107 nil
108 108 end
109 109
110 110 private
111 111
112 112 def before_save
113 113 # Strips url and root_url
114 114 url.strip!
115 115 root_url.strip!
116 116 true
117 117 end
118 118 end
@@ -1,155 +1,155
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 'redmine/scm/adapters/cvs_adapter'
19 19 require 'digest/sha1'
20 20
21 21 class Repository::Cvs < Repository
22 22 validates_presence_of :url, :root_url
23 23
24 24 def scm_adapter
25 25 Redmine::Scm::Adapters::CvsAdapter
26 26 end
27 27
28 28 def self.scm_name
29 29 'CVS'
30 30 end
31 31
32 32 def entry(path, identifier)
33 33 e = entries(path, identifier)
34 34 e ? e.first : nil
35 35 end
36 36
37 37 def entries(path=nil, identifier=nil)
38 38 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
39 39 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
40 40 if entries
41 41 entries.each() do |entry|
42 42 unless entry.lastrev.nil? || entry.lastrev.identifier
43 43 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
44 44 if change
45 45 entry.lastrev.identifier=change.changeset.revision
46 46 entry.lastrev.author=change.changeset.committer
47 47 entry.lastrev.revision=change.revision
48 48 entry.lastrev.branch=change.branch
49 49 end
50 50 end
51 51 end
52 52 end
53 53 entries
54 54 end
55 55
56 def diff(path, rev, rev_to, type)
56 def diff(path, rev, rev_to)
57 57 #convert rev to revision. CVS can't handle changesets here
58 58 diff=[]
59 59 changeset_from=changesets.find_by_revision(rev)
60 60 if rev_to.to_i > 0
61 61 changeset_to=changesets.find_by_revision(rev_to)
62 62 end
63 63 changeset_from.changes.each() do |change_from|
64 64
65 65 revision_from=nil
66 66 revision_to=nil
67 67
68 68 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
69 69
70 70 if revision_from
71 71 if changeset_to
72 72 changeset_to.changes.each() do |change_to|
73 73 revision_to=change_to.revision if change_to.path==change_from.path
74 74 end
75 75 end
76 76 unless revision_to
77 77 revision_to=scm.get_previous_revision(revision_from)
78 78 end
79 diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
79 diff=diff+scm.diff(change_from.path, revision_from, revision_to)
80 80 end
81 81 end
82 82 return diff
83 83 end
84 84
85 85 def fetch_changesets
86 86 # some nifty bits to introduce a commit-id with cvs
87 87 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
88 88 # we now take a guess using the author, the commitlog and the commit-date.
89 89
90 90 # last one is the next step to take. the commit-date is not equal for all
91 91 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
92 92 # we use a small delta here, to merge all changes belonging to _one_ changeset
93 93 time_delta=10.seconds
94 94
95 95 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
96 96 transaction do
97 97 tmp_rev_num = 1
98 98 scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
99 99 # only add the change to the database, if it doen't exists. the cvs log
100 100 # is not exclusive at all.
101 101 unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
102 102 revision
103 103 cs = changesets.find(:first, :conditions=>{
104 104 :committed_on=>revision.time-time_delta..revision.time+time_delta,
105 105 :committer=>revision.author,
106 106 :comments=>revision.message
107 107 })
108 108
109 109 # create a new changeset....
110 110 unless cs
111 111 # we use a temporaray revision number here (just for inserting)
112 112 # later on, we calculate a continous positive number
113 113 latest = changesets.find(:first, :order => 'id DESC')
114 114 cs = Changeset.create(:repository => self,
115 115 :revision => "_#{tmp_rev_num}",
116 116 :committer => revision.author,
117 117 :committed_on => revision.time,
118 118 :comments => revision.message)
119 119 tmp_rev_num += 1
120 120 end
121 121
122 122 #convert CVS-File-States to internal Action-abbrevations
123 123 #default action is (M)odified
124 124 action="M"
125 125 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
126 126 action="A" #add-action always at first revision (= 1.1)
127 127 elsif revision.paths[0][:action]=="dead"
128 128 action="D" #dead-state is similar to Delete
129 129 end
130 130
131 131 Change.create(:changeset => cs,
132 132 :action => action,
133 133 :path => scm.with_leading_slash(revision.paths[0][:path]),
134 134 :revision => revision.paths[0][:revision],
135 135 :branch => revision.paths[0][:branch]
136 136 )
137 137 end
138 138 end
139 139
140 140 # Renumber new changesets in chronological order
141 141 changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
142 142 changeset.update_attribute :revision, next_revision_number
143 143 end
144 144 end # transaction
145 145 end
146 146
147 147 private
148 148
149 149 # Returns the next revision number to assign to a CVS changeset
150 150 def next_revision_number
151 151 # Need to retrieve existing revision numbers to sort them as integers
152 152 @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
153 153 @current_revision_number += 1
154 154 end
155 155 end
@@ -1,90 +1,90
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 'redmine/scm/adapters/darcs_adapter'
19 19
20 20 class Repository::Darcs < Repository
21 21 validates_presence_of :url
22 22
23 23 def scm_adapter
24 24 Redmine::Scm::Adapters::DarcsAdapter
25 25 end
26 26
27 27 def self.scm_name
28 28 'Darcs'
29 29 end
30 30
31 31 def entries(path=nil, identifier=nil)
32 32 patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
33 33 entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
34 34 if entries
35 35 entries.each do |entry|
36 36 # Search the DB for the entry's last change
37 37 changeset = changesets.find_by_scmid(entry.lastrev.scmid) if entry.lastrev && !entry.lastrev.scmid.blank?
38 38 if changeset
39 39 entry.lastrev.identifier = changeset.revision
40 40 entry.lastrev.name = changeset.revision
41 41 entry.lastrev.time = changeset.committed_on
42 42 entry.lastrev.author = changeset.committer
43 43 end
44 44 end
45 45 end
46 46 entries
47 47 end
48 48
49 def diff(path, rev, rev_to, type)
49 def diff(path, rev, rev_to)
50 50 patch_from = changesets.find_by_revision(rev)
51 51 return nil if patch_from.nil?
52 52 patch_to = changesets.find_by_revision(rev_to) if rev_to
53 53 if path.blank?
54 54 path = patch_from.changes.collect{|change| change.path}.join(' ')
55 55 end
56 patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil, type) : nil
56 patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil
57 57 end
58 58
59 59 def fetch_changesets
60 60 scm_info = scm.info
61 61 if scm_info
62 62 db_last_id = latest_changeset ? latest_changeset.scmid : nil
63 63 next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
64 64 # latest revision in the repository
65 65 scm_revision = scm_info.lastrev.scmid
66 66 unless changesets.find_by_scmid(scm_revision)
67 67 revisions = scm.revisions('', db_last_id, nil, :with_path => true)
68 68 transaction do
69 69 revisions.reverse_each do |revision|
70 70 changeset = Changeset.create(:repository => self,
71 71 :revision => next_rev,
72 72 :scmid => revision.scmid,
73 73 :committer => revision.author,
74 74 :committed_on => revision.time,
75 75 :comments => revision.message)
76 76
77 77 revision.paths.each do |change|
78 78 Change.create(:changeset => changeset,
79 79 :action => change[:action],
80 80 :path => change[:path],
81 81 :from_path => change[:from_path],
82 82 :from_revision => change[:from_revision])
83 83 end
84 84 next_rev += 1
85 85 end if revisions
86 86 end
87 87 end
88 88 end
89 89 end
90 90 end
@@ -1,91 +1,91
1 1 <h2><%= l(:label_revision) %> <%= format_revision(@rev) %> <%= @path.gsub(/^.*\//, '') %></h2>
2 2
3 3 <!-- Choose view type -->
4 4 <% form_tag({ :controller => 'repositories', :action => 'diff'}, :method => 'get') do %>
5 5 <% params.each do |k, p| %>
6 6 <% if k != "type" %>
7 7 <%= hidden_field_tag(k,p) %>
8 8 <% end %>
9 9 <% end %>
10 10 <p><label><%= l(:label_view_diff) %></label>
11 11 <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p>
12 12 <% end %>
13 13
14 14 <% cache(@cache_key) do -%>
15 <% @diff.each do |table_file| -%>
15 <% Redmine::UnifiedDiff.new(@diff, @diff_type).each do |table_file| -%>
16 16 <div class="autoscroll">
17 17 <% if @diff_type == 'sbs' -%>
18 18 <table class="filecontent CodeRay">
19 19 <thead>
20 20 <tr><th colspan="4" class="filename"><%= table_file.file_name %></th></tr>
21 21 <tr>
22 22 <th colspan="2">@<%= format_revision @rev %></th>
23 23 <th colspan="2">@<%= format_revision @rev_to %></th>
24 24 </tr>
25 25 </thead>
26 26 <tbody>
27 27 <% prev_line_left, prev_line_right = nil, nil -%>
28 28 <% table_file.keys.sort.each do |key| -%>
29 29 <% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%>
30 30 <tr class="spacing"><td colspan="4"></td></tr>
31 31 <% end -%>
32 32 <tr>
33 33 <th class="line-num"><%= table_file[key].nb_line_left %></th>
34 34 <td class="line-code <%= table_file[key].type_diff_left %>">
35 35 <pre><%=to_utf8 table_file[key].line_left %></pre>
36 36 </td>
37 37 <th class="line-num"><%= table_file[key].nb_line_right %></th>
38 38 <td class="line-code <%= table_file[key].type_diff_right %>">
39 39 <pre><%=to_utf8 table_file[key].line_right %></pre>
40 40 </td>
41 41 </tr>
42 42 <% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%>
43 43 <% end -%>
44 44 </tbody>
45 45 </table>
46 46
47 47 <% else -%>
48 48 <table class="filecontent CodeRay">
49 49 <thead>
50 50 <tr><th colspan="3" class="filename"><%= table_file.file_name %></th></tr>
51 51 <tr>
52 52 <th>@<%= format_revision @rev %></th>
53 53 <th>@<%= format_revision @rev_to %></th>
54 54 <th></th>
55 55 </tr>
56 56 </thead>
57 57 <tbody>
58 58 <% prev_line_left, prev_line_right = nil, nil -%>
59 59 <% table_file.keys.sort.each do |key, line| %>
60 60 <% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%>
61 61 <tr class="spacing"><td colspan="3"></td></tr>
62 62 <% end -%>
63 63 <tr>
64 64 <th class="line-num"><%= table_file[key].nb_line_left %></th>
65 65 <th class="line-num"><%= table_file[key].nb_line_right %></th>
66 66 <% if table_file[key].line_left.empty? -%>
67 67 <td class="line-code <%= table_file[key].type_diff_right %>">
68 68 <pre><%=to_utf8 table_file[key].line_right %></pre>
69 69 </td>
70 70 <% else -%>
71 71 <td class="line-code <%= table_file[key].type_diff_left %>">
72 72 <pre><%=to_utf8 table_file[key].line_left %></pre>
73 73 </td>
74 74 <% end -%>
75 75 </tr>
76 76 <% prev_line_left = table_file[key].nb_line_left.to_i if table_file[key].nb_line_left.to_i > 0 -%>
77 77 <% prev_line_right = table_file[key].nb_line_right.to_i if table_file[key].nb_line_right.to_i > 0 -%>
78 78 <% end -%>
79 79 </tbody>
80 80 </table>
81 81 <% end -%>
82 82
83 83 </div>
84 84 <% end -%>
85 85 <% end -%>
86 86
87 87 <% html_title(with_leading_slash(@path), 'Diff') -%>
88 88
89 89 <% content_for :header_tags do %>
90 90 <%= stylesheet_link_tag "scm" %>
91 91 <% end %>
@@ -1,420 +1,261
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 def initialize(url, root_url=nil, login=nil, password=nil)
28 28 @url = url
29 29 @login = login if login && !login.empty?
30 30 @password = (password || "") if @login
31 31 @root_url = root_url.blank? ? retrieve_root_url : root_url
32 32 end
33 33
34 34 def adapter_name
35 35 'Abstract'
36 36 end
37 37
38 38 def supports_cat?
39 39 true
40 40 end
41 41
42 42 def supports_annotate?
43 43 respond_to?('annotate')
44 44 end
45 45
46 46 def root_url
47 47 @root_url
48 48 end
49 49
50 50 def url
51 51 @url
52 52 end
53 53
54 54 # get info about the svn repository
55 55 def info
56 56 return nil
57 57 end
58 58
59 59 # Returns the entry identified by path and revision identifier
60 60 # or nil if entry doesn't exist in the repository
61 61 def entry(path=nil, identifier=nil)
62 62 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
63 63 search_path = parts[0..-2].join('/')
64 64 search_name = parts[-1]
65 65 if search_path.blank? && search_name.blank?
66 66 # Root entry
67 67 Entry.new(:path => '', :kind => 'dir')
68 68 else
69 69 # Search for the entry in the parent directory
70 70 es = entries(search_path, identifier)
71 71 es ? es.detect {|e| e.name == search_name} : nil
72 72 end
73 73 end
74 74
75 75 # Returns an Entries collection
76 76 # or nil if the given path doesn't exist in the repository
77 77 def entries(path=nil, identifier=nil)
78 78 return nil
79 79 end
80 80
81 81 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
82 82 return nil
83 83 end
84 84
85 def diff(path, identifier_from, identifier_to=nil, type="inline")
85 def diff(path, identifier_from, identifier_to=nil)
86 86 return nil
87 87 end
88 88
89 89 def cat(path, identifier=nil)
90 90 return nil
91 91 end
92 92
93 93 def with_leading_slash(path)
94 94 path ||= ''
95 95 (path[0,1]!="/") ? "/#{path}" : path
96 96 end
97 97
98 98 def with_trailling_slash(path)
99 99 path ||= ''
100 100 (path[-1,1] == "/") ? path : "#{path}/"
101 101 end
102 102
103 103 def without_leading_slash(path)
104 104 path ||= ''
105 105 path.gsub(%r{^/+}, '')
106 106 end
107 107
108 108 def without_trailling_slash(path)
109 109 path ||= ''
110 110 (path[-1,1] == "/") ? path[0..-2] : path
111 111 end
112 112
113 113 def shell_quote(str)
114 114 if RUBY_PLATFORM =~ /mswin/
115 115 '"' + str.gsub(/"/, '\\"') + '"'
116 116 else
117 117 "'" + str.gsub(/'/, "'\"'\"'") + "'"
118 118 end
119 119 end
120 120
121 121 private
122 122 def retrieve_root_url
123 123 info = self.info
124 124 info ? info.root_url : nil
125 125 end
126 126
127 127 def target(path)
128 128 path ||= ''
129 129 base = path.match(/^\//) ? root_url : url
130 130 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
131 131 end
132 132
133 133 def logger
134 134 RAILS_DEFAULT_LOGGER
135 135 end
136 136
137 137 def shellout(cmd, &block)
138 138 logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
139 139 begin
140 140 IO.popen(cmd, "r+") do |io|
141 141 io.close_write
142 142 block.call(io) if block_given?
143 143 end
144 144 rescue Errno::ENOENT => e
145 145 msg = strip_credential(e.message)
146 146 # The command failed, log it and re-raise
147 147 logger.error("SCM command failed: #{strip_credential(cmd)}\n with: #{msg}")
148 148 raise CommandFailed.new(msg)
149 149 end
150 150 end
151 151
152 152 # Hides username/password in a given command
153 153 def self.hide_credential(cmd)
154 154 q = (RUBY_PLATFORM =~ /mswin/ ? '"' : "'")
155 155 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
156 156 end
157 157
158 158 def strip_credential(cmd)
159 159 self.class.hide_credential(cmd)
160 160 end
161 161 end
162 162
163 163 class Entries < Array
164 164 def sort_by_name
165 165 sort {|x,y|
166 166 if x.kind == y.kind
167 167 x.name <=> y.name
168 168 else
169 169 x.kind <=> y.kind
170 170 end
171 171 }
172 172 end
173 173
174 174 def revisions
175 175 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
176 176 end
177 177 end
178 178
179 179 class Info
180 180 attr_accessor :root_url, :lastrev
181 181 def initialize(attributes={})
182 182 self.root_url = attributes[:root_url] if attributes[:root_url]
183 183 self.lastrev = attributes[:lastrev]
184 184 end
185 185 end
186 186
187 187 class Entry
188 188 attr_accessor :name, :path, :kind, :size, :lastrev
189 189 def initialize(attributes={})
190 190 self.name = attributes[:name] if attributes[:name]
191 191 self.path = attributes[:path] if attributes[:path]
192 192 self.kind = attributes[:kind] if attributes[:kind]
193 193 self.size = attributes[:size].to_i if attributes[:size]
194 194 self.lastrev = attributes[:lastrev]
195 195 end
196 196
197 197 def is_file?
198 198 'file' == self.kind
199 199 end
200 200
201 201 def is_dir?
202 202 'dir' == self.kind
203 203 end
204 204
205 205 def is_text?
206 206 Redmine::MimeType.is_type?('text', name)
207 207 end
208 208 end
209 209
210 210 class Revisions < Array
211 211 def latest
212 212 sort {|x,y|
213 213 unless x.time.nil? or y.time.nil?
214 214 x.time <=> y.time
215 215 else
216 216 0
217 217 end
218 218 }.last
219 219 end
220 220 end
221 221
222 222 class Revision
223 223 attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
224 224 def initialize(attributes={})
225 225 self.identifier = attributes[:identifier]
226 226 self.scmid = attributes[:scmid]
227 227 self.name = attributes[:name] || self.identifier
228 228 self.author = attributes[:author]
229 229 self.time = attributes[:time]
230 230 self.message = attributes[:message] || ""
231 231 self.paths = attributes[:paths]
232 232 self.revision = attributes[:revision]
233 233 self.branch = attributes[:branch]
234 234 end
235 235
236 236 end
237
238 # A line of Diff
239 class Diff
240 attr_accessor :nb_line_left
241 attr_accessor :line_left
242 attr_accessor :nb_line_right
243 attr_accessor :line_right
244 attr_accessor :type_diff_right
245 attr_accessor :type_diff_left
246 237
247 def initialize ()
248 self.nb_line_left = ''
249 self.nb_line_right = ''
250 self.line_left = ''
251 self.line_right = ''
252 self.type_diff_right = ''
253 self.type_diff_left = ''
254 end
255
256 def inspect
257 puts '### Start Line Diff ###'
258 puts self.nb_line_left
259 puts self.line_left
260 puts self.nb_line_right
261 puts self.line_right
262 end
263 end
264
265 class DiffTableList < Array
266 def initialize (diff, type="inline")
267 diff_table = DiffTable.new type
268 diff.each do |line|
269 if line =~ /^(---|\+\+\+) (.*)$/
270 self << diff_table if diff_table.length > 1
271 diff_table = DiffTable.new type
272 end
273 a = diff_table.add_line line
274 end
275 self << diff_table unless diff_table.empty?
276 self
277 end
278 end
279
280 # Class for create a Diff
281 class DiffTable < Hash
282 attr_reader :file_name, :line_num_l, :line_num_r
283
284 # Initialize with a Diff file and the type of Diff View
285 # The type view must be inline or sbs (side_by_side)
286 def initialize(type="inline")
287 @parsing = false
288 @nb_line = 1
289 @start = false
290 @before = 'same'
291 @second = true
292 @type = type
293 end
294
295 # Function for add a line of this Diff
296 def add_line(line)
297 unless @parsing
298 if line =~ /^(---|\+\+\+) (.*)$/
299 @file_name = $2
300 return false
301 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
302 @line_num_l = $5.to_i
303 @line_num_r = $2.to_i
304 @parsing = true
305 end
306 else
307 if line =~ /^[^\+\-\s@\\]/
308 @parsing = false
309 return false
310 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
311 @line_num_l = $5.to_i
312 @line_num_r = $2.to_i
313 else
314 @nb_line += 1 if parse_line(line, @type)
315 end
316 end
317 return true
318 end
319
320 def inspect
321 puts '### DIFF TABLE ###'
322 puts "file : #{file_name}"
323 self.each do |d|
324 d.inspect
325 end
326 end
327
328 private
329 # Test if is a Side By Side type
330 def sbs?(type, func)
331 if @start and type == "sbs"
332 if @before == func and @second
333 tmp_nb_line = @nb_line
334 self[tmp_nb_line] = Diff.new
335 else
336 @second = false
337 tmp_nb_line = @start
338 @start += 1
339 @nb_line -= 1
340 end
341 else
342 tmp_nb_line = @nb_line
343 @start = @nb_line
344 self[tmp_nb_line] = Diff.new
345 @second = true
346 end
347 unless self[tmp_nb_line]
348 @nb_line += 1
349 self[tmp_nb_line] = Diff.new
350 else
351 self[tmp_nb_line]
352 end
353 end
354
355 # Escape the HTML for the diff
356 def escapeHTML(line)
357 CGI.escapeHTML(line)
358 end
359
360 def parse_line(line, type="inline")
361 if line[0, 1] == "+"
362 diff = sbs? type, 'add'
363 @before = 'add'
364 diff.line_left = escapeHTML line[1..-1]
365 diff.nb_line_left = @line_num_l
366 diff.type_diff_left = 'diff_in'
367 @line_num_l += 1
368 true
369 elsif line[0, 1] == "-"
370 diff = sbs? type, 'remove'
371 @before = 'remove'
372 diff.line_right = escapeHTML line[1..-1]
373 diff.nb_line_right = @line_num_r
374 diff.type_diff_right = 'diff_out'
375 @line_num_r += 1
376 true
377 elsif line[0, 1] =~ /\s/
378 @before = 'same'
379 @start = false
380 diff = Diff.new
381 diff.line_right = escapeHTML line[1..-1]
382 diff.nb_line_right = @line_num_r
383 diff.line_left = escapeHTML line[1..-1]
384 diff.nb_line_left = @line_num_l
385 self[@nb_line] = diff
386 @line_num_l += 1
387 @line_num_r += 1
388 true
389 elsif line[0, 1] = "\\"
390 true
391 else
392 false
393 end
394 end
395 end
396
397 238 class Annotate
398 239 attr_reader :lines, :revisions
399 240
400 241 def initialize
401 242 @lines = []
402 243 @revisions = []
403 244 end
404 245
405 246 def add_line(line, revision)
406 247 @lines << line
407 248 @revisions << revision
408 249 end
409 250
410 251 def content
411 252 content = lines.join("\n")
412 253 end
413 254
414 255 def empty?
415 256 lines.empty?
416 257 end
417 258 end
418 259 end
419 260 end
420 261 end
@@ -1,185 +1,185
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 'redmine/scm/adapters/abstract_adapter'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class BazaarAdapter < AbstractAdapter
24 24
25 25 # Bazaar executable name
26 26 BZR_BIN = "bzr"
27 27
28 28 # Get info about the repository
29 29 def info
30 30 cmd = "#{BZR_BIN} revno #{target('')}"
31 31 info = nil
32 32 shellout(cmd) do |io|
33 33 if io.read =~ %r{^(\d+)$}
34 34 info = Info.new({:root_url => url,
35 35 :lastrev => Revision.new({
36 36 :identifier => $1
37 37 })
38 38 })
39 39 end
40 40 end
41 41 return nil if $? && $?.exitstatus != 0
42 42 info
43 43 rescue CommandFailed
44 44 return nil
45 45 end
46 46
47 47 # Returns an Entries collection
48 48 # or nil if the given path doesn't exist in the repository
49 49 def entries(path=nil, identifier=nil)
50 50 path ||= ''
51 51 entries = Entries.new
52 52 cmd = "#{BZR_BIN} ls -v --show-ids"
53 53 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
54 54 cmd << " #{target(path)}"
55 55 shellout(cmd) do |io|
56 56 prefix = "#{url}/#{path}".gsub('\\', '/')
57 57 logger.debug "PREFIX: #{prefix}"
58 58 re = %r{^V\s+#{Regexp.escape(prefix)}(\/?)([^\/]+)(\/?)\s+(\S+)$}
59 59 io.each_line do |line|
60 60 next unless line =~ re
61 61 entries << Entry.new({:name => $2.strip,
62 62 :path => ((path.empty? ? "" : "#{path}/") + $2.strip),
63 63 :kind => ($3.blank? ? 'file' : 'dir'),
64 64 :size => nil,
65 65 :lastrev => Revision.new(:revision => $4.strip)
66 66 })
67 67 end
68 68 end
69 69 return nil if $? && $?.exitstatus != 0
70 70 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
71 71 entries.sort_by_name
72 72 end
73 73
74 74 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
75 75 path ||= ''
76 76 identifier_from = 'last:1' unless identifier_from and identifier_from.to_i > 0
77 77 identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
78 78 revisions = Revisions.new
79 79 cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to.to_i}..#{identifier_from} #{target(path)}"
80 80 shellout(cmd) do |io|
81 81 revision = nil
82 82 parsing = nil
83 83 io.each_line do |line|
84 84 if line =~ /^----/
85 85 revisions << revision if revision
86 86 revision = Revision.new(:paths => [], :message => '')
87 87 parsing = nil
88 88 else
89 89 next unless revision
90 90
91 91 if line =~ /^revno: (\d+)$/
92 92 revision.identifier = $1.to_i
93 93 elsif line =~ /^committer: (.+)$/
94 94 revision.author = $1.strip
95 95 elsif line =~ /^revision-id:(.+)$/
96 96 revision.scmid = $1.strip
97 97 elsif line =~ /^timestamp: (.+)$/
98 98 revision.time = Time.parse($1).localtime
99 99 elsif line =~ /^ -----/
100 100 # partial revisions
101 101 parsing = nil unless parsing == 'message'
102 102 elsif line =~ /^(message|added|modified|removed|renamed):/
103 103 parsing = $1
104 104 elsif line =~ /^ (.*)$/
105 105 if parsing == 'message'
106 106 revision.message << "#{$1}\n"
107 107 else
108 108 if $1 =~ /^(.*)\s+(\S+)$/
109 109 path = $1.strip
110 110 revid = $2
111 111 case parsing
112 112 when 'added'
113 113 revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
114 114 when 'modified'
115 115 revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
116 116 when 'removed'
117 117 revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
118 118 when 'renamed'
119 119 new_path = path.split('=>').last
120 120 revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
121 121 end
122 122 end
123 123 end
124 124 else
125 125 parsing = nil
126 126 end
127 127 end
128 128 end
129 129 revisions << revision if revision
130 130 end
131 131 return nil if $? && $?.exitstatus != 0
132 132 revisions
133 133 end
134 134
135 def diff(path, identifier_from, identifier_to=nil, type="inline")
135 def diff(path, identifier_from, identifier_to=nil)
136 136 path ||= ''
137 137 if identifier_to
138 138 identifier_to = identifier_to.to_i
139 139 else
140 140 identifier_to = identifier_from.to_i - 1
141 141 end
142 142 cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
143 143 diff = []
144 144 shellout(cmd) do |io|
145 145 io.each_line do |line|
146 146 diff << line
147 147 end
148 148 end
149 149 #return nil if $? && $?.exitstatus != 0
150 DiffTableList.new diff, type
150 diff
151 151 end
152 152
153 153 def cat(path, identifier=nil)
154 154 cmd = "#{BZR_BIN} cat"
155 155 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
156 156 cmd << " #{target(path)}"
157 157 cat = nil
158 158 shellout(cmd) do |io|
159 159 io.binmode
160 160 cat = io.read
161 161 end
162 162 return nil if $? && $?.exitstatus != 0
163 163 cat
164 164 end
165 165
166 166 def annotate(path, identifier=nil)
167 167 cmd = "#{BZR_BIN} annotate --all"
168 168 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
169 169 cmd << " #{target(path)}"
170 170 blame = Annotate.new
171 171 shellout(cmd) do |io|
172 172 author = nil
173 173 identifier = nil
174 174 io.each_line do |line|
175 175 next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
176 176 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
177 177 end
178 178 end
179 179 return nil if $? && $?.exitstatus != 0
180 180 blame
181 181 end
182 182 end
183 183 end
184 184 end
185 185 end
@@ -1,354 +1,354
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 'redmine/scm/adapters/abstract_adapter'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class CvsAdapter < AbstractAdapter
24 24
25 25 # CVS executable name
26 26 CVS_BIN = "cvs"
27 27
28 28 # Guidelines for the input:
29 29 # url -> the project-path, relative to the cvsroot (eg. module name)
30 30 # root_url -> the good old, sometimes damned, CVSROOT
31 31 # login -> unnecessary
32 32 # password -> unnecessary too
33 33 def initialize(url, root_url=nil, login=nil, password=nil)
34 34 @url = url
35 35 @login = login if login && !login.empty?
36 36 @password = (password || "") if @login
37 37 #TODO: better Exception here (IllegalArgumentException)
38 38 raise CommandFailed if root_url.blank?
39 39 @root_url = root_url
40 40 end
41 41
42 42 def root_url
43 43 @root_url
44 44 end
45 45
46 46 def url
47 47 @url
48 48 end
49 49
50 50 def info
51 51 logger.debug "<cvs> info"
52 52 Info.new({:root_url => @root_url, :lastrev => nil})
53 53 end
54 54
55 55 def get_previous_revision(revision)
56 56 CvsRevisionHelper.new(revision).prevRev
57 57 end
58 58
59 59 # Returns an Entries collection
60 60 # or nil if the given path doesn't exist in the repository
61 61 # this method is used by the repository-browser (aka LIST)
62 62 def entries(path=nil, identifier=nil)
63 63 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
64 64 path_with_project="#{url}#{with_leading_slash(path)}"
65 65 entries = Entries.new
66 66 cmd = "#{CVS_BIN} -d #{root_url} rls -ed"
67 67 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
68 68 cmd << " #{path_with_project}"
69 69 shellout(cmd) do |io|
70 70 io.each_line(){|line|
71 71 fields=line.chop.split('/',-1)
72 72 logger.debug(">>InspectLine #{fields.inspect}")
73 73
74 74 if fields[0]!="D"
75 75 entries << Entry.new({:name => fields[-5],
76 76 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
77 77 :path => "#{path}/#{fields[-5]}",
78 78 :kind => 'file',
79 79 :size => nil,
80 80 :lastrev => Revision.new({
81 81 :revision => fields[-4],
82 82 :name => fields[-4],
83 83 :time => Time.parse(fields[-3]),
84 84 :author => ''
85 85 })
86 86 })
87 87 else
88 88 entries << Entry.new({:name => fields[1],
89 89 :path => "#{path}/#{fields[1]}",
90 90 :kind => 'dir',
91 91 :size => nil,
92 92 :lastrev => nil
93 93 })
94 94 end
95 95 }
96 96 end
97 97 return nil if $? && $?.exitstatus != 0
98 98 entries.sort_by_name
99 99 end
100 100
101 101 STARTLOG="----------------------------"
102 102 ENDLOG ="============================================================================="
103 103
104 104 # Returns all revisions found between identifier_from and identifier_to
105 105 # in the repository. both identifier have to be dates or nil.
106 106 # these method returns nothing but yield every result in block
107 107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
108 108 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
109 109
110 110 path_with_project="#{url}#{with_leading_slash(path)}"
111 111 cmd = "#{CVS_BIN} -d #{root_url} rlog"
112 112 cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
113 113 cmd << " #{path_with_project}"
114 114 shellout(cmd) do |io|
115 115 state="entry_start"
116 116
117 117 commit_log=String.new
118 118 revision=nil
119 119 date=nil
120 120 author=nil
121 121 entry_path=nil
122 122 entry_name=nil
123 123 file_state=nil
124 124 branch_map=nil
125 125
126 126 io.each_line() do |line|
127 127
128 128 if state!="revision" && /^#{ENDLOG}/ =~ line
129 129 commit_log=String.new
130 130 revision=nil
131 131 state="entry_start"
132 132 end
133 133
134 134 if state=="entry_start"
135 135 branch_map=Hash.new
136 136 # gsub(/^:.*@[^:]+:\d*/, '') is here to remove :pserver:anonymous@foo.bar: string if present in the url
137 137 if /^RCS file: #{Regexp.escape(root_url.gsub(/^:.*@[^:]+:\d*/, ''))}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
138 138 entry_path = normalize_cvs_path($1)
139 139 entry_name = normalize_path(File.basename($1))
140 140 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
141 141 elsif /^head: (.+)$/ =~ line
142 142 entry_headRev = $1 #unless entry.nil?
143 143 elsif /^symbolic names:/ =~ line
144 144 state="symbolic" #unless entry.nil?
145 145 elsif /^#{STARTLOG}/ =~ line
146 146 commit_log=String.new
147 147 state="revision"
148 148 end
149 149 next
150 150 elsif state=="symbolic"
151 151 if /^(.*):\s(.*)/ =~ (line.strip)
152 152 branch_map[$1]=$2
153 153 else
154 154 state="tags"
155 155 next
156 156 end
157 157 elsif state=="tags"
158 158 if /^#{STARTLOG}/ =~ line
159 159 commit_log = ""
160 160 state="revision"
161 161 elsif /^#{ENDLOG}/ =~ line
162 162 state="head"
163 163 end
164 164 next
165 165 elsif state=="revision"
166 166 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
167 167 if revision
168 168
169 169 revHelper=CvsRevisionHelper.new(revision)
170 170 revBranch="HEAD"
171 171
172 172 branch_map.each() do |branch_name,branch_point|
173 173 if revHelper.is_in_branch_with_symbol(branch_point)
174 174 revBranch=branch_name
175 175 end
176 176 end
177 177
178 178 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
179 179
180 180 yield Revision.new({
181 181 :time => date,
182 182 :author => author,
183 183 :message=>commit_log.chomp,
184 184 :paths => [{
185 185 :revision => revision,
186 186 :branch=> revBranch,
187 187 :path=>entry_path,
188 188 :name=>entry_name,
189 189 :kind=>'file',
190 190 :action=>file_state
191 191 }]
192 192 })
193 193 end
194 194
195 195 commit_log=String.new
196 196 revision=nil
197 197
198 198 if /^#{ENDLOG}/ =~ line
199 199 state="entry_start"
200 200 end
201 201 next
202 202 end
203 203
204 204 if /^branches: (.+)$/ =~ line
205 205 #TODO: version.branch = $1
206 206 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
207 207 revision = $1
208 208 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
209 209 date = Time.parse($1)
210 210 author = /author: ([^;]+)/.match(line)[1]
211 211 file_state = /state: ([^;]+)/.match(line)[1]
212 212 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
213 213 # useful for stats or something else
214 214 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
215 215 # unless linechanges.nil?
216 216 # version.line_plus = linechanges[1]
217 217 # version.line_minus = linechanges[2]
218 218 # else
219 219 # version.line_plus = 0
220 220 # version.line_minus = 0
221 221 # end
222 222 else
223 223 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
224 224 end
225 225 end
226 226 end
227 227 end
228 228 end
229 229
230 def diff(path, identifier_from, identifier_to=nil, type="inline")
230 def diff(path, identifier_from, identifier_to=nil)
231 231 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
232 232 path_with_project="#{url}#{with_leading_slash(path)}"
233 233 cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{path_with_project}"
234 234 diff = []
235 235 shellout(cmd) do |io|
236 236 io.each_line do |line|
237 237 diff << line
238 238 end
239 239 end
240 240 return nil if $? && $?.exitstatus != 0
241 DiffTableList.new diff, type
241 diff
242 242 end
243 243
244 244 def cat(path, identifier=nil)
245 245 identifier = (identifier) ? identifier : "HEAD"
246 246 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
247 247 path_with_project="#{url}#{with_leading_slash(path)}"
248 248 cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{path_with_project}"
249 249 cat = nil
250 250 shellout(cmd) do |io|
251 251 cat = io.read
252 252 end
253 253 return nil if $? && $?.exitstatus != 0
254 254 cat
255 255 end
256 256
257 257 def annotate(path, identifier=nil)
258 258 identifier = (identifier) ? identifier : "HEAD"
259 259 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
260 260 path_with_project="#{url}#{with_leading_slash(path)}"
261 261 cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{path_with_project}"
262 262 blame = Annotate.new
263 263 shellout(cmd) do |io|
264 264 io.each_line do |line|
265 265 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
266 266 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
267 267 end
268 268 end
269 269 return nil if $? && $?.exitstatus != 0
270 270 blame
271 271 end
272 272
273 273 private
274 274
275 275 # convert a date/time into the CVS-format
276 276 def time_to_cvstime(time)
277 277 return nil if time.nil?
278 278 unless time.kind_of? Time
279 279 time = Time.parse(time)
280 280 end
281 281 return time.strftime("%Y-%m-%d %H:%M:%S")
282 282 end
283 283
284 284 def normalize_cvs_path(path)
285 285 normalize_path(path.gsub(/Attic\//,''))
286 286 end
287 287
288 288 def normalize_path(path)
289 289 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
290 290 end
291 291 end
292 292
293 293 class CvsRevisionHelper
294 294 attr_accessor :complete_rev, :revision, :base, :branchid
295 295
296 296 def initialize(complete_rev)
297 297 @complete_rev = complete_rev
298 298 parseRevision()
299 299 end
300 300
301 301 def branchPoint
302 302 return @base
303 303 end
304 304
305 305 def branchVersion
306 306 if isBranchRevision
307 307 return @base+"."+@branchid
308 308 end
309 309 return @base
310 310 end
311 311
312 312 def isBranchRevision
313 313 !@branchid.nil?
314 314 end
315 315
316 316 def prevRev
317 317 unless @revision==0
318 318 return buildRevision(@revision-1)
319 319 end
320 320 return buildRevision(@revision)
321 321 end
322 322
323 323 def is_in_branch_with_symbol(branch_symbol)
324 324 bpieces=branch_symbol.split(".")
325 325 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
326 326 return (branchVersion==branch_start)
327 327 end
328 328
329 329 private
330 330 def buildRevision(rev)
331 331 if rev== 0
332 332 @base
333 333 elsif @branchid.nil?
334 334 @base+"."+rev.to_s
335 335 else
336 336 @base+"."+@branchid+"."+rev.to_s
337 337 end
338 338 end
339 339
340 340 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
341 341 def parseRevision()
342 342 pieces=@complete_rev.split(".")
343 343 @revision=pieces.last.to_i
344 344 baseSize=1
345 345 baseSize+=(pieces.size/2)
346 346 @base=pieces[0..-baseSize].join(".")
347 347 if baseSize > 2
348 348 @branchid=pieces[-2]
349 349 end
350 350 end
351 351 end
352 352 end
353 353 end
354 354 end
@@ -1,156 +1,156
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 'redmine/scm/adapters/abstract_adapter'
19 19 require 'rexml/document'
20 20
21 21 module Redmine
22 22 module Scm
23 23 module Adapters
24 24 class DarcsAdapter < AbstractAdapter
25 25 # Darcs executable name
26 26 DARCS_BIN = "darcs"
27 27
28 28 def initialize(url, root_url=nil, login=nil, password=nil)
29 29 @url = url
30 30 @root_url = url
31 31 end
32 32
33 33 def supports_cat?
34 34 false
35 35 end
36 36
37 37 # Get info about the svn repository
38 38 def info
39 39 rev = revisions(nil,nil,nil,{:limit => 1})
40 40 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
41 41 end
42 42
43 43 # Returns an Entries collection
44 44 # or nil if the given path doesn't exist in the repository
45 45 def entries(path=nil, identifier=nil)
46 46 path_prefix = (path.blank? ? '' : "#{path}/")
47 47 path = '.' if path.blank?
48 48 entries = Entries.new
49 49 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output"
50 50 cmd << " --match \"hash #{identifier}\"" if identifier
51 51 cmd << " #{path}"
52 52 shellout(cmd) do |io|
53 53 begin
54 54 doc = REXML::Document.new(io)
55 55 if doc.root.name == 'directory'
56 56 doc.elements.each('directory/*') do |element|
57 57 next unless ['file', 'directory'].include? element.name
58 58 entries << entry_from_xml(element, path_prefix)
59 59 end
60 60 elsif doc.root.name == 'file'
61 61 entries << entry_from_xml(doc.root, path_prefix)
62 62 end
63 63 rescue
64 64 end
65 65 end
66 66 return nil if $? && $?.exitstatus != 0
67 67 entries.sort_by_name
68 68 end
69 69
70 70 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
71 71 path = '.' if path.blank?
72 72 revisions = Revisions.new
73 73 cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output"
74 74 cmd << " --from-match \"hash #{identifier_from}\"" if identifier_from
75 75 cmd << " --last #{options[:limit].to_i}" if options[:limit]
76 76 shellout(cmd) do |io|
77 77 begin
78 78 doc = REXML::Document.new(io)
79 79 doc.elements.each("changelog/patch") do |patch|
80 80 message = patch.elements['name'].text
81 81 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
82 82 revisions << Revision.new({:identifier => nil,
83 83 :author => patch.attributes['author'],
84 84 :scmid => patch.attributes['hash'],
85 85 :time => Time.parse(patch.attributes['local_date']),
86 86 :message => message,
87 87 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
88 88 })
89 89 end
90 90 rescue
91 91 end
92 92 end
93 93 return nil if $? && $?.exitstatus != 0
94 94 revisions
95 95 end
96 96
97 def diff(path, identifier_from, identifier_to=nil, type="inline")
97 def diff(path, identifier_from, identifier_to=nil)
98 98 path = '*' if path.blank?
99 99 cmd = "#{DARCS_BIN} diff --repodir #{@url}"
100 100 if identifier_to.nil?
101 101 cmd << " --match \"hash #{identifier_from}\""
102 102 else
103 103 cmd << " --to-match \"hash #{identifier_from}\""
104 104 cmd << " --from-match \"hash #{identifier_to}\""
105 105 end
106 106 cmd << " -u #{path}"
107 107 diff = []
108 108 shellout(cmd) do |io|
109 109 io.each_line do |line|
110 110 diff << line
111 111 end
112 112 end
113 113 return nil if $? && $?.exitstatus != 0
114 DiffTableList.new diff, type
114 diff
115 115 end
116 116
117 117 private
118 118
119 119 def entry_from_xml(element, path_prefix)
120 120 Entry.new({:name => element.attributes['name'],
121 121 :path => path_prefix + element.attributes['name'],
122 122 :kind => element.name == 'file' ? 'file' : 'dir',
123 123 :size => nil,
124 124 :lastrev => Revision.new({
125 125 :identifier => nil,
126 126 :scmid => element.elements['modified'].elements['patch'].attributes['hash']
127 127 })
128 128 })
129 129 end
130 130
131 131 # Retrieve changed paths for a single patch
132 132 def get_paths_for_patch(hash)
133 133 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --summary --xml-output"
134 134 cmd << " --match \"hash #{hash}\" "
135 135 paths = []
136 136 shellout(cmd) do |io|
137 137 begin
138 138 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
139 139 # A root element is added so that REXML doesn't raise an error
140 140 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
141 141 doc.elements.each('fake_root/summary/*') do |modif|
142 142 paths << {:action => modif.name[0,1].upcase,
143 143 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
144 144 }
145 145 end
146 146 rescue
147 147 end
148 148 end
149 149 paths
150 150 rescue CommandFailed
151 151 paths
152 152 end
153 153 end
154 154 end
155 155 end
156 156 end
@@ -1,260 +1,260
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 'redmine/scm/adapters/abstract_adapter'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class GitAdapter < AbstractAdapter
24 24
25 25 # Git executable name
26 26 GIT_BIN = "git"
27 27
28 28 # Get the revision of a particuliar file
29 29 def get_rev (rev,path)
30 30
31 31 if rev != 'latest' && !rev.nil?
32 32 cmd="#{GIT_BIN} --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}"
33 33 else
34 34 branch = shellout("#{GIT_BIN} --git-dir #{target('')} branch") { |io| io.grep(/\*/)[0].strip.match(/\* (.*)/)[1] }
35 35 cmd="#{GIT_BIN} --git-dir #{target('')} log -1 #{branch} -- #{shell_quote path}"
36 36 end
37 37 rev=[]
38 38 i=0
39 39 shellout(cmd) do |io|
40 40 files=[]
41 41 changeset = {}
42 42 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
43 43
44 44 io.each_line do |line|
45 45 if line =~ /^commit ([0-9a-f]{40})$/
46 46 key = "commit"
47 47 value = $1
48 48 if (parsing_descr == 1 || parsing_descr == 2)
49 49 parsing_descr = 0
50 50 rev = Revision.new({:identifier => changeset[:commit],
51 51 :scmid => changeset[:commit],
52 52 :author => changeset[:author],
53 53 :time => Time.parse(changeset[:date]),
54 54 :message => changeset[:description],
55 55 :paths => files
56 56 })
57 57 changeset = {}
58 58 files = []
59 59 end
60 60 changeset[:commit] = $1
61 61 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
62 62 key = $1
63 63 value = $2
64 64 if key == "Author"
65 65 changeset[:author] = value
66 66 elsif key == "Date"
67 67 changeset[:date] = value
68 68 end
69 69 elsif (parsing_descr == 0) && line.chomp.to_s == ""
70 70 parsing_descr = 1
71 71 changeset[:description] = ""
72 72 elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
73 73 parsing_descr = 2
74 74 fileaction = $1
75 75 filepath = $2
76 76 files << {:action => fileaction, :path => filepath}
77 77 elsif (parsing_descr == 1) && line.chomp.to_s == ""
78 78 parsing_descr = 2
79 79 elsif (parsing_descr == 1)
80 80 changeset[:description] << line
81 81 end
82 82 end
83 83 rev = Revision.new({:identifier => changeset[:commit],
84 84 :scmid => changeset[:commit],
85 85 :author => changeset[:author],
86 86 :time => (changeset[:date] ? Time.parse(changeset[:date]) : nil),
87 87 :message => changeset[:description],
88 88 :paths => files
89 89 })
90 90
91 91 end
92 92
93 93 get_rev('latest',path) if rev == []
94 94
95 95 return nil if $? && $?.exitstatus != 0
96 96 return rev
97 97 end
98 98
99 99
100 100 def info
101 101 revs = revisions(url,nil,nil,{:limit => 1})
102 102 if revs && revs.any?
103 103 Info.new(:root_url => url, :lastrev => revs.first)
104 104 else
105 105 nil
106 106 end
107 107 rescue Errno::ENOENT => e
108 108 return nil
109 109 end
110 110
111 111 def entries(path=nil, identifier=nil)
112 112 path ||= ''
113 113 entries = Entries.new
114 114 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
115 115 cmd << shell_quote("HEAD:" + path) if identifier.nil?
116 116 cmd << shell_quote(identifier + ":" + path) if identifier
117 117 shellout(cmd) do |io|
118 118 io.each_line do |line|
119 119 e = line.chomp.to_s
120 120 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/
121 121 type = $1
122 122 sha = $2
123 123 size = $3
124 124 name = $4
125 125 entries << Entry.new({:name => name,
126 126 :path => (path.empty? ? name : "#{path}/#{name}"),
127 127 :kind => ((type == "tree") ? 'dir' : 'file'),
128 128 :size => ((type == "tree") ? nil : size),
129 129 :lastrev => get_rev(identifier,(path.empty? ? name : "#{path}/#{name}"))
130 130
131 131 }) unless entries.detect{|entry| entry.name == name}
132 132 end
133 133 end
134 134 end
135 135 return nil if $? && $?.exitstatus != 0
136 136 entries.sort_by_name
137 137 end
138 138
139 139 def revisions(path, identifier_from, identifier_to, options={})
140 140 revisions = Revisions.new
141 141 cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw "
142 142 cmd << " -n #{options[:limit].to_i} " if (!options.nil?) && options[:limit]
143 143 cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
144 144 cmd << " #{shell_quote identifier_to} " if identifier_to
145 145 #cmd << " HEAD " if !identifier_to
146 146 shellout(cmd) do |io|
147 147 files=[]
148 148 changeset = {}
149 149 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
150 150 revno = 1
151 151
152 152 io.each_line do |line|
153 153 if line =~ /^commit ([0-9a-f]{40})$/
154 154 key = "commit"
155 155 value = $1
156 156 if (parsing_descr == 1 || parsing_descr == 2)
157 157 parsing_descr = 0
158 158 revisions << Revision.new({:identifier => changeset[:commit],
159 159 :scmid => changeset[:commit],
160 160 :author => changeset[:author],
161 161 :time => Time.parse(changeset[:date]),
162 162 :message => changeset[:description],
163 163 :paths => files
164 164 })
165 165 changeset = {}
166 166 files = []
167 167 revno = revno + 1
168 168 end
169 169 changeset[:commit] = $1
170 170 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
171 171 key = $1
172 172 value = $2
173 173 if key == "Author"
174 174 changeset[:author] = value
175 175 elsif key == "Date"
176 176 changeset[:date] = value
177 177 end
178 178 elsif (parsing_descr == 0) && line.chomp.to_s == ""
179 179 parsing_descr = 1
180 180 changeset[:description] = ""
181 181 elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
182 182 parsing_descr = 2
183 183 fileaction = $1
184 184 filepath = $2
185 185 files << {:action => fileaction, :path => filepath}
186 186 elsif (parsing_descr == 1) && line.chomp.to_s == ""
187 187 parsing_descr = 2
188 188 elsif (parsing_descr == 1)
189 189 changeset[:description] << line[4..-1]
190 190 end
191 191 end
192 192
193 193 revisions << Revision.new({:identifier => changeset[:commit],
194 194 :scmid => changeset[:commit],
195 195 :author => changeset[:author],
196 196 :time => Time.parse(changeset[:date]),
197 197 :message => changeset[:description],
198 198 :paths => files
199 199 }) if changeset[:commit]
200 200
201 201 end
202 202
203 203 return nil if $? && $?.exitstatus != 0
204 204 revisions
205 205 end
206 206
207 def diff(path, identifier_from, identifier_to=nil, type="inline")
207 def diff(path, identifier_from, identifier_to=nil)
208 208 path ||= ''
209 209 if !identifier_to
210 210 identifier_to = nil
211 211 end
212 212
213 213 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}" if identifier_to.nil?
214 214 cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}" if !identifier_to.nil?
215 215 cmd << " -- #{shell_quote path}" unless path.empty?
216 216 diff = []
217 217 shellout(cmd) do |io|
218 218 io.each_line do |line|
219 219 diff << line
220 220 end
221 221 end
222 222 return nil if $? && $?.exitstatus != 0
223 DiffTableList.new diff, type
223 diff
224 224 end
225 225
226 226 def annotate(path, identifier=nil)
227 227 identifier = 'HEAD' if identifier.blank?
228 228 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{shell_quote identifier} -- #{shell_quote path}"
229 229 blame = Annotate.new
230 230 content = nil
231 231 shellout(cmd) { |io| io.binmode; content = io.read }
232 232 return nil if $? && $?.exitstatus != 0
233 233 # git annotates binary files
234 234 return nil if content.is_binary_data?
235 235 content.split("\n").each do |line|
236 236 next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)/
237 237 blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip))
238 238 end
239 239 blame
240 240 end
241 241
242 242 def cat(path, identifier=nil)
243 243 if identifier.nil?
244 244 identifier = 'HEAD'
245 245 end
246 246 cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}"
247 247 cat = nil
248 248 shellout(cmd) do |io|
249 249 io.binmode
250 250 cat = io.read
251 251 end
252 252 return nil if $? && $?.exitstatus != 0
253 253 cat
254 254 end
255 255 end
256 256 end
257 257 end
258 258
259 259 end
260 260
@@ -1,203 +1,203
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 'redmine/scm/adapters/abstract_adapter'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class MercurialAdapter < AbstractAdapter
24 24
25 25 # Mercurial executable name
26 26 HG_BIN = "hg"
27 27 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 28 TEMPLATE_NAME = "hg-template"
29 29 TEMPLATE_EXTENSION = "tmpl"
30 30
31 31 def info
32 32 cmd = "#{HG_BIN} -R #{target('')} root"
33 33 root_url = nil
34 34 shellout(cmd) do |io|
35 35 root_url = io.gets
36 36 end
37 37 return nil if $? && $?.exitstatus != 0
38 38 info = Info.new({:root_url => root_url.chomp,
39 39 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
40 40 })
41 41 info
42 42 rescue CommandFailed
43 43 return nil
44 44 end
45 45
46 46 def entries(path=nil, identifier=nil)
47 47 path ||= ''
48 48 entries = Entries.new
49 49 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
50 50 cmd << " -r " + (identifier ? identifier.to_s : "tip")
51 51 cmd << " " + shell_quote("path:#{path}") unless path.empty?
52 52 shellout(cmd) do |io|
53 53 io.each_line do |line|
54 54 # HG uses antislashs as separator on Windows
55 55 line = line.gsub(/\\/, "/")
56 56 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
57 57 e ||= line
58 58 e = e.chomp.split(%r{[\/\\]})
59 59 entries << Entry.new({:name => e.first,
60 60 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
61 61 :kind => (e.size > 1 ? 'dir' : 'file'),
62 62 :lastrev => Revision.new
63 63 }) unless entries.detect{|entry| entry.name == e.first}
64 64 end
65 65 end
66 66 end
67 67 return nil if $? && $?.exitstatus != 0
68 68 entries.sort_by_name
69 69 end
70 70
71 71 # Fetch the revisions by using a template file that
72 72 # makes Mercurial produce a xml output.
73 73 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
74 74 revisions = Revisions.new
75 75 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{self.template_path}"
76 76 if identifier_from && identifier_to
77 77 cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
78 78 elsif identifier_from
79 79 cmd << " -r #{identifier_from.to_i}:"
80 80 end
81 81 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
82 82 cmd << " #{path}" if path
83 83 shellout(cmd) do |io|
84 84 begin
85 85 # HG doesn't close the XML Document...
86 86 doc = REXML::Document.new(io.read << "</log>")
87 87 doc.elements.each("log/logentry") do |logentry|
88 88 paths = []
89 89 copies = logentry.get_elements('paths/path-copied')
90 90 logentry.elements.each("paths/path") do |path|
91 91 # Detect if the added file is a copy
92 92 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
93 93 from_path = c.attributes['copyfrom-path']
94 94 from_rev = logentry.attributes['revision']
95 95 end
96 96 paths << {:action => path.attributes['action'],
97 97 :path => "/#{path.text}",
98 98 :from_path => from_path ? "/#{from_path}" : nil,
99 99 :from_revision => from_rev ? from_rev : nil
100 100 }
101 101 end
102 102 paths.sort! { |x,y| x[:path] <=> y[:path] }
103 103
104 104 revisions << Revision.new({:identifier => logentry.attributes['revision'],
105 105 :scmid => logentry.attributes['node'],
106 106 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
107 107 :time => Time.parse(logentry.elements['date'].text).localtime,
108 108 :message => logentry.elements['msg'].text,
109 109 :paths => paths
110 110 })
111 111 end
112 112 rescue
113 113 logger.debug($!)
114 114 end
115 115 end
116 116 return nil if $? && $?.exitstatus != 0
117 117 revisions
118 118 end
119 119
120 def diff(path, identifier_from, identifier_to=nil, type="inline")
120 def diff(path, identifier_from, identifier_to=nil)
121 121 path ||= ''
122 122 if identifier_to
123 123 identifier_to = identifier_to.to_i
124 124 else
125 125 identifier_to = identifier_from.to_i - 1
126 126 end
127 127 cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
128 128 cmd << " -I #{target(path)}" unless path.empty?
129 129 diff = []
130 130 shellout(cmd) do |io|
131 131 io.each_line do |line|
132 132 diff << line
133 133 end
134 134 end
135 135 return nil if $? && $?.exitstatus != 0
136 DiffTableList.new diff, type
136 diff
137 137 end
138 138
139 139 def cat(path, identifier=nil)
140 140 cmd = "#{HG_BIN} -R #{target('')} cat"
141 141 cmd << " -r " + (identifier ? identifier.to_s : "tip")
142 142 cmd << " #{target(path)}"
143 143 cat = nil
144 144 shellout(cmd) do |io|
145 145 io.binmode
146 146 cat = io.read
147 147 end
148 148 return nil if $? && $?.exitstatus != 0
149 149 cat
150 150 end
151 151
152 152 def annotate(path, identifier=nil)
153 153 path ||= ''
154 154 cmd = "#{HG_BIN} -R #{target('')}"
155 155 cmd << " annotate -n -u"
156 156 cmd << " -r " + (identifier ? identifier.to_s : "tip")
157 157 cmd << " -r #{identifier.to_i}" if identifier
158 158 cmd << " #{target(path)}"
159 159 blame = Annotate.new
160 160 shellout(cmd) do |io|
161 161 io.each_line do |line|
162 162 next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
163 163 blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
164 164 end
165 165 end
166 166 return nil if $? && $?.exitstatus != 0
167 167 blame
168 168 end
169 169
170 170 # The hg version version is expressed either as a
171 171 # release number (eg 0.9.5 or 1.0) or as a revision
172 172 # id composed of 12 hexa characters.
173 173 def hgversion
174 174 theversion = hgversion_from_command_line
175 175 if theversion.match(/^\d+(\.\d+)+/)
176 176 theversion.split(".").collect(&:to_i)
177 177 # elsif match = theversion.match(/[[:xdigit:]]{12}/)
178 178 # match[0]
179 179 else
180 180 "Unknown version"
181 181 end
182 182 end
183 183
184 184 def template_path
185 185 @template ||= begin
186 186 if hgversion.is_a?(String) or ((hgversion <=> [0,9,5]) > 0)
187 187 ver = "1.0"
188 188 else
189 189 ver = "0.9.5"
190 190 end
191 191 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
192 192 end
193 193 end
194 194
195 195 private
196 196
197 197 def hgversion_from_command_line
198 198 @hgversion ||= %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
199 199 end
200 200 end
201 201 end
202 202 end
203 203 end
@@ -1,187 +1,187
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 'redmine/scm/adapters/abstract_adapter'
19 19 require 'rexml/document'
20 20
21 21 module Redmine
22 22 module Scm
23 23 module Adapters
24 24 class SubversionAdapter < AbstractAdapter
25 25
26 26 # SVN executable name
27 27 SVN_BIN = "svn"
28 28
29 29 # Get info about the svn repository
30 30 def info
31 31 cmd = "#{SVN_BIN} info --xml #{target('')}"
32 32 cmd << credentials_string
33 33 info = nil
34 34 shellout(cmd) do |io|
35 35 begin
36 36 doc = REXML::Document.new(io)
37 37 #root_url = doc.elements["info/entry/repository/root"].text
38 38 info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,
39 39 :lastrev => Revision.new({
40 40 :identifier => doc.elements["info/entry/commit"].attributes['revision'],
41 41 :time => Time.parse(doc.elements["info/entry/commit/date"].text).localtime,
42 42 :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")
43 43 })
44 44 })
45 45 rescue
46 46 end
47 47 end
48 48 return nil if $? && $?.exitstatus != 0
49 49 info
50 50 rescue CommandFailed
51 51 return nil
52 52 end
53 53
54 54 # Returns an Entries collection
55 55 # or nil if the given path doesn't exist in the repository
56 56 def entries(path=nil, identifier=nil)
57 57 path ||= ''
58 58 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
59 59 entries = Entries.new
60 60 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
61 61 cmd << credentials_string
62 62 shellout(cmd) do |io|
63 63 output = io.read
64 64 begin
65 65 doc = REXML::Document.new(output)
66 66 doc.elements.each("lists/list/entry") do |entry|
67 67 # Skip directory if there is no commit date (usually that
68 68 # means that we don't have read access to it)
69 69 next if entry.attributes['kind'] == 'dir' && entry.elements['commit'].elements['date'].nil?
70 70 entries << Entry.new({:name => entry.elements['name'].text,
71 71 :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
72 72 :kind => entry.attributes['kind'],
73 73 :size => (entry.elements['size'] and entry.elements['size'].text).to_i,
74 74 :lastrev => Revision.new({
75 75 :identifier => entry.elements['commit'].attributes['revision'],
76 76 :time => Time.parse(entry.elements['commit'].elements['date'].text).localtime,
77 77 :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")
78 78 })
79 79 })
80 80 end
81 81 rescue Exception => e
82 82 logger.error("Error parsing svn output: #{e.message}")
83 83 logger.error("Output was:\n #{output}")
84 84 end
85 85 end
86 86 return nil if $? && $?.exitstatus != 0
87 87 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
88 88 entries.sort_by_name
89 89 end
90 90
91 91 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
92 92 path ||= ''
93 93 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
94 94 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
95 95 revisions = Revisions.new
96 96 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
97 97 cmd << credentials_string
98 98 cmd << " --verbose " if options[:with_paths]
99 99 cmd << ' ' + target(path)
100 100 shellout(cmd) do |io|
101 101 begin
102 102 doc = REXML::Document.new(io)
103 103 doc.elements.each("log/logentry") do |logentry|
104 104 paths = []
105 105 logentry.elements.each("paths/path") do |path|
106 106 paths << {:action => path.attributes['action'],
107 107 :path => path.text,
108 108 :from_path => path.attributes['copyfrom-path'],
109 109 :from_revision => path.attributes['copyfrom-rev']
110 110 }
111 111 end
112 112 paths.sort! { |x,y| x[:path] <=> y[:path] }
113 113
114 114 revisions << Revision.new({:identifier => logentry.attributes['revision'],
115 115 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
116 116 :time => Time.parse(logentry.elements['date'].text).localtime,
117 117 :message => logentry.elements['msg'].text,
118 118 :paths => paths
119 119 })
120 120 end
121 121 rescue
122 122 end
123 123 end
124 124 return nil if $? && $?.exitstatus != 0
125 125 revisions
126 126 end
127 127
128 128 def diff(path, identifier_from, identifier_to=nil, type="inline")
129 129 path ||= ''
130 130 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
131 131 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
132 132
133 133 cmd = "#{SVN_BIN} diff -r "
134 134 cmd << "#{identifier_to}:"
135 135 cmd << "#{identifier_from}"
136 136 cmd << " #{target(path)}@#{identifier_from}"
137 137 cmd << credentials_string
138 138 diff = []
139 139 shellout(cmd) do |io|
140 140 io.each_line do |line|
141 141 diff << line
142 142 end
143 143 end
144 144 return nil if $? && $?.exitstatus != 0
145 DiffTableList.new diff, type
145 diff
146 146 end
147 147
148 148 def cat(path, identifier=nil)
149 149 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
150 150 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
151 151 cmd << credentials_string
152 152 cat = nil
153 153 shellout(cmd) do |io|
154 154 io.binmode
155 155 cat = io.read
156 156 end
157 157 return nil if $? && $?.exitstatus != 0
158 158 cat
159 159 end
160 160
161 161 def annotate(path, identifier=nil)
162 162 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
163 163 cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
164 164 cmd << credentials_string
165 165 blame = Annotate.new
166 166 shellout(cmd) do |io|
167 167 io.each_line do |line|
168 168 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
169 169 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
170 170 end
171 171 end
172 172 return nil if $? && $?.exitstatus != 0
173 173 blame
174 174 end
175 175
176 176 private
177 177
178 178 def credentials_string
179 179 str = ''
180 180 str << " --username #{shell_quote(@login)}" unless @login.blank?
181 181 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
182 182 str
183 183 end
184 184 end
185 185 end
186 186 end
187 187 end
General Comments 0
You need to be logged in to leave comments. Login now