##// END OF EJS Templates
Added basic support for CVS and Mercurial SCMs....
Jean-Philippe Lang -
r556:438161ad1fd3
parent child
Show More
@@ -0,0 +1,150
1 # redMine - project management software
2 # Copyright (C) 2006-2007 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 require 'redmine/scm/adapters/cvs_adapter'
19 require 'digest/sha1'
20
21 class Repository::Cvs < Repository
22 validates_presence_of :url, :root_url
23
24 def scm_adapter
25 Redmine::Scm::Adapters::CvsAdapter
26 end
27
28 def self.scm_name
29 'CVS'
30 end
31
32 def entry(path, identifier)
33 e = entries(path, identifier)
34 e ? e.first : nil
35 end
36
37 def entries(path=nil, identifier=nil)
38 entries=scm.entries(path, identifier)
39 if entries
40 entries.each() do |entry|
41 unless entry.lastrev.nil? || entry.lastrev.identifier
42 change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
43 if change
44 entry.lastrev.identifier=change.changeset.revision
45 entry.lastrev.author=change.changeset.committer
46 entry.lastrev.revision=change.revision
47 entry.lastrev.branch=change.branch
48 end
49 end
50 end
51 end
52 entries
53 end
54
55 def diff(path, rev, rev_to, type)
56 #convert rev to revision. CVS can't handle changesets here
57 diff=[]
58 changeset_from=changesets.find_by_revision(rev)
59 if rev_to.to_i > 0
60 changeset_to=changesets.find_by_revision(rev_to)
61 end
62 changeset_from.changes.each() do |change_from|
63
64 revision_from=nil
65 revision_to=nil
66
67 revision_from=change_from.revision if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
68
69 if revision_from
70 if changeset_to
71 changeset_to.changes.each() do |change_to|
72 revision_to=change_to.revision if change_to.path==change_from.path
73 end
74 end
75 unless revision_to
76 revision_to=scm.get_previous_revision(revision_from)
77 end
78 diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
79 end
80 end
81 return diff
82 end
83
84 def fetch_changesets
85 #not the preferred way with CVS. maybe we should introduce always a cron-job for this
86 last_commit = changesets.maximum(:committed_on)
87
88 # some nifty bits to introduce a commit-id with cvs
89 # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
90 # we now take a guess using the author, the commitlog and the commit-date.
91
92 # last one is the next step to take. the commit-date is not equal for all
93 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
94 # we use a small delta here, to merge all changes belonging to _one_ changeset
95 time_delta=10.seconds
96
97 transaction do
98 scm.revisions('', last_commit, nil, :with_paths => true) do |revision|
99 # only add the change to the database, if it doen't exists. the cvs log
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])
102 revision
103 cs=Changeset.find(:first, :conditions=>{
104 :committed_on=>revision.time-time_delta..revision.time+time_delta,
105 :committer=>revision.author,
106 :comments=>revision.message
107 })
108
109 # create a new changeset....
110 unless cs
111 # we use a negative changeset-number here (just for inserting)
112 # later on, we calculate a continous positive number
113 next_rev = changesets.minimum(:revision)
114 next_rev = 0 if next_rev.nil? or next_rev > 0
115 next_rev = next_rev - 1
116
117 cs=Changeset.create(:repository => self,
118 :revision => next_rev,
119 :committer => revision.author,
120 :committed_on => revision.time,
121 :comments => revision.message)
122 end
123
124 #convert CVS-File-States to internal Action-abbrevations
125 #default action is (M)odified
126 action="M"
127 if revision.paths[0][:action]=="Exp" && revision.paths[0][:revision]=="1.1"
128 action="A" #add-action always at first revision (= 1.1)
129 elsif revision.paths[0][:action]=="dead"
130 action="D" #dead-state is similar to Delete
131 end
132
133 Change.create(:changeset => cs,
134 :action => action,
135 :path => scm.with_leading_slash(revision.paths[0][:path]),
136 :revision => revision.paths[0][:revision],
137 :branch => revision.paths[0][:branch]
138 )
139 end
140 end
141
142 next_rev = [changesets.maximum(:revision) || 0, 0].max
143 changesets.find(:all, :conditions=>["revision < 0"], :order=>"committed_on ASC").each() do |changeset|
144 next_rev = next_rev + 1
145 changeset.revision = next_rev
146 changeset.save!
147 end
148 end
149 end
150 end
@@ -0,0 +1,81
1 # redMine - project management software
2 # Copyright (C) 2006-2007 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 require 'redmine/scm/adapters/mercurial_adapter'
19
20 class Repository::Mercurial < Repository
21 attr_protected :root_url
22 validates_presence_of :url
23
24 def scm_adapter
25 Redmine::Scm::Adapters::MercurialAdapter
26 end
27
28 def self.scm_name
29 'Mercurial'
30 end
31
32 def entries(path=nil, identifier=nil)
33 entries=scm.entries(path, identifier)
34 if entries
35 entries.each do |entry|
36 next unless entry.is_file?
37 # Search the DB for the entry's last change
38 change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
39 if change
40 entry.lastrev.identifier = change.changeset.revision
41 entry.lastrev.name = change.changeset.revision
42 entry.lastrev.author = change.changeset.committer
43 entry.lastrev.revision = change.revision
44 end
45 end
46 end
47 entries
48 end
49
50 def fetch_changesets
51 scm_info = scm.info
52 if scm_info
53 # latest revision found in database
54 db_revision = latest_changeset ? latest_changeset.revision : nil
55 # latest revision in the repository
56 scm_revision = scm_info.lastrev.identifier.to_i
57
58 unless changesets.find_by_revision(scm_revision)
59 revisions = scm.revisions('', db_revision, nil)
60 transaction do
61 revisions.reverse_each do |revision|
62 changeset = Changeset.create(:repository => self,
63 :revision => revision.identifier,
64 :scmid => revision.scmid,
65 :committer => revision.author,
66 :committed_on => revision.time,
67 :comments => revision.message)
68
69 revision.paths.each do |change|
70 Change.create(:changeset => changeset,
71 :action => change[:action],
72 :path => change[:path],
73 :from_path => change[:from_path],
74 :from_revision => change[:from_revision])
75 end
76 end
77 end
78 end
79 end
80 end
81 end
@@ -0,0 +1,69
1 # redMine - project management software
2 # Copyright (C) 2006-2007 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 require 'redmine/scm/adapters/subversion_adapter'
19
20 class Repository::Subversion < Repository
21 attr_protected :root_url
22 validates_presence_of :url
23 validates_format_of :url, :with => /^(http|https|svn|file):\/\/.+/i
24
25 def scm_adapter
26 Redmine::Scm::Adapters::SubversionAdapter
27 end
28
29 def self.scm_name
30 'Subversion'
31 end
32
33 def fetch_changesets
34 scm_info = scm.info
35 if scm_info
36 # latest revision found in database
37 db_revision = latest_changeset ? latest_changeset.revision : 0
38 # latest revision in the repository
39 scm_revision = scm_info.lastrev.identifier.to_i
40 if db_revision < scm_revision
41 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
42 identifier_from = db_revision + 1
43 while (identifier_from <= scm_revision)
44 # loads changesets by batches of 200
45 identifier_to = [identifier_from + 199, scm_revision].min
46 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
47 transaction do
48 revisions.reverse_each do |revision|
49 changeset = Changeset.create(:repository => self,
50 :revision => revision.identifier,
51 :committer => revision.author,
52 :committed_on => revision.time,
53 :comments => revision.message)
54
55 revision.paths.each do |change|
56 Change.create(:changeset => changeset,
57 :action => change[:action],
58 :path => change[:path],
59 :from_path => change[:from_path],
60 :from_revision => change[:from_revision])
61 end
62 end
63 end unless revisions.nil?
64 identifier_from = identifier_to + 1
65 end
66 end
67 end
68 end
69 end
@@ -0,0 +1,3
1 <% fields_for :repository, repository, { :builder => TabularFormBuilder, :lang => current_language} do |f| %>
2 <%= repository_field_tags(f, repository) %>
3 <% end %>
@@ -0,0 +1,13
1 <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2>
2
3 <h3><%=h @entry.name %></h3>
4
5 <p>
6 <% if @entry.is_text? %>
7 <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
8 <% end %>
9 <%= link_to l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' } %>
10 <%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
11 </p>
12
13 <%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changes, :entry => @entry }%>
@@ -0,0 +1,9
1 class AddChangesRevision < ActiveRecord::Migration
2 def self.up
3 add_column :changes, :revision, :string
4 end
5
6 def self.down
7 remove_column :changes, :revision
8 end
9 end
@@ -0,0 +1,9
1 class AddChangesBranch < ActiveRecord::Migration
2 def self.up
3 add_column :changes, :branch, :string
4 end
5
6 def self.down
7 remove_column :changes, :branch
8 end
9 end
@@ -0,0 +1,9
1 class AddChangesetsScmid < ActiveRecord::Migration
2 def self.up
3 add_column :changesets, :scmid, :string
4 end
5
6 def self.down
7 remove_column :changesets, :scmid
8 end
9 end
@@ -0,0 +1,11
1 class AddRepositoriesType < ActiveRecord::Migration
2 def self.up
3 add_column :repositories, :type, :string
4 # Set class name for existing SVN repositories
5 Repository.update_all "type = 'Subversion'"
6 end
7
8 def self.down
9 remove_column :repositories, :type
10 end
11 end
@@ -0,0 +1,9
1 class AddRepositoriesChangesPermission < ActiveRecord::Migration
2 def self.up
3 Permission.create :controller => 'repositories', :action => 'changes', :description => 'label_change_plural', :sort => 1475, :is_public => true, :mail_option => 0, :mail_enabled => 0
4 end
5
6 def self.down
7 Permission.find_by_controller_and_action('repositories', 'changes').destroy
8 end
9 end
@@ -0,0 +1,352
1 # redMine - project management software
2 # Copyright (C) 2006-2007 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 require 'redmine/scm/adapters/abstract_adapter'
19
20 module Redmine
21 module Scm
22 module Adapters
23 class CvsAdapter < AbstractAdapter
24
25 # CVS executable name
26 CVS_BIN = "cvs"
27
28 # Guidelines for the input:
29 # url -> the project-path, relative to the cvsroot (eg. module name)
30 # root_url -> the good old, sometimes damned, CVSROOT
31 # login -> unnecessary
32 # password -> unnecessary too
33 def initialize(url, root_url=nil, login=nil, password=nil)
34 @url = url
35 @login = login if login && !login.empty?
36 @password = (password || "") if @login
37 #TODO: better Exception here (IllegalArgumentException)
38 raise CommandFailed if root_url.blank?
39 @root_url = root_url
40 end
41
42 def root_url
43 @root_url
44 end
45
46 def url
47 @url
48 end
49
50 def info
51 logger.debug "<cvs> info"
52 Info.new({:root_url => @root_url, :lastrev => nil})
53 end
54
55 def get_previous_revision(revision)
56 CvsRevisionHelper.new(revision).prevRev
57 end
58
59 # Returns the entry identified by path and revision identifier
60 # or nil if entry doesn't exist in the repository
61 # this method returns all revisions from one single SCM-Entry
62 def entry(path=nil, identifier="HEAD")
63 e = entries(path, identifier)
64 logger.debug("<cvs-result> #{e.first.inspect}") if e
65 e ? e.first : nil
66 end
67
68 # Returns an Entries collection
69 # or nil if the given path doesn't exist in the repository
70 # this method is used by the repository-browser (aka LIST)
71 def entries(path=nil, identifier=nil)
72 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
73 path_with_project="#{url}#{with_leading_slash(path)}"
74 entries = Entries.new
75 cmd = "#{CVS_BIN} -d #{root_url} rls -ed #{path_with_project}"
76 shellout(cmd) do |io|
77 io.each_line(){|line|
78 fields=line.chop.split('/',-1)
79 logger.debug(">>InspectLine #{fields.inspect}")
80
81 if fields[0]!="D"
82 entries << Entry.new({:name => fields[-5],
83 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
84 :path => "#{path}/#{fields[-5]}",
85 :kind => 'file',
86 :size => nil,
87 :lastrev => Revision.new({
88 :revision => fields[-4],
89 :name => fields[-4],
90 :time => Time.parse(fields[-3]),
91 :author => ''
92 })
93 })
94 else
95 entries << Entry.new({:name => fields[1],
96 :path => "#{path}/#{fields[1]}",
97 :kind => 'dir',
98 :size => nil,
99 :lastrev => nil
100 })
101 end
102 }
103 end
104 return nil if $? && $?.exitstatus != 0
105 entries.sort_by_name
106 rescue Errno::ENOENT => e
107 raise CommandFailed
108 end
109
110 STARTLOG="----------------------------"
111 ENDLOG ="============================================================================="
112
113 # Returns all revisions found between identifier_from and identifier_to
114 # in the repository. both identifier have to be dates or nil.
115 # these method returns nothing but yield every result in block
116 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
117 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
118
119 path_with_project="#{url}#{with_leading_slash(path)}"
120 cmd = "#{CVS_BIN} -d #{root_url} rlog"
121 cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
122 cmd << " #{path_with_project}"
123 shellout(cmd) do |io|
124 state="entry_start"
125
126 commit_log=String.new
127 revision=nil
128 date=nil
129 author=nil
130 entry_path=nil
131 entry_name=nil
132 file_state=nil
133 branch_map=nil
134
135 io.each_line() do |line|
136
137 if state!="revision" && /^#{ENDLOG}/ =~ line
138 commit_log=String.new
139 revision=nil
140 state="entry_start"
141 end
142
143 if state=="entry_start"
144 branch_map=Hash.new
145 if /^RCS file: #{Regexp.escape(root_url)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
146 entry_path = normalize_cvs_path($1)
147 entry_name = normalize_path(File.basename($1))
148 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
149 elsif /^head: (.+)$/ =~ line
150 entry_headRev = $1 #unless entry.nil?
151 elsif /^symbolic names:/ =~ line
152 state="symbolic" #unless entry.nil?
153 elsif /^#{STARTLOG}/ =~ line
154 commit_log=String.new
155 state="revision"
156 end
157 next
158 elsif state=="symbolic"
159 if /^(.*):\s(.*)/ =~ (line.strip)
160 branch_map[$1]=$2
161 else
162 state="tags"
163 next
164 end
165 elsif state=="tags"
166 if /^#{STARTLOG}/ =~ line
167 commit_log = ""
168 state="revision"
169 elsif /^#{ENDLOG}/ =~ line
170 state="head"
171 end
172 next
173 elsif state=="revision"
174 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
175 if revision
176
177 revHelper=CvsRevisionHelper.new(revision)
178 revBranch="HEAD"
179
180 branch_map.each() do |branch_name,branch_point|
181 if revHelper.is_in_branch_with_symbol(branch_point)
182 revBranch=branch_name
183 end
184 end
185
186 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
187
188 yield Revision.new({
189 :time => date,
190 :author => author,
191 :message=>commit_log.chomp,
192 :paths => [{
193 :revision => revision,
194 :branch=> revBranch,
195 :path=>entry_path,
196 :name=>entry_name,
197 :kind=>'file',
198 :action=>file_state
199 }]
200 })
201 end
202
203 commit_log=String.new
204 revision=nil
205
206 if /^#{ENDLOG}/ =~ line
207 state="entry_start"
208 end
209 next
210 end
211
212 if /^branches: (.+)$/ =~ line
213 #TODO: version.branch = $1
214 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
215 revision = $1
216 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
217 date = Time.parse($1)
218 author = /author: ([^;]+)/.match(line)[1]
219 file_state = /state: ([^;]+)/.match(line)[1]
220 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
221 # useful for stats or something else
222 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
223 # unless linechanges.nil?
224 # version.line_plus = linechanges[1]
225 # version.line_minus = linechanges[2]
226 # else
227 # version.line_plus = 0
228 # version.line_minus = 0
229 # end
230 else
231 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
232 end
233 end
234 end
235 end
236 rescue Errno::ENOENT => e
237 raise CommandFailed
238 end
239
240 def diff(path, identifier_from, identifier_to=nil, type="inline")
241 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
242 path_with_project="#{url}#{with_leading_slash(path)}"
243 cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{path_with_project}"
244 diff = []
245 shellout(cmd) do |io|
246 io.each_line do |line|
247 diff << line
248 end
249 end
250 return nil if $? && $?.exitstatus != 0
251 DiffTableList.new diff, type
252 rescue Errno::ENOENT => e
253 raise CommandFailed
254 end
255
256 def cat(path, identifier=nil)
257 identifier = (identifier) ? identifier : "HEAD"
258 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
259 path_with_project="#{url}#{with_leading_slash(path)}"
260 cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{path_with_project}"
261 cat = nil
262 shellout(cmd) do |io|
263 cat = io.read
264 end
265 return nil if $? && $?.exitstatus != 0
266 cat
267 rescue Errno::ENOENT => e
268 raise CommandFailed
269 end
270
271 private
272
273 # convert a date/time into the CVS-format
274 def time_to_cvstime(time)
275 return nil if time.nil?
276 unless time.kind_of? Time
277 time = Time.parse(time)
278 end
279 return time.strftime("%Y-%m-%d %H:%M:%S")
280 end
281
282 def normalize_cvs_path(path)
283 normalize_path(path.gsub(/Attic\//,''))
284 end
285
286 def normalize_path(path)
287 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
288 end
289 end
290
291 class CvsRevisionHelper
292 attr_accessor :complete_rev, :revision, :base, :branchid
293
294 def initialize(complete_rev)
295 @complete_rev = complete_rev
296 parseRevision()
297 end
298
299 def branchPoint
300 return @base
301 end
302
303 def branchVersion
304 if isBranchRevision
305 return @base+"."+@branchid
306 end
307 return @base
308 end
309
310 def isBranchRevision
311 !@branchid.nil?
312 end
313
314 def prevRev
315 unless @revision==0
316 return buildRevision(@revision-1)
317 end
318 return buildRevision(@revision)
319 end
320
321 def is_in_branch_with_symbol(branch_symbol)
322 bpieces=branch_symbol.split(".")
323 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
324 return (branchVersion==branch_start)
325 end
326
327 private
328 def buildRevision(rev)
329 if rev== 0
330 @base
331 elsif @branchid.nil?
332 @base+"."+rev.to_s
333 else
334 @base+"."+@branchid+"."+rev.to_s
335 end
336 end
337
338 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
339 def parseRevision()
340 pieces=@complete_rev.split(".")
341 @revision=pieces.last.to_i
342 baseSize=1
343 baseSize+=(pieces.size/2)
344 @base=pieces[0..-baseSize].join(".")
345 if baseSize > 2
346 @branchid=pieces[-2]
347 end
348 end
349 end
350 end
351 end
352 end
@@ -0,0 +1,163
1 # redMine - project management software
2 # Copyright (C) 2006-2007 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 require 'redmine/scm/adapters/abstract_adapter'
19
20 module Redmine
21 module Scm
22 module Adapters
23 class MercurialAdapter < AbstractAdapter
24
25 # Mercurial executable name
26 HG_BIN = "hg"
27
28 def info
29 cmd = "#{HG_BIN} -R #{target('')} root"
30 root_url = nil
31 shellout(cmd) do |io|
32 root_url = io.gets
33 end
34 return nil if $? && $?.exitstatus != 0
35 info = Info.new({:root_url => root_url.chomp,
36 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
37 })
38 info
39 rescue Errno::ENOENT => e
40 return nil
41 end
42
43 def entries(path=nil, identifier=nil)
44 path ||= ''
45 entries = Entries.new
46 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target(path)} locate -X */*/*"
47 cmd << " -r #{identifier.to_i}" if identifier
48 cmd << " * */*"
49 shellout(cmd) do |io|
50 io.each_line do |line|
51 e = line.chomp.split('\\')
52 entries << Entry.new({:name => e.first,
53 :path => (path.empty? ? e.first : "#{path}/#{e.first}"),
54 :kind => (e.size > 1 ? 'dir' : 'file'),
55 :lastrev => Revision.new
56 }) unless entries.detect{|entry| entry.name == e.first}
57 end
58 end
59 return nil if $? && $?.exitstatus != 0
60 entries.sort_by_name
61 rescue Errno::ENOENT => e
62 raise CommandFailed
63 end
64
65 def entry(path=nil, identifier=nil)
66 path ||= ''
67 search_path = path.split('/')[0..-2].join('/')
68 entry_name = path.split('/').last
69 e = entries(search_path, identifier)
70 e ? e.detect{|entry| entry.name == entry_name} : nil
71 end
72
73 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
74 revisions = Revisions.new
75 cmd = "#{HG_BIN} -v -R #{target('')} log"
76 cmd << " -r #{identifier_from.to_i}:" if identifier_from
77 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
78 shellout(cmd) do |io|
79 changeset = {}
80 parsing_descr = false
81 line_feeds = 0
82
83 io.each_line do |line|
84 if line =~ /^(\w+):\s*(.*)$/
85 key = $1
86 value = $2
87 if parsing_descr && line_feeds > 1
88 parsing_descr = false
89 revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i,
90 :scmid => changeset[:changeset].split(':').last,
91 :author => changeset[:user],
92 :time => Time.parse(changeset[:date]),
93 :message => changeset[:description],
94 :paths => changeset[:files].split.collect{|path| {:action => 'X', :path => "/#{path}"}}
95 })
96 changeset = {}
97 end
98 if !parsing_descr
99 changeset.store key.to_sym, value
100 if $1 == "description"
101 parsing_descr = true
102 line_feeds = 0
103 next
104 end
105 end
106 end
107 if parsing_descr
108 changeset[:description] << line
109 line_feeds += 1 if line.chomp.empty?
110 end
111 end
112 revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i,
113 :scmid => changeset[:changeset].split(':').last,
114 :author => changeset[:user],
115 :time => Time.parse(changeset[:date]),
116 :message => changeset[:description],
117 :paths => changeset[:files].split.collect{|path| {:action => 'X', :path => "/#{path}"}}
118 })
119 end
120 return nil if $? && $?.exitstatus != 0
121 revisions
122 rescue Errno::ENOENT => e
123 raise CommandFailed
124 end
125
126 def diff(path, identifier_from, identifier_to=nil, type="inline")
127 path ||= ''
128 if identifier_to
129 identifier_to = identifier_to.to_i
130 else
131 identifier_to = identifier_from.to_i - 1
132 end
133 cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
134 cmd << " -I #{target(path)}" unless path.empty?
135 diff = []
136 shellout(cmd) do |io|
137 io.each_line do |line|
138 diff << line
139 end
140 end
141 return nil if $? && $?.exitstatus != 0
142 DiffTableList.new diff, type
143
144 rescue Errno::ENOENT => e
145 raise CommandFailed
146 end
147
148 def cat(path, identifier=nil)
149 cmd = "#{HG_BIN} -R #{target('')} cat #{target(path)}"
150 cat = nil
151 shellout(cmd) do |io|
152 io.binmode
153 cat = io.read
154 end
155 return nil if $? && $?.exitstatus != 0
156 cat
157 rescue Errno::ENOENT => e
158 raise CommandFailed
159 end
160 end
161 end
162 end
163 end
@@ -0,0 +1,173
1 # redMine - project management software
2 # Copyright (C) 2006-2007 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 require 'redmine/scm/adapters/abstract_adapter'
19 require 'rexml/document'
20
21 module Redmine
22 module Scm
23 module Adapters
24 class SubversionAdapter < AbstractAdapter
25
26 # SVN executable name
27 SVN_BIN = "svn"
28
29 # Get info about the svn repository
30 def info
31 cmd = "#{SVN_BIN} info --xml #{target('')}"
32 cmd << " --username #{@login} --password #{@password}" if @login
33 info = nil
34 shellout(cmd) do |io|
35 begin
36 doc = REXML::Document.new(io)
37 #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({
40 :identifier => doc.elements["info/entry/commit"].attributes['revision'],
41 :time => Time.parse(doc.elements["info/entry/commit/date"].text),
42 :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")
43 })
44 })
45 rescue
46 end
47 end
48 return nil if $? && $?.exitstatus != 0
49 info
50 rescue Errno::ENOENT => e
51 return nil
52 end
53
54 # Returns the entry identified by path and revision identifier
55 # or nil if entry doesn't exist in the repository
56 def entry(path=nil, identifier=nil)
57 e = entries(path, identifier)
58 e ? e.first : nil
59 end
60
61 # Returns an Entries collection
62 # or nil if the given path doesn't exist in the repository
63 def entries(path=nil, identifier=nil)
64 path ||= ''
65 identifier = 'HEAD' unless identifier and identifier > 0
66 entries = Entries.new
67 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
68 cmd << " --username #{@login} --password #{@password}" if @login
69 shellout(cmd) do |io|
70 begin
71 doc = REXML::Document.new(io)
72 doc.elements.each("lists/list/entry") do |entry|
73 entries << Entry.new({:name => entry.elements['name'].text,
74 :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
75 :kind => entry.attributes['kind'],
76 :size => (entry.elements['size'] and entry.elements['size'].text).to_i,
77 :lastrev => Revision.new({
78 :identifier => entry.elements['commit'].attributes['revision'],
79 :time => Time.parse(entry.elements['commit'].elements['date'].text),
80 :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")
81 })
82 })
83 end
84 rescue
85 end
86 end
87 return nil if $? && $?.exitstatus != 0
88 entries.sort_by_name
89 rescue Errno::ENOENT => e
90 raise CommandFailed
91 end
92
93 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
94 path ||= ''
95 identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0
96 identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
97 revisions = Revisions.new
98 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
99 cmd << " --username #{@login} --password #{@password}" if @login
100 cmd << " --verbose " if options[:with_paths]
101 cmd << target(path)
102 shellout(cmd) do |io|
103 begin
104 doc = REXML::Document.new(io)
105 doc.elements.each("log/logentry") do |logentry|
106 paths = []
107 logentry.elements.each("paths/path") do |path|
108 paths << {:action => path.attributes['action'],
109 :path => path.text,
110 :from_path => path.attributes['copyfrom-path'],
111 :from_revision => path.attributes['copyfrom-rev']
112 }
113 end
114 paths.sort! { |x,y| x[:path] <=> y[:path] }
115
116 revisions << Revision.new({:identifier => logentry.attributes['revision'],
117 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
118 :time => Time.parse(logentry.elements['date'].text),
119 :message => logentry.elements['msg'].text,
120 :paths => paths
121 })
122 end
123 rescue
124 end
125 end
126 return nil if $? && $?.exitstatus != 0
127 revisions
128 rescue Errno::ENOENT => e
129 raise CommandFailed
130 end
131
132 def diff(path, identifier_from, identifier_to=nil, type="inline")
133 path ||= ''
134 if identifier_to and identifier_to.to_i > 0
135 identifier_to = identifier_to.to_i
136 else
137 identifier_to = identifier_from.to_i - 1
138 end
139 cmd = "#{SVN_BIN} diff -r "
140 cmd << "#{identifier_to}:"
141 cmd << "#{identifier_from}"
142 cmd << "#{target(path)}@#{identifier_from}"
143 cmd << " --username #{@login} --password #{@password}" if @login
144 diff = []
145 shellout(cmd) do |io|
146 io.each_line do |line|
147 diff << line
148 end
149 end
150 return nil if $? && $?.exitstatus != 0
151 DiffTableList.new diff, type
152 rescue Errno::ENOENT => e
153 raise CommandFailed
154 end
155
156 def cat(path, identifier=nil)
157 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
158 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
159 cmd << " --username #{@login} --password #{@password}" if @login
160 cat = nil
161 shellout(cmd) do |io|
162 io.binmode
163 cat = io.read
164 end
165 return nil if $? && $?.exitstatus != 0
166 cat
167 rescue Errno::ENOENT => e
168 raise CommandFailed
169 end
170 end
171 end
172 end
173 end
@@ -35,6 +35,8 class ProjectsController < ApplicationController
35 helper IssuesHelper
35 helper IssuesHelper
36 helper :queries
36 helper :queries
37 include QueriesHelper
37 include QueriesHelper
38 helper :repositories
39 include RepositoriesHelper
38
40
39 def index
41 def index
40 list
42 list
@@ -70,7 +72,7 class ProjectsController < ApplicationController
70 @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
72 @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
71 @project.custom_values = @custom_values
73 @project.custom_values = @custom_values
72 if params[:repository_enabled] && params[:repository_enabled] == "1"
74 if params[:repository_enabled] && params[:repository_enabled] == "1"
73 @project.repository = Repository.new
75 @project.repository = Repository.factory(params[:repository_scm])
74 @project.repository.attributes = params[:repository]
76 @project.repository.attributes = params[:repository]
75 end
77 end
76 if "1" == params[:wiki_enabled]
78 if "1" == params[:wiki_enabled]
@@ -116,8 +118,8 class ProjectsController < ApplicationController
116 when "0"
118 when "0"
117 @project.repository = nil
119 @project.repository = nil
118 when "1"
120 when "1"
119 @project.repository ||= Repository.new
121 @project.repository ||= Repository.factory(params[:repository_scm])
120 @project.repository.update_attributes params[:repository]
122 @project.repository.update_attributes params[:repository] if @project.repository
121 end
123 end
122 end
124 end
123 if params[:wiki_enabled]
125 if params[:wiki_enabled]
@@ -21,42 +21,42 require 'digest/sha1'
21
21
22 class RepositoriesController < ApplicationController
22 class RepositoriesController < ApplicationController
23 layout 'base'
23 layout 'base'
24 before_filter :find_project
24 before_filter :find_project, :except => [:update_form]
25 before_filter :authorize, :except => [:stats, :graph]
25 before_filter :authorize, :except => [:update_form, :stats, :graph]
26 before_filter :check_project_privacy, :only => [:stats, :graph]
26 before_filter :check_project_privacy, :only => [:stats, :graph]
27
27
28 def show
28 def show
29 # check if new revisions have been committed in the repository
30 @repository.fetch_changesets if Setting.autofetch_changesets?
29 # get entries for the browse frame
31 # get entries for the browse frame
30 @entries = @repository.scm.entries('')
32 @entries = @repository.entries('')
31 show_error and return unless @entries
33 show_error and return unless @entries
32 # check if new revisions have been committed in the repository
34 # latest changesets
33 scm_latestrev = @entries.revisions.latest
35 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
34 if Setting.autofetch_changesets? && scm_latestrev && ((@repository.latest_changeset.nil?) || (@repository.latest_changeset.revision < scm_latestrev.identifier.to_i))
35 @repository.fetch_changesets
36 @repository.reload
37 end
38 @changesets = @repository.changesets.find(:all, :limit => 5, :order => "committed_on DESC")
39 end
36 end
40
37
41 def browse
38 def browse
42 @entries = @repository.scm.entries(@path, @rev)
39 @entries = @repository.entries(@path, @rev)
43 show_error and return unless @entries
40 show_error and return unless @entries
41 end
42
43 def changes
44 @entry = @repository.scm.entry(@path, @rev)
45 show_error and return unless @entry
46 @changes = Change.find(:all, :include => :changeset,
47 :conditions => ["repository_id = ? AND path = ?", @repository.id, @path.with_leading_slash],
48 :order => "committed_on DESC")
44 end
49 end
45
50
46 def revisions
51 def revisions
47 unless @path == ''
52 @changeset_count = @repository.changesets.count
48 @entry = @repository.scm.entry(@path, @rev)
53 @changeset_pages = Paginator.new self, @changeset_count,
49 show_error and return unless @entry
54 25,
50 end
55 params['page']
51 @repository.changesets_with_path @path do
56 @changesets = @repository.changesets.find(:all,
52 @changeset_count = @repository.changesets.count(:select => "DISTINCT #{Changeset.table_name}.id")
57 :limit => @changeset_pages.items_per_page,
53 @changeset_pages = Paginator.new self, @changeset_count,
58 :offset => @changeset_pages.current.offset)
54 25,
59
55 params['page']
56 @changesets = @repository.changesets.find(:all,
57 :limit => @changeset_pages.items_per_page,
58 :offset => @changeset_pages.current.offset)
59 end
60 render :action => "revisions", :layout => false if request.xhr?
60 render :action => "revisions", :layout => false if request.xhr?
61 end
61 end
62
62
@@ -81,12 +81,12 class RepositoriesController < ApplicationController
81 end
81 end
82
82
83 def diff
83 def diff
84 @rev_to = (params[:rev_to] && params[:rev_to].to_i > 0) ? params[:rev_to].to_i : (@rev - 1)
84 @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
85 @diff_type = ('sbs' == params[:type]) ? 'sbs' : 'inline'
85 @diff_type = ('sbs' == params[:type]) ? 'sbs' : 'inline'
86
86
87 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
87 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
88 unless read_fragment(@cache_key)
88 unless read_fragment(@cache_key)
89 @diff = @repository.scm.diff(@path, @rev, @rev_to, type)
89 @diff = @repository.diff(@path, @rev, @rev_to, type)
90 show_error and return unless @diff
90 show_error and return unless @diff
91 end
91 end
92 end
92 end
@@ -110,6 +110,11 class RepositoriesController < ApplicationController
110 end
110 end
111 end
111 end
112
112
113 def update_form
114 @repository = Repository.factory(params[:repository_scm])
115 render :partial => 'projects/repository', :locals => {:repository => @repository}
116 end
117
113 private
118 private
114 def find_project
119 def find_project
115 @project = Project.find(params[:id])
120 @project = Project.find(params[:id])
@@ -117,7 +122,7 private
117 render_404 and return false unless @repository
122 render_404 and return false unless @repository
118 @path = params[:path].squeeze('/') if params[:path]
123 @path = params[:path].squeeze('/') if params[:path]
119 @path ||= ''
124 @path ||= ''
120 @rev = params[:rev].to_i if params[:rev] and params[:rev].to_i > 0
125 @rev = params[:rev].to_i if params[:rev]
121 rescue ActiveRecord::RecordNotFound
126 rescue ActiveRecord::RecordNotFound
122 render_404
127 render_404
123 end
128 end
@@ -218,3 +223,9 class Date
218 (date.year - self.year)*52 + (date.cweek - self.cweek)
223 (date.year - self.year)*52 + (date.cweek - self.cweek)
219 end
224 end
220 end
225 end
226
227 class String
228 def with_leading_slash
229 starts_with?('/') ? self : "/#{self}"
230 end
231 end
@@ -251,7 +251,9 class TabularFormBuilder < ActionView::Helpers::FormBuilder
251 src = <<-END_SRC
251 src = <<-END_SRC
252 def #{selector}(field, options = {})
252 def #{selector}(field, options = {})
253 return super if options.delete :no_label
253 return super if options.delete :no_label
254 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
254 label_text = l(options[:label]) if options[:label]
255 label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym)
256 label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required)
255 label = @template.content_tag("label", label_text,
257 label = @template.content_tag("label", label_text,
256 :class => (@object && @object.errors[field] ? "error" : nil),
258 :class => (@object && @object.errors[field] ? "error" : nil),
257 :for => (@object_name.to_s + "_" + field.to_s))
259 :for => (@object_name.to_s + "_" + field.to_s))
@@ -16,4 +16,39
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 module RepositoriesHelper
18 module RepositoriesHelper
19 def repository_field_tags(form, repository)
20 method = repository.class.name.demodulize.underscore + "_field_tags"
21 send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
22 end
23
24 def scm_select_tag
25 container = [[]]
26 REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]}
27 select_tag('repository_scm',
28 options_for_select(container, @project.repository.class.name.demodulize),
29 :disabled => (@project.repository && !@project.repository.new_record?),
30 :onchange => remote_function(:update => "repository_fields", :url => { :controller => 'repositories', :action => 'update_form', :id => @project }, :with => "Form.serialize(this.form)")
31 )
32 end
33
34 def with_leading_slash(path)
35 path ||= ''
36 path.starts_with?("/") ? "/#{path}" : path
37 end
38
39 def subversion_field_tags(form, repository)
40 content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
41 '<br />(http://, https://, svn://, file:///)') +
42 content_tag('p', form.text_field(:login, :size => 30)) +
43 content_tag('p', form.password_field(:password, :size => 30))
44 end
45
46 def mercurial_field_tags(form, repository)
47 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
48 end
49
50 def cvs_field_tags(form, repository)
51 content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
52 content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
53 end
19 end
54 end
@@ -17,70 +17,31
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 => 'revision DESC'
20 has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC"
21 has_many :changes, :through => :changesets
21 has_many :changes, :through => :changesets
22 has_one :latest_changeset, :class_name => 'Changeset', :foreign_key => :repository_id, :order => 'revision DESC'
23
24 attr_protected :root_url
25
26 validates_presence_of :url
27 validates_format_of :url, :with => /^(http|https|svn|file):\/\/.+/i
28
22
29 def scm
23 def scm
30 @scm ||= SvnRepos::Base.new url, root_url, login, password
24 @scm ||= self.scm_adapter.new url, root_url, login, password
31 update_attribute(:root_url, @scm.root_url) if root_url.blank?
25 update_attribute(:root_url, @scm.root_url) if root_url.blank?
32 @scm
26 @scm
33 end
27 end
34
28
35 def url=(str)
29 def scm_name
36 super if root_url.blank?
30 self.class.scm_name
37 end
31 end
38
32
39 def changesets_with_path(path="")
33 def entries(path=nil, identifier=nil)
40 path = "/#{path}%"
34 scm.entries(path, identifier)
41 path = url.gsub(/^#{root_url}/, '') + path if root_url && root_url != url
42 path.squeeze!("/")
43 # Custom select and joins is done to allow conditions on changes table without loading associated Change objects
44 # Required for changesets with a great number of changes (eg. 100,000)
45 Changeset.with_scope(:find => { :select => "DISTINCT #{Changeset.table_name}.*", :joins => "LEFT OUTER JOIN #{Change.table_name} ON #{Change.table_name}.changeset_id = #{Changeset.table_name}.id", :conditions => ["#{Change.table_name}.path LIKE ?", path] }) do
46 yield
47 end
48 end
35 end
49
36
50 def fetch_changesets
37 def diff(path, rev, rev_to, type)
51 scm_info = scm.info
38 scm.diff(path, rev, rev_to, type)
52 if scm_info
53 lastrev_identifier = scm_info.lastrev.identifier.to_i
54 if latest_changeset.nil? || latest_changeset.revision < lastrev_identifier
55 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
56 identifier_from = latest_changeset ? latest_changeset.revision + 1 : 1
57 while (identifier_from <= lastrev_identifier)
58 # loads changesets by batches of 200
59 identifier_to = [identifier_from + 199, lastrev_identifier].min
60 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
61 transaction do
62 revisions.reverse_each do |revision|
63 changeset = Changeset.create(:repository => self,
64 :revision => revision.identifier,
65 :committer => revision.author,
66 :committed_on => revision.time,
67 :comments => revision.message)
68
69 revision.paths.each do |change|
70 Change.create(:changeset => changeset,
71 :action => change[:action],
72 :path => change[:path],
73 :from_path => change[:from_path],
74 :from_revision => change[:from_revision])
75 end
76 end
77 end unless revisions.nil?
78 identifier_from = identifier_to + 1
79 end
80 end
81 end
82 end
39 end
83
40
41 def latest_changeset
42 @latest_changeset ||= changesets.find(:first)
43 end
44
84 def scan_changesets_for_issue_ids
45 def scan_changesets_for_issue_ids
85 self.changesets.each(&:scan_comment_for_issue_ids)
46 self.changesets.each(&:scan_comment_for_issue_ids)
86 end
47 end
@@ -96,4 +57,19 class Repository < ActiveRecord::Base
96 def self.scan_changesets_for_issue_ids
57 def self.scan_changesets_for_issue_ids
97 find(:all).each(&:scan_changesets_for_issue_ids)
58 find(:all).each(&:scan_changesets_for_issue_ids)
98 end
59 end
60
61 def self.scm_name
62 'Abstract'
63 end
64
65 def self.available_scm
66 subclasses.collect {|klass| [klass.scm_name, klass.name]}
67 end
68
69 def self.factory(klass_name, *args)
70 klass = "Repository::#{klass_name}".constantize
71 klass.new(*args)
72 rescue
73 nil
74 end
99 end
75 end
@@ -27,17 +27,17
27 <!--[eoform:project]-->
27 <!--[eoform:project]-->
28 </div>
28 </div>
29
29
30 <div class="box"><h3><%= check_box_tag "repository_enabled", 1, !@project.repository.nil?, :onclick => "Element.toggle('repository');" %> <%= l(:label_repository) %></h3>
30 <div class="box">
31 <%= hidden_field_tag "repository_enabled", 0 %>
31 <h3><%= check_box_tag "repository_enabled", 1, !@project.repository.nil?, :onclick => "Element.toggle('repository');" %> <%= l(:label_repository) %></h3>
32 <div id="repository">
32 <%= hidden_field_tag "repository_enabled", 0 %>
33 <% fields_for :repository, @project.repository, { :builder => TabularFormBuilder, :lang => current_language} do |repository| %>
33 <div id="repository">
34 <p><%= repository.text_field :url, :size => 60, :required => true, :disabled => (@project.repository && !@project.repository.root_url.blank?) %><br />(http://, https://, svn://, file:///)</p>
34 <p class="tabular"><label>SCM</label><%= scm_select_tag %></p>
35 <p><%= repository.text_field :login, :size => 30 %></p>
35 <div id="repository_fields">
36 <p><%= repository.password_field :password, :size => 30 %></p>
36 <%= render :partial => 'projects/repository', :locals => {:repository => @project.repository} if @project.repository %>
37 <% end %>
37 </div>
38 </div>
38 </div>
39 </div>
39 <%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %>
40 <%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %>
40 </div>
41
41
42 <div class="box">
42 <div class="box">
43 <h3><%= check_box_tag "wiki_enabled", 1, !@project.wiki.nil?, :onclick => "Element.toggle('wiki');" %> <%= l(:label_wiki) %></h3>
43 <h3><%= check_box_tag "wiki_enabled", 1, !@project.wiki.nil?, :onclick => "Element.toggle('wiki');" %> <%= l(:label_wiki) %></h3>
@@ -58,4 +58,4
58 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
58 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
59 <%= javascript_include_tag 'calendar/calendar-setup' %>
59 <%= javascript_include_tag 'calendar/calendar-setup' %>
60 <%= stylesheet_link_tag 'calendar' %>
60 <%= stylesheet_link_tag 'calendar' %>
61 <% end %> No newline at end of file
61 <% end %>
@@ -11,15 +11,15
11 <% total_size = 0
11 <% total_size = 0
12 @entries.each do |entry| %>
12 @entries.each do |entry| %>
13 <tr class="<%= cycle 'odd', 'even' %>">
13 <tr class="<%= cycle 'odd', 'even' %>">
14 <td><%= link_to h(entry.name), { :action => (entry.is_dir? ? 'browse' : 'revisions'), :id => @project, :path => entry.path, :rev => @rev }, :class => ("icon " + (entry.is_dir? ? 'icon-folder' : 'icon-file')) %></td>
14 <td><%= link_to h(entry.name), { :action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev }, :class => ("icon " + (entry.is_dir? ? 'icon-folder' : 'icon-file')) %></td>
15 <td align="right"><%= number_to_human_size(entry.size) unless entry.is_dir? %></td>
15 <td align="right"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
16 <td align="right"><%= link_to entry.lastrev.identifier, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier %></td>
16 <td align="right"><%= link_to(entry.lastrev.name, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
17 <td align="center"><%= format_time(entry.lastrev.time) %></td>
17 <td align="center"><%= format_time(entry.lastrev.time) if entry.lastrev %></td>
18 <td align="center"><em><%=h entry.lastrev.author %></em></td>
18 <td align="center"><em><%=h(entry.lastrev.author) if entry.lastrev %></em></td>
19 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) %>
19 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %>
20 <td><%=h truncate(changeset.comments, 100) unless changeset.nil? %></td>
20 <td><%=h truncate(changeset.comments, 100) unless changeset.nil? %></td>
21 </tr>
21 </tr>
22 <% total_size += entry.size
22 <% total_size += entry.size if entry.size
23 end %>
23 end %>
24 </tbody>
24 </tbody>
25 </table>
25 </table>
@@ -5,7 +5,8 if 'file' == kind
5 filename = dirs.pop
5 filename = dirs.pop
6 end
6 end
7 link_path = ''
7 link_path = ''
8 dirs.each do |dir|
8 dirs.each do |dir|
9 next if dir.blank?
9 link_path << '/' unless link_path.empty?
10 link_path << '/' unless link_path.empty?
10 link_path << "#{dir}"
11 link_path << "#{dir}"
11 %>
12 %>
@@ -15,4 +16,4 dirs.each do |dir|
15 / <%= link_to h(filename), :action => 'revisions', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %>
16 / <%= link_to h(filename), :action => 'revisions', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %>
16 <% end %>
17 <% end %>
17
18
18 <%= "@ #{revision}" if revision %> No newline at end of file
19 <%= "@ #{revision}" if revision %>
@@ -9,12 +9,13
9 <th><%= l(:field_comments) %></th>
9 <th><%= l(:field_comments) %></th>
10 </tr></thead>
10 </tr></thead>
11 <tbody>
11 <tbody>
12 <% show_diff = entry && entry.is_file? && changesets.size > 1 %>
12 <% show_diff = entry && entry.is_file? && revisions.size > 1 %>
13 <% line_num = 1 %>
13 <% line_num = 1 %>
14 <% changesets.each do |changeset| %>
14 <% revisions.each do |revision| %>
15 <% changeset = revision.is_a?(Change) ? revision.changeset : revision %>
15 <tr class="<%= cycle 'odd', 'even' %>">
16 <tr class="<%= cycle 'odd', 'even' %>">
16 <th align="center" style="width:3em;"><%= link_to changeset.revision, :action => 'revision', :id => project, :rev => changeset.revision %></th>
17 <th align="center" style="width:3em;"><%= link_to (revision.revision || changeset.revision), :action => 'revision', :id => project, :rev => changeset.revision %></th>
17 <td align="center" style="width:1em;"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < changesets.size) %></td>
18 <td align="center" style="width:1em;"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
18 <td align="center" style="width:1em;"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
19 <td align="center" style="width:1em;"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
19 <td align="center" style="width:15%"><%= format_time(changeset.committed_on) %></td>
20 <td align="center" style="width:15%"><%= format_time(changeset.committed_on) %></td>
20 <td align="center" style="width:15%"><em><%=h changeset.committer %></em></td>
21 <td align="center" style="width:15%"><em><%=h changeset.committer %></em></td>
@@ -7,7 +7,9
7
7
8 <h2><%= l(:label_revision) %> <%= @changeset.revision %></h2>
8 <h2><%= l(:label_revision) %> <%= @changeset.revision %></h2>
9
9
10 <p><em><%= @changeset.committer %>, <%= format_time(@changeset.committed_on) %></em></p>
10 <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
11 <em><%= @changeset.committer %>, <%= format_time(@changeset.committed_on) %></em></p>
12
11 <%= textilizable @changeset.comments %>
13 <%= textilizable @changeset.comments %>
12
14
13 <% if @changeset.issues.any? %>
15 <% if @changeset.issues.any? %>
@@ -30,7 +32,7
30 <tbody>
32 <tbody>
31 <% @changes.each do |change| %>
33 <% @changes.each do |change| %>
32 <tr class="<%= cycle 'odd', 'even' %>">
34 <tr class="<%= cycle 'odd', 'even' %>">
33 <td><div class="square action_<%= change.action %>"></div> <%= change.path %></td>
35 <td><div class="square action_<%= change.action %>"></div> <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %></td>
34 <td align="right">
36 <td align="right">
35 <% if change.action == "M" %>
37 <% if change.action == "M" %>
36 <%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => change.path, :rev => @changeset.revision %>
38 <%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => change.path, :rev => @changeset.revision %>
@@ -5,25 +5,13
5 <% end %>
5 <% end %>
6 </div>
6 </div>
7
7
8 <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2>
8 <h2><%= l(:label_revision_plural) %></h2>
9
9
10 <% if @entry && @entry.is_file? %>
10 <%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%>
11 <h3><%=h @entry.name %></h3>
12 <p>
13 <% if @entry.is_text? %>
14 <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
15 <% end %>
16 <%= link_to l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' } %>
17 (<%= number_to_human_size @entry.size %>)</p>
18 <% end %>
19
20 <h3><%= l(:label_revision_plural) %></h3>
21
22 <%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :changesets => @changesets, :entry => @entry }%>
23
11
24 <p><%= pagination_links_full @changeset_pages %>
12 <p><%= pagination_links_full @changeset_pages %>
25 [ <%= @changeset_pages.current.first_item %> - <%= @changeset_pages.current.last_item %> / <%= @changeset_count %> ]</p>
13 [ <%= @changeset_pages.current.first_item %> - <%= @changeset_pages.current.last_item %> / <%= @changeset_count %> ]</p>
26
14
27 <% content_for :header_tags do %>
15 <% content_for :header_tags do %>
28 <%= stylesheet_link_tag "scm" %>
16 <%= stylesheet_link_tag "scm" %>
29 <% end %> No newline at end of file
17 <% end %>
@@ -2,17 +2,19
2 <%= link_to l(:label_statistics), {:action => 'stats', :id => @project}, :class => 'icon icon-stats' %>
2 <%= link_to l(:label_statistics), {:action => 'stats', :id => @project}, :class => 'icon icon-stats' %>
3 </div>
3 </div>
4
4
5 <h2><%= l(:label_repository) %></h2>
5 <h2><%= l(:label_repository) %> (<%= @repository.scm_name %>)</h2>
6
6
7 <% unless @entries.nil? %>
7 <h3><%= l(:label_browse) %></h3>
8 <h3><%= l(:label_browse) %></h3>
8 <%= render :partial => 'dir_list' %>
9 <%= render :partial => 'dir_list' %>
10 <% end %>
9
11
10 <% unless @changesets.empty? %>
12 <% unless @changesets.empty? %>
11 <h3><%= l(:label_latest_revision_plural) %></h3>
13 <h3><%= l(:label_latest_revision_plural) %></h3>
12 <%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :changesets => @changesets, :entry => nil }%>
14 <%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%>
13 <p><%= link_to l(:label_view_revisions), :action => 'revisions', :id => @project %></p>
15 <p><%= link_to l(:label_view_revisions), :action => 'revisions', :id => @project %></p>
14 <% end %>
16 <% end %>
15
17
16 <% content_for :header_tags do %>
18 <% content_for :header_tags do %>
17 <%= stylesheet_link_tag "scm" %>
19 <%= stylesheet_link_tag "scm" %>
18 <% end %> No newline at end of file
20 <% end %>
@@ -167,8 +167,8 setting_host_name: Хост
167 setting_text_formatting: Форматиране на текста
167 setting_text_formatting: Форматиране на текста
168 setting_wiki_compression: Wiki компресиране на историята
168 setting_wiki_compression: Wiki компресиране на историята
169 setting_feeds_limit: Лимит на Feeds
169 setting_feeds_limit: Лимит на Feeds
170 setting_autofetch_changesets: Автоматично обработване на commits в SVN склада
170 setting_autofetch_changesets: Автоматично обработване на commits в склада
171 setting_sys_api_enabled: Разрешаване на WS за управление на SVN склада
171 setting_sys_api_enabled: Разрешаване на WS за управление на склада
172 setting_commit_ref_keywords: Отбелязващи ключови думи
172 setting_commit_ref_keywords: Отбелязващи ключови думи
173 setting_commit_fix_keywords: Приключващи ключови думи
173 setting_commit_fix_keywords: Приключващи ключови думи
174 setting_autologin: Autologin
174 setting_autologin: Autologin
@@ -318,7 +318,7 label_ago: преди дни
318 label_contains: съдържа
318 label_contains: съдържа
319 label_not_contains: не съдържа
319 label_not_contains: не съдържа
320 label_day_plural: дни
320 label_day_plural: дни
321 label_repository: SVN Склад
321 label_repository: Склад
322 label_browse: Разглеждане
322 label_browse: Разглеждане
323 label_modification: %d промяна
323 label_modification: %d промяна
324 label_modification_plural: %d промени
324 label_modification_plural: %d промени
@@ -67,7 +67,7 notice_successful_delete: Erfolgreiche Löschung.
67 notice_successful_connection: Verbindung erfolgreich.
67 notice_successful_connection: Verbindung erfolgreich.
68 notice_file_not_found: Anhang besteht nicht oder ist gelöscht worden.
68 notice_file_not_found: Anhang besteht nicht oder ist gelöscht worden.
69 notice_locking_conflict: Datum wurde von einem anderen Benutzer geändert.
69 notice_locking_conflict: Datum wurde von einem anderen Benutzer geändert.
70 notice_scm_error: Eintrag und/oder Revision besteht nicht im SVN.
70 notice_scm_error: Eintrag und/oder Revision besteht nicht im Projektarchiv.
71 notice_not_authorized: You are not authorized to access this page.
71 notice_not_authorized: You are not authorized to access this page.
72
72
73 mail_subject_lost_password: Ihr redMine Kennwort
73 mail_subject_lost_password: Ihr redMine Kennwort
@@ -167,7 +167,7 setting_host_name: Host Name
167 setting_text_formatting: Textformatierung
167 setting_text_formatting: Textformatierung
168 setting_wiki_compression: Wiki-Historie komprimieren
168 setting_wiki_compression: Wiki-Historie komprimieren
169 setting_feeds_limit: Limit Feed Inhalt
169 setting_feeds_limit: Limit Feed Inhalt
170 setting_autofetch_changesets: Autofetch SVN commits
170 setting_autofetch_changesets: Autofetch commits
171 setting_sys_api_enabled: Enable WS for repository management
171 setting_sys_api_enabled: Enable WS for repository management
172 setting_commit_ref_keywords: Referencing keywords
172 setting_commit_ref_keywords: Referencing keywords
173 setting_commit_fix_keywords: Fixing keywords
173 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 label_ago: vor
318 label_contains: enthält
318 label_contains: enthält
319 label_not_contains: enthält nicht
319 label_not_contains: enthält nicht
320 label_day_plural: Tage
320 label_day_plural: Tage
321 label_repository: SVN Projektarchiv
321 label_repository: Projektarchiv
322 label_browse: Codebrowser
322 label_browse: Codebrowser
323 label_modification: %d Änderung
323 label_modification: %d Änderung
324 label_modification_plural: %d Änderungen
324 label_modification_plural: %d Änderungen
@@ -167,7 +167,7 setting_host_name: Host name
167 setting_text_formatting: Text formatting
167 setting_text_formatting: Text formatting
168 setting_wiki_compression: Wiki history compression
168 setting_wiki_compression: Wiki history compression
169 setting_feeds_limit: Feed content limit
169 setting_feeds_limit: Feed content limit
170 setting_autofetch_changesets: Autofetch SVN commits
170 setting_autofetch_changesets: Autofetch commits
171 setting_sys_api_enabled: Enable WS for repository management
171 setting_sys_api_enabled: Enable WS for repository management
172 setting_commit_ref_keywords: Referencing keywords
172 setting_commit_ref_keywords: Referencing keywords
173 setting_commit_fix_keywords: Fixing keywords
173 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 label_ago: days ago
318 label_contains: contains
318 label_contains: contains
319 label_not_contains: doesn't contain
319 label_not_contains: doesn't contain
320 label_day_plural: days
320 label_day_plural: days
321 label_repository: SVN Repository
321 label_repository: Repository
322 label_browse: Browse
322 label_browse: Browse
323 label_modification: %d change
323 label_modification: %d change
324 label_modification_plural: %d changes
324 label_modification_plural: %d changes
@@ -167,7 +167,7 setting_host_name: Nombre de anfitrión
167 setting_text_formatting: Formato de texto
167 setting_text_formatting: Formato de texto
168 setting_wiki_compression: Compresión de la historia de Wiki
168 setting_wiki_compression: Compresión de la historia de Wiki
169 setting_feeds_limit: Feed content limit
169 setting_feeds_limit: Feed content limit
170 setting_autofetch_changesets: Autofetch SVN commits
170 setting_autofetch_changesets: Autofetch commits
171 setting_sys_api_enabled: Enable WS for repository management
171 setting_sys_api_enabled: Enable WS for repository management
172 setting_commit_ref_keywords: Referencing keywords
172 setting_commit_ref_keywords: Referencing keywords
173 setting_commit_fix_keywords: Fixing keywords
173 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 label_ago: hace
318 label_contains: contiene
318 label_contains: contiene
319 label_not_contains: no contiene
319 label_not_contains: no contiene
320 label_day_plural: días
320 label_day_plural: días
321 label_repository: Depósito SVN
321 label_repository: Depósito
322 label_browse: Hojear
322 label_browse: Hojear
323 label_modification: %d modificación
323 label_modification: %d modificación
324 label_modification_plural: %d modificaciones
324 label_modification_plural: %d modificaciones
@@ -167,7 +167,7 setting_host_name: Nom d'hôte
167 setting_text_formatting: Formatage du texte
167 setting_text_formatting: Formatage du texte
168 setting_wiki_compression: Compression historique wiki
168 setting_wiki_compression: Compression historique wiki
169 setting_feeds_limit: Limite du contenu des flux RSS
169 setting_feeds_limit: Limite du contenu des flux RSS
170 setting_autofetch_changesets: Récupération auto. des commits SVN
170 setting_autofetch_changesets: Récupération auto. des commits
171 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
171 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
172 setting_commit_ref_keywords: Mot-clés de référencement
172 setting_commit_ref_keywords: Mot-clés de référencement
173 setting_commit_fix_keywords: Mot-clés de résolution
173 setting_commit_fix_keywords: Mot-clés de résolution
@@ -318,7 +318,7 label_ago: il y a
318 label_contains: contient
318 label_contains: contient
319 label_not_contains: ne contient pas
319 label_not_contains: ne contient pas
320 label_day_plural: jours
320 label_day_plural: jours
321 label_repository: Dépôt SVN
321 label_repository: Dépôt
322 label_browse: Parcourir
322 label_browse: Parcourir
323 label_modification: %d modification
323 label_modification: %d modification
324 label_modification_plural: %d modifications
324 label_modification_plural: %d modifications
@@ -450,7 +450,7 text_length_between: Longueur comprise entre %d et %d caractères.
450 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
450 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
451 text_unallowed_characters: Caractères non autorisés
451 text_unallowed_characters: Caractères non autorisés
452 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
452 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
453 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires SVN
453 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
454
454
455 default_role_manager: Manager
455 default_role_manager: Manager
456 default_role_developper: Développeur
456 default_role_developper: Développeur
@@ -167,7 +167,7 setting_host_name: Nome host
167 setting_text_formatting: Formattazione testo
167 setting_text_formatting: Formattazione testo
168 setting_wiki_compression: Compressione di storia di Wiki
168 setting_wiki_compression: Compressione di storia di Wiki
169 setting_feeds_limit: Limite contenuti del feed
169 setting_feeds_limit: Limite contenuti del feed
170 setting_autofetch_changesets: Acquisisci automaticamente le commit SVN
170 setting_autofetch_changesets: Acquisisci automaticamente le commit
171 setting_sys_api_enabled: Abilita WS per la gestione del repository
171 setting_sys_api_enabled: Abilita WS per la gestione del repository
172 setting_commit_ref_keywords: Referencing keywords
172 setting_commit_ref_keywords: Referencing keywords
173 setting_commit_fix_keywords: Fixing keywords
173 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 label_ago: giorni fa
318 label_contains: contiene
318 label_contains: contiene
319 label_not_contains: non contiene
319 label_not_contains: non contiene
320 label_day_plural: giorni
320 label_day_plural: giorni
321 label_repository: SVN Repository
321 label_repository: Repository
322 label_browse: Browse
322 label_browse: Browse
323 label_modification: %d modifica
323 label_modification: %d modifica
324 label_modification_plural: %d modifiche
324 label_modification_plural: %d modifiche
@@ -168,7 +168,7 setting_host_name: ホスト名
168 setting_text_formatting: テキストの書式
168 setting_text_formatting: テキストの書式
169 setting_wiki_compression: Wiki履歴を圧縮する
169 setting_wiki_compression: Wiki履歴を圧縮する
170 setting_feeds_limit: フィード内容の上限
170 setting_feeds_limit: フィード内容の上限
171 setting_autofetch_changesets: SVNコミットを自動取得する
171 setting_autofetch_changesets: コミットを自動取得する
172 setting_sys_api_enabled: リポジトリ管理用のWeb Serviceを有効化する
172 setting_sys_api_enabled: リポジトリ管理用のWeb Serviceを有効化する
173 setting_commit_ref_keywords: 参照用キーワード
173 setting_commit_ref_keywords: 参照用キーワード
174 setting_commit_fix_keywords: 修正用キーワード
174 setting_commit_fix_keywords: 修正用キーワード
@@ -319,7 +319,7 label_ago: 日前
319 label_contains: 含む
319 label_contains: 含む
320 label_not_contains: 含まない
320 label_not_contains: 含まない
321 label_day_plural:
321 label_day_plural:
322 label_repository: SVNリポジトリ
322 label_repository: リポジトリ
323 label_browse: ブラウズ
323 label_browse: ブラウズ
324 label_modification: %d点の変更
324 label_modification: %d点の変更
325 label_modification_plural: %d点の変更
325 label_modification_plural: %d点の変更
@@ -167,7 +167,7 setting_host_name: Host naam
167 setting_text_formatting: Tekst formaat
167 setting_text_formatting: Tekst formaat
168 setting_wiki_compression: Wiki geschiedenis comprimeren
168 setting_wiki_compression: Wiki geschiedenis comprimeren
169 setting_feeds_limit: Feed inhoud limiet
169 setting_feeds_limit: Feed inhoud limiet
170 setting_autofetch_changesets: Haal SVN commits automatisch op
170 setting_autofetch_changesets: Haal commits automatisch op
171 setting_sys_api_enabled: Gebruik WS voor repository beheer
171 setting_sys_api_enabled: Gebruik WS voor repository beheer
172 setting_commit_ref_keywords: Referencing keywords
172 setting_commit_ref_keywords: Referencing keywords
173 setting_commit_fix_keywords: Fixing keywords
173 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 label_ago: dagen geleden
318 label_contains: bevat
318 label_contains: bevat
319 label_not_contains: bevat niet
319 label_not_contains: bevat niet
320 label_day_plural: dagen
320 label_day_plural: dagen
321 label_repository: SVN Repository
321 label_repository: Repository
322 label_browse: Blader
322 label_browse: Blader
323 label_modification: %d wijziging
323 label_modification: %d wijziging
324 label_modification_plural: %d wijzigingen
324 label_modification_plural: %d wijzigingen
@@ -167,7 +167,7 setting_host_name: Servidor
167 setting_text_formatting: Formato do texto
167 setting_text_formatting: Formato do texto
168 setting_wiki_compression: Compactacao do historio do Wiki
168 setting_wiki_compression: Compactacao do historio do Wiki
169 setting_feeds_limit: Limite do Feed
169 setting_feeds_limit: Limite do Feed
170 setting_autofetch_changesets: Autofetch SVN commits
170 setting_autofetch_changesets: Autofetch commits
171 setting_sys_api_enabled: Ativa WS para gerenciamento do repositorio
171 setting_sys_api_enabled: Ativa WS para gerenciamento do repositorio
172 setting_commit_ref_keywords: Referencing keywords
172 setting_commit_ref_keywords: Referencing keywords
173 setting_commit_fix_keywords: Fixing keywords
173 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 label_ago: dias atras
318 label_contains: contem
318 label_contains: contem
319 label_not_contains: nao contem
319 label_not_contains: nao contem
320 label_day_plural: dias
320 label_day_plural: dias
321 label_repository: SVN Repository
321 label_repository: Repository
322 label_browse: Browse
322 label_browse: Browse
323 label_modification: %d change
323 label_modification: %d change
324 label_modification_plural: %d changes
324 label_modification_plural: %d changes
@@ -167,7 +167,7 setting_host_name: Servidor
167 setting_text_formatting: Formato do texto
167 setting_text_formatting: Formato do texto
168 setting_wiki_compression: Compactação do histórico do Wiki
168 setting_wiki_compression: Compactação do histórico do Wiki
169 setting_feeds_limit: Limite do Feed
169 setting_feeds_limit: Limite do Feed
170 setting_autofetch_changesets: Buscar automaticamente commits do SVN
170 setting_autofetch_changesets: Buscar automaticamente commits
171 setting_sys_api_enabled: Ativa WS para gerenciamento do repositório
171 setting_sys_api_enabled: Ativa WS para gerenciamento do repositório
172 setting_commit_ref_keywords: Palavras-chave de referôncia
172 setting_commit_ref_keywords: Palavras-chave de referôncia
173 setting_commit_fix_keywords: Palavras-chave fixas
173 setting_commit_fix_keywords: Palavras-chave fixas
@@ -318,7 +318,7 label_ago: dias atrás
318 label_contains: contém
318 label_contains: contém
319 label_not_contains: não contém
319 label_not_contains: não contém
320 label_day_plural: dias
320 label_day_plural: dias
321 label_repository: Repositório SVN
321 label_repository: Repositório
322 label_browse: Procurar
322 label_browse: Procurar
323 label_modification: %d mudança
323 label_modification: %d mudança
324 label_modification_plural: %d mudanças
324 label_modification_plural: %d mudanças
@@ -167,7 +167,7 setting_host_name: Värddatornamn
167 setting_text_formatting: Textformattering
167 setting_text_formatting: Textformattering
168 setting_wiki_compression: Wiki historiekomprimering
168 setting_wiki_compression: Wiki historiekomprimering
169 setting_feeds_limit: Feed innehållsgräns
169 setting_feeds_limit: Feed innehållsgräns
170 setting_autofetch_changesets: Automatisk hämtning av SVN commits
170 setting_autofetch_changesets: Automatisk hämtning av commits
171 setting_sys_api_enabled: Aktivera WS för repository management
171 setting_sys_api_enabled: Aktivera WS för repository management
172 setting_commit_ref_keywords: Referencing keywords
172 setting_commit_ref_keywords: Referencing keywords
173 setting_commit_fix_keywords: Fixing keywords
173 setting_commit_fix_keywords: Fixing keywords
@@ -318,7 +318,7 label_ago: dagar sedan
318 label_contains: innehåller
318 label_contains: innehåller
319 label_not_contains: innehåller inte
319 label_not_contains: innehåller inte
320 label_day_plural: dagar
320 label_day_plural: dagar
321 label_repository: SVN Repositorie
321 label_repository: Repositorie
322 label_browse: Bläddra
322 label_browse: Bläddra
323 label_modification: %d ändring
323 label_modification: %d ändring
324 label_modification_plural: %d ändringar
324 label_modification_plural: %d ändringar
@@ -170,7 +170,7 setting_host_name: 主机名称
170 setting_text_formatting: 文本格式
170 setting_text_formatting: 文本格式
171 setting_wiki_compression: Wiki history compression
171 setting_wiki_compression: Wiki history compression
172 setting_feeds_limit: Feed content limit
172 setting_feeds_limit: Feed content limit
173 setting_autofetch_changesets: Autofetch SVN commits
173 setting_autofetch_changesets: Autofetch commits
174 setting_sys_api_enabled: Enable WS for repository management
174 setting_sys_api_enabled: Enable WS for repository management
175 setting_commit_ref_keywords: Referencing keywords
175 setting_commit_ref_keywords: Referencing keywords
176 setting_commit_fix_keywords: Fixing keywords
176 setting_commit_fix_keywords: Fixing keywords
@@ -321,7 +321,7 label_ago: 之前天数
321 label_contains: 包含
321 label_contains: 包含
322 label_not_contains: 不包含
322 label_not_contains: 不包含
323 label_day_plural: 天数
323 label_day_plural: 天数
324 label_repository: SVN 版本库
324 label_repository: 版本库
325 label_browse: 浏览
325 label_browse: 浏览
326 label_modification: %d 个更新
326 label_modification: %d 个更新
327 label_modification_plural: %d 个更新
327 label_modification_plural: %d 个更新
@@ -1,3 +1,5
1 require 'redmine/version'
1 require 'redmine/version'
2 require 'redmine/mime_type'
2 require 'redmine/mime_type'
3 require 'redmine/acts_as_watchable/init'
3 require 'redmine/acts_as_watchable/init'
4
5 REDMINE_SUPPORTED_SCM = %w( Subversion Mercurial Cvs )
This diff has been collapsed as it changes many lines, (679 lines changed) Show them Hide them
@@ -15,422 +15,327
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 'rexml/document'
19 require 'cgi'
18 require 'cgi'
20
19
21 module SvnRepos
20 module Redmine
22
21 module Scm
23 class CommandFailed < StandardError #:nodoc:
22 module Adapters
24 end
23 class CommandFailed < StandardError #:nodoc:
25
24 end
26 class Base
25
26 class AbstractAdapter #:nodoc:
27 def initialize(url, root_url=nil, login=nil, password=nil)
28 @url = url
29 @login = login if login && !login.empty?
30 @password = (password || "") if @login
31 @root_url = root_url.blank? ? retrieve_root_url : root_url
32 end
27
33
28 def initialize(url, root_url=nil, login=nil, password=nil)
34 def adapter_name
29 @url = url
35 'Abstract'
30 @login = login if login && !login.empty?
36 end
31 @password = (password || "") if @login
37
32 @root_url = root_url.blank? ? retrieve_root_url : root_url
38 def root_url
33 end
39 @root_url
34
40 end
35 def root_url
41
36 @root_url
42 def url
37 end
43 @url
38
44 end
39 def url
45
40 @url
46 # get info about the svn repository
41 end
47 def info
42
48 return nil
43 # get info about the svn repository
49 end
44 def info
50
45 cmd = "svn info --xml #{target('')}"
51 # Returns the entry identified by path and revision identifier
46 cmd << " --username #{@login} --password #{@password}" if @login
52 # or nil if entry doesn't exist in the repository
47 info = nil
53 def entry(path=nil, identifier=nil)
48 shellout(cmd) do |io|
54 e = entries(path, identifier)
49 begin
55 e ? e.first : nil
50 doc = REXML::Document.new(io)
56 end
51 #root_url = doc.elements["info/entry/repository/root"].text
57
52 info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,
58 # Returns an Entries collection
53 :lastrev => Revision.new({
59 # or nil if the given path doesn't exist in the repository
54 :identifier => doc.elements["info/entry/commit"].attributes['revision'],
60 def entries(path=nil, identifier=nil)
55 :time => Time.parse(doc.elements["info/entry/commit/date"].text),
61 return nil
56 :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")
57 })
58 })
59 rescue
60 end
62 end
61 end
62 return nil if $? && $?.exitstatus != 0
63 info
64 rescue Errno::ENOENT => e
65 return nil
66 end
67
68 # Returns the entry identified by path and revision identifier
69 # or nil if entry doesn't exist in the repository
70 def entry(path=nil, identifier=nil)
71 e = entries(path, identifier)
72 e ? e.first : nil
73 end
74
63
75 # Returns an Entries collection
64 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
76 # or nil if the given path doesn't exist in the repository
65 return nil
77 def entries(path=nil, identifier=nil)
66 end
78 path ||= ''
67
79 identifier = 'HEAD' unless identifier and identifier > 0
68 def diff(path, identifier_from, identifier_to=nil, type="inline")
80 entries = Entries.new
69 return nil
81 cmd = "svn list --xml #{target(path)}@#{identifier}"
70 end
82 cmd << " --username #{@login} --password #{@password}" if @login
71
83 shellout(cmd) do |io|
72 def cat(path, identifier=nil)
84 begin
73 return nil
85 doc = REXML::Document.new(io)
86 doc.elements.each("lists/list/entry") do |entry|
87 entries << Entry.new({:name => entry.elements['name'].text,
88 :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
89 :kind => entry.attributes['kind'],
90 :size => (entry.elements['size'] and entry.elements['size'].text).to_i,
91 :lastrev => Revision.new({
92 :identifier => entry.elements['commit'].attributes['revision'],
93 :time => Time.parse(entry.elements['commit'].elements['date'].text),
94 :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")
95 })
96 })
97 end
98 rescue
99 end
74 end
100 end
101 return nil if $? && $?.exitstatus != 0
102 entries.sort_by_name
103 rescue Errno::ENOENT => e
104 raise CommandFailed
105 end
106
75
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
76 def with_leading_slash(path)
108 path ||= ''
77 path ||= ''
109 identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0
78 (path[0,1]!="/") ? "/#{path}" : path
110 identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
79 end
111 revisions = Revisions.new
80
112 cmd = "svn log --xml -r #{identifier_from}:#{identifier_to}"
81 private
113 cmd << " --username #{@login} --password #{@password}" if @login
82 def retrieve_root_url
114 cmd << " --verbose " if options[:with_paths]
83 info = self.info
115 cmd << target(path)
84 info ? info.root_url : nil
116 shellout(cmd) do |io|
85 end
117 begin
86
118 doc = REXML::Document.new(io)
87 def target(path)
119 doc.elements.each("log/logentry") do |logentry|
88 path ||= ""
120 paths = []
89 base = path.match(/^\//) ? root_url : url
121 logentry.elements.each("paths/path") do |path|
90 " \"" << "#{base}/#{path}".gsub(/["?<>\*]/, '') << "\""
122 paths << {:action => path.attributes['action'],
91 end
123 :path => path.text,
124 :from_path => path.attributes['copyfrom-path'],
125 :from_revision => path.attributes['copyfrom-rev']
126 }
127 end
128 paths.sort! { |x,y| x[:path] <=> y[:path] }
129
92
130 revisions << Revision.new({:identifier => logentry.attributes['revision'],
93 def logger
131 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
94 RAILS_DEFAULT_LOGGER
132 :time => Time.parse(logentry.elements['date'].text),
95 end
133 :message => logentry.elements['msg'].text,
96
134 :paths => paths
97 def shellout(cmd, &block)
135 })
98 logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
99 IO.popen(cmd, "r+") do |io|
100 io.close_write
101 block.call(io) if block_given?
136 end
102 end
137 rescue
103 end
104 end
105
106 class Entries < Array
107 def sort_by_name
108 sort {|x,y|
109 if x.kind == y.kind
110 x.name <=> y.name
111 else
112 x.kind <=> y.kind
113 end
114 }
115 end
116
117 def revisions
118 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
138 end
119 end
139 end
120 end
140 return nil if $? && $?.exitstatus != 0
121
141 revisions
122 class Info
142 rescue Errno::ENOENT => e
123 attr_accessor :root_url, :lastrev
143 raise CommandFailed
124 def initialize(attributes={})
144 end
125 self.root_url = attributes[:root_url] if attributes[:root_url]
145
126 self.lastrev = attributes[:lastrev]
146 def diff(path, identifier_from, identifier_to=nil, type="inline")
127 end
147 path ||= ''
148 if identifier_to and identifier_to.to_i > 0
149 identifier_to = identifier_to.to_i
150 else
151 identifier_to = identifier_from.to_i - 1
152 end
128 end
153 cmd = "svn diff -r "
129
154 cmd << "#{identifier_to}:"
130 class Entry
155 cmd << "#{identifier_from}"
131 attr_accessor :name, :path, :kind, :size, :lastrev
156 cmd << "#{target(path)}@#{identifier_from}"
132 def initialize(attributes={})
157 cmd << " --username #{@login} --password #{@password}" if @login
133 self.name = attributes[:name] if attributes[:name]
158 diff = []
134 self.path = attributes[:path] if attributes[:path]
159 shellout(cmd) do |io|
135 self.kind = attributes[:kind] if attributes[:kind]
160 io.each_line do |line|
136 self.size = attributes[:size].to_i if attributes[:size]
161 diff << line
137 self.lastrev = attributes[:lastrev]
138 end
139
140 def is_file?
141 'file' == self.kind
142 end
143
144 def is_dir?
145 'dir' == self.kind
146 end
147
148 def is_text?
149 Redmine::MimeType.is_type?('text', name)
162 end
150 end
163 end
151 end
164 return nil if $? && $?.exitstatus != 0
152
165 DiffTableList.new diff, type
153 class Revisions < Array
166
154 def latest
167 rescue Errno::ENOENT => e
155 sort {|x,y|
168 raise CommandFailed
156 unless x.time.nil? or y.time.nil?
169 end
157 x.time <=> y.time
170
158 else
171 def cat(path, identifier=nil)
159 0
172 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
160 end
173 cmd = "svn cat #{target(path)}@#{identifier}"
161 }.last
174 cmd << " --username #{@login} --password #{@password}" if @login
162 end
175 cat = nil
176 shellout(cmd) do |io|
177 io.binmode
178 cat = io.read
179 end
163 end
180 return nil if $? && $?.exitstatus != 0
164
181 cat
165 class Revision
182 rescue Errno::ENOENT => e
166 attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
183 raise CommandFailed
167 def initialize(attributes={})
184 end
168 self.identifier = attributes[:identifier]
185
169 self.scmid = attributes[:scmid]
186 private
170 self.name = attributes[:name] || self.identifier
187 def retrieve_root_url
171 self.author = attributes[:author]
188 info = self.info
172 self.time = attributes[:time]
189 info ? info.root_url : nil
173 self.message = attributes[:message] || ""
190 end
174 self.paths = attributes[:paths]
175 self.revision = attributes[:revision]
176 self.branch = attributes[:branch]
177 end
191
178
192 def target(path)
179 end
193 path ||= ""
194 base = path.match(/^\//) ? root_url : url
195 " \"" << "#{base}/#{path}".gsub(/["?<>\*]/, '') << "\""
196 end
197
180
198 def logger
181 # A line of Diff
199 RAILS_DEFAULT_LOGGER
182 class Diff
200 end
183 attr_accessor :nb_line_left
184 attr_accessor :line_left
185 attr_accessor :nb_line_right
186 attr_accessor :line_right
187 attr_accessor :type_diff_right
188 attr_accessor :type_diff_left
189
190 def initialize ()
191 self.nb_line_left = ''
192 self.nb_line_right = ''
193 self.line_left = ''
194 self.line_right = ''
195 self.type_diff_right = ''
196 self.type_diff_left = ''
197 end
201
198
202 def shellout(cmd, &block)
199 def inspect
203 logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
200 puts '### Start Line Diff ###'
204 IO.popen(cmd, "r+") do |io|
201 puts self.nb_line_left
205 io.close_write
202 puts self.line_left
206 block.call(io) if block_given?
203 puts self.nb_line_right
204 puts self.line_right
205 end
207 end
206 end
208 end
207
209 end
208 class DiffTableList < Array
210
209 def initialize (diff, type="inline")
211 class Entries < Array
210 diff_table = DiffTable.new type
212 def sort_by_name
211 diff.each do |line|
213 sort {|x,y|
212 if line =~ /^(Index:|diff) (.*)$/
214 if x.kind == y.kind
213 self << diff_table if diff_table.length > 1
215 x.name <=> y.name
214 diff_table = DiffTable.new type
216 else
215 end
217 x.kind <=> y.kind
216 a = diff_table.add_line line
217 end
218 self << diff_table
218 end
219 end
219 }
220 end
220 end
221
221
222 def revisions
222 # Class for create a Diff
223 revisions ||= Revisions.new(collect{|entry| entry.lastrev})
223 class DiffTable < Hash
224 end
224 attr_reader :file_name, :line_num_l, :line_num_r
225 end
226
227 class Info
228 attr_accessor :root_url, :lastrev
229 def initialize(attributes={})
230 self.root_url = attributes[:root_url] if attributes[:root_url]
231 self.lastrev = attributes[:lastrev]
232 end
233 end
234
235 class Entry
236 attr_accessor :name, :path, :kind, :size, :lastrev
237 def initialize(attributes={})
238 self.name = attributes[:name] if attributes[:name]
239 self.path = attributes[:path] if attributes[:path]
240 self.kind = attributes[:kind] if attributes[:kind]
241 self.size = attributes[:size].to_i if attributes[:size]
242 self.lastrev = attributes[:lastrev]
243 end
244
225
245 def is_file?
226 # Initialize with a Diff file and the type of Diff View
246 'file' == self.kind
227 # The type view must be inline or sbs (side_by_side)
247 end
228 def initialize (type="inline")
229 @parsing = false
230 @nb_line = 1
231 @start = false
232 @before = 'same'
233 @second = true
234 @type = type
235 end
248
236
249 def is_dir?
237 # Function for add a line of this Diff
250 'dir' == self.kind
238 def add_line(line)
251 end
239 unless @parsing
240 if line =~ /^(Index:|diff) (.*)$/
241 @file_name = $2
242 return false
243 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
244 @line_num_l = $5.to_i
245 @line_num_r = $2.to_i
246 @parsing = true
247 end
248 else
249 if line =~ /^[^\+\-\s@\\]/
250 self.delete(self.keys.sort.last)
251 @parsing = false
252 return false
253 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
254 @line_num_l = $5.to_i
255 @line_num_r = $2.to_i
256 else
257 @nb_line += 1 if parse_line(line, @type)
258 end
259 end
260 return true
261 end
252
262
253 def is_text?
263 def inspect
254 Redmine::MimeType.is_type?('text', name)
264 puts '### DIFF TABLE ###'
255 end
265 puts "file : #{file_name}"
256 end
266 self.each do |d|
257
267 d.inspect
258 class Revisions < Array
268 end
259 def latest
269 end
260 sort {|x,y| x.time <=> y.time}.last
261 end
262 end
263
264 class Revision
265 attr_accessor :identifier, :author, :time, :message, :paths
266 def initialize(attributes={})
267 self.identifier = attributes[:identifier]
268 self.author = attributes[:author]
269 self.time = attributes[:time]
270 self.message = attributes[:message] || ""
271 self.paths = attributes[:paths]
272 end
273
274 end
275
276 # A line of Diff
277 class Diff
278
279 attr_accessor :nb_line_left
280 attr_accessor :line_left
281 attr_accessor :nb_line_right
282 attr_accessor :line_right
283 attr_accessor :type_diff_right
284 attr_accessor :type_diff_left
285
270
286 def initialize ()
271 private
287 self.nb_line_left = ''
272 # Test if is a Side By Side type
288 self.nb_line_right = ''
273 def sbs?(type, func)
289 self.line_left = ''
274 if @start and type == "sbs"
290 self.line_right = ''
275 if @before == func and @second
291 self.type_diff_right = ''
276 tmp_nb_line = @nb_line
292 self.type_diff_left = ''
277 self[tmp_nb_line] = Diff.new
293 end
278 else
294
279 @second = false
295 def inspect
280 tmp_nb_line = @start
296 puts '### Start Line Diff ###'
281 @start += 1
297 puts self.nb_line_left
282 @nb_line -= 1
298 puts self.line_left
299 puts self.nb_line_right
300 puts self.line_right
301 end
302 end
303
304 class DiffTableList < Array
305
306 def initialize (diff, type="inline")
307 diff_table = DiffTable.new type
308 diff.each do |line|
309 if line =~ /^Index: (.*)$/
310 self << diff_table if diff_table.length > 1
311 diff_table = DiffTable.new type
312 end
283 end
313 a = diff_table.add_line line
284 else
314 end
285 tmp_nb_line = @nb_line
315 self << diff_table
286 @start = @nb_line
316 end
287 self[tmp_nb_line] = Diff.new
317 end
288 @second = true
318
289 end
319 # Class for create a Diff
290 unless self[tmp_nb_line]
320 class DiffTable < Hash
291 @nb_line += 1
321
292 self[tmp_nb_line] = Diff.new
322 attr_reader :file_name, :line_num_l, :line_num_r
293 else
323
294 self[tmp_nb_line]
324 # Initialize with a Diff file and the type of Diff View
295 end
325 # The type view must be inline or sbs (side_by_side)
326 def initialize (type="inline")
327 @parsing = false
328 @nb_line = 1
329 @start = false
330 @before = 'same'
331 @second = true
332 @type = type
333 end
334
335 # Function for add a line of this Diff
336 def add_line(line)
337 unless @parsing
338 if line =~ /^Index: (.*)$/
339 @file_name = $1
340 return false
341 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
342 @line_num_l = $2.to_i
343 @line_num_r = $5.to_i
344 @parsing = true
345 end
296 end
346 else
297
347 if line =~ /^_+$/
298 # Escape the HTML for the diff
348 self.delete(self.keys.sort.last)
299 def escapeHTML(line)
349 @parsing = false
300 CGI.escapeHTML(line).gsub(/\s/, '&nbsp;')
350 return false
351 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
352 @line_num_l = $2.to_i
353 @line_num_r = $5.to_i
354 else
355 @nb_line += 1 if parse_line(line, @type)
356 end
301 end
357 end
302
358 return true
303 def parse_line (line, type="inline")
359 end
304 if line[0, 1] == "+"
360
305 diff = sbs? type, 'add'
361 def inspect
306 @before = 'add'
362 puts '### DIFF TABLE ###'
307 diff.line_left = escapeHTML line[1..-1]
363 puts "file : #{file_name}"
308 diff.nb_line_left = @line_num_l
364 self.each do |d|
309 diff.type_diff_left = 'diff_in'
365 d.inspect
310 @line_num_l += 1
366 end
311 true
367 end
312 elsif line[0, 1] == "-"
368
313 diff = sbs? type, 'remove'
369 private
314 @before = 'remove'
370
315 diff.line_right = escapeHTML line[1..-1]
371 # Test if is a Side By Side type
316 diff.nb_line_right = @line_num_r
372 def sbs?(type, func)
317 diff.type_diff_right = 'diff_out'
373 if @start and type == "sbs"
318 @line_num_r += 1
374 if @before == func and @second
319 true
375 tmp_nb_line = @nb_line
320 elsif line[0, 1] =~ /\s/
376 self[tmp_nb_line] = Diff.new
321 @before = 'same'
377 else
322 @start = false
378 @second = false
323 diff = Diff.new
379 tmp_nb_line = @start
324 diff.line_right = escapeHTML line[1..-1]
380 @start += 1
325 diff.nb_line_right = @line_num_r
381 @nb_line -= 1
326 diff.line_left = escapeHTML line[1..-1]
327 diff.nb_line_left = @line_num_l
328 self[@nb_line] = diff
329 @line_num_l += 1
330 @line_num_r += 1
331 true
332 elsif line[0, 1] = "\\"
333 true
334 else
335 false
336 end
382 end
337 end
383 else
384 tmp_nb_line = @nb_line
385 @start = @nb_line
386 self[tmp_nb_line] = Diff.new
387 @second = true
388 end
338 end
389 unless self[tmp_nb_line]
390 @nb_line += 1
391 self[tmp_nb_line] = Diff.new
392 else
393 self[tmp_nb_line]
394 end
395 end
396
397 # Escape the HTML for the diff
398 def escapeHTML(line)
399 CGI.escapeHTML(line).gsub(/\s/, '&nbsp;')
400 end
401
402 def parse_line (line, type="inline")
403 if line[0, 1] == "+"
404 diff = sbs? type, 'add'
405 @before = 'add'
406 diff.line_left = escapeHTML line[1..-1]
407 diff.nb_line_left = @line_num_l
408 diff.type_diff_left = 'diff_in'
409 @line_num_l += 1
410 true
411 elsif line[0, 1] == "-"
412 diff = sbs? type, 'remove'
413 @before = 'remove'
414 diff.line_right = escapeHTML line[1..-1]
415 diff.nb_line_right = @line_num_r
416 diff.type_diff_right = 'diff_out'
417 @line_num_r += 1
418 true
419 elsif line[0, 1] =~ /\s/
420 @before = 'same'
421 @start = false
422 diff = Diff.new
423 diff.line_right = escapeHTML line[1..-1]
424 diff.nb_line_right = @line_num_r
425 diff.line_left = escapeHTML line[1..-1]
426 diff.nb_line_left = @line_num_l
427 self[@nb_line] = diff
428 @line_num_l += 1
429 @line_num_r += 1
430 true
431 else
432 false
433 end
434 end
339 end
435 end
340 end
436 end No newline at end of file
341 end
@@ -25,7 +25,7 class RepositoryTest < Test::Unit::TestCase
25 end
25 end
26
26
27 def test_create
27 def test_create
28 repository = Repository.new(:project => Project.find(2))
28 repository = Repository::Subversion.new(:project => Project.find(2))
29 assert !repository.save
29 assert !repository.save
30
30
31 repository.url = "svn://localhost"
31 repository.url = "svn://localhost"
@@ -34,12 +34,6 class RepositoryTest < Test::Unit::TestCase
34
34
35 project = Project.find(2)
35 project = Project.find(2)
36 assert_equal repository, project.repository
36 assert_equal repository, project.repository
37 end
38
39 def test_cant_change_url
40 url = @repository.url
41 @repository.url = "svn://anotherhost"
42 assert_equal url, @repository.url
43 end
37 end
44
38
45 def test_scan_changesets_for_issue_ids
39 def test_scan_changesets_for_issue_ids
@@ -59,12 +53,4 class RepositoryTest < Test::Unit::TestCase
59 # ignoring commits referencing an issue of another project
53 # ignoring commits referencing an issue of another project
60 assert_equal [], Issue.find(4).changesets
54 assert_equal [], Issue.find(4).changesets
61 end
55 end
62
63 def test_changesets_with_path
64 @repository.changesets_with_path '/some/path' do
65 assert_equal 1, @repository.changesets.count(:select => "DISTINCT #{Changeset.table_name}.id")
66 changesets = @repository.changesets.find(:all)
67 assert_equal 1, changesets.size
68 end
69 end
70 end
56 end
General Comments 0
You need to be logged in to leave comments. Login now