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